mirror of
https://github.com/MrUnknownDE/VRCX.git
synced 2026-04-22 16:23:50 +02:00
feat: add quick search
This commit is contained in:
352
src/shared/utils/__tests__/globalSearchUtils.test.js
Normal file
352
src/shared/utils/__tests__/globalSearchUtils.test.js
Normal file
@@ -0,0 +1,352 @@
|
||||
import {
|
||||
isPrefixMatch,
|
||||
matchName,
|
||||
searchAvatars,
|
||||
searchFavoriteAvatars,
|
||||
searchFavoriteWorlds,
|
||||
searchFriends,
|
||||
searchGroups,
|
||||
searchWorlds
|
||||
} from '../globalSearchUtils';
|
||||
|
||||
const comparer = new Intl.Collator(undefined, {
|
||||
usage: 'search',
|
||||
sensitivity: 'base'
|
||||
});
|
||||
|
||||
// ── matchName ──────────────────────────────────────────────
|
||||
|
||||
describe('matchName', () => {
|
||||
test('matches substring', () => {
|
||||
expect(matchName('HelloWorld', 'llo', comparer)).toBe(true);
|
||||
});
|
||||
|
||||
test('case-insensitive', () => {
|
||||
expect(matchName('Alice', 'alice', comparer)).toBe(true);
|
||||
});
|
||||
|
||||
test('strips whitespace from query', () => {
|
||||
expect(matchName('Alice', 'al ice', comparer)).toBe(true);
|
||||
});
|
||||
|
||||
test('returns false for empty inputs', () => {
|
||||
expect(matchName('', 'query', comparer)).toBe(false);
|
||||
expect(matchName('name', '', comparer)).toBe(false);
|
||||
expect(matchName(null, 'query', comparer)).toBe(false);
|
||||
});
|
||||
|
||||
test('returns false for whitespace-only query', () => {
|
||||
expect(matchName('Alice', ' ', comparer)).toBe(false);
|
||||
});
|
||||
|
||||
test('no match', () => {
|
||||
expect(matchName('Alice', 'bob', comparer)).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
// ── isPrefixMatch ──────────────────────────────────────────
|
||||
|
||||
describe('isPrefixMatch', () => {
|
||||
test('detects prefix', () => {
|
||||
expect(isPrefixMatch('Alice', 'ali', comparer)).toBe(true);
|
||||
});
|
||||
|
||||
test('rejects non-prefix substring', () => {
|
||||
expect(isPrefixMatch('Alice', 'ice', comparer)).toBe(false);
|
||||
});
|
||||
|
||||
test('returns false for empty inputs', () => {
|
||||
expect(isPrefixMatch('', 'a', comparer)).toBe(false);
|
||||
expect(isPrefixMatch('Alice', '', comparer)).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
// ── searchFriends ──────────────────────────────────────────
|
||||
|
||||
describe('searchFriends', () => {
|
||||
/**
|
||||
*
|
||||
* @param id
|
||||
* @param name
|
||||
* @param memo
|
||||
* @param note
|
||||
*/
|
||||
function makeFriend(id, name, memo = '', note = '') {
|
||||
return [
|
||||
id,
|
||||
{
|
||||
id,
|
||||
name,
|
||||
memo,
|
||||
ref: {
|
||||
currentAvatarThumbnailImageUrl: `img_${id}`,
|
||||
note,
|
||||
$userColour: '#fff'
|
||||
}
|
||||
}
|
||||
];
|
||||
}
|
||||
|
||||
const friends = new Map([
|
||||
makeFriend('u1', 'Alice'),
|
||||
makeFriend('u2', 'Bob', '同事', ''),
|
||||
makeFriend('u3', 'Charlie', '', 'roommate'),
|
||||
makeFriend('u4', 'Dave')
|
||||
]);
|
||||
|
||||
test('matches by name', () => {
|
||||
const results = searchFriends('alice', friends, comparer);
|
||||
expect(results).toHaveLength(1);
|
||||
expect(results[0].id).toBe('u1');
|
||||
expect(results[0].matchedField).toBe('name');
|
||||
});
|
||||
|
||||
test('matches by memo', () => {
|
||||
const results = searchFriends('同事', friends, comparer);
|
||||
expect(results).toHaveLength(1);
|
||||
expect(results[0].id).toBe('u2');
|
||||
expect(results[0].matchedField).toBe('memo');
|
||||
expect(results[0].memo).toBe('同事');
|
||||
});
|
||||
|
||||
test('matches by note', () => {
|
||||
const results = searchFriends('roommate', friends, comparer);
|
||||
expect(results).toHaveLength(1);
|
||||
expect(results[0].id).toBe('u3');
|
||||
expect(results[0].matchedField).toBe('note');
|
||||
expect(results[0].note).toBe('roommate');
|
||||
});
|
||||
|
||||
test('returns empty for short / empty query', () => {
|
||||
expect(searchFriends('', friends, comparer)).toEqual([]);
|
||||
expect(searchFriends(null, friends, comparer)).toEqual([]);
|
||||
});
|
||||
|
||||
test('respects limit', () => {
|
||||
const many = new Map(
|
||||
Array.from({ length: 20 }, (_, i) =>
|
||||
makeFriend(`u${i}`, `Test${i}`)
|
||||
)
|
||||
);
|
||||
expect(searchFriends('Test', many, comparer, 5)).toHaveLength(5);
|
||||
});
|
||||
|
||||
test('prefix matches sort first', () => {
|
||||
const f = new Map([
|
||||
makeFriend('u1', 'XAliceX'),
|
||||
makeFriend('u2', 'Alice')
|
||||
]);
|
||||
const results = searchFriends('Alice', f, comparer);
|
||||
expect(results[0].id).toBe('u2'); // prefix match first
|
||||
});
|
||||
|
||||
test('skips entries without ref', () => {
|
||||
const broken = new Map([['u1', { id: 'u1', name: 'Test' }]]);
|
||||
expect(searchFriends('Test', broken, comparer)).toEqual([]);
|
||||
});
|
||||
});
|
||||
|
||||
// ── searchAvatars ──────────────────────────────────────────
|
||||
|
||||
describe('searchAvatars', () => {
|
||||
const avatarMap = new Map([
|
||||
[
|
||||
'a1',
|
||||
{
|
||||
id: 'a1',
|
||||
name: 'Cool Avatar',
|
||||
authorId: 'me',
|
||||
thumbnailImageUrl: 'img1'
|
||||
}
|
||||
],
|
||||
[
|
||||
'a2',
|
||||
{
|
||||
id: 'a2',
|
||||
name: 'Nice Avatar',
|
||||
authorId: 'other',
|
||||
thumbnailImageUrl: 'img2'
|
||||
}
|
||||
],
|
||||
[
|
||||
'a3',
|
||||
{
|
||||
id: 'a3',
|
||||
name: 'Cool Suit',
|
||||
authorId: 'me',
|
||||
thumbnailImageUrl: 'img3'
|
||||
}
|
||||
]
|
||||
]);
|
||||
|
||||
test('finds matching avatars', () => {
|
||||
const results = searchAvatars('Cool', avatarMap, comparer);
|
||||
expect(results).toHaveLength(2);
|
||||
});
|
||||
|
||||
test('filters by authorId', () => {
|
||||
const results = searchAvatars('Avatar', avatarMap, comparer, 'me');
|
||||
expect(results).toHaveLength(1);
|
||||
expect(results[0].id).toBe('a1');
|
||||
});
|
||||
|
||||
test('returns all when authorId is null', () => {
|
||||
const results = searchAvatars('Avatar', avatarMap, comparer, null);
|
||||
expect(results).toHaveLength(2);
|
||||
});
|
||||
|
||||
test('returns empty for null map', () => {
|
||||
expect(searchAvatars('test', null, comparer)).toEqual([]);
|
||||
});
|
||||
});
|
||||
|
||||
// ── searchWorlds ───────────────────────────────────────────
|
||||
|
||||
describe('searchWorlds', () => {
|
||||
const worldMap = new Map([
|
||||
[
|
||||
'w1',
|
||||
{
|
||||
id: 'w1',
|
||||
name: 'Fun World',
|
||||
authorId: 'me',
|
||||
thumbnailImageUrl: 'img1'
|
||||
}
|
||||
],
|
||||
[
|
||||
'w2',
|
||||
{
|
||||
id: 'w2',
|
||||
name: 'Fun Park',
|
||||
authorId: 'other',
|
||||
thumbnailImageUrl: 'img2'
|
||||
}
|
||||
]
|
||||
]);
|
||||
|
||||
test('finds matching worlds', () => {
|
||||
const results = searchWorlds('Fun', worldMap, comparer);
|
||||
expect(results).toHaveLength(2);
|
||||
});
|
||||
|
||||
test('filters by ownerId (authorId)', () => {
|
||||
const results = searchWorlds('Fun', worldMap, comparer, 'me');
|
||||
expect(results).toHaveLength(1);
|
||||
expect(results[0].id).toBe('w1');
|
||||
});
|
||||
});
|
||||
|
||||
// ── searchGroups ───────────────────────────────────────────
|
||||
|
||||
describe('searchGroups', () => {
|
||||
const groupMap = new Map([
|
||||
[
|
||||
'g1',
|
||||
{
|
||||
id: 'g1',
|
||||
name: 'My Group',
|
||||
ownerId: 'me',
|
||||
iconUrl: 'icon1'
|
||||
}
|
||||
],
|
||||
[
|
||||
'g2',
|
||||
{
|
||||
id: 'g2',
|
||||
name: 'Other Group',
|
||||
ownerId: 'other',
|
||||
iconUrl: 'icon2'
|
||||
}
|
||||
],
|
||||
[
|
||||
'g3',
|
||||
{
|
||||
id: 'g3',
|
||||
name: 'Another My Group',
|
||||
ownerId: 'me',
|
||||
iconUrl: 'icon3'
|
||||
}
|
||||
]
|
||||
]);
|
||||
|
||||
test('finds all matching groups', () => {
|
||||
const results = searchGroups('Group', groupMap, comparer);
|
||||
expect(results).toHaveLength(3);
|
||||
});
|
||||
|
||||
test('filters by ownerId', () => {
|
||||
const results = searchGroups('Group', groupMap, comparer, 'me');
|
||||
expect(results).toHaveLength(2);
|
||||
expect(results.every((r) => r.id !== 'g2')).toBe(true);
|
||||
});
|
||||
|
||||
test('returns all when ownerId is null', () => {
|
||||
const results = searchGroups('Group', groupMap, comparer, null);
|
||||
expect(results).toHaveLength(3);
|
||||
});
|
||||
});
|
||||
|
||||
// ── searchFavoriteAvatars ──────────────────────────────────
|
||||
|
||||
describe('searchFavoriteAvatars', () => {
|
||||
const favorites = [
|
||||
{
|
||||
name: 'Fav Avatar',
|
||||
ref: { id: 'fa1', name: 'Fav Avatar', thumbnailImageUrl: 'img1' }
|
||||
},
|
||||
{
|
||||
name: 'Cool Fav',
|
||||
ref: { id: 'fa2', name: 'Cool Fav', thumbnailImageUrl: 'img2' }
|
||||
},
|
||||
{ name: 'Broken', ref: null }
|
||||
];
|
||||
|
||||
test('finds matching favorite avatars', () => {
|
||||
const results = searchFavoriteAvatars('Fav', favorites, comparer);
|
||||
expect(results).toHaveLength(2);
|
||||
expect(results.map((r) => r.id)).toContain('fa1');
|
||||
});
|
||||
|
||||
test('skips entries with null ref', () => {
|
||||
const results = searchFavoriteAvatars('Broken', favorites, comparer);
|
||||
expect(results).toHaveLength(0);
|
||||
});
|
||||
|
||||
test('returns empty for null input', () => {
|
||||
expect(searchFavoriteAvatars('test', null, comparer)).toEqual([]);
|
||||
});
|
||||
});
|
||||
|
||||
// ── searchFavoriteWorlds ───────────────────────────────────
|
||||
|
||||
describe('searchFavoriteWorlds', () => {
|
||||
const favorites = [
|
||||
{
|
||||
name: 'Fav World',
|
||||
ref: {
|
||||
id: 'fw1',
|
||||
name: 'Fav World',
|
||||
thumbnailImageUrl: 'img1'
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'Cool Place',
|
||||
ref: {
|
||||
id: 'fw2',
|
||||
name: 'Cool Place',
|
||||
thumbnailImageUrl: 'img2'
|
||||
}
|
||||
}
|
||||
];
|
||||
|
||||
test('finds matching favorite worlds', () => {
|
||||
const results = searchFavoriteWorlds('Cool', favorites, comparer);
|
||||
expect(results).toHaveLength(1);
|
||||
expect(results[0].id).toBe('fw2');
|
||||
expect(results[0].type).toBe('world');
|
||||
});
|
||||
|
||||
test('returns empty for empty query', () => {
|
||||
expect(searchFavoriteWorlds('', favorites, comparer)).toEqual([]);
|
||||
});
|
||||
});
|
||||
336
src/shared/utils/globalSearchUtils.js
Normal file
336
src/shared/utils/globalSearchUtils.js
Normal file
@@ -0,0 +1,336 @@
|
||||
import { localeIncludes } from './base/string';
|
||||
import removeConfusables, { removeWhitespace } from '../../service/confusables';
|
||||
|
||||
/**
|
||||
* Tests whether a name matches a query using locale-aware comparison.
|
||||
* Handles confusable-character normalization and whitespace stripping.
|
||||
* @param {string} name - The display name to test
|
||||
* @param {string} query - The raw user query (may contain whitespace)
|
||||
* @param {Intl.Collator} comparer - Locale collator for comparison
|
||||
* @returns {boolean}
|
||||
*/
|
||||
export function matchName(name, query, comparer) {
|
||||
if (!name || !query) {
|
||||
return false;
|
||||
}
|
||||
const cleanQuery = removeWhitespace(query);
|
||||
if (!cleanQuery) {
|
||||
return false;
|
||||
}
|
||||
const cleanName = removeConfusables(name);
|
||||
if (localeIncludes(cleanName, cleanQuery, comparer)) {
|
||||
return true;
|
||||
}
|
||||
// Also check raw name for users searching with special characters
|
||||
return localeIncludes(name, cleanQuery, comparer);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check whether a query starts the name (for prioritizing prefix matches).
|
||||
* @param {string} name
|
||||
* @param {string} query
|
||||
* @param {Intl.Collator} comparer
|
||||
* @returns {boolean}
|
||||
*/
|
||||
export function isPrefixMatch(name, query, comparer) {
|
||||
if (!name || !query) {
|
||||
return false;
|
||||
}
|
||||
const cleanQuery = removeWhitespace(query);
|
||||
if (!cleanQuery) {
|
||||
return false;
|
||||
}
|
||||
return (
|
||||
comparer.compare(name.substring(0, cleanQuery.length), cleanQuery) === 0
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Search friends from the friends Map.
|
||||
* @param {string} query
|
||||
* @param {Map} friends - friendStore.friends Map
|
||||
* @param {Intl.Collator} comparer
|
||||
* @param {number} [limit]
|
||||
* @returns {Array<{id: string, name: string, type: string, imageUrl: string, ref: object}>}
|
||||
*/
|
||||
export function searchFriends(query, friends, comparer, limit = 10) {
|
||||
if (!query || !friends) {
|
||||
return [];
|
||||
}
|
||||
const results = [];
|
||||
for (const ctx of friends.values()) {
|
||||
if (typeof ctx.ref === 'undefined') {
|
||||
continue;
|
||||
}
|
||||
let match = matchName(ctx.name, query, comparer);
|
||||
let matchedField = match ? 'name' : null;
|
||||
// Include memo and note matching for friends (with raw query for spaces)
|
||||
if (!match && ctx.memo) {
|
||||
match = localeIncludes(ctx.memo, query, comparer);
|
||||
if (match) matchedField = 'memo';
|
||||
}
|
||||
if (!match && ctx.ref.note) {
|
||||
match = localeIncludes(ctx.ref.note, query, comparer);
|
||||
if (match) matchedField = 'note';
|
||||
}
|
||||
if (match) {
|
||||
results.push({
|
||||
id: ctx.id,
|
||||
name: ctx.name,
|
||||
type: 'friend',
|
||||
imageUrl: ctx.ref.currentAvatarThumbnailImageUrl,
|
||||
memo: ctx.memo || '',
|
||||
note: ctx.ref.note || '',
|
||||
matchedField,
|
||||
ref: ctx.ref
|
||||
});
|
||||
}
|
||||
}
|
||||
// Sort: prefix matches first, then alphabetically
|
||||
results.sort((a, b) => {
|
||||
const aPrefix = isPrefixMatch(a.name, query, comparer);
|
||||
const bPrefix = isPrefixMatch(b.name, query, comparer);
|
||||
if (aPrefix && !bPrefix) return -1;
|
||||
if (bPrefix && !aPrefix) return 1;
|
||||
return comparer.compare(a.name, b.name);
|
||||
});
|
||||
if (results.length > limit) {
|
||||
results.length = limit;
|
||||
}
|
||||
return results;
|
||||
}
|
||||
|
||||
/**
|
||||
* Search avatars from a Map (cachedAvatars or favorite avatars).
|
||||
* @param {string} query
|
||||
* @param {Map} avatarMap
|
||||
* @param {Intl.Collator} comparer
|
||||
* @param {string|null} [authorId] - If provided, only match avatars by this author
|
||||
* @param {number} [limit]
|
||||
* @returns {Array<{id: string, name: string, type: string, imageUrl: string}>}
|
||||
*/
|
||||
export function searchAvatars(
|
||||
query,
|
||||
avatarMap,
|
||||
comparer,
|
||||
authorId = null,
|
||||
limit = 10
|
||||
) {
|
||||
if (!query || !avatarMap) {
|
||||
return [];
|
||||
}
|
||||
const results = [];
|
||||
for (const ref of avatarMap.values()) {
|
||||
if (!ref || !ref.name) {
|
||||
continue;
|
||||
}
|
||||
if (authorId && ref.authorId !== authorId) {
|
||||
continue;
|
||||
}
|
||||
if (matchName(ref.name, query, comparer)) {
|
||||
results.push({
|
||||
id: ref.id,
|
||||
name: ref.name,
|
||||
type: 'avatar',
|
||||
imageUrl: ref.thumbnailImageUrl || ref.imageUrl
|
||||
});
|
||||
}
|
||||
}
|
||||
results.sort((a, b) => {
|
||||
const aPrefix = isPrefixMatch(a.name, query, comparer);
|
||||
const bPrefix = isPrefixMatch(b.name, query, comparer);
|
||||
if (aPrefix && !bPrefix) return -1;
|
||||
if (bPrefix && !aPrefix) return 1;
|
||||
return comparer.compare(a.name, b.name);
|
||||
});
|
||||
if (results.length > limit) {
|
||||
results.length = limit;
|
||||
}
|
||||
return results;
|
||||
}
|
||||
|
||||
/**
|
||||
* Search worlds from a Map (cachedWorlds or favorite worlds).
|
||||
* @param {string} query
|
||||
* @param {Map} worldMap
|
||||
* @param {Intl.Collator} comparer
|
||||
* @param {string|null} [ownerId] - If provided, only match worlds owned by this user
|
||||
* @param {number} [limit]
|
||||
* @returns {Array<{id: string, name: string, type: string, imageUrl: string}>}
|
||||
*/
|
||||
export function searchWorlds(
|
||||
query,
|
||||
worldMap,
|
||||
comparer,
|
||||
ownerId = null,
|
||||
limit = 10
|
||||
) {
|
||||
if (!query || !worldMap) {
|
||||
return [];
|
||||
}
|
||||
const results = [];
|
||||
for (const ref of worldMap.values()) {
|
||||
if (!ref || !ref.name) {
|
||||
continue;
|
||||
}
|
||||
if (ownerId && ref.authorId !== ownerId) {
|
||||
continue;
|
||||
}
|
||||
if (matchName(ref.name, query, comparer)) {
|
||||
results.push({
|
||||
id: ref.id,
|
||||
name: ref.name,
|
||||
type: 'world',
|
||||
imageUrl: ref.thumbnailImageUrl || ref.imageUrl
|
||||
});
|
||||
}
|
||||
}
|
||||
results.sort((a, b) => {
|
||||
const aPrefix = isPrefixMatch(a.name, query, comparer);
|
||||
const bPrefix = isPrefixMatch(b.name, query, comparer);
|
||||
if (aPrefix && !bPrefix) return -1;
|
||||
if (bPrefix && !aPrefix) return 1;
|
||||
return comparer.compare(a.name, b.name);
|
||||
});
|
||||
if (results.length > limit) {
|
||||
results.length = limit;
|
||||
}
|
||||
return results;
|
||||
}
|
||||
|
||||
/**
|
||||
* Search groups from a Map (currentUserGroups).
|
||||
* @param {string} query
|
||||
* @param {Map} groupMap
|
||||
* @param {Intl.Collator} comparer
|
||||
* @param {string|null} [ownerId] - If provided, only match groups owned by this user
|
||||
* @param {number} [limit]
|
||||
* @returns {Array<{id: string, name: string, type: string, imageUrl: string}>}
|
||||
*/
|
||||
export function searchGroups(
|
||||
query,
|
||||
groupMap,
|
||||
comparer,
|
||||
ownerId = null,
|
||||
limit = 10
|
||||
) {
|
||||
if (!query || !groupMap) {
|
||||
return [];
|
||||
}
|
||||
const results = [];
|
||||
for (const ref of groupMap.values()) {
|
||||
if (!ref || !ref.name) {
|
||||
continue;
|
||||
}
|
||||
if (ownerId && ref.ownerId !== ownerId) {
|
||||
continue;
|
||||
}
|
||||
if (matchName(ref.name, query, comparer)) {
|
||||
results.push({
|
||||
id: ref.id,
|
||||
name: ref.name,
|
||||
type: 'group',
|
||||
imageUrl: ref.iconUrl || ref.bannerUrl
|
||||
});
|
||||
}
|
||||
}
|
||||
results.sort((a, b) => {
|
||||
const aPrefix = isPrefixMatch(a.name, query, comparer);
|
||||
const bPrefix = isPrefixMatch(b.name, query, comparer);
|
||||
if (aPrefix && !bPrefix) return -1;
|
||||
if (bPrefix && !aPrefix) return 1;
|
||||
return comparer.compare(a.name, b.name);
|
||||
});
|
||||
if (results.length > limit) {
|
||||
results.length = limit;
|
||||
}
|
||||
return results;
|
||||
}
|
||||
|
||||
/**
|
||||
* Search favorite avatars from the favoriteStore array.
|
||||
* @param {string} query
|
||||
* @param {Array} favoriteAvatars - favoriteStore.favoriteAvatars array of { name, ref }
|
||||
* @param {Intl.Collator} comparer
|
||||
* @param {number} [limit]
|
||||
* @returns {Array<{id: string, name: string, type: string, imageUrl: string}>}
|
||||
*/
|
||||
export function searchFavoriteAvatars(
|
||||
query,
|
||||
favoriteAvatars,
|
||||
comparer,
|
||||
limit = 10
|
||||
) {
|
||||
if (!query || !favoriteAvatars) {
|
||||
return [];
|
||||
}
|
||||
const results = [];
|
||||
for (const ctx of favoriteAvatars) {
|
||||
if (!ctx?.ref?.name) {
|
||||
continue;
|
||||
}
|
||||
if (matchName(ctx.ref.name, query, comparer)) {
|
||||
results.push({
|
||||
id: ctx.ref.id,
|
||||
name: ctx.ref.name,
|
||||
type: 'avatar',
|
||||
imageUrl: ctx.ref.thumbnailImageUrl || ctx.ref.imageUrl
|
||||
});
|
||||
}
|
||||
}
|
||||
results.sort((a, b) => {
|
||||
const aPrefix = isPrefixMatch(a.name, query, comparer);
|
||||
const bPrefix = isPrefixMatch(b.name, query, comparer);
|
||||
if (aPrefix && !bPrefix) return -1;
|
||||
if (bPrefix && !aPrefix) return 1;
|
||||
return comparer.compare(a.name, b.name);
|
||||
});
|
||||
if (results.length > limit) {
|
||||
results.length = limit;
|
||||
}
|
||||
return results;
|
||||
}
|
||||
|
||||
/**
|
||||
* Search favorite worlds from the favoriteStore array.
|
||||
* @param {string} query
|
||||
* @param {Array} favoriteWorlds - favoriteStore.favoriteWorlds array of { name, ref }
|
||||
* @param {Intl.Collator} comparer
|
||||
* @param {number} [limit]
|
||||
* @returns {Array<{id: string, name: string, type: string, imageUrl: string}>}
|
||||
*/
|
||||
export function searchFavoriteWorlds(
|
||||
query,
|
||||
favoriteWorlds,
|
||||
comparer,
|
||||
limit = 10
|
||||
) {
|
||||
if (!query || !favoriteWorlds) {
|
||||
return [];
|
||||
}
|
||||
const results = [];
|
||||
for (const ctx of favoriteWorlds) {
|
||||
if (!ctx?.ref?.name) {
|
||||
continue;
|
||||
}
|
||||
if (matchName(ctx.ref.name, query, comparer)) {
|
||||
results.push({
|
||||
id: ctx.ref.id,
|
||||
name: ctx.ref.name,
|
||||
type: 'world',
|
||||
imageUrl: ctx.ref.thumbnailImageUrl || ctx.ref.imageUrl
|
||||
});
|
||||
}
|
||||
}
|
||||
results.sort((a, b) => {
|
||||
const aPrefix = isPrefixMatch(a.name, query, comparer);
|
||||
const bPrefix = isPrefixMatch(b.name, query, comparer);
|
||||
if (aPrefix && !bPrefix) return -1;
|
||||
if (bPrefix && !aPrefix) return 1;
|
||||
return comparer.compare(a.name, b.name);
|
||||
});
|
||||
if (results.length > limit) {
|
||||
results.length = limit;
|
||||
}
|
||||
return results;
|
||||
}
|
||||
Reference in New Issue
Block a user