diff --git a/Dotnet/LogWatcher.cs b/Dotnet/LogWatcher.cs index 62341d8b..b0b4213c 100644 --- a/Dotnet/LogWatcher.cs +++ b/Dotnet/LogWatcher.cs @@ -603,15 +603,15 @@ namespace VRCX 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 @@ namespace VRCX { // 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 @@ namespace VRCX 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 5c058463..bb817d86 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,1194 +283,10 @@ 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 = {}; - - API.eventHandlers = new Map(); - - 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); - } - }; - - API.$on = function (name, handler) { - var handlers = this.eventHandlers.get(name); - if (typeof handlers === 'undefined') { - handlers = []; - this.eventHandlers.set(name, handlers); - } - handlers.push(handler); - }; - - API.$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; - } - } - }; - - 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) : '{}'; - } - 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 (typeof endpoint !== 'undefined') { - text.push(JSON.stringify(endpoint)); - } - text = text.map((s) => 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)); - }; - - // #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; - } - }); - 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; - }; - - 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 = 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); - } - }, - 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 = API.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(); - } - }); - - // #endregion // #region | API: User // changeUserName: PUT users/${userId} {displayName: string, currentPassword: string} @@ -1456,95 +310,9 @@ speechSynthesis.getVoices(); // 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: '' - }; 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}`; - } 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.$on('USER:CURRENT:SAVE', function (args) { this.$emit('USER:CURRENT', args); }); @@ -1569,111 +337,6 @@ speechSynthesis.getVoices(); } }); - API.logout = function () { - this.$emit('LOGOUT'); - // return this.call('logout', { - // method: 'PUT' - // }).finally(() => { - // this.$emit('LOGOUT'); - // }); - }; - - /** - * @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.applyUserTrustLevel = function (ref) { ref.$isModerator = ref.developerType && ref.developerType !== 'none'; ref.$isTroll = false; @@ -1797,171 +460,6 @@ speechSynthesis.getVoices(); } }; - 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 { - 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 = 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 - }); - } - return ref; - }; - - API.getCurrentUser = function () { - $app.nextCurrentUserRefresh = 840; // 7mins - 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 { - this.$emit('USER:CURRENT', args); - } - return args; - }); - }; - var userUpdateQueue = []; var userUpdateTimer = null; var queueUserUpdate = function (ctx) { @@ -2068,7 +566,7 @@ speechSynthesis.getVoices(); ref.$online_for = player.joinTime; } if (ref.location === 'traveling') { - ref.$location = this.parseLocation(ref.travelingToLocation); + ref.$location = $utils.parseLocation(ref.travelingToLocation); if ( !this.currentTravelers.has(ref.id) && ref.travelingToLocation @@ -2083,7 +581,7 @@ speechSynthesis.getVoices(); $app.onPlayerTraveling(travelRef); } } else { - ref.$location = this.parseLocation(ref.location); + ref.$location = $utils.parseLocation(ref.location); if (this.currentTravelers.has(ref.id)) { this.currentTravelers.delete(ref.id); $app.sharedFeed.pendingUpdate = true; @@ -2116,7 +614,7 @@ speechSynthesis.getVoices(); this.applyUserLanguage(ref); // traveling if (ref.location === 'traveling') { - ref.$location = this.parseLocation(ref.travelingToLocation); + ref.$location = $utils.parseLocation(ref.travelingToLocation); if (!this.currentTravelers.has(ref.id)) { var travelRef = { created_at: new Date().toJSON(), @@ -2128,7 +626,7 @@ speechSynthesis.getVoices(); $app.onPlayerTraveling(travelRef); } } else { - ref.$location = this.parseLocation(ref.location); + ref.$location = $utils.parseLocation(ref.location); if (this.currentTravelers.has(ref.id)) { this.currentTravelers.delete(ref.id); $app.sharedFeed.pendingUpdate = true; @@ -2137,7 +635,7 @@ speechSynthesis.getVoices(); } for (var prop in ref) { if (Array.isArray(ref[prop]) && Array.isArray($ref[prop])) { - if (!arraysMatch(ref[prop], $ref[prop])) { + if (!$app.arraysMatch(ref[prop], $ref[prop])) { props[prop] = true; } } else if (ref[prop] !== Object(ref[prop])) { @@ -2251,32 +749,6 @@ speechSynthesis.getVoices(); }); }; - /** - * @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; - }); - }; - /** * @param params {string[]} * @returns {Promise<{json: any, params}>} @@ -2403,7 +875,7 @@ speechSynthesis.getVoices(); // 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); + const L = $utils.parseLocation(presenceLocation); return L.worldId; } @@ -2420,13 +892,13 @@ speechSynthesis.getVoices(); if ($app.isRealInstance(userLocation)) { console.warn('PWI: returning user location', userLocation); - const L = this.parseLocation(userLocation); + const L = $utils.parseLocation(userLocation); return L.worldId; } if ($app.isRealInstance(gameLogLocation)) { console.warn(`PWI: returning gamelog location: `, gameLogLocation); - const L = this.parseLocation(gameLogLocation); + const L = $utils.parseLocation(gameLogLocation); return L.worldId; } @@ -2828,7 +1300,7 @@ speechSynthesis.getVoices(); } else { Object.assign(ref, json); } - ref.$location = this.parseLocation(ref.location); + ref.$location = $utils.parseLocation(ref.location); if (json.world?.id) { this.getCachedWorld({ worldId: json.world.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); }); @@ -4980,517 +3452,6 @@ speechSynthesis.getVoices(); }); }; - // #endregion - // #region | API: WebSocket - - API.webSocket = null; - API.lastWebSocketMessage = ''; - - API.$on('LOGOUT', function () { - this.closeWebSocket(); - }); - - API.$on('USER:CURRENT', function () { - if ($app.friendLogInitStatus && this.webSocket === null) { - this.getAuth(); - } - }); - - API.$on('AUTH', function (args) { - if (args.json.ok) { - this.connectWebSocket(args.json.token); - } - }); - - 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 - } - }); - } - 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 - } - }); - } - 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': - 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 - } 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); - } - }); - - API.getAuth = function () { - return this.call('auth', { - method: 'GET' - }).then((json) => { - var args = { - json - }; - this.$emit('AUTH', args); - return args; - }); - }; - - 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; - } - }; - - 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(); - }; - // #endregion // #region | API: Visit @@ -5512,94 +3473,6 @@ speechSynthesis.getVoices(); // #endregion // #region | Misc - var extractFileId = (s) => { - var match = String(s).match(/file_[0-9A-Za-z-]+/); - return match ? match[0] : ''; - }; - - var extractFileVersion = (s) => { - var match = /(?:\/file_[0-9A-Za-z-]+\/)([0-9]+)/gi.exec(s); - return match ? match[1] : ''; - }; - - var 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'; - } - }; - - 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 - ) { - 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) - }); - } - } - 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; - }; - var $timers = []; Vue.component('timer', { @@ -5623,7 +3496,7 @@ speechSynthesis.getVoices(); this.text = '-'; return; } - this.text = timeToText(Date.now() - this.epoch); + this.text = $app.timeToText(Date.now() - this.epoch); } }, watch: { @@ -5636,7 +3509,7 @@ speechSynthesis.getVoices(); this.update(); }, destroyed() { - removeFromArray($timers, this); + $app.removeFromArray($timers, this); } }); @@ -5678,7 +3551,7 @@ speechSynthesis.getVoices(); 1000 * 60 * 60 * this.hours - Date.now(); if (epoch >= 0) { - this.text = timeToText(epoch); + this.text = $app.timeToText(epoch); } else { this.text = '-'; } @@ -5694,7 +3567,7 @@ speechSynthesis.getVoices(); this.update(); }, destroyed() { - removeFromArray($countDownTimers, this); + $app.removeFromArray($countDownTimers, this); } }); @@ -5705,108 +3578,7 @@ speechSynthesis.getVoices(); }, 5000); // #endregion - // #region | initialise ... stuff. Don't look at me, I don't work here - - 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(); - } - 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; - } - }); - } - }; + // #region | initialise $app.methods.refreshCustomCss = function () { if (document.contains(document.getElementById('app-custom-style'))) { @@ -5895,72 +3667,6 @@ speechSynthesis.getVoices(); await configRepository.setString('VRCX_branch', this.branch); }; - $app.methods.languageClass = function (language) { - var style = {}; - var mapping = languageMappings[language]; - if (typeof mapping !== 'undefined') { - style[mapping] = true; - } else { - style.unknown = true; - } - 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(); - } - } - } catch (err) { - API.isRefreshFriendsLoading = false; - console.error(err); - } - workerTimers.setTimeout(() => this.updateLoop(), 500); - }; - $app.methods.updateIsGameRunning = async function ( isGameRunning, isSteamVRRunning, @@ -6005,2156 +3711,13 @@ speechSynthesis.getVoices(); $app.data.debugWebRequests = false; $app.data.debugWebSocket = false; $app.data.debugUserDiff = false; + $app.data.debugCurrentUserDiff = false; $app.data.debugPhotonLogging = false; $app.data.debugGameLog = false; $app.data.debugFriendState = false; $app.data.APILastOnline = new Map(); - $app.data.sharedFeed = { - gameLog: { - wrist: [], - lastEntryDate: '' - }, - feedTable: { - wrist: [], - lastEntryDate: '' - }, - notificationTable: { - wrist: [], - lastEntryDate: '' - }, - friendLogTable: { - wrist: [], - lastEntryDate: '' - }, - moderationAgainstTable: { - wrist: [], - lastEntryDate: '' - }, - pendingUpdate: false - }; - - $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.updateSharedExecute = function (forceUpdate) { - try { - this.updateSharedFeedDebounce(forceUpdate); - } catch (err) { - console.error(err); - } - this.updateSharedFeedTimer = null; - this.updateSharedFeedPending = false; - this.updateSharedFeedPendingForceUpdate = false; - }; - - $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; - }); - 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 { - 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') { - 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; - }; - - $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); - } - }; - - $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; - } - 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; - } - } - this.sharedFeed.feedTable.wrist = wristArr; - this.sharedFeed.pendingUpdate = true; - }; - - $app.methods.queueFeedNoty = function (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); - } - }; - - $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; - } - 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; - } - 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; - }; - - $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); - } - }; - - $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 { - 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; - } - } - this.sharedFeed.friendLogTable.wrist = wristArr; - this.sharedFeed.pendingUpdate = true; - }; - - $app.methods.queueFriendLogNoty = function (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); - } - }; - - $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; - } - this.sharedFeed.moderationAgainstTable.lastEntryDate = - data[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 = 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.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); - } - var notyFilter = this.sharedFeedFilters.noty; - if (notyFilter[noty.type] && notyFilter[noty.type] === 'On') { - this.playNoty(noty); - } - }; - - $app.data.notyMap = []; - - $app.methods.playNoty = function (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, ''); - } - } - } - }; - - $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; - } - } - } - 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 - ); - } - } catch (err) { - console.error(imageUrl, err); - } - return imageLocation; - }; - - $app.methods.displayOverlayNotification = function ( - noty, - message, - imageFile - ) { - var image = ''; - if (imageFile) { - image = `file:///${imageFile}`; - } - 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'); - } - 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; - } - }; - - $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 - ); - } - 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; - } - }; - - $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.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}`; - } - } - return text; - }; - $app.methods.notifyMenu = function (index) { var { menu } = this.$refs; if (menu.activeIndex !== index) { @@ -8205,120 +3768,6 @@ speechSynthesis.getVoices(); $app.twoFactorAuthDialogVisible = false; }); - $app.methods.promptTOTP = function () { - 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(); - } - }); - }; - - $app.methods.promptOTP = function () { - 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(); - } - }); - }; - - $app.methods.promptEmailOTP = function () { - 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(); - } - } - ); - }; - $app.methods.resendEmail2fa = async function () { if (this.loginForm.lastUserLoggedIn) { var user = @@ -8444,7 +3893,7 @@ speechSynthesis.getVoices(); if (this.isLoggedIn) { new Noty({ type: 'success', - text: `See you again, ${escapeTag( + text: `See you again, ${$app.escapeTag( this.currentUser.displayName )}!` }).show(); @@ -8456,7 +3905,7 @@ speechSynthesis.getVoices(); API.$on('LOGIN', function (args) { new Noty({ type: 'success', - text: `Hello there, ${escapeTag( + text: `Hello there, ${$app.escapeTag( args.ref.displayName )}!` }).show(); @@ -8593,7 +4042,7 @@ speechSynthesis.getVoices(); } }; - $app.methods.updateStoredUser = async function (currentUser) { + $app.methods.updateStoredUser = async function (user) { var savedCredentials = {}; if ((await configRepository.getString('savedCredentials')) !== null) { savedCredentials = JSON.parse( @@ -8602,14 +4051,14 @@ speechSynthesis.getVoices(); } if (this.saveCredentials) { var credentialsToSave = { - user: currentUser, + user, loginParmas: this.saveCredentials }; - savedCredentials[currentUser.id] = credentialsToSave; + savedCredentials[user.id] = credentialsToSave; delete this.saveCredentials; - } else if (typeof savedCredentials[currentUser.id] !== 'undefined') { - savedCredentials[currentUser.id].user = currentUser; - savedCredentials[currentUser.id].cookies = + } else if (typeof savedCredentials[user.id] !== 'undefined') { + savedCredentials[user.id].user = user; + savedCredentials[user.id].cookies = await webApiService.getCookies(); } this.loginForm.savedCredentials = savedCredentials; @@ -8618,8 +4067,8 @@ speechSynthesis.getVoices(); 'savedCredentials', jsonCredentialsArray ); - this.loginForm.lastUserLoggedIn = currentUser.id; - await configRepository.setString('lastUserLoggedIn', currentUser.id); + this.loginForm.lastUserLoggedIn = user.id; + await configRepository.setString('lastUserLoggedIn', user.id); }; $app.methods.migrateStoredUsers = async function () { @@ -8642,447 +4091,6 @@ speechSynthesis.getVoices(); ); }; - $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; - } - 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(); - }); - }); - } - }); - }; - - $app.methods.deleteSavedLogin = async function (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(); - }; - - 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; - }); - - $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' - } - ] - } - }; - - $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' - }); - } - }); - } - }; - - // #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}`); - } - } - } - database.commit(); - }; - - $app.methods.getMemo = async function (userId) { - try { - return await database.getMemo(userId); - } catch (err) {} - return { - userId: '', - editedAt: '', - memo: '' - }; - }; - - $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.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]; - } - } - }); - }; - - // #endregion - // #region | World Memos - - $app.methods.getWorldMemo = async function (worldId) { - try { - return await database.getWorldMemo(worldId); - } catch (err) {} - return { - worldId: '', - editedAt: '', - memo: '' - }; - }; - - $app.methods.saveWorldMemo = function (worldId, memo) { - if (memo) { - database.setWorldMemo({ - worldId, - editedAt: new Date().toJSON(), - memo - }); - } else { - database.deleteWorldMemo(worldId); - } - }; - - // #endregion - // #region | App: Avatar Memos - - $app.methods.getAvatarMemo = async function (avatarId) { - try { - return await database.getAvatarMemoDB(avatarId); - } catch (err) { - console.error(err); - } - return { - avatarId: '', - editedAt: '', - memo: '' - }; - }; - - $app.methods.saveAvatarMemo = function (avatarId, memo) { - if (memo) { - database.setAvatarMemo({ - avatarId, - editedAt: new Date().toJSON(), - memo - }); - } else { - database.deleteAvatarMemo(avatarId); - } - }; - // #endregion // #region | App: Friends @@ -9251,7 +4259,7 @@ speechSynthesis.getVoices(); $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) { + if (this.nextCurrentUserRefresh < 720) { await API.getCurrentUser().catch((err) => { console.error(err); }); @@ -9342,7 +4350,7 @@ speechSynthesis.getVoices(); $nickName: '' }; if (this.friendLogInitStatus) { - this.getMemo(id).then((memo) => { + this.getUserMemo(id).then((memo) => { if (memo.userId === id) { ctx.memo = memo.memo; ctx.$nickName = ''; @@ -9387,14 +4395,14 @@ speechSynthesis.getVoices(); this.friends.delete(id); if (ctx.state === 'online') { if (ctx.isVIP) { - removeFromArray(this.vipFriends_, ctx); + $app.removeFromArray(this.vipFriends_, ctx); } else { - removeFromArray(this.onlineFriends_, ctx); + $app.removeFromArray(this.onlineFriends_, ctx); } } else if (ctx.state === 'active') { - removeFromArray(this.activeFriends_, ctx); + $app.removeFromArray(this.activeFriends_, ctx); } else { - removeFromArray(this.offlineFriends_, ctx); + $app.removeFromArray(this.offlineFriends_, ctx); } }; @@ -9442,11 +4450,11 @@ speechSynthesis.getVoices(); ctx.isVIP = isVIP; if (ctx.state === 'online') { if (ctx.isVIP) { - removeFromArray(this.onlineFriends_, ctx); + $app.removeFromArray(this.onlineFriends_, ctx); this.vipFriends_.push(ctx); this.sortVIPFriends = true; } else { - removeFromArray(this.vipFriends_, ctx); + $app.removeFromArray(this.vipFriends_, ctx); this.onlineFriends_.push(ctx); this.sortOnlineFriends = true; } @@ -9615,14 +4623,14 @@ speechSynthesis.getVoices(); } if (ctx.state === 'online') { if (ctx.isVIP) { - removeFromArray(this.vipFriends_, ctx); + $app.removeFromArray(this.vipFriends_, ctx); } else { - removeFromArray(this.onlineFriends_, ctx); + $app.removeFromArray(this.onlineFriends_, ctx); } } else if (ctx.state === 'active') { - removeFromArray(this.activeFriends_, ctx); + $app.removeFromArray(this.activeFriends_, ctx); } else { - removeFromArray(this.offlineFriends_, ctx); + $app.removeFromArray(this.offlineFriends_, ctx); } if (newState === 'online') { if (isVIP) { @@ -9652,7 +4660,7 @@ speechSynthesis.getVoices(); var worldName = ''; if (this.isRealInstance(location)) { try { - var L = API.parseLocation(location); + var L = $utils.parseLocation(location); if (L.worldId) { var args = await API.getCachedWorld({ worldId: L.worldId @@ -9671,7 +4679,7 @@ speechSynthesis.getVoices(); var groupName = ''; var groupId = data; if (!data.startsWith('grp_')) { - var L = API.parseLocation(data); + var L = $utils.parseLocation(data); groupId = L.groupId; if (!L.groupId) { return ''; @@ -10288,128 +5296,20 @@ speechSynthesis.getVoices(); // #endregion // #region | App: Feed - $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.tablePageSize = await configRepository.getInt( 'VRCX_tablePageSize', 15 ); - $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] - } - }; - - $app.data.feedSessionTable = []; - - $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; - }; + $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.data.dontLogMeOut = false; @@ -10455,7 +5355,7 @@ speechSynthesis.getVoices(); } } $app.getAvatarHistory(); - $app.getAllMemos(); + $app.getAllUserMemos(); if ($app.randomUserColours) { $app.getNameColour(this.currentUser.id).then((colour) => { this.currentUser.$userColour = colour; @@ -10773,97 +5673,6 @@ speechSynthesis.getVoices(); } }); - $app.methods.addFeed = function (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'); - }; - - $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; - }) - ); - } - } - }); - }; - - $app.methods.sweepFeed = function () { - 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); - } - }; - // #endregion // #region | App: gameLog @@ -10953,653 +5762,10 @@ speechSynthesis.getVoices(); statusImage: '' }; - $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; - } - return true; - }; - - $app.data.gameLogTable = { - data: [], - loading: false, - search: '', - 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] - } - }; - - $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) - ) { - return; - } - if (!this.gameLogSearch(entry)) { - 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; - } - 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; - } - 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) - ); - 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); - }; - - $app.methods.deleteGameLogEntryPrompt = function (row) { - this.$confirm('Continue? Delete Log', 'Confirm', { - confirmButtonText: 'Confirm', - cancelButtonText: 'Cancel', - type: 'info', - callback: (action) => { - if (action === 'confirm') { - this.deleteGameLogEntry(row); - } - } - }); - }; - - $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); - }); - }; - $app.data.lastLocationDestination = ''; $app.data.lastLocationDestinationTime = 0; - $app.data.lastVideoUrl = ''; - $app.data.lastResourceloadUrl = ''; - $app.data.gameLogApiLoggingEnabled = false; - $app.methods.addGameLogEntry = function (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 = 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) { + $app.methods.silentSearchUser = function (displayName) { var playerListRef = this.lastLocation.playerList.get(displayName); if ( !this.gameLogApiLoggingEnabled || @@ -11636,1987 +5802,6 @@ speechSynthesis.getVoices(); }); }; - $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) - ); - }; - - $app.data.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] - } - }; - - $app.data.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] - } - }; - - $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) - ); - } - } - }; - - $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; - }; - - $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; - } - } - 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' - }); - } - } - }; - - $app.methods.getPhotonIdFromDisplayName = function (displayName) { - var photonId = ''; - if (displayName) { - this.photonLobby.forEach((ref, id) => { - if ( - typeof ref !== 'undefined' && - ref.displayName === displayName - ) { - photonId = id; - } - }); - } - 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; - } - }); - } - 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 (id1 > id2) { - return -1; - } - 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 - }); - 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 = 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; - } - }; - - $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 - ); - 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.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); - } - } - }; - - $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 - }); - } - this.photonLobbyMaster = photonId; - } - }; - - $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.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.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.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.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)}`; - } - } - 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.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.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.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; - } - } - } - 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.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; - } - } - } - 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 - ); - 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 - ); - 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); - }; - - $app.methods.addGameLogMovieAndChill = function (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); - }; - $app.methods.lookupYouTubeVideo = async function (videoId) { var data = null; var apiKey = 'AIzaSyA-iUQCpWf5afEL3NanEOSxbzziPMU3bxY'; @@ -13787,227 +5972,6 @@ speechSynthesis.getVoices(); return length; }; - $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 - ); - // 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, ''); - } - }; - - $app.methods.setDiscordActive = async function (active) { - if (active !== this.isDiscordActive) { - this.isDiscordActive = await Discord.SetActive(active); - } - }; - $app.methods.updateAutoStateChange = function () { if ( this.autoStateChange === 'Off' || @@ -14538,14 +6502,14 @@ speechSynthesis.getVoices(); // WTF??? isTypeChanged = true; if (type === 'friend') { - removeFromArray(this.favoriteFriends_, ctx); - removeFromArray(this.favoriteFriendsSorted, ctx); + $app.removeFromArray(this.favoriteFriends_, ctx); + $app.removeFromArray(this.favoriteFriendsSorted, ctx); } else if (type === 'world') { - removeFromArray(this.favoriteWorlds_, ctx); - removeFromArray(this.favoriteWorldsSorted, ctx); + $app.removeFromArray(this.favoriteWorlds_, ctx); + $app.removeFromArray(this.favoriteWorldsSorted, ctx); } else if (type === 'avatar') { - removeFromArray(this.favoriteAvatars_, ctx); - removeFromArray(this.favoriteAvatarsSorted, ctx); + $app.removeFromArray(this.favoriteAvatars_, ctx); + $app.removeFromArray(this.favoriteAvatarsSorted, ctx); } } if (type === 'friend') { @@ -14647,14 +6611,14 @@ speechSynthesis.getVoices(); } else if (typeof ctx !== 'undefined') { this.favoriteObjects.delete(objectId); if (type === 'friend') { - removeFromArray(this.favoriteFriends_, ctx); - removeFromArray(this.favoriteFriendsSorted, ctx); + $app.removeFromArray(this.favoriteFriends_, ctx); + $app.removeFromArray(this.favoriteFriendsSorted, ctx); } else if (type === 'world') { - removeFromArray(this.favoriteWorlds_, ctx); - removeFromArray(this.favoriteWorldsSorted, ctx); + $app.removeFromArray(this.favoriteWorlds_, ctx); + $app.removeFromArray(this.favoriteWorldsSorted, ctx); } else if (type === 'avatar') { - removeFromArray(this.favoriteAvatars_, ctx); - removeFromArray(this.favoriteAvatarsSorted, ctx); + $app.removeFromArray(this.favoriteAvatars_, ctx); + $app.removeFromArray(this.favoriteAvatarsSorted, ctx); } } }; @@ -14684,47 +6648,6 @@ speechSynthesis.getVoices(); }); }; - $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', { @@ -15110,7 +7033,7 @@ speechSynthesis.getVoices(); type: 'info', callback: (action) => { if (action === 'confirm') { - removeFromArray(this.friendLogTable.data, row); + $app.removeFromArray(this.friendLogTable.data, row); database.deleteFriendLogHistory(row.rowId); } } @@ -15239,28 +7162,34 @@ speechSynthesis.getVoices(); 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 (!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 (!$utils.checkCanInvite(currentLocation)) { + return; + } - if (!$app.checkCanInvite(currentLocation)) return; - - var L = this.parseLocation(currentLocation); + var L = $utils.parseLocation(currentLocation); this.getCachedWorld({ worldId: L.worldId }).then((args1) => { @@ -15320,7 +7249,7 @@ speechSynthesis.getVoices(); API.$on('NOTIFICATION:SEE', function (args) { var { notificationId } = args.params; - removeFromArray($app.unseenNotifications, notificationId); + $app.removeFromArray($app.unseenNotifications, notificationId); if ($app.unseenNotifications.length === 0) { $app.selectMenu('notification'); } @@ -15373,7 +7302,7 @@ speechSynthesis.getVoices(); type: 'info', callback: (action) => { if (action === 'confirm') { - removeFromArray(this.notificationTable.data, row); + $app.removeFromArray(this.notificationTable.data, row); if ( row.type !== 'friendRequest' && row.type !== 'hiddenFriendRequest' @@ -15396,7 +7325,7 @@ speechSynthesis.getVoices(); if (this.lastLocation.location === 'traveling') { currentLocation = this.lastLocationDestination; } - var L = API.parseLocation(currentLocation); + var L = $utils.parseLocation(currentLocation); API.getCachedWorld({ worldId: L.worldId }).then((args) => { @@ -15734,13 +7663,13 @@ speechSynthesis.getVoices(); $app.data.maxTableSize = 1000; } database.setmaxTableSize($app.data.maxTableSize); - $app.data.photonLobbyTimeoutThreshold = await configRepository.getString( + $app.data.photonLobbyTimeoutThreshold = await configRepository.getInt( 'VRCX_photonLobbyTimeoutThreshold', 6000 ); - $app.data.clearVRCXCacheFrequency = await configRepository.getString( + $app.data.clearVRCXCacheFrequency = await configRepository.getInt( 'VRCX_clearVRCXCacheFrequency', - '172800' + 172800 ); $app.data.avatarRemoteDatabase = await configRepository.getBool( 'VRCX_avatarRemoteDatabase', @@ -16090,7 +8019,6 @@ speechSynthesis.getVoices(); 6000 ) ); - $app.data.photonLoggingEnabled = false; $app.data.gameLogDisabled = await configRepository.getBool( 'VRCX_gameLogDisabled', false @@ -16295,25 +8223,6 @@ speechSynthesis.getVoices(); '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 = { @@ -16527,27 +8436,6 @@ speechSynthesis.getVoices(); }; 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' @@ -16642,7 +8530,7 @@ speechSynthesis.getVoices(); 'wrld_74970324-58e8-4239-a17b-2c59dfdf00db', 'wrld_266523e8-9161-40da-acd0-6bd82e075833' ]; - var L = API.parseLocation(location); + var L = $utils.parseLocation(location); if (rpcWorlds.includes(L.worldId)) { return true; } @@ -16701,19 +8589,6 @@ speechSynthesis.getVoices(); $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', @@ -16809,161 +8684,11 @@ speechSynthesis.getVoices(); }; $app.methods.refreshConfigTreeData = function () { - this.configTreeData = buildTreeData(API.cachedConfig); + this.configTreeData = $utils.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' - }); - } - } - } - } - ); + this.currentUserTreeData = $utils.buildTreeData(API.currentUser); }; $app.methods.directAccessPaste = function () { @@ -17095,370 +8820,6 @@ speechSynthesis.getVoices(); 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; @@ -17469,72 +8830,10 @@ speechSynthesis.getVoices(); 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) => { + $app.methods.adjustDialogZ = function (el) { var z = 0; document .querySelectorAll('.v-modal,.el-dialog__wrapper') @@ -17625,17 +8924,6 @@ speechSynthesis.getVoices(); 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) { @@ -17846,17 +9134,15 @@ speechSynthesis.getVoices(); if (!userId) { return; } - this.$nextTick(() => adjustDialogZ(this.$refs.userDialog.$el)); + this.$nextTick(() => $app.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) => { + this.getUserMemo(userId).then((memo) => { if (memo.userId === userId) { - this.ignoreUserMemoSave = true; D.memo = memo.memo; var ref = this.friends.get(userId); if (ref) { @@ -18108,7 +9394,7 @@ speechSynthesis.getVoices(); if (!D.visible) { return; } - var L = API.parseLocation(D.ref.$location.tag); + var L = $utils.parseLocation(D.ref.$location.tag); if (updateInstanceOccupants && this.isRealInstance(L.tag)) { API.getInstance({ worldId: L.worldId, @@ -18492,7 +9778,7 @@ speechSynthesis.getVoices(); bundleSizes: [], lastUpdated: '' }; - var L = API.parseLocation(instanceId); + var L = $utils.parseLocation(instanceId); this.currentInstanceLocation = L; API.getWorld({ worldId: L.worldId @@ -18549,7 +9835,7 @@ speechSynthesis.getVoices(); if (typeof ref !== 'undefined') { this.currentInstanceWorld.instance = ref; } else { - var L = API.parseLocation(instanceId); + var L = $utils.parseLocation(instanceId); API.getInstance({ worldId: L.worldId, instanceId: L.instanceId @@ -18746,7 +10032,7 @@ speechSynthesis.getVoices(); var url = this.avatarRemoteDatabaseProviderList[i]; var avatarArray = await this.lookupAvatarsByAuthor(url, authorId); for (var avatar of avatarArray) { - if (extractFileId(avatar.imageUrl) === fileId) { + if ($utils.extractFileId(avatar.imageUrl) === fileId) { return avatar.id; } } @@ -18904,7 +10190,7 @@ speechSynthesis.getVoices(); if (fileId) { D.loading = false; for (let ref of array) { - if (extractFileId(ref.imageUrl) === fileId) { + if ($utils.extractFileId(ref.imageUrl) === fileId) { this.showAvatarDialog(ref.id); return; } @@ -19048,7 +10334,7 @@ speechSynthesis.getVoices(); return args; }); } else if (command === 'Invite Message') { - var L = API.parseLocation(this.lastLocation.location); + var L = $utils.parseLocation(this.lastLocation.location); API.getCachedWorld({ worldId: L.worldId }).then((args) => { @@ -19073,7 +10359,7 @@ speechSynthesis.getVoices(); if (this.lastLocation.location === 'traveling') { currentLocation = this.lastLocationDestination; } - var L = API.parseLocation(currentLocation); + var L = $utils.parseLocation(currentLocation); API.getCachedWorld({ worldId: L.worldId }).then((args) => { @@ -19149,10 +10435,10 @@ speechSynthesis.getVoices(); ...API.currentUser, ...D.ref }; - D.treeData = buildTreeData(treeData); + D.treeData = $utils.buildTreeData(treeData); return; } - D.treeData = buildTreeData(D.ref); + D.treeData = $utils.buildTreeData(D.ref); }; $app.methods.changeUserDialogAvatarSorting = function () { @@ -19204,17 +10490,6 @@ speechSynthesis.getVoices(); 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; }); @@ -19267,8 +10542,8 @@ speechSynthesis.getVoices(); continue; } var assetUrl = unityPackage.assetUrl; - var fileId = extractFileId(assetUrl); - var fileVersion = parseInt(extractFileVersion(assetUrl), 10); + var fileId = $utils.extractFileId(assetUrl); + var fileVersion = parseInt($utils.extractFileVersion(assetUrl), 10); if (!fileId) { continue; } @@ -19347,9 +10622,9 @@ speechSynthesis.getVoices(); }); $app.methods.showWorldDialog = function (tag, shortName) { - this.$nextTick(() => adjustDialogZ(this.$refs.worldDialog.$el)); + this.$nextTick(() => $app.adjustDialogZ(this.$refs.worldDialog.$el)); var D = this.worldDialog; - var L = API.parseLocation(tag); + var L = $utils.parseLocation(tag); if (L.worldId === '') { return; } @@ -19376,16 +10651,14 @@ speechSynthesis.getVoices(); D.isQuest = false; D.isIos = false; D.hasPersistData = false; - this.ignoreWorldMemoSave = true; D.memo = ''; - var LL = API.parseLocation(this.lastLocation.location); + 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) { - this.ignoreWorldMemoSave = true; D.memo = memo.memo; } }); @@ -19576,7 +10849,7 @@ speechSynthesis.getVoices(); 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}`); + const L = $utils.parseLocation(`${D.id}:${instance.id}`); instance.location = L.tag; if (!L.shortName) { L.shortName = instance.shortName; @@ -19759,7 +11032,7 @@ speechSynthesis.getVoices(); 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); + const L = $utils.parseLocation(instance.tag); instance.location = instance.tag; instance.$location = L; if (instance.friendCount === 0) { @@ -19950,7 +11223,7 @@ speechSynthesis.getVoices(); $app.methods.refreshWorldDialogTreeData = function () { var D = this.worldDialog; - D.treeData = buildTreeData(D.ref); + D.treeData = $utils.buildTreeData(D.ref); }; $app.computed.worldDialogPlatform = function () { @@ -20000,20 +11273,6 @@ speechSynthesis.getVoices(); 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; }); @@ -20036,7 +11295,7 @@ speechSynthesis.getVoices(); }); $app.methods.showAvatarDialog = function (avatarId) { - this.$nextTick(() => adjustDialogZ(this.$refs.avatarDialog.$el)); + this.$nextTick(() => $app.adjustDialogZ(this.$refs.avatarDialog.$el)); var D = this.avatarDialog; D.visible = true; D.loading = true; @@ -20061,7 +11320,6 @@ speechSynthesis.getVoices(); (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') { @@ -20120,7 +11378,6 @@ speechSynthesis.getVoices(); }); this.getAvatarMemo(avatarId).then((memo) => { if (D.id === memo.avatarId) { - this.ignoreAvatarMemoSave = true; D.memo = memo.memo; } }); @@ -20305,7 +11562,7 @@ speechSynthesis.getVoices(); $app.methods.checkAvatarCache = function (fileId) { var avatarId = ''; for (var ref of API.cachedAvatars.values()) { - if (extractFileId(ref.imageUrl) === fileId) { + if ($utils.extractFileId(ref.imageUrl) === fileId) { avatarId = ref.id; } } @@ -20328,7 +11585,7 @@ speechSynthesis.getVoices(); ownerUserId, currentAvatarImageUrl ) { - var fileId = extractFileId(currentAvatarImageUrl); + var fileId = $utils.extractFileId(currentAvatarImageUrl); if (!fileId) { this.$message({ message: 'Sorry, the author is unknown', @@ -20370,7 +11627,7 @@ speechSynthesis.getVoices(); $app.methods.refreshAvatarDialogTreeData = function () { var D = this.avatarDialog; - D.treeData = buildTreeData(D.ref); + D.treeData = $utils.buildTreeData(D.ref); }; $app.computed.avatarDialogPlatform = function () { @@ -20468,7 +11725,7 @@ speechSynthesis.getVoices(); }; $app.methods.showFavoriteDialog = function (type, objectId) { - this.$nextTick(() => adjustDialogZ(this.$refs.favoriteDialog.$el)); + this.$nextTick(() => $app.adjustDialogZ(this.$refs.favoriteDialog.$el)); var D = this.favoriteDialog; D.type = type; D.objectId = objectId; @@ -20579,7 +11836,7 @@ speechSynthesis.getVoices(); var receiverUserId = D.userIds.shift(); if (receiverUserId === API.currentUser.id) { // can't invite self!? - var L = API.parseLocation(D.worldId); + var L = $utils.parseLocation(D.worldId); API.selfInvite({ instanceId: L.instanceId, worldId: L.worldId @@ -20612,8 +11869,8 @@ speechSynthesis.getVoices(); if (!this.isRealInstance(tag)) { return; } - this.$nextTick(() => adjustDialogZ(this.$refs.inviteDialog.$el)); - var L = API.parseLocation(tag); + this.$nextTick(() => $app.adjustDialogZ(this.$refs.inviteDialog.$el)); + var L = $utils.parseLocation(tag); API.getCachedWorld({ worldId: L.worldId }).then((args) => { @@ -20672,7 +11929,9 @@ speechSynthesis.getVoices(); }; $app.methods.showSocialStatusDialog = function () { - this.$nextTick(() => adjustDialogZ(this.$refs.socialStatusDialog.$el)); + this.$nextTick(() => + $app.adjustDialogZ(this.$refs.socialStatusDialog.$el) + ); var D = this.socialStatusDialog; var { statusHistory } = API.currentUser; var statusHistoryArray = []; @@ -20698,72 +11957,7 @@ speechSynthesis.getVoices(); }; // #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 = { @@ -20801,7 +11995,7 @@ speechSynthesis.getVoices(); }; $app.methods.showBioDialog = function () { - this.$nextTick(() => adjustDialogZ(this.$refs.bioDialog.$el)); + this.$nextTick(() => $app.adjustDialogZ(this.$refs.bioDialog.$el)); var D = this.bioDialog; D.bio = API.currentUser.bio; D.bioLinks = API.currentUser.bioLinks.slice(); @@ -20844,7 +12038,7 @@ speechSynthesis.getVoices(); }; $app.methods.showPronounsDialog = function () { - this.$nextTick(() => adjustDialogZ(this.$refs.pronounsDialog.$el)); + this.$nextTick(() => $app.adjustDialogZ(this.$refs.pronounsDialog.$el)); var D = this.pronounsDialog; D.pronouns = API.currentUser.pronouns; D.visible = true; @@ -21060,7 +12254,7 @@ speechSynthesis.getVoices(); if (!this.isRealInstance(location)) { return; } - var L = API.parseLocation(location); + var L = $utils.parseLocation(location); API.selfInvite({ instanceId: L.instanceId, worldId: L.worldId, @@ -21081,7 +12275,7 @@ speechSynthesis.getVoices(); } else { D.location = D.worldId; } - var L = API.parseLocation(D.location); + var L = $utils.parseLocation(D.location); if (noChanges) { L.shortName = D.shortName; } else { @@ -21133,9 +12327,11 @@ speechSynthesis.getVoices(); if (!this.isRealInstance(tag)) { return; } - this.$nextTick(() => adjustDialogZ(this.$refs.newInstanceDialog.$el)); + this.$nextTick(() => + $app.adjustDialogZ(this.$refs.newInstanceDialog.$el) + ); var D = this.newInstanceDialog; - var L = API.parseLocation(tag); + var L = $utils.parseLocation(tag); if (D.worldId === L.worldId) { // reopening dialog, keep last open instance D.visible = true; @@ -21263,7 +12459,9 @@ speechSynthesis.getVoices(); }; $app.methods.showLaunchOptions = function () { - this.$nextTick(() => adjustDialogZ(this.$refs.launchOptionsDialog.$el)); + this.$nextTick(() => + $app.adjustDialogZ(this.$refs.launchOptionsDialog.$el) + ); var D = this.launchOptionsDialog; D.visible = true; }; @@ -21287,7 +12485,9 @@ speechSynthesis.getVoices(); }; $app.methods.showSetWorldTagsDialog = function () { - this.$nextTick(() => adjustDialogZ(this.$refs.setWorldTagsDialog.$el)); + this.$nextTick(() => + $app.adjustDialogZ(this.$refs.setWorldTagsDialog.$el) + ); var D = this.setWorldTagsDialog; D.visible = true; D.debugAllowed = false; @@ -21434,7 +12634,9 @@ speechSynthesis.getVoices(); }; $app.methods.showSetAvatarTagsDialog = function (avatarId) { - this.$nextTick(() => adjustDialogZ(this.$refs.setAvatarTagsDialog.$el)); + this.$nextTick(() => + $app.adjustDialogZ(this.$refs.setAvatarTagsDialog.$el) + ); var D = this.setAvatarTagsDialog; D.visible = true; D.loading = false; @@ -21667,7 +12869,7 @@ speechSynthesis.getVoices(); $app.methods.showNotificationPositionDialog = function () { this.$nextTick(() => - adjustDialogZ(this.$refs.notificationPositionDialog.$el) + $app.adjustDialogZ(this.$refs.notificationPositionDialog.$el) ); this.notificationPositionDialog.visible = true; }; @@ -21681,7 +12883,7 @@ speechSynthesis.getVoices(); $app.methods.showNotyFeedFiltersDialog = function () { this.$nextTick(() => - adjustDialogZ(this.$refs.notyFeedFiltersDialog.$el) + $app.adjustDialogZ(this.$refs.notyFeedFiltersDialog.$el) ); this.notyFeedFiltersDialog.visible = true; }; @@ -21695,7 +12897,7 @@ speechSynthesis.getVoices(); $app.methods.showWristFeedFiltersDialog = function () { this.$nextTick(() => - adjustDialogZ(this.$refs.wristFeedFiltersDialog.$el) + $app.adjustDialogZ(this.$refs.wristFeedFiltersDialog.$el) ); this.wristFeedFiltersDialog.visible = true; }; @@ -21734,7 +12936,7 @@ speechSynthesis.getVoices(); 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); + var L = $utils.parseLocation(location); L.shortName = shortName; $app.launchDialog.shortName = shortName; $app.launchDialog.secureOrShortName = secureOrShortName; @@ -21765,13 +12967,13 @@ speechSynthesis.getVoices(); if (!this.isRealInstance(tag)) { return; } - this.$nextTick(() => adjustDialogZ(this.$refs.launchDialog.$el)); + 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 = API.parseLocation(tag); + var L = $utils.parseLocation(tag); L.shortName = shortName; if (shortName) { D.shortUrl = `https://vrch.at/${shortName}`; @@ -21816,7 +13018,7 @@ speechSynthesis.getVoices(); desktopMode ) { var D = this.launchDialog; - var L = API.parseLocation(location); + var L = $utils.parseLocation(location); var args = []; if ( shortName && @@ -21919,7 +13121,7 @@ speechSynthesis.getVoices(); }; $app.methods.copyInstanceUrl = async function (location) { - var L = API.parseLocation(location); + var L = $utils.parseLocation(location); var args = await API.getInstanceShortName({ worldId: L.worldId, instanceId: L.instanceId @@ -22141,7 +13343,7 @@ speechSynthesis.getVoices(); }; $app.methods.compareCurrentVRCPlusIcon = function (userIcon) { - var currentUserIcon = extractFileId(API.currentUser.userIcon); + var currentUserIcon = $utils.extractFileId(API.currentUser.userIcon); if (userIcon === currentUserIcon) { return true; } @@ -22258,11 +13460,11 @@ speechSynthesis.getVoices(); $app.methods.userOnlineFor = function (ctx) { if (ctx.ref.state === 'online' && ctx.ref.$online_for) { - return timeToText(Date.now() - ctx.ref.$online_for); + return $utils.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); + return $utils.timeToText(Date.now() - ctx.ref.$active_for); } else if (ctx.ref.$offline_for) { - return timeToText(Date.now() - ctx.ref.$offline_for); + return $utils.timeToText(Date.now() - ctx.ref.$offline_for); } return '-'; }; @@ -22358,7 +13560,7 @@ speechSynthesis.getVoices(); inviteMessage ) { this.$nextTick(() => - adjustDialogZ(this.$refs.editInviteMessageDialog.$el) + $app.adjustDialogZ(this.$refs.editInviteMessageDialog.$el) ); var D = this.editInviteMessageDialog; D.newMessage = inviteMessage.message; @@ -22418,7 +13620,7 @@ speechSynthesis.getVoices(); inviteMessage ) { this.$nextTick(() => - adjustDialogZ(this.$refs.editAndSendInviteResponseDialog.$el) + $app.adjustDialogZ(this.$refs.editAndSendInviteResponseDialog.$el) ); this.editAndSendInviteResponseDialog = { newMessage: inviteMessage.message, @@ -22527,7 +13729,7 @@ speechSynthesis.getVoices(); }; API.refreshInviteMessageTableData('response'); this.$nextTick(() => - adjustDialogZ(this.$refs.sendInviteResponseDialog.$el) + $app.adjustDialogZ(this.$refs.sendInviteResponseDialog.$el) ); this.clearInviteImageUpload(); this.sendInviteResponseDialogVisible = true; @@ -22541,7 +13743,7 @@ speechSynthesis.getVoices(); return; } this.$nextTick(() => - adjustDialogZ(this.$refs.sendInviteResponseConfirmDialog.$el) + $app.adjustDialogZ(this.$refs.sendInviteResponseConfirmDialog.$el) ); this.sendInviteResponseConfirmDialog.visible = true; this.sendInviteResponseDialog.messageSlot = val.slot; @@ -22617,7 +13819,7 @@ speechSynthesis.getVoices(); }; API.refreshInviteMessageTableData('requestResponse'); this.$nextTick(() => - adjustDialogZ(this.$refs.sendInviteRequestResponseDialog.$el) + $app.adjustDialogZ(this.$refs.sendInviteRequestResponseDialog.$el) ); this.clearInviteImageUpload(); this.sendInviteRequestResponseDialogVisible = true; @@ -22638,7 +13840,7 @@ speechSynthesis.getVoices(); inviteMessage ) { this.$nextTick(() => - adjustDialogZ(this.$refs.editAndSendInviteDialog.$el) + $app.adjustDialogZ(this.$refs.editAndSendInviteDialog.$el) ); this.editAndSendInviteDialog = { newMessage: inviteMessage.message, @@ -22686,7 +13888,7 @@ speechSynthesis.getVoices(); var receiverUserId = J.userIds.shift(); if (receiverUserId === API.currentUser.id) { // can't invite self!? - var L = API.parseLocation(J.worldId); + var L = $utils.parseLocation(J.worldId); API.selfInvite({ instanceId: L.instanceId, worldId: L.worldId @@ -22812,7 +14014,9 @@ speechSynthesis.getVoices(); messageType: 'invite' }; API.refreshInviteMessageTableData('message'); - this.$nextTick(() => adjustDialogZ(this.$refs.sendInviteDialog.$el)); + this.$nextTick(() => + $app.adjustDialogZ(this.$refs.sendInviteDialog.$el) + ); this.clearInviteImageUpload(); this.sendInviteDialogVisible = true; }; @@ -22822,7 +14026,7 @@ speechSynthesis.getVoices(); return; } this.$nextTick(() => - adjustDialogZ(this.$refs.sendInviteConfirmDialog.$el) + $app.adjustDialogZ(this.$refs.sendInviteConfirmDialog.$el) ); this.sendInviteConfirmDialog.visible = true; this.sendInviteDialog.messageSlot = val.slot; @@ -22845,7 +14049,7 @@ speechSynthesis.getVoices(); var receiverUserId = J.userIds.shift(); if (receiverUserId === API.currentUser.id) { // can't invite self!? - var L = API.parseLocation(J.worldId); + var L = $utils.parseLocation(J.worldId); API.selfInvite({ instanceId: L.instanceId, worldId: L.worldId @@ -22964,7 +14168,7 @@ speechSynthesis.getVoices(); }; API.refreshInviteMessageTableData('request'); this.$nextTick(() => - adjustDialogZ(this.$refs.sendInviteRequestDialog.$el) + $app.adjustDialogZ(this.$refs.sendInviteRequestDialog.$el) ); this.clearInviteImageUpload(); this.sendInviteRequestDialogVisible = true; @@ -23347,7 +14551,7 @@ speechSynthesis.getVoices(); ); var avatarId = $app.avatarDialog.id; var { imageUrl } = $app.avatarDialog.ref; - var fileId = extractFileId(imageUrl); + var fileId = $utils.extractFileId(imageUrl); if (!fileId) { $app.$message({ message: 'Current avatar image invalid', @@ -23683,7 +14887,7 @@ speechSynthesis.getVoices(); ); var worldId = $app.worldDialog.id; var { imageUrl } = $app.worldDialog.ref; - var fileId = extractFileId(imageUrl); + var fileId = $utils.extractFileId(imageUrl); if (!fileId) { $app.$message({ message: 'Current world image invalid', @@ -24010,7 +15214,7 @@ speechSynthesis.getVoices(); } else if (type === 'User') { imageUrl = this.userDialog.ref.currentAvatarImageUrl; } - var fileId = extractFileId(imageUrl); + var fileId = $utils.extractFileId(imageUrl); if (!fileId) { return; } @@ -24020,14 +15224,14 @@ speechSynthesis.getVoices(); if (command === 'Display') { this.previousImagesDialogVisible = true; this.$nextTick(() => - adjustDialogZ(this.$refs.previousImagesDialog.$el) + $app.adjustDialogZ(this.$refs.previousImagesDialog.$el) ); } if (type === 'Avatar') { if (command === 'Change') { this.changeAvatarImageDialogVisible = true; this.$nextTick(() => - adjustDialogZ(this.$refs.changeAvatarImageDialog.$el) + $app.adjustDialogZ(this.$refs.changeAvatarImageDialog.$el) ); } API.getAvatarImages(params).then((args) => { @@ -24044,7 +15248,7 @@ speechSynthesis.getVoices(); if (command === 'Change') { this.changeWorldImageDialogVisible = true; this.$nextTick(() => - adjustDialogZ(this.$refs.changeWorldImageDialog.$el) + $app.adjustDialogZ(this.$refs.changeWorldImageDialog.$el) ); } API.getWorldImages(params).then((args) => { @@ -24244,7 +15448,7 @@ speechSynthesis.getVoices(); API.cachedAvatarNames = new Map(); $app.methods.getAvatarName = async function (imageUrl) { - var fileId = extractFileId(imageUrl); + var fileId = $utils.extractFileId(imageUrl); if (!fileId) { return { ownerId: '', @@ -24432,7 +15636,9 @@ speechSynthesis.getVoices(); } }; await this.readVRChatConfigFile(); - this.$nextTick(() => adjustDialogZ(this.$refs.VRChatConfigDialog.$el)); + this.$nextTick(() => + $app.adjustDialogZ(this.$refs.VRChatConfigDialog.$el) + ); this.VRChatConfigDialog.visible = true; if (!this.VRChatUsedCacheSize) { this.getVRChatCacheSize(); @@ -24571,7 +15777,7 @@ speechSynthesis.getVoices(); $app.methods.processScreenshot = async function (path) { var newPath = path; if (this.screenshotHelper) { - var location = API.parseLocation(this.lastLocation.location); + var location = $utils.parseLocation(this.lastLocation.location); var metadata = { application: 'VRCX', version: 1, @@ -24702,7 +15908,7 @@ speechSynthesis.getVoices(); $app.methods.openScreenshotMetadataDialog = function () { this.$nextTick(() => - adjustDialogZ(this.$refs.screenshotMetadataDialog.$el) + $app.adjustDialogZ(this.$refs.screenshotMetadataDialog.$el) ); var D = this.screenshotMetadataDialog; D.visible = true; @@ -24965,7 +16171,9 @@ speechSynthesis.getVoices(); }; $app.methods.showYouTubeApiDialog = function () { - this.$nextTick(() => adjustDialogZ(this.$refs.youTubeApiDialog.$el)); + this.$nextTick(() => + $app.adjustDialogZ(this.$refs.youTubeApiDialog.$el) + ); var D = this.youTubeApiDialog; D.visible = true; }; @@ -25042,9 +16250,12 @@ speechSynthesis.getVoices(); if (!assetUrl) { assetUrl = ref.assetUrl; } - var id = extractFileId(assetUrl); - var version = parseInt(extractFileVersion(assetUrl), 10); - var variantVersion = parseInt(extractVariantVersion(assetUrl), 10); + 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: '' }; } @@ -25164,7 +16375,7 @@ speechSynthesis.getVoices(); }; $app.methods.showDownloadDialog = function () { - this.$nextTick(() => adjustDialogZ(this.$refs.downloadDialog.$el)); + this.$nextTick(() => $app.adjustDialogZ(this.$refs.downloadDialog.$el)); this.downloadDialog.visible = true; }; @@ -25215,9 +16426,12 @@ speechSynthesis.getVoices(); break; } } - var id = extractFileId(assetUrl); - var version = parseInt(extractFileVersion(assetUrl), 10); - var variantVersion = parseInt(extractVariantVersion(assetUrl), 10); + var id = $utils.extractFileId(assetUrl); + var version = parseInt($utils.extractFileVersion(assetUrl), 10); + var variantVersion = parseInt( + $utils.extractVariantVersion(assetUrl), + 10 + ); await AssetBundleCacher.DeleteCache( id, version, @@ -25376,9 +16590,12 @@ speechSynthesis.getVoices(); if (!assetUrl) { return null; } - var fileId = extractFileId(assetUrl); - var fileVersion = parseInt(extractFileVersion(assetUrl), 10); - var variantVersion = parseInt(extractVariantVersion(assetUrl), 10); + 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, @@ -25496,7 +16713,7 @@ speechSynthesis.getVoices(); }; $app.methods.refreshInstancePlayerCount = function (instance) { - var L = API.parseLocation(instance); + var L = $utils.parseLocation(instance); if (L.worldId && L.instanceId) { API.getInstance({ worldId: L.worldId, @@ -25616,7 +16833,7 @@ speechSynthesis.getVoices(); }); $app.methods.showGalleryDialog = function (pageNum) { - this.$nextTick(() => adjustDialogZ(this.$refs.galleryDialog.$el)); + this.$nextTick(() => $app.adjustDialogZ(this.$refs.galleryDialog.$el)); this.galleryDialogVisible = true; this.refreshGalleryTable(); this.refreshVRCPlusIconsTable(); @@ -25695,7 +16912,7 @@ speechSynthesis.getVoices(); }); $app.methods.compareCurrentProfilePic = function (fileId) { - var currentProfilePicOverride = extractFileId( + var currentProfilePicOverride = $utils.extractFileId( API.currentUser.profilePicOverride ); if (fileId === currentProfilePicOverride) { @@ -26124,7 +17341,7 @@ speechSynthesis.getVoices(); }; $app.methods.checkCanInvite = function (location) { - var L = API.parseLocation(location); + var L = $utils.parseLocation(location); var instance = API.cachedInstances.get(location); if (instance?.closedAt) { return false; @@ -26146,7 +17363,7 @@ speechSynthesis.getVoices(); }; $app.methods.checkCanInviteSelf = function (location) { - var L = API.parseLocation(location); + var L = $utils.parseLocation(location); var instance = API.cachedInstances.get(location); if (instance?.closedAt) { return false; @@ -26165,253 +17382,6 @@ speechSynthesis.getVoices(); await configRepository.setInt('VRCX_sidePanelWidth', this.asideWidth); }; - // VRCX auto update - - $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' - } - }; - - $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 - }; - this.downloadQueue.set('VRCXUpdate', { - ref, - type, - updateSetupUrl, - updateHashUrl, - size - }); - this.downloadQueueTable.data = Array.from(this.downloadQueue.values()); - if (!this.downloadInProgress) { - this.downloadFileQueueUpdate(); - } - }; - - $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; - } - } - if (!downloadUrl) { - return; - } - var name = release.name; - var type = 'Manual'; - this.downloadVRCXUpdate(downloadUrl, hashUrl, size, name, type); - this.VRCXUpdateDialog.visible = false; - this.showDownloadDialog(); - } - } - }; - - $app.methods.restartVRCX = function (isUpgrade) { - AppApi.RestartApplication(isUpgrade); - }; - - $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); - } - } - } - 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); - } - }; - - $app.methods.saveAutoUpdateVRCX = async function () { - if (this.autoUpdateVRCX === 'Off') { - this.pendingVRCXUpdate = false; - } - await configRepository.setString( - 'VRCX_autoUpdateVRCX', - this.autoUpdateVRCX - ); - }; - - $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; - 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 - ); - } - } - } - }; - $app.methods.compareUnityVersion = function (unitySortNumber) { if (!API.cachedConfig.sdkUnityVersion) { console.error('No cachedConfig.sdkUnityVersion'); @@ -26929,7 +17899,7 @@ speechSynthesis.getVoices(); $app.methods.showPreviousInstancesUserDialog = function (userRef) { this.$nextTick(() => - adjustDialogZ(this.$refs.previousInstancesUserDialog.$el) + $app.adjustDialogZ(this.$refs.previousInstancesUserDialog.$el) ); var D = this.previousInstancesUserDialog; D.userRef = userRef; @@ -26943,9 +17913,9 @@ speechSynthesis.getVoices(); database.getpreviousInstancesByUserId(D.userRef).then((data) => { var array = []; for (var ref of data.values()) { - ref.$location = API.parseLocation(ref.location); + ref.$location = $utils.parseLocation(ref.location); if (ref.time > 0) { - ref.timer = timeToText(ref.time); + ref.timer = $app.timeToText(ref.time); } else { ref.timer = ''; } @@ -26984,7 +17954,7 @@ speechSynthesis.getVoices(); .displayName, location: row.location }); - removeFromArray( + $app.removeFromArray( this.previousInstancesUserDialogTable.data, row ); @@ -27029,7 +17999,7 @@ speechSynthesis.getVoices(); $app.methods.showPreviousInstancesWorldDialog = function (worldRef) { this.$nextTick(() => - adjustDialogZ(this.$refs.previousInstancesWorldDialog.$el) + $app.adjustDialogZ(this.$refs.previousInstancesWorldDialog.$el) ); var D = this.previousInstancesWorldDialog; D.worldRef = worldRef; @@ -27043,9 +18013,9 @@ speechSynthesis.getVoices(); database.getpreviousInstancesByWorldId(D.worldRef).then((data) => { var array = []; for (var ref of data.values()) { - ref.$location = API.parseLocation(ref.location); + ref.$location = $utils.parseLocation(ref.location); if (ref.time > 0) { - ref.timer = timeToText(ref.time); + ref.timer = $app.timeToText(ref.time); } else { ref.timer = ''; } @@ -27068,7 +18038,7 @@ speechSynthesis.getVoices(); database.deleteGameLogInstanceByInstanceId({ location: row.location }); - removeFromArray( + $app.removeFromArray( this.previousInstancesWorldDialogTable.data, row ); @@ -27113,10 +18083,10 @@ speechSynthesis.getVoices(); $app.methods.showPreviousInstanceInfoDialog = function (instanceId) { this.$nextTick(() => - adjustDialogZ(this.$refs.previousInstanceInfoDialog.$el) + $app.adjustDialogZ(this.$refs.previousInstanceInfoDialog.$el) ); var D = this.previousInstanceInfoDialog; - D.$location = API.parseLocation(instanceId); + D.$location = $utils.parseLocation(instanceId); D.visible = true; D.loading = true; this.refreshPreviousInstanceInfoTable(); @@ -27127,7 +18097,7 @@ speechSynthesis.getVoices(); database.getPlayersFromInstance(D.$location.tag).then((data) => { var array = []; for (var entry of Array.from(data.values())) { - entry.timer = timeToText(entry.time); + entry.timer = $app.timeToText(entry.time); array.push(entry); } array.sort(compareByCreatedAt); @@ -27255,39 +18225,6 @@ speechSynthesis.getVoices(); } }; - $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; - 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 - ); - } - }; - $app.methods.getNameColour = async function (userId) { var hue = await AppApi.GetColourFromUserID(userId); return this.HueToHex(hue); @@ -27451,7 +18388,7 @@ speechSynthesis.getVoices(); ref.$online_for = API.currentUser.$online_for; ref.$offline_for = API.currentUser.$offline_for; - ref.$location = API.parseLocation(currentLocation); + ref.$location = $utils.parseLocation(currentLocation); if (!this.isGameRunning || this.gameLogDisabled) { ref.$location_at = API.currentUser.$location_at; ref.$travelingToTime = API.currentUser.$travelingToTime; @@ -27485,7 +18422,7 @@ speechSynthesis.getVoices(); } if (this.isRealInstance(location) && lastLocation !== location) { var dt = new Date().toJSON(); - var L = API.parseLocation(location); + var L = $utils.parseLocation(location); var entry = { created_at: dt, type: 'Location', @@ -27644,7 +18581,9 @@ speechSynthesis.getVoices(); }; $app.methods.showWorldImportDialog = function () { - this.$nextTick(() => adjustDialogZ(this.$refs.worldImportDialog.$el)); + this.$nextTick(() => + $app.adjustDialogZ(this.$refs.worldImportDialog.$el) + ); var D = this.worldImportDialog; this.resetWorldImport(); D.visible = true; @@ -27696,7 +18635,7 @@ speechSynthesis.getVoices(); $app.methods.deleteItemWorldImport = function (ref) { var D = this.worldImportDialog; - removeFromArray(this.worldImportTable.data, ref); + $app.removeFromArray(this.worldImportTable.data, ref); D.worldIdList.delete(ref.id); }; @@ -27754,7 +18693,7 @@ speechSynthesis.getVoices(); D.worldImportLocalFavoriteGroup ); } - removeFromArray(this.worldImportTable.data, ref); + $app.removeFromArray(this.worldImportTable.data, ref); D.worldIdList.delete(ref.id); D.importProgress++; } @@ -27790,7 +18729,7 @@ speechSynthesis.getVoices(); $app.methods.showWorldExportDialog = function () { this.$nextTick(() => - adjustDialogZ(this.$refs.worldExportDialogRef.$el) + $app.adjustDialogZ(this.$refs.worldExportDialogRef.$el) ); this.worldExportFavoriteGroup = null; this.worldExportLocalFavoriteGroup = null; @@ -27881,7 +18820,9 @@ speechSynthesis.getVoices(); }; $app.methods.showAvatarImportDialog = function () { - this.$nextTick(() => adjustDialogZ(this.$refs.avatarImportDialog.$el)); + this.$nextTick(() => + $app.adjustDialogZ(this.$refs.avatarImportDialog.$el) + ); var D = this.avatarImportDialog; this.resetAvatarImport(); D.visible = true; @@ -27933,7 +18874,7 @@ speechSynthesis.getVoices(); $app.methods.deleteItemAvatarImport = function (ref) { var D = this.avatarImportDialog; - removeFromArray(this.avatarImportTable.data, ref); + $app.removeFromArray(this.avatarImportTable.data, ref); D.avatarIdList.delete(ref.id); }; @@ -27991,7 +18932,7 @@ speechSynthesis.getVoices(); D.avatarImportLocalFavoriteGroup ); } - removeFromArray(this.avatarImportTable.data, ref); + $app.removeFromArray(this.avatarImportTable.data, ref); D.avatarIdList.delete(ref.id); D.importProgress++; } @@ -28027,7 +18968,7 @@ speechSynthesis.getVoices(); $app.methods.showAvatarExportDialog = function () { this.$nextTick(() => - adjustDialogZ(this.$refs.avatarExportDialogRef.$el) + $app.adjustDialogZ(this.$refs.avatarExportDialogRef.$el) ); this.avatarExportFavoriteGroup = null; this.avatarExportLocalFavoriteGroup = null; @@ -28120,7 +19061,9 @@ speechSynthesis.getVoices(); }; $app.methods.showFriendImportDialog = function () { - this.$nextTick(() => adjustDialogZ(this.$refs.friendImportDialog.$el)); + this.$nextTick(() => + $app.adjustDialogZ(this.$refs.friendImportDialog.$el) + ); var D = this.friendImportDialog; this.resetFriendImport(); D.visible = true; @@ -28170,7 +19113,7 @@ speechSynthesis.getVoices(); $app.methods.deleteItemFriendImport = function (ref) { var D = this.friendImportDialog; - removeFromArray(this.friendImportTable.data, ref); + $app.removeFromArray(this.friendImportTable.data, ref); D.userIdList.delete(ref.id); }; @@ -28211,7 +19154,7 @@ speechSynthesis.getVoices(); } var ref = data[i]; await this.addFavoriteUser(ref, D.friendImportFavoriteGroup); - removeFromArray(this.friendImportTable.data, ref); + $app.removeFromArray(this.friendImportTable.data, ref); D.userIdList.delete(ref.id); D.importProgress++; } @@ -28244,7 +19187,7 @@ speechSynthesis.getVoices(); $app.methods.showFriendExportDialog = function () { this.$nextTick(() => - adjustDialogZ(this.$refs.friendExportDialogRef.$el) + $app.adjustDialogZ(this.$refs.friendExportDialogRef.$el) ); this.friendExportFavoriteGroup = null; this.updateFriendExportDialog(); @@ -28380,7 +19323,9 @@ speechSynthesis.getVoices(); }); $app.methods.showNoteExportDialog = function () { - this.$nextTick(() => adjustDialogZ(this.$refs.noteExportDialog.$el)); + this.$nextTick(() => + $app.adjustDialogZ(this.$refs.noteExportDialog.$el) + ); var D = this.noteExportDialog; D.progress = 0; D.progressTotal = 0; @@ -28405,7 +19350,7 @@ speechSynthesis.getVoices(); }; $app.methods.removeFromNoteExportTable = function (ref) { - removeFromArray(this.noteExportTable.data, ref); + $app.removeFromArray(this.noteExportTable.data, ref); }; $app.methods.exportNoteExport = async function () { @@ -28421,7 +19366,7 @@ speechSynthesis.getVoices(); targetUserId: ctx.id, note: ctx.memo.slice(0, 256) }); - removeFromArray(this.noteExportTable.data, ctx); + $app.removeFromArray(this.noteExportTable.data, ctx); D.progress++; await new Promise((resolve) => { workerTimers.setTimeout(resolve, 5000); @@ -28449,7 +19394,7 @@ speechSynthesis.getVoices(); $app.methods.showAvatarProviderDialog = function () { this.$nextTick(() => - adjustDialogZ(this.$refs.avatarProviderDialog.$el) + $app.adjustDialogZ(this.$refs.avatarProviderDialog.$el) ); var D = this.avatarProviderDialog; D.visible = true; @@ -28623,7 +19568,7 @@ speechSynthesis.getVoices(); } } if (!worldInFavorites) { - removeFromArray(this.localWorldFavoritesList, worldId); + $app.removeFromArray(this.localWorldFavoritesList, worldId); database.removeWorldFromCache(worldId); } database.removeWorldFromFavorites(worldId, group); @@ -28785,7 +19730,7 @@ speechSynthesis.getVoices(); this.localWorldFavoriteGroups.push(newName); this.localWorldFavorites[newName] = this.localWorldFavorites[group]; - removeFromArray(this.localWorldFavoriteGroups, group); + $app.removeFromArray(this.localWorldFavoriteGroups, group); delete this.localWorldFavorites[group]; database.renameWorldFavoriteGroup(newName, group); this.sortLocalWorldFavorites(); @@ -28824,7 +19769,7 @@ speechSynthesis.getVoices(); worldIdRemoveList.add(favoriteGroup[i].id); } - removeFromArray(this.localWorldFavoriteGroups, group); + $app.removeFromArray(this.localWorldFavoriteGroups, group); delete this.localWorldFavorites[group]; database.deleteWorldFavoriteGroup(group); @@ -28847,7 +19792,7 @@ speechSynthesis.getVoices(); } worldIdRemoveList.forEach((id) => { - removeFromArray(this.localWorldFavoritesList, id); + $app.removeFromArray(this.localWorldFavoritesList, id); database.removeWorldFromCache(id); }); }; @@ -28979,7 +19924,7 @@ speechSynthesis.getVoices(); } } if (!avatarInFavorites) { - removeFromArray(this.localAvatarFavoritesList, avatarId); + $app.removeFromArray(this.localAvatarFavoritesList, avatarId); if (!this.avatarHistory.has(avatarId)) { database.removeAvatarFromCache(avatarId); } @@ -29154,7 +20099,7 @@ speechSynthesis.getVoices(); this.localAvatarFavoriteGroups.push(newName); this.localAvatarFavorites[newName] = this.localAvatarFavorites[group]; - removeFromArray(this.localAvatarFavoriteGroups, group); + $app.removeFromArray(this.localAvatarFavoriteGroups, group); delete this.localAvatarFavorites[group]; database.renameAvatarFavoriteGroup(newName, group); this.sortLocalAvatarFavorites(); @@ -29193,7 +20138,7 @@ speechSynthesis.getVoices(); avatarIdRemoveList.add(favoriteGroup[i].id); } - removeFromArray(this.localAvatarFavoriteGroups, group); + $app.removeFromArray(this.localAvatarFavoriteGroups, group); delete this.localAvatarFavorites[group]; database.deleteAvatarFavoriteGroup(group); @@ -29243,7 +20188,7 @@ speechSynthesis.getVoices(); } } if (!avatarInFavorites) { - removeFromArray(this.localAvatarFavoritesList, id); + $app.removeFromArray(this.localAvatarFavoritesList, id); if (!this.avatarHistory.has(id)) { database.removeAvatarFromCache(id); } @@ -29339,11 +20284,11 @@ speechSynthesis.getVoices(); continue; } if (ctx.isVIP) { - removeFromArray(this.onlineFriends_, ctx); + $app.removeFromArray(this.onlineFriends_, ctx); this.vipFriends_.push(ctx); this.sortVIPFriends = true; } else { - removeFromArray(this.vipFriends_, ctx); + $app.removeFromArray(this.vipFriends_, ctx); this.onlineFriends_.push(ctx); this.sortOnlineFriends = true; } @@ -29385,7 +20330,7 @@ speechSynthesis.getVoices(); $app.methods.showChatboxBlacklistDialog = function () { this.$nextTick(() => - adjustDialogZ(this.$refs.chatboxBlacklistDialog.$el) + $app.adjustDialogZ(this.$refs.chatboxBlacklistDialog.$el) ); var D = this.chatboxBlacklistDialog; D.visible = true; @@ -29433,15 +20378,13 @@ speechSynthesis.getVoices(); await this.saveChatboxUserBlacklist(); this.getCurrentInstanceUserList(); this.$nextTick(() => - adjustDialogZ(this.$refs.chatboxBlacklistDialog.$el) + $app.adjustDialogZ(this.$refs.chatboxBlacklistDialog.$el) ); }; // #endregion - // #region | App: Groups + // #region | App: Instance queuing - API.cachedGroups = new Map(); - API.currentUserGroups = new Map(); API.queuedInstances = new Map(); $app.methods.removeAllQueuedInstances = function () { @@ -29478,7 +20421,7 @@ speechSynthesis.getVoices(); return; } if (!API.queuedInstances.has(instanceId)) { - var L = API.parseLocation(instanceId); + var L = $utils.parseLocation(instanceId); if (L.worldId && L.instanceId) { API.getInstance({ worldId: L.worldId, @@ -29503,7 +20446,7 @@ speechSynthesis.getVoices(); ref.$msgBox.close(); API.queuedInstances.delete(instanceId); } - var L = API.parseLocation(instanceId); + var L = $utils.parseLocation(instanceId); var group = API.cachedGroups.get(L.groupId); var groupName = group?.name ?? ''; var worldName = ref?.$worldName ?? ''; @@ -29589,2085 +20532,7 @@ speechSynthesis.getVoices(); }); }; - /** - * @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) { - 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(); - }); - - $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 - }; - 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 - } - }); - } - }); - - $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.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 - }); - } - } - } - ); - }; - - /** - * @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; - }); - }; - - $app.methods.getAllGroupBans = async function (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; - } - }; - - 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); - } - }); - - $app.methods.getAllGroupLogs = async function (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; - } - }; - - /** - * @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; - }); - - $app.methods.getAuditLogTypeName = function (auditLogType) { - if (!auditLogType) { - return ''; - } - return auditLogType - .replace('group.', '') - .replace(/\./g, ' ') - .replace(/\b\w/g, (l) => l.toUpperCase()); - }; - - /** - * @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); - } - } - }); - - $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; - 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; - } - }; - - /** - * @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); - } - }); - - $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; - } - }; - - $app.methods.getAllGroupBlockedRequests = async function (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; - } - }; - - /** - * @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; - }; - - $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; - - this.groupChange( - ref, - `Owner changed from ${oldDisplayName} to ${newDisplayName}` - ); - }; - - $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`); - } - } - 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`); - } - } - }; - - $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 - } - }); - - // delay to wait for json to be assigned to ref - workerTimers.setTimeout(this.saveCurrentUserGroups, 100); - }; - - $app.data.currentUserGroupsInit = false; - - $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.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; - } - - 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; - }; - - 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 - }); - } - }; - - $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.showGroupDialog = function (groupId) { - if (!groupId) { - return; - } - if ( - this.groupMemberModeration.visible && - this.groupMemberModeration.id !== groupId - ) { - this.groupMemberModeration.visible = false; - } - 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; - }); - }; - - $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.data.groupDialogLastActiveTab = ''; - $app.data.groupDialogLastMembers = ''; - $app.data.groupDialogLastGallery = ''; - - $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.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 - }); - }; - - $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' - }); - } - return args; - }); - }; - - API.$on('LOGOUT', function () { - $app.groupDialog.visible = false; - }); - - $app.methods.leaveGroup = function (groupId) { - return API.leaveGroup({ - groupId - }); - }; - - $app.methods.cancelGroupRequest = function (groupId) { - return API.cancelGroupRequest({ - groupId - }); - }; - - $app.methods.setGroupRepresentation = function (groupId) { - return API.setGroupRepresentation(groupId, { isRepresenting: true }); - }; - - $app.methods.clearGroupRepresentation = function (groupId) { - return API.setGroupRepresentation(groupId, { isRepresenting: 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; - }); - }; - - $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; - }); - }; + // #endregion $app.methods.sendNotificationResponse = function ( notificationId, @@ -31691,441 +20556,6 @@ speechSynthesis.getVoices(); }); }; - $app.methods.onGroupJoined = function (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; - }); - } - }; - - $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'); - }); - } - }; - - // 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; - } - 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; - } else { - this.groupMembersSearchExecute(); - this.groupMembersSearchTimer = setTimeout(() => { - if (this.groupMembersSearchPending) { - this.groupMembersSearchExecute(); - } - this.groupMembersSearchTimer = null; - }, 500); - } - }; - - $app.methods.groupMembersSearchExecute = function () { - try { - this.groupMembersSearchDebounce(); - } catch (err) { - console.error(err); - } - this.groupMembersSearchTimer = null; - this.groupMembersSearchPending = false; - }; - - // group posts - - $app.methods.updateGroupPostSearch = function () { - 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; - }); - }; - - // 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 (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(); - }; - - $app.methods.loadMoreGroupMembers = async function () { - 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; - }); - }; - - $app.methods.loadAllGroupMembers = async function () { - 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(); - } - }; - - $app.methods.setGroupMemberSortOrder = async function (sortOrder) { - var D = this.groupDialog; - if (D.memberSortOrder === sortOrder) { - return; - } - D.memberSortOrder = sortOrder; - await this.getGroupDialogGroupMembers(); - }; - - $app.methods.setGroupMemberFilter = async function (filter) { - var D = this.groupDialog; - if (D.memberFilter === filter) { - return; - } - 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; - } - return false; - }; - - // group gallery - - $app.data.isGroupGalleryLoading = 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); - } - } - }); - - $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); - } - this.isGroupGalleryLoading = false; - }; - - $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; - } - } - } 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; - }); - D.userIds = [userId]; - } - }; - - API.$on('LOGOUT', function () { - $app.inviteGroupDialog.visible = false; - }); - - $app.methods.sendGroupInvite = function () { - 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(); - } - }); - }; - - $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; @@ -32204,7 +20634,7 @@ speechSynthesis.getVoices(); } var link = document.createElement('a'); link.href = response.data; - var fileName = `${extractFileId(url)}.png`; + var fileName = `${$utils.extractFileId(url)}.png`; if (!fileName) { fileName = `${url.split('/').pop()}.png`; } @@ -32218,7 +20648,7 @@ speechSynthesis.getVoices(); } catch { new Noty({ type: 'error', - text: escapeTag(`Failed to download image. ${url}`) + text: $app.escapeTag(`Failed to download image. ${url}`) }).show(); } }; @@ -32242,7 +20672,7 @@ speechSynthesis.getVoices(); } catch { new Noty({ type: 'error', - text: escapeTag('Failed to download JSON.') + text: $app.escapeTag('Failed to download JSON.') }).show(); } }; @@ -32351,7 +20781,6 @@ speechSynthesis.getVoices(); this.groupDialog.memberSortOrder = this.groupDialogSortingOptions.joinedAtDesc; }; - // $app.methods.applyLanguageStrings(); $app.data.appLanguage = (await configRepository.getString('VRCX_appLanguage')) ?? 'en'; @@ -32390,7 +20819,7 @@ speechSynthesis.getVoices(); // #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); + $app.currentUserFeedbackData = $utils.buildTreeData(args.json); } }); @@ -32398,28 +20827,6 @@ speechSynthesis.getVoices(); return API.getUserFeedback({ userId: API.currentUser.id }); }; - $app.methods.gameLogIsFriend = function (row) { - if (typeof row.isFriend !== 'undefined') { - return row.isFriend; - } - if (!row.userId) { - return false; - } - row.isFriend = this.friends.has(row.userId); - return row.isFriend; - }; - - $app.methods.gameLogIsFavorite = function (row) { - if (typeof row.isFavorite !== 'undefined') { - return row.isFavorite; - } - if (!row.userId) { - return false; - } - row.isFavorite = this.localFavoriteFriends.has(row.userId); - return row.isFavorite; - }; - $app.data.changeLogDialog = { visible: false, buildName: '', @@ -32427,7 +20834,9 @@ speechSynthesis.getVoices(); }; $app.methods.showChangeLogDialog = function () { - this.$nextTick(() => adjustDialogZ(this.$refs.changeLogDialog.$el)); + this.$nextTick(() => + $app.adjustDialogZ(this.$refs.changeLogDialog.$el) + ); this.changeLogDialog.visible = true; this.checkForVRCXUpdate(); }; @@ -32439,7 +20848,9 @@ speechSynthesis.getVoices(); }; $app.methods.showGallerySelectDialog = function () { - this.$nextTick(() => adjustDialogZ(this.$refs.gallerySelectDialog.$el)); + this.$nextTick(() => + $app.adjustDialogZ(this.$refs.gallerySelectDialog.$el) + ); var D = this.gallerySelectDialog; D.visible = true; this.refreshGalleryTable(); @@ -32538,7 +20949,7 @@ speechSynthesis.getVoices(); ref.avatarStats.totalTextureUsage / 1048576 ).toFixed(2)} MB`; } - $app.avatarDialog.fileAnalysis = buildTreeData(args.json); + $app.avatarDialog.fileAnalysis = $utils.buildTreeData(args.json); }); $app.methods.getAvatarFileAnalysis = function () { @@ -32564,8 +20975,8 @@ speechSynthesis.getVoices(); if (!assetUrl) { assetUrl = D.ref.assetUrl; } - var fileId = extractFileId(assetUrl); - var version = parseInt(extractFileVersion(assetUrl), 10); + var fileId = $utils.extractFileId(assetUrl); + var version = parseInt($utils.extractFileVersion(assetUrl), 10); if (!fileId || !version) { this.$message({ message: 'File Analysis unavailable', @@ -32593,7 +21004,7 @@ speechSynthesis.getVoices(); return; } this.$nextTick(() => - adjustDialogZ(this.$refs.fullscreenImageDialog.$el) + $app.adjustDialogZ(this.$refs.fullscreenImageDialog.$el) ); var D = this.fullscreenImageDialog; D.imageUrl = imageUrl; @@ -32683,1173 +21094,6 @@ speechSynthesis.getVoices(); }); }; - // #endregion - // #region | Dialog: registry backup dialog - - $app.data.registryBackupDialog = { - visible: false - }; - - $app.data.registryBackupTable = { - data: [], - tableProps: { - stripe: true, - size: 'mini', - defaultSort: { - prop: 'date', - order: 'descending' - } - }, - layout: 'table' - }; - - $app.methods.showRegistryBackupDialog = function () { - this.$nextTick(() => - adjustDialogZ(this.$refs.registryBackupDialog.$el) - ); - 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' - ); - if (!backupsJson) { - backupsJson = JSON.stringify([]); - } - this.registryBackupTable.data = JSON.parse(backupsJson); - }; - - $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' - } - ); - if (name.action === 'confirm') { - this.backupVrcRegistry(name.value); - } - }; - - $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) - ); - await this.updateRegistryBackupDialog(); - }; - - $app.methods.deleteVrcRegistryBackup = async function (row) { - var backups = this.registryBackupTable.data; - removeFromArray(backups, row); - await configRepository.setString( - 'VRCX_VRChatRegistryBackups', - JSON.stringify(backups) - ); - 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.saveVrcRegistryBackupToFile = function (row) { - this.downloadAndSaveJson(row.name, row.data); - }; - - $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 { - this.$message({ - message: 'Invalid JSON', - type: 'error' - }); - } - }; - - $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' - }); - }); - } - }); - }; - - $app.methods.clearVrcRegistryDialog = function () { - this.registryBackupTable.data = []; - }; - - $app.methods.checkAutoBackupRestoreVrcRegistry = async function () { - 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(); - } - }; - - $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; - } - } - 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 - removeFromArray(backups, backup); - } - }); - 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: '' - }; - - $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.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.data.groupLogsModerationTable = { - data: [], - filters: [ - { - prop: ['description'], - 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.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.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] - } - }; - - $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] - } - }; - - $app.data.groupMemberModerationTableForceUpdate = 0; - - $app.methods.setGroupMemberModerationTable = function (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++; - }; - - $app.methods.showGroupMemberModerationDialog = function (groupId) { - this.$nextTick(() => - 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); - }; - - $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; - } - } - 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++; - }; - - $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; - } - for (var i = 0; i < this.groupBlockedModerationTable.data.length; i++) { - var row = this.groupBlockedModerationTable.data[i]; - row.$selected = false; - } - // 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.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++; - }; - - $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.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++; - }; - - $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.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.$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.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' - }); - } finally { - D.progressCurrent = 0; - D.progressTotal = 0; - } - }; - - $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.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; - } - }; - - $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.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.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.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.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); - } - }); - - 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.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 - }); - } - } - 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; - } - }; - - $app.methods.selectGroupMemberUserId = async function () { - 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 = ''; - }; - - $app.methods.addGroupMemberToSelection = async function (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++; - }; - - $app.data.groupPostEditDialog = { - visible: false, - groupRef: {}, - title: '', - text: '', - sendNotification: true, - visibility: 'group', - roleIds: [], - postId: '', - groupId: '' - }; - - $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; - }); - 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; - }); - 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; - }); - D.visible = false; - }; - // #endregion // #region | V-Bucks @@ -33943,127 +21187,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 @@ -34160,8 +21283,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 @@ -34173,7 +21304,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 00000000..6ad6bb3a --- /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 00000000..c17fead7 --- /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 00000000..5fa48ca3 --- /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 00000000..4a4eae3f --- /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 00000000..ab180a3f --- /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 00000000..9bc2ccfb --- /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 00000000..4a34343a --- /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 00000000..759e3d40 --- /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 00000000..f3a66b5a --- /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 00000000..0f6d79cf --- /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 00000000..43869fe4 --- /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 00000000..1c009686 --- /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 00000000..0c614f92 --- /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 00000000..e762fca4 --- /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 00000000..899f163b --- /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 00000000..5b06942a --- /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 00000000..08c80670 --- /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 00000000..73373f04 --- /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 00000000..3eccdc04 --- /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 00000000..76ea1b91 --- /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 00000000..b4965335 --- /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 00000000..7a8bc4f2 --- /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 00000000..cbfedd8c --- /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 00000000..8d8fb709 --- /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 00000000..4cdf50a3 --- /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 23fd6161..247fad65 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") + include ./mixins/dialogs/worldDialog.pug + +worldDialog() - //- 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") + include ./mixins/dialogs/avatarDialog.pug + +avatarDialog() - //- 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") + include ./mixins/dialogs/groupDialog.pug + +groupDialog() - //- 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') }} + include ./mixins/dialogs/newInstance.pug + +newInstance() - 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') }} + include ./mixins/dialogs/feedFilters.pug + +feedFilters() - 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') }} + include ./mixins/dialogs/openSourceSoftwareNotice.pug + +openSourceSoftwareNotice() - 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') }} + include ./mixins/dialogs/groups.pug + +groups() - 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') }} + include ./mixins/dialogs/currentUser.pug + +currentUser() - .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") + include ./mixins/dialogs/invites.pug + +invites() - //- 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) }}) + include ./mixins/dialogs/launch.pug + +launch() - //- 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') }} + include ./mixins/dialogs/screenshotMetadata.pug + +screenshotMetadata() - //- 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') }} + include ./mixins/dialogs/vrcx.pug + +vrcx() - //- 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') }} + include ./mixins/dialogs/settings.pug + +settings() - //- 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') }} + include ./mixins/dialogs/previousInstances.pug + +previousInstances() - //- 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') }} + include ./mixins/dialogs/tags.pug + +tags() - //- 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') }} - - //- 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') }} - - //- 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') }} - - //- 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/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") - - //- 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 00000000..157a47c8 --- /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 00000000..49321b96 --- /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 00000000..a6b83f97 --- /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 00000000..3f735902 --- /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 00000000..23a967be --- /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 00000000..a8701416 --- /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 00000000..ca695b8b --- /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 00000000..903c71e1 --- /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 00000000..d04fc879 --- /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 00000000..0aa6c66f --- /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 00000000..f38be40b --- /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 00000000..be7a46ef --- /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 00000000..65e51e10 --- /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 00000000..37ca68d8 --- /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 00000000..3d72a194 --- /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 00000000..0add596b --- /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 00000000..9e04844b --- /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 00000000..4699814c --- /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 00000000..38e123ee --- /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 00000000..00d9d454 --- /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 3b58e8d7..0a345eff 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 9b4f3860..d06989db 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 8653e769..af45f62e 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 53e0bec9..0b6b78bd 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 afe83516..a553ac52 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({