mirror of
https://github.com/MrUnknownDE/VRCX.git
synced 2026-04-19 06:43:51 +02:00
improve performance and clean up
This commit is contained in:
@@ -289,7 +289,6 @@ export function showUserDialog(userId) {
|
||||
const moderationStore = useModerationStore();
|
||||
const favoriteStore = useFavoriteStore();
|
||||
const locationStore = useLocationStore();
|
||||
const searchStore = useSearchStore();
|
||||
const appearanceSettingsStore = useAppearanceSettingsStore();
|
||||
const t = i18n.global.t;
|
||||
|
||||
@@ -530,7 +529,6 @@ export function showUserDialog(userId) {
|
||||
});
|
||||
showUserDialogHistory.delete(userId);
|
||||
showUserDialogHistory.add(userId);
|
||||
searchStore.setQuickSearchItems(searchStore.quickSearchUserHistory());
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -235,6 +235,7 @@
|
||||
"same_instance": "Same instance",
|
||||
"active": "Active",
|
||||
"offline": "Offline",
|
||||
"search_placeholder": "Search friends",
|
||||
"spacing": "Spacing",
|
||||
"scale": "Scale",
|
||||
"separate_same_instance_friends": "Separate Same Instance Friends",
|
||||
|
||||
@@ -2,37 +2,26 @@ import { computed, ref, watch } from 'vue';
|
||||
import { defineStore } from 'pinia';
|
||||
import { toast } from 'vue-sonner';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import { useRouter } from 'vue-router';
|
||||
|
||||
import { compareByName, localeIncludes } from '../shared/utils';
|
||||
import { instanceRequest, userRequest } from '../api';
|
||||
import { groupRequest } from '../api/';
|
||||
import removeConfusables, { removeWhitespace } from '../services/confusables';
|
||||
import { useAppearanceSettingsStore } from './settings/appearance';
|
||||
import { useFriendStore } from './friend';
|
||||
import { showGroupDialog } from '../coordinators/groupCoordinator';
|
||||
import { showWorldDialog } from '../coordinators/worldCoordinator';
|
||||
import { showAvatarDialog } from '../coordinators/avatarCoordinator';
|
||||
import {
|
||||
applyUser,
|
||||
showUserDialog,
|
||||
lookupUser
|
||||
} from '../coordinators/userCoordinator';
|
||||
import { applyUser, showUserDialog } from '../coordinators/userCoordinator';
|
||||
import { useModalStore } from './modal';
|
||||
import { useUserStore } from './user';
|
||||
import { watchState } from '../services/watchState';
|
||||
|
||||
export const useSearchStore = defineStore('Search', () => {
|
||||
const userStore = useUserStore();
|
||||
const router = useRouter();
|
||||
const appearanceSettingsStore = useAppearanceSettingsStore();
|
||||
const friendStore = useFriendStore();
|
||||
const modalStore = useModalStore();
|
||||
const { t } = useI18n();
|
||||
|
||||
const searchText = ref('');
|
||||
const searchUserResults = ref([]);
|
||||
const quickSearchItems = ref([]);
|
||||
const friendsListSearch = ref('');
|
||||
|
||||
const directAccessPrompt = ref(null);
|
||||
@@ -65,13 +54,6 @@ export const useSearchStore = defineStore('Search', () => {
|
||||
searchText.value = value;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Array} value
|
||||
*/
|
||||
function setQuickSearchItems(value) {
|
||||
quickSearchItems.value = value;
|
||||
}
|
||||
|
||||
async function searchUserByDisplayName(displayName) {
|
||||
const params = {
|
||||
n: 10,
|
||||
@@ -110,133 +92,6 @@ export const useSearchStore = defineStore('Search', () => {
|
||||
});
|
||||
}
|
||||
|
||||
function quickSearchRemoteMethod(query) {
|
||||
if (!query) {
|
||||
quickSearchItems.value = quickSearchUserHistory();
|
||||
return;
|
||||
}
|
||||
|
||||
if (query.length < 2) {
|
||||
quickSearchItems.value = quickSearchUserHistory();
|
||||
return;
|
||||
}
|
||||
|
||||
const results = [];
|
||||
const cleanQuery = removeWhitespace(query);
|
||||
if (!cleanQuery) {
|
||||
quickSearchItems.value = quickSearchUserHistory();
|
||||
return;
|
||||
}
|
||||
|
||||
for (const ctx of friendStore.friends.values()) {
|
||||
if (typeof ctx.ref === 'undefined') {
|
||||
continue;
|
||||
}
|
||||
|
||||
const cleanName = removeConfusables(ctx.name);
|
||||
let match = localeIncludes(
|
||||
cleanName,
|
||||
cleanQuery,
|
||||
stringComparer.value
|
||||
);
|
||||
if (!match) {
|
||||
// Also check regular name in case search is with special characters
|
||||
match = localeIncludes(
|
||||
ctx.name,
|
||||
cleanQuery,
|
||||
stringComparer.value
|
||||
);
|
||||
}
|
||||
// Use query with whitespace for notes and memos as people are more
|
||||
// likely to include spaces in memos and notes
|
||||
if (!match && ctx.memo) {
|
||||
match = localeIncludes(ctx.memo, query, stringComparer.value);
|
||||
}
|
||||
if (!match && ctx.ref.note) {
|
||||
match = localeIncludes(
|
||||
ctx.ref.note,
|
||||
query,
|
||||
stringComparer.value
|
||||
);
|
||||
}
|
||||
|
||||
if (match) {
|
||||
results.push({
|
||||
value: ctx.id,
|
||||
label: ctx.name,
|
||||
ref: ctx.ref,
|
||||
name: ctx.name
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
results.sort(function (a, b) {
|
||||
const A =
|
||||
stringComparer.value.compare(
|
||||
a.name.substring(0, cleanQuery.length),
|
||||
cleanQuery
|
||||
) === 0;
|
||||
const B =
|
||||
stringComparer.value.compare(
|
||||
b.name.substring(0, cleanQuery.length),
|
||||
cleanQuery
|
||||
) === 0;
|
||||
if (A && !B) {
|
||||
return -1;
|
||||
} else if (B && !A) {
|
||||
return 1;
|
||||
}
|
||||
return compareByName(a, b);
|
||||
});
|
||||
if (results.length > 4) {
|
||||
results.length = 4;
|
||||
}
|
||||
results.push({
|
||||
value: `search:${query}`,
|
||||
label: query
|
||||
});
|
||||
|
||||
quickSearchItems.value = results;
|
||||
}
|
||||
|
||||
function quickSearchChange(value) {
|
||||
if (!value) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (value.startsWith('search:')) {
|
||||
const searchTerm = value.slice(7);
|
||||
if (quickSearchItems.value.length > 1 && searchTerm.length) {
|
||||
friendsListSearch.value = searchTerm;
|
||||
router.push({ name: 'friend-list' });
|
||||
} else {
|
||||
router.push({ name: 'search' });
|
||||
searchText.value = searchTerm;
|
||||
lookupUser({ displayName: searchTerm });
|
||||
}
|
||||
} else {
|
||||
showUserDialog(value);
|
||||
}
|
||||
}
|
||||
|
||||
function quickSearchUserHistory() {
|
||||
const userHistory = Array.from(userStore.showUserDialogHistory.values())
|
||||
.reverse()
|
||||
.slice(0, 5);
|
||||
const results = [];
|
||||
userHistory.forEach((userId) => {
|
||||
const ref = userStore.cachedUsers.get(userId);
|
||||
if (typeof ref !== 'undefined') {
|
||||
results.push({
|
||||
value: ref.id,
|
||||
label: ref.name,
|
||||
ref
|
||||
});
|
||||
}
|
||||
});
|
||||
return results;
|
||||
}
|
||||
|
||||
async function directAccessPaste() {
|
||||
let cbText = '';
|
||||
if (LINUX) {
|
||||
@@ -430,20 +285,15 @@ export const useSearchStore = defineStore('Search', () => {
|
||||
searchText,
|
||||
searchUserResults,
|
||||
stringComparer,
|
||||
quickSearchItems,
|
||||
friendsListSearch,
|
||||
|
||||
clearSearch,
|
||||
searchUserByDisplayName,
|
||||
moreSearchUser,
|
||||
quickSearchUserHistory,
|
||||
quickSearchRemoteMethod,
|
||||
quickSearchChange,
|
||||
directAccessParse,
|
||||
directAccessPaste,
|
||||
directAccessWorld,
|
||||
verifyShortName,
|
||||
setSearchText,
|
||||
setQuickSearchItems
|
||||
setSearchText
|
||||
};
|
||||
});
|
||||
|
||||
@@ -134,9 +134,7 @@
|
||||
useAppearanceSettingsStore,
|
||||
useFriendStore,
|
||||
useModalStore,
|
||||
useSearchStore,
|
||||
useUserStore,
|
||||
useVrcxStore
|
||||
useSearchStore
|
||||
} from '../../stores';
|
||||
import { friendRequest, userRequest } from '../../api';
|
||||
import { DataTableLayout } from '../../components/ui/data-table';
|
||||
@@ -261,6 +259,7 @@
|
||||
watch(
|
||||
() => route.path,
|
||||
() => {
|
||||
refreshFriendStats();
|
||||
nextTick(() => friendsListSearchChange());
|
||||
},
|
||||
{ immediate: true }
|
||||
@@ -269,10 +268,16 @@
|
||||
watch(
|
||||
() => friends.value.size,
|
||||
() => {
|
||||
refreshFriendStats();
|
||||
friendsListSearchChange();
|
||||
}
|
||||
);
|
||||
|
||||
function refreshFriendStats() {
|
||||
getAllUserStats();
|
||||
getAllUserMutualCount();
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
@@ -319,8 +324,6 @@
|
||||
results.push(ctx.ref);
|
||||
}
|
||||
friendsListDisplayData.value = results;
|
||||
getAllUserStats();
|
||||
getAllUserMutualCount();
|
||||
table.setPageIndex(0);
|
||||
table.setSorting([...defaultSorting]);
|
||||
sorting.value = [...defaultSorting];
|
||||
|
||||
@@ -312,6 +312,8 @@ describe('FriendList.vue', () => {
|
||||
|
||||
const wrapper = mount(FriendList);
|
||||
await flushAsync();
|
||||
expect(mocks.getAllUserStats).toHaveBeenCalledTimes(1);
|
||||
expect(mocks.getAllUserMutualCount).toHaveBeenCalledTimes(1);
|
||||
|
||||
wrapper.vm.friendsListSearchFilterVIP = true;
|
||||
wrapper.vm.friendsListSearchChange();
|
||||
@@ -320,8 +322,8 @@ describe('FriendList.vue', () => {
|
||||
expect(
|
||||
wrapper.vm.friendsListDisplayData.map((item) => item.id)
|
||||
).toEqual(['usr_1']);
|
||||
expect(mocks.getAllUserStats).toHaveBeenCalled();
|
||||
expect(mocks.getAllUserMutualCount).toHaveBeenCalled();
|
||||
expect(mocks.getAllUserStats).toHaveBeenCalledTimes(1);
|
||||
expect(mocks.getAllUserMutualCount).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
test('opens charts tab from toolbar button', async () => {
|
||||
|
||||
@@ -9,7 +9,10 @@
|
||||
</TabsList>
|
||||
</Tabs>
|
||||
<div class="friend-view__actions">
|
||||
<InputGroupSearch v-model="searchTerm" class="friend-view__search" placeholder="Search Friend" />
|
||||
<InputGroupSearch
|
||||
v-model="searchTerm"
|
||||
class="friend-view__search"
|
||||
:placeholder="t('view.friends_locations.search_placeholder')" />
|
||||
<TooltipWrapper :content="t('view.charts.instance_activity.settings.header')" side="top">
|
||||
<div>
|
||||
<Popover>
|
||||
@@ -151,7 +154,7 @@
|
||||
import { Slider } from '../../components/ui/slider';
|
||||
import { Switch } from '../../components/ui/switch';
|
||||
import { getFriendsLocations } from '../../shared/utils/location.js';
|
||||
import { getFriendsSortFunction } from '../../shared/utils';
|
||||
import { debounce, getFriendsSortFunction } from '../../shared/utils';
|
||||
|
||||
import FriendLocationCard from './components/FriendsLocationsCard.vue';
|
||||
import configRepository from '../../services/config.js';
|
||||
@@ -196,12 +199,18 @@
|
||||
|
||||
const cardScaleBase = ref(1);
|
||||
const cardSpacingBase = ref(1);
|
||||
const persistCardScale = debounce((value) => {
|
||||
configRepository.setString('VRCX_FriendLocationCardScale', value.toString());
|
||||
}, 200);
|
||||
const persistCardSpacing = debounce((value) => {
|
||||
configRepository.setString('VRCX_FriendLocationCardSpacing', value.toString());
|
||||
}, 200);
|
||||
|
||||
const cardScale = computed({
|
||||
get: () => cardScaleBase.value,
|
||||
set: (value) => {
|
||||
cardScaleBase.value = value;
|
||||
configRepository.setString('VRCX_FriendLocationCardScale', value.toString());
|
||||
persistCardScale(value);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -209,7 +218,7 @@
|
||||
get: () => cardSpacingBase.value,
|
||||
set: (value) => {
|
||||
cardSpacingBase.value = value;
|
||||
configRepository.setString('VRCX_FriendLocationCardSpacing', value.toString());
|
||||
persistCardSpacing(value);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -251,6 +260,8 @@
|
||||
|
||||
const scrollbarRef = ref();
|
||||
const gridWidth = ref(0);
|
||||
let measureScheduled = false;
|
||||
let pendingGridWidthUpdate = false;
|
||||
let resizeObserver;
|
||||
let cleanupResize;
|
||||
|
||||
@@ -314,6 +325,29 @@
|
||||
}))
|
||||
: [];
|
||||
|
||||
const getFriendIdentity = (friend) => friend?.id ?? friend?.userId ?? friend?.displayName ?? 'unknown';
|
||||
|
||||
const getEntryIdentity = (entry) => entry?.id ?? getFriendIdentity(entry?.friend);
|
||||
|
||||
const scheduleVirtualMeasure = ({ updateGridWidth: shouldUpdateGridWidth = false } = {}) => {
|
||||
pendingGridWidthUpdate = pendingGridWidthUpdate || shouldUpdateGridWidth;
|
||||
if (measureScheduled) {
|
||||
return;
|
||||
}
|
||||
|
||||
measureScheduled = true;
|
||||
nextTick(() => {
|
||||
measureScheduled = false;
|
||||
|
||||
if (pendingGridWidthUpdate) {
|
||||
pendingGridWidthUpdate = false;
|
||||
updateGridWidth();
|
||||
}
|
||||
|
||||
virtualizer.value?.measure?.();
|
||||
});
|
||||
};
|
||||
|
||||
const sameInstanceGroups = computed(() => {
|
||||
const source = friendsInSameInstance?.value;
|
||||
if (!Array.isArray(source) || source.length === 0) return [];
|
||||
@@ -379,10 +413,6 @@
|
||||
return allFavoriteOnlineFriends.value.filter((friend) => displayedVipIds.value.has(friend.id));
|
||||
});
|
||||
|
||||
const vipFriendsByGroupStatus = computed(() => {
|
||||
return visibleFavoriteOnlineFriends.value;
|
||||
});
|
||||
|
||||
const onlineFriendsByGroupStatus = computed(() => {
|
||||
const selectedGroups = sidebarFavoriteGroups.value;
|
||||
if (selectedGroups.length === 0) {
|
||||
@@ -501,7 +531,7 @@
|
||||
return toEntries(onlineFriendsByGroupStatus.value);
|
||||
}
|
||||
case 'favorite':
|
||||
return toEntries(vipFriendsByGroupStatus.value);
|
||||
return toEntries(visibleFavoriteOnlineFriends.value);
|
||||
case 'same-instance':
|
||||
return sameInstanceEntries.value;
|
||||
case 'active':
|
||||
@@ -554,18 +584,36 @@
|
||||
return buildSameInstanceGroups(filteredFriends.value);
|
||||
});
|
||||
|
||||
const mergedSameInstanceEntries = computed(() => {
|
||||
const mergedEntriesBySection = computed(() => {
|
||||
if (!shouldMergeSameInstance.value) {
|
||||
return [];
|
||||
return {
|
||||
sameInstance: [],
|
||||
online: []
|
||||
};
|
||||
}
|
||||
return filteredFriends.value.filter((entry) => entry.section === 'same-instance');
|
||||
|
||||
const sameInstance = [];
|
||||
const online = [];
|
||||
for (const entry of filteredFriends.value) {
|
||||
if (entry.section === 'same-instance') {
|
||||
sameInstance.push(entry);
|
||||
} else {
|
||||
online.push(entry);
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
sameInstance,
|
||||
online
|
||||
};
|
||||
});
|
||||
|
||||
const mergedSameInstanceEntries = computed(() => {
|
||||
return mergedEntriesBySection.value.sameInstance;
|
||||
});
|
||||
|
||||
const mergedOnlineEntries = computed(() => {
|
||||
if (!shouldMergeSameInstance.value) {
|
||||
return [];
|
||||
}
|
||||
return filteredFriends.value.filter((entry) => entry.section !== 'same-instance');
|
||||
return mergedEntriesBySection.value.online;
|
||||
});
|
||||
|
||||
const mergedSameInstanceGroups = computed(() => {
|
||||
@@ -575,61 +623,13 @@
|
||||
return buildSameInstanceGroups(mergedSameInstanceEntries.value);
|
||||
});
|
||||
|
||||
const gridStyle = computed(() => {
|
||||
const computeGridLayout = (count = 1, options = {}) => {
|
||||
const baseWidth = 220;
|
||||
const baseGap = 14;
|
||||
const scale = cardScale.value;
|
||||
const spacing = cardSpacing.value;
|
||||
const minWidth = baseWidth * scale;
|
||||
const gap = Math.max(6, (baseGap + (scale - 1) * 10) * spacing);
|
||||
|
||||
return (count = 1, options = {}) => {
|
||||
const containerWidth = Math.max(gridWidth.value ?? 0, 0);
|
||||
const itemCount = Math.max(Number(count) || 0, 0);
|
||||
const safeCount = itemCount > 0 ? itemCount : 1;
|
||||
const maxColumns = Math.max(1, Math.floor((containerWidth + gap) / (minWidth + gap)) || 1);
|
||||
const preferredColumns = options?.preferredColumns;
|
||||
const requestedColumns = preferredColumns
|
||||
? Math.max(1, Math.min(Math.round(preferredColumns), maxColumns))
|
||||
: maxColumns;
|
||||
const columns = Math.max(1, Math.min(safeCount, requestedColumns));
|
||||
const forceStretch = Boolean(options?.forceStretch);
|
||||
const disableAutoStretch = Boolean(options?.disableAutoStretch);
|
||||
const matchMaxColumnWidth = Boolean(options?.matchMaxColumnWidth);
|
||||
const shouldStretch = !disableAutoStretch && (forceStretch || itemCount >= maxColumns);
|
||||
|
||||
let cardWidth = minWidth;
|
||||
const maxColumnWidth = maxColumns > 0 ? (containerWidth - gap * (maxColumns - 1)) / maxColumns : minWidth;
|
||||
|
||||
if (shouldStretch && columns > 0) {
|
||||
const columnsWidth = containerWidth - gap * (columns - 1);
|
||||
const rawWidth = columnsWidth > 0 ? columnsWidth / columns : minWidth;
|
||||
|
||||
if (Number.isFinite(rawWidth) && rawWidth > 0) {
|
||||
cardWidth = Math.max(minWidth, rawWidth);
|
||||
}
|
||||
} else if (matchMaxColumnWidth && Number.isFinite(maxColumnWidth) && maxColumnWidth > 0) {
|
||||
cardWidth = Math.max(minWidth, maxColumnWidth);
|
||||
}
|
||||
|
||||
return {
|
||||
'--friend-card-min-width': `${Math.round(minWidth)}px`,
|
||||
'--friend-card-gap': `${Math.round(gap)}px`,
|
||||
'--friend-card-target-width': `${Math.round(cardWidth)}px`,
|
||||
'--friend-grid-columns': `${columns}`,
|
||||
'--friend-card-spacing': `${spacing.toFixed(2)}`
|
||||
};
|
||||
};
|
||||
});
|
||||
|
||||
const getGridMetrics = (count = 1, options = {}) => {
|
||||
const baseWidth = 220;
|
||||
const baseGap = 14;
|
||||
const scale = cardScale.value;
|
||||
const spacing = cardSpacing.value;
|
||||
const minWidth = baseWidth * scale;
|
||||
const gap = Math.max(6, (baseGap + (scale - 1) * 10) * spacing);
|
||||
|
||||
const containerWidth = Math.max(gridWidth.value ?? 0, 0);
|
||||
const itemCount = Math.max(Number(count) || 0, 0);
|
||||
const safeCount = itemCount > 0 ? itemCount : 1;
|
||||
@@ -666,12 +666,25 @@
|
||||
};
|
||||
};
|
||||
|
||||
const gridStyle = computed(() => {
|
||||
return (count = 1, options = {}) => {
|
||||
const { minWidth, gap, cardWidth, columns } = computeGridLayout(count, options);
|
||||
return {
|
||||
'--friend-card-min-width': `${Math.round(minWidth)}px`,
|
||||
'--friend-card-gap': `${Math.round(gap)}px`,
|
||||
'--friend-card-target-width': `${Math.round(cardWidth)}px`,
|
||||
'--friend-grid-columns': `${columns}`,
|
||||
'--friend-card-spacing': `${cardSpacing.value.toFixed(2)}`
|
||||
};
|
||||
};
|
||||
});
|
||||
|
||||
const chunkCardItems = (items = [], keyPrefix = 'row') => {
|
||||
const safeItems = Array.isArray(items) ? items : [];
|
||||
if (!safeItems.length) {
|
||||
return [];
|
||||
}
|
||||
const { columns } = getGridMetrics(safeItems.length, { matchMaxColumnWidth: true });
|
||||
const { columns } = computeGridLayout(safeItems.length, { matchMaxColumnWidth: true });
|
||||
const safeColumns = Math.max(1, columns || 1);
|
||||
const rows = [];
|
||||
|
||||
@@ -705,7 +718,7 @@
|
||||
const friends = Array.isArray(group.friends) ? group.friends : [];
|
||||
if (friends.length) {
|
||||
const items = friends.map((friend) => ({
|
||||
key: `f:${friend?.id ?? friend?.userId ?? friend?.displayName ?? Math.random()}`,
|
||||
key: `f:${getFriendIdentity(friend)}`,
|
||||
friend,
|
||||
displayInstanceInfo: true
|
||||
}));
|
||||
@@ -728,7 +741,7 @@
|
||||
const friends = Array.isArray(group.friends) ? group.friends : [];
|
||||
if (friends.length) {
|
||||
const items = friends.map((friend) => ({
|
||||
key: `f:${friend?.id ?? friend?.userId ?? friend?.displayName ?? Math.random()}`,
|
||||
key: `f:${getFriendIdentity(friend)}`,
|
||||
friend,
|
||||
displayInstanceInfo: false
|
||||
}));
|
||||
@@ -743,7 +756,7 @@
|
||||
const online = mergedOnlineEntries.value;
|
||||
if (online.length) {
|
||||
const items = online.map((entry) => ({
|
||||
key: `e:${entry?.id ?? entry?.friend?.id ?? entry?.friend?.displayName ?? Math.random()}`,
|
||||
key: `e:${getEntryIdentity(entry)}`,
|
||||
friend: entry.friend,
|
||||
displayInstanceInfo: true
|
||||
}));
|
||||
@@ -766,7 +779,7 @@
|
||||
});
|
||||
if (!isCollapsed) {
|
||||
const items = group.friends.map((friend) => ({
|
||||
key: `fg:${group.key}:${friend?.id ?? friend?.userId ?? friend?.displayName ?? Math.random()}`,
|
||||
key: `fg:${group.key}:${getFriendIdentity(friend)}`,
|
||||
friend,
|
||||
displayInstanceInfo: true
|
||||
}));
|
||||
@@ -779,7 +792,7 @@
|
||||
const entries = filteredFriends.value;
|
||||
if (entries.length) {
|
||||
const items = entries.map((entry) => ({
|
||||
key: `e:${entry?.id ?? entry?.friend?.id ?? entry?.friend?.displayName ?? Math.random()}`,
|
||||
key: `e:${getEntryIdentity(entry)}`,
|
||||
friend: entry.friend,
|
||||
displayInstanceInfo: true
|
||||
}));
|
||||
@@ -814,7 +827,7 @@
|
||||
}
|
||||
|
||||
const itemCount = Array.isArray(row.items) ? row.items.length : 0;
|
||||
const { columns, gap } = getGridMetrics(itemCount, { matchMaxColumnWidth: true });
|
||||
const { columns, gap } = computeGridLayout(itemCount, { matchMaxColumnWidth: true });
|
||||
const safeColumns = Math.max(1, columns || 1);
|
||||
const rows = Math.max(1, Math.ceil(itemCount / safeColumns));
|
||||
const scale = cardScale.value;
|
||||
@@ -853,10 +866,7 @@
|
||||
const getRowCount = (row) => (row && row.type === 'header' ? row.count : 0);
|
||||
|
||||
watch([searchTerm, activeSegment], () => {
|
||||
nextTick(() => {
|
||||
updateGridWidth();
|
||||
virtualizer.value?.measure?.();
|
||||
});
|
||||
scheduleVirtualMeasure({ updateGridWidth: true });
|
||||
});
|
||||
|
||||
watch(showSameInstance, (value) => {
|
||||
@@ -867,19 +877,13 @@
|
||||
activeSegment.value = 'online';
|
||||
}
|
||||
|
||||
nextTick(() => {
|
||||
updateGridWidth();
|
||||
virtualizer.value?.measure?.();
|
||||
});
|
||||
scheduleVirtualMeasure({ updateGridWidth: true });
|
||||
});
|
||||
|
||||
watch(
|
||||
() => filteredFriends.value.length,
|
||||
() => {
|
||||
nextTick(() => {
|
||||
updateGridWidth();
|
||||
virtualizer.value?.measure?.();
|
||||
});
|
||||
scheduleVirtualMeasure({ updateGridWidth: true });
|
||||
}
|
||||
);
|
||||
|
||||
@@ -887,23 +891,17 @@
|
||||
if (!settingsReady.value) {
|
||||
return;
|
||||
}
|
||||
nextTick(() => {
|
||||
updateGridWidth();
|
||||
virtualizer.value?.measure?.();
|
||||
});
|
||||
scheduleVirtualMeasure({ updateGridWidth: true });
|
||||
});
|
||||
|
||||
watch(virtualRows, () => {
|
||||
nextTick(() => {
|
||||
virtualizer.value?.measure?.();
|
||||
});
|
||||
scheduleVirtualMeasure();
|
||||
});
|
||||
|
||||
onMounted(() => {
|
||||
nextTick(() => {
|
||||
setupResizeHandling();
|
||||
updateGridWidth();
|
||||
virtualizer.value?.measure?.();
|
||||
scheduleVirtualMeasure({ updateGridWidth: true });
|
||||
});
|
||||
});
|
||||
|
||||
@@ -944,8 +942,7 @@
|
||||
settingsReady.value = true;
|
||||
nextTick(() => {
|
||||
setupResizeHandling();
|
||||
updateGridWidth();
|
||||
virtualizer.value?.measure?.();
|
||||
scheduleVirtualMeasure({ updateGridWidth: true });
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -94,6 +94,13 @@ vi.mock('../../../shared/utils/location.js', () => ({
|
||||
}));
|
||||
|
||||
vi.mock('../../../shared/utils', () => ({
|
||||
debounce: (fn, delay) => {
|
||||
let timer = null;
|
||||
return (...args) => {
|
||||
clearTimeout(timer);
|
||||
timer = setTimeout(() => fn(...args), delay);
|
||||
};
|
||||
},
|
||||
getFriendsSortFunction: () => (a, b) =>
|
||||
String(a?.displayName ?? '').localeCompare(String(b?.displayName ?? ''))
|
||||
}));
|
||||
@@ -295,13 +302,17 @@ describe('FriendsLocations.vue', () => {
|
||||
});
|
||||
|
||||
test('persists card scale and same-instance preferences', async () => {
|
||||
vi.useFakeTimers();
|
||||
const wrapper = mount(FriendsLocations);
|
||||
await flushSettings();
|
||||
mocks.configSetString.mockClear();
|
||||
mocks.configSetBool.mockClear();
|
||||
|
||||
await wrapper.get('[data-testid="set-scale"]').trigger('click');
|
||||
await wrapper
|
||||
.get('[data-testid="toggle-same-instance"]')
|
||||
.trigger('click');
|
||||
vi.advanceTimersByTime(200);
|
||||
|
||||
expect(mocks.configSetString).toHaveBeenCalledWith(
|
||||
'VRCX_FriendLocationCardScale',
|
||||
@@ -311,6 +322,20 @@ describe('FriendsLocations.vue', () => {
|
||||
'VRCX_FriendLocationShowSameInstance',
|
||||
true
|
||||
);
|
||||
vi.useRealTimers();
|
||||
});
|
||||
|
||||
test('coalesces repeated virtualizer measure requests in the same tick', async () => {
|
||||
const wrapper = mount(FriendsLocations);
|
||||
await flushSettings();
|
||||
mocks.virtualMeasure.mockClear();
|
||||
|
||||
wrapper.vm.searchTerm = 'alice';
|
||||
wrapper.vm.activeSegment = 'offline';
|
||||
await nextTick();
|
||||
await nextTick();
|
||||
|
||||
expect(mocks.virtualMeasure).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
test('renders empty state when no rows match', async () => {
|
||||
|
||||
Reference in New Issue
Block a user