diff --git a/src/components/dialogs/InviteDialog/InviteDialog.vue b/src/components/dialogs/InviteDialog/InviteDialog.vue index cdfaed91..59f5da89 100644 --- a/src/components/dialogs/InviteDialog/InviteDialog.vue +++ b/src/components/dialogs/InviteDialog/InviteDialog.vue @@ -138,6 +138,29 @@ params: {} }); + const friendSections = computed(() => [ + { + key: 'friendsInInstance', + label: t('dialog.invite.friends_in_instance'), + friends: props.inviteDialog?.friendsInInstance ?? [] + }, + { + key: 'vip', + label: t('side_panel.favorite'), + friends: vipFriends.value + }, + { + key: 'online', + label: t('side_panel.online'), + friends: onlineFriends.value + }, + { + key: 'active', + label: t('side_panel.active'), + friends: activeFriends.value + } + ]); + const userPickerGroups = computed(() => { const groups = []; @@ -156,7 +179,7 @@ }); } - const addFriendGroup = (key, label, friends) => { + const addFriendGroup = ({ key, label, friends }) => { if (!friends?.length) return; groups.push({ key, @@ -174,10 +197,7 @@ }); }; - addFriendGroup('friendsInInstance', t('dialog.invite.friends_in_instance'), props.inviteDialog?.friendsInInstance); - addFriendGroup('vip', t('side_panel.favorite'), vipFriends.value); - addFriendGroup('online', t('side_panel.online'), onlineFriends.value); - addFriendGroup('active', t('side_panel.active'), activeFriends.value); + friendSections.value.forEach(addFriendGroup); return groups; }); @@ -194,10 +214,11 @@ const friendById = computed(() => { const map = new Map(); - for (const friend of props.inviteDialog?.friendsInInstance ?? []) map.set(friend.id, friend); - for (const friend of vipFriends.value) map.set(friend.id, friend); - for (const friend of onlineFriends.value) map.set(friend.id, friend); - for (const friend of activeFriends.value) map.set(friend.id, friend); + for (const section of friendSections.value) { + for (const friend of section.friends ?? []) { + map.set(friend.id, friend); + } + } return map; }); diff --git a/src/components/dialogs/InviteGroupDialog.vue b/src/components/dialogs/InviteGroupDialog.vue index f1aef59b..20af7049 100644 --- a/src/components/dialogs/InviteGroupDialog.vue +++ b/src/components/dialogs/InviteGroupDialog.vue @@ -142,12 +142,36 @@ } ]); + const friendSections = computed(() => [ + { + key: 'vip', + label: t('side_panel.favorite'), + friends: vipFriends.value + }, + { + key: 'online', + label: t('side_panel.online'), + friends: onlineFriends.value + }, + { + key: 'active', + label: t('side_panel.active'), + friends: activeFriends.value + }, + { + key: 'offline', + label: t('side_panel.offline'), + friends: offlineFriends.value + } + ]); + const friendById = computed(() => { const map = new Map(); - for (const friend of vipFriends.value) map.set(friend.id, friend); - for (const friend of onlineFriends.value) map.set(friend.id, friend); - for (const friend of activeFriends.value) map.set(friend.id, friend); - for (const friend of offlineFriends.value) map.set(friend.id, friend); + for (const section of friendSections.value) { + for (const friend of section.friends ?? []) { + map.set(friend.id, friend); + } + } return map; }); @@ -190,7 +214,7 @@ }); } - const addFriendGroup = (key, label, friends) => { + const addFriendGroup = ({ key, label, friends }) => { if (!friends?.length) return; groups.push({ key, @@ -208,10 +232,7 @@ }); }; - addFriendGroup('vip', t('side_panel.favorite'), vipFriends.value); - addFriendGroup('online', t('side_panel.online'), onlineFriends.value); - addFriendGroup('active', t('side_panel.active'), activeFriends.value); - addFriendGroup('offline', t('side_panel.offline'), offlineFriends.value); + friendSections.value.forEach(addFriendGroup); return groups; }); diff --git a/src/components/dialogs/NewInstanceDialog/NewInstanceDialog.vue b/src/components/dialogs/NewInstanceDialog/NewInstanceDialog.vue index bf869a48..8ed90469 100644 --- a/src/components/dialogs/NewInstanceDialog/NewInstanceDialog.vue +++ b/src/components/dialogs/NewInstanceDialog/NewInstanceDialog.vue @@ -675,12 +675,36 @@ return names.slice(0, 3).join(', ') + (names.length > 3 ? ` +${names.length - 3}` : ''); }); + const friendSections = computed(() => [ + { + key: 'vip', + label: t('side_panel.favorite'), + friends: vipFriends.value + }, + { + key: 'online', + label: t('side_panel.online'), + friends: onlineFriends.value + }, + { + key: 'active', + label: t('side_panel.active'), + friends: activeFriends.value + }, + { + key: 'offline', + label: t('side_panel.offline'), + friends: offlineFriends.value + } + ]); + const friendById = computed(() => { const map = new Map(); - for (const friend of vipFriends.value) map.set(friend.id, friend); - for (const friend of onlineFriends.value) map.set(friend.id, friend); - for (const friend of activeFriends.value) map.set(friend.id, friend); - for (const friend of offlineFriends.value) map.set(friend.id, friend); + for (const section of friendSections.value) { + for (const friend of section.friends ?? []) { + map.set(friend.id, friend); + } + } return map; }); @@ -714,7 +738,7 @@ }); } - const addFriendGroup = (key, label, friends) => { + const addFriendGroup = ({ key, label, friends }) => { if (!friends?.length) return; groups.push({ key, @@ -732,10 +756,7 @@ }); }; - addFriendGroup('vip', t('side_panel.favorite'), vipFriends.value); - addFriendGroup('online', t('side_panel.online'), onlineFriends.value); - addFriendGroup('active', t('side_panel.active'), activeFriends.value); - addFriendGroup('offline', t('side_panel.offline'), offlineFriends.value); + friendSections.value.forEach(addFriendGroup); return groups; }); diff --git a/src/coordinators/friendPresenceCoordinator.js b/src/coordinators/friendPresenceCoordinator.js index d22059a9..79b2e06f 100644 --- a/src/coordinators/friendPresenceCoordinator.js +++ b/src/coordinators/friendPresenceCoordinator.js @@ -121,6 +121,7 @@ export async function runUpdateFriendDelayedCheckFlow( syncFriendSearchIndex(ctx); } ctx.isVIP = isVIP; + friendStore.reindexSortedFriend(ctx); } /** @@ -209,6 +210,7 @@ export async function runUpdateFriendFlow( ctx.name = ref.displayName; syncFriendSearchIndex(ctx); } + friendStore.reindexSortedFriend(ctx); return; } if ( @@ -248,6 +250,7 @@ export async function runUpdateFriendFlow( previousLocationAt: $location_at }); ctx.pendingOffline = true; + friendStore.reindexSortedFriend(ctx); return; } ctx.ref = ref; @@ -262,6 +265,8 @@ export async function runUpdateFriendFlow( $location_at, { now, nowIso } ); + } else { + friendStore.reindexSortedFriend(ctx); } } @@ -309,4 +314,3 @@ export async function runPendingOfflineTickFlow({ } } } - diff --git a/src/coordinators/userCoordinator.js b/src/coordinators/userCoordinator.js index 868bbd1b..7c3f9a82 100644 --- a/src/coordinators/userCoordinator.js +++ b/src/coordinators/userCoordinator.js @@ -184,6 +184,7 @@ export function applyUser(json) { friendCtx.ref = ref; friendCtx.name = ref.displayName; syncFriendSearchIndex(friendCtx); + friendStore.reindexSortedFriend(friendCtx); } if (ref.id === currentUser.id) { if (ref.status) { diff --git a/src/stores/friend.js b/src/stores/friend.js index 4e40c9b0..a6056451 100644 --- a/src/stores/friend.js +++ b/src/stores/friend.js @@ -1,4 +1,4 @@ -import { computed, reactive, ref, watch } from 'vue'; +import { computed, reactive, ref, shallowRef, watch } from 'vue'; import { defineStore } from 'pinia'; import { useRouter } from 'vue-router'; @@ -56,6 +56,63 @@ export const useFriendStore = defineStore('Friend', () => { const friends = reactive(new Map()); const localFavoriteFriends = reactive(new Set()); + const sortedFriends = shallowRef([]); + let sortedFriendsBatchDepth = 0; + let pendingSortedFriendsRebuild = false; + + const derivedDebugCounters = reactive({ + allFavoriteFriendIds: 0, + allFavoriteOnlineFriends: 0, + vipFriends: 0, + onlineFriends: 0, + activeFriends: 0, + offlineFriends: 0, + friendsInSameInstance: 0 + }); + + /** + * Tracks recomputes for the hottest friend-derived lists. + * Guarded by AppDebug.debugFriendState so normal behavior stays unchanged. + * @param {keyof typeof derivedDebugCounters} name + * @param {number} resultSize + */ + function trackDerivedDebug(name, resultSize) { + derivedDebugCounters[name] += 1; + if (!AppDebug.debugFriendState) { + return; + } + console.log('[friendStore derived]', { + name, + count: derivedDebugCounters[name], + resultSize, + friendCount: friends.size, + sortMethods: appearanceSettingsStore.sidebarSortMethods + }); + } + + /** + * + */ + function resetDerivedDebugCounters() { + for (const key in derivedDebugCounters) { + derivedDebugCounters[key] = 0; + } + if (AppDebug.debugFriendState) { + console.log('[friendStore derived] counters reset'); + } + } + + /** + * + * @returns {Record} + */ + function getDerivedDebugCounters() { + const snapshot = { ...derivedDebugCounters }; + if (AppDebug.debugFriendState) { + console.log('[friendStore derived] counters snapshot', snapshot); + } + return snapshot; + } const allFavoriteFriendIds = computed(() => { const favoriteStore = useFavoriteStore(); @@ -73,20 +130,131 @@ export const useFriendStore = defineStore('Friend', () => { } } } + trackDerivedDebug('allFavoriteFriendIds', set.size); return set; }); + /** + * + * @returns {(a: object, b: object) => number} + */ + function getSortedFriendsComparator() { + return getFriendsSortFunction(appearanceSettingsStore.sidebarSortMethods); + } + + /** + * + * @param {string} id + * @returns {number} + */ + function findSortedFriendIndex(id) { + return sortedFriends.value.findIndex((friend) => friend.id === id); + } + + /** + * + */ + function rebuildSortedFriends() { + sortedFriends.value = Array.from(friends.values()).sort( + getSortedFriendsComparator() + ); + pendingSortedFriendsRebuild = false; + } + + /** + * + */ + function beginSortedFriendsBatch() { + sortedFriendsBatchDepth += 1; + } + + /** + * + */ + function endSortedFriendsBatch() { + if (sortedFriendsBatchDepth === 0) { + return; + } + sortedFriendsBatchDepth -= 1; + if (sortedFriendsBatchDepth === 0 && pendingSortedFriendsRebuild) { + rebuildSortedFriends(); + } + } + + /** + * + * @template T + * @param {() => T} fn + * @returns {T} + */ + function runInSortedFriendsBatch(fn) { + beginSortedFriendsBatch(); + try { + return fn(); + } finally { + endSortedFriendsBatch(); + } + } + + /** + * + * @param {string} id + */ + function removeSortedFriend(id) { + if (sortedFriendsBatchDepth > 0) { + pendingSortedFriendsRebuild = true; + return; + } + const index = findSortedFriendIndex(id); + if (index === -1) { + return; + } + const next = sortedFriends.value.slice(); + next.splice(index, 1); + sortedFriends.value = next; + } + + /** + * + * @param {object | string} input + */ + function reindexSortedFriend(input) { + const ctx = + typeof input === 'string' ? friends.get(input) : input; + if (!ctx) { + return; + } + if (sortedFriendsBatchDepth > 0) { + pendingSortedFriendsRebuild = true; + return; + } + const compare = getSortedFriendsComparator(); + const next = sortedFriends.value.slice(); + const existingIndex = next.findIndex((friend) => friend.id === ctx.id); + if (existingIndex !== -1) { + next.splice(existingIndex, 1); + } + let low = 0; + let high = next.length; + while (low < high) { + const mid = Math.floor((low + high) / 2); + if (compare(next[mid], ctx) <= 0) { + low = mid + 1; + } else { + high = mid; + } + } + next.splice(low, 0, ctx); + sortedFriends.value = next; + } + const allFavoriteOnlineFriends = computed(() => { - return Array.from(friends.values()) - .filter( - (f) => - f.state === 'online' && allFavoriteFriendIds.value.has(f.id) - ) - .sort( - getFriendsSortFunction( - appearanceSettingsStore.sidebarSortMethods - ) - ); + const favoriteIds = allFavoriteFriendIds.value; + const result = sortedFriends.value.filter( + (f) => f.state === 'online' && favoriteIds.has(f.id) + ); + trackDerivedDebug('allFavoriteOnlineFriends', result.length); + return result; }); const isRefreshFriendsLoading = ref(false); @@ -131,50 +299,42 @@ export const useFriendStore = defineStore('Friend', () => { ); const vipFriends = computed(() => { - return Array.from(friends.values()) - .filter((f) => f.state === 'online' && f.isVIP) - .sort( - getFriendsSortFunction( - appearanceSettingsStore.sidebarSortMethods - ) - ); + const result = sortedFriends.value.filter( + (f) => f.state === 'online' && f.isVIP + ); + trackDerivedDebug('vipFriends', result.length); + return result; }); const onlineFriends = computed(() => { - return Array.from(friends.values()) - .filter((f) => f.state === 'online' && !f.isVIP) - .sort( - getFriendsSortFunction( - appearanceSettingsStore.sidebarSortMethods - ) - ); + const result = sortedFriends.value.filter( + (f) => f.state === 'online' && !f.isVIP + ); + trackDerivedDebug('onlineFriends', result.length); + return result; }); const activeFriends = computed(() => { - return Array.from(friends.values()) - .filter((f) => f.state === 'active') - .sort( - getFriendsSortFunction( - appearanceSettingsStore.sidebarSortMethods - ) - ); + const result = sortedFriends.value.filter((f) => f.state === 'active'); + trackDerivedDebug('activeFriends', result.length); + return result; }); const offlineFriends = computed(() => { - return Array.from(friends.values()) - .filter((f) => f.state === 'offline' || !f.state) - .sort( - getFriendsSortFunction( - appearanceSettingsStore.sidebarSortMethods - ) - ); + const result = sortedFriends.value.filter( + (f) => f.state === 'offline' || !f.state + ); + trackDerivedDebug('offlineFriends', result.length); + return result; }); const friendsInSameInstance = computed(() => { const friendsList = {}; - const allFriends = [...vipFriends.value, ...onlineFriends.value]; - allFriends.forEach((friend) => { + sortedFriends.value.forEach((friend) => { + if (friend.state !== 'online') { + return; + } if (!friend.ref?.$location) { return; } @@ -200,23 +360,22 @@ export const useFriendStore = defineStore('Friend', () => { const sortedFriendsList = []; for (const group of Object.values(friendsList)) { if (group.length > 1) { - sortedFriendsList.push( - group.sort( - getFriendsSortFunction( - appearanceSettingsStore.sidebarSortMethods - ) - ) - ); + // Group order already matches the globally sorted online list. + sortedFriendsList.push(group); } } - return sortedFriendsList.sort((a, b) => b.length - a.length); + const result = sortedFriendsList.sort((a, b) => b.length - a.length); + trackDerivedDebug('friendsInSameInstance', result.length); + return result; }); watch( () => watchState.isLoggedIn, (isLoggedIn) => { friends.clear(); + sortedFriends.value = []; + pendingSortedFriendsRebuild = false; state.friendNumber = 0; friendLog.clear(); friendLogTable.value.data = []; @@ -236,6 +395,14 @@ export const useFriendStore = defineStore('Friend', () => { { flush: 'sync' } ); + watch( + () => appearanceSettingsStore.sidebarSortMethods, + () => { + rebuildSortedFriends(); + }, + { deep: true } + ); + watch( () => watchState.isFriendsLoaded, (isFriendsLoaded) => { @@ -293,13 +460,16 @@ export const useFriendStore = defineStore('Friend', () => { * */ function updateSidebarFavorites() { - for (const ctx of friends.values()) { - const isVIP = localFavoriteFriends.has(ctx.id); - if (ctx.isVIP === isVIP) { - continue; + runInSortedFriendsBatch(() => { + for (const ctx of friends.values()) { + const isVIP = localFavoriteFriends.has(ctx.id); + if (ctx.isVIP === isVIP) { + continue; + } + ctx.isVIP = isVIP; + reindexSortedFriend(ctx); } - ctx.isVIP = isVIP; - } + }); } /** @@ -320,6 +490,7 @@ export const useFriendStore = defineStore('Friend', () => { return; } friends.delete(id); + removeSortedFriend(id); } /** @@ -327,38 +498,40 @@ export const useFriendStore = defineStore('Friend', () => { * @param ref */ function refreshFriendsStatus(ref) { - let id; - const map = new Map(); - for (id of ref.friends) { - map.set(id, 'offline'); - } - for (id of ref.offlineFriends) { - map.set(id, 'offline'); - } - for (id of ref.activeFriends) { - map.set(id, 'active'); - } - 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); + return runInSortedFriendsBatch(() => { + let id; + const map = new Map(); + for (id of ref.friends) { + map.set(id, 'offline'); } - } - for (id of friends.keys()) { - if (map.has(id) === false) { - deleteFriend(id); - removed.push(id); + for (id of ref.offlineFriends) { + map.set(id, 'offline'); } - } - return { added, removed }; + for (id of ref.activeFriends) { + map.set(id, 'active'); + } + 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 }; + }); } /** @@ -408,6 +581,29 @@ export const useFriendStore = defineStore('Friend', () => { ctx.name = ref.name; } friends.set(id, ctx); + watchState.isLoggedIn = true + // Startup fill flow: + // + // login + // -> runInitFriendsListFlow() + // -> initFriendLog() / getFriendLog() + // -> refreshFriendsStatus(currentUser) + // -> addFriend(...) + // -> friends.set(id, ctx) + // -> reindexSortedFriend(ctx) + // + // During batch init, reindexSortedFriend() only marks the list dirty. + // When the batch ends: + // -> rebuildSortedFriends() + // -> sortedFriends = sorted(Array.from(friends.values())) + // + // After full friend payloads arrive: + // -> applyUser(friend) + // -> update ctx.ref / ctx.name + // -> reindexSortedFriend(ctx) + // -> batch end + // -> rebuildSortedFriends() + reindexSortedFriend(ctx); } /** @@ -672,14 +868,17 @@ export const useFriendStore = defineStore('Friend', () => { friend.displayName = item.displayName; friendListMap.set(item.userId, friend); } - for (item of friendListMap.values()) { - ref = friends.get(item.userId); - if (ref?.ref) { - ref.ref.$joinCount = item.joinCount; - ref.ref.$lastSeen = item.lastSeen; - ref.ref.$timeSpent = item.timeSpent; + runInSortedFriendsBatch(() => { + for (item of friendListMap.values()) { + ref = friends.get(item.userId); + if (ref?.ref) { + ref.ref.$joinCount = item.joinCount; + ref.ref.$lastSeen = item.lastSeen; + ref.ref.$timeSpent = item.timeSpent; + reindexSortedFriend(ref); + } } - } + }); } /** @@ -687,12 +886,15 @@ export const useFriendStore = defineStore('Friend', () => { */ async function getAllUserMutualCount() { const mutualCountMap = await database.getMutualCountForAllUsers(); - for (const [userId, mutualCount] of mutualCountMap.entries()) { - const ref = friends.get(userId); - if (ref?.ref) { - ref.ref.$mutualCount = mutualCount; + runInSortedFriendsBatch(() => { + for (const [userId, mutualCount] of mutualCountMap.entries()) { + const ref = friends.get(userId); + if (ref?.ref) { + ref.ref.$mutualCount = mutualCount; + reindexSortedFriend(ref); + } } - } + }); } /** @@ -714,17 +916,19 @@ export const useFriendStore = defineStore('Friend', () => { refreshFriendsStatus(currentUser); const sqlValues = []; const friends = await refreshFriends(); - for (const friend of friends) { - const ref = applyUser(friend); - const row = { - userId: ref.id, - displayName: ref.displayName, - trustLevel: ref.$trustLevel, - friendNumber: 0 - }; - friendLog.set(friend.id, row); - sqlValues.unshift(row); - } + runInSortedFriendsBatch(() => { + for (const friend of friends) { + const ref = applyUser(friend); + const row = { + userId: ref.id, + displayName: ref.displayName, + trustLevel: ref.$trustLevel, + friendNumber: 0 + }; + friendLog.set(friend.id, row); + sqlValues.unshift(row); + } + }); database.setFriendLogCurrentArray(sqlValues); await configRepository.setBool(`friendLogInit_${currentUser.id}`, true); watchState.isFriendsLoaded = true; @@ -808,6 +1012,7 @@ export const useFriendStore = defineStore('Friend', () => { const friendRef = friends.get(userId); if (friendRef?.ref) { friendRef.ref.$friendNumber = friendNumber; + reindexSortedFriend(friendRef); } } @@ -830,11 +1035,13 @@ export const useFriendStore = defineStore('Friend', () => { } const friendOrder = userStore.currentUser.friends; - for (let i = 0; i < friendOrder.length; i++) { - const userId = friendOrder[i]; - state.friendNumber++; - setFriendNumber(state.friendNumber, userId); - } + runInSortedFriendsBatch(() => { + for (let i = 0; i < friendOrder.length; i++) { + const userId = friendOrder[i]; + state.friendNumber++; + setFriendNumber(state.friendNumber, userId); + } + }); if (state.friendNumber === 0) { state.friendNumber = friends.size; } @@ -1179,6 +1386,9 @@ export const useFriendStore = defineStore('Friend', () => { getFriendLog, tryApplyFriendOrder, resetFriendLog, + reindexSortedFriend, + resetDerivedDebugCounters, + getDerivedDebugCounters, initFriendLogHistoryTable, setIsRefreshFriendsLoading }; diff --git a/src/views/FriendsLocations/FriendsLocations.vue b/src/views/FriendsLocations/FriendsLocations.vue index 29e9644d..a1767307 100644 --- a/src/views/FriendsLocations/FriendsLocations.vue +++ b/src/views/FriendsLocations/FriendsLocations.vue @@ -371,10 +371,16 @@ return ids; }); - const vipFriendsByGroupStatus = computed(() => { + const visibleFavoriteOnlineFriends = computed(() => { const selectedGroups = sidebarFavoriteGroups.value; - if (selectedGroups.length === 0) return allFavoriteOnlineFriends.value; - return allFavoriteOnlineFriends.value.filter((f) => displayedVipIds.value.has(f.id)); + if (selectedGroups.length === 0) { + return allFavoriteOnlineFriends.value; + } + return allFavoriteOnlineFriends.value.filter((friend) => displayedVipIds.value.has(friend.id)); + }); + + const vipFriendsByGroupStatus = computed(() => { + return visibleFavoriteOnlineFriends.value; }); const onlineFriendsByGroupStatus = computed(() => { @@ -382,10 +388,11 @@ if (selectedGroups.length === 0) { return onlineFriends.value.filter((f) => !allFavoriteFriendIds.value.has(f.id)); } - const nonFavOnline = onlineFriends.value.filter((f) => !displayedVipIds.value.has(f.id)); + const selectedIds = displayedVipIds.value; + const nonFavOnline = onlineFriends.value.filter((f) => !selectedIds.has(f.id)); const existingIds = new Set(nonFavOnline.map((f) => f.id)); const unselectedGroupFriends = allFavoriteOnlineFriends.value.filter( - (f) => !displayedVipIds.value.has(f.id) && !existingIds.has(f.id) + (f) => !selectedIds.has(f.id) && !existingIds.has(f.id) ); return [...nonFavOnline, ...unselectedGroupFriends].sort(getFriendsSortFunction(sidebarSortMethods.value)); }); @@ -415,7 +422,7 @@ const result = []; for (const { key, groupName, memberIds } of groups) { - const filteredFriends = allFavoriteOnlineFriends.value.filter((friend) => memberIds.has(friend.id)); + const filteredFriends = visibleFavoriteOnlineFriends.value.filter((friend) => memberIds.has(friend.id)); if (filteredFriends.length > 0) { result.push({ key, groupName, friends: filteredFriends }); } @@ -432,6 +439,15 @@ }); }); + const searchableEntries = computed(() => + uniqueEntries([ + ...toEntries(allFavoriteOnlineFriends.value), + ...toEntries(onlineFriends.value), + ...toEntries(activeFriends.value), + ...toEntries(offlineFriends.value) + ]) + ); + /** * * @param groupKey @@ -446,14 +462,7 @@ const filteredFriends = computed(() => { if (normalizedSearchTerm.value) { - const pools = [ - ...toEntries(allFavoriteOnlineFriends.value), - ...toEntries(onlineFriends.value), - ...toEntries(activeFriends.value), - ...toEntries(offlineFriends.value) - ]; - - return uniqueEntries(pools).filter(({ friend }) => { + return searchableEntries.value.filter(({ friend }) => { const haystack = `${friend.displayName ?? friend.name ?? ''} ${friend.signature ?? ''} ${friend.worldName ?? ''}`.toLowerCase(); return haystack.includes(normalizedSearchTerm.value); diff --git a/src/views/Sidebar/components/FriendsSidebar.vue b/src/views/Sidebar/components/FriendsSidebar.vue index 21e27e01..4ad369ee 100644 --- a/src/views/Sidebar/components/FriendsSidebar.vue +++ b/src/views/Sidebar/components/FriendsSidebar.vue @@ -275,6 +275,36 @@ const shouldHideSameInstance = computed(() => isSidebarGroupByInstance.value && isHideFriendsInSameInstance.value); + const selectedFavoriteGroupIds = computed(() => { + const selectedGroups = sidebarFavoriteGroups.value; + const hasFilter = selectedGroups.length > 0; + if (!hasFilter) { + return allFavoriteFriendIds.value; + } + + const ids = new Set(); + const remoteFriendsByGroup = groupedByGroupKeyFavoriteFriends.value; + for (const key of selectedGroups) { + if (key.startsWith('local:')) { + const groupName = key.slice(6); + const userIds = localFriendFavorites.value?.[groupName]; + if (userIds) { + for (const id of userIds) ids.add(id); + } + } else if (remoteFriendsByGroup[key]) { + for (const friend of remoteFriendsByGroup[key]) ids.add(friend.id); + } + } + return ids; + }); + + const visibleFavoriteOnlineFriends = computed(() => { + const filtered = allFavoriteOnlineFriends.value.filter((friend) => + selectedFavoriteGroupIds.value.has(friend.id) + ); + return excludeSameInstance(filtered); + }); + /** * * @param list @@ -293,23 +323,11 @@ return excludeSameInstance(onlineFriends.value.filter((f) => !allFavoriteFriendIds.value.has(f.id))); } // When group filter is active, friends in unselected groups should appear in the online list - const displayedVipIds = new Set(); - const remoteFriendsByGroup = groupedByGroupKeyFavoriteFriends.value; - for (const key of selectedGroups) { - if (key.startsWith('local:')) { - const groupName = key.slice(6); - const userIds = localFriendFavorites.value?.[groupName]; - if (userIds) { - for (const id of userIds) displayedVipIds.add(id); - } - } else if (remoteFriendsByGroup[key]) { - for (const f of remoteFriendsByGroup[key]) displayedVipIds.add(f.id); - } - } - const nonFavOnline = onlineFriends.value.filter((f) => !displayedVipIds.has(f.id)); + const selectedIds = selectedFavoriteGroupIds.value; + const nonFavOnline = onlineFriends.value.filter((f) => !selectedIds.has(f.id)); const existingIds = new Set(nonFavOnline.map((f) => f.id)); const unselectedGroupFriends = allFavoriteOnlineFriends.value.filter( - (f) => !displayedVipIds.has(f.id) && !existingIds.has(f.id) + (f) => !selectedIds.has(f.id) && !existingIds.has(f.id) ); return excludeSameInstance( [...nonFavOnline, ...unselectedGroupFriends].sort(getFriendsSortFunction(sidebarSortMethods.value)) @@ -317,26 +335,7 @@ }); const vipFriendsByGroupStatus = computed(() => { - const selectedGroups = sidebarFavoriteGroups.value; - const hasFilter = selectedGroups.length > 0; - if (!hasFilter) { - return excludeSameInstance(allFavoriteOnlineFriends.value); - } - // Filter to only include VIP friends whose group key is in selectedGroups - const allowedIds = new Set(); - const remoteFriendsByGroup = groupedByGroupKeyFavoriteFriends.value; - for (const key of selectedGroups) { - if (key.startsWith('local:')) { - const groupName = key.slice(6); - const userIds = localFriendFavorites.value?.[groupName]; - if (userIds) { - for (const id of userIds) allowedIds.add(id); - } - } else if (remoteFriendsByGroup[key]) { - for (const f of remoteFriendsByGroup[key]) allowedIds.add(f.id); - } - } - return excludeSameInstance(allFavoriteOnlineFriends.value.filter((f) => allowedIds.has(f.id))); + return visibleFavoriteOnlineFriends.value; }); // VIP friends divide by group @@ -369,9 +368,7 @@ // Filter vipFriends per group, preserving vipFriends sort order const result = []; for (const { key, groupName, memberIds } of groups) { - const filteredFriends = excludeSameInstance( - allFavoriteOnlineFriends.value.filter((friend) => memberIds.has(friend.id)) - ); + const filteredFriends = visibleFavoriteOnlineFriends.value.filter((friend) => memberIds.has(friend.id)); if (filteredFriends.length > 0) { result.push(filteredFriends.map((item) => ({ groupName, key, ...item }))); }