refactor untils

This commit is contained in:
pa
2026-03-10 21:40:52 +09:00
parent 1dfd0bf54c
commit fe176f22ff
36 changed files with 1062 additions and 628 deletions

View File

@@ -600,14 +600,18 @@
import { avatarRequest } from '../../../api'; import { avatarRequest } from '../../../api';
import { database } from '../../../services/database'; import { database } from '../../../services/database';
import { formatJsonVars } from '../../../shared/utils/base/ui'; import { formatJsonVars } from '../../../shared/utils/base/ui';
import { handleImageUploadInput } from '../../../shared/utils/imageUpload'; import { handleImageUploadInput } from '../../../coordinators/imageUploadCoordinator';
import { runDeleteVRChatCacheFlow as deleteVRChatCache } from '../../../coordinators/gameCoordinator'; import { runDeleteVRChatCacheFlow as deleteVRChatCache } from '../../../coordinators/gameCoordinator';
import { showAvatarDialog, applyAvatar, selectAvatarWithoutConfirmation } from '../../../coordinators/avatarCoordinator'; import {
showAvatarDialog,
applyAvatar,
selectAvatarWithoutConfirmation
} from '../../../coordinators/avatarCoordinator';
import { useAvatarDialogCommands } from './useAvatarDialogCommands'; import { useAvatarDialogCommands } from './useAvatarDialogCommands';
import DialogJsonTab from '../DialogJsonTab.vue'; import DialogJsonTab from '../DialogJsonTab.vue';
import ImageCropDialog from '../ImageCropDialog.vue'; import ImageCropDialog from '../ImageCropDialog.vue';
import { showUserDialog } from '../../../coordinators/userCoordinator'; import { showUserDialog } from '../../../coordinators/userCoordinator';
const SetAvatarStylesDialog = defineAsyncComponent(() => import('./SetAvatarStylesDialog.vue')); const SetAvatarStylesDialog = defineAsyncComponent(() => import('./SetAvatarStylesDialog.vue'));
const SetAvatarTagsDialog = defineAsyncComponent(() => import('./SetAvatarTagsDialog.vue')); const SetAvatarTagsDialog = defineAsyncComponent(() => import('./SetAvatarTagsDialog.vue'));
@@ -617,8 +621,7 @@ import { showUserDialog } from '../../../coordinators/userCoordinator';
const avatarStore = useAvatarStore(); const avatarStore = useAvatarStore();
const { cachedAvatarModerations, cachedAvatars } = avatarStore; const { cachedAvatarModerations, cachedAvatars } = avatarStore;
const { avatarDialog } = storeToRefs(avatarStore); const { avatarDialog } = storeToRefs(avatarStore);
const { getAvatarGallery, applyAvatarModeration } = const { getAvatarGallery, applyAvatarModeration } = avatarStore;
avatarStore;
const { showFavoriteDialog } = useFavoriteStore(); const { showFavoriteDialog } = useFavoriteStore();
const { isGameRunning } = storeToRefs(useGameStore()); const { isGameRunning } = storeToRefs(useGameStore());
const { showFullscreenImageDialog } = useGalleryStore(); const { showFullscreenImageDialog } = useGalleryStore();

View File

