diff --git a/src/components/GlobalSearchDialog.vue b/src/components/GlobalSearchDialog.vue index 351772d6..db8bcf47 100644 --- a/src/components/GlobalSearchDialog.vue +++ b/src/components/GlobalSearchDialog.vue @@ -6,10 +6,11 @@ import { useI18n } from 'vue-i18n'; import { useGlobalSearchStore } from '../stores/globalSearch'; - import { userImage } from '../shared/utils'; + import { useUserDisplay } from '../composables/useUserDisplay'; import GlobalSearchSync from './GlobalSearchSync.vue'; + const { userImage } = useUserDisplay(); const globalSearchStore = useGlobalSearchStore(); const { isOpen, diff --git a/src/components/InstanceActionBar.vue b/src/components/InstanceActionBar.vue index 4fdb3f00..7f6b5bc8 100644 --- a/src/components/InstanceActionBar.vue +++ b/src/components/InstanceActionBar.vue @@ -159,7 +159,8 @@ useModalStore, useUserStore } from '../stores'; - import { checkCanInviteSelf, formatDateFilter, hasGroupPermission, parseLocation } from '../shared/utils'; + import { formatDateFilter, hasGroupPermission, parseLocation } from '../shared/utils'; + import { useInviteChecks } from '../composables/useInviteChecks'; import { instanceRequest, miscRequest } from '../api'; import { showUserDialog } from '../coordinators/userCoordinator'; @@ -180,6 +181,7 @@ const { instanceJoinHistory } = storeToRefs(instanceStore); const { canOpenInstanceInGame } = storeToRefs(inviteStore); const { isOpeningInstance } = storeToRefs(launchStore); + const { checkCanInviteSelf } = useInviteChecks(); const props = defineProps({ location: { diff --git a/src/components/dialogs/AvatarDialog/AvatarDialog.vue b/src/components/dialogs/AvatarDialog/AvatarDialog.vue index bf4d73d2..640fdc3d 100644 --- a/src/components/dialogs/AvatarDialog/AvatarDialog.vue +++ b/src/components/dialogs/AvatarDialog/AvatarDialog.vue @@ -571,6 +571,7 @@ import { useAppearanceSettingsStore, + useAuthStore, useAvatarStore, useFavoriteStore, useGalleryStore, @@ -622,6 +623,7 @@ import { showUserDialog } from '../../../coordinators/userCoordinator'; const { isGameRunning } = storeToRefs(useGameStore()); const { showFullscreenImageDialog } = useGalleryStore(); const { isDarkMode } = storeToRefs(useAppearanceSettingsStore()); + const authStore = useAuthStore(); const modalStore = useModalStore(); const uiStore = useUiStore(); @@ -698,7 +700,7 @@ import { showUserDialog } from '../../../coordinators/userCoordinator'; // skip imposters continue; } - if (!compareUnityVersion(unityPackage.unitySortNumber)) { + if (!compareUnityVersion(unityPackage.unitySortNumber, authStore.cachedConfig.sdkUnityVersion)) { continue; } let platform = 'PC'; diff --git a/src/components/dialogs/GroupDialog/GroupDialogMembersTab.vue b/src/components/dialogs/GroupDialog/GroupDialogMembersTab.vue index 15dd038d..1f001be8 100644 --- a/src/components/dialogs/GroupDialog/GroupDialogMembersTab.vue +++ b/src/components/dialogs/GroupDialog/GroupDialogMembersTab.vue @@ -210,13 +210,15 @@ import { storeToRefs } from 'pinia'; import { useI18n } from 'vue-i18n'; - import { downloadAndSaveJson, hasGroupPermission, userImage } from '../../../shared/utils'; + import { downloadAndSaveJson, hasGroupPermission } from '../../../shared/utils'; + import { useUserDisplay } from '../../../composables/useUserDisplay'; import { useGroupStore, useUserStore } from '../../../stores'; import { applyGroupMember, handleGroupMember } from '../../../coordinators/groupCoordinator'; import { groupDialogSortingOptions } from '../../../shared/constants'; import { useGroupMembers } from './useGroupMembers'; import { showUserDialog } from '../../../coordinators/userCoordinator'; + const { userImage } = useUserDisplay(); const { t } = useI18n(); diff --git a/src/components/dialogs/GroupDialog/GroupMemberModerationDialog.vue b/src/components/dialogs/GroupDialog/GroupMemberModerationDialog.vue index b21be5e5..b3d173f3 100644 --- a/src/components/dialogs/GroupDialog/GroupMemberModerationDialog.vue +++ b/src/components/dialogs/GroupDialog/GroupMemberModerationDialog.vue @@ -123,7 +123,8 @@ import { useAppearanceSettingsStore, useGalleryStore, useGroupStore, useUserStore } from '../../../stores'; import { applyGroupMember, handleGroupMember, handleGroupMemberProps } from '../../../coordinators/groupCoordinator'; - import { hasGroupPermission, userImage, userImageFull } from '../../../shared/utils'; + import { hasGroupPermission } from '../../../shared/utils'; + import { useUserDisplay } from '../../../composables/useUserDisplay'; import { groupDialogFilterOptions, groupDialogSortingOptions } from '../../../shared/constants'; import { groupRequest, userRequest } from '../../../api'; import { resolveRoleNames } from './groupModerationUtils'; @@ -142,6 +143,7 @@ import { showUserDialog } from '../../../coordinators/userCoordinator'; // ── Stores ─────────────────────────────────────────────────── + const { userImage, userImageFull } = useUserDisplay(); const appearanceSettingsStore = useAppearanceSettingsStore(); const { randomUserColours } = storeToRefs(appearanceSettingsStore); diff --git a/src/components/dialogs/InviteDialog/InviteDialog.vue b/src/components/dialogs/InviteDialog/InviteDialog.vue index 1dcca53e..a9a4d6ba 100644 --- a/src/components/dialogs/InviteDialog/InviteDialog.vue +++ b/src/components/dialogs/InviteDialog/InviteDialog.vue @@ -106,12 +106,14 @@ import { useI18n } from 'vue-i18n'; import { useFriendStore, useGalleryStore, useInviteStore, useModalStore, useUserStore } from '../../../stores'; - import { parseLocation, userImage, userStatusClass } from '../../../shared/utils'; + import { parseLocation } from '../../../shared/utils'; + import { useUserDisplay } from '../../../composables/useUserDisplay'; import { instanceRequest, notificationRequest } from '../../../api'; import { VirtualCombobox } from '../../ui/virtual-combobox'; import SendInviteDialog from './SendInviteDialog.vue'; + const { userImage, userStatusClass } = useUserDisplay(); const { vipFriends, onlineFriends, activeFriends } = storeToRefs(useFriendStore()); const { refreshInviteMessageTableData } = useInviteStore(); const { currentUser } = storeToRefs(useUserStore()); diff --git a/src/components/dialogs/InviteGroupDialog.vue b/src/components/dialogs/InviteGroupDialog.vue index d8633b1b..7d9ce9f6 100644 --- a/src/components/dialogs/InviteGroupDialog.vue +++ b/src/components/dialogs/InviteGroupDialog.vue @@ -96,13 +96,15 @@ import { toast } from 'vue-sonner'; import { useI18n } from 'vue-i18n'; - import { hasGroupPermission, userImage, userStatusClass } from '../../shared/utils'; + import { hasGroupPermission } from '../../shared/utils'; + import { useUserDisplay } from '../../composables/useUserDisplay'; import { useFriendStore, useGroupStore, useModalStore } from '../../stores'; import { groupRequest, queryRequest } from '../../api'; import { VirtualCombobox } from '../ui/virtual-combobox'; import configRepository from '../../services/config'; + const { userImage, userStatusClass } = useUserDisplay(); const { vipFriends, onlineFriends, activeFriends, offlineFriends } = storeToRefs(useFriendStore()); const { currentUserGroups, inviteGroupDialog } = storeToRefs(useGroupStore()); const { applyGroup } = useGroupStore(); diff --git a/src/components/dialogs/LaunchDialog.vue b/src/components/dialogs/LaunchDialog.vue index 90e93237..99aea3b5 100644 --- a/src/components/dialogs/LaunchDialog.vue +++ b/src/components/dialogs/LaunchDialog.vue @@ -161,7 +161,8 @@ useLocationStore, useModalStore } from '../../stores'; - import { checkCanInvite, getLaunchURL, isRealInstance, parseLocation } from '../../shared/utils'; + import { getLaunchURL, isRealInstance, parseLocation } from '../../shared/utils'; + import { useInviteChecks } from '../../composables/useInviteChecks'; import { instanceRequest, queryRequest } from '../../api'; import InviteDialog from './InviteDialog/InviteDialog.vue'; @@ -178,6 +179,7 @@ const { canOpenInstanceInGame } = storeToRefs(useInviteStore()); const { isGameRunning } = storeToRefs(useGameStore()); + const { checkCanInvite } = useInviteChecks(); const launchModeLabel = computed(() => launchDialog.value.desktop ? t('dialog.launch.start_as_desktop') : t('dialog.launch.launch') diff --git a/src/components/dialogs/ModerateGroupDialog.vue b/src/components/dialogs/ModerateGroupDialog.vue index 6d18b77d..5169f99a 100644 --- a/src/components/dialogs/ModerateGroupDialog.vue +++ b/src/components/dialogs/ModerateGroupDialog.vue @@ -63,11 +63,13 @@ import { storeToRefs } from 'pinia'; import { useI18n } from 'vue-i18n'; - import { hasGroupModerationPermission, userImage } from '../../shared/utils'; + import { hasGroupModerationPermission } from '../../shared/utils'; + import { useUserDisplay } from '../../composables/useUserDisplay'; import { VirtualCombobox } from '../ui/virtual-combobox'; import { queryRequest } from '../../api'; import { useGroupStore } from '../../stores'; + const { userImage } = useUserDisplay(); const { currentUserGroups, moderateGroupDialog } = storeToRefs(useGroupStore()); const { showGroupMemberModerationDialog } = useGroupStore(); const { t } = useI18n(); diff --git a/src/components/dialogs/UserDialog/UserActionDropdown.vue b/src/components/dialogs/UserDialog/UserActionDropdown.vue index fd24ea17..1a28af24 100644 --- a/src/components/dialogs/UserDialog/UserActionDropdown.vue +++ b/src/components/dialogs/UserDialog/UserActionDropdown.vue @@ -251,7 +251,7 @@ DropdownMenuTrigger } from '../../ui/dropdown-menu'; import { useGameStore, useLocationStore, useUserStore } from '../../../stores'; - import { checkCanInvite } from '../../../shared/utils'; + import { useInviteChecks } from '../../../composables/useInviteChecks'; const props = defineProps({ userDialogCommand: { @@ -265,6 +265,7 @@ const { userDialog, currentUser } = storeToRefs(useUserStore()); const { isGameRunning } = storeToRefs(useGameStore()); const { lastLocation } = storeToRefs(useLocationStore()); + const { checkCanInvite } = useInviteChecks(); const hasRequest = computed(() => userDialog.value.incomingRequest || userDialog.value.outgoingRequest); const hasRisk = computed( diff --git a/src/components/dialogs/WorldDialog/WorldDialogInfoTab.vue b/src/components/dialogs/WorldDialog/WorldDialogInfoTab.vue index 48548a30..9af45596 100644 --- a/src/components/dialogs/WorldDialog/WorldDialogInfoTab.vue +++ b/src/components/dialogs/WorldDialog/WorldDialogInfoTab.vue @@ -269,13 +269,14 @@ import { useI18n } from 'vue-i18n'; import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger } from '../../ui/dropdown-menu'; - import { useInstanceStore, useWorldStore } from '../../../stores'; + import { useAuthStore, useInstanceStore, useWorldStore } from '../../../stores'; import { openExternalLink } from '../../../shared/utils'; import { useWorldDialogInfo } from './useWorldDialogInfo'; const { t } = useI18n(); const { worldDialog } = storeToRefs(useWorldStore()); + const authStore = useAuthStore(); const { showPreviousInstancesListDialog: openPreviousInstancesListDialog } = useInstanceStore(); const { @@ -293,7 +294,7 @@ copyWorldName, commaNumber, formatDateFilter - } = useWorldDialogInfo(worldDialog, { t, toast }); + } = useWorldDialogInfo(worldDialog, { t, toast, sdkUnityVersion: authStore.cachedConfig.sdkUnityVersion }); /** * diff --git a/src/components/dialogs/WorldDialog/WorldDialogInstancesTab.vue b/src/components/dialogs/WorldDialog/WorldDialogInstancesTab.vue index 426cc337..1411a9ac 100644 --- a/src/components/dialogs/WorldDialog/WorldDialogInstancesTab.vue +++ b/src/components/dialogs/WorldDialog/WorldDialogInstancesTab.vue @@ -108,7 +108,8 @@ import { storeToRefs } from 'pinia'; import { useI18n } from 'vue-i18n'; - import { refreshInstancePlayerCount, userImage, userStatusClass } from '../../../shared/utils'; + import { refreshInstancePlayerCount } from '../../../shared/utils'; + import { useUserDisplay } from '../../../composables/useUserDisplay'; import { useAppearanceSettingsStore, useInstanceStore, @@ -118,9 +119,10 @@ } from '../../../stores'; import InstanceActionBar from '../../InstanceActionBar.vue'; -import { showUserDialog } from '../../../coordinators/userCoordinator'; + import { showUserDialog } from '../../../coordinators/userCoordinator'; const { t } = useI18n(); + const { userImage, userStatusClass } = useUserDisplay(); const { isAgeGatedInstancesVisible } = storeToRefs(useAppearanceSettingsStore()); diff --git a/src/components/dialogs/WorldDialog/useWorldDialogInfo.js b/src/components/dialogs/WorldDialog/useWorldDialogInfo.js index fda5c3e7..8ce4b78c 100644 --- a/src/components/dialogs/WorldDialog/useWorldDialogInfo.js +++ b/src/components/dialogs/WorldDialog/useWorldDialogInfo.js @@ -16,7 +16,7 @@ import { database } from '../../../services/database'; * @param {Function} deps.toast - toast notification function * @returns {Object} info composable API */ -export function useWorldDialogInfo(worldDialog, { t, toast }) { +export function useWorldDialogInfo(worldDialog, { t, toast, sdkUnityVersion }) { const memo = computed({ get() { return worldDialog.value.memo; @@ -71,7 +71,7 @@ export function useWorldDialogInfo(worldDialog, { t, toast }) { const platforms = []; if (ref.unityPackages) { for (const unityPackage of ref.unityPackages) { - if (!compareUnityVersion(unityPackage.unitySortNumber)) { + if (!compareUnityVersion(unityPackage.unitySortNumber, sdkUnityVersion)) { continue; } let platform = 'PC'; diff --git a/src/composables/useInviteChecks.js b/src/composables/useInviteChecks.js new file mode 100644 index 00000000..9bafbbda --- /dev/null +++ b/src/composables/useInviteChecks.js @@ -0,0 +1,39 @@ +import { + useFriendStore, + useInstanceStore, + useLocationStore, + useUserStore +} from '../stores'; +import { + checkCanInvite as checkCanInvitePure, + checkCanInviteSelf as checkCanInviteSelfPure +} from '../shared/utils/invite'; + +/** + * Composable that provides store-aware invite check functions. + * Delegates to the pure utility functions after resolving store data. + */ +export function useInviteChecks() { + const userStore = useUserStore(); + const locationStore = useLocationStore(); + const instanceStore = useInstanceStore(); + const friendStore = useFriendStore(); + + function checkCanInvite(location) { + return checkCanInvitePure(location, { + currentUserId: userStore.currentUser.id, + lastLocationStr: locationStore.lastLocation.location, + cachedInstances: instanceStore.cachedInstances + }); + } + + function checkCanInviteSelf(location) { + return checkCanInviteSelfPure(location, { + currentUserId: userStore.currentUser.id, + cachedInstances: instanceStore.cachedInstances, + friends: friendStore.friends + }); + } + + return { checkCanInvite, checkCanInviteSelf }; +} diff --git a/src/composables/useUserDisplay.js b/src/composables/useUserDisplay.js new file mode 100644 index 00000000..d72abc1b --- /dev/null +++ b/src/composables/useUserDisplay.js @@ -0,0 +1,43 @@ +import { useAppearanceSettingsStore, useUserStore } from '../stores'; +import { + userImage as userImagePure, + userImageFull as userImageFullPure, + userStatusClass as userStatusClassPure +} from '../shared/utils/user'; + +/** + * Composable that provides store-aware user display functions. + * Delegates to the pure utility functions after resolving store data. + */ +export function useUserDisplay() { + const userStore = useUserStore(); + const appearanceStore = useAppearanceSettingsStore(); + + function userStatusClass(user, pendingOffline = false) { + return userStatusClassPure(user, pendingOffline, userStore.currentUser); + } + + function userImage( + user, + isIcon = false, + resolution = '128', + isUserDialogIcon = false + ) { + return userImagePure( + user, + isIcon, + resolution, + isUserDialogIcon, + appearanceStore.displayVRCPlusIconsAsAvatar + ); + } + + function userImageFull(user) { + return userImageFullPure( + user, + appearanceStore.displayVRCPlusIconsAsAvatar + ); + } + + return { userStatusClass, userImage, userImageFull }; +} diff --git a/src/coordinators/friendRelationshipCoordinator.js b/src/coordinators/friendRelationshipCoordinator.js index 46b0b0bc..65e90946 100644 --- a/src/coordinators/friendRelationshipCoordinator.js +++ b/src/coordinators/friendRelationshipCoordinator.js @@ -321,7 +321,7 @@ export function updateUserCurrentStatus(ref) { friendStore.updateOnlineFriendCounter(); if (appearanceSettingsStore.randomUserColours) { - getNameColour(userStore.currentUser.id).then((colour) => { + getNameColour(userStore.currentUser.id, appearanceSettingsStore.isDarkMode).then((colour) => { userStore.setCurrentUserColour(colour); }); } diff --git a/src/shared/utils/__tests__/invite.test.js b/src/shared/utils/__tests__/invite.test.js index 32a0a9ec..c0c7b35c 100644 --- a/src/shared/utils/__tests__/invite.test.js +++ b/src/shared/utils/__tests__/invite.test.js @@ -1,151 +1,154 @@ import { beforeEach, describe, expect, test, vi } from 'vitest'; - -// Mock stores -vi.mock('../../../stores', () => ({ - useFriendStore: vi.fn(), - useInstanceStore: vi.fn(), - useLocationStore: vi.fn(), - useUserStore: vi.fn() -})); - -// Mock transitive deps -vi.mock('../../../views/Feed/Feed.vue', () => ({ - default: { template: '
' } -})); -vi.mock('../../../views/Feed/columns.jsx', () => ({ columns: [] })); -vi.mock('../../../plugins/router', () => ({ - default: { push: vi.fn(), currentRoute: { value: {} } } -})); - -import { - useFriendStore, - useInstanceStore, - useLocationStore, - useUserStore -} from '../../../stores'; import { checkCanInvite, checkCanInviteSelf } from '../invite'; +const storeMocks = vi.hoisted(() => ({ + useUserStore: vi.fn(() => ({ currentUser: { id: 'usr_me' } })), + useLocationStore: vi.fn(() => ({ + lastLocation: { location: '', friendList: new Set() } + })), + useInstanceStore: vi.fn(() => ({ cachedInstances: new Map() })), + useFriendStore: vi.fn(() => ({ friends: new Map() })) +})); + +vi.mock('../../../stores', () => storeMocks); + describe('Invite Utils', () => { beforeEach(() => { - useUserStore.mockReturnValue({ - currentUser: { id: 'usr_me' } - }); - useLocationStore.mockReturnValue({ - lastLocation: { location: 'wrld_last:12345' } - }); - useInstanceStore.mockReturnValue({ - cachedInstances: new Map() - }); - useFriendStore.mockReturnValue({ - friends: new Map() - }); + vi.clearAllMocks(); }); + const defaultInviteDeps = { + currentUserId: 'usr_me', + lastLocationStr: 'wrld_last:12345', + cachedInstances: new Map() + }; + + const defaultSelfDeps = { + currentUserId: 'usr_me', + cachedInstances: new Map(), + friends: new Map() + }; + describe('checkCanInvite', () => { + test('does not access stores when deps are provided (pure path)', () => { + checkCanInvite('wrld_123:instance', defaultInviteDeps); + expect(storeMocks.useUserStore).not.toHaveBeenCalled(); + expect(storeMocks.useLocationStore).not.toHaveBeenCalled(); + expect(storeMocks.useInstanceStore).not.toHaveBeenCalled(); + }); + test('returns false for empty location', () => { - expect(checkCanInvite('')).toBe(false); - expect(checkCanInvite(null)).toBe(false); + expect(checkCanInvite('', defaultInviteDeps)).toBe(false); + expect(checkCanInvite(null, defaultInviteDeps)).toBe(false); }); test('returns true for public instance', () => { - expect(checkCanInvite('wrld_123:instance')).toBe(true); + expect(checkCanInvite('wrld_123:instance', defaultInviteDeps)).toBe(true); }); test('returns true for group instance', () => { expect( checkCanInvite( - 'wrld_123:instance~group(grp_123)~groupAccessType(public)' + 'wrld_123:instance~group(grp_123)~groupAccessType(public)', + defaultInviteDeps ) ).toBe(true); }); test('returns true for own instance', () => { - expect(checkCanInvite('wrld_123:instance~private(usr_me)')).toBe( + expect(checkCanInvite('wrld_123:instance~private(usr_me)', defaultInviteDeps)).toBe( true ); }); test('returns false for invite-only instance owned by another', () => { - expect(checkCanInvite('wrld_123:instance~private(usr_other)')).toBe( + expect(checkCanInvite('wrld_123:instance~private(usr_other)', defaultInviteDeps)).toBe( false ); }); test('returns false for friends-only instance', () => { - expect(checkCanInvite('wrld_123:instance~friends(usr_other)')).toBe( + expect(checkCanInvite('wrld_123:instance~friends(usr_other)', defaultInviteDeps)).toBe( false ); }); test('returns true for friends+ instance if current location matches', () => { const location = 'wrld_123:instance~hidden(usr_other)'; - useLocationStore.mockReturnValue({ - lastLocation: { location } - }); - expect(checkCanInvite(location)).toBe(true); + expect(checkCanInvite(location, { + ...defaultInviteDeps, + lastLocationStr: location + })).toBe(true); }); test('returns false for friends+ instance if not in that location', () => { - expect(checkCanInvite('wrld_123:instance~hidden(usr_other)')).toBe( + expect(checkCanInvite('wrld_123:instance~hidden(usr_other)', defaultInviteDeps)).toBe( false ); }); test('returns false for closed instance', () => { const location = 'wrld_123:instance'; - useInstanceStore.mockReturnValue({ + expect(checkCanInvite(location, { + ...defaultInviteDeps, cachedInstances: new Map([ [location, { closedAt: '2024-01-01' }] ]) - }); - expect(checkCanInvite(location)).toBe(false); + })).toBe(false); }); }); describe('checkCanInviteSelf', () => { + test('does not access stores when deps are provided (pure path)', () => { + checkCanInviteSelf('wrld_123:instance', defaultSelfDeps); + expect(storeMocks.useUserStore).not.toHaveBeenCalled(); + expect(storeMocks.useInstanceStore).not.toHaveBeenCalled(); + expect(storeMocks.useFriendStore).not.toHaveBeenCalled(); + }); + test('returns false for empty location', () => { - expect(checkCanInviteSelf('')).toBe(false); - expect(checkCanInviteSelf(null)).toBe(false); + expect(checkCanInviteSelf('', defaultSelfDeps)).toBe(false); + expect(checkCanInviteSelf(null, defaultSelfDeps)).toBe(false); }); test('returns true for own instance', () => { expect( - checkCanInviteSelf('wrld_123:instance~private(usr_me)') + checkCanInviteSelf('wrld_123:instance~private(usr_me)', defaultSelfDeps) ).toBe(true); }); test('returns true for public instance', () => { - expect(checkCanInviteSelf('wrld_123:instance')).toBe(true); + expect(checkCanInviteSelf('wrld_123:instance', defaultSelfDeps)).toBe(true); }); test('returns true for friends-only instance if user is a friend', () => { - useFriendStore.mockReturnValue({ - friends: new Map([['usr_owner', {}]]) - }); expect( - checkCanInviteSelf('wrld_123:instance~friends(usr_owner)') + checkCanInviteSelf('wrld_123:instance~friends(usr_owner)', { + ...defaultSelfDeps, + friends: new Map([['usr_owner', {}]]) + }) ).toBe(true); }); test('returns false for friends-only instance if user is not a friend', () => { expect( - checkCanInviteSelf('wrld_123:instance~friends(usr_other)') + checkCanInviteSelf('wrld_123:instance~friends(usr_other)', defaultSelfDeps) ).toBe(false); }); test('returns false for closed instance', () => { const location = 'wrld_123:instance'; - useInstanceStore.mockReturnValue({ + expect(checkCanInviteSelf(location, { + ...defaultSelfDeps, cachedInstances: new Map([ [location, { closedAt: '2024-01-01' }] ]) - }); - expect(checkCanInviteSelf(location)).toBe(false); + })).toBe(false); }); test('returns true for invite instance (not owned, not closed)', () => { expect( - checkCanInviteSelf('wrld_123:instance~private(usr_other)') + checkCanInviteSelf('wrld_123:instance~private(usr_other)', defaultSelfDeps) ).toBe(true); }); }); diff --git a/src/shared/utils/__tests__/user.test.js b/src/shared/utils/__tests__/user.test.js index 4c164f7c..2edc0ab5 100644 --- a/src/shared/utils/__tests__/user.test.js +++ b/src/shared/utils/__tests__/user.test.js @@ -1,11 +1,5 @@ import { beforeEach, describe, expect, test, vi } from 'vitest'; -// Mock stores -vi.mock('../../../stores', () => ({ - useUserStore: vi.fn(), - useAppearanceSettingsStore: vi.fn() -})); - // Mock common.js vi.mock('../common', () => ({ convertFileUrlToImageUrl: vi.fn((url) => `converted:${url}`) @@ -21,16 +15,22 @@ vi.mock('../base/ui', () => ({ HueToHex: vi.fn((h) => `#hue${h}`) })); -// Mock transitive deps that get pulled in via stores -vi.mock('../../../views/Feed/Feed.vue', () => ({ - default: { template: '
' } -})); -vi.mock('../../../views/Feed/columns.jsx', () => ({ columns: [] })); -vi.mock('../../../plugins/router', () => ({ - default: { push: vi.fn(), currentRoute: { value: {} } } +const storeMocks = vi.hoisted(() => ({ + useUserStore: vi.fn(() => ({ + currentUser: { + id: 'usr_store', + presence: { platform: 'standalonewindows' }, + onlineFriends: [], + activeFriends: [] + } + })), + useAppearanceSettingsStore: vi.fn(() => ({ + displayVRCPlusIconsAsAvatar: false + })) })); -import { useAppearanceSettingsStore, useUserStore } from '../../../stores'; +vi.mock('../../../stores', () => storeMocks); + import { languageClass, parseUserUrl, @@ -209,28 +209,42 @@ describe('User Utils', () => { }); }); - describe('userStatusClass (with store mock)', () => { + describe('userStatusClass (explicit currentUser)', () => { + let currentUser; + beforeEach(() => { - useUserStore.mockReturnValue({ - currentUser: { - id: 'usr_me', - presence: { platform: 'standalonewindows' }, - onlineFriends: [], - activeFriends: [] - } - }); + vi.clearAllMocks(); + currentUser = { + id: 'usr_me', + presence: { platform: 'standalonewindows' }, + onlineFriends: [], + activeFriends: [] + }; + }); + + test('does not access stores when currentUser is passed (pure path)', () => { + userStatusClass( + { id: 'usr_me', status: 'active', isFriend: true }, + false, + currentUser + ); + expect(storeMocks.useUserStore).not.toHaveBeenCalled(); }); test('returns null for undefined user', () => { - expect(userStatusClass(undefined)).toBeNull(); + expect(userStatusClass(undefined, false, currentUser)).toBeNull(); }); test('returns current user style with status', () => { - const result = userStatusClass({ - id: 'usr_me', - status: 'active', - isFriend: true - }); + const result = userStatusClass( + { + id: 'usr_me', + status: 'active', + isFriend: true + }, + false, + currentUser + ); expect(result).toMatchObject({ 'status-icon': true, online: true, @@ -239,35 +253,37 @@ describe('User Utils', () => { }); test('returns mobile true for non-PC platform on current user', () => { - useUserStore.mockReturnValue({ - currentUser: { + currentUser.presence = { platform: 'android' }; + const result = userStatusClass( + { id: 'usr_me', - presence: { platform: 'android' }, - onlineFriends: [], - activeFriends: [] - } - }); - const result = userStatusClass({ - id: 'usr_me', - status: 'active' - }); + status: 'active' + }, + false, + currentUser + ); expect(result.mobile).toBe(true); }); test('returns null for non-friend users', () => { expect( - userStatusClass({ - id: 'usr_other', - status: 'active', - isFriend: false - }) + userStatusClass( + { + id: 'usr_other', + status: 'active', + isFriend: false + }, + false, + currentUser + ) ).toBeNull(); }); test('returns offline style for pending offline friend', () => { const result = userStatusClass( { id: 'usr_other', isFriend: true, status: 'active' }, - true + true, + currentUser ); expect(result).toMatchObject({ 'status-icon': true, @@ -309,7 +325,7 @@ describe('User Utils', () => { status, location, state - }); + }, false, currentUser); expect(result[expected]).toBe(true); } }); @@ -321,7 +337,7 @@ describe('User Utils', () => { status: 'active', location: 'offline', state: '' - }); + }, false, currentUser); expect(result.offline).toBe(true); }); @@ -332,7 +348,7 @@ describe('User Utils', () => { status: 'busy', location: 'private', state: 'active' - }); + }, false, currentUser); expect(result.active).toBe(true); }); @@ -344,7 +360,7 @@ describe('User Utils', () => { location: 'wrld_1', state: 'online', $platform: 'android' - }); + }, false, currentUser); expect(result.mobile).toBe(true); }); @@ -356,7 +372,7 @@ describe('User Utils', () => { location: 'wrld_1', state: 'online', $platform: 'standalonewindows' - }); + }, false, currentUser); expect(result.mobile).toBeUndefined(); }); @@ -364,7 +380,7 @@ describe('User Utils', () => { const result = userStatusClass({ userId: 'usr_me', status: 'busy' - }); + }, false, currentUser); expect(result).toMatchObject({ 'status-icon': true, busy: true, @@ -373,79 +389,78 @@ describe('User Utils', () => { }); test('handles private location with empty state (temp fix branch)', () => { - useUserStore.mockReturnValue({ - currentUser: { - id: 'usr_me', - onlineFriends: [], - activeFriends: ['usr_f'] - } - }); + currentUser.activeFriends = ['usr_f']; const result = userStatusClass({ id: 'usr_f', isFriend: true, status: 'busy', location: 'private', state: '' - }); + }, false, currentUser); // activeFriends includes usr_f → active expect(result.active).toBe(true); }); test('handles private location temp fix → offline branch', () => { - useUserStore.mockReturnValue({ - currentUser: { - id: 'usr_me', - onlineFriends: [], - activeFriends: [] - } - }); + currentUser.activeFriends = []; const result = userStatusClass({ id: 'usr_f', isFriend: true, status: 'busy', location: 'private', state: '' - }); + }, false, currentUser); expect(result.offline).toBe(true); }); }); - describe('userImage (with store mock)', () => { - beforeEach(() => { - useAppearanceSettingsStore.mockReturnValue({ - displayVRCPlusIconsAsAvatar: false - }); + describe('userImage (explicit settings)', () => { + test('does not access appearance store when setting is passed (pure path)', () => { + userImage( + { thumbnailUrl: 'https://img.com/thumb' }, + false, + '128', + false, + false + ); + expect(storeMocks.useAppearanceSettingsStore).not.toHaveBeenCalled(); }); test('returns empty string for falsy user', () => { - expect(userImage(null)).toBe(''); - expect(userImage(undefined)).toBe(''); + expect(userImage(null, false, '128', false, false)).toBe(''); + expect(userImage(undefined, false, '128', false, false)).toBe(''); }); test('returns profilePicOverrideThumbnail when available', () => { const user = { profilePicOverrideThumbnail: 'https://img.com/pic/256/thumb' }; - expect(userImage(user)).toBe('https://img.com/pic/256/thumb'); + expect(userImage(user, false, '128', false, false)).toBe( + 'https://img.com/pic/256/thumb' + ); }); test('replaces resolution for icon mode with profilePicOverrideThumbnail', () => { const user = { profilePicOverrideThumbnail: 'https://img.com/pic/256/thumb' }; - expect(userImage(user, true, '64')).toBe( + expect(userImage(user, true, '64', false, false)).toBe( 'https://img.com/pic/64/thumb' ); }); test('returns profilePicOverride when no thumbnail', () => { const user = { profilePicOverride: 'https://img.com/full' }; - expect(userImage(user)).toBe('https://img.com/full'); + expect(userImage(user, false, '128', false, false)).toBe( + 'https://img.com/full' + ); }); test('returns thumbnailUrl as fallback', () => { const user = { thumbnailUrl: 'https://img.com/thumb' }; - expect(userImage(user)).toBe('https://img.com/thumb'); + expect(userImage(user, false, '128', false, false)).toBe( + 'https://img.com/thumb' + ); }); test('returns currentAvatarThumbnailImageUrl as fallback', () => { @@ -453,7 +468,9 @@ describe('User Utils', () => { currentAvatarThumbnailImageUrl: 'https://img.com/avatar/256/thumb' }; - expect(userImage(user)).toBe('https://img.com/avatar/256/thumb'); + expect(userImage(user, false, '128', false, false)).toBe( + 'https://img.com/avatar/256/thumb' + ); }); test('replaces resolution for icon mode with currentAvatarThumbnailImageUrl', () => { @@ -461,7 +478,7 @@ describe('User Utils', () => { currentAvatarThumbnailImageUrl: 'https://img.com/avatar/256/thumb' }; - expect(userImage(user, true, '64')).toBe( + expect(userImage(user, true, '64', false, false)).toBe( 'https://img.com/avatar/64/thumb' ); }); @@ -470,39 +487,37 @@ describe('User Utils', () => { const user = { currentAvatarImageUrl: 'https://img.com/avatar/full' }; - expect(userImage(user)).toBe('https://img.com/avatar/full'); + expect(userImage(user, false, '128', false, false)).toBe( + 'https://img.com/avatar/full' + ); }); test('converts currentAvatarImageUrl for icon mode', () => { const user = { currentAvatarImageUrl: 'https://img.com/avatar/full' }; - expect(userImage(user, true)).toBe( + expect(userImage(user, true, '128', false, false)).toBe( 'converted:https://img.com/avatar/full' ); }); test('returns empty string when user has no image fields', () => { - expect(userImage({})).toBe(''); + expect(userImage({}, false, '128', false, false)).toBe(''); }); test('returns userIcon when displayVRCPlusIconsAsAvatar is true', () => { - useAppearanceSettingsStore.mockReturnValue({ - displayVRCPlusIconsAsAvatar: true - }); const user = { userIcon: 'https://img.com/icon', thumbnailUrl: 'https://img.com/thumb' }; - expect(userImage(user)).toBe('https://img.com/icon'); + expect(userImage(user, false, '128', false, true)).toBe( + 'https://img.com/icon' + ); }); test('converts userIcon for icon mode when VRCPlus setting enabled', () => { - useAppearanceSettingsStore.mockReturnValue({ - displayVRCPlusIconsAsAvatar: true - }); const user = { userIcon: 'https://img.com/icon' }; - expect(userImage(user, true)).toBe( + expect(userImage(user, true, '128', false, true)).toBe( 'converted:https://img.com/icon' ); }); @@ -512,21 +527,23 @@ describe('User Utils', () => { userIcon: 'https://img.com/icon', thumbnailUrl: 'https://img.com/thumb' }; - expect(userImage(user, false, '128', true)).toBe( + expect(userImage(user, false, '128', true, false)).toBe( 'https://img.com/icon' ); }); }); - describe('userImageFull (with store mock)', () => { - beforeEach(() => { - useAppearanceSettingsStore.mockReturnValue({ - displayVRCPlusIconsAsAvatar: false - }); + describe('userImageFull (explicit settings)', () => { + test('does not access appearance store when setting is passed (pure path)', () => { + userImageFull( + { currentAvatarImageUrl: 'https://img.com/avatar' }, + false + ); + expect(storeMocks.useAppearanceSettingsStore).not.toHaveBeenCalled(); }); test('returns empty string for falsy user', () => { - expect(userImageFull(null)).toBe(''); + expect(userImageFull(null, false)).toBe(''); }); test('returns profilePicOverride when available', () => { @@ -534,25 +551,22 @@ describe('User Utils', () => { profilePicOverride: 'https://img.com/full', currentAvatarImageUrl: 'https://img.com/avatar' }; - expect(userImageFull(user)).toBe('https://img.com/full'); + expect(userImageFull(user, false)).toBe('https://img.com/full'); }); test('returns currentAvatarImageUrl as fallback', () => { const user = { currentAvatarImageUrl: 'https://img.com/avatar' }; - expect(userImageFull(user)).toBe('https://img.com/avatar'); + expect(userImageFull(user, false)).toBe('https://img.com/avatar'); }); test('returns userIcon when VRCPlus setting enabled', () => { - useAppearanceSettingsStore.mockReturnValue({ - displayVRCPlusIconsAsAvatar: true - }); const user = { userIcon: 'https://img.com/icon', profilePicOverride: 'https://img.com/full' }; - expect(userImageFull(user)).toBe('https://img.com/icon'); + expect(userImageFull(user, true)).toBe('https://img.com/icon'); }); }); }); diff --git a/src/shared/utils/avatar.js b/src/shared/utils/avatar.js index f24bb76e..c2e6c87f 100644 --- a/src/shared/utils/avatar.js +++ b/src/shared/utils/avatar.js @@ -1,5 +1,4 @@ import { replaceBioSymbols } from './base/string'; -import { useAuthStore } from '../../stores'; /** * @@ -95,10 +94,9 @@ function getPlatformInfo(unityPackages) { * @param {string} unitySortNumber * @returns {boolean} */ -function compareUnityVersion(unitySortNumber) { - const authStore = useAuthStore(); - if (!authStore.cachedConfig.sdkUnityVersion) { - console.error('No cachedConfig.sdkUnityVersion'); +function compareUnityVersion(unitySortNumber, sdkUnityVersion) { + if (!sdkUnityVersion) { + console.error('No sdkUnityVersion provided'); return false; } @@ -106,9 +104,9 @@ function compareUnityVersion(unitySortNumber) { // 2019.4.31f1 2019 04 31 000 // 5.3.4p1 5 03 04 010 // 2019.4.31f1c1 is a thing - const array = authStore.cachedConfig.sdkUnityVersion.split('.'); + const array = sdkUnityVersion.split('.'); if (array.length < 3) { - console.error('Invalid cachedConfig.sdkUnityVersion'); + console.error('Invalid sdkUnityVersion'); return false; } let currentUnityVersion = array[0]; diff --git a/src/shared/utils/base/devtool.js b/src/shared/utils/base/devtool.js index f519e743..455fb88b 100644 --- a/src/shared/utils/base/devtool.js +++ b/src/shared/utils/base/devtool.js @@ -3,7 +3,7 @@ import { extractFileVersion, extractVariantVersion } from '../common'; -import { useAvatarStore, useWorldStore } from '../../../stores'; +import { useAuthStore, useAvatarStore, useWorldStore } from '../../../stores'; import { compareUnityVersion } from '../avatar'; /** @@ -12,6 +12,8 @@ import { compareUnityVersion } from '../avatar'; * @returns {Promise} */ async function getBundleLocation(input) { + const authStore = useAuthStore(); + const sdkUnityVersion = authStore.cachedConfig.sdkUnityVersion; const worldStore = useWorldStore(); const avatarStore = useAvatarStore(); let unityPackage; @@ -36,7 +38,7 @@ async function getBundleLocation(input) { } if ( unityPackage.platform === 'standalonewindows' && - compareUnityVersion(unityPackage.unitySortNumber) + compareUnityVersion(unityPackage.unitySortNumber, sdkUnityVersion) ) { assetUrl = unityPackage.assetUrl; if (unityPackage.variant !== 'standard') { @@ -59,7 +61,7 @@ async function getBundleLocation(input) { unityPackage = unityPackages[i]; if ( unityPackage.platform === 'standalonewindows' && - compareUnityVersion(unityPackage.unitySortNumber) + compareUnityVersion(unityPackage.unitySortNumber, sdkUnityVersion) ) { assetUrl = unityPackage.assetUrl; break; diff --git a/src/shared/utils/base/ui.js b/src/shared/utils/base/ui.js index be42ae89..35d45ca3 100644 --- a/src/shared/utils/base/ui.js +++ b/src/shared/utils/base/ui.js @@ -1,5 +1,4 @@ import { ref } from 'vue'; -import { storeToRefs } from 'pinia'; import { toast } from 'vue-sonner'; import { @@ -11,7 +10,6 @@ import { import { i18n } from '../../../plugins/i18n'; import { router } from '../../../plugins/router'; import { textToHex } from './string'; -import { useAppearanceSettingsStore } from '../../../stores'; import configRepository from '../../../services/config.js'; @@ -320,11 +318,9 @@ async function refreshCustomScript() { * @param {number} hue * @returns {string} */ -function HueToHex(hue) { - const appSettingsStore = useAppearanceSettingsStore(); - const { isDarkMode } = storeToRefs(appSettingsStore); +function HueToHex(hue, isDarkMode) { // this.HSVtoRGB(hue / 65535, .8, .8); - if (isDarkMode.value) { + if (isDarkMode) { return HSVtoRGB(hue / 65535, 0.6, 1); } return HSVtoRGB(hue / 65535, 1, 0.7); diff --git a/src/shared/utils/common.js b/src/shared/utils/common.js index 1df0ac3d..ee2de7d1 100644 --- a/src/shared/utils/common.js +++ b/src/shared/utils/common.js @@ -2,6 +2,7 @@ import { storeToRefs } from 'pinia'; import { toast } from 'vue-sonner'; import { + useAuthStore, useAvatarStore, useInstanceStore, useModalStore, @@ -47,6 +48,8 @@ function downloadAndSaveJson(fileName, data) { } async function deleteVRChatCache(ref) { + const authStore = useAuthStore(); + const sdkUnityVersion = authStore.cachedConfig.sdkUnityVersion; let assetUrl = ''; let variant = ''; for (let i = ref.unityPackages.length - 1; i > -1; i--) { @@ -60,7 +63,7 @@ async function deleteVRChatCache(ref) { } if ( unityPackage.platform === 'standalonewindows' && - compareUnityVersion(unityPackage.unitySortNumber) + compareUnityVersion(unityPackage.unitySortNumber, sdkUnityVersion) ) { assetUrl = unityPackage.assetUrl; if (!unityPackage.variant || unityPackage.variant === 'standard') { @@ -86,6 +89,8 @@ async function checkVRChatCache(ref) { if (!ref.unityPackages) { return { Item1: -1, Item2: false, Item3: '' }; } + const authStore = useAuthStore(); + const sdkUnityVersion = authStore.cachedConfig.sdkUnityVersion; let assetUrl = ''; let variant = ''; for (let i = ref.unityPackages.length - 1; i > -1; i--) { @@ -95,7 +100,7 @@ async function checkVRChatCache(ref) { } if ( unityPackage.platform === 'standalonewindows' && - compareUnityVersion(unityPackage.unitySortNumber) + compareUnityVersion(unityPackage.unitySortNumber, sdkUnityVersion) ) { assetUrl = unityPackage.assetUrl; if (!unityPackage.variant || unityPackage.variant === 'standard') { @@ -153,7 +158,7 @@ function copyToClipboard(text, message = 'Copied successfully!') { * @param {number} resolution * @returns {string} */ -function convertFileUrlToImageUrl(url, resolution = 128) { +function convertFileUrlToImageUrl(url, resolution = 128, endpointDomain = AppDebug.endpointDomain) { if (!url) { return ''; } @@ -170,7 +175,7 @@ function convertFileUrlToImageUrl(url, resolution = 128) { if (match) { const fileId = match[1]; const version = match[2]; - return `${AppDebug.endpointDomain}/image/file_${fileId}/${version}/${resolution}`; + return `${endpointDomain}/image/file_${fileId}/${version}/${resolution}`; } // no match return origin url return url; @@ -223,6 +228,8 @@ function openDiscordProfile(discordId) { * @returns {Promise} */ async function getBundleDateSize(ref) { + const authStore = useAuthStore(); + const sdkUnityVersion = authStore.cachedConfig.sdkUnityVersion; const avatarStore = useAvatarStore(); const { avatarDialog } = storeToRefs(avatarStore); const worldStore = useWorldStore(); @@ -243,7 +250,7 @@ async function getBundleDateSize(ref) { ) { continue; } - if (!compareUnityVersion(unityPackage.unitySortNumber)) { + if (!compareUnityVersion(unityPackage.unitySortNumber, sdkUnityVersion)) { continue; } diff --git a/src/shared/utils/invite.js b/src/shared/utils/invite.js index f9c6e2e0..f4d26f83 100644 --- a/src/shared/utils/invite.js +++ b/src/shared/utils/invite.js @@ -1,39 +1,34 @@ -import { - useFriendStore, - useInstanceStore, - useLocationStore, - useUserStore -} from '../../stores'; import { parseLocation } from './location'; /** * * @param {string} location - * @returns + * @param {object} deps + * @param {string} deps.currentUserId - current user's id + * @param {string} deps.lastLocationStr - last location string from location store + * @param {Map} deps.cachedInstances - instance cache map + * @returns {boolean} */ -function checkCanInvite(location) { +function checkCanInvite(location, deps) { if (!location) { return false; } - const userStore = useUserStore(); - const locationStore = useLocationStore(); - const instanceStore = useInstanceStore(); const L = parseLocation(location); - const instance = instanceStore.cachedInstances.get(location); + const instance = deps.cachedInstances?.get(location); if (instance?.closedAt) { return false; } if ( L.accessType === 'public' || L.accessType === 'group' || - L.userId === userStore.currentUser.id + L.userId === deps.currentUserId ) { return true; } if (L.accessType === 'invite' || L.accessType === 'friends') { return false; } - if (locationStore.lastLocation.location === location) { + if (deps.lastLocationStr === location) { return true; } return false; @@ -42,24 +37,25 @@ function checkCanInvite(location) { /** * * @param {string} location - * @returns + * @param {object} deps + * @param {string} deps.currentUserId - current user's id + * @param {Map} deps.cachedInstances - instance cache map + * @param {Map} deps.friends - friends map + * @returns {boolean} */ -function checkCanInviteSelf(location) { +function checkCanInviteSelf(location, deps) { if (!location) { return false; } - const userStore = useUserStore(); - const instanceStore = useInstanceStore(); - const friendStore = useFriendStore(); const L = parseLocation(location); - const instance = instanceStore.cachedInstances.get(location); + const instance = deps.cachedInstances?.get(location); if (instance?.closedAt) { return false; } - if (L.userId === userStore.currentUser.id) { + if (L.userId === deps.currentUserId) { return true; } - if (L.accessType === 'friends' && !friendStore.friends.has(L.userId)) { + if (L.accessType === 'friends' && !deps.friends?.has(L.userId)) { return false; } return true; diff --git a/src/shared/utils/location.js b/src/shared/utils/location.js index b33786f9..19391f38 100644 --- a/src/shared/utils/location.js +++ b/src/shared/utils/location.js @@ -1,5 +1,4 @@ import { isRealInstance } from './instance.js'; -import { useLocationStore } from '../../stores/location.js'; export { parseLocation, @@ -10,10 +9,12 @@ export { /** * - * @param friendsArr + * @param {Array} friendsArr + * @param {object} lastLocation - last location from location store + * @param {Set} lastLocation.friendList + * @param {string} lastLocation.location */ -function getFriendsLocations(friendsArr) { - const locationStore = useLocationStore(); +function getFriendsLocations(friendsArr, lastLocation) { // prevent the instance title display as "Traveling". if (!friendsArr?.length) { return ''; @@ -28,9 +29,11 @@ function getFriendsLocations(friendsArr) { return friend.ref.travelingToLocation; } } - for (const friend of friendsArr) { - if (locationStore.lastLocation.friendList.has(friend.id)) { - return locationStore.lastLocation.location; + if (lastLocation) { + for (const friend of friendsArr) { + if (lastLocation.friendList.has(friend.id)) { + return lastLocation.location; + } } } return friendsArr[0].ref?.location; diff --git a/src/shared/utils/user.js b/src/shared/utils/user.js index ac7f55cf..440644e3 100644 --- a/src/shared/utils/user.js +++ b/src/shared/utils/user.js @@ -1,4 +1,3 @@ -import { useAppearanceSettingsStore, useUserStore } from '../../stores'; import { HueToHex } from './base/ui'; import { convertFileUrlToImageUrl } from './common'; import { languageMappings } from '../constants'; @@ -40,21 +39,22 @@ function languageClass(language) { /** * * @param {string} userId + * @param {boolean} isDarkMode * @returns */ -async function getNameColour(userId) { +async function getNameColour(userId, isDarkMode) { const hue = await AppApi.GetColourFromUserID(userId); - return HueToHex(hue); + return HueToHex(hue, isDarkMode); } /** * * @param {object} user * @param {boolean} pendingOffline + * @param {object} currentUser - current user object from useUserStore * @returns */ -function userStatusClass(user, pendingOffline = false) { - const userStore = useUserStore(); +function userStatusClass(user, pendingOffline = false, currentUser) { const style = { 'status-icon': true }; @@ -67,8 +67,8 @@ function userStatusClass(user, pendingOffline = false) { } else if (user.userId) { id = user.userId; } - if (id === userStore.currentUser.id) { - const platform = userStore.currentUser.presence?.platform; + if (id === currentUser?.id) { + const platform = currentUser.presence?.platform; return { ...style, ...statusClass(user.status), @@ -89,10 +89,10 @@ function userStatusClass(user, pendingOffline = false) { user.location === 'private' && user.state === '' && id && - !userStore.currentUser.onlineFriends.includes(id) + !(currentUser?.onlineFriends || []).includes(id) ) { // temp fix - if (userStore.currentUser.activeFriends.includes(id)) { + if ((currentUser?.activeFriends || []).includes(id)) { // Active style.active = true; } else { @@ -166,21 +166,22 @@ function statusClass(status) { * @param {boolean} isIcon - is use for icon (about 40x40) * @param {string} resolution - requested icon resolution (default 128), * @param {boolean} isUserDialogIcon - is use for user dialog icon + * @param {boolean} displayVRCPlusIconsAsAvatar - from appearance settings store * @returns {string} - img url */ function userImage( user, isIcon = false, resolution = '128', - isUserDialogIcon = false + isUserDialogIcon = false, + displayVRCPlusIconsAsAvatar = false ) { - const appAppearanceSettingsStore = useAppearanceSettingsStore(); if (!user) { return ''; } if ( (isUserDialogIcon && user.userIcon) || - (appAppearanceSettingsStore.displayVRCPlusIconsAsAvatar && + (displayVRCPlusIconsAsAvatar && user.userIcon) ) { if (isIcon) { @@ -225,15 +226,15 @@ function userImage( /** * * @param {object} user + * @param {boolean} displayVRCPlusIconsAsAvatar - from appearance settings store * @returns {string|*} */ -function userImageFull(user) { +function userImageFull(user, displayVRCPlusIconsAsAvatar = false) { if (!user) { return ''; } - const appAppearanceSettingsStore = useAppearanceSettingsStore(); if ( - appAppearanceSettingsStore.displayVRCPlusIconsAsAvatar && + displayVRCPlusIconsAsAvatar && user.userIcon ) { return user.userIcon; diff --git a/src/stores/notification/index.js b/src/stores/notification/index.js index 09b7a950..81095240 100644 --- a/src/stores/notification/index.js +++ b/src/stores/notification/index.js @@ -347,7 +347,11 @@ export const useNotificationStore = defineStore('Notification', () => { } } } - if (!checkCanInvite(currentLocation)) { + if (!checkCanInvite(currentLocation, { + currentUserId: userStore.currentUser.id, + lastLocationStr: locationStore.lastLocation.location, + cachedInstances: instanceStore.cachedInstances + })) { return; } diff --git a/src/stores/settings/appearance.js b/src/stores/settings/appearance.js index 7ee9fe78..bab91d55 100644 --- a/src/stores/settings/appearance.js +++ b/src/stores/settings/appearance.js @@ -405,7 +405,7 @@ export const useAppearanceSettingsStore = defineStore( }); } if (randomUserColours.value) { - const colour = await getNameColour(userStore.currentUser.id); + const colour = await getNameColour(userStore.currentUser.id, isDarkMode.value); userStore.setCurrentUserColour(colour); userColourInit(); } else { @@ -441,7 +441,7 @@ export const useAppearanceSettingsStore = defineStore( for (const [userId, hue] of Object.entries(dictObject)) { const ref = userStore.cachedUsers.get(userId); if (typeof ref !== 'undefined') { - ref.$userColour = HueToHex(hue); + ref.$userColour = HueToHex(hue, isDarkMode.value); } } } @@ -460,7 +460,7 @@ export const useAppearanceSettingsStore = defineStore( ref.$trustSortNum = trust.trustSortNum; if (randomUserColours.value && watchState.isFriendsLoaded) { if (!ref.$userColour) { - getNameColour(ref.id).then((colour) => { + getNameColour(ref.id, isDarkMode.value).then((colour) => { ref.$userColour = colour; }); } @@ -1042,7 +1042,7 @@ export const useAppearanceSettingsStore = defineStore( if (!randomUserColours.value) { return; } - const colour = await getNameColour(userStore.currentUser.id); + const colour = await getNameColour(userStore.currentUser.id, isDarkMode.value); userStore.setCurrentUserColour(colour); await userColourInit(); } diff --git a/src/views/Charts/components/MutualFriends.vue b/src/views/Charts/components/MutualFriends.vue index 39176cfd..75e76eaf 100644 --- a/src/views/Charts/components/MutualFriends.vue +++ b/src/views/Charts/components/MutualFriends.vue @@ -274,13 +274,14 @@ useModalStore, useUserStore } from '../../../stores'; - import { userImage, userStatusClass } from '../../../shared/utils'; + import { useUserDisplay } from '../../../composables/useUserDisplay'; import { showUserDialog } from '../../../coordinators/userCoordinator'; import { database } from '../../../services/database'; import { watchState } from '../../../services/watchState'; import configRepository from '../../../services/config'; + const { userImage, userStatusClass } = useUserDisplay(); const { t } = useI18n(); const friendStore = useFriendStore(); const userStore = useUserStore(); diff --git a/src/views/Favorites/FavoritesFriend.vue b/src/views/Favorites/FavoritesFriend.vue index 2f1f02a1..d669508d 100644 --- a/src/views/Favorites/FavoritesFriend.vue +++ b/src/views/Favorites/FavoritesFriend.vue @@ -350,7 +350,8 @@ } from '../../components/ui/dropdown-menu'; import { useAppearanceSettingsStore, useFavoriteStore, useModalStore, useUserStore } from '../../stores'; import { ResizableHandle, ResizablePanel, ResizablePanelGroup } from '../../components/ui/resizable'; - import { debounce, userImage } from '../../shared/utils'; + import { debounce } from '../../shared/utils'; + import { useUserDisplay } from '../../composables/useUserDisplay'; import { favoriteRequest } from '../../api'; import { useFavoritesCardScaling } from './composables/useFavoritesCardScaling.js'; import { useFavoritesGroupPanel } from './composables/useFavoritesGroupPanel.js'; @@ -370,6 +371,7 @@ import FavoritesToolbar from './components/FavoritesToolbar.vue'; import FriendExportDialog from './dialogs/FriendExportDialog.vue'; + const { userImage } = useUserDisplay(); const friendGroupVisibilityOptions = ref(['public', 'friends', 'private']); const { diff --git a/src/views/Favorites/components/FavoritesFriendItem.vue b/src/views/Favorites/components/FavoritesFriendItem.vue index c0985bc9..f270098e 100644 --- a/src/views/Favorites/components/FavoritesFriendItem.vue +++ b/src/views/Favorites/components/FavoritesFriendItem.vue @@ -93,10 +93,11 @@ import { favoriteRequest } from '../../../api'; import { removeLocalFriendFavorite } from '../../../coordinators/favoriteCoordinator'; import { useFavoriteStore } from '../../../stores'; - import { userImage } from '../../../shared/utils'; + import { useUserDisplay } from '../../../composables/useUserDisplay'; import FavoritesMoveDropdown from './FavoritesMoveDropdown.vue'; + const { userImage } = useUserDisplay(); const props = defineProps({ favorite: { type: Object, required: true }, group: { type: Object, default: null }, diff --git a/src/views/Favorites/dialogs/FriendImportDialog.vue b/src/views/Favorites/dialogs/FriendImportDialog.vue index ba4ca525..6635c2fd 100644 --- a/src/views/Favorites/dialogs/FriendImportDialog.vue +++ b/src/views/Favorites/dialogs/FriendImportDialog.vue @@ -125,7 +125,8 @@ import { toast } from 'vue-sonner'; import { useI18n } from 'vue-i18n'; - import { removeFromArray, userImage, userImageFull } from '../../../shared/utils'; + import { removeFromArray } from '../../../shared/utils'; + import { useUserDisplay } from '../../../composables/useUserDisplay'; import { useFavoriteStore, useGalleryStore, useUserStore } from '../../../stores'; import { addLocalFriendFavorite } from '../../../coordinators/favoriteCoordinator'; import { favoriteRequest, userRequest } from '../../../api'; @@ -133,6 +134,7 @@ import { useVrcxVueTable } from '../../../lib/table/useVrcxVueTable'; import { showUserDialog } from '../../../coordinators/userCoordinator'; + const { userImage, userImageFull } = useUserDisplay(); const { t } = useI18n(); const emit = defineEmits(['update:friendImportDialogInput']); diff --git a/src/views/FriendsLocations/FriendsLocations.vue b/src/views/FriendsLocations/FriendsLocations.vue index 534dd955..29e9644d 100644 --- a/src/views/FriendsLocations/FriendsLocations.vue +++ b/src/views/FriendsLocations/FriendsLocations.vue @@ -147,7 +147,7 @@ import { useVirtualizer } from '@tanstack/vue-virtual'; import { Popover, PopoverContent, PopoverTrigger } from '../../components/ui/popover'; - import { useAppearanceSettingsStore, useFavoriteStore, useFriendStore } from '../../stores'; + import { useAppearanceSettingsStore, useFavoriteStore, useFriendStore, useLocationStore } from '../../stores'; import { Slider } from '../../components/ui/slider'; import { Switch } from '../../components/ui/switch'; import { getFriendsLocations } from '../../shared/utils/location.js'; @@ -175,6 +175,9 @@ const favoriteStore = useFavoriteStore(); const { favoriteFriendGroups, groupedByGroupKeyFavoriteFriends, localFriendFavorites } = storeToRefs(favoriteStore); + const locationStore = useLocationStore(); + const { lastLocation } = storeToRefs(locationStore); + const collapsedGroups = reactive(new Set()); const SEGMENTED_BASE_OPTIONS = [ @@ -319,7 +322,7 @@ .map((group, index) => { if (!Array.isArray(group) || group.length === 0) return null; const friends = group; - const instanceId = getFriendsLocations(friends) || `instance-${index + 1}`; + const instanceId = getFriendsLocations(friends, lastLocation.value) || `instance-${index + 1}`; return { instanceId: String(instanceId), friends diff --git a/src/views/FriendsLocations/components/FriendsLocationsCard.vue b/src/views/FriendsLocations/components/FriendsLocationsCard.vue index 49fb93db..4fd045ed 100644 --- a/src/views/FriendsLocations/components/FriendsLocationsCard.vue +++ b/src/views/FriendsLocations/components/FriendsLocationsCard.vue @@ -75,13 +75,14 @@ import { toast } from 'vue-sonner'; import { useI18n } from 'vue-i18n'; - import { isRealInstance, parseLocation, userImage, userStatusClass } from '../../../shared/utils'; + import { isRealInstance, parseLocation } from '../../../shared/utils'; import { useGameStore, useLaunchStore, useLocationStore, useUserStore } from '../../../stores'; import { instanceRequest, notificationRequest, queryRequest } from '../../../api'; - import { checkCanInvite, checkCanInviteSelf } from '../../../shared/utils/invite.js'; + import { useInviteChecks } from '../../../composables/useInviteChecks'; + import { useUserDisplay } from '../../../composables/useUserDisplay'; import Location from '../../../components/Location.vue'; -import { showUserDialog } from '../../../coordinators/userCoordinator'; + import { showUserDialog } from '../../../coordinators/userCoordinator'; const { t } = useI18n(); const { showSendBoopDialog } = useUserStore(); @@ -89,6 +90,8 @@ import { showUserDialog } from '../../../coordinators/userCoordinator'; const { lastLocation, lastLocationDestination } = storeToRefs(useLocationStore()); const { isGameRunning } = storeToRefs(useGameStore()); const { currentUser } = storeToRefs(useUserStore()); + const { checkCanInvite, checkCanInviteSelf } = useInviteChecks(); + const { userImage, userStatusClass } = useUserDisplay(); const props = defineProps({ friend: { diff --git a/src/views/Login/Login.vue b/src/views/Login/Login.vue index d7157bd8..f74c5dcd 100644 --- a/src/views/Login/Login.vue +++ b/src/views/Login/Login.vue @@ -179,11 +179,13 @@ import { useAppearanceSettingsStore, useAuthStore, useVRCXUpdaterStore } from '../../stores'; import { getLanguageName, languageCodes } from '../../localization'; - import { openExternalLink, userImage } from '../../shared/utils'; + import { openExternalLink } from '../../shared/utils'; + import { useUserDisplay } from '../../composables/useUserDisplay'; import { watchState } from '../../services/watchState'; import LoginSettingsDialog from './Dialog/LoginSettingsDialog.vue'; + const { userImage } = useUserDisplay(); const { showVRCXUpdateDialog } = useVRCXUpdaterStore(); const router = useRouter(); const route = useRoute(); diff --git a/src/views/Notifications/columns.jsx b/src/views/Notifications/columns.jsx index 2e4cbf50..723f1326 100644 --- a/src/views/Notifications/columns.jsx +++ b/src/views/Notifications/columns.jsx @@ -27,10 +27,12 @@ import { } from 'lucide-vue-next'; import { storeToRefs } from 'pinia'; -import { checkCanInvite, formatDateFilter } from '../../shared/utils'; +import { formatDateFilter } from '../../shared/utils'; +import { checkCanInvite } from '../../shared/utils/invite'; import { i18n } from '../../plugins'; import { useGameStore, + useInstanceStore, useLocationStore, useUiStore, useUserStore, @@ -70,10 +72,16 @@ export const createColumns = ({ const { isGameRunning } = storeToRefs(useGameStore()); const { isNotificationExpired } = useNotificationStore(); + const { cachedInstances } = storeToRefs(useInstanceStore()); + const canInvite = () => { const location = lastLocation.value?.location; return ( - Boolean(location) && isGameRunning.value && checkCanInvite(location) + Boolean(location) && isGameRunning.value && checkCanInvite(location, { + currentUserId: currentUser.value?.id, + lastLocationStr: lastLocation.value?.location, + cachedInstances: cachedInstances.value + }) ); }; diff --git a/src/views/Sidebar/components/FriendItem.vue b/src/views/Sidebar/components/FriendItem.vue index 2b7d89f4..741da58a 100644 --- a/src/views/Sidebar/components/FriendItem.vue +++ b/src/views/Sidebar/components/FriendItem.vue @@ -77,7 +77,7 @@ import Timer from '@/components/Timer.vue'; import { useAppearanceSettingsStore, useFriendStore, useUserStore } from '../../../stores'; - import { userImage, userStatusClass } from '../../../shared/utils'; + import { useUserDisplay } from '../../../composables/useUserDisplay'; import '@/styles/status-icon.css'; import { showUserDialog } from '../../../coordinators/userCoordinator'; @@ -90,6 +90,7 @@ import { confirmDeleteFriend } from '../../../coordinators/friendRelationshipCoo const { hideNicknames } = storeToRefs(useAppearanceSettingsStore()); const { isRefreshFriendsLoading, allFavoriteFriendIds } = storeToRefs(useFriendStore()); + const { userImage, userStatusClass } = useUserDisplay(); const { t } = useI18n(); diff --git a/src/views/Sidebar/components/FriendsSidebar.vue b/src/views/Sidebar/components/FriendsSidebar.vue index ba275153..1667400c 100644 --- a/src/views/Sidebar/components/FriendsSidebar.vue +++ b/src/views/Sidebar/components/FriendsSidebar.vue @@ -204,9 +204,10 @@ useUserStore } from '../../../stores'; import { buildFriendRow, buildInstanceHeaderRow, buildToggleRow, estimateRowSize } from '../friendsSidebarUtils'; - import { getFriendsSortFunction, isRealInstance, userImage, userStatusClass } from '../../../shared/utils'; + import { getFriendsSortFunction, isRealInstance } from '../../../shared/utils'; import { instanceRequest, notificationRequest, queryRequest, userRequest } from '../../../api'; - import { checkCanInvite, checkCanInviteSelf } from '../../../shared/utils/invite.js'; + import { useInviteChecks } from '../../../composables/useInviteChecks'; + import { useUserDisplay } from '../../../composables/useUserDisplay'; import { getFriendsLocations } from '../../../shared/utils/location.js'; import { parseLocation } from '../../../shared/utils'; @@ -246,6 +247,8 @@ import { showUserDialog } from '../../../coordinators/userCoordinator'; const { lastLocation, lastLocationDestination } = storeToRefs(useLocationStore()); const { isGameRunning } = storeToRefs(useGameStore()); const { currentUser } = storeToRefs(useUserStore()); + const { checkCanInvite, checkCanInviteSelf } = useInviteChecks(); + const { userImage, userStatusClass } = useUserDisplay(); const isFriendsGroupMe = ref(true); const isVIPFriends = ref(true); @@ -476,7 +479,7 @@ import { showUserDialog } from '../../../coordinators/userCoordinator'; if (!friendArr || !friendArr.length) return; const groupKey = friendArr?.[0]?.ref?.$location?.tag ?? `group-${groupIndex}`; rows.push( - buildInstanceHeaderRow(getFriendsLocations(friendArr), friendArr.length, `instance:${groupKey}`) + buildInstanceHeaderRow(getFriendsLocations(friendArr, lastLocation.value), friendArr.length, `instance:${groupKey}`) ); friendArr.forEach((friend, idx) => { rows.push( diff --git a/src/views/Sidebar/components/GroupsSidebar.vue b/src/views/Sidebar/components/GroupsSidebar.vue index f42f0e63..b654d8b0 100644 --- a/src/views/Sidebar/components/GroupsSidebar.vue +++ b/src/views/Sidebar/components/GroupsSidebar.vue @@ -95,7 +95,8 @@ ContextMenuTrigger } from '../../../components/ui/context-menu'; import { buildGroupHeaderRow, buildGroupItemRow, estimateGroupRowSize, getGroupId } from '../groupsSidebarUtils'; - import { checkCanInviteSelf, convertFileUrlToImageUrl, parseLocation } from '../../../shared/utils'; + import { convertFileUrlToImageUrl, parseLocation } from '../../../shared/utils'; + import { useInviteChecks } from '../../../composables/useInviteChecks'; import { useAppearanceSettingsStore, useGroupStore, useLaunchStore } from '../../../stores'; import { instanceRequest } from '../../../api'; @@ -108,6 +109,7 @@ const { isAgeGatedInstancesVisible } = storeToRefs(useAppearanceSettingsStore()); const { showGroupDialog, sortGroupInstancesByInGame } = useGroupStore(); const { groupInstances } = storeToRefs(useGroupStore()); + const { checkCanInviteSelf } = useInviteChecks(); const groupInstancesCfg = ref({}); const scrollViewportRef = ref(null); diff --git a/src/views/Sidebar/components/NotificationItem.vue b/src/views/Sidebar/components/NotificationItem.vue index a8468b59..454df93c 100644 --- a/src/views/Sidebar/components/NotificationItem.vue +++ b/src/views/Sidebar/components/NotificationItem.vue @@ -257,7 +257,8 @@ import { useGameStore, useGroupStore, useLocationStore, useNotificationStore, useUserStore } from '../../../stores'; import { showGroupDialog } from '../../../coordinators/groupCoordinator'; import { showUserDialog } from '../../../coordinators/userCoordinator'; - import { checkCanInvite, userImage } from '../../../shared/utils'; + import { useInviteChecks } from '../../../composables/useInviteChecks'; + import { useUserDisplay } from '../../../composables/useUserDisplay'; import Location from '../../../components/Location.vue'; @@ -275,6 +276,8 @@ const { lastLocation } = storeToRefs(useLocationStore()); const { isGameRunning } = storeToRefs(useGameStore()); const { openNotificationLink, isNotificationExpired } = useNotificationStore(); + const { checkCanInvite } = useInviteChecks(); + const { userImage } = useUserDisplay(); const senderName = computed(() => { const n = props.notification; diff --git a/src/views/Tools/dialogs/NoteExportDialog.vue b/src/views/Tools/dialogs/NoteExportDialog.vue index 856748bb..cd47154e 100644 --- a/src/views/Tools/dialogs/NoteExportDialog.vue +++ b/src/views/Tools/dialogs/NoteExportDialog.vue @@ -59,7 +59,8 @@ import { storeToRefs } from 'pinia'; import { useI18n } from 'vue-i18n'; - import { removeFromArray, userImage, userImageFull } from '../../../shared/utils'; + import { removeFromArray } from '../../../shared/utils'; + import { useUserDisplay } from '../../../composables/useUserDisplay'; import { useFriendStore, useGalleryStore, useUserStore } from '../../../stores'; import { createColumns } from './noteExportColumns.jsx'; import { miscRequest } from '../../../api'; @@ -68,6 +69,7 @@ import * as workerTimers from 'worker-timers'; import { showUserDialog } from '../../../coordinators/userCoordinator'; + const { userImage, userImageFull } = useUserDisplay(); const { t } = useI18n(); const { friends } = storeToRefs(useFriendStore());