diff --git a/common/util/parse.ts b/common/util/parse.ts index 94b5ab7f..30dcb952 100644 --- a/common/util/parse.ts +++ b/common/util/parse.ts @@ -49,6 +49,16 @@ export function parseWordsAsTags(text: string) { return parseTags(taggedText) } +// TODO: fuzzy matching +export const wordIn = (word: string, corpus: string) => + corpus.toLocaleLowerCase().includes(word.toLocaleLowerCase()) + +const checkAgainstQuery = (query: string, corpus: string) => + query.split(' ').every((word) => wordIn(word, corpus)) + +export const searchInAny = (query: string, ...fields: string[]) => + fields.some((field) => checkAgainstQuery(query, field)) + // can't just do [StarterKit, Image...] because it doesn't work with cjs imports export const exhibitExts = [ Blockquote, diff --git a/web/components/filter-select-users.tsx b/web/components/filter-select-users.tsx index 7ce73cf8..a19ab6af 100644 --- a/web/components/filter-select-users.tsx +++ b/web/components/filter-select-users.tsx @@ -7,6 +7,7 @@ import { Menu, Transition } from '@headlessui/react' import { Avatar } from 'web/components/avatar' import { Row } from 'web/components/layout/row' import { UserLink } from 'web/components/user-page' +import { searchInAny } from 'common/util/parse' export function FilterSelectUsers(props: { setSelectedUsers: (users: User[]) => void @@ -35,8 +36,7 @@ export function FilterSelectUsers(props: { return ( !selectedUsers.map((user) => user.name).includes(user.name) && !ignoreUserIds.includes(user.id) && - (user.name.toLowerCase().includes(query.toLowerCase()) || - user.username.toLowerCase().includes(query.toLowerCase())) + searchInAny(query, user.name, user.username) ) }) ) diff --git a/web/components/groups/group-selector.tsx b/web/components/groups/group-selector.tsx index ea1597f2..2417403a 100644 --- a/web/components/groups/group-selector.tsx +++ b/web/components/groups/group-selector.tsx @@ -11,6 +11,7 @@ import { CreateGroupButton } from 'web/components/groups/create-group-button' import { useState } from 'react' import { useMemberGroups } from 'web/hooks/use-group' import { User } from 'common/user' +import { searchInAny } from 'common/util/parse' export function GroupSelector(props: { selectedGroup?: Group @@ -22,14 +23,10 @@ export function GroupSelector(props: { const [isCreatingNewGroup, setIsCreatingNewGroup] = useState(false) const [query, setQuery] = useState('') - const memberGroups = useMemberGroups(creator?.id) - const filteredGroups = memberGroups - ? query === '' - ? memberGroups - : memberGroups.filter((group) => { - return group.name.toLowerCase().includes(query.toLowerCase()) - }) - : [] + const memberGroups = useMemberGroups(creator?.id) ?? [] + const filteredGroups = memberGroups.filter((group) => + searchInAny(query, group.name) + ) if (!showSelector || !creator) { return ( diff --git a/web/hooks/use-sort-and-query-params.tsx b/web/hooks/use-sort-and-query-params.tsx index a2590249..a2248c2e 100644 --- a/web/hooks/use-sort-and-query-params.tsx +++ b/web/hooks/use-sort-and-query-params.tsx @@ -16,11 +16,6 @@ export type Sort = | 'resolve-date' | 'last-updated' -export function checkAgainstQuery(query: string, corpus: string) { - const queryWords = query.toLowerCase().split(' ') - return queryWords.every((word) => corpus.toLowerCase().includes(word)) -} - export function getSavedSort() { // TODO: this obviously doesn't work with SSR, common sense would suggest // that we should save things like this in cookies so the server has them diff --git a/web/pages/charity/index.tsx b/web/pages/charity/index.tsx index 46201c3d..92e6b69f 100644 --- a/web/pages/charity/index.tsx +++ b/web/pages/charity/index.tsx @@ -20,6 +20,7 @@ import { manaToUSD } from 'common/util/format' import { quadraticMatches } from 'common/quadratic-funding' import { Txn } from 'common/txn' import { useTracking } from 'web/hooks/use-tracking' +import { searchInAny } from 'common/util/parse' export async function getStaticProps() { const txns = await getAllCharityTxns() @@ -88,10 +89,12 @@ export default function Charity(props: { () => charities.filter( (charity) => - charity.name.toLowerCase().includes(query.toLowerCase()) || - charity.preview.toLowerCase().includes(query.toLowerCase()) || - charity.description.toLowerCase().includes(query.toLowerCase()) || - (charity.tags as string[])?.includes(query.toLowerCase()) + searchInAny( + query, + charity.name, + charity.preview, + charity.description + ) || (charity.tags as string[])?.includes(query.toLowerCase()) ), [charities, query] ) diff --git a/web/pages/contract-search-firestore.tsx b/web/pages/contract-search-firestore.tsx index 3ac11993..0ef8cdfe 100644 --- a/web/pages/contract-search-firestore.tsx +++ b/web/pages/contract-search-firestore.tsx @@ -1,4 +1,5 @@ import { Answer } from 'common/answer' +import { searchInAny } from 'common/util/parse' import { sortBy } from 'lodash' import { useState } from 'react' import { ContractsGrid } from 'web/components/contract/contracts-list' @@ -28,22 +29,14 @@ export default function ContractSearchFirestore(props: { const [sort, setSort] = useState(initialSort || 'newest') const [query, setQuery] = useState(initialQuery) - const queryWords = query.toLowerCase().split(' ') - function check(corpus: string) { - return queryWords.every((word) => corpus.toLowerCase().includes(word)) - } - - let matches = (contracts ?? []).filter( - (c) => - check(c.question) || - check(c.creatorName) || - check(c.creatorUsername) || - check(c.lowercaseTags.map((tag) => `#${tag}`).join(' ')) || - check( - ((c as any).answers ?? []) - .map((answer: Answer) => answer.text) - .join(' ') - ) + let matches = (contracts ?? []).filter((c) => + searchInAny( + query, + c.question, + c.creatorName, + c.lowercaseTags.map((tag) => `#${tag}`).join(' '), + ((c as any).answers ?? []).map((answer: Answer) => answer.text).join(' ') + ) ) if (sort === 'newest') { diff --git a/web/pages/group/[...slugs]/index.tsx b/web/pages/group/[...slugs]/index.tsx index 9620894f..5fd564ea 100644 --- a/web/pages/group/[...slugs]/index.tsx +++ b/web/pages/group/[...slugs]/index.tsx @@ -40,10 +40,7 @@ import React, { useEffect, useState } from 'react' import { GroupChat } from 'web/components/groups/group-chat' import { LoadingIndicator } from 'web/components/loading-indicator' import { Modal } from 'web/components/layout/modal' -import { - checkAgainstQuery, - getSavedSort, -} from 'web/hooks/use-sort-and-query-params' +import { getSavedSort } from 'web/hooks/use-sort-and-query-params' import { ChoicesToggleGroup } from 'web/components/choices-toggle-group' import { toast } from 'react-hot-toast' import { useCommentsOnGroup } from 'web/hooks/use-comments' @@ -56,6 +53,7 @@ import { SearchIcon } from '@heroicons/react/outline' import { useTipTxns } from 'web/hooks/use-tip-txns' import { JoinOrLeaveGroupButton } from 'web/components/groups/groups-button' import { OnlineUserList } from 'web/components/online-user-list' +import { searchInAny } from 'common/util/parse' export const getStaticProps = fromPropz(getStaticPropz) export async function getStaticPropz(props: { params: { slugs: string[] } }) { @@ -446,9 +444,8 @@ function GroupMemberSearch(props: { members: User[]; group: Group }) { } // TODO use find-active-contracts to sort by? - const matches = sortBy(members, [(member) => member.name]).filter( - (m) => - checkAgainstQuery(query, m.name) || checkAgainstQuery(query, m.username) + const matches = sortBy(members, [(member) => member.name]).filter((m) => + searchInAny(query, m.name, m.username) ) const matchLimit = 25 diff --git a/web/pages/groups.tsx b/web/pages/groups.tsx index 87ac1501..9e21c346 100644 --- a/web/pages/groups.tsx +++ b/web/pages/groups.tsx @@ -12,12 +12,12 @@ import { useUser } from 'web/hooks/use-user' import { groupPath, listAllGroups } from 'web/lib/firebase/groups' import { getUser, User } from 'web/lib/firebase/users' import { Tabs } from 'web/components/layout/tabs' -import { checkAgainstQuery } from 'web/hooks/use-sort-and-query-params' import { SiteLink } from 'web/components/site-link' import clsx from 'clsx' import { Avatar } from 'web/components/avatar' import { JoinOrLeaveGroupButton } from 'web/components/groups/groups-button' import { UserLink } from 'web/components/user-page' +import { searchInAny } from 'common/util/parse' export async function getStaticProps() { const groups = await listAllGroups().catch((_) => []) @@ -71,11 +71,13 @@ export default function Groups(props: { const matches = sortBy(groups, [ (group) => -1 * group.contractIds.length, (group) => -1 * group.memberIds.length, - ]).filter( - (g) => - checkAgainstQuery(query, g.name) || - checkAgainstQuery(query, g.about || '') || - checkAgainstQuery(query, creatorsDict[g.creatorId].username) + ]).filter((g) => + searchInAny( + query, + g.name, + g.about || '', + creatorsDict[g.creatorId].username + ) ) const matchesOrderedByRecentActivity = sortBy(groups, [ @@ -84,11 +86,13 @@ export default function Groups(props: { (group.mostRecentChatActivityTime ?? group.mostRecentContractAddedTime ?? group.mostRecentActivityTime), - ]).filter( - (g) => - checkAgainstQuery(query, g.name) || - checkAgainstQuery(query, g.about || '') || - checkAgainstQuery(query, creatorsDict[g.creatorId].username) + ]).filter((g) => + searchInAny( + query, + g.name, + g.about || '', + creatorsDict[g.creatorId].username + ) ) // Not strictly necessary, but makes the "hold delete" experience less laggy