diff --git a/Dotnet/LogWatcher.cs b/Dotnet/LogWatcher.cs index 62341d8b1..b0b4213cd 100644 --- a/Dotnet/LogWatcher.cs +++ b/Dotnet/LogWatcher.cs @@ -603,15 +603,15 @@ private bool ParseLogVideoError(FileInfo fileInfo, LogContext logContext, string var data = line.Substring(offset + 24); if (data == logContext.LastVideoError) return true; - logContext.LastVideoError = data; + logContext.LastVideoError = data; - AppendLog(new[] - { - fileInfo.Name, - ConvertLogTimeToISO8601(line), - "event", - "VideoError: " + data - }); + AppendLog(new[] + { + fileInfo.Name, + ConvertLogTimeToISO8601(line), + "event", + "VideoError: " + data + }); return true; } @@ -1015,6 +1015,19 @@ private bool ParseLogUdonException(FileInfo fileInfo, string line) { // 2022.11.29 04:27:33 Error - [UdonBehaviour] An exception occurred during Udon execution, this UdonBehaviour will be halted. // VRC.Udon.VM.UdonVMException: An exception occurred in an UdonVM, execution will be halted. --->VRC.Udon.VM.UdonVMException: An exception occurred during EXTERN to 'VRCSDKBaseVRCPlayerApi.__get_displayName__SystemString'. --->System.NullReferenceException: Object reference not set to an instance of an object. + + if (line.Contains("[PyPyDance]")) + { + AppendLog(new[] + { + fileInfo.Name, + ConvertLogTimeToISO8601(line), + "udon-exception", + line + }); + return true; + } + var lineOffset = line.IndexOf(" ---> VRC.Udon.VM.UdonVMException: "); if (lineOffset < 0) return false; @@ -1193,7 +1206,7 @@ private bool ParseOscFailedToStart(FileInfo fileInfo, LogContext logContext, str fileInfo.Name, ConvertLogTimeToISO8601(line), "event", - $"VRChat couldn't start OSC server, you may be affected by (https://vrchat.canny.io/bug-reports/p/installexe-breaks-osc-port-binding) \"{line.Substring(offset)}\"" + $"VRChat couldn't start OSC server, \"{line.Substring(offset)}\"" }); return true; } diff --git a/html/src/app.js b/html/src/app.js index f4c5208b9..bfe78138c 100644 --- a/html/src/app.js +++ b/html/src/app.js @@ -1,4 +1,4 @@ -// Copyright(c) 2019-2022 pypy, Natsumi and individual contributors. +// Copyright(c) 2019-2024 pypy, Natsumi and individual contributors. // All rights reserved. // // This work is licensed under the terms of the MIT license. @@ -16,28 +16,53 @@ import VueI18n from 'vue-i18n'; import { DataTables } from 'vue-data-tables'; import ElementUI from 'element-ui'; import * as workerTimers from 'worker-timers'; -import VueMarkdown from 'vue-markdown'; import 'default-passive-events'; +// util classes import configRepository from './repository/config.js'; import webApiService from './service/webapi.js'; -import gameLogService from './service/gamelog.js'; import security from './security.js'; import database from './repository/database.js'; import * as localizedStrings from './localization/localizedStrings.js'; import removeConfusables, { removeWhitespace } from './libsAndLolisAndSugoiLibs/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'; + +// 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 _booping from './classes/booping.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'; + +// API classes +import _config from './classes/API/config.js'; // #endregion +// some workaround for failing to get voice list first run speechSynthesis.getVoices(); // #region | Hey look it's most of VRCX! (async function () { - var $app = null; - - // #region | Init + // #region | Init Cef C# bindings await CefSharp.BindObjectAsync( 'AppApi', 'WebApi', @@ -49,8 +74,151 @@ speechSynthesis.getVoices(); 'AssetBundleCacher' ); + // #region | localization + Vue.use(VueI18n); + const i18n = new VueI18n({ + locale: 'en', + fallbackLocale: 'en', + messages: localizedStrings + }); + const $t = i18n.t.bind(i18n); + Vue.use(ElementUI, { + i18n: (key, value) => i18n.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 $utils = new _utils().$utils; + 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), + booping: new _booping($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) + }; + await configRepository.init(); + const app = { + data: { + API, + isGameRunning: false, + isGameNoVR: true, + isSteamVRRunning: false, + isHmdAfk: false, + appVersion: '', + latestAppVersion: '' + }, + i18n, + computed: {}, + methods: { + ...$utils + }, + watch: {}, + el: '#x-app', + async mounted() { + await this.initLanguage(); + await this.changeThemeMode(); + await AppApi.SetUserAgent(); + 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_USER_DIALOG', (userId) => + this.showUserDialog(userId) + ); + API.$on('SHOW_WORLD_DIALOG', (tag) => this.showWorldDialog(tag)); + API.$on('SHOW_WORLD_DIALOG_SHORTNAME', (tag) => + this.verifyShortName('', tag) + ); + API.$on('SHOW_GROUP_DIALOG', (groupId) => + this.showGroupDialog(groupId) + ); + API.$on('SHOW_LAUNCH_DIALOG', (tag, shortName) => + this.showLaunchDialog(tag, shortName) + ); + this.updateLoop(); + this.getGameLogTable(); + this.refreshCustomCss(); + this.refreshCustomScript(); + this.checkVRChatDebugLogging(); + this.checkAutoBackupRestoreVrcRegistry(); + await this.migrateStoredUsers(); + 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; + } + }); + } + }; + for (let value of Object.values(vrcxClasses)) { + app.methods = { ...app.methods, ...value._methods }; + app.data = { ...app.data, ...value._data }; + } + 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. @@ -86,47 +254,7 @@ speechSynthesis.getVoices(); }); // #endregion - // #region | Init: Define VRCX database helper functions, flush timer - - VRCXStorage.GetArray = async function (key) { - try { - var array = JSON.parse(await this.Get(key)); - if (Array.isArray(array)) { - return array; - } - } catch (err) { - console.error(err); - } - return []; - }; - - VRCXStorage.SetArray = function (key, value) { - this.Set(key, JSON.stringify(value)); - }; - VRCXStorage.GetObject = async function (key) { - try { - var object = JSON.parse(await this.Get(key)); - if (object === Object(object)) { - return object; - } - } catch (err) { - console.error(err); - } - return {}; - }; - - VRCXStorage.SetObject = function (key, value) { - this.Set(key, JSON.stringify(value)); - }; - - workerTimers.setInterval( - () => { - VRCXStorage.Flush(); - }, - 5 * 60 * 1000 - ); - // #endregion // #region | Init: Noty, Vue, Vue-Markdown, ElementUI, VueI18n, VueLazyLoad, Vue filters, dark stylesheet Noty.overrideDefaults({ @@ -139,98 +267,8 @@ speechSynthesis.getVoices(); timeout: 6000 }); - Vue.component('vue-markdown', VueMarkdown); - - Vue.use(VueI18n); - - var i18n = new VueI18n({ - locale: 'en', - fallbackLocale: 'en', - messages: localizedStrings - }); - - var $t = i18n.t.bind(i18n); - - Vue.use(ElementUI, { - i18n: (key, value) => i18n.t(key, value) - }); - - var removeFromArray = function (array, item) { - var { length } = array; - for (var i = 0; i < length; ++i) { - if (array[i] === item) { - array.splice(i, 1); - return true; - } - } - return false; - }; - - var arraysMatch = function (a, b) { - if (!Array.isArray(a) || !Array.isArray(b)) { - return false; - } - return ( - a.length === b.length && - a.every( - (element, index) => - JSON.stringify(element) === JSON.stringify(b[index]) - ) - ); - }; - - var escapeTag = function (tag) { - var s = String(tag); - return s.replace(/["&'<>]/g, (c) => `&#${c.charCodeAt(0)};`); - }; - Vue.filter('escapeTag', escapeTag); - - var commaNumber = function (num) { - if (!num) { - return '0'; - } - var s = String(Number(num)); - return s.replace(/(\d)(?=(\d{3})+(?!\d))/g, '$1,'); - }; - Vue.filter('commaNumber', commaNumber); - - var textToHex = function (text) { - var s = String(text); - return s - .split('') - .map((c) => c.charCodeAt(0).toString(16)) - .join(' '); - }; - Vue.filter('textToHex', textToHex); - - var timeToText = function (sec) { - var n = Number(sec); - if (isNaN(n)) { - return escapeTag(sec); - } - n = Math.floor(n / 1000); - var arr = []; - if (n < 0) { - n = -n; - } - if (n >= 86400) { - arr.push(`${Math.floor(n / 86400)}d`); - n %= 86400; - } - if (n >= 3600) { - arr.push(`${Math.floor(n / 3600)}h`); - n %= 3600; - } - if (n >= 60) { - arr.push(`${Math.floor(n / 60)}m`); - n %= 60; - } - if (arr.length === 0 && n < 60) { - arr.push(`${n}s`); - } - return arr.join(' '); - }; - Vue.filter('timeToText', timeToText); + Vue.filter('commaNumber', $utils.commaNumber); + Vue.filter('textToHex', $utils.textToHex); Vue.use(VueLazyload, { preLoad: 1, @@ -245,1387 +283,478 @@ speechSynthesis.getVoices(); Vue.use(DataTables); // #endregion - // #region | Init: Languages - - // vrchat to famfamfam - var languageMappings = { - eng: 'us', - kor: 'kr', - rus: 'ru', - spa: 'es', - por: 'pt', - zho: 'cn', - deu: 'de', - jpn: 'jp', - fra: 'fr', - swe: 'se', - nld: 'nl', - pol: 'pl', - dan: 'dk', - nor: 'no', - ita: 'it', - tha: 'th', - fin: 'fi', - hun: 'hu', - ces: 'cz', - tur: 'tr', - ara: 'ae', - ron: 'ro', - vie: 'vn', - ukr: 'ua', - ase: 'us', - bfi: 'gb', - dse: 'nl', - fsl: 'fr', - jsl: 'jp', - kvk: 'kr', - - mlt: 'mt', - ind: 'id', - hrv: 'hr', - heb: 'he', - afr: 'af', - ben: 'be', - bul: 'bg', - cmn: 'cn', - cym: 'cy', - ell: 'el', - est: 'et', - fil: 'ph', - gla: 'gd', - gle: 'ga', - hin: 'hi', - hmn: 'cn', - hye: 'hy', - isl: 'is', - lav: 'lv', - lit: 'lt', - ltz: 'lb', - mar: 'hi', - mkd: 'mk', - msa: 'my', - sco: 'gd', - slk: 'sk', - slv: 'sl', - tel: 'hi', - mri: 'nz', - wuu: 'cn', - yue: 'cn', - tws: 'cn', - asf: 'au', - nzs: 'nz', - gsg: 'de', - epo: 'eo', - tok: 'tok' - }; - // #endregion + // #endregion // #region | API: This is NOT all the api functions, not even close :( - // #region | API: Base - var API = {}; + // #region | API: User - API.eventHandlers = new Map(); + // 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} - API.$emit = function (name, ...args) { - if ($app.debug) { - console.log(name, ...args); - } - var handlers = this.eventHandlers.get(name); - if (typeof handlers === 'undefined') { - return; - } - try { - for (var handler of handlers) { - handler.apply(this, args); - } - } catch (err) { - console.error(err); - } - }; + // 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 } ] } - API.$on = function (name, handler) { - var handlers = this.eventHandlers.get(name); - if (typeof handlers === 'undefined') { - handlers = []; - this.eventHandlers.set(name, handlers); - } - handlers.push(handler); - }; + // 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.$off = function (name, handler) { - var handlers = this.eventHandlers.get(name); - if (typeof handlers === 'undefined') { + API.$on('USER', function (args) { + if (!args?.json?.displayName) { + console.error('API.$on(USER) invalid args', args); return; } - var { length } = handlers; - for (var i = 0; i < length; ++i) { - if (handlers[i] === handler) { - if (length > 1) { - handlers.splice(i, 1); - } else { - this.eventHandlers.delete(name); + $app.updateFriend({ id: args.json.id, state: args.json.state }); + args.ref = this.applyUser(args.json); + }); + + API.$on('USER:LIST', function (args) { + for (var json of args.json) { + this.$emit('USER', { + json, + params: { + userId: json.id } - break; - } + }); } - }; - - API.pendingGetRequests = new Map(); - API.failedGetRequests = new Map(); - API.endpointDomainVrchat = 'https://api.vrchat.cloud/api/1'; - API.websocketDomainVrchat = 'wss://pipeline.vrchat.cloud'; - API.endpointDomain = 'https://api.vrchat.cloud/api/1'; - API.websocketDomain = 'wss://pipeline.vrchat.cloud'; - API.attemptingAutoLogin = false; + }); - API.call = function (endpoint, options) { - var init = { - url: `${API.endpointDomain}/${endpoint}`, - method: 'GET', - ...options - }; - var { params } = init; - if (init.method === 'GET') { - // don't retry recent 404/403 - if (this.failedGetRequests.has(endpoint)) { - var lastRun = this.failedGetRequests.get(endpoint); - if (lastRun >= Date.now() - 900000) { - // 15mins - throw new Error( - `Bailing request due to recent 404/403, ${endpoint}` - ); - } - this.failedGetRequests.delete(endpoint); - } - // transform body to url - if (params === Object(params)) { - var url = new URL(init.url); - var { searchParams } = url; - for (var key in params) { - searchParams.set(key, params[key]); - } - init.url = url.toString(); - } - // merge requests - var req = this.pendingGetRequests.get(init.url); - if (typeof req !== 'undefined') { - if (req.time >= Date.now() - 10000) { - // 10s - return req.req; - } - this.pendingGetRequests.delete(init.url); - } - } else if ( - init.uploadImage || - init.uploadFilePUT || - init.uploadImageLegacy - ) { - // nothing - } else { - init.headers = { - 'Content-Type': 'application/json;charset=utf-8', - ...init.headers - }; - init.body = - params === Object(params) ? JSON.stringify(params) : '{}'; + 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; } - var req = webApiService - .execute(init) - .catch((err) => { - this.$throw(0, err, endpoint); - }) - .then((response) => { - if (!response.data) { - return response; - } - try { - response.data = JSON.parse(response.data); - if ($app.debugWebRequests) { - console.log(init, response.data); - } - return response; - } catch (e) {} - if (response.status === 200) { - this.$throw(0, 'Invalid JSON response', endpoint); - } - if ( - response.status === 429 && - init.url.endsWith('/instances/groups') - ) { - $app.nextGroupInstanceRefresh = 120; // 1min - throw new Error( - `${response.status}: rate limited ${endpoint}` - ); - } - if (response.status === 504 || response.status === 502) { - // ignore expected API errors - throw new Error( - `${response.status}: ${response.data} ${endpoint}` - ); - } - this.$throw(response.status, endpoint); - return {}; - }) - .then(({ data, status }) => { - if (status === 200) { - if (!data) { - return data; - } - var text = ''; - if (data.success === Object(data.success)) { - text = data.success.message; - } else if (data.OK === String(data.OK)) { - text = data.OK; - } - if (text) { - new Noty({ - type: 'success', - text: escapeTag(text) - }).show(); - } - return data; - } - if ( - status === 401 && - data.error.message === '"Missing Credentials"' - ) { - this.$emit('AUTOLOGIN'); - throw new Error('401: Missing Credentials'); - } - if ( - status === 401 && - data.error.message === '"Unauthorized"' && - endpoint !== 'auth/user' - ) { - // trigger 2FA dialog - if (!$app.twoFactorAuthDialogVisible) { - $app.API.getCurrentUser(); - } - throw new Error('401: Unauthorized'); - } - if (status === 403 && endpoint === 'config') { - $app.$alert( - 'VRChat currently blocks most VPNs. Please disable any connected VPNs and try again.', - 'Login Error 403' - ); - this.logout(); - throw new Error(`403: ${endpoint}`); - } - if ( - init.method === 'GET' && - status === 404 && - endpoint.startsWith('avatars/') - ) { - $app.$message({ - message: 'Avatar private or deleted', - type: 'error' - }); - $app.avatarDialog.visible = false; - throw new Error(`404: ${data.error.message} ${endpoint}`); - } - if (status === 404 && endpoint.endsWith('/persist/exists')) { - return false; - } - if ( - init.method === 'GET' && - (status === 404 || status === 403) && - !endpoint.startsWith('auth/user') - ) { - this.failedGetRequests.set(endpoint, Date.now()); - } - if ( - init.method === 'GET' && - status === 404 && - endpoint.startsWith('users/') && - endpoint.split('/').length - 1 === 1 - ) { - throw new Error(`404: ${data.error.message} ${endpoint}`); - } - if ( - status === 404 && - endpoint.startsWith('invite/') && - init.inviteId - ) { - this.expireNotification(init.inviteId); - } - if ( - status === 403 && - endpoint.startsWith('invite/myself/to/') - ) { - throw new Error(`403: ${data.error.message} ${endpoint}`); - } - if (data && data.error === Object(data.error)) { - this.$throw( - data.error.status_code || status, - data.error.message, - endpoint - ); - } else if (data && typeof data.error === 'string') { - this.$throw( - data.status_code || status, - data.error, - endpoint - ); - } - this.$throw(status, data, endpoint); - return data; - }); - if (init.method === 'GET') { - req.finally(() => { - this.pendingGetRequests.delete(init.url); - }); - this.pendingGetRequests.set(init.url, { req, time: Date.now() }); - } - return req; - }; - - API.statusCodes = { - 100: 'Continue', - 101: 'Switching Protocols', - 102: 'Processing', - 103: 'Early Hints', - 200: 'OK', - 201: 'Created', - 202: 'Accepted', - 203: 'Non-Authoritative Information', - 204: 'No Content', - 205: 'Reset Content', - 206: 'Partial Content', - 207: 'Multi-Status', - 208: 'Already Reported', - 226: 'IM Used', - 300: 'Multiple Choices', - 301: 'Moved Permanently', - 302: 'Found', - 303: 'See Other', - 304: 'Not Modified', - 305: 'Use Proxy', - 306: 'Switch Proxy', - 307: 'Temporary Redirect', - 308: 'Permanent Redirect', - 400: 'Bad Request', - 401: 'Unauthorized', - 402: 'Payment Required', - 403: 'Forbidden', - 404: 'Not Found', - 405: 'Method Not Allowed', - 406: 'Not Acceptable', - 407: 'Proxy Authentication Required', - 408: 'Request Timeout', - 409: 'Conflict', - 410: 'Gone', - 411: 'Length Required', - 412: 'Precondition Failed', - 413: 'Payload Too Large', - 414: 'URI Too Long', - 415: 'Unsupported Media Type', - 416: 'Range Not Satisfiable', - 417: 'Expectation Failed', - 418: "I'm a teapot", - 421: 'Misdirected Request', - 422: 'Unprocessable Entity', - 423: 'Locked', - 424: 'Failed Dependency', - 425: 'Too Early', - 426: 'Upgrade Required', - 428: 'Precondition Required', - 429: 'Too Many Requests', - 431: 'Request Header Fields Too Large', - 451: 'Unavailable For Legal Reasons', - 500: 'Internal Server Error', - 501: 'Not Implemented', - 502: 'Bad Gateway', - 503: 'Service Unavailable', - 504: 'Gateway Timeout', - 505: 'HTTP Version Not Supported', - 506: 'Variant Also Negotiates', - 507: 'Insufficient Storage', - 508: 'Loop Detected', - 510: 'Not Extended', - 511: 'Network Authentication Required', - // CloudFlare Error - 520: 'Web server returns an unknown error', - 521: 'Web server is down', - 522: 'Connection timed out', - 523: 'Origin is unreachable', - 524: 'A timeout occurred', - 525: 'SSL handshake failed', - 526: 'Invalid SSL certificate', - 527: 'Railgun Listener to origin error' - }; - - // FIXME : extra를 없애줘 - API.$throw = function (code, error, endpoint) { - var text = []; - if (code > 0) { - var status = this.statusCodes[code]; - if (typeof status === 'undefined') { - text.push(`${code}`); - } else { - text.push(`${code} ${status}`); - } - } - if (typeof error !== 'undefined') { - text.push(JSON.stringify(error)); + if (tags.includes('system_troll')) { + ref.$isTroll = true; } - if (typeof endpoint !== 'undefined') { - text.push(JSON.stringify(endpoint)); + if (tags.includes('system_probable_troll') && !ref.$isTroll) { + ref.$isProbableTroll = true; } - text = text.map((s) => escapeTag(s)).join('
'); - if (text.length) { - if (this.errorNoty) { - this.errorNoty.close(); - } - this.errorNoty = new Noty({ - type: 'error', - text - }).show(); + 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; } - throw new Error(text); - }; - - API.$bulk = function (options, args) { - if ('handle' in options) { - options.handle.call(this, args, options); + if (ref.$isTroll || ref.$isProbableTroll) { + trustColor = 'troll'; + ref.$trustSortNum += 0.1; } - if ( - args.json.length > 0 && - ((options.params.offset += args.json.length), - // eslint-disable-next-line no-nested-ternary - options.N > 0 - ? options.N > options.params.offset - : options.N < 0 - ? args.json.length - : options.params.n === args.json.length) - ) { - this.bulk(options); - } else if ('done' in options) { - options.done.call(this, true, options); + if (ref.$isModerator) { + trustColor = 'vip'; + ref.$trustSortNum += 0.3; } - return args; - }; - - API.bulk = function (options) { - this[options.fn](options.params) - .catch((err) => { - if ('done' in options) { - options.done.call(this, false, options); - } - throw err; - }) - .then((args) => this.$bulk(options, args)); - }; - - // #endregion - // #region | API: Config - - API.cachedConfig = {}; - - API.$on('CONFIG', function (args) { - args.ref = this.applyConfig(args.json); - }); - - API.applyConfig = function (json) { - var ref = { - ...json - }; - this.cachedConfig = ref; - return ref; - }; - - API.getConfig = function () { - return this.call('config', { - method: 'GET' - }).then((json) => { - var args = { - json - }; - this.$emit('CONFIG', args); - return args; - }); - }; - - // #endregion - // #region | API: Location - - API.parseLocation = function (tag) { - var _tag = String(tag || ''); - var ctx = { - tag: _tag, - isOffline: false, - isPrivate: false, - isTraveling: false, - worldId: '', - instanceId: '', - instanceName: '', - accessType: '', - accessTypeName: '', - region: '', - shortName: '', - userId: null, - hiddenId: null, - privateId: null, - friendsId: null, - groupId: null, - groupAccessType: null, - canRequestInvite: false, - strict: false - }; - if (_tag === 'offline' || _tag === 'offline:offline') { - ctx.isOffline = true; - } else if (_tag === 'private' || _tag === 'private:private') { - ctx.isPrivate = true; - } else if (_tag === 'traveling' || _tag === 'traveling:traveling') { - ctx.isTraveling = true; - } else if (_tag.startsWith('local') === false) { - var sep = _tag.indexOf(':'); - // technically not part of instance id, but might be there when coping id from url so why not support it - var shortNameQualifier = '&shortName='; - var shortNameIndex = _tag.indexOf(shortNameQualifier); - if (shortNameIndex >= 0) { - ctx.shortName = _tag.substr( - shortNameIndex + shortNameQualifier.length - ); - _tag = _tag.substr(0, shortNameIndex); - } - if (sep >= 0) { - ctx.worldId = _tag.substr(0, sep); - ctx.instanceId = _tag.substr(sep + 1); - ctx.instanceId.split('~').forEach((s, i) => { - if (i) { - var A = s.indexOf('('); - var Z = A >= 0 ? s.lastIndexOf(')') : -1; - var key = Z >= 0 ? s.substr(0, A) : s; - var value = A < Z ? s.substr(A + 1, Z - A - 1) : ''; - if (key === 'hidden') { - ctx.hiddenId = value; - } else if (key === 'private') { - ctx.privateId = value; - } else if (key === 'friends') { - ctx.friendsId = value; - } else if (key === 'canRequestInvite') { - ctx.canRequestInvite = true; - } else if (key === 'region') { - ctx.region = value; - } else if (key === 'group') { - ctx.groupId = value; - } else if (key === 'groupAccessType') { - ctx.groupAccessType = value; - } else if (key === 'strict') { - ctx.strict = true; - } - } else { - ctx.instanceName = s; - } + if ($app.randomUserColours && $app.friendLogInitStatus) { + if (!ref.$userColour) { + $app.getNameColour(ref.id).then((colour) => { + ref.$userColour = colour; }); - ctx.accessType = 'public'; - if (ctx.privateId !== null) { - if (ctx.canRequestInvite) { - // InvitePlus - ctx.accessType = 'invite+'; - } else { - // InviteOnly - ctx.accessType = 'invite'; - } - ctx.userId = ctx.privateId; - } else if (ctx.friendsId !== null) { - // FriendsOnly - ctx.accessType = 'friends'; - ctx.userId = ctx.friendsId; - } else if (ctx.hiddenId !== null) { - // FriendsOfGuests - ctx.accessType = 'friends+'; - ctx.userId = ctx.hiddenId; - } else if (ctx.groupId !== null) { - // Group - ctx.accessType = 'group'; - } - ctx.accessTypeName = ctx.accessType; - if (ctx.groupAccessType !== null) { - if (ctx.groupAccessType === 'public') { - ctx.accessTypeName = 'groupPublic'; - } else if (ctx.groupAccessType === 'plus') { - ctx.accessTypeName = 'groupPlus'; - } - } - } else { - ctx.worldId = _tag; } + } else { + ref.$userColour = $app.trustColor[trustColor]; } - return ctx; }; - Vue.component('launch', { - template: - '', - props: { - location: String - }, - methods: { - parse() { - this.$el.style.display = $app.checkCanInviteSelf(this.location) - ? '' - : 'none'; - }, - confirm() { - API.$emit('SHOW_LAUNCH_DIALOG', this.location); + // FIXME: it may performance issue. review here + API.applyUserLanguage = function (ref) { + ref.$languages = []; + var { tags } = ref; + for (var tag of tags) { + if (tag.startsWith('language_') === false) { + continue; } - }, - watch: { - location() { - this.parse(); + var key = tag.substr(9); + var value = $app.subsetOfLanguages[key]; + if (typeof value === 'undefined') { + continue; } - }, - mounted() { - this.parse(); + ref.$languages.push({ + key, + value + }); } - }); + }; - Vue.component('invite-yourself', { - template: - '', - props: { - location: String, - shortname: String - }, - methods: { - parse() { - this.$el.style.display = $app.checkCanInviteSelf(this.location) - ? '' - : 'none'; - }, - confirm() { - $app.selfInvite(this.location, this.shortname); - } - }, - watch: { - location() { - this.parse(); - } - }, - mounted() { - this.parse(); + API.applyPresenceLocation = function (ref) { + var presence = ref.presence; + if ($app.isRealInstance(presence.world)) { + ref.$locationTag = `${presence.world}:${presence.instance}`; + } else { + ref.$locationTag = presence.world; } - }); + if ($app.isRealInstance(presence.travelingToWorld)) { + ref.$travelingToLocation = `${presence.travelingToWorld}:${presence.travelingToInstance}`; + } else { + ref.$travelingToLocation = presence.travelingToWorld; + } + $app.updateCurrentUserLocation(); + }; - Vue.component('location', { - template: - "" + - '' + - '{{ text }}' + - '({{ groupName }})' + - '' + - '', - props: { - location: String, - traveling: String, - hint: { - type: String, - default: '' - }, - grouphint: { - type: String, - default: '' - }, - link: { - type: Boolean, - default: true - } - }, - data() { - return { - text: this.location, - region: this.region, - strict: this.strict, - isTraveling: this.isTraveling, - groupName: this.groupName - }; - }, - methods: { - parse() { - this.isTraveling = false; - this.groupName = ''; - var instanceId = this.location; - if ( - typeof this.traveling !== 'undefined' && - this.location === 'traveling' - ) { - instanceId = this.traveling; - this.isTraveling = true; - } - this.text = instanceId; - var L = API.parseLocation(instanceId); - if (L.isOffline) { - this.text = 'Offline'; - } else if (L.isPrivate) { - this.text = 'Private'; - } else if (L.isTraveling) { - this.text = 'Traveling'; - } else if (typeof this.hint === 'string' && this.hint !== '') { - if (L.instanceId) { - this.text = `${this.hint} #${L.instanceName} ${L.accessTypeName}`; - } else { - this.text = this.hint; - } - } else if (L.worldId) { - var ref = API.cachedWorlds.get(L.worldId); - if (typeof ref === 'undefined') { - $app.getWorldName(L.worldId).then((worldName) => { - if (L.tag === instanceId) { - if (L.instanceId) { - this.text = `${worldName} #${L.instanceName} ${L.accessTypeName}`; - } else { - this.text = worldName; - } - } - }); - } else if (L.instanceId) { - this.text = `${ref.name} #${L.instanceName} ${L.accessTypeName}`; - } else { - this.text = ref.name; - } - } - if (this.grouphint) { - this.groupName = this.grouphint; - } else if (L.groupId) { - this.groupName = L.groupId; - $app.getGroupName(instanceId).then((groupName) => { - if (L.tag === instanceId) { - this.groupName = groupName; - } - }); - } - this.region = ''; - if (!L.isOffline && !L.isPrivate && !L.isTraveling) { - this.region = L.region; - if (!L.region && L.instanceId) { - this.region = 'us'; - } - } - this.strict = L.strict; - }, - showWorldDialog() { - if (this.link) { - var instanceId = this.location; - if (this.traveling && this.location === 'traveling') { - instanceId = this.traveling; - } - if (!instanceId && this.hint.length === 8) { - // shortName - API.$emit('SHOW_WORLD_DIALOG_SHORTNAME', this.hint); - return; - } - API.$emit('SHOW_WORLD_DIALOG', instanceId); - } - }, - showGroupDialog() { - var location = this.location; - if (this.isTraveling) { - location = this.traveling; - } - if (!location || !this.link) { - return; - } - var L = API.parseLocation(location); - if (!L.groupId) { - return; - } - API.$emit('SHOW_GROUP_DIALOG', L.groupId); + 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); } - }, - watch: { - location() { - this.parse(); + } + for (var groupId of this.currentUserGroups.keys()) { + if (!groups.includes(groupId)) { + $app.onGroupLeft(groupId); } - }, - created() { - this.parse(); } - }); + }; - Vue.component('location-world', { - template: - '' + - '' + - '#{{ instanceName }} {{ accessTypeName }}' + - '({{ groupName }})' + - '' + - '', - props: { - locationobject: Object, - currentuserid: String, - worlddialogshortname: String, - grouphint: { - type: String, - default: '' - } - }, - data() { - return { - location: this.location, - instanceName: this.instanceName, - accessTypeName: this.accessTypeName, - region: this.region, - shortName: this.shortName, - isUnlocked: this.isUnlocked, - strict: this.strict, - groupName: this.groupName + var userUpdateQueue = []; + var userUpdateTimer = null; + var queueUserUpdate = function (ctx) { + userUpdateQueue.push(ctx); + if (userUpdateTimer !== null) { + return; + } + userUpdateTimer = workerTimers.setTimeout(() => { + userUpdateTimer = null; + var { length } = userUpdateQueue; + for (var i = 0; i < length; ++i) { + API.$emit('USER:UPDATE', userUpdateQueue[i]); + } + userUpdateQueue.length = 0; + }, 1); + }; + + API.applyUser = function (json) { + var ref = this.cachedUsers.get(json.id); + if (typeof json.statusDescription !== 'undefined') { + json.statusDescription = $app.replaceBioSymbols( + json.statusDescription + ); + json.statusDescription = $app.removeEmojis(json.statusDescription); + } + if (typeof json.bio !== 'undefined') { + json.bio = $app.replaceBioSymbols(json.bio); + } + if (typeof json.note !== 'undefined') { + json.note = $app.replaceBioSymbols(json.note); + } + if (json.currentAvatarImageUrl === $app.robotUrl) { + delete json.currentAvatarImageUrl; + delete json.currentAvatarThumbnailImageUrl; + } + if (typeof ref === 'undefined') { + ref = { + 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: '', + // + ...json }; - }, - methods: { - parse() { - this.location = this.locationobject.tag; - this.instanceName = this.locationobject.instanceName; - this.accessTypeName = this.locationobject.accessTypeName; - this.strict = this.locationobject.strict; - this.shortName = this.locationobject.shortName; - - this.isUnlocked = false; + if ($app.lastLocation.playerList.has(json.displayName)) { + // update $location_at from instance join time + var player = $app.lastLocation.playerList.get(json.displayName); + ref.$location_at = player.joinTime; + ref.$online_for = player.joinTime; + } + if (ref.location === 'traveling') { + ref.$location = $utils.parseLocation(ref.travelingToLocation); if ( - (this.worlddialogshortname && - this.locationobject.shortName && - this.worlddialogshortname === - this.locationobject.shortName) || - this.currentuserid === this.locationobject.userId + !this.currentTravelers.has(ref.id) && + ref.travelingToLocation ) { - this.isUnlocked = true; + var travelRef = { + created_at: new Date().toJSON(), + ...ref + }; + this.currentTravelers.set(ref.id, travelRef); + $app.sharedFeed.pendingUpdate = true; + $app.updateSharedFeed(false); + $app.onPlayerTraveling(travelRef); } - - this.region = this.locationobject.region; - if (!this.region) { - this.region = 'us'; + } else { + ref.$location = $utils.parseLocation(ref.location); + if (this.currentTravelers.has(ref.id)) { + this.currentTravelers.delete(ref.id); + $app.sharedFeed.pendingUpdate = true; + $app.updateSharedFeed(false); } - - this.groupName = ''; - if (this.grouphint) { - this.groupName = this.grouphint; - } else if (this.locationobject.groupId) { - this.groupName = this.locationobject.groupId; - $app.getGroupName(this.locationobject.groupId).then( - (groupName) => { - this.groupName = groupName; - } - ); + } + 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; } - }, - showLaunchDialog() { - API.$emit('SHOW_LAUNCH_DIALOG', this.location, this.shortName); - }, - showGroupDialog() { - if (!this.location) { - return; + } + 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 = $utils.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); } - var L = API.parseLocation(this.location); - if (!L.groupId) { - return; + } else { + ref.$location = $utils.parseLocation(ref.location); + if (this.currentTravelers.has(ref.id)) { + this.currentTravelers.delete(ref.id); + $app.sharedFeed.pendingUpdate = true; + $app.updateSharedFeed(false); } - API.$emit('SHOW_GROUP_DIALOG', L.groupId); - } - }, - watch: { - locationobject() { - this.parse(); } - }, - created() { - this.parse(); - } - }); - - Vue.component('last-join', { - template: - '' + - '' + - '
' + - '{{ $t("dialog.user.info.last_join") }} ' + - '
' + - '' + - '
' + - '
', - props: { - location: String, - currentlocation: String - }, - data() { - return { - lastJoin: this.lastJoin - }; - }, - methods: { - parse() { - this.lastJoin = $app.instanceJoinHistory.get(this.location); + 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; + } } - }, - watch: { - location() { - this.parse(); - }, - currentlocation() { - this.parse(); + 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]; + } } - }, - created() { - this.parse(); - } - }); - - Vue.component('instance-info', { - template: - '
' + - '' + - '
' + - '' + - '' + - 'PC: {{ platforms.standalonewindows }}
' + - 'Android: {{ platforms.android }}
' + - '{{ $t("dialog.user.info.instance_game_version") }} {{ gameServerVersion }}
' + - '{{ $t("dialog.user.info.instance_queuing_enabled") }}
' + - '{{ $t("dialog.user.info.instance_users") }}
' + - '' + - '
' + - '' + - '
' + - '{{ occupants }}/{{ capacity }}' + - '({{ friendcount }})' + - '{{ $t("dialog.user.info.instance_full") }}' + - '{{ $t("dialog.user.info.instance_hard_closed") }}' + - '{{ $t("dialog.user.info.instance_closed") }}' + - '{{ $t("dialog.user.info.instance_queue") }} {{ queueSize }}' + - '
', - props: { - location: String, - instance: Object, - friendcount: Number, - updateelement: Number - }, - data() { - return { - isValidInstance: this.isValidInstance, - isFull: this.isFull, - isClosed: this.isClosed, - isHardClosed: this.isHardClosed, - closedAt: this.closedAt, - occupants: this.occupants, - capacity: this.capacity, - queueSize: this.queueSize, - queueEnabled: this.queueEnabled, - platforms: this.platforms, - userList: this.userList, - gameServerVersion: this.gameServerVersion, - canCloseInstance: this.canCloseInstance - }; - }, - methods: { - parse() { - this.isValidInstance = false; - this.isFull = false; - this.isClosed = false; - this.isHardClosed = false; - this.closedAt = ''; - this.occupants = 0; - this.capacity = 0; - this.queueSize = 0; - this.queueEnabled = false; - this.platforms = []; - this.userList = []; - this.gameServerVersion = ''; - this.canCloseInstance = false; - if ( - !this.location || - !this.instance || - Object.keys(this.instance).length === 0 - ) { - return; + // 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; } - this.isValidInstance = true; - this.isFull = - typeof this.instance.hasCapacityForYou !== 'undefined' && - !this.instance.hasCapacityForYou; - if (this.instance.closedAt) { - this.isClosed = true; - this.closedAt = this.instance.closedAt; - } - this.isHardClosed = this.instance.hardClose === true; - this.occupants = this.instance.userCount; - if (this.location === $app.lastLocation.location) { - // use gameLog for occupants when in same location - this.occupants = $app.lastLocation.playerList.size; - } - this.capacity = this.instance.capacity; - this.gameServerVersion = this.instance.gameServerVersion; - this.queueSize = this.instance.queueSize; - if (this.instance.platforms) { - this.platforms = this.instance.platforms; - } - if (this.instance.users) { - this.userList = this.instance.users; - } - if (this.instance.ownerId === API.currentUser.id) { - this.canCloseInstance = true; - } else if (this.instance?.ownerId?.startsWith('grp_')) { - // check group perms - var groupId = this.instance.ownerId; - var group = API.cachedGroups.get(groupId); - this.canCloseInstance = $app.hasGroupPermission( - group, - 'group-instance-moderate' - ); + queueUserUpdate({ + ref, + props + }); + if ($app.debugUserDiff) { + delete props.last_login; + delete props.last_activity; + if (Object.keys(props).length !== 0) { + console.log('>', ref.displayName, props); + } } - }, - showUserDialog(userId) { - API.$emit('SHOW_USER_DIALOG', userId); } - }, - watch: { - updateelement() { - this.parse(); - }, - location() { - this.parse(); - }, - friendcount() { - this.parse(); + } + if (ref.id === this.currentUser.id) { + if (ref.status) { + this.currentUser.status = ref.status; } - }, - created() { - this.parse(); + $app.updateCurrentUserLocation(); } - }); + this.$emit('USER:APPLY', ref); + return ref; + }; - Vue.component('avatar-info', { - template: - '
' + - '{{ avatarName }}' + - '{{ avatarType }}' + - '{{ avatarTags }}' + - '
', - props: { - imageurl: String, - userid: String, - hintownerid: String, - hintavatarname: String, - avatartags: Array - }, - data() { - return { - avatarName: this.avatarName, - avatarType: this.avatarType, - avatarTags: this.avatarTags, - color: this.color - }; - }, - methods: { - async parse() { - this.ownerId = ''; - this.avatarName = ''; - this.avatarType = ''; - this.color = ''; - this.avatarTags = ''; - if (!this.imageurl) { - this.avatarName = '-'; - } else if (this.hintownerid) { - this.avatarName = this.hintavatarname; - this.ownerId = this.hintownerid; - } else { - try { - var avatarInfo = await $app.getAvatarName( - this.imageurl - ); - this.avatarName = avatarInfo.avatarName; - this.ownerId = avatarInfo.ownerId; - } catch (err) {} - } - if (typeof this.userid === 'undefined' || !this.ownerId) { - this.color = ''; - this.avatarType = ''; - } else if (this.ownerId === this.userid) { - this.color = 'avatar-info-own'; - this.avatarType = '(own)'; - } else { - this.color = 'avatar-info-public'; - this.avatarType = '(public)'; - } - if (typeof this.avatartags === 'object') { - var tagString = ''; - for (var i = 0; i < this.avatartags.length; i++) { - var tagName = this.avatartags[i].replace( - 'content_', - '' - ); - tagString += tagName; - if (i < this.avatartags.length - 1) { - tagString += ', '; - } - } - this.avatarTags = tagString; - } - }, - confirm() { - if (!this.imageurl) { - return; - } - $app.showAvatarAuthorDialog( - this.userid, - this.ownerId, - this.imageurl - ); - } - }, - watch: { - imageurl() { - this.parse(); - }, - userid() { - this.parse(); - }, - avatartags() { - this.parse(); - } - }, - mounted() { - this.parse(); - } - }); - - Vue.component('display-name', { - template: - '{{ username }}', - props: { - userid: String, - location: String, - key: Number - }, - data() { - return { - username: this.username + /** + * Fetch user from API. + * @param {{ userId: string }} params identifier of registered user + * @returns {Promise<{json: any, params}>} + */ + API.getUser = function (params) { + return this.call(`users/${params.userId}`, { + method: 'GET' + }).then((json) => { + var args = { + json, + params }; - }, - methods: { - async parse() { - this.username = this.userid; - if (this.userid) { - var args = await API.getCachedUser({ userId: this.userid }); - } - if ( - typeof args !== 'undefined' && - typeof args.json !== 'undefined' && - typeof args.json.displayName !== 'undefined' - ) { - this.username = args.json.displayName; - } - }, - showUserDialog() { - $app.showUserDialog(this.userid); - } - }, - watch: { - location() { - this.parse(); - }, - key() { - this.parse(); - } - }, - mounted() { - this.parse(); - } - }); - - // #endregion - // #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.isLoggedIn = false; - API.cachedUsers = new Map(); - API.currentUser = { - $userColour: '' + this.$emit('USER', args); + return args; + }); }; - API.currentTravelers = new Map(); - - API.$on('USER:CURRENT', function (args) { - var { json } = args; - args.ref = this.applyCurrentUser(json); - // when isGameRunning use gameLog instead of API - var $location = this.parseLocation($app.lastLocation.location); - var $travelingLocation = this.parseLocation( - $app.lastLocationDestination - ); - var location = $app.lastLocation.location; - var instanceId = $location.instanceId; - var worldId = $location.worldId; - var travelingToLocation = $app.lastLocationDestination; - var travelingToWorld = $travelingLocation.worldId; - var travelingToInstance = $travelingLocation.instanceId; - if (!$app.isGameRunning && json.presence) { - if ($app.isRealInstance(json.presence.world)) { - location = `${json.presence.world}:${json.presence.instance}`; - travelingToLocation = `${json.presence.travelingToWorld}:${json.presence.travelingToInstance}`; + /** + * Fetch user from cache if they're in it. Otherwise, calls API. + * @param {{ userId: string }} params identifier of registered user + * @returns {Promise<{json: any, params}>} + */ + API.getCachedUser = function (params) { + return new Promise((resolve, reject) => { + var ref = this.cachedUsers.get(params.userId); + if (typeof ref === 'undefined') { + this.getUser(params).catch(reject).then(resolve); } else { - location = json.presence.world; - travelingToLocation = json.presence.travelingToWorld; - } - instanceId = json.presence.instance; - worldId = json.presence.world; - travelingToInstance = json.presence.travelingToInstance; - travelingToWorld = json.presence.travelingToWorld; - } - - this.applyUser({ - allowAvatarCopying: json.allowAvatarCopying, - badges: json.badges, - bio: json.bio, - bioLinks: json.bioLinks, - currentAvatarImageUrl: json.currentAvatarImageUrl, - currentAvatarTags: json.currentAvatarTags, - currentAvatarThumbnailImageUrl: json.currentAvatarThumbnailImageUrl, - date_joined: json.date_joined, - developerType: json.developerType, - displayName: json.displayName, - friendKey: json.friendKey, - // json.friendRequestStatus - missing from currentUser - id: json.id, - // instanceId - missing from currentUser - isFriend: json.isFriend, - last_activity: json.last_activity, - last_login: json.last_login, - last_mobile: json.last_mobile, - last_platform: json.last_platform, - // location - missing from currentUser - // platform - missing from currentUser - // note - missing from currentUser - profilePicOverride: json.profilePicOverride, - // profilePicOverrideThumbnail - missing from currentUser - pronouns: json.pronouns, - state: json.state, - status: json.status, - statusDescription: json.statusDescription, - tags: json.tags, - // travelingToInstance - missing from currentUser - // travelingToLocation - missing from currentUser - // travelingToWorld - missing from currentUser - userIcon: json.userIcon, - // worldId - missing from currentUser - fallbackAvatar: json.fallbackAvatar, - - // Location from gameLog/presence - location, - instanceId, - worldId, - travelingToLocation, - travelingToInstance, - travelingToWorld, - - // set VRCX online/offline timers - $online_for: this.currentUser.$online_for, - $offline_for: this.currentUser.$offline_for, - $location_at: this.currentUser.$location_at, - $travelingToTime: this.currentUser.$travelingToTime + resolve({ + cache: true, + json: ref, + params, + ref + }); + } }); - }); - - API.$on('USER:CURRENT:SAVE', function (args) { - this.$emit('USER:CURRENT', args); - }); - - API.$on('USER', function (args) { - if (!args?.json?.displayName) { - console.error('API.$on(USER) invalid args', args); - return; - } - $app.updateFriend({ id: args.json.id, state: args.json.state }); - args.ref = this.applyUser(args.json); - }); - - API.$on('USER:LIST', function (args) { - for (var json of args.json) { - this.$emit('USER', { - json, - params: { - userId: json.id - } - }); - } - }); - - API.logout = function () { - this.$emit('LOGOUT'); - // return this.call('logout', { - // method: 'PUT' - // }).finally(() => { - // this.$emit('LOGOUT'); - // }); }; + /** @typedef {{ + * n: number, + * offset: number, + * search: string, + * sort: 'nuisanceFactor' | 'created' | '_created_at' | 'last_login', + * order: 'ascending', 'descending' + }} GetUsersParameters */ /** - * @param {{ username: string, password: string }} params credential to login - * @returns {Promise<{origin: boolean, json: any, params}>} + * Fetch multiple users from API. + * @param params {GetUsersParameters} filtering and sorting parameters + * @returns {Promise<{json: any, params}>} */ - API.login = function (params) { - var { username, password, saveCredentials, cipher } = params; - username = encodeURIComponent(username); - password = encodeURIComponent(password); - var auth = btoa(`${username}:${password}`); - if (saveCredentials) { - delete params.saveCredentials; - if (cipher) { - params.password = cipher; - delete params.cipher; - } - $app.saveCredentials = params; - } - return this.call('auth/user', { + API.getUsers = function (params) { + return this.call('users', { method: 'GET', - headers: { - Authorization: `Basic ${auth}` - } + params }).then((json) => { var args = { json, - params, - origin: true + params }; - if ( - json.requiresTwoFactorAuth && - json.requiresTwoFactorAuth.includes('emailOtp') - ) { - this.$emit('USER:EMAILOTP', args); - } else if (json.requiresTwoFactorAuth) { - this.$emit('USER:2FA', args); - } else { - this.$emit('USER:CURRENT', args); - } + this.$emit('USER:LIST', args); return args; }); }; /** - * @param {{ code: string }} params One-time password + * @param params {string[]} * @returns {Promise<{json: any, params}>} */ - API.verifyOTP = function (params) { - return this.call('auth/twofactorauth/otp/verify', { + API.addUserTags = function (params) { + return this.call(`users/${this.currentUser.id}/addTags`, { method: 'POST', params }).then((json) => { @@ -1633,17 +762,17 @@ speechSynthesis.getVoices(); json, params }; - this.$emit('OTP', args); + this.$emit('USER:CURRENT:SAVE', args); return args; }); }; /** - * @param {{ code: string }} params One-time token + * @param params {string[]} * @returns {Promise<{json: any, params}>} */ - API.verifyTOTP = function (params) { - return this.call('auth/twofactorauth/totp/verify', { + API.removeUserTags = function (params) { + return this.call(`users/${this.currentUser.id}/removeTags`, { method: 'POST', params }).then((json) => { @@ -1651,656 +780,376 @@ speechSynthesis.getVoices(); json, params }; - this.$emit('TOTP', args); + this.$emit('USER:CURRENT:SAVE', args); return args; }); }; /** - * @param {{ code: string }} params One-time token + * @param params {{ userId: string }} * @returns {Promise<{json: any, params}>} */ - API.verifyEmailOTP = function (params) { - return this.call('auth/twofactorauth/emailotp/verify', { - method: 'POST', - params + API.getUserFeedback = function (params) { + return this.call(`users/${params.userId}/feedback`, { + method: 'GET', + params: { + n: 100 + } }).then((json) => { var args = { json, params }; - this.$emit('EMAILOTP', args); + this.$emit('USER:FEEDBACK', args); return args; }); }; - 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; + // #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 + } + }); } - if (tags.includes('system_troll')) { - ref.$isTroll = true; + }); + + 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; } - if (tags.includes('system_probable_troll') && !ref.$isTroll) { - ref.$isProbableTroll = true; + }); + + 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 (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]; + if (gameLogLocation === 'traveling') { + gameLogLocation = $app.lastLocationDestination; } - }; - // FIXME: it may performance issue. review here - API.applyUserLanguage = function (ref) { - ref.$languages = []; - var { tags } = ref; - for (var tag of tags) { - if (tag.startsWith('language_') === false) { - continue; - } - var key = tag.substr(9); - var value = $app.subsetOfLanguages[key]; - if (typeof value === 'undefined') { - continue; - } - ref.$languages.push({ - key, - value - }); + let presenceLocation = this.currentUser.$locationTag; + if (presenceLocation === 'traveling') { + presenceLocation = this.currentUser.$travelingToLocation; } - }; - API.applyPresenceLocation = function (ref) { - var presence = ref.presence; - if ($app.isRealInstance(presence.world)) { - ref.$locationTag = `${presence.world}:${presence.instance}`; - } else { - ref.$locationTag = presence.world; - } - if ($app.isRealInstance(presence.travelingToWorld)) { - ref.$travelingToLocation = `${presence.travelingToWorld}:${presence.travelingToInstance}`; - } else { - ref.$travelingToLocation = presence.travelingToWorld; + // 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 = $utils.parseLocation(presenceLocation); + return L.worldId; } - $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; + const args = await this.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 + ); - // update group list - for (var groupId of groups) { - if (!this.currentUserGroups.has(groupId)) { - $app.onGroupJoined(groupId); - } + if ($app.isRealInstance(userLocation)) { + console.warn('PWI: returning user location', userLocation); + const L = $utils.parseLocation(userLocation); + return L.worldId; } - for (var groupId of this.currentUserGroups.keys()) { - if (!groups.includes(groupId)) { - $app.onGroupLeft(groupId); - } + + if ($app.isRealInstance(gameLogLocation)) { + console.warn(`PWI: returning gamelog location: `, gameLogLocation); + const L = $utils.parseLocation(gameLogLocation); + return L.worldId; } + + console.error( + `PWI: all locations invalid: `, + gameLogLocation, + userLocation + ); + return 'test_world'; }; - API.applyCurrentUser = function (json) { - var ref = this.currentUser; - if (this.isLoggedIn) { - if (json.currentAvatar !== ref.currentAvatar) { - $app.addAvatarToHistory(json.currentAvatar); - } - Object.assign(ref, json); - if (ref.homeLocation !== ref.$homeLocation.tag) { - ref.$homeLocation = this.parseLocation(ref.homeLocation); - // apply home location name to user dialog - if ($app.userDialog.visible && $app.userDialog.id === ref.id) { - $app.getWorldName(API.currentUser.homeLocation).then( - (worldName) => { - $app.userDialog.$homeLocationName = worldName; - } - ); - } - } - ref.$isVRCPlus = ref.tags.includes('system_supporter'); - this.applyUserTrustLevel(ref); - this.applyUserLanguage(ref); - this.applyPresenceLocation(ref); - this.applyQueuedInstance(ref.queuedInstance); - this.applyPresenceGroups(ref); - } else { + API.applyWorld = function (json) { + var ref = this.cachedWorlds.get(json.id); + if (typeof ref === 'undefined') { ref = { - acceptedPrivacyVersion: 0, - acceptedTOSVersion: 0, - accountDeletionDate: null, - accountDeletionLog: null, - activeFriends: [], - allowAvatarCopying: false, - badges: [], - bio: '', - bioLinks: [], - currentAvatar: '', - currentAvatarAssetUrl: '', - currentAvatarImageUrl: '', - currentAvatarTags: [], - currentAvatarThumbnailImageUrl: '', - date_joined: '', - developerType: '', - displayName: '', - emailVerified: false, - fallbackAvatar: '', - friendGroupNames: [], - friendKey: '', - friends: [], - googleId: '', - hasBirthday: false, - hasEmail: false, - hasLoggedInFromClient: false, - hasPendingEmail: false, - hideContentFilterSettings: false, - homeLocation: '', id: '', - isBoopingEnabled: false, - isFriend: false, - last_activity: '', - last_login: '', - last_mobile: null, - last_platform: '', - obfuscatedEmail: '', - obfuscatedPendingEmail: '', - oculusId: '', - offlineFriends: [], - onlineFriends: [], - pastDisplayNames: [], - picoId: '', - presence: { - avatarThumbnail: '', - currentAvatarTags: '', - displayName: '', - groups: [], - id: '', - instance: '', - instanceType: '', - platform: '', - profilePicOverride: '', - status: '', - travelingToInstance: '', - travelingToWorld: '', - userIcon: '', - world: '', - ...json.presence - }, - profilePicOverride: '', - pronouns: '', - queuedInstance: '', - state: '', - status: '', - statusDescription: '', - statusFirstTime: false, - statusHistory: [], - steamDetails: {}, - steamId: '', + name: '', + description: '', + authorId: '', + authorName: '', + capacity: 0, + recommendedCapacity: 0, tags: [], - twoFactorAuthEnabled: false, - twoFactorAuthEnabledDate: null, - unsubscribe: false, + releaseStatus: '', + imageUrl: '', + thumbnailImageUrl: '', + assetUrl: '', + assetUrlObject: {}, + pluginUrl: '', + pluginUrlObject: {}, + unityPackageUrl: '', + unityPackageUrlObject: {}, + unityPackages: [], + version: 0, + favorites: 0, + created_at: '', updated_at: '', - userIcon: '', - userLanguage: '', - userLanguageCode: '', - username: '', - viveId: '', + publicationDate: '', + labsPublicationDate: '', + visits: 0, + popularity: 0, + heat: 0, + publicOccupants: 0, + privateOccupants: 0, + occupants: 0, + instances: [], + featured: false, + organization: '', + previewYoutubeId: '', // VRCX - $online_for: Date.now(), - $offline_for: '', - $location_at: Date.now(), - $travelingToTime: Date.now(), - $homeLocation: {}, - $isVRCPlus: false, - $isModerator: false, - $isTroll: false, - $isProbableTroll: false, - $trustLevel: 'Visitor', - $trustClass: 'x-tag-untrusted', - $userColour: '', - $trustSortNum: 1, - $languages: [], - $locationTag: '', - $travelingToLocation: '', - $vbucks: null, + $isLabs: false, + // ...json }; - ref.$homeLocation = this.parseLocation(ref.homeLocation); - ref.$isVRCPlus = ref.tags.includes('system_supporter'); - this.applyUserTrustLevel(ref); - this.applyUserLanguage(ref); - this.applyPresenceLocation(ref); - this.applyPresenceGroups(ref); - this.currentUser = ref; - this.isLoggedIn = true; - this.$emit('LOGIN', { - json, - ref - }); + this.cachedWorlds.set(ref.id, ref); + } else { + Object.assign(ref, json); } + ref.$isLabs = ref.tags.includes('system_labs'); + ref.name = $app.replaceBioSymbols(ref.name); + ref.description = $app.replaceBioSymbols(ref.description); return ref; }; - API.getCurrentUser = function () { - $app.nextCurrentUserRefresh = 840; // 7mins - return this.call('auth/user', { + /** + * + * @param {{worldId: string}} params + * @returns {Promise<{json: any, params}>} + */ + API.getWorld = function (params) { + return this.call(`worlds/${params.worldId}`, { method: 'GET' }).then((json) => { var args = { json, - origin: true + params }; - if ( - json.requiresTwoFactorAuth && - json.requiresTwoFactorAuth.includes('emailOtp') - ) { - this.$emit('USER:EMAILOTP', args); - } else if (json.requiresTwoFactorAuth) { - this.$emit('USER:2FA', args); - } else { - this.$emit('USER:CURRENT', args); - } + this.$emit('WORLD', args); return args; }); }; - var userUpdateQueue = []; - var userUpdateTimer = null; - var queueUserUpdate = function (ctx) { - userUpdateQueue.push(ctx); - if (userUpdateTimer !== null) { - return; - } - userUpdateTimer = workerTimers.setTimeout(() => { - userUpdateTimer = null; - var { length } = userUpdateQueue; - for (var i = 0; i < length; ++i) { - API.$emit('USER:UPDATE', userUpdateQueue[i]); + /** + * @param {{worldId: string}} params + * @returns {Promise<{json: any, params}>} + */ + API.getCachedWorld = function (params) { + return new Promise((resolve, reject) => { + var ref = this.cachedWorlds.get(params.worldId); + if (typeof ref === 'undefined') { + this.getWorld(params).catch(reject).then(resolve); + } else { + resolve({ + cache: true, + json: ref, + params, + ref + }); } - userUpdateQueue.length = 0; - }, 1); + }); }; - API.applyUser = function (json) { - var ref = this.cachedUsers.get(json.id); - if (typeof json.statusDescription !== 'undefined') { - json.statusDescription = $app.replaceBioSymbols( - json.statusDescription - ); - json.statusDescription = $app.removeEmojis(json.statusDescription); - } - if (typeof json.bio !== 'undefined') { - json.bio = $app.replaceBioSymbols(json.bio); - } - if (typeof json.note !== 'undefined') { - json.note = $app.replaceBioSymbols(json.note); - } - if (json.currentAvatarImageUrl === $app.robotUrl) { - delete json.currentAvatarImageUrl; - delete json.currentAvatarThumbnailImageUrl; + /** + * @typedef {{ + n: number, + offset: number, + search: string, + userId: string, + user: 'me' | 'friend', + sort: 'popularity' | 'heat' | 'trust' | 'shuffle' | 'favorites' | 'reportScore' | 'reportCount' | 'publicationDate' | 'labsPublicationDate' | 'created' | '_created_at' | 'updated' | '_updated_at' | 'order', + order: 'ascending' | 'descending', + releaseStatus: 'public' | 'private' | 'hidden' | 'all', + featured: boolean + }} WorldSearchParameter + */ + /** + * + * @param {WorldSearchParameter} params + * @param {string?} option sub-path of calling endpoint + * @returns {Promise<{json: any, params, option}>} + */ + API.getWorlds = function (params, option) { + var endpoint = 'worlds'; + if (typeof option !== 'undefined') { + endpoint = `worlds/${option}`; } - if (typeof ref === 'undefined') { - ref = { - 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: '', - // - ...json + return this.call(endpoint, { + method: 'GET', + params + }).then((json) => { + var args = { + json, + params, + option }; - if ($app.lastLocation.playerList.has(json.displayName)) { - // update $location_at from instance join time - var player = $app.lastLocation.playerList.get(json.displayName); - ref.$location_at = player.joinTime; - ref.$online_for = player.joinTime; - } - if (ref.location === 'traveling') { - ref.$location = this.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 = this.parseLocation(ref.location); - if (this.currentTravelers.has(ref.id)) { - this.currentTravelers.delete(ref.id); - $app.sharedFeed.pendingUpdate = true; - $app.updateSharedFeed(false); - } - } - 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 = this.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 = this.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 (!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; - } - queueUserUpdate({ - 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.id === this.currentUser.id) { - if (ref.status) { - this.currentUser.status = ref.status; - } - $app.updateCurrentUserLocation(); - } - this.$emit('USER:APPLY', ref); - return ref; + this.$emit('WORLD:LIST', args); + return args; + }); }; /** - * Fetch user from API. - * @param {{ userId: string }} params identifier of registered user + * @param {{worldId: string}} params * @returns {Promise<{json: any, params}>} */ - API.getUser = function (params) { - return this.call(`users/${params.userId}`, { - method: 'GET' + API.deleteWorld = function (params) { + return this.call(`worlds/${params.worldId}`, { + method: 'DELETE' }).then((json) => { var args = { json, params }; - this.$emit('USER', args); + this.$emit('WORLD:DELETE', args); return args; }); }; /** - * Fetch user from cache if they're in it. Otherwise, calls API. - * @param {{ userId: string }} params identifier of registered user + * @param {{id: string}} params * @returns {Promise<{json: any, params}>} */ - API.getCachedUser = function (params) { - return new Promise((resolve, reject) => { - var ref = this.cachedUsers.get(params.userId); - if (typeof ref === 'undefined') { - this.getUser(params).catch(reject).then(resolve); - } else { - resolve({ - cache: true, - json: ref, - params, - ref - }); - } - }); + API.saveWorld = function (params) { + return this.call(`worlds/${params.id}`, { + method: 'PUT', + params + }).then((json) => { + var args = { + json, + params + }; + this.$emit('WORLD:SAVE', args); + return args; + }); }; - /** @typedef {{ - * n: number, - * offset: number, - * search: string, - * sort: 'nuisanceFactor' | 'created' | '_created_at' | 'last_login', - * order: 'ascending', 'descending' - }} GetUsersParameters */ /** - * Fetch multiple users from API. - * @param params {GetUsersParameters} filtering and sorting parameters + * @param {{worldId: string}} params * @returns {Promise<{json: any, params}>} */ - API.getUsers = function (params) { - return this.call('users', { - method: 'GET', + API.publishWorld = function (params) { + return this.call(`worlds/${params.worldId}/publish`, { + method: 'PUT', params }).then((json) => { var args = { json, params }; - this.$emit('USER:LIST', args); + this.$emit('WORLD:SAVE', args); return args; }); }; /** - * @typedef {{ - * status: 'active' | 'offline' | 'busy' | 'ask me' | 'join me', - * statusDescription: string - * }} SaveCurrentUserParameters - */ - - /** - * Updates current user's status. - * @param params {SaveCurrentUserParameters} new status to be set + * @param {{worldId: string}} params * @returns {Promise<{json: any, params}>} */ - API.saveCurrentUser = function (params) { - return this.call(`users/${this.currentUser.id}`, { - method: 'PUT', + API.unpublishWorld = function (params) { + return this.call(`worlds/${params.worldId}/publish`, { + method: 'DELETE', params }).then((json) => { var args = { json, params }; - this.$emit('USER:CURRENT:SAVE', args); + this.$emit('WORLD:SAVE', args); return args; }); }; + // #endregion + // #region | API: Instance + + API.cachedInstances = new Map(); + /** - * @param params {string[]} + * @param {{worldId: string, instanceId: string}} params * @returns {Promise<{json: any, params}>} */ - API.addUserTags = function (params) { - return this.call(`users/${this.currentUser.id}/addTags`, { - method: 'POST', - params + API.getInstance = function (params) { + return this.call(`instances/${params.worldId}:${params.instanceId}`, { + method: 'GET' }).then((json) => { var args = { json, params }; - this.$emit('USER:CURRENT:SAVE', args); + this.$emit('INSTANCE', args); return args; }); }; /** - * @param params {string[]} + * @typedef {{ + * worldId: string, + * type: string, + * region: string, + * ownerId: string, + * roleIds: string[], + * groupAccessType: string, + * queueEnabled: boolean + * }} CreateInstanceParameter + */ + + /** + * @param {CreateInstanceParameter} params * @returns {Promise<{json: any, params}>} */ - API.removeUserTags = function (params) { - return this.call(`users/${this.currentUser.id}/removeTags`, { + API.createInstance = function (params) { + return this.call('instances', { method: 'POST', params }).then((json) => { @@ -2308,1114 +1157,737 @@ speechSynthesis.getVoices(); json, params }; - this.$emit('USER:CURRENT:SAVE', args); + this.$emit('INSTANCE', args); return args; }); }; /** - * @param params {{ userId: string }} - * @returns {Promise<{json: any, params}>} + * @param {{ worldId: string, instanceId: string, shortName: string }} instance + * @returns {Promise<{instance, json: T, params: {}}>} */ - API.getUserFeedback = function (params) { - return this.call(`users/${params.userId}/feedback`, { - method: 'GET', - params: { - n: 100 + API.getInstanceShortName = function (instance) { + var params = {}; + if (instance.shortName) { + params.shortName = instance.shortName; + } + return this.call( + `instances/${instance.worldId}:${instance.instanceId}/shortName`, + { + method: 'GET', + params } - }).then((json) => { + ).then((json) => { var args = { json, + instance, params }; - this.$emit('USER:FEEDBACK', args); + this.$emit('INSTANCE:SHORTNAME', args); return args; }); }; - // #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', { + /** + * @param {{ shortName: string }} params + * @returns {Promise<{json: any, params}>} + */ + API.getInstanceFromShortName = function (params) { + return this.call(`instances/s/${params.shortName}`, { + method: 'GET' + }).then((json) => { + var args = { 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 - } + params + }; + this.$emit('INSTANCE', args); + return args; }); - }); - - 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 = this.parseLocation(presenceLocation); - return L.worldId; - } - - const args = await this.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 ($app.isRealInstance(userLocation)) { - console.warn('PWI: returning user location', userLocation); - const L = this.parseLocation(userLocation); - return L.worldId; - } - - if ($app.isRealInstance(gameLogLocation)) { - console.warn(`PWI: returning gamelog location: `, gameLogLocation); - const L = this.parseLocation(gameLogLocation); - return L.worldId; + /** + * Send invite to current user. + * @param {{ worldId: string, instanceId: string, shortName: string }} instance + * @returns {Promise<{instance, json: any, params}>} + */ + API.selfInvite = function (instance) { + /** + * @type {{ shortName?: string }} + */ + var params = {}; + if (instance.shortName) { + params.shortName = instance.shortName; } - - console.error( - `PWI: all locations invalid: `, - gameLogLocation, - userLocation - ); - return 'test_world'; + return this.call( + `invite/myself/to/${instance.worldId}:${instance.instanceId}`, + { + method: 'POST', + params + } + ) + .then((json) => { + var args = { + json, + instance, + params + }; + return args; + }) + .catch((err) => { + if (err?.error?.message) { + $app.$message({ + message: err.error.message, + type: 'error' + }); + throw err; + } + $app.$message({ + message: "you're not allowed to access this instance.", + type: 'error' + }); + throw err; + }); }; - API.applyWorld = function (json) { - var ref = this.cachedWorlds.get(json.id); + API.applyInstance = function (json) { + var ref = this.cachedInstances.get(json.id); if (typeof ref === 'undefined') { ref = { id: '', + location: '', + instanceId: '', name: '', - description: '', - authorId: '', - authorName: '', + worldId: '', + type: '', + ownerId: '', + tags: [], + active: false, + full: false, + n_users: 0, + hasCapacityForYou: true, // not present depending on endpoint 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: '', + 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: '', + 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, // VRCX - $isLabs: false, - // + $fetchedAt: '', ...json }; - this.cachedWorlds.set(ref.id, ref); + this.cachedInstances.set(ref.id, ref); } else { Object.assign(ref, json); } - ref.$isLabs = ref.tags.includes('system_labs'); - ref.name = $app.replaceBioSymbols(ref.name); - ref.description = $app.replaceBioSymbols(ref.description); + ref.$location = $utils.parseLocation(ref.location); + if (json.world?.id) { + this.getCachedWorld({ + worldId: json.world.id + }).then((args) => { + ref.world = args.ref; + return args; + }); + } + if (!json.$fetchedAt) { + ref.$fetchedAt = new Date().toJSON(); + } return ref; }; - /** - * - * @param {{worldId: string}} params - * @returns {Promise<{json: any, params}>} - */ - API.getWorld = function (params) { - return this.call(`worlds/${params.worldId}`, { - method: 'GET' - }).then((json) => { - var args = { + 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(); + } + // 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) { + this.$emit('USER', { json, - params - }; - this.$emit('WORLD', args); - return args; - }); - }; + params: { + userId: json.id + } + }); - /** - * @param {{worldId: string}} params - * @returns {Promise<{json: any, params}>} - */ - API.getCachedWorld = function (params) { - return new Promise((resolve, reject) => { - var ref = this.cachedWorlds.get(params.worldId); - if (typeof ref === 'undefined') { - this.getWorld(params).catch(reject).then(resolve); - } else { - resolve({ - cache: true, - json: ref, - params, - ref + var state = 'offline'; + if (json.platform === 'web') { + state = 'active'; + } else if (json.platform) { + state = 'online'; + } + var ref = $app.friends.get(json.id); + if (ref?.state !== state) { + if ($app.debugFriendState) { + console.log( + `Bulk friend fetch, friend state does not match ${json.displayName} from ${ref?.state} to ${state}` + ); + } + this.getUser({ + userId: json.id + }); + } else if (json.location === 'traveling') { + if ($app.debugFriendState) { + console.log( + 'Bulk friend fetch, fetching traveling user', + json + ); + } + this.getUser({ + userId: json.id }); } - }); - }; - /** - * @typedef {{ - n: number, - offset: number, - search: string, - userId: string, - user: 'me' | 'friend', - sort: 'popularity' | 'heat' | 'trust' | 'shuffle' | 'favorites' | 'reportScore' | 'reportCount' | 'publicationDate' | 'labsPublicationDate' | 'created' | '_created_at' | 'updated' | '_updated_at' | 'order', - order: 'ascending' | 'descending', - releaseStatus: 'public' | 'private' | 'hidden' | 'all', - featured: boolean - }} WorldSearchParameter - */ - /** - * - * @param {WorldSearchParameter} params - * @param {string?} option sub-path of calling endpoint - * @returns {Promise<{json: any, params, option}>} - */ - API.getWorlds = function (params, option) { - var endpoint = 'worlds'; - if (typeof option !== 'undefined') { - endpoint = `worlds/${option}`; + // if ( + // !args.params.offline && + // json.platform !== 'web' && + // json.location === 'offline' + // ) { + // console.log('Fetching offline user', json); + // this.getUser({ + // userId: json.id + // }); + // } } - return this.call(endpoint, { - method: 'GET', - params - }).then((json) => { - var args = { - json, - params, - option - }; - this.$emit('WORLD:LIST', args); - return args; - }); - }; + }); - /** - * @param {{worldId: string}} params - * @returns {Promise<{json: any, params}>} - */ - API.deleteWorld = function (params) { - return this.call(`worlds/${params.worldId}`, { - method: 'DELETE' - }).then((json) => { - var args = { - json, - params - }; - this.$emit('WORLD:DELETE', args); - return args; - }); - }; + API.isRefreshFriendsLoading = false; - /** - * @param {{id: string}} params - * @returns {Promise<{json: any, params}>} - */ - API.saveWorld = function (params) { - return this.call(`worlds/${params.id}`, { - method: 'PUT', - params - }).then((json) => { - var args = { - json, - params - }; - this.$emit('WORLD:SAVE', args); - return args; - }); + API.refreshFriends = async function () { + this.isRefreshFriendsLoading = true; + try { + var onlineFriends = await this.refreshOnlineFriends(); + var offlineFriends = await this.refreshOfflineFriends(); + var friends = onlineFriends.concat(offlineFriends); + this.isRefreshFriendsLoading = false; + return friends; + } catch (err) { + this.isRefreshFriendsLoading = false; + throw err; + } + }; + + API.refreshOnlineFriends = async function () { + var friends = []; + var params = { + n: 50, + offset: 0, + offline: false + }; + var N = + this.currentUser.onlineFriends.length + + this.currentUser.activeFriends.length; + var count = Math.trunc(N / 50); + mainLoop: for (var i = count; i > -1; i--) { + if (params.offset > 5000) { + // API offset limit is 5000 + break; + } + retryLoop: for (var j = 0; j < 10; j++) { + // handle 429 ratelimit error, retry 10 times + try { + var args = await this.getFriends(params); + 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; + } + if (j === 9) { + throw err; + } + await new Promise((resolve) => { + workerTimers.setTimeout(resolve, 5000); + }); + } + } + params.offset += 50; + } + return friends; + }; + + API.refreshOfflineFriends = async function () { + var friends = []; + var params = { + n: 50, + offset: 0, + offline: true + }; + var onlineCount = + this.currentUser.onlineFriends.length + + this.currentUser.activeFriends.length; + var N = this.currentUser.friends.length - onlineCount; + var count = Math.trunc(N / 50); + mainLoop: for (var i = count; i > -1; i--) { + if (params.offset > 5000) { + // API offset limit is 5000 + break; + } + retryLoop: for (var j = 0; j < 10; j++) { + // handle 429 ratelimit error, retry 10 times + try { + var args = await this.getFriends(params); + 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; + } + if (j === 9) { + throw err; + } + await new Promise((resolve) => { + workerTimers.setTimeout(resolve, 5000); + }); + } + } + params.offset += 50; + } + return friends; }; /** - * @param {{worldId: string}} params + * Fetch friends of current user. + * @param {{ n: number, offset: number, offline: boolean }} params * @returns {Promise<{json: any, params}>} */ - API.publishWorld = function (params) { - return this.call(`worlds/${params.worldId}/publish`, { - method: 'PUT', + API.getFriends = function (params) { + return this.call('auth/user/friends', { + method: 'GET', params }).then((json) => { var args = { json, params }; - this.$emit('WORLD:SAVE', args); + this.$emit('FRIEND:LIST', args); return args; }); }; /** - * @param {{worldId: string}} params + * @param {{ userId: string }} params * @returns {Promise<{json: any, params}>} */ - API.unpublishWorld = function (params) { - return this.call(`worlds/${params.worldId}/publish`, { - method: 'DELETE', - params + API.deleteFriend = function (params) { + return this.call(`auth/user/friends/${params.userId}`, { + method: 'DELETE' }).then((json) => { var args = { json, params }; - this.$emit('WORLD:SAVE', args); + this.$emit('FRIEND:DELETE', args); return args; }); }; - // #endregion - // #region | API: Instance - - API.cachedInstances = new Map(); - /** - * @param {{worldId: string, instanceId: string}} params - * @returns {Promise<{json: any, params}>} + * @param {{ userId: string }} params + * @returns {Promise<{json: T, params}>} */ - API.getInstance = function (params) { - return this.call(`instances/${params.worldId}:${params.instanceId}`, { - method: 'GET' + API.sendFriendRequest = function (params) { + return this.call(`user/${params.userId}/friendRequest`, { + method: 'POST' }).then((json) => { var args = { json, params }; - this.$emit('INSTANCE', args); + this.$emit('FRIEND:REQUEST', args); return args; }); }; /** - * @typedef {{ - * worldId: string, - * type: string, - * region: string, - * ownerId: string, - * roleIds: string[], - * groupAccessType: string, - * queueEnabled: boolean - * }} CreateInstanceParameter - */ - - /** - * @param {CreateInstanceParameter} params + * @param {{ userId: string }} params * @returns {Promise<{json: any, params}>} */ - API.createInstance = function (params) { - return this.call('instances', { - method: 'POST', - params + API.cancelFriendRequest = function (params) { + return this.call(`user/${params.userId}/friendRequest`, { + method: 'DELETE' }).then((json) => { var args = { json, params }; - this.$emit('INSTANCE', args); + this.$emit('FRIEND:REQUEST:CANCEL', args); return args; }); }; - /** - * @param {{ worldId: string, instanceId: string, shortName: string }} instance - * @returns {Promise<{instance, json: T, params: {}}>} - */ - API.getInstanceShortName = function (instance) { - var params = {}; - if (instance.shortName) { - params.shortName = instance.shortName; - } - return this.call( - `instances/${instance.worldId}:${instance.instanceId}/shortName`, - { - method: 'GET', - params - } - ).then((json) => { + API.deleteHiddenFriendRequest = function (params, userId) { + return this.call(`user/${userId}/friendRequest`, { + method: 'DELETE', + params + }).then((json) => { var args = { json, - instance, - params + params, + userId }; - this.$emit('INSTANCE:SHORTNAME', args); + this.$emit('NOTIFICATION:HIDE', args); return args; }); }; /** - * @param {{ shortName: string }} params + * @param {{ userId: string }} params * @returns {Promise<{json: any, params}>} */ - API.getInstanceFromShortName = function (params) { - return this.call(`instances/s/${params.shortName}`, { + API.getFriendStatus = function (params) { + return this.call(`user/${params.userId}/friendStatus`, { method: 'GET' }).then((json) => { var args = { json, params }; - this.$emit('INSTANCE', args); + this.$emit('FRIEND:STATUS', args); return args; }); }; - /** - * Send invite to current user. - * @param {{ worldId: string, instanceId: string, shortName: string }} instance - * @returns {Promise<{instance, json: any, params}>} - */ - API.selfInvite = function (instance) { - /** - * @type {{ shortName?: string }} - */ - var params = {}; - if (instance.shortName) { - params.shortName = instance.shortName; + // #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 + } + }); } - return this.call( - `invite/myself/to/${instance.worldId}:${instance.instanceId}`, - { - method: 'POST', - params + }); + + API.$on('AVATAR:SAVE', function (args) { + var { json } = args; + this.$emit('AVATAR', { + json, + params: { + avatarId: json.id } - ) - .then((json) => { - var args = { - json, - instance, - params - }; - return args; - }) - .catch((err) => { - if (err?.error?.message) { - $app.$message({ - message: err.error.message, - type: 'error' - }); - throw err; + }); + }); + + 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); } - $app.$message({ - message: "you're not allowed to access this instance.", - type: 'error' - }); - throw err; - }); - }; + } + var array = Array.from(map.values()); + $app.sortUserDialogAvatars(array); + } + }); - API.applyInstance = function (json) { - var ref = this.cachedInstances.get(json.id); + API.applyAvatar = function (json) { + var ref = this.cachedAvatars.get(json.id); if (typeof ref === 'undefined') { ref = { id: '', - location: '', - instanceId: '', name: '', - worldId: '', - type: '', - ownerId: '', + description: '', + authorId: '', + authorName: '', 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: '', - 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, - // VRCX - $fetchedAt: '', + assetUrl: '', + assetUrlObject: {}, + imageUrl: '', + thumbnailImageUrl: '', + releaseStatus: '', + version: 0, + unityPackages: [], + unityPackageUrl: '', + unityPackageUrlObject: {}, + created_at: '', + updated_at: '', + featured: false, ...json }; - this.cachedInstances.set(ref.id, ref); + 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; + } } - ref.$location = this.parseLocation(ref.location); - if (json.world?.id) { - this.getCachedWorld({ - worldId: json.world.id - }).then((args) => { - ref.world = args.ref; - return args; - }); - } - if (!json.$fetchedAt) { - ref.$fetchedAt = new Date().toJSON(); - } + ref.name = $app.replaceBioSymbols(ref.name); + ref.description = $app.replaceBioSymbols(ref.description); return ref; }; - API.$on('INSTANCE', function (args) { - var { json } = args; - if (!json) { - return; - } - args.ref = this.applyInstance(args.json); - }); + /** + * @param {{ avatarId: string }} params + * @returns {Promise<{json: any, params}>} + */ + API.getAvatar = function (params) { + return this.call(`avatars/${params.avatarId}`, { + method: 'GET' + }).then((json) => { + var args = { + json, + params + }; + this.$emit('AVATAR', args); + return args; + }); + }; - 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(); - } - // hacky workaround to force update instance info - $app.updateInstanceInfo++; - }); + /** + * @typedef {{ + * n: number, + * offset: number, + * search: string, + * userId: string, + * user: 'me' | 'friends' + * sort: 'created' | 'updated' | 'order' | '_created_at' | '_updated_at', + * order: 'ascending' | 'descending', + * releaseStatus: 'public' | 'private' | 'hidden' | 'all', + * featured: boolean + * }} GetAvatarsParameter + */ + /** + * + * @param {GetAvatarsParameter} params + * @returns {Promise<{json: any, params}>} + */ + API.getAvatars = function (params) { + return this.call('avatars', { + method: 'GET', + params + }).then((json) => { + var args = { + json, + params + }; + this.$emit('AVATAR:LIST', args); + return args; + }); + }; - // #endregion - // #region | API: Friend + /** + * @param {{ id: string, releaseStatus: 'public' | 'private' }} params + * @returns {Promise<{json: any, params}>} + */ + API.saveAvatar = function (params) { + return this.call(`avatars/${params.id}`, { + method: 'PUT', + params + }).then((json) => { + var args = { + json, + params + }; + this.$emit('AVATAR:SAVE', args); + return args; + }); + }; - API.$on('FRIEND:LIST', function (args) { - for (var json of args.json) { - this.$emit('USER', { + /** + * @param {{avatarId: string }} params + * @returns {Promise<{json: any, params}>} + */ + API.selectAvatar = function (params) { + return this.call(`avatars/${params.avatarId}/select`, { + method: 'PUT', + params + }).then((json) => { + var args = { json, - params: { - userId: json.id - } - }); - - var state = 'offline'; - if (json.platform === 'web') { - state = 'active'; - } else if (json.platform) { - state = 'online'; - } - var ref = $app.friends.get(json.id); - if (ref?.state !== state) { - if ($app.debugFriendState) { - console.log( - `Bulk friend fetch, friend state does not match ${json.displayName} from ${ref?.state} to ${state}` - ); - } - this.getUser({ - userId: json.id - }); - } else if (json.location === 'traveling') { - if ($app.debugFriendState) { - console.log( - 'Bulk friend fetch, fetching traveling user', - json - ); - } - this.getUser({ - userId: json.id - }); - } - - // if ( - // !args.params.offline && - // json.platform !== 'web' && - // json.location === 'offline' - // ) { - // console.log('Fetching offline user', json); - // this.getUser({ - // userId: json.id - // }); - // } - } - }); - - API.isRefreshFriendsLoading = false; - - API.refreshFriends = async function () { - this.isRefreshFriendsLoading = true; - try { - var onlineFriends = await this.refreshOnlineFriends(); - var offlineFriends = await this.refreshOfflineFriends(); - var friends = onlineFriends.concat(offlineFriends); - this.isRefreshFriendsLoading = false; - return friends; - } catch (err) { - this.isRefreshFriendsLoading = false; - throw err; - } - }; - - API.refreshOnlineFriends = async function () { - var friends = []; - var params = { - n: 50, - offset: 0, - offline: false - }; - var N = - this.currentUser.onlineFriends.length + - this.currentUser.activeFriends.length; - var count = Math.trunc(N / 50); - mainLoop: for (var i = count; i > -1; i--) { - if (params.offset > 5000) { - // API offset limit is 5000 - break; - } - retryLoop: for (var j = 0; j < 10; j++) { - // handle 429 ratelimit error, retry 10 times - try { - var args = await this.getFriends(params); - 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; - } - if (j === 9) { - throw err; - } - await new Promise((resolve) => { - workerTimers.setTimeout(resolve, 5000); - }); - } - } - params.offset += 50; - } - return friends; - }; - - API.refreshOfflineFriends = async function () { - var friends = []; - var params = { - n: 50, - offset: 0, - offline: true - }; - var onlineCount = - this.currentUser.onlineFriends.length + - this.currentUser.activeFriends.length; - var N = this.currentUser.friends.length - onlineCount; - var count = Math.trunc(N / 50); - mainLoop: for (var i = count; i > -1; i--) { - if (params.offset > 5000) { - // API offset limit is 5000 - break; - } - retryLoop: for (var j = 0; j < 10; j++) { - // handle 429 ratelimit error, retry 10 times - try { - var args = await this.getFriends(params); - 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; - } - if (j === 9) { - throw err; - } - await new Promise((resolve) => { - workerTimers.setTimeout(resolve, 5000); - }); - } - } - params.offset += 50; - } - return friends; - }; + params + }; + this.$emit('AVATAR:SELECT', args); + return args; + }); + }; /** - * Fetch friends of current user. - * @param {{ n: number, offset: number, offline: boolean }} params - * @returns {Promise<{json: any, params}>} + * @param {{ avatarId: string }} params + * @return { Promise<{json: any, params}> } */ - API.getFriends = function (params) { - return this.call('auth/user/friends', { - method: 'GET', + API.selectFallbackAvatar = function (params) { + return this.call(`avatars/${params.avatarId}/selectfallback`, { + method: 'PUT', params }).then((json) => { var args = { json, params }; - this.$emit('FRIEND:LIST', args); + this.$emit('AVATAR:SELECT', args); return args; }); }; /** - * @param {{ userId: string }} params - * @returns {Promise<{json: any, params}>} + * @param {{ avatarId: string }} params + * @return { Promise<{json: any, params}> } */ - API.deleteFriend = function (params) { - return this.call(`auth/user/friends/${params.userId}`, { + API.deleteAvatar = function (params) { + return this.call(`avatars/${params.avatarId}`, { method: 'DELETE' }).then((json) => { var args = { json, params }; - this.$emit('FRIEND:DELETE', args); + this.$emit('AVATAR:DELETE', args); return args; }); }; /** - * @param {{ userId: string }} params - * @returns {Promise<{json: T, params}>} + * @param {{ avatarId: string }} params + * @returns {Promise<{json: any, params}>} */ - API.sendFriendRequest = function (params) { - return this.call(`user/${params.userId}/friendRequest`, { + API.createImposter = function (params) { + return this.call(`avatars/${params.avatarId}/impostor/enqueue`, { method: 'POST' }).then((json) => { var args = { json, params }; - this.$emit('FRIEND:REQUEST', args); + this.$emit('AVATAR:IMPOSTER:CREATE', args); return args; }); }; /** - * @param {{ userId: string }} params - * @returns {Promise<{json: any, params}>} + * @param {{ avatarId: string }} params + * @returns {Promise<{json: T, params}>} */ - API.cancelFriendRequest = function (params) { - return this.call(`user/${params.userId}/friendRequest`, { + API.deleteImposter = function (params) { + return this.call(`avatars/${params.avatarId}/impostor`, { method: 'DELETE' }).then((json) => { var args = { json, params }; - this.$emit('FRIEND:REQUEST:CANCEL', args); - return args; - }); - }; - - API.deleteHiddenFriendRequest = function (params, userId) { - return this.call(`user/${userId}/friendRequest`, { - method: 'DELETE', - params - }).then((json) => { - var args = { - json, - params, - userId - }; - this.$emit('NOTIFICATION:HIDE', args); - return args; - }); - }; - - /** - * @param {{ userId: string }} params - * @returns {Promise<{json: any, params}>} - */ - API.getFriendStatus = function (params) { - return this.call(`user/${params.userId}/friendStatus`, { - method: 'GET' - }).then((json) => { - var args = { - json, - params - }; - this.$emit('FRIEND:STATUS', args); + this.$emit('AVATAR:IMPOSTER:DELETE', args); return args; }); }; // #endregion - // #region | API: Avatar + // #region | API: Notification - API.cachedAvatars = new Map(); + API.isNotificationsLoading = false; - API.$on('AVATAR', function (args) { - args.ref = this.applyAvatar(args.json); + API.$on('LOGIN', function () { + this.isNotificationsLoading = false; }); - API.$on('AVATAR:LIST', function (args) { + 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('AVATAR', { + this.$emit('NOTIFICATION', { json, params: { - avatarId: json.id + notificationId: 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); + API.$on('NOTIFICATION:LIST:HIDDEN', function (args) { + for (var json of args.json) { + json.type = 'ignoredFriendRequest'; + this.$emit('NOTIFICATION', { + json, + params: { + notificationId: json.id } - } - 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 = { - id: '', - name: '', - description: '', - authorId: '', - authorName: '', - tags: [], - assetUrl: '', - assetUrlObject: {}, - imageUrl: '', - thumbnailImageUrl: '', - releaseStatus: '', - version: 0, - unityPackages: [], - unityPackageUrl: '', - unityPackageUrlObject: {}, - created_at: '', - updated_at: '', - featured: false, - ...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; - } - } - ref.name = $app.replaceBioSymbols(ref.name); - ref.description = $app.replaceBioSymbols(ref.description); - return ref; - }; - - /** - * @param {{ avatarId: string }} params - * @returns {Promise<{json: any, params}>} - */ - API.getAvatar = function (params) { - return this.call(`avatars/${params.avatarId}`, { - method: 'GET' - }).then((json) => { - var args = { - json, - params - }; - this.$emit('AVATAR', args); - return args; - }); - }; - - /** - * @typedef {{ - * n: number, - * offset: number, - * search: string, - * userId: string, - * user: 'me' | 'friends' - * sort: 'created' | 'updated' | 'order' | '_created_at' | '_updated_at', - * order: 'ascending' | 'descending', - * releaseStatus: 'public' | 'private' | 'hidden' | 'all', - * featured: boolean - * }} GetAvatarsParameter - */ - /** - * - * @param {GetAvatarsParameter} params - * @returns {Promise<{json: any, params}>} - */ - API.getAvatars = function (params) { - return this.call('avatars', { - method: 'GET', - params - }).then((json) => { - var args = { - json, - params - }; - this.$emit('AVATAR:LIST', args); - return args; - }); - }; - - /** - * @param {{ id: string, releaseStatus: 'public' | 'private' }} params - * @returns {Promise<{json: any, params}>} - */ - API.saveAvatar = function (params) { - return this.call(`avatars/${params.id}`, { - method: 'PUT', - params - }).then((json) => { - var args = { - json, - params - }; - this.$emit('AVATAR:SAVE', args); - return args; - }); - }; - - /** - * @param {{avatarId: string }} params - * @returns {Promise<{json: any, params}>} - */ - API.selectAvatar = function (params) { - return this.call(`avatars/${params.avatarId}/select`, { - method: 'PUT', - params - }).then((json) => { - var args = { - json, - params - }; - this.$emit('AVATAR:SELECT', args); - return args; - }); - }; - - /** - * @param {{ avatarId: string }} params - * @return { Promise<{json: any, params}> } - */ - API.selectFallbackAvatar = function (params) { - return this.call(`avatars/${params.avatarId}/selectfallback`, { - method: 'PUT', - params - }).then((json) => { - var args = { - json, - params - }; - this.$emit('AVATAR:SELECT', args); - return args; - }); - }; - - /** - * @param {{ avatarId: string }} params - * @return { Promise<{json: any, params}> } - */ - API.deleteAvatar = function (params) { - return this.call(`avatars/${params.avatarId}`, { - method: 'DELETE' - }).then((json) => { - var args = { - json, - params - }; - this.$emit('AVATAR:DELETE', args); - return args; - }); - }; - - /** - * @param {{ avatarId: string }} params - * @returns {Promise<{json: any, params}>} - */ - API.createImposter = function (params) { - return this.call(`avatars/${params.avatarId}/impostor/enqueue`, { - method: 'POST' - }).then((json) => { - var args = { - json, - params - }; - this.$emit('AVATAR:IMPOSTER:CREATE', args); - return args; - }); - }; - - /** - * @param {{ avatarId: string }} params - * @returns {Promise<{json: T, params}>} - */ - API.deleteImposter = function (params) { - return this.call(`avatars/${params.avatarId}/impostor`, { - method: 'DELETE' - }).then((json) => { - var args = { - json, - params - }; - this.$emit('AVATAR:IMPOSTER:DELETE', args); - return args; - }); - }; - - // #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 - } - }); + }); } }); @@ -3769,7 +2241,7 @@ speechSynthesis.getVoices(); this.$emit('NOTIFICATION:HIDE', args); new Noty({ type: 'success', - text: escapeTag(args.json) + text: $app.escapeTag(args.json) }).show(); console.log('NOTIFICATION:RESPONSE', args); }); @@ -4981,28872 +3453,17644 @@ speechSynthesis.getVoices(); }; // #endregion - // #region | API: WebSocket + // #region | API: Visit - API.webSocket = null; - API.lastWebSocketMessage = ''; + API.getVisits = function () { + return this.call('visits', { + method: 'GET' + }).then((json) => { + var args = { + json + }; + this.$emit('VISITS', args); + return args; + }); + }; - API.$on('LOGOUT', function () { - this.closeWebSocket(); - }); + // #endregion + // API - API.$on('USER:CURRENT', function () { - if ($app.friendLogInitStatus && this.webSocket === null) { - this.getAuth(); - } - }); + // #endregion + // #region | Misc - API.$on('AUTH', function (args) { - if (args.json.ok) { - this.connectWebSocket(args.json.token); - } - }); + var $timers = []; - API.$on('PIPELINE', function (args) { - var { type, content, err } = args.json; - if (typeof err !== 'undefined') { - console.error('PIPELINE: error', args); - if (this.errorNoty) { - this.errorNoty.close(); - } - this.errorNoty = new Noty({ - type: 'error', - text: escapeTag(`WebSocket Error: ${err}`) - }).show(); - return; - } - if (typeof content === 'undefined') { - console.error('PIPELINE: missing content', args); - return; - } - if (typeof content.user !== 'undefined') { - // I forgot about this... - delete content.user.state; - } - switch (type) { - case 'notification': - this.$emit('NOTIFICATION', { - json: content, - params: { - notificationId: content.id - } - }); - this.$emit('PIPELINE:NOTIFICATION', { - json: content, - params: { - notificationId: content.id - } - }); - break; - - case 'notification-v2': - console.log('notification-v2', content); - this.$emit('NOTIFICATION:V2', { - json: content, - params: { - notificationId: content.id - } - }); - break; - - case 'notification-v2-delete': - console.log('notification-v2-delete', content); - for (var id of content.ids) { - this.$emit('NOTIFICATION:HIDE', { - params: { - notificationId: id - } - }); - this.$emit('NOTIFICATION:SEE', { - params: { - notificationId: id - } - }); - } - break; - - case 'notification-v2-update': - console.log('notification-v2-update', content); - this.$emit('NOTIFICATION:V2:UPDATE', { - json: content.updates, - params: { - notificationId: content.id - } - }); - break; - - case 'see-notification': - this.$emit('NOTIFICATION:SEE', { - params: { - notificationId: content - } - }); - break; - - case 'hide-notification': - this.$emit('NOTIFICATION:HIDE', { - params: { - notificationId: content - } - }); - this.$emit('NOTIFICATION:SEE', { - params: { - notificationId: content - } - }); - break; - - case 'response-notification': - this.$emit('NOTIFICATION:HIDE', { - params: { - notificationId: content.notificationId - } - }); - this.$emit('NOTIFICATION:SEE', { - params: { - notificationId: content.notificationId - } - }); - break; - - case 'friend-add': - this.$emit('USER', { - json: content.user, - params: { - userId: content.userId - } - }); - this.$emit('FRIEND:ADD', { - params: { - userId: content.userId - } - }); - break; - - case 'friend-delete': - this.$emit('FRIEND:DELETE', { - params: { - userId: content.userId - } - }); - break; - - case 'friend-online': - if (content?.user?.id) { - this.$emit('USER', { - json: { - location: content.location, - travelingToLocation: content.travelingToLocation, - ...content.user, - state: 'online' - }, - params: { - userId: content.userId - } - }); - } else { - this.$emit('FRIEND:STATE', { - json: { - state: 'online' - }, - params: { - userId: content.userId - } - }); + Vue.component('timer', { + template: '', + props: { + epoch: { + type: Number, + default() { + return Date.now(); } - break; - - case 'friend-active': - if (content?.user?.id) { - this.$emit('USER', { - json: { - ...content.user, - state: 'active' - }, - params: { - userId: content.userId - } - }); - } else { - this.$emit('FRIEND:STATE', { - json: { - state: 'active' - }, - params: { - userId: content.userId - } - }); + } + }, + data() { + return { + text: '' + }; + }, + methods: { + update() { + if (!this.epoch) { + this.text = '-'; + return; } - break; - - case 'friend-offline': - this.$emit('FRIEND:STATE', { - json: { - state: 'offline' - }, - params: { - userId: content.userId - } - }); - break; + this.text = $app.timeToText(Date.now() - this.epoch); + } + }, + watch: { + date() { + this.update(); + } + }, + mounted() { + $timers.push(this); + this.update(); + }, + destroyed() { + $app.removeFromArray($timers, this); + } + }); - case 'friend-update': - this.$emit('USER', { - json: content.user, - params: { - userId: content.userId - } - }); - break; + workerTimers.setInterval(function () { + for (var $timer of $timers) { + $timer.update(); + } + }, 5000); - case 'friend-location': - if (!content?.user?.id) { - var ref = this.cachedUsers.get(content.userId); - if (typeof ref !== 'undefined') { - this.$emit('USER', { - json: { - ...ref, - location: content.location, - travelingToLocation: content.travelingToLocation - }, - params: { - userId: content.userId - } - }); - } - break; - } - this.$emit('USER', { - json: { - location: content.location, - travelingToLocation: content.travelingToLocation, - ...content.user - // state: 'online' - }, - params: { - userId: content.userId - } - }); - break; + // Countdown timer - case 'user-update': - this.$emit('USER:CURRENT', { - json: content.user, - params: { - userId: content.userId - } - }); - break; + var $countDownTimers = []; - case 'user-location': - // update current user location - if (content.userId !== this.currentUser.id) { - console.error('user-location wrong userId', content); - break; + Vue.component('countdown-timer', { + template: '', + props: { + datetime: { + type: String, + default() { + return ''; } - - // content.user: {} - // content.world: {} - - this.currentUser.presence.instance = content.instance; - this.currentUser.presence.world = content.worldId; - $app.setCurrentUserLocation(content.location); - break; - - case 'group-joined': - // var groupId = content.groupId; - // $app.onGroupJoined(groupId); - break; - - case 'group-left': - // var groupId = content.groupId; - // $app.onGroupLeft(groupId); - break; - - case 'group-role-updated': - var groupId = content.role.groupId; - API.getGroup({ groupId, includeRoles: true }); - console.log('group-role-updated', content); - - // content { - // role: { - // createdAt: string, - // description: string, - // groupId: string, - // id: string, - // isManagementRole: boolean, - // isSelfAssignable: boolean, - // name: string, - // order: number, - // permissions: string[], - // requiresPurchase: boolean, - // requiresTwoFactor: boolean - break; - - case 'group-member-updated': - var groupId = content.member.groupId; - if ( - $app.groupDialog.visible && - $app.groupDialog.id === groupId - ) { - $app.getGroupDialogGroup(groupId); + }, + hours: { + type: Number, + default() { + return 1; } - this.$emit('GROUP:MEMBER', { - json: content.member, - params: { - groupId - } - }); - console.log('group-member-updated', content); - - // content { - // groupId: string, - // id: string, - // isRepresenting: boolean, - // isSubscribedToAnnouncements: boolean, - // joinedAt: string, - // membershipStatus: string, - // roleIds: string[], - // userId: string, - // visibility: string - // } - break; - - case 'instance-queue-joined': - case 'instance-queue-position': - var instanceId = content.instanceLocation; - var position = content.position ?? 0; - var queueSize = content.queueSize ?? 0; - $app.instanceQueueUpdate(instanceId, position, queueSize); - break; - - case 'instance-queue-ready': - var instanceId = content.instanceLocation; - // var expiry = Date.parse(content.expiry); - $app.instanceQueueReady(instanceId); - break; - - case 'instance-queue-left': - console.log('instance-queue-left', content); - var instanceId = content.instanceLocation; - $app.removeQueuedInstance(instanceId); - // $app.instanceQueueClear(); - break; - - case 'content-refresh': - var contentType = content.contentType; - console.log('content-refresh', content); - if (contentType === 'icon') { - if ($app.galleryDialogVisible) { - $app.refreshVRCPlusIconsTable(); - } - } else if (contentType === 'gallery') { - if ($app.galleryDialogVisible) { - $app.refreshGalleryTable(); - } - } else if (contentType === 'emoji') { - if ($app.galleryDialogVisible) { - $app.refreshEmojiTable(); - } - } else if (contentType === 'sticker') { - if ($app.galleryDialogVisible) { - $app.refreshStickerTable(); - } - } else if (contentType === 'avatar') { - // hmm, utilizing this might be too spamy and cause UI to move around - } else if (contentType === 'world') { - // hmm - } else if (contentType === 'created') { - // on avatar upload + } + }, + 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 { - console.log('Unknown content-refresh', content); - } - break; - - case 'instance-closed': - // TODO: get worldName, groupName, hardClose - var noty = { - type: 'instance.closed', - location: content.instanceLocation, - message: 'Instance Closed', - created_at: new Date().toJSON() - }; - if ( - $app.notificationTable.filters[0].value.length === 0 || - $app.notificationTable.filters[0].value.includes(noty.type) - ) { - $app.notifyMenu('notification'); + this.text = '-'; } - $app.queueNotificationNoty(noty); - $app.notificationTable.data.push(noty); - $app.updateSharedFeed(true); - break; - - default: - console.log('Unknown pipeline type', args.json); + } + }, + watch: { + date() { + this.update(); + } + }, + mounted() { + $countDownTimers.push(this); + this.update(); + }, + destroyed() { + $app.removeFromArray($countDownTimers, this); } }); - API.getAuth = function () { - return this.call('auth', { - method: 'GET' - }).then((json) => { - var args = { - json - }; - this.$emit('AUTH', args); - return args; + 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); + } }); }; - API.connectWebSocket = function (token) { - if (this.webSocket === null) { - var socket = new WebSocket(`${API.websocketDomain}/?auth=${token}`); - socket.onopen = () => { - if ($app.debugWebSocket) { - console.log('WebSocket connected'); - } - }; - socket.onclose = () => { - if (this.webSocket === socket) { - this.webSocket = null; - } - try { - socket.close(); - } catch (err) {} - if ($app.debugWebSocket) { - console.log('WebSocket closed'); - } - workerTimers.setTimeout(() => { - if ( - this.isLoggedIn && - $app.friendLogInitStatus && - this.webSocket === null - ) { - this.getAuth(); - } - }, 5000); - }; - socket.onerror = () => { - if (this.errorNoty) { - this.errorNoty.close(); - } - this.errorNoty = new Noty({ - type: 'error', - text: 'WebSocket Error' - }).show(); - socket.onclose(); - }; - socket.onmessage = ({ data }) => { - try { - if (this.lastWebSocketMessage === data) { - // pls no spam - return; - } - this.lastWebSocketMessage = data; - var json = JSON.parse(data); - try { - json.content = JSON.parse(json.content); - } catch (err) {} - this.$emit('PIPELINE', { - json - }); - if ($app.debugWebSocket && json.content) { - var displayName = ''; - var user = this.cachedUsers.get(json.content.userId); - if (user) { - displayName = user.displayName; - } - console.log( - 'WebSocket', - json.type, - displayName, - json.content - ); - } - } catch (err) { - console.error(err); - } - }; - this.webSocket = socket; + $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); + } + } + }); }; - API.closeWebSocket = function () { - var socket = this.webSocket; - if (socket === null) { + $app.methods.compareAppVersion = async function () { + if (!this.appVersion) { return; } - this.webSocket = null; - try { - socket.close(); - } catch (err) {} + var lastVersion = await configRepository.getString( + 'VRCX_lastVRCXVersion', + '' + ); + if (!lastVersion) { + await configRepository.setString( + 'VRCX_lastVRCXVersion', + this.appVersion + ); + return; + } + if (lastVersion !== this.appVersion) { + await configRepository.setString( + 'VRCX_lastVRCXVersion', + this.appVersion + ); + if ( + (await configRepository.getString('VRCX_branch')) === 'Stable' + ) { + this.showChangeLogDialog(); + } + } }; - API.reconnectWebSocket = function () { - if (!this.isLoggedIn || !$app.friendLogInitStatus) { + $app.methods.setBranch = async function () { + if (!this.appVersion) { return; } - this.closeWebSocket(); - this.getAuth(); + if (this.appVersion.includes('VRCX Nightly')) { + this.branch = 'Nightly'; + } else { + this.branch = 'Stable'; + } + await configRepository.setString('VRCX_branch', this.branch); }; - // #endregion - // #region | API: Visit - - API.getVisits = function () { - return this.call('visits', { - method: 'GET' - }).then((json) => { - var args = { - json - }; - this.$emit('VISITS', args); - return args; - }); + $app.methods.updateIsGameRunning = async function ( + isGameRunning, + isSteamVRRunning, + isHmdAfk + ) { + if (isGameRunning !== this.isGameRunning) { + this.isGameRunning = isGameRunning; + if (isGameRunning) { + API.currentUser.$online_for = Date.now(); + API.currentUser.$offline_for = ''; + } 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.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(); }; - // #endregion - // API - - // #endregion - // #region | Misc + $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; - var extractFileId = (s) => { - var match = String(s).match(/file_[0-9A-Za-z-]+/); - return match ? match[0] : ''; - }; + $app.data.APILastOnline = new Map(); - var extractFileVersion = (s) => { - var match = /(?:\/file_[0-9A-Za-z-]+\/)([0-9]+)/gi.exec(s); - return match ? match[1] : ''; + $app.methods.notifyMenu = function (index) { + var { menu } = this.$refs; + if (menu.activeIndex !== index) { + var item = menu.items[index]; + if (item) { + item.$el.classList.add('notify'); + } + } }; - var extractVariantVersion = (url) => { - if (!url) { - return '0'; + $app.methods.selectMenu = function (index) { + // NOTE + // 툴팁이 쌓여서 느려지기 때문에 날려줌. + // 근데 이 방법이 안전한지는 모르겠음 + document.querySelectorAll('[role="tooltip"]').forEach((node) => { + node.remove(); + }); + var item = this.$refs.menu.items[index]; + if (item) { + item.$el.classList.remove('notify'); } - try { - const params = new URLSearchParams(new URL(url).search); - const version = params.get('v'); - if (version) { - return version; - } - return '0'; - } catch { - return '0'; + if (index === 'notification') { + this.unseenNotifications = []; } - }; - var buildTreeData = (json) => { - var node = []; - for (var key in json) { - if (key[0] === '$') { - continue; - } - var value = json[key]; - if (Array.isArray(value) && value.length === 0) { - node.push({ - key, - value: '[]' - }); - } else if ( - value === Object(value) && - Object.keys(value).length === 0 + workerTimers.setTimeout(() => { + // fix some weird sorting bug when disabling data tables + if ( + typeof this.$refs.playerModerationTableRef?.sortData !== + 'undefined' ) { - node.push({ - key, - value: '{}' - }); - } else if (Array.isArray(value)) { - node.push({ - children: value.map((val, idx) => { - if (val === Object(val)) { - return { - children: buildTreeData(val), - key: idx - }; - } - return { - key: idx, - value: val - }; - }), - key - }); - } else if (value === Object(value)) { - node.push({ - children: buildTreeData(value), - key - }); - } else { - node.push({ - key, - value: String(value) - }); + this.$refs.playerModerationTableRef.sortData.prop = 'created'; } - } - node.sort(function (a, b) { - var A = String(a.key).toUpperCase(); - var B = String(b.key).toUpperCase(); - if (A < B) { - return -1; + if ( + typeof this.$refs.notificationTableRef?.sortData !== 'undefined' + ) { + this.$refs.notificationTableRef.sortData.prop = 'created_at'; } - if (A > B) { - return 1; + if (typeof this.$refs.friendLogTableRef?.sortData !== 'undefined') { + this.$refs.friendLogTableRef.sortData.prop = 'created_at'; } - return 0; - }); - return node; + }, 100); }; - var $timers = []; + $app.data.twoFactorAuthDialogVisible = false; - Vue.component('timer', { - template: '', - props: { - epoch: { - type: Number, - default() { - return Date.now(); - } - } - }, - data() { - return { - text: '' - }; - }, - methods: { - update() { - if (!this.epoch) { - this.text = '-'; - return; - } - this.text = timeToText(Date.now() - this.epoch); - } - }, - watch: { - date() { - this.update(); - } - }, - mounted() { - $timers.push(this); - this.update(); - }, - destroyed() { - removeFromArray($timers, this); - } + API.$on('LOGIN', function () { + $app.twoFactorAuthDialogVisible = false; }); - workerTimers.setInterval(function () { - for (var $timer of $timers) { - $timer.update(); - } - }, 5000); - - // Countdown timer - - var $countDownTimers = []; - - Vue.component('countdown-timer', { - template: '', - 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 = timeToText(epoch); - } else { - this.text = '-'; - } - } - }, - watch: { - date() { - this.update(); + $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; } - }, - mounted() { - $countDownTimers.push(this); - this.update(); - }, - destroyed() { - removeFromArray($countDownTimers, this); - } - }); - - workerTimers.setInterval(function () { - for (var $countDownTimer of $countDownTimers) { - $countDownTimer.update(); } - }, 5000); + new Noty({ + type: 'error', + text: 'Cannot send 2FA email without saved credentials. Please login again.' + }).show(); + this.promptEmailOTP(); + }; - // #endregion - // #region | initialise ... stuff. Don't look at me, I don't work here + $app.data.exportFriendsListDialog = false; + $app.data.exportFriendsListCsv = ''; + $app.data.exportFriendsListJson = ''; - var $app = { - data: { - API, - nextCurrentUserRefresh: 0, - nextFriendsRefresh: 0, - nextGroupInstanceRefresh: 0, - nextAppUpdateCheck: 7200, - ipcTimeout: 0, - nextClearVRCXCacheCheck: 0, - nextDiscordUpdate: 0, - nextAutoStateChange: 0, - isDiscordActive: false, - isGameRunning: false, - isGameNoVR: true, - isSteamVRRunning: false, - isHmdAfk: false, - appVersion: '', - latestAppVersion: '', - ossDialog: false - }, - i18n, - computed: {}, - methods: {}, - watch: {}, - el: '#x-app', - async mounted() { - await this.initLanguage(); - await this.changeThemeMode(); - await AppApi.SetUserAgent(); - this.appVersion = await AppApi.GetVersion(); - await this.compareAppVersion(); - await this.setBranch(); - if (this.autoUpdateVRCX !== 'Off') { - this.checkForVRCXUpdate(); + $app.methods.showExportFriendsListDialog = function () { + var { friends } = API.currentUser; + if (Array.isArray(friends) === false) { + return; + } + var lines = ['UserID,DisplayName,Memo']; + var _ = function (str) { + if (/[\x00-\x1f,"]/.test(str) === true) { + return `"${str.replace(/"/g, '""')}"`; } - await AppApi.CheckGameRunning(); - this.isGameNoVR = await configRepository.getBool('isGameNoVR'); - await AppApi.SetAppLauncherSettings( - this.enableAppLauncher, - this.enableAppLauncherAutoClose - ); - API.$on('SHOW_USER_DIALOG', (userId) => - this.showUserDialog(userId) - ); - API.$on('SHOW_WORLD_DIALOG', (tag) => this.showWorldDialog(tag)); - API.$on('SHOW_WORLD_DIALOG_SHORTNAME', (tag) => - this.verifyShortName('', tag) - ); - API.$on('SHOW_GROUP_DIALOG', (groupId) => - this.showGroupDialog(groupId) - ); - API.$on('SHOW_LAUNCH_DIALOG', (tag, shortName) => - this.showLaunchDialog(tag, shortName) - ); - this.updateLoop(); - this.getGameLogTable(); - this.refreshCustomCss(); - this.refreshCustomScript(); - this.checkVRChatDebugLogging(); - this.checkAutoBackupRestoreVrcRegistry(); - await this.migrateStoredUsers(); - 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 = 120; // 1min - console.error(err); - }); - return args; - }); - } else { - this.loginForm.loading = false; - } - }); + return str; + }; + var friendsList = []; + for (var userId of friends) { + var ref = this.friends.get(userId); + var name = (typeof ref !== 'undefined' && ref.name) || ''; + var memo = + (typeof ref !== 'undefined' && ref.memo.replace(/\n/g, ' ')) || + ''; + lines.push(`${_(userId)},${_(name)},${_(memo)}`); + friendsList.push(userId); } + this.exportFriendsListJson = JSON.stringify( + { friends: friendsList }, + null, + 4 + ); + this.exportFriendsListCsv = lines.join('\n'); + this.exportFriendsListDialog = true; }; - $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.data.exportAvatarsListDialog = false; + $app.data.exportAvatarsListCsv = ''; - $app.methods.refreshCustomScript = function () { - if (document.contains(document.getElementById('app-custom-script'))) { - document.getElementById('app-custom-script').remove(); + $app.methods.showExportAvatarsListDialog = function () { + for (var ref of API.cachedAvatars.values()) { + if (ref.authorId === API.currentUser.id) { + API.cachedAvatars.delete(ref.id); + } } - 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); + var params = { + n: 50, + offset: 0, + sort: 'updated', + order: 'descending', + releaseStatus: 'all', + user: 'me' + }; + var map = new Map(); + API.bulk({ + fn: '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 avatars = Array.from(map.values()); + if (Array.isArray(avatars) === false) { + return; + } + var lines = ['AvatarID,AvatarName']; + var _ = function (str) { + if (/[\x00-\x1f,"]/.test(str) === true) { + return `"${str.replace(/"/g, '""')}"`; + } + return str; + }; + for (var avatar of avatars) { + lines.push(`${_(avatar.id)},${_(avatar.name)}`); + } + this.exportAvatarsListCsv = lines.join('\n'); + this.exportAvatarsListDialog = true; } }); }; - $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); - } + 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, ${$app.escapeTag( + this.currentUser.displayName + )}!` + }).show(); + } + this.isLoggedIn = false; + $app.friendLogInitStatus = false; + }); + + API.$on('LOGIN', function (args) { + new Noty({ + type: 'success', + text: `Hello there, ${$app.escapeTag( + args.ref.displayName + )}!` + }).show(); + $app.$refs.menu.activeIndex = 'feed'; + }); + + 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.methods.compareAppVersion = async function () { - if (!this.appVersion) { - return; + $app.data.enablePrimaryPassword = await configRepository.getBool( + 'enablePrimaryPassword', + false + ); + $app.data.enablePrimaryPasswordDialog = { + visible: false, + password: '', + rePassword: '', + beforeClose(done) { + $app._data.enablePrimaryPassword = false; + done(); } - var lastVersion = await configRepository.getString( - 'VRCX_lastVRCXVersion', - '' - ); - if (!lastVersion) { - await configRepository.setString( - 'VRCX_lastVRCXVersion', - this.appVersion - ); - return; + }; + $app.methods.enablePrimaryPasswordChange = function () { + 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 + ); + }); } - if (lastVersion !== this.appVersion) { - await configRepository.setString( - 'VRCX_lastVRCXVersion', - this.appVersion - ); - if ( - (await configRepository.getString('VRCX_branch')) === 'Stable' - ) { - this.showChangeLogDialog(); + }; + $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.setBranch = async function () { - if (!this.appVersion) { - return; + $app.methods.updateStoredUser = async function (user) { + var savedCredentials = {}; + if ((await configRepository.getString('savedCredentials')) !== null) { + savedCredentials = JSON.parse( + await configRepository.getString('savedCredentials') + ); } - if (this.appVersion.includes('VRCX Nightly')) { - this.branch = 'Nightly'; - } else { - this.branch = 'Stable'; + 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(); } - await configRepository.setString('VRCX_branch', this.branch); + 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.languageClass = function (language) { - var style = {}; - var mapping = languageMappings[language]; - if (typeof mapping !== 'undefined') { - style[mapping] = true; - } else { - style.unknown = true; + $app.methods.migrateStoredUsers = async function () { + var savedCredentials = {}; + if ((await configRepository.getString('savedCredentials')) !== null) { + savedCredentials = JSON.parse( + await configRepository.getString('savedCredentials') + ); } - return style; - }; - - $app.methods.updateLoop = function () { - try { - if (API.isLoggedIn === true) { - if (--this.nextFriendsRefresh <= 0) { - this.nextFriendsRefresh = 7200; // 1hour - this.refreshFriendsList(); - this.updateStoredUser(API.currentUser); - if (this.isGameRunning) { - API.refreshPlayerModerations(); - } - } - if (--this.nextCurrentUserRefresh <= 0) { - API.getCurrentUser(); - } - if (--this.nextGroupInstanceRefresh <= 0) { - if (this.friendLogInitStatus) { - this.nextGroupInstanceRefresh = 600; // 5min - API.getUsersGroupInstances(); - } - AppApi.CheckGameRunning(); - } - if (--this.nextAppUpdateCheck <= 0) { - this.nextAppUpdateCheck = 7200; // 1hour - if (this.autoUpdateVRCX !== 'Off') { - this.checkForVRCXUpdate(); - } - } - if (--this.ipcTimeout <= 0) { - this.ipcEnabled = false; - } - if ( - --this.nextClearVRCXCacheCheck <= 0 && - this.clearVRCXCacheFrequency > 0 - ) { - this.nextClearVRCXCacheCheck = this.clearVRCXCacheFrequency; - this.clearVRCXCache(); - } - if (--this.nextDiscordUpdate <= 0) { - this.nextDiscordUpdate = 7; - if (this.discordActive) { - this.updateDiscord(); - } - } - if (--this.nextAutoStateChange <= 0) { - this.nextAutoStateChange = 7; - this.updateAutoStateChange(); - } + for (let name in savedCredentials) { + var userId = savedCredentials[name]?.user?.id; + if (userId && userId !== name) { + savedCredentials[userId] = savedCredentials[name]; + delete savedCredentials[name]; } - } catch (err) { - API.isRefreshFriendsLoading = false; - console.error(err); } - workerTimers.setTimeout(() => this.updateLoop(), 500); + await configRepository.setString( + 'savedCredentials', + JSON.stringify(savedCredentials) + ); }; - $app.methods.updateIsGameRunning = async function ( - isGameRunning, - isSteamVRRunning, - isHmdAfk - ) { - if (isGameRunning !== this.isGameRunning) { - this.isGameRunning = isGameRunning; - if (isGameRunning) { - API.currentUser.$online_for = Date.now(); - API.currentUser.$offline_for = ''; - } 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.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.debugPhotonLogging = false; - $app.data.debugGameLog = false; - $app.data.debugFriendState = false; + // #endregion + // #region | App: Friends - $app.data.APILastOnline = new Map(); + $app.data.friends = new Map(); + $app.data.pendingActiveFriends = new Set(); + $app.data.friendsNo = 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.data.sharedFeed = { - gameLog: { - wrist: [], - lastEntryDate: '' - }, - feedTable: { - wrist: [], - lastEntryDate: '' - }, - notificationTable: { - wrist: [], - lastEntryDate: '' - }, - friendLogTable: { - wrist: [], - lastEntryDate: '' - }, - moderationAgainstTable: { - wrist: [], - lastEntryDate: '' - }, - pendingUpdate: false + $app.methods.saveFriendsGroupStates = async function () { + await configRepository.setBool( + 'VRCX_isFriendsGroupMe', + this.isFriendsGroupMe + ); + await configRepository.setBool( + 'VRCX_isFriendsGroupFavorites', + this.isVIPFriends + ); + await configRepository.setBool( + 'VRCX_isFriendsGroupOnline', + this.isOnlineFriends + ); + await configRepository.setBool( + 'VRCX_isFriendsGroupActive', + this.isActiveFriends + ); + await configRepository.setBool( + 'VRCX_isFriendsGroupOffline', + this.isOfflineFriends + ); }; - $app.data.updateSharedFeedTimer = null; - $app.data.updateSharedFeedPending = false; - $app.data.updateSharedFeedPendingForceUpdate = false; - $app.methods.updateSharedFeed = function (forceUpdate) { - if (!this.friendLogInitStatus) { - return; - } - if (this.updateSharedFeedTimer) { - if (forceUpdate) { - this.updateSharedFeedPendingForceUpdate = true; - } - this.updateSharedFeedPending = true; - } else { - this.updateSharedExecute(forceUpdate); - this.updateSharedFeedTimer = setTimeout(() => { - if (this.updateSharedFeedPending) { - this.updateSharedExecute( - this.updateSharedFeedPendingForceUpdate - ); - } - this.updateSharedFeedTimer = null; - }, 150); - } + $app.methods.loadFriendsGroupStates = async function () { + this.isFriendsGroupMe = await configRepository.getBool( + 'VRCX_isFriendsGroupMe', + true + ); + this.isVIPFriends = await configRepository.getBool( + 'VRCX_isFriendsGroupFavorites', + true + ); + this.isOnlineFriends = await configRepository.getBool( + 'VRCX_isFriendsGroupOnline', + true + ); + this.isActiveFriends = await configRepository.getBool( + 'VRCX_isFriendsGroupActive', + false + ); + this.isOfflineFriends = await configRepository.getBool( + 'VRCX_isFriendsGroupOffline', + false + ); }; - $app.methods.updateSharedExecute = function (forceUpdate) { - try { - this.updateSharedFeedDebounce(forceUpdate); - } catch (err) { - console.error(err); - } - this.updateSharedFeedTimer = null; - this.updateSharedFeedPending = false; - this.updateSharedFeedPendingForceUpdate = false; - }; + API.$on('LOGIN', function () { + $app.loadFriendsGroupStates(); + }); - $app.methods.updateSharedFeedDebounce = function (forceUpdate) { - this.updateSharedFeedGameLog(forceUpdate); - this.updateSharedFeedFeedTable(forceUpdate); - this.updateSharedFeedNotificationTable(forceUpdate); - this.updateSharedFeedFriendLogTable(forceUpdate); - this.updateSharedFeedModerationAgainstTable(forceUpdate); - var feeds = this.sharedFeed; - if (!feeds.pendingUpdate) { - return; - } - var wristFeed = []; - wristFeed = wristFeed.concat( - feeds.gameLog.wrist, - feeds.feedTable.wrist, - feeds.notificationTable.wrist, - feeds.friendLogTable.wrist, - feeds.moderationAgainstTable.wrist - ); - // OnPlayerJoining/Traveling - API.currentTravelers.forEach((ref) => { - var isFavorite = this.localFavoriteFriends.has(ref.id); - if ( - (this.sharedFeedFilters.wrist.OnPlayerJoining === 'Friends' || - (this.sharedFeedFilters.wrist.OnPlayerJoining === 'VIP' && - isFavorite)) && - !$app.lastLocation.playerList.has(ref.displayName) - ) { - if (ref.$location.tag === $app.lastLocation.location) { - var feedEntry = { - ...ref, - isFavorite, - isFriend: true, - type: 'OnPlayerJoining' - }; - wristFeed.unshift(feedEntry); - } else { - var worldRef = API.cachedWorlds.get(ref.$location.worldId); - var groupName = ''; - if (ref.$location.groupId) { - var groupRef = API.cachedGroups.get( - ref.$location.groupId - ); - if (typeof groupRef !== 'undefined') { - groupName = groupRef.name; - } else { - // no group cache, fetch group and try again - API.getGroup({ - groupId: ref.$location.groupId - }) - .then((args) => { - workerTimers.setTimeout(() => { - // delay to allow for group cache to update - $app.sharedFeed.pendingUpdate = true; - $app.updateSharedFeed(false); - }, 100); - return args; - }) - .catch((err) => { - console.error(err); - }); - } - } - if (typeof worldRef !== 'undefined') { - var feedEntry = { - created_at: ref.created_at, - type: 'GPS', - userId: ref.id, - displayName: ref.displayName, - location: ref.$location.tag, - worldName: worldRef.name, - groupName, - previousLocation: '', - isFavorite, - time: 0, - isFriend: true, - isTraveling: true - }; - wristFeed.unshift(feedEntry); - } else { - // no world cache, fetch world and try again - API.getWorld({ - worldId: ref.$location.worldId - }) - .then((args) => { - workerTimers.setTimeout(() => { - // delay to allow for world cache to update - $app.sharedFeed.pendingUpdate = true; - $app.updateSharedFeed(false); - }, 100); - return args; - }) - .catch((err) => { - console.error(err); - }); - } - } - } - }); - wristFeed.sort(function (a, b) { - if (a.created_at < b.created_at) { - return 1; - } - if (a.created_at > b.created_at) { - return -1; - } - return 0; + $app.methods.fetchActiveFriend = function (userId) { + this.pendingActiveFriends.add(userId); + // FIXME: handle error + return API.getUser({ + userId + }).then((args) => { + this.pendingActiveFriends.delete(userId); + return args; }); - wristFeed.splice(16); - AppApi.ExecuteVrFeedFunction( - 'wristFeedUpdate', - JSON.stringify(wristFeed) - ); - this.applyUserDialogLocation(); - this.applyWorldDialogInstances(); - this.applyGroupDialogInstances(); - feeds.pendingUpdate = false; }; - $app.methods.updateSharedFeedGameLog = function (forceUpdate) { - // Location, OnPlayerJoined, OnPlayerLeft - var data = this.gameLogSessionTable; - var i = data.length; - if (i > 0) { - if ( - data[i - 1].created_at === - this.sharedFeed.gameLog.lastEntryDate && - forceUpdate === false - ) { - return; - } - this.sharedFeed.gameLog.lastEntryDate = data[i - 1].created_at; - } else { + API.$on('USER:CURRENT', function (args) { + $app.checkActiveFriends(args.json); + }); + + $app.methods.checkActiveFriends = function (ref) { + if ( + Array.isArray(ref.activeFriends) === false || + !this.friendLogInitStatus + ) { return; } - var bias = new Date(Date.now() - 86400000).toJSON(); // 24 hours - var wristArr = []; - var w = 0; - var wristFilter = this.sharedFeedFilters.wrist; - var currentUserLeaveTime = 0; - var locationJoinTime = 0; - for (var i = data.length - 1; i > -1; i--) { - var ctx = data[i]; - if (ctx.created_at < bias) { - break; - } - if (ctx.type === 'Notification') { + for (var userId of ref.activeFriends) { + if (this.pendingActiveFriends.has(userId)) { continue; } - // on Location change remove OnPlayerLeft - if (ctx.type === 'LocationDestination') { - currentUserLeaveTime = Date.parse(ctx.created_at); - var currentUserLeaveTimeOffset = - currentUserLeaveTime + 5 * 1000; - for (var k = w - 1; k > -1; k--) { - var feedItem = wristArr[k]; - if ( - (feedItem.type === 'OnPlayerLeft' || - feedItem.type === 'BlockedOnPlayerLeft' || - feedItem.type === 'MutedOnPlayerLeft') && - Date.parse(feedItem.created_at) >= - currentUserLeaveTime && - Date.parse(feedItem.created_at) <= - currentUserLeaveTimeOffset - ) { - wristArr.splice(k, 1); - w--; - } - } - } - // on Location change remove OnPlayerJoined - if (ctx.type === 'Location') { - locationJoinTime = Date.parse(ctx.created_at); - var locationJoinTimeOffset = locationJoinTime + 20 * 1000; - for (var k = w - 1; k > -1; k--) { - var feedItem = wristArr[k]; - if ( - (feedItem.type === 'OnPlayerJoined' || - feedItem.type === 'BlockedOnPlayerJoined' || - feedItem.type === 'MutedOnPlayerJoined') && - Date.parse(feedItem.created_at) >= locationJoinTime && - Date.parse(feedItem.created_at) <= - locationJoinTimeOffset - ) { - wristArr.splice(k, 1); - w--; - } - } - } - // remove current user - if ( - (ctx.type === 'OnPlayerJoined' || - ctx.type === 'OnPlayerLeft' || - ctx.type === 'PortalSpawn') && - ctx.displayName === API.currentUser.displayName - ) { + var user = API.cachedUsers.get(userId); + if (typeof user !== 'undefined' && user.status !== 'offline') { continue; } - var isFriend = false; - var isFavorite = false; - if (ctx.userId) { - isFriend = this.friends.has(ctx.userId); - isFavorite = this.localFavoriteFriends.has(ctx.userId); - } else if (ctx.displayName) { - for (var ref of API.cachedUsers.values()) { - if (ref.displayName === ctx.displayName) { - isFriend = this.friends.has(ref.id); - isFavorite = this.localFavoriteFriends.has(ref.id); - break; - } - } - } - // add tag colour - var tagColour = ''; - if (ctx.userId) { - var tagRef = this.customUserTags.get(ctx.userId); - if (typeof tagRef !== 'undefined') { - tagColour = tagRef.colour; - } - } - // BlockedOnPlayerJoined, BlockedOnPlayerLeft, MutedOnPlayerJoined, MutedOnPlayerLeft - if (ctx.type === 'OnPlayerJoined' || ctx.type === 'OnPlayerLeft') { - for (var ref of API.cachedPlayerModerations.values()) { - if ( - ref.targetDisplayName !== ctx.displayName && - ref.sourceUserId !== ctx.userId - ) { - continue; - } - - if (ref.type === 'block') { - var type = `Blocked${ctx.type}`; - } else if (ref.type === 'mute') { - var type = `Muted${ctx.type}`; - } else { - continue; - } - - var entry = { - created_at: ctx.created_at, - type, - displayName: ref.targetDisplayName, - userId: ref.targetUserId, - isFriend, - isFavorite - }; - if ( - wristFilter[type] && - (wristFilter[type] === 'Everyone' || - (wristFilter[type] === 'Friends' && isFriend) || - (wristFilter[type] === 'VIP' && isFavorite)) - ) { - wristArr.unshift(entry); - } - this.queueGameLogNoty(entry); - } - } - // when too many user joins happen at once when switching instances - // the "w" counter maxes out and wont add any more entries - // until the onJoins are cleared by "Location" - // e.g. if a "VideoPlay" occurs between "OnPlayerJoined" and "Location" it wont be added - if ( - w < 50 && - wristFilter[ctx.type] && - (wristFilter[ctx.type] === 'On' || - wristFilter[ctx.type] === 'Everyone' || - (wristFilter[ctx.type] === 'Friends' && isFriend) || - (wristFilter[ctx.type] === 'VIP' && isFavorite)) - ) { - wristArr.push({ - ...ctx, - tagColour, - isFriend, - isFavorite - }); - ++w; + if (this.pendingActiveFriends.size >= 5) { + break; } + this.fetchActiveFriend(userId); } - this.sharedFeed.gameLog.wrist = wristArr; - this.sharedFeed.pendingUpdate = true; }; - $app.methods.queueGameLogNoty = function (noty) { - // remove join/leave notifications when switching worlds - if ( - noty.type === 'OnPlayerJoined' || - noty.type === 'BlockedOnPlayerJoined' || - noty.type === 'MutedOnPlayerJoined' - ) { - var bias = this.lastLocation.date + 30 * 1000; // 30 secs - if (Date.parse(noty.created_at) <= bias) { - return; - } - } - if ( - noty.type === 'OnPlayerLeft' || - noty.type === 'BlockedOnPlayerLeft' || - noty.type === 'MutedOnPlayerLeft' - ) { - var bias = this.lastLocationDestinationTime + 5 * 1000; // 5 secs - if (Date.parse(noty.created_at) <= bias) { - return; - } - } - if ( - noty.type === 'Notification' || - noty.type === 'LocationDestination' - // skip unused entries - ) { - return; - } - if (noty.type === 'VideoPlay') { - if (!noty.videoName) { - // skip video without name - return; - } - noty.notyName = noty.videoName; - if (noty.displayName) { - // add requester's name to noty - noty.notyName = `${noty.videoName} (${noty.displayName})`; - } - } - if ( - noty.type !== 'VideoPlay' && - noty.displayName === API.currentUser.displayName - ) { - // remove current user - return; - } - noty.isFriend = false; - noty.isFavorite = false; - if (noty.userId) { - noty.isFriend = this.friends.has(noty.userId); - noty.isFavorite = this.localFavoriteFriends.has(noty.userId); - } else if (noty.displayName) { - for (var ref of API.cachedUsers.values()) { - if (ref.displayName === noty.displayName) { - noty.isFriend = this.friends.has(ref.id); - noty.isFavorite = this.localFavoriteFriends.has(ref.id); - break; - } - } - } - var notyFilter = this.sharedFeedFilters.noty; - if ( - notyFilter[noty.type] && - (notyFilter[noty.type] === 'On' || - notyFilter[noty.type] === 'Everyone' || - (notyFilter[noty.type] === 'Friends' && noty.isFriend) || - (notyFilter[noty.type] === 'VIP' && noty.isFavorite)) - ) { - this.playNoty(noty); - } - }; + API.$on('LOGIN', function () { + $app.friends.clear(); + $app.pendingActiveFriends.clear(); + $app.friendsNo = 0; + $app.isVIPFriends = true; + $app.isOnlineFriends = true; + $app.isActiveFriends = true; + $app.isOfflineFriends = false; + $app.vipFriends_ = []; + $app.onlineFriends_ = []; + $app.activeFriends_ = []; + $app.offlineFriends_ = []; + $app.sortVIPFriends = false; + $app.sortOnlineFriends = false; + $app.sortActiveFriends = false; + $app.sortOfflineFriends = false; + }); - $app.methods.updateSharedFeedFeedTable = function (forceUpdate) { - // GPS, Online, Offline, Status, Avatar - var data = this.feedSessionTable; - var i = data.length; - if (i > 0) { - if ( - data[i - 1].created_at === - this.sharedFeed.feedTable.lastEntryDate && - forceUpdate === false - ) { - return; - } - this.sharedFeed.feedTable.lastEntryDate = data[i - 1].created_at; - } else { - return; + API.$on('USER:CURRENT', function (args) { + // USER:CURRENT에서 처리를 함 + $app.refreshFriends(args.ref, args.origin); + $app.updateOnlineFriendCoutner(); + + if ($app.randomUserColours) { + $app.getNameColour(this.currentUser.id).then((colour) => { + this.currentUser.$userColour = colour; + }); } - var bias = new Date(Date.now() - 86400000).toJSON(); // 24 hours - var wristArr = []; - var w = 0; - var wristFilter = this.sharedFeedFilters.wrist; - for (var i = data.length - 1; i > -1; i--) { - var ctx = data[i]; - if (ctx.created_at < bias) { - break; - } - if (ctx.type === 'Avatar') { - continue; - } - // hide private worlds from feed - if ( - this.hidePrivateFromFeed && - ctx.type === 'GPS' && - ctx.location === 'private' - ) { - continue; - } - var isFriend = this.friends.has(ctx.userId); - var isFavorite = this.localFavoriteFriends.has(ctx.userId); - if ( - w < 20 && - wristFilter[ctx.type] && - (wristFilter[ctx.type] === 'Friends' || - (wristFilter[ctx.type] === 'VIP' && isFavorite)) - ) { - wristArr.push({ - ...ctx, - isFriend, - isFavorite - }); - ++w; - } + }); + + 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 }); + }); + + API.$on('LOGIN', function () { + $app.nextFriendsRefresh = 0; + }); + + $app.methods.refreshFriendsList = async function () { + // If we just got user less then 1 min before code call, don't call it again + if (this.nextCurrentUserRefresh < 720) { + await API.getCurrentUser().catch((err) => { + console.error(err); + }); } - this.sharedFeed.feedTable.wrist = wristArr; - this.sharedFeed.pendingUpdate = true; + await API.refreshFriends().catch((err) => { + console.error(err); + }); + API.reconnectWebSocket(); }; - $app.methods.queueFeedNoty = function (noty) { - if (noty.type === 'Avatar') { - return; + $app.methods.refreshFriends = function (ref, origin) { + var map = new Map(); + for (var id of ref.friends) { + map.set(id, 'offline'); } - // hide private worlds from feed - if ( - this.hidePrivateFromFeed && - noty.type === 'GPS' && - noty.location === 'private' - ) { - return; + for (var id of ref.offlineFriends) { + map.set(id, 'offline'); } - noty.isFriend = this.friends.has(noty.userId); - noty.isFavorite = this.localFavoriteFriends.has(noty.userId); - var notyFilter = this.sharedFeedFilters.noty; - if ( - notyFilter[noty.type] && - (notyFilter[noty.type] === 'Everyone' || - (notyFilter[noty.type] === 'Friends' && noty.isFriend) || - (notyFilter[noty.type] === 'VIP' && noty.isFavorite)) - ) { - this.playNoty(noty); + for (var id of ref.activeFriends) { + map.set(id, 'active'); } - }; - - $app.methods.updateSharedFeedNotificationTable = function (forceUpdate) { - // invite, requestInvite, requestInviteResponse, inviteResponse, friendRequest - var { data } = this.notificationTable; - var i = data.length; - if (i > 0) { - if ( - data[i - 1].created_at === - this.sharedFeed.notificationTable.lastEntryDate && - forceUpdate === false - ) { - return; - } - this.sharedFeed.notificationTable.lastEntryDate = - data[i - 1].created_at; - } else { - return; + for (var id of ref.onlineFriends) { + map.set(id, 'online'); } - var bias = new Date(Date.now() - 86400000).toJSON(); // 24 hours - var wristArr = []; - var w = 0; - var wristFilter = this.sharedFeedFilters.wrist; - for (var i = data.length - 1; i > -1; i--) { - var ctx = data[i]; - if (ctx.created_at < bias) { - break; - } - if (ctx.senderUserId === API.currentUser.id) { - continue; + for (var [id, state] of map) { + if (this.friends.has(id)) { + this.updateFriend({ id, state, origin }); + } else { + this.addFriend(id, state); } - var isFriend = this.friends.has(ctx.senderUserId); - var isFavorite = this.localFavoriteFriends.has(ctx.senderUserId); - if ( - w < 20 && - wristFilter[ctx.type] && - (wristFilter[ctx.type] === 'On' || - wristFilter[ctx.type] === 'Friends' || - (wristFilter[ctx.type] === 'VIP' && isFavorite)) - ) { - wristArr.push({ - ...ctx, - isFriend, - isFavorite - }); - ++w; + } + for (var id of this.friends.keys()) { + if (map.has(id) === false) { + this.deleteFriend(id); } } - this.sharedFeed.notificationTable.wrist = wristArr; - this.sharedFeed.pendingUpdate = true; - }; - $app.methods.queueNotificationNoty = function (noty) { - noty.isFriend = this.friends.has(noty.senderUserId); - noty.isFavorite = this.localFavoriteFriends.has(noty.senderUserId); - var notyFilter = this.sharedFeedFilters.noty; - if ( - notyFilter[noty.type] && - (notyFilter[noty.type] === 'On' || - notyFilter[noty.type] === 'Friends' || - (notyFilter[noty.type] === 'VIP' && noty.isFavorite)) - ) { - this.playNoty(noty); - } + this.saveFriendOrder(); }; - $app.methods.updateSharedFeedFriendLogTable = function (forceUpdate) { - // TrustLevel, Friend, FriendRequest, Unfriend, DisplayName - var { data } = this.friendLogTable; - var i = data.length; - if (i > 0) { - if ( - data[i - 1].created_at === - this.sharedFeed.friendLogTable.lastEntryDate && - forceUpdate === false - ) { - return; - } - this.sharedFeed.friendLogTable.lastEntryDate = - data[i - 1].created_at; - } else { + $app.methods.saveFriendOrder = async function () { + var currentTime = Date.now(); + var lastStoreTime = await configRepository.getString( + `VRCX_lastStoreTime_${API.currentUser.id}`, + '' + ); + // store once every week + if (lastStoreTime && currentTime - lastStoreTime < 604800000) { return; } - var bias = new Date(Date.now() - 86400000).toJSON(); // 24 hours - var wristArr = []; - var w = 0; - var wristFilter = this.sharedFeedFilters.wrist; - for (var i = data.length - 1; i > -1; i--) { - var ctx = data[i]; - if (ctx.created_at < bias) { - break; - } - if (ctx.type === 'FriendRequest') { - continue; - } - var isFriend = this.friends.has(ctx.userId); - var isFavorite = this.localFavoriteFriends.has(ctx.userId); - if ( - w < 20 && - wristFilter[ctx.type] && - (wristFilter[ctx.type] === 'On' || - wristFilter[ctx.type] === 'Friends' || - (wristFilter[ctx.type] === 'VIP' && isFavorite)) - ) { - wristArr.push({ - ...ctx, - isFriend, - isFavorite - }); - ++w; + var storedData = {}; + try { + var data = await configRepository.getString( + `VRCX_friendOrder_${API.currentUser.id}` + ); + if (data) { + var storedData = JSON.parse(data); } + } catch (err) { + console.error(err); } - this.sharedFeed.friendLogTable.wrist = wristArr; - this.sharedFeed.pendingUpdate = true; + storedData[currentTime] = Array.from(this.friends.keys()); + await configRepository.setString( + `VRCX_friendOrder_${API.currentUser.id}`, + JSON.stringify(storedData) + ); + await configRepository.setString( + `VRCX_lastStoreTime_${API.currentUser.id}`, + currentTime + ); }; - $app.methods.queueFriendLogNoty = function (noty) { - if (noty.type === 'FriendRequest') { + $app.methods.addFriend = function (id, state) { + if (this.friends.has(id)) { return; } - noty.isFriend = this.friends.has(noty.userId); - noty.isFavorite = this.localFavoriteFriends.has(noty.userId); - var notyFilter = this.sharedFeedFilters.noty; - if ( - notyFilter[noty.type] && - (notyFilter[noty.type] === 'On' || - notyFilter[noty.type] === 'Friends' || - (notyFilter[noty.type] === 'VIP' && noty.isFavorite)) - ) { - this.playNoty(noty); + var ref = API.cachedUsers.get(id); + var isVIP = this.localFavoriteFriends.has(id); + var ctx = { + id, + state: state || 'offline', + isVIP, + ref, + name: '', + no: ++this.friendsNo, + memo: '', + pendingOffline: false, + $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]; + } + } + }); } - }; - - $app.methods.updateSharedFeedModerationAgainstTable = function ( - forceUpdate - ) { - // Unblocked, Blocked, Muted, Unmuted - var data = this.moderationAgainstTable; - var i = data.length; - if (i > 0) { - if ( - data[i - 1].created_at === - this.sharedFeed.moderationAgainstTable.lastEntryDate && - forceUpdate === false - ) { - return; + if (typeof ref === 'undefined') { + var friendLogRef = this.friendLog.get(id); + if (friendLogRef?.displayName) { + ctx.name = friendLogRef.displayName; } - this.sharedFeed.moderationAgainstTable.lastEntryDate = - data[i - 1].created_at; } else { - return; + ctx.name = ref.name; } - var bias = new Date(Date.now() - 86400000).toJSON(); // 24 hours - var wristArr = []; - var w = 0; - var wristFilter = this.sharedFeedFilters.wrist; - for (var i = data.length - 1; i > -1; i--) { - var ctx = data[i]; - if (ctx.created_at < bias) { - break; - } - var isFriend = this.friends.has(ctx.userId); - var isFavorite = this.localFavoriteFriends.has(ctx.userId); - // add tag colour - var tagColour = ''; - var tagRef = this.customUserTags.get(ctx.userId); - if (typeof tagRef !== 'undefined') { - tagColour = tagRef.colour; - } - if ( - w < 20 && - wristFilter[ctx.type] && - wristFilter[ctx.type] === 'On' - ) { - wristArr.push({ - ...ctx, - isFriend, - isFavorite, - tagColour - }); - ++w; + 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; } - this.sharedFeed.moderationAgainstTable.wrist = wristArr; - this.sharedFeed.pendingUpdate = true; }; - $app.methods.queueModerationNoty = function (noty) { - noty.isFriend = false; - noty.isFavorite = false; - if (noty.userId) { - noty.isFriend = this.friends.has(noty.userId); - noty.isFavorite = this.localFavoriteFriends.has(noty.userId); + $app.methods.deleteFriend = function (id) { + var ctx = this.friends.get(id); + if (typeof ctx === 'undefined') { + return; } - var notyFilter = this.sharedFeedFilters.noty; - if (notyFilter[noty.type] && notyFilter[noty.type] === 'On') { - this.playNoty(noty); + 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.data.notyMap = []; + $app.data.updateFriendInProgress = new Map(); - $app.methods.playNoty = function (noty) { - if (API.currentUser.status === 'busy' || !this.friendLogInitStatus) { + $app.methods.updateFriend = function (ctx) { + var { id, state, origin } = ctx; + var stateInput = state; + var ctx = this.friends.get(id); + if (typeof ctx === 'undefined') { return; } - var displayName = ''; - if (noty.displayName) { - displayName = noty.displayName; - } else if (noty.senderUsername) { - displayName = noty.senderUsername; - } else if (noty.sourceDisplayName) { - displayName = noty.sourceDisplayName; - } - if (displayName) { - // don't play noty twice - var notyId = `${noty.type},${displayName}`; - if ( - this.notyMap[notyId] && - this.notyMap[notyId] >= noty.created_at - ) { - return; - } - this.notyMap[notyId] = noty.created_at; - } - var bias = new Date(Date.now() - 60000).toJSON(); - if (noty.created_at < bias) { - // don't play noty if it's over 1min old - return; - } - - var playNotificationTTS = false; - if ( - this.notificationTTS === 'Always' || - (this.notificationTTS === 'Inside VR' && - !this.isGameNoVR && - this.isGameRunning) || - (this.notificationTTS === 'Game Closed' && !this.isGameRunning) || - (this.notificationTTS === 'Game Running' && this.isGameRunning) - ) { - playNotificationTTS = true; - } - var playDesktopToast = false; - if ( - this.desktopToast === 'Always' || - (this.desktopToast === 'Outside VR' && !this.isSteamVRRunning) || - (this.desktopToast === 'Inside VR' && this.isSteamVRRunning) || - (this.desktopToast === 'Game Closed' && !this.isGameRunning) || - (this.desktopToast === 'Game Running' && this.isGameRunning) || - (this.desktopToast === 'Desktop Mode' && - this.isGameNoVR && - this.isGameRunning) || - (this.afkDesktopToast && - this.isHmdAfk && - this.isGameRunning && - !this.isGameNoVR) - ) { - // this if statement looks like it has seen better days - playDesktopToast = true; - } - var playXSNotification = this.xsNotifications; - var playOvrtHudNotifications = this.ovrtHudNotifications; - var playOvrtWristNotifications = this.ovrtWristNotifications; - var playOverlayNotification = false; - if ( - this.overlayNotifications && - !this.isGameNoVR && - this.isGameRunning - ) { - playOverlayNotification = true; - } - var message = ''; - if (noty.title) { - message = `${noty.title}, ${noty.message}`; - } else if (noty.message) { - message = noty.message; - } - var messageList = [ - 'inviteMessage', - 'requestMessage', - 'responseMessage' - ]; - for (var k = 0; k < messageList.length; k++) { - if ( - typeof noty.details !== 'undefined' && - typeof noty.details[messageList[k]] !== 'undefined' - ) { - message = `, ${noty.details[messageList[k]]}`; - } + if (stateInput === 'online') { + this.APILastOnline.set(id, Date.now()); + ctx.pendingOffline = false; } - if (playNotificationTTS) { - this.playNotyTTS(noty, message); + var ref = API.cachedUsers.get(id); + var isVIP = this.localFavoriteFriends.has(id); + var location = ''; + var $location_at = ''; + if (typeof ref !== 'undefined') { + var { location, $location_at } = ref; } - if ( - playDesktopToast || - playXSNotification || - playOvrtHudNotifications || - playOvrtWristNotifications || - playOverlayNotification - ) { - if (this.imageNotifications) { - this.notySaveImage(noty).then((image) => { - if (playXSNotification) { - this.displayXSNotification(noty, message, image); + 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) { + API.getUser({ + userId: id + }); } - if ( - playOvrtHudNotifications || - playOvrtWristNotifications - ) { - this.displayOvrtNotification( - playOvrtHudNotifications, - playOvrtWristNotifications, - noty, - message, - image - ); + if (ctx.isVIP) { + this.sortVIPFriends = true; + } else { + this.sortOnlineFriends = true; } - if (playDesktopToast) { - this.displayDesktopToast(noty, message, image); + } + } + 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 (playOverlayNotification) { - this.displayOverlayNotification(noty, message, image); + } + } + 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 ( + origin && + ctx.state !== 'online' && + typeof ref !== 'undefined' && + this.isRealInstance(ref.location) + ) { + API.getUser({ + userId: id }); - } else { - if (playXSNotification) { - this.displayXSNotification(noty, message, ''); - } - if (playOvrtHudNotifications || playOvrtWristNotifications) { - this.displayOvrtNotification( - playOvrtHudNotifications, - playOvrtWristNotifications, - noty, - message, - '' + } + } else if ( + ctx.state === 'online' && + (stateInput === 'active' || stateInput === 'offline') + ) { + ctx.ref = ref; + ctx.isVIP = isVIP; + if (typeof ref !== 'undefined') { + ctx.name = ref.displayName; + } + // delayed second check to prevent status flapping + var date = this.updateFriendInProgress.get(id); + if (date && date > Date.now() - this.pendingOfflineDelay + 5000) { + // check if already waiting + if (this.debugFriendState) { + console.log( + ctx.name, + new Date().toJSON(), + 'pendingOfflineCheck', + stateInput, + ctx.state ); } - if (playDesktopToast) { - this.displayDesktopToast(noty, message, ''); - } - if (playOverlayNotification) { - this.displayOverlayNotification(noty, message, ''); - } + return; } - } - }; - - $app.methods.notyGetImage = async function (noty) { - var imageUrl = ''; - var userId = ''; - if (noty.userId) { - userId = noty.userId; - } else if (noty.senderUserId) { - userId = noty.senderUserId; - } else if (noty.sourceUserId) { - userId = noty.sourceUserId; - } else if (noty.displayName) { - for (var ref of API.cachedUsers.values()) { - if (ref.displayName === noty.displayName) { - userId = ref.id; - break; - } + ctx.pendingOffline = true; + this.updateFriendInProgress.set(id, Date.now()); + // wait 2minutes then check if user came back online + workerTimers.setTimeout(() => { + ctx.pendingOffline = false; + this.updateFriendInProgress.delete(id); + this.updateFriendDelayedCheck( + id, + ctx, + stateInput, + location, + $location_at + ); + }, this.pendingOfflineDelay); + } else { + ctx.ref = ref; + ctx.isVIP = isVIP; + if (typeof ref !== 'undefined') { + ctx.name = ref.displayName; } + this.updateFriendDelayedCheck( + id, + ctx, + stateInput, + location, + $location_at + ); } - if (noty.thumbnailImageUrl) { - imageUrl = noty.thumbnailImageUrl; - } else if (noty.details && noty.details.imageUrl) { - imageUrl = noty.details.imageUrl; - } else if (noty.imageUrl) { - imageUrl = noty.imageUrl; - } else if (userId && !userId.startsWith('grp_')) { - imageUrl = await API.getCachedUser({ - userId - }) - .catch((err) => { - console.error(err); - return ''; - }) - .then((args) => { - if (!args.json) { - return ''; - } - if ( - this.displayVRCPlusIconsAsAvatar && - args.json.userIcon - ) { - return args.json.userIcon; - } - if (args.json.profilePicOverride) { - return args.json.profilePicOverride; - } - return args.json.currentAvatarThumbnailImageUrl; - }); - } - return imageUrl; }; - $app.methods.notySaveImage = async function (noty) { - var imageUrl = await this.notyGetImage(noty); - var fileId = extractFileId(imageUrl); - var fileVersion = extractFileVersion(imageUrl); - var imageLocation = ''; - try { - if (fileId && fileVersion) { - imageLocation = await AppApi.GetImage( - imageUrl, - fileId, - fileVersion - ); - } else if (imageUrl) { - fileVersion = imageUrl.split('/').pop(); // 1416226261.thumbnail-500.png - fileId = fileVersion.split('.').shift(); // 1416226261 - imageLocation = await AppApi.GetImage( - imageUrl, - fileId, - fileVersion + $app.methods.updateFriendDelayedCheck = async function ( + id, + ctx, + newState, + location, + $location_at + ) { + var date = this.APILastOnline.get(id); + if ( + ctx.state === 'online' && + (newState === 'active' || newState === 'offline') && + date && + date > Date.now() - 120000 + ) { + if (this.debugFriendState) { + console.log( + `falsePositiveOffline ${ctx.name} currentState:${ctx.ref.state} expectedState:${newState}` ); } - } catch (err) { - console.error(imageUrl, err); + return; } - return imageLocation; - }; - - $app.methods.displayOverlayNotification = function ( - noty, - message, - imageFile - ) { - var image = ''; - if (imageFile) { - image = `file:///${imageFile}`; + if (this.debugFriendState) { + console.log(ctx.name, 'updateFriendState', newState, ctx.state); } - AppApi.ExecuteVrOverlayFunction( - 'playNoty', - JSON.stringify({ noty, message, image }) - ); - }; - - $app.methods.playNotyTTS = function (noty, message) { - switch (noty.type) { - case 'OnPlayerJoined': - this.speak(`${noty.displayName} has joined`); - break; - case 'OnPlayerLeft': - this.speak(`${noty.displayName} has left`); - break; - case 'OnPlayerJoining': - this.speak(`${noty.displayName} is joining`); - break; - case 'GPS': - this.speak( - `${noty.displayName} is in ${this.displayLocation( - noty.location, - noty.worldName, - noty.groupName - )}` - ); - break; - case 'Online': - var locationName = ''; - if (noty.worldName) { - locationName = ` to ${this.displayLocation( - noty.location, - noty.worldName, - noty.groupName - )}`; - } - this.speak(`${noty.displayName} has logged in${locationName}`); - break; - case 'Offline': - this.speak(`${noty.displayName} has logged out`); - break; - case 'Status': - this.speak( - `${noty.displayName} status is now ${noty.status} ${noty.statusDescription}` - ); - break; - case 'invite': - this.speak( - `${ - noty.senderUsername - } has invited you to ${this.displayLocation( - noty.details.worldId, - noty.details.worldName, - noty.groupName - )}${message}` - ); - break; - case 'requestInvite': - this.speak( - `${noty.senderUsername} has requested an invite${message}` - ); - break; - case 'inviteResponse': - this.speak( - `${noty.senderUsername} has responded to your invite${message}` - ); - break; - case 'requestInviteResponse': - this.speak( - `${noty.senderUsername} has responded to your invite request${message}` - ); - break; - case 'friendRequest': - this.speak( - `${noty.senderUsername} has sent you a friend request` - ); - break; - case 'Friend': - this.speak(`${noty.displayName} is now your friend`); - break; - case 'Unfriend': - this.speak(`${noty.displayName} is no longer your friend`); - break; - case 'TrustLevel': - this.speak( - `${noty.displayName} trust level is now ${noty.trustLevel}` - ); - break; - case 'DisplayName': - this.speak( - `${noty.previousDisplayName} changed their name to ${noty.displayName}` - ); - break; - case 'boop': - this.speak(noty.message); - break; - case 'groupChange': - this.speak(`${noty.senderUsername} ${noty.message}`); - break; - case 'group.announcement': - this.speak(noty.message); - break; - case 'group.informative': - this.speak(noty.message); - break; - case 'group.invite': - this.speak(noty.message); - break; - case 'group.joinRequest': - this.speak(noty.message); - break; - case 'group.transfer': - this.speak(noty.message); - break; - case 'group.queueReady': - this.speak(noty.message); - break; - case 'instance.closed': - this.speak(noty.message); - break; - case 'PortalSpawn': - if (noty.displayName) { - this.speak( - `${ - noty.displayName - } has spawned a portal to ${this.displayLocation( - noty.instanceId, - noty.worldName, - noty.groupName - )}` - ); - } else { - this.speak('User has spawned a portal'); + 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(); } - break; - case 'AvatarChange': - this.speak( - `${noty.displayName} changed into avatar ${noty.name}` - ); - break; - case 'ChatBoxMessage': - this.speak(`${noty.displayName} said ${noty.text}`); - break; - case 'Event': - this.speak(noty.data); - break; - case 'External': - this.speak(noty.message); - break; - case 'VideoPlay': - this.speak(`Now playing: ${noty.notyName}`); - break; - case 'BlockedOnPlayerJoined': - this.speak(`Blocked user ${noty.displayName} has joined`); - break; - case 'BlockedOnPlayerLeft': - this.speak(`Blocked user ${noty.displayName} has left`); - break; - case 'MutedOnPlayerJoined': - this.speak(`Muted user ${noty.displayName} has joined`); - break; - case 'MutedOnPlayerLeft': - this.speak(`Muted user ${noty.displayName} has left`); - break; - case 'Blocked': - this.speak(`${noty.displayName} has blocked you`); - break; - case 'Unblocked': - this.speak(`${noty.displayName} has unblocked you`); - break; - case 'Muted': - this.speak(`${noty.displayName} has muted you`); - break; - case 'Unmuted': - this.speak(`${noty.displayName} has unmuted you`); - break; - } - }; - - $app.methods.displayXSNotification = function (noty, message, image) { - var timeout = Math.floor(parseInt(this.notificationTimeout, 10) / 1000); - switch (noty.type) { - case 'OnPlayerJoined': - AppApi.XSNotification( - 'VRCX', - `${noty.displayName} has joined`, - timeout, - image - ); - break; - case 'OnPlayerLeft': - AppApi.XSNotification( - 'VRCX', - `${noty.displayName} has left`, - timeout, - image - ); - break; - case 'OnPlayerJoining': - AppApi.XSNotification( - 'VRCX', - `${noty.displayName} is joining`, - timeout, - image - ); - break; - case 'GPS': - AppApi.XSNotification( - 'VRCX', - `${noty.displayName} is in ${this.displayLocation( - noty.location, - noty.worldName, - noty.groupName - )}`, - timeout, - image - ); - break; - case 'Online': - var locationName = ''; - if (noty.worldName) { - locationName = ` to ${this.displayLocation( - noty.location, - noty.worldName, - noty.groupName - )}`; - } - AppApi.XSNotification( - 'VRCX', - `${noty.displayName} has logged in${locationName}`, - timeout, - image - ); - break; - case 'Offline': - AppApi.XSNotification( - 'VRCX', - `${noty.displayName} has logged out`, - timeout, - image - ); - break; - case 'Status': - AppApi.XSNotification( - 'VRCX', - `${noty.displayName} status is now ${noty.status} ${noty.statusDescription}`, - timeout, - image - ); - break; - case 'invite': - AppApi.XSNotification( - 'VRCX', - `${ - noty.senderUsername - } has invited you to ${this.displayLocation( - noty.details.worldId, - noty.details.worldName - )}${message}`, - timeout, - image - ); - break; - case 'requestInvite': - AppApi.XSNotification( - 'VRCX', - `${noty.senderUsername} has requested an invite${message}`, - timeout, - image - ); - break; - case 'inviteResponse': - AppApi.XSNotification( - 'VRCX', - `${noty.senderUsername} has responded to your invite${message}`, - timeout, - image - ); - break; - case 'requestInviteResponse': - AppApi.XSNotification( - 'VRCX', - `${noty.senderUsername} has responded to your invite request${message}`, - timeout, - image - ); - break; - case 'friendRequest': - AppApi.XSNotification( - 'VRCX', - `${noty.senderUsername} has sent you a friend request`, - timeout, - image - ); - break; - case 'Friend': - AppApi.XSNotification( - 'VRCX', - `${noty.displayName} is now your friend`, - timeout, - image - ); - break; - case 'Unfriend': - AppApi.XSNotification( - 'VRCX', - `${noty.displayName} is no longer your friend`, - timeout, - image - ); - break; - case 'TrustLevel': - AppApi.XSNotification( - 'VRCX', - `${noty.displayName} trust level is now ${noty.trustLevel}`, - timeout, - image - ); - break; - case 'DisplayName': - AppApi.XSNotification( - 'VRCX', - `${noty.previousDisplayName} changed their name to ${noty.displayName}`, - timeout, - image - ); - break; - case 'boop': - AppApi.XSNotification('VRCX', noty.message, timeout, image); - break; - case 'groupChange': - AppApi.XSNotification( - 'VRCX', - `${noty.senderUsername}: ${noty.message}`, - timeout, - image - ); - break; - case 'group.announcement': - AppApi.XSNotification('VRCX', noty.message, timeout, image); - break; - case 'group.informative': - AppApi.XSNotification('VRCX', noty.message, timeout, image); - break; - case 'group.invite': - AppApi.XSNotification('VRCX', noty.message, timeout, image); - break; - case 'group.joinRequest': - AppApi.XSNotification('VRCX', noty.message, timeout, image); - break; - case 'group.transfer': - AppApi.XSNotification('VRCX', noty.message, timeout, image); - break; - case 'group.queueReady': - AppApi.XSNotification('VRCX', noty.message, timeout, image); - break; - case 'instance.closed': - AppApi.XSNotification('VRCX', noty.message, timeout, image); - break; - case 'PortalSpawn': - if (noty.displayName) { - AppApi.XSNotification( - 'VRCX', - `${ - noty.displayName - } has spawned a portal to ${this.displayLocation( - noty.instanceId, - noty.worldName, - noty.groupName - )}`, - timeout, - image - ); - } else { - AppApi.XSNotification( - 'VRCX', - 'User has spawned a portal', - timeout, - image - ); - } - break; - case 'AvatarChange': - AppApi.XSNotification( - 'VRCX', - `${noty.displayName} changed into avatar ${noty.name}`, - timeout, - image - ); - break; - case 'ChatBoxMessage': - AppApi.XSNotification( - 'VRCX', - `${noty.displayName} said ${noty.text}`, - timeout, - image - ); - break; - case 'Event': - AppApi.XSNotification('VRCX', noty.data, timeout, image); - break; - case 'External': - AppApi.XSNotification('VRCX', noty.message, timeout, image); - break; - case 'VideoPlay': - AppApi.XSNotification( - 'VRCX', - `Now playing: ${noty.notyName}`, - timeout, - image - ); - break; - case 'BlockedOnPlayerJoined': - AppApi.XSNotification( - 'VRCX', - `Blocked user ${noty.displayName} has joined`, - timeout, - image - ); - break; - case 'BlockedOnPlayerLeft': - AppApi.XSNotification( - 'VRCX', - `Blocked user ${noty.displayName} has left`, - timeout, - image - ); - break; - case 'MutedOnPlayerJoined': - AppApi.XSNotification( - 'VRCX', - `Muted user ${noty.displayName} has joined`, - timeout, - image - ); - break; - case 'MutedOnPlayerLeft': - AppApi.XSNotification( - 'VRCX', - `Muted user ${noty.displayName} has left`, - timeout, - image - ); - break; - case 'Blocked': - AppApi.XSNotification( - 'VRCX', - `${noty.displayName} has blocked you`, - timeout, - image - ); - break; - case 'Unblocked': - AppApi.XSNotification( - 'VRCX', - `${noty.displayName} has unblocked you`, - timeout, - image - ); - break; - case 'Muted': - AppApi.XSNotification( - 'VRCX', - `${noty.displayName} has muted you`, - timeout, - image - ); - break; - case 'Unmuted': - AppApi.XSNotification( - 'VRCX', - `${noty.displayName} has unmuted you`, - timeout, - image - ); - break; + 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(ref.location); + var groupName = await this.getGroupName(location); + var feed = { + created_at: new Date().toJSON(), + type: 'Online', + userId: id, + displayName: ctx.name, + location: ref.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; + ctx.ref.state = newState; + ctx.name = ref.displayName; + ctx.isVIP = isVIP; }; - $app.methods.displayOvrtNotification = function ( - playOvrtHudNotifications, - playOvrtWristNotifications, - noty, - message, - image - ) { - var timeout = Math.floor(parseInt(this.notificationTimeout, 10) / 1000); - switch (noty.type) { - case 'OnPlayerJoined': - AppApi.OVRTNotification( - playOvrtHudNotifications, - playOvrtWristNotifications, - 'VRCX', - `${noty.displayName} has joined`, - timeout, - image - ); - break; - case 'OnPlayerLeft': - AppApi.OVRTNotification( - playOvrtHudNotifications, - playOvrtWristNotifications, - 'VRCX', - `${noty.displayName} has left`, - timeout, - image - ); - break; - case 'OnPlayerJoining': - AppApi.OVRTNotification( - playOvrtHudNotifications, - playOvrtWristNotifications, - 'VRCX', - `${noty.displayName} is joining`, - timeout, - image - ); - break; - case 'GPS': - AppApi.OVRTNotification( - playOvrtHudNotifications, - playOvrtWristNotifications, - 'VRCX', - `${noty.displayName} is in ${this.displayLocation( - noty.location, - noty.worldName, - noty.groupName - )}`, - timeout, - image - ); - break; - case 'Online': - var locationName = ''; - if (noty.worldName) { - locationName = ` to ${this.displayLocation( - noty.location, - noty.worldName, - noty.groupName - )}`; - } - AppApi.OVRTNotification( - playOvrtHudNotifications, - playOvrtWristNotifications, - 'VRCX', - `${noty.displayName} has logged in${locationName}`, - timeout, - image - ); - break; - case 'Offline': - AppApi.OVRTNotification( - playOvrtHudNotifications, - playOvrtWristNotifications, - 'VRCX', - `${noty.displayName} has logged out`, - timeout, - image - ); - break; - case 'Status': - AppApi.OVRTNotification( - playOvrtHudNotifications, - playOvrtWristNotifications, - 'VRCX', - `${noty.displayName} status is now ${noty.status} ${noty.statusDescription}`, - timeout, - image - ); - break; - case 'invite': - AppApi.OVRTNotification( - playOvrtHudNotifications, - playOvrtWristNotifications, - 'VRCX', - `${ - noty.senderUsername - } has invited you to ${this.displayLocation( - noty.details.worldId, - noty.details.worldName - )}${message}`, - timeout, - image - ); - break; - case 'requestInvite': - AppApi.OVRTNotification( - playOvrtHudNotifications, - playOvrtWristNotifications, - 'VRCX', - `${noty.senderUsername} has requested an invite${message}`, - timeout, - image - ); - break; - case 'inviteResponse': - AppApi.OVRTNotification( - playOvrtHudNotifications, - playOvrtWristNotifications, - 'VRCX', - `${noty.senderUsername} has responded to your invite${message}`, - timeout, - image - ); - break; - case 'requestInviteResponse': - AppApi.OVRTNotification( - playOvrtHudNotifications, - playOvrtWristNotifications, - 'VRCX', - `${noty.senderUsername} has responded to your invite request${message}`, - timeout, - image - ); - break; - case 'friendRequest': - AppApi.OVRTNotification( - playOvrtHudNotifications, - playOvrtWristNotifications, - 'VRCX', - `${noty.senderUsername} has sent you a friend request`, - timeout, - image - ); - break; - case 'Friend': - AppApi.OVRTNotification( - playOvrtHudNotifications, - playOvrtWristNotifications, - 'VRCX', - `${noty.displayName} is now your friend`, - timeout, - image - ); - break; - case 'Unfriend': - AppApi.OVRTNotification( - playOvrtHudNotifications, - playOvrtWristNotifications, - 'VRCX', - `${noty.displayName} is no longer your friend`, - timeout, - image - ); - break; - case 'TrustLevel': - AppApi.OVRTNotification( - playOvrtHudNotifications, - playOvrtWristNotifications, - 'VRCX', - `${noty.displayName} trust level is now ${noty.trustLevel}`, - timeout, - image - ); - break; - case 'DisplayName': - AppApi.OVRTNotification( - playOvrtHudNotifications, - playOvrtWristNotifications, - 'VRCX', - `${noty.previousDisplayName} changed their name to ${noty.displayName}`, - timeout, - image - ); - break; - case 'boop': - AppApi.OVRTNotification( - playOvrtHudNotifications, - playOvrtWristNotifications, - 'VRCX', - noty.message, - timeout, - image - ); - break; - case 'groupChange': - AppApi.OVRTNotification( - playOvrtHudNotifications, - playOvrtWristNotifications, - 'VRCX', - `${noty.senderUsername}: ${noty.message}`, - timeout, - image - ); - break; - case 'group.announcement': - AppApi.OVRTNotification( - playOvrtHudNotifications, - playOvrtWristNotifications, - 'VRCX', - noty.message, - timeout, - image - ); - break; - case 'group.informative': - AppApi.OVRTNotification( - playOvrtHudNotifications, - playOvrtWristNotifications, - 'VRCX', - noty.message, - timeout, - image - ); - break; - case 'group.invite': - AppApi.OVRTNotification( - playOvrtHudNotifications, - playOvrtWristNotifications, - 'VRCX', - noty.message, - timeout, - image - ); - break; - case 'group.joinRequest': - AppApi.OVRTNotification( - playOvrtHudNotifications, - playOvrtWristNotifications, - 'VRCX', - noty.message, - timeout, - image - ); - break; - case 'group.transfer': - AppApi.OVRTNotification( - playOvrtHudNotifications, - playOvrtWristNotifications, - 'VRCX', - noty.message, - timeout, - image - ); - break; - case 'group.queueReady': - AppApi.OVRTNotification( - playOvrtHudNotifications, - playOvrtWristNotifications, - 'VRCX', - noty.message, - timeout, - image - ); - break; - case 'instance.closed': - AppApi.OVRTNotification( - playOvrtHudNotifications, - playOvrtWristNotifications, - 'VRCX', - noty.message, - timeout, - image - ); - break; - case 'PortalSpawn': - if (noty.displayName) { - AppApi.OVRTNotification( - playOvrtHudNotifications, - playOvrtWristNotifications, - 'VRCX', - `${ - noty.displayName - } has spawned a portal to ${this.displayLocation( - noty.instanceId, - noty.worldName, - noty.groupName - )}`, - timeout, - image - ); - } else { - AppApi.OVRTNotification( - playOvrtHudNotifications, - playOvrtWristNotifications, - 'VRCX', - 'User has spawned a portal', - timeout, - image - ); + $app.methods.getWorldName = async function (location) { + var worldName = ''; + if (this.isRealInstance(location)) { + try { + var L = $utils.parseLocation(location); + if (L.worldId) { + var args = await API.getCachedWorld({ + worldId: L.worldId + }); + worldName = args.ref.name; } - break; - case 'AvatarChange': - AppApi.OVRTNotification( - playOvrtHudNotifications, - playOvrtWristNotifications, - 'VRCX', - `${noty.displayName} changed into avatar ${noty.name}`, - timeout, - image - ); - break; - case 'ChatBoxMessage': - AppApi.OVRTNotification( - playOvrtHudNotifications, - playOvrtWristNotifications, - 'VRCX', - `${noty.displayName} said ${noty.text}`, - timeout, - image - ); - break; - case 'Event': - AppApi.OVRTNotification( - playOvrtHudNotifications, - playOvrtWristNotifications, - 'VRCX', - noty.data, - timeout, - image - ); - break; - case 'External': - AppApi.OVRTNotification( - playOvrtHudNotifications, - playOvrtWristNotifications, - 'VRCX', - noty.message, - timeout, - image - ); - break; - case 'VideoPlay': - AppApi.OVRTNotification( - playOvrtHudNotifications, - playOvrtWristNotifications, - 'VRCX', - `Now playing: ${noty.notyName}`, - timeout, - image - ); - break; - case 'BlockedOnPlayerJoined': - AppApi.OVRTNotification( - playOvrtHudNotifications, - playOvrtWristNotifications, - 'VRCX', - `Blocked user ${noty.displayName} has joined`, - timeout, - image - ); - break; - case 'BlockedOnPlayerLeft': - AppApi.OVRTNotification( - playOvrtHudNotifications, - playOvrtWristNotifications, - 'VRCX', - `Blocked user ${noty.displayName} has left`, - timeout, - image - ); - break; - case 'MutedOnPlayerJoined': - AppApi.OVRTNotification( - playOvrtHudNotifications, - playOvrtWristNotifications, - 'VRCX', - `Muted user ${noty.displayName} has joined`, - timeout, - image - ); - break; - case 'MutedOnPlayerLeft': - AppApi.OVRTNotification( - playOvrtHudNotifications, - playOvrtWristNotifications, - 'VRCX', - `Muted user ${noty.displayName} has left`, - timeout, - image - ); - break; - case 'Blocked': - AppApi.OVRTNotification( - playOvrtHudNotifications, - playOvrtWristNotifications, - 'VRCX', - `${noty.displayName} has blocked you`, - timeout, - image - ); - break; - case 'Unblocked': - AppApi.OVRTNotification( - playOvrtHudNotifications, - playOvrtWristNotifications, - 'VRCX', - `${noty.displayName} has unblocked you`, - timeout, - image - ); - break; - case 'Muted': - AppApi.OVRTNotification( - playOvrtHudNotifications, - playOvrtWristNotifications, - 'VRCX', - `${noty.displayName} has muted you`, - timeout, - image - ); - break; - case 'Unmuted': - AppApi.OVRTNotification( - playOvrtHudNotifications, - playOvrtWristNotifications, - 'VRCX', - `${noty.displayName} has unmuted you`, - timeout, - image - ); - break; + } catch (err) {} } + return worldName; }; - $app.methods.displayDesktopToast = function (noty, message, image) { - switch (noty.type) { - case 'OnPlayerJoined': - AppApi.DesktopNotification( - noty.displayName, - 'has joined', - image - ); - break; - case 'OnPlayerLeft': - AppApi.DesktopNotification(noty.displayName, 'has left', image); - break; - case 'OnPlayerJoining': - AppApi.DesktopNotification( - noty.displayName, - 'is joining', - image - ); - break; - case 'GPS': - AppApi.DesktopNotification( - noty.displayName, - `is in ${this.displayLocation( - noty.location, - noty.worldName, - noty.groupName - )}`, - image - ); - break; - case 'Online': - var locationName = ''; - if (noty.worldName) { - locationName = ` to ${this.displayLocation( - noty.location, - noty.worldName, - noty.groupName - )}`; - } - AppApi.DesktopNotification( - noty.displayName, - `has logged in${locationName}`, - image - ); - break; - case 'Offline': - AppApi.DesktopNotification( - noty.displayName, - 'has logged out', - image - ); - break; - case 'Status': - AppApi.DesktopNotification( - noty.displayName, - `status is now ${noty.status} ${noty.statusDescription}`, - image - ); - break; - case 'invite': - AppApi.DesktopNotification( - noty.senderUsername, - `has invited you to ${this.displayLocation( - noty.details.worldId, - noty.details.worldName - )}${message}`, - image - ); - break; - case 'requestInvite': - AppApi.DesktopNotification( - noty.senderUsername, - `has requested an invite${message}`, - image - ); - break; - case 'inviteResponse': - AppApi.DesktopNotification( - noty.senderUsername, - `has responded to your invite${message}`, - image - ); - break; - case 'requestInviteResponse': - AppApi.DesktopNotification( - noty.senderUsername, - `has responded to your invite request${message}`, - image - ); - break; - case 'friendRequest': - AppApi.DesktopNotification( - noty.senderUsername, - 'has sent you a friend request', - image - ); - break; - case 'Friend': - AppApi.DesktopNotification( - noty.displayName, - 'is now your friend', - image - ); - break; - case 'Unfriend': - AppApi.DesktopNotification( - noty.displayName, - 'is no longer your friend', - image - ); - break; - case 'TrustLevel': - AppApi.DesktopNotification( - noty.displayName, - `trust level is now ${noty.trustLevel}`, - image - ); - break; - case 'DisplayName': - AppApi.DesktopNotification( - noty.previousDisplayName, - `changed their name to ${noty.displayName}`, - image - ); - break; - case 'boop': - AppApi.DesktopNotification( - noty.senderUsername, - noty.message, - image - ); - break; - case 'groupChange': - AppApi.DesktopNotification( - noty.senderUsername, - noty.message, - image - ); - break; - case 'group.announcement': - AppApi.DesktopNotification( - 'Group Announcement', - noty.message, - image - ); - break; - case 'group.informative': - AppApi.DesktopNotification( - 'Group Informative', - noty.message, - image - ); - break; - case 'group.invite': - AppApi.DesktopNotification('Group Invite', noty.message, image); - break; - case 'group.joinRequest': - AppApi.DesktopNotification( - 'Group Join Request', - noty.message, - image - ); - break; - case 'group.transfer': - AppApi.DesktopNotification( - 'Group Transfer Request', - noty.message, - image - ); - break; - case 'group.queueReady': - AppApi.DesktopNotification( - 'Instance Queue Ready', - noty.message, - image - ); - break; - case 'instance.closed': - AppApi.DesktopNotification( - 'Instance Closed', - noty.message, - image - ); - break; - case 'PortalSpawn': - if (noty.displayName) { - AppApi.DesktopNotification( - noty.displayName, - `has spawned a portal to ${this.displayLocation( - noty.instanceId, - noty.worldName, - noty.groupName - )}`, - image - ); - } else { - AppApi.DesktopNotification( - '', - 'User has spawned a portal', - image - ); - } - break; - case 'AvatarChange': - AppApi.DesktopNotification( - noty.displayName, - `changed into avatar ${noty.name}`, - image - ); - break; - case 'ChatBoxMessage': - AppApi.DesktopNotification( - noty.displayName, - `said ${noty.text}`, - image - ); - break; - case 'Event': - AppApi.DesktopNotification('Event', noty.data, image); - break; - case 'External': - AppApi.DesktopNotification('External', noty.message, image); - break; - case 'VideoPlay': - AppApi.DesktopNotification('Now playing', noty.notyName, image); - break; - case 'BlockedOnPlayerJoined': - AppApi.DesktopNotification( - noty.displayName, - 'blocked user has joined', - image - ); - break; - case 'BlockedOnPlayerLeft': - AppApi.DesktopNotification( - noty.displayName, - 'blocked user has left', - image - ); - break; - case 'MutedOnPlayerJoined': - AppApi.DesktopNotification( - noty.displayName, - 'muted user has joined', - image - ); - break; - case 'MutedOnPlayerLeft': - AppApi.DesktopNotification( - noty.displayName, - 'muted user has left', - image - ); - break; - case 'Blocked': - AppApi.DesktopNotification( - noty.displayName, - 'has blocked you', - image - ); - break; - case 'Unblocked': - AppApi.DesktopNotification( - noty.displayName, - 'has unblocked you', - image - ); - break; - case 'Muted': - AppApi.DesktopNotification( - noty.displayName, - 'has muted you', - image - ); - break; - case 'Unmuted': - AppApi.DesktopNotification( - noty.displayName, - 'has unmuted you', - image - ); - break; + $app.methods.getGroupName = async function (data) { + if (!data) { + return ''; } - }; - - $app.methods.displayLocation = function (location, worldName, groupName) { - var text = worldName; - var L = API.parseLocation(location); - if (L.isOffline) { - text = 'Offline'; - } else if (L.isPrivate) { - text = 'Private'; - } else if (L.isTraveling) { - text = 'Traveling'; - } else if (L.worldId) { - if (groupName) { - text = `${worldName} ${L.accessTypeName}(${groupName})`; - } else if (L.instanceId) { - text = `${worldName} ${L.accessTypeName}`; + var groupName = ''; + var groupId = data; + if (!data.startsWith('grp_')) { + var L = $utils.parseLocation(data); + groupId = L.groupId; + if (!L.groupId) { + return ''; } } - return text; + try { + var args = await API.getCachedGroup({ + groupId + }); + groupName = args.ref.name; + } catch (err) {} + return groupName; }; - $app.methods.notifyMenu = function (index) { - var { menu } = this.$refs; - if (menu.activeIndex !== index) { - var item = menu.items[index]; - if (item) { - item.$el.classList.add('notify'); - } + $app.methods.updateFriendGPS = function (userId) { + var ctx = this.friends.get(userId); + if (ctx.isVIP) { + this.sortVIPFriends = true; + } else { + this.sortOnlineFriends = true; } }; - $app.methods.selectMenu = function (index) { - // NOTE - // 툴팁이 쌓여서 느려지기 때문에 날려줌. - // 근데 이 방법이 안전한지는 모르겠음 - document.querySelectorAll('[role="tooltip"]').forEach((node) => { - node.remove(); - }); - var item = this.$refs.menu.items[index]; - if (item) { - item.$el.classList.remove('notify'); - } - if (index === 'notification') { - this.unseenNotifications = []; + $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; } - - workerTimers.setTimeout(() => { - // fix some weird sorting bug when disabling data tables - if ( - typeof this.$refs.playerModerationTableRef?.sortData !== - 'undefined' - ) { - this.$refs.playerModerationTableRef.sortData.prop = 'created'; - } - if ( - typeof this.$refs.notificationTableRef?.sortData !== 'undefined' - ) { - this.$refs.notificationTableRef.sortData.prop = 'created_at'; - } - if (typeof this.$refs.friendLogTableRef?.sortData !== 'undefined') { - this.$refs.friendLogTableRef.sortData.prop = 'created_at'; - } - }, 100); }; - $app.data.twoFactorAuthDialogVisible = false; - - API.$on('LOGIN', function () { - $app.twoFactorAuthDialogVisible = false; - }); - - $app.methods.promptTOTP = function () { - if (this.twoFactorAuthDialogVisible) { - return; + // ascending + var compareByName = function (a, b) { + if (typeof a.name !== 'string' || typeof b.name !== 'string') { + return 0; } - AppApi.FlashWindow(); - this.twoFactorAuthDialogVisible = true; - this.$prompt($t('prompt.totp.description'), $t('prompt.totp.header'), { - distinguishCancelAndClose: true, - cancelButtonText: $t('prompt.totp.use_otp'), - confirmButtonText: $t('prompt.totp.verify'), - inputPlaceholder: $t('prompt.totp.input_placeholder'), - inputPattern: /^[0-9]{6}$/, - inputErrorMessage: $t('prompt.totp.input_error'), - callback: (action, instance) => { - if (action === 'confirm') { - API.verifyTOTP({ - code: instance.inputValue.trim() - }) - .catch((err) => { - this.promptTOTP(); - throw err; - }) - .then((args) => { - API.getCurrentUser(); - return args; - }); - } else if (action === 'cancel') { - this.promptOTP(); - } - }, - beforeClose: (action, instance, done) => { - this.twoFactorAuthDialogVisible = false; - done(); - } - }); + return a.name.localeCompare(b.name); }; - $app.methods.promptOTP = function () { - if (this.twoFactorAuthDialogVisible) { - return; + // ascending + var compareByDisplayName = function (a, b) { + if ( + typeof a.displayName !== 'string' || + typeof b.displayName !== 'string' + ) { + return 0; } - this.twoFactorAuthDialogVisible = true; - this.$prompt($t('prompt.otp.description'), $t('prompt.otp.header'), { - distinguishCancelAndClose: true, - cancelButtonText: $t('prompt.otp.use_totp'), - confirmButtonText: $t('prompt.otp.verify'), - inputPlaceholder: $t('prompt.otp.input_placeholder'), - inputPattern: /^[a-z0-9]{4}-[a-z0-9]{4}$/, - inputErrorMessage: $t('prompt.otp.input_error'), - callback: (action, instance) => { - if (action === 'confirm') { - API.verifyOTP({ - code: instance.inputValue.trim() - }) - .catch((err) => { - this.promptOTP(); - throw err; - }) - .then((args) => { - API.getCurrentUser(); - return args; - }); - } else if (action === 'cancel') { - this.promptTOTP(); - } - }, - beforeClose: (action, instance, done) => { - this.twoFactorAuthDialogVisible = false; - done(); - } - }); + return a.displayName.localeCompare(b.displayName); }; - $app.methods.promptEmailOTP = function () { - if (this.twoFactorAuthDialogVisible) { - return; + // descending + var compareByUpdatedAt = function (a, b) { + if ( + typeof a.updated_at !== 'string' || + typeof b.updated_at !== 'string' + ) { + return 0; } - AppApi.FlashWindow(); - this.twoFactorAuthDialogVisible = true; - this.$prompt( - $t('prompt.email_otp.description'), - $t('prompt.email_otp.header'), - { - distinguishCancelAndClose: true, - cancelButtonText: $t('prompt.email_otp.resend'), - confirmButtonText: $t('prompt.email_otp.verify'), - inputPlaceholder: $t('prompt.email_otp.input_placeholder'), - inputPattern: /^[0-9]{6}$/, - inputErrorMessage: $t('prompt.email_otp.input_error'), - callback: (action, instance) => { - if (action === 'confirm') { - API.verifyEmailOTP({ - code: instance.inputValue.trim() - }) - .catch((err) => { - this.promptEmailOTP(); - throw err; - }) - .then((args) => { - API.getCurrentUser(); - return args; - }); - } else if (action === 'cancel') { - this.resendEmail2fa(); - } - }, - beforeClose: (action, instance, done) => { - this.twoFactorAuthDialogVisible = false; - done(); - } - } - ); + var A = a.updated_at.toUpperCase(); + var B = b.updated_at.toUpperCase(); + if (A < B) { + return 1; + } + if (A > B) { + return -1; + } + return 0; }; - $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; - } + // descending + var compareByCreatedAt = function (a, b) { + if ( + typeof a.created_at !== 'string' || + typeof b.created_at !== 'string' + ) { + return 0; } - new Noty({ - type: 'error', - text: 'Cannot send 2FA email without saved credentials. Please login again.' - }).show(); - this.promptEmailOTP(); + var A = a.created_at.toUpperCase(); + var B = b.created_at.toUpperCase(); + if (A < B) { + return 1; + } + if (A > B) { + return -1; + } + return 0; }; - $app.data.exportFriendsListDialog = false; - $app.data.exportFriendsListCsv = ''; - $app.data.exportFriendsListJson = ''; - - $app.methods.showExportFriendsListDialog = function () { - var { friends } = API.currentUser; - if (Array.isArray(friends) === false) { - return; + // private + var compareByPrivate = function (a, b) { + if (typeof a.ref === 'undefined' || typeof b.ref === 'undefined') { + return 0; } - var lines = ['UserID,DisplayName,Memo']; - var _ = function (str) { - if (/[\x00-\x1f,"]/.test(str) === true) { - return `"${str.replace(/"/g, '""')}"`; - } - return str; - }; - var friendsList = []; - for (var userId of friends) { - var ref = this.friends.get(userId); - var name = (typeof ref !== 'undefined' && ref.name) || ''; - var memo = - (typeof ref !== 'undefined' && ref.memo.replace(/\n/g, ' ')) || - ''; - lines.push(`${_(userId)},${_(name)},${_(memo)}`); - friendsList.push(userId); + 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; } - this.exportFriendsListJson = JSON.stringify( - { friends: friendsList }, - null, - 4 - ); - this.exportFriendsListCsv = lines.join('\n'); - this.exportFriendsListDialog = true; + return 0; }; - $app.data.exportAvatarsListDialog = false; - $app.data.exportAvatarsListCsv = ''; - - $app.methods.showExportAvatarsListDialog = function () { - for (var ref of API.cachedAvatars.values()) { - if (ref.authorId === API.currentUser.id) { - API.cachedAvatars.delete(ref.id); - } + var compareByStatus = function (a, b) { + if (typeof a.ref === 'undefined' || typeof b.ref === 'undefined') { + return 0; } - var params = { - n: 50, - offset: 0, - sort: 'updated', - order: 'descending', - releaseStatus: 'all', - user: 'me' - }; - var map = new Map(); - API.bulk({ - fn: '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); - } + 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; } - }, - done: () => { - var avatars = Array.from(map.values()); - if (Array.isArray(avatars) === false) { - return; + break; + case 'active': + switch (a) { + case 'join me': + return -1; + case 'ask me': + return 1; + case 'busy': + return 1; } - var lines = ['AvatarID,AvatarName']; - var _ = function (str) { - if (/[\x00-\x1f,"]/.test(str) === true) { - return `"${str.replace(/"/g, '""')}"`; - } - return str; - }; - for (var avatar of avatars) { - lines.push(`${_(avatar.id)},${_(avatar.name)}`); + break; + case 'ask me': + switch (a) { + case 'join me': + return -1; + case 'active': + return -1; + case 'busy': + return 1; } - this.exportAvatarsListCsv = lines.join('\n'); - this.exportAvatarsListDialog = true; - } - }); + break; + case 'busy': + switch (a) { + case 'join me': + return -1; + case 'active': + return -1; + case 'ask me': + return -1; + } + break; + } + return 0; }; - API.$on('USER:2FA', function () { - AppApi.FocusWindow(); - $app.promptTOTP(); - }); - - API.$on('USER:EMAILOTP', function () { - AppApi.FocusWindow(); - $app.promptEmailOTP(); - }); + // 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; + }; - API.$on('LOGOUT', function () { - if (this.isLoggedIn) { - new Noty({ - type: 'success', - text: `See you again, ${escapeTag( - this.currentUser.displayName - )}!` - }).show(); + // 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; } - this.isLoggedIn = false; - $app.friendLogInitStatus = false; - }); - API.$on('LOGIN', function (args) { - new Noty({ - type: 'success', - text: `Hello there, ${escapeTag( - args.ref.displayName - )}!` - }).show(); - $app.$refs.menu.activeIndex = 'feed'; - }); + return a.ref.location.localeCompare(b.ref.location); + }; - 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); - }); + var compareByActivityField = function (a, b, field) { + if (typeof a.ref === 'undefined' || typeof b.ref === 'undefined') { + return 0; + } - $app.methods.checkPrimaryPassword = function (args) { - return new Promise((resolve, reject) => { - if (!this.enablePrimaryPassword) { - resolve(args.password); + // 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'); } - $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); - }); + return compareByActivityField(a, b, '$online_for'); + } + + return compareByActivityField(a, b, 'last_activity'); }; - $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 () { - 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 - ); + var getFriendsSortFunction = function (sortMethods) { + const sorts = []; + for (const sortMethod of sortMethods) { + switch (sortMethod) { + case 'Sort Alphabetically': + sorts.push(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 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; + }; }; - $app.methods.updateStoredUser = async function (currentUser) { - var savedCredentials = {}; - if ((await configRepository.getString('savedCredentials')) !== null) { - savedCredentials = JSON.parse( - await configRepository.getString('savedCredentials') - ); + // VIP friends + $app.computed.vipFriends = function () { + if (!this.sortVIPFriends) { + return this.vipFriends_; } - if (this.saveCredentials) { - var credentialsToSave = { - user: currentUser, - loginParmas: this.saveCredentials - }; - savedCredentials[currentUser.id] = credentialsToSave; - delete this.saveCredentials; - } else if (typeof savedCredentials[currentUser.id] !== 'undefined') { - savedCredentials[currentUser.id].user = currentUser; - savedCredentials[currentUser.id].cookies = - await webApiService.getCookies(); + 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.loginForm.savedCredentials = savedCredentials; - var jsonCredentialsArray = JSON.stringify(savedCredentials); - await configRepository.setString( - 'savedCredentials', - jsonCredentialsArray + this.sortOnlineFriends = false; + + this.onlineFriends_.sort( + getFriendsSortFunction(this.sidebarSortMethods) ); - this.loginForm.lastUserLoggedIn = currentUser.id; - await configRepository.setString('lastUserLoggedIn', currentUser.id); + + return this.onlineFriends_; }; - $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]; - } + // Active friends + $app.computed.activeFriends = function () { + if (!this.sortActiveFriends) { + return this.activeFriends_; } - await configRepository.setString( - 'savedCredentials', - JSON.stringify(savedCredentials) + this.sortActiveFriends = false; + + this.activeFriends_.sort( + getFriendsSortFunction(this.sidebarSortMethods) ); + + return this.activeFriends_; }; - $app.methods.relogin = async function (user) { - var { loginParmas } = user; - if (user.cookies) { - await webApiService.setCookies(user.cookies); - } - this.loginForm.lastUserLoggedIn = user.user.id; // for resend email 2fa - if (loginParmas.endpoint) { - API.endpointDomain = loginParmas.endpoint; - API.websocketDomain = loginParmas.websocket; - } else { - API.endpointDomain = API.endpointDomainVrchat; - API.websocketDomain = API.websocketDomainVrchat; + // Offline friends + $app.computed.offlineFriends = function () { + if (!this.sortOfflineFriends) { + return this.offlineFriends_; } - return new Promise((resolve, reject) => { - if (this.enablePrimaryPassword) { - this.checkPrimaryPassword(loginParmas) - .then((pwd) => { - this.loginForm.loading = true; - return API.getConfig() - .catch((err) => { - this.loginForm.loading = false; - reject(err); - }) - .then(() => { - API.login({ - username: loginParmas.username, - password: pwd, - cipher: loginParmas.password, - endpoint: loginParmas.endpoint, - websocket: loginParmas.websocket - }) - .catch((err2) => { - this.loginForm.loading = false; - // API.logout(); - reject(err2); - }) - .then(() => { - this.loginForm.loading = false; - resolve(); - }); - }); - }) - .catch((_) => { - this.$message({ - message: 'Incorrect primary password', - type: 'error' - }); - reject(_); - }); - } else { - API.getConfig() - .catch((err) => { - this.loginForm.loading = false; - reject(err); - }) - .then(() => { - API.login({ - username: loginParmas.username, - password: loginParmas.password, - endpoint: loginParmas.endpoint, - websocket: loginParmas.websocket - }) - .catch((err2) => { - this.loginForm.loading = false; - API.logout(); - reject(err2); - }) - .then(() => { - this.loginForm.loading = false; - resolve(); - }); - }); - } - }); - }; + this.sortOfflineFriends = false; - $app.methods.deleteSavedLogin = async function (userId) { - var savedCredentials = JSON.parse( - await configRepository.getString('savedCredentials') + this.offlineFriends_.sort( + getFriendsSortFunction(this.sidebarSortMethods) ); - delete savedCredentials[userId]; - // Disable primary password when no account is available. - if (Object.keys(savedCredentials).length === 0) { - this.enablePrimaryPassword = false; - await configRepository.setBool('enablePrimaryPassword', false); - } - this.loginForm.savedCredentials = savedCredentials; - var jsonCredentials = JSON.stringify(savedCredentials); - await configRepository.setString('savedCredentials', jsonCredentials); - new Noty({ - type: 'success', - text: 'Account removed.' - }).show(); + + return this.offlineFriends_; }; - API.$on('AUTOLOGIN', function () { - if (this.attemptingAutoLogin) { - return; - } - this.attemptingAutoLogin = true; - var user = - $app.loginForm.savedCredentials[$app.loginForm.lastUserLoggedIn]; + $app.methods.userStatusClass = function (user, pendingOffline) { + var style = {}; if (typeof user === 'undefined') { - this.attemptingAutoLogin = false; - return; + return style; } - if ($app.enablePrimaryPassword) { - this.logout(); - return; + var id = ''; + if (user.id) { + id = user.id; + } else if (user.userId) { + id = user.userId; } - $app.relogin(user) - .then(() => { - if (this.errorNoty) { - this.errorNoty.close(); - } - this.errorNoty = new Noty({ - type: 'success', - text: 'Automatically logged in.' - }).show(); - console.log('Automatically logged in.'); - }) - .catch((err) => { - if (this.errorNoty) { - this.errorNoty.close(); - } - this.errorNoty = new Noty({ - type: 'error', - text: 'Failed to login automatically.' - }).show(); - console.error('Failed to login automatically.', err); - }) - .finally(() => { - if (!navigator.onLine) { - this.errorNoty = new Noty({ - type: 'error', - text: `You're offline.` - }).show(); - console.error(`You're offline.`); - } - }); - }); - - API.$on('USER:CURRENT', function () { - this.attemptingAutoLogin = false; - }); - - API.$on('LOGOUT', function () { - this.attemptingAutoLogin = false; - }); - - $app.data.loginForm = { - loading: true, - username: '', - password: '', - endpoint: '', - websocket: '', - saveCredentials: false, - savedCredentials: - (await configRepository.getString('savedCredentials')) !== null - ? JSON.parse( - await configRepository.getString('savedCredentials') - ) - : {}, - lastUserLoggedIn: await configRepository.getString('lastUserLoggedIn'), - rules: { - username: [ - { - required: true, - trigger: 'blur' - } - ], - password: [ - { - required: true, - trigger: 'blur' - } - ] + if (id === API.currentUser.id) { + return this.statusClass(user.status); } - }; - - $app.methods.login = async function () { - await webApiService.clearCookies(); - this.$refs.loginForm.validate((valid) => { - if (valid && !this.loginForm.loading) { - this.loginForm.loading = true; - if (this.loginForm.endpoint) { - API.endpointDomain = this.loginForm.endpoint; - API.websocketDomain = this.loginForm.websocket; - } else { - API.endpointDomain = API.endpointDomainVrchat; - API.websocketDomain = API.websocketDomainVrchat; - } - API.getConfig() - .catch((err) => { - this.loginForm.loading = false; - throw err; - }) - .then((args) => { - if ( - this.loginForm.saveCredentials && - this.enablePrimaryPassword - ) { - $app.$prompt( - $t('prompt.primary_password.description'), - $t('prompt.primary_password.header'), - { - inputType: 'password', - inputPattern: /[\s\S]{1,32}/ - } - ) - .then(({ value }) => { - let saveCredential = - this.loginForm.savedCredentials[ - Object.keys( - this.loginForm.savedCredentials - )[0] - ]; - security - .decrypt( - saveCredential.loginParmas.password, - value - ) - .then(() => { - security - .encrypt( - this.loginForm.password, - value - ) - .then((pwd) => { - API.login({ - username: - this.loginForm - .username, - password: - this.loginForm - .password, - endpoint: - this.loginForm - .endpoint, - websocket: - this.loginForm - .websocket, - saveCredentials: - this.loginForm - .saveCredentials, - cipher: pwd - }).then(() => { - this.loginForm.username = - ''; - this.loginForm.password = - ''; - this.loginForm.endpoint = - ''; - this.loginForm.websocket = - ''; - }); - }); - }); - }) - .finally(() => { - this.loginForm.loading = false; - }); - return args; - } - API.login({ - username: this.loginForm.username, - password: this.loginForm.password, - endpoint: this.loginForm.endpoint, - websocket: this.loginForm.websocket, - saveCredentials: this.loginForm.saveCredentials - }) - .then(() => { - this.loginForm.username = ''; - this.loginForm.password = ''; - this.loginForm.endpoint = ''; - this.loginForm.websocket = ''; - }) - .finally(() => { - this.loginForm.loading = false; - }); - return args; - }); - } - }); - }; - - $app.methods.loginWithSteam = function () { - if (!this.loginForm.loading) { - this.loginForm.loading = true; - AppApi.LoginWithSteam() - .catch((err) => { - this.loginForm.loading = false; - throw err; - }) - .then((steamTicket) => { - if (steamTicket) { - API.getConfig() - .catch((err) => { - this.loginForm.loading = false; - throw err; - }) - .then((args) => { - API.loginWithSteam({ - steamTicket - }).finally(() => { - this.loginForm.loading = false; - }); - return args; - }); - } else { - this.loginForm.loading = false; - this.$message({ - message: 'It only works when VRChat is running.', - type: 'error' - }); - } - }); + if (!user.isFriend) { + return style; } - }; - - // #endregion - // #region | User Memos - - $app.methods.migrateMemos = async function () { - var json = JSON.parse(await VRCXStorage.GetAll()); - database.begin(); - for (var line in json) { - if (line.substring(0, 8) === 'memo_usr') { - var userId = line.substring(5); - var memo = json[line]; - if (memo) { - await this.saveMemo(userId, memo); - VRCXStorage.Remove(`memo_${userId}`); - } + 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; } - database.commit(); - }; - - $app.methods.getMemo = async function (userId) { - try { - return await database.getMemo(userId); - } catch (err) {} - return { - userId: '', - editedAt: '', - memo: '' - }; + if (user.last_platform && user.last_platform !== 'standalonewindows') { + style.mobile = true; + } + return style; }; - $app.methods.saveMemo = function (id, memo) { - if (memo) { - database.setMemo({ - userId: id, - editedAt: new Date().toJSON(), - memo - }); - } else { - database.deleteMemo(id); - } - var ref = this.friends.get(id); - if (ref) { - ref.memo = String(memo || ''); - if (memo) { - var array = memo.split('\n'); - ref.$nickName = array[0]; - } else { - ref.$nickName = ''; + $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.getAllMemos = async function () { - var memeos = await database.getAllMemos(); - memeos.forEach((memo) => { - var ref = $app.friends.get(memo.userId); - if (typeof ref !== 'undefined') { - ref.memo = memo.memo; - ref.$nickName = ''; - if (memo.memo) { - var array = memo.memo.split('\n'); - ref.$nickName = array[0]; + $app.methods.confirmDeleteFriend = function (id) { + this.$confirm('Continue? Unfriend', 'Confirm', { + confirmButtonText: 'Confirm', + cancelButtonText: 'Cancel', + type: 'info', + callback: (action) => { + if (action === 'confirm') { + API.deleteFriend({ + userId: id + }); } } }); }; // #endregion - // #region | World Memos + // #region | App: Quick Search - $app.methods.getWorldMemo = async function (worldId) { - try { - return await database.getWorldMemo(worldId); - } catch (err) {} - return { - worldId: '', - editedAt: '', - memo: '' - }; - }; + $app.data.quickSearch = ''; + $app.data.quickSearchItems = []; - $app.methods.saveWorldMemo = function (worldId, memo) { - if (memo) { - database.setWorldMemo({ - worldId, - editedAt: new Date().toJSON(), - memo - }); - } else { - database.deleteWorldMemo(worldId); + var localeIncludes = function (str, search, comparer) { + // These checks are stolen from https://stackoverflow.com/a/69623589/11030436 + if (search === '') { + return true; + } else if (!str || !search) { + return false; } - }; + const strObj = String(str); + const searchObj = String(search); - // #endregion - // #region | App: Avatar Memos + if (strObj.length === 0) { + return false; + } - $app.methods.getAvatarMemo = async function (avatarId) { - try { - return await database.getAvatarMemoDB(avatarId); - } catch (err) { - console.error(err); + if (searchObj.length > strObj.length) { + return false; } - return { - avatarId: '', - editedAt: '', - memo: '' - }; + + // Now simply loop through each substring and compare them + for (let i = 0; i < str.length - searchObj.length + 1; i++) { + const substr = strObj.substring(i, i + searchObj.length); + if (comparer.compare(substr, searchObj) === 0) { + return true; + } + } + return false; }; - $app.methods.saveAvatarMemo = function (avatarId, memo) { - if (memo) { - database.setAvatarMemo({ - avatarId, - editedAt: new Date().toJSON(), - memo - }); - } else { - database.deleteAvatarMemo(avatarId); + // 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; }; - // #endregion - // #region | App: Friends + $app.methods.quickSearchRemoteMethod = function (query) { + if (!query) { + this.quickSearchItems = []; + } - $app.data.friends = new Map(); - $app.data.pendingActiveFriends = new Set(); - $app.data.friendsNo = 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; + const results = []; + const cleanQuery = removeWhitespace(query); - $app.methods.saveFriendsGroupStates = async function () { - await configRepository.setBool( - 'VRCX_isFriendsGroupMe', - this.isFriendsGroupMe - ); - await configRepository.setBool( - 'VRCX_isFriendsGroupFavorites', - this.isVIPFriends - ); - await configRepository.setBool( - 'VRCX_isFriendsGroupOnline', - this.isOnlineFriends - ); - await configRepository.setBool( - 'VRCX_isFriendsGroupActive', - this.isActiveFriends - ); - await configRepository.setBool( - 'VRCX_isFriendsGroupOffline', - this.isOfflineFriends - ); - }; + for (let ctx of this.friends.values()) { + if (typeof ctx.ref === 'undefined') { + continue; + } - $app.methods.loadFriendsGroupStates = async function () { - this.isFriendsGroupMe = await configRepository.getBool( - 'VRCX_isFriendsGroupMe', - true - ); - this.isVIPFriends = await configRepository.getBool( - 'VRCX_isFriendsGroupFavorites', - true - ); - this.isOnlineFriends = await configRepository.getBool( - 'VRCX_isFriendsGroupOnline', - true - ); - this.isActiveFriends = await configRepository.getBool( - 'VRCX_isFriendsGroupActive', - false - ); - this.isOfflineFriends = await configRepository.getBool( - 'VRCX_isFriendsGroupOffline', - false - ); - }; + const cleanName = removeConfusables(ctx.name); + let match = localeIncludes( + cleanName, + cleanQuery, + this.stringComparer + ); + if (!match) { + // Also check regular name in case search is with special characters + match = 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 = localeIncludes(ctx.memo, query, this.stringComparer); + } + if (!match && ctx.ref.note) { + match = localeIncludes( + ctx.ref.note, + query, + this.stringComparer + ); + } - API.$on('LOGIN', function () { - $app.loadFriendsGroupStates(); - }); + if (match) { + results.push({ + value: ctx.id, + label: ctx.name, + ref: ctx.ref, + name: ctx.name + }); + } + } - $app.methods.fetchActiveFriend = function (userId) { - this.pendingActiveFriends.add(userId); - // FIXME: handle error - return API.getUser({ - userId - }).then((args) => { - this.pendingActiveFriends.delete(userId); - return args; + 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 compareByName(a, b); + }); + if (results.length > 4) { + results.length = 4; + } + results.push({ + value: `search:${query}`, + label: query }); - }; - API.$on('USER:CURRENT', function (args) { - $app.checkActiveFriends(args.json); - }); + this.quickSearchItems = results; + }; - $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; + $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.$refs.menu.activeIndex = 'friendsList'; + } else { + this.$refs.menu.activeIndex = 'search'; + this.searchText = searchText; + this.lookupUser({ displayName: searchText }); + } + } else { + this.showUserDialog(value); } - this.fetchActiveFriend(userId); + this.quickSearchVisibleChange(value); } }; - API.$on('LOGIN', function () { - $app.friends.clear(); - $app.pendingActiveFriends.clear(); - $app.friendsNo = 0; - $app.isVIPFriends = true; - $app.isOnlineFriends = true; - $app.isActiveFriends = true; - $app.isOfflineFriends = false; - $app.vipFriends_ = []; - $app.onlineFriends_ = []; - $app.activeFriends_ = []; - $app.offlineFriends_ = []; - $app.sortVIPFriends = false; - $app.sortOnlineFriends = false; - $app.sortActiveFriends = false; - $app.sortOfflineFriends = false; - }); - - API.$on('USER:CURRENT', function (args) { - // USER:CURRENT에서 처리를 함 - $app.refreshFriends(args.ref, args.origin); - $app.updateOnlineFriendCoutner(); - - if ($app.randomUserColours) { - $app.getNameColour(this.currentUser.id).then((colour) => { - this.currentUser.$userColour = colour; - }); + // NOTE: 그냥 열고 닫고 했을때 changed 이벤트 발생이 안되기 때문에 넣음 + $app.methods.quickSearchVisibleChange = function (value) { + if (value) { + this.quickSearch = ''; + this.quickSearchItems = []; + this.quickSearchUserHistory(); } - }); + }; - API.$on('FRIEND:ADD', function (args) { - $app.addFriend(args.params.userId); - }); + // #endregion + // #region | App: Quick Search User History - API.$on('FRIEND:DELETE', function (args) { - $app.deleteFriend(args.params.userId); - }); + $app.data.showUserDialogHistory = new Set(); - API.$on('FRIEND:STATE', function (args) { - $app.updateFriend({ - id: args.params.userId, - state: args.json.state + $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 + }); + } }); - }); + this.quickSearchItems = results; + }; - API.$on('FAVORITE', function (args) { - $app.updateFriend({ id: args.ref.favoriteId }); - }); + // #endregion + // #region | App: Feed - API.$on('FAVORITE:@DELETE', function (args) { - $app.updateFriend({ id: args.ref.favoriteId }); - }); + $app.data.tablePageSize = await configRepository.getInt( + 'VRCX_tablePageSize', + 15 + ); - API.$on('LOGIN', function () { - $app.nextFriendsRefresh = 0; - }); + $app.data.gameLogTable.pageSize = $app.data.tablePageSize; + $app.data.feedTable.pageSize = $app.data.tablePageSize; + $app.data.groupMemberModerationTable.pageSize = $app.data.tablePageSize; + $app.data.groupBansModerationTable.pageSize = $app.data.tablePageSize; + $app.data.groupLogsModerationTable.pageSize = $app.data.tablePageSize; + $app.data.groupInvitesModerationTable.pageSize = $app.data.tablePageSize; + $app.data.groupJoinRequestsModerationTable.pageSize = + $app.data.tablePageSize; + $app.data.groupBlockedModerationTable.pageSize = $app.data.tablePageSize; - $app.methods.refreshFriendsList = async function () { - // If we just got user less then 1 min before code call, don't call it again - if ($app.nextCurrentUserRefresh < 720) { - await API.getCurrentUser().catch((err) => { - console.error(err); - }); - } - await API.refreshFriends().catch((err) => { - console.error(err); - }); - API.reconnectWebSocket(); - }; + $app.data.dontLogMeOut = false; - $app.methods.refreshFriends = function (ref, origin) { - 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, origin }); + API.$on('LOGIN', async function (args) { + $app.friendLog = new Map(); + $app.feedTable.data = []; + $app.feedSessionTable = []; + $app.friendLogInitStatus = false; + await database.initUserTables(args.json.id); + $app.$refs.menu.activeIndex = 'feed'; + // 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(); + $app.feedTableLookup(); + // eslint-disable-next-line require-atomic-updates + $app.notificationTable.data = await database.getNotifications(); + await this.refreshNotifications(); + await $app.loadCurrentUserGroups( + args.json.id, + args.json?.presence?.groups + ); + await $app.getCurrentUserGroups(); + try { + if ( + await configRepository.getBool(`friendLogInit_${args.json.id}`) + ) { + await $app.getFriendLog(); } else { - this.addFriend(id, state); + await $app.initFriendLog(args.json.id); } - } - for (var id of this.friends.keys()) { - if (map.has(id) === false) { - this.deleteFriend(id); + } catch (err) { + if (!$app.dontLogMeOut) { + $app.$message({ + message: 'Failed to load freinds list, logging out', + type: 'error' + }); + this.logout(); + throw err; } } - - this.saveFriendOrder(); - }; - - $app.methods.saveFriendOrder = async function () { - var currentTime = Date.now(); - var lastStoreTime = await configRepository.getString( - `VRCX_lastStoreTime_${API.currentUser.id}`, - '' - ); - // store once every week - if (lastStoreTime && currentTime - lastStoreTime < 604800000) { - return; + $app.getAvatarHistory(); + $app.getAllUserMemos(); + if ($app.randomUserColours) { + $app.getNameColour(this.currentUser.id).then((colour) => { + this.currentUser.$userColour = colour; + }); + $app.userColourInit(); } - var storedData = {}; - try { - var data = await configRepository.getString( - `VRCX_friendOrder_${API.currentUser.id}` - ); - if (data) { - var storedData = JSON.parse(data); - } - } catch (err) { - console.error(err); + this.getAuth(); + $app.updateSharedFeed(true); + if ($app.isGameRunning) { + $app.loadPlayerList(); } - storedData[currentTime] = Array.from(this.friends.keys()); - await configRepository.setString( - `VRCX_friendOrder_${API.currentUser.id}`, - JSON.stringify(storedData) - ); - await configRepository.setString( - `VRCX_lastStoreTime_${API.currentUser.id}`, - currentTime - ); - }; + $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.addFriend = function (id, state) { - if (this.friends.has(id)) { + $app.methods.loadPlayerList = function () { + var data = this.gameLogSessionTable; + if (data.length === 0) { return; } - var ref = API.cachedUsers.get(id); - var isVIP = this.localFavoriteFriends.has(id); - var ctx = { - id, - state: state || 'offline', - isVIP, - ref, - name: '', - no: ++this.friendsNo, - memo: '', - pendingOffline: false, - $nickName: '' - }; - if (this.friendLogInitStatus) { - this.getMemo(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; + 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; } - } 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; + 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.displayName, userMap); + if (this.friends.has(ctx.userId)) { + this.lastLocation.friendList.set( + ctx.displayName, + userMap + ); + } + } + if (ctx.type === 'OnPlayerLeft') { + this.lastLocation.playerList.delete(ctx.displayName); + this.lastLocation.friendList.delete(ctx.displayName); + } } - } else if (ctx.state === 'active') { - this.activeFriends_.push(ctx); - this.sortActiveFriends = true; - } else { - this.offlineFriends_.push(ctx); - this.sortOfflineFriends = true; - } - }; + this.lastLocation.playerList.forEach((ref1) => { + if (ref1.userId && !API.cachedUsers.has(ref1.userId)) { + API.getUser({ userId: ref1.userId }); + } + }); - $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) { - removeFromArray(this.vipFriends_, ctx); - } else { - removeFromArray(this.onlineFriends_, ctx); - } - } else if (ctx.state === 'active') { - removeFromArray(this.activeFriends_, ctx); - } else { - removeFromArray(this.offlineFriends_, ctx); + this.updateCurrentUserLocation(); + this.updateCurrentInstanceWorld(); + this.updateVRLastLocation(); + this.getCurrentInstanceUserList(); + this.applyUserDialogLocation(); + this.applyWorldDialogInstances(); + this.applyGroupDialogInstances(); } }; - $app.data.updateFriendInProgress = new Map(); + $app.data.robotUrl = `${API.endpointDomain}/file/file_0e8c4e32-7444-44ea-ade4-313c010d4bae/1/file`; - $app.methods.updateFriend = function (ctx) { - var { id, state, origin } = ctx; - var stateInput = state; - var ctx = this.friends.get(id); - if (typeof ctx === 'undefined') { + API.$on('USER:UPDATE', async function (args) { + var { ref, props } = args; + var friend = $app.friends.get(ref.id); + if (typeof friend === 'undefined') { return; } - if (stateInput === 'online') { - this.APILastOnline.set(id, Date.now()); - ctx.pendingOffline = false; + if (props.location && ref.id === $app.userDialog.id) { + // update user dialog instance occupants + $app.applyUserDialogLocation(true); } - var ref = API.cachedUsers.get(id); - var isVIP = this.localFavoriteFriends.has(id); - var location = ''; - var $location_at = ''; - if (typeof ref !== 'undefined') { - var { location, $location_at } = ref; + if (props.location && ref.$location.worldId === $app.worldDialog.id) { + $app.applyWorldDialogInstances(); } - 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) { - API.getUser({ - userId: id - }); - } - if (ctx.isVIP) { - this.sortVIPFriends = true; - } else { - this.sortOnlineFriends = true; - } + if (props.location && ref.$location.groupId === $app.groupDialog.id) { + $app.applyGroupDialogInstances(); + } + if ( + 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 time = props.location[2]; + if (previousLocation === 'traveling') { + previousLocation = ref.$previousLocation; + var travelTime = Date.now() - ref.$travelingToTime; + time -= travelTime; + if (time < 0) { + time = 0; } } - if (ctx.isVIP !== isVIP) { - ctx.isVIP = isVIP; - if (ctx.state === 'online') { - if (ctx.isVIP) { - removeFromArray(this.onlineFriends_, ctx); - this.vipFriends_.push(ctx); - this.sortVIPFriends = true; - } else { - 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 ( - origin && - ctx.state !== 'online' && - typeof ref !== 'undefined' && - this.isRealInstance(ref.location) - ) { - API.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; - } - // delayed second check to prevent status flapping - var date = this.updateFriendInProgress.get(id); - if (date && date > Date.now() - this.pendingOfflineDelay + 5000) { - // check if already waiting - if (this.debugFriendState) { - console.log( - ctx.name, - new Date().toJSON(), - 'pendingOfflineCheck', - stateInput, - ctx.state - ); - } - return; - } - ctx.pendingOffline = true; - this.updateFriendInProgress.set(id, Date.now()); - // wait 2minutes then check if user came back online - workerTimers.setTimeout(() => { - ctx.pendingOffline = false; - this.updateFriendInProgress.delete(id); - this.updateFriendDelayedCheck( - id, - ctx, - stateInput, - location, - $location_at - ); - }, this.pendingOfflineDelay); - } else { - ctx.ref = ref; - ctx.isVIP = isVIP; - if (typeof ref !== 'undefined') { - ctx.name = ref.displayName; - } - this.updateFriendDelayedCheck( - id, - ctx, - stateInput, - location, - $location_at - ); - } - }; - - $app.methods.updateFriendDelayedCheck = async function ( - id, - ctx, - newState, - location, - $location_at - ) { - var date = this.APILastOnline.get(id); - if ( - ctx.state === 'online' && - (newState === 'active' || newState === 'offline') && - date && - date > Date.now() - 120000 - ) { - if (this.debugFriendState) { - console.log( - `falsePositiveOffline ${ctx.name} currentState:${ctx.ref.state} expectedState:${newState}` - ); - } - return; - } - if (this.debugFriendState) { - console.log(ctx.name, 'updateFriendState', newState, ctx.state); - } - 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); + if (ref.$previousLocation === props.location[0]) { + // location traveled to is the same + ref.$location_at = Date.now() - props.location[2]; + } else { + var worldName = await $app.getWorldName(props.location[0]); + var groupName = await $app.getGroupName(props.location[0]); var feed = { created_at: new Date().toJSON(), - type: 'Offline', + type: 'GPS', userId: ref.id, displayName: ref.displayName, - location, + location: props.location[0], worldName, groupName, + previousLocation, 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(ref.location); - var groupName = await this.getGroupName(location); - var feed = { - created_at: new Date().toJSON(), - type: 'Online', - userId: id, - displayName: ctx.name, - location: ref.location, - worldName, - groupName, - time: '' - }; - this.addFeed(feed); - database.addOnlineOfflineToDatabase(feed); - } - if (newState === 'active') { - ctx.ref.$active_for = Date.now(); + $app.addFeed(feed); + database.addGPSToDatabase(feed); + $app.updateFriendGPS(ref.id); + // clear previousLocation after GPS + ref.$previousLocation = ''; + ref.$travelingToTime = Date.now(); } } - if (ctx.state === 'online') { - if (ctx.isVIP) { - removeFromArray(this.vipFriends_, ctx); + 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 { - removeFromArray(this.onlineFriends_, ctx); + currentAvatarImageUrl = ref.currentAvatarImageUrl; + previousCurrentAvatarImageUrl = ref.currentAvatarImageUrl; } - } else if (ctx.state === 'active') { - removeFromArray(this.activeFriends_, ctx); - } else { - removeFromArray(this.offlineFriends_, ctx); - } - if (newState === 'online') { - if (isVIP) { - this.vipFriends_.push(ctx); - this.sortVIPFriends = true; + if (props.currentAvatarThumbnailImageUrl) { + currentAvatarThumbnailImageUrl = + props.currentAvatarThumbnailImageUrl[0]; + previousCurrentAvatarThumbnailImageUrl = + props.currentAvatarThumbnailImageUrl[1]; } else { - this.onlineFriends_.push(ctx); - this.sortOnlineFriends = true; + 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); } - } 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; - ctx.ref.state = newState; - ctx.name = ref.displayName; - ctx.isVIP = isVIP; - }; - - $app.methods.getWorldName = async function (location) { - var worldName = ''; - if (this.isRealInstance(location)) { - try { - var L = API.parseLocation(location); - if (L.worldId) { - var args = await API.getCachedWorld({ - worldId: L.worldId - }); - worldName = args.ref.name; - } - } catch (err) {} - } - return worldName; - }; - - $app.methods.getGroupName = async function (data) { - if (!data) { - return ''; } - var groupName = ''; - var groupId = data; - if (!data.startsWith('grp_')) { - var L = API.parseLocation(data); - groupId = L.groupId; - if (!L.groupId) { - return ''; + 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); } - 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; + 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); } - }; + }); - $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; - } - }; + // #endregion + // #region | App: gameLog - // ascending - var compareByName = function (a, b) { - if (typeof a.name !== 'string' || typeof b.name !== 'string') { - return 0; - } - return a.name.localeCompare(b.name); + $app.data.lastLocation = { + date: 0, + location: '', + name: '', + playerList: new Map(), + friendList: new Map() }; - // ascending - var compareByDisplayName = function (a, b) { - if ( - typeof a.displayName !== 'string' || - typeof b.displayName !== 'string' - ) { - return 0; + $app.methods.lastLocationReset = function (gameLogDate) { + var dateTime = gameLogDate; + if (!gameLogDate) { + dateTime = new Date().toJSON(); } - return a.displayName.localeCompare(b.displayName); - }; - - // descending - var compareByUpdatedAt = function (a, b) { - if ( - typeof a.updated_at !== 'string' || - typeof b.updated_at !== 'string' - ) { - return 0; + 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 A = a.updated_at.toUpperCase(); - var B = b.updated_at.toUpperCase(); - if (A < B) { - return 1; + 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); } - if (A > B) { - return -1; + 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); } - return 0; + this.gameLogApiLoggingEnabled = false; + 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(); }; - // descending - var compareByCreatedAt = function (a, b) { - if ( - typeof a.created_at !== 'string' || - typeof b.created_at !== 'string' - ) { - return 0; - } - var A = a.created_at.toUpperCase(); - var B = b.created_at.toUpperCase(); - if (A < B) { - return 1; - } - if (A > B) { - return -1; - } - return 0; + $app.data.lastLocation$ = { + tag: '', + instanceId: '', + accessType: '', + worldName: '', + worldCapacity: 0, + joinUrl: '', + statusName: '', + statusImage: '' }; - // 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; - }; + $app.data.lastLocationDestination = ''; + $app.data.lastLocationDestinationTime = 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; + $app.methods.silentSearchUser = function (displayName) { + var playerListRef = this.lastLocation.playerList.get(displayName); + if ( + !this.gameLogApiLoggingEnabled || + !playerListRef || + playerListRef.userId + ) { + return; } - if (a.ref.state === 'offline') { - return 1; + if (this.debugGameLog) { + console.log('Searching for userId for:', displayName); } - 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; + var params = { + n: 5, + offset: 0, + fuzzy: false, + search: displayName + }; + API.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); } - break; - case 'busy': - switch (a) { - case 'join me': - return -1; - case 'active': - return -1; - case 'ask me': - return -1; + if (json.displayName === displayName) { + nameFound = true; } - break; - } - return 0; + } + if (!nameFound) { + console.error('userId not found for', displayName); + } + return args; + }); }; - // 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; + $app.methods.lookupYouTubeVideo = async function (videoId) { + var data = null; + var apiKey = 'AIzaSyDC9AwAmtnMWpmk6mhs-iIStfXmH0vJxew'; + if (this.youTubeApiKey) { + apiKey = this.youTubeApiKey; } - if (a.$location_at > b.$location_at) { - return 1; + 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 0; + return data; }; - // 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; - } + $app.data.nowPlaying = { + url: '', + name: '', + length: 0, + startTime: 0, + offset: 0, + elapsed: 0, + percentage: 0, + remainingText: '', + playing: false + }; - return a.ref.location.localeCompare(b.ref.location); + $app.methods.clearNowPlaying = function () { + this.nowPlaying = { + url: '', + name: '', + length: 0, + startTime: 0, + offset: 0, + elapsed: 0, + percentage: 0, + remainingText: '', + playing: false + }; + this.updateVrNowPlaying(); }; - var compareByActivityField = function (a, b, field) { - if (typeof a.ref === 'undefined' || typeof b.ref === 'undefined') { - return 0; - } + $app.methods.setNowPlaying = function (ctx) { + if (this.nowPlaying.url !== ctx.videoUrl) { + this.queueGameLogNoty(ctx); + this.addGameLog(ctx); + database.addGamelogVideoPlayToDatabase(ctx); - // 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; + 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: '' + }; } - if ( - a.ref[field] > b.ref[field] || - (a.ref[field] === '' && b.ref[field] !== '') - ) { - return -1; + this.updateVrNowPlaying(); + if (!this.nowPlaying.playing && ctx.videoLength > 0) { + this.nowPlaying.playing = true; + this.updateNowPlaying(); } - 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'); + $app.methods.updateNowPlaying = function () { + var np = this.nowPlaying; + if (!this.nowPlaying.playing) { + return; } - - return compareByActivityField(a, b, 'last_activity'); + 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); }; - var getFriendsSortFunction = function (sortMethods) { - const sorts = []; - for (const sortMethod of sortMethods) { - switch (sortMethod) { - case 'Sort Alphabetically': - sorts.push(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 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; - } + $app.methods.updateVrNowPlaying = function () { + var json = JSON.stringify(this.nowPlaying); + AppApi.ExecuteVrFeedFunction('nowPlayingUpdate', json); + AppApi.ExecuteVrOverlayFunction('nowPlayingUpdate', json); + }; - return compareByLocationAt(b.ref, a.ref); - }); - break; - case 'Sort by Location': - sorts.push(compareByLocation); - break; - case 'None': - sorts.push(() => 0); - break; - } + $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 (a, b) => { - let res; - for (const sort of sorts) { - res = sort(a, b); - if (res !== 0) { - return res; - } - } - return res; - }; + return `${hoursOut + pad(minutes, 2)}:${pad(seconds, 2)}`; }; - // VIP friends - $app.computed.vipFriends = function () { - if (!this.sortVIPFriends) { - return this.vipFriends_; + $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]; } - this.sortVIPFriends = false; - - this.vipFriends_.sort(getFriendsSortFunction(this.sidebarSortMethods)); - return this.vipFriends_; + 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; }; - // Online friends - $app.computed.onlineFriends = function () { - if (!this.sortOnlineFriends) { - return this.onlineFriends_; + $app.methods.updateAutoStateChange = function () { + if ( + this.autoStateChange === 'Off' || + !this.isGameRunning || + !this.lastLocation.playerList.size || + this.lastLocation.location === '' || + this.lastLocation.location === 'traveling' + ) { + return; } - this.sortOnlineFriends = false; - this.onlineFriends_.sort( - getFriendsSortFunction(this.sidebarSortMethods) - ); + const otherPeopleExists = this.lastLocation.playerList.size > 1; + const prevStatus = API.currentUser.status; + let nextStatus = prevStatus; - return this.onlineFriends_; - }; + switch (this.autoStateChange) { + case 'Active or Ask Me': + nextStatus = otherPeopleExists ? 'ask me' : 'active'; + break; - // Active friends - $app.computed.activeFriends = function () { - if (!this.sortActiveFriends) { - return this.activeFriends_; - } - this.sortActiveFriends = false; + case 'Active or Busy': + nextStatus = otherPeopleExists ? 'busy' : 'active'; + break; - this.activeFriends_.sort( - getFriendsSortFunction(this.sidebarSortMethods) - ); + case 'Join Me or Ask Me': + nextStatus = otherPeopleExists ? 'ask me' : 'join me'; + break; - return this.activeFriends_; - }; + case 'Join Me or Busy': + nextStatus = otherPeopleExists ? 'busy' : 'join me'; + break; - // Offline friends - $app.computed.offlineFriends = function () { - if (!this.sortOfflineFriends) { - return this.offlineFriends_; + case 'Ask Me or Busy': + nextStatus = otherPeopleExists ? 'ask me' : 'busy'; + break; } - this.sortOfflineFriends = false; - this.offlineFriends_.sort( - getFriendsSortFunction(this.sidebarSortMethods) - ); + if (prevStatus === nextStatus) { + return; + } - return this.offlineFriends_; + API.saveCurrentUser({ + status: nextStatus + }); }; - $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); + $app.methods.lookupUser = async function (ref) { + if (ref.userId) { + this.showUserDialog(ref.userId); + return; } - if (!user.isFriend) { - return style; + if (!ref.displayName || ref.displayName.substring(0, 3) === 'ID:') { + return; } - 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; + for (var ctx of API.cachedUsers.values()) { + if (ctx.displayName === ref.displayName) { + this.showUserDialog(ctx.id); + return; } - } 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.last_platform && user.last_platform !== 'standalonewindows') { - style.mobile = true; + 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; + } } - 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') { - API.deleteFriend({ - userId: id - }); - } - } - }); + this.$refs.searchTab.currentName = '0'; + this.$refs.menu.activeIndex = 'search'; }; // #endregion - // #region | App: Quick Search + // #region | App: Search - $app.data.quickSearch = ''; - $app.data.quickSearchItems = []; + $app.data.searchText = ''; + $app.data.searchUserResults = []; + $app.data.searchUserParams = {}; + $app.data.searchWorldResults = []; + $app.data.searchWorldOption = ''; + $app.data.searchWorldParams = {}; + $app.data.searchAvatarResults = []; + $app.data.searchAvatarPage = []; + $app.data.searchAvatarPageNum = 0; + $app.data.searchAvatarFilter = ''; + $app.data.searchAvatarSort = ''; + $app.data.searchAvatarFilterRemote = ''; + $app.data.searchGroupResults = []; + $app.data.searchGroupParams = {}; + $app.data.isSearchUserLoading = false; + $app.data.isSearchWorldLoading = false; + $app.data.isSearchAvatarLoading = false; + $app.data.isSearchGroupLoading = false; - var localeIncludes = function (str, search, comparer) { - // These checks are stolen from https://stackoverflow.com/a/69623589/11030436 - if (search === '') { - return true; - } else if (!str || !search) { - return false; - } - const strObj = String(str); - const searchObj = String(search); + API.$on('LOGIN', function () { + $app.searchText = ''; + $app.searchUserResults = []; + $app.searchUserParams = {}; + $app.searchWorldResults = []; + $app.searchWorldOption = ''; + $app.searchWorldParams = {}; + $app.searchAvatarResults = []; + $app.searchAvatarPage = []; + $app.searchAvatarPageNum = 0; + $app.searchAvatarFilter = ''; + $app.searchAvatarSort = ''; + $app.searchAvatarFilterRemote = ''; + $app.searchGroupResults = []; + $app.searchGroupParams = {}; + $app.isSearchUserLoading = false; + $app.isSearchWorldLoading = false; + $app.isSearchAvatarLoading = false; + }); - if (strObj.length === 0) { - return false; - } + $app.methods.clearSearch = function () { + this.searchText = ''; + this.searchUserParams = {}; + this.searchWorldParams = {}; + this.searchUserResults = []; + this.searchWorldResults = []; + this.searchAvatarResults = []; + this.searchAvatarPage = []; + this.searchAvatarPageNum = 0; + this.searchGroupParams = {}; + this.searchGroupResults = []; + }; - if (searchObj.length > strObj.length) { - return false; + $app.methods.search = function () { + switch (this.$refs.searchTab.currentName) { + case '0': + this.searchUser(); + break; + case '1': + this.searchWorld({}); + break; + case '2': + this.searchAvatar(); + break; + case '3': + this.searchGroup(); + break; } + }; - // Now simply loop through each substring and compare them - for (let i = 0; i < str.length - searchObj.length + 1; i++) { - const substr = strObj.substring(i, i + searchObj.length); - if (comparer.compare(substr, searchObj) === 0) { - return true; - } - } - return false; + $app.methods.searchUserByDisplayName = async function (displayName) { + this.searchUserParams = { + n: 10, + offset: 0, + fuzzy: false, + search: displayName + }; + await this.moreSearchUser(); }; - // 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.searchUser = async function () { + this.searchUserParams = { + n: 10, + offset: 0, + search: this.searchText + }; + await this.moreSearchUser(); }; - $app.methods.quickSearchRemoteMethod = function (query) { - if (!query) { - this.quickSearchItems = []; + $app.methods.moreSearchUser = async function (go) { + var params = this.searchUserParams; + if (go) { + params.offset += params.n * go; + if (params.offset < 0) { + params.offset = 0; + } } + this.isSearchUserLoading = true; + await API.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; + }); + }; - const results = []; - const cleanQuery = removeWhitespace(query); + $app.data.searchWorldLabs = false; - for (let ctx of this.friends.values()) { - if (typeof ctx.ref === 'undefined') { - continue; + $app.methods.searchWorld = function (ref) { + this.searchWorldOption = ''; + var params = { + n: 10, + offset: 0 + }; + switch (ref.sortHeading) { + case 'featured': + params.sort = 'order'; + params.featured = 'true'; + break; + case 'trending': + params.sort = 'popularity'; + params.featured = 'false'; + break; + case 'updated': + params.sort = 'updated'; + break; + case 'created': + params.sort = 'created'; + break; + case 'publication': + params.sort = 'publicationDate'; + break; + case 'shuffle': + params.sort = 'shuffle'; + break; + case 'active': + this.searchWorldOption = 'active'; + break; + case 'recent': + this.searchWorldOption = 'recent'; + break; + case 'favorite': + this.searchWorldOption = 'favorites'; + break; + case 'labs': + params.sort = 'labsPublicationDate'; + break; + case 'heat': + params.sort = 'heat'; + params.featured = 'false'; + break; + default: + params.sort = 'relevance'; + params.search = this.replaceBioSymbols(this.searchText); + break; + } + params.order = ref.sortOrder || 'descending'; + if (ref.sortOwnership === 'mine') { + params.user = 'me'; + params.releaseStatus = 'all'; + } + if (ref.tag) { + params.tag = ref.tag; + } + if (!this.searchWorldLabs) { + if (params.tag) { + params.tag += ',system_approved'; + } else { + params.tag = 'system_approved'; } + } + // TODO: option.platform + this.searchWorldParams = params; + this.moreSearchWorld(); + }; - const cleanName = removeConfusables(ctx.name); - let match = localeIncludes( - cleanName, - cleanQuery, - this.stringComparer - ); - if (!match) { - // Also check regular name in case search is with special characters - match = localeIncludes( - ctx.name, - cleanQuery, - this.stringComparer - ); + $app.methods.moreSearchWorld = function (go) { + var params = this.searchWorldParams; + if (go) { + params.offset += params.n * go; + if (params.offset < 0) { + params.offset = 0; } - // 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 = localeIncludes(ctx.memo, query, this.stringComparer); + } + this.isSearchWorldLoading = true; + API.getWorlds(params, this.searchWorldOption) + .finally(() => { + this.isSearchWorldLoading = false; + }) + .then((args) => { + var map = new Map(); + for (var json of args.json) { + var ref = API.cachedWorlds.get(json.id); + if (typeof ref !== 'undefined') { + map.set(ref.id, ref); + } + } + this.searchWorldResults = Array.from(map.values()); + return args; + }); + }; + + $app.methods.searchAvatar = async function () { + this.isSearchAvatarLoading = true; + if (!this.searchAvatarFilter) { + this.searchAvatarFilter = 'all'; + } + if (!this.searchAvatarSort) { + this.searchAvatarSort = 'name'; + } + if (!this.searchAvatarFilterRemote) { + this.searchAvatarFilterRemote = 'all'; + } + if (this.searchAvatarFilterRemote !== 'local') { + this.searchAvatarSort = 'name'; + } + var avatars = new Map(); + var query = this.searchText.toUpperCase(); + if (!query) { + for (var ref of API.cachedAvatars.values()) { + switch (this.searchAvatarFilter) { + case 'all': + avatars.set(ref.id, ref); + break; + case 'public': + if (ref.releaseStatus === 'public') { + avatars.set(ref.id, ref); + } + break; + case 'private': + if (ref.releaseStatus === 'private') { + avatars.set(ref.id, ref); + } + break; + } } - if (!match && ctx.ref.note) { - match = localeIncludes( - ctx.ref.note, - query, - this.stringComparer - ); + this.isSearchAvatarLoading = false; + } else { + if ( + this.searchAvatarFilterRemote === 'all' || + this.searchAvatarFilterRemote === 'local' + ) { + for (var ref of API.cachedAvatars.values()) { + var match = ref.name.toUpperCase().includes(query); + if (!match && ref.description) { + match = ref.description.toUpperCase().includes(query); + } + if (!match && ref.authorName) { + match = ref.authorName.toUpperCase().includes(query); + } + if (match) { + switch (this.searchAvatarFilter) { + case 'all': + avatars.set(ref.id, ref); + break; + case 'public': + if (ref.releaseStatus === 'public') { + avatars.set(ref.id, ref); + } + break; + case 'private': + if (ref.releaseStatus === 'private') { + avatars.set(ref.id, ref); + } + break; + } + } + } } - - if (match) { - results.push({ - value: ctx.id, - label: ctx.name, - ref: ctx.ref, - name: ctx.name - }); + if ( + (this.searchAvatarFilterRemote === 'all' || + this.searchAvatarFilterRemote === 'remote') && + this.avatarRemoteDatabase && + query.length >= 3 + ) { + var data = await this.lookupAvatars('search', query); + if (data && typeof data === 'object') { + data.forEach((avatar) => { + avatars.set(avatar.id, avatar); + }); + } } + this.isSearchAvatarLoading = false; } - - 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; + var avatarsArray = Array.from(avatars.values()); + if (this.searchAvatarFilterRemote === 'local') { + switch (this.searchAvatarSort) { + case 'updated': + avatarsArray.sort(compareByUpdatedAt); + break; + case 'created': + avatarsArray.sort(compareByCreatedAt); + break; + case 'name': + avatarsArray.sort(compareByName); + break; } - return compareByName(a, b); - }); - if (results.length > 4) { - results.length = 4; } - results.push({ - value: `search:${query}`, - label: query - }); - - this.quickSearchItems = results; + this.searchAvatarPageNum = 0; + this.searchAvatarResults = avatarsArray; + this.searchAvatarPage = avatarsArray.slice(0, 10); }; - $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.$refs.menu.activeIndex = 'friendsList'; - } else { - this.$refs.menu.activeIndex = 'search'; - this.searchText = searchText; - this.lookupUser({ displayName: searchText }); - } - } else { - this.showUserDialog(value); - } - this.quickSearchVisibleChange(value); + $app.methods.moreSearchAvatar = function (n) { + if (n === -1) { + this.searchAvatarPageNum--; + var offset = this.searchAvatarPageNum * 10; } - }; - - // NOTE: 그냥 열고 닫고 했을때 changed 이벤트 발생이 안되기 때문에 넣음 - $app.methods.quickSearchVisibleChange = function (value) { - if (value) { - this.quickSearch = ''; - this.quickSearchItems = []; - this.quickSearchUserHistory(); + if (n === 1) { + this.searchAvatarPageNum++; + var offset = this.searchAvatarPageNum * 10; } + this.searchAvatarPage = this.searchAvatarResults.slice( + offset, + offset + 10 + ); }; - // #endregion - // #region | App: Quick Search User History - - $app.data.showUserDialogHistory = new Set(); + $app.methods.searchGroup = async function () { + this.searchGroupParams = { + n: 10, + offset: 0, + query: this.replaceBioSymbols(this.searchText) + }; + await this.moreSearchGroup(); + }; - $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 - }); + $app.methods.moreSearchGroup = async function (go) { + var params = this.searchGroupParams; + if (go) { + params.offset += params.n * go; + if (params.offset < 0) { + params.offset = 0; } - }); - this.quickSearchItems = results; + } + this.isSearchGroupLoading = true; + await API.groupSearch(params) + .finally(() => { + this.isSearchGroupLoading = false; + }) + .then((args) => { + var map = new Map(); + for (var json of args.json) { + var ref = API.cachedGroups.get(json.id); + if (typeof ref !== 'undefined') { + map.set(ref.id, ref); + } + } + this.searchGroupResults = Array.from(map.values()); + return args; + }); }; // #endregion - // #region | App: Feed + // #region | App: Favorite - $app.methods.feedSearch = function (row) { - var value = this.feedTable.search.toUpperCase(); - if (!value) { - return true; - } - if ( - value.startsWith('wrld_') && - String(row.location).toUpperCase().includes(value) - ) { - return true; - } - switch (row.type) { - case 'GPS': - if (String(row.displayName).toUpperCase().includes(value)) { - return true; - } - if (String(row.worldName).toUpperCase().includes(value)) { - return true; - } - return false; - case 'Online': - if (String(row.displayName).toUpperCase().includes(value)) { - return true; - } - if (String(row.worldName).toUpperCase().includes(value)) { - return true; - } - return false; - case 'Offline': - if (String(row.displayName).toUpperCase().includes(value)) { - return true; - } - if (String(row.worldName).toUpperCase().includes(value)) { - return true; - } - return false; - case 'Status': - if (String(row.displayName).toUpperCase().includes(value)) { - return true; - } - if (String(row.status).toUpperCase().includes(value)) { - return true; - } - if ( - String(row.statusDescription).toUpperCase().includes(value) - ) { - return true; - } - return false; - case 'Avatar': - if (String(row.displayName).toUpperCase().includes(value)) { - return true; - } - if (String(row.avatarName).toUpperCase().includes(value)) { - return true; - } - return false; - case 'Bio': - if (String(row.displayName).toUpperCase().includes(value)) { - return true; - } - if (String(row.bio).toUpperCase().includes(value)) { - return true; - } - if (String(row.previousBio).toUpperCase().includes(value)) { - return true; - } - return false; - } - return true; - }; + $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; - $app.data.tablePageSize = await configRepository.getInt( - 'VRCX_tablePageSize', - 15 - ); + 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; + }); - $app.data.feedTable = { - data: [], - search: '', - vip: false, - loading: false, - filter: [], - 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, 25, 50, 100] - } - }; + API.$on('FAVORITE', function (args) { + $app.applyFavorite(args.ref.type, args.ref.favoriteId, args.sortTop); + }); - $app.data.feedSessionTable = []; + API.$on('FAVORITE:@DELETE', function (args) { + $app.applyFavorite(args.ref.type, args.ref.favoriteId); + }); - $app.methods.feedTableLookup = async function () { - await configRepository.setString( - 'VRCX_feedTableFilters', - JSON.stringify(this.feedTable.filter) - ); - await configRepository.setBool( - 'VRCX_feedTableVIPFilter', - this.feedTable.vip - ); - this.feedTable.loading = true; - var vipList = []; - if (this.feedTable.vip) { - vipList = Array.from(this.localFavoriteFriends.values()); - } - this.feedTable.data = await database.lookupFeedDatabase( - this.feedTable.search, - this.feedTable.filter, - vipList - ); - this.feedTable.loading = false; - }; + API.$on('USER', function (args) { + $app.applyFavorite('friend', args.ref.id); + }); - $app.data.dontLogMeOut = false; + API.$on('WORLD', function (args) { + $app.applyFavorite('world', args.ref.id); + }); - API.$on('LOGIN', async function (args) { - $app.friendLog = new Map(); - $app.feedTable.data = []; - $app.feedSessionTable = []; - $app.friendLogInitStatus = false; - await database.initUserTables(args.json.id); - $app.$refs.menu.activeIndex = 'feed'; - // 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(); - $app.feedTableLookup(); - // eslint-disable-next-line require-atomic-updates - $app.notificationTable.data = await database.getNotifications(); - await this.refreshNotifications(); - await $app.loadCurrentUserGroups( - args.json.id, - args.json?.presence?.groups - ); - await $app.getCurrentUserGroups(); - try { - if ( - await configRepository.getBool(`friendLogInit_${args.json.id}`) - ) { - await $app.getFriendLog(); - } else { - await $app.initFriendLog(args.json.id); - } - } catch (err) { - if (!$app.dontLogMeOut) { - $app.$message({ - message: 'Failed to load freinds list, logging out', - type: 'error' - }); - this.logout(); - throw err; - } - } - $app.getAvatarHistory(); - $app.getAllMemos(); - if ($app.randomUserColours) { - $app.getNameColour(this.currentUser.id).then((colour) => { - this.currentUser.$userColour = colour; - }); - $app.userColourInit(); - } - 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(); + API.$on('AVATAR', function (args) { + $app.applyFavorite('avatar', args.ref.id); }); - $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() + $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 }; - 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; - } + 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; } - var userMap = { - displayName: ctx.displayName, - userId: ctx.userId, - joinTime: Date.parse(ctx.created_at), - lastAvatar: '' - }; - this.lastLocation.playerList.set(ctx.displayName, userMap); - if (this.friends.has(ctx.userId)) { - this.lastLocation.friendList.set( - ctx.displayName, - userMap - ); + } 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; } } - if (ctx.type === 'OnPlayerLeft') { - this.lastLocation.playerList.delete(ctx.displayName); - this.lastLocation.friendList.delete(ctx.displayName); - } - } - this.lastLocation.playerList.forEach((ref1) => { - if (ref1.userId && !API.cachedUsers.has(ref1.userId)) { - API.getUser({ userId: ref1.userId }); + 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); + } } - }); - - this.updateCurrentUserLocation(); - this.updateCurrentInstanceWorld(); - this.updateVRLastLocation(); - this.getCurrentInstanceUserList(); - this.applyUserDialogLocation(); - this.applyWorldDialogInstances(); - this.applyGroupDialogInstances(); + 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.data.robotUrl = `${API.endpointDomain}/file/file_0e8c4e32-7444-44ea-ade4-313c010d4bae/1/file`; + $app.methods.deleteFavorite = function (objectId) { + // FIXME: 메시지 수정 + this.$confirm('Continue? Delete Favorite', 'Confirm', { + confirmButtonText: 'Confirm', + cancelButtonText: 'Cancel', + type: 'info', + callback: (action) => { + if (action === 'confirm') { + API.deleteFavorite({ + objectId + }); + } + } + }); + }; - API.$on('USER:UPDATE', async function (args) { - var { ref, props } = args; - var friend = $app.friends.get(ref.id); - if (typeof friend === 'undefined') { + $app.methods.deleteFavoriteNoConfirm = function (objectId) { + if (!objectId) { return; } - if (props.location && ref.id === $app.userDialog.id) { - // update user dialog instance occupants - $app.applyUserDialogLocation(true); + API.deleteFavorite({ + objectId + }); + }; + + $app.methods.clearFavoriteGroup = function (ctx) { + // FIXME: 메시지 수정 + this.$confirm('Continue? Clear Group', 'Confirm', { + confirmButtonText: 'Confirm', + cancelButtonText: 'Cancel', + type: 'info', + callback: (action) => { + if (action === 'confirm') { + API.clearFavoriteGroup({ + type: ctx.type, + group: ctx.name + }); + } + } + }); + }; + + $app.computed.favoriteFriends = function () { + if (this.sortFavoriteFriends) { + this.sortFavoriteFriends = false; + this.favoriteFriendsSorted.sort(compareByName); } - if (props.location && ref.$location.worldId === $app.worldDialog.id) { - $app.applyWorldDialogInstances(); + if (this.sortFavorites) { + return this.favoriteFriends_; } - if (props.location && ref.$location.groupId === $app.groupDialog.id) { - $app.applyGroupDialogInstances(); + return this.favoriteFriendsSorted; + }; + + $app.computed.favoriteWorlds = function () { + if (this.sortFavoriteWorlds) { + this.sortFavoriteWorlds = false; + this.favoriteWorldsSorted.sort(compareByName); } - if ( - 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 time = props.location[2]; - if (previousLocation === 'traveling') { - previousLocation = ref.$previousLocation; - var travelTime = Date.now() - ref.$travelingToTime; - time -= travelTime; - if (time < 0) { - time = 0; - } - } - if (ref.$previousLocation === props.location[0]) { - // location traveled to is the same - ref.$location_at = Date.now() - props.location[2]; - } else { - var worldName = await $app.getWorldName(props.location[0]); - var groupName = await $app.getGroupName(props.location[0]); - var feed = { - created_at: new Date().toJSON(), - type: 'GPS', - userId: ref.id, - displayName: ref.displayName, - location: props.location[0], - 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 (this.sortFavorites) { + return this.favoriteWorlds_; } - 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); + return this.favoriteWorldsSorted; + }; + + $app.computed.favoriteAvatars = function () { + if (this.sortFavoriteAvatars) { + this.sortFavoriteAvatars = false; + this.favoriteAvatarsSorted.sort(compareByName); } - var imageMatches = false; - if ( - props.currentAvatarThumbnailImageUrl && - props.currentAvatarThumbnailImageUrl[0] && - props.currentAvatarThumbnailImageUrl[1] && - props.currentAvatarThumbnailImageUrl[0] === - props.currentAvatarThumbnailImageUrl[1] - ) { - imageMatches = true; + if (this.sortFavorites) { + return this.favoriteAvatars_; } - 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]; + 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') } - if (props.bio[1]) { - previousBio = props.bio[1]; + ], + tableProps: { + stripe: true, + size: 'mini', + defaultSort: { + prop: 'created_at', + order: 'descending' } - var feed = { - created_at: new Date().toJSON(), - type: 'Bio', - userId: ref.id, - displayName: ref.displayName, - bio, - previousBio - }; - $app.addFeed(feed); - database.addBioToDatabase(feed); + }, + pageSize: $app.data.tablePageSize, + paginationProps: { + small: true, + layout: 'sizes,prev,pager,next,total', + pageSizes: [10, 15, 25, 50, 100] } + }; + + API.$on('USER:CURRENT', function (args) { + $app.updateFriendships(args.ref); }); - $app.methods.addFeed = function (feed) { - this.queueFeedNoty(feed); - this.feedSessionTable.push(feed); - this.updateSharedFeed(false); + API.$on('USER', function (args) { + $app.updateFriendship(args.ref); if ( - this.feedTable.filter.length > 0 && - !this.feedTable.filter.includes(feed.type) + $app.friendLogInitStatus && + args.json.isFriend && + !$app.friendLog.has(args.ref.id) && + args.json.id !== this.currentUser.id ) { - return; - } - if (this.feedTable.vip && !this.localFavoriteFriends.has(feed.userId)) { - return; - } - if (!this.feedSearch(feed)) { - return; + $app.addFriendship(args.ref.id); } - this.feedTable.data.push(feed); - this.sweepFeed(); - this.notifyMenu('feed'); - }; + }); - $app.methods.clearFeed = function () { - // FIXME: 메시지 수정 - this.$confirm('Continue? Clear Feed', 'Confirm', { - confirmButtonText: 'Confirm', - cancelButtonText: 'Cancel', - type: 'info', - callback: (action) => { - if (action === 'confirm') { - // 필터된 데이터만 삭제 하려면.. 허어 - var T = this.feedTable; - T.data = T.data.filter( - (row) => - !T.filters.every((filter) => { - if (filter.value) { - if (!Array.isArray(filter.value)) { - if (filter.filterFn) { - return filter.filterFn(row, filter); - } - return String(row[filter.prop]) - .toUpperCase() - .includes( - String( - filter.value - ).toUpperCase() - ); - } - if (filter.value.length) { - if (filter.filterFn) { - return filter.filterFn(row, filter); - } - var prop = String( - row[filter.prop] - ).toUpperCase(); - return filter.value.some((v) => - prop.includes( - String(v).toUpperCase() - ) - ); - } - } - return true; - }) - ); - } - } - }); - }; + API.$on('FRIEND:ADD', function (args) { + $app.addFriendship(args.params.userId); + }); - $app.methods.sweepFeed = function () { - var { data } = this.feedTable; - var j = data.length; - if (j > this.maxTableSize) { - data.splice(0, j - this.maxTableSize); - } + API.$on('FRIEND:DELETE', function (args) { + $app.deleteFriendship(args.params.userId); + }); - var date = new Date(); - date.setDate(date.getDate() - 1); // 24 hour limit - var limit = date.toJSON(); - var i = 0; - var k = this.feedSessionTable.length; - while (i < k && this.feedSessionTable[i].created_at < limit) { - ++i; + API.$on('FRIEND:REQUEST', function (args) { + var ref = this.cachedUsers.get(args.params.userId); + if (typeof ref === 'undefined') { + return; } - if (i === k) { - this.feedSessionTable = []; - } else if (i) { - this.feedSessionTable.splice(0, i); + var friendLogHistory = { + created_at: new Date().toJSON(), + type: 'FriendRequest', + userId: ref.id, + displayName: ref.displayName + }; + $app.friendLogTable.data.push(friendLogHistory); + database.addFriendLogHistory(friendLogHistory); + }); + + API.$on('FRIEND:REQUEST:CANCEL', function (args) { + var ref = this.cachedUsers.get(args.params.userId); + if (typeof ref === 'undefined') { + return; } - }; + var friendLogHistory = { + created_at: new Date().toJSON(), + type: 'CancelFriendRequest', + userId: ref.id, + displayName: ref.displayName + }; + $app.friendLogTable.data.push(friendLogHistory); + database.addFriendLogHistory(friendLogHistory); + }); - // #endregion - // #region | App: gameLog + $app.data.friendLogInitStatus = false; - $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', + $app.methods.initFriendLog = async function (userId) { + await this.updateDatabaseVersion(); + 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, - 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() + trustLevel: ref.$trustLevel }; - database.updateGamelogLocationTimeToDatabase(update); + this.friendLog.set(friend.id, row); + sqlValues.unshift(row); } - this.gameLogApiLoggingEnabled = false; - 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(); + database.setFriendLogCurrentArray(sqlValues); + await configRepository.setBool(`friendLogInit_${userId}`, true); + this.friendLogInitStatus = true; }; - $app.data.lastLocation$ = { - tag: '', - instanceId: '', - accessType: '', - worldName: '', - worldCapacity: 0, - joinUrl: '', - statusName: '', - statusImage: '' + $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.gameLogSearch = function (row) { - var value = this.gameLogTable.search.toUpperCase(); - if (!value) { - return true; - } - if ( - value.startsWith('wrld_') && - String(row.location).toUpperCase().includes(value) - ) { - return true; - } - switch (row.type) { - case 'Location': - if (String(row.worldName).toUpperCase().includes(value)) { - return true; - } - return false; - case 'OnPlayerJoined': - if (String(row.displayName).toUpperCase().includes(value)) { - return true; - } - return false; - case 'OnPlayerLeft': - if (String(row.displayName).toUpperCase().includes(value)) { - return true; - } - return false; - case 'PortalSpawn': - if (String(row.displayName).toUpperCase().includes(value)) { - return true; - } - if (String(row.worldName).toUpperCase().includes(value)) { - return true; - } - return false; - case 'Event': - if (String(row.data).toUpperCase().includes(value)) { - return true; - } - return false; - case 'External': - if (String(row.message).toUpperCase().includes(value)) { - return true; - } - if (String(row.displayName).toUpperCase().includes(value)) { - return true; - } - return false; - case 'VideoPlay': - if (String(row.displayName).toUpperCase().includes(value)) { - return true; - } - if (String(row.videoName).toUpperCase().includes(value)) { - return true; - } - if (String(row.videoUrl).toUpperCase().includes(value)) { - return true; - } - return false; - case 'StringLoad': - case 'ImageLoad': - if (String(row.resourceUrl).toUpperCase().includes(value)) { - return true; - } - return false; + $app.methods.getFriendLog = async function () { + await this.updateDatabaseVersion(); + var friendLogCurrentArray = await database.getFriendLogCurrent(); + for (var friend of friendLogCurrentArray) { + this.friendLog.set(friend.userId, friend); } - return true; - }; - - $app.data.gameLogTable = { - data: [], - loading: false, - search: '', - filter: [], - tableProps: { - stripe: true, - size: 'mini', - defaultSort: { - prop: 'created_at', - order: 'descending' + this.friendLogTable.data = []; + this.friendLogTable.data = await database.getFriendLogHistory(); + await API.refreshFriends(); + 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); } - }, - pageSize: $app.data.tablePageSize, - paginationProps: { - small: true, - layout: 'sizes,prev,pager,next,total', - pageSizes: [10, 15, 25, 50, 100] + } + if (typeof API.currentUser.friends !== 'undefined') { + this.updateFriendships(API.currentUser); } }; - $app.data.gameLogSessionTable = []; - - $app.methods.gameLogTableLookup = async function () { - await configRepository.setString( - 'VRCX_gameLogTableFilters', - JSON.stringify(this.gameLogTable.filter) - ); - this.gameLogTable.loading = true; - this.gameLogTable.data = await database.lookupGameLogDatabase( - this.gameLogTable.search, - this.gameLogTable.filter - ); - this.gameLogTable.loading = false; - }; - - $app.methods.addGameLog = function (entry) { - this.gameLogSessionTable.push(entry); - this.updateSharedFeed(false); - if (entry.type === 'VideoPlay') { - // event time can be before last gameLog entry - this.updateSharedFeed(true); - } - if ( - entry.type === 'LocationDestination' || - entry.type === 'AvatarChange' || - entry.type === 'ChatBoxMessage' || - (entry.userId === API.currentUser.id && - (entry.type === 'OnPlayerJoined' || - entry.type === 'OnPlayerLeft')) - ) { - return; - } - if ( - this.gameLogTable.filter.length > 0 && - !this.gameLogTable.filter.includes(entry.type) - ) { + $app.methods.addFriendship = function (id) { + if (!this.friendLogInitStatus || this.friendLog.has(id)) { return; } - if (!this.gameLogSearch(entry)) { + var ref = API.cachedUsers.get(id); + if (typeof ref === 'undefined') { + try { + API.getUser({ + userId: id + }); + } catch {} return; } - this.gameLogTable.data.push(entry); - this.sweepGameLog(); - this.notifyMenu('gameLog'); - }; - - $app.methods.resetGameLog = async function () { - await gameLogService.reset(); - this.gameLogTable.data = []; - this.lastLocationReset(); - }; - - $app.methods.sweepGameLog = function () { - var { data } = this.gameLogTable; - var j = data.length; - if (j > this.maxTableSize) { - data.splice(0, j - this.maxTableSize); - } - - var date = new Date(); - date.setDate(date.getDate() - 1); // 24 hour limit - var limit = date.toJSON(); - var i = 0; - var k = this.gameLogSessionTable.length; - while (i < k && this.gameLogSessionTable[i].created_at < limit) { - ++i; - } - if (i === k) { - this.gameLogSessionTable = []; - } else if (i) { - this.gameLogSessionTable.splice(0, i); - } - }; - - $app.methods.refreshEntireGameLog = async function () { - await gameLogService.setDateTill('1970-01-01'); - await database.initTables(); - await this.resetGameLog(); - var location = ''; - for (var gameLog of await gameLogService.getAll()) { - if (gameLog.type === 'location') { - location = gameLog.location; + API.getFriendStatus({ + userId: id + }).then((args) => { + if (args.json.isFriend && !this.friendLog.has(id)) { + this.addFriend(id, ref.state); + var friendLogHistory = { + created_at: new Date().toJSON(), + type: 'Friend', + userId: id, + displayName: ref.displayName + }; + this.friendLogTable.data.push(friendLogHistory); + database.addFriendLogHistory(friendLogHistory); + this.queueFriendLogNoty(friendLogHistory); + var friendLogCurrent = { + userId: id, + displayName: ref.displayName, + trustLevel: ref.$trustLevel + }; + this.friendLog.set(id, friendLogCurrent); + database.setFriendLogCurrent(friendLogCurrent); + this.notifyMenu('friendLog'); + this.deleteFriendRequest(id); + this.updateSharedFeed(true); + API.getUser({ + userId: id + }).then(() => { + if (this.userDialog.visible && id === this.userDialog.id) { + this.applyUserDialogLocation(true); + } + }); } - this.addGameLogEntry(gameLog, location); - } - this.getGameLogTable(); - }; - - $app.methods.getGameLogTable = async function () { - await database.initTables(); - this.gameLogSessionTable = await database.getGamelogDatabase(); - var dateTill = await database.getLastDateGameLogDatabase(); - this.updateGameLog(dateTill); + }); }; - $app.methods.updateGameLog = async function (dateTill) { - await gameLogService.setDateTill(dateTill); - await gameLogService.reset(); - await new Promise((resolve) => { - workerTimers.setTimeout(resolve, 10000); - }); - var location = ''; - for (var gameLog of await gameLogService.getAll()) { - if (gameLog.type === 'location') { - location = gameLog.location; + $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; } - this.addGameLogEntry(gameLog, location); } }; - $app.methods.addGameLogEvent = function (json) { - var rawLogs = JSON.parse(json); - var gameLog = gameLogService.parseRawGameLog( - rawLogs[1], - rawLogs[2], - rawLogs.slice(3) - ); + $app.methods.deleteFriendship = function (id) { + var ctx = this.friendLog.get(id); + if (typeof ctx === 'undefined') { + return; + } + API.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 + }; + 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.displayName !== ref.displayName) { + if (ctx.displayName) { + var friendLogHistoryDisplayName = { + created_at: new Date().toJSON(), + type: 'DisplayName', + userId: ref.id, + displayName: ref.displayName, + previousDisplayName: ctx.displayName + }; + this.friendLogTable.data.push(friendLogHistoryDisplayName); + database.addFriendLogHistory(friendLogHistoryDisplayName); + this.queueFriendLogNoty(friendLogHistoryDisplayName); + var friendLogCurrent = { + userId: ref.id, + displayName: ref.displayName, + trustLevel: ref.$trustLevel + }; + this.friendLog.set(ref.id, friendLogCurrent); + database.setFriendLogCurrent(friendLogCurrent); + ctx.displayName = ref.displayName; + this.notifyMenu('friendLog'); + this.updateSharedFeed(true); + } + } if ( - this.debugGameLog && - gameLog.type !== 'photon-id' && - gameLog.type !== 'api-request' && - gameLog.type !== 'udon-exception' + ref.$trustLevel && + ctx.trustLevel && + ctx.trustLevel !== ref.$trustLevel ) { - console.log('gameLog:', gameLog); + 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 + }; + 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 + }; + this.friendLogTable.data.push(friendLogHistoryTrustLevel); + database.addFriendLogHistory(friendLogHistoryTrustLevel); + this.queueFriendLogNoty(friendLogHistoryTrustLevel); + var friendLogCurrent2 = { + userId: ref.id, + displayName: ref.displayName, + trustLevel: ref.$trustLevel + }; + this.friendLog.set(ref.id, friendLogCurrent2); + database.setFriendLogCurrent(friendLogCurrent2); + this.notifyMenu('friendLog'); + this.updateSharedFeed(true); } - this.addGameLogEntry(gameLog, this.lastLocation.location); + ctx.trustLevel = ref.$trustLevel; }; - $app.methods.deleteGameLogEntryPrompt = function (row) { + $app.methods.deleteFriendLog = function (row) { this.$confirm('Continue? Delete Log', 'Confirm', { confirmButtonText: 'Confirm', cancelButtonText: 'Cancel', type: 'info', callback: (action) => { if (action === 'confirm') { - this.deleteGameLogEntry(row); + $app.removeFromArray(this.friendLogTable.data, row); + database.deleteFriendLogHistory(row.rowId); } } }); }; - $app.methods.deleteGameLogEntry = function (row) { - removeFromArray(this.gameLogTable.data, row); - database.deleteGameLogEntry(row); - console.log(row); - database.getGamelogDatabase().then((data) => { - this.gameLogSessionTable = data; - this.updateSharedFeed(true); - }); + // #endregion + // #region | App: Moderation + + $app.data.playerModerationTable = { + data: [], + lastRunLength: 0, + filters: [ + { + prop: 'type', + value: [], + filterFn: (row, filter) => + filter.value.some((v) => v === row.type) + }, + { + prop: ['sourceDisplayName', 'targetDisplayName'], + value: '' + } + ], + tableProps: { + stripe: true, + size: 'mini', + defaultSort: { + prop: 'created', + order: 'descending' + } + }, + pageSize: $app.data.tablePageSize, + paginationProps: { + small: true, + layout: 'sizes,prev,pager,next,total', + pageSizes: [10, 15, 25, 50, 100] + } }; - $app.data.lastLocationDestination = ''; - $app.data.lastLocationDestinationTime = 0; - $app.data.lastVideoUrl = ''; - $app.data.lastResourceloadUrl = ''; - $app.data.gameLogApiLoggingEnabled = false; + API.$on('LOGIN', function () { + $app.playerModerationTable.data = []; + }); - $app.methods.addGameLogEntry = function (gameLog, location) { - if (this.gameLogDisabled) { - return; + API.$on('PLAYER-MODERATION', function (args) { + var { ref } = args; + var array = $app.playerModerationTable.data; + var { length } = array; + for (var i = 0; i < length; ++i) { + if (array[i].id === ref.id) { + Vue.set(array, i, ref); + return; + } } - var userId = gameLog.userId; - if (!userId && gameLog.displayName) { - for (var ref of API.cachedUsers.values()) { - if (ref.displayName === gameLog.displayName) { - userId = ref.id; - break; - } + $app.playerModerationTable.data.push(ref); + }); + + API.$on('PLAYER-MODERATION:@DELETE', function (args) { + var { ref } = args; + var array = $app.playerModerationTable.data; + var { length } = array; + for (var i = 0; i < length; ++i) { + if (array[i].id === ref.id) { + array.splice(i, 1); + return; } } - switch (gameLog.type) { - case 'location-destination': - if (this.isGameRunning) { - // needs to be added before OnPlayerLeft entries from LocationReset - this.addGameLog({ - created_at: gameLog.dt, - type: 'LocationDestination', - location: gameLog.location + }); + + $app.methods.deletePlayerModeration = function (row) { + // FIXME: 메시지 수정 + this.$confirm('Continue? Delete Moderation', 'Confirm', { + confirmButtonText: 'Confirm', + cancelButtonText: 'Cancel', + type: 'info', + callback: (action) => { + if (action === 'confirm') { + API.deletePlayerModeration({ + moderated: row.targetUserId, + type: row.type }); - this.lastLocationReset(gameLog.dt); - this.lastLocation.location = 'traveling'; - this.lastLocationDestination = gameLog.location; - this.lastLocationDestinationTime = Date.parse(gameLog.dt); - this.removeQueuedInstance(gameLog.location); - this.updateCurrentUserLocation(); - this.clearNowPlaying(); - this.updateCurrentInstanceWorld(); - this.applyUserDialogLocation(); - this.applyWorldDialogInstances(); - this.applyGroupDialogInstances(); - } - break; - case 'location': - this.addInstanceJoinHistory( - this.lastLocation.location, - gameLog.dt - ); - var worldName = this.replaceBioSymbols(gameLog.worldName); - if (this.isGameRunning) { - this.lastLocationReset(gameLog.dt); - this.clearNowPlaying(); - this.lastLocation = { - date: Date.parse(gameLog.dt), - location: gameLog.location, - name: worldName, - playerList: new Map(), - friendList: new Map() - }; - this.removeQueuedInstance(gameLog.location); - this.updateCurrentUserLocation(); - this.updateVRLastLocation(); - this.updateCurrentInstanceWorld(); - this.applyUserDialogLocation(); - this.applyWorldDialogInstances(); - this.applyGroupDialogInstances(); - } - this.addInstanceJoinHistory(gameLog.location, gameLog.dt); - var L = API.parseLocation(gameLog.location); - var entry = { - created_at: gameLog.dt, - type: 'Location', - location: gameLog.location, - worldId: L.worldId, - worldName, - groupName: '', - time: 0 - }; - this.getGroupName(gameLog.location).then((groupName) => { - entry.groupName = groupName; - }); - this.addGamelogLocationToDatabase(entry); - break; - case 'player-joined': - var joinTime = Date.parse(gameLog.dt); - var userMap = { - displayName: gameLog.displayName, - userId, - joinTime, - lastAvatar: '' - }; - this.lastLocation.playerList.set(gameLog.displayName, userMap); - if (userId) { - var ref = API.cachedUsers.get(userId); - if (userId === API.currentUser.id) { - // skip - } else if (this.friends.has(userId)) { - this.lastLocation.friendList.set( - gameLog.displayName, - userMap - ); - if ( - ref.location !== this.lastLocation.location && - ref.travelingToLocation !== - this.lastLocation.location - ) { - // fix $location_at with private - ref.$location_at = joinTime; - } - } else if (typeof ref !== 'undefined') { - // set $location_at to join time if user isn't a friend - ref.$location_at = joinTime; - } else { - if (this.debugGameLog || this.debugWebRequests) { - console.log('Fetching user from gameLog:', userId); - } - API.getUser({ userId }); - } - } else { - // try fetch userId from previous encounter using database - database - .getUserIdFromDisplayName(gameLog.displayName) - .then((oldUserId) => { - if (this.isGameRunning) { - if (oldUserId) { - API.getUser({ userId: oldUserId }); - } else if (Date.now() - joinTime < 5 * 1000) { - workerTimers.setTimeout( - () => - this.silentSeachUser( - gameLog.displayName - ), - 10 * 1000 - ); - } - } - }); - } - this.updateVRLastLocation(); - this.getCurrentInstanceUserList(); - var entry = { - created_at: gameLog.dt, - type: 'OnPlayerJoined', - displayName: gameLog.displayName, - location, - userId, - time: 0 - }; - database.addGamelogJoinLeaveToDatabase(entry); - break; - case 'player-left': - var ref = this.lastLocation.playerList.get(gameLog.displayName); - if (typeof ref === 'undefined') { - break; - } - var time = Date.now() - ref.joinTime; - this.lastLocation.playerList.delete(gameLog.displayName); - this.lastLocation.friendList.delete(gameLog.displayName); - this.photonLobbyAvatars.delete(userId); - this.updateVRLastLocation(); - this.getCurrentInstanceUserList(); - var entry = { - created_at: gameLog.dt, - type: 'OnPlayerLeft', - displayName: gameLog.displayName, - location, - userId, - time - }; - database.addGamelogJoinLeaveToDatabase(entry); - break; - case 'portal-spawn': - if (this.ipcEnabled && this.isGameRunning) { - break; - } - var entry = { - created_at: gameLog.dt, - type: 'PortalSpawn', - location, - displayName: '', - userId: '', - instanceId: '', - worldName: '' - }; - database.addGamelogPortalSpawnToDatabase(entry); - break; - case 'video-play': - gameLog.videoUrl = decodeURI(gameLog.videoUrl); - if (this.lastVideoUrl === gameLog.videoUrl) { - break; - } - this.lastVideoUrl = gameLog.videoUrl; - this.addGameLogVideo(gameLog, location, userId); - break; - case 'video-sync': - var timestamp = gameLog.timestamp.replace(/,/g, ''); - if (this.nowPlaying.playing) { - this.nowPlaying.offset = parseInt(timestamp, 10); - } - break; - case 'resource-load-string': - case 'resource-load-image': - if ( - !this.logResourceLoad || - this.lastResourceloadUrl === gameLog.resourceUrl - ) { - break; - } - this.lastResourceloadUrl = gameLog.resourceUrl; - var entry = { - created_at: gameLog.dt, - type: - gameLog.type === 'resource-load-string' - ? 'StringLoad' - : 'ImageLoad', - resourceUrl: gameLog.resourceUrl, - location - }; - database.addGamelogResourceLoadToDatabase(entry); - break; - case 'screenshot': - // var entry = { - // created_at: gameLog.dt, - // type: 'Event', - // data: `Screenshot Processed: ${gameLog.screenshotPath.replace( - // /^.*[\\/]/, - // '' - // )}` - // }; - // database.addGamelogEventToDatabase(entry); - - this.processScreenshot(gameLog.screenshotPath); - break; - case 'api-request': - var bias = Date.parse(gameLog.dt) + 60 * 1000; - if ( - !this.isGameRunning || - this.lastLocation.location === '' || - this.lastLocation.location === 'traveling' || - bias < Date.now() - ) { - break; - } - var userId = ''; - try { - var url = new URL(gameLog.url); - var urlParams = new URLSearchParams(gameLog.url); - if (url.pathname.substring(0, 13) === '/api/1/users/') { - var pathArray = url.pathname.split('/'); - userId = pathArray[4]; - } else if (urlParams.has('userId')) { - userId = urlParams.get('userId'); - } - } catch (err) { - console.error(err); - } - if (!userId) { - break; - } - this.gameLogApiLoggingEnabled = true; - if ( - API.cachedUsers.has(userId) || - API.cachedPlayerModerationsUserIds.has(userId) - ) { - break; - } - if (this.debugGameLog || this.debugWebRequests) { - console.log('Fetching user from gameLog:', userId); - } - API.getUser({ userId }); - break; - case 'avatar-change': - var ref = this.lastLocation.playerList.get(gameLog.displayName); - if ( - this.photonLoggingEnabled || - typeof ref === 'undefined' || - ref.lastAvatar === gameLog.avatarName - ) { - break; - } - if (!ref.lastAvatar) { - ref.lastAvatar = gameLog.avatarName; - this.lastLocation.playerList.set(gameLog.displayName, ref); - break; - } - ref.lastAvatar = gameLog.avatarName; - this.lastLocation.playerList.set(gameLog.displayName, ref); - var entry = { - created_at: gameLog.dt, - type: 'AvatarChange', - userId, - name: gameLog.avatarName, - displayName: gameLog.displayName - }; - break; - case 'vrcx': - // VideoPlay(PyPyDance) "https://jd.pypy.moe/api/v1/videos/jr1NX4Jo8GE.mp4",0.1001,239.606,"0905 : [J-POP] 【まなこ】金曜日のおはよう 踊ってみた (vernities)" - var type = gameLog.data.substr(0, gameLog.data.indexOf(' ')); - if (type === 'VideoPlay(PyPyDance)') { - this.addGameLogPyPyDance(gameLog, location); - } else if (type === 'VideoPlay(VRDancing)') { - this.addGameLogVRDancing(gameLog, location); - } else if (type === 'VideoPlay(ZuwaZuwaDance)') { - this.addGameLogZuwaZuwaDance(gameLog, location); - } else if (type === 'LSMedia') { - this.addGameLogLSMedia(gameLog, location); - } else if (type === 'Movie&Chill') { - this.addGameLogMovieAndChill(gameLog, location); - } - break; - case 'photon-id': - if (!this.isGameRunning || !this.friendLogInitStatus) { - break; - } - var photonId = parseInt(gameLog.photonId, 10); - var ref = this.photonLobby.get(photonId); - if (typeof ref === 'undefined') { - for (var ctx of API.cachedUsers.values()) { - if (ctx.displayName === gameLog.displayName) { - this.photonLobby.set(photonId, ctx); - this.photonLobbyCurrent.set(photonId, ctx); - break; - } - } - var ctx = { - displayName: gameLog.displayName - }; - this.photonLobby.set(photonId, ctx); - this.photonLobbyCurrent.set(photonId, ctx); - this.getCurrentInstanceUserList(); - } - break; - case 'notification': - // var entry = { - // created_at: gameLog.dt, - // type: 'Notification', - // data: gameLog.json - // }; - break; - case 'event': - var entry = { - created_at: gameLog.dt, - type: 'Event', - data: gameLog.event - }; - database.addGamelogEventToDatabase(entry); - break; - case 'vrc-quit': - if (!this.isGameRunning) { - break; - } - if (this.vrcQuitFix) { - var bias = Date.parse(gameLog.dt) + 3000; - if (bias < Date.now()) { - console.log('QuitFix: Bias too low, not killing VRC'); - break; - } - AppApi.QuitGame().then((processCount) => { - if (processCount > 1) { - console.log( - 'QuitFix: More than 1 process running, not killing VRC' - ); - } else if (processCount === 1) { - console.log('QuitFix: Killed VRC'); - } else { - console.log( - 'QuitFix: Nothing to kill, no VRC process running' - ); - } - }); - } - break; - case 'openvr-init': - this.isGameNoVR = false; - configRepository.setBool('isGameNoVR', this.isGameNoVR); - this.updateOpenVR(); - break; - case 'desktop-mode': - this.isGameNoVR = true; - configRepository.setBool('isGameNoVR', this.isGameNoVR); - this.updateOpenVR(); - break; - case 'udon-exception': - if (this.udonExceptionLogging) { - console.log('UdonException', gameLog.data); - } - // var entry = { - // created_at: gameLog.dt, - // type: 'Event', - // data: gameLog.data - // }; - // database.addGamelogEventToDatabase(entry); - break; - } - if (entry) { - // add tag colour - if (entry.userId) { - var tagRef = this.customUserTags.get(entry.userId); - if (typeof tagRef !== 'undefined') { - entry.tagColour = tagRef.colour; - } - } - this.queueGameLogNoty(entry); - this.addGameLog(entry); - } - }; - - $app.methods.silentSeachUser = function (displayName) { - var playerListRef = this.lastLocation.playerList.get(displayName); - if ( - !this.gameLogApiLoggingEnabled || - !playerListRef || - playerListRef.userId - ) { - return; - } - if (this.debugGameLog) { - console.log('Searching for userId for:', displayName); - } - var params = { - n: 5, - offset: 0, - fuzzy: false, - search: displayName - }; - API.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.addGamelogLocationToDatabase = async function (input) { - var groupName = await this.getGroupName(input.location); - var entry = { - ...input, - groupName - }; - database.addGamelogLocationToDatabase(entry); - }; - - $app.data.moderationEventQueue = new Map(); - $app.data.moderationAgainstTable = []; - $app.data.photonLobby = new Map(); - $app.data.photonLobbyMaster = 0; - $app.data.photonLobbyCurrentUser = 0; - $app.data.photonLobbyUserData = new Map(); - $app.data.photonLobbyCurrent = new Map(); - $app.data.photonLobbyAvatars = new Map(); - $app.data.photonLobbyLastModeration = new Map(); - $app.data.photonLobbyWatcherLoop = false; - $app.data.photonLobbyTimeout = []; - $app.data.photonLobbyJointime = new Map(); - $app.data.photonLobbyActivePortals = new Map(); - $app.data.photonEvent7List = new Map(); - $app.data.photonLastEvent7List = ''; - $app.data.photonLastChatBoxMsg = new Map(); - - $app.data.photonEventType = [ - 'MeshVisibility', - 'AnimationFloat', - 'AnimationBool', - 'AnimationTrigger', - 'AudioTrigger', - 'PlayAnimation', - 'SendMessage', - 'SetParticlePlaying', - 'TeleportPlayer', - 'RunConsoleCommand', - 'SetGameObjectActive', - 'SetWebPanelURI', - 'SetWebPanelVolume', - 'SpawnObject', - 'SendRPC', - 'ActivateCustomTrigger', - 'DestroyObject', - 'SetLayer', - 'SetMaterial', - 'AddHealth', - 'AddDamage', - 'SetComponentActive', - 'AnimationInt', - 'AnimationIntAdd', - 'AnimationIntSubtract', - 'AnimationIntMultiply', - 'AnimationIntDivide', - 'AddVelocity', - 'SetVelocity', - 'AddAngularVelocity', - 'SetAngularVelocity', - 'AddForce', - 'SetUIText', - 'CallUdonMethod' - ]; - - $app.data.oldPhotonEmojis = [ - 'Angry', - 'Blushing', - 'Crying', - 'Frown', - 'Hand Wave', - 'Hang Ten', - 'In Love', - 'Jack O Lantern', - 'Kiss', - 'Laugh', - 'Skull', - 'Smile', - 'Spooky Ghost', - 'Stoic', - 'Sunglasses', - 'Thinking', - 'Thumbs Down', - 'Thumbs Up', - 'Tongue Out', - 'Wow', - 'Bats', - 'Cloud', - 'Fire', - 'Snow Fall', - 'Snowball', - 'Splash', - 'Web', - 'Beer', - 'Candy', - 'Candy Cane', - 'Candy Corn', - 'Champagne', - 'Drink', - 'Gingerbread', - 'Ice Cream', - 'Pineapple', - 'Pizza', - 'Tomato', - 'Beachball', - 'Coal', - 'Confetti', - 'Gift', - 'Gifts', - 'Life Ring', - 'Mistletoe', - 'Money', - 'Neon Shades', - 'Sun Lotion', - 'Boo', - 'Broken Heart', - 'Exclamation', - 'Go', - 'Heart', - 'Music Note', - 'Question', - 'Stop', - 'Zzz' - ]; - - $app.data.photonEmojis = [ - 'Angry', - 'Blushing', - 'Crying', - 'Frown', - 'Hand Wave', - 'Hang Ten', - 'In Love', - 'Jack O Lantern', - 'Kiss', - 'Laugh', - 'Skull', - 'Smile', - 'Spooky Ghost', - 'Stoic', - 'Sunglasses', - 'Thinking', - 'Thumbs Down', - 'Thumbs Up', - 'Tongue Out', - 'Wow', - 'Arrow Point', - "Can't see", - 'Hourglass', - 'Keyboard', - 'No Headphones', - 'No Mic', - 'Portal', - 'Shush', - 'Bats', - 'Cloud', - 'Fire', - 'Snow Fall', - 'Snowball', - 'Splash', - 'Web', - 'Beer', - 'Candy', - 'Candy Cane', - 'Candy Corn', - 'Champagne', - 'Drink', - 'Gingerbread', - 'Ice Cream', - 'Pineapple', - 'Pizza', - 'Tomato', - 'Beachball', - 'Coal', - 'Confetti', - 'Gift', - 'Gifts', - 'Life Ring', - 'Mistletoe', - 'Money', - 'Neon Shades', - 'Sun Lotion', - 'Boo', - 'Broken Heart', - 'Exclamation', - 'Go', - 'Heart', - 'Music Note', - 'Question', - 'Stop', - 'Zzz' - ]; - - $app.methods.startLobbyWatcherLoop = function () { - if (!this.photonLobbyWatcherLoop) { - this.photonLobbyWatcherLoop = true; - this.photonLobbyWatcher(); - } - }; - - $app.methods.photonLobbyWatcherLoopStop = function () { - this.photonLobbyWatcherLoop = false; - this.photonLobbyTimeout = []; - AppApi.ExecuteVrOverlayFunction('updateHudTimeout', '[]'); - }; - - $app.methods.photonLobbyWatcher = function () { - if (!this.photonLobbyWatcherLoop) { - return; - } - if (this.photonLobbyCurrent.size === 0) { - this.photonLobbyWatcherLoopStop(); - return; - } - var dtNow = Date.now(); - var bias2 = this.photonLastEvent7List + 1.5 * 1000; - if (dtNow > bias2 || this.lastLocation.playerList.size <= 1) { - if (this.photonLobbyTimeout.length > 0) { - AppApi.ExecuteVrOverlayFunction('updateHudTimeout', '[]'); - } - this.photonLobbyTimeout = []; - workerTimers.setTimeout(() => this.photonLobbyWatcher(), 500); - return; - } - var hudTimeout = []; - this.photonEvent7List.forEach((dt, id) => { - var timeSinceLastEvent = dtNow - Date.parse(dt); - if ( - timeSinceLastEvent > this.photonLobbyTimeoutThreshold && - id !== this.photonLobbyCurrentUser - ) { - if (this.photonLobbyJointime.has(id)) { - var { joinTime } = this.photonLobbyJointime.get(id); - } - if (!joinTime) { - console.log(`${id} missing join time`); - } - if (joinTime && joinTime + 70000 < dtNow) { - // wait 70secs for user to load in - hudTimeout.unshift({ - userId: this.getUserIdFromPhotonId(id), - displayName: this.getDisplayNameFromPhotonId(id), - time: Math.round(timeSinceLastEvent / 1000), - rawTime: timeSinceLastEvent - }); - } - } - }); - if (this.photonLobbyTimeout.length > 0 || hudTimeout.length > 0) { - hudTimeout.sort(function (a, b) { - if (a.rawTime > b.rawTime) { - return 1; - } - if (a.rawTime < b.rawTime) { - return -1; - } - return 0; - }); - if (this.timeoutHudOverlay) { - if ( - this.timeoutHudOverlayFilter === 'VIP' || - this.timeoutHudOverlayFilter === 'Friends' - ) { - var filteredHudTimeout = []; - hudTimeout.forEach((item) => { - if ( - this.timeoutHudOverlayFilter === 'VIP' && - API.cachedFavoritesByObjectId.has(item.userId) - ) { - filteredHudTimeout.push(item); - } else if ( - this.timeoutHudOverlayFilter === 'Friends' && - this.friends.has(item.userId) - ) { - filteredHudTimeout.push(item); - } - }); - } else { - var filteredHudTimeout = hudTimeout; - } - AppApi.ExecuteVrOverlayFunction( - 'updateHudTimeout', - JSON.stringify(filteredHudTimeout) - ); - } - this.photonLobbyTimeout = hudTimeout; - this.getCurrentInstanceUserList(); - } - workerTimers.setTimeout(() => this.photonLobbyWatcher(), 500); - }; - - $app.data.photonEventTableFilter = ''; - $app.data.photonEventTableTypeFilter = []; - $app.data.photonEventTableTypeOverlayFilter = []; - $app.data.photonEventTableTypeFilterList = [ - 'Event', - 'OnPlayerJoined', - 'OnPlayerLeft', - 'ChangeAvatar', - 'ChangeStatus', - 'ChangeGroup', - 'PortalSpawn', - 'DeletedPortal', - 'ChatBoxMessage', - 'Moderation', - 'Camera', - 'SpawnEmoji', - 'MasterMigrate' - ]; - - $app.methods.photonEventTableFilterChange = async function () { - this.photonEventTable.filters[0].value = this.photonEventTableFilter; - this.photonEventTable.filters[1].value = - this.photonEventTableTypeFilter; - - this.photonEventTablePrevious.filters[0].value = - this.photonEventTableFilter; - this.photonEventTablePrevious.filters[1].value = - this.photonEventTableTypeFilter; - - await configRepository.setString( - 'VRCX_photonEventTypeFilter', - JSON.stringify(this.photonEventTableTypeFilter) - ); - await configRepository.setString( - 'VRCX_photonEventTypeOverlayFilter', - JSON.stringify(this.photonEventTableTypeOverlayFilter) - ); - }; + // #endregion + // #region | App: Notification - $app.data.photonEventTable = { + $app.data.notificationTable = { data: [], filters: [ - { - prop: ['displayName', 'text'], - value: '' - }, { prop: 'type', value: [], filterFn: (row, filter) => filter.value.some((v) => v === row.type) - } - ], - tableProps: { - stripe: true, - size: 'mini' - }, - pageSize: 10, - paginationProps: { - small: true, - layout: 'sizes,prev,pager,next,total', - pageSizes: [5, 10, 15, 25, 50] - } - }; - - $app.data.photonEventTablePrevious = { - data: [], - filters: [ - { - prop: ['displayName', 'text'], - value: '' }, { - prop: 'type', - value: [], - filterFn: (row, filter) => - filter.value.some((v) => v === row.type) + prop: ['senderUsername', 'message'], + value: '' } ], tableProps: { stripe: true, - size: 'mini' + size: 'mini', + defaultSort: { + prop: 'created_at', + order: 'descending' + } }, - pageSize: 10, + pageSize: $app.data.tablePageSize, paginationProps: { small: true, layout: 'sizes,prev,pager,next,total', - pageSizes: [5, 10, 15, 25, 50] - } - }; - - $app.methods.addEntryPhotonEvent = function (input) { - var isMaster = false; - if (input.photonId === this.photonLobbyMaster) { - isMaster = true; - } - var joinTimeRef = this.photonLobbyJointime.get(input.photonId); - var isModerator = joinTimeRef?.canModerateInstance; - var photonUserRef = this.photonLobby.get(input.photonId); - var displayName = ''; - var userId = ''; - var isFriend = false; - if (typeof photonUserRef !== 'undefined') { - displayName = photonUserRef.displayName; - userId = photonUserRef.id; - isFriend = photonUserRef.isFriend; - } - var isFavorite = this.localFavoriteFriends.has(userId); - var colour = ''; - var tagRef = this.customUserTags.get(userId); - if (typeof tagRef !== 'undefined') { - colour = tagRef.colour; - } - var feed = { - displayName, - userId, - isFavorite, - isFriend, - isMaster, - isModerator, - colour, - ...input - }; - this.photonEventTable.data.unshift(feed); - if ( - this.photonEventTableTypeOverlayFilter.length > 0 && - !this.photonEventTableTypeOverlayFilter.includes(feed.type) - ) { - return; - } - if (this.photonEventOverlay) { - if ( - this.photonEventOverlayFilter === 'VIP' || - this.photonEventOverlayFilter === 'Friends' - ) { - if ( - feed.userId && - ((this.photonEventOverlayFilter === 'VIP' && isFavorite) || - (this.photonEventOverlayFilter === 'Friends' && - isFriend)) - ) { - AppApi.ExecuteVrOverlayFunction( - 'addEntryHudFeed', - JSON.stringify(feed) - ); - } - } else { - AppApi.ExecuteVrOverlayFunction( - 'addEntryHudFeed', - JSON.stringify(feed) - ); - } + pageSizes: [10, 15, 25, 50, 100] } }; - $app.methods.getDisplayNameFromPhotonId = function (photonId) { - var displayName = ''; - if (photonId) { - var ref = this.photonLobby.get(photonId); - displayName = `ID:${photonId}`; - if ( - typeof ref !== 'undefined' && - typeof ref.displayName !== 'undefined' - ) { - displayName = ref.displayName; - } - } - return displayName; - }; + API.$on('LOGIN', function () { + $app.notificationTable.data = []; + }); - $app.methods.getUserIdFromPhotonId = function (photonId) { - var userId = ''; - if (photonId) { - var ref = this.photonLobby.get(photonId); - if (typeof ref !== 'undefined' && typeof ref.id !== 'undefined') { - userId = ref.id; - } + API.$on('PIPELINE:NOTIFICATION', function (args) { + var ref = args.json; + if ( + ref.type !== 'requestInvite' || + $app.autoAcceptInviteRequests === 'Off' + ) { + return; } - return userId; - }; - $app.methods.showUserFromPhotonId = function (photonId) { - if (photonId) { - var ref = this.photonLobby.get(photonId); - if (typeof ref !== 'undefined') { - if (typeof ref.id !== 'undefined') { - this.showUserDialog(ref.id); - } else if (typeof ref.displayName !== 'undefined') { - this.lookupUser(ref); - } - } else { - this.$message({ - message: 'No user info available', - type: 'error' - }); - } + var currentLocation = $app.lastLocation.location; + if ($app.lastLocation.location === 'traveling') { + currentLocation = $app.lastLocationDestination; } - }; - - $app.methods.getPhotonIdFromDisplayName = function (displayName) { - var photonId = ''; - if (displayName) { - this.photonLobby.forEach((ref, id) => { - if ( - typeof ref !== 'undefined' && - ref.displayName === displayName - ) { - photonId = id; - } - }); + if (!currentLocation) { + return; } - return photonId; - }; - - $app.methods.getPhotonIdFromUserId = function (userId) { - var photonId = ''; - if (userId) { - this.photonLobby.forEach((ref, id) => { - if (typeof ref !== 'undefined' && ref.id === userId) { - photonId = id; - } - }); + if ( + $app.autoAcceptInviteRequests === 'All Favorites' && + !$app.favoriteFriends.some((x) => x.id === ref.senderUserId) + ) { + return; } - return photonId; - }; - - $app.methods.sortPhotonId = function (a, b, field) { - var id1 = this.getPhotonIdFromDisplayName(a[field]); - var id2 = this.getPhotonIdFromDisplayName(b[field]); - if (id1 < id2) { - return 1; + if ( + $app.autoAcceptInviteRequests === 'Selected Favorites' && + !$app.localFavoriteFriends.has(ref.senderUserId) + ) { + return; } - if (id1 > id2) { - return -1; + if (!$utils.checkCanInvite(currentLocation)) { + return; } - return 0; - }; - $app.methods.parsePhotonEvent = function (data, gameLogDate) { - switch (data.Code) { - case 253: - // SetUserProperties - if (data.Parameters[253] === -1) { - for (var i in data.Parameters[251]) { - var id = parseInt(i, 10); - var user = data.Parameters[251][i]; - this.parsePhotonUser(id, user.user, gameLogDate); - this.parsePhotonAvatarChange( - id, - user.user, - user.avatarDict, - gameLogDate - ); - this.parsePhotonGroupChange( - id, - user.user, - user.groupOnNameplate, - gameLogDate - ); - 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(gameLogDate), - 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 - }); - this.photonUserJoin(id, user, gameLogDate); - } - } else { - console.log('oldSetUserProps', data); - var id = parseInt(data.Parameters[253], 10); - var user = data.Parameters[251]; - this.parsePhotonUser(id, user.user, gameLogDate); - this.parsePhotonAvatarChange( - id, - user.user, - user.avatarDict, - gameLogDate - ); - this.parsePhotonGroupChange( - id, - user.user, - user.groupOnNameplate, - gameLogDate - ); - 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(gameLogDate), - 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 - }); - this.photonUserJoin(id, user, gameLogDate); - } - break; - case 42: - // SetUserProperties - var id = parseInt(data.Parameters[254], 10); - var user = data.Parameters[245]; - this.parsePhotonUser(id, user.user, gameLogDate); - this.parsePhotonAvatarChange( - id, - user.user, - user.avatarDict, - gameLogDate - ); - this.parsePhotonGroupChange( - id, - user.user, - user.groupOnNameplate, - gameLogDate - ); - this.parsePhotonAvatar(user.avatarDict); - this.parsePhotonAvatar(user.favatarDict); - var lobbyJointime = this.photonLobbyJointime.get(id); - this.photonLobbyJointime.set(id, { - hasInstantiated: true, - ...lobbyJointime, - inVRMode: user.inVRMode, - avatarEyeHeight: user.avatarEyeHeight, - canModerateInstance: user.canModerateInstance, - groupOnNameplate: user.groupOnNameplate, - showGroupBadgeToOthers: user.showGroupBadgeToOthers, - showSocialRank: user.showSocialRank, - useImpostorAsFallback: user.useImpostorAsFallback, - platform: user.platform + var L = $utils.parseLocation(currentLocation); + this.getCachedWorld({ + worldId: L.worldId + }).then((args1) => { + this.sendInvite( + { + instanceId: L.tag, + worldId: L.tag, + worldName: args1.ref.name, + rsvp: true + }, + ref.senderUserId + ) + .then((_args) => { + $app.$message(`Auto invite sent to ${ref.senderUsername}`); + return _args; + }) + .catch((err) => { + console.error(err); }); - break; - case 255: - // Join - if (typeof data.Parameters[249] !== 'undefined') { - this.parsePhotonUser( - data.Parameters[254], - data.Parameters[249].user, - gameLogDate - ); - this.parsePhotonAvatarChange( - data.Parameters[254], - data.Parameters[249].user, - data.Parameters[249].avatarDict, - gameLogDate - ); - this.parsePhotonGroupChange( - data.Parameters[254], - data.Parameters[249].user, - data.Parameters[249].groupOnNameplate, - gameLogDate - ); - this.parsePhotonAvatar(data.Parameters[249].avatarDict); - this.parsePhotonAvatar(data.Parameters[249].favatarDict); - } - this.parsePhotonLobbyIds(data.Parameters[252]); - var hasInstantiated = false; - if (this.photonLobbyCurrentUser === data.Parameters[254]) { - // fix current user - hasInstantiated = true; + }); + }); + + $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) { + if ( + $app.notificationTable.filters[0].value.length === 0 || + $app.notificationTable.filters[0].value.includes(ref.type) + ) { + $app.notifyMenu('notification'); } - var ref = this.photonLobbyCurrent.get(data.Parameters[254]); - if (typeof ref !== 'undefined') { - // fix for join event firing twice - // fix instantiation happening out of order before join event - hasInstantiated = ref.hasInstantiated; - } - this.photonLobbyJointime.set(data.Parameters[254], { - joinTime: Date.parse(gameLogDate), - hasInstantiated, - inVRMode: data.Parameters[249].inVRMode, - avatarEyeHeight: data.Parameters[249].avatarEyeHeight, - canModerateInstance: - data.Parameters[249].canModerateInstance, - groupOnNameplate: data.Parameters[249].groupOnNameplate, - showGroupBadgeToOthers: - data.Parameters[249].showGroupBadgeToOthers, - showSocialRank: data.Parameters[249].showSocialRank, - useImpostorAsFallback: - data.Parameters[249].useImpostorAsFallback, - platform: data.Parameters[249].platform - }); - this.photonUserJoin( - data.Parameters[254], - data.Parameters[249], - gameLogDate - ); - this.startLobbyWatcherLoop(); - break; - case 254: - // Leave - var photonId = data.Parameters[254]; - this.photonUserLeave(photonId, gameLogDate); - this.photonLobbyCurrent.delete(photonId); - this.photonLobbyLastModeration.delete(photonId); - this.photonLobbyJointime.delete(photonId); - this.photonEvent7List.delete(photonId); - this.parsePhotonLobbyIds(data.Parameters[252]); - if (typeof data.Parameters[203] !== 'undefined') { - this.setPhotonLobbyMaster( - data.Parameters[203], - gameLogDate - ); + $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) { + $app.selectMenu('notification'); + } + }); + + $app.methods.acceptNotification = function (row) { + // FIXME: 메시지 수정 + this.$confirm('Continue? Accept Friend Request', 'Confirm', { + confirmButtonText: 'Confirm', + cancelButtonText: 'Cancel', + type: 'info', + callback: (action) => { + if (action === 'confirm') { + API.acceptNotification({ + notificationId: row.id + }); } - break; - case 4: - // Sync - this.setPhotonLobbyMaster(data.Parameters[254], gameLogDate); - break; - case 33: - // Moderation - if (data.Parameters[245]['0'] === 21) { - if (data.Parameters[245]['1']) { - var photonId = data.Parameters[245]['1']; - var block = data.Parameters[245]['10']; - var mute = data.Parameters[245]['11']; - var ref = this.photonLobby.get(photonId); - if ( - typeof ref !== 'undefined' && - typeof ref.id !== 'undefined' - ) { - this.photonModerationUpdate( - ref, - photonId, - block, - mute, - gameLogDate - ); - } else { - this.moderationEventQueue.set(photonId, { - block, - mute, - gameLogDate - }); - } + } + }); + }; + + $app.methods.hideNotification = function (row) { + this.$confirm(`Continue? Decline ${row.type}`, 'Confirm', { + confirmButtonText: 'Confirm', + cancelButtonText: 'Cancel', + type: 'info', + callback: (action) => { + if (action === 'confirm') { + if (row.type === 'ignoredFriendRequest') { + API.deleteHiddenFriendRequest( + { + notificationId: row.id + }, + row.senderUserId + ); } else { - var blockArray = data.Parameters[245]['10']; - var muteArray = data.Parameters[245]['11']; - var idList = new Map(); - blockArray.forEach((photonId1) => { - if (muteArray.includes(photonId1)) { - idList.set(photonId1, { - isMute: true, - isBlock: true - }); - } else { - idList.set(photonId1, { - isMute: false, - isBlock: true - }); - } - }); - muteArray.forEach((photonId2) => { - if (!idList.has(photonId2)) { - idList.set(photonId2, { - isMute: true, - isBlock: false - }); - } - }); - idList.forEach(({ isMute, isBlock }, photonId3) => { - var ref1 = this.photonLobby.get(photonId3); - if ( - typeof ref1 !== 'undefined' && - typeof ref1.id !== 'undefined' - ) { - this.photonModerationUpdate( - ref1, - photonId3, - isBlock, - isMute, - gameLogDate - ); - } else { - this.moderationEventQueue.set(photonId3, { - block: isBlock, - mute: isMute, - gameLogDate - }); - } + API.hideNotification({ + notificationId: row.id }); } - } else if ( - data.Parameters[245]['0'] === 13 || - data.Parameters[245]['0'] === 25 - ) { - var msg = data.Parameters[245]['2']; + } + } + }); + }; + + $app.methods.deleteNotificationLog = function (row) { + this.$confirm(`Continue? Delete ${row.type}`, 'Confirm', { + confirmButtonText: 'Confirm', + cancelButtonText: 'Cancel', + type: 'info', + callback: (action) => { + if (action === 'confirm') { + $app.removeFromArray(this.notificationTable.data, row); if ( - typeof msg === 'string' && - typeof data.Parameters[245]['14'] === 'object' + row.type !== 'friendRequest' && + row.type !== 'ignoredFriendRequest' ) { - for (var prop in data.Parameters[245]['14']) { - var value = data.Parameters[245]['14'][prop]; - msg = msg.replace(`{{${prop}}}`, value); - } + database.deleteNotification(row.id); } - this.addEntryPhotonEvent({ - photonId, - text: msg, - type: 'Moderation', - color: 'yellow', - created_at: gameLogDate - }); - } - break; - case 202: - // Instantiate - if (!this.photonLobby.has(data.Parameters[254])) { - this.photonLobby.set(data.Parameters[254]); - } - if (!this.photonLobbyCurrent.has(data.Parameters[254])) { - this.photonLobbyCurrent.set(data.Parameters[254]); - } - var lobbyJointime = this.photonLobbyJointime.get( - data.Parameters[254] - ); - if (typeof lobbyJointime !== 'undefined') { - this.photonLobbyJointime.set(data.Parameters[254], { - ...lobbyJointime, - hasInstantiated: true - }); - } else { - this.photonLobbyJointime.set(data.Parameters[254], { - joinTime: Date.parse(gameLogDate), - hasInstantiated: true - }); - } - break; - case 43: - // Chatbox Message - var photonId = data.Parameters[254]; - var text = data.Parameters[245]; - if (this.photonLobbyCurrentUser === photonId) { - return; - } - var lastMsg = this.photonLastChatBoxMsg.get(photonId); - if (lastMsg === text) { - return; - } - this.photonLastChatBoxMsg.set(photonId, text); - var userId = this.getUserIdFromPhotonId(photonId); - if ( - this.chatboxUserBlacklist.has(userId) || - this.checkChatboxBlacklist(text) - ) { - return; } - this.addEntryPhotonEvent({ - photonId, - text, - type: 'ChatBoxMessage', - created_at: gameLogDate - }); - var entry = { - userId, - displayName: this.getDisplayNameFromPhotonId(photonId), - created_at: gameLogDate, - type: 'ChatBoxMessage', - text - }; - this.queueGameLogNoty(entry); - this.addGameLog(entry); - break; - case 70: - // Portal Spawn - if (data.Parameters[245][0] === 20) { - var portalId = data.Parameters[245][1]; - var userId = data.Parameters[245][2]; - var shortName = data.Parameters[245][5]; - var worldName = data.Parameters[245][8].name; - this.addPhotonPortalSpawn( - gameLogDate, - userId, - shortName, - worldName - ); - this.photonLobbyActivePortals.set(portalId, { - userId, - shortName, - worldName, - created_at: Date.parse(gameLogDate), - playerCount: 0, - pendingLeave: 0 - }); - } else if (data.Parameters[245][0] === 21) { - var portalId = data.Parameters[245][1]; - var userId = data.Parameters[245][2]; - var playerCount = data.Parameters[245][3]; - var shortName = data.Parameters[245][5]; - var worldName = ''; - this.addPhotonPortalSpawn( - gameLogDate, - userId, - shortName, - worldName - ); - this.photonLobbyActivePortals.set(portalId, { - userId, - shortName, - worldName, - created_at: Date.parse(gameLogDate), - playerCount: 0, - pendingLeave: 0 - }); - } else if (data.Parameters[245][0] === 22) { - var portalId = data.Parameters[245][1]; - var text = 'DeletedPortal'; - var ref = this.photonLobbyActivePortals.get(portalId); - if (typeof ref !== 'undefined') { - var worldName = ref.worldName; - var playerCount = ref.playerCount; - var time = timeToText( - Date.parse(gameLogDate) - ref.created_at - ); - text = `DeletedPortal after ${time} with ${playerCount} players to "${worldName}"`; - } - this.addEntryPhotonEvent({ - text, - type: 'DeletedPortal', - created_at: gameLogDate - }); - this.photonLobbyActivePortals.delete(portalId); - } else if (data.Parameters[245][0] === 23) { - var portalId = data.Parameters[245][1]; - var playerCount = data.Parameters[245][3]; - var ref = this.photonLobbyActivePortals.get(portalId); - if (typeof ref !== 'undefined') { - ref.pendingLeave++; - ref.playerCount = playerCount; + } + }); + }; + + $app.methods.acceptRequestInvite = function (row) { + this.$confirm('Continue? Send Invite', 'Confirm', { + confirmButtonText: 'Confirm', + cancelButtonText: 'Cancel', + type: 'info', + callback: (action) => { + if (action === 'confirm') { + var currentLocation = this.lastLocation.location; + if (this.lastLocation.location === 'traveling') { + currentLocation = this.lastLocationDestination; } - } else if (data.Parameters[245][0] === 24) { - this.addEntryPhotonEvent({ - text: 'PortalError failed to create portal', - type: 'DeletedPortal', - created_at: gameLogDate + var L = $utils.parseLocation(currentLocation); + API.getCachedWorld({ + worldId: L.worldId + }).then((args) => { + API.sendInvite( + { + instanceId: L.tag, + worldId: L.tag, + worldName: args.ref.name, + rsvp: true + }, + row.senderUserId + ).then((_args) => { + this.$message('Invite sent'); + API.hideNotification({ + notificationId: row.id + }); + return _args; + }); }); } - break; - case 71: - // Spawn Emoji - var photonId = data.Parameters[254]; - if (photonId === this.photonLobbyCurrentUser) { - return; - } - var type = data.Parameters[245][0]; - var emojiName = ''; - var imageUrl = ''; - if (type === 0) { - var emojiId = data.Parameters[245][2]; - emojiName = this.photonEmojis[emojiId]; - } else if (type === 1) { - emojiName = 'Custom'; - var fileId = data.Parameters[245][1]; - imageUrl = `https://api.vrchat.cloud/api/1/file/${fileId}/1/`; - } - this.addEntryPhotonEvent({ - photonId, - text: emojiName, - type: 'SpawnEmoji', - created_at: gameLogDate, - imageUrl, - fileId - }); - break; - } + } + }); }; - $app.methods.parseVRCEvent = function (json) { - // VRC Event - var datetime = json.dt; - var eventData = json.VRCEventData; - var senderId = eventData.Sender; - if (this.debugPhotonLogging) { - console.log('VrcEvent:', json); - } - if (eventData.EventName === '_SendOnSpawn') { - return; - } else if (eventData.EventType > 34) { - var entry = { - created_at: datetime, - type: 'Event', - data: `${this.getDisplayNameFromPhotonId( - senderId - )} called non existent RPC ${eventData.EventType}` - }; - this.addPhotonEventToGameLog(entry); - return; - } - if (eventData.EventType === 14) { - var type = 'Event'; - if (eventData.EventName === 'ChangeVisibility') { - if (eventData.Data[0] === true) { - var text = 'EnableCamera'; - } else if (eventData.Data[0] === false) { - var text = 'DisableCamera'; - } - type = 'Camera'; - } else if (eventData.EventName === 'PhotoCapture') { - var text = 'PhotoCapture'; - type = 'Camera'; - } else if (eventData.EventName === 'TimerBloop') { - var text = 'TimerBloop'; - type = 'Camera'; - } else if (eventData.EventName === 'ReloadAvatarNetworkedRPC') { - var text = 'AvatarReset'; - } else if (eventData.EventName === 'ReleaseBones') { - var text = 'ResetPhysBones'; - } else if (eventData.EventName === 'SpawnEmojiRPC') { - var text = this.oldPhotonEmojis[eventData.Data]; - type = 'SpawnEmoji'; - } else { - var eventVrc = ''; - if (eventData.Data && eventData.Data.length > 0) { - eventVrc = ` ${JSON.stringify(eventData.Data).replace( - /"([^(")"]+)":/g, - '$1:' - )}`; - } - var text = `${eventData.EventName}${eventVrc}`; - } - this.addEntryPhotonEvent({ - photonId: senderId, - text, - type, - created_at: datetime - }); - } else { - var eventName = ''; - if (eventData.EventName) { - eventName = ` ${JSON.stringify(eventData.EventName).replace( - /"([^(")"]+)":/g, - '$1:' - )}`; - } - if (this.debugPhotonLogging) { - var displayName = this.getDisplayNameFromPhotonId(senderId); - var feed = `RPC ${displayName} ${ - this.photonEventType[eventData.EventType] - }${eventName}`; - console.log('VrcRpc:', feed); - } - } - }; - - $app.methods.parsePhotonPortalSpawn = async function ( - created_at, - instanceId, - ref, - portalType, - shortName, - photonId - ) { - var worldName = shortName; - if (instanceId) { - worldName = await this.getWorldName(instanceId); - } - this.addEntryPhotonEvent({ - photonId, - text: `${portalType} PortalSpawn to ${worldName}`, - type: 'PortalSpawn', - shortName, - location: instanceId, - worldName, - created_at - }); - this.addPhotonEventToGameLog({ - created_at, - type: 'PortalSpawn', - displayName: ref.displayName, - location: this.lastLocation.location, - userId: ref.id, - instanceId, - worldName - }); - }; - - $app.methods.addPhotonPortalSpawn = async function ( - gameLogDate, - userId, - shortName, - worldName - ) { - var instance = await API.getInstanceFromShortName({ shortName }); - var location = instance.json.location; - var L = API.parseLocation(location); - var groupName = ''; - if (L.groupId) { - groupName = await this.getGroupName(L.groupId); - } - if (!worldName) { - // eslint-disable-next-line no-param-reassign - worldName = await this.getWorldName(location); - } - // var newShortName = instance.json.shortName; - // var portalType = 'Secure'; - // if (shortName === newShortName) { - // portalType = 'Unlocked'; - // } - var displayLocation = this.displayLocation( - location, - worldName, - groupName + // Save Table Filters + $app.methods.saveTableFilters = async function () { + await configRepository.setString( + 'VRCX_friendLogTableFilters', + JSON.stringify(this.friendLogTable.filters[0].value) + ); + await configRepository.setString( + 'VRCX_playerModerationTableFilters', + JSON.stringify(this.playerModerationTable.filters[0].value) + ); + await configRepository.setString( + 'VRCX_notificationTableFilters', + JSON.stringify(this.notificationTable.filters[0].value) ); - this.addEntryPhotonEvent({ - photonId: this.getPhotonIdFromUserId(userId), - text: `PortalSpawn to ${displayLocation}`, - type: 'PortalSpawn', - shortName, - location, - worldName, - groupName, - created_at: gameLogDate - }); - this.addPhotonEventToGameLog({ - created_at: gameLogDate, - type: 'PortalSpawn', - displayName: this.getDisplayName(userId), - location: this.lastLocation.location, - userId, - instanceId: location, - worldName, - groupName - }); }; - $app.methods.addPhotonEventToGameLog = function (entry) { - this.queueGameLogNoty(entry); - this.addGameLog(entry); - if (entry.type === 'PortalSpawn') { - database.addGamelogPortalSpawnToDatabase(entry); - } else if (entry.type === 'Event') { - database.addGamelogEventToDatabase(entry); - } - }; + $app.data.feedTable.filter = JSON.parse( + await configRepository.getString('VRCX_feedTableFilters', '[]') + ); + $app.data.feedTable.vip = await configRepository.getBool( + 'VRCX_feedTableVIPFilter', + 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.playerModerationTable.filters[0].value = JSON.parse( + await configRepository.getString( + 'VRCX_playerModerationTableFilters', + '[]' + ) + ); + $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', + '[]' + ) + ); - $app.methods.parsePhotonLobbyIds = function (lobbyIds) { - lobbyIds.forEach((id) => { - if (!this.photonLobby.has(id)) { - this.photonLobby.set(id); - } - if (!this.photonLobbyCurrent.has(id)) { - this.photonLobbyCurrent.set(id); - } - }); - for (var id of this.photonLobbyCurrent.keys()) { - if (!lobbyIds.includes(id)) { - this.photonLobbyCurrent.delete(id); - this.photonEvent7List.delete(id); - } - } - }; + // #endregion + // #region | App: Profile + Settings - $app.methods.setPhotonLobbyMaster = function (photonId, gameLogDate) { - if (this.photonLobbyMaster !== photonId) { - if (this.photonLobbyMaster !== 0) { - this.addEntryPhotonEvent({ - photonId, - text: `Photon Master Migrate`, - type: 'MasterMigrate', - created_at: gameLogDate - }); + $app.data.configTreeData = []; + $app.data.currentUserTreeData = []; + $app.data.currentUserFeedbackData = []; + $app.data.pastDisplayNameTable = { + data: [], + tableProps: { + stripe: true, + size: 'mini', + defaultSort: { + prop: 'updated_at', + order: 'descending' } - this.photonLobbyMaster = photonId; - } + }, + layout: 'table' }; - - $app.methods.parsePhotonUser = async function ( - photonId, - user, - gameLogDate - ) { - if (typeof user === 'undefined') { - console.error('PhotonUser: user is undefined', photonId); - return; - } - var tags = []; - if (typeof user.tags !== 'undefined') { - tags = user.tags; - } - var ref = API.cachedUsers.get(user.id); - var photonUser = { - id: user.id, - displayName: user.displayName, - developerType: user.developerType, - profilePicOverride: user.profilePicOverride, - currentAvatarImageUrl: user.currentAvatarImageUrl, - currentAvatarThumbnailImageUrl: user.currentAvatarThumbnailImageUrl, - userIcon: user.userIcon, - last_platform: user.last_platform, - allowAvatarCopying: user.allowAvatarCopying, - status: user.status, - statusDescription: user.statusDescription, - bio: user.bio, - tags - }; - this.photonLobby.set(photonId, photonUser); - this.photonLobbyCurrent.set(photonId, photonUser); - this.photonLobbyUserDataUpdate(photonId, photonUser, gameLogDate); - - var bias = Date.parse(gameLogDate) + 60 * 1000; // 1min - if (bias > Date.now()) { - if (typeof ref === 'undefined' || typeof ref.id === 'undefined') { - try { - var args = await API.getUser({ - userId: user.id - }); - ref = args.ref; - } catch (err) { - console.error(err); - ref = photonUser; - } - } else if ( - !ref.isFriend && - this.lastLocation.playerList.has(ref.displayName) - ) { - var { joinTime } = this.lastLocation.playerList.get( - ref.displayName - ); - if (!joinTime) { - joinTime = Date.parse(gameLogDate); - } - ref.$location_at = joinTime; - ref.$online_for = joinTime; - } - if ( - typeof ref.id !== 'undefined' && - ref.currentAvatarImageUrl !== user.currentAvatarImageUrl - ) { - API.applyUser({ - ...ref, - currentAvatarImageUrl: user.currentAvatarImageUrl, - currentAvatarThumbnailImageUrl: - user.currentAvatarThumbnailImageUrl - }); - } - } - if (typeof ref !== 'undefined' && typeof ref.id !== 'undefined') { - this.photonLobby.set(photonId, ref); - this.photonLobbyCurrent.set(photonId, ref); - // check moderation queue - if (this.moderationEventQueue.has(photonId)) { - var { block, mute, gameLogDate } = - this.moderationEventQueue.get(photonId); - this.moderationEventQueue.delete(photonId); - this.photonModerationUpdate( - ref, - photonId, - block, - mute, - gameLogDate - ); - } - } + $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.methods.photonLobbyUserDataUpdate = function ( - photonId, - photonUser, - gameLogDate - ) { - var ref = this.photonLobbyUserData.get(photonId); - if ( - typeof ref !== 'undefined' && - photonId !== this.photonLobbyCurrentUser && - (photonUser.status !== ref.status || - photonUser.statusDescription !== ref.statusDescription) - ) { - this.addEntryPhotonEvent({ - photonId, - type: 'ChangeStatus', - status: photonUser.status, - previousStatus: ref.status, - statusDescription: this.replaceBioSymbols( - photonUser.statusDescription - ), - previousStatusDescription: this.replaceBioSymbols( - ref.statusDescription - ), - created_at: Date.parse(gameLogDate) - }); - } - this.photonLobbyUserData.set(photonId, photonUser); + $app.data.inviteResponseMessageTable = { + data: [], + tableProps: { + stripe: true, + size: 'mini' + }, + layout: 'table', + visible: false }; - - $app.methods.photonUserJoin = function (photonId, user, gameLogDate) { - if (photonId === this.photonLobbyCurrentUser) { - return; - } - var avatar = user.avatarDict; - avatar.name = this.replaceBioSymbols(avatar.name); - avatar.description = this.replaceBioSymbols(avatar.description); - var platform = ''; - if (user.last_platform === 'android') { - platform = 'Android'; - } else if (user.last_platform === 'ios') { - platform = 'iOS'; - } else if (user.inVRMode) { - platform = 'VR'; - } else { - platform = 'Desktop'; - } - this.photonUserSusieCheck(photonId, user, gameLogDate); - this.checkVRChatCache(avatar).then((cacheInfo) => { - var inCache = false; - if (cacheInfo.Item1 > 0) { - inCache = true; - } - this.addEntryPhotonEvent({ - photonId, - text: 'has joined', - type: 'OnPlayerJoined', - created_at: gameLogDate, - avatar, - inCache, - platform - }); - }); + $app.data.inviteRequestMessageTable = { + data: [], + tableProps: { + stripe: true, + size: 'mini' + }, + layout: 'table', + visible: false }; - - $app.methods.photonUserSusieCheck = function (photonId, user, gameLogDate) { - var text = ''; - if (typeof user.modTag !== 'undefined') { - text = `Moderator has joined ${user.modTag}`; - } else if (user.isInvisible) { - text = 'User joined invisible'; - } - if (text) { - this.addEntryPhotonEvent({ - photonId, - text, - type: 'Event', - color: 'yellow', - created_at: gameLogDate - }); - var entry = { - created_at: new Date().toJSON(), - type: 'Event', - data: `${text} - ${this.getDisplayNameFromPhotonId( - photonId - )} (${this.getUserIdFromPhotonId(photonId)})` - }; - this.queueGameLogNoty(entry); - this.addGameLog(entry); - database.addGamelogEventToDatabase(entry); - } + $app.data.inviteRequestResponseMessageTable = { + data: [], + tableProps: { + stripe: true, + size: 'mini' + }, + layout: 'table', + visible: false }; - - $app.methods.photonUserLeave = function (photonId, gameLogDate) { - if (!this.photonLobbyCurrent.has(photonId)) { - return; - } - var text = 'has left'; - var lastEvent = this.photonEvent7List.get(parseInt(photonId, 10)); - if (typeof lastEvent !== 'undefined') { - var timeSinceLastEvent = Date.now() - Date.parse(lastEvent); - if (timeSinceLastEvent > 10 * 1000) { - // 10 seconds - text = `has timed out after ${timeToText(timeSinceLastEvent)}`; + $app.data.friendsListTable = { + data: [], + tableProps: { + stripe: true, + size: 'mini', + defaultSort: { + prop: '$friendNum', + order: 'descending' } + }, + pageSize: 100, + paginationProps: { + small: true, + layout: 'sizes,prev,pager,next,total', + pageSizes: [50, 100, 250, 500] } - this.photonLobbyActivePortals.forEach((portal) => { - if (portal.pendingLeave > 0) { - text = `has left through portal to "${portal.worldName}"`; - portal.pendingLeave--; - } - }); - this.addEntryPhotonEvent({ - photonId, - text, - type: 'OnPlayerLeft', - created_at: gameLogDate - }); }; - - $app.methods.photonModerationUpdate = function ( - ref, - photonId, - block, - mute, - gameLogDate - ) { - database.getModeration(ref.id).then((row) => { - var lastType = this.photonLobbyLastModeration.get(photonId); - var type = ''; - var text = ''; - if (block) { - type = 'Blocked'; - text = 'Blocked'; - } else if (mute) { - type = 'Muted'; - text = 'Muted'; - } - if (row.userId) { - if (!block && row.block) { - type = 'Unblocked'; - text = 'Unblocked'; - } else if (!mute && row.mute) { - type = 'Unmuted'; - text = 'Unmuted'; - } - if (block === row.block && mute === row.mute) { - // no change - if (type && type !== lastType) { - this.addEntryPhotonEvent({ - photonId, - text: `Moderation ${text}`, - type: 'Moderation', - color: 'yellow', - created_at: gameLogDate - }); - } - this.photonLobbyLastModeration.set(photonId, type); - return; - } - } - this.photonLobbyLastModeration.set(photonId, type); - this.moderationAgainstTable.forEach((item) => { - if (item.userId === ref.id && item.type === type) { - removeFromArray(this.moderationAgainstTable, item); - } - }); - if (type) { - this.addEntryPhotonEvent({ - photonId, - text: `Moderation ${text}`, - type: 'Moderation', - color: 'yellow', - created_at: gameLogDate - }); - var noty = { - created_at: new Date().toJSON(), - userId: ref.id, - displayName: ref.displayName, - type - }; - this.queueModerationNoty(noty); - var entry = { - created_at: gameLogDate, - userId: ref.id, - displayName: ref.displayName, - type - }; - this.moderationAgainstTable.push(entry); - } - if (block || mute || block !== row.block || mute !== row.mute) { - this.updateSharedFeed(true); - } - if (block || mute) { - database.setModeration({ - userId: ref.id, - updatedAt: gameLogDate, - displayName: ref.displayName, - block, - mute - }); - } else if (row.block || row.mute) { - database.deleteModeration(ref.id); - } - }); - }; - - $app.methods.parsePhotonAvatarChange = function ( - photonId, - user, - avatar, - gameLogDate - ) { - if (typeof avatar === 'undefined') { - return; - } - if (typeof user === 'undefined') { - console.error('PhotonAvatarChange: user is undefined', photonId); - return; - } - var oldAvatarId = this.photonLobbyAvatars.get(user.id); - if ( - oldAvatarId && - oldAvatarId !== avatar.id && - photonId !== this.photonLobbyCurrentUser - ) { - avatar.name = this.replaceBioSymbols(avatar.name); - avatar.description = this.replaceBioSymbols(avatar.description); - this.checkVRChatCache(avatar).then((cacheInfo) => { - var inCache = false; - if (cacheInfo.Item1 > 0) { - inCache = true; - } - var entry = { - created_at: new Date().toJSON(), - type: 'AvatarChange', - userId: user.id, - displayName: user.displayName, - name: avatar.name, - description: avatar.description, - avatarId: avatar.id, - authorId: avatar.authorId, - releaseStatus: avatar.releaseStatus, - imageUrl: avatar.imageUrl, - thumbnailImageUrl: avatar.thumbnailImageUrl - }; - this.queueGameLogNoty(entry); - this.addGameLog(entry); - this.addEntryPhotonEvent({ - photonId, - displayName: user.displayName, - userId: user.id, - text: `ChangeAvatar ${avatar.name}`, - type: 'ChangeAvatar', - created_at: gameLogDate, - avatar, - inCache - }); - }); - } - this.photonLobbyAvatars.set(user.id, avatar.id); - }; - - $app.methods.parsePhotonGroupChange = async function ( - photonId, - user, - groupId, - gameLogDate - ) { - if ( - typeof user === 'undefined' || - !this.photonLobbyJointime.has(photonId) - ) { - return; - } - var { groupOnNameplate } = this.photonLobbyJointime.get(photonId); - if ( - typeof groupOnNameplate !== 'undefined' && - groupOnNameplate !== groupId && - photonId !== this.photonLobbyCurrentUser - ) { - var groupName = await this.getGroupName(groupId); - var previousGroupName = await this.getGroupName(groupOnNameplate); - this.addEntryPhotonEvent({ - photonId, - displayName: user.displayName, - userId: user.id, - text: `ChangeGroup ${groupName}`, - type: 'ChangeGroup', - created_at: gameLogDate, - groupId, - groupName, - previousGroupId: groupOnNameplate, - previousGroupName - }); + $app.data.downloadHistoryTable = { + data: [], + tableProps: { + stripe: true, + size: 'mini' + }, + pageSize: 10, + paginationProps: { + small: true, + layout: 'prev,pager,next', + pageSizes: [10, 25, 50, 100] } }; - - $app.methods.parsePhotonAvatar = function (avatar) { - if (typeof avatar === 'undefined' || typeof avatar.id === 'undefined') { - console.error('PhotonAvatar: avatar is undefined'); - return; - } - var tags = []; - var unityPackages = []; - if (typeof avatar.tags !== 'undefined') { - tags = avatar.tags; - } - if (typeof avatar.unityPackages !== 'undefined') { - unityPackages = avatar.unityPackages; - } - if (!avatar.assetUrl && unityPackages.length > 0) { - for (var unityPackage of unityPackages) { - if ( - unityPackage.variant && - unityPackage.variant !== 'standard' && - unityPackage.variant !== 'security' - ) { - continue; - } - if (unityPackage.platform === 'standalonewindows') { - avatar.assetUrl = unityPackage.assetUrl; - } - } - } - API.applyAvatar({ - id: avatar.id, - authorId: avatar.authorId, - authorName: avatar.authorName, - updated_at: avatar.updated_at, - description: avatar.description, - imageUrl: avatar.imageUrl, - thumbnailImageUrl: avatar.thumbnailImageUrl, - name: avatar.name, - releaseStatus: avatar.releaseStatus, - version: avatar.version, - tags, - unityPackages - }); + $app.data.downloadQueueTable = { + data: [], + tableProps: { + stripe: true, + size: 'mini' + }, + layout: 'table' }; - - $app.methods.addGameLogVideo = async function (gameLog, location, userId) { - var videoUrl = gameLog.videoUrl; - var youtubeVideoId = ''; - var videoId = ''; - var videoName = ''; - var videoLength = ''; - var displayName = ''; - var videoPos = 8; // video loading delay - if (typeof gameLog.displayName !== 'undefined') { - displayName = gameLog.displayName; - } - if (typeof gameLog.videoPos !== 'undefined') { - videoPos = gameLog.videoPos; - } - if (!this.isRpcWorld(location) || gameLog.videoId === 'YouTube') { - // skip PyPyDance and VRDancing videos - try { - var url = new URL(videoUrl); - if ( - url.origin === 'https://t-ne.x0.to' || - url.origin === 'https://nextnex.com' || - url.origin === 'https://r.0cm.org' - ) { - url = new URL(url.searchParams.get('url')); - } - if (videoUrl.startsWith('https://u2b.cx/')) { - url = new URL(videoUrl.substring(15)); - } - var id1 = url.pathname; - var id2 = url.searchParams.get('v'); - if (id1 && id1.length === 12) { - // https://youtu.be/ - youtubeVideoId = id1.substring(1, 12); - } - if (id1 && id1.length === 19) { - // https://www.youtube.com/shorts/ - youtubeVideoId = id1.substring(8, 19); - } - if (id2 && id2.length === 11) { - // https://www.youtube.com/watch?v= - // https://music.youtube.com/watch?v= - youtubeVideoId = id2; - } - if (this.youTubeApi && youtubeVideoId) { - var data = await this.lookupYouTubeVideo(youtubeVideoId); - if (data || data.pageInfo.totalResults !== 0) { - videoId = 'YouTube'; - videoName = data.items[0].snippet.title; - videoLength = this.convertYoutubeTime( - data.items[0].contentDetails.duration - ); - } - } - } catch { - console.error(`Invalid URL: ${url}`); - } - var entry = { - created_at: gameLog.dt, - type: 'VideoPlay', - videoUrl, - videoId, - videoName, - videoLength, - location, - displayName, - userId, - videoPos - }; - this.setNowPlaying(entry); - } + $app.data.socialStatusHistoryTable = { + data: [], + tableProps: { + stripe: true, + size: 'mini' + }, + layout: 'table' }; - - $app.methods.addGameLogPyPyDance = function (gameLog, location) { - var data = - /VideoPlay\(PyPyDance\) "(.+?)",([\d.]+),([\d.]+),"(.*)"/g.exec( - gameLog.data - ); - if (!data) { - console.error('failed to parse', gameLog.data); - return; - } - var videoUrl = data[1]; - var videoPos = Number(data[2]); - var videoLength = Number(data[3]); - var title = data[4]; - var bracketArray = title.split('('); - var text1 = bracketArray.pop(); - var displayName = text1.slice(0, -1); - var text2 = bracketArray.join('('); - if (text2 === 'URL ') { - var videoId = 'YouTube'; - } else { - var videoId = text2.substr(0, text2.indexOf(':') - 1); - text2 = text2.substr(text2.indexOf(':') + 2); - } - var videoName = text2.slice(0, -1); - if (displayName === 'Random') { - displayName = ''; - } - if (videoUrl === this.nowPlaying.url) { - var entry = { - created_at: gameLog.dt, - videoUrl, - videoLength, - videoPos - }; - this.setNowPlaying(entry); - return; - } - var userId = ''; - if (displayName) { - for (var ref of API.cachedUsers.values()) { - if (ref.displayName === displayName) { - userId = ref.id; - break; - } + $app.data.currentInstanceUserList = { + data: [], + tableProps: { + stripe: true, + size: 'mini', + defaultSort: { + prop: 'timer', + order: 'descending' } - } - if (videoId === 'YouTube') { - var entry = { - dt: gameLog.dt, - videoUrl, - displayName, - videoPos, - videoId - }; - this.addGameLogVideo(entry, location, userId); - } else { - var entry = { - created_at: gameLog.dt, - type: 'VideoPlay', - videoUrl, - videoId, - videoName, - videoLength, - location, - displayName, - userId, - videoPos - }; - this.setNowPlaying(entry); - } + }, + layout: 'table' }; - - $app.methods.addGameLogVRDancing = function (gameLog, location) { - var data = - /VideoPlay\(VRDancing\) "(.+?)",([\d.]+),([\d.]+),(-?[\d.]+),"(.+?)","(.+?)"/g.exec( - gameLog.data - ); - if (!data) { - console.error('failed to parse', gameLog.data); - return; - } - var videoUrl = data[1]; - var videoPos = Number(data[2]); - var videoLength = Number(data[3]); - var videoId = Number(data[4]); - var displayName = data[5]; - var videoName = data[6]; - if (videoId === -1) { - videoId = 'YouTube'; - } - if (parseInt(videoPos, 10) === parseInt(videoLength, 10)) { - // ummm okay - videoPos = 0; - } - if (videoUrl === this.nowPlaying.url) { - var entry = { - created_at: gameLog.dt, - videoUrl, - videoLength, - videoPos - }; - this.setNowPlaying(entry); - return; - } - var userId = ''; - if (displayName) { - for (var ref of API.cachedUsers.values()) { - if (ref.displayName === displayName) { - userId = ref.id; - break; - } - } + $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.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.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 + ); + if (await configRepository.getInt('VRCX_asidewidth')) { + // migrate to new defaults + $app.data.asideWidth = await configRepository.getInt('VRCX_asidewidth'); + if ($app.data.asideWidth < 300) { + $app.data.asideWidth = 300; } - if (videoId === 'YouTube') { - var entry = { - dt: gameLog.dt, - videoUrl, - displayName, - videoPos, - videoId - }; - this.addGameLogVideo(entry, location, userId); - } else { - var entry = { - created_at: gameLog.dt, - type: 'VideoPlay', - videoUrl, - videoId, - videoName, - videoLength, - location, - displayName, - userId, - videoPos - }; - this.setNowPlaying(entry); - } - }; - - $app.methods.addGameLogZuwaZuwaDance = function (gameLog, location) { - var data = - /VideoPlay\(ZuwaZuwaDance\) "(.+?)",([\d.]+),([\d.]+),(-?[\d.]+),"(.+?)","(.+?)"/g.exec( - gameLog.data + await configRepository.setInt( + 'VRCX_sidePanelWidth', + $app.data.asideWidth + ); + await configRepository.remove('VRCX_asidewidth'); + } + $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://avtr.just-h.party/vrcx_search.php" ]' + ) + ); + $app.data.pendingOfflineDelay = 110000; + 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 ); - if (!data) { - console.error('failed to parse', gameLog.data); - return; - } - var videoUrl = data[1]; - var videoPos = Number(data[2]); - var videoLength = Number(data[3]); - var videoId = Number(data[4]); - var displayName = data[5]; - var videoName = data[6]; - if (displayName === 'Random') { - displayName = ''; - } - if (videoId === 9999) { - videoId = 'YouTube'; - } - if (videoUrl === this.nowPlaying.url) { - var entry = { - created_at: gameLog.dt, - videoUrl, - videoLength, - videoPos - }; - this.setNowPlaying(entry); - return; - } - var userId = ''; - if (displayName) { - for (var ref of API.cachedUsers.values()) { - if (ref.displayName === displayName) { - userId = ref.id; - break; - } - } - } - if (videoId === 'YouTube') { - var entry = { - dt: gameLog.dt, - videoUrl, - displayName, - videoPos, - videoId - }; - this.addGameLogVideo(entry, location, userId); - } else { - var entry = { - created_at: gameLog.dt, - type: 'VideoPlay', - videoUrl, - videoId, - videoName, - videoLength, - location, - displayName, - userId, - videoPos - }; - this.setNowPlaying(entry); } - }; - - $app.methods.addGameLogLSMedia = function (gameLog, location) { - // [VRCX] LSMedia 0,4268.981,Natsumi-sama,, - // [VRCX] LSMedia 0,6298.292,Natsumi-sama,The Outfit (2022), 1080p - var data = /LSMedia ([\d.]+),([\d.]+),(.+?),(.+?),(?=[^,]*$)/g.exec( - gameLog.data + await configRepository.remove('VRCX_avatarRemoteDatabaseProvider'); + await configRepository.setString( + 'VRCX_avatarRemoteDatabaseProviderList', + JSON.stringify($app.data.avatarRemoteDatabaseProviderList) ); - if (!data) { - return; - } - var videoPos = Number(data[1]); - var videoLength = Number(data[2]); - var displayName = data[3]; - var videoName = this.replaceBioSymbols(data[4]); - var videoUrl = videoName; - var videoId = 'LSMedia'; - if (videoUrl === this.nowPlaying.url) { - var entry = { - created_at: gameLog.dt, - videoUrl, - videoLength, - videoPos - }; - this.setNowPlaying(entry); - return; - } - var userId = ''; - if (displayName) { - for (var ref of API.cachedUsers.values()) { - if (ref.displayName === displayName) { - userId = ref.id; - break; - } - } + } + 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 () { + 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; } - var entry = { - created_at: gameLog.dt, - type: 'VideoPlay', - videoUrl, - videoId, - videoName, - videoLength, - location, - displayName, - userId, - videoPos - }; - this.setNowPlaying(entry); + 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.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.addGameLogMovieAndChill = function (gameLog, location) { - // [VRCX] Movie&Chill CurrentTime,Length,PlayerName,MovieName - var data = /Movie&Chill ([\d.]+),([\d.]+),(.+?),(.*)/g.exec( - gameLog.data + $app.methods.saveSortFavoritesOption = async function () { + this.getLocalWorldFavorites(); + await configRepository.setBool( + 'VRCX_sortFavorites', + this.sortFavorites ); - if (!data) { - return; - } - var videoPos = Number(data[1]); - var videoLength = Number(data[2]); - var displayName = data[3]; - var videoName = data[4]; - var videoUrl = videoName; - var videoId = 'Movie&Chill'; - if (!videoName) { - return; - } - if (videoUrl === this.nowPlaying.url) { - var entry = { - created_at: gameLog.dt, - videoUrl, - videoLength, - videoPos - }; - this.setNowPlaying(entry); - return; - } - var userId = ''; - if (displayName) { - for (var ref of API.cachedUsers.values()) { - if (ref.displayName === displayName) { - userId = ref.id; - break; - } - } - } - var entry = { - created_at: gameLog.dt, - type: 'VideoPlay', - videoUrl, - videoId, - videoName, - videoLength, - location, - displayName, - userId, - videoPos - }; - this.setNowPlaying(entry); }; - - $app.methods.lookupYouTubeVideo = async function (videoId) { - var data = null; - var apiKey = 'AIzaSyDC9AwAmtnMWpmk6mhs-iIStfXmH0vJxew'; - 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.saveUserDialogOption = async function () { + await configRepository.setBool( + 'VRCX_hideUserNotes', + this.hideUserNotes + ); + await configRepository.setBool( + 'VRCX_hideUserMemos', + this.hideUserMemos + ); }; - - $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.saveFriendLogOptions = async function () { + await configRepository.setBool( + 'VRCX_hideUnfriends', + this.hideUnfriends + ); + this.friendLogTable.filters[2].value = this.hideUnfriends; }; - - $app.methods.setNowPlaying = function (ctx) { - if (this.nowPlaying.url !== ctx.videoUrl) { - 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.data.TTSvoices = speechSynthesis.getVoices(); + $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.data.themeMode = await configRepository.getString( + 'VRCX_ThemeMode', + 'system' + ); - $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.data.isDarkMode = false; - $app.methods.updateVrNowPlaying = function () { - var json = JSON.stringify(this.nowPlaying); - AppApi.ExecuteVrFeedFunction('nowPlayingUpdate', json); - AppApi.ExecuteVrOverlayFunction('nowPlayingUpdate', json); + $app.methods.systemIsDarkMode = function () { + return window.matchMedia('(prefers-color-scheme: dark)').matches; }; - $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)}`; + 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.convertYoutubeTime = function (duration) { - var a = duration.match(/\d+/g); + $app.methods.changeThemeMode = async function () { if ( - duration.indexOf('M') >= 0 && - duration.indexOf('H') === -1 && - duration.indexOf('S') === -1 + document.contains(document.getElementById('app-theme-dark-style')) ) { - a = [0, a[0], 0]; - } - if (duration.indexOf('H') >= 0 && duration.indexOf('M') === -1) { - a = [a[0], 0, a[1]]; + document.getElementById('app-theme-dark-style').remove(); } - if ( - duration.indexOf('H') >= 0 && - duration.indexOf('M') === -1 && - duration.indexOf('S') === -1 - ) { - a = [a[0], 0, 0]; + if (document.contains(document.getElementById('app-theme-style'))) { + document.getElementById('app-theme-style').remove(); } - var length = 0; - if (a.length === 3) { - length += parseInt(a[0], 10) * 3600; - length += parseInt(a[1], 10) * 60; - length += parseInt(a[2], 10); + 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 (a.length === 2) { - length += parseInt(a[0], 10) * 60; - length += parseInt(a[1], 10); + 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 (a.length === 1) { - length += parseInt(a[0], 10); + if ($appThemeStyle.href) { + document.head.appendChild($appThemeStyle); } - return length; + this.updateVRConfigVars(); + await this.updatetrustColor(); }; - $app.methods.updateDiscord = function () { - var currentLocation = this.lastLocation.location; - var timeStamp = this.lastLocation.date; - if (this.lastLocation.location === 'traveling') { - currentLocation = this.lastLocationDestination; - timeStamp = this.lastLocationDestinationTime; - } - if ( - !this.discordActive || - !this.isGameRunning || - (!currentLocation && !this.lastLocation$.tag) - ) { - this.setDiscordActive(false); - return; - } - this.setDiscordActive(true); - var L = this.lastLocation$; - if (currentLocation !== this.lastLocation$.tag) { - Discord.SetTimestamps(timeStamp, 0); - L = API.parseLocation(currentLocation); - L.worldName = ''; - L.thumbnailImageUrl = ''; - L.worldCapacity = 0; - L.joinUrl = ''; - L.accessName = ''; - if (L.worldId) { - var ref = API.cachedWorlds.get(L.worldId); - if (ref) { - L.worldName = ref.name; - L.thumbnailImageUrl = ref.thumbnailImageUrl; - L.worldCapacity = ref.capacity; - } else { - API.getWorld({ - worldId: L.worldId - }).then((args) => { - L.worldName = args.ref.name; - L.thumbnailImageUrl = args.ref.thumbnailImageUrl; - L.worldCapacity = args.ref.capacity; - return args; - }); - } - if (this.isGameNoVR) { - var platform = 'Desktop'; - } else { - var platform = 'VR'; - } - var groupAccessType = ''; - if (L.groupAccessType) { - if (L.groupAccessType === 'public') { - groupAccessType = 'Public'; - } else if (L.groupAccessType === 'plus') { - groupAccessType = 'Plus'; - } - } - switch (L.accessType) { - case 'public': - L.joinUrl = this.getLaunchURL(L); - L.accessName = `Public #${L.instanceName} (${platform})`; - break; - case 'invite+': - L.accessName = `Invite+ #${L.instanceName} (${platform})`; - break; - case 'invite': - L.accessName = `Invite #${L.instanceName} (${platform})`; - break; - case 'friends': - L.accessName = `Friends #${L.instanceName} (${platform})`; - break; - case 'friends+': - L.accessName = `Friends+ #${L.instanceName} (${platform})`; - break; - case 'group': - L.accessName = `Group #${L.instanceName} (${platform})`; - this.getGroupName(L.groupId).then((groupName) => { - if (groupName) { - L.accessName = `Group${groupAccessType}(${groupName}) #${L.instanceName} (${platform})`; - } - }); - break; - } - } - this.lastLocation$ = L; - } - var hidePrivate = false; - if ( - this.discordHideInvite && - (L.accessType === 'invite' || - L.accessType === 'invite+' || - L.groupAccessType === 'members') - ) { - hidePrivate = true; - } - switch (API.currentUser.status) { - case 'active': - L.statusName = 'Online'; - L.statusImage = 'active'; - break; - case 'join me': - L.statusName = 'Join Me'; - L.statusImage = 'joinme'; - break; - case 'ask me': - L.statusName = 'Ask Me'; - L.statusImage = 'askme'; - if (this.discordHideInvite) { - hidePrivate = true; - } - break; - case 'busy': - L.statusName = 'Do Not Disturb'; - L.statusImage = 'busy'; - hidePrivate = true; - break; - } - var appId = '883308884863901717'; - var bigIcon = 'vrchat'; - var partyId = `${L.worldId}:${L.instanceName}`; - var partySize = this.lastLocation.playerList.size; - var partyMaxSize = L.worldCapacity; - if (partySize > partyMaxSize) { - partyMaxSize = partySize; - } - var buttonText = 'Join'; - var buttonUrl = L.joinUrl; - if (!this.discordJoinButton) { - buttonText = ''; - buttonUrl = ''; - } - if (!this.discordInstance) { - partySize = 0; - partyMaxSize = 0; - } - if (hidePrivate) { - partyId = ''; - partySize = 0; - partyMaxSize = 0; - buttonText = ''; - buttonUrl = ''; - } else if (this.isRpcWorld(L.tag)) { - // custom world rpc - if ( - L.worldId === 'wrld_f20326da-f1ac-45fc-a062-609723b097b1' || - L.worldId === 'wrld_10e5e467-fc65-42ed-8957-f02cace1398c' || - L.worldId === 'wrld_04899f23-e182-4a8d-b2c7-2c74c7c15534' - ) { - appId = '784094509008551956'; - bigIcon = 'pypy'; - } else if ( - L.worldId === 'wrld_42377cf1-c54f-45ed-8996-5875b0573a83' || - L.worldId === 'wrld_dd6d2888-dbdc-47c2-bc98-3d631b2acd7c' - ) { - appId = '846232616054030376'; - bigIcon = 'vr_dancing'; - } else if ( - L.worldId === 'wrld_52bdcdab-11cd-4325-9655-0fb120846945' || - L.worldId === 'wrld_2d40da63-8f1f-4011-8a9e-414eb8530acd' - ) { - appId = '939473404808007731'; - bigIcon = 'zuwa_zuwa_dance'; - } else if ( - L.worldId === 'wrld_74970324-58e8-4239-a17b-2c59dfdf00db' || - L.worldId === 'wrld_db9d878f-6e76-4776-8bf2-15bcdd7fc445' || - L.worldId === 'wrld_435bbf25-f34f-4b8b-82c6-cd809057eb8e' || - L.worldId === 'wrld_f767d1c8-b249-4ecc-a56f-614e433682c8' - ) { - appId = '968292722391785512'; - bigIcon = 'ls_media'; - } else if ( - L.worldId === 'wrld_266523e8-9161-40da-acd0-6bd82e075833' - ) { - appId = '1095440531821170820'; - bigIcon = 'movie_and_chill'; - } - if (this.nowPlaying.name) { - L.worldName = this.nowPlaying.name; - } - if (this.nowPlaying.playing) { - Discord.SetTimestamps( - Date.now(), - (this.nowPlaying.startTime - - this.nowPlaying.offset + - this.nowPlaying.length) * - 1000 - ); - } - } else if (!this.discordHideImage && L.thumbnailImageUrl) { - bigIcon = L.thumbnailImageUrl; - } - Discord.SetAssets( - bigIcon, // big icon - 'Powered by VRCX', // big icon hover text - L.statusImage, // small icon - L.statusName, // small icon hover text - partyId, // party id - partySize, // party size - partyMaxSize, // party max size - buttonText, // button text - buttonUrl, // button url - appId // app id + $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', ''); + } + $app.data.proxyServer = await VRCXStorage.Get('VRCX_ProxyServer'); + $app.data.disableWorldDatabase = + (await VRCXStorage.Get('VRCX_DisableWorldDatabase')) === 'true'; + $app.methods.saveVRCXWindowOption = async function () { + await configRepository.setBool( + 'VRCX_StartAtWindowsStartup', + this.isStartAtWindowsStartup ); - // NOTE - // 글자 수가 짧으면 업데이트가 안된다.. - if (L.worldName.length < 2) { - L.worldName += '\uFFA0'.repeat(2 - L.worldName.length); - } - if (hidePrivate) { - Discord.SetText('Private', ''); - Discord.SetTimestamps(0, 0); - } else if (this.discordInstance) { - Discord.SetText(L.worldName, L.accessName); - } else { - Discord.SetText(L.worldName, ''); - } + VRCXStorage.Set( + 'VRCX_StartAsMinimizedState', + this.isStartAsMinimizedState.toString() + ); + VRCXStorage.Set('VRCX_CloseToTray', this.isCloseToTray.toString()); + VRCXStorage.Set( + 'VRCX_DisableWorldDatabase', + this.disableWorldDatabase.toString() + ); + AppApi.SetStartup(this.isStartAtWindowsStartup); }; - - $app.methods.setDiscordActive = async function (active) { - if (active !== this.isDiscordActive) { - this.isDiscordActive = await Discord.SetActive(active); + $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 () { + 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 () { + await configRepository.setBool( + 'VRCX_logResourceLoad', + this.logResourceLoad + ); + await configRepository.setBool( + 'VRCX_logEmptyAvatars', + this.logEmptyAvatars + ); + }; + $app.data.autoStateChange = await configRepository.getString( + 'VRCX_autoStateChange', + 'Off' + ); + $app.data.autoAcceptInviteRequests = await configRepository.getString( + 'VRCX_autoAcceptInviteRequests', + 'Off' + ); + $app.methods.saveAutomationOptions = async function () { + await configRepository.setString( + 'VRCX_autoStateChange', + this.autoStateChange + ); + await configRepository.setString( + 'VRCX_autoAcceptInviteRequests', + this.autoAcceptInviteRequests + ); + }; + $app.data.vrcRegistryAutoBackup = await configRepository.getBool( + 'VRCX_vrcRegistryAutoBackup', + true + ); + $app.methods.saveVrcRegistryAutoBackup = async function () { + await configRepository.setBool( + 'VRCX_vrcRegistryAutoBackup', + this.vrcRegistryAutoBackup + ); + }; + $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]; + } - $app.methods.updateAutoStateChange = function () { - if ( - this.autoStateChange === 'Off' || - !this.isGameRunning || - !this.lastLocation.playerList.size || - this.lastLocation.location === '' || - this.lastLocation.location === 'traveling' - ) { - return; - } - - const otherPeopleExists = this.lastLocation.playerList.size > 1; - const prevStatus = API.currentUser.status; - let nextStatus = prevStatus; - - switch (this.autoStateChange) { - case 'Active or Ask Me': - nextStatus = otherPeopleExists ? 'ask me' : 'active'; - break; + // Migrate old settings + // Assume all exist if one does + const orderFriendsGroupPrivate = await configRepository.getBool( + 'orderFriendGroupPrivate' + ); + if (orderFriendsGroupPrivate !== null) { + await configRepository.remove('orderFriendGroupPrivate'); - case 'Active or Busy': - nextStatus = otherPeopleExists ? 'busy' : 'active'; - break; + const orderFriendsGroupStatus = await configRepository.getBool( + 'orderFriendsGroupStatus' + ); + await configRepository.remove('orderFriendsGroupStatus'); - case 'Join Me or Ask Me': - nextStatus = otherPeopleExists ? 'ask me' : 'join me'; - break; + const orderFriendsGroupGPS = await configRepository.getBool( + 'orderFriendGroupGPS' + ); + await configRepository.remove('orderFriendGroupGPS'); - case 'Join Me or Busy': - nextStatus = otherPeopleExists ? 'busy' : 'join me'; - break; + const orderOnlineFor = + await configRepository.getBool('orderFriendGroup0'); + await configRepository.remove('orderFriendGroup0'); + await configRepository.remove('orderFriendGroup1'); + await configRepository.remove('orderFriendGroup2'); + await configRepository.remove('orderFriendGroup3'); - case 'Ask Me or Busy': - nextStatus = otherPeopleExists ? 'ask me' : 'busy'; - break; - } - - if (prevStatus === nextStatus) { - return; - } - - API.saveCurrentUser({ - status: nextStatus - }); - }; - - $app.methods.lookupUser = async function (ref) { - if (ref.userId) { - this.showUserDialog(ref.userId); - return; - } - if (!ref.displayName || ref.displayName.substring(0, 3) === 'ID:') { - return; + var sortOrder = []; + if (orderFriendsGroupPrivate) { + sortOrder.push('Sort Private to Bottom'); } - for (var ctx of API.cachedUsers.values()) { - if (ctx.displayName === ref.displayName) { - this.showUserDialog(ctx.id); - return; - } + if (orderFriendsGroupStatus) { + sortOrder.push('Sort by Status'); } - 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; - } + if (orderOnlineFor && orderFriendsGroupGPS) { + sortOrder.push('Sort by Time in Instance'); } - this.$refs.searchTab.currentName = '0'; - this.$refs.menu.activeIndex = 'search'; - }; - - // #endregion - // #region | App: Search - - $app.data.searchText = ''; - $app.data.searchUserResults = []; - $app.data.searchUserParams = {}; - $app.data.searchWorldResults = []; - $app.data.searchWorldOption = ''; - $app.data.searchWorldParams = {}; - $app.data.searchAvatarResults = []; - $app.data.searchAvatarPage = []; - $app.data.searchAvatarPageNum = 0; - $app.data.searchAvatarFilter = ''; - $app.data.searchAvatarSort = ''; - $app.data.searchAvatarFilterRemote = ''; - $app.data.searchGroupResults = []; - $app.data.searchGroupParams = {}; - $app.data.isSearchUserLoading = false; - $app.data.isSearchWorldLoading = false; - $app.data.isSearchAvatarLoading = false; - $app.data.isSearchGroupLoading = false; - - API.$on('LOGIN', function () { - $app.searchText = ''; - $app.searchUserResults = []; - $app.searchUserParams = {}; - $app.searchWorldResults = []; - $app.searchWorldOption = ''; - $app.searchWorldParams = {}; - $app.searchAvatarResults = []; - $app.searchAvatarPage = []; - $app.searchAvatarPageNum = 0; - $app.searchAvatarFilter = ''; - $app.searchAvatarSort = ''; - $app.searchAvatarFilterRemote = ''; - $app.searchGroupResults = []; - $app.searchGroupParams = {}; - $app.isSearchUserLoading = false; - $app.isSearchWorldLoading = false; - $app.isSearchAvatarLoading = false; - }); - - $app.methods.clearSearch = function () { - this.searchText = ''; - this.searchUserParams = {}; - this.searchWorldParams = {}; - this.searchUserResults = []; - this.searchWorldResults = []; - this.searchAvatarResults = []; - this.searchAvatarPage = []; - this.searchAvatarPageNum = 0; - this.searchGroupParams = {}; - this.searchGroupResults = []; - }; - - $app.methods.search = function () { - switch (this.$refs.searchTab.currentName) { - case '0': - this.searchUser(); - break; - case '1': - this.searchWorld({}); - break; - case '2': - this.searchAvatar(); - break; - case '3': - this.searchGroup(); - break; + if (!orderOnlineFor) { + sortOrder.push('Sort Alphabetically'); } - }; - - $app.methods.searchUserByDisplayName = async function (displayName) { - this.searchUserParams = { - n: 10, - offset: 0, - fuzzy: false, - search: displayName - }; - await this.moreSearchUser(); - }; - $app.methods.searchUser = async function () { - this.searchUserParams = { - n: 10, - offset: 0, - search: this.searchText - }; - await this.moreSearchUser(); - }; - - $app.methods.moreSearchUser = async function (go) { - var params = this.searchUserParams; - if (go) { - params.offset += params.n * go; - if (params.offset < 0) { - params.offset = 0; + 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]; } - this.isSearchUserLoading = true; - await API.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; - }); - }; - - $app.data.searchWorldLabs = false; + await configRepository.setString( + 'VRCX_sidebarSortMethods', + JSON.stringify(sortOrder) + ); + } - $app.methods.searchWorld = function (ref) { - this.searchWorldOption = ''; - var params = { - n: 10, - offset: 0 - }; - switch (ref.sortHeading) { - case 'featured': - params.sort = 'order'; - params.featured = 'true'; - break; - case 'trending': - params.sort = 'popularity'; - params.featured = 'false'; - break; - case 'updated': - params.sort = 'updated'; - break; - case 'created': - params.sort = 'created'; - break; - case 'publication': - params.sort = 'publicationDate'; - break; - case 'shuffle': - params.sort = 'shuffle'; - break; - case 'active': - this.searchWorldOption = 'active'; - break; - case 'recent': - this.searchWorldOption = 'recent'; - break; - case 'favorite': - this.searchWorldOption = 'favorites'; - break; - case 'labs': - params.sort = 'labsPublicationDate'; - break; - case 'heat': - params.sort = 'heat'; - params.featured = 'false'; - break; - default: - params.sort = 'relevance'; - params.search = this.replaceBioSymbols(this.searchText); - break; + $app.methods.saveSidebarSortOrder = async function () { + if (this.sidebarSortMethod1 === this.sidebarSortMethod2) { + this.sidebarSortMethod2 = ''; } - params.order = ref.sortOrder || 'descending'; - if (ref.sortOwnership === 'mine') { - params.user = 'me'; - params.releaseStatus = 'all'; + if (this.sidebarSortMethod1 === this.sidebarSortMethod3) { + this.sidebarSortMethod3 = ''; } - if (ref.tag) { - params.tag = ref.tag; + if (this.sidebarSortMethod2 === this.sidebarSortMethod3) { + this.sidebarSortMethod3 = ''; } - if (!this.searchWorldLabs) { - if (params.tag) { - params.tag += ',system_approved'; - } else { - params.tag = 'system_approved'; - } + if (!this.sidebarSortMethod1) { + this.sidebarSortMethod2 = ''; } - // TODO: option.platform - this.searchWorldParams = params; - this.moreSearchWorld(); - }; - - $app.methods.moreSearchWorld = function (go) { - var params = this.searchWorldParams; - if (go) { - params.offset += params.n * go; - if (params.offset < 0) { - params.offset = 0; - } + if (!this.sidebarSortMethod2) { + this.sidebarSortMethod3 = ''; } - this.isSearchWorldLoading = true; - API.getWorlds(params, this.searchWorldOption) - .finally(() => { - this.isSearchWorldLoading = false; - }) - .then((args) => { - var map = new Map(); - for (var json of args.json) { - var ref = API.cachedWorlds.get(json.id); - if (typeof ref !== 'undefined') { - map.set(ref.id, ref); - } - } - this.searchWorldResults = Array.from(map.values()); - return args; - }); - }; - - $app.methods.searchAvatar = async function () { - this.isSearchAvatarLoading = true; - if (!this.searchAvatarFilter) { - this.searchAvatarFilter = 'all'; - } - if (!this.searchAvatarSort) { - this.searchAvatarSort = 'name'; - } - if (!this.searchAvatarFilterRemote) { - this.searchAvatarFilterRemote = 'all'; - } - if (this.searchAvatarFilterRemote !== 'local') { - this.searchAvatarSort = 'name'; - } - var avatars = new Map(); - var query = this.searchText.toUpperCase(); - if (!query) { - for (var ref of API.cachedAvatars.values()) { - switch (this.searchAvatarFilter) { - case 'all': - avatars.set(ref.id, ref); - break; - case 'public': - if (ref.releaseStatus === 'public') { - avatars.set(ref.id, ref); - } - break; - case 'private': - if (ref.releaseStatus === 'private') { - avatars.set(ref.id, ref); - } - break; - } - } - this.isSearchAvatarLoading = false; - } else { - if ( - this.searchAvatarFilterRemote === 'all' || - this.searchAvatarFilterRemote === 'local' - ) { - for (var ref of API.cachedAvatars.values()) { - var match = ref.name.toUpperCase().includes(query); - if (!match && ref.description) { - match = ref.description.toUpperCase().includes(query); - } - if (!match && ref.authorName) { - match = ref.authorName.toUpperCase().includes(query); - } - if (match) { - switch (this.searchAvatarFilter) { - case 'all': - avatars.set(ref.id, ref); - break; - case 'public': - if (ref.releaseStatus === 'public') { - avatars.set(ref.id, ref); - } - break; - case 'private': - if (ref.releaseStatus === 'private') { - avatars.set(ref.id, ref); - } - break; - } - } - } - } - if ( - (this.searchAvatarFilterRemote === 'all' || - this.searchAvatarFilterRemote === 'remote') && - this.avatarRemoteDatabase && - query.length >= 3 - ) { - var data = await this.lookupAvatars('search', query); - if (data && typeof data === 'object') { - data.forEach((avatar) => { - avatars.set(avatar.id, avatar); - }); - } - } - this.isSearchAvatarLoading = false; - } - var avatarsArray = Array.from(avatars.values()); - if (this.searchAvatarFilterRemote === 'local') { - switch (this.searchAvatarSort) { - case 'updated': - avatarsArray.sort(compareByUpdatedAt); - break; - case 'created': - avatarsArray.sort(compareByCreatedAt); - break; - case 'name': - avatarsArray.sort(compareByName); - break; - } - } - this.searchAvatarPageNum = 0; - this.searchAvatarResults = avatarsArray; - this.searchAvatarPage = avatarsArray.slice(0, 10); - }; - - $app.methods.moreSearchAvatar = function (n) { - if (n === -1) { - this.searchAvatarPageNum--; - var offset = this.searchAvatarPageNum * 10; - } - if (n === 1) { - this.searchAvatarPageNum++; - var offset = this.searchAvatarPageNum * 10; - } - this.searchAvatarPage = this.searchAvatarResults.slice( - offset, - offset + 10 - ); - }; - - $app.methods.searchGroup = async function () { - this.searchGroupParams = { - n: 10, - offset: 0, - query: this.replaceBioSymbols(this.searchText) - }; - await this.moreSearchGroup(); - }; - - $app.methods.moreSearchGroup = async function (go) { - var params = this.searchGroupParams; - if (go) { - params.offset += params.n * go; - if (params.offset < 0) { - params.offset = 0; - } - } - this.isSearchGroupLoading = true; - await API.groupSearch(params) - .finally(() => { - this.isSearchGroupLoading = false; - }) - .then((args) => { - var map = new Map(); - for (var json of args.json) { - var ref = API.cachedGroups.get(json.id); - if (typeof ref !== 'undefined') { - map.set(ref.id, ref); - } - } - this.searchGroupResults = 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') { - removeFromArray(this.favoriteFriends_, ctx); - removeFromArray(this.favoriteFriendsSorted, ctx); - } else if (type === 'world') { - removeFromArray(this.favoriteWorlds_, ctx); - removeFromArray(this.favoriteWorldsSorted, ctx); - } else if (type === 'avatar') { - removeFromArray(this.favoriteAvatars_, ctx); - 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') { - removeFromArray(this.favoriteFriends_, ctx); - removeFromArray(this.favoriteFriendsSorted, ctx); - } else if (type === 'world') { - removeFromArray(this.favoriteWorlds_, ctx); - removeFromArray(this.favoriteWorldsSorted, ctx); - } else if (type === 'avatar') { - removeFromArray(this.favoriteAvatars_, ctx); - removeFromArray(this.favoriteAvatarsSorted, ctx); - } - } - }; - - $app.methods.deleteFavorite = function (objectId) { - // FIXME: 메시지 수정 - this.$confirm('Continue? Delete Favorite', 'Confirm', { - confirmButtonText: 'Confirm', - cancelButtonText: 'Cancel', - type: 'info', - callback: (action) => { - if (action === 'confirm') { - API.deleteFavorite({ - objectId - }); - } - } - }); - }; - - $app.methods.deleteFavoriteNoConfirm = function (objectId) { - if (!objectId) { - return; - } - API.deleteFavorite({ - objectId - }); - }; - - $app.methods.changeFavoriteGroupName = function (ctx) { - this.$prompt( - $t('prompt.change_favorite_group_name.description'), - $t('prompt.change_favorite_group_name.header'), - { - distinguishCancelAndClose: true, - cancelButtonText: $t( - 'prompt.change_favorite_group_name.cancel' - ), - confirmButtonText: $t( - 'prompt.change_favorite_group_name.change' - ), - inputPlaceholder: $t( - 'prompt.change_favorite_group_name.input_placeholder' - ), - inputValue: ctx.displayName, - inputPattern: /\S+/, - inputErrorMessage: $t( - 'prompt.change_favorite_group_name.input_error' - ), - callback: (action, instance) => { - if (action === 'confirm') { - API.saveFavoriteGroup({ - type: ctx.type, - group: ctx.name, - displayName: instance.inputValue - }).then((args) => { - this.$message({ - message: $t( - 'prompt.change_favorite_group_name.message.success' - ), - type: 'success' - }); - return args; - }); - } - } - } - ); - }; - - $app.methods.clearFavoriteGroup = function (ctx) { - // FIXME: 메시지 수정 - this.$confirm('Continue? Clear Group', 'Confirm', { - confirmButtonText: 'Confirm', - cancelButtonText: 'Cancel', - type: 'info', - callback: (action) => { - if (action === 'confirm') { - API.clearFavoriteGroup({ - type: ctx.type, - group: ctx.name - }); - } - } - }); - }; - - $app.computed.favoriteFriends = function () { - if (this.sortFavoriteFriends) { - this.sortFavoriteFriends = false; - this.favoriteFriendsSorted.sort(compareByName); - } - if (this.sortFavorites) { - return this.favoriteFriends_; - } - return this.favoriteFriendsSorted; - }; - - $app.computed.favoriteWorlds = function () { - if (this.sortFavoriteWorlds) { - this.sortFavoriteWorlds = false; - this.favoriteWorldsSorted.sort(compareByName); - } - if (this.sortFavorites) { - return this.favoriteWorlds_; - } - return this.favoriteWorldsSorted; - }; - - $app.computed.favoriteAvatars = function () { - if (this.sortFavoriteAvatars) { - this.sortFavoriteAvatars = false; - this.favoriteAvatarsSorted.sort(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, 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); - }); - - API.$on('FRIEND:REQUEST', function (args) { - var ref = this.cachedUsers.get(args.params.userId); - if (typeof ref === 'undefined') { - return; - } - var friendLogHistory = { - created_at: new Date().toJSON(), - type: 'FriendRequest', - userId: ref.id, - displayName: ref.displayName - }; - $app.friendLogTable.data.push(friendLogHistory); - database.addFriendLogHistory(friendLogHistory); - }); - - API.$on('FRIEND:REQUEST:CANCEL', function (args) { - var ref = this.cachedUsers.get(args.params.userId); - if (typeof ref === 'undefined') { - return; - } - var friendLogHistory = { - created_at: new Date().toJSON(), - type: 'CancelFriendRequest', - userId: ref.id, - displayName: ref.displayName - }; - $app.friendLogTable.data.push(friendLogHistory); - database.addFriendLogHistory(friendLogHistory); - }); - - $app.data.friendLogInitStatus = false; - - $app.methods.initFriendLog = async function (userId) { - await this.updateDatabaseVersion(); - 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 - }; - this.friendLog.set(friend.id, row); - sqlValues.unshift(row); - } - database.setFriendLogCurrentArray(sqlValues); - await configRepository.setBool(`friendLogInit_${userId}`, 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 () { - await this.updateDatabaseVersion(); - 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(); - await API.refreshFriends(); - 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 API.currentUser.friends !== 'undefined') { - this.updateFriendships(API.currentUser); - } - }; - - $app.methods.addFriendship = function (id) { - if (!this.friendLogInitStatus || this.friendLog.has(id)) { - return; - } - var ref = API.cachedUsers.get(id); - if (typeof ref === 'undefined') { - try { - API.getUser({ - userId: id - }); - } catch {} - return; - } - API.getFriendStatus({ - userId: id - }).then((args) => { - if (args.json.isFriend && !this.friendLog.has(id)) { - this.addFriend(id, ref.state); - var friendLogHistory = { - created_at: new Date().toJSON(), - type: 'Friend', - userId: id, - displayName: ref.displayName - }; - this.friendLogTable.data.push(friendLogHistory); - database.addFriendLogHistory(friendLogHistory); - this.queueFriendLogNoty(friendLogHistory); - var friendLogCurrent = { - userId: id, - displayName: ref.displayName, - trustLevel: ref.$trustLevel - }; - this.friendLog.set(id, friendLogCurrent); - database.setFriendLogCurrent(friendLogCurrent); - this.notifyMenu('friendLog'); - this.deleteFriendRequest(id); - this.updateSharedFeed(true); - API.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; - } - API.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 - }; - 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.displayName !== ref.displayName) { - if (ctx.displayName) { - var friendLogHistoryDisplayName = { - created_at: new Date().toJSON(), - type: 'DisplayName', - userId: ref.id, - displayName: ref.displayName, - previousDisplayName: ctx.displayName - }; - this.friendLogTable.data.push(friendLogHistoryDisplayName); - database.addFriendLogHistory(friendLogHistoryDisplayName); - this.queueFriendLogNoty(friendLogHistoryDisplayName); - var friendLogCurrent = { - userId: ref.id, - displayName: ref.displayName, - trustLevel: ref.$trustLevel - }; - 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 - }; - 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 - }; - this.friendLogTable.data.push(friendLogHistoryTrustLevel); - database.addFriendLogHistory(friendLogHistoryTrustLevel); - this.queueFriendLogNoty(friendLogHistoryTrustLevel); - var friendLogCurrent2 = { - userId: ref.id, - displayName: ref.displayName, - trustLevel: ref.$trustLevel - }; - this.friendLog.set(ref.id, friendLogCurrent2); - database.setFriendLogCurrent(friendLogCurrent2); - this.notifyMenu('friendLog'); - this.updateSharedFeed(true); - } - ctx.trustLevel = ref.$trustLevel; - }; - - $app.methods.deleteFriendLog = function (row) { - this.$confirm('Continue? Delete Log', 'Confirm', { - confirmButtonText: 'Confirm', - cancelButtonText: 'Cancel', - type: 'info', - callback: (action) => { - if (action === 'confirm') { - removeFromArray(this.friendLogTable.data, row); - database.deleteFriendLogHistory(row.rowId); - } - } - }); - }; - - // #endregion - // #region | App: Moderation - - $app.data.playerModerationTable = { - data: [], - lastRunLength: 0, - filters: [ - { - prop: 'type', - value: [], - filterFn: (row, filter) => - filter.value.some((v) => v === row.type) - }, - { - prop: ['sourceDisplayName', 'targetDisplayName'], - value: '' - } - ], - tableProps: { - stripe: true, - size: 'mini', - defaultSort: { - prop: 'created', - order: 'descending' - } - }, - pageSize: $app.data.tablePageSize, - paginationProps: { - small: true, - layout: 'sizes,prev,pager,next,total', - pageSizes: [10, 15, 25, 50, 100] - } - }; - - API.$on('LOGIN', function () { - $app.playerModerationTable.data = []; - }); - - API.$on('PLAYER-MODERATION', function (args) { - var { ref } = args; - var array = $app.playerModerationTable.data; - var { length } = array; - for (var 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) { - var { ref } = args; - var array = $app.playerModerationTable.data; - var { length } = array; - for (var i = 0; i < length; ++i) { - if (array[i].id === ref.id) { - array.splice(i, 1); - return; - } - } - }); - - $app.methods.deletePlayerModeration = function (row) { - // FIXME: 메시지 수정 - this.$confirm('Continue? Delete Moderation', 'Confirm', { - confirmButtonText: 'Confirm', - cancelButtonText: 'Cancel', - type: 'info', - callback: (action) => { - if (action === 'confirm') { - API.deletePlayerModeration({ - moderated: row.targetUserId, - type: row.type - }); - } - } - }); - }; - - // #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, 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 = this.parseLocation(currentLocation); - this.getCachedWorld({ - worldId: L.worldId - }).then((args1) => { - this.sendInvite( - { - instanceId: L.tag, - worldId: L.tag, - worldName: args1.ref.name, - rsvp: true - }, - ref.senderUserId - ) - .then((_args) => { - $app.$message(`Auto invite sent to ${ref.senderUsername}`); - 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) { - 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; - removeFromArray($app.unseenNotifications, notificationId); - if ($app.unseenNotifications.length === 0) { - $app.selectMenu('notification'); - } - }); - - $app.methods.acceptNotification = function (row) { - // FIXME: 메시지 수정 - this.$confirm('Continue? Accept Friend Request', 'Confirm', { - confirmButtonText: 'Confirm', - cancelButtonText: 'Cancel', - type: 'info', - callback: (action) => { - if (action === 'confirm') { - API.acceptNotification({ - notificationId: row.id - }); - } - } - }); - }; - - $app.methods.hideNotification = function (row) { - this.$confirm(`Continue? Decline ${row.type}`, 'Confirm', { - confirmButtonText: 'Confirm', - cancelButtonText: 'Cancel', - type: 'info', - callback: (action) => { - if (action === 'confirm') { - if (row.type === 'ignoredFriendRequest') { - API.deleteHiddenFriendRequest( - { - notificationId: row.id - }, - row.senderUserId - ); - } else { - API.hideNotification({ - notificationId: row.id - }); - } - } - } - }); - }; - - $app.methods.deleteNotificationLog = function (row) { - this.$confirm(`Continue? Delete ${row.type}`, 'Confirm', { - confirmButtonText: 'Confirm', - cancelButtonText: 'Cancel', - type: 'info', - callback: (action) => { - if (action === 'confirm') { - removeFromArray(this.notificationTable.data, row); - if ( - row.type !== 'friendRequest' && - row.type !== 'ignoredFriendRequest' - ) { - database.deleteNotification(row.id); - } - } - } - }); - }; - - $app.methods.acceptRequestInvite = function (row) { - this.$confirm('Continue? Send Invite', 'Confirm', { - confirmButtonText: 'Confirm', - cancelButtonText: 'Cancel', - type: 'info', - callback: (action) => { - if (action === 'confirm') { - var currentLocation = this.lastLocation.location; - if (this.lastLocation.location === 'traveling') { - currentLocation = this.lastLocationDestination; - } - var L = API.parseLocation(currentLocation); - API.getCachedWorld({ - worldId: L.worldId - }).then((args) => { - API.sendInvite( - { - instanceId: L.tag, - worldId: L.tag, - worldName: args.ref.name, - rsvp: true - }, - row.senderUserId - ).then((_args) => { - this.$message('Invite sent'); - API.hideNotification({ - notificationId: row.id - }); - return _args; - }); - }); - } - } - }); - }; - - // Save Table Filters - $app.methods.saveTableFilters = async function () { - await configRepository.setString( - 'VRCX_friendLogTableFilters', - JSON.stringify(this.friendLogTable.filters[0].value) - ); - await configRepository.setString( - 'VRCX_playerModerationTableFilters', - JSON.stringify(this.playerModerationTable.filters[0].value) - ); - await configRepository.setString( - 'VRCX_notificationTableFilters', - JSON.stringify(this.notificationTable.filters[0].value) - ); - }; - - $app.data.feedTable.filter = JSON.parse( - await configRepository.getString('VRCX_feedTableFilters', '[]') - ); - $app.data.feedTable.vip = await configRepository.getBool( - 'VRCX_feedTableVIPFilter', - 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.playerModerationTable.filters[0].value = JSON.parse( - await configRepository.getString( - 'VRCX_playerModerationTableFilters', - '[]' - ) - ); - $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.configTreeData = []; - $app.data.currentUserTreeData = []; - $app.data.currentUserFeedbackData = []; - $app.data.pastDisplayNameTable = { - data: [], - tableProps: { - stripe: true, - size: 'mini', - defaultSort: { - prop: 'updated_at', - order: 'descending' - } - }, - layout: 'table' - }; - $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.friendsListTable = { - data: [], - tableProps: { - stripe: true, - size: 'mini', - defaultSort: { - prop: '$friendNum', - order: 'descending' - } - }, - pageSize: 100, - paginationProps: { - small: true, - layout: 'sizes,prev,pager,next,total', - pageSizes: [50, 100, 250, 500] - } - }; - $app.data.downloadHistoryTable = { - data: [], - tableProps: { - stripe: true, - size: 'mini' - }, - pageSize: 10, - paginationProps: { - small: true, - layout: 'prev,pager,next', - pageSizes: [10, 25, 50, 100] - } - }; - $app.data.downloadQueueTable = { - data: [], - tableProps: { - stripe: true, - size: 'mini' - }, - layout: 'table' - }; - $app.data.socialStatusHistoryTable = { - data: [], - tableProps: { - stripe: true, - size: 'mini' - }, - layout: 'table' - }; - $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.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.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 - ); - if (await configRepository.getInt('VRCX_asidewidth')) { - // migrate to new defaults - $app.data.asideWidth = await configRepository.getInt('VRCX_asidewidth'); - if ($app.data.asideWidth < 300) { - $app.data.asideWidth = 300; - } - await configRepository.setInt( - 'VRCX_sidePanelWidth', - $app.data.asideWidth - ); - await configRepository.remove('VRCX_asidewidth'); - } - $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.getString( - 'VRCX_photonLobbyTimeoutThreshold', - 6000 - ); - $app.data.clearVRCXCacheFrequency = await configRepository.getString( - '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://avtr.just-h.party/vrcx_search.php" ]' - ) - ); - $app.data.pendingOfflineDelay = 110000; - 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 () { - 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.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 () { - await configRepository.setBool( - 'VRCX_hideUserNotes', - this.hideUserNotes - ); - await configRepository.setBool( - 'VRCX_hideUserMemos', - this.hideUserMemos - ); - }; - $app.methods.saveFriendLogOptions = async function () { - await configRepository.setBool( - 'VRCX_hideUnfriends', - this.hideUnfriends - ); - this.friendLogTable.filters[2].value = this.hideUnfriends; - }; - $app.data.TTSvoices = speechSynthesis.getVoices(); - $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.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(); - }; - - $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', ''); - } - $app.data.proxyServer = await VRCXStorage.Get('VRCX_ProxyServer'); - $app.data.disableWorldDatabase = - (await VRCXStorage.Get('VRCX_DisableWorldDatabase')) === 'true'; - $app.methods.saveVRCXWindowOption = async function () { - await configRepository.setBool( - 'VRCX_StartAtWindowsStartup', - this.isStartAtWindowsStartup - ); - VRCXStorage.Set( - 'VRCX_StartAsMinimizedState', - this.isStartAsMinimizedState.toString() - ); - VRCXStorage.Set('VRCX_CloseToTray', this.isCloseToTray.toString()); - VRCXStorage.Set( - 'VRCX_DisableWorldDatabase', - this.disableWorldDatabase.toString() - ); - 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.photonLoggingEnabled = false; - $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 () { - 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 () { - await configRepository.setBool( - 'VRCX_logResourceLoad', - this.logResourceLoad - ); - await configRepository.setBool( - 'VRCX_logEmptyAvatars', - this.logEmptyAvatars - ); - }; - $app.data.autoStateChange = await configRepository.getString( - 'VRCX_autoStateChange', - 'Off' - ); - $app.data.autoAcceptInviteRequests = await configRepository.getString( - 'VRCX_autoAcceptInviteRequests', - 'Off' - ); - $app.methods.saveAutomationOptions = async function () { - await configRepository.setString( - 'VRCX_autoStateChange', - this.autoStateChange - ); - await configRepository.setString( - 'VRCX_autoAcceptInviteRequests', - this.autoAcceptInviteRequests - ); - }; - $app.data.vrcRegistryAutoBackup = await configRepository.getBool( - 'VRCX_vrcRegistryAutoBackup', - true - ); - $app.methods.saveVrcRegistryAutoBackup = async function () { - await configRepository.setBool( - 'VRCX_vrcRegistryAutoBackup', - this.vrcRegistryAutoBackup - ); - }; - $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 - ); - $app.methods.saveDiscordOption = async function () { - await configRepository.setBool('discordActive', this.discordActive); - await configRepository.setBool('discordInstance', this.discordInstance); - await configRepository.setBool( - 'discordJoinButton', - this.discordJoinButton - ); - await configRepository.setBool( - 'discordHideInvite', - this.discordHideInvite - ); - await configRepository.setBool( - 'discordHideImage', - this.discordHideImage - ); - this.lastLocation$.tag = ''; - this.nextDiscordUpdate = 7; - this.updateDiscord(); - }; - - // 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 () { - 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.methods.saveSharedFeedFilters = function () { - configRepository.setString( - 'sharedFeedFilters', - JSON.stringify(this.sharedFeedFilters) - ); - this.updateSharedFeed(true); - }; - - $app.methods.resetSharedFeedFilters = async function () { - if (await configRepository.getString('sharedFeedFilters')) { - this.sharedFeedFilters = JSON.parse( - await configRepository.getString( - 'sharedFeedFilters', - JSON.stringify(this.sharedFeedFiltersDefaults) - ) - ); - } else { - this.sharedFeedFilters = this.sharedFeedFiltersDefaults; - } - }; - - $app.data.notificationPosition = await configRepository.getString( - 'VRCX_notificationPosition', - 'topCenter' - ); - $app.methods.changeNotificationPosition = async function () { - 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.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 = API.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.currentUserTreeData = []; - $app.pastDisplayNameTable.data = []; - }); - - API.$on('USER:CURRENT', function (args) { - if (args.ref.pastDisplayNames) { - $app.pastDisplayNameTable.data = args.ref.pastDisplayNames; - } - }); - - API.$on('VISITS', function (args) { - $app.visits = args.json; - }); - - $app.methods.logout = function () { - this.$confirm('Continue? Logout', 'Confirm', { - confirmButtonText: 'Confirm', - cancelButtonText: 'Cancel', - type: 'info', - callback: (action) => { - if (action === 'confirm') { - API.logout(); - } - } - }); - }; - - $app.methods.resetHome = function () { - this.$confirm('Continue? Reset Home', 'Confirm', { - confirmButtonText: 'Confirm', - cancelButtonText: 'Cancel', - type: 'info', - callback: (action) => { - if (action === 'confirm') { - API.saveCurrentUser({ - homeLocation: '' - }).then((args) => { - this.$message({ - message: 'Home world has been reset', - type: 'success' - }); - return args; - }); - } - } - }); - }; - - $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 = 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 = 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.refreshConfigTreeData = function () { - this.configTreeData = buildTreeData(API.cachedConfig); - }; - - $app.methods.refreshCurrentUserTreeData = function () { - this.currentUserTreeData = buildTreeData(API.currentUser); - }; - - $app.methods.promptUserIdDialog = function () { - this.$prompt( - $t('prompt.direct_access_user_id.description'), - $t('prompt.direct_access_user_id.header'), - { - distinguishCancelAndClose: true, - confirmButtonText: $t('prompt.direct_access_user_id.ok'), - cancelButtonText: $t('prompt.direct_access_user_id.cancel'), - inputPattern: /\S+/, - inputErrorMessage: $t( - 'prompt.direct_access_user_id.input_error' - ), - callback: (action, instance) => { - if (action === 'confirm' && instance.inputValue) { - var testUrl = instance.inputValue.substring(0, 15); - if (testUrl === 'https://vrchat.') { - var userId = this.parseUserUrl(instance.inputValue); - if (userId) { - this.showUserDialog(userId); - } else { - this.$message({ - message: $t( - 'prompt.direct_access_user_id.message.error' - ), - type: 'error' - }); - } - } else { - this.showUserDialog(instance.inputValue); - } - } - } - } - ); - }; - - $app.methods.promptUsernameDialog = function () { - this.$prompt( - $t('prompt.direct_access_username.description'), - $t('prompt.direct_access_username.header'), - { - distinguishCancelAndClose: true, - confirmButtonText: $t('prompt.direct_access_username.ok'), - cancelButtonText: $t('prompt.direct_access_username.cancel'), - inputPattern: /\S+/, - inputErrorMessage: $t( - 'prompt.direct_access_username.input_error' - ), - callback: (action, instance) => { - if (action === 'confirm' && instance.inputValue) { - this.lookupUser({ displayName: instance.inputValue }); - } - } - } - ); - }; - - $app.methods.promptWorldDialog = function () { - this.$prompt( - $t('prompt.direct_access_world_id.description'), - $t('prompt.direct_access_world_id.header'), - { - distinguishCancelAndClose: true, - confirmButtonText: $t('prompt.direct_access_world_id.ok'), - cancelButtonText: $t('prompt.direct_access_world_id.cancel'), - inputPattern: /\S+/, - inputErrorMessage: $t( - 'prompt.direct_access_world_id.input_error' - ), - callback: (action, instance) => { - if (action === 'confirm' && instance.inputValue) { - if (!this.directAccessWorld(instance.inputValue)) { - this.$message({ - message: $t( - 'prompt.direct_access_world_id.message.error' - ), - type: 'error' - }); - } - } - } - } - ); - }; - - $app.methods.promptAvatarDialog = function () { - this.$prompt( - $t('prompt.direct_access_avatar_id.description'), - $t('prompt.direct_access_avatar_id.header'), - { - distinguishCancelAndClose: true, - confirmButtonText: $t('prompt.direct_access_avatar_id.ok'), - cancelButtonText: $t('prompt.direct_access_avatar_id.cancel'), - inputPattern: /\S+/, - inputErrorMessage: $t( - 'prompt.direct_access_avatar_id.input_error' - ), - callback: (action, instance) => { - if (action === 'confirm' && instance.inputValue) { - var testUrl = instance.inputValue.substring(0, 15); - if (testUrl === 'https://vrchat.') { - var avatarId = this.parseAvatarUrl( - instance.inputValue - ); - if (avatarId) { - this.showAvatarDialog(avatarId); - } else { - this.$message({ - message: $t( - 'prompt.direct_access_avatar_id.message.error' - ), - type: 'error' - }); - } - } else { - this.showAvatarDialog(instance.inputValue); - } - } - } - } - ); - }; - - $app.methods.promptOmniDirectDialog = function () { - this.$prompt( - $t('prompt.direct_access_omni.description'), - $t('prompt.direct_access_omni.header'), - { - distinguishCancelAndClose: true, - confirmButtonText: $t('prompt.direct_access_omni.ok'), - cancelButtonText: $t('prompt.direct_access_omni.cancel'), - inputPattern: /\S+/, - inputErrorMessage: $t('prompt.direct_access_omni.input_error'), - callback: (action, instance) => { - if (action === 'confirm' && instance.inputValue) { - var input = instance.inputValue.trim(); - if (!this.directAccessParse(input)) { - this.$message({ - message: $t( - 'prompt.direct_access_omni.message.error' - ), - type: 'error' - }); - } - } - } - } - ); - }; - - $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; - if (urlPath.substring(5, 12) === '/world/') { - var worldId = urlPath.substring(12); - 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 API.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) { - API.groupStrictsearch({ query: shortCode }).then((args) => { - for (var group of args.json) { - 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; - if (urlPath.substring(5, 11) === '/user/') { - var userId = urlPath.substring(11); - this.showUserDialog(userId); - return true; - } else if (urlPath.substring(5, 13) === '/avatar/') { - var avatarId = urlPath.substring(13); - this.showAvatarDialog(avatarId); - return true; - } else if (urlPath.substring(5, 12) === '/group/') { - var groupId = urlPath.substring(12); - 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.promptNotificationTimeout = function () { - this.$prompt( - $t('prompt.notification_timeout.description'), - $t('prompt.notification_timeout.header'), - { - distinguishCancelAndClose: true, - confirmButtonText: $t('prompt.notification_timeout.ok'), - cancelButtonText: $t('prompt.notification_timeout.cancel'), - inputValue: this.notificationTimeout / 1000, - inputPattern: /\d+$/, - inputErrorMessage: $t( - 'prompt.notification_timeout.input_error' - ), - callback: async (action, instance) => { - if ( - action === 'confirm' && - instance.inputValue && - !isNaN(instance.inputValue) - ) { - this.notificationTimeout = Math.trunc( - Number(instance.inputValue) * 1000 - ); - await configRepository.setString( - 'VRCX_notificationTimeout', - this.notificationTimeout - ); - this.updateVRConfigVars(); - } - } - } - ); - }; - - $app.methods.promptPhotonOverlayMessageTimeout = function () { - this.$prompt( - $t('prompt.overlay_message_timeout.description'), - $t('prompt.overlay_message_timeout.header'), - { - distinguishCancelAndClose: true, - confirmButtonText: $t('prompt.overlay_message_timeout.ok'), - cancelButtonText: $t('prompt.overlay_message_timeout.cancel'), - inputValue: this.photonOverlayMessageTimeout / 1000, - inputPattern: /\d+$/, - inputErrorMessage: $t( - 'prompt.overlay_message_timeout.input_error' - ), - callback: async (action, instance) => { - if ( - action === 'confirm' && - instance.inputValue && - !isNaN(instance.inputValue) - ) { - this.photonOverlayMessageTimeout = Math.trunc( - Number(instance.inputValue) * 1000 - ); - await configRepository.setString( - 'VRCX_photonOverlayMessageTimeout', - this.photonOverlayMessageTimeout - ); - this.updateVRConfigVars(); - } - } - } - ); - }; - - $app.methods.promptRenameAvatar = function (avatar) { - this.$prompt( - $t('prompt.rename_avatar.description'), - $t('prompt.rename_avatar.header'), - { - distinguishCancelAndClose: true, - confirmButtonText: $t('prompt.rename_avatar.ok'), - cancelButtonText: $t('prompt.rename_avatar.cancel'), - inputValue: avatar.ref.name, - inputErrorMessage: $t('prompt.rename_avatar.input_error'), - callback: (action, instance) => { - if ( - action === 'confirm' && - instance.inputValue !== avatar.ref.name - ) { - API.saveAvatar({ - id: avatar.id, - name: instance.inputValue - }).then((args) => { - this.$message({ - message: $t( - 'prompt.rename_avatar.message.success' - ), - type: 'success' - }); - return args; - }); - } - } - } - ); - }; - - $app.methods.promptChangeAvatarDescription = function (avatar) { - this.$prompt( - $t('prompt.change_avatar_description.description'), - $t('prompt.change_avatar_description.header'), - { - distinguishCancelAndClose: true, - confirmButtonText: $t('prompt.change_avatar_description.ok'), - cancelButtonText: $t('prompt.change_avatar_description.cancel'), - inputValue: avatar.ref.description, - inputErrorMessage: $t( - 'prompt.change_avatar_description.input_error' - ), - callback: (action, instance) => { - if ( - action === 'confirm' && - instance.inputValue !== avatar.ref.description - ) { - API.saveAvatar({ - id: avatar.id, - description: instance.inputValue - }).then((args) => { - this.$message({ - message: $t( - 'prompt.change_avatar_description.message.success' - ), - type: 'success' - }); - return args; - }); - } - } - } - ); - }; - - $app.methods.promptRenameWorld = function (world) { - this.$prompt( - $t('prompt.rename_world.description'), - $t('prompt.rename_world.header'), - { - distinguishCancelAndClose: true, - confirmButtonText: $t('prompt.rename_world.ok'), - cancelButtonText: $t('prompt.rename_world.cancel'), - inputValue: world.ref.name, - inputErrorMessage: $t('prompt.rename_world.input_error'), - callback: (action, instance) => { - if ( - action === 'confirm' && - instance.inputValue !== world.ref.name - ) { - API.saveWorld({ - id: world.id, - name: instance.inputValue - }).then((args) => { - this.$message({ - message: $t( - 'prompt.rename_world.message.success' - ), - type: 'success' - }); - return args; - }); - } - } - } - ); - }; - - $app.methods.promptChangeWorldDescription = function (world) { - this.$prompt( - $t('prompt.change_world_description.description'), - $t('prompt.change_world_description.header'), - { - distinguishCancelAndClose: true, - confirmButtonText: $t('prompt.change_world_description.ok'), - cancelButtonText: $t('prompt.change_world_description.cancel'), - inputValue: world.ref.description, - inputErrorMessage: $t( - 'prompt.change_world_description.input_error' - ), - callback: (action, instance) => { - if ( - action === 'confirm' && - instance.inputValue !== world.ref.description - ) { - API.saveWorld({ - id: world.id, - description: instance.inputValue - }).then((args) => { - this.$message({ - message: $t( - 'prompt.change_world_description.message.success' - ), - type: 'success' - }); - return args; - }); - } - } - } - ); - }; - - $app.methods.promptChangeWorldCapacity = function (world) { - this.$prompt( - $t('prompt.change_world_capacity.description'), - $t('prompt.change_world_capacity.header'), - { - distinguishCancelAndClose: true, - confirmButtonText: $t('prompt.change_world_capacity.ok'), - cancelButtonText: $t('prompt.change_world_capacity.cancel'), - inputValue: world.ref.capacity, - inputPattern: /\d+$/, - inputErrorMessage: $t( - 'prompt.change_world_capacity.input_error' - ), - callback: (action, instance) => { - if ( - action === 'confirm' && - instance.inputValue !== world.ref.capacity - ) { - API.saveWorld({ - id: world.id, - capacity: instance.inputValue - }).then((args) => { - this.$message({ - message: $t( - 'prompt.change_world_capacity.message.success' - ), - type: 'success' - }); - return args; - }); - } - } - } - ); - }; - - $app.methods.promptChangeWorldRecommendedCapacity = function (world) { - this.$prompt( - $t('prompt.change_world_recommended_capacity.description'), - $t('prompt.change_world_recommended_capacity.header'), - { - distinguishCancelAndClose: true, - confirmButtonText: $t('prompt.change_world_capacity.ok'), - cancelButtonText: $t('prompt.change_world_capacity.cancel'), - inputValue: world.ref.recommendedCapacity, - inputPattern: /\d+$/, - inputErrorMessage: $t( - 'prompt.change_world_recommended_capacity.input_error' - ), - callback: (action, instance) => { - if ( - action === 'confirm' && - instance.inputValue !== world.ref.recommendedCapacity - ) { - API.saveWorld({ - id: world.id, - recommendedCapacity: instance.inputValue - }).then((args) => { - this.$message({ - message: $t( - 'prompt.change_world_recommended_capacity.message.success' - ), - type: 'success' - }); - return args; - }); - } - } - } - ); - }; - - $app.methods.promptChangeWorldYouTubePreview = function (world) { - this.$prompt( - $t('prompt.change_world_preview.description'), - $t('prompt.change_world_preview.header'), - { - distinguishCancelAndClose: true, - confirmButtonText: $t('prompt.change_world_preview.ok'), - cancelButtonText: $t('prompt.change_world_preview.cancel'), - inputValue: world.ref.previewYoutubeId, - inputErrorMessage: $t( - 'prompt.change_world_preview.input_error' - ), - callback: (action, instance) => { - if ( - action === 'confirm' && - instance.inputValue !== world.ref.previewYoutubeId - ) { - if (instance.inputValue.length > 11) { - try { - var url = new URL(instance.inputValue); - var id1 = url.pathname; - var id2 = url.searchParams.get('v'); - if (id1 && id1.length === 12) { - instance.inputValue = id1.substring(1, 12); - } - if (id2 && id2.length === 11) { - instance.inputValue = id2; - } - } catch { - this.$message({ - message: $t( - 'prompt.change_world_preview.message.error' - ), - type: 'error' - }); - return; - } - } - if ( - instance.inputValue !== world.ref.previewYoutubeId - ) { - API.saveWorld({ - id: world.id, - previewYoutubeId: instance.inputValue - }).then((args) => { - this.$message({ - message: $t( - 'prompt.change_world_preview.message.success' - ), - type: 'success' - }); - return args; - }); - } - } - } - } - ); - }; - - $app.methods.promptMaxTableSizeDialog = function () { - this.$prompt( - $t('prompt.change_table_size.description'), - $t('prompt.change_table_size.header'), - { - distinguishCancelAndClose: true, - confirmButtonText: $t('prompt.change_table_size.save'), - cancelButtonText: $t('prompt.change_table_size.cancel'), - inputValue: this.maxTableSize, - inputPattern: /\d+$/, - inputErrorMessage: $t('prompt.change_table_size.input_error'), - callback: async (action, instance) => { - if (action === 'confirm' && instance.inputValue) { - if (instance.inputValue > 10000) { - instance.inputValue = 10000; - } - this.maxTableSize = instance.inputValue; - await configRepository.setString( - 'VRCX_maxTableSize', - this.maxTableSize - ); - database.setmaxTableSize(this.maxTableSize); - this.feedTableLookup(); - this.gameLogTableLookup(); - } - } - } - ); - }; - - $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); - }; - - $app.methods.promptPhotonLobbyTimeoutThreshold = function () { - this.$prompt( - $t('prompt.photon_lobby_timeout.description'), - $t('prompt.photon_lobby_timeout.header'), - { - distinguishCancelAndClose: true, - confirmButtonText: $t('prompt.photon_lobby_timeout.ok'), - cancelButtonText: $t('prompt.photon_lobby_timeout.cancel'), - inputValue: this.photonLobbyTimeoutThreshold / 1000, - inputPattern: /\d+$/, - inputErrorMessage: $t( - 'prompt.photon_lobby_timeout.input_error' - ), - callback: async (action, instance) => { - if ( - action === 'confirm' && - instance.inputValue && - !isNaN(instance.inputValue) - ) { - this.photonLobbyTimeoutThreshold = Math.trunc( - Number(instance.inputValue) * 1000 - ); - await configRepository.setString( - 'VRCX_photonLobbyTimeoutThreshold', - this.photonLobbyTimeoutThreshold - ); - } - } - } - ); - }; - - $app.methods.promptAutoClearVRCXCacheFrequency = function () { - this.$prompt( - $t('prompt.auto_clear_cache.description'), - $t('prompt.auto_clear_cache.header'), - { - distinguishCancelAndClose: true, - confirmButtonText: $t('prompt.auto_clear_cache.ok'), - cancelButtonText: $t('prompt.auto_clear_cache.cancel'), - inputValue: this.clearVRCXCacheFrequency / 3600 / 2, - inputPattern: /\d+$/, - inputErrorMessage: $t('prompt.auto_clear_cache.input_error'), - callback: async (action, instance) => { - if ( - action === 'confirm' && - instance.inputValue && - !isNaN(instance.inputValue) - ) { - this.clearVRCXCacheFrequency = Math.trunc( - Number(instance.inputValue) * 3600 * 2 - ); - await configRepository.setString( - 'VRCX_clearVRCXCacheFrequency', - this.clearVRCXCacheFrequency - ); - } - } - } - ); - }; - - // #endregion - // #region | App: Dialog - - var adjustDialogZ = (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' - }, - 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: '' - }, - joinCount: 0, - timeSpent: 0, - lastSeen: '', - avatarModeration: 0, - previousDisplayNames: [], - dateFriended: '', - unFriended: false, - dateFriendedInfo: [] - }; - - $app.data.ignoreUserMemoSave = false; - - $app.watch['userDialog.memo'] = function () { - if (this.ignoreUserMemoSave) { - this.ignoreUserMemoSave = false; - return; - } - var D = this.userDialog; - this.saveMemo(D.id, D.memo); - }; - - $app.methods.setUserDialogWorldSorting = async function (sortOrder) { - var D = this.userDialog; - if (D.worldSorting === sortOrder) { - return; - } - D.worldSorting = sortOrder; - await this.refreshUserDialogWorlds(); - }; - - $app.methods.setUserDialogWorldOrder = async function (order) { - var D = this.userDialog; - if (D.worldOrder === order) { - return; - } - D.worldOrder = order; - await this.refreshUserDialogWorlds(); - }; - - $app.methods.getFaviconUrl = function (resource) { - try { - var url = new URL(resource); - return `https://icons.duckduckgo.com/ip2/${url.host}.ico`; - } catch (err) { - return ''; - } - }; - - 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('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', function (args) { - var D = $app.userDialog; - if (D.visible === false || D.id !== args.params.userId) { - return; - } - if (args.json.success) { - D.isFriend = true; - } else { - D.outgoingRequest = true; - } - }); - - 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: '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; - } - this.$nextTick(() => adjustDialogZ(this.$refs.userDialog.$el)); - var D = this.userDialog; - D.id = userId; - D.treeData = []; - this.ignoreUserMemoSave = true; - D.memo = ''; - D.note = ''; - D.noteSaving = false; - this.getMemo(userId).then((memo) => { - if (memo.userId === userId) { - this.ignoreUserMemoSave = true; - D.memo = memo.memo; - var ref = this.friends.get(userId); - if (ref) { - ref.memo = String(memo.memo || ''); - if (memo.memo) { - var array = memo.memo.split('\n'); - ref.$nickName = array[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.representedGroup = { - bannerUrl: '', - description: '', - discriminator: '', - groupId: '', - iconUrl: '', - isRepresenting: false, - memberCount: 0, - memberVisibility: '', - name: '', - ownerId: '', - privacy: '', - shortCode: '' - }; - 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); - API.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) { - D.loading = false; - 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 (var 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 (this.$refs.userDialogTabs.currentName === '0') { - this.userDialogLastActiveTab = $t( - 'dialog.user.info.header' - ); - } else if (this.$refs.userDialogTabs.currentName === '1') { - this.userDialogLastActiveTab = $t( - 'dialog.user.groups.header' - ); - if (this.userDialogLastGroup !== userId) { - this.userDialogLastGroup = userId; - this.getUserGroups(userId); - } - } else if (this.$refs.userDialogTabs.currentName === '2') { - this.userDialogLastActiveTab = $t( - 'dialog.user.worlds.header' - ); - this.setUserDialogWorlds(userId); - if (this.userDialogLastWorld !== userId) { - this.userDialogLastWorld = userId; - this.refreshUserDialogWorlds(); - } - } else if (this.$refs.userDialogTabs.currentName === '3') { - this.userDialogLastActiveTab = $t( - 'dialog.user.favorite_worlds.header' - ); - if (this.userDialogLastFavoriteWorld !== userId) { - this.userDialogLastFavoriteWorld = userId; - this.getUserFavoriteWorlds(userId); - } - } else if (this.$refs.userDialogTabs.currentName === '4') { - this.userDialogLastActiveTab = $t( - 'dialog.user.avatars.header' - ); - this.setUserDialogAvatars(userId); - this.userDialogLastAvatar = userId; - if ( - userId === API.currentUser.id && - D.avatars.length === 0 - ) { - this.refreshUserDialogAvatars(); - } - this.setUserDialogAvatarsRemote(userId); - } else if (this.$refs.userDialogTabs.currentName === '5') { - this.userDialogLastActiveTab = $t( - 'dialog.user.json.header' - ); - this.refreshUserDialogTreeData(); - } - if (args.cache) { - API.getUser(args.params); - } - var inCurrentWorld = false; - if (this.lastLocation.playerList.has(D.ref.displayName)) { - inCurrentWorld = true; - } - if (userId !== API.currentUser.id) { - database - .getUserStats(D.ref, inCurrentWorld) - .then((ref1) => { - if (ref1.userId === D.id) { - D.lastSeen = ref1.created_at; - D.joinCount = ref1.joinCount; - D.timeSpent = ref1.timeSpent; - } - var 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); - } - } - }); - var 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.created_at; - D.joinCount = ref1.joinCount; - D.timeSpent = ref1.timeSpent; - } - }); - } - API.getRepresentedGroup({ userId }).then((args1) => { - D.representedGroup = args1.json; - return args1; - }); - } - return args; - }); - this.showUserDialogHistory.delete(userId); - this.showUserDialogHistory.add(userId); - }; - - $app.methods.applyUserDialogLocation = function (updateInstanceOccupants) { - var D = this.userDialog; - if (!D.visible) { - return; - } - var L = API.parseLocation(D.ref.$location.tag); - if (updateInstanceOccupants && this.isRealInstance(L.tag)) { - API.getInstance({ - worldId: L.worldId, - instanceId: L.instanceId - }); - } - D.$location = L; - if (L.userId) { - var ref = API.cachedUsers.get(L.userId); - if (typeof ref === 'undefined') { - API.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.displayName === user.displayName; - }); - 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 (!this.isRealInstance(L.tag)) { - 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 - if ($app.lastLocation.playerList.has(ref.displayName)) { - var playerListRef = $app.lastLocation.playerList.get( - ref.displayName - ); - if (!playerListRef.userId) { - playerListRef.userId = ref.id; - $app.lastLocation.playerList.set( - ref.displayName, - playerListRef - ); - if ($app.lastLocation.friendList.has(ref.displayName)) { - $app.lastLocation.friendList.set( - ref.displayName, - playerListRef - ); - } - } - // add/remove friends from lastLocation.friendList - if ( - !$app.lastLocation.friendList.has(ref.displayName) && - $app.friends.has(ref.id) - ) { - var userMap = { - displayName: ref.displayName, - userId: ref.id, - joinTime: playerListRef.joinTime - }; - $app.lastLocation.friendList.set(ref.displayName, userMap); - } - if ( - $app.lastLocation.friendList.has(ref.displayName) && - !$app.friends.has(ref.id) - ) { - $app.lastLocation.friendList.delete(ref.displayName); - } - $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.displayName) - ) { - 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.displayName - ); - 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, - stickersDisabled: false, - inCache: false, - cacheSize: '', - bundleSizes: [], - lastUpdated: '' - }; - $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, - stickersDisabled: 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, - stickersDisabled: false, - inCache: false, - cacheSize: '', - bundleSizes: [], - lastUpdated: '' - }; - var L = API.parseLocation(instanceId); - this.currentInstanceLocation = L; - API.getWorld({ - worldId: L.worldId - }).then((args) => { - this.currentInstanceWorld.ref = args.ref; - var { isPC, isQuest, isIos } = this.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'); - this.currentInstanceWorld.stickersDisabled = - args.ref?.tags.includes('feature_stickers_disabled'); - this.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 { - API.getCachedWorld({ - worldId: this.currentInstanceLocation.worldId - }).then((args) => { - this.currentInstanceWorld.ref = args.ref; - var { isPC, isQuest, isIos } = this.getAvailablePlatforms( - args.ref.unityPackages - ); - this.currentInstanceWorld.isPC = isPC; - this.currentInstanceWorld.isQuest = isQuest; - this.currentInstanceWorld.isIos = isIos; - this.checkVRChatCache(args.ref).then((cacheInfo) => { - if (cacheInfo.Item1 > 0) { - this.currentInstanceWorld.inCache = true; - this.currentInstanceWorld.cacheSize = `${( - cacheInfo.Item1 / 1048576 - ).toFixed(2)} MB`; - } - }); - }); - } - if (this.isRealInstance(instanceId)) { - var ref = API.cachedInstances.get(instanceId); - if (typeof ref !== 'undefined') { - this.currentInstanceWorld.instance = ref; - } else { - var L = API.parseLocation(instanceId); - API.getInstance({ - worldId: L.worldId, - instanceId: L.instanceId - }).then((args) => { - this.currentInstanceWorld.instance = args.ref; - }); - } - } - }; - - $app.methods.getAvailablePlatforms = function (unityPackages) { - var isPC = false; - var isQuest = false; - var isIos = false; - if (typeof unityPackages === 'object') { - for (var unityPackage of unityPackages) { - if ( - unityPackage.variant && - unityPackage.variant !== 'standard' && - unityPackage.variant !== 'security' - ) { - continue; - } - if (unityPackage.platform === 'standalonewindows') { - isPC = true; - } else if (unityPackage.platform === 'android') { - isQuest = true; - } else if (unityPackage.platform === 'ios') { - isIos = true; - } - } - } - return { isPC, isQuest, isIos }; - }; - - $app.methods.getPlatformInfo = function (unityPackages) { - var pc = {}; - var android = {}; - var ios = {}; - if (typeof unityPackages === 'object') { - for (var unityPackage of unityPackages) { - if ( - unityPackage.variant && - unityPackage.variant !== 'standard' && - unityPackage.variant !== 'security' - ) { - continue; - } - if (unityPackage.platform === 'standalonewindows') { - pc = unityPackage; - } else if (unityPackage.platform === 'android') { - android = unityPackage; - } else if (unityPackage.platform === 'ios') { - ios = unityPackage; - } - } - } - return { pc, android, ios }; - }; - - $app.methods.replaceVrcPackageUrl = function (url) { - if (!url) { - return ''; - } - return url.replace('https://api.vrchat.cloud/', 'https://vrchat.com/'); - }; - - $app.methods.selectCurrentInstanceRow = function (val) { - if (val === null) { - return; - } - var ref = val.ref; - if (ref.id) { - this.showUserDialog(ref.id); - } else { - this.lookupUser(ref); - } - }; - - $app.methods.updateTimers = function () { - for (var $timer of $timers) { - $timer.update(); - } - }; - - $app.methods.setUserDialogWorlds = function (userId) { - var worlds = []; - for (var ref of API.cachedWorlds.values()) { - if (ref.authorId === userId) { - worlds.push(ref); - } - } - $app.userDialog.worlds = worlds; - }; - - $app.methods.setUserDialogAvatars = function (userId) { - var avatars = new Set(); - this.userDialogAvatars.forEach((avatar) => { - avatars.add(avatar.id, avatar); - }); - for (var ref of API.cachedAvatars.values()) { - if (ref.authorId === userId && !avatars.has(ref.id)) { - this.userDialog.avatars.push(ref); - } - } - this.sortUserDialogAvatars(this.userDialog.avatars); - }; - - $app.methods.setUserDialogAvatarsRemote = async function (userId) { - if (this.avatarRemoteDatabase && userId !== API.currentUser.id) { - var data = await this.lookupAvatars('authorId', userId); - var avatars = new Set(); - this.userDialogAvatars.forEach((avatar) => { - avatars.add(avatar.id, avatar); - }); - if (data && typeof data === 'object') { - data.forEach((avatar) => { - if (avatar.id && !avatars.has(avatar.id)) { - this.userDialog.avatars.push(avatar); - } - }); - } - this.userDialog.avatarSorting = 'name'; - this.userDialog.avatarReleaseStatus = 'all'; - } - this.sortUserDialogAvatars(this.userDialog.avatars); - }; - - $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' - } - }); - 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' - } - }); - 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(compareByUpdatedAt); - } else { - array.sort(compareByName); - } - D.avatars = array; - }; - - $app.methods.refreshUserDialogWorlds = function () { - var D = this.userDialog; - if (D.isWorldsLoading) { - return; - } - D.isWorldsLoading = true; - var params = { - n: 50, - offset: 0, - sort: this.userDialog.worldSorting.value, - order: this.userDialog.worldOrder.value, - // user: 'friends', - userId: D.id, - releaseStatus: 'public' - }; - if (params.userId === API.currentUser.id) { - params.user = 'me'; - params.releaseStatus = 'all'; - } - var map = new Map(); - for (var ref of API.cachedWorlds.values()) { - if ( - ref.authorId === D.id && - (ref.authorId === API.currentUser.id || - ref.releaseStatus === 'public') - ) { - API.cachedWorlds.delete(ref.id); - } - } - API.bulk({ - fn: 'getWorlds', - N: -1, - params, - handle: (args) => { - for (var json of args.json) { - var $ref = API.cachedWorlds.get(json.id); - if (typeof $ref !== 'undefined') { - map.set($ref.id, $ref); - } - } - }, - done: () => { - if (D.id === params.userId) { - this.setUserDialogWorlds(D.id); - } - D.isWorldsLoading = false; - } - }); - }; - - $app.methods.refreshUserDialogAvatars = function (fileId) { - var D = this.userDialog; - if (D.isAvatarsLoading) { - return; - } - D.isAvatarsLoading = true; - if (fileId) { - D.loading = true; - } - 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: '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' - }); - } - } - }); - }; - - var performUserDialogCommand = (command, userId) => { - switch (command) { - case 'Delete Favorite': - API.deleteFavorite({ - objectId: userId - }); - break; - case 'Accept Friend Request': - var key = API.getFriendRequest(userId); - if (key === '') { - API.sendFriendRequest({ - userId - }); - } else { - API.acceptNotification({ - notificationId: key - }); - } - break; - case 'Decline Friend Request': - var key = API.getFriendRequest(userId); - if (key === '') { - API.cancelFriendRequest({ - userId - }); - } else { - API.hideNotification({ - notificationId: key - }); - } - break; - case 'Cancel Friend Request': - API.cancelFriendRequest({ - userId - }); - break; - case 'Send Friend Request': - API.sendFriendRequest({ - userId - }); - break; - case 'Unblock': - API.deletePlayerModeration({ - moderated: userId, - type: 'block' - }); - break; - case 'Block': - API.sendPlayerModeration({ - moderated: userId, - type: 'block' - }); - break; - case 'Unmute': - API.deletePlayerModeration({ - moderated: userId, - type: 'mute' - }); - break; - case 'Mute': - API.sendPlayerModeration({ - moderated: userId, - type: 'mute' - }); - break; - case 'Enable Avatar Interaction': - API.deletePlayerModeration({ - moderated: userId, - type: 'interactOff' - }); - break; - case 'Disable Avatar Interaction': - API.sendPlayerModeration({ - moderated: userId, - type: 'interactOff' - }); - break; - case 'Unmute Chatbox': - API.deletePlayerModeration({ - moderated: userId, - type: 'muteChat' - }); - break; - case 'Mute Chatbox': - API.sendPlayerModeration({ - moderated: userId, - type: 'muteChat' - }); - break; - case 'Report Hacking': - $app.reportUserForHacking(userId); - break; - case 'Unfriend': - API.deleteFriend({ - userId - }); - break; - } - }; - - $app.methods.userDialogCommand = function (command) { - var D = this.userDialog; - if (D.visible === false) { - return; - } - if (command === 'Refresh') { - this.showUserDialog(D.id); - } else if (command === 'Add Favorite') { - this.showFavoriteDialog('friend', D.id); - } else if (command === 'Edit Social Status') { - this.showSocialStatusDialog(); - } else if (command === 'Edit Language') { - this.showLanguageDialog(); - } else if (command === 'Edit Bio') { - this.showBioDialog(); - } else if (command === 'Edit Pronouns') { - this.showPronounsDialog(); - } else if (command === 'Logout') { - this.logout(); - } else if (command === 'Request Invite') { - API.sendRequestInvite( - { - platform: 'standalonewindows' - }, - D.id - ).then((args) => { - this.$message('Request invite sent'); - return args; - }); - } else if (command === 'Invite Message') { - var L = API.parseLocation(this.lastLocation.location); - API.getCachedWorld({ - worldId: L.worldId - }).then((args) => { - this.showSendInviteDialog( - { - instanceId: this.lastLocation.location, - worldId: this.lastLocation.location, - worldName: args.ref.name - }, - D.id - ); - }); - } else if (command === 'Request Invite Message') { - this.showSendInviteRequestDialog( - { - platform: 'standalonewindows' - }, - D.id - ); - } else if (command === 'Invite') { - var currentLocation = this.lastLocation.location; - if (this.lastLocation.location === 'traveling') { - currentLocation = this.lastLocationDestination; - } - var L = API.parseLocation(currentLocation); - API.getCachedWorld({ - worldId: L.worldId - }).then((args) => { - API.sendInvite( - { - instanceId: L.tag, - worldId: L.tag, - worldName: args.ref.name - }, - D.id - ).then((_args) => { - this.$message('Invite sent'); - return _args; - }); - }); - } else if (command === 'Show Avatar Author') { - var { currentAvatarImageUrl } = D.ref; - this.showAvatarAuthorDialog( - D.id, - D.$avatarInfo.ownerId, - currentAvatarImageUrl - ); - } else if (command === 'Show Fallback Avatar Details') { - var { fallbackAvatar } = D.ref; - if (fallbackAvatar) { - this.showAvatarDialog(fallbackAvatar); - } else { - this.$message({ - message: 'No fallback avatar set', - type: 'error' - }); - } - } else if (command === 'Previous Images') { - this.displayPreviousImages('User', 'Display'); - } else if (command === 'Previous Instances') { - this.showPreviousInstancesUserDialog(D.ref); - } else if (command === 'Manage Gallery') { - this.showGalleryDialog(); - } else if (command === 'Invite To Group') { - this.showInviteGroupDialog('', D.id); - } else if (command === 'Send Boop') { - this.showSendBoopDialog(D.id); - } else if (command === 'Hide Avatar') { - if (D.isHideAvatar) { - this.setPlayerModeration(D.id, 0); - } else { - this.setPlayerModeration(D.id, 4); - } - } else if (command === 'Show Avatar') { - if (D.isShowAvatar) { - this.setPlayerModeration(D.id, 0); - } else { - this.setPlayerModeration(D.id, 5); - } - } else { - this.$confirm(`Continue? ${command}`, 'Confirm', { - confirmButtonText: 'Confirm', - cancelButtonText: 'Cancel', - type: 'info', - callback: (action) => { - if (action === 'confirm') { - performUserDialogCommand(command, D.id); - } - } - }); - } - }; - - $app.methods.refreshUserDialogTreeData = function () { - var D = this.userDialog; - if (D.id === API.currentUser.id) { - var treeData = { - ...API.currentUser, - ...D.ref - }; - D.treeData = buildTreeData(treeData); - return; - } - D.treeData = buildTreeData(D.ref); - }; - - $app.methods.changeUserDialogAvatarSorting = function () { - var D = this.userDialog; - this.sortUserDialogAvatars(D.avatars); - }; - - $app.computed.userDialogAvatars = function () { - var { avatars, avatarReleaseStatus } = this.userDialog; - if ( - avatarReleaseStatus === 'public' || - avatarReleaseStatus === 'private' - ) { - return avatars.filter( - (avatar) => avatar.releaseStatus === avatarReleaseStatus - ); - } - return avatars; - }; - - // #endregion - // #region | App: World Dialog - - $app.data.worldDialog = { - visible: false, - loading: false, - id: '', - memo: '', - $location: {}, - ref: {}, - isFavorite: false, - avatarScalingDisabled: false, - focusViewDisabled: false, - stickersDisabled: 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 - }; - - $app.data.ignoreWorldMemoSave = false; - - $app.watch['worldDialog.memo'] = function () { - if (this.ignoreWorldMemoSave) { - this.ignoreWorldMemoSave = false; - return; - } - var D = this.worldDialog; - this.saveWorldMemo(D.id, D.memo); - }; - - 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'); - D.stickersDisabled = ref.tags?.includes('feature_stickers_disabled'); - $app.applyWorldDialogInstances(); - for (var room of D.rooms) { - if ($app.isRealInstance(room.tag)) { - API.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 (!this.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 API.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) { - this.$nextTick(() => adjustDialogZ(this.$refs.worldDialog.$el)); - var D = this.worldDialog; - var L = API.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.stickersDisabled = false; - D.isPC = false; - D.isQuest = false; - D.isIos = false; - D.hasPersistData = false; - this.ignoreWorldMemoSave = true; - D.memo = ''; - var LL = API.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) { - this.ignoreWorldMemoSave = true; - 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; - } - }); - API.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 } = this.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.stickersDisabled = args.ref?.tags.includes( - 'feature_stickers_disabled' - ); - D.isPC = isPC; - D.isQuest = isQuest; - D.isIos = isIos; - this.updateVRChatWorldCache(); - API.hasWorldPersistData({ worldId: D.id }); - if (args.cache) { - API.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.displayName === user.displayName; - }); - 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 = API.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') { - API.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.displayName === user.displayName; - }); - 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 = API.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 ($app.isRealInstance(room.tag)) { - API.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 'Refresh': - this.showWorldDialog(D.id); - break; - case 'New Instance': - this.showNewInstanceDialog(D.$location.tag); - break; - case 'Add Favorite': - this.showFavoriteDialog('world', D.id); - break; - case 'Rename': - this.promptRenameWorld(D); - break; - case 'Change Image': - this.displayPreviousImages('World', 'Change'); - break; - case 'Previous Images': - this.displayPreviousImages('World', 'Display'); - break; - case 'Previous Instances': - this.showPreviousInstancesWorldDialog(D.ref); - 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; - case 'Change Tags': - this.showSetWorldTagsDialog(); - break; - case 'Download Unity Package': - this.openExternalLink( - this.replaceVrcPackageUrl( - this.worldDialog.ref.unityPackageUrl - ) - ); - break; - default: - this.$confirm(`Continue? ${command}`, 'Confirm', { - confirmButtonText: 'Confirm', - cancelButtonText: 'Cancel', - type: 'info', - callback: (action) => { - if (action !== 'confirm') { - return; - } - switch (command) { - case 'Delete Favorite': - API.deleteFavorite({ - objectId: D.id - }); - break; - case 'Make Home': - API.saveCurrentUser({ - homeLocation: D.id - }).then((args) => { - this.$message({ - message: 'Home world updated', - type: 'success' - }); - return args; - }); - break; - case 'Reset Home': - API.saveCurrentUser({ - homeLocation: '' - }).then((args) => { - this.$message({ - message: 'Home world has been reset', - type: 'success' - }); - return args; - }); - break; - case 'Publish': - API.publishWorld({ - worldId: D.id - }).then((args) => { - this.$message({ - message: 'World has been published', - type: 'success' - }); - return args; - }); - break; - case 'Unpublish': - API.unpublishWorld({ - worldId: D.id - }).then((args) => { - this.$message({ - message: 'World has been unpublished', - type: 'success' - }); - return args; - }); - break; - case 'Delete Persistent Data': - API.deleteWorldPersistData({ - worldId: D.id - }).then((args) => { - this.$message({ - message: - 'Persistent data has been deleted', - type: 'success' - }); - return args; - }); - break; - case 'Delete': - API.deleteWorld({ - worldId: D.id - }).then((args) => { - this.$message({ - message: 'World has been deleted', - type: 'success' - }); - D.visible = false; - return args; - }); - break; - } - } - }); - break; - } - }; - - $app.methods.refreshWorldDialogTreeData = function () { - var D = this.worldDialog; - D.treeData = buildTreeData(D.ref); - }; - - $app.computed.worldDialogPlatform = function () { - var { ref } = this.worldDialog; - var platforms = []; - if (ref.unityPackages) { - for (var unityPackage of ref.unityPackages) { - var platform = 'PC'; - if (unityPackage.platform === 'standalonewindows') { - platform = 'PC'; - } else if (unityPackage.platform === 'android') { - platform = 'Android'; - } else if (unityPackage.platform) { - ({ platform } = unityPackage); - } - platforms.unshift(`${platform}/${unityPackage.unityVersion}`); - } - } - return platforms.join(', '); - }; - - // #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, - treeData: [], - bundleSizes: [], - platformInfo: {}, - lastUpdated: '', - inCache: false, - cacheSize: 0, - cacheLocked: false, - cachePath: '', - fileAnalysis: {} - }; - - $app.data.ignoreAvatarMemoSave = false; - - $app.watch['avatarDialog.memo'] = function () { - if (this.ignoreAvatarMemoSave) { - this.ignoreAvatarMemoSave = false; - return; - } - var D = this.avatarDialog; - if (D.visible === false) { - return; - } - this.saveAvatarMemo(D.id, D.memo); - }; - - API.$on('LOGOUT', function () { - $app.avatarDialog.visible = false; - }); - - 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) { - this.$nextTick(() => adjustDialogZ(this.$refs.avatarDialog.$el)); - var D = this.avatarDialog; - D.visible = true; - D.loading = true; - D.id = avatarId; - D.fileAnalysis = {}; - D.treeData = []; - 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.isFavorite = - API.cachedFavoritesByObjectId.has(avatarId) || - (this.isLocalUserVrcplusSupporter() && - this.localAvatarFavoritesList.includes(avatarId)); - D.isBlocked = API.cachedAvatarModerations.has(avatarId); - this.ignoreAvatarMemoSave = true; - D.memo = ''; - 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; - } - } - API.getAvatar({ avatarId }) - .then((args) => { - var { ref } = args; - D.ref = ref; - this.updateVRChatAvatarCache(); - if ( - ref.imageUrl === API.currentUser.currentAvatarImageUrl && - !ref.assetUrl - ) { - D.ref.assetUrl = API.currentUser.currentAvatarAssetUrl; - } - if (/quest/.test(ref.tags)) { - D.isQuestFallback = true; - } - var { isPC, isQuest, isIos } = this.getAvailablePlatforms( - args.ref.unityPackages - ); - D.isPC = isPC; - D.isQuest = isQuest; - D.isIos = isIos; - D.platformInfo = this.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.loading = false; - D.visible = false; - throw err; - }) - .finally(() => { - D.loading = false; - }); - this.getAvatarMemo(avatarId).then((memo) => { - if (D.id === memo.avatarId) { - this.ignoreAvatarMemoSave = true; - D.memo = memo.memo; - } - }); - }; - - $app.methods.selectAvatarWithConfirmation = function (id) { - this.$confirm(`Continue? Select Avatar`, 'Confirm', { - confirmButtonText: 'Confirm', - cancelButtonText: 'Cancel', - type: 'info', - callback: (action) => { - if (action !== 'confirm') { - return; - } - API.selectAvatar({ - avatarId: id - }).then((args) => { - this.$message({ - message: 'Avatar changed', - type: 'success' - }); - return args; - }); - } - }); - }; - - $app.methods.avatarDialogCommand = function (command) { - var D = this.avatarDialog; - if (D.visible === false) { - return; - } - switch (command) { - case 'Refresh': - this.showAvatarDialog(D.id); - break; - case 'Rename': - this.promptRenameAvatar(D); - break; - case 'Change Image': - this.displayPreviousImages('Avatar', 'Change'); - break; - case 'Previous Images': - this.displayPreviousImages('Avatar', 'Display'); - break; - case 'Change Description': - this.promptChangeAvatarDescription(D); - break; - case 'Change Content Tags': - this.showSetAvatarTagsDialog(D.id); - break; - case 'Download Unity Package': - this.openExternalLink( - this.replaceVrcPackageUrl( - this.avatarDialog.ref.unityPackageUrl - ) - ); - break; - case 'Add Favorite': - this.showFavoriteDialog('avatar', D.id); - break; - default: - this.$confirm(`Continue? ${command}`, 'Confirm', { - confirmButtonText: 'Confirm', - cancelButtonText: 'Cancel', - type: 'info', - callback: (action) => { - if (action !== 'confirm') { - return; - } - switch (command) { - case 'Delete Favorite': - API.deleteFavorite({ - objectId: D.id - }); - break; - case 'Select Avatar': - API.selectAvatar({ - avatarId: D.id - }).then((args) => { - this.$message({ - message: 'Avatar changed', - type: 'success' - }); - return args; - }); - break; - case 'Select Fallback Avatar': - API.selectFallbackAvatar({ - avatarId: D.id - }).then((args) => { - this.$message({ - message: 'Fallback avatar changed', - type: 'success' - }); - return args; - }); - break; - case 'Block Avatar': - API.sendAvatarModeration({ - avatarModerationType: 'block', - targetAvatarId: D.id - }).then((args) => { - this.$message({ - message: 'Avatar blocked', - type: 'success' - }); - return args; - }); - break; - case 'Unblock Avatar': - API.deleteAvatarModeration({ - avatarModerationType: 'block', - targetAvatarId: D.id - }); - break; - case 'Make Public': - API.saveAvatar({ - id: D.id, - releaseStatus: 'public' - }).then((args) => { - this.$message({ - message: 'Avatar updated to public', - type: 'success' - }); - return args; - }); - break; - case 'Make Private': - API.saveAvatar({ - id: D.id, - releaseStatus: 'private' - }).then((args) => { - this.$message({ - message: 'Avatar updated to private', - type: 'success' - }); - return args; - }); - break; - case 'Delete': - API.deleteAvatar({ - avatarId: D.id - }).then((args) => { - this.$message({ - message: 'Avatar deleted', - type: 'success' - }); - D.visible = false; - return args; - }); - break; - case 'Delete Imposter': - API.deleteImposter({ - avatarId: D.id - }).then((args) => { - this.$message({ - message: 'Imposter deleted', - type: 'success' - }); - return args; - }); - break; - case 'Create Imposter': - API.createImposter({ - avatarId: D.id - }).then((args) => { - this.$message({ - message: 'Imposter queued for creation', - type: 'success' - }); - return args; - }); - break; - } - } - }); - break; - } - }; - - $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", - type: 'warning' - }); - } else { - this.showUserDialog(avatarInfo.ownerId); - } - } - if (avatarId) { - this.showAvatarDialog(avatarId); - } - } - }; - - $app.methods.refreshAvatarDialogTreeData = function () { - var D = this.avatarDialog; - D.treeData = buildTreeData(D.ref); - }; - - $app.computed.avatarDialogPlatform = function () { - var { ref } = this.avatarDialog; - var platforms = []; - if (ref.unityPackages) { - for (var unityPackage of ref.unityPackages) { - if ( - unityPackage.variant && - unityPackage.variant !== 'standard' && - unityPackage.variant !== 'security' - ) { - continue; - } - var platform = 'PC'; - if (unityPackage.platform === 'standalonewindows') { - platform = 'PC'; - } else if (unityPackage.platform === 'android') { - platform = 'Android'; - } else if (unityPackage.platform) { - ({ platform } = unityPackage); - } - platforms.push(`${platform}/${unityPackage.unityVersion}`); - } - } - return platforms.join(', '); - }; - - // #endregion - // #region | App: Favorite Dialog - - $app.data.favoriteDialog = { - visible: false, - loading: false, - type: '', - objectId: '', - groups: [], - currentGroup: {} - }; - - API.$on('LOGOUT', function () { - $app.favoriteDialog.visible = false; - }); - - $app.methods.addFavorite = function (group) { - var D = this.favoriteDialog; - D.loading = true; - API.addFavorite({ - type: D.type, - favoriteId: D.objectId, - tags: group.name - }) - .finally(() => { - D.loading = false; - }) - .then((args) => { - return args; - }); - }; - - $app.methods.addFavoriteWorld = function (ref, group) { - return API.addFavorite({ - type: 'world', - favoriteId: ref.id, - tags: group.name - }); - }; - - $app.methods.addFavoriteAvatar = function (ref, group) { - return API.addFavorite({ - type: 'avatar', - favoriteId: ref.id, - tags: group.name - }); - }; - - $app.methods.addFavoriteUser = function (ref, group) { - return API.addFavorite({ - type: 'friend', - favoriteId: ref.id, - tags: group.name - }); - }; - - $app.methods.moveFavorite = function (ref, group, type) { - API.deleteFavorite({ - objectId: ref.id - }).then(() => { - API.addFavorite({ - type, - favoriteId: ref.id, - tags: group.name - }); - }); - }; - - $app.methods.showFavoriteDialog = function (type, objectId) { - this.$nextTick(() => adjustDialogZ(this.$refs.favoriteDialog.$el)); - var D = this.favoriteDialog; - D.type = type; - D.objectId = objectId; - if (type === 'friend') { - D.groups = API.favoriteFriendGroups; - D.visible = true; - } else if (type === 'world') { - D.groups = API.favoriteWorldGroups; - D.visible = true; - } else if (type === 'avatar') { - D.groups = API.favoriteAvatarGroups; - 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: Invite Dialog - - $app.data.inviteDialog = { - visible: false, - loading: false, - worldId: '', - worldName: '', - userIds: [], - friendsInInstance: [] - }; - - API.$on('LOGOUT', function () { - $app.inviteDialog.visible = false; - }); - - $app.methods.addFriendsInInstanceToInvite = function () { - var D = this.inviteDialog; - for (var friend of D.friendsInInstance) { - if (!D.userIds.includes(friend.id)) { - D.userIds.push(friend.id); - } - } - }; - - $app.methods.addFavoriteFriendsToInvite = function () { - var D = this.inviteDialog; - for (var friend of this.vipFriends) { - if (!D.userIds.includes(friend.id)) { - D.userIds.push(friend.id); - } - } - }; - - $app.methods.addSelfToInvite = function () { - var D = this.inviteDialog; - if (!D.userIds.includes(API.currentUser.id)) { - D.userIds.push(API.currentUser.id); - } - }; - - $app.methods.sendInvite = function () { - this.$confirm('Continue? Invite', 'Confirm', { - confirmButtonText: 'Confirm', - cancelButtonText: 'Cancel', - type: 'info', - callback: (action) => { - var D = this.inviteDialog; - if (action !== 'confirm' || D.loading === true) { - return; - } - D.loading = true; - var inviteLoop = () => { - if (D.userIds.length > 0) { - var receiverUserId = D.userIds.shift(); - if (receiverUserId === API.currentUser.id) { - // can't invite self!? - var L = API.parseLocation(D.worldId); - API.selfInvite({ - instanceId: L.instanceId, - worldId: L.worldId - }).finally(inviteLoop); - } else { - API.sendInvite( - { - instanceId: D.worldId, - worldId: D.worldId, - worldName: D.worldName - }, - receiverUserId - ).finally(inviteLoop); - } - } else { - D.loading = false; - D.visible = false; - this.$message({ - message: 'Invite sent', - type: 'success' - }); - } - }; - inviteLoop(); - } - }); - }; - - $app.methods.showInviteDialog = function (tag) { - if (!this.isRealInstance(tag)) { - return; - } - this.$nextTick(() => adjustDialogZ(this.$refs.inviteDialog.$el)); - var L = API.parseLocation(tag); - API.getCachedWorld({ - worldId: L.worldId - }).then((args) => { - var D = this.inviteDialog; - D.userIds = []; - D.worldId = L.tag; - D.worldName = args.ref.name; - D.friendsInInstance = []; - var friendsInCurrentInstance = this.lastLocation.friendList; - for (var friend of friendsInCurrentInstance.values()) { - var ctx = this.friends.get(friend.userId); - if (typeof ctx.ref === 'undefined') { - continue; - } - D.friendsInInstance.push(ctx); - } - D.visible = true; - }); - }; - - // #endregion - // #region | App: Social Status Dialog - - $app.data.socialStatusDialog = { - visible: false, - loading: false, - status: '', - statusDescription: '' - }; - - API.$on('LOGOUT', function () { - $app.socialStatusDialog.visible = false; - }); - - $app.methods.saveSocialStatus = function () { - var D = this.socialStatusDialog; - if (D.loading) { - return; - } - D.loading = true; - API.saveCurrentUser({ - status: D.status, - statusDescription: D.statusDescription - }) - .finally(() => { - D.loading = false; - }) - .then((args) => { - D.visible = false; - this.$message({ - message: 'Status updated', - type: 'success' - }); - return args; - }); - }; - - $app.methods.showSocialStatusDialog = function () { - this.$nextTick(() => adjustDialogZ(this.$refs.socialStatusDialog.$el)); - var D = this.socialStatusDialog; - var { statusHistory } = API.currentUser; - var statusHistoryArray = []; - for (var i = 0; i < statusHistory.length; ++i) { - var addStatus = { - no: i + 1, - status: statusHistory[i] - }; - statusHistoryArray.push(addStatus); - } - this.socialStatusHistoryTable.data = statusHistoryArray; - D.status = API.currentUser.status; - D.statusDescription = API.currentUser.statusDescription; - D.visible = true; - }; - - $app.methods.setSocialStatusFromHistory = function (val) { - if (val === null) { - return; - } - var D = this.socialStatusDialog; - D.statusDescription = val.status; - }; - - // #endregion - // #region | App: Language Dialog - - $app.data.subsetOfLanguages = []; - - $app.data.languageDialog = { - visible: false, - loading: false, - languageChoice: false, - languageValue: '', - languages: [] - }; - - API.$on('CONFIG', function (args) { - var languages = args.ref?.constants?.LANGUAGE?.SPOKEN_LANGUAGE_OPTIONS; - if (!languages) { - return; - } - $app.subsetOfLanguages = languages; - var data = []; - for (var key in languages) { - var value = languages[key]; - data.push({ - key, - value - }); - } - $app.languageDialog.languages = data; - }); - - API.$on('LOGOUT', function () { - $app.languageDialog.visible = false; - }); - - $app.methods.addUserLanguage = function (language) { - if (language !== String(language)) { - return; - } - var D = this.languageDialog; - D.loading = true; - API.addUserTags({ - tags: [`language_${language}`] - }).finally(function () { - D.loading = false; - }); - }; - - $app.methods.removeUserLanguage = function (language) { - if (language !== String(language)) { - return; - } - var D = this.languageDialog; - D.loading = true; - API.removeUserTags({ - tags: [`language_${language}`] - }).finally(function () { - D.loading = false; - }); - }; - - $app.methods.showLanguageDialog = function () { - this.$nextTick(() => adjustDialogZ(this.$refs.languageDialog.$el)); - var D = this.languageDialog; - D.visible = true; - }; - - // #endregion - // #region | App: Bio Dialog - - $app.data.bioDialog = { - visible: false, - loading: false, - bio: '', - bioLinks: [] - }; - - API.$on('LOGOUT', function () { - $app.bioDialog.visible = false; - }); - - $app.methods.saveBio = function () { - var D = this.bioDialog; - if (D.loading) { - return; - } - D.loading = true; - API.saveCurrentUser({ - bio: D.bio, - bioLinks: D.bioLinks - }) - .finally(() => { - D.loading = false; - }) - .then((args) => { - D.visible = false; - this.$message({ - message: 'Bio updated', - type: 'success' - }); - return args; - }); - }; - - $app.methods.showBioDialog = function () { - this.$nextTick(() => adjustDialogZ(this.$refs.bioDialog.$el)); - var D = this.bioDialog; - D.bio = API.currentUser.bio; - D.bioLinks = API.currentUser.bioLinks.slice(); - D.visible = true; - }; - - // #endregion - // #region | App: Pronouns Dialog - - $app.data.pronounsDialog = { - visible: false, - loading: false, - pronouns: '' - }; - - API.$on('LOGOUT', function () { - $app.pronounsDialog.visible = false; - }); - - $app.methods.savePronouns = function () { - var D = this.pronounsDialog; - if (D.loading) { - return; - } - D.loading = true; - API.saveCurrentUser({ - pronouns: D.pronouns - }) - .finally(() => { - D.loading = false; - }) - .then((args) => { - D.visible = false; - this.$message({ - message: 'Pronouns updated', - type: 'success' - }); - return args; - }); - }; - - $app.methods.showPronounsDialog = function () { - this.$nextTick(() => adjustDialogZ(this.$refs.pronounsDialog.$el)); - var D = this.pronounsDialog; - D.pronouns = API.currentUser.pronouns; - D.visible = true; - }; - - // #endregion - // #region | App: New Instance Dialog - - $app.data.newInstanceDialog = { - visible: false, - loading: false, - selectedTab: '0', - instanceCreated: false, - queueEnabled: false, - worldId: '', - instanceId: '', - instanceName: '', - userId: '', - accessType: '', - region: '', - groupRegion: '', - groupId: '', - groupAccessType: '', - strict: false, - location: '', - shortName: '', - url: '', - secureOrShortName: '', - lastSelectedGroupId: '', - selectedGroupRoles: [], - roleIds: [], - groupRef: {} - }; - - API.$on('LOGOUT', function () { - $app.newInstanceDialog.visible = false; - }); - - $app.methods.buildLegacyInstance = function () { - var D = this.newInstanceDialog; - D.instanceCreated = false; - D.shortName = ''; - D.secureOrShortName = ''; - var tags = []; - if (D.instanceName) { - D.instanceName = D.instanceName.replace(/[^A-Za-z0-9]/g, ''); - tags.push(D.instanceName); - } else { - var randValue = (99999 * Math.random() + 1).toFixed(0); - tags.push(String(randValue).padStart(5, '0')); - } - if (!D.userId) { - D.userId = API.currentUser.id; - } - var userId = D.userId; - if (D.accessType !== 'public') { - if (D.accessType === 'friends+') { - tags.push(`~hidden(${userId})`); - } else if (D.accessType === 'friends') { - tags.push(`~friends(${userId})`); - } else if (D.accessType === 'group') { - tags.push(`~group(${D.groupId})`); - tags.push(`~groupAccessType(${D.groupAccessType})`); - } else { - tags.push(`~private(${userId})`); - } - if (D.accessType === 'invite+') { - tags.push('~canRequestInvite'); - } - } - if (D.region === 'US West') { - tags.push(`~region(us)`); - } else if (D.region === 'US East') { - tags.push(`~region(use)`); - } else if (D.region === 'Europe') { - tags.push(`~region(eu)`); - } else if (D.region === 'Japan') { - tags.push(`~region(jp)`); - } - if (D.accessType !== 'invite' && D.accessType !== 'friends') { - D.strict = false; - } - if (D.strict) { - tags.push('~strict'); - } - if (D.groupId && D.groupId !== D.lastSelectedGroupId) { - D.roleIds = []; - var ref = API.cachedGroups.get(D.groupId); - if (typeof ref !== 'undefined') { - D.groupRef = ref; - D.selectedGroupRoles = ref.roles; - API.getGroupRoles({ - groupId: D.groupId - }).then((args) => { - D.lastSelectedGroupId = D.groupId; - D.selectedGroupRoles = args.json; - ref.roles = args.json; - }); - } - } - if (!D.groupId) { - D.roleIds = []; - D.selectedGroupRoles = []; - D.groupRef = {}; - D.lastSelectedGroupId = ''; - } - D.instanceId = tags.join(''); - this.updateNewInstanceDialog(false); - this.saveNewInstanceDialog(); - }; - - $app.methods.buildInstance = function () { - var D = this.newInstanceDialog; - D.instanceCreated = false; - D.instanceId = ''; - D.shortName = ''; - D.secureOrShortName = ''; - if (!D.userId) { - D.userId = API.currentUser.id; - } - if (D.groupId && D.groupId !== D.lastSelectedGroupId) { - D.roleIds = []; - var ref = API.cachedGroups.get(D.groupId); - if (typeof ref !== 'undefined') { - D.groupRef = ref; - D.selectedGroupRoles = ref.roles; - API.getGroupRoles({ - groupId: D.groupId - }).then((args) => { - D.lastSelectedGroupId = D.groupId; - D.selectedGroupRoles = args.json; - ref.roles = args.json; - }); - } - } - if (!D.groupId) { - D.roleIds = []; - D.groupRef = {}; - D.selectedGroupRoles = []; - D.lastSelectedGroupId = ''; - } - this.saveNewInstanceDialog(); - }; - - $app.methods.createNewInstance = function () { - var D = this.newInstanceDialog; - if (D.loading) { - return; - } - D.loading = true; - 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 params = { - type, - canRequestInvite, - worldId: D.worldId, - ownerId: API.currentUser.id, - region - }; - if (type === 'group') { - params.groupAccessType = D.groupAccessType; - params.ownerId = D.groupId; - params.queueEnabled = D.queueEnabled; - if (D.groupAccessType === 'members') { - params.roleIds = D.roleIds; - params.canRequestInvite = true; - } else if (D.groupAccessType === 'plus') { - params.canRequestInvite = true; - } - } - API.createInstance(params) - .then((args) => { - D.location = args.json.location; - D.instanceId = args.json.instanceId; - D.secureOrShortName = - args.json.shortName || args.json.secureName; - D.instanceCreated = true; - this.updateNewInstanceDialog(); - return args; - }) - .finally(() => { - D.loading = false; - }); - }; - - $app.methods.selfInvite = function (location, shortName) { - if (!this.isRealInstance(location)) { - return; - } - var L = API.parseLocation(location); - API.selfInvite({ - instanceId: L.instanceId, - worldId: L.worldId, - shortName - }).then((args) => { - this.$message({ - message: 'Self invite sent', - type: 'success' - }); - return args; - }); - }; - - $app.methods.updateNewInstanceDialog = function (noChanges) { - var D = this.newInstanceDialog; - if (D.instanceId) { - D.location = `${D.worldId}:${D.instanceId}`; - } else { - D.location = D.worldId; - } - var L = API.parseLocation(D.location); - if (noChanges) { - L.shortName = D.shortName; - } else { - D.shortName = ''; - } - D.url = this.getLaunchURL(L); - }; - - $app.methods.saveNewInstanceDialog = async function () { - await configRepository.setString( - 'instanceDialogAccessType', - this.newInstanceDialog.accessType - ); - await configRepository.setString( - 'instanceRegion', - this.newInstanceDialog.region - ); - await configRepository.setString( - 'instanceDialogInstanceName', - this.newInstanceDialog.instanceName - ); - if (this.newInstanceDialog.userId === API.currentUser.id) { - await configRepository.setString('instanceDialogUserId', ''); - } else { - await configRepository.setString( - 'instanceDialogUserId', - this.newInstanceDialog.userId - ); - } - await configRepository.setString( - 'instanceDialogGroupId', - this.newInstanceDialog.groupId - ); - await configRepository.setString( - 'instanceDialogGroupAccessType', - this.newInstanceDialog.groupAccessType - ); - await configRepository.setBool( - 'instanceDialogStrict', - this.newInstanceDialog.strict - ); - await configRepository.setBool( - 'instanceDialogQueueEnabled', - this.newInstanceDialog.queueEnabled - ); - }; - - $app.methods.showNewInstanceDialog = async function (tag) { - if (!this.isRealInstance(tag)) { - return; - } - this.$nextTick(() => adjustDialogZ(this.$refs.newInstanceDialog.$el)); - var D = this.newInstanceDialog; - var L = API.parseLocation(tag); - if (D.worldId === L.worldId) { - // reopening dialog, keep last open instance - D.visible = true; - return; - } - D.worldId = L.worldId; - D.accessType = await configRepository.getString( - 'instanceDialogAccessType', - 'public' - ); - D.region = await configRepository.getString( - 'instanceRegion', - 'US West' - ); - D.instanceName = await configRepository.getString( - 'instanceDialogInstanceName', - '' - ); - D.userId = await configRepository.getString('instanceDialogUserId', ''); - D.groupId = await configRepository.getString( - 'instanceDialogGroupId', - '' - ); - D.groupAccessType = await configRepository.getString( - 'instanceDialogGroupAccessType', - 'plus' - ); - D.queueEnabled = await configRepository.getBool( - 'instanceDialogQueueEnabled', - true - ); - D.instanceCreated = false; - D.lastSelectedGroupId = ''; - D.selectedGroupRoles = []; - D.groupRef = {}; - D.roleIds = []; - D.strict = false; - D.shortName = ''; - D.secureOrShortName = ''; - API.getGroupPermissions({ userId: API.currentUser.id }); - this.buildInstance(); - this.buildLegacyInstance(); - this.updateNewInstanceDialog(); - D.visible = true; - }; - - $app.methods.makeHome = function (tag) { - this.$confirm('Continue? Make Home', 'Confirm', { - confirmButtonText: 'Confirm', - cancelButtonText: 'Cancel', - type: 'info', - callback: (action) => { - if (action !== 'confirm') { - return; - } - API.saveCurrentUser({ - homeLocation: tag - }).then((args) => { - this.$message({ - message: 'Home world updated', - type: 'success' - }); - return args; - }); - } - }); - }; - - // #endregion - // #region | App: Launch Options Dialog - - $app.data.launchOptionsDialog = { - visible: false, - launchArguments: await configRepository.getString('launchArguments'), - vrcLaunchPathOverride: await configRepository.getString( - 'vrcLaunchPathOverride' - ) - }; - - API.$on('LOGIN', async function () { - var D = $app.launchOptionsDialog; - if ( - D.vrcLaunchPathOverride === null || - D.vrcLaunchPathOverride === 'null' - ) { - D.vrcLaunchPathOverride = ''; - await configRepository.setString( - 'vrcLaunchPathOverride', - D.vrcLaunchPathOverride - ); - } - }); - - API.$on('LOGOUT', function () { - $app.launchOptionsDialog.visible = false; - }); - - $app.methods.updateLaunchOptions = function () { - var D = this.launchOptionsDialog; - D.launchArguments = String(D.launchArguments) - .replace(/\s+/g, ' ') - .trim(); - configRepository.setString('launchArguments', D.launchArguments); - if ( - D.vrcLaunchPathOverride && - D.vrcLaunchPathOverride.endsWith('.exe') && - !D.vrcLaunchPathOverride.endsWith('launch.exe') - ) { - this.$message({ - message: - 'Invalid path, you must enter VRChat folder or launch.exe', - type: 'error' - }); - return; - } - configRepository.setString( - 'vrcLaunchPathOverride', - D.vrcLaunchPathOverride + this.sidebarSortMethods = [ + this.sidebarSortMethod1, + this.sidebarSortMethod2, + this.sidebarSortMethod3 + ]; + await configRepository.setString( + 'VRCX_sidebarSortMethods', + JSON.stringify(this.sidebarSortMethods) ); - this.$message({ - message: 'Updated launch options', - type: 'success' - }); - D.visible = false; - }; - - $app.methods.showLaunchOptions = function () { - this.$nextTick(() => adjustDialogZ(this.$refs.launchOptionsDialog.$el)); - var D = this.launchOptionsDialog; - D.visible = true; - }; - - // #endregion - // #region | App: Set World Tags Dialog - - $app.data.setWorldTagsDialog = { - visible: false, - authorTags: [], - contentTags: [], - debugAllowed: false, - avatarScalingDisabled: false, - focusViewDisabled: false, - stickersDisabled: false, - contentHorror: false, - contentGore: false, - contentViolence: false, - contentAdult: false, - contentSex: false - }; - - $app.methods.showSetWorldTagsDialog = function () { - this.$nextTick(() => adjustDialogZ(this.$refs.setWorldTagsDialog.$el)); - var D = this.setWorldTagsDialog; - D.visible = true; - D.debugAllowed = false; - D.avatarScalingDisabled = false; - D.focusViewDisabled = false; - D.stickersDisabled = false; - D.contentHorror = false; - D.contentGore = false; - D.contentViolence = false; - D.contentAdult = false; - D.contentSex = false; - var oldTags = this.worldDialog.ref.tags; - var authorTags = []; - var contentTags = []; - oldTags.forEach((tag) => { - if (tag.startsWith('author_tag_')) { - authorTags.unshift(tag.substring(11)); - } - if (tag.startsWith('content_')) { - contentTags.unshift(tag.substring(8)); - } - switch (tag) { - case 'content_horror': - D.contentHorror = true; - break; - case 'content_gore': - D.contentGore = true; - break; - case 'content_violence': - D.contentViolence = true; - break; - case 'content_adult': - D.contentAdult = true; - break; - case 'content_sex': - D.contentSex = true; - break; - - case 'debug_allowed': - D.debugAllowed = true; - break; - case 'feature_avatar_scaling_disabled': - D.avatarScalingDisabled = true; - break; - case 'feature_focus_view_disabled': - D.focusViewDisabled = true; - break; - case 'feature_stickers_disabled': - D.stickersDisabled = true; - break; - } - }); - D.authorTags = authorTags.toString(); - D.contentTags = contentTags.toString(); - }; - - $app.methods.saveSetWorldTagsDialog = function () { - var D = this.setWorldTagsDialog; - var authorTags = D.authorTags.trim().split(','); - var contentTags = D.contentTags.trim().split(','); - var tags = []; - authorTags.forEach((tag) => { - if (tag) { - tags.unshift(`author_tag_${tag}`); - } - }); - // add back custom tags - contentTags.forEach((tag) => { - switch (tag) { - case 'horror': - case 'gore': - case 'violence': - case 'adult': - case 'sex': - case '': - break; - default: - tags.unshift(`content_${tag}`); - break; - } - }); - if (D.contentHorror) { - tags.unshift('content_horror'); - } - if (D.contentGore) { - tags.unshift('content_gore'); - } - if (D.contentViolence) { - tags.unshift('content_violence'); - } - if (D.contentAdult) { - tags.unshift('content_adult'); - } - if (D.contentSex) { - tags.unshift('content_sex'); - } - if (D.debugAllowed) { - tags.unshift('debug_allowed'); - } - if (D.avatarScalingDisabled) { - tags.unshift('feature_avatar_scaling_disabled'); - } - if (D.focusViewDisabled) { - tags.unshift('feature_focus_view_disabled'); - } - if (D.stickersDisabled) { - tags.unshift('feature_stickers_disabled'); - } - API.saveWorld({ - id: this.worldDialog.id, - tags - }).then((args) => { - this.$message({ - message: 'Tags updated', - type: 'success' - }); - D.visible = false; - if ( - this.worldDialog.visible && - this.worldDialog.id === args.json.id - ) { - this.showWorldDialog(args.json.id); - } - return args; - }); - }; - - // #endregion - // #region | App: Set Avatar Tags Dialog - - $app.data.setAvatarTagsDialog = { - visible: false, - loading: false, - ownAvatars: [], - selectedCount: 0, - forceUpdate: 0, - selectedTags: [], - selectedTagsCsv: '', - contentHorror: false, - contentGore: false, - contentViolence: false, - contentAdult: false, - contentSex: false + 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 + ); - $app.methods.showSetAvatarTagsDialog = function (avatarId) { - this.$nextTick(() => adjustDialogZ(this.$refs.setAvatarTagsDialog.$el)); - var D = this.setAvatarTagsDialog; - D.visible = true; - D.loading = false; - D.ownAvatars = []; - D.forceUpdate = 0; - D.selectedTags = []; - D.selectedTagsCsv = ''; - D.contentHorror = false; - D.contentGore = false; - D.contentViolence = false; - D.contentAdult = false; - D.contentSex = false; - var oldTags = this.avatarDialog.ref.tags; - oldTags.forEach((tag) => { - switch (tag) { - case 'content_horror': - D.contentHorror = true; - break; - case 'content_gore': - D.contentGore = true; - break; - case 'content_violence': - D.contentViolence = true; - break; - case 'content_adult': - D.contentAdult = true; - break; - case 'content_sex': - D.contentSex = true; - break; - default: - if (tag.startsWith('content_')) { - D.selectedTags.push(tag.substring(8)); - } - break; - } - }); - for (var ref of API.cachedAvatars.values()) { - if (ref.authorId === API.currentUser.id) { - ref.$selected = false; - ref.$tagString = ''; - if (avatarId === ref.id) { - ref.$selected = true; - var conentTags = []; - ref.tags.forEach((tag) => { - if (tag.startsWith('content_')) { - conentTags.push(tag.substring(8)); - } - }); - for (var i = 0; i < conentTags.length; ++i) { - var tag = conentTags[i]; - if (i < conentTags.length - 1) { - ref.$tagString += `${tag}, `; - } else { - ref.$tagString += tag; - } - } - } - D.ownAvatars.push(ref); - } + // 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' } - this.updateAvatarTagsSelection(); - this.updateSelectedAvatarTags(); }; + $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.methods.updateSelectedAvatarTags = function () { - var D = this.setAvatarTagsDialog; - if (D.contentHorror) { - if (!D.selectedTags.includes('content_horror')) { - D.selectedTags.push('content_horror'); - } - } else if (D.selectedTags.includes('content_horror')) { - D.selectedTags.splice(D.selectedTags.indexOf('content_horror'), 1); - } - if (D.contentGore) { - if (!D.selectedTags.includes('content_gore')) { - D.selectedTags.push('content_gore'); - } - } else if (D.selectedTags.includes('content_gore')) { - D.selectedTags.splice(D.selectedTags.indexOf('content_gore'), 1); + $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 () { + if (typeof API.currentUser?.id === 'undefined') { + return; } - if (D.contentViolence) { - if (!D.selectedTags.includes('content_violence')) { - D.selectedTags.push('content_violence'); - } - } else if (D.selectedTags.includes('content_violence')) { - D.selectedTags.splice( - D.selectedTags.indexOf('content_violence'), - 1 - ); + 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); + }); } - if (D.contentAdult) { - if (!D.selectedTags.includes('content_adult')) { - D.selectedTags.push('content_adult'); - } - } else if (D.selectedTags.includes('content_adult')) { - D.selectedTags.splice(D.selectedTags.indexOf('content_adult'), 1); + 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 = ''; } - if (D.contentSex) { - if (!D.selectedTags.includes('content_sex')) { - D.selectedTags.push('content_sex'); - } - } else if (D.selectedTags.includes('content_sex')) { - D.selectedTags.splice(D.selectedTags.indexOf('content_sex'), 1); + 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; } `; } - - D.selectedTagsCsv = D.selectedTags.join(',').replace(/content_/g, ''); + style.innerHTML = newCSS; + document.getElementsByTagName('head')[0].appendChild(style); }; + await $app.methods.updatetrustColorClasses(); - $app.methods.updateInputAvatarTags = function () { - var D = this.setAvatarTagsDialog; - D.contentHorror = false; - D.contentGore = false; - D.contentViolence = false; - D.contentAdult = false; - D.contentSex = false; - var tags = D.selectedTagsCsv.split(','); - D.selectedTags = []; - for (var tag of tags) { - switch (tag) { - case 'horror': - D.contentHorror = true; - break; - case 'gore': - D.contentGore = true; - break; - case 'violence': - D.contentViolence = true; - break; - case 'adult': - D.contentAdult = true; - break; - case 'sex': - D.contentSex = true; - break; - } - if (!D.selectedTags.includes(`content_${tag}`)) { - D.selectedTags.push(`content_${tag}`); - } - } + $app.data.notificationPosition = await configRepository.getString( + 'VRCX_notificationPosition', + 'topCenter' + ); + $app.methods.changeNotificationPosition = async function () { + await configRepository.setString( + 'VRCX_notificationPosition', + this.notificationPosition + ); + this.updateVRConfigVars(); }; - $app.data.avatarContentTags = [ - 'content_horror', - 'content_gore', - 'content_violence', - 'content_adult', - 'content_sex' - ]; + $app.data.youTubeApi = await configRepository.getBool( + 'VRCX_youtubeAPI', + false + ); + $app.data.youTubeApiKey = await configRepository.getString( + 'VRCX_youtubeAPIKey', + '' + ); - $app.methods.saveSetAvatarTagsDialog = async function () { - var D = this.setAvatarTagsDialog; - if (D.loading) { - return; + $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.methods.updateVRConfigVars = function () { + var notificationTheme = 'relax'; + if (this.isDarkMode) { + notificationTheme = 'sunset'; } - D.loading = true; - try { - for (var i = D.ownAvatars.length - 1; i >= 0; --i) { - var ref = D.ownAvatars[i]; - if (!D.visible) { - break; - } - if (!ref.$selected) { - continue; - } - var tags = [...D.selectedTags]; - for (var tag of ref.tags) { - if (!tag.startsWith('content_')) { - tags.push(tag); - } - } - await API.saveAvatar({ - id: ref.id, - tags - }); - D.selectedCount--; - } - } catch (err) { - this.$message({ - message: 'Error saving avatar tags', - type: 'error' - }); - } finally { - D.loading = false; - D.visible = false; + 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 = $utils.parseLocation(location); + if (rpcWorlds.includes(L.worldId)) { + return true; } + return false; }; - $app.methods.updateAvatarTagsSelection = function () { - var D = this.setAvatarTagsDialog; - D.selectedCount = 0; - for (var ref of D.ownAvatars) { - if (ref.$selected) { - D.selectedCount++; - } - ref.$tagString = ''; - var conentTags = []; - ref.tags.forEach((tag) => { - if (tag.startsWith('content_')) { - conentTags.push(tag.substring(8)); - } - }); - for (var i = 0; i < conentTags.length; ++i) { - var tag = conentTags[i]; - if (i < conentTags.length - 1) { - ref.$tagString += `${tag}, `; - } else { - ref.$tagString += tag; + $app.methods.updateVRLastLocation = function () { + var progressPie = false; + if (this.progressPie) { + progressPie = true; + if (this.progressPieFilter) { + if (!this.isRpcWorld(this.lastLocation.location)) { + progressPie = false; } } } - this.setAvatarTagsDialog.forceUpdate++; - }; - - $app.methods.setAvatarTagsSelectToggle = function () { - var D = this.setAvatarTagsDialog; - var allSelected = D.ownAvatars.length === D.selectedCount; - for (var ref of D.ownAvatars) { - ref.$selected = !allSelected; + var onlineFor = ''; + if (!this.hideUptimeFromFeed) { + onlineFor = API.currentUser.$online_for; } - this.updateAvatarTagsSelection(); + 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); }; - // #endregion - // #region | App: Notification position - - $app.data.notificationPositionDialog = { - visible: false + $app.methods.vrInit = function () { + this.updateVRConfigVars(); + this.updateVRLastLocation(); + this.updateVrNowPlaying(); + this.updateSharedFeed(true); + this.onlineFriendCount = 0; + this.updateOnlineFriendCoutner(); }; - $app.methods.showNotificationPositionDialog = function () { - this.$nextTick(() => - adjustDialogZ(this.$refs.notificationPositionDialog.$el) - ); - this.notificationPositionDialog.visible = true; - }; + API.$on('LOGIN', function () { + $app.currentUserTreeData = []; + $app.pastDisplayNameTable.data = []; + }); - // #endregion - // #region | App: Noty feed filters + API.$on('USER:CURRENT', function (args) { + if (args.ref.pastDisplayNames) { + $app.pastDisplayNameTable.data = args.ref.pastDisplayNames; + } + }); - $app.data.notyFeedFiltersDialog = { - visible: false - }; + API.$on('VISITS', function (args) { + $app.visits = args.json; + }); - $app.methods.showNotyFeedFiltersDialog = function () { - this.$nextTick(() => - adjustDialogZ(this.$refs.notyFeedFiltersDialog.$el) - ); - this.notyFeedFiltersDialog.visible = true; + $app.methods.resetHome = function () { + this.$confirm('Continue? Reset Home', 'Confirm', { + confirmButtonText: 'Confirm', + cancelButtonText: 'Cancel', + type: 'info', + callback: (action) => { + if (action === 'confirm') { + API.saveCurrentUser({ + homeLocation: '' + }).then((args) => { + this.$message({ + message: 'Home world has been reset', + type: 'success' + }); + return args; + }); + } + } + }); }; - // #endregion - // #region | App: Wrist feed filters + $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.data.wristFeedFiltersDialog = { - visible: false + $app.methods.getTTSVoiceName = function () { + var 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.showWristFeedFiltersDialog = function () { - this.$nextTick(() => - adjustDialogZ(this.$refs.wristFeedFiltersDialog.$el) + $app.methods.changeTTSVoice = async function (index) { + this.notificationTTSVoice = index; + await configRepository.setString( + 'VRCX_notificationTTSVoice', + this.notificationTTSVoice ); - this.wristFeedFiltersDialog.visible = true; + var voices = speechSynthesis.getVoices(); + if (voices.length === 0) { + return; + } + var voiceName = voices[index].name; + speechSynthesis.cancel(); + this.speak(voiceName); }; - // #endregion - // #region | App: Launch Dialog - - $app.data.launchDialog = { - visible: false, - loading: false, - desktop: await configRepository.getBool('launchAsDesktop'), - tag: '', - location: '', - url: '', - shortName: '', - shortUrl: '', - secureOrShortName: '' + $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.saveLaunchDialog = async function () { - await configRepository.setBool( - 'launchAsDesktop', - this.launchDialog.desktop - ); + $app.methods.refreshConfigTreeData = function () { + this.configTreeData = $utils.buildTreeData(API.cachedConfig); }; - API.$on('LOGOUT', function () { - $app.launchDialog.visible = false; - }); + $app.methods.refreshCurrentUserTreeData = function () { + this.currentUserTreeData = $utils.buildTreeData(API.currentUser); + }; - API.$on('INSTANCE:SHORTNAME', function (args) { - if (!args.json) { - return; - } - var shortName = args.json.shortName; - var secureOrShortName = args.json.shortName || args.json.secureName; - var location = `${args.instance.worldId}:${args.instance.instanceId}`; - if (location === $app.launchDialog.tag) { - var L = this.parseLocation(location); - L.shortName = shortName; - $app.launchDialog.shortName = shortName; - $app.launchDialog.secureOrShortName = secureOrShortName; - if (shortName) { - $app.launchDialog.shortUrl = `https://vrch.at/${shortName}`; + $app.methods.directAccessPaste = function () { + AppApi.GetClipboard().then((clipboard) => { + if (!this.directAccessParse(clipboard.trim())) { + this.promptOmniDirectDialog(); } - $app.launchDialog.url = $app.getLaunchURL(L); - } - if (location === $app.newInstanceDialog.location) { - $app.newInstanceDialog.shortName = shortName; - $app.newInstanceDialog.secureOrShortName = secureOrShortName; - $app.updateNewInstanceDialog(true); - } - }); - - $app.methods.addShortNameToFullUrl = function (input, shortName) { - if (input.trim().length === 0 || !shortName) { - return input; - } - var url = new URL(input); - var urlParams = new URLSearchParams(url.search); - urlParams.set('shortName', shortName); - url.search = urlParams.toString(); - return url.toString(); + }); }; - $app.methods.showLaunchDialog = function (tag, shortName) { - if (!this.isRealInstance(tag)) { - return; - } - this.$nextTick(() => adjustDialogZ(this.$refs.launchDialog.$el)); - var D = this.launchDialog; - D.tag = tag; - D.secureOrShortName = shortName; - D.shortUrl = ''; - D.shortName = shortName; - var L = API.parseLocation(tag); - L.shortName = shortName; - if (shortName) { - D.shortUrl = `https://vrch.at/${shortName}`; - } - if (L.instanceId) { - D.location = `${L.worldId}:${L.instanceId}`; - } else { - D.location = L.worldId; + $app.methods.directAccessWorld = function (textBoxInput) { + var input = textBoxInput; + if (input.startsWith('/home/')) { + input = `https://vrchat.com${input}`; } - D.url = this.getLaunchURL(L); - D.visible = true; - if (!shortName) { - API.getInstanceShortName({ - worldId: L.worldId, - instanceId: L.instanceId - }); + 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; + if (urlPath.substring(5, 12) === '/world/') { + var worldId = urlPath.substring(12); + 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.getLaunchURL = function (instance) { - var L = instance; - if (L.instanceId) { - if (L.shortName) { - return `https://vrchat.com/home/launch?worldId=${encodeURIComponent( - L.worldId - )}&instanceId=${encodeURIComponent( - L.instanceId - )}&shortName=${encodeURIComponent(L.shortName)}`; + $app.methods.verifyShortName = function (location, shortName) { + return API.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 `https://vrchat.com/home/launch?worldId=${encodeURIComponent( - L.worldId - )}&instanceId=${encodeURIComponent(L.instanceId)}`; - } - return `https://vrchat.com/home/launch?worldId=${encodeURIComponent( - L.worldId - )}`; + return args; + }); }; - $app.methods.launchGame = async function ( - location, - shortName, - desktopMode - ) { - var D = this.launchDialog; - var L = API.parseLocation(location); - var args = []; - if ( - shortName && - L.instanceType !== 'public' && - L.groupAccessType !== 'public' - ) { - args.push(`vrchat://launch?id=${location}&shortName=${shortName}`); - } else { - // fetch shortName - var newShortName = ''; - var response = await API.getInstanceShortName({ - worldId: L.worldId, - instanceId: L.instanceId - }); - if (response.json) { - if (response.json.shortName) { - newShortName = response.json.shortName; - } else { - newShortName = response.json.secureName; + $app.methods.showGroupDialogShortCode = function (shortCode) { + API.groupStrictsearch({ query: shortCode }).then((args) => { + for (var group of args.json) { + if (`${group.shortCode}.${group.discriminator}` === shortCode) { + this.showGroupDialog(group.id); } } - if (newShortName) { - args.push( - `vrchat://launch?id=${location}&shortName=${newShortName}` - ); - } else { - args.push(`vrchat://launch?id=${location}`); - } + return args; + }); + }; + + $app.methods.directAccessParse = function (input) { + if (!input) { + return false; } - var { launchArguments, vrcLaunchPathOverride } = - this.launchOptionsDialog; - if (launchArguments) { - args.push(launchArguments); + if (this.directAccessWorld(input)) { + return true; } - if (desktopMode) { - args.push('--no-vr'); + if (input.startsWith('https://vrchat.')) { + var url = new URL(input); + var urlPath = url.pathname; + if (urlPath.substring(5, 11) === '/user/') { + var userId = urlPath.substring(11); + this.showUserDialog(userId); + return true; + } else if (urlPath.substring(5, 13) === '/avatar/') { + var avatarId = urlPath.substring(13); + this.showAvatarDialog(avatarId); + return true; + } else if (urlPath.substring(5, 12) === '/group/') { + var groupId = urlPath.substring(12); + 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; } - if (vrcLaunchPathOverride) { - 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' - }); + 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; } - console.log('Launch Game', args.join(' '), desktopMode); - D.visible = false; }; // #endregion - // #region | App: Copy To Clipboard + // #region | App: User Dialog - $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.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, - $app.methods.copyInstanceMessage = function (input) { - this.copyToClipboard(input); - this.$message({ - message: 'Instance copied to clipboard', - type: 'success' - }); - return input; + $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' + }, + 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: '' + }, + joinCount: 0, + timeSpent: 0, + lastSeen: '', + avatarModeration: 0, + previousDisplayNames: [], + dateFriended: '', + unFriended: false, + dateFriendedInfo: [] }; - $app.methods.copyInstanceUrl = async function (location) { - var L = API.parseLocation(location); - var args = await API.getInstanceShortName({ - worldId: L.worldId, - instanceId: L.instanceId - }); - if (args.json && args.json.shortName) { - L.shortName = args.json.shortName; + $app.methods.setUserDialogWorldSorting = async function (sortOrder) { + var D = this.userDialog; + if (D.worldSorting === sortOrder) { + return; } - var newUrl = this.getLaunchURL(L); - this.copyInstanceMessage(newUrl); + D.worldSorting = sortOrder; + await this.refreshUserDialogWorlds(); }; - $app.methods.copyAvatarId = function (avatarId) { - this.$message({ - message: 'Avatar ID copied to clipboard', - type: 'success' - }); - this.copyToClipboard(avatarId); + $app.methods.setUserDialogWorldOrder = async function (order) { + var D = this.userDialog; + if (D.worldOrder === order) { + return; + } + D.worldOrder = order; + await this.refreshUserDialogWorlds(); }; - $app.methods.copyAvatarUrl = function (avatarId) { - this.$message({ - message: 'Avatar URL copied to clipboard', - type: 'success' - }); - this.copyToClipboard(`https://vrchat.com/home/avatar/${avatarId}`); + $app.methods.getFaviconUrl = function (resource) { + try { + var url = new URL(resource); + return `https://icons.duckduckgo.com/ip2/${url.host}.ico`; + } catch (err) { + return ''; + } }; - $app.methods.copyWorldId = function (worldId) { - this.$message({ - message: 'World ID copied to clipboard', - type: 'success' - }); - this.copyToClipboard(worldId); - }; + API.$on('LOGOUT', function () { + $app.userDialog.visible = false; + }); - $app.methods.copyWorldUrl = function (worldId) { - this.$message({ - message: 'World URL copied to clipboard', - type: 'success' - }); - this.copyToClipboard(`https://vrchat.com/home/world/${worldId}`); - }; + 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; + } + }); - $app.methods.copyWorldName = function (worldName) { - this.$message({ - message: 'World name copied to clipboard', - type: 'success' - }); - this.copyToClipboard(worldName); - }; + API.$on('WORLD', function (args) { + var D = $app.userDialog; + if (D.visible === false || D.$location.worldId !== args.ref.id) { + return; + } + $app.applyUserDialogLocation(); + }); - $app.methods.copyUserId = function (userId) { - this.$message({ - message: 'User ID copied to clipboard', - type: 'success' - }); - this.copyToClipboard(userId); - }; + 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; + }); - $app.methods.copyUserURL = function (userId) { - this.$message({ - message: 'User URL copied to clipboard', - type: 'success' - }); - this.copyToClipboard(`https://vrchat.com/home/user/${userId}`); - }; + API.$on('FRIEND:REQUEST', function (args) { + var D = $app.userDialog; + if (D.visible === false || D.id !== args.params.userId) { + return; + } + if (args.json.success) { + D.isFriend = true; + } else { + D.outgoingRequest = true; + } + }); - $app.methods.copyUserDisplayName = function (displayName) { - this.$message({ - message: 'User DisplayName copied to clipboard', - type: 'success' - }); - this.copyToClipboard(displayName); - }; + API.$on('FRIEND:REQUEST:CANCEL', function (args) { + var D = $app.userDialog; + if (D.visible === false || D.id !== args.params.userId) { + return; + } + D.outgoingRequest = false; + }); - $app.methods.copyGroupId = function (groupId) { - this.$message({ - message: 'Group ID copied to clipboard', - type: 'success' - }); - this.copyToClipboard(groupId); - }; + 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; + }); - $app.methods.copyGroupUrl = function (groupUrl) { - this.$message({ - message: 'Group URL copied to clipboard', - type: 'success' - }); - this.copyToClipboard(groupUrl); - }; + 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; + }); - $app.methods.copyImageUrl = function (imageUrl) { - this.$message({ - message: 'ImageUrl copied to clipboard', - type: 'success' - }); - this.copyToClipboard(imageUrl); - }; + 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; + }); - $app.methods.copyText = function (text) { - this.$message({ - message: 'Text copied to clipboard', - type: 'success' - }); - this.copyToClipboard(text); - }; + API.$on('FRIEND:DELETE', function (args) { + var D = $app.userDialog; + if (D.visible === false || D.id !== args.params.userId) { + return; + } + D.isFriend = false; + }); - $app.methods.copyLink = function (text) { - this.$message({ - message: 'Link copied to clipboard', + 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: 'User moderated', type: 'success' }); - this.copyToClipboard(text); - }; + }); - // #endregion - // #region | App: VRCPlus Icons + 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('LOGIN', function () { - $app.VRCPlusIconsTable = []; + 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; }); - $app.methods.refreshVRCPlusIconsTable = function () { - this.galleryDialogIconsLoading = true; - var params = { - n: 100, - tag: 'icon' - }; - API.getFileList(params); - }; + API.$on('FAVORITE:@DELETE', function (args) { + var D = $app.userDialog; + if (D.visible === false || D.id !== args.ref.favoriteId) { + return; + } + D.isFavorite = false; + }); - API.getFileList = function (params) { - return this.call('files', { - method: 'GET', - params - }).then((json) => { - var args = { - json, - params - }; - this.$emit('FILES:LIST', args); - return args; + $app.methods.showUserDialog = function (userId) { + if (!userId) { + return; + } + this.$nextTick(() => $app.adjustDialogZ(this.$refs.userDialog.$el)); + var 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; + var ref = this.friends.get(userId); + if (ref) { + ref.memo = String(memo.memo || ''); + if (memo.memo) { + var array = memo.memo.split('\n'); + ref.$nickName = array[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.representedGroup = { + bannerUrl: '', + description: '', + discriminator: '', + groupId: '', + iconUrl: '', + isRepresenting: false, + memberCount: 0, + memberVisibility: '', + name: '', + ownerId: '', + privacy: '', + shortCode: '' + }; + 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); + API.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) { + D.loading = false; + 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 (var 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 (this.$refs.userDialogTabs.currentName === '0') { + this.userDialogLastActiveTab = $t( + 'dialog.user.info.header' + ); + } else if (this.$refs.userDialogTabs.currentName === '1') { + this.userDialogLastActiveTab = $t( + 'dialog.user.groups.header' + ); + if (this.userDialogLastGroup !== userId) { + this.userDialogLastGroup = userId; + this.getUserGroups(userId); + } + } else if (this.$refs.userDialogTabs.currentName === '2') { + this.userDialogLastActiveTab = $t( + 'dialog.user.worlds.header' + ); + this.setUserDialogWorlds(userId); + if (this.userDialogLastWorld !== userId) { + this.userDialogLastWorld = userId; + this.refreshUserDialogWorlds(); + } + } else if (this.$refs.userDialogTabs.currentName === '3') { + this.userDialogLastActiveTab = $t( + 'dialog.user.favorite_worlds.header' + ); + if (this.userDialogLastFavoriteWorld !== userId) { + this.userDialogLastFavoriteWorld = userId; + this.getUserFavoriteWorlds(userId); + } + } else if (this.$refs.userDialogTabs.currentName === '4') { + this.userDialogLastActiveTab = $t( + 'dialog.user.avatars.header' + ); + this.setUserDialogAvatars(userId); + this.userDialogLastAvatar = userId; + if ( + userId === API.currentUser.id && + D.avatars.length === 0 + ) { + this.refreshUserDialogAvatars(); + } + this.setUserDialogAvatarsRemote(userId); + } else if (this.$refs.userDialogTabs.currentName === '5') { + this.userDialogLastActiveTab = $t( + 'dialog.user.json.header' + ); + this.refreshUserDialogTreeData(); + } + if (args.cache) { + API.getUser(args.params); + } + var inCurrentWorld = false; + if (this.lastLocation.playerList.has(D.ref.displayName)) { + inCurrentWorld = true; + } + if (userId !== API.currentUser.id) { + database + .getUserStats(D.ref, inCurrentWorld) + .then((ref1) => { + if (ref1.userId === D.id) { + D.lastSeen = ref1.created_at; + D.joinCount = ref1.joinCount; + D.timeSpent = ref1.timeSpent; + } + var 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); + } + } + }); + var 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.created_at; + D.joinCount = ref1.joinCount; + D.timeSpent = ref1.timeSpent; + } + }); + } + API.getRepresentedGroup({ userId }).then((args1) => { + D.representedGroup = args1.json; + return args1; + }); + } + return args; + }); + this.showUserDialogHistory.delete(userId); + this.showUserDialogHistory.add(userId); }; - API.$on('FILES:LIST', function (args) { - if (args.params.tag === 'icon') { - $app.VRCPlusIconsTable = args.json.reverse(); - $app.galleryDialogIconsLoading = false; - } - }); - - $app.methods.setVRCPlusIcon = function (fileId) { - if (!API.currentUser.$isVRCPlus) { - this.$message({ - message: 'VRCPlus required', - type: 'error' - }); + $app.methods.applyUserDialogLocation = function (updateInstanceOccupants) { + var D = this.userDialog; + if (!D.visible) { return; } - var userIcon = ''; - if (fileId) { - userIcon = `${API.endpointDomain}/file/${fileId}/1`; + var L = $utils.parseLocation(D.ref.$location.tag); + if (updateInstanceOccupants && this.isRealInstance(L.tag)) { + API.getInstance({ + worldId: L.worldId, + instanceId: L.instanceId + }); } - if (userIcon === API.currentUser.userIcon) { - return; + D.$location = L; + if (L.userId) { + var ref = API.cachedUsers.get(L.userId); + if (typeof ref === 'undefined') { + API.getUser({ + userId: L.userId + }).then((args) => { + Vue.set(L, 'user', args.ref); + return args; + }); + } else { + L.user = ref; + } } - API.saveCurrentUser({ - userIcon - }).then((args) => { - this.$message({ - message: 'Icon changed', - type: 'success' - }); - return args; - }); - }; - - $app.methods.deleteVRCPlusIcon = function (fileId) { - API.deleteFile(fileId).then((args) => { - API.$emit('VRCPLUSICON:DELETE', args); - return args; - }); - }; - - API.$on('VRCPLUSICON:DELETE', function (args) { - var array = $app.VRCPlusIconsTable; - var { length } = array; - for (var i = 0; i < length; ++i) { - if (args.fileId === array[i].id) { - array.splice(i, 1); - break; + 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 } } - }); - - API.deleteFile = function (fileId) { - return this.call(`file/${fileId}`, { - method: 'DELETE' - }).then((json) => { - var args = { - json, - fileId - }; - return args; - }); - }; - - API.deleteFileVersion = function (params) { - return this.call(`file/${params.fileId}/${params.version}`, { - method: 'DELETE' - }).then((json) => { - var args = { - json, - params - }; - return args; - }); - }; - - $app.methods.compareCurrentVRCPlusIcon = function (userIcon) { - var currentUserIcon = extractFileId(API.currentUser.userIcon); - if (userIcon === currentUserIcon) { - return true; + // 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.displayName === user.displayName; + }); + if (addUser) { + var ref = API.cachedUsers.get(friend.userId); + if (typeof ref !== 'undefined') { + users.push(ref); + } + } + } + friendCount = users.length - 1; } - return false; - }; - - $app.methods.onFileChangeVRCPlusIcon = function (e) { - var clearFile = function () { - if (document.querySelector('#VRCPlusIconUploadButton')) { - document.querySelector('#VRCPlusIconUploadButton').value = ''; + 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); + } + } } - }; - var files = e.target.files || e.dataTransfer.files; - if (!files.length) { - return; + friendCount = users.length; } - if (files[0].size >= 100000000) { - // 100MB - $app.$message({ - message: 'File size too large', - type: 'error' - }); - clearFile(); - return; + 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 (!this.isRealInstance(L.tag)) { + D.instance = { + id: L.instanceId, + tag: L.tag, + $location: L, + friendCount: 0, + users: [], + shortName: '', + ref: {} + }; } - if (!files[0].type.match(/image.*/)) { - $app.$message({ - message: "File isn't an image", - type: 'error' - }); - clearFile(); - return; + var instanceRef = API.cachedInstances.get(L.tag); + if (typeof instanceRef !== 'undefined') { + D.instance.ref = instanceRef; } - var r = new FileReader(); - r.onload = function () { - var base64Body = btoa(r.result); - API.uploadVRCPlusIcon(base64Body).then((args) => { - $app.$message({ - message: 'Icon uploaded', - type: 'success' - }); - return args; - }); - }; - r.readAsBinaryString(files[0]); - clearFile(); + D.instance.friendCount = friendCount; + this.updateTimers(); }; - $app.methods.displayVRCPlusIconUpload = function () { - document.getElementById('VRCPlusIconUploadButton').click(); - }; + // #endregion + // #region | App: player list - API.uploadVRCPlusIcon = function (imageData) { - var params = { - tag: 'icon' - }; - return this.call('file/image', { - uploadImage: true, - postData: JSON.stringify(params), - imageData - }).then((json) => { - var args = { - json, - params - }; - this.$emit('VRCPLUSICON:ADD', args); - return args; - }); - }; + API.$on('LOGIN', function () { + $app.currentInstanceUserList.data = []; + }); - API.$on('VRCPLUSICON:ADD', function (args) { - if (Object.keys($app.VRCPlusIconsTable).length !== 0) { - $app.VRCPlusIconsTable.unshift(args.json); + API.$on('USER:APPLY', function (ref) { + // add user ref to playerList, friendList, photonLobby, photonLobbyCurrent + if ($app.lastLocation.playerList.has(ref.displayName)) { + var playerListRef = $app.lastLocation.playerList.get( + ref.displayName + ); + if (!playerListRef.userId) { + playerListRef.userId = ref.id; + $app.lastLocation.playerList.set( + ref.displayName, + playerListRef + ); + if ($app.lastLocation.friendList.has(ref.displayName)) { + $app.lastLocation.friendList.set( + ref.displayName, + playerListRef + ); + } + } + // add/remove friends from lastLocation.friendList + if ( + !$app.lastLocation.friendList.has(ref.displayName) && + $app.friends.has(ref.id) + ) { + var userMap = { + displayName: ref.displayName, + userId: ref.id, + joinTime: playerListRef.joinTime + }; + $app.lastLocation.friendList.set(ref.displayName, userMap); + } + if ( + $app.lastLocation.friendList.has(ref.displayName) && + !$app.friends.has(ref.id) + ) { + $app.lastLocation.friendList.delete(ref.displayName); + } + $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.uploadImage = ''; - - $app.methods.inviteImageUpload = function (e) { - var files = e.target.files || e.dataTransfer.files; - if (!files.length) { + $app.data.updatePlayerListTimer = null; + $app.data.updatePlayerListPending = false; + $app.methods.getCurrentInstanceUserList = function () { + if (!this.friendLogInitStatus) { return; } - if (files[0].size >= 100000000) { - // 100MB - $app.$message({ - message: 'File size too large', - type: 'error' - }); - this.clearInviteImageUpload(); - return; + if (this.updatePlayerListTimer) { + this.updatePlayerListPending = true; + } else { + this.updatePlayerListExecute(); + this.updatePlayerListTimer = setTimeout(() => { + if (this.updatePlayerListPending) { + this.updatePlayerListExecute(); + } + this.updatePlayerListTimer = null; + }, 150); } - if (!files[0].type.match(/image.*/)) { - $app.$message({ - message: "File isn't a png", - type: 'error' + }; + + $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; + } + } }); - this.clearInviteImageUpload(); - return; + 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.displayName) + ) { + 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.displayName + ); + 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); + } + } + } } - var r = new FileReader(); - r.onload = function () { - $app.uploadImage = btoa(r.result); - }; - r.readAsBinaryString(files[0]); + this.currentInstanceUserList.data = users; + this.updateTimers(); }; - $app.methods.clearInviteImageUpload = function () { - this.clearImageGallerySelect(); - var buttonList = document.querySelectorAll('.inviteImageUploadButton'); - buttonList.forEach((button) => (button.value = '')); - this.uploadImage = ''; - }; + $app.data.updateInstanceInfo = 0; - $app.methods.userOnlineFor = function (ctx) { - if (ctx.ref.state === 'online' && ctx.ref.$online_for) { - return timeToText(Date.now() - ctx.ref.$online_for); - } else if (ctx.ref.state === 'active' && ctx.ref.$active_for) { - return timeToText(Date.now() - ctx.ref.$active_for); - } else if (ctx.ref.$offline_for) { - return timeToText(Date.now() - ctx.ref.$offline_for); - } - return '-'; + $app.data.currentInstanceWorld = { + ref: {}, + instance: {}, + isPC: false, + isQuest: false, + isIos: false, + avatarScalingDisabled: false, + focusViewDisabled: false, + stickersDisabled: false, + inCache: false, + cacheSize: '', + bundleSizes: [], + lastUpdated: '' }; + $app.data.currentInstanceLocation = {}; - $app.methods.userOnlineForTimestamp = function (ctx) { - if (ctx.ref.state === 'online' && ctx.ref.$online_for) { - return ctx.ref.$online_for; - } else if (ctx.ref.state === 'active' && ctx.ref.$active_for) { - return ctx.ref.$active_for; - } else if (ctx.ref.$offline_for) { - return ctx.ref.$offline_for; + $app.methods.updateCurrentInstanceWorld = function () { + var instanceId = this.lastLocation.location; + if (this.lastLocation.location === 'traveling') { + instanceId = this.lastLocationDestination; } - return 0; - }; - - // #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; - }); - - $app.methods.refreshInviteMessageTable = function (messageType) { - API.refreshInviteMessageTableData(messageType); - }; - - API.refreshInviteMessageTableData = function (messageType) { - return this.call(`message/${this.currentUser.id}/${messageType}`, { - method: 'GET' - }).then((json) => { - var args = { - json, - messageType - }; - this.$emit(`INVITE:${messageType.toUpperCase()}`, args); - return args; - }); - }; - - 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; - }); - - API.editInviteMessage = function (params, messageType, slot) { - return this.call( - `message/${this.currentUser.id}/${messageType}/${slot}`, - { - method: 'PUT', - params - } - ).then((json) => { - var args = { - json, - params, - messageType, - slot + if (!instanceId) { + this.currentInstanceWorld = { + ref: {}, + instance: {}, + isPC: false, + isQuest: false, + isIos: false, + avatarScalingDisabled: false, + focusViewDisabled: false, + stickersDisabled: false, + inCache: false, + cacheSize: '', + bundleSizes: [], + lastUpdated: '' }; - return args; - }); - }; - - // #endregion - // #region | App: Edit Invite Message Dialog - - $app.data.editInviteMessageDialog = { - visible: false, - inviteMessage: {}, - messageType: '', - newMessage: '' - }; - - $app.methods.showEditInviteMessageDialog = function ( - messageType, - inviteMessage - ) { - this.$nextTick(() => - adjustDialogZ(this.$refs.editInviteMessageDialog.$el) - ); - var D = this.editInviteMessageDialog; - D.newMessage = inviteMessage.message; - D.visible = true; - D.inviteMessage = inviteMessage; - D.messageType = messageType; - }; - - $app.methods.saveEditInviteMessage = function () { - var D = this.editInviteMessageDialog; - D.visible = false; - if (D.inviteMessage.message !== D.newMessage) { - var slot = D.inviteMessage.slot; - var messageType = D.messageType; - var params = { - message: D.newMessage + this.currentInstanceLocation = {}; + } else if (instanceId !== this.currentInstanceLocation.tag) { + this.currentInstanceWorld = { + ref: {}, + instance: {}, + isPC: false, + isQuest: false, + isIos: false, + avatarScalingDisabled: false, + focusViewDisabled: false, + stickersDisabled: false, + inCache: false, + cacheSize: '', + bundleSizes: [], + lastUpdated: '' }; - API.editInviteMessage(params, messageType, slot) - .catch((err) => { - throw err; - }) - .then((args) => { - API.$emit(`INVITE:${messageType.toUpperCase()}`, args); - if (args.json[slot].message === D.inviteMessage.message) { - this.$message({ - message: - "VRChat API didn't update message, try again", - type: 'error' - }); - throw new Error( - "VRChat API didn't update message, try again" - ); - } else { - this.$message('Invite message updated'); + var L = $utils.parseLocation(instanceId); + this.currentInstanceLocation = L; + API.getWorld({ + worldId: L.worldId + }).then((args) => { + this.currentInstanceWorld.ref = args.ref; + var { isPC, isQuest, isIos } = this.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'); + this.currentInstanceWorld.stickersDisabled = + args.ref?.tags.includes('feature_stickers_disabled'); + this.checkVRChatCache(args.ref).then((cacheInfo) => { + if (cacheInfo.Item1 > 0) { + this.currentInstanceWorld.inCache = true; + this.currentInstanceWorld.cacheSize = `${( + cacheInfo.Item1 / 1048576 + ).toFixed(2)} MB`; } - return args; }); + this.getBundleDateSize(args.ref).then((bundleSizes) => { + this.currentInstanceWorld.bundleSizes = bundleSizes; + }); + return args; + }); + } else { + API.getCachedWorld({ + worldId: this.currentInstanceLocation.worldId + }).then((args) => { + this.currentInstanceWorld.ref = args.ref; + var { isPC, isQuest, isIos } = this.getAvailablePlatforms( + args.ref.unityPackages + ); + this.currentInstanceWorld.isPC = isPC; + this.currentInstanceWorld.isQuest = isQuest; + this.currentInstanceWorld.isIos = isIos; + this.checkVRChatCache(args.ref).then((cacheInfo) => { + if (cacheInfo.Item1 > 0) { + this.currentInstanceWorld.inCache = true; + this.currentInstanceWorld.cacheSize = `${( + cacheInfo.Item1 / 1048576 + ).toFixed(2)} MB`; + } + }); + }); + } + if (this.isRealInstance(instanceId)) { + var ref = API.cachedInstances.get(instanceId); + if (typeof ref !== 'undefined') { + this.currentInstanceWorld.instance = ref; + } else { + var L = $utils.parseLocation(instanceId); + API.getInstance({ + worldId: L.worldId, + instanceId: L.instanceId + }).then((args) => { + this.currentInstanceWorld.instance = args.ref; + }); + } } }; - $app.methods.cancelEditInviteMessage = function () { - this.editInviteMessageDialog.visible = false; + $app.methods.getAvailablePlatforms = function (unityPackages) { + var isPC = false; + var isQuest = false; + var isIos = false; + if (typeof unityPackages === 'object') { + for (var unityPackage of unityPackages) { + if ( + unityPackage.variant && + unityPackage.variant !== 'standard' && + unityPackage.variant !== 'security' + ) { + continue; + } + if (unityPackage.platform === 'standalonewindows') { + isPC = true; + } else if (unityPackage.platform === 'android') { + isQuest = true; + } else if (unityPackage.platform === 'ios') { + isIos = true; + } + } + } + return { isPC, isQuest, isIos }; }; - // #endregion - // #region | App: Edit and Send Invite Response Message Dialog - - $app.data.editAndSendInviteResponseDialog = { - visible: false, - inviteMessage: {}, - messageType: '', - newMessage: '' + $app.methods.getPlatformInfo = function (unityPackages) { + var pc = {}; + var android = {}; + var ios = {}; + if (typeof unityPackages === 'object') { + for (var unityPackage of unityPackages) { + if ( + unityPackage.variant && + unityPackage.variant !== 'standard' && + unityPackage.variant !== 'security' + ) { + continue; + } + if (unityPackage.platform === 'standalonewindows') { + pc = unityPackage; + } else if (unityPackage.platform === 'android') { + android = unityPackage; + } else if (unityPackage.platform === 'ios') { + ios = unityPackage; + } + } + } + return { pc, android, ios }; }; - $app.methods.showEditAndSendInviteResponseDialog = function ( - messageType, - inviteMessage - ) { - this.$nextTick(() => - adjustDialogZ(this.$refs.editAndSendInviteResponseDialog.$el) - ); - this.editAndSendInviteResponseDialog = { - newMessage: inviteMessage.message, - visible: true, - messageType, - inviteMessage - }; + $app.methods.replaceVrcPackageUrl = function (url) { + if (!url) { + return ''; + } + return url.replace('https://api.vrchat.cloud/', 'https://vrchat.com/'); }; - $app.methods.saveEditAndSendInviteResponse = async function () { - var D = this.editAndSendInviteResponseDialog; - D.visible = false; - var messageType = D.messageType; - var slot = D.inviteMessage.slot; - if (D.inviteMessage.message !== D.newMessage) { - var params = { - message: D.newMessage - }; - await API.editInviteMessage(params, messageType, slot) - .catch((err) => { - throw err; - }) - .then((args) => { - API.$emit(`INVITE:${messageType.toUpperCase()}`, args); - if (args.json[slot].message === D.inviteMessage.message) { - this.$message({ - message: - "VRChat API didn't update message, try again", - type: 'error' - }); - throw new Error( - "VRChat API didn't update message, try again" - ); - } else { - this.$message('Invite message updated'); - } - return args; - }); + $app.methods.selectCurrentInstanceRow = function (val) { + if (val === null) { + return; } - var I = this.sendInviteResponseDialog; - var params = { - responseSlot: slot, - rsvp: true - }; - if ($app.uploadImage) { - API.sendInviteResponsePhoto(params, I.invite.id) - .catch((err) => { - throw err; - }) - .then((args) => { - API.hideNotification({ - notificationId: I.invite.id - }); - this.$message({ - message: 'Invite response message sent', - type: 'success' - }); - this.sendInviteResponseDialogVisible = false; - this.sendInviteRequestResponseDialogVisible = false; - return args; - }); + var ref = val.ref; + if (ref.id) { + this.showUserDialog(ref.id); } else { - API.sendInviteResponse(params, I.invite.id) - .catch((err) => { - throw err; - }) - .then((args) => { - API.hideNotification({ - notificationId: I.invite.id - }); - this.$message({ - message: 'Invite response message sent', - type: 'success' - }); - this.sendInviteResponseDialogVisible = false; - this.sendInviteRequestResponseDialogVisible = false; - return args; - }); + this.lookupUser(ref); } }; - $app.methods.cancelEditAndSendInviteResponse = function () { - this.editAndSendInviteResponseDialog.visible = false; + $app.methods.updateTimers = function () { + for (var $timer of $timers) { + $timer.update(); + } }; - $app.data.sendInviteResponseDialog = { - message: '', - messageSlot: 0, - invite: {} + $app.methods.setUserDialogWorlds = function (userId) { + var worlds = []; + for (var ref of API.cachedWorlds.values()) { + if (ref.authorId === userId) { + worlds.push(ref); + } + } + $app.userDialog.worlds = worlds; }; - $app.data.sendInviteResponseDialogVisible = false; - - $app.data.sendInviteResponseConfirmDialog = { - visible: false + $app.methods.setUserDialogAvatars = function (userId) { + var avatars = new Set(); + this.userDialogAvatars.forEach((avatar) => { + avatars.add(avatar.id, avatar); + }); + for (var ref of API.cachedAvatars.values()) { + if (ref.authorId === userId && !avatars.has(ref.id)) { + this.userDialog.avatars.push(ref); + } + } + this.sortUserDialogAvatars(this.userDialog.avatars); }; - API.$on('LOGIN', function () { - $app.sendInviteResponseDialogVisible = false; - $app.sendInviteResponseConfirmDialog.visible = false; - }); - - $app.methods.showSendInviteResponseDialog = function (invite) { - this.sendInviteResponseDialog = { - invite - }; - API.refreshInviteMessageTableData('response'); - this.$nextTick(() => - adjustDialogZ(this.$refs.sendInviteResponseDialog.$el) - ); - this.clearInviteImageUpload(); - this.sendInviteResponseDialogVisible = true; + $app.methods.setUserDialogAvatarsRemote = async function (userId) { + if (this.avatarRemoteDatabase && userId !== API.currentUser.id) { + var data = await this.lookupAvatars('authorId', userId); + var avatars = new Set(); + this.userDialogAvatars.forEach((avatar) => { + avatars.add(avatar.id, avatar); + }); + if (data && typeof data === 'object') { + data.forEach((avatar) => { + if (avatar.id && !avatars.has(avatar.id)) { + this.userDialog.avatars.push(avatar); + } + }); + } + this.userDialog.avatarSorting = 'name'; + this.userDialog.avatarReleaseStatus = 'all'; + } + this.sortUserDialogAvatars(this.userDialog.avatars); }; - $app.methods.showSendInviteResponseConfirmDialog = function (val) { - if ( - this.editAndSendInviteResponseDialog.visible === true || - val === null - ) { - return; + $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' + } + }); + 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); + } + }); + } } - this.$nextTick(() => - adjustDialogZ(this.$refs.sendInviteResponseConfirmDialog.$el) - ); - this.sendInviteResponseConfirmDialog.visible = true; - this.sendInviteResponseDialog.messageSlot = val.slot; - }; - - $app.methods.cancelSendInviteResponse = function () { - this.sendInviteResponseDialogVisible = false; + return avatars; }; - $app.methods.cancelInviteResponseConfirm = function () { - this.sendInviteResponseConfirmDialog.visible = false; + $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 ($utils.extractFileId(avatar.imageUrl) === fileId) { + return avatar.id; + } + } + } + return null; }; - $app.methods.sendInviteResponseConfirm = function () { - var D = this.sendInviteResponseDialog; - var params = { - responseSlot: D.messageSlot, - rsvp: true - }; - if ($app.uploadImage) { - API.sendInviteResponsePhoto(params, D.invite.id, D.messageType) - .catch((err) => { - throw err; - }) - .then((args) => { - API.hideNotification({ - notificationId: D.invite.id - }); - this.$message({ - message: 'Invite response photo message sent', - type: 'success' - }); - return args; - }); - } else { - API.sendInviteResponse(params, D.invite.id, D.messageType) - .catch((err) => { - throw err; - }) - .then((args) => { - API.hideNotification({ - notificationId: D.invite.id - }); - this.$message({ - message: 'Invite response message sent', - type: 'success' - }); - return args; + $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' + } + }); + 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' + }); } - this.sendInviteResponseDialogVisible = false; - this.sendInviteRequestResponseDialogVisible = false; - this.sendInviteResponseConfirmDialog.visible = false; + return avatars; }; - // #endregion - // #region | App: Invite Request Response Message Dialog - - $app.data.sendInviteRequestResponseDialogVisible = false; - - $app.methods.cancelSendInviteRequestResponse = function () { - this.sendInviteRequestResponseDialogVisible = false; + $app.methods.sortUserDialogAvatars = function (array) { + var D = this.userDialog; + if (D.avatarSorting === 'update') { + array.sort(compareByUpdatedAt); + } else { + array.sort(compareByName); + } + D.avatars = array; }; - API.$on('LOGIN', function () { - $app.sendInviteRequestResponseDialogVisible = false; - $app.showSendInviteResponseConfirmDialog.visible = false; - }); - - $app.methods.showSendInviteRequestResponseDialog = function (invite) { - this.sendInviteResponseDialog = { - invite + $app.methods.refreshUserDialogWorlds = function () { + var D = this.userDialog; + if (D.isWorldsLoading) { + return; + } + D.isWorldsLoading = true; + var params = { + n: 50, + offset: 0, + sort: this.userDialog.worldSorting.value, + order: this.userDialog.worldOrder.value, + // user: 'friends', + userId: D.id, + releaseStatus: 'public' }; - API.refreshInviteMessageTableData('requestResponse'); - this.$nextTick(() => - adjustDialogZ(this.$refs.sendInviteRequestResponseDialog.$el) - ); - this.clearInviteImageUpload(); - this.sendInviteRequestResponseDialogVisible = true; - }; - - // #endregion - // #region | App: Invite Message Dialog - - $app.data.editAndSendInviteDialog = { - visible: false, - messageType: '', - newMessage: '', - inviteMessage: {} + if (params.userId === API.currentUser.id) { + params.user = 'me'; + params.releaseStatus = 'all'; + } + var map = new Map(); + for (var ref of API.cachedWorlds.values()) { + if ( + ref.authorId === D.id && + (ref.authorId === API.currentUser.id || + ref.releaseStatus === 'public') + ) { + API.cachedWorlds.delete(ref.id); + } + } + API.bulk({ + fn: 'getWorlds', + N: -1, + params, + handle: (args) => { + for (var json of args.json) { + var $ref = API.cachedWorlds.get(json.id); + if (typeof $ref !== 'undefined') { + map.set($ref.id, $ref); + } + } + }, + done: () => { + if (D.id === params.userId) { + this.setUserDialogWorlds(D.id); + } + D.isWorldsLoading = false; + } + }); }; - $app.methods.showEditAndSendInviteDialog = function ( - messageType, - inviteMessage - ) { - this.$nextTick(() => - adjustDialogZ(this.$refs.editAndSendInviteDialog.$el) - ); - this.editAndSendInviteDialog = { - newMessage: inviteMessage.message, - visible: true, - messageType, - inviteMessage + $app.methods.refreshUserDialogAvatars = function (fileId) { + var D = this.userDialog; + if (D.isAvatarsLoading) { + return; + } + D.isAvatarsLoading = true; + if (fileId) { + D.loading = true; + } + 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: '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 ($utils.extractFileId(ref.imageUrl) === fileId) { + this.showAvatarDialog(ref.id); + return; + } + } + this.$message({ + message: 'Own avatar not found', + type: 'error' + }); + } + } + }); }; - $app.methods.saveEditAndSendInvite = async function () { - var D = this.editAndSendInviteDialog; - D.visible = false; - var messageType = D.messageType; - var slot = D.inviteMessage.slot; - if (D.inviteMessage.message !== D.newMessage) { - var params = { - message: D.newMessage - }; - await API.editInviteMessage(params, messageType, slot) - .catch((err) => { - throw err; - }) - .then((args) => { - API.$emit(`INVITE:${messageType.toUpperCase()}`, args); - if (args.json[slot].message === D.inviteMessage.message) { - this.$message({ - message: - "VRChat API didn't update message, try again", - type: 'error' - }); - throw new Error( - "VRChat API didn't update message, try again" - ); - } else { - this.$message('Invite message updated'); - } - return args; + var performUserDialogCommand = (command, userId) => { + switch (command) { + case 'Delete Favorite': + API.deleteFavorite({ + objectId: userId }); - } - var I = this.sendInviteDialog; - var J = this.inviteDialog; - if (J.visible) { - var inviteLoop = () => { - if (J.userIds.length > 0) { - var receiverUserId = J.userIds.shift(); - if (receiverUserId === API.currentUser.id) { - // can't invite self!? - var L = API.parseLocation(J.worldId); - API.selfInvite({ - instanceId: L.instanceId, - worldId: L.worldId - }).finally(inviteLoop); - } else if ($app.uploadImage) { - API.sendInvitePhoto( - { - instanceId: J.worldId, - worldId: J.worldId, - worldName: J.worldName, - messageSlot: slot - }, - receiverUserId - ).finally(inviteLoop); - } else { - API.sendInvite( - { - instanceId: J.worldId, - worldId: J.worldId, - worldName: J.worldName, - messageSlot: slot - }, - receiverUserId - ).finally(inviteLoop); - } + break; + case 'Accept Friend Request': + var key = API.getFriendRequest(userId); + if (key === '') { + API.sendFriendRequest({ + userId + }); } else { - J.loading = false; - J.visible = false; - this.$message({ - message: 'Invite sent', - type: 'success' + API.acceptNotification({ + notificationId: key }); } - }; - inviteLoop(); - } else if (I.messageType === 'invite') { - I.params.messageSlot = slot; - if ($app.uploadImage) { - API.sendInvitePhoto(I.params, I.userId) - .catch((err) => { - throw err; - }) - .then((args) => { - this.$message({ - message: 'Invite photo message sent', - type: 'success' - }); - return args; + break; + case 'Decline Friend Request': + var key = API.getFriendRequest(userId); + if (key === '') { + API.cancelFriendRequest({ + userId + }); + } else { + API.hideNotification({ + notificationId: key }); + } + break; + case 'Cancel Friend Request': + API.cancelFriendRequest({ + userId + }); + break; + case 'Send Friend Request': + API.sendFriendRequest({ + userId + }); + break; + case 'Unblock': + API.deletePlayerModeration({ + moderated: userId, + type: 'block' + }); + break; + case 'Block': + API.sendPlayerModeration({ + moderated: userId, + type: 'block' + }); + break; + case 'Unmute': + API.deletePlayerModeration({ + moderated: userId, + type: 'mute' + }); + break; + case 'Mute': + API.sendPlayerModeration({ + moderated: userId, + type: 'mute' + }); + break; + case 'Enable Avatar Interaction': + API.deletePlayerModeration({ + moderated: userId, + type: 'interactOff' + }); + break; + case 'Disable Avatar Interaction': + API.sendPlayerModeration({ + moderated: userId, + type: 'interactOff' + }); + break; + case 'Unmute Chatbox': + API.deletePlayerModeration({ + moderated: userId, + type: 'muteChat' + }); + break; + case 'Mute Chatbox': + API.sendPlayerModeration({ + moderated: userId, + type: 'muteChat' + }); + break; + case 'Report Hacking': + $app.reportUserForHacking(userId); + break; + case 'Unfriend': + API.deleteFriend({ + userId + }); + break; + } + }; + + $app.methods.userDialogCommand = function (command) { + var D = this.userDialog; + if (D.visible === false) { + return; + } + if (command === 'Refresh') { + this.showUserDialog(D.id); + } else if (command === 'Add Favorite') { + this.showFavoriteDialog('friend', D.id); + } else if (command === 'Edit Social Status') { + this.showSocialStatusDialog(); + } else if (command === 'Edit Language') { + this.showLanguageDialog(); + } else if (command === 'Edit Bio') { + this.showBioDialog(); + } else if (command === 'Edit Pronouns') { + this.showPronounsDialog(); + } else if (command === 'Logout') { + this.logout(); + } else if (command === 'Request Invite') { + API.sendRequestInvite( + { + platform: 'standalonewindows' + }, + D.id + ).then((args) => { + this.$message('Request invite sent'); + return args; + }); + } else if (command === 'Invite Message') { + var L = $utils.parseLocation(this.lastLocation.location); + API.getCachedWorld({ + worldId: L.worldId + }).then((args) => { + this.showSendInviteDialog( + { + instanceId: this.lastLocation.location, + worldId: this.lastLocation.location, + worldName: args.ref.name + }, + D.id + ); + }); + } else if (command === 'Request Invite Message') { + this.showSendInviteRequestDialog( + { + platform: 'standalonewindows' + }, + D.id + ); + } else if (command === 'Invite') { + var currentLocation = this.lastLocation.location; + if (this.lastLocation.location === 'traveling') { + currentLocation = this.lastLocationDestination; + } + var L = $utils.parseLocation(currentLocation); + API.getCachedWorld({ + worldId: L.worldId + }).then((args) => { + API.sendInvite( + { + instanceId: L.tag, + worldId: L.tag, + worldName: args.ref.name + }, + D.id + ).then((_args) => { + this.$message('Invite sent'); + return _args; + }); + }); + } else if (command === 'Show Avatar Author') { + var { currentAvatarImageUrl } = D.ref; + this.showAvatarAuthorDialog( + D.id, + D.$avatarInfo.ownerId, + currentAvatarImageUrl + ); + } else if (command === 'Show Fallback Avatar Details') { + var { fallbackAvatar } = D.ref; + if (fallbackAvatar) { + this.showAvatarDialog(fallbackAvatar); } else { - API.sendInvite(I.params, I.userId) - .catch((err) => { - throw err; - }) - .then((args) => { - this.$message({ - message: 'Invite message sent', - type: 'success' - }); - return args; - }); + this.$message({ + message: 'No fallback avatar set', + type: 'error' + }); } - } else if (I.messageType === 'requestInvite') { - I.params.requestSlot = slot; - if ($app.uploadImage) { - API.sendRequestInvitePhoto(I.params, I.userId) - .catch((err) => { - this.clearInviteImageUpload(); - throw err; - }) - .then((args) => { - this.$message({ - message: 'Request invite photo message sent', - type: 'success' - }); - return args; - }); + } else if (command === 'Previous Images') { + this.displayPreviousImages('User', 'Display'); + } else if (command === 'Previous Instances') { + this.showPreviousInstancesUserDialog(D.ref); + } else if (command === 'Manage Gallery') { + this.showGalleryDialog(); + } else if (command === 'Invite To Group') { + this.showInviteGroupDialog('', D.id); + } else if (command === 'Send Boop') { + this.showSendBoopDialog(D.id); + } else if (command === 'Hide Avatar') { + if (D.isHideAvatar) { + this.setPlayerModeration(D.id, 0); } else { - API.sendRequestInvite(I.params, I.userId) - .catch((err) => { - throw err; - }) - .then((args) => { - this.$message({ - message: 'Request invite message sent', - type: 'success' - }); - return args; - }); + this.setPlayerModeration(D.id, 4); + } + } else if (command === 'Show Avatar') { + if (D.isShowAvatar) { + this.setPlayerModeration(D.id, 0); + } else { + this.setPlayerModeration(D.id, 5); } + } else { + this.$confirm(`Continue? ${command}`, 'Confirm', { + confirmButtonText: 'Confirm', + cancelButtonText: 'Cancel', + type: 'info', + callback: (action) => { + if (action === 'confirm') { + performUserDialogCommand(command, D.id); + } + } + }); } - this.sendInviteDialogVisible = false; - this.sendInviteRequestDialogVisible = false; }; - $app.methods.cancelEditAndSendInvite = function () { - this.editAndSendInviteDialog.visible = false; + $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); }; - $app.data.sendInviteDialog = { - message: '', - messageSlot: 0, - userId: '', - messageType: '', - params: {} + $app.methods.changeUserDialogAvatarSorting = function () { + var D = this.userDialog; + this.sortUserDialogAvatars(D.avatars); }; - $app.data.sendInviteDialogVisible = false; - - $app.data.sendInviteConfirmDialog = { - visible: false + $app.computed.userDialogAvatars = function () { + var { avatars, avatarReleaseStatus } = this.userDialog; + if ( + avatarReleaseStatus === 'public' || + avatarReleaseStatus === 'private' + ) { + return avatars.filter( + (avatar) => avatar.releaseStatus === avatarReleaseStatus + ); + } + return avatars; }; - API.$on('LOGIN', function () { - $app.sendInviteDialogVisible = false; - $app.sendInviteConfirmDialog.visible = false; - }); + // #endregion + // #region | App: World Dialog - $app.methods.showSendInviteDialog = function (params, userId) { - this.sendInviteDialog = { - params, - userId, - messageType: 'invite' - }; - API.refreshInviteMessageTableData('message'); - this.$nextTick(() => adjustDialogZ(this.$refs.sendInviteDialog.$el)); - this.clearInviteImageUpload(); - this.sendInviteDialogVisible = true; + $app.data.worldDialog = { + visible: false, + loading: false, + id: '', + memo: '', + $location: {}, + ref: {}, + isFavorite: false, + avatarScalingDisabled: false, + focusViewDisabled: false, + stickersDisabled: 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 }; - $app.methods.showSendInviteConfirmDialog = function (val) { - if (this.editAndSendInviteDialog.visible === true || val === null) { + 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; } - this.$nextTick(() => - adjustDialogZ(this.$refs.sendInviteConfirmDialog.$el) + D.ref = ref; + D.avatarScalingDisabled = ref.tags?.includes( + 'feature_avatar_scaling_disabled' ); - this.sendInviteConfirmDialog.visible = true; - this.sendInviteDialog.messageSlot = val.slot; - }; + D.focusViewDisabled = ref.tags?.includes('feature_focus_view_disabled'); + D.stickersDisabled = ref.tags?.includes('feature_stickers_disabled'); + $app.applyWorldDialogInstances(); + for (var room of D.rooms) { + if ($app.isRealInstance(room.tag)) { + API.getInstance({ + worldId: D.id, + instanceId: room.id + }); + } + } + if (D.bundleSizes.length === 0) { + $app.getBundleDateSize(ref).then((bundleSizes) => { + D.bundleSizes = bundleSizes; + }); + } + }); - $app.methods.cancelSendInvite = function () { - this.sendInviteDialogVisible = false; - }; + $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 (!this.compareUnityVersion(unityPackage.unitySortNumber)) { + continue; + } - $app.methods.cancelInviteConfirm = function () { - this.sendInviteConfirmDialog.visible = false; - }; + var platform = unityPackage.platform; + if (bundleSizes[platform]) { + continue; + } + var assetUrl = unityPackage.assetUrl; + var fileId = $utils.extractFileId(assetUrl); + var fileVersion = parseInt($utils.extractFileVersion(assetUrl), 10); + if (!fileId) { + continue; + } + var args = await API.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 + }; - $app.methods.sendInviteConfirm = function () { - var D = this.sendInviteDialog; - var J = this.inviteDialog; - if (J.visible) { - var inviteLoop = () => { - if (J.userIds.length > 0) { - var receiverUserId = J.userIds.shift(); - if (receiverUserId === API.currentUser.id) { - // can't invite self!? - var L = API.parseLocation(J.worldId); - API.selfInvite({ - instanceId: L.instanceId, - worldId: L.worldId - }).finally(inviteLoop); - } else if ($app.uploadImage) { - API.sendInvitePhoto( - { - instanceId: J.worldId, - worldId: J.worldId, - worldName: J.worldName, - messageSlot: D.messageSlot - }, - receiverUserId - ).finally(inviteLoop); - } else { - API.sendInvite( - { - instanceId: J.worldId, - worldId: J.worldId, - worldName: J.worldName, - messageSlot: D.messageSlot - }, - receiverUserId - ).finally(inviteLoop); + // 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; + } } - } else { - J.loading = false; - J.visible = false; - this.$message({ - message: 'Invite message sent', - type: 'success' - }); + // 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; } - }; - inviteLoop(); - } else if (D.messageType === 'invite') { - D.params.messageSlot = D.messageSlot; - if ($app.uploadImage) { - API.sendInvitePhoto(D.params, D.userId) - .catch((err) => { - throw err; - }) - .then((args) => { - this.$message({ - message: 'Invite photo message sent', - type: 'success' - }); - return args; - }); - } else { - API.sendInvite(D.params, D.userId) - .catch((err) => { - throw err; - }) - .then((args) => { - this.$message({ - message: 'Invite message sent', - type: 'success' - }); - return args; - }); - } - } else if (D.messageType === 'requestInvite') { - D.params.requestSlot = D.messageSlot; - if ($app.uploadImage) { - API.sendRequestInvitePhoto(D.params, D.userId) - .catch((err) => { - this.clearInviteImageUpload(); - throw err; - }) - .then((args) => { - this.$message({ - message: 'Request invite photo message sent', - type: 'success' - }); - return args; - }); - } else { - API.sendRequestInvite(D.params, D.userId) - .catch((err) => { - throw err; - }) - .then((args) => { - this.$message({ - message: 'Request invite message sent', - type: 'success' - }); - return args; - }); } } - this.sendInviteDialogVisible = false; - this.sendInviteRequestDialogVisible = false; - this.sendInviteConfirmDialog.visible = false; - }; - - // #endregion - // #region | App: Invite Request Message Dialog - - $app.data.sendInviteRequestDialogVisible = false; - $app.methods.cancelSendInviteRequest = function () { - this.sendInviteRequestDialogVisible = false; + return bundleSizes; }; - API.$on('LOGIN', function () { - $app.sendInviteRequestDialogVisible = false; - $app.showSendInviteConfirmDialog.visible = false; + 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; }); - $app.methods.showSendInviteRequestDialog = function (params, userId) { - this.sendInviteDialog = { - params, - userId, - messageType: 'requestInvite' - }; - API.refreshInviteMessageTableData('request'); - this.$nextTick(() => - adjustDialogZ(this.$refs.sendInviteRequestDialog.$el) - ); - this.clearInviteImageUpload(); - this.sendInviteRequestDialogVisible = 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) { + this.$nextTick(() => $app.adjustDialogZ(this.$refs.worldDialog.$el)); + var D = this.worldDialog; + var L = $utils.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.stickersDisabled = false; + D.isPC = false; + D.isQuest = false; + D.isIos = false; + D.hasPersistData = false; + D.memo = ''; + var LL = $utils.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; + } + }); + API.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 } = this.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.stickersDisabled = args.ref?.tags.includes( + 'feature_stickers_disabled' + ); + D.isPC = isPC; + D.isQuest = isQuest; + D.isIos = isIos; + this.updateVRChatWorldCache(); + API.hasWorldPersistData({ worldId: D.id }); + if (args.cache) { + API.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; + }); }; - // #endregion - // #region | App: Friends List - - API.$on('LOGIN', function () { - $app.friendsListTable.data = []; - }); - - $app.methods.selectFriendsListRow = function (val) { - if (val === null) { - return; - } - if (!val.id) { - this.lookupUser(val); + $app.methods.applyWorldDialogInstances = function () { + var D = this.worldDialog; + if (!D.visible) { return; } - this.showUserDialog(val.id); - }; - - $app.data.friendsListSearch = ''; - $app.data.friendsListSearchFilterVIP = false; - $app.data.friendsListSearchFilters = []; - $app.data.friendsListSelectAllCheckbox = false; - $app.data.friendsListBulkUnfriendMode = false; - $app.data.friendsListBulkUnfriendForceUpdate = 0; - - $app.methods.toggleFriendsListBulkUnfriendMode = function () { - if (!this.friendsListBulkUnfriendMode) { - this.friendsListTable.data.forEach((ref) => { - ref.$selected = false; - }); + 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: {} + }; + } } - }; - - $app.methods.showBulkUnfriendSelectionConfirm = function () { - var pendingUnfriendList = this.friendsListTable.data.reduce( - (acc, ctx) => { - if (ctx.$selected) { - acc.push(ctx.displayName); - } - return acc; - }, - [] - ); - var elementsTicked = pendingUnfriendList.length; - if (elementsTicked === 0) { - return; + 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: {} + }; } - this.$confirm( - `Are you sure you want to delete ${elementsTicked} friends? - This can negatively affect your trust rank, - This action cannot be undone.`, - `Delete ${elementsTicked} friends?`, - { - confirmButtonText: 'Confirm', - cancelButtonText: 'Cancel', - type: 'info', - showInput: true, - inputType: 'textarea', - inputValue: pendingUnfriendList.join('\r\n'), - callback: (action) => { - if (action === 'confirm') { - this.bulkUnfriendSelection(); + 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.displayName === user.displayName; + }); + if (addUser) { + var ref = API.cachedUsers.get(friend.userId); + if (typeof ref !== 'undefined') { + instance.users.push(ref); } } } - ); - }; - - $app.methods.bulkUnfriendSelection = function () { - for (var ctx of this.friendsListTable.data) { - if (ctx.$selected) { - API.deleteFriend({ - userId: ctx.id - }); - } } - }; - - // $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.friendsListSearchChange = function () { - this.friendsListTable.data = []; - var filters = [...this.friendsListSearchFilters]; - if (filters.length === 0) { - filters = ['Display Name', 'Rank', 'Status', 'Bio', 'Memo']; + 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 results = []; - if (this.friendsListSearch) { - var query = this.friendsListSearch; - var cleanedQuery = removeWhitespace(query); + 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 } - - for (var ctx of this.friends.values()) { - if (typeof ctx.ref === 'undefined') { - continue; + 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 = $utils.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') { + API.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 (typeof ctx.ref.$selected === 'undefined') { - ctx.ref.$selected = false; + if (this.instanceUsersSortAlphabetical) { + instance.users.sort(compareByDisplayName); + } else { + instance.users.sort(compareByLocationAt); } - if (this.friendsListSearchFilterVIP && !ctx.isVIP) { - continue; + 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; } - if (query && filters) { - var match = false; - if ( - !match && - filters.includes('Display Name') && - ctx.ref.displayName - ) { - match = - localeIncludes( - ctx.ref.displayName, - cleanedQuery, - this.stringComparer - ) || - localeIncludes( - removeConfusables(ctx.ref.displayName), - cleanedQuery, - this.stringComparer - ); - } - if (!match && filters.includes('Memo') && ctx.memo) { - match = localeIncludes( - ctx.memo, - query, - this.stringComparer - ); - } - if (!match && filters.includes('Bio') && ctx.ref.bio) { - match = localeIncludes( - ctx.ref.bio, - query, - this.stringComparer - ); - } - if ( - !match && - filters.includes('Status') && - ctx.ref.statusDescription - ) { - match = localeIncludes( - ctx.ref.statusDescription, - query, - this.stringComparer - ); + } + 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; } - if (!match && filters.includes('Rank') && ctx.ref.$friendNum) { - match = String(ctx.ref.$trustLevel) - .toUpperCase() - .includes(query.toUpperCase()); + 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; } - if (!match) { - continue; + 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; } - ctx.ref.$friendNum = ctx.no; - results.push(ctx.ref); - } - this.getAllUserStats(); - this.friendsListTable.data = results; + // 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.getAllUserStats = 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); + $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 + }; } } - - database.getAllUserStats(userIds, displayNames).then((data) => { - 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 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.displayName === user.displayName; + }); + if (addUser) { + var ref = API.cachedUsers.get(friend.userId); + if (typeof ref !== 'undefined') { + instance.users.push(ref); } } - - var friend = friendListMap.get(item.userId); - if (!friend) { - friendListMap.set(item.userId, item); - continue; - } - 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.created_at; - ref.ref.$timeSpent = item.timeSpent; - } + } + 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; } - }); - }; - - $app.methods.getUserStats = async function (ctx) { - var ref = await database.getUserStats(ctx); - /* eslint-disable require-atomic-updates */ - ctx.$joinCount = ref.joinCount; - ctx.$lastSeen = ref.created_at; - ctx.$timeSpent = ref.timeSpent; - /* eslint-enable require-atomic-updates */ - }; - - $app.watch.friendsListSearch = $app.methods.friendsListSearchChange; - $app.data.friendsListLoading = false; - $app.data.friendsListLoadingProgress = ''; - - $app.methods.friendsListLoadUsers = async function () { - this.friendsListLoading = true; - var i = 0; - var toFetch = []; - for (var ctx of this.friends.values()) { - if (ctx.ref && !ctx.ref.date_joined) { - toFetch.push(ctx.id); + 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 length = toFetch.length; - for (var userId of toFetch) { - if (!this.friendsListLoading) { - this.friendsListLoadingProgress = ''; - return; + 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 = $utils.parseLocation(instance.tag); + instance.location = instance.tag; + instance.$location = L; + if (instance.friendCount === 0) { + instance.friendCount = instance.users.length; } - i++; - this.friendsListLoadingProgress = `${i}/${length}`; - await API.getUser({ - userId - }); + if (this.instanceUsersSortAlphabetical) { + instance.users.sort(compareByDisplayName); + } else { + instance.users.sort(compareByLocationAt); + } + rooms.push(instance); } - this.friendsListLoadingProgress = ''; - this.friendsListLoading = false; - }; - - $app.methods.sortAlphabetically = function (a, b, field) { - if (!a[field] || !b[field]) { - return 0; + // get instance + for (var room of rooms) { + var ref = API.cachedInstances.get(room.tag); + if (typeof ref !== 'undefined') { + room.ref = ref; + } else if ($app.isRealInstance(room.tag)) { + API.getInstance({ + worldId: room.$location.worldId, + instanceId: room.$location.instanceId + }); + } } - return a[field].toLowerCase().localeCompare(b[field].toLowerCase()); - }; - - $app.methods.sortLanguages = function (a, b) { - var sortedA = []; - var sortedB = []; - a.$languages.forEach((item) => { - sortedA.push(item.value); - }); - b.$languages.forEach((item) => { - sortedB.push(item.value); + 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; }); - sortedA.sort(); - sortedB.sort(); - return JSON.stringify(sortedA).localeCompare(JSON.stringify(sortedB)); - }; - - $app.methods.genMd5 = async function (file) { - var response = await AppApi.MD5File(file); - return response; - }; - - $app.methods.resizeImageToFitLimits = async function (file) { - var response = await AppApi.ResizeImageToFitLimits(file); - return response; - }; - - $app.methods.genSig = async function (file) { - var response = await AppApi.SignFile(file); - return response; - }; - - $app.methods.genLength = async function (file) { - var response = await AppApi.FileLength(file); - return response; + D.instances = rooms; + this.updateTimers(); }; - // Upload avatar image - - $app.methods.onFileChangeAvatarImage = function (e) { - var clearFile = function () { - if (document.querySelector('#AvatarImageUploadButton')) { - document.querySelector('#AvatarImageUploadButton').value = ''; - } - }; - var files = e.target.files || e.dataTransfer.files; - if ( - !files.length || - !this.avatarDialog.visible || - this.avatarDialog.loading - ) { - clearFile(); - return; - } - if (files[0].size >= 100000000) { - // 100MB - $app.$message({ - message: 'File size too large', - type: 'error' - }); - clearFile(); - return; - } - if (!files[0].type.match(/image.*/)) { - $app.$message({ - message: "File isn't a png", - type: 'error' - }); - clearFile(); + $app.methods.worldDialogCommand = function (command) { + var D = this.worldDialog; + if (D.visible === false) { return; } - this.avatarDialog.loading = true; - this.changeAvatarImageDialogLoading = true; - var r = new FileReader(); - r.onload = async function (file) { - var base64File = await $app.resizeImageToFitLimits(btoa(r.result)); - // 10MB - var fileMd5 = await $app.genMd5(base64File); - var fileSizeInBytes = parseInt(file.total, 10); - var base64SignatureFile = await $app.genSig(base64File); - var signatureMd5 = await $app.genMd5(base64SignatureFile); - var signatureSizeInBytes = parseInt( - await $app.genLength(base64SignatureFile), - 10 - ); - var avatarId = $app.avatarDialog.id; - var { imageUrl } = $app.avatarDialog.ref; - var fileId = extractFileId(imageUrl); - if (!fileId) { - $app.$message({ - message: 'Current avatar image invalid', - type: 'error' + switch (command) { + case 'Refresh': + this.showWorldDialog(D.id); + break; + case 'New Instance': + this.showNewInstanceDialog(D.$location.tag); + break; + case 'Add Favorite': + this.showFavoriteDialog('world', D.id); + break; + case 'Rename': + this.promptRenameWorld(D); + break; + case 'Change Image': + this.displayPreviousImages('World', 'Change'); + break; + case 'Previous Images': + this.displayPreviousImages('World', 'Display'); + break; + case 'Previous Instances': + this.showPreviousInstancesWorldDialog(D.ref); + 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; + case 'Change Tags': + this.showSetWorldTagsDialog(); + break; + case 'Download Unity Package': + this.openExternalLink( + this.replaceVrcPackageUrl( + this.worldDialog.ref.unityPackageUrl + ) + ); + break; + default: + this.$confirm(`Continue? ${command}`, 'Confirm', { + confirmButtonText: 'Confirm', + cancelButtonText: 'Cancel', + type: 'info', + callback: (action) => { + if (action !== 'confirm') { + return; + } + switch (command) { + case 'Delete Favorite': + API.deleteFavorite({ + objectId: D.id + }); + break; + case 'Make Home': + API.saveCurrentUser({ + homeLocation: D.id + }).then((args) => { + this.$message({ + message: 'Home world updated', + type: 'success' + }); + return args; + }); + break; + case 'Reset Home': + API.saveCurrentUser({ + homeLocation: '' + }).then((args) => { + this.$message({ + message: 'Home world has been reset', + type: 'success' + }); + return args; + }); + break; + case 'Publish': + API.publishWorld({ + worldId: D.id + }).then((args) => { + this.$message({ + message: 'World has been published', + type: 'success' + }); + return args; + }); + break; + case 'Unpublish': + API.unpublishWorld({ + worldId: D.id + }).then((args) => { + this.$message({ + message: 'World has been unpublished', + type: 'success' + }); + return args; + }); + break; + case 'Delete Persistent Data': + API.deleteWorldPersistData({ + worldId: D.id + }).then((args) => { + this.$message({ + message: + 'Persistent data has been deleted', + type: 'success' + }); + return args; + }); + break; + case 'Delete': + API.deleteWorld({ + worldId: D.id + }).then((args) => { + this.$message({ + message: 'World has been deleted', + type: 'success' + }); + D.visible = false; + return args; + }); + break; + } + } }); - clearFile(); - return; - } - $app.avatarImage = { - base64File, - fileMd5, - base64SignatureFile, - signatureMd5, - fileId, - avatarId - }; - var params = { - fileMd5, - fileSizeInBytes, - signatureMd5, - signatureSizeInBytes - }; - API.uploadAvatarImage(params, fileId); - }; - r.readAsBinaryString(files[0]); - clearFile(); + break; + } }; - API.uploadAvatarImage = async function (params, fileId) { - try { - return await this.call(`file/${fileId}`, { - method: 'POST', - params - }).then((json) => { - var args = { - json, - params, - fileId - }; - this.$emit('AVATARIMAGE:INIT', args); - return args; - }); - } catch (err) { - console.error(err); - this.uploadAvatarFailCleanup(fileId); + $app.methods.refreshWorldDialogTreeData = function () { + var D = this.worldDialog; + D.treeData = $utils.buildTreeData(D.ref); + }; + + $app.computed.worldDialogPlatform = function () { + var { ref } = this.worldDialog; + var platforms = []; + if (ref.unityPackages) { + for (var unityPackage of ref.unityPackages) { + var platform = 'PC'; + if (unityPackage.platform === 'standalonewindows') { + platform = 'PC'; + } else if (unityPackage.platform === 'android') { + platform = 'Android'; + } else if (unityPackage.platform) { + ({ platform } = unityPackage); + } + platforms.unshift(`${platform}/${unityPackage.unityVersion}`); + } } - return void 0; + return platforms.join(', '); }; - API.uploadAvatarFailCleanup = async function (fileId) { - var json = await this.call(`file/${fileId}`, { - method: 'GET' - }); - var fileId = json.id; - var fileVersion = json.versions[json.versions.length - 1].version; - this.call(`file/${fileId}/${fileVersion}/signature/finish`, { - method: 'PUT' - }); - this.call(`file/${fileId}/${fileVersion}/file/finish`, { - method: 'PUT' - }); - $app.avatarDialog.loading = false; - $app.changeAvatarImageDialogLoading = false; + // #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, + treeData: [], + bundleSizes: [], + platformInfo: {}, + lastUpdated: '', + inCache: false, + cacheSize: 0, + cacheLocked: false, + cachePath: '', + fileAnalysis: {} }; - API.$on('AVATARIMAGE:INIT', function (args) { - var fileId = args.json.id; - var fileVersion = - args.json.versions[args.json.versions.length - 1].version; - var params = { - fileId, - fileVersion - }; - this.uploadAvatarImageFileStart(params); + API.$on('LOGOUT', function () { + $app.avatarDialog.visible = false; }); - API.uploadAvatarImageFileStart = async function (params) { - try { - return await this.call( - `file/${params.fileId}/${params.fileVersion}/file/start`, - { - method: 'PUT' - } - ).then((json) => { - var args = { - json, - params - }; - this.$emit('AVATARIMAGE:FILESTART', args); - return args; - }); - } catch (err) { - console.error(err); - this.uploadAvatarFailCleanup(params.fileId); + API.$on('FAVORITE', function (args) { + var { ref } = args; + var D = $app.avatarDialog; + if (D.visible === false || ref.$isDeleted || ref.favoriteId !== D.id) { + return; } - return void 0; - }; + D.isFavorite = true; + }); - API.$on('AVATARIMAGE:FILESTART', function (args) { - var { url } = args.json; - var { fileId, fileVersion } = args.params; - var params = { - url, - fileId, - fileVersion - }; - this.uploadAvatarImageFileAWS(params); + API.$on('FAVORITE:@DELETE', function (args) { + var D = $app.avatarDialog; + if (D.visible === false || D.id !== args.ref.favoriteId) { + return; + } + D.isFavorite = false; }); - API.uploadAvatarImageFileAWS = function (params) { - return webApiService - .execute({ - url: params.url, - uploadFilePUT: true, - fileData: $app.avatarImage.base64File, - fileMIME: 'image/png', - headers: { - 'Content-MD5': $app.avatarImage.fileMd5 + $app.methods.showAvatarDialog = function (avatarId) { + this.$nextTick(() => $app.adjustDialogZ(this.$refs.avatarDialog.$el)); + var D = this.avatarDialog; + D.visible = true; + D.loading = true; + D.id = avatarId; + D.fileAnalysis = {}; + D.treeData = []; + 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.isFavorite = + API.cachedFavoritesByObjectId.has(avatarId) || + (this.isLocalUserVrcplusSupporter() && + this.localAvatarFavoritesList.includes(avatarId)); + D.isBlocked = API.cachedAvatarModerations.has(avatarId); + D.memo = ''; + 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; + } + } + API.getAvatar({ avatarId }) + .then((args) => { + var { ref } = args; + D.ref = ref; + this.updateVRChatAvatarCache(); + if ( + ref.imageUrl === API.currentUser.currentAvatarImageUrl && + !ref.assetUrl + ) { + D.ref.assetUrl = API.currentUser.currentAvatarAssetUrl; + } + if (/quest/.test(ref.tags)) { + D.isQuestFallback = true; } - }) - .then((json) => { - if (json.status !== 200) { - $app.avatarDialog.loading = false; - $app.changeAvatarImageDialogLoading = false; - this.$throw('Avatar image upload failed', json, params.url); + var { isPC, isQuest, isIos } = this.getAvailablePlatforms( + args.ref.unityPackages + ); + D.isPC = isPC; + D.isQuest = isQuest; + D.isIos = isIos; + D.platformInfo = this.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; + } } - var args = { - json, - params - }; - this.$emit('AVATARIMAGE:FILEAWS', args); - return args; + if (D.bundleSizes.length === 0) { + this.getBundleDateSize(ref).then((bundleSizes) => { + D.bundleSizes = bundleSizes; + }); + } + }) + .catch((err) => { + D.loading = false; + D.visible = false; + throw err; + }) + .finally(() => { + D.loading = false; }); + this.getAvatarMemo(avatarId).then((memo) => { + if (D.id === memo.avatarId) { + D.memo = memo.memo; + } + }); }; - API.$on('AVATARIMAGE:FILEAWS', function (args) { - var { fileId, fileVersion } = args.params; - var params = { - fileId, - fileVersion - }; - this.uploadAvatarImageFileFinish(params); - }); - - API.uploadAvatarImageFileFinish = function (params) { - return this.call( - `file/${params.fileId}/${params.fileVersion}/file/finish`, - { - method: 'PUT', - params: { - maxParts: 0, - nextPartNumber: 0 + $app.methods.selectAvatarWithConfirmation = function (id) { + this.$confirm(`Continue? Select Avatar`, 'Confirm', { + confirmButtonText: 'Confirm', + cancelButtonText: 'Cancel', + type: 'info', + callback: (action) => { + if (action !== 'confirm') { + return; } + API.selectAvatar({ + avatarId: id + }).then((args) => { + this.$message({ + message: 'Avatar changed', + type: 'success' + }); + return args; + }); } - ).then((json) => { - var args = { - json, - params - }; - this.$emit('AVATARIMAGE:FILEFINISH', args); - return args; }); }; - API.$on('AVATARIMAGE:FILEFINISH', function (args) { - var { fileId, fileVersion } = args.params; - var params = { - fileId, - fileVersion - }; - this.uploadAvatarImageSigStart(params); - }); - - API.uploadAvatarImageSigStart = async function (params) { - try { - return await this.call( - `file/${params.fileId}/${params.fileVersion}/signature/start`, - { - method: 'PUT' - } - ).then((json) => { - var args = { - json, - params - }; - this.$emit('AVATARIMAGE:SIGSTART', args); - return args; - }); - } catch (err) { - console.error(err); - this.uploadAvatarFailCleanup(params.fileId); + $app.methods.avatarDialogCommand = function (command) { + var D = this.avatarDialog; + if (D.visible === false) { + return; + } + switch (command) { + case 'Refresh': + this.showAvatarDialog(D.id); + break; + case 'Rename': + this.promptRenameAvatar(D); + break; + case 'Change Image': + this.displayPreviousImages('Avatar', 'Change'); + break; + case 'Previous Images': + this.displayPreviousImages('Avatar', 'Display'); + break; + case 'Change Description': + this.promptChangeAvatarDescription(D); + break; + case 'Change Content Tags': + this.showSetAvatarTagsDialog(D.id); + break; + case 'Download Unity Package': + this.openExternalLink( + this.replaceVrcPackageUrl( + this.avatarDialog.ref.unityPackageUrl + ) + ); + break; + case 'Add Favorite': + this.showFavoriteDialog('avatar', D.id); + break; + default: + this.$confirm(`Continue? ${command}`, 'Confirm', { + confirmButtonText: 'Confirm', + cancelButtonText: 'Cancel', + type: 'info', + callback: (action) => { + if (action !== 'confirm') { + return; + } + switch (command) { + case 'Delete Favorite': + API.deleteFavorite({ + objectId: D.id + }); + break; + case 'Select Avatar': + API.selectAvatar({ + avatarId: D.id + }).then((args) => { + this.$message({ + message: 'Avatar changed', + type: 'success' + }); + return args; + }); + break; + case 'Select Fallback Avatar': + API.selectFallbackAvatar({ + avatarId: D.id + }).then((args) => { + this.$message({ + message: 'Fallback avatar changed', + type: 'success' + }); + return args; + }); + break; + case 'Block Avatar': + API.sendAvatarModeration({ + avatarModerationType: 'block', + targetAvatarId: D.id + }).then((args) => { + this.$message({ + message: 'Avatar blocked', + type: 'success' + }); + return args; + }); + break; + case 'Unblock Avatar': + API.deleteAvatarModeration({ + avatarModerationType: 'block', + targetAvatarId: D.id + }); + break; + case 'Make Public': + API.saveAvatar({ + id: D.id, + releaseStatus: 'public' + }).then((args) => { + this.$message({ + message: 'Avatar updated to public', + type: 'success' + }); + return args; + }); + break; + case 'Make Private': + API.saveAvatar({ + id: D.id, + releaseStatus: 'private' + }).then((args) => { + this.$message({ + message: 'Avatar updated to private', + type: 'success' + }); + return args; + }); + break; + case 'Delete': + API.deleteAvatar({ + avatarId: D.id + }).then((args) => { + this.$message({ + message: 'Avatar deleted', + type: 'success' + }); + D.visible = false; + return args; + }); + break; + case 'Delete Imposter': + API.deleteImposter({ + avatarId: D.id + }).then((args) => { + this.$message({ + message: 'Imposter deleted', + type: 'success' + }); + return args; + }); + break; + case 'Create Imposter': + API.createImposter({ + avatarId: D.id + }).then((args) => { + this.$message({ + message: 'Imposter queued for creation', + type: 'success' + }); + return args; + }); + break; + } + } + }); + break; } - return void 0; }; - API.$on('AVATARIMAGE:SIGSTART', function (args) { - var { url } = args.json; - var { fileId, fileVersion } = args.params; - var params = { - url, - fileId, - fileVersion - }; - this.uploadAvatarImageSigAWS(params); - }); - - API.uploadAvatarImageSigAWS = function (params) { - return webApiService - .execute({ - url: params.url, - uploadFilePUT: true, - fileData: $app.avatarImage.base64SignatureFile, - fileMIME: 'application/x-rsync-signature', - headers: { - 'Content-MD5': $app.avatarImage.signatureMd5 - } - }) - .then((json) => { - if (json.status !== 200) { - $app.avatarDialog.loading = false; - $app.changeAvatarImageDialogLoading = false; - this.$throw('Avatar image upload failed', json, params.url); - } - var args = { - json, - params - }; - this.$emit('AVATARIMAGE:SIGAWS', args); - return args; - }); + $app.methods.checkAvatarCache = function (fileId) { + var avatarId = ''; + for (var ref of API.cachedAvatars.values()) { + if ($utils.extractFileId(ref.imageUrl) === fileId) { + avatarId = ref.id; + } + } + return avatarId; }; - API.$on('AVATARIMAGE:SIGAWS', function (args) { - var { fileId, fileVersion } = args.params; - var params = { - fileId, - fileVersion - }; - this.uploadAvatarImageSigFinish(params); - }); + $app.methods.checkAvatarCacheRemote = async function (fileId, ownerUserId) { + if (this.avatarRemoteDatabase) { + var avatarId = await this.lookupAvatarByImageFileId( + ownerUserId, + fileId + ); + return avatarId; + } + return null; + }; - API.uploadAvatarImageSigFinish = function (params) { - return this.call( - `file/${params.fileId}/${params.fileVersion}/signature/finish`, - { - method: 'PUT', - params: { - maxParts: 0, - nextPartNumber: 0 + $app.methods.showAvatarAuthorDialog = async function ( + refUserId, + ownerUserId, + currentAvatarImageUrl + ) { + var fileId = $utils.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); } } - ).then((json) => { - var args = { - json, - params - }; - this.$emit('AVATARIMAGE:SIGFINISH', args); - return args; - }); + if (!avatarId) { + avatarId = await this.checkAvatarCacheRemote( + fileId, + avatarInfo.ownerId + ); + } + if (!avatarId) { + if (avatarInfo.ownerId === refUserId) { + this.$message({ + message: "It's personal (own) avatar", + type: 'warning' + }); + } else { + this.showUserDialog(avatarInfo.ownerId); + } + } + if (avatarId) { + this.showAvatarDialog(avatarId); + } + } }; - API.$on('AVATARIMAGE:SIGFINISH', function (args) { - var { fileId, fileVersion } = args.params; - var parmas = { - id: $app.avatarImage.avatarId, - imageUrl: `${API.endpointDomain}/file/${fileId}/${fileVersion}/file` - }; - this.setAvatarImage(parmas); - }); - - API.setAvatarImage = function (params) { - return this.call(`avatars/${params.id}`, { - method: 'PUT', - params - }).then((json) => { - var args = { - json, - params - }; - this.$emit('AVATARIMAGE:SET', args); - this.$emit('AVATAR', args); - return args; - }); + $app.methods.refreshAvatarDialogTreeData = function () { + var D = this.avatarDialog; + D.treeData = $utils.buildTreeData(D.ref); }; - // Upload world image - - $app.methods.onFileChangeWorldImage = function (e) { - var clearFile = function () { - if (document.querySelector('#WorldImageUploadButton')) { - document.querySelector('#WorldImageUploadButton').value = ''; + $app.computed.avatarDialogPlatform = function () { + var { ref } = this.avatarDialog; + var platforms = []; + if (ref.unityPackages) { + for (var unityPackage of ref.unityPackages) { + if ( + unityPackage.variant && + unityPackage.variant !== 'standard' && + unityPackage.variant !== 'security' + ) { + continue; + } + var platform = 'PC'; + if (unityPackage.platform === 'standalonewindows') { + platform = 'PC'; + } else if (unityPackage.platform === 'android') { + platform = 'Android'; + } else if (unityPackage.platform) { + ({ platform } = unityPackage); + } + platforms.push(`${platform}/${unityPackage.unityVersion}`); } - }; - var files = e.target.files || e.dataTransfer.files; - if ( - !files.length || - !this.worldDialog.visible || - this.worldDialog.loading - ) { - clearFile(); - return; - } - if (files[0].size >= 100000000) { - // 100MB - $app.$message({ - message: 'File size too large', - type: 'error' - }); - clearFile(); - return; - } - if (!files[0].type.match(/image.*/)) { - $app.$message({ - message: "File isn't a png", - type: 'error' - }); - clearFile(); - return; } - this.worldDialog.loading = true; - this.changeWorldImageDialogLoading = true; - var r = new FileReader(); - r.onload = async function (file) { - var base64File = await $app.resizeImageToFitLimits(btoa(r.result)); - // 10MB - var fileMd5 = await $app.genMd5(base64File); - var fileSizeInBytes = parseInt(file.total, 10); - var base64SignatureFile = await $app.genSig(base64File); - var signatureMd5 = await $app.genMd5(base64SignatureFile); - var signatureSizeInBytes = parseInt( - await $app.genLength(base64SignatureFile), - 10 - ); - var worldId = $app.worldDialog.id; - var { imageUrl } = $app.worldDialog.ref; - var fileId = extractFileId(imageUrl); - if (!fileId) { - $app.$message({ - message: 'Current world image invalid', - type: 'error' - }); - clearFile(); - return; - } - $app.worldImage = { - base64File, - fileMd5, - base64SignatureFile, - signatureMd5, - fileId, - worldId - }; - var params = { - fileMd5, - fileSizeInBytes, - signatureMd5, - signatureSizeInBytes - }; - API.uploadWorldImage(params, fileId); - }; - r.readAsBinaryString(files[0]); - clearFile(); + return platforms.join(', '); + }; + + // #endregion + // #region | App: Favorite Dialog + + $app.data.favoriteDialog = { + visible: false, + loading: false, + type: '', + objectId: '', + groups: [], + currentGroup: {} }; - API.uploadWorldImage = async function (params, fileId) { - try { - return await this.call(`file/${fileId}`, { - method: 'POST', - params - }).then((json) => { - var args = { - json, - params, - fileId - }; - this.$emit('WORLDIMAGE:INIT', args); + API.$on('LOGOUT', function () { + $app.favoriteDialog.visible = false; + }); + + $app.methods.addFavorite = function (group) { + var D = this.favoriteDialog; + D.loading = true; + API.addFavorite({ + type: D.type, + favoriteId: D.objectId, + tags: group.name + }) + .finally(() => { + D.loading = false; + }) + .then((args) => { return args; }); - } catch (err) { - console.error(err); - this.uploadWorldFailCleanup(fileId); - } - return void 0; }; - API.uploadWorldFailCleanup = async function (fileId) { - var json = await this.call(`file/${fileId}`, { - method: 'GET' + $app.methods.addFavoriteWorld = function (ref, group) { + return API.addFavorite({ + type: 'world', + favoriteId: ref.id, + tags: group.name }); - var fileId = json.id; - var fileVersion = json.versions[json.versions.length - 1].version; - this.call(`file/${fileId}/${fileVersion}/signature/finish`, { - method: 'PUT' + }; + + $app.methods.addFavoriteAvatar = function (ref, group) { + return API.addFavorite({ + type: 'avatar', + favoriteId: ref.id, + tags: group.name }); - this.call(`file/${fileId}/${fileVersion}/file/finish`, { - method: 'PUT' + }; + + $app.methods.addFavoriteUser = function (ref, group) { + return API.addFavorite({ + type: 'friend', + favoriteId: ref.id, + tags: group.name }); - $app.worldDialog.loading = false; - $app.changeWorldImageDialogLoading = false; }; - API.$on('WORLDIMAGE:INIT', function (args) { - var fileId = args.json.id; - var fileVersion = - args.json.versions[args.json.versions.length - 1].version; - var params = { - fileId, - fileVersion - }; - this.uploadWorldImageFileStart(params); - }); + $app.methods.moveFavorite = function (ref, group, type) { + API.deleteFavorite({ + objectId: ref.id + }).then(() => { + API.addFavorite({ + type, + favoriteId: ref.id, + tags: group.name + }); + }); + }; - API.uploadWorldImageFileStart = async function (params) { - try { - return await this.call( - `file/${params.fileId}/${params.fileVersion}/file/start`, - { - method: 'PUT' + $app.methods.showFavoriteDialog = function (type, objectId) { + this.$nextTick(() => $app.adjustDialogZ(this.$refs.favoriteDialog.$el)); + var D = this.favoriteDialog; + D.type = type; + D.objectId = objectId; + if (type === 'friend') { + D.groups = API.favoriteFriendGroups; + D.visible = true; + } else if (type === 'world') { + D.groups = API.favoriteWorldGroups; + D.visible = true; + } else if (type === 'avatar') { + D.groups = API.favoriteAvatarGroups; + 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; } - ).then((json) => { - var args = { - json, - params - }; - this.$emit('WORLDIMAGE:FILESTART', args); - return args; - }); - } catch (err) { - console.error(err); - this.uploadWorldFailCleanup(params.fileId); + } + 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; + } + } } - return void 0; }; - API.$on('WORLDIMAGE:FILESTART', function (args) { - var { url } = args.json; - var { fileId, fileVersion } = args.params; - var params = { - url, - fileId, - fileVersion - }; - this.uploadWorldImageFileAWS(params); + API.$on('FAVORITE:ADD', function (args) { + $app.updateFavoriteDialog(args.params.favoriteId); }); - API.uploadWorldImageFileAWS = function (params) { - return webApiService - .execute({ - url: params.url, - uploadFilePUT: true, - fileData: $app.worldImage.base64File, - fileMIME: 'image/png', - headers: { - 'Content-MD5': $app.worldImage.fileMd5 - } - }) - .then((json) => { - if (json.status !== 200) { - $app.worldDialog.loading = false; - $app.changeWorldImageDialogLoading = false; - this.$throw('World image upload failed', json, params.url); + API.$on('FAVORITE:DELETE', function (args) { + $app.updateFavoriteDialog(args.params.objectId); + }); + + // #endregion + // #region | App: Invite Dialog + + $app.data.inviteDialog = { + visible: false, + loading: false, + worldId: '', + worldName: '', + userIds: [], + friendsInInstance: [] + }; + + API.$on('LOGOUT', function () { + $app.inviteDialog.visible = false; + }); + + $app.methods.addFriendsInInstanceToInvite = function () { + var D = this.inviteDialog; + for (var friend of D.friendsInInstance) { + if (!D.userIds.includes(friend.id)) { + D.userIds.push(friend.id); + } + } + }; + + $app.methods.addFavoriteFriendsToInvite = function () { + var D = this.inviteDialog; + for (var friend of this.vipFriends) { + if (!D.userIds.includes(friend.id)) { + D.userIds.push(friend.id); + } + } + }; + + $app.methods.addSelfToInvite = function () { + var D = this.inviteDialog; + if (!D.userIds.includes(API.currentUser.id)) { + D.userIds.push(API.currentUser.id); + } + }; + + $app.methods.sendInvite = function () { + this.$confirm('Continue? Invite', 'Confirm', { + confirmButtonText: 'Confirm', + cancelButtonText: 'Cancel', + type: 'info', + callback: (action) => { + var D = this.inviteDialog; + if (action !== 'confirm' || D.loading === true) { + return; } - var args = { - json, - params + D.loading = true; + var inviteLoop = () => { + if (D.userIds.length > 0) { + var receiverUserId = D.userIds.shift(); + if (receiverUserId === API.currentUser.id) { + // can't invite self!? + var L = $utils.parseLocation(D.worldId); + API.selfInvite({ + instanceId: L.instanceId, + worldId: L.worldId + }).finally(inviteLoop); + } else { + API.sendInvite( + { + instanceId: D.worldId, + worldId: D.worldId, + worldName: D.worldName + }, + receiverUserId + ).finally(inviteLoop); + } + } else { + D.loading = false; + D.visible = false; + this.$message({ + message: 'Invite sent', + type: 'success' + }); + } }; - this.$emit('WORLDIMAGE:FILEAWS', args); - return args; - }); + inviteLoop(); + } + }); }; - API.$on('WORLDIMAGE:FILEAWS', function (args) { - var { fileId, fileVersion } = args.params; - var params = { - fileId, - fileVersion - }; - this.uploadWorldImageFileFinish(params); - }); - - API.uploadWorldImageFileFinish = function (params) { - return this.call( - `file/${params.fileId}/${params.fileVersion}/file/finish`, - { - method: 'PUT', - params: { - maxParts: 0, - nextPartNumber: 0 + $app.methods.showInviteDialog = function (tag) { + if (!this.isRealInstance(tag)) { + return; + } + this.$nextTick(() => $app.adjustDialogZ(this.$refs.inviteDialog.$el)); + var L = $utils.parseLocation(tag); + API.getCachedWorld({ + worldId: L.worldId + }).then((args) => { + var D = this.inviteDialog; + D.userIds = []; + D.worldId = L.tag; + D.worldName = args.ref.name; + D.friendsInInstance = []; + var friendsInCurrentInstance = this.lastLocation.friendList; + for (var friend of friendsInCurrentInstance.values()) { + var ctx = this.friends.get(friend.userId); + if (typeof ctx.ref === 'undefined') { + continue; } + D.friendsInInstance.push(ctx); } - ).then((json) => { - var args = { - json, - params - }; - this.$emit('WORLDIMAGE:FILEFINISH', args); - return args; + D.visible = true; }); }; - API.$on('WORLDIMAGE:FILEFINISH', function (args) { - var { fileId, fileVersion } = args.params; - var params = { - fileId, - fileVersion - }; - this.uploadWorldImageSigStart(params); + // #endregion + // #region | App: Social Status Dialog + + $app.data.socialStatusDialog = { + visible: false, + loading: false, + status: '', + statusDescription: '' + }; + + API.$on('LOGOUT', function () { + $app.socialStatusDialog.visible = false; }); - API.uploadWorldImageSigStart = async function (params) { - try { - return await this.call( - `file/${params.fileId}/${params.fileVersion}/signature/start`, - { - method: 'PUT' - } - ).then((json) => { - var args = { - json, - params - }; - this.$emit('WORLDIMAGE:SIGSTART', args); + $app.methods.saveSocialStatus = function () { + var D = this.socialStatusDialog; + if (D.loading) { + return; + } + D.loading = true; + API.saveCurrentUser({ + status: D.status, + statusDescription: D.statusDescription + }) + .finally(() => { + D.loading = false; + }) + .then((args) => { + D.visible = false; + this.$message({ + message: 'Status updated', + type: 'success' + }); return args; }); - } catch (err) { - console.error(err); - this.uploadWorldFailCleanup(params.fileId); + }; + + $app.methods.showSocialStatusDialog = function () { + this.$nextTick(() => + $app.adjustDialogZ(this.$refs.socialStatusDialog.$el) + ); + var D = this.socialStatusDialog; + var { statusHistory } = API.currentUser; + var statusHistoryArray = []; + for (var i = 0; i < statusHistory.length; ++i) { + var addStatus = { + no: i + 1, + status: statusHistory[i] + }; + statusHistoryArray.push(addStatus); } - return void 0; + this.socialStatusHistoryTable.data = statusHistoryArray; + D.status = API.currentUser.status; + D.statusDescription = API.currentUser.statusDescription; + D.visible = true; }; - API.$on('WORLDIMAGE:SIGSTART', function (args) { - var { url } = args.json; - var { fileId, fileVersion } = args.params; - var params = { - url, - fileId, - fileVersion - }; - this.uploadWorldImageSigAWS(params); + $app.methods.setSocialStatusFromHistory = function (val) { + if (val === null) { + return; + } + var D = this.socialStatusDialog; + D.statusDescription = val.status; + }; + + // #endregion + + // #region | App: Bio Dialog + + $app.data.bioDialog = { + visible: false, + loading: false, + bio: '', + bioLinks: [] + }; + + API.$on('LOGOUT', function () { + $app.bioDialog.visible = false; }); - API.uploadWorldImageSigAWS = function (params) { - return webApiService - .execute({ - url: params.url, - uploadFilePUT: true, - fileData: $app.worldImage.base64SignatureFile, - fileMIME: 'application/x-rsync-signature', - headers: { - 'Content-MD5': $app.worldImage.signatureMd5 - } + $app.methods.saveBio = function () { + var D = this.bioDialog; + if (D.loading) { + return; + } + D.loading = true; + API.saveCurrentUser({ + bio: D.bio, + bioLinks: D.bioLinks + }) + .finally(() => { + D.loading = false; }) - .then((json) => { - if (json.status !== 200) { - $app.worldDialog.loading = false; - $app.changeWorldImageDialogLoading = false; - this.$throw('World image upload failed', json, params.url); - } - var args = { - json, - params - }; - this.$emit('WORLDIMAGE:SIGAWS', args); + .then((args) => { + D.visible = false; + this.$message({ + message: 'Bio updated', + type: 'success' + }); return args; }); }; - API.$on('WORLDIMAGE:SIGAWS', function (args) { - var { fileId, fileVersion } = args.params; - var params = { - fileId, - fileVersion - }; - this.uploadWorldImageSigFinish(params); + $app.methods.showBioDialog = function () { + this.$nextTick(() => $app.adjustDialogZ(this.$refs.bioDialog.$el)); + var D = this.bioDialog; + D.bio = API.currentUser.bio; + D.bioLinks = API.currentUser.bioLinks.slice(); + D.visible = true; + }; + + // #endregion + // #region | App: Pronouns Dialog + + $app.data.pronounsDialog = { + visible: false, + loading: false, + pronouns: '' + }; + + API.$on('LOGOUT', function () { + $app.pronounsDialog.visible = false; }); - API.uploadWorldImageSigFinish = function (params) { - return this.call( - `file/${params.fileId}/${params.fileVersion}/signature/finish`, - { - method: 'PUT', - params: { - maxParts: 0, - nextPartNumber: 0 - } - } - ).then((json) => { - var args = { - json, - params - }; - this.$emit('WORLDIMAGE:SIGFINISH', args); - return args; - }); + $app.methods.savePronouns = function () { + var D = this.pronounsDialog; + if (D.loading) { + return; + } + D.loading = true; + API.saveCurrentUser({ + pronouns: D.pronouns + }) + .finally(() => { + D.loading = false; + }) + .then((args) => { + D.visible = false; + this.$message({ + message: 'Pronouns updated', + type: 'success' + }); + return args; + }); }; - API.$on('WORLDIMAGE:SIGFINISH', function (args) { - var { fileId, fileVersion } = args.params; - var parmas = { - id: $app.worldImage.worldId, - imageUrl: `${API.endpointDomain}/file/${fileId}/${fileVersion}/file` - }; - this.setWorldImage(parmas); - }); + $app.methods.showPronounsDialog = function () { + this.$nextTick(() => $app.adjustDialogZ(this.$refs.pronounsDialog.$el)); + var D = this.pronounsDialog; + D.pronouns = API.currentUser.pronouns; + D.visible = true; + }; - API.setWorldImage = function (params) { - return this.call(`worlds/${params.id}`, { - method: 'PUT', - params - }).then((json) => { - var args = { - json, - params - }; - this.$emit('WORLDIMAGE:SET', args); - this.$emit('WORLD', args); - return args; - }); + // #endregion + // #region | App: New Instance Dialog + + $app.data.newInstanceDialog = { + visible: false, + loading: false, + selectedTab: '0', + instanceCreated: false, + queueEnabled: false, + worldId: '', + instanceId: '', + instanceName: '', + userId: '', + accessType: '', + region: '', + groupRegion: '', + groupId: '', + groupAccessType: '', + strict: false, + location: '', + shortName: '', + url: '', + secureOrShortName: '', + lastSelectedGroupId: '', + selectedGroupRoles: [], + roleIds: [], + groupRef: {} }; - API.$on('AVATARIMAGE:SET', function (args) { - $app.avatarDialog.loading = false; - $app.changeAvatarImageDialogLoading = false; - if (args.json.imageUrl === args.params.imageUrl) { - $app.$message({ - message: 'Avatar image changed', - type: 'success' - }); - $app.displayPreviousImages('Avatar', 'Change'); - } else { - this.$throw(0, 'Avatar image change failed', args.params.imageUrl); - } + API.$on('LOGOUT', function () { + $app.newInstanceDialog.visible = false; }); - API.$on('WORLDIMAGE:SET', function (args) { - $app.worldDialog.loading = false; - $app.changeWorldImageDialogLoading = false; - if (args.json.imageUrl === args.params.imageUrl) { - $app.$message({ - message: 'World image changed', - type: 'success' - }); - $app.displayPreviousImages('World', 'Change'); + $app.methods.buildLegacyInstance = function () { + var D = this.newInstanceDialog; + D.instanceCreated = false; + D.shortName = ''; + D.secureOrShortName = ''; + var tags = []; + if (D.instanceName) { + D.instanceName = D.instanceName.replace(/[^A-Za-z0-9]/g, ''); + tags.push(D.instanceName); } else { - this.$throw(0, 'World image change failed', args.params.imageUrl); + var randValue = (99999 * Math.random() + 1).toFixed(0); + tags.push(String(randValue).padStart(5, '0')); } - }); - - // Set avatar/world image - - $app.methods.displayPreviousImages = function (type, command) { - this.previousImagesTableFileId = ''; - this.previousImagesTable = []; - var imageUrl = ''; - if (type === 'Avatar') { - var { imageUrl } = this.avatarDialog.ref; - } else if (type === 'World') { - var { imageUrl } = this.worldDialog.ref; - } else if (type === 'User') { - imageUrl = this.userDialog.ref.currentAvatarImageUrl; + if (!D.userId) { + D.userId = API.currentUser.id; } - var fileId = extractFileId(imageUrl); - if (!fileId) { - return; + var userId = D.userId; + if (D.accessType !== 'public') { + if (D.accessType === 'friends+') { + tags.push(`~hidden(${userId})`); + } else if (D.accessType === 'friends') { + tags.push(`~friends(${userId})`); + } else if (D.accessType === 'group') { + tags.push(`~group(${D.groupId})`); + tags.push(`~groupAccessType(${D.groupAccessType})`); + } else { + tags.push(`~private(${userId})`); + } + if (D.accessType === 'invite+') { + tags.push('~canRequestInvite'); + } } - var params = { - fileId - }; - if (command === 'Display') { - this.previousImagesDialogVisible = true; - this.$nextTick(() => - adjustDialogZ(this.$refs.previousImagesDialog.$el) - ); + if (D.region === 'US West') { + tags.push(`~region(us)`); + } else if (D.region === 'US East') { + tags.push(`~region(use)`); + } else if (D.region === 'Europe') { + tags.push(`~region(eu)`); + } else if (D.region === 'Japan') { + tags.push(`~region(jp)`); } - if (type === 'Avatar') { - if (command === 'Change') { - this.changeAvatarImageDialogVisible = true; - this.$nextTick(() => - adjustDialogZ(this.$refs.changeAvatarImageDialog.$el) - ); - } - API.getAvatarImages(params).then((args) => { - this.previousImagesTableFileId = args.json.id; - var images = []; - args.json.versions.forEach((item) => { - if (!item.deleted) { - images.unshift(item); - } + if (D.accessType !== 'invite' && D.accessType !== 'friends') { + D.strict = false; + } + if (D.strict) { + tags.push('~strict'); + } + if (D.groupId && D.groupId !== D.lastSelectedGroupId) { + D.roleIds = []; + var ref = API.cachedGroups.get(D.groupId); + if (typeof ref !== 'undefined') { + D.groupRef = ref; + D.selectedGroupRoles = ref.roles; + API.getGroupRoles({ + groupId: D.groupId + }).then((args) => { + D.lastSelectedGroupId = D.groupId; + D.selectedGroupRoles = args.json; + ref.roles = args.json; }); - this.checkPreviousImageAvailable(images); - }); - } else if (type === 'World') { - if (command === 'Change') { - this.changeWorldImageDialogVisible = true; - this.$nextTick(() => - adjustDialogZ(this.$refs.changeWorldImageDialog.$el) - ); } - API.getWorldImages(params).then((args) => { - this.previousImagesTableFileId = args.json.id; - var images = []; - args.json.versions.forEach((item) => { - if (!item.deleted) { - images.unshift(item); - } - }); - this.checkPreviousImageAvailable(images); - }); - } else if (type === 'User') { - API.getAvatarImages(params).then((args) => { - this.previousImagesTableFileId = args.json.id; - var images = []; - args.json.versions.forEach((item) => { - if (!item.deleted) { - images.unshift(item); - } + } + if (!D.groupId) { + D.roleIds = []; + D.selectedGroupRoles = []; + D.groupRef = {}; + D.lastSelectedGroupId = ''; + } + D.instanceId = tags.join(''); + this.updateNewInstanceDialog(false); + this.saveNewInstanceDialog(); + }; + + $app.methods.buildInstance = function () { + var D = this.newInstanceDialog; + D.instanceCreated = false; + D.instanceId = ''; + D.shortName = ''; + D.secureOrShortName = ''; + if (!D.userId) { + D.userId = API.currentUser.id; + } + if (D.groupId && D.groupId !== D.lastSelectedGroupId) { + D.roleIds = []; + var ref = API.cachedGroups.get(D.groupId); + if (typeof ref !== 'undefined') { + D.groupRef = ref; + D.selectedGroupRoles = ref.roles; + API.getGroupRoles({ + groupId: D.groupId + }).then((args) => { + D.lastSelectedGroupId = D.groupId; + D.selectedGroupRoles = args.json; + ref.roles = args.json; }); - this.checkPreviousImageAvailable(images); - }); + } + } + if (!D.groupId) { + D.roleIds = []; + D.groupRef = {}; + D.selectedGroupRoles = []; + D.lastSelectedGroupId = ''; + } + this.saveNewInstanceDialog(); + }; + + $app.methods.createNewInstance = function () { + var D = this.newInstanceDialog; + if (D.loading) { + return; + } + D.loading = true; + 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; } - }; - - $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); - } + 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 params = { + type, + canRequestInvite, + worldId: D.worldId, + ownerId: API.currentUser.id, + region + }; + if (type === 'group') { + params.groupAccessType = D.groupAccessType; + params.ownerId = D.groupId; + params.queueEnabled = D.queueEnabled; + if (D.groupAccessType === 'members') { + params.roleIds = D.roleIds; + params.canRequestInvite = true; + } else if (D.groupAccessType === 'plus') { + params.canRequestInvite = true; } } + API.createInstance(params) + .then((args) => { + D.location = args.json.location; + D.instanceId = args.json.instanceId; + D.secureOrShortName = + args.json.shortName || args.json.secureName; + D.instanceCreated = true; + this.updateNewInstanceDialog(); + return args; + }) + .finally(() => { + D.loading = false; + }); }; - $app.data.previousImagesDialogVisible = false; - $app.data.changeAvatarImageDialogVisible = false; - $app.data.changeAvatarImageDialogLoading = false; - $app.data.changeWorldImageDialogVisible = false; - $app.data.changeWorldImageDialogLoading = false; - $app.data.previousImagesTable = []; - $app.data.previousImagesFileId = ''; - - API.$on('LOGIN', function () { - $app.previousImagesTable = []; - $app.previousImagesDialogVisible = false; - }); - - API.getAvatarImages = function (params) { - return this.call(`file/${params.fileId}`, { - method: 'GET', - params - }).then((json) => { - var args = { - json, - params - }; - this.$emit('AVATARIMAGE:GET', args); + $app.methods.selfInvite = function (location, shortName) { + if (!this.isRealInstance(location)) { + return; + } + var L = $utils.parseLocation(location); + API.selfInvite({ + instanceId: L.instanceId, + worldId: L.worldId, + shortName + }).then((args) => { + this.$message({ + message: 'Self invite sent', + type: 'success' + }); return args; }); }; - API.getWorldImages = function (params) { - return this.call(`file/${params.fileId}`, { - method: 'GET', - params - }).then((json) => { - var args = { - json, - params - }; - this.$emit('WORLDIMAGE:GET', args); - return args; - }); + $app.methods.updateNewInstanceDialog = function (noChanges) { + var D = this.newInstanceDialog; + if (D.instanceId) { + D.location = `${D.worldId}:${D.instanceId}`; + } else { + D.location = D.worldId; + } + var L = $utils.parseLocation(D.location); + if (noChanges) { + L.shortName = D.shortName; + } else { + D.shortName = ''; + } + D.url = this.getLaunchURL(L); }; - API.$on('AVATARIMAGE:GET', function (args) { - $app.storeAvatarImage(args); - }); + $app.methods.saveNewInstanceDialog = async function () { + await configRepository.setString( + 'instanceDialogAccessType', + this.newInstanceDialog.accessType + ); + await configRepository.setString( + 'instanceRegion', + this.newInstanceDialog.region + ); + await configRepository.setString( + 'instanceDialogInstanceName', + this.newInstanceDialog.instanceName + ); + if (this.newInstanceDialog.userId === API.currentUser.id) { + await configRepository.setString('instanceDialogUserId', ''); + } else { + await configRepository.setString( + 'instanceDialogUserId', + this.newInstanceDialog.userId + ); + } + await configRepository.setString( + 'instanceDialogGroupId', + this.newInstanceDialog.groupId + ); + await configRepository.setString( + 'instanceDialogGroupAccessType', + this.newInstanceDialog.groupAccessType + ); + await configRepository.setBool( + 'instanceDialogStrict', + this.newInstanceDialog.strict + ); + await configRepository.setBool( + 'instanceDialogQueueEnabled', + this.newInstanceDialog.queueEnabled + ); + }; - $app.methods.storeAvatarImage = function (args) { - var refCreatedAt = args.json.versions[0]; - var fileCreatedAt = refCreatedAt.created_at; - var fileId = args.params.fileId; - var avatarName = ''; - var imageName = args.json.name; - var avatarNameRegex = /Avatar - (.*) - Image -/gi.exec(imageName); - if (avatarNameRegex) { - avatarName = this.replaceBioSymbols(avatarNameRegex[1]); + $app.methods.showNewInstanceDialog = async function (tag) { + if (!this.isRealInstance(tag)) { + return; } - var ownerId = args.json.ownerId; - var avatarInfo = { - ownerId, - avatarName, - fileCreatedAt - }; - API.cachedAvatarNames.set(fileId, avatarInfo); - return avatarInfo; + this.$nextTick(() => + $app.adjustDialogZ(this.$refs.newInstanceDialog.$el) + ); + var D = this.newInstanceDialog; + var L = $utils.parseLocation(tag); + if (D.worldId === L.worldId) { + // reopening dialog, keep last open instance + D.visible = true; + return; + } + D.worldId = L.worldId; + D.accessType = await configRepository.getString( + 'instanceDialogAccessType', + 'public' + ); + D.region = await configRepository.getString( + 'instanceRegion', + 'US West' + ); + D.instanceName = await configRepository.getString( + 'instanceDialogInstanceName', + '' + ); + D.userId = await configRepository.getString('instanceDialogUserId', ''); + D.groupId = await configRepository.getString( + 'instanceDialogGroupId', + '' + ); + D.groupAccessType = await configRepository.getString( + 'instanceDialogGroupAccessType', + 'plus' + ); + D.queueEnabled = await configRepository.getBool( + 'instanceDialogQueueEnabled', + true + ); + D.instanceCreated = false; + D.lastSelectedGroupId = ''; + D.selectedGroupRoles = []; + D.groupRef = {}; + D.roleIds = []; + D.strict = false; + D.shortName = ''; + D.secureOrShortName = ''; + API.getGroupPermissions({ userId: API.currentUser.id }); + this.buildInstance(); + this.buildLegacyInstance(); + this.updateNewInstanceDialog(); + D.visible = true; }; - $app.methods.setAvatarImage = function (image) { - this.changeAvatarImageDialogLoading = true; - var parmas = { - id: this.avatarDialog.id, - imageUrl: `${API.endpointDomain}/file/${this.previousImagesTableFileId}/${image.version}/file` - }; - API.setAvatarImage(parmas).finally(() => { - this.changeAvatarImageDialogLoading = false; - this.changeAvatarImageDialogVisible = false; + $app.methods.makeHome = function (tag) { + this.$confirm('Continue? Make Home', 'Confirm', { + confirmButtonText: 'Confirm', + cancelButtonText: 'Cancel', + type: 'info', + callback: (action) => { + if (action !== 'confirm') { + return; + } + API.saveCurrentUser({ + homeLocation: tag + }).then((args) => { + this.$message({ + message: 'Home world updated', + type: 'success' + }); + return args; + }); + } }); }; - $app.methods.uploadAvatarImage = function () { - document.getElementById('AvatarImageUploadButton').click(); + // #endregion + // #region | App: Launch Options Dialog + + $app.data.launchOptionsDialog = { + visible: false, + launchArguments: await configRepository.getString('launchArguments'), + vrcLaunchPathOverride: await configRepository.getString( + 'vrcLaunchPathOverride' + ) }; - $app.methods.deleteAvatarImage = function () { - this.changeAvatarImageDialogLoading = true; - var parmas = { - fileId: this.previousImagesTableFileId, - version: this.previousImagesTable[0].version - }; - API.deleteFileVersion(parmas) - .then((args) => { - this.previousImagesTableFileId = args.json.id; - var images = []; - args.json.versions.forEach((item) => { - if (!item.deleted) { - images.unshift(item); - } - }); - this.checkPreviousImageAvailable(images); - }) - .finally(() => { - this.changeAvatarImageDialogLoading = false; + API.$on('LOGIN', async function () { + var D = $app.launchOptionsDialog; + if ( + D.vrcLaunchPathOverride === null || + D.vrcLaunchPathOverride === 'null' + ) { + D.vrcLaunchPathOverride = ''; + await configRepository.setString( + 'vrcLaunchPathOverride', + D.vrcLaunchPathOverride + ); + } + }); + + API.$on('LOGOUT', function () { + $app.launchOptionsDialog.visible = false; + }); + + $app.methods.updateLaunchOptions = function () { + var D = this.launchOptionsDialog; + D.launchArguments = String(D.launchArguments) + .replace(/\s+/g, ' ') + .trim(); + configRepository.setString('launchArguments', D.launchArguments); + if ( + D.vrcLaunchPathOverride && + D.vrcLaunchPathOverride.endsWith('.exe') && + !D.vrcLaunchPathOverride.endsWith('launch.exe') + ) { + this.$message({ + message: + 'Invalid path, you must enter VRChat folder or launch.exe', + type: 'error' }); + return; + } + configRepository.setString( + 'vrcLaunchPathOverride', + D.vrcLaunchPathOverride + ); + this.$message({ + message: 'Updated launch options', + type: 'success' + }); + D.visible = false; }; - $app.methods.setWorldImage = function (image) { - this.changeWorldImageDialogLoading = true; - var parmas = { - id: this.worldDialog.id, - imageUrl: `${API.endpointDomain}/file/${this.previousImagesTableFileId}/${image.version}/file` - }; - API.setWorldImage(parmas).finally(() => { - this.changeWorldImageDialogLoading = false; - this.changeWorldImageDialogVisible = false; - }); + $app.methods.showLaunchOptions = function () { + this.$nextTick(() => + $app.adjustDialogZ(this.$refs.launchOptionsDialog.$el) + ); + var D = this.launchOptionsDialog; + D.visible = true; }; - $app.methods.uploadWorldImage = function () { - document.getElementById('WorldImageUploadButton').click(); + // #endregion + // #region | App: Set World Tags Dialog + + $app.data.setWorldTagsDialog = { + visible: false, + authorTags: [], + contentTags: [], + debugAllowed: false, + avatarScalingDisabled: false, + focusViewDisabled: false, + stickersDisabled: false, + contentHorror: false, + contentGore: false, + contentViolence: false, + contentAdult: false, + contentSex: false }; - $app.methods.deleteWorldImage = function () { - this.changeWorldImageDialogLoading = true; - var parmas = { - fileId: this.previousImagesTableFileId, - version: this.previousImagesTable[0].version - }; - API.deleteFileVersion(parmas) - .then((args) => { - this.previousImagesTableFileId = args.json.id; - var images = []; - args.json.versions.forEach((item) => { - if (!item.deleted) { - images.unshift(item); - } - }); - this.checkPreviousImageAvailable(images); - }) - .finally(() => { - this.changeWorldImageDialogLoading = false; - }); + $app.methods.showSetWorldTagsDialog = function () { + this.$nextTick(() => + $app.adjustDialogZ(this.$refs.setWorldTagsDialog.$el) + ); + var D = this.setWorldTagsDialog; + D.visible = true; + D.debugAllowed = false; + D.avatarScalingDisabled = false; + D.focusViewDisabled = false; + D.stickersDisabled = false; + D.contentHorror = false; + D.contentGore = false; + D.contentViolence = false; + D.contentAdult = false; + D.contentSex = false; + var oldTags = this.worldDialog.ref.tags; + var authorTags = []; + var contentTags = []; + oldTags.forEach((tag) => { + if (tag.startsWith('author_tag_')) { + authorTags.unshift(tag.substring(11)); + } + if (tag.startsWith('content_')) { + contentTags.unshift(tag.substring(8)); + } + switch (tag) { + case 'content_horror': + D.contentHorror = true; + break; + case 'content_gore': + D.contentGore = true; + break; + case 'content_violence': + D.contentViolence = true; + break; + case 'content_adult': + D.contentAdult = true; + break; + case 'content_sex': + D.contentSex = true; + break; + + case 'debug_allowed': + D.debugAllowed = true; + break; + case 'feature_avatar_scaling_disabled': + D.avatarScalingDisabled = true; + break; + case 'feature_focus_view_disabled': + D.focusViewDisabled = true; + break; + case 'feature_stickers_disabled': + D.stickersDisabled = true; + break; + } + }); + D.authorTags = authorTags.toString(); + D.contentTags = contentTags.toString(); }; - $app.methods.compareCurrentImage = function (image) { - if ( - `${API.endpointDomain}/file/${this.previousImagesTableFileId}/${image.version}/file` === - this.avatarDialog.ref.imageUrl - ) { - return true; + $app.methods.saveSetWorldTagsDialog = function () { + var D = this.setWorldTagsDialog; + var authorTags = D.authorTags.trim().split(','); + var contentTags = D.contentTags.trim().split(','); + var tags = []; + authorTags.forEach((tag) => { + if (tag) { + tags.unshift(`author_tag_${tag}`); + } + }); + // add back custom tags + contentTags.forEach((tag) => { + switch (tag) { + case 'horror': + case 'gore': + case 'violence': + case 'adult': + case 'sex': + case '': + break; + default: + tags.unshift(`content_${tag}`); + break; + } + }); + if (D.contentHorror) { + tags.unshift('content_horror'); } - return false; + if (D.contentGore) { + tags.unshift('content_gore'); + } + if (D.contentViolence) { + tags.unshift('content_violence'); + } + if (D.contentAdult) { + tags.unshift('content_adult'); + } + if (D.contentSex) { + tags.unshift('content_sex'); + } + if (D.debugAllowed) { + tags.unshift('debug_allowed'); + } + if (D.avatarScalingDisabled) { + tags.unshift('feature_avatar_scaling_disabled'); + } + if (D.focusViewDisabled) { + tags.unshift('feature_focus_view_disabled'); + } + if (D.stickersDisabled) { + tags.unshift('feature_stickers_disabled'); + } + API.saveWorld({ + id: this.worldDialog.id, + tags + }).then((args) => { + this.$message({ + message: 'Tags updated', + type: 'success' + }); + D.visible = false; + if ( + this.worldDialog.visible && + this.worldDialog.id === args.json.id + ) { + this.showWorldDialog(args.json.id); + } + return args; + }); }; - // Avatar names - - API.cachedAvatarNames = new Map(); + // #endregion + // #region | App: Set Avatar Tags Dialog - $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 API.getAvatarImages({ fileId }); - return this.storeAvatarImage(args); + $app.data.setAvatarTagsDialog = { + visible: false, + loading: false, + ownAvatars: [], + selectedCount: 0, + forceUpdate: 0, + selectedTags: [], + selectedTagsCsv: '', + contentHorror: false, + contentGore: false, + contentViolence: false, + contentAdult: false, + contentSex: false }; - $app.data.discordNamesDialogVisible = false; - $app.data.discordNamesContent = ''; - - $app.methods.showDiscordNamesDialog = function () { - var { friends } = API.currentUser; - if (Array.isArray(friends) === false) { - return; - } - var lines = ['DisplayName,DiscordName']; - var _ = function (str) { - if (/[\x00-\x1f,"]/.test(str) === true) { - return `"${str.replace(/"/g, '""')}"`; - } - return str; - }; - for (var userId of friends) { - var { ref } = this.friends.get(userId); - var discord = ''; - if (typeof ref === 'undefined') { - continue; - } - var name = ref.displayName; - if (ref.statusDescription) { - var statusRegex = /(?:discord|dc|dis)(?: |=|:|˸|;)(.*)/gi.exec( - ref.statusDescription - ); - if (statusRegex) { - discord = statusRegex[1]; - } + $app.methods.showSetAvatarTagsDialog = function (avatarId) { + this.$nextTick(() => + $app.adjustDialogZ(this.$refs.setAvatarTagsDialog.$el) + ); + var D = this.setAvatarTagsDialog; + D.visible = true; + D.loading = false; + D.ownAvatars = []; + D.forceUpdate = 0; + D.selectedTags = []; + D.selectedTagsCsv = ''; + D.contentHorror = false; + D.contentGore = false; + D.contentViolence = false; + D.contentAdult = false; + D.contentSex = false; + var oldTags = this.avatarDialog.ref.tags; + oldTags.forEach((tag) => { + switch (tag) { + case 'content_horror': + D.contentHorror = true; + break; + case 'content_gore': + D.contentGore = true; + break; + case 'content_violence': + D.contentViolence = true; + break; + case 'content_adult': + D.contentAdult = true; + break; + case 'content_sex': + D.contentSex = true; + break; + default: + if (tag.startsWith('content_')) { + D.selectedTags.push(tag.substring(8)); + } + break; } - if (!discord && ref.bio) { - var bioRegex = /(?:discord|dc|dis)(?: |=|:|˸|;)(.*)/gi.exec( - ref.bio - ); - if (bioRegex) { - discord = bioRegex[1]; + }); + for (var ref of API.cachedAvatars.values()) { + if (ref.authorId === API.currentUser.id) { + ref.$selected = false; + ref.$tagString = ''; + if (avatarId === ref.id) { + ref.$selected = true; + var conentTags = []; + ref.tags.forEach((tag) => { + if (tag.startsWith('content_')) { + conentTags.push(tag.substring(8)); + } + }); + for (var i = 0; i < conentTags.length; ++i) { + var tag = conentTags[i]; + if (i < conentTags.length - 1) { + ref.$tagString += `${tag}, `; + } else { + ref.$tagString += tag; + } + } } + D.ownAvatars.push(ref); } - if (!discord) { - continue; - } - discord = discord.trim(); - lines.push(`${_(name)},${_(discord)}`); } - this.discordNamesContent = lines.join('\n'); - this.discordNamesDialogVisible = true; + this.updateAvatarTagsSelection(); + this.updateSelectedAvatarTags(); }; - // userDialog world/avatar tab click - - $app.data.userDialogLastActiveTab = ''; - $app.data.userDialogLastAvatar = ''; - $app.data.userDialogLastWorld = ''; - $app.data.userDialogLastFavoriteWorld = ''; - $app.data.userDialogLastGroup = ''; - - $app.methods.userDialogTabClick = function (obj) { - var userId = this.userDialog.id; - if (this.userDialogLastActiveTab === obj.label) { - return; + $app.methods.updateSelectedAvatarTags = function () { + var D = this.setAvatarTagsDialog; + if (D.contentHorror) { + if (!D.selectedTags.includes('content_horror')) { + D.selectedTags.push('content_horror'); + } + } else if (D.selectedTags.includes('content_horror')) { + D.selectedTags.splice(D.selectedTags.indexOf('content_horror'), 1); } - if (obj.label === $t('dialog.user.groups.header')) { - if (this.userDialogLastGroup !== userId) { - this.userDialogLastGroup = userId; - this.getUserGroups(userId); + if (D.contentGore) { + if (!D.selectedTags.includes('content_gore')) { + D.selectedTags.push('content_gore'); } - } else if (obj.label === $t('dialog.user.avatars.header')) { - this.setUserDialogAvatars(userId); - if (this.userDialogLastAvatar !== userId) { - this.userDialogLastAvatar = userId; - if ( - userId === API.currentUser.id && - this.userDialog.avatars.length === 0 - ) { - this.refreshUserDialogAvatars(); - } else { - this.setUserDialogAvatarsRemote(userId); - } + } else if (D.selectedTags.includes('content_gore')) { + D.selectedTags.splice(D.selectedTags.indexOf('content_gore'), 1); + } + if (D.contentViolence) { + if (!D.selectedTags.includes('content_violence')) { + D.selectedTags.push('content_violence'); } - } else if (obj.label === $t('dialog.user.worlds.header')) { - this.setUserDialogWorlds(userId); - if (this.userDialogLastWorld !== userId) { - this.userDialogLastWorld = userId; - this.refreshUserDialogWorlds(); + } else if (D.selectedTags.includes('content_violence')) { + D.selectedTags.splice( + D.selectedTags.indexOf('content_violence'), + 1 + ); + } + if (D.contentAdult) { + if (!D.selectedTags.includes('content_adult')) { + D.selectedTags.push('content_adult'); } - } else if (obj.label === $t('dialog.user.favorite_worlds.header')) { - if (this.userDialogLastFavoriteWorld !== userId) { - this.userDialogLastFavoriteWorld = userId; - this.getUserFavoriteWorlds(userId); + } else if (D.selectedTags.includes('content_adult')) { + D.selectedTags.splice(D.selectedTags.indexOf('content_adult'), 1); + } + if (D.contentSex) { + if (!D.selectedTags.includes('content_sex')) { + D.selectedTags.push('content_sex'); } - } else if (obj.label === $t('dialog.user.json.header')) { - this.refreshUserDialogTreeData(); + } else if (D.selectedTags.includes('content_sex')) { + D.selectedTags.splice(D.selectedTags.indexOf('content_sex'), 1); } - this.userDialogLastActiveTab = obj.label; - }; - // VRChat Config JSON - - $app.data.VRChatConfigFile = {}; - $app.data.VRChatConfigList = {}; + D.selectedTagsCsv = D.selectedTags.join(',').replace(/content_/g, ''); + }; - $app.methods.readVRChatConfigFile = async function () { - this.VRChatConfigFile = {}; - var config = await AppApi.ReadConfigFile(); - if (config) { - try { - this.VRChatConfigFile = JSON.parse(config); - } catch { - this.$message({ - message: 'Invalid JSON in config.json', - type: 'error' - }); - throw new Error('Invalid JSON in config.json'); + $app.methods.updateInputAvatarTags = function () { + var D = this.setAvatarTagsDialog; + D.contentHorror = false; + D.contentGore = false; + D.contentViolence = false; + D.contentAdult = false; + D.contentSex = false; + var tags = D.selectedTagsCsv.split(','); + D.selectedTags = []; + for (var tag of tags) { + switch (tag) { + case 'horror': + D.contentHorror = true; + break; + case 'gore': + D.contentGore = true; + break; + case 'violence': + D.contentViolence = true; + break; + case 'adult': + D.contentAdult = true; + break; + case 'sex': + D.contentSex = true; + break; + } + if (!D.selectedTags.includes(`content_${tag}`)) { + D.selectedTags.push(`content_${tag}`); } } }; - $app.methods.WriteVRChatConfigFile = function () { - var json = JSON.stringify(this.VRChatConfigFile, null, '\t'); - AppApi.WriteConfigFile(json); - }; - - $app.data.VRChatConfigDialog = { - visible: false - }; - - API.$on('LOGIN', function () { - $app.VRChatConfigDialog.visible = false; - }); + $app.data.avatarContentTags = [ + 'content_horror', + 'content_gore', + 'content_violence', + 'content_adult', + 'content_sex' + ]; - $app.methods.showVRChatConfig = async function () { - this.VRChatConfigList = { - cache_size: { - name: $t('dialog.config_json.max_cache_size'), - default: '20', - type: 'number', - min: 20 - }, - cache_expiry_delay: { - name: $t('dialog.config_json.cache_expiry_delay'), - default: '30', - type: 'number', - min: 30 - }, - cache_directory: { - name: $t('dialog.config_json.cache_directory'), - default: '%AppData%\\..\\LocalLow\\VRChat\\VRChat' - }, - picture_output_folder: { - name: $t('dialog.config_json.picture_directory'), - // my pictures folder - default: `%UserProfile%\\Pictures\\VRChat` - }, - // dynamic_bone_max_affected_transform_count: { - // name: 'Dynamic Bones Limit Max Transforms (0 disable all transforms)', - // default: '32', - // type: 'number', - // min: 0 - // }, - // dynamic_bone_max_collider_check_count: { - // name: 'Dynamic Bones Limit Max Collider Collisions (0 disable all colliders)', - // default: '8', - // type: 'number', - // min: 0 - // }, - fpv_steadycam_fov: { - name: $t('dialog.config_json.fpv_steadycam_fov'), - default: '50', - type: 'number', - min: 30, - max: 110 + $app.methods.saveSetAvatarTagsDialog = async function () { + var D = this.setAvatarTagsDialog; + if (D.loading) { + return; + } + D.loading = true; + try { + for (var i = D.ownAvatars.length - 1; i >= 0; --i) { + var ref = D.ownAvatars[i]; + if (!D.visible) { + break; + } + if (!ref.$selected) { + continue; + } + var tags = [...D.selectedTags]; + for (var tag of ref.tags) { + if (!tag.startsWith('content_')) { + tags.push(tag); + } + } + await API.saveAvatar({ + id: ref.id, + tags + }); + D.selectedCount--; } - }; - await this.readVRChatConfigFile(); - this.$nextTick(() => adjustDialogZ(this.$refs.VRChatConfigDialog.$el)); - this.VRChatConfigDialog.visible = true; - if (!this.VRChatUsedCacheSize) { - this.getVRChatCacheSize(); + } catch (err) { + this.$message({ + message: 'Error saving avatar tags', + type: 'error' + }); + } finally { + D.loading = false; + D.visible = false; } }; - $app.methods.saveVRChatConfigFile = function () { - for (var item in this.VRChatConfigFile) { - if (item === 'picture_output_split_by_date') { - // this one is default true, it's special - if (this.VRChatConfigFile[item]) { - delete this.VRChatConfigFile[item]; + $app.methods.updateAvatarTagsSelection = function () { + var D = this.setAvatarTagsDialog; + D.selectedCount = 0; + for (var ref of D.ownAvatars) { + if (ref.$selected) { + D.selectedCount++; + } + ref.$tagString = ''; + var conentTags = []; + ref.tags.forEach((tag) => { + if (tag.startsWith('content_')) { + conentTags.push(tag.substring(8)); + } + }); + for (var i = 0; i < conentTags.length; ++i) { + var tag = conentTags[i]; + if (i < conentTags.length - 1) { + ref.$tagString += `${tag}, `; + } else { + ref.$tagString += tag; } - } else if (this.VRChatConfigFile[item] === '') { - delete this.VRChatConfigFile[item]; - } else if ( - typeof this.VRChatConfigFile[item] === 'boolean' && - this.VRChatConfigFile[item] === false - ) { - delete this.VRChatConfigFile[item]; - } else if ( - typeof this.VRChatConfigFile[item] === 'string' && - !isNaN(this.VRChatConfigFile[item]) - ) { - this.VRChatConfigFile[item] = parseInt( - this.VRChatConfigFile[item], - 10 - ); } } - this.VRChatConfigDialog.visible = false; - this.WriteVRChatConfigFile(); + this.setAvatarTagsDialog.forceUpdate++; }; - $app.data.VRChatScreenshotResolutions = [ - { name: '1280x720 (720p)', width: 1280, height: 720 }, - { name: '1920x1080 (1080p Default)', width: '', height: '' }, - { name: '2560x1440 (1440p)', width: 2560, height: 1440 }, - { name: '3840x2160 (4K)', width: 3840, height: 2160 } - ]; - - $app.data.VRChatCameraResolutions = [ - { name: '1280x720 (720p)', width: 1280, height: 720 }, - { name: '1920x1080 (1080p Default)', width: '', height: '' }, - { name: '2560x1440 (1440p)', width: 2560, height: 1440 }, - { name: '3840x2160 (4K)', width: 3840, height: 2160 }, - { name: '7680x4320 (8K)', width: 7680, height: 4320 } - ]; - - $app.methods.getVRChatResolution = function (res) { - switch (res) { - case '1280x720': - return '1280x720 (720p)'; - case '1920x1080': - return '1920x1080 (1080p)'; - case '2560x1440': - return '2560x1440 (2K)'; - case '3840x2160': - return '3840x2160 (4K)'; - case '7680x4320': - return '7680x4320 (8K)'; + $app.methods.setAvatarTagsSelectToggle = function () { + var D = this.setAvatarTagsDialog; + var allSelected = D.ownAvatars.length === D.selectedCount; + for (var ref of D.ownAvatars) { + ref.$selected = !allSelected; } - return `${res} (Custom)`; + this.updateAvatarTagsSelection(); }; - $app.methods.getVRChatCameraResolution = function () { - if ( - this.VRChatConfigFile.camera_res_height && - this.VRChatConfigFile.camera_res_width - ) { - var res = `${this.VRChatConfigFile.camera_res_width}x${this.VRChatConfigFile.camera_res_height}`; - return this.getVRChatResolution(res); - } - return '1920x1080 (1080p)'; + // #endregion + // #region | App: Notification position + + $app.data.notificationPositionDialog = { + visible: false }; - $app.methods.getVRChatScreenshotResolution = function () { - if ( - this.VRChatConfigFile.screenshot_res_height && - this.VRChatConfigFile.screenshot_res_width - ) { - var res = `${this.VRChatConfigFile.screenshot_res_width}x${this.VRChatConfigFile.screenshot_res_height}`; - return this.getVRChatResolution(res); - } - return '1920x1080 (1080p)'; + $app.methods.showNotificationPositionDialog = function () { + this.$nextTick(() => + $app.adjustDialogZ(this.$refs.notificationPositionDialog.$el) + ); + this.notificationPositionDialog.visible = true; }; - $app.methods.setVRChatCameraResolution = function (res) { - this.VRChatConfigFile.camera_res_height = res.height; - this.VRChatConfigFile.camera_res_width = res.width; + // #endregion + // #region | App: Noty feed filters + + $app.data.notyFeedFiltersDialog = { + visible: false }; - $app.methods.setVRChatScreenshotResolution = function (res) { - this.VRChatConfigFile.screenshot_res_height = res.height; - this.VRChatConfigFile.screenshot_res_width = res.width; + $app.methods.showNotyFeedFiltersDialog = function () { + this.$nextTick(() => + $app.adjustDialogZ(this.$refs.notyFeedFiltersDialog.$el) + ); + this.notyFeedFiltersDialog.visible = true; }; - // Auto Launch Shortcuts + // #endregion + // #region | App: Wrist feed filters - $app.methods.openShortcutFolder = function () { - AppApi.OpenShortcutFolder(); + $app.data.wristFeedFiltersDialog = { + visible: false }; - $app.methods.updateAppLauncherSettings = async function () { - await configRepository.setBool( - 'VRCX_enableAppLauncher', - this.enableAppLauncher - ); - await configRepository.setBool( - 'VRCX_enableAppLauncherAutoClose', - this.enableAppLauncherAutoClose - ); - await AppApi.SetAppLauncherSettings( - this.enableAppLauncher, - this.enableAppLauncherAutoClose + $app.methods.showWristFeedFiltersDialog = function () { + this.$nextTick(() => + $app.adjustDialogZ(this.$refs.wristFeedFiltersDialog.$el) ); + this.wristFeedFiltersDialog.visible = true; + }; + + // #endregion + // #region | App: Launch Dialog + + $app.data.launchDialog = { + visible: false, + loading: false, + desktop: await configRepository.getBool('launchAsDesktop'), + tag: '', + location: '', + url: '', + shortName: '', + shortUrl: '', + secureOrShortName: '' }; - // Screenshot Helper - - $app.methods.saveScreenshotHelper = async function () { - await configRepository.setBool( - 'VRCX_screenshotHelper', - this.screenshotHelper - ); - await configRepository.setBool( - 'VRCX_screenshotHelperModifyFilename', - this.screenshotHelperModifyFilename - ); + $app.methods.saveLaunchDialog = async function () { await configRepository.setBool( - 'VRCX_screenshotHelperCopyToClipboard', - this.screenshotHelperCopyToClipboard + 'launchAsDesktop', + this.launchDialog.desktop ); }; - $app.methods.processScreenshot = async function (path) { - var newPath = path; - if (this.screenshotHelper) { - var location = API.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 - }); + API.$on('LOGOUT', function () { + $app.launchDialog.visible = false; + }); + + API.$on('INSTANCE:SHORTNAME', function (args) { + if (!args.json) { + return; + } + var shortName = args.json.shortName; + var secureOrShortName = args.json.shortName || args.json.secureName; + var location = `${args.instance.worldId}:${args.instance.instanceId}`; + if (location === $app.launchDialog.tag) { + var L = $utils.parseLocation(location); + L.shortName = shortName; + $app.launchDialog.shortName = shortName; + $app.launchDialog.secureOrShortName = secureOrShortName; + if (shortName) { + $app.launchDialog.shortUrl = `https://vrch.at/${shortName}`; } - newPath = await AppApi.AddScreenshotMetadata( - path, - JSON.stringify(metadata), - location.worldId, - this.screenshotHelperModifyFilename - ); + $app.launchDialog.url = $app.getLaunchURL(L); } - if (this.screenshotHelperCopyToClipboard) { - await AppApi.CopyImageToClipboard(newPath); + if (location === $app.newInstanceDialog.location) { + $app.newInstanceDialog.shortName = shortName; + $app.newInstanceDialog.secureOrShortName = secureOrShortName; + $app.updateNewInstanceDialog(true); } - }; - - $app.methods.getAndDisplayScreenshot = function ( - path, - needsCarouselFiles = true - ) { - AppApi.GetScreenshotMetadata(path).then((metadata) => - this.displayScreenshotMetadata(metadata, needsCarouselFiles) - ); - }; + }); - $app.methods.getAndDisplayLastScreenshot = function () { - this.screenshotMetadataResetSearch(); - AppApi.GetLastScreenshot().then((path) => - this.getAndDisplayScreenshot(path) - ); + $app.methods.addShortNameToFullUrl = function (input, shortName) { + if (input.trim().length === 0 || !shortName) { + return input; + } + var url = new URL(input); + var urlParams = new URLSearchParams(url.search); + urlParams.set('shortName', shortName); + url.search = urlParams.toString(); + return url.toString(); }; - /** - * Function receives an unmodified json string grabbed from the screenshot file - * Error checking and and verification of data is done in .NET already; In the case that the data/file is invalid, a JSON object with the token "error" will be returned containing a description of the problem. - * Example: {"error":"Invalid file selected. Please select a valid VRChat screenshot."} - * See docs/screenshotMetadata.json for schema - * @param {string} metadata - JSON string grabbed from PNG file - * @param {string} needsCarouselFiles - Whether or not to get the last/next files for the carousel - * @returns {void} - */ - $app.methods.displayScreenshotMetadata = async function ( - json, - needsCarouselFiles = true - ) { - var D = this.screenshotMetadataDialog; - var metadata = JSON.parse(json); - if (typeof metadata === 'undefined' || !metadata.sourceFile) { - D.metadata = {}; - D.metadata.error = - 'Invalid file selected. Please select a valid VRChat screenshot.'; + $app.methods.showLaunchDialog = function (tag, shortName) { + if (!this.isRealInstance(tag)) { return; } + this.$nextTick(() => $app.adjustDialogZ(this.$refs.launchDialog.$el)); + var D = this.launchDialog; + D.tag = tag; + D.secureOrShortName = shortName; + D.shortUrl = ''; + D.shortName = shortName; + var L = $utils.parseLocation(tag); + L.shortName = shortName; + if (shortName) { + D.shortUrl = `https://vrch.at/${shortName}`; + } + if (L.instanceId) { + D.location = `${L.worldId}:${L.instanceId}`; + } else { + D.location = L.worldId; + } + D.url = this.getLaunchURL(L); + D.visible = true; + if (!shortName) { + API.getInstanceShortName({ + worldId: L.worldId, + instanceId: L.instanceId + }); + } + }; - // Get extra data for display dialog like resolution, file size, etc - D.loading = true; - var extraData = await AppApi.GetExtraScreenshotData( - metadata.sourceFile, - needsCarouselFiles - ); - D.loading = false; - var extraDataObj = JSON.parse(extraData); - Object.assign(metadata, extraDataObj); - - // console.log("Displaying screenshot metadata", json, "extra data", extraDataObj, "path", json.filePath) - - D.metadata = metadata; + $app.methods.getLaunchURL = function (instance) { + var L = instance; + if (L.instanceId) { + if (L.shortName) { + return `https://vrchat.com/home/launch?worldId=${encodeURIComponent( + L.worldId + )}&instanceId=${encodeURIComponent( + L.instanceId + )}&shortName=${encodeURIComponent(L.shortName)}`; + } + return `https://vrchat.com/home/launch?worldId=${encodeURIComponent( + L.worldId + )}&instanceId=${encodeURIComponent(L.instanceId)}`; + } + return `https://vrchat.com/home/launch?worldId=${encodeURIComponent( + L.worldId + )}`; + }; - var regex = metadata.fileName.match( - /VRChat_((\d{3,})x(\d{3,})_(\d{4})-(\d{2})-(\d{2})_(\d{2})-(\d{2})-(\d{2})\.(\d{1,})|(\d{4})-(\d{2})-(\d{2})_(\d{2})-(\d{2})-(\d{2})\.(\d{3})_(\d{3,})x(\d{3,}))/ - ); - if (regex) { - if (typeof regex[2] !== 'undefined' && regex[4].length === 4) { - // old format - // VRChat_3840x2160_2022-02-02_03-21-39.771 - var date = `${regex[4]}-${regex[5]}-${regex[6]}`; - var time = `${regex[7]}:${regex[8]}:${regex[9]}`; - D.metadata.dateTime = Date.parse(`${date} ${time}`); - // D.metadata.resolution = `${regex[2]}x${regex[3]}`; - } else if ( - typeof regex[11] !== 'undefined' && - regex[11].length === 4 - ) { - // new format - // VRChat_2023-02-16_10-39-25.274_3840x2160 - var date = `${regex[11]}-${regex[12]}-${regex[13]}`; - var time = `${regex[14]}:${regex[15]}:${regex[16]}`; - D.metadata.dateTime = Date.parse(`${date} ${time}`); - // D.metadata.resolution = `${regex[18]}x${regex[19]}`; + $app.methods.launchGame = async function ( + location, + shortName, + desktopMode + ) { + var D = this.launchDialog; + var L = $utils.parseLocation(location); + var args = []; + if ( + shortName && + L.instanceType !== 'public' && + L.groupAccessType !== 'public' + ) { + args.push(`vrchat://launch?id=${location}&shortName=${shortName}`); + } else { + // fetch shortName + var newShortName = ''; + var response = await API.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?id=${location}&shortName=${newShortName}` + ); + } else { + args.push(`vrchat://launch?id=${location}`); } } - if (!D.metadata.dateTime) { - D.metadata.dateTime = Date.parse(json.creationDate); + var { launchArguments, vrcLaunchPathOverride } = + this.launchOptionsDialog; + if (launchArguments) { + args.push(launchArguments); } - - if (this.fullscreenImageDialog?.visible) { - this.showFullscreenImageDialog(D.metadata.filePath); + if (desktopMode) { + args.push('--no-vr'); + } + if (vrcLaunchPathOverride) { + 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 { - this.openScreenshotMetadataDialog(); + 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); + D.visible = false; }; - $app.data.screenshotMetadataDialog = { - visible: false, - loading: false, - search: '', - searchType: 'Player Name', - searchTypes: ['Player Name', 'Player ID', 'World Name', 'World ID'], - metadata: {}, - isUploading: false + // #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.openScreenshotMetadataDialog = function () { - this.$nextTick(() => - adjustDialogZ(this.$refs.screenshotMetadataDialog.$el) - ); - var D = this.screenshotMetadataDialog; - D.visible = true; + $app.methods.copyInstanceMessage = function (input) { + this.copyToClipboard(input); + this.$message({ + message: 'Instance copied to clipboard', + type: 'success' + }); + return input; }; - $app.methods.showScreenshotMetadataDialog = function () { - var D = this.screenshotMetadataDialog; - if (!D.metadata.filePath) { - this.getAndDisplayLastScreenshot(); + $app.methods.copyInstanceUrl = async function (location) { + var L = $utils.parseLocation(location); + var args = await API.getInstanceShortName({ + worldId: L.worldId, + instanceId: L.instanceId + }); + if (args.json && args.json.shortName) { + L.shortName = args.json.shortName; } - this.openScreenshotMetadataDialog(); + var newUrl = this.getLaunchURL(L); + this.copyInstanceMessage(newUrl); }; - $app.methods.screenshotMetadataResetSearch = function () { - var D = this.screenshotMetadataDialog; - - D.search = ''; - D.searchIndex = null; - D.searchResults = null; + $app.methods.copyAvatarId = function (avatarId) { + this.$message({ + message: 'Avatar ID copied to clipboard', + type: 'success' + }); + this.copyToClipboard(avatarId); }; - $app.data.screenshotMetadataSearchInputs = 0; - $app.methods.screenshotMetadataSearch = function () { - var D = this.screenshotMetadataDialog; - - // Don't search if user is still typing - this.screenshotMetadataSearchInputs++; - let current = this.screenshotMetadataSearchInputs; - setTimeout(() => { - if (current !== this.screenshotMetadataSearchInputs) { - return; - } - this.screenshotMetadataSearchInputs = 0; - - if (D.search === '') { - this.screenshotMetadataResetSearch(); - if (D.metadata.filePath !== null) { - // Re-retrieve the current screenshot metadata and get previous/next files for regular carousel directory navigation - this.getAndDisplayScreenshot(D.metadata.filePath, true); - } - return; - } - - var searchType = D.searchTypes.indexOf(D.searchType); // Matches the search type enum in .NET - D.loading = true; - AppApi.FindScreenshotsBySearch(D.search, searchType) - .then((json) => { - var results = JSON.parse(json); - - if (results.length === 0) { - D.metadata = {}; - D.metadata.error = 'No results found'; - - D.searchIndex = null; - D.searchResults = null; - return; - } - - D.searchIndex = 0; - D.searchResults = results; - - // console.log("Search results", results) - this.getAndDisplayScreenshot(results[0], false); - }) - .finally(() => { - D.loading = false; - }); - }, 500); + $app.methods.copyAvatarUrl = function (avatarId) { + this.$message({ + message: 'Avatar URL copied to clipboard', + type: 'success' + }); + this.copyToClipboard(`https://vrchat.com/home/avatar/${avatarId}`); }; - $app.methods.screenshotMetadataCarouselChangeSearch = function (index) { - var D = this.screenshotMetadataDialog; - var searchIndex = D.searchIndex; - var filesArr = D.searchResults; - - if (searchIndex === null) { - return; - } - - if (index === 0) { - if (searchIndex > 0) { - this.getAndDisplayScreenshot(filesArr[searchIndex - 1], false); - searchIndex--; - } else { - this.getAndDisplayScreenshot( - filesArr[filesArr.length - 1], - false - ); - searchIndex = filesArr.length - 1; - } - } else if (index === 2) { - if (searchIndex < filesArr.length - 1) { - this.getAndDisplayScreenshot(filesArr[searchIndex + 1], false); - searchIndex++; - } else { - this.getAndDisplayScreenshot(filesArr[0], false); - searchIndex = 0; - } - } - - if (typeof this.$refs.screenshotMetadataCarousel !== 'undefined') { - this.$refs.screenshotMetadataCarousel.setActiveItem(1); - } - - D.searchIndex = searchIndex; + $app.methods.copyWorldId = function (worldId) { + this.$message({ + message: 'World ID copied to clipboard', + type: 'success' + }); + this.copyToClipboard(worldId); }; - $app.methods.screenshotMetadataCarouselChange = function (index) { - var D = this.screenshotMetadataDialog; - var searchIndex = D.searchIndex; - - if (searchIndex !== null) { - this.screenshotMetadataCarouselChangeSearch(index); - return; - } + $app.methods.copyWorldUrl = function (worldId) { + this.$message({ + message: 'World URL copied to clipboard', + type: 'success' + }); + this.copyToClipboard(`https://vrchat.com/home/world/${worldId}`); + }; - if (index === 0) { - if (D.metadata.previousFilePath) { - this.getAndDisplayScreenshot(D.metadata.previousFilePath); - } else { - this.getAndDisplayScreenshot(D.metadata.filePath); - } - } - if (index === 2) { - if (D.metadata.nextFilePath) { - this.getAndDisplayScreenshot(D.metadata.nextFilePath); - } else { - this.getAndDisplayScreenshot(D.metadata.filePath); - } - } - if (typeof this.$refs.screenshotMetadataCarousel !== 'undefined') { - this.$refs.screenshotMetadataCarousel.setActiveItem(1); - } + $app.methods.copyWorldName = function (worldName) { + this.$message({ + message: 'World name copied to clipboard', + type: 'success' + }); + this.copyToClipboard(worldName); + }; - if (this.fullscreenImageDialog.visible) { - // TODO - } + $app.methods.copyUserId = function (userId) { + this.$message({ + message: 'User ID copied to clipboard', + type: 'success' + }); + this.copyToClipboard(userId); }; - $app.methods.uploadScreenshotToGallery = function () { - var D = this.screenshotMetadataDialog; - if (D.metadata.fileSizeBytes > 10000000) { - $app.$message({ - message: 'File size too large', - type: 'error' - }); - return; - } - D.isUploading = true; - AppApi.GetFileBase64(D.metadata.filePath) - .then((base64Body) => { - API.uploadGalleryImage(base64Body) - .then((args) => { - $app.$message({ - message: 'Gallery image uploaded', - type: 'success' - }); - return args; - }) - .finally(() => { - D.isUploading = false; - }); - }) - .catch((err) => { - $app.$message({ - message: 'Failed to upload gallery image', - type: 'error' - }); - console.error(err); - D.isUploading = false; - }); + $app.methods.copyUserURL = function (userId) { + this.$message({ + message: 'User URL copied to clipboard', + type: 'success' + }); + this.copyToClipboard(`https://vrchat.com/home/user/${userId}`); }; - /** - * 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; + $app.methods.copyUserDisplayName = function (displayName) { + this.$message({ + message: 'User DisplayName copied to clipboard', + type: 'success' + }); + this.copyToClipboard(displayName); }; - $app.methods.handleDrop = function (event) { - if (this.currentlyDroppingFile === null) { - return; - } - console.log('Dropped file into viewer: ', this.currentlyDroppingFile); + $app.methods.copyGroupId = function (groupId) { + this.$message({ + message: 'Group ID copied to clipboard', + type: 'success' + }); + this.copyToClipboard(groupId); + }; - this.screenshotMetadataResetSearch(); - this.getAndDisplayScreenshot(this.currentlyDroppingFile); + $app.methods.copyGroupUrl = function (groupUrl) { + this.$message({ + message: 'Group URL copied to clipboard', + type: 'success' + }); + this.copyToClipboard(groupUrl); + }; - event.preventDefault(); + $app.methods.copyImageUrl = function (imageUrl) { + this.$message({ + message: 'ImageUrl copied to clipboard', + type: 'success' + }); + this.copyToClipboard(imageUrl); }; - $app.methods.copyImageToClipboard = function (path) { - AppApi.CopyImageToClipboard(path).then(() => { - this.$message({ - message: 'Image copied to clipboard', - type: 'success' - }); + $app.methods.copyText = function (text) { + this.$message({ + message: 'Text copied to clipboard', + type: 'success' }); + this.copyToClipboard(text); }; - $app.methods.openImageFolder = function (path) { - AppApi.OpenFolderAndSelectItem(path).then(() => { - this.$message({ - message: 'Opened image folder', - type: 'success' - }); + $app.methods.copyLink = function (text) { + this.$message({ + message: 'Link copied to clipboard', + type: 'success' }); + this.copyToClipboard(text); }; - // YouTube API + // #endregion + // #region | App: VRCPlus Icons - $app.data.youTubeApiDialog = { - visible: false + API.$on('LOGIN', function () { + $app.VRCPlusIconsTable = []; + }); + + $app.methods.refreshVRCPlusIconsTable = function () { + this.galleryDialogIconsLoading = true; + var params = { + n: 100, + tag: 'icon' + }; + API.getFileList(params); }; - API.$on('LOGOUT', function () { - $app.youTubeApiDialog.visible = false; + API.getFileList = function (params) { + return this.call('files', { + method: 'GET', + params + }).then((json) => { + var args = { + json, + params + }; + this.$emit('FILES:LIST', args); + return args; + }); + }; + + API.$on('FILES:LIST', function (args) { + if (args.params.tag === 'icon') { + $app.VRCPlusIconsTable = args.json.reverse(); + $app.galleryDialogIconsLoading = false; + } }); - $app.methods.testYouTubeApiKey = async function () { - if (!this.youTubeApiKey) { + $app.methods.setVRCPlusIcon = function (fileId) { + if (!API.currentUser.$isVRCPlus) { this.$message({ - message: 'YouTube API key removed', - type: 'success' + message: 'VRCPlus required', + type: 'error' }); - this.youTubeApiDialog.visible = false; return; } - var data = await this.lookupYouTubeVideo('dQw4w9WgXcQ'); - if (!data) { - this.youTubeApiKey = ''; - this.$message({ - message: 'Invalid YouTube API key', - type: 'error' - }); - } else { - await configRepository.setString( - 'VRCX_youtubeAPIKey', - this.youTubeApiKey - ); + var userIcon = ''; + if (fileId) { + userIcon = `${API.endpointDomain}/file/${fileId}/1`; + } + if (userIcon === API.currentUser.userIcon) { + return; + } + API.saveCurrentUser({ + userIcon + }).then((args) => { this.$message({ - message: 'YouTube API key valid!', + message: 'Icon changed', type: 'success' }); - this.youTubeApiDialog.visible = false; - } - }; - - $app.methods.changeYouTubeApi = async function () { - 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.$nextTick(() => adjustDialogZ(this.$refs.youTubeApiDialog.$el)); - var D = this.youTubeApiDialog; - D.visible = true; - }; - - // 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 = ''; - this.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; - }); - } + return args; + }); }; - $app.methods.updateVRChatAvatarCache = function () { - var D = this.avatarDialog; - if (D.visible) { - D.inCache = false; - D.cacheSize = 0; - D.cacheLocked = false; - D.cachePath = ''; - this.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.deleteVRCPlusIcon = function (fileId) { + API.deleteFile(fileId).then((args) => { + API.$emit('VRCPLUSICON:DELETE', args); + return args; + }); }; - // eslint-disable-next-line require-await - $app.methods.checkVRChatCache = async function (ref) { - if (!ref.unityPackages) { - return { Item1: -1, Item2: false, Item3: '' }; - } - var assetUrl = ''; - var variant = ''; - for (var i = ref.unityPackages.length - 1; i > -1; i--) { - var unityPackage = ref.unityPackages[i]; - if ( - unityPackage.variant && - unityPackage.variant !== 'standard' && - unityPackage.variant !== 'security' - ) { - continue; - } - if ( - unityPackage.platform === 'standalonewindows' && - this.compareUnityVersion(unityPackage.unitySortNumber) - ) { - assetUrl = unityPackage.assetUrl; - if (unityPackage.variant !== 'standard') { - variant = unityPackage.variant; - } + API.$on('VRCPLUSICON:DELETE', function (args) { + var array = $app.VRCPlusIconsTable; + var { length } = array; + for (var i = 0; i < length; ++i) { + if (args.fileId === array[i].id) { + array.splice(i, 1); break; } } - if (!assetUrl) { - assetUrl = ref.assetUrl; - } - var id = extractFileId(assetUrl); - var version = parseInt(extractFileVersion(assetUrl), 10); - var variantVersion = parseInt(extractVariantVersion(assetUrl), 10); - if (!id || !version) { - return { Item1: -1, Item2: false, Item3: '' }; - } + }); - return AssetBundleCacher.CheckVRChatCache( - id, - version, - variant, - variantVersion - ); + API.deleteFile = function (fileId) { + return this.call(`file/${fileId}`, { + method: 'DELETE' + }).then((json) => { + var args = { + json, + fileId + }; + return args; + }); }; - API.getBundles = function (fileId) { - return this.call(`file/${fileId}`, { - method: 'GET' + API.deleteFileVersion = function (params) { + return this.call(`file/${params.fileId}/${params.version}`, { + method: 'DELETE' }).then((json) => { var args = { - json + json, + params }; return args; }); }; - $app.data.cacheAutoDownloadHistory = new Set(); - - $app.methods.downloadFileQueueUpdate = async function () { - if (this.downloadQueue.size === 0) { - return; + $app.methods.compareCurrentVRCPlusIcon = function (userIcon) { + var currentUserIcon = $utils.extractFileId(API.currentUser.userIcon); + if (userIcon === currentUserIcon) { + return true; } - this.downloadProgress = 0; - this.downloadIsProcessing = false; - this.downloadInProgress = true; - this.downloadCurrent = this.downloadQueue.values().next().value; - this.downloadCurrent.id = this.downloadQueue.keys().next().value; - var { ref } = this.downloadCurrent; - this.downloadQueue.delete(ref.id); - this.downloadQueueTable.data = Array.from(this.downloadQueue.values()); - - var fileUrl = this.downloadCurrent.updateSetupUrl; - var hashUrl = this.downloadCurrent.updateHashUrl; - var size = this.downloadCurrent.size; - await AssetBundleCacher.DownloadFile(fileUrl, hashUrl, size); - this.downloadFileProgress(); + return false; }; - $app.methods.cancelDownload = function (id) { - AssetBundleCacher.CancelDownload(); - if (this.downloadQueue.has(id)) { - this.downloadQueue.delete(id); - this.downloadQueueTable.data = Array.from( - this.downloadQueue.values() - ); + $app.methods.onFileChangeVRCPlusIcon = function (e) { + var clearFile = function () { + if (document.querySelector('#VRCPlusIconUploadButton')) { + document.querySelector('#VRCPlusIconUploadButton').value = ''; + } + }; + var files = e.target.files || e.dataTransfer.files; + if (!files.length) { + return; } - }; - - $app.methods.cancelAllDownloads = function () { - if (typeof this.downloadCurrent.id !== 'undefined') { - this.cancelDownload(this.downloadCurrent.id); + if (files[0].size >= 100000000) { + // 100MB + $app.$message({ + message: 'File size too large', + type: 'error' + }); + clearFile(); + return; } - for (var queue of this.downloadQueue.values()) { - this.cancelDownload(queue.ref.id); + if (!files[0].type.match(/image.*/)) { + $app.$message({ + message: "File isn't an image", + type: 'error' + }); + clearFile(); + return; } - }; - - $app.data.downloadProgress = 0; - $app.data.downloadInProgress = false; - $app.data.downloadIsProcessing = false; - $app.data.downloadQueue = new Map(); - $app.data.downloadCurrent = {}; - - $app.methods.downloadFileProgress = async function () { - var downloadProgress = await AssetBundleCacher.CheckDownloadProgress(); - switch (downloadProgress) { - case -4: - this.$message({ - message: 'Download canceled', - type: 'info' - }); - this.downloadFileComplete('Canceled'); - return; - case -14: - this.$message({ - message: 'Download failed, hash mismatch', - type: 'error' - }); - this.downloadFileComplete('Failed'); - return; - case -15: - this.$message({ - message: 'Download failed, size mismatch', - type: 'error' + var r = new FileReader(); + r.onload = function () { + var base64Body = btoa(r.result); + API.uploadVRCPlusIcon(base64Body).then((args) => { + $app.$message({ + message: 'Icon uploaded', + type: 'success' }); - this.downloadFileComplete('Failed'); - return; - case -16: - if (this.downloadCurrent.ref.id === 'VRCXUpdate') { - this.downloadDialog.visible = false; - this.pendingVRCXInstall = this.downloadCurrent.ref.name; - this.showVRCXUpdateDialog(); - } - this.downloadFileComplete('Success'); - return; - default: - this.downloadProgress = downloadProgress; - } - workerTimers.setTimeout(() => this.downloadFileProgress(), 150); + return args; + }); + }; + r.readAsBinaryString(files[0]); + clearFile(); }; - $app.methods.downloadFileComplete = function (status) { - this.downloadCurrent.status = status; - this.downloadCurrent.date = Date.now(); - this.downloadHistoryTable.data.unshift(this.downloadCurrent); - this.downloadCurrent = {}; - this.downloadProgress = 0; - this.downloadInProgress = false; - this.downloadFileQueueUpdate(); + $app.methods.displayVRCPlusIconUpload = function () { + document.getElementById('VRCPlusIconUploadButton').click(); }; - $app.methods.showDownloadDialog = function () { - this.$nextTick(() => adjustDialogZ(this.$refs.downloadDialog.$el)); - this.downloadDialog.visible = true; + API.uploadVRCPlusIcon = function (imageData) { + var params = { + tag: 'icon' + }; + return this.call('file/image', { + uploadImage: true, + postData: JSON.stringify(params), + imageData + }).then((json) => { + var args = { + json, + params + }; + this.$emit('VRCPLUSICON:ADD', args); + return args; + }); }; - $app.data.downloadDialog = { - visible: false - }; + API.$on('VRCPLUSICON:ADD', function (args) { + if (Object.keys($app.VRCPlusIconsTable).length !== 0) { + $app.VRCPlusIconsTable.unshift(args.json); + } + }); - $app.methods.downloadProgressText = function () { - if (this.downloadIsProcessing) { - return 'Processing'; + $app.data.uploadImage = ''; + + $app.methods.inviteImageUpload = function (e) { + var files = e.target.files || e.dataTransfer.files; + if (!files.length) { + return; } - if (this.downloadProgress >= 0) { - return `${this.downloadProgress}%`; + if (files[0].size >= 100000000) { + // 100MB + $app.$message({ + message: 'File size too large', + type: 'error' + }); + this.clearInviteImageUpload(); + return; } - return ''; + if (!files[0].type.match(/image.*/)) { + $app.$message({ + message: "File isn't a png", + type: 'error' + }); + this.clearInviteImageUpload(); + return; + } + var r = new FileReader(); + r.onload = function () { + $app.uploadImage = btoa(r.result); + }; + r.readAsBinaryString(files[0]); }; - $app.methods.getDisplayName = function (userId) { - if (userId) { - var ref = API.cachedUsers.get(userId); - if (ref.displayName) { - return ref.displayName; - } - } - return ''; + $app.methods.clearInviteImageUpload = function () { + this.clearImageGallerySelect(); + var buttonList = document.querySelectorAll('.inviteImageUploadButton'); + buttonList.forEach((button) => (button.value = '')); + this.uploadImage = ''; }; - $app.methods.deleteVRChatCache = async function (ref) { - var assetUrl = ''; - var variant = ''; - for (var i = ref.unityPackages.length - 1; i > -1; i--) { - var unityPackage = ref.unityPackages[i]; - if ( - unityPackage.variant && - unityPackage.variant !== 'standard' && - unityPackage.variant !== 'security' - ) { - continue; - } - if ( - unityPackage.platform === 'standalonewindows' && - this.compareUnityVersion(unityPackage.unitySortNumber) - ) { - assetUrl = unityPackage.assetUrl; - if (unityPackage.variant !== 'standard') { - variant = unityPackage.variant; - } - break; - } + $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); } - var id = extractFileId(assetUrl); - var version = parseInt(extractFileVersion(assetUrl), 10); - var variantVersion = parseInt(extractVariantVersion(assetUrl), 10); - await AssetBundleCacher.DeleteCache( - id, - version, - variant, - variantVersion - ); - this.getVRChatCacheSize(); - this.updateVRChatWorldCache(); - this.updateVRChatAvatarCache(); + return '-'; }; - $app.methods.showDeleteAllVRChatCacheConfirm = function () { - this.$confirm(`Continue? Delete all VRChat cache`, 'Confirm', { - confirmButtonText: 'Confirm', - cancelButtonText: 'Cancel', - type: 'info', - callback: (action) => { - if (action === 'confirm') { - this.deleteAllVRChatCache(); - } - } - }); + $app.methods.userOnlineForTimestamp = function (ctx) { + if (ctx.ref.state === 'online' && ctx.ref.$online_for) { + return ctx.ref.$online_for; + } else if (ctx.ref.state === 'active' && ctx.ref.$active_for) { + return ctx.ref.$active_for; + } else if (ctx.ref.$offline_for) { + return ctx.ref.$offline_for; + } + return 0; }; - $app.methods.deleteAllVRChatCache = async function () { - await AssetBundleCacher.DeleteAllCache(); - this.getVRChatCacheSize(); - }; + // #endregion + // #region | App: Invite Messages - $app.methods.autoVRChatCacheManagement = function () { - if (this.autoSweepVRChatCache) { - this.sweepVRChatCache(); - } - }; + 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; + }); - $app.methods.sweepVRChatCache = async function () { - await AssetBundleCacher.SweepCache(); - if (this.VRChatConfigDialog.visible) { - this.getVRChatCacheSize(); - } + $app.methods.refreshInviteMessageTable = function (messageType) { + API.refreshInviteMessageTableData(messageType); }; - $app.methods.checkIfGameCrashed = function () { - if (!this.relaunchVRChatAfterCrash) { - return; - } - var { location } = this.lastLocation; - AppApi.VrcClosedGracefully().then((result) => { - if (result || !this.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 - ); + API.refreshInviteMessageTableData = function (messageType) { + return this.call(`message/${this.currentUser.id}/${messageType}`, { + method: 'GET' + }).then((json) => { + var args = { + json, + messageType + }; + this.$emit(`INVITE:${messageType.toUpperCase()}`, args); + return args; }); }; - $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' + 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; + }); + + API.editInviteMessage = function (params, messageType, slot) { + return this.call( + `message/${this.currentUser.id}/${messageType}/${slot}`, + { + method: 'PUT', + params + } + ).then((json) => { + var args = { + json, + params, + messageType, + slot + }; + return args; }); - 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; + // #endregion + // #region | App: Edit Invite Message Dialog - $app.methods.getVRChatCacheSize = async function () { - this.VRChatCacheSizeLoading = true; - var totalCacheSize = 20; - if (this.VRChatConfigFile.cache_size) { - totalCacheSize = this.VRChatConfigFile.cache_size; - } - this.VRChatTotalCacheSize = totalCacheSize; - var usedCacheSize = await AssetBundleCacher.GetCacheSize(); - this.VRChatUsedCacheSize = (usedCacheSize / 1073741824).toFixed(2); - this.VRChatCacheSizeLoading = false; + $app.data.editInviteMessageDialog = { + visible: false, + inviteMessage: {}, + messageType: '', + newMessage: '' }; - $app.methods.getBundleLocation = async function (input) { - var assetUrl = input; - var variant = ''; - if (assetUrl) { - // continue - } else if ( - this.avatarDialog.visible && - this.avatarDialog.ref.unityPackages.length > 0 - ) { - var unityPackages = this.avatarDialog.ref.unityPackages; - for (let i = unityPackages.length - 1; i > -1; i--) { - var unityPackage = unityPackages[i]; - if ( - unityPackage.variant && - unityPackage.variant !== 'standard' && - unityPackage.variant !== 'security' - ) { - continue; - } - if ( - unityPackage.platform === 'standalonewindows' && - this.compareUnityVersion(unityPackage.unitySortNumber) - ) { - assetUrl = unityPackage.assetUrl; - if (unityPackage.variant !== 'standard') { - variant = unityPackage.variant; - } - break; - } - } - } else if ( - this.avatarDialog.visible && - this.avatarDialog.ref.assetUrl - ) { - assetUrl = this.avatarDialog.ref.assetUrl; - } else if ( - this.worldDialog.visible && - this.worldDialog.ref.unityPackages.length > 0 - ) { - var unityPackages = this.worldDialog.ref.unityPackages; - for (let i = unityPackages.length - 1; i > -1; i--) { - var unityPackage = unityPackages[i]; - if ( - unityPackage.platform === 'standalonewindows' && - this.compareUnityVersion(unityPackage.unitySortNumber) - ) { - assetUrl = unityPackage.assetUrl; - break; - } - } - } else if (this.worldDialog.visible && this.worldDialog.ref.assetUrl) { - assetUrl = this.worldDialog.ref.assetUrl; - } - if (!assetUrl) { - return null; - } - var fileId = extractFileId(assetUrl); - var fileVersion = parseInt(extractFileVersion(assetUrl), 10); - var variantVersion = parseInt(extractVariantVersion(assetUrl), 10); - var assetLocation = await AssetBundleCacher.GetVRChatCacheFullLocation( - fileId, - fileVersion, - variant, - variantVersion - ); - var cacheInfo = await AssetBundleCacher.CheckVRChatCache( - fileId, - fileVersion, - variant, - variantVersion + $app.methods.showEditInviteMessageDialog = function ( + messageType, + inviteMessage + ) { + this.$nextTick(() => + $app.adjustDialogZ(this.$refs.editInviteMessageDialog.$el) ); - var inCache = false; - if (cacheInfo.Item1 > 0) { - inCache = true; - } - console.log(`InCache: ${inCache}`); - var fullAssetLocation = `${assetLocation}\\__data`; - console.log(fullAssetLocation); - return fullAssetLocation; + var D = this.editInviteMessageDialog; + D.newMessage = inviteMessage.message; + D.visible = true; + D.inviteMessage = inviteMessage; + D.messageType = messageType; }; - API.$on('LOGIN', function () { - $app.downloadDialog.visible = 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; + $app.methods.saveEditInviteMessage = function () { + var D = this.editInviteMessageDialog; + D.visible = false; + if (D.inviteMessage.message !== D.newMessage) { + var slot = D.inviteMessage.slot; + var messageType = D.messageType; + var params = { + message: D.newMessage + }; + API.editInviteMessage(params, messageType, slot) + .catch((err) => { + throw err; + }) + .then((args) => { + API.$emit(`INVITE:${messageType.toUpperCase()}`, args); + if (args.json[slot].message === D.inviteMessage.message) { + this.$message({ + message: + "VRChat API didn't update message, try again", + type: 'error' + }); + throw new Error( + "VRChat API didn't update message, try again" + ); + } else { + this.$message('Invite message updated'); + } + return args; + }); } - return void 0; }; - // Parse Avatar URL - - $app.methods.parseAvatarUrl = function (avatar) { - var url = new URL(avatar); - var urlPath = url.pathname; - if (urlPath.substring(5, 13) === '/avatar/') { - var avatarId = urlPath.substring(13); - return avatarId; - } - return void 0; + $app.methods.cancelEditInviteMessage = function () { + this.editInviteMessageDialog.visible = false; }; - // userDialog Favorite Worlds + // #endregion + // #region | App: Edit and Send Invite Response Message Dialog - $app.data.userFavoriteWorlds = []; + $app.data.editAndSendInviteResponseDialog = { + visible: false, + inviteMessage: {}, + messageType: '', + newMessage: '' + }; - $app.methods.getUserFavoriteWorlds = async function (userId) { - this.userDialog.isFavoriteWorldsLoading = true; - this.$refs.favoriteWorlds.currentName = '0'; // select first tab - this.userFavoriteWorlds = []; - var worldLists = []; - var params = { - ownerId: userId, - n: 100 + $app.methods.showEditAndSendInviteResponseDialog = function ( + messageType, + inviteMessage + ) { + this.$nextTick(() => + $app.adjustDialogZ(this.$refs.editAndSendInviteResponseDialog.$el) + ); + this.editAndSendInviteResponseDialog = { + newMessage: inviteMessage.message, + visible: true, + messageType, + inviteMessage }; - var json = await API.call('favorite/groups', { - method: 'GET', - params - }); - for (var i = 0; i < json.length; ++i) { - var list = json[i]; - if (list.type !== 'world') { - continue; - } + }; + + $app.methods.saveEditAndSendInviteResponse = async function () { + var D = this.editAndSendInviteResponseDialog; + D.visible = false; + var messageType = D.messageType; + var slot = D.inviteMessage.slot; + if (D.inviteMessage.message !== D.newMessage) { var params = { - n: 100, - offset: 0, - userId, - tag: list.name + message: D.newMessage }; - try { - var args = await API.getFavoriteWorlds(params); - worldLists.push([list.displayName, list.visibility, args.json]); - } catch (err) {} + await API.editInviteMessage(params, messageType, slot) + .catch((err) => { + throw err; + }) + .then((args) => { + API.$emit(`INVITE:${messageType.toUpperCase()}`, args); + if (args.json[slot].message === D.inviteMessage.message) { + this.$message({ + message: + "VRChat API didn't update message, try again", + type: 'error' + }); + throw new Error( + "VRChat API didn't update message, try again" + ); + } else { + this.$message('Invite message updated'); + } + return args; + }); } - this.userFavoriteWorlds = worldLists; - this.userDialog.isFavoriteWorldsLoading = false; - }; - - $app.data.worldGroupVisibilityOptions = ['private', 'friends', 'public']; - - $app.methods.userFavoriteWorldsStatus = function (visibility) { - var style = {}; - if (visibility === 'public') { - style.online = true; - } else if (visibility === 'friends') { - style.joinme = true; + var I = this.sendInviteResponseDialog; + var params = { + responseSlot: slot, + rsvp: true + }; + if ($app.uploadImage) { + API.sendInviteResponsePhoto(params, I.invite.id) + .catch((err) => { + throw err; + }) + .then((args) => { + API.hideNotification({ + notificationId: I.invite.id + }); + this.$message({ + message: 'Invite response message sent', + type: 'success' + }); + this.sendInviteResponseDialogVisible = false; + this.sendInviteRequestResponseDialogVisible = false; + return args; + }); } else { - style.busy = true; + API.sendInviteResponse(params, I.invite.id) + .catch((err) => { + throw err; + }) + .then((args) => { + API.hideNotification({ + notificationId: I.invite.id + }); + this.$message({ + message: 'Invite response message sent', + type: 'success' + }); + this.sendInviteResponseDialogVisible = false; + this.sendInviteRequestResponseDialogVisible = false; + return args; + }); } - return style; }; - $app.methods.changeWorldGroupVisibility = function (name, visibility) { - var params = { - type: 'world', - group: name, - visibility - }; - API.saveFavoriteGroup(params).then((args) => { - this.$message({ - message: 'Group visibility changed', - type: 'success' - }); - return args; - }); + $app.methods.cancelEditAndSendInviteResponse = function () { + this.editAndSendInviteResponseDialog.visible = false; }; - $app.methods.refreshInstancePlayerCount = function (instance) { - var L = API.parseLocation(instance); - if (L.worldId && L.instanceId) { - API.getInstance({ - worldId: L.worldId, - instanceId: L.instanceId - }); - } + $app.data.sendInviteResponseDialog = { + message: '', + messageSlot: 0, + invite: {} }; - // userDialog Groups + $app.data.sendInviteResponseDialogVisible = false; - $app.data.userGroups = { - groups: [], - ownGroups: [], - mutualGroups: [], - remainingGroups: [] + $app.data.sendInviteResponseConfirmDialog = { + visible: false }; - $app.methods.getUserGroups = async function (userId) { - this.userDialog.isGroupsLoading = true; - this.userGroups = { - groups: [], - ownGroups: [], - mutualGroups: [], - remainingGroups: [] + API.$on('LOGIN', function () { + $app.sendInviteResponseDialogVisible = false; + $app.sendInviteResponseConfirmDialog.visible = false; + }); + + $app.methods.showSendInviteResponseDialog = function (invite) { + this.sendInviteResponseDialog = { + invite }; - var args = await API.getGroups({ userId }); - if (userId !== this.userDialog.id) { - this.userDialog.isGroupsLoading = false; + API.refreshInviteMessageTableData('response'); + this.$nextTick(() => + $app.adjustDialogZ(this.$refs.sendInviteResponseDialog.$el) + ); + this.clearInviteImageUpload(); + this.sendInviteResponseDialogVisible = true; + }; + + $app.methods.showSendInviteResponseConfirmDialog = function (val) { + if ( + this.editAndSendInviteResponseDialog.visible === true || + val === null + ) { return; } - if (userId === API.currentUser.id) { - // update current user groups - API.currentUserGroups.clear(); - args.json.forEach((group) => { - var ref = API.applyGroup(group); - API.currentUserGroups.set(group.id, ref); - }); - this.saveCurrentUserGroups(); - } - this.userGroups.groups = args.json; - for (var i = 0; i < args.json.length; ++i) { - var group = args.json[i]; - if (!group?.id) { - console.error('getUserGroups, group ID is missing', group); - continue; - } - if (group.ownerId === userId) { - this.userGroups.ownGroups.unshift(group); - } - if (userId === API.currentUser.id) { - // skip mutual groups for current user - if (group.ownerId !== userId) { - this.userGroups.remainingGroups.unshift(group); - } - continue; - } - if (group.mutualGroup) { - this.userGroups.mutualGroups.unshift(group); - } - if (!group.mutualGroup && group.ownerId !== userId) { - this.userGroups.remainingGroups.unshift(group); - } - } - this.userDialog.isGroupsLoading = false; - if (userId === API.currentUser.id) { - this.sortCurrentUserGroups(); - } + this.$nextTick(() => + $app.adjustDialogZ(this.$refs.sendInviteResponseConfirmDialog.$el) + ); + this.sendInviteResponseConfirmDialog.visible = true; + this.sendInviteResponseDialog.messageSlot = val.slot; }; - $app.methods.getCurrentUserGroups = async function () { - var args = await API.getGroups({ userId: API.currentUser.id }); - API.currentUserGroups.clear(); - for (var group of args.json) { - var ref = API.applyGroup(group); - API.currentUserGroups.set(group.id, ref); - } - await API.getGroupPermissions({ userId: API.currentUser.id }); - this.saveCurrentUserGroups(); + $app.methods.cancelSendInviteResponse = function () { + this.sendInviteResponseDialogVisible = false; }; - $app.methods.sortCurrentUserGroups = function () { - var groupList = []; - var sortGroups = function (a, b) { - var aIndex = groupList.indexOf(a?.id); - var bIndex = groupList.indexOf(b?.id); - if (aIndex === -1 && bIndex === -1) { - return 0; - } - if (aIndex === -1) { - return 1; - } - if (bIndex === -1) { - return -1; - } - return aIndex - bIndex; + $app.methods.cancelInviteResponseConfirm = function () { + this.sendInviteResponseConfirmDialog.visible = false; + }; + + $app.methods.sendInviteResponseConfirm = function () { + var D = this.sendInviteResponseDialog; + var params = { + responseSlot: D.messageSlot, + rsvp: true }; - AppApi.GetVRChatRegistryKey( - `VRC_GROUP_ORDER_${API.currentUser.id}` - ).then((json) => { - groupList = JSON.parse(json); - this.userGroups.remainingGroups.sort(sortGroups); - }); + if ($app.uploadImage) { + API.sendInviteResponsePhoto(params, D.invite.id, D.messageType) + .catch((err) => { + throw err; + }) + .then((args) => { + API.hideNotification({ + notificationId: D.invite.id + }); + this.$message({ + message: 'Invite response photo message sent', + type: 'success' + }); + return args; + }); + } else { + API.sendInviteResponse(params, D.invite.id, D.messageType) + .catch((err) => { + throw err; + }) + .then((args) => { + API.hideNotification({ + notificationId: D.invite.id + }); + this.$message({ + message: 'Invite response message sent', + type: 'success' + }); + return args; + }); + } + this.sendInviteResponseDialogVisible = false; + this.sendInviteRequestResponseDialogVisible = false; + this.sendInviteResponseConfirmDialog.visible = false; }; // #endregion - // #region | Gallery + // #region | App: Invite Request Response Message Dialog - $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.sendInviteRequestResponseDialogVisible = false; + + $app.methods.cancelSendInviteRequestResponse = function () { + this.sendInviteRequestResponseDialogVisible = false; + }; API.$on('LOGIN', function () { - $app.galleryTable = []; + $app.sendInviteRequestResponseDialogVisible = false; + $app.showSendInviteResponseConfirmDialog.visible = false; }); - $app.methods.showGalleryDialog = function (pageNum) { - this.$nextTick(() => adjustDialogZ(this.$refs.galleryDialog.$el)); - this.galleryDialogVisible = true; - this.refreshGalleryTable(); - this.refreshVRCPlusIconsTable(); - this.refreshEmojiTable(); - this.refreshStickerTable(); - workerTimers.setTimeout(() => this.setGalleryTab(pageNum), 100); + $app.methods.showSendInviteRequestResponseDialog = function (invite) { + this.sendInviteResponseDialog = { + invite + }; + API.refreshInviteMessageTableData('requestResponse'); + this.$nextTick(() => + $app.adjustDialogZ(this.$refs.sendInviteRequestResponseDialog.$el) + ); + this.clearInviteImageUpload(); + this.sendInviteRequestResponseDialogVisible = true; }; - $app.methods.setGalleryTab = function (pageNum) { - if ( - typeof pageNum !== 'undefined' && - typeof this.$refs.galleryTabs !== 'undefined' - ) { - this.$refs.galleryTabs.setCurrentName(`${pageNum}`); + // #endregion + // #region | App: Invite Message Dialog + + $app.data.editAndSendInviteDialog = { + visible: false, + messageType: '', + newMessage: '', + inviteMessage: {} + }; + + $app.methods.showEditAndSendInviteDialog = function ( + messageType, + inviteMessage + ) { + this.$nextTick(() => + $app.adjustDialogZ(this.$refs.editAndSendInviteDialog.$el) + ); + this.editAndSendInviteDialog = { + newMessage: inviteMessage.message, + visible: true, + messageType, + inviteMessage + }; + }; + + $app.methods.saveEditAndSendInvite = async function () { + var D = this.editAndSendInviteDialog; + D.visible = false; + var messageType = D.messageType; + var slot = D.inviteMessage.slot; + if (D.inviteMessage.message !== D.newMessage) { + var params = { + message: D.newMessage + }; + await API.editInviteMessage(params, messageType, slot) + .catch((err) => { + throw err; + }) + .then((args) => { + API.$emit(`INVITE:${messageType.toUpperCase()}`, args); + if (args.json[slot].message === D.inviteMessage.message) { + this.$message({ + message: + "VRChat API didn't update message, try again", + type: 'error' + }); + throw new Error( + "VRChat API didn't update message, try again" + ); + } else { + this.$message('Invite message updated'); + } + return args; + }); + } + var I = this.sendInviteDialog; + var J = this.inviteDialog; + if (J.visible) { + var inviteLoop = () => { + if (J.userIds.length > 0) { + var receiverUserId = J.userIds.shift(); + if (receiverUserId === API.currentUser.id) { + // can't invite self!? + var L = $utils.parseLocation(J.worldId); + API.selfInvite({ + instanceId: L.instanceId, + worldId: L.worldId + }).finally(inviteLoop); + } else if ($app.uploadImage) { + API.sendInvitePhoto( + { + instanceId: J.worldId, + worldId: J.worldId, + worldName: J.worldName, + messageSlot: slot + }, + receiverUserId + ).finally(inviteLoop); + } else { + API.sendInvite( + { + instanceId: J.worldId, + worldId: J.worldId, + worldName: J.worldName, + messageSlot: slot + }, + receiverUserId + ).finally(inviteLoop); + } + } else { + J.loading = false; + J.visible = false; + this.$message({ + message: 'Invite sent', + type: 'success' + }); + } + }; + inviteLoop(); + } else if (I.messageType === 'invite') { + I.params.messageSlot = slot; + if ($app.uploadImage) { + API.sendInvitePhoto(I.params, I.userId) + .catch((err) => { + throw err; + }) + .then((args) => { + this.$message({ + message: 'Invite photo message sent', + type: 'success' + }); + return args; + }); + } else { + API.sendInvite(I.params, I.userId) + .catch((err) => { + throw err; + }) + .then((args) => { + this.$message({ + message: 'Invite message sent', + type: 'success' + }); + return args; + }); + } + } else if (I.messageType === 'requestInvite') { + I.params.requestSlot = slot; + if ($app.uploadImage) { + API.sendRequestInvitePhoto(I.params, I.userId) + .catch((err) => { + this.clearInviteImageUpload(); + throw err; + }) + .then((args) => { + this.$message({ + message: 'Request invite photo message sent', + type: 'success' + }); + return args; + }); + } else { + API.sendRequestInvite(I.params, I.userId) + .catch((err) => { + throw err; + }) + .then((args) => { + this.$message({ + message: 'Request invite message sent', + type: 'success' + }); + return args; + }); + } } + this.sendInviteDialogVisible = false; + this.sendInviteRequestDialogVisible = false; }; - $app.methods.refreshGalleryTable = function () { - this.galleryDialogGalleryLoading = true; - var params = { - n: 100, - tag: 'gallery' - }; - API.getFileList(params); + $app.methods.cancelEditAndSendInvite = function () { + this.editAndSendInviteDialog.visible = false; }; - API.$on('FILES:LIST', function (args) { - if (args.params.tag === 'gallery') { - $app.galleryTable = args.json.reverse(); - $app.galleryDialogGalleryLoading = false; - } - }); - - $app.methods.setProfilePicOverride = function (fileId) { - if (!API.currentUser.$isVRCPlus) { - this.$message({ - message: 'VRCPlus required', - type: 'error' - }); - return; - } - var profilePicOverride = ''; - if (fileId) { - profilePicOverride = `${API.endpointDomain}/file/${fileId}/1`; - } - if (profilePicOverride === API.currentUser.profilePicOverride) { - return; - } - API.saveCurrentUser({ - profilePicOverride - }).then((args) => { - this.$message({ - message: 'Profile picture changed', - type: 'success' - }); - return args; - }); + $app.data.sendInviteDialog = { + message: '', + messageSlot: 0, + userId: '', + messageType: '', + params: {} }; - $app.methods.deleteGalleryImage = function (fileId) { - API.deleteFile(fileId).then((args) => { - API.$emit('GALLERYIMAGE:DELETE', args); - return args; - }); + $app.data.sendInviteDialogVisible = false; + + $app.data.sendInviteConfirmDialog = { + visible: false }; - API.$on('GALLERYIMAGE:DELETE', function (args) { - var array = $app.galleryTable; - var { length } = array; - for (var i = 0; i < length; ++i) { - if (args.fileId === array[i].id) { - array.splice(i, 1); - break; - } - } + API.$on('LOGIN', function () { + $app.sendInviteDialogVisible = false; + $app.sendInviteConfirmDialog.visible = false; }); - $app.methods.compareCurrentProfilePic = function (fileId) { - var currentProfilePicOverride = extractFileId( - API.currentUser.profilePicOverride + $app.methods.showSendInviteDialog = function (params, userId) { + this.sendInviteDialog = { + params, + userId, + messageType: 'invite' + }; + API.refreshInviteMessageTableData('message'); + this.$nextTick(() => + $app.adjustDialogZ(this.$refs.sendInviteDialog.$el) ); - if (fileId === currentProfilePicOverride) { - return true; - } - return false; + this.clearInviteImageUpload(); + this.sendInviteDialogVisible = true; }; - $app.methods.onFileChangeGallery = function (e) { - var clearFile = function () { - if (document.querySelector('#GalleryUploadButton')) { - document.querySelector('#GalleryUploadButton').value = ''; - } - }; - var files = e.target.files || e.dataTransfer.files; - if (!files.length) { - return; - } - if (files[0].size >= 100000000) { - // 100MB - $app.$message({ - message: 'File size too large', - type: 'error' - }); - clearFile(); - return; - } - if (!files[0].type.match(/image.*/)) { - $app.$message({ - message: "File isn't an image", - type: 'error' - }); - clearFile(); + $app.methods.showSendInviteConfirmDialog = function (val) { + if (this.editAndSendInviteDialog.visible === true || val === null) { return; } - var r = new FileReader(); - r.onload = function () { - var base64Body = btoa(r.result); - API.uploadGalleryImage(base64Body).then((args) => { - $app.$message({ - message: 'Gallery image uploaded', - type: 'success' - }); - return args; - }); - }; - r.readAsBinaryString(files[0]); - clearFile(); + this.$nextTick(() => + $app.adjustDialogZ(this.$refs.sendInviteConfirmDialog.$el) + ); + this.sendInviteConfirmDialog.visible = true; + this.sendInviteDialog.messageSlot = val.slot; }; - $app.methods.displayGalleryUpload = function () { - document.getElementById('GalleryUploadButton').click(); + $app.methods.cancelSendInvite = function () { + this.sendInviteDialogVisible = false; }; - API.uploadGalleryImage = function (imageData) { - var params = { - tag: 'gallery' - }; - return this.call('file/image', { - uploadImage: true, - postData: JSON.stringify(params), - imageData - }).then((json) => { - var args = { - json, - params - }; - this.$emit('GALLERYIMAGE:ADD', args); - return args; - }); + $app.methods.cancelInviteConfirm = function () { + this.sendInviteConfirmDialog.visible = false; }; - API.$on('GALLERYIMAGE:ADD', function (args) { - if (Object.keys($app.galleryTable).length !== 0) { - $app.galleryTable.unshift(args.json); + $app.methods.sendInviteConfirm = function () { + var D = this.sendInviteDialog; + var J = this.inviteDialog; + if (J.visible) { + var inviteLoop = () => { + if (J.userIds.length > 0) { + var receiverUserId = J.userIds.shift(); + if (receiverUserId === API.currentUser.id) { + // can't invite self!? + var L = $utils.parseLocation(J.worldId); + API.selfInvite({ + instanceId: L.instanceId, + worldId: L.worldId + }).finally(inviteLoop); + } else if ($app.uploadImage) { + API.sendInvitePhoto( + { + instanceId: J.worldId, + worldId: J.worldId, + worldName: J.worldName, + messageSlot: D.messageSlot + }, + receiverUserId + ).finally(inviteLoop); + } else { + API.sendInvite( + { + instanceId: J.worldId, + worldId: J.worldId, + worldName: J.worldName, + messageSlot: D.messageSlot + }, + receiverUserId + ).finally(inviteLoop); + } + } else { + J.loading = false; + J.visible = false; + this.$message({ + message: 'Invite message sent', + type: 'success' + }); + } + }; + inviteLoop(); + } else if (D.messageType === 'invite') { + D.params.messageSlot = D.messageSlot; + if ($app.uploadImage) { + API.sendInvitePhoto(D.params, D.userId) + .catch((err) => { + throw err; + }) + .then((args) => { + this.$message({ + message: 'Invite photo message sent', + type: 'success' + }); + return args; + }); + } else { + API.sendInvite(D.params, D.userId) + .catch((err) => { + throw err; + }) + .then((args) => { + this.$message({ + message: 'Invite message sent', + type: 'success' + }); + return args; + }); + } + } else if (D.messageType === 'requestInvite') { + D.params.requestSlot = D.messageSlot; + if ($app.uploadImage) { + API.sendRequestInvitePhoto(D.params, D.userId) + .catch((err) => { + this.clearInviteImageUpload(); + throw err; + }) + .then((args) => { + this.$message({ + message: 'Request invite photo message sent', + type: 'success' + }); + return args; + }); + } else { + API.sendRequestInvite(D.params, D.userId) + .catch((err) => { + throw err; + }) + .then((args) => { + this.$message({ + message: 'Request invite message sent', + type: 'success' + }); + return args; + }); + } } - }); + this.sendInviteDialogVisible = false; + this.sendInviteRequestDialogVisible = false; + this.sendInviteConfirmDialog.visible = false; + }; // #endregion - // #region | Sticker - API.$on('LOGIN', function () { - $app.stickerTable = []; - }); + // #region | App: Invite Request Message Dialog - $app.methods.refreshStickerTable = function () { - this.galleryDialogStickersLoading = true; - var params = { - n: 100, - tag: 'sticker' - }; - API.getFileList(params); + $app.data.sendInviteRequestDialogVisible = false; + + $app.methods.cancelSendInviteRequest = function () { + this.sendInviteRequestDialogVisible = false; }; - API.$on('FILES:LIST', function (args) { - if (args.params.tag === 'sticker') { - $app.stickerTable = args.json.reverse(); - $app.galleryDialogStickersLoading = false; - } + API.$on('LOGIN', function () { + $app.sendInviteRequestDialogVisible = false; + $app.showSendInviteConfirmDialog.visible = false; }); - $app.methods.deleteSticker = function (fileId) { - API.deleteFile(fileId).then((args) => { - API.$emit('STICKER:DELETE', args); - return args; - }); + $app.methods.showSendInviteRequestDialog = function (params, userId) { + this.sendInviteDialog = { + params, + userId, + messageType: 'requestInvite' + }; + API.refreshInviteMessageTableData('request'); + this.$nextTick(() => + $app.adjustDialogZ(this.$refs.sendInviteRequestDialog.$el) + ); + this.clearInviteImageUpload(); + this.sendInviteRequestDialogVisible = true; }; - API.$on('STICKER:DELETE', function (args) { - var array = $app.stickerTable; - var { length } = array; - for (var i = 0; i < length; ++i) { - if (args.fileId === array[i].id) { - array.splice(i, 1); - break; - } - } + // #endregion + // #region | App: Friends List + + API.$on('LOGIN', function () { + $app.friendsListTable.data = []; }); - $app.methods.onFileChangeSticker = function (e) { - var clearFile = function () { - if (document.querySelector('#StickerUploadButton')) { - document.querySelector('#StickerUploadButton').value = ''; - } - }; - var files = e.target.files || e.dataTransfer.files; - if (!files.length) { + $app.methods.selectFriendsListRow = function (val) { + if (val === null) { return; } - if (files[0].size >= 100000000) { - // 100MB - $app.$message({ - message: 'File size too large', - type: 'error' - }); - clearFile(); + if (!val.id) { + this.lookupUser(val); return; } - if (!files[0].type.match(/image.*/)) { - $app.$message({ - message: "File isn't an image", - type: 'error' + this.showUserDialog(val.id); + }; + + $app.data.friendsListSearch = ''; + $app.data.friendsListSearchFilterVIP = false; + $app.data.friendsListSearchFilters = []; + $app.data.friendsListSelectAllCheckbox = false; + $app.data.friendsListBulkUnfriendMode = false; + $app.data.friendsListBulkUnfriendForceUpdate = 0; + + $app.methods.toggleFriendsListBulkUnfriendMode = function () { + if (!this.friendsListBulkUnfriendMode) { + this.friendsListTable.data.forEach((ref) => { + ref.$selected = false; }); - clearFile(); + } + }; + + $app.methods.showBulkUnfriendSelectionConfirm = function () { + var pendingUnfriendList = this.friendsListTable.data.reduce( + (acc, ctx) => { + if (ctx.$selected) { + acc.push(ctx.displayName); + } + return acc; + }, + [] + ); + var elementsTicked = pendingUnfriendList.length; + if (elementsTicked === 0) { return; } - var r = new FileReader(); - r.onload = function () { - var params = { - tag: 'sticker', - maskTag: 'square' - }; - var base64Body = btoa(r.result); - API.uploadSticker(base64Body, params).then((args) => { - $app.$message({ - message: 'Sticker uploaded', - type: 'success' - }); - return args; - }); - }; - r.readAsBinaryString(files[0]); - clearFile(); + this.$confirm( + `Are you sure you want to delete ${elementsTicked} friends? + This can negatively affect your trust rank, + This action cannot be undone.`, + `Delete ${elementsTicked} friends?`, + { + confirmButtonText: 'Confirm', + cancelButtonText: 'Cancel', + type: 'info', + showInput: true, + inputType: 'textarea', + inputValue: pendingUnfriendList.join('\r\n'), + callback: (action) => { + if (action === 'confirm') { + this.bulkUnfriendSelection(); + } + } + } + ); }; - $app.methods.displayStickerUpload = function () { - document.getElementById('StickerUploadButton').click(); + $app.methods.bulkUnfriendSelection = function () { + for (var ctx of this.friendsListTable.data) { + if (ctx.$selected) { + API.deleteFriend({ + userId: ctx.id + }); + } + } }; - API.uploadSticker = function (imageData, params) { - return this.call('file/image', { - uploadImage: true, - postData: JSON.stringify(params), - imageData - }).then((json) => { - var args = { - json, - params - }; - this.$emit('STICKER:ADD', args); - return args; - }); + // $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.friendsListSearchChange = function () { + this.friendsListTable.data = []; + var filters = [...this.friendsListSearchFilters]; + if (filters.length === 0) { + filters = ['Display Name', 'Rank', 'Status', 'Bio', 'Memo']; + } + var results = []; + if (this.friendsListSearch) { + var query = this.friendsListSearch; + var cleanedQuery = removeWhitespace(query); + } + + for (var ctx of this.friends.values()) { + if (typeof ctx.ref === 'undefined') { + continue; + } + if (typeof ctx.ref.$selected === 'undefined') { + ctx.ref.$selected = false; + } + if (this.friendsListSearchFilterVIP && !ctx.isVIP) { + continue; + } + if (query && filters) { + var match = false; + if ( + !match && + filters.includes('Display Name') && + ctx.ref.displayName + ) { + match = + localeIncludes( + ctx.ref.displayName, + cleanedQuery, + this.stringComparer + ) || + localeIncludes( + removeConfusables(ctx.ref.displayName), + cleanedQuery, + this.stringComparer + ); + } + if (!match && filters.includes('Memo') && ctx.memo) { + match = localeIncludes( + ctx.memo, + query, + this.stringComparer + ); + } + if (!match && filters.includes('Bio') && ctx.ref.bio) { + match = localeIncludes( + ctx.ref.bio, + query, + this.stringComparer + ); + } + if ( + !match && + filters.includes('Status') && + ctx.ref.statusDescription + ) { + match = localeIncludes( + ctx.ref.statusDescription, + query, + this.stringComparer + ); + } + if (!match && filters.includes('Rank') && ctx.ref.$friendNum) { + match = String(ctx.ref.$trustLevel) + .toUpperCase() + .includes(query.toUpperCase()); + } + if (!match) { + continue; + } + } + ctx.ref.$friendNum = ctx.no; + results.push(ctx.ref); + } + this.getAllUserStats(); + this.friendsListTable.data = results; }; - API.$on('STICKER:ADD', function (args) { - if (Object.keys($app.stickerTable).length !== 0) { - $app.stickerTable.unshift(args.json); + $app.methods.getAllUserStats = 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); + } } - }); - - // #endregion - // #region | Emoji - API.$on('LOGIN', function () { - $app.emojiTable = []; - }); + database.getAllUserStats(userIds, displayNames).then((data) => { + 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; + } + } - $app.methods.refreshEmojiTable = function () { - this.galleryDialogEmojisLoading = true; - var params = { - n: 100, - tag: 'emoji' - }; - API.getFileList(params); + var friend = friendListMap.get(item.userId); + if (!friend) { + friendListMap.set(item.userId, item); + continue; + } + 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.created_at; + ref.ref.$timeSpent = item.timeSpent; + } + } + }); }; - API.$on('FILES:LIST', function (args) { - if (args.params.tag === 'emoji') { - $app.emojiTable = args.json.reverse(); - $app.galleryDialogEmojisLoading = false; - } - }); - - $app.methods.deleteEmoji = function (fileId) { - API.deleteFile(fileId).then((args) => { - API.$emit('EMOJI:DELETE', args); - return args; - }); + $app.methods.getUserStats = async function (ctx) { + var ref = await database.getUserStats(ctx); + /* eslint-disable require-atomic-updates */ + ctx.$joinCount = ref.joinCount; + ctx.$lastSeen = ref.created_at; + ctx.$timeSpent = ref.timeSpent; + /* eslint-enable require-atomic-updates */ }; - API.$on('EMOJI:DELETE', function (args) { - var array = $app.emojiTable; - var { length } = array; - for (var i = 0; i < length; ++i) { - if (args.fileId === array[i].id) { - array.splice(i, 1); - break; - } - } - }); + $app.watch.friendsListSearch = $app.methods.friendsListSearchChange; + $app.data.friendsListLoading = false; + $app.data.friendsListLoadingProgress = ''; - $app.methods.onFileChangeEmoji = function (e) { - var clearFile = function () { - if (document.querySelector('#EmojiUploadButton')) { - document.querySelector('#EmojiUploadButton').value = ''; + $app.methods.friendsListLoadUsers = async function () { + this.friendsListLoading = true; + var i = 0; + var toFetch = []; + for (var ctx of this.friends.values()) { + if (ctx.ref && !ctx.ref.date_joined) { + toFetch.push(ctx.id); } - }; - var files = e.target.files || e.dataTransfer.files; - if (!files.length) { - return; - } - if (files[0].size >= 100000000) { - // 100MB - $app.$message({ - message: 'File size too large', - type: 'error' - }); - clearFile(); - return; - } - if (!files[0].type.match(/image.*/)) { - $app.$message({ - message: "File isn't an image", - type: 'error' - }); - clearFile(); - return; } - var r = new FileReader(); - r.onload = function () { - var params = { - tag: $app.emojiAnimType ? 'emojianimated' : 'emoji', - animationStyle: $app.emojiAnimationStyle.toLowerCase(), - maskTag: 'square' - }; - if ($app.emojiAnimType) { - params.frames = $app.emojiAnimFrameCount; - params.framesOverTime = $app.emojiAnimFps; - } - if ($app.emojiAnimLoopPingPong) { - params.loopStyle = 'pingpong'; + var length = toFetch.length; + for (var userId of toFetch) { + if (!this.friendsListLoading) { + this.friendsListLoadingProgress = ''; + return; } - var base64Body = btoa(r.result); - API.uploadEmoji(base64Body, params).then((args) => { - $app.$message({ - message: 'Emoji uploaded', - type: 'success' - }); - return args; + i++; + this.friendsListLoadingProgress = `${i}/${length}`; + await API.getUser({ + userId }); - }; - r.readAsBinaryString(files[0]); - clearFile(); - }; - - $app.methods.displayEmojiUpload = function () { - document.getElementById('EmojiUploadButton').click(); - }; - - API.uploadEmoji = function (imageData, params) { - return this.call('file/image', { - uploadImage: true, - postData: JSON.stringify(params), - imageData - }).then((json) => { - var args = { - json, - params - }; - this.$emit('EMOJI:ADD', args); - return args; - }); + } + this.friendsListLoadingProgress = ''; + this.friendsListLoading = false; }; - API.$on('EMOJI:ADD', function (args) { - if (Object.keys($app.emojiTable).length !== 0) { - $app.emojiTable.unshift(args.json); + $app.methods.sortAlphabetically = function (a, b, field) { + if (!a[field] || !b[field]) { + return 0; } - }); + return a[field].toLowerCase().localeCompare(b[field].toLowerCase()); + }; - $app.data.emojiAnimFps = 15; - $app.data.emojiAnimFrameCount = 4; - $app.data.emojiAnimType = false; - $app.data.emojiAnimationStyle = 'Stop'; - $app.data.emojiAnimLoopPingPong = false; - $app.data.emojiAnimationStyleUrl = - 'https://assets.vrchat.com/www/images/emoji-previews/'; - $app.data.emojiAnimationStyleList = { - Aura: 'Preview_B2-Aura.gif', - Bats: 'Preview_B2-Fall_Bats.gif', - Bees: 'Preview_B2-Bees.gif', - Bounce: 'Preview_B2-Bounce.gif', - Cloud: 'Preview_B2-Cloud.gif', - Confetti: 'Preview_B2-Winter_Confetti.gif', - Crying: 'Preview_B2-Crying.gif', - Dislike: 'Preview_B2-Dislike.gif', - Fire: 'Preview_B2-Fire.gif', - Idea: 'Preview_B2-Idea.gif', - Lasers: 'Preview_B2-Lasers.gif', - Like: 'Preview_B2-Like.gif', - Magnet: 'Preview_B2-Magnet.gif', - Mistletoe: 'Preview_B2-Winter_Mistletoe.gif', - Money: 'Preview_B2-Money.gif', - Noise: 'Preview_B2-Noise.gif', - Orbit: 'Preview_B2-Orbit.gif', - Pizza: 'Preview_B2-Pizza.gif', - Rain: 'Preview_B2-Rain.gif', - Rotate: 'Preview_B2-Rotate.gif', - Shake: 'Preview_B2-Shake.gif', - Snow: 'Preview_B2-Spin.gif', - Snowball: 'Preview_B2-Winter_Snowball.gif', - Spin: 'Preview_B2-Spin.gif', - Splash: 'Preview_B2-SummerSplash.gif', - Stop: 'Preview_B2-Stop.gif', - ZZZ: 'Preview_B2-ZZZ.gif' + $app.methods.sortLanguages = function (a, b) { + var sortedA = []; + var sortedB = []; + a.$languages.forEach((item) => { + sortedA.push(item.value); + }); + b.$languages.forEach((item) => { + sortedB.push(item.value); + }); + sortedA.sort(); + sortedB.sort(); + return JSON.stringify(sortedA).localeCompare(JSON.stringify(sortedB)); }; - $app.methods.generateEmojiStyle = function ( - url, - fps, - frameCount, - loopStyle - ) { - let framesPerLine = 2; - if (frameCount > 4) framesPerLine = 4; - if (frameCount > 16) framesPerLine = 8; - const animationDurationMs = (1000 / fps) * frameCount; - const frameSize = 1024 / framesPerLine; - const scale = 100 / (frameSize / 200); - const animStyle = loopStyle === 'pingpong' ? 'alternate' : 'none'; - const style = ` - transform: scale(${scale / 100}); - transform-origin: top left; - width: ${frameSize}px; - height: ${frameSize}px; - background: url('${url}') 0 0; - animation: ${animationDurationMs}ms steps(1) 0s infinite ${animStyle} running animated-emoji-${frameCount}; - `; - return style; + $app.methods.genMd5 = async function (file) { + var response = await AppApi.MD5File(file); + return response; }; - // #endregion - // #region Misc + $app.methods.resizeImageToFitLimits = async function (file) { + var response = await AppApi.ResizeImageToFitLimits(file); + return response; + }; - $app.methods.replaceBioSymbols = function (text) { - if (!text) { - return ''; - } - var symbolList = { - '@': '@', - '#': '#', - $: '$', - '%': '%', - '&': '&', - '=': '=', - '+': '+', - '/': '⁄', - '\\': '\', - ';': ';', - ':': '˸', - ',': '‚', - '?': '?', - '!': 'ǃ', - '"': '"', - '<': '≺', - '>': '≻', - '.': '․', - '^': '^', - '{': '{', - '}': '}', - '[': '[', - ']': ']', - '(': '(', - ')': ')', - '|': '|', - '*': '∗' - }; - var newText = text; - for (var key in symbolList) { - var regex = new RegExp(symbolList[key], 'g'); - newText = newText.replace(regex, key); - } - return newText.replace(/ {1,}/g, ' ').trimRight(); + $app.methods.genSig = async function (file) { + var response = await AppApi.SignFile(file); + return response; }; - $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.genLength = async function (file) { + var response = await AppApi.FileLength(file); + return response; }; - $app.methods.checkCanInvite = function (location) { - var L = API.parseLocation(location); - var instance = API.cachedInstances.get(location); - if (instance?.closedAt) { - return false; - } + // Upload avatar image + + $app.methods.onFileChangeAvatarImage = function (e) { + var clearFile = function () { + if (document.querySelector('#AvatarImageUploadButton')) { + document.querySelector('#AvatarImageUploadButton').value = ''; + } + }; + var files = e.target.files || e.dataTransfer.files; if ( - L.accessType === 'public' || - L.accessType === 'group' || - L.userId === API.currentUser.id + !files.length || + !this.avatarDialog.visible || + this.avatarDialog.loading ) { - return true; + clearFile(); + return; } - if (L.accessType === 'invite' || L.accessType === 'friends') { - return false; + if (files[0].size >= 100000000) { + // 100MB + $app.$message({ + message: 'File size too large', + type: 'error' + }); + clearFile(); + return; } - if (this.lastLocation.location === location) { - return true; + if (!files[0].type.match(/image.*/)) { + $app.$message({ + message: "File isn't a png", + type: 'error' + }); + clearFile(); + return; } - return false; + this.avatarDialog.loading = true; + this.changeAvatarImageDialogLoading = true; + var r = new FileReader(); + r.onload = async function (file) { + var base64File = await $app.resizeImageToFitLimits(btoa(r.result)); + // 10MB + var fileMd5 = await $app.genMd5(base64File); + var fileSizeInBytes = parseInt(file.total, 10); + var base64SignatureFile = await $app.genSig(base64File); + var signatureMd5 = await $app.genMd5(base64SignatureFile); + var signatureSizeInBytes = parseInt( + await $app.genLength(base64SignatureFile), + 10 + ); + var avatarId = $app.avatarDialog.id; + var { imageUrl } = $app.avatarDialog.ref; + var fileId = $utils.extractFileId(imageUrl); + if (!fileId) { + $app.$message({ + message: 'Current avatar image invalid', + type: 'error' + }); + clearFile(); + return; + } + $app.avatarImage = { + base64File, + fileMd5, + base64SignatureFile, + signatureMd5, + fileId, + avatarId + }; + var params = { + fileMd5, + fileSizeInBytes, + signatureMd5, + signatureSizeInBytes + }; + API.uploadAvatarImage(params, fileId); + }; + r.readAsBinaryString(files[0]); + clearFile(); }; - $app.methods.checkCanInviteSelf = function (location) { - var L = API.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; + API.uploadAvatarImage = async function (params, fileId) { + try { + return await this.call(`file/${fileId}`, { + method: 'POST', + params + }).then((json) => { + var args = { + json, + params, + fileId + }; + this.$emit('AVATARIMAGE:INIT', args); + return args; + }); + } catch (err) { + console.error(err); + this.uploadAvatarFailCleanup(fileId); } - return true; + return void 0; }; - $app.methods.setAsideWidth = async function () { - document.getElementById('aside').style.width = `${this.asideWidth}px`; - await configRepository.setInt('VRCX_sidePanelWidth', this.asideWidth); + API.uploadAvatarFailCleanup = async function (fileId) { + var json = await this.call(`file/${fileId}`, { + method: 'GET' + }); + var fileId = json.id; + var fileVersion = json.versions[json.versions.length - 1].version; + this.call(`file/${fileId}/${fileVersion}/signature/finish`, { + method: 'PUT' + }); + this.call(`file/${fileId}/${fileVersion}/file/finish`, { + method: 'PUT' + }); + $app.avatarDialog.loading = false; + $app.changeAvatarImageDialogLoading = false; }; - // VRCX auto update + API.$on('AVATARIMAGE:INIT', function (args) { + var fileId = args.json.id; + var fileVersion = + args.json.versions[args.json.versions.length - 1].version; + var params = { + fileId, + fileVersion + }; + this.uploadAvatarImageFileStart(params); + }); - $app.data.VRCXUpdateDialog = { - visible: false, - updatePending: false, - updatePendingIsLatest: false, - release: '', - releases: [], - json: {} - }; - - $app.data.checkingForVRCXUpdate = false; - $app.data.pendingVRCXInstall = ''; - $app.data.pendingVRCXUpdate = false; - - $app.data.branches = { - Stable: { - name: 'Stable', - urlReleases: 'https://api0.vrcx.app/releases/stable', - urlLatest: 'https://api0.vrcx.app/releases/stable/latest' - }, - Nightly: { - name: 'Nightly', - urlReleases: 'https://api0.vrcx.app/releases/nightly', - urlLatest: 'https://api0.vrcx.app/releases/nightly/latest' + API.uploadAvatarImageFileStart = async function (params) { + try { + return await this.call( + `file/${params.fileId}/${params.fileVersion}/file/start`, + { + method: 'PUT' + } + ).then((json) => { + var args = { + json, + params + }; + this.$emit('AVATARIMAGE:FILESTART', args); + return args; + }); + } catch (err) { + console.error(err); + this.uploadAvatarFailCleanup(params.fileId); } + return void 0; }; - $app.methods.showVRCXUpdateDialog = async function () { - this.$nextTick(() => adjustDialogZ(this.$refs.VRCXUpdateDialog.$el)); - var D = this.VRCXUpdateDialog; - D.visible = true; - D.updatePendingIsLatest = false; - D.updatePending = await AppApi.CheckForUpdateExe(); - this.loadBranchVersions(); - }; - - $app.methods.downloadVRCXUpdate = function ( - updateSetupUrl, - updateHashUrl, - size, - name, - type - ) { - var ref = { - id: 'VRCXUpdate', - name + API.$on('AVATARIMAGE:FILESTART', function (args) { + var { url } = args.json; + var { fileId, fileVersion } = args.params; + var params = { + url, + fileId, + fileVersion }; - this.downloadQueue.set('VRCXUpdate', { - ref, - type, - updateSetupUrl, - updateHashUrl, - size - }); - this.downloadQueueTable.data = Array.from(this.downloadQueue.values()); - if (!this.downloadInProgress) { - this.downloadFileQueueUpdate(); - } - }; + this.uploadAvatarImageFileAWS(params); + }); - $app.methods.installVRCXUpdate = function () { - for (var release of this.VRCXUpdateDialog.releases) { - if (release.name === this.VRCXUpdateDialog.release) { - var downloadUrl = ''; - var hashUrl = ''; - var size = 0; - for (var asset of release.assets) { - if (asset.state !== 'uploaded') { - continue; - } - if ( - asset.content_type === 'application/x-msdownload' || - asset.content_type === 'application/x-msdos-program' - ) { - downloadUrl = asset.browser_download_url; - size = asset.size; - continue; - } - if ( - asset.name === 'SHA256SUMS.txt' && - asset.content_type === 'text/plain' - ) { - hashUrl = asset.browser_download_url; - continue; - } + API.uploadAvatarImageFileAWS = function (params) { + return webApiService + .execute({ + url: params.url, + uploadFilePUT: true, + fileData: $app.avatarImage.base64File, + fileMIME: 'image/png', + headers: { + 'Content-MD5': $app.avatarImage.fileMd5 } - if (!downloadUrl) { - return; + }) + .then((json) => { + if (json.status !== 200) { + $app.avatarDialog.loading = false; + $app.changeAvatarImageDialogLoading = false; + this.$throw('Avatar image upload failed', json, params.url); } - var name = release.name; - var type = 'Manual'; - this.downloadVRCXUpdate(downloadUrl, hashUrl, size, name, type); - this.VRCXUpdateDialog.visible = false; - this.showDownloadDialog(); - } - } + var args = { + json, + params + }; + this.$emit('AVATARIMAGE:FILEAWS', args); + return args; + }); }; - $app.methods.restartVRCX = function (isUpgrade) { - AppApi.RestartApplication(isUpgrade); - }; + API.$on('AVATARIMAGE:FILEAWS', function (args) { + var { fileId, fileVersion } = args.params; + var params = { + fileId, + fileVersion + }; + this.uploadAvatarImageFileFinish(params); + }); - $app.methods.loadBranchVersions = async function () { - var D = this.VRCXUpdateDialog; - var url = this.branches[this.branch].urlReleases; - this.checkingForVRCXUpdate = true; - try { - var response = await webApiService.execute({ - url, - method: 'GET' - }); - } finally { - this.checkingForVRCXUpdate = false; - } - var json = JSON.parse(response.data); - if (this.debugWebRequests) { - console.log(json, response); - } - var releases = []; - if (typeof json !== 'object' || json.message) { - $app.$message({ - message: `Failed to check for update, "${json.message}"`, - type: 'error' - }); - return; - } - for (var release of json) { - for (var asset of release.assets) { - if ( - (asset.content_type === 'application/x-msdownload' || - asset.content_type === 'application/x-msdos-program') && - asset.state === 'uploaded' - ) { - releases.push(release); + API.uploadAvatarImageFileFinish = function (params) { + return this.call( + `file/${params.fileId}/${params.fileVersion}/file/finish`, + { + method: 'PUT', + params: { + maxParts: 0, + nextPartNumber: 0 } } - } - D.releases = releases; - D.release = json[0].name; - this.VRCXUpdateDialog.updatePendingIsLatest = false; - if (D.release === this.pendingVRCXInstall) { - // update already downloaded and latest version - this.VRCXUpdateDialog.updatePendingIsLatest = true; - } - if ((await configRepository.getString('VRCX_branch')) !== this.branch) { - await configRepository.setString('VRCX_branch', this.branch); - } + ).then((json) => { + var args = { + json, + params + }; + this.$emit('AVATARIMAGE:FILEFINISH', args); + return args; + }); }; - $app.methods.saveAutoUpdateVRCX = async function () { - if (this.autoUpdateVRCX === 'Off') { - this.pendingVRCXUpdate = false; - } - await configRepository.setString( - 'VRCX_autoUpdateVRCX', - this.autoUpdateVRCX - ); - }; + API.$on('AVATARIMAGE:FILEFINISH', function (args) { + var { fileId, fileVersion } = args.params; + var params = { + fileId, + fileVersion + }; + this.uploadAvatarImageSigStart(params); + }); - $app.methods.checkForVRCXUpdate = async function () { - if ( - !this.appVersion || - this.appVersion === 'VRCX Nightly Build' || - this.appVersion === 'VRCX Build' - ) { - return; - } - if (this.branch === 'Beta') { - // move Beta users to stable - this.branch = 'Stable'; - await configRepository.setString('VRCX_branch', this.branch); - } - var url = this.branches[this.branch].urlLatest; - this.checkingForVRCXUpdate = true; + API.uploadAvatarImageSigStart = async function (params) { try { - var response = await webApiService.execute({ - url, - method: 'GET' - }); - } finally { - this.checkingForVRCXUpdate = false; - } - this.pendingVRCXUpdate = false; - var json = JSON.parse(response.data); - if (this.debugWebRequests) { - console.log(json, response); - } - if (json === Object(json) && json.name && json.published_at) { - this.VRCXUpdateDialog.updateJson = json; - this.changeLogDialog.buildName = json.name; - this.changeLogDialog.changeLog = this.changeLogRemoveLinks( - json.body - ); - this.latestAppVersion = json.name; - var name = json.name; - this.VRCXUpdateDialog.updatePendingIsLatest = false; - if (name === this.pendingVRCXInstall) { - // update already downloaded - this.VRCXUpdateDialog.updatePendingIsLatest = true; - } else if (name > this.appVersion) { - var downloadUrl = ''; - var hashUrl = ''; - var size = 0; - for (var asset of json.assets) { - if (asset.state !== 'uploaded') { - continue; - } - if ( - asset.content_type === 'application/x-msdownload' || - asset.content_type === 'application/x-msdos-program' - ) { - downloadUrl = asset.browser_download_url; - size = asset.size; - continue; - } - if ( - asset.name === 'SHA256SUMS.txt' && - asset.content_type === 'text/plain' - ) { - hashUrl = asset.browser_download_url; - continue; - } - } - if (!downloadUrl) { - return; - } - this.pendingVRCXUpdate = true; - this.notifyMenu('settings'); - var type = 'Auto'; - if (!API.isLoggedIn) { - this.showVRCXUpdateDialog(); - } else if (this.autoUpdateVRCX === 'Notify') { - // this.showVRCXUpdateDialog(); - } else if (this.autoUpdateVRCX === 'Auto Download') { - this.downloadVRCXUpdate( - downloadUrl, - hashUrl, - size, - name, - type - ); + return await this.call( + `file/${params.fileId}/${params.fileVersion}/signature/start`, + { + method: 'PUT' } - } - } - }; - - $app.methods.compareUnityVersion = function (unitySortNumber) { - if (!API.cachedConfig.sdkUnityVersion) { - console.error('No cachedConfig.sdkUnityVersion'); - return false; - } - - // 2022.3.6f1 2022 03 06 000 - // 2019.4.31f1 2019 04 31 000 - // 5.3.4p1 5 03 04 010 - // 2019.4.31f1c1 is a thing - var array = API.cachedConfig.sdkUnityVersion.split('.'); - if (array.length < 3) { - console.error('Invalid cachedConfig.sdkUnityVersion'); - return false; - } - var currentUnityVersion = array[0]; - currentUnityVersion += array[1].padStart(2, '0'); - var indexFirstLetter = array[2].search(/[a-zA-Z]/); - if (indexFirstLetter > -1) { - currentUnityVersion += array[2] - .substr(0, indexFirstLetter) - .padStart(2, '0'); - currentUnityVersion += '0'; - var letter = array[2].substr(indexFirstLetter, 1); - if (letter === 'p') { - currentUnityVersion += '1'; - } else { - // f - currentUnityVersion += '0'; - } - currentUnityVersion += '0'; - } else { - // just in case - currentUnityVersion += '000'; - } - // just in case - currentUnityVersion = currentUnityVersion.replace(/\D/g, ''); - - if ( - parseInt(unitySortNumber, 10) <= parseInt(currentUnityVersion, 10) - ) { - return true; + ).then((json) => { + var args = { + json, + params + }; + this.$emit('AVATARIMAGE:SIGSTART', args); + return args; + }); + } catch (err) { + console.error(err); + this.uploadAvatarFailCleanup(params.fileId); } - return false; + return void 0; }; - $app.methods.userImage = function (user) { - if (typeof user === 'undefined') { - return ''; - } - if (this.displayVRCPlusIconsAsAvatar && user.userIcon) { - return user.userIcon; - } - if (user.profilePicOverrideThumbnail) { - return user.profilePicOverrideThumbnail; - } - if (user.profilePicOverride) { - return user.profilePicOverride; - } - if (user.thumbnailUrl) { - return user.thumbnailUrl; - } - return user.currentAvatarThumbnailImageUrl; - }; + API.$on('AVATARIMAGE:SIGSTART', function (args) { + var { url } = args.json; + var { fileId, fileVersion } = args.params; + var params = { + url, + fileId, + fileVersion + }; + this.uploadAvatarImageSigAWS(params); + }); - $app.methods.userImageFull = function (user) { - if (this.displayVRCPlusIconsAsAvatar && user.userIcon) { - return user.userIcon; - } - if (user.profilePicOverride) { - return user.profilePicOverride; - } - return user.currentAvatarImageUrl; + API.uploadAvatarImageSigAWS = function (params) { + return webApiService + .execute({ + url: params.url, + uploadFilePUT: true, + fileData: $app.avatarImage.base64SignatureFile, + fileMIME: 'application/x-rsync-signature', + headers: { + 'Content-MD5': $app.avatarImage.signatureMd5 + } + }) + .then((json) => { + if (json.status !== 200) { + $app.avatarDialog.loading = false; + $app.changeAvatarImageDialogLoading = false; + this.$throw('Avatar image upload failed', json, params.url); + } + var args = { + json, + params + }; + this.$emit('AVATARIMAGE:SIGAWS', args); + return args; + }); }; - $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;' - ); - }; + API.$on('AVATARIMAGE:SIGAWS', function (args) { + var { fileId, fileVersion } = args.params; + var params = { + fileId, + fileVersion + }; + this.uploadAvatarImageSigFinish(params); + }); - $app.methods.clearVRCXCache = function () { - API.failedGetRequests = new Map(); - API.cachedUsers.forEach((ref, id) => { - if ( - !this.friends.has(id) && - !this.lastLocation.playerList.has(ref.displayName) && - 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 && - !$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.uploadAvatarImageSigFinish = function (params) { + return this.call( + `file/${params.fileId}/${params.fileVersion}/signature/finish`, + { + method: 'PUT', + params: { + maxParts: 0, + nextPartNumber: 0 + } } + ).then((json) => { + var args = { + json, + params + }; + this.$emit('AVATARIMAGE:SIGFINISH', args); + return args; }); - 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() + API.$on('AVATARIMAGE:SIGFINISH', function (args) { + var { fileId, fileVersion } = args.params; + var parmas = { + id: $app.avatarImage.avatarId, + imageUrl: `${API.endpointDomain}/file/${fileId}/${fileVersion}/file` }; + this.setAvatarImage(parmas); + }); + + API.setAvatarImage = function (params) { + return this.call(`avatars/${params.id}`, { + method: 'PUT', + params + }).then((json) => { + var args = { + json, + params + }; + this.$emit('AVATARIMAGE:SET', args); + this.$emit('AVATAR', args); + return args; + }); }; - $app.data.ipcEnabled = false; - $app.methods.ipcEvent = function (json) { - if (!this.friendLogInitStatus) { + // Upload world image + + $app.methods.onFileChangeWorldImage = function (e) { + var clearFile = function () { + if (document.querySelector('#WorldImageUploadButton')) { + document.querySelector('#WorldImageUploadButton').value = ''; + } + }; + var files = e.target.files || e.dataTransfer.files; + if ( + !files.length || + !this.worldDialog.visible || + this.worldDialog.loading + ) { + clearFile(); return; } - try { - var data = JSON.parse(json); - } catch { - console.log(`IPC invalid JSON, ${json}`); + if (files[0].size >= 100000000) { + // 100MB + $app.$message({ + message: 'File size too large', + type: 'error' + }); + clearFile(); 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': - AppApi.FocusWindow(); - this.eventLaunchCommand(data.command); - break; - case 'VRCXLaunch': - console.log('VRCXLaunch:', data); - break; - default: - console.log('IPC:', data); + if (!files[0].type.match(/image.*/)) { + $app.$message({ + message: "File isn't a png", + type: 'error' + }); + clearFile(); + return; } + this.worldDialog.loading = true; + this.changeWorldImageDialogLoading = true; + var r = new FileReader(); + r.onload = async function (file) { + var base64File = await $app.resizeImageToFitLimits(btoa(r.result)); + // 10MB + var fileMd5 = await $app.genMd5(base64File); + var fileSizeInBytes = parseInt(file.total, 10); + var base64SignatureFile = await $app.genSig(base64File); + var signatureMd5 = await $app.genMd5(base64SignatureFile); + var signatureSizeInBytes = parseInt( + await $app.genLength(base64SignatureFile), + 10 + ); + var worldId = $app.worldDialog.id; + var { imageUrl } = $app.worldDialog.ref; + var fileId = $utils.extractFileId(imageUrl); + if (!fileId) { + $app.$message({ + message: 'Current world image invalid', + type: 'error' + }); + clearFile(); + return; + } + $app.worldImage = { + base64File, + fileMd5, + base64SignatureFile, + signatureMd5, + fileId, + worldId + }; + var params = { + fileMd5, + fileSizeInBytes, + signatureMd5, + signatureSizeInBytes + }; + API.uploadWorldImage(params, fileId); + }; + r.readAsBinaryString(files[0]); + clearFile(); }; - $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 + API.uploadWorldImage = async function (params, fileId) { + try { + return await this.call(`file/${fileId}`, { + method: 'POST', + params + }).then((json) => { + var args = { + json, + params, + fileId + }; + this.$emit('WORLDIMAGE:INIT', args); + return args; }); - } else { - this.customUserTags.delete(data.UserId); + } catch (err) { + console.error(err); + this.uploadWorldFailCleanup(fileId); } - var feedUpdate = { - userId: data.UserId, - colour: data.TagColour + return void 0; + }; + + API.uploadWorldFailCleanup = async function (fileId) { + var json = await this.call(`file/${fileId}`, { + method: 'GET' + }); + var fileId = json.id; + var fileVersion = json.versions[json.versions.length - 1].version; + this.call(`file/${fileId}/${fileVersion}/signature/finish`, { + method: 'PUT' + }); + this.call(`file/${fileId}/${fileVersion}/file/finish`, { + method: 'PUT' + }); + $app.worldDialog.loading = false; + $app.changeWorldImageDialogLoading = false; + }; + + API.$on('WORLDIMAGE:INIT', function (args) { + var fileId = args.json.id; + var fileVersion = + args.json.versions[args.json.versions.length - 1].version; + var params = { + fileId, + fileVersion }; - 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.uploadWorldImageFileStart(params); + }); + + API.uploadWorldImageFileStart = async function (params) { + try { + return await this.call( + `file/${params.fileId}/${params.fileVersion}/file/start`, + { + method: 'PUT' + } + ).then((json) => { + var args = { + json, + params + }; + this.$emit('WORLDIMAGE:FILESTART', args); + return args; + }); + } catch (err) { + console.error(err); + this.uploadWorldFailCleanup(params.fileId); } - this.updateSharedFeed(true); + return void 0; }; - $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; + API.$on('WORLDIMAGE:FILESTART', function (args) { + var { url } = args.json; + var { fileId, fileVersion } = args.params; + var params = { + url, + fileId, + fileVersion + }; + this.uploadWorldImageFileAWS(params); + }); + + API.uploadWorldImageFileAWS = function (params) { + return webApiService + .execute({ + url: params.url, + uploadFilePUT: true, + fileData: $app.worldImage.base64File, + fileMIME: 'image/png', + headers: { + 'Content-MD5': $app.worldImage.fileMd5 } - var entry = { - created_at: new Date().toJSON(), - type: 'Event', - data: data.Data + }) + .then((json) => { + if (json.status !== 200) { + $app.worldDialog.loading = false; + $app.changeWorldImageDialogLoading = false; + this.$throw('World image upload failed', json, params.url); + } + var args = { + json, + params }; - 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 + this.$emit('WORLDIMAGE:FILEAWS', args); + return args; + }); + }; + + API.$on('WORLDIMAGE:FILEAWS', function (args) { + var { fileId, fileVersion } = args.params; + var params = { + fileId, + fileVersion + }; + this.uploadWorldImageFileFinish(params); + }); + + API.uploadWorldImageFileFinish = function (params) { + return this.call( + `file/${params.fileId}/${params.fileVersion}/file/finish`, + { + method: 'PUT', + params: { + maxParts: 0, + nextPartNumber: 0 + } + } + ).then((json) => { + var args = { + json, + params + }; + this.$emit('WORLDIMAGE:FILEFINISH', args); + return args; + }); + }; + + API.$on('WORLDIMAGE:FILEFINISH', function (args) { + var { fileId, fileVersion } = args.params; + var params = { + fileId, + fileVersion + }; + this.uploadWorldImageSigStart(params); + }); + + API.uploadWorldImageSigStart = async function (params) { + try { + return await this.call( + `file/${params.fileId}/${params.fileVersion}/signature/start`, + { + method: 'PUT' + } + ).then((json) => { + var args = { + json, + params }; - database.addGamelogExternalToDatabase(entry); - this.queueGameLogNoty(entry); - this.addGameLog(entry); - break; - default: - console.log('VRCXMessage:', data); - break; + this.$emit('WORLDIMAGE:SIGSTART', args); + return args; + }); + } catch (err) { + console.error(err); + this.uploadWorldFailCleanup(params.fileId); } + return void 0; }; - $app.methods.photonEventPulse = function () { - this.photonEventCount++; - this.photonEventIcon = true; - workerTimers.setTimeout(() => (this.photonEventIcon = false), 150); - }; + API.$on('WORLDIMAGE:SIGSTART', function (args) { + var { url } = args.json; + var { fileId, fileVersion } = args.params; + var params = { + url, + fileId, + fileVersion + }; + this.uploadWorldImageSigAWS(params); + }); - $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]; + API.uploadWorldImageSigAWS = function (params) { + return webApiService + .execute({ + url: params.url, + uploadFilePUT: true, + fileData: $app.worldImage.base64SignatureFile, + fileMIME: 'application/x-rsync-signature', + headers: { + 'Content-MD5': $app.worldImage.signatureMd5 } - 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 - }); - } + }) + .then((json) => { + if (json.status !== 200) { + $app.worldDialog.loading = false; + $app.changeWorldImageDialogLoading = false; + this.$throw('World image upload failed', json, params.url); } - if (typeof data.Parameters[252] !== 'undefined') { - this.parsePhotonLobbyIds(data.Parameters[252]); + var args = { + json, + params + }; + this.$emit('WORLDIMAGE:SIGAWS', args); + return args; + }); + }; + + API.$on('WORLDIMAGE:SIGAWS', function (args) { + var { fileId, fileVersion } = args.params; + var params = { + fileId, + fileVersion + }; + this.uploadWorldImageSigFinish(params); + }); + + API.uploadWorldImageSigFinish = function (params) { + return this.call( + `file/${params.fileId}/${params.fileVersion}/signature/finish`, + { + method: 'PUT', + params: { + maxParts: 0, + nextPartNumber: 0 } - this.photonEvent7List = new Map(); - break; - } + } + ).then((json) => { + var args = { + json, + params + }; + this.$emit('WORLDIMAGE:SIGFINISH', args); + return args; + }); }; - API.$on('LOGIN', async function () { - var command = await AppApi.GetLaunchCommand(); - if (command) { - $app.eventLaunchCommand(command); + API.$on('WORLDIMAGE:SIGFINISH', function (args) { + var { fileId, fileVersion } = args.params; + var parmas = { + id: $app.worldImage.worldId, + imageUrl: `${API.endpointDomain}/file/${fileId}/${fileVersion}/file` + }; + this.setWorldImage(parmas); + }); + + API.setWorldImage = function (params) { + return this.call(`worlds/${params.id}`, { + method: 'PUT', + params + }).then((json) => { + var args = { + json, + params + }; + this.$emit('WORLDIMAGE:SET', args); + this.$emit('WORLD', args); + return args; + }); + }; + + API.$on('AVATARIMAGE:SET', function (args) { + $app.avatarDialog.loading = false; + $app.changeAvatarImageDialogLoading = false; + if (args.json.imageUrl === args.params.imageUrl) { + $app.$message({ + message: 'Avatar image changed', + type: 'success' + }); + $app.displayPreviousImages('Avatar', 'Change'); + } else { + this.$throw(0, 'Avatar image change failed', args.params.imageUrl); } }); - $app.methods.eventLaunchCommand = function (input) { - if (!API.isLoggedIn) { + API.$on('WORLDIMAGE:SET', function (args) { + $app.worldDialog.loading = false; + $app.changeWorldImageDialogLoading = false; + if (args.json.imageUrl === args.params.imageUrl) { + $app.$message({ + message: 'World image changed', + type: 'success' + }); + $app.displayPreviousImages('World', 'Change'); + } else { + this.$throw(0, 'World image change failed', args.params.imageUrl); + } + }); + + // Set avatar/world image + + $app.methods.displayPreviousImages = function (type, command) { + this.previousImagesTableFileId = ''; + this.previousImagesTable = []; + var imageUrl = ''; + if (type === 'Avatar') { + var { imageUrl } = this.avatarDialog.ref; + } else if (type === 'World') { + var { imageUrl } = this.worldDialog.ref; + } else if (type === 'User') { + imageUrl = this.userDialog.ref.currentAvatarImageUrl; + } + var fileId = $utils.extractFileId(imageUrl); + if (!fileId) { return; } - var args = input.split('/'); - var command = args[0]; - var commandArg = args[1]; - 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(':'); - API.getCachedWorld({ worldId: id }).then((args1) => { - this.directAccessWorld(id); - this.addLocalWorldFavorite(id, group); - return args1; + var params = { + fileId + }; + if (command === 'Display') { + this.previousImagesDialogVisible = true; + this.$nextTick(() => + $app.adjustDialogZ(this.$refs.previousImagesDialog.$el) + ); + } + if (type === 'Avatar') { + if (command === 'Change') { + this.changeAvatarImageDialogVisible = true; + this.$nextTick(() => + $app.adjustDialogZ(this.$refs.changeAvatarImageDialog.$el) + ); + } + API.getAvatarImages(params).then((args) => { + this.previousImagesTableFileId = args.json.id; + var images = []; + args.json.versions.forEach((item) => { + if (!item.deleted) { + images.unshift(item); + } + }); + this.checkPreviousImageAvailable(images); + }); + } else if (type === 'World') { + if (command === 'Change') { + this.changeWorldImageDialogVisible = true; + this.$nextTick(() => + $app.adjustDialogZ(this.$refs.changeWorldImageDialog.$el) + ); + } + API.getWorldImages(params).then((args) => { + this.previousImagesTableFileId = args.json.id; + var images = []; + args.json.versions.forEach((item) => { + if (!item.deleted) { + images.unshift(item); + } + }); + this.checkPreviousImageAvailable(images); + }); + } else if (type === 'User') { + API.getAvatarImages(params).then((args) => { + this.previousImagesTableFileId = args.json.id; + var images = []; + args.json.versions.forEach((item) => { + if (!item.deleted) { + images.unshift(item); + } }); - break; - case 'addavatardb': - this.addAvatarProvider(input.replace('addavatardb/', '')); - break; - case 'import': - var type = args[1]; - if (!type) break; - var data = input.replace(`import/${type}/`, ''); - if (type === 'avatar') { - this.showAvatarImportDialog(); - this.avatarImportDialog.input = data; - } else if (type === 'world') { - this.showWorldImportDialog(); - this.worldImportDialog.input = data; - } else if (type === 'friend') { - this.showFriendImportDialog(); - this.friendImportDialog.input = data; - } - break; + this.checkPreviousImageAvailable(images); + }); } }; - $app.methods.toggleAvatarCopying = function () { - API.saveCurrentUser({ - allowAvatarCopying: !API.currentUser.allowAvatarCopying - }).then((args) => { - return args; - }); - }; - - $app.methods.toggleAllowBooping = function () { - API.saveCurrentUser({ - isBoopingEnabled: !API.currentUser.isBoopingEnabled - }).then((args) => { - return args; - }); - }; - - // #endregion - // #region | App: Previous Instances User Dialog - - $app.data.previousInstancesUserDialogTable = { - data: [], - filters: [ - { - prop: 'worldName', - value: '' - } - ], - tableProps: { - stripe: true, - size: 'mini', - defaultSort: { - prop: 'created_at', - order: 'descending' + $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); + } } - }, - pageSize: 10, - paginationProps: { - small: true, - layout: 'sizes,prev,pager,next,total', - pageSizes: [10, 25, 50, 100] } }; - $app.data.previousInstancesUserDialog = { - visible: false, - loading: false, - forceUpdate: 0, - userRef: {} - }; + $app.data.previousImagesDialogVisible = false; + $app.data.changeAvatarImageDialogVisible = false; + $app.data.changeAvatarImageDialogLoading = false; + $app.data.changeWorldImageDialogVisible = false; + $app.data.changeWorldImageDialogLoading = false; + $app.data.previousImagesTable = []; + $app.data.previousImagesFileId = ''; - $app.methods.showPreviousInstancesUserDialog = function (userRef) { - this.$nextTick(() => - adjustDialogZ(this.$refs.previousInstancesUserDialog.$el) - ); - var D = this.previousInstancesUserDialog; - D.userRef = userRef; - D.visible = true; - D.loading = true; - this.refreshPreviousInstancesUserTable(); - }; + API.$on('LOGIN', function () { + $app.previousImagesTable = []; + $app.previousImagesDialogVisible = false; + }); - $app.methods.refreshPreviousInstancesUserTable = function () { - var D = this.previousInstancesUserDialog; - database.getpreviousInstancesByUserId(D.userRef).then((data) => { - var array = []; - for (var ref of data.values()) { - ref.$location = API.parseLocation(ref.location); - if (ref.time > 0) { - ref.timer = timeToText(ref.time); - } else { - ref.timer = ''; - } - array.push(ref); - } - array.sort(compareByCreatedAt); - this.previousInstancesUserDialogTable.data = array; - D.loading = false; - workerTimers.setTimeout(() => D.forceUpdate++, 150); + API.getAvatarImages = function (params) { + return this.call(`file/${params.fileId}`, { + method: 'GET', + params + }).then((json) => { + var args = { + json, + params + }; + this.$emit('AVATARIMAGE:GET', args); + return args; }); }; - $app.methods.getDisplayNameFromUserId = function (userId) { - var displayName = userId; - var ref = API.cachedUsers.get(userId); - if ( - typeof ref !== 'undefined' && - typeof ref.displayName !== 'undefined' - ) { - displayName = ref.displayName; - } - return displayName; - }; - - $app.methods.confirmDeleteGameLogUserInstance = function (row) { - this.$confirm('Continue? Delete', 'Confirm', { - confirmButtonText: 'Confirm', - cancelButtonText: 'Cancel', - type: 'info', - callback: (action) => { - if (action === 'confirm') { - database.deleteGameLogInstance({ - id: this.previousInstancesUserDialog.userRef.id, - displayName: - this.previousInstancesUserDialog.userRef - .displayName, - location: row.location - }); - removeFromArray( - this.previousInstancesUserDialogTable.data, - row - ); - } - } + API.getWorldImages = function (params) { + return this.call(`file/${params.fileId}`, { + method: 'GET', + params + }).then((json) => { + var args = { + json, + params + }; + this.$emit('WORLDIMAGE:GET', args); + return args; }); }; - // #endregion - // #region | App: Previous Instances World Dialog + API.$on('AVATARIMAGE:GET', function (args) { + $app.storeAvatarImage(args); + }); - $app.data.previousInstancesWorldDialogTable = { - data: [], - filters: [ - { - prop: 'groupName', - value: '' - } - ], - tableProps: { - stripe: true, - size: 'mini', - defaultSort: { - prop: 'created_at', - order: 'descending' - } - }, - pageSize: 10, - paginationProps: { - small: true, - layout: 'sizes,prev,pager,next,total', - pageSizes: [10, 25, 50, 100] + $app.methods.storeAvatarImage = function (args) { + var refCreatedAt = args.json.versions[0]; + var fileCreatedAt = refCreatedAt.created_at; + var fileId = args.params.fileId; + var avatarName = ''; + var imageName = args.json.name; + var avatarNameRegex = /Avatar - (.*) - Image -/gi.exec(imageName); + if (avatarNameRegex) { + avatarName = this.replaceBioSymbols(avatarNameRegex[1]); } + var ownerId = args.json.ownerId; + var avatarInfo = { + ownerId, + avatarName, + fileCreatedAt + }; + API.cachedAvatarNames.set(fileId, avatarInfo); + return avatarInfo; }; - $app.data.previousInstancesWorldDialog = { - visible: false, - loading: false, - forceUpdate: 0, - worldRef: {} - }; - - $app.methods.showPreviousInstancesWorldDialog = function (worldRef) { - this.$nextTick(() => - adjustDialogZ(this.$refs.previousInstancesWorldDialog.$el) - ); - var D = this.previousInstancesWorldDialog; - D.worldRef = worldRef; - D.visible = true; - D.loading = true; - this.refreshPreviousInstancesWorldTable(); - }; - - $app.methods.refreshPreviousInstancesWorldTable = function () { - var D = this.previousInstancesWorldDialog; - database.getpreviousInstancesByWorldId(D.worldRef).then((data) => { - var array = []; - for (var ref of data.values()) { - ref.$location = API.parseLocation(ref.location); - if (ref.time > 0) { - ref.timer = timeToText(ref.time); - } else { - ref.timer = ''; - } - array.push(ref); - } - array.sort(compareByCreatedAt); - this.previousInstancesWorldDialogTable.data = array; - D.loading = false; - workerTimers.setTimeout(() => D.forceUpdate++, 150); + $app.methods.setAvatarImage = function (image) { + this.changeAvatarImageDialogLoading = true; + var parmas = { + id: this.avatarDialog.id, + imageUrl: `${API.endpointDomain}/file/${this.previousImagesTableFileId}/${image.version}/file` + }; + API.setAvatarImage(parmas).finally(() => { + this.changeAvatarImageDialogLoading = false; + this.changeAvatarImageDialogVisible = false; }); }; - $app.methods.confirmDeleteGameLogWorldInstance = function (row) { - this.$confirm('Continue? Delete', 'Confirm', { - confirmButtonText: 'Confirm', - cancelButtonText: 'Cancel', - type: 'info', - callback: (action) => { - if (action === 'confirm') { - database.deleteGameLogInstanceByInstanceId({ - location: row.location - }); - removeFromArray( - this.previousInstancesWorldDialogTable.data, - row - ); - } - } - }); + $app.methods.uploadAvatarImage = function () { + document.getElementById('AvatarImageUploadButton').click(); }; - // #endregion - // #region | App: Previous Instance Info Dialog + $app.methods.deleteAvatarImage = function () { + this.changeAvatarImageDialogLoading = true; + var parmas = { + fileId: this.previousImagesTableFileId, + version: this.previousImagesTable[0].version + }; + API.deleteFileVersion(parmas) + .then((args) => { + this.previousImagesTableFileId = args.json.id; + var images = []; + args.json.versions.forEach((item) => { + if (!item.deleted) { + images.unshift(item); + } + }); + this.checkPreviousImageAvailable(images); + }) + .finally(() => { + this.changeAvatarImageDialogLoading = false; + }); + }; - $app.data.previousInstanceInfoDialogTable = { - data: [], - filters: [ - { - prop: 'displayName', - value: '' - } - ], - tableProps: { - stripe: true, - size: 'mini', - defaultSort: { - prop: 'created_at', - order: 'descending' - } - }, - pageSize: 10, - paginationProps: { - small: true, - layout: 'sizes,prev,pager,next,total', - pageSizes: [10, 25, 50, 100] - } + $app.methods.setWorldImage = function (image) { + this.changeWorldImageDialogLoading = true; + var parmas = { + id: this.worldDialog.id, + imageUrl: `${API.endpointDomain}/file/${this.previousImagesTableFileId}/${image.version}/file` + }; + API.setWorldImage(parmas).finally(() => { + this.changeWorldImageDialogLoading = false; + this.changeWorldImageDialogVisible = false; + }); }; - $app.data.previousInstanceInfoDialog = { - visible: false, - loading: false, - forceUpdate: 0, - $location: {} + $app.methods.uploadWorldImage = function () { + document.getElementById('WorldImageUploadButton').click(); }; - $app.methods.showPreviousInstanceInfoDialog = function (instanceId) { - this.$nextTick(() => - adjustDialogZ(this.$refs.previousInstanceInfoDialog.$el) - ); - var D = this.previousInstanceInfoDialog; - D.$location = API.parseLocation(instanceId); - D.visible = true; - D.loading = true; - this.refreshPreviousInstanceInfoTable(); + $app.methods.deleteWorldImage = function () { + this.changeWorldImageDialogLoading = true; + var parmas = { + fileId: this.previousImagesTableFileId, + version: this.previousImagesTable[0].version + }; + API.deleteFileVersion(parmas) + .then((args) => { + this.previousImagesTableFileId = args.json.id; + var images = []; + args.json.versions.forEach((item) => { + if (!item.deleted) { + images.unshift(item); + } + }); + this.checkPreviousImageAvailable(images); + }) + .finally(() => { + this.changeWorldImageDialogLoading = false; + }); }; - $app.methods.refreshPreviousInstanceInfoTable = function () { - var D = this.previousInstanceInfoDialog; - database.getPlayersFromInstance(D.$location.tag).then((data) => { - var array = []; - for (var entry of Array.from(data.values())) { - entry.timer = timeToText(entry.time); - array.push(entry); - } - array.sort(compareByCreatedAt); - this.previousInstanceInfoDialogTable.data = array; - D.loading = false; - workerTimers.setTimeout(() => D.forceUpdate++, 150); - }); + $app.methods.compareCurrentImage = function (image) { + if ( + `${API.endpointDomain}/file/${this.previousImagesTableFileId}/${image.version}/file` === + this.avatarDialog.ref.imageUrl + ) { + return true; + } + return false; }; - $app.data.dtHour12 = await configRepository.getBool('VRCX_dtHour12', false); - $app.data.dtIsoFormat = await configRepository.getBool( - 'VRCX_dtIsoFormat', - false - ); - $app.methods.setDatetimeFormat = async function () { - 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; + // Avatar names + + API.cachedAvatarNames = new Map(); + + $app.methods.getAvatarName = async function (imageUrl) { + var fileId = $utils.extractFileId(imageUrl); + if (!fileId) { + return { + ownerId: '', + avatarName: '-' + }; } - if (typeof this.dtIsoFormat !== 'undefined') { - if (isoFormat !== this.dtIsoFormat) { - await configRepository.setBool( - 'VRCX_dtIsoFormat', - this.dtIsoFormat - ); - } - var isoFormat = this.dtIsoFormat; + if (API.cachedAvatarNames.has(fileId)) { + return API.cachedAvatarNames.get(fileId); } - 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(',', ''); + var args = await API.getAvatarImages({ fileId }); + return this.storeAvatarImage(args); + }; + + $app.data.discordNamesDialogVisible = false; + $app.data.discordNamesContent = ''; + + $app.methods.showDiscordNamesDialog = function () { + var { friends } = API.currentUser; + if (Array.isArray(friends) === false) { + return; + } + var lines = ['DisplayName,DiscordName']; + var _ = function (str) { + if (/[\x00-\x1f,"]/.test(str) === true) { + return `"${str.replace(/"/g, '""')}"`; } - return '-'; + return str; }; - if (isoFormat) { - formatDate1 = function (date, format) { - if (!date) { - return '-'; + for (var userId of friends) { + var { ref } = this.friends.get(userId); + var discord = ''; + if (typeof ref === 'undefined') { + continue; + } + var name = ref.displayName; + if (ref.statusDescription) { + var statusRegex = /(?:discord|dc|dis)(?: |=|:|˸|;)(.*)/gi.exec( + ref.statusDescription + ); + if (statusRegex) { + discord = statusRegex[1]; } - var dt = new Date(date); - if (format === 'long') { - return dt.toISOString(); - } 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(',', ''); + } + if (!discord && ref.bio) { + var bioRegex = /(?:discord|dc|dis)(?: |=|:|˸|;)(.*)/gi.exec( + ref.bio + ); + if (bioRegex) { + discord = bioRegex[1]; } - return '-'; - }; + } + if (!discord) { + continue; + } + discord = discord.trim(); + lines.push(`${_(name)},${_(discord)}`); } - Vue.filter('formatDate', formatDate1); + this.discordNamesContent = lines.join('\n'); + this.discordNamesDialogVisible = true; }; - $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 = ''; - }; + // userDialog world/avatar tab click - $app.data.mouseDownClass = []; - $app.data.mouseUpClass = []; - $app.methods.dialogMouseDown = function (e) { - this.mouseDownClass = [...e.target.classList]; - }; - $app.methods.dialogMouseUp = function (e) { - this.mouseUpClass = [...e.target.classList]; - }; - $app.methods.beforeDialogClose = function (done) { - if ( - this.mouseDownClass.includes('el-dialog__wrapper') && - this.mouseUpClass.includes('el-dialog__wrapper') - ) { - done(); - } else if ( - this.mouseDownClass.includes('el-dialog__close') && - this.mouseUpClass.includes('el-dialog__close') - ) { - done(); - } - }; + $app.data.userDialogLastActiveTab = ''; + $app.data.userDialogLastAvatar = ''; + $app.data.userDialogLastWorld = ''; + $app.data.userDialogLastFavoriteWorld = ''; + $app.data.userDialogLastGroup = ''; - $app.methods.disableGameLogDialog = async function () { - if (this.isGameRunning) { - this.$message({ - message: - 'VRChat needs to be closed before this option can be changed', - type: 'error' - }); - this.gameLogDisabled = !this.gameLogDisabled; + $app.methods.userDialogTabClick = function (obj) { + var userId = this.userDialog.id; + if (this.userDialogLastActiveTab === obj.label) { return; } - if (this.gameLogDisabled) { - this.$confirm('Continue? Disable GameLog', 'Confirm', { - confirmButtonText: 'Confirm', - cancelButtonText: 'Cancel', - type: 'info', - callback: async (action) => { - if (action !== 'confirm') { - this.gameLogDisabled = !this.gameLogDisabled; - await configRepository.setBool( - 'VRCX_gameLogDisabled', - this.gameLogDisabled - ); - } + if (obj.label === $t('dialog.user.groups.header')) { + if (this.userDialogLastGroup !== userId) { + this.userDialogLastGroup = userId; + this.getUserGroups(userId); + } + } else if (obj.label === $t('dialog.user.avatars.header')) { + this.setUserDialogAvatars(userId); + if (this.userDialogLastAvatar !== userId) { + this.userDialogLastAvatar = userId; + if ( + userId === API.currentUser.id && + this.userDialog.avatars.length === 0 + ) { + this.refreshUserDialogAvatars(); + } else { + this.setUserDialogAvatarsRemote(userId); } - }); - } else { - await configRepository.setBool( - 'VRCX_gameLogDisabled', - this.gameLogDisabled - ); + } + } else if (obj.label === $t('dialog.user.worlds.header')) { + this.setUserDialogWorlds(userId); + if (this.userDialogLastWorld !== userId) { + this.userDialogLastWorld = userId; + this.refreshUserDialogWorlds(); + } + } else if (obj.label === $t('dialog.user.favorite_worlds.header')) { + if (this.userDialogLastFavoriteWorld !== userId) { + this.userDialogLastFavoriteWorld = userId; + this.getUserFavoriteWorlds(userId); + } + } else if (obj.label === $t('dialog.user.json.header')) { + this.refreshUserDialogTreeData(); } + this.userDialogLastActiveTab = obj.label; }; - $app.methods.getNameColour = async function (userId) { - var hue = await AppApi.GetColourFromUserID(userId); - return this.HueToHex(hue); - }; + // VRChat Config JSON - $app.methods.userColourInit = async function () { - var dictObject = await AppApi.GetColourBulk( - Array.from(API.cachedUsers.keys()) - ); - for (var [userId, hue] of Object.entries(dictObject)) { - var ref = API.cachedUsers.get(userId); - if (typeof ref !== 'undefined') { - ref.$userColour = this.HueToHex(hue); + $app.data.VRChatConfigFile = {}; + $app.data.VRChatConfigList = {}; + + $app.methods.readVRChatConfigFile = async function () { + this.VRChatConfigFile = {}; + var config = await AppApi.ReadConfigFile(); + if (config) { + try { + this.VRChatConfigFile = JSON.parse(config); + } catch { + this.$message({ + message: 'Invalid JSON in config.json', + type: 'error' + }); + throw new Error('Invalid JSON in config.json'); } } }; - $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.WriteVRChatConfigFile = function () { + var json = JSON.stringify(this.VRChatConfigFile, null, '\t'); + AppApi.WriteConfigFile(json); }; - $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.data.VRChatConfigDialog = { + visible: false }; - $app.methods.isFriendOnline = function (friend) { - if ( - typeof friend === 'undefined' || - typeof friend.ref === 'undefined' - ) { - return false; - } - if (friend.state === 'online') { - return true; + API.$on('LOGIN', function () { + $app.VRChatConfigDialog.visible = false; + }); + + $app.methods.showVRChatConfig = async function () { + this.VRChatConfigList = { + cache_size: { + name: $t('dialog.config_json.max_cache_size'), + default: '20', + type: 'number', + min: 20 + }, + cache_expiry_delay: { + name: $t('dialog.config_json.cache_expiry_delay'), + default: '30', + type: 'number', + min: 30 + }, + cache_directory: { + name: $t('dialog.config_json.cache_directory'), + default: '%AppData%\\..\\LocalLow\\VRChat\\VRChat' + }, + picture_output_folder: { + name: $t('dialog.config_json.picture_directory'), + // my pictures folder + default: `%UserProfile%\\Pictures\\VRChat` + }, + // dynamic_bone_max_affected_transform_count: { + // name: 'Dynamic Bones Limit Max Transforms (0 disable all transforms)', + // default: '32', + // type: 'number', + // min: 0 + // }, + // dynamic_bone_max_collider_check_count: { + // name: 'Dynamic Bones Limit Max Collider Collisions (0 disable all colliders)', + // default: '8', + // type: 'number', + // min: 0 + // }, + fpv_steadycam_fov: { + name: $t('dialog.config_json.fpv_steadycam_fov'), + default: '50', + type: 'number', + min: 30, + max: 110 + } + }; + await this.readVRChatConfigFile(); + this.$nextTick(() => + $app.adjustDialogZ(this.$refs.VRChatConfigDialog.$el) + ); + this.VRChatConfigDialog.visible = true; + if (!this.VRChatUsedCacheSize) { + this.getVRChatCacheSize(); } - if (friend.state !== 'online' && friend.ref.location !== 'private') { - // wat - return true; + }; + + $app.methods.saveVRChatConfigFile = function () { + for (var item in this.VRChatConfigFile) { + if (item === 'picture_output_split_by_date') { + // this one is default true, it's special + if (this.VRChatConfigFile[item]) { + delete this.VRChatConfigFile[item]; + } + } else if (this.VRChatConfigFile[item] === '') { + delete this.VRChatConfigFile[item]; + } else if ( + typeof this.VRChatConfigFile[item] === 'boolean' && + this.VRChatConfigFile[item] === false + ) { + delete this.VRChatConfigFile[item]; + } else if ( + typeof this.VRChatConfigFile[item] === 'string' && + !isNaN(this.VRChatConfigFile[item]) + ) { + this.VRChatConfigFile[item] = parseInt( + this.VRChatConfigFile[item], + 10 + ); + } } - return false; + this.VRChatConfigDialog.visible = false; + this.WriteVRChatConfigFile(); }; - $app.methods.isRealInstance = function (instanceId) { - if (!instanceId) { - return false; - } - switch (instanceId) { - case 'offline': - case 'offline:offline': - case 'private': - case 'private:private': - case 'traveling': - case 'traveling:traveling': - case instanceId.startsWith('local'): - return false; + $app.data.VRChatScreenshotResolutions = [ + { name: '1280x720 (720p)', width: 1280, height: 720 }, + { name: '1920x1080 (1080p Default)', width: '', height: '' }, + { name: '2560x1440 (1440p)', width: 2560, height: 1440 }, + { name: '3840x2160 (4K)', width: 3840, height: 2160 } + ]; + + $app.data.VRChatCameraResolutions = [ + { name: '1280x720 (720p)', width: 1280, height: 720 }, + { name: '1920x1080 (1080p Default)', width: '', height: '' }, + { name: '2560x1440 (1440p)', width: 2560, height: 1440 }, + { name: '3840x2160 (4K)', width: 3840, height: 2160 }, + { name: '7680x4320 (8K)', width: 7680, height: 4320 } + ]; + + $app.methods.getVRChatResolution = function (res) { + switch (res) { + case '1280x720': + return '1280x720 (720p)'; + case '1920x1080': + return '1920x1080 (1080p)'; + case '2560x1440': + return '2560x1440 (2K)'; + case '3840x2160': + return '3840x2160 (4K)'; + case '7680x4320': + return '7680x4320 (8K)'; } - return true; + return `${res} (Custom)`; }; - $app.methods.onPlayerTraveling = function (ref) { + $app.methods.getVRChatCameraResolution = function () { if ( - !this.isGameRunning || - !this.lastLocation.location || - this.lastLocation.location !== ref.travelingToLocation || - ref.id === API.currentUser.id || - this.lastLocation.playerList.has(ref.displayName) + this.VRChatConfigFile.camera_res_height && + this.VRChatConfigFile.camera_res_width ) { - return; + var res = `${this.VRChatConfigFile.camera_res_width}x${this.VRChatConfigFile.camera_res_height}`; + return this.getVRChatResolution(res); } - - var onPlayerJoining = { - created_at: new Date(ref.created_at).toJSON(), - userId: ref.id, - displayName: ref.displayName, - type: 'OnPlayerJoining' - }; - this.queueFeedNoty(onPlayerJoining); + return '1920x1080 (1080p)'; }; - $app.methods.updateCurrentUserLocation = function () { - API.currentUser.$travelingToTime = this.lastLocationDestinationTime; - 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; - if (API.currentUser.$location === 'traveling') { - currentLocation = API.currentUser.$travelingToLocation; - } - ref.location = API.currentUser.$locationTag; - ref.travelingToLocation = API.currentUser.$travelingToLocation; - + $app.methods.getVRChatScreenshotResolution = function () { if ( - this.isGameRunning && - !this.gameLogDisabled && - this.lastLocation.location !== '' + this.VRChatConfigFile.screenshot_res_height && + this.VRChatConfigFile.screenshot_res_width ) { - // 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; + var res = `${this.VRChatConfigFile.screenshot_res_width}x${this.VRChatConfigFile.screenshot_res_height}`; + return this.getVRChatResolution(res); } + return '1920x1080 (1080p)'; + }; - ref.$online_for = API.currentUser.$online_for; - ref.$offline_for = API.currentUser.$offline_for; - ref.$location = API.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; - } + $app.methods.setVRChatCameraResolution = function (res) { + this.VRChatConfigFile.camera_res_height = res.height; + this.VRChatConfigFile.camera_res_width = res.width; }; - $app.methods.setCurrentUserLocation = async function (location) { - API.currentUser.$location_at = Date.now(); - API.currentUser.$travelingToTime = Date.now(); - API.currentUser.$locationTag = location; - this.updateCurrentUserLocation(); + $app.methods.setVRChatScreenshotResolution = function (res) { + this.VRChatConfigFile.screenshot_res_height = res.height; + this.VRChatConfigFile.screenshot_res_width = res.width; + }; - // 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 (this.isRealInstance(location) && lastLocation !== location) { - var dt = new Date().toJSON(); - var L = API.parseLocation(location); - 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); + // Auto Launch Shortcuts - this.applyUserDialogLocation(); - this.applyWorldDialogInstances(); - this.applyGroupDialogInstances(); - } + $app.methods.openShortcutFolder = function () { + AppApi.OpenShortcutFolder(); }; - $app.data.avatarHistory = new Set(); - $app.data.avatarHistoryArray = []; + $app.methods.updateAppLauncherSettings = async function () { + await configRepository.setBool( + 'VRCX_enableAppLauncher', + this.enableAppLauncher + ); + await configRepository.setBool( + 'VRCX_enableAppLauncherAutoClose', + this.enableAppLauncherAutoClose + ); + await AppApi.SetAppLauncherSettings( + this.enableAppLauncher, + this.enableAppLauncherAutoClose + ); + }; - $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++) { - this.avatarHistory.add(historyArray[i].id); - API.applyAvatar(historyArray[i]); - } + // Screenshot Helper + + $app.methods.saveScreenshotHelper = async function () { + await configRepository.setBool( + 'VRCX_screenshotHelper', + this.screenshotHelper + ); + await configRepository.setBool( + 'VRCX_screenshotHelperModifyFilename', + this.screenshotHelperModifyFilename + ); + await configRepository.setBool( + 'VRCX_screenshotHelperCopyToClipboard', + this.screenshotHelperCopyToClipboard + ); }; - $app.methods.addAvatarToHistory = function (avatarId) { - API.getAvatar({ avatarId }).then((args) => { - var { ref } = args; - 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); - } + $app.methods.processScreenshot = async function (path) { + var newPath = path; + if (this.screenshotHelper) { + var location = $utils.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 + }); } - this.avatarHistoryArray.unshift(ref); - database.addAvatarToCache(ref); - - this.avatarHistory.delete(ref.id); - this.avatarHistory.add(ref.id); - database.addAvatarToHistory(ref.id); - }); + newPath = await AppApi.AddScreenshotMetadata( + path, + JSON.stringify(metadata), + location.worldId, + this.screenshotHelperModifyFilename + ); + } + if (this.screenshotHelperCopyToClipboard) { + await AppApi.CopyImageToClipboard(newPath); + } }; - $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.getAndDisplayScreenshot = function ( + path, + needsCarouselFiles = true + ) { + AppApi.GetScreenshotMetadata(path).then((metadata) => + this.displayScreenshotMetadata(metadata, needsCarouselFiles) + ); }; - $app.methods.clearAvatarHistory = function () { - this.avatarHistory = new Set(); - this.avatarHistoryArray = []; - database.clearAvatarHistory(); + $app.methods.getAndDisplayLastScreenshot = function () { + this.screenshotMetadataResetSearch(); + AppApi.GetLastScreenshot().then((path) => + this.getAndDisplayScreenshot(path) + ); }; - $app.data.databaseVersion = await configRepository.getInt( - 'VRCX_databaseVersion', - 0 - ); + /** + * Function receives an unmodified json string grabbed from the screenshot file + * Error checking and and verification of data is done in .NET already; In the case that the data/file is invalid, a JSON object with the token "error" will be returned containing a description of the problem. + * Example: {"error":"Invalid file selected. Please select a valid VRChat screenshot."} + * See docs/screenshotMetadata.json for schema + * @param {string} metadata - JSON string grabbed from PNG file + * @param {string} needsCarouselFiles - Whether or not to get the last/next files for the carousel + * @returns {void} + */ + $app.methods.displayScreenshotMetadata = async function ( + json, + needsCarouselFiles = true + ) { + var D = this.screenshotMetadataDialog; + var metadata = JSON.parse(json); + if (typeof metadata === 'undefined' || !metadata.sourceFile) { + D.metadata = {}; + D.metadata.error = + 'Invalid file selected. Please select a valid VRChat screenshot.'; + return; + } - $app.methods.updateDatabaseVersion = async function () { - var databaseVersion = 9; - 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.updateTableForGroupNames(); // alter tables to include group name - await database.fixBrokenNotifications(); // fix notifications being null - await database.fixBrokenGroupChange(); // fix spam group left & name change - await database.fixCancelFriendRequestTypo(); // fix CancelFriendRequst typo - await database.vacuum(); // succ - 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(); + // Get extra data for display dialog like resolution, file size, etc + D.loading = true; + var extraData = await AppApi.GetExtraScreenshotData( + metadata.sourceFile, + needsCarouselFiles + ); + D.loading = false; + var extraDataObj = JSON.parse(extraData); + Object.assign(metadata, extraDataObj); + + // console.log("Displaying screenshot metadata", json, "extra data", extraDataObj, "path", json.filePath) + + D.metadata = metadata; + + var regex = metadata.fileName.match( + /VRChat_((\d{3,})x(\d{3,})_(\d{4})-(\d{2})-(\d{2})_(\d{2})-(\d{2})-(\d{2})\.(\d{1,})|(\d{4})-(\d{2})-(\d{2})_(\d{2})-(\d{2})-(\d{2})\.(\d{3})_(\d{3,})x(\d{3,}))/ + ); + if (regex) { + if (typeof regex[2] !== 'undefined' && regex[4].length === 4) { + // old format + // VRChat_3840x2160_2022-02-02_03-21-39.771 + var date = `${regex[4]}-${regex[5]}-${regex[6]}`; + var time = `${regex[7]}:${regex[8]}:${regex[9]}`; + D.metadata.dateTime = Date.parse(`${date} ${time}`); + // D.metadata.resolution = `${regex[2]}x${regex[3]}`; + } else if ( + typeof regex[11] !== 'undefined' && + regex[11].length === 4 + ) { + // new format + // VRChat_2023-02-16_10-39-25.274_3840x2160 + var date = `${regex[11]}-${regex[12]}-${regex[13]}`; + var time = `${regex[14]}:${regex[15]}:${regex[16]}`; + D.metadata.dateTime = Date.parse(`${date} ${time}`); + // D.metadata.resolution = `${regex[18]}x${regex[19]}`; } } - }; + if (!D.metadata.dateTime) { + D.metadata.dateTime = Date.parse(json.creationDate); + } - // #endregion - // #region | App: world favorite import + if (this.fullscreenImageDialog?.visible) { + this.showFullscreenImageDialog(D.metadata.filePath); + } else { + this.openScreenshotMetadataDialog(); + } + }; - $app.data.worldImportDialog = { + $app.data.screenshotMetadataDialog = { visible: false, loading: false, - progress: 0, - progressTotal: 0, - input: '', - worldIdList: new Set(), - errors: '', - worldImportFavoriteGroup: null, - worldImportLocalFavoriteGroup: null, - importProgress: 0, - importProgressTotal: 0 - }; - - $app.data.worldImportTable = { - data: [], - tableProps: { - stripe: true, - size: 'mini' - }, - layout: 'table' + search: '', + searchType: 'Player Name', + searchTypes: ['Player Name', 'Player ID', 'World Name', 'World ID'], + metadata: {}, + isUploading: false }; - $app.methods.showWorldImportDialog = function () { - this.$nextTick(() => adjustDialogZ(this.$refs.worldImportDialog.$el)); - var D = this.worldImportDialog; - this.resetWorldImport(); + $app.methods.openScreenshotMetadataDialog = function () { + this.$nextTick(() => + $app.adjustDialogZ(this.$refs.screenshotMetadataDialog.$el) + ); + var D = this.screenshotMetadataDialog; D.visible = true; }; - $app.methods.processWorldImportList = async function () { - var D = this.worldImportDialog; - D.loading = true; - var regexWorldId = - /wrld_[0-9A-Fa-f]{8}-([0-9A-Fa-f]{4}-){3}[0-9A-Fa-f]{12}/g; - var match = []; - var worldIdList = new Set(); - while ((match = regexWorldId.exec(D.input)) !== null) { - worldIdList.add(match[0]); + $app.methods.showScreenshotMetadataDialog = function () { + var D = this.screenshotMetadataDialog; + if (!D.metadata.filePath) { + this.getAndDisplayLastScreenshot(); } - D.input = ''; - D.errors = ''; - D.progress = 0; - D.progressTotal = worldIdList.size; - var data = Array.from(worldIdList); - for (var i = 0; i < data.length; ++i) { - if (!D.visible) { - this.resetWorldImport(); - } - if (!D.loading || !D.visible) { - break; + this.openScreenshotMetadataDialog(); + }; + + $app.methods.screenshotMetadataResetSearch = function () { + var D = this.screenshotMetadataDialog; + + D.search = ''; + D.searchIndex = null; + D.searchResults = null; + }; + + $app.data.screenshotMetadataSearchInputs = 0; + $app.methods.screenshotMetadataSearch = function () { + var D = this.screenshotMetadataDialog; + + // Don't search if user is still typing + this.screenshotMetadataSearchInputs++; + let current = this.screenshotMetadataSearchInputs; + setTimeout(() => { + if (current !== this.screenshotMetadataSearchInputs) { + return; } - var worldId = data[i]; - if (!D.worldIdList.has(worldId)) { - try { - var args = await API.getWorld({ - worldId - }); - this.worldImportTable.data.push(args.ref); - D.worldIdList.add(worldId); - } catch (err) { - D.errors = D.errors.concat( - `WorldId: ${worldId}\n${err}\n\n` - ); + this.screenshotMetadataSearchInputs = 0; + + if (D.search === '') { + this.screenshotMetadataResetSearch(); + if (D.metadata.filePath !== null) { + // Re-retrieve the current screenshot metadata and get previous/next files for regular carousel directory navigation + this.getAndDisplayScreenshot(D.metadata.filePath, true); } + return; } - D.progress++; - if (D.progress === worldIdList.size) { - D.progress = 0; - } - } - D.loading = false; - }; - $app.methods.deleteItemWorldImport = function (ref) { - var D = this.worldImportDialog; - removeFromArray(this.worldImportTable.data, ref); - D.worldIdList.delete(ref.id); - }; + var searchType = D.searchTypes.indexOf(D.searchType); // Matches the search type enum in .NET + D.loading = true; + AppApi.FindScreenshotsBySearch(D.search, searchType) + .then((json) => { + var results = JSON.parse(json); - $app.methods.resetWorldImport = function () { - var D = this.worldImportDialog; - D.input = ''; - D.errors = ''; - }; + if (results.length === 0) { + D.metadata = {}; + D.metadata.error = 'No results found'; - $app.methods.clearWorldImportTable = function () { - var D = this.worldImportDialog; - this.worldImportTable.data = []; - D.worldIdList = new Set(); - }; + D.searchIndex = null; + D.searchResults = null; + return; + } - $app.methods.selectWorldImportGroup = function (group) { - var D = this.worldImportDialog; - D.worldImportLocalFavoriteGroup = null; - D.worldImportFavoriteGroup = group; - }; + D.searchIndex = 0; + D.searchResults = results; - $app.methods.selectWorldImportLocalGroup = function (group) { - var D = this.worldImportDialog; - D.worldImportFavoriteGroup = null; - D.worldImportLocalFavoriteGroup = group; + // console.log("Search results", results) + this.getAndDisplayScreenshot(results[0], false); + }) + .finally(() => { + D.loading = false; + }); + }, 500); }; - $app.methods.cancelWorldImport = function () { - var D = this.worldImportDialog; - D.loading = false; - }; + $app.methods.screenshotMetadataCarouselChangeSearch = function (index) { + var D = this.screenshotMetadataDialog; + var searchIndex = D.searchIndex; + var filesArr = D.searchResults; - $app.methods.importWorldImportTable = async function () { - var D = this.worldImportDialog; - if (!D.worldImportFavoriteGroup && !D.worldImportLocalFavoriteGroup) { + if (searchIndex === null) { return; } - D.loading = true; - var data = [...this.worldImportTable.data].reverse(); - D.importProgressTotal = data.length; - try { - for (var i = data.length - 1; i >= 0; i--) { - if (!D.loading || !D.visible) { - break; - } - var ref = data[i]; - if (D.worldImportFavoriteGroup) { - await this.addFavoriteWorld( - ref, - D.worldImportFavoriteGroup - ); - } else if (D.worldImportLocalFavoriteGroup) { - this.addLocalWorldFavorite( - ref.id, - D.worldImportLocalFavoriteGroup - ); - } - removeFromArray(this.worldImportTable.data, ref); - D.worldIdList.delete(ref.id); - D.importProgress++; + + if (index === 0) { + if (searchIndex > 0) { + this.getAndDisplayScreenshot(filesArr[searchIndex - 1], false); + searchIndex--; + } else { + this.getAndDisplayScreenshot( + filesArr[filesArr.length - 1], + false + ); + searchIndex = filesArr.length - 1; + } + } else if (index === 2) { + if (searchIndex < filesArr.length - 1) { + this.getAndDisplayScreenshot(filesArr[searchIndex + 1], false); + searchIndex++; + } else { + this.getAndDisplayScreenshot(filesArr[0], false); + searchIndex = 0; } - } catch (err) { - D.errors = `Name: ${ref.name}\nWorldId: ${ref.id}\n${err}\n\n`; - } finally { - D.importProgress = 0; - D.importProgressTotal = 0; - D.loading = false; } - }; - - API.$on('LOGIN', function () { - $app.clearWorldImportTable(); - $app.resetWorldImport(); - $app.worldImportDialog.visible = false; - $app.worldImportFavoriteGroup = null; - $app.worldImportLocalFavoriteGroup = null; - $app.worldExportDialogVisible = false; - $app.worldExportFavoriteGroup = null; - $app.worldExportLocalFavoriteGroup = null; - }); + if (typeof this.$refs.screenshotMetadataCarousel !== 'undefined') { + this.$refs.screenshotMetadataCarousel.setActiveItem(1); + } - // #endregion - // #region | App: world favorite export + D.searchIndex = searchIndex; + }; - $app.data.worldExportDialogRef = {}; - $app.data.worldExportDialogVisible = false; - $app.data.worldExportContent = ''; - $app.data.worldExportFavoriteGroup = null; - $app.data.worldExportLocalFavoriteGroup = null; + $app.methods.screenshotMetadataCarouselChange = function (index) { + var D = this.screenshotMetadataDialog; + var searchIndex = D.searchIndex; - $app.methods.showWorldExportDialog = function () { - this.$nextTick(() => - adjustDialogZ(this.$refs.worldExportDialogRef.$el) - ); - this.worldExportFavoriteGroup = null; - this.worldExportLocalFavoriteGroup = null; - this.updateWorldExportDialog(); - this.worldExportDialogVisible = true; - }; + if (searchIndex !== null) { + this.screenshotMetadataCarouselChangeSearch(index); + return; + } - $app.methods.updateWorldExportDialog = function () { - var _ = function (str) { - if (/[\x00-\x1f,"]/.test(str) === true) { - return `"${str.replace(/"/g, '""')}"`; - } - return str; - }; - var lines = ['WorldID,Name']; - if (this.worldExportFavoriteGroup) { - API.favoriteWorldGroups.forEach((group) => { - if (this.worldExportFavoriteGroup === group) { - $app.favoriteWorlds.forEach((ref) => { - if (group.key === ref.groupKey) { - lines.push(`${_(ref.id)},${_(ref.name)}`); - } - }); - } - }); - } else if (this.worldExportLocalFavoriteGroup) { - var favoriteGroup = - this.localWorldFavorites[this.worldExportLocalFavoriteGroup]; - if (!favoriteGroup) { - return; - } - for (var i = 0; i < favoriteGroup.length; ++i) { - var ref = favoriteGroup[i]; - lines.push(`${_(ref.id)},${_(ref.name)}`); + if (index === 0) { + if (D.metadata.previousFilePath) { + this.getAndDisplayScreenshot(D.metadata.previousFilePath); + } else { + this.getAndDisplayScreenshot(D.metadata.filePath); } - } else { - // export all - this.favoriteWorlds.forEach((ref1) => { - lines.push(`${_(ref1.id)},${_(ref1.name)}`); - }); - for (var i = 0; i < this.localWorldFavoritesList.length; ++i) { - var worldId = this.localWorldFavoritesList[i]; - var ref2 = API.cachedWorlds.get(worldId); - if (typeof ref2 !== 'undefined') { - lines.push(`${_(ref2.id)},${_(ref2.name)}`); - } + } + if (index === 2) { + if (D.metadata.nextFilePath) { + this.getAndDisplayScreenshot(D.metadata.nextFilePath); + } else { + this.getAndDisplayScreenshot(D.metadata.filePath); } } - this.worldExportContent = lines.join('\n'); + if (typeof this.$refs.screenshotMetadataCarousel !== 'undefined') { + this.$refs.screenshotMetadataCarousel.setActiveItem(1); + } + + if (this.fullscreenImageDialog.visible) { + // TODO + } }; - $app.methods.selectWorldExportGroup = function (group) { - this.worldExportFavoriteGroup = group; - this.worldExportLocalFavoriteGroup = null; - this.updateWorldExportDialog(); + $app.methods.uploadScreenshotToGallery = function () { + var D = this.screenshotMetadataDialog; + if (D.metadata.fileSizeBytes > 10000000) { + $app.$message({ + message: 'File size too large', + type: 'error' + }); + return; + } + D.isUploading = true; + AppApi.GetFileBase64(D.metadata.filePath) + .then((base64Body) => { + API.uploadGalleryImage(base64Body) + .then((args) => { + $app.$message({ + message: 'Gallery image uploaded', + type: 'success' + }); + return args; + }) + .finally(() => { + D.isUploading = false; + }); + }) + .catch((err) => { + $app.$message({ + message: 'Failed to upload gallery image', + type: 'error' + }); + console.error(err); + D.isUploading = false; + }); }; - $app.methods.selectWorldExportLocalGroup = function (group) { - this.worldExportLocalFavoriteGroup = group; - this.worldExportFavoriteGroup = null; - this.updateWorldExportDialog(); + /** + * 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; }; - // #endregion - // #region | App: avatar favorite import + $app.methods.handleDrop = function (event) { + if (this.currentlyDroppingFile === null) { + return; + } + console.log('Dropped file into viewer: ', this.currentlyDroppingFile); + + this.screenshotMetadataResetSearch(); + this.getAndDisplayScreenshot(this.currentlyDroppingFile); + + event.preventDefault(); + }; - $app.data.avatarImportDialog = { - visible: false, - loading: false, - progress: 0, - progressTotal: 0, - input: '', - avatarIdList: new Set(), - errors: '', - avatarImportFavoriteGroup: null, - avatarImportLocalFavoriteGroup: null, - importProgress: 0, - importProgressTotal: 0 + $app.methods.copyImageToClipboard = function (path) { + AppApi.CopyImageToClipboard(path).then(() => { + this.$message({ + message: 'Image copied to clipboard', + type: 'success' + }); + }); }; - $app.data.avatarImportTable = { - data: [], - tableProps: { - stripe: true, - size: 'mini' - }, - layout: 'table' + $app.methods.openImageFolder = function (path) { + AppApi.OpenFolderAndSelectItem(path).then(() => { + this.$message({ + message: 'Opened image folder', + type: 'success' + }); + }); }; - $app.methods.showAvatarImportDialog = function () { - this.$nextTick(() => adjustDialogZ(this.$refs.avatarImportDialog.$el)); - var D = this.avatarImportDialog; - this.resetAvatarImport(); - D.visible = true; + // YouTube API + + $app.data.youTubeApiDialog = { + visible: false }; - $app.methods.processAvatarImportList = async function () { - var D = this.avatarImportDialog; - D.loading = true; - var regexAvatarId = - /avtr_[0-9A-Fa-f]{8}-([0-9A-Fa-f]{4}-){3}[0-9A-Fa-f]{12}/g; - var match = []; - var avatarIdList = new Set(); - while ((match = regexAvatarId.exec(D.input)) !== null) { - avatarIdList.add(match[0]); + API.$on('LOGOUT', function () { + $app.youTubeApiDialog.visible = false; + }); + + $app.methods.testYouTubeApiKey = async function () { + if (!this.youTubeApiKey) { + this.$message({ + message: 'YouTube API key removed', + type: 'success' + }); + this.youTubeApiDialog.visible = false; + return; } - D.input = ''; - D.errors = ''; - D.progress = 0; - D.progressTotal = avatarIdList.size; - var data = Array.from(avatarIdList); - for (var i = 0; i < data.length; ++i) { - if (!D.visible) { - this.resetAvatarImport(); - } - if (!D.loading || !D.visible) { - break; - } - var avatarId = data[i]; - if (!D.avatarIdList.has(avatarId)) { - try { - var args = await API.getAvatar({ - avatarId - }); - this.avatarImportTable.data.push(args.ref); - D.avatarIdList.add(avatarId); - } catch (err) { - D.errors = D.errors.concat( - `AvatarId: ${avatarId}\n${err}\n\n` - ); - } - } - D.progress++; - if (D.progress === avatarIdList.size) { - D.progress = 0; - } + var data = await this.lookupYouTubeVideo('dQw4w9WgXcQ'); + if (!data) { + this.youTubeApiKey = ''; + this.$message({ + message: 'Invalid YouTube API key', + type: 'error' + }); + } else { + await configRepository.setString( + 'VRCX_youtubeAPIKey', + this.youTubeApiKey + ); + this.$message({ + message: 'YouTube API key valid!', + type: 'success' + }); + this.youTubeApiDialog.visible = false; } - D.loading = false; - }; - - $app.methods.deleteItemAvatarImport = function (ref) { - var D = this.avatarImportDialog; - removeFromArray(this.avatarImportTable.data, ref); - D.avatarIdList.delete(ref.id); }; - $app.methods.resetAvatarImport = function () { - var D = this.avatarImportDialog; - D.input = ''; - D.errors = ''; + $app.methods.changeYouTubeApi = async function () { + 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.clearAvatarImportTable = function () { - var D = this.avatarImportDialog; - this.avatarImportTable.data = []; - D.avatarIdList = new Set(); + $app.methods.showYouTubeApiDialog = function () { + this.$nextTick(() => + $app.adjustDialogZ(this.$refs.youTubeApiDialog.$el) + ); + var D = this.youTubeApiDialog; + D.visible = true; }; - $app.methods.selectAvatarImportGroup = function (group) { - var D = this.avatarImportDialog; - D.avatarImportLocalFavoriteGroup = null; - D.avatarImportFavoriteGroup = group; - }; + // Asset Bundle Cacher - $app.methods.selectAvatarImportLocalGroup = function (group) { - var D = this.avatarImportDialog; - D.avatarImportFavoriteGroup = null; - D.avatarImportLocalFavoriteGroup = group; + $app.methods.updateVRChatWorldCache = function () { + var D = this.worldDialog; + if (D.visible) { + D.inCache = false; + D.cacheSize = 0; + D.cacheLocked = false; + D.cachePath = ''; + this.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.cancelAvatarImport = function () { - var D = this.avatarImportDialog; - D.loading = false; + $app.methods.updateVRChatAvatarCache = function () { + var D = this.avatarDialog; + if (D.visible) { + D.inCache = false; + D.cacheSize = 0; + D.cacheLocked = false; + D.cachePath = ''; + this.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.importAvatarImportTable = async function () { - var D = this.avatarImportDialog; - if (!D.avatarImportFavoriteGroup && !D.avatarImportLocalFavoriteGroup) { - return; + // eslint-disable-next-line require-await + $app.methods.checkVRChatCache = async function (ref) { + if (!ref.unityPackages) { + return { Item1: -1, Item2: false, Item3: '' }; } - D.loading = true; - var data = [...this.avatarImportTable.data].reverse(); - D.importProgressTotal = data.length; - try { - for (var i = data.length - 1; i >= 0; i--) { - if (!D.loading || !D.visible) { - break; - } - var ref = data[i]; - if (D.avatarImportFavoriteGroup) { - await this.addFavoriteAvatar( - ref, - D.avatarImportFavoriteGroup - ); - } else if (D.avatarImportLocalFavoriteGroup) { - this.addLocalAvatarFavorite( - ref.id, - D.avatarImportLocalFavoriteGroup - ); + var assetUrl = ''; + var variant = ''; + for (var i = ref.unityPackages.length - 1; i > -1; i--) { + var unityPackage = ref.unityPackages[i]; + if ( + unityPackage.variant && + unityPackage.variant !== 'standard' && + unityPackage.variant !== 'security' + ) { + continue; + } + if ( + unityPackage.platform === 'standalonewindows' && + this.compareUnityVersion(unityPackage.unitySortNumber) + ) { + assetUrl = unityPackage.assetUrl; + if (unityPackage.variant !== 'standard') { + variant = unityPackage.variant; } - removeFromArray(this.avatarImportTable.data, ref); - D.avatarIdList.delete(ref.id); - D.importProgress++; + break; } - } catch (err) { - D.errors = `Name: ${ref.name}\nAvatarId: ${ref.id}\n${err}\n\n`; - } finally { - D.importProgress = 0; - D.importProgressTotal = 0; - D.loading = false; } + if (!assetUrl) { + assetUrl = ref.assetUrl; + } + var id = $utils.extractFileId(assetUrl); + var version = parseInt($utils.extractFileVersion(assetUrl), 10); + var variantVersion = parseInt( + $utils.extractVariantVersion(assetUrl), + 10 + ); + if (!id || !version) { + return { Item1: -1, Item2: false, Item3: '' }; + } + + return AssetBundleCacher.CheckVRChatCache( + id, + version, + variant, + variantVersion + ); }; - API.$on('LOGIN', function () { - $app.clearAvatarImportTable(); - $app.resetAvatarImport(); - $app.avatarImportDialog.visible = false; - $app.avatarImportFavoriteGroup = null; - $app.avatarImportLocalFavoriteGroup = null; + API.getBundles = function (fileId) { + return this.call(`file/${fileId}`, { + method: 'GET' + }).then((json) => { + var args = { + json + }; + return args; + }); + }; - $app.avatarExportDialogVisible = false; - $app.avatarExportFavoriteGroup = null; - $app.avatarExportLocalFavoriteGroup = null; - }); + $app.data.cacheAutoDownloadHistory = new Set(); - // #endregion - // #region | App: avatar favorite export + $app.methods.downloadFileQueueUpdate = async function () { + if (this.downloadQueue.size === 0) { + return; + } + this.downloadProgress = 0; + this.downloadIsProcessing = false; + this.downloadInProgress = true; + this.downloadCurrent = this.downloadQueue.values().next().value; + this.downloadCurrent.id = this.downloadQueue.keys().next().value; + var { ref } = this.downloadCurrent; + this.downloadQueue.delete(ref.id); + this.downloadQueueTable.data = Array.from(this.downloadQueue.values()); - $app.data.avatarExportDialogRef = {}; - $app.data.avatarExportDialogVisible = false; - $app.data.avatarExportContent = ''; - $app.data.avatarExportFavoriteGroup = null; - $app.data.avatarExportLocalFavoriteGroup = null; + var fileUrl = this.downloadCurrent.updateSetupUrl; + var hashUrl = this.downloadCurrent.updateHashUrl; + var size = this.downloadCurrent.size; + await AssetBundleCacher.DownloadFile(fileUrl, hashUrl, size); + this.downloadFileProgress(); + }; - $app.methods.showAvatarExportDialog = function () { - this.$nextTick(() => - adjustDialogZ(this.$refs.avatarExportDialogRef.$el) - ); - this.avatarExportFavoriteGroup = null; - this.avatarExportLocalFavoriteGroup = null; - this.updateAvatarExportDialog(); - this.avatarExportDialogVisible = true; + $app.methods.cancelDownload = function (id) { + AssetBundleCacher.CancelDownload(); + if (this.downloadQueue.has(id)) { + this.downloadQueue.delete(id); + this.downloadQueueTable.data = Array.from( + this.downloadQueue.values() + ); + } }; - $app.methods.updateAvatarExportDialog = function () { - var _ = function (str) { - if (/[\x00-\x1f,"]/.test(str) === true) { - return `"${str.replace(/"/g, '""')}"`; - } - return str; - }; - var lines = ['AvatarID,Name']; - if (this.avatarExportFavoriteGroup) { - API.favoriteAvatarGroups.forEach((group) => { - if ( - !this.avatarExportFavoriteGroup || - this.avatarExportFavoriteGroup === group - ) { - $app.favoriteAvatars.forEach((ref) => { - if (group.key === ref.groupKey) { - lines.push(`${_(ref.id)},${_(ref.name)}`); - } - }); - } - }); - } else if (this.avatarExportLocalFavoriteGroup) { - var favoriteGroup = - this.localAvatarFavorites[this.avatarExportLocalFavoriteGroup]; - if (!favoriteGroup) { + $app.methods.cancelAllDownloads = function () { + if (typeof this.downloadCurrent.id !== 'undefined') { + this.cancelDownload(this.downloadCurrent.id); + } + for (var queue of this.downloadQueue.values()) { + this.cancelDownload(queue.ref.id); + } + }; + + $app.data.downloadProgress = 0; + $app.data.downloadInProgress = false; + $app.data.downloadIsProcessing = false; + $app.data.downloadQueue = new Map(); + $app.data.downloadCurrent = {}; + + $app.methods.downloadFileProgress = async function () { + var downloadProgress = await AssetBundleCacher.CheckDownloadProgress(); + switch (downloadProgress) { + case -4: + this.$message({ + message: 'Download canceled', + type: 'info' + }); + this.downloadFileComplete('Canceled'); return; - } - for (var i = 0; i < favoriteGroup.length; ++i) { - var ref = favoriteGroup[i]; - lines.push(`${_(ref.id)},${_(ref.name)}`); - } - } else { - // export all - this.favoriteAvatars.forEach((ref1) => { - lines.push(`${_(ref1.id)},${_(ref1.name)}`); - }); - for (var i = 0; i < this.localAvatarFavoritesList.length; ++i) { - var avatarId = this.localAvatarFavoritesList[i]; - var ref2 = API.cachedAvatars.get(avatarId); - if (typeof ref2 !== 'undefined') { - lines.push(`${_(ref2.id)},${_(ref2.name)}`); + case -14: + this.$message({ + message: 'Download failed, hash mismatch', + type: 'error' + }); + this.downloadFileComplete('Failed'); + return; + case -15: + this.$message({ + message: 'Download failed, size mismatch', + type: 'error' + }); + this.downloadFileComplete('Failed'); + return; + case -16: + if (this.downloadCurrent.ref.id === 'VRCXUpdate') { + this.downloadDialog.visible = false; + this.pendingVRCXInstall = this.downloadCurrent.ref.name; + this.showVRCXUpdateDialog(); } - } + this.downloadFileComplete('Success'); + return; + default: + this.downloadProgress = downloadProgress; } - this.avatarExportContent = lines.join('\n'); + workerTimers.setTimeout(() => this.downloadFileProgress(), 150); }; - $app.methods.selectAvatarExportGroup = function (group) { - this.avatarExportFavoriteGroup = group; - this.avatarExportLocalFavoriteGroup = null; - this.updateAvatarExportDialog(); + $app.methods.downloadFileComplete = function (status) { + this.downloadCurrent.status = status; + this.downloadCurrent.date = Date.now(); + this.downloadHistoryTable.data.unshift(this.downloadCurrent); + this.downloadCurrent = {}; + this.downloadProgress = 0; + this.downloadInProgress = false; + this.downloadFileQueueUpdate(); }; - $app.methods.selectAvatarExportLocalGroup = function (group) { - this.avatarExportLocalFavoriteGroup = group; - this.avatarExportFavoriteGroup = null; - this.updateAvatarExportDialog(); + $app.methods.showDownloadDialog = function () { + this.$nextTick(() => $app.adjustDialogZ(this.$refs.downloadDialog.$el)); + this.downloadDialog.visible = true; }; - // #endregion - // #region | App: friend favorite import - - $app.data.friendImportDialog = { - visible: false, - loading: false, - progress: 0, - progressTotal: 0, - input: '', - userIdList: new Set(), - errors: '', - friendImportFavoriteGroup: null, - importProgress: 0, - importProgressTotal: 0 + $app.data.downloadDialog = { + visible: false }; - $app.data.friendImportTable = { - data: [], - tableProps: { - stripe: true, - size: 'mini' - }, - layout: 'table' + $app.methods.downloadProgressText = function () { + if (this.downloadIsProcessing) { + return 'Processing'; + } + if (this.downloadProgress >= 0) { + return `${this.downloadProgress}%`; + } + return ''; }; - $app.methods.showFriendImportDialog = function () { - this.$nextTick(() => adjustDialogZ(this.$refs.friendImportDialog.$el)); - var D = this.friendImportDialog; - this.resetFriendImport(); - D.visible = true; + $app.methods.getDisplayName = function (userId) { + if (userId) { + var ref = API.cachedUsers.get(userId); + if (ref.displayName) { + return ref.displayName; + } + } + return ''; }; - $app.methods.processFriendImportList = async function () { - var D = this.friendImportDialog; - D.loading = true; - var regexFriendId = - /usr_[0-9A-Fa-f]{8}-([0-9A-Fa-f]{4}-){3}[0-9A-Fa-f]{12}/g; - var match = []; - var userIdList = new Set(); - while ((match = regexFriendId.exec(D.input)) !== null) { - userIdList.add(match[0]); - } - D.input = ''; - D.errors = ''; - D.progress = 0; - D.progressTotal = userIdList.size; - var data = Array.from(userIdList); - for (var i = 0; i < data.length; ++i) { - if (!D.visible) { - this.resetFriendImport(); - } - if (!D.loading || !D.visible) { - break; - } - var userId = data[i]; - if (!D.userIdList.has(userId)) { - try { - var args = await API.getUser({ - userId - }); - this.friendImportTable.data.push(args.ref); - D.userIdList.add(userId); - } catch (err) { - D.errors = D.errors.concat(`UserId: ${userId}\n${err}\n\n`); - } + $app.methods.deleteVRChatCache = async function (ref) { + var assetUrl = ''; + var variant = ''; + for (var i = ref.unityPackages.length - 1; i > -1; i--) { + var unityPackage = ref.unityPackages[i]; + if ( + unityPackage.variant && + unityPackage.variant !== 'standard' && + unityPackage.variant !== 'security' + ) { + continue; } - D.progress++; - if (D.progress === userIdList.size) { - D.progress = 0; + if ( + unityPackage.platform === 'standalonewindows' && + this.compareUnityVersion(unityPackage.unitySortNumber) + ) { + assetUrl = unityPackage.assetUrl; + if (unityPackage.variant !== 'standard') { + variant = unityPackage.variant; + } + break; } } - D.loading = false; - }; - - $app.methods.deleteItemFriendImport = function (ref) { - var D = this.friendImportDialog; - removeFromArray(this.friendImportTable.data, ref); - D.userIdList.delete(ref.id); + var id = $utils.extractFileId(assetUrl); + var version = parseInt($utils.extractFileVersion(assetUrl), 10); + var variantVersion = parseInt( + $utils.extractVariantVersion(assetUrl), + 10 + ); + await AssetBundleCacher.DeleteCache( + id, + version, + variant, + variantVersion + ); + this.getVRChatCacheSize(); + this.updateVRChatWorldCache(); + this.updateVRChatAvatarCache(); }; - $app.methods.resetFriendImport = function () { - var D = this.friendImportDialog; - D.input = ''; - D.errors = ''; + $app.methods.showDeleteAllVRChatCacheConfirm = function () { + this.$confirm(`Continue? Delete all VRChat cache`, 'Confirm', { + confirmButtonText: 'Confirm', + cancelButtonText: 'Cancel', + type: 'info', + callback: (action) => { + if (action === 'confirm') { + this.deleteAllVRChatCache(); + } + } + }); }; - $app.methods.clearFriendImportTable = function () { - var D = this.friendImportDialog; - this.friendImportTable.data = []; - D.userIdList = new Set(); + $app.methods.deleteAllVRChatCache = async function () { + await AssetBundleCacher.DeleteAllCache(); + this.getVRChatCacheSize(); }; - $app.methods.selectFriendImportGroup = function (group) { - var D = this.friendImportDialog; - D.friendImportFavoriteGroup = group; + $app.methods.autoVRChatCacheManagement = function () { + if (this.autoSweepVRChatCache) { + this.sweepVRChatCache(); + } }; - $app.methods.cancelFriendImport = function () { - var D = this.friendImportDialog; - D.loading = false; + $app.methods.sweepVRChatCache = async function () { + await AssetBundleCacher.SweepCache(); + if (this.VRChatConfigDialog.visible) { + this.getVRChatCacheSize(); + } }; - $app.methods.importFriendImportTable = async function () { - var D = this.friendImportDialog; - D.loading = true; - if (!D.friendImportFavoriteGroup) { + $app.methods.checkIfGameCrashed = function () { + if (!this.relaunchVRChatAfterCrash) { return; } - var data = [...this.friendImportTable.data].reverse(); - D.importProgressTotal = data.length; - try { - for (var i = data.length - 1; i >= 0; i--) { - if (!D.loading || !D.visible) { - break; - } - var ref = data[i]; - await this.addFavoriteUser(ref, D.friendImportFavoriteGroup); - removeFromArray(this.friendImportTable.data, ref); - D.userIdList.delete(ref.id); - D.importProgress++; + var { location } = this.lastLocation; + AppApi.VrcClosedGracefully().then((result) => { + if (result || !this.isRealInstance(location)) { + return; } - } catch (err) { - D.errors = `Name: ${ref.displayName}\nUserId: ${ref.id}\n${err}\n\n`; - } finally { - D.importProgress = 0; - D.importProgressTotal = 0; - D.loading = false; - } + // 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 + ); + }); }; - API.$on('LOGIN', function () { - $app.clearFriendImportTable(); - $app.resetFriendImport(); - $app.friendImportDialog.visible = false; - $app.friendImportFavoriteGroup = null; - - $app.friendExportDialogVisible = false; - $app.friendExportFavoriteGroup = null; - }); - - // #endregion - // #region | App: friend favorite export + $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.friendExportDialogRef = {}; - $app.data.friendExportDialogVisible = false; - $app.data.friendExportContent = ''; - $app.data.friendExportFavoriteGroup = null; + $app.data.VRChatUsedCacheSize = ''; + $app.data.VRChatTotalCacheSize = ''; + $app.data.VRChatCacheSizeLoading = false; - $app.methods.showFriendExportDialog = function () { - this.$nextTick(() => - adjustDialogZ(this.$refs.friendExportDialogRef.$el) - ); - this.friendExportFavoriteGroup = null; - this.updateFriendExportDialog(); - this.friendExportDialogVisible = true; + $app.methods.getVRChatCacheSize = async function () { + this.VRChatCacheSizeLoading = true; + var totalCacheSize = 20; + if (this.VRChatConfigFile.cache_size) { + totalCacheSize = this.VRChatConfigFile.cache_size; + } + this.VRChatTotalCacheSize = totalCacheSize; + var usedCacheSize = await AssetBundleCacher.GetCacheSize(); + this.VRChatUsedCacheSize = (usedCacheSize / 1073741824).toFixed(2); + this.VRChatCacheSizeLoading = false; }; - $app.methods.updateFriendExportDialog = function () { - var _ = function (str) { - if (/[\x00-\x1f,"]/.test(str) === true) { - return `"${str.replace(/"/g, '""')}"`; - } - return str; - }; - var lines = ['UserID,Name']; - API.favoriteFriendGroups.forEach((group) => { - if ( - !this.friendExportFavoriteGroup || - this.friendExportFavoriteGroup === group - ) { - $app.favoriteFriends.forEach((ref) => { - if (group.key === ref.groupKey) { - lines.push(`${_(ref.id)},${_(ref.name)}`); + $app.methods.getBundleLocation = async function (input) { + var assetUrl = input; + var variant = ''; + if (assetUrl) { + // continue + } else if ( + this.avatarDialog.visible && + this.avatarDialog.ref.unityPackages.length > 0 + ) { + var unityPackages = this.avatarDialog.ref.unityPackages; + for (let i = unityPackages.length - 1; i > -1; i--) { + var unityPackage = unityPackages[i]; + if ( + unityPackage.variant && + unityPackage.variant !== 'standard' && + unityPackage.variant !== 'security' + ) { + continue; + } + if ( + unityPackage.platform === 'standalonewindows' && + this.compareUnityVersion(unityPackage.unitySortNumber) + ) { + assetUrl = unityPackage.assetUrl; + if (unityPackage.variant !== 'standard') { + variant = unityPackage.variant; } - }); + break; + } } - }); - this.friendExportContent = lines.join('\n'); + } else if ( + this.avatarDialog.visible && + this.avatarDialog.ref.assetUrl + ) { + assetUrl = this.avatarDialog.ref.assetUrl; + } else if ( + this.worldDialog.visible && + this.worldDialog.ref.unityPackages.length > 0 + ) { + var unityPackages = this.worldDialog.ref.unityPackages; + for (let i = unityPackages.length - 1; i > -1; i--) { + var unityPackage = unityPackages[i]; + if ( + unityPackage.platform === 'standalonewindows' && + this.compareUnityVersion(unityPackage.unitySortNumber) + ) { + assetUrl = unityPackage.assetUrl; + break; + } + } + } else if (this.worldDialog.visible && this.worldDialog.ref.assetUrl) { + assetUrl = this.worldDialog.ref.assetUrl; + } + if (!assetUrl) { + return null; + } + var fileId = $utils.extractFileId(assetUrl); + var fileVersion = parseInt($utils.extractFileVersion(assetUrl), 10); + var variantVersion = parseInt( + $utils.extractVariantVersion(assetUrl), + 10 + ); + var assetLocation = await AssetBundleCacher.GetVRChatCacheFullLocation( + fileId, + fileVersion, + variant, + variantVersion + ); + var cacheInfo = await AssetBundleCacher.CheckVRChatCache( + fileId, + fileVersion, + variant, + variantVersion + ); + var inCache = false; + if (cacheInfo.Item1 > 0) { + inCache = true; + } + console.log(`InCache: ${inCache}`); + var fullAssetLocation = `${assetLocation}\\__data`; + console.log(fullAssetLocation); + return fullAssetLocation; }; - $app.methods.selectFriendExportGroup = function (group) { - this.friendExportFavoriteGroup = group; - this.updateFriendExportDialog(); - }; + API.$on('LOGIN', function () { + $app.downloadDialog.visible = false; + }); - // #endregion - // #region | App: user dialog notes + // Parse User URL - API.saveNote = function (params) { - return this.call('userNotes', { - method: 'POST', - params - }).then((json) => { - var args = { - json, - params - }; - this.$emit('NOTE', args); - return args; - }); + $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; }; - API.$on('NOTE', function (args) { - var note = ''; - var targetUserId = ''; - if (typeof args.json !== 'undefined') { - note = $app.replaceBioSymbols(args.json.note); - } - if (typeof args.params !== 'undefined') { - targetUserId = args.params.targetUserId; - } - if (targetUserId === $app.userDialog.id) { - if (note === args.params.note) { - $app.userDialog.noteSaving = false; - $app.userDialog.note = note; - } else { - // response is cached sadge :< - this.getUser({ userId: targetUserId }); - } - } - var ref = API.cachedUsers.get(targetUserId); - if (typeof ref !== 'undefined') { - ref.note = note; - } - }); + // Parse Avatar URL - $app.methods.checkNote = function (ref, note) { - if (ref.note !== note) { - this.addNote(ref.id, note); + $app.methods.parseAvatarUrl = function (avatar) { + var url = new URL(avatar); + var urlPath = url.pathname; + if (urlPath.substring(5, 13) === '/avatar/') { + var avatarId = urlPath.substring(13); + return avatarId; } + return void 0; }; - $app.methods.cleanNote = function (note) { - // remove newlines because they aren't supported - $app.userDialog.note = note.replace(/[\r\n]/g, ''); + // userDialog Favorite Worlds + + $app.data.userFavoriteWorlds = []; + + $app.methods.getUserFavoriteWorlds = async function (userId) { + this.userDialog.isFavoriteWorldsLoading = true; + this.$refs.favoriteWorlds.currentName = '0'; // select first tab + this.userFavoriteWorlds = []; + var worldLists = []; + var params = { + ownerId: userId, + n: 100 + }; + var json = await API.call('favorite/groups', { + method: 'GET', + params + }); + for (var i = 0; i < json.length; ++i) { + var list = json[i]; + if (list.type !== 'world') { + continue; + } + var params = { + n: 100, + offset: 0, + userId, + tag: list.name + }; + try { + var args = await API.getFavoriteWorlds(params); + worldLists.push([list.displayName, list.visibility, args.json]); + } catch (err) {} + } + this.userFavoriteWorlds = worldLists; + this.userDialog.isFavoriteWorldsLoading = false; }; - $app.methods.addNote = function (userId, note) { - if (this.userDialog.id === userId) { - this.userDialog.noteSaving = true; + $app.data.worldGroupVisibilityOptions = ['private', 'friends', 'public']; + + $app.methods.userFavoriteWorldsStatus = function (visibility) { + var style = {}; + if (visibility === 'public') { + style.online = true; + } else if (visibility === 'friends') { + style.joinme = true; + } else { + style.busy = true; } - return API.saveNote({ - targetUserId: userId, - note + return style; + }; + + $app.methods.changeWorldGroupVisibility = function (name, visibility) { + var params = { + type: 'world', + group: name, + visibility + }; + API.saveFavoriteGroup(params).then((args) => { + this.$message({ + message: 'Group visibility changed', + type: 'success' + }); + return args; }); }; - $app.methods.deleteNote = function (userId) { - if (this.userDialog.id === userId) { - this.userDialog.noteSaving = true; + $app.methods.refreshInstancePlayerCount = function (instance) { + var L = $utils.parseLocation(instance); + if (L.worldId && L.instanceId) { + API.getInstance({ + worldId: L.worldId, + instanceId: L.instanceId + }); } - return API.saveNote({ - targetUserId: userId, - note: '' - }); }; - // #endregion - // #region | App: note export + // userDialog Groups - $app.data.noteExportDialog = { - visible: false, - loading: false, - progress: 0, - progressTotal: 0, - errors: '' - }; - $app.data.noteExportTable = { - data: [], - tableProps: { - stripe: true, - size: 'mini' - }, - layout: 'table' + $app.data.userGroups = { + groups: [], + ownGroups: [], + mutualGroups: [], + remainingGroups: [] }; - API.$on('LOGIN', function () { - $app.noteExportTable.data = []; - $app.noteExportDialog.visible = false; - $app.noteExportDialog.loading = false; - $app.noteExportDialog.progress = 0; - $app.noteExportDialog.progressTotal = 0; - $app.noteExportDialog.errors = ''; - }); + $app.methods.getUserGroups = async function (userId) { + this.userDialog.isGroupsLoading = true; + this.userGroups = { + groups: [], + ownGroups: [], + mutualGroups: [], + remainingGroups: [] + }; + var args = await API.getGroups({ userId }); + if (userId !== this.userDialog.id) { + this.userDialog.isGroupsLoading = false; + return; + } + if (userId === API.currentUser.id) { + // update current user groups + API.currentUserGroups.clear(); + args.json.forEach((group) => { + var ref = API.applyGroup(group); + API.currentUserGroups.set(group.id, ref); + }); + this.saveCurrentUserGroups(); + } + this.userGroups.groups = args.json; + for (var i = 0; i < args.json.length; ++i) { + var group = args.json[i]; + if (!group?.id) { + console.error('getUserGroups, group ID is missing', group); + continue; + } + if (group.ownerId === userId) { + this.userGroups.ownGroups.unshift(group); + } + if (userId === API.currentUser.id) { + // skip mutual groups for current user + if (group.ownerId !== userId) { + this.userGroups.remainingGroups.unshift(group); + } + continue; + } + if (group.mutualGroup) { + this.userGroups.mutualGroups.unshift(group); + } + if (!group.mutualGroup && group.ownerId !== userId) { + this.userGroups.remainingGroups.unshift(group); + } + } + this.userDialog.isGroupsLoading = false; + if (userId === API.currentUser.id) { + this.sortCurrentUserGroups(); + } + }; - $app.methods.showNoteExportDialog = function () { - this.$nextTick(() => adjustDialogZ(this.$refs.noteExportDialog.$el)); - var D = this.noteExportDialog; - D.progress = 0; - D.progressTotal = 0; - D.loading = false; - D.visible = true; + $app.methods.getCurrentUserGroups = async function () { + var args = await API.getGroups({ userId: API.currentUser.id }); + API.currentUserGroups.clear(); + for (var group of args.json) { + var ref = API.applyGroup(group); + API.currentUserGroups.set(group.id, ref); + } + await API.getGroupPermissions({ userId: API.currentUser.id }); + this.saveCurrentUserGroups(); }; - $app.methods.updateNoteExportDialog = function () { - var data = []; - this.friends.forEach((ctx) => { - var newMemo = ctx.memo.replace(/[\r\n]/g, ' '); - if (ctx.memo && ctx.ref && ctx.ref.note !== newMemo.slice(0, 256)) { - data.push({ - id: ctx.id, - name: ctx.name, - memo: newMemo, - ref: ctx.ref - }); + $app.methods.sortCurrentUserGroups = function () { + var groupList = []; + var sortGroups = function (a, b) { + var aIndex = groupList.indexOf(a?.id); + var bIndex = groupList.indexOf(b?.id); + if (aIndex === -1 && bIndex === -1) { + return 0; + } + if (aIndex === -1) { + return 1; + } + if (bIndex === -1) { + return -1; } + return aIndex - bIndex; + }; + AppApi.GetVRChatRegistryKey( + `VRC_GROUP_ORDER_${API.currentUser.id}` + ).then((json) => { + groupList = JSON.parse(json); + this.userGroups.remainingGroups.sort(sortGroups); }); - this.noteExportTable.data = data; }; - $app.methods.removeFromNoteExportTable = function (ref) { - removeFromArray(this.noteExportTable.data, ref); - }; + // #endregion + // #region | Gallery - $app.methods.exportNoteExport = async function () { - var D = this.noteExportDialog; - D.loading = true; - var data = [...this.noteExportTable.data].reverse(); - D.progressTotal = data.length; - try { - for (var i = data.length - 1; i >= 0; i--) { - if (D.visible && D.loading) { - var ctx = data[i]; - await API.saveNote({ - targetUserId: ctx.id, - note: ctx.memo.slice(0, 256) - }); - removeFromArray(this.noteExportTable.data, ctx); - D.progress++; - await new Promise((resolve) => { - workerTimers.setTimeout(resolve, 5000); - }); - } - } - } catch (err) { - D.errors = `Name: ${ctx.name}\n${err}\n\n`; - } finally { - D.progress = 0; - D.progressTotal = 0; - D.loading = false; - } - }; + $app.data.galleryDialog = {}; + $app.data.galleryDialogVisible = false; + $app.data.galleryDialogGalleryLoading = false; + $app.data.galleryDialogIconsLoading = false; + $app.data.galleryDialogEmojisLoading = false; + $app.data.galleryDialogStickersLoading = false; - $app.methods.cancelNoteExport = function () { - this.noteExportDialog.loading = false; - }; + API.$on('LOGIN', function () { + $app.galleryTable = []; + }); - // avatar database provider + $app.methods.showGalleryDialog = function (pageNum) { + this.$nextTick(() => $app.adjustDialogZ(this.$refs.galleryDialog.$el)); + this.galleryDialogVisible = true; + this.refreshGalleryTable(); + this.refreshVRCPlusIconsTable(); + this.refreshEmojiTable(); + this.refreshStickerTable(); + workerTimers.setTimeout(() => this.setGalleryTab(pageNum), 100); + }; - $app.data.avatarProviderDialog = { - visible: false + $app.methods.setGalleryTab = function (pageNum) { + if ( + typeof pageNum !== 'undefined' && + typeof this.$refs.galleryTabs !== 'undefined' + ) { + this.$refs.galleryTabs.setCurrentName(`${pageNum}`); + } }; - $app.methods.showAvatarProviderDialog = function () { - this.$nextTick(() => - adjustDialogZ(this.$refs.avatarProviderDialog.$el) - ); - var D = this.avatarProviderDialog; - D.visible = true; + $app.methods.refreshGalleryTable = function () { + this.galleryDialogGalleryLoading = true; + var params = { + n: 100, + tag: 'gallery' + }; + API.getFileList(params); }; - $app.methods.addAvatarProvider = function (url) { - if (!url) { + API.$on('FILES:LIST', function (args) { + if (args.params.tag === 'gallery') { + $app.galleryTable = args.json.reverse(); + $app.galleryDialogGalleryLoading = false; + } + }); + + $app.methods.setProfilePicOverride = function (fileId) { + if (!API.currentUser.$isVRCPlus) { + this.$message({ + message: 'VRCPlus required', + type: 'error' + }); return; } - this.showAvatarProviderDialog(); - if (!this.avatarRemoteDatabaseProviderList.includes(url)) { - this.avatarRemoteDatabaseProviderList.push(url); + var profilePicOverride = ''; + if (fileId) { + profilePicOverride = `${API.endpointDomain}/file/${fileId}/1`; } - this.saveAvatarProviderList(); + if (profilePicOverride === API.currentUser.profilePicOverride) { + return; + } + API.saveCurrentUser({ + profilePicOverride + }).then((args) => { + this.$message({ + message: 'Profile picture changed', + type: 'success' + }); + return args; + }); }; - $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.deleteGalleryImage = function (fileId) { + API.deleteFile(fileId).then((args) => { + API.$emit('GALLERYIMAGE:DELETE', args); + return args; + }); }; - $app.methods.saveAvatarProviderList = async function () { - var length = this.avatarRemoteDatabaseProviderList.length; + API.$on('GALLERYIMAGE:DELETE', function (args) { + var array = $app.galleryTable; + var { length } = array; for (var i = 0; i < length; ++i) { - if (!this.avatarRemoteDatabaseProviderList[i]) { - this.avatarRemoteDatabaseProviderList.splice(i, 1); + if (args.fileId === array[i].id) { + array.splice(i, 1); + break; } } - await configRepository.setString( - 'VRCX_avatarRemoteDatabaseProviderList', - JSON.stringify(this.avatarRemoteDatabaseProviderList) + }); + + $app.methods.compareCurrentProfilePic = function (fileId) { + var currentProfilePicOverride = $utils.extractFileId( + API.currentUser.profilePicOverride ); - if (this.avatarRemoteDatabaseProviderList.length > 0) { - this.avatarRemoteDatabaseProvider = - this.avatarRemoteDatabaseProviderList[0]; - this.avatarRemoteDatabase = true; - } else { - this.avatarRemoteDatabaseProvider = ''; - this.avatarRemoteDatabase = false; + if (fileId === currentProfilePicOverride) { + return true; } - await configRepository.setBool( - 'VRCX_avatarRemoteDatabase', - this.avatarRemoteDatabase - ); - }; - - $app.methods.setAvatarProvider = function (provider) { - this.avatarRemoteDatabaseProvider = provider; + return false; }; - // #endregion - // #region | App: bulk unfavorite - - $app.data.bulkUnfavoriteMode = false; - - $app.methods.showBulkUnfavoriteSelectionConfirm = function () { - var elementsTicked = []; - // check favorites type - for (var ctx of this.favoriteFriends) { - if (ctx.$selected) { - elementsTicked.push(ctx.id); - } - } - for (var ctx of this.favoriteWorlds) { - if (ctx.$selected) { - elementsTicked.push(ctx.id); + $app.methods.onFileChangeGallery = function (e) { + var clearFile = function () { + if (document.querySelector('#GalleryUploadButton')) { + document.querySelector('#GalleryUploadButton').value = ''; } + }; + var files = e.target.files || e.dataTransfer.files; + if (!files.length) { + return; } - for (var ctx of this.favoriteAvatars) { - if (ctx.$selected) { - elementsTicked.push(ctx.id); - } + if (files[0].size >= 100000000) { + // 100MB + $app.$message({ + message: 'File size too large', + type: 'error' + }); + clearFile(); + return; } - if (elementsTicked.length === 0) { + if (!files[0].type.match(/image.*/)) { + $app.$message({ + message: "File isn't an image", + type: 'error' + }); + clearFile(); return; } - this.$confirm( - `Are you sure you want to unfavorite ${elementsTicked.length} favorites? - This action cannot be undone.`, - `Delete ${elementsTicked.length} favorites?`, - { - confirmButtonText: 'Confirm', - cancelButtonText: 'Cancel', - type: 'info', - callback: (action) => { - if (action === 'confirm') { - this.bulkUnfavoriteSelection(elementsTicked); - } - } - } - ); + var r = new FileReader(); + r.onload = function () { + var base64Body = btoa(r.result); + API.uploadGalleryImage(base64Body).then((args) => { + $app.$message({ + message: 'Gallery image uploaded', + type: 'success' + }); + return args; + }); + }; + r.readAsBinaryString(files[0]); + clearFile(); + }; + + $app.methods.displayGalleryUpload = function () { + document.getElementById('GalleryUploadButton').click(); + }; + + API.uploadGalleryImage = function (imageData) { + var params = { + tag: 'gallery' + }; + return this.call('file/image', { + uploadImage: true, + postData: JSON.stringify(params), + imageData + }).then((json) => { + var args = { + json, + params + }; + this.$emit('GALLERYIMAGE:ADD', args); + return args; + }); }; - $app.methods.bulkUnfavoriteSelection = function (elementsTicked) { - for (var id of elementsTicked) { - API.deleteFavorite({ - objectId: id - }); + API.$on('GALLERYIMAGE:ADD', function (args) { + if (Object.keys($app.galleryTable).length !== 0) { + $app.galleryTable.unshift(args.json); } - this.bulkUnfavoriteMode = false; - }; + }); // #endregion - // #region | App: local world favorites + // #region | Sticker + API.$on('LOGIN', function () { + $app.stickerTable = []; + }); - $app.data.localWorldFavoriteGroups = []; - $app.data.localWorldFavoritesList = []; - $app.data.localWorldFavorites = {}; + $app.methods.refreshStickerTable = function () { + this.galleryDialogStickersLoading = true; + var params = { + n: 100, + tag: 'sticker' + }; + API.getFileList(params); + }; - $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; + API.$on('FILES:LIST', function (args) { + if (args.params.tag === 'sticker') { + $app.stickerTable = args.json.reverse(); + $app.galleryDialogStickersLoading = false; } + }); + + $app.methods.deleteSticker = function (fileId) { + API.deleteFile(fileId).then((args) => { + API.$emit('STICKER:DELETE', args); + return args; + }); }; - $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); + API.$on('STICKER:DELETE', function (args) { + var array = $app.stickerTable; + var { length } = array; + for (var i = 0; i < length; ++i) { + if (args.fileId === array[i].id) { + array.splice(i, 1); + break; } } + }); - // 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; - } + $app.methods.onFileChangeSticker = function (e) { + var clearFile = function () { + if (document.querySelector('#StickerUploadButton')) { + document.querySelector('#StickerUploadButton').value = ''; } + }; + var files = e.target.files || e.dataTransfer.files; + if (!files.length) { + return; } - if (!worldInFavorites) { - removeFromArray(this.localWorldFavoritesList, worldId); - database.removeWorldFromCache(worldId); - } - database.removeWorldFromFavorites(worldId, group); - if ( - this.favoriteDialog.visible && - this.favoriteDialog.objectId === worldId - ) { - this.updateFavoriteDialog(worldId); + if (files[0].size >= 100000000) { + // 100MB + $app.$message({ + message: 'File size too large', + type: 'error' + }); + clearFile(); + return; } - if (this.worldDialog.visible && this.worldDialog.id === worldId) { - this.worldDialog.isFavorite = - API.cachedFavoritesByObjectId.has(worldId); + if (!files[0].type.match(/image.*/)) { + $app.$message({ + message: "File isn't an image", + type: 'error' + }); + clearFile(); + return; } + var r = new FileReader(); + r.onload = function () { + var params = { + tag: 'sticker', + maskTag: 'square' + }; + var base64Body = btoa(r.result); + API.uploadSticker(base64Body, params).then((args) => { + $app.$message({ + message: 'Sticker uploaded', + type: 'success' + }); + return args; + }); + }; + r.readAsBinaryString(files[0]); + clearFile(); + }; - // update UI - this.sortLocalWorldFavorites(); + $app.methods.displayStickerUpload = function () { + document.getElementById('StickerUploadButton').click(); }; - $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(); + API.uploadSticker = function (imageData, params) { + return this.call('file/image', { + uploadImage: true, + postData: JSON.stringify(params), + imageData + }).then((json) => { + var args = { + json, + params + }; + this.$emit('STICKER:ADD', args); + return args; + }); }; - $app.methods.hasLocalWorldFavorite = function (worldId, group) { - var favoriteGroup = this.localWorldFavorites[group]; - if (!favoriteGroup) { - return false; + API.$on('STICKER:ADD', function (args) { + if (Object.keys($app.stickerTable).length !== 0) { + $app.stickerTable.unshift(args.json); } - for (var i = 0; i < favoriteGroup.length; ++i) { - if (favoriteGroup[i].id === worldId) { - return true; - } + }); + + // #endregion + // #region | Emoji + + API.$on('LOGIN', function () { + $app.emojiTable = []; + }); + + $app.methods.refreshEmojiTable = function () { + this.galleryDialogEmojisLoading = true; + var params = { + n: 100, + tag: 'emoji' + }; + API.getFileList(params); + }; + + API.$on('FILES:LIST', function (args) { + if (args.params.tag === 'emoji') { + $app.emojiTable = args.json.reverse(); + $app.galleryDialogEmojisLoading = false; } - return false; + }); + + $app.methods.deleteEmoji = function (fileId) { + API.deleteFile(fileId).then((args) => { + API.$emit('EMOJI:DELETE', args); + return args; + }); }; - $app.methods.getLocalWorldFavoriteGroupLength = function (group) { - var favoriteGroup = this.localWorldFavorites[group]; - if (!favoriteGroup) { - return 0; + API.$on('EMOJI:DELETE', function (args) { + var array = $app.emojiTable; + var { length } = array; + for (var i = 0; i < length; ++i) { + if (args.fileId === array[i].id) { + array.splice(i, 1); + break; + } } - return favoriteGroup.length; - }; + }); - $app.methods.promptNewLocalWorldFavoriteGroup = 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.newLocalWorldFavoriteGroup(instance.inputValue); - } - } + $app.methods.onFileChangeEmoji = function (e) { + var clearFile = function () { + if (document.querySelector('#EmojiUploadButton')) { + document.querySelector('#EmojiUploadButton').value = ''; } - ); - }; - - $app.methods.newLocalWorldFavoriteGroup = function (group) { - if (this.localWorldFavoriteGroups.includes(group)) { + }; + var files = e.target.files || e.dataTransfer.files; + if (!files.length) { + return; + } + if (files[0].size >= 100000000) { + // 100MB $app.$message({ - message: $t('prompt.new_local_favorite_group.message.error', { - name: group - }), + message: 'File size too large', type: 'error' }); + clearFile(); return; } - if (!this.localWorldFavorites[group]) { - this.localWorldFavorites[group] = []; - } - if (!this.localWorldFavoriteGroups.includes(group)) { - this.localWorldFavoriteGroups.push(group); - } - this.sortLocalWorldFavorites(); - }; - - $app.methods.promptLocalWorldFavoriteGroupRename = 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.renameLocalWorldFavoriteGroup( - instance.inputValue, - group - ); - } - } - } - ); - }; - - $app.methods.renameLocalWorldFavoriteGroup = function (newName, group) { - if (this.localWorldFavoriteGroups.includes(newName)) { + if (!files[0].type.match(/image.*/)) { $app.$message({ - message: $t( - 'prompt.local_favorite_group_rename.message.error', - { name: newName } - ), + message: "File isn't an image", type: 'error' }); + clearFile(); return; } - this.localWorldFavoriteGroups.push(newName); - this.localWorldFavorites[newName] = this.localWorldFavorites[group]; - - removeFromArray(this.localWorldFavoriteGroups, group); - delete this.localWorldFavorites[group]; - database.renameWorldFavoriteGroup(newName, group); - this.sortLocalWorldFavorites(); + var r = new FileReader(); + r.onload = function () { + var params = { + tag: $app.emojiAnimType ? 'emojianimated' : 'emoji', + animationStyle: $app.emojiAnimationStyle.toLowerCase(), + maskTag: 'square' + }; + if ($app.emojiAnimType) { + params.frames = $app.emojiAnimFrameCount; + params.framesOverTime = $app.emojiAnimFps; + } + if ($app.emojiAnimLoopPingPong) { + params.loopStyle = 'pingpong'; + } + var base64Body = btoa(r.result); + API.uploadEmoji(base64Body, params).then((args) => { + $app.$message({ + message: 'Emoji uploaded', + type: 'success' + }); + return args; + }); + }; + r.readAsBinaryString(files[0]); + clearFile(); }; - $app.methods.promptLocalWorldFavoriteGroupDelete = function (group) { - this.$confirm(`Delete Group? ${group}`, 'Confirm', { - confirmButtonText: 'Confirm', - cancelButtonText: 'Cancel', - type: 'info', - callback: (action) => { - if (action === 'confirm') { - this.deleteLocalWorldFavoriteGroup(group); - } - } - }); + $app.methods.displayEmojiUpload = function () { + document.getElementById('EmojiUploadButton').click(); }; - $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(compareByName); - } - } - } + API.uploadEmoji = function (imageData, params) { + return this.call('file/image', { + uploadImage: true, + postData: JSON.stringify(params), + imageData + }).then((json) => { + var args = { + json, + params + }; + this.$emit('EMOJI:ADD', args); + return args; + }); }; - $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); + API.$on('EMOJI:ADD', function (args) { + if (Object.keys($app.emojiTable).length !== 0) { + $app.emojiTable.unshift(args.json); } + }); - 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; - } - } - } + $app.data.emojiAnimFps = 15; + $app.data.emojiAnimFrameCount = 4; + $app.data.emojiAnimType = false; + $app.data.emojiAnimationStyle = 'Stop'; + $app.data.emojiAnimLoopPingPong = false; + $app.data.emojiAnimationStyleUrl = + 'https://assets.vrchat.com/www/images/emoji-previews/'; + $app.data.emojiAnimationStyleList = { + Aura: 'Preview_B2-Aura.gif', + Bats: 'Preview_B2-Fall_Bats.gif', + Bees: 'Preview_B2-Bees.gif', + Bounce: 'Preview_B2-Bounce.gif', + Cloud: 'Preview_B2-Cloud.gif', + Confetti: 'Preview_B2-Winter_Confetti.gif', + Crying: 'Preview_B2-Crying.gif', + Dislike: 'Preview_B2-Dislike.gif', + Fire: 'Preview_B2-Fire.gif', + Idea: 'Preview_B2-Idea.gif', + Lasers: 'Preview_B2-Lasers.gif', + Like: 'Preview_B2-Like.gif', + Magnet: 'Preview_B2-Magnet.gif', + Mistletoe: 'Preview_B2-Winter_Mistletoe.gif', + Money: 'Preview_B2-Money.gif', + Noise: 'Preview_B2-Noise.gif', + Orbit: 'Preview_B2-Orbit.gif', + Pizza: 'Preview_B2-Pizza.gif', + Rain: 'Preview_B2-Rain.gif', + Rotate: 'Preview_B2-Rotate.gif', + Shake: 'Preview_B2-Shake.gif', + Snow: 'Preview_B2-Spin.gif', + Snowball: 'Preview_B2-Winter_Snowball.gif', + Spin: 'Preview_B2-Spin.gif', + Splash: 'Preview_B2-SummerSplash.gif', + Stop: 'Preview_B2-Stop.gif', + ZZZ: 'Preview_B2-ZZZ.gif' + }; - worldIdRemoveList.forEach((id) => { - removeFromArray(this.localWorldFavoritesList, id); - database.removeWorldFromCache(id); - }); + $app.methods.generateEmojiStyle = function ( + url, + fps, + frameCount, + loopStyle + ) { + let framesPerLine = 2; + if (frameCount > 4) framesPerLine = 4; + if (frameCount > 16) framesPerLine = 8; + const animationDurationMs = (1000 / fps) * frameCount; + const frameSize = 1024 / framesPerLine; + const scale = 100 / (frameSize / 200); + const animStyle = loopStyle === 'pingpong' ? 'alternate' : 'none'; + const style = ` + transform: scale(${scale / 100}); + transform-origin: top left; + width: ${frameSize}px; + height: ${frameSize}px; + background: url('${url}') 0 0; + animation: ${animationDurationMs}ms steps(1) 0s infinite ${animStyle} running animated-emoji-${frameCount}; + `; + return style; }; - 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(); - }); - - $app.data.worldFavoriteSearch = ''; - $app.data.worldFavoriteSearchResults = []; - - $app.methods.searchWorldFavorites = function () { - var search = this.worldFavoriteSearch.toLowerCase(); - if (search.length < 3) { - this.worldFavoriteSearchResults = []; - return; - } + // #endregion + // #region Misc - var results = []; - for (var i = 0; i < this.localWorldFavoriteGroups.length; ++i) { - var group = this.localWorldFavoriteGroups[i]; - if (!this.localWorldFavorites[group]) { - continue; - } - for (var j = 0; j < this.localWorldFavorites[group].length; ++j) { - var ref = this.localWorldFavorites[group][j]; - if (!ref || !ref.id) { - continue; - } - if ( - ref.name.toLowerCase().includes(search) || - ref.authorName.toLowerCase().includes(search) - ) { - results.push(ref); - } - } + $app.methods.replaceBioSymbols = function (text) { + if (!text) { + return ''; } - - for (var i = 0; i < this.favoriteWorlds.length; ++i) { - var ref = this.favoriteWorlds[i].ref; - if (!ref) { - continue; - } - if ( - ref.name.toLowerCase().includes(search) || - ref.authorName.toLowerCase().includes(search) - ) { - results.push(ref); - } + var symbolList = { + '@': '@', + '#': '#', + $: '$', + '%': '%', + '&': '&', + '=': '=', + '+': '+', + '/': '⁄', + '\\': '\', + ';': ';', + ':': '˸', + ',': '‚', + '?': '?', + '!': 'ǃ', + '"': '"', + '<': '≺', + '>': '≻', + '.': '․', + '^': '^', + '{': '{', + '}': '}', + '[': '[', + ']': ']', + '(': '(', + ')': ')', + '|': '|', + '*': '∗' + }; + var newText = text; + for (var key in symbolList) { + var regex = new RegExp(symbolList[key], 'g'); + newText = newText.replace(regex, key); } - - this.worldFavoriteSearchResults = results; + return newText.replace(/ {1,}/g, ' ').trimRight(); }; - // #endregion - // #region | App: Local Avatar Favorites - - $app.methods.isLocalUserVrcplusSupporter = function () { - return API.currentUser.$isVRCPlus; + $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.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); + $app.methods.checkCanInvite = function (location) { + var L = $utils.parseLocation(location); + var instance = API.cachedInstances.get(location); + if (instance?.closedAt) { + return false; } - this.localAvatarFavorites[group].unshift(ref); - database.addAvatarToCache(ref); - database.addAvatarToFavorites(avatarId, group); if ( - this.favoriteDialog.visible && - this.favoriteDialog.objectId === avatarId + L.accessType === 'public' || + L.accessType === 'group' || + L.userId === API.currentUser.id ) { - this.updateFavoriteDialog(avatarId); + return true; } - if (this.avatarDialog.visible && this.avatarDialog.id === avatarId) { - this.avatarDialog.isFavorite = true; + if (L.accessType === 'invite' || L.accessType === 'friends') { + return false; } - }; - - $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); - } + if (this.lastLocation.location === location) { + return true; } + return false; + }; - // 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) { - removeFromArray(this.localAvatarFavoritesList, avatarId); - if (!this.avatarHistory.has(avatarId)) { - database.removeAvatarFromCache(avatarId); - } + $app.methods.checkCanInviteSelf = function (location) { + var L = $utils.parseLocation(location); + var instance = API.cachedInstances.get(location); + if (instance?.closedAt) { + return false; } - database.removeAvatarFromFavorites(avatarId, group); - if ( - this.favoriteDialog.visible && - this.favoriteDialog.objectId === avatarId - ) { - this.updateFavoriteDialog(avatarId); + if (L.userId === API.currentUser.id) { + return true; } - if (this.avatarDialog.visible && this.avatarDialog.id === avatarId) { - this.avatarDialog.isFavorite = - API.cachedFavoritesByObjectId.has(avatarId); + if (L.accessType === 'friends' && !this.friends.has(L.userId)) { + return false; } - - // update UI - this.sortLocalAvatarFavorites(); + return true; }; - API.$on('AVATAR', function (args) { - if ($app.localAvatarFavoritesList.includes(args.ref.id)) { - // update db cache - database.addAvatarToCache(args.ref); - } - }); - - API.$on('LOGIN', function () { - $app.getLocalAvatarFavorites(); - }); + $app.methods.setAsideWidth = async function () { + document.getElementById('aside').style.width = `${this.asideWidth}px`; + await configRepository.setInt('VRCX_sidePanelWidth', this.asideWidth); + }; - $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'); + $app.methods.compareUnityVersion = function (unitySortNumber) { + if (!API.cachedConfig.sdkUnityVersion) { + console.error('No cachedConfig.sdkUnityVersion'); + return false; } - this.sortLocalAvatarFavorites(); - }; - $app.methods.hasLocalAvatarFavorite = function (avatarId, group) { - var favoriteGroup = this.localAvatarFavorites[group]; - if (!favoriteGroup) { + // 2022.3.6f1 2022 03 06 000 + // 2019.4.31f1 2019 04 31 000 + // 5.3.4p1 5 03 04 010 + // 2019.4.31f1c1 is a thing + var array = API.cachedConfig.sdkUnityVersion.split('.'); + if (array.length < 3) { + console.error('Invalid cachedConfig.sdkUnityVersion'); return false; } - for (var i = 0; i < favoriteGroup.length; ++i) { - if (favoriteGroup[i].id === avatarId) { - return true; + var currentUnityVersion = array[0]; + currentUnityVersion += array[1].padStart(2, '0'); + var indexFirstLetter = array[2].search(/[a-zA-Z]/); + if (indexFirstLetter > -1) { + currentUnityVersion += array[2] + .substr(0, indexFirstLetter) + .padStart(2, '0'); + currentUnityVersion += '0'; + var letter = array[2].substr(indexFirstLetter, 1); + if (letter === 'p') { + currentUnityVersion += '1'; + } else { + // f + currentUnityVersion += '0'; } + currentUnityVersion += '0'; + } else { + // just in case + currentUnityVersion += '000'; } - return false; - }; + // just in case + currentUnityVersion = currentUnityVersion.replace(/\D/g, ''); - $app.methods.getLocalAvatarFavoriteGroupLength = function (group) { - var favoriteGroup = this.localAvatarFavorites[group]; - if (!favoriteGroup) { - return 0; + if ( + parseInt(unitySortNumber, 10) <= parseInt(currentUnityVersion, 10) + ) { + return true; } - return favoriteGroup.length; - }; - - $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); - } - } - } - ); + return false; }; - $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; + $app.methods.userImage = function (user) { + if (typeof user === 'undefined') { + return ''; } - if (!this.localAvatarFavorites[group]) { - this.localAvatarFavorites[group] = []; + if (this.displayVRCPlusIconsAsAvatar && user.userIcon) { + return user.userIcon; } - if (!this.localAvatarFavoriteGroups.includes(group)) { - this.localAvatarFavoriteGroups.push(group); + if (user.profilePicOverrideThumbnail) { + return user.profilePicOverrideThumbnail; } - this.sortLocalAvatarFavorites(); + if (user.profilePicOverride) { + return user.profilePicOverride; + } + if (user.thumbnailUrl) { + return user.thumbnailUrl; + } + return user.currentAvatarThumbnailImageUrl; }; - $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.userImageFull = function (user) { + if (this.displayVRCPlusIconsAsAvatar && user.userIcon) { + return user.userIcon; + } + if (user.profilePicOverride) { + return user.profilePicOverride; + } + return user.currentAvatarImageUrl; }; - $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' - }); + $app.methods.showConsole = function () { + AppApi.ShowDevTools(); + if ( + this.debug || + this.debugWebRequests || + this.debugWebSocket || + this.debugUserDiff + ) { return; } - this.localAvatarFavoriteGroups.push(newName); - this.localAvatarFavorites[newName] = this.localAvatarFavorites[group]; - - removeFromArray(this.localAvatarFavoriteGroups, group); - delete this.localAvatarFavorites[group]; - database.renameAvatarFavoriteGroup(newName, group); - this.sortLocalAvatarFavorites(); + 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.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.clearVRCXCache = function () { + API.failedGetRequests = new Map(); + API.cachedUsers.forEach((ref, id) => { + if ( + !this.friends.has(id) && + !this.lastLocation.playerList.has(ref.displayName) && + id !== API.currentUser.id + ) { + API.cachedUsers.delete(id); } }); - }; - - $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(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); - } - - 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 + API.cachedWorlds.forEach((ref, id) => { + if ( + !API.cachedFavoritesByObjectId.has(id) && + ref.authorId !== API.currentUser.id && + !this.localWorldFavoritesList.includes(id) ) { - var avatarId = this.localAvatarFavorites[groupName][j].id; - if (avatarIdRemoveList.has(avatarId)) { - avatarIdRemoveList.delete(avatarId); - break; - } + API.cachedWorlds.delete(id); } - } - - 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; - } - } + }); + API.cachedAvatars.forEach((ref, id) => { + if ( + !API.cachedFavoritesByObjectId.has(id) && + ref.authorId !== API.currentUser.id && + !$app.avatarHistory.has(id) + ) { + API.cachedAvatars.delete(id); } - if (!avatarInFavorites) { - removeFromArray(this.localAvatarFavoritesList, id); - if (!this.avatarHistory.has(id)) { - database.removeAvatarFromCache(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.avatarFavoriteSearch = ''; - $app.data.avatarFavoriteSearchResults = []; + $app.data.sqliteTableSizes = {}; - $app.methods.searchAvatarFavorites = function () { - var search = this.avatarFavoriteSearch.toLowerCase(); - if (search.length < 3) { - this.avatarFavoriteSearchResults = []; + $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 (!this.friendLogInitStatus) { return; } - - var results = []; - for (var i = 0; i < this.localAvatarFavoriteGroups.length; ++i) { - var group = this.localAvatarFavoriteGroups[i]; - if (!this.localAvatarFavorites[group]) { - continue; - } - for (var j = 0; j < this.localAvatarFavorites[group].length; ++j) { - var ref = this.localAvatarFavorites[group][j]; - if (!ref || !ref.id) { - continue; + 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 ( - ref.name.toLowerCase().includes(search) || - ref.authorName.toLowerCase().includes(search) - ) { - results.push(ref); + if (this.debugPhotonLogging) { + console.log( + 'OnEvent', + data.OnEventData.Code, + data.OnEventData + ); } - } - } - - for (var i = 0; i < this.favoriteAvatars.length; ++i) { - var ref = this.favoriteAvatars[i].ref; - if (!ref) { - continue; - } - if ( - ref.name.toLowerCase().includes(search) || - ref.authorName.toLowerCase().includes(search) - ) { - results.push(ref); - } + 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': + AppApi.FocusWindow(); + this.eventLaunchCommand(data.command); + break; + case 'VRCXLaunch': + console.log('VRCXLaunch:', data); + break; + default: + console.log('IPC:', data); } - - this.avatarFavoriteSearchResults = results; }; - // #endregion - // #region | Local Favorite Friends + $app.data.externalNotifierVersion = 0; + $app.data.photonEventCount = 0; + $app.data.photonEventIcon = false; + $app.data.customUserTags = new Map(); - $app.data.localFavoriteFriends = new Set(); - $app.data.localFavoriteFriendsGroups = JSON.parse( - await configRepository.getString( - 'VRCX_localFavoriteFriendsGroups', - '[]' - ) - ); + $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.updateLocalFavoriteFriends = function () { - this.localFavoriteFriends.clear(); - for (var ref of API.cachedFavorites.values()) { - if ( - !ref.$isDeleted && - ref.type === 'friend' && - (this.localFavoriteFriendsGroups.length === 0 || - this.localFavoriteFriendsGroups.includes(ref.$groupKey)) - ) { - this.localFavoriteFriends.add(ref.favoriteId); - } + $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; } - 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) { - removeFromArray(this.onlineFriends_, ctx); - this.vipFriends_.push(ctx); - this.sortVIPFriends = true; - } else { - removeFromArray(this.vipFriends_, ctx); - this.onlineFriends_.push(ctx); - this.sortOnlineFriends = true; - } - } + $app.methods.photonEventPulse = function () { + this.photonEventCount++; + this.photonEventIcon = true; + workerTimers.setTimeout(() => (this.photonEventIcon = false), 150); }; - // #endregion - // #region | App: ChatBox Blacklist - $app.data.chatboxBlacklist = [ - 'NP: ', - 'Now Playing', - 'Now playing', - "▶️ '", - '( ▶️ ', - "' - '", - "' by '", - '[Spotify] ' - ]; - if (await configRepository.getString('VRCX_chatboxBlacklist')) { - $app.data.chatboxBlacklist = JSON.parse( - await configRepository.getString('VRCX_chatboxBlacklist') - ); - } - $app.data.chatboxBlacklistDialog = { - visible: false, - loading: false + $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('LOGOUT', function () { - $app.chatboxBlacklistDialog.visible = false; + API.$on('LOGIN', async function () { + var command = await AppApi.GetLaunchCommand(); + if (command) { + $app.eventLaunchCommand(command); + } }); - $app.methods.saveChatboxBlacklist = async function () { - await configRepository.setString( - 'VRCX_chatboxBlacklist', - JSON.stringify(this.chatboxBlacklist) - ); + $app.methods.eventLaunchCommand = function (input) { + if (!API.isLoggedIn) { + return; + } + var args = input.split('/'); + var command = args[0]; + var commandArg = args[1]; + 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(':'); + API.getCachedWorld({ worldId: id }).then((args1) => { + this.directAccessWorld(id); + this.addLocalWorldFavorite(id, group); + return args1; + }); + break; + case 'addavatardb': + this.addAvatarProvider(input.replace('addavatardb/', '')); + break; + case 'import': + var type = args[1]; + if (!type) break; + var data = input.replace(`import/${type}/`, ''); + if (type === 'avatar') { + this.showAvatarImportDialog(); + this.avatarImportDialog.input = data; + } else if (type === 'world') { + this.showWorldImportDialog(); + this.worldImportDialog.input = data; + } else if (type === 'friend') { + this.showFriendImportDialog(); + this.friendImportDialog.input = data; + } + break; + } }; - $app.methods.showChatboxBlacklistDialog = function () { - this.$nextTick(() => - adjustDialogZ(this.$refs.chatboxBlacklistDialog.$el) - ); - var D = this.chatboxBlacklistDialog; - D.visible = true; + $app.methods.toggleAvatarCopying = function () { + API.saveCurrentUser({ + allowAvatarCopying: !API.currentUser.allowAvatarCopying + }).then((args) => { + return args; + }); }; - $app.methods.checkChatboxBlacklist = function (msg) { - for (var i = 0; i < this.chatboxBlacklist.length; ++i) { - if (msg.includes(this.chatboxBlacklist[i])) { - return true; - } - } - return false; + $app.methods.toggleAllowBooping = function () { + API.saveCurrentUser({ + isBoopingEnabled: !API.currentUser.isBoopingEnabled + }).then((args) => { + return args; + }); }; // #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.saveChatboxUserBlacklist = async function () { - await configRepository.setString( - 'VRCX_chatboxUserBlacklist', - JSON.stringify(Object.fromEntries(this.chatboxUserBlacklist)) - ); - }; - - $app.methods.addChatboxUserBlacklist = async function (user) { - this.chatboxUserBlacklist.set(user.id, user.displayName); - await this.saveChatboxUserBlacklist(); - this.getCurrentInstanceUserList(); - }; + // #region | App: Previous Instances User Dialog - $app.methods.deleteChatboxUserBlacklist = async function (userId) { - this.chatboxUserBlacklist.delete(userId); - await this.saveChatboxUserBlacklist(); - this.getCurrentInstanceUserList(); - this.$nextTick(() => - adjustDialogZ(this.$refs.chatboxBlacklistDialog.$el) - ); + $app.data.previousInstancesUserDialogTable = { + data: [], + filters: [ + { + prop: 'worldName', + value: '' + } + ], + tableProps: { + stripe: true, + size: 'mini', + defaultSort: { + prop: 'created_at', + order: 'descending' + } + }, + pageSize: 10, + paginationProps: { + small: true, + layout: 'sizes,prev,pager,next,total', + pageSizes: [10, 25, 50, 100] + } }; - // #endregion - // #region | App: Groups - - API.cachedGroups = new Map(); - API.currentUserGroups = new Map(); - 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.data.previousInstancesUserDialog = { + visible: false, + loading: false, + forceUpdate: 0, + userRef: {} }; - $app.methods.removeQueuedInstance = function (instanceId) { - var ref = API.queuedInstances.get(instanceId); - if (typeof ref !== 'undefined') { - ref.$msgBox.close(); - API.queuedInstances.delete(instanceId); - } + $app.methods.showPreviousInstancesUserDialog = function (userRef) { + this.$nextTick(() => + $app.adjustDialogZ(this.$refs.previousInstancesUserDialog.$el) + ); + var D = this.previousInstancesUserDialog; + D.userRef = userRef; + D.visible = true; + D.loading = true; + this.refreshPreviousInstancesUserTable(); }; - API.applyQueuedInstance = function (instanceId) { - API.queuedInstances.forEach((ref) => { - if (ref.location !== instanceId) { - $app.$message({ - message: `Removed instance ${ref.$worldName} from queue`, - type: 'info' - }); - ref.$msgBox?.close(); - API.queuedInstances.delete(ref.location); + $app.methods.refreshPreviousInstancesUserTable = function () { + var D = this.previousInstancesUserDialog; + database.getpreviousInstancesByUserId(D.userRef).then((data) => { + var array = []; + for (var ref of data.values()) { + ref.$location = $utils.parseLocation(ref.location); + if (ref.time > 0) { + ref.timer = $app.timeToText(ref.time); + } else { + ref.timer = ''; + } + array.push(ref); } + array.sort(compareByCreatedAt); + this.previousInstancesUserDialogTable.data = array; + D.loading = false; + workerTimers.setTimeout(() => D.forceUpdate++, 150); }); - if (!instanceId) { - return; - } - if (!API.queuedInstances.has(instanceId)) { - var L = API.parseLocation(instanceId); - if (L.worldId && L.instanceId) { - API.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 = API.parseLocation(instanceId); - var group = API.cachedGroups.get(L.groupId); - var groupName = group?.name ?? ''; - var worldName = ref?.$worldName ?? ''; - var displayLocation = $app.displayLocation( - instanceId, - worldName, - groupName - ); - this.$message({ - message: `Instance ready to join ${displayLocation}`, - type: 'success' - }); - var noty = { - created_at: new Date().toJSON(), - type: 'group.queueReady', - imageUrl: group?.iconUrl, - message: `Instance ready to join ${displayLocation}`, - location: instanceId, - groupName, - worldName - }; + $app.methods.getDisplayNameFromUserId = function (userId) { + var displayName = userId; + var ref = API.cachedUsers.get(userId); if ( - this.notificationTable.filters[0].value.length === 0 || - this.notificationTable.filters[0].value.includes(noty.type) + typeof ref !== 'undefined' && + typeof ref.displayName !== 'undefined' ) { - 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); + displayName = ref.displayName; } - var displayLocation = this.displayLocation( - instanceId, - ref.$worldName, - ref.$groupName - ); - ref.$msgBox.message = `You are in position ${ref.position} of ${ref.queueSize} in the queue for ${displayLocation} `; - 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); - }); + return displayName; }; - /** - * @param {{ groupId: string }} params - */ - API.getGroup = function (params) { - return this.call(`groups/${params.groupId}`, { - method: 'GET', - params: { - includeRoles: params.includeRoles || false + $app.methods.confirmDeleteGameLogUserInstance = function (row) { + this.$confirm('Continue? Delete', 'Confirm', { + confirmButtonText: 'Confirm', + cancelButtonText: 'Cancel', + type: 'info', + callback: (action) => { + if (action === 'confirm') { + database.deleteGameLogInstance({ + id: this.previousInstancesUserDialog.userRef.id, + displayName: + this.previousInstancesUserDialog.userRef + .displayName, + location: row.location + }); + $app.removeFromArray( + this.previousInstancesUserDialogTable.data, + row + ); + } } - }).then((json) => { - var args = { - json, - params - }; - this.$emit('GROUP', args); - return args; }); }; - API.$on('GROUP', function (args) { - args.ref = this.applyGroup(args.json); - this.cachedGroups.set(args.ref.id, args.ref); - if (this.currentUserGroups.has(args.ref.id)) { - this.currentUserGroups.set(args.ref.id, args.ref); - } - }); + // #endregion + // #region | App: Previous Instances World Dialog - API.$on('GROUP', function (args) { - var { ref } = args; - var D = $app.groupDialog; - if (D.visible === false || D.id !== ref.id) { - return; + $app.data.previousInstancesWorldDialogTable = { + data: [], + filters: [ + { + prop: 'groupName', + value: '' + } + ], + tableProps: { + stripe: true, + size: 'mini', + defaultSort: { + prop: 'created_at', + order: 'descending' + } + }, + pageSize: 10, + paginationProps: { + small: true, + layout: 'sizes,prev,pager,next,total', + pageSizes: [10, 25, 50, 100] } - D.inGroup = ref.membershipStatus === 'member'; - D.ref = ref; - }); + }; - /** - * @param {{ userId: string }} params - * @return { Promise<{json: any, params}> } - */ - API.getRepresentedGroup = function (params) { - return this.call(`users/${params.userId}/groups/represented`, { - method: 'GET' - }).then((json) => { - var args = { - json, - params - }; - this.$emit('GROUP:REPRESENTED', args); - return args; - }); + $app.data.previousInstancesWorldDialog = { + visible: false, + loading: false, + forceUpdate: 0, + worldRef: {} + }; + + $app.methods.showPreviousInstancesWorldDialog = function (worldRef) { + this.$nextTick(() => + $app.adjustDialogZ(this.$refs.previousInstancesWorldDialog.$el) + ); + var D = this.previousInstancesWorldDialog; + D.worldRef = worldRef; + D.visible = true; + D.loading = true; + this.refreshPreviousInstancesWorldTable(); }; - API.$on('GROUP:REPRESENTED', function (args) { - var json = args.json; - if (!json.groupId) { - // no group - return; - } - json.$memberId = json.id; - json.id = json.groupId; - this.$emit('GROUP', { - json, - params: { - groupId: json.groupId, - userId: args.params.userId + $app.methods.refreshPreviousInstancesWorldTable = function () { + var D = this.previousInstancesWorldDialog; + database.getpreviousInstancesByWorldId(D.worldRef).then((data) => { + var array = []; + for (var ref of data.values()) { + ref.$location = $utils.parseLocation(ref.location); + if (ref.time > 0) { + ref.timer = $app.timeToText(ref.time); + } else { + ref.timer = ''; + } + array.push(ref); } - }); - }); - - /** - * @param {{ userId: string }} params - * @return { Promise<{json: any, params}> } - */ - API.getGroups = function (params) { - return this.call(`users/${params.userId}/groups`, { - method: 'GET' - }).then((json) => { - var args = { - json, - params - }; - this.$emit('GROUP:LIST', args); - return args; + array.sort(compareByCreatedAt); + this.previousInstancesWorldDialogTable.data = array; + D.loading = false; + workerTimers.setTimeout(() => D.forceUpdate++, 150); }); }; - API.$on('GROUP:LIST', function (args) { - for (var json of args.json) { - json.$memberId = json.id; - json.id = json.groupId; - this.$emit('GROUP', { - json, - params: { - groupId: json.id, - userId: args.params.userId + $app.methods.confirmDeleteGameLogWorldInstance = function (row) { + this.$confirm('Continue? Delete', 'Confirm', { + confirmButtonText: 'Confirm', + cancelButtonText: 'Cancel', + type: 'info', + callback: (action) => { + if (action === 'confirm') { + database.deleteGameLogInstanceByInstanceId({ + location: row.location + }); + $app.removeFromArray( + this.previousInstancesWorldDialogTable.data, + row + ); } - }); - } - }); - - /** - * @param {{ groupId: string }} params - * @return { Promise<{json: any, params}> } - */ - API.joinGroup = function (params) { - return this.call(`groups/${params.groupId}/join`, { - method: 'POST' - }).then((json) => { - var args = { - json, - params - }; - this.$emit('GROUP:JOIN', args); - return args; + } }); }; - API.$on('GROUP:JOIN', function (args) { - var json = { - $memberId: args.json.id, - id: args.json.groupId, - membershipStatus: args.json.membershipStatus, - myMember: { - isRepresenting: args.json.isRepresenting, - id: args.json.id, - roleIds: args.json.roleIds, - joinedAt: args.json.joinedAt, - membershipStatus: args.json.membershipStatus, - visibility: args.json.visibility, - isSubscribedToAnnouncements: - args.json.isSubscribedToAnnouncements + // #endregion + // #region | App: Previous Instance Info Dialog + + $app.data.previousInstanceInfoDialogTable = { + data: [], + filters: [ + { + prop: 'displayName', + value: '' } - }; - var groupId = json.id; - this.$emit('GROUP', { - json, - params: { - groupId, - userId: args.params.userId + ], + tableProps: { + stripe: true, + size: 'mini', + defaultSort: { + prop: 'created_at', + order: 'descending' } - }); - if ($app.groupDialog.visible && $app.groupDialog.id === groupId) { - $app.groupDialog.inGroup = json.membershipStatus === 'member'; - $app.getGroupDialogGroup(groupId); + }, + pageSize: 10, + paginationProps: { + small: true, + layout: 'sizes,prev,pager,next,total', + pageSizes: [10, 25, 50, 100] } - }); + }; - /** - * @param {{ groupId: string }} params - * @return { Promise<{json: any, params}> } - */ - API.leaveGroup = function (params) { - return this.call(`groups/${params.groupId}/leave`, { - method: 'POST' - }).then((json) => { - var args = { - json, - params - }; - this.$emit('GROUP:LEAVE', args); - return args; - }); + $app.data.previousInstanceInfoDialog = { + visible: false, + loading: false, + forceUpdate: 0, + $location: {} }; - API.$on('GROUP:LEAVE', function (args) { - var groupId = args.params.groupId; - if ($app.groupDialog.visible && $app.groupDialog.id === groupId) { - $app.groupDialog.inGroup = false; - $app.getGroupDialogGroup(groupId); - } - if ( - $app.userDialog.visible && - $app.userDialog.id === this.currentUser.id && - $app.userDialog.representedGroup.id === groupId - ) { - $app.getCurrentUserRepresentedGroup(); - } - }); + $app.methods.showPreviousInstanceInfoDialog = function (instanceId) { + this.$nextTick(() => + $app.adjustDialogZ(this.$refs.previousInstanceInfoDialog.$el) + ); + var D = this.previousInstanceInfoDialog; + D.$location = $utils.parseLocation(instanceId); + D.visible = true; + D.loading = true; + this.refreshPreviousInstanceInfoTable(); + }; - /** - * @param {{ groupId: string }} params - * @return { Promise<{json: any, params}> } - */ - API.cancelGroupRequest = function (params) { - return this.call(`groups/${params.groupId}/requests`, { - method: 'DELETE' - }).then((json) => { - var args = { - json, - params - }; - this.$emit('GROUP:CANCELJOINREQUEST', args); - return args; + $app.methods.refreshPreviousInstanceInfoTable = function () { + var D = this.previousInstanceInfoDialog; + database.getPlayersFromInstance(D.$location.tag).then((data) => { + var array = []; + for (var entry of Array.from(data.values())) { + entry.timer = $app.timeToText(entry.time); + array.push(entry); + } + array.sort(compareByCreatedAt); + this.previousInstanceInfoDialogTable.data = array; + D.loading = false; + workerTimers.setTimeout(() => D.forceUpdate++, 150); }); }; - API.$on('GROUP:CANCELJOINREQUEST', function (args) { - var groupId = args.params.groupId; - if ($app.groupDialog.visible && $app.groupDialog.id === groupId) { - $app.getGroupDialogGroup(groupId); + $app.data.dtHour12 = await configRepository.getBool('VRCX_dtHour12', false); + $app.data.dtIsoFormat = await configRepository.getBool( + 'VRCX_dtIsoFormat', + false + ); + $app.methods.setDatetimeFormat = async function () { + 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; } - }); - - /* - groupId: string, - params: { - isRepresenting: bool + if (typeof this.dtIsoFormat !== 'undefined') { + if (isoFormat !== this.dtIsoFormat) { + await configRepository.setBool( + 'VRCX_dtIsoFormat', + this.dtIsoFormat + ); + } + var isoFormat = this.dtIsoFormat; } - */ - API.setGroupRepresentation = function (groupId, params) { - return this.call(`groups/${groupId}/representation`, { - method: 'PUT', - params - }).then((json) => { - var args = { - json, - groupId, - params + 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 '-'; + } + var dt = new Date(date); + if (format === 'long') { + return dt.toISOString(); + } 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 '-'; }; - this.$emit('GROUP:SETREPRESENTATION', args); - return args; - }); + } + Vue.filter('formatDate', formatDate1); }; + $app.methods.setDatetimeFormat(); - API.$on('GROUP:SETREPRESENTATION', function (args) { - if ($app.groupDialog.visible && $app.groupDialog.id === args.groupId) { - $app.groupDialog.ref.isRepresenting = args.params.isRepresenting; - } + $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.data.mouseDownClass = []; + $app.data.mouseUpClass = []; + $app.methods.dialogMouseDown = function (e) { + this.mouseDownClass = [...e.target.classList]; + }; + $app.methods.dialogMouseUp = function (e) { + this.mouseUpClass = [...e.target.classList]; + }; + $app.methods.beforeDialogClose = function (done) { if ( - $app.userDialog.visible && - $app.userDialog.id === this.currentUser.id + this.mouseDownClass.includes('el-dialog__wrapper') && + this.mouseUpClass.includes('el-dialog__wrapper') + ) { + done(); + } else if ( + this.mouseDownClass.includes('el-dialog__close') && + this.mouseUpClass.includes('el-dialog__close') ) { - $app.getCurrentUserRepresentedGroup(); + done(); } - }); + }; - /** - * @param {{ query: string }} params - * @return { Promise<{json: any, params}> } - */ - API.groupStrictsearch = function (params) { - return this.call(`groups/strictsearch`, { - method: 'GET', - params - }).then((json) => { - var args = { - json, - params - }; - this.$emit('GROUP:STRICTSEARCH', args); - return args; - }); + $app.methods.getNameColour = async function (userId) { + var hue = await AppApi.GetColourFromUserID(userId); + return this.HueToHex(hue); }; - API.$on('GROUP:STRICTSEARCH', function (args) { - for (var json of args.json) { - this.$emit('GROUP', { - json, - params: { - groupId: json.id - } - }); + $app.methods.userColourInit = async function () { + var dictObject = await AppApi.GetColourBulk( + Array.from(API.cachedUsers.keys()) + ); + for (var [userId, hue] of Object.entries(dictObject)) { + var ref = API.cachedUsers.get(userId); + if (typeof ref !== 'undefined') { + ref.$userColour = this.HueToHex(hue); + } } - }); + }; - /* - userId: string, - groupId: string, - params: { - visibility: string, - isSubscribedToAnnouncements: bool, - managerNotes: string + $app.methods.HueToHex = function (hue) { + // this.HSVtoRGB(hue / 65535, .8, .8); + if (this.isDarkMode) { + return this.HSVtoRGB(hue / 65535, 0.6, 1); } - */ - API.setGroupMemberProps = function (userId, groupId, params) { - return this.call(`groups/${groupId}/members/${userId}`, { - method: 'PUT', - params - }).then((json) => { - var args = { - json, - userId, - groupId, - params - }; - this.$emit('GROUP:MEMBER:PROPS', args); - return args; - }); + return this.HSVtoRGB(hue / 65535, 1, 0.7); }; - API.$on('GROUP:MEMBER:PROPS', function (args) { - if (args.userId !== this.currentUser.id) { - return; + $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 json = args.json; - json.$memberId = json.id; - json.id = json.groupId; - if ($app.groupDialog.visible && $app.groupDialog.id === json.groupId) { - $app.groupDialog.ref.myMember.visibility = json.visibility; - $app.groupDialog.ref.myMember.isSubscribedToAnnouncements = - json.isSubscribedToAnnouncements; + 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.isFriendOnline = function (friend) { if ( - $app.userDialog.visible && - $app.userDialog.id === this.currentUser.id + typeof friend === 'undefined' || + typeof friend.ref === 'undefined' ) { - $app.getCurrentUserRepresentedGroup(); + return false; } - this.$emit('GROUP:MEMBER', { - json, - params: { - groupId: json.groupId - } - }); - }); - - API.$on('GROUP:MEMBER:PROPS', function (args) { - if ($app.groupDialog.id === args.json.groupId) { - for (var i = 0; i < $app.groupDialog.members.length; ++i) { - var member = $app.groupDialog.members[i]; - if (member.userId === args.json.userId) { - Object.assign(member, this.applyGroupMember(args.json)); - break; - } - } - for ( - var i = 0; - i < $app.groupDialog.memberSearchResults.length; - ++i - ) { - var member = $app.groupDialog.memberSearchResults[i]; - if (member.userId === args.json.userId) { - Object.assign(member, this.applyGroupMember(args.json)); - break; - } - } + if (friend.state === 'online') { + return true; } - if ( - $app.groupMemberModeration.visible && - $app.groupMemberModeration.id === args.json.groupId - ) { - // force redraw table - $app.groupMembersSearch(); + if (friend.state !== 'online' && friend.ref.location !== 'private') { + // wat + return true; } - }); - - /** - * @param {{ - userId: string, - groupId: string, - roleId: string - }} params - * @return { Promise<{json: any, params}> } - */ - API.addGroupMemberRole = function (params) { - return this.call( - `groups/${params.groupId}/members/${params.userId}/roles/${params.roleId}`, - { - method: 'PUT' - } - ).then((json) => { - var args = { - json, - params - }; - this.$emit('GROUP:MEMBER:ROLE:CHANGE', args); - return args; - }); - }; - - /** - * @param {{ - userId: string, - groupId: string, - roleId: string - }} params - * @return { Promise<{json: any, params}> } - */ - API.removeGroupMemberRole = function (params) { - return this.call( - `groups/${params.groupId}/members/${params.userId}/roles/${params.roleId}`, - { - method: 'DELETE' - } - ).then((json) => { - var args = { - json, - params - }; - this.$emit('GROUP:MEMBER:ROLE:CHANGE', args); - return args; - }); + return false; }; - API.$on('GROUP:MEMBER:ROLE:CHANGE', function (args) { - if ($app.groupDialog.id === args.params.groupId) { - for (var i = 0; i < $app.groupDialog.members.length; ++i) { - var member = $app.groupDialog.members[i]; - if (member.userId === args.params.userId) { - member.roleIds = args.json; - break; - } - } - for ( - var i = 0; - i < $app.groupDialog.memberSearchResults.length; - ++i - ) { - var member = $app.groupDialog.memberSearchResults[i]; - if (member.userId === args.params.userId) { - member.roleIds = args.json; - break; - } - } + $app.methods.isRealInstance = function (instanceId) { + if (!instanceId) { + return false; } - - if ( - $app.groupMemberModeration.visible && - $app.groupMemberModeration.id === args.params.groupId - ) { - // force redraw table - $app.groupMembersSearch(); + switch (instanceId) { + case 'offline': + case 'offline:offline': + case 'private': + case 'private:private': + case 'traveling': + case 'traveling:traveling': + case instanceId.startsWith('local'): + return false; } - }); - - API.getGroupPermissions = function (params) { - return this.call(`users/${params.userId}/groups/permissions`, { - method: 'GET' - }).then((json) => { - var args = { - json, - params - }; - this.$emit('GROUP:PERMISSIONS', args); - return args; - }); + return true; }; - API.$on('GROUP:PERMISSIONS', function (args) { - if (args.params.userId !== this.currentUser.id) { + $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.displayName) + ) { return; } - var json = args.json; - for (var groupId in json) { - var permissions = json[groupId]; - var group = this.cachedGroups.get(groupId); - if (group) { - group.myMember.permissions = permissions; - } - } - }); - - // /** - // * @param {{ groupId: string }} params - // * @return { Promise<{json: any, params}> } - // */ - // API.getGroupAnnouncement = function (params) { - // return this.call(`groups/${params.groupId}/announcement`, { - // method: 'GET' - // }).then((json) => { - // var args = { - // json, - // params - // }; - // this.$emit('GROUP:ANNOUNCEMENT', args); - // return args; - // }); - // }; - - /** - * @param {{ - groupId: string, - n: number, - offset: number - }} params - * @return { Promise<{json: any, params}> } - */ - API.getGroupPosts = function (params) { - return this.call(`groups/${params.groupId}/posts`, { - method: 'GET', - params - }).then((json) => { - var args = { - json, - params - }; - this.$emit('GROUP:POSTS', args); - return args; - }); - }; - /** - * @param {{ groupId: string }} params - * @return { Promise<{json: any, params}> } - */ - API.getAllGroupPosts = async function (params) { - var posts = []; - var offset = 0; - var n = 100; - var total = 0; - do { - var args = await this.getGroupPosts({ - groupId: params.groupId, - n, - offset - }); - posts = posts.concat(args.json.posts); - total = args.json.total; - offset += n; - } while (offset < total); - var returnArgs = { - posts, - params + var onPlayerJoining = { + created_at: new Date(ref.created_at).toJSON(), + userId: ref.id, + displayName: ref.displayName, + type: 'OnPlayerJoining' }; - this.$emit('GROUP:POSTS:ALL', returnArgs); - return returnArgs; + this.queueFeedNoty(onPlayerJoining); }; - API.$on('GROUP:POSTS:ALL', function (args) { - var D = $app.groupDialog; - if (D.id === args.params.groupId) { - for (var post of args.posts) { - post.title = $app.replaceBioSymbols(post.title); - post.text = $app.replaceBioSymbols(post.text); - } - if (args.posts.length > 0) { - D.announcement = args.posts[0]; - } - D.posts = args.posts; - $app.updateGroupPostSearch(); + $app.methods.updateCurrentUserLocation = function () { + API.currentUser.$travelingToTime = this.lastLocationDestinationTime; + var ref = API.cachedUsers.get(API.currentUser.id); + if (typeof ref === 'undefined') { + return; } - }); - API.$on('GROUP:POST', function (args) { - var D = $app.groupDialog; - if (D.id !== args.params.groupId) { - return; + // update cached user with both gameLog and API locations + var currentLocation = API.currentUser.$locationTag; + if (API.currentUser.$location === 'traveling') { + currentLocation = API.currentUser.$travelingToLocation; } + ref.location = API.currentUser.$locationTag; + ref.travelingToLocation = API.currentUser.$travelingToLocation; - var newPost = args.json; - newPost.title = $app.replaceBioSymbols(newPost.title); - newPost.text = $app.replaceBioSymbols(newPost.text); - var hasPost = false; - // update existing post - for (var post of D.posts) { - if (post.id === newPost.id) { - Object.assign(post, newPost); - hasPost = true; - break; + 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; } - // set or update announcement - if (newPost.id === D.announcement.id || !D.announcement.id) { - D.announcement = newPost; - } - // add new post - if (!hasPost) { - D.posts.unshift(newPost); + + ref.$online_for = API.currentUser.$online_for; + ref.$offline_for = API.currentUser.$offline_for; + ref.$location = $utils.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; } - $app.updateGroupPostSearch(); - }); + }; + + $app.methods.setCurrentUserLocation = async function (location) { + API.currentUser.$location_at = Date.now(); + API.currentUser.$travelingToTime = Date.now(); + API.currentUser.$locationTag = location; + this.updateCurrentUserLocation(); - API.$on('GROUP:POST:DELETE', function (args) { - var D = $app.groupDialog; - if (D.id !== args.params.groupId) { + // 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 postId = args.params.postId; - // remove existing post - for (var post of D.posts) { - if (post.id === postId) { - removeFromArray(D.posts, post); + 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; } } - // remove/update announcement - if (postId === D.announcement.id) { - if (D.posts.length > 0) { - D.announcement = D.posts[0]; - } else { - D.announcement = {}; - } - } - $app.updateGroupPostSearch(); - }); - - $app.methods.confirmDeleteGroupPost = function (post) { - this.$confirm('Are you sure you want to delete this post?', 'Confirm', { - confirmButtonText: 'Confirm', - cancelButtonText: 'Cancel', - type: 'info', - callback: (action) => { - if (action === 'confirm') { - API.deleteGroupPost({ - groupId: post.groupId, - postId: post.id - }); - } - } - }); - }; - - /** - * @param {{ groupId: string, postId: string }} params - * @return { Promise<{json: any, params}> } - */ - API.deleteGroupPost = function (params) { - return this.call(`groups/${params.groupId}/posts/${params.postId}`, { - method: 'DELETE' - }).then((json) => { - var args = { - json, - params - }; - this.$emit('GROUP:POST:DELETE', args); - return args; - }); - }; - - API.editGroupPost = function (params) { - return this.call(`groups/${params.groupId}/posts/${params.postId}`, { - method: 'PUT', - params - }).then((json) => { - var args = { - json, - params - }; - this.$emit('GROUP:POST', args); - return args; - }); - }; - - API.createGroupPost = function (params) { - return this.call(`groups/${params.groupId}/posts`, { - method: 'POST', - params - }).then((json) => { - var args = { - json, - params + if (this.isRealInstance(location) && lastLocation !== location) { + var dt = new Date().toJSON(); + var L = $utils.parseLocation(location); + 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 }; - this.$emit('GROUP:POST', args); - return args; - }); - }; + database.addGamelogLocationToDatabase(entry); + this.queueGameLogNoty(entry); + this.addGameLog(entry); + this.addInstanceJoinHistory(location, dt); - /** - * @param {{ - groupId: string, - userId: string - }} params - * @return { Promise<{json: any, params}> } - */ - API.getGroupMember = function (params) { - return this.call(`groups/${params.groupId}/members/${params.userId}`, { - method: 'GET' - }).then((json) => { - var args = { - json, - params - }; - this.$emit('GROUP:MEMBER', args); - return args; - }); + this.applyUserDialogLocation(); + this.applyWorldDialogInstances(); + this.applyGroupDialogInstances(); + } }; - /** - * @param {{ - groupId: string, - n: number, - offset: number - }} params - * @return { Promise<{json: any, params}> } - */ - API.getGroupMembers = function (params) { - return this.call(`groups/${params.groupId}/members`, { - method: 'GET', - params - }).then((json) => { - var args = { - json, - params - }; - this.$emit('GROUP:MEMBERS', args); - return args; - }); - }; + $app.data.avatarHistory = new Set(); + $app.data.avatarHistoryArray = []; - API.$on('GROUP:MEMBERS', function (args) { - for (var json of args.json) { - this.$emit('GROUP:MEMBER', { - json, - params: { - groupId: args.params.groupId - } - }); + $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++) { + this.avatarHistory.add(historyArray[i].id); + API.applyAvatar(historyArray[i]); } - }); - - API.$on('GROUP:MEMBER', function (args) { - args.ref = this.applyGroupMember(args.json); - }); - - /** - * @param {{ - groupId: string, - query: string, - n: number, - offset: number - }} params - * @return { Promise<{json: any, params}> } - */ - API.getGroupMembersSearch = function (params) { - return this.call(`groups/${params.groupId}/members/search`, { - method: 'GET', - params - }).then((json) => { - var args = { - json, - params - }; - this.$emit('GROUP:MEMBERS:SEARCH', args); - return args; - }); }; - API.$on('GROUP:MEMBERS:SEARCH', function (args) { - for (var json of args.json.results) { - this.$emit('GROUP:MEMBER', { - json, - params: { - groupId: args.params.groupId - } - }); - } - }); - - $app.methods.blockGroup = function (groupId) { - this.$confirm('Are you sure you want to block this group?', 'Confirm', { - confirmButtonText: 'Confirm', - cancelButtonText: 'Cancel', - type: 'info', - callback: (action) => { - if (action === 'confirm') { - API.blockGroup({ - groupId - }); - } + $app.methods.addAvatarToHistory = function (avatarId) { + API.getAvatar({ avatarId }).then((args) => { + var { ref } = args; + if (ref.authorId === API.currentUser.id) { + return; } - }); - }; - - $app.methods.unblockGroup = function (groupId) { - this.$confirm( - 'Are you sure you want to unblock this group?', - 'Confirm', - { - confirmButtonText: 'Confirm', - cancelButtonText: 'Cancel', - type: 'info', - callback: (action) => { - if (action === 'confirm') { - API.unblockGroup({ - groupId, - userId: API.currentUser.id - }); - } + var historyArray = this.avatarHistoryArray; + for (var i = 0; i < historyArray.length; ++i) { + if (historyArray[i].id === ref.id) { + historyArray.splice(i, 1); } } - ); - }; - - /** - * @param {{ - groupId: string - * }} params - * @return { Promise<{json: any, params}> } - */ - API.blockGroup = function (params) { - return this.call(`groups/${params.groupId}/block`, { - method: 'POST' - }).then((json) => { - var args = { - json, - params - }; - this.$emit('GROUP:BLOCK', args); - return args; - }); - }; + this.avatarHistoryArray.unshift(ref); + database.addAvatarToCache(ref); - /** - * @param {{ - groupId: string, - userId: string - * }} params - * @return { Promise<{json: any, params}> } - */ - API.unblockGroup = function (params) { - return this.call(`groups/${params.groupId}/members/${params.userId}`, { - method: 'DELETE' - }).then((json) => { - var args = { - json, - params - }; - this.$emit('GROUP:UNBLOCK', args); - return args; + this.avatarHistory.delete(ref.id); + this.avatarHistory.add(ref.id); + database.addAvatarToHistory(ref.id); }); }; - API.$on('GROUP:BLOCK', function (args) { - if ( - $app.groupDialog.visible && - $app.groupDialog.id === args.params.groupId - ) { - $app.showGroupDialog(args.params.groupId); - } - }); - - API.$on('GROUP:UNBLOCK', function (args) { - if ( - $app.groupDialog.visible && - $app.groupDialog.id === args.params.groupId - ) { - $app.showGroupDialog(args.params.groupId); - } - }); - - /** - * @param {{ - groupId: string, - userId: string - * }} params - * @return { Promise<{json: any, params}> } - */ - API.sendGroupInvite = function (params) { - return this.call(`groups/${params.groupId}/invites`, { - method: 'POST', - params: { - userId: params.userId + $app.methods.promptClearAvatarHistory = function () { + this.$confirm('Continue? Clear Avatar History', 'Confirm', { + confirmButtonText: 'Confirm', + cancelButtonText: 'Cancel', + type: 'info', + callback: (action) => { + if (action === 'confirm') { + this.clearAvatarHistory(); + } } - }).then((json) => { - var args = { - json, - params - }; - this.$emit('GROUP:INVITE', args); - return args; }); }; - /** - * @param {{ - groupId: string, - userId: string - }} params - * @return { Promise<{json: any, params}> } - */ - API.kickGroupMember = function (params) { - return this.call(`groups/${params.groupId}/members/${params.userId}`, { - method: 'DELETE' - }).then((json) => { - var args = { - json, - params - }; - this.$emit('GROUP:MEMBER:KICK', args); - return args; - }); + $app.methods.clearAvatarHistory = function () { + this.avatarHistory = new Set(); + this.avatarHistoryArray = []; + database.clearAvatarHistory(); }; - /** - * @param {{ groupId: string, userId: string }} params - * @return { Promise<{json: any, params}> } - */ - API.banGroupMember = function (params) { - return this.call(`groups/${params.groupId}/bans`, { - method: 'POST', - params: { - userId: params.userId + $app.data.databaseVersion = await configRepository.getInt( + 'VRCX_databaseVersion', + 0 + ); + + $app.methods.updateDatabaseVersion = async function () { + var databaseVersion = 9; + if (this.databaseVersion < databaseVersion) { + if (this.databaseVersion) { + var msgBox = this.$message({ + message: + 'DO NOT CLOSE VRCX, database upgrade in progress...', + type: 'warning', + duration: 0 + }); } - }).then((json) => { - var args = { - json, - params - }; - this.$emit('GROUP:MEMBER:BAN', args); - return args; - }); + 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.updateTableForGroupNames(); // alter tables to include group name + await database.fixBrokenNotifications(); // fix notifications being null + await database.fixBrokenGroupChange(); // fix spam group left & name change + await database.fixCancelFriendRequestTypo(); // fix CancelFriendRequst typo + await database.vacuum(); // succ + 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(); + } + } }; - /** - * @param {{ groupId: string, userId: string }} params - * @return { Promise<{json: any, params}> } - */ - API.unbanGroupMember = function (params) { - return this.call(`groups/${params.groupId}/bans/${params.userId}`, { - method: 'DELETE' - }).then((json) => { - var args = { - json, - params - }; - this.$emit('GROUP:MEMBER:UNBAN', args); - return args; - }); + // #endregion + // #region | App: world favorite import + + $app.data.worldImportDialog = { + visible: false, + loading: false, + progress: 0, + progressTotal: 0, + input: '', + worldIdList: new Set(), + errors: '', + worldImportFavoriteGroup: null, + worldImportLocalFavoriteGroup: null, + importProgress: 0, + importProgressTotal: 0 }; - API.deleteSentGroupInvite = function (params) { - return this.call(`groups/${params.groupId}/invites/${params.userId}`, { - method: 'DELETE' - }).then((json) => { - var args = { - json, - params - }; - this.$emit('GROUP:INVITE:DELETE', args); - return args; - }); + $app.data.worldImportTable = { + data: [], + tableProps: { + stripe: true, + size: 'mini' + }, + layout: 'table' }; - API.deleteBlockedGroupRequest = function (params) { - return this.call(`groups/${params.groupId}/members/${params.userId}`, { - method: 'DELETE' - }).then((json) => { - var args = { - json, - params - }; - this.$emit('GROUP:BLOCKED:DELETE', args); - return args; - }); + $app.methods.showWorldImportDialog = function () { + this.$nextTick(() => + $app.adjustDialogZ(this.$refs.worldImportDialog.$el) + ); + var D = this.worldImportDialog; + this.resetWorldImport(); + D.visible = true; }; - API.acceptGroupInviteRequest = function (params) { - return this.call(`groups/${params.groupId}/requests/${params.userId}`, { - method: 'PUT', - params: { - action: 'accept' + $app.methods.processWorldImportList = async function () { + var D = this.worldImportDialog; + D.loading = true; + var regexWorldId = + /wrld_[0-9A-Fa-f]{8}-([0-9A-Fa-f]{4}-){3}[0-9A-Fa-f]{12}/g; + var match = []; + var worldIdList = new Set(); + while ((match = regexWorldId.exec(D.input)) !== null) { + worldIdList.add(match[0]); + } + D.input = ''; + D.errors = ''; + D.progress = 0; + D.progressTotal = worldIdList.size; + var data = Array.from(worldIdList); + for (var i = 0; i < data.length; ++i) { + if (!D.visible) { + this.resetWorldImport(); } - }).then((json) => { - var args = { - json, - params - }; - this.$emit('GROUP:INVITE:ACCEPT', args); - return args; - }); + if (!D.loading || !D.visible) { + break; + } + var worldId = data[i]; + if (!D.worldIdList.has(worldId)) { + try { + var args = await API.getWorld({ + worldId + }); + this.worldImportTable.data.push(args.ref); + D.worldIdList.add(worldId); + } catch (err) { + D.errors = D.errors.concat( + `WorldId: ${worldId}\n${err}\n\n` + ); + } + } + D.progress++; + if (D.progress === worldIdList.size) { + D.progress = 0; + } + } + D.loading = false; }; - API.rejectGroupInviteRequest = function (params) { - return this.call(`groups/${params.groupId}/requests/${params.userId}`, { - method: 'PUT', - params: { - action: 'reject' - } - }).then((json) => { - var args = { - json, - params - }; - this.$emit('GROUP:INVITE:REJECT', args); - return args; - }); + $app.methods.deleteItemWorldImport = function (ref) { + var D = this.worldImportDialog; + $app.removeFromArray(this.worldImportTable.data, ref); + D.worldIdList.delete(ref.id); }; - API.blockGroupInviteRequest = function (params) { - return this.call(`groups/${params.groupId}/requests/${params.userId}`, { - method: 'PUT', - params: { - action: 'reject', - block: true - } - }).then((json) => { - var args = { - json, - params - }; - this.$emit('GROUP:INVITE:BLOCK', args); - return args; - }); + $app.methods.resetWorldImport = function () { + var D = this.worldImportDialog; + D.input = ''; + D.errors = ''; + }; + + $app.methods.clearWorldImportTable = function () { + var D = this.worldImportDialog; + this.worldImportTable.data = []; + D.worldIdList = new Set(); + }; + + $app.methods.selectWorldImportGroup = function (group) { + var D = this.worldImportDialog; + D.worldImportLocalFavoriteGroup = null; + D.worldImportFavoriteGroup = group; }; - API.getGroupBans = function (params) { - return this.call(`groups/${params.groupId}/bans`, { - method: 'GET', - params - }).then((json) => { - var args = { - json, - params - }; - this.$emit('GROUP:BANS', args); - return args; - }); + $app.methods.selectWorldImportLocalGroup = function (group) { + var D = this.worldImportDialog; + D.worldImportFavoriteGroup = null; + D.worldImportLocalFavoriteGroup = group; }; - $app.methods.getAllGroupBans = async function (groupId) { - this.groupBansModerationTable.data = []; - var params = { - groupId, - n: 100, - offset: 0 - }; - var count = 50; // 5000 max - this.isGroupMembersLoading = true; + $app.methods.cancelWorldImport = function () { + var D = this.worldImportDialog; + D.loading = false; + }; + + $app.methods.importWorldImportTable = async function () { + var D = this.worldImportDialog; + if (!D.worldImportFavoriteGroup && !D.worldImportLocalFavoriteGroup) { + return; + } + D.loading = true; + var data = [...this.worldImportTable.data].reverse(); + D.importProgressTotal = data.length; try { - for (var i = 0; i < count; i++) { - var args = await API.getGroupBans(params); - params.offset += params.n; - if (args.json.length < params.n) { + for (var i = data.length - 1; i >= 0; i--) { + if (!D.loading || !D.visible) { break; } - if (!this.groupMemberModeration.visible) { - break; + var ref = data[i]; + if (D.worldImportFavoriteGroup) { + await this.addFavoriteWorld( + ref, + D.worldImportFavoriteGroup + ); + } else if (D.worldImportLocalFavoriteGroup) { + this.addLocalWorldFavorite( + ref.id, + D.worldImportLocalFavoriteGroup + ); } + $app.removeFromArray(this.worldImportTable.data, ref); + D.worldIdList.delete(ref.id); + D.importProgress++; } } catch (err) { - this.$message({ - message: 'Failed to get group bans', - type: 'error' - }); + D.errors = `Name: ${ref.name}\nWorldId: ${ref.id}\n${err}\n\n`; } finally { - this.isGroupMembersLoading = false; + D.importProgress = 0; + D.importProgressTotal = 0; + D.loading = false; } }; - API.$on('GROUP:BANS', function (args) { - if ($app.groupMemberModeration.id !== args.params.groupId) { - return; - } + API.$on('LOGIN', function () { + $app.clearWorldImportTable(); + $app.resetWorldImport(); + $app.worldImportDialog.visible = false; + $app.worldImportFavoriteGroup = null; + $app.worldImportLocalFavoriteGroup = null; - for (var json of args.json) { - var ref = this.applyGroupMember(json); - $app.groupBansModerationTable.data.push(ref); - } + $app.worldExportDialogVisible = false; + $app.worldExportFavoriteGroup = null; + $app.worldExportLocalFavoriteGroup = null; }); - $app.methods.getAllGroupLogs = async function (groupId) { - this.groupLogsModerationTable.data = []; - var params = { - groupId, - n: 100, - offset: 0 + // #endregion + // #region | App: world favorite export + + $app.data.worldExportDialogRef = {}; + $app.data.worldExportDialogVisible = false; + $app.data.worldExportContent = ''; + $app.data.worldExportFavoriteGroup = null; + $app.data.worldExportLocalFavoriteGroup = null; + + $app.methods.showWorldExportDialog = function () { + this.$nextTick(() => + $app.adjustDialogZ(this.$refs.worldExportDialogRef.$el) + ); + this.worldExportFavoriteGroup = null; + this.worldExportLocalFavoriteGroup = null; + this.updateWorldExportDialog(); + this.worldExportDialogVisible = true; + }; + + $app.methods.updateWorldExportDialog = function () { + var _ = function (str) { + if (/[\x00-\x1f,"]/.test(str) === true) { + return `"${str.replace(/"/g, '""')}"`; + } + return str; }; - if (this.groupMemberModeration.selectedAuditLogTypes.length) { - params.eventTypes = - this.groupMemberModeration.selectedAuditLogTypes; - } - var count = 50; // 5000 max - this.isGroupMembersLoading = true; - try { - for (var i = 0; i < count; i++) { - var args = await API.getGroupLogs(params); - params.offset += params.n; - if (!args.json.hasNext) { - break; - } - if (!this.groupMemberModeration.visible) { - break; + var lines = ['WorldID,Name']; + if (this.worldExportFavoriteGroup) { + API.favoriteWorldGroups.forEach((group) => { + if (this.worldExportFavoriteGroup === group) { + $app.favoriteWorlds.forEach((ref) => { + if (group.key === ref.groupKey) { + lines.push(`${_(ref.id)},${_(ref.name)}`); + } + }); } + }); + } else if (this.worldExportLocalFavoriteGroup) { + var favoriteGroup = + this.localWorldFavorites[this.worldExportLocalFavoriteGroup]; + if (!favoriteGroup) { + return; } - } catch (err) { - this.$message({ - message: 'Failed to get group logs', - type: 'error' + for (var i = 0; i < favoriteGroup.length; ++i) { + var ref = favoriteGroup[i]; + lines.push(`${_(ref.id)},${_(ref.name)}`); + } + } else { + // export all + this.favoriteWorlds.forEach((ref1) => { + lines.push(`${_(ref1.id)},${_(ref1.name)}`); }); - } finally { - this.isGroupMembersLoading = false; + for (var i = 0; i < this.localWorldFavoritesList.length; ++i) { + var worldId = this.localWorldFavoritesList[i]; + var ref2 = API.cachedWorlds.get(worldId); + if (typeof ref2 !== 'undefined') { + lines.push(`${_(ref2.id)},${_(ref2.name)}`); + } + } } + this.worldExportContent = lines.join('\n'); }; - /** - * @param {{ groupId: string }} params - * @return { Promise<{json: any, params}> } - */ - API.getGroupAuditLogTypes = function (params) { - return this.call(`groups/${params.groupId}/auditLogTypes`, { - method: 'GET' - }).then((json) => { - var args = { - json, - params - }; - this.$emit('GROUP:AUDITLOGTYPES', args); - return args; - }); + $app.methods.selectWorldExportGroup = function (group) { + this.worldExportFavoriteGroup = group; + this.worldExportLocalFavoriteGroup = null; + this.updateWorldExportDialog(); + }; + + $app.methods.selectWorldExportLocalGroup = function (group) { + this.worldExportLocalFavoriteGroup = group; + this.worldExportFavoriteGroup = null; + this.updateWorldExportDialog(); + }; + + // #endregion + // #region | App: avatar favorite import + + $app.data.avatarImportDialog = { + visible: false, + loading: false, + progress: 0, + progressTotal: 0, + input: '', + avatarIdList: new Set(), + errors: '', + avatarImportFavoriteGroup: null, + avatarImportLocalFavoriteGroup: null, + importProgress: 0, + importProgressTotal: 0 + }; + + $app.data.avatarImportTable = { + data: [], + tableProps: { + stripe: true, + size: 'mini' + }, + layout: 'table' + }; + + $app.methods.showAvatarImportDialog = function () { + this.$nextTick(() => + $app.adjustDialogZ(this.$refs.avatarImportDialog.$el) + ); + var D = this.avatarImportDialog; + this.resetAvatarImport(); + D.visible = true; + }; + + $app.methods.processAvatarImportList = async function () { + var D = this.avatarImportDialog; + D.loading = true; + var regexAvatarId = + /avtr_[0-9A-Fa-f]{8}-([0-9A-Fa-f]{4}-){3}[0-9A-Fa-f]{12}/g; + var match = []; + var avatarIdList = new Set(); + while ((match = regexAvatarId.exec(D.input)) !== null) { + avatarIdList.add(match[0]); + } + D.input = ''; + D.errors = ''; + D.progress = 0; + D.progressTotal = avatarIdList.size; + var data = Array.from(avatarIdList); + for (var i = 0; i < data.length; ++i) { + if (!D.visible) { + this.resetAvatarImport(); + } + if (!D.loading || !D.visible) { + break; + } + var avatarId = data[i]; + if (!D.avatarIdList.has(avatarId)) { + try { + var args = await API.getAvatar({ + avatarId + }); + this.avatarImportTable.data.push(args.ref); + D.avatarIdList.add(avatarId); + } catch (err) { + D.errors = D.errors.concat( + `AvatarId: ${avatarId}\n${err}\n\n` + ); + } + } + D.progress++; + if (D.progress === avatarIdList.size) { + D.progress = 0; + } + } + D.loading = false; + }; + + $app.methods.deleteItemAvatarImport = function (ref) { + var D = this.avatarImportDialog; + $app.removeFromArray(this.avatarImportTable.data, ref); + D.avatarIdList.delete(ref.id); + }; + + $app.methods.resetAvatarImport = function () { + var D = this.avatarImportDialog; + D.input = ''; + D.errors = ''; + }; + + $app.methods.clearAvatarImportTable = function () { + var D = this.avatarImportDialog; + this.avatarImportTable.data = []; + D.avatarIdList = new Set(); }; - API.$on('GROUP:AUDITLOGTYPES', function (args) { - if ($app.groupMemberModeration.id !== args.params.groupId) { - return; - } - $app.groupMemberModeration.auditLogTypes = args.json; - }); + $app.methods.selectAvatarImportGroup = function (group) { + var D = this.avatarImportDialog; + D.avatarImportLocalFavoriteGroup = null; + D.avatarImportFavoriteGroup = group; + }; - $app.methods.getAuditLogTypeName = function (auditLogType) { - if (!auditLogType) { - return ''; - } - return auditLogType - .replace('group.', '') - .replace(/\./g, ' ') - .replace(/\b\w/g, (l) => l.toUpperCase()); + $app.methods.selectAvatarImportLocalGroup = function (group) { + var D = this.avatarImportDialog; + D.avatarImportFavoriteGroup = null; + D.avatarImportLocalFavoriteGroup = group; }; - /** - * @param {{ groupId: string, eventTypes: array }} params - * @return { Promise<{json: any, params}> } - */ - API.getGroupLogs = function (params) { - return this.call(`groups/${params.groupId}/auditLogs`, { - method: 'GET', - params - }).then((json) => { - var args = { - json, - params - }; - this.$emit('GROUP:LOGS', args); - return args; - }); + $app.methods.cancelAvatarImport = function () { + var D = this.avatarImportDialog; + D.loading = false; }; - API.$on('GROUP:LOGS', function (args) { - if ($app.groupMemberModeration.id !== args.params.groupId) { + $app.methods.importAvatarImportTable = async function () { + var D = this.avatarImportDialog; + if (!D.avatarImportFavoriteGroup && !D.avatarImportLocalFavoriteGroup) { return; } - - for (var json of args.json.results) { - const existsInData = $app.groupLogsModerationTable.data.some( - (dataItem) => dataItem.id === json.id - ); - if (!existsInData) { - $app.groupLogsModerationTable.data.push(json); - } - } - }); - - $app.methods.getAllGroupInvitesAndJoinRequests = async function (groupId) { - await this.getAllGroupInvites(groupId); - await this.getAllGroupJoinRequests(groupId); - await this.getAllGroupBlockedRequests(groupId); - }; - - $app.methods.getAllGroupInvites = async function (groupId) { - this.groupInvitesModerationTable.data = []; - var params = { - groupId, - n: 100, - offset: 0 - }; - var count = 50; // 5000 max - this.isGroupMembersLoading = true; + D.loading = true; + var data = [...this.avatarImportTable.data].reverse(); + D.importProgressTotal = data.length; try { - for (var i = 0; i < count; i++) { - var args = await API.getGroupInvites(params); - params.offset += params.n; - if (args.json.length < params.n) { + for (var i = data.length - 1; i >= 0; i--) { + if (!D.loading || !D.visible) { break; } - if (!this.groupMemberModeration.visible) { - break; + var ref = data[i]; + if (D.avatarImportFavoriteGroup) { + await this.addFavoriteAvatar( + ref, + D.avatarImportFavoriteGroup + ); + } else if (D.avatarImportLocalFavoriteGroup) { + this.addLocalAvatarFavorite( + ref.id, + D.avatarImportLocalFavoriteGroup + ); } + $app.removeFromArray(this.avatarImportTable.data, ref); + D.avatarIdList.delete(ref.id); + D.importProgress++; } } catch (err) { - this.$message({ - message: 'Failed to get group invites', - type: 'error' - }); + D.errors = `Name: ${ref.name}\nAvatarId: ${ref.id}\n${err}\n\n`; } finally { - this.isGroupMembersLoading = false; + D.importProgress = 0; + D.importProgressTotal = 0; + D.loading = false; } }; - /** - * @param {{ groupId: string }} params - * @return { Promise<{json: any, params}> } - */ - API.getGroupInvites = function (params) { - return this.call(`groups/${params.groupId}/invites`, { - method: 'GET', - params - }).then((json) => { - var args = { - json, - params - }; - this.$emit('GROUP:INVITES', args); - return args; - }); - }; - - API.$on('GROUP:INVITES', function (args) { - if ($app.groupMemberModeration.id !== args.params.groupId) { - return; - } + API.$on('LOGIN', function () { + $app.clearAvatarImportTable(); + $app.resetAvatarImport(); + $app.avatarImportDialog.visible = false; + $app.avatarImportFavoriteGroup = null; + $app.avatarImportLocalFavoriteGroup = null; - for (var json of args.json) { - var ref = this.applyGroupMember(json); - $app.groupInvitesModerationTable.data.push(ref); - } + $app.avatarExportDialogVisible = false; + $app.avatarExportFavoriteGroup = null; + $app.avatarExportLocalFavoriteGroup = null; }); - $app.methods.getAllGroupJoinRequests = async function (groupId) { - this.groupJoinRequestsModerationTable.data = []; - var params = { - groupId, - n: 100, - offset: 0 - }; - var count = 50; // 5000 max - this.isGroupMembersLoading = true; - try { - for (var i = 0; i < count; i++) { - var args = await API.getGroupJoinRequests(params); - params.offset += params.n; - if (args.json.length < params.n) { - break; - } - if (!this.groupMemberModeration.visible) { - break; - } - } - } catch (err) { - this.$message({ - message: 'Failed to get group join requests', - type: 'error' - }); - } finally { - this.isGroupMembersLoading = false; - } + // #endregion + // #region | App: avatar favorite export + + $app.data.avatarExportDialogRef = {}; + $app.data.avatarExportDialogVisible = false; + $app.data.avatarExportContent = ''; + $app.data.avatarExportFavoriteGroup = null; + $app.data.avatarExportLocalFavoriteGroup = null; + + $app.methods.showAvatarExportDialog = function () { + this.$nextTick(() => + $app.adjustDialogZ(this.$refs.avatarExportDialogRef.$el) + ); + this.avatarExportFavoriteGroup = null; + this.avatarExportLocalFavoriteGroup = null; + this.updateAvatarExportDialog(); + this.avatarExportDialogVisible = true; }; - $app.methods.getAllGroupBlockedRequests = async function (groupId) { - this.groupBlockedModerationTable.data = []; - var params = { - groupId, - n: 100, - offset: 0, - blocked: true + $app.methods.updateAvatarExportDialog = function () { + var _ = function (str) { + if (/[\x00-\x1f,"]/.test(str) === true) { + return `"${str.replace(/"/g, '""')}"`; + } + return str; }; - var count = 50; // 5000 max - this.isGroupMembersLoading = true; - try { - for (var i = 0; i < count; i++) { - var args = await API.getGroupJoinRequests(params); - params.offset += params.n; - if (args.json.length < params.n) { - break; - } - if (!this.groupMemberModeration.visible) { - break; + var lines = ['AvatarID,Name']; + if (this.avatarExportFavoriteGroup) { + API.favoriteAvatarGroups.forEach((group) => { + if ( + !this.avatarExportFavoriteGroup || + this.avatarExportFavoriteGroup === group + ) { + $app.favoriteAvatars.forEach((ref) => { + if (group.key === ref.groupKey) { + lines.push(`${_(ref.id)},${_(ref.name)}`); + } + }); } - } - } catch (err) { - this.$message({ - message: 'Failed to get group join requests', - type: 'error' }); - } finally { - this.isGroupMembersLoading = false; - } - }; - - /** - * @param {{ groupId: string }} params - * @return { Promise<{json: any, params}> } - */ - API.getGroupJoinRequests = function (params) { - return this.call(`groups/${params.groupId}/requests`, { - method: 'GET', - params - }).then((json) => { - var args = { - json, - params - }; - this.$emit('GROUP:JOINREQUESTS', args); - return args; - }); - }; - - API.$on('GROUP:JOINREQUESTS', function (args) { - if ($app.groupMemberModeration.id !== args.params.groupId) { - return; - } - - if (!args.params.blocked) { - for (var json of args.json) { - var ref = this.applyGroupMember(json); - $app.groupJoinRequestsModerationTable.data.push(ref); + } else if (this.avatarExportLocalFavoriteGroup) { + var favoriteGroup = + this.localAvatarFavorites[this.avatarExportLocalFavoriteGroup]; + if (!favoriteGroup) { + return; + } + for (var i = 0; i < favoriteGroup.length; ++i) { + var ref = favoriteGroup[i]; + lines.push(`${_(ref.id)},${_(ref.name)}`); } } else { - for (var json of args.json) { - var ref = this.applyGroupMember(json); - $app.groupBlockedModerationTable.data.push(ref); + // export all + this.favoriteAvatars.forEach((ref1) => { + lines.push(`${_(ref1.id)},${_(ref1.name)}`); + }); + for (var i = 0; i < this.localAvatarFavoritesList.length; ++i) { + var avatarId = this.localAvatarFavoritesList[i]; + var ref2 = API.cachedAvatars.get(avatarId); + if (typeof ref2 !== 'undefined') { + lines.push(`${_(ref2.id)},${_(ref2.name)}`); + } } } - }); - - /** - * @param {{ groupId: string }} params - * @return { Promise<{json: any, params}> } - */ - API.getGroupInstances = function (params) { - return this.call( - `users/${this.currentUser.id}/instances/groups/${params.groupId}`, - { - method: 'GET' - } - ).then((json) => { - var args = { - json, - params - }; - this.$emit('GROUP:INSTANCES', args); - return args; - }); + this.avatarExportContent = lines.join('\n'); }; - API.$on('GROUP:INSTANCES', function (args) { - if ($app.groupDialog.id === args.params.groupId) { - $app.applyGroupDialogInstances(args.json.instances); - } - }); - - API.$on('GROUP:INSTANCES', function (args) { - for (var json of args.json.instances) { - this.$emit('INSTANCE', { - json, - params: { - fetchedAt: args.json.fetchedAt - } - }); - this.getCachedWorld({ - worldId: json.world.id - }).then((args1) => { - json.world = args1.ref; - return args1; - }); - // get queue size etc - this.getInstance({ - worldId: json.worldId, - instanceId: json.instanceId - }); - } - }); + $app.methods.selectAvatarExportGroup = function (group) { + this.avatarExportFavoriteGroup = group; + this.avatarExportLocalFavoriteGroup = null; + this.updateAvatarExportDialog(); + }; - /** - * @param {{ groupId: string }} params - * @return { Promise<{json: any, params}> } - */ + $app.methods.selectAvatarExportLocalGroup = function (group) { + this.avatarExportLocalFavoriteGroup = group; + this.avatarExportFavoriteGroup = null; + this.updateAvatarExportDialog(); + }; - API.getGroupRoles = function (params) { - return this.call(`groups/${params.groupId}/roles`, { - method: 'GET', - params - }).then((json) => { - var args = { - json, - params - }; - this.$emit('GROUP:ROLES', args); - return args; - }); + // #endregion + // #region | App: friend favorite import + + $app.data.friendImportDialog = { + visible: false, + loading: false, + progress: 0, + progressTotal: 0, + input: '', + userIdList: new Set(), + errors: '', + friendImportFavoriteGroup: null, + importProgress: 0, + importProgressTotal: 0 }; - API.getRequestedGroups = function () { - return this.call(`users/${this.currentUser.id}/groups/requested`, { - method: 'GET' - }).then((json) => { - var args = { - json - }; - this.$emit('GROUP:REQUESTED', args); - return args; - }); + $app.data.friendImportTable = { + data: [], + tableProps: { + stripe: true, + size: 'mini' + }, + layout: 'table' }; - API.getUsersGroupInstances = function () { - return this.call(`users/${this.currentUser.id}/instances/groups`, { - method: 'GET' - }).then((json) => { - var args = { - json - }; - this.$emit('GROUP:USER:INSTANCES', args); - return args; - }); + $app.methods.showFriendImportDialog = function () { + this.$nextTick(() => + $app.adjustDialogZ(this.$refs.friendImportDialog.$el) + ); + var D = this.friendImportDialog; + this.resetFriendImport(); + D.visible = true; }; - API.$on('GROUP:USER:INSTANCES', function (args) { - $app.groupInstances = []; - for (var json of args.json.instances) { - if (args.json.fetchedAt) { - // tack on fetchedAt - json.$fetchedAt = args.json.fetchedAt; + $app.methods.processFriendImportList = async function () { + var D = this.friendImportDialog; + D.loading = true; + var regexFriendId = + /usr_[0-9A-Fa-f]{8}-([0-9A-Fa-f]{4}-){3}[0-9A-Fa-f]{12}/g; + var match = []; + var userIdList = new Set(); + while ((match = regexFriendId.exec(D.input)) !== null) { + userIdList.add(match[0]); + } + D.input = ''; + D.errors = ''; + D.progress = 0; + D.progressTotal = userIdList.size; + var data = Array.from(userIdList); + for (var i = 0; i < data.length; ++i) { + if (!D.visible) { + this.resetFriendImport(); } - this.$emit('INSTANCE', { - json, - params: { - fetchedAt: args.json.fetchedAt - } - }); - var ref = this.cachedGroups.get(json.ownerId); - if (typeof ref === 'undefined') { - if ($app.friendLogInitStatus) { - this.getGroup({ groupId: json.ownerId }); + if (!D.loading || !D.visible) { + break; + } + var userId = data[i]; + if (!D.userIdList.has(userId)) { + try { + var args = await API.getUser({ + userId + }); + this.friendImportTable.data.push(args.ref); + D.userIdList.add(userId); + } catch (err) { + D.errors = D.errors.concat(`UserId: ${userId}\n${err}\n\n`); } - return; } - $app.groupInstances.push({ - group: ref, - instance: this.applyInstance(json) - }); + D.progress++; + if (D.progress === userIdList.size) { + D.progress = 0; + } } - }); - - /** - * @param {{ - query: string, - n: number, - offset: number, - order: string, - sortBy: string - }} params - * @return { Promise<{json: any, params}> } - */ - API.groupSearch = function (params) { - return this.call(`groups`, { - method: 'GET', - params - }).then((json) => { - var args = { - json, - params - }; - this.$emit('GROUP:SEARCH', args); - return args; - }); + D.loading = false; }; - API.$on('GROUP:SEARCH', function (args) { - for (var json of args.json) { - this.$emit('GROUP', { - json, - params: { - groupId: json.id - } - }); - } - }); + $app.methods.deleteItemFriendImport = function (ref) { + var D = this.friendImportDialog; + $app.removeFromArray(this.friendImportTable.data, ref); + D.userIdList.delete(ref.id); + }; - /** - * @param {{ groupId: string }} params - * @return { Promise<{json: any, params}> } - */ - API.getCachedGroup = function (params) { - return new Promise((resolve, reject) => { - var ref = this.cachedGroups.get(params.groupId); - if (typeof ref === 'undefined') { - this.getGroup(params).catch(reject).then(resolve); - } else { - resolve({ - cache: true, - json: ref, - params, - ref - }); - } - }); + $app.methods.resetFriendImport = function () { + var D = this.friendImportDialog; + D.input = ''; + D.errors = ''; }; - API.applyGroup = function (json) { - var ref = this.cachedGroups.get(json.id); - json.rules = $app.replaceBioSymbols(json.rules); - json.name = $app.replaceBioSymbols(json.name); - json.description = $app.replaceBioSymbols(json.description); - if (typeof ref === 'undefined') { - ref = { - id: '', - name: '', - shortCode: '', - description: '', - bannerId: '', - bannerUrl: '', - createdAt: '', - discriminator: '', - galleries: [], - iconId: '', - iconUrl: '', - isVerified: false, - joinState: '', - languages: [], - links: [], - memberCount: 0, - memberCountSyncedAt: '', - membershipStatus: '', - onlineMemberCount: 0, - ownerId: '', - privacy: '', - rules: null, - tags: [], - // in group - initialRoleIds: [], - myMember: { - bannedAt: null, - groupId: '', - has2FA: false, - id: '', - isRepresenting: false, - isSubscribedToAnnouncements: false, - joinedAt: '', - managerNotes: '', - membershipStatus: '', - permissions: [], - roleIds: [], - userId: '', - visibility: '', - _created_at: '', - _id: '', - _updated_at: '' - }, - updatedAt: '', - // includeRoles: true - roles: [], - // group list - $memberId: '', - groupId: '', - isRepresenting: false, - memberVisibility: false, - mutualGroup: false, - // VRCX - $languages: [], - ...json - }; - this.cachedGroups.set(ref.id, ref); - } else { - if (this.currentUserGroups.has(ref.id)) { - // compare group props - if ( - ref.ownerId && - json.ownerId && - ref.ownerId !== json.ownerId - ) { - // owner changed - $app.groupOwnerChange(json, ref.ownerId, json.ownerId); - } - if (ref.name && json.name && ref.name !== json.name) { - // name changed - $app.groupChange( - json, - `Name changed from ${ref.name} to ${json.name}` - ); - } - if (ref.myMember?.roleIds && json.myMember?.roleIds) { - var oldRoleIds = ref.myMember.roleIds; - var newRoleIds = json.myMember.roleIds; - if ( - oldRoleIds.length !== newRoleIds.length || - !oldRoleIds.every( - (value, index) => value === newRoleIds[index] - ) - ) { - // roleIds changed - $app.groupRoleChange( - json, - ref.roles, - json.roles, - oldRoleIds, - newRoleIds - ); - } - } - } - Object.assign(ref, json); - } - ref.$url = `https://vrc.group/${ref.shortCode}.${ref.discriminator}`; - this.applyGroupLanguage(ref); - return ref; + $app.methods.clearFriendImportTable = function () { + var D = this.friendImportDialog; + this.friendImportTable.data = []; + D.userIdList = new Set(); }; - $app.methods.groupOwnerChange = async function (ref, oldUserId, newUserId) { - var oldUser = await API.getCachedUser({ - userId: oldUserId - }); - var newUser = await API.getCachedUser({ - userId: newUserId - }); - var oldDisplayName = oldUser?.ref?.displayName; - var newDisplayName = newUser?.ref?.displayName; + $app.methods.selectFriendImportGroup = function (group) { + var D = this.friendImportDialog; + D.friendImportFavoriteGroup = group; + }; - this.groupChange( - ref, - `Owner changed from ${oldDisplayName} to ${newDisplayName}` - ); + $app.methods.cancelFriendImport = function () { + var D = this.friendImportDialog; + D.loading = false; }; - $app.methods.groupRoleChange = function ( - ref, - oldRoles, - newRoles, - oldRoleIds, - newRoleIds - ) { - // check for removed/added roleIds - for (var roleId of oldRoleIds) { - if (!newRoleIds.includes(roleId)) { - var roleName = ''; - var role = oldRoles.find((fineRole) => fineRole.id === roleId); - if (role) { - roleName = role.name; - } - this.groupChange(ref, `Role ${roleName} removed`); - } + $app.methods.importFriendImportTable = async function () { + var D = this.friendImportDialog; + D.loading = true; + if (!D.friendImportFavoriteGroup) { + return; } - for (var roleId of newRoleIds) { - if (!oldRoleIds.includes(roleId)) { - var roleName = ''; - var role = newRoles.find((fineRole) => fineRole.id === roleId); - if (role) { - roleName = role.name; + var data = [...this.friendImportTable.data].reverse(); + D.importProgressTotal = data.length; + try { + for (var i = data.length - 1; i >= 0; i--) { + if (!D.loading || !D.visible) { + break; } - this.groupChange(ref, `Role ${roleName} added`); + var ref = data[i]; + await this.addFavoriteUser(ref, D.friendImportFavoriteGroup); + $app.removeFromArray(this.friendImportTable.data, ref); + D.userIdList.delete(ref.id); + D.importProgress++; } + } catch (err) { + D.errors = `Name: ${ref.displayName}\nUserId: ${ref.id}\n${err}\n\n`; + } finally { + D.importProgress = 0; + D.importProgressTotal = 0; + D.loading = false; } }; - $app.methods.groupChange = function (ref, message) { - if (!this.currentUserGroupsInit) { - return; - } - // oh the level of cursed for compibility - var json = { - id: Math.random().toString(36), - type: 'groupChange', - senderUserId: ref.id, - senderUsername: ref.name, - imageUrl: ref.iconUrl, - details: { - imageUrl: ref.iconUrl - }, - message, - created_at: new Date().toJSON() - }; - API.$emit('NOTIFICATION', { - json, - params: { - notificationId: json.id - } - }); + API.$on('LOGIN', function () { + $app.clearFriendImportTable(); + $app.resetFriendImport(); + $app.friendImportDialog.visible = false; + $app.friendImportFavoriteGroup = null; - // delay to wait for json to be assigned to ref - workerTimers.setTimeout(this.saveCurrentUserGroups, 100); - }; + $app.friendExportDialogVisible = false; + $app.friendExportFavoriteGroup = null; + }); - $app.data.currentUserGroupsInit = false; + // #endregion + // #region | App: friend favorite export - $app.methods.saveCurrentUserGroups = function () { - if (!this.currentUserGroupsInit) { - return; - } - var groups = []; - for (var ref of API.currentUserGroups.values()) { - groups.push({ - id: ref.id, - name: ref.name, - ownerId: ref.ownerId, - iconUrl: ref.iconUrl, - roles: ref.roles, - roleIds: ref.myMember?.roleIds - }); - } - configRepository.setString( - `VRCX_currentUserGroups_${API.currentUser.id}`, - JSON.stringify(groups) + $app.data.friendExportDialogRef = {}; + $app.data.friendExportDialogVisible = false; + $app.data.friendExportContent = ''; + $app.data.friendExportFavoriteGroup = null; + + $app.methods.showFriendExportDialog = function () { + this.$nextTick(() => + $app.adjustDialogZ(this.$refs.friendExportDialogRef.$el) ); + this.friendExportFavoriteGroup = null; + this.updateFriendExportDialog(); + this.friendExportDialogVisible = true; + }; + + $app.methods.updateFriendExportDialog = function () { + var _ = function (str) { + if (/[\x00-\x1f,"]/.test(str) === true) { + return `"${str.replace(/"/g, '""')}"`; + } + return str; + }; + var lines = ['UserID,Name']; + API.favoriteFriendGroups.forEach((group) => { + if ( + !this.friendExportFavoriteGroup || + this.friendExportFavoriteGroup === group + ) { + $app.favoriteFriends.forEach((ref) => { + if (group.key === ref.groupKey) { + lines.push(`${_(ref.id)},${_(ref.name)}`); + } + }); + } + }); + this.friendExportContent = lines.join('\n'); }; - $app.methods.loadCurrentUserGroups = async function (userId, groups) { - var savedGroups = JSON.parse( - await configRepository.getString( - `VRCX_currentUserGroups_${userId}`, - '[]' - ) - ); - API.cachedGroups.clear(); - API.currentUserGroups.clear(); - for (var group of savedGroups) { - var ref = { - id: group.id, - name: group.name, - iconUrl: group.iconUrl, - ownerId: group.ownerId, - roles: group.roles, - myMember: { - roleIds: group.roleIds - } - }; - API.cachedGroups.set(group.id, ref); - API.currentUserGroups.set(group.id, ref); - } - - if (groups) { - for (var i = 0; i < groups.length; i++) { - var groupId = groups[i]; - var groupRef = API.cachedGroups.get(groupId); - if ( - typeof groupRef !== 'undefined' && - groupRef.myMember?.roleIds?.length > 0 - ) { - continue; - } + $app.methods.selectFriendExportGroup = function (group) { + this.friendExportFavoriteGroup = group; + this.updateFriendExportDialog(); + }; - try { - var args = await API.getGroup({ - groupId, - includeRoles: true - }); - var ref = API.applyGroup(args.json); - API.currentUserGroups.set(groupId, ref); - } catch (err) { - console.error(err); - } - } - } + // #endregion + // #region | App: user dialog notes - this.currentUserGroupsInit = true; + API.saveNote = function (params) { + return this.call('userNotes', { + method: 'POST', + params + }).then((json) => { + var args = { + json, + params + }; + this.$emit('NOTE', args); + return args; + }); }; - API.applyGroupMember = function (json) { - if (typeof json?.user !== 'undefined') { - if (json.userId === this.currentUser.id) { - json.user = this.currentUser; - json.$displayName = this.currentUser.displayName; + API.$on('NOTE', function (args) { + var note = ''; + var targetUserId = ''; + if (typeof args.json !== 'undefined') { + note = $app.replaceBioSymbols(args.json.note); + } + if (typeof args.params !== 'undefined') { + targetUserId = args.params.targetUserId; + } + if (targetUserId === $app.userDialog.id) { + if (note === args.params.note) { + $app.userDialog.noteSaving = false; + $app.userDialog.note = note; } else { - var ref = this.cachedUsers.get(json.user.id); - if (typeof ref !== 'undefined') { - json.user = ref; - json.$displayName = ref.displayName; - } else { - json.$displayName = json.user?.displayName; - } + // response is cached sadge :< + this.getUser({ userId: targetUserId }); } } - return json; - }; - - API.applyGroupLanguage = function (ref) { - ref.$languages = []; - var { languages } = ref; - if (!languages) { - return; + var ref = API.cachedUsers.get(targetUserId); + if (typeof ref !== 'undefined') { + ref.note = note; } - for (var language of languages) { - var value = $app.subsetOfLanguages[language]; - if (typeof value === 'undefined') { - continue; - } - ref.$languages.push({ - key: language, - value - }); + }); + + $app.methods.checkNote = function (ref, note) { + if (ref.note !== note) { + this.addNote(ref.id, note); } }; - $app.data.groupDialog = { - visible: false, - loading: false, - treeData: [], - id: '', - inGroup: false, - ownerDisplayName: '', - ref: {}, - announcement: {}, - posts: [], - postsFiltered: [], - members: [], - memberSearch: '', - memberSearchResults: [], - instances: [], - memberRoles: [], - memberFilter: { - name: $t('dialog.group.members.filters.everyone'), - id: null - }, - memberSortOrder: { - name: $t('dialog.group.members.sorting.joined_at_desc'), - value: 'joinedAt:desc' - }, - postsSearch: '', - galleries: {} + $app.methods.cleanNote = function (note) { + // remove newlines because they aren't supported + $app.userDialog.note = note.replace(/[\r\n]/g, ''); }; - $app.methods.showGroupDialog = function (groupId) { - if (!groupId) { - return; - } - if ( - this.groupMemberModeration.visible && - this.groupMemberModeration.id !== groupId - ) { - this.groupMemberModeration.visible = false; + $app.methods.addNote = function (userId, note) { + if (this.userDialog.id === userId) { + this.userDialog.noteSaving = true; } - this.$nextTick(() => adjustDialogZ(this.$refs.groupDialog.$el)); - var D = this.groupDialog; - D.visible = true; - D.loading = true; - D.id = groupId; - D.inGroup = false; - D.ownerDisplayName = ''; - D.treeData = []; - D.announcement = {}; - D.posts = []; - D.postsFiltered = []; - D.instances = []; - D.memberRoles = []; - D.memberSearch = ''; - D.memberSearchResults = []; - if (this.groupDialogLastGallery !== groupId) { - D.galleries = {}; - } - if (this.groupDialogLastMembers !== groupId) { - D.members = []; - D.memberFilter = this.groupDialogFilterOptions.everyone; - } - API.getCachedGroup({ - groupId - }) - .catch((err) => { - D.loading = false; - D.visible = false; - this.$message({ - message: 'Failed to load group', - type: 'error' - }); - throw err; - }) - .then((args) => { - if (groupId === args.ref.id) { - D.loading = false; - D.ref = args.ref; - D.inGroup = args.ref.membershipStatus === 'member'; - D.ownerDisplayName = args.ref.ownerId; - API.getCachedUser({ - userId: args.ref.ownerId - }).then((args1) => { - D.ownerDisplayName = args1.ref.displayName; - return args1; - }); - this.applyGroupDialogInstances(); - this.getGroupDialogGroup(groupId); - } - }); - }; - - $app.methods.getGroupDialogGroup = function (groupId) { - var D = this.groupDialog; - return API.getGroup({ groupId, includeRoles: true }) - .catch((err) => { - throw err; - }) - .then((args1) => { - if (D.id === args1.ref.id) { - D.ref = args1.ref; - D.inGroup = args1.ref.membershipStatus === 'member'; - for (var role of args1.ref.roles) { - if ( - D.ref && - D.ref.myMember && - Array.isArray(D.ref.myMember.roleIds) && - D.ref.myMember.roleIds.includes(role.id) - ) { - D.memberRoles.push(role); - } - } - API.getAllGroupPosts({ - groupId - }); - if (D.inGroup) { - API.getGroupInstances({ - groupId - }); - } - if (this.$refs.groupDialogTabs.currentName === '0') { - this.groupDialogLastActiveTab = $t( - 'dialog.group.info.header' - ); - } else if (this.$refs.groupDialogTabs.currentName === '1') { - this.groupDialogLastActiveTab = $t( - 'dialog.group.posts.header' - ); - } else if (this.$refs.groupDialogTabs.currentName === '2') { - this.groupDialogLastActiveTab = $t( - 'dialog.group.members.header' - ); - if (this.groupDialogLastMembers !== groupId) { - this.groupDialogLastMembers = groupId; - this.getGroupDialogGroupMembers(); - } - } else if (this.$refs.groupDialogTabs.currentName === '3') { - this.groupDialogLastActiveTab = $t( - 'dialog.group.gallery.header' - ); - if (this.groupDialogLastGallery !== groupId) { - this.groupDialogLastGallery = groupId; - this.getGroupGalleries(); - } - } else if (this.$refs.groupDialogTabs.currentName === '4') { - this.groupDialogLastActiveTab = $t( - 'dialog.group.json.header' - ); - this.refreshGroupDialogTreeData(); - } - } - return args1; - }); + return API.saveNote({ + targetUserId: userId, + note + }); }; - $app.methods.groupDialogCommand = function (command) { - var D = this.groupDialog; - if (D.visible === false) { - return; - } - switch (command) { - case 'Refresh': - this.showGroupDialog(D.id); - break; - case 'Moderation Tools': - this.showGroupMemberModerationDialog(D.id); - break; - case 'Create Post': - this.showGroupPostEditDialog(D.id, null); - break; - case 'Leave Group': - this.leaveGroup(D.id); - break; - case 'Block Group': - this.blockGroup(D.id); - break; - case 'Unblock Group': - this.unblockGroup(D.id); - break; - case 'Visibility Everyone': - this.setGroupVisibility(D.id, 'visible'); - break; - case 'Visibility Friends': - this.setGroupVisibility(D.id, 'friends'); - break; - case 'Visibility Hidden': - this.setGroupVisibility(D.id, 'hidden'); - break; - case 'Subscribe To Announcements': - this.setGroupSubscription(D.id, true); - break; - case 'Unsubscribe To Announcements': - this.setGroupSubscription(D.id, false); - break; - case 'Invite To Group': - this.showInviteGroupDialog(D.id, ''); - break; + $app.methods.deleteNote = function (userId) { + if (this.userDialog.id === userId) { + this.userDialog.noteSaving = true; } + return API.saveNote({ + targetUserId: userId, + note: '' + }); }; - $app.data.groupDialogLastActiveTab = ''; - $app.data.groupDialogLastMembers = ''; - $app.data.groupDialogLastGallery = ''; + // #endregion + // #region | App: note export - $app.methods.groupDialogTabClick = function (obj) { - var groupId = this.groupDialog.id; - if (this.groupDialogLastActiveTab === obj.label) { - return; - } - if (obj.label === $t('dialog.group.info.header')) { - // - } else if (obj.label === $t('dialog.group.posts.header')) { - // - } else if (obj.label === $t('dialog.group.members.header')) { - if (this.groupDialogLastMembers !== groupId) { - this.groupDialogLastMembers = groupId; - this.getGroupDialogGroupMembers(); - } - } else if (obj.label === $t('dialog.group.gallery.header')) { - if (this.groupDialogLastGallery !== groupId) { - this.groupDialogLastGallery = groupId; - this.getGroupGalleries(); - } - } else if (obj.label === $t('dialog.group.json.header')) { - this.refreshGroupDialogTreeData(); - } - this.groupDialogLastActiveTab = obj.label; + $app.data.noteExportDialog = { + visible: false, + loading: false, + progress: 0, + progressTotal: 0, + errors: '' + }; + $app.data.noteExportTable = { + data: [], + tableProps: { + stripe: true, + size: 'mini' + }, + layout: 'table' }; - $app.methods.refreshGroupDialogTreeData = function () { - var D = this.groupDialog; - D.treeData = buildTreeData({ - group: D.ref, - posts: D.posts, - instances: D.instances, - members: D.members, - galleries: D.galleries - }); + API.$on('LOGIN', function () { + $app.noteExportTable.data = []; + $app.noteExportDialog.visible = false; + $app.noteExportDialog.loading = false; + $app.noteExportDialog.progress = 0; + $app.noteExportDialog.progressTotal = 0; + $app.noteExportDialog.errors = ''; + }); + + $app.methods.showNoteExportDialog = function () { + this.$nextTick(() => + $app.adjustDialogZ(this.$refs.noteExportDialog.$el) + ); + var D = this.noteExportDialog; + D.progress = 0; + D.progressTotal = 0; + D.loading = false; + D.visible = true; }; - $app.methods.joinGroup = function (groupId) { - if (!groupId) { - return null; - } - return API.joinGroup({ - groupId - }).then((args) => { - if (args.json.membershipStatus === 'member') { - this.$message({ - message: 'Group joined', - type: 'success' - }); - } else if (args.json.membershipStatus === 'requested') { - this.$message({ - message: 'Group join request sent', - type: 'success' + $app.methods.updateNoteExportDialog = function () { + var data = []; + this.friends.forEach((ctx) => { + var newMemo = ctx.memo.replace(/[\r\n]/g, ' '); + if (ctx.memo && ctx.ref && ctx.ref.note !== newMemo.slice(0, 256)) { + data.push({ + id: ctx.id, + name: ctx.name, + memo: newMemo, + ref: ctx.ref }); } - return args; - }); - }; - - API.$on('LOGOUT', function () { - $app.groupDialog.visible = false; - }); - - $app.methods.leaveGroup = function (groupId) { - return API.leaveGroup({ - groupId }); + this.noteExportTable.data = data; }; - $app.methods.cancelGroupRequest = function (groupId) { - return API.cancelGroupRequest({ - groupId - }); + $app.methods.removeFromNoteExportTable = function (ref) { + $app.removeFromArray(this.noteExportTable.data, ref); }; - $app.methods.setGroupRepresentation = function (groupId) { - return API.setGroupRepresentation(groupId, { isRepresenting: true }); + $app.methods.exportNoteExport = async function () { + var D = this.noteExportDialog; + D.loading = true; + var data = [...this.noteExportTable.data].reverse(); + D.progressTotal = data.length; + try { + for (var i = data.length - 1; i >= 0; i--) { + if (D.visible && D.loading) { + var ctx = data[i]; + await API.saveNote({ + targetUserId: ctx.id, + note: ctx.memo.slice(0, 256) + }); + $app.removeFromArray(this.noteExportTable.data, ctx); + D.progress++; + await new Promise((resolve) => { + workerTimers.setTimeout(resolve, 5000); + }); + } + } + } catch (err) { + D.errors = `Name: ${ctx.name}\n${err}\n\n`; + } finally { + D.progress = 0; + D.progressTotal = 0; + D.loading = false; + } }; - $app.methods.clearGroupRepresentation = function (groupId) { - return API.setGroupRepresentation(groupId, { isRepresenting: false }); + $app.methods.cancelNoteExport = function () { + this.noteExportDialog.loading = false; }; - $app.methods.setGroupVisibility = function (groupId, visibility) { - return API.setGroupMemberProps(API.currentUser.id, groupId, { - visibility - }).then((args) => { - this.$message({ - message: 'Group visibility updated', - type: 'success' - }); - return args; - }); - }; + // avatar database provider - $app.methods.setGroupSubscription = function (groupId, subscribe) { - return API.setGroupMemberProps(API.currentUser.id, groupId, { - isSubscribedToAnnouncements: subscribe - }).then((args) => { - this.$message({ - message: 'Group subscription updated', - type: 'success' - }); - return args; - }); + $app.data.avatarProviderDialog = { + visible: false }; - $app.methods.sendNotificationResponse = function ( - notificationId, - responses, - responseType - ) { - if (!Array.isArray(responses) || responses.length === 0) { - return null; - } - var responseData = ''; - for (var i = 0; i < responses.length; i++) { - if (responses[i].type === responseType) { - responseData = responses[i].data; - break; - } - } - return API.sendNotificationResponse({ - notificationId, - responseType, - responseData - }); + $app.methods.showAvatarProviderDialog = function () { + this.$nextTick(() => + $app.adjustDialogZ(this.$refs.avatarProviderDialog.$el) + ); + var D = this.avatarProviderDialog; + D.visible = true; }; - $app.methods.onGroupJoined = function (groupId) { - if ( - this.groupMemberModeration.visible && - this.groupMemberModeration.id === groupId - ) { - // ignore this event if we were the one to trigger it + $app.methods.addAvatarProvider = function (url) { + if (!url) { return; } - if (!API.currentUserGroups.has(groupId)) { - API.currentUserGroups.set(groupId, { - id: groupId, - name: '', - iconUrl: '' - }); - API.getGroup({ groupId, includeRoles: true }).then((args) => { - var ref = API.applyGroup(args.json); - API.currentUserGroups.set(groupId, ref); - this.saveCurrentUserGroups(); - return args; - }); + this.showAvatarProviderDialog(); + if (!this.avatarRemoteDatabaseProviderList.includes(url)) { + this.avatarRemoteDatabaseProviderList.push(url); } + this.saveAvatarProviderList(); }; - $app.methods.onGroupLeft = function (groupId) { - if (this.groupDialog.visible && this.groupDialog.id === groupId) { - this.showGroupDialog(groupId); - } - if (API.currentUserGroups.has(groupId)) { - API.currentUserGroups.delete(groupId); - API.getCachedGroup({ groupId }).then((args) => { - this.groupChange(args.ref, 'Left group'); - }); + $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(); }; - // group search - - $app.methods.groupMembersSearchDebounce = function () { - var D = this.groupDialog; - var search = D.memberSearch; - D.memberSearchResults = []; - if (!search || search.length < 3) { - this.setGroupMemberModerationTable(D.members); - return; + $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); + } } - this.isGroupMembersLoading = true; - API.getGroupMembersSearch({ - groupId: D.id, - query: search, - n: 100, - offset: 0 - }) - .then((args) => { - if (D.id === args.params.groupId) { - D.memberSearchResults = args.json.results; - this.setGroupMemberModerationTable(args.json.results); - } - }) - .finally(() => { - this.isGroupMembersLoading = false; - }); - }; - - $app.data.groupMembersSearchTimer = null; - $app.data.groupMembersSearchPending = false; - $app.methods.groupMembersSearch = function () { - if (this.groupMembersSearchTimer) { - this.groupMembersSearchPending = true; + await configRepository.setString( + 'VRCX_avatarRemoteDatabaseProviderList', + JSON.stringify(this.avatarRemoteDatabaseProviderList) + ); + if (this.avatarRemoteDatabaseProviderList.length > 0) { + this.avatarRemoteDatabaseProvider = + this.avatarRemoteDatabaseProviderList[0]; + this.avatarRemoteDatabase = true; } else { - this.groupMembersSearchExecute(); - this.groupMembersSearchTimer = setTimeout(() => { - if (this.groupMembersSearchPending) { - this.groupMembersSearchExecute(); - } - this.groupMembersSearchTimer = null; - }, 500); + this.avatarRemoteDatabaseProvider = ''; + this.avatarRemoteDatabase = false; } + await configRepository.setBool( + 'VRCX_avatarRemoteDatabase', + this.avatarRemoteDatabase + ); }; - $app.methods.groupMembersSearchExecute = function () { - try { - this.groupMembersSearchDebounce(); - } catch (err) { - console.error(err); - } - this.groupMembersSearchTimer = null; - this.groupMembersSearchPending = false; + $app.methods.setAvatarProvider = function (provider) { + this.avatarRemoteDatabaseProvider = provider; }; - // group posts + // #endregion + // #region | App: bulk unfavorite - $app.methods.updateGroupPostSearch = function () { - var D = this.groupDialog; - var search = D.postsSearch.toLowerCase(); - D.postsFiltered = D.posts.filter((post) => { - if (search === '') { - return true; + $app.data.bulkUnfavoriteMode = false; + + $app.methods.showBulkUnfavoriteSelectionConfirm = function () { + var elementsTicked = []; + // check favorites type + for (var ctx of this.favoriteFriends) { + if (ctx.$selected) { + elementsTicked.push(ctx.id); } - if (post.title.toLowerCase().includes(search)) { - return true; + } + for (var ctx of this.favoriteWorlds) { + if (ctx.$selected) { + elementsTicked.push(ctx.id); } - if (post.text.toLowerCase().includes(search)) { - return true; + } + for (var ctx of this.favoriteAvatars) { + if (ctx.$selected) { + elementsTicked.push(ctx.id); } - return false; - }); - }; - - // group members - - $app.data.isGroupMembersLoading = false; - $app.data.isGroupMembersDone = false; - $app.data.loadMoreGroupMembersParams = {}; - - $app.methods.getGroupDialogGroupMembers = async function () { - var D = this.groupDialog; - D.members = []; - this.isGroupMembersDone = false; - this.loadMoreGroupMembersParams = { - n: 100, - offset: 0, - groupId: D.id - }; - if (D.memberSortOrder.value) { - this.loadMoreGroupMembersParams.sort = D.memberSortOrder.value; } - if (D.memberFilter.id !== null) { - this.loadMoreGroupMembersParams.roleId = D.memberFilter.id; + if (elementsTicked.length === 0) { + return; } - if (D.inGroup) { - await API.getGroupMember({ - groupId: D.id, - userId: API.currentUser.id - }).then((args) => { - if (args.json) { - args.json.user = API.currentUser; - if (D.memberFilter.id === null) { - // when flitered by role don't include self - D.members.push(args.json); + this.$confirm( + `Are you sure you want to unfavorite ${elementsTicked.length} favorites? + This action cannot be undone.`, + `Delete ${elementsTicked.length} favorites?`, + { + confirmButtonText: 'Confirm', + cancelButtonText: 'Cancel', + type: 'info', + callback: (action) => { + if (action === 'confirm') { + this.bulkUnfavoriteSelection(elementsTicked); } } - return args; + } + ); + }; + + $app.methods.bulkUnfavoriteSelection = function (elementsTicked) { + for (var id of elementsTicked) { + API.deleteFavorite({ + objectId: id }); } - await this.loadMoreGroupMembers(); + this.bulkUnfavoriteMode = false; }; - $app.methods.loadMoreGroupMembers = async function () { - if (this.isGroupMembersDone || this.isGroupMembersLoading) { + // #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 D = this.groupDialog; - var params = this.loadMoreGroupMembersParams; - D.memberSearch = ''; - this.isGroupMembersLoading = true; - await API.getGroupMembers(params) - .finally(() => { - this.isGroupMembersLoading = false; - }) - .then((args) => { - for (var i = 0; i < args.json.length; i++) { - var member = args.json[i]; - if (member.userId === API.currentUser.id) { - if ( - D.members.length > 0 && - D.members[0].userId === API.currentUser.id - ) { - // remove duplicate and keep sort order - D.members.splice(0, 1); - } - break; - } - } - if (args.json.length < params.n) { - this.isGroupMembersDone = true; + 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; } - D.members = [...D.members, ...args.json]; - this.setGroupMemberModerationTable(D.members); - params.offset += params.n; - return args; - }) - .catch((err) => { - this.isGroupMembersDone = true; - throw err; - }); + } + } + 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.loadAllGroupMembers = async function () { - if (this.isGroupMembersLoading) { - return; + $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); + } } - await this.getGroupDialogGroupMembers(); - while (this.groupDialog.visible && !this.isGroupMembersDone) { - this.isGroupMembersLoading = true; - await new Promise((resolve) => { - workerTimers.setTimeout(resolve, 1000); - }); - this.isGroupMembersLoading = false; - await this.loadMoreGroupMembers(); + 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); } - }; - - $app.methods.setGroupMemberSortOrder = async function (sortOrder) { - var D = this.groupDialog; - if (D.memberSortOrder === sortOrder) { - return; + if (this.localWorldFavoriteGroups.length === 0) { + // default group + this.localWorldFavorites.Favorites = []; + this.localWorldFavoriteGroups.push('Favorites'); } - D.memberSortOrder = sortOrder; - await this.getGroupDialogGroupMembers(); + this.sortLocalWorldFavorites(); }; - $app.methods.setGroupMemberFilter = async function (filter) { - var D = this.groupDialog; - if (D.memberFilter === filter) { - return; + $app.methods.hasLocalWorldFavorite = function (worldId, group) { + var favoriteGroup = this.localWorldFavorites[group]; + if (!favoriteGroup) { + return false; } - D.memberFilter = filter; - await this.getGroupDialogGroupMembers(); - }; - - $app.methods.getCurrentUserRepresentedGroup = function () { - return API.getRepresentedGroup({ - userId: API.currentUser.id - }).then((args) => { - this.userDialog.representedGroup = args.json; - return args; - }); - }; - - // group permissions - - $app.methods.hasGroupPermission = function (ref, permission) { - if ( - ref && - ref.myMember && - ref.myMember.permissions && - (ref.myMember.permissions.includes('*') || - ref.myMember.permissions.includes(permission)) - ) { - return true; + for (var i = 0; i < favoriteGroup.length; ++i) { + if (favoriteGroup[i].id === worldId) { + return true; + } } return false; }; - // group gallery - - $app.data.isGroupGalleryLoading = false; + $app.methods.getLocalWorldFavoriteGroupLength = function (group) { + var favoriteGroup = this.localWorldFavorites[group]; + if (!favoriteGroup) { + return 0; + } + return favoriteGroup.length; + }; - /** - * @param {{ - groupId: string, - galleryId: string, - n: number, - offset: number - }} params - * @return { Promise<{json: any, params}> } - */ - API.getGroupGallery = function (params) { - return this.call( - `groups/${params.groupId}/galleries/${params.galleryId}`, + $app.methods.promptNewLocalWorldFavoriteGroup = function () { + this.$prompt( + $t('prompt.new_local_favorite_group.description'), + $t('prompt.new_local_favorite_group.header'), { - method: 'GET', - params: { - n: params.n, - offset: params.offset + 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.newLocalWorldFavoriteGroup(instance.inputValue); + } } } - ).then((json) => { - var args = { - json, - params - }; - this.$emit('GROUP:GALLERY', args); - return args; - }); + ); }; - API.$on('GROUP:GALLERY', function (args) { - for (var json of args.json) { - if ($app.groupDialog.id === json.groupId) { - if (!$app.groupDialog.galleries[json.galleryId]) { - $app.groupDialog.galleries[json.galleryId] = []; - } - $app.groupDialog.galleries[json.galleryId].push(json); - } + $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; } - }); - - $app.methods.getGroupGalleries = async function () { - this.groupDialog.galleries = {}; - this.$refs.groupDialogGallery.currentName = '0'; // select first tab - this.isGroupGalleryLoading = true; - for (var i = 0; i < this.groupDialog.ref.galleries.length; i++) { - var gallery = this.groupDialog.ref.galleries[i]; - await this.getGroupGallery(this.groupDialog.id, gallery.id); + if (!this.localWorldFavorites[group]) { + this.localWorldFavorites[group] = []; + } + if (!this.localWorldFavoriteGroups.includes(group)) { + this.localWorldFavoriteGroups.push(group); } - this.isGroupGalleryLoading = false; + this.sortLocalWorldFavorites(); }; - $app.methods.getGroupGallery = async function (groupId, galleryId) { - try { - var params = { - groupId, - galleryId, - n: 100, - offset: 0 - }; - var count = 50; // 5000 max - for (var i = 0; i < count; i++) { - var args = await API.getGroupGallery(params); - params.offset += 100; - if (args.json.length < 100) { - break; + $app.methods.promptLocalWorldFavoriteGroupRename = 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.renameLocalWorldFavoriteGroup( + instance.inputValue, + group + ); + } } } - } catch (err) { - console.error(err); - } - }; - - $app.methods.groupGalleryStatus = function (gallery) { - var style = {}; - if (!gallery.membersOnly) { - style.joinme = true; - } else if (!gallery.roleIdsToView) { - style.online = true; - } else { - style.busy = true; - } - return style; + ); }; - // group invite users - - $app.data.inviteGroupDialog = { - visible: false, - loading: false, - groupId: '', - groupName: '', - userId: '', - userIds: [], - userObject: {} - }; - - $app.methods.showInviteGroupDialog = function (groupId, userId) { - this.$nextTick(() => adjustDialogZ(this.$refs.inviteGroupDialog.$el)); - var D = this.inviteGroupDialog; - D.userIds = ''; - D.groups = []; - D.groupId = groupId; - D.groupName = groupId; - D.userId = userId; - D.userObject = {}; - D.visible = true; - if (groupId) { - API.getCachedGroup({ - groupId - }) - .then((args) => { - D.groupName = args.ref.name; - }) - .catch(() => { - D.groupId = ''; - }); - this.isAllowedToInviteToGroup(); - } - - if (userId) { - API.getCachedUser({ userId }).then((args) => { - D.userObject = args.ref; + $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' }); - D.userIds = [userId]; + return; } - }; + this.localWorldFavoriteGroups.push(newName); + this.localWorldFavorites[newName] = this.localWorldFavorites[group]; - API.$on('LOGOUT', function () { - $app.inviteGroupDialog.visible = false; - }); + $app.removeFromArray(this.localWorldFavoriteGroups, group); + delete this.localWorldFavorites[group]; + database.renameWorldFavoriteGroup(newName, group); + this.sortLocalWorldFavorites(); + }; - $app.methods.sendGroupInvite = function () { - this.$confirm('Continue? Invite User(s) To Group', 'Confirm', { + $app.methods.promptLocalWorldFavoriteGroupDelete = function (group) { + this.$confirm(`Delete Group? ${group}`, 'Confirm', { confirmButtonText: 'Confirm', cancelButtonText: 'Cancel', type: 'info', callback: (action) => { - var D = this.inviteGroupDialog; - if (action !== 'confirm' || D.loading === true) { - return; + if (action === 'confirm') { + this.deleteLocalWorldFavoriteGroup(group); } - D.loading = true; - var inviteLoop = () => { - if (D.userIds.length === 0) { - D.loading = false; - return; - } - var receiverUserId = D.userIds.shift(); - API.sendGroupInvite({ - groupId: D.groupId, - userId: receiverUserId - }) - .then(inviteLoop) - .catch(() => { - D.loading = false; - }); - }; - inviteLoop(); } }); }; - $app.methods.isAllowedToInviteToGroup = function () { - var D = this.inviteGroupDialog; - var groupId = D.groupId; - if (!groupId) { - return; - } - D.loading = true; - API.getGroup({ groupId }) - .then((args) => { - if (this.hasGroupPermission(args.ref, 'group-invites-manage')) { - return args; - } - // not allowed to invite - D.groupId = ''; - this.$message({ - type: 'error', - message: 'You are not allowed to invite to this group' - }); - return args; - }) - .finally(() => { - D.loading = false; - }); - }; - - $app.methods.openNotificationLink = function (link) { - if (!link) { - return; - } - var data = link.split(':'); - if (!data.length) { - return; - } - switch (data[0]) { - case 'group': - this.showGroupDialog(data[1]); - break; - case 'user': - this.showUserDialog(data[1]); - break; + $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(compareByName); + } + } } }; - $app.methods.checkVRChatDebugLogging = async function () { - if (this.gameLogDisabled) { - return; + $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); } - try { - var loggingEnabled = - await AppApi.GetVRChatRegistryKey('LOGGING_ENABLED'); - if (loggingEnabled === null) { - // key not found - return; - } - if (loggingEnabled === 1) { - // already enabled - return; + + $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; } - 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; + 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; + } } - 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); } + + worldIdRemoveList.forEach((id) => { + $app.removeFromArray(this.localWorldFavoritesList, id); + database.removeWorldFromCache(id); + }); }; - $app.methods.downloadAndSaveImage = async function (url) { - if (!url) { + 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(); + }); + + $app.data.worldFavoriteSearch = ''; + $app.data.worldFavoriteSearchResults = []; + + $app.methods.searchWorldFavorites = function () { + var search = this.worldFavoriteSearch.toLowerCase(); + if (search.length < 3) { + this.worldFavoriteSearchResults = []; return; } - this.$message({ - message: 'Downloading image...', - type: 'info' - }); - try { - var response = await webApiService.execute({ - url, - method: 'GET' - }); - if ( - response.status !== 200 || - !response.data.startsWith('data:image/png') - ) { - throw new Error(`Error: ${response.data}`); - } - var link = document.createElement('a'); - link.href = response.data; - var fileName = `${extractFileId(url)}.png`; - if (!fileName) { - fileName = `${url.split('/').pop()}.png`; + + var results = []; + for (var i = 0; i < this.localWorldFavoriteGroups.length; ++i) { + var group = this.localWorldFavoriteGroups[i]; + if (!this.localWorldFavorites[group]) { + continue; } - if (!fileName) { - fileName = 'image.png'; + for (var j = 0; j < this.localWorldFavorites[group].length; ++j) { + var ref = this.localWorldFavorites[group][j]; + if (!ref || !ref.id) { + continue; + } + if ( + ref.name.toLowerCase().includes(search) || + ref.authorName.toLowerCase().includes(search) + ) { + results.push(ref); + } } - link.setAttribute('download', fileName); - document.body.appendChild(link); - link.click(); - document.body.removeChild(link); - } catch { - new Noty({ - type: 'error', - text: escapeTag(`Failed to download image. ${url}`) - }).show(); } - }; - $app.methods.downloadAndSaveJson = function (fileName, data) { - if (!fileName || !data) { - return; - } - try { - var link = document.createElement('a'); - link.setAttribute( - 'href', - `data:application/json;charset=utf-8,${encodeURIComponent( - JSON.stringify(data, null, 2) - )}` - ); - link.setAttribute('download', `${fileName}.json`); - document.body.appendChild(link); - link.click(); - document.body.removeChild(link); - } catch { - new Noty({ - type: 'error', - text: escapeTag('Failed to download JSON.') - }).show(); + for (var i = 0; i < this.favoriteWorlds.length; ++i) { + var ref = this.favoriteWorlds[i].ref; + if (!ref) { + continue; + } + if ( + ref.name.toLowerCase().includes(search) || + ref.authorName.toLowerCase().includes(search) + ) { + results.push(ref); + } } - }; - $app.methods.setPlayerModeration = function (userId, type) { - var D = this.userDialog; - AppApi.SetVRChatUserModeration(API.currentUser.id, userId, type).then( - (result) => { - if (result) { - if (type === 4) { - D.isShowAvatar = false; - D.isHideAvatar = true; - } else if (type === 5) { - D.isShowAvatar = true; - D.isHideAvatar = false; - } else { - D.isShowAvatar = false; - D.isHideAvatar = false; - } - } else { - $app.$message({ - message: 'Failed to change avatar moderation', - type: 'error' - }); - } - } - ); + this.worldFavoriteSearchResults = results; }; // #endregion - // #region | App: Language - - $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' - } - }; + // #region | App: Local Avatar Favorites - this.userDialogWorldOrderOptions = { - descending: { - name: $t('dialog.user.worlds.order.descending'), - value: 'descending' - }, - ascending: { - name: $t('dialog.user.worlds.order.ascending'), - value: 'ascending' - } - }; + $app.methods.isLocalUserVrcplusSupporter = function () { + return API.currentUser.$isVRCPlus; }; - $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: '' - } - }; + $app.data.localAvatarFavoriteGroups = []; + $app.data.localAvatarFavoritesList = []; + $app.data.localAvatarFavorites = {}; - 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.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.applyLanguageStrings = function () { - // repply sorting strings - this.applyUserDialogSortingStrings(); - this.applyGroupDialogSortingStrings(); - this.userDialog.worldSorting = - this.userDialogWorldSortingOptions.updated; - this.userDialog.worldOrder = - this.userDialogWorldOrderOptions.descending; - this.groupDialog.memberFilter = this.groupDialogFilterOptions.everyone; - this.groupDialog.memberSortOrder = - this.groupDialogSortingOptions.joinedAtDesc; - }; - // $app.methods.applyLanguageStrings(); + $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); + } + } - $app.data.appLanguage = - (await configRepository.getString('VRCX_appLanguage')) ?? 'en'; - 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; + // 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; } - var lang = result.split('-')[0]; - i18n.availableLocales.forEach((ref) => { - var refLang = ref.split('_')[0]; - if (refLang === lang) { - this.changeAppLanguage(ref); + 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); } - $app.applyLanguageStrings(); - }; - - $app.methods.changeAppLanguage = function (language) { - console.log('Language changed:', language); - this.appLanguage = language; - i18n.locale = language; - configRepository.setString('VRCX_appLanguage', language); - this.applyLanguageStrings(); - this.updateVRConfigVars(); - this._stringComparer = undefined; + // update UI + this.sortLocalAvatarFavorites(); }; - // #endregion - // #region | App: Random unsorted app methods, data structs, API functions, and an API feedback/file analysis event - API.$on('USER:FEEDBACK', function (args) { - if (args.params.userId === this.currentUser.id) { - $app.currentUserFeedbackData = buildTreeData(args.json); + API.$on('AVATAR', function (args) { + if ($app.localAvatarFavoritesList.includes(args.ref.id)) { + // update db cache + database.addAvatarToCache(args.ref); } }); - $app.methods.getCurrentUserFeedback = function () { - return API.getUserFeedback({ userId: API.currentUser.id }); - }; + API.$on('LOGIN', function () { + $app.getLocalAvatarFavorites(); + }); - $app.methods.gameLogIsFriend = function (row) { - if (typeof row.isFriend !== 'undefined') { - return row.isFriend; + $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); + } } - if (!row.userId) { - return false; + 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'); } - row.isFriend = this.friends.has(row.userId); - return row.isFriend; + this.sortLocalAvatarFavorites(); }; - $app.methods.gameLogIsFavorite = function (row) { - if (typeof row.isFavorite !== 'undefined') { - return row.isFavorite; - } - if (!row.userId) { + $app.methods.hasLocalAvatarFavorite = function (avatarId, group) { + var favoriteGroup = this.localAvatarFavorites[group]; + if (!favoriteGroup) { return false; } - row.isFavorite = this.localFavoriteFriends.has(row.userId); - return row.isFavorite; - }; - - $app.data.changeLogDialog = { - visible: false, - buildName: '', - changeLog: '' + for (var i = 0; i < favoriteGroup.length; ++i) { + if (favoriteGroup[i].id === avatarId) { + return true; + } + } + return false; }; - $app.methods.showChangeLogDialog = function () { - this.$nextTick(() => adjustDialogZ(this.$refs.changeLogDialog.$el)); - this.changeLogDialog.visible = true; - this.checkForVRCXUpdate(); + $app.methods.getLocalAvatarFavoriteGroupLength = function (group) { + var favoriteGroup = this.localAvatarFavorites[group]; + if (!favoriteGroup) { + return 0; + } + return favoriteGroup.length; }; - $app.data.gallerySelectDialog = { - visible: false, - selectedFileId: '', - selectedImageUrl: '' + $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.showGallerySelectDialog = function () { - this.$nextTick(() => adjustDialogZ(this.$refs.gallerySelectDialog.$el)); - var D = this.gallerySelectDialog; - D.visible = true; - this.refreshGalleryTable(); + $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.selectImageGallerySelect = function (imageUrl, fileId) { - var D = this.gallerySelectDialog; - D.selectedFileId = fileId; - D.selectedImageUrl = imageUrl; - D.visible = false; - console.log(imageUrl, fileId); + $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.clearImageGallerySelect = function () { - var D = this.gallerySelectDialog; - D.selectedFileId = ''; - D.selectedImageUrl = ''; - }; + $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.methods.reportUserForHacking = function (userId) { - API.reportUser({ - userId, - contentType: 'user', - reason: 'behavior-hacking', - type: 'report' - }); + $app.removeFromArray(this.localAvatarFavoriteGroups, group); + delete this.localAvatarFavorites[group]; + database.renameAvatarFavoriteGroup(newName, group); + this.sortLocalAvatarFavorites(); }; - /** - * @param {{ - userId: string, - contentType: string, - reason: string, - type: string - }} params - * @return { Promise<{json: any, params}> } - */ - API.reportUser = function (params) { - return this.call(`feedback/${params.userId}/user`, { - method: 'POST', - params: { - contentType: params.contentType, - reason: params.reason, - type: params.type + $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); + } } - }).then((json) => { - var args = { - json, - params - }; - this.$emit('FEEDBACK:REPORT:USER', args); - return args; }); }; - $app.methods.changeLogRemoveLinks = function (text) { - return text.replace(/([^!])\[[^\]]+\]\([^)]+\)/g, '$1'); - }; - - /** - * @param {{ - fileId: string, - version: number - }} params - * @return { Promise<{json: any, params}> } - - */ - API.getFileAnalysis = function (params) { - return this.call(`analysis/${params.fileId}/${params.version}`, { - method: 'GET' - }).then((json) => { - var args = { - json, - params - }; - this.$emit('FILE:ANALYSIS', args); - return args; - }); + $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(compareByName); + } + } + } }; - API.$on('FILE:ANALYSIS', function (args) { - if (!$app.avatarDialog.visible) { - return; - } - var ref = args.json; - if (typeof ref.fileSize !== 'undefined') { - ref._fileSize = `${(ref.fileSize / 1048576).toFixed(2)} MB`; - } - if (typeof ref.uncompressedSize !== 'undefined') { - ref._uncompressedSize = `${(ref.uncompressedSize / 1048576).toFixed( - 2 - )} MB`; - } - if (typeof ref.avatarStats?.totalTextureUsage !== 'undefined') { - ref._totalTextureUsage = `${( - ref.avatarStats.totalTextureUsage / 1048576 - ).toFixed(2)} MB`; + $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.avatarDialog.fileAnalysis = buildTreeData(args.json); - }); - $app.methods.getAvatarFileAnalysis = function () { - var D = this.avatarDialog; - var assetUrl = ''; - for (let i = D.ref.unityPackages.length - 1; i > -1; i--) { - var unityPackage = D.ref.unityPackages[i]; - if ( - unityPackage.variant && - unityPackage.variant !== 'standard' && - unityPackage.variant !== 'security' - ) { + $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; } - if ( - unityPackage.platform === 'standalonewindows' && - this.compareUnityVersion(unityPackage.unitySortNumber) + for ( + var j = 0; + j < this.localAvatarFavorites[groupName].length; + ++j ) { - assetUrl = unityPackage.assetUrl; - break; + var avatarId = this.localAvatarFavorites[groupName][j].id; + if (avatarIdRemoveList.has(avatarId)) { + avatarIdRemoveList.delete(avatarId); + break; + } } } - if (!assetUrl) { - assetUrl = D.ref.assetUrl; - } - var fileId = extractFileId(assetUrl); - var version = parseInt(extractFileVersion(assetUrl), 10); - if (!fileId || !version) { - this.$message({ - message: 'File Analysis unavailable', - type: 'error' - }); - return; - } - API.getFileAnalysis({ fileId, version }); - }; - $app.methods.openFolderGeneric = function (path) { - AppApi.OpenFolderAndSelectItem(path, true); + 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 | Dialog: fullscreen image - - $app.data.fullscreenImageDialog = { - visible: false, - imageUrl: '' - }; + $app.data.avatarFavoriteSearch = ''; + $app.data.avatarFavoriteSearchResults = []; - $app.methods.showFullscreenImageDialog = function (imageUrl) { - if (!imageUrl) { + $app.methods.searchAvatarFavorites = function () { + var search = this.avatarFavoriteSearch.toLowerCase(); + if (search.length < 3) { + this.avatarFavoriteSearchResults = []; return; } - this.$nextTick(() => - adjustDialogZ(this.$refs.fullscreenImageDialog.$el) - ); - var D = this.fullscreenImageDialog; - D.imageUrl = imageUrl; - 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' - }); + var results = []; + for (var i = 0; i < this.localAvatarFavoriteGroups.length; ++i) { + var group = this.localAvatarFavoriteGroups[i]; + if (!this.localAvatarFavorites[group]) { + continue; } - }); - }; - - $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' - }); + for (var j = 0; j < this.localAvatarFavorites[group].length; ++j) { + var ref = this.localAvatarFavorites[group][j]; + if (!ref || !ref.id) { + continue; + } + if ( + ref.name.toLowerCase().includes(search) || + ref.authorName.toLowerCase().includes(search) + ) { + results.push(ref); + } } - }); - }; + } - $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' - }); + for (var i = 0; i < this.favoriteAvatars.length; ++i) { + var ref = this.favoriteAvatars[i].ref; + if (!ref) { + continue; } - }); + if ( + ref.name.toLowerCase().includes(search) || + ref.authorName.toLowerCase().includes(search) + ) { + results.push(ref); + } + } + + this.avatarFavoriteSearchResults = results; }; - $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' - }); + // #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 (var ref of API.cachedFavorites.values()) { + if ( + !ref.$isDeleted && + ref.type === 'friend' && + (this.localFavoriteFriendsGroups.length === 0 || + this.localFavoriteFriendsGroups.includes(ref.$groupKey)) + ) { + this.localFavoriteFriends.add(ref.favoriteId); } - }); + } + this.updateSidebarFriendsList(); + + configRepository.setString( + 'VRCX_localFavoriteFriendsGroups', + JSON.stringify(this.localFavoriteFriendsGroups) + ); }; - $app.methods.openCrashVrcCrashDumps = function () { - AppApi.OpenCrashVrcCrashDumps().then((result) => { - if (result) { - this.$message({ - message: 'Folder opened', - type: 'success' - }); + $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 { - this.$message({ - message: "Folder dosn't exist", - type: 'error' - }); + $app.removeFromArray(this.vipFriends_, ctx); + this.onlineFriends_.push(ctx); + this.sortOnlineFriends = true; } - }); + } }; // #endregion - // #region | Dialog: registry backup dialog - - $app.data.registryBackupDialog = { - visible: false + // #region | App: ChatBox Blacklist + $app.data.chatboxBlacklist = [ + 'NP: ', + 'Now Playing', + 'Now playing', + "▶️ '", + '( ▶️ ', + "' - '", + "' by '", + '[Spotify] ' + ]; + if (await configRepository.getString('VRCX_chatboxBlacklist')) { + $app.data.chatboxBlacklist = JSON.parse( + await configRepository.getString('VRCX_chatboxBlacklist') + ); + } + $app.data.chatboxBlacklistDialog = { + visible: false, + loading: false }; - $app.data.registryBackupTable = { - data: [], - tableProps: { - stripe: true, - size: 'mini', - defaultSort: { - prop: 'date', - order: 'descending' - } - }, - layout: 'table' - }; + API.$on('LOGOUT', function () { + $app.chatboxBlacklistDialog.visible = false; + }); - $app.methods.showRegistryBackupDialog = function () { - this.$nextTick(() => - adjustDialogZ(this.$refs.registryBackupDialog.$el) + $app.methods.saveChatboxBlacklist = async function () { + await configRepository.setString( + 'VRCX_chatboxBlacklist', + JSON.stringify(this.chatboxBlacklist) ); - var D = this.registryBackupDialog; - D.visible = true; - this.updateRegistryBackupDialog(); }; - $app.methods.updateRegistryBackupDialog = async function () { - var D = this.registryBackupDialog; - this.registryBackupTable.data = []; - if (!D.visible) { - return; - } - var backupsJson = await configRepository.getString( - 'VRCX_VRChatRegistryBackups' + $app.methods.showChatboxBlacklistDialog = function () { + this.$nextTick(() => + $app.adjustDialogZ(this.$refs.chatboxBlacklistDialog.$el) ); - if (!backupsJson) { - backupsJson = JSON.stringify([]); - } - this.registryBackupTable.data = JSON.parse(backupsJson); + var D = this.chatboxBlacklistDialog; + D.visible = true; }; - $app.methods.promptVrcRegistryBackupName = async function () { - var name = await this.$prompt( - 'Enter a name for the backup', - 'Backup Name', - { - confirmButtonText: 'Confirm', - cancelButtonText: 'Cancel', - inputPattern: /\S+/, - inputErrorMessage: 'Name is required', - inputValue: 'Backup' + $app.methods.checkChatboxBlacklist = function (msg) { + for (var i = 0; i < this.chatboxBlacklist.length; ++i) { + if (msg.includes(this.chatboxBlacklist[i])) { + return true; } - ); - if (name.action === 'confirm') { - this.backupVrcRegistry(name.value); } + return false; }; - $app.methods.backupVrcRegistry = async function (name) { - var regJson = await AppApi.GetVRChatRegistry(); - var newBackup = { - name, - date: new Date().toJSON(), - data: regJson - }; - var backupsJson = await configRepository.getString( - 'VRCX_VRChatRegistryBackups' - ); - if (!backupsJson) { - backupsJson = JSON.stringify([]); - } - var backups = JSON.parse(backupsJson); - backups.push(newBackup); - await configRepository.setString( - 'VRCX_VRChatRegistryBackups', - JSON.stringify(backups) + // #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' + ) + ) + ) ); - await this.updateRegistryBackupDialog(); - }; + } - $app.methods.deleteVrcRegistryBackup = async function (row) { - var backups = this.registryBackupTable.data; - removeFromArray(backups, row); + $app.methods.saveChatboxUserBlacklist = async function () { await configRepository.setString( - 'VRCX_VRChatRegistryBackups', - JSON.stringify(backups) + 'VRCX_chatboxUserBlacklist', + JSON.stringify(Object.fromEntries(this.chatboxUserBlacklist)) ); - await this.updateRegistryBackupDialog(); }; - $app.methods.restoreVrcRegistryBackup = function (row) { - this.$confirm('Continue? Restore Backup', 'Confirm', { - confirmButtonText: 'Confirm', - cancelButtonText: 'Cancel', - type: 'warning', - callback: (action) => { - if (action !== 'confirm') { - return; - } - var data = JSON.stringify(row.data); - AppApi.SetVRChatRegistry(data) - .then(() => { - this.$message({ - message: 'VRC registry settings restored', - type: 'success' - }); - }) - .catch((e) => { - console.error(e); - this.$message({ - message: `Failed to restore VRC registry settings, check console for full error: ${e}`, - type: 'error' - }); - }); - } - }); + $app.methods.addChatboxUserBlacklist = async function (user) { + this.chatboxUserBlacklist.set(user.id, user.displayName); + await this.saveChatboxUserBlacklist(); + this.getCurrentInstanceUserList(); }; - $app.methods.saveVrcRegistryBackupToFile = function (row) { - this.downloadAndSaveJson(row.name, row.data); + $app.methods.deleteChatboxUserBlacklist = async function (userId) { + this.chatboxUserBlacklist.delete(userId); + await this.saveChatboxUserBlacklist(); + this.getCurrentInstanceUserList(); + this.$nextTick(() => + $app.adjustDialogZ(this.$refs.chatboxBlacklistDialog.$el) + ); }; - $app.methods.restoreVrcRegistryFromFile = function (json) { - try { - var data = JSON.parse(json); - if (!data || typeof data !== 'object') { - throw new Error('Invalid JSON'); - } - // quick check to make sure it's a valid registry backup - for (var key in data) { - var value = data[key]; - if ( - typeof value !== 'object' || - typeof value.type !== 'number' || - typeof value.data === 'undefined' - ) { - throw new Error('Invalid JSON'); - } - } - AppApi.SetVRChatRegistry(json) - .then(() => { - this.$message({ - message: 'VRC registry settings restored', - type: 'success' - }); - }) - .catch((e) => { - console.error(e); - this.$message({ - message: `Failed to restore VRC registry settings, check console for full error: ${e}`, - type: 'error' - }); - }); - } catch { + // #endregion + // #region | App: Instance queuing + + API.queuedInstances = new Map(); + + $app.methods.removeAllQueuedInstances = function () { + API.queuedInstances.forEach((ref) => { this.$message({ - message: 'Invalid JSON', - type: 'error' + 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); } }; - $app.methods.deleteVrcRegistry = function () { - this.$confirm('Continue? Delete VRC Registry Settings', 'Confirm', { - confirmButtonText: 'Confirm', - cancelButtonText: 'Cancel', - type: 'warning', - callback: (action) => { - if (action !== 'confirm') { - return; - } - AppApi.DeleteVRChatRegistryFolder().then(() => { - this.$message({ - message: 'VRC registry settings deleted', - type: 'success' - }); + API.applyQueuedInstance = function (instanceId) { + API.queuedInstances.forEach((ref) => { + if (ref.location !== instanceId) { + $app.$message({ + message: `Removed instance ${ref.$worldName} from queue`, + type: 'info' }); + ref.$msgBox?.close(); + API.queuedInstances.delete(ref.location); } }); - }; - - $app.methods.clearVrcRegistryDialog = function () { - this.registryBackupTable.data = []; - }; - - $app.methods.checkAutoBackupRestoreVrcRegistry = async function () { - if (!this.vrcRegistryAutoBackup) { + if (!instanceId) { return; } - - // check for auto restore - var hasVRChatRegistryFolder = await AppApi.HasVRChatRegistryFolder(); - if (!hasVRChatRegistryFolder) { - var lastBackupDate = await configRepository.getString( - 'VRCX_VRChatRegistryLastBackupDate' - ); - var lastRestoreCheck = await configRepository.getString( - 'VRCX_VRChatRegistryLastRestoreCheck' - ); - if ( - !lastBackupDate || - (lastRestoreCheck && - lastBackupDate && - lastRestoreCheck === lastBackupDate) - ) { - // only ask to restore once and when backup is present - return; + if (!API.queuedInstances.has(instanceId)) { + var L = $utils.parseLocation(instanceId); + if (L.worldId && L.instanceId) { + API.getInstance({ + worldId: L.worldId, + instanceId: L.instanceId + }).then((args) => { + if (args.json?.queueSize) { + $app.instanceQueueUpdate( + instanceId, + args.json?.queueSize, + args.json?.queueSize + ); + } + }); } - // popup message about auto restore - this.$alert( - $t('dialog.registry_backup.restore_prompt'), - $t('dialog.registry_backup.header') - ); - this.showRegistryBackupDialog(); - await AppApi.FocusWindow(); - await configRepository.setString( - 'VRCX_VRChatRegistryLastRestoreCheck', - lastBackupDate - ); - } else { - await this.autoBackupVrcRegistry(); + $app.instanceQueueUpdate(instanceId, 0, 0); } }; - $app.methods.autoBackupVrcRegistry = async function () { - var date = new Date(); - var lastBackupDate = await configRepository.getString( - 'VRCX_VRChatRegistryLastBackupDate' - ); - if (lastBackupDate) { - var lastBackup = new Date(lastBackupDate); - var diff = date.getTime() - lastBackup.getTime(); - var diffDays = Math.floor(diff / (1000 * 60 * 60 * 24)); - if (diffDays < 7) { - return; - } + $app.methods.instanceQueueReady = function (instanceId) { + var ref = API.queuedInstances.get(instanceId); + if (typeof ref !== 'undefined') { + ref.$msgBox.close(); + API.queuedInstances.delete(instanceId); } - var backupsJson = await configRepository.getString( - 'VRCX_VRChatRegistryBackups' + var L = $utils.parseLocation(instanceId); + var group = API.cachedGroups.get(L.groupId); + var groupName = group?.name ?? ''; + var worldName = ref?.$worldName ?? ''; + var displayLocation = $app.displayLocation( + instanceId, + worldName, + groupName ); - if (!backupsJson) { - backupsJson = JSON.stringify([]); - } - var backups = JSON.parse(backupsJson); - backups.forEach((backup) => { - if (backup.name === 'Auto Backup') { - // remove old auto backup - removeFromArray(backups, backup); - } + this.$message({ + message: `Instance ready to join ${displayLocation}`, + type: 'success' }); - await configRepository.setString( - 'VRCX_VRChatRegistryBackups', - JSON.stringify(backups) - ); - this.backupVrcRegistry('Auto Backup'); - await configRepository.setString( - 'VRCX_VRChatRegistryLastBackupDate', - date.toJSON() - ); - }; - - // #endregion - // #region | Dialog: group member moderation - - $app.data.groupMemberModeration = { - visible: false, - loading: false, - id: '', - groupRef: {}, - auditLogTypes: [], - selectedAuditLogTypes: [], - note: '', - selectedUsers: new Map(), - selectedUsersArray: [], - selectedRoles: [], - progressCurrent: 0, - progressTotal: 0, - selectUserId: '' + var noty = { + created_at: new Date().toJSON(), + type: 'group.queueReady', + imageUrl: group?.iconUrl, + message: `Instance ready to join ${displayLocation}`, + 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.data.groupMemberModerationTable = { - data: [], - tableProps: { - stripe: true, - size: 'mini' - }, - pageSize: $app.data.tablePageSize, - paginationProps: { - small: true, - layout: 'sizes,prev,pager,next,total', - pageSizes: [10, 15, 25, 50, 100] + $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); + } + var displayLocation = this.displayLocation( + instanceId, + ref.$worldName, + ref.$groupName + ); + ref.$msgBox.message = `You are in position ${ref.position} of ${ref.queueSize} in the queue for ${displayLocation} `; + API.queuedInstances.set(instanceId, ref); + // workerTimers.setTimeout(this.instanceQueueTimeout, 3600000); }; - $app.data.groupBansModerationTable = { - data: [], - filters: [ - { - prop: ['$displayName'], - value: '' - } - ], - tableProps: { - stripe: true, - size: 'mini' - }, - pageSize: $app.data.tablePageSize, - paginationProps: { - small: true, - layout: 'sizes,prev,pager,next,total', - pageSizes: [10, 15, 25, 50, 100] - } + $app.methods.instanceQueueClear = function () { + // remove all instances from queue + API.queuedInstances.forEach((ref) => { + ref.$msgBox.close(); + API.queuedInstances.delete(ref.location); + }); }; - $app.data.groupLogsModerationTable = { - data: [], - filters: [ - { - prop: ['description'], - value: '' + // #endregion + + $app.methods.sendNotificationResponse = function ( + notificationId, + responses, + responseType + ) { + if (!Array.isArray(responses) || responses.length === 0) { + return null; + } + var responseData = ''; + for (var i = 0; i < responses.length; i++) { + if (responses[i].type === responseType) { + responseData = responses[i].data; + break; } - ], - tableProps: { - stripe: true, - size: 'mini' - }, - pageSize: $app.data.tablePageSize, - paginationProps: { - small: true, - layout: 'sizes,prev,pager,next,total', - pageSizes: [10, 15, 25, 50, 100] } + return API.sendNotificationResponse({ + notificationId, + responseType, + responseData + }); }; - $app.data.groupInvitesModerationTable = { - data: [], - tableProps: { - stripe: true, - size: 'mini' - }, - pageSize: $app.data.tablePageSize, - paginationProps: { - small: true, - layout: 'sizes,prev,pager,next,total', - pageSizes: [10, 15, 25, 50, 100] + $app.methods.openNotificationLink = function (link) { + if (!link) { + return; } - }; - - $app.data.groupJoinRequestsModerationTable = { - data: [], - tableProps: { - stripe: true, - size: 'mini' - }, - pageSize: $app.data.tablePageSize, - paginationProps: { - small: true, - layout: 'sizes,prev,pager,next,total', - pageSizes: [10, 15, 25, 50, 100] + var data = link.split(':'); + if (!data.length) { + return; } - }; - - $app.data.groupBlockedModerationTable = { - data: [], - tableProps: { - stripe: true, - size: 'mini' - }, - pageSize: $app.data.tablePageSize, - paginationProps: { - small: true, - layout: 'sizes,prev,pager,next,total', - pageSizes: [10, 15, 25, 50, 100] + switch (data[0]) { + case 'group': + this.showGroupDialog(data[1]); + break; + case 'user': + this.showUserDialog(data[1]); + break; } }; - $app.data.groupMemberModerationTableForceUpdate = 0; - - $app.methods.setGroupMemberModerationTable = function (data) { - if (!this.groupMemberModeration.visible) { + $app.methods.checkVRChatDebugLogging = async function () { + if (this.gameLogDisabled) { return; } - for (var i = 0; i < data.length; i++) { - var member = data[i]; - member.$selected = this.groupMemberModeration.selectedUsers.has( - member.userId + try { + var loggingEnabled = + await AppApi.GetVRChatRegistryKey('LOGGING_ENABLED'); + if (loggingEnabled === null) { + // key not found + return; + } + if (loggingEnabled === 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); } - this.groupMemberModerationTable.data = data; - // force redraw - this.groupMemberModerationTableForceUpdate++; }; - $app.methods.showGroupMemberModerationDialog = function (groupId) { - this.$nextTick(() => - adjustDialogZ(this.$refs.groupMemberModeration.$el) - ); - if (groupId !== this.groupDialog.id) { + $app.methods.downloadAndSaveImage = async function (url) { + if (!url) { return; } - var D = this.groupMemberModeration; - D.id = groupId; - D.selectedUsers.clear(); - D.selectedUsersArray = []; - D.selectedRoles = []; - D.groupRef = {}; - D.auditLogTypes = []; - D.selectedAuditLogTypes = []; - API.getCachedGroup({ groupId }).then((args) => { - D.groupRef = args.ref; - if (this.hasGroupPermission(D.groupRef, 'group-audit-view')) { - API.getGroupAuditLogTypes({ groupId }); - } + this.$message({ + message: 'Downloading image...', + type: 'info' }); - this.groupMemberModerationTableForceUpdate = 0; - D.visible = true; - this.setGroupMemberModerationTable(this.groupDialog.members); - }; - - $app.methods.groupMemberModerationTableSelectionChange = function (row) { - var D = this.groupMemberModeration; - if (row.$selected && !D.selectedUsers.has(row.userId)) { - D.selectedUsers.set(row.userId, row); - } else if (!row.$selected && D.selectedUsers.has(row.userId)) { - D.selectedUsers.delete(row.userId); - } - D.selectedUsersArray = Array.from(D.selectedUsers.values()); - // force redraw - this.groupMemberModerationTableForceUpdate++; - }; - - $app.methods.deleteSelectedGroupMember = function (user) { - var D = this.groupMemberModeration; - D.selectedUsers.delete(user.userId); - D.selectedUsersArray = Array.from(D.selectedUsers.values()); - for (var i = 0; i < this.groupMemberModerationTable.data.length; i++) { - var row = this.groupMemberModerationTable.data[i]; - if (row.userId === user.userId) { - row.$selected = false; - break; - } - } - for (var i = 0; i < this.groupBansModerationTable.data.length; i++) { - var row = this.groupBansModerationTable.data[i]; - if (row.userId === user.userId) { - row.$selected = false; - break; - } - } - for (var i = 0; i < this.groupInvitesModerationTable.data.length; i++) { - var row = this.groupInvitesModerationTable.data[i]; - if (row.userId === user.userId) { - row.$selected = false; - break; + try { + var response = await webApiService.execute({ + url, + method: 'GET' + }); + if ( + response.status !== 200 || + !response.data.startsWith('data:image/png') + ) { + throw new Error(`Error: ${response.data}`); } - } - for ( - var i = 0; - i < this.groupJoinRequestsModerationTable.data.length; - i++ - ) { - var row = this.groupJoinRequestsModerationTable.data[i]; - if (row.userId === user.userId) { - row.$selected = false; - break; + var link = document.createElement('a'); + link.href = response.data; + var fileName = `${$utils.extractFileId(url)}.png`; + if (!fileName) { + fileName = `${url.split('/').pop()}.png`; } - } - for (var i = 0; i < this.groupBlockedModerationTable.data.length; i++) { - var row = this.groupBlockedModerationTable.data[i]; - if (row.userId === user.userId) { - row.$selected = false; - break; + if (!fileName) { + fileName = 'image.png'; } + link.setAttribute('download', fileName); + document.body.appendChild(link); + link.click(); + document.body.removeChild(link); + } catch { + new Noty({ + type: 'error', + text: $app.escapeTag(`Failed to download image. ${url}`) + }).show(); } - - // force redraw - this.groupMemberModerationTableForceUpdate++; }; - $app.methods.clearSelectedGroupMembers = function () { - var D = this.groupMemberModeration; - D.selectedUsers.clear(); - D.selectedUsersArray = []; - for (var i = 0; i < this.groupMemberModerationTable.data.length; i++) { - var row = this.groupMemberModerationTable.data[i]; - row.$selected = false; - } - for (var i = 0; i < this.groupBansModerationTable.data.length; i++) { - var row = this.groupBansModerationTable.data[i]; - row.$selected = false; - } - for (var i = 0; i < this.groupInvitesModerationTable.data.length; i++) { - var row = this.groupInvitesModerationTable.data[i]; - row.$selected = false; - } - for ( - var i = 0; - i < this.groupJoinRequestsModerationTable.data.length; - i++ - ) { - var row = this.groupJoinRequestsModerationTable.data[i]; - row.$selected = false; + $app.methods.downloadAndSaveJson = function (fileName, data) { + if (!fileName || !data) { + return; } - for (var i = 0; i < this.groupBlockedModerationTable.data.length; i++) { - var row = this.groupBlockedModerationTable.data[i]; - row.$selected = false; + try { + var link = document.createElement('a'); + link.setAttribute( + 'href', + `data:application/json;charset=utf-8,${encodeURIComponent( + JSON.stringify(data, null, 2) + )}` + ); + link.setAttribute('download', `${fileName}.json`); + document.body.appendChild(link); + link.click(); + document.body.removeChild(link); + } catch { + new Noty({ + type: 'error', + text: $app.escapeTag('Failed to download JSON.') + }).show(); } - // force redraw - this.groupMemberModerationTableForceUpdate++; }; - $app.methods.selectAllGroupMembers = function () { - var D = this.groupMemberModeration; - for (var i = 0; i < this.groupMemberModerationTable.data.length; i++) { - var row = this.groupMemberModerationTable.data[i]; - row.$selected = true; - D.selectedUsers.set(row.userId, row); - } - D.selectedUsersArray = Array.from(D.selectedUsers.values()); - // force redraw - this.groupMemberModerationTableForceUpdate++; + $app.methods.setPlayerModeration = function (userId, type) { + var D = this.userDialog; + AppApi.SetVRChatUserModeration(API.currentUser.id, userId, type).then( + (result) => { + if (result) { + if (type === 4) { + D.isShowAvatar = false; + D.isHideAvatar = true; + } else if (type === 5) { + D.isShowAvatar = true; + D.isHideAvatar = false; + } else { + D.isShowAvatar = false; + D.isHideAvatar = false; + } + } else { + $app.$message({ + message: 'Failed to change avatar moderation', + type: 'error' + }); + } + } + ); }; - $app.methods.selectAllGroupBans = function () { - var D = this.groupMemberModeration; - for (var i = 0; i < this.groupBansModerationTable.data.length; i++) { - var row = this.groupBansModerationTable.data[i]; - row.$selected = true; - D.selectedUsers.set(row.userId, row); - } - D.selectedUsersArray = Array.from(D.selectedUsers.values()); - // force redraw - this.groupMemberModerationTableForceUpdate++; - }; + // #endregion + // #region | App: Language - $app.methods.selectAllGroupInvites = function () { - var D = this.groupMemberModeration; - for (var i = 0; i < this.groupInvitesModerationTable.data.length; i++) { - var row = this.groupInvitesModerationTable.data[i]; - row.$selected = true; - D.selectedUsers.set(row.userId, row); - } - D.selectedUsersArray = Array.from(D.selectedUsers.values()); - // force redraw - this.groupMemberModerationTableForceUpdate++; - }; + $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' + } + }; - $app.methods.selectAllGroupJoinRequests = function () { - var D = this.groupMemberModeration; - for ( - var i = 0; - i < this.groupJoinRequestsModerationTable.data.length; - i++ - ) { - var row = this.groupJoinRequestsModerationTable.data[i]; - row.$selected = true; - D.selectedUsers.set(row.userId, row); - } - D.selectedUsersArray = Array.from(D.selectedUsers.values()); - // force redraw - this.groupMemberModerationTableForceUpdate++; + this.userDialogWorldOrderOptions = { + descending: { + name: $t('dialog.user.worlds.order.descending'), + value: 'descending' + }, + ascending: { + name: $t('dialog.user.worlds.order.ascending'), + value: 'ascending' + } + }; }; - $app.methods.selectAllGroupBlocked = function () { - var D = this.groupMemberModeration; - for (var i = 0; i < this.groupBlockedModerationTable.data.length; i++) { - var row = this.groupBlockedModerationTable.data[i]; - row.$selected = true; - D.selectedUsers.set(row.userId, row); - } - D.selectedUsersArray = Array.from(D.selectedUsers.values()); - // force redraw - this.groupMemberModerationTableForceUpdate++; - }; + $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: '' + } + }; - $app.methods.groupMembersKick = async function () { - var D = this.groupMemberModeration; - var memberCount = D.selectedUsersArray.length; - D.progressTotal = memberCount; - try { - for (var i = 0; i < memberCount; i++) { - if (!D.visible || !D.progressTotal) { - break; - } - var user = D.selectedUsersArray[i]; - D.progressCurrent = i + 1; - if (user.userId === API.currentUser.id) { - continue; - } - await API.kickGroupMember({ - groupId: D.id, - userId: user.userId - }); - console.log(`Kicking ${user.userId} ${i + 1}/${memberCount}`); + this.groupDialogFilterOptions = { + everyone: { + name: $t('dialog.group.members.filters.everyone'), + id: null + }, + usersWithNoRole: { + name: $t('dialog.group.members.filters.users_with_no_role'), + id: '' } - this.$message({ - message: `Kicked ${memberCount} group members`, - type: 'success' - }); - } catch (err) { - console.error(err); - this.$message({ - message: `Failed to kick group member: ${err}`, - type: 'error' - }); - } finally { - D.progressCurrent = 0; - D.progressTotal = 0; - } + }; }; - $app.methods.groupMembersBan = async function () { - var D = this.groupMemberModeration; - var memberCount = D.selectedUsersArray.length; - D.progressTotal = memberCount; - try { - for (var i = 0; i < memberCount; i++) { - if (!D.visible || !D.progressTotal) { - break; - } - var user = D.selectedUsersArray[i]; - D.progressCurrent = i + 1; - if (user.userId === API.currentUser.id) { - continue; - } - await API.banGroupMember({ - groupId: D.id, - userId: user.userId - }); - console.log(`Banning ${user.userId} ${i + 1}/${memberCount}`); - } - this.$message({ - message: `Banned ${memberCount} group members`, - type: 'success' - }); - } catch (err) { - console.error(err); - this.$message({ - message: `Failed to ban group member: ${err}`, - type: 'error' - }); - } finally { - D.progressCurrent = 0; - D.progressTotal = 0; - } + $app.methods.applyLanguageStrings = function () { + // repply sorting strings + this.applyUserDialogSortingStrings(); + this.applyGroupDialogSortingStrings(); + this.userDialog.worldSorting = + this.userDialogWorldSortingOptions.updated; + this.userDialog.worldOrder = + this.userDialogWorldOrderOptions.descending; + this.groupDialog.memberFilter = this.groupDialogFilterOptions.everyone; + this.groupDialog.memberSortOrder = + this.groupDialogSortingOptions.joinedAtDesc; }; - $app.methods.groupMembersUnban = async function () { - var D = this.groupMemberModeration; - var memberCount = D.selectedUsersArray.length; - D.progressTotal = memberCount; - try { - for (var i = 0; i < memberCount; i++) { - if (!D.visible || !D.progressTotal) { - break; - } - var user = D.selectedUsersArray[i]; - D.progressCurrent = i + 1; - if (user.userId === API.currentUser.id) { - continue; - } - await API.unbanGroupMember({ - groupId: D.id, - userId: user.userId - }); - console.log(`Unbanning ${user.userId} ${i + 1}/${memberCount}`); - } - this.$message({ - message: `Unbanned ${memberCount} group members`, - type: 'success' - }); - } catch (err) { - console.error(err); - this.$message({ - message: `Failed to unban group member: ${err}`, - type: 'error' + $app.data.appLanguage = + (await configRepository.getString('VRCX_appLanguage')) ?? 'en'; + 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); + } }); - } finally { - D.progressCurrent = 0; - D.progressTotal = 0; } + + $app.applyLanguageStrings(); }; - $app.methods.groupMembersDeleteSentInvite = async function () { - var D = this.groupMemberModeration; - var memberCount = D.selectedUsersArray.length; - D.progressTotal = memberCount; - try { - for (var i = 0; i < memberCount; i++) { - if (!D.visible || !D.progressTotal) { - break; - } - var user = D.selectedUsersArray[i]; - D.progressCurrent = i + 1; - if (user.userId === API.currentUser.id) { - continue; - } - await API.deleteSentGroupInvite({ - groupId: D.id, - userId: user.userId - }); - console.log( - `Deleting group invite ${user.userId} ${i + 1}/${memberCount}` - ); - } - this.$message({ - message: `Deleted ${memberCount} group invites`, - type: 'success' - }); - } catch (err) { - console.error(err); - this.$message({ - message: `Failed to delete group invites: ${err}`, - type: 'error' - }); - } finally { - D.progressCurrent = 0; - D.progressTotal = 0; - } + $app.methods.changeAppLanguage = function (language) { + console.log('Language changed:', language); + this.appLanguage = language; + i18n.locale = language; + configRepository.setString('VRCX_appLanguage', language); + this.applyLanguageStrings(); + this.updateVRConfigVars(); + this._stringComparer = undefined; }; - $app.methods.groupMembersDeleteBlockedRequest = async function () { - var D = this.groupMemberModeration; - var memberCount = D.selectedUsersArray.length; - D.progressTotal = memberCount; - try { - for (var i = 0; i < memberCount; i++) { - if (!D.visible || !D.progressTotal) { - break; - } - var user = D.selectedUsersArray[i]; - D.progressCurrent = i + 1; - if (user.userId === API.currentUser.id) { - continue; - } - await API.deleteBlockedGroupRequest({ - groupId: D.id, - userId: user.userId - }); - console.log( - `Deleting blocked group request ${user.userId} ${i + 1}/${memberCount}` - ); - } - this.$message({ - message: `Deleted ${memberCount} blocked group requests`, - type: 'success' - }); - } catch (err) { - console.error(err); - this.$message({ - message: `Failed to delete blocked group requests: ${err}`, - type: 'error' - }); - } finally { - D.progressCurrent = 0; - D.progressTotal = 0; + // #endregion + // #region | App: Random unsorted app methods, data structs, API functions, and an API feedback/file analysis event + API.$on('USER:FEEDBACK', function (args) { + if (args.params.userId === this.currentUser.id) { + $app.currentUserFeedbackData = $utils.buildTreeData(args.json); } + }); + + $app.methods.getCurrentUserFeedback = function () { + return API.getUserFeedback({ userId: API.currentUser.id }); }; - $app.methods.groupMembersAcceptInviteRequest = async function () { - var D = this.groupMemberModeration; - var memberCount = D.selectedUsersArray.length; - D.progressTotal = memberCount; - try { - for (var i = 0; i < memberCount; i++) { - if (!D.visible || !D.progressTotal) { - break; - } - var user = D.selectedUsersArray[i]; - D.progressCurrent = i + 1; - if (user.userId === API.currentUser.id) { - continue; - } - await API.acceptGroupInviteRequest({ - groupId: D.id, - userId: user.userId - }); - console.log( - `Accepting group join request ${user.userId} ${i + 1}/${memberCount}` - ); - } - this.$message({ - message: `Accepted ${memberCount} group join requests`, - type: 'success' - }); - } catch (err) { - console.error(err); - this.$message({ - message: `Failed to accept group join requests: ${err}`, - type: 'error' - }); - } finally { - D.progressCurrent = 0; - D.progressTotal = 0; - } + $app.data.changeLogDialog = { + visible: false, + buildName: '', + changeLog: '' }; - $app.methods.groupMembersRejectInviteRequest = async function () { - var D = this.groupMemberModeration; - var memberCount = D.selectedUsersArray.length; - D.progressTotal = memberCount; - try { - for (var i = 0; i < memberCount; i++) { - if (!D.visible || !D.progressTotal) { - break; - } - var user = D.selectedUsersArray[i]; - D.progressCurrent = i + 1; - if (user.userId === API.currentUser.id) { - continue; - } - await API.rejectGroupInviteRequest({ - groupId: D.id, - userId: user.userId - }); - console.log( - `Rejecting group join request ${user.userId} ${i + 1}/${memberCount}` - ); - } - this.$message({ - message: `Rejected ${memberCount} group join requests`, - type: 'success' - }); - } catch (err) { - console.error(err); - this.$message({ - message: `Failed to reject group join requests: ${err}`, - type: 'error' - }); - } finally { - D.progressCurrent = 0; - D.progressTotal = 0; - } + $app.methods.showChangeLogDialog = function () { + this.$nextTick(() => + $app.adjustDialogZ(this.$refs.changeLogDialog.$el) + ); + this.changeLogDialog.visible = true; + this.checkForVRCXUpdate(); }; - $app.methods.groupMembersBlockJoinRequest = async function () { - var D = this.groupMemberModeration; - var memberCount = D.selectedUsersArray.length; - D.progressTotal = memberCount; - try { - for (var i = 0; i < memberCount; i++) { - if (!D.visible || !D.progressTotal) { - break; - } - var user = D.selectedUsersArray[i]; - D.progressCurrent = i + 1; - if (user.userId === API.currentUser.id) { - continue; - } - await API.blockGroupInviteRequest({ - groupId: D.id, - userId: user.userId - }); - console.log( - `Blocking group join request ${user.userId} ${i + 1}/${memberCount}` - ); - } - this.$message({ - message: `Blocked ${memberCount} group join requests`, - type: 'success' - }); - } catch (err) { - console.error(err); - this.$message({ - message: `Failed to block group join requests: ${err}`, - type: 'error' - }); - } finally { - D.progressCurrent = 0; - D.progressTotal = 0; - } + $app.data.gallerySelectDialog = { + visible: false, + selectedFileId: '', + selectedImageUrl: '' }; - $app.methods.groupMembersSaveNote = async function () { - var D = this.groupMemberModeration; - var memberCount = D.selectedUsersArray.length; - D.progressTotal = memberCount; - try { - for (var i = 0; i < memberCount; i++) { - if (!D.visible || !D.progressTotal) { - break; - } - var user = D.selectedUsersArray[i]; - D.progressCurrent = i + 1; - if (user.managerNotes === D.note) { - continue; - } - await API.setGroupMemberProps(user.userId, D.id, { - managerNotes: D.note - }); - console.log( - `Setting note ${D.note} ${user.userId} ${ - i + 1 - }/${memberCount}` - ); - } - this.$message({ - message: `Saved notes for ${memberCount} group members`, - type: 'success' - }); - } catch (err) { - console.error(err); - this.$message({ - message: `Failed to set group member note: ${err}`, - type: 'error' - }); - } finally { - D.progressCurrent = 0; - D.progressTotal = 0; - } + $app.methods.showGallerySelectDialog = function () { + this.$nextTick(() => + $app.adjustDialogZ(this.$refs.gallerySelectDialog.$el) + ); + var D = this.gallerySelectDialog; + D.visible = true; + this.refreshGalleryTable(); }; - $app.methods.groupMembersAddRoles = async function () { - var D = this.groupMemberModeration; - var memberCount = D.selectedUsersArray.length; - D.progressTotal = memberCount; - try { - for (var i = 0; i < memberCount; i++) { - if (!D.visible || !D.progressTotal) { - break; - } - var user = D.selectedUsersArray[i]; - D.progressCurrent = i + 1; - var rolesToAdd = []; - D.selectedRoles.forEach((roleId) => { - if (!user.roleIds.includes(roleId)) { - rolesToAdd.push(roleId); - } - }); + $app.methods.selectImageGallerySelect = function (imageUrl, fileId) { + var D = this.gallerySelectDialog; + D.selectedFileId = fileId; + D.selectedImageUrl = imageUrl; + D.visible = false; + console.log(imageUrl, fileId); + }; - if (!rolesToAdd.length) { - continue; - } - for (var j = 0; j < rolesToAdd.length; j++) { - var roleId = rolesToAdd[j]; - console.log( - `Adding role: ${roleId} ${user.userId} ${ - i + 1 - }/${memberCount}` - ); - await API.addGroupMemberRole({ - groupId: D.id, - userId: user.userId, - roleId - }); - } - } - this.$message({ - message: 'Added group member roles', - type: 'success' - }); - } catch (err) { - console.error(err); - this.$message({ - message: `Failed to add group member roles: ${err}`, - type: 'error' - }); - } finally { - D.progressCurrent = 0; - D.progressTotal = 0; - } + $app.methods.clearImageGallerySelect = function () { + var D = this.gallerySelectDialog; + D.selectedFileId = ''; + D.selectedImageUrl = ''; }; - $app.methods.groupMembersRemoveRoles = async function () { - var D = this.groupMemberModeration; - var memberCount = D.selectedUsersArray.length; - D.progressTotal = memberCount; - try { - for (var i = 0; i < memberCount; i++) { - if (!D.visible || !D.progressTotal) { - break; - } - var user = D.selectedUsersArray[i]; - D.progressCurrent = i + 1; - var rolesToRemove = []; - D.selectedRoles.forEach((roleId) => { - if (user.roleIds.includes(roleId)) { - rolesToRemove.push(roleId); - } - }); - if (!rolesToRemove.length) { - continue; - } - for (var j = 0; j < rolesToRemove.length; j++) { - var roleId = rolesToRemove[j]; - console.log( - `Removing role ${roleId} ${user.userId} ${ - i + 1 - }/${memberCount}` - ); - await API.removeGroupMemberRole({ - groupId: D.id, - userId: user.userId, - roleId - }); - } + $app.methods.reportUserForHacking = function (userId) { + API.reportUser({ + userId, + contentType: 'user', + reason: 'behavior-hacking', + type: 'report' + }); + }; + + /** + * @param {{ + userId: string, + contentType: string, + reason: string, + type: string + }} params + * @return { Promise<{json: any, params}> } + */ + API.reportUser = function (params) { + return this.call(`feedback/${params.userId}/user`, { + method: 'POST', + params: { + contentType: params.contentType, + reason: params.reason, + type: params.type } - this.$message({ - message: 'Roles removed', - type: 'success' - }); - } catch (err) { - console.error(err); - this.$message({ - message: `Failed to remove group member roles: ${err}`, - type: 'error' - }); - } finally { - D.progressCurrent = 0; - D.progressTotal = 0; - } + }).then((json) => { + var args = { + json, + params + }; + this.$emit('FEEDBACK:REPORT:USER', args); + return args; + }); + }; + + $app.methods.changeLogRemoveLinks = function (text) { + return text.replace(/([^!])\[[^\]]+\]\([^)]+\)/g, '$1'); + }; + + /** + * @param {{ + fileId: string, + version: number + }} params + * @return { Promise<{json: any, params}> } + + */ + API.getFileAnalysis = function (params) { + return this.call(`analysis/${params.fileId}/${params.version}`, { + method: 'GET' + }).then((json) => { + var args = { + json, + params + }; + this.$emit('FILE:ANALYSIS', args); + return args; + }); }; - $app.methods.selectGroupMemberUserId = async function () { - var D = this.groupMemberModeration; - if (!D.selectUserId) { + API.$on('FILE:ANALYSIS', function (args) { + if (!$app.avatarDialog.visible) { return; } - - var regexUserId = - /usr_[0-9A-Fa-f]{8}-([0-9A-Fa-f]{4}-){3}[0-9A-Fa-f]{12}/g; - var match = []; - var userIdList = new Set(); - while ((match = regexUserId.exec(D.selectUserId)) !== null) { - userIdList.add(match[0]); + var ref = args.json; + if (typeof ref.fileSize !== 'undefined') { + ref._fileSize = `${(ref.fileSize / 1048576).toFixed(2)} MB`; } - if (userIdList.size === 0) { - // for those users missing the usr_ prefix - userIdList.add(D.selectUserId); + if (typeof ref.uncompressedSize !== 'undefined') { + ref._uncompressedSize = `${(ref.uncompressedSize / 1048576).toFixed( + 2 + )} MB`; } - for (var userId of userIdList) { - try { - await this.addGroupMemberToSelection(userId); - } catch { - console.error(`Failed to add user ${userId}`); + if (typeof ref.avatarStats?.totalTextureUsage !== 'undefined') { + ref._totalTextureUsage = `${( + ref.avatarStats.totalTextureUsage / 1048576 + ).toFixed(2)} MB`; + } + $app.avatarDialog.fileAnalysis = $utils.buildTreeData(args.json); + }); + + $app.methods.getAvatarFileAnalysis = function () { + var D = this.avatarDialog; + var assetUrl = ''; + for (let i = D.ref.unityPackages.length - 1; i > -1; i--) { + var unityPackage = D.ref.unityPackages[i]; + if ( + unityPackage.variant && + unityPackage.variant !== 'standard' && + unityPackage.variant !== 'security' + ) { + continue; + } + if ( + unityPackage.platform === 'standalonewindows' && + this.compareUnityVersion(unityPackage.unitySortNumber) + ) { + assetUrl = unityPackage.assetUrl; + break; } } + if (!assetUrl) { + assetUrl = D.ref.assetUrl; + } + var fileId = $utils.extractFileId(assetUrl); + var version = parseInt($utils.extractFileVersion(assetUrl), 10); + if (!fileId || !version) { + this.$message({ + message: 'File Analysis unavailable', + type: 'error' + }); + return; + } + API.getFileAnalysis({ fileId, version }); + }; - D.selectUserId = ''; + $app.methods.openFolderGeneric = function (path) { + AppApi.OpenFolderAndSelectItem(path, true); }; - $app.methods.addGroupMemberToSelection = async function (userId) { - var D = this.groupMemberModeration; + // #endregion + // #region | Dialog: fullscreen image - // fetch memeber if there is one - // banned members don't have a user object + $app.data.fullscreenImageDialog = { + visible: false, + imageUrl: '' + }; - var memeber = {}; - var memeberArgs = await API.getGroupMember({ - groupId: D.id, - userId - }); - if (memeberArgs.json) { - memeber = API.applyGroupMember(memeberArgs.json); - } - if (memeber.user) { - D.selectedUsers.set(memeber.userId, memeber); - D.selectedUsersArray = Array.from(D.selectedUsers.values()); - this.groupMemberModerationTableForceUpdate++; + $app.methods.showFullscreenImageDialog = function (imageUrl) { + if (!imageUrl) { return; } + this.$nextTick(() => + $app.adjustDialogZ(this.$refs.fullscreenImageDialog.$el) + ); + var D = this.fullscreenImageDialog; + D.imageUrl = imageUrl; + D.visible = true; + }; - var userArgs = await API.getCachedUser({ - userId - }); - memeber.userId = userArgs.json.id; - memeber.user = userArgs.json; - memeber.displayName = userArgs.json.displayName; + // #endregion + // #region | Open common folders - D.selectedUsers.set(memeber.userId, memeber); - D.selectedUsersArray = Array.from(D.selectedUsers.values()); - this.groupMemberModerationTableForceUpdate++; + $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.data.groupPostEditDialog = { - visible: false, - groupRef: {}, - title: '', - text: '', - sendNotification: true, - visibility: 'group', - roleIds: [], - postId: '', - groupId: '' + $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.showGroupPostEditDialog = function (groupId, post) { - this.$nextTick(() => adjustDialogZ(this.$refs.groupPostEditDialog.$el)); - var D = this.groupPostEditDialog; - D.sendNotification = true; - D.groupRef = {}; - D.title = ''; - D.text = ''; - D.visibility = 'group'; - D.roleIds = []; - D.postId = ''; - D.groupId = groupId; - $app.gallerySelectDialog.selectedFileId = ''; - $app.gallerySelectDialog.selectedImageUrl = ''; - if (post) { - D.title = post.title; - D.text = post.text; - D.visibility = post.visibility; - D.roleIds = post.roleIds; - D.postId = post.id; - $app.gallerySelectDialog.selectedFileId = post.imageId; - $app.gallerySelectDialog.selectedImageUrl = post.imageUrl; - } - API.getCachedGroup({ groupId }).then((args) => { - D.groupRef = args.ref; + $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' + }); + } }); - D.visible = true; }; - $app.methods.editGroupPost = function () { - var D = this.groupPostEditDialog; - if (!D.groupId || !D.postId) { - return; - } - var params = { - groupId: D.groupId, - postId: D.postId, - title: D.title, - text: D.text, - roleIds: D.roleIds, - visibility: D.visibility, - imageId: null - }; - if (this.gallerySelectDialog.selectedFileId) { - params.imageId = this.gallerySelectDialog.selectedFileId; - } - API.editGroupPost(params).then((args) => { - this.$message({ - message: 'Group post edited', - type: 'success' - }); - return args; + $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' + }); + } }); - D.visible = false; }; - $app.methods.createGroupPost = function () { - var D = this.groupPostEditDialog; - var params = { - groupId: D.groupId, - title: D.title, - text: D.text, - roleIds: D.roleIds, - visibility: D.visibility, - sendNotification: D.sendNotification, - imageId: null - }; - if (this.gallerySelectDialog.selectedFileId) { - params.imageId = this.gallerySelectDialog.selectedFileId; - } - API.createGroupPost(params).then((args) => { - this.$message({ - message: 'Group post created', - type: 'success' - }); - return args; + $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' + }); + } }); - D.visible = false; }; // #endregion @@ -33942,127 +21186,6 @@ speechSynthesis.getVoices(); AppApi.SetZoom(this.zoomLevel / 10 - 10); }; - // #endregion - // #region | Boops - - /** - * @param {{ - userId: string, - emojiId: string - }} params - * @returns {Promise<{json: any, params}>} - */ - API.sendBoop = function (params) { - return this.call(`users/${params.userId}/boop`, { - method: 'POST', - params - }).then((json) => { - var args = { - json, - params - }; - this.$emit('BOOP:SEND', args); - return args; - }); - }; - - $app.methods.sendBoop = function () { - var D = this.sendBoopDialog; - this.dismissBoop(D.userId); - var params = { - userId: D.userId - }; - if (D.fileId) { - params.emojiId = D.fileId; - } - API.sendBoop(params); - D.visible = false; - }; - - $app.methods.dismissBoop = function (userId) { - // JANK: This is a hack to remove boop notifications when responding - var array = this.notificationTable.data; - for (var i = array.length - 1; i >= 0; i--) { - var ref = array[i]; - if ( - ref.type !== 'boop' || - ref.$isExpired || - ref.senderUserId !== userId - ) { - continue; - } - API.sendNotificationResponse({ - notificationId: ref.id, - responseType: 'delete', - responseData: '' - }); - } - }; - - $app.data.sendBoopDialog = { - visible: false, - userId: '', - fileId: '' - }; - - $app.methods.showSendBoopDialog = function (userId) { - this.$nextTick(() => adjustDialogZ(this.$refs.sendBoopDialog.$el)); - var D = this.sendBoopDialog; - D.userId = userId; - D.visible = true; - if (this.emojiTable.length === 0 && API.currentUser.$isVRCPlus) { - this.refreshEmojiTable(); - } - }; - - $app.methods.getEmojiValue = function (emojiName) { - if (!emojiName) { - return ''; - } - return `vrchat_${emojiName.replace(/ /g, '_').toLowerCase()}`; - }; - - $app.methods.getEmojiName = function (emojiValue) { - // uppercase first letter of each word - if (!emojiValue) { - return ''; - } - return emojiValue - .replace('vrchat_', '') - .replace(/_/g, ' ') - .replace(/\b\w/g, (l) => l.toUpperCase()); - }; - - // #endregion - - // #region proxy settings - - $app.methods.promptProxySettings = function () { - this.$prompt( - $t('prompt.proxy_settings.description'), - $t('prompt.proxy_settings.header'), - { - distinguishCancelAndClose: true, - confirmButtonText: $t('prompt.proxy_settings.restart'), - cancelButtonText: $t('prompt.proxy_settings.close'), - inputValue: this.proxyServer, - inputPlaceholder: $t('prompt.proxy_settings.placeholder'), - callback: async (action, instance) => { - this.proxyServer = instance.inputValue; - await VRCXStorage.Set('VRCX_ProxyServer', this.proxyServer); - await VRCXStorage.Flush(); - await new Promise((resolve) => { - workerTimers.setTimeout(resolve, 100); - }); - if (action === 'confirm') { - var isUpgrade = false; - this.restartVRCX(isUpgrade); - } - } - } - ); - }; - // #endregion // #region instance join history @@ -34159,8 +21282,16 @@ speechSynthesis.getVoices(); // #endregion + $app.data.ossDialog = false; + + // "$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 @@ -34172,7 +21303,7 @@ speechSynthesis.getVoices(); // }; // $app.methods.showTemplateDialog = function () { -// this.$nextTick(() => adjustDialogZ(this.$refs.templateDialog.$el)); +// this.$nextTick(() => $app.adjustDialogZ(this.$refs.templateDialog.$el)); // var D = this.templateDialog; // D.visible = true; // }; diff --git a/html/src/classes/API/config.js b/html/src/classes/API/config.js new file mode 100644 index 000000000..6ad6bb3a6 --- /dev/null +++ b/html/src/classes/API/config.js @@ -0,0 +1,37 @@ +import { baseClass, $app, API, $t, $utils } from '../baseClass.js'; + +export default class extends baseClass { + constructor(_app, _API, _t) { + super(_app, _API, _t); + } + + init() { + API.getConfig = function () { + return this.call('config', { + method: 'GET' + }).then((json) => { + var args = { + json + }; + this.$emit('CONFIG', args); + return args; + }); + }; + + API.$on('CONFIG', function (args) { + args.ref = this.applyConfig(args.json); + }); + + API.applyConfig = function (json) { + var ref = { + ...json + }; + this.cachedConfig = ref; + return ref; + }; + } + + _data = {}; + + _methods = {}; +} diff --git a/html/src/classes/_classTemplate.js b/html/src/classes/_classTemplate.js new file mode 100644 index 000000000..c17fead72 --- /dev/null +++ b/html/src/classes/_classTemplate.js @@ -0,0 +1,16 @@ +import * as workerTimers from 'worker-timers'; +import configRepository from '../repository/config.js'; +import database from '../repository/database.js'; +import { baseClass, $app, API, $t, $utils } from './baseClass.js'; + +export default class extends baseClass { + constructor(_app, _API, _t) { + super(_app, _API, _t); + } + + init() {} + + _data = {}; + + _methods = {}; +} diff --git a/html/src/classes/apiInit.js b/html/src/classes/apiInit.js new file mode 100644 index 000000000..5fa48ca32 --- /dev/null +++ b/html/src/classes/apiInit.js @@ -0,0 +1,53 @@ +import { baseClass, $app, API, $t } from './baseClass.js'; + +export default class extends baseClass { + constructor(_app) { + super(_app); + } + + eventHandlers = new Map(); + + $emit = function (name, ...args) { + if ($app.debug) { + console.log(name, ...args); + } + var handlers = this.eventHandlers.get(name); + if (typeof handlers === 'undefined') { + return; + } + try { + for (var handler of handlers) { + handler.apply(this, args); + } + } catch (err) { + console.error(err); + } + }; + + $on = function (name, handler) { + var handlers = this.eventHandlers.get(name); + if (typeof handlers === 'undefined') { + handlers = []; + this.eventHandlers.set(name, handlers); + } + handlers.push(handler); + }; + + $off = function (name, handler) { + var handlers = this.eventHandlers.get(name); + if (typeof handlers === 'undefined') { + return; + } + var { length } = handlers; + for (var i = 0; i < length; ++i) { + if (handlers[i] === handler) { + if (length > 1) { + handlers.splice(i, 1); + } else { + this.eventHandlers.delete(name); + } + break; + } + } + }; +} diff --git a/html/src/classes/apiLogin.js b/html/src/classes/apiLogin.js new file mode 100644 index 000000000..4a4eae3f6 --- /dev/null +++ b/html/src/classes/apiLogin.js @@ -0,0 +1,438 @@ +import Noty from 'noty'; +import security from '../security.js'; +import configRepository from '../repository/config.js'; +import { baseClass, $app, API, $t } from './baseClass.js'; +/* eslint-disable no-unused-vars */ +let webApiService = {}; +/* eslint-enable no-unused-vars */ + +export default class extends baseClass { + constructor(_app, _API, _t, _webApiService) { + super(_app, _API, _t); + webApiService = _webApiService; + } + + async init() { + $app.savedCredentials = + (await configRepository.getString('savedCredentials')) !== null + ? JSON.parse( + await configRepository.getString('savedCredentials') + ) + : {}; + $app.lastUserLoggedIn = + await configRepository.getString('lastUserLoggedIn'); + + API.isLoggedIn = false; + API.attemptingAutoLogin = false; + + /** + * @param {{ username: string, password: string }} params credential to login + * @returns {Promise<{origin: boolean, json: any, params}>} + */ + API.login = function (params) { + var { username, password, saveCredentials, cipher } = params; + username = encodeURIComponent(username); + password = encodeURIComponent(password); + var auth = btoa(`${username}:${password}`); + if (saveCredentials) { + delete params.saveCredentials; + if (cipher) { + params.password = cipher; + delete params.cipher; + } + $app.saveCredentials = params; + } + return this.call('auth/user', { + method: 'GET', + headers: { + Authorization: `Basic ${auth}` + } + }).then((json) => { + var args = { + json, + params, + origin: true + }; + if ( + json.requiresTwoFactorAuth && + json.requiresTwoFactorAuth.includes('emailOtp') + ) { + this.$emit('USER:EMAILOTP', args); + } else if (json.requiresTwoFactorAuth) { + this.$emit('USER:2FA', args); + } else { + this.$emit('USER:CURRENT', args); + } + return args; + }); + }; + + /** + * @param {{ code: string }} params One-time password + * @returns {Promise<{json: any, params}>} + */ + API.verifyOTP = function (params) { + return this.call('auth/twofactorauth/otp/verify', { + method: 'POST', + params + }).then((json) => { + var args = { + json, + params + }; + this.$emit('OTP', args); + return args; + }); + }; + + /** + * @param {{ code: string }} params One-time token + * @returns {Promise<{json: any, params}>} + */ + API.verifyTOTP = function (params) { + return this.call('auth/twofactorauth/totp/verify', { + method: 'POST', + params + }).then((json) => { + var args = { + json, + params + }; + this.$emit('TOTP', args); + return args; + }); + }; + + /** + * @param {{ code: string }} params One-time token + * @returns {Promise<{json: any, params}>} + */ + API.verifyEmailOTP = function (params) { + return this.call('auth/twofactorauth/emailotp/verify', { + method: 'POST', + params + }).then((json) => { + var args = { + json, + params + }; + this.$emit('EMAILOTP', args); + return args; + }); + }; + + API.$on('AUTOLOGIN', function () { + if (this.attemptingAutoLogin) { + return; + } + this.attemptingAutoLogin = true; + var user = + $app.loginForm.savedCredentials[ + $app.loginForm.lastUserLoggedIn + ]; + if (typeof user === 'undefined') { + this.attemptingAutoLogin = false; + return; + } + if ($app.enablePrimaryPassword) { + this.logout(); + return; + } + $app.relogin(user) + .then(() => { + if (this.errorNoty) { + this.errorNoty.close(); + } + this.errorNoty = new Noty({ + type: 'success', + text: 'Automatically logged in.' + }).show(); + console.log('Automatically logged in.'); + }) + .catch((err) => { + if (this.errorNoty) { + this.errorNoty.close(); + } + this.errorNoty = new Noty({ + type: 'error', + text: 'Failed to login automatically.' + }).show(); + console.error('Failed to login automatically.', err); + }) + .finally(() => { + if (!navigator.onLine) { + this.errorNoty = new Noty({ + type: 'error', + text: `You're offline.` + }).show(); + console.error(`You're offline.`); + } + }); + }); + + API.$on('USER:CURRENT', function () { + this.attemptingAutoLogin = false; + }); + + API.$on('LOGOUT', function () { + this.attemptingAutoLogin = false; + }); + + API.logout = function () { + this.$emit('LOGOUT'); + // return this.call('logout', { + // method: 'PUT' + // }).finally(() => { + // this.$emit('LOGOUT'); + // }); + }; + } + + _data = { + loginForm: { + loading: true, + username: '', + password: '', + endpoint: '', + websocket: '', + saveCredentials: false, + savedCredentials: {}, + lastUserLoggedIn: '', + rules: { + username: [ + { + required: true, + trigger: 'blur' + } + ], + password: [ + { + required: true, + trigger: 'blur' + } + ] + } + } + }; + + _methods = { + async relogin(user) { + var { loginParmas } = user; + if (user.cookies) { + await webApiService.setCookies(user.cookies); + } + this.loginForm.lastUserLoggedIn = user.user.id; // for resend email 2fa + if (loginParmas.endpoint) { + API.endpointDomain = loginParmas.endpoint; + API.websocketDomain = loginParmas.websocket; + } else { + API.endpointDomain = API.endpointDomainVrchat; + API.websocketDomain = API.websocketDomainVrchat; + } + return new Promise((resolve, reject) => { + if (this.enablePrimaryPassword) { + this.checkPrimaryPassword(loginParmas) + .then((pwd) => { + this.loginForm.loading = true; + return API.getConfig() + .catch((err) => { + this.loginForm.loading = false; + reject(err); + }) + .then(() => { + API.login({ + username: loginParmas.username, + password: pwd, + cipher: loginParmas.password, + endpoint: loginParmas.endpoint, + websocket: loginParmas.websocket + }) + .catch((err2) => { + this.loginForm.loading = false; + // API.logout(); + reject(err2); + }) + .then(() => { + this.loginForm.loading = false; + resolve(); + }); + }); + }) + .catch((_) => { + this.$message({ + message: 'Incorrect primary password', + type: 'error' + }); + reject(_); + }); + } else { + API.getConfig() + .catch((err) => { + this.loginForm.loading = false; + reject(err); + }) + .then(() => { + API.login({ + username: loginParmas.username, + password: loginParmas.password, + endpoint: loginParmas.endpoint, + websocket: loginParmas.websocket + }) + .catch((err2) => { + this.loginForm.loading = false; + API.logout(); + reject(err2); + }) + .then(() => { + this.loginForm.loading = false; + resolve(); + }); + }); + } + }); + }, + + async deleteSavedLogin(userId) { + var savedCredentials = JSON.parse( + await configRepository.getString('savedCredentials') + ); + delete savedCredentials[userId]; + // Disable primary password when no account is available. + if (Object.keys(savedCredentials).length === 0) { + this.enablePrimaryPassword = false; + await configRepository.setBool('enablePrimaryPassword', false); + } + this.loginForm.savedCredentials = savedCredentials; + var jsonCredentials = JSON.stringify(savedCredentials); + await configRepository.setString( + 'savedCredentials', + jsonCredentials + ); + new Noty({ + type: 'success', + text: 'Account removed.' + }).show(); + }, + + async login() { + await webApiService.clearCookies(); + this.$refs.loginForm.validate((valid) => { + if (valid && !this.loginForm.loading) { + this.loginForm.loading = true; + if (this.loginForm.endpoint) { + API.endpointDomain = this.loginForm.endpoint; + API.websocketDomain = this.loginForm.websocket; + } else { + API.endpointDomain = API.endpointDomainVrchat; + API.websocketDomain = API.websocketDomainVrchat; + } + API.getConfig() + .catch((err) => { + this.loginForm.loading = false; + throw err; + }) + .then((args) => { + if ( + this.loginForm.saveCredentials && + this.enablePrimaryPassword + ) { + $app.$prompt( + $t('prompt.primary_password.description'), + $t('prompt.primary_password.header'), + { + inputType: 'password', + inputPattern: /[\s\S]{1,32}/ + } + ) + .then(({ value }) => { + let saveCredential = + this.loginForm.savedCredentials[ + Object.keys( + this.loginForm + .savedCredentials + )[0] + ]; + security + .decrypt( + saveCredential.loginParmas + .password, + value + ) + .then(() => { + security + .encrypt( + this.loginForm.password, + value + ) + .then((pwd) => { + API.login({ + username: + this.loginForm + .username, + password: + this.loginForm + .password, + endpoint: + this.loginForm + .endpoint, + websocket: + this.loginForm + .websocket, + saveCredentials: + this.loginForm + .saveCredentials, + cipher: pwd + }).then(() => { + this.loginForm.username = + ''; + this.loginForm.password = + ''; + this.loginForm.endpoint = + ''; + this.loginForm.websocket = + ''; + }); + }); + }); + }) + .finally(() => { + this.loginForm.loading = false; + }); + return args; + } + API.login({ + username: this.loginForm.username, + password: this.loginForm.password, + endpoint: this.loginForm.endpoint, + websocket: this.loginForm.websocket, + saveCredentials: this.loginForm.saveCredentials + }) + .then(() => { + this.loginForm.username = ''; + this.loginForm.password = ''; + this.loginForm.endpoint = ''; + this.loginForm.websocket = ''; + }) + .finally(() => { + this.loginForm.loading = false; + }); + return args; + }); + } + }); + }, + + logout() { + this.$confirm('Continue? Logout', 'Confirm', { + confirmButtonText: 'Confirm', + cancelButtonText: 'Cancel', + type: 'info', + callback: (action) => { + if (action === 'confirm') { + API.logout(); + } + } + }); + } + }; +} diff --git a/html/src/classes/apiRequestHandler.js b/html/src/classes/apiRequestHandler.js new file mode 100644 index 000000000..ab180a3fe --- /dev/null +++ b/html/src/classes/apiRequestHandler.js @@ -0,0 +1,372 @@ +import Noty from 'noty'; +import { baseClass, $app, API, $t } from './baseClass.js'; +/* eslint-disable no-unused-vars */ +let webApiService = {}; +/* eslint-enable no-unused-vars */ + +export default class extends baseClass { + constructor(_app, _API, _t, _webApiService) { + super(_app, _API, _t); + webApiService = _webApiService; + } + + init() { + API.cachedConfig = {}; + API.pendingGetRequests = new Map(); + API.failedGetRequests = new Map(); + API.endpointDomainVrchat = 'https://api.vrchat.cloud/api/1'; + API.websocketDomainVrchat = 'wss://pipeline.vrchat.cloud'; + API.endpointDomain = 'https://api.vrchat.cloud/api/1'; + API.websocketDomain = 'wss://pipeline.vrchat.cloud'; + + API.call = function (endpoint, options) { + var init = { + url: `${API.endpointDomain}/${endpoint}`, + method: 'GET', + ...options + }; + var { params } = init; + if (init.method === 'GET') { + // don't retry recent 404/403 + if (this.failedGetRequests.has(endpoint)) { + var lastRun = this.failedGetRequests.get(endpoint); + if (lastRun >= Date.now() - 900000) { + // 15mins + throw new Error( + `Bailing request due to recent 404/403, ${endpoint}` + ); + } + this.failedGetRequests.delete(endpoint); + } + // transform body to url + if (params === Object(params)) { + var url = new URL(init.url); + var { searchParams } = url; + for (var key in params) { + searchParams.set(key, params[key]); + } + init.url = url.toString(); + } + // merge requests + var req = this.pendingGetRequests.get(init.url); + if (typeof req !== 'undefined') { + if (req.time >= Date.now() - 10000) { + // 10s + return req.req; + } + this.pendingGetRequests.delete(init.url); + } + } else if ( + init.uploadImage || + init.uploadFilePUT || + init.uploadImageLegacy + ) { + // nothing + } else { + init.headers = { + 'Content-Type': 'application/json;charset=utf-8', + ...init.headers + }; + init.body = + params === Object(params) ? JSON.stringify(params) : '{}'; + } + var req = webApiService + .execute(init) + .catch((err) => { + this.$throw(0, err, endpoint); + }) + .then((response) => { + if (!response.data) { + return response; + } + try { + response.data = JSON.parse(response.data); + if ($app.debugWebRequests) { + console.log(init, response.data); + } + return response; + } catch (e) {} + if (response.status === 200) { + this.$throw(0, 'Invalid JSON response', endpoint); + } + if ( + response.status === 429 && + init.url.endsWith('/instances/groups') + ) { + $app.nextGroupInstanceRefresh = 120; // 1min + throw new Error( + `${response.status}: rate limited ${endpoint}` + ); + } + if (response.status === 504 || response.status === 502) { + // ignore expected API errors + throw new Error( + `${response.status}: ${response.data} ${endpoint}` + ); + } + this.$throw(response.status, endpoint); + return {}; + }) + .then(({ data, status }) => { + if (status === 200) { + if (!data) { + return data; + } + var text = ''; + if (data.success === Object(data.success)) { + text = data.success.message; + } else if (data.OK === String(data.OK)) { + text = data.OK; + } + if (text) { + new Noty({ + type: 'success', + text: $app.escapeTag(text) + }).show(); + } + return data; + } + if ( + status === 401 && + data.error.message === '"Missing Credentials"' + ) { + this.$emit('AUTOLOGIN'); + throw new Error('401: Missing Credentials'); + } + if ( + status === 401 && + data.error.message === '"Unauthorized"' && + endpoint !== 'auth/user' + ) { + // trigger 2FA dialog + if (!$app.twoFactorAuthDialogVisible) { + $app.API.getCurrentUser(); + } + throw new Error('401: Unauthorized'); + } + if (status === 403 && endpoint === 'config') { + $app.$alert( + 'VRChat currently blocks most VPNs. Please disable any connected VPNs and try again.', + 'Login Error 403' + ); + this.logout(); + throw new Error(`403: ${endpoint}`); + } + if ( + init.method === 'GET' && + status === 404 && + endpoint.startsWith('avatars/') + ) { + $app.$message({ + message: 'Avatar private or deleted', + type: 'error' + }); + $app.avatarDialog.visible = false; + throw new Error( + `404: ${data.error.message} ${endpoint}` + ); + } + if ( + status === 404 && + endpoint.endsWith('/persist/exists') + ) { + return false; + } + if ( + init.method === 'GET' && + (status === 404 || status === 403) && + !endpoint.startsWith('auth/user') + ) { + this.failedGetRequests.set(endpoint, Date.now()); + } + if ( + init.method === 'GET' && + status === 404 && + endpoint.startsWith('users/') && + endpoint.split('/').length - 1 === 1 + ) { + throw new Error( + `404: ${data.error.message} ${endpoint}` + ); + } + if ( + status === 404 && + endpoint.startsWith('invite/') && + init.inviteId + ) { + this.expireNotification(init.inviteId); + } + if ( + status === 403 && + endpoint.startsWith('invite/myself/to/') + ) { + throw new Error( + `403: ${data.error.message} ${endpoint}` + ); + } + if (data && data.error === Object(data.error)) { + this.$throw( + data.error.status_code || status, + data.error.message, + endpoint + ); + } else if (data && typeof data.error === 'string') { + this.$throw( + data.status_code || status, + data.error, + endpoint + ); + } + this.$throw(status, data, endpoint); + return data; + }); + if (init.method === 'GET') { + req.finally(() => { + this.pendingGetRequests.delete(init.url); + }); + this.pendingGetRequests.set(init.url, { + req, + time: Date.now() + }); + } + return req; + }; + + // FIXME : extra를 없애줘 + API.$throw = function (code, error, endpoint) { + var text = []; + if (code > 0) { + var status = this.statusCodes[code]; + if (typeof status === 'undefined') { + text.push(`${code}`); + } else { + text.push(`${code} ${status}`); + } + } + if (typeof error !== 'undefined') { + text.push(JSON.stringify(error)); + } + if (typeof endpoint !== 'undefined') { + text.push(JSON.stringify(endpoint)); + } + text = text.map((s) => $app.escapeTag(s)).join('
'); + if (text.length) { + if (this.errorNoty) { + this.errorNoty.close(); + } + this.errorNoty = new Noty({ + type: 'error', + text + }).show(); + } + throw new Error(text); + }; + + API.$bulk = function (options, args) { + if ('handle' in options) { + options.handle.call(this, args, options); + } + if ( + args.json.length > 0 && + ((options.params.offset += args.json.length), + // eslint-disable-next-line no-nested-ternary + options.N > 0 + ? options.N > options.params.offset + : options.N < 0 + ? args.json.length + : options.params.n === args.json.length) + ) { + this.bulk(options); + } else if ('done' in options) { + options.done.call(this, true, options); + } + return args; + }; + + API.bulk = function (options) { + this[options.fn](options.params) + .catch((err) => { + if ('done' in options) { + options.done.call(this, false, options); + } + throw err; + }) + .then((args) => this.$bulk(options, args)); + }; + + API.statusCodes = { + 100: 'Continue', + 101: 'Switching Protocols', + 102: 'Processing', + 103: 'Early Hints', + 200: 'OK', + 201: 'Created', + 202: 'Accepted', + 203: 'Non-Authoritative Information', + 204: 'No Content', + 205: 'Reset Content', + 206: 'Partial Content', + 207: 'Multi-Status', + 208: 'Already Reported', + 226: 'IM Used', + 300: 'Multiple Choices', + 301: 'Moved Permanently', + 302: 'Found', + 303: 'See Other', + 304: 'Not Modified', + 305: 'Use Proxy', + 306: 'Switch Proxy', + 307: 'Temporary Redirect', + 308: 'Permanent Redirect', + 400: 'Bad Request', + 401: 'Unauthorized', + 402: 'Payment Required', + 403: 'Forbidden', + 404: 'Not Found', + 405: 'Method Not Allowed', + 406: 'Not Acceptable', + 407: 'Proxy Authentication Required', + 408: 'Request Timeout', + 409: 'Conflict', + 410: 'Gone', + 411: 'Length Required', + 412: 'Precondition Failed', + 413: 'Payload Too Large', + 414: 'URI Too Long', + 415: 'Unsupported Media Type', + 416: 'Range Not Satisfiable', + 417: 'Expectation Failed', + 418: "I'm a teapot", + 421: 'Misdirected Request', + 422: 'Unprocessable Entity', + 423: 'Locked', + 424: 'Failed Dependency', + 425: 'Too Early', + 426: 'Upgrade Required', + 428: 'Precondition Required', + 429: 'Too Many Requests', + 431: 'Request Header Fields Too Large', + 451: 'Unavailable For Legal Reasons', + 500: 'Internal Server Error', + 501: 'Not Implemented', + 502: 'Bad Gateway', + 503: 'Service Unavailable', + 504: 'Gateway Timeout', + 505: 'HTTP Version Not Supported', + 506: 'Variant Also Negotiates', + 507: 'Insufficient Storage', + 508: 'Loop Detected', + 510: 'Not Extended', + 511: 'Network Authentication Required', + // CloudFlare Error + 520: 'Web server returns an unknown error', + 521: 'Web server is down', + 522: 'Connection timed out', + 523: 'Origin is unreachable', + 524: 'A timeout occurred', + 525: 'SSL handshake failed', + 526: 'Invalid SSL certificate', + 527: 'Railgun Listener to origin error' + }; + } +} diff --git a/html/src/classes/baseClass.js b/html/src/classes/baseClass.js new file mode 100644 index 000000000..9bc2ccfb4 --- /dev/null +++ b/html/src/classes/baseClass.js @@ -0,0 +1,29 @@ +import _utils from './utils'; +let $utils = new _utils().$utils; +/* eslint-disable no-unused-vars */ +let $app = {}; +let API = {}; +let $t = {}; +/* eslint-enable no-unused-vars */ + +class baseClass { + constructor(_app, _API, _t) { + $app = _app; + API = _API; + $t = _t; + + this.init(); + } + + updateRef(_app) { + $app = _app; + } + + init() {} + + _data = {}; + + _methods = {}; +} + +export { baseClass, $app, API, $t, $utils }; diff --git a/html/src/classes/booping.js b/html/src/classes/booping.js new file mode 100644 index 000000000..4a34343aa --- /dev/null +++ b/html/src/classes/booping.js @@ -0,0 +1,103 @@ +import { baseClass, $app, API, $t, $utils } from './baseClass.js'; + +export default class extends baseClass { + constructor(_app, _API, _t) { + super(_app, _API, _t); + } + + init() { + /** + * @params {{ + userId: string, + emojiId: string + }} params + * @returns {Promise<{json: any, params}>} + */ + API.sendBoop = function (params) { + return this.call(`users/${params.userId}/boop`, { + method: 'POST', + params + }).then((json) => { + var args = { + json, + params + }; + this.$emit('BOOP:SEND', args); + return args; + }); + }; + } + + _data = { + sendBoopDialog: { + visible: false, + userId: '', + fileId: '' + } + }; + + _methods = { + sendBoop() { + var D = this.sendBoopDialog; + this.dismissBoop(D.userId); + var params = { + userId: D.userId + }; + if (D.fileId) { + params.emojiId = D.fileId; + } + API.sendBoop(params); + D.visible = false; + }, + + dismissBoop(userId) { + // JANK: This is a hack to remove boop notifications when responding + var array = this.notificationTable.data; + for (var i = array.length - 1; i >= 0; i--) { + var ref = array[i]; + if ( + ref.type !== 'boop' || + ref.$isExpired || + ref.senderUserId !== userId + ) { + continue; + } + API.sendNotificationResponse({ + notificationId: ref.id, + responseType: 'delete', + responseData: '' + }); + } + }, + + showSendBoopDialog(userId) { + this.$nextTick(() => + $app.adjustDialogZ(this.$refs.sendBoopDialog.$el) + ); + var D = this.sendBoopDialog; + D.userId = userId; + D.visible = true; + if (this.emojiTable.length === 0 && API.currentUser.$isVRCPlus) { + this.refreshEmojiTable(); + } + }, + + getEmojiValue(emojiName) { + if (!emojiName) { + return ''; + } + return `vrchat_${emojiName.replace(/ /g, '_').toLowerCase()}`; + }, + + getEmojiName(emojiValue) { + // uppercase first letter of each word + if (!emojiValue) { + return ''; + } + return emojiValue + .replace('vrchat_', '') + .replace(/_/g, ' ') + .replace(/\b\w/g, (l) => l.toUpperCase()); + } + }; +} diff --git a/html/src/classes/currentUser.js b/html/src/classes/currentUser.js new file mode 100644 index 000000000..759e3d405 --- /dev/null +++ b/html/src/classes/currentUser.js @@ -0,0 +1,339 @@ +import { baseClass, $app, API, $t, $utils } from './baseClass.js'; + +export default class extends baseClass { + constructor(_app, _API, _t) { + super(_app, _API, _t); + } + + init() { + API.currentUser = { + $userColour: '' + }; + + API.getCurrentUser = function () { + return this.call('auth/user', { + method: 'GET' + }).then((json) => { + var args = { + json, + origin: true + }; + if ( + json.requiresTwoFactorAuth && + json.requiresTwoFactorAuth.includes('emailOtp') + ) { + this.$emit('USER:EMAILOTP', args); + } else if (json.requiresTwoFactorAuth) { + this.$emit('USER:2FA', args); + } else { + if ($app.debugCurrentUserDiff) { + var ref = args.json; + var $ref = this.currentUser; + var props = {}; + for (var prop in $ref) { + if ($ref[prop] !== Object($ref[prop])) { + props[prop] = true; + } + } + 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 { + if ( + prop.startsWith('$') || + prop === 'offlineFriends' || + prop === 'onlineFriends' || + prop === 'activeFriends' + ) { + delete props[prop]; + continue; + } + props[prop] = [tobe, asis]; + has = true; + } + } + if (has) { + console.log('API.getCurrentUser diff', props); + } + } + $app.nextCurrentUserRefresh = 420; // 7mins + this.$emit('USER:CURRENT', args); + } + return args; + }); + }; + + API.$on('USER:CURRENT', function (args) { + var { json } = args; + args.ref = this.applyCurrentUser(json); + + // when isGameRunning use gameLog instead of API + var $location = $app.parseLocation($app.lastLocation.location); + var $travelingLocation = $app.parseLocation( + $app.lastLocationDestination + ); + var location = $app.lastLocation.location; + var instanceId = $location.instanceId; + var worldId = $location.worldId; + var travelingToLocation = $app.lastLocationDestination; + var travelingToWorld = $travelingLocation.worldId; + var travelingToInstance = $travelingLocation.instanceId; + if (!$app.isGameRunning && json.presence) { + if ($app.isRealInstance(json.presence.world)) { + location = `${json.presence.world}:${json.presence.instance}`; + travelingToLocation = `${json.presence.travelingToWorld}:${json.presence.travelingToInstance}`; + } else { + location = json.presence.world; + travelingToLocation = json.presence.travelingToWorld; + } + instanceId = json.presence.instance; + worldId = json.presence.world; + travelingToInstance = json.presence.travelingToInstance; + travelingToWorld = json.presence.travelingToWorld; + } + + this.applyUser({ + allowAvatarCopying: json.allowAvatarCopying, + badges: json.badges, + bio: json.bio, + bioLinks: json.bioLinks, + currentAvatarImageUrl: json.currentAvatarImageUrl, + currentAvatarTags: json.currentAvatarTags, + currentAvatarThumbnailImageUrl: + json.currentAvatarThumbnailImageUrl, + date_joined: json.date_joined, + developerType: json.developerType, + displayName: json.displayName, + friendKey: json.friendKey, + // json.friendRequestStatus - missing from currentUser + id: json.id, + // instanceId - missing from currentUser + isFriend: json.isFriend, + last_activity: json.last_activity, + last_login: json.last_login, + last_mobile: json.last_mobile, + last_platform: json.last_platform, + // location - missing from currentUser + // platform - missing from currentUser + // note - missing from currentUser + profilePicOverride: json.profilePicOverride, + // profilePicOverrideThumbnail - missing from currentUser + pronouns: json.pronouns, + state: json.state, + status: json.status, + statusDescription: json.statusDescription, + tags: json.tags, + // travelingToInstance - missing from currentUser + // travelingToLocation - missing from currentUser + // travelingToWorld - missing from currentUser + userIcon: json.userIcon, + // worldId - missing from currentUser + fallbackAvatar: json.fallbackAvatar, + + // Location from gameLog/presence + location, + instanceId, + worldId, + travelingToLocation, + travelingToInstance, + travelingToWorld, + + // set VRCX online/offline timers + $online_for: this.currentUser.$online_for, + $offline_for: this.currentUser.$offline_for, + $location_at: this.currentUser.$location_at, + $travelingToTime: this.currentUser.$travelingToTime + }); + }); + + API.applyCurrentUser = function (json) { + var ref = this.currentUser; + if (this.isLoggedIn) { + if (json.currentAvatar !== ref.currentAvatar) { + $app.addAvatarToHistory(json.currentAvatar); + } + Object.assign(ref, json); + if (ref.homeLocation !== ref.$homeLocation.tag) { + ref.$homeLocation = $app.parseLocation(ref.homeLocation); + // apply home location name to user dialog + if ( + $app.userDialog.visible && + $app.userDialog.id === ref.id + ) { + $app.getWorldName(API.currentUser.homeLocation).then( + (worldName) => { + $app.userDialog.$homeLocationName = worldName; + } + ); + } + } + ref.$isVRCPlus = ref.tags.includes('system_supporter'); + this.applyUserTrustLevel(ref); + this.applyUserLanguage(ref); + this.applyPresenceLocation(ref); + this.applyQueuedInstance(ref.queuedInstance); + this.applyPresenceGroups(ref); + } else { + ref = { + acceptedPrivacyVersion: 0, + acceptedTOSVersion: 0, + accountDeletionDate: null, + accountDeletionLog: null, + activeFriends: [], + allowAvatarCopying: false, + badges: [], + bio: '', + bioLinks: [], + currentAvatar: '', + currentAvatarAssetUrl: '', + currentAvatarImageUrl: '', + currentAvatarTags: [], + currentAvatarThumbnailImageUrl: '', + date_joined: '', + developerType: '', + displayName: '', + emailVerified: false, + fallbackAvatar: '', + friendGroupNames: [], + friendKey: '', + friends: [], + googleId: '', + hasBirthday: false, + hasEmail: false, + hasLoggedInFromClient: false, + hasPendingEmail: false, + hideContentFilterSettings: false, + homeLocation: '', + id: '', + isBoopingEnabled: false, + isFriend: false, + last_activity: '', + last_login: '', + last_mobile: null, + last_platform: '', + obfuscatedEmail: '', + obfuscatedPendingEmail: '', + oculusId: '', + offlineFriends: [], + onlineFriends: [], + pastDisplayNames: [], + picoId: '', + presence: { + avatarThumbnail: '', + currentAvatarTags: '', + displayName: '', + groups: [], + id: '', + instance: '', + instanceType: '', + platform: '', + profilePicOverride: '', + status: '', + travelingToInstance: '', + travelingToWorld: '', + userIcon: '', + world: '', + ...json.presence + }, + profilePicOverride: '', + pronouns: '', + queuedInstance: '', + state: '', + status: '', + statusDescription: '', + statusFirstTime: false, + statusHistory: [], + steamDetails: {}, + steamId: '', + tags: [], + twoFactorAuthEnabled: false, + twoFactorAuthEnabledDate: null, + unsubscribe: false, + updated_at: '', + userIcon: '', + userLanguage: '', + userLanguageCode: '', + username: '', + viveId: '', + // VRCX + $online_for: Date.now(), + $offline_for: '', + $location_at: Date.now(), + $travelingToTime: Date.now(), + $homeLocation: {}, + $isVRCPlus: false, + $isModerator: false, + $isTroll: false, + $isProbableTroll: false, + $trustLevel: 'Visitor', + $trustClass: 'x-tag-untrusted', + $userColour: '', + $trustSortNum: 1, + $languages: [], + $locationTag: '', + $travelingToLocation: '', + $vbucks: null, + ...json + }; + ref.$homeLocation = $app.parseLocation(ref.homeLocation); + ref.$isVRCPlus = ref.tags.includes('system_supporter'); + this.applyUserTrustLevel(ref); + this.applyUserLanguage(ref); + this.applyPresenceLocation(ref); + this.applyPresenceGroups(ref); + this.currentUser = ref; + this.isLoggedIn = true; + this.$emit('LOGIN', { + json, + ref + }); + } + return ref; + }; + + /** + * @typedef {{ + * status: 'active' | 'offline' | 'busy' | 'ask me' | 'join me', + * statusDescription: string + * }} SaveCurrentUserParameters + */ + + /** + * Updates current user's status. + * @param params {SaveCurrentUserParameters} new status to be set + * @returns {Promise<{json: any, params}>} + */ + API.saveCurrentUser = function (params) { + return this.call(`users/${this.currentUser.id}`, { + method: 'PUT', + params + }).then((json) => { + var args = { + json, + params + }; + this.$emit('USER:CURRENT:SAVE', args); + return args; + }); + }; + } + + _data = {}; + + _methods = {}; +} diff --git a/html/src/classes/discordRpc.js b/html/src/classes/discordRpc.js new file mode 100644 index 000000000..f3a66b5a8 --- /dev/null +++ b/html/src/classes/discordRpc.js @@ -0,0 +1,263 @@ +import configRepository from '../repository/config.js'; +import { baseClass, $app, API, $t, $utils } from './baseClass.js'; + +export default class extends baseClass { + constructor(_app, _API, _t) { + super(_app, _API, _t); + } + + _data = { + isDiscordActive: false, + discordActive: false, + discordInstance: true, + discordJoinButton: false, + discordHideInvite: true, + discordHideImage: false + }; + + _methods = { + updateDiscord() { + var currentLocation = this.lastLocation.location; + var timeStamp = this.lastLocation.date; + if (this.lastLocation.location === 'traveling') { + currentLocation = this.lastLocationDestination; + timeStamp = this.lastLocationDestinationTime; + } + if ( + !this.discordActive || + !this.isGameRunning || + (!currentLocation && !this.lastLocation$.tag) + ) { + this.setDiscordActive(false); + return; + } + this.setDiscordActive(true); + var L = this.lastLocation$; + if (currentLocation !== this.lastLocation$.tag) { + Discord.SetTimestamps(timeStamp, 0); + L = $app.parseLocation(currentLocation); + L.worldName = ''; + L.thumbnailImageUrl = ''; + L.worldCapacity = 0; + L.joinUrl = ''; + L.accessName = ''; + if (L.worldId) { + var ref = API.cachedWorlds.get(L.worldId); + if (ref) { + L.worldName = ref.name; + L.thumbnailImageUrl = ref.thumbnailImageUrl; + L.worldCapacity = ref.capacity; + } else { + API.getWorld({ + worldId: L.worldId + }).then((args) => { + L.worldName = args.ref.name; + L.thumbnailImageUrl = args.ref.thumbnailImageUrl; + L.worldCapacity = args.ref.capacity; + return args; + }); + } + if (this.isGameNoVR) { + var platform = 'Desktop'; + } else { + var platform = 'VR'; + } + var groupAccessType = ''; + if (L.groupAccessType) { + if (L.groupAccessType === 'public') { + groupAccessType = 'Public'; + } else if (L.groupAccessType === 'plus') { + groupAccessType = 'Plus'; + } + } + switch (L.accessType) { + case 'public': + L.joinUrl = this.getLaunchURL(L); + L.accessName = `Public #${L.instanceName} (${platform})`; + break; + case 'invite+': + L.accessName = `Invite+ #${L.instanceName} (${platform})`; + break; + case 'invite': + L.accessName = `Invite #${L.instanceName} (${platform})`; + break; + case 'friends': + L.accessName = `Friends #${L.instanceName} (${platform})`; + break; + case 'friends+': + L.accessName = `Friends+ #${L.instanceName} (${platform})`; + break; + case 'group': + L.accessName = `Group #${L.instanceName} (${platform})`; + this.getGroupName(L.groupId).then((groupName) => { + if (groupName) { + L.accessName = `Group${groupAccessType}(${groupName}) #${L.instanceName} (${platform})`; + } + }); + break; + } + } + this.lastLocation$ = L; + } + var hidePrivate = false; + if ( + this.discordHideInvite && + (L.accessType === 'invite' || + L.accessType === 'invite+' || + L.groupAccessType === 'members') + ) { + hidePrivate = true; + } + switch (API.currentUser.status) { + case 'active': + L.statusName = 'Online'; + L.statusImage = 'active'; + break; + case 'join me': + L.statusName = 'Join Me'; + L.statusImage = 'joinme'; + break; + case 'ask me': + L.statusName = 'Ask Me'; + L.statusImage = 'askme'; + if (this.discordHideInvite) { + hidePrivate = true; + } + break; + case 'busy': + L.statusName = 'Do Not Disturb'; + L.statusImage = 'busy'; + hidePrivate = true; + break; + } + var appId = '883308884863901717'; + var bigIcon = 'vrchat'; + var partyId = `${L.worldId}:${L.instanceName}`; + var partySize = this.lastLocation.playerList.size; + var partyMaxSize = L.worldCapacity; + if (partySize > partyMaxSize) { + partyMaxSize = partySize; + } + var buttonText = 'Join'; + var buttonUrl = L.joinUrl; + if (!this.discordJoinButton) { + buttonText = ''; + buttonUrl = ''; + } + if (!this.discordInstance) { + partySize = 0; + partyMaxSize = 0; + } + if (hidePrivate) { + partyId = ''; + partySize = 0; + partyMaxSize = 0; + buttonText = ''; + buttonUrl = ''; + } else if (this.isRpcWorld(L.tag)) { + // custom world rpc + if ( + L.worldId === 'wrld_f20326da-f1ac-45fc-a062-609723b097b1' || + L.worldId === 'wrld_10e5e467-fc65-42ed-8957-f02cace1398c' || + L.worldId === 'wrld_04899f23-e182-4a8d-b2c7-2c74c7c15534' + ) { + appId = '784094509008551956'; + bigIcon = 'pypy'; + } else if ( + L.worldId === 'wrld_42377cf1-c54f-45ed-8996-5875b0573a83' || + L.worldId === 'wrld_dd6d2888-dbdc-47c2-bc98-3d631b2acd7c' + ) { + appId = '846232616054030376'; + bigIcon = 'vr_dancing'; + } else if ( + L.worldId === 'wrld_52bdcdab-11cd-4325-9655-0fb120846945' || + L.worldId === 'wrld_2d40da63-8f1f-4011-8a9e-414eb8530acd' + ) { + appId = '939473404808007731'; + bigIcon = 'zuwa_zuwa_dance'; + } else if ( + L.worldId === 'wrld_74970324-58e8-4239-a17b-2c59dfdf00db' || + L.worldId === 'wrld_db9d878f-6e76-4776-8bf2-15bcdd7fc445' || + L.worldId === 'wrld_435bbf25-f34f-4b8b-82c6-cd809057eb8e' || + L.worldId === 'wrld_f767d1c8-b249-4ecc-a56f-614e433682c8' + ) { + appId = '968292722391785512'; + bigIcon = 'ls_media'; + } else if ( + L.worldId === 'wrld_266523e8-9161-40da-acd0-6bd82e075833' + ) { + appId = '1095440531821170820'; + bigIcon = 'movie_and_chill'; + } + if (this.nowPlaying.name) { + L.worldName = this.nowPlaying.name; + } + if (this.nowPlaying.playing) { + Discord.SetTimestamps( + Date.now(), + (this.nowPlaying.startTime - + this.nowPlaying.offset + + this.nowPlaying.length) * + 1000 + ); + } + } else if (!this.discordHideImage && L.thumbnailImageUrl) { + bigIcon = L.thumbnailImageUrl; + } + Discord.SetAssets( + bigIcon, // big icon + 'Powered by VRCX', // big icon hover text + L.statusImage, // small icon + L.statusName, // small icon hover text + partyId, // party id + partySize, // party size + partyMaxSize, // party max size + buttonText, // button text + buttonUrl, // button url + appId // app id + ); + // NOTE + // 글자 수가 짧으면 업데이트가 안된다.. + if (L.worldName.length < 2) { + L.worldName += '\uFFA0'.repeat(2 - L.worldName.length); + } + if (hidePrivate) { + Discord.SetText('Private', ''); + Discord.SetTimestamps(0, 0); + } else if (this.discordInstance) { + Discord.SetText(L.worldName, L.accessName); + } else { + Discord.SetText(L.worldName, ''); + } + }, + + async setDiscordActive(active) { + if (active !== this.isDiscordActive) { + this.isDiscordActive = await Discord.SetActive(active); + } + }, + + async saveDiscordOption() { + await configRepository.setBool('discordActive', this.discordActive); + await configRepository.setBool( + 'discordInstance', + this.discordInstance + ); + await configRepository.setBool( + 'discordJoinButton', + this.discordJoinButton + ); + await configRepository.setBool( + 'discordHideInvite', + this.discordHideInvite + ); + await configRepository.setBool( + 'discordHideImage', + this.discordHideImage + ); + this.lastLocation$.tag = ''; + this.nextDiscordUpdate = 3; + this.updateDiscord(); + } + }; +} diff --git a/html/src/classes/feed.js b/html/src/classes/feed.js new file mode 100644 index 000000000..0f6d79cf7 --- /dev/null +++ b/html/src/classes/feed.js @@ -0,0 +1,178 @@ +import { baseClass, $app, API, $t, $utils } from './baseClass.js'; +import configRepository from '../repository/config.js'; + +export default class extends baseClass { + constructor(_app, _API, _t) { + super(_app, _API, _t); + } + + _data = { + feedTable: { + data: [], + search: '', + vip: false, + loading: false, + filter: [], + tableProps: { + stripe: true, + size: 'mini', + defaultSort: { + prop: 'created_at', + order: 'descending' + } + }, + pageSize: 15, + paginationProps: { + small: true, + layout: 'sizes,prev,pager,next,total', + pageSizes: [10, 15, 25, 50, 100] + } + }, + + feedSessionTable: [] + }; + + _methods = { + feedSearch(row) { + var value = this.feedTable.search.toUpperCase(); + if (!value) { + return true; + } + if ( + value.startsWith('wrld_') && + String(row.location).toUpperCase().includes(value) + ) { + return true; + } + switch (row.type) { + case 'GPS': + if (String(row.displayName).toUpperCase().includes(value)) { + return true; + } + if (String(row.worldName).toUpperCase().includes(value)) { + return true; + } + return false; + case 'Online': + if (String(row.displayName).toUpperCase().includes(value)) { + return true; + } + if (String(row.worldName).toUpperCase().includes(value)) { + return true; + } + return false; + case 'Offline': + if (String(row.displayName).toUpperCase().includes(value)) { + return true; + } + if (String(row.worldName).toUpperCase().includes(value)) { + return true; + } + return false; + case 'Status': + if (String(row.displayName).toUpperCase().includes(value)) { + return true; + } + if (String(row.status).toUpperCase().includes(value)) { + return true; + } + if ( + String(row.statusDescription) + .toUpperCase() + .includes(value) + ) { + return true; + } + return false; + case 'Avatar': + if (String(row.displayName).toUpperCase().includes(value)) { + return true; + } + if (String(row.avatarName).toUpperCase().includes(value)) { + return true; + } + return false; + case 'Bio': + if (String(row.displayName).toUpperCase().includes(value)) { + return true; + } + if (String(row.bio).toUpperCase().includes(value)) { + return true; + } + if (String(row.previousBio).toUpperCase().includes(value)) { + return true; + } + return false; + } + return true; + }, + + async feedTableLookup() { + await configRepository.setString( + 'VRCX_feedTableFilters', + JSON.stringify(this.feedTable.filter) + ); + await configRepository.setBool( + 'VRCX_feedTableVIPFilter', + this.feedTable.vip + ); + this.feedTable.loading = true; + var vipList = []; + if (this.feedTable.vip) { + vipList = Array.from(this.localFavoriteFriends.values()); + } + this.feedTable.data = await database.lookupFeedDatabase( + this.feedTable.search, + this.feedTable.filter, + vipList + ); + this.feedTable.loading = false; + }, + + addFeed(feed) { + this.queueFeedNoty(feed); + this.feedSessionTable.push(feed); + this.updateSharedFeed(false); + if ( + this.feedTable.filter.length > 0 && + !this.feedTable.filter.includes(feed.type) + ) { + return; + } + if ( + this.feedTable.vip && + !this.localFavoriteFriends.has(feed.userId) + ) { + return; + } + if (!this.feedSearch(feed)) { + return; + } + this.feedTable.data.push(feed); + this.sweepFeed(); + this.notifyMenu('feed'); + }, + + sweepFeed() { + var { data } = this.feedTable; + var j = data.length; + if (j > this.maxTableSize) { + data.splice(0, j - this.maxTableSize); + } + + var date = new Date(); + date.setDate(date.getDate() - 1); // 24 hour limit + var limit = date.toJSON(); + var i = 0; + var k = this.feedSessionTable.length; + while (i < k && this.feedSessionTable[i].created_at < limit) { + ++i; + } + if (i === k) { + this.feedSessionTable = []; + } else if (i) { + this.feedSessionTable.splice(0, i); + } + } + }; +} diff --git a/html/src/classes/gameLog.js b/html/src/classes/gameLog.js new file mode 100644 index 000000000..43869fe4a --- /dev/null +++ b/html/src/classes/gameLog.js @@ -0,0 +1,1122 @@ +import * as workerTimers from 'worker-timers'; +import gameLogService from '../service/gamelog.js'; +import configRepository from '../repository/config.js'; +import { baseClass, $app, API, $t, $utils } from './baseClass.js'; + +export default class extends baseClass { + constructor(_app, _API, _t) { + super(_app, _API, _t); + } + + _data = { + gameLogTable: { + data: [], + loading: false, + search: '', + filter: [], + tableProps: { + stripe: true, + size: 'mini', + defaultSort: { + prop: 'created_at', + order: 'descending' + } + }, + pageSize: 15, + paginationProps: { + small: true, + layout: 'sizes,prev,pager,next,total', + pageSizes: [10, 15, 25, 50, 100] + } + }, + gameLogSessionTable: [], + gameLogApiLoggingEnabled: false, + lastVideoUrl: '', + lastResourceloadUrl: '' + }; + + _methods = { + addGameLogEntry(gameLog, location) { + if (this.gameLogDisabled) { + return; + } + var userId = gameLog.userId; + if (!userId && gameLog.displayName) { + for (var ref of API.cachedUsers.values()) { + if (ref.displayName === gameLog.displayName) { + userId = ref.id; + break; + } + } + } + switch (gameLog.type) { + case 'location-destination': + if (this.isGameRunning) { + // needs to be added before OnPlayerLeft entries from LocationReset + this.addGameLog({ + created_at: gameLog.dt, + type: 'LocationDestination', + location: gameLog.location + }); + this.lastLocationReset(gameLog.dt); + this.lastLocation.location = 'traveling'; + this.lastLocationDestination = gameLog.location; + this.lastLocationDestinationTime = Date.parse( + gameLog.dt + ); + this.removeQueuedInstance(gameLog.location); + this.updateCurrentUserLocation(); + this.clearNowPlaying(); + this.updateCurrentInstanceWorld(); + this.applyUserDialogLocation(); + this.applyWorldDialogInstances(); + this.applyGroupDialogInstances(); + } + break; + case 'location': + this.addInstanceJoinHistory( + this.lastLocation.location, + gameLog.dt + ); + var worldName = this.replaceBioSymbols(gameLog.worldName); + if (this.isGameRunning) { + this.lastLocationReset(gameLog.dt); + this.clearNowPlaying(); + this.lastLocation = { + date: Date.parse(gameLog.dt), + location: gameLog.location, + name: worldName, + playerList: new Map(), + friendList: new Map() + }; + this.removeQueuedInstance(gameLog.location); + this.updateCurrentUserLocation(); + this.updateVRLastLocation(); + this.updateCurrentInstanceWorld(); + this.applyUserDialogLocation(); + this.applyWorldDialogInstances(); + this.applyGroupDialogInstances(); + } + this.addInstanceJoinHistory(gameLog.location, gameLog.dt); + var L = $utils.parseLocation(gameLog.location); + var entry = { + created_at: gameLog.dt, + type: 'Location', + location: gameLog.location, + worldId: L.worldId, + worldName, + groupName: '', + time: 0 + }; + this.getGroupName(gameLog.location).then((groupName) => { + entry.groupName = groupName; + }); + this.addGamelogLocationToDatabase(entry); + break; + case 'player-joined': + var joinTime = Date.parse(gameLog.dt); + var userMap = { + displayName: gameLog.displayName, + userId, + joinTime, + lastAvatar: '' + }; + this.lastLocation.playerList.set( + gameLog.displayName, + userMap + ); + if (userId) { + var ref = API.cachedUsers.get(userId); + if (userId === API.currentUser.id) { + // skip + } else if (this.friends.has(userId)) { + this.lastLocation.friendList.set( + gameLog.displayName, + userMap + ); + if ( + ref.location !== this.lastLocation.location && + ref.travelingToLocation !== + this.lastLocation.location + ) { + // fix $location_at with private + ref.$location_at = joinTime; + } + } else if (typeof ref !== 'undefined') { + // set $location_at to join time if user isn't a friend + ref.$location_at = joinTime; + } else { + if (this.debugGameLog || this.debugWebRequests) { + console.log( + 'Fetching user from gameLog:', + userId + ); + } + API.getUser({ userId }); + } + } else { + // TODO: remove this + // try fetch userId from previous encounter using database + database + .getUserIdFromDisplayName(gameLog.displayName) + .then((oldUserId) => { + if (this.isGameRunning) { + if (oldUserId) { + API.getUser({ userId: oldUserId }); + } else if ( + Date.now() - joinTime < + 5 * 1000 + ) { + workerTimers.setTimeout( + () => + this.silentSearchUser( + gameLog.displayName + ), + 10 * 1000 + ); + } + } + }); + } + this.updateVRLastLocation(); + this.getCurrentInstanceUserList(); + var entry = { + created_at: gameLog.dt, + type: 'OnPlayerJoined', + displayName: gameLog.displayName, + location, + userId, + time: 0 + }; + database.addGamelogJoinLeaveToDatabase(entry); + break; + case 'player-left': + var ref = this.lastLocation.playerList.get( + gameLog.displayName + ); + if (typeof ref === 'undefined') { + break; + } + var time = Date.now() - ref.joinTime; + this.lastLocation.playerList.delete(gameLog.displayName); + this.lastLocation.friendList.delete(gameLog.displayName); + this.photonLobbyAvatars.delete(userId); + this.updateVRLastLocation(); + this.getCurrentInstanceUserList(); + var entry = { + created_at: gameLog.dt, + type: 'OnPlayerLeft', + displayName: gameLog.displayName, + location, + userId, + time + }; + database.addGamelogJoinLeaveToDatabase(entry); + break; + case 'portal-spawn': + if (this.ipcEnabled && this.isGameRunning) { + break; + } + var entry = { + created_at: gameLog.dt, + type: 'PortalSpawn', + location, + displayName: '', + userId: '', + instanceId: '', + worldName: '' + }; + database.addGamelogPortalSpawnToDatabase(entry); + break; + case 'video-play': + gameLog.videoUrl = decodeURI(gameLog.videoUrl); + if (this.lastVideoUrl === gameLog.videoUrl) { + break; + } + this.lastVideoUrl = gameLog.videoUrl; + this.addGameLogVideo(gameLog, location, userId); + break; + case 'video-sync': + var timestamp = gameLog.timestamp.replace(/,/g, ''); + if (this.nowPlaying.playing) { + this.nowPlaying.offset = parseInt(timestamp, 10); + } + break; + case 'resource-load-string': + case 'resource-load-image': + if ( + !this.logResourceLoad || + this.lastResourceloadUrl === gameLog.resourceUrl + ) { + break; + } + this.lastResourceloadUrl = gameLog.resourceUrl; + var entry = { + created_at: gameLog.dt, + type: + gameLog.type === 'resource-load-string' + ? 'StringLoad' + : 'ImageLoad', + resourceUrl: gameLog.resourceUrl, + location + }; + database.addGamelogResourceLoadToDatabase(entry); + break; + case 'screenshot': + // var entry = { + // created_at: gameLog.dt, + // type: 'Event', + // data: `Screenshot Processed: ${gameLog.screenshotPath.replace( + // /^.*[\\/]/, + // '' + // )}` + // }; + // database.addGamelogEventToDatabase(entry); + + this.processScreenshot(gameLog.screenshotPath); + break; + case 'api-request': + var bias = Date.parse(gameLog.dt) + 60 * 1000; + if ( + !this.isGameRunning || + this.lastLocation.location === '' || + this.lastLocation.location === 'traveling' || + bias < Date.now() + ) { + break; + } + var userId = ''; + try { + var url = new URL(gameLog.url); + var urlParams = new URLSearchParams(gameLog.url); + if (url.pathname.substring(0, 13) === '/api/1/users/') { + var pathArray = url.pathname.split('/'); + userId = pathArray[4]; + } else if (urlParams.has('userId')) { + userId = urlParams.get('userId'); + } + } catch (err) { + console.error(err); + } + if (!userId) { + break; + } + this.gameLogApiLoggingEnabled = true; + if ( + API.cachedUsers.has(userId) || + API.cachedPlayerModerationsUserIds.has(userId) + ) { + break; + } + if (this.debugGameLog || this.debugWebRequests) { + console.log('Fetching user from gameLog:', userId); + } + API.getUser({ userId }); + break; + case 'avatar-change': + var ref = this.lastLocation.playerList.get( + gameLog.displayName + ); + if ( + this.photonLoggingEnabled || + typeof ref === 'undefined' || + ref.lastAvatar === gameLog.avatarName + ) { + break; + } + if (!ref.lastAvatar) { + ref.lastAvatar = gameLog.avatarName; + this.lastLocation.playerList.set( + gameLog.displayName, + ref + ); + break; + } + ref.lastAvatar = gameLog.avatarName; + this.lastLocation.playerList.set(gameLog.displayName, ref); + var entry = { + created_at: gameLog.dt, + type: 'AvatarChange', + userId, + name: gameLog.avatarName, + displayName: gameLog.displayName + }; + break; + case 'vrcx': + // VideoPlay(PyPyDance) "https://jd.pypy.moe/api/v1/videos/jr1NX4Jo8GE.mp4",0.1001,239.606,"0905 : [J-POP] 【まなこ】金曜日のおはよう 踊ってみた (vernities)" + var type = gameLog.data.substr( + 0, + gameLog.data.indexOf(' ') + ); + if (type === 'VideoPlay(PyPyDance)') { + this.addGameLogPyPyDance(gameLog, location); + } else if (type === 'VideoPlay(VRDancing)') { + this.addGameLogVRDancing(gameLog, location); + } else if (type === 'VideoPlay(ZuwaZuwaDance)') { + this.addGameLogZuwaZuwaDance(gameLog, location); + } else if (type === 'LSMedia') { + this.addGameLogLSMedia(gameLog, location); + } else if (type === 'Movie&Chill') { + this.addGameLogMovieAndChill(gameLog, location); + } + break; + case 'photon-id': + if (!this.isGameRunning || !this.friendLogInitStatus) { + break; + } + var photonId = parseInt(gameLog.photonId, 10); + var ref = this.photonLobby.get(photonId); + if (typeof ref === 'undefined') { + for (var ctx of API.cachedUsers.values()) { + if (ctx.displayName === gameLog.displayName) { + this.photonLobby.set(photonId, ctx); + this.photonLobbyCurrent.set(photonId, ctx); + break; + } + } + var ctx = { + displayName: gameLog.displayName + }; + this.photonLobby.set(photonId, ctx); + this.photonLobbyCurrent.set(photonId, ctx); + this.getCurrentInstanceUserList(); + } + break; + case 'notification': + // var entry = { + // created_at: gameLog.dt, + // type: 'Notification', + // data: gameLog.json + // }; + break; + case 'event': + var entry = { + created_at: gameLog.dt, + type: 'Event', + data: gameLog.event + }; + database.addGamelogEventToDatabase(entry); + break; + case 'vrc-quit': + if (!this.isGameRunning) { + break; + } + if (this.vrcQuitFix) { + var bias = Date.parse(gameLog.dt) + 3000; + if (bias < Date.now()) { + console.log( + 'QuitFix: Bias too low, not killing VRC' + ); + break; + } + AppApi.QuitGame().then((processCount) => { + if (processCount > 1) { + console.log( + 'QuitFix: More than 1 process running, not killing VRC' + ); + } else if (processCount === 1) { + console.log('QuitFix: Killed VRC'); + } else { + console.log( + 'QuitFix: Nothing to kill, no VRC process running' + ); + } + }); + } + break; + case 'openvr-init': + this.isGameNoVR = false; + configRepository.setBool('isGameNoVR', this.isGameNoVR); + this.updateOpenVR(); + break; + case 'desktop-mode': + this.isGameNoVR = true; + configRepository.setBool('isGameNoVR', this.isGameNoVR); + this.updateOpenVR(); + break; + case 'udon-exception': + if (this.udonExceptionLogging) { + console.log('UdonException', gameLog.data); + } + // var entry = { + // created_at: gameLog.dt, + // type: 'Event', + // data: gameLog.data + // }; + // database.addGamelogEventToDatabase(entry); + break; + } + if (entry) { + // add tag colour + if (entry.userId) { + var tagRef = this.customUserTags.get(entry.userId); + if (typeof tagRef !== 'undefined') { + entry.tagColour = tagRef.colour; + } + } + this.queueGameLogNoty(entry); + this.addGameLog(entry); + } + }, + + addGameLog(entry) { + this.gameLogSessionTable.push(entry); + this.updateSharedFeed(false); + if (entry.type === 'VideoPlay') { + // event time can be before last gameLog entry + this.updateSharedFeed(true); + } + if ( + entry.type === 'LocationDestination' || + entry.type === 'AvatarChange' || + entry.type === 'ChatBoxMessage' || + (entry.userId === API.currentUser.id && + (entry.type === 'OnPlayerJoined' || + entry.type === 'OnPlayerLeft')) + ) { + return; + } + if ( + this.gameLogTable.filter.length > 0 && + !this.gameLogTable.filter.includes(entry.type) + ) { + return; + } + if (!this.gameLogSearch(entry)) { + return; + } + this.gameLogTable.data.push(entry); + this.sweepGameLog(); + this.notifyMenu('gameLog'); + }, + + async addGamelogLocationToDatabase(input) { + var groupName = await this.getGroupName(input.location); + var entry = { + ...input, + groupName + }; + database.addGamelogLocationToDatabase(entry); + }, + + async addGameLogVideo(gameLog, location, userId) { + var videoUrl = gameLog.videoUrl; + var youtubeVideoId = ''; + var videoId = ''; + var videoName = ''; + var videoLength = ''; + var displayName = ''; + var videoPos = 8; // video loading delay + if (typeof gameLog.displayName !== 'undefined') { + displayName = gameLog.displayName; + } + if (typeof gameLog.videoPos !== 'undefined') { + videoPos = gameLog.videoPos; + } + if (!this.isRpcWorld(location) || gameLog.videoId === 'YouTube') { + // skip PyPyDance and VRDancing videos + try { + var url = new URL(videoUrl); + if ( + url.origin === 'https://t-ne.x0.to' || + url.origin === 'https://nextnex.com' || + url.origin === 'https://r.0cm.org' + ) { + url = new URL(url.searchParams.get('url')); + } + if (videoUrl.startsWith('https://u2b.cx/')) { + url = new URL(videoUrl.substring(15)); + } + var id1 = url.pathname; + var id2 = url.searchParams.get('v'); + if (id1 && id1.length === 12) { + // https://youtu.be/ + youtubeVideoId = id1.substring(1, 12); + } + if (id1 && id1.length === 19) { + // https://www.youtube.com/shorts/ + youtubeVideoId = id1.substring(8, 19); + } + if (id2 && id2.length === 11) { + // https://www.youtube.com/watch?v= + // https://music.youtube.com/watch?v= + youtubeVideoId = id2; + } + if (this.youTubeApi && youtubeVideoId) { + var data = + await this.lookupYouTubeVideo(youtubeVideoId); + if (data || data.pageInfo.totalResults !== 0) { + videoId = 'YouTube'; + videoName = data.items[0].snippet.title; + videoLength = this.convertYoutubeTime( + data.items[0].contentDetails.duration + ); + } + } + } catch { + console.error(`Invalid URL: ${url}`); + } + var entry = { + created_at: gameLog.dt, + type: 'VideoPlay', + videoUrl, + videoId, + videoName, + videoLength, + location, + displayName, + userId, + videoPos + }; + this.setNowPlaying(entry); + } + }, + + addGameLogPyPyDance(gameLog, location) { + var data = + /VideoPlay\(PyPyDance\) "(.+?)",([\d.]+),([\d.]+),"(.*)"/g.exec( + gameLog.data + ); + if (!data) { + console.error('failed to parse', gameLog.data); + return; + } + var videoUrl = data[1]; + var videoPos = Number(data[2]); + var videoLength = Number(data[3]); + var title = data[4]; + var bracketArray = title.split('('); + var text1 = bracketArray.pop(); + var displayName = text1.slice(0, -1); + var text2 = bracketArray.join('('); + if (text2 === 'Custom URL') { + var videoId = 'YouTube'; + } else { + var videoId = text2.substr(0, text2.indexOf(':') - 1); + text2 = text2.substr(text2.indexOf(':') + 2); + } + var videoName = text2.slice(0, -1); + if (displayName === 'Random') { + displayName = ''; + } + if (videoUrl === this.nowPlaying.url) { + var entry = { + created_at: gameLog.dt, + videoUrl, + videoLength, + videoPos + }; + this.setNowPlaying(entry); + return; + } + var userId = ''; + if (displayName) { + for (var ref of API.cachedUsers.values()) { + if (ref.displayName === displayName) { + userId = ref.id; + break; + } + } + } + if (videoId === 'YouTube') { + var entry = { + dt: gameLog.dt, + videoUrl, + displayName, + videoPos, + videoId + }; + this.addGameLogVideo(entry, location, userId); + } else { + var entry = { + created_at: gameLog.dt, + type: 'VideoPlay', + videoUrl, + videoId, + videoName, + videoLength, + location, + displayName, + userId, + videoPos + }; + this.setNowPlaying(entry); + } + }, + + addGameLogVRDancing(gameLog, location) { + var data = + /VideoPlay\(VRDancing\) "(.+?)",([\d.]+),([\d.]+),(-?[\d.]+),"(.+?)","(.+?)"/g.exec( + gameLog.data + ); + if (!data) { + console.error('failed to parse', gameLog.data); + return; + } + var videoUrl = data[1]; + var videoPos = Number(data[2]); + var videoLength = Number(data[3]); + var videoId = Number(data[4]); + var displayName = data[5]; + var videoName = data[6]; + if (videoId === -1) { + videoId = 'YouTube'; + } + if (parseInt(videoPos, 10) === parseInt(videoLength, 10)) { + // ummm okay + videoPos = 0; + } + if (videoUrl === this.nowPlaying.url) { + var entry = { + created_at: gameLog.dt, + videoUrl, + videoLength, + videoPos + }; + this.setNowPlaying(entry); + return; + } + var userId = ''; + if (displayName) { + for (var ref of API.cachedUsers.values()) { + if (ref.displayName === displayName) { + userId = ref.id; + break; + } + } + } + if (videoId === 'YouTube') { + var entry = { + dt: gameLog.dt, + videoUrl, + displayName, + videoPos, + videoId + }; + this.addGameLogVideo(entry, location, userId); + } else { + var entry = { + created_at: gameLog.dt, + type: 'VideoPlay', + videoUrl, + videoId, + videoName, + videoLength, + location, + displayName, + userId, + videoPos + }; + this.setNowPlaying(entry); + } + }, + + addGameLogZuwaZuwaDance(gameLog, location) { + var data = + /VideoPlay\(ZuwaZuwaDance\) "(.+?)",([\d.]+),([\d.]+),(-?[\d.]+),"(.+?)","(.+?)"/g.exec( + gameLog.data + ); + if (!data) { + console.error('failed to parse', gameLog.data); + return; + } + var videoUrl = data[1]; + var videoPos = Number(data[2]); + var videoLength = Number(data[3]); + var videoId = Number(data[4]); + var displayName = data[5]; + var videoName = data[6]; + if (displayName === 'Random') { + displayName = ''; + } + if (videoId === 9999) { + videoId = 'YouTube'; + } + if (videoUrl === this.nowPlaying.url) { + var entry = { + created_at: gameLog.dt, + videoUrl, + videoLength, + videoPos + }; + this.setNowPlaying(entry); + return; + } + var userId = ''; + if (displayName) { + for (var ref of API.cachedUsers.values()) { + if (ref.displayName === displayName) { + userId = ref.id; + break; + } + } + } + if (videoId === 'YouTube') { + var entry = { + dt: gameLog.dt, + videoUrl, + displayName, + videoPos, + videoId + }; + this.addGameLogVideo(entry, location, userId); + } else { + var entry = { + created_at: gameLog.dt, + type: 'VideoPlay', + videoUrl, + videoId, + videoName, + videoLength, + location, + displayName, + userId, + videoPos + }; + this.setNowPlaying(entry); + } + }, + + addGameLogLSMedia(gameLog, location) { + // [VRCX] LSMedia 0,4268.981,Natsumi-sama,, + // [VRCX] LSMedia 0,6298.292,Natsumi-sama,The Outfit (2022), 1080p + var data = /LSMedia ([\d.]+),([\d.]+),(.+?),(.+?),(?=[^,]*$)/g.exec( + gameLog.data + ); + if (!data) { + return; + } + var videoPos = Number(data[1]); + var videoLength = Number(data[2]); + var displayName = data[3]; + var videoName = this.replaceBioSymbols(data[4]); + var videoUrl = videoName; + var videoId = 'LSMedia'; + if (videoUrl === this.nowPlaying.url) { + var entry = { + created_at: gameLog.dt, + videoUrl, + videoLength, + videoPos + }; + this.setNowPlaying(entry); + return; + } + var userId = ''; + if (displayName) { + for (var ref of API.cachedUsers.values()) { + if (ref.displayName === displayName) { + userId = ref.id; + break; + } + } + } + var entry = { + created_at: gameLog.dt, + type: 'VideoPlay', + videoUrl, + videoId, + videoName, + videoLength, + location, + displayName, + userId, + videoPos + }; + this.setNowPlaying(entry); + }, + + addGameLogMovieAndChill(gameLog, location) { + // [VRCX] Movie&Chill CurrentTime,Length,PlayerName,MovieName + var data = /Movie&Chill ([\d.]+),([\d.]+),(.+?),(.*)/g.exec( + gameLog.data + ); + if (!data) { + return; + } + var videoPos = Number(data[1]); + var videoLength = Number(data[2]); + var displayName = data[3]; + var videoName = data[4]; + var videoUrl = videoName; + var videoId = 'Movie&Chill'; + if (!videoName) { + return; + } + if (videoUrl === this.nowPlaying.url) { + var entry = { + created_at: gameLog.dt, + videoUrl, + videoLength, + videoPos + }; + this.setNowPlaying(entry); + return; + } + var userId = ''; + if (displayName) { + for (var ref of API.cachedUsers.values()) { + if (ref.displayName === displayName) { + userId = ref.id; + break; + } + } + } + var entry = { + created_at: gameLog.dt, + type: 'VideoPlay', + videoUrl, + videoId, + videoName, + videoLength, + location, + displayName, + userId, + videoPos + }; + this.setNowPlaying(entry); + }, + + async gameLogTableLookup() { + await configRepository.setString( + 'VRCX_gameLogTableFilters', + JSON.stringify(this.gameLogTable.filter) + ); + this.gameLogTable.loading = true; + this.gameLogTable.data = await database.lookupGameLogDatabase( + this.gameLogTable.search, + this.gameLogTable.filter + ); + this.gameLogTable.loading = false; + }, + + sweepGameLog() { + var { data } = this.gameLogTable; + var j = data.length; + if (j > this.maxTableSize) { + data.splice(0, j - this.maxTableSize); + } + + var date = new Date(); + date.setDate(date.getDate() - 1); // 24 hour limit + var limit = date.toJSON(); + var i = 0; + var k = this.gameLogSessionTable.length; + while (i < k && this.gameLogSessionTable[i].created_at < limit) { + ++i; + } + if (i === k) { + this.gameLogSessionTable = []; + } else if (i) { + this.gameLogSessionTable.splice(0, i); + } + }, + + // async resetGameLog() { + // await gameLogService.reset(); + // this.gameLogTable.data = []; + // this.lastLocationReset(); + // }, + + // async refreshEntireGameLog() { + // await gameLogService.setDateTill('1970-01-01'); + // await database.initTables(); + // await this.resetGameLog(); + // var location = ''; + // for (var gameLog of await gameLogService.getAll()) { + // if (gameLog.type === 'location') { + // location = gameLog.location; + // } + // this.addGameLogEntry(gameLog, location); + // } + // this.getGameLogTable(); + // }, + + async getGameLogTable() { + await database.initTables(); + this.gameLogSessionTable = await database.getGamelogDatabase(); + var dateTill = await database.getLastDateGameLogDatabase(); + this.updateGameLog(dateTill); + }, + + async updateGameLog(dateTill) { + await gameLogService.setDateTill(dateTill); + await gameLogService.reset(); + await new Promise((resolve) => { + workerTimers.setTimeout(resolve, 10000); + }); + var location = ''; + for (var gameLog of await gameLogService.getAll()) { + if (gameLog.type === 'location') { + location = gameLog.location; + } + this.addGameLogEntry(gameLog, location); + } + }, + + addGameLogEvent(json) { + var rawLogs = JSON.parse(json); + var gameLog = gameLogService.parseRawGameLog( + rawLogs[1], + rawLogs[2], + rawLogs.slice(3) + ); + if ( + this.debugGameLog && + gameLog.type !== 'photon-id' && + gameLog.type !== 'api-request' && + gameLog.type !== 'udon-exception' + ) { + console.log('gameLog:', gameLog); + } + this.addGameLogEntry(gameLog, this.lastLocation.location); + }, + + deleteGameLogEntryPrompt(row) { + this.$confirm('Continue? Delete Log', 'Confirm', { + confirmButtonText: 'Confirm', + cancelButtonText: 'Cancel', + type: 'info', + callback: (action) => { + if (action === 'confirm') { + this.deleteGameLogEntry(row); + } + } + }); + }, + + deleteGameLogEntry(row) { + $app.removeFromArray(this.gameLogTable.data, row); + database.deleteGameLogEntry(row); + console.log(row); + database.getGamelogDatabase().then((data) => { + this.gameLogSessionTable = data; + this.updateSharedFeed(true); + }); + }, + + gameLogSearch(row) { + var value = this.gameLogTable.search.toUpperCase(); + if (!value) { + return true; + } + if ( + value.startsWith('wrld_') && + String(row.location).toUpperCase().includes(value) + ) { + return true; + } + switch (row.type) { + case 'Location': + if (String(row.worldName).toUpperCase().includes(value)) { + return true; + } + return false; + case 'OnPlayerJoined': + if (String(row.displayName).toUpperCase().includes(value)) { + return true; + } + return false; + case 'OnPlayerLeft': + if (String(row.displayName).toUpperCase().includes(value)) { + return true; + } + return false; + case 'PortalSpawn': + if (String(row.displayName).toUpperCase().includes(value)) { + return true; + } + if (String(row.worldName).toUpperCase().includes(value)) { + return true; + } + return false; + case 'Event': + if (String(row.data).toUpperCase().includes(value)) { + return true; + } + return false; + case 'External': + if (String(row.message).toUpperCase().includes(value)) { + return true; + } + if (String(row.displayName).toUpperCase().includes(value)) { + return true; + } + return false; + case 'VideoPlay': + if (String(row.displayName).toUpperCase().includes(value)) { + return true; + } + if (String(row.videoName).toUpperCase().includes(value)) { + return true; + } + if (String(row.videoUrl).toUpperCase().includes(value)) { + return true; + } + return false; + case 'StringLoad': + case 'ImageLoad': + if (String(row.resourceUrl).toUpperCase().includes(value)) { + return true; + } + return false; + } + return true; + }, + + gameLogIsFriend(row) { + if (typeof row.isFriend !== 'undefined') { + return row.isFriend; + } + if (!row.userId) { + return false; + } + row.isFriend = this.friends.has(row.userId); + return row.isFriend; + }, + + gameLogIsFavorite(row) { + if (typeof row.isFavorite !== 'undefined') { + return row.isFavorite; + } + if (!row.userId) { + return false; + } + row.isFavorite = this.localFavoriteFriends.has(row.userId); + return row.isFavorite; + }, + + async disableGameLogDialog() { + if (this.isGameRunning) { + this.$message({ + message: + 'VRChat needs to be closed before this option can be changed', + type: 'error' + }); + this.gameLogDisabled = !this.gameLogDisabled; + return; + } + if (this.gameLogDisabled) { + this.$confirm('Continue? Disable GameLog', 'Confirm', { + confirmButtonText: 'Confirm', + cancelButtonText: 'Cancel', + type: 'info', + callback: async (action) => { + if (action !== 'confirm') { + this.gameLogDisabled = !this.gameLogDisabled; + await configRepository.setBool( + 'VRCX_gameLogDisabled', + this.gameLogDisabled + ); + } + } + }); + } else { + await configRepository.setBool( + 'VRCX_gameLogDisabled', + this.gameLogDisabled + ); + } + } + }; +} diff --git a/html/src/classes/gameRealtimeLogging.js b/html/src/classes/gameRealtimeLogging.js new file mode 100644 index 000000000..1c009686c --- /dev/null +++ b/html/src/classes/gameRealtimeLogging.js @@ -0,0 +1,1544 @@ +import * as workerTimers from 'worker-timers'; +import configRepository from '../repository/config.js'; +import { baseClass, $app, API, $t, $utils } from './baseClass.js'; + +export default class extends baseClass { + constructor(_app, _API, _t) { + super(_app, _API, _t); + } + + _data = { + photonLoggingEnabled: false, + moderationEventQueue: new Map(), + moderationAgainstTable: [], + photonLobby: new Map(), + photonLobbyMaster: 0, + photonLobbyCurrentUser: 0, + photonLobbyUserData: new Map(), + photonLobbyCurrent: new Map(), + photonLobbyAvatars: new Map(), + photonLobbyLastModeration: new Map(), + photonLobbyWatcherLoop: false, + photonLobbyTimeout: [], + photonLobbyJointime: new Map(), + photonLobbyActivePortals: new Map(), + photonEvent7List: new Map(), + photonLastEvent7List: '', + photonLastChatBoxMsg: new Map(), + + photonEventTable: { + data: [], + filters: [ + { + prop: ['displayName', 'text'], + value: '' + }, + { + prop: 'type', + value: [], + filterFn: (row, filter) => + filter.value.some((v) => v === row.type) + } + ], + tableProps: { + stripe: true, + size: 'mini' + }, + pageSize: 10, + paginationProps: { + small: true, + layout: 'sizes,prev,pager,next,total', + pageSizes: [5, 10, 15, 25, 50] + } + }, + + photonEventTablePrevious: { + data: [], + filters: [ + { + prop: ['displayName', 'text'], + value: '' + }, + { + prop: 'type', + value: [], + filterFn: (row, filter) => + filter.value.some((v) => v === row.type) + } + ], + tableProps: { + stripe: true, + size: 'mini' + }, + pageSize: 10, + paginationProps: { + small: true, + layout: 'sizes,prev,pager,next,total', + pageSizes: [5, 10, 15, 25, 50] + } + }, + + photonEventType: [ + 'MeshVisibility', + 'AnimationFloat', + 'AnimationBool', + 'AnimationTrigger', + 'AudioTrigger', + 'PlayAnimation', + 'SendMessage', + 'SetParticlePlaying', + 'TeleportPlayer', + 'RunConsoleCommand', + 'SetGameObjectActive', + 'SetWebPanelURI', + 'SetWebPanelVolume', + 'SpawnObject', + 'SendRPC', + 'ActivateCustomTrigger', + 'DestroyObject', + 'SetLayer', + 'SetMaterial', + 'AddHealth', + 'AddDamage', + 'SetComponentActive', + 'AnimationInt', + 'AnimationIntAdd', + 'AnimationIntSubtract', + 'AnimationIntMultiply', + 'AnimationIntDivide', + 'AddVelocity', + 'SetVelocity', + 'AddAngularVelocity', + 'SetAngularVelocity', + 'AddForce', + 'SetUIText', + 'CallUdonMethod' + ], + + photonEmojis: [ + 'Angry', + 'Blushing', + 'Crying', + 'Frown', + 'Hand Wave', + 'Hang Ten', + 'In Love', + 'Jack O Lantern', + 'Kiss', + 'Laugh', + 'Skull', + 'Smile', + 'Spooky Ghost', + 'Stoic', + 'Sunglasses', + 'Thinking', + 'Thumbs Down', + 'Thumbs Up', + 'Tongue Out', + 'Wow', + 'Arrow Point', + "Can't see", + 'Hourglass', + 'Keyboard', + 'No Headphones', + 'No Mic', + 'Portal', + 'Shush', + 'Bats', + 'Cloud', + 'Fire', + 'Snow Fall', + 'Snowball', + 'Splash', + 'Web', + 'Beer', + 'Candy', + 'Candy Cane', + 'Candy Corn', + 'Champagne', + 'Drink', + 'Gingerbread', + 'Ice Cream', + 'Pineapple', + 'Pizza', + 'Tomato', + 'Beachball', + 'Coal', + 'Confetti', + 'Gift', + 'Gifts', + 'Life Ring', + 'Mistletoe', + 'Money', + 'Neon Shades', + 'Sun Lotion', + 'Boo', + 'Broken Heart', + 'Exclamation', + 'Go', + 'Heart', + 'Music Note', + 'Question', + 'Stop', + 'Zzz' + ], + + photonEventTableFilter: '', + photonEventTableTypeFilter: [], + photonEventTableTypeOverlayFilter: [], + photonEventTableTypeFilterList: [ + 'Event', + 'OnPlayerJoined', + 'OnPlayerLeft', + 'ChangeAvatar', + 'ChangeStatus', + 'ChangeGroup', + 'PortalSpawn', + 'DeletedPortal', + 'ChatBoxMessage', + 'Moderation', + 'Camera', + 'SpawnEmoji', + 'MasterMigrate' + ] + }; + + _methods = { + startLobbyWatcherLoop() { + if (!this.photonLobbyWatcherLoop) { + this.photonLobbyWatcherLoop = true; + this.photonLobbyWatcher(); + } + }, + + photonLobbyWatcherLoopStop() { + this.photonLobbyWatcherLoop = false; + this.photonLobbyTimeout = []; + AppApi.ExecuteVrOverlayFunction('updateHudTimeout', '[]'); + }, + + photonLobbyWatcher() { + if (!this.photonLobbyWatcherLoop) { + return; + } + if (this.photonLobbyCurrent.size === 0) { + this.photonLobbyWatcherLoopStop(); + return; + } + var dtNow = Date.now(); + var bias2 = this.photonLastEvent7List + 1.5 * 1000; + if (dtNow > bias2 || this.lastLocation.playerList.size <= 1) { + if (this.photonLobbyTimeout.length > 0) { + AppApi.ExecuteVrOverlayFunction('updateHudTimeout', '[]'); + } + this.photonLobbyTimeout = []; + workerTimers.setTimeout(() => this.photonLobbyWatcher(), 500); + return; + } + var hudTimeout = []; + this.photonEvent7List.forEach((dt, id) => { + var timeSinceLastEvent = dtNow - Date.parse(dt); + if ( + timeSinceLastEvent > this.photonLobbyTimeoutThreshold && + id !== this.photonLobbyCurrentUser + ) { + if (this.photonLobbyJointime.has(id)) { + var { joinTime } = this.photonLobbyJointime.get(id); + } + if (!joinTime) { + console.log(`${id} missing join time`); + } + if (joinTime && joinTime + 70000 < dtNow) { + // wait 70secs for user to load in + hudTimeout.unshift({ + userId: this.getUserIdFromPhotonId(id), + displayName: this.getDisplayNameFromPhotonId(id), + time: Math.round(timeSinceLastEvent / 1000), + rawTime: timeSinceLastEvent + }); + } + } + }); + if (this.photonLobbyTimeout.length > 0 || hudTimeout.length > 0) { + hudTimeout.sort(function (a, b) { + if (a.rawTime > b.rawTime) { + return 1; + } + if (a.rawTime < b.rawTime) { + return -1; + } + return 0; + }); + if (this.timeoutHudOverlay) { + if ( + this.timeoutHudOverlayFilter === 'VIP' || + this.timeoutHudOverlayFilter === 'Friends' + ) { + var filteredHudTimeout = []; + hudTimeout.forEach((item) => { + if ( + this.timeoutHudOverlayFilter === 'VIP' && + API.cachedFavoritesByObjectId.has(item.userId) + ) { + filteredHudTimeout.push(item); + } else if ( + this.timeoutHudOverlayFilter === 'Friends' && + this.friends.has(item.userId) + ) { + filteredHudTimeout.push(item); + } + }); + } else { + var filteredHudTimeout = hudTimeout; + } + AppApi.ExecuteVrOverlayFunction( + 'updateHudTimeout', + JSON.stringify(filteredHudTimeout) + ); + } + this.photonLobbyTimeout = hudTimeout; + this.getCurrentInstanceUserList(); + } + workerTimers.setTimeout(() => this.photonLobbyWatcher(), 500); + }, + + addEntryPhotonEvent(input) { + var isMaster = false; + if (input.photonId === this.photonLobbyMaster) { + isMaster = true; + } + var joinTimeRef = this.photonLobbyJointime.get(input.photonId); + var isModerator = joinTimeRef?.canModerateInstance; + var photonUserRef = this.photonLobby.get(input.photonId); + var displayName = ''; + var userId = ''; + var isFriend = false; + if (typeof photonUserRef !== 'undefined') { + displayName = photonUserRef.displayName; + userId = photonUserRef.id; + isFriend = photonUserRef.isFriend; + } + var isFavorite = this.localFavoriteFriends.has(userId); + var colour = ''; + var tagRef = this.customUserTags.get(userId); + if (typeof tagRef !== 'undefined') { + colour = tagRef.colour; + } + var feed = { + displayName, + userId, + isFavorite, + isFriend, + isMaster, + isModerator, + colour, + ...input + }; + this.photonEventTable.data.unshift(feed); + if ( + this.photonEventTableTypeOverlayFilter.length > 0 && + !this.photonEventTableTypeOverlayFilter.includes(feed.type) + ) { + return; + } + if (this.photonEventOverlay) { + if ( + this.photonEventOverlayFilter === 'VIP' || + this.photonEventOverlayFilter === 'Friends' + ) { + if ( + feed.userId && + ((this.photonEventOverlayFilter === 'VIP' && + isFavorite) || + (this.photonEventOverlayFilter === 'Friends' && + isFriend)) + ) { + AppApi.ExecuteVrOverlayFunction( + 'addEntryHudFeed', + JSON.stringify(feed) + ); + } + } else { + AppApi.ExecuteVrOverlayFunction( + 'addEntryHudFeed', + JSON.stringify(feed) + ); + } + } + }, + + getDisplayNameFromPhotonId(photonId) { + var displayName = ''; + if (photonId) { + var ref = this.photonLobby.get(photonId); + displayName = `ID:${photonId}`; + if ( + typeof ref !== 'undefined' && + typeof ref.displayName !== 'undefined' + ) { + displayName = ref.displayName; + } + } + return displayName; + }, + + getUserIdFromPhotonId(photonId) { + var userId = ''; + if (photonId) { + var ref = this.photonLobby.get(photonId); + if ( + typeof ref !== 'undefined' && + typeof ref.id !== 'undefined' + ) { + userId = ref.id; + } + } + return userId; + }, + + showUserFromPhotonId(photonId) { + if (photonId) { + var ref = this.photonLobby.get(photonId); + if (typeof ref !== 'undefined') { + if (typeof ref.id !== 'undefined') { + this.showUserDialog(ref.id); + } else if (typeof ref.displayName !== 'undefined') { + this.lookupUser(ref); + } + } else { + this.$message({ + message: 'No user info available', + type: 'error' + }); + } + } + }, + + getPhotonIdFromDisplayName(displayName) { + var photonId = ''; + if (displayName) { + this.photonLobby.forEach((ref, id) => { + if ( + typeof ref !== 'undefined' && + ref.displayName === displayName + ) { + photonId = id; + } + }); + } + return photonId; + }, + + getPhotonIdFromUserId(userId) { + var photonId = ''; + if (userId) { + this.photonLobby.forEach((ref, id) => { + if (typeof ref !== 'undefined' && ref.id === userId) { + photonId = id; + } + }); + } + return photonId; + }, + + sortPhotonId(a, b, field) { + var id1 = this.getPhotonIdFromDisplayName(a[field]); + var id2 = this.getPhotonIdFromDisplayName(b[field]); + if (id1 < id2) { + return 1; + } + if (id1 > id2) { + return -1; + } + return 0; + }, + + parsePhotonEvent(data, gameLogDate) { + switch (data.Code) { + case 253: + // SetUserProperties + if (data.Parameters[253] === -1) { + for (var i in data.Parameters[251]) { + var id = parseInt(i, 10); + var user = data.Parameters[251][i]; + this.parsePhotonUser(id, user.user, gameLogDate); + this.parsePhotonAvatarChange( + id, + user.user, + user.avatarDict, + gameLogDate + ); + this.parsePhotonGroupChange( + id, + user.user, + user.groupOnNameplate, + gameLogDate + ); + 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(gameLogDate), + 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 + }); + this.photonUserJoin(id, user, gameLogDate); + } + } else { + console.log('oldSetUserProps', data); + var id = parseInt(data.Parameters[253], 10); + var user = data.Parameters[251]; + this.parsePhotonUser(id, user.user, gameLogDate); + this.parsePhotonAvatarChange( + id, + user.user, + user.avatarDict, + gameLogDate + ); + this.parsePhotonGroupChange( + id, + user.user, + user.groupOnNameplate, + gameLogDate + ); + 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(gameLogDate), + 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 + }); + this.photonUserJoin(id, user, gameLogDate); + } + break; + case 42: + // SetUserProperties + var id = parseInt(data.Parameters[254], 10); + var user = data.Parameters[245]; + this.parsePhotonUser(id, user.user, gameLogDate); + this.parsePhotonAvatarChange( + id, + user.user, + user.avatarDict, + gameLogDate + ); + this.parsePhotonGroupChange( + id, + user.user, + user.groupOnNameplate, + gameLogDate + ); + this.parsePhotonAvatar(user.avatarDict); + this.parsePhotonAvatar(user.favatarDict); + var lobbyJointime = this.photonLobbyJointime.get(id); + this.photonLobbyJointime.set(id, { + hasInstantiated: true, + ...lobbyJointime, + inVRMode: user.inVRMode, + avatarEyeHeight: user.avatarEyeHeight, + canModerateInstance: user.canModerateInstance, + groupOnNameplate: user.groupOnNameplate, + showGroupBadgeToOthers: user.showGroupBadgeToOthers, + showSocialRank: user.showSocialRank, + useImpostorAsFallback: user.useImpostorAsFallback, + platform: user.platform + }); + break; + case 255: + // Join + if (typeof data.Parameters[249] !== 'undefined') { + this.parsePhotonUser( + data.Parameters[254], + data.Parameters[249].user, + gameLogDate + ); + this.parsePhotonAvatarChange( + data.Parameters[254], + data.Parameters[249].user, + data.Parameters[249].avatarDict, + gameLogDate + ); + this.parsePhotonGroupChange( + data.Parameters[254], + data.Parameters[249].user, + data.Parameters[249].groupOnNameplate, + gameLogDate + ); + this.parsePhotonAvatar(data.Parameters[249].avatarDict); + this.parsePhotonAvatar( + data.Parameters[249].favatarDict + ); + } + this.parsePhotonLobbyIds(data.Parameters[252]); + var hasInstantiated = false; + if (this.photonLobbyCurrentUser === data.Parameters[254]) { + // fix current user + hasInstantiated = true; + } + var ref = this.photonLobbyCurrent.get(data.Parameters[254]); + if (typeof ref !== 'undefined') { + // fix for join event firing twice + // fix instantiation happening out of order before join event + hasInstantiated = ref.hasInstantiated; + } + this.photonLobbyJointime.set(data.Parameters[254], { + joinTime: Date.parse(gameLogDate), + hasInstantiated, + inVRMode: data.Parameters[249].inVRMode, + avatarEyeHeight: data.Parameters[249].avatarEyeHeight, + canModerateInstance: + data.Parameters[249].canModerateInstance, + groupOnNameplate: data.Parameters[249].groupOnNameplate, + showGroupBadgeToOthers: + data.Parameters[249].showGroupBadgeToOthers, + showSocialRank: data.Parameters[249].showSocialRank, + useImpostorAsFallback: + data.Parameters[249].useImpostorAsFallback, + platform: data.Parameters[249].platform + }); + this.photonUserJoin( + data.Parameters[254], + data.Parameters[249], + gameLogDate + ); + this.startLobbyWatcherLoop(); + break; + case 254: + // Leave + var photonId = data.Parameters[254]; + this.photonUserLeave(photonId, gameLogDate); + this.photonLobbyCurrent.delete(photonId); + this.photonLobbyLastModeration.delete(photonId); + this.photonLobbyJointime.delete(photonId); + this.photonEvent7List.delete(photonId); + this.parsePhotonLobbyIds(data.Parameters[252]); + if (typeof data.Parameters[203] !== 'undefined') { + this.setPhotonLobbyMaster( + data.Parameters[203], + gameLogDate + ); + } + break; + case 4: + // Sync + this.setPhotonLobbyMaster( + data.Parameters[254], + gameLogDate + ); + break; + case 33: + // Moderation + if (data.Parameters[245]['0'] === 21) { + if (data.Parameters[245]['1']) { + var photonId = data.Parameters[245]['1']; + var block = data.Parameters[245]['10']; + var mute = data.Parameters[245]['11']; + var ref = this.photonLobby.get(photonId); + if ( + typeof ref !== 'undefined' && + typeof ref.id !== 'undefined' + ) { + this.photonModerationUpdate( + ref, + photonId, + block, + mute, + gameLogDate + ); + } else { + this.moderationEventQueue.set(photonId, { + block, + mute, + gameLogDate + }); + } + } else { + var blockArray = data.Parameters[245]['10']; + var muteArray = data.Parameters[245]['11']; + var idList = new Map(); + blockArray.forEach((photonId1) => { + if (muteArray.includes(photonId1)) { + idList.set(photonId1, { + isMute: true, + isBlock: true + }); + } else { + idList.set(photonId1, { + isMute: false, + isBlock: true + }); + } + }); + muteArray.forEach((photonId2) => { + if (!idList.has(photonId2)) { + idList.set(photonId2, { + isMute: true, + isBlock: false + }); + } + }); + idList.forEach(({ isMute, isBlock }, photonId3) => { + var ref1 = this.photonLobby.get(photonId3); + if ( + typeof ref1 !== 'undefined' && + typeof ref1.id !== 'undefined' + ) { + this.photonModerationUpdate( + ref1, + photonId3, + isBlock, + isMute, + gameLogDate + ); + } else { + this.moderationEventQueue.set(photonId3, { + block: isBlock, + mute: isMute, + gameLogDate + }); + } + }); + } + } else if ( + data.Parameters[245]['0'] === 13 || + data.Parameters[245]['0'] === 25 + ) { + var msg = data.Parameters[245]['2']; + if ( + typeof msg === 'string' && + typeof data.Parameters[245]['14'] === 'object' + ) { + for (var prop in data.Parameters[245]['14']) { + var value = data.Parameters[245]['14'][prop]; + msg = msg.replace(`{{${prop}}}`, value); + } + } + this.addEntryPhotonEvent({ + photonId, + text: msg, + type: 'Moderation', + color: 'yellow', + created_at: gameLogDate + }); + } + break; + case 202: + // Instantiate + if (!this.photonLobby.has(data.Parameters[254])) { + this.photonLobby.set(data.Parameters[254]); + } + if (!this.photonLobbyCurrent.has(data.Parameters[254])) { + this.photonLobbyCurrent.set(data.Parameters[254]); + } + var lobbyJointime = this.photonLobbyJointime.get( + data.Parameters[254] + ); + if (typeof lobbyJointime !== 'undefined') { + this.photonLobbyJointime.set(data.Parameters[254], { + ...lobbyJointime, + hasInstantiated: true + }); + } else { + this.photonLobbyJointime.set(data.Parameters[254], { + joinTime: Date.parse(gameLogDate), + hasInstantiated: true + }); + } + break; + case 43: + // Chatbox Message + var photonId = data.Parameters[254]; + var text = data.Parameters[245]; + if (this.photonLobbyCurrentUser === photonId) { + return; + } + var lastMsg = this.photonLastChatBoxMsg.get(photonId); + if (lastMsg === text) { + return; + } + this.photonLastChatBoxMsg.set(photonId, text); + var userId = this.getUserIdFromPhotonId(photonId); + if ( + this.chatboxUserBlacklist.has(userId) || + this.checkChatboxBlacklist(text) + ) { + return; + } + this.addEntryPhotonEvent({ + photonId, + text, + type: 'ChatBoxMessage', + created_at: gameLogDate + }); + var entry = { + userId, + displayName: this.getDisplayNameFromPhotonId(photonId), + created_at: gameLogDate, + type: 'ChatBoxMessage', + text + }; + this.queueGameLogNoty(entry); + this.addGameLog(entry); + break; + case 70: + // Portal Spawn + if (data.Parameters[245][0] === 20) { + var portalId = data.Parameters[245][1]; + var userId = data.Parameters[245][2]; + var shortName = data.Parameters[245][5]; + var worldName = data.Parameters[245][8].name; + this.addPhotonPortalSpawn( + gameLogDate, + userId, + shortName, + worldName + ); + this.photonLobbyActivePortals.set(portalId, { + userId, + shortName, + worldName, + created_at: Date.parse(gameLogDate), + playerCount: 0, + pendingLeave: 0 + }); + } else if (data.Parameters[245][0] === 21) { + var portalId = data.Parameters[245][1]; + var userId = data.Parameters[245][2]; + var playerCount = data.Parameters[245][3]; + var shortName = data.Parameters[245][5]; + var worldName = ''; + this.addPhotonPortalSpawn( + gameLogDate, + userId, + shortName, + worldName + ); + this.photonLobbyActivePortals.set(portalId, { + userId, + shortName, + worldName, + created_at: Date.parse(gameLogDate), + playerCount: 0, + pendingLeave: 0 + }); + } else if (data.Parameters[245][0] === 22) { + var portalId = data.Parameters[245][1]; + var text = 'DeletedPortal'; + var ref = this.photonLobbyActivePortals.get(portalId); + if (typeof ref !== 'undefined') { + var worldName = ref.worldName; + var playerCount = ref.playerCount; + var time = $app.timeToText( + Date.parse(gameLogDate) - ref.created_at + ); + text = `DeletedPortal after ${time} with ${playerCount} players to "${worldName}"`; + } + this.addEntryPhotonEvent({ + text, + type: 'DeletedPortal', + created_at: gameLogDate + }); + this.photonLobbyActivePortals.delete(portalId); + } else if (data.Parameters[245][0] === 23) { + var portalId = data.Parameters[245][1]; + var playerCount = data.Parameters[245][3]; + var ref = this.photonLobbyActivePortals.get(portalId); + if (typeof ref !== 'undefined') { + ref.pendingLeave++; + ref.playerCount = playerCount; + } + } else if (data.Parameters[245][0] === 24) { + this.addEntryPhotonEvent({ + text: 'PortalError failed to create portal', + type: 'DeletedPortal', + created_at: gameLogDate + }); + } + break; + case 71: + // Spawn Emoji + var photonId = data.Parameters[254]; + if (photonId === this.photonLobbyCurrentUser) { + return; + } + var type = data.Parameters[245][0]; + var emojiName = ''; + var imageUrl = ''; + if (type === 0) { + var emojiId = data.Parameters[245][2]; + emojiName = this.photonEmojis[emojiId]; + } else if (type === 1) { + emojiName = 'Custom'; + var fileId = data.Parameters[245][1]; + imageUrl = `https://api.vrchat.cloud/api/1/file/${fileId}/1/`; + } + this.addEntryPhotonEvent({ + photonId, + text: emojiName, + type: 'SpawnEmoji', + created_at: gameLogDate, + imageUrl, + fileId + }); + break; + } + }, + + parseVRCEvent(json) { + // VRC Event + var datetime = json.dt; + var eventData = json.VRCEventData; + var senderId = eventData.Sender; + if (this.debugPhotonLogging) { + console.log('VrcEvent:', json); + } + if (eventData.EventName === '_SendOnSpawn') { + return; + } else if (eventData.EventType > 34) { + var entry = { + created_at: datetime, + type: 'Event', + data: `${this.getDisplayNameFromPhotonId( + senderId + )} called non existent RPC ${eventData.EventType}` + }; + this.addPhotonEventToGameLog(entry); + return; + } + if (eventData.EventType === 14) { + var type = 'Event'; + if (eventData.EventName === 'ChangeVisibility') { + if (eventData.Data[0] === true) { + var text = 'EnableCamera'; + } else if (eventData.Data[0] === false) { + var text = 'DisableCamera'; + } + type = 'Camera'; + } else if (eventData.EventName === 'PhotoCapture') { + var text = 'PhotoCapture'; + type = 'Camera'; + } else if (eventData.EventName === 'TimerBloop') { + var text = 'TimerBloop'; + type = 'Camera'; + } else if (eventData.EventName === 'ReloadAvatarNetworkedRPC') { + var text = 'AvatarReset'; + } else if (eventData.EventName === 'ReleaseBones') { + var text = 'ResetPhysBones'; + } else if (eventData.EventName === 'SpawnEmojiRPC') { + var text = this.oldPhotonEmojis[eventData.Data]; + type = 'SpawnEmoji'; + } else { + var eventVrc = ''; + if (eventData.Data && eventData.Data.length > 0) { + eventVrc = ` ${JSON.stringify(eventData.Data).replace( + /"([^(")"]+)":/g, + '$1:' + )}`; + } + var text = `${eventData.EventName}${eventVrc}`; + } + this.addEntryPhotonEvent({ + photonId: senderId, + text, + type, + created_at: datetime + }); + } else { + var eventName = ''; + if (eventData.EventName) { + eventName = ` ${JSON.stringify(eventData.EventName).replace( + /"([^(")"]+)":/g, + '$1:' + )}`; + } + if (this.debugPhotonLogging) { + var displayName = this.getDisplayNameFromPhotonId(senderId); + var feed = `RPC ${displayName} ${ + this.photonEventType[eventData.EventType] + }${eventName}`; + console.log('VrcRpc:', feed); + } + } + }, + + async parsePhotonPortalSpawn( + created_at, + instanceId, + ref, + portalType, + shortName, + photonId + ) { + var worldName = shortName; + if (instanceId) { + worldName = await this.getWorldName(instanceId); + } + this.addEntryPhotonEvent({ + photonId, + text: `${portalType} PortalSpawn to ${worldName}`, + type: 'PortalSpawn', + shortName, + location: instanceId, + worldName, + created_at + }); + this.addPhotonEventToGameLog({ + created_at, + type: 'PortalSpawn', + displayName: ref.displayName, + location: this.lastLocation.location, + userId: ref.id, + instanceId, + worldName + }); + }, + + async addPhotonPortalSpawn(gameLogDate, userId, shortName, worldName) { + var instance = await API.getInstanceFromShortName({ shortName }); + var location = instance.json.location; + var L = $utils.parseLocation(location); + var groupName = ''; + if (L.groupId) { + groupName = await this.getGroupName(L.groupId); + } + if (!worldName) { + // eslint-disable-next-line no-param-reassign + worldName = await this.getWorldName(location); + } + // var newShortName = instance.json.shortName; + // var portalType = 'Secure'; + // if (shortName === newShortName) { + // portalType = 'Unlocked'; + // } + var displayLocation = this.displayLocation( + location, + worldName, + groupName + ); + this.addEntryPhotonEvent({ + photonId: this.getPhotonIdFromUserId(userId), + text: `PortalSpawn to ${displayLocation}`, + type: 'PortalSpawn', + shortName, + location, + worldName, + groupName, + created_at: gameLogDate + }); + this.addPhotonEventToGameLog({ + created_at: gameLogDate, + type: 'PortalSpawn', + displayName: this.getDisplayName(userId), + location: this.lastLocation.location, + userId, + instanceId: location, + worldName, + groupName + }); + }, + + addPhotonEventToGameLog(entry) { + this.queueGameLogNoty(entry); + this.addGameLog(entry); + if (entry.type === 'PortalSpawn') { + database.addGamelogPortalSpawnToDatabase(entry); + } else if (entry.type === 'Event') { + database.addGamelogEventToDatabase(entry); + } + }, + + parsePhotonLobbyIds(lobbyIds) { + lobbyIds.forEach((id) => { + if (!this.photonLobby.has(id)) { + this.photonLobby.set(id); + } + if (!this.photonLobbyCurrent.has(id)) { + this.photonLobbyCurrent.set(id); + } + }); + for (var id of this.photonLobbyCurrent.keys()) { + if (!lobbyIds.includes(id)) { + this.photonLobbyCurrent.delete(id); + this.photonEvent7List.delete(id); + } + } + }, + + setPhotonLobbyMaster(photonId, gameLogDate) { + if (this.photonLobbyMaster !== photonId) { + if (this.photonLobbyMaster !== 0) { + this.addEntryPhotonEvent({ + photonId, + text: `Photon Master Migrate`, + type: 'MasterMigrate', + created_at: gameLogDate + }); + } + this.photonLobbyMaster = photonId; + } + }, + + async parsePhotonUser(photonId, user, gameLogDate) { + if (typeof user === 'undefined') { + console.error('PhotonUser: user is undefined', photonId); + return; + } + var tags = []; + if (typeof user.tags !== 'undefined') { + tags = user.tags; + } + var ref = API.cachedUsers.get(user.id); + var photonUser = { + id: user.id, + displayName: user.displayName, + developerType: user.developerType, + profilePicOverride: user.profilePicOverride, + currentAvatarImageUrl: user.currentAvatarImageUrl, + currentAvatarThumbnailImageUrl: + user.currentAvatarThumbnailImageUrl, + userIcon: user.userIcon, + last_platform: user.last_platform, + allowAvatarCopying: user.allowAvatarCopying, + status: user.status, + statusDescription: user.statusDescription, + bio: user.bio, + tags + }; + this.photonLobby.set(photonId, photonUser); + this.photonLobbyCurrent.set(photonId, photonUser); + this.photonLobbyUserDataUpdate(photonId, photonUser, gameLogDate); + + var bias = Date.parse(gameLogDate) + 60 * 1000; // 1min + if (bias > Date.now()) { + if ( + typeof ref === 'undefined' || + typeof ref.id === 'undefined' + ) { + try { + var args = await API.getUser({ + userId: user.id + }); + ref = args.ref; + } catch (err) { + console.error(err); + ref = photonUser; + } + } else if ( + !ref.isFriend && + this.lastLocation.playerList.has(ref.displayName) + ) { + var { joinTime } = this.lastLocation.playerList.get( + ref.displayName + ); + if (!joinTime) { + joinTime = Date.parse(gameLogDate); + } + ref.$location_at = joinTime; + ref.$online_for = joinTime; + } + if ( + typeof ref.id !== 'undefined' && + ref.currentAvatarImageUrl !== user.currentAvatarImageUrl + ) { + API.applyUser({ + ...ref, + currentAvatarImageUrl: user.currentAvatarImageUrl, + currentAvatarThumbnailImageUrl: + user.currentAvatarThumbnailImageUrl + }); + } + } + if (typeof ref !== 'undefined' && typeof ref.id !== 'undefined') { + this.photonLobby.set(photonId, ref); + this.photonLobbyCurrent.set(photonId, ref); + // check moderation queue + if (this.moderationEventQueue.has(photonId)) { + var { block, mute, gameLogDate } = + this.moderationEventQueue.get(photonId); + this.moderationEventQueue.delete(photonId); + this.photonModerationUpdate( + ref, + photonId, + block, + mute, + gameLogDate + ); + } + } + }, + + photonLobbyUserDataUpdate(photonId, photonUser, gameLogDate) { + var ref = this.photonLobbyUserData.get(photonId); + if ( + typeof ref !== 'undefined' && + photonId !== this.photonLobbyCurrentUser && + (photonUser.status !== ref.status || + photonUser.statusDescription !== ref.statusDescription) + ) { + this.addEntryPhotonEvent({ + photonId, + type: 'ChangeStatus', + status: photonUser.status, + previousStatus: ref.status, + statusDescription: this.replaceBioSymbols( + photonUser.statusDescription + ), + previousStatusDescription: this.replaceBioSymbols( + ref.statusDescription + ), + created_at: Date.parse(gameLogDate) + }); + } + this.photonLobbyUserData.set(photonId, photonUser); + }, + + photonUserJoin(photonId, user, gameLogDate) { + if (photonId === this.photonLobbyCurrentUser) { + return; + } + var avatar = user.avatarDict; + avatar.name = this.replaceBioSymbols(avatar.name); + avatar.description = this.replaceBioSymbols(avatar.description); + var platform = ''; + if (user.last_platform === 'android') { + platform = 'Android'; + } else if (user.last_platform === 'ios') { + platform = 'iOS'; + } else if (user.inVRMode) { + platform = 'VR'; + } else { + platform = 'Desktop'; + } + this.photonUserSusieCheck(photonId, user, gameLogDate); + this.checkVRChatCache(avatar).then((cacheInfo) => { + var inCache = false; + if (cacheInfo.Item1 > 0) { + inCache = true; + } + this.addEntryPhotonEvent({ + photonId, + text: 'has joined', + type: 'OnPlayerJoined', + created_at: gameLogDate, + avatar, + inCache, + platform + }); + }); + }, + + photonUserSusieCheck(photonId, user, gameLogDate) { + var text = ''; + if (typeof user.modTag !== 'undefined') { + text = `Moderator has joined ${user.modTag}`; + } else if (user.isInvisible) { + text = 'User joined invisible'; + } + if (text) { + this.addEntryPhotonEvent({ + photonId, + text, + type: 'Event', + color: 'yellow', + created_at: gameLogDate + }); + var entry = { + created_at: new Date().toJSON(), + type: 'Event', + data: `${text} - ${this.getDisplayNameFromPhotonId( + photonId + )} (${this.getUserIdFromPhotonId(photonId)})` + }; + this.queueGameLogNoty(entry); + this.addGameLog(entry); + database.addGamelogEventToDatabase(entry); + } + }, + + photonUserLeave(photonId, gameLogDate) { + if (!this.photonLobbyCurrent.has(photonId)) { + return; + } + var text = 'has left'; + var lastEvent = this.photonEvent7List.get(parseInt(photonId, 10)); + if (typeof lastEvent !== 'undefined') { + var timeSinceLastEvent = Date.now() - Date.parse(lastEvent); + if (timeSinceLastEvent > 10 * 1000) { + // 10 seconds + text = `has timed out after ${$app.timeToText(timeSinceLastEvent)}`; + } + } + this.photonLobbyActivePortals.forEach((portal) => { + if (portal.pendingLeave > 0) { + text = `has left through portal to "${portal.worldName}"`; + portal.pendingLeave--; + } + }); + this.addEntryPhotonEvent({ + photonId, + text, + type: 'OnPlayerLeft', + created_at: gameLogDate + }); + }, + + photonModerationUpdate(ref, photonId, block, mute, gameLogDate) { + database.getModeration(ref.id).then((row) => { + var lastType = this.photonLobbyLastModeration.get(photonId); + var type = ''; + var text = ''; + if (block) { + type = 'Blocked'; + text = 'Blocked'; + } else if (mute) { + type = 'Muted'; + text = 'Muted'; + } + if (row.userId) { + if (!block && row.block) { + type = 'Unblocked'; + text = 'Unblocked'; + } else if (!mute && row.mute) { + type = 'Unmuted'; + text = 'Unmuted'; + } + if (block === row.block && mute === row.mute) { + // no change + if (type && type !== lastType) { + this.addEntryPhotonEvent({ + photonId, + text: `Moderation ${text}`, + type: 'Moderation', + color: 'yellow', + created_at: gameLogDate + }); + } + this.photonLobbyLastModeration.set(photonId, type); + return; + } + } + this.photonLobbyLastModeration.set(photonId, type); + this.moderationAgainstTable.forEach((item) => { + if (item.userId === ref.id && item.type === type) { + $app.removeFromArray(this.moderationAgainstTable, item); + } + }); + if (type) { + this.addEntryPhotonEvent({ + photonId, + text: `Moderation ${text}`, + type: 'Moderation', + color: 'yellow', + created_at: gameLogDate + }); + var noty = { + created_at: new Date().toJSON(), + userId: ref.id, + displayName: ref.displayName, + type + }; + this.queueModerationNoty(noty); + var entry = { + created_at: gameLogDate, + userId: ref.id, + displayName: ref.displayName, + type + }; + this.moderationAgainstTable.push(entry); + } + if (block || mute || block !== row.block || mute !== row.mute) { + this.updateSharedFeed(true); + } + if (block || mute) { + database.setModeration({ + userId: ref.id, + updatedAt: gameLogDate, + displayName: ref.displayName, + block, + mute + }); + } else if (row.block || row.mute) { + database.deleteModeration(ref.id); + } + }); + }, + + parsePhotonAvatarChange(photonId, user, avatar, gameLogDate) { + if (typeof avatar === 'undefined') { + return; + } + if (typeof user === 'undefined') { + console.error( + 'PhotonAvatarChange: user is undefined', + photonId + ); + return; + } + var oldAvatarId = this.photonLobbyAvatars.get(user.id); + if ( + oldAvatarId && + oldAvatarId !== avatar.id && + photonId !== this.photonLobbyCurrentUser + ) { + avatar.name = this.replaceBioSymbols(avatar.name); + avatar.description = this.replaceBioSymbols(avatar.description); + this.checkVRChatCache(avatar).then((cacheInfo) => { + var inCache = false; + if (cacheInfo.Item1 > 0) { + inCache = true; + } + var entry = { + created_at: new Date().toJSON(), + type: 'AvatarChange', + userId: user.id, + displayName: user.displayName, + name: avatar.name, + description: avatar.description, + avatarId: avatar.id, + authorId: avatar.authorId, + releaseStatus: avatar.releaseStatus, + imageUrl: avatar.imageUrl, + thumbnailImageUrl: avatar.thumbnailImageUrl + }; + this.queueGameLogNoty(entry); + this.addGameLog(entry); + this.addEntryPhotonEvent({ + photonId, + displayName: user.displayName, + userId: user.id, + text: `ChangeAvatar ${avatar.name}`, + type: 'ChangeAvatar', + created_at: gameLogDate, + avatar, + inCache + }); + }); + } + this.photonLobbyAvatars.set(user.id, avatar.id); + }, + + async parsePhotonGroupChange(photonId, user, groupId, gameLogDate) { + if ( + typeof user === 'undefined' || + !this.photonLobbyJointime.has(photonId) + ) { + return; + } + var { groupOnNameplate } = this.photonLobbyJointime.get(photonId); + if ( + typeof groupOnNameplate !== 'undefined' && + groupOnNameplate !== groupId && + photonId !== this.photonLobbyCurrentUser + ) { + var groupName = await this.getGroupName(groupId); + var previousGroupName = + await this.getGroupName(groupOnNameplate); + this.addEntryPhotonEvent({ + photonId, + displayName: user.displayName, + userId: user.id, + text: `ChangeGroup ${groupName}`, + type: 'ChangeGroup', + created_at: gameLogDate, + groupId, + groupName, + previousGroupId: groupOnNameplate, + previousGroupName + }); + } + }, + + parsePhotonAvatar(avatar) { + if ( + typeof avatar === 'undefined' || + typeof avatar.id === 'undefined' + ) { + console.error('PhotonAvatar: avatar is undefined'); + return; + } + var tags = []; + var unityPackages = []; + if (typeof avatar.tags !== 'undefined') { + tags = avatar.tags; + } + if (typeof avatar.unityPackages !== 'undefined') { + unityPackages = avatar.unityPackages; + } + if (!avatar.assetUrl && unityPackages.length > 0) { + for (var unityPackage of unityPackages) { + if ( + unityPackage.variant && + unityPackage.variant !== 'standard' && + unityPackage.variant !== 'security' + ) { + continue; + } + if (unityPackage.platform === 'standalonewindows') { + avatar.assetUrl = unityPackage.assetUrl; + } + } + } + API.applyAvatar({ + id: avatar.id, + authorId: avatar.authorId, + authorName: avatar.authorName, + updated_at: avatar.updated_at, + description: avatar.description, + imageUrl: avatar.imageUrl, + thumbnailImageUrl: avatar.thumbnailImageUrl, + name: avatar.name, + releaseStatus: avatar.releaseStatus, + version: avatar.version, + tags, + unityPackages + }); + }, + + async photonEventTableFilterChange() { + this.photonEventTable.filters[0].value = + this.photonEventTableFilter; + this.photonEventTable.filters[1].value = + this.photonEventTableTypeFilter; + + this.photonEventTablePrevious.filters[0].value = + this.photonEventTableFilter; + this.photonEventTablePrevious.filters[1].value = + this.photonEventTableTypeFilter; + + await configRepository.setString( + 'VRCX_photonEventTypeFilter', + JSON.stringify(this.photonEventTableTypeFilter) + ); + await configRepository.setString( + 'VRCX_photonEventTypeOverlayFilter', + JSON.stringify(this.photonEventTableTypeOverlayFilter) + ); + } + }; +} diff --git a/html/src/classes/groups.js b/html/src/classes/groups.js new file mode 100644 index 000000000..0c614f925 --- /dev/null +++ b/html/src/classes/groups.js @@ -0,0 +1,3528 @@ +import * as workerTimers from 'worker-timers'; +import configRepository from '../repository/config.js'; +import { baseClass, $app, API, $t, $utils } from './baseClass.js'; + +export default class extends baseClass { + constructor(_app, _API, _t) { + super(_app, _API, _t); + } + + init() { + API.cachedGroups = new Map(); + API.currentUserGroups = new Map(); + + /** + * @param {{ groupId: string }} params + */ + API.getGroup = function (params) { + return this.call(`groups/${params.groupId}`, { + method: 'GET', + params: { + includeRoles: params.includeRoles || false + } + }).then((json) => { + var args = { + json, + params + }; + this.$emit('GROUP', args); + return args; + }); + }; + + API.$on('GROUP', function (args) { + args.ref = this.applyGroup(args.json); + this.cachedGroups.set(args.ref.id, args.ref); + if (this.currentUserGroups.has(args.ref.id)) { + this.currentUserGroups.set(args.ref.id, args.ref); + } + }); + + API.$on('GROUP', function (args) { + var { ref } = args; + var D = $app.groupDialog; + if (D.visible === false || D.id !== ref.id) { + return; + } + D.inGroup = ref.membershipStatus === 'member'; + D.ref = ref; + }); + + /** + * @param {{ userId: string }} params + * @return { Promise<{json: any, params}> } + */ + API.getRepresentedGroup = function (params) { + return this.call(`users/${params.userId}/groups/represented`, { + method: 'GET' + }).then((json) => { + var args = { + json, + params + }; + this.$emit('GROUP:REPRESENTED', args); + return args; + }); + }; + + API.$on('GROUP:REPRESENTED', function (args) { + var json = args.json; + if (!json.groupId) { + // no group + return; + } + json.$memberId = json.id; + json.id = json.groupId; + this.$emit('GROUP', { + json, + params: { + groupId: json.groupId, + userId: args.params.userId + } + }); + }); + + /** + * @param {{ userId: string }} params + * @return { Promise<{json: any, params}> } + */ + API.getGroups = function (params) { + return this.call(`users/${params.userId}/groups`, { + method: 'GET' + }).then((json) => { + var args = { + json, + params + }; + this.$emit('GROUP:LIST', args); + return args; + }); + }; + + API.$on('GROUP:LIST', function (args) { + for (var json of args.json) { + json.$memberId = json.id; + json.id = json.groupId; + this.$emit('GROUP', { + json, + params: { + groupId: json.id, + userId: args.params.userId + } + }); + } + }); + + /** + * @param {{ groupId: string }} params + * @return { Promise<{json: any, params}> } + */ + API.joinGroup = function (params) { + return this.call(`groups/${params.groupId}/join`, { + method: 'POST' + }).then((json) => { + var args = { + json, + params + }; + this.$emit('GROUP:JOIN', args); + return args; + }); + }; + + API.$on('GROUP:JOIN', function (args) { + var json = { + $memberId: args.json.id, + id: args.json.groupId, + membershipStatus: args.json.membershipStatus, + myMember: { + isRepresenting: args.json.isRepresenting, + id: args.json.id, + roleIds: args.json.roleIds, + joinedAt: args.json.joinedAt, + membershipStatus: args.json.membershipStatus, + visibility: args.json.visibility, + isSubscribedToAnnouncements: + args.json.isSubscribedToAnnouncements + } + }; + var groupId = json.id; + this.$emit('GROUP', { + json, + params: { + groupId, + userId: args.params.userId + } + }); + if ($app.groupDialog.visible && $app.groupDialog.id === groupId) { + $app.groupDialog.inGroup = json.membershipStatus === 'member'; + $app.getGroupDialogGroup(groupId); + } + }); + + /** + * @param {{ groupId: string }} params + * @return { Promise<{json: any, params}> } + */ + API.leaveGroup = function (params) { + return this.call(`groups/${params.groupId}/leave`, { + method: 'POST' + }).then((json) => { + var args = { + json, + params + }; + this.$emit('GROUP:LEAVE', args); + return args; + }); + }; + + API.$on('GROUP:LEAVE', function (args) { + var groupId = args.params.groupId; + if ($app.groupDialog.visible && $app.groupDialog.id === groupId) { + $app.groupDialog.inGroup = false; + $app.getGroupDialogGroup(groupId); + } + if ( + $app.userDialog.visible && + $app.userDialog.id === this.currentUser.id && + $app.userDialog.representedGroup.id === groupId + ) { + $app.getCurrentUserRepresentedGroup(); + } + }); + + /** + * @param {{ groupId: string }} params + * @return { Promise<{json: any, params}> } + */ + API.cancelGroupRequest = function (params) { + return this.call(`groups/${params.groupId}/requests`, { + method: 'DELETE' + }).then((json) => { + var args = { + json, + params + }; + this.$emit('GROUP:CANCELJOINREQUEST', args); + return args; + }); + }; + + API.$on('GROUP:CANCELJOINREQUEST', function (args) { + var groupId = args.params.groupId; + if ($app.groupDialog.visible && $app.groupDialog.id === groupId) { + $app.getGroupDialogGroup(groupId); + } + }); + + /* + groupId: string, + params: { + isRepresenting: bool + } + */ + API.setGroupRepresentation = function (groupId, params) { + return this.call(`groups/${groupId}/representation`, { + method: 'PUT', + params + }).then((json) => { + var args = { + json, + groupId, + params + }; + this.$emit('GROUP:SETREPRESENTATION', args); + return args; + }); + }; + + API.$on('GROUP:SETREPRESENTATION', function (args) { + if ( + $app.groupDialog.visible && + $app.groupDialog.id === args.groupId + ) { + $app.groupDialog.ref.isRepresenting = + args.params.isRepresenting; + } + if ( + $app.userDialog.visible && + $app.userDialog.id === this.currentUser.id + ) { + $app.getCurrentUserRepresentedGroup(); + } + }); + + /** + * @param {{ query: string }} params + * @return { Promise<{json: any, params}> } + */ + API.groupStrictsearch = function (params) { + return this.call(`groups/strictsearch`, { + method: 'GET', + params + }).then((json) => { + var args = { + json, + params + }; + this.$emit('GROUP:STRICTSEARCH', args); + return args; + }); + }; + + API.$on('GROUP:STRICTSEARCH', function (args) { + for (var json of args.json) { + this.$emit('GROUP', { + json, + params: { + groupId: json.id + } + }); + } + }); + + /* + userId: string, + groupId: string, + params: { + visibility: string, + isSubscribedToAnnouncements: bool, + managerNotes: string + } + */ + API.setGroupMemberProps = function (userId, groupId, params) { + return this.call(`groups/${groupId}/members/${userId}`, { + method: 'PUT', + params + }).then((json) => { + var args = { + json, + userId, + groupId, + params + }; + this.$emit('GROUP:MEMBER:PROPS', args); + return args; + }); + }; + + API.$on('GROUP:MEMBER:PROPS', function (args) { + if (args.userId !== this.currentUser.id) { + return; + } + var json = args.json; + json.$memberId = json.id; + json.id = json.groupId; + if ( + $app.groupDialog.visible && + $app.groupDialog.id === json.groupId + ) { + $app.groupDialog.ref.myMember.visibility = json.visibility; + $app.groupDialog.ref.myMember.isSubscribedToAnnouncements = + json.isSubscribedToAnnouncements; + } + if ( + $app.userDialog.visible && + $app.userDialog.id === this.currentUser.id + ) { + $app.getCurrentUserRepresentedGroup(); + } + this.$emit('GROUP:MEMBER', { + json, + params: { + groupId: json.groupId + } + }); + }); + + API.$on('GROUP:MEMBER:PROPS', function (args) { + if ($app.groupDialog.id === args.json.groupId) { + for (var i = 0; i < $app.groupDialog.members.length; ++i) { + var member = $app.groupDialog.members[i]; + if (member.userId === args.json.userId) { + Object.assign(member, this.applyGroupMember(args.json)); + break; + } + } + for ( + var i = 0; + i < $app.groupDialog.memberSearchResults.length; + ++i + ) { + var member = $app.groupDialog.memberSearchResults[i]; + if (member.userId === args.json.userId) { + Object.assign(member, this.applyGroupMember(args.json)); + break; + } + } + } + if ( + $app.groupMemberModeration.visible && + $app.groupMemberModeration.id === args.json.groupId + ) { + // force redraw table + $app.groupMembersSearch(); + } + }); + + /** + * @param {{ + userId: string, + groupId: string, + roleId: string + }} params + * @return { Promise<{json: any, params}> } + */ + API.addGroupMemberRole = function (params) { + return this.call( + `groups/${params.groupId}/members/${params.userId}/roles/${params.roleId}`, + { + method: 'PUT' + } + ).then((json) => { + var args = { + json, + params + }; + this.$emit('GROUP:MEMBER:ROLE:CHANGE', args); + return args; + }); + }; + + /** + * @param {{ + userId: string, + groupId: string, + roleId: string + }} params + * @return { Promise<{json: any, params}> } + */ + API.removeGroupMemberRole = function (params) { + return this.call( + `groups/${params.groupId}/members/${params.userId}/roles/${params.roleId}`, + { + method: 'DELETE' + } + ).then((json) => { + var args = { + json, + params + }; + this.$emit('GROUP:MEMBER:ROLE:CHANGE', args); + return args; + }); + }; + + API.$on('GROUP:MEMBER:ROLE:CHANGE', function (args) { + if ($app.groupDialog.id === args.params.groupId) { + for (var i = 0; i < $app.groupDialog.members.length; ++i) { + var member = $app.groupDialog.members[i]; + if (member.userId === args.params.userId) { + member.roleIds = args.json; + break; + } + } + for ( + var i = 0; + i < $app.groupDialog.memberSearchResults.length; + ++i + ) { + var member = $app.groupDialog.memberSearchResults[i]; + if (member.userId === args.params.userId) { + member.roleIds = args.json; + break; + } + } + } + + if ( + $app.groupMemberModeration.visible && + $app.groupMemberModeration.id === args.params.groupId + ) { + // force redraw table + $app.groupMembersSearch(); + } + }); + + API.getGroupPermissions = function (params) { + return this.call(`users/${params.userId}/groups/permissions`, { + method: 'GET' + }).then((json) => { + var args = { + json, + params + }; + this.$emit('GROUP:PERMISSIONS', args); + return args; + }); + }; + + API.$on('GROUP:PERMISSIONS', function (args) { + if (args.params.userId !== this.currentUser.id) { + return; + } + var json = args.json; + for (var groupId in json) { + var permissions = json[groupId]; + var group = this.cachedGroups.get(groupId); + if (group) { + group.myMember.permissions = permissions; + } + } + }); + + // /** + // * @param {{ groupId: string }} params + // * @return { Promise<{json: any, params}> } + // */ + // API.getGroupAnnouncement = function (params) { + // return this.call(`groups/${params.groupId}/announcement`, { + // method: 'GET' + // }).then((json) => { + // var args = { + // json, + // params + // }; + // this.$emit('GROUP:ANNOUNCEMENT', args); + // return args; + // }); + // }; + + /** + * @param {{ + groupId: string, + n: number, + offset: number + }} params + * @return { Promise<{json: any, params}> } + */ + API.getGroupPosts = function (params) { + return this.call(`groups/${params.groupId}/posts`, { + method: 'GET', + params + }).then((json) => { + var args = { + json, + params + }; + this.$emit('GROUP:POSTS', args); + return args; + }); + }; + + /** + * @param {{ groupId: string }} params + * @return { Promise<{json: any, params}> } + */ + API.getAllGroupPosts = async function (params) { + var posts = []; + var offset = 0; + var n = 100; + var total = 0; + do { + var args = await this.getGroupPosts({ + groupId: params.groupId, + n, + offset + }); + posts = posts.concat(args.json.posts); + total = args.json.total; + offset += n; + } while (offset < total); + var returnArgs = { + posts, + params + }; + this.$emit('GROUP:POSTS:ALL', returnArgs); + return returnArgs; + }; + + API.$on('GROUP:POSTS:ALL', function (args) { + var D = $app.groupDialog; + if (D.id === args.params.groupId) { + for (var post of args.posts) { + post.title = $app.replaceBioSymbols(post.title); + post.text = $app.replaceBioSymbols(post.text); + } + if (args.posts.length > 0) { + D.announcement = args.posts[0]; + } + D.posts = args.posts; + $app.updateGroupPostSearch(); + } + }); + + API.$on('GROUP:POST', function (args) { + var D = $app.groupDialog; + if (D.id !== args.params.groupId) { + return; + } + + var newPost = args.json; + newPost.title = $app.replaceBioSymbols(newPost.title); + newPost.text = $app.replaceBioSymbols(newPost.text); + var hasPost = false; + // update existing post + for (var post of D.posts) { + if (post.id === newPost.id) { + Object.assign(post, newPost); + hasPost = true; + break; + } + } + // set or update announcement + if (newPost.id === D.announcement.id || !D.announcement.id) { + D.announcement = newPost; + } + // add new post + if (!hasPost) { + D.posts.unshift(newPost); + } + $app.updateGroupPostSearch(); + }); + + API.$on('GROUP:POST:DELETE', function (args) { + var D = $app.groupDialog; + if (D.id !== args.params.groupId) { + return; + } + + var postId = args.params.postId; + // remove existing post + for (var post of D.posts) { + if (post.id === postId) { + $app.removeFromArray(D.posts, post); + break; + } + } + // remove/update announcement + if (postId === D.announcement.id) { + if (D.posts.length > 0) { + D.announcement = D.posts[0]; + } else { + D.announcement = {}; + } + } + $app.updateGroupPostSearch(); + }); + + /** + * @param {{ groupId: string, postId: string }} params + * @return { Promise<{json: any, params}> } + */ + API.deleteGroupPost = function (params) { + return this.call( + `groups/${params.groupId}/posts/${params.postId}`, + { + method: 'DELETE' + } + ).then((json) => { + var args = { + json, + params + }; + this.$emit('GROUP:POST:DELETE', args); + return args; + }); + }; + + API.editGroupPost = function (params) { + return this.call( + `groups/${params.groupId}/posts/${params.postId}`, + { + method: 'PUT', + params + } + ).then((json) => { + var args = { + json, + params + }; + this.$emit('GROUP:POST', args); + return args; + }); + }; + + API.createGroupPost = function (params) { + return this.call(`groups/${params.groupId}/posts`, { + method: 'POST', + params + }).then((json) => { + var args = { + json, + params + }; + this.$emit('GROUP:POST', args); + return args; + }); + }; + + /** + * @param {{ + groupId: string, + userId: string + }} params + * @return { Promise<{json: any, params}> } + */ + API.getGroupMember = function (params) { + return this.call( + `groups/${params.groupId}/members/${params.userId}`, + { + method: 'GET' + } + ).then((json) => { + var args = { + json, + params + }; + this.$emit('GROUP:MEMBER', args); + return args; + }); + }; + + /** + * @param {{ + groupId: string, + n: number, + offset: number + }} params + * @return { Promise<{json: any, params}> } + */ + API.getGroupMembers = function (params) { + return this.call(`groups/${params.groupId}/members`, { + method: 'GET', + params + }).then((json) => { + var args = { + json, + params + }; + this.$emit('GROUP:MEMBERS', args); + return args; + }); + }; + + API.$on('GROUP:MEMBERS', function (args) { + for (var json of args.json) { + this.$emit('GROUP:MEMBER', { + json, + params: { + groupId: args.params.groupId + } + }); + } + }); + + API.$on('GROUP:MEMBER', function (args) { + args.ref = this.applyGroupMember(args.json); + }); + + /** + * @param {{ + groupId: string, + query: string, + n: number, + offset: number + }} params + * @return { Promise<{json: any, params}> } + */ + API.getGroupMembersSearch = function (params) { + return this.call(`groups/${params.groupId}/members/search`, { + method: 'GET', + params + }).then((json) => { + var args = { + json, + params + }; + this.$emit('GROUP:MEMBERS:SEARCH', args); + return args; + }); + }; + + API.$on('GROUP:MEMBERS:SEARCH', function (args) { + for (var json of args.json.results) { + this.$emit('GROUP:MEMBER', { + json, + params: { + groupId: args.params.groupId + } + }); + } + }); + + /** + * @param {{ + groupId: string + * }} params + * @return { Promise<{json: any, params}> } + */ + API.blockGroup = function (params) { + return this.call(`groups/${params.groupId}/block`, { + method: 'POST' + }).then((json) => { + var args = { + json, + params + }; + this.$emit('GROUP:BLOCK', args); + return args; + }); + }; + + /** + * @param {{ + groupId: string, + userId: string + * }} params + * @return { Promise<{json: any, params}> } + */ + API.unblockGroup = function (params) { + return this.call( + `groups/${params.groupId}/members/${params.userId}`, + { + method: 'DELETE' + } + ).then((json) => { + var args = { + json, + params + }; + this.$emit('GROUP:UNBLOCK', args); + return args; + }); + }; + + API.$on('GROUP:BLOCK', function (args) { + if ( + $app.groupDialog.visible && + $app.groupDialog.id === args.params.groupId + ) { + $app.showGroupDialog(args.params.groupId); + } + }); + + API.$on('GROUP:UNBLOCK', function (args) { + if ( + $app.groupDialog.visible && + $app.groupDialog.id === args.params.groupId + ) { + $app.showGroupDialog(args.params.groupId); + } + }); + + /** + * @param {{ + groupId: string, + userId: string + * }} params + * @return { Promise<{json: any, params}> } + */ + API.sendGroupInvite = function (params) { + return this.call(`groups/${params.groupId}/invites`, { + method: 'POST', + params: { + userId: params.userId + } + }).then((json) => { + var args = { + json, + params + }; + this.$emit('GROUP:INVITE', args); + return args; + }); + }; + + /** + * @param {{ + groupId: string, + userId: string + }} params + * @return { Promise<{json: any, params}> } + */ + API.kickGroupMember = function (params) { + return this.call( + `groups/${params.groupId}/members/${params.userId}`, + { + method: 'DELETE' + } + ).then((json) => { + var args = { + json, + params + }; + this.$emit('GROUP:MEMBER:KICK', args); + return args; + }); + }; + + /** + * @param {{ groupId: string, userId: string }} params + * @return { Promise<{json: any, params}> } + */ + API.banGroupMember = function (params) { + return this.call(`groups/${params.groupId}/bans`, { + method: 'POST', + params: { + userId: params.userId + } + }).then((json) => { + var args = { + json, + params + }; + this.$emit('GROUP:MEMBER:BAN', args); + return args; + }); + }; + + /** + * @param {{ groupId: string, userId: string }} params + * @return { Promise<{json: any, params}> } + */ + API.unbanGroupMember = function (params) { + return this.call(`groups/${params.groupId}/bans/${params.userId}`, { + method: 'DELETE' + }).then((json) => { + var args = { + json, + params + }; + this.$emit('GROUP:MEMBER:UNBAN', args); + return args; + }); + }; + + API.deleteSentGroupInvite = function (params) { + return this.call( + `groups/${params.groupId}/invites/${params.userId}`, + { + method: 'DELETE' + } + ).then((json) => { + var args = { + json, + params + }; + this.$emit('GROUP:INVITE:DELETE', args); + return args; + }); + }; + + API.deleteBlockedGroupRequest = function (params) { + return this.call( + `groups/${params.groupId}/members/${params.userId}`, + { + method: 'DELETE' + } + ).then((json) => { + var args = { + json, + params + }; + this.$emit('GROUP:BLOCKED:DELETE', args); + return args; + }); + }; + + API.acceptGroupInviteRequest = function (params) { + return this.call( + `groups/${params.groupId}/requests/${params.userId}`, + { + method: 'PUT', + params: { + action: 'accept' + } + } + ).then((json) => { + var args = { + json, + params + }; + this.$emit('GROUP:INVITE:ACCEPT', args); + return args; + }); + }; + + API.rejectGroupInviteRequest = function (params) { + return this.call( + `groups/${params.groupId}/requests/${params.userId}`, + { + method: 'PUT', + params: { + action: 'reject' + } + } + ).then((json) => { + var args = { + json, + params + }; + this.$emit('GROUP:INVITE:REJECT', args); + return args; + }); + }; + + API.blockGroupInviteRequest = function (params) { + return this.call( + `groups/${params.groupId}/requests/${params.userId}`, + { + method: 'PUT', + params: { + action: 'reject', + block: true + } + } + ).then((json) => { + var args = { + json, + params + }; + this.$emit('GROUP:INVITE:BLOCK', args); + return args; + }); + }; + + API.getGroupBans = function (params) { + return this.call(`groups/${params.groupId}/bans`, { + method: 'GET', + params + }).then((json) => { + var args = { + json, + params + }; + this.$emit('GROUP:BANS', args); + return args; + }); + }; + + API.$on('GROUP:BANS', function (args) { + if ($app.groupMemberModeration.id !== args.params.groupId) { + return; + } + + for (var json of args.json) { + var ref = this.applyGroupMember(json); + $app.groupBansModerationTable.data.push(ref); + } + }); + + /** + * @param {{ groupId: string }} params + * @return { Promise<{json: any, params}> } + */ + API.getGroupAuditLogTypes = function (params) { + return this.call(`groups/${params.groupId}/auditLogTypes`, { + method: 'GET' + }).then((json) => { + var args = { + json, + params + }; + this.$emit('GROUP:AUDITLOGTYPES', args); + return args; + }); + }; + + API.$on('GROUP:AUDITLOGTYPES', function (args) { + if ($app.groupMemberModeration.id !== args.params.groupId) { + return; + } + + $app.groupMemberModeration.auditLogTypes = args.json; + }); + + /** + * @param {{ groupId: string, eventTypes: array }} params + * @return { Promise<{json: any, params}> } + */ + API.getGroupLogs = function (params) { + return this.call(`groups/${params.groupId}/auditLogs`, { + method: 'GET', + params + }).then((json) => { + var args = { + json, + params + }; + this.$emit('GROUP:LOGS', args); + return args; + }); + }; + + API.$on('GROUP:LOGS', function (args) { + if ($app.groupMemberModeration.id !== args.params.groupId) { + return; + } + + for (var json of args.json.results) { + const existsInData = $app.groupLogsModerationTable.data.some( + (dataItem) => dataItem.id === json.id + ); + if (!existsInData) { + $app.groupLogsModerationTable.data.push(json); + } + } + }); + + /** + * @param {{ groupId: string }} params + * @return { Promise<{json: any, params}> } + */ + API.getGroupInvites = function (params) { + return this.call(`groups/${params.groupId}/invites`, { + method: 'GET', + params + }).then((json) => { + var args = { + json, + params + }; + this.$emit('GROUP:INVITES', args); + return args; + }); + }; + + API.$on('GROUP:INVITES', function (args) { + if ($app.groupMemberModeration.id !== args.params.groupId) { + return; + } + + for (var json of args.json) { + var ref = this.applyGroupMember(json); + $app.groupInvitesModerationTable.data.push(ref); + } + }); + + /** + * @param {{ groupId: string }} params + * @return { Promise<{json: any, params}> } + */ + API.getGroupJoinRequests = function (params) { + return this.call(`groups/${params.groupId}/requests`, { + method: 'GET', + params + }).then((json) => { + var args = { + json, + params + }; + this.$emit('GROUP:JOINREQUESTS', args); + return args; + }); + }; + + API.$on('GROUP:JOINREQUESTS', function (args) { + if ($app.groupMemberModeration.id !== args.params.groupId) { + return; + } + + if (!args.params.blocked) { + for (var json of args.json) { + var ref = this.applyGroupMember(json); + $app.groupJoinRequestsModerationTable.data.push(ref); + } + } else { + for (var json of args.json) { + var ref = this.applyGroupMember(json); + $app.groupBlockedModerationTable.data.push(ref); + } + } + }); + + /** + * @param {{ groupId: string }} params + * @return { Promise<{json: any, params}> } + */ + API.getGroupInstances = function (params) { + return this.call( + `users/${this.currentUser.id}/instances/groups/${params.groupId}`, + { + method: 'GET' + } + ).then((json) => { + var args = { + json, + params + }; + this.$emit('GROUP:INSTANCES', args); + return args; + }); + }; + + API.$on('GROUP:INSTANCES', function (args) { + if ($app.groupDialog.id === args.params.groupId) { + $app.applyGroupDialogInstances(args.json.instances); + } + }); + + API.$on('GROUP:INSTANCES', function (args) { + for (var json of args.json.instances) { + this.$emit('INSTANCE', { + json, + params: { + fetchedAt: args.json.fetchedAt + } + }); + this.getCachedWorld({ + worldId: json.world.id + }).then((args1) => { + json.world = args1.ref; + return args1; + }); + // get queue size etc + this.getInstance({ + worldId: json.worldId, + instanceId: json.instanceId + }); + } + }); + + /** + * @param {{ groupId: string }} params + * @return { Promise<{json: any, params}> } + */ + + API.getGroupRoles = function (params) { + return this.call(`groups/${params.groupId}/roles`, { + method: 'GET', + params + }).then((json) => { + var args = { + json, + params + }; + this.$emit('GROUP:ROLES', args); + return args; + }); + }; + + API.getRequestedGroups = function () { + return this.call(`users/${this.currentUser.id}/groups/requested`, { + method: 'GET' + }).then((json) => { + var args = { + json + }; + this.$emit('GROUP:REQUESTED', args); + return args; + }); + }; + + API.getUsersGroupInstances = function () { + return this.call(`users/${this.currentUser.id}/instances/groups`, { + method: 'GET' + }).then((json) => { + var args = { + json + }; + this.$emit('GROUP:USER:INSTANCES', args); + return args; + }); + }; + + API.$on('GROUP:USER:INSTANCES', function (args) { + $app.groupInstances = []; + for (var json of args.json.instances) { + if (args.json.fetchedAt) { + // tack on fetchedAt + json.$fetchedAt = args.json.fetchedAt; + } + this.$emit('INSTANCE', { + json, + params: { + fetchedAt: args.json.fetchedAt + } + }); + var ref = this.cachedGroups.get(json.ownerId); + if (typeof ref === 'undefined') { + if ($app.friendLogInitStatus) { + this.getGroup({ groupId: json.ownerId }); + } + return; + } + $app.groupInstances.push({ + group: ref, + instance: this.applyInstance(json) + }); + } + }); + + /** + * @param {{ + query: string, + n: number, + offset: number, + order: string, + sortBy: string + }} params + * @return { Promise<{json: any, params}> } + */ + API.groupSearch = function (params) { + return this.call(`groups`, { + method: 'GET', + params + }).then((json) => { + var args = { + json, + params + }; + this.$emit('GROUP:SEARCH', args); + return args; + }); + }; + + API.$on('GROUP:SEARCH', function (args) { + for (var json of args.json) { + this.$emit('GROUP', { + json, + params: { + groupId: json.id + } + }); + } + }); + + /** + * @param {{ groupId: string }} params + * @return { Promise<{json: any, params}> } + */ + API.getCachedGroup = function (params) { + return new Promise((resolve, reject) => { + var ref = this.cachedGroups.get(params.groupId); + if (typeof ref === 'undefined') { + this.getGroup(params).catch(reject).then(resolve); + } else { + resolve({ + cache: true, + json: ref, + params, + ref + }); + } + }); + }; + + API.applyGroup = function (json) { + var ref = this.cachedGroups.get(json.id); + json.rules = $app.replaceBioSymbols(json.rules); + json.name = $app.replaceBioSymbols(json.name); + json.description = $app.replaceBioSymbols(json.description); + if (typeof ref === 'undefined') { + ref = { + id: '', + name: '', + shortCode: '', + description: '', + bannerId: '', + bannerUrl: '', + createdAt: '', + discriminator: '', + galleries: [], + iconId: '', + iconUrl: '', + isVerified: false, + joinState: '', + languages: [], + links: [], + memberCount: 0, + memberCountSyncedAt: '', + membershipStatus: '', + onlineMemberCount: 0, + ownerId: '', + privacy: '', + rules: null, + tags: [], + // in group + initialRoleIds: [], + myMember: { + bannedAt: null, + groupId: '', + has2FA: false, + id: '', + isRepresenting: false, + isSubscribedToAnnouncements: false, + joinedAt: '', + managerNotes: '', + membershipStatus: '', + permissions: [], + roleIds: [], + userId: '', + visibility: '', + _created_at: '', + _id: '', + _updated_at: '' + }, + updatedAt: '', + // includeRoles: true + roles: [], + // group list + $memberId: '', + groupId: '', + isRepresenting: false, + memberVisibility: false, + mutualGroup: false, + // VRCX + $languages: [], + ...json + }; + this.cachedGroups.set(ref.id, ref); + } else { + if (this.currentUserGroups.has(ref.id)) { + // compare group props + if ( + ref.ownerId && + json.ownerId && + ref.ownerId !== json.ownerId + ) { + // owner changed + $app.groupOwnerChange(json, ref.ownerId, json.ownerId); + } + if (ref.name && json.name && ref.name !== json.name) { + // name changed + $app.groupChange( + json, + `Name changed from ${ref.name} to ${json.name}` + ); + } + if (ref.myMember?.roleIds && json.myMember?.roleIds) { + var oldRoleIds = ref.myMember.roleIds; + var newRoleIds = json.myMember.roleIds; + if ( + oldRoleIds.length !== newRoleIds.length || + !oldRoleIds.every( + (value, index) => value === newRoleIds[index] + ) + ) { + // roleIds changed + $app.groupRoleChange( + json, + ref.roles, + json.roles, + oldRoleIds, + newRoleIds + ); + } + } + } + Object.assign(ref, json); + } + ref.$url = `https://vrc.group/${ref.shortCode}.${ref.discriminator}`; + this.applyGroupLanguage(ref); + return ref; + }; + + API.applyGroupMember = function (json) { + if (typeof json?.user !== 'undefined') { + if (json.userId === this.currentUser.id) { + json.user = this.currentUser; + json.$displayName = this.currentUser.displayName; + } else { + var ref = this.cachedUsers.get(json.user.id); + if (typeof ref !== 'undefined') { + json.user = ref; + json.$displayName = ref.displayName; + } else { + json.$displayName = json.user?.displayName; + } + } + } + return json; + }; + + API.applyGroupLanguage = function (ref) { + ref.$languages = []; + var { languages } = ref; + if (!languages) { + return; + } + for (var language of languages) { + var value = $app.subsetOfLanguages[language]; + if (typeof value === 'undefined') { + continue; + } + ref.$languages.push({ + key: language, + value + }); + } + }; + + API.$on('LOGOUT', function () { + $app.groupDialog.visible = false; + $app.inviteGroupDialog.visible = false; + $app.groupPostEditDialog.visible = false; + }); + + /** + * @param {{ + groupId: string, + galleryId: string, + n: number, + offset: number + }} params + * @return { Promise<{json: any, params}> } + */ + API.getGroupGallery = function (params) { + return this.call( + `groups/${params.groupId}/galleries/${params.galleryId}`, + { + method: 'GET', + params: { + n: params.n, + offset: params.offset + } + } + ).then((json) => { + var args = { + json, + params + }; + this.$emit('GROUP:GALLERY', args); + return args; + }); + }; + + API.$on('GROUP:GALLERY', function (args) { + for (var json of args.json) { + if ($app.groupDialog.id === json.groupId) { + if (!$app.groupDialog.galleries[json.galleryId]) { + $app.groupDialog.galleries[json.galleryId] = []; + } + $app.groupDialog.galleries[json.galleryId].push(json); + } + } + }); + } + + _data = { + currentUserGroupsInit: false, + groupDialogLastActiveTab: '', + groupDialogLastMembers: '', + groupDialogLastGallery: '', + groupMembersSearchTimer: null, + groupMembersSearchPending: false, + isGroupMembersLoading: false, + isGroupMembersDone: false, + isGroupGalleryLoading: false, + loadMoreGroupMembersParams: {}, + groupMemberModerationTableForceUpdate: 0, + + groupDialog: { + visible: false, + loading: false, + treeData: [], + id: '', + inGroup: false, + ownerDisplayName: '', + ref: {}, + announcement: {}, + posts: [], + postsFiltered: [], + members: [], + memberSearch: '', + memberSearchResults: [], + instances: [], + memberRoles: [], + memberFilter: { + name: $t('dialog.group.members.filters.everyone'), + id: null + }, + memberSortOrder: { + name: $t('dialog.group.members.sorting.joined_at_desc'), + value: 'joinedAt:desc' + }, + postsSearch: '', + galleries: {} + }, + inviteGroupDialog: { + visible: false, + loading: false, + groupId: '', + groupName: '', + userId: '', + userIds: [], + userObject: {} + }, + groupPostEditDialog: { + visible: false, + groupRef: {}, + title: '', + text: '', + sendNotification: true, + visibility: 'group', + roleIds: [], + postId: '', + groupId: '' + }, + groupMemberModeration: { + visible: false, + loading: false, + id: '', + groupRef: {}, + auditLogTypes: [], + selectedAuditLogTypes: [], + note: '', + selectedUsers: new Map(), + selectedUsersArray: [], + selectedRoles: [], + progressCurrent: 0, + progressTotal: 0, + selectUserId: '' + }, + groupMemberModerationTable: { + data: [], + tableProps: { + stripe: true, + size: 'mini' + }, + pageSize: 15, + paginationProps: { + small: true, + layout: 'sizes,prev,pager,next,total', + pageSizes: [10, 15, 25, 50, 100] + } + }, + groupBansModerationTable: { + data: [], + filters: [ + { + prop: ['$displayName'], + value: '' + } + ], + tableProps: { + stripe: true, + size: 'mini' + }, + pageSize: 15, + paginationProps: { + small: true, + layout: 'sizes,prev,pager,next,total', + pageSizes: [10, 15, 25, 50, 100] + } + }, + groupLogsModerationTable: { + data: [], + filters: [ + { + prop: ['description'], + value: '' + } + ], + tableProps: { + stripe: true, + size: 'mini' + }, + pageSize: 15, + paginationProps: { + small: true, + layout: 'sizes,prev,pager,next,total', + pageSizes: [10, 15, 25, 50, 100] + } + }, + groupInvitesModerationTable: { + data: [], + tableProps: { + stripe: true, + size: 'mini' + }, + pageSize: 15, + paginationProps: { + small: true, + layout: 'sizes,prev,pager,next,total', + pageSizes: [10, 15, 25, 50, 100] + } + }, + groupJoinRequestsModerationTable: { + data: [], + tableProps: { + stripe: true, + size: 'mini' + }, + pageSize: 15, + paginationProps: { + small: true, + layout: 'sizes,prev,pager,next,total', + pageSizes: [10, 15, 25, 50, 100] + } + }, + groupBlockedModerationTable: { + data: [], + tableProps: { + stripe: true, + size: 'mini' + }, + pageSize: 15, + paginationProps: { + small: true, + layout: 'sizes,prev,pager,next,total', + pageSizes: [10, 15, 25, 50, 100] + } + } + }; + + _methods = { + confirmDeleteGroupPost(post) { + this.$confirm( + 'Are you sure you want to delete this post?', + 'Confirm', + { + confirmButtonText: 'Confirm', + cancelButtonText: 'Cancel', + type: 'info', + callback: (action) => { + if (action === 'confirm') { + API.deleteGroupPost({ + groupId: post.groupId, + postId: post.id + }); + } + } + } + ); + }, + + blockGroup(groupId) { + this.$confirm( + 'Are you sure you want to block this group?', + 'Confirm', + { + confirmButtonText: 'Confirm', + cancelButtonText: 'Cancel', + type: 'info', + callback: (action) => { + if (action === 'confirm') { + API.blockGroup({ + groupId + }); + } + } + } + ); + }, + + unblockGroup(groupId) { + this.$confirm( + 'Are you sure you want to unblock this group?', + 'Confirm', + { + confirmButtonText: 'Confirm', + cancelButtonText: 'Cancel', + type: 'info', + callback: (action) => { + if (action === 'confirm') { + API.unblockGroup({ + groupId, + userId: API.currentUser.id + }); + } + } + } + ); + }, + + async getAllGroupBans(groupId) { + this.groupBansModerationTable.data = []; + var params = { + groupId, + n: 100, + offset: 0 + }; + var count = 50; // 5000 max + this.isGroupMembersLoading = true; + try { + for (var i = 0; i < count; i++) { + var args = await API.getGroupBans(params); + params.offset += params.n; + if (args.json.length < params.n) { + break; + } + if (!this.groupMemberModeration.visible) { + break; + } + } + } catch (err) { + this.$message({ + message: 'Failed to get group bans', + type: 'error' + }); + } finally { + this.isGroupMembersLoading = false; + } + }, + + async getAllGroupLogs(groupId) { + this.groupLogsModerationTable.data = []; + var params = { + groupId, + n: 100, + offset: 0 + }; + if (this.groupMemberModeration.selectedAuditLogTypes.length) { + params.eventTypes = + this.groupMemberModeration.selectedAuditLogTypes; + } + var count = 50; // 5000 max + this.isGroupMembersLoading = true; + try { + for (var i = 0; i < count; i++) { + var args = await API.getGroupLogs(params); + params.offset += params.n; + if (!args.json.hasNext) { + break; + } + if (!this.groupMemberModeration.visible) { + break; + } + } + } catch (err) { + this.$message({ + message: 'Failed to get group logs', + type: 'error' + }); + } finally { + this.isGroupMembersLoading = false; + } + }, + + getAuditLogTypeName(auditLogType) { + if (!auditLogType) { + return ''; + } + return auditLogType + .replace('group.', '') + .replace(/\./g, ' ') + .replace(/\b\w/g, (l) => l.toUpperCase()); + }, + + async getAllGroupInvitesAndJoinRequests(groupId) { + await this.getAllGroupInvites(groupId); + await this.getAllGroupJoinRequests(groupId); + await this.getAllGroupBlockedRequests(groupId); + }, + + async getAllGroupInvites(groupId) { + this.groupInvitesModerationTable.data = []; + var params = { + groupId, + n: 100, + offset: 0 + }; + var count = 50; // 5000 max + this.isGroupMembersLoading = true; + try { + for (var i = 0; i < count; i++) { + var args = await API.getGroupInvites(params); + params.offset += params.n; + if (args.json.length < params.n) { + break; + } + if (!this.groupMemberModeration.visible) { + break; + } + } + } catch (err) { + this.$message({ + message: 'Failed to get group invites', + type: 'error' + }); + } finally { + this.isGroupMembersLoading = false; + } + }, + + async getAllGroupJoinRequests(groupId) { + this.groupJoinRequestsModerationTable.data = []; + var params = { + groupId, + n: 100, + offset: 0 + }; + var count = 50; // 5000 max + this.isGroupMembersLoading = true; + try { + for (var i = 0; i < count; i++) { + var args = await API.getGroupJoinRequests(params); + params.offset += params.n; + if (args.json.length < params.n) { + break; + } + if (!this.groupMemberModeration.visible) { + break; + } + } + } catch (err) { + this.$message({ + message: 'Failed to get group join requests', + type: 'error' + }); + } finally { + this.isGroupMembersLoading = false; + } + }, + + async getAllGroupBlockedRequests(groupId) { + this.groupBlockedModerationTable.data = []; + var params = { + groupId, + n: 100, + offset: 0, + blocked: true + }; + var count = 50; // 5000 max + this.isGroupMembersLoading = true; + try { + for (var i = 0; i < count; i++) { + var args = await API.getGroupJoinRequests(params); + params.offset += params.n; + if (args.json.length < params.n) { + break; + } + if (!this.groupMemberModeration.visible) { + break; + } + } + } catch (err) { + this.$message({ + message: 'Failed to get group join requests', + type: 'error' + }); + } finally { + this.isGroupMembersLoading = false; + } + }, + + async groupOwnerChange(ref, oldUserId, newUserId) { + var oldUser = await API.getCachedUser({ + userId: oldUserId + }); + var newUser = await API.getCachedUser({ + userId: newUserId + }); + var oldDisplayName = oldUser?.ref?.displayName; + var newDisplayName = newUser?.ref?.displayName; + + this.groupChange( + ref, + `Owner changed from ${oldDisplayName} to ${newDisplayName}` + ); + }, + + groupRoleChange(ref, oldRoles, newRoles, oldRoleIds, newRoleIds) { + // check for removed/added roleIds + for (var roleId of oldRoleIds) { + if (!newRoleIds.includes(roleId)) { + var roleName = ''; + var role = oldRoles.find( + (fineRole) => fineRole.id === roleId + ); + if (role) { + roleName = role.name; + } + this.groupChange(ref, `Role ${roleName} removed`); + } + } + for (var roleId of newRoleIds) { + if (!oldRoleIds.includes(roleId)) { + var roleName = ''; + var role = newRoles.find( + (fineRole) => fineRole.id === roleId + ); + if (role) { + roleName = role.name; + } + this.groupChange(ref, `Role ${roleName} added`); + } + } + }, + + groupChange(ref, message) { + if (!this.currentUserGroupsInit) { + return; + } + // oh the level of cursed for compibility + var json = { + id: Math.random().toString(36), + type: 'groupChange', + senderUserId: ref.id, + senderUsername: ref.name, + imageUrl: ref.iconUrl, + details: { + imageUrl: ref.iconUrl + }, + message, + created_at: new Date().toJSON() + }; + API.$emit('NOTIFICATION', { + json, + params: { + notificationId: json.id + } + }); + + // delay to wait for json to be assigned to ref + workerTimers.setTimeout(this.saveCurrentUserGroups, 100); + }, + + saveCurrentUserGroups() { + if (!this.currentUserGroupsInit) { + return; + } + var groups = []; + for (var ref of API.currentUserGroups.values()) { + groups.push({ + id: ref.id, + name: ref.name, + ownerId: ref.ownerId, + iconUrl: ref.iconUrl, + roles: ref.roles, + roleIds: ref.myMember?.roleIds + }); + } + configRepository.setString( + `VRCX_currentUserGroups_${API.currentUser.id}`, + JSON.stringify(groups) + ); + }, + + async loadCurrentUserGroups(userId, groups) { + var savedGroups = JSON.parse( + await configRepository.getString( + `VRCX_currentUserGroups_${userId}`, + '[]' + ) + ); + API.cachedGroups.clear(); + API.currentUserGroups.clear(); + for (var group of savedGroups) { + var ref = { + id: group.id, + name: group.name, + iconUrl: group.iconUrl, + ownerId: group.ownerId, + roles: group.roles, + myMember: { + roleIds: group.roleIds + } + }; + API.cachedGroups.set(group.id, ref); + API.currentUserGroups.set(group.id, ref); + } + + if (groups) { + for (var i = 0; i < groups.length; i++) { + var groupId = groups[i]; + var groupRef = API.cachedGroups.get(groupId); + if ( + typeof groupRef !== 'undefined' && + groupRef.myMember?.roleIds?.length > 0 + ) { + continue; + } + + try { + var args = await API.getGroup({ + groupId, + includeRoles: true + }); + var ref = API.applyGroup(args.json); + API.currentUserGroups.set(groupId, ref); + } catch (err) { + console.error(err); + } + } + } + + this.currentUserGroupsInit = true; + }, + + showGroupDialog(groupId) { + if (!groupId) { + return; + } + if ( + this.groupMemberModeration.visible && + this.groupMemberModeration.id !== groupId + ) { + this.groupMemberModeration.visible = false; + } + this.$nextTick(() => + $app.adjustDialogZ(this.$refs.groupDialog.$el) + ); + var D = this.groupDialog; + D.visible = true; + D.loading = true; + D.id = groupId; + D.inGroup = false; + D.ownerDisplayName = ''; + D.treeData = []; + D.announcement = {}; + D.posts = []; + D.postsFiltered = []; + D.instances = []; + D.memberRoles = []; + D.memberSearch = ''; + D.memberSearchResults = []; + if (this.groupDialogLastGallery !== groupId) { + D.galleries = {}; + } + if (this.groupDialogLastMembers !== groupId) { + D.members = []; + D.memberFilter = this.groupDialogFilterOptions.everyone; + } + API.getCachedGroup({ + groupId + }) + .catch((err) => { + D.loading = false; + D.visible = false; + this.$message({ + message: 'Failed to load group', + type: 'error' + }); + throw err; + }) + .then((args) => { + if (groupId === args.ref.id) { + D.loading = false; + D.ref = args.ref; + D.inGroup = args.ref.membershipStatus === 'member'; + D.ownerDisplayName = args.ref.ownerId; + API.getCachedUser({ + userId: args.ref.ownerId + }).then((args1) => { + D.ownerDisplayName = args1.ref.displayName; + return args1; + }); + this.applyGroupDialogInstances(); + this.getGroupDialogGroup(groupId); + } + }); + }, + + getGroupDialogGroup(groupId) { + var D = this.groupDialog; + return API.getGroup({ groupId, includeRoles: true }) + .catch((err) => { + throw err; + }) + .then((args1) => { + if (D.id === args1.ref.id) { + D.ref = args1.ref; + D.inGroup = args1.ref.membershipStatus === 'member'; + for (var role of args1.ref.roles) { + if ( + D.ref && + D.ref.myMember && + Array.isArray(D.ref.myMember.roleIds) && + D.ref.myMember.roleIds.includes(role.id) + ) { + D.memberRoles.push(role); + } + } + API.getAllGroupPosts({ + groupId + }); + if (D.inGroup) { + API.getGroupInstances({ + groupId + }); + } + if (this.$refs.groupDialogTabs.currentName === '0') { + this.groupDialogLastActiveTab = $t( + 'dialog.group.info.header' + ); + } else if ( + this.$refs.groupDialogTabs.currentName === '1' + ) { + this.groupDialogLastActiveTab = $t( + 'dialog.group.posts.header' + ); + } else if ( + this.$refs.groupDialogTabs.currentName === '2' + ) { + this.groupDialogLastActiveTab = $t( + 'dialog.group.members.header' + ); + if (this.groupDialogLastMembers !== groupId) { + this.groupDialogLastMembers = groupId; + this.getGroupDialogGroupMembers(); + } + } else if ( + this.$refs.groupDialogTabs.currentName === '3' + ) { + this.groupDialogLastActiveTab = $t( + 'dialog.group.gallery.header' + ); + if (this.groupDialogLastGallery !== groupId) { + this.groupDialogLastGallery = groupId; + this.getGroupGalleries(); + } + } else if ( + this.$refs.groupDialogTabs.currentName === '4' + ) { + this.groupDialogLastActiveTab = $t( + 'dialog.group.json.header' + ); + this.refreshGroupDialogTreeData(); + } + } + return args1; + }); + }, + + groupDialogCommand(command) { + var D = this.groupDialog; + if (D.visible === false) { + return; + } + switch (command) { + case 'Refresh': + this.showGroupDialog(D.id); + break; + case 'Moderation Tools': + this.showGroupMemberModerationDialog(D.id); + break; + case 'Create Post': + this.showGroupPostEditDialog(D.id, null); + break; + case 'Leave Group': + this.leaveGroup(D.id); + break; + case 'Block Group': + this.blockGroup(D.id); + break; + case 'Unblock Group': + this.unblockGroup(D.id); + break; + case 'Visibility Everyone': + this.setGroupVisibility(D.id, 'visible'); + break; + case 'Visibility Friends': + this.setGroupVisibility(D.id, 'friends'); + break; + case 'Visibility Hidden': + this.setGroupVisibility(D.id, 'hidden'); + break; + case 'Subscribe To Announcements': + this.setGroupSubscription(D.id, true); + break; + case 'Unsubscribe To Announcements': + this.setGroupSubscription(D.id, false); + break; + case 'Invite To Group': + this.showInviteGroupDialog(D.id, ''); + break; + } + }, + + groupDialogTabClick(obj) { + var groupId = this.groupDialog.id; + if (this.groupDialogLastActiveTab === obj.label) { + return; + } + if (obj.label === $t('dialog.group.info.header')) { + // + } else if (obj.label === $t('dialog.group.posts.header')) { + // + } else if (obj.label === $t('dialog.group.members.header')) { + if (this.groupDialogLastMembers !== groupId) { + this.groupDialogLastMembers = groupId; + this.getGroupDialogGroupMembers(); + } + } else if (obj.label === $t('dialog.group.gallery.header')) { + if (this.groupDialogLastGallery !== groupId) { + this.groupDialogLastGallery = groupId; + this.getGroupGalleries(); + } + } else if (obj.label === $t('dialog.group.json.header')) { + this.refreshGroupDialogTreeData(); + } + this.groupDialogLastActiveTab = obj.label; + }, + + refreshGroupDialogTreeData() { + var D = this.groupDialog; + D.treeData = $utils.buildTreeData({ + group: D.ref, + posts: D.posts, + instances: D.instances, + members: D.members, + galleries: D.galleries + }); + }, + + joinGroup(groupId) { + if (!groupId) { + return null; + } + return API.joinGroup({ + groupId + }).then((args) => { + if (args.json.membershipStatus === 'member') { + this.$message({ + message: 'Group joined', + type: 'success' + }); + } else if (args.json.membershipStatus === 'requested') { + this.$message({ + message: 'Group join request sent', + type: 'success' + }); + } + return args; + }); + }, + + leaveGroup(groupId) { + return API.leaveGroup({ + groupId + }); + }, + + cancelGroupRequest(groupId) { + return API.cancelGroupRequest({ + groupId + }); + }, + + setGroupRepresentation(groupId) { + return API.setGroupRepresentation(groupId, { + isRepresenting: true + }); + }, + + clearGroupRepresentation(groupId) { + return API.setGroupRepresentation(groupId, { + isRepresenting: false + }); + }, + + setGroupVisibility(groupId, visibility) { + return API.setGroupMemberProps(API.currentUser.id, groupId, { + visibility + }).then((args) => { + this.$message({ + message: 'Group visibility updated', + type: 'success' + }); + return args; + }); + }, + + setGroupSubscription(groupId, subscribe) { + return API.setGroupMemberProps(API.currentUser.id, groupId, { + isSubscribedToAnnouncements: subscribe + }).then((args) => { + this.$message({ + message: 'Group subscription updated', + type: 'success' + }); + return args; + }); + }, + + onGroupJoined(groupId) { + if ( + this.groupMemberModeration.visible && + this.groupMemberModeration.id === groupId + ) { + // ignore this event if we were the one to trigger it + return; + } + if (!API.currentUserGroups.has(groupId)) { + API.currentUserGroups.set(groupId, { + id: groupId, + name: '', + iconUrl: '' + }); + API.getGroup({ groupId, includeRoles: true }).then((args) => { + var ref = API.applyGroup(args.json); + API.currentUserGroups.set(groupId, ref); + this.saveCurrentUserGroups(); + return args; + }); + } + }, + + onGroupLeft(groupId) { + if (this.groupDialog.visible && this.groupDialog.id === groupId) { + this.showGroupDialog(groupId); + } + if (API.currentUserGroups.has(groupId)) { + API.currentUserGroups.delete(groupId); + API.getCachedGroup({ groupId }).then((args) => { + this.groupChange(args.ref, 'Left group'); + }); + } + }, + + groupMembersSearchDebounce() { + var D = this.groupDialog; + var search = D.memberSearch; + D.memberSearchResults = []; + if (!search || search.length < 3) { + this.setGroupMemberModerationTable(D.members); + return; + } + this.isGroupMembersLoading = true; + API.getGroupMembersSearch({ + groupId: D.id, + query: search, + n: 100, + offset: 0 + }) + .then((args) => { + if (D.id === args.params.groupId) { + D.memberSearchResults = args.json.results; + this.setGroupMemberModerationTable(args.json.results); + } + }) + .finally(() => { + this.isGroupMembersLoading = false; + }); + }, + + groupMembersSearch() { + if (this.groupMembersSearchTimer) { + this.groupMembersSearchPending = true; + } else { + this.groupMembersSearchExecute(); + this.groupMembersSearchTimer = setTimeout(() => { + if (this.groupMembersSearchPending) { + this.groupMembersSearchExecute(); + } + this.groupMembersSearchTimer = null; + }, 500); + } + }, + + groupMembersSearchExecute() { + try { + this.groupMembersSearchDebounce(); + } catch (err) { + console.error(err); + } + this.groupMembersSearchTimer = null; + this.groupMembersSearchPending = false; + }, + + updateGroupPostSearch() { + var D = this.groupDialog; + var search = D.postsSearch.toLowerCase(); + D.postsFiltered = D.posts.filter((post) => { + if (search === '') { + return true; + } + if (post.title.toLowerCase().includes(search)) { + return true; + } + if (post.text.toLowerCase().includes(search)) { + return true; + } + return false; + }); + }, + + async getGroupDialogGroupMembers() { + var D = this.groupDialog; + D.members = []; + this.isGroupMembersDone = false; + this.loadMoreGroupMembersParams = { + n: 100, + offset: 0, + groupId: D.id + }; + if (D.memberSortOrder.value) { + this.loadMoreGroupMembersParams.sort = D.memberSortOrder.value; + } + if (D.memberFilter.id !== null) { + this.loadMoreGroupMembersParams.roleId = D.memberFilter.id; + } + if (D.inGroup) { + await API.getGroupMember({ + groupId: D.id, + userId: API.currentUser.id + }).then((args) => { + if (args.json) { + args.json.user = API.currentUser; + if (D.memberFilter.id === null) { + // when flitered by role don't include self + D.members.push(args.json); + } + } + return args; + }); + } + await this.loadMoreGroupMembers(); + }, + + async loadMoreGroupMembers() { + if (this.isGroupMembersDone || this.isGroupMembersLoading) { + return; + } + var D = this.groupDialog; + var params = this.loadMoreGroupMembersParams; + D.memberSearch = ''; + this.isGroupMembersLoading = true; + await API.getGroupMembers(params) + .finally(() => { + this.isGroupMembersLoading = false; + }) + .then((args) => { + for (var i = 0; i < args.json.length; i++) { + var member = args.json[i]; + if (member.userId === API.currentUser.id) { + if ( + D.members.length > 0 && + D.members[0].userId === API.currentUser.id + ) { + // remove duplicate and keep sort order + D.members.splice(0, 1); + } + break; + } + } + if (args.json.length < params.n) { + this.isGroupMembersDone = true; + } + D.members = [...D.members, ...args.json]; + this.setGroupMemberModerationTable(D.members); + params.offset += params.n; + return args; + }) + .catch((err) => { + this.isGroupMembersDone = true; + throw err; + }); + }, + + async loadAllGroupMembers() { + if (this.isGroupMembersLoading) { + return; + } + await this.getGroupDialogGroupMembers(); + while (this.groupDialog.visible && !this.isGroupMembersDone) { + this.isGroupMembersLoading = true; + await new Promise((resolve) => { + workerTimers.setTimeout(resolve, 1000); + }); + this.isGroupMembersLoading = false; + await this.loadMoreGroupMembers(); + } + }, + + async setGroupMemberSortOrder(sortOrder) { + var D = this.groupDialog; + if (D.memberSortOrder === sortOrder) { + return; + } + D.memberSortOrder = sortOrder; + await this.getGroupDialogGroupMembers(); + }, + + async setGroupMemberFilter(filter) { + var D = this.groupDialog; + if (D.memberFilter === filter) { + return; + } + D.memberFilter = filter; + await this.getGroupDialogGroupMembers(); + }, + + getCurrentUserRepresentedGroup() { + return API.getRepresentedGroup({ + userId: API.currentUser.id + }).then((args) => { + this.userDialog.representedGroup = args.json; + return args; + }); + }, + + hasGroupPermission(ref, permission) { + if ( + ref && + ref.myMember && + ref.myMember.permissions && + (ref.myMember.permissions.includes('*') || + ref.myMember.permissions.includes(permission)) + ) { + return true; + } + return false; + }, + + async getGroupGalleries() { + this.groupDialog.galleries = {}; + this.$refs.groupDialogGallery.currentName = '0'; // select first tab + this.isGroupGalleryLoading = true; + for (var i = 0; i < this.groupDialog.ref.galleries.length; i++) { + var gallery = this.groupDialog.ref.galleries[i]; + await this.getGroupGallery(this.groupDialog.id, gallery.id); + } + this.isGroupGalleryLoading = false; + }, + + async getGroupGallery(groupId, galleryId) { + try { + var params = { + groupId, + galleryId, + n: 100, + offset: 0 + }; + var count = 50; // 5000 max + for (var i = 0; i < count; i++) { + var args = await API.getGroupGallery(params); + params.offset += 100; + if (args.json.length < 100) { + break; + } + } + } catch (err) { + console.error(err); + } + }, + + groupGalleryStatus(gallery) { + var style = {}; + if (!gallery.membersOnly) { + style.joinme = true; + } else if (!gallery.roleIdsToView) { + style.online = true; + } else { + style.busy = true; + } + return style; + }, + + showInviteGroupDialog(groupId, userId) { + this.$nextTick(() => + $app.adjustDialogZ(this.$refs.inviteGroupDialog.$el) + ); + var D = this.inviteGroupDialog; + D.userIds = ''; + D.groups = []; + D.groupId = groupId; + D.groupName = groupId; + D.userId = userId; + D.userObject = {}; + D.visible = true; + if (groupId) { + API.getCachedGroup({ + groupId + }) + .then((args) => { + D.groupName = args.ref.name; + }) + .catch(() => { + D.groupId = ''; + }); + this.isAllowedToInviteToGroup(); + } + + if (userId) { + API.getCachedUser({ userId }).then((args) => { + D.userObject = args.ref; + }); + D.userIds = [userId]; + } + }, + + sendGroupInvite() { + this.$confirm('Continue? Invite User(s) To Group', 'Confirm', { + confirmButtonText: 'Confirm', + cancelButtonText: 'Cancel', + type: 'info', + callback: (action) => { + var D = this.inviteGroupDialog; + if (action !== 'confirm' || D.loading === true) { + return; + } + D.loading = true; + var inviteLoop = () => { + if (D.userIds.length === 0) { + D.loading = false; + return; + } + var receiverUserId = D.userIds.shift(); + API.sendGroupInvite({ + groupId: D.groupId, + userId: receiverUserId + }) + .then(inviteLoop) + .catch(() => { + D.loading = false; + }); + }; + inviteLoop(); + } + }); + }, + + isAllowedToInviteToGroup() { + var D = this.inviteGroupDialog; + var groupId = D.groupId; + if (!groupId) { + return; + } + D.loading = true; + API.getGroup({ groupId }) + .then((args) => { + if ( + this.hasGroupPermission( + args.ref, + 'group-invites-manage' + ) + ) { + return args; + } + // not allowed to invite + D.groupId = ''; + this.$message({ + type: 'error', + message: 'You are not allowed to invite to this group' + }); + return args; + }) + .finally(() => { + D.loading = false; + }); + }, + + showGroupPostEditDialog(groupId, post) { + this.$nextTick(() => + $app.adjustDialogZ(this.$refs.groupPostEditDialog.$el) + ); + var D = this.groupPostEditDialog; + D.sendNotification = true; + D.groupRef = {}; + D.title = ''; + D.text = ''; + D.visibility = 'group'; + D.roleIds = []; + D.postId = ''; + D.groupId = groupId; + $app.gallerySelectDialog.selectedFileId = ''; + $app.gallerySelectDialog.selectedImageUrl = ''; + if (post) { + D.title = post.title; + D.text = post.text; + D.visibility = post.visibility; + D.roleIds = post.roleIds; + D.postId = post.id; + $app.gallerySelectDialog.selectedFileId = post.imageId; + $app.gallerySelectDialog.selectedImageUrl = post.imageUrl; + } + API.getCachedGroup({ groupId }).then((args) => { + D.groupRef = args.ref; + }); + D.visible = true; + }, + + editGroupPost() { + var D = this.groupPostEditDialog; + if (!D.groupId || !D.postId) { + return; + } + var params = { + groupId: D.groupId, + postId: D.postId, + title: D.title, + text: D.text, + roleIds: D.roleIds, + visibility: D.visibility, + imageId: null + }; + if (this.gallerySelectDialog.selectedFileId) { + params.imageId = this.gallerySelectDialog.selectedFileId; + } + API.editGroupPost(params).then((args) => { + this.$message({ + message: 'Group post edited', + type: 'success' + }); + return args; + }); + D.visible = false; + }, + + createGroupPost() { + var D = this.groupPostEditDialog; + var params = { + groupId: D.groupId, + title: D.title, + text: D.text, + roleIds: D.roleIds, + visibility: D.visibility, + sendNotification: D.sendNotification, + imageId: null + }; + if (this.gallerySelectDialog.selectedFileId) { + params.imageId = this.gallerySelectDialog.selectedFileId; + } + API.createGroupPost(params).then((args) => { + this.$message({ + message: 'Group post created', + type: 'success' + }); + return args; + }); + D.visible = false; + }, + + setGroupMemberModerationTable(data) { + if (!this.groupMemberModeration.visible) { + return; + } + for (var i = 0; i < data.length; i++) { + var member = data[i]; + member.$selected = this.groupMemberModeration.selectedUsers.has( + member.userId + ); + } + this.groupMemberModerationTable.data = data; + // force redraw + this.groupMemberModerationTableForceUpdate++; + }, + + showGroupMemberModerationDialog(groupId) { + this.$nextTick(() => + $app.adjustDialogZ(this.$refs.groupMemberModeration.$el) + ); + if (groupId !== this.groupDialog.id) { + return; + } + var D = this.groupMemberModeration; + D.id = groupId; + D.selectedUsers.clear(); + D.selectedUsersArray = []; + D.selectedRoles = []; + D.groupRef = {}; + D.auditLogTypes = []; + D.selectedAuditLogTypes = []; + API.getCachedGroup({ groupId }).then((args) => { + D.groupRef = args.ref; + if (this.hasGroupPermission(D.groupRef, 'group-audit-view')) { + API.getGroupAuditLogTypes({ groupId }); + } + }); + this.groupMemberModerationTableForceUpdate = 0; + D.visible = true; + this.setGroupMemberModerationTable(this.groupDialog.members); + }, + + groupMemberModerationTableSelectionChange(row) { + var D = this.groupMemberModeration; + if (row.$selected && !D.selectedUsers.has(row.userId)) { + D.selectedUsers.set(row.userId, row); + } else if (!row.$selected && D.selectedUsers.has(row.userId)) { + D.selectedUsers.delete(row.userId); + } + D.selectedUsersArray = Array.from(D.selectedUsers.values()); + // force redraw + this.groupMemberModerationTableForceUpdate++; + }, + + deleteSelectedGroupMember(user) { + var D = this.groupMemberModeration; + D.selectedUsers.delete(user.userId); + D.selectedUsersArray = Array.from(D.selectedUsers.values()); + for ( + var i = 0; + i < this.groupMemberModerationTable.data.length; + i++ + ) { + var row = this.groupMemberModerationTable.data[i]; + if (row.userId === user.userId) { + row.$selected = false; + break; + } + } + for ( + var i = 0; + i < this.groupBansModerationTable.data.length; + i++ + ) { + var row = this.groupBansModerationTable.data[i]; + if (row.userId === user.userId) { + row.$selected = false; + break; + } + } + for ( + var i = 0; + i < this.groupInvitesModerationTable.data.length; + i++ + ) { + var row = this.groupInvitesModerationTable.data[i]; + if (row.userId === user.userId) { + row.$selected = false; + break; + } + } + for ( + var i = 0; + i < this.groupJoinRequestsModerationTable.data.length; + i++ + ) { + var row = this.groupJoinRequestsModerationTable.data[i]; + if (row.userId === user.userId) { + row.$selected = false; + break; + } + } + for ( + var i = 0; + i < this.groupBlockedModerationTable.data.length; + i++ + ) { + var row = this.groupBlockedModerationTable.data[i]; + if (row.userId === user.userId) { + row.$selected = false; + break; + } + } + + // force redraw + this.groupMemberModerationTableForceUpdate++; + }, + + clearSelectedGroupMembers() { + var D = this.groupMemberModeration; + D.selectedUsers.clear(); + D.selectedUsersArray = []; + for ( + var i = 0; + i < this.groupMemberModerationTable.data.length; + i++ + ) { + var row = this.groupMemberModerationTable.data[i]; + row.$selected = false; + } + for ( + var i = 0; + i < this.groupBansModerationTable.data.length; + i++ + ) { + var row = this.groupBansModerationTable.data[i]; + row.$selected = false; + } + for ( + var i = 0; + i < this.groupInvitesModerationTable.data.length; + i++ + ) { + var row = this.groupInvitesModerationTable.data[i]; + row.$selected = false; + } + for ( + var i = 0; + i < this.groupJoinRequestsModerationTable.data.length; + i++ + ) { + var row = this.groupJoinRequestsModerationTable.data[i]; + row.$selected = false; + } + for ( + var i = 0; + i < this.groupBlockedModerationTable.data.length; + i++ + ) { + var row = this.groupBlockedModerationTable.data[i]; + row.$selected = false; + } + // force redraw + this.groupMemberModerationTableForceUpdate++; + }, + + selectAllGroupMembers() { + var D = this.groupMemberModeration; + for ( + var i = 0; + i < this.groupMemberModerationTable.data.length; + i++ + ) { + var row = this.groupMemberModerationTable.data[i]; + row.$selected = true; + D.selectedUsers.set(row.userId, row); + } + D.selectedUsersArray = Array.from(D.selectedUsers.values()); + // force redraw + this.groupMemberModerationTableForceUpdate++; + }, + + selectAllGroupBans() { + var D = this.groupMemberModeration; + for ( + var i = 0; + i < this.groupBansModerationTable.data.length; + i++ + ) { + var row = this.groupBansModerationTable.data[i]; + row.$selected = true; + D.selectedUsers.set(row.userId, row); + } + D.selectedUsersArray = Array.from(D.selectedUsers.values()); + // force redraw + this.groupMemberModerationTableForceUpdate++; + }, + + selectAllGroupInvites() { + var D = this.groupMemberModeration; + for ( + var i = 0; + i < this.groupInvitesModerationTable.data.length; + i++ + ) { + var row = this.groupInvitesModerationTable.data[i]; + row.$selected = true; + D.selectedUsers.set(row.userId, row); + } + D.selectedUsersArray = Array.from(D.selectedUsers.values()); + // force redraw + this.groupMemberModerationTableForceUpdate++; + }, + + selectAllGroupJoinRequests() { + var D = this.groupMemberModeration; + for ( + var i = 0; + i < this.groupJoinRequestsModerationTable.data.length; + i++ + ) { + var row = this.groupJoinRequestsModerationTable.data[i]; + row.$selected = true; + D.selectedUsers.set(row.userId, row); + } + D.selectedUsersArray = Array.from(D.selectedUsers.values()); + // force redraw + this.groupMemberModerationTableForceUpdate++; + }, + + selectAllGroupBlocked() { + var D = this.groupMemberModeration; + for ( + var i = 0; + i < this.groupBlockedModerationTable.data.length; + i++ + ) { + var row = this.groupBlockedModerationTable.data[i]; + row.$selected = true; + D.selectedUsers.set(row.userId, row); + } + D.selectedUsersArray = Array.from(D.selectedUsers.values()); + // force redraw + this.groupMemberModerationTableForceUpdate++; + }, + + async groupMembersKick() { + var D = this.groupMemberModeration; + var memberCount = D.selectedUsersArray.length; + D.progressTotal = memberCount; + try { + for (var i = 0; i < memberCount; i++) { + if (!D.visible || !D.progressTotal) { + break; + } + var user = D.selectedUsersArray[i]; + D.progressCurrent = i + 1; + if (user.userId === API.currentUser.id) { + continue; + } + await API.kickGroupMember({ + groupId: D.id, + userId: user.userId + }); + console.log( + `Kicking ${user.userId} ${i + 1}/${memberCount}` + ); + } + this.$message({ + message: `Kicked ${memberCount} group members`, + type: 'success' + }); + } catch (err) { + console.error(err); + this.$message({ + message: `Failed to kick group member: ${err}`, + type: 'error' + }); + } finally { + D.progressCurrent = 0; + D.progressTotal = 0; + } + }, + + async groupMembersBan() { + var D = this.groupMemberModeration; + var memberCount = D.selectedUsersArray.length; + D.progressTotal = memberCount; + try { + for (var i = 0; i < memberCount; i++) { + if (!D.visible || !D.progressTotal) { + break; + } + var user = D.selectedUsersArray[i]; + D.progressCurrent = i + 1; + if (user.userId === API.currentUser.id) { + continue; + } + await API.banGroupMember({ + groupId: D.id, + userId: user.userId + }); + console.log( + `Banning ${user.userId} ${i + 1}/${memberCount}` + ); + } + this.$message({ + message: `Banned ${memberCount} group members`, + type: 'success' + }); + } catch (err) { + console.error(err); + this.$message({ + message: `Failed to ban group member: ${err}`, + type: 'error' + }); + } finally { + D.progressCurrent = 0; + D.progressTotal = 0; + } + }, + + async groupMembersUnban() { + var D = this.groupMemberModeration; + var memberCount = D.selectedUsersArray.length; + D.progressTotal = memberCount; + try { + for (var i = 0; i < memberCount; i++) { + if (!D.visible || !D.progressTotal) { + break; + } + var user = D.selectedUsersArray[i]; + D.progressCurrent = i + 1; + if (user.userId === API.currentUser.id) { + continue; + } + await API.unbanGroupMember({ + groupId: D.id, + userId: user.userId + }); + console.log( + `Unbanning ${user.userId} ${i + 1}/${memberCount}` + ); + } + this.$message({ + message: `Unbanned ${memberCount} group members`, + type: 'success' + }); + } catch (err) { + console.error(err); + this.$message({ + message: `Failed to unban group member: ${err}`, + type: 'error' + }); + } finally { + D.progressCurrent = 0; + D.progressTotal = 0; + } + }, + + async groupMembersDeleteSentInvite() { + var D = this.groupMemberModeration; + var memberCount = D.selectedUsersArray.length; + D.progressTotal = memberCount; + try { + for (var i = 0; i < memberCount; i++) { + if (!D.visible || !D.progressTotal) { + break; + } + var user = D.selectedUsersArray[i]; + D.progressCurrent = i + 1; + if (user.userId === API.currentUser.id) { + continue; + } + await API.deleteSentGroupInvite({ + groupId: D.id, + userId: user.userId + }); + console.log( + `Deleting group invite ${user.userId} ${i + 1}/${memberCount}` + ); + } + this.$message({ + message: `Deleted ${memberCount} group invites`, + type: 'success' + }); + } catch (err) { + console.error(err); + this.$message({ + message: `Failed to delete group invites: ${err}`, + type: 'error' + }); + } finally { + D.progressCurrent = 0; + D.progressTotal = 0; + } + }, + + async groupMembersDeleteBlockedRequest() { + var D = this.groupMemberModeration; + var memberCount = D.selectedUsersArray.length; + D.progressTotal = memberCount; + try { + for (var i = 0; i < memberCount; i++) { + if (!D.visible || !D.progressTotal) { + break; + } + var user = D.selectedUsersArray[i]; + D.progressCurrent = i + 1; + if (user.userId === API.currentUser.id) { + continue; + } + await API.deleteBlockedGroupRequest({ + groupId: D.id, + userId: user.userId + }); + console.log( + `Deleting blocked group request ${user.userId} ${i + 1}/${memberCount}` + ); + } + this.$message({ + message: `Deleted ${memberCount} blocked group requests`, + type: 'success' + }); + } catch (err) { + console.error(err); + this.$message({ + message: `Failed to delete blocked group requests: ${err}`, + type: 'error' + }); + } finally { + D.progressCurrent = 0; + D.progressTotal = 0; + } + }, + + async groupMembersAcceptInviteRequest() { + var D = this.groupMemberModeration; + var memberCount = D.selectedUsersArray.length; + D.progressTotal = memberCount; + try { + for (var i = 0; i < memberCount; i++) { + if (!D.visible || !D.progressTotal) { + break; + } + var user = D.selectedUsersArray[i]; + D.progressCurrent = i + 1; + if (user.userId === API.currentUser.id) { + continue; + } + await API.acceptGroupInviteRequest({ + groupId: D.id, + userId: user.userId + }); + console.log( + `Accepting group join request ${user.userId} ${i + 1}/${memberCount}` + ); + } + this.$message({ + message: `Accepted ${memberCount} group join requests`, + type: 'success' + }); + } catch (err) { + console.error(err); + this.$message({ + message: `Failed to accept group join requests: ${err}`, + type: 'error' + }); + } finally { + D.progressCurrent = 0; + D.progressTotal = 0; + } + }, + + async groupMembersRejectInviteRequest() { + var D = this.groupMemberModeration; + var memberCount = D.selectedUsersArray.length; + D.progressTotal = memberCount; + try { + for (var i = 0; i < memberCount; i++) { + if (!D.visible || !D.progressTotal) { + break; + } + var user = D.selectedUsersArray[i]; + D.progressCurrent = i + 1; + if (user.userId === API.currentUser.id) { + continue; + } + await API.rejectGroupInviteRequest({ + groupId: D.id, + userId: user.userId + }); + console.log( + `Rejecting group join request ${user.userId} ${i + 1}/${memberCount}` + ); + } + this.$message({ + message: `Rejected ${memberCount} group join requests`, + type: 'success' + }); + } catch (err) { + console.error(err); + this.$message({ + message: `Failed to reject group join requests: ${err}`, + type: 'error' + }); + } finally { + D.progressCurrent = 0; + D.progressTotal = 0; + } + }, + + async groupMembersBlockJoinRequest() { + var D = this.groupMemberModeration; + var memberCount = D.selectedUsersArray.length; + D.progressTotal = memberCount; + try { + for (var i = 0; i < memberCount; i++) { + if (!D.visible || !D.progressTotal) { + break; + } + var user = D.selectedUsersArray[i]; + D.progressCurrent = i + 1; + if (user.userId === API.currentUser.id) { + continue; + } + await API.blockGroupInviteRequest({ + groupId: D.id, + userId: user.userId + }); + console.log( + `Blocking group join request ${user.userId} ${i + 1}/${memberCount}` + ); + } + this.$message({ + message: `Blocked ${memberCount} group join requests`, + type: 'success' + }); + } catch (err) { + console.error(err); + this.$message({ + message: `Failed to block group join requests: ${err}`, + type: 'error' + }); + } finally { + D.progressCurrent = 0; + D.progressTotal = 0; + } + }, + + async groupMembersSaveNote() { + var D = this.groupMemberModeration; + var memberCount = D.selectedUsersArray.length; + D.progressTotal = memberCount; + try { + for (var i = 0; i < memberCount; i++) { + if (!D.visible || !D.progressTotal) { + break; + } + var user = D.selectedUsersArray[i]; + D.progressCurrent = i + 1; + if (user.managerNotes === D.note) { + continue; + } + await API.setGroupMemberProps(user.userId, D.id, { + managerNotes: D.note + }); + console.log( + `Setting note ${D.note} ${user.userId} ${ + i + 1 + }/${memberCount}` + ); + } + this.$message({ + message: `Saved notes for ${memberCount} group members`, + type: 'success' + }); + } catch (err) { + console.error(err); + this.$message({ + message: `Failed to set group member note: ${err}`, + type: 'error' + }); + } finally { + D.progressCurrent = 0; + D.progressTotal = 0; + } + }, + + async groupMembersAddRoles() { + var D = this.groupMemberModeration; + var memberCount = D.selectedUsersArray.length; + D.progressTotal = memberCount; + try { + for (var i = 0; i < memberCount; i++) { + if (!D.visible || !D.progressTotal) { + break; + } + var user = D.selectedUsersArray[i]; + D.progressCurrent = i + 1; + var rolesToAdd = []; + D.selectedRoles.forEach((roleId) => { + if (!user.roleIds.includes(roleId)) { + rolesToAdd.push(roleId); + } + }); + + if (!rolesToAdd.length) { + continue; + } + for (var j = 0; j < rolesToAdd.length; j++) { + var roleId = rolesToAdd[j]; + console.log( + `Adding role: ${roleId} ${user.userId} ${ + i + 1 + }/${memberCount}` + ); + await API.addGroupMemberRole({ + groupId: D.id, + userId: user.userId, + roleId + }); + } + } + this.$message({ + message: 'Added group member roles', + type: 'success' + }); + } catch (err) { + console.error(err); + this.$message({ + message: `Failed to add group member roles: ${err}`, + type: 'error' + }); + } finally { + D.progressCurrent = 0; + D.progressTotal = 0; + } + }, + + async groupMembersRemoveRoles() { + var D = this.groupMemberModeration; + var memberCount = D.selectedUsersArray.length; + D.progressTotal = memberCount; + try { + for (var i = 0; i < memberCount; i++) { + if (!D.visible || !D.progressTotal) { + break; + } + var user = D.selectedUsersArray[i]; + D.progressCurrent = i + 1; + var rolesToRemove = []; + D.selectedRoles.forEach((roleId) => { + if (user.roleIds.includes(roleId)) { + rolesToRemove.push(roleId); + } + }); + if (!rolesToRemove.length) { + continue; + } + for (var j = 0; j < rolesToRemove.length; j++) { + var roleId = rolesToRemove[j]; + console.log( + `Removing role ${roleId} ${user.userId} ${ + i + 1 + }/${memberCount}` + ); + await API.removeGroupMemberRole({ + groupId: D.id, + userId: user.userId, + roleId + }); + } + } + this.$message({ + message: 'Roles removed', + type: 'success' + }); + } catch (err) { + console.error(err); + this.$message({ + message: `Failed to remove group member roles: ${err}`, + type: 'error' + }); + } finally { + D.progressCurrent = 0; + D.progressTotal = 0; + } + }, + + async selectGroupMemberUserId() { + var D = this.groupMemberModeration; + if (!D.selectUserId) { + return; + } + + var regexUserId = + /usr_[0-9A-Fa-f]{8}-([0-9A-Fa-f]{4}-){3}[0-9A-Fa-f]{12}/g; + var match = []; + var userIdList = new Set(); + while ((match = regexUserId.exec(D.selectUserId)) !== null) { + userIdList.add(match[0]); + } + if (userIdList.size === 0) { + // for those users missing the usr_ prefix + userIdList.add(D.selectUserId); + } + for (var userId of userIdList) { + try { + await this.addGroupMemberToSelection(userId); + } catch { + console.error(`Failed to add user ${userId}`); + } + } + + D.selectUserId = ''; + }, + + async addGroupMemberToSelection(userId) { + var D = this.groupMemberModeration; + + // fetch memeber if there is one + // banned members don't have a user object + + var memeber = {}; + var memeberArgs = await API.getGroupMember({ + groupId: D.id, + userId + }); + if (memeberArgs.json) { + memeber = API.applyGroupMember(memeberArgs.json); + } + if (memeber.user) { + D.selectedUsers.set(memeber.userId, memeber); + D.selectedUsersArray = Array.from(D.selectedUsers.values()); + this.groupMemberModerationTableForceUpdate++; + return; + } + + var userArgs = await API.getCachedUser({ + userId + }); + memeber.userId = userArgs.json.id; + memeber.user = userArgs.json; + memeber.displayName = userArgs.json.displayName; + + D.selectedUsers.set(memeber.userId, memeber); + D.selectedUsersArray = Array.from(D.selectedUsers.values()); + this.groupMemberModerationTableForceUpdate++; + } + }; +} diff --git a/html/src/classes/languages.js b/html/src/classes/languages.js new file mode 100644 index 000000000..e762fca46 --- /dev/null +++ b/html/src/classes/languages.js @@ -0,0 +1,162 @@ +import { baseClass, $app, API, $t, $utils } from './baseClass.js'; + +export default class extends baseClass { + constructor(_app, _API, _t) { + super(_app, _API, _t); + } + + init() { + API.$on('CONFIG', function (args) { + var languages = + args.ref?.constants?.LANGUAGE?.SPOKEN_LANGUAGE_OPTIONS; + if (!languages) { + return; + } + $app.subsetOfLanguages = languages; + var data = []; + for (var key in languages) { + var value = languages[key]; + data.push({ + key, + value + }); + } + $app.languageDialog.languages = data; + }); + + API.$on('LOGOUT', function () { + $app.languageDialog.visible = false; + }); + } + + _data = { + // vrchat to famfamfam language mappings + languageMappings: { + eng: 'us', + kor: 'kr', + rus: 'ru', + spa: 'es', + por: 'pt', + zho: 'cn', + deu: 'de', + jpn: 'jp', + fra: 'fr', + swe: 'se', + nld: 'nl', + pol: 'pl', + dan: 'dk', + nor: 'no', + ita: 'it', + tha: 'th', + fin: 'fi', + hun: 'hu', + ces: 'cz', + tur: 'tr', + ara: 'ae', + ron: 'ro', + vie: 'vn', + ukr: 'ua', + ase: 'us', + bfi: 'gb', + dse: 'nl', + fsl: 'fr', + jsl: 'jp', + kvk: 'kr', + + mlt: 'mt', + ind: 'id', + hrv: 'hr', + heb: 'he', + afr: 'af', + ben: 'be', + bul: 'bg', + cmn: 'cn', + cym: 'cy', + ell: 'el', + est: 'et', + fil: 'ph', + gla: 'gd', + gle: 'ga', + hin: 'hi', + hmn: 'cn', + hye: 'hy', + isl: 'is', + lav: 'lv', + lit: 'lt', + ltz: 'lb', + mar: 'hi', + mkd: 'mk', + msa: 'my', + sco: 'gd', + slk: 'sk', + slv: 'sl', + tel: 'hi', + mri: 'nz', + wuu: 'cn', + yue: 'cn', + tws: 'cn', + asf: 'au', + nzs: 'nz', + gsg: 'de', + epo: 'eo', + tok: 'tok' + }, + + subsetOfLanguages: [], + + languageDialog: { + visible: false, + loading: false, + languageChoice: false, + languageValue: '', + languages: [] + } + }; + + _methods = { + languageClass(language) { + var style = {}; + var mapping = this.languageMappings[language]; + if (typeof mapping !== 'undefined') { + style[mapping] = true; + } else { + style.unknown = true; + } + return style; + }, + + addUserLanguage(language) { + if (language !== String(language)) { + return; + } + var D = this.languageDialog; + D.loading = true; + API.addUserTags({ + tags: [`language_${language}`] + }).finally(function () { + D.loading = false; + }); + }, + + removeUserLanguage(language) { + if (language !== String(language)) { + return; + } + var D = this.languageDialog; + D.loading = true; + API.removeUserTags({ + tags: [`language_${language}`] + }).finally(function () { + D.loading = false; + }); + }, + + showLanguageDialog() { + this.$nextTick(() => + $app.adjustDialogZ(this.$refs.languageDialog.$el) + ); + var D = this.languageDialog; + D.visible = true; + } + }; +} diff --git a/html/src/classes/memos.js b/html/src/classes/memos.js new file mode 100644 index 000000000..899f163b1 --- /dev/null +++ b/html/src/classes/memos.js @@ -0,0 +1,146 @@ +import { baseClass, $app, API, $t, $utils } from './baseClass.js'; + +export default class extends baseClass { + constructor(_app, _API, _t) { + super(_app, _API, _t); + } + + init() {} + + _data = { + hideUserMemos: false + }; + + _methods = { + async migrateMemos() { + var json = JSON.parse(await VRCXStorage.GetAll()); + database.begin(); + for (var line in json) { + if (line.substring(0, 8) === 'memo_usr') { + var userId = line.substring(5); + var memo = json[line]; + if (memo) { + await this.saveUserMemo(userId, memo); + VRCXStorage.Remove(`memo_${userId}`); + } + } + } + database.commit(); + }, + + onUserMemoChange() { + var D = this.userDialog; + this.saveUserMemo(D.id, D.memo); + }, + + async getUserMemo(userId) { + try { + return await database.getUserMemo(userId); + } catch (err) { + console.error(err); + return { + userId: '', + editedAt: '', + memo: '' + }; + } + }, + + saveUserMemo(id, memo) { + if (memo) { + database.setUserMemo({ + userId: id, + editedAt: new Date().toJSON(), + memo + }); + } else { + database.deleteUserMemo(id); + } + var ref = this.friends.get(id); + if (ref) { + ref.memo = String(memo || ''); + if (memo) { + var array = memo.split('\n'); + ref.$nickName = array[0]; + } else { + ref.$nickName = ''; + } + } + }, + + async getAllUserMemos() { + var memos = await database.getAllUserMemos(); + memos.forEach((memo) => { + var ref = $app.friends.get(memo.userId); + if (typeof ref !== 'undefined') { + ref.memo = memo.memo; + ref.$nickName = ''; + if (memo.memo) { + var array = memo.memo.split('\n'); + ref.$nickName = array[0]; + } + } + }); + }, + + onWorldMemoChange() { + var D = this.worldDialog; + this.saveWorldMemo(D.id, D.memo); + }, + + async getWorldMemo(worldId) { + try { + return await database.getWorldMemo(worldId); + } catch (err) { + console.error(err); + return { + worldId: '', + editedAt: '', + memo: '' + }; + } + }, + + saveWorldMemo(worldId, memo) { + if (memo) { + database.setWorldMemo({ + worldId, + editedAt: new Date().toJSON(), + memo + }); + } else { + database.deleteWorldMemo(worldId); + } + }, + + onAvatarMemoChange() { + var D = this.avatarDialog; + this.saveAvatarMemo(D.id, D.memo); + }, + + async getAvatarMemo(avatarId) { + try { + return await database.getAvatarMemoDB(avatarId); + } catch (err) { + console.error(err); + return { + avatarId: '', + editedAt: '', + memo: '' + }; + } + }, + + saveAvatarMemo(avatarId, memo) { + if (memo) { + database.setAvatarMemo({ + avatarId, + editedAt: new Date().toJSON(), + memo + }); + } else { + database.deleteAvatarMemo(avatarId); + } + } + }; +} diff --git a/html/src/classes/prompts.js b/html/src/classes/prompts.js new file mode 100644 index 000000000..5b06942a1 --- /dev/null +++ b/html/src/classes/prompts.js @@ -0,0 +1,809 @@ +import * as workerTimers from 'worker-timers'; +import configRepository from '../repository/config.js'; +import database from '../repository/database.js'; +import { baseClass, $app, API, $t, $utils } from './baseClass.js'; + +export default class extends baseClass { + constructor(_app, _API, _t) { + super(_app, _API, _t); + } + + _methods = { + promptTOTP() { + if (this.twoFactorAuthDialogVisible) { + return; + } + AppApi.FlashWindow(); + this.twoFactorAuthDialogVisible = true; + this.$prompt( + $t('prompt.totp.description'), + $t('prompt.totp.header'), + { + distinguishCancelAndClose: true, + cancelButtonText: $t('prompt.totp.use_otp'), + confirmButtonText: $t('prompt.totp.verify'), + inputPlaceholder: $t('prompt.totp.input_placeholder'), + inputPattern: /^[0-9]{6}$/, + inputErrorMessage: $t('prompt.totp.input_error'), + callback: (action, instance) => { + if (action === 'confirm') { + API.verifyTOTP({ + code: instance.inputValue.trim() + }) + .catch((err) => { + this.promptTOTP(); + throw err; + }) + .then((args) => { + API.getCurrentUser(); + return args; + }); + } else if (action === 'cancel') { + this.promptOTP(); + } + }, + beforeClose: (action, instance, done) => { + this.twoFactorAuthDialogVisible = false; + done(); + } + } + ); + }, + + promptOTP() { + if (this.twoFactorAuthDialogVisible) { + return; + } + this.twoFactorAuthDialogVisible = true; + this.$prompt( + $t('prompt.otp.description'), + $t('prompt.otp.header'), + { + distinguishCancelAndClose: true, + cancelButtonText: $t('prompt.otp.use_totp'), + confirmButtonText: $t('prompt.otp.verify'), + inputPlaceholder: $t('prompt.otp.input_placeholder'), + inputPattern: /^[a-z0-9]{4}-[a-z0-9]{4}$/, + inputErrorMessage: $t('prompt.otp.input_error'), + callback: (action, instance) => { + if (action === 'confirm') { + API.verifyOTP({ + code: instance.inputValue.trim() + }) + .catch((err) => { + this.promptOTP(); + throw err; + }) + .then((args) => { + API.getCurrentUser(); + return args; + }); + } else if (action === 'cancel') { + this.promptTOTP(); + } + }, + beforeClose: (action, instance, done) => { + this.twoFactorAuthDialogVisible = false; + done(); + } + } + ); + }, + + promptEmailOTP() { + if (this.twoFactorAuthDialogVisible) { + return; + } + AppApi.FlashWindow(); + this.twoFactorAuthDialogVisible = true; + this.$prompt( + $t('prompt.email_otp.description'), + $t('prompt.email_otp.header'), + { + distinguishCancelAndClose: true, + cancelButtonText: $t('prompt.email_otp.resend'), + confirmButtonText: $t('prompt.email_otp.verify'), + inputPlaceholder: $t('prompt.email_otp.input_placeholder'), + inputPattern: /^[0-9]{6}$/, + inputErrorMessage: $t('prompt.email_otp.input_error'), + callback: (action, instance) => { + if (action === 'confirm') { + API.verifyEmailOTP({ + code: instance.inputValue.trim() + }) + .catch((err) => { + this.promptEmailOTP(); + throw err; + }) + .then((args) => { + API.getCurrentUser(); + return args; + }); + } else if (action === 'cancel') { + this.resendEmail2fa(); + } + }, + beforeClose: (action, instance, done) => { + this.twoFactorAuthDialogVisible = false; + done(); + } + } + ); + }, + + promptUserIdDialog() { + this.$prompt( + $t('prompt.direct_access_user_id.description'), + $t('prompt.direct_access_user_id.header'), + { + distinguishCancelAndClose: true, + confirmButtonText: $t('prompt.direct_access_user_id.ok'), + cancelButtonText: $t('prompt.direct_access_user_id.cancel'), + inputPattern: /\S+/, + inputErrorMessage: $t( + 'prompt.direct_access_user_id.input_error' + ), + callback: (action, instance) => { + if (action === 'confirm' && instance.inputValue) { + var testUrl = instance.inputValue.substring(0, 15); + if (testUrl === 'https://vrchat.') { + var userId = this.parseUserUrl( + instance.inputValue + ); + if (userId) { + this.showUserDialog(userId); + } else { + this.$message({ + message: $t( + 'prompt.direct_access_user_id.message.error' + ), + type: 'error' + }); + } + } else { + this.showUserDialog(instance.inputValue); + } + } + } + } + ); + }, + + promptUsernameDialog() { + this.$prompt( + $t('prompt.direct_access_username.description'), + $t('prompt.direct_access_username.header'), + { + distinguishCancelAndClose: true, + confirmButtonText: $t('prompt.direct_access_username.ok'), + cancelButtonText: $t( + 'prompt.direct_access_username.cancel' + ), + inputPattern: /\S+/, + inputErrorMessage: $t( + 'prompt.direct_access_username.input_error' + ), + callback: (action, instance) => { + if (action === 'confirm' && instance.inputValue) { + this.lookupUser({ + displayName: instance.inputValue + }); + } + } + } + ); + }, + + promptWorldDialog() { + this.$prompt( + $t('prompt.direct_access_world_id.description'), + $t('prompt.direct_access_world_id.header'), + { + distinguishCancelAndClose: true, + confirmButtonText: $t('prompt.direct_access_world_id.ok'), + cancelButtonText: $t( + 'prompt.direct_access_world_id.cancel' + ), + inputPattern: /\S+/, + inputErrorMessage: $t( + 'prompt.direct_access_world_id.input_error' + ), + callback: (action, instance) => { + if (action === 'confirm' && instance.inputValue) { + if (!this.directAccessWorld(instance.inputValue)) { + this.$message({ + message: $t( + 'prompt.direct_access_world_id.message.error' + ), + type: 'error' + }); + } + } + } + } + ); + }, + + promptAvatarDialog() { + this.$prompt( + $t('prompt.direct_access_avatar_id.description'), + $t('prompt.direct_access_avatar_id.header'), + { + distinguishCancelAndClose: true, + confirmButtonText: $t('prompt.direct_access_avatar_id.ok'), + cancelButtonText: $t( + 'prompt.direct_access_avatar_id.cancel' + ), + inputPattern: /\S+/, + inputErrorMessage: $t( + 'prompt.direct_access_avatar_id.input_error' + ), + callback: (action, instance) => { + if (action === 'confirm' && instance.inputValue) { + var testUrl = instance.inputValue.substring(0, 15); + if (testUrl === 'https://vrchat.') { + var avatarId = this.parseAvatarUrl( + instance.inputValue + ); + if (avatarId) { + this.showAvatarDialog(avatarId); + } else { + this.$message({ + message: $t( + 'prompt.direct_access_avatar_id.message.error' + ), + type: 'error' + }); + } + } else { + this.showAvatarDialog(instance.inputValue); + } + } + } + } + ); + }, + + promptOmniDirectDialog() { + this.$prompt( + $t('prompt.direct_access_omni.description'), + $t('prompt.direct_access_omni.header'), + { + distinguishCancelAndClose: true, + confirmButtonText: $t('prompt.direct_access_omni.ok'), + cancelButtonText: $t('prompt.direct_access_omni.cancel'), + inputPattern: /\S+/, + inputErrorMessage: $t( + 'prompt.direct_access_omni.input_error' + ), + callback: (action, instance) => { + if (action === 'confirm' && instance.inputValue) { + var input = instance.inputValue.trim(); + if (!this.directAccessParse(input)) { + this.$message({ + message: $t( + 'prompt.direct_access_omni.message.error' + ), + type: 'error' + }); + } + } + } + } + ); + }, + + changeFavoriteGroupName(ctx) { + this.$prompt( + $t('prompt.change_favorite_group_name.description'), + $t('prompt.change_favorite_group_name.header'), + { + distinguishCancelAndClose: true, + cancelButtonText: $t( + 'prompt.change_favorite_group_name.cancel' + ), + confirmButtonText: $t( + 'prompt.change_favorite_group_name.change' + ), + inputPlaceholder: $t( + 'prompt.change_favorite_group_name.input_placeholder' + ), + inputValue: ctx.displayName, + inputPattern: /\S+/, + inputErrorMessage: $t( + 'prompt.change_favorite_group_name.input_error' + ), + callback: (action, instance) => { + if (action === 'confirm') { + API.saveFavoriteGroup({ + type: ctx.type, + group: ctx.name, + displayName: instance.inputValue + }).then((args) => { + this.$message({ + message: $t( + 'prompt.change_favorite_group_name.message.success' + ), + type: 'success' + }); + return args; + }); + } + } + } + ); + }, + + promptNotificationTimeout() { + this.$prompt( + $t('prompt.notification_timeout.description'), + $t('prompt.notification_timeout.header'), + { + distinguishCancelAndClose: true, + confirmButtonText: $t('prompt.notification_timeout.ok'), + cancelButtonText: $t('prompt.notification_timeout.cancel'), + inputValue: this.notificationTimeout / 1000, + inputPattern: /\d+$/, + inputErrorMessage: $t( + 'prompt.notification_timeout.input_error' + ), + callback: async (action, instance) => { + if ( + action === 'confirm' && + instance.inputValue && + !isNaN(instance.inputValue) + ) { + this.notificationTimeout = Math.trunc( + Number(instance.inputValue) * 1000 + ); + await configRepository.setString( + 'VRCX_notificationTimeout', + this.notificationTimeout + ); + this.updateVRConfigVars(); + } + } + } + ); + }, + + promptPhotonOverlayMessageTimeout() { + this.$prompt( + $t('prompt.overlay_message_timeout.description'), + $t('prompt.overlay_message_timeout.header'), + { + distinguishCancelAndClose: true, + confirmButtonText: $t('prompt.overlay_message_timeout.ok'), + cancelButtonText: $t( + 'prompt.overlay_message_timeout.cancel' + ), + inputValue: this.photonOverlayMessageTimeout / 1000, + inputPattern: /\d+$/, + inputErrorMessage: $t( + 'prompt.overlay_message_timeout.input_error' + ), + callback: async (action, instance) => { + if ( + action === 'confirm' && + instance.inputValue && + !isNaN(instance.inputValue) + ) { + this.photonOverlayMessageTimeout = Math.trunc( + Number(instance.inputValue) * 1000 + ); + await configRepository.setString( + 'VRCX_photonOverlayMessageTimeout', + this.photonOverlayMessageTimeout + ); + this.updateVRConfigVars(); + } + } + } + ); + }, + + promptRenameAvatar(avatar) { + this.$prompt( + $t('prompt.rename_avatar.description'), + $t('prompt.rename_avatar.header'), + { + distinguishCancelAndClose: true, + confirmButtonText: $t('prompt.rename_avatar.ok'), + cancelButtonText: $t('prompt.rename_avatar.cancel'), + inputValue: avatar.ref.name, + inputErrorMessage: $t('prompt.rename_avatar.input_error'), + callback: (action, instance) => { + if ( + action === 'confirm' && + instance.inputValue !== avatar.ref.name + ) { + API.saveAvatar({ + id: avatar.id, + name: instance.inputValue + }).then((args) => { + this.$message({ + message: $t( + 'prompt.rename_avatar.message.success' + ), + type: 'success' + }); + return args; + }); + } + } + } + ); + }, + + promptChangeAvatarDescription(avatar) { + this.$prompt( + $t('prompt.change_avatar_description.description'), + $t('prompt.change_avatar_description.header'), + { + distinguishCancelAndClose: true, + confirmButtonText: $t( + 'prompt.change_avatar_description.ok' + ), + cancelButtonText: $t( + 'prompt.change_avatar_description.cancel' + ), + inputValue: avatar.ref.description, + inputErrorMessage: $t( + 'prompt.change_avatar_description.input_error' + ), + callback: (action, instance) => { + if ( + action === 'confirm' && + instance.inputValue !== avatar.ref.description + ) { + API.saveAvatar({ + id: avatar.id, + description: instance.inputValue + }).then((args) => { + this.$message({ + message: $t( + 'prompt.change_avatar_description.message.success' + ), + type: 'success' + }); + return args; + }); + } + } + } + ); + }, + + promptRenameWorld(world) { + this.$prompt( + $t('prompt.rename_world.description'), + $t('prompt.rename_world.header'), + { + distinguishCancelAndClose: true, + confirmButtonText: $t('prompt.rename_world.ok'), + cancelButtonText: $t('prompt.rename_world.cancel'), + inputValue: world.ref.name, + inputErrorMessage: $t('prompt.rename_world.input_error'), + callback: (action, instance) => { + if ( + action === 'confirm' && + instance.inputValue !== world.ref.name + ) { + API.saveWorld({ + id: world.id, + name: instance.inputValue + }).then((args) => { + this.$message({ + message: $t( + 'prompt.rename_world.message.success' + ), + type: 'success' + }); + return args; + }); + } + } + } + ); + }, + + promptChangeWorldDescription(world) { + this.$prompt( + $t('prompt.change_world_description.description'), + $t('prompt.change_world_description.header'), + { + distinguishCancelAndClose: true, + confirmButtonText: $t('prompt.change_world_description.ok'), + cancelButtonText: $t( + 'prompt.change_world_description.cancel' + ), + inputValue: world.ref.description, + inputErrorMessage: $t( + 'prompt.change_world_description.input_error' + ), + callback: (action, instance) => { + if ( + action === 'confirm' && + instance.inputValue !== world.ref.description + ) { + API.saveWorld({ + id: world.id, + description: instance.inputValue + }).then((args) => { + this.$message({ + message: $t( + 'prompt.change_world_description.message.success' + ), + type: 'success' + }); + return args; + }); + } + } + } + ); + }, + + promptChangeWorldCapacity(world) { + this.$prompt( + $t('prompt.change_world_capacity.description'), + $t('prompt.change_world_capacity.header'), + { + distinguishCancelAndClose: true, + confirmButtonText: $t('prompt.change_world_capacity.ok'), + cancelButtonText: $t('prompt.change_world_capacity.cancel'), + inputValue: world.ref.capacity, + inputPattern: /\d+$/, + inputErrorMessage: $t( + 'prompt.change_world_capacity.input_error' + ), + callback: (action, instance) => { + if ( + action === 'confirm' && + instance.inputValue !== world.ref.capacity + ) { + API.saveWorld({ + id: world.id, + capacity: instance.inputValue + }).then((args) => { + this.$message({ + message: $t( + 'prompt.change_world_capacity.message.success' + ), + type: 'success' + }); + return args; + }); + } + } + } + ); + }, + + promptChangeWorldRecommendedCapacity(world) { + this.$prompt( + $t('prompt.change_world_recommended_capacity.description'), + $t('prompt.change_world_recommended_capacity.header'), + { + distinguishCancelAndClose: true, + confirmButtonText: $t('prompt.change_world_capacity.ok'), + cancelButtonText: $t('prompt.change_world_capacity.cancel'), + inputValue: world.ref.recommendedCapacity, + inputPattern: /\d+$/, + inputErrorMessage: $t( + 'prompt.change_world_recommended_capacity.input_error' + ), + callback: (action, instance) => { + if ( + action === 'confirm' && + instance.inputValue !== + world.ref.recommendedCapacity + ) { + API.saveWorld({ + id: world.id, + recommendedCapacity: instance.inputValue + }).then((args) => { + this.$message({ + message: $t( + 'prompt.change_world_recommended_capacity.message.success' + ), + type: 'success' + }); + return args; + }); + } + } + } + ); + }, + + promptChangeWorldYouTubePreview(world) { + this.$prompt( + $t('prompt.change_world_preview.description'), + $t('prompt.change_world_preview.header'), + { + distinguishCancelAndClose: true, + confirmButtonText: $t('prompt.change_world_preview.ok'), + cancelButtonText: $t('prompt.change_world_preview.cancel'), + inputValue: world.ref.previewYoutubeId, + inputErrorMessage: $t( + 'prompt.change_world_preview.input_error' + ), + callback: (action, instance) => { + if ( + action === 'confirm' && + instance.inputValue !== world.ref.previewYoutubeId + ) { + if (instance.inputValue.length > 11) { + try { + var url = new URL(instance.inputValue); + var id1 = url.pathname; + var id2 = url.searchParams.get('v'); + if (id1 && id1.length === 12) { + instance.inputValue = id1.substring( + 1, + 12 + ); + } + if (id2 && id2.length === 11) { + instance.inputValue = id2; + } + } catch { + this.$message({ + message: $t( + 'prompt.change_world_preview.message.error' + ), + type: 'error' + }); + return; + } + } + if ( + instance.inputValue !== + world.ref.previewYoutubeId + ) { + API.saveWorld({ + id: world.id, + previewYoutubeId: instance.inputValue + }).then((args) => { + this.$message({ + message: $t( + 'prompt.change_world_preview.message.success' + ), + type: 'success' + }); + return args; + }); + } + } + } + } + ); + }, + + promptMaxTableSizeDialog() { + this.$prompt( + $t('prompt.change_table_size.description'), + $t('prompt.change_table_size.header'), + { + distinguishCancelAndClose: true, + confirmButtonText: $t('prompt.change_table_size.save'), + cancelButtonText: $t('prompt.change_table_size.cancel'), + inputValue: this.maxTableSize, + inputPattern: /\d+$/, + inputErrorMessage: $t( + 'prompt.change_table_size.input_error' + ), + callback: async (action, instance) => { + if (action === 'confirm' && instance.inputValue) { + if (instance.inputValue > 10000) { + instance.inputValue = 10000; + } + this.maxTableSize = instance.inputValue; + await configRepository.setString( + 'VRCX_maxTableSize', + this.maxTableSize + ); + database.setmaxTableSize(this.maxTableSize); + this.feedTableLookup(); + this.gameLogTableLookup(); + } + } + } + ); + }, + + promptProxySettings() { + this.$prompt( + $t('prompt.proxy_settings.description'), + $t('prompt.proxy_settings.header'), + { + distinguishCancelAndClose: true, + confirmButtonText: $t('prompt.proxy_settings.restart'), + cancelButtonText: $t('prompt.proxy_settings.close'), + inputValue: this.proxyServer, + inputPlaceholder: $t('prompt.proxy_settings.placeholder'), + callback: async (action, instance) => { + this.proxyServer = instance.inputValue; + await VRCXStorage.Set( + 'VRCX_ProxyServer', + this.proxyServer + ); + await VRCXStorage.Flush(); + await new Promise((resolve) => { + workerTimers.setTimeout(resolve, 100); + }); + if (action === 'confirm') { + var isUpgrade = false; + this.restartVRCX(isUpgrade); + } + } + } + ); + }, + + promptPhotonLobbyTimeoutThreshold() { + this.$prompt( + $t('prompt.photon_lobby_timeout.description'), + $t('prompt.photon_lobby_timeout.header'), + { + distinguishCancelAndClose: true, + confirmButtonText: $t('prompt.photon_lobby_timeout.ok'), + cancelButtonText: $t('prompt.photon_lobby_timeout.cancel'), + inputValue: this.photonLobbyTimeoutThreshold / 1000, + inputPattern: /\d+$/, + inputErrorMessage: $t( + 'prompt.photon_lobby_timeout.input_error' + ), + callback: async (action, instance) => { + if ( + action === 'confirm' && + instance.inputValue && + !isNaN(instance.inputValue) + ) { + this.photonLobbyTimeoutThreshold = Math.trunc( + Number(instance.inputValue) * 1000 + ); + await configRepository.setString( + 'VRCX_photonLobbyTimeoutThreshold', + this.photonLobbyTimeoutThreshold + ); + } + } + } + ); + }, + + promptAutoClearVRCXCacheFrequency() { + this.$prompt( + $t('prompt.auto_clear_cache.description'), + $t('prompt.auto_clear_cache.header'), + { + distinguishCancelAndClose: true, + confirmButtonText: $t('prompt.auto_clear_cache.ok'), + cancelButtonText: $t('prompt.auto_clear_cache.cancel'), + inputValue: this.clearVRCXCacheFrequency / 3600 / 2, + inputPattern: /\d+$/, + inputErrorMessage: $t( + 'prompt.auto_clear_cache.input_error' + ), + callback: async (action, instance) => { + if ( + action === 'confirm' && + instance.inputValue && + !isNaN(instance.inputValue) + ) { + this.clearVRCXCacheFrequency = Math.trunc( + Number(instance.inputValue) * 3600 * 2 + ); + await configRepository.setString( + 'VRCX_clearVRCXCacheFrequency', + this.clearVRCXCacheFrequency + ); + } + } + } + ); + } + }; +} diff --git a/html/src/classes/sharedFeed.js b/html/src/classes/sharedFeed.js new file mode 100644 index 000000000..08c80670c --- /dev/null +++ b/html/src/classes/sharedFeed.js @@ -0,0 +1,595 @@ +import * as workerTimers from 'worker-timers'; +import configRepository from '../repository/config.js'; +import { baseClass, $app, API, $t, $utils } from './baseClass.js'; + +export default class extends baseClass { + constructor(_app, _API, _t) { + super(_app, _API, _t); + } + + _data = { + sharedFeed: { + gameLog: { + wrist: [], + lastEntryDate: '' + }, + feedTable: { + wrist: [], + lastEntryDate: '' + }, + notificationTable: { + wrist: [], + lastEntryDate: '' + }, + friendLogTable: { + wrist: [], + lastEntryDate: '' + }, + moderationAgainstTable: { + wrist: [], + lastEntryDate: '' + }, + pendingUpdate: false + }, + updateSharedFeedTimer: null, + updateSharedFeedPending: false, + updateSharedFeedPendingForceUpdate: false + }; + + _methods = { + updateSharedFeed(forceUpdate) { + if (!this.friendLogInitStatus) { + return; + } + if (this.updateSharedFeedTimer) { + if (forceUpdate) { + this.updateSharedFeedPendingForceUpdate = true; + } + this.updateSharedFeedPending = true; + } else { + this.updateSharedExecute(forceUpdate); + this.updateSharedFeedTimer = setTimeout(() => { + if (this.updateSharedFeedPending) { + this.updateSharedExecute( + this.updateSharedFeedPendingForceUpdate + ); + } + this.updateSharedFeedTimer = null; + }, 150); + } + }, + + updateSharedExecute(forceUpdate) { + try { + this.updateSharedFeedDebounce(forceUpdate); + } catch (err) { + console.error(err); + } + this.updateSharedFeedTimer = null; + this.updateSharedFeedPending = false; + this.updateSharedFeedPendingForceUpdate = false; + }, + + updateSharedFeedDebounce(forceUpdate) { + this.updateSharedFeedGameLog(forceUpdate); + this.updateSharedFeedFeedTable(forceUpdate); + this.updateSharedFeedNotificationTable(forceUpdate); + this.updateSharedFeedFriendLogTable(forceUpdate); + this.updateSharedFeedModerationAgainstTable(forceUpdate); + var feeds = this.sharedFeed; + if (!feeds.pendingUpdate) { + return; + } + var wristFeed = []; + wristFeed = wristFeed.concat( + feeds.gameLog.wrist, + feeds.feedTable.wrist, + feeds.notificationTable.wrist, + feeds.friendLogTable.wrist, + feeds.moderationAgainstTable.wrist + ); + // OnPlayerJoining/Traveling + API.currentTravelers.forEach((ref) => { + var isFavorite = this.localFavoriteFriends.has(ref.id); + if ( + (this.sharedFeedFilters.wrist.OnPlayerJoining === + 'Friends' || + (this.sharedFeedFilters.wrist.OnPlayerJoining === + 'VIP' && + isFavorite)) && + !$app.lastLocation.playerList.has(ref.displayName) + ) { + if (ref.$location.tag === $app.lastLocation.location) { + var feedEntry = { + ...ref, + isFavorite, + isFriend: true, + type: 'OnPlayerJoining' + }; + wristFeed.unshift(feedEntry); + } else { + var worldRef = API.cachedWorlds.get( + ref.$location.worldId + ); + var groupName = ''; + if (ref.$location.groupId) { + var groupRef = API.cachedGroups.get( + ref.$location.groupId + ); + if (typeof groupRef !== 'undefined') { + groupName = groupRef.name; + } else { + // no group cache, fetch group and try again + API.getGroup({ + groupId: ref.$location.groupId + }) + .then((args) => { + workerTimers.setTimeout(() => { + // delay to allow for group cache to update + $app.sharedFeed.pendingUpdate = true; + $app.updateSharedFeed(false); + }, 100); + return args; + }) + .catch((err) => { + console.error(err); + }); + } + } + if (typeof worldRef !== 'undefined') { + var feedEntry = { + created_at: ref.created_at, + type: 'GPS', + userId: ref.id, + displayName: ref.displayName, + location: ref.$location.tag, + worldName: worldRef.name, + groupName, + previousLocation: '', + isFavorite, + time: 0, + isFriend: true, + isTraveling: true + }; + wristFeed.unshift(feedEntry); + } else { + // no world cache, fetch world and try again + API.getWorld({ + worldId: ref.$location.worldId + }) + .then((args) => { + workerTimers.setTimeout(() => { + // delay to allow for world cache to update + $app.sharedFeed.pendingUpdate = true; + $app.updateSharedFeed(false); + }, 100); + return args; + }) + .catch((err) => { + console.error(err); + }); + } + } + } + }); + wristFeed.sort(function (a, b) { + if (a.created_at < b.created_at) { + return 1; + } + if (a.created_at > b.created_at) { + return -1; + } + return 0; + }); + wristFeed.splice(16); + AppApi.ExecuteVrFeedFunction( + 'wristFeedUpdate', + JSON.stringify(wristFeed) + ); + this.applyUserDialogLocation(); + this.applyWorldDialogInstances(); + this.applyGroupDialogInstances(); + feeds.pendingUpdate = false; + }, + + updateSharedFeedGameLog(forceUpdate) { + // Location, OnPlayerJoined, OnPlayerLeft + var sessionTable = this.gameLogSessionTable; + var i = sessionTable.length; + if (i > 0) { + if ( + sessionTable[i - 1].created_at === + this.sharedFeed.gameLog.lastEntryDate && + forceUpdate === false + ) { + return; + } + this.sharedFeed.gameLog.lastEntryDate = + sessionTable[i - 1].created_at; + } else { + return; + } + var bias = new Date(Date.now() - 86400000).toJSON(); // 24 hours + var wristArr = []; + var w = 0; + var wristFilter = this.sharedFeedFilters.wrist; + var currentUserLeaveTime = 0; + var locationJoinTime = 0; + for (var i = sessionTable.length - 1; i > -1; i--) { + var ctx = sessionTable[i]; + if (ctx.created_at < bias) { + break; + } + if (ctx.type === 'Notification') { + continue; + } + // on Location change remove OnPlayerLeft + if (ctx.type === 'LocationDestination') { + currentUserLeaveTime = Date.parse(ctx.created_at); + var currentUserLeaveTimeOffset = + currentUserLeaveTime + 5 * 1000; + for (var k = w - 1; k > -1; k--) { + var feedItem = wristArr[k]; + if ( + (feedItem.type === 'OnPlayerLeft' || + feedItem.type === 'BlockedOnPlayerLeft' || + feedItem.type === 'MutedOnPlayerLeft') && + Date.parse(feedItem.created_at) >= + currentUserLeaveTime && + Date.parse(feedItem.created_at) <= + currentUserLeaveTimeOffset + ) { + wristArr.splice(k, 1); + w--; + } + } + } + // on Location change remove OnPlayerJoined + if (ctx.type === 'Location') { + locationJoinTime = Date.parse(ctx.created_at); + var locationJoinTimeOffset = locationJoinTime + 20 * 1000; + for (var k = w - 1; k > -1; k--) { + var feedItem = wristArr[k]; + if ( + (feedItem.type === 'OnPlayerJoined' || + feedItem.type === 'BlockedOnPlayerJoined' || + feedItem.type === 'MutedOnPlayerJoined') && + Date.parse(feedItem.created_at) >= + locationJoinTime && + Date.parse(feedItem.created_at) <= + locationJoinTimeOffset + ) { + wristArr.splice(k, 1); + w--; + } + } + } + // remove current user + if ( + (ctx.type === 'OnPlayerJoined' || + ctx.type === 'OnPlayerLeft' || + ctx.type === 'PortalSpawn') && + ctx.displayName === API.currentUser.displayName + ) { + continue; + } + var isFriend = false; + var isFavorite = false; + if (ctx.userId) { + isFriend = this.friends.has(ctx.userId); + isFavorite = this.localFavoriteFriends.has(ctx.userId); + } else if (ctx.displayName) { + for (var ref of API.cachedUsers.values()) { + if (ref.displayName === ctx.displayName) { + isFriend = this.friends.has(ref.id); + isFavorite = this.localFavoriteFriends.has(ref.id); + break; + } + } + } + // add tag colour + var tagColour = ''; + if (ctx.userId) { + var tagRef = this.customUserTags.get(ctx.userId); + if (typeof tagRef !== 'undefined') { + tagColour = tagRef.colour; + } + } + // BlockedOnPlayerJoined, BlockedOnPlayerLeft, MutedOnPlayerJoined, MutedOnPlayerLeft + if ( + ctx.type === 'OnPlayerJoined' || + ctx.type === 'OnPlayerLeft' + ) { + for (var ref of API.cachedPlayerModerations.values()) { + if ( + ref.targetDisplayName !== ctx.displayName && + ref.sourceUserId !== ctx.userId + ) { + continue; + } + + if (ref.type === 'block') { + var type = `Blocked${ctx.type}`; + } else if (ref.type === 'mute') { + var type = `Muted${ctx.type}`; + } else { + continue; + } + + var entry = { + created_at: ctx.created_at, + type, + displayName: ref.targetDisplayName, + userId: ref.targetUserId, + isFriend, + isFavorite + }; + if ( + wristFilter[type] && + (wristFilter[type] === 'Everyone' || + (wristFilter[type] === 'Friends' && isFriend) || + (wristFilter[type] === 'VIP' && isFavorite)) + ) { + wristArr.unshift(entry); + } + this.queueGameLogNoty(entry); + } + } + // when too many user joins happen at once when switching instances + // the "w" counter maxes out and wont add any more entries + // until the onJoins are cleared by "Location" + // e.g. if a "VideoPlay" occurs between "OnPlayerJoined" and "Location" it wont be added + if ( + w < 50 && + wristFilter[ctx.type] && + (wristFilter[ctx.type] === 'On' || + wristFilter[ctx.type] === 'Everyone' || + (wristFilter[ctx.type] === 'Friends' && isFriend) || + (wristFilter[ctx.type] === 'VIP' && isFavorite)) + ) { + wristArr.push({ + ...ctx, + tagColour, + isFriend, + isFavorite + }); + ++w; + } + } + this.sharedFeed.gameLog.wrist = wristArr; + this.sharedFeed.pendingUpdate = true; + }, + + updateSharedFeedFeedTable(forceUpdate) { + // GPS, Online, Offline, Status, Avatar + var feedSession = this.feedSessionTable; + var i = feedSession.length; + if (i > 0) { + if ( + feedSession[i - 1].created_at === + this.sharedFeed.feedTable.lastEntryDate && + forceUpdate === false + ) { + return; + } + this.sharedFeed.feedTable.lastEntryDate = + feedSession[i - 1].created_at; + } else { + return; + } + var bias = new Date(Date.now() - 86400000).toJSON(); // 24 hours + var wristArr = []; + var w = 0; + var wristFilter = this.sharedFeedFilters.wrist; + for (var i = feedSession.length - 1; i > -1; i--) { + var ctx = feedSession[i]; + if (ctx.created_at < bias) { + break; + } + if (ctx.type === 'Avatar') { + continue; + } + // hide private worlds from feed + if ( + this.hidePrivateFromFeed && + ctx.type === 'GPS' && + ctx.location === 'private' + ) { + continue; + } + var isFriend = this.friends.has(ctx.userId); + var isFavorite = this.localFavoriteFriends.has(ctx.userId); + if ( + w < 20 && + wristFilter[ctx.type] && + (wristFilter[ctx.type] === 'Friends' || + (wristFilter[ctx.type] === 'VIP' && isFavorite)) + ) { + wristArr.push({ + ...ctx, + isFriend, + isFavorite + }); + ++w; + } + } + this.sharedFeed.feedTable.wrist = wristArr; + this.sharedFeed.pendingUpdate = true; + }, + + updateSharedFeedNotificationTable(forceUpdate) { + // invite, requestInvite, requestInviteResponse, inviteResponse, friendRequest + var notificationTable = this.notificationTable; + var i = notificationTable.length; + if (i > 0) { + if ( + notificationTable[i - 1].created_at === + this.sharedFeed.notificationTable.lastEntryDate && + forceUpdate === false + ) { + return; + } + this.sharedFeed.notificationTable.lastEntryDate = + notificationTable[i - 1].created_at; + } else { + return; + } + var bias = new Date(Date.now() - 86400000).toJSON(); // 24 hours + var wristArr = []; + var w = 0; + var wristFilter = this.sharedFeedFilters.wrist; + for (var i = notificationTable.length - 1; i > -1; i--) { + var ctx = notificationTable[i]; + if (ctx.created_at < bias) { + break; + } + if (ctx.senderUserId === API.currentUser.id) { + continue; + } + var isFriend = this.friends.has(ctx.senderUserId); + var isFavorite = this.localFavoriteFriends.has( + ctx.senderUserId + ); + if ( + w < 20 && + wristFilter[ctx.type] && + (wristFilter[ctx.type] === 'On' || + wristFilter[ctx.type] === 'Friends' || + (wristFilter[ctx.type] === 'VIP' && isFavorite)) + ) { + wristArr.push({ + ...ctx, + isFriend, + isFavorite + }); + ++w; + } + } + this.sharedFeed.notificationTable.wrist = wristArr; + this.sharedFeed.pendingUpdate = true; + }, + + updateSharedFeedFriendLogTable(forceUpdate) { + // TrustLevel, Friend, FriendRequest, Unfriend, DisplayName + var friendLog = this.friendLogTable; + var i = friendLog.length; + if (i > 0) { + if ( + friendLog[i - 1].created_at === + this.sharedFeed.friendLogTable.lastEntryDate && + forceUpdate === false + ) { + return; + } + this.sharedFeed.friendLogTable.lastEntryDate = + friendLog[i - 1].created_at; + } else { + return; + } + var bias = new Date(Date.now() - 86400000).toJSON(); // 24 hours + var wristArr = []; + var w = 0; + var wristFilter = this.sharedFeedFilters.wrist; + for (var i = friendLog.length - 1; i > -1; i--) { + var ctx = friendLog[i]; + if (ctx.created_at < bias) { + break; + } + if (ctx.type === 'FriendRequest') { + continue; + } + var isFriend = this.friends.has(ctx.userId); + var isFavorite = this.localFavoriteFriends.has(ctx.userId); + if ( + w < 20 && + wristFilter[ctx.type] && + (wristFilter[ctx.type] === 'On' || + wristFilter[ctx.type] === 'Friends' || + (wristFilter[ctx.type] === 'VIP' && isFavorite)) + ) { + wristArr.push({ + ...ctx, + isFriend, + isFavorite + }); + ++w; + } + } + this.sharedFeed.friendLogTable.wrist = wristArr; + this.sharedFeed.pendingUpdate = true; + }, + + updateSharedFeedModerationAgainstTable(forceUpdate) { + // Unblocked, Blocked, Muted, Unmuted + var moderationAgainst = this.moderationAgainstTable; + var i = moderationAgainst.length; + if (i > 0) { + if ( + moderationAgainst[i - 1].created_at === + this.sharedFeed.moderationAgainstTable.lastEntryDate && + forceUpdate === false + ) { + return; + } + this.sharedFeed.moderationAgainstTable.lastEntryDate = + moderationAgainst[i - 1].created_at; + } else { + return; + } + var bias = new Date(Date.now() - 86400000).toJSON(); // 24 hours + var wristArr = []; + var w = 0; + var wristFilter = this.sharedFeedFilters.wrist; + for (var i = moderationAgainst.length - 1; i > -1; i--) { + var ctx = moderationAgainst[i]; + if (ctx.created_at < bias) { + break; + } + var isFriend = this.friends.has(ctx.userId); + var isFavorite = this.localFavoriteFriends.has(ctx.userId); + // add tag colour + var tagColour = ''; + var tagRef = this.customUserTags.get(ctx.userId); + if (typeof tagRef !== 'undefined') { + tagColour = tagRef.colour; + } + if ( + w < 20 && + wristFilter[ctx.type] && + wristFilter[ctx.type] === 'On' + ) { + wristArr.push({ + ...ctx, + isFriend, + isFavorite, + tagColour + }); + ++w; + } + } + this.sharedFeed.moderationAgainstTable.wrist = wristArr; + this.sharedFeed.pendingUpdate = true; + }, + + saveSharedFeedFilters() { + configRepository.setString( + 'sharedFeedFilters', + JSON.stringify(this.sharedFeedFilters) + ); + this.updateSharedFeed(true); + }, + + async resetSharedFeedFilters() { + if (await configRepository.getString('sharedFeedFilters')) { + this.sharedFeedFilters = JSON.parse( + await configRepository.getString( + 'sharedFeedFilters', + JSON.stringify(this.sharedFeedFiltersDefaults) + ) + ); + } else { + this.sharedFeedFilters = this.sharedFeedFiltersDefaults; + } + } + }; +} diff --git a/html/src/classes/uiComponents.js b/html/src/classes/uiComponents.js new file mode 100644 index 000000000..73373f043 --- /dev/null +++ b/html/src/classes/uiComponents.js @@ -0,0 +1,602 @@ +import Vue from 'vue'; +import VueMarkdown from 'vue-markdown'; +import { baseClass, $app, API, $t, $utils } from './baseClass.js'; + +export default class extends baseClass { + constructor(_app, _API, _t) { + super(_app, _API, _t); + } + + init() { + Vue.component('vue-markdown', VueMarkdown); + + Vue.component('launch', { + template: + '', + props: { + location: String + }, + methods: { + parse() { + this.$el.style.display = $app.checkCanInviteSelf( + this.location + ) + ? '' + : 'none'; + }, + confirm() { + API.$emit('SHOW_LAUNCH_DIALOG', this.location); + } + }, + watch: { + location() { + this.parse(); + } + }, + mounted() { + this.parse(); + } + }); + + Vue.component('invite-yourself', { + template: + '', + props: { + location: String, + shortname: String + }, + methods: { + parse() { + this.$el.style.display = $app.checkCanInviteSelf( + this.location + ) + ? '' + : 'none'; + }, + confirm() { + $app.selfInvite(this.location, this.shortname); + } + }, + watch: { + location() { + this.parse(); + } + }, + mounted() { + this.parse(); + } + }); + + Vue.component('location', { + template: + "" + + '' + + '{{ text }}' + + '({{ groupName }})' + + '' + + '', + props: { + location: String, + traveling: String, + hint: { + type: String, + default: '' + }, + grouphint: { + type: String, + default: '' + }, + link: { + type: Boolean, + default: true + } + }, + data() { + return { + text: this.location, + region: this.region, + strict: this.strict, + isTraveling: this.isTraveling, + groupName: this.groupName + }; + }, + methods: { + parse() { + this.isTraveling = false; + this.groupName = ''; + var instanceId = this.location; + if ( + typeof this.traveling !== 'undefined' && + this.location === 'traveling' + ) { + instanceId = this.traveling; + this.isTraveling = true; + } + this.text = instanceId; + var L = $utils.parseLocation(instanceId); + if (L.isOffline) { + this.text = 'Offline'; + } else if (L.isPrivate) { + this.text = 'Private'; + } else if (L.isTraveling) { + this.text = 'Traveling'; + } else if ( + typeof this.hint === 'string' && + this.hint !== '' + ) { + if (L.instanceId) { + this.text = `${this.hint} #${L.instanceName} ${L.accessTypeName}`; + } else { + this.text = this.hint; + } + } else if (L.worldId) { + var ref = API.cachedWorlds.get(L.worldId); + if (typeof ref === 'undefined') { + $app.getWorldName(L.worldId).then((worldName) => { + if (L.tag === instanceId) { + if (L.instanceId) { + this.text = `${worldName} #${L.instanceName} ${L.accessTypeName}`; + } else { + this.text = worldName; + } + } + }); + } else if (L.instanceId) { + this.text = `${ref.name} #${L.instanceName} ${L.accessTypeName}`; + } else { + this.text = ref.name; + } + } + if (this.grouphint) { + this.groupName = this.grouphint; + } else if (L.groupId) { + this.groupName = L.groupId; + $app.getGroupName(instanceId).then((groupName) => { + if (L.tag === instanceId) { + this.groupName = groupName; + } + }); + } + this.region = ''; + if (!L.isOffline && !L.isPrivate && !L.isTraveling) { + this.region = L.region; + if (!L.region && L.instanceId) { + this.region = 'us'; + } + } + this.strict = L.strict; + }, + showWorldDialog() { + if (this.link) { + var instanceId = this.location; + if (this.traveling && this.location === 'traveling') { + instanceId = this.traveling; + } + if (!instanceId && this.hint.length === 8) { + // shortName + API.$emit('SHOW_WORLD_DIALOG_SHORTNAME', this.hint); + return; + } + API.$emit('SHOW_WORLD_DIALOG', instanceId); + } + }, + showGroupDialog() { + var location = this.location; + if (this.isTraveling) { + location = this.traveling; + } + if (!location || !this.link) { + return; + } + var L = $utils.parseLocation(location); + if (!L.groupId) { + return; + } + API.$emit('SHOW_GROUP_DIALOG', L.groupId); + } + }, + watch: { + location() { + this.parse(); + } + }, + created() { + this.parse(); + } + }); + + Vue.component('location-world', { + template: + '' + + '' + + '#{{ instanceName }} {{ accessTypeName }}' + + '({{ groupName }})' + + '' + + '', + props: { + locationobject: Object, + currentuserid: String, + worlddialogshortname: String, + grouphint: { + type: String, + default: '' + } + }, + data() { + return { + location: this.location, + instanceName: this.instanceName, + accessTypeName: this.accessTypeName, + region: this.region, + shortName: this.shortName, + isUnlocked: this.isUnlocked, + strict: this.strict, + groupName: this.groupName + }; + }, + methods: { + parse() { + this.location = this.locationobject.tag; + this.instanceName = this.locationobject.instanceName; + this.accessTypeName = this.locationobject.accessTypeName; + this.strict = this.locationobject.strict; + this.shortName = this.locationobject.shortName; + + this.isUnlocked = false; + if ( + (this.worlddialogshortname && + this.locationobject.shortName && + this.worlddialogshortname === + this.locationobject.shortName) || + this.currentuserid === this.locationobject.userId + ) { + this.isUnlocked = true; + } + + this.region = this.locationobject.region; + if (!this.region) { + this.region = 'us'; + } + + this.groupName = ''; + if (this.grouphint) { + this.groupName = this.grouphint; + } else if (this.locationobject.groupId) { + this.groupName = this.locationobject.groupId; + $app.getGroupName(this.locationobject.groupId).then( + (groupName) => { + this.groupName = groupName; + } + ); + } + }, + showLaunchDialog() { + API.$emit( + 'SHOW_LAUNCH_DIALOG', + this.location, + this.shortName + ); + }, + showGroupDialog() { + if (!this.location) { + return; + } + var L = $utils.parseLocation(this.location); + if (!L.groupId) { + return; + } + API.$emit('SHOW_GROUP_DIALOG', L.groupId); + } + }, + watch: { + locationobject() { + this.parse(); + } + }, + created() { + this.parse(); + } + }); + + Vue.component('last-join', { + template: + '' + + '' + + '
' + + '{{ $t("dialog.user.info.last_join") }} ' + + '
' + + '' + + '
' + + '
', + props: { + location: String, + currentlocation: String + }, + data() { + return { + lastJoin: this.lastJoin + }; + }, + methods: { + parse() { + this.lastJoin = $app.instanceJoinHistory.get(this.location); + } + }, + watch: { + location() { + this.parse(); + }, + currentlocation() { + this.parse(); + } + }, + created() { + this.parse(); + } + }); + + Vue.component('instance-info', { + template: + '
' + + '' + + '
' + + '' + + '' + + 'PC: {{ platforms.standalonewindows }}
' + + 'Android: {{ platforms.android }}
' + + '{{ $t("dialog.user.info.instance_game_version") }} {{ gameServerVersion }}
' + + '{{ $t("dialog.user.info.instance_queuing_enabled") }}
' + + '{{ $t("dialog.user.info.instance_users") }}
' + + '' + + '
' + + '' + + '
' + + '{{ occupants }}/{{ capacity }}' + + '({{ friendcount }})' + + '{{ $t("dialog.user.info.instance_full") }}' + + '{{ $t("dialog.user.info.instance_hard_closed") }}' + + '{{ $t("dialog.user.info.instance_closed") }}' + + '{{ $t("dialog.user.info.instance_queue") }} {{ queueSize }}' + + '
', + props: { + location: String, + instance: Object, + friendcount: Number, + updateelement: Number + }, + data() { + return { + isValidInstance: this.isValidInstance, + isFull: this.isFull, + isClosed: this.isClosed, + isHardClosed: this.isHardClosed, + closedAt: this.closedAt, + occupants: this.occupants, + capacity: this.capacity, + queueSize: this.queueSize, + queueEnabled: this.queueEnabled, + platforms: this.platforms, + userList: this.userList, + gameServerVersion: this.gameServerVersion, + canCloseInstance: this.canCloseInstance + }; + }, + methods: { + parse() { + this.isValidInstance = false; + this.isFull = false; + this.isClosed = false; + this.isHardClosed = false; + this.closedAt = ''; + this.occupants = 0; + this.capacity = 0; + this.queueSize = 0; + this.queueEnabled = false; + this.platforms = []; + this.userList = []; + this.gameServerVersion = ''; + this.canCloseInstance = false; + if ( + !this.location || + !this.instance || + Object.keys(this.instance).length === 0 + ) { + return; + } + this.isValidInstance = true; + this.isFull = + typeof this.instance.hasCapacityForYou !== + 'undefined' && !this.instance.hasCapacityForYou; + if (this.instance.closedAt) { + this.isClosed = true; + this.closedAt = this.instance.closedAt; + } + this.isHardClosed = this.instance.hardClose === true; + this.occupants = this.instance.userCount; + if (this.location === $app.lastLocation.location) { + // use gameLog for occupants when in same location + this.occupants = $app.lastLocation.playerList.size; + } + this.capacity = this.instance.capacity; + this.gameServerVersion = this.instance.gameServerVersion; + this.queueSize = this.instance.queueSize; + if (this.instance.platforms) { + this.platforms = this.instance.platforms; + } + if (this.instance.users) { + this.userList = this.instance.users; + } + if (this.instance.ownerId === API.currentUser.id) { + this.canCloseInstance = true; + } else if (this.instance?.ownerId?.startsWith('grp_')) { + // check group perms + var groupId = this.instance.ownerId; + var group = API.cachedGroups.get(groupId); + this.canCloseInstance = $app.hasGroupPermission( + group, + 'group-instance-moderate' + ); + } + }, + showUserDialog(userId) { + API.$emit('SHOW_USER_DIALOG', userId); + } + }, + watch: { + updateelement() { + this.parse(); + }, + location() { + this.parse(); + }, + friendcount() { + this.parse(); + } + }, + created() { + this.parse(); + } + }); + + Vue.component('avatar-info', { + template: + '
' + + '{{ avatarName }}' + + '{{ avatarType }}' + + '{{ avatarTags }}' + + '
', + props: { + imageurl: String, + userid: String, + hintownerid: String, + hintavatarname: String, + avatartags: Array + }, + data() { + return { + avatarName: this.avatarName, + avatarType: this.avatarType, + avatarTags: this.avatarTags, + color: this.color + }; + }, + methods: { + async parse() { + this.ownerId = ''; + this.avatarName = ''; + this.avatarType = ''; + this.color = ''; + this.avatarTags = ''; + if (!this.imageurl) { + this.avatarName = '-'; + } else if (this.hintownerid) { + this.avatarName = this.hintavatarname; + this.ownerId = this.hintownerid; + } else { + try { + var avatarInfo = await $app.getAvatarName( + this.imageurl + ); + this.avatarName = avatarInfo.avatarName; + this.ownerId = avatarInfo.ownerId; + } catch (err) {} + } + if (typeof this.userid === 'undefined' || !this.ownerId) { + this.color = ''; + this.avatarType = ''; + } else if (this.ownerId === this.userid) { + this.color = 'avatar-info-own'; + this.avatarType = '(own)'; + } else { + this.color = 'avatar-info-public'; + this.avatarType = '(public)'; + } + if (typeof this.avatartags === 'object') { + var tagString = ''; + for (var i = 0; i < this.avatartags.length; i++) { + var tagName = this.avatartags[i].replace( + 'content_', + '' + ); + tagString += tagName; + if (i < this.avatartags.length - 1) { + tagString += ', '; + } + } + this.avatarTags = tagString; + } + }, + confirm() { + if (!this.imageurl) { + return; + } + $app.showAvatarAuthorDialog( + this.userid, + this.ownerId, + this.imageurl + ); + } + }, + watch: { + imageurl() { + this.parse(); + }, + userid() { + this.parse(); + }, + avatartags() { + this.parse(); + } + }, + mounted() { + this.parse(); + } + }); + + Vue.component('display-name', { + template: + '{{ username }}', + props: { + userid: String, + location: String, + key: Number + }, + data() { + return { + username: this.username + }; + }, + methods: { + async parse() { + this.username = this.userid; + if (this.userid) { + var args = await API.getCachedUser({ + userId: this.userid + }); + } + if ( + typeof args !== 'undefined' && + typeof args.json !== 'undefined' && + typeof args.json.displayName !== 'undefined' + ) { + this.username = args.json.displayName; + } + }, + showUserDialog() { + $app.showUserDialog(this.userid); + } + }, + watch: { + location() { + this.parse(); + }, + key() { + this.parse(); + } + }, + mounted() { + this.parse(); + } + }); + } +} diff --git a/html/src/classes/updateLoop.js b/html/src/classes/updateLoop.js new file mode 100644 index 000000000..3eccdc04d --- /dev/null +++ b/html/src/classes/updateLoop.js @@ -0,0 +1,78 @@ +import * as workerTimers from 'worker-timers'; +import { baseClass, $app, API, $t, $utils } from './baseClass.js'; + +export default class extends baseClass { + constructor(_app, _API, _t) { + super(_app, _API, _t); + } + + _data = { + nextCurrentUserRefresh: 0, + nextFriendsRefresh: 0, + nextGroupInstanceRefresh: 0, + nextAppUpdateCheck: 3600, + ipcTimeout: 0, + nextClearVRCXCacheCheck: 0, + nextDiscordUpdate: 0, + nextAutoStateChange: 0 + }; + + _methods = { + updateLoop() { + try { + if (API.isLoggedIn === true) { + if (--this.nextCurrentUserRefresh <= 0) { + this.nextCurrentUserRefresh = 300; // 5min + API.getCurrentUser(); + } + if (--this.nextFriendsRefresh <= 0) { + this.nextFriendsRefresh = 3600; // 1hour + this.refreshFriendsList(); + this.updateStoredUser(API.currentUser); + if (this.isGameRunning) { + API.refreshPlayerModerations(); + } + } + if (--this.nextGroupInstanceRefresh <= 0) { + if (this.friendLogInitStatus) { + this.nextGroupInstanceRefresh = 300; // 5min + API.getUsersGroupInstances(); + } + AppApi.CheckGameRunning(); + } + if (--this.nextAppUpdateCheck <= 0) { + this.nextAppUpdateCheck = 3600; // 1hour + if (this.autoUpdateVRCX !== 'Off') { + this.checkForVRCXUpdate(); + } + } + if (--this.ipcTimeout <= 0) { + this.ipcEnabled = false; + } + if ( + --this.nextClearVRCXCacheCheck <= 0 && + this.clearVRCXCacheFrequency > 0 + ) { + this.nextClearVRCXCacheCheck = + this.clearVRCXCacheFrequency / 2; + this.clearVRCXCache(); + } + if (--this.nextDiscordUpdate <= 0) { + this.nextDiscordUpdate = 3; + if (this.discordActive) { + this.updateDiscord(); + } + } + if (--this.nextAutoStateChange <= 0) { + this.nextAutoStateChange = 3; + this.updateAutoStateChange(); + } + } + } catch (err) { + API.isRefreshFriendsLoading = false; + console.error(err); + } + workerTimers.setTimeout(() => this.updateLoop(), 1000); + } + }; +} diff --git a/html/src/classes/utils.js b/html/src/classes/utils.js new file mode 100644 index 000000000..76ea1b91b --- /dev/null +++ b/html/src/classes/utils.js @@ -0,0 +1,302 @@ +export default class { + $utils = { + removeFromArray(array, item) { + var { length } = array; + for (var i = 0; i < length; ++i) { + if (array[i] === item) { + array.splice(i, 1); + return true; + } + } + return false; + }, + + arraysMatch(a, b) { + if (!Array.isArray(a) || !Array.isArray(b)) { + return false; + } + return ( + a.length === b.length && + a.every( + (element, index) => + JSON.stringify(element) === JSON.stringify(b[index]) + ) + ); + }, + + escapeTag(tag) { + var s = String(tag); + return s.replace(/["&'<>]/g, (c) => `&#${c.charCodeAt(0)};`); + }, + + escapeTagRecursive(obj) { + if (typeof obj === 'string') { + return this.escapeTag(obj); + } + if (typeof obj === 'object') { + for (var key in obj) { + obj[key] = this.escapeTagRecursive(obj[key]); + } + } + return obj; + }, + + timeToText(sec) { + var n = Number(sec); + if (isNaN(n)) { + return this.escapeTag(sec); + } + n = Math.floor(n / 1000); + var arr = []; + if (n < 0) { + n = -n; + } + if (n >= 86400) { + arr.push(`${Math.floor(n / 86400)}d`); + n %= 86400; + } + if (n >= 3600) { + arr.push(`${Math.floor(n / 3600)}h`); + n %= 3600; + } + if (n >= 60) { + arr.push(`${Math.floor(n / 60)}m`); + n %= 60; + } + if (arr.length === 0 && n < 60) { + arr.push(`${n}s`); + } + return arr.join(' '); + }, + + textToHex(text) { + var s = String(text); + return s + .split('') + .map((c) => c.charCodeAt(0).toString(16)) + .join(' '); + }, + + commaNumber(num) { + if (!num) { + return '0'; + } + var s = String(Number(num)); + return s.replace(/(\d)(?=(\d{3})+(?!\d))/g, '$1,'); + }, + + parseLocation(tag) { + var _tag = String(tag || ''); + var ctx = { + tag: _tag, + isOffline: false, + isPrivate: false, + isTraveling: false, + worldId: '', + instanceId: '', + instanceName: '', + accessType: '', + accessTypeName: '', + region: '', + shortName: '', + userId: null, + hiddenId: null, + privateId: null, + friendsId: null, + groupId: null, + groupAccessType: null, + canRequestInvite: false, + strict: false + }; + if (_tag === 'offline' || _tag === 'offline:offline') { + ctx.isOffline = true; + } else if (_tag === 'private' || _tag === 'private:private') { + ctx.isPrivate = true; + } else if (_tag === 'traveling' || _tag === 'traveling:traveling') { + ctx.isTraveling = true; + } else if (_tag.startsWith('local') === false) { + var sep = _tag.indexOf(':'); + // technically not part of instance id, but might be there when coping id from url so why not support it + var shortNameQualifier = '&shortName='; + var shortNameIndex = _tag.indexOf(shortNameQualifier); + if (shortNameIndex >= 0) { + ctx.shortName = _tag.substr( + shortNameIndex + shortNameQualifier.length + ); + _tag = _tag.substr(0, shortNameIndex); + } + if (sep >= 0) { + ctx.worldId = _tag.substr(0, sep); + ctx.instanceId = _tag.substr(sep + 1); + ctx.instanceId.split('~').forEach((s, i) => { + if (i) { + var A = s.indexOf('('); + var Z = A >= 0 ? s.lastIndexOf(')') : -1; + var key = Z >= 0 ? s.substr(0, A) : s; + var value = A < Z ? s.substr(A + 1, Z - A - 1) : ''; + if (key === 'hidden') { + ctx.hiddenId = value; + } else if (key === 'private') { + ctx.privateId = value; + } else if (key === 'friends') { + ctx.friendsId = value; + } else if (key === 'canRequestInvite') { + ctx.canRequestInvite = true; + } else if (key === 'region') { + ctx.region = value; + } else if (key === 'group') { + ctx.groupId = value; + } else if (key === 'groupAccessType') { + ctx.groupAccessType = value; + } else if (key === 'strict') { + ctx.strict = true; + } + } else { + ctx.instanceName = s; + } + }); + ctx.accessType = 'public'; + if (ctx.privateId !== null) { + if (ctx.canRequestInvite) { + // InvitePlus + ctx.accessType = 'invite+'; + } else { + // InviteOnly + ctx.accessType = 'invite'; + } + ctx.userId = ctx.privateId; + } else if (ctx.friendsId !== null) { + // FriendsOnly + ctx.accessType = 'friends'; + ctx.userId = ctx.friendsId; + } else if (ctx.hiddenId !== null) { + // FriendsOfGuests + ctx.accessType = 'friends+'; + ctx.userId = ctx.hiddenId; + } else if (ctx.groupId !== null) { + // Group + ctx.accessType = 'group'; + } + ctx.accessTypeName = ctx.accessType; + if (ctx.groupAccessType !== null) { + if (ctx.groupAccessType === 'public') { + ctx.accessTypeName = 'groupPublic'; + } else if (ctx.groupAccessType === 'plus') { + ctx.accessTypeName = 'groupPlus'; + } + } + } else { + ctx.worldId = _tag; + } + } + return ctx; + }, + + displayLocation(location, worldName, groupName) { + var text = worldName; + var L = this.parseLocation(location); + if (L.isOffline) { + text = 'Offline'; + } else if (L.isPrivate) { + text = 'Private'; + } else if (L.isTraveling) { + text = 'Traveling'; + } else if (L.worldId) { + if (groupName) { + text = `${worldName} ${L.accessTypeName}(${groupName})`; + } else if (L.instanceId) { + text = `${worldName} ${L.accessTypeName}`; + } + } + return text; + }, + + extractFileId(s) { + var match = String(s).match(/file_[0-9A-Za-z-]+/); + return match ? match[0] : ''; + }, + + extractFileVersion(s) { + var match = /(?:\/file_[0-9A-Za-z-]+\/)([0-9]+)/gi.exec(s); + return match ? match[1] : ''; + }, + + extractVariantVersion(url) { + if (!url) { + return '0'; + } + try { + const params = new URLSearchParams(new URL(url).search); + const version = params.get('v'); + if (version) { + return version; + } + return '0'; + } catch { + return '0'; + } + }, + + buildTreeData(json) { + var node = []; + for (var key in json) { + if (key[0] === '$') { + continue; + } + var value = json[key]; + if (Array.isArray(value) && value.length === 0) { + node.push({ + key, + value: '[]' + }); + } else if ( + value === Object(value) && + Object.keys(value).length === 0 + ) { + node.push({ + key, + value: '{}' + }); + } else if (Array.isArray(value)) { + node.push({ + children: value.map((val, idx) => { + if (val === Object(val)) { + return { + children: this.buildTreeData(val), + key: idx + }; + } + return { + key: idx, + value: val + }; + }), + key + }); + } else if (value === Object(value)) { + node.push({ + children: this.buildTreeData(value), + key + }); + } else { + node.push({ + key, + value: String(value) + }); + } + } + node.sort(function (a, b) { + var A = String(a.key).toUpperCase(); + var B = String(b.key).toUpperCase(); + if (A < B) { + return -1; + } + if (A > B) { + return 1; + } + return 0; + }); + return node; + } + }; +} diff --git a/html/src/classes/vrcRegistry.js b/html/src/classes/vrcRegistry.js new file mode 100644 index 000000000..b49653359 --- /dev/null +++ b/html/src/classes/vrcRegistry.js @@ -0,0 +1,275 @@ +import configRepository from '../repository/config.js'; +import { baseClass, $app, API, $t, $utils } from './baseClass.js'; + +export default class extends baseClass { + constructor(_app, _API, _t) { + super(_app, _API, _t); + } + + init() {} + + _data = { + registryBackupDialog: { + visible: false + }, + + registryBackupTable: { + data: [], + tableProps: { + stripe: true, + size: 'mini', + defaultSort: { + prop: 'date', + order: 'descending' + } + }, + layout: 'table' + } + }; + + _methods = { + showRegistryBackupDialog() { + this.$nextTick(() => + $app.adjustDialogZ(this.$refs.registryBackupDialog.$el) + ); + var D = this.registryBackupDialog; + D.visible = true; + this.updateRegistryBackupDialog(); + }, + + async updateRegistryBackupDialog() { + var D = this.registryBackupDialog; + this.registryBackupTable.data = []; + if (!D.visible) { + return; + } + var backupsJson = await configRepository.getString( + 'VRCX_VRChatRegistryBackups' + ); + if (!backupsJson) { + backupsJson = JSON.stringify([]); + } + this.registryBackupTable.data = JSON.parse(backupsJson); + }, + + async promptVrcRegistryBackupName() { + var name = await this.$prompt( + 'Enter a name for the backup', + 'Backup Name', + { + confirmButtonText: 'Confirm', + cancelButtonText: 'Cancel', + inputPattern: /\S+/, + inputErrorMessage: 'Name is required', + inputValue: 'Backup' + } + ); + if (name.action === 'confirm') { + this.backupVrcRegistry(name.value); + } + }, + + async backupVrcRegistry(name) { + var regJson = await AppApi.GetVRChatRegistry(); + var newBackup = { + name, + date: new Date().toJSON(), + data: regJson + }; + var backupsJson = await configRepository.getString( + 'VRCX_VRChatRegistryBackups' + ); + if (!backupsJson) { + backupsJson = JSON.stringify([]); + } + var backups = JSON.parse(backupsJson); + backups.push(newBackup); + await configRepository.setString( + 'VRCX_VRChatRegistryBackups', + JSON.stringify(backups) + ); + await this.updateRegistryBackupDialog(); + }, + + async deleteVrcRegistryBackup(row) { + var backups = this.registryBackupTable.data; + $app.removeFromArray(backups, row); + await configRepository.setString( + 'VRCX_VRChatRegistryBackups', + JSON.stringify(backups) + ); + await this.updateRegistryBackupDialog(); + }, + + restoreVrcRegistryBackup(row) { + this.$confirm('Continue? Restore Backup', 'Confirm', { + confirmButtonText: 'Confirm', + cancelButtonText: 'Cancel', + type: 'warning', + callback: (action) => { + if (action !== 'confirm') { + return; + } + var data = JSON.stringify(row.data); + AppApi.SetVRChatRegistry(data) + .then(() => { + this.$message({ + message: 'VRC registry settings restored', + type: 'success' + }); + }) + .catch((e) => { + console.error(e); + this.$message({ + message: `Failed to restore VRC registry settings, check console for full error: ${e}`, + type: 'error' + }); + }); + } + }); + }, + + saveVrcRegistryBackupToFile(row) { + this.downloadAndSaveJson(row.name, row.data); + }, + + restoreVrcRegistryFromFile(json) { + try { + var data = JSON.parse(json); + if (!data || typeof data !== 'object') { + throw new Error('Invalid JSON'); + } + // quick check to make sure it's a valid registry backup + for (var key in data) { + var value = data[key]; + if ( + typeof value !== 'object' || + typeof value.type !== 'number' || + typeof value.data === 'undefined' + ) { + throw new Error('Invalid JSON'); + } + } + AppApi.SetVRChatRegistry(json) + .then(() => { + this.$message({ + message: 'VRC registry settings restored', + type: 'success' + }); + }) + .catch((e) => { + console.error(e); + this.$message({ + message: `Failed to restore VRC registry settings, check console for full error: ${e}`, + type: 'error' + }); + }); + } catch { + this.$message({ + message: 'Invalid JSON', + type: 'error' + }); + } + }, + + deleteVrcRegistry() { + this.$confirm('Continue? Delete VRC Registry Settings', 'Confirm', { + confirmButtonText: 'Confirm', + cancelButtonText: 'Cancel', + type: 'warning', + callback: (action) => { + if (action !== 'confirm') { + return; + } + AppApi.DeleteVRChatRegistryFolder().then(() => { + this.$message({ + message: 'VRC registry settings deleted', + type: 'success' + }); + }); + } + }); + }, + + clearVrcRegistryDialog() { + this.registryBackupTable.data = []; + }, + + async checkAutoBackupRestoreVrcRegistry() { + if (!this.vrcRegistryAutoBackup) { + return; + } + + // check for auto restore + var hasVRChatRegistryFolder = + await AppApi.HasVRChatRegistryFolder(); + if (!hasVRChatRegistryFolder) { + var lastBackupDate = await configRepository.getString( + 'VRCX_VRChatRegistryLastBackupDate' + ); + var lastRestoreCheck = await configRepository.getString( + 'VRCX_VRChatRegistryLastRestoreCheck' + ); + if ( + !lastBackupDate || + (lastRestoreCheck && + lastBackupDate && + lastRestoreCheck === lastBackupDate) + ) { + // only ask to restore once and when backup is present + return; + } + // popup message about auto restore + this.$alert( + $t('dialog.registry_backup.restore_prompt'), + $t('dialog.registry_backup.header') + ); + this.showRegistryBackupDialog(); + await AppApi.FocusWindow(); + await configRepository.setString( + 'VRCX_VRChatRegistryLastRestoreCheck', + lastBackupDate + ); + } else { + await this.autoBackupVrcRegistry(); + } + }, + + async autoBackupVrcRegistry() { + var date = new Date(); + var lastBackupDate = await configRepository.getString( + 'VRCX_VRChatRegistryLastBackupDate' + ); + if (lastBackupDate) { + var lastBackup = new Date(lastBackupDate); + var diff = date.getTime() - lastBackup.getTime(); + var diffDays = Math.floor(diff / (1000 * 60 * 60 * 24)); + if (diffDays < 7) { + return; + } + } + var backupsJson = await configRepository.getString( + 'VRCX_VRChatRegistryBackups' + ); + if (!backupsJson) { + backupsJson = JSON.stringify([]); + } + var backups = JSON.parse(backupsJson); + backups.forEach((backup) => { + if (backup.name === 'Auto Backup') { + // remove old auto backup + $app.removeFromArray(backups, backup); + } + }); + await configRepository.setString( + 'VRCX_VRChatRegistryBackups', + JSON.stringify(backups) + ); + this.backupVrcRegistry('Auto Backup'); + await configRepository.setString( + 'VRCX_VRChatRegistryLastBackupDate', + date.toJSON() + ); + } + }; +} diff --git a/html/src/classes/vrcxJsonStorage.js b/html/src/classes/vrcxJsonStorage.js new file mode 100644 index 000000000..7a8bc4f2c --- /dev/null +++ b/html/src/classes/vrcxJsonStorage.js @@ -0,0 +1,52 @@ +import * as workerTimers from 'worker-timers'; +/* eslint-disable no-unused-vars */ +let VRCXStorage = {}; +/* eslint-enable no-unused-vars */ + +export default class { + constructor(_VRCXStorage) { + VRCXStorage = _VRCXStorage; + this.init(); + } + + init() { + VRCXStorage.GetArray = async function (key) { + try { + var array = JSON.parse(await this.Get(key)); + if (Array.isArray(array)) { + return array; + } + } catch (err) { + console.error(err); + } + return []; + }; + + VRCXStorage.SetArray = function (key, value) { + this.Set(key, JSON.stringify(value)); + }; + + VRCXStorage.GetObject = async function (key) { + try { + var object = JSON.parse(await this.Get(key)); + if (object === Object(object)) { + return object; + } + } catch (err) { + console.error(err); + } + return {}; + }; + + VRCXStorage.SetObject = function (key, value) { + this.Set(key, JSON.stringify(value)); + }; + + workerTimers.setInterval( + () => { + VRCXStorage.Flush(); + }, + 5 * 60 * 1000 + ); + } +} diff --git a/html/src/classes/vrcxNotifications.js b/html/src/classes/vrcxNotifications.js new file mode 100644 index 000000000..cbfedd8c4 --- /dev/null +++ b/html/src/classes/vrcxNotifications.js @@ -0,0 +1,1611 @@ +import { baseClass, $app, API, $t, $utils } from './baseClass.js'; + +export default class extends baseClass { + constructor(_app, _API, _t) { + super(_app, _API, _t); + } + + _data = { + notyMap: [] + }; + + _methods = { + queueGameLogNoty(noty) { + // remove join/leave notifications when switching worlds + if ( + noty.type === 'OnPlayerJoined' || + noty.type === 'BlockedOnPlayerJoined' || + noty.type === 'MutedOnPlayerJoined' + ) { + var bias = this.lastLocation.date + 30 * 1000; // 30 secs + if (Date.parse(noty.created_at) <= bias) { + return; + } + } + if ( + noty.type === 'OnPlayerLeft' || + noty.type === 'BlockedOnPlayerLeft' || + noty.type === 'MutedOnPlayerLeft' + ) { + var bias = this.lastLocationDestinationTime + 5 * 1000; // 5 secs + if (Date.parse(noty.created_at) <= bias) { + return; + } + } + if ( + noty.type === 'Notification' || + noty.type === 'LocationDestination' + // skip unused entries + ) { + return; + } + if (noty.type === 'VideoPlay') { + if (!noty.videoName) { + // skip video without name + return; + } + noty.notyName = noty.videoName; + if (noty.displayName) { + // add requester's name to noty + noty.notyName = `${noty.videoName} (${noty.displayName})`; + } + } + if ( + noty.type !== 'VideoPlay' && + noty.displayName === API.currentUser.displayName + ) { + // remove current user + return; + } + noty.isFriend = false; + noty.isFavorite = false; + if (noty.userId) { + noty.isFriend = this.friends.has(noty.userId); + noty.isFavorite = this.localFavoriteFriends.has(noty.userId); + } else if (noty.displayName) { + for (var ref of API.cachedUsers.values()) { + if (ref.displayName === noty.displayName) { + noty.isFriend = this.friends.has(ref.id); + noty.isFavorite = this.localFavoriteFriends.has(ref.id); + break; + } + } + } + var notyFilter = this.sharedFeedFilters.noty; + if ( + notyFilter[noty.type] && + (notyFilter[noty.type] === 'On' || + notyFilter[noty.type] === 'Everyone' || + (notyFilter[noty.type] === 'Friends' && noty.isFriend) || + (notyFilter[noty.type] === 'VIP' && noty.isFavorite)) + ) { + this.playNoty(noty); + } + }, + + queueFeedNoty(noty) { + if (noty.type === 'Avatar') { + return; + } + // hide private worlds from feed + if ( + this.hidePrivateFromFeed && + noty.type === 'GPS' && + noty.location === 'private' + ) { + return; + } + noty.isFriend = this.friends.has(noty.userId); + noty.isFavorite = this.localFavoriteFriends.has(noty.userId); + var notyFilter = this.sharedFeedFilters.noty; + if ( + notyFilter[noty.type] && + (notyFilter[noty.type] === 'Everyone' || + (notyFilter[noty.type] === 'Friends' && noty.isFriend) || + (notyFilter[noty.type] === 'VIP' && noty.isFavorite)) + ) { + this.playNoty(noty); + } + }, + + queueNotificationNoty(noty) { + noty.isFriend = this.friends.has(noty.senderUserId); + noty.isFavorite = this.localFavoriteFriends.has(noty.senderUserId); + var notyFilter = this.sharedFeedFilters.noty; + if ( + notyFilter[noty.type] && + (notyFilter[noty.type] === 'On' || + notyFilter[noty.type] === 'Friends' || + (notyFilter[noty.type] === 'VIP' && noty.isFavorite)) + ) { + this.playNoty(noty); + } + }, + + queueFriendLogNoty(noty) { + if (noty.type === 'FriendRequest') { + return; + } + noty.isFriend = this.friends.has(noty.userId); + noty.isFavorite = this.localFavoriteFriends.has(noty.userId); + var notyFilter = this.sharedFeedFilters.noty; + if ( + notyFilter[noty.type] && + (notyFilter[noty.type] === 'On' || + notyFilter[noty.type] === 'Friends' || + (notyFilter[noty.type] === 'VIP' && noty.isFavorite)) + ) { + this.playNoty(noty); + } + }, + + queueModerationNoty(noty) { + noty.isFriend = false; + noty.isFavorite = false; + if (noty.userId) { + noty.isFriend = this.friends.has(noty.userId); + noty.isFavorite = this.localFavoriteFriends.has(noty.userId); + } + var notyFilter = this.sharedFeedFilters.noty; + if (notyFilter[noty.type] && notyFilter[noty.type] === 'On') { + this.playNoty(noty); + } + }, + + playNoty(noty) { + if ( + API.currentUser.status === 'busy' || + !this.friendLogInitStatus + ) { + return; + } + var displayName = ''; + if (noty.displayName) { + displayName = noty.displayName; + } else if (noty.senderUsername) { + displayName = noty.senderUsername; + } else if (noty.sourceDisplayName) { + displayName = noty.sourceDisplayName; + } + if (displayName) { + // don't play noty twice + var notyId = `${noty.type},${displayName}`; + if ( + this.notyMap[notyId] && + this.notyMap[notyId] >= noty.created_at + ) { + return; + } + this.notyMap[notyId] = noty.created_at; + } + var bias = new Date(Date.now() - 60000).toJSON(); + if (noty.created_at < bias) { + // don't play noty if it's over 1min old + return; + } + + var playNotificationTTS = false; + if ( + this.notificationTTS === 'Always' || + (this.notificationTTS === 'Inside VR' && + !this.isGameNoVR && + this.isGameRunning) || + (this.notificationTTS === 'Game Closed' && + !this.isGameRunning) || + (this.notificationTTS === 'Game Running' && this.isGameRunning) + ) { + playNotificationTTS = true; + } + var playDesktopToast = false; + if ( + this.desktopToast === 'Always' || + (this.desktopToast === 'Outside VR' && + !this.isSteamVRRunning) || + (this.desktopToast === 'Inside VR' && this.isSteamVRRunning) || + (this.desktopToast === 'Game Closed' && !this.isGameRunning) || + (this.desktopToast === 'Game Running' && this.isGameRunning) || + (this.desktopToast === 'Desktop Mode' && + this.isGameNoVR && + this.isGameRunning) || + (this.afkDesktopToast && + this.isHmdAfk && + this.isGameRunning && + !this.isGameNoVR) + ) { + // this if statement looks like it has seen better days + playDesktopToast = true; + } + var playXSNotification = this.xsNotifications; + var playOvrtHudNotifications = this.ovrtHudNotifications; + var playOvrtWristNotifications = this.ovrtWristNotifications; + var playOverlayNotification = false; + if ( + this.overlayNotifications && + !this.isGameNoVR && + this.isGameRunning + ) { + playOverlayNotification = true; + } + var message = ''; + if (noty.title) { + message = `${noty.title}, ${noty.message}`; + } else if (noty.message) { + message = noty.message; + } + var messageList = [ + 'inviteMessage', + 'requestMessage', + 'responseMessage' + ]; + for (var k = 0; k < messageList.length; k++) { + if ( + typeof noty.details !== 'undefined' && + typeof noty.details[messageList[k]] !== 'undefined' + ) { + message = `, ${noty.details[messageList[k]]}`; + } + } + if (playNotificationTTS) { + this.playNotyTTS(noty, message); + } + if ( + playDesktopToast || + playXSNotification || + playOvrtHudNotifications || + playOvrtWristNotifications || + playOverlayNotification + ) { + if (this.imageNotifications) { + this.notySaveImage(noty).then((image) => { + if (playXSNotification) { + this.displayXSNotification(noty, message, image); + } + if ( + playOvrtHudNotifications || + playOvrtWristNotifications + ) { + this.displayOvrtNotification( + playOvrtHudNotifications, + playOvrtWristNotifications, + noty, + message, + image + ); + } + if (playDesktopToast) { + this.displayDesktopToast(noty, message, image); + } + if (playOverlayNotification) { + this.displayOverlayNotification( + noty, + message, + image + ); + } + }); + } else { + if (playXSNotification) { + this.displayXSNotification(noty, message, ''); + } + if ( + playOvrtHudNotifications || + playOvrtWristNotifications + ) { + this.displayOvrtNotification( + playOvrtHudNotifications, + playOvrtWristNotifications, + noty, + message, + '' + ); + } + if (playDesktopToast) { + this.displayDesktopToast(noty, message, ''); + } + if (playOverlayNotification) { + this.displayOverlayNotification(noty, message, ''); + } + } + } + }, + + async notyGetImage(noty) { + var imageUrl = ''; + var userId = ''; + if (noty.userId) { + userId = noty.userId; + } else if (noty.senderUserId) { + userId = noty.senderUserId; + } else if (noty.sourceUserId) { + userId = noty.sourceUserId; + } else if (noty.displayName) { + for (var ref of API.cachedUsers.values()) { + if (ref.displayName === noty.displayName) { + userId = ref.id; + break; + } + } + } + if (noty.thumbnailImageUrl) { + imageUrl = noty.thumbnailImageUrl; + } else if (noty.details && noty.details.imageUrl) { + imageUrl = noty.details.imageUrl; + } else if (noty.imageUrl) { + imageUrl = noty.imageUrl; + } else if (userId && !userId.startsWith('grp_')) { + imageUrl = await API.getCachedUser({ + userId + }) + .catch((err) => { + console.error(err); + return ''; + }) + .then((args) => { + if (!args.json) { + return ''; + } + if ( + this.displayVRCPlusIconsAsAvatar && + args.json.userIcon + ) { + return args.json.userIcon; + } + if (args.json.profilePicOverride) { + return args.json.profilePicOverride; + } + return args.json.currentAvatarThumbnailImageUrl; + }); + } + return imageUrl; + }, + + async notySaveImage(noty) { + var imageUrl = await this.notyGetImage(noty); + var fileId = this.extractFileId(imageUrl); + var fileVersion = this.extractFileVersion(imageUrl); + var imageLocation = ''; + try { + if (fileId && fileVersion) { + imageLocation = await AppApi.GetImage( + imageUrl, + fileId, + fileVersion + ); + } else if (imageUrl) { + fileVersion = imageUrl.split('/').pop(); // 1416226261.thumbnail-500.png + fileId = fileVersion.split('.').shift(); // 1416226261 + imageLocation = await AppApi.GetImage( + imageUrl, + fileId, + fileVersion + ); + } + } catch (err) { + console.error(imageUrl, err); + } + return imageLocation; + }, + + displayOverlayNotification(noty, message, imageFile) { + var image = ''; + if (imageFile) { + image = `file:///${imageFile}`; + } + AppApi.ExecuteVrOverlayFunction( + 'playNoty', + JSON.stringify({ noty, message, image }) + ); + }, + + playNotyTTS(noty, message) { + switch (noty.type) { + case 'OnPlayerJoined': + this.speak(`${noty.displayName} has joined`); + break; + case 'OnPlayerLeft': + this.speak(`${noty.displayName} has left`); + break; + case 'OnPlayerJoining': + this.speak(`${noty.displayName} is joining`); + break; + case 'GPS': + this.speak( + `${noty.displayName} is in ${this.displayLocation( + noty.location, + noty.worldName, + noty.groupName + )}` + ); + break; + case 'Online': + var locationName = ''; + if (noty.worldName) { + locationName = ` to ${this.displayLocation( + noty.location, + noty.worldName, + noty.groupName + )}`; + } + this.speak( + `${noty.displayName} has logged in${locationName}` + ); + break; + case 'Offline': + this.speak(`${noty.displayName} has logged out`); + break; + case 'Status': + this.speak( + `${noty.displayName} status is now ${noty.status} ${noty.statusDescription}` + ); + break; + case 'invite': + this.speak( + `${ + noty.senderUsername + } has invited you to ${this.displayLocation( + noty.details.worldId, + noty.details.worldName, + noty.groupName + )}${message}` + ); + break; + case 'requestInvite': + this.speak( + `${noty.senderUsername} has requested an invite${message}` + ); + break; + case 'inviteResponse': + this.speak( + `${noty.senderUsername} has responded to your invite${message}` + ); + break; + case 'requestInviteResponse': + this.speak( + `${noty.senderUsername} has responded to your invite request${message}` + ); + break; + case 'friendRequest': + this.speak( + `${noty.senderUsername} has sent you a friend request` + ); + break; + case 'Friend': + this.speak(`${noty.displayName} is now your friend`); + break; + case 'Unfriend': + this.speak(`${noty.displayName} is no longer your friend`); + break; + case 'TrustLevel': + this.speak( + `${noty.displayName} trust level is now ${noty.trustLevel}` + ); + break; + case 'DisplayName': + this.speak( + `${noty.previousDisplayName} changed their name to ${noty.displayName}` + ); + break; + case 'boop': + this.speak(noty.message); + break; + case 'groupChange': + this.speak(`${noty.senderUsername} ${noty.message}`); + break; + case 'group.announcement': + this.speak(noty.message); + break; + case 'group.informative': + this.speak(noty.message); + break; + case 'group.invite': + this.speak(noty.message); + break; + case 'group.joinRequest': + this.speak(noty.message); + break; + case 'group.transfer': + this.speak(noty.message); + break; + case 'group.queueReady': + this.speak(noty.message); + break; + case 'instance.closed': + this.speak(noty.message); + break; + case 'PortalSpawn': + if (noty.displayName) { + this.speak( + `${ + noty.displayName + } has spawned a portal to ${this.displayLocation( + noty.instanceId, + noty.worldName, + noty.groupName + )}` + ); + } else { + this.speak('User has spawned a portal'); + } + break; + case 'AvatarChange': + this.speak( + `${noty.displayName} changed into avatar ${noty.name}` + ); + break; + case 'ChatBoxMessage': + this.speak(`${noty.displayName} said ${noty.text}`); + break; + case 'Event': + this.speak(noty.data); + break; + case 'External': + this.speak(noty.message); + break; + case 'VideoPlay': + this.speak(`Now playing: ${noty.notyName}`); + break; + case 'BlockedOnPlayerJoined': + this.speak(`Blocked user ${noty.displayName} has joined`); + break; + case 'BlockedOnPlayerLeft': + this.speak(`Blocked user ${noty.displayName} has left`); + break; + case 'MutedOnPlayerJoined': + this.speak(`Muted user ${noty.displayName} has joined`); + break; + case 'MutedOnPlayerLeft': + this.speak(`Muted user ${noty.displayName} has left`); + break; + case 'Blocked': + this.speak(`${noty.displayName} has blocked you`); + break; + case 'Unblocked': + this.speak(`${noty.displayName} has unblocked you`); + break; + case 'Muted': + this.speak(`${noty.displayName} has muted you`); + break; + case 'Unmuted': + this.speak(`${noty.displayName} has unmuted you`); + break; + } + }, + + displayXSNotification(noty, message, image) { + var timeout = Math.floor( + parseInt(this.notificationTimeout, 10) / 1000 + ); + switch (noty.type) { + case 'OnPlayerJoined': + AppApi.XSNotification( + 'VRCX', + `${noty.displayName} has joined`, + timeout, + image + ); + break; + case 'OnPlayerLeft': + AppApi.XSNotification( + 'VRCX', + `${noty.displayName} has left`, + timeout, + image + ); + break; + case 'OnPlayerJoining': + AppApi.XSNotification( + 'VRCX', + `${noty.displayName} is joining`, + timeout, + image + ); + break; + case 'GPS': + AppApi.XSNotification( + 'VRCX', + `${noty.displayName} is in ${this.displayLocation( + noty.location, + noty.worldName, + noty.groupName + )}`, + timeout, + image + ); + break; + case 'Online': + var locationName = ''; + if (noty.worldName) { + locationName = ` to ${this.displayLocation( + noty.location, + noty.worldName, + noty.groupName + )}`; + } + AppApi.XSNotification( + 'VRCX', + `${noty.displayName} has logged in${locationName}`, + timeout, + image + ); + break; + case 'Offline': + AppApi.XSNotification( + 'VRCX', + `${noty.displayName} has logged out`, + timeout, + image + ); + break; + case 'Status': + AppApi.XSNotification( + 'VRCX', + `${noty.displayName} status is now ${noty.status} ${noty.statusDescription}`, + timeout, + image + ); + break; + case 'invite': + AppApi.XSNotification( + 'VRCX', + `${ + noty.senderUsername + } has invited you to ${this.displayLocation( + noty.details.worldId, + noty.details.worldName + )}${message}`, + timeout, + image + ); + break; + case 'requestInvite': + AppApi.XSNotification( + 'VRCX', + `${noty.senderUsername} has requested an invite${message}`, + timeout, + image + ); + break; + case 'inviteResponse': + AppApi.XSNotification( + 'VRCX', + `${noty.senderUsername} has responded to your invite${message}`, + timeout, + image + ); + break; + case 'requestInviteResponse': + AppApi.XSNotification( + 'VRCX', + `${noty.senderUsername} has responded to your invite request${message}`, + timeout, + image + ); + break; + case 'friendRequest': + AppApi.XSNotification( + 'VRCX', + `${noty.senderUsername} has sent you a friend request`, + timeout, + image + ); + break; + case 'Friend': + AppApi.XSNotification( + 'VRCX', + `${noty.displayName} is now your friend`, + timeout, + image + ); + break; + case 'Unfriend': + AppApi.XSNotification( + 'VRCX', + `${noty.displayName} is no longer your friend`, + timeout, + image + ); + break; + case 'TrustLevel': + AppApi.XSNotification( + 'VRCX', + `${noty.displayName} trust level is now ${noty.trustLevel}`, + timeout, + image + ); + break; + case 'DisplayName': + AppApi.XSNotification( + 'VRCX', + `${noty.previousDisplayName} changed their name to ${noty.displayName}`, + timeout, + image + ); + break; + case 'boop': + AppApi.XSNotification('VRCX', noty.message, timeout, image); + break; + case 'groupChange': + AppApi.XSNotification( + 'VRCX', + `${noty.senderUsername}: ${noty.message}`, + timeout, + image + ); + break; + case 'group.announcement': + AppApi.XSNotification('VRCX', noty.message, timeout, image); + break; + case 'group.informative': + AppApi.XSNotification('VRCX', noty.message, timeout, image); + break; + case 'group.invite': + AppApi.XSNotification('VRCX', noty.message, timeout, image); + break; + case 'group.joinRequest': + AppApi.XSNotification('VRCX', noty.message, timeout, image); + break; + case 'group.transfer': + AppApi.XSNotification('VRCX', noty.message, timeout, image); + break; + case 'group.queueReady': + AppApi.XSNotification('VRCX', noty.message, timeout, image); + break; + case 'instance.closed': + AppApi.XSNotification('VRCX', noty.message, timeout, image); + break; + case 'PortalSpawn': + if (noty.displayName) { + AppApi.XSNotification( + 'VRCX', + `${ + noty.displayName + } has spawned a portal to ${this.displayLocation( + noty.instanceId, + noty.worldName, + noty.groupName + )}`, + timeout, + image + ); + } else { + AppApi.XSNotification( + 'VRCX', + 'User has spawned a portal', + timeout, + image + ); + } + break; + case 'AvatarChange': + AppApi.XSNotification( + 'VRCX', + `${noty.displayName} changed into avatar ${noty.name}`, + timeout, + image + ); + break; + case 'ChatBoxMessage': + AppApi.XSNotification( + 'VRCX', + `${noty.displayName} said ${noty.text}`, + timeout, + image + ); + break; + case 'Event': + AppApi.XSNotification('VRCX', noty.data, timeout, image); + break; + case 'External': + AppApi.XSNotification('VRCX', noty.message, timeout, image); + break; + case 'VideoPlay': + AppApi.XSNotification( + 'VRCX', + `Now playing: ${noty.notyName}`, + timeout, + image + ); + break; + case 'BlockedOnPlayerJoined': + AppApi.XSNotification( + 'VRCX', + `Blocked user ${noty.displayName} has joined`, + timeout, + image + ); + break; + case 'BlockedOnPlayerLeft': + AppApi.XSNotification( + 'VRCX', + `Blocked user ${noty.displayName} has left`, + timeout, + image + ); + break; + case 'MutedOnPlayerJoined': + AppApi.XSNotification( + 'VRCX', + `Muted user ${noty.displayName} has joined`, + timeout, + image + ); + break; + case 'MutedOnPlayerLeft': + AppApi.XSNotification( + 'VRCX', + `Muted user ${noty.displayName} has left`, + timeout, + image + ); + break; + case 'Blocked': + AppApi.XSNotification( + 'VRCX', + `${noty.displayName} has blocked you`, + timeout, + image + ); + break; + case 'Unblocked': + AppApi.XSNotification( + 'VRCX', + `${noty.displayName} has unblocked you`, + timeout, + image + ); + break; + case 'Muted': + AppApi.XSNotification( + 'VRCX', + `${noty.displayName} has muted you`, + timeout, + image + ); + break; + case 'Unmuted': + AppApi.XSNotification( + 'VRCX', + `${noty.displayName} has unmuted you`, + timeout, + image + ); + break; + } + }, + + displayOvrtNotification( + playOvrtHudNotifications, + playOvrtWristNotifications, + noty, + message, + image + ) { + var timeout = Math.floor( + parseInt(this.notificationTimeout, 10) / 1000 + ); + switch (noty.type) { + case 'OnPlayerJoined': + AppApi.OVRTNotification( + playOvrtHudNotifications, + playOvrtWristNotifications, + 'VRCX', + `${noty.displayName} has joined`, + timeout, + image + ); + break; + case 'OnPlayerLeft': + AppApi.OVRTNotification( + playOvrtHudNotifications, + playOvrtWristNotifications, + 'VRCX', + `${noty.displayName} has left`, + timeout, + image + ); + break; + case 'OnPlayerJoining': + AppApi.OVRTNotification( + playOvrtHudNotifications, + playOvrtWristNotifications, + 'VRCX', + `${noty.displayName} is joining`, + timeout, + image + ); + break; + case 'GPS': + AppApi.OVRTNotification( + playOvrtHudNotifications, + playOvrtWristNotifications, + 'VRCX', + `${noty.displayName} is in ${this.displayLocation( + noty.location, + noty.worldName, + noty.groupName + )}`, + timeout, + image + ); + break; + case 'Online': + var locationName = ''; + if (noty.worldName) { + locationName = ` to ${this.displayLocation( + noty.location, + noty.worldName, + noty.groupName + )}`; + } + AppApi.OVRTNotification( + playOvrtHudNotifications, + playOvrtWristNotifications, + 'VRCX', + `${noty.displayName} has logged in${locationName}`, + timeout, + image + ); + break; + case 'Offline': + AppApi.OVRTNotification( + playOvrtHudNotifications, + playOvrtWristNotifications, + 'VRCX', + `${noty.displayName} has logged out`, + timeout, + image + ); + break; + case 'Status': + AppApi.OVRTNotification( + playOvrtHudNotifications, + playOvrtWristNotifications, + 'VRCX', + `${noty.displayName} status is now ${noty.status} ${noty.statusDescription}`, + timeout, + image + ); + break; + case 'invite': + AppApi.OVRTNotification( + playOvrtHudNotifications, + playOvrtWristNotifications, + 'VRCX', + `${ + noty.senderUsername + } has invited you to ${this.displayLocation( + noty.details.worldId, + noty.details.worldName + )}${message}`, + timeout, + image + ); + break; + case 'requestInvite': + AppApi.OVRTNotification( + playOvrtHudNotifications, + playOvrtWristNotifications, + 'VRCX', + `${noty.senderUsername} has requested an invite${message}`, + timeout, + image + ); + break; + case 'inviteResponse': + AppApi.OVRTNotification( + playOvrtHudNotifications, + playOvrtWristNotifications, + 'VRCX', + `${noty.senderUsername} has responded to your invite${message}`, + timeout, + image + ); + break; + case 'requestInviteResponse': + AppApi.OVRTNotification( + playOvrtHudNotifications, + playOvrtWristNotifications, + 'VRCX', + `${noty.senderUsername} has responded to your invite request${message}`, + timeout, + image + ); + break; + case 'friendRequest': + AppApi.OVRTNotification( + playOvrtHudNotifications, + playOvrtWristNotifications, + 'VRCX', + `${noty.senderUsername} has sent you a friend request`, + timeout, + image + ); + break; + case 'Friend': + AppApi.OVRTNotification( + playOvrtHudNotifications, + playOvrtWristNotifications, + 'VRCX', + `${noty.displayName} is now your friend`, + timeout, + image + ); + break; + case 'Unfriend': + AppApi.OVRTNotification( + playOvrtHudNotifications, + playOvrtWristNotifications, + 'VRCX', + `${noty.displayName} is no longer your friend`, + timeout, + image + ); + break; + case 'TrustLevel': + AppApi.OVRTNotification( + playOvrtHudNotifications, + playOvrtWristNotifications, + 'VRCX', + `${noty.displayName} trust level is now ${noty.trustLevel}`, + timeout, + image + ); + break; + case 'DisplayName': + AppApi.OVRTNotification( + playOvrtHudNotifications, + playOvrtWristNotifications, + 'VRCX', + `${noty.previousDisplayName} changed their name to ${noty.displayName}`, + timeout, + image + ); + break; + case 'boop': + AppApi.OVRTNotification( + playOvrtHudNotifications, + playOvrtWristNotifications, + 'VRCX', + noty.message, + timeout, + image + ); + break; + case 'groupChange': + AppApi.OVRTNotification( + playOvrtHudNotifications, + playOvrtWristNotifications, + 'VRCX', + `${noty.senderUsername}: ${noty.message}`, + timeout, + image + ); + break; + case 'group.announcement': + AppApi.OVRTNotification( + playOvrtHudNotifications, + playOvrtWristNotifications, + 'VRCX', + noty.message, + timeout, + image + ); + break; + case 'group.informative': + AppApi.OVRTNotification( + playOvrtHudNotifications, + playOvrtWristNotifications, + 'VRCX', + noty.message, + timeout, + image + ); + break; + case 'group.invite': + AppApi.OVRTNotification( + playOvrtHudNotifications, + playOvrtWristNotifications, + 'VRCX', + noty.message, + timeout, + image + ); + break; + case 'group.joinRequest': + AppApi.OVRTNotification( + playOvrtHudNotifications, + playOvrtWristNotifications, + 'VRCX', + noty.message, + timeout, + image + ); + break; + case 'group.transfer': + AppApi.OVRTNotification( + playOvrtHudNotifications, + playOvrtWristNotifications, + 'VRCX', + noty.message, + timeout, + image + ); + break; + case 'group.queueReady': + AppApi.OVRTNotification( + playOvrtHudNotifications, + playOvrtWristNotifications, + 'VRCX', + noty.message, + timeout, + image + ); + break; + case 'instance.closed': + AppApi.OVRTNotification( + playOvrtHudNotifications, + playOvrtWristNotifications, + 'VRCX', + noty.message, + timeout, + image + ); + break; + case 'PortalSpawn': + if (noty.displayName) { + AppApi.OVRTNotification( + playOvrtHudNotifications, + playOvrtWristNotifications, + 'VRCX', + `${ + noty.displayName + } has spawned a portal to ${this.displayLocation( + noty.instanceId, + noty.worldName, + noty.groupName + )}`, + timeout, + image + ); + } else { + AppApi.OVRTNotification( + playOvrtHudNotifications, + playOvrtWristNotifications, + 'VRCX', + 'User has spawned a portal', + timeout, + image + ); + } + break; + case 'AvatarChange': + AppApi.OVRTNotification( + playOvrtHudNotifications, + playOvrtWristNotifications, + 'VRCX', + `${noty.displayName} changed into avatar ${noty.name}`, + timeout, + image + ); + break; + case 'ChatBoxMessage': + AppApi.OVRTNotification( + playOvrtHudNotifications, + playOvrtWristNotifications, + 'VRCX', + `${noty.displayName} said ${noty.text}`, + timeout, + image + ); + break; + case 'Event': + AppApi.OVRTNotification( + playOvrtHudNotifications, + playOvrtWristNotifications, + 'VRCX', + noty.data, + timeout, + image + ); + break; + case 'External': + AppApi.OVRTNotification( + playOvrtHudNotifications, + playOvrtWristNotifications, + 'VRCX', + noty.message, + timeout, + image + ); + break; + case 'VideoPlay': + AppApi.OVRTNotification( + playOvrtHudNotifications, + playOvrtWristNotifications, + 'VRCX', + `Now playing: ${noty.notyName}`, + timeout, + image + ); + break; + case 'BlockedOnPlayerJoined': + AppApi.OVRTNotification( + playOvrtHudNotifications, + playOvrtWristNotifications, + 'VRCX', + `Blocked user ${noty.displayName} has joined`, + timeout, + image + ); + break; + case 'BlockedOnPlayerLeft': + AppApi.OVRTNotification( + playOvrtHudNotifications, + playOvrtWristNotifications, + 'VRCX', + `Blocked user ${noty.displayName} has left`, + timeout, + image + ); + break; + case 'MutedOnPlayerJoined': + AppApi.OVRTNotification( + playOvrtHudNotifications, + playOvrtWristNotifications, + 'VRCX', + `Muted user ${noty.displayName} has joined`, + timeout, + image + ); + break; + case 'MutedOnPlayerLeft': + AppApi.OVRTNotification( + playOvrtHudNotifications, + playOvrtWristNotifications, + 'VRCX', + `Muted user ${noty.displayName} has left`, + timeout, + image + ); + break; + case 'Blocked': + AppApi.OVRTNotification( + playOvrtHudNotifications, + playOvrtWristNotifications, + 'VRCX', + `${noty.displayName} has blocked you`, + timeout, + image + ); + break; + case 'Unblocked': + AppApi.OVRTNotification( + playOvrtHudNotifications, + playOvrtWristNotifications, + 'VRCX', + `${noty.displayName} has unblocked you`, + timeout, + image + ); + break; + case 'Muted': + AppApi.OVRTNotification( + playOvrtHudNotifications, + playOvrtWristNotifications, + 'VRCX', + `${noty.displayName} has muted you`, + timeout, + image + ); + break; + case 'Unmuted': + AppApi.OVRTNotification( + playOvrtHudNotifications, + playOvrtWristNotifications, + 'VRCX', + `${noty.displayName} has unmuted you`, + timeout, + image + ); + break; + } + }, + + displayDesktopToast(noty, message, image) { + switch (noty.type) { + case 'OnPlayerJoined': + AppApi.DesktopNotification( + noty.displayName, + 'has joined', + image + ); + break; + case 'OnPlayerLeft': + AppApi.DesktopNotification( + noty.displayName, + 'has left', + image + ); + break; + case 'OnPlayerJoining': + AppApi.DesktopNotification( + noty.displayName, + 'is joining', + image + ); + break; + case 'GPS': + AppApi.DesktopNotification( + noty.displayName, + `is in ${this.displayLocation( + noty.location, + noty.worldName, + noty.groupName + )}`, + image + ); + break; + case 'Online': + var locationName = ''; + if (noty.worldName) { + locationName = ` to ${this.displayLocation( + noty.location, + noty.worldName, + noty.groupName + )}`; + } + AppApi.DesktopNotification( + noty.displayName, + `has logged in${locationName}`, + image + ); + break; + case 'Offline': + AppApi.DesktopNotification( + noty.displayName, + 'has logged out', + image + ); + break; + case 'Status': + AppApi.DesktopNotification( + noty.displayName, + `status is now ${noty.status} ${noty.statusDescription}`, + image + ); + break; + case 'invite': + AppApi.DesktopNotification( + noty.senderUsername, + `has invited you to ${this.displayLocation( + noty.details.worldId, + noty.details.worldName + )}${message}`, + image + ); + break; + case 'requestInvite': + AppApi.DesktopNotification( + noty.senderUsername, + `has requested an invite${message}`, + image + ); + break; + case 'inviteResponse': + AppApi.DesktopNotification( + noty.senderUsername, + `has responded to your invite${message}`, + image + ); + break; + case 'requestInviteResponse': + AppApi.DesktopNotification( + noty.senderUsername, + `has responded to your invite request${message}`, + image + ); + break; + case 'friendRequest': + AppApi.DesktopNotification( + noty.senderUsername, + 'has sent you a friend request', + image + ); + break; + case 'Friend': + AppApi.DesktopNotification( + noty.displayName, + 'is now your friend', + image + ); + break; + case 'Unfriend': + AppApi.DesktopNotification( + noty.displayName, + 'is no longer your friend', + image + ); + break; + case 'TrustLevel': + AppApi.DesktopNotification( + noty.displayName, + `trust level is now ${noty.trustLevel}`, + image + ); + break; + case 'DisplayName': + AppApi.DesktopNotification( + noty.previousDisplayName, + `changed their name to ${noty.displayName}`, + image + ); + break; + case 'boop': + AppApi.DesktopNotification( + noty.senderUsername, + noty.message, + image + ); + break; + case 'groupChange': + AppApi.DesktopNotification( + noty.senderUsername, + noty.message, + image + ); + break; + case 'group.announcement': + AppApi.DesktopNotification( + 'Group Announcement', + noty.message, + image + ); + break; + case 'group.informative': + AppApi.DesktopNotification( + 'Group Informative', + noty.message, + image + ); + break; + case 'group.invite': + AppApi.DesktopNotification( + 'Group Invite', + noty.message, + image + ); + break; + case 'group.joinRequest': + AppApi.DesktopNotification( + 'Group Join Request', + noty.message, + image + ); + break; + case 'group.transfer': + AppApi.DesktopNotification( + 'Group Transfer Request', + noty.message, + image + ); + break; + case 'group.queueReady': + AppApi.DesktopNotification( + 'Instance Queue Ready', + noty.message, + image + ); + break; + case 'instance.closed': + AppApi.DesktopNotification( + 'Instance Closed', + noty.message, + image + ); + break; + case 'PortalSpawn': + if (noty.displayName) { + AppApi.DesktopNotification( + noty.displayName, + `has spawned a portal to ${this.displayLocation( + noty.instanceId, + noty.worldName, + noty.groupName + )}`, + image + ); + } else { + AppApi.DesktopNotification( + '', + 'User has spawned a portal', + image + ); + } + break; + case 'AvatarChange': + AppApi.DesktopNotification( + noty.displayName, + `changed into avatar ${noty.name}`, + image + ); + break; + case 'ChatBoxMessage': + AppApi.DesktopNotification( + noty.displayName, + `said ${noty.text}`, + image + ); + break; + case 'Event': + AppApi.DesktopNotification('Event', noty.data, image); + break; + case 'External': + AppApi.DesktopNotification('External', noty.message, image); + break; + case 'VideoPlay': + AppApi.DesktopNotification( + 'Now playing', + noty.notyName, + image + ); + break; + case 'BlockedOnPlayerJoined': + AppApi.DesktopNotification( + noty.displayName, + 'blocked user has joined', + image + ); + break; + case 'BlockedOnPlayerLeft': + AppApi.DesktopNotification( + noty.displayName, + 'blocked user has left', + image + ); + break; + case 'MutedOnPlayerJoined': + AppApi.DesktopNotification( + noty.displayName, + 'muted user has joined', + image + ); + break; + case 'MutedOnPlayerLeft': + AppApi.DesktopNotification( + noty.displayName, + 'muted user has left', + image + ); + break; + case 'Blocked': + AppApi.DesktopNotification( + noty.displayName, + 'has blocked you', + image + ); + break; + case 'Unblocked': + AppApi.DesktopNotification( + noty.displayName, + 'has unblocked you', + image + ); + break; + case 'Muted': + AppApi.DesktopNotification( + noty.displayName, + 'has muted you', + image + ); + break; + case 'Unmuted': + AppApi.DesktopNotification( + noty.displayName, + 'has unmuted you', + image + ); + break; + } + } + }; +} diff --git a/html/src/classes/vrcxUpdater.js b/html/src/classes/vrcxUpdater.js new file mode 100644 index 000000000..8d8fb7098 --- /dev/null +++ b/html/src/classes/vrcxUpdater.js @@ -0,0 +1,264 @@ +import { baseClass, $app, API, $t, $utils } from './baseClass.js'; + +export default class extends baseClass { + constructor(_app, _API, _t) { + super(_app, _API, _t); + } + + _data = { + VRCXUpdateDialog: { + visible: false, + updatePending: false, + updatePendingIsLatest: false, + release: '', + releases: [], + json: {} + }, + branch: 'Stable', + autoUpdateVRCX: 'Auto Download', + checkingForVRCXUpdate: false, + pendingVRCXInstall: '', + pendingVRCXUpdate: false, + branches: { + Stable: { + name: 'Stable', + urlReleases: 'https://api0.vrcx.app/releases/stable', + urlLatest: 'https://api0.vrcx.app/releases/stable/latest' + }, + Nightly: { + name: 'Nightly', + urlReleases: 'https://api0.vrcx.app/releases/nightly', + urlLatest: 'https://api0.vrcx.app/releases/nightly/latest' + } + } + }; + + _methods = { + async showVRCXUpdateDialog() { + this.$nextTick(() => + $app.adjustDialogZ(this.$refs.VRCXUpdateDialog.$el) + ); + var D = this.VRCXUpdateDialog; + D.visible = true; + D.updatePendingIsLatest = false; + D.updatePending = await AppApi.CheckForUpdateExe(); + this.loadBranchVersions(); + }, + + downloadVRCXUpdate(updateSetupUrl, updateHashUrl, size, name, type) { + var ref = { + id: 'VRCXUpdate', + name + }; + this.downloadQueue.set('VRCXUpdate', { + ref, + type, + updateSetupUrl, + updateHashUrl, + size + }); + this.downloadQueueTable.data = Array.from( + this.downloadQueue.values() + ); + if (!this.downloadInProgress) { + this.downloadFileQueueUpdate(); + } + }, + + installVRCXUpdate() { + for (var release of this.VRCXUpdateDialog.releases) { + if (release.name === this.VRCXUpdateDialog.release) { + var downloadUrl = ''; + var hashUrl = ''; + var size = 0; + for (var asset of release.assets) { + if (asset.state !== 'uploaded') { + continue; + } + if ( + asset.content_type === 'application/x-msdownload' || + asset.content_type === 'application/x-msdos-program' + ) { + downloadUrl = asset.browser_download_url; + size = asset.size; + continue; + } + if ( + asset.name === 'SHA256SUMS.txt' && + asset.content_type === 'text/plain' + ) { + hashUrl = asset.browser_download_url; + continue; + } + } + if (!downloadUrl) { + return; + } + var name = release.name; + var type = 'Manual'; + this.downloadVRCXUpdate( + downloadUrl, + hashUrl, + size, + name, + type + ); + this.VRCXUpdateDialog.visible = false; + this.showDownloadDialog(); + } + } + }, + + async loadBranchVersions() { + var D = this.VRCXUpdateDialog; + var url = this.branches[this.branch].urlReleases; + this.checkingForVRCXUpdate = true; + try { + var response = await webApiService.execute({ + url, + method: 'GET' + }); + } finally { + this.checkingForVRCXUpdate = false; + } + var json = JSON.parse(response.data); + if (this.debugWebRequests) { + console.log(json, response); + } + var releases = []; + if (typeof json !== 'object' || json.message) { + $app.$message({ + message: `Failed to check for update, "${json.message}"`, + type: 'error' + }); + return; + } + for (var release of json) { + for (var asset of release.assets) { + if ( + (asset.content_type === 'application/x-msdownload' || + asset.content_type === + 'application/x-msdos-program') && + asset.state === 'uploaded' + ) { + releases.push(release); + } + } + } + D.releases = releases; + D.release = json[0].name; + this.VRCXUpdateDialog.updatePendingIsLatest = false; + if (D.release === this.pendingVRCXInstall) { + // update already downloaded and latest version + this.VRCXUpdateDialog.updatePendingIsLatest = true; + } + if ( + (await configRepository.getString('VRCX_branch')) !== + this.branch + ) { + await configRepository.setString('VRCX_branch', this.branch); + } + }, + + async checkForVRCXUpdate() { + if ( + !this.appVersion || + this.appVersion === 'VRCX Nightly Build' || + this.appVersion === 'VRCX Build' + ) { + return; + } + if (this.branch === 'Beta') { + // move Beta users to stable + this.branch = 'Stable'; + await configRepository.setString('VRCX_branch', this.branch); + } + var url = this.branches[this.branch].urlLatest; + this.checkingForVRCXUpdate = true; + try { + var response = await webApiService.execute({ + url, + method: 'GET' + }); + } finally { + this.checkingForVRCXUpdate = false; + } + this.pendingVRCXUpdate = false; + var json = JSON.parse(response.data); + if (this.debugWebRequests) { + console.log(json, response); + } + if (json === Object(json) && json.name && json.published_at) { + this.VRCXUpdateDialog.updateJson = json; + this.changeLogDialog.buildName = json.name; + this.changeLogDialog.changeLog = this.changeLogRemoveLinks( + json.body + ); + this.latestAppVersion = json.name; + var name = json.name; + this.VRCXUpdateDialog.updatePendingIsLatest = false; + if (name === this.pendingVRCXInstall) { + // update already downloaded + this.VRCXUpdateDialog.updatePendingIsLatest = true; + } else if (name > this.appVersion) { + var downloadUrl = ''; + var hashUrl = ''; + var size = 0; + for (var asset of json.assets) { + if (asset.state !== 'uploaded') { + continue; + } + if ( + asset.content_type === 'application/x-msdownload' || + asset.content_type === 'application/x-msdos-program' + ) { + downloadUrl = asset.browser_download_url; + size = asset.size; + continue; + } + if ( + asset.name === 'SHA256SUMS.txt' && + asset.content_type === 'text/plain' + ) { + hashUrl = asset.browser_download_url; + continue; + } + } + if (!downloadUrl) { + return; + } + this.pendingVRCXUpdate = true; + this.notifyMenu('settings'); + var type = 'Auto'; + if (!API.isLoggedIn) { + this.showVRCXUpdateDialog(); + } else if (this.autoUpdateVRCX === 'Notify') { + // this.showVRCXUpdateDialog(); + } else if (this.autoUpdateVRCX === 'Auto Download') { + this.downloadVRCXUpdate( + downloadUrl, + hashUrl, + size, + name, + type + ); + } + } + } + }, + + restartVRCX(isUpgrade) { + AppApi.RestartApplication(isUpgrade); + }, + + async saveAutoUpdateVRCX() { + if (this.autoUpdateVRCX === 'Off') { + this.pendingVRCXUpdate = false; + } + await configRepository.setString( + 'VRCX_autoUpdateVRCX', + this.autoUpdateVRCX + ); + } + }; +} diff --git a/html/src/classes/websocket.js b/html/src/classes/websocket.js new file mode 100644 index 000000000..4cdf50a31 --- /dev/null +++ b/html/src/classes/websocket.js @@ -0,0 +1,519 @@ +import * as workerTimers from 'worker-timers'; +import Noty from 'noty'; +import { baseClass, $app, API, $t } from './baseClass.js'; + +export default class extends baseClass { + constructor(_app, _API, _t) { + super(_app, _API, _t); + } + + init() { + API.webSocket = null; + API.lastWebSocketMessage = ''; + + API.$on('USER:CURRENT', function () { + if ($app.friendLogInitStatus && this.webSocket === null) { + this.getAuth(); + } + }); + + API.getAuth = function () { + return this.call('auth', { + method: 'GET' + }).then((json) => { + var args = { + json + }; + this.$emit('AUTH', args); + return args; + }); + }; + + API.$on('AUTH', function (args) { + if (args.json.ok) { + this.connectWebSocket(args.json.token); + } + }); + + API.connectWebSocket = function (token) { + if (this.webSocket !== null) { + return; + } + var socket = new WebSocket(`${API.websocketDomain}/?auth=${token}`); + socket.onopen = () => { + if ($app.debugWebSocket) { + console.log('WebSocket connected'); + } + }; + socket.onclose = () => { + if (this.webSocket === socket) { + this.webSocket = null; + } + try { + socket.close(); + } catch (err) {} + if ($app.debugWebSocket) { + console.log('WebSocket closed'); + } + workerTimers.setTimeout(() => { + if ( + this.isLoggedIn && + $app.friendLogInitStatus && + this.webSocket === null + ) { + this.getAuth(); + } + }, 5000); + }; + socket.onerror = () => { + if (this.errorNoty) { + this.errorNoty.close(); + } + this.errorNoty = new Noty({ + type: 'error', + text: 'WebSocket Error' + }).show(); + socket.onclose(); + }; + socket.onmessage = ({ data }) => { + try { + if (this.lastWebSocketMessage === data) { + // pls no spam + return; + } + this.lastWebSocketMessage = data; + var json = JSON.parse(data); + try { + json.content = JSON.parse(json.content); + } catch (err) {} + this.$emit('PIPELINE', { + json + }); + if ($app.debugWebSocket && json.content) { + var displayName = ''; + var user = this.cachedUsers.get(json.content.userId); + if (user) { + displayName = user.displayName; + } + console.log( + 'WebSocket', + json.type, + displayName, + json.content + ); + } + } catch (err) { + console.error(err); + } + }; + this.webSocket = socket; + }; + + API.$on('LOGOUT', function () { + this.closeWebSocket(); + }); + + API.closeWebSocket = function () { + var socket = this.webSocket; + if (socket === null) { + return; + } + this.webSocket = null; + try { + socket.close(); + } catch (err) {} + }; + + API.reconnectWebSocket = function () { + if (!this.isLoggedIn || !$app.friendLogInitStatus) { + return; + } + this.closeWebSocket(); + this.getAuth(); + }; + + API.$on('PIPELINE', function (args) { + var { type, content, err } = args.json; + if (typeof err !== 'undefined') { + console.error('PIPELINE: error', args); + if (this.errorNoty) { + this.errorNoty.close(); + } + this.errorNoty = new Noty({ + type: 'error', + text: $app.escapeTag(`WebSocket Error: ${err}`) + }).show(); + return; + } + if (typeof content === 'undefined') { + console.error('PIPELINE: missing content', args); + return; + } + if (typeof content.user !== 'undefined') { + // I forgot about this... + delete content.user.state; + } + switch (type) { + case 'notification': + this.$emit('NOTIFICATION', { + json: content, + params: { + notificationId: content.id + } + }); + this.$emit('PIPELINE:NOTIFICATION', { + json: content, + params: { + notificationId: content.id + } + }); + break; + + case 'notification-v2': + console.log('notification-v2', content); + this.$emit('NOTIFICATION:V2', { + json: content, + params: { + notificationId: content.id + } + }); + break; + + case 'notification-v2-delete': + console.log('notification-v2-delete', content); + for (var id of content.ids) { + this.$emit('NOTIFICATION:HIDE', { + params: { + notificationId: id + } + }); + this.$emit('NOTIFICATION:SEE', { + params: { + notificationId: id + } + }); + } + break; + + case 'notification-v2-update': + console.log('notification-v2-update', content); + this.$emit('NOTIFICATION:V2:UPDATE', { + json: content.updates, + params: { + notificationId: content.id + } + }); + break; + + case 'see-notification': + this.$emit('NOTIFICATION:SEE', { + params: { + notificationId: content + } + }); + break; + + case 'hide-notification': + this.$emit('NOTIFICATION:HIDE', { + params: { + notificationId: content + } + }); + this.$emit('NOTIFICATION:SEE', { + params: { + notificationId: content + } + }); + break; + + case 'response-notification': + this.$emit('NOTIFICATION:HIDE', { + params: { + notificationId: content.notificationId + } + }); + this.$emit('NOTIFICATION:SEE', { + params: { + notificationId: content.notificationId + } + }); + break; + + case 'friend-add': + this.$emit('USER', { + json: content.user, + params: { + userId: content.userId + } + }); + this.$emit('FRIEND:ADD', { + params: { + userId: content.userId + } + }); + break; + + case 'friend-delete': + this.$emit('FRIEND:DELETE', { + params: { + userId: content.userId + } + }); + break; + + case 'friend-online': + if (content?.user?.id) { + this.$emit('USER', { + json: { + location: content.location, + travelingToLocation: + content.travelingToLocation, + ...content.user + }, + params: { + userId: content.userId + } + }); + } else { + this.$emit('FRIEND:STATE', { + json: { + state: 'online' + }, + params: { + userId: content.userId + } + }); + } + break; + + case 'friend-active': + if (content?.user?.id) { + this.$emit('USER', { + json: content.user, + params: { + userId: content.userId + } + }); + } else { + this.$emit('FRIEND:STATE', { + json: { + state: 'active' + }, + params: { + userId: content.userId + } + }); + } + break; + + case 'friend-offline': + this.$emit('FRIEND:STATE', { + json: { + state: 'offline' + }, + params: { + userId: content.userId + } + }); + break; + + case 'friend-update': + this.$emit('USER', { + json: content.user, + params: { + userId: content.userId + } + }); + break; + + case 'friend-location': + if (!content?.user?.id) { + var ref = this.cachedUsers.get(content.userId); + if (typeof ref !== 'undefined') { + this.$emit('USER', { + json: { + ...ref, + location: content.location, + travelingToLocation: + content.travelingToLocation + }, + params: { + userId: content.userId + } + }); + } + break; + } + this.$emit('USER', { + json: { + location: content.location, + travelingToLocation: content.travelingToLocation, + ...content.user + // state: 'online' + }, + params: { + userId: content.userId + } + }); + break; + + case 'user-update': + this.$emit('USER:CURRENT', { + json: content.user, + params: { + userId: content.userId + } + }); + break; + + case 'user-location': + // update current user location + if (content.userId !== this.currentUser.id) { + console.error('user-location wrong userId', content); + break; + } + + // content.user: {} + // content.world: {} + + this.currentUser.presence.instance = content.instance; + this.currentUser.presence.world = content.worldId; + $app.setCurrentUserLocation(content.location); + break; + + case 'group-joined': + // var groupId = content.groupId; + // $app.onGroupJoined(groupId); + break; + + case 'group-left': + // var groupId = content.groupId; + // $app.onGroupLeft(groupId); + break; + + case 'group-role-updated': + var groupId = content.role.groupId; + API.getGroup({ groupId, includeRoles: true }); + console.log('group-role-updated', content); + + // content { + // role: { + // createdAt: string, + // description: string, + // groupId: string, + // id: string, + // isManagementRole: boolean, + // isSelfAssignable: boolean, + // name: string, + // order: number, + // permissions: string[], + // requiresPurchase: boolean, + // requiresTwoFactor: boolean + break; + + case 'group-member-updated': + var groupId = content.member.groupId; + if ( + $app.groupDialog.visible && + $app.groupDialog.id === groupId + ) { + $app.getGroupDialogGroup(groupId); + } + this.$emit('GROUP:MEMBER', { + json: content.member, + params: { + groupId + } + }); + console.log('group-member-updated', content); + + // content { + // groupId: string, + // id: string, + // isRepresenting: boolean, + // isSubscribedToAnnouncements: boolean, + // joinedAt: string, + // membershipStatus: string, + // roleIds: string[], + // userId: string, + // visibility: string + // } + break; + + case 'instance-queue-joined': + case 'instance-queue-position': + var instanceId = content.instanceLocation; + var position = content.position ?? 0; + var queueSize = content.queueSize ?? 0; + $app.instanceQueueUpdate(instanceId, position, queueSize); + break; + + case 'instance-queue-ready': + var instanceId = content.instanceLocation; + // var expiry = Date.parse(content.expiry); + $app.instanceQueueReady(instanceId); + break; + + case 'instance-queue-left': + var instanceId = content.instanceLocation; + $app.removeQueuedInstance(instanceId); + // $app.instanceQueueClear(); + break; + + case 'content-refresh': + var contentType = content.contentType; + console.log('content-refresh', content); + if (contentType === 'icon') { + if ($app.galleryDialogVisible) { + $app.refreshVRCPlusIconsTable(); + } + } else if (contentType === 'gallery') { + if ($app.galleryDialogVisible) { + $app.refreshGalleryTable(); + } + } else if (contentType === 'emoji') { + if ($app.galleryDialogVisible) { + $app.refreshEmojiTable(); + } + } else if (contentType === 'avatar') { + // hmm, utilizing this might be too spamy and cause UI to move around + } else if (contentType === 'world') { + // hmm + } else if (contentType === 'created') { + // on avatar upload + } else { + console.log('Unknown content-refresh', content); + } + break; + + case 'instance-closed': + // TODO: get worldName, groupName, hardClose + var noty = { + type: 'instance.closed', + location: content.instanceLocation, + message: 'Instance Closed', + created_at: new Date().toJSON() + }; + if ( + $app.notificationTable.filters[0].value.length === 0 || + $app.notificationTable.filters[0].value.includes( + noty.type + ) + ) { + $app.notifyMenu('notification'); + } + $app.queueNotificationNoty(noty); + $app.notificationTable.data.push(noty); + $app.updateSharedFeed(true); + break; + + default: + console.log('Unknown pipeline type', args.json); + } + }); + } + + _data = {}; + + _methods = {}; +} diff --git a/html/src/index.pug b/html/src/index.pug index 23fd61611..247fad65f 100644 --- a/html/src/index.pug +++ b/html/src/index.pug @@ -43,7 +43,7 @@ html +menuitem('profile', "{{ $t('nav_tooltip.profile') }}", 'el-icon-user') +menuitem('settings', "{{ $t('nav_tooltip.settings') }}", 'el-icon-s-tools') - //- ### Tabs ## + //- ### Tabs ### template(v-if="API.isLoggedIn") //- feed @@ -90,3751 +90,62 @@ html include ./mixins/tabs/settings.pug +settingsTab() - //- friends list sidebar - .x-aside-container(v-show="$refs.menu && $refs.menu.activeIndex !== 'friendsList'" id="aside") - div(style="display:flex;align-items:baseline") - el-select(v-model="quickSearch" clearable :placeholder="$t('side_panel.search_placeholder')" filterable remote :remote-method="quickSearchRemoteMethod" popper-class="x-quick-search" @change="quickSearchChange" @visible-change="quickSearchVisibleChange" style="flex:1;padding:10px") - el-option(v-for="item in quickSearchItems" :key="item.value" :value="item.value" :label="item.label") - .x-friend-item - template(v-if="item.ref") - .detail - span.name(v-text="item.ref.displayName" :style="{'color':item.ref.$userColour}") - span.extra(v-if="!item.ref.isFriend") - span.extra(v-else-if="item.ref.state === 'offline'") {{ $t('side_panel.search_result_active') }} - span.extra(v-else-if="item.ref.state === 'active'") {{ $t('side_panel.search_result_offline') }} - location.extra(v-else :location="item.ref.location" :traveling="item.ref.travelingToLocation" :link="false") - img.avatar(v-lazy="userImage(item.ref)") - span(v-else) {{ $t('side_panel.search_result_more') }} #[span(v-text="item.label" style="font-weight:bold")] - el-tooltip(placement="bottom" :content="$t('side_panel.direct_access_tooltip')" :disabled="hideTooltips") - el-button(type="default" @click="directAccessPaste" size="mini" icon="el-icon-discover" circle) - el-tooltip(placement="bottom" :content="$t('side_panel.refresh_tooltip')" :disabled="hideTooltips") - el-button(type="default" @click="refreshFriendsList" :loading="API.isRefreshFriendsLoading" size="mini" icon="el-icon-refresh" circle style="margin-right:10px") - el-tabs.zero-margin-tabs(stretch="true" style="height:calc(100% - 60px;margin-top:5px") - el-tab-pane - template(#label) - span {{ $t('side_panel.friends') }} - span(style="color:#909399;font-size:12px;margin-left:10px") ({{ onlineFriendCount }}/{{ friends.size }}) - .x-friend-list(style="padding:10px 5px") - .x-friend-group.x-link(@click="isFriendsGroupMe = !isFriendsGroupMe; saveFriendsGroupStates()" style="padding:0px 0px 5px") - i.el-icon-arrow-right(:class="{ rotate: isFriendsGroupMe }") - span(style="margin-left:5px") {{ $t('side_panel.me') }} - div(v-show="isFriendsGroupMe") - .x-friend-item(:key="API.currentUser.id" @click="showUserDialog(API.currentUser.id)") - .avatar(:class="userStatusClass(API.currentUser)") - img(v-lazy="userImage(API.currentUser)") - .detail - span.name(v-text="API.currentUser.displayName" :style="{'color':API.currentUser.$userColour}") - location.extra(v-if="isGameRunning && !gameLogDisabled" :location="lastLocation.location" :traveling="lastLocationDestination" :link="false") - location.extra(v-else-if="isRealInstance(API.currentUser.$locationTag) || isRealInstance(API.currentUser.$travelingToLocation)" :location="API.currentUser.$locationTag" :traveling="API.currentUser.$travelingToLocation" :link="false") - span.extra(v-else v-text="API.currentUser.statusDescription") - .x-friend-group.x-link(@click="isVIPFriends = !isVIPFriends; saveFriendsGroupStates()" v-show="vipFriends.length") - i.el-icon-arrow-right(:class="{ rotate: isVIPFriends }") - span(style="margin-left:5px") {{ $t('side_panel.favorite') }} ― {{ vipFriends.length }} - div(v-show="isVIPFriends") - .x-friend-item(v-for="friend in vipFriends" :key="friend.id" @click="showUserDialog(friend.id)") - template(v-if="friend.ref") - .avatar(:class="userStatusClass(friend.ref, friend.pendingOffline)") - img(v-lazy="userImage(friend.ref)") - .detail - span.name(v-if="!hideNicknames && friend.$nickName" :style="{'color':friend.ref.$userColour}") {{ friend.ref.displayName }} ({{ friend.$nickName }}) - span.name(v-else v-text="friend.ref.displayName" :style="{'color':friend.ref.$userColour}") - span.extra(v-if="friend.pendingOffline") #[i.el-icon-warning-outline] {{ $t('side_panel.pending_offline') }} - location.extra(v-else :location="friend.ref.location" :traveling="friend.ref.travelingToLocation" :link="false") - template(v-else) - span(v-text="friend.name || friend.id") - el-button(type="text" icon="el-icon-close" size="mini" @click.stop="confirmDeleteFriend(friend.id)" style="margin-left:5px") - .x-friend-group.x-link(@click="isOnlineFriends = !isOnlineFriends; saveFriendsGroupStates()" v-show="onlineFriends.length") - i.el-icon-arrow-right(:class="{ rotate: isOnlineFriends }") - span(style="margin-left:5px") {{ $t('side_panel.online') }} ― {{ onlineFriends.length }} - div(v-show="isOnlineFriends") - .x-friend-item(v-for="friend in onlineFriends" :key="friend.id" @click="showUserDialog(friend.id)") - template(v-if="friend.ref") - .avatar(:class="userStatusClass(friend.ref, friend.pendingOffline)") - img(v-lazy="userImage(friend.ref)") - .detail - span.name(v-if="!hideNicknames && friend.$nickName" :style="{'color':friend.ref.$userColour}") {{ friend.ref.displayName }} ({{ friend.$nickName }}) - span.name(v-else v-text="friend.ref.displayName" :style="{'color':friend.ref.$userColour}") - span.extra(v-if="friend.pendingOffline") #[i.el-icon-warning-outline] {{ $t('side_panel.pending_offline') }} - location.extra(v-else :location="friend.ref.location" :traveling="friend.ref.travelingToLocation" :link="false") - template(v-else) - span(v-text="friend.name || friend.id") - el-button(type="text" icon="el-icon-close" size="mini" @click.stop="confirmDeleteFriend(friend.id)" style="margin-left:5px") - .x-friend-group.x-link(@click="isActiveFriends = !isActiveFriends; saveFriendsGroupStates()" v-show="activeFriends.length") - i.el-icon-arrow-right(:class="{ rotate: isActiveFriends }") - span(style="margin-left:5px") {{ $t('side_panel.active') }} ― {{ activeFriends.length }} - div(v-show="isActiveFriends") - .x-friend-item(v-for="friend in activeFriends" :key="friend.id" @click="showUserDialog(friend.id)") - template(v-if="friend.ref") - .avatar - img(v-lazy="userImage(friend.ref)") - .detail - span.name(v-if="!hideNicknames && friend.$nickName" :style="{'color':friend.ref.$userColour}") {{ friend.ref.displayName }} ({{ friend.$nickName }}) - span.name(v-else v-text="friend.ref.displayName" :style="{'color':friend.ref.$userColour}") - span.extra(v-text="friend.ref.statusDescription" :link="false") - template(v-else) - span(v-text="friend.name || friend.id") - el-button(type="text" icon="el-icon-close" size="mini" @click.stop="confirmDeleteFriend(friend.id)" style="margin-left:5px") - .x-friend-group.x-link(@click="isOfflineFriends = !isOfflineFriends; saveFriendsGroupStates()" v-show="offlineFriends.length") - i.el-icon-arrow-right(:class="{ rotate: isOfflineFriends }") - span(style="margin-left:5px") {{ $t('side_panel.offline') }} ― {{ offlineFriends.length }} - div(v-show="isOfflineFriends") - .x-friend-item(v-for="friend in offlineFriends" :key="friend.id" @click="showUserDialog(friend.id)") - template(v-if="friend.ref") - .avatar - img(v-lazy="userImage(friend.ref)") - .detail - span.name(v-if="!hideNicknames && friend.$nickName" :style="{'color':friend.ref.$userColour}") {{ friend.ref.displayName }} ({{ friend.$nickName }}) - span.name(v-else v-text="friend.ref.displayName" :style="{'color':friend.ref.$userColour}") - span.extra(v-text="friend.ref.statusDescription") - template(v-else) - span(v-text="friend.name || friend.id") - el-button(type="text" icon="el-icon-close" size="mini" @click.stop="confirmDeleteFriend(friend.id)" style="margin-left:5px") - el-tab-pane - template(#label) - span {{ $t('side_panel.groups') }} - span(style="color:#909399;font-size:12px;margin-left:10px") ({{ groupInstances.length }}) - .x-friend-list(style="padding:10px 5px") - .x-friend-item(v-for="ref in groupInstances" :key="ref.instance.id" @click="showGroupDialog(ref.instance.ownerId)") - .avatar - img(v-lazy="ref.group.iconUrl") - .detail - span.name - span(v-text="ref.group.name") - span(style="font-weight:normal;margin-left:5px") ({{ ref.instance.userCount }}/{{ ref.instance.capacity }}) - location.extra(:location="ref.instance.location" :link="false") - + include ./mixins/friendsListSidebar.pug + +friendsListSidebar() //- ## Dialogs ## -\\ + include ./mixins/dialogs/userDialog.pug + +userDialog() - //- dialog: user - el-dialog.x-dialog.x-user-dialog(:before-close="beforeDialogClose" @mousedown.native="dialogMouseDown" @mouseup.native="dialogMouseUp" ref="userDialog" :visible.sync="userDialog.visible" :show-close="false" width="770px") - div(v-loading="userDialog.loading") - div(style="display:flex") - el-popover(v-if="userDialog.ref.profilePicOverrideThumbnail || userDialog.ref.profilePicOverride" placement="right" width="500px" trigger="click") - template(slot="reference") - img.x-link(v-if="userDialog.ref.profilePicOverrideThumbnail" v-lazy="userDialog.ref.profilePicOverrideThumbnail" style="flex:none;height:120px;width:213.33px;border-radius:12px;object-fit:cover") - img.x-link(v-else v-lazy="userDialog.ref.profilePicOverride" style="flex:none;height:120px;width:213.33px;border-radius:12px;object-fit:cover") - img.x-link(v-lazy="userDialog.ref.profilePicOverride" style="height:400px" @click="showFullscreenImageDialog(userDialog.ref.profilePicOverride)") - el-popover(v-else placement="right" width="500px" trigger="click") - img.x-link(slot="reference" v-lazy="userDialog.ref.currentAvatarThumbnailImageUrl" style="flex:none;height:120px;width:160px;border-radius:12px;object-fit:cover") - img.x-link(v-lazy="userDialog.ref.currentAvatarImageUrl" style="height:500px" @click="showFullscreenImageDialog(userDialog.ref.currentAvatarImageUrl)") - div(style="flex:1;display:flex;align-items:center;margin-left:15px") - div(style="flex:1") - div - el-tooltip(v-if="userDialog.ref.status" placement="top") - template(#content) - span(v-if="userDialog.ref.state === 'active'") {{ $t('dialog.user.status.active') }} - span(v-else-if="userDialog.ref.state === 'offline'") {{ $t('dialog.user.status.offline') }} - span(v-else-if="userDialog.ref.status === 'active'") {{ $t('dialog.user.status.online') }} - span(v-else-if="userDialog.ref.status === 'join me'") {{ $t('dialog.user.status.join_me') }} - span(v-else-if="userDialog.ref.status === 'ask me'") {{ $t('dialog.user.status.ask_me') }} - span(v-else-if="userDialog.ref.status === 'busy'") {{ $t('dialog.user.status.busy') }} - span(v-else) {{ $t('dialog.user.status.offline') }} - i.x-user-status(:class="userStatusClass(userDialog.ref)") - template(v-if="userDialog.previousDisplayNames.length > 0") - el-tooltip(placement="bottom") - template(#content) - span {{ $t('dialog.user.previous_display_names') }} - div(v-for="displayName in userDialog.previousDisplayNames" placement="top") - span(v-text="displayName") - i.el-icon-caret-bottom - el-popover(placement="top" trigger="click") - span.dialog-title(slot="reference" v-text="userDialog.ref.displayName" style="margin-left:5px;margin-right:5px;cursor:pointer") - span(style="display:block;text-align:center;font-family:monospace") {{ userDialog.ref.displayName | textToHex }} - el-tooltip(v-if="userDialog.ref.pronouns" placement="top" :content="$t('dialog.user.pronouns')" :disabled="hideTooltips") - span.x-grey(v-text="userDialog.ref.pronouns" style="margin-right:5px;font-family:monospace;font-size:12px") - el-tooltip(v-for="item in userDialog.ref.$languages" :key="item.key" placement="top") - template(#content) - span {{ item.value }} ({{ item.key }}) - span.flags(:class="languageClass(item.key)" style="display:inline-block;margin-right:5px") - template(v-if="userDialog.ref.id === API.currentUser.id") - br - el-popover(placement="top" trigger="click") - span.x-grey(slot="reference" v-text="API.currentUser.username" style="margin-right:10px;font-family:monospace;font-size:12px;cursor:pointer") - span(style="display:block;text-align:center;font-family:monospace") {{ API.currentUser.username | textToHex }} - div - el-tag.name(type="info" effect="plain" size="mini" :class="userDialog.ref.$trustClass" v-text="userDialog.ref.$trustLevel" style="margin-right:5px;margin-top:5px") - el-tag.x-tag-friend(v-if="userDialog.isFriend && userDialog.friend" type="info" effect="plain" size="mini" style="margin-right:5px;margin-top:5px") {{ $t('dialog.user.tags.friend_no', { number: userDialog.friend.no }) }} - el-tag.x-tag-troll(v-if="userDialog.ref.$isTroll" type="info" effect="plain" size="mini" style="margin-right:5px;margin-top:5px") Nuisance - el-tag.x-tag-troll(v-if="userDialog.ref.$isProbableTroll" type="info" effect="plain" size="mini" style="margin-right:5px;margin-top:5px") Almost Nuisance - el-tag.x-tag-vip(v-if="userDialog.ref.$isModerator" type="info" effect="plain" size="mini" style="margin-right:5px;margin-top:5px") {{ $t('dialog.user.tags.vrchat_team') }} - el-tag.x-tag-vrcplus(v-if="userDialog.ref.$isVRCPlus" type="info" effect="plain" size="mini" style="margin-right:5px;margin-top:5px") VRC+ - el-tag.x-tag-platform-pc(v-if="userDialog.ref.last_platform === 'standalonewindows'" type="info" effect="plain" size="mini" style="margin-right:5px;margin-top:5px") PC - el-tag.x-tag-platform-quest(v-else-if="userDialog.ref.last_platform === 'android'" type="info" effect="plain" size="mini" style="margin-right:5px;margin-top:5px") Android - el-tag.x-tag-platform-ios(v-else-if="userDialog.ref.last_platform === 'ios'" type="info" effect="plain" size="mini" style="margin-right:5px;margin-top:5px") iOS - el-tag.x-tag-platform-other(v-else-if="userDialog.ref.last_platform" type="info" effect="plain" size="mini" style="margin-right:5px;margin-top:5px") {{ userDialog.ref.last_platform }} - el-tag.name(v-if="userDialog.ref.$customTag" type="info" effect="plain" size="mini" v-text="userDialog.ref.$customTag" :style="{'color':userDialog.ref.$customTagColour, 'border-color':userDialog.ref.$customTagColour}" style="margin-right:5px;margin-top:5px") - div(style="margin-top:5px") - span(v-text="userDialog.ref.statusDescription" style="font-size:12px") - div(v-if="userDialog.ref.userIcon" style="flex:none;margin-right:10px") - el-popover(placement="right" width="500px" trigger="click") - img.x-link(slot="reference" v-lazy="userDialog.ref.userIcon" style="flex:none;width:120px;height:120px;border-radius:12px;object-fit:cover") - img.x-link(v-lazy="userDialog.ref.userIcon" style="height:500px" @click="showFullscreenImageDialog(userDialog.ref.userIcon)") - div(style="flex:none") - template(v-if="(API.currentUser.id !== userDialog.ref.id && userDialog.isFriend) || userDialog.isFavorite") - el-tooltip(v-if="userDialog.isFavorite" placement="top" :content="$t('dialog.user.actions.unfavorite_tooltip')" :disabled="hideTooltips") - el-button(@click="userDialogCommand('Add Favorite')" type="warning" icon="el-icon-star-on" circle) - el-tooltip(v-else placement="top" :content="$t('dialog.user.actions.favorite_tooltip')" :disabled="hideTooltips") - el-button(type="default" @click="userDialogCommand('Add Favorite')" icon="el-icon-star-off" circle) - el-dropdown(trigger="click" @command="userDialogCommand" size="small") - el-button(:type="(userDialog.incomingRequest || userDialog.outgoingRequest) ? 'success' : (userDialog.isBlock || userDialog.isMute) ? 'danger' : 'default'" icon="el-icon-more" circle style="margin-left:5px") - el-dropdown-menu(#default="dropdown") - el-dropdown-item(icon="el-icon-refresh" command="Refresh") {{ $t('dialog.user.actions.refresh') }} - template(v-if="userDialog.ref.id === API.currentUser.id") - el-dropdown-item(icon="el-icon-picture-outline" command="Manage Gallery" divided) {{ $t('dialog.user.actions.manage_gallery_icon') }} - el-dropdown-item(icon="el-icon-s-custom" command="Show Avatar Author") {{ $t('dialog.user.actions.show_avatar_author') }} - el-dropdown-item(icon="el-icon-s-custom" command="Show Fallback Avatar Details") {{ $t('dialog.user.actions.show_fallback_avatar') }} - el-dropdown-item(icon="el-icon-edit" command="Edit Social Status" divided) {{ $t('dialog.user.actions.edit_status') }} - el-dropdown-item(icon="el-icon-edit" command="Edit Language") {{ $t('dialog.user.actions.edit_language') }} - el-dropdown-item(icon="el-icon-edit" command="Edit Bio") {{ $t('dialog.user.actions.edit_bio') }} - el-dropdown-item(icon="el-icon-edit" command="Edit Pronouns") {{ $t('dialog.user.actions.edit_pronouns') }} - el-dropdown-item(icon="el-icon-switch-button" command="Logout" divided) {{ $t('dialog.user.actions.logout') }} - template(v-else) - template(v-if="userDialog.isFriend") - el-dropdown-item(icon="el-icon-postcard" command="Request Invite" divided) {{ $t('dialog.user.actions.request_invite') }} - el-dropdown-item(icon="el-icon-postcard" command="Request Invite Message") {{ $t('dialog.user.actions.request_invite_with_message') }} - template(v-if="lastLocation.location && isGameRunning && checkCanInvite(lastLocation.location)") - el-dropdown-item(icon="el-icon-message" command="Invite") {{ $t('dialog.user.actions.invite') }} - el-dropdown-item(icon="el-icon-message" command="Invite Message") {{ $t('dialog.user.actions.invite_with_message') }} - template(v-else-if="userDialog.incomingRequest") - el-dropdown-item(icon="el-icon-check" command="Accept Friend Request") {{ $t('dialog.user.actions.accept_friend_request') }} - el-dropdown-item(icon="el-icon-close" command="Decline Friend Request") {{ $t('dialog.user.actions.decline_friend_request') }} - el-dropdown-item(v-else-if="userDialog.outgoingRequest" icon="el-icon-close" command="Cancel Friend Request") {{ $t('dialog.user.actions.cancel_friend_request') }} - el-dropdown-item(v-else icon="el-icon-plus" command="Send Friend Request") {{ $t('dialog.user.actions.send_friend_request') }} - el-dropdown-item(icon="el-icon-message" command="Invite To Group") {{ $t('dialog.user.actions.invite_to_group') }} - //- el-dropdown-item(icon="el-icon-thumb" command="Send Boop" :disabled="!API.currentUser.isBoopingEnabled") {{ $t('dialog.user.actions.send_boop') }} - el-dropdown-item(icon="el-icon-s-custom" command="Show Avatar Author" divided) {{ $t('dialog.user.actions.show_avatar_author') }} - el-dropdown-item(icon="el-icon-s-custom" command="Show Fallback Avatar Details") {{ $t('dialog.user.actions.show_fallback_avatar') }} - el-dropdown-item(icon="el-icon-tickets" command="Previous Instances") {{ $t('dialog.user.actions.show_previous_instances') }} - el-dropdown-item(v-if="userDialog.ref.currentAvatarImageUrl" icon="el-icon-picture-outline" command="Previous Images") {{ $t('dialog.user.actions.show_previous_images') }} - el-dropdown-item(v-if="userDialog.isBlock" icon="el-icon-circle-check" command="Unblock" divided style="color:#F56C6C") {{ $t('dialog.user.actions.moderation_unblock') }} - el-dropdown-item(v-else icon="el-icon-circle-close" command="Block" divided :disabled="userDialog.ref.$isModerator") {{ $t('dialog.user.actions.moderation_block') }} - el-dropdown-item(v-if="userDialog.isMute" icon="el-icon-microphone" command="Unmute" style="color:#F56C6C") {{ $t('dialog.user.actions.moderation_unmute') }} - el-dropdown-item(v-else icon="el-icon-turn-off-microphone" command="Mute" :disabled="userDialog.ref.$isModerator") {{ $t('dialog.user.actions.moderation_mute') }} - el-dropdown-item(v-if="userDialog.isMuteChat" icon="el-icon-chat-line-round" command="Unmute Chatbox" style="color:#F56C6C") {{ $t('dialog.user.actions.moderation_enable_chatbox') }} - el-dropdown-item(v-else icon="el-icon-chat-dot-round" command="Mute Chatbox") {{ $t('dialog.user.actions.moderation_disable_chatbox') }} - el-dropdown-item(icon="el-icon-user-solid" command="Show Avatar") - i.el-icon-check.el-icon--left(v-if="userDialog.isShowAvatar") - span {{ $t('dialog.user.actions.moderation_show_avatar') }} - el-dropdown-item(icon="el-icon-user" command="Hide Avatar") - i.el-icon-check.el-icon--left(v-if="userDialog.isHideAvatar") - span {{ $t('dialog.user.actions.moderation_hide_avatar') }} - el-dropdown-item(v-if="userDialog.isInteractOff" icon="el-icon-thumb" command="Enable Avatar Interaction" style="color:#F56C6C") {{ $t('dialog.user.actions.moderation_enable_avatar_interaction') }} - el-dropdown-item(v-else icon="el-icon-circle-close" command="Disable Avatar Interaction") {{ $t('dialog.user.actions.moderation_disable_avatar_interaction') }} - el-dropdown-item(icon="el-icon-s-flag" command="Report Hacking" :disabled="userDialog.ref.$isModerator") {{ $t('dialog.user.actions.report_hacking') }} - template(v-if="userDialog.isFriend") - el-dropdown-item(icon="el-icon-delete" command="Unfriend" divided style="color:#F56C6C") {{ $t('dialog.user.actions.unfriend') }} - el-tabs(ref="userDialogTabs" @tab-click="userDialogTabClick") - el-tab-pane(:label="$t('dialog.user.info.header')") - template(v-if="isFriendOnline(userDialog.friend) || API.currentUser.id === userDialog.id") - div(v-if="userDialog.ref.location" style="display:flex;flex-direction:column;margin-bottom:10px;padding-bottom:10px;border-bottom:1px solid #e4e7ed14") - div(style="flex:none") - template(v-if="isRealInstance(userDialog.$location.tag)") - el-tooltip(placement="top" :content="$t('dialog.user.info.launch_invite_tooltip')" :disabled="hideTooltips") - launch(:location="userDialog.$location.tag") - el-tooltip(placement="top" :content="$t('dialog.user.info.self_invite_tooltip')" :disabled="hideTooltips") - invite-yourself(:location="userDialog.$location.tag" :shortname="userDialog.$location.shortName" style="margin-left:5px") - el-tooltip(placement="top" :content="$t('dialog.user.info.refresh_instance_info')" :disabled="hideTooltips") - el-button(@click="refreshInstancePlayerCount(userDialog.$location.tag)" size="mini" icon="el-icon-refresh" style="margin-left:5px" circle) - last-join(:location="userDialog.$location.tag" :currentlocation="lastLocation.location") - instance-info(:location="userDialog.$location.tag" :instance="userDialog.instance.ref" :friendcount="userDialog.instance.friendCount" :updateelement="updateInstanceInfo") - location(:location="userDialog.ref.location" :traveling="userDialog.ref.travelingToLocation" style="display:block;margin-top:5px") - .x-friend-list(style="flex:1;margin-top:10px;max-height:150px") - .x-friend-item(v-if="userDialog.$location.userId" @click="showUserDialog(userDialog.$location.userId)" class="x-friend-item-border") - template(v-if="userDialog.$location.user") - .avatar(:class="userStatusClass(userDialog.$location.user)") - img(v-lazy="userImage(userDialog.$location.user)") - .detail - span.name(v-text="userDialog.$location.user.displayName" :style="{'color':userDialog.$location.user.$userColour}") - span.extra {{ $t('dialog.user.info.instance_creator') }} - span(v-else v-text="userDialog.$location.userId") - .x-friend-item(v-for="user in userDialog.users" :key="user.id" @click="showUserDialog(user.id)" class="x-friend-item-border") - .avatar(:class="userStatusClass(user)") - img(v-lazy="userImage(user)") - .detail - span.name(v-text="user.displayName" :style="{'color':user.$userColour}") - span.extra(v-if="user.location === 'traveling'") - i.el-icon-loading(style="margin-right:5px") - timer(:epoch="user.$travelingToTime") - span.extra(v-else) - timer(:epoch="user.$location_at") - .x-friend-list(style="max-height:none") - .x-friend-item(v-if="!hideUserNotes" style="width:100%;cursor:default") - .detail - span.name {{ $t('dialog.user.info.note') }} - el-input(v-model="userDialog.note" type="textarea" maxlength="256" show-word-limit :rows="2" :autosize="{ minRows: 1, maxRows: 20 }" @change="checkNote(userDialog.ref, userDialog.note)" @input="cleanNote(userDialog.note)" :placeholder="$t('dialog.user.info.note_placeholder')" size="mini" resize="none") - div(style="float:right") - i.el-icon-loading(v-if="userDialog.noteSaving" style="margin-left:5px") - i.el-icon-more-outline(v-else-if="userDialog.note !== userDialog.ref.note" style="margin-left:5px") - el-button(v-if="userDialog.note" type="text" icon="el-icon-delete" size="mini" @click="deleteNote(userDialog.id)" style="margin-left:5px") - .x-friend-item(v-if="!hideUserMemos" style="width:100%;cursor:default") - .detail - span.name {{ $t('dialog.user.info.memo') }} - el-input.extra(v-model="userDialog.memo" type="textarea" :rows="2" :autosize="{ minRows: 1, maxRows: 20 }" :placeholder="$t('dialog.user.info.memo_placeholder')" size="mini" resize="none") - .x-friend-item(style="width:100%;cursor:default") - .detail - span.name(v-if="userDialog.id !== API.currentUser.id && userDialog.ref.profilePicOverride && userDialog.ref.currentAvatarImageUrl") {{ $t('dialog.user.info.avatar_info_last_seen') }} - span.name(v-else) {{ $t('dialog.user.info.avatar_info') }} - .extra - avatar-info(:imageurl="userDialog.ref.currentAvatarImageUrl" :userid="userDialog.id" :avatartags="userDialog.ref.currentAvatarTags") - .x-friend-item(style="width:100%;cursor:default") - .detail - span.name {{ $t('dialog.user.info.represented_group') }} - .extra(v-if="userDialog.representedGroup?.isRepresenting") - div(style="display:inline-block;flex:none;margin-right:5px") - el-popover(placement="right" width="500px" trigger="click") - img.x-link(slot="reference" v-lazy="userDialog.representedGroup.iconUrl" style="flex:none;width:60px;height:60px;border-radius:4px;object-fit:cover") - img.x-link(v-lazy="userDialog.representedGroup.iconUrl" style="height:500px" @click="showFullscreenImageDialog(userDialog.representedGroup.iconUrl)") - span(style="vertical-align:top;cursor:pointer" @click="showGroupDialog(userDialog.representedGroup.groupId)") - span(v-if="userDialog.representedGroup.ownerId === userDialog.id" style="margin-right:5px") 👑 - span(v-text="userDialog.representedGroup.name" style="margin-right:5px") - span ({{ userDialog.representedGroup.memberCount }}) - .extra(v-else) - - .x-friend-item(style="width:100%;cursor:default") - .detail - span.name {{ $t('dialog.user.info.bio') }} - pre.extra(style="font-family:inherit;font-size:12px;white-space:pre-wrap;margin:0 0.5em 0 0") {{ userDialog.ref.bio || '-' }} - div(v-if="userDialog.id === API.currentUser.id" style="float:right") - el-button(type="text" icon="el-icon-edit" size="mini" @click="showBioDialog" style="margin-left:5px") - div(style="margin-top:5px") - el-tooltip(v-if="link" v-for="(link, index) in userDialog.ref.bioLinks" :key="index") - template(#content) - span(v-text="link") - img(:src="getFaviconUrl(link)" style="width:16px;height:16px;vertical-align:middle;margin-right:5px;cursor:pointer" @click.stop="openExternalLink(link)") - template(v-if="API.currentUser.id !== userDialog.id") - .x-friend-item(style="cursor:default") - .detail - span.name {{ $t('dialog.user.info.last_seen') }} - el-tooltip(v-if="!hideTooltips" placement="top" style="margin-left:5px" :content="$t('dialog.user.info.accuracy_notice')") - i.el-icon-warning - span.extra {{ userDialog.lastSeen | formatDate('long') }} - .x-friend-item(@click="showPreviousInstancesUserDialog(userDialog.ref)") - .detail - span.name {{ $t('dialog.user.info.join_count') }} - el-tooltip(v-if="!hideTooltips" placement="top" style="margin-left:5px" :content="$t('dialog.user.info.accuracy_notice')") - i.el-icon-warning - span.extra(v-if="userDialog.joinCount === 0") - - span.extra(v-else v-text="userDialog.joinCount") - .x-friend-item(style="cursor:default") - .detail - span.name {{ $t('dialog.user.info.time_together') }} - el-tooltip(v-if="!hideTooltips" placement="top" style="margin-left:5px" :content="$t('dialog.user.info.accuracy_notice')") - i.el-icon-warning - span.extra(v-if="userDialog.timeSpent === 0") - - span.extra(v-else) {{ userDialog.timeSpent | timeToText }} - template(v-else) - .x-friend-item(@click="showPreviousInstancesUserDialog(userDialog.ref)") - .detail - span.name {{ $t('dialog.user.info.play_time') }} - el-tooltip(v-if="!hideTooltips" placement="top" style="margin-left:5px" :content="$t('dialog.user.info.accuracy_notice')") - i.el-icon-warning - span.extra(v-if="userDialog.timeSpent === 0") - - span.extra(v-else) {{ userDialog.timeSpent | timeToText }} - .x-friend-item(style="cursor:default") - el-tooltip(placement="top") - template(#content) - span {{ userOnlineForTimestamp(userDialog) | formatDate('short') }} - .detail - span.name(v-if="userDialog.ref.state === 'online' && userDialog.ref.$online_for") {{ $t('dialog.user.info.online_for') }} - el-tooltip(v-if="!hideTooltips" placement="top" style="margin-left:5px" :content="$t('dialog.user.info.accuracy_notice')") - i.el-icon-warning - span.name(v-else) {{ $t('dialog.user.info.offline_for') }} - el-tooltip(v-if="!hideTooltips" placement="top" style="margin-left:5px" :content="$t('dialog.user.info.accuracy_notice')") - i.el-icon-warning - span.extra {{ userOnlineFor(userDialog) | timeToText }} - .x-friend-item(style="cursor:default") - el-tooltip(placement="top") - template(#content) - span {{ $t('dialog.user.info.last_login') }} {{ userDialog.ref.last_login | formatDate('short') }} - .detail - span.name {{ $t('dialog.user.info.last_activity') }} - span.extra {{ userDialog.ref.last_activity | formatDate('long') }} - .x-friend-item(style="cursor:default") - .detail - span.name {{ $t('dialog.user.info.date_joined') }} - span.extra(v-text="userDialog.ref.date_joined") - .x-friend-item(v-if="API.currentUser.id !== userDialog.id" style="cursor:default") - el-tooltip(placement="top") - template(#content v-if="userDialog.dateFriendedInfo.length") - template(v-for="ref in userDialog.dateFriendedInfo") - span {{ ref.type }}: {{ ref.created_at | formatDate('long') }} - br - template(#content v-else) - span - - .detail - span.name(v-if="userDialog.unFriended") {{ $t('dialog.user.info.unfriended') }} - el-tooltip(v-if="!hideTooltips" placement="top" style="margin-left:5px" :content="$t('dialog.user.info.accuracy_notice')") - i.el-icon-warning - span.name(v-else) {{ $t('dialog.user.info.friended') }} - el-tooltip(v-if="!hideTooltips" placement="top" style="margin-left:5px" :content="$t('dialog.user.info.accuracy_notice')") - i.el-icon-warning - span.extra {{ userDialog.dateFriended | formatDate('long') }} - template(v-if="API.currentUser.id === userDialog.id") - .x-friend-item(@click="toggleAvatarCopying") - .detail - span.name {{ $t('dialog.user.info.avatar_cloning') }} - span.extra(v-if="API.currentUser.allowAvatarCopying" style="color:#67C23A") {{ $t('dialog.user.info.avatar_cloning_allow') }} - span.extra(v-else style="color:#F56C6C") {{ $t('dialog.user.info.avatar_cloning_deny') }} - //- .x-friend-item(@click="toggleAllowBooping") - //- .detail - //- span.name {{ $t('dialog.user.info.booping') }} - //- span.extra(v-if="API.currentUser.isBoopingEnabled" style="color:#67C23A") {{ $t('dialog.user.info.avatar_cloning_allow') }} - //- span.extra(v-else style="color:#F56C6C") {{ $t('dialog.user.info.avatar_cloning_deny') }} - template(v-else) - .x-friend-item(style="cursor:default") - .detail - span.name {{ $t('dialog.user.info.avatar_cloning') }} - span.extra(v-if="userDialog.ref.allowAvatarCopying" style="color:#67C23A") {{ $t('dialog.user.info.avatar_cloning_allow') }} - span.extra(v-else style="color:#F56C6C") {{ $t('dialog.user.info.avatar_cloning_deny') }} - .x-friend-item(v-if="userDialog.ref.id === API.currentUser.id && API.currentUser.homeLocation" @click="showWorldDialog(API.currentUser.homeLocation)" style="width:100%") - .detail - span.name {{ $t('dialog.user.info.home_location') }} - span.extra - span(v-text="userDialog.$homeLocationName") - el-button(@click.stop="resetHome()" size="mini" icon="el-icon-delete" circle style="margin-left:5px") - .x-friend-item(style="width:100%;cursor:default") - .detail - span.name {{ $t('dialog.user.info.id') }} - span.extra {{ userDialog.id }} - el-tooltip(placement="top" :content="$t('dialog.user.info.id_tooltip')" :disabled="hideTooltips") - el-dropdown(trigger="click" @click.native.stop size="mini" style="margin-left:5px") - el-button(type="default" icon="el-icon-s-order" size="mini" circle) - el-dropdown-menu(#default="dropdown") - el-dropdown-item(@click.native="copyUserId(userDialog.id)") {{ $t('dialog.user.info.copy_id') }} - el-dropdown-item(@click.native="copyUserURL(userDialog.id)") {{ $t('dialog.user.info.copy_url') }} - el-dropdown-item(@click.native="copyUserDisplayName(userDialog.ref.displayName)") {{ $t('dialog.user.info.copy_display_name') }} - el-tab-pane(:label="$t('dialog.user.groups.header')") - el-button(type="default" :loading="userDialog.isGroupsLoading" @click="getUserGroups(userDialog.id)" size="mini" icon="el-icon-refresh" circle) - span(style="margin-left:5px") {{ $t('dialog.user.groups.total_count', { count: userGroups.groups.length }) }} - div(v-loading="userDialog.isGroupsLoading" style="margin-top:10px") - template(v-if="userGroups.ownGroups.length > 0") - span(style="font-weight:bold;font-size:16px") {{ $t('dialog.user.groups.own_groups') }} - span(style="color:#909399;font-size:12px;margin-left:5px") {{ userGroups.ownGroups.length }}/{{ API.cachedConfig?.constants?.GROUPS?.MAX_OWNED }} - .x-friend-list(style="margin-top:10px;margin-bottom:15px;min-height:60px") - .x-friend-item(v-for="group in userGroups.ownGroups" :key="group.id" @click="showGroupDialog(group.id)" class="x-friend-item-border") - .avatar - img(v-lazy="group.iconUrl") - .detail - span.name(v-text="group.name") - span.extra - el-tooltip(v-if="group.isRepresenting" placement="top" :content="$t('dialog.group.members.representing')") - i.el-icon-collection-tag(style="margin-right:5px") - el-tooltip(v-if="group.memberVisibility !== 'visible'" placement="top") - template(#content) - span {{ $t('dialog.group.members.visibility') }} {{ group.memberVisibility }} - i.el-icon-view(style="margin-right:5px") - span ({{ group.memberCount }}) - template(v-if="userGroups.mutualGroups.length > 0") - span(style="font-weight:bold;font-size:16px") {{ $t('dialog.user.groups.mutual_groups') }} - span(style="color:#909399;font-size:12px;margin-left:5px") {{ userGroups.mutualGroups.length }} - .x-friend-list(style="margin-top:10px;margin-bottom:15px;min-height:60px") - .x-friend-item(v-for="group in userGroups.mutualGroups" :key="group.id" @click="showGroupDialog(group.id)" class="x-friend-item-border") - .avatar - img(v-lazy="group.iconUrl") - .detail - span.name(v-text="group.name") - span.extra - el-tooltip(v-if="group.isRepresenting" placement="top" :content="$t('dialog.group.members.representing')") - i.el-icon-collection-tag(style="margin-right:5px") - el-tooltip(v-if="group.memberVisibility !== 'visible'" placement="top") - template(#content) - span {{ $t('dialog.group.members.visibility') }} {{ group.memberVisibility }} - i.el-icon-view(style="margin-right:5px") - span ({{ group.memberCount }}) - template(v-if="userGroups.remainingGroups.length > 0") - span(style="font-weight:bold;font-size:16px") {{ $t('dialog.user.groups.groups') }} - span(style="color:#909399;font-size:12px;margin-left:5px") {{ userGroups.remainingGroups.length }} - template(v-if="API.currentUser.id === userDialog.id") - |/ - template(v-if="API.currentUser.$isVRCPlus") - | {{ API.cachedConfig?.constants?.GROUPS?.MAX_JOINED_PLUS }} - template(v-else) - | {{ API.cachedConfig?.constants?.GROUPS?.MAX_JOINED }} - .x-friend-list(style="margin-top:10px;margin-bottom:15px;min-height:60px") - .x-friend-item(v-for="group in userGroups.remainingGroups" :key="group.id" @click="showGroupDialog(group.id)" class="x-friend-item-border") - .avatar - img(v-lazy="group.iconUrl") - .detail - span.name(v-text="group.name") - span.extra - el-tooltip(v-if="group.isRepresenting" placement="top" :content="$t('dialog.group.members.representing')") - i.el-icon-collection-tag(style="margin-right:5px") - el-tooltip(v-if="group.memberVisibility !== 'visible'" placement="top") - template(#content) - span {{ $t('dialog.group.members.visibility') }} {{ group.memberVisibility }} - i.el-icon-view(style="margin-right:5px") - span ({{ group.memberCount }}) - el-tab-pane(:label="$t('dialog.user.worlds.header')") - el-button(type="default" :loading="userDialog.isWorldsLoading" @click="refreshUserDialogWorlds()" size="mini" icon="el-icon-refresh" circle) - span(style="margin-left:5px") {{ $t('dialog.user.worlds.total_count', { count: userDialog.worlds.length }) }} - div(style="float:right") - span(style="margin-right:5px") {{ $t('dialog.user.worlds.sort_by') }} - el-dropdown(@click.native.stop trigger="click" size="small" style="margin-right:5px" :disabled="userDialog.isWorldsLoading") - el-button(size="mini") - span {{ userDialog.worldSorting.name }} #[i.el-icon-arrow-down.el-icon--right] - el-dropdown-menu(#default="dropdown") - el-dropdown-item(v-for="(item) in userDialogWorldSortingOptions" v-text="item.name" @click.native="setUserDialogWorldSorting(item)") - span(style="margin-right:5px") {{ $t('dialog.user.worlds.order_by') }} - el-dropdown(@click.native.stop trigger="click" size="small" style="margin-right:5px" :disabled="userDialog.isWorldsLoading") - el-button(size="mini") - span {{ userDialog.worldOrder.name }} #[i.el-icon-arrow-down.el-icon--right] - el-dropdown-menu(#default="dropdown") - el-dropdown-item(v-for="(item) in userDialogWorldOrderOptions" v-text="item.name" @click.native="setUserDialogWorldOrder(item)") - .x-friend-list(v-loading="userDialog.isWorldsLoading" style="margin-top:10px;min-height:60px") - .x-friend-item(v-for="world in userDialog.worlds" :key="world.id" @click="showWorldDialog(world.id)" class="x-friend-item-border") - .avatar - img(v-lazy="world.thumbnailImageUrl") - .detail - span.name(v-text="world.name") - span.extra(v-if="world.occupants") ({{ world.occupants }}) - el-tab-pane(:label="$t('dialog.user.favorite_worlds.header')") - el-button(type="default" :loading="userDialog.isFavoriteWorldsLoading" @click="getUserFavoriteWorlds(userDialog.id)" size="mini" icon="el-icon-refresh" circle) - el-tabs.zero-margin-tabs(type="card" ref="favoriteWorlds" v-loading="userDialog.isFavoriteWorldsLoading" style="margin-top:10px") - template(v-for="(list, index) in userFavoriteWorlds" v-if="list") - el-tab-pane - span(slot="label") - span(v-text="list[0]" style="font-weight:bold;font-size:16px") - i.x-user-status(style="margin-left:5px" :class="userFavoriteWorldsStatus(list[1])") - span(style="color:#909399;font-size:12px;margin-left:5px") {{ list[2].length }}/{{ API.favoriteLimits.maxFavoritesPerGroup.world }} - .x-friend-list(style="margin-top:10px;margin-bottom:15px;min-height:60px") - .x-friend-item(v-for="world in list[2]" :key="world.id" @click="showWorldDialog(world.id)" class="x-friend-item-border") - .avatar - img(v-lazy="world.thumbnailImageUrl") - .detail - span.name(v-text="world.name") - span.extra(v-if="world.occupants") ({{ world.occupants }}) - el-tab-pane(:label="$t('dialog.user.avatars.header')") - template(v-if="userDialog.ref.id === API.currentUser.id") - el-button(type="default" :loading="userDialog.isAvatarsLoading" @click="refreshUserDialogAvatars()" size="mini" icon="el-icon-refresh" circle) - span(style="margin-left:5px") {{ $t('dialog.user.avatars.total_count', { count: userDialogAvatars.length }) }} - el-radio-group(v-if="userDialog.ref.id === API.currentUser.id" v-model="userDialog.avatarSorting" size="mini" style="margin-left:30px;margin-right:30px" @change="changeUserDialogAvatarSorting") - el-radio(label="name") {{ $t('dialog.user.avatars.sort_by_name') }} - el-radio(label="update") {{ $t('dialog.user.avatars.sort_by_update') }} - el-radio-group(v-if="userDialog.ref.id === API.currentUser.id" v-model="userDialog.avatarReleaseStatus" size="mini" style="margin-left:30px") - el-radio(label="all") {{ $t('dialog.user.avatars.all') }} - el-radio(label="public") {{ $t('dialog.user.avatars.public') }} - el-radio(label="private") {{ $t('dialog.user.avatars.private') }} - .x-friend-list(style="margin-top:10px;min-height:60px") - .x-friend-item(v-for="avatar in userDialogAvatars" @click="showAvatarDialog(avatar.id)" class="x-friend-item-border") - .avatar - img(v-if="avatar.thumbnailImageUrl" v-lazy="avatar.thumbnailImageUrl") - .detail - span.name(v-text="avatar.name") - span.extra(v-text="avatar.releaseStatus" v-if="avatar.releaseStatus === 'public'" style="color: #67c23a;") - span.extra(v-text="avatar.releaseStatus" v-else-if="avatar.releaseStatus === 'private'" style="color: #f56c6c;") - span.extra(v-text="avatar.releaseStatus" v-else) - el-tab-pane(:label="$t('dialog.user.json.header')") - el-button(type="default" @click="refreshUserDialogTreeData()" size="mini" icon="el-icon-refresh" circle) - el-button(type="default" @click="downloadAndSaveJson(userDialog.id, userDialog.ref)" size="mini" icon="el-icon-download" circle style="margin-left:5px") - el-tree(:data="userDialog.treeData" style="margin-top:5px;font-size:12px") - template(#default="scope") - span - span(v-text="scope.data.key" style="font-weight:bold;margin-right:5px") - span(v-if="!scope.data.children" v-text="scope.data.value") - - //- dialog: world - el-dialog.x-dialog.x-world-dialog(:before-close="beforeDialogClose" @mousedown.native="dialogMouseDown" @mouseup.native="dialogMouseUp" ref="worldDialog" :visible.sync="worldDialog.visible" :show-close="false" width="770px") - div(v-loading="worldDialog.loading") - div(style="display:flex") - el-popover(placement="right" width="500px" trigger="click") - img.x-link(slot="reference" v-lazy="worldDialog.ref.thumbnailImageUrl" style="flex:none;width:160px;height:120px;border-radius:12px") - img.x-link(v-lazy="worldDialog.ref.imageUrl" style="width:500px;height:375px" @click="showFullscreenImageDialog(worldDialog.ref.imageUrl)") - div(style="flex:1;display:flex;align-items:center;margin-left:15px") - div(style="flex:1") - div - i.el-icon-s-home(v-show="API.currentUser.$homeLocation && API.currentUser.$homeLocation.worldId === worldDialog.id" style="margin-right:5px") - span.dialog-title(v-text="worldDialog.ref.name") - div(style="margin-top:5px") - span.x-link.x-grey(v-text="worldDialog.ref.authorName" @click="showUserDialog(worldDialog.ref.authorId)" style="font-family:monospace") - div - el-tag(v-if="worldDialog.ref.$isLabs" type="primary" effect="plain" size="mini" style="margin-right:5px;margin-top:5px") {{ $t('dialog.world.tags.labs') }} - el-tag(v-else-if="worldDialog.ref.releaseStatus === 'public'" type="success" effect="plain" size="mini" style="margin-right:5px;margin-top:5px") {{ $t('dialog.world.tags.public') }} - el-tag(v-else type="danger" effect="plain" size="mini" style="margin-right:5px;margin-top:5px") {{ $t('dialog.world.tags.private') }} - el-tag.x-tag-platform-pc(v-if="worldDialog.isPC" type="info" effect="plain" size="mini" style="margin-right:5px;margin-top:5px") PC - span.x-grey(v-if="worldDialog.bundleSizes['standalonewindows']" style=";margin-left:5px;border-left:inherit;padding-left:5px") {{ worldDialog.bundleSizes['standalonewindows'].fileSize }} - el-tag.x-tag-platform-quest(v-if="worldDialog.isQuest" type="info" effect="plain" size="mini" style="margin-right:5px;margin-top:5px") Android - span.x-grey(v-if="worldDialog.bundleSizes['android']" style="margin-left:5px;border-left:inherit;padding-left:5px") {{ worldDialog.bundleSizes['android'].fileSize }} - el-tag.x-tag-platform-ios(v-if="worldDialog.isIos" type="info" effect="plain" size="mini" style="margin-right:5px;margin-top:5px") iOS - span.x-grey(v-if="worldDialog.bundleSizes['ios']" style="margin-left:5px;border-left:inherit;padding-left:5px") {{ worldDialog.bundleSizes['ios'].fileSize }} - el-tag(v-if="worldDialog.avatarScalingDisabled" type="warning" effect="plain" size="mini" style="margin-right:5px;margin-top:5px") {{ $t('dialog.world.tags.avatar_scaling_disabled') }} - el-tag(v-if="worldDialog.focusViewDisabled" type="warning" effect="plain" size="mini" style="margin-right:5px;margin-top:5px") {{ $t('dialog.world.tags.focus_view_disabled') }} - el-tag(v-if="worldDialog.stickersDisabled" type="warning" effect="plain" size="mini" style="margin-right:5px;margin-top:5px") {{ $t('dialog.world.tags.stickers_disabled') }} - el-tag(v-if="worldDialog.ref.unityPackageUrl" type="success" effect="plain" size="mini" style="margin-right:5px;margin-top:5px") {{ $t('dialog.world.tags.future_proofing') }} - el-tag.x-link(v-if="worldDialog.inCache" type="info" effect="plain" size="mini" style="margin-right:5px;margin-top:5px" @click="openFolderGeneric(worldDialog.cachePath)") - span(v-text="worldDialog.cacheSize") - | {{ $t('dialog.world.tags.cache')}} - div - template(v-for="tag in worldDialog.ref.tags") - el-tag(v-if="tag.startsWith('content_')" :key="tag" effect="plain" size="mini" style="margin-right:5px;margin-top:5px") - template(v-if="tag === 'content_horror'") {{ $t('dialog.world.tags.content_horror') }} - template(v-else-if="tag === 'content_gore'") {{ $t('dialog.world.tags.content_gore') }} - template(v-else-if="tag === 'content_violence'") {{ $t('dialog.world.tags.content_violence') }} - template(v-else-if="tag === 'content_adult'") {{ $t('dialog.world.tags.content_adult') }} - template(v-else-if="tag === 'content_sex'") {{ $t('dialog.world.tags.content_sex') }} - template(v-else) {{ tag.replace('content_', '') }} - div(style="margin-top:5px") - span(v-show="worldDialog.ref.name !== worldDialog.ref.description" v-text="worldDialog.ref.description" style="font-size:12px") - div(style="flex:none;margin-left:10px") - el-tooltip(v-if="worldDialog.inCache" placement="top" :content="$t('dialog.world.actions.delete_cache_tooltip')" :disabled="hideTooltips") - el-button(icon="el-icon-delete" circle @click="deleteVRChatCache(worldDialog.ref)" :disabled="isGameRunning && worldDialog.cacheLocked") - el-tooltip(v-if="worldDialog.isFavorite" placement="top" :content="$t('dialog.world.actions.favorites_tooltip')" :disabled="hideTooltips") - el-button(type="default" icon="el-icon-star-on" circle @click="worldDialogCommand('Add Favorite')" style="margin-left:5px") - el-tooltip(v-else placement="top" :content="$t('dialog.world.actions.favorites_tooltip')" :disabled="hideTooltips") - el-button(type="default" icon="el-icon-star-off" circle @click="worldDialogCommand('Add Favorite')" style="margin-left:5px") - el-dropdown(trigger="click" @command="worldDialogCommand" size="small" style="margin-left:5px") - el-button(type="default" icon="el-icon-more" circle) - el-dropdown-menu(#default="dropdown") - el-dropdown-item(icon="el-icon-refresh" command="Refresh") {{ $t('dialog.world.actions.refresh') }} - el-dropdown-item(icon="el-icon-s-flag" command="New Instance" divided) {{ $t('dialog.world.actions.new_instance') }} - el-dropdown-item(v-if="API.currentUser.$homeLocation && API.currentUser.$homeLocation.worldId === worldDialog.id" icon="el-icon-magic-stick" command="Reset Home" divided) {{ $t('dialog.world.actions.reset_home') }} - el-dropdown-item(v-else icon="el-icon-s-home" command="Make Home" divided) {{ $t('dialog.world.actions.make_home') }} - el-dropdown-item(icon="el-icon-tickets" command="Previous Instances") {{ $t('dialog.world.actions.show_previous_instances') }} - template(v-if="API.currentUser.id !== worldDialog.ref.authorId") - el-dropdown-item(icon="el-icon-picture-outline" command="Previous Images") {{ $t('dialog.world.actions.show_previous_images') }} - el-dropdown-item(:disabled="!worldDialog.hasPersistData" icon="el-icon-upload" command="Delete Persistent Data") {{ $t('dialog.world.actions.delete_persistent_data') }} - template(v-else) - el-dropdown-item(icon="el-icon-edit" command="Rename") {{ $t('dialog.world.actions.rename') }} - el-dropdown-item(icon="el-icon-edit" command="Change Description") {{ $t('dialog.world.actions.change_description') }} - el-dropdown-item(icon="el-icon-edit" command="Change Capacity") {{ $t('dialog.world.actions.change_capacity') }} - el-dropdown-item(icon="el-icon-edit" command="Change Recommended Capacity") {{ $t('dialog.world.actions.change_recommended_capacity') }} - el-dropdown-item(icon="el-icon-edit" command="Change YouTube Preview") {{ $t('dialog.world.actions.change_preview') }} - el-dropdown-item(icon="el-icon-edit" command="Change Tags") {{ $t('dialog.world.actions.change_tags') }} - el-dropdown-item(icon="el-icon-picture-outline" command="Change Image") {{ $t('dialog.world.actions.change_image') }} - el-dropdown-item(v-if="worldDialog.ref.unityPackageUrl" icon="el-icon-download" command="Download Unity Package") {{ $t('dialog.world.actions.download_package') }} - el-dropdown-item(v-if="worldDialog.ref.tags.includes('system_approved') || worldDialog.ref.tags.includes('system_labs')" icon="el-icon-view" command="Unpublish" divided) {{ $t('dialog.world.actions.unpublish') }} - el-dropdown-item(v-else icon="el-icon-view" command="Publish" divided) {{ $t('dialog.world.actions.publish_to_labs') }} - el-dropdown-item(:disabled="!worldDialog.hasPersistData" icon="el-icon-upload" command="Delete Persistent Data") {{ $t('dialog.world.actions.delete_persistent_data') }} - el-dropdown-item(icon="el-icon-delete" command="Delete" style="color:#F56C6C") {{ $t('dialog.world.actions.delete') }} - el-tabs - el-tab-pane(:label="$t('dialog.world.instances.header')") - div. - #[i.el-icon-user] {{ $t('dialog.world.instances.public_count', { count: worldDialog.ref.publicOccupants }) }} - #[i.el-icon-user-solid(style="margin-left:10px")] {{ $t('dialog.world.instances.private_count', { count: worldDialog.ref.privateOccupants }) }} - #[i.el-icon-check(style="margin-left:10px")] {{ $t('dialog.world.instances.capacity_count', { count: worldDialog.ref.recommendedCapacity, max: worldDialog.ref.capacity }) }} - div(v-for="room in worldDialog.rooms" :key="room.id") - div(style="margin:5px 0") - location-world(:locationobject="room.$location" :currentuserid="API.currentUser.id" :worlddialogshortname="worldDialog.$location.shortName") - el-tooltip(placement="top" :content="$t('dialog.world.instances.self_invite_tooltip')" :disabled="hideTooltips") - invite-yourself(:location="room.$location.tag" :shortname="room.$location.shortName" style="margin-left:5px") - el-tooltip(placement="top" :content="$t('dialog.world.instances.refresh_instance_info')" :disabled="hideTooltips") - el-button(@click="refreshInstancePlayerCount(room.tag)" size="mini" icon="el-icon-refresh" style="margin-left:5px" circle) - last-join(:location="room.$location.tag" :currentlocation="lastLocation.location") - instance-info(:location="room.tag" :instance="room.ref" :friendcount="room.friendCount" :updateelement="updateInstanceInfo") - .x-friend-list(style="margin:10px 0;max-height:unset" v-if="room.$location.userId || room.users.length") - .x-friend-item(v-if="room.$location.userId" @click="showUserDialog(room.$location.userId)" class="x-friend-item-border") - template(v-if="room.$location.user") - .avatar(:class="userStatusClass(room.$location.user)") - img(v-lazy="userImage(room.$location.user)") - .detail - span.name(v-text="room.$location.user.displayName" :style="{'color':room.$location.user.$userColour}") - span.extra {{ $t('dialog.world.instances.instance_creator') }} - span(v-else v-text="room.$location.userId") - .x-friend-item(v-for="user in room.users" :key="user.id" @click="showUserDialog(user.id)" class="x-friend-item-border") - .avatar(:class="userStatusClass(user)") - img(v-lazy="userImage(user)") - .detail - span.name(v-text="user.displayName" :style="{'color':user.$userColour}") - span.extra(v-if="user.location === 'traveling'") - i.el-icon-loading(style="margin-right:5px") - timer(:epoch="user.$travelingToTime") - span.extra(v-else) - timer(:epoch="user.$location_at") - el-tab-pane(:label="$t('dialog.world.info.header')") - .x-friend-list(style="max-height:none") - .x-friend-item(style="width:100%;cursor:default") - .detail - span.name {{ $t('dialog.world.info.memo') }} - el-input.extra(v-model="worldDialog.memo" type="textarea" :rows="2" :autosize="{ minRows: 1, maxRows: 20 }" :placeholder="$t('dialog.world.info.memo_placeholder')" size="mini" resize="none") - div(style="width:100%;display:flex") - .x-friend-item(style="width:100%;cursor:default") - .detail - span.name {{ $t('dialog.world.info.id') }} - span.extra {{ worldDialog.id }} - el-tooltip(placement="top" :content="$t('dialog.world.info.id_tooltip')" :disabled="hideTooltips") - el-dropdown(trigger="click" @click.native.stop size="mini" style="margin-left:5px") - el-button(type="default" icon="el-icon-s-order" size="mini" circle) - el-dropdown-menu(#default="dropdown") - el-dropdown-item(@click.native="copyWorldId(worldDialog.id)") {{ $t('dialog.world.info.copy_id') }} - el-dropdown-item(@click.native="copyWorldUrl(worldDialog.id)") {{ $t('dialog.world.info.copy_url') }} - el-dropdown-item(@click.native="copyWorldName(worldDialog.ref.name)") {{ $t('dialog.world.info.copy_name') }} - .x-friend-item(v-if="worldDialog.ref.previewYoutubeId" style="width:350px" @click="openExternalLink(`https://www.youtube.com/watch?v=${worldDialog.ref.previewYoutubeId}`)") - .detail - span.name {{ $t('dialog.world.info.youtube_preview') }} - span.extra https://www.youtube.com/watch?v={{ worldDialog.ref.previewYoutubeId }} - .x-friend-item(style="width:100%;cursor:default") - .detail - span.name {{ $t('dialog.world.info.author_tags') }} - span.extra(v-if="worldDialog.ref.tags?.filter(tag => tag.startsWith('author_tag')).length > 0") {{ worldDialog.ref.tags.filter(tag => tag.startsWith('author_tag')).map(tag => tag.replace('author_tag_', '')).join(', ') }} - span.extra(v-else) - - .x-friend-item(style="cursor:default") - .detail - span.name {{ $t('dialog.world.info.players') }} - span.extra {{ worldDialog.ref.occupants | commaNumber }} - .x-friend-item(style="cursor:default") - .detail - span.name {{ $t('dialog.world.info.favorites') }} - span.extra {{ worldDialog.ref.favorites | commaNumber }} - | #[template(v-if="worldDialog.ref.favorites > 0 && worldDialog.ref.visits > 0") ({{ Math.round(((worldDialog.ref.favorites - worldDialog.ref.visits) / worldDialog.ref.visits * 100 + 100) * 100) / 100 }}%)] - .x-friend-item(style="cursor:default") - .detail - span.name {{ $t('dialog.world.info.visits') }} - span.extra {{ worldDialog.ref.visits | commaNumber }} - .x-friend-item(style="cursor:default") - .detail - span.name {{ $t('dialog.world.info.capacity') }} - span.extra {{ worldDialog.ref.recommendedCapacity | commaNumber }} ({{ worldDialog.ref.capacity | commaNumber }}) - .x-friend-item(style="cursor:default") - .detail - span.name {{ $t('dialog.world.info.created_at') }} - span.extra {{ worldDialog.ref.created_at | formatDate('long') }} - .x-friend-item(style="cursor:default") - .detail - span.name {{ $t('dialog.world.info.last_updated') }} - span.extra(v-if="worldDialog.lastUpdated") {{ worldDialog.lastUpdated | formatDate('long') }} - span.extra(v-else) {{ worldDialog.ref.updated_at | formatDate('long') }} - .x-friend-item(v-if="worldDialog.ref.labsPublicationDate !== 'none'" style="cursor:default") - .detail - span.name {{ $t('dialog.world.info.labs_publication_date') }} - span.extra {{ worldDialog.ref.labsPublicationDate | formatDate('long') }} - .x-friend-item(v-if="worldDialog.ref.publicationDate !== 'none'" style="cursor:default") - .detail - span.name {{ $t('dialog.world.info.publication_date') }} - el-tooltip(v-if="worldDialog.ref.publicationDate && worldDialog.ref.publicationDate !== 'none' && worldDialog.ref.labsPublicationDate && worldDialog.ref.labsPublicationDate !== 'none'" placement="top" style="margin-left:5px") - template(#content) - span {{ $t('dialog.world.info.time_in_labs') }} {{ new Date(worldDialog.ref.publicationDate) - new Date(worldDialog.ref.labsPublicationDate) | timeToText }} - i.el-icon-arrow-down - span.extra {{ worldDialog.ref.publicationDate | formatDate('long') }} - .x-friend-item(style="cursor:default") - .detail - span.name {{ $t('dialog.world.info.version') }} - span.extra(v-text="worldDialog.ref.version") - .x-friend-item(style="cursor:default") - .detail - span.name {{ $t('dialog.world.info.heat') }} - span.extra {{ worldDialog.ref.heat | commaNumber }} {{ '🔥'.repeat(worldDialog.ref.heat) }} - .x-friend-item(style="cursor:default") - .detail - span.name {{ $t('dialog.world.info.popularity') }} - span.extra {{ worldDialog.ref.popularity | commaNumber }} {{ '💖'.repeat(worldDialog.ref.popularity) }} - .x-friend-item(style="width:100%;cursor:default") - .detail - span.name {{ $t('dialog.world.info.platform') }} - span.extra(v-text="worldDialogPlatform") - .x-friend-item(style="cursor:default") - .detail - span.name {{ $t('dialog.world.info.last_visited') }} - el-tooltip(v-if="!hideTooltips" placement="top" style="margin-left:5px" :content="$t('dialog.world.info.accuracy_notice')") - i.el-icon-warning - span.extra {{ worldDialog.lastVisit | formatDate('long') }} - .x-friend-item(@click="showPreviousInstancesWorldDialog(worldDialog.ref)") - .detail - span.name {{ $t('dialog.world.info.visit_count') }} - el-tooltip(v-if="!hideTooltips" placement="top" style="margin-left:5px" :content="$t('dialog.world.info.accuracy_notice')") - i.el-icon-warning - span.extra(v-text="worldDialog.visitCount") - .x-friend-item(style="cursor:default") - .detail - span.name {{ $t('dialog.world.info.time_spent') }} - el-tooltip(v-if="!hideTooltips" placement="top" style="margin-left:5px" :content="$t('dialog.world.info.accuracy_notice')") - i.el-icon-warning - span.extra(v-if="worldDialog.timeSpent === 0") - - span.extra(v-else) {{ worldDialog.timeSpent | timeToText }} - el-tab-pane(:label="$t('dialog.world.json.header')") - el-button(type="default" @click="refreshWorldDialogTreeData()" size="mini" icon="el-icon-refresh" circle) - el-button(type="default" @click="downloadAndSaveJson(worldDialog.id, worldDialog.ref)" size="mini" icon="el-icon-download" circle style="margin-left:5px") - el-tree(:data="worldDialog.treeData" style="margin-top:5px;font-size:12px") - template(#default="scope") - span - span(v-text="scope.data.key" style="font-weight:bold;margin-right:5px") - span(v-if="!scope.data.children" v-text="scope.data.value") - - //- dialog: avatar - el-dialog.x-dialog.x-avatar-dialog(:before-close="beforeDialogClose" @mousedown.native="dialogMouseDown" @mouseup.native="dialogMouseUp" ref="avatarDialog" :visible.sync="avatarDialog.visible" :show-close="false" width="600px") - div(v-loading="avatarDialog.loading") - div(style="display:flex") - el-popover(placement="right" width="500px" trigger="click") - img.x-link(slot="reference" v-lazy="avatarDialog.ref.thumbnailImageUrl" style="flex:none;width:160px;height:120px;border-radius:12px") - img.x-link(v-lazy="avatarDialog.ref.imageUrl" style="width:500px;height:375px" @click="showFullscreenImageDialog(avatarDialog.ref.imageUrl)") - div(style="flex:1;display:flex;align-items:center;margin-left:15px") - div(style="flex:1") - div - span.dialog-title(v-text="avatarDialog.ref.name") - div(style="margin-top:5px") - span.x-link.x-grey(v-text="avatarDialog.ref.authorName" @click="showUserDialog(avatarDialog.ref.authorId)" style="font-family:monospace") - div - el-tag(v-if="avatarDialog.ref.releaseStatus === 'public'" type="success" effect="plain" size="mini" style="margin-right:5px;margin-top:5px") {{ $t('dialog.avatar.tags.public') }} - el-tag(v-else type="danger" effect="plain" size="mini" style="margin-right:5px;margin-top:5px") {{ $t('dialog.avatar.tags.private') }} - el-tag.x-tag-platform-pc(v-if="avatarDialog.isPC" type="info" effect="plain" size="mini" style="margin-right:5px;margin-top:5px") PC - span.x-grey(v-if="avatarDialog.platformInfo.pc" style=";margin-left:5px;border-left:inherit;padding-left:5px") {{ avatarDialog.platformInfo.pc.performanceRating }} - span.x-grey(v-if="avatarDialog.bundleSizes['standalonewindows']" style=";margin-left:5px;border-left:inherit;padding-left:5px") {{ avatarDialog.bundleSizes['standalonewindows'].fileSize }} - el-tag.x-tag-platform-quest(v-if="avatarDialog.isQuest" type="info" effect="plain" size="mini" style="margin-right:5px;margin-top:5px") Android - span.x-grey(v-if="avatarDialog.platformInfo.android" style=";margin-left:5px;border-left:inherit;padding-left:5px") {{ avatarDialog.platformInfo.android.performanceRating }} - span.x-grey(v-if="avatarDialog.bundleSizes['android']" style="margin-left:5px;border-left:inherit;padding-left:5px") {{ avatarDialog.bundleSizes['android'].fileSize }} - el-tag.x-tag-platform-ios(v-if="avatarDialog.isIos" type="info" effect="plain" size="mini" style="margin-right:5px;margin-top:5px") iOS - span.x-grey(v-if="avatarDialog.platformInfo.ios" style=";margin-left:5px;border-left:inherit;padding-left:5px") {{ avatarDialog.platformInfo.ios.performanceRating }} - span.x-grey(v-if="avatarDialog.bundleSizes['ios']" style="margin-left:5px;border-left:inherit;padding-left:5px") {{ avatarDialog.bundleSizes['ios'].fileSize }} - el-tag.x-link(v-if="avatarDialog.inCache" type="info" effect="plain" size="mini" @click="openFolderGeneric(avatarDialog.cachePath)" style="margin-right:5px;margin-top:5px") - span(v-text="avatarDialog.cacheSize") - | {{ $t('dialog.avatar.tags.cache') }} - el-tag(v-if="avatarDialog.isQuestFallback" type="info" effect="plain" size="mini" style="margin-right:5px;margin-top:5px") {{ $t('dialog.avatar.tags.fallback') }} - el-tag(v-if="avatarDialog.hasImposter" type="info" effect="plain" size="mini" style="margin-right:5px;margin-top:5px") {{ $t('dialog.avatar.tags.impostor') }} - span.x-grey(v-if="avatarDialog.imposterVersion" style="margin-left:5px;border-left:inherit;padding-left:5px") v{{ avatarDialog.imposterVersion }} - el-tag(v-if="avatarDialog.ref.unityPackageUrl" type="success" effect="plain" size="mini" style="margin-right:5px;margin-top:5px") {{ $t('dialog.avatar.tags.future_proofing') }} - div - template(v-for="tag in avatarDialog.ref.tags") - el-tag(v-if="tag.startsWith('content_')" :key="tag" effect="plain" size="mini" style="margin-right:5px;margin-top:5px") - template(v-if="tag === 'content_horror'") {{ $t('dialog.avatar.tags.content_horror') }} - template(v-else-if="tag === 'content_gore'") {{ $t('dialog.avatar.tags.content_gore') }} - template(v-else-if="tag === 'content_violence'") {{ $t('dialog.avatar.tags.content_violence') }} - template(v-else-if="tag === 'content_adult'") {{ $t('dialog.avatar.tags.content_adult') }} - template(v-else-if="tag === 'content_sex'") {{ $t('dialog.avatar.tags.content_sex') }} - template(v-else) {{ tag.replace('content_', '') }} - div(style="margin-top:5px") - span(v-show="avatarDialog.ref.name !== avatarDialog.ref.description" v-text="avatarDialog.ref.description" style="font-size:12px") - div(style="flex:none;margin-left:10px") - el-tooltip(v-if="avatarDialog.inCache" placement="top" :content="$t('dialog.avatar.actions.delete_cache_tooltip')" :disabled="hideTooltips") - el-button(icon="el-icon-delete" circle @click="deleteVRChatCache(avatarDialog.ref)" :disabled="isGameRunning && avatarDialog.cacheLocked") - el-tooltip(v-if="avatarDialog.isFavorite" placement="top" :content="$t('dialog.avatar.actions.favorite_tooltip')" :disabled="hideTooltips") - el-button(type="warning" icon="el-icon-star-on" circle @click="avatarDialogCommand('Add Favorite')" style="margin-left:5px") - el-tooltip(v-else placement="top" :content="$t('dialog.avatar.actions.favorite_tooltip')" :disabled="hideTooltips") - el-button(type="default" icon="el-icon-star-off" circle @click="avatarDialogCommand('Add Favorite')" style="margin-left:5px") - el-dropdown(trigger="click" @command="avatarDialogCommand" size="small" style="margin-left:5px") - el-button(:type="avatarDialog.isBlocked ? 'danger' : 'default'" icon="el-icon-more" circle) - el-dropdown-menu(#default="dropdown") - el-dropdown-item(icon="el-icon-refresh" command="Refresh") {{ $t('dialog.avatar.actions.refresh') }} - el-dropdown-item(icon="el-icon-check" :disabled="API.currentUser.currentAvatar === avatarDialog.id" command="Select Avatar") {{ $t('dialog.avatar.actions.select') }} - el-dropdown-item(v-if="/quest/.test(avatarDialog.ref.tags)" icon="el-icon-check" command="Select Fallback Avatar") {{ $t('dialog.avatar.actions.select_fallback') }} - el-dropdown-item(v-if="avatarDialog.isBlocked" icon="el-icon-circle-check" command="Unblock Avatar" style="color:#F56C6C") {{ $t('dialog.avatar.actions.unblock') }} - el-dropdown-item(v-else icon="el-icon-circle-close" command="Block Avatar") {{ $t('dialog.avatar.actions.block') }} - el-dropdown-item(v-if="avatarDialog.ref.authorId !== API.currentUser.id" icon="el-icon-picture-outline" command="Previous Images") {{ $t('dialog.avatar.actions.show_previous_images') }} - template(v-if="avatarDialog.ref.authorId === API.currentUser.id") - el-dropdown-item(v-if="avatarDialog.ref.releaseStatus === 'public'" icon="el-icon-user-solid" command="Make Private" divided) {{ $t('dialog.avatar.actions.make_private') }} - el-dropdown-item(v-else icon="el-icon-user" command="Make Public" divided) {{ $t('dialog.avatar.actions.make_public') }} - el-dropdown-item(icon="el-icon-edit" command="Rename") {{ $t('dialog.avatar.actions.rename') }} - el-dropdown-item(icon="el-icon-edit" command="Change Description") {{ $t('dialog.avatar.actions.change_description') }} - el-dropdown-item(icon="el-icon-edit" command="Change Content Tags") {{ $t('dialog.avatar.actions.change_content_tags') }} - el-dropdown-item(icon="el-icon-picture-outline" command="Change Image") {{ $t('dialog.avatar.actions.change_image') }} - el-dropdown-item(v-if="avatarDialog.ref.unityPackageUrl" icon="el-icon-download" command="Download Unity Package") {{ $t('dialog.avatar.actions.download_package') }} - el-dropdown-item(v-if="avatarDialog.hasImposter" icon="el-icon-delete" command="Delete Imposter" style="color:#F56C6C") {{ $t('dialog.avatar.actions.delete_impostor') }} - el-dropdown-item(v-else icon="el-icon-user" command="Create Imposter") {{ $t('dialog.avatar.actions.create_impostor') }} - el-dropdown-item(icon="el-icon-delete" command="Delete" style="color:#F56C6C" divided) {{ $t('dialog.avatar.actions.delete') }} - el-tabs - el-tab-pane(:label="$t('dialog.avatar.info.header')") - .x-friend-list - .x-friend-item(style="width:100%;cursor:default") - .detail - span.name {{ $t('dialog.avatar.info.memo') }} - el-input.extra(v-model="avatarDialog.memo" size="mini" type="textarea" :rows="2" :autosize="{minRows: 1, maxRows: 20}" :placeholder="$t('dialog.avatar.info.memo_placeholder')" resize="none") - .x-friend-item(style="width:100%;cursor:default") - .detail - span.name {{ $t('dialog.avatar.info.id') }} - span.extra {{ avatarDialog.id }} - el-tooltip(placement="top" :content="$t('dialog.avatar.info.id_tooltip')" :disabled="hideTooltips") - el-dropdown(trigger="click" @click.native.stop size="mini" style="margin-left:5px") - el-button(type="default" icon="el-icon-s-order" size="mini" circle) - el-dropdown-menu(#default="dropdown") - el-dropdown-item(@click.native="copyAvatarId(avatarDialog.id)") {{ $t('dialog.avatar.info.copy_id') }} - el-dropdown-item(@click.native="copyAvatarUrl(avatarDialog.id)") {{ $t('dialog.avatar.info.copy_url') }} - .x-friend-item(style="cursor:default") - .detail - span.name {{ $t('dialog.avatar.info.created_at') }} - span.extra {{ avatarDialog.ref.created_at | formatDate('long') }} - .x-friend-item(style="cursor:default") - .detail - span.name {{ $t('dialog.avatar.info.last_updated') }} - span.extra(v-if="avatarDialog.lastUpdated") {{ avatarDialog.lastUpdated | formatDate('long') }} - span.extra(v-else) {{ avatarDialog.ref.updated_at | formatDate('long') }} - .x-friend-item(style="cursor:default") - .detail - span.name {{ $t('dialog.avatar.info.version') }} - span.extra(v-if="avatarDialog.ref.version !== 0" v-text="avatarDialog.ref.version") - span.extra(v-else) - - .x-friend-item(style="width:100%;cursor:default") - .detail - span.name {{ $t('dialog.avatar.info.platform') }} - span.extra(v-if="avatarDialogPlatform" v-text="avatarDialogPlatform") - span.extra(v-else) - - el-tab-pane(:label="$t('dialog.avatar.json.header')") - el-button(type="default" @click="refreshAvatarDialogTreeData()" size="mini" icon="el-icon-refresh" circle) - el-tooltip(placement="top" :content="$t('dialog.avatar.json.file_analysis')" :disabled="hideTooltips") - el-button(type="default" @click="getAvatarFileAnalysis" size="mini" icon="el-icon-s-data" circle style="margin-left:5px") - el-button(type="default" @click="downloadAndSaveJson(avatarDialog.id, avatarDialog.ref)" size="mini" icon="el-icon-download" circle style="margin-left:5px") - el-tree(v-if="Object.keys(avatarDialog.fileAnalysis).length > 0" :data="avatarDialog.fileAnalysis" style="margin-top:5px;font-size:12px") - template(#default="scope") - span - span(v-text="scope.data.key" style="font-weight:bold;margin-right:5px") - span(v-if="!scope.data.children" v-text="scope.data.value") - el-tree(:data="avatarDialog.treeData" style="margin-top:5px;font-size:12px") - template(#default="scope") - span - span(v-text="scope.data.key" style="font-weight:bold;margin-right:5px") - span(v-if="!scope.data.children" v-text="scope.data.value") - - //- dialog: group - el-dialog.x-dialog.x-group-dialog(:before-close="beforeDialogClose" @mousedown.native="dialogMouseDown" @mouseup.native="dialogMouseUp" ref="groupDialog" :visible.sync="groupDialog.visible" :show-close="false" width="770px") - .group-banner-image - el-popover(placement="right" width="500px" trigger="click") - img.x-link(slot="reference" v-lazy="groupDialog.ref.bannerUrl" style="flex:none;width:100%;aspect-ratio:6/1;object-fit:cover;border-radius:4px") - img.x-link(v-lazy="groupDialog.ref.bannerUrl" style="width:854px;height:480px" @click="showFullscreenImageDialog(groupDialog.ref.bannerUrl)") - .group-body(v-loading="groupDialog.loading") - div(style="display:flex") - el-popover(placement="right" width="500px" trigger="click") - img.x-link(slot="reference" v-lazy="groupDialog.ref.iconUrl" style="flex:none;width:120px;height:120px;border-radius:12px") - img.x-link(v-lazy="groupDialog.ref.iconUrl" style="width:500px;height:500px" @click="showFullscreenImageDialog(groupDialog.ref.iconUrl)") - div(style="flex:1;display:flex;align-items:center;margin-left:15px") - .group-header(style="flex:1") - span(v-if="groupDialog.ref.ownerId === API.currentUser.id" style="margin-right:5px") 👑 - span.dialog-title(v-text="groupDialog.ref.name" style="margin-right:5px") - span.group-discriminator.x-grey(style="font-family:monospace;font-size:12px;margin-right:5px") {{ groupDialog.ref.shortCode }}.{{ groupDialog.ref.discriminator }} - el-tooltip(v-for="item in groupDialog.ref.$languages" :key="item.key" placement="top") - template(#content) - span {{ item.value }} ({{ item.key }}) - span.flags(:class="languageClass(item.key)" style="display:inline-block;margin-right:5px") - div(style="margin-top:5px") - span.x-link.x-grey(v-text="groupDialog.ownerDisplayName" @click="showUserDialog(groupDialog.ref.ownerId)" style="font-family:monospace") - .group-tags - el-tag(v-if="groupDialog.ref.isVerified" type="info" effect="plain" size="mini" style="margin-right:5px;margin-top:5px") {{ $t('dialog.group.tags.verified') }} - - el-tag(v-if="groupDialog.ref.privacy === 'private'" type="danger" effect="plain" size="mini" style="margin-right:5px;margin-top:5px") {{ $t('dialog.group.tags.private') }} - el-tag(v-if="groupDialog.ref.privacy === 'default'" type="success" effect="plain" size="mini" style="margin-right:5px;margin-top:5px") {{ $t('dialog.group.tags.public') }} - - el-tag(v-if="groupDialog.ref.joinState === 'open'" type="success" effect="plain" size="mini" style="margin-right:5px;margin-top:5px") {{ $t('dialog.group.tags.open') }} - el-tag(v-else-if="groupDialog.ref.joinState === 'request'" type="warning" effect="plain" size="mini" style="margin-right:5px;margin-top:5px") {{ $t('dialog.group.tags.request') }} - el-tag(v-else-if="groupDialog.ref.joinState === 'invite'" type="danger" effect="plain" size="mini" style="margin-right:5px;margin-top:5px") {{ $t('dialog.group.tags.invite') }} - el-tag(v-else-if="groupDialog.ref.joinState === 'closed'" type="danger" effect="plain" size="mini" style="margin-right:5px;margin-top:5px") {{ $t('dialog.group.tags.closed') }} - - el-tag(v-if="groupDialog.inGroup" type="info" effect="plain" size="mini" style="margin-right:5px;margin-top:5px") {{ $t('dialog.group.tags.joined') }} - el-tag(v-if="groupDialog.ref.myMember && groupDialog.ref.myMember.bannedAt" type="danger" effect="plain" size="mini" style="margin-right:5px;margin-top:5px") {{ $t('dialog.group.tags.banned') }} - - template(v-if="groupDialog.inGroup && groupDialog.ref.myMember") - el-tag(v-if="groupDialog.ref.myMember.visibility === 'visible'" type="info" effect="plain" size="mini" style="margin-right:5px;margin-top:5px") {{ $t('dialog.group.tags.visible') }} - el-tag(v-else-if="groupDialog.ref.myMember.visibility === 'friends'" type="info" effect="plain" size="mini" style="margin-right:5px;margin-top:5px") {{ $t('dialog.group.tags.friends') }} - el-tag(v-else-if="groupDialog.ref.myMember.visibility === 'hidden'" type="info" effect="plain" size="mini" style="margin-right:5px;margin-top:5px") {{ $t('dialog.group.tags.hidden') }} - el-tag(v-if="groupDialog.ref.myMember.isSubscribedToAnnouncements" type="info" effect="plain" size="mini" style="margin-right:5px;margin-top:5px") {{ $t('dialog.group.tags.subscribed') }} - - .group-description(style="margin-top:5px") - span(v-show="groupDialog.ref.name !== groupDialog.ref.description" v-text="groupDialog.ref.description" style="font-size:12px") - div(style="flex:none;margin-left:10px") - template(v-if="groupDialog.inGroup && groupDialog.ref?.myMember") - el-tooltip(v-if="groupDialog.ref.myMember?.isRepresenting" placement="top" :content="$t('dialog.group.actions.unrepresent_tooltip')" :disabled="hideTooltips") - el-button(type="warning" icon="el-icon-star-on" circle @click="clearGroupRepresentation(groupDialog.id)" style="margin-left:5px") - el-tooltip(v-else placement="top" :content="$t('dialog.group.actions.represent_tooltip')" :disabled="hideTooltips") - span - el-button(type="default" icon="el-icon-star-off" circle @click="setGroupRepresentation(groupDialog.id)" style="margin-left:5px" :disabled="groupDialog.ref.privacy === 'private'") - template(v-else-if="groupDialog.ref.myMember?.membershipStatus === 'requested'") - el-tooltip(placement="top" :content="$t('dialog.group.actions.cancel_join_request_tooltip')" :disabled="hideTooltips") - span - el-button(type="default" icon="el-icon-close" circle @click="cancelGroupRequest(groupDialog.id)" style="margin-left:5px") - template(v-else-if="groupDialog.ref.myMember?.membershipStatus === 'invited'") - el-tooltip(placement="top" :content="$t('dialog.group.actions.pending_request_tooltip')" :disabled="hideTooltips") - span - el-button(type="default" icon="el-icon-check" circle @click="joinGroup(groupDialog.id)" style="margin-left:5px") - template(v-else) - el-tooltip(v-if="groupDialog.ref.joinState === 'request'" placement="top" :content="$t('dialog.group.actions.request_join_tooltip')" :disabled="hideTooltips") - el-button(type="default" icon="el-icon-message" circle @click="joinGroup(groupDialog.id)" style="margin-left:5px") - el-tooltip(v-if="groupDialog.ref.joinState === 'invite'" placement="top" :content="$t('dialog.group.actions.invite_required_tooltip')" :disabled="hideTooltips") - span - el-button(type="default" icon="el-icon-message" disabled circle style="margin-left:5px") - el-tooltip(v-if="groupDialog.ref.joinState === 'open'" placement="top" :content="$t('dialog.group.actions.join_group_tooltip')" :disabled="hideTooltips") - el-button(type="default" icon="el-icon-check" circle @click="joinGroup(groupDialog.id)" style="margin-left:5px") - el-dropdown(trigger="click" @command="groupDialogCommand" size="small" style="margin-left:5px") - el-button(:type="groupDialog.ref.membershipStatus === 'userblocked' ? 'danger' : 'default'" icon="el-icon-more" circle) - el-dropdown-menu(#default="dropdown") - el-dropdown-item(icon="el-icon-refresh" command="Refresh") {{ $t('dialog.group.actions.refresh') }} - template(v-if="groupDialog.inGroup") - template(v-if="groupDialog.ref.myMember") - el-dropdown-item(v-if="groupDialog.ref.myMember.isSubscribedToAnnouncements" icon="el-icon-close" command="Unsubscribe To Announcements" divided) {{ $t('dialog.group.actions.unsubscribe') }} - el-dropdown-item(v-else icon="el-icon-check" command="Subscribe To Announcements" divided) {{ $t('dialog.group.actions.subscribe') }} - el-dropdown-item(v-if="hasGroupPermission(groupDialog.ref, 'group-invites-manage')" icon="el-icon-message" command="Invite To Group") {{ $t('dialog.group.actions.invite_to_group') }} - template(v-if="hasGroupPermission(groupDialog.ref, 'group-announcement-manage')") - el-dropdown-item(icon="el-icon-tickets" command="Create Post") {{ $t('dialog.group.actions.create_post') }} - //- template(v-if="hasGroupPermission(groupDialog.ref, 'group-members-manage')") - el-dropdown-item(icon="el-icon-s-operation" command="Moderation Tools") {{ $t('dialog.group.actions.moderation_tools') }} - template(v-if="groupDialog.ref.myMember && groupDialog.ref.privacy === 'default'") - el-dropdown-item(icon="el-icon-view" command="Visibility Everyone" divided) #[i.el-icon-check(v-if="groupDialog.ref.myMember.visibility === 'visible'")] {{ $t('dialog.group.actions.visibility_everyone') }} - el-dropdown-item(icon="el-icon-view" command="Visibility Friends") #[i.el-icon-check(v-if="groupDialog.ref.myMember.visibility === 'friends'")] {{ $t('dialog.group.actions.visibility_friends') }} - el-dropdown-item(icon="el-icon-view" command="Visibility Hidden") #[i.el-icon-check(v-if="groupDialog.ref.myMember.visibility === 'hidden'")] {{ $t('dialog.group.actions.visibility_hidden') }} - el-dropdown-item(icon="el-icon-delete" command="Leave Group" style="color:#F56C6C" divided) {{ $t('dialog.group.actions.leave') }} - template(v-else) - el-dropdown-item(v-if="groupDialog.ref.membershipStatus === 'userblocked'" icon="el-icon-circle-check" command="Unblock Group" style="color:#F56C6C" divided) {{ $t('dialog.group.actions.unblock') }} - el-dropdown-item(v-else icon="el-icon-circle-close" command="Block Group" divided) {{ $t('dialog.group.actions.block') }} - el-tabs(ref="groupDialogTabs" @tab-click="groupDialogTabClick") - el-tab-pane(:label="$t('dialog.group.info.header')") - .group-banner-image-info - el-popover(placement="right" width="500px" trigger="click") - img.x-link(slot="reference" v-lazy="groupDialog.ref.bannerUrl" style="flex:none;width:100%;aspect-ratio:6/1;object-fit:cover;border-radius:4px") - img.x-link(v-lazy="groupDialog.ref.bannerUrl" style="width:854px;height:480px" @click="showFullscreenImageDialog(groupDialog.ref.bannerUrl)") - .x-friend-list(style="max-height:none") - span(v-if="groupDialog.instances.length" style="font-size:12px;font-weight:bold;margin:5px") {{ $t('dialog.group.info.instances') }} - div(v-for="room in groupDialog.instances" :key="room.tag" style="width:100%") - div(style="margin:5px 0") - location(:location="room.tag") - el-tooltip(placement="top" content="Invite yourself" :disabled="hideTooltips") - invite-yourself(:location="room.tag" style="margin-left:5px") - el-tooltip(placement="top" content="Refresh player count" :disabled="hideTooltips") - el-button(@click="refreshInstancePlayerCount(room.tag)" size="mini" icon="el-icon-refresh" style="margin-left:5px" circle) - last-join(:location="room.tag" :currentlocation="lastLocation.location") - instance-info(:location="room.tag" :instance="room.ref" :friendcount="room.friendCount" :updateelement="updateInstanceInfo") - .x-friend-list(style="margin:10px 0;padding:0;max-height:unset" v-if="room.users.length") - .x-friend-item(v-for="user in room.users" :key="user.id" @click="showUserDialog(user.id)" class="x-friend-item-border") - .avatar(:class="userStatusClass(user)") - img(v-lazy="userImage(user)") - .detail - span.name(v-text="user.displayName" :style="{'color':user.$userColour}") - span.extra(v-if="user.location === 'traveling'") - i.el-icon-loading(style="margin-right:5px") - timer(:epoch="user.$travelingToTime") - span.extra(v-else) - timer(:epoch="user.$location_at") - .x-friend-item(style="width:100%;cursor:default") - .detail - span.name {{ $t('dialog.group.info.announcement') }} - span(style="display:block" v-text="groupDialog.announcement.title") - div(v-if="groupDialog.announcement.imageUrl" style="display:inline-block;margin-right:5px") - el-popover(placement="right" width="500px" trigger="click") - img.x-link(slot="reference" v-lazy="groupDialog.announcement.imageUrl" style="flex:none;width:60px;height:60px;border-radius:4px;object-fit:cover") - img.x-link(v-lazy="groupDialog.announcement.imageUrl" style="height:500px" @click="showFullscreenImageDialog(groupDialog.announcement.imageUrl)") - pre.extra(style="display:inline-block;vertical-align:top;font-family:inherit;font-size:12px;white-space:pre-wrap;margin:0") {{ groupDialog.announcement.text || '-' }} - br - .extra(v-if="groupDialog.announcement.id" style="float:right;margin-left:5px") - el-tooltip(v-if="groupDialog.announcement.roleIds.length" placement="top") - template(#content) - span {{ $t('dialog.group.posts.visibility') }} - br - template(v-for="roleId in groupDialog.announcement.roleIds" :key="roleId") - span(v-for="(role, rIndex) in groupDialog.ref.roles" :key="rIndex" v-if="role.id === roleId" v-text="role.name") - span(v-if="groupDialog.announcement.roleIds.indexOf(roleId) < groupDialog.announcement.roleIds.length - 1") ,  - i.el-icon-view(style="margin-right:5px") - display-name(:userid="groupDialog.announcement.authorId" style="margin-right:5px") - span(v-if="groupDialog.announcement.editorId" style="margin-right:5px") ({{ $t('dialog.group.posts.edited_by') }} #[display-name(:userid="groupDialog.announcement.editorId")]) - el-tooltip(placement="bottom") - template(#content) - span {{ $t('dialog.group.posts.created_at') }} {{ groupDialog.announcement.createdAt | formatDate('long') }} - template(v-if="groupDialog.announcement.updatedAt !== groupDialog.announcement.createdAt") - br - span {{ $t('dialog.group.posts.edited_at') }} {{ groupDialog.announcement.updatedAt | formatDate('long') }} - timer(:epoch="Date.parse(groupDialog.announcement.updatedAt)") - template(v-if="hasGroupPermission(groupDialog.ref, 'group-announcement-manage')") - el-tooltip(placement="top" :content="$t('dialog.group.posts.edit_tooltip')" :disabled="hideTooltips") - el-button(type="text" icon="el-icon-edit" size="mini" style="margin-left:5px" @click="showGroupPostEditDialog(groupDialog.id, groupDialog.announcement)") - el-tooltip(placement="top" :content="$t('dialog.group.posts.delete_tooltip')" :disabled="hideTooltips") - el-button(type="text" icon="el-icon-delete" size="mini" style="margin-left:5px" @click="confirmDeleteGroupPost(groupDialog.announcement)") - .x-friend-item(style="width:100%;cursor:default") - .detail - span.name {{ $t('dialog.group.info.rules') }} - pre.extra(style="font-family:inherit;font-size:12px;white-space:pre-wrap;margin:0 0.5em 0 0") {{ groupDialog.ref.rules || '-' }} - .x-friend-item(style="cursor:default") - .detail - span.name {{ $t('dialog.group.info.members') }} - .extra {{ groupDialog.ref.memberCount }} ({{ groupDialog.ref.onlineMemberCount }}) - .x-friend-item(style="cursor:default") - .detail - span.name {{ $t('dialog.group.info.created_at') }} - span.extra {{ groupDialog.ref.createdAt | formatDate('long') }} - .x-friend-item(style="cursor:default") - .detail - span.name {{ $t('dialog.group.info.links') }} - div(v-if="groupDialog.ref.links && groupDialog.ref.links.length > 0" style="margin-top:5px") - el-tooltip(v-if="link" v-for="(link, index) in groupDialog.ref.links" :key="index") - template(#content) - span(v-text="link") - img(:src="getFaviconUrl(link)" style="width:16px;height:16px;vertical-align:middle;margin-right:5px;cursor:pointer" @click.stop="openExternalLink(link)") - .extra(v-else) - - .x-friend-item(style="width:350px;cursor:default") - .detail - span.name {{ $t('dialog.group.info.url') }} - span.extra {{ groupDialog.ref.$url }} - el-tooltip(placement="top" :content="$t('dialog.group.info.url_tooltip')" :disabled="hideTooltips") - el-button(type="default" @click="copyGroupUrl(groupDialog.ref.$url)" size="mini" icon="el-icon-s-order" circle style="margin-left:5px") - .x-friend-item(style="width:350px;cursor:default") - .detail - span.name {{ $t('dialog.group.info.id') }} - span.extra {{ groupDialog.id }} - el-tooltip(placement="top" :content="$t('dialog.group.info.id_tooltip')" :disabled="hideTooltips") - el-button(type="default" @click="copyGroupId(groupDialog.id)" size="mini" icon="el-icon-s-order" circle style="margin-left:5px") - div(v-if="groupDialog.ref.membershipStatus === 'member'" style="width:100%;margin-top:10px;border-top:1px solid #e4e7ed14") - div(style="width:100%;display:flex;margin-top:10px") - .x-friend-item(style="cursor:default") - .detail - span.name {{ $t('dialog.group.info.joined_at') }} - span.extra {{ groupDialog.ref.myMember.joinedAt | formatDate('long') }} - .x-friend-item(style="cursor:default") - .detail - span.name {{ $t('dialog.group.info.roles') }} - span.extra(v-if="groupDialog.memberRoles.length === 0") - - span.extra(v-else) - template(v-for="(role, rIndex) in groupDialog.memberRoles" :key="rIndex") - el-tooltip(placement="top") - template(#content) - span {{ $t('dialog.group.info.role') }} {{ role.name }} - br - span {{ $t('dialog.group.info.role_description') }} {{ role.description }} - br - span(v-if="role.updatedAt") {{ $t('dialog.group.info.role_updated_at') }} {{ role.updatedAt | formatDate('long') }} - span(v-else) {{ $t('dialog.group.info.role_created_at') }} {{ role.createdAt | formatDate('long') }} - br - span {{ $t('dialog.group.info.role_permissions') }} - br - template(v-for="(permission, pIndex) in role.permissions" :key="pIndex") - span {{ permission }} - br - span {{ role.name }}{{ rIndex < groupDialog.memberRoles.length - 1 ? ', ' : '' }} - el-tab-pane(:label="$t('dialog.group.posts.header')") - template(v-if="groupDialog.visible") - span(style="margin-right:10px") {{ $t('dialog.group.posts.posts_count') }} {{ groupDialog.posts.length }} - el-input(v-model="groupDialog.postsSearch" @input="updateGroupPostSearch" clearable size="mini" :placeholder="$t('dialog.group.posts.search_placeholder')" style="width:89%;margin-bottom:10px") - .x-friend-list - .x-friend-item(v-for="post in groupDialog.postsFiltered" :key="post.id" style="width:100%;cursor:default") - .detail - span(style="display:block" v-text="post.title") - div(v-if="post.imageUrl" style="display:inline-block;margin-right:5px") - el-popover(placement="right" width="500px" trigger="click") - img.x-link(slot="reference" v-lazy="post.imageUrl" style="flex:none;width:60px;height:60px;border-radius:4px;object-fit:cover") - img.x-link(v-lazy="post.imageUrl" style="height:500px" @click="showFullscreenImageDialog(post.imageUrl)") - pre.extra(style="display:inline-block;vertical-align:top;font-family:inherit;font-size:12px;white-space:pre-wrap;margin:0") {{ post.text || '-' }} - br - .extra(v-if="post.authorId" style="float:right;margin-left:5px") - el-tooltip(v-if="post.roleIds.length" placement="top") - template(#content) - span {{ $t('dialog.group.posts.visibility') }} - br - template(v-for="roleId in post.roleIds" :key="roleId") - span(v-for="(role, rIndex) in groupDialog.ref.roles" :key="rIndex" v-if="role.id === roleId" v-text="role.name") - span(v-if="post.roleIds.indexOf(roleId) < post.roleIds.length - 1") ,  - i.el-icon-view(style="margin-right:5px") - display-name(:userid="post.authorId" style="margin-right:5px") - span(v-if="post.editorId" style="margin-right:5px") ({{ $t('dialog.group.posts.edited_by') }} #[display-name(:userid="post.editorId")]) - el-tooltip(placement="bottom") - template(#content) - span {{ $t('dialog.group.posts.created_at') }} {{ post.createdAt | formatDate('long') }} - template(v-if="post.updatedAt !== post.createdAt") - br - span {{ $t('dialog.group.posts.edited_at') }} {{ post.updatedAt | formatDate('long') }} - timer(:epoch="Date.parse(post.updatedAt)") - template(v-if="hasGroupPermission(groupDialog.ref, 'group-announcement-manage')") - el-tooltip(placement="top" :content="$t('dialog.group.posts.edit_tooltip')" :disabled="hideTooltips") - el-button(type="text" icon="el-icon-edit" size="mini" style="margin-left:5px" @click="showGroupPostEditDialog(groupDialog.id, post)") - el-tooltip(placement="top" :content="$t('dialog.group.posts.delete_tooltip')" :disabled="hideTooltips") - el-button(type="text" icon="el-icon-delete" size="mini" style="margin-left:5px" @click="confirmDeleteGroupPost(post)") - el-tab-pane(:label="$t('dialog.group.members.header')") - template(v-if="groupDialog.visible") - span(v-if="hasGroupPermission(groupDialog.ref, 'group-members-viewall')" style="font-weight:bold;font-size:16px") {{ $t('dialog.group.members.all_members') }} - span(v-else style="font-weight:bold;font-size:16px") {{ $t('dialog.group.members.friends_only') }} - div(style="margin-top:10px") - el-button(type="default" @click="loadAllGroupMembers" size="mini" icon="el-icon-refresh" :loading="isGroupMembersLoading" circle) - el-button(type="default" @click="downloadAndSaveJson(`${groupDialog.id}_members`, groupDialog.members)" size="mini" icon="el-icon-download" circle style="margin-left:5px") - span(v-if="groupDialog.memberSearch.length" style="font-size:14px;margin-left:5px;margin-right:5px") {{ groupDialog.memberSearchResults.length }}/{{ groupDialog.ref.memberCount }} - span(v-else style="font-size:14px;margin-left:5px;margin-right:5px") {{ groupDialog.members.length }}/{{ groupDialog.ref.memberCount }} - div(v-if="hasGroupPermission(groupDialog.ref, 'group-members-manage')" style="float:right") - span(style="margin-right:5px") {{ $t('dialog.group.members.sort_by') }} - el-dropdown(@click.native.stop trigger="click" size="small" style="margin-right:5px" :disabled="isGroupMembersLoading || groupDialog.memberSearch.length") - el-button(size="mini") - span {{ groupDialog.memberSortOrder.name }} #[i.el-icon-arrow-down.el-icon--right] - el-dropdown-menu(#default="dropdown") - el-dropdown-item(v-for="(item) in groupDialogSortingOptions" v-text="item.name" @click.native="setGroupMemberSortOrder(item)") - span(style="margin-right:5px") {{ $t('dialog.group.members.filter') }} - el-dropdown(@click.native.stop trigger="click" size="small" style="margin-right:5px" :disabled="isGroupMembersLoading || groupDialog.memberSearch.length") - el-button(size="mini") - span {{ groupDialog.memberFilter.name }} #[i.el-icon-arrow-down.el-icon--right] - el-dropdown-menu(#default="dropdown") - el-dropdown-item(v-for="(item) in groupDialogFilterOptions" v-text="item.name" @click.native="setGroupMemberFilter(item)") - el-dropdown-item(v-for="(item) in groupDialog.ref.roles" v-if="!item.defaultRole" v-text="item.name" @click.native="setGroupMemberFilter(item)") - el-input(v-model="groupDialog.memberSearch" @input="groupMembersSearch" clearable size="mini" :placeholder="$t('dialog.group.members.search')" style="margin-top:10px;margin-bottom:10px") - .x-friend-list(v-if="groupDialog.memberSearch.length" v-loading="isGroupMembersLoading" style="margin-top:10px;overflow:auto;max-height:250px;min-width:130px") - .x-friend-item(v-for="user in groupDialog.memberSearchResults" :key="user.id" @click="showUserDialog(user.userId)" class="x-friend-item-border") - .avatar - img(v-lazy="userImage(user.user)") - .detail - span.name(v-text="user.user.displayName" :style="{'color':user.user.$userColour}") - span.extra - template(v-if="hasGroupPermission(groupDialog.ref, 'group-members-manage')") - el-tooltip(v-if="user.isRepresenting" placement="top" :content="$t('dialog.group.members.representing')") - i.el-icon-collection-tag(style="margin-right:5px") - el-tooltip(v-if="user.visibility !== 'visible'" placement="top") - template(#content) - span {{ $t('dialog.group.members.visibility') }} {{ user.visibility }} - i.el-icon-view(style="margin-right:5px") - el-tooltip(v-if="!user.isSubscribedToAnnouncements" placement="top" :content="$t('dialog.group.members.unsubscribed_announcements')") - i.el-icon-chat-line-square(style="margin-right:5px") - el-tooltip(v-if="user.managerNotes" placement="top") - template(#content) - span {{ $t('dialog.group.members.manager_notes') }} - br - span {{ user.managerNotes }} - i.el-icon-edit-outline(style="margin-right:5px") - template(v-for="roleId in user.roleIds" :key="roleId") - span(v-for="(role, rIndex) in groupDialog.ref.roles" :key="rIndex" v-if="role.id === roleId" v-text="role.name") - span(v-if="user.roleIds.indexOf(roleId) < user.roleIds.length - 1") ,  - ul.infinite-list.x-friend-list(v-else-if="groupDialog.members.length > 0" v-infinite-scroll="loadMoreGroupMembers" style="margin-top:10px;overflow:auto;max-height:250px;min-width:130px") - li.infinite-list-item.x-friend-item(v-for="user in groupDialog.members" :key="user.id" @click="showUserDialog(user.userId)" class="x-friend-item-border") - .avatar - img(v-lazy="userImage(user.user)") - .detail - span.name(v-text="user.user.displayName" :style="{'color':user.user.$userColour}") - span.extra - template(v-if="hasGroupPermission(groupDialog.ref, 'group-members-manage')") - el-tooltip(v-if="user.isRepresenting" placement="top" :content="$t('dialog.group.members.representing')") - i.el-icon-collection-tag(style="margin-right:5px") - el-tooltip(v-if="user.visibility !== 'visible'" placement="top") - template(#content) - span {{ $t('dialog.group.members.visibility') }} {{ user.visibility }} - i.el-icon-view(style="margin-right:5px") - el-tooltip(v-if="!user.isSubscribedToAnnouncements" placement="top" :content="$t('dialog.group.members.unsubscribed_announcements')") - i.el-icon-chat-line-square(style="margin-right:5px") - el-tooltip(v-if="user.managerNotes" placement="top") - template(#content) - span {{ $t('dialog.group.members.manager_notes') }} - br - span {{ user.managerNotes }} - i.el-icon-edit-outline(style="margin-right:5px") - template(v-for="roleId in user.roleIds" :key="roleId") - span(v-for="(role, rIndex) in groupDialog.ref.roles" :key="rIndex" v-if="role.id === roleId" v-text="role.name") - span(v-if="user.roleIds.indexOf(roleId) < user.roleIds.length - 1") ,  - .x-friend-item(v-if="!isGroupMembersDone" v-loading="isGroupMembersLoading" style="width:100%;height:45px;text-align:center" @click="loadMoreGroupMembers") - .detail(v-if="!isGroupMembersLoading") - span.name {{ $t('dialog.group.members.load_more') }} - el-tab-pane(:label="$t('dialog.group.gallery.header')") - el-button(type="default" size="mini" icon="el-icon-refresh" @click="getGroupGalleries" :loading="isGroupGalleryLoading" circle) - el-tabs(type="card" v-loading="isGroupGalleryLoading" ref="groupDialogGallery" style="margin-top:10px") - template(v-for="(gallery, index) in groupDialog.ref.galleries") - el-tab-pane - span(slot="label") - span(v-text="gallery.name" style="font-weight:bold;font-size:16px") - i.x-user-status(style="margin-left:5px" :class="groupGalleryStatus(gallery)") - span(style="color:#909399;font-size:12px;margin-left:5px") {{ groupDialog.galleries[gallery.id] ? groupDialog.galleries[gallery.id].length : 0 }} - span(v-text="gallery.description" style="color:#c7c7c7;padding:10px") - el-carousel(:interval="0" height="600px" style="margin-top:10px") - el-carousel-item(v-for="image in groupDialog.galleries[gallery.id]" :key="image.id") - el-popover(placement="top" width="700px" trigger="click") - img.x-link(slot="reference" v-lazy="image.imageUrl" style="width:100%;height:100%;object-fit:contain") - img.x-link(v-lazy="image.imageUrl" style="height:700px" @click="showFullscreenImageDialog(image.imageUrl)") - el-tab-pane(:label="$t('dialog.group.json.header')") - el-button(type="default" @click="refreshGroupDialogTreeData()" size="mini" icon="el-icon-refresh" circle) - el-button(type="default" @click="downloadAndSaveJson(groupDialog.id, groupDialog.ref)" size="mini" icon="el-icon-download" circle style="margin-left:5px") - el-tree(:data="groupDialog.treeData" style="margin-top:5px;font-size:12px") - template(#default="scope") - span - span(v-text="scope.data.key" style="font-weight:bold;margin-right:5px") - span(v-if="!scope.data.children" v-text="scope.data.value") - - //- dialog: favorite - el-dialog.x-dialog(:before-close="beforeDialogClose" @mousedown.native="dialogMouseDown" @mouseup.native="dialogMouseUp" ref="favoriteDialog" :visible.sync="favoriteDialog.visible" :title="$t('dialog.favorite.header')" width="300px") - div(v-if="favoriteDialog.visible" v-loading="favoriteDialog.loading") - span(style="display:block;text-align:center") {{ $t('dialog.favorite.vrchat_favorites') }} - template(v-if="favoriteDialog.currentGroup && favoriteDialog.currentGroup.key") - el-button(style="display:block;width:100%;margin:10px 0" @click="deleteFavoriteNoConfirm(favoriteDialog.objectId)") #[i.el-icon-check] {{ favoriteDialog.currentGroup.displayName }} ({{ favoriteDialog.currentGroup.count }} / {{ favoriteDialog.currentGroup.capacity }}) - template(v-else) - el-button(v-for="group in favoriteDialog.groups" :key="group" style="display:block;width:100%;margin:10px 0" @click="addFavorite(group)") {{ group.displayName }} ({{ group.count }} / {{ group.capacity }}) - div(v-if="favoriteDialog.visible && favoriteDialog.type === 'world'" style="margin-top:20px") - span(style="display:block;text-align:center") {{ $t('dialog.favorite.local_favorites') }} - template(v-for="group in localWorldFavoriteGroups" :key="group") - el-button(v-if="hasLocalWorldFavorite(favoriteDialog.objectId, group)" style="display:block;width:100%;margin:10px 0" @click="removeLocalWorldFavorite(favoriteDialog.objectId, group)") #[i.el-icon-check] {{ group }} ({{ getLocalWorldFavoriteGroupLength(group) }}) - el-button(v-else style="display:block;width:100%;margin:10px 0" @click="addLocalWorldFavorite(favoriteDialog.objectId, group)") {{ group }} ({{ getLocalWorldFavoriteGroupLength(group) }}) - div(v-if="favoriteDialog.visible && favoriteDialog.type === 'avatar'" style="margin-top:20px") - span(style="display:block;text-align:center") {{ $t('dialog.favorite.local_avatar_favorites') }} - template(v-for="group in localAvatarFavoriteGroups" :key="group") - el-button(v-if="hasLocalAvatarFavorite(favoriteDialog.objectId, group)" style="display:block;width:100%;margin:10px 0" @click="removeLocalAvatarFavorite(favoriteDialog.objectId, group)") #[i.el-icon-check] {{ group }} ({{ getLocalAvatarFavoriteGroupLength(group) }}) - el-button(v-else style="display:block;width:100%;margin:10px 0" :disabled="!isLocalUserVrcplusSupporter()" @click="addLocalAvatarFavorite(favoriteDialog.objectId, group)") {{ group }} ({{ getLocalAvatarFavoriteGroupLength(group) }}) - - //- dialog: invite - el-dialog.x-dialog(:before-close="beforeDialogClose" @mousedown.native="dialogMouseDown" @mouseup.native="dialogMouseUp" ref="inviteDialog" :visible.sync="inviteDialog.visible" :title="$t('dialog.invite.header')" width="500px") - div(v-if="inviteDialog.visible" v-loading="inviteDialog.loading") - location(:location="inviteDialog.worldId" :link="false") - br - el-button(size="mini" v-text="$t('dialog.invite.add_self')" @click="addSelfToInvite" style="margin-top:10px") - el-button(size="mini" v-text="$t('dialog.invite.add_friends_in_instance')" @click="addFriendsInInstanceToInvite" :disabled="inviteDialog.friendsInInstance.length === 0" style="margin-top:10px") - el-button(size="mini" v-text="$t('dialog.invite.add_favorite_friends')" @click="addFavoriteFriendsToInvite" :disabled="vipFriends.length === 0" style="margin-top:10px") - el-select(v-model="inviteDialog.userIds" multiple clearable :placeholder="$t('dialog.invite.select_placeholder')" filterable :disabled="inviteDialog.loading" style="width:100%;margin-top:15px") - el-option-group(v-if="API.currentUser" :label="$t('side_panel.me')") - el-option.x-friend-item(:label="API.currentUser.displayName" :value="API.currentUser.id" style="height:auto") - .avatar(:class="userStatusClass(API.currentUser)") - img(v-lazy="userImage(API.currentUser)") - .detail - span.name(v-text="API.currentUser.displayName") - el-option-group(v-if="inviteDialog.friendsInInstance.length" :label="$t('dialog.invite.friends_in_instance')") - el-option.x-friend-item(v-for="friend in inviteDialog.friendsInInstance" :key="friend.id" :label="friend.name" :value="friend.id" style="height:auto") - template(v-if="friend.ref") - .avatar(:class="userStatusClass(friend.ref)") - img(v-lazy="userImage(friend.ref)") - .detail - span.name(v-text="friend.ref.displayName" :style="{'color':friend.ref.$userColour}") - span(v-else v-text="friend.id") - el-option-group(v-if="vipFriends.length" :label="$t('side_panel.favorite')") - el-option.x-friend-item(v-for="friend in vipFriends" :key="friend.id" :label="friend.name" :value="friend.id" style="height:auto") - template(v-if="friend.ref") - .avatar(:class="userStatusClass(friend.ref)") - img(v-lazy="userImage(friend.ref)") - .detail - span.name(v-text="friend.ref.displayName" :style="{'color':friend.ref.$userColour}") - span(v-else v-text="friend.id") - el-option-group(v-if="onlineFriends.length" :label="$t('side_panel.online')") - el-option.x-friend-item(v-for="friend in onlineFriends" :key="friend.id" :label="friend.name" :value="friend.id" style="height:auto") - template(v-if="friend.ref") - .avatar(:class="userStatusClass(friend.ref)") - img(v-lazy="userImage(friend.ref)") - .detail - span.name(v-text="friend.ref.displayName" :style="{'color':friend.ref.$userColour}") - span(v-else v-text="friend.id") - el-option-group(v-if="activeFriends.length" :label="$t('side_panel.active')") - el-option.x-friend-item(v-for="friend in activeFriends" :key="friend.id" :label="friend.name" :value="friend.id" style="height:auto") - template(v-if="friend.ref") - .avatar - img(v-lazy="userImage(friend.ref)") - .detail - span.name(v-text="friend.ref.displayName" :style="{'color':friend.ref.$userColour}") - span(v-else v-text="friend.id") - template(#footer) - el-button(size="small" :disabled="inviteDialog.loading || !inviteDialog.userIds.length" @click="showSendInviteDialog()") {{ $t('dialog.invite.invite_with_message') }} - el-button(type="primary" size="small" :disabled="inviteDialog.loading || !inviteDialog.userIds.length" @click="sendInvite()") {{ $t('dialog.invite.invite') }} - - //- dialog: social status - el-dialog.x-dialog(:before-close="beforeDialogClose" @mousedown.native="dialogMouseDown" @mouseup.native="dialogMouseUp" ref="socialStatusDialog" :visible.sync="socialStatusDialog.visible" :title="$t('dialog.social_status.header')" width="400px") - div(v-loading="socialStatusDialog.loading") - el-collapse(style="border:0") - el-collapse-item - template(slot="title") - span(style="font-size:16px") {{ $t('dialog.social_status.history') }} - data-tables(v-bind="socialStatusHistoryTable" @row-click="setSocialStatusFromHistory" style="cursor:pointer") - el-table-column(:label="$t('table.social_status.no')" prop="no" width="50") - el-table-column(:label="$t('table.social_status.status')" prop="status") - el-select(v-model="socialStatusDialog.status" style="display:block;margin-top:10px") - el-option(:label="$t('dialog.user.status.join_me')" value="join me"). - #[i.x-user-status.joinme] {{ $t('dialog.user.status.join_me') }} - el-option(:label="$t('dialog.user.status.online')" value="active"). - #[i.x-user-status.online] {{ $t('dialog.user.status.online') }} - el-option(:label="$t('dialog.user.status.ask_me')" value="ask me"). - #[i.x-user-status.askme] {{ $t('dialog.user.status.ask_me') }} - el-option(:label="$t('dialog.user.status.busy')" value="busy"). - #[i.x-user-status.busy] {{ $t('dialog.user.status.busy') }} - el-option(v-if="API.currentUser.$isModerator" :label="$t('dialog.user.status.offline')" value="offline"). - #[i.x-user-status.offline] {{ $t('dialog.user.status.offline') }} - el-input(v-model="socialStatusDialog.statusDescription" :placeholder="$t('dialog.social_status.status_placeholder')" maxlength="32" show-word-limit style="display:block;margin-top:10px") - template(#footer) - el-button(type="primary" size="small" :disabled="socialStatusDialog.loading" @click="saveSocialStatus") {{ $t('dialog.social_status.update') }} - - //- dialog: language - el-dialog.x-dialog(:before-close="beforeDialogClose" @mousedown.native="dialogMouseDown" @mouseup.native="dialogMouseUp" ref="languageDialog" :visible.sync="languageDialog.visible" :title="$t('dialog.language.header')" width="400px") - div(v-loading="languageDialog.loading") - div(style="margin:5px 0") - el-tag(v-for="item in API.currentUser.$languages" :key="item.key" size="small" type="info" effect="plain" closable @close="removeUserLanguage(item.key)" style="margin-right:5px") - span.flags(:class="languageClass(item.key)" style="display:inline-block;margin-right:5px") - | {{ item.value }} ({{ item.key }}) - div(v-if="languageDialog.languageChoice === true") - el-select(v-model="languageDialog.languageValue" :placeholder="$t('dialog.language.select_language')" size="mini") - el-option(v-for="item in languageDialog.languages" :key="item.key" :value="item.key" :label="item.value") - span.flags(:class="languageClass(item.key)" style="display:inline-block;margin-right:5px") - | {{ item.value }} ({{ item.key }}) - el-button(@click="languageDialog.languageChoice=false; addUserLanguage(languageDialog.languageValue)" size="mini") {{ $t('dialog.language.ok') }} - el-button(@click="languageDialog.languageChoice=false" size="mini" style="margin-left:0") {{ $t('dialog.language.cancel') }} - div(v-else) - el-button(@click="languageDialog.languageValue='';languageDialog.languageChoice=true" size="mini") {{ $t('dialog.language.add_language') }} - - //- dialog: bio - el-dialog.x-dialog(:before-close="beforeDialogClose" @mousedown.native="dialogMouseDown" @mouseup.native="dialogMouseUp" ref="bioDialog" :visible.sync="bioDialog.visible" :title="$t('dialog.bio.header')" width="600px") - div(v-loading="bioDialog.loading") - el-input(type="textarea" v-model="bioDialog.bio" size="mini" maxlength="512" show-word-limit :autosize="{ minRows:2, maxRows:5 }" :placeholder="$t('dialog.bio.bio_placeholder')") - el-input(v-for="(link, index) in bioDialog.bioLinks" :key="index" :value="link" v-model="bioDialog.bioLinks[index]" size="small" style="margin-top:5px") - img(slot="prepend" :src="getFaviconUrl(link)" style="width:16px;height:16px") - el-button(slot="append" icon="el-icon-delete" @click="bioDialog.bioLinks.splice(index, 1)") - el-button(@click="bioDialog.bioLinks.push('')" :disabled="bioDialog.bioLinks.length >= 3" size="mini" style="margin-top:5px") {{ $t('dialog.bio.add_link') }} - template(#footer) - el-button(type="primary" size="small" :disabled="bioDialog.loading" @click="saveBio") {{ $t('dialog.bio.update') }} - - //- dialog: pronouns - el-dialog.x-dialog(:before-close="beforeDialogClose" @mousedown.native="dialogMouseDown" @mouseup.native="dialogMouseUp" ref="pronounsDialog" :visible.sync="pronounsDialog.visible" :title="$t('dialog.pronouns.header')" width="600px") - div(v-loading="pronounsDialog.loading") - el-input(type="textarea" v-model="pronounsDialog.pronouns" size="mini" maxlength="32" show-word-limit :autosize="{ minRows:2, maxRows:5 }" :placeholder="$t('dialog.pronouns.pronouns_placeholder')") - template(#footer) - el-button(type="primary" size="small" :disabled="pronounsDialog.loading" @click="savePronouns") {{ $t('dialog.pronouns.update') }} - - //- dialog: new instance - el-dialog.x-dialog(:before-close="beforeDialogClose" @mousedown.native="dialogMouseDown" @mouseup.native="dialogMouseUp" ref="newInstanceDialog" :visible.sync="newInstanceDialog.visible" :title="$t('dialog.new_instance.header')" width="650px") - el-tabs(type="card" v-model="newInstanceDialog.selectedTab") - el-tab-pane(:label="$t('dialog.new_instance.normal')") - el-form(v-if="newInstanceDialog.visible" :model="newInstanceDialog" label-width="150px") - el-form-item(:label="$t('dialog.new_instance.access_type')") - el-radio-group(v-model="newInstanceDialog.accessType" size="mini" @change="buildInstance") - el-radio-button(label="public") {{ $t('dialog.new_instance.access_type_public') }} - el-radio-button(label="group") {{ $t('dialog.new_instance.access_type_group') }} - el-radio-button(label="friends+") {{ $t('dialog.new_instance.access_type_friend_plus') }} - el-radio-button(label="friends") {{ $t('dialog.new_instance.access_type_friend') }} - el-radio-button(label="invite+") {{ $t('dialog.new_instance.access_type_invite_plus') }} - el-radio-button(label="invite") {{ $t('dialog.new_instance.access_type_invite') }} - el-form-item(:label="$t('dialog.new_instance.group_access_type')" v-if="newInstanceDialog.accessType === 'group'") - el-radio-group(v-model="newInstanceDialog.groupAccessType" size="mini" @change="buildInstance") - el-radio-button(label="members" :disabled="!hasGroupPermission(newInstanceDialog.groupRef, 'group-instance-open-create')") {{ $t('dialog.new_instance.group_access_type_members') }} - el-radio-button(label="plus" :disabled="!hasGroupPermission(newInstanceDialog.groupRef, 'group-instance-plus-create')") {{ $t('dialog.new_instance.group_access_type_plus') }} - el-radio-button(label="public" :disabled="!hasGroupPermission(newInstanceDialog.groupRef, 'group-instance-public-create') || newInstanceDialog.groupRef.privacy === 'private'") {{ $t('dialog.new_instance.group_access_type_public') }} - el-form-item(:label="$t('dialog.new_instance.region')") - el-radio-group(v-model="newInstanceDialog.region" size="mini" @change="buildInstance") - el-radio-button(label="US West") {{ $t('dialog.new_instance.region_usw') }} - el-radio-button(label="US East") {{ $t('dialog.new_instance.region_use') }} - el-radio-button(label="Europe") {{ $t('dialog.new_instance.region_eu') }} - el-radio-button(label="Japan") {{ $t('dialog.new_instance.region_jp') }} - el-form-item(:label="$t('dialog.new_instance.queueEnabled')" v-if="newInstanceDialog.accessType === 'group'") - el-checkbox(v-model="newInstanceDialog.queueEnabled" @change="buildInstance") - el-form-item(:label="$t('dialog.new_instance.world_id')") - el-input(v-model="newInstanceDialog.worldId" size="mini" @click.native="$event.target.tagName === 'INPUT' && $event.target.select()" @change="buildInstance") - el-form-item(:label="$t('dialog.new_instance.group_id')" v-if="newInstanceDialog.accessType === 'group'") - el-select(v-model="newInstanceDialog.groupId" clearable :placeholder="$t('dialog.new_instance.group_placeholder')" filterable style="width:100%" @change="buildInstance") - el-option-group(:label="$t('dialog.new_instance.group_placeholder')") - el-option.x-friend-item(v-if="group && (hasGroupPermission(group, 'group-instance-public-create') || hasGroupPermission(group, 'group-instance-plus-create') || hasGroupPermission(group, 'group-instance-open-create'))" v-for="group in API.currentUserGroups.values()" :key="group.id" :label="group.name" :value="group.id" style="height:auto;width:478px") - .avatar - img(v-lazy="group.iconUrl") - .detail - span.name(v-text="group.name") - el-form-item(:label="$t('dialog.new_instance.roles')" v-if="newInstanceDialog.accessType === 'group' && newInstanceDialog.groupAccessType === 'members'") - el-select(v-model="newInstanceDialog.roleIds" multiple clearable :placeholder="$t('dialog.new_instance.role_placeholder')" style="width:100%" @change="buildInstance") - el-option-group(:label="$t('dialog.new_instance.role_placeholder')") - el-option.x-friend-item(v-for="role in newInstanceDialog.selectedGroupRoles" :key="role.id" :label="role.name" :value="role.id" style="height:auto;width:478px") - .detail - span.name(v-text="role.name") - template(v-if="newInstanceDialog.instanceCreated") - el-form-item(:label="$t('dialog.new_instance.location')") - el-input(v-model="newInstanceDialog.location" size="mini" readonly @click.native="$event.target.tagName === 'INPUT' && $event.target.select()") - el-form-item(:label="$t('dialog.new_instance.url')") - el-input(v-model="newInstanceDialog.url" size="mini" readonly) - el-tab-pane(:label="$t('dialog.new_instance.legacy')") - el-form(v-if="newInstanceDialog.visible" :model="newInstanceDialog" label-width="150px") - el-form-item(:label="$t('dialog.new_instance.access_type')") - el-radio-group(v-model="newInstanceDialog.accessType" size="mini" @change="buildLegacyInstance") - el-radio-button(label="public") {{ $t('dialog.new_instance.access_type_public') }} - el-radio-button(label="group") {{ $t('dialog.new_instance.access_type_group') }} - el-radio-button(label="friends+") {{ $t('dialog.new_instance.access_type_friend_plus') }} - el-radio-button(label="friends") {{ $t('dialog.new_instance.access_type_friend') }} - el-radio-button(label="invite+") {{ $t('dialog.new_instance.access_type_invite_plus') }} - el-radio-button(label="invite") {{ $t('dialog.new_instance.access_type_invite') }} - el-form-item(:label="$t('dialog.new_instance.group_access_type')" v-if="newInstanceDialog.accessType === 'group'") - el-radio-group(v-model="newInstanceDialog.groupAccessType" size="mini" @change="buildLegacyInstance") - el-radio-button(label="members") {{ $t('dialog.new_instance.group_access_type_members') }} - el-radio-button(label="plus") {{ $t('dialog.new_instance.group_access_type_plus') }} - el-radio-button(label="public") {{ $t('dialog.new_instance.group_access_type_public') }} - //- el-form-item(label="Strict" v-if="newInstanceDialog.accessType === 'friends' || newInstanceDialog.accessType === 'invite'") - //- el-checkbox(v-model="newInstanceDialog.strict") Prevent non friends joining via URL/Instance ID - el-form-item(:label="$t('dialog.new_instance.region')") - el-radio-group(v-model="newInstanceDialog.region" size="mini" @change="buildLegacyInstance") - el-radio-button(label="US West") {{ $t('dialog.new_instance.region_usw') }} - el-radio-button(label="US East") {{ $t('dialog.new_instance.region_use') }} - el-radio-button(label="Europe") {{ $t('dialog.new_instance.region_eu') }} - el-radio-button(label="Japan") {{ $t('dialog.new_instance.region_jp') }} - el-form-item(:label="$t('dialog.new_instance.world_id')") - el-input(v-model="newInstanceDialog.worldId" size="mini" @click.native="$event.target.tagName === 'INPUT' && $event.target.select()" @change="buildLegacyInstance") - el-form-item(:label="$t('dialog.new_instance.instance_id')") - el-input(v-model="newInstanceDialog.instanceName" :placeholder="$t('dialog.new_instance.instance_id_placeholder')" size="mini" @change="buildLegacyInstance") - el-form-item(:label="$t('dialog.new_instance.instance_creator')" v-if="newInstanceDialog.accessType !== 'public' && newInstanceDialog.accessType !== 'group'") - el-select(v-model="newInstanceDialog.userId" clearable :placeholder="$t('dialog.new_instance.instance_creator_placeholder')" filterable style="width:100%" @change="buildLegacyInstance") - el-option-group(v-if="API.currentUser" :label="$t('side_panel.me')") - el-option.x-friend-item(:label="API.currentUser.displayName" :value="API.currentUser.id" style="height:auto") - .avatar(:class="userStatusClass(API.currentUser)") - img(v-lazy="userImage(API.currentUser)") - .detail - span.name(v-text="API.currentUser.displayName") - el-option-group(v-if="vipFriends.length" :label="$t('side_panel.favorite')") - el-option.x-friend-item(v-for="friend in vipFriends" :key="friend.id" :label="friend.name" :value="friend.id" style="height:auto") - template(v-if="friend.ref") - .avatar(:class="userStatusClass(friend.ref)") - img(v-lazy="userImage(friend.ref)") - .detail - span.name(v-text="friend.ref.displayName" :style="{'color':friend.ref.$userColour}") - span(v-else v-text="friend.id") - el-option-group(v-if="onlineFriends.length" :label="$t('side_panel.online')") - el-option.x-friend-item(v-for="friend in onlineFriends" :key="friend.id" :label="friend.name" :value="friend.id" style="height:auto") - template(v-if="friend.ref") - .avatar(:class="userStatusClass(friend.ref)") - img(v-lazy="userImage(friend.ref)") - .detail - span.name(v-text="friend.ref.displayName" :style="{'color':friend.ref.$userColour}") - span(v-else v-text="friend.id") - el-option-group(v-if="activeFriends.length" :label="$t('side_panel.active')") - el-option.x-friend-item(v-for="friend in activeFriends" :key="friend.id" :label="friend.name" :value="friend.id" style="height:auto") - template(v-if="friend.ref") - .avatar - img(v-lazy="userImage(friend.ref)") - .detail - span.name(v-text="friend.ref.displayName" :style="{'color':friend.ref.$userColour}") - span(v-else v-text="friend.id") - el-option-group(v-if="offlineFriends.length" :label="$t('side_panel.offline')") - el-option.x-friend-item(v-for="friend in offlineFriends" :key="friend.id" :label="friend.name" :value="friend.id" style="height:auto") - template(v-if="friend.ref") - .avatar - img(v-lazy="userImage(friend.ref)") - .detail - span.name(v-text="friend.ref.displayName" :style="{'color':friend.ref.$userColour}") - span(v-else v-text="friend.id") - el-form-item(:label="$t('dialog.new_instance.group_id')" v-if="newInstanceDialog.accessType === 'group'") - el-select(v-model="newInstanceDialog.groupId" clearable :placeholder="$t('dialog.new_instance.group_placeholder')" filterable style="width:100%" @change="buildLegacyInstance") - el-option-group(:label="$t('dialog.new_instance.group_placeholder')") - el-option.x-friend-item(v-if="group" v-for="group in API.currentUserGroups.values()" :key="group.id" :label="group.name" :value="group.id" style="height:auto;width:478px") - .avatar - img(v-lazy="group.iconUrl") - .detail - span.name(v-text="group.name") - el-form-item(:label="$t('dialog.new_instance.location')") - el-input(v-model="newInstanceDialog.location" size="mini" readonly @click.native="$event.target.tagName === 'INPUT' && $event.target.select()") - el-form-item(:label="$t('dialog.new_instance.url')") - el-input(v-model="newInstanceDialog.url" size="mini" readonly) - template(#footer v-if="newInstanceDialog.selectedTab === '0'") - template(v-if="newInstanceDialog.instanceCreated") - el-button(size="small" @click="copyInstanceUrl(newInstanceDialog.location)") {{ $t('dialog.new_instance.copy_url') }} - el-button(size="small" @click="selfInvite(newInstanceDialog.location)") {{ $t('dialog.new_instance.self_invite') }} - el-button(size="small" @click="showInviteDialog(newInstanceDialog.location)" :disabled="(newInstanceDialog.accessType === 'friends' || newInstanceDialog.accessType === 'invite') && newInstanceDialog.userId !== API.currentUser.id") {{ $t('dialog.new_instance.invite') }} - el-button(type="primary" size="small" @click="showLaunchDialog(newInstanceDialog.location, newInstanceDialog.shortName)") {{ $t('dialog.new_instance.launch') }} - template(v-else) - el-button(type="primary" size="small" @click="createNewInstance()") {{ $t('dialog.new_instance.create_instance') }} - template(#footer v-else-if="newInstanceDialog.selectedTab === '1'") - el-button(size="small" @click="copyInstanceUrl(newInstanceDialog.location)") {{ $t('dialog.new_instance.copy_url') }} - el-button(size="small" @click="selfInvite(newInstanceDialog.location)") {{ $t('dialog.new_instance.self_invite') }} - el-button(size="small" @click="showInviteDialog(newInstanceDialog.location)" :disabled="(newInstanceDialog.accessType === 'friends' || newInstanceDialog.accessType === 'invite') && newInstanceDialog.userId !== API.currentUser.id") {{ $t('dialog.new_instance.invite') }} - el-button(type="primary" size="small" @click="showLaunchDialog(newInstanceDialog.location, newInstanceDialog.shortName)") {{ $t('dialog.new_instance.launch') }} - - //- dialog: launch options - el-dialog.x-dialog(:before-close="beforeDialogClose" @mousedown.native="dialogMouseDown" @mouseup.native="dialogMouseUp" ref="launchOptionsDialog" :visible.sync="launchOptionsDialog.visible" :title="$t('dialog.launch_options.header')" width="600px") - div(style="font-size:12px") - | {{ $t('dialog.launch_options.description') }} #[br] - | {{ $t('dialog.launch_options.example') }} #[el-tag(size="mini") --fps=144] - el-input(type="textarea" v-model="launchOptionsDialog.launchArguments" size="mini" show-word-limit :autosize="{ minRows:2, maxRows:5 }" placeholder="" style="margin-top:10px") - div(style="font-size:12px;margin-top:10px") - | {{ $t('dialog.launch_options.path_override') }} - el-input(type="textarea" v-model="launchOptionsDialog.vrcLaunchPathOverride" placeholder="C:\\Program Files (x86)\\Steam\\steamapps\\common\\VRChat" :rows="1" style="display:block;margin-top:10px") - template(#footer) - div(style="display:flex") - el-button(size="small" @click="openExternalLink('https://docs.vrchat.com/docs/launch-options')") {{ $t('dialog.launch_options.vrchat_docs') }} - el-button(size="small" @click="openExternalLink('https://docs.unity3d.com/Manual/CommandLineArguments.html')") {{ $t('dialog.launch_options.unity_manual') }} - el-button(type="primary" size="small" :disabled="launchOptionsDialog.loading" @click="updateLaunchOptions" style="margin-left:auto") {{ $t('dialog.launch_options.save') }} - - //- dialog: VRChat Config JSON - el-dialog.x-dialog(:before-close="beforeDialogClose" @mousedown.native="dialogMouseDown" @mouseup.native="dialogMouseUp" ref="VRChatConfigDialog" :visible.sync="VRChatConfigDialog.visible" :title="$t('dialog.config_json.header')" width="420px") - div(style='font-size:12px;word-break:keep-all') - | {{ $t('dialog.config_json.description1') }} #[br] - | {{ $t('dialog.config_json.description2') }} - br - span(style="margin-right:5px") {{ $t('dialog.config_json.cache_size') }} - span(v-text="VRChatUsedCacheSize") - span / - span(v-text="VRChatTotalCacheSize") - span GB - el-tooltip(placement="top" :content="$t('dialog.config_json.refresh')" :disabled="hideTooltips") - el-button(type="default" :loading="VRChatCacheSizeLoading" @click="getVRChatCacheSize" size="small" icon="el-icon-refresh" circle style="margin-left:5px") - br - span {{ $t('dialog.config_json.delete_all_cache') }} - el-button(size="small" style="margin-left:5px" icon="el-icon-delete" @click="showDeleteAllVRChatCacheConfirm()") {{ $t('dialog.config_json.delete_cache') }} - br - span {{ $t('dialog.config_json.delete_old_cache') }} - el-button(size="small" style="margin-left:5px" icon="el-icon-folder-delete" @click="sweepVRChatCache()") {{ $t('dialog.config_json.sweep_cache') }} - br - div(style="display:inline-block;margin-top:10px" v-for="(item, value) in VRChatConfigList" :key="value") - span(v-text="item.name" style="word-break:keep-all") - |: - el-input(v-model="VRChatConfigFile[value]" :placeholder="item.default" size="mini" :type="item.type?item.type:'text'" :min="item.min" :max="item.max") - div(style="display:inline-block;margin-top:10px") - span {{ $t('dialog.config_json.camera_resolution') }} - br - el-dropdown(@command="(command) => setVRChatCameraResolution(command)" size="small" trigger="click" style="margin-top:5px") - el-button(size="small") - span #[span(v-text="getVRChatCameraResolution()")] #[i.el-icon-arrow-down.el-icon--right] - el-dropdown-menu(#default="dropdown") - el-dropdown-item(v-for="row in VRChatCameraResolutions" :key="row.index" v-text="row.name" :command="row") - div(style="display:inline-block;margin-top:10px;margin-left:10px") - span {{ $t('dialog.config_json.screenshot_resolution') }} - br - el-dropdown(@command="(command) => setVRChatScreenshotResolution(command)" size="small" trigger="click" style="margin-top:5px") - el-button(size="small") - span #[span(v-text="getVRChatScreenshotResolution()")] #[i.el-icon-arrow-down.el-icon--right] - el-dropdown-menu(#default="dropdown") - el-dropdown-item(v-for="row in VRChatScreenshotResolutions" :key="row.index" v-text="row.name" :command="row") - el-checkbox(v-model="VRChatConfigFile.picture_output_split_by_date" style="margin-top:5px;display:block" :checked="true") {{ $t('dialog.config_json.picture_sort_by_date') }} - el-checkbox(v-model="VRChatConfigFile.disableRichPresence" style="margin-top:5px;display:block") {{ $t('dialog.config_json.disable_discord_presence') }} - template(#footer) - el-button(size="small" @click="openExternalLink('https://docs.vrchat.com/docs/configuration-file')") {{ $t('dialog.config_json.vrchat_docs') }} - el-button(size="small" @click="VRChatConfigDialog.visible = false") {{ $t('dialog.config_json.cancel') }} - el-button(type="primary" size="small" :disabled="VRChatConfigDialog.loading" @click="saveVRChatConfigFile") {{ $t('dialog.config_json.save') }} - - //- dialog: YouTube Api Dialog - el-dialog.x-dialog(:before-close="beforeDialogClose" @mousedown.native="dialogMouseDown" @mouseup.native="dialogMouseUp" ref="youTubeApiDialog" :visible.sync="youTubeApiDialog.visible" :title="$t('dialog.youtube_api.header')" width="400px") - div(style='font-size:12px;') - | {{ $t('dialog.youtube_api.description') }} #[br] - el-input(type="textarea" v-model="youTubeApiKey" :placeholder="$t('dialog.youtube_api.placeholder')" maxlength="39" show-word-limit style="display:block;margin-top:10px") - template(#footer) - div(style="display:flex") - el-button(size="small" @click="openExternalLink('https://rapidapi.com/blog/how-to-get-youtube-api-key/')") {{ $t('dialog.youtube_api.guide') }} - el-button(type="primary" size="small" @click="testYouTubeApiKey" style="margin-left:auto") {{ $t('dialog.youtube_api.save') }} - - //- dialog: Set World Tags - el-dialog.x-dialog(:before-close="beforeDialogClose" @mousedown.native="dialogMouseDown" @mouseup.native="dialogMouseUp" ref="setWorldTagsDialog" :visible.sync="setWorldTagsDialog.visible" :title="$t('dialog.set_world_tags.header')" width="400px") - el-checkbox(v-model="setWorldTagsDialog.avatarScalingDisabled") {{ $t('dialog.set_world_tags.avatar_scaling_disabled') }} - br - el-checkbox(v-model="setWorldTagsDialog.focusViewDisabled") {{ $t('dialog.set_world_tags.focus_view_disabled') }} - br - el-checkbox(v-model="setWorldTagsDialog.stickersDisabled") {{ $t('dialog.set_world_tags.stickers_disabled') }} - br - el-checkbox(v-model="setWorldTagsDialog.debugAllowed") {{ $t('dialog.set_world_tags.enable_debugging') }} - div(style='font-size:12px;margin-top:10px') - | {{ $t('dialog.set_world_tags.author_tags') }} #[br] - el-input(type="textarea" v-model="setWorldTagsDialog.authorTags" size="mini" show-word-limit :autosize="{ minRows:2, maxRows:5 }" placeholder="" style="margin-top:10px") - div(style='font-size:12px;margin-top:10px') - | {{ $t('dialog.set_world_tags.content_tags') }} #[br] - el-checkbox(v-model="setWorldTagsDialog.contentHorror") {{ $t('dialog.set_world_tags.content_horror') }} - br - el-checkbox(v-model="setWorldTagsDialog.contentGore") {{ $t('dialog.set_world_tags.content_gore') }} - br - el-checkbox(v-model="setWorldTagsDialog.contentViolence") {{ $t('dialog.set_world_tags.content_violence') }} - br - el-checkbox(v-model="setWorldTagsDialog.contentAdult") {{ $t('dialog.set_world_tags.content_adult') }} - br - el-checkbox(v-model="setWorldTagsDialog.contentSex") {{ $t('dialog.set_world_tags.content_sex') }} - //- el-input(type="textarea" v-model="setWorldTagsDialog.contentTags" size="mini" show-word-limit :autosize="{ minRows:2, maxRows:5 }" placeholder="" style="margin-top:10px") - template(#footer) - div(style="display:flex") - el-button(size="small" @click="setWorldTagsDialog.visible = false") {{ $t('dialog.set_world_tags.cancel') }} - el-button(type="primary" size="small" @click="saveSetWorldTagsDialog") {{ $t('dialog.set_world_tags.save') }} - - //- dialog: Set Avatar Tags - el-dialog.x-dialog(:before-close="beforeDialogClose" @mousedown.native="dialogMouseDown" @mouseup.native="dialogMouseUp" ref="setAvatarTagsDialog" :visible.sync="setAvatarTagsDialog.visible" :title="$t('dialog.set_avatar_tags.header')" width="770px") - template(v-if="setAvatarTagsDialog.visible") - el-checkbox(v-model="setAvatarTagsDialog.contentHorror" @change="updateSelectedAvatarTags") {{ $t('dialog.set_avatar_tags.content_horror') }} - br - el-checkbox(v-model="setAvatarTagsDialog.contentGore" @change="updateSelectedAvatarTags") {{ $t('dialog.set_avatar_tags.content_gore') }} - br - el-checkbox(v-model="setAvatarTagsDialog.contentViolence" @change="updateSelectedAvatarTags") {{ $t('dialog.set_avatar_tags.content_violence') }} - br - el-checkbox(v-model="setAvatarTagsDialog.contentAdult" @change="updateSelectedAvatarTags") {{ $t('dialog.set_avatar_tags.content_adult') }} - br - el-checkbox(v-model="setAvatarTagsDialog.contentSex" @change="updateSelectedAvatarTags") {{ $t('dialog.set_avatar_tags.content_sex') }} - br - el-input(v-model="setAvatarTagsDialog.selectedTagsCsv" @input="updateInputAvatarTags" size="mini" :autosize="{ minRows:2, maxRows:5 }" :placeholder="$t('dialog.set_avatar_tags.custom_tags_placeholder')" style="margin-top:10px") - template(v-if="setAvatarTagsDialog.ownAvatars.length === setAvatarTagsDialog.selectedCount") - el-button(size="small" @click="setAvatarTagsSelectToggle") {{ $t('dialog.set_avatar_tags.select_none') }} - template(v-else) - el-button(size="small" @click="setAvatarTagsSelectToggle") {{ $t('dialog.set_avatar_tags.select_all') }} - span(style="margin-left:5px") {{ setAvatarTagsDialog.selectedCount }} / {{ setAvatarTagsDialog.ownAvatars.length }} - span(v-if="setAvatarTagsDialog.loading" style="margin-left:5px") - i.el-icon-loading - br - .x-friend-list(style="margin-top:10px;min-height:60px;max-height:280px") - .x-friend-item(v-for="avatar in setAvatarTagsDialog.ownAvatars" :key="setAvatarTagsDialog.forceUpdate" @click="showAvatarDialog(avatar.id)" class="x-friend-item-border" style="width:350px") - .avatar - img(v-if="avatar.thumbnailImageUrl" v-lazy="avatar.thumbnailImageUrl") - .detail - span.name(v-text="avatar.name") - span.extra(v-text="avatar.releaseStatus" v-if="avatar.releaseStatus === 'public'" style="color: #67c23a;") - span.extra(v-text="avatar.releaseStatus" v-else-if="avatar.releaseStatus === 'private'" style="color: #f56c6c;") - span.extra(v-text="avatar.releaseStatus" v-else) - span.extra(v-text="avatar.$tagString") - el-button(type="text" size="mini" @click.stop style="margin-left:5px") - el-checkbox(v-model="avatar.$selected" @change="updateAvatarTagsSelection") - template(#footer) - el-button(size="small" @click="setAvatarTagsDialog.visible = false") {{ $t('dialog.set_avatar_tags.cancel') }} - el-button(type="primary" size="small" @click="saveSetAvatarTagsDialog") {{ $t('dialog.set_avatar_tags.save') }} - - //- dialog: Cache Download - el-dialog.x-dialog(:before-close="beforeDialogClose" @mousedown.native="dialogMouseDown" @mouseup.native="dialogMouseUp" ref="downloadDialog" :visible.sync="downloadDialog.visible" :title="$t('dialog.download_history.header')" width="770px") - template(v-if="downloadDialog.visible") - div(v-if="downloadInProgress && downloadCurrent.ref") - span(v-text="downloadCurrent.ref.name") - el-button(type="text" icon="el-icon-close" size="mini" @click="cancelDownload(downloadCurrent.id)" style="margin-left:5px") - el-progress(:percentage="downloadProgress" :format="downloadProgressText") - template(v-if="downloadQueueTable.data.length >= 1") - span(style="margin-top:15px") {{ $t('dialog.download_history.queue') }} - data-tables(v-bind="downloadQueueTable" style="margin-top:10px") - el-table-column(:label="$t('table.download_history.name')" prop="name") - el-table-column(:label="$t('table.download_history.type')" prop="type" width="70") - el-table-column(:label="$t('table.download_history.cancel')" width="60" align="right") - template(v-once #default="scope") - el-button(type="text" icon="el-icon-close" size="mini" @click="cancelDownload(scope.row.ref.id)") - span(style="margin-top:15px") {{ $t('dialog.download_history.history') }} - data-tables(v-bind="downloadHistoryTable" style="margin-top:10px") - el-table-column(:label="$t('table.download_history.time')" prop="date" width="90") - template(v-once #default="scope") - timer(:epoch="scope.row.date") - el-table-column(:label="$t('table.download_history.name')" prop="name") - template(v-once #default="scope") - span(v-text="scope.row.ref.name") - el-table-column(:label="$t('table.download_history.type')" prop="type" width="70") - el-table-column(:label="$t('table.download_history.status')" prop="status" width="80") - template(#footer) - el-button(v-if="downloadQueue.size >= 1" size="small" @click="cancelAllDownloads") {{ $t('dialog.download_history.cancel_all') }} - el-button(size="small" @click="downloadDialog.visible = false") {{ $t('dialog.download_history.close') }} - - //- dialog: update VRCX - el-dialog.x-dialog(:before-close="beforeDialogClose" @mousedown.native="dialogMouseDown" @mouseup.native="dialogMouseUp" ref="VRCXUpdateDialog" :visible.sync="VRCXUpdateDialog.visible" :title="$t('dialog.vrcx_updater.header')" width="400px") - div(v-loading="checkingForVRCXUpdate" style="margin-top:15px") - div(v-if="VRCXUpdateDialog.updatePending" style="margin-bottom:15px") - span(v-text="pendingVRCXInstall") - br - span {{ $t('dialog.vrcx_updater.ready_for_update') }} - el-select(v-model="branch" @change="loadBranchVersions" style="display:inline-block;width:150px;margin-right:15px") - el-option(v-once v-for="branch in branches" :key="branch.name" :label="branch.name" :value="branch.name") - el-select(v-model="VRCXUpdateDialog.release" style="display:inline-block;width:150px") - el-option(v-for="item in VRCXUpdateDialog.releases" :key="item.name" :label="item.tag_name" :value="item.name") - div(v-if="!VRCXUpdateDialog.updatePending && VRCXUpdateDialog.release === appVersion" style="margin-top:15px") - span {{ $t('dialog.vrcx_updater.latest_version') }} - template(#footer) - el-button(v-if="(VRCXUpdateDialog.updatePending && VRCXUpdateDialog.release !== pendingVRCXInstall) || VRCXUpdateDialog.release !== appVersion" type="primary" size="small" @click="installVRCXUpdate") {{ $t('dialog.vrcx_updater.download') }} - el-button(v-if="VRCXUpdateDialog.updatePending" type="primary" size="small" @click="restartVRCX(true)") {{ $t('dialog.vrcx_updater.install') }} - - //- dialog: launch - el-dialog.x-dialog(:before-close="beforeDialogClose" @mousedown.native="dialogMouseDown" @mouseup.native="dialogMouseUp" ref="launchDialog" :visible.sync="launchDialog.visible" :title="$t('dialog.launch.header')" width="450px") - el-form(:model="launchDialog" label-width="80px") - el-form-item(:label="$t('dialog.launch.url')") - el-input(v-model="launchDialog.url" size="mini" @click.native="$event.target.tagName === 'INPUT' && $event.target.select()" style="width:260px") - el-tooltip(placement="right" :content="$t('dialog.launch.copy_tooltip')" :disabled="hideTooltips") - el-button(@click="copyInstanceMessage(launchDialog.url)" size="mini" icon="el-icon-s-order" style="margin-right:5px" circle) - el-form-item(v-if="launchDialog.shortUrl" :label="$t('dialog.launch.short_url')") - el-tooltip(placement="top" style="margin-left:5px" :content="$t('dialog.launch.short_url_notice')") - i.el-icon-warning - el-input(v-model="launchDialog.shortUrl" size="mini" @click.native="$event.target.tagName === 'INPUT' && $event.target.select()" style="width:241px") - el-tooltip(placement="right" :content="$t('dialog.launch.copy_tooltip')" :disabled="hideTooltips") - el-button(@click="copyInstanceMessage(launchDialog.shortUrl)" size="mini" icon="el-icon-s-order" style="margin-right:5px" circle) - el-form-item(:label="$t('dialog.launch.location')") - el-input(v-model="launchDialog.location" size="mini" @click.native="$event.target.tagName === 'INPUT' && $event.target.select()" style="width:260px") - el-tooltip(placement="right" :content="$t('dialog.launch.copy_tooltip')" :disabled="hideTooltips") - el-button(@click="copyInstanceMessage(launchDialog.location)" size="mini" icon="el-icon-s-order" style="margin-right:5px" circle) - template(#footer) - el-checkbox(v-model="launchDialog.desktop" @change="saveLaunchDialog" style="float:left;margin-top:5px") {{ $t('dialog.launch.start_as_desktop') }} - el-button(size="small" @click="showPreviousInstanceInfoDialog(launchDialog.location)") {{ $t('dialog.launch.info') }} - el-button(size="small" @click="showInviteDialog(launchDialog.location)" :disabled="!checkCanInvite(launchDialog.location)") {{ $t('dialog.launch.invite') }} - el-button(type="primary" size="small" @click="launchGame(launchDialog.location, launchDialog.shortName, launchDialog.desktop)" :disabled="!launchDialog.secureOrShortName") {{ $t('dialog.launch.launch') }} - - //- dialog: export friends list - el-dialog.x-dialog(:before-close="beforeDialogClose" @mousedown.native="dialogMouseDown" @mouseup.native="dialogMouseUp" :visible.sync="exportFriendsListDialog" :title="$t('dialog.export_friends_list.header')" width="650px") - el-tabs(type="card") - el-tab-pane(:label="$t('dialog.export_friends_list.csv')") - el-input(type="textarea" v-if="exportFriendsListDialog" v-model="exportFriendsListCsv" size="mini" rows="15" resize="none" readonly style="margin-top:15px" @click.native="$event.target.tagName === 'TEXTAREA' && $event.target.select()") - el-tab-pane(:label="$t('dialog.export_friends_list.json')") - el-input(type="textarea" v-if="exportFriendsListDialog" v-model="exportFriendsListJson" size="mini" rows="15" resize="none" readonly style="margin-top:15px" @click.native="$event.target.tagName === 'TEXTAREA' && $event.target.select()") - - //- dialog: export avatars list - el-dialog.x-dialog(:before-close="beforeDialogClose" @mousedown.native="dialogMouseDown" @mouseup.native="dialogMouseUp" :visible.sync="exportAvatarsListDialog" :title="$t('dialog.export_own_avatars.header')" width="650px") - el-input(type="textarea" v-if="exportAvatarsListDialog" v-model="exportAvatarsListCsv" size="mini" rows="15" resize="none" readonly style="margin-top:15px" @click.native="$event.target.tagName === 'TEXTAREA' && $event.target.select()") - - //- dialog: Discord username list - el-dialog.x-dialog(:before-close="beforeDialogClose" @mousedown.native="dialogMouseDown" @mouseup.native="dialogMouseUp" :visible.sync="discordNamesDialogVisible" :title="$t('dialog.discord_names.header')" width="650px") - div(style='font-size:12px;') - | {{ $t('dialog.discord_names.description') }} - el-input(type="textarea" v-if="discordNamesDialogVisible" v-model="discordNamesContent" size="mini" rows="15" resize="none" readonly style="margin-top:15px") - - //- dialog: Notification position - el-dialog.x-dialog(:before-close="beforeDialogClose" @mousedown.native="dialogMouseDown" @mouseup.native="dialogMouseUp" ref="notificationPositionDialog" :visible.sync="notificationPositionDialog.visible" :title="$t('dialog.notification_position.header')" width="400px") - div(style='font-size:12px;') - | {{ $t('dialog.notification_position.description') }} - svg.notification-position(version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" viewBox="0 0 300 200" style="margin-top:15px;" xml:space="preserve") - path(style="fill:black;" d="M291.89,5A3.11,3.11,0,0,1,295,8.11V160.64a3.11,3.11,0,0,1-3.11,3.11H8.11A3.11,3.11,0,0,1,5,160.64V8.11A3.11,3.11,0,0,1,8.11,5H291.89m0-5H8.11A8.11,8.11,0,0,0,0,8.11V160.64a8.11,8.11,0,0,0,8.11,8.11H291.89a8.11,8.11,0,0,0,8.11-8.11V8.11A8.11,8.11,0,0,0,291.89,0Z") - rect(style="fill:#c4c4c4;" x="5" y="5" width="290" height="158.75" rx="2.5") - el-radio-group(v-model="notificationPosition" size="mini" @change="changeNotificationPosition") - el-radio(label="topLeft" v-model="notificationPosition" style="margin:0;position:absolute;left:35px;top:120px;") ‎ - el-radio(label="top" v-model="notificationPosition" style="margin:0;position:absolute;left:195px;top:120px;") ‎ - el-radio(label="topRight" v-model="notificationPosition" style="margin:0;position:absolute;right:25px;top:120px;") ‎ - el-radio(label="centerLeft" v-model="notificationPosition" style="margin:0;position:absolute;left:35px;top:200px;") ‎ - el-radio(label="center" v-model="notificationPosition" style="margin:0;position:absolute;left:195px;top:200px;") ‎ - el-radio(label="centerRight" v-model="notificationPosition" style="margin:0;position:absolute;right:25px;top:200px;") ‎ - el-radio(label="bottomLeft" v-model="notificationPosition" style="margin:0;position:absolute;left:35px;top:280px;") ‎ - el-radio(label="bottom" v-model="notificationPosition" style="margin:0;position:absolute;left:195px;top:280px;") ‎ - el-radio(label="bottomRight" v-model="notificationPosition" style="margin:0;position:absolute;right:25px;top:280px;") ‎ - template(#footer) - div(style="display:flex") - el-button(type="primary" size="small" style="margin-left:auto" @click="notificationPositionDialog.visible = false") {{ $t('dialog.notification_position.ok') }} - - //- dialog: Noty feed filters - el-dialog.x-dialog(:before-close="beforeDialogClose" @mousedown.native="dialogMouseDown" @mouseup.native="dialogMouseUp" ref="notyFeedFiltersDialog" :visible.sync="notyFeedFiltersDialog.visible" :title="$t('dialog.shared_feed_filters.notification')" width="550px") - .toggle-list - .toggle-item - span.toggle-name OnPlayerJoining - el-radio-group(v-model="sharedFeedFilters.noty.OnPlayerJoining" size="mini" @change="saveSharedFeedFilters") - el-radio-button(label="Off") {{ $t('dialog.shared_feed_filters.off') }} - el-radio-button(label="VIP") {{ $t('dialog.shared_feed_filters.favorite') }} - el-radio-button(label="Friends") {{ $t('dialog.shared_feed_filters.friends') }} - .toggle-item - span.toggle-name OnPlayerJoined - el-radio-group(v-model="sharedFeedFilters.noty.OnPlayerJoined" size="mini" @change="saveSharedFeedFilters") - el-radio-button(label="Off") {{ $t('dialog.shared_feed_filters.off') }} - el-radio-button(label="VIP") {{ $t('dialog.shared_feed_filters.favorite') }} - el-radio-button(label="Friends") {{ $t('dialog.shared_feed_filters.friends') }} - el-radio-button(label="Everyone") {{ $t('dialog.shared_feed_filters.everyone') }} - .toggle-item - span.toggle-name OnPlayerLeft - el-radio-group(v-model="sharedFeedFilters.noty.OnPlayerLeft" size="mini" @change="saveSharedFeedFilters") - el-radio-button(label="Off") {{ $t('dialog.shared_feed_filters.off') }} - el-radio-button(label="VIP") {{ $t('dialog.shared_feed_filters.favorite') }} - el-radio-button(label="Friends") {{ $t('dialog.shared_feed_filters.friends') }} - el-radio-button(label="Everyone") {{ $t('dialog.shared_feed_filters.everyone') }} - .toggle-item - span.toggle-name Online - el-radio-group(v-model="sharedFeedFilters.noty.Online" size="mini" @change="saveSharedFeedFilters") - el-radio-button(label="Off") {{ $t('dialog.shared_feed_filters.off') }} - el-radio-button(label="VIP") {{ $t('dialog.shared_feed_filters.favorite') }} - el-radio-button(label="Friends") {{ $t('dialog.shared_feed_filters.friends') }} - .toggle-item - span.toggle-name Offline - el-radio-group(v-model="sharedFeedFilters.noty.Offline" size="mini" @change="saveSharedFeedFilters") - el-radio-button(label="Off") {{ $t('dialog.shared_feed_filters.off') }} - el-radio-button(label="VIP") {{ $t('dialog.shared_feed_filters.favorite') }} - el-radio-button(label="Friends") {{ $t('dialog.shared_feed_filters.friends') }} - .toggle-item - span.toggle-name GPS - el-radio-group(v-model="sharedFeedFilters.noty.GPS" size="mini" @change="saveSharedFeedFilters") - el-radio-button(label="Off") {{ $t('dialog.shared_feed_filters.off') }} - el-radio-button(label="VIP") {{ $t('dialog.shared_feed_filters.favorite') }} - el-radio-button(label="Friends") {{ $t('dialog.shared_feed_filters.friends') }} - .toggle-item - span.toggle-name Status - el-radio-group(v-model="sharedFeedFilters.noty.Status" size="mini" @change="saveSharedFeedFilters") - el-radio-button(label="Off") {{ $t('dialog.shared_feed_filters.off') }} - el-radio-button(label="VIP") {{ $t('dialog.shared_feed_filters.favorite') }} - el-radio-button(label="Friends") {{ $t('dialog.shared_feed_filters.friends') }} - .toggle-item - span.toggle-name Invite - el-radio-group(v-model="sharedFeedFilters.noty.invite" size="mini" @change="saveSharedFeedFilters") - el-radio-button(label="Off") {{ $t('dialog.shared_feed_filters.off') }} - el-radio-button(label="VIP") {{ $t('dialog.shared_feed_filters.favorite') }} - el-radio-button(label="Friends") {{ $t('dialog.shared_feed_filters.friends') }} - .toggle-item - span.toggle-name Request Invite - el-radio-group(v-model="sharedFeedFilters.noty.requestInvite" size="mini" @change="saveSharedFeedFilters") - el-radio-button(label="Off") {{ $t('dialog.shared_feed_filters.off') }} - el-radio-button(label="VIP") {{ $t('dialog.shared_feed_filters.favorite') }} - el-radio-button(label="Friends") {{ $t('dialog.shared_feed_filters.friends') }} - .toggle-item - span.toggle-name Invite Response - el-radio-group(v-model="sharedFeedFilters.noty.inviteResponse" size="mini" @change="saveSharedFeedFilters") - el-radio-button(label="Off") {{ $t('dialog.shared_feed_filters.off') }} - el-radio-button(label="VIP") {{ $t('dialog.shared_feed_filters.favorite') }} - el-radio-button(label="Friends") {{ $t('dialog.shared_feed_filters.friends') }} - .toggle-item - span.toggle-name Request Invite Response - el-radio-group(v-model="sharedFeedFilters.noty.requestInviteResponse" size="mini" @change="saveSharedFeedFilters") - el-radio-button(label="Off") {{ $t('dialog.shared_feed_filters.off') }} - el-radio-button(label="VIP") {{ $t('dialog.shared_feed_filters.favorite') }} - el-radio-button(label="Friends") {{ $t('dialog.shared_feed_filters.friends') }} - .toggle-item - span.toggle-name Friend Request - el-radio-group(v-model="sharedFeedFilters.noty.friendRequest" size="mini" @change="saveSharedFeedFilters") - el-radio-button(label="Off") {{ $t('dialog.shared_feed_filters.off') }} - el-radio-button(label="On") {{ $t('dialog.shared_feed_filters.on') }} - .toggle-item - span.toggle-name New Friend - el-radio-group(v-model="sharedFeedFilters.noty.Friend" size="mini" @change="saveSharedFeedFilters") - el-radio-button(label="Off") {{ $t('dialog.shared_feed_filters.off') }} - el-radio-button(label="On") {{ $t('dialog.shared_feed_filters.on') }} - .toggle-item - span.toggle-name Unfriend - el-radio-group(v-model="sharedFeedFilters.noty.Unfriend" size="mini" @change="saveSharedFeedFilters") - el-radio-button(label="Off") {{ $t('dialog.shared_feed_filters.off') }} - el-radio-button(label="On") {{ $t('dialog.shared_feed_filters.on') }} - .toggle-item - span.toggle-name Display Name Change - el-radio-group(v-model="sharedFeedFilters.noty.DisplayName" size="mini" @change="saveSharedFeedFilters") - el-radio-button(label="Off") {{ $t('dialog.shared_feed_filters.off') }} - el-radio-button(label="VIP") {{ $t('dialog.shared_feed_filters.favorite') }} - el-radio-button(label="Friends") {{ $t('dialog.shared_feed_filters.friends') }} - .toggle-item - span.toggle-name Trust Level Change - el-radio-group(v-model="sharedFeedFilters.noty.TrustLevel" size="mini" @change="saveSharedFeedFilters") - el-radio-button(label="Off") {{ $t('dialog.shared_feed_filters.off') }} - el-radio-button(label="VIP") {{ $t('dialog.shared_feed_filters.favorite') }} - el-radio-button(label="Friends") {{ $t('dialog.shared_feed_filters.friends') }} - //- .toggle-item - //- span.toggle-name Boop - //- el-radio-group(v-model="sharedFeedFilters.noty.boop" size="mini" @change="saveSharedFeedFilters") - //- el-radio-button(label="Off") {{ $t('dialog.shared_feed_filters.off') }} - //- el-radio-button(label="On") {{ $t('dialog.shared_feed_filters.on') }} - .toggle-item - span.toggle-name Group Change - el-tooltip(placement="top" style="margin-left:5px" content="When you've left or been kicked from a group, group name changed, group owner changed, role added/removed") - i.el-icon-info - el-radio-group(v-model="sharedFeedFilters.noty.groupChange" size="mini" @change="saveSharedFeedFilters") - el-radio-button(label="Off") {{ $t('dialog.shared_feed_filters.off') }} - el-radio-button(label="On") {{ $t('dialog.shared_feed_filters.on') }} - .toggle-item - span.toggle-name Group Announcement - el-radio-group(v-model="sharedFeedFilters.noty['group.announcement']" size="mini" @change="saveSharedFeedFilters") - el-radio-button(label="Off") {{ $t('dialog.shared_feed_filters.off') }} - el-radio-button(label="On") {{ $t('dialog.shared_feed_filters.on') }} - .toggle-item - span.toggle-name Group Join - el-tooltip(placement="top" style="margin-left:5px" content="When your request to join a group has been approved") - i.el-icon-info - el-radio-group(v-model="sharedFeedFilters.noty['group.informative']" size="mini" @change="saveSharedFeedFilters") - el-radio-button(label="Off") {{ $t('dialog.shared_feed_filters.off') }} - el-radio-button(label="On") {{ $t('dialog.shared_feed_filters.on') }} - .toggle-item - span.toggle-name Group Invite - el-tooltip(placement="top" style="margin-left:5px" content="When someone invites you to join a group") - i.el-icon-info - el-radio-group(v-model="sharedFeedFilters.noty['group.invite']" size="mini" @change="saveSharedFeedFilters") - el-radio-button(label="Off") {{ $t('dialog.shared_feed_filters.off') }} - el-radio-button(label="On") {{ $t('dialog.shared_feed_filters.on') }} - .toggle-item - span.toggle-name Group Join Request - el-tooltip(placement="top" style="margin-left:5px" content="When someone requests to join a group you're a moderator for") - i.el-icon-info - el-radio-group(v-model="sharedFeedFilters.noty['group.joinRequest']" size="mini" @change="saveSharedFeedFilters") - el-radio-button(label="Off") {{ $t('dialog.shared_feed_filters.off') }} - el-radio-button(label="On") {{ $t('dialog.shared_feed_filters.on') }} - .toggle-item - span.toggle-name Group Transfer Request - el-radio-group(v-model="sharedFeedFilters.noty['group.transfer']" size="mini" @change="saveSharedFeedFilters") - el-radio-button(label="Off") {{ $t('dialog.shared_feed_filters.off') }} - el-radio-button(label="On") {{ $t('dialog.shared_feed_filters.on') }} - .toggle-item - span.toggle-name Instance Queue Ready - el-radio-group(v-model="sharedFeedFilters.noty['group.queueReady']" size="mini" @change="saveSharedFeedFilters") - el-radio-button(label="Off") {{ $t('dialog.shared_feed_filters.off') }} - el-radio-button(label="On") {{ $t('dialog.shared_feed_filters.on') }} - .toggle-item - span.toggle-name Instance Closed - el-tooltip(placement="top" style="margin-left:5px" content="When the instance you're in has been closed preventing anyone from joining") - i.el-icon-info - el-radio-group(v-model="sharedFeedFilters.noty['instance.closed']" size="mini" @change="saveSharedFeedFilters") - el-radio-button(label="Off") {{ $t('dialog.shared_feed_filters.off') }} - el-radio-button(label="On") {{ $t('dialog.shared_feed_filters.on') }} - .toggle-item - span.toggle-name Video Play - el-tooltip(placement="top" style="margin-left:5px" content="Requires VRCX YouTube API option enabled") - i.el-icon-warning - el-radio-group(v-model="sharedFeedFilters.noty.VideoPlay" size="mini" @change="saveSharedFeedFilters") - el-radio-button(label="Off") {{ $t('dialog.shared_feed_filters.off') }} - el-radio-button(label="On") {{ $t('dialog.shared_feed_filters.on') }} - .toggle-item - span.toggle-name Miscellaneous Events - el-tooltip(placement="top" style="margin-left:5px" content="Misc event from VRC game log: VRC crash auto rejoin, shader keyword limit, joining instance blocked by master, error loading video, audio device changed, error joining instance, kicked from instance, VRChat failing to start OSC server, etc...") - i.el-icon-info - el-radio-group(v-model="sharedFeedFilters.noty.Event" size="mini" @change="saveSharedFeedFilters") - el-radio-button(label="Off") {{ $t('dialog.shared_feed_filters.off') }} - el-radio-button(label="On") {{ $t('dialog.shared_feed_filters.on') }} - .toggle-item - span.toggle-name External App - el-radio-group(v-model="sharedFeedFilters.noty.External" size="mini" @change="saveSharedFeedFilters") - el-radio-button(label="Off") {{ $t('dialog.shared_feed_filters.off') }} - el-radio-button(label="On") {{ $t('dialog.shared_feed_filters.on') }} - .toggle-item - span.toggle-name Blocked Player Joins - el-radio-group(v-model="sharedFeedFilters.noty.BlockedOnPlayerJoined" size="mini" @change="saveSharedFeedFilters") - el-radio-button(label="Off") {{ $t('dialog.shared_feed_filters.off') }} - el-radio-button(label="VIP") {{ $t('dialog.shared_feed_filters.favorite') }} - el-radio-button(label="Friends") {{ $t('dialog.shared_feed_filters.friends') }} - el-radio-button(label="Everyone") {{ $t('dialog.shared_feed_filters.everyone') }} - .toggle-item - span.toggle-name Blocked Player Leaves - el-radio-group(v-model="sharedFeedFilters.noty.BlockedOnPlayerLeft" size="mini" @change="saveSharedFeedFilters") - el-radio-button(label="Off") {{ $t('dialog.shared_feed_filters.off') }} - el-radio-button(label="VIP") {{ $t('dialog.shared_feed_filters.favorite') }} - el-radio-button(label="Friends") {{ $t('dialog.shared_feed_filters.friends') }} - el-radio-button(label="Everyone") {{ $t('dialog.shared_feed_filters.everyone') }} - .toggle-item - span.toggle-name Muted Player Joins - el-radio-group(v-model="sharedFeedFilters.noty.MutedOnPlayerJoined" size="mini" @change="saveSharedFeedFilters") - el-radio-button(label="Off") {{ $t('dialog.shared_feed_filters.off') }} - el-radio-button(label="VIP") {{ $t('dialog.shared_feed_filters.favorite') }} - el-radio-button(label="Friends") {{ $t('dialog.shared_feed_filters.friends') }} - el-radio-button(label="Everyone") {{ $t('dialog.shared_feed_filters.everyone') }} - .toggle-item - span.toggle-name Muted Player Leaves - el-radio-group(v-model="sharedFeedFilters.noty.MutedOnPlayerLeft" size="mini" @change="saveSharedFeedFilters") - el-radio-button(label="Off") {{ $t('dialog.shared_feed_filters.off') }} - el-radio-button(label="VIP") {{ $t('dialog.shared_feed_filters.favorite') }} - el-radio-button(label="Friends") {{ $t('dialog.shared_feed_filters.friends') }} - el-radio-button(label="Everyone") {{ $t('dialog.shared_feed_filters.everyone') }} - .toggle-item - span.toggle-name Lobby Avatar Change - el-radio-group(v-model="sharedFeedFilters.noty.AvatarChange" size="mini" @change="saveSharedFeedFilters") - el-radio-button(label="Off") {{ $t('dialog.shared_feed_filters.off') }} - el-radio-button(label="VIP") {{ $t('dialog.shared_feed_filters.favorite') }} - el-radio-button(label="Friends") {{ $t('dialog.shared_feed_filters.friends') }} - el-radio-button(label="Everyone") {{ $t('dialog.shared_feed_filters.everyone') }} - template(v-if="photonLoggingEnabled") - br - .toggle-item - span.toggle-name Photon Event Logging - .toggle-item - span.toggle-name Portal Spawn - el-radio-group(v-model="sharedFeedFilters.noty.PortalSpawn" size="mini" @change="saveSharedFeedFilters") - el-radio-button(label="Off") {{ $t('dialog.shared_feed_filters.off') }} - el-radio-button(label="VIP") {{ $t('dialog.shared_feed_filters.favorite') }} - el-radio-button(label="Friends") {{ $t('dialog.shared_feed_filters.friends') }} - el-radio-button(label="Everyone") {{ $t('dialog.shared_feed_filters.everyone') }} - .toggle-item - span.toggle-name Lobby ChatBox Message - el-radio-group(v-model="sharedFeedFilters.noty.ChatBoxMessage" size="mini" @change="saveSharedFeedFilters") - el-radio-button(label="Off") {{ $t('dialog.shared_feed_filters.off') }} - el-radio-button(label="VIP") {{ $t('dialog.shared_feed_filters.favorite') }} - el-radio-button(label="Friends") {{ $t('dialog.shared_feed_filters.friends') }} - el-radio-button(label="Everyone") {{ $t('dialog.shared_feed_filters.everyone') }} - .toggle-item - span.toggle-name Blocked - el-radio-group(v-model="sharedFeedFilters.noty.Blocked" size="mini" @change="saveSharedFeedFilters") - el-radio-button(label="Off") {{ $t('dialog.shared_feed_filters.off') }} - el-radio-button(label="On") {{ $t('dialog.shared_feed_filters.on') }} - .toggle-item - span.toggle-name Unblocked - el-radio-group(v-model="sharedFeedFilters.noty.Unblocked" size="mini" @change="saveSharedFeedFilters") - el-radio-button(label="Off") {{ $t('dialog.shared_feed_filters.off') }} - el-radio-button(label="On") {{ $t('dialog.shared_feed_filters.on') }} - .toggle-item - span.toggle-name Muted - el-radio-group(v-model="sharedFeedFilters.noty.Muted" size="mini" @change="saveSharedFeedFilters") - el-radio-button(label="Off") {{ $t('dialog.shared_feed_filters.off') }} - el-radio-button(label="On") {{ $t('dialog.shared_feed_filters.on') }} - .toggle-item - span.toggle-name Unmuted - el-radio-group(v-model="sharedFeedFilters.noty.Unmuted" size="mini" @change="saveSharedFeedFilters") - el-radio-button(label="Off") {{ $t('dialog.shared_feed_filters.off') }} - el-radio-button(label="On") {{ $t('dialog.shared_feed_filters.on') }} - template(#footer) - el-button(type="small" @click="resetSharedFeedFilters") {{ $t('dialog.shared_feed_filters.reset') }} - el-button(size="small" style="margin-left:10px" @click="notyFeedFiltersDialog.visible = false") {{ $t('dialog.shared_feed_filters.close') }} + include ./mixins/dialogs/worldDialog.pug + +worldDialog() - //- dialog: wrist feed filters - el-dialog.x-dialog(:before-close="beforeDialogClose" @mousedown.native="dialogMouseDown" @mouseup.native="dialogMouseUp" ref="wristFeedFiltersDialog" :visible.sync="wristFeedFiltersDialog.visible" :title="$t('dialog.shared_feed_filters.wrist')" width="550px") - .toggle-list - .toggle-item - span.toggle-name Self Location - el-radio-group(v-model="sharedFeedFilters.wrist.Location" size="mini" @change="saveSharedFeedFilters") - el-radio-button(label="Off") {{ $t('dialog.shared_feed_filters.off') }} - el-radio-button(label="On") {{ $t('dialog.shared_feed_filters.on') }} - .toggle-item - span.toggle-name OnPlayerJoining - el-radio-group(v-model="sharedFeedFilters.wrist.OnPlayerJoining" size="mini" @change="saveSharedFeedFilters") - el-radio-button(label="Off") {{ $t('dialog.shared_feed_filters.off') }} - el-radio-button(label="VIP") {{ $t('dialog.shared_feed_filters.favorite') }} - el-radio-button(label="Friends") {{ $t('dialog.shared_feed_filters.friends') }} - .toggle-item - span.toggle-name OnPlayerJoined - el-radio-group(v-model="sharedFeedFilters.wrist.OnPlayerJoined" size="mini" @change="saveSharedFeedFilters") - el-radio-button(label="Off") {{ $t('dialog.shared_feed_filters.off') }} - el-radio-button(label="VIP") {{ $t('dialog.shared_feed_filters.favorite') }} - el-radio-button(label="Friends") {{ $t('dialog.shared_feed_filters.friends') }} - el-radio-button(label="Everyone") {{ $t('dialog.shared_feed_filters.everyone') }} - .toggle-item - span.toggle-name OnPlayerLeft - el-radio-group(v-model="sharedFeedFilters.wrist.OnPlayerLeft" size="mini" @change="saveSharedFeedFilters") - el-radio-button(label="Off") {{ $t('dialog.shared_feed_filters.off') }} - el-radio-button(label="VIP") {{ $t('dialog.shared_feed_filters.favorite') }} - el-radio-button(label="Friends") {{ $t('dialog.shared_feed_filters.friends') }} - el-radio-button(label="Everyone") {{ $t('dialog.shared_feed_filters.everyone') }} - .toggle-item - span.toggle-name Online - el-radio-group(v-model="sharedFeedFilters.wrist.Online" size="mini" @change="saveSharedFeedFilters") - el-radio-button(label="Off") {{ $t('dialog.shared_feed_filters.off') }} - el-radio-button(label="VIP") {{ $t('dialog.shared_feed_filters.favorite') }} - el-radio-button(label="Friends") {{ $t('dialog.shared_feed_filters.friends') }} - .toggle-item - span.toggle-name Offline - el-radio-group(v-model="sharedFeedFilters.wrist.Offline" size="mini" @change="saveSharedFeedFilters") - el-radio-button(label="Off") {{ $t('dialog.shared_feed_filters.off') }} - el-radio-button(label="VIP") {{ $t('dialog.shared_feed_filters.favorite') }} - el-radio-button(label="Friends") {{ $t('dialog.shared_feed_filters.friends') }} - .toggle-item - span.toggle-name GPS - el-radio-group(v-model="sharedFeedFilters.wrist.GPS" size="mini" @change="saveSharedFeedFilters") - el-radio-button(label="Off") {{ $t('dialog.shared_feed_filters.off') }} - el-radio-button(label="VIP") {{ $t('dialog.shared_feed_filters.favorite') }} - el-radio-button(label="Friends") {{ $t('dialog.shared_feed_filters.friends') }} - .toggle-item - span.toggle-name Status - el-radio-group(v-model="sharedFeedFilters.wrist.Status" size="mini" @change="saveSharedFeedFilters") - el-radio-button(label="Off") {{ $t('dialog.shared_feed_filters.off') }} - el-radio-button(label="VIP") {{ $t('dialog.shared_feed_filters.favorite') }} - el-radio-button(label="Friends") {{ $t('dialog.shared_feed_filters.friends') }} - .toggle-item - span.toggle-name Invite - el-radio-group(v-model="sharedFeedFilters.wrist.invite" size="mini" @change="saveSharedFeedFilters") - el-radio-button(label="Off") {{ $t('dialog.shared_feed_filters.off') }} - el-radio-button(label="VIP") {{ $t('dialog.shared_feed_filters.favorite') }} - el-radio-button(label="Friends") {{ $t('dialog.shared_feed_filters.friends') }} - .toggle-item - span.toggle-name Request Invite - el-radio-group(v-model="sharedFeedFilters.wrist.requestInvite" size="mini" @change="saveSharedFeedFilters") - el-radio-button(label="Off") {{ $t('dialog.shared_feed_filters.off') }} - el-radio-button(label="VIP") {{ $t('dialog.shared_feed_filters.favorite') }} - el-radio-button(label="Friends") {{ $t('dialog.shared_feed_filters.friends') }} - .toggle-item - span.toggle-name Invite Response - el-radio-group(v-model="sharedFeedFilters.wrist.inviteResponse" size="mini" @change="saveSharedFeedFilters") - el-radio-button(label="Off") {{ $t('dialog.shared_feed_filters.off') }} - el-radio-button(label="VIP") {{ $t('dialog.shared_feed_filters.favorite') }} - el-radio-button(label="Friends") {{ $t('dialog.shared_feed_filters.friends') }} - .toggle-item - span.toggle-name Request Invite Response - el-radio-group(v-model="sharedFeedFilters.wrist.requestInviteResponse" size="mini" @change="saveSharedFeedFilters") - el-radio-button(label="Off") {{ $t('dialog.shared_feed_filters.off') }} - el-radio-button(label="VIP") {{ $t('dialog.shared_feed_filters.favorite') }} - el-radio-button(label="Friends") {{ $t('dialog.shared_feed_filters.friends') }} - .toggle-item - span.toggle-name Friend Request - el-radio-group(v-model="sharedFeedFilters.wrist.friendRequest" size="mini" @change="saveSharedFeedFilters") - el-radio-button(label="Off") {{ $t('dialog.shared_feed_filters.off') }} - el-radio-button(label="On") {{ $t('dialog.shared_feed_filters.on') }} - .toggle-item - span.toggle-name New Friend - el-radio-group(v-model="sharedFeedFilters.wrist.Friend" size="mini" @change="saveSharedFeedFilters") - el-radio-button(label="Off") {{ $t('dialog.shared_feed_filters.off') }} - el-radio-button(label="On") {{ $t('dialog.shared_feed_filters.on') }} - .toggle-item - span.toggle-name Unfriend - el-radio-group(v-model="sharedFeedFilters.wrist.Unfriend" size="mini" @change="saveSharedFeedFilters") - el-radio-button(label="Off") {{ $t('dialog.shared_feed_filters.off') }} - el-radio-button(label="On") {{ $t('dialog.shared_feed_filters.on') }} - .toggle-item - span.toggle-name Display Name Change - el-radio-group(v-model="sharedFeedFilters.wrist.DisplayName" size="mini" @change="saveSharedFeedFilters") - el-radio-button(label="Off") {{ $t('dialog.shared_feed_filters.off') }} - el-radio-button(label="VIP") {{ $t('dialog.shared_feed_filters.favorite') }} - el-radio-button(label="Friends") {{ $t('dialog.shared_feed_filters.friends') }} - .toggle-item - span.toggle-name Trust Level Change - el-radio-group(v-model="sharedFeedFilters.wrist.TrustLevel" size="mini" @change="saveSharedFeedFilters") - el-radio-button(label="Off") {{ $t('dialog.shared_feed_filters.off') }} - el-radio-button(label="VIP") {{ $t('dialog.shared_feed_filters.favorite') }} - el-radio-button(label="Friends") {{ $t('dialog.shared_feed_filters.friends') }} - //- .toggle-item - //- span.toggle-name Boop - //- el-radio-group(v-model="sharedFeedFilters.wrist.boop" size="mini" @change="saveSharedFeedFilters") - //- el-radio-button(label="Off") {{ $t('dialog.shared_feed_filters.off') }} - //- el-radio-button(label="On") {{ $t('dialog.shared_feed_filters.on') }} - .toggle-item - span.toggle-name Group Change - el-tooltip(placement="top" style="margin-left:5px" content="When you've left or been kicked from a group, group name changed, group owner changed, role added/removed") - i.el-icon-info - el-radio-group(v-model="sharedFeedFilters.wrist.groupChange" size="mini" @change="saveSharedFeedFilters") - el-radio-button(label="Off") {{ $t('dialog.shared_feed_filters.off') }} - el-radio-button(label="On") {{ $t('dialog.shared_feed_filters.on') }} - .toggle-item - span.toggle-name Group Announcement - el-radio-group(v-model="sharedFeedFilters.wrist['group.announcement']" size="mini" @change="saveSharedFeedFilters") - el-radio-button(label="Off") {{ $t('dialog.shared_feed_filters.off') }} - el-radio-button(label="On") {{ $t('dialog.shared_feed_filters.on') }} - .toggle-item - span.toggle-name Group Join - el-tooltip(placement="top" style="margin-left:5px" content="When your request to join a group has been approved") - i.el-icon-info - el-radio-group(v-model="sharedFeedFilters.wrist['group.informative']" size="mini" @change="saveSharedFeedFilters") - el-radio-button(label="Off") {{ $t('dialog.shared_feed_filters.off') }} - el-radio-button(label="On") {{ $t('dialog.shared_feed_filters.on') }} - .toggle-item - span.toggle-name Group Invite - el-tooltip(placement="top" style="margin-left:5px" content="When someone invites you to join a group") - i.el-icon-info - el-radio-group(v-model="sharedFeedFilters.wrist['group.invite']" size="mini" @change="saveSharedFeedFilters") - el-radio-button(label="Off") {{ $t('dialog.shared_feed_filters.off') }} - el-radio-button(label="On") {{ $t('dialog.shared_feed_filters.on') }} - .toggle-item - span.toggle-name Group Join Request - el-tooltip(placement="top" style="margin-left:5px" content="When someone requests to join a group you're a moderator for") - i.el-icon-info - el-radio-group(v-model="sharedFeedFilters.wrist['group.joinRequest']" size="mini" @change="saveSharedFeedFilters") - el-radio-button(label="Off") {{ $t('dialog.shared_feed_filters.off') }} - el-radio-button(label="On") {{ $t('dialog.shared_feed_filters.on') }} - .toggle-item - span.toggle-name Group Transfer Request - el-radio-group(v-model="sharedFeedFilters.wrist['group.transfer']" size="mini" @change="saveSharedFeedFilters") - el-radio-button(label="Off") {{ $t('dialog.shared_feed_filters.off') }} - el-radio-button(label="On") {{ $t('dialog.shared_feed_filters.on') }} - .toggle-item - span.toggle-name Instance Queue Ready - el-radio-group(v-model="sharedFeedFilters.wrist['group.queueReady']" size="mini" @change="saveSharedFeedFilters") - el-radio-button(label="Off") {{ $t('dialog.shared_feed_filters.off') }} - el-radio-button(label="On") {{ $t('dialog.shared_feed_filters.on') }} - .toggle-item - span.toggle-name Instance Closed - el-tooltip(placement="top" style="margin-left:5px" content="When the instance you're in has been closed preventing anyone from joining") - i.el-icon-info - el-radio-group(v-model="sharedFeedFilters.wrist['instance.closed']" size="mini" @change="saveSharedFeedFilters") - el-radio-button(label="Off") {{ $t('dialog.shared_feed_filters.off') }} - el-radio-button(label="On") {{ $t('dialog.shared_feed_filters.on') }} - .toggle-item - span.toggle-name Video Play - el-tooltip(placement="top" style="margin-left:5px" content="Requires VRCX YouTube API option enabled") - i.el-icon-warning - el-radio-group(v-model="sharedFeedFilters.wrist.VideoPlay" size="mini" @change="saveSharedFeedFilters") - el-radio-button(label="Off") {{ $t('dialog.shared_feed_filters.off') }} - el-radio-button(label="On") {{ $t('dialog.shared_feed_filters.on') }} - .toggle-item - span.toggle-name Miscellaneous Events - el-tooltip(placement="top" style="margin-left:5px" content="Misc event from VRC game log: VRC crash auto rejoin, shader keyword limit, joining instance blocked by master, error loading video, audio device changed, error joining instance, kicked from instance, VRChat failing to start OSC server, etc...") - i.el-icon-info - el-radio-group(v-model="sharedFeedFilters.wrist.Event" size="mini" @change="saveSharedFeedFilters") - el-radio-button(label="Off") {{ $t('dialog.shared_feed_filters.off') }} - el-radio-button(label="On") {{ $t('dialog.shared_feed_filters.on') }} - .toggle-item - span.toggle-name External App - el-radio-group(v-model="sharedFeedFilters.wrist.External" size="mini" @change="saveSharedFeedFilters") - el-radio-button(label="Off") {{ $t('dialog.shared_feed_filters.off') }} - el-radio-button(label="On") {{ $t('dialog.shared_feed_filters.on') }} - .toggle-item - span.toggle-name Blocked Player Joins - el-radio-group(v-model="sharedFeedFilters.wrist.BlockedOnPlayerJoined" size="mini" @change="saveSharedFeedFilters") - el-radio-button(label="Off") {{ $t('dialog.shared_feed_filters.off') }} - el-radio-button(label="VIP") {{ $t('dialog.shared_feed_filters.favorite') }} - el-radio-button(label="Friends") {{ $t('dialog.shared_feed_filters.friends') }} - el-radio-button(label="Everyone") {{ $t('dialog.shared_feed_filters.everyone') }} - .toggle-item - span.toggle-name Blocked Player Leaves - el-radio-group(v-model="sharedFeedFilters.wrist.BlockedOnPlayerLeft" size="mini" @change="saveSharedFeedFilters") - el-radio-button(label="Off") {{ $t('dialog.shared_feed_filters.off') }} - el-radio-button(label="VIP") {{ $t('dialog.shared_feed_filters.favorite') }} - el-radio-button(label="Friends") {{ $t('dialog.shared_feed_filters.friends') }} - el-radio-button(label="Everyone") {{ $t('dialog.shared_feed_filters.everyone') }} - .toggle-item - span.toggle-name Muted Player Joins - el-radio-group(v-model="sharedFeedFilters.wrist.MutedOnPlayerJoined" size="mini" @change="saveSharedFeedFilters") - el-radio-button(label="Off") {{ $t('dialog.shared_feed_filters.off') }} - el-radio-button(label="VIP") {{ $t('dialog.shared_feed_filters.favorite') }} - el-radio-button(label="Friends") {{ $t('dialog.shared_feed_filters.friends') }} - el-radio-button(label="Everyone") {{ $t('dialog.shared_feed_filters.everyone') }} - .toggle-item - span.toggle-name Muted Player Leaves - el-radio-group(v-model="sharedFeedFilters.wrist.MutedOnPlayerLeft" size="mini" @change="saveSharedFeedFilters") - el-radio-button(label="Off") {{ $t('dialog.shared_feed_filters.off') }} - el-radio-button(label="VIP") {{ $t('dialog.shared_feed_filters.favorite') }} - el-radio-button(label="Friends") {{ $t('dialog.shared_feed_filters.friends') }} - el-radio-button(label="Everyone") {{ $t('dialog.shared_feed_filters.everyone') }} - .toggle-item - span.toggle-name Lobby Avatar Change - el-radio-group(v-model="sharedFeedFilters.wrist.AvatarChange" size="mini" @change="saveSharedFeedFilters") - el-radio-button(label="Off") {{ $t('dialog.shared_feed_filters.off') }} - el-radio-button(label="VIP") {{ $t('dialog.shared_feed_filters.favorite') }} - el-radio-button(label="Friends") {{ $t('dialog.shared_feed_filters.friends') }} - el-radio-button(label="Everyone") {{ $t('dialog.shared_feed_filters.everyone') }} - template(v-if="photonLoggingEnabled") - br - .toggle-item - span.toggle-name Photon Event Logging - .toggle-item - span.toggle-name Portal Spawn - el-radio-group(v-model="sharedFeedFilters.wrist.PortalSpawn" size="mini" @change="saveSharedFeedFilters") - el-radio-button(label="Off") {{ $t('dialog.shared_feed_filters.off') }} - el-radio-button(label="VIP") {{ $t('dialog.shared_feed_filters.favorite') }} - el-radio-button(label="Friends") {{ $t('dialog.shared_feed_filters.friends') }} - el-radio-button(label="Everyone") {{ $t('dialog.shared_feed_filters.everyone') }} - .toggle-item - span.toggle-name Lobby ChatBox Message - el-radio-group(v-model="sharedFeedFilters.wrist.ChatBoxMessage" size="mini" @change="saveSharedFeedFilters") - el-radio-button(label="Off") {{ $t('dialog.shared_feed_filters.off') }} - el-radio-button(label="VIP") {{ $t('dialog.shared_feed_filters.favorite') }} - el-radio-button(label="Friends") {{ $t('dialog.shared_feed_filters.friends') }} - el-radio-button(label="Everyone") {{ $t('dialog.shared_feed_filters.everyone') }} - .toggle-item - span.toggle-name Blocked - el-radio-group(v-model="sharedFeedFilters.wrist.Blocked" size="mini" @change="saveSharedFeedFilters") - el-radio-button(label="Off") {{ $t('dialog.shared_feed_filters.off') }} - el-radio-button(label="On") {{ $t('dialog.shared_feed_filters.on') }} - .toggle-item - span.toggle-name Unblocked - el-radio-group(v-model="sharedFeedFilters.wrist.Unblocked" size="mini" @change="saveSharedFeedFilters") - el-radio-button(label="Off") {{ $t('dialog.shared_feed_filters.off') }} - el-radio-button(label="On") {{ $t('dialog.shared_feed_filters.on') }} - .toggle-item - span.toggle-name Muted - el-radio-group(v-model="sharedFeedFilters.wrist.Muted" size="mini" @change="saveSharedFeedFilters") - el-radio-button(label="Off") {{ $t('dialog.shared_feed_filters.off') }} - el-radio-button(label="On") {{ $t('dialog.shared_feed_filters.on') }} - .toggle-item - span.toggle-name Unmuted - el-radio-group(v-model="sharedFeedFilters.wrist.Unmuted" size="mini" @change="saveSharedFeedFilters") - el-radio-button(label="Off") {{ $t('dialog.shared_feed_filters.off') }} - el-radio-button(label="On") {{ $t('dialog.shared_feed_filters.on') }} - template(#footer) - el-button(type="small" @click="resetSharedFeedFilters") {{ $t('dialog.shared_feed_filters.reset') }} - el-button(size="small" @click="wristFeedFiltersDialog.visible = false") {{ $t('dialog.shared_feed_filters.close') }} + include ./mixins/dialogs/avatarDialog.pug + +avatarDialog() - //- dialog: Edit Invite Message - el-dialog.x-dialog(:before-close="beforeDialogClose" @mousedown.native="dialogMouseDown" @mouseup.native="dialogMouseUp" ref="editInviteMessageDialog" :visible.sync="editInviteMessageDialog.visible" :title="$t('dialog.edit_invite_message.header')" width="400px") - div(style='font-size:12px') - span {{ $t('dialog.edit_invite_message.description') }} - el-input(type="textarea" v-model="editInviteMessageDialog.newMessage" size="mini" maxlength="64" show-word-limit :autosize="{ minRows:2, maxRows:5 }" placeholder="" style="margin-top:10px") - template(#footer) - el-button(type="small" @click="cancelEditInviteMessage") {{ $t('dialog.edit_invite_message.cancel') }} - el-button(type="primary" size="small" @click="saveEditInviteMessage") {{ $t('dialog.edit_invite_message.save') }} + include ./mixins/dialogs/groupDialog.pug + +groupDialog() - //- dialog: Edit And Send Invite Response Message - el-dialog.x-dialog(:before-close="beforeDialogClose" @mousedown.native="dialogMouseDown" @mouseup.native="dialogMouseUp" ref="editAndSendInviteResponseDialog" :visible.sync="editAndSendInviteResponseDialog.visible" :title="$t('dialog.edit_send_invite_response_message.header')" width="400px") - div(style='font-size:12px') - span {{ $t('dialog.edit_send_invite_response_message.description') }} - el-input(type="textarea" v-model="editAndSendInviteResponseDialog.newMessage" size="mini" maxlength="64" show-word-limit :autosize="{ minRows:2, maxRows:5 }" placeholder="" style="margin-top:10px") - template(#footer) - el-button(type="small" @click="cancelEditAndSendInviteResponse") {{ $t('dialog.edit_send_invite_response_message.cancel') }} - el-button(type="primary" size="small" @click="saveEditAndSendInviteResponse") {{ $t('dialog.edit_send_invite_response_message.send') }} + include ./mixins/dialogs/newInstance.pug + +newInstance() - //- dialog Table: Send Invite Response Message - el-dialog.x-dialog(:before-close="beforeDialogClose" @mousedown.native="dialogMouseDown" @mouseup.native="dialogMouseUp" ref="sendInviteResponseDialog" :visible.sync="sendInviteResponseDialogVisible" :title="$t('dialog.invite_response_message.header')" width="800px") - template(v-if="API.currentUser.$isVRCPlus") - input.inviteImageUploadButton(type="file" accept="image/png,image/jpg,image/jpeg,image/webp,image/bmp,image/gif" @change="inviteImageUpload") - data-tables(v-if="sendInviteResponseDialogVisible" v-bind="inviteResponseMessageTable" @row-click="showSendInviteResponseConfirmDialog" style="margin-top:10px;cursor:pointer") - el-table-column(:label="$t('table.profile.invite_messages.slot')" prop="slot" sortable="custom" width="70") - el-table-column(:label="$t('table.profile.invite_messages.message')" prop="message") - el-table-column(:label="$t('table.profile.invite_messages.cool_down')" prop="updatedAt" sortable="custom" width="110" align="right") - template(v-once #default="scope") - countdown-timer(:datetime="scope.row.updatedAt" :hours="1") - el-table-column(:label="$t('table.profile.invite_messages.action')" width="70" align="right") - template(v-once #default="scope") - el-button(type="text" icon="el-icon-edit" size="mini" @click="showEditAndSendInviteResponseDialog('response', scope.row)") - template(#footer) - el-button(type="small" @click="cancelSendInviteResponse") {{ $t('dialog.invite_response_message.cancel') }} - el-button(type="small" @click="API.refreshInviteMessageTableData('response')") {{ $t('dialog.invite_response_message.refresh') }} + include ./mixins/dialogs/feedFilters.pug + +feedFilters() - //- dialog Table: Send Invite Request Response Message - el-dialog.x-dialog(:before-close="beforeDialogClose" @mousedown.native="dialogMouseDown" @mouseup.native="dialogMouseUp" ref="sendInviteRequestResponseDialog" :visible.sync="sendInviteRequestResponseDialogVisible" :title="$t('dialog.invite_request_response_message.header')" width="800px") - template(v-if="API.currentUser.$isVRCPlus") - input.inviteImageUploadButton(type="file" accept="image/png,image/jpg,image/jpeg,image/webp,image/bmp,image/gif" @change="inviteImageUpload") - data-tables(v-if="sendInviteRequestResponseDialogVisible" v-bind="inviteRequestResponseMessageTable" @row-click="showSendInviteResponseConfirmDialog" style="margin-top:10px;cursor:pointer") - el-table-column(:label="$t('table.profile.invite_messages.slot')" prop="slot" sortable="custom" width="70") - el-table-column(:label="$t('table.profile.invite_messages.message')" prop="message") - el-table-column(:label="$t('table.profile.invite_messages.cool_down')" prop="updatedAt" sortable="custom" width="110" align="right") - template(v-once #default="scope") - countdown-timer(:datetime="scope.row.updatedAt" :hours="1") - el-table-column(:label="$t('table.profile.invite_messages.action')" width="70" align="right") - template(v-once #default="scope") - el-button(type="text" icon="el-icon-edit" size="mini" @click="showEditAndSendInviteResponseDialog('requestResponse', scope.row)") - template(#footer) - el-button(type="small" @click="cancelSendInviteRequestResponse") {{ $t('dialog.invite_request_response_message.cancel') }} - el-button(type="small" @click="API.refreshInviteMessageTableData('requestResponse')") {{ $t('dialog.invite_request_response_message.refresh') }} + include ./mixins/dialogs/openSourceSoftwareNotice.pug + +openSourceSoftwareNotice() - //- dialog: Send Invite Response Message Confirm - el-dialog.x-dialog(:before-close="beforeDialogClose" @mousedown.native="dialogMouseDown" @mouseup.native="dialogMouseUp" ref="sendInviteResponseConfirmDialog" :visible.sync="sendInviteResponseConfirmDialog.visible" :title="$t('dialog.invite_response_message.header')" width="400px") - div(style='font-size:12px') - span {{ $t('dialog.invite_response_message.confirmation') }} - template(#footer) - el-button(type="small" @click="cancelInviteResponseConfirm") {{ $t('dialog.invite_response_message.cancel') }} - el-button(type="primary" size="small" @click="sendInviteResponseConfirm") {{ $t('dialog.invite_response_message.confirm') }} + include ./mixins/dialogs/groups.pug + +groups() - //- dialog Table: Send Invite Message - el-dialog.x-dialog(:before-close="beforeDialogClose" @mousedown.native="dialogMouseDown" @mouseup.native="dialogMouseUp" ref="sendInviteDialog" :visible.sync="sendInviteDialogVisible" :title="$t('dialog.invite_message.header')" width="800px") - template(v-if="API.currentUser.$isVRCPlus") - //- template(v-if="gallerySelectDialog.selectedFileId") - //- div(style="display:inline-block;flex:none;margin-right:5px") - //- el-popover(placement="right" width="500px" trigger="click") - //- img.x-link(slot="reference" v-lazy="gallerySelectDialog.selectedImageUrl" style="flex:none;width:60px;height:60px;border-radius:4px;object-fit:cover") - //- img.x-link(v-lazy="gallerySelectDialog.selectedImageUrl" style="height:500px" @click="showFullscreenImageDialog(gallerySelectDialog.selectedImageUrl)") - //- el-button(size="mini" @click="clearImageGallerySelect" style="vertical-align:top") {{ $t('dialog.invite_message.clear_selected_image') }} - //- template(v-else) - //- el-button(size="mini" @click="showGallerySelectDialog" style="margin-right:5px") {{ $t('dialog.invite_message.select_image') }} - input.inviteImageUploadButton(type="file" accept="image/png,image/jpg,image/jpeg,image/webp,image/bmp,image/gif" @change="inviteImageUpload") - data-tables(v-if="sendInviteDialogVisible" v-bind="inviteMessageTable" @row-click="showSendInviteConfirmDialog" style="margin-top:10px;cursor:pointer") - el-table-column(:label="$t('table.profile.invite_messages.slot')" prop="slot" sortable="custom" width="70") - el-table-column(:label="$t('table.profile.invite_messages.message')" prop="message") - el-table-column(:label="$t('table.profile.invite_messages.cool_down')" prop="updatedAt" sortable="custom" width="110" align="right") - template(v-once #default="scope") - countdown-timer(:datetime="scope.row.updatedAt" :hours="1") - el-table-column(:label="$t('table.profile.invite_messages.action')" width="70" align="right") - template(v-once #default="scope") - el-button(type="text" icon="el-icon-edit" size="mini" @click="showEditAndSendInviteDialog('message', scope.row)") - template(#footer) - el-button(type="small" @click="cancelSendInvite") {{ $t('dialog.invite_message.cancel') }} - el-button(type="small" @click="API.refreshInviteMessageTableData('message')") {{ $t('dialog.invite_message.refresh') }} + include ./mixins/dialogs/currentUser.pug + +currentUser() - //- dialog Table: Send Invite Request Message - el-dialog.x-dialog(:before-close="beforeDialogClose" @mousedown.native="dialogMouseDown" @mouseup.native="dialogMouseUp" ref="sendInviteRequestDialog" :visible.sync="sendInviteRequestDialogVisible" :title="$t('dialog.invite_request_message.header')" width="800px") - template(v-if="API.currentUser.$isVRCPlus") - input.inviteImageUploadButton(type="file" accept="image/png,image/jpg,image/jpeg,image/webp,image/bmp,image/gif" @change="inviteImageUpload") - data-tables(v-if="sendInviteRequestDialogVisible" v-bind="inviteRequestMessageTable" @row-click="showSendInviteConfirmDialog" style="margin-top:10px;cursor:pointer") - el-table-column(:label="$t('table.profile.invite_messages.slot')" prop="slot" sortable="custom" width="70") - el-table-column(:label="$t('table.profile.invite_messages.message')" prop="message") - el-table-column(:label="$t('table.profile.invite_messages.cool_down')" prop="updatedAt" sortable="custom" width="110" align="right") - template(v-once #default="scope") - countdown-timer(:datetime="scope.row.updatedAt" :hours="1") - el-table-column(:label="$t('table.profile.invite_messages.action')" width="70" align="right") - template(v-once #default="scope") - el-button(type="text" icon="el-icon-edit" size="mini" @click="showEditAndSendInviteDialog('request', scope.row)") - template(#footer) - el-button(type="small" @click="cancelSendInviteRequest") {{ $t('dialog.invite_request_message.cancel') }} - el-button(type="small" @click="API.refreshInviteMessageTableData('request')") {{ $t('dialog.invite_request_message.refresh') }} + include ./mixins/dialogs/invites.pug + +invites() - //- dialog: Send Invite Message Confirm - el-dialog.x-dialog(:before-close="beforeDialogClose" @mousedown.native="dialogMouseDown" @mouseup.native="dialogMouseUp" ref="sendInviteConfirmDialog" :visible.sync="sendInviteConfirmDialog.visible" :title="$t('dialog.invite_message.header')" width="400px") - div(style='font-size:12px') - span {{ $t('dialog.invite_message.confirmation') }} - template(#footer) - el-button(type="small" @click="cancelInviteConfirm") {{ $t('dialog.invite_message.cancel') }} - el-button(type="primary" size="small" @click="sendInviteConfirm") {{ $t('dialog.invite_message.confirm') }} + include ./mixins/dialogs/launch.pug + +launch() - //- dialog: Edit And Send Invite Message - el-dialog.x-dialog(:before-close="beforeDialogClose" @mousedown.native="dialogMouseDown" @mouseup.native="dialogMouseUp" ref="editAndSendInviteDialog" :visible.sync="editAndSendInviteDialog.visible" :title="$t('dialog.edit_send_invite_message.header')" width="400px") - div(style='font-size:12px') - span {{ $t('dialog.edit_send_invite_message.description') }} - el-input(type="textarea" v-model="editAndSendInviteDialog.newMessage" size="mini" maxlength="64" show-word-limit :autosize="{ minRows:2, maxRows:5 }" placeholder="" style="margin-top:10px") - template(#footer) - el-button(type="small" @click="cancelEditAndSendInvite") {{ $t('dialog.edit_send_invite_message.cancel') }} - el-button(type="primary" size="small" @click="saveEditAndSendInvite") {{ $t('dialog.edit_send_invite_message.send') }} + include ./mixins/dialogs/screenshotMetadata.pug + +screenshotMetadata() - //- dialog: Change avatar image - el-dialog.x-dialog(:before-close="beforeDialogClose" @mousedown.native="dialogMouseDown" @mouseup.native="dialogMouseUp" ref="changeAvatarImageDialog" :visible.sync="changeAvatarImageDialogVisible" :title="$t('dialog.change_content_image.avatar')" width="850px") - div(v-if="changeAvatarImageDialogVisible" v-loading="changeAvatarImageDialogLoading") - input(type="file" accept="image/png,image/jpg,image/jpeg,image/webp,image/bmp,image/gif" @change="onFileChangeAvatarImage" id="AvatarImageUploadButton" style="display:none") - span {{ $t('dialog.change_content_image.description') }} - br - el-button-group(style="padding-bottom:10px;padding-top:10px") - el-button(type="default" size="small" @click="displayPreviousImages('Avatar', 'Change')" icon="el-icon-refresh") {{ $t('dialog.change_content_image.refresh') }} - el-button(type="default" size="small" @click="uploadAvatarImage" icon="el-icon-upload2") {{ $t('dialog.change_content_image.upload') }} - //- el-button(type="default" size="small" @click="deleteAvatarImage" icon="el-icon-delete") Delete Latest Image - br - div(style="display:inline-block" v-for="image in previousImagesTable" :key="image.version" v-if="image.file") - .x-change-image-item(@click="setAvatarImage(image)" style="cursor:pointer" :class="{ 'current-image': compareCurrentImage(image) }") - img.image(v-lazy="image.file.url") + include ./mixins/dialogs/vrcx.pug + +vrcx() - //- dialog: Change world image - el-dialog.x-dialog(:before-close="beforeDialogClose" @mousedown.native="dialogMouseDown" @mouseup.native="dialogMouseUp" ref="changeWorldImageDialog" :visible.sync="changeWorldImageDialogVisible" :title="$t('dialog.change_content_image.world')" width="850px") - div(v-if="changeWorldImageDialogVisible" v-loading="changeWorldImageDialogLoading") - input(type="file" accept="image/png,image/jpg,image/jpeg,image/webp,image/bmp,image/gif" @change="onFileChangeWorldImage" id="WorldImageUploadButton" style="display:none") - span {{ $t('dialog.change_content_image.description') }} - br - el-button-group(style="padding-bottom:10px;padding-top:10px") - el-button(type="default" size="small" @click="displayPreviousImages('World', 'Change')" icon="el-icon-refresh") {{ $t('dialog.change_content_image.refresh') }} - el-button(type="default" size="small" @click="uploadWorldImage" icon="el-icon-upload2") {{ $t('dialog.change_content_image.upload') }} - //- el-button(type="default" size="small" @click="deleteWorldImage" icon="el-icon-delete") Delete Latest Image - br - div(style="display:inline-block" v-for="image in previousImagesTable" :key="image.version" v-if="image.file") - .x-change-image-item(@click="setWorldImage(image)" style="cursor:pointer" :class="{ 'current-image': compareCurrentImage(image) }") - img.image(v-lazy="image.file.url") + include ./mixins/dialogs/settings.pug + +settings() - //- dialog: Display previous avatar/world images - el-dialog.x-dialog(:before-close="beforeDialogClose" @mousedown.native="dialogMouseDown" @mouseup.native="dialogMouseUp" ref="previousImagesDialog" :visible.sync="previousImagesDialogVisible" :title="$t('dialog.previous_images.header')" width="800px") - div(v-if="previousImagesDialogVisible") - div(style="display:inline-block" v-for="image in previousImagesTable" :key="image.version" v-if="image.file") - el-popover.x-change-image-item(placement="right" width="500px" trigger="click") - img.x-link(slot="reference" v-lazy="image.file.url") - img.x-link(v-lazy="image.file.url" style="width:500px;height:375px" @click="showFullscreenImageDialog(image.file.url)") + include ./mixins/dialogs/previousInstances.pug + +previousInstances() - //- dialog: Gallery/VRCPlusIcons - el-dialog.x-dialog(:before-close="beforeDialogClose" @mousedown.native="dialogMouseDown" @mouseup.native="dialogMouseUp" ref="galleryDialog" :visible.sync="galleryDialogVisible" :title="$t('dialog.gallery_icons.header')" width="100%") - span(style="padding-bottom:10px") {{ $t('dialog.gallery_icons.description') }} - br - br - el-tabs(type="card" ref="galleryTabs") - el-tab-pane(v-if="galleryDialogVisible" v-loading="galleryDialogGalleryLoading") - span(slot="label") {{ $t('dialog.gallery_icons.gallery') }} - span(style="color:#909399;font-size:12px;margin-left:5px") {{ galleryTable.length }}/64 - input(type="file" accept="image/png,image/jpg,image/jpeg,image/webp,image/bmp,image/gif" @change="onFileChangeGallery" id="GalleryUploadButton" style="display:none") - el-button-group - el-button(type="default" size="small" @click="refreshGalleryTable" icon="el-icon-refresh") {{ $t('dialog.gallery_icons.refresh') }} - el-button(type="default" size="small" @click="displayGalleryUpload" icon="el-icon-upload2" :disabled="!API.currentUser.$isVRCPlus") {{ $t('dialog.gallery_icons.upload') }} - el-button(type="default" size="small" @click="setProfilePicOverride('')" icon="el-icon-close" :disabled="!API.currentUser.profilePicOverride") {{ $t('dialog.gallery_icons.clear') }} - br - .x-friend-item(v-if="image.versions && image.versions.length > 0" v-for="image in galleryTable" :key="image.id" style="display:inline-block;margin-top:10px;width:unset;cursor:default") - .vrcplus-icon(v-if="image.versions[image.versions.length - 1].file.url" @click="setProfilePicOverride(image.id)" :class="{ 'current-vrcplus-icon': compareCurrentProfilePic(image.id) }") - img.avatar(v-lazy="image.versions[image.versions.length - 1].file.url") - div(style="float:right;margin-top:5px") - el-button(type="default" @click="showFullscreenImageDialog(image.versions[image.versions.length - 1].file.url)" size="mini" icon="el-icon-download" circle) - el-button(type="default" @click="deleteGalleryImage(image.id)" size="mini" icon="el-icon-delete" circle style="margin-left:5px") - el-tab-pane(v-if="galleryDialogVisible" v-loading="galleryDialogIconsLoading") - span(slot="label") {{ $t('dialog.gallery_icons.icons') }} - span(style="color:#909399;font-size:12px;margin-left:5px") {{ VRCPlusIconsTable.length }}/64 - input(type="file" accept="image/png,image/jpg,image/jpeg,image/webp,image/bmp,image/gif" @change="onFileChangeVRCPlusIcon" id="VRCPlusIconUploadButton" style="display:none") - el-button-group - el-button(type="default" size="small" @click="refreshVRCPlusIconsTable" icon="el-icon-refresh") {{ $t('dialog.gallery_icons.refresh') }} - el-button(type="default" size="small" @click="displayVRCPlusIconUpload" icon="el-icon-upload2" :disabled="!API.currentUser.$isVRCPlus") {{ $t('dialog.gallery_icons.upload') }} - el-button(type="default" size="small" @click="setVRCPlusIcon('')" icon="el-icon-close" :disabled="!API.currentUser.userIcon") {{ $t('dialog.gallery_icons.clear') }} - br - .x-friend-item(v-if="image.versions && image.versions.length > 0" v-for="image in VRCPlusIconsTable" :key="image.id" style="display:inline-block;margin-top:10px;width:unset;cursor:default") - .vrcplus-icon(v-if="image.versions[image.versions.length - 1].file.url" @click="setVRCPlusIcon(image.id)" :class="{ 'current-vrcplus-icon': compareCurrentVRCPlusIcon(image.id) }") - img.avatar(v-lazy="image.versions[image.versions.length - 1].file.url") - div(style="float:right;margin-top:5px") - el-button(type="default" @click="showFullscreenImageDialog(image.versions[image.versions.length - 1].file.url)" size="mini" icon="el-icon-download" circle) - el-button(type="default" @click="deleteVRCPlusIcon(image.id)" size="mini" icon="el-icon-delete" circle style="margin-left:5px") - el-tab-pane(v-if="galleryDialogVisible" v-loading="galleryDialogEmojisLoading") - span(slot="label") {{ $t('dialog.gallery_icons.emojis') }} - span(style="color:#909399;font-size:12px;margin-left:5px") {{ emojiTable.length }}/9 - input(type="file" accept="image/png,image/jpg,image/jpeg,image/webp,image/bmp,image/gif" @change="onFileChangeEmoji" id="EmojiUploadButton" style="display:none") - el-button-group(style="margin-right:10px") - el-button(type="default" size="small" @click="refreshEmojiTable" icon="el-icon-refresh") {{ $t('dialog.gallery_icons.refresh') }} - el-button(type="default" size="small" @click="displayEmojiUpload" icon="el-icon-upload2" :disabled="!API.currentUser.$isVRCPlus") {{ $t('dialog.gallery_icons.upload') }} - el-select(v-model="emojiAnimationStyle" popper-class="max-height-el-select") - el-option-group {{ $t('dialog.gallery_icons.emoji_animation_styles') }} - el-option.x-friend-item(v-for="(fileName, styleName) in emojiAnimationStyleList" :key="fileName" :label="styleName" :value="styleName" style="height:auto") - .avatar(style="width:200px;height:200px") - img(v-lazy="`${emojiAnimationStyleUrl}${fileName}`") - .detail - span.name(v-text="styleName" style="margin-right:100px") - el-checkbox(v-model="emojiAnimType" style="margin-left:10px;margin-right:10px") - span {{ $t('dialog.gallery_icons.emoji_animation_type') }} - template(v-if="emojiAnimType") - span(style="margin-right:10px") {{ $t('dialog.gallery_icons.emoji_animation_fps') }} - el-input-number(size="small" v-model="emojiAnimFps" :min="1" :max="64" style="margin-right:10px;width:112px") - span(style="margin-right:10px") {{ $t('dialog.gallery_icons.emoji_animation_frame_count') }} - el-input-number(size="small" v-model="emojiAnimFrameCount" :min="2" :max="64" style="margin-right:10px;width:112px") - el-checkbox(v-model="emojiAnimLoopPingPong" style="margin-left:10px;margin-right:10px") - span {{ $t('dialog.gallery_icons.emoji_loop_pingpong') }} - br - span {{ $t('dialog.gallery_icons.flipbook_info') }} - br - .x-friend-item(v-if="image.versions && image.versions.length > 0" v-for="image in emojiTable" :key="image.id" style="display:inline-block;margin-top:10px;width:unset;cursor:default") - .vrcplus-icon(v-if="image.versions[image.versions.length - 1].file.url" style="overflow:hidden" @click="showFullscreenImageDialog(image.versions[image.versions.length - 1].file.url)") - template(v-if="image.frames") - .avatar(:style="generateEmojiStyle(image.versions[image.versions.length - 1].file.url, image.framesOverTime, image.frames, image.loopStyle)") - template(v-else) - img.avatar(v-lazy="image.versions[image.versions.length - 1].file.url") - div(style="display:inline-block;margin:5px") - span(v-if="image.loopStyle === 'pingpong'") #[i.el-icon-refresh.el-icon--left] - span(style="margin-right:5px") {{ image.animationStyle }} - span(v-if="image.framesOverTime" style="margin-right:5px") {{ image.framesOverTime }}fps - span(v-if="image.frames" style="margin-right:5px") {{ image.frames }}frames - br - div(style="float:right;margin-top:5px") - el-button(type="default" @click="showFullscreenImageDialog(image.versions[image.versions.length - 1].file.url)" size="mini" icon="el-icon-download" circle) - el-button(type="default" @click="deleteEmoji(image.id)" size="mini" icon="el-icon-delete" circle style="margin-left:5px") - el-tab-pane(v-if="galleryDialogVisible" v-loading="galleryDialogStickersLoading") - span(slot="label") {{ $t('dialog.gallery_icons.stickers') }} - span(style="color:#909399;font-size:12px;margin-left:5px") {{ stickerTable.length }}/9 - input(type="file" accept="image/png,image/jpg,image/jpeg,image/webp,image/bmp,image/gif" @change="onFileChangeSticker" id="StickerUploadButton" style="display:none") - el-button-group - el-button(type="default" size="small" @click="refreshStickerTable" icon="el-icon-refresh") {{ $t('dialog.gallery_icons.refresh') }} - el-button(type="default" size="small" @click="displayStickerUpload" icon="el-icon-upload2" :disabled="!API.currentUser.$isVRCPlus") {{ $t('dialog.gallery_icons.upload') }} - br - .x-friend-item(v-if="image.versions && image.versions.length > 0" v-for="image in stickerTable" :key="image.id" style="display:inline-block;margin-top:10px;width:unset;cursor:default") - .vrcplus-icon(v-if="image.versions[image.versions.length - 1].file.url" style="overflow:hidden" @click="showFullscreenImageDialog(image.versions[image.versions.length - 1].file.url)") - img.avatar(v-lazy="image.versions[image.versions.length - 1].file.url") - div(style="float:right;margin-top:5px") - el-button(type="default" @click="showFullscreenImageDialog(image.versions[image.versions.length - 1].file.url)" size="mini" icon="el-icon-download" circle) - el-button(type="default" @click="deleteSticker(image.id)" size="mini" icon="el-icon-delete" circle style="margin-left:5px") + include ./mixins/dialogs/tags.pug + +tags() - //- dialog Table: Previous Instances User - el-dialog.x-dialog(:before-close="beforeDialogClose" @mousedown.native="dialogMouseDown" @mouseup.native="dialogMouseUp" ref="previousInstancesUserDialog" :visible.sync="previousInstancesUserDialog.visible" :title="$t('dialog.previous_instances.header')" width="1000px") - span(v-text="previousInstancesUserDialog.userRef.displayName" style="font-size:14px") - el-input(v-model="previousInstancesUserDialogTable.filters[0].value" :placeholder="$t('dialog.previous_instances.search_placeholder')" style="display:block;width:150px;margin-top:15px") - data-tables(v-if="previousInstancesUserDialog.visible" v-bind="previousInstancesUserDialogTable" v-loading="previousInstancesUserDialog.loading" style="margin-top:10px") - el-table-column(:label="$t('table.previous_instances.date')" prop="created_at" sortable width="170") - template(v-once #default="scope") - span {{ scope.row.created_at | formatDate('long') }} - el-table-column(:label="$t('table.previous_instances.world')" prop="name" sortable) - template(v-once #default="scope") - location(:location="scope.row.location" :hint="scope.row.worldName" :grouphint="scope.row.groupName") - el-table-column(:label="$t('table.previous_instances.instance_creator')" prop="location" width="170") - template(v-once #default="scope") - display-name(:userid="scope.row.$location.userId" :location="scope.row.$location.tag" :key="previousInstancesUserDialog.forceUpdate") - el-table-column(:label="$t('table.previous_instances.time')" prop="time" width="100" sortable) - template(v-once #default="scope") - span(v-text="scope.row.timer") - el-table-column(:label="$t('table.previous_instances.action')" width="90" align="right") - template(v-once #default="scope") - el-button(type="text" icon="el-icon-info" size="mini" @click="showLaunchDialog(scope.row.location)") - el-button(type="text" icon="el-icon-tickets" size="mini" @click="showPreviousInstanceInfoDialog(scope.row.location)") - el-button(type="text" icon="el-icon-close" size="mini" @click="confirmDeleteGameLogUserInstance(scope.row)") - - //- dialog Table: Previous Instances World - el-dialog.x-dialog(:before-close="beforeDialogClose" @mousedown.native="dialogMouseDown" @mouseup.native="dialogMouseUp" ref="previousInstancesWorldDialog" :visible.sync="previousInstancesWorldDialog.visible" :title="$t('dialog.previous_instances.header')" width="1000px") - span(v-text="previousInstancesWorldDialog.worldRef.name" style="font-size:14px") - el-input(v-model="previousInstancesWorldDialogTable.filters[0].value" :placeholder="$t('dialog.previous_instances.search_placeholder')" style="display:block;width:150px;margin-top:15px") - data-tables(v-if="previousInstancesWorldDialog.visible" v-bind="previousInstancesWorldDialogTable" v-loading="previousInstancesWorldDialog.loading" style="margin-top:10px") - el-table-column(:label="$t('table.previous_instances.date')" prop="created_at" sortable width="170") - template(v-once #default="scope") - span {{ scope.row.created_at | formatDate('long') }} - el-table-column(:label="$t('table.previous_instances.instance_name')" prop="name") - template(v-once #default="scope") - location-world(:locationobject="scope.row.$location" :grouphint="scope.row.groupName" :currentuserid="API.currentUser.id") - el-table-column(:label="$t('table.previous_instances.instance_creator')" prop="location") - template(v-once #default="scope") - display-name(:userid="scope.row.$location.userId" :location="scope.row.$location.tag" :key="previousInstancesWorldDialog.forceUpdate") - el-table-column(:label="$t('table.previous_instances.time')" prop="time" width="100" sortable) - template(v-once #default="scope") - span(v-text="scope.row.timer") - el-table-column(:label="$t('table.previous_instances.action')" width="90" align="right") - template(v-once #default="scope") - el-button(type="text" icon="el-icon-tickets" size="mini" @click="showPreviousInstanceInfoDialog(scope.row.location)") - el-button(type="text" icon="el-icon-close" size="mini" @click="confirmDeleteGameLogWorldInstance(scope.row)") - - //- dialog Table: Previous Instance Info - el-dialog.x-dialog(:before-close="beforeDialogClose" @mousedown.native="dialogMouseDown" @mouseup.native="dialogMouseUp" ref="previousInstanceInfoDialog" :visible.sync="previousInstanceInfoDialog.visible" :title="$t('dialog.previous_instances.info')" width="800px") - location(:location="previousInstanceInfoDialog.$location.tag" style="font-size:14px") - el-input(v-model="previousInstanceInfoDialogTable.filters[0].value" placeholder="Search" style="display:block;width:150px;margin-top:15px") - data-tables(v-if="previousInstanceInfoDialog.visible" v-bind="previousInstanceInfoDialogTable" v-loading="previousInstanceInfoDialog.loading" style="margin-top:10px") - el-table-column(:label="$t('table.previous_instances.date')" prop="created_at" sortable width="120") - template(v-once #default="scope") - el-tooltip(placement="left") - template(#content) - span {{ scope.row.created_at | formatDate('long') }} - span {{ scope.row.created_at | formatDate('short') }} - el-table-column(:label="$t('table.gameLog.icon')" prop="isFriend" width="70") - template(v-once #default="scope") - template(v-if="gameLogIsFriend(scope.row)") - el-tooltip(v-if="gameLogIsFavorite(scope.row)" placement="top" content="Favorite") - span ⭐ - el-tooltip(v-else placement="top" content="Friend") - span 💚 - el-table-column(:label="$t('table.previous_instances.display_name')" prop="displayName" sortable) - template(v-once #default="scope") - span.x-link(v-text="scope.row.displayName" @click="lookupUser(scope.row)") - el-table-column(:label="$t('table.previous_instances.time')" prop="time" width="90" sortable) - template(v-once #default="scope") - span(v-text="scope.row.timer") - el-table-column(:label="$t('table.previous_instances.count')" prop="count" width="90" sortable) - template(v-once #default="scope") - span(v-text="scope.row.count") - - //- dialog: export world list - el-dialog.x-dialog(:before-close="beforeDialogClose" @mousedown.native="dialogMouseDown" @mouseup.native="dialogMouseUp" ref="worldExportDialogRef" :visible.sync="worldExportDialogVisible" :title="$t('dialog.world_export.header')" width="650px") - el-dropdown(@click.native.stop trigger="click" size="small") - el-button(size="mini") - span(v-if="worldExportFavoriteGroup") {{ worldExportFavoriteGroup.displayName }} ({{ worldExportFavoriteGroup.count }}/{{ worldExportFavoriteGroup.capacity }}) #[i.el-icon-arrow-down.el-icon--right] - span(v-else) All Favorites #[i.el-icon-arrow-down.el-icon--right] - el-dropdown-menu(#default="dropdown") - el-dropdown-item(style="display:block;margin:10px 0" @click.native="selectWorldExportGroup(null)") None - template(v-for="groupAPI in API.favoriteWorldGroups" :key="groupAPI.name") - el-dropdown-item(style="display:block;margin:10px 0" @click.native="selectWorldExportGroup(groupAPI)") {{ groupAPI.displayName }} ({{ groupAPI.count }}/{{ groupAPI.capacity }}) - el-dropdown(@click.native.stop trigger="click" size="small" style="margin-left:10px") - el-button(size="mini") - span(v-if="worldExportLocalFavoriteGroup") {{ worldExportLocalFavoriteGroup }} ({{ getLocalWorldFavoriteGroupLength(worldExportLocalFavoriteGroup) }}) #[i.el-icon-arrow-down.el-icon--right] - span(v-else) Select Group #[i.el-icon-arrow-down.el-icon--right] - el-dropdown-menu(#default="dropdown") - el-dropdown-item(style="display:block;margin:10px 0" @click.native="selectWorldExportLocalGroup(null)") None - template(v-for="group in localWorldFavoriteGroups" :key="group") - el-dropdown-item(style="display:block;margin:10px 0" @click.native="selectWorldExportLocalGroup(group)") {{ group }} ({{ localWorldFavorites[group].length }}) - br - el-input(type="textarea" v-if="worldExportDialogVisible" v-model="worldExportContent" size="mini" rows="15" resize="none" readonly style="margin-top:15px" @click.native="$event.target.tagName === 'TEXTAREA' && $event.target.select()") - - //- dialog: World import dialog - el-dialog.x-dialog(:before-close="beforeDialogClose" @mousedown.native="dialogMouseDown" @mouseup.native="dialogMouseUp" ref="worldImportDialog" :visible.sync="worldImportDialog.visible" :title="$t('dialog.world_import.header')" width="650px") - div(style="font-size:12px") - | {{ $t('dialog.world_import.description') }} - el-input(type="textarea" v-model="worldImportDialog.input" size="mini" rows="10" resize="none" style="margin-top:15px") - el-button(size="small" @click="processWorldImportList" :disabled="!worldImportDialog.input") {{ $t('dialog.world_import.process_list') }} - span(v-if="worldImportDialog.progress" style="margin-top:10px") #[i.el-icon-loading(style="margin-right:5px")] {{ $t('dialog.world_import.process_progress') }} {{ worldImportDialog.progress }}/{{ worldImportDialog.progressTotal }} - br - el-dropdown(@click.native.stop trigger="click" size="small" style="margin-right:5px") - el-button(size="mini") - span(v-if="worldImportDialog.worldImportFavoriteGroup") {{ worldImportDialog.worldImportFavoriteGroup.displayName }} ({{ worldImportDialog.worldImportFavoriteGroup.count }}/{{ worldImportDialog.worldImportFavoriteGroup.capacity }}) #[i.el-icon-arrow-down.el-icon--right] - span(v-else) {{ $t('dialog.world_import.select_vrchat_group_placeholder') }} #[i.el-icon-arrow-down.el-icon--right] - el-dropdown-menu(#default="dropdown") - template(v-for="groupAPI in API.favoriteWorldGroups" :key="groupAPI.name") - el-dropdown-item(style="display:block;margin:10px 0" @click.native="selectWorldImportGroup(groupAPI)" :disabled="groupAPI.count >= groupAPI.capacity") {{ groupAPI.displayName }} ({{ groupAPI.count }}/{{ groupAPI.capacity }}) - el-dropdown(@click.native.stop trigger="click" size="small" style="margin:5px") - el-button(size="mini") - span(v-if="worldImportDialog.worldImportLocalFavoriteGroup") {{ worldImportDialog.worldImportLocalFavoriteGroup }} ({{ getLocalWorldFavoriteGroupLength(worldImportDialog.worldImportLocalFavoriteGroup) }}) #[i.el-icon-arrow-down.el-icon--right] - span(v-else) {{ $t('dialog.world_import.select_local_group_placeholder') }} #[i.el-icon-arrow-down.el-icon--right] - el-dropdown-menu(#default="dropdown") - template(v-for="group in localWorldFavoriteGroups" :key="group") - el-dropdown-item(style="display:block;margin:10px 0" @click.native="selectWorldImportLocalGroup(group)" ) {{ group }} ({{ getLocalWorldFavoriteGroupLength(group) }}) - el-button(size="small" @click="importWorldImportTable" style="margin:5px" :disabled="worldImportTable.data.length === 0 || (!worldImportDialog.worldImportFavoriteGroup && !worldImportDialog.worldImportLocalFavoriteGroup)") {{ $t('dialog.world_import.import') }} - el-button(v-if="worldImportDialog.loading" size="small" @click="cancelWorldImport" style="margin-top:10px") {{ $t('dialog.world_import.cancel') }} - span(v-if="worldImportDialog.worldImportFavoriteGroup") {{ worldImportTable.data.length }} / {{ worldImportDialog.worldImportFavoriteGroup.capacity - worldImportDialog.worldImportFavoriteGroup.count }} - span(v-if="worldImportDialog.importProgress" style="margin:10px") #[i.el-icon-loading(style="margin-right:5px")] {{ $t('dialog.world_import.import_progress') }} {{ worldImportDialog.importProgress }}/{{ worldImportDialog.importProgressTotal }} - br - el-button(size="small" @click="clearWorldImportTable") {{ $t('dialog.world_import.clear_table') }} - template(v-if="worldImportDialog.errors") - el-button(size="small" @click="worldImportDialog.errors = ''" style="margin-left:5px") {{ $t('dialog.world_import.clear_errors') }} - h2(style="font-weight:bold;margin:0") {{ $t('dialog.world_import.errors') }} - pre(v-text="worldImportDialog.errors" style="white-space:pre-wrap;font-size:12px") - data-tables(v-if="worldImportDialog.visible" v-bind="worldImportTable" v-loading="worldImportDialog.loading" style="margin-top:10px") - el-table-column(:label="$t('table.import.image')" width="70" prop="thumbnailImageUrl") - template(v-once #default="scope") - el-popover(placement="right" height="500px" trigger="hover") - img.friends-list-avatar(slot="reference" v-lazy="scope.row.thumbnailImageUrl") - img.friends-list-avatar(v-lazy="scope.row.imageUrl" style="height:500px;cursor:pointer" @click="showFullscreenImageDialog(scope.row.imageUrl)") - el-table-column(:label="$t('table.import.name')" prop="name") - template(v-once #default="scope") - span.x-link(v-text="scope.row.name" @click="showWorldDialog(scope.row.id)") - el-table-column(:label="$t('table.import.author')" width="120" prop="authorName") - template(v-once #default="scope") - span.x-link(v-text="scope.row.authorName" @click="showUserDialog(scope.row.authorId)") - el-table-column(:label="$t('table.import.status')" width="70" prop="releaseStatus") - template(v-once #default="scope") - span(v-text="scope.row.releaseStatus" v-if="scope.row.releaseStatus === 'public'" style="color:#67c23a") - span(v-text="scope.row.releaseStatus" v-else-if="scope.row.releaseStatus === 'private'" style="color:#f56c6c") - span(v-text="scope.row.releaseStatus" v-else) - el-table-column(:label="$t('table.import.action')" width="90" align="right") - template(v-once #default="scope") - el-button(type="text" icon="el-icon-close" size="mini" @click="deleteItemWorldImport(scope.row)") - - //- dialog: export avatar list - el-dialog.x-dialog(:before-close="beforeDialogClose" @mousedown.native="dialogMouseDown" @mouseup.native="dialogMouseUp" ref="avatarExportDialogRef" :visible.sync="avatarExportDialogVisible" :title="$t('dialog.avatar_export.header')" width="650px") - el-dropdown(@click.native.stop trigger="click" size="small") - el-button(size="mini") - span(v-if="avatarExportFavoriteGroup") {{ avatarExportFavoriteGroup.displayName }} ({{ avatarExportFavoriteGroup.count }}/{{ avatarExportFavoriteGroup.capacity }}) #[i.el-icon-arrow-down.el-icon--right] - span(v-else) All Favorites #[i.el-icon-arrow-down.el-icon--right] - el-dropdown-menu(#default="dropdown") - el-dropdown-item(style="display:block;margin:10px 0" @click.native="selectAvatarExportGroup(null)") All Favorites - template(v-for="groupAPI in API.favoriteAvatarGroups" :key="groupAPI.name") - el-dropdown-item(style="display:block;margin:10px 0" @click.native="selectAvatarExportGroup(groupAPI)") {{ groupAPI.displayName }} ({{ groupAPI.count }}/{{ groupAPI.capacity }}) - el-dropdown(@click.native.stop trigger="click" size="small" style="margin-left:10px") - el-button(size="mini") - span(v-if="avatarExportLocalFavoriteGroup") {{ avatarExportLocalFavoriteGroup }} ({{ getLocalAvatarFavoriteGroupLength(avatarExportLocalFavoriteGroup) }}) #[i.el-icon-arrow-down.el-icon--right] - span(v-else) Select Group #[i.el-icon-arrow-down.el-icon--right] - el-dropdown-menu(#default="dropdown") - el-dropdown-item(style="display:block;margin:10px 0" @click.native="selectAvatarExportLocalGroup(null)") None - template(v-for="group in localAvatarFavoriteGroups" :key="group") - el-dropdown-item(style="display:block;margin:10px 0" @click.native="selectAvatarExportLocalGroup(group)" ) {{ group }} ({{ getLocalAvatarFavoriteGroupLength(group) }}) - br - el-input(type="textarea" v-if="avatarExportDialogVisible" v-model="avatarExportContent" size="mini" rows="15" resize="none" readonly style="margin-top:15px" @click.native="$event.target.tagName === 'TEXTAREA' && $event.target.select()") - - //- dialog: Avatar import dialog - el-dialog.x-dialog(:before-close="beforeDialogClose" @mousedown.native="dialogMouseDown" @mouseup.native="dialogMouseUp" ref="avatarImportDialog" :visible.sync="avatarImportDialog.visible" :title="$t('dialog.avatar_import.header')" width="650px") - div(style="font-size:12px") - | {{ $t('dialog.avatar_import.description') }} - el-input(type="textarea" v-model="avatarImportDialog.input" size="mini" rows="10" resize="none" style="margin-top:15px") - el-button(size="small" @click="processAvatarImportList" :disabled="!avatarImportDialog.input") {{ $t('dialog.avatar_import.process_list') }} - span(v-if="avatarImportDialog.progress" style="margin-top:10px") #[i.el-icon-loading(style="margin-right:5px")] {{ $t('dialog.avatar_import.process_progress') }} {{ avatarImportDialog.progress }}/{{ avatarImportDialog.progressTotal }} - br - el-dropdown(@click.native.stop trigger="click" size="small") - el-button(size="mini") - span(v-if="avatarImportDialog.avatarImportFavoriteGroup") {{ avatarImportDialog.avatarImportFavoriteGroup.displayName }} ({{ avatarImportDialog.avatarImportFavoriteGroup.count }}/{{ avatarImportDialog.avatarImportFavoriteGroup.capacity }}) #[i.el-icon-arrow-down.el-icon--right] - span(v-else) {{ $t('dialog.avatar_import.select_group_placeholder') }} #[i.el-icon-arrow-down.el-icon--right] - el-dropdown-menu(#default="dropdown") - template(v-for="groupAPI in API.favoriteAvatarGroups" :key="groupAPI.name") - el-dropdown-item(style="display:block;margin:10px 0" @click.native="selectAvatarImportGroup(groupAPI)" :disabled="groupAPI.count >= groupAPI.capacity") {{ groupAPI.displayName }} ({{ groupAPI.count }}/{{ groupAPI.capacity }}) - el-dropdown(@click.native.stop trigger="click" size="small" style="margin:5px") - el-button(size="mini") - span(v-if="avatarImportDialog.avatarImportLocalFavoriteGroup") {{ avatarImportDialog.avatarImportLocalFavoriteGroup }} ({{ getLocalAvatarFavoriteGroupLength(avatarImportDialog.avatarImportLocalFavoriteGroup) }}) #[i.el-icon-arrow-down.el-icon--right] - span(v-else) {{ $t('dialog.avatar_import.select_group_placeholder') }} #[i.el-icon-arrow-down.el-icon--right] - el-dropdown-menu(#default="dropdown") - template(v-for="group in localAvatarFavoriteGroups" :key="group") - el-dropdown-item(style="display:block;margin:10px 0" @click.native="selectAvatarImportLocalGroup(group)" ) {{ group }} ({{ getLocalAvatarFavoriteGroupLength(group) }}) - el-button(size="small" @click="importAvatarImportTable" style="margin:5px" :disabled="avatarImportTable.data.length === 0 || (!avatarImportDialog.avatarImportFavoriteGroup && !avatarImportDialog.avatarImportLocalFavoriteGroup)") {{ $t('dialog.avatar_import.import') }} - el-button(v-if="avatarImportDialog.loading" size="small" @click="cancelAvatarImport" style="margin-top:10px") {{ $t('dialog.avatar_import.cancel') }} - span(v-if="avatarImportDialog.avatarImportFavoriteGroup") {{ avatarImportTable.data.length }} / {{ avatarImportDialog.avatarImportFavoriteGroup.capacity - avatarImportDialog.avatarImportFavoriteGroup.count }} - span(v-if="avatarImportDialog.importProgress" style="margin:10px") #[i.el-icon-loading(style="margin-right:5px")] {{ $t('dialog.avatar_import.import_progress') }} {{ avatarImportDialog.importProgress }}/{{ avatarImportDialog.importProgressTotal }} - br - el-button(size="small" @click="clearAvatarImportTable") {{ $t('dialog.avatar_import.clear_table') }} - template(v-if="avatarImportDialog.errors") - el-button(size="small" @click="avatarImportDialog.errors = ''" style="margin-left:5px") {{ $t('dialog.avatar_import.clear_errors') }} - h2(style="font-weight:bold;margin:0") {{ $t('dialog.avatar_import.errors') }} - pre(v-text="avatarImportDialog.errors" style="white-space:pre-wrap;font-size:12px") - data-tables(v-if="avatarImportDialog.visible" v-bind="avatarImportTable" v-loading="avatarImportDialog.loading" style="margin-top:10px") - el-table-column(:label="$t('table.import.image')" width="70" prop="thumbnailImageUrl") - template(v-once #default="scope") - el-popover(placement="right" height="500px" trigger="hover") - img.friends-list-avatar(slot="reference" v-lazy="scope.row.thumbnailImageUrl") - img.friends-list-avatar(v-lazy="scope.row.imageUrl" style="height:500px;cursor:pointer" @click="showFullscreenImageDialog(scope.row.imageUrl)") - el-table-column(:label="$t('table.import.name')" prop="name") - template(v-once #default="scope") - span.x-link(v-text="scope.row.name" @click="showAvatarDialog(scope.row.id)") - el-table-column(:label="$t('table.import.author')" width="120" prop="authorName") - template(v-once #default="scope") - span.x-link(v-text="scope.row.authorName" @click="showUserDialog(scope.row.authorId)") - el-table-column(:label="$t('table.import.status')" width="70" prop="releaseStatus") - template(v-once #default="scope") - span(v-text="scope.row.releaseStatus" v-if="scope.row.releaseStatus === 'public'" style="color:#67c23a") - span(v-text="scope.row.releaseStatus" v-else-if="scope.row.releaseStatus === 'private'" style="color:#f56c6c") - span(v-text="scope.row.releaseStatus" v-else) - el-table-column(:label="$t('table.import.action')" width="90" align="right") - template(v-once #default="scope") - el-button(type="text" icon="el-icon-close" size="mini" @click="deleteItemAvatarImport(scope.row)") - - //- dialog: export friend list - el-dialog.x-dialog(:before-close="beforeDialogClose" @mousedown.native="dialogMouseDown" @mouseup.native="dialogMouseUp" ref="friendExportDialogRef" :visible.sync="friendExportDialogVisible" :title="$t('dialog.friend_export.header')" width="650px") - el-dropdown(@click.native.stop trigger="click" size="small") - el-button(size="mini") - span(v-if="friendExportFavoriteGroup") {{ friendExportFavoriteGroup.displayName }} ({{ friendExportFavoriteGroup.count }}/{{ friendExportFavoriteGroup.capacity }}) #[i.el-icon-arrow-down.el-icon--right] - span(v-else) All Favorites #[i.el-icon-arrow-down.el-icon--right] - el-dropdown-menu(#default="dropdown") - el-dropdown-item(style="display:block;margin:10px 0" @click.native="selectFriendExportGroup(null)") All Favorites - template(v-for="groupAPI in API.favoriteFriendGroups" :key="groupAPI.name") - el-dropdown-item(style="display:block;margin:10px 0" @click.native="selectFriendExportGroup(groupAPI)") {{ groupAPI.displayName }} ({{ groupAPI.count }}/{{ groupAPI.capacity }}) - br - el-input(type="textarea" v-if="friendExportDialogVisible" v-model="friendExportContent" size="mini" rows="15" resize="none" readonly style="margin-top:15px" @click.native="$event.target.tagName === 'TEXTAREA' && $event.target.select()") - - //- dialog: Friend import dialog - el-dialog.x-dialog(:before-close="beforeDialogClose" @mousedown.native="dialogMouseDown" @mouseup.native="dialogMouseUp" ref="friendImportDialog" :visible.sync="friendImportDialog.visible" :title="$t('dialog.friend_import.header')" width="650px") - div(style="font-size:12px") - | {{ $t('dialog.friend_import.description') }} - el-input(type="textarea" v-model="friendImportDialog.input" size="mini" rows="10" resize="none" style="margin-top:15px") - el-button(size="small" @click="processFriendImportList" :disabled="!friendImportDialog.input") {{ $t('dialog.friend_import.process_list') }} - span(v-if="friendImportDialog.progress" style="margin-top:10px") #[i.el-icon-loading(style="margin-right:5px")] {{ $t('dialog.friend_import.process_progress') }} {{ friendImportDialog.progress }}/{{ friendImportDialog.progressTotal }} - br - el-dropdown(@click.native.stop trigger="click" size="small") - el-button(size="mini") - span(v-if="friendImportDialog.friendImportFavoriteGroup") {{ friendImportDialog.friendImportFavoriteGroup.displayName }} ({{ friendImportDialog.friendImportFavoriteGroup.count }}/{{ friendImportDialog.friendImportFavoriteGroup.capacity }}) #[i.el-icon-arrow-down.el-icon--right] - span(v-else) {{ $t('dialog.friend_import.select_group_placeholder') }} #[i.el-icon-arrow-down.el-icon--right] - el-dropdown-menu(#default="dropdown") - template(v-for="groupAPI in API.favoriteFriendGroups" :key="groupAPI.name") - el-dropdown-item(style="display:block;margin:10px 0" @click.native="selectFriendImportGroup(groupAPI)" :disabled="groupAPI.count >= groupAPI.capacity") {{ groupAPI.displayName }} ({{ groupAPI.count }}/{{ groupAPI.capacity }}) - el-button(size="small" @click="importFriendImportTable" style="margin:5px" :disabled="friendImportTable.data.length === 0 || !friendImportDialog.friendImportFavoriteGroup") {{ $t('dialog.friend_import.import') }} - el-button(v-if="friendImportDialog.loading" size="small" @click="cancelFriendImport" style="margin-top:10px") {{ $t('dialog.friend_import.cancel') }} - span(v-if="friendImportDialog.friendImportFavoriteGroup") {{ friendImportTable.data.length }} / {{ friendImportDialog.friendImportFavoriteGroup.capacity - friendImportDialog.friendImportFavoriteGroup.count }} - span(v-if="friendImportDialog.importProgress" style="margin:10px") #[i.el-icon-loading(style="margin-right:5px")] {{ $t('dialog.friend_import.import_progress') }} {{ friendImportDialog.importProgress }}/{{ friendImportDialog.importProgressTotal }} - br - el-button(size="small" @click="clearFriendImportTable") {{ $t('dialog.friend_import.clear_table') }} - template(v-if="friendImportDialog.errors") - el-button(size="small" @click="friendImportDialog.errors = ''" style="margin-left:5px") {{ $t('dialog.friend_import.clear_errors') }} - h2(style="font-weight:bold;margin:0") {{ $t('dialog.friend_import.errors') }} - pre(v-text="friendImportDialog.errors" style="white-space:pre-wrap;font-size:12px") - data-tables(v-if="friendImportDialog.visible" v-bind="friendImportTable" v-loading="friendImportDialog.loading" style="margin-top:10px") - el-table-column(:label="$t('table.import.image')" width="70" prop="currentAvatarThumbnailImageUrl") - template(v-once #default="scope") - el-popover(placement="right" height="500px" trigger="hover") - img.friends-list-avatar(slot="reference" v-lazy="userImage(scope.row)") - img.friends-list-avatar(v-lazy="userImageFull(scope.row)" style="height:500px;cursor:pointer" @click="showFullscreenImageDialog(userImageFull(scope.row))") - el-table-column(:label="$t('table.import.name')" prop="displayName") - template(v-once #default="scope") - span.x-link(v-text="scope.row.displayName" @click="showUserDialog(scope.row.id)") - el-table-column(:label="$t('table.import.action')" width="90" align="right") - template(v-once #default="scope") - el-button(type="text" icon="el-icon-close" size="mini" @click="deleteItemFriendImport(scope.row)") - - //- dialog: Note export dialog - el-dialog.x-dialog(:before-close="beforeDialogClose" @mousedown.native="dialogMouseDown" @mouseup.native="dialogMouseUp" ref="noteExportDialog" :visible.sync="noteExportDialog.visible" :title="$t('dialog.note_export.header')" width="1000px") - div(style="font-size:12px") - | {{ $t('dialog.note_export.description1') }} #[br] - | {{ $t('dialog.note_export.description2') }} #[br] - | {{ $t('dialog.note_export.description3') }} #[br] - | {{ $t('dialog.note_export.description4') }} #[br] - | {{ $t('dialog.note_export.description5') }} #[br] - | {{ $t('dialog.note_export.description6') }} #[br] - | {{ $t('dialog.note_export.description7') }} #[br] - | {{ $t('dialog.note_export.description8') }} #[br] - el-button(size="small" @click="updateNoteExportDialog" :disabled="noteExportDialog.loading" style="margin-top:10px") {{ $t('dialog.note_export.refresh') }} - el-button(size="small" @click="exportNoteExport" :disabled="noteExportDialog.loading" style="margin-top:10px") {{ $t('dialog.note_export.export') }} - el-button(v-if="noteExportDialog.loading" size="small" @click="cancelNoteExport" style="margin-top:10px") {{ $t('dialog.note_export.cancel') }} - span(v-if="noteExportDialog.loading" style="margin:10px") #[i.el-icon-loading(style="margin-right:5px")] {{ $t('dialog.note_export.progress') }} {{ noteExportDialog.progress }}/{{ noteExportDialog.progressTotal }} - template(v-if="noteExportDialog.errors") - el-button(size="small" @click="noteExportDialog.errors = ''") {{ $t('dialog.note_export.clear_errors') }} - h2(style="font-weight:bold;margin:0") {{ $t('dialog.note_export.errors') }} - pre(v-text="noteExportDialog.errors" style="white-space:pre-wrap;font-size:12px") - data-tables(v-if="noteExportDialog.visible" v-bind="noteExportTable" v-loading="noteExportDialog.loading" style="margin-top:10px") - el-table-column(:label="$t('table.import.image')" width="70" prop="currentAvatarThumbnailImageUrl") - template(v-once #default="scope") - el-popover(placement="right" height="500px" trigger="hover") - img.friends-list-avatar(slot="reference" v-lazy="userImage(scope.row.ref)") - img.friends-list-avatar(v-lazy="userImageFull(scope.row.ref)" style="height:500px;cursor:pointer" @click="showFullscreenImageDialog(userImageFull(scope.row.ref))") - el-table-column(:label="$t('table.import.name')" width="170" prop="name") - template(v-once #default="scope") - span.x-link(v-text="scope.row.name" @click="showUserDialog(scope.row.id)") - el-table-column(:label="$t('table.import.note')" prop="memo") - template(v-once #default="scope") - el-input(v-model="scope.row.memo" type="textarea" maxlength="256" show-word-limit :rows="2" :autosize="{ minRows: 1, maxRows: 10 }" size="mini" resize="none") - el-table-column(:label="$t('table.import.skip_export')" width="90" align="right") - template(v-once #default="scope") - el-button(type="text" icon="el-icon-close" size="mini" @click="removeFromNoteExportTable(scope.row)") - - //- dialog: avatar database provider - el-dialog.x-dialog(:before-close="beforeDialogClose" @mousedown.native="dialogMouseDown" @mouseup.native="dialogMouseUp" ref="avatarProviderDialog" :visible.sync="avatarProviderDialog.visible" :title="$t('dialog.avatar_database_provider.header')" width="600px") - div - el-input(v-for="(provider, index) in avatarRemoteDatabaseProviderList" :key="index" :value="provider" v-model="avatarRemoteDatabaseProviderList[index]" @change="saveAvatarProviderList" size="small" style="margin-top:5px") - el-button(slot="append" icon="el-icon-delete" @click="removeAvatarProvider(provider)") - el-button(@click="avatarRemoteDatabaseProviderList.push('')" size="mini" style="margin-top:5px") {{ $t('dialog.avatar_database_provider.add_provider') }} - - //- dialog: chatbox blacklist - el-dialog.x-dialog(:before-close="beforeDialogClose" @mousedown.native="dialogMouseDown" @mouseup.native="dialogMouseUp" ref="chatboxBlacklistDialog" :visible.sync="chatboxBlacklistDialog.visible" :title="$t('dialog.chatbox_blacklist.header')" width="600px") - div(v-loading="chatboxBlacklistDialog.loading" v-if="chatboxBlacklistDialog.visible") - h2 {{ $t('dialog.chatbox_blacklist.keyword_blacklist') }} - el-input(v-for="(item, index) in chatboxBlacklist" :key="index" :value="item" v-model="chatboxBlacklist[index]" size="small" style="margin-top:5px" @change="saveChatboxBlacklist") - el-button(slot="append" icon="el-icon-delete" @click="chatboxBlacklist.splice(index, 1); saveChatboxBlacklist()") - el-button(@click="chatboxBlacklist.push('')" size="mini" style="margin-top:5px") {{ $t('dialog.chatbox_blacklist.add_item') }} - br - h2 {{ $t('dialog.chatbox_blacklist.user_blacklist') }} - el-tag(v-for="user in chatboxUserBlacklist" type="info" disable-transitions="true" :key="user[0]" style="margin-right:5px;margin-top:5px" closable @close="deleteChatboxUserBlacklist(user[0])") - span {{user[1]}} - - //- dialog: invite group - el-dialog.x-dialog(:before-close="beforeDialogClose" @mousedown.native="dialogMouseDown" @mouseup.native="dialogMouseUp" ref="inviteGroupDialog" :visible.sync="inviteGroupDialog.visible" :title="$t('dialog.invite_to_group.header')" width="450px") - div(v-if="inviteGroupDialog.visible" v-loading="inviteGroupDialog.loading") - span {{ $t('dialog.invite_to_group.description') }} - br - el-select(v-model="inviteGroupDialog.groupId" clearable :placeholder="$t('dialog.invite_to_group.choose_group_placeholder')" filterable :disabled="inviteGroupDialog.loading" @change="isAllowedToInviteToGroup" style="margin-top:15px") - el-option-group(v-if="API.currentUserGroups.size" :label="$t('dialog.invite_to_group.groups')" style="width:410px") - el-option.x-friend-item(v-for="group in API.currentUserGroups.values()" :key="group.id" :label="group.name" :value="group.id" style="height:auto") - .avatar - img(v-lazy="group.iconUrl") - .detail - span.name(v-text="group.name") - el-select(v-model="inviteGroupDialog.userIds" multiple clearable :placeholder="$t('dialog.invite_to_group.choose_friends_placeholder')" filterable :disabled="inviteGroupDialog.loading" style="width:100%;margin-top:15px") - el-option-group(v-if="inviteGroupDialog.userId" :label="$t('dialog.invite_to_group.selected_users')") - el-option.x-friend-item(:key="inviteGroupDialog.userObject.id" :label="inviteGroupDialog.userObject.displayName" :value="inviteGroupDialog.userObject.id" style="height:auto") - template(v-if="inviteGroupDialog.userObject.id") - .avatar(:class="userStatusClass(inviteGroupDialog.userObject)") - img(v-lazy="userImage(inviteGroupDialog.userObject)") - .detail - span.name(v-text="inviteGroupDialog.userObject.displayName" :style="{'color':inviteGroupDialog.userObject.$userColour}") - span(v-else v-text="inviteGroupDialog.userId") - el-option-group(v-if="vipFriends.length" :label="$t('side_panel.favorite')") - el-option.x-friend-item(v-for="friend in vipFriends" :key="friend.id" :label="friend.name" :value="friend.id" style="height:auto") - template(v-if="friend.ref") - .avatar(:class="userStatusClass(friend.ref)") - img(v-lazy="userImage(friend.ref)") - .detail - span.name(v-text="friend.ref.displayName" :style="{'color':friend.ref.$userColour}") - span(v-else v-text="friend.id") - el-option-group(v-if="onlineFriends.length" :label="$t('side_panel.online')") - el-option.x-friend-item(v-for="friend in onlineFriends" :key="friend.id" :label="friend.name" :value="friend.id" style="height:auto") - template(v-if="friend.ref") - .avatar(:class="userStatusClass(friend.ref)") - img(v-lazy="userImage(friend.ref)") - .detail - span.name(v-text="friend.ref.displayName" :style="{'color':friend.ref.$userColour}") - span(v-else v-text="friend.id") - el-option-group(v-if="activeFriends.length" :label="$t('side_panel.active')") - el-option.x-friend-item(v-for="friend in activeFriends" :key="friend.id" :label="friend.name" :value="friend.id" style="height:auto") - template(v-if="friend.ref") - .avatar - img(v-lazy="userImage(friend.ref)") - .detail - span.name(v-text="friend.ref.displayName" :style="{'color':friend.ref.$userColour}") - span(v-else v-text="friend.id") - el-option-group(v-if="offlineFriends.length" :label="$t('side_panel.offline')") - el-option.x-friend-item(v-for="friend in offlineFriends" :key="friend.id" :label="friend.name" :value="friend.id" style="height:auto") - template(v-if="friend.ref") - .avatar - img(v-lazy="userImage(friend.ref)") - .detail - span.name(v-text="friend.ref.displayName" :style="{'color':friend.ref.$userColour}") - span(v-else v-text="friend.id") - template(#footer) - el-button(type="primary" size="small" :disabled="inviteGroupDialog.loading || !inviteGroupDialog.userIds.length" @click="sendGroupInvite()") Invite - - //- dialog: screenshot metadata - el-dialog.x-dialog(:before-close="beforeDialogClose" @mousedown.native="dialogMouseDown" @mouseup.native="dialogMouseUp" ref="screenshotMetadataDialog" :visible.sync="screenshotMetadataDialog.visible" :title="$t('dialog.screenshot_metadata.header')" width="1050px") - div(v-if="screenshotMetadataDialog.visible" v-loading="screenshotMetadataDialog.loading" @dragover.prevent @dragenter.prevent @drop="handleDrop" style="-webkit-app-region: drag") - span(style="margin-left:5px;color:#909399;font-family:monospace") {{ $t('dialog.screenshot_metadata.drag') }} - br - br - el-button(size="small" icon="el-icon-folder-opened" @click="AppApi.OpenScreenshotFileDialog()") {{ $t('dialog.screenshot_metadata.browse') }} - el-button(size="small" icon="el-icon-picture-outline" @click="getAndDisplayLastScreenshot()") {{ $t('dialog.screenshot_metadata.last_screenshot') }} - el-button(size="small" icon="el-icon-copy-document" @click="copyImageToClipboard(screenshotMetadataDialog.metadata.filePath)") {{ $t('dialog.screenshot_metadata.copy_image') }} - el-button(size="small" icon="el-icon-folder" @click="openImageFolder(screenshotMetadataDialog.metadata.filePath)") {{ $t('dialog.screenshot_metadata.open_folder') }} - el-button(v-if="API.currentUser.$isVRCPlus && screenshotMetadataDialog.metadata.filePath" size="small" icon="el-icon-upload2" @click="uploadScreenshotToGallery") {{ $t('dialog.screenshot_metadata.upload') }} - br - br - //- Search bar input - el-input(v-model="screenshotMetadataDialog.search" size="small" placeholder="Search" clearable style="width:200px" @input="screenshotMetadataSearch") - //- Search index/total label - template(v-if="screenshotMetadataDialog.searchIndex != null") - span(style="white-space:pre-wrap;font-size:12px;margin-left:10px") {{ (screenshotMetadataDialog.searchIndex + 1) + "/" + screenshotMetadataDialog.searchResults.length }} - //- Search type dropdown - el-select(v-model="screenshotMetadataDialog.searchType" size="small" placeholder="Search Type" style="width:150px;margin-left:10px" @change="screenshotMetadataSearch") - el-option(v-for="type in screenshotMetadataDialog.searchTypes" :key="type" :label="type" :value="type") - br - br - span(v-text="screenshotMetadataDialog.metadata.fileName") - br - span(v-if="screenshotMetadataDialog.metadata.dateTime" style="margin-right:5px") {{ screenshotMetadataDialog.metadata.dateTime | formatDate('long') }} - span(v-if="screenshotMetadataDialog.metadata.fileResolution" v-text="screenshotMetadataDialog.metadata.fileResolution" style="margin-right:5px") - el-tag(v-if="screenshotMetadataDialog.metadata.fileSize" type="info" effect="plain" size="mini" v-text="screenshotMetadataDialog.metadata.fileSize") - br - location(v-if="screenshotMetadataDialog.metadata.world" :location="screenshotMetadataDialog.metadata.world.instanceId" :hint="screenshotMetadataDialog.metadata.world.name") - br - span.x-link(v-if="screenshotMetadataDialog.metadata.author" v-text="screenshotMetadataDialog.metadata.author.displayName" @click="showUserDialog(screenshotMetadataDialog.metadata.author.id)" style="color:#909399;font-family:monospace") - br - el-carousel(ref="screenshotMetadataCarousel" :interval="0" initial-index="1" indicator-position="none" arrow="always" height="600px" style="margin-top:10px" @change="screenshotMetadataCarouselChange") - el-carousel-item - span(placement="top" width="700px" trigger="click") - img.x-link(slot="reference" v-lazy="screenshotMetadataDialog.metadata.previousFilePath" style="width:100%;height:100%;object-fit:contain") - el-carousel-item - span(placement="top" width="700px" trigger="click" @click="showFullscreenImageDialog(screenshotMetadataDialog.metadata.filePath)") - img.x-link(slot="reference" v-lazy="screenshotMetadataDialog.metadata.filePath" style="width:100%;height:100%;object-fit:contain") - el-carousel-item - span(placement="top" width="700px" trigger="click") - img.x-link(slot="reference" v-lazy="screenshotMetadataDialog.metadata.nextFilePath" style="width:100%;height:100%;object-fit:contain") - br - template(v-if="screenshotMetadataDialog.metadata.error") - pre(v-text="screenshotMetadataDialog.metadata.error" style="white-space:pre-wrap;font-size:12px") - br - span(v-for="user in screenshotMetadataDialog.metadata.players" style="margin-top:5px") - span.x-link(v-text="user.displayName" @click="lookupUser(user)") - span(v-if="user.pos" v-text="'('+user.pos.x+', '+user.pos.y+', '+user.pos.z+')'" style="margin-left:5px;color:#909399;font-family:monospace") - br - - //- dialog: change log - el-dialog.x-dialog(:before-close="beforeDialogClose" @mousedown.native="dialogMouseDown" @mouseup.native="dialogMouseUp" ref="changeLogDialog" :visible.sync="changeLogDialog.visible" :title="$t('dialog.change_log.header')" width="800px") - .changelog-dialog(v-if="changeLogDialog.visible") - h2(v-text="changeLogDialog.buildName") - span {{ $t('dialog.change_log.description') }} #[a.x-link(@click="openExternalLink('https://www.patreon.com/Natsumi_VRCX')") Patreon], #[a.x-link(@click="openExternalLink('https://ko-fi.com/natsumi_sama')") Ko-fi]. - vue-markdown(:source="changeLogDialog.changeLog" :linkify="false") - template(#footer) - el-button(type="small" @click="openExternalLink('https://github.com/vrcx-team/VRCX/releases')") {{ $t('dialog.change_log.github') }} - el-button(type="small" @click="openExternalLink('https://patreon.com/Natsumi_VRCX')") {{ $t('dialog.change_log.donate') }} - el-button(type="small" @click="changeLogDialog.visible = false") {{ $t('dialog.change_log.close') }} - - //- dialog: gallery select - el-dialog.x-dialog(:before-close="beforeDialogClose" @mousedown.native="dialogMouseDown" @mouseup.native="dialogMouseUp" ref="gallerySelectDialog" :visible.sync="gallerySelectDialog.visible" :title="$t('dialog.gallery_select.header')" width="100%") - div(v-if="gallerySelectDialog.visible") - span(slot="label") {{ $t('dialog.gallery_select.gallery') }} - span(style="color:#909399;font-size:12px;margin-left:5px") {{ galleryTable.length }}/64 - br - input(type="file" accept="image/png,image/jpg,image/jpeg,image/webp,image/bmp,image/gif" @change="onFileChangeGallery" id="GalleryUploadButton" style="display:none") - el-button-group - el-button(type="default" size="small" @click="selectImageGallerySelect('', '')" icon="el-icon-close") {{ $t('dialog.gallery_select.none') }} - el-button(type="default" size="small" @click="refreshGalleryTable" icon="el-icon-refresh") {{ $t('dialog.gallery_select.refresh') }} - el-button(type="default" size="small" @click="displayGalleryUpload" icon="el-icon-upload2" :disabled="!API.currentUser.$isVRCPlus") {{ $t('dialog.gallery_select.upload') }} - br - .x-friend-item(v-if="image.versions && image.versions.length > 0" v-for="image in galleryTable" :key="image.id" style="display:inline-block;margin-top:10px;width:unset;cursor:default") - .vrcplus-icon(v-if="image.versions[image.versions.length - 1].file.url" @click="selectImageGallerySelect(image.versions[image.versions.length - 1].file.url, image.id)") - img.avatar(v-lazy="image.versions[image.versions.length - 1].file.url") - - //- dialog: full screen image - el-dialog.x-dialog(ref="fullscreenImageDialog" :before-close="beforeDialogClose" @mousedown.native="dialogMouseDown" @mouseup.native="dialogMouseUp" :visible.sync="fullscreenImageDialog.visible" top="3vh" width="97vw") - el-button(@click="copyImageUrl(fullscreenImageDialog.imageUrl)" size="mini" icon="el-icon-s-order" circle) - el-button(type="default" size="mini" icon="el-icon-download" circle @click="downloadAndSaveImage(fullscreenImageDialog.imageUrl)" style="margin-left:5px") - img(v-lazy="fullscreenImageDialog.imageUrl" style="width:100%;height:100vh;object-fit:contain") - - el-dialog.x-dialog(:before-close="beforeDialogClose" @closed="clearVrcRegistryDialog" @mousedown.native="dialogMouseDown" @mouseup.native="dialogMouseUp" ref="registryBackupDialog" :visible.sync="registryBackupDialog.visible" :title="$t('dialog.registry_backup.header')" width="600px") - div(v-if="registryBackupDialog.visible" style="margin-top:10px") - div.options-container - div.options-container-item - span.name {{ $t('dialog.registry_backup.auto_backup') }} - el-switch(v-model="vrcRegistryAutoBackup" @change="saveVrcRegistryAutoBackup") - el-button(@click="promptVrcRegistryBackupName" size="small") {{ $t('dialog.registry_backup.backup') }} - el-button(@click="AppApi.OpenVrcRegJsonFileDialog()" size="small") {{ $t('dialog.registry_backup.restore_from_file') }} - el-button(@click="deleteVrcRegistry" size="small") {{ $t('dialog.registry_backup.reset') }} - data-tables(v-bind="registryBackupTable" style="margin-top:10px") - el-table-column(:label="$t('dialog.registry_backup.name')" prop="name") - el-table-column(:label="$t('dialog.registry_backup.date')" prop="date") - template(v-once #default="scope") - span {{ scope.row.date | formatDate('long') }} - el-table-column(:label="$t('dialog.registry_backup.action')" width="90" align="right") - template(v-once #default="scope") - el-tooltip(placement="top" :content="$t('dialog.registry_backup.restore')" :disabled="hideTooltips") - el-button(type="text" icon="el-icon-upload2" size="mini" @click="restoreVrcRegistryBackup(scope.row)") - el-tooltip(placement="top" :content="$t('dialog.registry_backup.save_to_file')" :disabled="hideTooltips") - el-button(type="text" icon="el-icon-download" size="mini" @click="saveVrcRegistryBackupToFile(scope.row)") - el-tooltip(placement="top" :content="$t('dialog.registry_backup.delete')" :disabled="hideTooltips") - el-button(type="text" icon="el-icon-delete" size="mini" @click="deleteVrcRegistryBackup(scope.row)") - - //- dialog: group moderation - el-dialog.x-dialog(:before-close="beforeDialogClose" @mousedown.native="dialogMouseDown" @mouseup.native="dialogMouseUp" ref="groupMemberModeration" :visible.sync="groupMemberModeration.visible" :title="$t('dialog.group_member_moderation.header')" width="90vw") - div(v-if="groupMemberModeration.visible") - h3(v-text="groupMemberModeration.groupRef.name") - el-tabs(type="card" style="height:100%") - el-tab-pane(:label="$t('dialog.group_member_moderation.members')") - div(style="margin-top:10px") - el-button(type="default" @click="loadAllGroupMembers" size="mini" icon="el-icon-refresh" :loading="isGroupMembersLoading" circle) - span(style="font-size:14px;margin-left:5px;margin-right:5px") {{ groupMemberModerationTable.data.length }}/{{ groupMemberModeration.groupRef.memberCount }} - div(style="float:right;margin-top:5px") - span(style="margin-right:5px") {{ $t('dialog.group.members.sort_by') }} - el-dropdown(@click.native.stop trigger="click" size="small" style="margin-right:5px" :disabled="isGroupMembersLoading || groupDialog.memberSearch.length || !hasGroupPermission(groupDialog.ref, 'group-bans-manage')") - el-button(size="mini") - span {{ groupDialog.memberSortOrder.name }} #[i.el-icon-arrow-down.el-icon--right] - el-dropdown-menu(#default="dropdown") - el-dropdown-item(v-for="(item) in groupDialogSortingOptions" v-text="item.name" @click.native="setGroupMemberSortOrder(item)") - span(style="margin-right:5px") {{ $t('dialog.group.members.filter') }} - el-dropdown(@click.native.stop trigger="click" size="small" style="margin-right:5px" :disabled="isGroupMembersLoading || groupDialog.memberSearch.length || !hasGroupPermission(groupDialog.ref, 'group-bans-manage')") - el-button(size="mini") - span {{ groupDialog.memberFilter.name }} #[i.el-icon-arrow-down.el-icon--right] - el-dropdown-menu(#default="dropdown") - el-dropdown-item(v-for="(item) in groupDialogFilterOptions" v-text="item.name" @click.native="setGroupMemberFilter(item)") - el-dropdown-item(v-for="(item) in groupDialog.ref.roles" v-if="!item.defaultRole" v-text="item.name" @click.native="setGroupMemberFilter(item)") - el-input(v-model="groupDialog.memberSearch" :disabled="!hasGroupPermission(groupDialog.ref, 'group-bans-manage')" @input="groupMembersSearch" clearable size="mini" :placeholder="$t('dialog.group.members.search')" style="margin-top:10px;margin-bottom:10px") - br - el-button(size="small" @click="selectAllGroupMembers") {{ $t('dialog.group_member_moderation.select_all') }} - data-tables(v-bind="groupMemberModerationTable" style="margin-top:10px") - el-table-column(width="55" prop="$selected" :key="groupMemberModerationTableForceUpdate") - template(v-once #default="scope") - el-button(type="text" size="mini" @click.stop) - el-checkbox(v-model="scope.row.$selected" @change="groupMemberModerationTableSelectionChange(scope.row)") - el-table-column(:label="$t('dialog.group_member_moderation.avatar')" width="70" prop="photo") - template(v-once #default="scope") - el-popover(placement="right" height="500px" trigger="hover") - img.friends-list-avatar(slot="reference" v-lazy="userImage(scope.row.user)") - img.friends-list-avatar(v-lazy="userImageFull(scope.row.user)" style="height:500px;cursor:pointer" @click="showFullscreenImageDialog(userImageFull(scope.row.user))") - el-table-column(:label="$t('dialog.group_member_moderation.display_name')" width="160" prop="$displayName" sortable) - template(v-once #default="scope") - span(style="cursor:pointer" @click="showUserDialog(scope.row.userId)") - span(v-if="randomUserColours" v-text="scope.row.user.displayName" :style="{'color':scope.row.user.$userColour}") - span(v-else v-text="scope.row.user.displayName") - el-table-column(:label="$t('dialog.group_member_moderation.roles')" prop="roleIds" sortable) - template(v-once #default="scope") - template(v-for="roleId in scope.row.roleIds" :key="roleId") - span(v-for="(role, rIndex) in groupMemberModeration.groupRef.roles" :key="rIndex" v-if="role.id === roleId" v-text="role.name") - span(v-if="scope.row.roleIds.indexOf(roleId) < scope.row.roleIds.length - 1") ,  - el-table-column(:label="$t('dialog.group_member_moderation.notes')" prop="managerNotes" sortable) - template(v-once #default="scope") - span(v-text="scope.row.managerNotes" @click.stop) - el-table-column(:label="$t('dialog.group_member_moderation.joined_at')" width="170" prop="joinedAt" sortable) - template(v-once #default="scope") - span {{ scope.row.joinedAt | formatDate('long') }} - el-table-column(:label="$t('dialog.group_member_moderation.visibility')" width="120" prop="visibility" sortable) - template(v-once #default="scope") - span(v-text="scope.row.visibility") - el-tab-pane(:label="$t('dialog.group_member_moderation.bans')" :disabled="!hasGroupPermission(groupDialog.ref, 'group-bans-manage')") - div(style="margin-top:10px") - el-button(type="default" @click="getAllGroupBans(groupMemberModeration.id)" size="mini" icon="el-icon-refresh" :loading="isGroupMembersLoading" circle) - span(style="font-size:14px;margin-left:5px;margin-right:5px") {{ groupBansModerationTable.data.length }} - br - el-input(v-model="groupBansModerationTable.filters[0].value" clearable size="mini" :placeholder="$t('dialog.group.members.search')" style="margin-top:10px;margin-bottom:10px") - br - el-button(size="small" @click="selectAllGroupBans") {{ $t('dialog.group_member_moderation.select_all') }} - data-tables(v-bind="groupBansModerationTable" style="margin-top:10px") - el-table-column(width="55" prop="$selected" :key="groupMemberModerationTableForceUpdate") - template(v-once #default="scope") - el-button(type="text" size="mini" @click.stop) - el-checkbox(v-model="scope.row.$selected" @change="groupMemberModerationTableSelectionChange(scope.row)") - el-table-column(:label="$t('dialog.group_member_moderation.avatar')" width="70" prop="photo") - template(v-once #default="scope") - el-popover(placement="right" height="500px" trigger="hover") - img.friends-list-avatar(slot="reference" v-lazy="userImage(scope.row.user)") - img.friends-list-avatar(v-lazy="userImageFull(scope.row.user)" style="height:500px;cursor:pointer" @click="showFullscreenImageDialog(userImageFull(scope.row.user))") - el-table-column(:label="$t('dialog.group_member_moderation.display_name')" width="160" prop="$displayName" sortable) - template(v-once #default="scope") - span(style="cursor:pointer" @click="showUserDialog(scope.row.userId)") - span(v-if="randomUserColours" v-text="scope.row.user.displayName" :style="{'color':scope.row.user.$userColour}") - span(v-else v-text="scope.row.user.displayName") - el-table-column(:label="$t('dialog.group_member_moderation.roles')" prop="roleIds" sortable) - template(v-once #default="scope") - template(v-for="roleId in scope.row.roleIds" :key="roleId") - span(v-for="(role, rIndex) in groupMemberModeration.groupRef.roles" :key="rIndex" v-if="role.id === roleId" v-text="role.name") - span(v-if="scope.row.roleIds.indexOf(roleId) < scope.row.roleIds.length - 1") ,  - el-table-column(:label="$t('dialog.group_member_moderation.notes')" prop="managerNotes" sortable) - template(v-once #default="scope") - span(v-text="scope.row.managerNotes" @click.stop) - el-table-column(:label="$t('dialog.group_member_moderation.joined_at')" width="170" prop="joinedAt" sortable) - template(v-once #default="scope") - span {{ scope.row.joinedAt | formatDate('long') }} - el-table-column(:label="$t('dialog.group_member_moderation.banned_at')" width="170" prop="joinedAt" sortable) - template(v-once #default="scope") - span {{ scope.row.bannedAt | formatDate('long') }} - el-tab-pane(:label="$t('dialog.group_member_moderation.invites')" :disabled="!hasGroupPermission(groupDialog.ref, 'group-invites-manage')") - div(style="margin-top:10px") - el-button(type="default" @click="getAllGroupInvitesAndJoinRequests(groupMemberModeration.id)" size="mini" icon="el-icon-refresh" :loading="isGroupMembersLoading" circle) - br - el-tabs - el-tab-pane - span(slot="label") - span(v-text="$t('dialog.group_member_moderation.sent_invites')" style="font-weight:bold;font-size:16px") - span(style="color:#909399;font-size:12px;margin-left:5px") {{ groupInvitesModerationTable.data.length }} - el-button(size="small" @click="selectAllGroupInvites") {{ $t('dialog.group_member_moderation.select_all') }} - data-tables(v-bind="groupInvitesModerationTable" style="margin-top:10px") - el-table-column(width="55" prop="$selected" :key="groupMemberModerationTableForceUpdate") - template(v-once #default="scope") - el-button(type="text" size="mini" @click.stop) - el-checkbox(v-model="scope.row.$selected" @change="groupMemberModerationTableSelectionChange(scope.row)") - el-table-column(:label="$t('dialog.group_member_moderation.avatar')" width="70" prop="photo") - template(v-once #default="scope") - el-popover(placement="right" height="500px" trigger="hover") - img.friends-list-avatar(slot="reference" v-lazy="userImage(scope.row.user)") - img.friends-list-avatar(v-lazy="userImageFull(scope.row.user)" style="height:500px;cursor:pointer" @click="showFullscreenImageDialog(userImageFull(scope.row.user))") - el-table-column(:label="$t('dialog.group_member_moderation.display_name')" width="160" prop="$displayName" sortable) - template(v-once #default="scope") - span(style="cursor:pointer" @click="showUserDialog(scope.row.userId)") - span(v-if="randomUserColours" v-text="scope.row.user.displayName" :style="{'color':scope.row.user.$userColour}") - span(v-else v-text="scope.row.user.displayName") - el-table-column(:label="$t('dialog.group_member_moderation.notes')" prop="managerNotes" sortable) - template(v-once #default="scope") - span(v-text="scope.row.managerNotes" @click.stop) - br - el-button(@click="groupMembersDeleteSentInvite" :disabled="groupMemberModeration.progressCurrent || !hasGroupPermission(groupDialog.ref, 'group-invites-manage')") {{ $t('dialog.group_member_moderation.delete_sent_invite') }} - el-tab-pane - span(slot="label") - span(v-text="$t('dialog.group_member_moderation.join_requests')" style="font-weight:bold;font-size:16px") - span(style="color:#909399;font-size:12px;margin-left:5px") {{ groupJoinRequestsModerationTable.data.length }} - el-button(size="small" @click="selectAllGroupJoinRequests") {{ $t('dialog.group_member_moderation.select_all') }} - data-tables(v-bind="groupJoinRequestsModerationTable" style="margin-top:10px") - el-table-column(width="55" prop="$selected" :key="groupMemberModerationTableForceUpdate") - template(v-once #default="scope") - el-button(type="text" size="mini" @click.stop) - el-checkbox(v-model="scope.row.$selected" @change="groupMemberModerationTableSelectionChange(scope.row)") - el-table-column(:label="$t('dialog.group_member_moderation.avatar')" width="70" prop="photo") - template(v-once #default="scope") - el-popover(placement="right" height="500px" trigger="hover") - img.friends-list-avatar(slot="reference" v-lazy="userImage(scope.row.user)") - img.friends-list-avatar(v-lazy="userImageFull(scope.row.user)" style="height:500px;cursor:pointer" @click="showFullscreenImageDialog(userImageFull(scope.row.user))") - el-table-column(:label="$t('dialog.group_member_moderation.display_name')" width="160" prop="$displayName" sortable) - template(v-once #default="scope") - span(style="cursor:pointer" @click="showUserDialog(scope.row.userId)") - span(v-if="randomUserColours" v-text="scope.row.user.displayName" :style="{'color':scope.row.user.$userColour}") - span(v-else v-text="scope.row.user.displayName") - el-table-column(:label="$t('dialog.group_member_moderation.notes')" prop="managerNotes" sortable) - template(v-once #default="scope") - span(v-text="scope.row.managerNotes" @click.stop) - br - el-button(@click="groupMembersAcceptInviteRequest" :disabled="groupMemberModeration.progressCurrent || !hasGroupPermission(groupDialog.ref, 'group-invites-manage')") {{ $t('dialog.group_member_moderation.accept_join_requests') }} - el-button(@click="groupMembersRejectInviteRequest" :disabled="groupMemberModeration.progressCurrent || !hasGroupPermission(groupDialog.ref, 'group-invites-manage')") {{ $t('dialog.group_member_moderation.reject_join_requests') }} - el-button(@click="groupMembersBlockJoinRequest" :disabled="groupMemberModeration.progressCurrent || !hasGroupPermission(groupDialog.ref, 'group-invites-manage')") {{ $t('dialog.group_member_moderation.block_join_requests') }} - el-tab-pane - span(slot="label") - span(v-text="$t('dialog.group_member_moderation.blocked_requests')" style="font-weight:bold;font-size:16px") - span(style="color:#909399;font-size:12px;margin-left:5px") {{ groupBlockedModerationTable.data.length }} - el-button(size="small" @click="selectAllGroupBlocked") {{ $t('dialog.group_member_moderation.select_all') }} - data-tables(v-bind="groupBlockedModerationTable" style="margin-top:10px") - el-table-column(width="55" prop="$selected" :key="groupMemberModerationTableForceUpdate") - template(v-once #default="scope") - el-button(type="text" size="mini" @click.stop) - el-checkbox(v-model="scope.row.$selected" @change="groupMemberModerationTableSelectionChange(scope.row)") - el-table-column(:label="$t('dialog.group_member_moderation.avatar')" width="70" prop="photo") - template(v-once #default="scope") - el-popover(placement="right" height="500px" trigger="hover") - img.friends-list-avatar(slot="reference" v-lazy="userImage(scope.row.user)") - img.friends-list-avatar(v-lazy="userImageFull(scope.row.user)" style="height:500px;cursor:pointer" @click="showFullscreenImageDialog(userImageFull(scope.row.user))") - el-table-column(:label="$t('dialog.group_member_moderation.display_name')" width="160" prop="$displayName" sortable) - template(v-once #default="scope") - span(style="cursor:pointer" @click="showUserDialog(scope.row.userId)") - span(v-if="randomUserColours" v-text="scope.row.user.displayName" :style="{'color':scope.row.user.$userColour}") - span(v-else v-text="scope.row.user.displayName") - el-table-column(:label="$t('dialog.group_member_moderation.notes')" prop="managerNotes" sortable) - template(v-once #default="scope") - span(v-text="scope.row.managerNotes" @click.stop) - br - el-button(@click="groupMembersDeleteBlockedRequest" :disabled="groupMemberModeration.progressCurrent || !hasGroupPermission(groupDialog.ref, 'group-invites-manage')") {{ $t('dialog.group_member_moderation.delete_blocked_requests') }} - el-tab-pane(:label="$t('dialog.group_member_moderation.logs')" :disabled="!hasGroupPermission(groupDialog.ref, 'group-audit-view')") - div(style="margin-top:10px") - el-button(type="default" @click="getAllGroupLogs(groupMemberModeration.id)" size="mini" icon="el-icon-refresh" :loading="isGroupMembersLoading" circle) - span(style="font-size:14px;margin-left:5px;margin-right:5px") {{ groupLogsModerationTable.data.length }} - br - el-select(v-model="groupMemberModeration.selectedAuditLogTypes" multiple collapse-tags :placeholder="$t('dialog.group_member_moderation.filter_type')") - el-option-group(:label="$t('dialog.group_member_moderation.select_type')") - el-option.x-friend-item(v-for="type in groupMemberModeration.auditLogTypes" :key="type" :label="getAuditLogTypeName(type)" :value="type") - .detail - span.name(v-text="getAuditLogTypeName(type)") - el-input(v-model="groupLogsModerationTable.filters[0].value" :placeholder="$t('dialog.group_member_moderation.search_placeholder')" style="display:inline-block;width:150px;margin:10px") - br - data-tables(v-bind="groupLogsModerationTable" style="margin-top:10px") - el-table-column(:label="$t('dialog.group_member_moderation.created_at')" width="170" prop="created_at" sortable) - template(v-once #default="scope") - span {{ scope.row.created_at | formatDate('long') }} - el-table-column(:label="$t('dialog.group_member_moderation.type')" width="190" prop="eventType" sortable) - template(v-once #default="scope") - span(v-text="scope.row.eventType") - el-table-column(:label="$t('dialog.group_member_moderation.display_name')" width="160" prop="actorDisplayName" sortable) - template(v-once #default="scope") - span(style="cursor:pointer" @click="showUserDialog(scope.row.actorId)") - span(v-text="scope.row.actorDisplayName") - el-table-column(:label="$t('dialog.group_member_moderation.description')" prop="description") - template(v-once #default="scope") - span(v-text="scope.row.description") - el-table-column(:label="$t('dialog.group_member_moderation.data')" prop="data") - template(v-once #default="scope") - span(v-if="Object.keys(scope.row.data).length" v-text="JSON.stringify(scope.row.data)") - br - br - span.name {{ $t('dialog.group_member_moderation.user_id') }} - br - el-input(v-model="groupMemberModeration.selectUserId" size="mini" style="margin-top:5px;width:340px" :placeholder="$t('dialog.group_member_moderation.user_id_placeholder')" clearable) - el-button(size="small" @click="selectGroupMemberUserId" :disabled="!groupMemberModeration.selectUserId") {{ $t('dialog.group_member_moderation.select_user') }} - br - br - span.name {{ $t('dialog.group_member_moderation.selected_users') }} - el-button(type="default" @click="clearSelectedGroupMembers" size="mini" icon="el-icon-delete" circle style="margin-left:5px") - br - el-tag(v-for="user in groupMemberModeration.selectedUsersArray" type="info" disable-transitions="true" :key="user.id" style="margin-right:5px;margin-top:5px" closable @close="deleteSelectedGroupMember(user)") - span {{ user.user?.displayName }} #[i.el-icon-warning(v-if="user.membershipStatus !== 'member'" style="margin-left:5px")] - br - br - span.name {{ $t('dialog.group_member_moderation.notes') }} - el-input.extra(v-model="groupMemberModeration.note" type="textarea" :rows="2" :autosize="{ minRows: 1, maxRows: 20 }" :placeholder="$t('dialog.group_member_moderation.note_placeholder')" size="mini" resize="none" style="margin-top:5px") - br - br - span.name {{ $t('dialog.group_member_moderation.selected_roles') }} - br - el-select(v-model="groupMemberModeration.selectedRoles" clearable multiple :placeholder="$t('dialog.group_member_moderation.choose_roles_placeholder')" filterable style="margin-top:5px") - el-option-group(:label="$t('dialog.group_member_moderation.roles')") - el-option.x-friend-item(v-for="role in groupMemberModeration.groupRef.roles" :key="role.id" :label="role.name" :value="role.id" style="height:auto") - .detail - span.name(v-text="role.name") - br - br - span.name {{ $t('dialog.group_member_moderation.actions') }} - br - el-button(@click="groupMembersAddRoles" :disabled="!groupMemberModeration.selectedRoles.length || groupMemberModeration.progressCurrent || !hasGroupPermission(groupDialog.ref, 'group-roles-assign')") {{ $t('dialog.group_member_moderation.add_roles') }} - el-button(@click="groupMembersRemoveRoles" :disabled="!groupMemberModeration.selectedRoles.length || groupMemberModeration.progressCurrent || !hasGroupPermission(groupDialog.ref, 'group-roles-assign')") {{ $t('dialog.group_member_moderation.remove_roles') }} - el-button(@click="groupMembersSaveNote" :disabled="groupMemberModeration.progressCurrent || !hasGroupPermission(groupDialog.ref, 'group-members-manage')") {{ $t('dialog.group_member_moderation.save_note') }} - el-button(@click="groupMembersKick" :disabled="groupMemberModeration.progressCurrent || !hasGroupPermission(groupDialog.ref, 'group-members-remove')") {{ $t('dialog.group_member_moderation.kick') }} - el-button(@click="groupMembersBan" :disabled="groupMemberModeration.progressCurrent || !hasGroupPermission(groupDialog.ref, 'group-bans-manage')") {{ $t('dialog.group_member_moderation.ban') }} - el-button(@click="groupMembersUnban" :disabled="groupMemberModeration.progressCurrent || !hasGroupPermission(groupDialog.ref, 'group-bans-manage')") {{ $t('dialog.group_member_moderation.unban') }} - span(v-if="groupMemberModeration.progressCurrent" style="margin-top:10px") #[i.el-icon-loading(style="margin-left:5px;margin-right:5px")] {{ $t('dialog.group_member_moderation.progress') }} {{ groupMemberModeration.progressCurrent }}/{{ groupMemberModeration.progressTotal }} - el-button(v-if="groupMemberModeration.progressCurrent" @click="groupMemberModeration.progressTotal = 0" style="margin-left:5px") {{ $t('dialog.group_member_moderation.cancel') }} - - //- dialog: group posts - el-dialog.x-dialog(:before-close="beforeDialogClose" @mousedown.native="dialogMouseDown" @mouseup.native="dialogMouseUp" ref="groupPostEditDialog" :visible.sync="groupPostEditDialog.visible" :title="$t('dialog.group_post_edit.header')" width="650px") - div(v-if="groupPostEditDialog.visible") - h3(v-text="groupPostEditDialog.groupRef.name") - el-form(:model="groupPostEditDialog" label-width="150px") - el-form-item(:label="$t('dialog.group_post_edit.title')") - el-input(v-model="groupPostEditDialog.title" size="mini") - el-form-item(:label="$t('dialog.group_post_edit.message')") - el-input(v-model="groupPostEditDialog.text" type="textarea" :rows="4" :autosize="{ minRows: 4, maxRows: 20 }" style="margin-top:10px" resize="none") - el-form-item - el-checkbox(v-if="!groupPostEditDialog.postId" v-model="groupPostEditDialog.sendNotification" size="small") {{ $t('dialog.group_post_edit.send_notification') }} - el-form-item(:label="$t('dialog.group_post_edit.post_visibility')") - el-radio-group(v-model="groupPostEditDialog.visibility" size="small") - el-radio(label="public") {{ $t('dialog.group_post_edit.visibility_public') }} - el-radio(label="group") {{ $t('dialog.group_post_edit.visibility_group') }} - el-form-item(v-if="groupPostEditDialog.visibility === 'group'" :label="$t('dialog.new_instance.roles')") - el-select(v-model="groupPostEditDialog.roleIds" multiple clearable :placeholder="$t('dialog.new_instance.role_placeholder')" style="width:100%") - el-option-group(:label="$t('dialog.new_instance.role_placeholder')") - el-option.x-friend-item(v-for="role in groupPostEditDialog.groupRef?.roles" :key="role.id" :label="role.name" :value="role.id" style="height:auto;width:478px") - .detail - span.name(v-text="role.name") - el-form-item(:label="$t('dialog.group_post_edit.image')") - template(v-if="gallerySelectDialog.selectedFileId") - div(style="display:inline-block;flex:none;margin-right:5px") - el-popover(placement="right" width="500px" trigger="click") - img.x-link(slot="reference" v-lazy="gallerySelectDialog.selectedImageUrl" style="flex:none;width:60px;height:60px;border-radius:4px;object-fit:cover") - img.x-link(v-lazy="gallerySelectDialog.selectedImageUrl" style="height:500px" @click="showFullscreenImageDialog(gallerySelectDialog.selectedImageUrl)") - el-button(size="mini" @click="clearImageGallerySelect" style="vertical-align:top") {{ $t('dialog.invite_message.clear_selected_image') }} - template(v-else) - el-button(size="mini" @click="showGallerySelectDialog" style="margin-right:5px") {{ $t('dialog.invite_message.select_image') }} - - template(#footer) - el-button(size="small" @click="groupPostEditDialog.visible = false") {{ $t('dialog.group_post_edit.cancel') }} - el-button(v-if="groupPostEditDialog.postId" size="small" @click="editGroupPost") {{ $t('dialog.group_post_edit.edit_post') }} - el-button(v-else size="small" @click="createGroupPost") {{ $t('dialog.group_post_edit.create_post') }} - - //- dialog: send boop - el-dialog.x-dialog(:before-close="beforeDialogClose" @mousedown.native="dialogMouseDown" @mouseup.native="dialogMouseUp" ref="sendBoopDialog" :visible.sync="sendBoopDialog.visible" :title="$t('dialog.boop_dialog.header')" width="450px") - div(v-if="sendBoopDialog.visible") - el-select(v-model="sendBoopDialog.userId" :placeholder="$t('dialog.new_instance.instance_creator_placeholder')" filterable style="width:100%") - el-option-group(v-if="vipFriends.length" :label="$t('side_panel.favorite')") - el-option.x-friend-item(v-for="friend in vipFriends" :key="friend.id" :label="friend.name" :value="friend.id" style="height:auto") - template(v-if="friend.ref") - .avatar(:class="userStatusClass(friend.ref)") - img(v-lazy="userImage(friend.ref)") - .detail - span.name(v-text="friend.ref.displayName" :style="{'color':friend.ref.$userColour}") - span(v-else v-text="friend.id") - el-option-group(v-if="onlineFriends.length" :label="$t('side_panel.online')") - el-option.x-friend-item(v-for="friend in onlineFriends" :key="friend.id" :label="friend.name" :value="friend.id" style="height:auto") - template(v-if="friend.ref") - .avatar(:class="userStatusClass(friend.ref)") - img(v-lazy="userImage(friend.ref)") - .detail - span.name(v-text="friend.ref.displayName" :style="{'color':friend.ref.$userColour}") - span(v-else v-text="friend.id") - el-option-group(v-if="activeFriends.length" :label="$t('side_panel.active')") - el-option.x-friend-item(v-for="friend in activeFriends" :key="friend.id" :label="friend.name" :value="friend.id" style="height:auto") - template(v-if="friend.ref") - .avatar - img(v-lazy="userImage(friend.ref)") - .detail - span.name(v-text="friend.ref.displayName" :style="{'color':friend.ref.$userColour}") - span(v-else v-text="friend.id") - el-option-group(v-if="offlineFriends.length" :label="$t('side_panel.offline')") - el-option.x-friend-item(v-for="friend in offlineFriends" :key="friend.id" :label="friend.name" :value="friend.id" style="height:auto") - template(v-if="friend.ref") - .avatar - img(v-lazy="userImage(friend.ref)") - .detail - span.name(v-text="friend.ref.displayName" :style="{'color':friend.ref.$userColour}") - span(v-else v-text="friend.id") - br - br - el-select(v-model="sendBoopDialog.fileId" clearable :placeholder="$t('dialog.boop_dialog.select_emoji')" size="small" style="width:100%" popper-class="max-height-el-select") - el-option-group(:label="$t('dialog.boop_dialog.my_emojis')") - el-option(v-if="image.versions && image.versions.length > 0" v-for="image in emojiTable" :key="image.id" :value="image.id" style="width:100%;height:100%") - .vrcplus-icon(v-if="image.versions[image.versions.length - 1].file.url" style="overflow:hidden;width:200px;height:200px;padding:10px") - template(v-if="image.frames") - .avatar(:style="generateEmojiStyle(image.versions[image.versions.length - 1].file.url, image.framesOverTime, image.frames, image.loopStyle)") - template(v-else) - img.avatar(v-lazy="image.versions[image.versions.length - 1].file.url" style="width:200px;height:200px") - el-option-group(:label="$t('dialog.boop_dialog.default_emojis')") - el-option(v-for="emojiName in photonEmojis" :key="emojiName" :value="getEmojiValue(emojiName)" style="width:100%;height:100%") - span(v-text="emojiName") - template(#footer) - el-button(size="small" @click="showGalleryDialog(2)") {{ $t('dialog.boop_dialog.emoji_manager') }} - el-button(size="small" @click="sendBoopDialog.visible = false") {{ $t('dialog.boop_dialog.cancel') }} - el-button(size="small" @click="sendBoop" :disabled="!sendBoopDialog.userId") {{ $t('dialog.boop_dialog.send') }} + include ./mixins/dialogs/boops.pug + +boops() //- el-dialog.x-dialog(:before-close="beforeDialogClose" @mousedown.native="dialogMouseDown" @mouseup.native="dialogMouseUp" ref="templateDialog" :visible.sync="templateDialog.visible" :title="$t('dialog.template_dialog.header')" width="450px") - //- dialog: open source software notice - el-dialog.x-dialog(:before-close="beforeDialogClose" @mousedown.native="dialogMouseDown" @mouseup.native="dialogMouseUp" :visible.sync="ossDialog" :title="$t('dialog.open_source.header')" width="650px") - div(v-if="ossDialog" style="height:350px;overflow:hidden scroll;word-break:break-all") - div - span {{ $t('dialog.open_source.description') }} - div(style="margin-top:15px") - p(style="font-weight:bold") animate.css - pre(style="font-size:12px;white-space:pre-line"). - The MIT License (MIT) - - Copyright (c) 2019 Daniel Eden - - Permission is hereby granted, free of charge, to any person obtaining a copy - of this software and associated documentation files (the "Software"), to deal - in the Software without restriction, including without limitation the rights - to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - copies of the Software, and to permit persons to whom the Software is - furnished to do so, subject to the following conditions: - - The above copyright notice and this permission notice shall be included in all - copies or substantial portions of the Software. - - THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE - SOFTWARE. - div(style="margin-top:15px") - p(style="font-weight:bold") CefSharp - pre(style="font-size:12px;white-space:pre-line"). - // Copyright © The CefSharp Authors. All rights reserved. - // - // Redistribution and use in source and binary forms, with or without - // modification, are permitted provided that the following conditions are - // met: - // - // * Redistributions of source code must retain the above copyright - // notice, this list of conditions and the following disclaimer. - // - // * Redistributions in binary form must reproduce the above - // copyright notice, this list of conditions and the following disclaimer - // in the documentation and/or other materials provided with the - // distribution. - // - // * Neither the name of Google Inc. nor the name Chromium Embedded - // Framework nor the name CefSharp nor the names of its contributors - // may be used to endorse or promote products derived from this software - // without specific prior written permission. - // - // THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS - // "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT - // LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR - // A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT - // OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, - // SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT - // LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, - // DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY - // THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT - // (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE - // OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - div(style="margin-top:15px") - p(style="font-weight:bold") DiscordRichPresence - pre(style="font-size:12px;white-space:pre-line"). - MIT License - - Copyright (c) 2018 Lachee - - Permission is hereby granted, free of charge, to any person obtaining a copy - of this software and associated documentation files (the "Software"), to deal - in the Software without restriction, including without limitation the rights - to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - copies of the Software, and to permit persons to whom the Software is - furnished to do so, subject to the following conditions: - - The above copyright notice and this permission notice shall be included in all - copies or substantial portions of the Software. - - THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE - SOFTWARE. - div(style="margin-top:15px") - p(style="font-weight:bold") element - pre(style="font-size:12px;white-space:pre-line"). - The MIT License (MIT) - - Copyright (c) 2016-present ElemeFE - - Permission is hereby granted, free of charge, to any person obtaining a copy - of this software and associated documentation files (the "Software"), to deal - in the Software without restriction, including without limitation the rights - to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - copies of the Software, and to permit persons to whom the Software is - furnished to do so, subject to the following conditions: - - The above copyright notice and this permission notice shall be included in all - copies or substantial portions of the Software. - - THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE - SOFTWARE. - div(style="margin-top:15px") - p(style="font-weight:bold") librsync.net - pre(style="font-size:12px;white-space:pre-line"). - The MIT License (MIT) - - Copyright (c) 2015 Brad Dodson - - Permission is hereby granted, free of charge, to any person obtaining a copy - of this software and associated documentation files (the "Software"), to deal - in the Software without restriction, including without limitation the rights - to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - copies of the Software, and to permit persons to whom the Software is - furnished to do so, subject to the following conditions: - - The above copyright notice and this permission notice shall be included in all - copies or substantial portions of the Software. - - THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE - SOFTWARE. - div(style="margin-top:15px") - p(style="font-weight:bold") Newtonsoft.Json - pre(style="font-size:12px;white-space:pre-line"). - The MIT License (MIT) - - Copyright (c) 2007 James Newton-King - - Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: - - The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. - - THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - div(style="margin-top:15px") - p(style="font-weight:bold") normalize - pre(style="font-size:12px;white-space:pre-line"). - The MIT License (MIT) - - Copyright © Nicolas Gallagher and Jonathan Neal - - Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: - - The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. - - THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - div(style="margin-top:15px") - p(style="font-weight:bold") noty - pre(style="font-size:12px;white-space:pre-line"). - Copyright (c) 2012 Nedim Arabacı - - Permission is hereby granted, free of charge, to any person obtaining - a copy of this software and associated documentation files (the - "Software"), to deal in the Software without restriction, including - without limitation the rights to use, copy, modify, merge, publish, - distribute, sublicense, and/or sell copies of the Software, and to - permit persons to whom the Software is furnished to do so, subject to - the following conditions: - - The above copyright notice and this permission notice shall be - included in all copies or substantial portions of the Software. - - THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, - EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF - MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND - NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE - LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION - OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION - WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - div(style="margin-top:15px") - p(style="font-weight:bold") OpenVR SDK - pre(style="font-size:12px;white-space:pre-line"). - Copyright (c) 2015, Valve Corporation - All rights reserved. - - Redistribution and use in source and binary forms, with or without modification, - are permitted provided that the following conditions are met: - - 1. Redistributions of source code must retain the above copyright notice, this - list of conditions and the following disclaimer. - - 2. Redistributions in binary form must reproduce the above copyright notice, - this list of conditions and the following disclaimer in the documentation and/or - other materials provided with the distribution. - - 3. Neither the name of the copyright holder nor the names of its contributors - may be used to endorse or promote products derived from this software without - specific prior written permission. - - THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND - ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED - WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE - DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR - ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES - (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; - LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON - ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT - (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS - SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - div(style="margin-top:15px") - p(style="font-weight:bold") Twemoji - pre(style="font-size:12px;white-space:pre-line"). - MIT License - - Copyright (c) 2021 Twitter - - Permission is hereby granted, free of charge, to any person obtaining a copy - of this software and associated documentation files (the "Software"), to deal - in the Software without restriction, including without limitation the rights - to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - copies of the Software, and to permit persons to whom the Software is - furnished to do so, subject to the following conditions: - - The above copyright notice and this permission notice shall be included in all - copies or substantial portions of the Software. - - THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE - SOFTWARE. - div(style="margin-top:15px") - p(style="font-weight:bold") SharpDX - pre(style="font-size:12px;white-space:pre-line"). - Copyright (c) 2010-2014 SharpDX - Alexandre Mutel - - Permission is hereby granted, free of charge, to any person obtaining a copy - of this software and associated documentation files (the "Software"), to deal - in the Software without restriction, including without limitation the rights - to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - copies of the Software, and to permit persons to whom the Software is - furnished to do so, subject to the following conditions: - - The above copyright notice and this permission notice shall be included in - all copies or substantial portions of the Software. - - THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - THE SOFTWARE. - div(style="margin-top:15px") - p(style="font-weight:bold") vue - pre(style="font-size:12px;white-space:pre-line"). - The MIT License (MIT) - - Copyright (c) 2013-present, Yuxi (Evan) You - - Permission is hereby granted, free of charge, to any person obtaining a copy - of this software and associated documentation files (the "Software"), to deal - in the Software without restriction, including without limitation the rights - to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - copies of the Software, and to permit persons to whom the Software is - furnished to do so, subject to the following conditions: - - The above copyright notice and this permission notice shall be included in - all copies or substantial portions of the Software. - - THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - THE SOFTWARE. - div(style="margin-top:15px") - p(style="font-weight:bold") vue-data-tables - pre(style="font-size:12px;white-space:pre-line"). - The MIT License (MIT) - - Copyright (c) 2018 Leon Zhang - - Permission is hereby granted, free of charge, to any person obtaining a copy - of this software and associated documentation files (the "Software"), to deal - in the Software without restriction, including without limitation the rights - to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - copies of the Software, and to permit persons to whom the Software is - furnished to do so, subject to the following conditions: - - The above copyright notice and this permission notice shall be included in all - copies or substantial portions of the Software. - - THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE - SOFTWARE. - div(style="margin-top:15px") - p(style="font-weight:bold") vue-lazyload - pre(style="font-size:12px;white-space:pre-line"). - The MIT License (MIT) - - Copyright (c) 2016 Awe - - Permission is hereby granted, free of charge, to any person obtaining a copy - of this software and associated documentation files (the "Software"), to deal - in the Software without restriction, including without limitation the rights - to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - copies of the Software, and to permit persons to whom the Software is - furnished to do so, subject to the following conditions: - - The above copyright notice and this permission notice shall be included in all - copies or substantial portions of the Software. - - THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE - SOFTWARE. - - div(style="margin-top:15px") - p(style="font-weight:bold") Encode Sans Font (from Dark Vanilla) - pre(style="font-size:12px;white-space:pre-line"). - SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 - Copyright (c) 2020 June 20, Impallari Type, Andres Torresi, Jacques Le Bailly - (https://fonts.google.com/specimen/Encode+Sans), - with Reserved Font Name: Encode Sans. - - PREAMBLE: - The goals of the Open Font License (OFL) are to stimulate worldwide development - of collaborative font projects, to support the font creation efforts of academic - and linguistic communities, and to provide a free and open framework in which - fonts may be shared and improved in partnership with others. - - The OFL allows the licensed fonts to be used, studied, modified and redistributed - freely as long as they are not sold by themselves. The fonts, including any - derivative works, can be bundled, embedded, redistributed and/or sold with any - software provided that any reserved names are not used by derivative works. - The fonts and derivatives, however, cannot be released under any other type of - license. The requirement for fonts to remain under this license does not apply - to any document created using the fonts or their derivatives. - - PERMISSION & CONDITIONS - Permission is hereby granted, free of charge, to any person obtaining a copy of - the Font Software, to use, study, copy, merge, embed, modify, redistribute, and - sell modified and unmodified copies of the Font Software, subject to the - following conditions: - - 1. Neither the Font Software nor any of its individual components, in Original or - Modified Versions, may be sold by itself. - - 2. Original or Modified Versions of the Font Software may be bundled, redistributed - and/or sold with any software, provided that each copy contains the above copyright - notice and this license. These can be included either as stand-alone text files, - human-readable headers or in the appropriate machine-readable metadata fields within - text or binary files as long as those fields can be easily viewed by the user. - - 3. No Modified Version of the Font Software may use the Reserved Font Name(s) unless - explicit written permission is granted by the corresponding Copyright Holder. This - restriction only applies to the primary font name as presented to the users. - - 4. The name(s) of the Copyright Holder(s) or the Author(s) of the Font Software shall - not be used to promote, endorse or advertise any Modified Version, except to - acknowledge the contribution(s) of the Copyright Holder(s) and the Author(s) or with - their explicit written permission. - - 5. The Font Software, modified or unmodified, in part or in whole, must be distributed - entirely under this license, and must not be distributed under any other license. - The requirement for fonts to remain under this license does not apply to any document - created using the Font Software. - - TERMINATION - This license becomes null and void if any of the above conditions are not met. - - DISCLAIMER - THE FONT SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF MERCHANTABILITY, FITNESS - FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT OF COPYRIGHT, PATENT, TRADEMARK, OR - OTHER RIGHT. IN NO EVENT SHALL THE COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, - DAMAGES OR OTHER LIABILITY, INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, - OR CONSEQUENTIAL DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, - ARISING FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM OTHER - DEALINGS IN THE FONT SOFTWARE. - - //- dialog: Enable primary password - el-dialog.x-dialog( - :visible.sync="enablePrimaryPasswordDialog.visible" - :before-close="enablePrimaryPasswordDialog.beforeClose" - ref="primaryPasswordDialog" - :close-on-click-modal="false" - :title="$t('dialog.primary_password.header')" - width="400px" - ) - el-input( - v-model="enablePrimaryPasswordDialog.password" - :placeholder="$t('dialog.primary_password.password_placeholder')" - type="password" - size="mini" - maxlength="32" - show-password - autofocus - ) - el-input( - v-model="enablePrimaryPasswordDialog.rePassword" - :placeholder="$t('dialog.primary_password.re_input_placeholder')" - type="password" - style="margin-top:5px" - size="mini" - maxlength="32" - show-password - ) - template(#footer) - el-button( - type="primary" size="small" @click="setPrimaryPassword" - :disabled="enablePrimaryPasswordDialog.password.length===0||enablePrimaryPasswordDialog.password!==enablePrimaryPasswordDialog.rePassword" - ) {{ $t('dialog.primary_password.ok') }} script(src="vendor.js") - script(src="app.js") + script(src="app.js") \ No newline at end of file diff --git a/html/src/mixins/dialogs/avatarDialog.pug b/html/src/mixins/dialogs/avatarDialog.pug new file mode 100644 index 000000000..157a47c8f --- /dev/null +++ b/html/src/mixins/dialogs/avatarDialog.pug @@ -0,0 +1,122 @@ + +mixin avatarDialog() + el-dialog.x-dialog.x-avatar-dialog(:before-close="beforeDialogClose" @mousedown.native="dialogMouseDown" @mouseup.native="dialogMouseUp" ref="avatarDialog" :visible.sync="avatarDialog.visible" :show-close="false" width="600px") + div(v-loading="avatarDialog.loading") + div(style="display:flex") + el-popover(placement="right" width="500px" trigger="click") + img.x-link(slot="reference" v-lazy="avatarDialog.ref.thumbnailImageUrl" style="flex:none;width:160px;height:120px;border-radius:12px") + img.x-link(v-lazy="avatarDialog.ref.imageUrl" style="width:500px;height:375px" @click="showFullscreenImageDialog(avatarDialog.ref.imageUrl)") + div(style="flex:1;display:flex;align-items:center;margin-left:15px") + div(style="flex:1") + div + span.dialog-title(v-text="avatarDialog.ref.name") + div(style="margin-top:5px") + span.x-link.x-grey(v-text="avatarDialog.ref.authorName" @click="showUserDialog(avatarDialog.ref.authorId)" style="font-family:monospace") + div + el-tag(v-if="avatarDialog.ref.releaseStatus === 'public'" type="success" effect="plain" size="mini" style="margin-right:5px;margin-top:5px") {{ $t('dialog.avatar.tags.public') }} + el-tag(v-else type="danger" effect="plain" size="mini" style="margin-right:5px;margin-top:5px") {{ $t('dialog.avatar.tags.private') }} + el-tag.x-tag-platform-pc(v-if="avatarDialog.isPC" type="info" effect="plain" size="mini" style="margin-right:5px;margin-top:5px") PC + span.x-grey(v-if="avatarDialog.platformInfo.pc" style=";margin-left:5px;border-left:inherit;padding-left:5px") {{ avatarDialog.platformInfo.pc.performanceRating }} + span.x-grey(v-if="avatarDialog.bundleSizes['standalonewindows']" style=";margin-left:5px;border-left:inherit;padding-left:5px") {{ avatarDialog.bundleSizes['standalonewindows'].fileSize }} + el-tag.x-tag-platform-quest(v-if="avatarDialog.isQuest" type="info" effect="plain" size="mini" style="margin-right:5px;margin-top:5px") Android + span.x-grey(v-if="avatarDialog.platformInfo.android" style=";margin-left:5px;border-left:inherit;padding-left:5px") {{ avatarDialog.platformInfo.android.performanceRating }} + span.x-grey(v-if="avatarDialog.bundleSizes['android']" style="margin-left:5px;border-left:inherit;padding-left:5px") {{ avatarDialog.bundleSizes['android'].fileSize }} + el-tag.x-tag-platform-ios(v-if="avatarDialog.isIos" type="info" effect="plain" size="mini" style="margin-right:5px;margin-top:5px") iOS + span.x-grey(v-if="avatarDialog.platformInfo.ios" style=";margin-left:5px;border-left:inherit;padding-left:5px") {{ avatarDialog.platformInfo.ios.performanceRating }} + span.x-grey(v-if="avatarDialog.bundleSizes['ios']" style="margin-left:5px;border-left:inherit;padding-left:5px") {{ avatarDialog.bundleSizes['ios'].fileSize }} + el-tag.x-link(v-if="avatarDialog.inCache" type="info" effect="plain" size="mini" @click="openFolderGeneric(avatarDialog.cachePath)" style="margin-right:5px;margin-top:5px") + span(v-text="avatarDialog.cacheSize") + | {{ $t('dialog.avatar.tags.cache') }} + el-tag(v-if="avatarDialog.isQuestFallback" type="info" effect="plain" size="mini" style="margin-right:5px;margin-top:5px") {{ $t('dialog.avatar.tags.fallback') }} + el-tag(v-if="avatarDialog.hasImposter" type="info" effect="plain" size="mini" style="margin-right:5px;margin-top:5px") {{ $t('dialog.avatar.tags.impostor') }} + span.x-grey(v-if="avatarDialog.imposterVersion" style="margin-left:5px;border-left:inherit;padding-left:5px") v{{ avatarDialog.imposterVersion }} + el-tag(v-if="avatarDialog.ref.unityPackageUrl" type="success" effect="plain" size="mini" style="margin-right:5px;margin-top:5px") {{ $t('dialog.avatar.tags.future_proofing') }} + div + template(v-for="tag in avatarDialog.ref.tags") + el-tag(v-if="tag.startsWith('content_')" :key="tag" effect="plain" size="mini" style="margin-right:5px;margin-top:5px") + template(v-if="tag === 'content_horror'") {{ $t('dialog.avatar.tags.content_horror') }} + template(v-else-if="tag === 'content_gore'") {{ $t('dialog.avatar.tags.content_gore') }} + template(v-else-if="tag === 'content_violence'") {{ $t('dialog.avatar.tags.content_violence') }} + template(v-else-if="tag === 'content_adult'") {{ $t('dialog.avatar.tags.content_adult') }} + template(v-else-if="tag === 'content_sex'") {{ $t('dialog.avatar.tags.content_sex') }} + template(v-else) {{ tag.replace('content_', '') }} + div(style="margin-top:5px") + span(v-show="avatarDialog.ref.name !== avatarDialog.ref.description" v-text="avatarDialog.ref.description" style="font-size:12px") + div(style="flex:none;margin-left:10px") + el-tooltip(v-if="avatarDialog.inCache" placement="top" :content="$t('dialog.avatar.actions.delete_cache_tooltip')" :disabled="hideTooltips") + el-button(icon="el-icon-delete" circle @click="deleteVRChatCache(avatarDialog.ref)" :disabled="isGameRunning && avatarDialog.cacheLocked") + el-tooltip(v-if="avatarDialog.isFavorite" placement="top" :content="$t('dialog.avatar.actions.favorite_tooltip')" :disabled="hideTooltips") + el-button(type="warning" icon="el-icon-star-on" circle @click="avatarDialogCommand('Add Favorite')" style="margin-left:5px") + el-tooltip(v-else placement="top" :content="$t('dialog.avatar.actions.favorite_tooltip')" :disabled="hideTooltips") + el-button(type="default" icon="el-icon-star-off" circle @click="avatarDialogCommand('Add Favorite')" style="margin-left:5px") + el-dropdown(trigger="click" @command="avatarDialogCommand" size="small" style="margin-left:5px") + el-button(:type="avatarDialog.isBlocked ? 'danger' : 'default'" icon="el-icon-more" circle) + el-dropdown-menu(#default="dropdown") + el-dropdown-item(icon="el-icon-refresh" command="Refresh") {{ $t('dialog.avatar.actions.refresh') }} + el-dropdown-item(icon="el-icon-check" :disabled="API.currentUser.currentAvatar === avatarDialog.id" command="Select Avatar") {{ $t('dialog.avatar.actions.select') }} + el-dropdown-item(v-if="/quest/.test(avatarDialog.ref.tags)" icon="el-icon-check" command="Select Fallback Avatar") {{ $t('dialog.avatar.actions.select_fallback') }} + el-dropdown-item(v-if="avatarDialog.isBlocked" icon="el-icon-circle-check" command="Unblock Avatar" style="color:#F56C6C") {{ $t('dialog.avatar.actions.unblock') }} + el-dropdown-item(v-else icon="el-icon-circle-close" command="Block Avatar") {{ $t('dialog.avatar.actions.block') }} + el-dropdown-item(v-if="avatarDialog.ref.authorId !== API.currentUser.id" icon="el-icon-picture-outline" command="Previous Images") {{ $t('dialog.avatar.actions.show_previous_images') }} + template(v-if="avatarDialog.ref.authorId === API.currentUser.id") + el-dropdown-item(v-if="avatarDialog.ref.releaseStatus === 'public'" icon="el-icon-user-solid" command="Make Private" divided) {{ $t('dialog.avatar.actions.make_private') }} + el-dropdown-item(v-else icon="el-icon-user" command="Make Public" divided) {{ $t('dialog.avatar.actions.make_public') }} + el-dropdown-item(icon="el-icon-edit" command="Rename") {{ $t('dialog.avatar.actions.rename') }} + el-dropdown-item(icon="el-icon-edit" command="Change Description") {{ $t('dialog.avatar.actions.change_description') }} + el-dropdown-item(icon="el-icon-edit" command="Change Content Tags") {{ $t('dialog.avatar.actions.change_content_tags') }} + el-dropdown-item(icon="el-icon-picture-outline" command="Change Image") {{ $t('dialog.avatar.actions.change_image') }} + el-dropdown-item(v-if="avatarDialog.ref.unityPackageUrl" icon="el-icon-download" command="Download Unity Package") {{ $t('dialog.avatar.actions.download_package') }} + el-dropdown-item(v-if="avatarDialog.hasImposter" icon="el-icon-delete" command="Delete Imposter" style="color:#F56C6C") {{ $t('dialog.avatar.actions.delete_impostor') }} + el-dropdown-item(v-else icon="el-icon-user" command="Create Imposter") {{ $t('dialog.avatar.actions.create_impostor') }} + el-dropdown-item(icon="el-icon-delete" command="Delete" style="color:#F56C6C" divided) {{ $t('dialog.avatar.actions.delete') }} + el-tabs + el-tab-pane(:label="$t('dialog.avatar.info.header')") + .x-friend-list + .x-friend-item(style="width:100%;cursor:default") + .detail + span.name {{ $t('dialog.avatar.info.memo') }} + el-input.extra(v-model="avatarDialog.memo" @change="onAvatarMemoChange" size="mini" type="textarea" :rows="2" :autosize="{minRows: 1, maxRows: 20}" :placeholder="$t('dialog.avatar.info.memo_placeholder')" resize="none") + .x-friend-item(style="width:100%;cursor:default") + .detail + span.name {{ $t('dialog.avatar.info.id') }} + span.extra {{ avatarDialog.id }} + el-tooltip(placement="top" :content="$t('dialog.avatar.info.id_tooltip')" :disabled="hideTooltips") + el-dropdown(trigger="click" @click.native.stop size="mini" style="margin-left:5px") + el-button(type="default" icon="el-icon-s-order" size="mini" circle) + el-dropdown-menu(#default="dropdown") + el-dropdown-item(@click.native="copyAvatarId(avatarDialog.id)") {{ $t('dialog.avatar.info.copy_id') }} + el-dropdown-item(@click.native="copyAvatarUrl(avatarDialog.id)") {{ $t('dialog.avatar.info.copy_url') }} + .x-friend-item(style="cursor:default") + .detail + span.name {{ $t('dialog.avatar.info.created_at') }} + span.extra {{ avatarDialog.ref.created_at | formatDate('long') }} + .x-friend-item(style="cursor:default") + .detail + span.name {{ $t('dialog.avatar.info.last_updated') }} + span.extra(v-if="avatarDialog.lastUpdated") {{ avatarDialog.lastUpdated | formatDate('long') }} + span.extra(v-else) {{ avatarDialog.ref.updated_at | formatDate('long') }} + .x-friend-item(style="cursor:default") + .detail + span.name {{ $t('dialog.avatar.info.version') }} + span.extra(v-if="avatarDialog.ref.version !== 0" v-text="avatarDialog.ref.version") + span.extra(v-else) - + .x-friend-item(style="width:100%;cursor:default") + .detail + span.name {{ $t('dialog.avatar.info.platform') }} + span.extra(v-if="avatarDialogPlatform" v-text="avatarDialogPlatform") + span.extra(v-else) - + el-tab-pane(:label="$t('dialog.avatar.json.header')") + el-button(type="default" @click="refreshAvatarDialogTreeData()" size="mini" icon="el-icon-refresh" circle) + el-tooltip(placement="top" :content="$t('dialog.avatar.json.file_analysis')" :disabled="hideTooltips") + el-button(type="default" @click="getAvatarFileAnalysis" size="mini" icon="el-icon-s-data" circle style="margin-left:5px") + el-button(type="default" @click="downloadAndSaveJson(avatarDialog.id, avatarDialog.ref)" size="mini" icon="el-icon-download" circle style="margin-left:5px") + el-tree(v-if="Object.keys(avatarDialog.fileAnalysis).length > 0" :data="avatarDialog.fileAnalysis" style="margin-top:5px;font-size:12px") + template(#default="scope") + span + span(v-text="scope.data.key" style="font-weight:bold;margin-right:5px") + span(v-if="!scope.data.children" v-text="scope.data.value") + el-tree(:data="avatarDialog.treeData" style="margin-top:5px;font-size:12px") + template(#default="scope") + span + span(v-text="scope.data.key" style="font-weight:bold;margin-right:5px") + span(v-if="!scope.data.children" v-text="scope.data.value") diff --git a/html/src/mixins/dialogs/boops.pug b/html/src/mixins/dialogs/boops.pug new file mode 100644 index 000000000..49321b96b --- /dev/null +++ b/html/src/mixins/dialogs/boops.pug @@ -0,0 +1,53 @@ +mixin boops() + el-dialog.x-dialog(:before-close="beforeDialogClose" @mousedown.native="dialogMouseDown" @mouseup.native="dialogMouseUp" ref="sendBoopDialog" :visible.sync="sendBoopDialog.visible" :title="$t('dialog.boop_dialog.header')" width="450px") + div(v-if="sendBoopDialog.visible") + el-select(v-model="sendBoopDialog.userId" :placeholder="$t('dialog.new_instance.instance_creator_placeholder')" filterable style="width:100%") + el-option-group(v-if="vipFriends.length" :label="$t('side_panel.favorite')") + el-option.x-friend-item(v-for="friend in vipFriends" :key="friend.id" :label="friend.name" :value="friend.id" style="height:auto") + template(v-if="friend.ref") + .avatar(:class="userStatusClass(friend.ref)") + img(v-lazy="userImage(friend.ref)") + .detail + span.name(v-text="friend.ref.displayName" :style="{'color':friend.ref.$userColour}") + span(v-else v-text="friend.id") + el-option-group(v-if="onlineFriends.length" :label="$t('side_panel.online')") + el-option.x-friend-item(v-for="friend in onlineFriends" :key="friend.id" :label="friend.name" :value="friend.id" style="height:auto") + template(v-if="friend.ref") + .avatar(:class="userStatusClass(friend.ref)") + img(v-lazy="userImage(friend.ref)") + .detail + span.name(v-text="friend.ref.displayName" :style="{'color':friend.ref.$userColour}") + span(v-else v-text="friend.id") + el-option-group(v-if="activeFriends.length" :label="$t('side_panel.active')") + el-option.x-friend-item(v-for="friend in activeFriends" :key="friend.id" :label="friend.name" :value="friend.id" style="height:auto") + template(v-if="friend.ref") + .avatar + img(v-lazy="userImage(friend.ref)") + .detail + span.name(v-text="friend.ref.displayName" :style="{'color':friend.ref.$userColour}") + span(v-else v-text="friend.id") + el-option-group(v-if="offlineFriends.length" :label="$t('side_panel.offline')") + el-option.x-friend-item(v-for="friend in offlineFriends" :key="friend.id" :label="friend.name" :value="friend.id" style="height:auto") + template(v-if="friend.ref") + .avatar + img(v-lazy="userImage(friend.ref)") + .detail + span.name(v-text="friend.ref.displayName" :style="{'color':friend.ref.$userColour}") + span(v-else v-text="friend.id") + br + br + el-select(v-model="sendBoopDialog.fileId" clearable :placeholder="$t('dialog.boop_dialog.select_emoji')" size="small" style="width:100%" popper-class="max-height-el-select") + el-option-group(:label="$t('dialog.boop_dialog.my_emojis')") + el-option(v-if="image.versions && image.versions.length > 0" v-for="image in emojiTable" :key="image.id" :value="image.id" style="width:100%;height:100%") + .vrcplus-icon(v-if="image.versions[image.versions.length - 1].file.url" style="overflow:hidden;width:200px;height:200px;padding:10px") + template(v-if="image.frames") + .avatar(:style="generateEmojiStyle(image.versions[image.versions.length - 1].file.url, image.framesOverTime, image.frames, image.loopStyle)") + template(v-else) + img.avatar(v-lazy="image.versions[image.versions.length - 1].file.url" style="width:200px;height:200px") + el-option-group(:label="$t('dialog.boop_dialog.default_emojis')") + el-option(v-for="emojiName in photonEmojis" :key="emojiName" :value="getEmojiValue(emojiName)" style="width:100%;height:100%") + span(v-text="emojiName") + template(#footer) + el-button(size="small" @click="showGalleryDialog(2)") {{ $t('dialog.boop_dialog.emoji_manager') }} + el-button(size="small" @click="sendBoopDialog.visible = false") {{ $t('dialog.boop_dialog.cancel') }} + el-button(size="small" @click="sendBoop" :disabled="!sendBoopDialog.userId") {{ $t('dialog.boop_dialog.send') }} diff --git a/html/src/mixins/dialogs/currentUser.pug b/html/src/mixins/dialogs/currentUser.pug new file mode 100644 index 000000000..a6b83f97c --- /dev/null +++ b/html/src/mixins/dialogs/currentUser.pug @@ -0,0 +1,152 @@ +mixin currentUser() + //- dialog: social status + el-dialog.x-dialog(:before-close="beforeDialogClose" @mousedown.native="dialogMouseDown" @mouseup.native="dialogMouseUp" ref="socialStatusDialog" :visible.sync="socialStatusDialog.visible" :title="$t('dialog.social_status.header')" width="400px") + div(v-loading="socialStatusDialog.loading") + el-collapse(style="border:0") + el-collapse-item + template(slot="title") + span(style="font-size:16px") {{ $t('dialog.social_status.history') }} + data-tables(v-bind="socialStatusHistoryTable" @row-click="setSocialStatusFromHistory" style="cursor:pointer") + el-table-column(:label="$t('table.social_status.no')" prop="no" width="50") + el-table-column(:label="$t('table.social_status.status')" prop="status") + el-select(v-model="socialStatusDialog.status" style="display:block;margin-top:10px") + el-option(:label="$t('dialog.user.status.join_me')" value="join me"). + #[i.x-user-status.joinme] {{ $t('dialog.user.status.join_me') }} + el-option(:label="$t('dialog.user.status.online')" value="active"). + #[i.x-user-status.online] {{ $t('dialog.user.status.online') }} + el-option(:label="$t('dialog.user.status.ask_me')" value="ask me"). + #[i.x-user-status.askme] {{ $t('dialog.user.status.ask_me') }} + el-option(:label="$t('dialog.user.status.busy')" value="busy"). + #[i.x-user-status.busy] {{ $t('dialog.user.status.busy') }} + el-option(v-if="API.currentUser.$isModerator" :label="$t('dialog.user.status.offline')" value="offline"). + #[i.x-user-status.offline] {{ $t('dialog.user.status.offline') }} + el-input(v-model="socialStatusDialog.statusDescription" :placeholder="$t('dialog.social_status.status_placeholder')" maxlength="32" show-word-limit style="display:block;margin-top:10px") + template(#footer) + el-button(type="primary" size="small" :disabled="socialStatusDialog.loading" @click="saveSocialStatus") {{ $t('dialog.social_status.update') }} + + //- dialog: language + el-dialog.x-dialog(:before-close="beforeDialogClose" @mousedown.native="dialogMouseDown" @mouseup.native="dialogMouseUp" ref="languageDialog" :visible.sync="languageDialog.visible" :title="$t('dialog.language.header')" width="400px") + div(v-loading="languageDialog.loading") + div(style="margin:5px 0") + el-tag(v-for="item in API.currentUser.$languages" :key="item.key" size="small" type="info" effect="plain" closable @close="removeUserLanguage(item.key)" style="margin-right:5px") + span.flags(:class="languageClass(item.key)" style="display:inline-block;margin-right:5px") + | {{ item.value }} ({{ item.key }}) + div(v-if="languageDialog.languageChoice === true") + el-select(v-model="languageDialog.languageValue" :placeholder="$t('dialog.language.select_language')" size="mini") + el-option(v-for="item in languageDialog.languages" :key="item.key" :value="item.key" :label="item.value") + span.flags(:class="languageClass(item.key)" style="display:inline-block;margin-right:5px") + | {{ item.value }} ({{ item.key }}) + el-button(@click="languageDialog.languageChoice=false; addUserLanguage(languageDialog.languageValue)" size="mini") {{ $t('dialog.language.ok') }} + el-button(@click="languageDialog.languageChoice=false" size="mini" style="margin-left:0") {{ $t('dialog.language.cancel') }} + div(v-else) + el-button(@click="languageDialog.languageValue='';languageDialog.languageChoice=true" size="mini") {{ $t('dialog.language.add_language') }} + + //- dialog: bio + el-dialog.x-dialog(:before-close="beforeDialogClose" @mousedown.native="dialogMouseDown" @mouseup.native="dialogMouseUp" ref="bioDialog" :visible.sync="bioDialog.visible" :title="$t('dialog.bio.header')" width="600px") + div(v-loading="bioDialog.loading") + el-input(type="textarea" v-model="bioDialog.bio" size="mini" maxlength="512" show-word-limit :autosize="{ minRows:2, maxRows:5 }" :placeholder="$t('dialog.bio.bio_placeholder')") + el-input(v-for="(link, index) in bioDialog.bioLinks" :key="index" :value="link" v-model="bioDialog.bioLinks[index]" size="small" style="margin-top:5px") + img(slot="prepend" :src="getFaviconUrl(link)" style="width:16px;height:16px") + el-button(slot="append" icon="el-icon-delete" @click="bioDialog.bioLinks.splice(index, 1)") + el-button(@click="bioDialog.bioLinks.push('')" :disabled="bioDialog.bioLinks.length >= 3" size="mini" style="margin-top:5px") {{ $t('dialog.bio.add_link') }} + template(#footer) + el-button(type="primary" size="small" :disabled="bioDialog.loading" @click="saveBio") {{ $t('dialog.bio.update') }} + + //- dialog: pronouns + el-dialog.x-dialog(:before-close="beforeDialogClose" @mousedown.native="dialogMouseDown" @mouseup.native="dialogMouseUp" ref="pronounsDialog" :visible.sync="pronounsDialog.visible" :title="$t('dialog.pronouns.header')" width="600px") + div(v-loading="pronounsDialog.loading") + el-input(type="textarea" v-model="pronounsDialog.pronouns" size="mini" maxlength="32" show-word-limit :autosize="{ minRows:2, maxRows:5 }" :placeholder="$t('dialog.pronouns.pronouns_placeholder')") + template(#footer) + el-button(type="primary" size="small" :disabled="pronounsDialog.loading" @click="savePronouns") {{ $t('dialog.pronouns.update') }} + + //- dialog: Gallery/VRCPlusIcons + el-dialog.x-dialog(:before-close="beforeDialogClose" @mousedown.native="dialogMouseDown" @mouseup.native="dialogMouseUp" ref="galleryDialog" :visible.sync="galleryDialogVisible" :title="$t('dialog.gallery_icons.header')" width="100%") + span(style="padding-bottom:10px") {{ $t('dialog.gallery_icons.description') }} + br + br + el-tabs(type="card" ref="galleryTabs") + el-tab-pane(v-if="galleryDialogVisible" v-loading="galleryDialogGalleryLoading") + span(slot="label") {{ $t('dialog.gallery_icons.gallery') }} + span(style="color:#909399;font-size:12px;margin-left:5px") {{ galleryTable.length }}/64 + input(type="file" accept="image/png,image/jpg,image/jpeg,image/webp,image/bmp,image/gif" @change="onFileChangeGallery" id="GalleryUploadButton" style="display:none") + el-button-group + el-button(type="default" size="small" @click="refreshGalleryTable" icon="el-icon-refresh") {{ $t('dialog.gallery_icons.refresh') }} + el-button(type="default" size="small" @click="displayGalleryUpload" icon="el-icon-upload2" :disabled="!API.currentUser.$isVRCPlus") {{ $t('dialog.gallery_icons.upload') }} + el-button(type="default" size="small" @click="setProfilePicOverride('')" icon="el-icon-close" :disabled="!API.currentUser.profilePicOverride") {{ $t('dialog.gallery_icons.clear') }} + br + .x-friend-item(v-if="image.versions && image.versions.length > 0" v-for="image in galleryTable" :key="image.id" style="display:inline-block;margin-top:10px;width:unset;cursor:default") + .vrcplus-icon(v-if="image.versions[image.versions.length - 1].file.url" @click="setProfilePicOverride(image.id)" :class="{ 'current-vrcplus-icon': compareCurrentProfilePic(image.id) }") + img.avatar(v-lazy="image.versions[image.versions.length - 1].file.url") + div(style="float:right;margin-top:5px") + el-button(type="default" @click="showFullscreenImageDialog(image.versions[image.versions.length - 1].file.url)" size="mini" icon="el-icon-download" circle) + el-button(type="default" @click="deleteGalleryImage(image.id)" size="mini" icon="el-icon-delete" circle style="margin-left:5px") + el-tab-pane(v-if="galleryDialogVisible" v-loading="galleryDialogIconsLoading") + span(slot="label") {{ $t('dialog.gallery_icons.icons') }} + span(style="color:#909399;font-size:12px;margin-left:5px") {{ VRCPlusIconsTable.length }}/64 + input(type="file" accept="image/png,image/jpg,image/jpeg,image/webp,image/bmp,image/gif" @change="onFileChangeVRCPlusIcon" id="VRCPlusIconUploadButton" style="display:none") + el-button-group + el-button(type="default" size="small" @click="refreshVRCPlusIconsTable" icon="el-icon-refresh") {{ $t('dialog.gallery_icons.refresh') }} + el-button(type="default" size="small" @click="displayVRCPlusIconUpload" icon="el-icon-upload2" :disabled="!API.currentUser.$isVRCPlus") {{ $t('dialog.gallery_icons.upload') }} + el-button(type="default" size="small" @click="setVRCPlusIcon('')" icon="el-icon-close" :disabled="!API.currentUser.userIcon") {{ $t('dialog.gallery_icons.clear') }} + br + .x-friend-item(v-if="image.versions && image.versions.length > 0" v-for="image in VRCPlusIconsTable" :key="image.id" style="display:inline-block;margin-top:10px;width:unset;cursor:default") + .vrcplus-icon(v-if="image.versions[image.versions.length - 1].file.url" @click="setVRCPlusIcon(image.id)" :class="{ 'current-vrcplus-icon': compareCurrentVRCPlusIcon(image.id) }") + img.avatar(v-lazy="image.versions[image.versions.length - 1].file.url") + div(style="float:right;margin-top:5px") + el-button(type="default" @click="showFullscreenImageDialog(image.versions[image.versions.length - 1].file.url)" size="mini" icon="el-icon-download" circle) + el-button(type="default" @click="deleteVRCPlusIcon(image.id)" size="mini" icon="el-icon-delete" circle style="margin-left:5px") + el-tab-pane(v-if="galleryDialogVisible" v-loading="galleryDialogEmojisLoading") + span(slot="label") {{ $t('dialog.gallery_icons.emojis') }} + span(style="color:#909399;font-size:12px;margin-left:5px") {{ emojiTable.length }}/9 + input(type="file" accept="image/png,image/jpg,image/jpeg,image/webp,image/bmp,image/gif" @change="onFileChangeEmoji" id="EmojiUploadButton" style="display:none") + el-button-group(style="margin-right:10px") + el-button(type="default" size="small" @click="refreshEmojiTable" icon="el-icon-refresh") {{ $t('dialog.gallery_icons.refresh') }} + el-button(type="default" size="small" @click="displayEmojiUpload" icon="el-icon-upload2" :disabled="!API.currentUser.$isVRCPlus") {{ $t('dialog.gallery_icons.upload') }} + el-select(v-model="emojiAnimationStyle" popper-class="max-height-el-select") + el-option-group {{ $t('dialog.gallery_icons.emoji_animation_styles') }} + el-option.x-friend-item(v-for="(fileName, styleName) in emojiAnimationStyleList" :key="fileName" :label="styleName" :value="styleName" style="height:auto") + .avatar(style="width:200px;height:200px") + img(v-lazy="`${emojiAnimationStyleUrl}${fileName}`") + .detail + span.name(v-text="styleName" style="margin-right:100px") + el-checkbox(v-model="emojiAnimType" style="margin-left:10px;margin-right:10px") + span {{ $t('dialog.gallery_icons.emoji_animation_type') }} + template(v-if="emojiAnimType") + span(style="margin-right:10px") {{ $t('dialog.gallery_icons.emoji_animation_fps') }} + el-input-number(size="small" v-model="emojiAnimFps" :min="1" :max="64" style="margin-right:10px;width:112px") + span(style="margin-right:10px") {{ $t('dialog.gallery_icons.emoji_animation_frame_count') }} + el-input-number(size="small" v-model="emojiAnimFrameCount" :min="2" :max="64" style="margin-right:10px;width:112px") + el-checkbox(v-model="emojiAnimLoopPingPong" style="margin-left:10px;margin-right:10px") + span {{ $t('dialog.gallery_icons.emoji_loop_pingpong') }} + br + span {{ $t('dialog.gallery_icons.flipbook_info') }} + br + .x-friend-item(v-if="image.versions && image.versions.length > 0" v-for="image in emojiTable" :key="image.id" style="display:inline-block;margin-top:10px;width:unset;cursor:default") + .vrcplus-icon(v-if="image.versions[image.versions.length - 1].file.url" style="overflow:hidden" @click="showFullscreenImageDialog(image.versions[image.versions.length - 1].file.url)") + template(v-if="image.frames") + .avatar(:style="generateEmojiStyle(image.versions[image.versions.length - 1].file.url, image.framesOverTime, image.frames, image.loopStyle)") + template(v-else) + img.avatar(v-lazy="image.versions[image.versions.length - 1].file.url") + div(style="display:inline-block;margin:5px") + span(v-if="image.loopStyle === 'pingpong'") #[i.el-icon-refresh.el-icon--left] + span(style="margin-right:5px") {{ image.animationStyle }} + span(v-if="image.framesOverTime" style="margin-right:5px") {{ image.framesOverTime }}fps + span(v-if="image.frames" style="margin-right:5px") {{ image.frames }}frames + br + div(style="float:right;margin-top:5px") + el-button(type="default" @click="showFullscreenImageDialog(image.versions[image.versions.length - 1].file.url)" size="mini" icon="el-icon-download" circle) + el-button(type="default" @click="deleteEmoji(image.id)" size="mini" icon="el-icon-delete" circle style="margin-left:5px") + el-tab-pane(v-if="galleryDialogVisible" v-loading="galleryDialogStickersLoading") + span(slot="label") {{ $t('dialog.gallery_icons.stickers') }} + span(style="color:#909399;font-size:12px;margin-left:5px") {{ stickerTable.length }}/9 + input(type="file" accept="image/png,image/jpg,image/jpeg,image/webp,image/bmp,image/gif" @change="onFileChangeSticker" id="StickerUploadButton" style="display:none") + el-button-group + el-button(type="default" size="small" @click="refreshStickerTable" icon="el-icon-refresh") {{ $t('dialog.gallery_icons.refresh') }} + el-button(type="default" size="small" @click="displayStickerUpload" icon="el-icon-upload2" :disabled="!API.currentUser.$isVRCPlus") {{ $t('dialog.gallery_icons.upload') }} + br + .x-friend-item(v-if="image.versions && image.versions.length > 0" v-for="image in stickerTable" :key="image.id" style="display:inline-block;margin-top:10px;width:unset;cursor:default") + .vrcplus-icon(v-if="image.versions[image.versions.length - 1].file.url" style="overflow:hidden" @click="showFullscreenImageDialog(image.versions[image.versions.length - 1].file.url)") + img.avatar(v-lazy="image.versions[image.versions.length - 1].file.url") + div(style="float:right;margin-top:5px") + el-button(type="default" @click="showFullscreenImageDialog(image.versions[image.versions.length - 1].file.url)" size="mini" icon="el-icon-download" circle) + el-button(type="default" @click="deleteSticker(image.id)" size="mini" icon="el-icon-delete" circle style="margin-left:5px") diff --git a/html/src/mixins/dialogs/favorites.pug b/html/src/mixins/dialogs/favorites.pug new file mode 100644 index 000000000..3f735902e --- /dev/null +++ b/html/src/mixins/dialogs/favorites.pug @@ -0,0 +1,230 @@ +mixin favorites() + //- dialog: favorite + el-dialog.x-dialog(:before-close="beforeDialogClose" @mousedown.native="dialogMouseDown" @mouseup.native="dialogMouseUp" ref="favoriteDialog" :visible.sync="favoriteDialog.visible" :title="$t('dialog.favorite.header')" width="300px") + div(v-if="favoriteDialog.visible" v-loading="favoriteDialog.loading") + span(style="display:block;text-align:center") {{ $t('dialog.favorite.vrchat_favorites') }} + template(v-if="favoriteDialog.currentGroup && favoriteDialog.currentGroup.key") + el-button(style="display:block;width:100%;margin:10px 0" @click="deleteFavoriteNoConfirm(favoriteDialog.objectId)") #[i.el-icon-check] {{ favoriteDialog.currentGroup.displayName }} ({{ favoriteDialog.currentGroup.count }} / {{ favoriteDialog.currentGroup.capacity }}) + template(v-else) + el-button(v-for="group in favoriteDialog.groups" :key="group" style="display:block;width:100%;margin:10px 0" @click="addFavorite(group)") {{ group.displayName }} ({{ group.count }} / {{ group.capacity }}) + div(v-if="favoriteDialog.visible && favoriteDialog.type === 'world'" style="margin-top:20px") + span(style="display:block;text-align:center") {{ $t('dialog.favorite.local_favorites') }} + template(v-for="group in localWorldFavoriteGroups" :key="group") + el-button(v-if="hasLocalWorldFavorite(favoriteDialog.objectId, group)" style="display:block;width:100%;margin:10px 0" @click="removeLocalWorldFavorite(favoriteDialog.objectId, group)") #[i.el-icon-check] {{ group }} ({{ getLocalWorldFavoriteGroupLength(group) }}) + el-button(v-else style="display:block;width:100%;margin:10px 0" @click="addLocalWorldFavorite(favoriteDialog.objectId, group)") {{ group }} ({{ getLocalWorldFavoriteGroupLength(group) }}) + div(v-if="favoriteDialog.visible && favoriteDialog.type === 'avatar'" style="margin-top:20px") + span(style="display:block;text-align:center") {{ $t('dialog.favorite.local_avatar_favorites') }} + template(v-for="group in localAvatarFavoriteGroups" :key="group") + el-button(v-if="hasLocalAvatarFavorite(favoriteDialog.objectId, group)" style="display:block;width:100%;margin:10px 0" @click="removeLocalAvatarFavorite(favoriteDialog.objectId, group)") #[i.el-icon-check] {{ group }} ({{ getLocalAvatarFavoriteGroupLength(group) }}) + el-button(v-else style="display:block;width:100%;margin:10px 0" :disabled="!isLocalUserVrcplusSupporter()" @click="addLocalAvatarFavorite(favoriteDialog.objectId, group)") {{ group }} ({{ getLocalAvatarFavoriteGroupLength(group) }}) + + //- dialog: export friends list + el-dialog.x-dialog(:before-close="beforeDialogClose" @mousedown.native="dialogMouseDown" @mouseup.native="dialogMouseUp" :visible.sync="exportFriendsListDialog" :title="$t('dialog.export_friends_list.header')" width="650px") + el-tabs(type="card") + el-tab-pane(:label="$t('dialog.export_friends_list.csv')") + el-input(type="textarea" v-if="exportFriendsListDialog" v-model="exportFriendsListCsv" size="mini" rows="15" resize="none" readonly style="margin-top:15px" @click.native="$event.target.tagName === 'TEXTAREA' && $event.target.select()") + el-tab-pane(:label="$t('dialog.export_friends_list.json')") + el-input(type="textarea" v-if="exportFriendsListDialog" v-model="exportFriendsListJson" size="mini" rows="15" resize="none" readonly style="margin-top:15px" @click.native="$event.target.tagName === 'TEXTAREA' && $event.target.select()") + + //- dialog: export avatars list + el-dialog.x-dialog(:before-close="beforeDialogClose" @mousedown.native="dialogMouseDown" @mouseup.native="dialogMouseUp" :visible.sync="exportAvatarsListDialog" :title="$t('dialog.export_own_avatars.header')" width="650px") + el-input(type="textarea" v-if="exportAvatarsListDialog" v-model="exportAvatarsListCsv" size="mini" rows="15" resize="none" readonly style="margin-top:15px" @click.native="$event.target.tagName === 'TEXTAREA' && $event.target.select()") + + //- dialog: export world list + el-dialog.x-dialog(:before-close="beforeDialogClose" @mousedown.native="dialogMouseDown" @mouseup.native="dialogMouseUp" ref="worldExportDialogRef" :visible.sync="worldExportDialogVisible" :title="$t('dialog.world_export.header')" width="650px") + el-dropdown(@click.native.stop trigger="click" size="small") + el-button(size="mini") + span(v-if="worldExportFavoriteGroup") {{ worldExportFavoriteGroup.displayName }} ({{ worldExportFavoriteGroup.count }}/{{ worldExportFavoriteGroup.capacity }}) #[i.el-icon-arrow-down.el-icon--right] + span(v-else) All Favorites #[i.el-icon-arrow-down.el-icon--right] + el-dropdown-menu(#default="dropdown") + el-dropdown-item(style="display:block;margin:10px 0" @click.native="selectWorldExportGroup(null)") None + template(v-for="groupAPI in API.favoriteWorldGroups" :key="groupAPI.name") + el-dropdown-item(style="display:block;margin:10px 0" @click.native="selectWorldExportGroup(groupAPI)") {{ groupAPI.displayName }} ({{ groupAPI.count }}/{{ groupAPI.capacity }}) + el-dropdown(@click.native.stop trigger="click" size="small" style="margin-left:10px") + el-button(size="mini") + span(v-if="worldExportLocalFavoriteGroup") {{ worldExportLocalFavoriteGroup }} ({{ getLocalWorldFavoriteGroupLength(worldExportLocalFavoriteGroup) }}) #[i.el-icon-arrow-down.el-icon--right] + span(v-else) Select Group #[i.el-icon-arrow-down.el-icon--right] + el-dropdown-menu(#default="dropdown") + el-dropdown-item(style="display:block;margin:10px 0" @click.native="selectWorldExportLocalGroup(null)") None + template(v-for="group in localWorldFavoriteGroups" :key="group") + el-dropdown-item(style="display:block;margin:10px 0" @click.native="selectWorldExportLocalGroup(group)") {{ group }} ({{ localWorldFavorites[group].length }}) + br + el-input(type="textarea" v-if="worldExportDialogVisible" v-model="worldExportContent" size="mini" rows="15" resize="none" readonly style="margin-top:15px" @click.native="$event.target.tagName === 'TEXTAREA' && $event.target.select()") + + //- dialog: World import dialog + el-dialog.x-dialog(:before-close="beforeDialogClose" @mousedown.native="dialogMouseDown" @mouseup.native="dialogMouseUp" ref="worldImportDialog" :visible.sync="worldImportDialog.visible" :title="$t('dialog.world_import.header')" width="650px") + div(style="font-size:12px") + | {{ $t('dialog.world_import.description') }} + el-input(type="textarea" v-model="worldImportDialog.input" size="mini" rows="10" resize="none" style="margin-top:15px") + el-button(size="small" @click="processWorldImportList" :disabled="!worldImportDialog.input") {{ $t('dialog.world_import.process_list') }} + span(v-if="worldImportDialog.progress" style="margin-top:10px") #[i.el-icon-loading(style="margin-right:5px")] {{ $t('dialog.world_import.process_progress') }} {{ worldImportDialog.progress }}/{{ worldImportDialog.progressTotal }} + br + el-dropdown(@click.native.stop trigger="click" size="small" style="margin-right:5px") + el-button(size="mini") + span(v-if="worldImportDialog.worldImportFavoriteGroup") {{ worldImportDialog.worldImportFavoriteGroup.displayName }} ({{ worldImportDialog.worldImportFavoriteGroup.count }}/{{ worldImportDialog.worldImportFavoriteGroup.capacity }}) #[i.el-icon-arrow-down.el-icon--right] + span(v-else) {{ $t('dialog.world_import.select_vrchat_group_placeholder') }} #[i.el-icon-arrow-down.el-icon--right] + el-dropdown-menu(#default="dropdown") + template(v-for="groupAPI in API.favoriteWorldGroups" :key="groupAPI.name") + el-dropdown-item(style="display:block;margin:10px 0" @click.native="selectWorldImportGroup(groupAPI)" :disabled="groupAPI.count >= groupAPI.capacity") {{ groupAPI.displayName }} ({{ groupAPI.count }}/{{ groupAPI.capacity }}) + el-dropdown(@click.native.stop trigger="click" size="small" style="margin:5px") + el-button(size="mini") + span(v-if="worldImportDialog.worldImportLocalFavoriteGroup") {{ worldImportDialog.worldImportLocalFavoriteGroup }} ({{ getLocalWorldFavoriteGroupLength(worldImportDialog.worldImportLocalFavoriteGroup) }}) #[i.el-icon-arrow-down.el-icon--right] + span(v-else) {{ $t('dialog.world_import.select_local_group_placeholder') }} #[i.el-icon-arrow-down.el-icon--right] + el-dropdown-menu(#default="dropdown") + template(v-for="group in localWorldFavoriteGroups" :key="group") + el-dropdown-item(style="display:block;margin:10px 0" @click.native="selectWorldImportLocalGroup(group)" ) {{ group }} ({{ getLocalWorldFavoriteGroupLength(group) }}) + el-button(size="small" @click="importWorldImportTable" style="margin:5px" :disabled="worldImportTable.data.length === 0 || (!worldImportDialog.worldImportFavoriteGroup && !worldImportDialog.worldImportLocalFavoriteGroup)") {{ $t('dialog.world_import.import') }} + el-button(v-if="worldImportDialog.loading" size="small" @click="cancelWorldImport" style="margin-top:10px") {{ $t('dialog.world_import.cancel') }} + span(v-if="worldImportDialog.worldImportFavoriteGroup") {{ worldImportTable.data.length }} / {{ worldImportDialog.worldImportFavoriteGroup.capacity - worldImportDialog.worldImportFavoriteGroup.count }} + span(v-if="worldImportDialog.importProgress" style="margin:10px") #[i.el-icon-loading(style="margin-right:5px")] {{ $t('dialog.world_import.import_progress') }} {{ worldImportDialog.importProgress }}/{{ worldImportDialog.importProgressTotal }} + br + el-button(size="small" @click="clearWorldImportTable") {{ $t('dialog.world_import.clear_table') }} + template(v-if="worldImportDialog.errors") + el-button(size="small" @click="worldImportDialog.errors = ''" style="margin-left:5px") {{ $t('dialog.world_import.clear_errors') }} + h2(style="font-weight:bold;margin:0") {{ $t('dialog.world_import.errors') }} + pre(v-text="worldImportDialog.errors" style="white-space:pre-wrap;font-size:12px") + data-tables(v-if="worldImportDialog.visible" v-bind="worldImportTable" v-loading="worldImportDialog.loading" style="margin-top:10px") + el-table-column(:label="$t('table.import.image')" width="70" prop="thumbnailImageUrl") + template(v-once #default="scope") + el-popover(placement="right" height="500px" trigger="hover") + img.friends-list-avatar(slot="reference" v-lazy="scope.row.thumbnailImageUrl") + img.friends-list-avatar(v-lazy="scope.row.imageUrl" style="height:500px;cursor:pointer" @click="showFullscreenImageDialog(scope.row.imageUrl)") + el-table-column(:label="$t('table.import.name')" prop="name") + template(v-once #default="scope") + span.x-link(v-text="scope.row.name" @click="showWorldDialog(scope.row.id)") + el-table-column(:label="$t('table.import.author')" width="120" prop="authorName") + template(v-once #default="scope") + span.x-link(v-text="scope.row.authorName" @click="showUserDialog(scope.row.authorId)") + el-table-column(:label="$t('table.import.status')" width="70" prop="releaseStatus") + template(v-once #default="scope") + span(v-text="scope.row.releaseStatus" v-if="scope.row.releaseStatus === 'public'" style="color:#67c23a") + span(v-text="scope.row.releaseStatus" v-else-if="scope.row.releaseStatus === 'private'" style="color:#f56c6c") + span(v-text="scope.row.releaseStatus" v-else) + el-table-column(:label="$t('table.import.action')" width="90" align="right") + template(v-once #default="scope") + el-button(type="text" icon="el-icon-close" size="mini" @click="deleteItemWorldImport(scope.row)") + + //- dialog: export avatar list + el-dialog.x-dialog(:before-close="beforeDialogClose" @mousedown.native="dialogMouseDown" @mouseup.native="dialogMouseUp" ref="avatarExportDialogRef" :visible.sync="avatarExportDialogVisible" :title="$t('dialog.avatar_export.header')" width="650px") + el-dropdown(@click.native.stop trigger="click" size="small") + el-button(size="mini") + span(v-if="avatarExportFavoriteGroup") {{ avatarExportFavoriteGroup.displayName }} ({{ avatarExportFavoriteGroup.count }}/{{ avatarExportFavoriteGroup.capacity }}) #[i.el-icon-arrow-down.el-icon--right] + span(v-else) All Favorites #[i.el-icon-arrow-down.el-icon--right] + el-dropdown-menu(#default="dropdown") + el-dropdown-item(style="display:block;margin:10px 0" @click.native="selectAvatarExportGroup(null)") All Favorites + template(v-for="groupAPI in API.favoriteAvatarGroups" :key="groupAPI.name") + el-dropdown-item(style="display:block;margin:10px 0" @click.native="selectAvatarExportGroup(groupAPI)") {{ groupAPI.displayName }} ({{ groupAPI.count }}/{{ groupAPI.capacity }}) + el-dropdown(@click.native.stop trigger="click" size="small" style="margin-left:10px") + el-button(size="mini") + span(v-if="avatarExportLocalFavoriteGroup") {{ avatarExportLocalFavoriteGroup }} ({{ getLocalAvatarFavoriteGroupLength(avatarExportLocalFavoriteGroup) }}) #[i.el-icon-arrow-down.el-icon--right] + span(v-else) Select Group #[i.el-icon-arrow-down.el-icon--right] + el-dropdown-menu(#default="dropdown") + el-dropdown-item(style="display:block;margin:10px 0" @click.native="selectAvatarExportLocalGroup(null)") None + template(v-for="group in localAvatarFavoriteGroups" :key="group") + el-dropdown-item(style="display:block;margin:10px 0" @click.native="selectAvatarExportLocalGroup(group)" ) {{ group }} ({{ getLocalAvatarFavoriteGroupLength(group) }}) + br + el-input(type="textarea" v-if="avatarExportDialogVisible" v-model="avatarExportContent" size="mini" rows="15" resize="none" readonly style="margin-top:15px" @click.native="$event.target.tagName === 'TEXTAREA' && $event.target.select()") + + //- dialog: Avatar import dialog + el-dialog.x-dialog(:before-close="beforeDialogClose" @mousedown.native="dialogMouseDown" @mouseup.native="dialogMouseUp" ref="avatarImportDialog" :visible.sync="avatarImportDialog.visible" :title="$t('dialog.avatar_import.header')" width="650px") + div(style="font-size:12px") + | {{ $t('dialog.avatar_import.description') }} + el-input(type="textarea" v-model="avatarImportDialog.input" size="mini" rows="10" resize="none" style="margin-top:15px") + el-button(size="small" @click="processAvatarImportList" :disabled="!avatarImportDialog.input") {{ $t('dialog.avatar_import.process_list') }} + span(v-if="avatarImportDialog.progress" style="margin-top:10px") #[i.el-icon-loading(style="margin-right:5px")] {{ $t('dialog.avatar_import.process_progress') }} {{ avatarImportDialog.progress }}/{{ avatarImportDialog.progressTotal }} + br + el-dropdown(@click.native.stop trigger="click" size="small") + el-button(size="mini") + span(v-if="avatarImportDialog.avatarImportFavoriteGroup") {{ avatarImportDialog.avatarImportFavoriteGroup.displayName }} ({{ avatarImportDialog.avatarImportFavoriteGroup.count }}/{{ avatarImportDialog.avatarImportFavoriteGroup.capacity }}) #[i.el-icon-arrow-down.el-icon--right] + span(v-else) {{ $t('dialog.avatar_import.select_group_placeholder') }} #[i.el-icon-arrow-down.el-icon--right] + el-dropdown-menu(#default="dropdown") + template(v-for="groupAPI in API.favoriteAvatarGroups" :key="groupAPI.name") + el-dropdown-item(style="display:block;margin:10px 0" @click.native="selectAvatarImportGroup(groupAPI)" :disabled="groupAPI.count >= groupAPI.capacity") {{ groupAPI.displayName }} ({{ groupAPI.count }}/{{ groupAPI.capacity }}) + el-dropdown(@click.native.stop trigger="click" size="small" style="margin:5px") + el-button(size="mini") + span(v-if="avatarImportDialog.avatarImportLocalFavoriteGroup") {{ avatarImportDialog.avatarImportLocalFavoriteGroup }} ({{ getLocalAvatarFavoriteGroupLength(avatarImportDialog.avatarImportLocalFavoriteGroup) }}) #[i.el-icon-arrow-down.el-icon--right] + span(v-else) {{ $t('dialog.avatar_import.select_group_placeholder') }} #[i.el-icon-arrow-down.el-icon--right] + el-dropdown-menu(#default="dropdown") + template(v-for="group in localAvatarFavoriteGroups" :key="group") + el-dropdown-item(style="display:block;margin:10px 0" @click.native="selectAvatarImportLocalGroup(group)" ) {{ group }} ({{ getLocalAvatarFavoriteGroupLength(group) }}) + el-button(size="small" @click="importAvatarImportTable" style="margin:5px" :disabled="avatarImportTable.data.length === 0 || (!avatarImportDialog.avatarImportFavoriteGroup && !avatarImportDialog.avatarImportLocalFavoriteGroup)") {{ $t('dialog.avatar_import.import') }} + el-button(v-if="avatarImportDialog.loading" size="small" @click="cancelAvatarImport" style="margin-top:10px") {{ $t('dialog.avatar_import.cancel') }} + span(v-if="avatarImportDialog.avatarImportFavoriteGroup") {{ avatarImportTable.data.length }} / {{ avatarImportDialog.avatarImportFavoriteGroup.capacity - avatarImportDialog.avatarImportFavoriteGroup.count }} + span(v-if="avatarImportDialog.importProgress" style="margin:10px") #[i.el-icon-loading(style="margin-right:5px")] {{ $t('dialog.avatar_import.import_progress') }} {{ avatarImportDialog.importProgress }}/{{ avatarImportDialog.importProgressTotal }} + br + el-button(size="small" @click="clearAvatarImportTable") {{ $t('dialog.avatar_import.clear_table') }} + template(v-if="avatarImportDialog.errors") + el-button(size="small" @click="avatarImportDialog.errors = ''" style="margin-left:5px") {{ $t('dialog.avatar_import.clear_errors') }} + h2(style="font-weight:bold;margin:0") {{ $t('dialog.avatar_import.errors') }} + pre(v-text="avatarImportDialog.errors" style="white-space:pre-wrap;font-size:12px") + data-tables(v-if="avatarImportDialog.visible" v-bind="avatarImportTable" v-loading="avatarImportDialog.loading" style="margin-top:10px") + el-table-column(:label="$t('table.import.image')" width="70" prop="thumbnailImageUrl") + template(v-once #default="scope") + el-popover(placement="right" height="500px" trigger="hover") + img.friends-list-avatar(slot="reference" v-lazy="scope.row.thumbnailImageUrl") + img.friends-list-avatar(v-lazy="scope.row.imageUrl" style="height:500px;cursor:pointer" @click="showFullscreenImageDialog(scope.row.imageUrl)") + el-table-column(:label="$t('table.import.name')" prop="name") + template(v-once #default="scope") + span.x-link(v-text="scope.row.name" @click="showAvatarDialog(scope.row.id)") + el-table-column(:label="$t('table.import.author')" width="120" prop="authorName") + template(v-once #default="scope") + span.x-link(v-text="scope.row.authorName" @click="showUserDialog(scope.row.authorId)") + el-table-column(:label="$t('table.import.status')" width="70" prop="releaseStatus") + template(v-once #default="scope") + span(v-text="scope.row.releaseStatus" v-if="scope.row.releaseStatus === 'public'" style="color:#67c23a") + span(v-text="scope.row.releaseStatus" v-else-if="scope.row.releaseStatus === 'private'" style="color:#f56c6c") + span(v-text="scope.row.releaseStatus" v-else) + el-table-column(:label="$t('table.import.action')" width="90" align="right") + template(v-once #default="scope") + el-button(type="text" icon="el-icon-close" size="mini" @click="deleteItemAvatarImport(scope.row)") + + //- dialog: export friend list + el-dialog.x-dialog(:before-close="beforeDialogClose" @mousedown.native="dialogMouseDown" @mouseup.native="dialogMouseUp" ref="friendExportDialogRef" :visible.sync="friendExportDialogVisible" :title="$t('dialog.friend_export.header')" width="650px") + el-dropdown(@click.native.stop trigger="click" size="small") + el-button(size="mini") + span(v-if="friendExportFavoriteGroup") {{ friendExportFavoriteGroup.displayName }} ({{ friendExportFavoriteGroup.count }}/{{ friendExportFavoriteGroup.capacity }}) #[i.el-icon-arrow-down.el-icon--right] + span(v-else) All Favorites #[i.el-icon-arrow-down.el-icon--right] + el-dropdown-menu(#default="dropdown") + el-dropdown-item(style="display:block;margin:10px 0" @click.native="selectFriendExportGroup(null)") All Favorites + template(v-for="groupAPI in API.favoriteFriendGroups" :key="groupAPI.name") + el-dropdown-item(style="display:block;margin:10px 0" @click.native="selectFriendExportGroup(groupAPI)") {{ groupAPI.displayName }} ({{ groupAPI.count }}/{{ groupAPI.capacity }}) + br + el-input(type="textarea" v-if="friendExportDialogVisible" v-model="friendExportContent" size="mini" rows="15" resize="none" readonly style="margin-top:15px" @click.native="$event.target.tagName === 'TEXTAREA' && $event.target.select()") + + //- dialog: Friend import dialog + el-dialog.x-dialog(:before-close="beforeDialogClose" @mousedown.native="dialogMouseDown" @mouseup.native="dialogMouseUp" ref="friendImportDialog" :visible.sync="friendImportDialog.visible" :title="$t('dialog.friend_import.header')" width="650px") + div(style="font-size:12px") + | {{ $t('dialog.friend_import.description') }} + el-input(type="textarea" v-model="friendImportDialog.input" size="mini" rows="10" resize="none" style="margin-top:15px") + el-button(size="small" @click="processFriendImportList" :disabled="!friendImportDialog.input") {{ $t('dialog.friend_import.process_list') }} + span(v-if="friendImportDialog.progress" style="margin-top:10px") #[i.el-icon-loading(style="margin-right:5px")] {{ $t('dialog.friend_import.process_progress') }} {{ friendImportDialog.progress }}/{{ friendImportDialog.progressTotal }} + br + el-dropdown(@click.native.stop trigger="click" size="small") + el-button(size="mini") + span(v-if="friendImportDialog.friendImportFavoriteGroup") {{ friendImportDialog.friendImportFavoriteGroup.displayName }} ({{ friendImportDialog.friendImportFavoriteGroup.count }}/{{ friendImportDialog.friendImportFavoriteGroup.capacity }}) #[i.el-icon-arrow-down.el-icon--right] + span(v-else) {{ $t('dialog.friend_import.select_group_placeholder') }} #[i.el-icon-arrow-down.el-icon--right] + el-dropdown-menu(#default="dropdown") + template(v-for="groupAPI in API.favoriteFriendGroups" :key="groupAPI.name") + el-dropdown-item(style="display:block;margin:10px 0" @click.native="selectFriendImportGroup(groupAPI)" :disabled="groupAPI.count >= groupAPI.capacity") {{ groupAPI.displayName }} ({{ groupAPI.count }}/{{ groupAPI.capacity }}) + el-button(size="small" @click="importFriendImportTable" style="margin:5px" :disabled="friendImportTable.data.length === 0 || !friendImportDialog.friendImportFavoriteGroup") {{ $t('dialog.friend_import.import') }} + el-button(v-if="friendImportDialog.loading" size="small" @click="cancelFriendImport" style="margin-top:10px") {{ $t('dialog.friend_import.cancel') }} + span(v-if="friendImportDialog.friendImportFavoriteGroup") {{ friendImportTable.data.length }} / {{ friendImportDialog.friendImportFavoriteGroup.capacity - friendImportDialog.friendImportFavoriteGroup.count }} + span(v-if="friendImportDialog.importProgress" style="margin:10px") #[i.el-icon-loading(style="margin-right:5px")] {{ $t('dialog.friend_import.import_progress') }} {{ friendImportDialog.importProgress }}/{{ friendImportDialog.importProgressTotal }} + br + el-button(size="small" @click="clearFriendImportTable") {{ $t('dialog.friend_import.clear_table') }} + template(v-if="friendImportDialog.errors") + el-button(size="small" @click="friendImportDialog.errors = ''" style="margin-left:5px") {{ $t('dialog.friend_import.clear_errors') }} + h2(style="font-weight:bold;margin:0") {{ $t('dialog.friend_import.errors') }} + pre(v-text="friendImportDialog.errors" style="white-space:pre-wrap;font-size:12px") + data-tables(v-if="friendImportDialog.visible" v-bind="friendImportTable" v-loading="friendImportDialog.loading" style="margin-top:10px") + el-table-column(:label="$t('table.import.image')" width="70" prop="currentAvatarThumbnailImageUrl") + template(v-once #default="scope") + el-popover(placement="right" height="500px" trigger="hover") + img.friends-list-avatar(slot="reference" v-lazy="userImage(scope.row)") + img.friends-list-avatar(v-lazy="userImageFull(scope.row)" style="height:500px;cursor:pointer" @click="showFullscreenImageDialog(userImageFull(scope.row))") + el-table-column(:label="$t('table.import.name')" prop="displayName") + template(v-once #default="scope") + span.x-link(v-text="scope.row.displayName" @click="showUserDialog(scope.row.id)") + el-table-column(:label="$t('table.import.action')" width="90" align="right") + template(v-once #default="scope") + el-button(type="text" icon="el-icon-close" size="mini" @click="deleteItemFriendImport(scope.row)") diff --git a/html/src/mixins/dialogs/feedFilters.pug b/html/src/mixins/dialogs/feedFilters.pug new file mode 100644 index 000000000..23a967bef --- /dev/null +++ b/html/src/mixins/dialogs/feedFilters.pug @@ -0,0 +1,503 @@ +mixin feedFilters() + //- dialog: Noty feed filters + el-dialog.x-dialog(:before-close="beforeDialogClose" @mousedown.native="dialogMouseDown" @mouseup.native="dialogMouseUp" ref="notyFeedFiltersDialog" :visible.sync="notyFeedFiltersDialog.visible" :title="$t('dialog.shared_feed_filters.notification')" width="550px") + .toggle-list + .toggle-item + span.toggle-name OnPlayerJoining + el-radio-group(v-model="sharedFeedFilters.noty.OnPlayerJoining" size="mini" @change="saveSharedFeedFilters") + el-radio-button(label="Off") {{ $t('dialog.shared_feed_filters.off') }} + el-radio-button(label="VIP") {{ $t('dialog.shared_feed_filters.favorite') }} + el-radio-button(label="Friends") {{ $t('dialog.shared_feed_filters.friends') }} + .toggle-item + span.toggle-name OnPlayerJoined + el-radio-group(v-model="sharedFeedFilters.noty.OnPlayerJoined" size="mini" @change="saveSharedFeedFilters") + el-radio-button(label="Off") {{ $t('dialog.shared_feed_filters.off') }} + el-radio-button(label="VIP") {{ $t('dialog.shared_feed_filters.favorite') }} + el-radio-button(label="Friends") {{ $t('dialog.shared_feed_filters.friends') }} + el-radio-button(label="Everyone") {{ $t('dialog.shared_feed_filters.everyone') }} + .toggle-item + span.toggle-name OnPlayerLeft + el-radio-group(v-model="sharedFeedFilters.noty.OnPlayerLeft" size="mini" @change="saveSharedFeedFilters") + el-radio-button(label="Off") {{ $t('dialog.shared_feed_filters.off') }} + el-radio-button(label="VIP") {{ $t('dialog.shared_feed_filters.favorite') }} + el-radio-button(label="Friends") {{ $t('dialog.shared_feed_filters.friends') }} + el-radio-button(label="Everyone") {{ $t('dialog.shared_feed_filters.everyone') }} + .toggle-item + span.toggle-name Online + el-radio-group(v-model="sharedFeedFilters.noty.Online" size="mini" @change="saveSharedFeedFilters") + el-radio-button(label="Off") {{ $t('dialog.shared_feed_filters.off') }} + el-radio-button(label="VIP") {{ $t('dialog.shared_feed_filters.favorite') }} + el-radio-button(label="Friends") {{ $t('dialog.shared_feed_filters.friends') }} + .toggle-item + span.toggle-name Offline + el-radio-group(v-model="sharedFeedFilters.noty.Offline" size="mini" @change="saveSharedFeedFilters") + el-radio-button(label="Off") {{ $t('dialog.shared_feed_filters.off') }} + el-radio-button(label="VIP") {{ $t('dialog.shared_feed_filters.favorite') }} + el-radio-button(label="Friends") {{ $t('dialog.shared_feed_filters.friends') }} + .toggle-item + span.toggle-name GPS + el-radio-group(v-model="sharedFeedFilters.noty.GPS" size="mini" @change="saveSharedFeedFilters") + el-radio-button(label="Off") {{ $t('dialog.shared_feed_filters.off') }} + el-radio-button(label="VIP") {{ $t('dialog.shared_feed_filters.favorite') }} + el-radio-button(label="Friends") {{ $t('dialog.shared_feed_filters.friends') }} + .toggle-item + span.toggle-name Status + el-radio-group(v-model="sharedFeedFilters.noty.Status" size="mini" @change="saveSharedFeedFilters") + el-radio-button(label="Off") {{ $t('dialog.shared_feed_filters.off') }} + el-radio-button(label="VIP") {{ $t('dialog.shared_feed_filters.favorite') }} + el-radio-button(label="Friends") {{ $t('dialog.shared_feed_filters.friends') }} + .toggle-item + span.toggle-name Invite + el-radio-group(v-model="sharedFeedFilters.noty.invite" size="mini" @change="saveSharedFeedFilters") + el-radio-button(label="Off") {{ $t('dialog.shared_feed_filters.off') }} + el-radio-button(label="VIP") {{ $t('dialog.shared_feed_filters.favorite') }} + el-radio-button(label="Friends") {{ $t('dialog.shared_feed_filters.friends') }} + .toggle-item + span.toggle-name Request Invite + el-radio-group(v-model="sharedFeedFilters.noty.requestInvite" size="mini" @change="saveSharedFeedFilters") + el-radio-button(label="Off") {{ $t('dialog.shared_feed_filters.off') }} + el-radio-button(label="VIP") {{ $t('dialog.shared_feed_filters.favorite') }} + el-radio-button(label="Friends") {{ $t('dialog.shared_feed_filters.friends') }} + .toggle-item + span.toggle-name Invite Response + el-radio-group(v-model="sharedFeedFilters.noty.inviteResponse" size="mini" @change="saveSharedFeedFilters") + el-radio-button(label="Off") {{ $t('dialog.shared_feed_filters.off') }} + el-radio-button(label="VIP") {{ $t('dialog.shared_feed_filters.favorite') }} + el-radio-button(label="Friends") {{ $t('dialog.shared_feed_filters.friends') }} + .toggle-item + span.toggle-name Request Invite Response + el-radio-group(v-model="sharedFeedFilters.noty.requestInviteResponse" size="mini" @change="saveSharedFeedFilters") + el-radio-button(label="Off") {{ $t('dialog.shared_feed_filters.off') }} + el-radio-button(label="VIP") {{ $t('dialog.shared_feed_filters.favorite') }} + el-radio-button(label="Friends") {{ $t('dialog.shared_feed_filters.friends') }} + .toggle-item + span.toggle-name Friend Request + el-radio-group(v-model="sharedFeedFilters.noty.friendRequest" size="mini" @change="saveSharedFeedFilters") + el-radio-button(label="Off") {{ $t('dialog.shared_feed_filters.off') }} + el-radio-button(label="On") {{ $t('dialog.shared_feed_filters.on') }} + .toggle-item + span.toggle-name New Friend + el-radio-group(v-model="sharedFeedFilters.noty.Friend" size="mini" @change="saveSharedFeedFilters") + el-radio-button(label="Off") {{ $t('dialog.shared_feed_filters.off') }} + el-radio-button(label="On") {{ $t('dialog.shared_feed_filters.on') }} + .toggle-item + span.toggle-name Unfriend + el-radio-group(v-model="sharedFeedFilters.noty.Unfriend" size="mini" @change="saveSharedFeedFilters") + el-radio-button(label="Off") {{ $t('dialog.shared_feed_filters.off') }} + el-radio-button(label="On") {{ $t('dialog.shared_feed_filters.on') }} + .toggle-item + span.toggle-name Display Name Change + el-radio-group(v-model="sharedFeedFilters.noty.DisplayName" size="mini" @change="saveSharedFeedFilters") + el-radio-button(label="Off") {{ $t('dialog.shared_feed_filters.off') }} + el-radio-button(label="VIP") {{ $t('dialog.shared_feed_filters.favorite') }} + el-radio-button(label="Friends") {{ $t('dialog.shared_feed_filters.friends') }} + .toggle-item + span.toggle-name Trust Level Change + el-radio-group(v-model="sharedFeedFilters.noty.TrustLevel" size="mini" @change="saveSharedFeedFilters") + el-radio-button(label="Off") {{ $t('dialog.shared_feed_filters.off') }} + el-radio-button(label="VIP") {{ $t('dialog.shared_feed_filters.favorite') }} + el-radio-button(label="Friends") {{ $t('dialog.shared_feed_filters.friends') }} + //- .toggle-item + //- span.toggle-name Boop + //- el-radio-group(v-model="sharedFeedFilters.noty.boop" size="mini" @change="saveSharedFeedFilters") + //- el-radio-button(label="Off") {{ $t('dialog.shared_feed_filters.off') }} + //- el-radio-button(label="On") {{ $t('dialog.shared_feed_filters.on') }} + .toggle-item + span.toggle-name Group Change + el-tooltip(placement="top" style="margin-left:5px" content="When you've left or been kicked from a group, group name changed, group owner changed, role added/removed") + i.el-icon-info + el-radio-group(v-model="sharedFeedFilters.noty.groupChange" size="mini" @change="saveSharedFeedFilters") + el-radio-button(label="Off") {{ $t('dialog.shared_feed_filters.off') }} + el-radio-button(label="On") {{ $t('dialog.shared_feed_filters.on') }} + .toggle-item + span.toggle-name Group Announcement + el-radio-group(v-model="sharedFeedFilters.noty['group.announcement']" size="mini" @change="saveSharedFeedFilters") + el-radio-button(label="Off") {{ $t('dialog.shared_feed_filters.off') }} + el-radio-button(label="On") {{ $t('dialog.shared_feed_filters.on') }} + .toggle-item + span.toggle-name Group Join + el-tooltip(placement="top" style="margin-left:5px" content="When your request to join a group has been approved") + i.el-icon-info + el-radio-group(v-model="sharedFeedFilters.noty['group.informative']" size="mini" @change="saveSharedFeedFilters") + el-radio-button(label="Off") {{ $t('dialog.shared_feed_filters.off') }} + el-radio-button(label="On") {{ $t('dialog.shared_feed_filters.on') }} + .toggle-item + span.toggle-name Group Invite + el-tooltip(placement="top" style="margin-left:5px" content="When someone invites you to join a group") + i.el-icon-info + el-radio-group(v-model="sharedFeedFilters.noty['group.invite']" size="mini" @change="saveSharedFeedFilters") + el-radio-button(label="Off") {{ $t('dialog.shared_feed_filters.off') }} + el-radio-button(label="On") {{ $t('dialog.shared_feed_filters.on') }} + .toggle-item + span.toggle-name Group Join Request + el-tooltip(placement="top" style="margin-left:5px" content="When someone requests to join a group you're a moderator for") + i.el-icon-info + el-radio-group(v-model="sharedFeedFilters.noty['group.joinRequest']" size="mini" @change="saveSharedFeedFilters") + el-radio-button(label="Off") {{ $t('dialog.shared_feed_filters.off') }} + el-radio-button(label="On") {{ $t('dialog.shared_feed_filters.on') }} + .toggle-item + span.toggle-name Group Transfer Request + el-radio-group(v-model="sharedFeedFilters.noty['group.transfer']" size="mini" @change="saveSharedFeedFilters") + el-radio-button(label="Off") {{ $t('dialog.shared_feed_filters.off') }} + el-radio-button(label="On") {{ $t('dialog.shared_feed_filters.on') }} + .toggle-item + span.toggle-name Instance Queue Ready + el-radio-group(v-model="sharedFeedFilters.noty['group.queueReady']" size="mini" @change="saveSharedFeedFilters") + el-radio-button(label="Off") {{ $t('dialog.shared_feed_filters.off') }} + el-radio-button(label="On") {{ $t('dialog.shared_feed_filters.on') }} + .toggle-item + span.toggle-name Instance Closed + el-tooltip(placement="top" style="margin-left:5px" content="When the instance you're in has been closed preventing anyone from joining") + i.el-icon-info + el-radio-group(v-model="sharedFeedFilters.noty['instance.closed']" size="mini" @change="saveSharedFeedFilters") + el-radio-button(label="Off") {{ $t('dialog.shared_feed_filters.off') }} + el-radio-button(label="On") {{ $t('dialog.shared_feed_filters.on') }} + .toggle-item + span.toggle-name Video Play + el-tooltip(placement="top" style="margin-left:5px" content="Requires VRCX YouTube API option enabled") + i.el-icon-warning + el-radio-group(v-model="sharedFeedFilters.noty.VideoPlay" size="mini" @change="saveSharedFeedFilters") + el-radio-button(label="Off") {{ $t('dialog.shared_feed_filters.off') }} + el-radio-button(label="On") {{ $t('dialog.shared_feed_filters.on') }} + .toggle-item + span.toggle-name Miscellaneous Events + el-tooltip(placement="top" style="margin-left:5px" content="Misc event from VRC game log: VRC crash auto rejoin, shader keyword limit, joining instance blocked by master, error loading video, audio device changed, error joining instance, kicked from instance, VRChat failing to start OSC server, etc...") + i.el-icon-info + el-radio-group(v-model="sharedFeedFilters.noty.Event" size="mini" @change="saveSharedFeedFilters") + el-radio-button(label="Off") {{ $t('dialog.shared_feed_filters.off') }} + el-radio-button(label="On") {{ $t('dialog.shared_feed_filters.on') }} + .toggle-item + span.toggle-name External App + el-radio-group(v-model="sharedFeedFilters.noty.External" size="mini" @change="saveSharedFeedFilters") + el-radio-button(label="Off") {{ $t('dialog.shared_feed_filters.off') }} + el-radio-button(label="On") {{ $t('dialog.shared_feed_filters.on') }} + .toggle-item + span.toggle-name Blocked Player Joins + el-radio-group(v-model="sharedFeedFilters.noty.BlockedOnPlayerJoined" size="mini" @change="saveSharedFeedFilters") + el-radio-button(label="Off") {{ $t('dialog.shared_feed_filters.off') }} + el-radio-button(label="VIP") {{ $t('dialog.shared_feed_filters.favorite') }} + el-radio-button(label="Friends") {{ $t('dialog.shared_feed_filters.friends') }} + el-radio-button(label="Everyone") {{ $t('dialog.shared_feed_filters.everyone') }} + .toggle-item + span.toggle-name Blocked Player Leaves + el-radio-group(v-model="sharedFeedFilters.noty.BlockedOnPlayerLeft" size="mini" @change="saveSharedFeedFilters") + el-radio-button(label="Off") {{ $t('dialog.shared_feed_filters.off') }} + el-radio-button(label="VIP") {{ $t('dialog.shared_feed_filters.favorite') }} + el-radio-button(label="Friends") {{ $t('dialog.shared_feed_filters.friends') }} + el-radio-button(label="Everyone") {{ $t('dialog.shared_feed_filters.everyone') }} + .toggle-item + span.toggle-name Muted Player Joins + el-radio-group(v-model="sharedFeedFilters.noty.MutedOnPlayerJoined" size="mini" @change="saveSharedFeedFilters") + el-radio-button(label="Off") {{ $t('dialog.shared_feed_filters.off') }} + el-radio-button(label="VIP") {{ $t('dialog.shared_feed_filters.favorite') }} + el-radio-button(label="Friends") {{ $t('dialog.shared_feed_filters.friends') }} + el-radio-button(label="Everyone") {{ $t('dialog.shared_feed_filters.everyone') }} + .toggle-item + span.toggle-name Muted Player Leaves + el-radio-group(v-model="sharedFeedFilters.noty.MutedOnPlayerLeft" size="mini" @change="saveSharedFeedFilters") + el-radio-button(label="Off") {{ $t('dialog.shared_feed_filters.off') }} + el-radio-button(label="VIP") {{ $t('dialog.shared_feed_filters.favorite') }} + el-radio-button(label="Friends") {{ $t('dialog.shared_feed_filters.friends') }} + el-radio-button(label="Everyone") {{ $t('dialog.shared_feed_filters.everyone') }} + .toggle-item + span.toggle-name Lobby Avatar Change + el-radio-group(v-model="sharedFeedFilters.noty.AvatarChange" size="mini" @change="saveSharedFeedFilters") + el-radio-button(label="Off") {{ $t('dialog.shared_feed_filters.off') }} + el-radio-button(label="VIP") {{ $t('dialog.shared_feed_filters.favorite') }} + el-radio-button(label="Friends") {{ $t('dialog.shared_feed_filters.friends') }} + el-radio-button(label="Everyone") {{ $t('dialog.shared_feed_filters.everyone') }} + template(v-if="photonLoggingEnabled") + br + .toggle-item + span.toggle-name Photon Event Logging + .toggle-item + span.toggle-name Portal Spawn + el-radio-group(v-model="sharedFeedFilters.noty.PortalSpawn" size="mini" @change="saveSharedFeedFilters") + el-radio-button(label="Off") {{ $t('dialog.shared_feed_filters.off') }} + el-radio-button(label="VIP") {{ $t('dialog.shared_feed_filters.favorite') }} + el-radio-button(label="Friends") {{ $t('dialog.shared_feed_filters.friends') }} + el-radio-button(label="Everyone") {{ $t('dialog.shared_feed_filters.everyone') }} + .toggle-item + span.toggle-name Lobby ChatBox Message + el-radio-group(v-model="sharedFeedFilters.noty.ChatBoxMessage" size="mini" @change="saveSharedFeedFilters") + el-radio-button(label="Off") {{ $t('dialog.shared_feed_filters.off') }} + el-radio-button(label="VIP") {{ $t('dialog.shared_feed_filters.favorite') }} + el-radio-button(label="Friends") {{ $t('dialog.shared_feed_filters.friends') }} + el-radio-button(label="Everyone") {{ $t('dialog.shared_feed_filters.everyone') }} + .toggle-item + span.toggle-name Blocked + el-radio-group(v-model="sharedFeedFilters.noty.Blocked" size="mini" @change="saveSharedFeedFilters") + el-radio-button(label="Off") {{ $t('dialog.shared_feed_filters.off') }} + el-radio-button(label="On") {{ $t('dialog.shared_feed_filters.on') }} + .toggle-item + span.toggle-name Unblocked + el-radio-group(v-model="sharedFeedFilters.noty.Unblocked" size="mini" @change="saveSharedFeedFilters") + el-radio-button(label="Off") {{ $t('dialog.shared_feed_filters.off') }} + el-radio-button(label="On") {{ $t('dialog.shared_feed_filters.on') }} + .toggle-item + span.toggle-name Muted + el-radio-group(v-model="sharedFeedFilters.noty.Muted" size="mini" @change="saveSharedFeedFilters") + el-radio-button(label="Off") {{ $t('dialog.shared_feed_filters.off') }} + el-radio-button(label="On") {{ $t('dialog.shared_feed_filters.on') }} + .toggle-item + span.toggle-name Unmuted + el-radio-group(v-model="sharedFeedFilters.noty.Unmuted" size="mini" @change="saveSharedFeedFilters") + el-radio-button(label="Off") {{ $t('dialog.shared_feed_filters.off') }} + el-radio-button(label="On") {{ $t('dialog.shared_feed_filters.on') }} + template(#footer) + el-button(type="small" @click="resetSharedFeedFilters") {{ $t('dialog.shared_feed_filters.reset') }} + el-button(size="small" style="margin-left:10px" @click="notyFeedFiltersDialog.visible = false") {{ $t('dialog.shared_feed_filters.close') }} + + //- dialog: wrist feed filters + el-dialog.x-dialog(:before-close="beforeDialogClose" @mousedown.native="dialogMouseDown" @mouseup.native="dialogMouseUp" ref="wristFeedFiltersDialog" :visible.sync="wristFeedFiltersDialog.visible" :title="$t('dialog.shared_feed_filters.wrist')" width="550px") + .toggle-list + .toggle-item + span.toggle-name Self Location + el-radio-group(v-model="sharedFeedFilters.wrist.Location" size="mini" @change="saveSharedFeedFilters") + el-radio-button(label="Off") {{ $t('dialog.shared_feed_filters.off') }} + el-radio-button(label="On") {{ $t('dialog.shared_feed_filters.on') }} + .toggle-item + span.toggle-name OnPlayerJoining + el-radio-group(v-model="sharedFeedFilters.wrist.OnPlayerJoining" size="mini" @change="saveSharedFeedFilters") + el-radio-button(label="Off") {{ $t('dialog.shared_feed_filters.off') }} + el-radio-button(label="VIP") {{ $t('dialog.shared_feed_filters.favorite') }} + el-radio-button(label="Friends") {{ $t('dialog.shared_feed_filters.friends') }} + .toggle-item + span.toggle-name OnPlayerJoined + el-radio-group(v-model="sharedFeedFilters.wrist.OnPlayerJoined" size="mini" @change="saveSharedFeedFilters") + el-radio-button(label="Off") {{ $t('dialog.shared_feed_filters.off') }} + el-radio-button(label="VIP") {{ $t('dialog.shared_feed_filters.favorite') }} + el-radio-button(label="Friends") {{ $t('dialog.shared_feed_filters.friends') }} + el-radio-button(label="Everyone") {{ $t('dialog.shared_feed_filters.everyone') }} + .toggle-item + span.toggle-name OnPlayerLeft + el-radio-group(v-model="sharedFeedFilters.wrist.OnPlayerLeft" size="mini" @change="saveSharedFeedFilters") + el-radio-button(label="Off") {{ $t('dialog.shared_feed_filters.off') }} + el-radio-button(label="VIP") {{ $t('dialog.shared_feed_filters.favorite') }} + el-radio-button(label="Friends") {{ $t('dialog.shared_feed_filters.friends') }} + el-radio-button(label="Everyone") {{ $t('dialog.shared_feed_filters.everyone') }} + .toggle-item + span.toggle-name Online + el-radio-group(v-model="sharedFeedFilters.wrist.Online" size="mini" @change="saveSharedFeedFilters") + el-radio-button(label="Off") {{ $t('dialog.shared_feed_filters.off') }} + el-radio-button(label="VIP") {{ $t('dialog.shared_feed_filters.favorite') }} + el-radio-button(label="Friends") {{ $t('dialog.shared_feed_filters.friends') }} + .toggle-item + span.toggle-name Offline + el-radio-group(v-model="sharedFeedFilters.wrist.Offline" size="mini" @change="saveSharedFeedFilters") + el-radio-button(label="Off") {{ $t('dialog.shared_feed_filters.off') }} + el-radio-button(label="VIP") {{ $t('dialog.shared_feed_filters.favorite') }} + el-radio-button(label="Friends") {{ $t('dialog.shared_feed_filters.friends') }} + .toggle-item + span.toggle-name GPS + el-radio-group(v-model="sharedFeedFilters.wrist.GPS" size="mini" @change="saveSharedFeedFilters") + el-radio-button(label="Off") {{ $t('dialog.shared_feed_filters.off') }} + el-radio-button(label="VIP") {{ $t('dialog.shared_feed_filters.favorite') }} + el-radio-button(label="Friends") {{ $t('dialog.shared_feed_filters.friends') }} + .toggle-item + span.toggle-name Status + el-radio-group(v-model="sharedFeedFilters.wrist.Status" size="mini" @change="saveSharedFeedFilters") + el-radio-button(label="Off") {{ $t('dialog.shared_feed_filters.off') }} + el-radio-button(label="VIP") {{ $t('dialog.shared_feed_filters.favorite') }} + el-radio-button(label="Friends") {{ $t('dialog.shared_feed_filters.friends') }} + .toggle-item + span.toggle-name Invite + el-radio-group(v-model="sharedFeedFilters.wrist.invite" size="mini" @change="saveSharedFeedFilters") + el-radio-button(label="Off") {{ $t('dialog.shared_feed_filters.off') }} + el-radio-button(label="VIP") {{ $t('dialog.shared_feed_filters.favorite') }} + el-radio-button(label="Friends") {{ $t('dialog.shared_feed_filters.friends') }} + .toggle-item + span.toggle-name Request Invite + el-radio-group(v-model="sharedFeedFilters.wrist.requestInvite" size="mini" @change="saveSharedFeedFilters") + el-radio-button(label="Off") {{ $t('dialog.shared_feed_filters.off') }} + el-radio-button(label="VIP") {{ $t('dialog.shared_feed_filters.favorite') }} + el-radio-button(label="Friends") {{ $t('dialog.shared_feed_filters.friends') }} + .toggle-item + span.toggle-name Invite Response + el-radio-group(v-model="sharedFeedFilters.wrist.inviteResponse" size="mini" @change="saveSharedFeedFilters") + el-radio-button(label="Off") {{ $t('dialog.shared_feed_filters.off') }} + el-radio-button(label="VIP") {{ $t('dialog.shared_feed_filters.favorite') }} + el-radio-button(label="Friends") {{ $t('dialog.shared_feed_filters.friends') }} + .toggle-item + span.toggle-name Request Invite Response + el-radio-group(v-model="sharedFeedFilters.wrist.requestInviteResponse" size="mini" @change="saveSharedFeedFilters") + el-radio-button(label="Off") {{ $t('dialog.shared_feed_filters.off') }} + el-radio-button(label="VIP") {{ $t('dialog.shared_feed_filters.favorite') }} + el-radio-button(label="Friends") {{ $t('dialog.shared_feed_filters.friends') }} + .toggle-item + span.toggle-name Friend Request + el-radio-group(v-model="sharedFeedFilters.wrist.friendRequest" size="mini" @change="saveSharedFeedFilters") + el-radio-button(label="Off") {{ $t('dialog.shared_feed_filters.off') }} + el-radio-button(label="On") {{ $t('dialog.shared_feed_filters.on') }} + .toggle-item + span.toggle-name New Friend + el-radio-group(v-model="sharedFeedFilters.wrist.Friend" size="mini" @change="saveSharedFeedFilters") + el-radio-button(label="Off") {{ $t('dialog.shared_feed_filters.off') }} + el-radio-button(label="On") {{ $t('dialog.shared_feed_filters.on') }} + .toggle-item + span.toggle-name Unfriend + el-radio-group(v-model="sharedFeedFilters.wrist.Unfriend" size="mini" @change="saveSharedFeedFilters") + el-radio-button(label="Off") {{ $t('dialog.shared_feed_filters.off') }} + el-radio-button(label="On") {{ $t('dialog.shared_feed_filters.on') }} + .toggle-item + span.toggle-name Display Name Change + el-radio-group(v-model="sharedFeedFilters.wrist.DisplayName" size="mini" @change="saveSharedFeedFilters") + el-radio-button(label="Off") {{ $t('dialog.shared_feed_filters.off') }} + el-radio-button(label="VIP") {{ $t('dialog.shared_feed_filters.favorite') }} + el-radio-button(label="Friends") {{ $t('dialog.shared_feed_filters.friends') }} + .toggle-item + span.toggle-name Trust Level Change + el-radio-group(v-model="sharedFeedFilters.wrist.TrustLevel" size="mini" @change="saveSharedFeedFilters") + el-radio-button(label="Off") {{ $t('dialog.shared_feed_filters.off') }} + el-radio-button(label="VIP") {{ $t('dialog.shared_feed_filters.favorite') }} + el-radio-button(label="Friends") {{ $t('dialog.shared_feed_filters.friends') }} + //- .toggle-item + //- span.toggle-name Boop + //- el-radio-group(v-model="sharedFeedFilters.wrist.boop" size="mini" @change="saveSharedFeedFilters") + //- el-radio-button(label="Off") {{ $t('dialog.shared_feed_filters.off') }} + //- el-radio-button(label="On") {{ $t('dialog.shared_feed_filters.on') }} + .toggle-item + span.toggle-name Group Change + el-tooltip(placement="top" style="margin-left:5px" content="When you've left or been kicked from a group, group name changed, group owner changed, role added/removed") + i.el-icon-info + el-radio-group(v-model="sharedFeedFilters.wrist.groupChange" size="mini" @change="saveSharedFeedFilters") + el-radio-button(label="Off") {{ $t('dialog.shared_feed_filters.off') }} + el-radio-button(label="On") {{ $t('dialog.shared_feed_filters.on') }} + .toggle-item + span.toggle-name Group Announcement + el-radio-group(v-model="sharedFeedFilters.wrist['group.announcement']" size="mini" @change="saveSharedFeedFilters") + el-radio-button(label="Off") {{ $t('dialog.shared_feed_filters.off') }} + el-radio-button(label="On") {{ $t('dialog.shared_feed_filters.on') }} + .toggle-item + span.toggle-name Group Join + el-tooltip(placement="top" style="margin-left:5px" content="When your request to join a group has been approved") + i.el-icon-info + el-radio-group(v-model="sharedFeedFilters.wrist['group.informative']" size="mini" @change="saveSharedFeedFilters") + el-radio-button(label="Off") {{ $t('dialog.shared_feed_filters.off') }} + el-radio-button(label="On") {{ $t('dialog.shared_feed_filters.on') }} + .toggle-item + span.toggle-name Group Invite + el-tooltip(placement="top" style="margin-left:5px" content="When someone invites you to join a group") + i.el-icon-info + el-radio-group(v-model="sharedFeedFilters.wrist['group.invite']" size="mini" @change="saveSharedFeedFilters") + el-radio-button(label="Off") {{ $t('dialog.shared_feed_filters.off') }} + el-radio-button(label="On") {{ $t('dialog.shared_feed_filters.on') }} + .toggle-item + span.toggle-name Group Join Request + el-tooltip(placement="top" style="margin-left:5px" content="When someone requests to join a group you're a moderator for") + i.el-icon-info + el-radio-group(v-model="sharedFeedFilters.wrist['group.joinRequest']" size="mini" @change="saveSharedFeedFilters") + el-radio-button(label="Off") {{ $t('dialog.shared_feed_filters.off') }} + el-radio-button(label="On") {{ $t('dialog.shared_feed_filters.on') }} + .toggle-item + span.toggle-name Group Transfer Request + el-radio-group(v-model="sharedFeedFilters.wrist['group.transfer']" size="mini" @change="saveSharedFeedFilters") + el-radio-button(label="Off") {{ $t('dialog.shared_feed_filters.off') }} + el-radio-button(label="On") {{ $t('dialog.shared_feed_filters.on') }} + .toggle-item + span.toggle-name Instance Queue Ready + el-radio-group(v-model="sharedFeedFilters.wrist['group.queueReady']" size="mini" @change="saveSharedFeedFilters") + el-radio-button(label="Off") {{ $t('dialog.shared_feed_filters.off') }} + el-radio-button(label="On") {{ $t('dialog.shared_feed_filters.on') }} + .toggle-item + span.toggle-name Instance Closed + el-tooltip(placement="top" style="margin-left:5px" content="When the instance you're in has been closed preventing anyone from joining") + i.el-icon-info + el-radio-group(v-model="sharedFeedFilters.wrist['instance.closed']" size="mini" @change="saveSharedFeedFilters") + el-radio-button(label="Off") {{ $t('dialog.shared_feed_filters.off') }} + el-radio-button(label="On") {{ $t('dialog.shared_feed_filters.on') }} + .toggle-item + span.toggle-name Video Play + el-tooltip(placement="top" style="margin-left:5px" content="Requires VRCX YouTube API option enabled") + i.el-icon-warning + el-radio-group(v-model="sharedFeedFilters.wrist.VideoPlay" size="mini" @change="saveSharedFeedFilters") + el-radio-button(label="Off") {{ $t('dialog.shared_feed_filters.off') }} + el-radio-button(label="On") {{ $t('dialog.shared_feed_filters.on') }} + .toggle-item + span.toggle-name Miscellaneous Events + el-tooltip(placement="top" style="margin-left:5px" content="Misc event from VRC game log: VRC crash auto rejoin, shader keyword limit, joining instance blocked by master, error loading video, audio device changed, error joining instance, kicked from instance, VRChat failing to start OSC server, etc...") + i.el-icon-info + el-radio-group(v-model="sharedFeedFilters.wrist.Event" size="mini" @change="saveSharedFeedFilters") + el-radio-button(label="Off") {{ $t('dialog.shared_feed_filters.off') }} + el-radio-button(label="On") {{ $t('dialog.shared_feed_filters.on') }} + .toggle-item + span.toggle-name External App + el-radio-group(v-model="sharedFeedFilters.wrist.External" size="mini" @change="saveSharedFeedFilters") + el-radio-button(label="Off") {{ $t('dialog.shared_feed_filters.off') }} + el-radio-button(label="On") {{ $t('dialog.shared_feed_filters.on') }} + .toggle-item + span.toggle-name Blocked Player Joins + el-radio-group(v-model="sharedFeedFilters.wrist.BlockedOnPlayerJoined" size="mini" @change="saveSharedFeedFilters") + el-radio-button(label="Off") {{ $t('dialog.shared_feed_filters.off') }} + el-radio-button(label="VIP") {{ $t('dialog.shared_feed_filters.favorite') }} + el-radio-button(label="Friends") {{ $t('dialog.shared_feed_filters.friends') }} + el-radio-button(label="Everyone") {{ $t('dialog.shared_feed_filters.everyone') }} + .toggle-item + span.toggle-name Blocked Player Leaves + el-radio-group(v-model="sharedFeedFilters.wrist.BlockedOnPlayerLeft" size="mini" @change="saveSharedFeedFilters") + el-radio-button(label="Off") {{ $t('dialog.shared_feed_filters.off') }} + el-radio-button(label="VIP") {{ $t('dialog.shared_feed_filters.favorite') }} + el-radio-button(label="Friends") {{ $t('dialog.shared_feed_filters.friends') }} + el-radio-button(label="Everyone") {{ $t('dialog.shared_feed_filters.everyone') }} + .toggle-item + span.toggle-name Muted Player Joins + el-radio-group(v-model="sharedFeedFilters.wrist.MutedOnPlayerJoined" size="mini" @change="saveSharedFeedFilters") + el-radio-button(label="Off") {{ $t('dialog.shared_feed_filters.off') }} + el-radio-button(label="VIP") {{ $t('dialog.shared_feed_filters.favorite') }} + el-radio-button(label="Friends") {{ $t('dialog.shared_feed_filters.friends') }} + el-radio-button(label="Everyone") {{ $t('dialog.shared_feed_filters.everyone') }} + .toggle-item + span.toggle-name Muted Player Leaves + el-radio-group(v-model="sharedFeedFilters.wrist.MutedOnPlayerLeft" size="mini" @change="saveSharedFeedFilters") + el-radio-button(label="Off") {{ $t('dialog.shared_feed_filters.off') }} + el-radio-button(label="VIP") {{ $t('dialog.shared_feed_filters.favorite') }} + el-radio-button(label="Friends") {{ $t('dialog.shared_feed_filters.friends') }} + el-radio-button(label="Everyone") {{ $t('dialog.shared_feed_filters.everyone') }} + .toggle-item + span.toggle-name Lobby Avatar Change + el-radio-group(v-model="sharedFeedFilters.wrist.AvatarChange" size="mini" @change="saveSharedFeedFilters") + el-radio-button(label="Off") {{ $t('dialog.shared_feed_filters.off') }} + el-radio-button(label="VIP") {{ $t('dialog.shared_feed_filters.favorite') }} + el-radio-button(label="Friends") {{ $t('dialog.shared_feed_filters.friends') }} + el-radio-button(label="Everyone") {{ $t('dialog.shared_feed_filters.everyone') }} + template(v-if="photonLoggingEnabled") + br + .toggle-item + span.toggle-name Photon Event Logging + .toggle-item + span.toggle-name Portal Spawn + el-radio-group(v-model="sharedFeedFilters.wrist.PortalSpawn" size="mini" @change="saveSharedFeedFilters") + el-radio-button(label="Off") {{ $t('dialog.shared_feed_filters.off') }} + el-radio-button(label="VIP") {{ $t('dialog.shared_feed_filters.favorite') }} + el-radio-button(label="Friends") {{ $t('dialog.shared_feed_filters.friends') }} + el-radio-button(label="Everyone") {{ $t('dialog.shared_feed_filters.everyone') }} + .toggle-item + span.toggle-name Lobby ChatBox Message + el-radio-group(v-model="sharedFeedFilters.wrist.ChatBoxMessage" size="mini" @change="saveSharedFeedFilters") + el-radio-button(label="Off") {{ $t('dialog.shared_feed_filters.off') }} + el-radio-button(label="VIP") {{ $t('dialog.shared_feed_filters.favorite') }} + el-radio-button(label="Friends") {{ $t('dialog.shared_feed_filters.friends') }} + el-radio-button(label="Everyone") {{ $t('dialog.shared_feed_filters.everyone') }} + .toggle-item + span.toggle-name Blocked + el-radio-group(v-model="sharedFeedFilters.wrist.Blocked" size="mini" @change="saveSharedFeedFilters") + el-radio-button(label="Off") {{ $t('dialog.shared_feed_filters.off') }} + el-radio-button(label="On") {{ $t('dialog.shared_feed_filters.on') }} + .toggle-item + span.toggle-name Unblocked + el-radio-group(v-model="sharedFeedFilters.wrist.Unblocked" size="mini" @change="saveSharedFeedFilters") + el-radio-button(label="Off") {{ $t('dialog.shared_feed_filters.off') }} + el-radio-button(label="On") {{ $t('dialog.shared_feed_filters.on') }} + .toggle-item + span.toggle-name Muted + el-radio-group(v-model="sharedFeedFilters.wrist.Muted" size="mini" @change="saveSharedFeedFilters") + el-radio-button(label="Off") {{ $t('dialog.shared_feed_filters.off') }} + el-radio-button(label="On") {{ $t('dialog.shared_feed_filters.on') }} + .toggle-item + span.toggle-name Unmuted + el-radio-group(v-model="sharedFeedFilters.wrist.Unmuted" size="mini" @change="saveSharedFeedFilters") + el-radio-button(label="Off") {{ $t('dialog.shared_feed_filters.off') }} + el-radio-button(label="On") {{ $t('dialog.shared_feed_filters.on') }} + template(#footer) + el-button(type="small" @click="resetSharedFeedFilters") {{ $t('dialog.shared_feed_filters.reset') }} + el-button(size="small" @click="wristFeedFiltersDialog.visible = false") {{ $t('dialog.shared_feed_filters.close') }} diff --git a/html/src/mixins/dialogs/groupDialog.pug b/html/src/mixins/dialogs/groupDialog.pug new file mode 100644 index 000000000..a87014169 --- /dev/null +++ b/html/src/mixins/dialogs/groupDialog.pug @@ -0,0 +1,347 @@ + +mixin groupDialog() + el-dialog.x-dialog.x-group-dialog(:before-close="beforeDialogClose" @mousedown.native="dialogMouseDown" @mouseup.native="dialogMouseUp" ref="groupDialog" :visible.sync="groupDialog.visible" :show-close="false" width="770px") + .group-banner-image + el-popover(placement="right" width="500px" trigger="click") + img.x-link(slot="reference" v-lazy="groupDialog.ref.bannerUrl" style="flex:none;width:100%;aspect-ratio:6/1;object-fit:cover;border-radius:4px") + img.x-link(v-lazy="groupDialog.ref.bannerUrl" style="width:854px;height:480px" @click="showFullscreenImageDialog(groupDialog.ref.bannerUrl)") + .group-body(v-loading="groupDialog.loading") + div(style="display:flex") + el-popover(placement="right" width="500px" trigger="click") + img.x-link(slot="reference" v-lazy="groupDialog.ref.iconUrl" style="flex:none;width:120px;height:120px;border-radius:12px") + img.x-link(v-lazy="groupDialog.ref.iconUrl" style="width:500px;height:500px" @click="showFullscreenImageDialog(groupDialog.ref.iconUrl)") + div(style="flex:1;display:flex;align-items:center;margin-left:15px") + .group-header(style="flex:1") + span(v-if="groupDialog.ref.ownerId === API.currentUser.id" style="margin-right:5px") 👑 + span.dialog-title(v-text="groupDialog.ref.name" style="margin-right:5px") + span.group-discriminator.x-grey(style="font-family:monospace;font-size:12px;margin-right:5px") {{ groupDialog.ref.shortCode }}.{{ groupDialog.ref.discriminator }} + el-tooltip(v-for="item in groupDialog.ref.$languages" :key="item.key" placement="top") + template(#content) + span {{ item.value }} ({{ item.key }}) + span.flags(:class="languageClass(item.key)" style="display:inline-block;margin-right:5px") + div(style="margin-top:5px") + span.x-link.x-grey(v-text="groupDialog.ownerDisplayName" @click="showUserDialog(groupDialog.ref.ownerId)" style="font-family:monospace") + .group-tags + el-tag(v-if="groupDialog.ref.isVerified" type="info" effect="plain" size="mini" style="margin-right:5px;margin-top:5px") {{ $t('dialog.group.tags.verified') }} + + el-tag(v-if="groupDialog.ref.privacy === 'private'" type="danger" effect="plain" size="mini" style="margin-right:5px;margin-top:5px") {{ $t('dialog.group.tags.private') }} + el-tag(v-if="groupDialog.ref.privacy === 'default'" type="success" effect="plain" size="mini" style="margin-right:5px;margin-top:5px") {{ $t('dialog.group.tags.public') }} + + el-tag(v-if="groupDialog.ref.joinState === 'open'" type="success" effect="plain" size="mini" style="margin-right:5px;margin-top:5px") {{ $t('dialog.group.tags.open') }} + el-tag(v-else-if="groupDialog.ref.joinState === 'request'" type="warning" effect="plain" size="mini" style="margin-right:5px;margin-top:5px") {{ $t('dialog.group.tags.request') }} + el-tag(v-else-if="groupDialog.ref.joinState === 'invite'" type="danger" effect="plain" size="mini" style="margin-right:5px;margin-top:5px") {{ $t('dialog.group.tags.invite') }} + el-tag(v-else-if="groupDialog.ref.joinState === 'closed'" type="danger" effect="plain" size="mini" style="margin-right:5px;margin-top:5px") {{ $t('dialog.group.tags.closed') }} + + el-tag(v-if="groupDialog.inGroup" type="info" effect="plain" size="mini" style="margin-right:5px;margin-top:5px") {{ $t('dialog.group.tags.joined') }} + el-tag(v-if="groupDialog.ref.myMember && groupDialog.ref.myMember.bannedAt" type="danger" effect="plain" size="mini" style="margin-right:5px;margin-top:5px") {{ $t('dialog.group.tags.banned') }} + + template(v-if="groupDialog.inGroup && groupDialog.ref.myMember") + el-tag(v-if="groupDialog.ref.myMember.visibility === 'visible'" type="info" effect="plain" size="mini" style="margin-right:5px;margin-top:5px") {{ $t('dialog.group.tags.visible') }} + el-tag(v-else-if="groupDialog.ref.myMember.visibility === 'friends'" type="info" effect="plain" size="mini" style="margin-right:5px;margin-top:5px") {{ $t('dialog.group.tags.friends') }} + el-tag(v-else-if="groupDialog.ref.myMember.visibility === 'hidden'" type="info" effect="plain" size="mini" style="margin-right:5px;margin-top:5px") {{ $t('dialog.group.tags.hidden') }} + el-tag(v-if="groupDialog.ref.myMember.isSubscribedToAnnouncements" type="info" effect="plain" size="mini" style="margin-right:5px;margin-top:5px") {{ $t('dialog.group.tags.subscribed') }} + + .group-description(style="margin-top:5px") + span(v-show="groupDialog.ref.name !== groupDialog.ref.description" v-text="groupDialog.ref.description" style="font-size:12px") + div(style="flex:none;margin-left:10px") + template(v-if="groupDialog.inGroup && groupDialog.ref?.myMember") + el-tooltip(v-if="groupDialog.ref.myMember?.isRepresenting" placement="top" :content="$t('dialog.group.actions.unrepresent_tooltip')" :disabled="hideTooltips") + el-button(type="warning" icon="el-icon-star-on" circle @click="clearGroupRepresentation(groupDialog.id)" style="margin-left:5px") + el-tooltip(v-else placement="top" :content="$t('dialog.group.actions.represent_tooltip')" :disabled="hideTooltips") + span + el-button(type="default" icon="el-icon-star-off" circle @click="setGroupRepresentation(groupDialog.id)" style="margin-left:5px" :disabled="groupDialog.ref.privacy === 'private'") + template(v-else-if="groupDialog.ref.myMember?.membershipStatus === 'requested'") + el-tooltip(placement="top" :content="$t('dialog.group.actions.cancel_join_request_tooltip')" :disabled="hideTooltips") + span + el-button(type="default" icon="el-icon-close" circle @click="cancelGroupRequest(groupDialog.id)" style="margin-left:5px") + template(v-else-if="groupDialog.ref.myMember?.membershipStatus === 'invited'") + el-tooltip(placement="top" :content="$t('dialog.group.actions.pending_request_tooltip')" :disabled="hideTooltips") + span + el-button(type="default" icon="el-icon-check" circle @click="joinGroup(groupDialog.id)" style="margin-left:5px") + template(v-else) + el-tooltip(v-if="groupDialog.ref.joinState === 'request'" placement="top" :content="$t('dialog.group.actions.request_join_tooltip')" :disabled="hideTooltips") + el-button(type="default" icon="el-icon-message" circle @click="joinGroup(groupDialog.id)" style="margin-left:5px") + el-tooltip(v-if="groupDialog.ref.joinState === 'invite'" placement="top" :content="$t('dialog.group.actions.invite_required_tooltip')" :disabled="hideTooltips") + span + el-button(type="default" icon="el-icon-message" disabled circle style="margin-left:5px") + el-tooltip(v-if="groupDialog.ref.joinState === 'open'" placement="top" :content="$t('dialog.group.actions.join_group_tooltip')" :disabled="hideTooltips") + el-button(type="default" icon="el-icon-check" circle @click="joinGroup(groupDialog.id)" style="margin-left:5px") + el-dropdown(trigger="click" @command="groupDialogCommand" size="small" style="margin-left:5px") + el-button(:type="groupDialog.ref.membershipStatus === 'userblocked' ? 'danger' : 'default'" icon="el-icon-more" circle) + el-dropdown-menu(#default="dropdown") + el-dropdown-item(icon="el-icon-refresh" command="Refresh") {{ $t('dialog.group.actions.refresh') }} + template(v-if="groupDialog.inGroup") + template(v-if="groupDialog.ref.myMember") + el-dropdown-item(v-if="groupDialog.ref.myMember.isSubscribedToAnnouncements" icon="el-icon-close" command="Unsubscribe To Announcements" divided) {{ $t('dialog.group.actions.unsubscribe') }} + el-dropdown-item(v-else icon="el-icon-check" command="Subscribe To Announcements" divided) {{ $t('dialog.group.actions.subscribe') }} + el-dropdown-item(v-if="hasGroupPermission(groupDialog.ref, 'group-invites-manage')" icon="el-icon-message" command="Invite To Group") {{ $t('dialog.group.actions.invite_to_group') }} + template(v-if="hasGroupPermission(groupDialog.ref, 'group-announcement-manage')") + el-dropdown-item(icon="el-icon-tickets" command="Create Post") {{ $t('dialog.group.actions.create_post') }} + //- template(v-if="hasGroupPermission(groupDialog.ref, 'group-members-manage')") + el-dropdown-item(icon="el-icon-s-operation" command="Moderation Tools") {{ $t('dialog.group.actions.moderation_tools') }} + template(v-if="groupDialog.ref.myMember && groupDialog.ref.privacy === 'default'") + el-dropdown-item(icon="el-icon-view" command="Visibility Everyone" divided) #[i.el-icon-check(v-if="groupDialog.ref.myMember.visibility === 'visible'")] {{ $t('dialog.group.actions.visibility_everyone') }} + el-dropdown-item(icon="el-icon-view" command="Visibility Friends") #[i.el-icon-check(v-if="groupDialog.ref.myMember.visibility === 'friends'")] {{ $t('dialog.group.actions.visibility_friends') }} + el-dropdown-item(icon="el-icon-view" command="Visibility Hidden") #[i.el-icon-check(v-if="groupDialog.ref.myMember.visibility === 'hidden'")] {{ $t('dialog.group.actions.visibility_hidden') }} + el-dropdown-item(icon="el-icon-delete" command="Leave Group" style="color:#F56C6C" divided) {{ $t('dialog.group.actions.leave') }} + template(v-else) + el-dropdown-item(v-if="groupDialog.ref.membershipStatus === 'userblocked'" icon="el-icon-circle-check" command="Unblock Group" style="color:#F56C6C" divided) {{ $t('dialog.group.actions.unblock') }} + el-dropdown-item(v-else icon="el-icon-circle-close" command="Block Group" divided) {{ $t('dialog.group.actions.block') }} + el-tabs(ref="groupDialogTabs" @tab-click="groupDialogTabClick") + el-tab-pane(:label="$t('dialog.group.info.header')") + .group-banner-image-info + el-popover(placement="right" width="500px" trigger="click") + img.x-link(slot="reference" v-lazy="groupDialog.ref.bannerUrl" style="flex:none;width:100%;aspect-ratio:6/1;object-fit:cover;border-radius:4px") + img.x-link(v-lazy="groupDialog.ref.bannerUrl" style="width:854px;height:480px" @click="showFullscreenImageDialog(groupDialog.ref.bannerUrl)") + .x-friend-list(style="max-height:none") + span(v-if="groupDialog.instances.length" style="font-size:12px;font-weight:bold;margin:5px") {{ $t('dialog.group.info.instances') }} + div(v-for="room in groupDialog.instances" :key="room.tag" style="width:100%") + div(style="margin:5px 0") + location(:location="room.tag") + el-tooltip(placement="top" content="Invite yourself" :disabled="hideTooltips") + invite-yourself(:location="room.tag" style="margin-left:5px") + el-tooltip(placement="top" content="Refresh player count" :disabled="hideTooltips") + el-button(@click="refreshInstancePlayerCount(room.tag)" size="mini" icon="el-icon-refresh" style="margin-left:5px" circle) + last-join(:location="room.tag" :currentlocation="lastLocation.location") + instance-info(:location="room.tag" :instance="room.ref" :friendcount="room.friendCount" :updateelement="updateInstanceInfo") + .x-friend-list(style="margin:10px 0;padding:0;max-height:unset" v-if="room.users.length") + .x-friend-item(v-for="user in room.users" :key="user.id" @click="showUserDialog(user.id)" class="x-friend-item-border") + .avatar(:class="userStatusClass(user)") + img(v-lazy="userImage(user)") + .detail + span.name(v-text="user.displayName" :style="{'color':user.$userColour}") + span.extra(v-if="user.location === 'traveling'") + i.el-icon-loading(style="margin-right:5px") + timer(:epoch="user.$travelingToTime") + span.extra(v-else) + timer(:epoch="user.$location_at") + .x-friend-item(style="width:100%;cursor:default") + .detail + span.name {{ $t('dialog.group.info.announcement') }} + span(style="display:block" v-text="groupDialog.announcement.title") + div(v-if="groupDialog.announcement.imageUrl" style="display:inline-block;margin-right:5px") + el-popover(placement="right" width="500px" trigger="click") + img.x-link(slot="reference" v-lazy="groupDialog.announcement.imageUrl" style="flex:none;width:60px;height:60px;border-radius:4px;object-fit:cover") + img.x-link(v-lazy="groupDialog.announcement.imageUrl" style="height:500px" @click="showFullscreenImageDialog(groupDialog.announcement.imageUrl)") + pre.extra(style="display:inline-block;vertical-align:top;font-family:inherit;font-size:12px;white-space:pre-wrap;margin:0") {{ groupDialog.announcement.text || '-' }} + br + .extra(v-if="groupDialog.announcement.id" style="float:right;margin-left:5px") + el-tooltip(v-if="groupDialog.announcement.roleIds.length" placement="top") + template(#content) + span {{ $t('dialog.group.posts.visibility') }} + br + template(v-for="roleId in groupDialog.announcement.roleIds" :key="roleId") + span(v-for="(role, rIndex) in groupDialog.ref.roles" :key="rIndex" v-if="role.id === roleId" v-text="role.name") + span(v-if="groupDialog.announcement.roleIds.indexOf(roleId) < groupDialog.announcement.roleIds.length - 1") ,  + i.el-icon-view(style="margin-right:5px") + display-name(:userid="groupDialog.announcement.authorId" style="margin-right:5px") + span(v-if="groupDialog.announcement.editorId" style="margin-right:5px") ({{ $t('dialog.group.posts.edited_by') }} #[display-name(:userid="groupDialog.announcement.editorId")]) + el-tooltip(placement="bottom") + template(#content) + span {{ $t('dialog.group.posts.created_at') }} {{ groupDialog.announcement.createdAt | formatDate('long') }} + template(v-if="groupDialog.announcement.updatedAt !== groupDialog.announcement.createdAt") + br + span {{ $t('dialog.group.posts.edited_at') }} {{ groupDialog.announcement.updatedAt | formatDate('long') }} + timer(:epoch="Date.parse(groupDialog.announcement.updatedAt)") + template(v-if="hasGroupPermission(groupDialog.ref, 'group-announcement-manage')") + el-tooltip(placement="top" :content="$t('dialog.group.posts.edit_tooltip')" :disabled="hideTooltips") + el-button(type="text" icon="el-icon-edit" size="mini" style="margin-left:5px" @click="showGroupPostEditDialog(groupDialog.id, groupDialog.announcement)") + el-tooltip(placement="top" :content="$t('dialog.group.posts.delete_tooltip')" :disabled="hideTooltips") + el-button(type="text" icon="el-icon-delete" size="mini" style="margin-left:5px" @click="confirmDeleteGroupPost(groupDialog.announcement)") + .x-friend-item(style="width:100%;cursor:default") + .detail + span.name {{ $t('dialog.group.info.rules') }} + pre.extra(style="font-family:inherit;font-size:12px;white-space:pre-wrap;margin:0 0.5em 0 0") {{ groupDialog.ref.rules || '-' }} + .x-friend-item(style="cursor:default") + .detail + span.name {{ $t('dialog.group.info.members') }} + .extra {{ groupDialog.ref.memberCount }} ({{ groupDialog.ref.onlineMemberCount }}) + .x-friend-item(style="cursor:default") + .detail + span.name {{ $t('dialog.group.info.created_at') }} + span.extra {{ groupDialog.ref.createdAt | formatDate('long') }} + .x-friend-item(style="cursor:default") + .detail + span.name {{ $t('dialog.group.info.links') }} + div(v-if="groupDialog.ref.links && groupDialog.ref.links.length > 0" style="margin-top:5px") + el-tooltip(v-if="link" v-for="(link, index) in groupDialog.ref.links" :key="index") + template(#content) + span(v-text="link") + img(:src="getFaviconUrl(link)" style="width:16px;height:16px;vertical-align:middle;margin-right:5px;cursor:pointer" @click.stop="openExternalLink(link)") + .extra(v-else) - + .x-friend-item(style="width:350px;cursor:default") + .detail + span.name {{ $t('dialog.group.info.url') }} + span.extra {{ groupDialog.ref.$url }} + el-tooltip(placement="top" :content="$t('dialog.group.info.url_tooltip')" :disabled="hideTooltips") + el-button(type="default" @click="copyGroupUrl(groupDialog.ref.$url)" size="mini" icon="el-icon-s-order" circle style="margin-left:5px") + .x-friend-item(style="width:350px;cursor:default") + .detail + span.name {{ $t('dialog.group.info.id') }} + span.extra {{ groupDialog.id }} + el-tooltip(placement="top" :content="$t('dialog.group.info.id_tooltip')" :disabled="hideTooltips") + el-button(type="default" @click="copyGroupId(groupDialog.id)" size="mini" icon="el-icon-s-order" circle style="margin-left:5px") + div(v-if="groupDialog.ref.membershipStatus === 'member'" style="width:100%;margin-top:10px;border-top:1px solid #e4e7ed14") + div(style="width:100%;display:flex;margin-top:10px") + .x-friend-item(style="cursor:default") + .detail + span.name {{ $t('dialog.group.info.joined_at') }} + span.extra {{ groupDialog.ref.myMember.joinedAt | formatDate('long') }} + .x-friend-item(style="cursor:default") + .detail + span.name {{ $t('dialog.group.info.roles') }} + span.extra(v-if="groupDialog.memberRoles.length === 0") - + span.extra(v-else) + template(v-for="(role, rIndex) in groupDialog.memberRoles" :key="rIndex") + el-tooltip(placement="top") + template(#content) + span {{ $t('dialog.group.info.role') }} {{ role.name }} + br + span {{ $t('dialog.group.info.role_description') }} {{ role.description }} + br + span(v-if="role.updatedAt") {{ $t('dialog.group.info.role_updated_at') }} {{ role.updatedAt | formatDate('long') }} + span(v-else) {{ $t('dialog.group.info.role_created_at') }} {{ role.createdAt | formatDate('long') }} + br + span {{ $t('dialog.group.info.role_permissions') }} + br + template(v-for="(permission, pIndex) in role.permissions" :key="pIndex") + span {{ permission }} + br + span {{ role.name }}{{ rIndex < groupDialog.memberRoles.length - 1 ? ', ' : '' }} + el-tab-pane(:label="$t('dialog.group.posts.header')") + template(v-if="groupDialog.visible") + span(style="margin-right:10px") {{ $t('dialog.group.posts.posts_count') }} {{ groupDialog.posts.length }} + el-input(v-model="groupDialog.postsSearch" @input="updateGroupPostSearch" clearable size="mini" :placeholder="$t('dialog.group.posts.search_placeholder')" style="width:89%;margin-bottom:10px") + .x-friend-list + .x-friend-item(v-for="post in groupDialog.postsFiltered" :key="post.id" style="width:100%;cursor:default") + .detail + span(style="display:block" v-text="post.title") + div(v-if="post.imageUrl" style="display:inline-block;margin-right:5px") + el-popover(placement="right" width="500px" trigger="click") + img.x-link(slot="reference" v-lazy="post.imageUrl" style="flex:none;width:60px;height:60px;border-radius:4px;object-fit:cover") + img.x-link(v-lazy="post.imageUrl" style="height:500px" @click="showFullscreenImageDialog(post.imageUrl)") + pre.extra(style="display:inline-block;vertical-align:top;font-family:inherit;font-size:12px;white-space:pre-wrap;margin:0") {{ post.text || '-' }} + br + .extra(v-if="post.authorId" style="float:right;margin-left:5px") + el-tooltip(v-if="post.roleIds.length" placement="top") + template(#content) + span {{ $t('dialog.group.posts.visibility') }} + br + template(v-for="roleId in post.roleIds" :key="roleId") + span(v-for="(role, rIndex) in groupDialog.ref.roles" :key="rIndex" v-if="role.id === roleId" v-text="role.name") + span(v-if="post.roleIds.indexOf(roleId) < post.roleIds.length - 1") ,  + i.el-icon-view(style="margin-right:5px") + display-name(:userid="post.authorId" style="margin-right:5px") + span(v-if="post.editorId" style="margin-right:5px") ({{ $t('dialog.group.posts.edited_by') }} #[display-name(:userid="post.editorId")]) + el-tooltip(placement="bottom") + template(#content) + span {{ $t('dialog.group.posts.created_at') }} {{ post.createdAt | formatDate('long') }} + template(v-if="post.updatedAt !== post.createdAt") + br + span {{ $t('dialog.group.posts.edited_at') }} {{ post.updatedAt | formatDate('long') }} + timer(:epoch="Date.parse(post.updatedAt)") + template(v-if="hasGroupPermission(groupDialog.ref, 'group-announcement-manage')") + el-tooltip(placement="top" :content="$t('dialog.group.posts.edit_tooltip')" :disabled="hideTooltips") + el-button(type="text" icon="el-icon-edit" size="mini" style="margin-left:5px" @click="showGroupPostEditDialog(groupDialog.id, post)") + el-tooltip(placement="top" :content="$t('dialog.group.posts.delete_tooltip')" :disabled="hideTooltips") + el-button(type="text" icon="el-icon-delete" size="mini" style="margin-left:5px" @click="confirmDeleteGroupPost(post)") + el-tab-pane(:label="$t('dialog.group.members.header')") + template(v-if="groupDialog.visible") + span(v-if="hasGroupPermission(groupDialog.ref, 'group-members-viewall')" style="font-weight:bold;font-size:16px") {{ $t('dialog.group.members.all_members') }} + span(v-else style="font-weight:bold;font-size:16px") {{ $t('dialog.group.members.friends_only') }} + div(style="margin-top:10px") + el-button(type="default" @click="loadAllGroupMembers" size="mini" icon="el-icon-refresh" :loading="isGroupMembersLoading" circle) + el-button(type="default" @click="downloadAndSaveJson(`${groupDialog.id}_members`, groupDialog.members)" size="mini" icon="el-icon-download" circle style="margin-left:5px") + span(v-if="groupDialog.memberSearch.length" style="font-size:14px;margin-left:5px;margin-right:5px") {{ groupDialog.memberSearchResults.length }}/{{ groupDialog.ref.memberCount }} + span(v-else style="font-size:14px;margin-left:5px;margin-right:5px") {{ groupDialog.members.length }}/{{ groupDialog.ref.memberCount }} + div(v-if="hasGroupPermission(groupDialog.ref, 'group-members-manage')" style="float:right") + span(style="margin-right:5px") {{ $t('dialog.group.members.sort_by') }} + el-dropdown(@click.native.stop trigger="click" size="small" style="margin-right:5px" :disabled="isGroupMembersLoading || groupDialog.memberSearch.length") + el-button(size="mini") + span {{ groupDialog.memberSortOrder.name }} #[i.el-icon-arrow-down.el-icon--right] + el-dropdown-menu(#default="dropdown") + el-dropdown-item(v-for="(item) in groupDialogSortingOptions" v-text="item.name" @click.native="setGroupMemberSortOrder(item)") + span(style="margin-right:5px") {{ $t('dialog.group.members.filter') }} + el-dropdown(@click.native.stop trigger="click" size="small" style="margin-right:5px" :disabled="isGroupMembersLoading || groupDialog.memberSearch.length") + el-button(size="mini") + span {{ groupDialog.memberFilter.name }} #[i.el-icon-arrow-down.el-icon--right] + el-dropdown-menu(#default="dropdown") + el-dropdown-item(v-for="(item) in groupDialogFilterOptions" v-text="item.name" @click.native="setGroupMemberFilter(item)") + el-dropdown-item(v-for="(item) in groupDialog.ref.roles" v-if="!item.defaultRole" v-text="item.name" @click.native="setGroupMemberFilter(item)") + el-input(v-model="groupDialog.memberSearch" @input="groupMembersSearch" clearable size="mini" :placeholder="$t('dialog.group.members.search')" style="margin-top:10px;margin-bottom:10px") + .x-friend-list(v-if="groupDialog.memberSearch.length" v-loading="isGroupMembersLoading" style="margin-top:10px;overflow:auto;max-height:250px;min-width:130px") + .x-friend-item(v-for="user in groupDialog.memberSearchResults" :key="user.id" @click="showUserDialog(user.userId)" class="x-friend-item-border") + .avatar + img(v-lazy="userImage(user.user)") + .detail + span.name(v-text="user.user.displayName" :style="{'color':user.user.$userColour}") + span.extra + template(v-if="hasGroupPermission(groupDialog.ref, 'group-members-manage')") + el-tooltip(v-if="user.isRepresenting" placement="top" :content="$t('dialog.group.members.representing')") + i.el-icon-collection-tag(style="margin-right:5px") + el-tooltip(v-if="user.visibility !== 'visible'" placement="top") + template(#content) + span {{ $t('dialog.group.members.visibility') }} {{ user.visibility }} + i.el-icon-view(style="margin-right:5px") + el-tooltip(v-if="!user.isSubscribedToAnnouncements" placement="top" :content="$t('dialog.group.members.unsubscribed_announcements')") + i.el-icon-chat-line-square(style="margin-right:5px") + el-tooltip(v-if="user.managerNotes" placement="top") + template(#content) + span {{ $t('dialog.group.members.manager_notes') }} + br + span {{ user.managerNotes }} + i.el-icon-edit-outline(style="margin-right:5px") + template(v-for="roleId in user.roleIds" :key="roleId") + span(v-for="(role, rIndex) in groupDialog.ref.roles" :key="rIndex" v-if="role.id === roleId" v-text="role.name") + span(v-if="user.roleIds.indexOf(roleId) < user.roleIds.length - 1") ,  + ul.infinite-list.x-friend-list(v-else-if="groupDialog.members.length > 0" v-infinite-scroll="loadMoreGroupMembers" style="margin-top:10px;overflow:auto;max-height:250px;min-width:130px") + li.infinite-list-item.x-friend-item(v-for="user in groupDialog.members" :key="user.id" @click="showUserDialog(user.userId)" class="x-friend-item-border") + .avatar + img(v-lazy="userImage(user.user)") + .detail + span.name(v-text="user.user.displayName" :style="{'color':user.user.$userColour}") + span.extra + template(v-if="hasGroupPermission(groupDialog.ref, 'group-members-manage')") + el-tooltip(v-if="user.isRepresenting" placement="top" :content="$t('dialog.group.members.representing')") + i.el-icon-collection-tag(style="margin-right:5px") + el-tooltip(v-if="user.visibility !== 'visible'" placement="top") + template(#content) + span {{ $t('dialog.group.members.visibility') }} {{ user.visibility }} + i.el-icon-view(style="margin-right:5px") + el-tooltip(v-if="!user.isSubscribedToAnnouncements" placement="top" :content="$t('dialog.group.members.unsubscribed_announcements')") + i.el-icon-chat-line-square(style="margin-right:5px") + el-tooltip(v-if="user.managerNotes" placement="top") + template(#content) + span {{ $t('dialog.group.members.manager_notes') }} + br + span {{ user.managerNotes }} + i.el-icon-edit-outline(style="margin-right:5px") + template(v-for="roleId in user.roleIds" :key="roleId") + span(v-for="(role, rIndex) in groupDialog.ref.roles" :key="rIndex" v-if="role.id === roleId" v-text="role.name") + span(v-if="user.roleIds.indexOf(roleId) < user.roleIds.length - 1") ,  + .x-friend-item(v-if="!isGroupMembersDone" v-loading="isGroupMembersLoading" style="width:100%;height:45px;text-align:center" @click="loadMoreGroupMembers") + .detail(v-if="!isGroupMembersLoading") + span.name {{ $t('dialog.group.members.load_more') }} + el-tab-pane(:label="$t('dialog.group.gallery.header')") + el-button(type="default" size="mini" icon="el-icon-refresh" @click="getGroupGalleries" :loading="isGroupGalleryLoading" circle) + el-tabs(type="card" v-loading="isGroupGalleryLoading" ref="groupDialogGallery" style="margin-top:10px") + template(v-for="(gallery, index) in groupDialog.ref.galleries") + el-tab-pane + span(slot="label") + span(v-text="gallery.name" style="font-weight:bold;font-size:16px") + i.x-user-status(style="margin-left:5px" :class="groupGalleryStatus(gallery)") + span(style="color:#909399;font-size:12px;margin-left:5px") {{ groupDialog.galleries[gallery.id] ? groupDialog.galleries[gallery.id].length : 0 }} + span(v-text="gallery.description" style="color:#c7c7c7;padding:10px") + el-carousel(:interval="0" height="600px" style="margin-top:10px") + el-carousel-item(v-for="image in groupDialog.galleries[gallery.id]" :key="image.id") + el-popover(placement="top" width="700px" trigger="click") + img.x-link(slot="reference" v-lazy="image.imageUrl" style="width:100%;height:100%;object-fit:contain") + img.x-link(v-lazy="image.imageUrl" style="height:700px" @click="showFullscreenImageDialog(image.imageUrl)") + el-tab-pane(:label="$t('dialog.group.json.header')") + el-button(type="default" @click="refreshGroupDialogTreeData()" size="mini" icon="el-icon-refresh" circle) + el-button(type="default" @click="downloadAndSaveJson(groupDialog.id, groupDialog.ref)" size="mini" icon="el-icon-download" circle style="margin-left:5px") + el-tree(:data="groupDialog.treeData" style="margin-top:5px;font-size:12px") + template(#default="scope") + span + span(v-text="scope.data.key" style="font-weight:bold;margin-right:5px") + span(v-if="!scope.data.children" v-text="scope.data.value") diff --git a/html/src/mixins/dialogs/groups.pug b/html/src/mixins/dialogs/groups.pug new file mode 100644 index 000000000..ca695b8be --- /dev/null +++ b/html/src/mixins/dialogs/groups.pug @@ -0,0 +1,334 @@ +mixin groups() + //- dialog: invite group + el-dialog.x-dialog(:before-close="beforeDialogClose" @mousedown.native="dialogMouseDown" @mouseup.native="dialogMouseUp" ref="inviteGroupDialog" :visible.sync="inviteGroupDialog.visible" :title="$t('dialog.invite_to_group.header')" width="450px") + div(v-if="inviteGroupDialog.visible" v-loading="inviteGroupDialog.loading") + span {{ $t('dialog.invite_to_group.description') }} + br + el-select(v-model="inviteGroupDialog.groupId" clearable :placeholder="$t('dialog.invite_to_group.choose_group_placeholder')" filterable :disabled="inviteGroupDialog.loading" @change="isAllowedToInviteToGroup" style="margin-top:15px") + el-option-group(v-if="API.currentUserGroups.size" :label="$t('dialog.invite_to_group.groups')" style="width:410px") + el-option.x-friend-item(v-for="group in API.currentUserGroups.values()" :key="group.id" :label="group.name" :value="group.id" style="height:auto") + .avatar + img(v-lazy="group.iconUrl") + .detail + span.name(v-text="group.name") + el-select(v-model="inviteGroupDialog.userIds" multiple clearable :placeholder="$t('dialog.invite_to_group.choose_friends_placeholder')" filterable :disabled="inviteGroupDialog.loading" style="width:100%;margin-top:15px") + el-option-group(v-if="inviteGroupDialog.userId" :label="$t('dialog.invite_to_group.selected_users')") + el-option.x-friend-item(:key="inviteGroupDialog.userObject.id" :label="inviteGroupDialog.userObject.displayName" :value="inviteGroupDialog.userObject.id" style="height:auto") + template(v-if="inviteGroupDialog.userObject.id") + .avatar(:class="userStatusClass(inviteGroupDialog.userObject)") + img(v-lazy="userImage(inviteGroupDialog.userObject)") + .detail + span.name(v-text="inviteGroupDialog.userObject.displayName" :style="{'color':inviteGroupDialog.userObject.$userColour}") + span(v-else v-text="inviteGroupDialog.userId") + el-option-group(v-if="vipFriends.length" :label="$t('side_panel.favorite')") + el-option.x-friend-item(v-for="friend in vipFriends" :key="friend.id" :label="friend.name" :value="friend.id" style="height:auto") + template(v-if="friend.ref") + .avatar(:class="userStatusClass(friend.ref)") + img(v-lazy="userImage(friend.ref)") + .detail + span.name(v-text="friend.ref.displayName" :style="{'color':friend.ref.$userColour}") + span(v-else v-text="friend.id") + el-option-group(v-if="onlineFriends.length" :label="$t('side_panel.online')") + el-option.x-friend-item(v-for="friend in onlineFriends" :key="friend.id" :label="friend.name" :value="friend.id" style="height:auto") + template(v-if="friend.ref") + .avatar(:class="userStatusClass(friend.ref)") + img(v-lazy="userImage(friend.ref)") + .detail + span.name(v-text="friend.ref.displayName" :style="{'color':friend.ref.$userColour}") + span(v-else v-text="friend.id") + el-option-group(v-if="activeFriends.length" :label="$t('side_panel.active')") + el-option.x-friend-item(v-for="friend in activeFriends" :key="friend.id" :label="friend.name" :value="friend.id" style="height:auto") + template(v-if="friend.ref") + .avatar + img(v-lazy="userImage(friend.ref)") + .detail + span.name(v-text="friend.ref.displayName" :style="{'color':friend.ref.$userColour}") + span(v-else v-text="friend.id") + el-option-group(v-if="offlineFriends.length" :label="$t('side_panel.offline')") + el-option.x-friend-item(v-for="friend in offlineFriends" :key="friend.id" :label="friend.name" :value="friend.id" style="height:auto") + template(v-if="friend.ref") + .avatar + img(v-lazy="userImage(friend.ref)") + .detail + span.name(v-text="friend.ref.displayName" :style="{'color':friend.ref.$userColour}") + span(v-else v-text="friend.id") + template(#footer) + el-button(type="primary" size="small" :disabled="inviteGroupDialog.loading || !inviteGroupDialog.userIds.length" @click="sendGroupInvite()") Invite + + //- dialog: group moderation + el-dialog.x-dialog(:before-close="beforeDialogClose" @mousedown.native="dialogMouseDown" @mouseup.native="dialogMouseUp" ref="groupMemberModeration" :visible.sync="groupMemberModeration.visible" :title="$t('dialog.group_member_moderation.header')" width="90vw") + div(v-if="groupMemberModeration.visible") + h3(v-text="groupMemberModeration.groupRef.name") + el-tabs(type="card" style="height:100%") + el-tab-pane(:label="$t('dialog.group_member_moderation.members')") + div(style="margin-top:10px") + el-button(type="default" @click="loadAllGroupMembers" size="mini" icon="el-icon-refresh" :loading="isGroupMembersLoading" circle) + span(style="font-size:14px;margin-left:5px;margin-right:5px") {{ groupMemberModerationTable.data.length }}/{{ groupMemberModeration.groupRef.memberCount }} + div(style="float:right;margin-top:5px") + span(style="margin-right:5px") {{ $t('dialog.group.members.sort_by') }} + el-dropdown(@click.native.stop trigger="click" size="small" style="margin-right:5px" :disabled="isGroupMembersLoading || groupDialog.memberSearch.length || !hasGroupPermission(groupDialog.ref, 'group-bans-manage')") + el-button(size="mini") + span {{ groupDialog.memberSortOrder.name }} #[i.el-icon-arrow-down.el-icon--right] + el-dropdown-menu(#default="dropdown") + el-dropdown-item(v-for="(item) in groupDialogSortingOptions" v-text="item.name" @click.native="setGroupMemberSortOrder(item)") + span(style="margin-right:5px") {{ $t('dialog.group.members.filter') }} + el-dropdown(@click.native.stop trigger="click" size="small" style="margin-right:5px" :disabled="isGroupMembersLoading || groupDialog.memberSearch.length || !hasGroupPermission(groupDialog.ref, 'group-bans-manage')") + el-button(size="mini") + span {{ groupDialog.memberFilter.name }} #[i.el-icon-arrow-down.el-icon--right] + el-dropdown-menu(#default="dropdown") + el-dropdown-item(v-for="(item) in groupDialogFilterOptions" v-text="item.name" @click.native="setGroupMemberFilter(item)") + el-dropdown-item(v-for="(item) in groupDialog.ref.roles" v-if="!item.defaultRole" v-text="item.name" @click.native="setGroupMemberFilter(item)") + el-input(v-model="groupDialog.memberSearch" :disabled="!hasGroupPermission(groupDialog.ref, 'group-bans-manage')" @input="groupMembersSearch" clearable size="mini" :placeholder="$t('dialog.group.members.search')" style="margin-top:10px;margin-bottom:10px") + br + el-button(size="small" @click="selectAllGroupMembers") {{ $t('dialog.group_member_moderation.select_all') }} + data-tables(v-bind="groupMemberModerationTable" style="margin-top:10px") + el-table-column(width="55" prop="$selected" :key="groupMemberModerationTableForceUpdate") + template(v-once #default="scope") + el-button(type="text" size="mini" @click.stop) + el-checkbox(v-model="scope.row.$selected" @change="groupMemberModerationTableSelectionChange(scope.row)") + el-table-column(:label="$t('dialog.group_member_moderation.avatar')" width="70" prop="photo") + template(v-once #default="scope") + el-popover(placement="right" height="500px" trigger="hover") + img.friends-list-avatar(slot="reference" v-lazy="userImage(scope.row.user)") + img.friends-list-avatar(v-lazy="userImageFull(scope.row.user)" style="height:500px;cursor:pointer" @click="showFullscreenImageDialog(userImageFull(scope.row.user))") + el-table-column(:label="$t('dialog.group_member_moderation.display_name')" width="160" prop="$displayName" sortable) + template(v-once #default="scope") + span(style="cursor:pointer" @click="showUserDialog(scope.row.userId)") + span(v-if="randomUserColours" v-text="scope.row.user.displayName" :style="{'color':scope.row.user.$userColour}") + span(v-else v-text="scope.row.user.displayName") + el-table-column(:label="$t('dialog.group_member_moderation.roles')" prop="roleIds" sortable) + template(v-once #default="scope") + template(v-for="roleId in scope.row.roleIds" :key="roleId") + span(v-for="(role, rIndex) in groupMemberModeration.groupRef.roles" :key="rIndex" v-if="role.id === roleId" v-text="role.name") + span(v-if="scope.row.roleIds.indexOf(roleId) < scope.row.roleIds.length - 1") ,  + el-table-column(:label="$t('dialog.group_member_moderation.notes')" prop="managerNotes" sortable) + template(v-once #default="scope") + span(v-text="scope.row.managerNotes" @click.stop) + el-table-column(:label="$t('dialog.group_member_moderation.joined_at')" width="170" prop="joinedAt" sortable) + template(v-once #default="scope") + span {{ scope.row.joinedAt | formatDate('long') }} + el-table-column(:label="$t('dialog.group_member_moderation.visibility')" width="120" prop="visibility" sortable) + template(v-once #default="scope") + span(v-text="scope.row.visibility") + el-tab-pane(:label="$t('dialog.group_member_moderation.bans')" :disabled="!hasGroupPermission(groupDialog.ref, 'group-bans-manage')") + div(style="margin-top:10px") + el-button(type="default" @click="getAllGroupBans(groupMemberModeration.id)" size="mini" icon="el-icon-refresh" :loading="isGroupMembersLoading" circle) + span(style="font-size:14px;margin-left:5px;margin-right:5px") {{ groupBansModerationTable.data.length }} + br + el-input(v-model="groupBansModerationTable.filters[0].value" clearable size="mini" :placeholder="$t('dialog.group.members.search')" style="margin-top:10px;margin-bottom:10px") + br + el-button(size="small" @click="selectAllGroupBans") {{ $t('dialog.group_member_moderation.select_all') }} + data-tables(v-bind="groupBansModerationTable" style="margin-top:10px") + el-table-column(width="55" prop="$selected" :key="groupMemberModerationTableForceUpdate") + template(v-once #default="scope") + el-button(type="text" size="mini" @click.stop) + el-checkbox(v-model="scope.row.$selected" @change="groupMemberModerationTableSelectionChange(scope.row)") + el-table-column(:label="$t('dialog.group_member_moderation.avatar')" width="70" prop="photo") + template(v-once #default="scope") + el-popover(placement="right" height="500px" trigger="hover") + img.friends-list-avatar(slot="reference" v-lazy="userImage(scope.row.user)") + img.friends-list-avatar(v-lazy="userImageFull(scope.row.user)" style="height:500px;cursor:pointer" @click="showFullscreenImageDialog(userImageFull(scope.row.user))") + el-table-column(:label="$t('dialog.group_member_moderation.display_name')" width="160" prop="$displayName" sortable) + template(v-once #default="scope") + span(style="cursor:pointer" @click="showUserDialog(scope.row.userId)") + span(v-if="randomUserColours" v-text="scope.row.user.displayName" :style="{'color':scope.row.user.$userColour}") + span(v-else v-text="scope.row.user.displayName") + el-table-column(:label="$t('dialog.group_member_moderation.roles')" prop="roleIds" sortable) + template(v-once #default="scope") + template(v-for="roleId in scope.row.roleIds" :key="roleId") + span(v-for="(role, rIndex) in groupMemberModeration.groupRef.roles" :key="rIndex" v-if="role.id === roleId" v-text="role.name") + span(v-if="scope.row.roleIds.indexOf(roleId) < scope.row.roleIds.length - 1") ,  + el-table-column(:label="$t('dialog.group_member_moderation.notes')" prop="managerNotes" sortable) + template(v-once #default="scope") + span(v-text="scope.row.managerNotes" @click.stop) + el-table-column(:label="$t('dialog.group_member_moderation.joined_at')" width="170" prop="joinedAt" sortable) + template(v-once #default="scope") + span {{ scope.row.joinedAt | formatDate('long') }} + el-table-column(:label="$t('dialog.group_member_moderation.banned_at')" width="170" prop="joinedAt" sortable) + template(v-once #default="scope") + span {{ scope.row.bannedAt | formatDate('long') }} + el-tab-pane(:label="$t('dialog.group_member_moderation.invites')" :disabled="!hasGroupPermission(groupDialog.ref, 'group-invites-manage')") + div(style="margin-top:10px") + el-button(type="default" @click="getAllGroupInvitesAndJoinRequests(groupMemberModeration.id)" size="mini" icon="el-icon-refresh" :loading="isGroupMembersLoading" circle) + br + el-tabs + el-tab-pane + span(slot="label") + span(v-text="$t('dialog.group_member_moderation.sent_invites')" style="font-weight:bold;font-size:16px") + span(style="color:#909399;font-size:12px;margin-left:5px") {{ groupInvitesModerationTable.data.length }} + el-button(size="small" @click="selectAllGroupInvites") {{ $t('dialog.group_member_moderation.select_all') }} + data-tables(v-bind="groupInvitesModerationTable" style="margin-top:10px") + el-table-column(width="55" prop="$selected" :key="groupMemberModerationTableForceUpdate") + template(v-once #default="scope") + el-button(type="text" size="mini" @click.stop) + el-checkbox(v-model="scope.row.$selected" @change="groupMemberModerationTableSelectionChange(scope.row)") + el-table-column(:label="$t('dialog.group_member_moderation.avatar')" width="70" prop="photo") + template(v-once #default="scope") + el-popover(placement="right" height="500px" trigger="hover") + img.friends-list-avatar(slot="reference" v-lazy="userImage(scope.row.user)") + img.friends-list-avatar(v-lazy="userImageFull(scope.row.user)" style="height:500px;cursor:pointer" @click="showFullscreenImageDialog(userImageFull(scope.row.user))") + el-table-column(:label="$t('dialog.group_member_moderation.display_name')" width="160" prop="$displayName" sortable) + template(v-once #default="scope") + span(style="cursor:pointer" @click="showUserDialog(scope.row.userId)") + span(v-if="randomUserColours" v-text="scope.row.user.displayName" :style="{'color':scope.row.user.$userColour}") + span(v-else v-text="scope.row.user.displayName") + el-table-column(:label="$t('dialog.group_member_moderation.notes')" prop="managerNotes" sortable) + template(v-once #default="scope") + span(v-text="scope.row.managerNotes" @click.stop) + br + el-button(@click="groupMembersDeleteSentInvite" :disabled="groupMemberModeration.progressCurrent || !hasGroupPermission(groupDialog.ref, 'group-invites-manage')") {{ $t('dialog.group_member_moderation.delete_sent_invite') }} + el-tab-pane + span(slot="label") + span(v-text="$t('dialog.group_member_moderation.join_requests')" style="font-weight:bold;font-size:16px") + span(style="color:#909399;font-size:12px;margin-left:5px") {{ groupJoinRequestsModerationTable.data.length }} + el-button(size="small" @click="selectAllGroupJoinRequests") {{ $t('dialog.group_member_moderation.select_all') }} + data-tables(v-bind="groupJoinRequestsModerationTable" style="margin-top:10px") + el-table-column(width="55" prop="$selected" :key="groupMemberModerationTableForceUpdate") + template(v-once #default="scope") + el-button(type="text" size="mini" @click.stop) + el-checkbox(v-model="scope.row.$selected" @change="groupMemberModerationTableSelectionChange(scope.row)") + el-table-column(:label="$t('dialog.group_member_moderation.avatar')" width="70" prop="photo") + template(v-once #default="scope") + el-popover(placement="right" height="500px" trigger="hover") + img.friends-list-avatar(slot="reference" v-lazy="userImage(scope.row.user)") + img.friends-list-avatar(v-lazy="userImageFull(scope.row.user)" style="height:500px;cursor:pointer" @click="showFullscreenImageDialog(userImageFull(scope.row.user))") + el-table-column(:label="$t('dialog.group_member_moderation.display_name')" width="160" prop="$displayName" sortable) + template(v-once #default="scope") + span(style="cursor:pointer" @click="showUserDialog(scope.row.userId)") + span(v-if="randomUserColours" v-text="scope.row.user.displayName" :style="{'color':scope.row.user.$userColour}") + span(v-else v-text="scope.row.user.displayName") + el-table-column(:label="$t('dialog.group_member_moderation.notes')" prop="managerNotes" sortable) + template(v-once #default="scope") + span(v-text="scope.row.managerNotes" @click.stop) + br + el-button(@click="groupMembersAcceptInviteRequest" :disabled="groupMemberModeration.progressCurrent || !hasGroupPermission(groupDialog.ref, 'group-invites-manage')") {{ $t('dialog.group_member_moderation.accept_join_requests') }} + el-button(@click="groupMembersRejectInviteRequest" :disabled="groupMemberModeration.progressCurrent || !hasGroupPermission(groupDialog.ref, 'group-invites-manage')") {{ $t('dialog.group_member_moderation.reject_join_requests') }} + el-button(@click="groupMembersBlockJoinRequest" :disabled="groupMemberModeration.progressCurrent || !hasGroupPermission(groupDialog.ref, 'group-invites-manage')") {{ $t('dialog.group_member_moderation.block_join_requests') }} + el-tab-pane + span(slot="label") + span(v-text="$t('dialog.group_member_moderation.blocked_requests')" style="font-weight:bold;font-size:16px") + span(style="color:#909399;font-size:12px;margin-left:5px") {{ groupBlockedModerationTable.data.length }} + el-button(size="small" @click="selectAllGroupBlocked") {{ $t('dialog.group_member_moderation.select_all') }} + data-tables(v-bind="groupBlockedModerationTable" style="margin-top:10px") + el-table-column(width="55" prop="$selected" :key="groupMemberModerationTableForceUpdate") + template(v-once #default="scope") + el-button(type="text" size="mini" @click.stop) + el-checkbox(v-model="scope.row.$selected" @change="groupMemberModerationTableSelectionChange(scope.row)") + el-table-column(:label="$t('dialog.group_member_moderation.avatar')" width="70" prop="photo") + template(v-once #default="scope") + el-popover(placement="right" height="500px" trigger="hover") + img.friends-list-avatar(slot="reference" v-lazy="userImage(scope.row.user)") + img.friends-list-avatar(v-lazy="userImageFull(scope.row.user)" style="height:500px;cursor:pointer" @click="showFullscreenImageDialog(userImageFull(scope.row.user))") + el-table-column(:label="$t('dialog.group_member_moderation.display_name')" width="160" prop="$displayName" sortable) + template(v-once #default="scope") + span(style="cursor:pointer" @click="showUserDialog(scope.row.userId)") + span(v-if="randomUserColours" v-text="scope.row.user.displayName" :style="{'color':scope.row.user.$userColour}") + span(v-else v-text="scope.row.user.displayName") + el-table-column(:label="$t('dialog.group_member_moderation.notes')" prop="managerNotes" sortable) + template(v-once #default="scope") + span(v-text="scope.row.managerNotes" @click.stop) + br + el-button(@click="groupMembersDeleteBlockedRequest" :disabled="groupMemberModeration.progressCurrent || !hasGroupPermission(groupDialog.ref, 'group-invites-manage')") {{ $t('dialog.group_member_moderation.delete_blocked_requests') }} + el-tab-pane(:label="$t('dialog.group_member_moderation.logs')" :disabled="!hasGroupPermission(groupDialog.ref, 'group-audit-view')") + div(style="margin-top:10px") + el-button(type="default" @click="getAllGroupLogs(groupMemberModeration.id)" size="mini" icon="el-icon-refresh" :loading="isGroupMembersLoading" circle) + span(style="font-size:14px;margin-left:5px;margin-right:5px") {{ groupLogsModerationTable.data.length }} + br + el-select(v-model="groupMemberModeration.selectedAuditLogTypes" multiple collapse-tags :placeholder="$t('dialog.group_member_moderation.filter_type')") + el-option-group(:label="$t('dialog.group_member_moderation.select_type')") + el-option.x-friend-item(v-for="type in groupMemberModeration.auditLogTypes" :key="type" :label="getAuditLogTypeName(type)" :value="type") + .detail + span.name(v-text="getAuditLogTypeName(type)") + el-input(v-model="groupLogsModerationTable.filters[0].value" :placeholder="$t('dialog.group_member_moderation.search_placeholder')" style="display:inline-block;width:150px;margin:10px") + br + data-tables(v-bind="groupLogsModerationTable" style="margin-top:10px") + el-table-column(:label="$t('dialog.group_member_moderation.created_at')" width="170" prop="created_at" sortable) + template(v-once #default="scope") + span {{ scope.row.created_at | formatDate('long') }} + el-table-column(:label="$t('dialog.group_member_moderation.type')" width="190" prop="eventType" sortable) + template(v-once #default="scope") + span(v-text="scope.row.eventType") + el-table-column(:label="$t('dialog.group_member_moderation.display_name')" width="160" prop="actorDisplayName" sortable) + template(v-once #default="scope") + span(style="cursor:pointer" @click="showUserDialog(scope.row.actorId)") + span(v-text="scope.row.actorDisplayName") + el-table-column(:label="$t('dialog.group_member_moderation.description')" prop="description") + template(v-once #default="scope") + span(v-text="scope.row.description") + el-table-column(:label="$t('dialog.group_member_moderation.data')" prop="data") + template(v-once #default="scope") + span(v-if="Object.keys(scope.row.data).length" v-text="JSON.stringify(scope.row.data)") + br + br + span.name {{ $t('dialog.group_member_moderation.user_id') }} + br + el-input(v-model="groupMemberModeration.selectUserId" size="mini" style="margin-top:5px;width:340px" :placeholder="$t('dialog.group_member_moderation.user_id_placeholder')" clearable) + el-button(size="small" @click="selectGroupMemberUserId" :disabled="!groupMemberModeration.selectUserId") {{ $t('dialog.group_member_moderation.select_user') }} + br + br + span.name {{ $t('dialog.group_member_moderation.selected_users') }} + el-button(type="default" @click="clearSelectedGroupMembers" size="mini" icon="el-icon-delete" circle style="margin-left:5px") + br + el-tag(v-for="user in groupMemberModeration.selectedUsersArray" type="info" disable-transitions="true" :key="user.id" style="margin-right:5px;margin-top:5px" closable @close="deleteSelectedGroupMember(user)") + span {{ user.user?.displayName }} #[i.el-icon-warning(v-if="user.membershipStatus !== 'member'" style="margin-left:5px")] + br + br + span.name {{ $t('dialog.group_member_moderation.notes') }} + el-input.extra(v-model="groupMemberModeration.note" type="textarea" :rows="2" :autosize="{ minRows: 1, maxRows: 20 }" :placeholder="$t('dialog.group_member_moderation.note_placeholder')" size="mini" resize="none" style="margin-top:5px") + br + br + span.name {{ $t('dialog.group_member_moderation.selected_roles') }} + br + el-select(v-model="groupMemberModeration.selectedRoles" clearable multiple :placeholder="$t('dialog.group_member_moderation.choose_roles_placeholder')" filterable style="margin-top:5px") + el-option-group(:label="$t('dialog.group_member_moderation.roles')") + el-option.x-friend-item(v-for="role in groupMemberModeration.groupRef.roles" :key="role.id" :label="role.name" :value="role.id" style="height:auto") + .detail + span.name(v-text="role.name") + br + br + span.name {{ $t('dialog.group_member_moderation.actions') }} + br + el-button(@click="groupMembersAddRoles" :disabled="!groupMemberModeration.selectedRoles.length || groupMemberModeration.progressCurrent || !hasGroupPermission(groupDialog.ref, 'group-roles-assign')") {{ $t('dialog.group_member_moderation.add_roles') }} + el-button(@click="groupMembersRemoveRoles" :disabled="!groupMemberModeration.selectedRoles.length || groupMemberModeration.progressCurrent || !hasGroupPermission(groupDialog.ref, 'group-roles-assign')") {{ $t('dialog.group_member_moderation.remove_roles') }} + el-button(@click="groupMembersSaveNote" :disabled="groupMemberModeration.progressCurrent || !hasGroupPermission(groupDialog.ref, 'group-members-manage')") {{ $t('dialog.group_member_moderation.save_note') }} + el-button(@click="groupMembersKick" :disabled="groupMemberModeration.progressCurrent || !hasGroupPermission(groupDialog.ref, 'group-members-remove')") {{ $t('dialog.group_member_moderation.kick') }} + el-button(@click="groupMembersBan" :disabled="groupMemberModeration.progressCurrent || !hasGroupPermission(groupDialog.ref, 'group-bans-manage')") {{ $t('dialog.group_member_moderation.ban') }} + el-button(@click="groupMembersUnban" :disabled="groupMemberModeration.progressCurrent || !hasGroupPermission(groupDialog.ref, 'group-bans-manage')") {{ $t('dialog.group_member_moderation.unban') }} + span(v-if="groupMemberModeration.progressCurrent" style="margin-top:10px") #[i.el-icon-loading(style="margin-left:5px;margin-right:5px")] {{ $t('dialog.group_member_moderation.progress') }} {{ groupMemberModeration.progressCurrent }}/{{ groupMemberModeration.progressTotal }} + el-button(v-if="groupMemberModeration.progressCurrent" @click="groupMemberModeration.progressTotal = 0" style="margin-left:5px") {{ $t('dialog.group_member_moderation.cancel') }} + + //- dialog: group posts + el-dialog.x-dialog(:before-close="beforeDialogClose" @mousedown.native="dialogMouseDown" @mouseup.native="dialogMouseUp" ref="groupPostEditDialog" :visible.sync="groupPostEditDialog.visible" :title="$t('dialog.group_post_edit.header')" width="650px") + div(v-if="groupPostEditDialog.visible") + h3(v-text="groupPostEditDialog.groupRef.name") + el-form(:model="groupPostEditDialog" label-width="150px") + el-form-item(:label="$t('dialog.group_post_edit.title')") + el-input(v-model="groupPostEditDialog.title" size="mini") + el-form-item(:label="$t('dialog.group_post_edit.message')") + el-input(v-model="groupPostEditDialog.text" type="textarea" :rows="4" :autosize="{ minRows: 4, maxRows: 20 }" style="margin-top:10px" resize="none") + el-form-item + el-checkbox(v-if="!groupPostEditDialog.postId" v-model="groupPostEditDialog.sendNotification" size="small") {{ $t('dialog.group_post_edit.send_notification') }} + el-form-item(:label="$t('dialog.group_post_edit.post_visibility')") + el-radio-group(v-model="groupPostEditDialog.visibility" size="small") + el-radio(label="public") {{ $t('dialog.group_post_edit.visibility_public') }} + el-radio(label="group") {{ $t('dialog.group_post_edit.visibility_group') }} + el-form-item(v-if="groupPostEditDialog.visibility === 'group'" :label="$t('dialog.new_instance.roles')") + el-select(v-model="groupPostEditDialog.roleIds" multiple clearable :placeholder="$t('dialog.new_instance.role_placeholder')" style="width:100%") + el-option-group(:label="$t('dialog.new_instance.role_placeholder')") + el-option.x-friend-item(v-for="role in groupPostEditDialog.groupRef?.roles" :key="role.id" :label="role.name" :value="role.id" style="height:auto;width:478px") + .detail + span.name(v-text="role.name") + el-form-item(:label="$t('dialog.group_post_edit.image')") + template(v-if="gallerySelectDialog.selectedFileId") + div(style="display:inline-block;flex:none;margin-right:5px") + el-popover(placement="right" width="500px" trigger="click") + img.x-link(slot="reference" v-lazy="gallerySelectDialog.selectedImageUrl" style="flex:none;width:60px;height:60px;border-radius:4px;object-fit:cover") + img.x-link(v-lazy="gallerySelectDialog.selectedImageUrl" style="height:500px" @click="showFullscreenImageDialog(gallerySelectDialog.selectedImageUrl)") + el-button(size="mini" @click="clearImageGallerySelect" style="vertical-align:top") {{ $t('dialog.invite_message.clear_selected_image') }} + template(v-else) + el-button(size="mini" @click="showGallerySelectDialog" style="margin-right:5px") {{ $t('dialog.invite_message.select_image') }} + + template(#footer) + el-button(size="small" @click="groupPostEditDialog.visible = false") {{ $t('dialog.group_post_edit.cancel') }} + el-button(v-if="groupPostEditDialog.postId" size="small" @click="editGroupPost") {{ $t('dialog.group_post_edit.edit_post') }} + el-button(v-else size="small" @click="createGroupPost") {{ $t('dialog.group_post_edit.create_post') }} diff --git a/html/src/mixins/dialogs/images.pug b/html/src/mixins/dialogs/images.pug new file mode 100644 index 000000000..903c71e18 --- /dev/null +++ b/html/src/mixins/dialogs/images.pug @@ -0,0 +1,60 @@ +mixin images() + //- dialog: Change avatar image + el-dialog.x-dialog(:before-close="beforeDialogClose" @mousedown.native="dialogMouseDown" @mouseup.native="dialogMouseUp" ref="changeAvatarImageDialog" :visible.sync="changeAvatarImageDialogVisible" :title="$t('dialog.change_content_image.avatar')" width="850px") + div(v-if="changeAvatarImageDialogVisible" v-loading="changeAvatarImageDialogLoading") + input(type="file" accept="image/png,image/jpg,image/jpeg,image/webp,image/bmp,image/gif" @change="onFileChangeAvatarImage" id="AvatarImageUploadButton" style="display:none") + span {{ $t('dialog.change_content_image.description') }} + br + el-button-group(style="padding-bottom:10px;padding-top:10px") + el-button(type="default" size="small" @click="displayPreviousImages('Avatar', 'Change')" icon="el-icon-refresh") {{ $t('dialog.change_content_image.refresh') }} + el-button(type="default" size="small" @click="uploadAvatarImage" icon="el-icon-upload2") {{ $t('dialog.change_content_image.upload') }} + //- el-button(type="default" size="small" @click="deleteAvatarImage" icon="el-icon-delete") Delete Latest Image + br + div(style="display:inline-block" v-for="image in previousImagesTable" :key="image.version" v-if="image.file") + .x-change-image-item(@click="setAvatarImage(image)" style="cursor:pointer" :class="{ 'current-image': compareCurrentImage(image) }") + img.image(v-lazy="image.file.url") + + //- dialog: Change world image + el-dialog.x-dialog(:before-close="beforeDialogClose" @mousedown.native="dialogMouseDown" @mouseup.native="dialogMouseUp" ref="changeWorldImageDialog" :visible.sync="changeWorldImageDialogVisible" :title="$t('dialog.change_content_image.world')" width="850px") + div(v-if="changeWorldImageDialogVisible" v-loading="changeWorldImageDialogLoading") + input(type="file" accept="image/png,image/jpg,image/jpeg,image/webp,image/bmp,image/gif" @change="onFileChangeWorldImage" id="WorldImageUploadButton" style="display:none") + span {{ $t('dialog.change_content_image.description') }} + br + el-button-group(style="padding-bottom:10px;padding-top:10px") + el-button(type="default" size="small" @click="displayPreviousImages('World', 'Change')" icon="el-icon-refresh") {{ $t('dialog.change_content_image.refresh') }} + el-button(type="default" size="small" @click="uploadWorldImage" icon="el-icon-upload2") {{ $t('dialog.change_content_image.upload') }} + //- el-button(type="default" size="small" @click="deleteWorldImage" icon="el-icon-delete") Delete Latest Image + br + div(style="display:inline-block" v-for="image in previousImagesTable" :key="image.version" v-if="image.file") + .x-change-image-item(@click="setWorldImage(image)" style="cursor:pointer" :class="{ 'current-image': compareCurrentImage(image) }") + img.image(v-lazy="image.file.url") + + //- dialog: Display previous avatar/world images + el-dialog.x-dialog(:before-close="beforeDialogClose" @mousedown.native="dialogMouseDown" @mouseup.native="dialogMouseUp" ref="previousImagesDialog" :visible.sync="previousImagesDialogVisible" :title="$t('dialog.previous_images.header')" width="800px") + div(v-if="previousImagesDialogVisible") + div(style="display:inline-block" v-for="image in previousImagesTable" :key="image.version" v-if="image.file") + el-popover.x-change-image-item(placement="right" width="500px" trigger="click") + img.x-link(slot="reference" v-lazy="image.file.url") + img.x-link(v-lazy="image.file.url" style="width:500px;height:375px" @click="showFullscreenImageDialog(image.file.url)") + + //- dialog: gallery select + el-dialog.x-dialog(:before-close="beforeDialogClose" @mousedown.native="dialogMouseDown" @mouseup.native="dialogMouseUp" ref="gallerySelectDialog" :visible.sync="gallerySelectDialog.visible" :title="$t('dialog.gallery_select.header')" width="100%") + div(v-if="gallerySelectDialog.visible") + span(slot="label") {{ $t('dialog.gallery_select.gallery') }} + span(style="color:#909399;font-size:12px;margin-left:5px") {{ galleryTable.length }}/64 + br + input(type="file" accept="image/png,image/jpg,image/jpeg,image/webp,image/bmp,image/gif" @change="onFileChangeGallery" id="GalleryUploadButton" style="display:none") + el-button-group + el-button(type="default" size="small" @click="selectImageGallerySelect('', '')" icon="el-icon-close") {{ $t('dialog.gallery_select.none') }} + el-button(type="default" size="small" @click="refreshGalleryTable" icon="el-icon-refresh") {{ $t('dialog.gallery_select.refresh') }} + el-button(type="default" size="small" @click="displayGalleryUpload" icon="el-icon-upload2" :disabled="!API.currentUser.$isVRCPlus") {{ $t('dialog.gallery_select.upload') }} + br + .x-friend-item(v-if="image.versions && image.versions.length > 0" v-for="image in galleryTable" :key="image.id" style="display:inline-block;margin-top:10px;width:unset;cursor:default") + .vrcplus-icon(v-if="image.versions[image.versions.length - 1].file.url" @click="selectImageGallerySelect(image.versions[image.versions.length - 1].file.url, image.id)") + img.avatar(v-lazy="image.versions[image.versions.length - 1].file.url") + + //- dialog: full screen image + el-dialog.x-dialog(ref="fullscreenImageDialog" :before-close="beforeDialogClose" @mousedown.native="dialogMouseDown" @mouseup.native="dialogMouseUp" :visible.sync="fullscreenImageDialog.visible" top="3vh" width="97vw") + el-button(@click="copyImageUrl(fullscreenImageDialog.imageUrl)" size="mini" icon="el-icon-s-order" circle) + el-button(type="default" size="mini" icon="el-icon-download" circle @click="downloadAndSaveImage(fullscreenImageDialog.imageUrl)" style="margin-left:5px") + img(v-lazy="fullscreenImageDialog.imageUrl" style="width:100%;height:100vh;object-fit:contain") diff --git a/html/src/mixins/dialogs/invites.pug b/html/src/mixins/dialogs/invites.pug new file mode 100644 index 000000000..d04fc879a --- /dev/null +++ b/html/src/mixins/dialogs/invites.pug @@ -0,0 +1,170 @@ +mixin invites() + //- dialog: invite + el-dialog.x-dialog(:before-close="beforeDialogClose" @mousedown.native="dialogMouseDown" @mouseup.native="dialogMouseUp" ref="inviteDialog" :visible.sync="inviteDialog.visible" :title="$t('dialog.invite.header')" width="500px") + div(v-if="inviteDialog.visible" v-loading="inviteDialog.loading") + location(:location="inviteDialog.worldId" :link="false") + br + el-button(size="mini" v-text="$t('dialog.invite.add_self')" @click="addSelfToInvite" style="margin-top:10px") + el-button(size="mini" v-text="$t('dialog.invite.add_friends_in_instance')" @click="addFriendsInInstanceToInvite" :disabled="inviteDialog.friendsInInstance.length === 0" style="margin-top:10px") + el-button(size="mini" v-text="$t('dialog.invite.add_favorite_friends')" @click="addFavoriteFriendsToInvite" :disabled="vipFriends.length === 0" style="margin-top:10px") + el-select(v-model="inviteDialog.userIds" multiple clearable :placeholder="$t('dialog.invite.select_placeholder')" filterable :disabled="inviteDialog.loading" style="width:100%;margin-top:15px") + el-option-group(v-if="API.currentUser" :label="$t('side_panel.me')") + el-option.x-friend-item(:label="API.currentUser.displayName" :value="API.currentUser.id" style="height:auto") + .avatar(:class="userStatusClass(API.currentUser)") + img(v-lazy="userImage(API.currentUser)") + .detail + span.name(v-text="API.currentUser.displayName") + el-option-group(v-if="inviteDialog.friendsInInstance.length" :label="$t('dialog.invite.friends_in_instance')") + el-option.x-friend-item(v-for="friend in inviteDialog.friendsInInstance" :key="friend.id" :label="friend.name" :value="friend.id" style="height:auto") + template(v-if="friend.ref") + .avatar(:class="userStatusClass(friend.ref)") + img(v-lazy="userImage(friend.ref)") + .detail + span.name(v-text="friend.ref.displayName" :style="{'color':friend.ref.$userColour}") + span(v-else v-text="friend.id") + el-option-group(v-if="vipFriends.length" :label="$t('side_panel.favorite')") + el-option.x-friend-item(v-for="friend in vipFriends" :key="friend.id" :label="friend.name" :value="friend.id" style="height:auto") + template(v-if="friend.ref") + .avatar(:class="userStatusClass(friend.ref)") + img(v-lazy="userImage(friend.ref)") + .detail + span.name(v-text="friend.ref.displayName" :style="{'color':friend.ref.$userColour}") + span(v-else v-text="friend.id") + el-option-group(v-if="onlineFriends.length" :label="$t('side_panel.online')") + el-option.x-friend-item(v-for="friend in onlineFriends" :key="friend.id" :label="friend.name" :value="friend.id" style="height:auto") + template(v-if="friend.ref") + .avatar(:class="userStatusClass(friend.ref)") + img(v-lazy="userImage(friend.ref)") + .detail + span.name(v-text="friend.ref.displayName" :style="{'color':friend.ref.$userColour}") + span(v-else v-text="friend.id") + el-option-group(v-if="activeFriends.length" :label="$t('side_panel.active')") + el-option.x-friend-item(v-for="friend in activeFriends" :key="friend.id" :label="friend.name" :value="friend.id" style="height:auto") + template(v-if="friend.ref") + .avatar + img(v-lazy="userImage(friend.ref)") + .detail + span.name(v-text="friend.ref.displayName" :style="{'color':friend.ref.$userColour}") + span(v-else v-text="friend.id") + template(#footer) + el-button(size="small" :disabled="inviteDialog.loading || !inviteDialog.userIds.length" @click="showSendInviteDialog()") {{ $t('dialog.invite.invite_with_message') }} + el-button(type="primary" size="small" :disabled="inviteDialog.loading || !inviteDialog.userIds.length" @click="sendInvite()") {{ $t('dialog.invite.invite') }} + + //- dialog: Edit Invite Message + el-dialog.x-dialog(:before-close="beforeDialogClose" @mousedown.native="dialogMouseDown" @mouseup.native="dialogMouseUp" ref="editInviteMessageDialog" :visible.sync="editInviteMessageDialog.visible" :title="$t('dialog.edit_invite_message.header')" width="400px") + div(style='font-size:12px') + span {{ $t('dialog.edit_invite_message.description') }} + el-input(type="textarea" v-model="editInviteMessageDialog.newMessage" size="mini" maxlength="64" show-word-limit :autosize="{ minRows:2, maxRows:5 }" placeholder="" style="margin-top:10px") + template(#footer) + el-button(type="small" @click="cancelEditInviteMessage") {{ $t('dialog.edit_invite_message.cancel') }} + el-button(type="primary" size="small" @click="saveEditInviteMessage") {{ $t('dialog.edit_invite_message.save') }} + + //- dialog: Edit And Send Invite Response Message + el-dialog.x-dialog(:before-close="beforeDialogClose" @mousedown.native="dialogMouseDown" @mouseup.native="dialogMouseUp" ref="editAndSendInviteResponseDialog" :visible.sync="editAndSendInviteResponseDialog.visible" :title="$t('dialog.edit_send_invite_response_message.header')" width="400px") + div(style='font-size:12px') + span {{ $t('dialog.edit_send_invite_response_message.description') }} + el-input(type="textarea" v-model="editAndSendInviteResponseDialog.newMessage" size="mini" maxlength="64" show-word-limit :autosize="{ minRows:2, maxRows:5 }" placeholder="" style="margin-top:10px") + template(#footer) + el-button(type="small" @click="cancelEditAndSendInviteResponse") {{ $t('dialog.edit_send_invite_response_message.cancel') }} + el-button(type="primary" size="small" @click="saveEditAndSendInviteResponse") {{ $t('dialog.edit_send_invite_response_message.send') }} + + //- dialog Table: Send Invite Response Message + el-dialog.x-dialog(:before-close="beforeDialogClose" @mousedown.native="dialogMouseDown" @mouseup.native="dialogMouseUp" ref="sendInviteResponseDialog" :visible.sync="sendInviteResponseDialogVisible" :title="$t('dialog.invite_response_message.header')" width="800px") + template(v-if="API.currentUser.$isVRCPlus") + input.inviteImageUploadButton(type="file" accept="image/png,image/jpg,image/jpeg,image/webp,image/bmp,image/gif" @change="inviteImageUpload") + data-tables(v-if="sendInviteResponseDialogVisible" v-bind="inviteResponseMessageTable" @row-click="showSendInviteResponseConfirmDialog" style="margin-top:10px;cursor:pointer") + el-table-column(:label="$t('table.profile.invite_messages.slot')" prop="slot" sortable="custom" width="70") + el-table-column(:label="$t('table.profile.invite_messages.message')" prop="message") + el-table-column(:label="$t('table.profile.invite_messages.cool_down')" prop="updatedAt" sortable="custom" width="110" align="right") + template(v-once #default="scope") + countdown-timer(:datetime="scope.row.updatedAt" :hours="1") + el-table-column(:label="$t('table.profile.invite_messages.action')" width="70" align="right") + template(v-once #default="scope") + el-button(type="text" icon="el-icon-edit" size="mini" @click="showEditAndSendInviteResponseDialog('response', scope.row)") + template(#footer) + el-button(type="small" @click="cancelSendInviteResponse") {{ $t('dialog.invite_response_message.cancel') }} + el-button(type="small" @click="API.refreshInviteMessageTableData('response')") {{ $t('dialog.invite_response_message.refresh') }} + + //- dialog Table: Send Invite Request Response Message + el-dialog.x-dialog(:before-close="beforeDialogClose" @mousedown.native="dialogMouseDown" @mouseup.native="dialogMouseUp" ref="sendInviteRequestResponseDialog" :visible.sync="sendInviteRequestResponseDialogVisible" :title="$t('dialog.invite_request_response_message.header')" width="800px") + template(v-if="API.currentUser.$isVRCPlus") + input.inviteImageUploadButton(type="file" accept="image/png,image/jpg,image/jpeg,image/webp,image/bmp,image/gif" @change="inviteImageUpload") + data-tables(v-if="sendInviteRequestResponseDialogVisible" v-bind="inviteRequestResponseMessageTable" @row-click="showSendInviteResponseConfirmDialog" style="margin-top:10px;cursor:pointer") + el-table-column(:label="$t('table.profile.invite_messages.slot')" prop="slot" sortable="custom" width="70") + el-table-column(:label="$t('table.profile.invite_messages.message')" prop="message") + el-table-column(:label="$t('table.profile.invite_messages.cool_down')" prop="updatedAt" sortable="custom" width="110" align="right") + template(v-once #default="scope") + countdown-timer(:datetime="scope.row.updatedAt" :hours="1") + el-table-column(:label="$t('table.profile.invite_messages.action')" width="70" align="right") + template(v-once #default="scope") + el-button(type="text" icon="el-icon-edit" size="mini" @click="showEditAndSendInviteResponseDialog('requestResponse', scope.row)") + template(#footer) + el-button(type="small" @click="cancelSendInviteRequestResponse") {{ $t('dialog.invite_request_response_message.cancel') }} + el-button(type="small" @click="API.refreshInviteMessageTableData('requestResponse')") {{ $t('dialog.invite_request_response_message.refresh') }} + + //- dialog: Send Invite Response Message Confirm + el-dialog.x-dialog(:before-close="beforeDialogClose" @mousedown.native="dialogMouseDown" @mouseup.native="dialogMouseUp" ref="sendInviteResponseConfirmDialog" :visible.sync="sendInviteResponseConfirmDialog.visible" :title="$t('dialog.invite_response_message.header')" width="400px") + div(style='font-size:12px') + span {{ $t('dialog.invite_response_message.confirmation') }} + template(#footer) + el-button(type="small" @click="cancelInviteResponseConfirm") {{ $t('dialog.invite_response_message.cancel') }} + el-button(type="primary" size="small" @click="sendInviteResponseConfirm") {{ $t('dialog.invite_response_message.confirm') }} + + //- dialog Table: Send Invite Message + el-dialog.x-dialog(:before-close="beforeDialogClose" @mousedown.native="dialogMouseDown" @mouseup.native="dialogMouseUp" ref="sendInviteDialog" :visible.sync="sendInviteDialogVisible" :title="$t('dialog.invite_message.header')" width="800px") + template(v-if="API.currentUser.$isVRCPlus") + //- template(v-if="gallerySelectDialog.selectedFileId") + //- div(style="display:inline-block;flex:none;margin-right:5px") + //- el-popover(placement="right" width="500px" trigger="click") + //- img.x-link(slot="reference" v-lazy="gallerySelectDialog.selectedImageUrl" style="flex:none;width:60px;height:60px;border-radius:4px;object-fit:cover") + //- img.x-link(v-lazy="gallerySelectDialog.selectedImageUrl" style="height:500px" @click="showFullscreenImageDialog(gallerySelectDialog.selectedImageUrl)") + //- el-button(size="mini" @click="clearImageGallerySelect" style="vertical-align:top") {{ $t('dialog.invite_message.clear_selected_image') }} + //- template(v-else) + //- el-button(size="mini" @click="showGallerySelectDialog" style="margin-right:5px") {{ $t('dialog.invite_message.select_image') }} + input.inviteImageUploadButton(type="file" accept="image/png,image/jpg,image/jpeg,image/webp,image/bmp,image/gif" @change="inviteImageUpload") + data-tables(v-if="sendInviteDialogVisible" v-bind="inviteMessageTable" @row-click="showSendInviteConfirmDialog" style="margin-top:10px;cursor:pointer") + el-table-column(:label="$t('table.profile.invite_messages.slot')" prop="slot" sortable="custom" width="70") + el-table-column(:label="$t('table.profile.invite_messages.message')" prop="message") + el-table-column(:label="$t('table.profile.invite_messages.cool_down')" prop="updatedAt" sortable="custom" width="110" align="right") + template(v-once #default="scope") + countdown-timer(:datetime="scope.row.updatedAt" :hours="1") + el-table-column(:label="$t('table.profile.invite_messages.action')" width="70" align="right") + template(v-once #default="scope") + el-button(type="text" icon="el-icon-edit" size="mini" @click="showEditAndSendInviteDialog('message', scope.row)") + template(#footer) + el-button(type="small" @click="cancelSendInvite") {{ $t('dialog.invite_message.cancel') }} + el-button(type="small" @click="API.refreshInviteMessageTableData('message')") {{ $t('dialog.invite_message.refresh') }} + + //- dialog Table: Send Invite Request Message + el-dialog.x-dialog(:before-close="beforeDialogClose" @mousedown.native="dialogMouseDown" @mouseup.native="dialogMouseUp" ref="sendInviteRequestDialog" :visible.sync="sendInviteRequestDialogVisible" :title="$t('dialog.invite_request_message.header')" width="800px") + template(v-if="API.currentUser.$isVRCPlus") + input.inviteImageUploadButton(type="file" accept="image/png,image/jpg,image/jpeg,image/webp,image/bmp,image/gif" @change="inviteImageUpload") + data-tables(v-if="sendInviteRequestDialogVisible" v-bind="inviteRequestMessageTable" @row-click="showSendInviteConfirmDialog" style="margin-top:10px;cursor:pointer") + el-table-column(:label="$t('table.profile.invite_messages.slot')" prop="slot" sortable="custom" width="70") + el-table-column(:label="$t('table.profile.invite_messages.message')" prop="message") + el-table-column(:label="$t('table.profile.invite_messages.cool_down')" prop="updatedAt" sortable="custom" width="110" align="right") + template(v-once #default="scope") + countdown-timer(:datetime="scope.row.updatedAt" :hours="1") + el-table-column(:label="$t('table.profile.invite_messages.action')" width="70" align="right") + template(v-once #default="scope") + el-button(type="text" icon="el-icon-edit" size="mini" @click="showEditAndSendInviteDialog('request', scope.row)") + template(#footer) + el-button(type="small" @click="cancelSendInviteRequest") {{ $t('dialog.invite_request_message.cancel') }} + el-button(type="small" @click="API.refreshInviteMessageTableData('request')") {{ $t('dialog.invite_request_message.refresh') }} + + //- dialog: Send Invite Message Confirm + el-dialog.x-dialog(:before-close="beforeDialogClose" @mousedown.native="dialogMouseDown" @mouseup.native="dialogMouseUp" ref="sendInviteConfirmDialog" :visible.sync="sendInviteConfirmDialog.visible" :title="$t('dialog.invite_message.header')" width="400px") + div(style='font-size:12px') + span {{ $t('dialog.invite_message.confirmation') }} + template(#footer) + el-button(type="small" @click="cancelInviteConfirm") {{ $t('dialog.invite_message.cancel') }} + el-button(type="primary" size="small" @click="sendInviteConfirm") {{ $t('dialog.invite_message.confirm') }} + + //- dialog: Edit And Send Invite Message + el-dialog.x-dialog(:before-close="beforeDialogClose" @mousedown.native="dialogMouseDown" @mouseup.native="dialogMouseUp" ref="editAndSendInviteDialog" :visible.sync="editAndSendInviteDialog.visible" :title="$t('dialog.edit_send_invite_message.header')" width="400px") + div(style='font-size:12px') + span {{ $t('dialog.edit_send_invite_message.description') }} + el-input(type="textarea" v-model="editAndSendInviteDialog.newMessage" size="mini" maxlength="64" show-word-limit :autosize="{ minRows:2, maxRows:5 }" placeholder="" style="margin-top:10px") + template(#footer) + el-button(type="small" @click="cancelEditAndSendInvite") {{ $t('dialog.edit_send_invite_message.cancel') }} + el-button(type="primary" size="small" @click="saveEditAndSendInvite") {{ $t('dialog.edit_send_invite_message.send') }} diff --git a/html/src/mixins/dialogs/launch.pug b/html/src/mixins/dialogs/launch.pug new file mode 100644 index 000000000..0aa6c66f3 --- /dev/null +++ b/html/src/mixins/dialogs/launch.pug @@ -0,0 +1,38 @@ +mixin launch() + //- dialog: launch + el-dialog.x-dialog(:before-close="beforeDialogClose" @mousedown.native="dialogMouseDown" @mouseup.native="dialogMouseUp" ref="launchDialog" :visible.sync="launchDialog.visible" :title="$t('dialog.launch.header')" width="450px") + el-form(:model="launchDialog" label-width="80px") + el-form-item(:label="$t('dialog.launch.url')") + el-input(v-model="launchDialog.url" size="mini" @click.native="$event.target.tagName === 'INPUT' && $event.target.select()" style="width:260px") + el-tooltip(placement="right" :content="$t('dialog.launch.copy_tooltip')" :disabled="hideTooltips") + el-button(@click="copyInstanceMessage(launchDialog.url)" size="mini" icon="el-icon-s-order" style="margin-right:5px" circle) + el-form-item(v-if="launchDialog.shortUrl" :label="$t('dialog.launch.short_url')") + el-tooltip(placement="top" style="margin-left:5px" :content="$t('dialog.launch.short_url_notice')") + i.el-icon-warning + el-input(v-model="launchDialog.shortUrl" size="mini" @click.native="$event.target.tagName === 'INPUT' && $event.target.select()" style="width:241px") + el-tooltip(placement="right" :content="$t('dialog.launch.copy_tooltip')" :disabled="hideTooltips") + el-button(@click="copyInstanceMessage(launchDialog.shortUrl)" size="mini" icon="el-icon-s-order" style="margin-right:5px" circle) + el-form-item(:label="$t('dialog.launch.location')") + el-input(v-model="launchDialog.location" size="mini" @click.native="$event.target.tagName === 'INPUT' && $event.target.select()" style="width:260px") + el-tooltip(placement="right" :content="$t('dialog.launch.copy_tooltip')" :disabled="hideTooltips") + el-button(@click="copyInstanceMessage(launchDialog.location)" size="mini" icon="el-icon-s-order" style="margin-right:5px" circle) + template(#footer) + el-checkbox(v-model="launchDialog.desktop" @change="saveLaunchDialog" style="float:left;margin-top:5px") {{ $t('dialog.launch.start_as_desktop') }} + el-button(size="small" @click="showPreviousInstanceInfoDialog(launchDialog.location)") {{ $t('dialog.launch.info') }} + el-button(size="small" @click="showInviteDialog(launchDialog.location)" :disabled="!checkCanInvite(launchDialog.location)") {{ $t('dialog.launch.invite') }} + el-button(type="primary" size="small" @click="launchGame(launchDialog.location, launchDialog.shortName, launchDialog.desktop)" :disabled="!launchDialog.secureOrShortName") {{ $t('dialog.launch.launch') }} + + //- dialog: launch options + el-dialog.x-dialog(:before-close="beforeDialogClose" @mousedown.native="dialogMouseDown" @mouseup.native="dialogMouseUp" ref="launchOptionsDialog" :visible.sync="launchOptionsDialog.visible" :title="$t('dialog.launch_options.header')" width="600px") + div(style="font-size:12px") + | {{ $t('dialog.launch_options.description') }} #[br] + | {{ $t('dialog.launch_options.example') }} #[el-tag(size="mini") --fps=144] + el-input(type="textarea" v-model="launchOptionsDialog.launchArguments" size="mini" show-word-limit :autosize="{ minRows:2, maxRows:5 }" placeholder="" style="margin-top:10px") + div(style="font-size:12px;margin-top:10px") + | {{ $t('dialog.launch_options.path_override') }} + el-input(type="textarea" v-model="launchOptionsDialog.vrcLaunchPathOverride" placeholder="C:\\Program Files (x86)\\Steam\\steamapps\\common\\VRChat" :rows="1" style="display:block;margin-top:10px") + template(#footer) + div(style="display:flex") + el-button(size="small" @click="openExternalLink('https://docs.vrchat.com/docs/launch-options')") {{ $t('dialog.launch_options.vrchat_docs') }} + el-button(size="small" @click="openExternalLink('https://docs.unity3d.com/Manual/CommandLineArguments.html')") {{ $t('dialog.launch_options.unity_manual') }} + el-button(type="primary" size="small" :disabled="launchOptionsDialog.loading" @click="updateLaunchOptions" style="margin-left:auto") {{ $t('dialog.launch_options.save') }} diff --git a/html/src/mixins/dialogs/newInstance.pug b/html/src/mixins/dialogs/newInstance.pug new file mode 100644 index 000000000..f38be40b6 --- /dev/null +++ b/html/src/mixins/dialogs/newInstance.pug @@ -0,0 +1,139 @@ +mixin newInstance() + el-dialog.x-dialog(:before-close="beforeDialogClose" @mousedown.native="dialogMouseDown" @mouseup.native="dialogMouseUp" ref="newInstanceDialog" :visible.sync="newInstanceDialog.visible" :title="$t('dialog.new_instance.header')" width="650px") + el-tabs(type="card" v-model="newInstanceDialog.selectedTab") + el-tab-pane(:label="$t('dialog.new_instance.normal')") + el-form(v-if="newInstanceDialog.visible" :model="newInstanceDialog" label-width="150px") + el-form-item(:label="$t('dialog.new_instance.access_type')") + el-radio-group(v-model="newInstanceDialog.accessType" size="mini" @change="buildInstance") + el-radio-button(label="public") {{ $t('dialog.new_instance.access_type_public') }} + el-radio-button(label="group") {{ $t('dialog.new_instance.access_type_group') }} + el-radio-button(label="friends+") {{ $t('dialog.new_instance.access_type_friend_plus') }} + el-radio-button(label="friends") {{ $t('dialog.new_instance.access_type_friend') }} + el-radio-button(label="invite+") {{ $t('dialog.new_instance.access_type_invite_plus') }} + el-radio-button(label="invite") {{ $t('dialog.new_instance.access_type_invite') }} + el-form-item(:label="$t('dialog.new_instance.group_access_type')" v-if="newInstanceDialog.accessType === 'group'") + el-radio-group(v-model="newInstanceDialog.groupAccessType" size="mini" @change="buildInstance") + el-radio-button(label="members" :disabled="!hasGroupPermission(newInstanceDialog.groupRef, 'group-instance-open-create')") {{ $t('dialog.new_instance.group_access_type_members') }} + el-radio-button(label="plus" :disabled="!hasGroupPermission(newInstanceDialog.groupRef, 'group-instance-plus-create')") {{ $t('dialog.new_instance.group_access_type_plus') }} + el-radio-button(label="public" :disabled="!hasGroupPermission(newInstanceDialog.groupRef, 'group-instance-public-create') || newInstanceDialog.groupRef.privacy === 'private'") {{ $t('dialog.new_instance.group_access_type_public') }} + el-form-item(:label="$t('dialog.new_instance.region')") + el-radio-group(v-model="newInstanceDialog.region" size="mini" @change="buildInstance") + el-radio-button(label="US West") {{ $t('dialog.new_instance.region_usw') }} + el-radio-button(label="US East") {{ $t('dialog.new_instance.region_use') }} + el-radio-button(label="Europe") {{ $t('dialog.new_instance.region_eu') }} + el-radio-button(label="Japan") {{ $t('dialog.new_instance.region_jp') }} + el-form-item(:label="$t('dialog.new_instance.queueEnabled')" v-if="newInstanceDialog.accessType === 'group'") + el-checkbox(v-model="newInstanceDialog.queueEnabled" @change="buildInstance") + el-form-item(:label="$t('dialog.new_instance.world_id')") + el-input(v-model="newInstanceDialog.worldId" size="mini" @click.native="$event.target.tagName === 'INPUT' && $event.target.select()" @change="buildInstance") + el-form-item(:label="$t('dialog.new_instance.group_id')" v-if="newInstanceDialog.accessType === 'group'") + el-select(v-model="newInstanceDialog.groupId" clearable :placeholder="$t('dialog.new_instance.group_placeholder')" filterable style="width:100%" @change="buildInstance") + el-option-group(:label="$t('dialog.new_instance.group_placeholder')") + el-option.x-friend-item(v-if="group && (hasGroupPermission(group, 'group-instance-public-create') || hasGroupPermission(group, 'group-instance-plus-create') || hasGroupPermission(group, 'group-instance-open-create'))" v-for="group in API.currentUserGroups.values()" :key="group.id" :label="group.name" :value="group.id" style="height:auto;width:478px") + .avatar + img(v-lazy="group.iconUrl") + .detail + span.name(v-text="group.name") + el-form-item(:label="$t('dialog.new_instance.roles')" v-if="newInstanceDialog.accessType === 'group' && newInstanceDialog.groupAccessType === 'members'") + el-select(v-model="newInstanceDialog.roleIds" multiple clearable :placeholder="$t('dialog.new_instance.role_placeholder')" style="width:100%" @change="buildInstance") + el-option-group(:label="$t('dialog.new_instance.role_placeholder')") + el-option.x-friend-item(v-for="role in newInstanceDialog.selectedGroupRoles" :key="role.id" :label="role.name" :value="role.id" style="height:auto;width:478px") + .detail + span.name(v-text="role.name") + template(v-if="newInstanceDialog.instanceCreated") + el-form-item(:label="$t('dialog.new_instance.location')") + el-input(v-model="newInstanceDialog.location" size="mini" readonly @click.native="$event.target.tagName === 'INPUT' && $event.target.select()") + el-form-item(:label="$t('dialog.new_instance.url')") + el-input(v-model="newInstanceDialog.url" size="mini" readonly) + el-tab-pane(:label="$t('dialog.new_instance.legacy')") + el-form(v-if="newInstanceDialog.visible" :model="newInstanceDialog" label-width="150px") + el-form-item(:label="$t('dialog.new_instance.access_type')") + el-radio-group(v-model="newInstanceDialog.accessType" size="mini" @change="buildLegacyInstance") + el-radio-button(label="public") {{ $t('dialog.new_instance.access_type_public') }} + el-radio-button(label="group") {{ $t('dialog.new_instance.access_type_group') }} + el-radio-button(label="friends+") {{ $t('dialog.new_instance.access_type_friend_plus') }} + el-radio-button(label="friends") {{ $t('dialog.new_instance.access_type_friend') }} + el-radio-button(label="invite+") {{ $t('dialog.new_instance.access_type_invite_plus') }} + el-radio-button(label="invite") {{ $t('dialog.new_instance.access_type_invite') }} + el-form-item(:label="$t('dialog.new_instance.group_access_type')" v-if="newInstanceDialog.accessType === 'group'") + el-radio-group(v-model="newInstanceDialog.groupAccessType" size="mini" @change="buildLegacyInstance") + el-radio-button(label="members") {{ $t('dialog.new_instance.group_access_type_members') }} + el-radio-button(label="plus") {{ $t('dialog.new_instance.group_access_type_plus') }} + el-radio-button(label="public") {{ $t('dialog.new_instance.group_access_type_public') }} + //- el-form-item(label="Strict" v-if="newInstanceDialog.accessType === 'friends' || newInstanceDialog.accessType === 'invite'") + //- el-checkbox(v-model="newInstanceDialog.strict") Prevent non friends joining via URL/Instance ID + el-form-item(:label="$t('dialog.new_instance.region')") + el-radio-group(v-model="newInstanceDialog.region" size="mini" @change="buildLegacyInstance") + el-radio-button(label="US West") {{ $t('dialog.new_instance.region_usw') }} + el-radio-button(label="US East") {{ $t('dialog.new_instance.region_use') }} + el-radio-button(label="Europe") {{ $t('dialog.new_instance.region_eu') }} + el-radio-button(label="Japan") {{ $t('dialog.new_instance.region_jp') }} + el-form-item(:label="$t('dialog.new_instance.world_id')") + el-input(v-model="newInstanceDialog.worldId" size="mini" @click.native="$event.target.tagName === 'INPUT' && $event.target.select()" @change="buildLegacyInstance") + el-form-item(:label="$t('dialog.new_instance.instance_id')") + el-input(v-model="newInstanceDialog.instanceName" :placeholder="$t('dialog.new_instance.instance_id_placeholder')" size="mini" @change="buildLegacyInstance") + el-form-item(:label="$t('dialog.new_instance.instance_creator')" v-if="newInstanceDialog.accessType !== 'public' && newInstanceDialog.accessType !== 'group'") + el-select(v-model="newInstanceDialog.userId" clearable :placeholder="$t('dialog.new_instance.instance_creator_placeholder')" filterable style="width:100%" @change="buildLegacyInstance") + el-option-group(v-if="API.currentUser" :label="$t('side_panel.me')") + el-option.x-friend-item(:label="API.currentUser.displayName" :value="API.currentUser.id" style="height:auto") + .avatar(:class="userStatusClass(API.currentUser)") + img(v-lazy="userImage(API.currentUser)") + .detail + span.name(v-text="API.currentUser.displayName") + el-option-group(v-if="vipFriends.length" :label="$t('side_panel.favorite')") + el-option.x-friend-item(v-for="friend in vipFriends" :key="friend.id" :label="friend.name" :value="friend.id" style="height:auto") + template(v-if="friend.ref") + .avatar(:class="userStatusClass(friend.ref)") + img(v-lazy="userImage(friend.ref)") + .detail + span.name(v-text="friend.ref.displayName" :style="{'color':friend.ref.$userColour}") + span(v-else v-text="friend.id") + el-option-group(v-if="onlineFriends.length" :label="$t('side_panel.online')") + el-option.x-friend-item(v-for="friend in onlineFriends" :key="friend.id" :label="friend.name" :value="friend.id" style="height:auto") + template(v-if="friend.ref") + .avatar(:class="userStatusClass(friend.ref)") + img(v-lazy="userImage(friend.ref)") + .detail + span.name(v-text="friend.ref.displayName" :style="{'color':friend.ref.$userColour}") + span(v-else v-text="friend.id") + el-option-group(v-if="activeFriends.length" :label="$t('side_panel.active')") + el-option.x-friend-item(v-for="friend in activeFriends" :key="friend.id" :label="friend.name" :value="friend.id" style="height:auto") + template(v-if="friend.ref") + .avatar + img(v-lazy="userImage(friend.ref)") + .detail + span.name(v-text="friend.ref.displayName" :style="{'color':friend.ref.$userColour}") + span(v-else v-text="friend.id") + el-option-group(v-if="offlineFriends.length" :label="$t('side_panel.offline')") + el-option.x-friend-item(v-for="friend in offlineFriends" :key="friend.id" :label="friend.name" :value="friend.id" style="height:auto") + template(v-if="friend.ref") + .avatar + img(v-lazy="userImage(friend.ref)") + .detail + span.name(v-text="friend.ref.displayName" :style="{'color':friend.ref.$userColour}") + span(v-else v-text="friend.id") + el-form-item(:label="$t('dialog.new_instance.group_id')" v-if="newInstanceDialog.accessType === 'group'") + el-select(v-model="newInstanceDialog.groupId" clearable :placeholder="$t('dialog.new_instance.group_placeholder')" filterable style="width:100%" @change="buildLegacyInstance") + el-option-group(:label="$t('dialog.new_instance.group_placeholder')") + el-option.x-friend-item(v-if="group" v-for="group in API.currentUserGroups.values()" :key="group.id" :label="group.name" :value="group.id" style="height:auto;width:478px") + .avatar + img(v-lazy="group.iconUrl") + .detail + span.name(v-text="group.name") + el-form-item(:label="$t('dialog.new_instance.location')") + el-input(v-model="newInstanceDialog.location" size="mini" readonly @click.native="$event.target.tagName === 'INPUT' && $event.target.select()") + el-form-item(:label="$t('dialog.new_instance.url')") + el-input(v-model="newInstanceDialog.url" size="mini" readonly) + template(#footer v-if="newInstanceDialog.selectedTab === '0'") + template(v-if="newInstanceDialog.instanceCreated") + el-button(size="small" @click="copyInstanceUrl(newInstanceDialog.location)") {{ $t('dialog.new_instance.copy_url') }} + el-button(size="small" @click="selfInvite(newInstanceDialog.location)") {{ $t('dialog.new_instance.self_invite') }} + el-button(size="small" @click="showInviteDialog(newInstanceDialog.location)" :disabled="(newInstanceDialog.accessType === 'friends' || newInstanceDialog.accessType === 'invite') && newInstanceDialog.userId !== API.currentUser.id") {{ $t('dialog.new_instance.invite') }} + el-button(type="primary" size="small" @click="showLaunchDialog(newInstanceDialog.location, newInstanceDialog.shortName)") {{ $t('dialog.new_instance.launch') }} + template(v-else) + el-button(type="primary" size="small" @click="createNewInstance()") {{ $t('dialog.new_instance.create_instance') }} + template(#footer v-else-if="newInstanceDialog.selectedTab === '1'") + el-button(size="small" @click="copyInstanceUrl(newInstanceDialog.location)") {{ $t('dialog.new_instance.copy_url') }} + el-button(size="small" @click="selfInvite(newInstanceDialog.location)") {{ $t('dialog.new_instance.self_invite') }} + el-button(size="small" @click="showInviteDialog(newInstanceDialog.location)" :disabled="(newInstanceDialog.accessType === 'friends' || newInstanceDialog.accessType === 'invite') && newInstanceDialog.userId !== API.currentUser.id") {{ $t('dialog.new_instance.invite') }} + el-button(type="primary" size="small" @click="showLaunchDialog(newInstanceDialog.location, newInstanceDialog.shortName)") {{ $t('dialog.new_instance.launch') }} diff --git a/html/src/mixins/dialogs/openSourceSoftwareNotice.pug b/html/src/mixins/dialogs/openSourceSoftwareNotice.pug new file mode 100644 index 000000000..be7a46efa --- /dev/null +++ b/html/src/mixins/dialogs/openSourceSoftwareNotice.pug @@ -0,0 +1,394 @@ +mixin openSourceSoftwareNotice() + //- dialog: open source software notice + el-dialog.x-dialog(:before-close="beforeDialogClose" @mousedown.native="dialogMouseDown" @mouseup.native="dialogMouseUp" :visible.sync="ossDialog" :title="$t('dialog.open_source.header')" width="650px") + div(v-if="ossDialog" style="height:350px;overflow:hidden scroll;word-break:break-all") + div + span {{ $t('dialog.open_source.description') }} + div(style="margin-top:15px") + p(style="font-weight:bold") animate.css + pre(style="font-size:12px;white-space:pre-line"). + The MIT License (MIT) + + Copyright (c) 2019 Daniel Eden + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in all + copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + SOFTWARE. + div(style="margin-top:15px") + p(style="font-weight:bold") CefSharp + pre(style="font-size:12px;white-space:pre-line"). + // Copyright © The CefSharp Authors. All rights reserved. + // + // Redistribution and use in source and binary forms, with or without + // modification, are permitted provided that the following conditions are + // met: + // + // * Redistributions of source code must retain the above copyright + // notice, this list of conditions and the following disclaimer. + // + // * Redistributions in binary form must reproduce the above + // copyright notice, this list of conditions and the following disclaimer + // in the documentation and/or other materials provided with the + // distribution. + // + // * Neither the name of Google Inc. nor the name Chromium Embedded + // Framework nor the name CefSharp nor the names of its contributors + // may be used to endorse or promote products derived from this software + // without specific prior written permission. + // + // THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + // "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + // LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + // A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + // OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + // SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + // LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + // DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + // THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + // (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + // OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + div(style="margin-top:15px") + p(style="font-weight:bold") DiscordRichPresence + pre(style="font-size:12px;white-space:pre-line"). + MIT License + + Copyright (c) 2018 Lachee + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in all + copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + SOFTWARE. + div(style="margin-top:15px") + p(style="font-weight:bold") element + pre(style="font-size:12px;white-space:pre-line"). + The MIT License (MIT) + + Copyright (c) 2016-present ElemeFE + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in all + copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + SOFTWARE. + div(style="margin-top:15px") + p(style="font-weight:bold") librsync.net + pre(style="font-size:12px;white-space:pre-line"). + The MIT License (MIT) + + Copyright (c) 2015 Brad Dodson + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in all + copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + SOFTWARE. + div(style="margin-top:15px") + p(style="font-weight:bold") Newtonsoft.Json + pre(style="font-size:12px;white-space:pre-line"). + The MIT License (MIT) + + Copyright (c) 2007 James Newton-King + + Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + div(style="margin-top:15px") + p(style="font-weight:bold") normalize + pre(style="font-size:12px;white-space:pre-line"). + The MIT License (MIT) + + Copyright © Nicolas Gallagher and Jonathan Neal + + Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + div(style="margin-top:15px") + p(style="font-weight:bold") noty + pre(style="font-size:12px;white-space:pre-line"). + Copyright (c) 2012 Nedim Arabacı + + Permission is hereby granted, free of charge, to any person obtaining + a copy of this software and associated documentation files (the + "Software"), to deal in the Software without restriction, including + without limitation the rights to use, copy, modify, merge, publish, + distribute, sublicense, and/or sell copies of the Software, and to + permit persons to whom the Software is furnished to do so, subject to + the following conditions: + + The above copyright notice and this permission notice shall be + included in all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE + LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION + OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + div(style="margin-top:15px") + p(style="font-weight:bold") OpenVR SDK + pre(style="font-size:12px;white-space:pre-line"). + Copyright (c) 2015, Valve Corporation + All rights reserved. + + Redistribution and use in source and binary forms, with or without modification, + are permitted provided that the following conditions are met: + + 1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + + 2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation and/or + other materials provided with the distribution. + + 3. Neither the name of the copyright holder nor the names of its contributors + may be used to endorse or promote products derived from this software without + specific prior written permission. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR + ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON + ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + div(style="margin-top:15px") + p(style="font-weight:bold") Twemoji + pre(style="font-size:12px;white-space:pre-line"). + MIT License + + Copyright (c) 2021 Twitter + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in all + copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + SOFTWARE. + div(style="margin-top:15px") + p(style="font-weight:bold") SharpDX + pre(style="font-size:12px;white-space:pre-line"). + Copyright (c) 2010-2014 SharpDX - Alexandre Mutel + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in + all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + THE SOFTWARE. + div(style="margin-top:15px") + p(style="font-weight:bold") vue + pre(style="font-size:12px;white-space:pre-line"). + The MIT License (MIT) + + Copyright (c) 2013-present, Yuxi (Evan) You + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in + all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + THE SOFTWARE. + div(style="margin-top:15px") + p(style="font-weight:bold") vue-data-tables + pre(style="font-size:12px;white-space:pre-line"). + The MIT License (MIT) + + Copyright (c) 2018 Leon Zhang + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in all + copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + SOFTWARE. + div(style="margin-top:15px") + p(style="font-weight:bold") vue-lazyload + pre(style="font-size:12px;white-space:pre-line"). + The MIT License (MIT) + + Copyright (c) 2016 Awe + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in all + copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + SOFTWARE. + + div(style="margin-top:15px") + p(style="font-weight:bold") Encode Sans Font (from Dark Vanilla) + pre(style="font-size:12px;white-space:pre-line"). + SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 + Copyright (c) 2020 June 20, Impallari Type, Andres Torresi, Jacques Le Bailly + (https://fonts.google.com/specimen/Encode+Sans), + with Reserved Font Name: Encode Sans. + + PREAMBLE: + The goals of the Open Font License (OFL) are to stimulate worldwide development + of collaborative font projects, to support the font creation efforts of academic + and linguistic communities, and to provide a free and open framework in which + fonts may be shared and improved in partnership with others. + + The OFL allows the licensed fonts to be used, studied, modified and redistributed + freely as long as they are not sold by themselves. The fonts, including any + derivative works, can be bundled, embedded, redistributed and/or sold with any + software provided that any reserved names are not used by derivative works. + The fonts and derivatives, however, cannot be released under any other type of + license. The requirement for fonts to remain under this license does not apply + to any document created using the fonts or their derivatives. + + PERMISSION & CONDITIONS + Permission is hereby granted, free of charge, to any person obtaining a copy of + the Font Software, to use, study, copy, merge, embed, modify, redistribute, and + sell modified and unmodified copies of the Font Software, subject to the + following conditions: + + 1. Neither the Font Software nor any of its individual components, in Original or + Modified Versions, may be sold by itself. + + 2. Original or Modified Versions of the Font Software may be bundled, redistributed + and/or sold with any software, provided that each copy contains the above copyright + notice and this license. These can be included either as stand-alone text files, + human-readable headers or in the appropriate machine-readable metadata fields within + text or binary files as long as those fields can be easily viewed by the user. + + 3. No Modified Version of the Font Software may use the Reserved Font Name(s) unless + explicit written permission is granted by the corresponding Copyright Holder. This + restriction only applies to the primary font name as presented to the users. + + 4. The name(s) of the Copyright Holder(s) or the Author(s) of the Font Software shall + not be used to promote, endorse or advertise any Modified Version, except to + acknowledge the contribution(s) of the Copyright Holder(s) and the Author(s) or with + their explicit written permission. + + 5. The Font Software, modified or unmodified, in part or in whole, must be distributed + entirely under this license, and must not be distributed under any other license. + The requirement for fonts to remain under this license does not apply to any document + created using the Font Software. + + TERMINATION + This license becomes null and void if any of the above conditions are not met. + + DISCLAIMER + THE FONT SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF MERCHANTABILITY, FITNESS + FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT OF COPYRIGHT, PATENT, TRADEMARK, OR + OTHER RIGHT. IN NO EVENT SHALL THE COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, + DAMAGES OR OTHER LIABILITY, INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, + OR CONSEQUENTIAL DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, + ARISING FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM OTHER + DEALINGS IN THE FONT SOFTWARE. diff --git a/html/src/mixins/dialogs/previousInstances.pug b/html/src/mixins/dialogs/previousInstances.pug new file mode 100644 index 000000000..65e51e10a --- /dev/null +++ b/html/src/mixins/dialogs/previousInstances.pug @@ -0,0 +1,73 @@ +mixin previousInstances() + //- dialog Table: Previous Instances User + el-dialog.x-dialog(:before-close="beforeDialogClose" @mousedown.native="dialogMouseDown" @mouseup.native="dialogMouseUp" ref="previousInstancesUserDialog" :visible.sync="previousInstancesUserDialog.visible" :title="$t('dialog.previous_instances.header')" width="1000px") + span(v-text="previousInstancesUserDialog.userRef.displayName" style="font-size:14px") + el-input(v-model="previousInstancesUserDialogTable.filters[0].value" :placeholder="$t('dialog.previous_instances.search_placeholder')" style="display:block;width:150px;margin-top:15px") + data-tables(v-if="previousInstancesUserDialog.visible" v-bind="previousInstancesUserDialogTable" v-loading="previousInstancesUserDialog.loading" style="margin-top:10px") + el-table-column(:label="$t('table.previous_instances.date')" prop="created_at" sortable width="170") + template(v-once #default="scope") + span {{ scope.row.created_at | formatDate('long') }} + el-table-column(:label="$t('table.previous_instances.world')" prop="name" sortable) + template(v-once #default="scope") + location(:location="scope.row.location" :hint="scope.row.worldName" :grouphint="scope.row.groupName") + el-table-column(:label="$t('table.previous_instances.instance_creator')" prop="location" width="170") + template(v-once #default="scope") + display-name(:userid="scope.row.$location.userId" :location="scope.row.$location.tag" :key="previousInstancesUserDialog.forceUpdate") + el-table-column(:label="$t('table.previous_instances.time')" prop="time" width="100" sortable) + template(v-once #default="scope") + span(v-text="scope.row.timer") + el-table-column(:label="$t('table.previous_instances.action')" width="90" align="right") + template(v-once #default="scope") + el-button(type="text" icon="el-icon-info" size="mini" @click="showLaunchDialog(scope.row.location)") + el-button(type="text" icon="el-icon-tickets" size="mini" @click="showPreviousInstanceInfoDialog(scope.row.location)") + el-button(type="text" icon="el-icon-close" size="mini" @click="confirmDeleteGameLogUserInstance(scope.row)") + + //- dialog Table: Previous Instances World + el-dialog.x-dialog(:before-close="beforeDialogClose" @mousedown.native="dialogMouseDown" @mouseup.native="dialogMouseUp" ref="previousInstancesWorldDialog" :visible.sync="previousInstancesWorldDialog.visible" :title="$t('dialog.previous_instances.header')" width="1000px") + span(v-text="previousInstancesWorldDialog.worldRef.name" style="font-size:14px") + el-input(v-model="previousInstancesWorldDialogTable.filters[0].value" :placeholder="$t('dialog.previous_instances.search_placeholder')" style="display:block;width:150px;margin-top:15px") + data-tables(v-if="previousInstancesWorldDialog.visible" v-bind="previousInstancesWorldDialogTable" v-loading="previousInstancesWorldDialog.loading" style="margin-top:10px") + el-table-column(:label="$t('table.previous_instances.date')" prop="created_at" sortable width="170") + template(v-once #default="scope") + span {{ scope.row.created_at | formatDate('long') }} + el-table-column(:label="$t('table.previous_instances.instance_name')" prop="name") + template(v-once #default="scope") + location-world(:locationobject="scope.row.$location" :grouphint="scope.row.groupName" :currentuserid="API.currentUser.id") + el-table-column(:label="$t('table.previous_instances.instance_creator')" prop="location") + template(v-once #default="scope") + display-name(:userid="scope.row.$location.userId" :location="scope.row.$location.tag" :key="previousInstancesWorldDialog.forceUpdate") + el-table-column(:label="$t('table.previous_instances.time')" prop="time" width="100" sortable) + template(v-once #default="scope") + span(v-text="scope.row.timer") + el-table-column(:label="$t('table.previous_instances.action')" width="90" align="right") + template(v-once #default="scope") + el-button(type="text" icon="el-icon-tickets" size="mini" @click="showPreviousInstanceInfoDialog(scope.row.location)") + el-button(type="text" icon="el-icon-close" size="mini" @click="confirmDeleteGameLogWorldInstance(scope.row)") + + //- dialog Table: Previous Instance Info + el-dialog.x-dialog(:before-close="beforeDialogClose" @mousedown.native="dialogMouseDown" @mouseup.native="dialogMouseUp" ref="previousInstanceInfoDialog" :visible.sync="previousInstanceInfoDialog.visible" :title="$t('dialog.previous_instances.info')" width="800px") + location(:location="previousInstanceInfoDialog.$location.tag" style="font-size:14px") + el-input(v-model="previousInstanceInfoDialogTable.filters[0].value" placeholder="Search" style="display:block;width:150px;margin-top:15px") + data-tables(v-if="previousInstanceInfoDialog.visible" v-bind="previousInstanceInfoDialogTable" v-loading="previousInstanceInfoDialog.loading" style="margin-top:10px") + el-table-column(:label="$t('table.previous_instances.date')" prop="created_at" sortable width="120") + template(v-once #default="scope") + el-tooltip(placement="left") + template(#content) + span {{ scope.row.created_at | formatDate('long') }} + span {{ scope.row.created_at | formatDate('short') }} + el-table-column(:label="$t('table.gameLog.icon')" prop="isFriend" width="70") + template(v-once #default="scope") + template(v-if="gameLogIsFriend(scope.row)") + el-tooltip(v-if="gameLogIsFavorite(scope.row)" placement="top" content="Favorite") + span ⭐ + el-tooltip(v-else placement="top" content="Friend") + span 💚 + el-table-column(:label="$t('table.previous_instances.display_name')" prop="displayName" sortable) + template(v-once #default="scope") + span.x-link(v-text="scope.row.displayName" @click="lookupUser(scope.row)") + el-table-column(:label="$t('table.previous_instances.time')" prop="time" width="90" sortable) + template(v-once #default="scope") + span(v-text="scope.row.timer") + el-table-column(:label="$t('table.previous_instances.count')" prop="count" width="90" sortable) + template(v-once #default="scope") + span(v-text="scope.row.count") diff --git a/html/src/mixins/dialogs/screenshotMetadata.pug b/html/src/mixins/dialogs/screenshotMetadata.pug new file mode 100644 index 000000000..37ca68d8f --- /dev/null +++ b/html/src/mixins/dialogs/screenshotMetadata.pug @@ -0,0 +1,51 @@ +mixin screenshotMetadata() + el-dialog.x-dialog(:before-close="beforeDialogClose" @mousedown.native="dialogMouseDown" @mouseup.native="dialogMouseUp" ref="screenshotMetadataDialog" :visible.sync="screenshotMetadataDialog.visible" :title="$t('dialog.screenshot_metadata.header')" width="1050px") + div(v-if="screenshotMetadataDialog.visible" v-loading="screenshotMetadataDialog.loading" @dragover.prevent @dragenter.prevent @drop="handleDrop" style="-webkit-app-region: drag") + span(style="margin-left:5px;color:#909399;font-family:monospace") {{ $t('dialog.screenshot_metadata.drag') }} + br + br + el-button(size="small" icon="el-icon-folder-opened" @click="AppApi.OpenScreenshotFileDialog()") {{ $t('dialog.screenshot_metadata.browse') }} + el-button(size="small" icon="el-icon-picture-outline" @click="getAndDisplayLastScreenshot()") {{ $t('dialog.screenshot_metadata.last_screenshot') }} + el-button(size="small" icon="el-icon-copy-document" @click="copyImageToClipboard(screenshotMetadataDialog.metadata.filePath)") {{ $t('dialog.screenshot_metadata.copy_image') }} + el-button(size="small" icon="el-icon-folder" @click="openImageFolder(screenshotMetadataDialog.metadata.filePath)") {{ $t('dialog.screenshot_metadata.open_folder') }} + el-button(v-if="API.currentUser.$isVRCPlus && screenshotMetadataDialog.metadata.filePath" size="small" icon="el-icon-upload2" @click="uploadScreenshotToGallery") {{ $t('dialog.screenshot_metadata.upload') }} + br + br + //- Search bar input + el-input(v-model="screenshotMetadataDialog.search" size="small" placeholder="Search" clearable style="width:200px" @input="screenshotMetadataSearch") + //- Search index/total label + template(v-if="screenshotMetadataDialog.searchIndex != null") + span(style="white-space:pre-wrap;font-size:12px;margin-left:10px") {{ (screenshotMetadataDialog.searchIndex + 1) + "/" + screenshotMetadataDialog.searchResults.length }} + //- Search type dropdown + el-select(v-model="screenshotMetadataDialog.searchType" size="small" placeholder="Search Type" style="width:150px;margin-left:10px" @change="screenshotMetadataSearch") + el-option(v-for="type in screenshotMetadataDialog.searchTypes" :key="type" :label="type" :value="type") + br + br + span(v-text="screenshotMetadataDialog.metadata.fileName") + br + span(v-if="screenshotMetadataDialog.metadata.dateTime" style="margin-right:5px") {{ screenshotMetadataDialog.metadata.dateTime | formatDate('long') }} + span(v-if="screenshotMetadataDialog.metadata.fileResolution" v-text="screenshotMetadataDialog.metadata.fileResolution" style="margin-right:5px") + el-tag(v-if="screenshotMetadataDialog.metadata.fileSize" type="info" effect="plain" size="mini" v-text="screenshotMetadataDialog.metadata.fileSize") + br + location(v-if="screenshotMetadataDialog.metadata.world" :location="screenshotMetadataDialog.metadata.world.instanceId" :hint="screenshotMetadataDialog.metadata.world.name") + br + span.x-link(v-if="screenshotMetadataDialog.metadata.author" v-text="screenshotMetadataDialog.metadata.author.displayName" @click="showUserDialog(screenshotMetadataDialog.metadata.author.id)" style="color:#909399;font-family:monospace") + br + el-carousel(ref="screenshotMetadataCarousel" :interval="0" initial-index="1" indicator-position="none" arrow="always" height="600px" style="margin-top:10px" @change="screenshotMetadataCarouselChange") + el-carousel-item + span(placement="top" width="700px" trigger="click") + img.x-link(slot="reference" v-lazy="screenshotMetadataDialog.metadata.previousFilePath" style="width:100%;height:100%;object-fit:contain") + el-carousel-item + span(placement="top" width="700px" trigger="click" @click="showFullscreenImageDialog(screenshotMetadataDialog.metadata.filePath)") + img.x-link(slot="reference" v-lazy="screenshotMetadataDialog.metadata.filePath" style="width:100%;height:100%;object-fit:contain") + el-carousel-item + span(placement="top" width="700px" trigger="click") + img.x-link(slot="reference" v-lazy="screenshotMetadataDialog.metadata.nextFilePath" style="width:100%;height:100%;object-fit:contain") + br + template(v-if="screenshotMetadataDialog.metadata.error") + pre(v-text="screenshotMetadataDialog.metadata.error" style="white-space:pre-wrap;font-size:12px") + br + span(v-for="user in screenshotMetadataDialog.metadata.players" style="margin-top:5px") + span.x-link(v-text="user.displayName" @click="lookupUser(user)") + span(v-if="user.pos" v-text="'('+user.pos.x+', '+user.pos.y+', '+user.pos.z+')'" style="margin-left:5px;color:#909399;font-family:monospace") + br diff --git a/html/src/mixins/dialogs/settings.pug b/html/src/mixins/dialogs/settings.pug new file mode 100644 index 000000000..3d72a1942 --- /dev/null +++ b/html/src/mixins/dialogs/settings.pug @@ -0,0 +1,195 @@ +mixin settings() + //- dialog: VRChat Config JSON + el-dialog.x-dialog(:before-close="beforeDialogClose" @mousedown.native="dialogMouseDown" @mouseup.native="dialogMouseUp" ref="VRChatConfigDialog" :visible.sync="VRChatConfigDialog.visible" :title="$t('dialog.config_json.header')" width="420px") + div(style='font-size:12px;word-break:keep-all') + | {{ $t('dialog.config_json.description1') }} #[br] + | {{ $t('dialog.config_json.description2') }} + br + span(style="margin-right:5px") {{ $t('dialog.config_json.cache_size') }} + span(v-text="VRChatUsedCacheSize") + span / + span(v-text="VRChatTotalCacheSize") + span GB + el-tooltip(placement="top" :content="$t('dialog.config_json.refresh')" :disabled="hideTooltips") + el-button(type="default" :loading="VRChatCacheSizeLoading" @click="getVRChatCacheSize" size="small" icon="el-icon-refresh" circle style="margin-left:5px") + br + span {{ $t('dialog.config_json.delete_all_cache') }} + el-button(size="small" style="margin-left:5px" icon="el-icon-delete" @click="showDeleteAllVRChatCacheConfirm()") {{ $t('dialog.config_json.delete_cache') }} + br + span {{ $t('dialog.config_json.delete_old_cache') }} + el-button(size="small" style="margin-left:5px" icon="el-icon-folder-delete" @click="sweepVRChatCache()") {{ $t('dialog.config_json.sweep_cache') }} + br + div(style="display:inline-block;margin-top:10px" v-for="(item, value) in VRChatConfigList" :key="value") + span(v-text="item.name" style="word-break:keep-all") + |: + el-input(v-model="VRChatConfigFile[value]" :placeholder="item.default" size="mini" :type="item.type?item.type:'text'" :min="item.min" :max="item.max") + div(style="display:inline-block;margin-top:10px") + span {{ $t('dialog.config_json.camera_resolution') }} + br + el-dropdown(@command="(command) => setVRChatCameraResolution(command)" size="small" trigger="click" style="margin-top:5px") + el-button(size="small") + span #[span(v-text="getVRChatCameraResolution()")] #[i.el-icon-arrow-down.el-icon--right] + el-dropdown-menu(#default="dropdown") + el-dropdown-item(v-for="row in VRChatCameraResolutions" :key="row.index" v-text="row.name" :command="row") + div(style="display:inline-block;margin-top:10px;margin-left:10px") + span {{ $t('dialog.config_json.screenshot_resolution') }} + br + el-dropdown(@command="(command) => setVRChatScreenshotResolution(command)" size="small" trigger="click" style="margin-top:5px") + el-button(size="small") + span #[span(v-text="getVRChatScreenshotResolution()")] #[i.el-icon-arrow-down.el-icon--right] + el-dropdown-menu(#default="dropdown") + el-dropdown-item(v-for="row in VRChatScreenshotResolutions" :key="row.index" v-text="row.name" :command="row") + el-checkbox(v-model="VRChatConfigFile.picture_output_split_by_date" style="margin-top:5px;display:block" :checked="true") {{ $t('dialog.config_json.picture_sort_by_date') }} + el-checkbox(v-model="VRChatConfigFile.disableRichPresence" style="margin-top:5px;display:block") {{ $t('dialog.config_json.disable_discord_presence') }} + template(#footer) + el-button(size="small" @click="openExternalLink('https://docs.vrchat.com/docs/configuration-file')") {{ $t('dialog.config_json.vrchat_docs') }} + el-button(size="small" @click="VRChatConfigDialog.visible = false") {{ $t('dialog.config_json.cancel') }} + el-button(type="primary" size="small" :disabled="VRChatConfigDialog.loading" @click="saveVRChatConfigFile") {{ $t('dialog.config_json.save') }} + + //- dialog: YouTube Api Dialog + el-dialog.x-dialog(:before-close="beforeDialogClose" @mousedown.native="dialogMouseDown" @mouseup.native="dialogMouseUp" ref="youTubeApiDialog" :visible.sync="youTubeApiDialog.visible" :title="$t('dialog.youtube_api.header')" width="400px") + div(style='font-size:12px;') + | {{ $t('dialog.youtube_api.description') }} #[br] + el-input(type="textarea" v-model="youTubeApiKey" :placeholder="$t('dialog.youtube_api.placeholder')" maxlength="39" show-word-limit style="display:block;margin-top:10px") + template(#footer) + div(style="display:flex") + el-button(size="small" @click="openExternalLink('https://rapidapi.com/blog/how-to-get-youtube-api-key/')") {{ $t('dialog.youtube_api.guide') }} + el-button(type="primary" size="small" @click="testYouTubeApiKey" style="margin-left:auto") {{ $t('dialog.youtube_api.save') }} + + //- dialog: Discord username list + el-dialog.x-dialog(:before-close="beforeDialogClose" @mousedown.native="dialogMouseDown" @mouseup.native="dialogMouseUp" :visible.sync="discordNamesDialogVisible" :title="$t('dialog.discord_names.header')" width="650px") + div(style='font-size:12px;') + | {{ $t('dialog.discord_names.description') }} + el-input(type="textarea" v-if="discordNamesDialogVisible" v-model="discordNamesContent" size="mini" rows="15" resize="none" readonly style="margin-top:15px") + + //- dialog: Note export dialog + el-dialog.x-dialog(:before-close="beforeDialogClose" @mousedown.native="dialogMouseDown" @mouseup.native="dialogMouseUp" ref="noteExportDialog" :visible.sync="noteExportDialog.visible" :title="$t('dialog.note_export.header')" width="1000px") + div(style="font-size:12px") + | {{ $t('dialog.note_export.description1') }} #[br] + | {{ $t('dialog.note_export.description2') }} #[br] + | {{ $t('dialog.note_export.description3') }} #[br] + | {{ $t('dialog.note_export.description4') }} #[br] + | {{ $t('dialog.note_export.description5') }} #[br] + | {{ $t('dialog.note_export.description6') }} #[br] + | {{ $t('dialog.note_export.description7') }} #[br] + | {{ $t('dialog.note_export.description8') }} #[br] + el-button(size="small" @click="updateNoteExportDialog" :disabled="noteExportDialog.loading" style="margin-top:10px") {{ $t('dialog.note_export.refresh') }} + el-button(size="small" @click="exportNoteExport" :disabled="noteExportDialog.loading" style="margin-top:10px") {{ $t('dialog.note_export.export') }} + el-button(v-if="noteExportDialog.loading" size="small" @click="cancelNoteExport" style="margin-top:10px") {{ $t('dialog.note_export.cancel') }} + span(v-if="noteExportDialog.loading" style="margin:10px") #[i.el-icon-loading(style="margin-right:5px")] {{ $t('dialog.note_export.progress') }} {{ noteExportDialog.progress }}/{{ noteExportDialog.progressTotal }} + template(v-if="noteExportDialog.errors") + el-button(size="small" @click="noteExportDialog.errors = ''") {{ $t('dialog.note_export.clear_errors') }} + h2(style="font-weight:bold;margin:0") {{ $t('dialog.note_export.errors') }} + pre(v-text="noteExportDialog.errors" style="white-space:pre-wrap;font-size:12px") + data-tables(v-if="noteExportDialog.visible" v-bind="noteExportTable" v-loading="noteExportDialog.loading" style="margin-top:10px") + el-table-column(:label="$t('table.import.image')" width="70" prop="currentAvatarThumbnailImageUrl") + template(v-once #default="scope") + el-popover(placement="right" height="500px" trigger="hover") + img.friends-list-avatar(slot="reference" v-lazy="userImage(scope.row.ref)") + img.friends-list-avatar(v-lazy="userImageFull(scope.row.ref)" style="height:500px;cursor:pointer" @click="showFullscreenImageDialog(userImageFull(scope.row.ref))") + el-table-column(:label="$t('table.import.name')" width="170" prop="name") + template(v-once #default="scope") + span.x-link(v-text="scope.row.name" @click="showUserDialog(scope.row.id)") + el-table-column(:label="$t('table.import.note')" prop="memo") + template(v-once #default="scope") + el-input(v-model="scope.row.memo" type="textarea" maxlength="256" show-word-limit :rows="2" :autosize="{ minRows: 1, maxRows: 10 }" size="mini" resize="none") + el-table-column(:label="$t('table.import.skip_export')" width="90" align="right") + template(v-once #default="scope") + el-button(type="text" icon="el-icon-close" size="mini" @click="removeFromNoteExportTable(scope.row)") + + //- dialog: chatbox blacklist + el-dialog.x-dialog(:before-close="beforeDialogClose" @mousedown.native="dialogMouseDown" @mouseup.native="dialogMouseUp" ref="chatboxBlacklistDialog" :visible.sync="chatboxBlacklistDialog.visible" :title="$t('dialog.chatbox_blacklist.header')" width="600px") + div(v-loading="chatboxBlacklistDialog.loading" v-if="chatboxBlacklistDialog.visible") + h2 {{ $t('dialog.chatbox_blacklist.keyword_blacklist') }} + el-input(v-for="(item, index) in chatboxBlacklist" :key="index" :value="item" v-model="chatboxBlacklist[index]" size="small" style="margin-top:5px" @change="saveChatboxBlacklist") + el-button(slot="append" icon="el-icon-delete" @click="chatboxBlacklist.splice(index, 1); saveChatboxBlacklist()") + el-button(@click="chatboxBlacklist.push('')" size="mini" style="margin-top:5px") {{ $t('dialog.chatbox_blacklist.add_item') }} + br + h2 {{ $t('dialog.chatbox_blacklist.user_blacklist') }} + el-tag(v-for="user in chatboxUserBlacklist" type="info" disable-transitions="true" :key="user[0]" style="margin-right:5px;margin-top:5px" closable @close="deleteChatboxUserBlacklist(user[0])") + span {{user[1]}} + + //- dialog: Notification position + el-dialog.x-dialog(:before-close="beforeDialogClose" @mousedown.native="dialogMouseDown" @mouseup.native="dialogMouseUp" ref="notificationPositionDialog" :visible.sync="notificationPositionDialog.visible" :title="$t('dialog.notification_position.header')" width="400px") + div(style='font-size:12px;') + | {{ $t('dialog.notification_position.description') }} + svg.notification-position(version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" viewBox="0 0 300 200" style="margin-top:15px;" xml:space="preserve") + path(style="fill:black;" d="M291.89,5A3.11,3.11,0,0,1,295,8.11V160.64a3.11,3.11,0,0,1-3.11,3.11H8.11A3.11,3.11,0,0,1,5,160.64V8.11A3.11,3.11,0,0,1,8.11,5H291.89m0-5H8.11A8.11,8.11,0,0,0,0,8.11V160.64a8.11,8.11,0,0,0,8.11,8.11H291.89a8.11,8.11,0,0,0,8.11-8.11V8.11A8.11,8.11,0,0,0,291.89,0Z") + rect(style="fill:#c4c4c4;" x="5" y="5" width="290" height="158.75" rx="2.5") + el-radio-group(v-model="notificationPosition" size="mini" @change="changeNotificationPosition") + el-radio(label="topLeft" v-model="notificationPosition" style="margin:0;position:absolute;left:35px;top:120px;") ‎ + el-radio(label="top" v-model="notificationPosition" style="margin:0;position:absolute;left:195px;top:120px;") ‎ + el-radio(label="topRight" v-model="notificationPosition" style="margin:0;position:absolute;right:25px;top:120px;") ‎ + el-radio(label="centerLeft" v-model="notificationPosition" style="margin:0;position:absolute;left:35px;top:200px;") ‎ + el-radio(label="center" v-model="notificationPosition" style="margin:0;position:absolute;left:195px;top:200px;") ‎ + el-radio(label="centerRight" v-model="notificationPosition" style="margin:0;position:absolute;right:25px;top:200px;") ‎ + el-radio(label="bottomLeft" v-model="notificationPosition" style="margin:0;position:absolute;left:35px;top:280px;") ‎ + el-radio(label="bottom" v-model="notificationPosition" style="margin:0;position:absolute;left:195px;top:280px;") ‎ + el-radio(label="bottomRight" v-model="notificationPosition" style="margin:0;position:absolute;right:25px;top:280px;") ‎ + template(#footer) + div(style="display:flex") + el-button(type="primary" size="small" style="margin-left:auto" @click="notificationPositionDialog.visible = false") {{ $t('dialog.notification_position.ok') }} + + //- dialog: avatar database provider + el-dialog.x-dialog(:before-close="beforeDialogClose" @mousedown.native="dialogMouseDown" @mouseup.native="dialogMouseUp" ref="avatarProviderDialog" :visible.sync="avatarProviderDialog.visible" :title="$t('dialog.avatar_database_provider.header')" width="600px") + div + el-input(v-for="(provider, index) in avatarRemoteDatabaseProviderList" :key="index" :value="provider" v-model="avatarRemoteDatabaseProviderList[index]" @change="saveAvatarProviderList" size="small" style="margin-top:5px") + el-button(slot="append" icon="el-icon-delete" @click="removeAvatarProvider(provider)") + el-button(@click="avatarRemoteDatabaseProviderList.push('')" size="mini" style="margin-top:5px") {{ $t('dialog.avatar_database_provider.add_provider') }} + + //- dialog: Registry Auto Backup + el-dialog.x-dialog(:before-close="beforeDialogClose" @closed="clearVrcRegistryDialog" @mousedown.native="dialogMouseDown" @mouseup.native="dialogMouseUp" ref="registryBackupDialog" :visible.sync="registryBackupDialog.visible" :title="$t('dialog.registry_backup.header')" width="600px") + div(v-if="registryBackupDialog.visible" style="margin-top:10px") + div.options-container + div.options-container-item + span.name {{ $t('dialog.registry_backup.auto_backup') }} + el-switch(v-model="vrcRegistryAutoBackup" @change="saveVrcRegistryAutoBackup") + el-button(@click="promptVrcRegistryBackupName" size="small") {{ $t('dialog.registry_backup.backup') }} + el-button(@click="AppApi.OpenVrcRegJsonFileDialog()" size="small") {{ $t('dialog.registry_backup.restore_from_file') }} + el-button(@click="deleteVrcRegistry" size="small") {{ $t('dialog.registry_backup.reset') }} + data-tables(v-bind="registryBackupTable" style="margin-top:10px") + el-table-column(:label="$t('dialog.registry_backup.name')" prop="name") + el-table-column(:label="$t('dialog.registry_backup.date')" prop="date") + template(v-once #default="scope") + span {{ scope.row.date | formatDate('long') }} + el-table-column(:label="$t('dialog.registry_backup.action')" width="90" align="right") + template(v-once #default="scope") + el-tooltip(placement="top" :content="$t('dialog.registry_backup.restore')" :disabled="hideTooltips") + el-button(type="text" icon="el-icon-upload2" size="mini" @click="restoreVrcRegistryBackup(scope.row)") + el-tooltip(placement="top" :content="$t('dialog.registry_backup.save_to_file')" :disabled="hideTooltips") + el-button(type="text" icon="el-icon-download" size="mini" @click="saveVrcRegistryBackupToFile(scope.row)") + el-tooltip(placement="top" :content="$t('dialog.registry_backup.delete')" :disabled="hideTooltips") + el-button(type="text" icon="el-icon-delete" size="mini" @click="deleteVrcRegistryBackup(scope.row)") + + //- dialog: Enable primary password + el-dialog.x-dialog( + :visible.sync="enablePrimaryPasswordDialog.visible" + :before-close="enablePrimaryPasswordDialog.beforeClose" + ref="primaryPasswordDialog" + :close-on-click-modal="false" + :title="$t('dialog.primary_password.header')" + width="400px" + ) + el-input( + v-model="enablePrimaryPasswordDialog.password" + :placeholder="$t('dialog.primary_password.password_placeholder')" + type="password" + size="mini" + maxlength="32" + show-password + autofocus + ) + el-input( + v-model="enablePrimaryPasswordDialog.rePassword" + :placeholder="$t('dialog.primary_password.re_input_placeholder')" + type="password" + style="margin-top:5px" + size="mini" + maxlength="32" + show-password + ) + template(#footer) + el-button( + type="primary" size="small" @click="setPrimaryPassword" + :disabled="enablePrimaryPasswordDialog.password.length===0||enablePrimaryPasswordDialog.password!==enablePrimaryPasswordDialog.rePassword" + ) {{ $t('dialog.primary_password.ok') }} diff --git a/html/src/mixins/dialogs/tags.pug b/html/src/mixins/dialogs/tags.pug new file mode 100644 index 000000000..0add596b5 --- /dev/null +++ b/html/src/mixins/dialogs/tags.pug @@ -0,0 +1,67 @@ +mixin tags() + //- dialog: Set World Tags + el-dialog.x-dialog(:before-close="beforeDialogClose" @mousedown.native="dialogMouseDown" @mouseup.native="dialogMouseUp" ref="setWorldTagsDialog" :visible.sync="setWorldTagsDialog.visible" :title="$t('dialog.set_world_tags.header')" width="400px") + el-checkbox(v-model="setWorldTagsDialog.avatarScalingDisabled") {{ $t('dialog.set_world_tags.avatar_scaling_disabled') }} + br + el-checkbox(v-model="setWorldTagsDialog.focusViewDisabled") {{ $t('dialog.set_world_tags.focus_view_disabled') }} + br + el-checkbox(v-model="setWorldTagsDialog.stickersDisabled") {{ $t('dialog.set_world_tags.stickers_disabled') }} + br + el-checkbox(v-model="setWorldTagsDialog.debugAllowed") {{ $t('dialog.set_world_tags.enable_debugging') }} + div(style='font-size:12px;margin-top:10px') + | {{ $t('dialog.set_world_tags.author_tags') }} #[br] + el-input(type="textarea" v-model="setWorldTagsDialog.authorTags" size="mini" show-word-limit :autosize="{ minRows:2, maxRows:5 }" placeholder="" style="margin-top:10px") + div(style='font-size:12px;margin-top:10px') + | {{ $t('dialog.set_world_tags.content_tags') }} #[br] + el-checkbox(v-model="setWorldTagsDialog.contentHorror") {{ $t('dialog.set_world_tags.content_horror') }} + br + el-checkbox(v-model="setWorldTagsDialog.contentGore") {{ $t('dialog.set_world_tags.content_gore') }} + br + el-checkbox(v-model="setWorldTagsDialog.contentViolence") {{ $t('dialog.set_world_tags.content_violence') }} + br + el-checkbox(v-model="setWorldTagsDialog.contentAdult") {{ $t('dialog.set_world_tags.content_adult') }} + br + el-checkbox(v-model="setWorldTagsDialog.contentSex") {{ $t('dialog.set_world_tags.content_sex') }} + //- el-input(type="textarea" v-model="setWorldTagsDialog.contentTags" size="mini" show-word-limit :autosize="{ minRows:2, maxRows:5 }" placeholder="" style="margin-top:10px") + template(#footer) + div(style="display:flex") + el-button(size="small" @click="setWorldTagsDialog.visible = false") {{ $t('dialog.set_world_tags.cancel') }} + el-button(type="primary" size="small" @click="saveSetWorldTagsDialog") {{ $t('dialog.set_world_tags.save') }} + + //- dialog: Set Avatar Tags + el-dialog.x-dialog(:before-close="beforeDialogClose" @mousedown.native="dialogMouseDown" @mouseup.native="dialogMouseUp" ref="setAvatarTagsDialog" :visible.sync="setAvatarTagsDialog.visible" :title="$t('dialog.set_avatar_tags.header')" width="770px") + template(v-if="setAvatarTagsDialog.visible") + el-checkbox(v-model="setAvatarTagsDialog.contentHorror" @change="updateSelectedAvatarTags") {{ $t('dialog.set_avatar_tags.content_horror') }} + br + el-checkbox(v-model="setAvatarTagsDialog.contentGore" @change="updateSelectedAvatarTags") {{ $t('dialog.set_avatar_tags.content_gore') }} + br + el-checkbox(v-model="setAvatarTagsDialog.contentViolence" @change="updateSelectedAvatarTags") {{ $t('dialog.set_avatar_tags.content_violence') }} + br + el-checkbox(v-model="setAvatarTagsDialog.contentAdult" @change="updateSelectedAvatarTags") {{ $t('dialog.set_avatar_tags.content_adult') }} + br + el-checkbox(v-model="setAvatarTagsDialog.contentSex" @change="updateSelectedAvatarTags") {{ $t('dialog.set_avatar_tags.content_sex') }} + br + el-input(v-model="setAvatarTagsDialog.selectedTagsCsv" @input="updateInputAvatarTags" size="mini" :autosize="{ minRows:2, maxRows:5 }" :placeholder="$t('dialog.set_avatar_tags.custom_tags_placeholder')" style="margin-top:10px") + template(v-if="setAvatarTagsDialog.ownAvatars.length === setAvatarTagsDialog.selectedCount") + el-button(size="small" @click="setAvatarTagsSelectToggle") {{ $t('dialog.set_avatar_tags.select_none') }} + template(v-else) + el-button(size="small" @click="setAvatarTagsSelectToggle") {{ $t('dialog.set_avatar_tags.select_all') }} + span(style="margin-left:5px") {{ setAvatarTagsDialog.selectedCount }} / {{ setAvatarTagsDialog.ownAvatars.length }} + span(v-if="setAvatarTagsDialog.loading" style="margin-left:5px") + i.el-icon-loading + br + .x-friend-list(style="margin-top:10px;min-height:60px;max-height:280px") + .x-friend-item(v-for="avatar in setAvatarTagsDialog.ownAvatars" :key="setAvatarTagsDialog.forceUpdate" @click="showAvatarDialog(avatar.id)" class="x-friend-item-border" style="width:350px") + .avatar + img(v-if="avatar.thumbnailImageUrl" v-lazy="avatar.thumbnailImageUrl") + .detail + span.name(v-text="avatar.name") + span.extra(v-text="avatar.releaseStatus" v-if="avatar.releaseStatus === 'public'" style="color: #67c23a;") + span.extra(v-text="avatar.releaseStatus" v-else-if="avatar.releaseStatus === 'private'" style="color: #f56c6c;") + span.extra(v-text="avatar.releaseStatus" v-else) + span.extra(v-text="avatar.$tagString") + el-button(type="text" size="mini" @click.stop style="margin-left:5px") + el-checkbox(v-model="avatar.$selected" @change="updateAvatarTagsSelection") + template(#footer) + el-button(size="small" @click="setAvatarTagsDialog.visible = false") {{ $t('dialog.set_avatar_tags.cancel') }} + el-button(type="primary" size="small" @click="saveSetAvatarTagsDialog") {{ $t('dialog.set_avatar_tags.save') }} diff --git a/html/src/mixins/dialogs/userDialog.pug b/html/src/mixins/dialogs/userDialog.pug new file mode 100644 index 000000000..9e04844bc --- /dev/null +++ b/html/src/mixins/dialogs/userDialog.pug @@ -0,0 +1,425 @@ +mixin userDialog() + el-dialog.x-dialog.x-user-dialog(:before-close="beforeDialogClose" @mousedown.native="dialogMouseDown" @mouseup.native="dialogMouseUp" ref="userDialog" :visible.sync="userDialog.visible" :show-close="false" width="770px") + div(v-loading="userDialog.loading") + div(style="display:flex") + el-popover(v-if="userDialog.ref.profilePicOverrideThumbnail || userDialog.ref.profilePicOverride" placement="right" width="500px" trigger="click") + template(slot="reference") + img.x-link(v-if="userDialog.ref.profilePicOverrideThumbnail" v-lazy="userDialog.ref.profilePicOverrideThumbnail" style="flex:none;height:120px;width:213.33px;border-radius:12px;object-fit:cover") + img.x-link(v-else v-lazy="userDialog.ref.profilePicOverride" style="flex:none;height:120px;width:213.33px;border-radius:12px;object-fit:cover") + img.x-link(v-lazy="userDialog.ref.profilePicOverride" style="height:400px" @click="showFullscreenImageDialog(userDialog.ref.profilePicOverride)") + el-popover(v-else placement="right" width="500px" trigger="click") + img.x-link(slot="reference" v-lazy="userDialog.ref.currentAvatarThumbnailImageUrl" style="flex:none;height:120px;width:160px;border-radius:12px;object-fit:cover") + img.x-link(v-lazy="userDialog.ref.currentAvatarImageUrl" style="height:500px" @click="showFullscreenImageDialog(userDialog.ref.currentAvatarImageUrl)") + div(style="flex:1;display:flex;align-items:center;margin-left:15px") + div(style="flex:1") + div + el-tooltip(v-if="userDialog.ref.status" placement="top") + template(#content) + span(v-if="userDialog.ref.state === 'active'") {{ $t('dialog.user.status.active') }} + span(v-else-if="userDialog.ref.state === 'offline'") {{ $t('dialog.user.status.offline') }} + span(v-else-if="userDialog.ref.status === 'active'") {{ $t('dialog.user.status.online') }} + span(v-else-if="userDialog.ref.status === 'join me'") {{ $t('dialog.user.status.join_me') }} + span(v-else-if="userDialog.ref.status === 'ask me'") {{ $t('dialog.user.status.ask_me') }} + span(v-else-if="userDialog.ref.status === 'busy'") {{ $t('dialog.user.status.busy') }} + span(v-else) {{ $t('dialog.user.status.offline') }} + i.x-user-status(:class="userStatusClass(userDialog.ref)") + template(v-if="userDialog.previousDisplayNames.length > 0") + el-tooltip(placement="bottom") + template(#content) + span {{ $t('dialog.user.previous_display_names') }} + div(v-for="displayName in userDialog.previousDisplayNames" placement="top") + span(v-text="displayName") + i.el-icon-caret-bottom + el-popover(placement="top" trigger="click") + span.dialog-title(slot="reference" v-text="userDialog.ref.displayName" style="margin-left:5px;margin-right:5px;cursor:pointer") + span(style="display:block;text-align:center;font-family:monospace") {{ userDialog.ref.displayName | textToHex }} + el-tooltip(v-if="userDialog.ref.pronouns" placement="top" :content="$t('dialog.user.pronouns')" :disabled="hideTooltips") + span.x-grey(v-text="userDialog.ref.pronouns" style="margin-right:5px;font-family:monospace;font-size:12px") + el-tooltip(v-for="item in userDialog.ref.$languages" :key="item.key" placement="top") + template(#content) + span {{ item.value }} ({{ item.key }}) + span.flags(:class="languageClass(item.key)" style="display:inline-block;margin-right:5px") + template(v-if="userDialog.ref.id === API.currentUser.id") + br + el-popover(placement="top" trigger="click") + span.x-grey(slot="reference" v-text="API.currentUser.username" style="margin-right:10px;font-family:monospace;font-size:12px;cursor:pointer") + span(style="display:block;text-align:center;font-family:monospace") {{ API.currentUser.username | textToHex }} + div + el-tag.name(type="info" effect="plain" size="mini" :class="userDialog.ref.$trustClass" v-text="userDialog.ref.$trustLevel" style="margin-right:5px;margin-top:5px") + el-tag.x-tag-friend(v-if="userDialog.isFriend && userDialog.friend" type="info" effect="plain" size="mini" style="margin-right:5px;margin-top:5px") {{ $t('dialog.user.tags.friend_no', { number: userDialog.friend.no }) }} + el-tag.x-tag-troll(v-if="userDialog.ref.$isTroll" type="info" effect="plain" size="mini" style="margin-right:5px;margin-top:5px") Nuisance + el-tag.x-tag-troll(v-if="userDialog.ref.$isProbableTroll" type="info" effect="plain" size="mini" style="margin-right:5px;margin-top:5px") Almost Nuisance + el-tag.x-tag-vip(v-if="userDialog.ref.$isModerator" type="info" effect="plain" size="mini" style="margin-right:5px;margin-top:5px") {{ $t('dialog.user.tags.vrchat_team') }} + el-tag.x-tag-vrcplus(v-if="userDialog.ref.$isVRCPlus" type="info" effect="plain" size="mini" style="margin-right:5px;margin-top:5px") VRC+ + el-tag.x-tag-platform-pc(v-if="userDialog.ref.last_platform === 'standalonewindows'" type="info" effect="plain" size="mini" style="margin-right:5px;margin-top:5px") PC + el-tag.x-tag-platform-quest(v-else-if="userDialog.ref.last_platform === 'android'" type="info" effect="plain" size="mini" style="margin-right:5px;margin-top:5px") Android + el-tag.x-tag-platform-ios(v-else-if="userDialog.ref.last_platform === 'ios'" type="info" effect="plain" size="mini" style="margin-right:5px;margin-top:5px") iOS + el-tag.x-tag-platform-other(v-else-if="userDialog.ref.last_platform" type="info" effect="plain" size="mini" style="margin-right:5px;margin-top:5px") {{ userDialog.ref.last_platform }} + el-tag.name(v-if="userDialog.ref.$customTag" type="info" effect="plain" size="mini" v-text="userDialog.ref.$customTag" :style="{'color':userDialog.ref.$customTagColour, 'border-color':userDialog.ref.$customTagColour}" style="margin-right:5px;margin-top:5px") + div(style="margin-top:5px") + span(v-text="userDialog.ref.statusDescription" style="font-size:12px") + div(v-if="userDialog.ref.userIcon" style="flex:none;margin-right:10px") + el-popover(placement="right" width="500px" trigger="click") + img.x-link(slot="reference" v-lazy="userDialog.ref.userIcon" style="flex:none;width:120px;height:120px;border-radius:12px;object-fit:cover") + img.x-link(v-lazy="userDialog.ref.userIcon" style="height:500px" @click="showFullscreenImageDialog(userDialog.ref.userIcon)") + div(style="flex:none") + template(v-if="(API.currentUser.id !== userDialog.ref.id && userDialog.isFriend) || userDialog.isFavorite") + el-tooltip(v-if="userDialog.isFavorite" placement="top" :content="$t('dialog.user.actions.unfavorite_tooltip')" :disabled="hideTooltips") + el-button(@click="userDialogCommand('Add Favorite')" type="warning" icon="el-icon-star-on" circle) + el-tooltip(v-else placement="top" :content="$t('dialog.user.actions.favorite_tooltip')" :disabled="hideTooltips") + el-button(type="default" @click="userDialogCommand('Add Favorite')" icon="el-icon-star-off" circle) + el-dropdown(trigger="click" @command="userDialogCommand" size="small") + el-button(:type="(userDialog.incomingRequest || userDialog.outgoingRequest) ? 'success' : (userDialog.isBlock || userDialog.isMute) ? 'danger' : 'default'" icon="el-icon-more" circle style="margin-left:5px") + el-dropdown-menu(#default="dropdown") + el-dropdown-item(icon="el-icon-refresh" command="Refresh") {{ $t('dialog.user.actions.refresh') }} + template(v-if="userDialog.ref.id === API.currentUser.id") + el-dropdown-item(icon="el-icon-picture-outline" command="Manage Gallery" divided) {{ $t('dialog.user.actions.manage_gallery_icon') }} + el-dropdown-item(icon="el-icon-s-custom" command="Show Avatar Author") {{ $t('dialog.user.actions.show_avatar_author') }} + el-dropdown-item(icon="el-icon-s-custom" command="Show Fallback Avatar Details") {{ $t('dialog.user.actions.show_fallback_avatar') }} + el-dropdown-item(icon="el-icon-edit" command="Edit Social Status" divided) {{ $t('dialog.user.actions.edit_status') }} + el-dropdown-item(icon="el-icon-edit" command="Edit Language") {{ $t('dialog.user.actions.edit_language') }} + el-dropdown-item(icon="el-icon-edit" command="Edit Bio") {{ $t('dialog.user.actions.edit_bio') }} + el-dropdown-item(icon="el-icon-edit" command="Edit Pronouns") {{ $t('dialog.user.actions.edit_pronouns') }} + el-dropdown-item(icon="el-icon-switch-button" command="Logout" divided) {{ $t('dialog.user.actions.logout') }} + template(v-else) + template(v-if="userDialog.isFriend") + el-dropdown-item(icon="el-icon-postcard" command="Request Invite" divided) {{ $t('dialog.user.actions.request_invite') }} + el-dropdown-item(icon="el-icon-postcard" command="Request Invite Message") {{ $t('dialog.user.actions.request_invite_with_message') }} + template(v-if="lastLocation.location && isGameRunning && checkCanInvite(lastLocation.location)") + el-dropdown-item(icon="el-icon-message" command="Invite") {{ $t('dialog.user.actions.invite') }} + el-dropdown-item(icon="el-icon-message" command="Invite Message") {{ $t('dialog.user.actions.invite_with_message') }} + template(v-else-if="userDialog.incomingRequest") + el-dropdown-item(icon="el-icon-check" command="Accept Friend Request") {{ $t('dialog.user.actions.accept_friend_request') }} + el-dropdown-item(icon="el-icon-close" command="Decline Friend Request") {{ $t('dialog.user.actions.decline_friend_request') }} + el-dropdown-item(v-else-if="userDialog.outgoingRequest" icon="el-icon-close" command="Cancel Friend Request") {{ $t('dialog.user.actions.cancel_friend_request') }} + el-dropdown-item(v-else icon="el-icon-plus" command="Send Friend Request") {{ $t('dialog.user.actions.send_friend_request') }} + el-dropdown-item(icon="el-icon-message" command="Invite To Group") {{ $t('dialog.user.actions.invite_to_group') }} + //- el-dropdown-item(icon="el-icon-thumb" command="Send Boop" :disabled="!API.currentUser.isBoopingEnabled") {{ $t('dialog.user.actions.send_boop') }} + el-dropdown-item(icon="el-icon-s-custom" command="Show Avatar Author" divided) {{ $t('dialog.user.actions.show_avatar_author') }} + el-dropdown-item(icon="el-icon-s-custom" command="Show Fallback Avatar Details") {{ $t('dialog.user.actions.show_fallback_avatar') }} + el-dropdown-item(icon="el-icon-tickets" command="Previous Instances") {{ $t('dialog.user.actions.show_previous_instances') }} + el-dropdown-item(v-if="userDialog.ref.currentAvatarImageUrl" icon="el-icon-picture-outline" command="Previous Images") {{ $t('dialog.user.actions.show_previous_images') }} + el-dropdown-item(v-if="userDialog.isBlock" icon="el-icon-circle-check" command="Unblock" divided style="color:#F56C6C") {{ $t('dialog.user.actions.moderation_unblock') }} + el-dropdown-item(v-else icon="el-icon-circle-close" command="Block" divided :disabled="userDialog.ref.$isModerator") {{ $t('dialog.user.actions.moderation_block') }} + el-dropdown-item(v-if="userDialog.isMute" icon="el-icon-microphone" command="Unmute" style="color:#F56C6C") {{ $t('dialog.user.actions.moderation_unmute') }} + el-dropdown-item(v-else icon="el-icon-turn-off-microphone" command="Mute" :disabled="userDialog.ref.$isModerator") {{ $t('dialog.user.actions.moderation_mute') }} + el-dropdown-item(v-if="userDialog.isMuteChat" icon="el-icon-chat-line-round" command="Unmute Chatbox" style="color:#F56C6C") {{ $t('dialog.user.actions.moderation_enable_chatbox') }} + el-dropdown-item(v-else icon="el-icon-chat-dot-round" command="Mute Chatbox") {{ $t('dialog.user.actions.moderation_disable_chatbox') }} + el-dropdown-item(icon="el-icon-user-solid" command="Show Avatar") + i.el-icon-check.el-icon--left(v-if="userDialog.isShowAvatar") + span {{ $t('dialog.user.actions.moderation_show_avatar') }} + el-dropdown-item(icon="el-icon-user" command="Hide Avatar") + i.el-icon-check.el-icon--left(v-if="userDialog.isHideAvatar") + span {{ $t('dialog.user.actions.moderation_hide_avatar') }} + el-dropdown-item(v-if="userDialog.isInteractOff" icon="el-icon-thumb" command="Enable Avatar Interaction" style="color:#F56C6C") {{ $t('dialog.user.actions.moderation_enable_avatar_interaction') }} + el-dropdown-item(v-else icon="el-icon-circle-close" command="Disable Avatar Interaction") {{ $t('dialog.user.actions.moderation_disable_avatar_interaction') }} + el-dropdown-item(icon="el-icon-s-flag" command="Report Hacking" :disabled="userDialog.ref.$isModerator") {{ $t('dialog.user.actions.report_hacking') }} + template(v-if="userDialog.isFriend") + el-dropdown-item(icon="el-icon-delete" command="Unfriend" divided style="color:#F56C6C") {{ $t('dialog.user.actions.unfriend') }} + el-tabs(ref="userDialogTabs" @tab-click="userDialogTabClick") + el-tab-pane(:label="$t('dialog.user.info.header')") + template(v-if="isFriendOnline(userDialog.friend) || API.currentUser.id === userDialog.id") + div(v-if="userDialog.ref.location" style="display:flex;flex-direction:column;margin-bottom:10px;padding-bottom:10px;border-bottom:1px solid #e4e7ed14") + div(style="flex:none") + template(v-if="isRealInstance(userDialog.$location.tag)") + el-tooltip(placement="top" :content="$t('dialog.user.info.launch_invite_tooltip')" :disabled="hideTooltips") + launch(:location="userDialog.$location.tag") + el-tooltip(placement="top" :content="$t('dialog.user.info.self_invite_tooltip')" :disabled="hideTooltips") + invite-yourself(:location="userDialog.$location.tag" :shortname="userDialog.$location.shortName" style="margin-left:5px") + el-tooltip(placement="top" :content="$t('dialog.user.info.refresh_instance_info')" :disabled="hideTooltips") + el-button(@click="refreshInstancePlayerCount(userDialog.$location.tag)" size="mini" icon="el-icon-refresh" style="margin-left:5px" circle) + last-join(:location="userDialog.$location.tag" :currentlocation="lastLocation.location") + instance-info(:location="userDialog.$location.tag" :instance="userDialog.instance.ref" :friendcount="userDialog.instance.friendCount" :updateelement="updateInstanceInfo") + location(:location="userDialog.ref.location" :traveling="userDialog.ref.travelingToLocation" style="display:block;margin-top:5px") + .x-friend-list(style="flex:1;margin-top:10px;max-height:150px") + .x-friend-item(v-if="userDialog.$location.userId" @click="showUserDialog(userDialog.$location.userId)" class="x-friend-item-border") + template(v-if="userDialog.$location.user") + .avatar(:class="userStatusClass(userDialog.$location.user)") + img(v-lazy="userImage(userDialog.$location.user)") + .detail + span.name(v-text="userDialog.$location.user.displayName" :style="{'color':userDialog.$location.user.$userColour}") + span.extra {{ $t('dialog.user.info.instance_creator') }} + span(v-else v-text="userDialog.$location.userId") + .x-friend-item(v-for="user in userDialog.users" :key="user.id" @click="showUserDialog(user.id)" class="x-friend-item-border") + .avatar(:class="userStatusClass(user)") + img(v-lazy="userImage(user)") + .detail + span.name(v-text="user.displayName" :style="{'color':user.$userColour}") + span.extra(v-if="user.location === 'traveling'") + i.el-icon-loading(style="margin-right:5px") + timer(:epoch="user.$travelingToTime") + span.extra(v-else) + timer(:epoch="user.$location_at") + .x-friend-list(style="max-height:none") + .x-friend-item(v-if="!hideUserNotes" style="width:100%;cursor:default") + .detail + span.name {{ $t('dialog.user.info.note') }} + el-input(v-model="userDialog.note" type="textarea" maxlength="256" show-word-limit :rows="2" :autosize="{ minRows: 1, maxRows: 20 }" @change="checkNote(userDialog.ref, userDialog.note)" @input="cleanNote(userDialog.note)" :placeholder="$t('dialog.user.info.note_placeholder')" size="mini" resize="none") + div(style="float:right") + i.el-icon-loading(v-if="userDialog.noteSaving" style="margin-left:5px") + i.el-icon-more-outline(v-else-if="userDialog.note !== userDialog.ref.note" style="margin-left:5px") + el-button(v-if="userDialog.note" type="text" icon="el-icon-delete" size="mini" @click="deleteNote(userDialog.id)" style="margin-left:5px") + .x-friend-item(v-if="!hideUserMemos" style="width:100%;cursor:default") + .detail + span.name {{ $t('dialog.user.info.memo') }} + el-input.extra(v-model="userDialog.memo" @change="onUserMemoChange" type="textarea" :rows="2" :autosize="{ minRows: 1, maxRows: 20 }" :placeholder="$t('dialog.user.info.memo_placeholder')" size="mini" resize="none") + .x-friend-item(style="width:100%;cursor:default") + .detail + span.name(v-if="userDialog.id !== API.currentUser.id && userDialog.ref.profilePicOverride && userDialog.ref.currentAvatarImageUrl") {{ $t('dialog.user.info.avatar_info_last_seen') }} + span.name(v-else) {{ $t('dialog.user.info.avatar_info') }} + .extra + avatar-info(:imageurl="userDialog.ref.currentAvatarImageUrl" :userid="userDialog.id" :avatartags="userDialog.ref.currentAvatarTags") + .x-friend-item(style="width:100%;cursor:default") + .detail + span.name {{ $t('dialog.user.info.represented_group') }} + .extra(v-if="userDialog.representedGroup?.isRepresenting") + div(style="display:inline-block;flex:none;margin-right:5px") + el-popover(placement="right" width="500px" trigger="click") + img.x-link(slot="reference" v-lazy="userDialog.representedGroup.iconUrl" style="flex:none;width:60px;height:60px;border-radius:4px;object-fit:cover") + img.x-link(v-lazy="userDialog.representedGroup.iconUrl" style="height:500px" @click="showFullscreenImageDialog(userDialog.representedGroup.iconUrl)") + span(style="vertical-align:top;cursor:pointer" @click="showGroupDialog(userDialog.representedGroup.groupId)") + span(v-if="userDialog.representedGroup.ownerId === userDialog.id" style="margin-right:5px") 👑 + span(v-text="userDialog.representedGroup.name" style="margin-right:5px") + span ({{ userDialog.representedGroup.memberCount }}) + .extra(v-else) - + .x-friend-item(style="width:100%;cursor:default") + .detail + span.name {{ $t('dialog.user.info.bio') }} + pre.extra(style="font-family:inherit;font-size:12px;white-space:pre-wrap;margin:0 0.5em 0 0") {{ userDialog.ref.bio || '-' }} + div(v-if="userDialog.id === API.currentUser.id" style="float:right") + el-button(type="text" icon="el-icon-edit" size="mini" @click="showBioDialog" style="margin-left:5px") + div(style="margin-top:5px") + el-tooltip(v-if="link" v-for="(link, index) in userDialog.ref.bioLinks" :key="index") + template(#content) + span(v-text="link") + img(:src="getFaviconUrl(link)" style="width:16px;height:16px;vertical-align:middle;margin-right:5px;cursor:pointer" @click.stop="openExternalLink(link)") + template(v-if="API.currentUser.id !== userDialog.id") + .x-friend-item(style="cursor:default") + .detail + span.name {{ $t('dialog.user.info.last_seen') }} + el-tooltip(v-if="!hideTooltips" placement="top" style="margin-left:5px" :content="$t('dialog.user.info.accuracy_notice')") + i.el-icon-warning + span.extra {{ userDialog.lastSeen | formatDate('long') }} + .x-friend-item(@click="showPreviousInstancesUserDialog(userDialog.ref)") + .detail + span.name {{ $t('dialog.user.info.join_count') }} + el-tooltip(v-if="!hideTooltips" placement="top" style="margin-left:5px" :content="$t('dialog.user.info.accuracy_notice')") + i.el-icon-warning + span.extra(v-if="userDialog.joinCount === 0") - + span.extra(v-else v-text="userDialog.joinCount") + .x-friend-item(style="cursor:default") + .detail + span.name {{ $t('dialog.user.info.time_together') }} + el-tooltip(v-if="!hideTooltips" placement="top" style="margin-left:5px" :content="$t('dialog.user.info.accuracy_notice')") + i.el-icon-warning + span.extra(v-if="userDialog.timeSpent === 0") - + span.extra(v-else) {{ timeToText(userDialog.timeSpent) }} + template(v-else) + .x-friend-item(@click="showPreviousInstancesUserDialog(userDialog.ref)") + .detail + span.name {{ $t('dialog.user.info.play_time') }} + el-tooltip(v-if="!hideTooltips" placement="top" style="margin-left:5px" :content="$t('dialog.user.info.accuracy_notice')") + i.el-icon-warning + span.extra(v-if="userDialog.timeSpent === 0") - + span.extra(v-else) {{ timeToText(userDialog.timeSpent) }} + .x-friend-item(style="cursor:default") + el-tooltip(placement="top") + template(#content) + span {{ userOnlineForTimestamp(userDialog) | formatDate('short') }} + .detail + span.name(v-if="userDialog.ref.state === 'online' && userDialog.ref.$online_for") {{ $t('dialog.user.info.online_for') }} + el-tooltip(v-if="!hideTooltips" placement="top" style="margin-left:5px" :content="$t('dialog.user.info.accuracy_notice')") + i.el-icon-warning + span.name(v-else) {{ $t('dialog.user.info.offline_for') }} + el-tooltip(v-if="!hideTooltips" placement="top" style="margin-left:5px" :content="$t('dialog.user.info.accuracy_notice')") + i.el-icon-warning + span.extra {{ userOnlineFor(userDialog) }} + .x-friend-item(style="cursor:default") + el-tooltip(placement="top") + template(#content) + span {{ $t('dialog.user.info.last_login') }} {{ userDialog.ref.last_login | formatDate('short') }} + .detail + span.name {{ $t('dialog.user.info.last_activity') }} + span.extra {{ userDialog.ref.last_activity | formatDate('long') }} + .x-friend-item(style="cursor:default") + .detail + span.name {{ $t('dialog.user.info.date_joined') }} + span.extra(v-text="userDialog.ref.date_joined") + .x-friend-item(v-if="API.currentUser.id !== userDialog.id" style="cursor:default") + el-tooltip(placement="top") + template(#content v-if="userDialog.dateFriendedInfo.length") + template(v-for="ref in userDialog.dateFriendedInfo") + span {{ ref.type }}: {{ ref.created_at | formatDate('long') }} + br + template(#content v-else) + span - + .detail + span.name(v-if="userDialog.unFriended") {{ $t('dialog.user.info.unfriended') }} + el-tooltip(v-if="!hideTooltips" placement="top" style="margin-left:5px" :content="$t('dialog.user.info.accuracy_notice')") + i.el-icon-warning + span.name(v-else) {{ $t('dialog.user.info.friended') }} + el-tooltip(v-if="!hideTooltips" placement="top" style="margin-left:5px" :content="$t('dialog.user.info.accuracy_notice')") + i.el-icon-warning + span.extra {{ userDialog.dateFriended | formatDate('long') }} + template(v-if="API.currentUser.id === userDialog.id") + .x-friend-item(@click="toggleAvatarCopying") + .detail + span.name {{ $t('dialog.user.info.avatar_cloning') }} + span.extra(v-if="API.currentUser.allowAvatarCopying" style="color:#67C23A") {{ $t('dialog.user.info.avatar_cloning_allow') }} + span.extra(v-else style="color:#F56C6C") {{ $t('dialog.user.info.avatar_cloning_deny') }} + //- .x-friend-item(@click="toggleAllowBooping") + //- .detail + //- span.name {{ $t('dialog.user.info.booping') }} + //- span.extra(v-if="API.currentUser.isBoopingEnabled" style="color:#67C23A") {{ $t('dialog.user.info.avatar_cloning_allow') }} + //- span.extra(v-else style="color:#F56C6C") {{ $t('dialog.user.info.avatar_cloning_deny') }} + template(v-else) + .x-friend-item(style="cursor:default") + .detail + span.name {{ $t('dialog.user.info.avatar_cloning') }} + span.extra(v-if="userDialog.ref.allowAvatarCopying" style="color:#67C23A") {{ $t('dialog.user.info.avatar_cloning_allow') }} + span.extra(v-else style="color:#F56C6C") {{ $t('dialog.user.info.avatar_cloning_deny') }} + .x-friend-item(v-if="userDialog.ref.id === API.currentUser.id && API.currentUser.homeLocation" @click="showWorldDialog(API.currentUser.homeLocation)" style="width:100%") + .detail + span.name {{ $t('dialog.user.info.home_location') }} + span.extra + span(v-text="userDialog.$homeLocationName") + el-button(@click.stop="resetHome()" size="mini" icon="el-icon-delete" circle style="margin-left:5px") + .x-friend-item(style="width:100%;cursor:default") + .detail + span.name {{ $t('dialog.user.info.id') }} + span.extra {{ userDialog.id }} + el-tooltip(placement="top" :content="$t('dialog.user.info.id_tooltip')" :disabled="hideTooltips") + el-dropdown(trigger="click" @click.native.stop size="mini" style="margin-left:5px") + el-button(type="default" icon="el-icon-s-order" size="mini" circle) + el-dropdown-menu(#default="dropdown") + el-dropdown-item(@click.native="copyUserId(userDialog.id)") {{ $t('dialog.user.info.copy_id') }} + el-dropdown-item(@click.native="copyUserURL(userDialog.id)") {{ $t('dialog.user.info.copy_url') }} + el-dropdown-item(@click.native="copyUserDisplayName(userDialog.ref.displayName)") {{ $t('dialog.user.info.copy_display_name') }} + el-tab-pane(:label="$t('dialog.user.groups.header')") + el-button(type="default" :loading="userDialog.isGroupsLoading" @click="getUserGroups(userDialog.id)" size="mini" icon="el-icon-refresh" circle) + span(style="margin-left:5px") {{ $t('dialog.user.groups.total_count', { count: userGroups.groups.length }) }} + div(v-loading="userDialog.isGroupsLoading" style="margin-top:10px") + template(v-if="userGroups.ownGroups.length > 0") + span(style="font-weight:bold;font-size:16px") {{ $t('dialog.user.groups.own_groups') }} + span(style="color:#909399;font-size:12px;margin-left:5px") {{ userGroups.ownGroups.length }}/{{ API.cachedConfig?.constants?.GROUPS?.MAX_OWNED }} + .x-friend-list(style="margin-top:10px;margin-bottom:15px;min-height:60px") + .x-friend-item(v-for="group in userGroups.ownGroups" :key="group.id" @click="showGroupDialog(group.id)" class="x-friend-item-border") + .avatar + img(v-lazy="group.iconUrl") + .detail + span.name(v-text="group.name") + span.extra + el-tooltip(v-if="group.isRepresenting" placement="top" :content="$t('dialog.group.members.representing')") + i.el-icon-collection-tag(style="margin-right:5px") + el-tooltip(v-if="group.memberVisibility !== 'visible'" placement="top") + template(#content) + span {{ $t('dialog.group.members.visibility') }} {{ group.memberVisibility }} + i.el-icon-view(style="margin-right:5px") + span ({{ group.memberCount }}) + template(v-if="userGroups.mutualGroups.length > 0") + span(style="font-weight:bold;font-size:16px") {{ $t('dialog.user.groups.mutual_groups') }} + span(style="color:#909399;font-size:12px;margin-left:5px") {{ userGroups.mutualGroups.length }} + .x-friend-list(style="margin-top:10px;margin-bottom:15px;min-height:60px") + .x-friend-item(v-for="group in userGroups.mutualGroups" :key="group.id" @click="showGroupDialog(group.id)" class="x-friend-item-border") + .avatar + img(v-lazy="group.iconUrl") + .detail + span.name(v-text="group.name") + span.extra + el-tooltip(v-if="group.isRepresenting" placement="top" :content="$t('dialog.group.members.representing')") + i.el-icon-collection-tag(style="margin-right:5px") + el-tooltip(v-if="group.memberVisibility !== 'visible'" placement="top") + template(#content) + span {{ $t('dialog.group.members.visibility') }} {{ group.memberVisibility }} + i.el-icon-view(style="margin-right:5px") + span ({{ group.memberCount }}) + template(v-if="userGroups.remainingGroups.length > 0") + span(style="font-weight:bold;font-size:16px") {{ $t('dialog.user.groups.groups') }} + span(style="color:#909399;font-size:12px;margin-left:5px") {{ userGroups.remainingGroups.length }} + template(v-if="API.currentUser.id === userDialog.id") + |/ + template(v-if="API.currentUser.$isVRCPlus") + | {{ API.cachedConfig?.constants?.GROUPS?.MAX_JOINED_PLUS }} + template(v-else) + | {{ API.cachedConfig?.constants?.GROUPS?.MAX_JOINED }} + .x-friend-list(style="margin-top:10px;margin-bottom:15px;min-height:60px") + .x-friend-item(v-for="group in userGroups.remainingGroups" :key="group.id" @click="showGroupDialog(group.id)" class="x-friend-item-border") + .avatar + img(v-lazy="group.iconUrl") + .detail + span.name(v-text="group.name") + span.extra + el-tooltip(v-if="group.isRepresenting" placement="top" :content="$t('dialog.group.members.representing')") + i.el-icon-collection-tag(style="margin-right:5px") + el-tooltip(v-if="group.memberVisibility !== 'visible'" placement="top") + template(#content) + span {{ $t('dialog.group.members.visibility') }} {{ group.memberVisibility }} + i.el-icon-view(style="margin-right:5px") + span ({{ group.memberCount }}) + el-tab-pane(:label="$t('dialog.user.worlds.header')") + el-button(type="default" :loading="userDialog.isWorldsLoading" @click="refreshUserDialogWorlds()" size="mini" icon="el-icon-refresh" circle) + span(style="margin-left:5px") {{ $t('dialog.user.worlds.total_count', { count: userDialog.worlds.length }) }} + div(style="float:right") + span(style="margin-right:5px") {{ $t('dialog.user.worlds.sort_by') }} + el-dropdown(@click.native.stop trigger="click" size="small" style="margin-right:5px" :disabled="userDialog.isWorldsLoading") + el-button(size="mini") + span {{ userDialog.worldSorting.name }} #[i.el-icon-arrow-down.el-icon--right] + el-dropdown-menu(#default="dropdown") + el-dropdown-item(v-for="(item) in userDialogWorldSortingOptions" v-text="item.name" @click.native="setUserDialogWorldSorting(item)") + span(style="margin-right:5px") {{ $t('dialog.user.worlds.order_by') }} + el-dropdown(@click.native.stop trigger="click" size="small" style="margin-right:5px" :disabled="userDialog.isWorldsLoading") + el-button(size="mini") + span {{ userDialog.worldOrder.name }} #[i.el-icon-arrow-down.el-icon--right] + el-dropdown-menu(#default="dropdown") + el-dropdown-item(v-for="(item) in userDialogWorldOrderOptions" v-text="item.name" @click.native="setUserDialogWorldOrder(item)") + .x-friend-list(v-loading="userDialog.isWorldsLoading" style="margin-top:10px;min-height:60px") + .x-friend-item(v-for="world in userDialog.worlds" :key="world.id" @click="showWorldDialog(world.id)" class="x-friend-item-border") + .avatar + img(v-lazy="world.thumbnailImageUrl") + .detail + span.name(v-text="world.name") + span.extra(v-if="world.occupants") ({{ world.occupants }}) + el-tab-pane(:label="$t('dialog.user.favorite_worlds.header')") + el-button(type="default" :loading="userDialog.isFavoriteWorldsLoading" @click="getUserFavoriteWorlds(userDialog.id)" size="mini" icon="el-icon-refresh" circle) + el-tabs.zero-margin-tabs(type="card" ref="favoriteWorlds" v-loading="userDialog.isFavoriteWorldsLoading" style="margin-top:10px") + template(v-for="(list, index) in userFavoriteWorlds" v-if="list") + el-tab-pane + span(slot="label") + span(v-text="list[0]" style="font-weight:bold;font-size:16px") + i.x-user-status(style="margin-left:5px" :class="userFavoriteWorldsStatus(list[1])") + span(style="color:#909399;font-size:12px;margin-left:5px") {{ list[2].length }}/{{ API.favoriteLimits.maxFavoritesPerGroup.world }} + .x-friend-list(style="margin-top:10px;margin-bottom:15px;min-height:60px") + .x-friend-item(v-for="world in list[2]" :key="world.id" @click="showWorldDialog(world.id)" class="x-friend-item-border") + .avatar + img(v-lazy="world.thumbnailImageUrl") + .detail + span.name(v-text="world.name") + span.extra(v-if="world.occupants") ({{ world.occupants }}) + el-tab-pane(:label="$t('dialog.user.avatars.header')") + template(v-if="userDialog.ref.id === API.currentUser.id") + el-button(type="default" :loading="userDialog.isAvatarsLoading" @click="refreshUserDialogAvatars()" size="mini" icon="el-icon-refresh" circle) + span(style="margin-left:5px") {{ $t('dialog.user.avatars.total_count', { count: userDialogAvatars.length }) }} + el-radio-group(v-if="userDialog.ref.id === API.currentUser.id" v-model="userDialog.avatarSorting" size="mini" style="margin-left:30px;margin-right:30px" @change="changeUserDialogAvatarSorting") + el-radio(label="name") {{ $t('dialog.user.avatars.sort_by_name') }} + el-radio(label="update") {{ $t('dialog.user.avatars.sort_by_update') }} + el-radio-group(v-if="userDialog.ref.id === API.currentUser.id" v-model="userDialog.avatarReleaseStatus" size="mini" style="margin-left:30px") + el-radio(label="all") {{ $t('dialog.user.avatars.all') }} + el-radio(label="public") {{ $t('dialog.user.avatars.public') }} + el-radio(label="private") {{ $t('dialog.user.avatars.private') }} + .x-friend-list(style="margin-top:10px;min-height:60px") + .x-friend-item(v-for="avatar in userDialogAvatars" @click="showAvatarDialog(avatar.id)" class="x-friend-item-border") + .avatar + img(v-if="avatar.thumbnailImageUrl" v-lazy="avatar.thumbnailImageUrl") + .detail + span.name(v-text="avatar.name") + span.extra(v-text="avatar.releaseStatus" v-if="avatar.releaseStatus === 'public'" style="color: #67c23a;") + span.extra(v-text="avatar.releaseStatus" v-else-if="avatar.releaseStatus === 'private'" style="color: #f56c6c;") + span.extra(v-text="avatar.releaseStatus" v-else) + el-tab-pane(:label="$t('dialog.user.json.header')") + el-button(type="default" @click="refreshUserDialogTreeData()" size="mini" icon="el-icon-refresh" circle) + el-button(type="default" @click="downloadAndSaveJson(userDialog.id, userDialog.ref)" size="mini" icon="el-icon-download" circle style="margin-left:5px") + el-tree(:data="userDialog.treeData" style="margin-top:5px;font-size:12px") + template(#default="scope") + span + span(v-text="scope.data.key" style="font-weight:bold;margin-right:5px") + span(v-if="!scope.data.children" v-text="scope.data.value") diff --git a/html/src/mixins/dialogs/vrcx.pug b/html/src/mixins/dialogs/vrcx.pug new file mode 100644 index 000000000..4699814c3 --- /dev/null +++ b/html/src/mixins/dialogs/vrcx.pug @@ -0,0 +1,57 @@ +mixin vrcx() + //- dialog: Cache Download + el-dialog.x-dialog(:before-close="beforeDialogClose" @mousedown.native="dialogMouseDown" @mouseup.native="dialogMouseUp" ref="downloadDialog" :visible.sync="downloadDialog.visible" :title="$t('dialog.download_history.header')" width="770px") + template(v-if="downloadDialog.visible") + div(v-if="downloadInProgress && downloadCurrent.ref") + span(v-text="downloadCurrent.ref.name") + el-button(type="text" icon="el-icon-close" size="mini" @click="cancelDownload(downloadCurrent.id)" style="margin-left:5px") + el-progress(:percentage="downloadProgress" :format="downloadProgressText") + template(v-if="downloadQueueTable.data.length >= 1") + span(style="margin-top:15px") {{ $t('dialog.download_history.queue') }} + data-tables(v-bind="downloadQueueTable" style="margin-top:10px") + el-table-column(:label="$t('table.download_history.name')" prop="name") + el-table-column(:label="$t('table.download_history.type')" prop="type" width="70") + el-table-column(:label="$t('table.download_history.cancel')" width="60" align="right") + template(v-once #default="scope") + el-button(type="text" icon="el-icon-close" size="mini" @click="cancelDownload(scope.row.ref.id)") + span(style="margin-top:15px") {{ $t('dialog.download_history.history') }} + data-tables(v-bind="downloadHistoryTable" style="margin-top:10px") + el-table-column(:label="$t('table.download_history.time')" prop="date" width="90") + template(v-once #default="scope") + timer(:epoch="scope.row.date") + el-table-column(:label="$t('table.download_history.name')" prop="name") + template(v-once #default="scope") + span(v-text="scope.row.ref.name") + el-table-column(:label="$t('table.download_history.type')" prop="type" width="70") + el-table-column(:label="$t('table.download_history.status')" prop="status" width="80") + template(#footer) + el-button(v-if="downloadQueue.size >= 1" size="small" @click="cancelAllDownloads") {{ $t('dialog.download_history.cancel_all') }} + el-button(size="small" @click="downloadDialog.visible = false") {{ $t('dialog.download_history.close') }} + + //- dialog: update VRCX + el-dialog.x-dialog(:before-close="beforeDialogClose" @mousedown.native="dialogMouseDown" @mouseup.native="dialogMouseUp" ref="VRCXUpdateDialog" :visible.sync="VRCXUpdateDialog.visible" :title="$t('dialog.vrcx_updater.header')" width="400px") + div(v-loading="checkingForVRCXUpdate" style="margin-top:15px") + div(v-if="VRCXUpdateDialog.updatePending" style="margin-bottom:15px") + span(v-text="pendingVRCXInstall") + br + span {{ $t('dialog.vrcx_updater.ready_for_update') }} + el-select(v-model="branch" @change="loadBranchVersions" style="display:inline-block;width:150px;margin-right:15px") + el-option(v-once v-for="branch in branches" :key="branch.name" :label="branch.name" :value="branch.name") + el-select(v-model="VRCXUpdateDialog.release" style="display:inline-block;width:150px") + el-option(v-for="item in VRCXUpdateDialog.releases" :key="item.name" :label="item.tag_name" :value="item.name") + div(v-if="!VRCXUpdateDialog.updatePending && VRCXUpdateDialog.release === appVersion" style="margin-top:15px") + span {{ $t('dialog.vrcx_updater.latest_version') }} + template(#footer) + el-button(v-if="(VRCXUpdateDialog.updatePending && VRCXUpdateDialog.release !== pendingVRCXInstall) || VRCXUpdateDialog.release !== appVersion" type="primary" size="small" @click="installVRCXUpdate") {{ $t('dialog.vrcx_updater.download') }} + el-button(v-if="VRCXUpdateDialog.updatePending" type="primary" size="small" @click="restartVRCX(true)") {{ $t('dialog.vrcx_updater.install') }} + + //- dialog: change log + el-dialog.x-dialog(:before-close="beforeDialogClose" @mousedown.native="dialogMouseDown" @mouseup.native="dialogMouseUp" ref="changeLogDialog" :visible.sync="changeLogDialog.visible" :title="$t('dialog.change_log.header')" width="800px") + .changelog-dialog(v-if="changeLogDialog.visible") + h2(v-text="changeLogDialog.buildName") + span {{ $t('dialog.change_log.description') }} #[a.x-link(@click="openExternalLink('https://www.patreon.com/Natsumi_VRCX')") Patreon], #[a.x-link(@click="openExternalLink('https://ko-fi.com/natsumi_sama')") Ko-fi]. + vue-markdown(:source="changeLogDialog.changeLog" :linkify="false") + template(#footer) + el-button(type="small" @click="openExternalLink('https://github.com/vrcx-team/VRCX/releases')") {{ $t('dialog.change_log.github') }} + el-button(type="small" @click="openExternalLink('https://patreon.com/Natsumi_VRCX')") {{ $t('dialog.change_log.donate') }} + el-button(type="small" @click="changeLogDialog.visible = false") {{ $t('dialog.change_log.close') }} diff --git a/html/src/mixins/dialogs/worldDialog.pug b/html/src/mixins/dialogs/worldDialog.pug new file mode 100644 index 000000000..38e123ee0 --- /dev/null +++ b/html/src/mixins/dialogs/worldDialog.pug @@ -0,0 +1,216 @@ + +mixin worldDialog() + el-dialog.x-dialog.x-world-dialog(:before-close="beforeDialogClose" @mousedown.native="dialogMouseDown" @mouseup.native="dialogMouseUp" ref="worldDialog" :visible.sync="worldDialog.visible" :show-close="false" width="770px") + div(v-loading="worldDialog.loading") + div(style="display:flex") + el-popover(placement="right" width="500px" trigger="click") + img.x-link(slot="reference" v-lazy="worldDialog.ref.thumbnailImageUrl" style="flex:none;width:160px;height:120px;border-radius:12px") + img.x-link(v-lazy="worldDialog.ref.imageUrl" style="width:500px;height:375px" @click="showFullscreenImageDialog(worldDialog.ref.imageUrl)") + div(style="flex:1;display:flex;align-items:center;margin-left:15px") + div(style="flex:1") + div + i.el-icon-s-home(v-show="API.currentUser.$homeLocation && API.currentUser.$homeLocation.worldId === worldDialog.id" style="margin-right:5px") + span.dialog-title(v-text="worldDialog.ref.name") + div(style="margin-top:5px") + span.x-link.x-grey(v-text="worldDialog.ref.authorName" @click="showUserDialog(worldDialog.ref.authorId)" style="font-family:monospace") + div + el-tag(v-if="worldDialog.ref.$isLabs" type="primary" effect="plain" size="mini" style="margin-right:5px;margin-top:5px") {{ $t('dialog.world.tags.labs') }} + el-tag(v-else-if="worldDialog.ref.releaseStatus === 'public'" type="success" effect="plain" size="mini" style="margin-right:5px;margin-top:5px") {{ $t('dialog.world.tags.public') }} + el-tag(v-else type="danger" effect="plain" size="mini" style="margin-right:5px;margin-top:5px") {{ $t('dialog.world.tags.private') }} + el-tag.x-tag-platform-pc(v-if="worldDialog.isPC" type="info" effect="plain" size="mini" style="margin-right:5px;margin-top:5px") PC + span.x-grey(v-if="worldDialog.bundleSizes['standalonewindows']" style=";margin-left:5px;border-left:inherit;padding-left:5px") {{ worldDialog.bundleSizes['standalonewindows'].fileSize }} + el-tag.x-tag-platform-quest(v-if="worldDialog.isQuest" type="info" effect="plain" size="mini" style="margin-right:5px;margin-top:5px") Android + span.x-grey(v-if="worldDialog.bundleSizes['android']" style="margin-left:5px;border-left:inherit;padding-left:5px") {{ worldDialog.bundleSizes['android'].fileSize }} + el-tag.x-tag-platform-ios(v-if="worldDialog.isIos" type="info" effect="plain" size="mini" style="margin-right:5px;margin-top:5px") iOS + span.x-grey(v-if="worldDialog.bundleSizes['ios']" style="margin-left:5px;border-left:inherit;padding-left:5px") {{ worldDialog.bundleSizes['ios'].fileSize }} + el-tag(v-if="worldDialog.avatarScalingDisabled" type="warning" effect="plain" size="mini" style="margin-right:5px;margin-top:5px") {{ $t('dialog.world.tags.avatar_scaling_disabled') }} + el-tag(v-if="worldDialog.focusViewDisabled" type="warning" effect="plain" size="mini" style="margin-right:5px;margin-top:5px") {{ $t('dialog.world.tags.focus_view_disabled') }} + el-tag(v-if="worldDialog.stickersDisabled" type="warning" effect="plain" size="mini" style="margin-right:5px;margin-top:5px") {{ $t('dialog.world.tags.stickers_disabled') }} + el-tag(v-if="worldDialog.ref.unityPackageUrl" type="success" effect="plain" size="mini" style="margin-right:5px;margin-top:5px") {{ $t('dialog.world.tags.future_proofing') }} + el-tag.x-link(v-if="worldDialog.inCache" type="info" effect="plain" size="mini" style="margin-right:5px;margin-top:5px" @click="openFolderGeneric(worldDialog.cachePath)") + span(v-text="worldDialog.cacheSize") + | {{ $t('dialog.world.tags.cache')}} + div + template(v-for="tag in worldDialog.ref.tags") + el-tag(v-if="tag.startsWith('content_')" :key="tag" effect="plain" size="mini" style="margin-right:5px;margin-top:5px") + template(v-if="tag === 'content_horror'") {{ $t('dialog.world.tags.content_horror') }} + template(v-else-if="tag === 'content_gore'") {{ $t('dialog.world.tags.content_gore') }} + template(v-else-if="tag === 'content_violence'") {{ $t('dialog.world.tags.content_violence') }} + template(v-else-if="tag === 'content_adult'") {{ $t('dialog.world.tags.content_adult') }} + template(v-else-if="tag === 'content_sex'") {{ $t('dialog.world.tags.content_sex') }} + template(v-else) {{ tag.replace('content_', '') }} + div(style="margin-top:5px") + span(v-show="worldDialog.ref.name !== worldDialog.ref.description" v-text="worldDialog.ref.description" style="font-size:12px") + div(style="flex:none;margin-left:10px") + el-tooltip(v-if="worldDialog.inCache" placement="top" :content="$t('dialog.world.actions.delete_cache_tooltip')" :disabled="hideTooltips") + el-button(icon="el-icon-delete" circle @click="deleteVRChatCache(worldDialog.ref)" :disabled="isGameRunning && worldDialog.cacheLocked") + el-tooltip(v-if="worldDialog.isFavorite" placement="top" :content="$t('dialog.world.actions.favorites_tooltip')" :disabled="hideTooltips") + el-button(type="default" icon="el-icon-star-on" circle @click="worldDialogCommand('Add Favorite')" style="margin-left:5px") + el-tooltip(v-else placement="top" :content="$t('dialog.world.actions.favorites_tooltip')" :disabled="hideTooltips") + el-button(type="default" icon="el-icon-star-off" circle @click="worldDialogCommand('Add Favorite')" style="margin-left:5px") + el-dropdown(trigger="click" @command="worldDialogCommand" size="small" style="margin-left:5px") + el-button(type="default" icon="el-icon-more" circle) + el-dropdown-menu(#default="dropdown") + el-dropdown-item(icon="el-icon-refresh" command="Refresh") {{ $t('dialog.world.actions.refresh') }} + el-dropdown-item(icon="el-icon-s-flag" command="New Instance" divided) {{ $t('dialog.world.actions.new_instance') }} + el-dropdown-item(v-if="API.currentUser.$homeLocation && API.currentUser.$homeLocation.worldId === worldDialog.id" icon="el-icon-magic-stick" command="Reset Home" divided) {{ $t('dialog.world.actions.reset_home') }} + el-dropdown-item(v-else icon="el-icon-s-home" command="Make Home" divided) {{ $t('dialog.world.actions.make_home') }} + el-dropdown-item(icon="el-icon-tickets" command="Previous Instances") {{ $t('dialog.world.actions.show_previous_instances') }} + template(v-if="API.currentUser.id !== worldDialog.ref.authorId") + el-dropdown-item(icon="el-icon-picture-outline" command="Previous Images") {{ $t('dialog.world.actions.show_previous_images') }} + el-dropdown-item(:disabled="!worldDialog.hasPersistData" icon="el-icon-upload" command="Delete Persistent Data") {{ $t('dialog.world.actions.delete_persistent_data') }} + template(v-else) + el-dropdown-item(icon="el-icon-edit" command="Rename") {{ $t('dialog.world.actions.rename') }} + el-dropdown-item(icon="el-icon-edit" command="Change Description") {{ $t('dialog.world.actions.change_description') }} + el-dropdown-item(icon="el-icon-edit" command="Change Capacity") {{ $t('dialog.world.actions.change_capacity') }} + el-dropdown-item(icon="el-icon-edit" command="Change Recommended Capacity") {{ $t('dialog.world.actions.change_recommended_capacity') }} + el-dropdown-item(icon="el-icon-edit" command="Change YouTube Preview") {{ $t('dialog.world.actions.change_preview') }} + el-dropdown-item(icon="el-icon-edit" command="Change Tags") {{ $t('dialog.world.actions.change_tags') }} + el-dropdown-item(icon="el-icon-picture-outline" command="Change Image") {{ $t('dialog.world.actions.change_image') }} + el-dropdown-item(v-if="worldDialog.ref.unityPackageUrl" icon="el-icon-download" command="Download Unity Package") {{ $t('dialog.world.actions.download_package') }} + el-dropdown-item(v-if="worldDialog.ref.tags.includes('system_approved') || worldDialog.ref.tags.includes('system_labs')" icon="el-icon-view" command="Unpublish" divided) {{ $t('dialog.world.actions.unpublish') }} + el-dropdown-item(v-else icon="el-icon-view" command="Publish" divided) {{ $t('dialog.world.actions.publish_to_labs') }} + el-dropdown-item(:disabled="!worldDialog.hasPersistData" icon="el-icon-upload" command="Delete Persistent Data") {{ $t('dialog.world.actions.delete_persistent_data') }} + el-dropdown-item(icon="el-icon-delete" command="Delete" style="color:#F56C6C") {{ $t('dialog.world.actions.delete') }} + el-tabs + el-tab-pane(:label="$t('dialog.world.instances.header')") + div. + #[i.el-icon-user] {{ $t('dialog.world.instances.public_count', { count: worldDialog.ref.publicOccupants }) }} + #[i.el-icon-user-solid(style="margin-left:10px")] {{ $t('dialog.world.instances.private_count', { count: worldDialog.ref.privateOccupants }) }} + #[i.el-icon-check(style="margin-left:10px")] {{ $t('dialog.world.instances.capacity_count', { count: worldDialog.ref.recommendedCapacity, max: worldDialog.ref.capacity }) }} + div(v-for="room in worldDialog.rooms" :key="room.id") + div(style="margin:5px 0") + location-world(:locationobject="room.$location" :currentuserid="API.currentUser.id" :worlddialogshortname="worldDialog.$location.shortName") + el-tooltip(placement="top" :content="$t('dialog.world.instances.self_invite_tooltip')" :disabled="hideTooltips") + invite-yourself(:location="room.$location.tag" :shortname="room.$location.shortName" style="margin-left:5px") + el-tooltip(placement="top" :content="$t('dialog.world.instances.refresh_instance_info')" :disabled="hideTooltips") + el-button(@click="refreshInstancePlayerCount(room.tag)" size="mini" icon="el-icon-refresh" style="margin-left:5px" circle) + last-join(:location="room.$location.tag" :currentlocation="lastLocation.location") + instance-info(:location="room.tag" :instance="room.ref" :friendcount="room.friendCount" :updateelement="updateInstanceInfo") + .x-friend-list(style="margin:10px 0;max-height:unset" v-if="room.$location.userId || room.users.length") + .x-friend-item(v-if="room.$location.userId" @click="showUserDialog(room.$location.userId)" class="x-friend-item-border") + template(v-if="room.$location.user") + .avatar(:class="userStatusClass(room.$location.user)") + img(v-lazy="userImage(room.$location.user)") + .detail + span.name(v-text="room.$location.user.displayName" :style="{'color':room.$location.user.$userColour}") + span.extra {{ $t('dialog.world.instances.instance_creator') }} + span(v-else v-text="room.$location.userId") + .x-friend-item(v-for="user in room.users" :key="user.id" @click="showUserDialog(user.id)" class="x-friend-item-border") + .avatar(:class="userStatusClass(user)") + img(v-lazy="userImage(user)") + .detail + span.name(v-text="user.displayName" :style="{'color':user.$userColour}") + span.extra(v-if="user.location === 'traveling'") + i.el-icon-loading(style="margin-right:5px") + timer(:epoch="user.$travelingToTime") + span.extra(v-else) + timer(:epoch="user.$location_at") + el-tab-pane(:label="$t('dialog.world.info.header')") + .x-friend-list(style="max-height:none") + .x-friend-item(style="width:100%;cursor:default") + .detail + span.name {{ $t('dialog.world.info.memo') }} + el-input.extra(v-model="worldDialog.memo" @change="onWorldMemoChange" type="textarea" :rows="2" :autosize="{ minRows: 1, maxRows: 20 }" :placeholder="$t('dialog.world.info.memo_placeholder')" size="mini" resize="none") + div(style="width:100%;display:flex") + .x-friend-item(style="width:100%;cursor:default") + .detail + span.name {{ $t('dialog.world.info.id') }} + span.extra {{ worldDialog.id }} + el-tooltip(placement="top" :content="$t('dialog.world.info.id_tooltip')" :disabled="hideTooltips") + el-dropdown(trigger="click" @click.native.stop size="mini" style="margin-left:5px") + el-button(type="default" icon="el-icon-s-order" size="mini" circle) + el-dropdown-menu(#default="dropdown") + el-dropdown-item(@click.native="copyWorldId(worldDialog.id)") {{ $t('dialog.world.info.copy_id') }} + el-dropdown-item(@click.native="copyWorldUrl(worldDialog.id)") {{ $t('dialog.world.info.copy_url') }} + el-dropdown-item(@click.native="copyWorldName(worldDialog.ref.name)") {{ $t('dialog.world.info.copy_name') }} + .x-friend-item(v-if="worldDialog.ref.previewYoutubeId" style="width:350px" @click="openExternalLink(`https://www.youtube.com/watch?v=${worldDialog.ref.previewYoutubeId}`)") + .detail + span.name {{ $t('dialog.world.info.youtube_preview') }} + span.extra https://www.youtube.com/watch?v={{ worldDialog.ref.previewYoutubeId }} + .x-friend-item(style="width:100%;cursor:default") + .detail + span.name {{ $t('dialog.world.info.author_tags') }} + span.extra(v-if="worldDialog.ref.tags?.filter(tag => tag.startsWith('author_tag')).length > 0") {{ worldDialog.ref.tags.filter(tag => tag.startsWith('author_tag')).map(tag => tag.replace('author_tag_', '')).join(', ') }} + span.extra(v-else) - + .x-friend-item(style="cursor:default") + .detail + span.name {{ $t('dialog.world.info.players') }} + span.extra {{ worldDialog.ref.occupants | commaNumber }} + .x-friend-item(style="cursor:default") + .detail + span.name {{ $t('dialog.world.info.favorites') }} + span.extra {{ worldDialog.ref.favorites | commaNumber }} + | #[template(v-if="worldDialog.ref.favorites > 0 && worldDialog.ref.visits > 0") ({{ Math.round(((worldDialog.ref.favorites - worldDialog.ref.visits) / worldDialog.ref.visits * 100 + 100) * 100) / 100 }}%)] + .x-friend-item(style="cursor:default") + .detail + span.name {{ $t('dialog.world.info.visits') }} + span.extra {{ worldDialog.ref.visits | commaNumber }} + .x-friend-item(style="cursor:default") + .detail + span.name {{ $t('dialog.world.info.capacity') }} + span.extra {{ worldDialog.ref.recommendedCapacity | commaNumber }} ({{ worldDialog.ref.capacity | commaNumber }}) + .x-friend-item(style="cursor:default") + .detail + span.name {{ $t('dialog.world.info.created_at') }} + span.extra {{ worldDialog.ref.created_at | formatDate('long') }} + .x-friend-item(style="cursor:default") + .detail + span.name {{ $t('dialog.world.info.last_updated') }} + span.extra(v-if="worldDialog.lastUpdated") {{ worldDialog.lastUpdated | formatDate('long') }} + span.extra(v-else) {{ worldDialog.ref.updated_at | formatDate('long') }} + .x-friend-item(v-if="worldDialog.ref.labsPublicationDate !== 'none'" style="cursor:default") + .detail + span.name {{ $t('dialog.world.info.labs_publication_date') }} + span.extra {{ worldDialog.ref.labsPublicationDate | formatDate('long') }} + .x-friend-item(v-if="worldDialog.ref.publicationDate !== 'none'" style="cursor:default") + .detail + span.name {{ $t('dialog.world.info.publication_date') }} + el-tooltip(v-if="worldDialog.ref.publicationDate && worldDialog.ref.publicationDate !== 'none' && worldDialog.ref.labsPublicationDate && worldDialog.ref.labsPublicationDate !== 'none'" placement="top" style="margin-left:5px") + template(#content) + span {{ $t('dialog.world.info.time_in_labs') }} {{ timeToText(new Date(worldDialog.ref.publicationDate) - new Date(worldDialog.ref.labsPublicationDate)) }} + i.el-icon-arrow-down + span.extra {{ worldDialog.ref.publicationDate | formatDate('long') }} + .x-friend-item(style="cursor:default") + .detail + span.name {{ $t('dialog.world.info.version') }} + span.extra(v-text="worldDialog.ref.version") + .x-friend-item(style="cursor:default") + .detail + span.name {{ $t('dialog.world.info.heat') }} + span.extra {{ worldDialog.ref.heat | commaNumber }} {{ '🔥'.repeat(worldDialog.ref.heat) }} + .x-friend-item(style="cursor:default") + .detail + span.name {{ $t('dialog.world.info.popularity') }} + span.extra {{ worldDialog.ref.popularity | commaNumber }} {{ '💖'.repeat(worldDialog.ref.popularity) }} + .x-friend-item(style="width:100%;cursor:default") + .detail + span.name {{ $t('dialog.world.info.platform') }} + span.extra(v-text="worldDialogPlatform") + .x-friend-item(style="cursor:default") + .detail + span.name {{ $t('dialog.world.info.last_visited') }} + el-tooltip(v-if="!hideTooltips" placement="top" style="margin-left:5px" :content="$t('dialog.world.info.accuracy_notice')") + i.el-icon-warning + span.extra {{ worldDialog.lastVisit | formatDate('long') }} + .x-friend-item(@click="showPreviousInstancesWorldDialog(worldDialog.ref)") + .detail + span.name {{ $t('dialog.world.info.visit_count') }} + el-tooltip(v-if="!hideTooltips" placement="top" style="margin-left:5px" :content="$t('dialog.world.info.accuracy_notice')") + i.el-icon-warning + span.extra(v-text="worldDialog.visitCount") + .x-friend-item(style="cursor:default") + .detail + span.name {{ $t('dialog.world.info.time_spent') }} + el-tooltip(v-if="!hideTooltips" placement="top" style="margin-left:5px" :content="$t('dialog.world.info.accuracy_notice')") + i.el-icon-warning + span.extra(v-if="worldDialog.timeSpent === 0") - + span.extra(v-else) {{ timeToText(worldDialog.timeSpent) }} + el-tab-pane(:label="$t('dialog.world.json.header')") + el-button(type="default" @click="refreshWorldDialogTreeData()" size="mini" icon="el-icon-refresh" circle) + el-button(type="default" @click="downloadAndSaveJson(worldDialog.id, worldDialog.ref)" size="mini" icon="el-icon-download" circle style="margin-left:5px") + el-tree(:data="worldDialog.treeData" style="margin-top:5px;font-size:12px") + template(#default="scope") + span + span(v-text="scope.data.key" style="font-weight:bold;margin-right:5px") + span(v-if="!scope.data.children" v-text="scope.data.value") diff --git a/html/src/mixins/friendsListSidebar.pug b/html/src/mixins/friendsListSidebar.pug new file mode 100644 index 000000000..00d9d4542 --- /dev/null +++ b/html/src/mixins/friendsListSidebar.pug @@ -0,0 +1,113 @@ + +mixin friendsListSidebar() + .x-aside-container(v-show="$refs.menu && $refs.menu.activeIndex !== 'friendsList'" id="aside") + div(style="display:flex;align-items:baseline") + el-select(v-model="quickSearch" clearable :placeholder="$t('side_panel.search_placeholder')" filterable remote :remote-method="quickSearchRemoteMethod" popper-class="x-quick-search" @change="quickSearchChange" @visible-change="quickSearchVisibleChange" style="flex:1;padding:10px") + el-option(v-for="item in quickSearchItems" :key="item.value" :value="item.value" :label="item.label") + .x-friend-item + template(v-if="item.ref") + .detail + span.name(v-text="item.ref.displayName" :style="{'color':item.ref.$userColour}") + span.extra(v-if="!item.ref.isFriend") + span.extra(v-else-if="item.ref.state === 'offline'") {{ $t('side_panel.search_result_active') }} + span.extra(v-else-if="item.ref.state === 'active'") {{ $t('side_panel.search_result_offline') }} + location.extra(v-else :location="item.ref.location" :traveling="item.ref.travelingToLocation" :link="false") + img.avatar(v-lazy="userImage(item.ref)") + span(v-else) {{ $t('side_panel.search_result_more') }} #[span(v-text="item.label" style="font-weight:bold")] + el-tooltip(placement="bottom" :content="$t('side_panel.direct_access_tooltip')" :disabled="hideTooltips") + el-button(type="default" @click="directAccessPaste" size="mini" icon="el-icon-discover" circle) + el-tooltip(placement="bottom" :content="$t('side_panel.refresh_tooltip')" :disabled="hideTooltips") + el-button(type="default" @click="refreshFriendsList" :loading="API.isRefreshFriendsLoading" size="mini" icon="el-icon-refresh" circle style="margin-right:10px") + el-tabs.zero-margin-tabs(stretch="true" style="height:calc(100% - 60px;margin-top:5px") + el-tab-pane + template(#label) + span {{ $t('side_panel.friends') }} + span(style="color:#909399;font-size:12px;margin-left:10px") ({{ onlineFriendCount }}/{{ friends.size }}) + .x-friend-list(style="padding:10px 5px") + .x-friend-group.x-link(@click="isFriendsGroupMe = !isFriendsGroupMe; saveFriendsGroupStates()" style="padding:0px 0px 5px") + i.el-icon-arrow-right(:class="{ rotate: isFriendsGroupMe }") + span(style="margin-left:5px") {{ $t('side_panel.me') }} + div(v-show="isFriendsGroupMe") + .x-friend-item(:key="API.currentUser.id" @click="showUserDialog(API.currentUser.id)") + .avatar(:class="userStatusClass(API.currentUser)") + img(v-lazy="userImage(API.currentUser)") + .detail + span.name(v-text="API.currentUser.displayName" :style="{'color':API.currentUser.$userColour}") + location.extra(v-if="isGameRunning && !gameLogDisabled" :location="lastLocation.location" :traveling="lastLocationDestination" :link="false") + location.extra(v-else-if="isRealInstance(API.currentUser.$locationTag) || isRealInstance(API.currentUser.$travelingToLocation)" :location="API.currentUser.$locationTag" :traveling="API.currentUser.$travelingToLocation" :link="false") + span.extra(v-else v-text="API.currentUser.statusDescription") + .x-friend-group.x-link(@click="isVIPFriends = !isVIPFriends; saveFriendsGroupStates()" v-show="vipFriends.length") + i.el-icon-arrow-right(:class="{ rotate: isVIPFriends }") + span(style="margin-left:5px") {{ $t('side_panel.favorite') }} ― {{ vipFriends.length }} + div(v-show="isVIPFriends") + .x-friend-item(v-for="friend in vipFriends" :key="friend.id" @click="showUserDialog(friend.id)") + template(v-if="friend.ref") + .avatar(:class="userStatusClass(friend.ref, friend.pendingOffline)") + img(v-lazy="userImage(friend.ref)") + .detail + span.name(v-if="!hideNicknames && friend.$nickName" :style="{'color':friend.ref.$userColour}") {{ friend.ref.displayName }} ({{ friend.$nickName }}) + span.name(v-else v-text="friend.ref.displayName" :style="{'color':friend.ref.$userColour}") + span.extra(v-if="friend.pendingOffline") #[i.el-icon-warning-outline] {{ $t('side_panel.pending_offline') }} + location.extra(v-else :location="friend.ref.location" :traveling="friend.ref.travelingToLocation" :link="false") + template(v-else) + span(v-text="friend.name || friend.id") + el-button(type="text" icon="el-icon-close" size="mini" @click.stop="confirmDeleteFriend(friend.id)" style="margin-left:5px") + .x-friend-group.x-link(@click="isOnlineFriends = !isOnlineFriends; saveFriendsGroupStates()" v-show="onlineFriends.length") + i.el-icon-arrow-right(:class="{ rotate: isOnlineFriends }") + span(style="margin-left:5px") {{ $t('side_panel.online') }} ― {{ onlineFriends.length }} + div(v-show="isOnlineFriends") + .x-friend-item(v-for="friend in onlineFriends" :key="friend.id" @click="showUserDialog(friend.id)") + template(v-if="friend.ref") + .avatar(:class="userStatusClass(friend.ref, friend.pendingOffline)") + img(v-lazy="userImage(friend.ref)") + .detail + span.name(v-if="!hideNicknames && friend.$nickName" :style="{'color':friend.ref.$userColour}") {{ friend.ref.displayName }} ({{ friend.$nickName }}) + span.name(v-else v-text="friend.ref.displayName" :style="{'color':friend.ref.$userColour}") + span.extra(v-if="friend.pendingOffline") #[i.el-icon-warning-outline] {{ $t('side_panel.pending_offline') }} + location.extra(v-else :location="friend.ref.location" :traveling="friend.ref.travelingToLocation" :link="false") + template(v-else) + span(v-text="friend.name || friend.id") + el-button(type="text" icon="el-icon-close" size="mini" @click.stop="confirmDeleteFriend(friend.id)" style="margin-left:5px") + .x-friend-group.x-link(@click="isActiveFriends = !isActiveFriends; saveFriendsGroupStates()" v-show="activeFriends.length") + i.el-icon-arrow-right(:class="{ rotate: isActiveFriends }") + span(style="margin-left:5px") {{ $t('side_panel.active') }} ― {{ activeFriends.length }} + div(v-show="isActiveFriends") + .x-friend-item(v-for="friend in activeFriends" :key="friend.id" @click="showUserDialog(friend.id)") + template(v-if="friend.ref") + .avatar + img(v-lazy="userImage(friend.ref)") + .detail + span.name(v-if="!hideNicknames && friend.$nickName" :style="{'color':friend.ref.$userColour}") {{ friend.ref.displayName }} ({{ friend.$nickName }}) + span.name(v-else v-text="friend.ref.displayName" :style="{'color':friend.ref.$userColour}") + span.extra(v-text="friend.ref.statusDescription" :link="false") + template(v-else) + span(v-text="friend.name || friend.id") + el-button(type="text" icon="el-icon-close" size="mini" @click.stop="confirmDeleteFriend(friend.id)" style="margin-left:5px") + .x-friend-group.x-link(@click="isOfflineFriends = !isOfflineFriends; saveFriendsGroupStates()" v-show="offlineFriends.length") + i.el-icon-arrow-right(:class="{ rotate: isOfflineFriends }") + span(style="margin-left:5px") {{ $t('side_panel.offline') }} ― {{ offlineFriends.length }} + div(v-show="isOfflineFriends") + .x-friend-item(v-for="friend in offlineFriends" :key="friend.id" @click="showUserDialog(friend.id)") + template(v-if="friend.ref") + .avatar + img(v-lazy="userImage(friend.ref)") + .detail + span.name(v-if="!hideNicknames && friend.$nickName" :style="{'color':friend.ref.$userColour}") {{ friend.ref.displayName }} ({{ friend.$nickName }}) + span.name(v-else v-text="friend.ref.displayName" :style="{'color':friend.ref.$userColour}") + span.extra(v-text="friend.ref.statusDescription") + template(v-else) + span(v-text="friend.name || friend.id") + el-button(type="text" icon="el-icon-close" size="mini" @click.stop="confirmDeleteFriend(friend.id)" style="margin-left:5px") + el-tab-pane + template(#label) + span {{ $t('side_panel.groups') }} + span(style="color:#909399;font-size:12px;margin-left:10px") ({{ groupInstances.length }}) + .x-friend-list(style="padding:10px 5px") + .x-friend-item(v-for="ref in groupInstances" :key="ref.instance.id" @click="showGroupDialog(ref.instance.ownerId)") + .avatar + img(v-lazy="ref.group.iconUrl") + .detail + span.name + span(v-text="ref.group.name") + span(style="font-weight:normal;margin-left:5px") ({{ ref.instance.userCount }}/{{ ref.instance.capacity }}) + location.extra(:location="ref.instance.location" :link="false") diff --git a/html/src/mixins/tabs/feed.pug b/html/src/mixins/tabs/feed.pug index 3b58e8d7e..0a345eff2 100644 --- a/html/src/mixins/tabs/feed.pug +++ b/html/src/mixins/tabs/feed.pug @@ -9,14 +9,12 @@ mixin feedTab() el-select(v-model="feedTable.filter" @change="feedTableLookup" multiple clearable collapse-tags style="flex:1" :placeholder="$t('view.feed.filter_placeholder')") el-option(v-once v-for="type in ['GPS', 'Online', 'Offline', 'Status', 'Avatar', 'Bio']" :key="type" :label="type" :value="type") el-input(v-model="feedTable.search" :placeholder="$t('view.feed.search_placeholder')" @keyup.native.13="feedTableLookup" @change="feedTableLookup" clearable style="flex:none;width:150px;margin:0 10px") - //- el-tooltip(placement="bottom" content="Clear feed" :disabled="hideTooltips") - //- el-button(type="default" @click="clearFeed()" icon="el-icon-delete" circle style="flex:none") el-table-column(type="expand" width="20") template(v-once #default="scope") div(style="position:relative;font-size:14px") template(v-if="scope.row.type === 'GPS'") location(v-if="scope.row.previousLocation" :location="scope.row.previousLocation") - el-tag(type="info" effect="plain" size="mini" style="margin-left:5px") {{ scope.row.time | timeToText }} + el-tag(type="info" effect="plain" size="mini" style="margin-left:5px") {{ timeToText(scope.row.time) }} br span i.el-icon-right @@ -24,7 +22,7 @@ mixin feedTab() template(v-else-if="scope.row.type === 'Offline'") template(v-if="scope.row.location") location(:location="scope.row.location" :hint="scope.row.worldName" :grouphint="scope.row.groupName") - el-tag(type="info" effect="plain" size="mini" style="margin-left:5px") {{ scope.row.time | timeToText }} + el-tag(type="info" effect="plain" size="mini" style="margin-left:5px") {{ timeToText(scope.row.time) }} template(v-else-if="scope.row.type === 'Online'") location(v-if="scope.row.location" :location="scope.row.location" :hint="scope.row.worldName" :grouphint="scope.row.groupName") template(v-else-if="scope.row.type === 'Avatar'") diff --git a/html/src/mixins/tabs/friendsList.pug b/html/src/mixins/tabs/friendsList.pug index 9b4f3860c..d06989db3 100644 --- a/html/src/mixins/tabs/friendsList.pug +++ b/html/src/mixins/tabs/friendsList.pug @@ -67,7 +67,7 @@ mixin friendsListTab() el-table-column(:label="$t('table.friendList.joinCount')" width="120" prop="$joinCount" sortable) el-table-column(:label="$t('table.friendList.timeTogether')" width="140" prop="$timeSpent" sortable) template(v-once #default="scope") - span(v-if="scope.row.$timeSpent") {{ scope.row.$timeSpent | timeToText }} + span(v-if="scope.row.$timeSpent") {{ timeToText(scope.row.$timeSpent) }} el-table-column(:label="$t('table.friendList.lastSeen')" width="170" prop="$lastSeen" sortable :sort-method="(a, b) => sortAlphabetically(a, b, '$lastSeen')") template(v-once #default="scope") span {{ scope.row.$lastSeen | formatDate('long') }} diff --git a/html/src/mixins/tabs/gameLog.pug b/html/src/mixins/tabs/gameLog.pug index 8653e769d..af45f62e3 100644 --- a/html/src/mixins/tabs/gameLog.pug +++ b/html/src/mixins/tabs/gameLog.pug @@ -6,8 +6,6 @@ mixin gameLogTab() el-select(v-model="gameLogTable.filter" @change="gameLogTableLookup" multiple clearable collapse-tags style="flex:1" :placeholder="$t('view.game_log.filter_placeholder')") el-option(v-once v-for="type in ['Location', 'OnPlayerJoined', 'OnPlayerLeft', 'PortalSpawn', 'VideoPlay', 'Event', 'External', 'StringLoad', 'ImageLoad']" :key="type" :label="type" :value="type") el-input(v-model="gameLogTable.search" :placeholder="$t('view.game_log.search_placeholder')" @keyup.native.13="gameLogTableLookup" @change="gameLogTableLookup" clearable style="flex:none;width:150px;margin:0 10px") - //- el-tooltip(placement="bottom" content="Reload game log" :disabled="hideTooltips") - //- el-button(type="default" @click="resetGameLog" icon="el-icon-refresh" circle style="flex:none") el-table-column(:label="$t('table.gameLog.date')" prop="created_at" sortable="custom" width="120") template(v-once #default="scope") el-tooltip(placement="right") diff --git a/html/src/repository/database.js b/html/src/repository/database.js index 53e0bec9e..0b6b78bd5 100644 --- a/html/src/repository/database.js +++ b/html/src/repository/database.js @@ -186,7 +186,7 @@ class Database { // memos - async getMemo(userId) { + async getUserMemo(userId) { var row = {}; await sqliteService.execute( (dbRow) => { @@ -204,7 +204,7 @@ class Database { return row; } - async getAllMemos() { + async getAllUserMemos() { var memos = []; await sqliteService.execute((dbRow) => { var row = { @@ -216,7 +216,7 @@ class Database { return memos; } - setMemo(entry) { + setUserMemo(entry) { sqliteService.executeNonQuery( `INSERT OR REPLACE INTO memos (user_id, edited_at, memo) VALUES (@user_id, @edited_at, @memo)`, { @@ -227,7 +227,7 @@ class Database { ); } - deleteMemo(userId) { + deleteUserMemo(userId) { sqliteService.executeNonQuery( `DELETE FROM memos WHERE user_id = @user_id`, { diff --git a/html/src/vr.js b/html/src/vr.js index afe835162..a553ac52b 100644 --- a/html/src/vr.js +++ b/html/src/vr.js @@ -1,4 +1,4 @@ -// Copyright(c) 2019-2022 pypy, Natsumi and individual contributors. +// Copyright(c) 2019-2024 pypy, Natsumi and individual contributors. // All rights reserved. // // This work is licensed under the terms of the MIT license. @@ -15,10 +15,14 @@ import ElementUI from 'element-ui'; import * as workerTimers from 'worker-timers'; import MarqueeText from 'vue-marquee-text-component'; import * as localizedStrings from './localization/localizedStrings.js'; + +import _utils from './classes/utils.js'; + Vue.component('marquee-text', MarqueeText); (async function () { - var $app = null; + const $utils = new _utils().$utils; + let $app = {}; await CefSharp.BindObjectAsync('AppApiVr'); @@ -32,76 +36,19 @@ Vue.component('marquee-text', MarqueeText); timeout: 3000 }); + // localization Vue.use(VueI18n); - - var i18n = new VueI18n({ + const i18n = new VueI18n({ locale: 'en', fallbackLocale: 'en', messages: localizedStrings }); - - // var $t = i18n.t.bind(i18n); - + // eslint-disable-next-line no-unused-vars + const $t = i18n.t.bind(i18n); Vue.use(ElementUI, { i18n: (key, value) => i18n.t(key, value) }); - var escapeTag = (s) => - String(s).replace(/["&'<>]/gu, (c) => `&#${c.charCodeAt(0)};`); - Vue.filter('escapeTag', escapeTag); - - var escapeTagRecursive = (obj) => { - if (typeof obj === 'string') { - return escapeTag(obj); - } - if (typeof obj === 'object') { - for (var key in obj) { - obj[key] = escapeTagRecursive(obj[key]); - } - } - return obj; - }; - - var commaNumber = (n) => - String(Number(n) || 0).replace(/(\d)(?=(\d{3})+(?!\d))/gu, '$1,'); - Vue.filter('commaNumber', commaNumber); - - var textToHex = (s) => - String(s) - .split('') - .map((c) => c.charCodeAt(0).toString(16)) - .join(' '); - Vue.filter('textToHex', textToHex); - - var timeToText = function (sec) { - var n = Number(sec); - if (isNaN(n)) { - return escapeTag(sec); - } - n = Math.floor(n / 1000); - var arr = []; - if (n < 0) { - n = -n; - } - if (n >= 86400) { - arr.push(`${Math.floor(n / 86400)}d`); - n %= 86400; - } - if (n >= 3600) { - arr.push(`${Math.floor(n / 3600)}h`); - n %= 3600; - } - if (n >= 60) { - arr.push(`${Math.floor(n / 60)}m`); - n %= 60; - } - if (arr.length === 0 && n < 60) { - arr.push(`${n}s`); - } - return arr.join(' '); - }; - Vue.filter('timeToText', timeToText); - Vue.component('location', { template: '{{ text }}' + @@ -130,7 +77,7 @@ Vue.component('marquee-text', MarqueeText); methods: { parse() { this.text = this.location; - var L = $app.parseLocation(this.location); + var L = $utils.parseLocation(this.location); if (L.isOffline) { this.text = 'Offline'; } else if (L.isPrivate) { @@ -176,18 +123,7 @@ Vue.component('marquee-text', MarqueeText); } }); - var removeFromArray = function (array, item) { - var { length } = array; - for (var i = 0; i < length; ++i) { - if (array[i] === item) { - array.splice(i, 1); - return true; - } - } - return false; - }; - - var $app = { + const app = { i18n, data: { // 1 = 대시보드랑 손목에 보이는거 @@ -228,7 +164,9 @@ Vue.component('marquee-text', MarqueeText); deviceCount: 0 }, computed: {}, - methods: {}, + methods: { + ...$utils + }, watch: {}, el: '#x-app', mounted() { @@ -239,113 +177,7 @@ Vue.component('marquee-text', MarqueeText); } } }; - - $app.methods.parseLocation = function (tag) { - var _tag = String(tag || ''); - var ctx = { - tag: _tag, - isOffline: false, - isPrivate: false, - isTraveling: false, - worldId: '', - instanceId: '', - instanceName: '', - accessType: '', - accessTypeName: '', - region: '', - shortName: '', - userId: null, - hiddenId: null, - privateId: null, - friendsId: null, - groupId: null, - groupAccessType: null, - canRequestInvite: false, - strict: false - }; - if (_tag === 'offline' || _tag === 'offline:offline') { - ctx.isOffline = true; - } else if (_tag === 'private' || _tag === 'private:private') { - ctx.isPrivate = true; - } else if (_tag === 'traveling' || _tag === 'traveling:traveling') { - ctx.isTraveling = true; - } else if (_tag.startsWith('local') === false) { - var sep = _tag.indexOf(':'); - // technically not part of instance id, but might be there when coping id from url so why not support it - var shortNameQualifier = '&shortName='; - var shortNameIndex = _tag.indexOf(shortNameQualifier); - if (shortNameIndex >= 0) { - ctx.shortName = _tag.substr( - shortNameIndex + shortNameQualifier.length - ); - _tag = _tag.substr(0, shortNameIndex); - } - if (sep >= 0) { - ctx.worldId = _tag.substr(0, sep); - ctx.instanceId = _tag.substr(sep + 1); - ctx.instanceId.split('~').forEach((s, i) => { - if (i) { - var A = s.indexOf('('); - var Z = A >= 0 ? s.lastIndexOf(')') : -1; - var key = Z >= 0 ? s.substr(0, A) : s; - var value = A < Z ? s.substr(A + 1, Z - A - 1) : ''; - if (key === 'hidden') { - ctx.hiddenId = value; - } else if (key === 'private') { - ctx.privateId = value; - } else if (key === 'friends') { - ctx.friendsId = value; - } else if (key === 'canRequestInvite') { - ctx.canRequestInvite = true; - } else if (key === 'region') { - ctx.region = value; - } else if (key === 'group') { - ctx.groupId = value; - } else if (key === 'groupAccessType') { - ctx.groupAccessType = value; - } else if (key === 'strict') { - ctx.strict = true; - } - } else { - ctx.instanceName = s; - } - }); - ctx.accessType = 'public'; - if (ctx.privateId !== null) { - if (ctx.canRequestInvite) { - // InvitePlus - ctx.accessType = 'invite+'; - } else { - // InviteOnly - ctx.accessType = 'invite'; - } - ctx.userId = ctx.privateId; - } else if (ctx.friendsId !== null) { - // FriendsOnly - ctx.accessType = 'friends'; - ctx.userId = ctx.friendsId; - } else if (ctx.hiddenId !== null) { - // FriendsOfGuests - ctx.accessType = 'friends+'; - ctx.userId = ctx.hiddenId; - } else if (ctx.groupId !== null) { - // Group - ctx.accessType = 'group'; - } - ctx.accessTypeName = ctx.accessType; - if (ctx.groupAccessType !== null) { - if (ctx.groupAccessType === 'public') { - ctx.accessTypeName = 'groupPublic'; - } else if (ctx.groupAccessType === 'plus') { - ctx.accessTypeName = 'groupPlus'; - } - } - } else { - ctx.worldId = _tag; - } - } - return ctx; - }; + Object.assign($app, app); $app.methods.configUpdate = function (json) { this.config = JSON.parse(json); @@ -454,14 +286,14 @@ Vue.component('marquee-text', MarqueeText); this.cpuUsage = cpuUsage.toFixed(0); } if (this.lastLocation.date !== 0) { - this.lastLocationTimer = timeToText( + this.lastLocationTimer = $utils.timeToText( Date.now() - this.lastLocation.date ); } else { this.lastLocationTimer = ''; } if (this.lastLocation.onlineFor) { - this.onlineForTimer = timeToText( + this.onlineForTimer = $utils.timeToText( Date.now() - this.lastLocation.onlineFor ); } else { @@ -520,7 +352,7 @@ Vue.component('marquee-text', MarqueeText); if (this.config.pcUptimeOnFeed) { AppApiVr.GetUptime().then((uptime) => { if (uptime) { - this.pcUptime = timeToText(uptime); + this.pcUptime = $utils.timeToText(uptime); } }); } else { @@ -538,8 +370,8 @@ Vue.component('marquee-text', MarqueeText); console.error('noty is undefined'); return; } - var noty = escapeTagRecursive(noty); - var message = escapeTag(message) || ''; + var noty = $utils.escapeTagRecursive(noty); + var message = $utils.escapeTag(message) || ''; var text = ''; var img = ''; if (image) { @@ -727,25 +559,6 @@ Vue.component('marquee-text', MarqueeText); return style; }; - $app.methods.displayLocation = function (location, worldName, groupName) { - var text = worldName; - var L = this.parseLocation(location); - if (L.isOffline) { - text = 'Offline'; - } else if (L.isPrivate) { - text = 'Private'; - } else if (L.isTraveling) { - text = 'Traveling'; - } else if (L.worldId) { - if (groupName) { - text = `${worldName} ${L.accessTypeName}(${groupName})`; - } else if (L.instanceId) { - text = `${worldName} ${L.accessTypeName}`; - } - } - return escapeTag(text); - }; - $app.methods.notyClear = function () { Noty.closeAll(); }; @@ -769,7 +582,7 @@ Vue.component('marquee-text', MarqueeText); var dt = Date.now(); this.hudFeed.forEach((item) => { if (item.time + this.config.photonOverlayMessageTimeout < dt) { - removeFromArray(this.hudFeed, item); + $utils.removeFromArray(this.hudFeed, item); } }); if (this.hudFeed.length > 10) { @@ -790,7 +603,7 @@ Vue.component('marquee-text', MarqueeText); item.text === data.text ) { combo = item.combo + 1; - removeFromArray(this.hudFeed, item); + $utils.removeFromArray(this.hudFeed, item); } }); this.hudFeed.unshift({