Files
VRCX/src/service/websocket.js
Natsumi 3324d0d279 Upgrade to Vue3 and Element Plus (#1374)
* Update Vue devtools

* upgrade vue pinia element-plus vue-i18n, add vite

* fix: i18n

* global components

* change v-deep

* upgrade vue-lazyload

* data table

* update enlint and safe-dialog

* package.json and vite.config.js

* el-icon

* el-message

* vue 2 -> vue3 migration changes

* $pinia

* dialog

* el-popover slot

* lint

* chore

* slot

* scss

* remote state access

* misc

* jsconfig

* el-button size mini -> small

* :model-value

* ElMessageBox

* datatable

* remove v-lazyload

* template #dropdown

* mini -> small

* css

* byebye hideTooltips

* use sass-embedded

* Update SQLite, remove unneeded libraries

* Fix shift remove local avatar favorites

* Electron arm64

* arm64 support

* bye pug

* f-word vite hah

* misc

* remove safe dialog component

* Add self invite to launch dialog

* Fix errors

* Icons 1

* improve localfavorite loading performance

* improve favorites world item performance

* dialog visibility changes for Element Plus

* clear element plus error

* import performance

* revert App.vue hah

* hah

* Revert "Add self invite to launch dialog"

This reverts commit 4801cfad58.

* Toggle self invite/open in-game

* Self invite on launch dialog

* el-button icon

* el-icon

* fix user dialog tab switching logic

* fix PlayerList

* Formatting changes

* More icons

* Fix friend log table

* loading margin

* fix markdown

* fix world dialog tab switching issue

* Fixes and formatting

* fix: global i18n.t export

* fix favorites world tab not working

* Create instance, displayName

* Remove group members sort by userId

* Fix loading dialog tabs on swtich

* Star

* charts console.warn

* wip: fix charts

* wip: fix charts

* wip: charts composables

* fix favorite item tooltip warning

* Fixes and formatting

* Clean up image dialogs

* Remove unused method

* Fix platform/size border

* Fix platform/size border

* $vr

* fix friendExportDialogVisible binding

* ElMessageBox and Settings

* Login formatting

* Rename VR overlay query

* Fix image popover and userdialog badges

* Formatting

* Big buttons

* Fixes, update Cef

* Fix gameLog table nav buttons jumping around while using nav buttons

* Fix z-index

* vr overlay

* vite input add theme

* defineAsyncComponent

* ISO 639-1

* fix i18n

* clean t

* Formatting, fix calendar, rotate arrows

* Show user status when user is offline

* Fix VR overlay

* fix theme and clean up

* split InstanceActivity

* tweak

* Fix VR overlay formatting

* fix scss var

* AppDebug hahahaha

* Years

* remove reactive

* improve perf

* state hah…

* fix user rendering poblems when user object is not yet loaded

* improve perf

* Update avatar/world image uploader, licenses, remove previous images dialog (old images are now deleted)

* improve perf 1

* Suppress stray errors

* fix traveling location display issue

* Fix empty instance creator

* improve friend list refresh performance

* fix main charts

* fix chart

* Fix darkmode

* Fix avatar dialog tags

---------

Co-authored-by: pa <maplenagisa@gmail.com>
2025-09-12 10:45:24 +12:00

582 lines
19 KiB
JavaScript

import Noty from 'noty';
import * as workerTimers from 'worker-timers';
import { groupRequest } from '../api';
import { escapeTag, parseLocation } from '../shared/utils';
import {
useFriendStore,
useGalleryStore,
useGroupStore,
useInstanceStore,
useLocationStore,
useNotificationStore,
useSharedFeedStore,
useUiStore,
useUserStore
} from '../stores';
import { AppDebug } from './appConfig';
import { request } from './request';
import { watchState } from './watchState';
let webSocket = null;
let lastWebSocketMessage = '';
export function initWebsocket() {
if (!watchState.isFriendsLoaded || webSocket !== null) {
return;
}
return request('auth', {
method: 'GET'
})
.then((json) => {
const args = {
json
};
if (args.json.ok) {
connectWebSocket(args.json.token);
}
})
.catch((err) => {
console.error('WebSocket init error:', err);
});
}
/**
* @param {string} token
* @returns {void}
*/
function connectWebSocket(token) {
const userStore = useUserStore();
if (webSocket !== null) {
return;
}
const socket = new WebSocket(`${AppDebug.websocketDomain}/?auth=${token}`);
socket.onopen = () => {
if (AppDebug.debugWebSocket) {
console.log('WebSocket connected');
}
};
socket.onclose = () => {
if (webSocket === socket) {
webSocket = null;
}
try {
socket.close();
} catch (err) {
console.error('Error closing WebSocket:', err);
}
if (AppDebug.debugWebSocket) {
console.log('WebSocket closed');
}
workerTimers.setTimeout(() => {
if (
watchState.isLoggedIn &&
watchState.isFriendsLoaded &&
webSocket === null
) {
initWebsocket();
}
}, 5000);
};
socket.onerror = () => {
if (AppDebug.errorNoty) {
AppDebug.errorNoty.close();
}
AppDebug.errorNoty = new Noty({
type: 'error',
text: 'WebSocket Error'
}).show();
socket.onclose(
new CloseEvent('close', {
code: 1006, // Abnormal Closure
reason: 'WebSocket Error'
})
);
};
socket.onmessage = ({ data }) => {
try {
if (lastWebSocketMessage === data) {
// pls no spam
return;
}
lastWebSocketMessage = data;
let json;
try {
json = JSON.parse(data);
json.content = JSON.parse(json.content);
} catch {
// ignore parse error
}
handlePipeline({
json
});
if (AppDebug.debugWebSocket && json.content) {
let displayName = '';
const user = userStore.cachedUsers.get(json.content.userId);
if (user) {
displayName = user.displayName;
}
console.log('WebSocket', json.type, displayName, json.content);
}
} catch (err) {
console.error(err);
}
};
webSocket = socket;
}
/**
* @returns {void}
*/
export function closeWebSocket() {
const socket = webSocket;
if (socket === null) {
return;
}
webSocket = null;
try {
socket.close();
} catch (err) {
console.error('Error closing WebSocket:', err);
}
}
/**
* @returns {void}
*/
export function reconnectWebSocket() {
if (!watchState.isLoggedIn || !watchState.isFriendsLoaded) {
return;
}
closeWebSocket();
initWebsocket();
}
/**
* @param {object} args
*/
function handlePipeline(args) {
const userStore = useUserStore();
const locationStore = useLocationStore();
const galleryStore = useGalleryStore();
const notificationStore = useNotificationStore();
const sharedFeedStore = useSharedFeedStore();
const friendStore = useFriendStore();
const groupStore = useGroupStore();
const uiStore = useUiStore();
const instanceStore = useInstanceStore();
const { type, content, err } = args.json;
if (typeof err !== 'undefined') {
console.error('PIPELINE: error', args);
if (AppDebug.errorNoty) {
AppDebug.errorNoty.close();
}
AppDebug.errorNoty = new Noty({
type: 'error',
text: escapeTag(`WebSocket Error: ${err}`)
}).show();
return;
}
if (typeof content === 'undefined') {
console.error('PIPELINE: missing content', args);
return;
}
if (typeof content.user !== 'undefined') {
// I forgot about this...
delete content.user.state;
}
switch (type) {
case 'notification':
notificationStore.handleNotification({
json: content,
params: {
notificationId: content.id
}
});
notificationStore.handlePipelineNotification({
json: content,
params: {
notificationId: content.id
}
});
break;
case 'notification-v2':
console.log('notification-v2', content);
notificationStore.handleNotificationV2({
json: content,
params: {
notificationId: content.id
}
});
break;
case 'notification-v2-delete':
console.log('notification-v2-delete', content);
for (var id of content.ids) {
notificationStore.handleNotificationHide({
params: {
notificationId: id
}
});
notificationStore.handleNotificationSee({
params: {
notificationId: id
}
});
}
break;
case 'notification-v2-update':
console.log('notification-v2-update', content);
notificationStore.handleNotificationV2Update({
json: content.updates,
params: {
notificationId: content.id
}
});
break;
case 'see-notification':
notificationStore.handleNotificationSee({
params: {
notificationId: content
}
});
break;
case 'hide-notification':
notificationStore.handleNotificationHide({
params: {
notificationId: content
}
});
notificationStore.handleNotificationSee({
params: {
notificationId: content
}
});
break;
case 'response-notification':
notificationStore.handleNotificationHide({
params: {
notificationId: content.notificationId
}
});
notificationStore.handleNotificationSee({
params: {
notificationId: content.notificationId
}
});
break;
case 'friend-add':
userStore.applyUser(content.user);
friendStore.handleFriendAdd({
params: {
userId: content.userId
}
});
break;
case 'friend-delete':
friendStore.handleFriendDelete({
params: {
userId: content.userId
}
});
break;
case 'friend-online':
// Where is instanceId, travelingToWorld, travelingToInstance?
// More JANK, what a mess
const $location = parseLocation(content.location);
const $travelingToLocation = parseLocation(
content.travelingToLocation
);
if (content?.user?.id) {
const onlineJson = {
id: content.userId,
platform: content.platform,
state: 'online',
location: content.location,
worldId: content.worldId,
instanceId: $location.instanceId,
travelingToLocation: content.travelingToLocation,
travelingToWorld: $travelingToLocation.worldId,
travelingToInstance: $travelingToLocation.instanceId,
...content.user
};
userStore.applyUser(onlineJson);
} else {
console.error('friend-online missing user id', content);
friendStore.updateFriend(content.userId, 'online');
}
break;
case 'friend-active':
if (content?.user?.id) {
const activeJson = {
id: content.userId,
platform: content.platform,
state: 'active',
location: 'offline',
worldId: 'offline',
instanceId: 'offline',
travelingToLocation: 'offline',
travelingToWorld: 'offline',
travelingToInstance: 'offline',
...content.user
};
userStore.applyUser(activeJson);
} else {
console.error('friend-active missing user id', content);
friendStore.updateFriend(content.userId, 'active');
}
break;
case 'friend-offline':
// more JANK, hell yeah
const offlineJson = {
id: content.userId,
platform: content.platform,
state: 'offline',
location: 'offline',
worldId: 'offline',
instanceId: 'offline',
travelingToLocation: 'offline',
travelingToWorld: 'offline',
travelingToInstance: 'offline'
};
userStore.applyUser(offlineJson);
break;
case 'friend-update':
userStore.applyUser(content.user);
break;
case 'friend-location':
const $location1 = parseLocation(content.location);
const $travelingToLocation1 = parseLocation(
content.travelingToLocation
);
if (!content?.user?.id) {
console.error('friend-location missing user id', content);
const jankLocationJson = {
id: content.userId,
location: content.location,
worldId: content.worldId,
instanceId: $location1.instanceId,
travelingToLocation: content.travelingToLocation,
travelingToWorld: $travelingToLocation1.worldId,
travelingToInstance: $travelingToLocation1.instanceId
};
userStore.applyUser(jankLocationJson);
break;
}
const locationJson = {
location: content.location,
worldId: content.worldId,
instanceId: $location1.instanceId,
travelingToLocation: content.travelingToLocation,
travelingToWorld: $travelingToLocation1.worldId,
travelingToInstance: $travelingToLocation1.instanceId,
...content.user,
state: 'online' // JANK
};
userStore.applyUser(locationJson);
break;
case 'user-update':
userStore.applyCurrentUser(content.user);
break;
case 'user-location':
// update current user location
if (content.userId !== userStore.currentUser.id) {
console.error('user-location wrong userId', content);
break;
}
// content.user: {} // we don't trust this
// content.world: {} // this is long gone
// content.worldId // where did worldId go?
// content.instance // without worldId, this is useless
locationStore.setCurrentUserLocation(
content.location,
content.travelingToLocation
);
break;
case 'group-joined':
// var groupId = content.groupId;
// $app.onGroupJoined(groupId);
break;
case 'group-left':
// var groupId = content.groupId;
// $app.onGroupLeft(groupId);
break;
case 'group-role-updated':
const groupId = content.role.groupId;
groupRequest
.getGroup({ groupId, includeRoles: true })
.then((args) => groupStore.applyGroup(args.json));
console.log('group-role-updated', content);
// content {
// role: {
// createdAt: string,
// description: string,
// groupId: string,
// id: string,
// isManagementRole: boolean,
// isSelfAssignable: boolean,
// name: string,
// order: number,
// permissions: string[],
// requiresPurchase: boolean,
// requiresTwoFactor: boolean
break;
case 'group-member-updated':
var member = content.member;
if (!member) {
console.error('group-member-updated missing member', content);
break;
}
const groupId1 = member.groupId;
if (
groupStore.groupDialog.visible &&
groupStore.groupDialog.id === groupId1
) {
groupStore.getGroupDialogGroup(groupId1);
}
groupStore.handleGroupMember({
json: member,
params: {
groupId: groupId1
}
});
console.log('group-member-updated', member);
break;
case 'instance-queue-joined':
case 'instance-queue-position':
var instanceId = content.instanceLocation;
var position = content.position ?? 0;
var queueSize = content.queueSize ?? 0;
instanceStore.instanceQueueUpdate(instanceId, position, queueSize);
break;
case 'instance-queue-ready':
// var expiry = Date.parse(content.expiry);
instanceStore.instanceQueueReady(content.instanceLocation);
break;
case 'instance-queue-left':
instanceStore.removeQueuedInstance(content.instanceLocation);
// $app.instanceQueueClear();
break;
case 'content-refresh':
var contentType = content.contentType;
console.log('content-refresh', content);
if (contentType === 'icon') {
if (
galleryStore.galleryDialogVisible &&
!galleryStore.galleryDialogIconsLoading
) {
galleryStore.refreshVRCPlusIconsTable();
}
} else if (contentType === 'gallery') {
if (
galleryStore.galleryDialogVisible &&
!galleryStore.galleryDialogGalleryLoading
) {
galleryStore.refreshGalleryTable();
}
} else if (contentType === 'emoji') {
if (
galleryStore.galleryDialogVisible &&
!galleryStore.galleryDialogEmojisLoading
) {
galleryStore.refreshEmojiTable();
}
} else if (contentType === 'sticker') {
// on sticker upload
} else if (contentType === 'print') {
if (content.actionType === 'created') {
galleryStore.tryDeleteOldPrints();
} else if (
galleryStore.galleryDialogVisible &&
!galleryStore.galleryDialogPrintsLoading
) {
galleryStore.refreshPrintTable();
}
} else if (contentType === 'prints') {
// lol
} else if (contentType === 'avatar') {
// hmm, utilizing this might be too spamy and cause UI to move around
} else if (contentType === 'world') {
// hmm
} else if (contentType === 'created') {
// on avatar upload, might be gone now
} else if (contentType === 'avatargallery') {
// on avatar gallery image upload
} else if (contentType === 'invitePhoto') {
// on uploading invite photo
} else if (contentType === 'inventory') {
if (
galleryStore.galleryDialogVisible &&
!galleryStore.galleryDialogInventoryLoading
) {
galleryStore.getInventory();
}
// on consuming a bundle
// {contentType: 'inventory', itemId: 'inv_', itemType: 'prop', actionType: 'add'}
} else if (!contentType) {
console.log('content-refresh without contentType', content);
} else {
console.log(
'Unknown content-refresh type',
content.contentType
);
}
break;
case 'instance-closed':
// TODO: get worldName, groupName, hardClose
const noty = {
type: 'instance.closed',
location: content.instanceLocation,
message: 'Instance Closed',
created_at: new Date().toJSON()
};
if (
notificationStore.notificationTable.filters[0].value.length ===
0 ||
notificationStore.notificationTable.filters[0].value.includes(
noty.type
)
) {
uiStore.notifyMenu('notification');
}
notificationStore.queueNotificationNoty(noty);
notificationStore.notificationTable.data.push(noty);
sharedFeedStore.updateSharedFeed(true);
break;
default:
console.log('Unknown pipeline type', args.json);
}
}