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:
- '
' +
- '
' +
- '' +
- 'Closed At: {{ closedAt | formatDate(\'long\') }}' +
- '{{ $t("dialog.user.info.close_instance") }}' +
- '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:
+ '' +
+ '
' +
+ '' +
+ 'Closed At: {{ closedAt | formatDate(\'long\') }}' +
+ '{{ $t("dialog.user.info.close_instance") }}' +
+ '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({