@@ -30,11 +30,14 @@ vi.mock('../../../../shared/utils', () => ({
vi.mock('../../../../shared/utils/imageUpload', () => ({ vi.mock('../../../../shared/utils/imageUpload', () => ({
handleImageUploadInput: vi.fn(), handleImageUploadInput: vi.fn(),
readFileAsBase64: vi.fn(), readFileAsBase64: vi.fn(),
resizeImageToFitLimits: vi.fn(),
uploadImageLegacy: vi.fn(),
withUploadTimeout: vi.fn() withUploadTimeout: vi.fn()
})); }));
vi.mock('../../../../coordinators/imageUploadCoordinator', () => ({
resizeImageToFitLimits: vi.fn(),
uploadImageLegacy: vi.fn()
}));
const { copyToClipboard, openExternalLink } = const { copyToClipboard, openExternalLink } =
await import('../../../../shared/utils'); await import('../../../../shared/utils');
const { favoriteRequest, avatarRequest, avatarModerationRequest } = const { favoriteRequest, avatarRequest, avatarModerationRequest } =

View File

@@ -1,4 +1,5 @@
import { ref } from 'vue'; import { ref } from 'vue';
import { import {
avatarModerationRequest, avatarModerationRequest,
avatarRequest, avatarRequest,
@@ -11,18 +12,32 @@ import {
} from '../../../shared/utils'; } from '../../../shared/utils';
import { import {
handleImageUploadInput, handleImageUploadInput,
readFileAsBase64,
resizeImageToFitLimits, resizeImageToFitLimits,
uploadImageLegacy, uploadImageLegacy
} from '../../../coordinators/imageUploadCoordinator';
import {
readFileAsBase64,
withUploadTimeout withUploadTimeout
} from '../../../shared/utils/imageUpload'; } from '../../../shared/utils/imageUpload';
/** /**
* Composable for AvatarDialog command dispatch. * Composable for AvatarDialog command dispatch.
* Uses a command map pattern instead of nested switch-case chains. * Uses a command map pattern instead of nested switch-case chains.
*
* @param {import('vue').Ref} avatarDialog - reactive ref to the avatar dialog state * @param {import('vue').Ref} avatarDialog - reactive ref to the avatar dialog state
* @param {object} deps - external dependencies * @param {object} deps - external dependencies
* @param deps.t
* @param deps.toast
* @param deps.modalStore
* @param deps.userDialog
* @param deps.currentUser
* @param deps.cachedAvatars
* @param deps.cachedAvatarModerations
* @param deps.showAvatarDialog
* @param deps.showFavoriteDialog
* @param deps.applyAvatarModeration
* @param deps.applyAvatar
* @param deps.sortUserDialogAvatars
* @param deps.uiStore
* @returns {object} command composable API * @returns {object} command composable API
*/ */
export function useAvatarDialogCommands( export function useAvatarDialogCommands(
@@ -205,6 +220,9 @@ export function useAvatarDialogCommands(
// String commands: delegate to component callback // String commands: delegate to component callback
// Confirmed commands: { confirm: () => ({title, description, ...}), handler: fn } // Confirmed commands: { confirm: () => ({title, description, ...}), handler: fn }
/**
*
*/
function buildCommandMap() { function buildCommandMap() {
const D = () => avatarDialog.value; const D = () => avatarDialog.value;

View File

@@ -352,16 +352,16 @@
getFaviconUrl, getFaviconUrl,
hasGroupPermission, hasGroupPermission,
openExternalLink, openExternalLink,
refreshInstancePlayerCount,
userImage, userImage,
userStatusClass userStatusClass
} from '../../../shared/utils'; } from '../../../shared/utils';
import { refreshInstancePlayerCount } from '../../../coordinators/instanceCoordinator';
import { useGalleryStore, useGroupStore, useInstanceStore, useLocationStore, useUserStore } from '../../../stores'; import { useGalleryStore, useGroupStore, useInstanceStore, useLocationStore, useUserStore } from '../../../stores';
import { useGroupCalendarEvents } from './useGroupCalendarEvents'; import { useGroupCalendarEvents } from './useGroupCalendarEvents';
import GroupCalendarEventCard from '../../../views/Tools/components/GroupCalendarEventCard.vue'; import GroupCalendarEventCard from '../../../views/Tools/components/GroupCalendarEventCard.vue';
import InstanceActionBar from '../../InstanceActionBar.vue'; import InstanceActionBar from '../../InstanceActionBar.vue';
import { showUserDialog } from '../../../coordinators/userCoordinator'; import { showUserDialog } from '../../../coordinators/userCoordinator';
const props = defineProps({ const props = defineProps({
showGroupPostEditDialog: { showGroupPostEditDialog: {
@@ -376,7 +376,6 @@ import { showUserDialog } from '../../../coordinators/userCoordinator';
const { t } = useI18n(); const { t } = useI18n();
const { groupDialog } = storeToRefs(useGroupStore()); const { groupDialog } = storeToRefs(useGroupStore());
const { lastLocation } = storeToRefs(useLocationStore()); const { lastLocation } = storeToRefs(useLocationStore());
const { showFullscreenImageDialog } = useGalleryStore(); const { showFullscreenImageDialog } = useGalleryStore();

View File

@@ -54,7 +54,8 @@
import { useI18n } from 'vue-i18n'; import { useI18n } from 'vue-i18n';
import { miscRequest, userRequest } from '../../../api'; import { miscRequest, userRequest } from '../../../api';
import { replaceBioSymbols, saveUserMemo } from '../../../shared/utils'; import { replaceBioSymbols } from '../../../shared/utils';
import { saveUserMemo } from '../../../coordinators/memoCoordinator';
import { useAppearanceSettingsStore, useUserStore } from '../../../stores'; import { useAppearanceSettingsStore, useUserStore } from '../../../stores';
const { userDialog } = storeToRefs(useUserStore()); const { userDialog } = storeToRefs(useUserStore());

View File

@@ -483,13 +483,13 @@
isFriendOnline, isFriendOnline,
isRealInstance, isRealInstance,
openExternalLink, openExternalLink,
refreshInstancePlayerCount,
timeToText, timeToText,
userImage, userImage,
userOnlineFor, userOnlineFor,
userOnlineForTimestamp, userOnlineForTimestamp,
userStatusClass userStatusClass
} from '../../../shared/utils'; } from '../../../shared/utils';
import { refreshInstancePlayerCount } from '../../../coordinators/instanceCoordinator';
import { import {
useAdvancedSettingsStore, useAdvancedSettingsStore,
useAppearanceSettingsStore, useAppearanceSettingsStore,
@@ -505,7 +505,7 @@
import { queryRequest, userRequest } from '../../../api'; import { queryRequest, userRequest } from '../../../api';
import InstanceActionBar from '../../InstanceActionBar.vue'; import InstanceActionBar from '../../InstanceActionBar.vue';
import { showUserDialog } from '../../../coordinators/userCoordinator'; import { showUserDialog } from '../../../coordinators/userCoordinator';
const EditNoteAndMemoDialog = defineAsyncComponent(() => import('./EditNoteAndMemoDialog.vue')); const EditNoteAndMemoDialog = defineAsyncComponent(() => import('./EditNoteAndMemoDialog.vue'));

View File

@@ -108,7 +108,7 @@
import { storeToRefs } from 'pinia'; import { storeToRefs } from 'pinia';
import { useI18n } from 'vue-i18n'; import { useI18n } from 'vue-i18n';
import { refreshInstancePlayerCount } from '../../../shared/utils'; import { refreshInstancePlayerCount } from '../../../coordinators/instanceCoordinator';
import { useUserDisplay } from '../../../composables/useUserDisplay'; import { useUserDisplay } from '../../../composables/useUserDisplay';
import { import {
useAppearanceSettingsStore, useAppearanceSettingsStore,
@@ -125,7 +125,7 @@
const { userImage, userStatusClass } = useUserDisplay(); const { userImage, userStatusClass } = useUserDisplay();
const { isAgeGatedInstancesVisible } = storeToRefs(useAppearanceSettingsStore()); const { isAgeGatedInstancesVisible } = storeToRefs(useAppearanceSettingsStore());
const { currentUser } = storeToRefs(useUserStore()); const { currentUser } = storeToRefs(useUserStore());
const { worldDialog } = storeToRefs(useWorldStore()); const { worldDialog } = storeToRefs(useWorldStore());
const { lastLocation } = storeToRefs(useLocationStore()); const { lastLocation } = storeToRefs(useLocationStore());

View File

@@ -28,11 +28,14 @@ vi.mock('../../../../shared/utils', () => ({
vi.mock('../../../../shared/utils/imageUpload', () => ({ vi.mock('../../../../shared/utils/imageUpload', () => ({
handleImageUploadInput: vi.fn(), handleImageUploadInput: vi.fn(),
readFileAsBase64: vi.fn(), readFileAsBase64: vi.fn(),
resizeImageToFitLimits: vi.fn(),
uploadImageLegacy: vi.fn(),
withUploadTimeout: vi.fn((p) => p) withUploadTimeout: vi.fn((p) => p)
})); }));
vi.mock('../../../../coordinators/imageUploadCoordinator', () => ({
resizeImageToFitLimits: vi.fn(),
uploadImageLegacy: vi.fn()
}));
const { favoriteRequest, miscRequest, userRequest, worldRequest } = const { favoriteRequest, miscRequest, userRequest, worldRequest } =
await import('../../../../api'); await import('../../../../api');
const { openExternalLink } = await import('../../../../shared/utils'); const { openExternalLink } = await import('../../../../shared/utils');

View File

@@ -1,19 +1,21 @@
import { nextTick, ref } from 'vue'; import { nextTick, ref } from 'vue';
import {
handleImageUploadInput,
readFileAsBase64,
resizeImageToFitLimits,
uploadImageLegacy,
withUploadTimeout
} from '../../../shared/utils/imageUpload';
import { import {
favoriteRequest, favoriteRequest,
miscRequest, miscRequest,
userRequest, userRequest,
worldRequest worldRequest
} from '../../../api'; } from '../../../api';
import {
handleImageUploadInput,
resizeImageToFitLimits,
uploadImageLegacy
} from '../../../coordinators/imageUploadCoordinator';
import { openExternalLink, replaceVrcPackageUrl } from '../../../shared/utils'; import { openExternalLink, replaceVrcPackageUrl } from '../../../shared/utils';
import {
readFileAsBase64,
withUploadTimeout
} from '../../../shared/utils/imageUpload';
/** /**
* Composable for WorldDialog commands, prompt functions, and image upload. * Composable for WorldDialog commands, prompt functions, and image upload.
@@ -370,6 +372,9 @@ export function useWorldDialogCommands(
// String commands: delegate to component callback // String commands: delegate to component callback
// Confirmed commands: { confirm: () => ({title, description, ...}), handler: fn } // Confirmed commands: { confirm: () => ({title, description, ...}), handler: fn }
/**
*
*/
function buildCommandMap() { function buildCommandMap() {
const D = () => worldDialog.value; const D = () => worldDialog.value;

View File

@@ -0,0 +1,194 @@
import { storeToRefs } from 'pinia';
import { toast } from 'vue-sonner';
import {
useAuthStore,
useAvatarStore,
useInstanceStore,
useWorldStore
} from '../stores';
import {
extractFileId,
extractFileVersion,
extractVariantVersion
} from '../shared/utils/fileUtils';
import { compareUnityVersion } from '../shared/utils/avatar';
import { queryRequest } from '../api';
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--) {
const unityPackage = ref.unityPackages[i];
if (
unityPackage.variant &&
unityPackage.variant !== 'standard' &&
unityPackage.variant !== 'security'
) {
continue;
}
if (
unityPackage.platform === 'standalonewindows' &&
compareUnityVersion(unityPackage.unitySortNumber, sdkUnityVersion)
) {
assetUrl = unityPackage.assetUrl;
if (!unityPackage.variant || unityPackage.variant === 'standard') {
variant = 'security';
} else {
variant = unityPackage.variant;
}
break;
}
}
const id = extractFileId(assetUrl);
const version = parseInt(extractFileVersion(assetUrl), 10);
const variantVersion = parseInt(extractVariantVersion(assetUrl), 10);
await AssetBundleManager.DeleteCache(id, version, variant, variantVersion);
}
/**
*
* @param {object} ref
* @returns
*/
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--) {
const unityPackage = ref.unityPackages[i];
if (unityPackage.variant && unityPackage.variant !== 'security') {
continue;
}
if (
unityPackage.platform === 'standalonewindows' &&
compareUnityVersion(unityPackage.unitySortNumber, sdkUnityVersion)
) {
assetUrl = unityPackage.assetUrl;
if (!unityPackage.variant || unityPackage.variant === 'standard') {
variant = 'security';
} else {
variant = unityPackage.variant;
}
break;
}
}
if (!assetUrl) {
assetUrl = ref.assetUrl;
}
const id = extractFileId(assetUrl);
const version = parseInt(extractFileVersion(assetUrl), 10);
const variantVersion = parseInt(extractVariantVersion(assetUrl), 10);
if (!id || !version) {
return { Item1: -1, Item2: false, Item3: '' };
}
try {
return AssetBundleManager.CheckVRChatCache(
id,
version,
variant,
variantVersion
);
} catch (err) {
console.error('Failed reading VRChat cache size:', err);
toast.error(`Failed reading VRChat cache size: ${err}`);
return { Item1: -1, Item2: false, Item3: '' };
}
}
/**
*
* @param {object} ref
* @returns {Promise<object>}
*/
async function getBundleDateSize(ref) {
const authStore = useAuthStore();
const sdkUnityVersion = authStore.cachedConfig.sdkUnityVersion;
const avatarStore = useAvatarStore();
const { avatarDialog } = storeToRefs(avatarStore);
const worldStore = useWorldStore();
const { worldDialog } = storeToRefs(worldStore);
const instanceStore = useInstanceStore();
const { currentInstanceWorld, currentInstanceLocation } =
storeToRefs(instanceStore);
const bundleJson = {};
for (let i = ref.unityPackages.length - 1; i > -1; i--) {
const unityPackage = ref.unityPackages[i];
if (!unityPackage) {
continue;
}
if (
unityPackage.variant &&
unityPackage.variant !== 'standard' &&
unityPackage.variant !== 'security'
) {
continue;
}
if (
!compareUnityVersion(unityPackage.unitySortNumber, sdkUnityVersion)
) {
continue;
}
const platform = unityPackage.platform;
if (bundleJson[platform]) {
continue;
}
const assetUrl = unityPackage.assetUrl;
const fileId = extractFileId(assetUrl);
const version = parseInt(extractFileVersion(assetUrl), 10);
let variant = '';
if (!unityPackage.variant || unityPackage.variant === 'standard') {
variant = 'security';
} else {
variant = unityPackage.variant;
}
if (!fileId || !version) {
continue;
}
const args = await queryRequest.fetch('fileAnalysis', {
fileId,
version,
variant
});
if (!args?.json?.success) {
continue;
}
const json = args.json;
if (typeof json.fileSize !== 'undefined') {
json._fileSize = `${(json.fileSize / 1048576).toFixed(2)} MB`;
}
if (typeof json.uncompressedSize !== 'undefined') {
json._uncompressedSize = `${(json.uncompressedSize / 1048576).toFixed(2)} MB`;
}
if (typeof json.avatarStats?.totalTextureUsage !== 'undefined') {
json._totalTextureUsage = `${(json.avatarStats.totalTextureUsage / 1048576).toFixed(2)} MB`;
}
bundleJson[platform] = json;
if (avatarDialog.value.id === ref.id) {
// update avatar dialog
avatarDialog.value.fileAnalysis[platform] = json;
}
// update world dialog
if (worldDialog.value.id === ref.id) {
worldDialog.value.fileAnalysis[platform] = json;
}
// update player list
if (currentInstanceLocation.value.worldId === ref.id) {
currentInstanceWorld.value.fileAnalysis[platform] = json;
}
}
return bundleJson;
}
export { deleteVRChatCache, checkVRChatCache, getBundleDateSize };

View File

@@ -1,4 +1,4 @@
import { useAppearanceSettingsStore } from '../../../stores'; import { useAppearanceSettingsStore } from '../stores';
function padZero(num) { function padZero(num) {
return String(num).padStart(2, '0'); return String(num).padStart(2, '0');

View File

@@ -1,7 +1,7 @@
import { toast } from 'vue-sonner'; import { toast } from 'vue-sonner';
import { AppDebug } from '../services/appConfig'; import { AppDebug } from '../services/appConfig';
import { migrateMemos } from '../shared/utils'; import { migrateMemos } from './memoCoordinator';
import { reconnectWebSocket } from '../services/websocket'; import { reconnectWebSocket } from '../services/websocket';
import { useAuthStore } from '../stores/auth'; import { useAuthStore } from '../stores/auth';
import { useFriendStore } from '../stores/friend'; import { useFriendStore } from '../stores/friend';

View File

@@ -0,0 +1,198 @@
import { toast } from 'vue-sonner';
import { $throw } from '../services/request';
import { AppDebug } from '../services/appConfig.js';
import { extractFileId } from '../shared/utils';
import { imageRequest } from '../api';
function resolveMessage(message) {
if (typeof message === 'function') {
return message();
}
return message;
}
function getInputElement(selector) {
if (!selector) {
return null;
}
if (typeof selector === 'function') {
return selector();
}
if (typeof selector === 'string') {
return document.querySelector(selector);
}
return selector;
}
export function handleImageUploadInput(event, options = {}) {
const {
inputSelector,
// 20MB
maxSize = 20000000,
acceptPattern = /image.*/,
tooLargeMessage,
invalidTypeMessage,
onClear
} = options;
const clearInput = () => {
onClear?.();
const input = getInputElement(inputSelector);
if (input) {
input.value = '';
}
};
const files = event?.target?.files || event?.dataTransfer?.files;
if (!files || files.length === 0) {
clearInput();
return { file: null, clearInput };
}
const file = files[0];
if (file.size >= maxSize) {
if (tooLargeMessage) {
toast.error(resolveMessage(tooLargeMessage));
}
clearInput();
return { file: null, clearInput };
}
let acceptRegex = null;
if (acceptPattern) {
acceptRegex =
acceptPattern instanceof RegExp
? acceptPattern
: new RegExp(acceptPattern);
}
if (acceptRegex && !acceptRegex.test(file.type)) {
if (invalidTypeMessage) {
toast.error(resolveMessage(invalidTypeMessage));
}
clearInput();
return { file: null, clearInput };
}
return { file, clearInput };
}
/**
* @param {string} base64Data - base64 encoded image
* @returns {Promise<string>} resized base64 encoded image
*/
export async function resizeImageToFitLimits(base64Data) {
// frontend limit check = 20MB
return AppApi.ResizeImageToFitLimits(base64Data);
}
/**
* Upload image through AWS
* @param {'avatar'|'world'} type
* @param {object} opts
* @param {string} opts.entityId - avatar or world id
* @param {string} opts.imageUrl - current imageUrl on the entity
* @param {string} opts.base64File - base64 encoded image data
* @param {Blob} opts.blob - the original blob (used for file size)
*/
export async function uploadImageLegacy(
type,
{ entityId, imageUrl, base64File, blob }
) {
const apiMap = {
avatar: {
uploadImage: imageRequest.uploadAvatarImage,
fileStart: imageRequest.uploadAvatarImageFileStart,
fileFinish: imageRequest.uploadAvatarImageFileFinish,
sigStart: imageRequest.uploadAvatarImageSigStart,
sigFinish: imageRequest.uploadAvatarImageSigFinish,
setImage: imageRequest.setAvatarImage
},
world: {
uploadImage: imageRequest.uploadWorldImage,
fileStart: imageRequest.uploadWorldImageFileStart,
fileFinish: imageRequest.uploadWorldImageFileFinish,
sigStart: imageRequest.uploadWorldImageSigStart,
sigFinish: imageRequest.uploadWorldImageSigFinish,
setImage: imageRequest.setWorldImage
}
};
const api = apiMap[type];
const fileMd5 = await AppApi.MD5File(base64File);
const fileSizeInBytes = parseInt(blob.size, 10);
const base64SignatureFile = await AppApi.SignFile(base64File);
const signatureMd5 = await AppApi.MD5File(base64SignatureFile);
const signatureSizeInBytes = parseInt(
await AppApi.FileLength(base64SignatureFile),
10
);
const fileId = extractFileId(imageUrl);
// imageInit
const uploadRes = await api.uploadImage(
{ fileMd5, fileSizeInBytes, signatureMd5, signatureSizeInBytes },
fileId
);
const uploadedFileId = uploadRes.json.id;
const fileVersion =
uploadRes.json.versions[uploadRes.json.versions.length - 1].version;
// imageFileStart
const fileStartRes = await api.fileStart({
fileId: uploadedFileId,
fileVersion
});
// uploadImageFileAWS
const fileAwsRes = await webApiService.execute({
url: fileStartRes.json.url,
uploadFilePUT: true,
fileData: base64File,
fileMIME: 'image/png',
fileMD5: fileMd5
});
if (fileAwsRes.status !== 200) {
$throw(
fileAwsRes.status,
`${type} image upload failed`,
fileStartRes.json.url
);
}
// imageFileFinish
await api.fileFinish({ fileId: uploadedFileId, fileVersion });
// imageSigStart
const sigStartRes = await api.sigStart({
fileId: uploadedFileId,
fileVersion
});
// uploadImageSigAWS
const sigAwsRes = await webApiService.execute({
url: sigStartRes.json.url,
uploadFilePUT: true,
fileData: base64SignatureFile,
fileMIME: 'application/x-rsync-signature',
fileMD5: signatureMd5
});
if (sigAwsRes.status !== 200) {
$throw(
sigAwsRes.status,
`${type} image upload failed`,
sigStartRes.json.url
);
}
// imageSigFinish
await api.sigFinish({ fileId: uploadedFileId, fileVersion });
// imageSet
const newImageUrl = `${AppDebug.endpointDomain}/file/${uploadedFileId}/${fileVersion}/file`;
const setRes = await api.setImage({ id: entityId, imageUrl: newImageUrl });
if (setRes.json.imageUrl !== newImageUrl) {
$throw(0, `${type} image change failed`, newImageUrl);
}
}

View File

@@ -0,0 +1,18 @@
import { instanceRequest } from '../api';
import { parseLocation } from '../shared/utils/locationParser';
/**
*
* @param {object} instance
*/
function refreshInstancePlayerCount(instance) {
const L = parseLocation(instance);
if (L.isRealInstance) {
instanceRequest.getInstance({
worldId: L.worldId,
instanceId: L.instanceId
});
}
}
export { refreshInstancePlayerCount };

View File

@@ -1,5 +1,5 @@
import { useFriendStore, useUserStore } from '../../stores'; import { useFriendStore, useUserStore } from '../stores';
import { database } from '../../services/database'; import { database } from '../services/database';
/** /**
* @returns {Promise<void>} * @returns {Promise<void>}

View File

@@ -10,12 +10,12 @@ import {
evictMapCache, evictMapCache,
extractFileId, extractFileId,
findUserByDisplayName, findUserByDisplayName,
getUserMemo,
getWorldName, getWorldName,
isRealInstance, isRealInstance,
parseLocation, parseLocation,
sanitizeUserJson sanitizeUserJson
} from '../shared/utils'; } from '../shared/utils';
import { getUserMemo } from './memoCoordinator';
import { import {
avatarRequest, avatarRequest,
instanceRequest, instanceRequest,
@@ -73,7 +73,14 @@ export function applyUser(json) {
const moderationStore = useModerationStore(); const moderationStore = useModerationStore();
const photonStore = usePhotonStore(); const photonStore = usePhotonStore();
const { currentUser, cachedUsers, currentTravelers, customUserTags, state, userDialog } = userStore; const {
currentUser,
cachedUsers,
currentTravelers,
customUserTags,
state,
userDialog
} = userStore;
let ref = cachedUsers.get(json.id); let ref = cachedUsers.get(json.id);
let hasPropChanged = false; let hasPropChanged = false;
@@ -114,10 +121,8 @@ export function applyUser(json) {
if (json.state !== 'online') { if (json.state !== 'online') {
runUpdateFriendFlow(ref.id, json.state); runUpdateFriendFlow(ref.id, json.state);
} }
const { const { hasPropChanged: _hasPropChanged, changedProps: _changedProps } =
hasPropChanged: _hasPropChanged, diffObjectProps(ref, json, arraysMatch);
changedProps: _changedProps
} = diffObjectProps(ref, json, arraysMatch);
for (const prop in json) { for (const prop in json) {
if (typeof json[prop] !== 'undefined') { if (typeof json[prop] !== 'undefined') {
ref[prop] = json[prop]; ref[prop] = json[prop];
@@ -235,10 +240,7 @@ export function applyUser(json) {
} }
} }
if (hasPropChanged) { if (hasPropChanged) {
if ( if (changedProps.location && changedProps.location[0] !== 'traveling') {
changedProps.location &&
changedProps.location[0] !== 'traveling'
) {
const ts = Date.now(); const ts = Date.now();
changedProps.location.push(ts - ref.$location_at); changedProps.location.push(ts - ref.$location_at);
ref.$location_at = ts; ref.$location_at = ts;
@@ -286,11 +288,7 @@ export function showUserDialog(userId) {
const D = userDialog; const D = userDialog;
D.visible = true; D.visible = true;
if (isMainDialogOpen && D.id === userId) { if (isMainDialogOpen && D.id === userId) {
uiStore.setDialogCrumbLabel( uiStore.setDialogCrumbLabel('user', D.id, D.ref?.displayName || D.id);
'user',
D.id,
D.ref?.displayName || D.id
);
userStore.applyUserDialogLocation(true); userStore.applyUserDialogLocation(true);
return; return;
} }
@@ -429,8 +427,7 @@ export function showUserDialog(userId) {
D.joinCount = ref1.joinCount; D.joinCount = ref1.joinCount;
D.timeSpent = ref1.timeSpent; D.timeSpent = ref1.timeSpent;
} }
const displayNameMap = const displayNameMap = ref1.previousDisplayNames;
ref1.previousDisplayNames;
const userNotifications = const userNotifications =
await database.getFriendLogHistoryForUserId( await database.getFriendLogHistoryForUserId(
D.id, D.id,
@@ -457,12 +454,10 @@ export function showUserDialog(userId) {
} }
D.dateFriendedInfo = dateFriendedInfo; D.dateFriendedInfo = dateFriendedInfo;
if (dateFriendedInfo.length > 0) { if (dateFriendedInfo.length > 0) {
const latestFriendedInfo = const latestFriendedInfo = dateFriendedInfo[0];
dateFriendedInfo[0];
D.unFriended = D.unFriended =
latestFriendedInfo.type === 'Unfriend'; latestFriendedInfo.type === 'Unfriend';
D.dateFriended = D.dateFriended = latestFriendedInfo.created_at;
latestFriendedInfo.created_at;
} }
displayNameMap.forEach( displayNameMap.forEach(
(updated_at, displayName) => { (updated_at, displayName) => {
@@ -473,27 +468,24 @@ export function showUserDialog(userId) {
} }
); );
}); });
AppApi.GetVRChatUserModeration( AppApi.GetVRChatUserModeration(currentUser.id, userId).then(
currentUser.id, (result) => {
userId D.avatarModeration = result;
).then((result) => { if (result === 4) {
D.avatarModeration = result; D.isHideAvatar = true;
if (result === 4) { } else if (result === 5) {
D.isHideAvatar = true; D.isShowAvatar = true;
} else if (result === 5) { }
D.isShowAvatar = true;
} }
}); );
if (!currentUser.hasSharedConnectionsOptOut) { if (!currentUser.hasSharedConnectionsOptOut) {
try { try {
queryRequest queryRequest
.fetch('mutualCounts', { userId }) .fetch('mutualCounts', { userId })
.then((args) => { .then((args) => {
if (args.params.userId === D.id) { if (args.params.userId === D.id) {
D.mutualFriendCount = D.mutualFriendCount = args.json.friends;
args.json.friends; D.mutualGroupCount = args.json.groups;
D.mutualGroupCount =
args.json.groups;
} }
}); });
} catch (error) { } catch (error) {
@@ -501,8 +493,7 @@ export function showUserDialog(userId) {
} }
} }
} else { } else {
D.previousDisplayNames = D.previousDisplayNames = currentUser.pastDisplayNames;
currentUser.pastDisplayNames;
database database
.getUserStats(D.ref, inCurrentWorld) .getUserStats(D.ref, inCurrentWorld)
.then((ref1) => { .then((ref1) => {
@@ -673,11 +664,8 @@ export function handleConfig(args) {
if (typeof args.ref?.whiteListedAssetUrls !== 'object') { if (typeof args.ref?.whiteListedAssetUrls !== 'object') {
console.error('Invalid config whiteListedAssetUrls'); console.error('Invalid config whiteListedAssetUrls');
} }
AppApi.PopulateImageHosts( AppApi.PopulateImageHosts(JSON.stringify(args.ref.whiteListedAssetUrls));
JSON.stringify(args.ref.whiteListedAssetUrls) const languages = args.ref?.constants?.LANGUAGE?.SPOKEN_LANGUAGE_OPTIONS;
);
const languages =
args.ref?.constants?.LANGUAGE?.SPOKEN_LANGUAGE_OPTIONS;
if (!languages) { if (!languages) {
return; return;
} }
@@ -1047,10 +1035,7 @@ export function updateAutoStateChange() {
} }
const params = { status: newStatus }; const params = { status: newStatus };
if ( if (withCompany && generalSettingsStore.autoStateChangeCompanyDescEnabled) {
withCompany &&
generalSettingsStore.autoStateChangeCompanyDescEnabled
) {
params.statusDescription = params.statusDescription =
generalSettingsStore.autoStateChangeCompanyDesc; generalSettingsStore.autoStateChangeCompanyDesc;
} else if ( } else if (

View File

@@ -8,11 +8,11 @@ import {
evictMapCache, evictMapCache,
getAvailablePlatforms, getAvailablePlatforms,
getBundleDateSize, getBundleDateSize,
getWorldMemo,
isRealInstance, isRealInstance,
parseLocation, parseLocation,
sanitizeEntityJson sanitizeEntityJson
} from '../shared/utils'; } from '../shared/utils';
import { getWorldMemo } from './memoCoordinator';
import { instanceRequest, queryRequest, worldRequest } from '../api'; import { instanceRequest, queryRequest, worldRequest } from '../api';
import { database } from '../services/database'; import { database } from '../services/database';
import { patchWorldFromEvent } from '../queries'; import { patchWorldFromEvent } from '../queries';
@@ -118,21 +118,13 @@ export function showWorldDialog(tag, shortName = null, options = {}) {
.then((args) => { .then((args) => {
if (D.id === args.ref.id) { if (D.id === args.ref.id) {
D.ref = args.ref; D.ref = args.ref;
uiStore.setDialogCrumbLabel( uiStore.setDialogCrumbLabel('world', D.id, D.ref?.name || D.id);
'world',
D.id,
D.ref?.name || D.id
);
D.visible = true; D.visible = true;
D.loading = false; D.loading = false;
D.isFavorite = favoriteStore.getCachedFavoritesByObjectId( D.isFavorite = favoriteStore.getCachedFavoritesByObjectId(D.id);
D.id
);
if (!D.isFavorite) { if (!D.isFavorite) {
D.isFavorite = D.isFavorite =
favoriteStore.localWorldFavoritesList.includes( favoriteStore.localWorldFavoritesList.includes(D.id);
D.id
);
} }
let { isPC, isQuest, isIos } = getAvailablePlatforms( let { isPC, isQuest, isIos } = getAvailablePlatforms(
args.ref.unityPackages args.ref.unityPackages

View File

@@ -0,0 +1,153 @@
import { afterEach, beforeEach, describe, expect, test, vi } from 'vitest';
const mocks = vi.hoisted(() => ({
toast: {
success: vi.fn(),
error: vi.fn()
},
searchStore: {
directAccessParse: vi.fn()
},
modalStore: {
confirm: vi.fn()
},
i18n: {
global: {
t: vi.fn(() => 'copy failed')
}
}
}));
vi.mock('vue-sonner', () => ({
toast: mocks.toast
}));
vi.mock('../../../stores', () => ({
useSearchStore: () => mocks.searchStore,
useModalStore: () => mocks.modalStore
}));
vi.mock('../../../plugins/i18n', () => ({
i18n: mocks.i18n
}));
import {
copyToClipboard,
downloadAndSaveJson,
openDiscordProfile,
openExternalLink,
openFolderGeneric
} from '../appActions';
function flushPromises() {
return new Promise((resolve) => setTimeout(resolve, 0));
}
describe('appActions utils', () => {
let consoleErrorSpy;
beforeEach(() => {
consoleErrorSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
vi.clearAllMocks();
mocks.searchStore.directAccessParse.mockReturnValue(false);
mocks.modalStore.confirm.mockResolvedValue({ ok: false });
Object.defineProperty(navigator, 'clipboard', {
configurable: true,
value: {
writeText: vi.fn().mockResolvedValue(undefined)
}
});
globalThis.AppApi = {
OpenLink: vi.fn(),
OpenDiscordProfile: vi.fn().mockResolvedValue(undefined),
OpenFolderAndSelectItem: vi.fn()
};
});
afterEach(() => {
consoleErrorSpy.mockRestore();
});
test('downloadAndSaveJson returns early for invalid params', () => {
downloadAndSaveJson('', { a: 1 });
downloadAndSaveJson('name', null);
expect(document.querySelectorAll('a[download]').length).toBe(0);
});
test('downloadAndSaveJson creates and clicks download link', () => {
const appendSpy = vi.spyOn(document.body, 'appendChild');
const removeSpy = vi.spyOn(document.body, 'removeChild');
downloadAndSaveJson('profile', { id: 1 });
expect(appendSpy).toHaveBeenCalledTimes(1);
expect(removeSpy).toHaveBeenCalledTimes(1);
appendSpy.mockRestore();
removeSpy.mockRestore();
});
test('copyToClipboard shows success toast', async () => {
copyToClipboard('hello', 'copied');
await flushPromises();
expect(navigator.clipboard.writeText).toHaveBeenCalledWith('hello');
expect(mocks.toast.success).toHaveBeenCalledWith('copied');
});
test('copyToClipboard shows translated error toast on failure', async () => {
navigator.clipboard.writeText.mockRejectedValue(new Error('denied'));
copyToClipboard('hello');
await flushPromises();
await flushPromises();
expect(mocks.toast.error).toHaveBeenCalledWith('copy failed');
});
test('openExternalLink returns early when direct access parse succeeds', async () => {
mocks.searchStore.directAccessParse.mockReturnValue(true);
openExternalLink('vrcx://user/usr_1');
await flushPromises();
expect(mocks.modalStore.confirm).not.toHaveBeenCalled();
expect(AppApi.OpenLink).not.toHaveBeenCalled();
});
test('openExternalLink copies link when confirm is canceled', async () => {
mocks.modalStore.confirm.mockResolvedValue({
ok: false,
reason: 'cancel'
});
openExternalLink('https://example.com');
await flushPromises();
await flushPromises();
expect(navigator.clipboard.writeText).toHaveBeenCalledWith(
'https://example.com'
);
});
test('openExternalLink opens link when confirmed', async () => {
mocks.modalStore.confirm.mockResolvedValue({ ok: true });
openExternalLink('https://example.com');
await flushPromises();
expect(AppApi.OpenLink).toHaveBeenCalledWith('https://example.com');
});
test('openDiscordProfile validates empty discord id', () => {
openDiscordProfile('');
expect(mocks.toast.error).toHaveBeenCalledWith('No Discord ID provided!');
});
test('openDiscordProfile shows error toast when api fails', async () => {
AppApi.OpenDiscordProfile.mockRejectedValue(new Error('fail'));
openDiscordProfile('123');
await flushPromises();
expect(mocks.toast.error).toHaveBeenCalledWith(
'Failed to open Discord profile!'
);
});
test('openFolderGeneric delegates to AppApi', () => {
openFolderGeneric('/tmp/a.txt');
expect(AppApi.OpenFolderAndSelectItem).toHaveBeenCalledWith(
'/tmp/a.txt',
true
);
});
});

View File

@@ -0,0 +1,40 @@
import { describe, expect, test, vi } from 'vitest';
describe('loadEcharts', () => {
test('loads echarts module', async () => {
vi.resetModules();
vi.doMock('echarts', () => ({
__esModule: true,
marker: 'mock-echarts'
}));
const { loadEcharts } = await import('../chart.js');
const module = await loadEcharts();
expect(module).toMatchObject({ marker: 'mock-echarts' });
});
test('returns cached module reference on subsequent calls', async () => {
vi.resetModules();
vi.doMock('echarts', () => ({
__esModule: true,
marker: 'mock-echarts'
}));
const { loadEcharts } = await import('../chart.js');
const first = await loadEcharts();
const second = await loadEcharts();
expect(second).toBe(first);
});
test('rejects when echarts import fails', async () => {
vi.resetModules();
vi.doMock('echarts', () => {
throw new Error('import failed');
});
const { loadEcharts } = await import('../chart.js');
await expect(loadEcharts()).rejects.toThrow();
});
});

View File

@@ -13,7 +13,7 @@ vi.mock('../../../services/appConfig', () => ({
AppDebug: { endpointDomain: 'https://api.vrchat.cloud/api/1' } AppDebug: { endpointDomain: 'https://api.vrchat.cloud/api/1' }
})); }));
vi.mock('../../utils/index.js', () => ({ vi.mock('../../../shared/utils', () => ({
extractFileId: vi.fn() extractFileId: vi.fn()
})); }));
@@ -22,7 +22,8 @@ vi.mock('../../../api', () => ({
})); }));
import { toast } from 'vue-sonner'; import { toast } from 'vue-sonner';
import { handleImageUploadInput, withUploadTimeout } from '../imageUpload'; import { withUploadTimeout } from '../imageUpload';
import { handleImageUploadInput } from '../../../coordinators/imageUploadCoordinator';
// ─── withUploadTimeout ─────────────────────────────────────────────── // ─── withUploadTimeout ───────────────────────────────────────────────

View File

@@ -0,0 +1,162 @@
import { afterEach, beforeEach, describe, expect, test, vi } from 'vitest';
const mocks = vi.hoisted(() => ({
friends: new Map(),
setUserDialogMemo: vi.fn(),
database: {
getUserMemo: vi.fn(),
setUserMemo: vi.fn(),
deleteUserMemo: vi.fn(),
getAllUserMemos: vi.fn(),
getWorldMemo: vi.fn()
},
storage: {
GetAll: vi.fn(),
Remove: vi.fn()
}
}));
vi.mock('../../../stores', () => ({
useFriendStore: () => ({
friends: mocks.friends
}),
useUserStore: () => ({
setUserDialogMemo: (...args) => mocks.setUserDialogMemo(...args)
})
}));
vi.mock('../../../services/database', () => ({
database: mocks.database
}));
import {
getAllUserMemos,
getUserMemo,
getWorldMemo,
migrateMemos,
saveUserMemo
} from '../../../coordinators/memoCoordinator.js';
describe('memos utils', () => {
let consoleErrorSpy;
beforeEach(() => {
consoleErrorSpy = vi
.spyOn(console, 'error')
.mockImplementation(() => {});
mocks.friends = new Map();
mocks.setUserDialogMemo.mockReset();
mocks.database.getUserMemo.mockReset();
mocks.database.setUserMemo.mockReset();
mocks.database.deleteUserMemo.mockReset();
mocks.database.getAllUserMemos.mockReset();
mocks.database.getWorldMemo.mockReset();
mocks.storage.GetAll.mockReset();
mocks.storage.Remove.mockReset();
globalThis.VRCXStorage = mocks.storage;
});
afterEach(() => {
consoleErrorSpy.mockRestore();
});
test('getUserMemo returns fallback when database throws', async () => {
mocks.database.getUserMemo.mockRejectedValue(new Error('boom'));
const result = await getUserMemo('usr_1');
expect(result).toEqual({
userId: '',
editedAt: '',
memo: ''
});
});
test('getWorldMemo returns fallback when database throws', async () => {
mocks.database.getWorldMemo.mockRejectedValue(new Error('boom'));
const result = await getWorldMemo('wrld_1');
expect(result).toEqual({
worldId: '',
editedAt: '',
memo: ''
});
});
test('saveUserMemo persists memo and syncs friend fields', async () => {
const friend = { memo: '', $nickName: '' };
mocks.friends.set('usr_1', friend);
await saveUserMemo('usr_1', 'Nick\nmore');
expect(mocks.database.setUserMemo).toHaveBeenCalledTimes(1);
expect(mocks.database.deleteUserMemo).not.toHaveBeenCalled();
expect(friend.memo).toBe('Nick\nmore');
expect(friend.$nickName).toBe('Nick');
expect(mocks.setUserDialogMemo).toHaveBeenCalledWith('Nick\nmore');
});
test('saveUserMemo deletes memo and clears nickname on empty input', async () => {
const friend = { memo: 'old', $nickName: 'old' };
mocks.friends.set('usr_1', friend);
await saveUserMemo('usr_1', '');
expect(mocks.database.deleteUserMemo).toHaveBeenCalledWith('usr_1');
expect(friend.memo).toBe('');
expect(friend.$nickName).toBe('');
expect(mocks.setUserDialogMemo).toHaveBeenCalledWith('');
});
test('getAllUserMemos applies memo data to existing cached friends', async () => {
const friend1 = { memo: '', $nickName: '' };
const friend2 = { memo: '', $nickName: '' };
mocks.friends.set('usr_1', friend1);
mocks.friends.set('usr_2', friend2);
mocks.database.getAllUserMemos.mockResolvedValue([
{ userId: 'usr_1', memo: 'Alpha\nline2' },
{ userId: 'usr_2', memo: '' },
{ userId: 'usr_missing', memo: 'ignored' }
]);
await getAllUserMemos();
expect(friend1.memo).toBe('Alpha\nline2');
expect(friend1.$nickName).toBe('Alpha');
expect(friend2.memo).toBe('');
expect(friend2.$nickName).toBe('');
});
test('migrateMemos moves memo_usr entries to database and storage cleanup', async () => {
const friend = { memo: '', $nickName: '' };
mocks.friends.set('usr_1', friend);
mocks.storage.GetAll.mockResolvedValue(
JSON.stringify({
memo_usr_1: 'hello',
other_key: 'x',
memo_usr_2: ''
})
);
await migrateMemos();
expect(mocks.database.setUserMemo).toHaveBeenCalledTimes(1);
expect(mocks.database.setUserMemo).toHaveBeenCalledWith(
expect.objectContaining({
userId: 'usr_1',
memo: 'hello'
})
);
expect(mocks.storage.Remove).toHaveBeenCalledWith('memo_usr_1');
expect(mocks.storage.Remove).not.toHaveBeenCalledWith('memo_usr_2');
});
test('migrateMemos rejects for invalid JSON payload', async () => {
mocks.storage.GetAll.mockResolvedValue('{bad json');
await expect(migrateMemos()).rejects.toThrow();
expect(mocks.database.setUserMemo).not.toHaveBeenCalled();
expect(mocks.storage.Remove).not.toHaveBeenCalled();
});
});

View File

@@ -0,0 +1,102 @@
import { toast } from 'vue-sonner';
import { useModalStore, useSearchStore } from '../../stores';
import { escapeTag } from './base/string';
import { i18n } from '../../plugins/i18n';
/**
* @param {string} fileName
* @param {*} data
*/
function downloadAndSaveJson(fileName, data) {
if (!fileName || !data) {
return;
}
try {
const link = document.createElement('a');
link.setAttribute(
'href',
`data:application/json;charset=utf-8,${encodeURIComponent(
JSON.stringify(data, null, 2)
)}`
);
link.setAttribute('download', `${fileName}.json`);
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
} catch {
toast.error(escapeTag('Failed to download JSON.'));
}
}
/**
*
* @param {string} text
* @param {string} message
*/
function copyToClipboard(text, message = 'Copied successfully!') {
navigator.clipboard
.writeText(text)
.then(() => {
toast.success(message);
})
.catch((err) => {
console.error('Copy failed:', err);
toast.error(i18n.global.t('message.copy_failed'));
});
}
/**
*
* @param {string} link
*/
function openExternalLink(link) {
const searchStore = useSearchStore();
if (searchStore.directAccessParse(link)) {
return;
}
const modalStore = useModalStore();
modalStore
.confirm({
description: `${link}`,
title: 'Open External Link',
confirmText: 'Open',
cancelText: 'Copy'
})
.then(({ ok, reason }) => {
if (reason === 'cancel') {
copyToClipboard(link, 'Link copied to clipboard!');
return;
}
if (ok) {
AppApi.OpenLink(link);
return;
}
});
}
function openDiscordProfile(discordId) {
if (!discordId) {
toast.error('No Discord ID provided!');
return;
}
AppApi.OpenDiscordProfile(discordId).catch((err) => {
console.error('Failed to open Discord profile:', err);
toast.error('Failed to open Discord profile!');
});
}
// #region | App: Random unsorted app methods, data structs, API functions, and an API feedback/file analysis event
function openFolderGeneric(path) {
AppApi.OpenFolderAndSelectItem(path, true);
}
export {
downloadAndSaveJson,
copyToClipboard,
openExternalLink,
openDiscordProfile,
openFolderGeneric
};

View File

@@ -15,7 +15,7 @@ vi.mock('../../../../plugins/router', () => ({
})); }));
import { useAppearanceSettingsStore } from '../../../../stores'; import { useAppearanceSettingsStore } from '../../../../stores';
import { formatDateFilter } from '../date'; import { formatDateFilter } from '../../../../coordinators/dateCoordinator';
describe('formatDateFilter', () => { describe('formatDateFilter', () => {
beforeEach(() => { beforeEach(() => {

View File

@@ -1,14 +1,3 @@
import { storeToRefs } from 'pinia';
import { toast } from 'vue-sonner';
import {
useAuthStore,
useAvatarStore,
useInstanceStore,
useModalStore,
useSearchStore,
useWorldStore
} from '../../stores';
import { import {
extractFileId, extractFileId,
extractFileVersion, extractFileVersion,
@@ -17,148 +6,20 @@ import {
import { escapeTag, replaceBioSymbols } from './base/string'; import { escapeTag, replaceBioSymbols } from './base/string';
import { getFaviconUrl, replaceVrcPackageUrl } from './urlUtils'; import { getFaviconUrl, replaceVrcPackageUrl } from './urlUtils';
import { AppDebug } from '../../services/appConfig.js'; import { AppDebug } from '../../services/appConfig.js';
import { compareUnityVersion } from './avatar';
import { getAvailablePlatforms } from './platformUtils'; import { getAvailablePlatforms } from './platformUtils';
import { i18n } from '../../plugins/i18n';
import { queryRequest } from '../../api';
/**
* @param {string} fileName
* @param {*} data
*/
function downloadAndSaveJson(fileName, data) {
if (!fileName || !data) {
return;
}
try {
const link = document.createElement('a');
link.setAttribute(
'href',
`data:application/json;charset=utf-8,${encodeURIComponent(
JSON.stringify(data, null, 2)
)}`
);
link.setAttribute('download', `${fileName}.json`);
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
} catch {
toast.error(escapeTag('Failed to download JSON.'));
}
}
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--) {
const unityPackage = ref.unityPackages[i];
if (
unityPackage.variant &&
unityPackage.variant !== 'standard' &&
unityPackage.variant !== 'security'
) {
continue;
}
if (
unityPackage.platform === 'standalonewindows' &&
compareUnityVersion(unityPackage.unitySortNumber, sdkUnityVersion)
) {
assetUrl = unityPackage.assetUrl;
if (!unityPackage.variant || unityPackage.variant === 'standard') {
variant = 'security';
} else {
variant = unityPackage.variant;
}
break;
}
}
const id = extractFileId(assetUrl);
const version = parseInt(extractFileVersion(assetUrl), 10);
const variantVersion = parseInt(extractVariantVersion(assetUrl), 10);
await AssetBundleManager.DeleteCache(id, version, variant, variantVersion);
}
/**
*
* @param {object} ref
* @returns
*/
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--) {
const unityPackage = ref.unityPackages[i];
if (unityPackage.variant && unityPackage.variant !== 'security') {
continue;
}
if (
unityPackage.platform === 'standalonewindows' &&
compareUnityVersion(unityPackage.unitySortNumber, sdkUnityVersion)
) {
assetUrl = unityPackage.assetUrl;
if (!unityPackage.variant || unityPackage.variant === 'standard') {
variant = 'security';
} else {
variant = unityPackage.variant;
}
break;
}
}
if (!assetUrl) {
assetUrl = ref.assetUrl;
}
const id = extractFileId(assetUrl);
const version = parseInt(extractFileVersion(assetUrl), 10);
const variantVersion = parseInt(extractVariantVersion(assetUrl), 10);
if (!id || !version) {
return { Item1: -1, Item2: false, Item3: '' };
}
try {
return AssetBundleManager.CheckVRChatCache(
id,
version,
variant,
variantVersion
);
} catch (err) {
console.error('Failed reading VRChat cache size:', err);
toast.error(`Failed reading VRChat cache size: ${err}`);
return { Item1: -1, Item2: false, Item3: '' };
}
}
/**
*
* @param {string} text
* @param {string} message
*/
function copyToClipboard(text, message = 'Copied successfully!') {
navigator.clipboard
.writeText(text)
.then(() => {
toast.success(message);
})
.catch((err) => {
console.error('Copy failed:', err);
toast.error(i18n.global.t('message.copy_failed'));
});
}
/** /**
* *
* @param {string} url * @param {string} url
* @param {number} resolution * @param {number} resolution
* @param endpointDomain
* @returns {string} * @returns {string}
*/ */
function convertFileUrlToImageUrl(url, resolution = 128, endpointDomain = AppDebug.endpointDomain) { function convertFileUrlToImageUrl(
url,
resolution = 128,
endpointDomain = AppDebug.endpointDomain
) {
if (!url) { if (!url) {
return ''; return '';
} }
@@ -183,137 +44,9 @@ function convertFileUrlToImageUrl(url, resolution = 128, endpointDomain = AppDeb
/** /**
* *
* @param {string} link * @param func
* @param delay
*/ */
function openExternalLink(link) {
const searchStore = useSearchStore();
if (searchStore.directAccessParse(link)) {
return;
}
const modalStore = useModalStore();
modalStore
.confirm({
description: `${link}`,
title: 'Open External Link',
confirmText: 'Open',
cancelText: 'Copy'
})
.then(({ ok, reason }) => {
if (reason === 'cancel') {
copyToClipboard(link, 'Link copied to clipboard!');
return;
}
if (ok) {
AppApi.OpenLink(link);
return;
}
});
}
function openDiscordProfile(discordId) {
if (!discordId) {
toast.error('No Discord ID provided!');
return;
}
AppApi.OpenDiscordProfile(discordId).catch((err) => {
console.error('Failed to open Discord profile:', err);
toast.error('Failed to open Discord profile!');
});
}
/**
*
* @param {object} ref
* @returns {Promise<object>}
*/
async function getBundleDateSize(ref) {
const authStore = useAuthStore();
const sdkUnityVersion = authStore.cachedConfig.sdkUnityVersion;
const avatarStore = useAvatarStore();
const { avatarDialog } = storeToRefs(avatarStore);
const worldStore = useWorldStore();
const { worldDialog } = storeToRefs(worldStore);
const instanceStore = useInstanceStore();
const { currentInstanceWorld, currentInstanceLocation } =
storeToRefs(instanceStore);
const bundleJson = {};
for (let i = ref.unityPackages.length - 1; i > -1; i--) {
const unityPackage = ref.unityPackages[i];
if (!unityPackage) {
continue;
}
if (
unityPackage.variant &&
unityPackage.variant !== 'standard' &&
unityPackage.variant !== 'security'
) {
continue;
}
if (!compareUnityVersion(unityPackage.unitySortNumber, sdkUnityVersion)) {
continue;
}
const platform = unityPackage.platform;
if (bundleJson[platform]) {
continue;
}
const assetUrl = unityPackage.assetUrl;
const fileId = extractFileId(assetUrl);
const version = parseInt(extractFileVersion(assetUrl), 10);
let variant = '';
if (!unityPackage.variant || unityPackage.variant === 'standard') {
variant = 'security';
} else {
variant = unityPackage.variant;
}
if (!fileId || !version) {
continue;
}
const args = await queryRequest.fetch('fileAnalysis', {
fileId,
version,
variant
});
if (!args?.json?.success) {
continue;
}
const json = args.json;
if (typeof json.fileSize !== 'undefined') {
json._fileSize = `${(json.fileSize / 1048576).toFixed(2)} MB`;
}
if (typeof json.uncompressedSize !== 'undefined') {
json._uncompressedSize = `${(json.uncompressedSize / 1048576).toFixed(2)} MB`;
}
if (typeof json.avatarStats?.totalTextureUsage !== 'undefined') {
json._totalTextureUsage = `${(json.avatarStats.totalTextureUsage / 1048576).toFixed(2)} MB`;
}
bundleJson[platform] = json;
if (avatarDialog.value.id === ref.id) {
// update avatar dialog
avatarDialog.value.fileAnalysis[platform] = json;
}
// update world dialog
if (worldDialog.value.id === ref.id) {
worldDialog.value.fileAnalysis[platform] = json;
}
// update player list
if (currentInstanceLocation.value.worldId === ref.id) {
currentInstanceWorld.value.fileAnalysis[platform] = json;
}
}
return bundleJson;
}
// #region | App: Random unsorted app methods, data structs, API functions, and an API feedback/file analysis event
function openFolderGeneric(path) {
AppApi.OpenFolderAndSelectItem(path, true);
}
function debounce(func, delay) { function debounce(func, delay) {
let timer = null; let timer = null;
return function (...args) { return function (...args) {
@@ -325,12 +58,23 @@ function debounce(func, delay) {
}; };
} }
// Re-export from appActions and cacheCoordinator for backward compatibility
export { export {
getAvailablePlatforms,
downloadAndSaveJson, downloadAndSaveJson,
copyToClipboard,
openExternalLink,
openDiscordProfile,
openFolderGeneric
} from './appActions';
export {
deleteVRChatCache, deleteVRChatCache,
checkVRChatCache, checkVRChatCache,
copyToClipboard, getBundleDateSize
} from '../../coordinators/cacheCoordinator';
export {
getAvailablePlatforms,
getFaviconUrl, getFaviconUrl,
convertFileUrlToImageUrl, convertFileUrlToImageUrl,
replaceVrcPackageUrl, replaceVrcPackageUrl,
@@ -338,9 +82,5 @@ export {
extractFileVersion, extractFileVersion,
extractVariantVersion, extractVariantVersion,
replaceBioSymbols, replaceBioSymbols,
openExternalLink,
openDiscordProfile,
getBundleDateSize,
openFolderGeneric,
debounce debounce
}; };

View File

@@ -1,12 +1,9 @@
import { toast } from 'vue-sonner';
import { $throw } from '../../services/request';
import { AppDebug } from '../../services/appConfig.js';
import { extractFileId } from './index.js';
import { imageRequest } from '../../api';
const UPLOAD_TIMEOUT_MS = 30_000; const UPLOAD_TIMEOUT_MS = 30_000;
/**
*
* @param promise
*/
export function withUploadTimeout(promise) { export function withUploadTimeout(promise) {
return Promise.race([ return Promise.race([
promise, promise,
@@ -19,79 +16,6 @@ export function withUploadTimeout(promise) {
]); ]);
} }
function resolveMessage(message) {
if (typeof message === 'function') {
return message();
}
return message;
}
function getInputElement(selector) {
if (!selector) {
return null;
}
if (typeof selector === 'function') {
return selector();
}
if (typeof selector === 'string') {
return document.querySelector(selector);
}
return selector;
}
export function handleImageUploadInput(event, options = {}) {
const {
inputSelector,
// 20MB
maxSize = 20000000,
acceptPattern = /image.*/,
tooLargeMessage,
invalidTypeMessage,
onClear
} = options;
const clearInput = () => {
onClear?.();
const input = getInputElement(inputSelector);
if (input) {
input.value = '';
}
};
const files = event?.target?.files || event?.dataTransfer?.files;
if (!files || files.length === 0) {
clearInput();
return { file: null, clearInput };
}
const file = files[0];
if (file.size >= maxSize) {
if (tooLargeMessage) {
toast.error(resolveMessage(tooLargeMessage));
}
clearInput();
return { file: null, clearInput };
}
let acceptRegex = null;
if (acceptPattern) {
acceptRegex =
acceptPattern instanceof RegExp
? acceptPattern
: new RegExp(acceptPattern);
}
if (acceptRegex && !acceptRegex.test(file.type)) {
if (invalidTypeMessage) {
toast.error(resolveMessage(invalidTypeMessage));
}
clearInput();
return { file: null, clearInput };
}
return { file, clearInput };
}
/** /**
* File -> base64 * File -> base64
* @param {Blob|File} blob * @param {Blob|File} blob
@@ -113,122 +37,3 @@ export function readFileAsBase64(blob) {
r.readAsArrayBuffer(blob); r.readAsArrayBuffer(blob);
}); });
} }
/**
* @param {string} base64Data - base64 encoded image
* @returns {Promise<string>} resized base64 encoded image
*/
export async function resizeImageToFitLimits(base64Data) {
// frontend limit check = 20MB
return AppApi.ResizeImageToFitLimits(base64Data);
}
/**
* Upload image through AWS
* @param {'avatar'|'world'} type
* @param {object} opts
* @param {string} opts.entityId - avatar or world id
* @param {string} opts.imageUrl - current imageUrl on the entity
* @param {string} opts.base64File - base64 encoded image data
* @param {Blob} opts.blob - the original blob (used for file size)
*/
export async function uploadImageLegacy(
type,
{ entityId, imageUrl, base64File, blob }
) {
const apiMap = {
avatar: {
uploadImage: imageRequest.uploadAvatarImage,
fileStart: imageRequest.uploadAvatarImageFileStart,
fileFinish: imageRequest.uploadAvatarImageFileFinish,
sigStart: imageRequest.uploadAvatarImageSigStart,
sigFinish: imageRequest.uploadAvatarImageSigFinish,
setImage: imageRequest.setAvatarImage
},
world: {
uploadImage: imageRequest.uploadWorldImage,
fileStart: imageRequest.uploadWorldImageFileStart,
fileFinish: imageRequest.uploadWorldImageFileFinish,
sigStart: imageRequest.uploadWorldImageSigStart,
sigFinish: imageRequest.uploadWorldImageSigFinish,
setImage: imageRequest.setWorldImage
}
};
const api = apiMap[type];
const fileMd5 = await AppApi.MD5File(base64File);
const fileSizeInBytes = parseInt(blob.size, 10);
const base64SignatureFile = await AppApi.SignFile(base64File);
const signatureMd5 = await AppApi.MD5File(base64SignatureFile);
const signatureSizeInBytes = parseInt(
await AppApi.FileLength(base64SignatureFile),
10
);
const fileId = extractFileId(imageUrl);
// imageInit
const uploadRes = await api.uploadImage(
{ fileMd5, fileSizeInBytes, signatureMd5, signatureSizeInBytes },
fileId
);
const uploadedFileId = uploadRes.json.id;
const fileVersion =
uploadRes.json.versions[uploadRes.json.versions.length - 1].version;
// imageFileStart
const fileStartRes = await api.fileStart({
fileId: uploadedFileId,
fileVersion
});
// uploadImageFileAWS
const fileAwsRes = await webApiService.execute({
url: fileStartRes.json.url,
uploadFilePUT: true,
fileData: base64File,
fileMIME: 'image/png',
fileMD5: fileMd5
});
if (fileAwsRes.status !== 200) {
$throw(
fileAwsRes.status,
`${type} image upload failed`,
fileStartRes.json.url
);
}
// imageFileFinish
await api.fileFinish({ fileId: uploadedFileId, fileVersion });
// imageSigStart
const sigStartRes = await api.sigStart({
fileId: uploadedFileId,
fileVersion
});
// uploadImageSigAWS
const sigAwsRes = await webApiService.execute({
url: sigStartRes.json.url,
uploadFilePUT: true,
fileData: base64SignatureFile,
fileMIME: 'application/x-rsync-signature',
fileMD5: signatureMd5
});
if (sigAwsRes.status !== 200) {
$throw(
sigAwsRes.status,
`${type} image upload failed`,
sigStartRes.json.url
);
}
// imageSigFinish
await api.sigFinish({ fileId: uploadedFileId, fileVersion });
// imageSet
const newImageUrl = `${AppDebug.endpointDomain}/file/${uploadedFileId}/${fileVersion}/file`;
const setRes = await api.setImage({ id: entityId, imageUrl: newImageUrl });
if (setRes.json.imageUrl !== newImageUrl) {
$throw(0, `${type} image change failed`, newImageUrl);
}
}

View File

@@ -1,7 +1,7 @@
export * from './base/array'; export * from './base/array';
export * from './base/devtool'; export * from './base/devtool';
export * from './base/format'; export * from './base/format';
export * from './base/date'; export { formatDateFilter } from '../../coordinators/dateCoordinator';
export * from './base/string'; export * from './base/string';
export * from './avatar'; export * from './avatar';
export * from './chart'; export * from './chart';
@@ -20,7 +20,6 @@ export * from './gallery';
export * from './location'; export * from './location';
export * from './invite'; export * from './invite';
export * from './world'; export * from './world';
export * from './memos';
export * from './throttle'; export * from './throttle';
export * from './retry'; export * from './retry';
export * from './gameLog'; export * from './gameLog';

View File

@@ -1,20 +1,3 @@
import { instanceRequest } from '../../api';
import { parseLocation } from './locationParser';
/**
*
* @param {object} instance
*/
function refreshInstancePlayerCount(instance) {
const L = parseLocation(instance);
if (L.isRealInstance) {
instanceRequest.getInstance({
worldId: L.worldId,
instanceId: L.instanceId
});
}
}
/** /**
* *
* @param {string} instanceId * @param {string} instanceId
@@ -131,9 +114,4 @@ function buildLegacyInstanceTag({
return tags.join(''); return tags.join('');
} }
export { export { isRealInstance, getLaunchURL, buildLegacyInstanceTag };
refreshInstancePlayerCount,
isRealInstance,
getLaunchURL,
buildLegacyInstanceTag
};

View File

@@ -8,9 +8,9 @@ import {
createRateLimiter, createRateLimiter,
executeWithBackoff, executeWithBackoff,
getFriendsSortFunction, getFriendsSortFunction,
getUserMemo,
isRealInstance isRealInstance
} from '../shared/utils'; } from '../shared/utils';
import { getUserMemo } from '../coordinators/memoCoordinator';
import { friendRequest, userRequest } from '../api'; import { friendRequest, userRequest } from '../api';
import { import {
runInitFriendsListFlow, runInitFriendsListFlow,
@@ -261,8 +261,6 @@ export const useFriendStore = defineStore('Friend', () => {
init(); init();
/** /**
* *
*/ */
@@ -699,13 +697,11 @@ export const useFriendStore = defineStore('Friend', () => {
* @param {string} id * @param {string} id
*/ */
/** /**
* *
* @param {object} ref * @param {object} ref
*/ */
/** /**
* *
* @param {object} currentUser * @param {object} currentUser
@@ -1129,7 +1125,6 @@ export const useFriendStore = defineStore('Friend', () => {
* @param id * @param id
*/ */
/** /**
* Clears all entries in friendLog. * Clears all entries in friendLog.
* Uses .clear() instead of reassignment to keep the same Map reference, * Uses .clear() instead of reassignment to keep the same Map reference,

View File

@@ -16,7 +16,7 @@ import {
vrcPlusImageRequest vrcPlusImageRequest
} from '../api'; } from '../api';
import { AppDebug } from '../services/appConfig'; import { AppDebug } from '../services/appConfig';
import { handleImageUploadInput } from '../shared/utils/imageUpload'; import { handleImageUploadInput } from '../coordinators/imageUploadCoordinator';
import { router } from '../plugins/router'; import { router } from '../plugins/router';
import { useAdvancedSettingsStore } from './settings/advanced'; import { useAdvancedSettingsStore } from './settings/advanced';
import { useModalStore } from './modal'; import { useModalStore } from './modal';

View File

@@ -13,12 +13,12 @@ import {
escapeTag, escapeTag,
executeWithBackoff, executeWithBackoff,
findUserByDisplayName, findUserByDisplayName,
getUserMemo,
parseLocation, parseLocation,
parseNotificationDetails, parseNotificationDetails,
removeFromArray, removeFromArray,
sanitizeNotificationJson sanitizeNotificationJson
} from '../../shared/utils'; } from '../../shared/utils';
import { getUserMemo } from '../../coordinators/memoCoordinator';
import { import {
friendRequest, friendRequest,
instanceRequest, instanceRequest,
@@ -347,11 +347,13 @@ export const useNotificationStore = defineStore('Notification', () => {
} }
} }
} }
if (!checkCanInvite(currentLocation, { if (
currentUserId: userStore.currentUser.id, !checkCanInvite(currentLocation, {
lastLocationStr: locationStore.lastLocation.location, currentUserId: userStore.currentUser.id,
cachedInstances: instanceStore.cachedInstances lastLocationStr: locationStore.lastLocation.location,
})) { cachedInstances: instanceStore.cachedInstances
})
) {
return; return;
} }

View File

@@ -8,16 +8,12 @@ import {
compareByLocationAt, compareByLocationAt,
compareByName, compareByName,
compareByUpdatedAt, compareByUpdatedAt,
getAllUserMemos,
getUserMemo,
isRealInstance, isRealInstance,
parseLocation, parseLocation,
replaceBioSymbols replaceBioSymbols
} from '../shared/utils'; } from '../shared/utils';
import { import { getAllUserMemos, getUserMemo } from '../coordinators/memoCoordinator';
instanceRequest, import { instanceRequest, userRequest } from '../api';
userRequest
} from '../api';
import { AppDebug } from '../services/appConfig'; import { AppDebug } from '../services/appConfig';
import { database } from '../services/database'; import { database } from '../services/database';
import { runUpdateCurrentUserLocationFlow } from '../coordinators/locationCoordinator'; import { runUpdateCurrentUserLocationFlow } from '../coordinators/locationCoordinator';

View File

@@ -17,6 +17,7 @@ const mocks = vi.hoisted(() => ({
favoriteFriendGroups: null, favoriteFriendGroups: null,
groupedByGroupKeyFavoriteFriends: null, groupedByGroupKeyFavoriteFriends: null,
localFriendFavorites: null, localFriendFavorites: null,
lastLocation: null,
configGetString: vi.fn(), configGetString: vi.fn(),
configGetBool: vi.fn(), configGetBool: vi.fn(),
configSetString: vi.fn(), configSetString: vi.fn(),
@@ -37,6 +38,10 @@ mocks.sidebarSortMethods = mocks.makeRef('status');
mocks.favoriteFriendGroups = mocks.makeRef([]); mocks.favoriteFriendGroups = mocks.makeRef([]);
mocks.groupedByGroupKeyFavoriteFriends = mocks.makeRef({}); mocks.groupedByGroupKeyFavoriteFriends = mocks.makeRef({});
mocks.localFriendFavorites = mocks.makeRef({}); mocks.localFriendFavorites = mocks.makeRef({});
mocks.lastLocation = mocks.makeRef({
location: 'wrld_home:123',
friendList: new Map()
});
vi.mock('pinia', () => ({ vi.mock('pinia', () => ({
storeToRefs: (store) => store storeToRefs: (store) => store
@@ -68,6 +73,9 @@ vi.mock('../../../stores', () => ({
favoriteFriendGroups: mocks.favoriteFriendGroups, favoriteFriendGroups: mocks.favoriteFriendGroups,
groupedByGroupKeyFavoriteFriends: mocks.groupedByGroupKeyFavoriteFriends, groupedByGroupKeyFavoriteFriends: mocks.groupedByGroupKeyFavoriteFriends,
localFriendFavorites: mocks.localFriendFavorites localFriendFavorites: mocks.localFriendFavorites
}),
useLocationStore: () => ({
lastLocation: mocks.lastLocation
}) })
})); }));
@@ -225,6 +233,10 @@ describe('FriendsLocations.vue', () => {
mocks.favoriteFriendGroups.value = []; mocks.favoriteFriendGroups.value = [];
mocks.groupedByGroupKeyFavoriteFriends.value = {}; mocks.groupedByGroupKeyFavoriteFriends.value = {};
mocks.localFriendFavorites.value = {}; mocks.localFriendFavorites.value = {};
mocks.lastLocation.value = {
location: 'wrld_home:123',
friendList: new Map()
};
mocks.configGetString.mockReset(); mocks.configGetString.mockReset();
mocks.configGetBool.mockReset(); mocks.configGetBool.mockReset();

View File

@@ -314,20 +314,24 @@
import { useI18n } from 'vue-i18n'; import { useI18n } from 'vue-i18n';
import { useVirtualizer } from '@tanstack/vue-virtual'; import { useVirtualizer } from '@tanstack/vue-virtual';
import {
handleImageUploadInput,
readFileAsBase64,
resizeImageToFitLimits,
uploadImageLegacy,
withUploadTimeout
} from '../../shared/utils/imageUpload';
import { useAppearanceSettingsStore, useAvatarStore, useModalStore, useUserStore } from '../../stores'; import { useAppearanceSettingsStore, useAvatarStore, useModalStore, useUserStore } from '../../stores';
import { ContextMenuContent, ContextMenuItem, ContextMenuSeparator } from '../../components/ui/context-menu'; import { ContextMenuContent, ContextMenuItem, ContextMenuSeparator } from '../../components/ui/context-menu';
import { DropdownMenu, DropdownMenuContent, DropdownMenuTrigger } from '../../components/ui/dropdown-menu'; import { DropdownMenu, DropdownMenuContent, DropdownMenuTrigger } from '../../components/ui/dropdown-menu';
import { Field, FieldContent, FieldLabel } from '../../components/ui/field'; import { Field, FieldContent, FieldLabel } from '../../components/ui/field';
import { Popover, PopoverContent, PopoverTrigger } from '../../components/ui/popover'; import { Popover, PopoverContent, PopoverTrigger } from '../../components/ui/popover';
import {
applyAvatar,
selectAvatarWithoutConfirmation,
showAvatarDialog
} from '../../coordinators/avatarCoordinator';
import {
handleImageUploadInput,
resizeImageToFitLimits,
uploadImageLegacy
} from '../../coordinators/imageUploadCoordinator';
import { DataTableEmpty, DataTableLayout } from '../../components/ui/data-table'; import { DataTableEmpty, DataTableLayout } from '../../components/ui/data-table';
import { ToggleGroup, ToggleGroupItem } from '../../components/ui/toggle-group'; import { ToggleGroup, ToggleGroupItem } from '../../components/ui/toggle-group';
import { readFileAsBase64, withUploadTimeout } from '../../shared/utils/imageUpload';
import { Badge } from '../../components/ui/badge'; import { Badge } from '../../components/ui/badge';
import { Button } from '../../components/ui/button'; import { Button } from '../../components/ui/button';
import { Input } from '../../components/ui/input'; import { Input } from '../../components/ui/input';
@@ -347,7 +351,6 @@
import ManageTagsDialog from './ManageTagsDialog.vue'; import ManageTagsDialog from './ManageTagsDialog.vue';
import MyAvatarCard from './components/MyAvatarCard.vue'; import MyAvatarCard from './components/MyAvatarCard.vue';
import configRepository from '../../services/config.js'; import configRepository from '../../services/config.js';
import { showAvatarDialog, selectAvatarWithoutConfirmation, applyAvatar } from '../../coordinators/avatarCoordinator';
const { t } = useI18n(); const { t } = useI18n();
const appearanceSettingsStore = useAppearanceSettingsStore(); const appearanceSettingsStore = useAppearanceSettingsStore();

View File

@@ -55,6 +55,9 @@ const mocks = vi.hoisted(() => ({
gameStore: { gameStore: {
isGameRunning: { value: true } isGameRunning: { value: true }
}, },
instanceStore: {
cachedInstances: new Map()
},
configRepository: { configRepository: {
getBool: vi.fn(), getBool: vi.fn(),
setBool: vi.fn() setBool: vi.fn()
@@ -111,6 +114,7 @@ vi.mock('../../../../stores', () => ({
useGameStore: () => mocks.gameStore, useGameStore: () => mocks.gameStore,
useLaunchStore: () => mocks.launchStore, useLaunchStore: () => mocks.launchStore,
useLocationStore: () => mocks.locationStore, useLocationStore: () => mocks.locationStore,
useInstanceStore: () => mocks.instanceStore,
useUserStore: () => mocks.userStore useUserStore: () => mocks.userStore
})); }));
@@ -233,6 +237,7 @@ describe('FriendsSidebar.vue', () => {
mocks.friendStore.activeFriends.value = []; mocks.friendStore.activeFriends.value = [];
mocks.friendStore.offlineFriends.value = []; mocks.friendStore.offlineFriends.value = [];
mocks.friendStore.friendsInSameInstance.value = []; mocks.friendStore.friendsInSameInstance.value = [];
mocks.instanceStore.cachedInstances = new Map();
mocks.appearanceStore.isSidebarGroupByInstance.value = false; mocks.appearanceStore.isSidebarGroupByInstance.value = false;
mocks.appearanceStore.isHideFriendsInSameInstance.value = false; mocks.appearanceStore.isHideFriendsInSameInstance.value = false;

View File

@@ -18,15 +18,25 @@ const mocks = vi.hoisted(() => ({
}, },
userStore: { userStore: {
cachedUsers: new Map(), cachedUsers: new Map(),
showSendBoopDialog: vi.fn() showSendBoopDialog: vi.fn(),
currentUser: { id: 'usr_me' }
},
friendStore: {
friends: new Map()
}, },
groupStore: {}, groupStore: {},
locationStore: { locationStore: {
lastLocation: { value: { location: 'wrld_home:123' } } lastLocation: {
location: 'wrld_home:123',
value: { location: 'wrld_home:123' }
}
}, },
gameStore: { gameStore: {
isGameRunning: { value: true } isGameRunning: { value: true }
}, },
instanceStore: {
cachedInstances: new Map()
},
showUserDialog: vi.fn(), showUserDialog: vi.fn(),
showGroupDialog: vi.fn() showGroupDialog: vi.fn()
})); }));
@@ -42,9 +52,11 @@ vi.mock('pinia', async (importOriginal) => {
vi.mock('../../../../stores', () => ({ vi.mock('../../../../stores', () => ({
useNotificationStore: () => mocks.notificationStore, useNotificationStore: () => mocks.notificationStore,
useUserStore: () => mocks.userStore, useUserStore: () => mocks.userStore,
useFriendStore: () => mocks.friendStore,
useGroupStore: () => mocks.groupStore, useGroupStore: () => mocks.groupStore,
useLocationStore: () => mocks.locationStore, useLocationStore: () => mocks.locationStore,
useGameStore: () => mocks.gameStore useGameStore: () => mocks.gameStore,
useInstanceStore: () => mocks.instanceStore
})); }));
vi.mock('../../../../coordinators/userCoordinator', () => ({ vi.mock('../../../../coordinators/userCoordinator', () => ({
@@ -60,6 +72,13 @@ vi.mock('../../../../shared/utils', () => ({
userImage: vi.fn(() => 'https://example.com/avatar.png') userImage: vi.fn(() => 'https://example.com/avatar.png')
})); }));
vi.mock('../../../../composables/useUserDisplay', () => ({
useUserDisplay: () => ({
userImage: vi.fn(() => 'https://example.com/avatar.png'),
userStatusClass: vi.fn(() => '')
})
}));
vi.mock('vue-i18n', () => ({ vi.mock('vue-i18n', () => ({
useI18n: () => ({ useI18n: () => ({
t: (key) => key, t: (key) => key,
@@ -170,6 +189,8 @@ describe('NotificationItem.vue', () => {
mocks.userStore.showSendBoopDialog.mockReset(); mocks.userStore.showSendBoopDialog.mockReset();
mocks.showGroupDialog.mockReset(); mocks.showGroupDialog.mockReset();
mocks.userStore.cachedUsers = new Map(); mocks.userStore.cachedUsers = new Map();
mocks.friendStore.friends = new Map();
mocks.instanceStore.cachedInstances = new Map();
}); });
test('renders sender and opens user dialog on sender click', async () => { test('renders sender and opens user dialog on sender click', async () => {

View File

@@ -586,7 +586,8 @@
} from '../../shared/utils'; } from '../../shared/utils';
import { inventoryRequest, miscRequest, userRequest, vrcPlusIconRequest, vrcPlusImageRequest } from '../../api'; import { inventoryRequest, miscRequest, userRequest, vrcPlusIconRequest, vrcPlusImageRequest } from '../../api';
import { useAdvancedSettingsStore, useAuthStore, useGalleryStore, useModalStore, useUserStore } from '../../stores'; import { useAdvancedSettingsStore, useAuthStore, useGalleryStore, useModalStore, useUserStore } from '../../stores';
import { handleImageUploadInput, readFileAsBase64, withUploadTimeout } from '../../shared/utils/imageUpload'; import { readFileAsBase64, withUploadTimeout } from '../../shared/utils/imageUpload';
import { handleImageUploadInput } from '../../coordinators/imageUploadCoordinator';
import { emojiAnimationStyleList, emojiAnimationStyleUrl } from '../../shared/constants'; import { emojiAnimationStyleList, emojiAnimationStyleUrl } from '../../shared/constants';
import { AppDebug } from '../../services/appConfig'; import { AppDebug } from '../../services/appConfig';