From 95c4a1d3e6eea992d1e0c26ca47e4e5f361c6441 Mon Sep 17 00:00:00 2001 From: pa Date: Tue, 10 Mar 2026 15:25:23 +0900 Subject: [PATCH] refactor store --- src/api/auth.js | 4 +- src/api/avatar.js | 9 +- src/api/friend.js | 5 +- src/api/group.js | 6 +- src/api/image.js | 3 +- src/api/user.js | 17 +- src/api/world.js | 17 +- src/components/AvatarInfo.vue | 5 +- src/components/DisplayName.vue | 11 +- src/components/InstanceActionBar.vue | 5 +- src/components/Location.vue | 3 +- src/components/LocationWorld.vue | 7 +- .../dialogs/AvatarDialog/AvatarDialog.vue | 6 +- .../AvatarDialog/SetAvatarStylesDialog.vue | 2 +- .../AvatarDialog/SetAvatarTagsDialog.vue | 2 +- .../dialogs/GroupDialog/GroupDialog.vue | 15 +- .../GroupDialog/GroupDialogInfoTab.vue | 3 +- .../GroupDialog/GroupDialogMembersTab.vue | 5 +- .../GroupMemberModerationDialog.vue | 5 +- .../PreviousInstancesInfoDialog.vue | 3 +- .../dialogs/UserDialog/UserDialog.vue | 8 +- .../UserDialog/UserDialogAvatarsTab.vue | 2 +- .../UserDialogFavoriteWorldsTab.vue | 3 +- .../UserDialog/UserDialogGroupsTab.vue | 20 +- .../dialogs/UserDialog/UserDialogInfoTab.vue | 6 +- .../UserDialog/UserDialogMutualFriendsTab.vue | 3 +- .../UserDialog/UserDialogWorldsTab.vue | 3 +- .../WorldDialog/SetWorldTagsDialog.vue | 3 +- .../dialogs/WorldDialog/WorldDialog.vue | 6 +- .../WorldDialog/WorldDialogInstancesTab.vue | 3 +- src/coordinators/authAutoLoginCoordinator.js | 4 +- src/coordinators/authCoordinator.js | 7 +- src/coordinators/avatarCoordinator.js | 649 ++++++++++ src/coordinators/favoriteCoordinator.js | 10 +- src/coordinators/friendSyncCoordinator.js | 3 +- src/coordinators/gameCoordinator.js | 3 +- src/coordinators/groupCoordinator.js | 957 +++++++++++++++ src/coordinators/inviteCoordinator.js | 4 +- src/coordinators/moderationCoordinator.js | 6 +- src/coordinators/userCoordinator.js | 1070 +++++++++++++++++ src/coordinators/userEventCoordinator.js | 5 +- src/coordinators/userSessionCoordinator.js | 8 +- src/coordinators/worldCoordinator.js | 246 ++++ src/service/request.js | 3 +- src/service/websocket.js | 31 +- src/stores/auth.js | 10 +- src/stores/avatar.js | 634 +--------- src/stores/friend.js | 3 +- src/stores/globalSearch.js | 12 +- src/stores/group.js | 932 +------------- src/stores/notification/index.js | 8 +- src/stores/photon.js | 10 +- src/stores/search.js | 38 +- src/stores/ui.js | 12 +- src/stores/updateLoop.js | 8 +- src/stores/user.js | 1069 +--------------- src/stores/vrcx.js | 21 +- src/stores/world.js | 257 +--- .../components/InstanceActivityDetail.vue | 3 +- src/views/Charts/components/MutualFriends.vue | 3 +- src/views/Favorites/FavoritesAvatar.vue | 2 +- src/views/Favorites/FavoritesWorld.vue | 3 +- .../components/FavoritesAvatarItem.vue | 2 +- .../FavoritesAvatarLocalHistoryItem.vue | 2 +- .../Favorites/dialogs/AvatarImportDialog.vue | 5 +- .../Favorites/dialogs/FriendImportDialog.vue | 3 +- .../Favorites/dialogs/WorldImportDialog.vue | 6 +- src/views/Feed/columns.jsx | 5 +- src/views/FriendList/FriendList.vue | 3 +- src/views/FriendLog/columns.jsx | 5 +- .../components/FriendsLocationsCard.vue | 3 +- src/views/GameLog/columns.jsx | 11 +- src/views/Moderation/columns.jsx | 3 +- src/views/MyAvatars/MyAvatars.vue | 3 +- src/views/Notifications/columns.jsx | 10 +- src/views/PlayerList/PlayerList.vue | 3 +- .../components/PhotonEventTable.vue | 6 +- src/views/Search/Search.vue | 3 +- src/views/Sidebar/components/FriendItem.vue | 3 +- .../Sidebar/components/FriendsSidebar.vue | 3 +- .../Sidebar/components/NotificationItem.vue | 6 +- src/views/Tools/dialogs/NoteExportDialog.vue | 3 +- 82 files changed, 3243 insertions(+), 3066 deletions(-) create mode 100644 src/coordinators/avatarCoordinator.js create mode 100644 src/coordinators/groupCoordinator.js create mode 100644 src/coordinators/userCoordinator.js create mode 100644 src/coordinators/worldCoordinator.js diff --git a/src/api/auth.js b/src/api/auth.js index af3460c3..cca0dc91 100644 --- a/src/api/auth.js +++ b/src/api/auth.js @@ -1,5 +1,5 @@ import { request } from '../service/request'; -import { useUserStore } from '../stores'; +import { handleConfig } from '../coordinators/userCoordinator'; const loginReq = { /** @@ -63,7 +63,7 @@ const loginReq = { const args = { json }; - useUserStore().handleConfig(args); + handleConfig(args); return args; }); } diff --git a/src/api/avatar.js b/src/api/avatar.js index cf4a3146..e60b2926 100644 --- a/src/api/avatar.js +++ b/src/api/avatar.js @@ -1,6 +1,7 @@ import { patchAndRefetchActiveQuery, queryKeys } from '../queries'; import { request } from '../service/request'; import { useUserStore } from '../stores'; +import { applyCurrentUser } from '../coordinators/userCoordinator'; const avatarReq = { /** @@ -65,7 +66,7 @@ const avatarReq = { * @returns {Promise<{json: any, params}>} */ selectAvatar(params) { - const userStore = useUserStore(); + return request(`avatars/${params.avatarId}/select`, { method: 'PUT', params @@ -74,7 +75,7 @@ const avatarReq = { json, params }; - const ref = userStore.applyCurrentUser(json); + const ref = applyCurrentUser(json); patchAndRefetchActiveQuery({ queryKey: queryKeys.user(ref.id), nextData: { @@ -97,7 +98,7 @@ const avatarReq = { * @returns { Promise<{json: any, params}> } */ selectFallbackAvatar(params) { - const userStore = useUserStore(); + return request(`avatars/${params.avatarId}/selectfallback`, { method: 'PUT', params @@ -106,7 +107,7 @@ const avatarReq = { json, params }; - const ref = userStore.applyCurrentUser(json); + const ref = applyCurrentUser(json); patchAndRefetchActiveQuery({ queryKey: queryKeys.user(ref.id), nextData: { diff --git a/src/api/friend.js b/src/api/friend.js index ff317b2f..7f9750be 100644 --- a/src/api/friend.js +++ b/src/api/friend.js @@ -1,6 +1,7 @@ import { queryClient } from '../queries'; import { request } from '../service/request'; import { useUserStore } from '../stores/user'; +import { applyUser } from '../coordinators/userCoordinator'; /** * @@ -22,7 +23,7 @@ const friendReq = { * @type {import('../types/api/friend').GetFriends} */ getFriends(params) { - const userStore = useUserStore(); + return request('auth/user/friends', { method: 'GET', params @@ -36,7 +37,7 @@ const friendReq = { console.error('/friends gave us garbage', user); continue; } - userStore.applyUser(user); + applyUser(user); } return args; }); diff --git a/src/api/group.js b/src/api/group.js index fdbdead5..8e03725c 100644 --- a/src/api/group.js +++ b/src/api/group.js @@ -1,4 +1,5 @@ -import { useGroupStore, useUserStore } from '../stores'; +import { useUserStore } from '../stores'; +import { applyGroup } from '../coordinators/groupCoordinator'; import { queryClient } from '../queries'; import { request } from '../service/request'; @@ -89,12 +90,11 @@ const groupReq = { includeRoles: params.includeRoles || false } }).then((json) => { - const groupStore = useGroupStore(); const args = { json, params }; - args.ref = groupStore.applyGroup(json); + args.ref = applyGroup(json); return args; }); }, diff --git a/src/api/image.js b/src/api/image.js index 24321f4b..4d435f34 100644 --- a/src/api/image.js +++ b/src/api/image.js @@ -1,4 +1,5 @@ import { useAvatarStore, useWorldStore } from '../stores'; +import { applyWorld } from '../coordinators/worldCoordinator'; import { request } from '../service/request'; const imageReq = { @@ -267,7 +268,7 @@ const imageReq = { json, params }; - args.ref = worldStore.applyWorld(json); + args.ref = applyWorld(json); return args; }); }, diff --git a/src/api/user.js b/src/api/user.js index b59ac367..44bb0a50 100644 --- a/src/api/user.js +++ b/src/api/user.js @@ -1,6 +1,7 @@ import { patchAndRefetchActiveQuery, queryKeys } from '../queries'; import { request } from '../service/request'; import { useUserStore } from '../stores'; +import { applyUser, applyCurrentUser } from '../coordinators/userCoordinator'; /** * @returns {string} @@ -16,7 +17,7 @@ const userReq = { * @type {import('../types/api/user').GetUser} */ getUser(params) { - const userStore = useUserStore(); + return request(`users/${params.userId}`, { method: 'GET' }).then((json) => { @@ -29,7 +30,7 @@ const userReq = { const args = { json, params, - ref: userStore.applyUser(json) + ref: applyUser(json) }; return args; }); @@ -56,7 +57,7 @@ const userReq = { * @returns {Promise<{json: any, params: {tags: string[]}}>} */ addUserTags(params) { - const userStore = useUserStore(); + return request(`users/${getCurrentUserId()}/addTags`, { method: 'POST', params @@ -65,7 +66,7 @@ const userReq = { json, params }; - userStore.applyCurrentUser(json); + applyCurrentUser(json); return args; }); }, @@ -75,7 +76,7 @@ const userReq = { * @returns {Promise<{json: any, params: {tags: string[]}}>} */ removeUserTags(params) { - const userStore = useUserStore(); + return request(`users/${getCurrentUserId()}/removeTags`, { method: 'POST', params @@ -84,7 +85,7 @@ const userReq = { json, params }; - userStore.applyCurrentUser(json); + applyCurrentUser(json); return args; }); }, @@ -113,7 +114,7 @@ const userReq = { * @type {import('../types/api/user').GetCurrentUser} */ saveCurrentUser(params) { - const userStore = useUserStore(); + return request(`users/${getCurrentUserId()}`, { method: 'PUT', params @@ -121,7 +122,7 @@ const userReq = { const args = { json, params, - ref: userStore.applyCurrentUser(json) + ref: applyCurrentUser(json) }; patchAndRefetchActiveQuery({ queryKey: queryKeys.user(args.ref.id), diff --git a/src/api/world.js b/src/api/world.js index 89189595..4d159ee5 100644 --- a/src/api/world.js +++ b/src/api/world.js @@ -1,13 +1,12 @@ import { patchAndRefetchActiveQuery, queryKeys } from '../queries'; import { request } from '../service/request'; -import { useWorldStore } from '../stores'; +import { applyWorld } from '../coordinators/worldCoordinator'; const worldReq = { /** * @type {import('../types/api/world').GetWorld} */ getWorld(params) { - const worldStore = useWorldStore(); return request(`worlds/${params.worldId}`, { method: 'GET' }).then((json) => { @@ -15,7 +14,7 @@ const worldReq = { json, params }; - args.ref = worldStore.applyWorld(json); + args.ref = applyWorld(json); return args; }); }, @@ -24,7 +23,6 @@ const worldReq = { * @type {import('../types/api/world').GetWorlds} */ getWorlds(params, option) { - const worldStore = useWorldStore(); let endpoint = 'worlds'; if (typeof option !== 'undefined') { endpoint = `worlds/${option}`; @@ -39,7 +37,7 @@ const worldReq = { option }; for (const json of args.json) { - worldStore.applyWorld(json); + applyWorld(json); } return args; }); @@ -64,7 +62,6 @@ const worldReq = { * @type {import('../types/api/world').SaveWorld} */ saveWorld(params) { - const worldStore = useWorldStore(); return request(`worlds/${params.id}`, { method: 'PUT', params @@ -73,7 +70,7 @@ const worldReq = { json, params }; - args.ref = worldStore.applyWorld(json); + args.ref = applyWorld(json); patchAndRefetchActiveQuery({ queryKey: queryKeys.world(args.ref.id), nextData: args @@ -92,7 +89,6 @@ const worldReq = { * @returns {Promise<{json: any, params}>} */ publishWorld(params) { - const worldStore = useWorldStore(); return request(`worlds/${params.worldId}/publish`, { method: 'PUT', params @@ -101,7 +97,7 @@ const worldReq = { json, params }; - args.ref = worldStore.applyWorld(json); + args.ref = applyWorld(json); patchAndRefetchActiveQuery({ queryKey: queryKeys.world(args.ref.id), nextData: args @@ -120,7 +116,6 @@ const worldReq = { * @returns {Promise<{json: any, params}>} */ unpublishWorld(params) { - const worldStore = useWorldStore(); return request(`worlds/${params.worldId}/publish`, { method: 'DELETE', params @@ -129,7 +124,7 @@ const worldReq = { json, params }; - args.ref = worldStore.applyWorld(json); + args.ref = applyWorld(json); patchAndRefetchActiveQuery({ queryKey: queryKeys.world(args.ref.id), nextData: args diff --git a/src/components/AvatarInfo.vue b/src/components/AvatarInfo.vue index fe78bc05..02596387 100644 --- a/src/components/AvatarInfo.vue +++ b/src/components/AvatarInfo.vue @@ -22,6 +22,7 @@ import { TooltipWrapper } from './ui/tooltip'; import { useAvatarStore } from '../stores'; + import { getAvatarName, showAvatarAuthorDialog } from '../coordinators/avatarCoordinator'; const { t } = useI18n(); const avatarStore = useAvatarStore(); @@ -54,7 +55,7 @@ ownerId = props.hintownerid; } else { try { - const info = await avatarStore.getAvatarName(props.imageurl); + const info = await getAvatarName(props.imageurl); avatarName.value = info.avatarName; ownerId = info.ownerId; } catch { @@ -77,7 +78,7 @@ const confirm = () => { if (!props.imageurl) return; - avatarStore.showAvatarAuthorDialog(props.userid, ownerId, props.imageurl); + showAvatarAuthorDialog(props.userid, ownerId, props.imageurl); }; watch([() => props.imageurl, () => props.userid, () => props.avatartags], parse, { immediate: true }); diff --git a/src/components/DisplayName.vue b/src/components/DisplayName.vue index 2eea3781..31d9a9bb 100644 --- a/src/components/DisplayName.vue +++ b/src/components/DisplayName.vue @@ -1,14 +1,12 @@ + diff --git a/src/components/InstanceActionBar.vue b/src/components/InstanceActionBar.vue index e6fb259d..4fdb3f00 100644 --- a/src/components/InstanceActionBar.vue +++ b/src/components/InstanceActionBar.vue @@ -161,6 +161,7 @@ } from '../stores'; import { checkCanInviteSelf, formatDateFilter, hasGroupPermission, parseLocation } from '../shared/utils'; import { instanceRequest, miscRequest } from '../api'; + import { showUserDialog } from '../coordinators/userCoordinator'; defineOptions({ inheritAttrs: false @@ -352,9 +353,7 @@ } }; - const showUserDialog = (userId) => { - userStore.showUserDialog(userId); - }; + const closeInstance = (location) => { modalStore diff --git a/src/components/Location.vue b/src/components/Location.vue index 6c9363ae..2bac7065 100644 --- a/src/components/Location.vue +++ b/src/components/Location.vue @@ -50,12 +50,13 @@ useSearchStore, useWorldStore } from '../stores'; + import { showWorldDialog } from '../coordinators/worldCoordinator'; import { Spinner } from './ui/spinner'; import { accessTypeLocaleKeyMap } from '../shared/constants'; const { t } = useI18n(); - const { cachedWorlds, showWorldDialog } = useWorldStore(); + const { cachedWorlds } = useWorldStore(); const { showGroupDialog } = useGroupStore(); const { showPreviousInstancesInfoDialog } = useInstanceStore(); const { verifyShortName } = useSearchStore(); diff --git a/src/components/LocationWorld.vue b/src/components/LocationWorld.vue index 86721799..e1ff62a9 100644 --- a/src/components/LocationWorld.vue +++ b/src/components/LocationWorld.vue @@ -5,7 +5,7 @@ {{ accessTypeName }} #{{ instanceName }} - ({{ groupName }}) + ({{ groupName }}) @@ -20,6 +20,7 @@ import { useI18n } from 'vue-i18n'; import { useGroupStore, useInstanceStore, useLaunchStore } from '../stores'; + import { showGroupDialog } from '../coordinators/groupCoordinator'; import { getGroupName, parseLocation } from '../shared/utils'; import { accessTypeLocaleKeyMap } from '../shared/constants'; @@ -135,10 +136,10 @@ /** * */ - function showGroupDialog() { + function openLocationGroupDialog() { if (!location.value) return; const L = parseLocation(location.value); if (!L.groupId) return; - groupStore.showGroupDialog(L.groupId); + showGroupDialog(L.groupId); } diff --git a/src/components/dialogs/AvatarDialog/AvatarDialog.vue b/src/components/dialogs/AvatarDialog/AvatarDialog.vue index 65ce0f76..fb735c44 100644 --- a/src/components/dialogs/AvatarDialog/AvatarDialog.vue +++ b/src/components/dialogs/AvatarDialog/AvatarDialog.vue @@ -601,20 +601,22 @@ import { formatJsonVars } from '../../../shared/utils/base/ui'; import { handleImageUploadInput } from '../../../shared/utils/imageUpload'; import { runDeleteVRChatCacheFlow as deleteVRChatCache } from '../../../coordinators/gameCoordinator'; + import { showAvatarDialog, applyAvatar, selectAvatarWithoutConfirmation } from '../../../coordinators/avatarCoordinator'; import { useAvatarDialogCommands } from './useAvatarDialogCommands'; import DialogJsonTab from '../DialogJsonTab.vue'; import ImageCropDialog from '../ImageCropDialog.vue'; +import { showUserDialog } from '../../../coordinators/userCoordinator'; const SetAvatarStylesDialog = defineAsyncComponent(() => import('./SetAvatarStylesDialog.vue')); const SetAvatarTagsDialog = defineAsyncComponent(() => import('./SetAvatarTagsDialog.vue')); - const { showUserDialog, sortUserDialogAvatars } = useUserStore(); + const { sortUserDialogAvatars } = useUserStore(); const { userDialog, currentUser } = storeToRefs(useUserStore()); const avatarStore = useAvatarStore(); const { cachedAvatarModerations, cachedAvatars } = avatarStore; const { avatarDialog } = storeToRefs(avatarStore); - const { showAvatarDialog, getAvatarGallery, applyAvatarModeration, applyAvatar, selectAvatarWithoutConfirmation } = + const { getAvatarGallery, applyAvatarModeration } = avatarStore; const { showFavoriteDialog } = useFavoriteStore(); const { isGameRunning } = storeToRefs(useGameStore()); diff --git a/src/components/dialogs/AvatarDialog/SetAvatarStylesDialog.vue b/src/components/dialogs/AvatarDialog/SetAvatarStylesDialog.vue index 5f100ff6..dfb91e1e 100644 --- a/src/components/dialogs/AvatarDialog/SetAvatarStylesDialog.vue +++ b/src/components/dialogs/AvatarDialog/SetAvatarStylesDialog.vue @@ -93,6 +93,7 @@ import { avatarRequest, queryRequest } from '../../../api'; import { arraysMatch } from '../../../shared/utils'; import { useAvatarStore } from '../../../stores'; + import { applyAvatar } from '../../../coordinators/avatarCoordinator'; const props = defineProps({ setAvatarStylesDialog: { @@ -104,7 +105,6 @@ const emit = defineEmits(['update:setAvatarStylesDialog']); const { t } = useI18n(); - const { applyAvatar } = useAvatarStore(); const SELECT_CLEAR_VALUE = '__clear__'; diff --git a/src/components/dialogs/AvatarDialog/SetAvatarTagsDialog.vue b/src/components/dialogs/AvatarDialog/SetAvatarTagsDialog.vue index de667867..46aca514 100644 --- a/src/components/dialogs/AvatarDialog/SetAvatarTagsDialog.vue +++ b/src/components/dialogs/AvatarDialog/SetAvatarTagsDialog.vue @@ -128,8 +128,8 @@ import { avatarRequest } from '../../../api'; import { removeFromArray } from '../../../shared/utils'; import { useAvatarStore } from '../../../stores'; + import { showAvatarDialog, applyAvatar } from '../../../coordinators/avatarCoordinator'; - const { showAvatarDialog, applyAvatar } = useAvatarStore(); const { cachedAvatars } = useAvatarStore(); const { t } = useI18n(); diff --git a/src/components/dialogs/GroupDialog/GroupDialog.vue b/src/components/dialogs/GroupDialog/GroupDialog.vue index f3f7025b..0e1a752c 100644 --- a/src/components/dialogs/GroupDialog/GroupDialog.vue +++ b/src/components/dialogs/GroupDialog/GroupDialog.vue @@ -407,6 +407,13 @@ removeFromArray } from '../../../shared/utils'; import { useGalleryStore, useGroupStore, useModalStore, useUserStore } from '../../../stores'; + import { + getGroupDialogGroup, + showGroupDialog, + leaveGroupPrompt, + setGroupVisibility, + setGroupSubscription + } from '../../../coordinators/groupCoordinator'; import { groupRequest, queryRequest } from '../../../api'; import { queryKeys, refetchActiveEntityQuery } from '../../../queries'; import { Badge } from '../../ui/badge'; @@ -419,6 +426,7 @@ import GroupDialogPhotosTab from './GroupDialogPhotosTab.vue'; import GroupDialogPostsTab from './GroupDialogPostsTab.vue'; import GroupPostEditDialog from './GroupPostEditDialog.vue'; +import { showUserDialog } from '../../../coordinators/userCoordinator'; const { t } = useI18n(); const groupDialogTabs = computed(() => [ @@ -431,16 +439,11 @@ const modalStore = useModalStore(); - const { showUserDialog } = useUserStore(); + const { currentUser } = storeToRefs(useUserStore()); const { groupDialog, inviteGroupDialog } = storeToRefs(useGroupStore()); const { - getGroupDialogGroup, updateGroupPostSearch, - showGroupDialog, - leaveGroupPrompt, - setGroupVisibility, - setGroupSubscription, showGroupMemberModerationDialog } = useGroupStore(); diff --git a/src/components/dialogs/GroupDialog/GroupDialogInfoTab.vue b/src/components/dialogs/GroupDialog/GroupDialogInfoTab.vue index 86d44928..14ebb74e 100644 --- a/src/components/dialogs/GroupDialog/GroupDialogInfoTab.vue +++ b/src/components/dialogs/GroupDialog/GroupDialogInfoTab.vue @@ -361,6 +361,7 @@ import GroupCalendarEventCard from '../../../views/Tools/components/GroupCalendarEventCard.vue'; import InstanceActionBar from '../../InstanceActionBar.vue'; +import { showUserDialog } from '../../../coordinators/userCoordinator'; const props = defineProps({ showGroupPostEditDialog: { @@ -375,7 +376,7 @@ const { t } = useI18n(); - const { showUserDialog } = useUserStore(); + const { groupDialog } = storeToRefs(useGroupStore()); const { lastLocation } = storeToRefs(useLocationStore()); const { showFullscreenImageDialog } = useGalleryStore(); diff --git a/src/components/dialogs/GroupDialog/GroupDialogMembersTab.vue b/src/components/dialogs/GroupDialog/GroupDialogMembersTab.vue index 0022ffc5..15dd038d 100644 --- a/src/components/dialogs/GroupDialog/GroupDialogMembersTab.vue +++ b/src/components/dialogs/GroupDialog/GroupDialogMembersTab.vue @@ -212,15 +212,16 @@ import { downloadAndSaveJson, hasGroupPermission, userImage } from '../../../shared/utils'; 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 { t } = useI18n(); - const { showUserDialog } = useUserStore(); + const { currentUser } = storeToRefs(useUserStore()); const { groupDialog } = storeToRefs(useGroupStore()); - const { applyGroupMember, handleGroupMember } = useGroupStore(); const { isGroupMembersDone, diff --git a/src/components/dialogs/GroupDialog/GroupMemberModerationDialog.vue b/src/components/dialogs/GroupDialog/GroupMemberModerationDialog.vue index 1245da20..b21be5e5 100644 --- a/src/components/dialogs/GroupDialog/GroupMemberModerationDialog.vue +++ b/src/components/dialogs/GroupDialog/GroupMemberModerationDialog.vue @@ -122,6 +122,7 @@ import { useI18n } from 'vue-i18n'; import { useAppearanceSettingsStore, useGalleryStore, useGroupStore, useUserStore } from '../../../stores'; + import { applyGroupMember, handleGroupMember, handleGroupMemberProps } from '../../../coordinators/groupCoordinator'; import { hasGroupPermission, userImage, userImageFull } from '../../../shared/utils'; import { groupDialogFilterOptions, groupDialogSortingOptions } from '../../../shared/constants'; import { groupRequest, userRequest } from '../../../api'; @@ -138,14 +139,14 @@ import GroupModerationInvitesTab from './GroupModerationInvitesTab.vue'; import GroupModerationLogsTab from './GroupModerationLogsTab.vue'; import GroupModerationMembersTab from './GroupModerationMembersTab.vue'; +import { showUserDialog } from '../../../coordinators/userCoordinator'; // ── Stores ─────────────────────────────────────────────────── const appearanceSettingsStore = useAppearanceSettingsStore(); const { randomUserColours } = storeToRefs(appearanceSettingsStore); - const { showUserDialog } = useUserStore(); + const { currentUser } = storeToRefs(useUserStore()); const { groupDialog, groupMemberModeration } = storeToRefs(useGroupStore()); - const { applyGroupMember, handleGroupMember, handleGroupMemberProps } = useGroupStore(); const { showFullscreenImageDialog } = useGalleryStore(); const { t } = useI18n(); diff --git a/src/components/dialogs/PreviousInstancesDialog/PreviousInstancesInfoDialog.vue b/src/components/dialogs/PreviousInstancesDialog/PreviousInstancesInfoDialog.vue index 68691b63..44438bc6 100644 --- a/src/components/dialogs/PreviousInstancesDialog/PreviousInstancesInfoDialog.vue +++ b/src/components/dialogs/PreviousInstancesDialog/PreviousInstancesInfoDialog.vue @@ -43,8 +43,9 @@ import { createColumns } from './previousInstancesInfoColumns.jsx'; import { database } from '../../../service/database'; import { useVrcxVueTable } from '../../../lib/table/useVrcxVueTable'; +import { lookupUser } from '../../../coordinators/userCoordinator'; - const { lookupUser } = useUserStore(); + const { previousInstancesInfoDialog, previousInstancesInfoState } = storeToRefs(useInstanceStore()); const { gameLogIsFriend, gameLogIsFavorite } = useGameLogStore(); const { t } = useI18n(); diff --git a/src/components/dialogs/UserDialog/UserDialog.vue b/src/components/dialogs/UserDialog/UserDialog.vue index 428fb404..c1e15513 100644 --- a/src/components/dialogs/UserDialog/UserDialog.vue +++ b/src/components/dialogs/UserDialog/UserDialog.vue @@ -91,6 +91,7 @@ useNotificationStore, useUserStore } from '../../../stores'; + import { showGroupDialog } from '../../../coordinators/groupCoordinator'; import { copyToClipboard } from '../../../shared/utils'; import { formatJsonVars } from '../../../shared/utils/base/ui'; import { miscRequest } from '../../../api'; @@ -139,11 +140,12 @@ const instanceStore = useInstanceStore(); const { userDialog, languageDialog, currentUser, isLocalUserVrcPlusSupporter } = storeToRefs(useUserStore()); - const { cachedUsers, showUserDialog, refreshUserDialogAvatars, showSendBoopDialog } = useUserStore(); + const { cachedUsers, showSendBoopDialog } = useUserStore(); const { showFavoriteDialog } = useFavoriteStore(); - const { showAvatarDialog, showAvatarAuthorDialog } = useAvatarStore(); + import { showAvatarDialog, showAvatarAuthorDialog } from '../../../coordinators/avatarCoordinator'; +import { showUserDialog, refreshUserDialogAvatars } from '../../../coordinators/userCoordinator'; - const { showGroupDialog, showModerateGroupDialog } = useGroupStore(); + const { showModerateGroupDialog } = useGroupStore(); const { inviteGroupDialog } = storeToRefs(useGroupStore()); const { lastLocation, lastLocationDestination } = storeToRefs(useLocationStore()); const { refreshInviteMessageTableData } = useInviteStore(); diff --git a/src/components/dialogs/UserDialog/UserDialogAvatarsTab.vue b/src/components/dialogs/UserDialog/UserDialogAvatarsTab.vue index da798bd6..7fe1d471 100644 --- a/src/components/dialogs/UserDialog/UserDialogAvatarsTab.vue +++ b/src/components/dialogs/UserDialog/UserDialogAvatarsTab.vue @@ -119,7 +119,7 @@ const { userDialog, currentUser } = storeToRefs(userStore); const { sortUserDialogAvatars, refreshUserDialogAvatars } = userStore; - const { showAvatarDialog, lookupAvatars } = useAvatarStore(); + import { showAvatarDialog, lookupAvatars } from '../../../coordinators/avatarCoordinator'; const { cachedAvatars } = useAvatarStore(); const { avatarRemoteDatabase } = storeToRefs(useAdvancedSettingsStore()); diff --git a/src/components/dialogs/UserDialog/UserDialogFavoriteWorldsTab.vue b/src/components/dialogs/UserDialog/UserDialogFavoriteWorldsTab.vue index 5cd3c16a..6ea82202 100644 --- a/src/components/dialogs/UserDialog/UserDialogFavoriteWorldsTab.vue +++ b/src/components/dialogs/UserDialog/UserDialogFavoriteWorldsTab.vue @@ -76,6 +76,7 @@ import DeprecationAlert from '@/components/DeprecationAlert.vue'; import { useFavoriteStore, useUserStore, useWorldStore } from '../../../stores'; + import { showWorldDialog } from '../../../coordinators/worldCoordinator'; import { handleFavoriteWorldList } from '../../../coordinators/favoriteCoordinator'; import { favoriteRequest } from '../../../api'; @@ -83,7 +84,7 @@ const { userDialog, currentUser } = storeToRefs(useUserStore()); const { favoriteLimits } = storeToRefs(useFavoriteStore()); - const { showWorldDialog } = useWorldStore(); + const favoriteWorldsTab = ref('0'); const userDialogFavoriteWorldsRequestId = ref(0); diff --git a/src/components/dialogs/UserDialog/UserDialogGroupsTab.vue b/src/components/dialogs/UserDialog/UserDialogGroupsTab.vue index c2f584dd..209714b1 100644 --- a/src/components/dialogs/UserDialog/UserDialogGroupsTab.vue +++ b/src/components/dialogs/UserDialog/UserDialogGroupsTab.vue @@ -372,15 +372,7 @@ import { useI18n } from 'vue-i18n'; import { useAuthStore, useGroupStore, useUiStore, useUserStore } from '../../../stores'; - import { compareByMemberCount, compareByName } from '../../../shared/utils'; - import { groupRequest } from '../../../api'; - import { useOptionKeySelect } from '../../../composables/useOptionKeySelect'; - import { userDialogGroupSortingOptions } from '../../../shared/constants'; - - const { t } = useI18n(); - - const { userDialog, currentUser, isLocalUserVrcPlusSupporter } = storeToRefs(useUserStore()); - const { + import { showGroupDialog, applyGroup, saveCurrentUserGroups, @@ -389,7 +381,15 @@ leaveGroupPrompt, setGroupVisibility, handleGroupList - } = useGroupStore(); + } from '../../../coordinators/groupCoordinator'; + import { compareByMemberCount, compareByName } from '../../../shared/utils'; + import { groupRequest } from '../../../api'; + import { useOptionKeySelect } from '../../../composables/useOptionKeySelect'; + import { userDialogGroupSortingOptions } from '../../../shared/constants'; + + const { t } = useI18n(); + + const { userDialog, currentUser, isLocalUserVrcPlusSupporter } = storeToRefs(useUserStore()); const { currentUserGroups, inGameGroupOrder } = storeToRefs(useGroupStore()); const { cachedConfig } = storeToRefs(useAuthStore()); const { shiftHeld } = storeToRefs(useUiStore()); diff --git a/src/components/dialogs/UserDialog/UserDialogInfoTab.vue b/src/components/dialogs/UserDialog/UserDialogInfoTab.vue index 3b707881..2043307a 100644 --- a/src/components/dialogs/UserDialog/UserDialogInfoTab.vue +++ b/src/components/dialogs/UserDialog/UserDialogInfoTab.vue @@ -501,9 +501,11 @@ useUserStore, useWorldStore } from '../../../stores'; + import { showWorldDialog } from '../../../coordinators/worldCoordinator'; import { queryRequest, userRequest } from '../../../api'; import InstanceActionBar from '../../InstanceActionBar.vue'; +import { showUserDialog } from '../../../coordinators/userCoordinator'; const EditNoteAndMemoDialog = defineAsyncComponent(() => import('./EditNoteAndMemoDialog.vue')); @@ -518,8 +520,8 @@ const { bioLanguage, translationApi, translationApiType } = storeToRefs(useAdvancedSettingsStore()); const { translateText } = useAdvancedSettingsStore(); const { userDialog, currentUser } = storeToRefs(useUserStore()); - const { showUserDialog, toggleSharedConnectionsOptOut, toggleDiscordFriendsOptOut } = useUserStore(); - const { showWorldDialog } = useWorldStore(); + const { toggleSharedConnectionsOptOut, toggleDiscordFriendsOptOut } = useUserStore(); + const { showGroupDialog } = useGroupStore(); const { lastLocation } = storeToRefs(useLocationStore()); const { showFullscreenImageDialog } = useGalleryStore(); diff --git a/src/components/dialogs/UserDialog/UserDialogMutualFriendsTab.vue b/src/components/dialogs/UserDialog/UserDialogMutualFriendsTab.vue index f6dec69e..852d15a8 100644 --- a/src/components/dialogs/UserDialog/UserDialogMutualFriendsTab.vue +++ b/src/components/dialogs/UserDialog/UserDialogMutualFriendsTab.vue @@ -73,12 +73,13 @@ import { useUserStore } from '../../../stores'; import { userDialogMutualFriendSortingOptions } from '../../../shared/constants'; import { userRequest } from '../../../api'; +import { showUserDialog } from '../../../coordinators/userCoordinator'; const { t } = useI18n(); const userStore = useUserStore(); const { userDialog, currentUser } = storeToRefs(userStore); - const { cachedUsers, showUserDialog } = userStore; + const { cachedUsers } = userStore; const { selectedKey: userDialogMutualFriendSortingKey, selectByKey: setUserDialogMutualFriendSortingByKey } = useOptionKeySelect( diff --git a/src/components/dialogs/UserDialog/UserDialogWorldsTab.vue b/src/components/dialogs/UserDialog/UserDialogWorldsTab.vue index 46b5c4aa..9b655d2f 100644 --- a/src/components/dialogs/UserDialog/UserDialogWorldsTab.vue +++ b/src/components/dialogs/UserDialog/UserDialogWorldsTab.vue @@ -86,6 +86,7 @@ import { useI18n } from 'vue-i18n'; import { useUserStore, useWorldStore } from '../../../stores'; + import { showWorldDialog } from '../../../coordinators/worldCoordinator'; import { userDialogWorldOrderOptions, userDialogWorldSortingOptions } from '../../../shared/constants/'; import { queryRequest } from '../../../api'; import { useOptionKeySelect } from '../../../composables/useOptionKeySelect'; @@ -94,7 +95,7 @@ const userStore = useUserStore(); const { userDialog, currentUser } = storeToRefs(userStore); - const { cachedWorlds, showWorldDialog } = useWorldStore(); + const { cachedWorlds } = useWorldStore(); const userDialogWorldsRequestId = ref(0); diff --git a/src/components/dialogs/WorldDialog/SetWorldTagsDialog.vue b/src/components/dialogs/WorldDialog/SetWorldTagsDialog.vue index 06dcdb7d..f899b0fe 100644 --- a/src/components/dialogs/WorldDialog/SetWorldTagsDialog.vue +++ b/src/components/dialogs/WorldDialog/SetWorldTagsDialog.vue @@ -110,6 +110,7 @@ import { useI18n } from 'vue-i18n'; import { useWorldStore } from '../../../stores'; + import { showWorldDialog } from '../../../coordinators/worldCoordinator'; import { worldRequest } from '../../../api'; const props = defineProps({ @@ -133,7 +134,7 @@ const emit = defineEmits(['update:isSetWorldTagsDialogVisible']); - const { showWorldDialog } = useWorldStore(); + const { t } = useI18n(); diff --git a/src/components/dialogs/WorldDialog/WorldDialog.vue b/src/components/dialogs/WorldDialog/WorldDialog.vue index 1c3cbabf..168b57cb 100644 --- a/src/components/dialogs/WorldDialog/WorldDialog.vue +++ b/src/components/dialogs/WorldDialog/WorldDialog.vue @@ -395,6 +395,7 @@ useUserStore, useWorldStore } from '../../../stores'; + import { showWorldDialog } from '../../../coordinators/worldCoordinator'; import { DropdownMenu, DropdownMenuContent, @@ -412,15 +413,16 @@ import ImageCropDialog from '../ImageCropDialog.vue'; import WorldDialogInfoTab from './WorldDialogInfoTab.vue'; import WorldDialogInstancesTab from './WorldDialogInstancesTab.vue'; +import { showUserDialog } from '../../../coordinators/userCoordinator'; const SetWorldTagsDialog = defineAsyncComponent(() => import('./SetWorldTagsDialog.vue')); const WorldAllowedDomainsDialog = defineAsyncComponent(() => import('./WorldAllowedDomainsDialog.vue')); const NewInstanceDialog = defineAsyncComponent(() => import('../NewInstanceDialog.vue')); - const { showUserDialog } = useUserStore(); + const { currentUser, userDialog } = storeToRefs(useUserStore()); const { worldDialog } = storeToRefs(useWorldStore()); - const { cachedWorlds, showWorldDialog } = useWorldStore(); + const { cachedWorlds } = useWorldStore(); const { lastLocation } = storeToRefs(useLocationStore()); const { canOpenInstanceInGame } = useInviteStore(); const { showFavoriteDialog } = useFavoriteStore(); diff --git a/src/components/dialogs/WorldDialog/WorldDialogInstancesTab.vue b/src/components/dialogs/WorldDialog/WorldDialogInstancesTab.vue index a304f94f..426cc337 100644 --- a/src/components/dialogs/WorldDialog/WorldDialogInstancesTab.vue +++ b/src/components/dialogs/WorldDialog/WorldDialogInstancesTab.vue @@ -118,11 +118,12 @@ } from '../../../stores'; import InstanceActionBar from '../../InstanceActionBar.vue'; +import { showUserDialog } from '../../../coordinators/userCoordinator'; const { t } = useI18n(); const { isAgeGatedInstancesVisible } = storeToRefs(useAppearanceSettingsStore()); - const { showUserDialog } = useUserStore(); + const { currentUser } = storeToRefs(useUserStore()); const { worldDialog } = storeToRefs(useWorldStore()); const { lastLocation } = storeToRefs(useLocationStore()); diff --git a/src/coordinators/authAutoLoginCoordinator.js b/src/coordinators/authAutoLoginCoordinator.js index b97acfa9..af866948 100644 --- a/src/coordinators/authAutoLoginCoordinator.js +++ b/src/coordinators/authAutoLoginCoordinator.js @@ -1,5 +1,5 @@ import { toast } from 'vue-sonner'; -import { useI18n } from 'vue-i18n'; +import { i18n } from '../plugin/i18n'; import { AppDebug } from '../service/appConfig'; import { useAdvancedSettingsStore } from '../stores/settings/advanced'; @@ -17,7 +17,7 @@ export async function runHandleAutoLoginFlow({ } = {}) { const authStore = useAuthStore(); const advancedSettingsStore = useAdvancedSettingsStore(); - const { t } = useI18n(); + const t = i18n.global.t; if (authStore.attemptingAutoLogin) { return; diff --git a/src/coordinators/authCoordinator.js b/src/coordinators/authCoordinator.js index b229902c..285dedf9 100644 --- a/src/coordinators/authCoordinator.js +++ b/src/coordinators/authCoordinator.js @@ -1,4 +1,4 @@ -import { useI18n } from 'vue-i18n'; +import { i18n } from '../plugin/i18n'; import Noty from 'noty'; @@ -9,6 +9,7 @@ import { useAuthStore } from '../stores/auth'; import { useNotificationStore } from '../stores/notification'; import { useUpdateLoopStore } from '../stores/updateLoop'; import { useUserStore } from '../stores/user'; +import { applyCurrentUser } from './userCoordinator'; import { watchState } from '../service/watchState'; import configRepository from '../service/config'; @@ -21,7 +22,7 @@ export async function runLogoutFlow() { const authStore = useAuthStore(); const userStore = useUserStore(); const notificationStore = useNotificationStore(); - const { t } = useI18n(); + const t = i18n.global.t; if (watchState.isLoggedIn) { new Noty({ @@ -56,6 +57,6 @@ export function runLoginSuccessFlow(json) { const userStore = useUserStore(); updateLoopStore.setNextCurrentUserRefresh(420); // 7mins - userStore.applyCurrentUser(json); + applyCurrentUser(json); initWebsocket(); } diff --git a/src/coordinators/avatarCoordinator.js b/src/coordinators/avatarCoordinator.js new file mode 100644 index 00000000..77f48203 --- /dev/null +++ b/src/coordinators/avatarCoordinator.js @@ -0,0 +1,649 @@ +import { nextTick } from 'vue'; +import { toast } from 'vue-sonner'; +import { i18n } from '../plugin/i18n'; + +import { + createDefaultAvatarRef, + extractFileId, + getAvailablePlatforms, + getBundleDateSize, + getPlatformInfo, + replaceBioSymbols, + sanitizeEntityJson, + storeAvatarImage +} from '../shared/utils'; +import { avatarRequest, miscRequest, queryRequest } from '../api'; +import { AppDebug } from '../service/appConfig'; +import { database } from '../service/database'; +import { patchAvatarFromEvent } from '../queries'; +import { processBulk } from '../service/request'; +import { applyFavorite } from './favoriteCoordinator'; +import { refreshUserDialogAvatars, showUserDialog } from './userCoordinator'; +import { useAdvancedSettingsStore } from '../stores/settings/advanced'; +import { useAvatarProviderStore } from '../stores/avatarProvider'; +import { useAvatarStore } from '../stores/avatar'; +import { useFavoriteStore } from '../stores/favorite'; +import { useModalStore } from '../stores/modal'; +import { useUiStore } from '../stores/ui'; +import { useUserStore } from '../stores/user'; +import { useVRCXUpdaterStore } from '../stores/vrcxUpdater'; + +import webApiService from '../service/webapi'; + +/** + * @param {object} json + * @returns {object} ref + */ +export function applyAvatar(json) { + const avatarStore = useAvatarStore(); + const favoriteStore = useFavoriteStore(); + + sanitizeEntityJson(json, ['name', 'description']); + let ref = avatarStore.cachedAvatars.get(json.id); + if (typeof ref === 'undefined') { + ref = createDefaultAvatarRef(json); + avatarStore.cachedAvatars.set(ref.id, ref); + } else { + const { unityPackages } = ref; + Object.assign(ref, json); + if ( + json.unityPackages?.length > 0 && + unityPackages.length > 0 && + !json.unityPackages[0].assetUrl + ) { + ref.unityPackages = unityPackages; + } + } + for (const listing of ref.publishedListings) { + listing.displayName = replaceBioSymbols(listing.displayName); + listing.description = replaceBioSymbols(listing.description); + } + applyFavorite('avatar', ref.id); + if (favoriteStore.localAvatarFavoritesList.includes(ref.id)) { + const avatarRef = ref; + favoriteStore.syncLocalAvatarFavoriteRef(avatarRef); + + // update db cache + database.addAvatarToCache(avatarRef); + } + patchAvatarFromEvent(ref); + return ref; +} + +/** + * + * @param {string} avatarId + * @param options + * @returns + */ +export function showAvatarDialog(avatarId, options = {}) { + const avatarStore = useAvatarStore(); + const uiStore = useUiStore(); + const favoriteStore = useFavoriteStore(); + const userStore = useUserStore(); + const t = i18n.global.t; + + const D = avatarStore.avatarDialog; + const forceRefresh = Boolean(options?.forceRefresh); + const isMainDialogOpen = uiStore.openDialog({ + type: 'avatar', + id: avatarId + }); + D.visible = true; + if (isMainDialogOpen && D.id === avatarId && !forceRefresh) { + uiStore.setDialogCrumbLabel('avatar', D.id, D.ref?.name || D.id); + nextTick(() => (D.loading = false)); + return; + } + D.loading = true; + D.id = avatarId; + D.inCache = false; + D.cacheSize = ''; + D.cacheLocked = false; + D.cachePath = ''; + D.fileAnalysis = {}; + D.isQuestFallback = false; + D.isPC = false; + D.isQuest = false; + D.isIos = false; + D.hasImposter = false; + D.imposterVersion = ''; + D.platformInfo = {}; + D.galleryImages = []; + D.galleryLoading = true; + D.isFavorite = + favoriteStore.getCachedFavoritesByObjectId(avatarId) || + (userStore.isLocalUserVrcPlusSupporter && + favoriteStore.localAvatarFavoritesList.includes(avatarId)); + D.isBlocked = avatarStore.cachedAvatarModerations.has(avatarId); + const ref2 = avatarStore.cachedAvatars.get(avatarId); + if (typeof ref2 !== 'undefined') { + D.ref = ref2; + uiStore.setDialogCrumbLabel('avatar', D.id, D.ref?.name || D.id); + nextTick(() => (D.loading = false)); + } + const loadAvatarRequest = forceRefresh + ? avatarRequest.getAvatar({ avatarId }) + : queryRequest.fetch('avatar', { avatarId }); + loadAvatarRequest + .then((args) => { + const ref = applyAvatar(args.json); + D.ref = ref; + uiStore.setDialogCrumbLabel( + 'avatar', + D.id, + D.ref?.name || D.id + ); + avatarStore.getAvatarGallery(avatarId); + avatarStore.updateVRChatAvatarCache(); + if (/quest/.test(ref.tags)) { + D.isQuestFallback = true; + } + const { isPC, isQuest, isIos } = getAvailablePlatforms( + ref.unityPackages + ); + D.isPC = isPC; + D.isQuest = isQuest; + D.isIos = isIos; + D.platformInfo = getPlatformInfo(ref.unityPackages); + for (let i = ref.unityPackages.length - 1; i > -1; i--) { + const unityPackage = ref.unityPackages[i]; + if (unityPackage.variant === 'impostor') { + D.hasImposter = true; + D.imposterVersion = unityPackage.impostorizerVersion; + break; + } + } + if (Object.keys(D.fileAnalysis).length === 0) { + getBundleDateSize(ref); + } + }) + .catch((err) => { + D.loading = false; + D.id = null; + D.visible = false; + uiStore.jumpBackDialogCrumb(); + toast.error(t('message.api_handler.avatar_private_or_deleted')); + throw err; + }) + .finally(() => { + nextTick(() => (D.loading = false)); + }); +} + +/** + * + * @returns {Promise} + */ +export async function getAvatarHistory() { + const avatarStore = useAvatarStore(); + const userStore = useUserStore(); + + const historyArray = await database.getAvatarHistory( + userStore.currentUser.id + ); + for (let i = 0; i < historyArray.length; i++) { + const avatar = historyArray[i]; + if (avatar.authorId === userStore.currentUser.id) { + continue; + } + applyAvatar(avatar); + } + avatarStore.avatarHistory = historyArray; +} + +/** + * @param {string} avatarId + */ +export function addAvatarToHistory(avatarId) { + const avatarStore = useAvatarStore(); + const userStore = useUserStore(); + + avatarRequest + .getAvatar({ avatarId }) + .then((args) => { + const ref = applyAvatar(args.json); + + database.addAvatarToCache(ref); + database.addAvatarToHistory(ref.id); + + if (ref.authorId === userStore.currentUser.id) { + return; + } + + const historyArray = avatarStore.avatarHistory; + for (let i = 0; i < historyArray.length; ++i) { + if (historyArray[i].id === ref.id) { + historyArray.splice(i, 1); + } + } + + avatarStore.avatarHistory.unshift(ref); + }) + .catch((err) => { + console.error('Failed to add avatar to history:', err); + }); +} + +/** + * + */ +export function promptClearAvatarHistory() { + const avatarStore = useAvatarStore(); + const modalStore = useModalStore(); + const t = i18n.global.t; + + modalStore + .confirm({ + description: t('confirm.clear_avatar_history'), + title: 'Confirm' + }) + .then(({ ok }) => { + if (!ok) return; + avatarStore.clearAvatarHistory(); + }) + .catch(() => {}); +} + +/** + * + * @param {string} imageUrl + * @returns {Promise} + */ +export async function getAvatarName(imageUrl) { + const avatarStore = useAvatarStore(); + + const fileId = extractFileId(imageUrl); + if (!fileId) { + return { + ownerId: '', + avatarName: '-' + }; + } + if (avatarStore.cachedAvatarNames.has(fileId)) { + return avatarStore.cachedAvatarNames.get(fileId); + } + try { + const args = await miscRequest.getFile({ fileId }); + return storeAvatarImage(args, avatarStore.cachedAvatarNames); + } catch (error) { + console.error('Failed to get avatar images:', error); + return { + ownerId: '', + avatarName: '-' + }; + } +} + +/** + * + * @param type + * @param search + */ +export async function lookupAvatars(type, search) { + const avatarProviderStore = useAvatarProviderStore(); + const vrcxUpdaterStore = useVRCXUpdaterStore(); + + const avatars = new Map(); + if (type === 'search') { + try { + const url = `${ + avatarProviderStore.avatarRemoteDatabaseProvider + }?${type}=${encodeURIComponent(search)}&n=5000`; + const response = await webApiService.execute({ + url, + method: 'GET', + headers: { + Referer: 'https://vrcx.app', + 'VRCX-ID': vrcxUpdaterStore.vrcxId + } + }); + const json = JSON.parse(response.data); + if (AppDebug.debugWebRequests) { + console.log(url, json, response); + } + if (response.status === 200 && typeof json === 'object') { + json.forEach((avatar) => { + if (!avatars.has(avatar.Id)) { + const ref = { + authorId: '', + authorName: '', + name: '', + description: '', + id: '', + imageUrl: '', + thumbnailImageUrl: '', + created_at: '0001-01-01T00:00:00.0000000Z', + updated_at: '0001-01-01T00:00:00.0000000Z', + releaseStatus: 'public', + ...avatar + }; + avatars.set(ref.id, ref); + } + }); + } else { + throw new Error(`Error: ${response.data}`); + } + } catch (err) { + const msg = `Avatar search failed for ${search} with ${avatarProviderStore.avatarRemoteDatabaseProvider}\n${err}`; + console.error(msg); + toast.error(msg); + } + } else if (type === 'authorId') { + const length = + avatarProviderStore.avatarRemoteDatabaseProviderList.length; + for (let i = 0; i < length; ++i) { + const url = + avatarProviderStore.avatarRemoteDatabaseProviderList[i]; + const avatarArray = await lookupAvatarsByAuthor(url, search); + avatarArray.forEach((avatar) => { + if (!avatars.has(avatar.id)) { + avatars.set(avatar.id, avatar); + } + }); + } + } + return avatars; +} + +/** + * + * @param authorId + * @param fileId + */ +export async function lookupAvatarByImageFileId(authorId, fileId) { + const avatarProviderStore = useAvatarProviderStore(); + + for (const providerUrl of avatarProviderStore.avatarRemoteDatabaseProviderList) { + const avatar = await lookupAvatarByFileId(providerUrl, fileId); + if (avatar?.id) { + return avatar.id; + } + } + + for (const providerUrl of avatarProviderStore.avatarRemoteDatabaseProviderList) { + const avatarArray = await lookupAvatarsByAuthor( + providerUrl, + authorId + ); + for (const avatar of avatarArray) { + if (extractFileId(avatar.imageUrl) === fileId) { + return avatar.id; + } + } + } + return null; +} + +/** + * + * @param providerUrl + * @param fileId + */ +async function lookupAvatarByFileId(providerUrl, fileId) { + const vrcxUpdaterStore = useVRCXUpdaterStore(); + + try { + const url = `${providerUrl}?fileId=${encodeURIComponent(fileId)}`; + const response = await webApiService.execute({ + url, + method: 'GET', + headers: { + Referer: 'https://vrcx.app', + 'VRCX-ID': vrcxUpdaterStore.vrcxId + } + }); + const json = JSON.parse(response.data); + if (AppDebug.debugWebRequests) { + console.log(url, json, response); + } + if (response.status === 200 && typeof json === 'object') { + const ref = { + authorId: '', + authorName: '', + name: '', + description: '', + id: '', + imageUrl: '', + thumbnailImageUrl: '', + created_at: '0001-01-01T00:00:00.0000000Z', + updated_at: '0001-01-01T00:00:00.0000000Z', + releaseStatus: 'public', + ...json + }; + return ref; + } else { + return null; + } + } catch (err) { + // ignore errors for now, not all providers support this lookup type + return null; + } +} + +/** + * + * @param providerUrl + * @param authorId + */ +async function lookupAvatarsByAuthor(providerUrl, authorId) { + const vrcxUpdaterStore = useVRCXUpdaterStore(); + + const avatars = []; + if (!providerUrl || !authorId) { + return avatars; + } + const url = `${providerUrl}?authorId=${encodeURIComponent(authorId)}`; + try { + const response = await webApiService.execute({ + url, + method: 'GET', + headers: { + Referer: 'https://vrcx.app', + 'VRCX-ID': vrcxUpdaterStore.vrcxId + } + }); + const json = JSON.parse(response.data); + if (AppDebug.debugWebRequests) { + console.log(url, json, response); + } + if (response.status === 200 && typeof json === 'object') { + json.forEach((avatar) => { + const ref = { + authorId: '', + authorName: '', + name: '', + description: '', + id: '', + imageUrl: '', + thumbnailImageUrl: '', + created_at: '0001-01-01T00:00:00.0000000Z', + updated_at: '0001-01-01T00:00:00.0000000Z', + releaseStatus: 'public', + ...avatar + }; + avatars.push(ref); + }); + } else { + throw new Error(`Error: ${response.data}`); + } + } catch (err) { + const msg = `Avatar lookup failed for ${authorId} with ${url}\n${err}`; + console.error(msg); + toast.error(msg); + } + return avatars; +} + +/** + * + * @param id + */ +export function selectAvatarWithConfirmation(id) { + const modalStore = useModalStore(); + const t = i18n.global.t; + + modalStore + .confirm({ + description: t('confirm.select_avatar'), + title: 'Confirm' + }) + .then(({ ok }) => { + if (!ok) return; + selectAvatarWithoutConfirmation(id); + }) + .catch(() => {}); +} + +/** + * + * @param id + */ +export async function selectAvatarWithoutConfirmation(id) { + const userStore = useUserStore(); + + if (userStore.currentUser.currentAvatar === id) { + toast.info('Avatar already selected'); + return; + } + return avatarRequest + .selectAvatar({ + avatarId: id + }) + .then(() => { + toast.success('Avatar changed'); + }); +} + +/** + * + * @param fileId + */ +export function checkAvatarCache(fileId) { + const avatarStore = useAvatarStore(); + + let avatarId = ''; + for (let ref of avatarStore.cachedAvatars.values()) { + if (extractFileId(ref.imageUrl) === fileId) { + avatarId = ref.id; + } + } + return avatarId; +} + +/** + * + * @param fileId + * @param ownerUserId + */ +export async function checkAvatarCacheRemote(fileId, ownerUserId) { + const advancedSettingsStore = useAdvancedSettingsStore(); + const avatarStore = useAvatarStore(); + const t = i18n.global.t; + + if (advancedSettingsStore.avatarRemoteDatabase) { + try { + toast.dismiss(avatarStore.loadingToastId); + avatarStore.loadingToastId = toast.loading( + t('message.avatar_lookup.loading') + ); + const avatarId = await lookupAvatarByImageFileId( + ownerUserId, + fileId + ); + return avatarId; + } catch (err) { + console.error('Failed to lookup avatar by image file id:', err); + } finally { + toast.dismiss(avatarStore.loadingToastId); + } + } + return null; +} + +/** + * + * @param refUserId + * @param ownerUserId + * @param currentAvatarImageUrl + */ +export async function showAvatarAuthorDialog( + refUserId, + ownerUserId, + currentAvatarImageUrl +) { + const userStore = useUserStore(); + const t = i18n.global.t; + + const fileId = extractFileId(currentAvatarImageUrl); + if (!fileId) { + toast.error(t('message.avatar_lookup.failed')); + } else if (refUserId === userStore.currentUser.id) { + showAvatarDialog(userStore.currentUser.currentAvatar); + } else { + let avatarId = checkAvatarCache(fileId); + let avatarInfo; + if (!avatarId) { + avatarInfo = await getAvatarName(currentAvatarImageUrl); + if (avatarInfo.ownerId === userStore.currentUser.id) { + await refreshUserDialogAvatars(fileId); + return; + } + } + if (!avatarId) { + avatarId = await checkAvatarCacheRemote(fileId, ownerUserId); + } + if (!avatarId) { + if (ownerUserId === refUserId) { + toast.warning( + t('message.avatar_lookup.private_or_not_found') + ); + } else { + toast.warning(t('message.avatar_lookup.not_found')); + showUserDialog(avatarInfo.ownerId); + } + } + if (avatarId) { + showAvatarDialog(avatarId); + } + } +} + +/** + * + * @param avatarId + */ +export function addAvatarWearTime(avatarId) { + const userStore = useUserStore(); + + if (!userStore.currentUser.$previousAvatarSwapTime || !avatarId) { + return; + } + const timeSpent = + Date.now() - userStore.currentUser.$previousAvatarSwapTime; + database.addAvatarTimeSpent(avatarId, timeSpent); +} + +/** + * Preload all own avatars into cache at startup for global search. + */ +export async function preloadOwnAvatars() { + const params = { + n: 50, + offset: 0, + sort: 'updated', + order: 'descending', + releaseStatus: 'all', + user: 'me' + }; + await processBulk({ + fn: avatarRequest.getAvatars, + N: -1, + params, + handle: (args) => { + for (const json of args.json) { + applyAvatar(json); + } + } + }); +} diff --git a/src/coordinators/favoriteCoordinator.js b/src/coordinators/favoriteCoordinator.js index bd008d37..355cce90 100644 --- a/src/coordinators/favoriteCoordinator.js +++ b/src/coordinators/favoriteCoordinator.js @@ -2,10 +2,12 @@ import { toast } from 'vue-sonner'; import { useFavoriteStore } from '../stores/favorite'; import { useAppearanceSettingsStore } from '../stores/settings/appearance'; import { useAvatarStore } from '../stores/avatar'; +import { applyAvatar } from './avatarCoordinator'; import { useFriendStore } from '../stores/friend'; import { useGeneralSettingsStore } from '../stores/settings/general'; import { useUserStore } from '../stores/user'; import { useWorldStore } from '../stores/world'; +import { applyWorld } from './worldCoordinator'; import { runUpdateFriendFlow } from './friendPresenceCoordinator'; import { avatarRequest, favoriteRequest, queryRequest } from '../api'; import { database } from '../service/database'; @@ -177,7 +179,7 @@ export function handleFavoriteWorldList(args) { if (json.id === '???') { continue; } - worldStore.applyWorld(json); + applyWorld(json); } } @@ -191,7 +193,7 @@ export function handleFavoriteAvatarList(args) { if (json.releaseStatus === 'hidden') { continue; } - avatarStore.applyAvatar(json); + applyAvatar(json); } } @@ -652,7 +654,7 @@ export async function getLocalWorldFavorites() { for (let i = 0; i < worldCache.length; ++i) { const ref = worldCache[i]; if (!worldStore.cachedWorlds.has(ref.id)) { - worldStore.applyWorld(ref); + applyWorld(ref); } } @@ -909,7 +911,7 @@ export async function getLocalAvatarFavorites() { for (let i = 0; i < avatarCache.length; ++i) { const ref = avatarCache[i]; if (!avatarStore.cachedAvatars.has(ref.id)) { - avatarStore.applyAvatar(ref); + applyAvatar(ref); } } diff --git a/src/coordinators/friendSyncCoordinator.js b/src/coordinators/friendSyncCoordinator.js index c07c9f49..79888cf8 100644 --- a/src/coordinators/friendSyncCoordinator.js +++ b/src/coordinators/friendSyncCoordinator.js @@ -7,6 +7,7 @@ import { useAuthStore } from '../stores/auth'; import { useFriendStore } from '../stores/friend'; import { useUpdateLoopStore } from '../stores/updateLoop'; import { useUserStore } from '../stores/user'; +import { getCurrentUser } from './userCoordinator'; import { watchState } from '../service/watchState'; import configRepository from '../service/config'; @@ -21,7 +22,7 @@ export async function runRefreshFriendsListFlow() { // If we just got user less then 2 min before code call, don't call it again if (updateLoopStore.nextCurrentUserRefresh < 300) { - await userStore.getCurrentUser(); + await getCurrentUser(); } await friendStore.refreshFriends(); reconnectWebSocket(); diff --git a/src/coordinators/gameCoordinator.js b/src/coordinators/gameCoordinator.js index 562922f4..73b6793b 100644 --- a/src/coordinators/gameCoordinator.js +++ b/src/coordinators/gameCoordinator.js @@ -7,6 +7,7 @@ import { import { database } from '../service/database'; import { useAdvancedSettingsStore } from '../stores/settings/advanced'; import { useAvatarStore } from '../stores/avatar'; +import { addAvatarWearTime } from './avatarCoordinator'; import { useGameLogStore } from '../stores/gameLog'; import { useGameStore } from '../stores/game'; import { useInstanceStore } from '../stores/instance'; @@ -47,7 +48,7 @@ export async function runGameRunningChangedFlow(isGameRunning) { runAutoVRChatCacheManagementFlow(); runCheckIfGameCrashedFlow(); updateLoopStore.setIpcTimeout(0); - avatarStore.addAvatarWearTime(userStore.currentUser.currentAvatar); + addAvatarWearTime(userStore.currentUser.currentAvatar); } runLastLocationResetFlow(); diff --git a/src/coordinators/groupCoordinator.js b/src/coordinators/groupCoordinator.js new file mode 100644 index 00000000..3ab9f00b --- /dev/null +++ b/src/coordinators/groupCoordinator.js @@ -0,0 +1,957 @@ +import { nextTick } from 'vue'; +import { toast } from 'vue-sonner'; +import { i18n } from '../plugin/i18n'; + +import { + convertFileUrlToImageUrl, + createDefaultGroupRef, + sanitizeEntityJson, + replaceBioSymbols +} from '../shared/utils'; +import { groupRequest, instanceRequest, queryRequest } from '../api'; +import { database } from '../service/database'; +import { groupDialogFilterOptions } from '../shared/constants/'; +import { patchGroupFromEvent } from '../queries'; +import { useGameStore } from '../stores/game'; +import { useInstanceStore } from '../stores/instance'; +import { useModalStore } from '../stores/modal'; +import { useNotificationStore } from '../stores/notification'; +import { useUiStore } from '../stores/ui'; +import { useUserStore } from '../stores/user'; +import { useGroupStore } from '../stores/group'; +import { watchState } from '../service/watchState'; + +import configRepository from '../service/config'; + +import * as workerTimers from 'worker-timers'; + +// ─── Internal helpers (not exported) ───────────────────────────────────────── + +/** + * @param ref + */ +function applyGroupLanguage(ref) { + const userStore = useUserStore(); + ref.$languages = []; + const { languages } = ref; + if (!languages) { + return; + } + for (const language of languages) { + const value = userStore.subsetOfLanguages[language]; + if (typeof value === 'undefined') { + continue; + } + ref.$languages.push({ + key: language, + value + }); + } +} + +// ─── Core entity application ───────────────────────────────────────────────── + +/** + * + * @param {object} json + * @returns {object} ref + */ +export function applyGroup(json) { + const groupStore = useGroupStore(); + const userStore = useUserStore(); + let ref = groupStore.cachedGroups.get(json.id); + sanitizeEntityJson(json, ['rules', 'name', 'description']); + if (typeof ref === 'undefined') { + ref = createDefaultGroupRef(json); + groupStore.cachedGroups.set(ref.id, ref); + } else { + if (groupStore.currentUserGroups.has(ref.id)) { + // compare group props + if ( + ref.ownerId && + json.ownerId && + ref.ownerId !== json.ownerId + ) { + // owner changed + groupOwnerChange(json, ref.ownerId, json.ownerId); + } + if (ref.name && json.name && ref.name !== json.name) { + // name changed + groupChange( + json, + `Name changed from ${ref.name} to ${json.name}` + ); + } + if (ref.myMember?.roleIds && json.myMember?.roleIds) { + const oldRoleIds = ref.myMember.roleIds; + const newRoleIds = json.myMember.roleIds; + if ( + oldRoleIds.length !== newRoleIds.length || + !oldRoleIds.every( + (value, index) => value === newRoleIds[index] + ) + ) { + // roleIds changed + groupRoleChange( + json, + ref.roles, + json.roles, + oldRoleIds, + newRoleIds + ); + } + } + } + if (json.myMember) { + if (typeof json.myMember.roleIds === 'undefined') { + // keep roleIds + json.myMember.roleIds = ref.myMember.roleIds; + } + Object.assign(ref.myMember, json.myMember); + } + Object.assign(ref, json); + } + // update myMember without fetching member + if (typeof json.memberVisibility !== 'undefined') { + ref.myMember.visibility = json.memberVisibility; + } + if (typeof json.isRepresenting !== 'undefined') { + ref.myMember.isRepresenting = json.isRepresenting; + } + if (typeof json.membershipStatus !== 'undefined') { + ref.myMember.membershipStatus = json.membershipStatus; + } + if (typeof json.roleIds !== 'undefined') { + ref.myMember.roleIds = json.roleIds; + } + ref.$url = `https://vrc.group/${ref.shortCode}.${ref.discriminator}`; + applyGroupLanguage(ref); + + const currentUserGroupRef = groupStore.currentUserGroups.get(ref.id); + if (currentUserGroupRef) { + groupStore.currentUserGroups.set(ref.id, ref); + } + + const D = groupStore.groupDialog; + if (D.visible && D.id === ref.id) { + D.inGroup = ref.membershipStatus === 'member'; + D.ref = ref; + } + patchGroupFromEvent(ref); + return ref; +} + +/** + * + * @param {object} json + * @returns {*} + */ +export function applyGroupMember(json) { + const userStore = useUserStore(); + const groupStore = useGroupStore(); + let ref; + if (typeof json?.user !== 'undefined') { + if (json.userId === userStore.currentUser.id) { + json.user = userStore.currentUser; + json.$displayName = userStore.currentUser.displayName; + } else { + ref = userStore.cachedUsers.get(json.user.id); + if (typeof ref !== 'undefined') { + json.user = ref; + json.$displayName = ref.displayName; + } else { + json.$displayName = json.user?.displayName; + } + } + } + // update myMember without fetching member + if (json?.userId === userStore.currentUser.id) { + ref = groupStore.cachedGroups.get(json.groupId); + if (typeof ref !== 'undefined') { + const newJson = { + id: json.groupId, + memberVisibility: json.visibility, + isRepresenting: json.isRepresenting, + isSubscribedToAnnouncements: + json.isSubscribedToAnnouncements, + joinedAt: json.joinedAt, + roleIds: json.roleIds, + membershipStatus: json.membershipStatus + }; + applyGroup(newJson); + } + } + + return json; +} + +// ─── Group change notifications ────────────────────────────────────────────── + +/** + * + * @param ref + * @param message + */ +function groupChange(ref, message) { + const groupStore = useGroupStore(); + const notificationStore = useNotificationStore(); + if (!groupStore.currentUserGroupsInit) { + return; + } + // oh the level of cursed for compibility + const json = { + id: Math.random().toString(36), + type: 'groupChange', + senderUserId: ref.id, + senderUsername: ref.name, + imageUrl: ref.iconUrl, + details: { + imageUrl: ref.iconUrl + }, + message, + created_at: new Date().toJSON() + }; + notificationStore.handleNotification({ + json, + params: { + notificationId: json.id + } + }); + + // delay to wait for json to be assigned to ref + workerTimers.setTimeout(() => saveCurrentUserGroups(), 100); +} + +/** + * + * @param {object }ref + * @param {string} oldUserId + * @param {string} newUserId + * @returns {Promise} + */ +async function groupOwnerChange(ref, oldUserId, newUserId) { + const oldUser = await queryRequest.fetch('user', { + userId: oldUserId + }); + const newUser = await queryRequest.fetch('user', { + userId: newUserId + }); + const oldDisplayName = oldUser?.ref?.displayName; + const newDisplayName = newUser?.ref?.displayName; + + groupChange( + ref, + `Owner changed from ${oldDisplayName} to ${newDisplayName}` + ); +} + +/** + * + * @param {object} ref + * @param {Array} oldRoles + * @param {Array} newRoles + * @param {Array} oldRoleIds + * @param {Array} newRoleIds + */ +function groupRoleChange(ref, oldRoles, newRoles, oldRoleIds, newRoleIds) { + // check for removed/added roleIds + for (const roleId of oldRoleIds) { + if (!newRoleIds.includes(roleId)) { + let roleName = ''; + const role = oldRoles.find( + (fineRole) => fineRole.id === roleId + ); + if (role) { + roleName = role.name; + } + groupChange(ref, `Role ${roleName} removed`); + } + } + if (typeof newRoles !== 'undefined') { + for (const roleId of newRoleIds) { + if (!oldRoleIds.includes(roleId)) { + let roleName = ''; + const role = newRoles.find( + (fineRole) => fineRole.id === roleId + ); + if (role) { + roleName = role.name; + } + groupChange(ref, `Role ${roleName} added`); + } + } + } +} + +// ─── Dialog flows ──────────────────────────────────────────────────────────── + +/** + * + * @param groupId + * @param options + */ +export function showGroupDialog(groupId, options = {}) { + const t = i18n.global.t; + const groupStore = useGroupStore(); + const uiStore = useUiStore(); + const instanceStore = useInstanceStore(); + if (!groupId) { + return; + } + const forceRefresh = Boolean(options?.forceRefresh); + const isMainDialogOpen = uiStore.openDialog({ + type: 'group', + id: groupId + }); + const D = groupStore.groupDialog; + D.visible = true; + if (isMainDialogOpen && D.id === groupId && !forceRefresh) { + uiStore.setDialogCrumbLabel('group', D.id, D.ref?.name || D.id); + instanceStore.applyGroupDialogInstances(); + D.loading = false; + return; + } + D.loading = true; + D.id = groupId; + D.inGroup = false; + D.ownerDisplayName = ''; + D.announcement = {}; + D.posts = []; + D.postsFiltered = []; + D.instances = []; + D.memberRoles = []; + D.lastVisit = ''; + D.memberSearch = ''; + D.memberSearchResults = []; + D.galleries = {}; + D.members = []; + D.memberFilter = groupDialogFilterOptions.everyone; + D.calendar = []; + const loadGroupRequest = groupRequest.getGroup({ + groupId, + includeRoles: true + }); + + loadGroupRequest + .catch((err) => { + D.loading = false; + D.id = null; + D.visible = false; + uiStore.jumpBackDialogCrumb(); + toast.error(t('message.group.load_failed')); + throw err; + }) + .then((args) => { + const ref = args.ref || applyGroup(args.json); + if (groupId === ref.id) { + D.ref = ref; + uiStore.setDialogCrumbLabel( + 'group', + D.id, + D.ref?.name || D.id + ); + D.inGroup = ref.membershipStatus === 'member'; + D.ownerDisplayName = ref.ownerId; + D.visible = true; + D.loading = false; + queryRequest + .fetch('user', { + userId: ref.ownerId + }) + .then((args1) => { + D.ownerDisplayName = args1.ref.displayName; + }); + database.getLastGroupVisit(D.ref.name).then((r) => { + if (D.id === ref.id) { + D.lastVisit = r.created_at; + } + }); + instanceStore.applyGroupDialogInstances(); + getGroupDialogGroup(groupId, ref); + } + }); +} + +/** + * + * @param groupId + * @param {object} [existingRef] + * @returns { Promise } + */ +export function getGroupDialogGroup(groupId, existingRef) { + const groupStore = useGroupStore(); + const instanceStore = useInstanceStore(); + const D = groupStore.groupDialog; + D.isGetGroupDialogGroupLoading = false; + + const refPromise = existingRef + ? Promise.resolve({ ref: existingRef }) + : queryRequest + .fetch('group', { groupId, includeRoles: true }) + .then((args) => ({ ref: applyGroup(args.json), args })); + + return refPromise + .catch((err) => { + throw err; + }) + .then((result) => { + const ref = result.ref; + if (D.id === ref.id) { + D.loading = false; + D.ref = ref; + D.inGroup = ref.membershipStatus === 'member'; + D.memberRoles = []; + for (const role of ref.roles) { + if ( + D.ref && + D.ref.myMember && + Array.isArray(D.ref.myMember.roleIds) && + D.ref.myMember.roleIds.includes(role.id) + ) { + D.memberRoles.push(role); + } + } + groupStore.getAllGroupPosts({ + groupId + }); + D.isGetGroupDialogGroupLoading = true; + groupRequest + .getGroupInstances({ + groupId + }) + .then((args) => { + if (groupStore.groupDialog.id === args.params.groupId) { + instanceStore.applyGroupDialogInstances( + args.json.instances + ); + } + for (const json of args.json.instances) { + instanceStore.applyInstance(json); + queryRequest + .fetch('world', { + worldId: json.world.id + }) + .then((args1) => { + json.world = args1.ref; + }); + // get queue size etc + instanceRequest.getInstance({ + worldId: json.worldId, + instanceId: json.instanceId + }); + } + }); + queryRequest + .fetch('groupCalendar', { groupId }) + .then((args) => { + if (groupStore.groupDialog.id === args.params.groupId) { + D.calendar = args.json.results; + for (const event of D.calendar) { + Object.assign(event, groupStore.applyGroupEvent(event)); + // fetch again for isFollowing + queryRequest + .fetch('groupCalendarEvent', { + groupId, + eventId: event.id + }) + .then((args) => { + Object.assign( + event, + groupStore.applyGroupEvent(args.json) + ); + }); + } + } + }); + } + nextTick(() => (D.isGetGroupDialogGroupLoading = false)); + return result.args || result; + }); +} + +// ─── Group lifecycle flows ─────────────────────────────────────────────────── + +/** + * + * @param {object} ref + */ +export function applyPresenceGroups(ref) { + const groupStore = useGroupStore(); + if (!groupStore.currentUserGroupsInit) { + // wait for init before diffing + return; + } + const groups = ref.presence?.groups; + if (!groups) { + console.error('applyPresenceGroups: invalid groups', ref); + return; + } + if (groups.length === 0) { + // as it turns out, this is not the most trust worthly source of info + return; + } + + // update group list + for (const groupId of groups) { + if (!groupStore.currentUserGroups.has(groupId)) { + onGroupJoined(groupId); + } + } + for (const groupId of groupStore.currentUserGroups.keys()) { + if (!groups.includes(groupId)) { + onGroupLeft(groupId); + } + } +} + +/** + * + * @param {string} groupId + */ +export function onGroupJoined(groupId) { + const groupStore = useGroupStore(); + if (!groupStore.currentUserGroups.has(groupId)) { + groupStore.currentUserGroups.set(groupId, { + id: groupId, + name: '', + iconUrl: '' + }); + groupRequest + .getGroup({ groupId, includeRoles: true }) + .then((args) => { + applyGroup(args.json); + saveCurrentUserGroups(); + return args; + }); + } +} + +/** + * + * @param {string} groupId + */ +export async function onGroupLeft(groupId) { + const groupStore = useGroupStore(); + const args = await groupRequest.getGroup({ groupId }); + const ref = applyGroup(args.json); + if (ref.membershipStatus === 'member') { + // wtf, not trusting presence + console.error( + `onGroupLeft: presence lied, still a member of ${groupId}` + ); + return; + } + if (groupStore.groupDialog.visible && groupStore.groupDialog.id === groupId) { + showGroupDialog(groupId); + } + if (groupStore.currentUserGroups.has(groupId)) { + groupStore.currentUserGroups.delete(groupId); + groupChange(ref, 'Left group'); + + // delay to wait for json to be assigned to ref + workerTimers.setTimeout(() => saveCurrentUserGroups(), 100); + } +} + +// ─── User group management ─────────────────────────────────────────────────── + +/** + * + */ +export function saveCurrentUserGroups() { + const groupStore = useGroupStore(); + const userStore = useUserStore(); + if (!groupStore.currentUserGroupsInit) { + return; + } + const groups = []; + for (const ref of groupStore.currentUserGroups.values()) { + groups.push({ + id: ref.id, + name: ref.name, + ownerId: ref.ownerId, + iconUrl: ref.iconUrl, + roles: ref.roles, + roleIds: ref.myMember?.roleIds + }); + } + configRepository.setString( + `VRCX_currentUserGroups_${userStore.currentUser.id}`, + JSON.stringify(groups) + ); +} + +/** + * + * @param userId + * @param groups + */ +export async function loadCurrentUserGroups(userId, groups) { + const groupStore = useGroupStore(); + const savedGroups = JSON.parse( + await configRepository.getString( + `VRCX_currentUserGroups_${userId}`, + '[]' + ) + ); + groupStore.cachedGroups.clear(); + groupStore.currentUserGroups.clear(); + for (const group of savedGroups) { + const json = { + id: group.id, + name: group.name, + iconUrl: group.iconUrl, + ownerId: group.ownerId, + roles: group.roles, + myMember: { + roleIds: group.roleIds + } + }; + const ref = applyGroup(json); + groupStore.currentUserGroups.set(group.id, ref); + } + + if (groups) { + const promises = groups.map(async (groupId) => { + const groupRef = groupStore.cachedGroups.get(groupId); + + if ( + typeof groupRef !== 'undefined' && + groupRef.roles?.length > 0 + ) { + return; + } + + try { + console.log(`Fetching group with missing roles ${groupId}`); + const args = await groupRequest.getGroup({ + groupId, + includeRoles: true + }); + const ref = applyGroup(args.json); + groupStore.currentUserGroups.set(groupId, ref); + } catch (err) { + console.error(err); + } + }); + + await Promise.allSettled(promises); + } + + groupStore.currentUserGroupsInit = true; + getCurrentUserGroups(); +} + +/** + * + */ +export async function getCurrentUserGroups() { + const groupStore = useGroupStore(); + const userStore = useUserStore(); + const args = await groupRequest.getGroups({ + userId: userStore.currentUser.id + }); + handleGroupList(args); + groupStore.currentUserGroups.clear(); + for (const group of args.json) { + const ref = applyGroup(group); + if (!groupStore.currentUserGroups.has(group.id)) { + groupStore.currentUserGroups.set(group.id, ref); + } + } + const args1 = await groupRequest.getGroupPermissions({ + userId: userStore.currentUser.id + }); + handleGroupPermissions(args1); + saveCurrentUserGroups(); +} + +/** + * + */ +export function getCurrentUserRepresentedGroup() { + const userStore = useUserStore(); + return groupRequest + .getRepresentedGroup({ + userId: userStore.currentUser.id + }) + .then((args) => { + handleGroupRepresented(args); + return args; + }); +} + +/** + * + */ +export async function initUserGroups() { + const userStore = useUserStore(); + updateInGameGroupOrder(); + loadCurrentUserGroups( + userStore.currentUser.id, + userStore.currentUser?.presence?.groups + ); +} + +/** + * + */ +export async function updateInGameGroupOrder() { + const groupStore = useGroupStore(); + const gameStore = useGameStore(); + const userStore = useUserStore(); + groupStore.inGameGroupOrder = []; + try { + const json = await gameStore.getVRChatRegistryKey( + `VRC_GROUP_ORDER_${userStore.currentUser.id}` + ); + if (!json) { + return; + } + groupStore.inGameGroupOrder = JSON.parse(json); + } catch (err) { + console.error(err); + } +} + +// ─── Group actions ─────────────────────────────────────────────────────────── + +/** + * + * @param groupId + */ +export function leaveGroup(groupId) { + const groupStore = useGroupStore(); + const userStore = useUserStore(); + groupRequest + .leaveGroup({ + groupId + }) + .then((args) => { + const groupId = args.params.groupId; + if ( + groupStore.groupDialog.visible && + groupStore.groupDialog.id === groupId + ) { + groupStore.groupDialog.inGroup = false; + getGroupDialogGroup(groupId); + } + if ( + userStore.userDialog.visible && + userStore.userDialog.id === userStore.currentUser.id && + userStore.userDialog.representedGroup.id === groupId + ) { + getCurrentUserRepresentedGroup(); + } + }); +} + +/** + * + * @param groupId + */ +export function leaveGroupPrompt(groupId) { + const t = i18n.global.t; + const modalStore = useModalStore(); + modalStore + .confirm({ + description: t('confirm.leave_group'), + title: t('confirm.title') + }) + .then(({ ok }) => { + if (!ok) return; + leaveGroup(groupId); + }) + .catch(() => {}); +} + +/** + * + * @param groupId + * @param visibility + */ +export function setGroupVisibility(groupId, visibility) { + const t = i18n.global.t; + const userStore = useUserStore(); + return groupRequest + .setGroupMemberProps(userStore.currentUser.id, groupId, { + visibility + }) + .then((args) => { + handleGroupMemberProps(args); + toast.success(t('message.group.visibility_updated')); + return args; + }); +} + +/** + * + * @param groupId + * @param subscribe + */ +export function setGroupSubscription(groupId, subscribe) { + const t = i18n.global.t; + const userStore = useUserStore(); + return groupRequest + .setGroupMemberProps(userStore.currentUser.id, groupId, { + isSubscribedToAnnouncements: subscribe + }) + .then((args) => { + handleGroupMemberProps(args); + toast.success(t('message.group.subscription_updated')); + return args; + }); +} + +// ─── Event handlers ────────────────────────────────────────────────────────── + +/** + * + * @param args + */ +export function handleGroupRepresented(args) { + const userStore = useUserStore(); + const D = userStore.userDialog; + const json = args.json; + D.representedGroup = json; + D.representedGroup.$thumbnailUrl = convertFileUrlToImageUrl( + json.iconUrl + ); + if (!json || !json.isRepresenting) { + D.isRepresentedGroupLoading = false; + } + if (!json.groupId) { + // no group + return; + } + if (args.params.userId !== userStore.currentUser.id) { + // not current user, don't apply someone elses myMember + return; + } + json.$memberId = json.id; + json.id = json.groupId; + applyGroup(json); +} + +/** + * + * @param args + */ +export function handleGroupList(args) { + for (const json of args.json) { + json.$memberId = json.id; + json.id = json.groupId; + applyGroup(json); + } +} + +/** + * + * @param args + */ +export function handleGroupMemberProps(args) { + const groupStore = useGroupStore(); + const userStore = useUserStore(); + if (args.userId === userStore.currentUser.id) { + const json = args.json; + json.$memberId = json.id; + json.id = json.groupId; + if ( + groupStore.groupDialog.visible && + groupStore.groupDialog.id === json.groupId + ) { + groupStore.groupDialog.ref.myMember.visibility = json.visibility; + groupStore.groupDialog.ref.myMember.isSubscribedToAnnouncements = + json.isSubscribedToAnnouncements; + } + if ( + userStore.userDialog.visible && + userStore.userDialog.id === userStore.currentUser.id + ) { + getCurrentUserRepresentedGroup(); + } + handleGroupMember({ + json, + params: { + groupId: json.groupId + } + }); + } + let member; + if (groupStore.groupDialog.id === args.json.groupId) { + let i; + for (i = 0; i < groupStore.groupDialog.members.length; ++i) { + member = groupStore.groupDialog.members[i]; + if (member.userId === args.json.userId) { + Object.assign(member, applyGroupMember(args.json)); + break; + } + } + for (i = 0; i < groupStore.groupDialog.memberSearchResults.length; ++i) { + member = groupStore.groupDialog.memberSearchResults[i]; + if (member.userId === args.json.userId) { + Object.assign(member, applyGroupMember(args.json)); + break; + } + } + } +} + +/** + * + * @param args + */ +export function handleGroupPermissions(args) { + const groupStore = useGroupStore(); + const userStore = useUserStore(); + if (args.params.userId !== userStore.currentUser.id) { + return; + } + const json = args.json; + for (const groupId in json) { + const permissions = json[groupId]; + const group = groupStore.cachedGroups.get(groupId); + if (group) { + group.myMember.permissions = permissions; + } + } +} + +/** + * + * @param args + */ +export function handleGroupMember(args) { + args.ref = applyGroupMember(args.json); +} + +/** + * + * @param args + */ +export async function handleGroupUserInstances(args) { + const groupStore = useGroupStore(); + const instanceStore = useInstanceStore(); + groupStore.groupInstances = []; + for (const json of args.json.instances) { + if (args.json.fetchedAt) { + // tack on fetchedAt + json.$fetchedAt = args.json.fetchedAt; + } + const instanceRef = instanceStore.applyInstance(json); + const groupRef = groupStore.cachedGroups.get(json.ownerId); + if (typeof groupRef === 'undefined') { + if (watchState.isFriendsLoaded) { + const args = await groupRequest.getGroup({ + groupId: json.ownerId + }); + applyGroup(args.json); + } + return; + } + groupStore.groupInstances.push({ + group: groupRef, + instance: instanceRef + }); + } +} diff --git a/src/coordinators/inviteCoordinator.js b/src/coordinators/inviteCoordinator.js index f13c5dc5..78697bc3 100644 --- a/src/coordinators/inviteCoordinator.js +++ b/src/coordinators/inviteCoordinator.js @@ -1,5 +1,5 @@ import { toast } from 'vue-sonner'; -import { useI18n } from 'vue-i18n'; +import { i18n } from '../plugin/i18n'; import { instanceRequest } from '../api'; import { parseLocation } from '../shared/utils'; @@ -16,7 +16,7 @@ export function runNewInstanceSelfInviteFlow(worldId) { const instanceStore = useInstanceStore(); const launchStore = useLaunchStore(); const inviteStore = useInviteStore(); - const { t } = useI18n(); + const t = i18n.global.t; instanceStore.createNewInstance(worldId).then((args) => { const location = args?.json?.location; diff --git a/src/coordinators/moderationCoordinator.js b/src/coordinators/moderationCoordinator.js index ddb7248c..b4cff93c 100644 --- a/src/coordinators/moderationCoordinator.js +++ b/src/coordinators/moderationCoordinator.js @@ -19,9 +19,6 @@ export async function runRefreshPlayerModerationsFlow() { playerModerationRequest.getPlayerModerations(), avatarModerationRequest.getAvatarModerations() ]) - .finally(() => { - moderationStore.playerModerationTable.loading = false; - }) .then((res) => { avatarStore.resetCachedAvatarModerations(); if (res[1]?.json) { @@ -41,5 +38,8 @@ export async function runRefreshPlayerModerationsFlow() { 'Failed to load player/avatar moderations:', error ); + }) + .finally(() => { + moderationStore.playerModerationTable.loading = false; }); } diff --git a/src/coordinators/userCoordinator.js b/src/coordinators/userCoordinator.js new file mode 100644 index 00000000..3abf82ef --- /dev/null +++ b/src/coordinators/userCoordinator.js @@ -0,0 +1,1070 @@ +import { reactive } from 'vue'; +import { toast } from 'vue-sonner'; +import { i18n } from '../plugin/i18n'; + +import { + arraysMatch, + computeUserPlatform, + createDefaultUserRef, + diffObjectProps, + evictMapCache, + extractFileId, + findUserByDisplayName, + getUserMemo, + getWorldName, + isRealInstance, + parseLocation, + sanitizeUserJson +} from '../shared/utils'; +import { + avatarRequest, + instanceRequest, + queryRequest, + userRequest +} from '../api'; +import { processBulk, request } from '../service/request'; +import { AppDebug } from '../service/appConfig'; +import { database } from '../service/database'; +import { patchUserFromEvent } from '../queries'; +import { watchState } from '../service/watchState'; +import { applyAvatar, showAvatarDialog } from './avatarCoordinator'; +import { applyFavorite } from './favoriteCoordinator'; +import { + runAvatarSwapFlow, + runFirstLoginFlow, + runHomeLocationSyncFlow, + runPostApplySyncFlow +} from './userSessionCoordinator'; +import { runHandleUserUpdateFlow } from './userEventCoordinator'; +import { runUpdateCurrentUserLocationFlow } from './locationCoordinator'; +import { runUpdateFriendFlow } from './friendPresenceCoordinator'; +import { handleGroupRepresented } from './groupCoordinator'; +import { useAppearanceSettingsStore } from '../stores/settings/appearance'; +import { useAuthStore } from '../stores/auth'; +import { useAvatarStore } from '../stores/avatar'; +import { useFavoriteStore } from '../stores/favorite'; +import { useFriendStore } from '../stores/friend'; +import { useGameStore } from '../stores/game'; +import { useGeneralSettingsStore } from '../stores/settings/general'; +import { useInstanceStore } from '../stores/instance'; +import { useLocationStore } from '../stores/location'; +import { useModerationStore } from '../stores/moderation'; +import { useNotificationStore } from '../stores/notification'; +import { usePhotonStore } from '../stores/photon'; +import { useSearchStore } from '../stores/search'; +import { useSharedFeedStore } from '../stores/sharedFeed'; +import { useUiStore } from '../stores/ui'; +import { useUserStore } from '../stores/user'; + +const robotUrl = `${AppDebug.endpointDomain}/file/file_0e8c4e32-7444-44ea-ade4-313c010d4bae/1/file`; + +/** + * @param {import('../types/api/user').GetUserResponse} json + * @returns {import('../types/api/user').VrcxUser} + */ +export function applyUser(json) { + const userStore = useUserStore(); + const appearanceSettingsStore = useAppearanceSettingsStore(); + const friendStore = useFriendStore(); + const locationStore = useLocationStore(); + const instanceStore = useInstanceStore(); + const moderationStore = useModerationStore(); + const photonStore = usePhotonStore(); + + const { currentUser, cachedUsers, currentTravelers, customUserTags, state, userDialog } = userStore; + + let ref = cachedUsers.get(json.id); + let hasPropChanged = false; + let changedProps = {}; + sanitizeUserJson(json, robotUrl); + if (typeof ref === 'undefined') { + ref = reactive(createDefaultUserRef(json)); + if (locationStore.lastLocation.playerList.has(json.id)) { + const player = locationStore.lastLocation.playerList.get(json.id); + ref.$location_at = player.joinTime; + ref.$online_for = player.joinTime; + } + if (ref.isFriend || ref.id === currentUser.id) { + let newCount = state.instancePlayerCount.get(ref.location); + if (typeof newCount === 'undefined') { + newCount = 0; + } + newCount++; + state.instancePlayerCount.set(ref.location, newCount); + } + const tag = customUserTags.get(json.id); + if (tag) { + ref.$customTag = tag.tag; + ref.$customTagColour = tag.colour; + } else if (ref.$customTag) { + ref.$customTag = ''; + ref.$customTagColour = ''; + } + evictMapCache( + cachedUsers, + friendStore.friends.size + 300, + (_value, key) => friendStore.friends.has(key), + { logLabel: 'User cache cleanup' } + ); + cachedUsers.set(ref.id, ref); + runUpdateFriendFlow(ref.id); + } else { + if (json.state !== 'online') { + runUpdateFriendFlow(ref.id, json.state); + } + const { + hasPropChanged: _hasPropChanged, + changedProps: _changedProps + } = diffObjectProps(ref, json, arraysMatch); + for (const prop in json) { + if (typeof json[prop] !== 'undefined') { + ref[prop] = json[prop]; + } + } + hasPropChanged = _hasPropChanged; + changedProps = _changedProps; + } + ref.$moderations = moderationStore.getUserModerations(ref.id); + ref.$isVRCPlus = ref.tags.includes('system_supporter'); + appearanceSettingsStore.applyUserTrustLevel(ref); + userStore.applyUserLanguage(ref); + ref.$platform = computeUserPlatform(ref.platform, ref.last_platform); + // traveling + if (ref.location === 'traveling') { + ref.$location = parseLocation(ref.travelingToLocation); + if (!currentTravelers.has(ref.id) && ref.travelingToLocation) { + const travelRef = reactive({ + created_at: new Date().toJSON(), + ...ref + }); + currentTravelers.set(ref.id, travelRef); + onPlayerTraveling(travelRef); + } + } else { + ref.$location = parseLocation(ref.location); + currentTravelers.delete(ref.id); + } + if ( + !instanceStore.cachedInstances.has(ref.$location.tag) && + isRealInstance(ref.location) + ) { + instanceRequest.getInstance({ + worldId: ref.$location.worldId, + instanceId: ref.$location.instanceId + }); + } + if ( + ref.$isVRCPlus && + ref.badges && + ref.badges.every( + (x) => x.badgeId !== 'bdg_754f9935-0f97-49d8-b857-95afb9b673fa' + ) + ) { + ref.badges.unshift({ + badgeId: 'bdg_754f9935-0f97-49d8-b857-95afb9b673fa', + badgeName: 'Supporter', + badgeDescription: 'Supports VRChat through VRC+', + badgeImageUrl: + 'https://assets.vrchat.com/badges/fa/bdgai_583f6b13-91ab-4e1b-974e-ab91600b06cb.png', + hidden: true, + showcased: false + }); + } + const friendCtx = friendStore.friends.get(ref.id); + if (friendCtx) { + friendCtx.ref = ref; + friendCtx.name = ref.displayName; + } + if (ref.id === currentUser.id) { + if (ref.status) { + currentUser.status = ref.status; + } + runUpdateCurrentUserLocationFlow(); + } + // add user ref to playerList, friendList, photonLobby, photonLobbyCurrent + const playerListRef = locationStore.lastLocation.playerList.get(ref.id); + if (playerListRef) { + if ( + !locationStore.lastLocation.friendList.has(ref.id) && + friendStore.friends.has(ref.id) + ) { + const userMap = { + displayName: ref.displayName, + userId: ref.id, + joinTime: playerListRef.joinTime + }; + locationStore.lastLocation.friendList.set(ref.id, userMap); + } + if ( + locationStore.lastLocation.friendList.has(ref.id) && + !friendStore.friends.has(ref.id) + ) { + locationStore.lastLocation.friendList.delete(ref.id); + } + photonStore.photonLobby.forEach((ref1, id) => { + if ( + typeof ref1 !== 'undefined' && + ref1.displayName === ref.displayName && + ref1 !== ref + ) { + photonStore.photonLobby.set(id, ref); + if (photonStore.photonLobbyCurrent.has(id)) { + photonStore.photonLobbyCurrent.set(id, ref); + } + } + }); + instanceStore.getCurrentInstanceUserList(); + } + if (ref.state === 'online') { + runUpdateFriendFlow(ref.id, ref.state); + } + applyFavorite('friend', ref.id); + friendStore.userOnFriend(ref); + const D = userDialog; + if (D.visible && D.id === ref.id) { + D.ref = ref; + D.note = String(ref.note || ''); + D.incomingRequest = false; + D.outgoingRequest = false; + if (D.ref.friendRequestStatus === 'incoming') { + D.incomingRequest = true; + } else if (D.ref.friendRequestStatus === 'outgoing') { + D.outgoingRequest = true; + } + } + if (hasPropChanged) { + if ( + changedProps.location && + changedProps.location[0] !== 'traveling' + ) { + const ts = Date.now(); + changedProps.location.push(ts - ref.$location_at); + ref.$location_at = ts; + } + handleUserUpdate(ref, changedProps); + if (AppDebug.debugUserDiff) { + delete changedProps.last_login; + delete changedProps.last_activity; + if (Object.keys(changedProps).length !== 0) { + console.log('>', ref.displayName, changedProps); + } + } + } + patchUserFromEvent(ref); + return ref; +} + +/** + * @param {string} userId + */ +export function showUserDialog(userId) { + if ( + !userId || + typeof userId !== 'string' || + userId === 'usr_00000000-0000-0000-0000-000000000000' + ) { + return; + } + const userStore = useUserStore(); + const uiStore = useUiStore(); + const friendStore = useFriendStore(); + const moderationStore = useModerationStore(); + const favoriteStore = useFavoriteStore(); + const locationStore = useLocationStore(); + const searchStore = useSearchStore(); + const appearanceSettingsStore = useAppearanceSettingsStore(); + const t = i18n.global.t; + + const { currentUser, userDialog, showUserDialogHistory } = userStore; + + const isMainDialogOpen = uiStore.openDialog({ + type: 'user', + id: userId + }); + const D = userDialog; + D.visible = true; + if (isMainDialogOpen && D.id === userId) { + uiStore.setDialogCrumbLabel( + 'user', + D.id, + D.ref?.displayName || D.id + ); + userStore.applyUserDialogLocation(true); + return; + } + D.id = userId; + D.memo = ''; + D.note = ''; + getUserMemo(userId).then((memo) => { + if (memo.userId === userId) { + D.memo = memo.memo; + const ref = friendStore.friends.get(userId); + if (ref) { + ref.memo = String(memo.memo || ''); + if (memo.memo) { + ref.$nickName = memo.memo.split('\n')[0]; + } else { + ref.$nickName = ''; + } + } + } + }); + + D.loading = true; + D.avatars = []; + D.worlds = []; + D.instance = { + id: '', + tag: '', + $location: {}, + friendCount: 0, + users: [], + shortName: '', + ref: {} + }; + D.isRepresentedGroupLoading = true; + D.representedGroup = { + bannerId: '', + bannerUrl: '', + description: '', + discriminator: '', + groupId: '', + id: '', + iconUrl: '', + isRepresenting: false, + memberCount: 0, + memberVisibility: '', + name: '', + ownerId: '', + privacy: '', + shortCode: '', + $thumbnailUrl: '', + $memberId: '' + }; + D.lastSeen = ''; + D.joinCount = 0; + D.timeSpent = 0; + D.avatarModeration = 0; + D.isHideAvatar = false; + D.isShowAvatar = false; + D.previousDisplayNames = []; + D.dateFriended = ''; + D.unFriended = false; + D.dateFriendedInfo = []; + D.mutualFriendCount = 0; + D.mutualGroupCount = 0; + if (userId === currentUser.id) { + getWorldName(currentUser.homeLocation).then((worldName) => { + D.$homeLocationName = worldName; + }); + } + AppApi.SendIpc('ShowUserDialog', userId); + queryRequest + .fetch('user', { + userId + }) + .catch((err) => { + D.loading = false; + D.id = null; + D.visible = false; + uiStore.jumpBackDialogCrumb(); + toast.error(t('message.user.load_failed')); + throw err; + }) + .then((args) => { + if (args.ref.id === D.id) { + D.loading = false; + + D.ref = args.ref; + uiStore.setDialogCrumbLabel( + 'user', + D.id, + D.ref?.displayName || D.id + ); + D.friend = friendStore.friends.get(D.id); + D.isFriend = Boolean(D.friend); + D.note = String(D.ref.note || ''); + D.incomingRequest = false; + D.outgoingRequest = false; + D.isBlock = false; + D.isMute = false; + D.isInteractOff = false; + D.isMuteChat = false; + for (const ref of moderationStore.cachedPlayerModerations.values()) { + if ( + ref.targetUserId === D.id && + ref.sourceUserId === currentUser.id + ) { + if (ref.type === 'block') { + D.isBlock = true; + } else if (ref.type === 'mute') { + D.isMute = true; + } else if (ref.type === 'interactOff') { + D.isInteractOff = true; + } else if (ref.type === 'muteChat') { + D.isMuteChat = true; + } + } + } + D.isFavorite = + favoriteStore.getCachedFavoritesByObjectId(D.id) || + favoriteStore.isInAnyLocalFriendGroup(D.id); + if (D.ref.friendRequestStatus === 'incoming') { + D.incomingRequest = true; + } else if (D.ref.friendRequestStatus === 'outgoing') { + D.outgoingRequest = true; + } + let inCurrentWorld = false; + if (locationStore.lastLocation.playerList.has(D.ref.id)) { + inCurrentWorld = true; + } + if (userId !== currentUser.id) { + database + .getUserStats(D.ref, inCurrentWorld) + .then(async (ref1) => { + if (ref1.userId === D.id) { + D.lastSeen = ref1.lastSeen; + D.joinCount = ref1.joinCount; + D.timeSpent = ref1.timeSpent; + } + const displayNameMap = + ref1.previousDisplayNames; + const userNotifications = + await database.getFriendLogHistoryForUserId( + D.id, + ['DisplayName', 'Friend', 'Unfriend'] + ); + const dateFriendedInfo = []; + for (const notification of userNotifications) { + if (notification.userId !== D.id) { + continue; + } + if (notification.type === 'DisplayName') { + displayNameMap.set( + notification.previousDisplayName, + notification.created_at + ); + } + if ( + notification.type === 'Friend' || + (notification.type === 'Unfriend' && + !appearanceSettingsStore.hideUnfriends) + ) { + dateFriendedInfo.unshift(notification); + } + } + D.dateFriendedInfo = dateFriendedInfo; + if (dateFriendedInfo.length > 0) { + const latestFriendedInfo = + dateFriendedInfo[0]; + D.unFriended = + latestFriendedInfo.type === 'Unfriend'; + D.dateFriended = + latestFriendedInfo.created_at; + } + displayNameMap.forEach( + (updated_at, displayName) => { + D.previousDisplayNames.push({ + displayName, + updated_at + }); + } + ); + }); + AppApi.GetVRChatUserModeration( + currentUser.id, + userId + ).then((result) => { + D.avatarModeration = result; + if (result === 4) { + D.isHideAvatar = true; + } else if (result === 5) { + D.isShowAvatar = true; + } + }); + if (!currentUser.hasSharedConnectionsOptOut) { + try { + queryRequest + .fetch('mutualCounts', { userId }) + .then((args) => { + if (args.params.userId === D.id) { + D.mutualFriendCount = + args.json.friends; + D.mutualGroupCount = + args.json.groups; + } + }); + } catch (error) { + console.error(error); + } + } + } else { + D.previousDisplayNames = + currentUser.pastDisplayNames; + database + .getUserStats(D.ref, inCurrentWorld) + .then((ref1) => { + if (ref1.userId === D.id) { + D.lastSeen = ref1.lastSeen; + D.joinCount = ref1.joinCount; + D.timeSpent = ref1.timeSpent; + } + }); + } + queryRequest + .fetch('representedGroup', { userId }) + .then((args1) => { + handleGroupRepresented(args1); + }); + D.visible = true; + userStore.applyUserDialogLocation(true); + } + }); + showUserDialogHistory.delete(userId); + showUserDialogHistory.add(userId); + searchStore.setQuickSearchItems(searchStore.quickSearchUserHistory()); +} + +/** + * @param {object} ref + */ +function onPlayerTraveling(ref) { + const userStore = useUserStore(); + const gameStore = useGameStore(); + const locationStore = useLocationStore(); + const notificationStore = useNotificationStore(); + + if ( + !gameStore.isGameRunning || + !locationStore.lastLocation.location || + locationStore.lastLocation.location !== ref.travelingToLocation || + ref.id === userStore.currentUser.id || + locationStore.lastLocation.playerList.has(ref.id) + ) { + return; + } + + const onPlayerJoining = { + created_at: new Date(ref.created_at).toJSON(), + userId: ref.id, + displayName: ref.displayName, + type: 'OnPlayerJoining' + }; + notificationStore.queueFeedNoty(onPlayerJoining); +} + +/** + * @param {object} ref + * @param {object} props + */ +async function handleUserUpdate(ref, props) { + await runHandleUserUpdateFlow(ref, props); +} + +/** + * @param fileId + */ +export async function refreshUserDialogAvatars(fileId) { + const userStore = useUserStore(); + const avatarStore = useAvatarStore(); + const t = i18n.global.t; + + const D = userStore.userDialog; + const userId = D.id; + if (D.isAvatarsLoading) { + return; + } + D.isAvatarsLoading = true; + if (fileId) { + D.loading = true; + } + D.avatarSorting = 'update'; + D.avatarReleaseStatus = 'all'; + const params = { + n: 50, + offset: 0, + sort: 'updated', + order: 'descending', + releaseStatus: 'all', + user: 'me' + }; + for (const ref of avatarStore.cachedAvatars.values()) { + if (ref.authorId === D.id) { + avatarStore.cachedAvatars.delete(ref.id); + } + } + const map = new Map(); + await processBulk({ + fn: avatarRequest.getAvatars, + N: -1, + params, + handle: (args) => { + for (const json of args.json) { + const ref = applyAvatar(json); + map.set(ref.id, ref); + } + }, + done: () => { + const array = Array.from(map.values()); + if (userId === D.id) { + userStore.sortUserDialogAvatars(array); + } + D.isAvatarsLoading = false; + if (fileId) { + D.loading = false; + for (const ref of array) { + if (extractFileId(ref.imageUrl) === fileId) { + showAvatarDialog(ref.id); + return; + } + } + toast.error('Own avatar not found'); + } + } + }); +} + +/** + * @param ref + */ +export async function lookupUser(ref) { + const userStore = useUserStore(); + const searchStore = useSearchStore(); + + let ctx; + if (ref.userId) { + showUserDialog(ref.userId); + return; + } + if (!ref.displayName || ref.displayName.substring(0, 3) === 'ID:') { + return; + } + const found = findUserByDisplayName(userStore.cachedUsers, ref.displayName); + if (found) { + showUserDialog(found.id); + return; + } + searchStore.setSearchText(ref.displayName); + await searchStore.searchUserByDisplayName(ref.displayName); + for (ctx of searchStore.searchUserResults) { + if (ctx.displayName === ref.displayName) { + searchStore.setSearchText(''); + searchStore.clearSearch(); + showUserDialog(ctx.id); + return; + } + } +} + +/** + * @param {object} args + */ +export function handleConfig(args) { + const authStore = useAuthStore(); + const userStore = useUserStore(); + + const ref = { + ...args.json + }; + args.ref = ref; + authStore.setCachedConfig(ref); + if (typeof args.ref?.whiteListedAssetUrls !== 'object') { + console.error('Invalid config whiteListedAssetUrls'); + } + AppApi.PopulateImageHosts( + JSON.stringify(args.ref.whiteListedAssetUrls) + ); + const languages = + args.ref?.constants?.LANGUAGE?.SPOKEN_LANGUAGE_OPTIONS; + if (!languages) { + return; + } + userStore.subsetOfLanguages = languages; + const data = []; + for (const key in languages) { + const value = languages[key]; + data.push({ + key, + value + }); + } + userStore.languageDialog.languages = data; +} + +/** + * @param {import('../types/api/user').GetCurrentUserResponse} json + * @returns {import('../types/api/user').GetCurrentUserResponse} + */ +export function applyCurrentUser(json) { + const userStore = useUserStore(); + const appearanceSettingsStore = useAppearanceSettingsStore(); + const authStore = useAuthStore(); + const gameStore = useGameStore(); + const locationStore = useLocationStore(); + + authStore.setAttemptingAutoLogin(false); + let ref = userStore.currentUser; + runAvatarSwapFlow({ + json, + ref, + isLoggedIn: watchState.isLoggedIn + }); + if (watchState.isLoggedIn) { + for (const prop in json) { + if (typeof json[prop] !== 'undefined') { + ref[prop] = json[prop]; + } + } + } else { + ref = { + acceptedPrivacyVersion: 0, + acceptedTOSVersion: 0, + accountDeletionDate: null, + accountDeletionLog: null, + activeFriends: [], + ageVerificationStatus: '', + ageVerified: false, + allowAvatarCopying: false, + badges: [], + bio: '', + bioLinks: [], + currentAvatar: '', + currentAvatarImageUrl: '', + currentAvatarTags: [], + currentAvatarThumbnailImageUrl: '', + date_joined: '', + developerType: '', + discordDetails: { + global_name: '', + id: '' + }, + discordId: '', + displayName: '', + emailVerified: false, + fallbackAvatar: '', + friendGroupNames: [], + friendKey: '', + friends: [], + googleId: '', + hasBirthday: false, + hasDiscordFriendsOptOut: false, + hasEmail: false, + hasLoggedInFromClient: false, + hasPendingEmail: false, + hasSharedConnectionsOptOut: false, + hideContentFilterSettings: false, + homeLocation: '', + id: '', + isAdult: true, + isBoopingEnabled: false, + isFriend: false, + last_activity: '', + last_login: '', + last_mobile: null, + last_platform: '', + obfuscatedEmail: '', + obfuscatedPendingEmail: '', + oculusId: '', + offlineFriends: [], + onlineFriends: [], + pastDisplayNames: [], + picoId: '', + presence: { + avatarThumbnail: '', + currentAvatarTags: '', + debugflag: '', + displayName: '', + groups: [], + id: '', + instance: '', + instanceType: '', + platform: '', + profilePicOverride: '', + status: '', + travelingToInstance: '', + travelingToWorld: '', + userIcon: '', + world: '', + ...json.presence + }, + profilePicOverride: '', + profilePicOverrideThumbnail: '', + pronouns: '', + queuedInstance: '', + state: '', + status: '', + statusDescription: '', + statusFirstTime: false, + statusHistory: [], + steamDetails: {}, + steamId: '', + tags: [], + twoFactorAuthEnabled: false, + twoFactorAuthEnabledDate: null, + unsubscribe: false, + updated_at: '', + userIcon: '', + userLanguage: '', + userLanguageCode: '', + username: '', + viveId: '', + // VRCX + $online_for: null, + $offline_for: null, + $location_at: Date.now(), + $travelingToTime: Date.now(), + $previousAvatarSwapTime: null, + $homeLocation: {}, + $isVRCPlus: false, + $isModerator: false, + $isTroll: false, + $isProbableTroll: false, + $trustLevel: 'Visitor', + $trustClass: 'x-tag-untrusted', + $userColour: '', + $trustSortNum: 1, + $languages: [], + $locationTag: '', + $travelingToLocation: '', + ...json + }; + runFirstLoginFlow(ref); + } + + ref.$isVRCPlus = ref.tags.includes('system_supporter'); + appearanceSettingsStore.applyUserTrustLevel(ref); + userStore.applyUserLanguage(ref); + userStore.applyPresenceLocation(ref); + runPostApplySyncFlow(ref); + runHomeLocationSyncFlow(ref); + + // when isGameRunning use gameLog instead of API + const $location = parseLocation(locationStore.lastLocation.location); + const $travelingLocation = parseLocation( + locationStore.lastLocationDestination + ); + let location = locationStore.lastLocation.location; + let instanceId = $location.instanceId; + let worldId = $location.worldId; + let travelingToLocation = locationStore.lastLocationDestination; + let travelingToWorld = $travelingLocation.worldId; + let travelingToInstance = $travelingLocation.instanceId; + if (!gameStore.isGameRunning && json.presence) { + if (isRealInstance(json.presence.world)) { + location = `${json.presence.world}:${json.presence.instance}`; + } else { + location = json.presence.world; + } + if (isRealInstance(json.presence.travelingToWorld)) { + travelingToLocation = `${json.presence.travelingToWorld}:${json.presence.travelingToInstance}`; + } else { + travelingToLocation = json.presence.travelingToWorld; + } + instanceId = json.presence.instance; + worldId = json.presence.world; + travelingToInstance = json.presence.travelingToInstance; + travelingToWorld = json.presence.travelingToWorld; + } + const userRef = applyUser({ + ageVerificationStatus: json.ageVerificationStatus, + ageVerified: json.ageVerified, + allowAvatarCopying: json.allowAvatarCopying, + badges: json.badges, + bio: json.bio, + bioLinks: json.bioLinks, + currentAvatarImageUrl: json.currentAvatarImageUrl, + currentAvatarTags: json.currentAvatarTags, + currentAvatarThumbnailImageUrl: json.currentAvatarThumbnailImageUrl, + date_joined: json.date_joined, + developerType: json.developerType, + discordId: json.discordId, + displayName: json.displayName, + friendKey: json.friendKey, + id: json.id, + isFriend: json.isFriend, + last_activity: json.last_activity, + last_login: json.last_login, + last_mobile: json.last_mobile, + last_platform: json.last_platform, + profilePicOverride: json.profilePicOverride, + profilePicOverrideThumbnail: json.profilePicOverrideThumbnail, + pronouns: json.pronouns, + state: json.state, + status: json.status, + statusDescription: json.statusDescription, + tags: json.tags, + userIcon: json.userIcon, + location, + instanceId, + worldId, + travelingToLocation, + travelingToInstance, + travelingToWorld + }); + // set VRCX online/offline timers + userRef.$online_for = userStore.currentUser.$online_for; + userRef.$offline_for = userStore.currentUser.$offline_for; + userRef.$location_at = userStore.currentUser.$location_at; + userRef.$travelingToTime = userStore.currentUser.$travelingToTime; + if (json.presence?.platform) { + userRef.platform = json.presence.platform; + } + + return ref; +} + +/** + */ +export function getCurrentUser() { + const authStore = useAuthStore(); + return request('auth/user', { + method: 'GET' + }).then((json) => { + const args = { + json + }; + authStore.handleCurrentUserUpdate(json); + return args; + }); +} + +/** + * @param data + */ +export function addCustomTag(data) { + const userStore = useUserStore(); + const sharedFeedStore = useSharedFeedStore(); + + if (data.Tag) { + userStore.customUserTags.set(data.UserId, { + tag: data.Tag, + colour: data.TagColour + }); + } else { + userStore.customUserTags.delete(data.UserId); + } + const feedUpdate = { + userId: data.UserId, + colour: data.TagColour + }; + AppApi.ExecuteVrOverlayFunction( + 'updateHudFeedTag', + JSON.stringify(feedUpdate) + ); + const ref = userStore.cachedUsers.get(data.UserId); + if (typeof ref !== 'undefined') { + ref.$customTag = data.Tag; + ref.$customTagColour = data.TagColour; + } + sharedFeedStore.addTag(data.UserId, data.TagColour); +} + +/** + */ +export function updateAutoStateChange() { + const userStore = useUserStore(); + const generalSettingsStore = useGeneralSettingsStore(); + const gameStore = useGameStore(); + const locationStore = useLocationStore(); + const favoriteStore = useFavoriteStore(); + + if ( + !generalSettingsStore.autoStateChangeEnabled || + !gameStore.isGameRunning || + !locationStore.lastLocation.playerList.size || + locationStore.lastLocation.location === '' || + locationStore.lastLocation.location === 'traveling' + ) { + return; + } + + const $location = parseLocation(locationStore.lastLocation.location); + let instanceType = $location.accessType; + if (instanceType === 'group') { + if ($location.groupAccessType === 'members') { + instanceType = 'groupOnly'; + } else if ($location.groupAccessType === 'plus') { + instanceType = 'groupPlus'; + } else { + instanceType = 'groupPublic'; + } + } + if ( + generalSettingsStore.autoStateChangeInstanceTypes.length > 0 && + !generalSettingsStore.autoStateChangeInstanceTypes.includes( + instanceType + ) + ) { + return; + } + + let withCompany = locationStore.lastLocation.playerList.size > 1; + if (generalSettingsStore.autoStateChangeNoFriends) { + const selectedGroups = generalSettingsStore.autoStateChangeGroups; + if (selectedGroups.length > 0) { + const groupFriendIds = new Set(); + for (const ref of favoriteStore.cachedFavorites.values()) { + if ( + ref.type === 'friend' && + selectedGroups.includes(ref.$groupKey) + ) { + groupFriendIds.add(ref.favoriteId); + } + } + for (const selectedKey of selectedGroups) { + if (selectedKey.startsWith('local:')) { + const groupName = selectedKey.slice(6); + const userIds = + favoriteStore.localFriendFavorites[groupName]; + if (userIds) { + for (let i = 0; i < userIds.length; ++i) { + groupFriendIds.add(userIds[i]); + } + } + } + } + withCompany = false; + for (const friendId of locationStore.lastLocation.friendList.keys()) { + if (groupFriendIds.has(friendId)) { + withCompany = true; + break; + } + } + } else { + withCompany = locationStore.lastLocation.friendList.size >= 1; + } + } + + const currentStatus = userStore.currentUser.status; + const newStatus = withCompany + ? generalSettingsStore.autoStateChangeCompanyStatus + : generalSettingsStore.autoStateChangeAloneStatus; + + if (currentStatus === newStatus) { + return; + } + + const params = { status: newStatus }; + if ( + withCompany && + generalSettingsStore.autoStateChangeCompanyDescEnabled + ) { + params.statusDescription = + generalSettingsStore.autoStateChangeCompanyDesc; + } else if ( + !withCompany && + generalSettingsStore.autoStateChangeAloneDescEnabled + ) { + params.statusDescription = + generalSettingsStore.autoStateChangeAloneDesc; + } + + userRequest.saveCurrentUser(params).then(() => { + const text = `Status automatically changed to ${newStatus}`; + if (AppDebug.errorNoty) { + toast.dismiss(AppDebug.errorNoty); + } + AppDebug.errorNoty = toast.info(text); + console.log(text); + }); +} diff --git a/src/coordinators/userEventCoordinator.js b/src/coordinators/userEventCoordinator.js index 3d4cba42..b29c8c7b 100644 --- a/src/coordinators/userEventCoordinator.js +++ b/src/coordinators/userEventCoordinator.js @@ -2,6 +2,7 @@ import { getGroupName, getWorldName, parseLocation } from '../shared/utils'; import { AppDebug } from '../service/appConfig'; import { database } from '../service/database'; import { useAvatarStore } from '../stores/avatar'; +import { getAvatarName } from './avatarCoordinator'; import { useFeedStore } from '../stores/feed'; import { useFriendStore } from '../stores/friend'; import { useGeneralSettingsStore } from '../stores/settings/general'; @@ -222,7 +223,7 @@ export async function runHandleUserUpdateFlow( avatarName: '' }; try { - avatarInfo = await avatarStore.getAvatarName( + avatarInfo = await getAvatarName( currentAvatarImageUrl ); } catch (err) { @@ -233,7 +234,7 @@ export async function runHandleUserUpdateFlow( avatarName: '' }; try { - previousAvatarInfo = await avatarStore.getAvatarName( + previousAvatarInfo = await getAvatarName( previousCurrentAvatarImageUrl ); } catch (err) { diff --git a/src/coordinators/userSessionCoordinator.js b/src/coordinators/userSessionCoordinator.js index 377c3ac0..56aa30cb 100644 --- a/src/coordinators/userSessionCoordinator.js +++ b/src/coordinators/userSessionCoordinator.js @@ -2,9 +2,11 @@ import { getWorldName, parseLocation } from '../shared/utils'; import { runUpdateFriendshipsFlow } from './friendRelationshipCoordinator'; import { useAuthStore } from '../stores/auth'; import { useAvatarStore } from '../stores/avatar'; +import { addAvatarToHistory, addAvatarWearTime } from './avatarCoordinator'; import { useFriendStore } from '../stores/friend'; import { useGameStore } from '../stores/game'; import { useGroupStore } from '../stores/group'; +import { applyPresenceGroups } from './groupCoordinator'; import { useInstanceStore } from '../stores/instance'; import { useUserStore } from '../stores/user'; @@ -28,9 +30,9 @@ export function runAvatarSwapFlow( return; } if (json.currentAvatar !== ref.currentAvatar) { - avatarStore.addAvatarToHistory(json.currentAvatar); + addAvatarToHistory(json.currentAvatar); if (gameStore.isGameRunning) { - avatarStore.addAvatarWearTime(ref.currentAvatar); + addAvatarWearTime(ref.currentAvatar); ref.$previousAvatarSwapTime = now(); } } @@ -64,7 +66,7 @@ export function runPostApplySyncFlow(ref) { const instanceStore = useInstanceStore(); const friendStore = useFriendStore(); - groupStore.applyPresenceGroups(ref); + applyPresenceGroups(ref); instanceStore.applyQueuedInstance(ref.queuedInstance); friendStore.updateUserCurrentStatus(ref); if (typeof ref.friends !== 'undefined') { diff --git a/src/coordinators/worldCoordinator.js b/src/coordinators/worldCoordinator.js new file mode 100644 index 00000000..a7894bc9 --- /dev/null +++ b/src/coordinators/worldCoordinator.js @@ -0,0 +1,246 @@ +import { nextTick } from 'vue'; +import { toast } from 'vue-sonner'; +import { i18n } from '../plugin/i18n'; + +import { + checkVRChatCache, + createDefaultWorldRef, + evictMapCache, + getAvailablePlatforms, + getBundleDateSize, + getWorldMemo, + isRealInstance, + parseLocation, + sanitizeEntityJson +} from '../shared/utils'; +import { instanceRequest, queryRequest, worldRequest } from '../api'; +import { database } from '../service/database'; +import { patchWorldFromEvent } from '../queries'; +import { processBulk } from '../service/request'; +import { applyFavorite } from './favoriteCoordinator'; +import { useFavoriteStore } from '../stores/favorite'; +import { useInstanceStore } from '../stores/instance'; +import { useLocationStore } from '../stores/location'; +import { useUiStore } from '../stores/ui'; +import { useUserStore } from '../stores/user'; +import { useWorldStore } from '../stores/world'; + +/** + * @param {string} tag + * @param {string} shortName + * @param options + */ +export function showWorldDialog(tag, shortName = null, options = {}) { + const worldStore = useWorldStore(); + const uiStore = useUiStore(); + const instanceStore = useInstanceStore(); + const favoriteStore = useFavoriteStore(); + const locationStore = useLocationStore(); + const t = i18n.global.t; + + const D = worldStore.worldDialog; + const forceRefresh = Boolean(options?.forceRefresh); + const L = parseLocation(tag); + if (L.worldId === '') { + return; + } + const isMainDialogOpen = uiStore.openDialog({ + type: 'world', + id: L.worldId, + tag, + shortName + }); + D.visible = true; + if (isMainDialogOpen && D.id === L.worldId && !forceRefresh) { + uiStore.setDialogCrumbLabel('world', D.id, D.ref?.name || D.id); + instanceStore.applyWorldDialogInstances(); + nextTick(() => (D.loading = false)); + return; + } + L.shortName = shortName; + D.id = L.worldId; + D.$location = L; + D.loading = true; + D.inCache = false; + D.cacheSize = ''; + D.cacheLocked = false; + D.cachePath = ''; + D.fileAnalysis = {}; + D.rooms = []; + D.lastVisit = ''; + D.visitCount = 0; + D.timeSpent = 0; + D.isFavorite = false; + D.avatarScalingDisabled = false; + D.focusViewDisabled = false; + D.isPC = false; + D.isQuest = false; + D.isIos = false; + D.hasPersistData = false; + D.memo = ''; + const LL = parseLocation(locationStore.lastLocation.location); + let currentWorldMatch = false; + if (LL.worldId === D.id) { + currentWorldMatch = true; + } + getWorldMemo(D.id).then((memo) => { + if (memo.worldId === D.id) { + D.memo = memo.memo; + } + }); + database.getLastVisit(D.id, currentWorldMatch).then((ref) => { + if (ref.worldId === D.id) { + D.lastVisit = ref.created_at; + } + }); + database.getVisitCount(D.id).then((ref) => { + if (ref.worldId === D.id) { + D.visitCount = ref.visitCount; + } + }); + database.getTimeSpentInWorld(D.id).then((ref) => { + if (ref.worldId === D.id) { + D.timeSpent = ref.timeSpent; + } + }); + const loadWorldRequest = worldRequest.getWorld({ + worldId: L.worldId + }); + loadWorldRequest + .catch((err) => { + nextTick(() => (D.loading = false)); + D.id = null; + D.visible = false; + uiStore.jumpBackDialogCrumb(); + toast.error(t('message.world.load_failed')); + throw err; + }) + .then((args) => { + if (D.id === args.ref.id) { + D.ref = args.ref; + uiStore.setDialogCrumbLabel( + 'world', + D.id, + D.ref?.name || D.id + ); + D.visible = true; + D.loading = false; + D.isFavorite = favoriteStore.getCachedFavoritesByObjectId( + D.id + ); + if (!D.isFavorite) { + D.isFavorite = + favoriteStore.localWorldFavoritesList.includes( + D.id + ); + } + let { isPC, isQuest, isIos } = getAvailablePlatforms( + args.ref.unityPackages + ); + D.avatarScalingDisabled = args.ref?.tags.includes( + 'feature_avatar_scaling_disabled' + ); + D.focusViewDisabled = args.ref?.tags.includes( + 'feature_focus_view_disabled' + ); + D.isPC = isPC; + D.isQuest = isQuest; + D.isIos = isIos; + worldStore.updateVRChatWorldCache(); + queryRequest + .fetch('worldPersistData', { + worldId: D.id + }) + .then((args) => { + if ( + args.params.worldId === worldStore.worldDialog.id && + worldStore.worldDialog.visible + ) { + worldStore.worldDialog.hasPersistData = + args.json !== false; + } + }); + } + }); +} + +/** + * @param {object} json + * @returns {object} ref + */ +export function applyWorld(json) { + const worldStore = useWorldStore(); + const favoriteStore = useFavoriteStore(); + const instanceStore = useInstanceStore(); + const userStore = useUserStore(); + + sanitizeEntityJson(json, ['name', 'description']); + let ref = worldStore.cachedWorlds.get(json.id); + if (typeof ref === 'undefined') { + ref = createDefaultWorldRef(json); + evictMapCache(worldStore.cachedWorlds, 10000, () => false, { + logLabel: 'World cache cleanup' + }); + worldStore.cachedWorlds.set(ref.id, ref); + } else { + Object.assign(ref, json); + } + ref.$isLabs = ref.tags.includes('system_labs'); + applyFavorite('world', ref.id); + const userDialog = userStore.userDialog; + if (userDialog.visible && userDialog.$location.worldId === ref.id) { + userStore.applyUserDialogLocation(); + } + const worldDialog = worldStore.worldDialog; + if (worldDialog.visible && worldDialog.id === ref.id) { + worldDialog.ref = ref; + worldDialog.avatarScalingDisabled = ref.tags?.includes( + 'feature_avatar_scaling_disabled' + ); + worldDialog.focusViewDisabled = ref.tags?.includes( + 'feature_focus_view_disabled' + ); + instanceStore.applyWorldDialogInstances(); + for (const room of worldDialog.rooms) { + if (isRealInstance(room.tag)) { + instanceRequest.getInstance({ + worldId: worldDialog.id, + instanceId: room.id + }); + } + } + if (Object.keys(worldDialog.fileAnalysis).length === 0) { + getBundleDateSize(ref); + } + } + if (favoriteStore.localWorldFavoritesList.includes(ref.id)) { + // update db cache + database.addWorldToCache(ref); + } + patchWorldFromEvent(ref); + return ref; +} + +/** + * Preload all own worlds into cache at startup for global search. + */ +export async function preloadOwnWorlds() { + const params = { + n: 50, + offset: 0, + sort: 'updated', + order: 'descending', + releaseStatus: 'all', + user: 'me' + }; + await processBulk({ + fn: (p) => worldRequest.getWorlds(p), + N: -1, + params, + handle: (args) => { + for (const json of args.json) { + applyWorld(json); + } + } + }); +} diff --git a/src/service/request.js b/src/service/request.js index 5ac6607f..7bb20aea 100644 --- a/src/service/request.js +++ b/src/service/request.js @@ -7,6 +7,7 @@ import { useUpdateLoopStore, useUserStore } from '../stores'; +import { getCurrentUser } from '../coordinators/userCoordinator'; import { AppDebug } from './appConfig.js'; import { escapeTag } from '../shared/utils'; import { i18n } from '../plugin/i18n'; @@ -207,7 +208,7 @@ export function request(endpoint, options) { ) { // trigger 2FA dialog } if (!authStore.twoFactorAuthDialogVisible) { - userStore.getCurrentUser(); + getCurrentUser(); } $throw(401, t('api.status_code.401'), endpoint); } diff --git a/src/service/websocket.js b/src/service/websocket.js index dfe68fe2..d4130511 100644 --- a/src/service/websocket.js +++ b/src/service/websocket.js @@ -12,6 +12,13 @@ import { useUiStore, useUserStore } from '../stores'; +import { applyUser, applyCurrentUser } from '../coordinators/userCoordinator'; +import { + onGroupLeft, + applyGroup, + getGroupDialogGroup, + handleGroupMember +} from '../coordinators/groupCoordinator'; import { escapeTag, parseLocation } from '../shared/utils'; import { AppDebug } from './appConfig'; import { groupRequest } from '../api'; @@ -260,7 +267,7 @@ function handlePipeline(args) { break; case 'friend-add': - userStore.applyUser(content.user); + applyUser(content.user); friendStore.handleFriendAdd({ params: { userId: content.userId @@ -298,7 +305,7 @@ function handlePipeline(args) { ...content.user }; - userStore.applyUser(onlineJson); + applyUser(onlineJson); } else { console.error('friend-online missing user id', content); runUpdateFriendFlow(content.userId, 'online'); @@ -321,7 +328,7 @@ function handlePipeline(args) { ...content.user }; - userStore.applyUser(activeJson); + applyUser(activeJson); } else { console.error('friend-active missing user id', content); runUpdateFriendFlow(content.userId, 'active'); @@ -342,11 +349,11 @@ function handlePipeline(args) { travelingToWorld: 'offline', travelingToInstance: 'offline' }; - userStore.applyUser(offlineJson); + applyUser(offlineJson); break; case 'friend-update': - userStore.applyUser(content.user); + applyUser(content.user); break; case 'friend-location': @@ -365,7 +372,7 @@ function handlePipeline(args) { travelingToWorld: $travelingToLocation1.worldId, travelingToInstance: $travelingToLocation1.instanceId }; - userStore.applyUser(jankLocationJson); + applyUser(jankLocationJson); break; } const locationJson = { @@ -378,12 +385,12 @@ function handlePipeline(args) { ...content.user, state: 'online' // JANK }; - userStore.applyUser(locationJson); + applyUser(locationJson); break; case 'user-update': - userStore.applyCurrentUser(content.user); + applyCurrentUser(content.user); break; case 'user-location': @@ -410,14 +417,14 @@ function handlePipeline(args) { break; case 'group-left': - groupStore.onGroupLeft(content.groupId); + onGroupLeft(content.groupId); break; case 'group-role-updated': const groupId = content.role.groupId; groupRequest .getGroup({ groupId, includeRoles: true }) - .then((args) => groupStore.applyGroup(args.json)); + .then((args) => applyGroup(args.json)); console.log('group-role-updated', content); // content { @@ -446,9 +453,9 @@ function handlePipeline(args) { groupStore.groupDialog.visible && groupStore.groupDialog.id === groupId1 ) { - groupStore.getGroupDialogGroup(groupId1); + getGroupDialogGroup(groupId1); } - groupStore.handleGroupMember({ + handleGroupMember({ json: member, params: { groupId: groupId1 diff --git a/src/stores/auth.js b/src/stores/auth.js index f955af8c..a70d5b31 100644 --- a/src/stores/auth.js +++ b/src/stores/auth.js @@ -16,6 +16,7 @@ import { escapeTag } from '../shared/utils'; import { initWebsocket } from '../service/websocket'; import { request } from '../service/request'; import { runHandleAutoLoginFlow } from '../coordinators/authAutoLoginCoordinator'; +import { getCurrentUser } from '../coordinators/userCoordinator'; import { useAdvancedSettingsStore } from './settings/advanced'; import { useGeneralSettingsStore } from './settings/general'; import { useModalStore } from './modal'; @@ -197,8 +198,7 @@ export const useAuthStore = defineStore('Auth', () => { throw err; }) .then(() => { - userStore - .getCurrentUser() + getCurrentUser() .finally(() => { loginForm.value.loading = false; }) @@ -671,7 +671,7 @@ export const useAuthStore = defineStore('Auth', () => { clearCookiesTryLogin(); }) .then(() => { - userStore.getCurrentUser(); + getCurrentUser(); }); }) .catch(() => { @@ -713,7 +713,7 @@ export const useAuthStore = defineStore('Auth', () => { clearCookiesTryLogin(); }) .then(() => { - userStore.getCurrentUser(); + getCurrentUser(); }); }) .catch(() => { @@ -756,7 +756,7 @@ export const useAuthStore = defineStore('Auth', () => { promptEmailOTP(); }) .then(() => { - userStore.getCurrentUser(); + getCurrentUser(); }); }) .catch(() => { diff --git a/src/stores/avatar.js b/src/stores/avatar.js index c20bb227..09b20546 100644 --- a/src/stores/avatar.js +++ b/src/stores/avatar.js @@ -1,46 +1,13 @@ -import { nextTick, ref, watch } from 'vue'; +import { ref, watch } from 'vue'; import { defineStore } from 'pinia'; -import { toast } from 'vue-sonner'; -import { useI18n } from 'vue-i18n'; import { - checkVRChatCache, - createDefaultAvatarRef, - extractFileId, - getAvailablePlatforms, - getBundleDateSize, - getPlatformInfo, - replaceBioSymbols, - sanitizeEntityJson, - storeAvatarImage + checkVRChatCache } from '../shared/utils'; -import { avatarRequest, miscRequest, queryRequest } from '../api'; -import { AppDebug } from '../service/appConfig'; import { database } from '../service/database'; -import { patchAvatarFromEvent } from '../queries'; -import { processBulk } from '../service/request'; -import { applyFavorite } from '../coordinators/favoriteCoordinator'; -import { useAdvancedSettingsStore } from './settings/advanced'; -import { useAvatarProviderStore } from './avatarProvider'; -import { useFavoriteStore } from './favorite'; -import { useModalStore } from './modal'; -import { useUiStore } from './ui'; -import { useUserStore } from './user'; -import { useVRCXUpdaterStore } from './vrcxUpdater'; import { watchState } from '../service/watchState'; -import webApiService from '../service/webapi'; - export const useAvatarStore = defineStore('Avatar', () => { - const favoriteStore = useFavoriteStore(); - const avatarProviderStore = useAvatarProviderStore(); - const vrcxUpdaterStore = useVRCXUpdaterStore(); - const advancedSettingsStore = useAdvancedSettingsStore(); - const userStore = useUserStore(); - const modalStore = useModalStore(); - const uiStore = useUiStore(); - const { t } = useI18n(); - let cachedAvatarModerations = new Map(); let cachedAvatars = new Map(); let cachedAvatarNames = new Map(); @@ -83,145 +50,17 @@ export const useAvatarStore = defineStore('Avatar', () => { cachedAvatarModerations.clear(); avatarHistory.value = []; if (isLoggedIn) { - getAvatarHistory(); - preloadOwnAvatars(); + import('../coordinators/avatarCoordinator').then( + ({ getAvatarHistory, preloadOwnAvatars }) => { + getAvatarHistory(); + preloadOwnAvatars(); + } + ); } }, { flush: 'sync' } ); - /** - * @param {object} json - * @returns {object} ref - */ - function applyAvatar(json) { - sanitizeEntityJson(json, ['name', 'description']); - let ref = cachedAvatars.get(json.id); - if (typeof ref === 'undefined') { - ref = createDefaultAvatarRef(json); - cachedAvatars.set(ref.id, ref); - } else { - const { unityPackages } = ref; - Object.assign(ref, json); - if ( - json.unityPackages?.length > 0 && - unityPackages.length > 0 && - !json.unityPackages[0].assetUrl - ) { - ref.unityPackages = unityPackages; - } - } - for (const listing of ref.publishedListings) { - listing.displayName = replaceBioSymbols(listing.displayName); - listing.description = replaceBioSymbols(listing.description); - } - applyFavorite('avatar', ref.id); - if (favoriteStore.localAvatarFavoritesList.includes(ref.id)) { - const avatarRef = ref; - favoriteStore.syncLocalAvatarFavoriteRef(avatarRef); - - // update db cache - database.addAvatarToCache(avatarRef); - } - patchAvatarFromEvent(ref); - return ref; - } - - /** - * - * @param {string} avatarId - * @param options - * @returns - */ - function showAvatarDialog(avatarId, options = {}) { - const D = avatarDialog.value; - const forceRefresh = Boolean(options?.forceRefresh); - const isMainDialogOpen = uiStore.openDialog({ - type: 'avatar', - id: avatarId - }); - D.visible = true; - if (isMainDialogOpen && D.id === avatarId && !forceRefresh) { - uiStore.setDialogCrumbLabel('avatar', D.id, D.ref?.name || D.id); - nextTick(() => (D.loading = false)); - return; - } - D.loading = true; - D.id = avatarId; - D.inCache = false; - D.cacheSize = ''; - D.cacheLocked = false; - D.cachePath = ''; - D.fileAnalysis = {}; - D.isQuestFallback = false; - D.isPC = false; - D.isQuest = false; - D.isIos = false; - D.hasImposter = false; - D.imposterVersion = ''; - D.platformInfo = {}; - D.galleryImages = []; - D.galleryLoading = true; - D.isFavorite = - favoriteStore.getCachedFavoritesByObjectId(avatarId) || - (userStore.isLocalUserVrcPlusSupporter && - favoriteStore.localAvatarFavoritesList.includes(avatarId)); - D.isBlocked = cachedAvatarModerations.has(avatarId); - const ref2 = cachedAvatars.get(avatarId); - if (typeof ref2 !== 'undefined') { - D.ref = ref2; - uiStore.setDialogCrumbLabel('avatar', D.id, D.ref?.name || D.id); - nextTick(() => (D.loading = false)); - } - const loadAvatarRequest = forceRefresh - ? avatarRequest.getAvatar({ avatarId }) - : queryRequest.fetch('avatar', { avatarId }); - loadAvatarRequest - .then((args) => { - const ref = applyAvatar(args.json); - D.ref = ref; - uiStore.setDialogCrumbLabel( - 'avatar', - D.id, - D.ref?.name || D.id - ); - getAvatarGallery(avatarId); - updateVRChatAvatarCache(); - if (/quest/.test(ref.tags)) { - D.isQuestFallback = true; - } - const { isPC, isQuest, isIos } = getAvailablePlatforms( - ref.unityPackages - ); - D.isPC = isPC; - D.isQuest = isQuest; - D.isIos = isIos; - D.platformInfo = getPlatformInfo(ref.unityPackages); - for (let i = ref.unityPackages.length - 1; i > -1; i--) { - const unityPackage = ref.unityPackages[i]; - if (unityPackage.variant === 'impostor') { - D.hasImposter = true; - D.imposterVersion = unityPackage.impostorizerVersion; - break; - } - } - if (Object.keys(D.fileAnalysis).length === 0) { - getBundleDateSize(ref); - } - }) - .catch((err) => { - D.loading = false; - D.id = null; - D.visible = false; - uiStore.jumpBackDialogCrumb(); - toast.error(t('message.api_handler.avatar_private_or_deleted')); - throw err; - }) - .finally(() => { - nextTick(() => (D.loading = false)); - }); - } - /** * @param {boolean} value */ @@ -249,6 +88,7 @@ export const useAvatarStore = defineStore('Avatar', () => { * @returns {Promise} */ async function getAvatarGallery(avatarId) { + const { queryRequest } = await import('../api'); const D = avatarDialog.value; const args = await queryRequest .fetch('avatarGallery', { avatarId }) @@ -341,54 +181,6 @@ export const useAvatarStore = defineStore('Avatar', () => { } } - /** - * - * @returns {Promise} - */ - async function getAvatarHistory() { - const historyArray = await database.getAvatarHistory( - userStore.currentUser.id - ); - for (let i = 0; i < historyArray.length; i++) { - const avatar = historyArray[i]; - if (avatar.authorId === userStore.currentUser.id) { - continue; - } - applyAvatar(avatar); - } - avatarHistory.value = historyArray; - } - - /** - * @param {string} avatarId - */ - function addAvatarToHistory(avatarId) { - avatarRequest - .getAvatar({ avatarId }) - .then((args) => { - const ref = applyAvatar(args.json); - - database.addAvatarToCache(ref); - database.addAvatarToHistory(ref.id); - - if (ref.authorId === userStore.currentUser.id) { - return; - } - - const historyArray = avatarHistory.value; - for (let i = 0; i < historyArray.length; ++i) { - if (historyArray[i].id === ref.id) { - historyArray.splice(i, 1); - } - } - - avatarHistory.value.unshift(ref); - }) - .catch((err) => { - console.error('Failed to add avatar to history:', err); - }); - } - /** * */ @@ -397,423 +189,21 @@ export const useAvatarStore = defineStore('Avatar', () => { database.clearAvatarHistory(); } - /** - * - */ - function promptClearAvatarHistory() { - modalStore - .confirm({ - description: t('confirm.clear_avatar_history'), - title: 'Confirm' - }) - .then(({ ok }) => { - if (!ok) return; - clearAvatarHistory(); - }) - .catch(() => {}); - } - - /** - * - * @param {string} imageUrl - * @returns {Promise} - */ - async function getAvatarName(imageUrl) { - const fileId = extractFileId(imageUrl); - if (!fileId) { - return { - ownerId: '', - avatarName: '-' - }; - } - if (cachedAvatarNames.has(fileId)) { - return cachedAvatarNames.get(fileId); - } - try { - const args = await miscRequest.getFile({ fileId }); - return storeAvatarImage(args, cachedAvatarNames); - } catch (error) { - console.error('Failed to get avatar images:', error); - return { - ownerId: '', - avatarName: '-' - }; - } - } - - /** - * - * @param type - * @param search - */ - async function lookupAvatars(type, search) { - const avatars = new Map(); - if (type === 'search') { - try { - const url = `${ - avatarProviderStore.avatarRemoteDatabaseProvider - }?${type}=${encodeURIComponent(search)}&n=5000`; - const response = await webApiService.execute({ - url, - method: 'GET', - headers: { - Referer: 'https://vrcx.app', - 'VRCX-ID': vrcxUpdaterStore.vrcxId - } - }); - const json = JSON.parse(response.data); - if (AppDebug.debugWebRequests) { - console.log(url, json, response); - } - if (response.status === 200 && typeof json === 'object') { - json.forEach((avatar) => { - if (!avatars.has(avatar.Id)) { - const ref = { - authorId: '', - authorName: '', - name: '', - description: '', - id: '', - imageUrl: '', - thumbnailImageUrl: '', - created_at: '0001-01-01T00:00:00.0000000Z', - updated_at: '0001-01-01T00:00:00.0000000Z', - releaseStatus: 'public', - ...avatar - }; - avatars.set(ref.id, ref); - } - }); - } else { - throw new Error(`Error: ${response.data}`); - } - } catch (err) { - const msg = `Avatar search failed for ${search} with ${avatarProviderStore.avatarRemoteDatabaseProvider}\n${err}`; - console.error(msg); - toast.error(msg); - } - } else if (type === 'authorId') { - const length = - avatarProviderStore.avatarRemoteDatabaseProviderList.length; - for (let i = 0; i < length; ++i) { - const url = - avatarProviderStore.avatarRemoteDatabaseProviderList[i]; - const avatarArray = await lookupAvatarsByAuthor(url, search); - avatarArray.forEach((avatar) => { - if (!avatars.has(avatar.id)) { - avatars.set(avatar.id, avatar); - } - }); - } - } - return avatars; - } - - /** - * - * @param authorId - * @param fileId - */ - async function lookupAvatarByImageFileId(authorId, fileId) { - for (const providerUrl of avatarProviderStore.avatarRemoteDatabaseProviderList) { - const avatar = await lookupAvatarByFileId(providerUrl, fileId); - if (avatar?.id) { - return avatar.id; - } - } - - for (const providerUrl of avatarProviderStore.avatarRemoteDatabaseProviderList) { - const avatarArray = await lookupAvatarsByAuthor( - providerUrl, - authorId - ); - for (const avatar of avatarArray) { - if (extractFileId(avatar.imageUrl) === fileId) { - return avatar.id; - } - } - } - return null; - } - - /** - * - * @param providerUrl - * @param fileId - */ - async function lookupAvatarByFileId(providerUrl, fileId) { - try { - const url = `${providerUrl}?fileId=${encodeURIComponent(fileId)}`; - const response = await webApiService.execute({ - url, - method: 'GET', - headers: { - Referer: 'https://vrcx.app', - 'VRCX-ID': vrcxUpdaterStore.vrcxId - } - }); - const json = JSON.parse(response.data); - if (AppDebug.debugWebRequests) { - console.log(url, json, response); - } - if (response.status === 200 && typeof json === 'object') { - const ref = { - authorId: '', - authorName: '', - name: '', - description: '', - id: '', - imageUrl: '', - thumbnailImageUrl: '', - created_at: '0001-01-01T00:00:00.0000000Z', - updated_at: '0001-01-01T00:00:00.0000000Z', - releaseStatus: 'public', - ...json - }; - return ref; - } else { - return null; - } - } catch (err) { - // ignore errors for now, not all providers support this lookup type - return null; - } - } - - /** - * - * @param providerUrl - * @param authorId - */ - async function lookupAvatarsByAuthor(providerUrl, authorId) { - const avatars = []; - if (!providerUrl || !authorId) { - return avatars; - } - const url = `${providerUrl}?authorId=${encodeURIComponent(authorId)}`; - try { - const response = await webApiService.execute({ - url, - method: 'GET', - headers: { - Referer: 'https://vrcx.app', - 'VRCX-ID': vrcxUpdaterStore.vrcxId - } - }); - const json = JSON.parse(response.data); - if (AppDebug.debugWebRequests) { - console.log(url, json, response); - } - if (response.status === 200 && typeof json === 'object') { - json.forEach((avatar) => { - const ref = { - authorId: '', - authorName: '', - name: '', - description: '', - id: '', - imageUrl: '', - thumbnailImageUrl: '', - created_at: '0001-01-01T00:00:00.0000000Z', - updated_at: '0001-01-01T00:00:00.0000000Z', - releaseStatus: 'public', - ...avatar - }; - avatars.push(ref); - }); - } else { - throw new Error(`Error: ${response.data}`); - } - } catch (err) { - const msg = `Avatar lookup failed for ${authorId} with ${url}\n${err}`; - console.error(msg); - toast.error(msg); - } - return avatars; - } - - /** - * - * @param id - */ - function selectAvatarWithConfirmation(id) { - modalStore - .confirm({ - description: t('confirm.select_avatar'), - title: 'Confirm' - }) - .then(({ ok }) => { - if (!ok) return; - selectAvatarWithoutConfirmation(id); - }) - .catch(() => {}); - } - - /** - * - * @param id - */ - async function selectAvatarWithoutConfirmation(id) { - if (userStore.currentUser.currentAvatar === id) { - toast.info('Avatar already selected'); - return; - } - return avatarRequest - .selectAvatar({ - avatarId: id - }) - .then(() => { - toast.success('Avatar changed'); - }); - } - - /** - * - * @param fileId - */ - function checkAvatarCache(fileId) { - let avatarId = ''; - for (let ref of cachedAvatars.values()) { - if (extractFileId(ref.imageUrl) === fileId) { - avatarId = ref.id; - } - } - return avatarId; - } - - /** - * - * @param fileId - * @param ownerUserId - */ - async function checkAvatarCacheRemote(fileId, ownerUserId) { - if (advancedSettingsStore.avatarRemoteDatabase) { - try { - toast.dismiss(loadingToastId.value); - loadingToastId.value = toast.loading( - t('message.avatar_lookup.loading') - ); - const avatarId = await lookupAvatarByImageFileId( - ownerUserId, - fileId - ); - return avatarId; - } catch (err) { - console.error('Failed to lookup avatar by image file id:', err); - } finally { - toast.dismiss(loadingToastId.value); - } - } - return null; - } - - /** - * - * @param refUserId - * @param ownerUserId - * @param currentAvatarImageUrl - */ - async function showAvatarAuthorDialog( - refUserId, - ownerUserId, - currentAvatarImageUrl - ) { - const fileId = extractFileId(currentAvatarImageUrl); - if (!fileId) { - toast.error(t('message.avatar_lookup.failed')); - } else if (refUserId === userStore.currentUser.id) { - showAvatarDialog(userStore.currentUser.currentAvatar); - } else { - let avatarId = checkAvatarCache(fileId); - let avatarInfo; - if (!avatarId) { - avatarInfo = await getAvatarName(currentAvatarImageUrl); - if (avatarInfo.ownerId === userStore.currentUser.id) { - await userStore.refreshUserDialogAvatars(fileId); - return; - } - } - if (!avatarId) { - avatarId = await checkAvatarCacheRemote(fileId, ownerUserId); - } - if (!avatarId) { - if (ownerUserId === refUserId) { - toast.warning( - t('message.avatar_lookup.private_or_not_found') - ); - } else { - toast.warning(t('message.avatar_lookup.not_found')); - userStore.showUserDialog(avatarInfo.ownerId); - } - } - if (avatarId) { - showAvatarDialog(avatarId); - } - } - } - - /** - * - * @param avatarId - */ - function addAvatarWearTime(avatarId) { - if (!userStore.currentUser.$previousAvatarSwapTime || !avatarId) { - return; - } - const timeSpent = - Date.now() - userStore.currentUser.$previousAvatarSwapTime; - database.addAvatarTimeSpent(avatarId, timeSpent); - } - - /** - * Preload all own avatars into cache at startup for global search. - */ - async function preloadOwnAvatars() { - const params = { - n: 50, - offset: 0, - sort: 'updated', - order: 'descending', - releaseStatus: 'all', - user: 'me' - }; - await processBulk({ - fn: avatarRequest.getAvatars, - N: -1, - params, - handle: (args) => { - for (const json of args.json) { - applyAvatar(json); - } - } - }); - } - return { avatarDialog, avatarHistory, + loadingToastId, cachedAvatarModerations, cachedAvatars, cachedAvatarNames, - showAvatarDialog, applyAvatarModeration, resetCachedAvatarModerations, getAvatarGallery, updateVRChatAvatarCache, - getAvatarHistory, - addAvatarToHistory, - applyAvatar, - promptClearAvatarHistory, - getAvatarName, - lookupAvatars, - selectAvatarWithConfirmation, - selectAvatarWithoutConfirmation, + clearAvatarHistory, setAvatarDialogVisible, setAvatarDialogIsFavorite, - setAvatarDialogLoading, - showAvatarAuthorDialog, - addAvatarWearTime, - preloadOwnAvatars + setAvatarDialogLoading }; }); diff --git a/src/stores/friend.js b/src/stores/friend.js index 46120af1..bff6d477 100644 --- a/src/stores/friend.js +++ b/src/stores/friend.js @@ -25,6 +25,7 @@ import { runPendingOfflineTickFlow, runUpdateFriendFlow } from '../coordinators/friendPresenceCoordinator'; +import { applyUser } from '../coordinators/userCoordinator'; import { AppDebug } from '../service/appConfig'; import { database } from '../service/database'; import { useAppearanceSettingsStore } from './settings/appearance'; @@ -981,7 +982,7 @@ export const useFriendStore = defineStore('Friend', () => { const sqlValues = []; const friends = await refreshFriends(); for (const friend of friends) { - const ref = userStore.applyUser(friend); + const ref = applyUser(friend); const row = { userId: ref.id, displayName: ref.displayName, diff --git a/src/stores/globalSearch.js b/src/stores/globalSearch.js index 1829396b..09a26309 100644 --- a/src/stores/globalSearch.js +++ b/src/stores/globalSearch.js @@ -13,6 +13,10 @@ import { useAvatarStore } from './avatar'; import { useFavoriteStore } from './favorite'; import { useFriendStore } from './friend'; import { useGroupStore } from './group'; +import { showGroupDialog } from '../coordinators/groupCoordinator'; +import { showWorldDialog } from '../coordinators/worldCoordinator'; +import { showAvatarDialog } from '../coordinators/avatarCoordinator'; +import { showUserDialog } from '../coordinators/userCoordinator'; import { useUserStore } from './user'; import { useWorldStore } from './world'; @@ -166,16 +170,16 @@ export const useGlobalSearchStore = defineStore('GlobalSearch', () => { switch (item.type) { case 'friend': - userStore.showUserDialog(item.id); + showUserDialog(item.id); break; case 'avatar': - avatarStore.showAvatarDialog(item.id); + showAvatarDialog(item.id); break; case 'world': - worldStore.showWorldDialog(item.id); + showWorldDialog(item.id); break; case 'group': - groupStore.showGroupDialog(item.id); + showGroupDialog(item.id); break; } } diff --git a/src/stores/group.js b/src/stores/group.js index b842c7dc..b5cf9227 100644 --- a/src/stores/group.js +++ b/src/stores/group.js @@ -1,38 +1,16 @@ -import { nextTick, reactive, ref, watch } from 'vue'; +import { reactive, ref, watch } from 'vue'; import { defineStore } from 'pinia'; -import { toast } from 'vue-sonner'; import { useI18n } from 'vue-i18n'; import { - convertFileUrlToImageUrl, - createDefaultGroupRef, hasGroupPermission, - replaceBioSymbols, - sanitizeEntityJson + replaceBioSymbols } from '../shared/utils'; -import { groupRequest, instanceRequest, queryRequest } from '../api'; -import { database } from '../service/database'; +import { groupRequest, queryRequest } from '../api'; import { groupDialogFilterOptions } from '../shared/constants/'; -import { patchGroupFromEvent } from '../queries'; -import { useGameStore } from './game'; -import { useInstanceStore } from './instance'; -import { useModalStore } from './modal'; -import { useNotificationStore } from './notification'; -import { useUiStore } from './ui'; -import { useUserStore } from './user'; import { watchState } from '../service/watchState'; -import configRepository from '../service/config'; - -import * as workerTimers from 'worker-timers'; - export const useGroupStore = defineStore('Group', () => { - const instanceStore = useInstanceStore(); - const gameStore = useGameStore(); - const userStore = useUserStore(); - const notificationStore = useNotificationStore(); - const modalStore = useModalStore(); - const uiStore = useUiStore(); const { t } = useI18n(); let cachedGroups = new Map(); @@ -119,292 +97,15 @@ export const useGroupStore = defineStore('Group', () => { cachedGroups.clear(); currentUserGroups.clear(); if (isLoggedIn) { - initUserGroups(); + // initUserGroups is called from groupCoordinator via this watcher or externally + import('../coordinators/groupCoordinator').then((mod) => { + mod.initUserGroups(); + }); } }, { flush: 'sync' } ); - /** - * - * @param groupId - * @param options - */ - function showGroupDialog(groupId, options = {}) { - if (!groupId) { - return; - } - const forceRefresh = Boolean(options?.forceRefresh); - const isMainDialogOpen = uiStore.openDialog({ - type: 'group', - id: groupId - }); - const D = groupDialog.value; - D.visible = true; - if (isMainDialogOpen && D.id === groupId && !forceRefresh) { - uiStore.setDialogCrumbLabel('group', D.id, D.ref?.name || D.id); - instanceStore.applyGroupDialogInstances(); - D.loading = false; - return; - } - D.loading = true; - D.id = groupId; - D.inGroup = false; - D.ownerDisplayName = ''; - D.announcement = {}; - D.posts = []; - D.postsFiltered = []; - D.instances = []; - D.memberRoles = []; - D.lastVisit = ''; - D.memberSearch = ''; - D.memberSearchResults = []; - D.galleries = {}; - D.members = []; - D.memberFilter = groupDialogFilterOptions.everyone; - D.calendar = []; - const loadGroupRequest = groupRequest.getGroup({ - groupId, - includeRoles: true - }); - - loadGroupRequest - .catch((err) => { - D.loading = false; - D.id = null; - D.visible = false; - uiStore.jumpBackDialogCrumb(); - toast.error(t('message.group.load_failed')); - throw err; - }) - .then((args) => { - const ref = args.ref || applyGroup(args.json); - if (groupId === ref.id) { - D.ref = ref; - uiStore.setDialogCrumbLabel( - 'group', - D.id, - D.ref?.name || D.id - ); - D.inGroup = ref.membershipStatus === 'member'; - D.ownerDisplayName = ref.ownerId; - D.visible = true; - D.loading = false; - queryRequest - .fetch('user', { - userId: ref.ownerId - }) - .then((args1) => { - D.ownerDisplayName = args1.ref.displayName; - }); - database.getLastGroupVisit(D.ref.name).then((r) => { - if (D.id === ref.id) { - D.lastVisit = r.created_at; - } - }); - instanceStore.applyGroupDialogInstances(); - getGroupDialogGroup(groupId, ref); - } - }); - } - - /** - * - * @param {object }ref - * @param {string} oldUserId - * @param {string} newUserId - * @returns {Promise} - */ - async function groupOwnerChange(ref, oldUserId, newUserId) { - const oldUser = await queryRequest.fetch('user', { - userId: oldUserId - }); - const newUser = await queryRequest.fetch('user', { - userId: newUserId - }); - const oldDisplayName = oldUser?.ref?.displayName; - const newDisplayName = newUser?.ref?.displayName; - - groupChange( - ref, - `Owner changed from ${oldDisplayName} to ${newDisplayName}` - ); - } - - /** - * - * @param ref - * @param message - */ - function groupChange(ref, message) { - if (!currentUserGroupsInit.value) { - return; - } - // oh the level of cursed for compibility - const json = { - id: Math.random().toString(36), - type: 'groupChange', - senderUserId: ref.id, - senderUsername: ref.name, - imageUrl: ref.iconUrl, - details: { - imageUrl: ref.iconUrl - }, - message, - created_at: new Date().toJSON() - }; - notificationStore.handleNotification({ - json, - params: { - notificationId: json.id - } - }); - - // delay to wait for json to be assigned to ref - workerTimers.setTimeout(saveCurrentUserGroups, 100); - } - - /** - * - */ - function saveCurrentUserGroups() { - if (!currentUserGroupsInit.value) { - return; - } - const groups = []; - for (const ref of currentUserGroups.values()) { - groups.push({ - id: ref.id, - name: ref.name, - ownerId: ref.ownerId, - iconUrl: ref.iconUrl, - roles: ref.roles, - roleIds: ref.myMember?.roleIds - }); - } - configRepository.setString( - `VRCX_currentUserGroups_${userStore.currentUser.id}`, - JSON.stringify(groups) - ); - } - - /** - * - * @param {object} ref - * @param {Array} oldRoles - * @param {Array} newRoles - * @param {Array} oldRoleIds - * @param {Array} newRoleIds - */ - function groupRoleChange(ref, oldRoles, newRoles, oldRoleIds, newRoleIds) { - // check for removed/added roleIds - for (const roleId of oldRoleIds) { - if (!newRoleIds.includes(roleId)) { - let roleName = ''; - const role = oldRoles.find( - (fineRole) => fineRole.id === roleId - ); - if (role) { - roleName = role.name; - } - groupChange(ref, `Role ${roleName} removed`); - } - } - if (typeof newRoles !== 'undefined') { - for (const roleId of newRoleIds) { - if (!oldRoleIds.includes(roleId)) { - let roleName = ''; - const role = newRoles.find( - (fineRole) => fineRole.id === roleId - ); - if (role) { - roleName = role.name; - } - groupChange(ref, `Role ${roleName} added`); - } - } - } - } - - /** - * - * @param {object} ref - */ - function applyPresenceGroups(ref) { - if (!currentUserGroupsInit.value) { - // wait for init before diffing - return; - } - const groups = ref.presence?.groups; - if (!groups) { - console.error('applyPresenceGroups: invalid groups', ref); - return; - } - if (groups.length === 0) { - // as it turns out, this is not the most trust worthly source of info - return; - } - - // update group list - for (const groupId of groups) { - if (!currentUserGroups.has(groupId)) { - onGroupJoined(groupId); - } - } - for (const groupId of currentUserGroups.keys()) { - if (!groups.includes(groupId)) { - onGroupLeft(groupId); - } - } - } - - /** - * - * @param {string} groupId - */ - function onGroupJoined(groupId) { - if (!currentUserGroups.has(groupId)) { - currentUserGroups.set(groupId, { - id: groupId, - name: '', - iconUrl: '' - }); - groupRequest - .getGroup({ groupId, includeRoles: true }) - .then((args) => { - applyGroup(args.json); - saveCurrentUserGroups(); - return args; - }); - } - } - - /** - * - * @param {string} groupId - */ - async function onGroupLeft(groupId) { - const args = await groupRequest.getGroup({ groupId }); - const ref = applyGroup(args.json); - if (ref.membershipStatus === 'member') { - // wtf, not trusting presence - console.error( - `onGroupLeft: presence lied, still a member of ${groupId}` - ); - return; - } - if (groupDialog.value.visible && groupDialog.value.id === groupId) { - showGroupDialog(groupId); - } - if (currentUserGroups.has(groupId)) { - currentUserGroups.delete(groupId); - groupChange(ref, 'Left group'); - - // delay to wait for json to be assigned to ref - workerTimers.setTimeout(saveCurrentUserGroups, 100); - } - } - /** * * @param {{ groupId: string }} params @@ -449,101 +150,6 @@ export const useGroupStore = defineStore('Group', () => { return returnArgs; } - /** - * - * @param groupId - * @param {object} [existingRef] - * @returns { Promise } - */ - function getGroupDialogGroup(groupId, existingRef) { - const D = groupDialog.value; - D.isGetGroupDialogGroupLoading = false; - - const refPromise = existingRef - ? Promise.resolve({ ref: existingRef }) - : queryRequest - .fetch('group', { groupId, includeRoles: true }) - .then((args) => ({ ref: applyGroup(args.json), args })); - - return refPromise - .catch((err) => { - throw err; - }) - .then((result) => { - const ref = result.ref; - if (D.id === ref.id) { - D.loading = false; - D.ref = ref; - D.inGroup = ref.membershipStatus === 'member'; - D.memberRoles = []; - for (const role of ref.roles) { - if ( - D.ref && - D.ref.myMember && - Array.isArray(D.ref.myMember.roleIds) && - D.ref.myMember.roleIds.includes(role.id) - ) { - D.memberRoles.push(role); - } - } - getAllGroupPosts({ - groupId - }); - D.isGetGroupDialogGroupLoading = true; - groupRequest - .getGroupInstances({ - groupId - }) - .then((args) => { - if (groupDialog.value.id === args.params.groupId) { - instanceStore.applyGroupDialogInstances( - args.json.instances - ); - } - for (const json of args.json.instances) { - instanceStore.applyInstance(json); - queryRequest - .fetch('world', { - worldId: json.world.id - }) - .then((args1) => { - json.world = args1.ref; - }); - // get queue size etc - instanceRequest.getInstance({ - worldId: json.worldId, - instanceId: json.instanceId - }); - } - }); - queryRequest - .fetch('groupCalendar', { groupId }) - .then((args) => { - if (groupDialog.value.id === args.params.groupId) { - D.calendar = args.json.results; - for (const event of D.calendar) { - applyGroupEvent(event); - // fetch again for isFollowing - queryRequest - .fetch('groupCalendarEvent', { - groupId, - eventId: event.id - }) - .then((args) => { - Object.assign( - event, - applyGroupEvent(args.json) - ); - }); - } - } - }); - } - nextTick(() => (D.isGetGroupDialogGroupLoading = false)); - return result.args || result; - }); - } - /** * * @param event @@ -561,24 +167,6 @@ export const useGroupStore = defineStore('Group', () => { }; } - /** - * - */ - async function updateInGameGroupOrder() { - inGameGroupOrder.value = []; - try { - const json = await gameStore.getVRChatRegistryKey( - `VRC_GROUP_ORDER_${userStore.currentUser.id}` - ); - if (!json) { - return; - } - inGameGroupOrder.value = JSON.parse(json); - } catch (err) { - console.error(err); - } - } - /** * * @param a @@ -599,51 +187,6 @@ export const useGroupStore = defineStore('Group', () => { return aIndex - bIndex; } - /** - * - * @param groupId - */ - function leaveGroup(groupId) { - groupRequest - .leaveGroup({ - groupId - }) - .then((args) => { - const groupId = args.params.groupId; - if ( - groupDialog.value.visible && - groupDialog.value.id === groupId - ) { - groupDialog.value.inGroup = false; - getGroupDialogGroup(groupId); - } - if ( - userStore.userDialog.visible && - userStore.userDialog.id === userStore.currentUser.id && - userStore.userDialog.representedGroup.id === groupId - ) { - getCurrentUserRepresentedGroup(); - } - }); - } - - /** - * - * @param groupId - */ - function leaveGroupPrompt(groupId) { - modalStore - .confirm({ - description: t('confirm.leave_group'), - title: t('confirm.title') - }) - .then(({ ok }) => { - if (!ok) return; - leaveGroup(groupId); - }) - .catch(() => {}); - } - /** * */ @@ -664,234 +207,6 @@ export const useGroupStore = defineStore('Group', () => { }); } - /** - * - * @param groupId - * @param visibility - */ - function setGroupVisibility(groupId, visibility) { - return groupRequest - .setGroupMemberProps(userStore.currentUser.id, groupId, { - visibility - }) - .then((args) => { - handleGroupMemberProps(args); - toast.success(t('message.group.visibility_updated')); - return args; - }); - } - - /** - * - * @param groupId - * @param subscribe - */ - function setGroupSubscription(groupId, subscribe) { - return groupRequest - .setGroupMemberProps(userStore.currentUser.id, groupId, { - isSubscribedToAnnouncements: subscribe - }) - .then((args) => { - handleGroupMemberProps(args); - toast.success(t('message.group.subscription_updated')); - return args; - }); - } - - /** - * - * @param {object} json - * @returns {object} ref - */ - function applyGroup(json) { - let ref = cachedGroups.get(json.id); - sanitizeEntityJson(json, ['rules', 'name', 'description']); - if (typeof ref === 'undefined') { - ref = createDefaultGroupRef(json); - cachedGroups.set(ref.id, ref); - } else { - if (currentUserGroups.has(ref.id)) { - // compare group props - if ( - ref.ownerId && - json.ownerId && - ref.ownerId !== json.ownerId - ) { - // owner changed - groupOwnerChange(json, ref.ownerId, json.ownerId); - } - if (ref.name && json.name && ref.name !== json.name) { - // name changed - groupChange( - json, - `Name changed from ${ref.name} to ${json.name}` - ); - } - if (ref.myMember?.roleIds && json.myMember?.roleIds) { - const oldRoleIds = ref.myMember.roleIds; - const newRoleIds = json.myMember.roleIds; - if ( - oldRoleIds.length !== newRoleIds.length || - !oldRoleIds.every( - (value, index) => value === newRoleIds[index] - ) - ) { - // roleIds changed - groupRoleChange( - json, - ref.roles, - json.roles, - oldRoleIds, - newRoleIds - ); - } - } - } - if (json.myMember) { - if (typeof json.myMember.roleIds === 'undefined') { - // keep roleIds - json.myMember.roleIds = ref.myMember.roleIds; - } - Object.assign(ref.myMember, json.myMember); - } - Object.assign(ref, json); - } - // update myMember without fetching member - if (typeof json.memberVisibility !== 'undefined') { - ref.myMember.visibility = json.memberVisibility; - } - if (typeof json.isRepresenting !== 'undefined') { - ref.myMember.isRepresenting = json.isRepresenting; - } - if (typeof json.membershipStatus !== 'undefined') { - ref.myMember.membershipStatus = json.membershipStatus; - } - if (typeof json.roleIds !== 'undefined') { - ref.myMember.roleIds = json.roleIds; - } - ref.$url = `https://vrc.group/${ref.shortCode}.${ref.discriminator}`; - applyGroupLanguage(ref); - - const currentUserGroupRef = currentUserGroups.get(ref.id); - if (currentUserGroupRef) { - currentUserGroups.set(ref.id, ref); - } - - const D = groupDialog.value; - if (D.visible && D.id === ref.id) { - D.inGroup = ref.membershipStatus === 'member'; - D.ref = ref; - } - patchGroupFromEvent(ref); - return ref; - } - - /** - * - * @param args - */ - function handleGroupRepresented(args) { - const D = userStore.userDialog; - const json = args.json; - D.representedGroup = json; - D.representedGroup.$thumbnailUrl = convertFileUrlToImageUrl( - json.iconUrl - ); - if (!json || !json.isRepresenting) { - D.isRepresentedGroupLoading = false; - } - if (!json.groupId) { - // no group - return; - } - if (args.params.userId !== userStore.currentUser.id) { - // not current user, don't apply someone elses myMember - return; - } - json.$memberId = json.id; - json.id = json.groupId; - applyGroup(json); - } - - /** - * - * @param args - */ - function handleGroupList(args) { - for (const json of args.json) { - json.$memberId = json.id; - json.id = json.groupId; - applyGroup(json); - } - } - - /** - * - * @param args - */ - function handleGroupMemberProps(args) { - if (args.userId === userStore.currentUser.id) { - const json = args.json; - json.$memberId = json.id; - json.id = json.groupId; - if ( - groupDialog.value.visible && - groupDialog.value.id === json.groupId - ) { - groupDialog.value.ref.myMember.visibility = json.visibility; - groupDialog.value.ref.myMember.isSubscribedToAnnouncements = - json.isSubscribedToAnnouncements; - } - if ( - userStore.userDialog.visible && - userStore.userDialog.id === userStore.currentUser.id - ) { - getCurrentUserRepresentedGroup(); - } - handleGroupMember({ - json, - params: { - groupId: json.groupId - } - }); - } - let member; - if (groupDialog.value.id === args.json.groupId) { - let i; - for (i = 0; i < groupDialog.value.members.length; ++i) { - member = groupDialog.value.members[i]; - if (member.userId === args.json.userId) { - Object.assign(member, applyGroupMember(args.json)); - break; - } - } - for (i = 0; i < groupDialog.value.memberSearchResults.length; ++i) { - member = groupDialog.value.memberSearchResults[i]; - if (member.userId === args.json.userId) { - Object.assign(member, applyGroupMember(args.json)); - break; - } - } - } - } - - /** - * - * @param args - */ - function handleGroupPermissions(args) { - if (args.params.userId !== userStore.currentUser.id) { - return; - } - const json = args.json; - for (const groupId in json) { - const permissions = json[groupId]; - const group = cachedGroups.get(groupId); - if (group) { - group.myMember.permissions = permissions; - } - } - } /** * * @param {object} args @@ -925,43 +240,6 @@ export const useGroupStore = defineStore('Group', () => { updateGroupPostSearch(); } - /** - * - * @param args - */ - function handleGroupMember(args) { - args.ref = applyGroupMember(args.json); - } - - /** - * - * @param args - */ - async function handleGroupUserInstances(args) { - groupInstances.value = []; - for (const json of args.json.instances) { - if (args.json.fetchedAt) { - // tack on fetchedAt - json.$fetchedAt = args.json.fetchedAt; - } - const instanceRef = instanceStore.applyInstance(json); - const groupRef = cachedGroups.get(json.ownerId); - if (typeof groupRef === 'undefined') { - if (watchState.isFriendsLoaded) { - const args = await groupRequest.getGroup({ - groupId: json.ownerId - }); - applyGroup(args.json); - } - return; - } - groupInstances.value.push({ - group: groupRef, - instance: instanceRef - }); - } - } - /** * */ @@ -976,177 +254,6 @@ export const useGroupStore = defineStore('Group', () => { groupDialog.value.visible = value; } - /** - * - * @param {object} json - * @returns {*} - */ - function applyGroupMember(json) { - let ref; - if (typeof json?.user !== 'undefined') { - if (json.userId === userStore.currentUser.id) { - json.user = userStore.currentUser; - json.$displayName = userStore.currentUser.displayName; - } else { - ref = userStore.cachedUsers.get(json.user.id); - if (typeof ref !== 'undefined') { - json.user = ref; - json.$displayName = ref.displayName; - } else { - json.$displayName = json.user?.displayName; - } - } - } - // update myMember without fetching member - if (json?.userId === userStore.currentUser.id) { - ref = cachedGroups.get(json.groupId); - if (typeof ref !== 'undefined') { - const newJson = { - id: json.groupId, - memberVisibility: json.visibility, - isRepresenting: json.isRepresenting, - isSubscribedToAnnouncements: - json.isSubscribedToAnnouncements, - joinedAt: json.joinedAt, - roleIds: json.roleIds, - membershipStatus: json.membershipStatus - }; - applyGroup(newJson); - } - } - - return json; - } - - /** - * - * @param ref - */ - function applyGroupLanguage(ref) { - ref.$languages = []; - const { languages } = ref; - if (!languages) { - return; - } - for (const language of languages) { - const value = userStore.subsetOfLanguages[language]; - if (typeof value === 'undefined') { - continue; - } - ref.$languages.push({ - key: language, - value - }); - } - } - - /** - * - * @param userId - * @param groups - */ - async function loadCurrentUserGroups(userId, groups) { - const savedGroups = JSON.parse( - await configRepository.getString( - `VRCX_currentUserGroups_${userId}`, - '[]' - ) - ); - cachedGroups.clear(); - currentUserGroups.clear(); - for (const group of savedGroups) { - const json = { - id: group.id, - name: group.name, - iconUrl: group.iconUrl, - ownerId: group.ownerId, - roles: group.roles, - myMember: { - roleIds: group.roleIds - } - }; - const ref = applyGroup(json); - currentUserGroups.set(group.id, ref); - } - - if (groups) { - const promises = groups.map(async (groupId) => { - const groupRef = cachedGroups.get(groupId); - - if ( - typeof groupRef !== 'undefined' && - groupRef.roles?.length > 0 - ) { - return; - } - - try { - console.log(`Fetching group with missing roles ${groupId}`); - const args = await groupRequest.getGroup({ - groupId, - includeRoles: true - }); - const ref = applyGroup(args.json); - currentUserGroups.set(groupId, ref); - } catch (err) { - console.error(err); - } - }); - - await Promise.allSettled(promises); - } - - currentUserGroupsInit.value = true; - getCurrentUserGroups(); - } - - /** - * - */ - async function getCurrentUserGroups() { - const args = await groupRequest.getGroups({ - userId: userStore.currentUser.id - }); - handleGroupList(args); - currentUserGroups.clear(); - for (const group of args.json) { - const ref = applyGroup(group); - if (!currentUserGroups.has(group.id)) { - currentUserGroups.set(group.id, ref); - } - } - const args1 = await groupRequest.getGroupPermissions({ - userId: userStore.currentUser.id - }); - handleGroupPermissions(args1); - saveCurrentUserGroups(); - } - - /** - * - */ - function getCurrentUserRepresentedGroup() { - return groupRequest - .getRepresentedGroup({ - userId: userStore.currentUser.id - }) - .then((args) => { - handleGroupRepresented(args); - return args; - }); - } - - /** - * - */ - async function initUserGroups() { - updateInGameGroupOrder(); - loadCurrentUserGroups( - userStore.currentUser.id, - userStore.currentUser?.presence?.groups - ); - } - /** * * @param userId @@ -1194,33 +301,14 @@ export const useGroupStore = defineStore('Group', () => { inGameGroupOrder, groupInstances, currentUserGroupsInit, - initUserGroups, - showGroupDialog, - applyGroup, - saveCurrentUserGroups, - applyPresenceGroups, - getGroupDialogGroup, - updateInGameGroupOrder, + getAllGroupPosts, + applyGroupEvent, sortGroupInstancesByInGame, - leaveGroup, - leaveGroupPrompt, updateGroupPostSearch, - setGroupVisibility, - setGroupSubscription, - applyGroupMember, - loadCurrentUserGroups, handleGroupPost, - handleGroupUserInstances, clearGroupInstances, setGroupDialogVisible, - handleGroupMember, - handleGroupPermissions, - handleGroupMemberProps, - handleGroupList, - handleGroupRepresented, showModerateGroupDialog, - showGroupMemberModerationDialog, - onGroupLeft, - applyGroupEvent + showGroupMemberModerationDialog }; }); diff --git a/src/stores/notification/index.js b/src/stores/notification/index.js index 051a98dd..528f8800 100644 --- a/src/stores/notification/index.js +++ b/src/stores/notification/index.js @@ -44,6 +44,8 @@ import { useFriendStore } from '../friend'; import { useGameStore } from '../game'; import { useGeneralSettingsStore } from '../settings/general'; import { useGroupStore } from '../group'; +import { showGroupDialog } from '../../coordinators/groupCoordinator'; +import { showUserDialog } from '../../coordinators/userCoordinator'; import { useInstanceStore } from '../instance'; import { useLocationStore } from '../location'; import { useModalStore } from '../modal'; @@ -1415,10 +1417,10 @@ export const useNotificationStore = defineStore('Notification', () => { } switch (data[0]) { case 'group': - groupStore.showGroupDialog(data[1]); + showGroupDialog(data[1]); break; case 'user': - userStore.showUserDialog(data[1]); + showUserDialog(data[1]); break; case 'event': const ids = data[1].split(','); @@ -1427,7 +1429,7 @@ export const useNotificationStore = defineStore('Notification', () => { return; } - groupStore.showGroupDialog(ids[0]); + showGroupDialog(ids[0]); // ids[1] cal_ is the event id break; case 'openNotificationLink': diff --git a/src/stores/photon.js b/src/stores/photon.js index 7f639b80..aecf7054 100644 --- a/src/stores/photon.js +++ b/src/stores/photon.js @@ -20,6 +20,8 @@ import { photonEmojis, photonEventType } from '../shared/constants/photon'; import { AppDebug } from '../service/appConfig'; import { database } from '../service/database'; import { useAvatarStore } from './avatar'; +import { applyAvatar } from '../coordinators/avatarCoordinator'; +import { showUserDialog, lookupUser, applyUser } from '../coordinators/userCoordinator'; import { useFavoriteStore } from './favorite'; import { useFriendStore } from './friend'; import { useGameLogStore } from './gameLog'; @@ -424,9 +426,9 @@ export const usePhotonStore = defineStore('Photon', () => { const ref = photonLobby.value.get(photonId); if (typeof ref !== 'undefined') { if (typeof ref.id !== 'undefined') { - userStore.showUserDialog(ref.id); + showUserDialog(ref.id); } else if (typeof ref.displayName !== 'undefined') { - userStore.lookupUser(ref); + lookupUser(ref); } } else { toast.error('No user info available'); @@ -1475,7 +1477,7 @@ export const usePhotonStore = defineStore('Photon', () => { typeof ref.id !== 'undefined' && ref.currentAvatarImageUrl !== user.currentAvatarImageUrl ) { - userStore.applyUser({ + applyUser({ ...ref, currentAvatarImageUrl: user.currentAvatarImageUrl, currentAvatarThumbnailImageUrl: @@ -1803,7 +1805,7 @@ export const usePhotonStore = defineStore('Photon', () => { } } } - avatarStore.applyAvatar({ + applyAvatar({ id: avatar.id, authorId: avatar.authorId, authorName: avatar.authorName, diff --git a/src/stores/search.js b/src/stores/search.js index 9d513b79..84e32603 100644 --- a/src/stores/search.js +++ b/src/stores/search.js @@ -12,6 +12,10 @@ import { useAppearanceSettingsStore } from './settings/appearance'; import { useAvatarStore } from './avatar'; import { useFriendStore } from './friend'; import { useGroupStore } from './group'; +import { showGroupDialog } from '../coordinators/groupCoordinator'; +import { showWorldDialog } from '../coordinators/worldCoordinator'; +import { showAvatarDialog } from '../coordinators/avatarCoordinator'; +import { applyUser, showUserDialog, lookupUser } from '../coordinators/userCoordinator'; import { useModalStore } from './modal'; import { useUserStore } from './user'; import { useWorldStore } from './world'; @@ -93,7 +97,7 @@ export const useSearchStore = defineStore('Search', () => { console.error('getUsers gave us garbage', json); continue; } - userStore.applyUser(json); + applyUser(json); } const map = new Map(); @@ -210,10 +214,10 @@ export const useSearchStore = defineStore('Search', () => { } else { router.push({ name: 'search' }); searchText.value = searchTerm; - userStore.lookupUser({ displayName: searchTerm }); + lookupUser({ displayName: searchTerm }); } } else { - userStore.showUserDialog(value); + showUserDialog(value); } } @@ -273,15 +277,15 @@ export const useSearchStore = defineStore('Search', () => { const type = urlPathSplit[2]; if (type === 'user') { const userId = urlPathSplit[3]; - userStore.showUserDialog(userId); + showUserDialog(userId); return true; } else if (type === 'avatar') { const avatarId = urlPathSplit[3]; - avatarStore.showAvatarDialog(avatarId); + showAvatarDialog(avatarId); return true; } else if (type === 'group') { const groupId = urlPathSplit[3]; - groupStore.showGroupDialog(groupId); + showGroupDialog(groupId); return true; } } else if (input.startsWith('https://vrc.group/')) { @@ -295,16 +299,16 @@ export const useSearchStore = defineStore('Search', () => { input.substring(0, 4) === 'usr_' || /^[A-Za-z0-9]{10}$/g.test(input) ) { - userStore.showUserDialog(input); + showUserDialog(input); return true; } else if ( input.substring(0, 5) === 'avtr_' || input.substring(0, 2) === 'b_' ) { - avatarStore.showAvatarDialog(input); + showAvatarDialog(input); return true; } else if (input.substring(0, 4) === 'grp_') { - groupStore.showGroupDialog(input); + showGroupDialog(input); return true; } return false; @@ -331,7 +335,7 @@ export const useSearchStore = defineStore('Search', () => { const urlPathSplit = urlPath.split('/'); if (urlPathSplit.length >= 4 && urlPathSplit[2] === 'world') { worldId = urlPathSplit[3]; - worldStore.showWorldDialog(worldId); + showWorldDialog(worldId); return true; } else if (urlPath.substring(5, 12) === '/launch') { const urlParams = new URLSearchParams(url.search); @@ -343,10 +347,10 @@ export const useSearchStore = defineStore('Search', () => { if (shortName) { return verifyShortName(location, shortName); } - worldStore.showWorldDialog(location); + showWorldDialog(location); return true; } else if (worldId) { - worldStore.showWorldDialog(worldId); + showWorldDialog(worldId); return true; } } @@ -360,7 +364,7 @@ export const useSearchStore = defineStore('Search', () => { input = `https://vrchat.com/home/launch?worldId=${input}`; return directAccessWorld(input); } - worldStore.showWorldDialog(input.trim()); + showWorldDialog(input.trim()); return true; } return false; @@ -399,7 +403,7 @@ export const useSearchStore = defineStore('Search', () => { groupRequest.groupStrictsearch({ query: shortCode }).then((args) => { for (const group of args.json) { if (`${group.shortCode}.${group.discriminator}` === shortCode) { - groupStore.showGroupDialog(group.id); + showGroupDialog(group.id); break; } } @@ -414,11 +418,11 @@ export const useSearchStore = defineStore('Search', () => { const newLocation = args.json.location; const newShortName = args.json.shortName; if (newShortName) { - worldStore.showWorldDialog(newLocation, newShortName); + showWorldDialog(newLocation, newShortName); } else if (newLocation) { - worldStore.showWorldDialog(newLocation); + showWorldDialog(newLocation); } else { - worldStore.showWorldDialog(location); + showWorldDialog(location); } return args; }); diff --git a/src/stores/ui.js b/src/stores/ui.js index 27038b97..6192e47d 100644 --- a/src/stores/ui.js +++ b/src/stores/ui.js @@ -10,6 +10,10 @@ import { updateLocalizedStrings } from '../plugin/i18n'; import { useAppearanceSettingsStore } from './settings/appearance'; import { useAvatarStore } from './avatar'; import { useGroupStore } from './group'; +import { showGroupDialog } from '../coordinators/groupCoordinator'; +import { showWorldDialog } from '../coordinators/worldCoordinator'; +import { showAvatarDialog } from '../coordinators/avatarCoordinator'; +import { showUserDialog } from '../coordinators/userCoordinator'; import { useInstanceStore } from './instance'; import { useNotificationStore } from './notification'; import { useSearchStore } from './search'; @@ -142,19 +146,19 @@ export const useUiStore = defineStore('Ui', () => { } jumpDialogCrumb(index); if (item.type === 'user') { - userStore.showUserDialog(item.id); + showUserDialog(item.id); return; } if (item.type === 'world') { - worldStore.showWorldDialog(item.tag, item.shortName); + showWorldDialog(item.tag, item.shortName); return; } if (item.type === 'avatar') { - avatarStore.showAvatarDialog(item.id); + showAvatarDialog(item.id); return; } if (item.type === 'group') { - groupStore.showGroupDialog(item.id); + showGroupDialog(item.id); return; } if (item.type === 'previous-instances-user') { diff --git a/src/stores/updateLoop.js b/src/stores/updateLoop.js index 95fc1cd5..fcc477a3 100644 --- a/src/stores/updateLoop.js +++ b/src/stores/updateLoop.js @@ -12,6 +12,8 @@ import { useFriendStore } from './friend'; import { useGameLogStore } from './gameLog'; import { useGameStore } from './game'; import { useGroupStore } from './group'; +import { handleGroupUserInstances } from '../coordinators/groupCoordinator'; +import { getCurrentUser, updateAutoStateChange } from '../coordinators/userCoordinator'; import { useModerationStore } from './moderation'; import { useUserStore } from './user'; import { useVRCXUpdaterStore } from './vrcxUpdater'; @@ -73,7 +75,7 @@ export const useUpdateLoopStore = defineStore('UpdateLoop', () => { if (watchState.isLoggedIn) { if (--state.nextCurrentUserRefresh <= 0) { state.nextCurrentUserRefresh = 300; // 5min - userStore.getCurrentUser(); + getCurrentUser(); } if (--state.nextFriendsRefresh <= 0) { state.nextFriendsRefresh = 3600; // 1hour @@ -92,7 +94,7 @@ export const useUpdateLoopStore = defineStore('UpdateLoop', () => { state.nextGroupInstanceRefresh = 300; // 5min const args = await groupRequest.getUsersGroupInstances(); - groupStore.handleGroupUserInstances(args); + handleGroupUserInstances(args); } AppApi.CheckGameRunning(); } @@ -122,7 +124,7 @@ export const useUpdateLoopStore = defineStore('UpdateLoop', () => { } if (--state.nextAutoStateChange <= 0) { state.nextAutoStateChange = 3; - userStore.updateAutoStateChange(); + updateAutoStateChange(); } if (LINUX && --state.nextGetLogCheck <= 0) { state.nextGetLogCheck = 0.5; diff --git a/src/stores/user.js b/src/stores/user.js index a8a2218a..c9dc6cc4 100644 --- a/src/stores/user.js +++ b/src/stores/user.js @@ -1,64 +1,31 @@ import { computed, reactive, ref, shallowReactive, watch } from 'vue'; import { defineStore } from 'pinia'; -import { toast } from 'vue-sonner'; import { useI18n } from 'vue-i18n'; import { - arraysMatch, compareByCreatedAt, compareByDisplayName, compareByLocationAt, compareByName, compareByUpdatedAt, - computeUserPlatform, - createDefaultUserRef, - diffObjectProps, - evictMapCache, - extractFileId, - findUserByDisplayName, getAllUserMemos, getUserMemo, - getWorldName, isRealInstance, parseLocation, - replaceBioSymbols, - sanitizeUserJson + replaceBioSymbols } from '../shared/utils'; import { - avatarRequest, instanceRequest, - queryRequest, userRequest } from '../api'; -import { - runAvatarSwapFlow, - runFirstLoginFlow, - runHomeLocationSyncFlow, - runPostApplySyncFlow -} from '../coordinators/userSessionCoordinator'; -import { processBulk, request } from '../service/request'; import { AppDebug } from '../service/appConfig'; import { database } from '../service/database'; -import { patchUserFromEvent } from '../queries'; -import { runHandleUserUpdateFlow } from '../coordinators/userEventCoordinator'; import { runUpdateCurrentUserLocationFlow } from '../coordinators/locationCoordinator'; -import { runUpdateFriendFlow } from '../coordinators/friendPresenceCoordinator'; -import { applyFavorite } from '../coordinators/favoriteCoordinator'; import { useAppearanceSettingsStore } from './settings/appearance'; -import { useAuthStore } from './auth'; -import { useAvatarStore } from './avatar'; import { useFavoriteStore } from './favorite'; import { useFriendStore } from './friend'; -import { useGameStore } from './game'; -import { useGeneralSettingsStore } from './settings/general'; -import { useGroupStore } from './group'; import { useInstanceStore } from './instance'; import { useLocationStore } from './location'; -import { useModerationStore } from './moderation'; -import { useNotificationStore } from './notification'; -import { usePhotonStore } from './photon'; -import { useSearchStore } from './search'; -import { useSharedFeedStore } from './sharedFeed'; import { useUiStore } from './ui'; import { watchState } from '../service/watchState'; @@ -70,17 +37,7 @@ export const useUserStore = defineStore('User', () => { const favoriteStore = useFavoriteStore(); const locationStore = useLocationStore(); const instanceStore = useInstanceStore(); - const avatarStore = useAvatarStore(); - const generalSettingsStore = useGeneralSettingsStore(); - const searchStore = useSearchStore(); - const gameStore = useGameStore(); - const notificationStore = useNotificationStore(); - const authStore = useAuthStore(); - const groupStore = useGroupStore(); const uiStore = useUiStore(); - const moderationStore = useModerationStore(); - const photonStore = usePhotonStore(); - const sharedFeedStore = useSharedFeedStore(); const { t } = useI18n(); const currentUser = ref({ @@ -352,41 +309,6 @@ export const useUserStore = defineStore('User', () => { ); /** - * - * @param args - */ - function handleConfig(args) { - const authStore = useAuthStore(); - const ref = { - ...args.json - }; - args.ref = ref; - authStore.setCachedConfig(ref); - if (typeof args.ref?.whiteListedAssetUrls !== 'object') { - console.error('Invalid config whiteListedAssetUrls'); - } - AppApi.PopulateImageHosts( - JSON.stringify(args.ref.whiteListedAssetUrls) - ); - const languages = - args.ref?.constants?.LANGUAGE?.SPOKEN_LANGUAGE_OPTIONS; - if (!languages) { - return; - } - subsetOfLanguages.value = languages; - const data = []; - for (const key in languages) { - const value = languages[key]; - data.push({ - key, - value - }); - } - languageDialog.value.languages = data; - } - - /** - * * @param {object} ref */ function applyUserLanguage(ref) { @@ -411,7 +333,6 @@ export const useUserStore = defineStore('User', () => { } /** - * * @param {object} ref */ function applyPresenceLocation(ref) { @@ -429,487 +350,7 @@ export const useUserStore = defineStore('User', () => { runUpdateCurrentUserLocationFlow(); } - const robotUrl = `${AppDebug.endpointDomain}/file/file_0e8c4e32-7444-44ea-ade4-313c010d4bae/1/file`; /** - * - * @param {import('../types/api/user').GetUserResponse} json - * @returns {import('../types/api/user').VrcxUser} - */ - function applyUser(json) { - let ref = cachedUsers.get(json.id); - let hasPropChanged = false; - let changedProps = {}; - sanitizeUserJson(json, robotUrl); - if (typeof ref === 'undefined') { - ref = reactive(createDefaultUserRef(json)); - if (locationStore.lastLocation.playerList.has(json.id)) { - // update $location_at from instance join time - const player = locationStore.lastLocation.playerList.get( - json.id - ); - ref.$location_at = player.joinTime; - ref.$online_for = player.joinTime; - } - if (ref.isFriend || ref.id === currentUser.value.id) { - // update instancePlayerCount - let newCount = state.instancePlayerCount.get(ref.location); - if (typeof newCount === 'undefined') { - newCount = 0; - } - newCount++; - state.instancePlayerCount.set(ref.location, newCount); - } - const tag = customUserTags.get(json.id); - if (tag) { - ref.$customTag = tag.tag; - ref.$customTagColour = tag.colour; - } else if (ref.$customTag) { - ref.$customTag = ''; - ref.$customTagColour = ''; - } - evictMapCache( - cachedUsers, - friendStore.friends.size + 300, - (_value, key) => friendStore.friends.has(key), - { logLabel: 'User cache cleanup' } - ); - cachedUsers.set(ref.id, ref); - runUpdateFriendFlow(ref.id); - } else { - if (json.state !== 'online') { - // offline event before GPS to offline location - runUpdateFriendFlow(ref.id, json.state); - } - const { - hasPropChanged: _hasPropChanged, - changedProps: _changedProps - } = diffObjectProps(ref, json, arraysMatch); - for (const prop in json) { - if (typeof json[prop] !== 'undefined') { - ref[prop] = json[prop]; - } - } - hasPropChanged = _hasPropChanged; - changedProps = _changedProps; - } - ref.$moderations = moderationStore.getUserModerations(ref.id); - ref.$isVRCPlus = ref.tags.includes('system_supporter'); - appearanceSettingsStore.applyUserTrustLevel(ref); - applyUserLanguage(ref); - ref.$platform = computeUserPlatform(ref.platform, ref.last_platform); - // traveling - if (ref.location === 'traveling') { - ref.$location = parseLocation(ref.travelingToLocation); - if (!currentTravelers.has(ref.id) && ref.travelingToLocation) { - const travelRef = reactive({ - created_at: new Date().toJSON(), - ...ref - }); - currentTravelers.set(ref.id, travelRef); - onPlayerTraveling(travelRef); - } - } else { - ref.$location = parseLocation(ref.location); - currentTravelers.delete(ref.id); - } - if ( - !instanceStore.cachedInstances.has(ref.$location.tag) && - isRealInstance(ref.location) - ) { - instanceRequest.getInstance({ - worldId: ref.$location.worldId, - instanceId: ref.$location.instanceId - }); - } - if ( - ref.$isVRCPlus && - ref.badges && - ref.badges.every( - (x) => x.badgeId !== 'bdg_754f9935-0f97-49d8-b857-95afb9b673fa' - ) - ) { - // I doubt this will last long - ref.badges.unshift({ - badgeId: 'bdg_754f9935-0f97-49d8-b857-95afb9b673fa', - badgeName: 'Supporter', - badgeDescription: 'Supports VRChat through VRC+', - badgeImageUrl: - 'https://assets.vrchat.com/badges/fa/bdgai_583f6b13-91ab-4e1b-974e-ab91600b06cb.png', - hidden: true, - showcased: false - }); - } - const friendCtx = friendStore.friends.get(ref.id); - if (friendCtx) { - friendCtx.ref = ref; - friendCtx.name = ref.displayName; - } - if (ref.id === currentUser.value.id) { - if (ref.status) { - currentUser.value.status = ref.status; - } - runUpdateCurrentUserLocationFlow(); - } - // add user ref to playerList, friendList, photonLobby, photonLobbyCurrent - const playerListRef = locationStore.lastLocation.playerList.get(ref.id); - if (playerListRef) { - // add/remove friends from lastLocation.friendList - if ( - !locationStore.lastLocation.friendList.has(ref.id) && - friendStore.friends.has(ref.id) - ) { - const userMap = { - displayName: ref.displayName, - userId: ref.id, - joinTime: playerListRef.joinTime - }; - locationStore.lastLocation.friendList.set(ref.id, userMap); - } - if ( - locationStore.lastLocation.friendList.has(ref.id) && - !friendStore.friends.has(ref.id) - ) { - locationStore.lastLocation.friendList.delete(ref.id); - } - photonStore.photonLobby.forEach((ref1, id) => { - if ( - typeof ref1 !== 'undefined' && - ref1.displayName === ref.displayName && - ref1 !== ref - ) { - photonStore.photonLobby.set(id, ref); - if (photonStore.photonLobbyCurrent.has(id)) { - photonStore.photonLobbyCurrent.set(id, ref); - } - } - }); - instanceStore.getCurrentInstanceUserList(); - } - if (ref.state === 'online') { - runUpdateFriendFlow(ref.id, ref.state); // online/offline - } - applyFavorite('friend', ref.id); - friendStore.userOnFriend(ref); - const D = userDialog.value; - if (D.visible && D.id === ref.id) { - D.ref = ref; - D.note = String(ref.note || ''); - D.incomingRequest = false; - D.outgoingRequest = false; - if (D.ref.friendRequestStatus === 'incoming') { - D.incomingRequest = true; - } else if (D.ref.friendRequestStatus === 'outgoing') { - D.outgoingRequest = true; - } - } - if (hasPropChanged) { - if ( - changedProps.location && - changedProps.location[0] !== 'traveling' - ) { - const ts = Date.now(); - changedProps.location.push(ts - ref.$location_at); - ref.$location_at = ts; - } - handleUserUpdate(ref, changedProps); - if (AppDebug.debugUserDiff) { - delete changedProps.last_login; - delete changedProps.last_activity; - if (Object.keys(changedProps).length !== 0) { - console.log('>', ref.displayName, changedProps); - } - } - } - patchUserFromEvent(ref); - return ref; - } - - /** - * - * @param {string} userId - */ - function showUserDialog(userId) { - if ( - !userId || - typeof userId !== 'string' || - userId === 'usr_00000000-0000-0000-0000-000000000000' - ) { - return; - } - const isMainDialogOpen = uiStore.openDialog({ - type: 'user', - id: userId - }); - const D = userDialog.value; - D.visible = true; - if (isMainDialogOpen && D.id === userId) { - uiStore.setDialogCrumbLabel( - 'user', - D.id, - D.ref?.displayName || D.id - ); - applyUserDialogLocation(true); - return; - } - D.id = userId; - D.memo = ''; - D.note = ''; - getUserMemo(userId).then((memo) => { - if (memo.userId === userId) { - D.memo = memo.memo; - const ref = friendStore.friends.get(userId); - if (ref) { - ref.memo = String(memo.memo || ''); - if (memo.memo) { - ref.$nickName = memo.memo.split('\n')[0]; - } else { - ref.$nickName = ''; - } - } - } - }); - - D.loading = true; - D.avatars = []; - D.worlds = []; - D.instance = { - id: '', - tag: '', - $location: {}, - friendCount: 0, - users: [], - shortName: '', - ref: {} - }; - D.isRepresentedGroupLoading = true; - D.representedGroup = { - bannerId: '', - bannerUrl: '', - description: '', - discriminator: '', - groupId: '', - id: '', - iconUrl: '', - isRepresenting: false, - memberCount: 0, - memberVisibility: '', - name: '', - ownerId: '', - privacy: '', - shortCode: '', - $thumbnailUrl: '', - $memberId: '' - }; - D.lastSeen = ''; - D.joinCount = 0; - D.timeSpent = 0; - D.avatarModeration = 0; - D.isHideAvatar = false; - D.isShowAvatar = false; - D.previousDisplayNames = []; - D.dateFriended = ''; - D.unFriended = false; - D.dateFriendedInfo = []; - D.mutualFriendCount = 0; - D.mutualGroupCount = 0; - if (userId === currentUser.value.id) { - getWorldName(currentUser.value.homeLocation).then((worldName) => { - D.$homeLocationName = worldName; - }); - } - AppApi.SendIpc('ShowUserDialog', userId); - queryRequest - .fetch('user', { - userId - }) - .catch((err) => { - D.loading = false; - D.id = null; - D.visible = false; - uiStore.jumpBackDialogCrumb(); - toast.error(t('message.user.load_failed')); - throw err; - }) - .then((args) => { - if (args.ref.id === D.id) { - D.loading = false; - - D.ref = args.ref; - uiStore.setDialogCrumbLabel( - 'user', - D.id, - D.ref?.displayName || D.id - ); - D.friend = friendStore.friends.get(D.id); - D.isFriend = Boolean(D.friend); - D.note = String(D.ref.note || ''); - D.incomingRequest = false; - D.outgoingRequest = false; - D.isBlock = false; - D.isMute = false; - D.isInteractOff = false; - D.isMuteChat = false; - for (const ref of moderationStore.cachedPlayerModerations.values()) { - if ( - ref.targetUserId === D.id && - ref.sourceUserId === currentUser.value.id - ) { - if (ref.type === 'block') { - D.isBlock = true; - } else if (ref.type === 'mute') { - D.isMute = true; - } else if (ref.type === 'interactOff') { - D.isInteractOff = true; - } else if (ref.type === 'muteChat') { - D.isMuteChat = true; - } - } - } - D.isFavorite = - favoriteStore.getCachedFavoritesByObjectId(D.id) || - favoriteStore.isInAnyLocalFriendGroup(D.id); - if (D.ref.friendRequestStatus === 'incoming') { - D.incomingRequest = true; - } else if (D.ref.friendRequestStatus === 'outgoing') { - D.outgoingRequest = true; - } - let inCurrentWorld = false; - if (locationStore.lastLocation.playerList.has(D.ref.id)) { - inCurrentWorld = true; - } - if (userId !== currentUser.value.id) { - database - .getUserStats(D.ref, inCurrentWorld) - .then(async (ref1) => { - if (ref1.userId === D.id) { - D.lastSeen = ref1.lastSeen; - D.joinCount = ref1.joinCount; - D.timeSpent = ref1.timeSpent; - } - const displayNameMap = - ref1.previousDisplayNames; - const userNotifications = - await database.getFriendLogHistoryForUserId( - D.id, - ['DisplayName', 'Friend', 'Unfriend'] - ); - const dateFriendedInfo = []; - for (const notification of userNotifications) { - if (notification.userId !== D.id) { - continue; - } - if (notification.type === 'DisplayName') { - displayNameMap.set( - notification.previousDisplayName, - notification.created_at - ); - } - if ( - notification.type === 'Friend' || - (notification.type === 'Unfriend' && - !appearanceSettingsStore.hideUnfriends) - ) { - dateFriendedInfo.unshift(notification); - } - } - D.dateFriendedInfo = dateFriendedInfo; - if (dateFriendedInfo.length > 0) { - const latestFriendedInfo = - dateFriendedInfo[0]; - D.unFriended = - latestFriendedInfo.type === 'Unfriend'; - D.dateFriended = - latestFriendedInfo.created_at; - } - displayNameMap.forEach( - (updated_at, displayName) => { - D.previousDisplayNames.push({ - displayName, - updated_at - }); - } - ); - }); - AppApi.GetVRChatUserModeration( - currentUser.value.id, - userId - ).then((result) => { - D.avatarModeration = result; - if (result === 4) { - D.isHideAvatar = true; - } else if (result === 5) { - D.isShowAvatar = true; - } - }); - if (!currentUser.value.hasSharedConnectionsOptOut) { - try { - queryRequest - .fetch('mutualCounts', { userId }) - .then((args) => { - if (args.params.userId === D.id) { - D.mutualFriendCount = - args.json.friends; - D.mutualGroupCount = - args.json.groups; - } - }); - } catch (error) { - console.error(error); - } - } - } else { - D.previousDisplayNames = - currentUser.value.pastDisplayNames; - database - .getUserStats(D.ref, inCurrentWorld) - .then((ref1) => { - if (ref1.userId === D.id) { - D.lastSeen = ref1.lastSeen; - D.joinCount = ref1.joinCount; - D.timeSpent = ref1.timeSpent; - } - }); - } - queryRequest - .fetch('representedGroup', { userId }) - .then((args1) => { - groupStore.handleGroupRepresented(args1); - }); - D.visible = true; - applyUserDialogLocation(true); - } - }); - showUserDialogHistory.delete(userId); - showUserDialogHistory.add(userId); - searchStore.setQuickSearchItems(searchStore.quickSearchUserHistory()); - } - - /** - * - * @param {object} ref - */ - function onPlayerTraveling(ref) { - if ( - !gameStore.isGameRunning || - !locationStore.lastLocation.location || - locationStore.lastLocation.location !== ref.travelingToLocation || - ref.id === currentUser.value.id || - locationStore.lastLocation.playerList.has(ref.id) - ) { - return; - } - - const onPlayerJoining = { - created_at: new Date(ref.created_at).toJSON(), - userId: ref.id, - displayName: ref.displayName, - type: 'OnPlayerJoining' - }; - notificationStore.queueFeedNoty(onPlayerJoining); - } - - /** - * * @param {boolean} updateInstanceOccupants */ function applyUserDialogLocation(updateInstanceOccupants = false) { @@ -1036,7 +477,6 @@ export const useUserStore = defineStore('User', () => { } /** - * * @param array */ function sortUserDialogAvatars(array) { @@ -1052,247 +492,12 @@ export const useUserStore = defineStore('User', () => { } /** - * - * @param fileId - */ - async function refreshUserDialogAvatars(fileId) { - const D = userDialog.value; - const userId = D.id; - if (D.isAvatarsLoading) { - return; - } - D.isAvatarsLoading = true; - if (fileId) { - D.loading = true; - } - D.avatarSorting = 'update'; - D.avatarReleaseStatus = 'all'; - const params = { - n: 50, - offset: 0, - sort: 'updated', - order: 'descending', - releaseStatus: 'all', - user: 'me' - }; - for (const ref of avatarStore.cachedAvatars.values()) { - if (ref.authorId === D.id) { - avatarStore.cachedAvatars.delete(ref.id); - } - } - const map = new Map(); - await processBulk({ - fn: avatarRequest.getAvatars, - N: -1, - params, - handle: (args) => { - for (const json of args.json) { - const ref = avatarStore.applyAvatar(json); - map.set(ref.id, ref); - } - }, - done: () => { - const array = Array.from(map.values()); - if (userId === D.id) { - sortUserDialogAvatars(array); - } - D.isAvatarsLoading = false; - if (fileId) { - D.loading = false; - for (const ref of array) { - if (extractFileId(ref.imageUrl) === fileId) { - avatarStore.showAvatarDialog(ref.id); - return; - } - } - toast.error('Own avatar not found'); - } - } - }); - } - - /** - * - * @param ref - */ - async function lookupUser(ref) { - let ctx; - if (ref.userId) { - showUserDialog(ref.userId); - return; - } - if (!ref.displayName || ref.displayName.substring(0, 3) === 'ID:') { - return; - } - const found = findUserByDisplayName(cachedUsers, ref.displayName); - if (found) { - showUserDialog(found.id); - return; - } - searchStore.setSearchText(ref.displayName); - await searchStore.searchUserByDisplayName(ref.displayName); - for (ctx of searchStore.searchUserResults) { - if (ctx.displayName === ref.displayName) { - searchStore.setSearchText(''); - searchStore.clearSearch(); - showUserDialog(ctx.id); - return; - } - } - } - - /** - * @param {object} ref - * @param {object} props - * @returns {Promise} - */ - async function handleUserUpdate(ref, props) { - await runHandleUserUpdateFlow(ref, props); - } - - /** - * - */ - function updateAutoStateChange() { - if ( - !generalSettingsStore.autoStateChangeEnabled || - !gameStore.isGameRunning || - !locationStore.lastLocation.playerList.size || - locationStore.lastLocation.location === '' || - locationStore.lastLocation.location === 'traveling' - ) { - return; - } - - const $location = parseLocation(locationStore.lastLocation.location); - let instanceType = $location.accessType; - if (instanceType === 'group') { - if ($location.groupAccessType === 'members') { - instanceType = 'groupOnly'; - } else if ($location.groupAccessType === 'plus') { - instanceType = 'groupPlus'; - } else { - instanceType = 'groupPublic'; - } - } - if ( - generalSettingsStore.autoStateChangeInstanceTypes.length > 0 && - !generalSettingsStore.autoStateChangeInstanceTypes.includes( - instanceType - ) - ) { - return; - } - - let withCompany = locationStore.lastLocation.playerList.size > 1; - if (generalSettingsStore.autoStateChangeNoFriends) { - const selectedGroups = generalSettingsStore.autoStateChangeGroups; - if (selectedGroups.length > 0) { - const groupFriendIds = new Set(); - for (const ref of favoriteStore.cachedFavorites.values()) { - if ( - ref.type === 'friend' && - selectedGroups.includes(ref.$groupKey) - ) { - groupFriendIds.add(ref.favoriteId); - } - } - for (const selectedKey of selectedGroups) { - if (selectedKey.startsWith('local:')) { - const groupName = selectedKey.slice(6); - const userIds = - favoriteStore.localFriendFavorites[groupName]; - if (userIds) { - for (let i = 0; i < userIds.length; ++i) { - groupFriendIds.add(userIds[i]); - } - } - } - } - withCompany = false; - for (const friendId of locationStore.lastLocation.friendList.keys()) { - if (groupFriendIds.has(friendId)) { - withCompany = true; - break; - } - } - } else { - withCompany = locationStore.lastLocation.friendList.size >= 1; - } - } - - const currentStatus = currentUser.value.status; - const newStatus = withCompany - ? generalSettingsStore.autoStateChangeCompanyStatus - : generalSettingsStore.autoStateChangeAloneStatus; - - if (currentStatus === newStatus) { - return; - } - - const params = { status: newStatus }; - if ( - withCompany && - generalSettingsStore.autoStateChangeCompanyDescEnabled - ) { - params.statusDescription = - generalSettingsStore.autoStateChangeCompanyDesc; - } else if ( - !withCompany && - generalSettingsStore.autoStateChangeAloneDescEnabled - ) { - params.statusDescription = - generalSettingsStore.autoStateChangeAloneDesc; - } - - userRequest.saveCurrentUser(params).then(() => { - const text = `Status automatically changed to ${newStatus}`; - if (AppDebug.errorNoty) { - toast.dismiss(AppDebug.errorNoty); - } - AppDebug.errorNoty = toast.info(text); - console.log(text); - }); - } - - /** - * - * @param data - */ - function addCustomTag(data) { - if (data.Tag) { - customUserTags.set(data.UserId, { - tag: data.Tag, - colour: data.TagColour - }); - } else { - customUserTags.delete(data.UserId); - } - const feedUpdate = { - userId: data.UserId, - colour: data.TagColour - }; - AppApi.ExecuteVrOverlayFunction( - 'updateHudFeedTag', - JSON.stringify(feedUpdate) - ); - const ref = cachedUsers.get(data.UserId); - if (typeof ref !== 'undefined') { - ref.$customTag = data.Tag; - ref.$customTagColour = data.TagColour; - } - sharedFeedStore.addTag(data.UserId, data.TagColour); - } - - /** - * */ async function initUserNotes() { state.lastNoteCheck = new Date(); state.lastDbNoteDate = null; state.notes.clear(); try { - // todo: get users from store const users = cachedUsers; const dbNotes = await database.getAllUserNotes(); for (const note of dbNotes) { @@ -1315,13 +520,12 @@ export const useUserStore = defineStore('User', () => { } /** - * */ async function getLatestUserNotes() { state.lastNoteCheck = new Date(); const params = { offset: 0, - n: 10 // start light + n: 10 }; const newNotes = new Map(); let done = false; @@ -1348,7 +552,7 @@ export const useUserStore = defineStore('User', () => { if (done || args.json.length === 0) { break; } - params.n = 100; // crank it after first run + params.n = 100; await new Promise((resolve) => { workerTimers.setTimeout(resolve, 1000); }); @@ -1356,7 +560,6 @@ export const useUserStore = defineStore('User', () => { } catch (error) { console.error('Error fetching user notes:', error); } - // todo: get users from store const users = cachedUsers; for (const note of newNotes.values()) { @@ -1376,12 +579,10 @@ export const useUserStore = defineStore('User', () => { } /** - * * @param userId * @param newNote */ async function checkNote(userId, newNote) { - // last check was more than than 5 minutes ago if ( !state.lastNoteCheck || state.lastNoteCheck.getTime() + 5 * 60 * 1000 > Date.now() @@ -1402,255 +603,6 @@ export const useUserStore = defineStore('User', () => { } /** - * - */ - function getCurrentUser() { - return request('auth/user', { - method: 'GET' - }).then((json) => { - const args = { - json - }; - authStore.handleCurrentUserUpdate(json); - return args; - }); - } - - /** - * @param {import('../types/api/user').GetCurrentUserResponse} json - * @returns {import('../types/api/user').GetCurrentUserResponse} - */ - function applyCurrentUser(json) { - authStore.setAttemptingAutoLogin(false); - let ref = currentUser.value; - runAvatarSwapFlow({ - json, - ref, - isLoggedIn: watchState.isLoggedIn - }); - if (watchState.isLoggedIn) { - for (const prop in json) { - if (typeof json[prop] !== 'undefined') { - ref[prop] = json[prop]; - } - } - } else { - ref = { - acceptedPrivacyVersion: 0, - acceptedTOSVersion: 0, - accountDeletionDate: null, - accountDeletionLog: null, - activeFriends: [], - ageVerificationStatus: '', - ageVerified: false, - allowAvatarCopying: false, - badges: [], - bio: '', - bioLinks: [], - currentAvatar: '', - currentAvatarImageUrl: '', - currentAvatarTags: [], - currentAvatarThumbnailImageUrl: '', - date_joined: '', - developerType: '', - discordDetails: { - global_name: '', - id: '' - }, - discordId: '', - displayName: '', - emailVerified: false, - fallbackAvatar: '', - friendGroupNames: [], - friendKey: '', - friends: [], - googleId: '', - hasBirthday: false, - hasDiscordFriendsOptOut: false, - hasEmail: false, - hasLoggedInFromClient: false, - hasPendingEmail: false, - hasSharedConnectionsOptOut: false, - hideContentFilterSettings: false, - homeLocation: '', - id: '', - isAdult: true, - isBoopingEnabled: false, - isFriend: false, - last_activity: '', - last_login: '', - last_mobile: null, - last_platform: '', - obfuscatedEmail: '', - obfuscatedPendingEmail: '', - oculusId: '', - offlineFriends: [], - onlineFriends: [], - pastDisplayNames: [], - picoId: '', - presence: { - avatarThumbnail: '', - currentAvatarTags: '', - debugflag: '', - displayName: '', - groups: [], - id: '', - instance: '', - instanceType: '', - platform: '', - profilePicOverride: '', - status: '', - travelingToInstance: '', - travelingToWorld: '', - userIcon: '', - world: '', - ...json.presence - }, - profilePicOverride: '', - profilePicOverrideThumbnail: '', - pronouns: '', - queuedInstance: '', - state: '', - status: '', - statusDescription: '', - statusFirstTime: false, - statusHistory: [], - steamDetails: {}, - steamId: '', - tags: [], - twoFactorAuthEnabled: false, - twoFactorAuthEnabledDate: null, - unsubscribe: false, - updated_at: '', - userIcon: '', - userLanguage: '', - userLanguageCode: '', - username: '', - viveId: '', - // VRCX - $online_for: null, - $offline_for: null, - $location_at: Date.now(), - $travelingToTime: Date.now(), - $previousAvatarSwapTime: null, - $homeLocation: {}, - $isVRCPlus: false, - $isModerator: false, - $isTroll: false, - $isProbableTroll: false, - $trustLevel: 'Visitor', - $trustClass: 'x-tag-untrusted', - $userColour: '', - $trustSortNum: 1, - $languages: [], - $locationTag: '', - $travelingToLocation: '', - ...json - }; - runFirstLoginFlow(ref); - } - - ref.$isVRCPlus = ref.tags.includes('system_supporter'); - appearanceSettingsStore.applyUserTrustLevel(ref); - applyUserLanguage(ref); - applyPresenceLocation(ref); - runPostApplySyncFlow(ref); - runHomeLocationSyncFlow(ref); - - // when isGameRunning use gameLog instead of API - const $location = parseLocation(locationStore.lastLocation.location); - const $travelingLocation = parseLocation( - locationStore.lastLocationDestination - ); - let location = locationStore.lastLocation.location; - let instanceId = $location.instanceId; - let worldId = $location.worldId; - let travelingToLocation = locationStore.lastLocationDestination; - let travelingToWorld = $travelingLocation.worldId; - let travelingToInstance = $travelingLocation.instanceId; - if (!gameStore.isGameRunning && json.presence) { - if (isRealInstance(json.presence.world)) { - location = `${json.presence.world}:${json.presence.instance}`; - } else { - location = json.presence.world; - } - if (isRealInstance(json.presence.travelingToWorld)) { - travelingToLocation = `${json.presence.travelingToWorld}:${json.presence.travelingToInstance}`; - } else { - travelingToLocation = json.presence.travelingToWorld; - } - instanceId = json.presence.instance; - worldId = json.presence.world; - travelingToInstance = json.presence.travelingToInstance; - travelingToWorld = json.presence.travelingToWorld; - } - const userRef = applyUser({ - ageVerificationStatus: json.ageVerificationStatus, - ageVerified: json.ageVerified, - allowAvatarCopying: json.allowAvatarCopying, - badges: json.badges, - bio: json.bio, - bioLinks: json.bioLinks, - currentAvatarImageUrl: json.currentAvatarImageUrl, - currentAvatarTags: json.currentAvatarTags, - currentAvatarThumbnailImageUrl: json.currentAvatarThumbnailImageUrl, - date_joined: json.date_joined, - developerType: json.developerType, - discordId: json.discordId, - displayName: json.displayName, - friendKey: json.friendKey, - // json.friendRequestStatus - missing from currentUser - id: json.id, - // instanceId - missing from currentUser - isFriend: json.isFriend, - last_activity: json.last_activity, - last_login: json.last_login, - last_mobile: json.last_mobile, - last_platform: json.last_platform, - // location - missing from currentUser - // note - missing from currentUser - // platform - not always present - profilePicOverride: json.profilePicOverride, - profilePicOverrideThumbnail: json.profilePicOverrideThumbnail, - pronouns: json.pronouns, - state: json.state, - status: json.status, - statusDescription: json.statusDescription, - tags: json.tags, - // travelingToInstance - missing from currentUser - // travelingToLocation - missing from currentUser - // travelingToWorld - missing from currentUser - userIcon: json.userIcon, - // worldId - missing from currentUser - // fallbackAvatar - gone from user - - // Location from gameLog/presence - location, - instanceId, - worldId, - travelingToLocation, - travelingToInstance, - travelingToWorld - - // $online_for: currentUser.value.$online_for, - // $offline_for: currentUser.value.$offline_for, - // $location_at: currentUser.value.$location_at, - // $travelingToTime: currentUser.value.$travelingToTime - }); - // set VRCX online/offline timers - userRef.$online_for = currentUser.value.$online_for; - userRef.$offline_for = currentUser.value.$offline_for; - userRef.$location_at = currentUser.value.$location_at; - userRef.$travelingToTime = currentUser.value.$travelingToTime; - if (json.presence?.platform) { - userRef.platform = json.presence.platform; - } - - return ref; - } - - /** - * * @param userId */ function showSendBoopDialog(userId) { @@ -1710,7 +662,6 @@ export const useUserStore = defineStore('User', () => { } /** - * */ function markCurrentUserGameStarted() { currentUser.value.$online_for = Date.now(); @@ -1719,7 +670,6 @@ export const useUserStore = defineStore('User', () => { } /** - * */ function markCurrentUserGameStopped() { currentUser.value.$online_for = 0; @@ -1728,7 +678,6 @@ export const useUserStore = defineStore('User', () => { } /** - * */ function toggleSharedConnectionsOptOut() { userRequest.saveCurrentUser({ @@ -1738,7 +687,6 @@ export const useUserStore = defineStore('User', () => { } /** - * */ function toggleDiscordFriendsOptOut() { userRequest.saveCurrentUser({ @@ -1759,18 +707,11 @@ export const useUserStore = defineStore('User', () => { customUserTags, cachedUsers, isLocalUserVrcPlusSupporter, - applyCurrentUser, - applyUser, - showUserDialog, + applyUserLanguage, + applyPresenceLocation, applyUserDialogLocation, sortUserDialogAvatars, - refreshUserDialogAvatars, - lookupUser, - updateAutoStateChange, - addCustomTag, initUserNotes, - getCurrentUser, - handleConfig, showSendBoopDialog, setUserDialogMemo, setUserDialogVisible, diff --git a/src/stores/vrcx.js b/src/stores/vrcx.js index 51bfd162..24233c85 100644 --- a/src/stores/vrcx.js +++ b/src/stores/vrcx.js @@ -32,6 +32,10 @@ import { useGalleryStore } from './gallery'; import { useGameLogStore } from './gameLog'; import { useGameStore } from './game'; import { useGroupStore } from './group'; +import { showGroupDialog } from '../coordinators/groupCoordinator'; +import { showWorldDialog } from '../coordinators/worldCoordinator'; +import { showAvatarDialog, selectAvatarWithConfirmation, selectAvatarWithoutConfirmation } from '../coordinators/avatarCoordinator'; +import { showUserDialog, addCustomTag } from '../coordinators/userCoordinator'; import { useInstanceStore } from './instance'; import { useLocationStore } from './location'; import { useModalStore } from './modal'; @@ -335,7 +339,7 @@ export const useVrcxStore = defineStore('Vrcx', () => { let entry; switch (data.MsgType) { case 'CustomTag': - userStore.addCustomTag(data); + addCustomTag(data); break; case 'ClearCustomTags': userStore.customUserTags.forEach((value, key) => { @@ -669,17 +673,17 @@ export const useVrcxStore = defineStore('Vrcx', () => { !searchStore.directAccessWorld(input.replace('world/', '')) ) { // fallback for mangled world ids - worldStore.showWorldDialog(commandArg); + showWorldDialog(commandArg); } break; case 'avatar': - avatarStore.showAvatarDialog(commandArg); + showAvatarDialog(commandArg); break; case 'user': - userStore.showUserDialog(commandArg); + showUserDialog(commandArg); break; case 'group': - groupStore.showGroupDialog(commandArg); + showGroupDialog(commandArg); break; case 'local-favorite-world': console.log('local-favorite-world', commandArg); @@ -701,7 +705,7 @@ export const useVrcxStore = defineStore('Vrcx', () => { break; } avatarRequest.getAvatar({ avatarId: avatarIdFav }).then(() => { - avatarStore.showAvatarDialog(avatarIdFav); + showAvatarDialog(avatarIdFav); addLocalAvatarFavorite( avatarIdFav, avatarGroup @@ -722,12 +726,11 @@ export const useVrcxStore = defineStore('Vrcx', () => { break; } if (advancedSettingsStore.showConfirmationOnSwitchAvatar) { - avatarStore.selectAvatarWithConfirmation(avatarId); + selectAvatarWithConfirmation(avatarId); // Makes sure the window is focused shouldFocusWindow = true; } else { - avatarStore - .selectAvatarWithoutConfirmation(avatarId) + selectAvatarWithoutConfirmation(avatarId) .then(() => { toast.success('Avatar changed via launch command'); }); diff --git a/src/stores/world.js b/src/stores/world.js index 91c8ba50..b2fe8d16 100644 --- a/src/stores/world.js +++ b/src/stores/world.js @@ -1,39 +1,12 @@ -import { nextTick, reactive, shallowReactive, watch } from 'vue'; +import { reactive, shallowReactive, watch } from 'vue'; import { defineStore } from 'pinia'; -import { toast } from 'vue-sonner'; -import { useI18n } from 'vue-i18n'; import { - checkVRChatCache, - createDefaultWorldRef, - evictMapCache, - getAvailablePlatforms, - getBundleDateSize, - getWorldMemo, - isRealInstance, - parseLocation, - sanitizeEntityJson + checkVRChatCache } from '../shared/utils'; -import { instanceRequest, queryRequest, worldRequest } from '../api'; -import { database } from '../service/database'; -import { patchWorldFromEvent } from '../queries'; -import { processBulk } from '../service/request'; -import { applyFavorite } from '../coordinators/favoriteCoordinator'; -import { useFavoriteStore } from './favorite'; -import { useInstanceStore } from './instance'; -import { useLocationStore } from './location'; -import { useUiStore } from './ui'; -import { useUserStore } from './user'; import { watchState } from '../service/watchState'; export const useWorldStore = defineStore('World', () => { - const locationStore = useLocationStore(); - const favoriteStore = useFavoriteStore(); - const instanceStore = useInstanceStore(); - const userStore = useUserStore(); - const uiStore = useUiStore(); - const { t } = useI18n(); - const worldDialog = reactive({ visible: false, loading: false, @@ -69,145 +42,14 @@ export const useWorldStore = defineStore('World', () => { worldDialog.visible = false; cachedWorlds.clear(); if (isLoggedIn) { - preloadOwnWorlds(); + import('../coordinators/worldCoordinator').then( + ({ preloadOwnWorlds }) => preloadOwnWorlds() + ); } }, { flush: 'sync' } ); - /** - * - * @param {string} tag - * @param {string} shortName - * @param options - */ - function showWorldDialog(tag, shortName = null, options = {}) { - const D = worldDialog; - const forceRefresh = Boolean(options?.forceRefresh); - const L = parseLocation(tag); - if (L.worldId === '') { - return; - } - const isMainDialogOpen = uiStore.openDialog({ - type: 'world', - id: L.worldId, - tag, - shortName - }); - D.visible = true; - if (isMainDialogOpen && D.id === L.worldId && !forceRefresh) { - uiStore.setDialogCrumbLabel('world', D.id, D.ref?.name || D.id); - instanceStore.applyWorldDialogInstances(); - nextTick(() => (D.loading = false)); - return; - } - L.shortName = shortName; - D.id = L.worldId; - D.$location = L; - D.loading = true; - D.inCache = false; - D.cacheSize = ''; - D.cacheLocked = false; - D.cachePath = ''; - D.fileAnalysis = {}; - D.rooms = []; - D.lastVisit = ''; - D.visitCount = 0; - D.timeSpent = 0; - D.isFavorite = false; - D.avatarScalingDisabled = false; - D.focusViewDisabled = false; - D.isPC = false; - D.isQuest = false; - D.isIos = false; - D.hasPersistData = false; - D.memo = ''; - const LL = parseLocation(locationStore.lastLocation.location); - let currentWorldMatch = false; - if (LL.worldId === D.id) { - currentWorldMatch = true; - } - getWorldMemo(D.id).then((memo) => { - if (memo.worldId === D.id) { - D.memo = memo.memo; - } - }); - database.getLastVisit(D.id, currentWorldMatch).then((ref) => { - if (ref.worldId === D.id) { - D.lastVisit = ref.created_at; - } - }); - database.getVisitCount(D.id).then((ref) => { - if (ref.worldId === D.id) { - D.visitCount = ref.visitCount; - } - }); - database.getTimeSpentInWorld(D.id).then((ref) => { - if (ref.worldId === D.id) { - D.timeSpent = ref.timeSpent; - } - }); - const loadWorldRequest = worldRequest.getWorld({ - worldId: L.worldId - }); - loadWorldRequest - .catch((err) => { - nextTick(() => (D.loading = false)); - D.id = null; - D.visible = false; - uiStore.jumpBackDialogCrumb(); - toast.error(t('message.world.load_failed')); - throw err; - }) - .then((args) => { - if (D.id === args.ref.id) { - D.ref = args.ref; - uiStore.setDialogCrumbLabel( - 'world', - D.id, - D.ref?.name || D.id - ); - D.visible = true; - D.loading = false; - D.isFavorite = favoriteStore.getCachedFavoritesByObjectId( - D.id - ); - if (!D.isFavorite) { - D.isFavorite = - favoriteStore.localWorldFavoritesList.includes( - D.id - ); - } - let { isPC, isQuest, isIos } = getAvailablePlatforms( - args.ref.unityPackages - ); - D.avatarScalingDisabled = args.ref?.tags.includes( - 'feature_avatar_scaling_disabled' - ); - D.focusViewDisabled = args.ref?.tags.includes( - 'feature_focus_view_disabled' - ); - D.isPC = isPC; - D.isQuest = isQuest; - D.isIos = isIos; - updateVRChatWorldCache(); - queryRequest - .fetch('worldPersistData', { - worldId: D.id - }) - .then((args) => { - if ( - args.params.worldId === worldDialog.id && - worldDialog.visible - ) { - worldDialog.hasPersistData = - args.json !== false; - } - }); - } - }); - } - /** * @param {boolean} value */ @@ -250,99 +92,12 @@ export const useWorldStore = defineStore('World', () => { } } - /** - * - * @param WorldCache - */ - function cleanupWorldCache(WorldCache) { - evictMapCache(WorldCache, 10000, () => false, { - logLabel: 'World cache cleanup' - }); - } - - /** - * - * @param {object} json - * @returns {object} ref - */ - function applyWorld(json) { - sanitizeEntityJson(json, ['name', 'description']); - let ref = cachedWorlds.get(json.id); - if (typeof ref === 'undefined') { - ref = createDefaultWorldRef(json); - cleanupWorldCache(cachedWorlds); - cachedWorlds.set(ref.id, ref); - } else { - Object.assign(ref, json); - } - ref.$isLabs = ref.tags.includes('system_labs'); - applyFavorite('world', ref.id); - const userDialog = userStore.userDialog; - if (userDialog.visible && userDialog.$location.worldId === ref.id) { - userStore.applyUserDialogLocation(); - } - if (worldDialog.visible && worldDialog.id === ref.id) { - worldDialog.ref = ref; - worldDialog.avatarScalingDisabled = ref.tags?.includes( - 'feature_avatar_scaling_disabled' - ); - worldDialog.focusViewDisabled = ref.tags?.includes( - 'feature_focus_view_disabled' - ); - instanceStore.applyWorldDialogInstances(); - for (const room of worldDialog.rooms) { - if (isRealInstance(room.tag)) { - instanceRequest.getInstance({ - worldId: worldDialog.id, - instanceId: room.id - }); - } - } - if (Object.keys(worldDialog.fileAnalysis).length === 0) { - getBundleDateSize(ref); - } - } - if (favoriteStore.localWorldFavoritesList.includes(ref.id)) { - // update db cache - database.addWorldToCache(ref); - } - patchWorldFromEvent(ref); - return ref; - } - - /** - * Preload all own worlds into cache at startup for global search. - */ - async function preloadOwnWorlds() { - const params = { - n: 50, - offset: 0, - sort: 'updated', - order: 'descending', - releaseStatus: 'all', - user: 'me' - }; - await processBulk({ - fn: (p) => worldRequest.getWorlds(p), - N: -1, - params, - handle: (args) => { - for (const json of args.json) { - applyWorld(json); - } - } - }); - } - return { worldDialog, cachedWorlds, - showWorldDialog, setWorldDialogVisible, setWorldDialogIsFavorite, setWorldDialogLoading, - updateVRChatWorldCache, - applyWorld, - preloadOwnWorlds + updateVRChatWorldCache }; }); diff --git a/src/views/Charts/components/InstanceActivityDetail.vue b/src/views/Charts/components/InstanceActivityDetail.vue index 6a4ad01e..673854d8 100644 --- a/src/views/Charts/components/InstanceActivityDetail.vue +++ b/src/views/Charts/components/InstanceActivityDetail.vue @@ -28,9 +28,10 @@ import { timeToText } from '../../../shared/utils'; import * as echarts from 'echarts'; +import { showUserDialog } from '../../../coordinators/userCoordinator'; const { isDarkMode, dtHour12 } = storeToRefs(useAppearanceSettingsStore()); - const { showUserDialog } = useUserStore(); + const { currentUser } = storeToRefs(useUserStore()); const props = defineProps({ diff --git a/src/views/Charts/components/MutualFriends.vue b/src/views/Charts/components/MutualFriends.vue index 4f28b595..86d176c8 100644 --- a/src/views/Charts/components/MutualFriends.vue +++ b/src/views/Charts/components/MutualFriends.vue @@ -275,6 +275,7 @@ useUserStore } from '../../../stores'; import { userImage, userStatusClass } from '../../../shared/utils'; + import { showUserDialog } from '../../../coordinators/userCoordinator'; import { database } from '../../../service/database'; import { watchState } from '../../../service/watchState'; @@ -290,7 +291,7 @@ const { currentUser } = storeToRefs(userStore); const { isDarkMode } = storeToRefs(appearanceStore); const cachedUsers = userStore.cachedUsers; - const showUserDialog = (userId) => userStore.showUserDialog(userId); + const fetchState = chartsStore.mutualGraphFetchState; const status = chartsStore.mutualGraphStatus; diff --git a/src/views/Favorites/FavoritesAvatar.vue b/src/views/Favorites/FavoritesAvatar.vue index 671e14e9..34743f4b 100644 --- a/src/views/Favorites/FavoritesAvatar.vue +++ b/src/views/Favorites/FavoritesAvatar.vue @@ -519,7 +519,7 @@ handleFavoriteGroup } = favoriteStore; const { avatarHistory } = storeToRefs(useAvatarStore()); - const { promptClearAvatarHistory, showAvatarDialog, applyAvatar } = useAvatarStore(); + import { promptClearAvatarHistory, showAvatarDialog, applyAvatar } from '../../coordinators/avatarCoordinator'; const { isLocalUserVrcPlusSupporter } = storeToRefs(useUserStore()); const { t } = useI18n(); diff --git a/src/views/Favorites/FavoritesWorld.vue b/src/views/Favorites/FavoritesWorld.vue index 8f6e0a6b..a58ab3bb 100644 --- a/src/views/Favorites/FavoritesWorld.vue +++ b/src/views/Favorites/FavoritesWorld.vue @@ -388,6 +388,7 @@ DropdownMenuTrigger } from '../../components/ui/dropdown-menu'; import { useAppearanceSettingsStore, useFavoriteStore, useModalStore, useWorldStore } from '../../stores'; + import { showWorldDialog } from '../../coordinators/worldCoordinator'; import { ResizableHandle, ResizablePanel, ResizablePanelGroup } from '../../components/ui/resizable'; import { favoriteRequest, worldRequest } from '../../api'; import { debounce } from '../../shared/utils'; @@ -436,7 +437,7 @@ handleFavoriteGroup, localWorldFavoritesList } = favoriteStore; - const { showWorldDialog } = useWorldStore(); + const { cardScale: worldCardScale, diff --git a/src/views/Favorites/components/FavoritesAvatarItem.vue b/src/views/Favorites/components/FavoritesAvatarItem.vue index a91d6edd..2ce1a7ae 100644 --- a/src/views/Favorites/components/FavoritesAvatarItem.vue +++ b/src/views/Favorites/components/FavoritesAvatarItem.vue @@ -170,7 +170,7 @@ const { favoriteAvatarGroups } = storeToRefs(useFavoriteStore()); const { showFavoriteDialog } = useFavoriteStore(); - const { selectAvatarWithConfirmation } = useAvatarStore(); + import { selectAvatarWithConfirmation } from '../../../coordinators/avatarCoordinator'; const { shiftHeld } = storeToRefs(useUiStore()); const { currentUser } = storeToRefs(useUserStore()); diff --git a/src/views/Favorites/components/FavoritesAvatarLocalHistoryItem.vue b/src/views/Favorites/components/FavoritesAvatarLocalHistoryItem.vue index edf5ce69..f8bcc6db 100644 --- a/src/views/Favorites/components/FavoritesAvatarLocalHistoryItem.vue +++ b/src/views/Favorites/components/FavoritesAvatarLocalHistoryItem.vue @@ -75,7 +75,7 @@ const { t } = useI18n(); const { showFavoriteDialog, getCachedFavoritesByObjectId } = useFavoriteStore(); - const { selectAvatarWithConfirmation } = useAvatarStore(); + import { selectAvatarWithConfirmation } from '../../../coordinators/avatarCoordinator'; const { currentUser } = storeToRefs(useUserStore()); const props = defineProps({ diff --git a/src/views/Favorites/dialogs/AvatarImportDialog.vue b/src/views/Favorites/dialogs/AvatarImportDialog.vue index 8ba87009..c2fc06c7 100644 --- a/src/views/Favorites/dialogs/AvatarImportDialog.vue +++ b/src/views/Favorites/dialogs/AvatarImportDialog.vue @@ -131,11 +131,12 @@ const emit = defineEmits(['update:avatarImportDialogInput']); const { t } = useI18n(); - const { showUserDialog } = useUserStore(); + const { favoriteAvatarGroups, avatarImportDialogInput, avatarImportDialogVisible, localAvatarFavoriteGroups } = storeToRefs(useFavoriteStore()); const { localAvatarFavGroupLength, getCachedFavoritesByObjectId } = useFavoriteStore(); - const { showAvatarDialog, applyAvatar } = useAvatarStore(); + import { showAvatarDialog, applyAvatar } from '../../../coordinators/avatarCoordinator'; +import { showUserDialog } from '../../../coordinators/userCoordinator'; const { showFullscreenImageDialog } = useGalleryStore(); const avatarImportDialog = ref({ diff --git a/src/views/Favorites/dialogs/FriendImportDialog.vue b/src/views/Favorites/dialogs/FriendImportDialog.vue index 63d57950..ba4ca525 100644 --- a/src/views/Favorites/dialogs/FriendImportDialog.vue +++ b/src/views/Favorites/dialogs/FriendImportDialog.vue @@ -131,12 +131,13 @@ import { favoriteRequest, userRequest } from '../../../api'; import { createColumns } from './friendImportColumns.jsx'; import { useVrcxVueTable } from '../../../lib/table/useVrcxVueTable'; +import { showUserDialog } from '../../../coordinators/userCoordinator'; const { t } = useI18n(); const emit = defineEmits(['update:friendImportDialogInput']); - const { showUserDialog } = useUserStore(); + const { favoriteFriendGroups, friendImportDialogInput, friendImportDialogVisible, localFriendFavoriteGroups } = storeToRefs(useFavoriteStore()); const { showFullscreenImageDialog } = useGalleryStore(); diff --git a/src/views/Favorites/dialogs/WorldImportDialog.vue b/src/views/Favorites/dialogs/WorldImportDialog.vue index c43ccf87..1a41ff5f 100644 --- a/src/views/Favorites/dialogs/WorldImportDialog.vue +++ b/src/views/Favorites/dialogs/WorldImportDialog.vue @@ -127,17 +127,19 @@ import { useI18n } from 'vue-i18n'; import { useFavoriteStore, useGalleryStore, useUserStore, useWorldStore } from '../../../stores'; + import { showWorldDialog } from '../../../coordinators/worldCoordinator'; import { addLocalWorldFavorite } from '../../../coordinators/favoriteCoordinator'; import { favoriteRequest, worldRequest } from '../../../api'; import { createColumns } from './worldImportColumns.jsx'; import { removeFromArray } from '../../../shared/utils'; import { useVrcxVueTable } from '../../../lib/table/useVrcxVueTable'; +import { showUserDialog } from '../../../coordinators/userCoordinator'; - const { showUserDialog } = useUserStore(); + const { favoriteWorldGroups, worldImportDialogInput, worldImportDialogVisible, localWorldFavoriteGroups } = storeToRefs(useFavoriteStore()); const { localWorldFavGroupLength, getCachedFavoritesByObjectId } = useFavoriteStore(); - const { showWorldDialog } = useWorldStore(); + const { showFullscreenImageDialog } = useGalleryStore(); const emit = defineEmits(['update:worldImportDialogInput']); diff --git a/src/views/Feed/columns.jsx b/src/views/Feed/columns.jsx index a5f930a6..71172cba 100644 --- a/src/views/Feed/columns.jsx +++ b/src/views/Feed/columns.jsx @@ -16,7 +16,8 @@ import { } from 'lucide-vue-next'; import { formatDateFilter, statusClass, timeToText } from '../../shared/utils'; import { i18n } from '../../plugin'; -import { useUserStore, useGalleryStore } from '../../stores'; +import { useGalleryStore } from '../../stores'; +import { showUserDialog } from '../../coordinators/userCoordinator'; const { t } = i18n.global; @@ -276,7 +277,7 @@ export const columns = [ header: () => t('table.feed.user'), meta: { label: () => t('table.feed.user') }, cell: ({ row }) => { - const { showUserDialog } = useUserStore(); + const original = row.original; return ( { - const { showUserDialog } = useUserStore(); + const { shiftHeld } = storeToRefs(useUiStore()); return [ diff --git a/src/views/FriendsLocations/components/FriendsLocationsCard.vue b/src/views/FriendsLocations/components/FriendsLocationsCard.vue index 300128ad..49fb93db 100644 --- a/src/views/FriendsLocations/components/FriendsLocationsCard.vue +++ b/src/views/FriendsLocations/components/FriendsLocationsCard.vue @@ -81,9 +81,10 @@ import { checkCanInvite, checkCanInviteSelf } from '../../../shared/utils/invite.js'; import Location from '../../../components/Location.vue'; +import { showUserDialog } from '../../../coordinators/userCoordinator'; const { t } = useI18n(); - const { showUserDialog, showSendBoopDialog } = useUserStore(); + const { showSendBoopDialog } = useUserStore(); const launchStore = useLaunchStore(); const { lastLocation, lastLocationDestination } = storeToRefs(useLocationStore()); const { isGameRunning } = storeToRefs(useGameStore()); diff --git a/src/views/GameLog/columns.jsx b/src/views/GameLog/columns.jsx index 19e32aff..d26fa32a 100644 --- a/src/views/GameLog/columns.jsx +++ b/src/views/GameLog/columns.jsx @@ -9,10 +9,10 @@ import { formatDateFilter, openExternalLink } from '../../shared/utils'; import { i18n } from '../../plugin'; import { useInstanceStore, - useUiStore, - useUserStore, - useWorldStore + useUiStore } from '../../stores'; +import { lookupUser } from '../../coordinators/userCoordinator'; +import { showWorldDialog } from '../../coordinators/worldCoordinator'; const { t } = i18n.global; @@ -24,8 +24,9 @@ const UNACTIONABLE_TYPES = new Set([ ]); export const createColumns = ({ getCreatedAt, onDelete, onDeletePrompt }) => { - const { showWorldDialog } = useWorldStore(); - const { lookupUser } = useUserStore(); + + + const { showPreviousInstancesInfoDialog } = useInstanceStore(); const { shiftHeld } = storeToRefs(useUiStore()); diff --git a/src/views/Moderation/columns.jsx b/src/views/Moderation/columns.jsx index 6c5a710e..29669598 100644 --- a/src/views/Moderation/columns.jsx +++ b/src/views/Moderation/columns.jsx @@ -11,11 +11,12 @@ import { storeToRefs } from 'pinia'; import { formatDateFilter } from '../../shared/utils'; import { i18n } from '../../plugin'; import { useUiStore, useUserStore } from '../../stores'; +import { showUserDialog } from '../../coordinators/userCoordinator'; const { t, te } = i18n.global; export const createColumns = ({ onDelete, onDeletePrompt }) => { - const { showUserDialog } = useUserStore(); + const { shiftHeld } = storeToRefs(useUiStore()); const { currentUser } = storeToRefs(useUserStore()); diff --git a/src/views/MyAvatars/MyAvatars.vue b/src/views/MyAvatars/MyAvatars.vue index bd00f2ae..573ee730 100644 --- a/src/views/MyAvatars/MyAvatars.vue +++ b/src/views/MyAvatars/MyAvatars.vue @@ -347,12 +347,13 @@ import ManageTagsDialog from './ManageTagsDialog.vue'; import MyAvatarCard from './components/MyAvatarCard.vue'; import configRepository from '../../service/config.js'; + import { showAvatarDialog, selectAvatarWithoutConfirmation, applyAvatar } from '../../coordinators/avatarCoordinator'; const { t } = useI18n(); const appearanceSettingsStore = useAppearanceSettingsStore(); const avatarStore = useAvatarStore(); const modalStore = useModalStore(); - const { showAvatarDialog, selectAvatarWithoutConfirmation, applyAvatar } = avatarStore; + const { currentUser } = storeToRefs(useUserStore()); const pageSizes = computed(() => appearanceSettingsStore.tablePageSizes); diff --git a/src/views/Notifications/columns.jsx b/src/views/Notifications/columns.jsx index f867e2cc..3457ea50 100644 --- a/src/views/Notifications/columns.jsx +++ b/src/views/Notifications/columns.jsx @@ -31,13 +31,14 @@ import { checkCanInvite, formatDateFilter } from '../../shared/utils'; import { i18n } from '../../plugin'; import { useGameStore, - useGroupStore, useLocationStore, useUiStore, useUserStore, - useWorldStore, useNotificationStore } from '../../stores'; +import { showUserDialog } from '../../coordinators/userCoordinator'; +import { showWorldDialog } from '../../coordinators/worldCoordinator'; +import { showGroupDialog } from '../../coordinators/groupCoordinator'; import Emoji from '../../components/Emoji.vue'; @@ -61,9 +62,8 @@ export const createColumns = ({ deleteNotificationLog, deleteNotificationLogPrompt }) => { - const { showUserDialog, showSendBoopDialog } = useUserStore(); - const { showWorldDialog } = useWorldStore(); - const { showGroupDialog } = useGroupStore(); + const { showSendBoopDialog } = useUserStore(); + const { shiftHeld } = storeToRefs(useUiStore()); const { currentUser } = storeToRefs(useUserStore()); const { lastLocation } = storeToRefs(useLocationStore()); diff --git a/src/views/PlayerList/PlayerList.vue b/src/views/PlayerList/PlayerList.vue index d2c1ec44..80e81934 100644 --- a/src/views/PlayerList/PlayerList.vue +++ b/src/views/PlayerList/PlayerList.vue @@ -196,6 +196,7 @@ import ChatboxBlacklistDialog from './dialogs/ChatboxBlacklistDialog.vue'; import Timer from '../../components/Timer.vue'; +import { showUserDialog, lookupUser } from '../../coordinators/userCoordinator'; const PhotonEventTable = defineAsyncComponent(() => import('./components/PhotonEventTable.vue')); @@ -203,7 +204,7 @@ const photonStore = usePhotonStore(); const { photonLoggingEnabled, chatboxUserBlacklist } = storeToRefs(photonStore); const { saveChatboxUserBlacklist } = photonStore; - const { showUserDialog, lookupUser } = useUserStore(); + const { showWorldDialog } = useWorldStore(); const { lastLocation } = storeToRefs(useLocationStore()); const { currentInstanceLocation, currentInstanceWorld, currentInstanceUsersData } = storeToRefs(useInstanceStore()); diff --git a/src/views/PlayerList/components/PhotonEventTable.vue b/src/views/PlayerList/components/PhotonEventTable.vue index 0c7d3caf..05bc317a 100644 --- a/src/views/PlayerList/components/PhotonEventTable.vue +++ b/src/views/PlayerList/components/PhotonEventTable.vue @@ -83,9 +83,11 @@ useVrcxStore, useWorldStore } from '../../../stores'; + import { showWorldDialog } from '../../../coordinators/worldCoordinator'; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '../../../components/ui/select'; import { createColumns } from './photonEventColumns.jsx'; import { photonEventTableTypeFilterList } from '../../../shared/constants/photon'; +import { lookupUser } from '../../../coordinators/userCoordinator'; const emit = defineEmits(['show-chatbox-blacklist']); const { t } = useI18n(); @@ -106,9 +108,9 @@ const { stringComparer } = storeToRefs(useSearchStore()); - const { lookupUser } = useUserStore(); + const { showAvatarDialog } = useAvatarStore(); - const { showWorldDialog } = useWorldStore(); + const { showGroupDialog } = useGroupStore(); const { showFullscreenImageDialog } = useGalleryStore(); const { ipcEnabled } = storeToRefs(useVrcxStore()); diff --git a/src/views/Search/Search.vue b/src/views/Search/Search.vue index 458d434b..9e06a8b7 100644 --- a/src/views/Search/Search.vue +++ b/src/views/Search/Search.vue @@ -402,13 +402,14 @@ userImage } from '../../shared/utils'; import { groupRequest, worldRequest } from '../../api'; +import { showUserDialog, refreshUserDialogAvatars } from '../../coordinators/userCoordinator'; const { randomUserColours } = storeToRefs(useAppearanceSettingsStore()); const { avatarRemoteDatabase } = storeToRefs(useAdvancedSettingsStore()); const { avatarRemoteDatabaseProviderList, avatarRemoteDatabaseProvider } = storeToRefs(useAvatarProviderStore()); const { setAvatarProvider } = useAvatarProviderStore(); const { userDialog } = storeToRefs(useUserStore()); - const { showUserDialog, refreshUserDialogAvatars } = useUserStore(); + const { showAvatarDialog, lookupAvatars, cachedAvatars } = useAvatarStore(); const { cachedWorlds, showWorldDialog } = useWorldStore(); const { showGroupDialog } = useGroupStore(); diff --git a/src/views/Sidebar/components/FriendItem.vue b/src/views/Sidebar/components/FriendItem.vue index f3b3e80e..42acd9f5 100644 --- a/src/views/Sidebar/components/FriendItem.vue +++ b/src/views/Sidebar/components/FriendItem.vue @@ -80,6 +80,7 @@ import { userImage, userStatusClass } from '../../../shared/utils'; import '@/styles/status-icon.css'; +import { showUserDialog } from '../../../coordinators/userCoordinator'; const props = defineProps({ friend: { type: Object, required: true }, @@ -89,7 +90,7 @@ const { hideNicknames } = storeToRefs(useAppearanceSettingsStore()); const { isRefreshFriendsLoading, allFavoriteFriendIds } = storeToRefs(useFriendStore()); const { confirmDeleteFriend } = useFriendStore(); - const { showUserDialog } = useUserStore(); + const { t } = useI18n(); const isFriendTraveling = computed(() => props.friend.ref?.location === 'traveling'); diff --git a/src/views/Sidebar/components/FriendsSidebar.vue b/src/views/Sidebar/components/FriendsSidebar.vue index 85008063..43909068 100644 --- a/src/views/Sidebar/components/FriendsSidebar.vue +++ b/src/views/Sidebar/components/FriendsSidebar.vue @@ -216,6 +216,7 @@ import configRepository from '../../../service/config'; import '@/styles/status-icon.css'; +import { showUserDialog } from '../../../coordinators/userCoordinator'; const { t } = useI18n(); @@ -238,7 +239,7 @@ sidebarSortMethods } = storeToRefs(appearanceSettingsStore); const { gameLogDisabled } = storeToRefs(useAdvancedSettingsStore()); - const { showUserDialog, showSendBoopDialog } = useUserStore(); + const { showSendBoopDialog } = useUserStore(); const launchStore = useLaunchStore(); const { favoriteFriendGroups, groupedByGroupKeyFavoriteFriends, localFriendFavorites } = storeToRefs(useFavoriteStore()); diff --git a/src/views/Sidebar/components/NotificationItem.vue b/src/views/Sidebar/components/NotificationItem.vue index 49962a6c..a8468b59 100644 --- a/src/views/Sidebar/components/NotificationItem.vue +++ b/src/views/Sidebar/components/NotificationItem.vue @@ -255,6 +255,8 @@ import dayjs from 'dayjs'; 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 Location from '../../../components/Location.vue'; @@ -461,13 +463,13 @@ if (userId.startsWith('grp_') || n.type?.startsWith('group.') || n.type === 'groupChange') { const groupId = userId.startsWith('grp_') ? userId : n.data?.groupId || n.details?.groupId || ''; if (groupId) { - groupStore.showGroupDialog(groupId); + showGroupDialog(groupId); return; } } if (userId) { - userStore.showUserDialog(userId); + showUserDialog(userId); return; } diff --git a/src/views/Tools/dialogs/NoteExportDialog.vue b/src/views/Tools/dialogs/NoteExportDialog.vue index f1a78ba7..856748bb 100644 --- a/src/views/Tools/dialogs/NoteExportDialog.vue +++ b/src/views/Tools/dialogs/NoteExportDialog.vue @@ -66,11 +66,12 @@ import { useVrcxVueTable } from '../../../lib/table/useVrcxVueTable'; import * as workerTimers from 'worker-timers'; +import { showUserDialog } from '../../../coordinators/userCoordinator'; const { t } = useI18n(); const { friends } = storeToRefs(useFriendStore()); - const { showUserDialog } = useUserStore(); + const { showFullscreenImageDialog } = useGalleryStore(); const props = defineProps({