From 03d44f772d11220fb3efc105bd1f1a6a59d809e1 Mon Sep 17 00:00:00 2001 From: infinite-persistence Date: Fri, 10 Sep 2021 16:21:39 +0800 Subject: [PATCH] Add search bar to BlockList 6834 - Only supports channel-name search, per 6834. Channel-title search would probably be too heavy on the client side. - Fuzzy search is possible, but is too slow on huge lists. Ended up with a simpler `matchSorter.rankings.CONTAINS`, which I think would cover typical cases. --- package.json | 1 + ui/component/blockList/view.jsx | 106 +++++++++++++++++++++++++++++++- yarn.lock | 13 ++++ 3 files changed, 118 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index f748de73d..2237e2b60 100644 --- a/package.json +++ b/package.json @@ -56,6 +56,7 @@ "express": "^4.17.1", "humanize-duration": "^3.27.0", "if-env": "^1.0.4", + "match-sorter": "^6.3.0", "parse-duration": "^1.0.0", "react-datetime-picker": "^3.2.1", "react-plastic": "^1.1.1", diff --git a/ui/component/blockList/view.jsx b/ui/component/blockList/view.jsx index 8f692827a..3df4fb6db 100644 --- a/ui/component/blockList/view.jsx +++ b/ui/component/blockList/view.jsx @@ -1,13 +1,34 @@ // @flow +import { Combobox, ComboboxInput, ComboboxPopover, ComboboxList, ComboboxOption } from '@reach/combobox'; +// import '@reach/combobox/styles.css'; --> 'scss/third-party.scss' +import { matchSorter } from 'match-sorter'; import React from 'react'; import classnames from 'classnames'; import Button from 'component/button'; import ClaimList from 'component/claimList'; +import Icon from 'component/common/icon'; import Paginate from 'component/common/paginate'; import Yrbl from 'component/yrbl'; +import * as ICONS from 'constants/icons'; +import useThrottle from 'effects/use-throttle'; const PAGE_SIZE = 10; +function reduceUriToChannelName(uri: string) { + // 'parseURI' is too slow to handle a large list. Since our list should be + // kosher in the first place, just do a quick substring call. Add a + // try-catch just in case. + try { + return uri.substring(uri.indexOf('@') + 1, uri.indexOf('#')); + } catch { + return uri; + } +} + +// **************************************************************************** +// BlockList +// **************************************************************************** + type Props = { uris: Array, help: string, @@ -26,6 +47,7 @@ export default function BlockList(props: Props) { const hasLocalList = localList && localList.length > 0; const justBlocked = list && localList && localList.length < list.length; + const [searchList, setSearchList] = React.useState(null); // null: not searching; []: no results; const [page, setPage] = React.useState(1); let totalPages = 0; @@ -45,6 +67,14 @@ export default function BlockList(props: Props) { return undefined; } + function formatSearchSuggestion(suggestion: string) { + return reduceUriToChannelName(suggestion); + } + + function filterSearchResults(results: ?Array) { + setSearchList(results); + } + // ************************************************************************** // ************************************************************************** @@ -87,9 +117,17 @@ export default function BlockList(props: Props) { return ( <>
{help}
-
+
+ +
+
); } + +// **************************************************************************** +// SearchList +// **************************************************************************** + +type LsbProps = { + list: ?Array, + placeholder?: string, + formatter?: (suggestion: string) => string, + onResultsUpdated?: (?Array) => void, +}; + +function SearchList(props: LsbProps) { + const { list, placeholder, formatter, onResultsUpdated } = props; + const [term, setTerm] = React.useState(''); + const results = useAuthorMatch(term, list); + const handleChange = (event) => setTerm(event.target.value); + const handleSelect = (e) => setTerm(e); + + React.useEffect(() => { + if (onResultsUpdated) { + onResultsUpdated(results); + } + }, [results]); // eslint-disable-line react-hooks/exhaustive-deps + + return ( +
+ + + + + {results && ( + + {results.length > 0 ? ( + + {results.slice(0, 10).map((result, index) => ( + + ))} + + ) : ( + {__('No results found')} + )} + + )} + +
+ ); +} + +function useAuthorMatch(term, list) { + const throttledTerm = useThrottle(term, 200); + return React.useMemo(() => { + return !throttledTerm || throttledTerm.trim() === '' + ? null + : matchSorter(list, throttledTerm, { + keys: [(item) => reduceUriToChannelName(item)], + threshold: matchSorter.rankings.CONTAINS, + }); + }, [throttledTerm]); // eslint-disable-line react-hooks/exhaustive-deps +} diff --git a/yarn.lock b/yarn.lock index 407ee8e65..e656a4af5 100644 --- a/yarn.lock +++ b/yarn.lock @@ -10707,6 +10707,14 @@ marked@^1.2.3: resolved "https://registry.yarnpkg.com/marked/-/marked-1.2.8.tgz#5008ece15cfa43e653e85845f3525af4beb6bdd4" integrity sha512-lzmFjGnzWHkmbk85q/ILZjFoHHJIQGF+SxGEfIdGk/XhiTPhqGs37gbru6Kkd48diJnEyYwnG67nru0Z2gQtuQ== +match-sorter@^6.3.0: + version "6.3.0" + resolved "https://registry.yarnpkg.com/match-sorter/-/match-sorter-6.3.0.tgz#454a1b31ed218cddbce6231a0ecb5fdc549fed01" + integrity sha512-efYOf/wUpNb8FgNY+cOD2EIJI1S5I7YPKsw0LBp7wqPh5pmMS6i/wr3ZWwfwrAw1NvqTA2KUReVRWDX84lUcOQ== + dependencies: + "@babel/runtime" "^7.12.5" + remove-accents "0.4.2" + matcher@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/matcher/-/matcher-3.0.0.tgz#bd9060f4c5b70aa8041ccc6f80368760994f30ca" @@ -14047,6 +14055,11 @@ remark@^9.0.0: remark-stringify "^5.0.0" unified "^6.0.0" +remove-accents@0.4.2: + version "0.4.2" + resolved "https://registry.yarnpkg.com/remove-accents/-/remove-accents-0.4.2.tgz#0a43d3aaae1e80db919e07ae254b285d9e1c7bb5" + integrity sha1-CkPTqq4egNuRngeuJUsoXZ4ce7U= + remove-markdown@^0.3.0: version "0.3.0" resolved "https://registry.yarnpkg.com/remove-markdown/-/remove-markdown-0.3.0.tgz#5e4b667493a93579728f3d52ecc1db9ca505dc98"