Files
VRCX/src/app.js
2025-05-27 20:45:55 +10:00

13953 lines
467 KiB
JavaScript

// Copyright(c) 2019-2025 pypy, Natsumi and individual contributors.
// All rights reserved.
//
// This work is licensed under the terms of the MIT license.
// For a copy, see <https://opensource.org/licenses/MIT>.
// #region | Imports
import '@fontsource/noto-sans-kr';
import '@fontsource/noto-sans-jp';
import '@fontsource/noto-sans-sc';
import '@fontsource/noto-sans-tc';
import '@infolektuell/noto-color-emoji';
import Noty from 'noty';
import Vue from 'vue';
import VueLazyload from 'vue-lazyload';
import VueI18n from 'vue-i18n';
import { createI18n } from 'vue-i18n-bridge';
import { DataTables } from 'vue-data-tables';
import ElementUI from 'element-ui';
import dayjs from 'dayjs';
import duration from 'dayjs/plugin/duration';
import utc from 'dayjs/plugin/utc';
import timezone from 'dayjs/plugin/timezone';
import isSameOrAfter from 'dayjs/plugin/isSameOrAfter';
import * as workerTimers from 'worker-timers';
import 'default-passive-events';
// util classes
import configRepository from './service/config.js';
import webApiService from './service/webapi.js';
import security from './service/security.js';
import database from './service/database.js';
import * as localizedStrings from './localization/localizedStrings.js';
import removeConfusables, { removeWhitespace } from './service/confusables.js';
import $utils from './classes/utils.js';
import _apiInit from './classes/apiInit.js';
import _apiRequestHandler from './classes/apiRequestHandler.js';
import _vrcxJsonStorage from './classes/vrcxJsonStorage.js';
import {
userRequest,
worldRequest,
instanceRequest,
friendRequest,
avatarRequest,
notificationRequest,
playerModerationRequest,
avatarModerationRequest,
favoriteRequest,
vrcPlusIconRequest,
inviteMessagesRequest,
miscRequest,
imageRequest,
vrcPlusImageRequest,
groupRequest
} from './api';
import { userDialogGroupSortingOptions } from './composables/user/constants/userDialogGroupSortingOptions';
import {
getPrintFileName,
getPrintLocalDate,
languageClass
} from './composables/user/utils';
import {
compareUnityVersion,
getPlatformInfo,
storeAvatarImage
} from './composables/avatar/utils';
import { displayLocation } from './composables/instance/utils';
import LoginPage from './views/Login/Login.vue';
// tabs
import ModerationTab from './views/Moderation/Moderation.vue';
import ChartsTab from './views/Charts/Charts.vue';
import SideBar from './views/SideBar/SideBar.vue';
import NavMenu from './components/NavMenu.vue';
import FriendListTab from './views/FriendList/FriendList.vue';
import FavoritesTab from './views/Favorites/Favorites.vue';
import FriendLogTab from './views/FriendLog/FriendLog.vue';
import GameLogTab from './views/GameLog/GameLog.vue';
import NotificationTab from './views/Notifications/Notification.vue';
import FeedTab from './views/Feed/Feed.vue';
import SearchTab from './views/Search/Search.vue';
import ProfileTab from './views/Profile/Profile.vue';
import PlayerListTab from './views/PlayerList/PlayerList.vue';
// components
import SimpleSwitch from './components/SimpleSwitch.vue';
import Location from './components/Location.vue';
// dialogs
import WorldDialog from './components/dialogs/WorldDialog/WorldDialog.vue';
import PreviousInstancesInfoDialog from './components/dialogs/PreviousInstancesDialog/PreviousInstancesInfoDialog.vue';
import FriendImportDialog from './views/Favorites/dialogs/FriendImportDialog.vue';
import WorldImportDialog from './views/Favorites/dialogs/WorldImportDialog.vue';
import AvatarImportDialog from './views/Favorites/dialogs/AvatarImportDialog.vue';
import LaunchDialog from './components/dialogs/LaunchDialog.vue';
import ChooseFavoriteGroupDialog from './components/dialogs/ChooseFavoriteGroupDialog.vue';
import ExportFriendsListDialog from './views/Profile/dialogs/ExportFriendsListDialog.vue';
import ExportAvatarsListDialog from './views/Profile/dialogs/ExportAvatarsListDialog.vue';
import UserDialog from './components/dialogs/UserDialog/UserDialog.vue';
import GroupDialog from './components/dialogs/GroupDialog/GroupDialog.vue';
import InviteGroupDialog from './components/dialogs/InviteGroupDialog.vue';
import AvatarDialog from './components/dialogs/AvatarDialog/AvatarDialog.vue';
import FeedFiltersDialog from './views/Settings/dialogs/FeedFiltersDialog.vue';
import LaunchOptionsDialog from './views/Settings/dialogs/LaunchOptionsDialog.vue';
import OpenSourceSoftwareNoticeDialog from './views/Settings/dialogs/OpenSourceSoftwareNoticeDialog.vue';
import ChangelogDialog from './views/Settings/dialogs/ChangelogDialog.vue';
import VRCXUpdateDialog from './components/dialogs/VRCXUpdateDialog.vue';
import ScreenshotMetadataDialog from './views/Settings/dialogs/ScreenshotMetadataDialog.vue';
import DiscordNamesDialog from './views/Profile/dialogs/DiscordNamesDialog.vue';
import EditInviteMessageDialog from './views/Profile/dialogs/EditInviteMessageDialog.vue';
import NoteExportDialog from './views/Settings/dialogs/NoteExportDialog.vue';
import VRChatConfigDialog from './views/Settings/dialogs/VRChatConfigDialog.vue';
import YouTubeApiDialog from './views/Settings/dialogs/YouTubeApiDialog.vue';
import NotificationPositionDialog from './views/Settings/dialogs/NotificationPositionDialog.vue';
import AvatarProviderDialog from './views/Settings/dialogs/AvatarProviderDialog.vue';
import RegistryBackupDialog from './views/Settings/dialogs/RegistryBackupDialog.vue';
import PrimaryPasswordDialog from './views/Settings/dialogs/PrimaryPasswordDialog.vue';
import ChatboxBlacklistDialog from './views/PlayerList/dialogs/ChatboxBlacklistDialog.vue';
import FullscreenImageDialog from './components/dialogs/FullscreenImageDialog.vue';
import SafeDialog from './components/dialogs/SafeDialog.vue';
import { hasGroupPermission } from './composables/group/utils';
import { isRealInstance, parseLocation } from './composables/instance/utils';
import {
checkVRChatCache,
convertFileUrlToImageUrl,
deleteVRChatCache,
extractFileId,
extractFileVersion,
getAvailablePlatforms,
_utils
} from './composables/shared/utils';
// main app classes
import _sharedFeed from './classes/sharedFeed.js';
import _prompts from './classes/prompts.js';
import _vrcxNotifications from './classes/vrcxNotifications.js';
import _uiComponents from './classes/uiComponents.js';
import _websocket from './classes/websocket.js';
import _apiLogin from './classes/apiLogin.js';
import _currentUser from './classes/currentUser.js';
import _updateLoop from './classes/updateLoop.js';
import _discordRpc from './classes/discordRpc.js';
import _vrcxUpdater from './classes/vrcxUpdater.js';
import _gameLog from './classes/gameLog.js';
import _gameRealtimeLogging from './classes/gameRealtimeLogging.js';
import _feed from './classes/feed.js';
import _memos from './classes/memos.js';
import _languages from './classes/languages.js';
import _groups from './classes/groups.js';
import _vrcRegistry from './classes/vrcRegistry.js';
import _restoreFriendOrder from './classes/restoreFriendOrder.js';
import pugTemplate from './app.pug';
// API classes
import _config from './classes/API/config.js';
// #endregion
// some workaround for failing to get voice list first run
speechSynthesis.getVoices();
import InteropApi from './ipc-electron/interopApi.js';
console.log(`isLinux: ${LINUX}`);
// #region | Hey look it's most of VRCX!
(async function () {
// #region | Init Cef C# bindings
if (WINDOWS) {
await CefSharp.BindObjectAsync(
'AppApi',
'WebApi',
'SharedVariable',
'VRCXStorage',
'SQLite',
'LogWatcher',
'Discord',
'AssetBundleManager'
);
} else {
window.AppApi = InteropApi.AppApiElectron;
window.WebApi = InteropApi.WebApi;
window.SharedVariable = InteropApi.SharedVariable;
window.VRCXStorage = InteropApi.VRCXStorage;
window.SQLite = InteropApi.SQLiteLegacy;
window.LogWatcher = InteropApi.LogWatcher;
window.Discord = InteropApi.Discord;
window.AssetBundleManager = InteropApi.AssetBundleManager;
}
// #region | localization
Vue.use(VueI18n, { bridge: true });
const i18n = createI18n(
{
locale: 'en',
fallbackLocale: 'en',
messages: localizedStrings,
legacy: false,
globalInjection: true,
missingWarn: false,
warnHtmlMessage: false,
fallbackWarn: false
},
VueI18n
);
const $t = i18n.global.t;
Vue.use(i18n);
Vue.use(ElementUI, {
i18n: (key, value) => i18n.global.t(key, value)
});
// #endregion
// everything in this program is global stored in $app, I hate it, it is what it is
let $app = {};
const API = new _apiInit($app);
const vrcxJsonStorage = new _vrcxJsonStorage(VRCXStorage);
let vrcxClasses = {
// other classes
API,
apiRequestHandler: new _apiRequestHandler($app, API, $t, webApiService),
uiComponents: new _uiComponents($app, API, $t),
webSocket: new _websocket($app, API, $t),
// main classes
sharedFeed: new _sharedFeed($app, API, $t),
prompts: new _prompts($app, API, $t),
vrcxNotifications: new _vrcxNotifications($app, API, $t),
apiLogin: new _apiLogin($app, API, $t, webApiService),
currentUser: new _currentUser($app, API, $t),
updateLoop: new _updateLoop($app, API, $t),
discordRpc: new _discordRpc($app, API, $t),
vrcxUpdater: new _vrcxUpdater($app, API, $t),
gameLog: new _gameLog($app, API, $t),
gameRealtimeLogging: new _gameRealtimeLogging($app, API, $t),
feed: new _feed($app, API, $t),
memos: new _memos($app, API, $t),
config: new _config($app, API, $t),
languages: new _languages($app, API, $t),
groups: new _groups($app, API, $t),
vrcRegistry: new _vrcRegistry($app, API, $t),
restoreFriendOrder: new _restoreFriendOrder($app, API, $t)
};
await configRepository.init();
const app = {
template: pugTemplate,
data: {
API,
isGameRunning: false,
isGameNoVR: true,
isSteamVRRunning: false,
isHmdAfk: false,
isRunningUnderWine: false,
appVersion: '',
latestAppVersion: '',
shiftHeld: false
},
i18n,
computed: {},
methods: {
...$utils
},
watch: {},
components: {
LoginPage,
// tabs
ModerationTab,
ChartsTab,
FriendListTab,
FavoritesTab,
NotificationTab,
SearchTab,
// - others
SideBar,
NavMenu,
FriendLogTab,
GameLogTab,
FeedTab,
ProfileTab,
PlayerListTab,
// components
// - common
Location,
// - settings
SimpleSwitch,
// - dialogs
// - previous instances
PreviousInstancesInfoDialog,
UserDialog,
// - world
WorldDialog,
// - group
GroupDialog,
InviteGroupDialog,
// - avatar
AvatarDialog,
// - favorites
FriendImportDialog,
WorldImportDialog,
AvatarImportDialog,
// - favorites dialog
ChooseFavoriteGroupDialog,
ExportFriendsListDialog,
ExportAvatarsListDialog,
// - launch
LaunchDialog,
// - player list
ChatboxBlacklistDialog,
// - profile
DiscordNamesDialog,
// - settings
FeedFiltersDialog,
LaunchOptionsDialog,
OpenSourceSoftwareNoticeDialog,
ChangelogDialog,
VRCXUpdateDialog,
ScreenshotMetadataDialog,
EditInviteMessageDialog,
NoteExportDialog,
VRChatConfigDialog,
YouTubeApiDialog,
NotificationPositionDialog,
AvatarProviderDialog,
RegistryBackupDialog,
PrimaryPasswordDialog,
FullscreenImageDialog
},
provide() {
return {
API,
friends: this.friends,
showUserDialog: this.showUserDialog,
adjustDialogZ: this.adjustDialogZ,
getWorldName: this.getWorldName,
userImage: this.userImage,
userStatusClass: this.userStatusClass,
getGroupName: this.getGroupName,
userImageFull: this.userImageFull,
showFullscreenImageDialog: this.showFullscreenImageDialog,
statusClass: this.statusClass,
openExternalLink: this.openExternalLink,
showWorldDialog: this.showWorldDialog,
showAvatarDialog: this.showAvatarDialog,
showPreviousInstancesInfoDialog:
this.showPreviousInstancesInfoDialog,
showLaunchDialog: this.showLaunchDialog,
showFavoriteDialog: this.showFavoriteDialog,
displayPreviousImages: this.displayPreviousImages,
languageClass: this.languageClass,
showGroupDialog: this.showGroupDialog,
showGallerySelectDialog: this.showGallerySelectDialog,
showGalleryDialog: this.showGalleryDialog,
getImageUrlFromImageId: this.getImageUrlFromImageId,
getAvatarGallery: this.getAvatarGallery,
inviteImageUpload: this.inviteImageUpload,
clearInviteImageUpload: this.clearInviteImageUpload,
isLinux: this.isLinux
};
},
el: '#root',
beforeMount() {
this.changeThemeMode();
},
async mounted() {
await this.initLanguage();
try {
this.isRunningUnderWine = await AppApi.IsRunningUnderWine();
} catch (err) {
console.error(err);
}
await AppApi.SetUserAgent();
await this.loadVrcxId();
this.appVersion = await AppApi.GetVersion();
await this.compareAppVersion();
await this.setBranch();
if (this.autoUpdateVRCX !== 'Off') {
this.checkForVRCXUpdate();
}
await AppApi.CheckGameRunning();
this.isGameNoVR = await configRepository.getBool('isGameNoVR');
await AppApi.SetAppLauncherSettings(
this.enableAppLauncher,
this.enableAppLauncherAutoClose
);
API.$on('SHOW_WORLD_DIALOG_SHORTNAME', (tag) =>
this.verifyShortName('', tag)
);
this.updateLoop();
this.getGameLogTable();
this.refreshCustomCss();
this.refreshCustomScript();
this.checkVRChatDebugLogging();
this.checkAutoBackupRestoreVrcRegistry();
await this.migrateStoredUsers();
this.loginForm.savedCredentials =
(await configRepository.getString('savedCredentials')) !== null
? JSON.parse(
await configRepository.getString('savedCredentials')
)
: {};
this.loginForm.lastUserLoggedIn =
await configRepository.getString('lastUserLoggedIn');
this.$nextTick(async function () {
this.$el.style.display = '';
if (
!this.enablePrimaryPassword &&
(await configRepository.getString('lastUserLoggedIn')) !==
null
) {
var user =
this.loginForm.savedCredentials[
this.loginForm.lastUserLoggedIn
];
if (user?.loginParmas?.endpoint) {
API.endpointDomain = user.loginParmas.endpoint;
API.websocketDomain = user.loginParmas.websocket;
}
// login at startup
this.loginForm.loading = true;
API.getConfig()
.catch((err) => {
this.loginForm.loading = false;
throw err;
})
.then((args) => {
API.getCurrentUser()
.finally(() => {
this.loginForm.loading = false;
})
.catch((err) => {
this.nextCurrentUserRefresh = 60; // 1min
console.error(err);
});
return args;
});
} else {
this.loginForm.loading = false;
}
});
if (LINUX) {
setTimeout(() => {
this.updateTTSVoices();
}, 5000);
}
}
};
for (let value of Object.values(vrcxClasses)) {
app.methods = { ...app.methods, ...value._methods };
app.data = { ...app.data, ...value._data };
}
app.methods = { ...app.methods, ..._utils };
Object.assign($app, app);
// #endregion
// #region | Init: drop/keyup event listeners
// Make sure file drops outside of the screenshot manager don't navigate to the file path dropped.
// This issue persists on prompts created with prompt(), unfortunately. Not sure how to fix that.
document.body.addEventListener('drop', function (e) {
e.preventDefault();
});
document.addEventListener('keydown', function (e) {
if (e.shiftKey) {
$app.shiftHeld = true;
}
});
document.addEventListener('keyup', function (e) {
if (e.ctrlKey) {
if (e.key === 'I') {
$app.showConsole();
} else if (e.key === 'r') {
location.reload();
}
} else if (e.altKey && e.key === 'R') {
$app.refreshCustomCss();
}
if (!e.shiftKey) {
$app.shiftHeld = false;
}
});
addEventListener('wheel', (event) => {
if (event.ctrlKey) {
$app.getZoomLevel();
}
});
// #endregion
// #region | Init: Noty, Vue, Vue-Markdown, ElementUI, VueI18n, VueLazyLoad, Vue filters, dark stylesheet, dayjs
Noty.overrideDefaults({
animation: {
open: 'animate__animated animate__bounceInLeft',
close: 'animate__animated animate__bounceOutLeft'
},
layout: 'bottomLeft',
theme: 'mint',
timeout: 6000
});
Vue.filter('commaNumber', $utils.commaNumber);
Vue.filter('textToHex', $utils.textToHex);
Vue.use(VueLazyload, {
preLoad: 1,
observer: true,
observerOptions: {
rootMargin: '0px',
threshold: 0
},
attempt: 3
});
Vue.use(DataTables);
Vue.component('safe-dialog', SafeDialog);
dayjs.extend(duration);
dayjs.extend(utc);
dayjs.extend(timezone);
dayjs.extend(isSameOrAfter);
// #endregion
// #endregion
// #region | API: This is NOT all the api functions, not even close :(
// #region | API: User
// changeUserName: PUT users/${userId} {displayName: string, currentPassword: string}
// changeUserEmail: PUT users/${userId} {email: string, currentPassword: string}
// changePassword: PUT users/${userId} {password: string, currentPassword: string}
// updateTOSAggreement: PUT users/${userId} {acceptedTOSVersion: number}
// 2FA
// removeTwoFactorAuth: DELETE auth/twofactorauth
// getTwoFactorAuthpendingSecret: POST auth/twofactorauth/totp/pending -> { qrCodeDataUrl: string, secret: string }
// verifyTwoFactorAuthPendingSecret: POST auth/twofactorauth/totp/pending/verify { code: string } -> { verified: bool, enabled: bool }
// cancelVerifyTwoFactorAuthPendingSecret: DELETE auth/twofactorauth/totp/pending
// getTwoFactorAuthOneTimePasswords: GET auth/user/twofactorauth/otp -> { otp: [ { code: string, used: bool } ] }
// Account Link
// merge: PUT auth/user/merge {mergeToken: string}
// 링크됐다면 CurrentUser에 steamId, oculusId 값이 생기는듯
// 스팀 계정으로 로그인해도 steamId, steamDetails에 값이 생김
// Password Recovery
// sendLink: PUT auth/password {email: string}
// setNewPassword: PUT auth/password {emailToken: string, id: string, password: string}
API.cachedUsers = new Map();
API.currentTravelers = new Map();
API.$on('USER:CURRENT:SAVE', function (args) {
this.$emit('USER:CURRENT', args);
});
API.$on('USER', function (args) {
if (!args?.json?.id) {
console.error('API.$on(USER) invalid args', args);
return;
}
if (args.json.state === 'online') {
args.ref = this.applyUser(args.json); // GPS
$app.updateFriend({ id: args.json.id, state: args.json.state }); // online/offline
} else {
$app.updateFriend({ id: args.json.id, state: args.json.state }); // online/offline
args.ref = this.applyUser(args.json); // GPS
}
});
API.$on('USER:LIST', function (args) {
for (var json of args.json) {
if (!json.displayName) {
console.error('getUsers gave us garbage', json);
continue;
}
this.$emit('USER', {
json,
params: {
userId: json.id
}
});
}
});
API.applyUserTrustLevel = function (ref) {
ref.$isModerator = ref.developerType && ref.developerType !== 'none';
ref.$isTroll = false;
ref.$isProbableTroll = false;
var trustColor = '';
var { tags } = ref;
if (tags.includes('admin_moderator')) {
ref.$isModerator = true;
}
if (tags.includes('system_troll')) {
ref.$isTroll = true;
}
if (tags.includes('system_probable_troll') && !ref.$isTroll) {
ref.$isProbableTroll = true;
}
if (tags.includes('system_trust_veteran')) {
ref.$trustLevel = 'Trusted User';
ref.$trustClass = 'x-tag-veteran';
trustColor = 'veteran';
ref.$trustSortNum = 5;
} else if (tags.includes('system_trust_trusted')) {
ref.$trustLevel = 'Known User';
ref.$trustClass = 'x-tag-trusted';
trustColor = 'trusted';
ref.$trustSortNum = 4;
} else if (tags.includes('system_trust_known')) {
ref.$trustLevel = 'User';
ref.$trustClass = 'x-tag-known';
trustColor = 'known';
ref.$trustSortNum = 3;
} else if (tags.includes('system_trust_basic')) {
ref.$trustLevel = 'New User';
ref.$trustClass = 'x-tag-basic';
trustColor = 'basic';
ref.$trustSortNum = 2;
} else {
ref.$trustLevel = 'Visitor';
ref.$trustClass = 'x-tag-untrusted';
trustColor = 'untrusted';
ref.$trustSortNum = 1;
}
if (ref.$isTroll || ref.$isProbableTroll) {
trustColor = 'troll';
ref.$trustSortNum += 0.1;
}
if (ref.$isModerator) {
trustColor = 'vip';
ref.$trustSortNum += 0.3;
}
if ($app.randomUserColours && $app.friendLogInitStatus) {
if (!ref.$userColour) {
$app.getNameColour(ref.id).then((colour) => {
ref.$userColour = colour;
});
}
} else {
ref.$userColour = $app.trustColor[trustColor];
}
};
API.applyUserLanguage = function (ref) {
if (!ref || !ref.tags || !$app.subsetOfLanguages) {
return;
}
ref.$languages = [];
const languagePrefix = 'language_';
const prefixLength = languagePrefix.length;
for (const tag of ref.tags) {
if (tag.startsWith(languagePrefix)) {
const key = tag.substring(prefixLength);
const value = $app.subsetOfLanguages[key];
if (value !== undefined) {
ref.$languages.push({ key, value });
}
}
}
};
API.applyPresenceLocation = function (ref) {
var presence = ref.presence;
if (isRealInstance(presence.world)) {
ref.$locationTag = `${presence.world}:${presence.instance}`;
} else {
ref.$locationTag = presence.world;
}
if (isRealInstance(presence.travelingToWorld)) {
ref.$travelingToLocation = `${presence.travelingToWorld}:${presence.travelingToInstance}`;
} else {
ref.$travelingToLocation = presence.travelingToWorld;
}
$app.updateCurrentUserLocation();
};
API.applyPresenceGroups = function (ref) {
if (!this.currentUserGroupsInit) {
// wait for init before diffing
return;
}
var groups = ref.presence?.groups;
if (!groups) {
console.error('API.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 (var groupId of groups) {
if (!this.currentUserGroups.has(groupId)) {
$app.onGroupJoined(groupId);
}
}
for (var groupId of this.currentUserGroups.keys()) {
if (!groups.includes(groupId)) {
$app.onGroupLeft(groupId);
}
}
};
API.applyUser = function (json) {
var ref = this.cachedUsers.get(json.id);
if (typeof json.statusDescription !== 'undefined') {
json.statusDescription = $utils.replaceBioSymbols(
json.statusDescription
);
json.statusDescription = $app.removeEmojis(json.statusDescription);
}
if (typeof json.bio !== 'undefined') {
json.bio = $utils.replaceBioSymbols(json.bio);
}
if (typeof json.note !== 'undefined') {
json.note = $utils.replaceBioSymbols(json.note);
}
if (json.currentAvatarImageUrl === $app.robotUrl) {
delete json.currentAvatarImageUrl;
delete json.currentAvatarThumbnailImageUrl;
}
if (typeof ref === 'undefined') {
ref = {
ageVerificationStatus: '',
ageVerified: false,
allowAvatarCopying: false,
badges: [],
bio: '',
bioLinks: [],
currentAvatarImageUrl: '',
currentAvatarTags: [],
currentAvatarThumbnailImageUrl: '',
date_joined: '',
developerType: '',
displayName: '',
friendKey: '',
friendRequestStatus: '',
id: '',
instanceId: '',
isFriend: false,
last_activity: '',
last_login: '',
last_mobile: null,
last_platform: '',
location: '',
platform: '',
note: '',
profilePicOverride: '',
profilePicOverrideThumbnail: '',
pronouns: '',
state: '',
status: '',
statusDescription: '',
tags: [],
travelingToInstance: '',
travelingToLocation: '',
travelingToWorld: '',
userIcon: '',
worldId: '',
// only in bulk request
fallbackAvatar: '',
// VRCX
$location: {},
$location_at: Date.now(),
$online_for: Date.now(),
$travelingToTime: Date.now(),
$offline_for: '',
$active_for: Date.now(),
$isVRCPlus: false,
$isModerator: false,
$isTroll: false,
$isProbableTroll: false,
$trustLevel: 'Visitor',
$trustClass: 'x-tag-untrusted',
$userColour: '',
$trustSortNum: 1,
$languages: [],
$joinCount: 0,
$timeSpent: 0,
$lastSeen: '',
$nickName: '',
$previousLocation: '',
$customTag: '',
$customTagColour: '',
$friendNumber: 0,
//
...json
};
if ($app.lastLocation.playerList.has(json.id)) {
// update $location_at from instance join time
var player = $app.lastLocation.playerList.get(json.id);
ref.$location_at = player.joinTime;
ref.$online_for = player.joinTime;
}
if (ref.location === 'traveling') {
ref.$location = parseLocation(ref.travelingToLocation);
if (
!this.currentTravelers.has(ref.id) &&
ref.travelingToLocation
) {
var travelRef = {
created_at: new Date().toJSON(),
...ref
};
this.currentTravelers.set(ref.id, travelRef);
$app.sharedFeed.pendingUpdate = true;
$app.updateSharedFeed(false);
$app.onPlayerTraveling(travelRef);
}
} else {
ref.$location = parseLocation(ref.location);
if (this.currentTravelers.has(ref.id)) {
this.currentTravelers.delete(ref.id);
$app.sharedFeed.pendingUpdate = true;
$app.updateSharedFeed(false);
}
}
if (ref.isFriend || ref.id === this.currentUser.id) {
// update instancePlayerCount
var newCount = $app.instancePlayerCount.get(ref.location);
if (typeof newCount === 'undefined') {
newCount = 0;
}
newCount++;
$app.instancePlayerCount.set(ref.location, newCount);
}
if ($app.customUserTags.has(json.id)) {
var tag = $app.customUserTags.get(json.id);
ref.$customTag = tag.tag;
ref.$customTagColour = tag.colour;
} else if (ref.$customTag) {
ref.$customTag = '';
ref.$customTagColour = '';
}
ref.$isVRCPlus = ref.tags.includes('system_supporter');
this.applyUserTrustLevel(ref);
this.applyUserLanguage(ref);
this.cachedUsers.set(ref.id, ref);
} else {
var props = {};
for (var prop in ref) {
if (ref[prop] !== Object(ref[prop])) {
props[prop] = true;
}
}
var $ref = { ...ref };
Object.assign(ref, json);
ref.$isVRCPlus = ref.tags.includes('system_supporter');
this.applyUserTrustLevel(ref);
this.applyUserLanguage(ref);
// traveling
if (ref.location === 'traveling') {
ref.$location = parseLocation(ref.travelingToLocation);
if (!this.currentTravelers.has(ref.id)) {
var travelRef = {
created_at: new Date().toJSON(),
...ref
};
this.currentTravelers.set(ref.id, travelRef);
$app.sharedFeed.pendingUpdate = true;
$app.updateSharedFeed(false);
$app.onPlayerTraveling(travelRef);
}
} else {
ref.$location = parseLocation(ref.location);
if (this.currentTravelers.has(ref.id)) {
this.currentTravelers.delete(ref.id);
$app.sharedFeed.pendingUpdate = true;
$app.updateSharedFeed(false);
}
}
for (var prop in ref) {
if (Array.isArray(ref[prop]) && Array.isArray($ref[prop])) {
if (!$app.arraysMatch(ref[prop], $ref[prop])) {
props[prop] = true;
}
} else if (ref[prop] !== Object(ref[prop])) {
props[prop] = true;
}
}
var has = false;
for (var prop in props) {
var asis = $ref[prop];
var tobe = ref[prop];
if (asis === tobe) {
delete props[prop];
} else {
has = true;
props[prop] = [tobe, asis];
}
}
// FIXME
// if the status is offline, just ignore status and statusDescription only.
if (has && ref.status !== 'offline' && $ref.status !== 'offline') {
if (props.location && props.location[0] !== 'traveling') {
var ts = Date.now();
props.location.push(ts - ref.$location_at);
ref.$location_at = ts;
}
API.$emit('USER:UPDATE', {
ref,
props
});
if ($app.debugUserDiff) {
delete props.last_login;
delete props.last_activity;
if (Object.keys(props).length !== 0) {
console.log('>', ref.displayName, props);
}
}
}
}
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_8c9cf371-ffd2-4177-9894-1093e2e34bf7.png',
hidden: true,
showcased: false
});
}
var friendCtx = $app.friends.get(ref.id);
if (friendCtx) {
friendCtx.ref = ref;
friendCtx.name = ref.displayName;
}
if (ref.id === this.currentUser.id) {
if (ref.status) {
this.currentUser.status = ref.status;
}
$app.updateCurrentUserLocation();
}
this.$emit('USER:APPLY', ref);
return ref;
};
// #endregion
// #region | API: World
API.cachedWorlds = new Map();
API.$on('WORLD', function (args) {
args.ref = this.applyWorld(args.json);
});
API.$on('WORLD:LIST', function (args) {
for (var json of args.json) {
this.$emit('WORLD', {
json,
params: {
worldId: json.id
}
});
}
});
API.$on('WORLD:DELETE', function (args) {
var { json } = args;
this.cachedWorlds.delete(json.id);
if ($app.worldDialog.ref.authorId === json.authorId) {
var map = new Map();
for (var ref of this.cachedWorlds.values()) {
if (ref.authorId === json.authorId) {
map.set(ref.id, ref);
}
}
var array = Array.from(map.values());
$app.userDialog.worlds = array;
}
});
API.$on('WORLD:SAVE', function (args) {
var { json } = args;
this.$emit('WORLD', {
json,
params: {
worldId: json.id
}
});
});
API.getUserApiCurrentLocation = function () {
return this.currentUser?.presence?.world;
};
API.actuallyGetCurrentLocation = async function () {
let gameLogLocation = $app.lastLocation.location;
if (gameLogLocation.startsWith('local')) {
console.warn('PWI: local test mode', 'test_world');
return 'test_world';
}
if (gameLogLocation === 'traveling') {
gameLogLocation = $app.lastLocationDestination;
}
let presenceLocation = this.currentUser.$locationTag;
if (presenceLocation === 'traveling') {
presenceLocation = this.currentUser.$travelingToLocation;
}
// We want to use presence if it's valid to avoid extra API calls, but its prone to being outdated when this function is called.
// So we check if the presence location is the same as the gameLog location; If it is, the presence is (probably) valid and we can use it.
// If it's not, we need to get the user manually to get the correct location.
// If the user happens to be offline or the api is just being dumb, we assume that the user logged into VRCX is different than the one in-game and return the gameLog location.
// This is really dumb.
if (presenceLocation === gameLogLocation) {
const L = parseLocation(presenceLocation);
return L.worldId;
}
const args = await userRequest.getUser({ userId: this.currentUser.id });
const user = args.json;
let userLocation = user.location;
if (userLocation === 'traveling') {
userLocation = user.travelingToLocation;
}
console.warn(
"PWI: location didn't match, fetched user location",
userLocation
);
if (isRealInstance(userLocation)) {
console.warn('PWI: returning user location', userLocation);
const L = parseLocation(userLocation);
return L.worldId;
}
if (isRealInstance(gameLogLocation)) {
console.warn(`PWI: returning gamelog location: `, gameLogLocation);
const L = parseLocation(gameLogLocation);
return L.worldId;
}
console.error(
`PWI: all locations invalid: `,
gameLogLocation,
userLocation
);
return 'test_world';
};
API.applyWorld = function (json) {
var ref = this.cachedWorlds.get(json.id);
if (typeof ref === 'undefined') {
ref = {
id: '',
name: '',
description: '',
defaultContentSettings: {},
authorId: '',
authorName: '',
capacity: 0,
recommendedCapacity: 0,
tags: [],
releaseStatus: '',
imageUrl: '',
thumbnailImageUrl: '',
assetUrl: '',
assetUrlObject: {},
pluginUrl: '',
pluginUrlObject: {},
unityPackageUrl: '',
unityPackageUrlObject: {},
unityPackages: [],
version: 0,
favorites: 0,
created_at: '',
updated_at: '',
publicationDate: '',
labsPublicationDate: '',
visits: 0,
popularity: 0,
heat: 0,
publicOccupants: 0,
privateOccupants: 0,
occupants: 0,
instances: [],
featured: false,
organization: '',
previewYoutubeId: '',
// VRCX
$isLabs: false,
//
...json
};
this.cachedWorlds.set(ref.id, ref);
} else {
Object.assign(ref, json);
}
ref.$isLabs = ref.tags.includes('system_labs');
ref.name = $utils.replaceBioSymbols(ref.name);
ref.description = $utils.replaceBioSymbols(ref.description);
return ref;
};
// #endregion
// #region | API: Instance
API.cachedInstances = new Map();
API.applyInstance = function (json) {
var ref = this.cachedInstances.get(json.id);
if (typeof ref === 'undefined') {
ref = {
id: '',
location: '',
instanceId: '',
name: '',
worldId: '',
type: '',
ownerId: '',
tags: [],
active: false,
full: false,
n_users: 0,
hasCapacityForYou: true, // not present depending on endpoint
capacity: 0,
recommendedCapacity: 0,
userCount: 0,
queueEnabled: false, // only present with group instance type
queueSize: 0, // only present when queuing is enabled
platforms: {},
gameServerVersion: 0,
hardClose: null, // boolean or null
closedAt: null, // string or null
secureName: '',
shortName: '',
world: {},
users: [], // only present when you're the owner
clientNumber: '',
contentSettings: {},
photonRegion: '',
region: '',
canRequestInvite: false,
permanent: false,
private: '', // part of instance tag
hidden: '', // part of instance tag
nonce: '', // only present when you're the owner
strict: false, // deprecated
displayName: null,
groupAccessType: null, // only present with group instance type
roleRestricted: false, // only present with group instance type
instancePersistenceEnabled: null,
playerPersistenceEnabled: null,
ageGate: null,
// VRCX
$fetchedAt: '',
$disabledContentSettings: [],
...json
};
this.cachedInstances.set(ref.id, ref);
} else {
Object.assign(ref, json);
}
ref.$location = parseLocation(ref.location);
if (json.world?.id) {
worldRequest
.getCachedWorld({
worldId: json.world.id
})
.then((args) => {
ref.world = args.ref;
return args;
});
}
if (!json.$fetchedAt) {
ref.$fetchedAt = new Date().toJSON();
}
ref.$disabledContentSettings = [];
if (json.contentSettings && Object.keys(json.contentSettings).length) {
for (var setting in $app.instanceContentSettings) {
if (json.contentSettings[setting]) {
continue;
}
ref.$disabledContentSettings.push(
$app.instanceContentSettings[setting]
);
}
}
return ref;
};
API.$on('INSTANCE', function (args) {
var { json } = args;
if (!json) {
return;
}
args.ref = this.applyInstance(args.json);
});
API.$on('INSTANCE', function (args) {
if (!args.json?.id) {
return;
}
if (
$app.userDialog.visible &&
$app.userDialog.ref.$location.tag === args.json.id
) {
$app.applyUserDialogLocation();
}
if (
$app.worldDialog.visible &&
$app.worldDialog.id === args.json.worldId
) {
$app.applyWorldDialogInstances();
}
if (
$app.groupDialog.visible &&
$app.groupDialog.id === args.json.ownerId
) {
$app.applyGroupDialogInstances();
}
// FIXME:
// because use $refs to update data, can not trigger vue's reactivity system, so view will not update
// will fix this when refactor the core code, maybe
// old comment: hacky workaround to force update instance info
$app.updateInstanceInfo++;
});
// #endregion
// #region | API: Friend
API.$on('FRIEND:LIST', function (args) {
for (var json of args.json) {
if (!json.displayName) {
console.error('/friends gave us garbage', json);
continue;
}
this.$emit('USER', {
json,
params: {
userId: json.id
}
});
}
});
API.isRefreshFriendsLoading = false;
API.refreshFriends = async function () {
this.isRefreshFriendsLoading = true;
try {
var onlineFriends = await this.bulkRefreshFriends({
offline: false
});
var offlineFriends = await this.bulkRefreshFriends({
offline: true
});
var friends = onlineFriends.concat(offlineFriends);
friends = await this.refetchBrokenFriends(friends);
if (!$app.friendLogInitStatus) {
friends = await this.refreshRemainingFriends(friends);
}
this.isRefreshFriendsLoading = false;
return friends;
} catch (err) {
this.isRefreshFriendsLoading = false;
throw err;
}
};
API.bulkRefreshFriends = async function (params) {
var friends = [];
var params = {
...params,
n: 50,
offset: 0
};
// API offset limit *was* 5000
// it is now 7500
mainLoop: for (var i = 150; i > -1; i--) {
retryLoop: for (var j = 0; j < 10; j++) {
// handle 429 ratelimit error, retry 10 times
try {
var args = await friendRequest.getFriends(params);
if (!args.json || args.json.length === 0) {
break mainLoop;
}
friends = friends.concat(args.json);
break retryLoop;
} catch (err) {
console.error(err);
if (!API.currentUser.isLoggedIn) {
console.error(`User isn't logged in`);
break mainLoop;
}
if (err?.message?.includes('Not Found')) {
console.error('Awful workaround for awful VRC API bug');
break retryLoop;
}
await new Promise((resolve) => {
workerTimers.setTimeout(resolve, 5000);
});
}
}
params.offset += 50;
}
return friends;
};
API.refreshRemainingFriends = async function (friends) {
for (var userId of this.currentUser.friends) {
if (!friends.some((x) => x.id === userId)) {
try {
if (!API.isLoggedIn) {
console.error(`User isn't logged in`);
return friends;
}
console.log('Fetching remaining friend', userId);
var args = await userRequest.getUser({ userId });
friends.push(args.json);
} catch (err) {
console.error(err);
}
}
}
return friends;
};
API.refetchBrokenFriends = async function (friends) {
// attempt to fix broken data from bulk friend fetch
for (var i = 0; i < friends.length; i++) {
var friend = friends[i];
try {
// we don't update friend state here, it's not reliable
var state = 'offline';
if (friend.platform === 'web') {
state = 'active';
} else if (friend.platform) {
state = 'online';
}
var ref = $app.friends.get(friend.id);
if (ref?.state !== state) {
if ($app.debugFriendState) {
console.log(
`Refetching friend state it does not match ${friend.displayName} from ${ref?.state} to ${state}`,
friend
);
}
var args = await userRequest.getUser({
userId: friend.id
});
friends[i] = args.json;
} else if (friend.location === 'traveling') {
if ($app.debugFriendState) {
console.log(
'Refetching traveling friend',
friend.displayName
);
}
var args = await userRequest.getUser({
userId: friend.id
});
friends[i] = args.json;
}
} catch (err) {
console.error(err);
}
}
return friends;
};
// #endregion
// #region | API: Avatar
API.cachedAvatars = new Map();
API.$on('AVATAR', function (args) {
args.ref = this.applyAvatar(args.json);
});
API.$on('AVATAR:LIST', function (args) {
for (var json of args.json) {
this.$emit('AVATAR', {
json,
params: {
avatarId: json.id
}
});
}
});
API.$on('AVATAR:SAVE', function (args) {
var { json } = args;
this.$emit('AVATAR', {
json,
params: {
avatarId: json.id
}
});
});
API.$on('AVATAR:SELECT', function (args) {
this.$emit('USER:CURRENT', args);
});
API.$on('AVATAR:DELETE', function (args) {
var { json } = args;
this.cachedAvatars.delete(json._id);
if ($app.userDialog.id === json.authorId) {
var map = new Map();
for (var ref of this.cachedAvatars.values()) {
if (ref.authorId === json.authorId) {
map.set(ref.id, ref);
}
}
var array = Array.from(map.values());
$app.sortUserDialogAvatars(array);
}
});
API.applyAvatar = function (json) {
var ref = this.cachedAvatars.get(json.id);
if (typeof ref === 'undefined') {
ref = {
acknowledgements: '',
authorId: '',
authorName: '',
created_at: '',
description: '',
featured: false,
highestPrice: null,
id: '',
imageUrl: '',
lock: false,
lowestPrice: null,
name: '',
productId: null,
publishedListings: [],
releaseStatus: '',
searchable: false,
styles: [],
tags: [],
thumbnailImageUrl: '',
unityPackageUrl: '',
unityPackageUrlObject: {},
unityPackages: [],
updated_at: '',
version: 0,
...json
};
this.cachedAvatars.set(ref.id, ref);
} else {
var { 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 = $utils.replaceBioSymbols(listing.displayName);
listing.description = $utils.replaceBioSymbols(listing.description);
}
ref.name = $utils.replaceBioSymbols(ref.name);
ref.description = $utils.replaceBioSymbols(ref.description);
return ref;
};
// API.$on('AVATAR:IMPOSTER:DELETE', function (args) {
// if (
// $app.avatarDialog.visible &&
// args.params.avatarId === $app.avatarDialog.id
// ) {
// $app.showAvatarDialog($app.avatarDialog.id);
// }
// });
// #endregion
// #region | API: Notification
API.isNotificationsLoading = false;
API.$on('LOGIN', function () {
this.isNotificationsLoading = false;
});
API.$on('NOTIFICATION', function (args) {
args.ref = this.applyNotification(args.json);
});
API.$on('NOTIFICATION:LIST', function (args) {
for (var json of args.json) {
this.$emit('NOTIFICATION', {
json,
params: {
notificationId: json.id
}
});
}
});
API.$on('NOTIFICATION:LIST:HIDDEN', function (args) {
for (var json of args.json) {
json.type = 'ignoredFriendRequest';
this.$emit('NOTIFICATION', {
json,
params: {
notificationId: json.id
}
});
}
});
API.$on('NOTIFICATION:ACCEPT', function (args) {
var array = $app.notificationTable.data;
for (var i = array.length - 1; i >= 0; i--) {
if (array[i].id === args.params.notificationId) {
var ref = array[i];
break;
}
}
if (typeof ref === 'undefined') {
return;
}
ref.$isExpired = true;
args.ref = ref;
this.$emit('NOTIFICATION:EXPIRE', {
ref,
params: {
notificationId: ref.id
}
});
this.$emit('FRIEND:ADD', {
params: {
userId: ref.senderUserId
}
});
});
API.$on('NOTIFICATION:HIDE', function (args) {
var array = $app.notificationTable.data;
for (var i = array.length - 1; i >= 0; i--) {
if (array[i].id === args.params.notificationId) {
var ref = array[i];
break;
}
}
if (typeof ref === 'undefined') {
return;
}
args.ref = ref;
if (
ref.type === 'friendRequest' ||
ref.type === 'ignoredFriendRequest' ||
ref.type.includes('.')
) {
for (var i = array.length - 1; i >= 0; i--) {
if (array[i].id === ref.id) {
array.splice(i, 1);
break;
}
}
} else {
ref.$isExpired = true;
database.updateNotificationExpired(ref);
}
this.$emit('NOTIFICATION:EXPIRE', {
ref,
params: {
notificationId: ref.id
}
});
});
API.applyNotification = function (json) {
var array = $app.notificationTable.data;
for (var i = array.length - 1; i >= 0; i--) {
if (array[i].id === json.id) {
var ref = array[i];
break;
}
}
// delete any null in json
for (var key in json) {
if (json[key] === null) {
delete json[key];
}
}
if (typeof ref === 'undefined') {
ref = {
id: '',
senderUserId: '',
senderUsername: '',
type: '',
message: '',
details: {},
seen: false,
created_at: '',
// VRCX
$isExpired: false,
//
...json
};
} else {
Object.assign(ref, json);
ref.$isExpired = false;
}
if (ref.details !== Object(ref.details)) {
var details = {};
if (ref.details !== '{}') {
try {
var object = JSON.parse(ref.details);
if (object === Object(object)) {
details = object;
}
} catch (err) {}
}
ref.details = details;
}
return ref;
};
API.expireFriendRequestNotifications = function () {
var array = $app.notificationTable.data;
for (var i = array.length - 1; i >= 0; i--) {
if (
array[i].type === 'friendRequest' ||
array[i].type === 'ignoredFriendRequest' ||
array[i].type.includes('.')
) {
array.splice(i, 1);
}
}
};
API.expireNotification = function (notificationId) {
var array = $app.notificationTable.data;
for (var i = array.length - 1; i >= 0; i--) {
if (array[i].id === notificationId) {
var ref = array[i];
break;
}
}
if (typeof ref === 'undefined') {
return;
}
ref.$isExpired = true;
database.updateNotificationExpired(ref);
this.$emit('NOTIFICATION:EXPIRE', {
ref,
params: {
notificationId: ref.id
}
});
};
API.refreshNotifications = async function () {
this.isNotificationsLoading = true;
try {
this.expireFriendRequestNotifications();
var params = {
n: 100,
offset: 0
};
var count = 50; // 5000 max
for (var i = 0; i < count; i++) {
var args = await notificationRequest.getNotifications(params);
$app.unseenNotifications = [];
params.offset += 100;
if (args.json.length < 100) {
break;
}
}
var params = {
n: 100,
offset: 0
};
var count = 50; // 5000 max
for (var i = 0; i < count; i++) {
var args = await notificationRequest.getNotificationsV2(params);
$app.unseenNotifications = [];
params.offset += 100;
if (args.json.length < 100) {
break;
}
}
var params = {
n: 100,
offset: 0
};
var count = 50; // 5000 max
for (var i = 0; i < count; i++) {
var args =
await notificationRequest.getHiddenFriendRequests(params);
$app.unseenNotifications = [];
params.offset += 100;
if (args.json.length < 100) {
break;
}
}
} catch (err) {
console.error(err);
} finally {
this.isNotificationsLoading = false;
$app.notificationInitStatus = true;
}
};
API.$on('NOTIFICATION:V2:LIST', function (args) {
for (var json of args.json) {
this.$emit('NOTIFICATION:V2', { json });
}
});
API.$on('NOTIFICATION:V2', function (args) {
var json = args.json;
json.created_at = json.createdAt;
if (json.title && json.message) {
json.message = `${json.title}, ${json.message}`;
} else if (json.title) {
json.message = json.title;
}
this.$emit('NOTIFICATION', {
json,
params: {
notificationId: json.id
}
});
});
API.$on('NOTIFICATION:V2:UPDATE', function (args) {
var notificationId = args.params.notificationId;
var json = args.json;
if (!json) {
return;
}
json.id = notificationId;
this.$emit('NOTIFICATION', {
json,
params: {
notificationId
}
});
if (json.seen) {
this.$emit('NOTIFICATION:SEE', {
params: {
notificationId
}
});
}
});
API.$on('NOTIFICATION:RESPONSE', function (args) {
this.$emit('NOTIFICATION:HIDE', args);
new Noty({
type: 'success',
text: $app.escapeTag(args.json)
}).show();
console.log('NOTIFICATION:RESPONSE', args);
});
API.getFriendRequest = function (userId) {
var array = $app.notificationTable.data;
for (var i = array.length - 1; i >= 0; i--) {
if (
array[i].type === 'friendRequest' &&
array[i].senderUserId === userId
) {
return array[i].id;
}
}
return '';
};
// #endregion
// #region | API: PlayerModeration
API.cachedPlayerModerations = new Map();
API.cachedPlayerModerationsUserIds = new Set();
API.isPlayerModerationsLoading = false;
API.$on('LOGIN', function () {
this.cachedPlayerModerations.clear();
this.cachedPlayerModerationsUserIds.clear();
this.isPlayerModerationsLoading = false;
this.refreshPlayerModerations();
});
API.$on('PLAYER-MODERATION', function (args) {
args.ref = this.applyPlayerModeration(args.json);
});
API.$on('PLAYER-MODERATION:LIST', function (args) {
for (var json of args.json) {
this.$emit('PLAYER-MODERATION', {
json,
params: {
playerModerationId: json.id
}
});
}
});
API.$on('PLAYER-MODERATION:SEND', function (args) {
var ref = {
json: args.json,
params: {
playerModerationId: args.json.id
}
};
this.$emit('PLAYER-MODERATION', ref);
this.$emit('PLAYER-MODERATION:@SEND', ref);
});
API.$on('PLAYER-MODERATION:DELETE', function (args) {
var { type, moderated } = args.params;
var userId = this.currentUser.id;
for (var ref of this.cachedPlayerModerations.values()) {
if (
ref.type === type &&
ref.targetUserId === moderated &&
ref.sourceUserId === userId
) {
this.cachedPlayerModerations.delete(ref.id);
this.$emit('PLAYER-MODERATION:@DELETE', {
ref,
params: {
playerModerationId: ref.id
}
});
}
}
this.cachedPlayerModerationsUserIds.delete(moderated);
});
API.applyPlayerModeration = function (json) {
var ref = this.cachedPlayerModerations.get(json.id);
if (typeof ref === 'undefined') {
ref = {
id: '',
type: '',
sourceUserId: '',
sourceDisplayName: '',
targetUserId: '',
targetDisplayName: '',
created: '',
// VRCX
$isExpired: false,
//
...json
};
this.cachedPlayerModerations.set(ref.id, ref);
} else {
Object.assign(ref, json);
ref.$isExpired = false;
}
if (json.targetUserId) {
this.cachedPlayerModerationsUserIds.add(json.targetUserId);
}
return ref;
};
API.expirePlayerModerations = function () {
this.cachedPlayerModerationsUserIds.clear();
for (var ref of this.cachedPlayerModerations.values()) {
ref.$isExpired = true;
}
};
API.deleteExpiredPlayerModerations = function () {
for (var ref of this.cachedPlayerModerations.values()) {
if (!ref.$isExpired) {
continue;
}
this.$emit('PLAYER-MODERATION:@DELETE', {
ref,
params: {
playerModerationId: ref.id
}
});
}
};
API.refreshPlayerModerations = function () {
if (this.isPlayerModerationsLoading) {
return;
}
this.isPlayerModerationsLoading = true;
this.expirePlayerModerations();
Promise.all([
playerModerationRequest.getPlayerModerations(),
avatarModerationRequest.getAvatarModerations()
])
.finally(() => {
this.isPlayerModerationsLoading = false;
})
.then((res) => {
// 'AVATAR-MODERATION:LIST';
// TODO: compare with cachedAvatarModerations
this.cachedAvatarModerations = new Map();
for (var json of res[1]?.json) {
this.applyAvatarModeration(json);
}
this.deleteExpiredPlayerModerations();
});
};
// #endregion
// #region | API: AvatarModeration
API.cachedAvatarModerations = new Map();
API.applyAvatarModeration = function (json) {
// fix inconsistent Unix time response
if (typeof json.created === 'number') {
json.created = new Date(json.created).toJSON();
}
var ref = this.cachedAvatarModerations.get(json.targetAvatarId);
if (typeof ref === 'undefined') {
ref = {
avatarModerationType: '',
created: '',
targetAvatarId: '',
...json
};
this.cachedAvatarModerations.set(ref.targetAvatarId, ref);
} else {
Object.assign(ref, json);
}
// update avatar dialog
var D = $app.avatarDialog;
if (
D.visible &&
ref.avatarModerationType === 'block' &&
D.id === ref.targetAvatarId
) {
D.isBlocked = true;
}
return ref;
};
// #endregion
// #region | API: Favorite
API.cachedFavorites = new Map();
API.cachedFavoritesByObjectId = new Map();
API.cachedFavoriteGroups = new Map();
API.cachedFavoriteGroupsByTypeName = new Map();
API.favoriteFriendGroups = [];
API.favoriteWorldGroups = [];
API.favoriteAvatarGroups = [];
API.isFavoriteLoading = false;
API.isFavoriteGroupLoading = false;
API.favoriteLimits = {
maxFavoriteGroups: {
avatar: 6,
friend: 3,
world: 4
},
maxFavoritesPerGroup: {
avatar: 50,
friend: 150,
world: 100
}
};
API.$on('LOGIN', function () {
$app.localFavoriteFriends.clear();
$app.currentUserGroupsInit = false;
this.cachedFavorites.clear();
this.cachedFavoritesByObjectId.clear();
this.cachedFavoriteGroups.clear();
this.cachedFavoriteGroupsByTypeName.clear();
this.currentUserGroups.clear();
this.queuedInstances.clear();
this.favoriteFriendGroups = [];
this.favoriteWorldGroups = [];
this.favoriteAvatarGroups = [];
this.isFavoriteLoading = false;
this.isFavoriteGroupLoading = false;
this.refreshFavorites();
});
API.$on('FAVORITE', function (args) {
var ref = this.applyFavorite(args.json);
if (ref.$isDeleted) {
return;
}
args.ref = ref;
});
API.$on('FAVORITE:@DELETE', function (args) {
var { ref } = args;
if (ref.$groupRef !== null) {
--ref.$groupRef.count;
}
});
API.$on('FAVORITE:LIST', function (args) {
for (var json of args.json) {
this.$emit('FAVORITE', {
json,
params: {
favoriteId: json.id
},
sortTop: false
});
}
});
API.$on('FAVORITE:ADD', function (args) {
this.$emit('FAVORITE', {
json: args.json,
params: {
favoriteId: args.json.id
},
sortTop: true
});
});
API.$on('FAVORITE:ADD', function (args) {
if (
args.params.type === 'avatar' &&
!API.cachedAvatars.has(args.params.favoriteId)
) {
this.refreshFavoriteAvatars(args.params.tags);
}
if (
args.params.type === 'friend' &&
$app.localFavoriteFriendsGroups.includes(
'friend:' + args.params.tags
)
) {
$app.updateLocalFavoriteFriends();
}
});
API.$on('FAVORITE:DELETE', function (args) {
var ref = this.cachedFavoritesByObjectId.get(args.params.objectId);
if (typeof ref === 'undefined') {
return;
}
// 애초에 $isDeleted인데 여기로 올 수 가 있나..?
this.cachedFavoritesByObjectId.delete(args.params.objectId);
$app.localFavoriteFriends.delete(args.params.objectId);
$app.updateSidebarFriendsList();
if (ref.$isDeleted) {
return;
}
args.ref = ref;
ref.$isDeleted = true;
API.$emit('FAVORITE:@DELETE', {
ref,
params: {
favoriteId: ref.id
}
});
});
API.$on('FAVORITE:GROUP', function (args) {
var ref = this.applyFavoriteGroup(args.json);
if (ref.$isDeleted) {
return;
}
args.ref = ref;
if (ref.$groupRef !== null) {
ref.$groupRef.displayName = ref.displayName;
ref.$groupRef.visibility = ref.visibility;
}
});
API.$on('FAVORITE:GROUP:LIST', function (args) {
for (var json of args.json) {
this.$emit('FAVORITE:GROUP', {
json,
params: {
favoriteGroupId: json.id
}
});
}
});
API.$on('FAVORITE:GROUP:SAVE', function (args) {
this.$emit('FAVORITE:GROUP', {
json: args.json,
params: {
favoriteGroupId: args.json.id
}
});
});
API.$on('FAVORITE:GROUP:CLEAR', function (args) {
var key = `${args.params.type}:${args.params.group}`;
for (var ref of this.cachedFavorites.values()) {
if (ref.$isDeleted || ref.$groupKey !== key) {
continue;
}
this.cachedFavoritesByObjectId.delete(ref.favoriteId);
$app.localFavoriteFriends.delete(ref.favoriteId);
$app.updateSidebarFriendsList();
ref.$isDeleted = true;
API.$emit('FAVORITE:@DELETE', {
ref,
params: {
favoriteId: ref.id
}
});
}
});
API.$on('FAVORITE:WORLD:LIST', function (args) {
for (var json of args.json) {
if (json.id === '???') {
// FIXME
// json.favoriteId로 따로 불러와야 하나?
// 근데 ???가 많으면 과다 요청이 될듯
continue;
}
this.$emit('WORLD', {
json,
params: {
worldId: json.id
}
});
}
});
API.$on('FAVORITE:AVATAR:LIST', function (args) {
for (var json of args.json) {
if (json.releaseStatus === 'hidden') {
// NOTE: 얘는 또 더미 데이터로 옴
continue;
}
this.$emit('AVATAR', {
json,
params: {
avatarId: json.id
}
});
}
});
API.applyFavorite = function (json) {
var ref = this.cachedFavorites.get(json.id);
if (typeof ref === 'undefined') {
ref = {
id: '',
type: '',
favoriteId: '',
tags: [],
// VRCX
$isDeleted: false,
$isExpired: false,
$groupKey: '',
$groupRef: null,
//
...json
};
this.cachedFavorites.set(ref.id, ref);
this.cachedFavoritesByObjectId.set(ref.favoriteId, ref);
if (
ref.type === 'friend' &&
($app.localFavoriteFriendsGroups.length === 0 ||
$app.localFavoriteFriendsGroups.includes(ref.groupKey))
) {
$app.localFavoriteFriends.add(ref.favoriteId);
$app.updateSidebarFriendsList();
}
} else {
Object.assign(ref, json);
ref.$isExpired = false;
}
ref.$groupKey = `${ref.type}:${String(ref.tags[0])}`;
if (ref.$isDeleted === false && ref.$groupRef === null) {
var group = this.cachedFavoriteGroupsByTypeName.get(ref.$groupKey);
if (typeof group !== 'undefined') {
ref.$groupRef = group;
++group.count;
}
}
return ref;
};
API.expireFavorites = function () {
$app.localFavoriteFriends.clear();
this.cachedFavorites.clear();
this.cachedFavoritesByObjectId.clear();
$app.favoriteObjects.clear();
$app.favoriteFriends_ = [];
$app.favoriteFriendsSorted = [];
$app.favoriteWorlds_ = [];
$app.favoriteWorldsSorted = [];
$app.favoriteAvatars_ = [];
$app.favoriteAvatarsSorted = [];
};
API.deleteExpiredFavorites = function () {
for (var ref of this.cachedFavorites.values()) {
if (ref.$isDeleted || ref.$isExpired === false) {
continue;
}
ref.$isDeleted = true;
this.$emit('FAVORITE:@DELETE', {
ref,
params: {
favoriteId: ref.id
}
});
}
};
API.refreshFavoriteAvatars = function (tag) {
var n = Math.floor(Math.random() * (50 + 1)) + 50;
var params = {
n,
offset: 0,
tag
};
favoriteRequest.getFavoriteAvatars(params);
};
API.refreshFavoriteItems = function () {
var types = {
world: [0, favoriteRequest.getFavoriteWorlds],
avatar: [0, favoriteRequest.getFavoriteAvatars]
};
var tags = [];
for (var ref of this.cachedFavorites.values()) {
if (ref.$isDeleted) {
continue;
}
var type = types[ref.type];
if (typeof type === 'undefined') {
continue;
}
if (ref.type === 'avatar' && !tags.includes(ref.tags[0])) {
tags.push(ref.tags[0]);
}
++type[0];
}
for (var type in types) {
var [N, fn] = types[type];
if (N > 0) {
if (type === 'avatar') {
for (var tag of tags) {
var n = Math.floor(Math.random() * (50 + 1)) + 50;
this.bulk({
fn,
N,
params: {
n,
offset: 0,
tag
}
});
}
} else {
var n = Math.floor(Math.random() * (36 + 1)) + 64;
this.bulk({
fn,
N,
params: {
n,
offset: 0
}
});
}
}
}
};
API.refreshFavorites = async function () {
if (this.isFavoriteLoading) {
return;
}
this.isFavoriteLoading = true;
try {
await favoriteRequest.getFavoriteLimits();
} catch (err) {
console.error(err);
}
this.expireFavorites();
this.cachedFavoriteGroupsByTypeName.clear();
this.bulk({
fn: favoriteRequest.getFavorites,
N: -1,
params: {
n: 50,
offset: 0
},
done(ok) {
if (ok) {
this.deleteExpiredFavorites();
}
this.refreshFavoriteItems();
this.refreshFavoriteGroups();
$app.updateLocalFavoriteFriends();
this.isFavoriteLoading = false;
}
});
};
API.applyFavoriteGroup = function (json) {
var ref = this.cachedFavoriteGroups.get(json.id);
if (typeof ref === 'undefined') {
ref = {
id: '',
ownerId: '',
ownerDisplayName: '',
name: '',
displayName: '',
type: '',
visibility: '',
tags: [],
// VRCX
$isDeleted: false,
$isExpired: false,
$groupRef: null,
//
...json
};
this.cachedFavoriteGroups.set(ref.id, ref);
} else {
Object.assign(ref, json);
ref.$isExpired = false;
}
return ref;
};
API.buildFavoriteGroups = function () {
// 450 = ['group_0', 'group_1', 'group_2'] x 150
this.favoriteFriendGroups = [];
for (var i = 0; i < this.favoriteLimits.maxFavoriteGroups.friend; ++i) {
this.favoriteFriendGroups.push({
assign: false,
key: `friend:group_${i}`,
type: 'friend',
name: `group_${i}`,
displayName: `Group ${i + 1}`,
capacity: this.favoriteLimits.maxFavoritesPerGroup.friend,
count: 0,
visibility: 'private'
});
}
// 400 = ['worlds1', 'worlds2', 'worlds3', 'worlds4'] x 100
this.favoriteWorldGroups = [];
for (var i = 0; i < this.favoriteLimits.maxFavoriteGroups.world; ++i) {
this.favoriteWorldGroups.push({
assign: false,
key: `world:worlds${i + 1}`,
type: 'world',
name: `worlds${i + 1}`,
displayName: `Group ${i + 1}`,
capacity: this.favoriteLimits.maxFavoritesPerGroup.world,
count: 0,
visibility: 'private'
});
}
// 350 = ['avatars1', ...] x 50
// Favorite Avatars (0/50)
// VRC+ Group 1..5 (0/50)
this.favoriteAvatarGroups = [];
for (var i = 0; i < this.favoriteLimits.maxFavoriteGroups.avatar; ++i) {
this.favoriteAvatarGroups.push({
assign: false,
key: `avatar:avatars${i + 1}`,
type: 'avatar',
name: `avatars${i + 1}`,
displayName: `Group ${i + 1}`,
capacity: this.favoriteLimits.maxFavoritesPerGroup.avatar,
count: 0,
visibility: 'private'
});
}
var types = {
friend: this.favoriteFriendGroups,
world: this.favoriteWorldGroups,
avatar: this.favoriteAvatarGroups
};
var assigns = new Set();
// assign the same name first
for (var ref of this.cachedFavoriteGroups.values()) {
if (ref.$isDeleted) {
continue;
}
var groups = types[ref.type];
if (typeof groups === 'undefined') {
continue;
}
for (var group of groups) {
if (group.assign === false && group.name === ref.name) {
group.assign = true;
if (ref.displayName) {
group.displayName = ref.displayName;
}
group.visibility = ref.visibility;
ref.$groupRef = group;
assigns.add(ref.id);
break;
}
}
}
// assign the rest
// FIXME
// The order (cachedFavoriteGroups) is very important. It should be
// processed in the order in which the server responded. But since we
// used Map(), the order would be a mess. So we need something to solve
// this.
for (var ref of this.cachedFavoriteGroups.values()) {
if (ref.$isDeleted || assigns.has(ref.id)) {
continue;
}
var groups = types[ref.type];
if (typeof groups === 'undefined') {
continue;
}
for (var group of groups) {
if (group.assign === false) {
group.assign = true;
group.key = `${group.type}:${ref.name}`;
group.name = ref.name;
group.displayName = ref.displayName;
ref.$groupRef = group;
assigns.add(ref.id);
break;
}
}
}
// update favorites
this.cachedFavoriteGroupsByTypeName.clear();
for (var type in types) {
for (var group of types[type]) {
this.cachedFavoriteGroupsByTypeName.set(group.key, group);
}
}
for (var ref of this.cachedFavorites.values()) {
ref.$groupRef = null;
if (ref.$isDeleted) {
continue;
}
var group = this.cachedFavoriteGroupsByTypeName.get(ref.$groupKey);
if (typeof group === 'undefined') {
continue;
}
ref.$groupRef = group;
++group.count;
}
};
API.expireFavoriteGroups = function () {
for (var ref of this.cachedFavoriteGroups.values()) {
ref.$isExpired = true;
}
};
API.deleteExpiredFavoriteGroups = function () {
for (var ref of this.cachedFavoriteGroups.values()) {
if (ref.$isDeleted || ref.$isExpired === false) {
continue;
}
ref.$isDeleted = true;
this.$emit('FAVORITE:GROUP:@DELETE', {
ref,
params: {
favoriteGroupId: ref.id
}
});
}
};
API.$on('FAVORITE:LIMITS', function (args) {
this.favoriteLimits = {
...this.favoriteLimits,
...args.json
};
});
API.refreshFavoriteGroups = function () {
if (this.isFavoriteGroupLoading) {
return;
}
this.isFavoriteGroupLoading = true;
this.expireFavoriteGroups();
this.bulk({
fn: favoriteRequest.getFavoriteGroups,
N: -1,
params: {
n: 50,
offset: 0
},
done(ok) {
if (ok) {
this.deleteExpiredFavoriteGroups();
this.buildFavoriteGroups();
}
this.isFavoriteGroupLoading = false;
}
});
};
// #endregion
// #region | Misc
var $timers = [];
Vue.component('timer', {
template: '<span v-text="text"></span>',
props: {
epoch: {
type: Number,
default() {
return Date.now();
}
}
},
data() {
return {
text: ''
};
},
methods: {
update() {
if (!this.epoch) {
this.text = '-';
return;
}
this.text = $app.timeToText(Date.now() - this.epoch);
}
},
watch: {
date() {
this.update();
}
},
mounted() {
$timers.push(this);
this.update();
},
destroyed() {
$app.removeFromArray($timers, this);
}
});
workerTimers.setInterval(function () {
for (var $timer of $timers) {
$timer.update();
}
}, 5000);
// Countdown timer
var $countDownTimers = [];
Vue.component('countdown-timer', {
template: '<span v-text="text"></span>',
props: {
datetime: {
type: String,
default() {
return '';
}
},
hours: {
type: Number,
default() {
return 1;
}
}
},
data() {
return {
text: ''
};
},
methods: {
update() {
var epoch =
new Date(this.datetime).getTime() +
1000 * 60 * 60 * this.hours -
Date.now();
if (epoch >= 0) {
this.text = $app.timeToText(epoch);
} else {
this.text = '-';
}
}
},
watch: {
date() {
this.update();
}
},
mounted() {
$countDownTimers.push(this);
this.update();
},
destroyed() {
$app.removeFromArray($countDownTimers, this);
}
});
workerTimers.setInterval(function () {
for (var $countDownTimer of $countDownTimers) {
$countDownTimer.update();
}
}, 5000);
// #endregion
// #region | initialise
$app.methods.refreshCustomCss = function () {
if (document.contains(document.getElementById('app-custom-style'))) {
document.getElementById('app-custom-style').remove();
}
AppApi.CustomCssPath().then((customCss) => {
var head = document.head;
if (customCss) {
var $appCustomStyle = document.createElement('link');
$appCustomStyle.setAttribute('id', 'app-custom-style');
$appCustomStyle.rel = 'stylesheet';
$appCustomStyle.href = `file://${customCss}?_=${Date.now()}`;
head.appendChild($appCustomStyle);
}
});
};
$app.methods.refreshCustomScript = function () {
if (document.contains(document.getElementById('app-custom-script'))) {
document.getElementById('app-custom-script').remove();
}
AppApi.CustomScriptPath().then((customScript) => {
var head = document.head;
if (customScript) {
var $appCustomScript = document.createElement('script');
$appCustomScript.setAttribute('id', 'app-custom-script');
$appCustomScript.src = `file://${customScript}?_=${Date.now()}`;
head.appendChild($appCustomScript);
}
});
};
$app.methods.openExternalLink = function (link) {
this.$confirm(`${link}`, 'Open External Link', {
distinguishCancelAndClose: true,
confirmButtonText: 'Open',
cancelButtonText: 'Copy',
type: 'info',
callback: (action) => {
if (action === 'confirm') {
AppApi.OpenLink(link);
} else if (action === 'cancel') {
this.copyLink(link);
}
}
});
};
$app.methods.compareAppVersion = async function () {
if (!this.appVersion) {
return;
}
var currentVersion = this.appVersion.replace(' (Linux)', '');
var lastVersion = await configRepository.getString(
'VRCX_lastVRCXVersion',
''
);
if (!lastVersion) {
await configRepository.setString(
'VRCX_lastVRCXVersion',
currentVersion
);
return;
}
if (lastVersion !== currentVersion) {
await configRepository.setString(
'VRCX_lastVRCXVersion',
currentVersion
);
if (
(await configRepository.getString('VRCX_branch')) === 'Stable'
) {
this.showChangeLogDialog();
}
}
};
$app.methods.setBranch = async function () {
if (!this.appVersion) {
return;
}
var currentVersion = this.appVersion.replace(' (Linux)', '');
if (currentVersion.includes('VRCX Nightly')) {
this.branch = 'Nightly';
} else {
this.branch = 'Stable';
}
await configRepository.setString('VRCX_branch', this.branch);
};
$app.data.vrcxId = '';
$app.methods.loadVrcxId = async function () {
this.vrcxId = await configRepository.getString('VRCX_id', '');
if (!this.vrcxId) {
this.vrcxId = crypto.randomUUID();
await configRepository.setString('VRCX_id', this.vrcxId);
}
};
$app.methods.updateIsGameRunning = async function (
isGameRunning,
isSteamVRRunning,
isHmdAfk
) {
if (this.gameLogDisabled) {
return;
}
if (isGameRunning !== this.isGameRunning) {
this.isGameRunning = isGameRunning;
if (isGameRunning) {
API.currentUser.$online_for = Date.now();
API.currentUser.$offline_for = '';
API.currentUser.$previousAvatarSwapTime = Date.now();
} else {
await configRepository.setBool('isGameNoVR', this.isGameNoVR);
API.currentUser.$online_for = '';
API.currentUser.$offline_for = Date.now();
this.removeAllQueuedInstances();
this.autoVRChatCacheManagement();
this.checkIfGameCrashed();
this.ipcTimeout = 0;
this.addAvatarWearTime(API.currentUser.currentAvatar);
API.currentUser.$previousAvatarSwapTime = '';
}
this.lastLocationReset();
this.clearNowPlaying();
this.updateVRLastLocation();
workerTimers.setTimeout(
() => this.checkVRChatDebugLogging(),
60000
);
this.nextDiscordUpdate = 0;
console.log(new Date(), 'isGameRunning', isGameRunning);
}
if (isSteamVRRunning !== this.isSteamVRRunning) {
this.isSteamVRRunning = isSteamVRRunning;
console.log('isSteamVRRunning:', isSteamVRRunning);
}
if (isHmdAfk !== this.isHmdAfk) {
this.isHmdAfk = isHmdAfk;
console.log('isHmdAfk:', isHmdAfk);
}
this.updateOpenVR();
};
$app.data.debug = false;
$app.data.debugWebRequests = false;
$app.data.debugWebSocket = false;
$app.data.debugUserDiff = false;
$app.data.debugCurrentUserDiff = false;
$app.data.debugPhotonLogging = false;
$app.data.debugGameLog = false;
$app.data.debugFriendState = false;
$app.data.menuActiveIndex = 'feed';
$app.methods.notifyMenu = function (index) {
const navRef = this.$refs.menu.$children[0];
if (this.menuActiveIndex !== index) {
const item = navRef.items[index];
if (item) {
item.$el.classList.add('notify');
}
}
};
$app.methods.selectMenu = function (index) {
this.menuActiveIndex = index;
const item = this.$refs.menu.$children[0]?.items[index];
if (item) {
item.$el.classList.remove('notify');
}
if (index === 'notification') {
this.unseenNotifications = [];
}
};
$app.data.twoFactorAuthDialogVisible = false;
API.$on('LOGIN', function () {
$app.twoFactorAuthDialogVisible = false;
});
$app.methods.clearCookiesTryLogin = async function () {
await webApiService.clearCookies();
if (this.loginForm.lastUserLoggedIn) {
var user =
this.loginForm.savedCredentials[
this.loginForm.lastUserLoggedIn
];
if (typeof user !== 'undefined') {
delete user.cookies;
await this.relogin(user);
return;
}
}
};
$app.methods.resendEmail2fa = async function () {
if (this.loginForm.lastUserLoggedIn) {
var user =
this.loginForm.savedCredentials[
this.loginForm.lastUserLoggedIn
];
if (typeof user !== 'undefined') {
await webApiService.clearCookies();
delete user.cookies;
this.relogin(user).then(() => {
new Noty({
type: 'success',
text: 'Email 2FA resent.'
}).show();
});
return;
}
}
new Noty({
type: 'error',
text: 'Cannot send 2FA email without saved credentials. Please login again.'
}).show();
};
API.$on('USER:2FA', function () {
AppApi.FocusWindow();
$app.promptTOTP();
});
API.$on('USER:EMAILOTP', function () {
AppApi.FocusWindow();
$app.promptEmailOTP();
});
API.$on('LOGOUT', function () {
if (this.isLoggedIn) {
new Noty({
type: 'success',
text: `See you again, <strong>${$app.escapeTag(
this.currentUser.displayName
)}</strong>!`
}).show();
}
this.isLoggedIn = false;
$app.friendLogInitStatus = false;
$app.notificationInitStatus = false;
});
API.$on('LOGIN', function (args) {
new Noty({
type: 'success',
text: `Hello there, <strong>${$app.escapeTag(
args.ref.displayName
)}</strong>!`
}).show();
$app.updateStoredUser(this.currentUser);
});
API.$on('LOGOUT', async function () {
await $app.updateStoredUser(this.currentUser);
webApiService.clearCookies();
// eslint-disable-next-line require-atomic-updates
$app.loginForm.lastUserLoggedIn = '';
await configRepository.remove('lastUserLoggedIn');
// workerTimers.setTimeout(() => location.reload(), 500);
});
$app.methods.checkPrimaryPassword = function (args) {
return new Promise((resolve, reject) => {
if (!this.enablePrimaryPassword) {
resolve(args.password);
}
$app.$prompt(
$t('prompt.primary_password.description'),
$t('prompt.primary_password.header'),
{
inputType: 'password',
inputPattern: /[\s\S]{1,32}/
}
)
.then(({ value }) => {
security
.decrypt(args.password, value)
.then(resolve)
.catch(reject);
})
.catch(reject);
});
};
$app.data.enablePrimaryPassword = await configRepository.getBool(
'enablePrimaryPassword',
false
);
$app.data.enablePrimaryPasswordDialog = {
visible: false,
password: '',
rePassword: '',
beforeClose(done) {
$app._data.enablePrimaryPassword = false;
done();
}
};
$app.methods.enablePrimaryPasswordChange = function () {
// The function is only called in adv settings
this.enablePrimaryPassword = !this.enablePrimaryPassword;
this.enablePrimaryPasswordDialog.password = '';
this.enablePrimaryPasswordDialog.rePassword = '';
if (this.enablePrimaryPassword) {
this.enablePrimaryPasswordDialog.visible = true;
} else {
this.$prompt(
$t('prompt.primary_password.description'),
$t('prompt.primary_password.header'),
{
inputType: 'password',
inputPattern: /[\s\S]{1,32}/
}
)
.then(({ value }) => {
for (let userId in this.loginForm.savedCredentials) {
security
.decrypt(
this.loginForm.savedCredentials[userId]
.loginParmas.password,
value
)
.then(async (pt) => {
this.saveCredentials = {
username:
this.loginForm.savedCredentials[userId]
.loginParmas.username,
password: pt
};
await this.updateStoredUser(
this.loginForm.savedCredentials[userId].user
);
await configRepository.setBool(
'enablePrimaryPassword',
false
);
})
.catch(async () => {
this.enablePrimaryPassword = true;
await configRepository.setBool(
'enablePrimaryPassword',
true
);
});
}
})
.catch(async () => {
this.enablePrimaryPassword = true;
await configRepository.setBool(
'enablePrimaryPassword',
true
);
});
}
};
$app.methods.setPrimaryPassword = async function () {
await configRepository.setBool(
'enablePrimaryPassword',
this.enablePrimaryPassword
);
this.enablePrimaryPasswordDialog.visible = false;
if (this.enablePrimaryPassword) {
let key = this.enablePrimaryPasswordDialog.password;
for (let userId in this.loginForm.savedCredentials) {
security
.encrypt(
this.loginForm.savedCredentials[userId].loginParmas
.password,
key
)
.then((ct) => {
this.saveCredentials = {
username:
this.loginForm.savedCredentials[userId]
.loginParmas.username,
password: ct
};
this.updateStoredUser(
this.loginForm.savedCredentials[userId].user
);
});
}
}
};
$app.methods.updateStoredUser = async function (user) {
var savedCredentials = {};
if ((await configRepository.getString('savedCredentials')) !== null) {
savedCredentials = JSON.parse(
await configRepository.getString('savedCredentials')
);
}
if (this.saveCredentials) {
var credentialsToSave = {
user,
loginParmas: this.saveCredentials
};
savedCredentials[user.id] = credentialsToSave;
delete this.saveCredentials;
} else if (typeof savedCredentials[user.id] !== 'undefined') {
savedCredentials[user.id].user = user;
savedCredentials[user.id].cookies =
await webApiService.getCookies();
}
this.loginForm.savedCredentials = savedCredentials;
var jsonCredentialsArray = JSON.stringify(savedCredentials);
await configRepository.setString(
'savedCredentials',
jsonCredentialsArray
);
this.loginForm.lastUserLoggedIn = user.id;
await configRepository.setString('lastUserLoggedIn', user.id);
};
$app.methods.migrateStoredUsers = async function () {
var savedCredentials = {};
if ((await configRepository.getString('savedCredentials')) !== null) {
savedCredentials = JSON.parse(
await configRepository.getString('savedCredentials')
);
}
for (let name in savedCredentials) {
var userId = savedCredentials[name]?.user?.id;
if (userId && userId !== name) {
savedCredentials[userId] = savedCredentials[name];
delete savedCredentials[name];
}
}
await configRepository.setString(
'savedCredentials',
JSON.stringify(savedCredentials)
);
};
// #endregion
// #region | App: Friends
$app.data.friends = new Map();
$app.data.pendingActiveFriends = new Set();
$app.data.friendNumber = 0;
$app.data.isFriendsGroupMe = true;
$app.data.isVIPFriends = true;
$app.data.isOnlineFriends = true;
$app.data.isActiveFriends = true;
$app.data.isOfflineFriends = false;
$app.data.isGroupInstances = false;
$app.data.groupInstances = [];
$app.data.vipFriends_ = [];
$app.data.onlineFriends_ = [];
$app.data.activeFriends_ = [];
$app.data.offlineFriends_ = [];
$app.data.sortVIPFriends = false;
$app.data.sortOnlineFriends = false;
$app.data.sortActiveFriends = false;
$app.data.sortOfflineFriends = false;
$app.methods.fetchActiveFriend = function (userId) {
this.pendingActiveFriends.add(userId);
// FIXME: handle error
return userRequest
.getUser({
userId
})
.then((args) => {
this.pendingActiveFriends.delete(userId);
return args;
});
};
API.$on('USER:CURRENT', function (args) {
$app.checkActiveFriends(args.json);
});
$app.methods.checkActiveFriends = function (ref) {
if (
Array.isArray(ref.activeFriends) === false ||
!this.friendLogInitStatus
) {
return;
}
for (var userId of ref.activeFriends) {
if (this.pendingActiveFriends.has(userId)) {
continue;
}
var user = API.cachedUsers.get(userId);
if (typeof user !== 'undefined' && user.status !== 'offline') {
continue;
}
if (this.pendingActiveFriends.size >= 5) {
break;
}
this.fetchActiveFriend(userId);
}
};
API.$on('LOGIN', function () {
$app.friends.clear();
$app.pendingActiveFriends.clear();
$app.friendNumber = 0;
$app.isGroupInstances = false;
$app.groupInstances = [];
$app.vipFriends_ = [];
$app.onlineFriends_ = [];
$app.activeFriends_ = [];
$app.offlineFriends_ = [];
$app.sortVIPFriends = false;
$app.sortOnlineFriends = false;
$app.sortActiveFriends = false;
$app.sortOfflineFriends = false;
$app.updateInGameGroupOrder();
});
API.$on('USER:CURRENT', function (args) {
// USER:CURRENT에서 처리를 함
if ($app.friendLogInitStatus) {
$app.refreshFriends(args.ref, args.fromGetCurrentUser);
}
$app.updateOnlineFriendCoutner();
if ($app.randomUserColours) {
$app.getNameColour(this.currentUser.id).then((colour) => {
this.currentUser.$userColour = colour;
});
}
});
API.$on('FRIEND:ADD', function (args) {
$app.addFriend(args.params.userId);
});
API.$on('FRIEND:DELETE', function (args) {
$app.deleteFriend(args.params.userId);
});
API.$on('FRIEND:STATE', function (args) {
$app.updateFriend({
id: args.params.userId,
state: args.json.state
});
});
API.$on('FAVORITE', function (args) {
$app.updateFriend({ id: args.ref.favoriteId });
});
API.$on('FAVORITE:@DELETE', function (args) {
$app.updateFriend({ id: args.ref.favoriteId });
});
$app.methods.refreshFriendsList = async function () {
// If we just got user less then 2 min before code call, don't call it again
if (this.nextCurrentUserRefresh < 300) {
await API.getCurrentUser().catch((err) => {
console.error(err);
});
}
await API.refreshFriends().catch((err) => {
console.error(err);
});
API.reconnectWebSocket();
};
$app.methods.refreshFriends = function (ref, fromGetCurrentUser) {
var map = new Map();
for (var id of ref.friends) {
map.set(id, 'offline');
}
for (var id of ref.offlineFriends) {
map.set(id, 'offline');
}
for (var id of ref.activeFriends) {
map.set(id, 'active');
}
for (var id of ref.onlineFriends) {
map.set(id, 'online');
}
for (var [id, state] of map) {
if (this.friends.has(id)) {
this.updateFriend({ id, state, fromGetCurrentUser });
} else {
this.addFriend(id, state);
}
}
for (var id of this.friends.keys()) {
if (map.has(id) === false) {
this.deleteFriend(id);
}
}
};
$app.methods.addFriend = function (id, state) {
if (this.friends.has(id)) {
return;
}
var ref = API.cachedUsers.get(id);
var isVIP = this.localFavoriteFriends.has(id);
var name = '';
var friend = this.friendLog.get(id);
if (friend) {
name = friend.displayName;
}
var ctx = {
id,
state: state || 'offline',
isVIP,
ref,
name,
memo: '',
pendingOffline: false,
pendingOfflineTime: '',
pendingState: '',
$nickName: ''
};
if (this.friendLogInitStatus) {
this.getUserMemo(id).then((memo) => {
if (memo.userId === id) {
ctx.memo = memo.memo;
ctx.$nickName = '';
if (memo.memo) {
var array = memo.memo.split('\n');
ctx.$nickName = array[0];
}
}
});
}
if (typeof ref === 'undefined') {
var friendLogRef = this.friendLog.get(id);
if (friendLogRef?.displayName) {
ctx.name = friendLogRef.displayName;
}
} else {
ctx.name = ref.name;
}
this.friends.set(id, ctx);
if (ctx.state === 'online') {
if (ctx.isVIP) {
this.vipFriends_.push(ctx);
this.sortVIPFriends = true;
} else {
this.onlineFriends_.push(ctx);
this.sortOnlineFriends = true;
}
} else if (ctx.state === 'active') {
this.activeFriends_.push(ctx);
this.sortActiveFriends = true;
} else {
this.offlineFriends_.push(ctx);
this.sortOfflineFriends = true;
}
};
$app.methods.deleteFriend = function (id) {
var ctx = this.friends.get(id);
if (typeof ctx === 'undefined') {
return;
}
this.friends.delete(id);
if (ctx.state === 'online') {
if (ctx.isVIP) {
$app.removeFromArray(this.vipFriends_, ctx);
} else {
$app.removeFromArray(this.onlineFriends_, ctx);
}
} else if (ctx.state === 'active') {
$app.removeFromArray(this.activeFriends_, ctx);
} else {
$app.removeFromArray(this.offlineFriends_, ctx);
}
};
$app.methods.updateFriend = function (ctx) {
var { id, state, fromGetCurrentUser } = ctx;
var stateInput = state;
var ctx = this.friends.get(id);
if (typeof ctx === 'undefined') {
return;
}
var ref = API.cachedUsers.get(id);
if (stateInput) {
ctx.pendingState = stateInput;
if (typeof ref !== 'undefined') {
ctx.ref.state = stateInput;
}
}
if (stateInput === 'online') {
if (this.debugFriendState && ctx.pendingOffline) {
var time = (Date.now() - ctx.pendingOfflineTime) / 1000;
console.log(`${ctx.name} pendingOfflineCancelTime ${time}`);
}
ctx.pendingOffline = false;
ctx.pendingOfflineTime = '';
}
var isVIP = this.localFavoriteFriends.has(id);
var location = '';
var $location_at = '';
if (typeof ref !== 'undefined') {
var { location, $location_at } = ref;
}
if (typeof stateInput === 'undefined' || ctx.state === stateInput) {
// this is should be: undefined -> user
if (ctx.ref !== ref) {
ctx.ref = ref;
// NOTE
// AddFriend (CurrentUser) 이후,
// 서버에서 오는 순서라고 보면 될 듯.
if (ctx.state === 'online') {
if (this.friendLogInitStatus) {
userRequest.getUser({
userId: id
});
}
if (ctx.isVIP) {
this.sortVIPFriends = true;
} else {
this.sortOnlineFriends = true;
}
}
}
if (ctx.isVIP !== isVIP) {
ctx.isVIP = isVIP;
if (ctx.state === 'online') {
if (ctx.isVIP) {
$app.removeFromArray(this.onlineFriends_, ctx);
this.vipFriends_.push(ctx);
this.sortVIPFriends = true;
} else {
$app.removeFromArray(this.vipFriends_, ctx);
this.onlineFriends_.push(ctx);
this.sortOnlineFriends = true;
}
}
}
if (typeof ref !== 'undefined' && ctx.name !== ref.displayName) {
ctx.name = ref.displayName;
if (ctx.state === 'online') {
if (ctx.isVIP) {
this.sortVIPFriends = true;
} else {
this.sortOnlineFriends = true;
}
} else if (ctx.state === 'active') {
this.sortActiveFriends = true;
} else {
this.sortOfflineFriends = true;
}
}
// from getCurrentUser only, fetch user if offline in an instance
if (
fromGetCurrentUser &&
ctx.state !== 'online' &&
typeof ref !== 'undefined' &&
isRealInstance(ref.location)
) {
if (this.debugFriendState) {
console.log(
`Fetching offline friend in an instance from getCurrentUser ${ctx.name}`
);
}
userRequest.getUser({
userId: id
});
}
} else if (
ctx.state === 'online' &&
(stateInput === 'active' || stateInput === 'offline')
) {
ctx.ref = ref;
ctx.isVIP = isVIP;
if (typeof ref !== 'undefined') {
ctx.name = ref.displayName;
}
if (!this.friendLogInitStatus) {
this.updateFriendDelayedCheck(ctx, location, $location_at);
return;
}
// prevent status flapping
if (ctx.pendingOffline) {
if (this.debugFriendState) {
console.log(ctx.name, 'pendingOfflineAlreadyWaiting');
}
return;
}
if (this.debugFriendState) {
console.log(ctx.name, 'pendingOfflineBegin');
}
ctx.pendingOffline = true;
ctx.pendingOfflineTime = Date.now();
// wait 2minutes then check if user came back online
workerTimers.setTimeout(() => {
if (!ctx.pendingOffline) {
if (this.debugFriendState) {
console.log(ctx.name, 'pendingOfflineAlreadyCancelled');
}
return;
}
ctx.pendingOffline = false;
ctx.pendingOfflineTime = '';
if (ctx.pendingState === ctx.state) {
if (this.debugFriendState) {
console.log(
ctx.name,
'pendingOfflineCancelledStateMatched'
);
}
return;
}
if (this.debugFriendState) {
console.log(ctx.name, 'pendingOfflineEnd');
}
this.updateFriendDelayedCheck(ctx, location, $location_at);
}, this.pendingOfflineDelay);
} else {
ctx.ref = ref;
ctx.isVIP = isVIP;
if (typeof ref !== 'undefined') {
ctx.name = ref.displayName;
// wtf, from getCurrentUser only, fetch user if online in offline location
if (fromGetCurrentUser && stateInput === 'online') {
if (this.debugFriendState) {
console.log(
`Fetching friend coming online from getCurrentUser ${ctx.name}`
);
}
userRequest.getUser({
userId: id
});
return;
}
}
this.updateFriendDelayedCheck(ctx, location, $location_at);
}
};
$app.methods.updateFriendDelayedCheck = async function (
ctx,
location,
$location_at
) {
var id = ctx.id;
var newState = ctx.pendingState;
if (this.debugFriendState) {
console.log(
`${ctx.name} updateFriendState ${ctx.state} -> ${newState}`
);
if (
typeof ctx.ref !== 'undefined' &&
location !== ctx.ref.location
) {
console.log(
`${ctx.name} pendingOfflineLocation ${location} -> ${ctx.ref.location}`
);
}
}
if (!this.friends.has(id)) {
console.log('Friend not found', id);
return;
}
var isVIP = this.localFavoriteFriends.has(id);
var ref = ctx.ref;
if (ctx.state !== newState && typeof ctx.ref !== 'undefined') {
if (
(newState === 'offline' || newState === 'active') &&
ctx.state === 'online'
) {
ctx.ref.$online_for = '';
ctx.ref.$offline_for = Date.now();
ctx.ref.$active_for = '';
if (newState === 'active') {
ctx.ref.$active_for = Date.now();
}
var ts = Date.now();
var time = ts - $location_at;
var worldName = await this.getWorldName(location);
var groupName = await this.getGroupName(location);
var feed = {
created_at: new Date().toJSON(),
type: 'Offline',
userId: ref.id,
displayName: ref.displayName,
location,
worldName,
groupName,
time
};
this.addFeed(feed);
database.addOnlineOfflineToDatabase(feed);
} else if (
newState === 'online' &&
(ctx.state === 'offline' || ctx.state === 'active')
) {
ctx.ref.$previousLocation = '';
ctx.ref.$travelingToTime = Date.now();
ctx.ref.$location_at = Date.now();
ctx.ref.$online_for = Date.now();
ctx.ref.$offline_for = '';
ctx.ref.$active_for = '';
var worldName = await this.getWorldName(location);
var groupName = await this.getGroupName(location);
var feed = {
created_at: new Date().toJSON(),
type: 'Online',
userId: id,
displayName: ctx.name,
location,
worldName,
groupName,
time: ''
};
this.addFeed(feed);
database.addOnlineOfflineToDatabase(feed);
}
if (newState === 'active') {
ctx.ref.$active_for = Date.now();
}
}
if (ctx.state === 'online') {
if (ctx.isVIP) {
$app.removeFromArray(this.vipFriends_, ctx);
} else {
$app.removeFromArray(this.onlineFriends_, ctx);
}
} else if (ctx.state === 'active') {
$app.removeFromArray(this.activeFriends_, ctx);
} else {
$app.removeFromArray(this.offlineFriends_, ctx);
}
if (newState === 'online') {
if (isVIP) {
this.vipFriends_.push(ctx);
this.sortVIPFriends = true;
} else {
this.onlineFriends_.push(ctx);
this.sortOnlineFriends = true;
}
} else if (newState === 'active') {
this.activeFriends_.push(ctx);
this.sortActiveFriends = true;
} else {
this.offlineFriends_.push(ctx);
this.sortOfflineFriends = true;
}
if (ctx.state !== newState) {
this.updateOnlineFriendCoutner();
}
ctx.state = newState;
if (ref?.displayName) {
ctx.name = ref.displayName;
}
ctx.isVIP = isVIP;
};
$app.methods.getWorldName = async function (location) {
var worldName = '';
try {
var L = parseLocation(location);
if (L.isRealInstance && L.worldId) {
var args = await worldRequest.getCachedWorld({
worldId: L.worldId
});
worldName = args.ref.name;
}
} catch (e) {
throw e;
}
return worldName;
};
$app.methods.getGroupName = async function (data) {
if (!data) {
return '';
}
var groupName = '';
var groupId = data;
if (!data.startsWith('grp_')) {
var L = parseLocation(data);
groupId = L.groupId;
if (!L.groupId) {
return '';
}
}
try {
var args = await API.getCachedGroup({
groupId
});
groupName = args.ref.name;
} catch (err) {}
return groupName;
};
$app.methods.updateFriendGPS = function (userId) {
var ctx = this.friends.get(userId);
if (ctx.isVIP) {
this.sortVIPFriends = true;
} else {
this.sortOnlineFriends = true;
}
};
$app.data.onlineFriendCount = 0;
$app.methods.updateOnlineFriendCoutner = function () {
var onlineFriendCount =
this.vipFriends.length + this.onlineFriends.length;
if (onlineFriendCount !== this.onlineFriendCount) {
AppApi.ExecuteVrFeedFunction(
'updateOnlineFriendCount',
`${onlineFriendCount}`
);
this.onlineFriendCount = onlineFriendCount;
}
};
// ascending
var compareByDisplayName = function (a, b) {
if (
typeof a.displayName !== 'string' ||
typeof b.displayName !== 'string'
) {
return 0;
}
return a.displayName.localeCompare(b.displayName);
};
var compareByMemberCount = function (a, b) {
if (
typeof a.memberCount !== 'number' ||
typeof b.memberCount !== 'number'
) {
return 0;
}
return a.memberCount - b.memberCount;
};
// private
var compareByPrivate = function (a, b) {
if (typeof a.ref === 'undefined' || typeof b.ref === 'undefined') {
return 0;
}
if (a.ref.location === 'private' && b.ref.location === 'private') {
return 0;
} else if (a.ref.location === 'private') {
return 1;
} else if (b.ref.location === 'private') {
return -1;
}
return 0;
};
var compareByStatus = function (a, b) {
if (typeof a.ref === 'undefined' || typeof b.ref === 'undefined') {
return 0;
}
if (a.ref.status === b.ref.status) {
return 0;
}
if (a.ref.state === 'offline') {
return 1;
}
return $app.sortStatus(a.ref.status, b.ref.status);
};
$app.methods.sortStatus = function (a, b) {
switch (b) {
case 'join me':
switch (a) {
case 'active':
return 1;
case 'ask me':
return 1;
case 'busy':
return 1;
}
break;
case 'active':
switch (a) {
case 'join me':
return -1;
case 'ask me':
return 1;
case 'busy':
return 1;
}
break;
case 'ask me':
switch (a) {
case 'join me':
return -1;
case 'active':
return -1;
case 'busy':
return 1;
}
break;
case 'busy':
switch (a) {
case 'join me':
return -1;
case 'active':
return -1;
case 'ask me':
return -1;
}
break;
}
return 0;
};
// location at
var compareByLocationAt = function (a, b) {
if (a.location === 'traveling' && b.location === 'traveling') {
return 0;
}
if (a.location === 'traveling') {
return 1;
}
if (b.location === 'traveling') {
return -1;
}
if (a.$location_at < b.$location_at) {
return -1;
}
if (a.$location_at > b.$location_at) {
return 1;
}
return 0;
};
// location at but for the sidebar
var compareByLocation = function (a, b) {
if (typeof a.ref === 'undefined' || typeof b.ref === 'undefined') {
return 0;
}
if (a.state !== 'online' || b.state !== 'online') {
return 0;
}
return a.ref.location.localeCompare(b.ref.location);
};
var compareByActivityField = function (a, b, field) {
if (typeof a.ref === 'undefined' || typeof b.ref === 'undefined') {
return 0;
}
// When the field is just and empty string, it means they've been
// in whatever active state for the longest
if (
a.ref[field] < b.ref[field] ||
(a.ref[field] !== '' && b.ref[field] === '')
) {
return 1;
}
if (
a.ref[field] > b.ref[field] ||
(a.ref[field] === '' && b.ref[field] !== '')
) {
return -1;
}
return 0;
};
// last active
var compareByLastActive = function (a, b) {
if (a.state === 'online' && b.state === 'online') {
if (
a.ref?.$online_for &&
b.ref?.$online_for &&
a.ref.$online_for === b.ref.$online_for
) {
compareByActivityField(a, b, 'last_login');
}
return compareByActivityField(a, b, '$online_for');
}
return compareByActivityField(a, b, 'last_activity');
};
// last seen
var compareByLastSeen = function (a, b) {
return compareByActivityField(a, b, '$lastSeen');
};
var getFriendsSortFunction = function (sortMethods) {
const sorts = [];
for (const sortMethod of sortMethods) {
switch (sortMethod) {
case 'Sort Alphabetically':
sorts.push($utils.compareByName);
break;
case 'Sort Private to Bottom':
sorts.push(compareByPrivate);
break;
case 'Sort by Status':
sorts.push(compareByStatus);
break;
case 'Sort by Last Active':
sorts.push(compareByLastActive);
break;
case 'Sort by Last Seen':
sorts.push(compareByLastSeen);
break;
case 'Sort by Time in Instance':
sorts.push((a, b) => {
if (
typeof a.ref === 'undefined' ||
typeof b.ref === 'undefined'
) {
return 0;
}
if (a.state !== 'online' || b.state !== 'online') {
return 0;
}
return compareByLocationAt(b.ref, a.ref);
});
break;
case 'Sort by Location':
sorts.push(compareByLocation);
break;
case 'None':
sorts.push(() => 0);
break;
}
}
return (a, b) => {
let res;
for (const sort of sorts) {
res = sort(a, b);
if (res !== 0) {
return res;
}
}
return res;
};
};
// VIP friends
$app.computed.vipFriends = function () {
if (!this.sortVIPFriends) {
return this.vipFriends_;
}
this.sortVIPFriends = false;
this.vipFriends_.sort(getFriendsSortFunction(this.sidebarSortMethods));
return this.vipFriends_;
};
// Online friends
$app.computed.onlineFriends = function () {
if (!this.sortOnlineFriends) {
return this.onlineFriends_;
}
this.sortOnlineFriends = false;
this.onlineFriends_.sort(
getFriendsSortFunction(this.sidebarSortMethods)
);
return this.onlineFriends_;
};
// Active friends
$app.computed.activeFriends = function () {
if (!this.sortActiveFriends) {
return this.activeFriends_;
}
this.sortActiveFriends = false;
this.activeFriends_.sort(
getFriendsSortFunction(this.sidebarSortMethods)
);
return this.activeFriends_;
};
// Offline friends
$app.computed.offlineFriends = function () {
if (!this.sortOfflineFriends) {
return this.offlineFriends_;
}
this.sortOfflineFriends = false;
this.offlineFriends_.sort(
getFriendsSortFunction(this.sidebarSortMethods)
);
return this.offlineFriends_;
};
$app.methods.userStatusClass = function (user, pendingOffline) {
var style = {};
if (typeof user === 'undefined') {
return style;
}
var id = '';
if (user.id) {
id = user.id;
} else if (user.userId) {
id = user.userId;
}
if (id === API.currentUser.id) {
return this.statusClass(user.status);
}
if (!user.isFriend) {
return style;
}
if (pendingOffline) {
// Pending offline
style.offline = true;
} else if (
user.status !== 'active' &&
user.location === 'private' &&
user.state === '' &&
id &&
!API.currentUser.onlineFriends.includes(id)
) {
// temp fix
if (API.currentUser.activeFriends.includes(id)) {
// Active
style.active = true;
} else {
// Offline
style.offline = true;
}
} else if (user.state === 'active') {
// Active
style.active = true;
} else if (user.location === 'offline') {
// Offline
style.offline = true;
} else if (user.status === 'active') {
// Online
style.online = true;
} else if (user.status === 'join me') {
// Join Me
style.joinme = true;
} else if (user.status === 'ask me') {
// Ask Me
style.askme = true;
} else if (user.status === 'busy') {
// Do Not Disturb
style.busy = true;
}
if (
user.platform &&
user.platform !== 'standalonewindows' &&
user.platform !== 'web'
) {
style.mobile = true;
}
if (
user.last_platform &&
user.last_platform !== 'standalonewindows' &&
user.platform === 'web'
) {
style.mobile = true;
}
return style;
};
$app.methods.statusClass = function (status) {
var style = {};
if (typeof status !== 'undefined') {
if (status === 'active') {
// Online
style.online = true;
} else if (status === 'join me') {
// Join Me
style.joinme = true;
} else if (status === 'ask me') {
// Ask Me
style.askme = true;
} else if (status === 'busy') {
// Do Not Disturb
style.busy = true;
}
}
return style;
};
$app.methods.confirmDeleteFriend = function (id) {
this.$confirm('Continue? Unfriend', 'Confirm', {
confirmButtonText: 'Confirm',
cancelButtonText: 'Cancel',
type: 'info',
callback: (action) => {
if (action === 'confirm') {
friendRequest.deleteFriend({
userId: id
});
}
}
});
};
// #endregion
// #region | App: Quick Search
$app.data.quickSearchItems = [];
// Making a persistent comparer increases perf by like 10x lmao
$app.data._stringComparer = undefined;
$app.computed.stringComparer = function () {
if (typeof this._stringComparer === 'undefined') {
this._stringComparer = Intl.Collator(
this.appLanguage.replace('_', '-'),
{ usage: 'search', sensitivity: 'base' }
);
}
return this._stringComparer;
};
$app.methods.quickSearchRemoteMethod = function (query) {
if (!query) {
this.quickSearchItems = this.quickSearchUserHistory();
return;
}
const results = [];
const cleanQuery = removeWhitespace(query);
for (let ctx of this.friends.values()) {
if (typeof ctx.ref === 'undefined') {
continue;
}
const cleanName = removeConfusables(ctx.name);
let match = $utils.localeIncludes(
cleanName,
cleanQuery,
this.stringComparer
);
if (!match) {
// Also check regular name in case search is with special characters
match = $utils.localeIncludes(
ctx.name,
cleanQuery,
this.stringComparer
);
}
// Use query with whitespace for notes and memos as people are more
// likely to include spaces in memos and notes
if (!match && ctx.memo) {
match = $utils.localeIncludes(
ctx.memo,
query,
this.stringComparer
);
}
if (!match && ctx.ref.note) {
match = $utils.localeIncludes(
ctx.ref.note,
query,
this.stringComparer
);
}
if (match) {
results.push({
value: ctx.id,
label: ctx.name,
ref: ctx.ref,
name: ctx.name
});
}
}
results.sort(function (a, b) {
var A =
$app.stringComparer.compare(
a.name.substring(0, cleanQuery.length),
cleanQuery
) === 0;
var B =
$app.stringComparer.compare(
b.name.substring(0, cleanQuery.length),
cleanQuery
) === 0;
if (A && !B) {
return -1;
} else if (B && !A) {
return 1;
}
return $utils.compareByName(a, b);
});
if (results.length > 4) {
results.length = 4;
}
results.push({
value: `search:${query}`,
label: query
});
this.quickSearchItems = results;
};
$app.methods.quickSearchChange = function (value) {
if (value) {
if (value.startsWith('search:')) {
const searchText = value.substr(7);
if (this.quickSearchItems.length > 1 && searchText.length) {
this.friendsListSearch = searchText;
this.menuActiveIndex = 'friendList';
} else {
this.menuActiveIndex = 'search';
this.searchText = searchText;
this.lookupUser({ displayName: searchText });
}
} else {
this.showUserDialog(value);
}
}
};
// #endregion
// #region | App: Quick Search User History
$app.data.showUserDialogHistory = new Set();
$app.methods.quickSearchUserHistory = function () {
var userHistory = Array.from(this.showUserDialogHistory.values())
.reverse()
.slice(0, 5);
var results = [];
userHistory.forEach((userId) => {
var ref = API.cachedUsers.get(userId);
if (typeof ref !== 'undefined') {
results.push({
value: ref.id,
label: ref.name,
ref
});
}
});
return results;
};
// #endregion
// #region | App: Feed
$app.data.tablePageSize = await configRepository.getInt(
'VRCX_tablePageSize',
15
);
$app.data.gameLogTable.pageSize = $app.data.tablePageSize;
$app.data.feedTable.pageSize = $app.data.tablePageSize;
$app.data.dontLogMeOut = false;
API.$on('LOGIN', async function (args) {
// early loading indicator
this.isRefreshFriendsLoading = true;
$app.feedTable.loading = true;
$app.friendLog = new Map();
$app.feedTable.data = [];
$app.feedSessionTable = [];
$app.friendLogInitStatus = false;
$app.notificationInitStatus = false;
await database.initUserTables(args.json.id);
$app.menuActiveIndex = 'feed';
await $app.updateDatabaseVersion();
// eslint-disable-next-line require-atomic-updates
$app.gameLogTable.data = await database.lookupGameLogDatabase(
$app.gameLogTable.search,
$app.gameLogTable.filter
);
// eslint-disable-next-line require-atomic-updates
$app.feedSessionTable = await database.getFeedDatabase();
await $app.feedTableLookup();
// eslint-disable-next-line require-atomic-updates
$app.notificationTable.data = await database.getNotifications();
this.refreshNotifications();
$app.loadCurrentUserGroups(args.json.id, args.json?.presence?.groups);
try {
if (
await configRepository.getBool(`friendLogInit_${args.json.id}`)
) {
await $app.getFriendLog(args.ref);
} else {
await $app.initFriendLog(args.ref);
}
} catch (err) {
if (!$app.dontLogMeOut) {
$app.$message({
message: $t('message.friend.load_failed'),
type: 'error'
});
this.logout();
throw err;
}
}
await $app.getAvatarHistory();
await $app.getAllUserMemos();
if ($app.randomUserColours) {
$app.getNameColour(this.currentUser.id).then((colour) => {
this.currentUser.$userColour = colour;
});
await $app.userColourInit();
}
await $app.getAllUserStats();
$app.sortVIPFriends = true;
$app.sortOnlineFriends = true;
$app.sortActiveFriends = true;
$app.sortOfflineFriends = true;
this.getAuth();
$app.updateSharedFeed(true);
if ($app.isGameRunning) {
$app.loadPlayerList();
}
$app.vrInit();
// remove old data from json file and migrate to SQLite
if (await VRCXStorage.Get(`${args.json.id}_friendLogUpdatedAt`)) {
VRCXStorage.Remove(`${args.json.id}_feedTable`);
$app.migrateMemos();
$app.migrateFriendLog(args.json.id);
}
await AppApi.IPCAnnounceStart();
});
$app.methods.loadPlayerList = function () {
var data = this.gameLogSessionTable;
if (data.length === 0) {
return;
}
var length = 0;
for (var i = data.length - 1; i > -1; i--) {
var ctx = data[i];
if (ctx.type === 'Location') {
this.lastLocation = {
date: Date.parse(ctx.created_at),
location: ctx.location,
name: ctx.worldName,
playerList: new Map(),
friendList: new Map()
};
length = i;
break;
}
}
if (length > 0) {
for (var i = length + 1; i < data.length; i++) {
var ctx = data[i];
if (ctx.type === 'OnPlayerJoined') {
if (!ctx.userId) {
for (var ref of API.cachedUsers.values()) {
if (ref.displayName === ctx.displayName) {
ctx.userId = ref.id;
break;
}
}
}
var userMap = {
displayName: ctx.displayName,
userId: ctx.userId,
joinTime: Date.parse(ctx.created_at),
lastAvatar: ''
};
this.lastLocation.playerList.set(ctx.userId, userMap);
if (this.friends.has(ctx.userId)) {
this.lastLocation.friendList.set(ctx.userId, userMap);
}
}
if (ctx.type === 'OnPlayerLeft') {
this.lastLocation.playerList.delete(ctx.userId);
this.lastLocation.friendList.delete(ctx.userId);
}
}
this.lastLocation.playerList.forEach((ref1) => {
if (
ref1.userId &&
typeof ref1.userId === 'string' &&
!API.cachedUsers.has(ref1.userId)
) {
userRequest.getUser({ userId: ref1.userId });
}
});
this.updateCurrentUserLocation();
this.updateCurrentInstanceWorld();
this.updateVRLastLocation();
this.getCurrentInstanceUserList();
this.applyUserDialogLocation();
this.applyWorldDialogInstances();
this.applyGroupDialogInstances();
}
};
$app.data.instancePlayerCount = new Map();
$app.data.robotUrl = `${API.endpointDomain}/file/file_0e8c4e32-7444-44ea-ade4-313c010d4bae/1/file`;
API.$on('USER:UPDATE', async function (args) {
var { ref, props } = args;
var friend = $app.friends.get(ref.id);
if (typeof friend === 'undefined') {
return;
}
if (props.location) {
// update instancePlayerCount
var previousLocation = props.location[1];
var newLocation = props.location[0];
var oldCount = $app.instancePlayerCount.get(previousLocation);
if (typeof oldCount !== 'undefined') {
oldCount--;
if (oldCount <= 0) {
$app.instancePlayerCount.delete(previousLocation);
} else {
$app.instancePlayerCount.set(previousLocation, oldCount);
}
}
var newCount = $app.instancePlayerCount.get(newLocation);
if (typeof newCount === 'undefined') {
newCount = 0;
}
newCount++;
$app.instancePlayerCount.set(newLocation, newCount);
}
if (props.location && ref.id === $app.userDialog.id) {
// update user dialog instance occupants
$app.applyUserDialogLocation(true);
}
if (props.location && ref.$location.worldId === $app.worldDialog.id) {
$app.applyWorldDialogInstances();
}
if (props.location && ref.$location.groupId === $app.groupDialog.id) {
$app.applyGroupDialogInstances();
}
if (
!props.state &&
props.location &&
props.location[0] !== 'offline' &&
props.location[0] !== '' &&
props.location[1] !== 'offline' &&
props.location[1] !== '' &&
props.location[0] !== 'traveling'
) {
// skip GPS if user is offline or traveling
var previousLocation = props.location[1];
var newLocation = props.location[0];
var time = props.location[2];
if (previousLocation === 'traveling' && ref.$previousLocation) {
previousLocation = ref.$previousLocation;
var travelTime = Date.now() - ref.$travelingToTime;
time -= travelTime;
if (time < 0) {
time = 0;
}
}
if ($app.debugFriendState && previousLocation) {
console.log(
`${ref.displayName} GPS ${previousLocation} -> ${newLocation}`
);
}
if (previousLocation === 'offline') {
previousLocation = '';
}
if (!previousLocation) {
// no previous location
if ($app.debugFriendState) {
console.log(
ref.displayName,
'Ignoring GPS, no previous location',
newLocation
);
}
} else if (ref.$previousLocation === newLocation) {
// location traveled to is the same
ref.$location_at = Date.now() - time;
} else {
var worldName = await $app.getWorldName(newLocation);
var groupName = await $app.getGroupName(newLocation);
var feed = {
created_at: new Date().toJSON(),
type: 'GPS',
userId: ref.id,
displayName: ref.displayName,
location: newLocation,
worldName,
groupName,
previousLocation,
time
};
$app.addFeed(feed);
database.addGPSToDatabase(feed);
$app.updateFriendGPS(ref.id);
// clear previousLocation after GPS
ref.$previousLocation = '';
ref.$travelingToTime = Date.now();
}
}
if (
props.location &&
props.location[0] === 'traveling' &&
props.location[1] !== 'traveling'
) {
// store previous location when user is traveling
ref.$previousLocation = props.location[1];
ref.$travelingToTime = Date.now();
$app.updateFriendGPS(ref.id);
}
var imageMatches = false;
if (
props.currentAvatarThumbnailImageUrl &&
props.currentAvatarThumbnailImageUrl[0] &&
props.currentAvatarThumbnailImageUrl[1] &&
props.currentAvatarThumbnailImageUrl[0] ===
props.currentAvatarThumbnailImageUrl[1]
) {
imageMatches = true;
}
if (
(((props.currentAvatarImageUrl ||
props.currentAvatarThumbnailImageUrl) &&
!ref.profilePicOverride) ||
props.currentAvatarTags) &&
!imageMatches
) {
var currentAvatarImageUrl = '';
var previousCurrentAvatarImageUrl = '';
var currentAvatarThumbnailImageUrl = '';
var previousCurrentAvatarThumbnailImageUrl = '';
var currentAvatarTags = '';
var previousCurrentAvatarTags = '';
if (props.currentAvatarImageUrl) {
currentAvatarImageUrl = props.currentAvatarImageUrl[0];
previousCurrentAvatarImageUrl = props.currentAvatarImageUrl[1];
} else {
currentAvatarImageUrl = ref.currentAvatarImageUrl;
previousCurrentAvatarImageUrl = ref.currentAvatarImageUrl;
}
if (props.currentAvatarThumbnailImageUrl) {
currentAvatarThumbnailImageUrl =
props.currentAvatarThumbnailImageUrl[0];
previousCurrentAvatarThumbnailImageUrl =
props.currentAvatarThumbnailImageUrl[1];
} else {
currentAvatarThumbnailImageUrl =
ref.currentAvatarThumbnailImageUrl;
previousCurrentAvatarThumbnailImageUrl =
ref.currentAvatarThumbnailImageUrl;
}
if (props.currentAvatarTags) {
currentAvatarTags = props.currentAvatarTags[0];
previousCurrentAvatarTags = props.currentAvatarTags[1];
if (
ref.profilePicOverride &&
!props.currentAvatarThumbnailImageUrl
) {
// forget last seen avatar
ref.currentAvatarImageUrl = '';
ref.currentAvatarThumbnailImageUrl = '';
}
} else {
currentAvatarTags = ref.currentAvatarTags;
previousCurrentAvatarTags = ref.currentAvatarTags;
}
if (this.logEmptyAvatars || ref.currentAvatarImageUrl) {
var avatarInfo = {
ownerId: '',
avatarName: ''
};
try {
avatarInfo = await $app.getAvatarName(
currentAvatarImageUrl
);
} catch (err) {}
var previousAvatarInfo = {
ownerId: '',
avatarName: ''
};
try {
previousAvatarInfo = await $app.getAvatarName(
previousCurrentAvatarImageUrl
);
} catch (err) {}
var feed = {
created_at: new Date().toJSON(),
type: 'Avatar',
userId: ref.id,
displayName: ref.displayName,
ownerId: avatarInfo.ownerId,
previousOwnerId: previousAvatarInfo.ownerId,
avatarName: avatarInfo.avatarName,
previousAvatarName: previousAvatarInfo.avatarName,
currentAvatarImageUrl,
currentAvatarThumbnailImageUrl,
previousCurrentAvatarImageUrl,
previousCurrentAvatarThumbnailImageUrl,
currentAvatarTags,
previousCurrentAvatarTags
};
$app.addFeed(feed);
database.addAvatarToDatabase(feed);
}
}
if (props.status || props.statusDescription) {
var status = '';
var previousStatus = '';
var statusDescription = '';
var previousStatusDescription = '';
if (props.status) {
if (props.status[0]) {
status = props.status[0];
}
if (props.status[1]) {
previousStatus = props.status[1];
}
} else if (ref.status) {
status = ref.status;
previousStatus = ref.status;
}
if (props.statusDescription) {
if (props.statusDescription[0]) {
statusDescription = props.statusDescription[0];
}
if (props.statusDescription[1]) {
previousStatusDescription = props.statusDescription[1];
}
} else if (ref.statusDescription) {
statusDescription = ref.statusDescription;
previousStatusDescription = ref.statusDescription;
}
var feed = {
created_at: new Date().toJSON(),
type: 'Status',
userId: ref.id,
displayName: ref.displayName,
status,
statusDescription,
previousStatus,
previousStatusDescription
};
$app.addFeed(feed);
database.addStatusToDatabase(feed);
}
if (props.bio && props.bio[0] && props.bio[1]) {
var bio = '';
var previousBio = '';
if (props.bio[0]) {
bio = props.bio[0];
}
if (props.bio[1]) {
previousBio = props.bio[1];
}
var feed = {
created_at: new Date().toJSON(),
type: 'Bio',
userId: ref.id,
displayName: ref.displayName,
bio,
previousBio
};
$app.addFeed(feed);
database.addBioToDatabase(feed);
}
});
/**
* Function that prepare the Longest Common Subsequence (LCS) scores matrix
* @param {*} s1 String 1
* @param {*} s2 String 2
* @returns
*/
$app.methods.lcsMatrix = function (s1, s2) {
const m = s1.length;
const n = s2.length;
const dp = Array.from({ length: m + 1 }, () => Array(n + 1).fill(0));
// Fill the matrix for LCS
for (let i = 1; i <= m; i++) {
for (let j = 1; j <= n; j++) {
if (s1[i - 1] === s2[j - 1]) {
dp[i][j] = dp[i - 1][j - 1] + 1;
} else {
dp[i][j] = Math.max(dp[i - 1][j], dp[i][j - 1]);
}
}
}
return dp;
};
/**
* Function to find the longest common subsequence between two strings
* @param {string} str1
* @param {string} str2
* @returns {number[][]} A matrix that contains the longest common subsequence between both strings
*/
$app.methods.longestCommonSubsequence = function longestCommonSubsequence(
str1,
str2
) {
let lcs = [];
for (let i = 0; i <= str1.length; i++) {
lcs.push(new Array(str2.length + 1).fill(0));
}
for (let i = str1.length - 1; i >= 0; i--) {
for (let j = str2.length - 1; j >= 0; j--) {
if (str1[i] == str2[j]) {
lcs[i][j] = lcs[i + 1][j + 1] + 1;
} else {
lcs[i][j] = Math.max(lcs[i + 1][j], lcs[i][j + 1]);
}
}
}
return lcs;
};
/**
* Merge differences in both strings to get the longest common subsequence
* @param {{text: string, type: "add" | "remove" | "same"}[]} res
* @returns {{text: string, type: "add" | "remove" | "same"}[]} An array that contains the differences between both strings
*/
$app.methods.regoupDifferences = function regoupDifferences(res) {
let regrouped = [];
let text = '';
let type = '';
for (let i = 0; i < res.length; i++) {
if (i == 0) {
text = res[i].text;
type = res[i].type;
} else if (res[i].type == type) {
text += res[i].text;
} else {
regrouped.push({ text: text, type: type });
text = res[i].text;
type = res[i].type;
}
}
regrouped.push({ text: text, type: type });
return regrouped;
};
// #endregion
// #region | App: gameLog
$app.data.lastLocation = {
date: 0,
location: '',
name: '',
playerList: new Map(),
friendList: new Map()
};
$app.methods.lastLocationReset = function (gameLogDate) {
var dateTime = gameLogDate;
if (!gameLogDate) {
dateTime = new Date().toJSON();
}
var dateTimeStamp = Date.parse(dateTime);
this.photonLobby = new Map();
this.photonLobbyCurrent = new Map();
this.photonLobbyMaster = 0;
this.photonLobbyCurrentUser = 0;
this.photonLobbyUserData = new Map();
this.photonLobbyWatcherLoopStop();
this.photonLobbyAvatars = new Map();
this.photonLobbyLastModeration = new Map();
this.photonLobbyJointime = new Map();
this.photonLobbyActivePortals = new Map();
this.photonEvent7List = new Map();
this.photonLastEvent7List = '';
this.photonLastChatBoxMsg = new Map();
this.moderationEventQueue = new Map();
if (this.photonEventTable.data.length > 0) {
this.photonEventTablePrevious.data = this.photonEventTable.data;
this.photonEventTable.data = [];
}
var playerList = Array.from(this.lastLocation.playerList.values());
var dataBaseEntries = [];
for (var ref of playerList) {
var entry = {
created_at: dateTime,
type: 'OnPlayerLeft',
displayName: ref.displayName,
location: this.lastLocation.location,
userId: ref.userId,
time: dateTimeStamp - ref.joinTime
};
dataBaseEntries.unshift(entry);
this.addGameLog(entry);
}
database.addGamelogJoinLeaveBulk(dataBaseEntries);
if (this.lastLocation.date !== 0) {
var update = {
time: dateTimeStamp - this.lastLocation.date,
created_at: new Date(this.lastLocation.date).toJSON()
};
database.updateGamelogLocationTimeToDatabase(update);
}
this.lastLocationDestination = '';
this.lastLocationDestinationTime = 0;
this.lastLocation = {
date: 0,
location: '',
name: '',
playerList: new Map(),
friendList: new Map()
};
this.updateCurrentUserLocation();
this.updateCurrentInstanceWorld();
this.updateVRLastLocation();
this.getCurrentInstanceUserList();
this.lastVideoUrl = '';
this.lastResourceloadUrl = '';
this.applyUserDialogLocation();
this.applyWorldDialogInstances();
this.applyGroupDialogInstances();
};
$app.data.lastLocation$ = {
tag: '',
instanceId: '',
accessType: '',
worldName: '',
worldCapacity: 0,
joinUrl: '',
statusName: '',
statusImage: ''
};
$app.data.lastLocationDestination = '';
$app.data.lastLocationDestinationTime = 0;
// It's like he's going to be used somewhere, and commenting it out would be an error or something.
$app.methods.silentSearchUser = function (displayName) {
console.log('Searching for userId for:', displayName);
var params = {
n: 5,
offset: 0,
fuzzy: false,
search: displayName
};
userRequest.getUsers(params).then((args) => {
var map = new Map();
var nameFound = false;
for (var json of args.json) {
var ref = API.cachedUsers.get(json.id);
if (typeof ref !== 'undefined') {
map.set(ref.id, ref);
}
if (json.displayName === displayName) {
nameFound = true;
}
}
if (!nameFound) {
console.error('userId not found for', displayName);
}
return args;
});
};
$app.methods.lookupYouTubeVideo = async function (videoId) {
var data = null;
var apiKey = 'AIzaSyA-iUQCpWf5afEL3NanEOSxbzziPMU3bxY';
if (this.youTubeApiKey) {
apiKey = this.youTubeApiKey;
}
try {
var response = await webApiService.execute({
url: `https://www.googleapis.com/youtube/v3/videos?id=${encodeURIComponent(
videoId
)}&part=snippet,contentDetails&key=${apiKey}`,
method: 'GET',
headers: {
Referer: 'https://vrcx.app'
}
});
var json = JSON.parse(response.data);
if (this.debugWebRequests) {
console.log(json, response);
}
if (response.status === 200) {
data = json;
} else {
throw new Error(`Error: ${response.data}`);
}
} catch {
console.error(`YouTube video lookup failed for ${videoId}`);
}
return data;
};
$app.data.nowPlaying = {
url: '',
name: '',
length: 0,
startTime: 0,
offset: 0,
elapsed: 0,
percentage: 0,
remainingText: '',
playing: false
};
$app.methods.clearNowPlaying = function () {
this.nowPlaying = {
url: '',
name: '',
length: 0,
startTime: 0,
offset: 0,
elapsed: 0,
percentage: 0,
remainingText: '',
playing: false
};
this.updateVrNowPlaying();
};
$app.methods.setNowPlaying = function (ctx) {
if (this.nowPlaying.url !== ctx.videoUrl) {
if (!ctx.userId && ctx.displayName) {
for (var ref of API.cachedUsers.values()) {
if (ref.displayName === ctx.displayName) {
ctx.userId = ref.id;
break;
}
}
}
this.queueGameLogNoty(ctx);
this.addGameLog(ctx);
database.addGamelogVideoPlayToDatabase(ctx);
var displayName = '';
if (ctx.displayName) {
displayName = ` (${ctx.displayName})`;
}
var name = `${ctx.videoName}${displayName}`;
this.nowPlaying = {
url: ctx.videoUrl,
name,
length: ctx.videoLength,
startTime: Date.parse(ctx.created_at) / 1000,
offset: ctx.videoPos,
elapsed: 0,
percentage: 0,
remainingText: ''
};
} else {
this.nowPlaying = {
...this.nowPlaying,
length: ctx.videoLength,
startTime: Date.parse(ctx.created_at) / 1000,
offset: ctx.videoPos,
elapsed: 0,
percentage: 0,
remainingText: ''
};
}
this.updateVrNowPlaying();
if (!this.nowPlaying.playing && ctx.videoLength > 0) {
this.nowPlaying.playing = true;
this.updateNowPlaying();
}
};
$app.methods.updateNowPlaying = function () {
var np = this.nowPlaying;
if (!this.nowPlaying.playing) {
return;
}
var now = Date.now() / 1000;
np.elapsed = Math.round((now - np.startTime + np.offset) * 10) / 10;
if (np.elapsed >= np.length) {
this.clearNowPlaying();
return;
}
np.remainingText = this.formatSeconds(np.length - np.elapsed);
np.percentage = Math.round(((np.elapsed * 100) / np.length) * 10) / 10;
this.updateVrNowPlaying();
workerTimers.setTimeout(() => this.updateNowPlaying(), 1000);
};
$app.methods.updateVrNowPlaying = function () {
var json = JSON.stringify(this.nowPlaying);
AppApi.ExecuteVrFeedFunction('nowPlayingUpdate', json);
AppApi.ExecuteVrOverlayFunction('nowPlayingUpdate', json);
};
$app.methods.formatSeconds = function (duration) {
var pad = function (num, size) {
return `000${num}`.slice(size * -1);
},
time = parseFloat(duration).toFixed(3),
hours = Math.floor(time / 60 / 60),
minutes = Math.floor(time / 60) % 60,
seconds = Math.floor(time - minutes * 60);
var hoursOut = '';
if (hours > '0') {
hoursOut = `${pad(hours, 2)}:`;
}
return `${hoursOut + pad(minutes, 2)}:${pad(seconds, 2)}`;
};
$app.methods.convertYoutubeTime = function (duration) {
var a = duration.match(/\d+/g);
if (
duration.indexOf('M') >= 0 &&
duration.indexOf('H') === -1 &&
duration.indexOf('S') === -1
) {
a = [0, a[0], 0];
}
if (duration.indexOf('H') >= 0 && duration.indexOf('M') === -1) {
a = [a[0], 0, a[1]];
}
if (
duration.indexOf('H') >= 0 &&
duration.indexOf('M') === -1 &&
duration.indexOf('S') === -1
) {
a = [a[0], 0, 0];
}
var length = 0;
if (a.length === 3) {
length += parseInt(a[0], 10) * 3600;
length += parseInt(a[1], 10) * 60;
length += parseInt(a[2], 10);
}
if (a.length === 2) {
length += parseInt(a[0], 10) * 60;
length += parseInt(a[1], 10);
}
if (a.length === 1) {
length += parseInt(a[0], 10);
}
return length;
};
$app.data.instanceTypes = [
'invite',
'invite+',
'friends',
'friends+',
'public',
'groupPublic',
'groupPlus',
'groupOnly'
];
$app.methods.updateAutoStateChange = function () {
if (
!this.autoStateChangeEnabled ||
!this.isGameRunning ||
!this.lastLocation.playerList.size ||
this.lastLocation.location === '' ||
this.lastLocation.location === 'traveling'
) {
return;
}
var $location = parseLocation(this.lastLocation.location);
var instanceType = $location.accessType;
if (instanceType === 'group') {
if ($location.groupAccessType === 'members') {
instanceType = 'groupOnly';
} else if ($location.groupAccessType === 'plus') {
instanceType = 'groupPlus';
} else {
instanceType = 'groupPublic';
}
}
if (
this.autoStateChangeInstanceTypes.length > 0 &&
!this.autoStateChangeInstanceTypes.includes(instanceType)
) {
return;
}
var withCompany = this.lastLocation.playerList.size > 1;
if (this.autoStateChangeNoFriends) {
withCompany = this.lastLocation.friendList.size >= 1;
}
var currentStatus = API.currentUser.status;
var newStatus = withCompany
? this.autoStateChangeCompanyStatus
: this.autoStateChangeAloneStatus;
if (currentStatus === newStatus) {
return;
}
userRequest
.saveCurrentUser({
status: newStatus
})
.then(() => {
var text = `Status automaticly changed to ${newStatus}`;
if (this.errorNoty) {
this.errorNoty.close();
}
this.errorNoty = new Noty({
type: 'info',
text
}).show();
console.log(text);
});
};
$app.methods.lookupUser = async function (ref) {
if (ref.userId) {
this.showUserDialog(ref.userId);
return;
}
if (!ref.displayName || ref.displayName.substring(0, 3) === 'ID:') {
return;
}
for (var ctx of API.cachedUsers.values()) {
if (ctx.displayName === ref.displayName) {
this.showUserDialog(ctx.id);
return;
}
}
this.searchText = ref.displayName;
await this.searchUserByDisplayName(ref.displayName);
for (var ctx of this.searchUserResults) {
if (ctx.displayName === ref.displayName) {
this.searchText = '';
this.clearSearch();
this.showUserDialog(ctx.id);
return;
}
}
// this.$refs.searchTab.currentName = '0';
// this.menuActiveIndex = 'search';
};
// #endregion
// #region | App: Search
$app.data.searchText = '';
$app.data.searchUserResults = [];
API.$on('LOGIN', function () {
$app.searchText = '';
$app.searchUserResults = [];
});
$app.methods.clearSearch = function () {
this.searchText = '';
this.searchUserResults = [];
};
$app.methods.searchUserByDisplayName = async function (displayName) {
const params = {
n: 10,
offset: 0,
fuzzy: false,
search: displayName
};
await this.moreSearchUser(null, params);
};
$app.methods.moreSearchUser = async function (go, params) {
// var params = this.searchUserParams;
if (go) {
params.offset += params.n * go;
if (params.offset < 0) {
params.offset = 0;
}
}
this.isSearchUserLoading = true;
await userRequest
.getUsers(params)
.finally(() => {
this.isSearchUserLoading = false;
})
.then((args) => {
var map = new Map();
for (var json of args.json) {
var ref = API.cachedUsers.get(json.id);
if (typeof ref !== 'undefined') {
map.set(ref.id, ref);
}
}
this.searchUserResults = Array.from(map.values());
return args;
});
};
// #endregion
// #region | App: Favorite
$app.data.favoriteObjects = new Map();
$app.data.favoriteFriends_ = [];
$app.data.favoriteFriendsSorted = [];
$app.data.favoriteWorlds_ = [];
$app.data.favoriteWorldsSorted = [];
$app.data.favoriteAvatars_ = [];
$app.data.favoriteAvatarsSorted = [];
$app.data.sortFavoriteFriends = false;
$app.data.sortFavoriteWorlds = false;
$app.data.sortFavoriteAvatars = false;
API.$on('LOGIN', function () {
$app.favoriteObjects.clear();
$app.favoriteFriends_ = [];
$app.favoriteFriendsSorted = [];
$app.favoriteWorlds_ = [];
$app.favoriteWorldsSorted = [];
$app.favoriteAvatars_ = [];
$app.favoriteAvatarsSorted = [];
$app.sortFavoriteFriends = false;
$app.sortFavoriteWorlds = false;
$app.sortFavoriteAvatars = false;
});
API.$on('FAVORITE', function (args) {
$app.applyFavorite(args.ref.type, args.ref.favoriteId, args.sortTop);
});
API.$on('FAVORITE:@DELETE', function (args) {
$app.applyFavorite(args.ref.type, args.ref.favoriteId);
});
API.$on('USER', function (args) {
$app.applyFavorite('friend', args.ref.id);
});
API.$on('WORLD', function (args) {
$app.applyFavorite('world', args.ref.id);
});
API.$on('AVATAR', function (args) {
$app.applyFavorite('avatar', args.ref.id);
});
$app.methods.applyFavorite = async function (type, objectId, sortTop) {
var favorite = API.cachedFavoritesByObjectId.get(objectId);
var ctx = this.favoriteObjects.get(objectId);
if (typeof favorite !== 'undefined') {
var isTypeChanged = false;
if (typeof ctx === 'undefined') {
ctx = {
id: objectId,
type,
groupKey: favorite.$groupKey,
ref: null,
name: '',
$selected: false
};
this.favoriteObjects.set(objectId, ctx);
if (type === 'friend') {
var ref = API.cachedUsers.get(objectId);
if (typeof ref === 'undefined') {
ref = this.friendLog.get(objectId);
if (typeof ref !== 'undefined' && ref.displayName) {
ctx.name = ref.displayName;
}
} else {
ctx.ref = ref;
ctx.name = ref.displayName;
}
} else if (type === 'world') {
var ref = API.cachedWorlds.get(objectId);
if (typeof ref !== 'undefined') {
ctx.ref = ref;
ctx.name = ref.name;
}
} else if (type === 'avatar') {
var ref = API.cachedAvatars.get(objectId);
if (typeof ref !== 'undefined') {
ctx.ref = ref;
ctx.name = ref.name;
}
}
isTypeChanged = true;
} else {
if (ctx.type !== type) {
// WTF???
isTypeChanged = true;
if (type === 'friend') {
$app.removeFromArray(this.favoriteFriends_, ctx);
$app.removeFromArray(this.favoriteFriendsSorted, ctx);
} else if (type === 'world') {
$app.removeFromArray(this.favoriteWorlds_, ctx);
$app.removeFromArray(this.favoriteWorldsSorted, ctx);
} else if (type === 'avatar') {
$app.removeFromArray(this.favoriteAvatars_, ctx);
$app.removeFromArray(this.favoriteAvatarsSorted, ctx);
}
}
if (type === 'friend') {
var ref = API.cachedUsers.get(objectId);
if (typeof ref !== 'undefined') {
if (ctx.ref !== ref) {
ctx.ref = ref;
}
if (ctx.name !== ref.displayName) {
ctx.name = ref.displayName;
this.sortFavoriteFriends = true;
}
}
// else too bad
} else if (type === 'world') {
var ref = API.cachedWorlds.get(objectId);
if (typeof ref !== 'undefined') {
if (ctx.ref !== ref) {
ctx.ref = ref;
}
if (ctx.name !== ref.name) {
ctx.name = ref.name;
this.sortFavoriteWorlds = true;
}
} else {
// try fetch from local world favorites
var world = await database.getCachedWorldById(objectId);
if (world) {
ctx.ref = world;
ctx.name = world.name;
ctx.deleted = true;
this.sortFavoriteWorlds = true;
}
if (!world) {
// try fetch from local world history
var worldName =
await database.getGameLogWorldNameByWorldId(
objectId
);
if (worldName) {
ctx.name = worldName;
ctx.deleted = true;
this.sortFavoriteWorlds = true;
}
}
}
} else if (type === 'avatar') {
var ref = API.cachedAvatars.get(objectId);
if (typeof ref !== 'undefined') {
if (ctx.ref !== ref) {
ctx.ref = ref;
}
if (ctx.name !== ref.name) {
ctx.name = ref.name;
this.sortFavoriteAvatars = true;
}
} else {
// try fetch from local avatar history
var avatar =
await database.getCachedAvatarById(objectId);
if (avatar) {
ctx.ref = avatar;
ctx.name = avatar.name;
ctx.deleted = true;
this.sortFavoriteAvatars = true;
}
}
}
}
if (isTypeChanged) {
if (sortTop) {
if (type === 'friend') {
this.favoriteFriends_.unshift(ctx);
this.favoriteFriendsSorted.push(ctx);
this.sortFavoriteFriends = true;
} else if (type === 'world') {
this.favoriteWorlds_.unshift(ctx);
this.favoriteWorldsSorted.push(ctx);
this.sortFavoriteWorlds = true;
} else if (type === 'avatar') {
this.favoriteAvatars_.unshift(ctx);
this.favoriteAvatarsSorted.push(ctx);
this.sortFavoriteAvatars = true;
}
} else if (type === 'friend') {
this.favoriteFriends_.push(ctx);
this.favoriteFriendsSorted.push(ctx);
this.sortFavoriteFriends = true;
} else if (type === 'world') {
this.favoriteWorlds_.push(ctx);
this.favoriteWorldsSorted.push(ctx);
this.sortFavoriteWorlds = true;
} else if (type === 'avatar') {
this.favoriteAvatars_.push(ctx);
this.favoriteAvatarsSorted.push(ctx);
this.sortFavoriteAvatars = true;
}
}
} else if (typeof ctx !== 'undefined') {
this.favoriteObjects.delete(objectId);
if (type === 'friend') {
$app.removeFromArray(this.favoriteFriends_, ctx);
$app.removeFromArray(this.favoriteFriendsSorted, ctx);
} else if (type === 'world') {
$app.removeFromArray(this.favoriteWorlds_, ctx);
$app.removeFromArray(this.favoriteWorldsSorted, ctx);
} else if (type === 'avatar') {
$app.removeFromArray(this.favoriteAvatars_, ctx);
$app.removeFromArray(this.favoriteAvatarsSorted, ctx);
}
}
};
$app.methods.deleteFavoriteNoConfirm = function (objectId) {
if (!objectId) {
return;
}
this.favoriteDialog.visible = true;
favoriteRequest
.deleteFavorite({
objectId
})
.then(() => {
this.favoriteDialog.visible = false;
})
.finally(() => {
this.favoriteDialog.loading = false;
});
};
$app.computed.favoriteFriends = function () {
if (this.sortFavoriteFriends) {
this.sortFavoriteFriends = false;
this.favoriteFriendsSorted.sort($utils.compareByName);
}
if (this.sortFavorites) {
return this.favoriteFriends_;
}
return this.favoriteFriendsSorted;
};
$app.computed.groupedByGroupKeyFavoriteFriends = function () {
const groupedByGroupKeyFavoriteFriends = {};
this.favoriteFriends.forEach((friend) => {
if (friend.groupKey) {
if (!groupedByGroupKeyFavoriteFriends[friend.groupKey]) {
groupedByGroupKeyFavoriteFriends[friend.groupKey] = [];
}
groupedByGroupKeyFavoriteFriends[friend.groupKey].push(friend);
}
});
return groupedByGroupKeyFavoriteFriends;
};
$app.computed.favoriteWorlds = function () {
if (this.sortFavoriteWorlds) {
this.sortFavoriteWorlds = false;
this.favoriteWorldsSorted.sort($utils.compareByName);
}
if (this.sortFavorites) {
return this.favoriteWorlds_;
}
return this.favoriteWorldsSorted;
};
$app.computed.favoriteAvatars = function () {
if (this.sortFavoriteAvatars) {
this.sortFavoriteAvatars = false;
this.favoriteAvatarsSorted.sort($utils.compareByName);
}
if (this.sortFavorites) {
return this.favoriteAvatars_;
}
return this.favoriteAvatarsSorted;
};
// #endregion
// #region | App: friendLog
$app.data.friendLog = new Map();
$app.data.friendLogTable = {
data: [],
filters: [
{
prop: 'type',
value: [],
filterFn: (row, filter) =>
filter.value.some((v) => v === row.type)
},
{
prop: 'displayName',
value: ''
},
{
prop: 'type',
value: false,
filterFn: (row, filter) =>
!(filter.value && row.type === 'Unfriend')
}
],
tableProps: {
stripe: true,
size: 'mini',
defaultSort: {
prop: 'created_at',
order: 'descending'
}
},
pageSize: $app.data.tablePageSize,
paginationProps: {
small: true,
layout: 'sizes,prev,pager,next,total',
pageSizes: [10, 15, 20, 25, 50, 100]
}
};
API.$on('USER:CURRENT', function (args) {
$app.updateFriendships(args.ref);
});
API.$on('USER', function (args) {
$app.updateFriendship(args.ref);
if (
$app.friendLogInitStatus &&
args.json.isFriend &&
!$app.friendLog.has(args.ref.id) &&
args.json.id !== this.currentUser.id
) {
$app.addFriendship(args.ref.id);
}
});
API.$on('FRIEND:ADD', function (args) {
$app.addFriendship(args.params.userId);
});
API.$on('FRIEND:DELETE', function (args) {
$app.deleteFriendship(args.params.userId);
});
$app.data.friendLogInitStatus = false;
$app.data.notificationInitStatus = false;
$app.methods.initFriendLog = async function (currentUser) {
this.refreshFriends(currentUser, true);
var sqlValues = [];
var friends = await API.refreshFriends();
for (var friend of friends) {
var ref = API.applyUser(friend);
var row = {
userId: ref.id,
displayName: ref.displayName,
trustLevel: ref.$trustLevel,
friendNumber: 0
};
this.friendLog.set(friend.id, row);
sqlValues.unshift(row);
}
database.setFriendLogCurrentArray(sqlValues);
await configRepository.setBool(`friendLogInit_${currentUser.id}`, true);
this.friendLogInitStatus = true;
};
$app.methods.migrateFriendLog = async function (userId) {
VRCXStorage.Remove(`${userId}_friendLogUpdatedAt`);
VRCXStorage.Remove(`${userId}_friendLog`);
this.friendLogTable.data = await VRCXStorage.GetArray(
`${userId}_friendLogTable`
);
database.addFriendLogHistoryArray(this.friendLogTable.data);
VRCXStorage.Remove(`${userId}_friendLogTable`);
await configRepository.setBool(`friendLogInit_${userId}`, true);
};
$app.methods.getFriendLog = async function (currentUser) {
this.friendNumber = await configRepository.getInt(
`VRCX_friendNumber_${currentUser.id}`,
0
);
var maxFriendLogNumber = await database.getMaxFriendLogNumber();
if (this.friendNumber < maxFriendLogNumber) {
this.friendNumber = maxFriendLogNumber;
}
var friendLogCurrentArray = await database.getFriendLogCurrent();
for (var friend of friendLogCurrentArray) {
this.friendLog.set(friend.userId, friend);
}
this.friendLogTable.data = [];
this.friendLogTable.data = await database.getFriendLogHistory();
this.refreshFriends(currentUser, true);
await API.refreshFriends();
await this.tryRestoreFriendNumber();
this.friendLogInitStatus = true;
// check for friend/name/rank change AFTER friendLogInitStatus is set
for (var friend of friendLogCurrentArray) {
var ref = API.cachedUsers.get(friend.userId);
if (typeof ref !== 'undefined') {
this.updateFriendship(ref);
}
}
if (typeof currentUser.friends !== 'undefined') {
this.updateFriendships(currentUser);
}
};
$app.methods.addFriendship = function (id) {
if (
!this.friendLogInitStatus ||
this.friendLog.has(id) ||
id === API.currentUser.id
) {
return;
}
var ref = API.cachedUsers.get(id);
if (typeof ref === 'undefined') {
try {
userRequest.getUser({
userId: id
});
} catch (err) {
console.error('Fetch user on add as friend', err);
}
return;
}
friendRequest
.getFriendStatus({
userId: id
})
.then((args) => {
if (args.json.isFriend && !this.friendLog.has(id)) {
if (this.friendNumber === 0) {
this.friendNumber = this.friends.size;
}
ref.$friendNumber = ++this.friendNumber;
configRepository.setInt(
`VRCX_friendNumber_${API.currentUser.id}`,
this.friendNumber
);
this.addFriend(id, ref.state);
var friendLogHistory = {
created_at: new Date().toJSON(),
type: 'Friend',
userId: id,
displayName: ref.displayName,
friendNumber: ref.$friendNumber
};
this.friendLogTable.data.push(friendLogHistory);
database.addFriendLogHistory(friendLogHistory);
this.queueFriendLogNoty(friendLogHistory);
var friendLogCurrent = {
userId: id,
displayName: ref.displayName,
trustLevel: ref.$trustLevel,
friendNumber: ref.$friendNumber
};
this.friendLog.set(id, friendLogCurrent);
database.setFriendLogCurrent(friendLogCurrent);
this.notifyMenu('friendLog');
this.deleteFriendRequest(id);
this.updateSharedFeed(true);
userRequest
.getUser({
userId: id
})
.then(() => {
if (
this.userDialog.visible &&
id === this.userDialog.id
) {
this.applyUserDialogLocation(true);
}
});
}
});
};
$app.methods.deleteFriendRequest = function (userId) {
var array = $app.notificationTable.data;
for (var i = array.length - 1; i >= 0; i--) {
if (
array[i].type === 'friendRequest' &&
array[i].senderUserId === userId
) {
array.splice(i, 1);
return;
}
}
};
$app.methods.deleteFriendship = function (id) {
var ctx = this.friendLog.get(id);
if (typeof ctx === 'undefined') {
return;
}
friendRequest
.getFriendStatus({
userId: id
})
.then((args) => {
if (!args.json.isFriend && this.friendLog.has(id)) {
var friendLogHistory = {
created_at: new Date().toJSON(),
type: 'Unfriend',
userId: id,
displayName: ctx.displayName || id
};
this.friendLogTable.data.push(friendLogHistory);
database.addFriendLogHistory(friendLogHistory);
this.queueFriendLogNoty(friendLogHistory);
this.friendLog.delete(id);
database.deleteFriendLogCurrent(id);
if (!this.hideUnfriends) {
this.notifyMenu('friendLog');
}
this.updateSharedFeed(true);
this.deleteFriend(id);
}
});
};
$app.methods.updateFriendships = function (ref) {
var set = new Set();
for (var id of ref.friends) {
set.add(id);
this.addFriendship(id);
}
for (var id of this.friendLog.keys()) {
if (id === API.currentUser.id) {
this.friendLog.delete(id);
database.deleteFriendLogCurrent(id);
} else if (!set.has(id)) {
this.deleteFriendship(id);
}
}
};
$app.methods.updateFriendship = function (ref) {
var ctx = this.friendLog.get(ref.id);
if (!this.friendLogInitStatus || typeof ctx === 'undefined') {
return;
}
if (ctx.friendNumber) {
ref.$friendNumber = ctx.friendNumber;
}
if (ctx.displayName !== ref.displayName) {
if (ctx.displayName) {
var friendLogHistoryDisplayName = {
created_at: new Date().toJSON(),
type: 'DisplayName',
userId: ref.id,
displayName: ref.displayName,
previousDisplayName: ctx.displayName,
friendNumber: ref.$friendNumber
};
this.friendLogTable.data.push(friendLogHistoryDisplayName);
database.addFriendLogHistory(friendLogHistoryDisplayName);
this.queueFriendLogNoty(friendLogHistoryDisplayName);
var friendLogCurrent = {
userId: ref.id,
displayName: ref.displayName,
trustLevel: ref.$trustLevel,
friendNumber: ref.$friendNumber
};
this.friendLog.set(ref.id, friendLogCurrent);
database.setFriendLogCurrent(friendLogCurrent);
ctx.displayName = ref.displayName;
this.notifyMenu('friendLog');
this.updateSharedFeed(true);
}
}
if (
ref.$trustLevel &&
ctx.trustLevel &&
ctx.trustLevel !== ref.$trustLevel
) {
if (
(ctx.trustLevel === 'Trusted User' &&
ref.$trustLevel === 'Veteran User') ||
(ctx.trustLevel === 'Veteran User' &&
ref.$trustLevel === 'Trusted User')
) {
var friendLogCurrent3 = {
userId: ref.id,
displayName: ref.displayName,
trustLevel: ref.$trustLevel,
friendNumber: ref.$friendNumber
};
this.friendLog.set(ref.id, friendLogCurrent3);
database.setFriendLogCurrent(friendLogCurrent3);
return;
}
var friendLogHistoryTrustLevel = {
created_at: new Date().toJSON(),
type: 'TrustLevel',
userId: ref.id,
displayName: ref.displayName,
trustLevel: ref.$trustLevel,
previousTrustLevel: ctx.trustLevel,
friendNumber: ref.$friendNumber
};
this.friendLogTable.data.push(friendLogHistoryTrustLevel);
database.addFriendLogHistory(friendLogHistoryTrustLevel);
this.queueFriendLogNoty(friendLogHistoryTrustLevel);
var friendLogCurrent2 = {
userId: ref.id,
displayName: ref.displayName,
trustLevel: ref.$trustLevel,
friendNumber: ref.$friendNumber
};
this.friendLog.set(ref.id, friendLogCurrent2);
database.setFriendLogCurrent(friendLogCurrent2);
this.notifyMenu('friendLog');
this.updateSharedFeed(true);
}
ctx.trustLevel = ref.$trustLevel;
};
// #endregion
// #region | App: Moderation
$app.data.playerModerationTable = {
data: [],
pageSize: $app.data.tablePageSize
};
API.$on('LOGIN', function () {
$app.playerModerationTable.data = [];
});
API.$on('PLAYER-MODERATION', function (args) {
let { ref } = args;
let array = $app.playerModerationTable.data;
let { length } = array;
for (let i = 0; i < length; ++i) {
if (array[i].id === ref.id) {
Vue.set(array, i, ref);
return;
}
}
$app.playerModerationTable.data.push(ref);
});
API.$on('PLAYER-MODERATION:@DELETE', function (args) {
let { ref } = args;
let array = $app.playerModerationTable.data;
let { length } = array;
for (let i = 0; i < length; ++i) {
if (array[i].id === ref.id) {
array.splice(i, 1);
return;
}
}
});
// #endregion
// #region | App: Notification
$app.data.notificationTable = {
data: [],
filters: [
{
prop: 'type',
value: [],
filterFn: (row, filter) =>
filter.value.some((v) => v === row.type)
},
{
prop: ['senderUsername', 'message'],
value: ''
}
],
tableProps: {
stripe: true,
size: 'mini',
defaultSort: {
prop: 'created_at',
order: 'descending'
}
},
pageSize: $app.data.tablePageSize,
paginationProps: {
small: true,
layout: 'sizes,prev,pager,next,total',
pageSizes: [10, 15, 20, 25, 50, 100]
}
};
API.$on('LOGIN', function () {
$app.notificationTable.data = [];
});
API.$on('PIPELINE:NOTIFICATION', function (args) {
var ref = args.json;
if (
ref.type !== 'requestInvite' ||
$app.autoAcceptInviteRequests === 'Off'
) {
return;
}
var currentLocation = $app.lastLocation.location;
if ($app.lastLocation.location === 'traveling') {
currentLocation = $app.lastLocationDestination;
}
if (!currentLocation) {
return;
}
if (
$app.autoAcceptInviteRequests === 'All Favorites' &&
!$app.favoriteFriends.some((x) => x.id === ref.senderUserId)
) {
return;
}
if (
$app.autoAcceptInviteRequests === 'Selected Favorites' &&
!$app.localFavoriteFriends.has(ref.senderUserId)
) {
return;
}
if (!$app.checkCanInvite(currentLocation)) {
return;
}
var L = parseLocation(currentLocation);
worldRequest
.getCachedWorld({
worldId: L.worldId
})
.then((args1) => {
notificationRequest
.sendInvite(
{
instanceId: L.tag,
worldId: L.tag,
worldName: args1.ref.name,
rsvp: true
},
ref.senderUserId
)
.then((_args) => {
var text = `Auto invite sent to ${ref.senderUsername}`;
if (this.errorNoty) {
this.errorNoty.close();
}
this.errorNoty = new Noty({
type: 'info',
text
}).show();
console.log(text);
notificationRequest.hideNotification({
notificationId: ref.id
});
return _args;
})
.catch((err) => {
console.error(err);
});
});
});
$app.data.unseenNotifications = [];
API.$on('NOTIFICATION', function (args) {
var { ref } = args;
var array = $app.notificationTable.data;
var { length } = array;
for (var i = 0; i < length; ++i) {
if (array[i].id === ref.id) {
Vue.set(array, i, ref);
return;
}
}
if (ref.senderUserId !== this.currentUser.id) {
if (
ref.type !== 'friendRequest' &&
ref.type !== 'ignoredFriendRequest' &&
!ref.type.includes('.')
) {
database.addNotificationToDatabase(ref);
}
if ($app.friendLogInitStatus && $app.notificationInitStatus) {
if (
$app.notificationTable.filters[0].value.length === 0 ||
$app.notificationTable.filters[0].value.includes(ref.type)
) {
$app.notifyMenu('notification');
}
$app.unseenNotifications.push(ref.id);
$app.queueNotificationNoty(ref);
}
}
$app.notificationTable.data.push(ref);
$app.updateSharedFeed(true);
});
API.$on('NOTIFICATION:SEE', function (args) {
var { notificationId } = args.params;
$app.removeFromArray($app.unseenNotifications, notificationId);
if ($app.unseenNotifications.length === 0) {
const item = $app.$refs.menu.$children[0]?.items['notification'];
if (item) {
item.$el.classList.remove('notify');
}
}
});
$app.data.feedTable.filter = JSON.parse(
await configRepository.getString('VRCX_feedTableFilters', '[]')
);
$app.data.feedTable.vip = await configRepository.getBool(
'VRCX_feedTableVIPFilter',
false
);
$app.data.gameLogTable.vip = false;
// gameLog loads before favorites
// await configRepository.getBool(
// 'VRCX_gameLogTableVIPFilter',
// false
// );
$app.data.gameLogTable.filter = JSON.parse(
await configRepository.getString('VRCX_gameLogTableFilters', '[]')
);
$app.data.friendLogTable.filters[0].value = JSON.parse(
await configRepository.getString('VRCX_friendLogTableFilters', '[]')
);
$app.data.notificationTable.filters[0].value = JSON.parse(
await configRepository.getString('VRCX_notificationTableFilters', '[]')
);
$app.data.photonEventTableTypeFilter = JSON.parse(
await configRepository.getString('VRCX_photonEventTypeFilter', '[]')
);
$app.data.photonEventTable.filters[1].value =
$app.data.photonEventTableTypeFilter;
$app.data.photonEventTablePrevious.filters[1].value =
$app.data.photonEventTableTypeFilter;
$app.data.photonEventTableTypeOverlayFilter = JSON.parse(
await configRepository.getString(
'VRCX_photonEventTypeOverlayFilter',
'[]'
)
);
// #endregion
// #region | App: Profile + Settings
$app.data.pastDisplayNameTable = {
data: [],
tableProps: {
stripe: true,
size: 'mini',
defaultSort: {
prop: 'updated_at',
order: 'descending'
}
},
layout: 'table'
};
$app.data.printTable = [];
$app.data.stickerTable = [];
$app.data.emojiTable = [];
$app.data.VRCPlusIconsTable = [];
$app.data.galleryTable = [];
$app.data.inviteMessageTable = {
data: [],
tableProps: {
stripe: true,
size: 'mini'
},
layout: 'table',
visible: false
};
$app.data.inviteResponseMessageTable = {
data: [],
tableProps: {
stripe: true,
size: 'mini'
},
layout: 'table',
visible: false
};
$app.data.inviteRequestMessageTable = {
data: [],
tableProps: {
stripe: true,
size: 'mini'
},
layout: 'table',
visible: false
};
$app.data.inviteRequestResponseMessageTable = {
data: [],
tableProps: {
stripe: true,
size: 'mini'
},
layout: 'table',
visible: false
};
$app.data.currentInstanceUserList = {
data: [],
tableProps: {
stripe: true,
size: 'mini',
defaultSort: {
prop: 'timer',
order: 'descending'
}
},
layout: 'table'
};
$app.data.visits = 0;
$app.data.openVR = await configRepository.getBool('openVR', false);
$app.data.openVRAlways = await configRepository.getBool(
'openVRAlways',
false
);
$app.data.overlaybutton = await configRepository.getBool(
'VRCX_overlaybutton',
false
);
$app.data.overlayHand = await configRepository.getInt(
'VRCX_overlayHand',
0
);
$app.data.hidePrivateFromFeed = await configRepository.getBool(
'VRCX_hidePrivateFromFeed',
false
);
$app.data.hideDevicesFromFeed = await configRepository.getBool(
'VRCX_hideDevicesFromFeed',
false
);
$app.data.vrOverlayCpuUsage = await configRepository.getBool(
'VRCX_vrOverlayCpuUsage',
false
);
$app.data.hideUptimeFromFeed = await configRepository.getBool(
'VRCX_hideUptimeFromFeed',
false
);
$app.data.pcUptimeOnFeed = await configRepository.getBool(
'VRCX_pcUptimeOnFeed',
false
);
$app.data.overlayNotifications = await configRepository.getBool(
'VRCX_overlayNotifications',
true
);
$app.data.overlayWrist = await configRepository.getBool(
'VRCX_overlayWrist',
false
);
$app.data.xsNotifications = await configRepository.getBool(
'VRCX_xsNotifications',
true
);
$app.data.ovrtHudNotifications = await configRepository.getBool(
'VRCX_ovrtHudNotifications',
true
);
$app.data.ovrtWristNotifications = await configRepository.getBool(
'VRCX_ovrtWristNotifications',
false
);
$app.data.imageNotifications = await configRepository.getBool(
'VRCX_imageNotifications',
true
);
$app.data.desktopToast = await configRepository.getString(
'VRCX_desktopToast',
'Never'
);
$app.data.afkDesktopToast = await configRepository.getBool(
'VRCX_afkDesktopToast',
false
);
$app.data.overlayToast = await configRepository.getString(
'VRCX_overlayToast',
'Game Running'
);
$app.data.minimalFeed = await configRepository.getBool(
'VRCX_minimalFeed',
false
);
$app.data.displayVRCPlusIconsAsAvatar = await configRepository.getBool(
'displayVRCPlusIconsAsAvatar',
true
);
$app.data.hideTooltips = await configRepository.getBool(
'VRCX_hideTooltips',
false
);
$app.data.hideNicknames = await configRepository.getBool(
'VRCX_hideNicknames',
false
);
$app.data.notificationTTS = await configRepository.getString(
'VRCX_notificationTTS',
'Never'
);
$app.data.notificationTTSNickName = await configRepository.getBool(
'VRCX_notificationTTSNickName',
false
);
// It's not necessary to store it in configRepo because it's rarely used.
$app.data.isTestTTSVisible = false;
$app.data.notificationTTSVoice = await configRepository.getString(
'VRCX_notificationTTSVoice',
'0'
);
$app.data.notificationTimeout = await configRepository.getString(
'VRCX_notificationTimeout',
'3000'
);
$app.data.autoSweepVRChatCache = await configRepository.getBool(
'VRCX_autoSweepVRChatCache',
false
);
$app.data.relaunchVRChatAfterCrash = await configRepository.getBool(
'VRCX_relaunchVRChatAfterCrash',
false
);
$app.data.vrcQuitFix = await configRepository.getBool(
'VRCX_vrcQuitFix',
true
);
$app.data.vrBackgroundEnabled = await configRepository.getBool(
'VRCX_vrBackgroundEnabled',
false
);
$app.data.asideWidth = await configRepository.getInt(
'VRCX_sidePanelWidth',
300
);
$app.data.autoUpdateVRCX = await configRepository.getString(
'VRCX_autoUpdateVRCX',
'Auto Download'
);
if ($app.data.autoUpdateVRCX === 'Auto Install') {
$app.data.autoUpdateVRCX = 'Auto Download';
}
$app.data.branch = await configRepository.getString(
'VRCX_branch',
'Stable'
);
$app.data.maxTableSize = await configRepository.getInt(
'VRCX_maxTableSize',
1000
);
if ($app.data.maxTableSize > 10000) {
$app.data.maxTableSize = 1000;
}
database.setmaxTableSize($app.data.maxTableSize);
$app.data.photonLobbyTimeoutThreshold = await configRepository.getInt(
'VRCX_photonLobbyTimeoutThreshold',
6000
);
$app.data.clearVRCXCacheFrequency = await configRepository.getInt(
'VRCX_clearVRCXCacheFrequency',
172800
);
$app.data.avatarRemoteDatabase = await configRepository.getBool(
'VRCX_avatarRemoteDatabase',
true
);
$app.data.avatarRemoteDatabaseProvider = '';
$app.data.avatarRemoteDatabaseProviderList = JSON.parse(
await configRepository.getString(
'VRCX_avatarRemoteDatabaseProviderList',
'[ "https://api.avtrdb.com/v2/avatar/search/vrcx", "https://avtr.just-h.party/vrcx_search.php" ]'
)
);
if (
$app.data.avatarRemoteDatabaseProviderList.length === 1 &&
$app.data.avatarRemoteDatabaseProviderList[0] ===
'https://avtr.just-h.party/vrcx_search.php'
) {
$app.data.avatarRemoteDatabaseProviderList.unshift(
'https://api.avtrdb.com/v2/avatar/search/vrcx'
);
await configRepository.setString(
'VRCX_avatarRemoteDatabaseProviderList',
JSON.stringify($app.data.avatarRemoteDatabaseProviderList)
);
}
$app.data.pendingOfflineDelay = 180000;
// It's a mess, but it'll be fine afterward with the state manager
$app.data.isAgeGatedInstancesVisible = await configRepository.getBool(
'VRCX_isAgeGatedInstancesVisible',
true
);
$app.methods.toggleIsAgeGatedInstancesVisible = function () {
this.isAgeGatedInstancesVisible = !this.isAgeGatedInstancesVisible;
configRepository.setBool(
'VRCX_isAgeGatedInstancesVisible',
this.isAgeGatedInstancesVisible
);
};
if (await configRepository.getString('VRCX_avatarRemoteDatabaseProvider')) {
// move existing provider to new list
var avatarRemoteDatabaseProvider = await configRepository.getString(
'VRCX_avatarRemoteDatabaseProvider'
);
if (
!$app.data.avatarRemoteDatabaseProviderList.includes(
avatarRemoteDatabaseProvider
)
) {
$app.data.avatarRemoteDatabaseProviderList.push(
avatarRemoteDatabaseProvider
);
}
await configRepository.remove('VRCX_avatarRemoteDatabaseProvider');
await configRepository.setString(
'VRCX_avatarRemoteDatabaseProviderList',
JSON.stringify($app.data.avatarRemoteDatabaseProviderList)
);
}
if ($app.data.avatarRemoteDatabaseProviderList.length > 0) {
$app.data.avatarRemoteDatabaseProvider =
$app.data.avatarRemoteDatabaseProviderList[0];
}
$app.data.sortFavorites = await configRepository.getBool(
'VRCX_sortFavorites',
true
);
$app.data.randomUserColours = await configRepository.getBool(
'VRCX_randomUserColours',
false
);
$app.data.hideUserNotes = await configRepository.getBool(
'VRCX_hideUserNotes',
false
);
$app.data.hideUserMemos = await configRepository.getBool(
'VRCX_hideUserMemos',
false
);
$app.data.hideUnfriends = await configRepository.getBool(
'VRCX_hideUnfriends',
false
);
$app.data.friendLogTable.filters[2].value = $app.data.hideUnfriends;
$app.methods.saveOpenVROption = async function (configKey = '') {
switch (configKey) {
case 'openVR':
this.openVR = !this.openVR;
break;
case 'VRCX_hidePrivateFromFeed':
this.hidePrivateFromFeed = !this.hidePrivateFromFeed;
break;
case 'VRCX_hideDevicesFromFeed':
this.hideDevicesFromFeed = !this.hideDevicesFromFeed;
break;
case 'VRCX_vrOverlayCpuUsage':
this.vrOverlayCpuUsage = !this.vrOverlayCpuUsage;
break;
case 'VRCX_hideUptimeFromFeed':
this.hideUptimeFromFeed = !this.hideUptimeFromFeed;
break;
case 'VRCX_pcUptimeOnFeed':
this.pcUptimeOnFeed = !this.pcUptimeOnFeed;
break;
case 'VRCX_overlayNotifications':
this.overlayNotifications = !this.overlayNotifications;
break;
case 'VRCX_overlayWrist':
this.overlayWrist = !this.overlayWrist;
break;
case 'VRCX_xsNotifications':
this.xsNotifications = !this.xsNotifications;
break;
case 'VRCX_ovrtHudNotifications':
this.ovrtHudNotifications = !this.ovrtHudNotifications;
break;
case 'VRCX_ovrtWristNotifications':
this.ovrtWristNotifications = !this.ovrtWristNotifications;
break;
case 'VRCX_imageNotifications':
this.imageNotifications = !this.imageNotifications;
break;
case 'VRCX_afkDesktopToast':
this.afkDesktopToast = !this.afkDesktopToast;
break;
case 'VRCX_notificationTTSNickName':
this.notificationTTSNickName = !this.notificationTTSNickName;
break;
case 'VRCX_minimalFeed':
this.minimalFeed = !this.minimalFeed;
break;
case 'displayVRCPlusIconsAsAvatar':
this.displayVRCPlusIconsAsAvatar =
!this.displayVRCPlusIconsAsAvatar;
break;
case 'VRCX_hideTooltips':
this.hideTooltips = !this.hideTooltips;
break;
case 'VRCX_hideNicknames':
this.hideNicknames = !this.hideNicknames;
break;
case 'VRCX_autoSweepVRChatCache':
this.autoSweepVRChatCache = !this.autoSweepVRChatCache;
break;
case 'VRCX_relaunchVRChatAfterCrash':
this.relaunchVRChatAfterCrash = !this.relaunchVRChatAfterCrash;
break;
case 'VRCX_vrcQuitFix':
this.vrcQuitFix = !this.vrcQuitFix;
break;
case 'VRCX_vrBackgroundEnabled':
this.vrBackgroundEnabled = !this.vrBackgroundEnabled;
break;
case 'VRCX_avatarRemoteDatabase':
this.avatarRemoteDatabase = !this.avatarRemoteDatabase;
break;
case 'VRCX_udonExceptionLogging':
this.udonExceptionLogging = !this.udonExceptionLogging;
break;
default:
break;
}
await configRepository.setBool('openVR', this.openVR);
await configRepository.setBool('openVRAlways', this.openVRAlways);
await configRepository.setBool(
'VRCX_overlaybutton',
this.overlaybutton
);
this.overlayHand = parseInt(this.overlayHand, 10);
if (isNaN(this.overlayHand)) {
this.overlayHand = 0;
}
await configRepository.setInt('VRCX_overlayHand', this.overlayHand);
await configRepository.setBool(
'VRCX_hidePrivateFromFeed',
this.hidePrivateFromFeed
);
await configRepository.setBool(
'VRCX_hideDevicesFromFeed',
this.hideDevicesFromFeed
);
await configRepository.setBool(
'VRCX_vrOverlayCpuUsage',
this.vrOverlayCpuUsage
);
await configRepository.setBool(
'VRCX_hideUptimeFromFeed',
this.hideUptimeFromFeed
);
await configRepository.setBool(
'VRCX_pcUptimeOnFeed',
this.pcUptimeOnFeed
);
await configRepository.setBool(
'VRCX_overlayNotifications',
this.overlayNotifications
);
await configRepository.setBool('VRCX_overlayWrist', this.overlayWrist);
await configRepository.setBool(
'VRCX_xsNotifications',
this.xsNotifications
);
await configRepository.setBool(
'VRCX_ovrtHudNotifications',
this.ovrtHudNotifications
);
await configRepository.setBool(
'VRCX_ovrtWristNotifications',
this.ovrtWristNotifications
);
await configRepository.setBool(
'VRCX_imageNotifications',
this.imageNotifications
);
await configRepository.setString(
'VRCX_desktopToast',
this.desktopToast
);
await configRepository.setBool(
'VRCX_afkDesktopToast',
this.afkDesktopToast
);
await configRepository.setString(
'VRCX_overlayToast',
this.overlayToast
);
await configRepository.setBool(
'VRCX_notificationTTSNickName',
this.notificationTTSNickName
);
await configRepository.setBool('VRCX_minimalFeed', this.minimalFeed);
await configRepository.setBool(
'displayVRCPlusIconsAsAvatar',
this.displayVRCPlusIconsAsAvatar
);
await configRepository.setBool('VRCX_hideTooltips', this.hideTooltips);
await configRepository.setBool(
'VRCX_hideNicknames',
this.hideNicknames
);
await configRepository.setBool(
'VRCX_autoSweepVRChatCache',
this.autoSweepVRChatCache
);
await configRepository.setBool(
'VRCX_relaunchVRChatAfterCrash',
this.relaunchVRChatAfterCrash
);
await configRepository.setBool('VRCX_vrcQuitFix', this.vrcQuitFix);
await configRepository.setBool(
'VRCX_vrBackgroundEnabled',
this.vrBackgroundEnabled
);
await configRepository.setBool(
'VRCX_avatarRemoteDatabase',
this.avatarRemoteDatabase
);
await configRepository.setBool(
'VRCX_instanceUsersSortAlphabetical',
this.instanceUsersSortAlphabetical
);
await configRepository.setBool(
'VRCX_randomUserColours',
this.randomUserColours
);
await configRepository.setBool(
'VRCX_udonExceptionLogging',
this.udonExceptionLogging
);
this.updateSharedFeed(true);
this.updateVRConfigVars();
this.updateVRLastLocation();
AppApi.ExecuteVrOverlayFunction('notyClear', '');
this.updateOpenVR();
};
$app.methods.saveSortFavoritesOption = async function () {
this.getLocalWorldFavorites();
await configRepository.setBool(
'VRCX_sortFavorites',
this.sortFavorites
);
};
$app.methods.saveUserDialogOption = async function (configKey = '') {
if (configKey === 'VRCX_hideUserNotes') {
this.hideUserNotes = !this.hideUserNotes;
} else {
this.hideUserMemos = !this.hideUserMemos;
}
await configRepository.setBool(
'VRCX_hideUserNotes',
this.hideUserNotes
);
await configRepository.setBool(
'VRCX_hideUserMemos',
this.hideUserMemos
);
};
$app.methods.saveFriendLogOptions = async function () {
// The function is only called in adv settings
this.hideUnfriends = !this.hideUnfriends;
await configRepository.setBool(
'VRCX_hideUnfriends',
this.hideUnfriends
);
this.friendLogTable.filters[2].value = this.hideUnfriends;
};
$app.data.notificationTTSTest = '';
$app.data.TTSvoices = speechSynthesis.getVoices();
$app.methods.updateTTSVoices = function () {
this.TTSvoices = speechSynthesis.getVoices();
if (LINUX) {
let voices = speechSynthesis.getVoices();
let uniqueVoices = [];
voices.forEach((voice) => {
if (!uniqueVoices.some((v) => v.lang === voice.lang)) {
uniqueVoices.push(voice);
}
});
uniqueVoices = uniqueVoices.filter((v) => v.lang.startsWith('en'));
this.TTSvoices = uniqueVoices;
}
};
$app.methods.saveNotificationTTS = async function () {
speechSynthesis.cancel();
if (
(await configRepository.getString('VRCX_notificationTTS')) ===
'Never' &&
this.notificationTTS !== 'Never'
) {
this.speak('Notification text-to-speech enabled');
}
await configRepository.setString(
'VRCX_notificationTTS',
this.notificationTTS
);
this.updateVRConfigVars();
};
$app.methods.testNotificationTTS = function () {
speechSynthesis.cancel();
this.speak(this.notificationTTSTest);
};
$app.data.themeMode = await configRepository.getString(
'VRCX_ThemeMode',
'system'
);
$app.data.isDarkMode = false;
$app.methods.systemIsDarkMode = function () {
return window.matchMedia('(prefers-color-scheme: dark)').matches;
};
window
.matchMedia('(prefers-color-scheme: dark)')
.addEventListener('change', async () => {
if ($app.themeMode === 'system') {
await $app.changeThemeMode();
}
});
$app.methods.saveThemeMode = async function (newThemeMode) {
this.themeMode = newThemeMode;
await configRepository.setString('VRCX_ThemeMode', this.themeMode);
await this.changeThemeMode();
};
$app.methods.changeThemeMode = async function () {
if (
document.contains(document.getElementById('app-theme-dark-style'))
) {
document.getElementById('app-theme-dark-style').remove();
}
if (document.contains(document.getElementById('app-theme-style'))) {
document.getElementById('app-theme-style').remove();
}
var $appThemeStyle = document.createElement('link');
$appThemeStyle.setAttribute('id', 'app-theme-style');
$appThemeStyle.rel = 'stylesheet';
switch (this.themeMode) {
case 'light':
$appThemeStyle.href = '';
this.isDarkMode = false;
break;
case 'dark':
$appThemeStyle.href = '';
this.isDarkMode = true;
break;
case 'darkvanillaold':
$appThemeStyle.href = 'theme.darkvanillaold.css';
this.isDarkMode = true;
break;
case 'darkvanilla':
$appThemeStyle.href = 'theme.darkvanilla.css';
this.isDarkMode = true;
break;
case 'pink':
$appThemeStyle.href = 'theme.pink.css';
this.isDarkMode = true;
break;
case 'material3':
$appThemeStyle.href = 'theme.material3.css';
this.isDarkMode = true;
break;
case 'system':
this.isDarkMode = this.systemIsDarkMode();
break;
}
if (this.isDarkMode) {
AppApi.ChangeTheme(1);
var $appThemeDarkStyle = document.createElement('link');
$appThemeDarkStyle.setAttribute('id', 'app-theme-dark-style');
$appThemeDarkStyle.rel = 'stylesheet';
$appThemeDarkStyle.href = 'theme.dark.css';
document.head.appendChild($appThemeDarkStyle);
} else {
AppApi.ChangeTheme(0);
}
if ($appThemeStyle.href) {
document.head.appendChild($appThemeStyle);
}
this.updateVRConfigVars();
await this.updateTrustColor();
await this.applyWineEmojis();
};
$app.methods.applyWineEmojis = async function () {
if (document.contains(document.getElementById('app-emoji-font'))) {
document.getElementById('app-emoji-font').remove();
}
if (this.isRunningUnderWine) {
var $appEmojiFont = document.createElement('link');
$appEmojiFont.setAttribute('id', 'app-emoji-font');
$appEmojiFont.rel = 'stylesheet';
$appEmojiFont.href = 'emoji.font.css';
document.head.appendChild($appEmojiFont);
}
};
$app.data.isStartAtWindowsStartup = await configRepository.getBool(
'VRCX_StartAtWindowsStartup',
false
);
$app.data.isStartAsMinimizedState =
(await VRCXStorage.Get('VRCX_StartAsMinimizedState')) === 'true';
$app.data.isCloseToTray =
(await VRCXStorage.Get('VRCX_CloseToTray')) === 'true';
if (await configRepository.getBool('VRCX_CloseToTray')) {
// move back to JSON
$app.data.isCloseToTray =
await configRepository.getBool('VRCX_CloseToTray');
VRCXStorage.Set('VRCX_CloseToTray', $app.data.isCloseToTray.toString());
await configRepository.remove('VRCX_CloseToTray');
}
if (!(await VRCXStorage.Get('VRCX_DatabaseLocation'))) {
await VRCXStorage.Set('VRCX_DatabaseLocation', '');
}
if (!(await VRCXStorage.Get('VRCX_ProxyServer'))) {
await VRCXStorage.Set('VRCX_ProxyServer', '');
}
if ((await VRCXStorage.Get('VRCX_DisableGpuAcceleration')) === '') {
await VRCXStorage.Set('VRCX_DisableGpuAcceleration', 'false');
}
if (
(await VRCXStorage.Get('VRCX_DisableVrOverlayGpuAcceleration')) === ''
) {
await VRCXStorage.Set('VRCX_DisableVrOverlayGpuAcceleration', 'false');
}
$app.data.proxyServer = await VRCXStorage.Get('VRCX_ProxyServer');
$app.data.disableGpuAcceleration =
(await VRCXStorage.Get('VRCX_DisableGpuAcceleration')) === 'true';
$app.data.locationX = await VRCXStorage.Get('VRCX_LocationX');
$app.data.locationY = await VRCXStorage.Get('VRCX_LocationY');
$app.data.sizeWidth = await VRCXStorage.Get('VRCX_SizeWidth');
$app.data.sizeHeight = await VRCXStorage.Get('VRCX_SizeHeight');
$app.data.windowState = await VRCXStorage.Get('VRCX_WindowState');
$app.data.disableVrOverlayGpuAcceleration =
(await VRCXStorage.Get('VRCX_DisableVrOverlayGpuAcceleration')) ===
'true';
$app.data.disableWorldDatabase =
(await VRCXStorage.Get('VRCX_DisableWorldDatabase')) === 'true';
$app.methods.saveVRCXWindowOption = async function (configKey = '') {
switch (configKey) {
case 'VRCX_StartAtWindowsStartup':
this.isStartAtWindowsStartup = !this.isStartAtWindowsStartup;
break;
case 'VRCX_saveInstancePrints':
this.saveInstancePrints = !this.saveInstancePrints;
break;
case 'VRCX_cropInstancePrints':
this.cropInstancePrints = !this.cropInstancePrints;
this.cropPrintsChanged();
break;
case 'VRCX_saveInstanceStickers':
this.saveInstanceStickers = !this.saveInstanceStickers;
break;
case 'VRCX_StartAsMinimizedState':
this.isStartAsMinimizedState = !this.isStartAsMinimizedState;
break;
case 'VRCX_CloseToTray':
this.isCloseToTray = !this.isCloseToTray;
break;
case 'VRCX_DisableWorldDatabase':
this.disableWorldDatabase = !this.disableWorldDatabase;
break;
case 'VRCX_DisableGpuAcceleration':
this.disableGpuAcceleration = !this.disableGpuAcceleration;
break;
case 'VRCX_DisableVrOverlayGpuAcceleration':
this.disableVrOverlayGpuAcceleration =
!this.disableVrOverlayGpuAcceleration;
break;
default:
break;
}
await configRepository.setBool(
'VRCX_StartAtWindowsStartup',
this.isStartAtWindowsStartup
);
await configRepository.setBool(
'VRCX_saveInstancePrints',
this.saveInstancePrints
);
await configRepository.setBool(
'VRCX_cropInstancePrints',
this.cropInstancePrints
);
await configRepository.setBool(
'VRCX_saveInstanceStickers',
this.saveInstanceStickers
);
VRCXStorage.Set(
'VRCX_StartAsMinimizedState',
this.isStartAsMinimizedState.toString()
);
VRCXStorage.Set('VRCX_CloseToTray', this.isCloseToTray.toString());
VRCXStorage.Set(
'VRCX_DisableWorldDatabase',
this.disableWorldDatabase.toString()
);
VRCXStorage.Set(
'VRCX_DisableGpuAcceleration',
this.disableGpuAcceleration.toString()
);
VRCXStorage.Set(
'VRCX_DisableVrOverlayGpuAcceleration',
this.disableVrOverlayGpuAcceleration.toString()
);
if (LINUX) {
VRCXStorage.Set('VRCX_LocationX', this.locationX);
VRCXStorage.Set('VRCX_LocationY', this.locationY);
VRCXStorage.Set('VRCX_SizeWidth', this.sizeWidth);
VRCXStorage.Set('VRCX_SizeHeight', this.sizeHeight);
VRCXStorage.Set('VRCX_WindowState', this.windowState);
VRCXStorage.Flush();
} else {
AppApi.SetStartup(this.isStartAtWindowsStartup);
}
};
$app.data.photonEventOverlay = await configRepository.getBool(
'VRCX_PhotonEventOverlay',
false
);
$app.data.timeoutHudOverlay = await configRepository.getBool(
'VRCX_TimeoutHudOverlay',
false
);
$app.data.timeoutHudOverlayFilter = await configRepository.getString(
'VRCX_TimeoutHudOverlayFilter',
'Everyone'
);
$app.data.photonEventOverlayFilter = await configRepository.getString(
'VRCX_PhotonEventOverlayFilter',
'Everyone'
);
$app.data.photonOverlayMessageTimeout = Number(
await configRepository.getString(
'VRCX_photonOverlayMessageTimeout',
6000
)
);
$app.data.gameLogDisabled = await configRepository.getBool(
'VRCX_gameLogDisabled',
false
);
$app.data.udonExceptionLogging = await configRepository.getBool(
'VRCX_udonExceptionLogging',
false
);
$app.data.instanceUsersSortAlphabetical = await configRepository.getBool(
'VRCX_instanceUsersSortAlphabetical',
false
);
$app.methods.saveEventOverlay = async function (configKey = '') {
if (configKey === 'VRCX_PhotonEventOverlay') {
this.photonEventOverlay = !this.photonEventOverlay;
} else if (configKey === 'VRCX_TimeoutHudOverlay') {
this.timeoutHudOverlay = !this.timeoutHudOverlay;
}
await configRepository.setBool(
'VRCX_PhotonEventOverlay',
this.photonEventOverlay
);
await configRepository.setBool(
'VRCX_TimeoutHudOverlay',
this.timeoutHudOverlay
);
await configRepository.setString(
'VRCX_TimeoutHudOverlayFilter',
this.timeoutHudOverlayFilter
);
await configRepository.setString(
'VRCX_PhotonEventOverlayFilter',
this.photonEventOverlayFilter
);
if (!this.timeoutHudOverlay) {
AppApi.ExecuteVrOverlayFunction('updateHudTimeout', '[]');
}
this.updateOpenVR();
this.updateVRConfigVars();
};
$app.data.logResourceLoad = await configRepository.getBool(
'VRCX_logResourceLoad',
false
);
$app.data.logEmptyAvatars = await configRepository.getBool(
'VRCX_logEmptyAvatars',
false
);
$app.methods.saveLoggingOptions = async function (configKey = '') {
if (configKey === 'VRCX_logResourceLoad') {
this.logResourceLoad = !this.logResourceLoad;
} else {
this.logEmptyAvatars = !this.logEmptyAvatars;
}
await configRepository.setBool(
'VRCX_logResourceLoad',
this.logResourceLoad
);
await configRepository.setBool(
'VRCX_logEmptyAvatars',
this.logEmptyAvatars
);
};
$app.data.autoStateChangeEnabled = await configRepository.getBool(
'VRCX_autoStateChangeEnabled',
false
);
$app.data.autoStateChangeNoFriends = await configRepository.getBool(
'VRCX_autoStateChangeNoFriends',
false
);
$app.data.autoStateChangeInstanceTypes = JSON.parse(
await configRepository.getString(
'VRCX_autoStateChangeInstanceTypes',
'[]'
)
);
$app.data.autoStateChangeAloneStatus = await configRepository.getString(
'VRCX_autoStateChangeAloneStatus',
'join me'
);
$app.data.autoStateChangeCompanyStatus = await configRepository.getString(
'VRCX_autoStateChangeCompanyStatus',
'busy'
);
$app.data.autoAcceptInviteRequests = await configRepository.getString(
'VRCX_autoAcceptInviteRequests',
'Off'
);
$app.methods.saveAutomationOptions = async function (configKey = '') {
if (configKey === 'VRCX_autoStateChangeEnabled') {
this.autoStateChangeEnabled = !this.autoStateChangeEnabled;
await configRepository.setBool(
'VRCX_autoStateChangeEnabled',
this.autoStateChangeEnabled
);
}
await configRepository.setBool(
'VRCX_autoStateChangeNoFriends',
this.autoStateChangeNoFriends
);
await configRepository.setString(
'VRCX_autoStateChangeInstanceTypes',
JSON.stringify(this.autoStateChangeInstanceTypes)
);
await configRepository.setString(
'VRCX_autoStateChangeAloneStatus',
this.autoStateChangeAloneStatus
);
await configRepository.setString(
'VRCX_autoStateChangeCompanyStatus',
this.autoStateChangeCompanyStatus
);
await configRepository.setString(
'VRCX_autoAcceptInviteRequests',
this.autoAcceptInviteRequests
);
};
$app.data.isRegistryBackupDialogVisible = false;
$app.methods.showRegistryBackupDialog = function () {
this.isRegistryBackupDialogVisible = true;
};
$app.data.sidebarSortMethod1 = '';
$app.data.sidebarSortMethod2 = '';
$app.data.sidebarSortMethod3 = '';
$app.data.sidebarSortMethods = JSON.parse(
await configRepository.getString(
'VRCX_sidebarSortMethods',
JSON.stringify([
'Sort Private to Bottom',
'Sort by Time in Instance',
'Sort by Last Active'
])
)
);
if ($app.data.sidebarSortMethods?.length === 3) {
$app.data.sidebarSortMethod1 = $app.data.sidebarSortMethods[0];
$app.data.sidebarSortMethod2 = $app.data.sidebarSortMethods[1];
$app.data.sidebarSortMethod3 = $app.data.sidebarSortMethods[2];
}
// Migrate old settings
// Assume all exist if one does
const orderFriendsGroupPrivate = await configRepository.getBool(
'orderFriendGroupPrivate'
);
if (orderFriendsGroupPrivate !== null) {
await configRepository.remove('orderFriendGroupPrivate');
const orderFriendsGroupStatus = await configRepository.getBool(
'orderFriendsGroupStatus'
);
await configRepository.remove('orderFriendsGroupStatus');
const orderFriendsGroupGPS = await configRepository.getBool(
'orderFriendGroupGPS'
);
await configRepository.remove('orderFriendGroupGPS');
const orderOnlineFor =
await configRepository.getBool('orderFriendGroup0');
await configRepository.remove('orderFriendGroup0');
await configRepository.remove('orderFriendGroup1');
await configRepository.remove('orderFriendGroup2');
await configRepository.remove('orderFriendGroup3');
var sortOrder = [];
if (orderFriendsGroupPrivate) {
sortOrder.push('Sort Private to Bottom');
}
if (orderFriendsGroupStatus) {
sortOrder.push('Sort by Status');
}
if (orderOnlineFor && orderFriendsGroupGPS) {
sortOrder.push('Sort by Time in Instance');
}
if (!orderOnlineFor) {
sortOrder.push('Sort Alphabetically');
}
if (sortOrder.length > 0) {
while (sortOrder.length < 3) {
sortOrder.push('');
}
$app.data.sidebarSortMethods = sortOrder;
$app.data.sidebarSortMethod1 = sortOrder[0];
$app.data.sidebarSortMethod2 = sortOrder[1];
$app.data.sidebarSortMethod3 = sortOrder[2];
}
await configRepository.setString(
'VRCX_sidebarSortMethods',
JSON.stringify(sortOrder)
);
}
$app.methods.saveSidebarSortOrder = async function () {
if (this.sidebarSortMethod1 === this.sidebarSortMethod2) {
this.sidebarSortMethod2 = '';
}
if (this.sidebarSortMethod1 === this.sidebarSortMethod3) {
this.sidebarSortMethod3 = '';
}
if (this.sidebarSortMethod2 === this.sidebarSortMethod3) {
this.sidebarSortMethod3 = '';
}
if (!this.sidebarSortMethod1) {
this.sidebarSortMethod2 = '';
}
if (!this.sidebarSortMethod2) {
this.sidebarSortMethod3 = '';
}
this.sidebarSortMethods = [
this.sidebarSortMethod1,
this.sidebarSortMethod2,
this.sidebarSortMethod3
];
await configRepository.setString(
'VRCX_sidebarSortMethods',
JSON.stringify(this.sidebarSortMethods)
);
this.sortVIPFriends = true;
this.sortOnlineFriends = true;
this.sortActiveFriends = true;
this.sortOfflineFriends = true;
};
$app.data.discordActive = await configRepository.getBool(
'discordActive',
false
);
$app.data.discordInstance = await configRepository.getBool(
'discordInstance',
true
);
$app.data.discordJoinButton = await configRepository.getBool(
'discordJoinButton',
false
);
$app.data.discordHideInvite = await configRepository.getBool(
'discordHideInvite',
true
);
$app.data.discordHideImage = await configRepository.getBool(
'discordHideImage',
false
);
// setting defaults
$app.data.sharedFeedFiltersDefaults = {
noty: {
Location: 'Off',
OnPlayerJoined: 'VIP',
OnPlayerLeft: 'VIP',
OnPlayerJoining: 'VIP',
Online: 'VIP',
Offline: 'VIP',
GPS: 'Off',
Status: 'Off',
invite: 'Friends',
requestInvite: 'Friends',
inviteResponse: 'Friends',
requestInviteResponse: 'Friends',
friendRequest: 'On',
Friend: 'On',
Unfriend: 'On',
DisplayName: 'VIP',
TrustLevel: 'VIP',
boop: 'Off',
groupChange: 'On',
'group.announcement': 'On',
'group.informative': 'On',
'group.invite': 'On',
'group.joinRequest': 'Off',
'group.transfer': 'On',
'group.queueReady': 'On',
'instance.closed': 'On',
PortalSpawn: 'Everyone',
Event: 'On',
External: 'On',
VideoPlay: 'Off',
BlockedOnPlayerJoined: 'Off',
BlockedOnPlayerLeft: 'Off',
MutedOnPlayerJoined: 'Off',
MutedOnPlayerLeft: 'Off',
AvatarChange: 'Off',
ChatBoxMessage: 'Off',
Blocked: 'Off',
Unblocked: 'Off',
Muted: 'Off',
Unmuted: 'Off'
},
wrist: {
Location: 'On',
OnPlayerJoined: 'Everyone',
OnPlayerLeft: 'Everyone',
OnPlayerJoining: 'Friends',
Online: 'Friends',
Offline: 'Friends',
GPS: 'Friends',
Status: 'Friends',
invite: 'Friends',
requestInvite: 'Friends',
inviteResponse: 'Friends',
requestInviteResponse: 'Friends',
friendRequest: 'On',
Friend: 'On',
Unfriend: 'On',
DisplayName: 'Friends',
TrustLevel: 'Friends',
boop: 'On',
groupChange: 'On',
'group.announcement': 'On',
'group.informative': 'On',
'group.invite': 'On',
'group.joinRequest': 'On',
'group.transfer': 'On',
'group.queueReady': 'On',
'instance.closed': 'On',
PortalSpawn: 'Everyone',
Event: 'On',
External: 'On',
VideoPlay: 'On',
BlockedOnPlayerJoined: 'Off',
BlockedOnPlayerLeft: 'Off',
MutedOnPlayerJoined: 'Off',
MutedOnPlayerLeft: 'Off',
AvatarChange: 'Everyone',
ChatBoxMessage: 'Off',
Blocked: 'On',
Unblocked: 'On',
Muted: 'On',
Unmuted: 'On'
}
};
$app.data.sharedFeedFilters = $app.data.sharedFeedFiltersDefaults;
if (await configRepository.getString('sharedFeedFilters')) {
$app.data.sharedFeedFilters = JSON.parse(
await configRepository.getString(
'sharedFeedFilters',
JSON.stringify($app.data.sharedFeedFiltersDefaults)
)
);
}
if (!$app.data.sharedFeedFilters.noty.Blocked) {
$app.data.sharedFeedFilters.noty.Blocked = 'Off';
$app.data.sharedFeedFilters.noty.Unblocked = 'Off';
$app.data.sharedFeedFilters.noty.Muted = 'Off';
$app.data.sharedFeedFilters.noty.Unmuted = 'Off';
$app.data.sharedFeedFilters.wrist.Blocked = 'On';
$app.data.sharedFeedFilters.wrist.Unblocked = 'On';
$app.data.sharedFeedFilters.wrist.Muted = 'On';
$app.data.sharedFeedFilters.wrist.Unmuted = 'On';
}
if (!$app.data.sharedFeedFilters.noty['group.announcement']) {
$app.data.sharedFeedFilters.noty['group.announcement'] = 'On';
$app.data.sharedFeedFilters.noty['group.informative'] = 'On';
$app.data.sharedFeedFilters.noty['group.invite'] = 'On';
$app.data.sharedFeedFilters.noty['group.joinRequest'] = 'Off';
$app.data.sharedFeedFilters.wrist['group.announcement'] = 'On';
$app.data.sharedFeedFilters.wrist['group.informative'] = 'On';
$app.data.sharedFeedFilters.wrist['group.invite'] = 'On';
$app.data.sharedFeedFilters.wrist['group.joinRequest'] = 'On';
}
if (!$app.data.sharedFeedFilters.noty['group.queueReady']) {
$app.data.sharedFeedFilters.noty['group.queueReady'] = 'On';
$app.data.sharedFeedFilters.wrist['group.queueReady'] = 'On';
}
if (!$app.data.sharedFeedFilters.noty['instance.closed']) {
$app.data.sharedFeedFilters.noty['instance.closed'] = 'On';
$app.data.sharedFeedFilters.wrist['instance.closed'] = 'On';
}
if (!$app.data.sharedFeedFilters.noty.External) {
$app.data.sharedFeedFilters.noty.External = 'On';
$app.data.sharedFeedFilters.wrist.External = 'On';
}
if (!$app.data.sharedFeedFilters.noty.groupChange) {
$app.data.sharedFeedFilters.noty.groupChange = 'On';
$app.data.sharedFeedFilters.wrist.groupChange = 'On';
}
if (!$app.data.sharedFeedFilters.noty['group.transfer']) {
$app.data.sharedFeedFilters.noty['group.transfer'] = 'On';
$app.data.sharedFeedFilters.wrist['group.transfer'] = 'On';
}
if (!$app.data.sharedFeedFilters.noty.boop) {
$app.data.sharedFeedFilters.noty.boop = 'Off';
$app.data.sharedFeedFilters.wrist.boop = 'On';
}
$app.data.trustColor = JSON.parse(
await configRepository.getString(
'VRCX_trustColor',
JSON.stringify({
untrusted: '#CCCCCC',
basic: '#1778FF',
known: '#2BCF5C',
trusted: '#FF7B42',
veteran: '#B18FFF',
vip: '#FF2626',
troll: '#782F2F'
})
)
);
$app.methods.updateTrustColor = async function (setRandomColor = false) {
if (setRandomColor) {
this.randomUserColours = !this.randomUserColours;
}
if (typeof API.currentUser?.id === 'undefined') {
return;
}
await configRepository.setBool(
'VRCX_randomUserColours',
this.randomUserColours
);
await configRepository.setString(
'VRCX_trustColor',
JSON.stringify(this.trustColor)
);
if (this.randomUserColours) {
this.getNameColour(API.currentUser.id).then((colour) => {
API.currentUser.$userColour = colour;
});
this.userColourInit();
} else {
API.applyUserTrustLevel(API.currentUser);
API.cachedUsers.forEach((ref) => {
API.applyUserTrustLevel(ref);
});
}
await this.updateTrustColorClasses();
};
$app.methods.updateTrustColorClasses = async function () {
var trustColor = JSON.parse(
await configRepository.getString(
'VRCX_trustColor',
JSON.stringify({
untrusted: '#CCCCCC',
basic: '#1778FF',
known: '#2BCF5C',
trusted: '#FF7B42',
veteran: '#B18FFF',
vip: '#FF2626',
troll: '#782F2F'
})
)
);
if (document.getElementById('trustColor') !== null) {
document.getElementById('trustColor').outerHTML = '';
}
var style = document.createElement('style');
style.id = 'trustColor';
style.type = 'text/css';
var newCSS = '';
for (var rank in trustColor) {
newCSS += `.x-tag-${rank} { color: ${trustColor[rank]} !important; border-color: ${trustColor[rank]} !important; } `;
}
style.innerHTML = newCSS;
document.getElementsByTagName('head')[0].appendChild(style);
};
await $app.methods.updateTrustColorClasses();
$app.data.notificationPosition = await configRepository.getString(
'VRCX_notificationPosition',
'topCenter'
);
$app.methods.changeNotificationPosition = async function (value) {
this.notificationPosition = value;
await configRepository.setString(
'VRCX_notificationPosition',
this.notificationPosition
);
this.updateVRConfigVars();
};
$app.data.youTubeApi = await configRepository.getBool(
'VRCX_youtubeAPI',
false
);
$app.data.youTubeApiKey = await configRepository.getString(
'VRCX_youtubeAPIKey',
''
);
$app.data.progressPie = await configRepository.getBool(
'VRCX_progressPie',
false
);
$app.data.progressPieFilter = await configRepository.getBool(
'VRCX_progressPieFilter',
true
);
$app.data.screenshotHelper = await configRepository.getBool(
'VRCX_screenshotHelper',
true
);
$app.data.screenshotHelperModifyFilename = await configRepository.getBool(
'VRCX_screenshotHelperModifyFilename',
false
);
$app.data.screenshotHelperCopyToClipboard = await configRepository.getBool(
'VRCX_screenshotHelperCopyToClipboard',
false
);
$app.data.enableAppLauncher = await configRepository.getBool(
'VRCX_enableAppLauncher',
true
);
$app.data.enableAppLauncherAutoClose = await configRepository.getBool(
'VRCX_enableAppLauncherAutoClose',
true
);
$app.data.showConfirmationOnSwitchAvatar = await configRepository.getBool(
'VRCX_showConfirmationOnSwitchAvatar',
false
);
$app.methods.updateVRConfigVars = function () {
var notificationTheme = 'relax';
if (this.isDarkMode) {
notificationTheme = 'sunset';
}
var VRConfigVars = {
overlayNotifications: this.overlayNotifications,
hideDevicesFromFeed: this.hideDevicesFromFeed,
vrOverlayCpuUsage: this.vrOverlayCpuUsage,
minimalFeed: this.minimalFeed,
notificationPosition: this.notificationPosition,
notificationTimeout: this.notificationTimeout,
photonOverlayMessageTimeout: this.photonOverlayMessageTimeout,
notificationTheme,
backgroundEnabled: this.vrBackgroundEnabled,
dtHour12: this.dtHour12,
pcUptimeOnFeed: this.pcUptimeOnFeed,
appLanguage: this.appLanguage
};
var json = JSON.stringify(VRConfigVars);
AppApi.ExecuteVrFeedFunction('configUpdate', json);
AppApi.ExecuteVrOverlayFunction('configUpdate', json);
};
$app.methods.isRpcWorld = function (location) {
var rpcWorlds = [
'wrld_f20326da-f1ac-45fc-a062-609723b097b1',
'wrld_42377cf1-c54f-45ed-8996-5875b0573a83',
'wrld_dd6d2888-dbdc-47c2-bc98-3d631b2acd7c',
'wrld_52bdcdab-11cd-4325-9655-0fb120846945',
'wrld_2d40da63-8f1f-4011-8a9e-414eb8530acd',
'wrld_10e5e467-fc65-42ed-8957-f02cace1398c',
'wrld_04899f23-e182-4a8d-b2c7-2c74c7c15534',
'wrld_435bbf25-f34f-4b8b-82c6-cd809057eb8e',
'wrld_db9d878f-6e76-4776-8bf2-15bcdd7fc445',
'wrld_f767d1c8-b249-4ecc-a56f-614e433682c8',
'wrld_74970324-58e8-4239-a17b-2c59dfdf00db',
'wrld_266523e8-9161-40da-acd0-6bd82e075833'
];
var L = parseLocation(location);
if (rpcWorlds.includes(L.worldId)) {
return true;
}
return false;
};
$app.methods.updateVRLastLocation = function () {
var progressPie = false;
if (this.progressPie) {
progressPie = true;
if (this.progressPieFilter) {
if (!this.isRpcWorld(this.lastLocation.location)) {
progressPie = false;
}
}
}
var onlineFor = '';
if (!this.hideUptimeFromFeed) {
onlineFor = API.currentUser.$online_for;
}
var lastLocation = {
date: this.lastLocation.date,
location: this.lastLocation.location,
name: this.lastLocation.name,
playerList: Array.from(this.lastLocation.playerList.values()),
friendList: Array.from(this.lastLocation.friendList.values()),
progressPie,
onlineFor
};
var json = JSON.stringify(lastLocation);
AppApi.ExecuteVrFeedFunction('lastLocationUpdate', json);
AppApi.ExecuteVrOverlayFunction('lastLocationUpdate', json);
};
$app.methods.vrInit = function () {
this.updateVRConfigVars();
this.updateVRLastLocation();
this.updateVrNowPlaying();
this.updateSharedFeed(true);
this.onlineFriendCount = 0;
this.updateOnlineFriendCoutner();
};
API.$on('LOGIN', function () {
$app.pastDisplayNameTable.data = [];
});
API.$on('USER:CURRENT', function (args) {
if (args.ref.pastDisplayNames) {
$app.pastDisplayNameTable.data = args.ref.pastDisplayNames;
}
});
$app.methods.updateOpenVR = function () {
if (
this.openVR &&
this.isSteamVRRunning &&
((this.isGameRunning && !this.isGameNoVR) || this.openVRAlways)
) {
var hmdOverlay = false;
if (
this.overlayNotifications ||
this.progressPie ||
this.photonEventOverlay ||
this.timeoutHudOverlay
) {
hmdOverlay = true;
}
// active, hmdOverlay, wristOverlay, menuButton, overlayHand
AppApi.SetVR(
true,
hmdOverlay,
this.overlayWrist,
this.overlaybutton,
this.overlayHand
);
} else {
AppApi.SetVR(false, false, false, false, 0);
}
};
$app.methods.getTTSVoiceName = function () {
var voices;
if (LINUX) {
voices = this.TTSvoices;
} else {
voices = speechSynthesis.getVoices();
}
if (voices.length === 0) {
return '';
}
if (this.notificationTTSVoice >= voices.length) {
this.notificationTTSVoice = 0;
configRepository.setString(
'VRCX_notificationTTSVoice',
this.notificationTTSVoice
);
}
return voices[this.notificationTTSVoice].name;
};
$app.methods.changeTTSVoice = async function (index) {
this.notificationTTSVoice = index;
await configRepository.setString(
'VRCX_notificationTTSVoice',
this.notificationTTSVoice
);
var voices;
if (LINUX) {
voices = this.TTSvoices;
} else {
voices = speechSynthesis.getVoices();
}
if (voices.length === 0) {
return;
}
var voiceName = voices[index].name;
speechSynthesis.cancel();
this.speak(voiceName);
};
$app.methods.speak = function (text) {
var tts = new SpeechSynthesisUtterance();
var voices = speechSynthesis.getVoices();
if (voices.length === 0) {
return;
}
var index = 0;
if (this.notificationTTSVoice < voices.length) {
index = this.notificationTTSVoice;
}
tts.voice = voices[index];
tts.text = text;
speechSynthesis.speak(tts);
};
$app.methods.directAccessPaste = function () {
AppApi.GetClipboard().then((clipboard) => {
if (!this.directAccessParse(clipboard.trim())) {
this.promptOmniDirectDialog();
}
});
};
$app.methods.directAccessWorld = function (textBoxInput) {
var input = textBoxInput;
if (input.startsWith('/home/')) {
input = `https://vrchat.com${input}`;
}
if (input.length === 8) {
return this.verifyShortName('', input);
} else if (input.startsWith('https://vrch.at/')) {
var shortName = input.substring(16, 24);
return this.verifyShortName('', shortName);
} else if (
input.startsWith('https://vrchat.') ||
input.startsWith('/home/')
) {
var url = new URL(input);
var urlPath = url.pathname;
var urlPathSplit = urlPath.split('/');
if (urlPathSplit.length >= 4 && urlPathSplit[2] === 'world') {
var worldId = urlPathSplit[3];
this.showWorldDialog(worldId);
return true;
} else if (urlPath.substring(5, 12) === '/launch') {
var urlParams = new URLSearchParams(url.search);
var worldId = urlParams.get('worldId');
var instanceId = urlParams.get('instanceId');
if (instanceId) {
var shortName = urlParams.get('shortName');
var location = `${worldId}:${instanceId}`;
if (shortName) {
return this.verifyShortName(location, shortName);
}
this.showWorldDialog(location);
return true;
} else if (worldId) {
this.showWorldDialog(worldId);
return true;
}
}
} else if (input.substring(0, 5) === 'wrld_') {
// a bit hacky, but supports weird malformed inputs cut out from url, why not
if (input.indexOf('&instanceId=') >= 0) {
input = `https://vrchat.com/home/launch?worldId=${input}`;
return this.directAccessWorld(input);
}
this.showWorldDialog(input.trim());
return true;
}
return false;
};
$app.methods.verifyShortName = function (location, shortName) {
return instanceRequest
.getInstanceFromShortName({ shortName })
.then((args) => {
var newLocation = args.json.location;
var newShortName = args.json.shortName;
if (newShortName) {
this.showWorldDialog(newLocation, newShortName);
} else if (newLocation) {
this.showWorldDialog(newLocation);
} else {
this.showWorldDialog(location);
}
return args;
});
};
$app.methods.showGroupDialogShortCode = function (shortCode) {
groupRequest.groupStrictsearch({ query: shortCode }).then((args) => {
for (const group of args.json) {
// API.$on('GROUP:STRICTSEARCH', function (args) {
// for (var json of args.json) {
API.$emit('GROUP', {
group,
params: {
groupId: group.id
}
});
// }
// });
if (`${group.shortCode}.${group.discriminator}` === shortCode) {
this.showGroupDialog(group.id);
}
}
return args;
});
};
$app.methods.directAccessParse = function (input) {
if (!input) {
return false;
}
if (this.directAccessWorld(input)) {
return true;
}
if (input.startsWith('https://vrchat.')) {
var url = new URL(input);
var urlPath = url.pathname;
var urlPathSplit = urlPath.split('/');
if (urlPathSplit.length < 4) {
return false;
}
var type = urlPathSplit[2];
if (type === 'user') {
var userId = urlPathSplit[3];
this.showUserDialog(userId);
return true;
} else if (type === 'avatar') {
var avatarId = urlPathSplit[3];
this.showAvatarDialog(avatarId);
return true;
} else if (type === 'group') {
var groupId = urlPathSplit[3];
this.showGroupDialog(groupId);
return true;
}
} else if (input.startsWith('https://vrc.group/')) {
var shortCode = input.substring(18);
this.showGroupDialogShortCode(shortCode);
return true;
} else if (/^[A-Za-z0-9]{3,6}\.[0-9]{4}$/g.test(input)) {
this.showGroupDialogShortCode(input);
return true;
} else if (
input.substring(0, 4) === 'usr_' ||
/^[A-Za-z0-9]{10}$/g.test(input)
) {
this.showUserDialog(input);
return true;
} else if (input.substring(0, 5) === 'avtr_') {
this.showAvatarDialog(input);
return true;
} else if (input.substring(0, 4) === 'grp_') {
this.showGroupDialog(input);
return true;
}
return false;
};
$app.methods.setTablePageSize = async function (pageSize) {
this.tablePageSize = pageSize;
this.feedTable.pageSize = pageSize;
this.gameLogTable.pageSize = pageSize;
this.friendLogTable.pageSize = pageSize;
this.playerModerationTable.pageSize = pageSize;
this.notificationTable.pageSize = pageSize;
await configRepository.setInt('VRCX_tablePageSize', pageSize);
};
// #endregion
// #region | App: Dialog
$app.methods.adjustDialogZ = function (el) {
var z = 0;
document
.querySelectorAll('.v-modal,.el-dialog__wrapper')
.forEach((v) => {
var _z = Number(v.style.zIndex) || 0;
if (_z && _z > z && v !== el) {
z = _z;
}
});
if (z) {
el.style.zIndex = z + 1;
}
};
// #endregion
// #region | App: User Dialog
$app.data.userDialog = {
visible: false,
loading: false,
id: '',
ref: {},
friend: {},
isFriend: false,
note: '',
noteSaving: false,
incomingRequest: false,
outgoingRequest: false,
isBlock: false,
isMute: false,
isHideAvatar: false,
isShowAvatar: false,
isInteractOff: false,
isMuteChat: false,
isFavorite: false,
$location: {},
$homeLocationName: '',
users: [],
instance: {},
worlds: [],
avatars: [],
isWorldsLoading: false,
isFavoriteWorldsLoading: false,
isAvatarsLoading: false,
isGroupsLoading: false,
worldSorting: {
name: $t('dialog.user.worlds.sorting.updated'),
value: 'updated'
},
worldOrder: {
name: $t('dialog.user.worlds.order.descending'),
value: 'descending'
},
// because userDialogGroupSortingOptions, just i18n key
groupSorting: {
name: 'dialog.user.groups.sorting.alphabetical',
value: 'alphabetical'
},
avatarSorting: 'update',
avatarReleaseStatus: 'all',
treeData: [],
memo: '',
$avatarInfo: {
ownerId: '',
avatarName: '',
fileCreatedAt: ''
},
representedGroup: {
bannerUrl: '',
description: '',
discriminator: '',
groupId: '',
iconUrl: '',
isRepresenting: false,
memberCount: 0,
memberVisibility: '',
name: '',
ownerId: '',
privacy: '',
shortCode: '',
$thumbnailUrl: ''
},
isRepresentedGroupLoading: false,
joinCount: 0,
timeSpent: 0,
lastSeen: '',
avatarModeration: 0,
previousDisplayNames: [],
dateFriended: '',
unFriended: false,
dateFriendedInfo: []
};
API.$on('LOGOUT', function () {
$app.userDialog.visible = false;
});
API.$on('USER', function (args) {
var { ref } = args;
var D = $app.userDialog;
if (D.visible === false || D.id !== ref.id) {
return;
}
D.ref = ref;
D.note = String(ref.note || '');
D.noteSaving = false;
D.incomingRequest = false;
D.outgoingRequest = false;
if (D.ref.friendRequestStatus === 'incoming') {
D.incomingRequest = true;
} else if (D.ref.friendRequestStatus === 'outgoing') {
D.outgoingRequest = true;
}
});
API.$on('USER', function (args) {
// refresh user dialog JSON tab
if (
!$app.userDialog.visible ||
$app.userDialog.id !== args.ref.id ||
$app.$refs.userDialogTabs?.currentName !== '5'
) {
return;
}
$app.refreshUserDialogTreeData();
});
API.$on('WORLD', function (args) {
var D = $app.userDialog;
if (D.visible === false || D.$location.worldId !== args.ref.id) {
return;
}
$app.applyUserDialogLocation();
});
API.$on('FRIEND:STATUS', function (args) {
var D = $app.userDialog;
if (D.visible === false || D.id !== args.params.userId) {
return;
}
var { json } = args;
D.isFriend = json.isFriend;
D.incomingRequest = json.incomingRequest;
D.outgoingRequest = json.outgoingRequest;
});
API.$on('FRIEND:REQUEST:CANCEL', function (args) {
var D = $app.userDialog;
if (D.visible === false || D.id !== args.params.userId) {
return;
}
D.outgoingRequest = false;
});
API.$on('NOTIFICATION', function (args) {
var { ref } = args;
var D = $app.userDialog;
if (
D.visible === false ||
ref.$isDeleted ||
ref.type !== 'friendRequest' ||
ref.senderUserId !== D.id
) {
return;
}
D.incomingRequest = true;
});
API.$on('NOTIFICATION:ACCEPT', function (args) {
var { ref } = args;
var D = $app.userDialog;
// 얘는 @DELETE가 오고나서 ACCEPT가 옴
// 따라서 $isDeleted라면 ref가 undefined가 됨
if (
D.visible === false ||
typeof ref === 'undefined' ||
ref.type !== 'friendRequest' ||
ref.senderUserId !== D.id
) {
return;
}
D.isFriend = true;
});
API.$on('NOTIFICATION:EXPIRE', function (args) {
var { ref } = args;
var D = $app.userDialog;
if (
D.visible === false ||
ref.type !== 'friendRequest' ||
ref.senderUserId !== D.id
) {
return;
}
D.incomingRequest = false;
});
API.$on('FRIEND:DELETE', function (args) {
var D = $app.userDialog;
if (D.visible === false || D.id !== args.params.userId) {
return;
}
D.isFriend = false;
});
API.$on('PLAYER-MODERATION:@SEND', function (args) {
var { ref } = args;
var D = $app.userDialog;
if (
D.visible === false ||
(ref.targetUserId !== D.id &&
ref.sourceUserId !== this.currentUser.id)
) {
return;
}
if (ref.type === 'block') {
D.isBlock = true;
} else if (ref.type === 'mute') {
D.isMute = true;
} else if (ref.type === 'hideAvatar') {
D.isHideAvatar = true;
} else if (ref.type === 'interactOff') {
D.isInteractOff = true;
} else if (ref.type === 'muteChat') {
D.isMuteChat = true;
}
$app.$message({
message: $t('message.user.moderated'),
type: 'success'
});
});
API.$on('PLAYER-MODERATION:@DELETE', function (args) {
var { ref } = args;
var D = $app.userDialog;
if (
D.visible === false ||
ref.targetUserId !== D.id ||
ref.sourceUserId !== this.currentUser.id
) {
return;
}
if (ref.type === 'block') {
D.isBlock = false;
} else if (ref.type === 'mute') {
D.isMute = false;
} else if (ref.type === 'hideAvatar') {
D.isHideAvatar = false;
} else if (ref.type === 'interactOff') {
D.isInteractOff = false;
} else if (ref.type === 'muteChat') {
D.isMuteChat = false;
}
});
API.$on('FAVORITE', function (args) {
var { ref } = args;
var D = $app.userDialog;
if (D.visible === false || ref.$isDeleted || ref.favoriteId !== D.id) {
return;
}
D.isFavorite = true;
});
API.$on('FAVORITE:@DELETE', function (args) {
var D = $app.userDialog;
if (D.visible === false || D.id !== args.ref.favoriteId) {
return;
}
D.isFavorite = false;
});
$app.methods.showUserDialog = function (userId) {
if (!userId) {
return;
}
const D = this.userDialog;
D.id = userId;
D.treeData = [];
D.memo = '';
D.note = '';
D.noteSaving = false;
this.getUserMemo(userId).then((memo) => {
if (memo.userId === userId) {
D.memo = memo.memo;
const ref = this.friends.get(userId);
if (ref) {
ref.memo = String(memo.memo || '');
if (memo.memo) {
ref.$nickName = memo.memo.split('\n')[0];
} else {
ref.$nickName = '';
}
}
}
});
D.visible = true;
D.loading = true;
D.avatars = [];
D.worlds = [];
D.instance = {
id: '',
tag: '',
$location: {},
friendCount: 0,
users: [],
shortName: '',
ref: {}
};
D.isRepresentedGroupLoading = true;
D.representedGroup = {
bannerUrl: '',
description: '',
discriminator: '',
groupId: '',
iconUrl: '',
isRepresenting: false,
memberCount: 0,
memberVisibility: '',
name: '',
ownerId: '',
privacy: '',
shortCode: '',
$thumbnailUrl: ''
};
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 = [];
if (userId === API.currentUser.id) {
this.getWorldName(API.currentUser.homeLocation).then(
(worldName) => {
D.$homeLocationName = worldName;
}
);
}
AppApi.SendIpc('ShowUserDialog', userId);
userRequest
.getCachedUser({
userId
})
.catch((err) => {
D.loading = false;
D.visible = false;
this.$message({
message: 'Failed to load user',
type: 'error'
});
throw err;
})
.then((args) => {
if (args.ref.id === D.id) {
requestAnimationFrame(() => {
D.ref = args.ref;
D.friend = this.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 API.cachedPlayerModerations.values()) {
if (
ref.targetUserId === D.id &&
ref.sourceUserId === API.currentUser.id
) {
if (ref.type === 'block') {
D.isBlock = true;
} else if (ref.type === 'mute') {
D.isMute = true;
} else if (ref.type === 'hideAvatar') {
D.isHideAvatar = true;
} else if (ref.type === 'interactOff') {
D.isInteractOff = true;
} else if (ref.type === 'muteChat') {
D.isMuteChat = true;
}
}
}
D.isFavorite = API.cachedFavoritesByObjectId.has(D.id);
if (D.ref.friendRequestStatus === 'incoming') {
D.incomingRequest = true;
} else if (D.ref.friendRequestStatus === 'outgoing') {
D.outgoingRequest = true;
}
this.applyUserDialogLocation(true);
if (args.cache) {
userRequest.getUser(args.params);
}
let inCurrentWorld = false;
if (this.lastLocation.playerList.has(D.ref.id)) {
inCurrentWorld = true;
}
if (userId !== API.currentUser.id) {
database
.getUserStats(D.ref, inCurrentWorld)
.then((ref1) => {
if (ref1.userId === D.id) {
D.lastSeen = ref1.lastSeen;
D.joinCount = ref1.joinCount;
D.timeSpent = ref1.timeSpent;
}
let displayNameMap =
ref1.previousDisplayNames;
this.friendLogTable.data.forEach((ref2) => {
if (ref2.userId === D.id) {
if (ref2.type === 'DisplayName') {
displayNameMap.set(
ref2.previousDisplayName,
ref2.created_at
);
}
if (!D.dateFriended) {
if (ref2.type === 'Unfriend') {
D.unFriended = true;
if (!this.hideUnfriends) {
D.dateFriended =
ref2.created_at;
}
}
if (ref2.type === 'Friend') {
D.unFriended = false;
D.dateFriended =
ref2.created_at;
}
}
if (
ref2.type === 'Friend' ||
(ref2.type === 'Unfriend' &&
!this.hideUnfriends)
) {
D.dateFriendedInfo.push(ref2);
}
}
});
const displayNameMapSorted = new Map(
[...displayNameMap.entries()].sort(
(a, b) => b[1] - a[1]
)
);
D.previousDisplayNames = Array.from(
displayNameMapSorted.keys()
);
});
AppApi.GetVRChatUserModeration(
API.currentUser.id,
userId
).then((result) => {
D.avatarModeration = result;
if (result === 4) {
D.isHideAvatar = true;
} else if (result === 5) {
D.isShowAvatar = true;
}
});
} else {
database
.getUserStats(D.ref, inCurrentWorld)
.then((ref1) => {
if (ref1.userId === D.id) {
D.lastSeen = ref1.lastSeen;
D.joinCount = ref1.joinCount;
D.timeSpent = ref1.timeSpent;
}
});
}
groupRequest
.getRepresentedGroup({ userId })
.then((args1) => {
D.representedGroup = args1.json;
D.representedGroup.$thumbnailUrl =
convertFileUrlToImageUrl(
args1.json.iconUrl
);
if (!args1.json || !args1.json.isRepresenting) {
D.isRepresentedGroupLoading = false;
}
});
D.loading = false;
});
}
});
this.showUserDialogHistory.delete(userId);
this.showUserDialogHistory.add(userId);
this.quickSearchItems = this.quickSearchUserHistory();
};
$app.methods.applyUserDialogLocation = function (updateInstanceOccupants) {
var D = this.userDialog;
if (!D.visible) {
return;
}
var L = parseLocation(D.ref.$location.tag);
if (updateInstanceOccupants && L.isRealInstance) {
instanceRequest.getInstance({
worldId: L.worldId,
instanceId: L.instanceId
});
}
D.$location = L;
if (L.userId) {
var ref = API.cachedUsers.get(L.userId);
if (typeof ref === 'undefined') {
userRequest
.getUser({
userId: L.userId
})
.then((args) => {
Vue.set(L, 'user', args.ref);
return args;
});
} else {
L.user = ref;
}
}
var users = [];
var friendCount = 0;
var playersInInstance = this.lastLocation.playerList;
var cachedCurrentUser = API.cachedUsers.get(API.currentUser.id);
var currentLocation = cachedCurrentUser.$location.tag;
if (!L.isOffline && currentLocation === L.tag) {
var ref = API.cachedUsers.get(API.currentUser.id);
if (typeof ref !== 'undefined') {
users.push(ref); // add self
}
}
// dont use gamelog when using api location
if (
this.lastLocation.location === L.tag &&
playersInInstance.size > 0
) {
var friendsInInstance = this.lastLocation.friendList;
for (var friend of friendsInInstance.values()) {
// if friend isn't in instance add them
var addUser = !users.some(function (user) {
return friend.userId === user.id;
});
if (addUser) {
var ref = API.cachedUsers.get(friend.userId);
if (typeof ref !== 'undefined') {
users.push(ref);
}
}
}
friendCount = users.length - 1;
}
if (!L.isOffline) {
for (var friend of this.friends.values()) {
if (typeof friend.ref === 'undefined') {
continue;
}
if (friend.ref.location === this.lastLocation.location) {
// don't add friends to currentUser gameLog instance (except when traveling)
continue;
}
if (friend.ref.$location.tag === L.tag) {
if (
friend.state !== 'online' &&
friend.ref.location === 'private'
) {
// don't add offline friends to private instances
continue;
}
// if friend isn't in instance add them
var addUser = !users.some(function (user) {
return friend.name === user.displayName;
});
if (addUser) {
users.push(friend.ref);
}
}
}
friendCount = users.length;
}
if (this.instanceUsersSortAlphabetical) {
users.sort(compareByDisplayName);
} else {
users.sort(compareByLocationAt);
}
D.users = users;
if (
L.worldId &&
currentLocation === L.tag &&
playersInInstance.size > 0
) {
D.instance = {
id: L.instanceId,
tag: L.tag,
$location: L,
friendCount: 0,
users: [],
shortName: '',
ref: {}
};
}
if (!L.isRealInstance) {
D.instance = {
id: L.instanceId,
tag: L.tag,
$location: L,
friendCount: 0,
users: [],
shortName: '',
ref: {}
};
}
var instanceRef = API.cachedInstances.get(L.tag);
if (typeof instanceRef !== 'undefined') {
D.instance.ref = instanceRef;
}
D.instance.friendCount = friendCount;
this.updateTimers();
};
// #endregion
// #region | App: player list
API.$on('LOGIN', function () {
$app.currentInstanceUserList.data = [];
});
API.$on('USER:APPLY', function (ref) {
// add user ref to playerList, friendList, photonLobby, photonLobbyCurrent
var playerListRef = $app.lastLocation.playerList.get(ref.id);
if (playerListRef) {
// add/remove friends from lastLocation.friendList
if (
!$app.lastLocation.friendList.has(ref.id) &&
$app.friends.has(ref.id)
) {
var userMap = {
displayName: ref.displayName,
userId: ref.id,
joinTime: playerListRef.joinTime
};
$app.lastLocation.friendList.set(ref.id, userMap);
}
if (
$app.lastLocation.friendList.has(ref.id) &&
!$app.friends.has(ref.id)
) {
$app.lastLocation.friendList.delete(ref.id);
}
$app.photonLobby.forEach((ref1, id) => {
if (
typeof ref1 !== 'undefined' &&
ref1.displayName === ref.displayName &&
ref1 !== ref
) {
$app.photonLobby.set(id, ref);
if ($app.photonLobbyCurrent.has(id)) {
$app.photonLobbyCurrent.set(id, ref);
}
}
});
$app.getCurrentInstanceUserList();
}
});
$app.data.updatePlayerListTimer = null;
$app.data.updatePlayerListPending = false;
$app.methods.getCurrentInstanceUserList = function () {
if (!this.friendLogInitStatus) {
return;
}
if (this.updatePlayerListTimer) {
this.updatePlayerListPending = true;
} else {
this.updatePlayerListExecute();
this.updatePlayerListTimer = setTimeout(() => {
if (this.updatePlayerListPending) {
this.updatePlayerListExecute();
}
this.updatePlayerListTimer = null;
}, 150);
}
};
$app.methods.updatePlayerListExecute = function () {
try {
this.updatePlayerListDebounce();
} catch (err) {
console.error(err);
}
this.updatePlayerListTimer = null;
this.updatePlayerListPending = false;
};
$app.methods.updatePlayerListDebounce = function () {
var users = [];
var pushUser = function (ref) {
var photonId = '';
var isFriend = false;
$app.photonLobbyCurrent.forEach((ref1, id) => {
if (typeof ref1 !== 'undefined') {
if (
(typeof ref.id !== 'undefined' &&
typeof ref1.id !== 'undefined' &&
ref1.id === ref.id) ||
(typeof ref.displayName !== 'undefined' &&
typeof ref1.displayName !== 'undefined' &&
ref1.displayName === ref.displayName)
) {
photonId = id;
}
}
});
var isMaster = false;
if (
$app.photonLobbyMaster !== 0 &&
photonId === $app.photonLobbyMaster
) {
isMaster = true;
}
var isModerator = false;
var lobbyJointime = $app.photonLobbyJointime.get(photonId);
var inVRMode = null;
var groupOnNameplate = '';
if (typeof lobbyJointime !== 'undefined') {
inVRMode = lobbyJointime.inVRMode;
groupOnNameplate = lobbyJointime.groupOnNameplate;
isModerator = lobbyJointime.canModerateInstance;
}
// if (groupOnNameplate) {
// API.getCachedGroup({
// groupId: groupOnNameplate
// }).then((args) => {
// groupOnNameplate = args.ref.name;
// });
// }
var timeoutTime = 0;
if (typeof ref.id !== 'undefined') {
isFriend = ref.isFriend;
if (
$app.timeoutHudOverlayFilter === 'VIP' ||
$app.timeoutHudOverlayFilter === 'Friends'
) {
$app.photonLobbyTimeout.forEach((ref1) => {
if (ref1.userId === ref.id) {
timeoutTime = ref1.time;
}
});
} else {
$app.photonLobbyTimeout.forEach((ref1) => {
if (ref1.displayName === ref.displayName) {
timeoutTime = ref1.time;
}
});
}
}
users.push({
ref,
displayName: ref.displayName,
timer: ref.$location_at,
$trustSortNum: ref.$trustSortNum ?? 0,
photonId,
isMaster,
isModerator,
inVRMode,
groupOnNameplate,
isFriend,
timeoutTime
});
// get block, mute
};
var playersInInstance = this.lastLocation.playerList;
if (playersInInstance.size > 0) {
var ref = API.cachedUsers.get(API.currentUser.id);
if (typeof ref !== 'undefined' && playersInInstance.has(ref.id)) {
pushUser(ref);
}
for (var player of playersInInstance.values()) {
// if friend isn't in instance add them
if (player.displayName === API.currentUser.displayName) {
continue;
}
var addUser = !users.some(function (user) {
return player.displayName === user.displayName;
});
if (addUser) {
var ref = API.cachedUsers.get(player.userId);
if (typeof ref !== 'undefined') {
pushUser(ref);
} else {
var { joinTime } = this.lastLocation.playerList.get(
player.userId
);
if (!joinTime) {
joinTime = Date.now();
}
var ref = {
// if userId is missing just push displayName
displayName: player.displayName,
$location_at: joinTime,
$online_for: joinTime
};
pushUser(ref);
}
}
}
}
this.currentInstanceUserList.data = users;
this.updateTimers();
};
$app.data.updateInstanceInfo = 0;
$app.data.currentInstanceWorld = {
ref: {},
instance: {},
isPC: false,
isQuest: false,
isIos: false,
avatarScalingDisabled: false,
focusViewDisabled: false,
inCache: false,
cacheSize: '',
bundleSizes: [],
lastUpdated: ''
};
$app.data.currentInstanceWorldDescriptionExpanded = false;
$app.data.currentInstanceLocation = {};
$app.methods.updateCurrentInstanceWorld = function () {
var instanceId = this.lastLocation.location;
if (this.lastLocation.location === 'traveling') {
instanceId = this.lastLocationDestination;
}
if (!instanceId) {
this.currentInstanceWorld = {
ref: {},
instance: {},
isPC: false,
isQuest: false,
isIos: false,
avatarScalingDisabled: false,
focusViewDisabled: false,
inCache: false,
cacheSize: '',
bundleSizes: [],
lastUpdated: ''
};
this.currentInstanceLocation = {};
} else if (instanceId !== this.currentInstanceLocation.tag) {
this.currentInstanceWorld = {
ref: {},
instance: {},
isPC: false,
isQuest: false,
isIos: false,
avatarScalingDisabled: false,
focusViewDisabled: false,
inCache: false,
cacheSize: '',
bundleSizes: [],
lastUpdated: ''
};
var L = parseLocation(instanceId);
this.currentInstanceLocation = L;
worldRequest
.getWorld({
worldId: L.worldId
})
.then((args) => {
this.currentInstanceWorld.ref = args.ref;
var { isPC, isQuest, isIos } = getAvailablePlatforms(
args.ref.unityPackages
);
this.currentInstanceWorld.isPC = isPC;
this.currentInstanceWorld.isQuest = isQuest;
this.currentInstanceWorld.isIos = isIos;
this.currentInstanceWorld.avatarScalingDisabled =
args.ref?.tags.includes(
'feature_avatar_scaling_disabled'
);
this.currentInstanceWorld.focusViewDisabled =
args.ref?.tags.includes('feature_focus_view_disabled');
checkVRChatCache(args.ref).then((cacheInfo) => {
if (cacheInfo.Item1 > 0) {
this.currentInstanceWorld.inCache = true;
this.currentInstanceWorld.cacheSize = `${(
cacheInfo.Item1 / 1048576
).toFixed(2)} MB`;
}
});
this.getBundleDateSize(args.ref).then((bundleSizes) => {
this.currentInstanceWorld.bundleSizes = bundleSizes;
});
return args;
});
} else {
worldRequest
.getCachedWorld({
worldId: this.currentInstanceLocation.worldId
})
.then((args) => {
this.currentInstanceWorld.ref = args.ref;
var { isPC, isQuest, isIos } = getAvailablePlatforms(
args.ref.unityPackages
);
this.currentInstanceWorld.isPC = isPC;
this.currentInstanceWorld.isQuest = isQuest;
this.currentInstanceWorld.isIos = isIos;
checkVRChatCache(args.ref).then((cacheInfo) => {
if (cacheInfo.Item1 > 0) {
this.currentInstanceWorld.inCache = true;
this.currentInstanceWorld.cacheSize = `${(
cacheInfo.Item1 / 1048576
).toFixed(2)} MB`;
}
});
});
}
if (isRealInstance(instanceId)) {
var ref = API.cachedInstances.get(instanceId);
if (typeof ref !== 'undefined') {
this.currentInstanceWorld.instance = ref;
} else {
var L = parseLocation(instanceId);
if (L.isRealInstance) {
instanceRequest
.getInstance({
worldId: L.worldId,
instanceId: L.instanceId
})
.then((args) => {
this.currentInstanceWorld.instance = args.ref;
});
}
}
}
};
$app.methods.updateTimers = function () {
for (var $timer of $timers) {
$timer.update();
}
};
$app.methods.lookupAvatars = async function (type, search) {
var avatars = new Map();
if (type === 'search') {
try {
var response = await webApiService.execute({
url: `${
this.avatarRemoteDatabaseProvider
}?${type}=${encodeURIComponent(search)}&n=5000`,
method: 'GET',
headers: {
Referer: 'https://vrcx.app',
'VRCX-ID': this.vrcxId
}
});
var json = JSON.parse(response.data);
if (this.debugWebRequests) {
console.log(json, response);
}
if (response.status === 200 && typeof json === 'object') {
json.forEach((avatar) => {
if (!avatars.has(avatar.Id)) {
var 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) {
var msg = `Avatar search failed for ${search} with ${this.avatarRemoteDatabaseProvider}\n${err}`;
console.error(msg);
this.$message({
message: msg,
type: 'error'
});
}
} else if (type === 'authorId') {
var length = this.avatarRemoteDatabaseProviderList.length;
for (var i = 0; i < length; ++i) {
var url = this.avatarRemoteDatabaseProviderList[i];
var avatarArray = await this.lookupAvatarsByAuthor(url, search);
avatarArray.forEach((avatar) => {
if (!avatars.has(avatar.id)) {
avatars.set(avatar.id, avatar);
}
});
}
}
return avatars;
};
$app.methods.lookupAvatarByImageFileId = async function (authorId, fileId) {
var length = this.avatarRemoteDatabaseProviderList.length;
for (var i = 0; i < length; ++i) {
var url = this.avatarRemoteDatabaseProviderList[i];
var avatarArray = await this.lookupAvatarsByAuthor(url, authorId);
for (var avatar of avatarArray) {
if (extractFileId(avatar.imageUrl) === fileId) {
return avatar.id;
}
}
}
return null;
};
$app.methods.lookupAvatarsByAuthor = async function (url, authorId) {
var avatars = [];
if (!url) {
return avatars;
}
try {
var response = await webApiService.execute({
url: `${url}?authorId=${encodeURIComponent(authorId)}`,
method: 'GET',
headers: {
Referer: 'https://vrcx.app',
'VRCX-ID': this.vrcxId
}
});
var json = JSON.parse(response.data);
if (this.debugWebRequests) {
console.log(json, response);
}
if (response.status === 200 && typeof json === 'object') {
json.forEach((avatar) => {
var 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) {
var msg = `Avatar lookup failed for ${authorId} with ${url}\n${err}`;
console.error(msg);
this.$message({
message: msg,
type: 'error'
});
}
return avatars;
};
$app.methods.sortUserDialogAvatars = function (array) {
var D = this.userDialog;
if (D.avatarSorting === 'update') {
array.sort($utils.compareByUpdatedAt);
} else {
array.sort($utils.compareByName);
}
D.avatars = array;
};
$app.methods.refreshUserDialogAvatars = function (fileId) {
var D = this.userDialog;
if (D.isAvatarsLoading) {
return;
}
D.isAvatarsLoading = true;
if (fileId) {
D.loading = true;
}
D.avatarSorting = 'update';
D.avatarReleaseStatus = 'all';
var params = {
n: 50,
offset: 0,
sort: 'updated',
order: 'descending',
releaseStatus: 'all',
user: 'me'
};
for (let ref of API.cachedAvatars.values()) {
if (ref.authorId === D.id) {
API.cachedAvatars.delete(ref.id);
}
}
var map = new Map();
API.bulk({
fn: avatarRequest.getAvatars,
N: -1,
params,
handle: (args) => {
for (var json of args.json) {
var $ref = API.cachedAvatars.get(json.id);
if (typeof $ref !== 'undefined') {
map.set($ref.id, $ref);
}
}
},
done: () => {
var array = Array.from(map.values());
this.sortUserDialogAvatars(array);
D.isAvatarsLoading = false;
if (fileId) {
D.loading = false;
for (let ref of array) {
if (extractFileId(ref.imageUrl) === fileId) {
this.showAvatarDialog(ref.id);
return;
}
}
this.$message({
message: 'Own avatar not found',
type: 'error'
});
}
}
});
};
$app.methods.refreshUserDialogTreeData = function () {
var D = this.userDialog;
if (D.id === API.currentUser.id) {
var treeData = {
...API.currentUser,
...D.ref
};
D.treeData = $utils.buildTreeData(treeData);
return;
}
D.treeData = $utils.buildTreeData(D.ref);
};
// #endregion
// #region | App: World Dialog
$app.data.worldDialog = {
visible: false,
loading: false,
id: '',
memo: '',
$location: {},
ref: {},
isFavorite: false,
avatarScalingDisabled: false,
focusViewDisabled: false,
rooms: [],
treeData: [],
bundleSizes: [],
lastUpdated: '',
inCache: false,
cacheSize: 0,
cacheLocked: false,
cachePath: '',
lastVisit: '',
visitCount: 0,
timeSpent: 0,
isPC: false,
isQuest: false,
isIos: false,
hasPersistData: false
};
API.$on('LOGOUT', function () {
$app.worldDialog.visible = false;
});
API.$on('WORLD', function (args) {
var { ref } = args;
var D = $app.worldDialog;
if (D.visible === false || D.id !== ref.id) {
return;
}
D.ref = ref;
D.avatarScalingDisabled = ref.tags?.includes(
'feature_avatar_scaling_disabled'
);
D.focusViewDisabled = ref.tags?.includes('feature_focus_view_disabled');
$app.applyWorldDialogInstances();
for (var room of D.rooms) {
if (isRealInstance(room.tag)) {
instanceRequest.getInstance({
worldId: D.id,
instanceId: room.id
});
}
}
if (D.bundleSizes.length === 0) {
$app.getBundleDateSize(ref).then((bundleSizes) => {
D.bundleSizes = bundleSizes;
});
}
});
$app.methods.getBundleDateSize = async function (ref) {
var bundleSizes = [];
for (let i = ref.unityPackages.length - 1; i > -1; i--) {
var unityPackage = ref.unityPackages[i];
if (
unityPackage.variant &&
unityPackage.variant !== 'standard' &&
unityPackage.variant !== 'security'
) {
continue;
}
if (!compareUnityVersion(unityPackage.unitySortNumber)) {
continue;
}
var platform = unityPackage.platform;
if (bundleSizes[platform]) {
continue;
}
var assetUrl = unityPackage.assetUrl;
var fileId = extractFileId(assetUrl);
var fileVersion = parseInt(extractFileVersion(assetUrl), 10);
if (!fileId) {
continue;
}
var args = await miscRequest.getBundles(fileId);
if (!args?.json?.versions) {
continue;
}
var { versions } = args.json;
for (let j = versions.length - 1; j > -1; j--) {
var version = versions[j];
if (version.version === fileVersion) {
var createdAt = version.created_at;
var fileSize = `${(
version.file.sizeInBytes / 1048576
).toFixed(2)} MB`;
bundleSizes[platform] = {
createdAt,
fileSize
};
// update avatar dialog
if (this.avatarDialog.id === ref.id) {
this.avatarDialog.bundleSizes[platform] =
bundleSizes[platform];
if (
this.avatarDialog.lastUpdated < version.created_at
) {
this.avatarDialog.lastUpdated = version.created_at;
}
}
// update world dialog
if (this.worldDialog.id === ref.id) {
this.worldDialog.bundleSizes[platform] =
bundleSizes[platform];
if (this.worldDialog.lastUpdated < version.created_at) {
this.worldDialog.lastUpdated = version.created_at;
}
}
// update player list
if (this.currentInstanceLocation.worldId === ref.id) {
this.currentInstanceWorld.bundleSizes[platform] =
bundleSizes[platform];
if (
this.currentInstanceWorld.lastUpdated <
version.created_at
) {
this.currentInstanceWorld.lastUpdated =
version.created_at;
}
}
break;
}
}
}
return bundleSizes;
};
API.$on('FAVORITE', function (args) {
var { ref } = args;
var D = $app.worldDialog;
if (D.visible === false || ref.$isDeleted || ref.favoriteId !== D.id) {
return;
}
D.isFavorite = true;
});
API.$on('FAVORITE:@DELETE', function (args) {
var D = $app.worldDialog;
if (D.visible === false || D.id !== args.ref.favoriteId) {
return;
}
D.isFavorite = $app.localWorldFavoritesList.includes(D.id);
});
$app.methods.showWorldDialog = function (tag, shortName) {
const D = this.worldDialog;
const L = parseLocation(tag);
if (L.worldId === '') {
return;
}
L.shortName = shortName;
D.id = L.worldId;
D.$location = L;
D.treeData = [];
D.bundleSizes = [];
D.lastUpdated = '';
D.visible = true;
D.loading = true;
D.inCache = false;
D.cacheSize = 0;
D.cacheLocked = false;
D.rooms = [];
D.lastVisit = '';
D.visitCount = '';
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 = '';
var LL = parseLocation(this.lastLocation.location);
var currentWorldMatch = false;
if (LL.worldId === D.id) {
currentWorldMatch = true;
}
this.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;
}
});
worldRequest
.getCachedWorld({
worldId: L.worldId
})
.catch((err) => {
D.loading = false;
D.visible = false;
this.$message({
message: 'Failed to load world',
type: 'error'
});
throw err;
})
.then((args) => {
if (D.id === args.ref.id) {
D.loading = false;
D.ref = args.ref;
D.isFavorite = API.cachedFavoritesByObjectId.has(D.id);
if (!D.isFavorite) {
D.isFavorite = this.localWorldFavoritesList.includes(
D.id
);
}
var { 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;
this.updateVRChatWorldCache();
miscRequest.hasWorldPersistData({ worldId: D.id });
if (args.cache) {
worldRequest
.getWorld(args.params)
.catch((err) => {
throw err;
})
.then((args1) => {
if (D.id === args1.ref.id) {
D.ref = args1.ref;
this.updateVRChatWorldCache();
}
return args1;
});
}
}
return args;
});
};
$app.methods.applyWorldDialogInstances = function () {
var D = this.worldDialog;
if (!D.visible) {
return;
}
var instances = {};
if (D.ref.instances) {
for (var instance of D.ref.instances) {
// instance = [ instanceId, occupants ]
var instanceId = instance[0];
instances[instanceId] = {
id: instanceId,
tag: `${D.id}:${instanceId}`,
$location: {},
friendCount: 0,
users: [],
shortName: '',
ref: {}
};
}
}
var { instanceId, shortName } = D.$location;
if (instanceId && typeof instances[instanceId] === 'undefined') {
instances[instanceId] = {
id: instanceId,
tag: `${D.id}:${instanceId}`,
$location: {},
friendCount: 0,
users: [],
shortName,
ref: {}
};
}
var cachedCurrentUser = API.cachedUsers.get(API.currentUser.id);
var lastLocation$ = cachedCurrentUser.$location;
var playersInInstance = this.lastLocation.playerList;
if (lastLocation$.worldId === D.id && playersInInstance.size > 0) {
// pull instance json from cache
var friendsInInstance = this.lastLocation.friendList;
var instance = {
id: lastLocation$.instanceId,
tag: lastLocation$.tag,
$location: {},
friendCount: friendsInInstance.size,
users: [],
shortName: '',
ref: {}
};
instances[instance.id] = instance;
for (var friend of friendsInInstance.values()) {
// if friend isn't in instance add them
var addUser = !instance.users.some(function (user) {
return friend.userId === user.id;
});
if (addUser) {
var ref = API.cachedUsers.get(friend.userId);
if (typeof ref !== 'undefined') {
instance.users.push(ref);
}
}
}
}
for (var { ref } of this.friends.values()) {
if (
typeof ref === 'undefined' ||
typeof ref.$location === 'undefined' ||
ref.$location.worldId !== D.id ||
(ref.$location.instanceId === lastLocation$.instanceId &&
playersInInstance.size > 0 &&
ref.location !== 'traveling')
) {
continue;
}
if (ref.location === this.lastLocation.location) {
// don't add friends to currentUser gameLog instance (except when traveling)
continue;
}
var { instanceId } = ref.$location;
var instance = instances[instanceId];
if (typeof instance === 'undefined') {
instance = {
id: instanceId,
tag: `${D.id}:${instanceId}`,
$location: {},
friendCount: 0,
users: [],
shortName: '',
ref: {}
};
instances[instanceId] = instance;
}
instance.users.push(ref);
}
var ref = API.cachedUsers.get(API.currentUser.id);
if (typeof ref !== 'undefined' && ref.$location.worldId === D.id) {
var { instanceId } = ref.$location;
var instance = instances[instanceId];
if (typeof instance === 'undefined') {
instance = {
id: instanceId,
tag: `${D.id}:${instanceId}`,
$location: {},
friendCount: 0,
users: [],
shortName: '',
ref: {}
};
instances[instanceId] = instance;
}
instance.users.push(ref); // add self
}
var rooms = [];
for (var instance of Object.values(instances)) {
// due to references on callback of API.getUser()
// this should be block scope variable
const L = parseLocation(`${D.id}:${instance.id}`);
instance.location = L.tag;
if (!L.shortName) {
L.shortName = instance.shortName;
}
instance.$location = L;
if (L.userId) {
var ref = API.cachedUsers.get(L.userId);
if (typeof ref === 'undefined') {
userRequest
.getUser({
userId: L.userId
})
.then((args) => {
Vue.set(L, 'user', args.ref);
return args;
});
} else {
L.user = ref;
}
}
if (instance.friendCount === 0) {
instance.friendCount = instance.users.length;
}
if (this.instanceUsersSortAlphabetical) {
instance.users.sort(compareByDisplayName);
} else {
instance.users.sort(compareByLocationAt);
}
rooms.push(instance);
}
// get instance from cache
for (var room of rooms) {
var ref = API.cachedInstances.get(room.tag);
if (typeof ref !== 'undefined') {
room.ref = ref;
}
}
rooms.sort(function (a, b) {
// sort selected and current instance to top
if (
b.location === D.$location.tag ||
b.location === lastLocation$.tag
) {
// sort selected instance above current instance
if (a.location === D.$location.tag) {
return -1;
}
return 1;
}
if (
a.location === D.$location.tag ||
a.location === lastLocation$.tag
) {
// sort selected instance above current instance
if (b.location === D.$location.tag) {
return 1;
}
return -1;
}
// sort by number of users when no friends in instance
if (a.users.length === 0 && b.users.length === 0) {
if (a.ref?.userCount < b.ref?.userCount) {
return 1;
}
return -1;
}
// sort by number of friends in instance
if (a.users.length < b.users.length) {
return 1;
}
return -1;
});
D.rooms = rooms;
this.updateTimers();
};
$app.methods.applyGroupDialogInstances = function (inputInstances) {
var D = this.groupDialog;
if (!D.visible) {
return;
}
var instances = {};
for (var instance of D.instances) {
instances[instance.tag] = {
...instance,
friendCount: 0,
users: []
};
}
if (typeof inputInstances !== 'undefined') {
for (var instance of inputInstances) {
instances[instance.location] = {
id: instance.instanceId,
tag: instance.location,
$location: {},
friendCount: 0,
users: [],
shortName: instance.shortName,
ref: instance
};
}
}
var cachedCurrentUser = API.cachedUsers.get(API.currentUser.id);
var lastLocation$ = cachedCurrentUser.$location;
var currentLocation = lastLocation$.tag;
var playersInInstance = this.lastLocation.playerList;
if (lastLocation$.groupId === D.id && playersInInstance.size > 0) {
var friendsInInstance = this.lastLocation.friendList;
var instance = {
id: lastLocation$.instanceId,
tag: currentLocation,
$location: {},
friendCount: friendsInInstance.size,
users: [],
shortName: '',
ref: {}
};
instances[currentLocation] = instance;
for (var friend of friendsInInstance.values()) {
// if friend isn't in instance add them
var addUser = !instance.users.some(function (user) {
return friend.userId === user.id;
});
if (addUser) {
var ref = API.cachedUsers.get(friend.userId);
if (typeof ref !== 'undefined') {
instance.users.push(ref);
}
}
}
}
for (var { ref } of this.friends.values()) {
if (
typeof ref === 'undefined' ||
typeof ref.$location === 'undefined' ||
ref.$location.groupId !== D.id ||
(ref.$location.instanceId === lastLocation$.instanceId &&
playersInInstance.size > 0 &&
ref.location !== 'traveling')
) {
continue;
}
if (ref.location === this.lastLocation.location) {
// don't add friends to currentUser gameLog instance (except when traveling)
continue;
}
var { instanceId, tag } = ref.$location;
var instance = instances[tag];
if (typeof instance === 'undefined') {
instance = {
id: instanceId,
tag,
$location: {},
friendCount: 0,
users: [],
shortName: '',
ref: {}
};
instances[tag] = instance;
}
instance.users.push(ref);
}
var ref = API.cachedUsers.get(API.currentUser.id);
if (typeof ref !== 'undefined' && ref.$location.groupId === D.id) {
var { instanceId, tag } = ref.$location;
var instance = instances[tag];
if (typeof instance === 'undefined') {
instance = {
id: instanceId,
tag,
$location: {},
friendCount: 0,
users: [],
shortName: '',
ref: {}
};
instances[tag] = instance;
}
instance.users.push(ref); // add self
}
var rooms = [];
for (var instance of Object.values(instances)) {
// due to references on callback of API.getUser()
// this should be block scope variable
const L = parseLocation(instance.tag);
instance.location = instance.tag;
instance.$location = L;
if (instance.friendCount === 0) {
instance.friendCount = instance.users.length;
}
if (this.instanceUsersSortAlphabetical) {
instance.users.sort(compareByDisplayName);
} else {
instance.users.sort(compareByLocationAt);
}
rooms.push(instance);
}
// get instance
for (var room of rooms) {
var ref = API.cachedInstances.get(room.tag);
if (typeof ref !== 'undefined') {
room.ref = ref;
} else if (isRealInstance(room.tag)) {
instanceRequest.getInstance({
worldId: room.$location.worldId,
instanceId: room.$location.instanceId
});
}
}
rooms.sort(function (a, b) {
// sort current instance to top
if (b.location === currentLocation) {
return 1;
}
if (a.location === currentLocation) {
return -1;
}
// sort by number of users when no friends in instance
if (a.users.length === 0 && b.users.length === 0) {
if (a.ref?.userCount < b.ref?.userCount) {
return 1;
}
return -1;
}
// sort by number of friends in instance
if (a.users.length < b.users.length) {
return 1;
}
return -1;
});
D.instances = rooms;
this.updateTimers();
};
$app.methods.worldDialogCommand = function (command) {
var D = this.worldDialog;
if (D.visible === false) {
return;
}
switch (command) {
case 'New Instance and Self Invite':
this.newInstanceSelfInvite(D.id);
break;
case 'Rename':
this.promptRenameWorld(D);
break;
case 'Change Description':
this.promptChangeWorldDescription(D);
break;
case 'Change Capacity':
this.promptChangeWorldCapacity(D);
break;
case 'Change Recommended Capacity':
this.promptChangeWorldRecommendedCapacity(D);
break;
case 'Change YouTube Preview':
this.promptChangeWorldYouTubePreview(D);
break;
}
};
$app.methods.newInstanceSelfInvite = function (worldId) {
this.createNewInstance(worldId).then((args) => {
const location = args?.json?.location;
if (!location) {
this.$message({
message: 'Failed to create instance',
type: 'error'
});
return;
}
// self invite
var L = parseLocation(location);
if (!L.isRealInstance) {
return;
}
instanceRequest
.selfInvite({
instanceId: L.instanceId,
worldId: L.worldId
})
.then((args) => {
this.$message({
message: 'Self invite sent',
type: 'success'
});
return args;
});
});
};
// #endregion
// #region | App: Avatar Dialog
$app.data.avatarDialog = {
visible: false,
loading: false,
id: '',
memo: '',
ref: {},
isFavorite: false,
isBlocked: false,
isQuestFallback: false,
hasImposter: false,
imposterVersion: '',
isPC: false,
isQuest: false,
isIos: false,
bundleSizes: [],
platformInfo: {},
galleryImages: [],
galleryLoading: false,
lastUpdated: '',
inCache: false,
cacheSize: 0,
cacheLocked: false,
cachePath: ''
};
API.$on('FAVORITE', function (args) {
var { ref } = args;
var D = $app.avatarDialog;
if (D.visible === false || ref.$isDeleted || ref.favoriteId !== D.id) {
return;
}
D.isFavorite = true;
});
API.$on('FAVORITE:@DELETE', function (args) {
var D = $app.avatarDialog;
if (D.visible === false || D.id !== args.ref.favoriteId) {
return;
}
D.isFavorite = false;
});
$app.methods.showAvatarDialog = function (avatarId) {
var D = this.avatarDialog;
D.visible = true;
D.loading = true;
D.id = avatarId;
D.inCache = false;
D.cacheSize = 0;
D.cacheLocked = false;
D.cachePath = '';
D.isQuestFallback = false;
D.isPC = false;
D.isQuest = false;
D.isIos = false;
D.hasImposter = false;
D.imposterVersion = '';
D.lastUpdated = '';
D.bundleSizes = [];
D.platformInfo = {};
D.galleryImages = [];
D.galleryLoading = true;
D.isFavorite =
API.cachedFavoritesByObjectId.has(avatarId) ||
(API.currentUser.$isVRCPlus &&
this.localAvatarFavoritesList.includes(avatarId));
D.isBlocked = API.cachedAvatarModerations.has(avatarId);
var ref2 = API.cachedAvatars.get(avatarId);
if (typeof ref2 !== 'undefined') {
D.ref = ref2;
this.updateVRChatAvatarCache();
if (
ref2.releaseStatus !== 'public' &&
ref2.authorId !== API.currentUser.id
) {
D.loading = false;
return;
}
}
avatarRequest
.getAvatar({ avatarId })
.then((args) => {
var { ref } = args;
D.ref = ref;
this.getAvatarGallery(avatarId);
this.updateVRChatAvatarCache();
if (/quest/.test(ref.tags)) {
D.isQuestFallback = true;
}
var { isPC, isQuest, isIos } = getAvailablePlatforms(
args.ref.unityPackages
);
D.isPC = isPC;
D.isQuest = isQuest;
D.isIos = isIos;
D.platformInfo = getPlatformInfo(args.ref.unityPackages);
for (let i = ref.unityPackages.length - 1; i > -1; i--) {
var unityPackage = ref.unityPackages[i];
if (unityPackage.variant === 'impostor') {
D.hasImposter = true;
D.imposterVersion = unityPackage.impostorizerVersion;
break;
}
}
if (D.bundleSizes.length === 0) {
this.getBundleDateSize(ref).then((bundleSizes) => {
D.bundleSizes = bundleSizes;
});
}
})
.catch((err) => {
D.visible = false;
throw err;
})
.finally(() => {
this.$nextTick(() => (D.loading = false));
});
};
$app.methods.getAvatarGallery = async function (avatarId) {
var D = this.avatarDialog;
const args = await avatarRequest
.getAvatarGallery(avatarId)
.finally(() => {
D.galleryLoading = false;
});
if (args.params.galleryId !== D.id) {
return;
}
D.galleryImages = [];
// wtf is this? why is order sorting only needed if it's your own avatar?
const sortedGallery = args.json.sort((a, b) => {
if (!a.order && !b.order) {
return 0;
}
return a.order - b.order;
});
for (const file of sortedGallery) {
const url = file.versions[file.versions.length - 1].file.url;
D.galleryImages.push(url);
}
// for JSON tab treeData
D.ref.gallery = args.json;
return D.galleryImages;
};
$app.methods.selectAvatarWithConfirmation = function (id) {
this.$confirm(`Continue? Select Avatar`, 'Confirm', {
confirmButtonText: 'Confirm',
cancelButtonText: 'Cancel',
type: 'info',
callback: (action) => {
if (action !== 'confirm') {
return;
}
$app.selectAvatarWithoutConfirmation(id);
}
});
};
$app.methods.selectAvatarWithoutConfirmation = function (id) {
if (API.currentUser.currentAvatar === id) {
this.$message({
message: 'Avatar already selected',
type: 'info'
});
return;
}
avatarRequest
.selectAvatar({
avatarId: id
})
.then((args) => {
new Noty({
type: 'success',
text: 'Avatar changed via launch command'
}).show();
return args;
});
};
$app.methods.checkAvatarCache = function (fileId) {
var avatarId = '';
for (var ref of API.cachedAvatars.values()) {
if (extractFileId(ref.imageUrl) === fileId) {
avatarId = ref.id;
}
}
return avatarId;
};
$app.methods.checkAvatarCacheRemote = async function (fileId, ownerUserId) {
if (this.avatarRemoteDatabase) {
var avatarId = await this.lookupAvatarByImageFileId(
ownerUserId,
fileId
);
return avatarId;
}
return null;
};
$app.methods.showAvatarAuthorDialog = async function (
refUserId,
ownerUserId,
currentAvatarImageUrl
) {
var fileId = extractFileId(currentAvatarImageUrl);
if (!fileId) {
this.$message({
message: 'Sorry, the author is unknown',
type: 'error'
});
} else if (refUserId === API.currentUser.id) {
this.showAvatarDialog(API.currentUser.currentAvatar);
} else {
var avatarId = await this.checkAvatarCache(fileId);
if (!avatarId) {
var avatarInfo = await this.getAvatarName(
currentAvatarImageUrl
);
if (avatarInfo.ownerId === API.currentUser.id) {
this.refreshUserDialogAvatars(fileId);
}
}
if (!avatarId) {
avatarId = await this.checkAvatarCacheRemote(
fileId,
avatarInfo.ownerId
);
}
if (!avatarId) {
if (avatarInfo.ownerId === refUserId) {
this.$message({
message:
"It's personal (own) avatar or not found in avatar database",
type: 'warning'
});
} else {
this.$message({
message: 'Avatar not found in avatar database',
type: 'warning'
});
this.showUserDialog(avatarInfo.ownerId);
}
}
if (avatarId) {
this.showAvatarDialog(avatarId);
}
}
};
// #endregion
// #region | App: Favorite Dialog
$app.data.favoriteDialog = {
visible: false,
loading: false,
type: '',
objectId: '',
currentGroup: {}
};
$app.methods.showFavoriteDialog = function (type, objectId) {
const D = this.favoriteDialog;
D.type = type;
D.objectId = objectId;
D.visible = true;
this.updateFavoriteDialog(objectId);
};
$app.methods.updateFavoriteDialog = function (objectId) {
var D = this.favoriteDialog;
if (!D.visible || D.objectId !== objectId) {
return;
}
D.currentGroup = {};
var favorite = this.favoriteObjects.get(objectId);
if (favorite) {
for (var group of API.favoriteWorldGroups) {
if (favorite.groupKey === group.key) {
D.currentGroup = group;
return;
}
}
for (var group of API.favoriteAvatarGroups) {
if (favorite.groupKey === group.key) {
D.currentGroup = group;
return;
}
}
for (var group of API.favoriteFriendGroups) {
if (favorite.groupKey === group.key) {
D.currentGroup = group;
return;
}
}
}
};
API.$on('FAVORITE:ADD', function (args) {
$app.updateFavoriteDialog(args.params.favoriteId);
});
API.$on('FAVORITE:DELETE', function (args) {
$app.updateFavoriteDialog(args.params.objectId);
});
// #endregion
// #region | App: New Instance Dialog
$app.data.instanceContentSettings = [
'emoji',
'stickers',
'pedestals',
'prints',
'drones'
];
$app.methods.createNewInstance = async function (worldId = '', options) {
let D = options;
if (!D) {
D = {
loading: false,
accessType: await configRepository.getString(
'instanceDialogAccessType',
'public'
),
region: await configRepository.getString(
'instanceRegion',
'US West'
),
worldId: worldId,
groupId: await configRepository.getString(
'instanceDialogGroupId',
''
),
groupAccessType: await configRepository.getString(
'instanceDialogGroupAccessType',
'plus'
),
ageGate: await configRepository.getBool(
'instanceDialogAgeGate',
false
),
queueEnabled: await configRepository.getBool(
'instanceDialogQueueEnabled',
true
),
contentSettings: this.instanceContentSettings || [],
selectedContentSettings: JSON.parse(
await configRepository.getString(
'instanceDialogSelectedContentSettings',
JSON.stringify(this.instanceContentSettings || [])
)
),
roleIds: [],
groupRef: {}
};
}
var type = 'public';
var canRequestInvite = false;
switch (D.accessType) {
case 'friends':
type = 'friends';
break;
case 'friends+':
type = 'hidden';
break;
case 'invite':
type = 'private';
break;
case 'invite+':
type = 'private';
canRequestInvite = true;
break;
case 'group':
type = 'group';
break;
}
var region = 'us';
if (D.region === 'US East') {
region = 'use';
} else if (D.region === 'Europe') {
region = 'eu';
} else if (D.region === 'Japan') {
region = 'jp';
}
var contentSettings = {};
for (var setting of D.contentSettings) {
contentSettings[setting] =
D.selectedContentSettings.includes(setting);
}
var params = {
type,
canRequestInvite,
worldId: D.worldId,
ownerId: API.currentUser.id,
region,
contentSettings
};
if (type === 'group') {
params.groupAccessType = D.groupAccessType;
params.ownerId = D.groupId;
params.queueEnabled = D.queueEnabled;
if (D.groupAccessType === 'members') {
params.roleIds = D.roleIds;
}
}
if (
D.ageGate &&
type === 'group' &&
hasGroupPermission(D.groupRef, 'group-instance-age-gated-create')
) {
params.ageGate = true;
}
try {
var args = await instanceRequest.createInstance(params);
return args;
} catch (err) {
console.error(err);
return null;
}
};
$app.methods.makeHome = function (tag) {
this.$confirm('Continue? Make Home', 'Confirm', {
confirmButtonText: 'Confirm',
cancelButtonText: 'Cancel',
type: 'info',
callback: (action) => {
if (action !== 'confirm') {
return;
}
userRequest
.saveCurrentUser({
homeLocation: tag
})
.then((args) => {
this.$message({
message: 'Home world updated',
type: 'success'
});
return args;
});
}
});
};
// #endregion
// #region | App: Launch Options Dialog
$app.data.isLaunchOptionsDialogVisible = false;
$app.methods.showLaunchOptions = function () {
this.isLaunchOptionsDialogVisible = true;
};
// #endregion
// #region | App: Notification position
$app.data.isNotificationPositionDialogVisible = false;
$app.methods.showNotificationPositionDialog = function () {
this.isNotificationPositionDialogVisible = true;
};
// #endregion
// #region | App: Noty feed filters
// #region | App: Wrist feed filters
$app.data.feedFiltersDialogMode = '';
$app.methods.showNotyFeedFiltersDialog = function () {
this.feedFiltersDialogMode = 'noty';
};
$app.methods.showWristFeedFiltersDialog = function () {
this.feedFiltersDialogMode = 'wrist';
};
// #endregion
// #region | App: Launch Dialog
$app.data.launchDialogData = {
visible: false,
loading: false,
tag: '',
shortName: ''
};
$app.methods.showLaunchDialog = async function (tag, shortName) {
this.launchDialogData = {
visible: true,
// flag, use for trigger adjustDialogZ
loading: true,
tag,
shortName
};
this.$nextTick(() => (this.launchDialogData.loading = false));
};
$app.methods.launchGame = async function (
location,
shortName,
desktopMode
) {
var L = parseLocation(location);
var args = [];
if (
shortName &&
L.instanceType !== 'public' &&
L.groupAccessType !== 'public'
) {
args.push(
`vrchat://launch?ref=vrcx.app&id=${location}&shortName=${shortName}`
);
} else {
// fetch shortName
var newShortName = '';
var response = await instanceRequest.getInstanceShortName({
worldId: L.worldId,
instanceId: L.instanceId
});
if (response.json) {
if (response.json.shortName) {
newShortName = response.json.shortName;
} else {
newShortName = response.json.secureName;
}
}
if (newShortName) {
args.push(
`vrchat://launch?ref=vrcx.app&id=${location}&shortName=${newShortName}`
);
} else {
args.push(`vrchat://launch?ref=vrcx.app&id=${location}`);
}
}
const launchArguments =
await configRepository.getString('launchArguments');
const vrcLaunchPathOverride = await configRepository.getString(
'vrcLaunchPathOverride'
);
if (launchArguments) {
args.push(launchArguments);
}
if (desktopMode) {
args.push('--no-vr');
}
if (vrcLaunchPathOverride && !LINUX) {
AppApi.StartGameFromPath(
vrcLaunchPathOverride,
args.join(' ')
).then((result) => {
if (!result) {
this.$message({
message:
'Failed to launch VRChat, invalid custom path set',
type: 'error'
});
} else {
this.$message({
message: 'VRChat launched',
type: 'success'
});
}
});
} else {
AppApi.StartGame(args.join(' ')).then((result) => {
if (!result) {
this.$message({
message:
'Failed to find VRChat, set a custom path in launch options',
type: 'error'
});
} else {
this.$message({
message: 'VRChat launched',
type: 'success'
});
}
});
}
console.log('Launch Game', args.join(' '), desktopMode);
};
// #endregion
// #region | App: Copy To Clipboard
$app.methods.copyToClipboard = function (text) {
var textArea = document.createElement('textarea');
textArea.id = 'copy_to_clipboard';
textArea.value = text;
textArea.style.top = '0';
textArea.style.left = '0';
textArea.style.position = 'fixed';
document.body.appendChild(textArea);
textArea.focus();
textArea.select();
document.execCommand('copy');
document.getElementById('copy_to_clipboard').remove();
};
$app.methods.copyLink = function (text) {
this.$message({
message: 'Link copied to clipboard',
type: 'success'
});
this.copyToClipboard(text);
};
// #endregion
// #region | App: VRCPlus Icons
API.$on('LOGIN', function () {
$app.VRCPlusIconsTable = [];
});
$app.methods.refreshVRCPlusIconsTable = function () {
this.galleryDialogIconsLoading = true;
var params = {
n: 100,
tag: 'icon'
};
vrcPlusIconRequest.getFileList(params);
};
API.$on('FILES:LIST', function (args) {
if (args.params.tag === 'icon') {
$app.VRCPlusIconsTable = args.json.reverse();
$app.galleryDialogIconsLoading = false;
}
});
API.$on('VRCPLUSICON:ADD', function (args) {
if (Object.keys($app.VRCPlusIconsTable).length !== 0) {
$app.VRCPlusIconsTable.unshift(args.json);
}
});
$app.data.uploadImage = '';
$app.methods.inviteImageUpload = function (e) {
var files = e.target.files || e.dataTransfer.files;
if (!files.length) {
return;
}
if (files[0].size >= 100000000) {
// 100MB
$app.$message({
message: $t('message.file.too_large'),
type: 'error'
});
this.clearInviteImageUpload();
return;
}
if (!files[0].type.match(/image.*/)) {
$app.$message({
message: $t('message.file.not_image'),
type: 'error'
});
this.clearInviteImageUpload();
return;
}
var r = new FileReader();
r.onload = function () {
$app.uploadImage = btoa(r.result);
};
r.readAsBinaryString(files[0]);
};
$app.methods.clearInviteImageUpload = function () {
var buttonList = document.querySelectorAll('.inviteImageUploadButton');
buttonList.forEach((button) => (button.value = ''));
this.uploadImage = '';
};
$app.methods.userOnlineFor = function (ctx) {
if (ctx.ref.state === 'online' && ctx.ref.$online_for) {
return $utils.timeToText(Date.now() - ctx.ref.$online_for);
} else if (ctx.ref.state === 'active' && ctx.ref.$active_for) {
return $utils.timeToText(Date.now() - ctx.ref.$active_for);
} else if (ctx.ref.$offline_for) {
return $utils.timeToText(Date.now() - ctx.ref.$offline_for);
}
return '-';
};
// #endregion
// #region | App: Invite Messages
API.$on('LOGIN', function () {
$app.inviteMessageTable.data = [];
$app.inviteResponseMessageTable.data = [];
$app.inviteRequestMessageTable.data = [];
$app.inviteRequestResponseMessageTable.data = [];
$app.inviteMessageTable.visible = false;
$app.inviteResponseMessageTable.visible = false;
$app.inviteRequestMessageTable.visible = false;
$app.inviteRequestResponseMessageTable.visible = false;
});
// temp, invites.pug
API.refreshInviteMessageTableData =
inviteMessagesRequest.refreshInviteMessageTableData;
API.$on('INVITE:MESSAGE', function (args) {
$app.inviteMessageTable.data = args.json;
});
API.$on('INVITE:RESPONSE', function (args) {
$app.inviteResponseMessageTable.data = args.json;
});
API.$on('INVITE:REQUEST', function (args) {
$app.inviteRequestMessageTable.data = args.json;
});
API.$on('INVITE:REQUESTRESPONSE', function (args) {
$app.inviteRequestResponseMessageTable.data = args.json;
});
// #endregion
// #region | App: Edit Invite Message Dialog
$app.data.editInviteMessageDialog = {
visible: false,
inviteMessage: {},
messageType: '',
newMessage: ''
};
$app.methods.showEditInviteMessageDialog = function (
messageType,
inviteMessage
) {
var D = this.editInviteMessageDialog;
D.newMessage = inviteMessage.message;
D.visible = true;
D.inviteMessage = inviteMessage;
D.messageType = messageType;
};
// #endregion
// #region | App: Friends List
$app.data.friendsListSearch = '';
// $app.data.friendsListSelectAllCheckbox = false;
// $app.methods.showBulkUnfriendAllConfirm = function () {
// this.$confirm(
// `Are you sure you want to delete all your friends?
// This can negatively affect your trust rank,
// This action cannot be undone.`,
// 'Delete all friends?',
// {
// confirmButtonText: 'Confirm',
// cancelButtonText: 'Cancel',
// type: 'info',
// callback: (action) => {
// if (action === 'confirm') {
// this.bulkUnfriendAll();
// }
// }
// }
// );
// };
// $app.methods.bulkUnfriendAll = function () {
// for (var ctx of this.friendsListTable.data) {
// API.deleteFriend({
// userId: ctx.id
// });
// }
// };
$app.methods.getAllUserStats = async function () {
var userIds = [];
var displayNames = [];
for (var ctx of this.friends.values()) {
userIds.push(ctx.id);
if (ctx.ref?.displayName) {
displayNames.push(ctx.ref.displayName);
}
}
var data = await database.getAllUserStats(userIds, displayNames);
var friendListMap = new Map();
for (var item of data) {
if (!item.userId) {
// find userId from previous data with matching displayName
for (var ref of data) {
if (ref.displayName === item.displayName && ref.userId) {
item.userId = ref.userId;
}
}
// if still no userId, find userId from friends list
if (!item.userId) {
for (var ref of this.friends.values()) {
if (
ref?.ref?.id &&
ref.ref.displayName === item.displayName
) {
item.userId = ref.id;
}
}
}
// if still no userId, skip
if (!item.userId) {
continue;
}
}
var friend = friendListMap.get(item.userId);
if (!friend) {
friendListMap.set(item.userId, item);
continue;
}
if (Date.parse(item.lastSeen) > Date.parse(friend.lastSeen)) {
friend.lastSeen = item.lastSeen;
}
friend.timeSpent += item.timeSpent;
friend.joinCount += item.joinCount;
friend.displayName = item.displayName;
friendListMap.set(item.userId, friend);
}
for (var item of friendListMap.values()) {
var ref = this.friends.get(item.userId);
if (ref?.ref) {
ref.ref.$joinCount = item.joinCount;
ref.ref.$lastSeen = item.lastSeen;
ref.ref.$timeSpent = item.timeSpent;
}
}
};
$app.methods.getUserStats = async function (ctx) {
var ref = await database.getUserStats(ctx);
/* eslint-disable require-atomic-updates */
ctx.$joinCount = ref.joinCount;
ctx.$lastSeen = ref.lastSeen;
ctx.$timeSpent = ref.timeSpent;
/* eslint-enable require-atomic-updates */
};
// Set avatar/world image
$app.methods.checkPreviousImageAvailable = async function (images) {
this.previousImagesTable = [];
for (var image of images) {
if (image.file && image.file.url) {
var response = await fetch(image.file.url, {
method: 'HEAD',
redirect: 'follow'
}).catch((error) => {
console.log(error);
});
if (response.status === 200) {
this.previousImagesTable.push(image);
}
}
}
};
// todo: userdialog
$app.data.previousImagesDialogVisible = false;
$app.data.previousImagesTable = [];
API.$on('LOGIN', function () {
$app.previousImagesTable = [];
});
// Avatar names
API.cachedAvatarNames = new Map();
$app.methods.getAvatarName = async function (imageUrl) {
var fileId = extractFileId(imageUrl);
if (!fileId) {
return {
ownerId: '',
avatarName: '-'
};
}
if (API.cachedAvatarNames.has(fileId)) {
return API.cachedAvatarNames.get(fileId);
}
var args = await imageRequest.getAvatarImages({ fileId });
return storeAvatarImage(args);
};
// VRChat Config JSON
$app.data.isVRChatConfigDialogVisible = false;
$app.methods.showVRChatConfig = async function () {
this.isVRChatConfigDialogVisible = true;
if (!this.VRChatUsedCacheSize) {
this.getVRChatCacheSize();
}
};
// Auto Launch Shortcuts
$app.methods.openShortcutFolder = function () {
AppApi.OpenShortcutFolder();
};
$app.methods.updateAppLauncherSettings = async function (configKey = '') {
if (configKey === 'VRCX_enableAppLauncher') {
this.enableAppLauncher = !this.enableAppLauncher;
await configRepository.setBool(
'VRCX_enableAppLauncher',
this.enableAppLauncher
);
} else {
this.enableAppLauncherAutoClose = !this.enableAppLauncherAutoClose;
await configRepository.setBool(
'VRCX_enableAppLauncherAutoClose',
this.enableAppLauncherAutoClose
);
}
await AppApi.SetAppLauncherSettings(
this.enableAppLauncher,
this.enableAppLauncherAutoClose
);
};
// Screenshot Helper
$app.methods.saveScreenshotHelper = async function (configKey = '') {
if (configKey === 'VRCX_screenshotHelper') {
this.screenshotHelper = !this.screenshotHelper;
} else if (configKey === 'VRCX_screenshotHelperModifyFilename') {
this.screenshotHelperModifyFilename =
!this.screenshotHelperModifyFilename;
} else if (configKey === 'VRCX_screenshotHelperCopyToClipboard') {
this.screenshotHelperCopyToClipboard =
!this.screenshotHelperCopyToClipboard;
}
await configRepository.setBool(
'VRCX_screenshotHelper',
this.screenshotHelper
);
await configRepository.setBool(
'VRCX_screenshotHelperModifyFilename',
this.screenshotHelperModifyFilename
);
await configRepository.setBool(
'VRCX_screenshotHelperCopyToClipboard',
this.screenshotHelperCopyToClipboard
);
};
$app.methods.processScreenshot = async function (path) {
var newPath = path;
if (this.screenshotHelper) {
var location = parseLocation(this.lastLocation.location);
var metadata = {
application: 'VRCX',
version: 1,
author: {
id: API.currentUser.id,
displayName: API.currentUser.displayName
},
world: {
name: this.lastLocation.name,
id: location.worldId,
instanceId: this.lastLocation.location
},
players: []
};
for (var user of this.lastLocation.playerList.values()) {
metadata.players.push({
id: user.userId,
displayName: user.displayName
});
}
newPath = await AppApi.AddScreenshotMetadata(
path,
JSON.stringify(metadata),
location.worldId,
this.screenshotHelperModifyFilename
);
console.log('Screenshot metadata added', newPath);
}
if (this.screenshotHelperCopyToClipboard) {
await AppApi.CopyImageToClipboard(newPath);
console.log('Screenshot copied to clipboard', newPath);
}
};
$app.data.screenshotMetadataDialog = {
visible: false,
loading: false,
search: '',
searchType: 'Player Name',
searchTypes: ['Player Name', 'Player ID', 'World Name', 'World ID'],
metadata: {},
isUploading: false
};
$app.methods.showScreenshotMetadataDialog = function () {
this.screenshotMetadataDialog.visible = true;
};
$app.data.currentlyDroppingFile = null;
/**
* This function is called by .NET(CefCustomDragHandler#CefCustomDragHandler) when a file is dragged over a drop zone in the app window.
* @param {string} filePath - The full path to the file being dragged into the window
*/
$app.methods.dragEnterCef = function (filePath) {
this.currentlyDroppingFile = filePath;
};
// YouTube API
$app.data.isYouTubeApiDialogVisible = false;
$app.methods.changeYouTubeApi = async function (configKey = '') {
if (configKey === 'VRCX_youtubeAPI') {
this.youTubeApi = !this.youTubeApi;
} else if (configKey === 'VRCX_progressPie') {
this.progressPie = !this.progressPie;
} else if (configKey === 'VRCX_progressPieFilter') {
this.progressPieFilter = !this.progressPieFilter;
}
await configRepository.setBool('VRCX_youtubeAPI', this.youTubeApi);
await configRepository.setBool('VRCX_progressPie', this.progressPie);
await configRepository.setBool(
'VRCX_progressPieFilter',
this.progressPieFilter
);
this.updateVRLastLocation();
this.updateOpenVR();
};
$app.methods.showYouTubeApiDialog = function () {
this.isYouTubeApiDialogVisible = true;
};
// Launch Command Settings handling
$app.methods.toggleLaunchCommandSetting = async function (configKey = '') {
switch (configKey) {
case 'VRCX_showConfirmationOnSwitchAvatar':
this.showConfirmationOnSwitchAvatar =
!this.showConfirmationOnSwitchAvatar;
await configRepository.setBool(
'VRCX_showConfirmationOnSwitchAvatar',
this.showConfirmationOnSwitchAvatar
);
break;
default:
throw new Error(
'toggleLaunchCommandSetting: Unknown configKey'
);
}
};
// Asset Bundle Cacher
$app.methods.updateVRChatWorldCache = function () {
var D = this.worldDialog;
if (D.visible) {
D.inCache = false;
D.cacheSize = 0;
D.cacheLocked = false;
D.cachePath = '';
checkVRChatCache(D.ref).then((cacheInfo) => {
if (cacheInfo.Item1 > 0) {
D.inCache = true;
D.cacheSize = `${(cacheInfo.Item1 / 1048576).toFixed(
2
)} MB`;
D.cachePath = cacheInfo.Item3;
}
D.cacheLocked = cacheInfo.Item2;
});
}
};
$app.methods.updateVRChatAvatarCache = function () {
var D = this.avatarDialog;
if (D.visible) {
D.inCache = false;
D.cacheSize = 0;
D.cacheLocked = false;
D.cachePath = '';
checkVRChatCache(D.ref).then((cacheInfo) => {
if (cacheInfo.Item1 > 0) {
D.inCache = true;
D.cacheSize = `${(cacheInfo.Item1 / 1048576).toFixed(
2
)} MB`;
D.cachePath = cacheInfo.Item3;
}
D.cacheLocked = cacheInfo.Item2;
});
}
};
$app.methods.getDisplayName = function (userId) {
if (userId) {
var ref = API.cachedUsers.get(userId);
if (ref.displayName) {
return ref.displayName;
}
}
return '';
};
$app.methods.deleteVRChatCache = async function (ref) {
await deleteVRChatCache(ref);
this.getVRChatCacheSize();
this.updateVRChatWorldCache();
this.updateVRChatAvatarCache();
};
$app.methods.autoVRChatCacheManagement = function () {
if (this.autoSweepVRChatCache) {
this.sweepVRChatCache();
}
};
$app.methods.sweepVRChatCache = async function () {
var output = await AssetBundleManager.SweepCache();
console.log('SweepCache', output);
if (this.isVRChatConfigDialogVisible) {
this.getVRChatCacheSize();
}
};
$app.methods.checkIfGameCrashed = function () {
if (!this.relaunchVRChatAfterCrash) {
return;
}
var { location } = this.lastLocation;
AppApi.VrcClosedGracefully().then((result) => {
if (result || !isRealInstance(location)) {
return;
}
// wait a bit for SteamVR to potentially close before deciding to relaunch
var restartDelay = 8000;
if (this.isGameNoVR) {
// wait for game to close before relaunching
restartDelay = 2000;
}
workerTimers.setTimeout(
() => this.restartCrashedGame(location),
restartDelay
);
});
};
$app.methods.restartCrashedGame = function (location) {
if (!this.isGameNoVR && !this.isSteamVRRunning) {
console.log("SteamVR isn't running, not relaunching VRChat");
return;
}
AppApi.FocusWindow();
var message = 'VRChat crashed, attempting to rejoin last instance';
this.$message({
message,
type: 'info'
});
var entry = {
created_at: new Date().toJSON(),
type: 'Event',
data: message
};
database.addGamelogEventToDatabase(entry);
this.queueGameLogNoty(entry);
this.addGameLog(entry);
this.launchGame(location, '', this.isGameNoVR);
};
$app.data.VRChatUsedCacheSize = '';
$app.data.VRChatTotalCacheSize = '';
$app.data.VRChatCacheSizeLoading = false;
$app.methods.getVRChatCacheSize = async function () {
this.VRChatCacheSizeLoading = true;
var totalCacheSize = 30;
this.VRChatTotalCacheSize = totalCacheSize;
var usedCacheSize = await AssetBundleManager.GetCacheSize();
this.VRChatUsedCacheSize = (usedCacheSize / 1073741824).toFixed(2);
this.VRChatCacheSizeLoading = false;
};
// Parse User URL
$app.methods.parseUserUrl = function (user) {
var url = new URL(user);
var urlPath = url.pathname;
if (urlPath.substring(5, 11) === '/user/') {
var userId = urlPath.substring(11);
return userId;
}
return void 0;
};
// userDialog Groups
$app.data.inGameGroupOrder = [];
$app.methods.getVRChatRegistryKey = async function (key) {
if (LINUX) {
return AppApi.GetVRChatRegistryKeyString(key);
}
return AppApi.GetVRChatRegistryKey(key);
};
$app.methods.updateInGameGroupOrder = async function () {
this.inGameGroupOrder = [];
try {
var json = await this.getVRChatRegistryKey(
`VRC_GROUP_ORDER_${API.currentUser.id}`
);
if (!json) {
return;
}
this.inGameGroupOrder = JSON.parse(json);
} catch (err) {
console.error(err);
}
};
$app.methods.sortGroupInstancesByInGame = function (a, b) {
var aIndex = this.inGameGroupOrder.indexOf(a?.group?.id);
var bIndex = this.inGameGroupOrder.indexOf(b?.group?.id);
if (aIndex === -1 && bIndex === -1) {
return 0;
}
if (aIndex === -1) {
return 1;
}
if (bIndex === -1) {
return -1;
}
return aIndex - bIndex;
};
// #endregion
// #region | Gallery
$app.data.galleryDialog = {};
$app.data.galleryDialogVisible = false;
$app.data.galleryDialogGalleryLoading = false;
$app.data.galleryDialogIconsLoading = false;
$app.data.galleryDialogEmojisLoading = false;
$app.data.galleryDialogStickersLoading = false;
$app.data.galleryDialogPrintsLoading = false;
API.$on('LOGIN', function () {
$app.galleryTable = [];
});
$app.methods.showGalleryDialog = function (pageNum) {
this.galleryDialogVisible = true;
this.refreshGalleryTable();
this.refreshVRCPlusIconsTable();
this.refreshEmojiTable();
this.refreshStickerTable();
this.refreshPrintTable();
workerTimers.setTimeout(() => this.setGalleryTab(pageNum), 100);
};
$app.methods.setGalleryTab = function (pageNum) {
if (
typeof pageNum !== 'undefined' &&
typeof this.$refs.galleryTabs !== 'undefined'
) {
this.$refs.galleryTabs.setCurrentName(`${pageNum}`);
}
};
$app.methods.refreshGalleryTable = function () {
this.galleryDialogGalleryLoading = true;
var params = {
n: 100,
tag: 'gallery'
};
vrcPlusIconRequest.getFileList(params);
};
API.$on('FILES:LIST', function (args) {
if (args.params.tag === 'gallery') {
$app.galleryTable = args.json.reverse();
$app.galleryDialogGalleryLoading = false;
}
});
API.$on('GALLERYIMAGE:ADD', function (args) {
if (Object.keys($app.galleryTable).length !== 0) {
$app.galleryTable.unshift(args.json);
}
});
// #endregion
// #region | Sticker
API.$on('LOGIN', function () {
$app.stickerTable = [];
});
$app.methods.refreshStickerTable = function () {
this.galleryDialogStickersLoading = true;
var params = {
n: 100,
tag: 'sticker'
};
vrcPlusIconRequest.getFileList(params);
};
API.$on('FILES:LIST', function (args) {
if (args.params.tag === 'sticker') {
$app.stickerTable = args.json.reverse();
$app.galleryDialogStickersLoading = false;
}
});
$app.methods.displayStickerUpload = function () {
document.getElementById('StickerUploadButton').click();
};
API.$on('STICKER:ADD', function (args) {
if (Object.keys($app.stickerTable).length !== 0) {
$app.stickerTable.unshift(args.json);
}
});
$app.data.stickersCache = [];
$app.methods.trySaveStickerToFile = async function (displayName, fileId) {
if ($app.stickersCache.includes(fileId)) return;
$app.stickersCache.push(fileId);
if ($app.stickersCache.size > 100) {
$app.stickersCache.shift();
}
var args = await API.call(`file/${fileId}`);
var imageUrl = args.versions[1].file.url;
var createdAt = args.versions[0].created_at;
var monthFolder = createdAt.slice(0, 7);
var fileNameDate = createdAt
.replace(/:/g, '-')
.replace(/T/g, '_')
.replace(/Z/g, '');
var fileName = `${displayName}_${fileNameDate}_${fileId}.png`;
var filePath = await AppApi.SaveStickerToFile(
imageUrl,
this.ugcFolderPath,
monthFolder,
fileName
);
if (filePath) {
console.log(`Sticker saved to file: ${monthFolder}\\${fileName}`);
}
};
// #endregion
// #region | Prints
$app.methods.cropPrintsChanged = function () {
if (!this.cropInstancePrints) return;
this.$confirm(
$t(
'view.settings.advanced.advanced.save_instance_prints_to_file.crop_convert_old'
),
{
confirmButtonText: $t(
'view.settings.advanced.advanced.save_instance_prints_to_file.crop_convert_old_confirm'
),
cancelButtonText: $t(
'view.settings.advanced.advanced.save_instance_prints_to_file.crop_convert_old_cancel'
),
type: 'info',
showInput: false,
callback: async (action) => {
if (action === 'confirm') {
var msgBox = this.$message({
message: 'Batch print cropping in progress...',
type: 'warning',
duration: 0
});
try {
await AppApi.CropAllPrints(this.ugcFolderPath);
this.$message({
message: 'Batch print cropping complete',
type: 'success'
});
} catch (err) {
console.error(err);
this.$message({
message: `Batch print cropping failed: ${err}`,
type: 'error'
});
} finally {
msgBox.close();
}
}
}
}
);
};
API.$on('LOGIN', function () {
$app.printTable = [];
});
$app.methods.refreshPrintTable = function () {
this.galleryDialogPrintsLoading = true;
var params = {
n: 100
};
vrcPlusImageRequest.getPrints(params);
};
API.$on('PRINT:LIST', function (args) {
$app.printTable = args.json;
$app.galleryDialogPrintsLoading = false;
});
$app.data.printUploadNote = '';
$app.data.printCropBorder = true;
$app.data.saveInstancePrints = await configRepository.getBool(
'VRCX_saveInstancePrints',
false
);
$app.data.cropInstancePrints = await configRepository.getBool(
'VRCX_cropInstancePrints',
false
);
$app.data.saveInstanceStickers = await configRepository.getBool(
'VRCX_saveInstanceStickers',
false
);
$app.data.printCache = [];
$app.data.printQueue = [];
$app.data.printQueueWorker = undefined;
$app.methods.queueSavePrintToFile = function (printId) {
if (this.printCache.includes(printId)) {
return;
}
this.printCache.push(printId);
if (this.printCache.length > 100) {
this.printCache.shift();
}
this.printQueue.push(printId);
if (!this.printQueueWorker) {
this.printQueueWorker = workerTimers.setInterval(() => {
let printId = this.printQueue.shift();
if (printId) {
this.trySavePrintToFile(printId);
}
}, 2_500);
}
};
$app.methods.trySavePrintToFile = async function (printId) {
var args = await vrcPlusImageRequest.getPrint({ printId });
var imageUrl = args.json?.files?.image;
if (!imageUrl) {
console.error('Print image URL is missing', args);
return;
}
var print = args.json;
var createdAt = getPrintLocalDate(print);
try {
var owner = await userRequest.getCachedUser({
userId: print.ownerId
});
console.log(
`Print spawned by ${owner?.json?.displayName} id:${print.id} note:${print.note} authorName:${print.authorName} at:${new Date().toISOString()}`
);
} catch (err) {
console.error(err);
}
var monthFolder = createdAt.toISOString().slice(0, 7);
var fileName = getPrintFileName(print);
var filePath = await AppApi.SavePrintToFile(
imageUrl,
this.ugcFolderPath,
monthFolder,
fileName
);
if (filePath) {
console.log(`Print saved to file: ${monthFolder}\\${fileName}`);
if (this.cropInstancePrints) {
if (!(await AppApi.CropPrintImage(filePath))) {
console.error('Failed to crop print image');
}
}
}
if (this.printQueue.length == 0) {
workerTimers.clearInterval(this.printQueueWorker);
this.printQueueWorker = undefined;
}
};
// #endregion
// #region | Emoji
API.$on('LOGIN', function () {
$app.emojiTable = [];
});
$app.methods.refreshEmojiTable = function () {
this.galleryDialogEmojisLoading = true;
var params = {
n: 100,
tag: 'emoji'
};
vrcPlusIconRequest.getFileList(params);
};
API.$on('FILES:LIST', function (args) {
if (args.params.tag === 'emoji') {
$app.emojiTable = args.json.reverse();
$app.galleryDialogEmojisLoading = false;
}
});
API.$on('EMOJI:ADD', function (args) {
if (Object.keys($app.emojiTable).length !== 0) {
$app.emojiTable.unshift(args.json);
}
});
// #endregion
// #region Misc
$app.methods.removeEmojis = function (text) {
if (!text) {
return '';
}
return text
.replace(
/([\u2700-\u27BF]|[\uE000-\uF8FF]|\uD83C[\uDC00-\uDFFF]|\uD83D[\uDC00-\uDFFF]|[\u2011-\u26FF]|\uD83E[\uDD10-\uDDFF])/g,
''
)
.replace(/\s+/g, ' ')
.trim();
};
$app.methods.checkCanInvite = function (location) {
var L = parseLocation(location);
var instance = API.cachedInstances.get(location);
if (instance?.closedAt) {
return false;
}
if (
L.accessType === 'public' ||
L.accessType === 'group' ||
L.userId === API.currentUser.id
) {
return true;
}
if (L.accessType === 'invite' || L.accessType === 'friends') {
return false;
}
if (this.lastLocation.location === location) {
return true;
}
return false;
};
$app.methods.checkCanInviteSelf = function (location) {
var L = parseLocation(location);
var instance = API.cachedInstances.get(location);
if (instance?.closedAt) {
return false;
}
if (L.userId === API.currentUser.id) {
return true;
}
if (L.accessType === 'friends' && !this.friends.has(L.userId)) {
return false;
}
return true;
};
$app.methods.setAsideWidth = async function () {
await configRepository.setInt('VRCX_sidePanelWidth', this.asideWidth);
};
/**
* @param {object} user - User Ref Object
* @param {boolean} isIcon - is use for icon (about 40x40)
* @param {string} resolution - requested icon resolution (default 128),
* @param {boolean} isUserDialogIcon - is use for user dialog icon
* @returns {string} - img url
*
* VRC's 64 scaling doesn't look good, 128 is better, but some images might be overly sharp.
* 128 is smaller than 256 or the original image size, making it a good choice.
*
* TODO: code is messy cause I haven't figured out the img field, maybe refactor it later
*/
$app.methods.userImage = function (
user,
isIcon,
resolution = '128',
isUserDialogIcon = false
) {
if (!user) {
return '';
}
if (
(isUserDialogIcon && user.userIcon) ||
(this.displayVRCPlusIconsAsAvatar && user.userIcon)
) {
if (isIcon) {
return convertFileUrlToImageUrl(user.userIcon);
}
return user.userIcon;
}
if (user.profilePicOverrideThumbnail) {
if (isIcon) {
return user.profilePicOverrideThumbnail.replace(
'/256',
`/${resolution}`
);
}
return user.profilePicOverrideThumbnail;
}
if (user.profilePicOverride) {
return user.profilePicOverride;
}
if (user.thumbnailUrl) {
return user.thumbnailUrl;
}
if (user.currentAvatarThumbnailImageUrl) {
if (isIcon) {
return user.currentAvatarThumbnailImageUrl.replace(
'/256',
`/${resolution}`
);
}
return user.currentAvatarThumbnailImageUrl;
}
if (user.currentAvatarImageUrl) {
if (isIcon) {
return convertFileUrlToImageUrl(user.currentAvatarImageUrl);
}
return user.currentAvatarImageUrl;
}
return '';
};
$app.methods.userImageFull = function (user) {
if (this.displayVRCPlusIconsAsAvatar && user.userIcon) {
return user.userIcon;
}
if (user.profilePicOverride) {
return user.profilePicOverride;
}
return user.currentAvatarImageUrl;
};
$app.methods.getImageUrlFromImageId = function (imageId) {
return `https://api.vrchat.cloud/api/1/file/${imageId}/1/`;
};
$app.methods.showConsole = function () {
AppApi.ShowDevTools();
if (
this.debug ||
this.debugWebRequests ||
this.debugWebSocket ||
this.debugUserDiff
) {
return;
}
console.log(
'%cCareful! This might not do what you think.',
'background-color: red; color: yellow; font-size: 32px; font-weight: bold'
);
console.log(
'%cIf someone told you to copy-paste something here, it can give them access to your account.',
'font-size: 20px;'
);
};
$app.methods.clearVRCXCache = function () {
API.failedGetRequests = new Map();
API.cachedUsers.forEach((ref, id) => {
if (
!this.friends.has(id) &&
!this.lastLocation.playerList.has(ref.id) &&
id !== API.currentUser.id
) {
API.cachedUsers.delete(id);
}
});
API.cachedWorlds.forEach((ref, id) => {
if (
!API.cachedFavoritesByObjectId.has(id) &&
ref.authorId !== API.currentUser.id &&
!this.localWorldFavoritesList.includes(id)
) {
API.cachedWorlds.delete(id);
}
});
API.cachedAvatars.forEach((ref, id) => {
if (
!API.cachedFavoritesByObjectId.has(id) &&
ref.authorId !== API.currentUser.id &&
!this.localAvatarFavoritesList.includes(id) &&
!$app.avatarHistory.has(id)
) {
API.cachedAvatars.delete(id);
}
});
API.cachedGroups.forEach((ref, id) => {
if (!API.currentUserGroups.has(id)) {
API.cachedGroups.delete(id);
}
});
API.cachedInstances.forEach((ref, id) => {
// delete instances over an hour old
if (Date.parse(ref.$fetchedAt) < Date.now() - 3600000) {
API.cachedInstances.delete(id);
}
});
API.cachedAvatarNames = new Map();
this.customUserTags = new Map();
this.updateInstanceInfo = 0;
};
$app.data.sqliteTableSizes = {};
$app.methods.getSqliteTableSizes = async function () {
this.sqliteTableSizes = {
gps: await database.getGpsTableSize(),
status: await database.getStatusTableSize(),
bio: await database.getBioTableSize(),
avatar: await database.getAvatarTableSize(),
onlineOffline: await database.getOnlineOfflineTableSize(),
friendLogHistory: await database.getFriendLogHistoryTableSize(),
notification: await database.getNotificationTableSize(),
location: await database.getLocationTableSize(),
joinLeave: await database.getJoinLeaveTableSize(),
portalSpawn: await database.getPortalSpawnTableSize(),
videoPlay: await database.getVideoPlayTableSize(),
event: await database.getEventTableSize(),
external: await database.getExternalTableSize()
};
};
$app.data.ipcEnabled = false;
$app.methods.ipcEvent = function (json) {
if (!API.isLoggedIn) {
return;
}
try {
var data = JSON.parse(json);
} catch {
console.log(`IPC invalid JSON, ${json}`);
return;
}
switch (data.type) {
case 'OnEvent':
if (!this.isGameRunning) {
console.log('Game closed, skipped event', data);
return;
}
if (this.debugPhotonLogging) {
console.log(
'OnEvent',
data.OnEventData.Code,
data.OnEventData
);
}
this.parsePhotonEvent(data.OnEventData, data.dt);
this.photonEventPulse();
break;
case 'OnOperationResponse':
if (!this.isGameRunning) {
console.log('Game closed, skipped event', data);
return;
}
if (this.debugPhotonLogging) {
console.log(
'OnOperationResponse',
data.OnOperationResponseData.OperationCode,
data.OnOperationResponseData
);
}
this.parseOperationResponse(
data.OnOperationResponseData,
data.dt
);
this.photonEventPulse();
break;
case 'OnOperationRequest':
if (!this.isGameRunning) {
console.log('Game closed, skipped event', data);
return;
}
if (this.debugPhotonLogging) {
console.log(
'OnOperationRequest',
data.OnOperationRequestData.OperationCode,
data.OnOperationRequestData
);
}
break;
case 'VRCEvent':
if (!this.isGameRunning) {
console.log('Game closed, skipped event', data);
return;
}
this.parseVRCEvent(data);
this.photonEventPulse();
break;
case 'Event7List':
this.photonEvent7List.clear();
for (var [id, dt] of Object.entries(data.Event7List)) {
this.photonEvent7List.set(parseInt(id, 10), dt);
}
this.photonLastEvent7List = Date.parse(data.dt);
break;
case 'VrcxMessage':
if (this.debugPhotonLogging) {
console.log('VrcxMessage:', data);
}
this.eventVrcxMessage(data);
break;
case 'Ping':
if (!this.photonLoggingEnabled) {
this.photonLoggingEnabled = true;
configRepository.setBool('VRCX_photonLoggingEnabled', true);
}
this.ipcEnabled = true;
this.ipcTimeout = 60; // 30secs
break;
case 'MsgPing':
this.externalNotifierVersion = data.version;
break;
case 'LaunchCommand':
this.eventLaunchCommand(data.command);
break;
case 'VRCXLaunch':
console.log('VRCXLaunch:', data);
break;
default:
console.log('IPC:', data);
}
};
$app.data.externalNotifierVersion = 0;
$app.data.photonEventCount = 0;
$app.data.photonEventIcon = false;
$app.data.customUserTags = new Map();
$app.methods.addCustomTag = function (data) {
if (data.Tag) {
this.customUserTags.set(data.UserId, {
tag: data.Tag,
colour: data.TagColour
});
} else {
this.customUserTags.delete(data.UserId);
}
var feedUpdate = {
userId: data.UserId,
colour: data.TagColour
};
AppApi.ExecuteVrOverlayFunction(
'updateHudFeedTag',
JSON.stringify(feedUpdate)
);
var ref = API.cachedUsers.get(data.UserId);
if (typeof ref !== 'undefined') {
ref.$customTag = data.Tag;
ref.$customTagColour = data.TagColour;
}
this.updateSharedFeed(true);
};
$app.methods.eventVrcxMessage = function (data) {
switch (data.MsgType) {
case 'CustomTag':
this.addCustomTag(data);
break;
case 'ClearCustomTags':
this.customUserTags.forEach((value, key) => {
this.customUserTags.delete(key);
var ref = API.cachedUsers.get(key);
if (typeof ref !== 'undefined') {
ref.$customTag = '';
ref.$customTagColour = '';
}
});
break;
case 'Noty':
if (
this.photonLoggingEnabled ||
(this.externalNotifierVersion &&
this.externalNotifierVersion > 21)
) {
return;
}
var entry = {
created_at: new Date().toJSON(),
type: 'Event',
data: data.Data
};
database.addGamelogEventToDatabase(entry);
this.queueGameLogNoty(entry);
this.addGameLog(entry);
break;
case 'External':
var displayName = data.DisplayName ?? '';
var entry = {
created_at: new Date().toJSON(),
type: 'External',
message: data.Data,
displayName,
userId: data.UserId,
location: this.lastLocation.location
};
database.addGamelogExternalToDatabase(entry);
this.queueGameLogNoty(entry);
this.addGameLog(entry);
break;
default:
console.log('VRCXMessage:', data);
break;
}
};
$app.methods.photonEventPulse = function () {
this.photonEventCount++;
this.photonEventIcon = true;
workerTimers.setTimeout(() => (this.photonEventIcon = false), 150);
};
$app.methods.parseOperationResponse = function (data, dateTime) {
switch (data.OperationCode) {
case 226:
if (
typeof data.Parameters[248] !== 'undefined' &&
typeof data.Parameters[248][248] !== 'undefined'
) {
this.setPhotonLobbyMaster(data.Parameters[248][248]);
}
if (typeof data.Parameters[254] !== 'undefined') {
this.photonLobbyCurrentUser = data.Parameters[254];
}
if (typeof data.Parameters[249] !== 'undefined') {
for (var i in data.Parameters[249]) {
var id = parseInt(i, 10);
var user = data.Parameters[249][i];
this.parsePhotonUser(id, user.user, dateTime);
this.parsePhotonAvatarChange(
id,
user.user,
user.avatarDict,
dateTime
);
this.parsePhotonGroupChange(
id,
user.user,
user.groupOnNameplate,
dateTime
);
this.parsePhotonAvatar(user.avatarDict);
this.parsePhotonAvatar(user.favatarDict);
var hasInstantiated = false;
var lobbyJointime = this.photonLobbyJointime.get(id);
if (typeof lobbyJointime !== 'undefined') {
hasInstantiated = lobbyJointime.hasInstantiated;
}
this.photonLobbyJointime.set(id, {
joinTime: Date.parse(dateTime),
hasInstantiated,
inVRMode: user.inVRMode,
avatarEyeHeight: user.avatarEyeHeight,
canModerateInstance: user.canModerateInstance,
groupOnNameplate: user.groupOnNameplate,
showGroupBadgeToOthers: user.showGroupBadgeToOthers,
showSocialRank: user.showSocialRank,
useImpostorAsFallback: user.useImpostorAsFallback,
platform: user.platform
});
}
}
if (typeof data.Parameters[252] !== 'undefined') {
this.parsePhotonLobbyIds(data.Parameters[252]);
}
this.photonEvent7List = new Map();
break;
}
};
API.$on('LOGIN', async function () {
var command = await AppApi.GetLaunchCommand();
if (command) {
$app.eventLaunchCommand(command);
}
});
$app.methods.eventLaunchCommand = function (input) {
if (!API.isLoggedIn) {
return;
}
console.log('LaunchCommand:', input);
var args = input.split('/');
var command = args[0];
var commandArg = args[1]?.trim();
var shouldFocusWindow = true;
switch (command) {
case 'world':
this.directAccessWorld(input.replace('world/', ''));
break;
case 'avatar':
this.showAvatarDialog(commandArg);
break;
case 'user':
this.showUserDialog(commandArg);
break;
case 'group':
this.showGroupDialog(commandArg);
break;
case 'local-favorite-world':
console.log('local-favorite-world', commandArg);
var [id, group] = commandArg.split(':');
worldRequest.getCachedWorld({ worldId: id }).then((args1) => {
this.directAccessWorld(id);
this.addLocalWorldFavorite(id, group);
return args1;
});
break;
case 'addavatardb':
this.addAvatarProvider(input.replace('addavatardb/', ''));
break;
case 'switchavatar':
var avatarId = commandArg;
var regexAvatarId =
/avtr_[0-9A-Fa-f]{8}-([0-9A-Fa-f]{4}-){3}[0-9A-Fa-f]{12}/g;
if (!avatarId.match(regexAvatarId) || avatarId.length !== 41) {
this.$message({
message: 'Invalid Avatar ID',
type: 'error'
});
break;
}
if (this.showConfirmationOnSwitchAvatar) {
this.selectAvatarWithConfirmation(avatarId);
// Makes sure the window is focused
shouldFocusWindow = true;
} else {
this.selectAvatarWithoutConfirmation(avatarId);
shouldFocusWindow = false;
}
break;
case 'import':
var type = args[1];
if (!type) break;
var data = input.replace(`import/${type}/`, '');
if (type === 'avatar') {
this.avatarImportDialogInput = data;
this.showAvatarImportDialog();
} else if (type === 'world') {
this.worldImportDialogInput = data;
this.showWorldImportDialog();
} else if (type === 'friend') {
this.friendImportDialogInput = data;
this.showFriendImportDialog();
}
break;
}
if (shouldFocusWindow) {
AppApi.FocusWindow();
}
};
$app.methods.toggleAllowBooping = function () {
userRequest
.saveCurrentUser({
isBoopingEnabled: !API.currentUser.isBoopingEnabled
})
.then((args) => {
return args;
});
};
// #endregion
// #region | App: Previous Instances Info Dialog
$app.data.previousInstancesInfoDialogVisible = false;
$app.data.previousInstancesInfoDialogInstanceId = '';
$app.methods.showPreviousInstancesInfoDialog = function (instanceId) {
this.previousInstancesInfoDialogVisible = true;
this.previousInstancesInfoDialogInstanceId = instanceId;
};
$app.data.dtHour12 = await configRepository.getBool('VRCX_dtHour12', false);
$app.data.dtIsoFormat = await configRepository.getBool(
'VRCX_dtIsoFormat',
false
);
$app.methods.setDatetimeFormat = async function (setIsoFormat = false) {
if (setIsoFormat) {
this.dtIsoFormat = !this.dtIsoFormat;
}
var currentCulture = await AppApi.CurrentCulture();
var hour12 = await configRepository.getBool('VRCX_dtHour12');
var isoFormat = await configRepository.getBool('VRCX_dtIsoFormat');
if (typeof this.dtHour12 !== 'undefined') {
if (hour12 !== this.dtHour12) {
await configRepository.setBool('VRCX_dtHour12', this.dtHour12);
this.updateVRConfigVars();
}
var hour12 = this.dtHour12;
}
if (typeof this.dtIsoFormat !== 'undefined') {
if (isoFormat !== this.dtIsoFormat) {
await configRepository.setBool(
'VRCX_dtIsoFormat',
this.dtIsoFormat
);
}
var isoFormat = this.dtIsoFormat;
}
var formatDate1 = function (date, format) {
if (!date) {
return '-';
}
var dt = new Date(date);
if (format === 'long') {
return dt.toLocaleDateString(currentCulture, {
month: '2-digit',
day: '2-digit',
year: 'numeric',
hour: 'numeric',
minute: 'numeric',
second: 'numeric',
hourCycle: hour12 ? 'h12' : 'h23'
});
} else if (format === 'short') {
return dt
.toLocaleDateString(currentCulture, {
month: '2-digit',
day: '2-digit',
hour: 'numeric',
minute: 'numeric',
hourCycle: hour12 ? 'h12' : 'h23'
})
.replace(' AM', 'am')
.replace(' PM', 'pm')
.replace(',', '');
}
return '-';
};
if (isoFormat) {
formatDate1 = function (date, format) {
if (!date) {
return '-';
}
const dt = new Date(date);
if (format === 'long') {
const formatDate = (date) => {
const padZero = (num) => String(num).padStart(2, '0');
const year = date.getFullYear();
const month = padZero(date.getMonth() + 1);
const day = padZero(date.getDate());
const hours = padZero(date.getHours());
const minutes = padZero(date.getMinutes());
const seconds = padZero(date.getSeconds());
return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`;
};
return formatDate(dt);
} else if (format === 'short') {
return dt
.toLocaleDateString('en-nz', {
month: '2-digit',
day: '2-digit',
hour: 'numeric',
minute: 'numeric',
hourCycle: hour12 ? 'h12' : 'h23'
})
.replace(' AM', 'am')
.replace(' PM', 'pm')
.replace(',', '');
}
return '-';
};
}
Vue.filter('formatDate', formatDate1);
};
$app.methods.setDatetimeFormat();
$app.data.enableCustomEndpoint = await configRepository.getBool(
'VRCX_enableCustomEndpoint',
false
);
$app.methods.toggleCustomEndpoint = async function () {
await configRepository.setBool(
'VRCX_enableCustomEndpoint',
this.enableCustomEndpoint
);
this.loginForm.endpoint = '';
this.loginForm.websocket = '';
};
$app.methods.getNameColour = async function (userId) {
var hue = await AppApi.GetColourFromUserID(userId);
return this.HueToHex(hue);
};
$app.methods.userColourInit = async function () {
var dictObject = await AppApi.GetColourBulk(
Array.from(API.cachedUsers.keys())
);
if (LINUX) {
dictObject = Object.fromEntries(dictObject);
}
for (var [userId, hue] of Object.entries(dictObject)) {
var ref = API.cachedUsers.get(userId);
if (typeof ref !== 'undefined') {
ref.$userColour = this.HueToHex(hue);
}
}
};
$app.methods.HueToHex = function (hue) {
// this.HSVtoRGB(hue / 65535, .8, .8);
if (this.isDarkMode) {
return this.HSVtoRGB(hue / 65535, 0.6, 1);
}
return this.HSVtoRGB(hue / 65535, 1, 0.7);
};
$app.methods.HSVtoRGB = function (h, s, v) {
var r = 0;
var g = 0;
var b = 0;
if (arguments.length === 1) {
var s = h.s;
var v = h.v;
var h = h.h;
}
var i = Math.floor(h * 6);
var f = h * 6 - i;
var p = v * (1 - s);
var q = v * (1 - f * s);
var t = v * (1 - (1 - f) * s);
switch (i % 6) {
case 0:
r = v;
g = t;
b = p;
break;
case 1:
r = q;
g = v;
b = p;
break;
case 2:
r = p;
g = v;
b = t;
break;
case 3:
r = p;
g = q;
b = v;
break;
case 4:
r = t;
g = p;
b = v;
break;
case 5:
r = v;
g = p;
b = q;
break;
}
var red = Math.round(r * 255);
var green = Math.round(g * 255);
var blue = Math.round(b * 255);
var decColor = 0x1000000 + blue + 0x100 * green + 0x10000 * red;
return `#${decColor.toString(16).substr(1)}`;
};
$app.methods.onPlayerTraveling = function (ref) {
if (
!this.isGameRunning ||
!this.lastLocation.location ||
this.lastLocation.location !== ref.travelingToLocation ||
ref.id === API.currentUser.id ||
this.lastLocation.playerList.has(ref.id)
) {
return;
}
var onPlayerJoining = {
created_at: new Date(ref.created_at).toJSON(),
userId: ref.id,
displayName: ref.displayName,
type: 'OnPlayerJoining'
};
this.queueFeedNoty(onPlayerJoining);
};
$app.methods.updateCurrentUserLocation = function () {
var ref = API.cachedUsers.get(API.currentUser.id);
if (typeof ref === 'undefined') {
return;
}
// update cached user with both gameLog and API locations
var currentLocation = API.currentUser.$locationTag;
var L = parseLocation(currentLocation);
if (L.isTraveling) {
currentLocation = API.currentUser.$travelingToLocation;
}
ref.location = API.currentUser.$locationTag;
ref.travelingToLocation = API.currentUser.$travelingToLocation;
if (
this.isGameRunning &&
!this.gameLogDisabled &&
this.lastLocation.location !== ''
) {
// use gameLog instead of API when game is running
currentLocation = this.lastLocation.location;
if (this.lastLocation.location === 'traveling') {
currentLocation = this.lastLocationDestination;
}
ref.location = this.lastLocation.location;
ref.travelingToLocation = this.lastLocationDestination;
}
ref.$online_for = API.currentUser.$online_for;
ref.$offline_for = API.currentUser.$offline_for;
ref.$location = parseLocation(currentLocation);
if (!this.isGameRunning || this.gameLogDisabled) {
ref.$location_at = API.currentUser.$location_at;
ref.$travelingToTime = API.currentUser.$travelingToTime;
this.applyUserDialogLocation();
this.applyWorldDialogInstances();
this.applyGroupDialogInstances();
} else {
ref.$location_at = this.lastLocation.date;
ref.$travelingToTime = this.lastLocationDestinationTime;
API.currentUser.$travelingToTime = this.lastLocationDestinationTime;
}
};
$app.methods.setCurrentUserLocation = async function (
location,
travelingToLocation
) {
API.currentUser.$location_at = Date.now();
API.currentUser.$travelingToTime = Date.now();
API.currentUser.$locationTag = location;
API.currentUser.$travelingToLocation = travelingToLocation;
this.updateCurrentUserLocation();
// janky gameLog support for Quest
if (this.isGameRunning) {
// with the current state of things, lets not run this if we don't need to
return;
}
var lastLocation = '';
for (var i = this.gameLogSessionTable.length - 1; i > -1; i--) {
var item = this.gameLogSessionTable[i];
if (item.type === 'Location') {
lastLocation = item.location;
break;
}
}
if (lastLocation === location) {
return;
}
this.lastLocationDestination = '';
this.lastLocationDestinationTime = 0;
if (isRealInstance(location)) {
var dt = new Date().toJSON();
var L = parseLocation(location);
this.lastLocation.location = location;
this.lastLocation.date = dt;
var entry = {
created_at: dt,
type: 'Location',
location,
worldId: L.worldId,
worldName: await this.getWorldName(L.worldId),
groupName: await this.getGroupName(L.groupId),
time: 0
};
database.addGamelogLocationToDatabase(entry);
this.queueGameLogNoty(entry);
this.addGameLog(entry);
this.addInstanceJoinHistory(location, dt);
this.applyUserDialogLocation();
this.applyWorldDialogInstances();
this.applyGroupDialogInstances();
} else {
this.lastLocation.location = '';
this.lastLocation.date = '';
}
};
$app.data.avatarHistory = new Set();
$app.data.avatarHistoryArray = [];
$app.methods.getAvatarHistory = async function () {
this.avatarHistory = new Set();
var historyArray = await database.getAvatarHistory(API.currentUser.id);
this.avatarHistoryArray = historyArray;
for (var i = 0; i < historyArray.length; i++) {
var avatar = historyArray[i];
if (avatar.authorId === API.currentUser.id) {
continue;
}
this.avatarHistory.add(avatar.id);
API.applyAvatar(avatar);
}
};
$app.methods.addAvatarToHistory = function (avatarId) {
avatarRequest.getAvatar({ avatarId }).then((args) => {
var { ref } = args;
database.addAvatarToCache(ref);
database.addAvatarToHistory(ref.id);
if (ref.authorId === API.currentUser.id) {
return;
}
var historyArray = this.avatarHistoryArray;
for (var i = 0; i < historyArray.length; ++i) {
if (historyArray[i].id === ref.id) {
historyArray.splice(i, 1);
}
}
this.avatarHistoryArray.unshift(ref);
this.avatarHistory.delete(ref.id);
this.avatarHistory.add(ref.id);
});
};
$app.methods.addAvatarWearTime = function (avatarId) {
if (!API.currentUser.$previousAvatarSwapTime || !avatarId) {
return;
}
const timeSpent = Date.now() - API.currentUser.$previousAvatarSwapTime;
database.addAvatarTimeSpent(avatarId, timeSpent);
};
$app.methods.promptClearAvatarHistory = function () {
this.$confirm('Continue? Clear Avatar History', 'Confirm', {
confirmButtonText: 'Confirm',
cancelButtonText: 'Cancel',
type: 'info',
callback: (action) => {
if (action === 'confirm') {
this.clearAvatarHistory();
}
}
});
};
$app.methods.clearAvatarHistory = function () {
this.avatarHistory = new Set();
this.avatarHistoryArray = [];
database.clearAvatarHistory();
};
$app.data.databaseVersion = await configRepository.getInt(
'VRCX_databaseVersion',
0
);
$app.methods.updateDatabaseVersion = async function () {
var databaseVersion = 12;
if (this.databaseVersion < databaseVersion) {
if (this.databaseVersion) {
var msgBox = this.$message({
message:
'DO NOT CLOSE VRCX, database upgrade in progress...',
type: 'warning',
duration: 0
});
}
console.log(
`Updating database from ${this.databaseVersion} to ${databaseVersion}...`
);
try {
await database.cleanLegendFromFriendLog(); // fix friendLog spammed with crap
await database.fixGameLogTraveling(); // fix bug with gameLog location being set as traveling
await database.fixNegativeGPS(); // fix GPS being a negative value due to VRCX bug with traveling
await database.fixBrokenLeaveEntries(); // fix user instance timer being higher than current user location timer
await database.fixBrokenGroupInvites(); // fix notification v2 in wrong table
await database.fixBrokenNotifications(); // fix notifications being null
await database.fixBrokenGroupChange(); // fix spam group left & name change
await database.fixCancelFriendRequestTypo(); // fix CancelFriendRequst typo
await database.fixBrokenGameLogDisplayNames(); // fix gameLog display names "DisplayName (userId)"
await database.upgradeDatabaseVersion(); // update database version
await database.vacuum(); // succ
await database.optimize();
await configRepository.setInt(
'VRCX_databaseVersion',
databaseVersion
);
console.log('Database update complete.');
msgBox?.close();
if (this.databaseVersion) {
// only display when database exists
this.$message({
message: 'Database upgrade complete',
type: 'success'
});
}
this.databaseVersion = databaseVersion;
} catch (err) {
console.error(err);
msgBox?.close();
this.$message({
message:
'Database upgrade failed, check console for details',
type: 'error',
duration: 120000
});
AppApi.ShowDevTools();
}
}
};
// #endregion
// #region | App: world favorite import
$app.data.worldImportDialogVisible = false;
$app.data.worldImportDialogInput = '';
$app.methods.showWorldImportDialog = function () {
this.worldImportDialogVisible = true;
};
// #endregion
// #region | App: avatar favorite import
$app.data.avatarImportDialogVisible = false;
$app.data.avatarImportDialogInput = '';
$app.methods.showAvatarImportDialog = function () {
this.avatarImportDialogVisible = true;
};
// #endregion
// #region | App: friend favorite import
$app.data.friendImportDialogVisible = false;
$app.data.friendImportDialogInput = '';
$app.methods.showFriendImportDialog = function () {
this.friendImportDialogVisible = true;
};
// #endregion
// #region | App: note export
$app.data.isNoteExportDialogVisible = false;
$app.methods.showNoteExportDialog = function () {
this.isNoteExportDialogVisible = true;
};
// user generated content
$app.data.ugcFolderPath = await configRepository.getString(
'VRCX_userGeneratedContentPath',
''
);
$app.data.folderSelectorDialogVisible = false;
$app.methods.setUGCFolderPath = async function (path) {
await configRepository.setString('VRCX_userGeneratedContentPath', path);
this.ugcFolderPath = path;
};
$app.methods.resetUGCFolder = function () {
this.setUGCFolderPath('');
};
$app.methods.openUGCFolder = async function () {
if (LINUX && this.ugcFolderPath == null) {
this.resetUGCFolder();
}
await AppApi.OpenUGCPhotosFolder(this.ugcFolderPath);
};
$app.methods.folderSelectorDialog = async function (oldPath) {
if (this.folderSelectorDialogVisible) return;
if (!oldPath) {
oldPath = '';
}
this.folderSelectorDialogVisible = true;
var newFolder = '';
if (LINUX) {
newFolder = await window.electron.openDirectoryDialog();
} else {
newFolder = await AppApi.OpenFolderSelectorDialog(oldPath);
}
this.folderSelectorDialogVisible = false;
return newFolder;
};
$app.methods.openUGCFolderSelector = async function () {
var path = await this.folderSelectorDialog(this.ugcFolderPath);
await this.setUGCFolderPath(path);
};
// avatar database provider
$app.data.isAvatarProviderDialogVisible = false;
$app.methods.showAvatarProviderDialog = function () {
this.isAvatarProviderDialogVisible = true;
};
$app.methods.addAvatarProvider = function (url) {
if (!url) {
return;
}
this.showAvatarProviderDialog();
if (!this.avatarRemoteDatabaseProviderList.includes(url)) {
this.avatarRemoteDatabaseProviderList.push(url);
}
this.saveAvatarProviderList();
};
$app.methods.removeAvatarProvider = function (url) {
var length = this.avatarRemoteDatabaseProviderList.length;
for (var i = 0; i < length; ++i) {
if (this.avatarRemoteDatabaseProviderList[i] === url) {
this.avatarRemoteDatabaseProviderList.splice(i, 1);
}
}
this.saveAvatarProviderList();
};
$app.methods.saveAvatarProviderList = async function () {
var length = this.avatarRemoteDatabaseProviderList.length;
for (var i = 0; i < length; ++i) {
if (!this.avatarRemoteDatabaseProviderList[i]) {
this.avatarRemoteDatabaseProviderList.splice(i, 1);
}
}
await configRepository.setString(
'VRCX_avatarRemoteDatabaseProviderList',
JSON.stringify(this.avatarRemoteDatabaseProviderList)
);
if (this.avatarRemoteDatabaseProviderList.length > 0) {
this.avatarRemoteDatabaseProvider =
this.avatarRemoteDatabaseProviderList[0];
this.avatarRemoteDatabase = true;
} else {
this.avatarRemoteDatabaseProvider = '';
this.avatarRemoteDatabase = false;
}
await configRepository.setBool(
'VRCX_avatarRemoteDatabase',
this.avatarRemoteDatabase
);
};
$app.methods.setAvatarProvider = function (provider) {
this.avatarRemoteDatabaseProvider = provider;
};
// #endregion
// #region | App: bulk unfavorite
$app.methods.bulkCopyFavoriteSelection = function (type) {
let idList = '';
switch (type) {
case 'friend':
for (let ctx of this.favoriteFriends) {
if (ctx.$selected) {
idList += `${ctx.id}\n`;
}
}
this.friendImportDialogInput = idList;
this.showFriendImportDialog();
break;
case 'world':
for (let ctx of this.favoriteWorlds) {
if (ctx.$selected) {
idList += `${ctx.id}\n`;
}
}
this.worldImportDialogInput = idList;
this.showWorldImportDialog();
break;
case 'avatar':
for (let ctx of this.favoriteAvatars) {
if (ctx.$selected) {
idList += `${ctx.id}\n`;
}
}
this.avatarImportDialogInput = idList;
this.showAvatarImportDialog();
break;
default:
break;
}
console.log('Favorite selection\n', idList);
};
$app.methods.clearBulkFavoriteSelection = function () {
for (var ctx of this.favoriteFriends) {
ctx.$selected = false;
}
for (var ctx of this.favoriteWorlds) {
ctx.$selected = false;
}
for (var ctx of this.favoriteAvatars) {
ctx.$selected = false;
}
};
// #endregion
// #region | App: local world favorites
$app.data.localWorldFavoriteGroups = [];
$app.data.localWorldFavoritesList = [];
$app.data.localWorldFavorites = {};
$app.methods.addLocalWorldFavorite = function (worldId, group) {
if (this.hasLocalWorldFavorite(worldId, group)) {
return;
}
var ref = API.cachedWorlds.get(worldId);
if (typeof ref === 'undefined') {
return;
}
if (!this.localWorldFavoritesList.includes(worldId)) {
this.localWorldFavoritesList.push(worldId);
}
if (!this.localWorldFavorites[group]) {
this.localWorldFavorites[group] = [];
}
if (!this.localWorldFavoriteGroups.includes(group)) {
this.localWorldFavoriteGroups.push(group);
}
this.localWorldFavorites[group].unshift(ref);
database.addWorldToCache(ref);
database.addWorldToFavorites(worldId, group);
if (
this.favoriteDialog.visible &&
this.favoriteDialog.objectId === worldId
) {
this.updateFavoriteDialog(worldId);
}
if (this.worldDialog.visible && this.worldDialog.id === worldId) {
this.worldDialog.isFavorite = true;
}
};
$app.methods.removeLocalWorldFavorite = function (worldId, group) {
var favoriteGroup = this.localWorldFavorites[group];
for (var i = 0; i < favoriteGroup.length; ++i) {
if (favoriteGroup[i].id === worldId) {
favoriteGroup.splice(i, 1);
}
}
// remove from cache if no longer in favorites
var worldInFavorites = false;
for (var i = 0; i < this.localWorldFavoriteGroups.length; ++i) {
var groupName = this.localWorldFavoriteGroups[i];
if (!this.localWorldFavorites[groupName] || group === groupName) {
continue;
}
for (
var j = 0;
j < this.localWorldFavorites[groupName].length;
++j
) {
var id = this.localWorldFavorites[groupName][j].id;
if (id === worldId) {
worldInFavorites = true;
break;
}
}
}
if (!worldInFavorites) {
$app.removeFromArray(this.localWorldFavoritesList, worldId);
database.removeWorldFromCache(worldId);
}
database.removeWorldFromFavorites(worldId, group);
if (
this.favoriteDialog.visible &&
this.favoriteDialog.objectId === worldId
) {
this.updateFavoriteDialog(worldId);
}
if (this.worldDialog.visible && this.worldDialog.id === worldId) {
this.worldDialog.isFavorite =
API.cachedFavoritesByObjectId.has(worldId);
}
// update UI
this.sortLocalWorldFavorites();
};
$app.methods.getLocalWorldFavorites = async function () {
this.localWorldFavoriteGroups = [];
this.localWorldFavoritesList = [];
this.localWorldFavorites = {};
var worldCache = await database.getWorldCache();
for (var i = 0; i < worldCache.length; ++i) {
var ref = worldCache[i];
if (!API.cachedWorlds.has(ref.id)) {
API.applyWorld(ref);
}
}
var favorites = await database.getWorldFavorites();
for (var i = 0; i < favorites.length; ++i) {
var favorite = favorites[i];
if (!this.localWorldFavoritesList.includes(favorite.worldId)) {
this.localWorldFavoritesList.push(favorite.worldId);
}
if (!this.localWorldFavorites[favorite.groupName]) {
this.localWorldFavorites[favorite.groupName] = [];
}
if (!this.localWorldFavoriteGroups.includes(favorite.groupName)) {
this.localWorldFavoriteGroups.push(favorite.groupName);
}
var ref = API.cachedWorlds.get(favorite.worldId);
if (typeof ref === 'undefined') {
ref = {
id: favorite.worldId
};
}
this.localWorldFavorites[favorite.groupName].unshift(ref);
}
if (this.localWorldFavoriteGroups.length === 0) {
// default group
this.localWorldFavorites.Favorites = [];
this.localWorldFavoriteGroups.push('Favorites');
}
this.sortLocalWorldFavorites();
};
$app.methods.hasLocalWorldFavorite = function (worldId, group) {
var favoriteGroup = this.localWorldFavorites[group];
if (!favoriteGroup) {
return false;
}
for (var i = 0; i < favoriteGroup.length; ++i) {
if (favoriteGroup[i].id === worldId) {
return true;
}
}
return false;
};
$app.methods.getLocalWorldFavoriteGroupLength = function (group) {
var favoriteGroup = this.localWorldFavorites[group];
if (!favoriteGroup) {
return 0;
}
return favoriteGroup.length;
};
$app.methods.newLocalWorldFavoriteGroup = function (group) {
if (this.localWorldFavoriteGroups.includes(group)) {
$app.$message({
message: $t('prompt.new_local_favorite_group.message.error', {
name: group
}),
type: 'error'
});
return;
}
if (!this.localWorldFavorites[group]) {
this.localWorldFavorites[group] = [];
}
if (!this.localWorldFavoriteGroups.includes(group)) {
this.localWorldFavoriteGroups.push(group);
}
this.sortLocalWorldFavorites();
};
$app.methods.renameLocalWorldFavoriteGroup = function (newName, group) {
if (this.localWorldFavoriteGroups.includes(newName)) {
$app.$message({
message: $t(
'prompt.local_favorite_group_rename.message.error',
{ name: newName }
),
type: 'error'
});
return;
}
this.localWorldFavoriteGroups.push(newName);
this.localWorldFavorites[newName] = this.localWorldFavorites[group];
$app.removeFromArray(this.localWorldFavoriteGroups, group);
delete this.localWorldFavorites[group];
database.renameWorldFavoriteGroup(newName, group);
this.sortLocalWorldFavorites();
};
$app.methods.sortLocalWorldFavorites = function () {
this.localWorldFavoriteGroups.sort();
if (!this.sortFavorites) {
for (var i = 0; i < this.localWorldFavoriteGroups.length; ++i) {
var group = this.localWorldFavoriteGroups[i];
if (this.localWorldFavorites[group]) {
this.localWorldFavorites[group].sort($utils.compareByName);
}
}
}
};
$app.methods.deleteLocalWorldFavoriteGroup = function (group) {
// remove from cache if no longer in favorites
var worldIdRemoveList = new Set();
var favoriteGroup = this.localWorldFavorites[group];
for (var i = 0; i < favoriteGroup.length; ++i) {
worldIdRemoveList.add(favoriteGroup[i].id);
}
$app.removeFromArray(this.localWorldFavoriteGroups, group);
delete this.localWorldFavorites[group];
database.deleteWorldFavoriteGroup(group);
for (var i = 0; i < this.localWorldFavoriteGroups.length; ++i) {
var groupName = this.localWorldFavoriteGroups[i];
if (!this.localWorldFavorites[groupName]) {
continue;
}
for (
var j = 0;
j < this.localWorldFavorites[groupName].length;
++j
) {
var worldId = this.localWorldFavorites[groupName][j].id;
if (worldIdRemoveList.has(worldId)) {
worldIdRemoveList.delete(worldId);
break;
}
}
}
worldIdRemoveList.forEach((id) => {
$app.removeFromArray(this.localWorldFavoritesList, id);
database.removeWorldFromCache(id);
});
};
API.$on('WORLD', function (args) {
if ($app.localWorldFavoritesList.includes(args.ref.id)) {
// update db cache
database.addWorldToCache(args.ref);
}
});
API.$on('LOGIN', function () {
$app.getLocalWorldFavorites();
});
// #endregion
// #region | App: Local Avatar Favorites
$app.data.localAvatarFavoriteGroups = [];
$app.data.localAvatarFavoritesList = [];
$app.data.localAvatarFavorites = {};
$app.methods.addLocalAvatarFavorite = function (avatarId, group) {
if (this.hasLocalAvatarFavorite(avatarId, group)) {
return;
}
var ref = API.cachedAvatars.get(avatarId);
if (typeof ref === 'undefined') {
return;
}
if (!this.localAvatarFavoritesList.includes(avatarId)) {
this.localAvatarFavoritesList.push(avatarId);
}
if (!this.localAvatarFavorites[group]) {
this.localAvatarFavorites[group] = [];
}
if (!this.localAvatarFavoriteGroups.includes(group)) {
this.localAvatarFavoriteGroups.push(group);
}
this.localAvatarFavorites[group].unshift(ref);
database.addAvatarToCache(ref);
database.addAvatarToFavorites(avatarId, group);
if (
this.favoriteDialog.visible &&
this.favoriteDialog.objectId === avatarId
) {
this.updateFavoriteDialog(avatarId);
}
if (this.avatarDialog.visible && this.avatarDialog.id === avatarId) {
this.avatarDialog.isFavorite = true;
}
};
$app.methods.removeLocalAvatarFavorite = function (avatarId, group) {
var favoriteGroup = this.localAvatarFavorites[group];
for (var i = 0; i < favoriteGroup.length; ++i) {
if (favoriteGroup[i].id === avatarId) {
favoriteGroup.splice(i, 1);
}
}
// remove from cache if no longer in favorites
var avatarInFavorites = false;
for (var i = 0; i < this.localAvatarFavoriteGroups.length; ++i) {
var groupName = this.localAvatarFavoriteGroups[i];
if (!this.localAvatarFavorites[groupName] || group === groupName) {
continue;
}
for (
var j = 0;
j < this.localAvatarFavorites[groupName].length;
++j
) {
var id = this.localAvatarFavorites[groupName][j].id;
if (id === avatarId) {
avatarInFavorites = true;
break;
}
}
}
if (!avatarInFavorites) {
$app.removeFromArray(this.localAvatarFavoritesList, avatarId);
if (!this.avatarHistory.has(avatarId)) {
database.removeAvatarFromCache(avatarId);
}
}
database.removeAvatarFromFavorites(avatarId, group);
if (
this.favoriteDialog.visible &&
this.favoriteDialog.objectId === avatarId
) {
this.updateFavoriteDialog(avatarId);
}
if (this.avatarDialog.visible && this.avatarDialog.id === avatarId) {
this.avatarDialog.isFavorite =
API.cachedFavoritesByObjectId.has(avatarId);
}
// update UI
this.sortLocalAvatarFavorites();
};
API.$on('AVATAR', function (args) {
if ($app.localAvatarFavoritesList.includes(args.ref.id)) {
for (var i = 0; i < $app.localAvatarFavoriteGroups.length; ++i) {
var groupName = $app.localAvatarFavoriteGroups[i];
if (!$app.localAvatarFavorites[groupName]) {
continue;
}
for (
var j = 0;
j < $app.localAvatarFavorites[groupName].length;
++j
) {
var ref = $app.localAvatarFavorites[groupName][j];
if (ref.id === args.ref.id) {
$app.localAvatarFavorites[groupName][j] = args.ref;
}
}
}
// update db cache
database.addAvatarToCache(args.ref);
}
});
API.$on('LOGIN', function () {
$app.localAvatarFavoriteGroups = [];
$app.localAvatarFavoritesList = [];
$app.localAvatarFavorites = {};
workerTimers.setTimeout(() => $app.getLocalAvatarFavorites(), 100);
});
$app.methods.getLocalAvatarFavorites = async function () {
this.localAvatarFavoriteGroups = [];
this.localAvatarFavoritesList = [];
this.localAvatarFavorites = {};
var avatarCache = await database.getAvatarCache();
for (var i = 0; i < avatarCache.length; ++i) {
var ref = avatarCache[i];
if (!API.cachedAvatars.has(ref.id)) {
API.applyAvatar(ref);
}
}
var favorites = await database.getAvatarFavorites();
for (var i = 0; i < favorites.length; ++i) {
var favorite = favorites[i];
if (!this.localAvatarFavoritesList.includes(favorite.avatarId)) {
this.localAvatarFavoritesList.push(favorite.avatarId);
}
if (!this.localAvatarFavorites[favorite.groupName]) {
this.localAvatarFavorites[favorite.groupName] = [];
}
if (!this.localAvatarFavoriteGroups.includes(favorite.groupName)) {
this.localAvatarFavoriteGroups.push(favorite.groupName);
}
var ref = API.cachedAvatars.get(favorite.avatarId);
if (typeof ref === 'undefined') {
ref = {
id: favorite.avatarId
};
}
this.localAvatarFavorites[favorite.groupName].unshift(ref);
}
if (this.localAvatarFavoriteGroups.length === 0) {
// default group
this.localAvatarFavorites.Favorites = [];
this.localAvatarFavoriteGroups.push('Favorites');
}
this.sortLocalAvatarFavorites();
};
$app.methods.hasLocalAvatarFavorite = function (avatarId, group) {
var favoriteGroup = this.localAvatarFavorites[group];
if (!favoriteGroup) {
return false;
}
for (var i = 0; i < favoriteGroup.length; ++i) {
if (favoriteGroup[i].id === avatarId) {
return true;
}
}
return false;
};
$app.methods.promptNewLocalAvatarFavoriteGroup = function () {
this.$prompt(
$t('prompt.new_local_favorite_group.description'),
$t('prompt.new_local_favorite_group.header'),
{
distinguishCancelAndClose: true,
confirmButtonText: $t('prompt.new_local_favorite_group.ok'),
cancelButtonText: $t('prompt.new_local_favorite_group.cancel'),
inputPattern: /\S+/,
inputErrorMessage: $t(
'prompt.new_local_favorite_group.input_error'
),
callback: (action, instance) => {
if (action === 'confirm' && instance.inputValue) {
this.newLocalAvatarFavoriteGroup(instance.inputValue);
}
}
}
);
};
$app.methods.newLocalAvatarFavoriteGroup = function (group) {
if (this.localAvatarFavoriteGroups.includes(group)) {
$app.$message({
message: $t('prompt.new_local_favorite_group.message.error', {
name: group
}),
type: 'error'
});
return;
}
if (!this.localAvatarFavorites[group]) {
this.localAvatarFavorites[group] = [];
}
if (!this.localAvatarFavoriteGroups.includes(group)) {
this.localAvatarFavoriteGroups.push(group);
}
this.sortLocalAvatarFavorites();
};
$app.methods.promptLocalAvatarFavoriteGroupRename = function (group) {
this.$prompt(
$t('prompt.local_favorite_group_rename.description'),
$t('prompt.local_favorite_group_rename.header'),
{
distinguishCancelAndClose: true,
confirmButtonText: $t(
'prompt.local_favorite_group_rename.save'
),
cancelButtonText: $t(
'prompt.local_favorite_group_rename.cancel'
),
inputPattern: /\S+/,
inputErrorMessage: $t(
'prompt.local_favorite_group_rename.input_error'
),
inputValue: group,
callback: (action, instance) => {
if (action === 'confirm' && instance.inputValue) {
this.renameLocalAvatarFavoriteGroup(
instance.inputValue,
group
);
}
}
}
);
};
$app.methods.renameLocalAvatarFavoriteGroup = function (newName, group) {
if (this.localAvatarFavoriteGroups.includes(newName)) {
$app.$message({
message: $t(
'prompt.local_favorite_group_rename.message.error',
{ name: newName }
),
type: 'error'
});
return;
}
this.localAvatarFavoriteGroups.push(newName);
this.localAvatarFavorites[newName] = this.localAvatarFavorites[group];
$app.removeFromArray(this.localAvatarFavoriteGroups, group);
delete this.localAvatarFavorites[group];
database.renameAvatarFavoriteGroup(newName, group);
this.sortLocalAvatarFavorites();
};
$app.methods.promptLocalAvatarFavoriteGroupDelete = function (group) {
this.$confirm(`Delete Group? ${group}`, 'Confirm', {
confirmButtonText: 'Confirm',
cancelButtonText: 'Cancel',
type: 'info',
callback: (action) => {
if (action === 'confirm') {
this.deleteLocalAvatarFavoriteGroup(group);
}
}
});
};
$app.methods.sortLocalAvatarFavorites = function () {
this.localAvatarFavoriteGroups.sort();
if (!this.sortFavorites) {
for (var i = 0; i < this.localAvatarFavoriteGroups.length; ++i) {
var group = this.localAvatarFavoriteGroups[i];
if (this.localAvatarFavorites[group]) {
this.localAvatarFavorites[group].sort($utils.compareByName);
}
}
}
};
$app.methods.deleteLocalAvatarFavoriteGroup = function (group) {
// remove from cache if no longer in favorites
var avatarIdRemoveList = new Set();
var favoriteGroup = this.localAvatarFavorites[group];
for (var i = 0; i < favoriteGroup.length; ++i) {
avatarIdRemoveList.add(favoriteGroup[i].id);
}
$app.removeFromArray(this.localAvatarFavoriteGroups, group);
delete this.localAvatarFavorites[group];
database.deleteAvatarFavoriteGroup(group);
for (var i = 0; i < this.localAvatarFavoriteGroups.length; ++i) {
var groupName = this.localAvatarFavoriteGroups[i];
if (!this.localAvatarFavorites[groupName]) {
continue;
}
for (
var j = 0;
j < this.localAvatarFavorites[groupName].length;
++j
) {
var avatarId = this.localAvatarFavorites[groupName][j].id;
if (avatarIdRemoveList.has(avatarId)) {
avatarIdRemoveList.delete(avatarId);
break;
}
}
}
avatarIdRemoveList.forEach((id) => {
// remove from cache if no longer in favorites
var avatarInFavorites = false;
loop: for (
var i = 0;
i < this.localAvatarFavoriteGroups.length;
++i
) {
var groupName = this.localAvatarFavoriteGroups[i];
if (
!this.localAvatarFavorites[groupName] ||
group === groupName
) {
continue loop;
}
for (
var j = 0;
j < this.localAvatarFavorites[groupName].length;
++j
) {
var avatarId = this.localAvatarFavorites[groupName][j].id;
if (id === avatarId) {
avatarInFavorites = true;
break loop;
}
}
}
if (!avatarInFavorites) {
$app.removeFromArray(this.localAvatarFavoritesList, id);
if (!this.avatarHistory.has(id)) {
database.removeAvatarFromCache(id);
}
}
});
};
// #endregion
// #region | Local Favorite Friends
$app.data.localFavoriteFriends = new Set();
$app.data.localFavoriteFriendsGroups = JSON.parse(
await configRepository.getString(
'VRCX_localFavoriteFriendsGroups',
'[]'
)
);
$app.methods.updateLocalFavoriteFriends = function () {
this.localFavoriteFriends.clear();
for (const ref of API.cachedFavorites.values()) {
if (
!ref.$isDeleted &&
ref.type === 'friend' &&
(this.localFavoriteFriendsGroups.includes(ref.$groupKey) ||
this.localFavoriteFriendsGroups.length === 0)
) {
this.localFavoriteFriends.add(ref.favoriteId);
}
}
this.updateSidebarFriendsList();
configRepository.setString(
'VRCX_localFavoriteFriendsGroups',
JSON.stringify(this.localFavoriteFriendsGroups)
);
};
$app.methods.updateSidebarFriendsList = function () {
for (var ctx of this.friends.values()) {
var isVIP = this.localFavoriteFriends.has(ctx.id);
if (ctx.isVIP === isVIP) {
continue;
}
ctx.isVIP = isVIP;
if (ctx.state !== 'online') {
continue;
}
if (ctx.isVIP) {
$app.removeFromArray(this.onlineFriends_, ctx);
this.vipFriends_.push(ctx);
this.sortVIPFriends = true;
} else {
$app.removeFromArray(this.vipFriends_, ctx);
this.onlineFriends_.push(ctx);
this.sortOnlineFriends = true;
}
}
};
// #endregion
// #region | App: ChatBox Blacklist
$app.methods.checkChatboxBlacklist = function (msg) {
for (var i = 0; i < this.chatboxBlacklist.length; ++i) {
if (msg.includes(this.chatboxBlacklist[i])) {
return true;
}
}
return false;
};
// #endregion
// #region | App: ChatBox User Blacklist
$app.data.chatboxUserBlacklist = new Map();
if (await configRepository.getString('VRCX_chatboxUserBlacklist')) {
$app.data.chatboxUserBlacklist = new Map(
Object.entries(
JSON.parse(
await configRepository.getString(
'VRCX_chatboxUserBlacklist'
)
)
)
);
}
$app.methods.getLocalAvatarFavoriteGroupLength = function (group) {
var favoriteGroup = this.localAvatarFavorites[group];
if (!favoriteGroup) {
return 0;
}
return favoriteGroup.length;
};
$app.methods.saveChatboxUserBlacklist = async function () {
await configRepository.setString(
'VRCX_chatboxUserBlacklist',
JSON.stringify(Object.fromEntries(this.chatboxUserBlacklist))
);
};
// #endregion
// #region | App: Instance queuing
API.queuedInstances = new Map();
$app.methods.removeAllQueuedInstances = function () {
API.queuedInstances.forEach((ref) => {
this.$message({
message: `Removed instance ${ref.$worldName} from queue`,
type: 'info'
});
ref.$msgBox?.close();
});
API.queuedInstances.clear();
};
$app.methods.removeQueuedInstance = function (instanceId) {
var ref = API.queuedInstances.get(instanceId);
if (typeof ref !== 'undefined') {
ref.$msgBox.close();
API.queuedInstances.delete(instanceId);
}
};
API.applyQueuedInstance = function (instanceId) {
API.queuedInstances.forEach((ref) => {
if (ref.location !== instanceId) {
$app.$message({
message: $t('message.instance.removed_form_queue', {
worldName: ref.$worldName
}),
type: 'info'
});
ref.$msgBox?.close();
API.queuedInstances.delete(ref.location);
}
});
if (!instanceId) {
return;
}
if (!API.queuedInstances.has(instanceId)) {
var L = parseLocation(instanceId);
if (L.isRealInstance) {
instanceRequest
.getInstance({
worldId: L.worldId,
instanceId: L.instanceId
})
.then((args) => {
if (args.json?.queueSize) {
$app.instanceQueueUpdate(
instanceId,
args.json?.queueSize,
args.json?.queueSize
);
}
});
}
$app.instanceQueueUpdate(instanceId, 0, 0);
}
};
$app.methods.instanceQueueReady = function (instanceId) {
var ref = API.queuedInstances.get(instanceId);
if (typeof ref !== 'undefined') {
ref.$msgBox.close();
API.queuedInstances.delete(instanceId);
}
var L = parseLocation(instanceId);
var group = API.cachedGroups.get(L.groupId);
var groupName = group?.name ?? '';
var worldName = ref?.$worldName ?? '';
const location = displayLocation(instanceId, worldName, groupName);
this.$message({
message: `Instance ready to join ${location}`,
type: 'success'
});
var noty = {
created_at: new Date().toJSON(),
type: 'group.queueReady',
imageUrl: group?.iconUrl,
message: `Instance ready to join ${location}`,
location: instanceId,
groupName,
worldName
};
if (
this.notificationTable.filters[0].value.length === 0 ||
this.notificationTable.filters[0].value.includes(noty.type)
) {
this.notifyMenu('notification');
}
this.queueNotificationNoty(noty);
this.notificationTable.data.push(noty);
this.updateSharedFeed(true);
};
$app.methods.instanceQueueUpdate = async function (
instanceId,
position,
queueSize
) {
var ref = API.queuedInstances.get(instanceId);
if (typeof ref === 'undefined') {
ref = {
$msgBox: null,
$groupName: '',
$worldName: '',
location: instanceId,
position: 0,
queueSize: 0,
updatedAt: 0
};
}
ref.position = position;
ref.queueSize = queueSize;
ref.updatedAt = Date.now();
if (!ref.$msgBox || ref.$msgBox.closed) {
ref.$msgBox = this.$message({
message: '',
type: 'info',
duration: 0,
showClose: true,
customClass: 'vrc-instance-queue-message'
});
}
if (!ref.$groupName) {
ref.$groupName = await this.getGroupName(instanceId);
}
if (!ref.$worldName) {
ref.$worldName = await this.getWorldName(instanceId);
}
const location = displayLocation(
instanceId,
ref.$worldName,
ref.$groupName
);
ref.$msgBox.message = `You are in position ${ref.position} of ${ref.queueSize} in the queue for ${location} `;
API.queuedInstances.set(instanceId, ref);
// workerTimers.setTimeout(this.instanceQueueTimeout, 3600000);
};
$app.methods.instanceQueueClear = function () {
// remove all instances from queue
API.queuedInstances.forEach((ref) => {
ref.$msgBox.close();
API.queuedInstances.delete(ref.location);
});
};
// #endregion
$app.methods.checkVRChatDebugLogging = async function () {
if (this.gameLogDisabled) {
return;
}
try {
var loggingEnabled =
await this.getVRChatRegistryKey('LOGGING_ENABLED');
if (
loggingEnabled === null ||
typeof loggingEnabled === 'undefined'
) {
// key not found
return;
}
if (parseInt(loggingEnabled, 10) === 1) {
// already enabled
return;
}
var result = await AppApi.SetVRChatRegistryKey(
'LOGGING_ENABLED',
'1',
4
);
if (!result) {
// failed to set key
this.$alert(
'VRCX has noticed VRChat debug logging is disabled. VRCX requires debug logging in order to function correctly. Please enable debug logging in VRChat quick menu settings > debug > enable debug logging, then rejoin the instance or restart VRChat.',
'Enable debug logging'
);
console.error('Failed to enable debug logging', result);
return;
}
this.$alert(
'VRCX has noticed VRChat debug logging is disabled and automatically re-enabled it. VRCX requires debug logging in order to function correctly.',
'Enabled debug logging'
);
console.log('Enabled debug logging');
} catch (e) {
console.error(e);
}
};
// #endregion
// #region | App: Language
$app.data.userDialogWorldSortingOptions = {};
$app.data.userDialogWorldOrderOptions = {};
$app.methods.applyUserDialogSortingStrings = function () {
this.userDialogWorldSortingOptions = {
name: {
name: $t('dialog.user.worlds.sorting.name'),
value: 'name'
},
updated: {
name: $t('dialog.user.worlds.sorting.updated'),
value: 'updated'
},
created: {
name: $t('dialog.user.worlds.sorting.created'),
value: 'created'
},
favorites: {
name: $t('dialog.user.worlds.sorting.favorites'),
value: 'favorites'
},
popularity: {
name: $t('dialog.user.worlds.sorting.popularity'),
value: 'popularity'
}
};
this.userDialogWorldOrderOptions = {
descending: {
name: $t('dialog.user.worlds.order.descending'),
value: 'descending'
},
ascending: {
name: $t('dialog.user.worlds.order.ascending'),
value: 'ascending'
}
};
};
$app.data.groupDialogSortingOptions = {};
$app.data.groupDialogFilterOptions = {};
$app.methods.applyGroupDialogSortingStrings = function () {
this.groupDialogSortingOptions = {
joinedAtDesc: {
name: $t('dialog.group.members.sorting.joined_at_desc'),
value: 'joinedAt:desc'
},
joinedAtAsc: {
name: $t('dialog.group.members.sorting.joined_at_asc'),
value: 'joinedAt:asc'
},
userId: {
name: $t('dialog.group.members.sorting.user_id'),
value: ''
}
};
this.groupDialogFilterOptions = {
everyone: {
name: $t('dialog.group.members.filters.everyone'),
id: null
},
usersWithNoRole: {
name: $t('dialog.group.members.filters.users_with_no_role'),
id: ''
}
};
};
$app.methods.applyLanguageStrings = function () {
// repply sorting strings
this.applyUserDialogSortingStrings();
this.applyGroupDialogSortingStrings();
this.userDialog.worldSorting =
this.userDialogWorldSortingOptions.updated;
this.userDialog.worldOrder =
this.userDialogWorldOrderOptions.descending;
this.userDialog.groupSorting =
userDialogGroupSortingOptions.alphabetical;
this.groupDialog.memberFilter = this.groupDialogFilterOptions.everyone;
this.groupDialog.memberSortOrder =
this.groupDialogSortingOptions.joinedAtDesc;
};
$app.data.appLanguage =
(await configRepository.getString('VRCX_appLanguage')) ?? 'en';
$utils.changeCJKorder($app.data.appLanguage);
i18n.locale = $app.data.appLanguage;
$app.methods.initLanguage = async function () {
if (!(await configRepository.getString('VRCX_appLanguage'))) {
var result = await AppApi.CurrentLanguage();
if (!result) {
console.error('Failed to get current language');
this.changeAppLanguage('en');
return;
}
var lang = result.split('-')[0];
i18n.availableLocales.forEach((ref) => {
var refLang = ref.split('_')[0];
if (refLang === lang) {
this.changeAppLanguage(ref);
}
});
}
$app.applyLanguageStrings();
};
$app.methods.changeAppLanguage = function (language) {
console.log('Language changed:', language);
this.appLanguage = language;
$utils.changeCJKorder(language);
i18n.locale = language;
configRepository.setString('VRCX_appLanguage', language);
this.applyLanguageStrings();
this.updateVRConfigVars();
this._stringComparer = undefined;
};
// #endregion
// #region | App: Random unsorted app methods, data structs, API functions, and an API feedback/file analysis event
$app.data.changeLogDialog = {
visible: false,
buildName: '',
changeLog: ''
};
$app.methods.showChangeLogDialog = function () {
this.changeLogDialog.visible = true;
this.checkForVRCXUpdate();
};
$app.methods.changeLogRemoveLinks = function (text) {
return text.replace(/([^!])\[[^\]]+\]\([^)]+\)/g, '$1');
};
$app.methods.openFolderGeneric = function (path) {
AppApi.OpenFolderAndSelectItem(path, true);
};
// #endregion
// #region | Dialog: fullscreen image
$app.data.fullscreenImageDialog = {
visible: false,
imageUrl: '',
fileName: ''
};
$app.methods.showFullscreenImageDialog = function (imageUrl, fileName) {
if (!imageUrl) {
return;
}
const D = this.fullscreenImageDialog;
D.imageUrl = imageUrl;
D.fileName = fileName;
D.visible = true;
};
// #endregion
// #region | Open common folders
$app.methods.openVrcxAppDataFolder = function () {
AppApi.OpenVrcxAppDataFolder().then((result) => {
if (result) {
this.$message({
message: 'Folder opened',
type: 'success'
});
} else {
this.$message({
message: "Folder dosn't exist",
type: 'error'
});
}
});
};
$app.methods.openVrcAppDataFolder = function () {
AppApi.OpenVrcAppDataFolder().then((result) => {
if (result) {
this.$message({
message: 'Folder opened',
type: 'success'
});
} else {
this.$message({
message: "Folder dosn't exist",
type: 'error'
});
}
});
};
$app.methods.openVrcPhotosFolder = function () {
AppApi.OpenVrcPhotosFolder().then((result) => {
if (result) {
this.$message({
message: 'Folder opened',
type: 'success'
});
} else {
this.$message({
message: "Folder dosn't exist",
type: 'error'
});
}
});
};
$app.methods.openVrcScreenshotsFolder = function () {
AppApi.OpenVrcScreenshotsFolder().then((result) => {
if (result) {
this.$message({
message: 'Folder opened',
type: 'success'
});
} else {
this.$message({
message: "Folder dosn't exist",
type: 'error'
});
}
});
};
$app.methods.openCrashVrcCrashDumps = function () {
AppApi.OpenCrashVrcCrashDumps().then((result) => {
if (result) {
this.$message({
message: 'Folder opened',
type: 'success'
});
} else {
this.$message({
message: "Folder dosn't exist",
type: 'error'
});
}
});
};
// #endregion
// #region | Close instance
$app.methods.closeInstance = function (location) {
this.$confirm(
'Continue? Close Instance, nobody will be able to join',
'Confirm',
{
confirmButtonText: 'Confirm',
cancelButtonText: 'Cancel',
type: 'warning',
callback: (action) => {
if (action !== 'confirm') {
return;
}
miscRequest.closeInstance({ location, hardClose: false });
}
}
);
};
API.$on('INSTANCE:CLOSE', function (args) {
if (args.json) {
$app.$message({
message: $t('message.instance.closed'),
type: 'success'
});
this.$emit('INSTANCE', {
json: args.json
});
}
});
// #endregion
// #region | Settings: Zoom
$app.data.zoomLevel = ((await AppApi.GetZoom()) + 10) * 10;
$app.methods.getZoomLevel = async function () {
this.zoomLevel = ((await AppApi.GetZoom()) + 10) * 10;
};
$app.methods.setZoomLevel = function () {
AppApi.SetZoom(this.zoomLevel / 10 - 10);
};
// #endregion
// #region instance join history
$app.data.instanceJoinHistory = new Map();
API.$on('LOGIN', function () {
$app.instanceJoinHistory = new Map();
$app.getInstanceJoinHistory();
});
$app.methods.getInstanceJoinHistory = async function () {
this.instanceJoinHistory = await database.getInstanceJoinHistory();
};
$app.methods.addInstanceJoinHistory = function (location, dateTime) {
if (!location || !dateTime) {
return;
}
if (this.instanceJoinHistory.has(location)) {
this.instanceJoinHistory.delete(location);
}
var epoch = new Date(dateTime).getTime();
this.instanceJoinHistory.set(location, epoch);
};
// #endregion
// #region persistent data
API.$on('WORLD:PERSIST:HAS', function (args) {
if (
args.params.worldId === $app.worldDialog.id &&
$app.worldDialog.visible
) {
$app.worldDialog.hasPersistData = args.json !== false;
}
});
API.$on('WORLD:PERSIST:DELETE', function (args) {
if (
args.params.worldId === $app.worldDialog.id &&
$app.worldDialog.visible
) {
$app.worldDialog.hasPersistData = false;
}
});
// #endregion
$app.data.ossDialog = false;
$app.computed.isLinux = function () {
return LINUX;
};
// friendsListSidebar
// - DivideByFriendGroup
$app.data.isSidebarDivideByFriendGroup = await configRepository.getBool(
'VRCX_sidebarDivideByFriendGroup',
true
);
$app.methods.handleSwitchDivideByFriendGroup = async function () {
this.isSidebarDivideByFriendGroup = !this.isSidebarDivideByFriendGroup;
await configRepository.setBool(
'VRCX_sidebarDivideByFriendGroup',
this.isSidebarDivideByFriendGroup
);
};
// - SidebarGroupByInstance
$app.data.isSidebarGroupByInstance = await configRepository.getBool(
'VRCX_sidebarGroupByInstance',
true
);
$app.methods.toggleGroupByInstance = function () {
this.isSidebarGroupByInstance = !this.isSidebarGroupByInstance;
configRepository.setBool(
'VRCX_sidebarGroupByInstance',
this.isSidebarGroupByInstance
);
};
$app.data.isHideFriendsInSameInstance = await configRepository.getBool(
'VRCX_hideFriendsInSameInstance',
false
);
$app.methods.toggleHideFriendsInSameInstance = function () {
this.isHideFriendsInSameInstance = !this.isHideFriendsInSameInstance;
configRepository.setBool(
'VRCX_hideFriendsInSameInstance',
this.isHideFriendsInSameInstance
);
};
// #endregion
// #region | Tab Props
$app.computed.moderationTabBind = function () {
return {
menuActiveIndex: this.menuActiveIndex,
tableData: this.playerModerationTable,
shiftHeld: this.shiftHeld,
hideTooltips: this.hideTooltips
};
};
$app.computed.friendsListTabBind = function () {
return {
menuActiveIndex: this.menuActiveIndex,
friends: this.friends,
hideTooltips: this.hideTooltips,
randomUserColours: this.randomUserColours,
sortStatus: this.sortStatus,
confirmDeleteFriend: this.confirmDeleteFriend,
friendsListSearch: this.friendsListSearch,
stringComparer: this.stringComparer
};
};
$app.computed.friendsListTabEvent = function () {
return {
'get-all-user-stats': this.getAllUserStats,
'lookup-user': this.lookupUser,
'update:friends-list-search': (value) =>
(this.friendsListSearch = value)
};
};
$app.computed.sideBarTabBind = function () {
return {
isSideBarTabShow: this.isSideBarTabShow,
style: { width: `${this.asideWidth}px` },
vipFriends: this.vipFriends,
onlineFriends: this.onlineFriends,
quickSearchRemoteMethod: this.quickSearchRemoteMethod,
quickSearchItems: this.quickSearchItems,
hideTooltips: this.hideTooltips,
onlineFriendCount: this.onlineFriendCount,
friends: this.friends,
isGameRunning: this.isGameRunning,
isSidebarDivideByFriendGroup: this.isSidebarDivideByFriendGroup,
isSidebarGroupByInstance: this.isSidebarGroupByInstance,
isHideFriendsInSameInstance: this.isHideFriendsInSameInstance,
gameLogDisabled: this.gameLogDisabled,
lastLocation: this.lastLocation,
lastLocationDestination: this.lastLocationDestination,
hideNicknames: this.hideNicknames,
activeFriends: this.activeFriends,
offlineFriends: this.offlineFriends,
groupInstances: this.groupInstances,
inGameGroupOrder: this.inGameGroupOrder,
groupedByGroupKeyFavoriteFriends:
this.groupedByGroupKeyFavoriteFriends,
isAgeGatedInstancesVisible: this.isAgeGatedInstancesVisible
};
};
$app.computed.sideBarTabEvent = function () {
return {
'show-group-dialog': this.showGroupDialog,
'quick-search-change': this.quickSearchChange,
'direct-access-paste': this.directAccessPaste,
'refresh-friends-list': this.refreshFriendsList,
'confirm-delete-friend': this.confirmDeleteFriend
};
};
$app.computed.isSideBarTabShow = function () {
return !(
this.menuActiveIndex === 'friendList' ||
this.menuActiveIndex === 'charts'
);
};
$app.computed.favoritesTabBind = function () {
return {
menuActiveIndex: this.menuActiveIndex,
hideTooltips: this.hideTooltips,
shiftHeld: this.shiftHeld,
favoriteFriends: this.favoriteFriends,
sortFavorites: this.sortFavorites,
groupedByGroupKeyFavoriteFriends:
this.groupedByGroupKeyFavoriteFriends,
favoriteWorlds: this.favoriteWorlds,
localWorldFavoriteGroups: this.localWorldFavoriteGroups,
localWorldFavorites: this.localWorldFavorites,
avatarHistoryArray: this.avatarHistoryArray,
localAvatarFavoriteGroups: this.localAvatarFavoriteGroups,
localAvatarFavorites: this.localAvatarFavorites,
favoriteAvatars: this.favoriteAvatars,
localAvatarFavoritesList: this.localAvatarFavoritesList,
localWorldFavoritesList: this.localWorldFavoritesList
};
};
$app.computed.favoritesTabEvent = function () {
return {
'update:sort-favorites': (value) => (this.sortFavorites = value),
'clear-bulk-favorite-selection': this.clearBulkFavoriteSelection,
'bulk-copy-favorite-selection': this.bulkCopyFavoriteSelection,
'get-local-world-favorites': this.getLocalWorldFavorites,
'show-friend-import-dialog': this.showFriendImportDialog,
'save-sort-favorites-option': this.saveSortFavoritesOption,
'show-world-import-dialog': this.showWorldImportDialog,
'show-world-dialog': this.showWorldDialog,
'new-instance-self-invite': this.newInstanceSelfInvite,
'delete-local-world-favorite-group':
this.deleteLocalWorldFavoriteGroup,
'remove-local-world-favorite': this.removeLocalWorldFavorite,
'show-avatar-import-dialog': this.showAvatarImportDialog,
'show-avatar-dialog': this.showAvatarDialog,
'remove-local-avatar-favorite': this.removeLocalAvatarFavorite,
'select-avatar-with-confirmation':
this.selectAvatarWithConfirmation,
'prompt-clear-avatar-history': this.promptClearAvatarHistory,
'prompt-new-local-avatar-favorite-group':
this.promptNewLocalAvatarFavoriteGroup,
'prompt-local-avatar-favorite-group-rename':
this.promptLocalAvatarFavoriteGroupRename,
'prompt-local-avatar-favorite-group-delete':
this.promptLocalAvatarFavoriteGroupDelete,
'new-local-world-favorite-group': this.newLocalWorldFavoriteGroup,
'rename-local-world-favorite-group':
this.renameLocalWorldFavoriteGroup
};
};
$app.computed.chartsTabBind = function () {
return {
getWorldName: this.getWorldName,
isDarkMode: this.isDarkMode,
dtHour12: this.dtHour12,
friendsMap: this.friends,
localFavoriteFriends: this.localFavoriteFriends
};
};
$app.computed.chartsTabEvent = function () {
return {
'open-previous-instance-info-dialog':
this.showPreviousInstancesInfoDialog
};
};
$app.computed.friendLogTabBind = function () {
return {
menuActiveIndex: this.menuActiveIndex,
friendLogTable: this.friendLogTable,
shiftHeld: this.shiftHeld
};
};
$app.computed.gameLogTabBind = function () {
return {
menuActiveIndex: this.menuActiveIndex,
gameLogTable: this.gameLogTable,
shiftHeld: this.shiftHeld,
hideTooltips: this.hideTooltips,
gameLogIsFriend: this.gameLogIsFriend,
gameLogIsFavorite: this.gameLogIsFavorite
};
};
$app.computed.gameLogTabEvent = function () {
return {
gameLogTableLookup: this.gameLogTableLookup,
lookupUser: this.lookupUser,
updateGameLogSessionTable: (val) =>
(this.gameLogSessionTable = val),
updateSharedFeed: this.updateSharedFeed
};
};
$app.computed.notificationTabBind = function () {
return {
menuActiveIndex: this.menuActiveIndex,
notificationTable: this.notificationTable,
shiftHeld: this.shiftHeld,
hideTooltips: this.hideTooltips,
lastLocation: this.lastLocation,
lastLocationDestination: this.lastLocationDestination,
isGameRunning: this.isGameRunning,
inviteResponseMessageTable: this.inviteResponseMessageTable,
uploadImage: this.uploadImage,
checkCanInvite: this.checkCanInvite,
inviteRequestResponseMessageTable:
this.inviteRequestResponseMessageTable
};
};
$app.computed.notificationTabEvent = function () {
return {
inviteImageUpload: this.inviteImageUpload,
clearInviteImageUpload: this.clearInviteImageUpload
};
};
$app.computed.feedTabBind = function () {
return {
menuActiveIndex: this.menuActiveIndex,
hideTooltips: this.hideTooltips,
feedTable: this.feedTable
};
};
$app.computed.feedTabEvent = function () {
return {
feedTableLookup: this.feedTableLookup
};
};
$app.computed.searchTabBind = function () {
return {
menuActiveIndex: this.menuActiveIndex,
searchText: this.searchText,
searchUserResults: this.searchUserResults,
randomUserColours: this.randomUserColours,
avatarRemoteDatabaseProviderList:
this.avatarRemoteDatabaseProviderList,
avatarRemoteDatabaseProvider: this.avatarRemoteDatabaseProvider,
hideTooltips: this.hideTooltips,
userDialog: this.userDialog,
lookupAvatars: this.lookupAvatars,
avatarRemoteDatabase: this.avatarRemoteDatabase
};
};
$app.computed.searchTabEvent = function () {
return {
clearSearch: this.clearSearch,
setAvatarProvider: this.setAvatarProvider,
refreshUserDialogAvatars: this.refreshUserDialogAvatars,
moreSearchUser: this.moreSearchUser,
'update:searchText': (value) => (this.searchText = value)
};
};
$app.computed.profileTabBind = function () {
return {
menuActiveIndex: this.menuActiveIndex,
hideTooltips: this.hideTooltips,
inviteMessageTable: this.inviteMessageTable,
inviteResponseMessageTable: this.inviteResponseMessageTable,
inviteRequestMessageTable: this.inviteRequestMessageTable,
inviteRequestResponseMessageTable:
this.inviteRequestResponseMessageTable,
pastDisplayNameTable: this.pastDisplayNameTable,
friends: this.friends,
directAccessWorld: this.directAccessWorld
};
};
$app.computed.profileTabEvent = function () {
return {
logout: this.logout,
lookupUser: this.lookupUser,
showEditInviteMessageDialog: this.showEditInviteMessageDialog
};
};
$app.computed.playerListTabBind = function () {
return {
menuActiveIndex: this.menuActiveIndex,
currentInstanceWorld: this.currentInstanceWorld,
currentInstanceLocation: this.currentInstanceLocation,
currentInstanceWorldDescriptionExpanded:
this.currentInstanceWorldDescriptionExpanded,
photonLoggingEnabled: this.photonLoggingEnabled,
photonEventTableTypeFilter: this.photonEventTableTypeFilter,
photonEventTableTypeFilterList: this.photonEventTableTypeFilterList,
photonEventTableFilter: this.photonEventTableFilter,
hideTooltips: this.hideTooltips,
ipcEnabled: this.ipcEnabled,
photonEventIcon: this.photonEventIcon,
photonEventTable: this.photonEventTable,
photonEventTablePrevious: this.photonEventTablePrevious,
currentInstanceUserList: this.currentInstanceUserList,
chatboxUserBlacklist: this.chatboxUserBlacklist,
randomUserColours: this.randomUserColours,
lastLocation: this.lastLocation
};
};
$app.computed.playerListTabEvent = function () {
return {
photonEventTableFilterChange: this.photonEventTableFilterChange,
getCurrentInstanceUserList: this.getCurrentInstanceUserList,
showUserFromPhotonId: this.showUserFromPhotonId,
lookupUser: this.lookupUser
};
};
$app.computed.loginPageBind = function () {
return {
hideTooltips: this.hideTooltips,
loginForm: this.loginForm,
enableCustomEndpoint: this.enableCustomEndpoint
};
};
$app.computed.loginPageEvent = function () {
return {
showVRCXUpdateDialog: this.showVRCXUpdateDialog,
promptProxySettings: this.promptProxySettings,
toggleCustomEndpoint: this.toggleCustomEndpoint,
deleteSavedLogin: this.deleteSavedLogin,
login: this.login,
relogin: this.relogin
};
};
$app.computed.vrcxUpdateDialogBind = function () {
return {
VRCXUpdateDialog: this.VRCXUpdateDialog,
appVersion: this.appVersion,
checkingForVRCXUpdate: this.checkingForVRCXUpdate,
updateInProgress: this.updateInProgress,
updateProgress: this.updateProgress,
updateProgressText: this.updateProgressText,
pendingVRCXInstall: this.pendingVRCXInstall,
branch: this.branch,
branches: this.branches
};
};
$app.computed.vrcxUpdateDialogEvent = function () {
return {
'update:branch': (value) => (this.branch = value),
loadBranchVersions: this.loadBranchVersions,
cancelUpdate: this.cancelUpdate,
installVRCXUpdate: this.installVRCXUpdate,
restartVRCX: this.restartVRCX
};
};
$app.methods.languageClass = function (key) {
return languageClass(key);
};
// #endregion
// #region | Electron
if (LINUX) {
window.electron.onWindowPositionChanged((event, position) => {
window.$app.locationX = position.x;
window.$app.locationY = position.y;
window.$app.saveVRCXWindowOption();
});
window.electron.onWindowSizeChanged((event, size) => {
window.$app.sizeWidth = size.width;
window.$app.sizeHeight = size.height;
window.$app.saveVRCXWindowOption();
});
window.electron.onWindowStateChange((event, state) => {
window.$app.windowState = state;
window.$app.saveVRCXWindowOption();
});
// window.electron.onWindowClosed((event) => {
// window.$app.saveVRCXWindowOption();
// });
}
// #endregion
// "$app" is being replaced by Vue, update references inside all the classes
$app = new Vue($app);
window.$app = $app;
window.API = API;
window.$t = $t;
for (let value of Object.values(vrcxClasses)) {
value.updateRef($app);
}
})();
// #endregion
// // #endregion
// // #region | Dialog: templateDialog
// $app.data.templateDialog = {
// visible: false,
// };
// $app.methods.showTemplateDialog = function () {
// this.$nextTick(() => $app.adjustDialogZ(this.$refs.templateDialog.$el));
// var D = this.templateDialog;
// D.visible = true;
// };
// // #endregion