return (
<EmojiGroup key={id} id={id} label={label}>
{tab === EmojiBoardTab.Emoji
- ? searchResult
- .sort((a, b) => a.shortcode.localeCompare(b.shortcode))
- .map((emoji) =>
- 'unicode' in emoji ? (
- <EmojiItem
- key={emoji.unicode}
- label={emoji.label}
- type={EmojiType.Emoji}
- data={emoji.unicode}
- shortcode={emoji.shortcode}
- >
- {emoji.unicode}
- </EmojiItem>
- ) : (
- <EmojiItem
- key={emoji.shortcode}
- label={emoji.body || emoji.shortcode}
- type={EmojiType.CustomEmoji}
- data={emoji.url}
- shortcode={emoji.shortcode}
- >
- <img
- loading="lazy"
- className={css.CustomEmojiImg}
- alt={emoji.body || emoji.shortcode}
- src={mxcUrlToHttp(mx, emoji.url, useAuthentication) ?? emoji.url}
- />
- </EmojiItem>
- )
+ ? searchResult.map((emoji) =>
+ 'unicode' in emoji ? (
+ <EmojiItem
+ key={emoji.unicode}
+ label={emoji.label}
+ type={EmojiType.Emoji}
+ data={emoji.unicode}
+ shortcode={emoji.shortcode}
+ >
+ {emoji.unicode}
+ </EmojiItem>
+ ) : (
+ <EmojiItem
+ key={emoji.shortcode}
+ label={emoji.body || emoji.shortcode}
+ type={EmojiType.CustomEmoji}
+ data={emoji.url}
+ shortcode={emoji.shortcode}
+ >
+ <img
+ loading="lazy"
+ className={css.CustomEmojiImg}
+ alt={emoji.body || emoji.shortcode}
+ src={mxcUrlToHttp(mx, emoji.url, useAuthentication) ?? emoji.url}
+ />
+ </EmojiItem>
)
+ )
: searchResult.map((emoji) =>
'unicode' in emoji ? null : (
<StickerItem
export type SearchResetHandler = () => void;
+const performMatch = (
+ target: string | string[],
+ query: string,
+ options?: UseAsyncSearchOptions
+): string | undefined => {
+ if (Array.isArray(target)) {
+ const matchTarget = target.find((i) =>
+ matchQuery(normalize(i, options?.normalizeOptions), query, options?.matchOptions)
+ );
+ return matchTarget ? normalize(matchTarget, options?.normalizeOptions) : undefined;
+ }
+
+ const normalizedTargetStr = normalize(target, options?.normalizeOptions);
+ const matches = matchQuery(normalizedTargetStr, query, options?.matchOptions);
+ return matches ? normalizedTargetStr : undefined;
+};
+
+export const orderSearchItems = <TSearchItem extends object | string | number>(
+ query: string,
+ items: TSearchItem[],
+ getItemStr: SearchItemStrGetter<TSearchItem>,
+ options?: UseAsyncSearchOptions
+): TSearchItem[] => {
+ const orderedItems: TSearchItem[] = Array.from(items);
+
+ // we will consider "_" as word boundary char.
+ // because in more use-cases it is used. (like: emojishortcode)
+ const boundaryRegex = new RegExp(`(\\b|_)${query}`);
+ const perfectBoundaryRegex = new RegExp(`(\\b|_)${query}(\\b|_)`);
+
+ orderedItems.sort((i1, i2) => {
+ const str1 = performMatch(getItemStr(i1, query), query, options);
+ const str2 = performMatch(getItemStr(i2, query), query, options);
+
+ if (str1 === undefined && str2 === undefined) return 0;
+ if (str1 === undefined) return 1;
+ if (str2 === undefined) return -1;
+
+ let points1 = 0;
+ let points2 = 0;
+
+ // short string should score more
+ const pointsToSmallStr = (points: number) => {
+ if (str1.length < str2.length) points1 += points;
+ else if (str2.length < str1.length) points2 += points;
+ };
+ pointsToSmallStr(1);
+
+ // closes query match should score more
+ const indexIn1 = str1.indexOf(query);
+ const indexIn2 = str2.indexOf(query);
+ if (indexIn1 < indexIn2) points1 += 2;
+ else if (indexIn2 < indexIn1) points2 += 2;
+ else pointsToSmallStr(2);
+
+ // query match word start on boundary should score more
+ const boundaryIn1 = str1.match(boundaryRegex);
+ const boundaryIn2 = str2.match(boundaryRegex);
+ if (boundaryIn1 && boundaryIn2) pointsToSmallStr(4);
+ else if (boundaryIn1) points1 += 4;
+ else if (boundaryIn2) points2 += 4;
+
+ // query match word start and end on boundary should score more
+ const perfectBoundaryIn1 = str1.match(perfectBoundaryRegex);
+ const perfectBoundaryIn2 = str2.match(perfectBoundaryRegex);
+ if (perfectBoundaryIn1 && perfectBoundaryIn2) pointsToSmallStr(8);
+ else if (perfectBoundaryIn1) points1 += 8;
+ else if (perfectBoundaryIn2) points2 += 8;
+
+ return points2 - points1;
+ });
+
+ return orderedItems;
+};
+
export const useAsyncSearch = <TSearchItem extends object | string | number>(
list: TSearchItem[],
getItemStr: SearchItemStrGetter<TSearchItem>,
const handleMatch: MatchHandler<TSearchItem> = (item, query) => {
const itemStr = getItemStr(item, query);
- if (Array.isArray(itemStr))
- return !!itemStr.find((i) =>
- matchQuery(normalize(i, options?.normalizeOptions), query, options?.matchOptions)
- );
- return matchQuery(
- normalize(itemStr, options?.normalizeOptions),
- query,
- options?.matchOptions
- );
+
+ const strWithMatch = performMatch(itemStr, query, options);
+ return typeof strWithMatch === 'string';
};
const handleResult: ResultHandler<TSearchItem> = (results, query) =>
setResult({
query,
- items: [...results],
+ items: orderSearchItems(query, results, getItemStr, options),
});
return AsyncSearch(list, handleMatch, handleResult, options);