mirror of
https://github.com/MrUnknownDE/VRCX.git
synced 2026-05-03 13:36:04 +02:00
refactor global search
This commit is contained in:
@@ -5,10 +5,7 @@ import { nextTick, reactive } from 'vue';
|
||||
const mocks = vi.hoisted(() => ({
|
||||
workerInstances: [],
|
||||
friendStore: null,
|
||||
favoriteStore: null,
|
||||
avatarStore: null,
|
||||
worldStore: null,
|
||||
groupStore: null,
|
||||
searchIndexStore: null,
|
||||
userStore: null
|
||||
}));
|
||||
|
||||
@@ -29,17 +26,8 @@ vi.mock('../searchWorker.js?worker', () => ({
|
||||
vi.mock('../friend', () => ({
|
||||
useFriendStore: () => mocks.friendStore
|
||||
}));
|
||||
vi.mock('../favorite', () => ({
|
||||
useFavoriteStore: () => mocks.favoriteStore
|
||||
}));
|
||||
vi.mock('../avatar', () => ({
|
||||
useAvatarStore: () => mocks.avatarStore
|
||||
}));
|
||||
vi.mock('../world', () => ({
|
||||
useWorldStore: () => mocks.worldStore
|
||||
}));
|
||||
vi.mock('../group', () => ({
|
||||
useGroupStore: () => mocks.groupStore
|
||||
vi.mock('../searchIndex', () => ({
|
||||
useSearchIndexStore: () => mocks.searchIndexStore
|
||||
}));
|
||||
vi.mock('../user', () => ({
|
||||
useUserStore: () => mocks.userStore
|
||||
@@ -67,10 +55,30 @@ import { useGlobalSearchStore } from '../globalSearch';
|
||||
|
||||
function setupStores() {
|
||||
mocks.friendStore = reactive({ friends: new Map() });
|
||||
mocks.favoriteStore = reactive({ favoriteAvatars: [], favoriteWorlds: [] });
|
||||
mocks.avatarStore = reactive({ cachedAvatars: new Map() });
|
||||
mocks.worldStore = reactive({ cachedWorlds: new Map() });
|
||||
mocks.groupStore = reactive({ currentUserGroups: new Map() });
|
||||
mocks.searchIndexStore = reactive({
|
||||
version: 0,
|
||||
getSnapshot() {
|
||||
const friendsList = [];
|
||||
for (const ctx of (mocks.friendStore?.friends || new Map()).values()) {
|
||||
if (typeof ctx.ref === 'undefined') continue;
|
||||
friendsList.push({
|
||||
id: ctx.id,
|
||||
name: ctx.name,
|
||||
memo: ctx.memo || '',
|
||||
note: ctx.ref?.note || '',
|
||||
imageUrl: ctx.ref?.currentAvatarThumbnailImageUrl || ''
|
||||
});
|
||||
}
|
||||
return {
|
||||
friends: friendsList,
|
||||
avatars: [],
|
||||
worlds: [],
|
||||
groups: [],
|
||||
favAvatars: [],
|
||||
favWorlds: []
|
||||
};
|
||||
}
|
||||
});
|
||||
mocks.userStore = reactive({ currentUser: { id: 'usr_me' } });
|
||||
}
|
||||
|
||||
@@ -181,4 +189,37 @@ describe('useGlobalSearchStore', () => {
|
||||
expect(lastMessage.type).toBe('search');
|
||||
expect(lastMessage.payload.currentUserId).toBe('usr_other');
|
||||
});
|
||||
|
||||
test('re-dispatches search after index update when query is active', async () => {
|
||||
vi.useFakeTimers();
|
||||
const store = useGlobalSearchStore();
|
||||
store.isOpen = true;
|
||||
await nextTick();
|
||||
|
||||
store.setQuery('ab');
|
||||
await nextTick();
|
||||
|
||||
const worker = mocks.workerInstances[0];
|
||||
const callsBefore = worker.postMessage.mock.calls.length;
|
||||
|
||||
// Simulate searchIndex version bump (as if data arrived)
|
||||
mocks.searchIndexStore.version++;
|
||||
await nextTick();
|
||||
|
||||
// Fast-forward the 200ms debounce
|
||||
vi.advanceTimersByTime(200);
|
||||
await nextTick();
|
||||
|
||||
const newCalls = worker.postMessage.mock.calls.slice(callsBefore);
|
||||
const types = newCalls.map((c) => c[0].type);
|
||||
expect(types).toContain('updateIndex');
|
||||
expect(types).toContain('search');
|
||||
|
||||
// updateIndex should come before search
|
||||
const updateIdx = types.indexOf('updateIndex');
|
||||
const searchIdx = types.lastIndexOf('search');
|
||||
expect(updateIdx).toBeLessThan(searchIdx);
|
||||
|
||||
vi.useRealTimers();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -17,6 +17,7 @@ import {
|
||||
runPendingOfflineTickFlow,
|
||||
runUpdateFriendFlow
|
||||
} from '../coordinators/friendPresenceCoordinator';
|
||||
import { syncFriendSearchIndex } from '../coordinators/searchIndexCoordinator';
|
||||
import {
|
||||
updateFriendship,
|
||||
runUpdateFriendshipsFlow
|
||||
@@ -340,19 +341,24 @@ export const useFriendStore = defineStore('Friend', () => {
|
||||
for (id of ref.onlineFriends) {
|
||||
map.set(id, 'online');
|
||||
}
|
||||
const added = [];
|
||||
const removed = [];
|
||||
for (const friend of map) {
|
||||
const [id, state_input] = friend;
|
||||
if (friends.has(id)) {
|
||||
runUpdateFriendFlow(id, state_input);
|
||||
} else {
|
||||
addFriend(id, state_input);
|
||||
added.push(id);
|
||||
}
|
||||
}
|
||||
for (id of friends.keys()) {
|
||||
if (map.has(id) === false) {
|
||||
deleteFriend(id);
|
||||
removed.push(id);
|
||||
}
|
||||
}
|
||||
return { added, removed };
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -389,6 +395,7 @@ export const useFriendStore = defineStore('Friend', () => {
|
||||
const array = memo.memo.split('\n');
|
||||
ctx.$nickName = array[0];
|
||||
}
|
||||
syncFriendSearchIndex(ctx);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
+9
-111
@@ -1,11 +1,8 @@
|
||||
import { computed, effectScope, ref, watch } from 'vue';
|
||||
import { defineStore } from 'pinia';
|
||||
import { useAvatarStore } from './avatar';
|
||||
import { useFavoriteStore } from './favorite';
|
||||
import { useFriendStore } from './friend';
|
||||
import { useGroupStore } from './group';
|
||||
import { useSearchIndexStore } from './searchIndex';
|
||||
import { useUserStore } from './user';
|
||||
import { useWorldStore } from './world';
|
||||
import { showGroupDialog } from '../coordinators/groupCoordinator';
|
||||
import { showWorldDialog } from '../coordinators/worldCoordinator';
|
||||
import { showAvatarDialog } from '../coordinators/avatarCoordinator';
|
||||
@@ -15,11 +12,8 @@ import SearchWorker from './searchWorker.js?worker';
|
||||
|
||||
export const useGlobalSearchStore = defineStore('GlobalSearch', () => {
|
||||
const friendStore = useFriendStore();
|
||||
const favoriteStore = useFavoriteStore();
|
||||
const avatarStore = useAvatarStore();
|
||||
const worldStore = useWorldStore();
|
||||
const groupStore = useGroupStore();
|
||||
const userStore = useUserStore();
|
||||
const searchIndexStore = useSearchIndexStore();
|
||||
|
||||
const isOpen = ref(false);
|
||||
const query = ref('');
|
||||
@@ -73,81 +67,16 @@ export const useGlobalSearchStore = defineStore('GlobalSearch', () => {
|
||||
indexUpdateTimer = null;
|
||||
if (!isOpen.value) return;
|
||||
sendIndexUpdate();
|
||||
if (query.value && query.value.length >= 2) {
|
||||
dispatchSearch();
|
||||
}
|
||||
}, 200);
|
||||
}
|
||||
|
||||
function sendIndexUpdate() {
|
||||
const w = getWorker();
|
||||
|
||||
const friends = [];
|
||||
for (const ctx of friendStore.friends.values()) {
|
||||
if (typeof ctx.ref === 'undefined') continue;
|
||||
friends.push({
|
||||
id: ctx.id,
|
||||
name: ctx.name,
|
||||
memo: ctx.memo || '',
|
||||
note: ctx.ref.note || '',
|
||||
imageUrl: ctx.ref.currentAvatarThumbnailImageUrl
|
||||
});
|
||||
}
|
||||
|
||||
const avatars = [];
|
||||
for (const ref of avatarStore.cachedAvatars.values()) {
|
||||
if (!ref || !ref.name) continue;
|
||||
avatars.push({
|
||||
id: ref.id,
|
||||
name: ref.name,
|
||||
authorId: ref.authorId,
|
||||
imageUrl: ref.thumbnailImageUrl || ref.imageUrl
|
||||
});
|
||||
}
|
||||
|
||||
const worlds = [];
|
||||
for (const ref of worldStore.cachedWorlds.values()) {
|
||||
if (!ref || !ref.name) continue;
|
||||
worlds.push({
|
||||
id: ref.id,
|
||||
name: ref.name,
|
||||
authorId: ref.authorId,
|
||||
imageUrl: ref.thumbnailImageUrl || ref.imageUrl
|
||||
});
|
||||
}
|
||||
|
||||
const groups = [];
|
||||
for (const ref of groupStore.currentUserGroups.values()) {
|
||||
if (!ref || !ref.name) continue;
|
||||
groups.push({
|
||||
id: ref.id,
|
||||
name: ref.name,
|
||||
ownerId: ref.ownerId,
|
||||
imageUrl: ref.iconUrl || ref.bannerUrl
|
||||
});
|
||||
}
|
||||
|
||||
const favAvatars = [];
|
||||
for (const ctx of favoriteStore.favoriteAvatars) {
|
||||
if (!ctx?.ref?.name) continue;
|
||||
favAvatars.push({
|
||||
id: ctx.ref.id,
|
||||
name: ctx.ref.name,
|
||||
imageUrl: ctx.ref.thumbnailImageUrl || ctx.ref.imageUrl
|
||||
});
|
||||
}
|
||||
|
||||
const favWorlds = [];
|
||||
for (const ctx of favoriteStore.favoriteWorlds) {
|
||||
if (!ctx?.ref?.name) continue;
|
||||
favWorlds.push({
|
||||
id: ctx.ref.id,
|
||||
name: ctx.ref.name,
|
||||
imageUrl: ctx.ref.thumbnailImageUrl || ctx.ref.imageUrl
|
||||
});
|
||||
}
|
||||
|
||||
w.postMessage({
|
||||
type: 'updateIndex',
|
||||
payload: { friends, avatars, worlds, groups, favAvatars, favWorlds }
|
||||
});
|
||||
const payload = searchIndexStore.getSnapshot();
|
||||
w.postMessage({ type: 'updateIndex', payload });
|
||||
}
|
||||
|
||||
function stopIndexWatchers() {
|
||||
@@ -167,39 +96,8 @@ export const useGlobalSearchStore = defineStore('GlobalSearch', () => {
|
||||
indexWatchScope = effectScope();
|
||||
indexWatchScope.run(() => {
|
||||
watch(
|
||||
() => friendStore.friends,
|
||||
() => scheduleIndexUpdate(),
|
||||
{ deep: true }
|
||||
);
|
||||
|
||||
watch(
|
||||
() => avatarStore.cachedAvatars,
|
||||
() => scheduleIndexUpdate(),
|
||||
{ deep: true }
|
||||
);
|
||||
|
||||
watch(
|
||||
() => worldStore.cachedWorlds,
|
||||
() => scheduleIndexUpdate(),
|
||||
{ deep: true }
|
||||
);
|
||||
|
||||
watch(
|
||||
() => groupStore.currentUserGroups,
|
||||
() => scheduleIndexUpdate(),
|
||||
{ deep: true }
|
||||
);
|
||||
|
||||
watch(
|
||||
() => favoriteStore.favoriteAvatars,
|
||||
() => scheduleIndexUpdate(),
|
||||
{ deep: true }
|
||||
);
|
||||
|
||||
watch(
|
||||
() => favoriteStore.favoriteWorlds,
|
||||
() => scheduleIndexUpdate(),
|
||||
{ deep: true }
|
||||
() => searchIndexStore.version,
|
||||
() => scheduleIndexUpdate()
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -0,0 +1,294 @@
|
||||
import { ref } from 'vue';
|
||||
import { defineStore } from 'pinia';
|
||||
import { useFavoriteStore } from './favorite';
|
||||
|
||||
export const useSearchIndexStore = defineStore('SearchIndex', () => {
|
||||
const friends = new Map();
|
||||
const avatars = new Map();
|
||||
const worlds = new Map();
|
||||
const groups = new Map();
|
||||
const favAvatars = new Map();
|
||||
const favWorlds = new Map();
|
||||
|
||||
const version = ref(0);
|
||||
|
||||
|
||||
/**
|
||||
* Sync a friend context into the search index.
|
||||
* Extracts only the fields needed for searching.
|
||||
* @param {object} ctx - Friend context from friendStore.friends
|
||||
*/
|
||||
function syncFriend(ctx) {
|
||||
if (!ctx || !ctx.id) return;
|
||||
const entry = {
|
||||
id: ctx.id,
|
||||
name: ctx.name || '',
|
||||
memo: ctx.memo || '',
|
||||
note: ctx.ref?.note || '',
|
||||
imageUrl: ctx.ref?.currentAvatarThumbnailImageUrl || ''
|
||||
};
|
||||
const existing = friends.get(ctx.id);
|
||||
if (
|
||||
existing &&
|
||||
existing.name === entry.name &&
|
||||
existing.memo === entry.memo &&
|
||||
existing.note === entry.note &&
|
||||
existing.imageUrl === entry.imageUrl
|
||||
) {
|
||||
return;
|
||||
}
|
||||
friends.set(ctx.id, entry);
|
||||
version.value++;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} id
|
||||
*/
|
||||
function removeFriend(id) {
|
||||
if (friends.delete(id)) {
|
||||
version.value++;
|
||||
}
|
||||
}
|
||||
|
||||
function clearFriends() {
|
||||
if (friends.size > 0) {
|
||||
friends.clear();
|
||||
version.value++;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* @param {object} ref - Avatar data object
|
||||
*/
|
||||
function upsertAvatar(ref) {
|
||||
if (!ref || !ref.id) return;
|
||||
const entry = {
|
||||
id: ref.id,
|
||||
name: ref.name || '',
|
||||
authorId: ref.authorId || '',
|
||||
imageUrl: ref.thumbnailImageUrl || ref.imageUrl || ''
|
||||
};
|
||||
const existing = avatars.get(ref.id);
|
||||
if (
|
||||
existing &&
|
||||
existing.name === entry.name &&
|
||||
existing.authorId === entry.authorId &&
|
||||
existing.imageUrl === entry.imageUrl
|
||||
) {
|
||||
return;
|
||||
}
|
||||
avatars.set(ref.id, entry);
|
||||
version.value++;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} id
|
||||
*/
|
||||
function removeAvatar(id) {
|
||||
if (avatars.delete(id)) {
|
||||
version.value++;
|
||||
}
|
||||
}
|
||||
|
||||
function clearAvatars() {
|
||||
if (avatars.size > 0) {
|
||||
avatars.clear();
|
||||
version.value++;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* @param {object} ref - World data object
|
||||
*/
|
||||
function upsertWorld(ref) {
|
||||
if (!ref || !ref.id) return;
|
||||
const entry = {
|
||||
id: ref.id,
|
||||
name: ref.name || '',
|
||||
authorId: ref.authorId || '',
|
||||
imageUrl: ref.thumbnailImageUrl || ref.imageUrl || ''
|
||||
};
|
||||
const existing = worlds.get(ref.id);
|
||||
if (
|
||||
existing &&
|
||||
existing.name === entry.name &&
|
||||
existing.authorId === entry.authorId &&
|
||||
existing.imageUrl === entry.imageUrl
|
||||
) {
|
||||
return;
|
||||
}
|
||||
worlds.set(ref.id, entry);
|
||||
version.value++;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} id
|
||||
*/
|
||||
function removeWorld(id) {
|
||||
if (worlds.delete(id)) {
|
||||
version.value++;
|
||||
}
|
||||
}
|
||||
|
||||
function clearWorlds() {
|
||||
if (worlds.size > 0) {
|
||||
worlds.clear();
|
||||
version.value++;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* @param {object} ref - Group data object
|
||||
*/
|
||||
function upsertGroup(ref) {
|
||||
if (!ref || !ref.id) return;
|
||||
const entry = {
|
||||
id: ref.id,
|
||||
name: ref.name || '',
|
||||
ownerId: ref.ownerId || '',
|
||||
imageUrl: ref.iconUrl || ref.bannerUrl || ''
|
||||
};
|
||||
const existing = groups.get(ref.id);
|
||||
if (
|
||||
existing &&
|
||||
existing.name === entry.name &&
|
||||
existing.ownerId === entry.ownerId &&
|
||||
existing.imageUrl === entry.imageUrl
|
||||
) {
|
||||
return;
|
||||
}
|
||||
groups.set(ref.id, entry);
|
||||
version.value++;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} id
|
||||
*/
|
||||
function removeGroup(id) {
|
||||
if (groups.delete(id)) {
|
||||
version.value++;
|
||||
}
|
||||
}
|
||||
|
||||
function clearGroups() {
|
||||
if (groups.size > 0) {
|
||||
groups.clear();
|
||||
version.value++;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
function rebuildFavoritesFromStore() {
|
||||
const favoriteStore = useFavoriteStore();
|
||||
|
||||
const newFavAvatars = new Map();
|
||||
for (const ctx of favoriteStore.favoriteAvatars) {
|
||||
if (!ctx?.ref?.name) continue;
|
||||
newFavAvatars.set(ctx.ref.id, {
|
||||
id: ctx.ref.id,
|
||||
name: ctx.ref.name,
|
||||
imageUrl: ctx.ref.thumbnailImageUrl || ctx.ref.imageUrl || ''
|
||||
});
|
||||
}
|
||||
|
||||
const newFavWorlds = new Map();
|
||||
for (const ctx of favoriteStore.favoriteWorlds) {
|
||||
if (!ctx?.ref?.name) continue;
|
||||
newFavWorlds.set(ctx.ref.id, {
|
||||
id: ctx.ref.id,
|
||||
name: ctx.ref.name,
|
||||
imageUrl: ctx.ref.thumbnailImageUrl || ctx.ref.imageUrl || ''
|
||||
});
|
||||
}
|
||||
|
||||
let changed = false;
|
||||
if (favAvatars.size !== newFavAvatars.size) {
|
||||
changed = true;
|
||||
} else {
|
||||
for (const [id, entry] of newFavAvatars) {
|
||||
const existing = favAvatars.get(id);
|
||||
if (!existing || existing.name !== entry.name || existing.imageUrl !== entry.imageUrl) {
|
||||
changed = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (favWorlds.size !== newFavWorlds.size) {
|
||||
changed = true;
|
||||
} else if (!changed) {
|
||||
for (const [id, entry] of newFavWorlds) {
|
||||
const existing = favWorlds.get(id);
|
||||
if (!existing || existing.name !== entry.name || existing.imageUrl !== entry.imageUrl) {
|
||||
changed = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (changed) {
|
||||
favAvatars.clear();
|
||||
for (const [id, entry] of newFavAvatars) {
|
||||
favAvatars.set(id, entry);
|
||||
}
|
||||
favWorlds.clear();
|
||||
for (const [id, entry] of newFavWorlds) {
|
||||
favWorlds.set(id, entry);
|
||||
}
|
||||
version.value++;
|
||||
}
|
||||
}
|
||||
|
||||
function clearFavorites() {
|
||||
if (favAvatars.size > 0 || favWorlds.size > 0) {
|
||||
favAvatars.clear();
|
||||
favWorlds.clear();
|
||||
version.value++;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Build a snapshot from the internal index maps.
|
||||
* Used by globalSearch to send data to the Worker.
|
||||
* @returns {object} Plain object arrays ready for postMessage.
|
||||
*/
|
||||
function getSnapshot() {
|
||||
return {
|
||||
friends: Array.from(friends.values()),
|
||||
avatars: Array.from(avatars.values()),
|
||||
worlds: Array.from(worlds.values()),
|
||||
groups: Array.from(groups.values()),
|
||||
favAvatars: Array.from(favAvatars.values()),
|
||||
favWorlds: Array.from(favWorlds.values())
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
|
||||
return {
|
||||
version,
|
||||
|
||||
syncFriend,
|
||||
removeFriend,
|
||||
clearFriends,
|
||||
|
||||
upsertAvatar,
|
||||
removeAvatar,
|
||||
clearAvatars,
|
||||
|
||||
upsertWorld,
|
||||
removeWorld,
|
||||
clearWorlds,
|
||||
|
||||
upsertGroup,
|
||||
removeGroup,
|
||||
clearGroups,
|
||||
rebuildFavoritesFromStore,
|
||||
clearFavorites,
|
||||
|
||||
getSnapshot
|
||||
};
|
||||
});
|
||||
+77
-37
@@ -88,7 +88,6 @@ function removeWhitespace(a) {
|
||||
return a.replace(/\s/g, '');
|
||||
}
|
||||
|
||||
// ── Locale-aware string search ──────────────────────────────────────
|
||||
|
||||
function localeIncludes(str, search, comparer) {
|
||||
if (search === '') return true;
|
||||
@@ -104,25 +103,20 @@ function localeIncludes(str, search, comparer) {
|
||||
return false;
|
||||
}
|
||||
|
||||
function matchName(name, query, comparer) {
|
||||
if (!name || !query) return false;
|
||||
const cleanQuery = removeWhitespace(query);
|
||||
if (!cleanQuery) return false;
|
||||
const cleanName = removeConfusables(name);
|
||||
function matchName(name, cleanQuery, comparer, normalizedName) {
|
||||
if (!name || !cleanQuery) return false;
|
||||
const cleanName = normalizedName || removeConfusables(name);
|
||||
if (localeIncludes(cleanName, cleanQuery, comparer)) return true;
|
||||
return localeIncludes(name, cleanQuery, comparer);
|
||||
}
|
||||
|
||||
function isPrefixMatch(name, query, comparer) {
|
||||
if (!name || !query) return false;
|
||||
const cleanQuery = removeWhitespace(query);
|
||||
if (!cleanQuery) return false;
|
||||
function isPrefixMatch(name, cleanQuery, comparer) {
|
||||
if (!name || !cleanQuery) return false;
|
||||
return (
|
||||
comparer.compare(name.substring(0, cleanQuery.length), cleanQuery) === 0
|
||||
);
|
||||
}
|
||||
|
||||
// ── Index data (updated from main thread) ───────────────────────────
|
||||
|
||||
let indexedFriends = []; // { id, name, memo, note, imageUrl }
|
||||
let indexedAvatars = []; // { id, name, authorId, imageUrl }
|
||||
@@ -133,22 +127,54 @@ let indexedFavWorlds = []; // { id, name, imageUrl }
|
||||
|
||||
/**
|
||||
* Update the search index with fresh data snapshots.
|
||||
* Pre-computes normalized names to avoid per-search confusables overhead.
|
||||
* @param payload
|
||||
*/
|
||||
function updateIndex(payload) {
|
||||
if (payload.friends) indexedFriends = payload.friends;
|
||||
if (payload.avatars) indexedAvatars = payload.avatars;
|
||||
if (payload.worlds) indexedWorlds = payload.worlds;
|
||||
if (payload.groups) indexedGroups = payload.groups;
|
||||
if (payload.favAvatars) indexedFavAvatars = payload.favAvatars;
|
||||
if (payload.favWorlds) indexedFavWorlds = payload.favWorlds;
|
||||
if (payload.friends) {
|
||||
indexedFriends = payload.friends;
|
||||
for (const f of indexedFriends) {
|
||||
f._normalized = f.name ? removeConfusables(f.name) : '';
|
||||
}
|
||||
}
|
||||
if (payload.avatars) {
|
||||
indexedAvatars = payload.avatars;
|
||||
for (const a of indexedAvatars) {
|
||||
a._normalized = a.name ? removeConfusables(a.name) : '';
|
||||
}
|
||||
}
|
||||
if (payload.worlds) {
|
||||
indexedWorlds = payload.worlds;
|
||||
for (const w of indexedWorlds) {
|
||||
w._normalized = w.name ? removeConfusables(w.name) : '';
|
||||
}
|
||||
}
|
||||
if (payload.groups) {
|
||||
indexedGroups = payload.groups;
|
||||
for (const g of indexedGroups) {
|
||||
g._normalized = g.name ? removeConfusables(g.name) : '';
|
||||
}
|
||||
}
|
||||
if (payload.favAvatars) {
|
||||
indexedFavAvatars = payload.favAvatars;
|
||||
for (const a of indexedFavAvatars) {
|
||||
a._normalized = a.name ? removeConfusables(a.name) : '';
|
||||
}
|
||||
}
|
||||
if (payload.favWorlds) {
|
||||
indexedFavWorlds = payload.favWorlds;
|
||||
for (const w of indexedFavWorlds) {
|
||||
w._normalized = w.name ? removeConfusables(w.name) : '';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ── Search functions ────────────────────────────────────────────────
|
||||
|
||||
function searchFriends(query, comparer, limit = 10) {
|
||||
function searchFriends(query, cleanQuery, comparer, limit = 10) {
|
||||
const results = [];
|
||||
for (const ctx of indexedFriends) {
|
||||
let match = matchName(ctx.name, query, comparer);
|
||||
let match = matchName(ctx.name, cleanQuery, comparer, ctx._normalized);
|
||||
let matchedField = match ? 'name' : null;
|
||||
if (!match && ctx.memo) {
|
||||
match = localeIncludes(ctx.memo, query, comparer);
|
||||
@@ -170,19 +196,25 @@ function searchFriends(query, comparer, limit = 10) {
|
||||
});
|
||||
}
|
||||
}
|
||||
// Pre-compute prefix flags to avoid repeated Collator calls in sort
|
||||
for (const r of results) {
|
||||
r._isPrefix = isPrefixMatch(r.name, cleanQuery, comparer);
|
||||
}
|
||||
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;
|
||||
if (a._isPrefix && !b._isPrefix) return -1;
|
||||
if (b._isPrefix && !a._isPrefix) return 1;
|
||||
return comparer.compare(a.name, b.name);
|
||||
});
|
||||
if (results.length > limit) results.length = limit;
|
||||
// Clean up internal sort field before returning
|
||||
for (const r of results) {
|
||||
delete r._isPrefix;
|
||||
}
|
||||
return results;
|
||||
}
|
||||
|
||||
function searchItems(
|
||||
query,
|
||||
cleanQuery,
|
||||
items,
|
||||
type,
|
||||
comparer,
|
||||
@@ -194,7 +226,7 @@ function searchItems(
|
||||
for (const ref of items) {
|
||||
if (!ref || !ref.name) continue;
|
||||
if (ownerId && ref[ownerKey] !== ownerId) continue;
|
||||
if (matchName(ref.name, query, comparer)) {
|
||||
if (matchName(ref.name, cleanQuery, comparer, ref._normalized)) {
|
||||
results.push({
|
||||
id: ref.id,
|
||||
name: ref.name,
|
||||
@@ -203,14 +235,20 @@ function searchItems(
|
||||
});
|
||||
}
|
||||
}
|
||||
// Pre-compute prefix flags to avoid repeated Collator calls in sort
|
||||
for (const r of results) {
|
||||
r._isPrefix = isPrefixMatch(r.name, cleanQuery, comparer);
|
||||
}
|
||||
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;
|
||||
if (a._isPrefix && !b._isPrefix) return -1;
|
||||
if (b._isPrefix && !a._isPrefix) return 1;
|
||||
return comparer.compare(a.name, b.name);
|
||||
});
|
||||
if (results.length > limit) results.length = limit;
|
||||
// Clean up internal sort field before returning
|
||||
for (const r of results) {
|
||||
delete r._isPrefix;
|
||||
}
|
||||
return results;
|
||||
}
|
||||
|
||||
@@ -239,9 +277,12 @@ function handleSearch(payload) {
|
||||
sensitivity: 'base'
|
||||
});
|
||||
|
||||
const friends = searchFriends(query, comparer);
|
||||
// Pre-compute cleaned query once for all name searches
|
||||
const cleanQuery = removeWhitespace(query);
|
||||
|
||||
const friends = searchFriends(query, cleanQuery, comparer);
|
||||
const ownAvatars = searchItems(
|
||||
query,
|
||||
cleanQuery,
|
||||
indexedAvatars,
|
||||
'avatar',
|
||||
comparer,
|
||||
@@ -249,7 +290,7 @@ function handleSearch(payload) {
|
||||
currentUserId
|
||||
);
|
||||
const favAvatars = searchItems(
|
||||
query,
|
||||
cleanQuery,
|
||||
indexedFavAvatars,
|
||||
'avatar',
|
||||
comparer,
|
||||
@@ -257,7 +298,7 @@ function handleSearch(payload) {
|
||||
null
|
||||
);
|
||||
const ownWorlds = searchItems(
|
||||
query,
|
||||
cleanQuery,
|
||||
indexedWorlds,
|
||||
'world',
|
||||
comparer,
|
||||
@@ -265,7 +306,7 @@ function handleSearch(payload) {
|
||||
currentUserId
|
||||
);
|
||||
const favWorlds = searchItems(
|
||||
query,
|
||||
cleanQuery,
|
||||
indexedFavWorlds,
|
||||
'world',
|
||||
comparer,
|
||||
@@ -273,7 +314,7 @@ function handleSearch(payload) {
|
||||
null
|
||||
);
|
||||
const ownGroups = searchItems(
|
||||
query,
|
||||
cleanQuery,
|
||||
indexedGroups,
|
||||
'group',
|
||||
comparer,
|
||||
@@ -281,7 +322,7 @@ function handleSearch(payload) {
|
||||
currentUserId
|
||||
);
|
||||
const joinedGroups = searchItems(
|
||||
query,
|
||||
cleanQuery,
|
||||
indexedGroups,
|
||||
'group',
|
||||
comparer,
|
||||
@@ -314,7 +355,6 @@ function handleSearch(payload) {
|
||||
});
|
||||
}
|
||||
|
||||
// ── Message handler ─────────────────────────────────────────────────
|
||||
|
||||
self.addEventListener('message', (event) => {
|
||||
const { type, payload } = event.data;
|
||||
|
||||
@@ -21,6 +21,7 @@ import { useAppearanceSettingsStore } from './settings/appearance';
|
||||
import { useFriendStore } from './friend';
|
||||
import { useInstanceStore } from './instance';
|
||||
import { useLocationStore } from './location';
|
||||
import { syncFriendSearchIndex } from '../coordinators/searchIndexCoordinator';
|
||||
import { useUiStore } from './ui';
|
||||
import { watchState } from '../services/watchState';
|
||||
|
||||
@@ -499,6 +500,10 @@ export const useUserStore = defineStore('User', () => {
|
||||
const user = users.get(note.userId);
|
||||
if (user) {
|
||||
user.note = note.note;
|
||||
const friendCtx = friendStore.friends.get(note.userId);
|
||||
if (friendCtx) {
|
||||
syncFriendSearchIndex(friendCtx);
|
||||
}
|
||||
}
|
||||
if (
|
||||
!state.lastDbNoteDate ||
|
||||
@@ -568,6 +573,10 @@ export const useUserStore = defineStore('User', () => {
|
||||
const user = users.get(note.targetUserId);
|
||||
if (user) {
|
||||
user.note = note.note;
|
||||
const friendCtx = friendStore.friends.get(note.targetUserId);
|
||||
if (friendCtx) {
|
||||
syncFriendSearchIndex(friendCtx);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -44,6 +44,7 @@ import { useUpdateLoopStore } from './updateLoop';
|
||||
import { useUserStore } from './user';
|
||||
import { useVrcStatusStore } from './vrcStatus';
|
||||
import { clearVRCXCache } from '../coordinators/vrcxCoordinator';
|
||||
import { resetSearchIndexOnLogin } from '../coordinators/searchIndexCoordinator';
|
||||
import { watchState } from '../services/watchState';
|
||||
|
||||
import configRepository from '../services/config';
|
||||
@@ -177,6 +178,7 @@ export const useVrcxStore = defineStore('Vrcx', () => {
|
||||
refreshCustomScript();
|
||||
}
|
||||
|
||||
resetSearchIndexOnLogin();
|
||||
init();
|
||||
|
||||
/**
|
||||
|
||||
Reference in New Issue
Block a user