refactor store

This commit is contained in:
pa
2026-03-06 22:42:43 +09:00
parent e665b3815d
commit 8ddedb2d2d
29 changed files with 3269 additions and 888 deletions
+43 -37
View File
@@ -30,8 +30,8 @@
"@tailwindcss/vite": "^4.2.1", "@tailwindcss/vite": "^4.2.1",
"@tanstack/react-query": "^5.90.21", "@tanstack/react-query": "^5.90.21",
"@tanstack/vue-table": "^8.21.3", "@tanstack/vue-table": "^8.21.3",
"@tanstack/vue-virtual": "^3.13.19", "@tanstack/vue-virtual": "^3.13.21",
"@types/node": "^25.3.3", "@types/node": "^24.12.0",
"@vee-validate/zod": "^4.15.1", "@vee-validate/zod": "^4.15.1",
"@vitejs/plugin-vue": "^6.0.4", "@vitejs/plugin-vue": "^6.0.4",
"@vitejs/plugin-vue-jsx": "^5.1.4", "@vitejs/plugin-vue-jsx": "^5.1.4",
@@ -188,7 +188,6 @@
"integrity": "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==", "integrity": "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"@babel/code-frame": "^7.29.0", "@babel/code-frame": "^7.29.0",
"@babel/generator": "^7.29.0", "@babel/generator": "^7.29.0",
@@ -708,7 +707,6 @@
} }
], ],
"license": "MIT", "license": "MIT",
"peer": true,
"engines": { "engines": {
"node": ">=20.19.0" "node": ">=20.19.0"
}, },
@@ -749,7 +747,6 @@
} }
], ],
"license": "MIT", "license": "MIT",
"peer": true,
"engines": { "engines": {
"node": ">=20.19.0" "node": ">=20.19.0"
} }
@@ -1359,6 +1356,7 @@
"dev": true, "dev": true,
"license": "BSD-2-Clause", "license": "BSD-2-Clause",
"optional": true, "optional": true,
"peer": true,
"dependencies": { "dependencies": {
"cross-dirname": "^0.1.0", "cross-dirname": "^0.1.0",
"debug": "^4.3.4", "debug": "^4.3.4",
@@ -1380,6 +1378,7 @@
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"optional": true, "optional": true,
"peer": true,
"dependencies": { "dependencies": {
"graceful-fs": "^4.2.0", "graceful-fs": "^4.2.0",
"jsonfile": "^6.0.1", "jsonfile": "^6.0.1",
@@ -1396,6 +1395,7 @@
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"optional": true, "optional": true,
"peer": true,
"dependencies": { "dependencies": {
"universalify": "^2.0.0" "universalify": "^2.0.0"
}, },
@@ -1410,6 +1410,7 @@
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"optional": true, "optional": true,
"peer": true,
"engines": { "engines": {
"node": ">= 10.0.0" "node": ">= 10.0.0"
} }
@@ -4139,9 +4140,9 @@
} }
}, },
"node_modules/@tanstack/virtual-core": { "node_modules/@tanstack/virtual-core": {
"version": "3.13.19", "version": "3.13.21",
"resolved": "https://registry.npmjs.org/@tanstack/virtual-core/-/virtual-core-3.13.19.tgz", "resolved": "https://registry.npmjs.org/@tanstack/virtual-core/-/virtual-core-3.13.21.tgz",
"integrity": "sha512-/BMP7kNhzKOd7wnDeB8NrIRNLwkf5AhCYCvtfZV2GXWbBieFm/el0n6LOAXlTi6ZwHICSNnQcIxRCWHrLzDY+g==", "integrity": "sha512-ww+fmLHyCbPSf7JNbWZP3g7wl6SdNo3ah5Aiw+0e9FDErkVHLKprYUrwTm7dF646FtEkN/KkAKPYezxpmvOjxw==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"funding": { "funding": {
@@ -4227,13 +4228,13 @@
} }
}, },
"node_modules/@tanstack/vue-virtual": { "node_modules/@tanstack/vue-virtual": {
"version": "3.13.19", "version": "3.13.21",
"resolved": "https://registry.npmjs.org/@tanstack/vue-virtual/-/vue-virtual-3.13.19.tgz", "resolved": "https://registry.npmjs.org/@tanstack/vue-virtual/-/vue-virtual-3.13.21.tgz",
"integrity": "sha512-07Fp1TYuIziB4zIDA/moeDKHODePy3K1fN4c4VIAGnkxo1+uOvBJP7m54CoxKiQX6Q9a1dZnznrwOg9C86yvvA==", "integrity": "sha512-zneUNdQTcUhoDl6+ek+/O4S9gSZRAc2q7VLscZ4WZnFfZcHc3M7OyVCfSDC3hGuwFqzfL8Cx5bZF6zbGCYwXmw==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@tanstack/virtual-core": "3.13.19" "@tanstack/virtual-core": "3.13.21"
}, },
"funding": { "funding": {
"type": "github", "type": "github",
@@ -4352,13 +4353,13 @@
"license": "MIT" "license": "MIT"
}, },
"node_modules/@types/node": { "node_modules/@types/node": {
"version": "25.3.3", "version": "24.12.0",
"resolved": "https://registry.npmjs.org/@types/node/-/node-25.3.3.tgz", "resolved": "https://registry.npmjs.org/@types/node/-/node-24.12.0.tgz",
"integrity": "sha512-DpzbrH7wIcBaJibpKo9nnSQL0MTRdnWttGyE5haGwK86xgMOkFLp7vEyfQPGLOJh5wNYiJ3V9PmUMDhV9u8kkQ==", "integrity": "sha512-GYDxsZi3ChgmckRT9HPU0WEhKLP08ev/Yfcq2AstjrDASOYCSXeyjDsHg4v5t4jOj7cyDX3vmprafKlWIG9MXQ==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"undici-types": "~7.18.0" "undici-types": "~7.16.0"
} }
}, },
"node_modules/@types/plist": { "node_modules/@types/plist": {
@@ -4950,7 +4951,6 @@
"integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==", "integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"peer": true,
"bin": { "bin": {
"acorn": "bin/acorn" "acorn": "bin/acorn"
}, },
@@ -4984,7 +4984,6 @@
"integrity": "sha512-IWrosm/yrn43eiKqkfkHis7QioDleaXQHdDVPKg0FSwwd/DuvyX79TZnFOnYpB7dcsFAMmtFztZuXPDvSePkFw==", "integrity": "sha512-IWrosm/yrn43eiKqkfkHis7QioDleaXQHdDVPKg0FSwwd/DuvyX79TZnFOnYpB7dcsFAMmtFztZuXPDvSePkFw==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"fast-deep-equal": "^3.1.1", "fast-deep-equal": "^3.1.1",
"fast-json-stable-stringify": "^2.0.0", "fast-json-stable-stringify": "^2.0.0",
@@ -5541,7 +5540,6 @@
} }
], ],
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"baseline-browser-mapping": "^2.9.0", "baseline-browser-mapping": "^2.9.0",
"caniuse-lite": "^1.0.30001759", "caniuse-lite": "^1.0.30001759",
@@ -6192,7 +6190,8 @@
"integrity": "sha512-+R08/oI0nl3vfPcqftZRpytksBXDzOUveBq/NBVx0sUp1axwzPQrKinNx5yd5sxPu8j1wIy8AfnVQ+5eFdha6Q==", "integrity": "sha512-+R08/oI0nl3vfPcqftZRpytksBXDzOUveBq/NBVx0sUp1axwzPQrKinNx5yd5sxPu8j1wIy8AfnVQ+5eFdha6Q==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"optional": true "optional": true,
"peer": true
}, },
"node_modules/cross-env": { "node_modules/cross-env": {
"version": "10.1.0", "version": "10.1.0",
@@ -6539,7 +6538,6 @@
"integrity": "sha512-glMJgnTreo8CFINujtAhCgN96QAqApDMZ8Vl1r8f0QT8QprvC1UCltV4CcWj20YoIyLZx6IUskaJZ0NV8fokcg==", "integrity": "sha512-glMJgnTreo8CFINujtAhCgN96QAqApDMZ8Vl1r8f0QT8QprvC1UCltV4CcWj20YoIyLZx6IUskaJZ0NV8fokcg==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"app-builder-lib": "26.8.1", "app-builder-lib": "26.8.1",
"builder-util": "26.8.1", "builder-util": "26.8.1",
@@ -7006,6 +7004,7 @@
"dev": true, "dev": true,
"hasInstallScript": true, "hasInstallScript": true,
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"@electron/asar": "^3.2.1", "@electron/asar": "^3.2.1",
"debug": "^4.1.1", "debug": "^4.1.1",
@@ -7026,6 +7025,7 @@
"integrity": "sha512-YJDaCJZEnBmcbw13fvdAM9AwNOJwOzrE4pqMqBq5nFiEqXUqHwlK4B+3pUw6JNvfSPtX05xFHtYy/1ni01eGCw==", "integrity": "sha512-YJDaCJZEnBmcbw13fvdAM9AwNOJwOzrE4pqMqBq5nFiEqXUqHwlK4B+3pUw6JNvfSPtX05xFHtYy/1ni01eGCw==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"graceful-fs": "^4.1.2", "graceful-fs": "^4.1.2",
"jsonfile": "^4.0.0", "jsonfile": "^4.0.0",
@@ -7057,8 +7057,7 @@
"resolved": "https://registry.npmjs.org/embla-carousel/-/embla-carousel-8.6.0.tgz", "resolved": "https://registry.npmjs.org/embla-carousel/-/embla-carousel-8.6.0.tgz",
"integrity": "sha512-SjWyZBHJPbqxHOzckOfo8lHisEaJWmwd23XppYFYVh10bU66/Pn5tkVkbkCMZVdbUE5eTCI2nD8OyIP4Z+uwkA==", "integrity": "sha512-SjWyZBHJPbqxHOzckOfo8lHisEaJWmwd23XppYFYVh10bU66/Pn5tkVkbkCMZVdbUE5eTCI2nD8OyIP4Z+uwkA==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT"
"peer": true
}, },
"node_modules/embla-carousel-reactive-utils": { "node_modules/embla-carousel-reactive-utils": {
"version": "8.6.0", "version": "8.6.0",
@@ -7091,6 +7090,17 @@
"dev": true, "dev": true,
"license": "MIT" "license": "MIT"
}, },
"node_modules/encoding": {
"version": "0.1.13",
"resolved": "https://registry.npmjs.org/encoding/-/encoding-0.1.13.tgz",
"integrity": "sha512-ETBauow1T35Y/WZMkio9jiM0Z5xjHHmJ4XmjZOq1l/dXz3lr2sRn87nJy20RupqSh1F2m3HHPSp8ShIPQJrJ3A==",
"dev": true,
"license": "MIT",
"optional": true,
"dependencies": {
"iconv-lite": "^0.6.2"
}
},
"node_modules/end-of-stream": { "node_modules/end-of-stream": {
"version": "1.4.5", "version": "1.4.5",
"resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.5.tgz", "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.5.tgz",
@@ -7279,7 +7289,6 @@
"integrity": "sha512-VmQ+sifHUbI/IcSopBCF/HO3YiHQx/AVd3UVyYL6weuwW+HvON9VYn5l6Zl1WZzPWXPNZrSQpxwkkZ/VuvJZzg==", "integrity": "sha512-VmQ+sifHUbI/IcSopBCF/HO3YiHQx/AVd3UVyYL6weuwW+HvON9VYn5l6Zl1WZzPWXPNZrSQpxwkkZ/VuvJZzg==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/eslint-utils": "^4.8.0",
"@eslint-community/regexpp": "^4.12.1", "@eslint-community/regexpp": "^4.12.1",
@@ -7340,7 +7349,6 @@
"integrity": "sha512-82GZUjRS0p/jganf6q1rEO25VSoHH0hKPCTrgillPjdI/3bgBhAE1QzHrHTizjpRvy6pGAvKjDJtk2pF9NDq8w==", "integrity": "sha512-82GZUjRS0p/jganf6q1rEO25VSoHH0hKPCTrgillPjdI/3bgBhAE1QzHrHTizjpRvy6pGAvKjDJtk2pF9NDq8w==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"peer": true,
"bin": { "bin": {
"eslint-config-prettier": "bin/cli.js" "eslint-config-prettier": "bin/cli.js"
}, },
@@ -9804,6 +9812,7 @@
"integrity": "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==", "integrity": "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"minimist": "^1.2.6" "minimist": "^1.2.6"
}, },
@@ -10416,7 +10425,6 @@
"integrity": "sha512-l7pqLUFTI/+ESXn6k3nu30ZIzW5E2WZF/LaHJEpoq6ElcLD+wduZoB2kBN19du6K/4FDpPMazY2wJr+IndBtQw==", "integrity": "sha512-l7pqLUFTI/+ESXn6k3nu30ZIzW5E2WZF/LaHJEpoq6ElcLD+wduZoB2kBN19du6K/4FDpPMazY2wJr+IndBtQw==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"@vue/devtools-api": "^7.7.7" "@vue/devtools-api": "^7.7.7"
}, },
@@ -10497,6 +10505,7 @@
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"optional": true, "optional": true,
"peer": true,
"dependencies": { "dependencies": {
"commander": "^9.4.0" "commander": "^9.4.0"
}, },
@@ -10514,6 +10523,7 @@
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"optional": true, "optional": true,
"peer": true,
"engines": { "engines": {
"node": "^12.20.0 || >=14" "node": "^12.20.0 || >=14"
} }
@@ -10534,7 +10544,6 @@
"integrity": "sha512-UOnG6LftzbdaHZcKoPFtOcCKztrQ57WkHDeRD9t/PTQtmT0NHSeWWepj6pS0z/N7+08BHFDQVUrfmfMRcZwbMg==", "integrity": "sha512-UOnG6LftzbdaHZcKoPFtOcCKztrQ57WkHDeRD9t/PTQtmT0NHSeWWepj6pS0z/N7+08BHFDQVUrfmfMRcZwbMg==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"peer": true,
"bin": { "bin": {
"prettier": "bin/prettier.cjs" "prettier": "bin/prettier.cjs"
}, },
@@ -10875,6 +10884,7 @@
"deprecated": "Rimraf versions prior to v4 are no longer supported", "deprecated": "Rimraf versions prior to v4 are no longer supported",
"dev": true, "dev": true,
"license": "ISC", "license": "ISC",
"peer": true,
"dependencies": { "dependencies": {
"glob": "^7.1.3" "glob": "^7.1.3"
}, },
@@ -10889,6 +10899,7 @@
"deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me",
"dev": true, "dev": true,
"license": "ISC", "license": "ISC",
"peer": true,
"dependencies": { "dependencies": {
"fs.realpath": "^1.0.0", "fs.realpath": "^1.0.0",
"inflight": "^1.0.4", "inflight": "^1.0.4",
@@ -10910,6 +10921,7 @@
"integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==",
"dev": true, "dev": true,
"license": "ISC", "license": "ISC",
"peer": true,
"dependencies": { "dependencies": {
"brace-expansion": "^1.1.7" "brace-expansion": "^1.1.7"
}, },
@@ -11553,7 +11565,6 @@
"integrity": "sha512-/BUbeOwPGruiBOm0YQQ6ZMcLIZ6tf/W+Jcm7dxZyAX0tK3WP9/sq7/NAWBxPIxVahdGjCJoGwej0Gdrv0DxlQQ==", "integrity": "sha512-/BUbeOwPGruiBOm0YQQ6ZMcLIZ6tf/W+Jcm7dxZyAX0tK3WP9/sq7/NAWBxPIxVahdGjCJoGwej0Gdrv0DxlQQ==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"events": "^3.3.0", "events": "^3.3.0",
"graphology-utils": "^2.5.2" "graphology-utils": "^2.5.2"
@@ -12042,6 +12053,7 @@
"integrity": "sha512-yYrrsWnrXMcdsnu/7YMYAofM1ktpL5By7vZhf15CrXijWWrEYZks5AXBudalfSWJLlnen/QUJUB5aoB0kqZUGA==", "integrity": "sha512-yYrrsWnrXMcdsnu/7YMYAofM1ktpL5By7vZhf15CrXijWWrEYZks5AXBudalfSWJLlnen/QUJUB5aoB0kqZUGA==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"mkdirp": "^0.5.1", "mkdirp": "^0.5.1",
"rimraf": "~2.6.2" "rimraf": "~2.6.2"
@@ -12177,7 +12189,6 @@
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"peer": true,
"engines": { "engines": {
"node": ">=12" "node": ">=12"
}, },
@@ -12351,9 +12362,9 @@
} }
}, },
"node_modules/undici-types": { "node_modules/undici-types": {
"version": "7.18.2", "version": "7.16.0",
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.18.2.tgz", "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.16.0.tgz",
"integrity": "sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w==", "integrity": "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==",
"dev": true, "dev": true,
"license": "MIT" "license": "MIT"
}, },
@@ -12568,7 +12579,6 @@
"integrity": "sha512-w+N7Hifpc3gRjZ63vYBXA56dvvRlNWRczTdmCBBa+CotUzAPf5b7YMdMR/8CQoeYE5LX3W4wj6RYTgonm1b9DA==", "integrity": "sha512-w+N7Hifpc3gRjZ63vYBXA56dvvRlNWRczTdmCBBa+CotUzAPf5b7YMdMR/8CQoeYE5LX3W4wj6RYTgonm1b9DA==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"esbuild": "^0.27.0", "esbuild": "^0.27.0",
"fdir": "^6.5.0", "fdir": "^6.5.0",
@@ -12662,7 +12672,6 @@
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"peer": true,
"engines": { "engines": {
"node": ">=12" "node": ">=12"
}, },
@@ -12676,7 +12685,6 @@
"integrity": "sha512-hOQuK7h0FGKgBAas7v0mSAsnvrIgAvWmRFjmzpJ7SwFHH3g1k2u37JtYwOwmEKhK6ZO3v9ggDBBm0La1LCK4uQ==", "integrity": "sha512-hOQuK7h0FGKgBAas7v0mSAsnvrIgAvWmRFjmzpJ7SwFHH3g1k2u37JtYwOwmEKhK6ZO3v9ggDBBm0La1LCK4uQ==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"@vitest/expect": "4.0.18", "@vitest/expect": "4.0.18",
"@vitest/mocker": "4.0.18", "@vitest/mocker": "4.0.18",
@@ -12777,7 +12785,6 @@
"resolved": "https://registry.npmjs.org/vue/-/vue-3.5.29.tgz", "resolved": "https://registry.npmjs.org/vue/-/vue-3.5.29.tgz",
"integrity": "sha512-BZqN4Ze6mDQVNAni0IHeMJ5mwr8VAJ3MQC9FmprRhcBYENw+wOAAjRj8jfmN6FLl0j96OXbR+CjWhmAmM+QGnA==", "integrity": "sha512-BZqN4Ze6mDQVNAni0IHeMJ5mwr8VAJ3MQC9FmprRhcBYENw+wOAAjRj8jfmN6FLl0j96OXbR+CjWhmAmM+QGnA==",
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"@vue/compiler-dom": "3.5.29", "@vue/compiler-dom": "3.5.29",
"@vue/compiler-sfc": "3.5.29", "@vue/compiler-sfc": "3.5.29",
@@ -13504,7 +13511,6 @@
"integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==", "integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"peer": true,
"funding": { "funding": {
"url": "https://github.com/sponsors/colinhacks" "url": "https://github.com/sponsors/colinhacks"
} }
+2 -2
View File
@@ -49,8 +49,8 @@
"@tailwindcss/vite": "^4.2.1", "@tailwindcss/vite": "^4.2.1",
"@tanstack/react-query": "^5.90.21", "@tanstack/react-query": "^5.90.21",
"@tanstack/vue-table": "^8.21.3", "@tanstack/vue-table": "^8.21.3",
"@tanstack/vue-virtual": "^3.13.19", "@tanstack/vue-virtual": "^3.13.21",
"@types/node": "^25.3.3", "@types/node": "^24.12.0",
"@vee-validate/zod": "^4.15.1", "@vee-validate/zod": "^4.15.1",
"@vitejs/plugin-vue": "^6.0.4", "@vitejs/plugin-vue": "^6.0.4",
"@vitejs/plugin-vue-jsx": "^5.1.4", "@vitejs/plugin-vue-jsx": "^5.1.4",
+28 -35
View File
@@ -35,7 +35,14 @@
import { storeToRefs } from 'pinia'; import { storeToRefs } from 'pinia';
import { useI18n } from 'vue-i18n'; import { useI18n } from 'vue-i18n';
import { getGroupName, getWorldName, parseLocation, resolveRegion, translateAccessType } from '../shared/utils'; import {
getGroupName,
getLocationText,
getWorldName,
parseLocation,
resolveRegion,
translateAccessType
} from '../shared/utils';
import { import {
useAppearanceSettingsStore, useAppearanceSettingsStore,
useGroupStore, useGroupStore,
@@ -229,41 +236,27 @@
*/ */
function setText(L) { function setText(L) {
const accessTypeLabel = getAccessTypeLabel(L.accessTypeName); const accessTypeLabel = getAccessTypeLabel(L.accessTypeName);
const cachedRef = L.worldId ? cachedWorlds.get(L.worldId) : undefined;
const worldName = typeof cachedRef !== 'undefined' ? cachedRef.name : undefined;
if (L.isOffline) { text.value = getLocationText(L, {
text.value = t('location.offline'); hint: props.hint,
} else if (L.isPrivate) { worldName,
text.value = t('location.private'); accessTypeLabel,
} else if (L.isTraveling) { t
text.value = t('location.traveling'); });
} else if (typeof props.hint === 'string' && props.hint !== '') {
if (L.instanceId) { if (L.worldId && typeof cachedRef === 'undefined') {
text.value = `${props.hint} · ${accessTypeLabel}`; getWorldName(L.worldId).then((name) => {
} else { if (!isDisposed && name && currentInstanceId() === L.tag) {
text.value = props.hint; text.value = getLocationText(L, {
} hint: props.hint,
} else if (L.worldId) { worldName: name,
if (L.instanceId) { accessTypeLabel: getAccessTypeLabel(L.accessTypeName),
text.value = `${L.worldId} · ${accessTypeLabel}`; t
} else { });
text.value = L.worldId; }
} });
const ref = cachedWorlds.get(L.worldId);
if (typeof ref === 'undefined') {
getWorldName(L.worldId).then((name) => {
if (!isDisposed && name && currentInstanceId() === L.tag) {
if (L.instanceId) {
text.value = `${name} · ${getAccessTypeLabel(L.accessTypeName)}`;
} else {
text.value = name;
}
}
});
} else if (L.instanceId) {
text.value = `${ref.name} · ${accessTypeLabel}`;
} else {
text.value = ref.name;
}
} }
} }
+3
View File
@@ -179,5 +179,8 @@ export function isEntryNotified(entry, notifiedMenus) {
targets.push(lastSegment); targets.push(lastSegment);
} }
} }
if (!Array.isArray(notifiedMenus)) {
return false;
}
return targets.some((key) => notifiedMenus.includes(key)); return targets.some((key) => notifiedMenus.includes(key));
} }
@@ -0,0 +1,93 @@
import { describe, expect, it, vi } from 'vitest';
import { evictMapCache } from '../cacheUtils';
describe('evictMapCache', () => {
it('does nothing when cache is under maxSize', () => {
const cache = new Map([
['a', 1],
['b', 2]
]);
const result = evictMapCache(cache, 5, () => false);
expect(result.deletedCount).toBe(0);
expect(cache.size).toBe(2);
});
it('evicts entries when cache exceeds maxSize', () => {
const cache = new Map();
for (let i = 0; i < 10; i++) {
cache.set(`key_${i}`, i);
}
const result = evictMapCache(cache, 5, () => false);
expect(result.deletedCount).toBe(5);
expect(cache.size).toBe(5);
});
it('retains entries matching isRetainedFn', () => {
const cache = new Map([
['keep_1', 'retained'],
['keep_2', 'retained'],
['evict_1', 'evictable'],
['evict_2', 'evictable'],
['evict_3', 'evictable']
]);
const result = evictMapCache(cache, 2, (_value, key) =>
key.startsWith('keep_')
);
// Should have evicted evictable entries but retained keep entries
expect(cache.has('keep_1')).toBe(true);
expect(cache.has('keep_2')).toBe(true);
expect(result.deletedCount).toBe(3);
});
it('uses custom sortFn for eviction order', () => {
const cache = new Map([
['old', { age: 1 }],
['new', { age: 100 }],
['medium', { age: 50 }]
]);
const result = evictMapCache(cache, 1, () => false, {
sortFn: (a, b) => a.value.age - b.value.age
});
// Should evict oldest first
expect(result.deletedCount).toBe(2);
expect(cache.has('new')).toBe(true);
expect(cache.has('old')).toBe(false);
expect(cache.has('medium')).toBe(false);
});
it('logs when logLabel is provided', () => {
const spy = vi.spyOn(console, 'log').mockImplementation(() => {});
const cache = new Map([
['a', 1],
['b', 2],
['c', 3]
]);
evictMapCache(cache, 1, () => false, { logLabel: 'Test cleanup' });
expect(spy).toHaveBeenCalledWith(
expect.stringContaining('Test cleanup')
);
spy.mockRestore();
});
it('does not evict retained entries even when all need eviction', () => {
const cache = new Map([
['a', 1],
['b', 2],
['c', 3]
]);
const result = evictMapCache(cache, 1, () => true);
// All entries are retained
expect(result.deletedCount).toBe(0);
expect(cache.size).toBe(3);
});
it('handles exact maxSize (no eviction needed)', () => {
const cache = new Map([
['a', 1],
['b', 2]
]);
const result = evictMapCache(cache, 2, () => false);
expect(result.deletedCount).toBe(0);
expect(cache.size).toBe(2);
});
});
@@ -0,0 +1,241 @@
import { describe, expect, test, vi } from 'vitest';
import {
getPlatformLabel,
getStatusInfo,
getRpcWorldConfig,
isPopcornPalaceWorld
} from '../discordPresence';
import { ActivityType, StatusDisplayType } from '../../constants/discord';
const t = (key) => key;
describe('getPlatformLabel', () => {
test('returns VR label when game is running in VR', () => {
const result = getPlatformLabel('standalonewindows', true, false, t);
expect(result).toBe(' (view.settings.discord_presence.rpc.vr)');
});
test('returns desktop label when game is running in desktop mode', () => {
const result = getPlatformLabel('standalonewindows', true, true, t);
expect(result).toBe(' (view.settings.discord_presence.rpc.desktop)');
});
test('returns empty string for web platform', () => {
expect(getPlatformLabel('web', false, false, t)).toBe('');
});
test('returns (PC) for standalonewindows', () => {
expect(getPlatformLabel('standalonewindows', false, false, t)).toBe(
' (PC)'
);
});
test('returns (Android) for android', () => {
expect(getPlatformLabel('android', false, false, t)).toBe(' (Android)');
});
test('returns (iOS) for ios', () => {
expect(getPlatformLabel('ios', false, false, t)).toBe(' (iOS)');
});
test('returns platform name in parens for unknown platform', () => {
expect(getPlatformLabel('quest', false, false, t)).toBe(' (quest)');
});
test('returns empty string for empty/falsy platform when not game running', () => {
expect(getPlatformLabel('', false, false, t)).toBe('');
expect(getPlatformLabel(undefined, false, false, t)).toBe('');
});
});
describe('getStatusInfo', () => {
test('active status', () => {
const result = getStatusInfo('active', false, t);
expect(result).toEqual({
statusName: 'dialog.user.status.active',
statusImage: 'active',
hidePrivate: false
});
});
test('join me status', () => {
const result = getStatusInfo('join me', false, t);
expect(result).toEqual({
statusName: 'dialog.user.status.join_me',
statusImage: 'joinme',
hidePrivate: false
});
});
test('ask me status without hide invite', () => {
const result = getStatusInfo('ask me', false, t);
expect(result).toEqual({
statusName: 'dialog.user.status.ask_me',
statusImage: 'askme',
hidePrivate: false
});
});
test('ask me status with hide invite', () => {
const result = getStatusInfo('ask me', true, t);
expect(result).toEqual({
statusName: 'dialog.user.status.ask_me',
statusImage: 'askme',
hidePrivate: true
});
});
test('busy status always hides private', () => {
const result = getStatusInfo('busy', false, t);
expect(result).toEqual({
statusName: 'dialog.user.status.busy',
statusImage: 'busy',
hidePrivate: true
});
});
test('unknown status defaults to offline', () => {
const result = getStatusInfo('unknown', false, t);
expect(result).toEqual({
statusName: 'dialog.user.status.offline',
statusImage: 'offline',
hidePrivate: true
});
});
test('empty status defaults to offline', () => {
const result = getStatusInfo('', false, t);
expect(result).toEqual({
statusName: 'dialog.user.status.offline',
statusImage: 'offline',
hidePrivate: true
});
});
});
describe('getRpcWorldConfig', () => {
test('returns PyPyDance config for known PyPyDance world', () => {
const config = getRpcWorldConfig(
'wrld_f20326da-f1ac-45fc-a062-609723b097b1'
);
expect(config).toEqual({
activityType: ActivityType.Listening,
statusDisplayType: StatusDisplayType.Details,
appId: '784094509008551956',
bigIcon: 'pypy'
});
});
test('returns VR Dancing config', () => {
const config = getRpcWorldConfig(
'wrld_42377cf1-c54f-45ed-8996-5875b0573a83'
);
expect(config).toEqual({
activityType: ActivityType.Listening,
statusDisplayType: StatusDisplayType.Details,
appId: '846232616054030376',
bigIcon: 'vr_dancing'
});
});
test('returns ZuwaZuwa Dance config', () => {
const config = getRpcWorldConfig(
'wrld_52bdcdab-11cd-4325-9655-0fb120846945'
);
expect(config).toEqual({
activityType: ActivityType.Listening,
statusDisplayType: StatusDisplayType.Details,
appId: '939473404808007731',
bigIcon: 'zuwa_zuwa_dance'
});
});
test('returns LS Media config', () => {
const config = getRpcWorldConfig(
'wrld_74970324-58e8-4239-a17b-2c59dfdf00db'
);
expect(config).toEqual({
activityType: ActivityType.Watching,
statusDisplayType: StatusDisplayType.Details,
appId: '968292722391785512',
bigIcon: 'ls_media'
});
});
test('returns Popcorn Palace config', () => {
const config = getRpcWorldConfig(
'wrld_266523e8-9161-40da-acd0-6bd82e075833'
);
expect(config).toEqual({
activityType: ActivityType.Watching,
statusDisplayType: StatusDisplayType.Details,
appId: '1095440531821170820',
bigIcon: 'popcorn_palace'
});
});
test('returns null for unknown world', () => {
expect(getRpcWorldConfig('wrld_unknown')).toBeNull();
});
test('returns null for empty string', () => {
expect(getRpcWorldConfig('')).toBeNull();
});
test('returns a copy, not the original object', () => {
const a = getRpcWorldConfig(
'wrld_f20326da-f1ac-45fc-a062-609723b097b1'
);
const b = getRpcWorldConfig(
'wrld_f20326da-f1ac-45fc-a062-609723b097b1'
);
expect(a).toEqual(b);
expect(a).not.toBe(b);
});
test('covers all PyPyDance world IDs', () => {
const pypyIds = [
'wrld_f20326da-f1ac-45fc-a062-609723b097b1',
'wrld_10e5e467-fc65-42ed-8957-f02cace1398c',
'wrld_04899f23-e182-4a8d-b2c7-2c74c7c15534'
];
for (const id of pypyIds) {
const config = getRpcWorldConfig(id);
expect(config.appId).toBe('784094509008551956');
expect(config.bigIcon).toBe('pypy');
}
});
test('covers all LS Media world IDs', () => {
const lsIds = [
'wrld_74970324-58e8-4239-a17b-2c59dfdf00db',
'wrld_db9d878f-6e76-4776-8bf2-15bcdd7fc445',
'wrld_435bbf25-f34f-4b8b-82c6-cd809057eb8e',
'wrld_f767d1c8-b249-4ecc-a56f-614e433682c8'
];
for (const id of lsIds) {
const config = getRpcWorldConfig(id);
expect(config.appId).toBe('968292722391785512');
expect(config.bigIcon).toBe('ls_media');
}
});
});
describe('isPopcornPalaceWorld', () => {
test('returns true for Popcorn Palace worlds', () => {
expect(
isPopcornPalaceWorld('wrld_266523e8-9161-40da-acd0-6bd82e075833')
).toBe(true);
expect(
isPopcornPalaceWorld('wrld_27c7e6b2-d938-447e-a270-3d1a873e2cf3')
).toBe(true);
});
test('returns false for non-Popcorn Palace worlds', () => {
expect(
isPopcornPalaceWorld('wrld_f20326da-f1ac-45fc-a062-609723b097b1')
).toBe(false);
expect(isPopcornPalaceWorld('wrld_unknown')).toBe(false);
});
});
@@ -0,0 +1,418 @@
import {
sanitizeUserJson,
sanitizeEntityJson,
computeTrustLevel,
computeUserPlatform,
computeDisabledContentSettings,
diffObjectProps,
createDefaultUserRef,
createDefaultWorldRef,
createDefaultAvatarRef,
createDefaultGroupRef,
createDefaultInstanceRef,
createDefaultFavoriteGroupRef,
createDefaultFavoriteCachedRef
} from '../entityTransforms';
describe('sanitizeUserJson', () => {
it('applies replaceBioSymbols to statusDescription, bio, note', () => {
const json = {
statusDescription: 'hello world',
bio: 'test bio',
note: 'test note'
};
sanitizeUserJson(json, '');
// replaceBioSymbols replaces Unicode look-alikes with ASCII
expect(json.statusDescription).toContain('?');
expect(json.bio).toContain('#');
expect(json.note).toContain('@');
});
it('removes emojis from statusDescription', () => {
const json = { statusDescription: 'hello 🎉 world' };
sanitizeUserJson(json, '');
// removeEmojis removes emoji then collapses whitespace
expect(json.statusDescription).toBe('hello world');
});
it('strips robot avatar URL', () => {
const robotUrl = 'https://example.com/robot.png';
const json = {
currentAvatarImageUrl: robotUrl,
currentAvatarThumbnailImageUrl: 'thumb.png'
};
sanitizeUserJson(json, robotUrl);
expect(json.currentAvatarImageUrl).toBeUndefined();
expect(json.currentAvatarThumbnailImageUrl).toBeUndefined();
});
it('keeps avatar URL when it does not match robot', () => {
const json = {
currentAvatarImageUrl: 'https://example.com/user.png',
currentAvatarThumbnailImageUrl: 'thumb.png'
};
sanitizeUserJson(json, 'https://example.com/robot.png');
expect(json.currentAvatarImageUrl).toBe('https://example.com/user.png');
});
it('handles missing fields gracefully', () => {
const json = { id: 'usr_123' };
expect(() => sanitizeUserJson(json, '')).not.toThrow();
});
});
describe('sanitizeEntityJson', () => {
it('applies replaceBioSymbols to specified fields', () => {
const json = {
name: 'hello',
description: 'test',
other: 'unchanged'
};
sanitizeEntityJson(json, ['name', 'description']);
expect(json.name).toContain('?');
expect(json.description).toContain('#');
expect(json.other).toContain(''); // not sanitized, still has Unicode
});
it('skips falsy fields', () => {
const json = { name: '', description: null };
expect(() =>
sanitizeEntityJson(json, ['name', 'description'])
).not.toThrow();
});
});
describe('computeTrustLevel', () => {
it('returns Visitor for empty tags', () => {
const result = computeTrustLevel([], '');
expect(result.trustLevel).toBe('Visitor');
expect(result.trustClass).toBe('x-tag-untrusted');
expect(result.trustColorKey).toBe('untrusted');
expect(result.trustSortNum).toBe(1);
});
it('returns Trusted User for veteran tags', () => {
const result = computeTrustLevel(['system_trust_veteran'], '');
expect(result.trustLevel).toBe('Trusted User');
expect(result.trustClass).toBe('x-tag-veteran');
expect(result.trustColorKey).toBe('veteran');
expect(result.trustSortNum).toBe(5);
});
it('returns Known User for trusted tags', () => {
const result = computeTrustLevel(['system_trust_trusted'], '');
expect(result.trustLevel).toBe('Known User');
expect(result.trustSortNum).toBe(4);
});
it('returns User for known tags', () => {
const result = computeTrustLevel(['system_trust_known'], '');
expect(result.trustLevel).toBe('User');
expect(result.trustSortNum).toBe(3);
});
it('returns New User for basic tags', () => {
const result = computeTrustLevel(['system_trust_basic'], '');
expect(result.trustLevel).toBe('New User');
expect(result.trustSortNum).toBe(2);
});
it('detects troll status', () => {
const result = computeTrustLevel(
['system_troll', 'system_trust_known'],
''
);
expect(result.isTroll).toBe(true);
expect(result.trustColorKey).toBe('troll');
expect(result.trustSortNum).toBeCloseTo(3.1); // 3 + 0.1
});
it('detects probable troll when not already troll', () => {
const result = computeTrustLevel(
['system_probable_troll', 'system_trust_basic'],
''
);
expect(result.isProbableTroll).toBe(true);
expect(result.isTroll).toBe(false);
expect(result.trustColorKey).toBe('troll');
});
it('probable troll is not set when already troll', () => {
const result = computeTrustLevel(
['system_troll', 'system_probable_troll'],
''
);
expect(result.isTroll).toBe(true);
expect(result.isProbableTroll).toBe(false);
});
it('detects moderator from developerType', () => {
const result = computeTrustLevel([], 'internal');
expect(result.isModerator).toBe(true);
expect(result.trustColorKey).toBe('vip');
expect(result.trustSortNum).toBeCloseTo(1.3); // 1 + 0.3
});
it('detects moderator from admin_moderator tag', () => {
const result = computeTrustLevel(
['admin_moderator', 'system_trust_veteran'],
''
);
expect(result.isModerator).toBe(true);
expect(result.trustColorKey).toBe('vip');
});
it('does not treat "none" developerType as moderator', () => {
const result = computeTrustLevel([], 'none');
expect(result.isModerator).toBe(false);
});
});
describe('computeUserPlatform', () => {
it('returns platform when valid', () => {
expect(computeUserPlatform('standalonewindows', 'android')).toBe(
'standalonewindows'
);
});
it('falls back to last_platform when platform is "offline"', () => {
expect(computeUserPlatform('offline', 'android')).toBe('android');
});
it('falls back to last_platform when platform is "web"', () => {
expect(computeUserPlatform('web', 'ios')).toBe('ios');
});
it('falls back to last_platform when platform is empty', () => {
expect(computeUserPlatform('', 'standalonewindows')).toBe(
'standalonewindows'
);
});
it('returns empty string when both are empty', () => {
expect(computeUserPlatform('', '')).toBe('');
});
});
describe('computeDisabledContentSettings', () => {
const settingsList = ['gore', 'nudity', 'violence'];
it('returns empty for null contentSettings', () => {
expect(computeDisabledContentSettings(null, settingsList)).toEqual([]);
});
it('returns empty for empty object', () => {
expect(computeDisabledContentSettings({}, settingsList)).toEqual([]);
});
it('returns disabled settings (false values)', () => {
const result = computeDisabledContentSettings(
{ gore: false, nudity: true, violence: false },
settingsList
);
expect(result).toEqual(['gore', 'violence']);
});
it('skips undefined settings', () => {
const result = computeDisabledContentSettings(
{ gore: true },
settingsList
);
expect(result).toEqual([]);
});
});
describe('diffObjectProps', () => {
const arraysMatch = (a, b) =>
a.length === b.length && a.every((v, i) => v === b[i]);
it('detects changed primitive props', () => {
const ref = { name: 'old', id: '1' };
const json = { name: 'new', id: '1' };
const result = diffObjectProps(ref, json, arraysMatch);
expect(result.hasPropChanged).toBe(true);
expect(result.changedProps.name).toEqual(['new', 'old']);
});
it('detects unchanged props', () => {
const ref = { name: 'same', id: '1' };
const json = { name: 'same', id: '1' };
const result = diffObjectProps(ref, json, arraysMatch);
expect(result.hasPropChanged).toBe(false);
});
it('detects changed arrays', () => {
const ref = { tags: ['a', 'b'] };
const json = { tags: ['a', 'c'] };
const result = diffObjectProps(ref, json, arraysMatch);
expect(result.hasPropChanged).toBe(true);
expect(result.changedProps.tags).toBeDefined();
});
it('ignores props only in json (not in ref)', () => {
const ref = { id: '1' };
const json = { id: '1', newProp: 'value' };
const result = diffObjectProps(ref, json, arraysMatch);
expect(result.hasPropChanged).toBe(false);
});
it('ignores props only in ref (not in json)', () => {
const ref = { id: '1', extra: 'value' };
const json = { id: '1' };
const result = diffObjectProps(ref, json, arraysMatch);
expect(result.hasPropChanged).toBe(false);
});
});
describe('createDefaultUserRef', () => {
it('creates object with defaults', () => {
const ref = createDefaultUserRef({});
expect(ref.id).toBe('');
expect(ref.displayName).toBe('');
expect(ref.tags).toEqual([]);
expect(ref.$trustLevel).toBe('Visitor');
expect(ref.$platform).toBe('');
});
it('spreads json over defaults', () => {
const ref = createDefaultUserRef({
id: 'usr_123',
displayName: 'Test'
});
expect(ref.id).toBe('usr_123');
expect(ref.displayName).toBe('Test');
expect(ref.bio).toBe('');
});
});
describe('createDefaultWorldRef', () => {
it('creates object with defaults', () => {
const ref = createDefaultWorldRef({});
expect(ref.id).toBe('');
expect(ref.name).toBe('');
expect(ref.capacity).toBe(0);
expect(ref.$isLabs).toBe(false);
});
it('spreads json over defaults', () => {
const ref = createDefaultWorldRef({
id: 'wrld_123',
name: 'Test World'
});
expect(ref.id).toBe('wrld_123');
expect(ref.name).toBe('Test World');
});
});
describe('createDefaultAvatarRef', () => {
it('creates object with defaults', () => {
const ref = createDefaultAvatarRef({});
expect(ref.id).toBe('');
expect(ref.name).toBe('');
expect(ref.version).toBe(0);
expect(ref.tags).toEqual([]);
});
it('spreads json over defaults', () => {
const ref = createDefaultAvatarRef({
id: 'avtr_123',
name: 'My Avatar'
});
expect(ref.id).toBe('avtr_123');
expect(ref.name).toBe('My Avatar');
});
});
describe('createDefaultGroupRef', () => {
it('creates object with defaults including myMember', () => {
const ref = createDefaultGroupRef({});
expect(ref.id).toBe('');
expect(ref.name).toBe('');
expect(ref.myMember).toBeDefined();
expect(ref.myMember.roleIds).toEqual([]);
expect(ref.roles).toEqual([]);
});
it('spreads json over defaults', () => {
const ref = createDefaultGroupRef({
id: 'grp_123',
name: 'Test Group'
});
expect(ref.id).toBe('grp_123');
});
});
describe('createDefaultInstanceRef', () => {
it('creates object with defaults', () => {
const ref = createDefaultInstanceRef({});
expect(ref.id).toBe('');
expect(ref.capacity).toBe(0);
expect(ref.hasCapacityForYou).toBe(true);
expect(ref.$fetchedAt).toBe('');
expect(ref.$disabledContentSettings).toEqual([]);
});
it('spreads json over defaults', () => {
const ref = createDefaultInstanceRef({
id: 'wrld_123:12345',
capacity: 40
});
expect(ref.id).toBe('wrld_123:12345');
expect(ref.capacity).toBe(40);
});
});
describe('createDefaultFavoriteGroupRef', () => {
it('creates object with defaults', () => {
const ref = createDefaultFavoriteGroupRef({});
expect(ref.id).toBe('');
expect(ref.name).toBe('');
expect(ref.displayName).toBe('');
expect(ref.type).toBe('');
expect(ref.visibility).toBe('');
expect(ref.tags).toEqual([]);
});
it('spreads json over defaults', () => {
const ref = createDefaultFavoriteGroupRef({
id: 'fvgrp_1',
name: 'group_0',
displayName: 'Group 1',
type: 'friend'
});
expect(ref.id).toBe('fvgrp_1');
expect(ref.name).toBe('group_0');
expect(ref.displayName).toBe('Group 1');
expect(ref.type).toBe('friend');
});
});
describe('createDefaultFavoriteCachedRef', () => {
it('creates object with defaults and computes $groupKey', () => {
const ref = createDefaultFavoriteCachedRef({});
expect(ref.id).toBe('');
expect(ref.type).toBe('');
expect(ref.favoriteId).toBe('');
expect(ref.tags).toEqual([]);
expect(ref.$groupKey).toBe(':undefined');
});
it('computes $groupKey from type and first tag', () => {
const ref = createDefaultFavoriteCachedRef({
id: 'fav_1',
type: 'friend',
favoriteId: 'usr_123',
tags: ['group_0']
});
expect(ref.$groupKey).toBe('friend:group_0');
expect(ref.favoriteId).toBe('usr_123');
});
it('handles multiple tags (uses first)', () => {
const ref = createDefaultFavoriteCachedRef({
type: 'world',
tags: ['worlds1', 'worlds2']
});
expect(ref.$groupKey).toBe('world:worlds1');
});
});
+163 -1
View File
@@ -1,7 +1,13 @@
import { import {
compareGameLogRows, compareGameLogRows,
createJoinLeaveEntry,
createLocationEntry,
createPortalSpawnEntry,
createResourceLoadEntry,
gameLogSearchFilter, gameLogSearchFilter,
getGameLogCreatedAtTs getGameLogCreatedAtTs,
parseInventoryFromUrl,
parsePrintFromUrl
} from '../gameLog'; } from '../gameLog';
describe('gameLogSearchFilter', () => { describe('gameLogSearchFilter', () => {
@@ -184,3 +190,159 @@ describe('compareGameLogRows', () => {
expect(compareGameLogRows(a, b)).toBe(0); expect(compareGameLogRows(a, b)).toBe(0);
}); });
}); });
describe('createLocationEntry', () => {
test('creates entry with correct shape', () => {
const entry = createLocationEntry(
'2024-01-15T12:00:00Z',
'wrld_abc123~12345',
'wrld_abc123',
'Test World'
);
expect(entry).toEqual({
created_at: '2024-01-15T12:00:00Z',
type: 'Location',
location: 'wrld_abc123~12345',
worldId: 'wrld_abc123',
worldName: 'Test World',
groupName: '',
time: 0
});
});
});
describe('createJoinLeaveEntry', () => {
test('creates OnPlayerJoined entry with default time', () => {
const entry = createJoinLeaveEntry(
'OnPlayerJoined',
'2024-01-15T12:00:00Z',
'Alice',
'wrld_abc~123',
'usr_abc'
);
expect(entry).toEqual({
created_at: '2024-01-15T12:00:00Z',
type: 'OnPlayerJoined',
displayName: 'Alice',
location: 'wrld_abc~123',
userId: 'usr_abc',
time: 0
});
});
test('creates OnPlayerLeft entry with custom time', () => {
const entry = createJoinLeaveEntry(
'OnPlayerLeft',
'2024-01-15T12:30:00Z',
'Bob',
'wrld_xyz~456',
'usr_xyz',
1800000
);
expect(entry.type).toBe('OnPlayerLeft');
expect(entry.time).toBe(1800000);
});
});
describe('createPortalSpawnEntry', () => {
test('creates portal spawn entry with empty defaults', () => {
const entry = createPortalSpawnEntry(
'2024-01-15T12:00:00Z',
'wrld_abc~123'
);
expect(entry).toEqual({
created_at: '2024-01-15T12:00:00Z',
type: 'PortalSpawn',
location: 'wrld_abc~123',
displayName: '',
userId: '',
instanceId: '',
worldName: ''
});
});
});
describe('createResourceLoadEntry', () => {
test('maps resource-load-string to StringLoad', () => {
const entry = createResourceLoadEntry(
'resource-load-string',
'2024-01-15T12:00:00Z',
'https://cdn.example.com/res.json',
'wrld_abc~123'
);
expect(entry.type).toBe('StringLoad');
expect(entry.resourceUrl).toBe('https://cdn.example.com/res.json');
});
test('maps resource-load-image to ImageLoad', () => {
const entry = createResourceLoadEntry(
'resource-load-image',
'2024-01-15T12:00:00Z',
'https://cdn.example.com/img.png',
'wrld_abc~123'
);
expect(entry.type).toBe('ImageLoad');
});
});
describe('parseInventoryFromUrl', () => {
test('parses valid inventory URL', () => {
const url =
'https://api.vrchat.cloud/api/1/user/usr_032383a7-748c-4fb2-94e4-bcb928e5de6b/inventory/inv_75781d65-92fe-4a80-a1ff-27ee6e843b08';
const result = parseInventoryFromUrl(url);
expect(result).toEqual({
userId: 'usr_032383a7-748c-4fb2-94e4-bcb928e5de6b',
inventoryId: 'inv_75781d65-92fe-4a80-a1ff-27ee6e843b08'
});
});
test('returns null for non-inventory URL', () => {
expect(
parseInventoryFromUrl(
'https://api.vrchat.cloud/api/1/user/usr_abc/avatar'
)
).toBeNull();
});
test('returns null for invalid URL', () => {
expect(parseInventoryFromUrl('not a url')).toBeNull();
});
test('returns null for empty string', () => {
expect(parseInventoryFromUrl('')).toBeNull();
});
test('returns null if inventoryId length is wrong', () => {
expect(
parseInventoryFromUrl(
'https://api.vrchat.cloud/api/1/user/usr_abc/inventory/inv_short'
)
).toBeNull();
});
});
describe('parsePrintFromUrl', () => {
test('parses valid print URL', () => {
// printId is 41 chars: prnt_ (5) + UUID (36)
const printId = 'prnt_aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee';
const url = `https://api.vrchat.cloud/api/1/prints/${printId}`;
const result = parsePrintFromUrl(url);
expect(result).toBe(printId);
});
test('returns null for non-print URL', () => {
expect(
parsePrintFromUrl('https://api.vrchat.cloud/api/1/user/usr_abc')
).toBeNull();
});
test('returns null for invalid URL', () => {
expect(parsePrintFromUrl('not a url')).toBeNull();
});
test('returns null if printId has wrong length', () => {
expect(
parsePrintFromUrl('https://api.vrchat.cloud/api/1/prints/short')
).toBeNull();
});
});
@@ -1,9 +1,21 @@
import { vi } from 'vitest';
// Mock transitive deps from location.js → stores → columns.jsx → i18n
vi.mock('../../../views/Feed/Feed.vue', () => ({
default: { template: '<div />' }
}));
vi.mock('../../../views/Feed/columns.jsx', () => ({ columns: [] }));
vi.mock('../../../plugin/router', () => ({
default: { push: vi.fn(), currentRoute: { value: {} } }
}));
import { import {
displayLocation, displayLocation,
parseLocation, parseLocation,
resolveRegion, resolveRegion,
translateAccessType translateAccessType
} from '../locationParser'; } from '../locationParser';
import { getLocationText } from '../location';
import { accessTypeLocaleKeyMap } from '../../constants'; import { accessTypeLocaleKeyMap } from '../../constants';
describe('Location Utils', () => { describe('Location Utils', () => {
@@ -508,4 +520,78 @@ describe('Location Utils', () => {
); );
}); });
}); });
describe('getLocationText', () => {
const t = (key) => key;
const opts = (overrides = {}) => ({
hint: '',
worldName: undefined,
accessTypeLabel: 'Public',
t,
...overrides
});
test('returns offline label', () => {
const L = parseLocation('offline');
expect(getLocationText(L, opts())).toBe('location.offline');
});
test('returns private label', () => {
const L = parseLocation('private');
expect(getLocationText(L, opts())).toBe('location.private');
});
test('returns traveling label', () => {
const L = parseLocation('traveling');
expect(getLocationText(L, opts())).toBe('location.traveling');
});
test('returns hint with access type when instance exists', () => {
const L = parseLocation('wrld_12345:67890');
expect(getLocationText(L, opts({ hint: 'My World' }))).toBe(
'My World · Public'
);
});
test('returns hint alone when no instance', () => {
const L = parseLocation('wrld_12345');
expect(getLocationText(L, opts({ hint: 'My World' }))).toBe(
'My World'
);
});
test('returns world name with access type when cached', () => {
const L = parseLocation('wrld_12345:67890');
expect(getLocationText(L, opts({ worldName: 'Cool World' }))).toBe(
'Cool World · Public'
);
});
test('returns world name alone when no instance', () => {
const L = parseLocation('wrld_12345');
expect(getLocationText(L, opts({ worldName: 'Cool World' }))).toBe(
'Cool World'
);
});
test('falls back to worldId when no cached name', () => {
const L = parseLocation('wrld_12345:67890');
expect(getLocationText(L, opts())).toBe('wrld_12345 · Public');
});
test('returns empty string for empty location', () => {
const L = parseLocation('');
expect(getLocationText(L, opts())).toBe('');
});
test('hint takes priority over worldName', () => {
const L = parseLocation('wrld_12345:67890');
expect(
getLocationText(
L,
opts({ hint: 'Hint Text', worldName: 'World Name' })
)
).toBe('Hint Text · Public');
});
});
}); });
@@ -0,0 +1,211 @@
import { describe, it, expect, vi } from 'vitest';
import {
sanitizeNotificationJson,
parseNotificationDetails,
createDefaultNotificationRef,
createDefaultNotificationV2Ref,
applyBoopLegacyHandling
} from '../notificationTransforms';
describe('sanitizeNotificationJson', () => {
it('should remove null and undefined values', () => {
const json = { id: '1', message: null, type: undefined, seen: false };
const result = sanitizeNotificationJson(json);
expect(result).not.toHaveProperty('message');
expect(result).not.toHaveProperty('type');
expect(result).toHaveProperty('id', '1');
expect(result).toHaveProperty('seen', false);
});
it('should apply replaceBioSymbols to message', () => {
// replaceBioSymbols replaces Unicode look-alikes with ASCII, not zero-width spaces
const json = { message: 'hello world' };
const result = sanitizeNotificationJson(json);
expect(result.message).toContain('?');
});
it('should apply replaceBioSymbols to title', () => {
const json = { title: 'hello world' };
const result = sanitizeNotificationJson(json);
expect(result.title).toContain('?');
});
it('should not touch other fields', () => {
const json = { id: 'abc', seen: true, details: { x: 1 } };
const result = sanitizeNotificationJson(json);
expect(result).toEqual({ id: 'abc', seen: true, details: { x: 1 } });
});
it('should mutate and return the same object', () => {
const json = { id: '1', bad: null };
const result = sanitizeNotificationJson(json);
expect(result).toBe(json);
});
});
describe('parseNotificationDetails', () => {
it('should return object details as-is', () => {
const details = { worldId: 'wrld_123' };
expect(parseNotificationDetails(details)).toBe(details);
});
it('should parse JSON string details', () => {
const details = '{"worldId":"wrld_123"}';
expect(parseNotificationDetails(details)).toEqual({
worldId: 'wrld_123'
});
});
it('should return empty object for "{}"', () => {
expect(parseNotificationDetails('{}')).toEqual({});
});
it('should return empty object for invalid JSON', () => {
const spy = vi.spyOn(console, 'log').mockImplementation(() => {});
expect(parseNotificationDetails('not json')).toEqual({});
spy.mockRestore();
});
it('should return parsed array for JSON array string (arrays are objects)', () => {
expect(parseNotificationDetails('[1,2]')).toEqual([1, 2]);
});
it('should return empty object for null', () => {
expect(parseNotificationDetails(null)).toEqual({});
});
it('should return empty object for undefined', () => {
expect(parseNotificationDetails(undefined)).toEqual({});
});
});
describe('createDefaultNotificationRef', () => {
it('should create a ref with all default fields', () => {
const ref = createDefaultNotificationRef({});
expect(ref).toEqual({
id: '',
senderUserId: '',
senderUsername: '',
type: '',
message: '',
details: {},
seen: false,
created_at: '',
$isExpired: false
});
});
it('should merge json over defaults', () => {
const ref = createDefaultNotificationRef({
id: 'noti_1',
type: 'friendRequest',
senderUserId: 'usr_abc'
});
expect(ref.id).toBe('noti_1');
expect(ref.type).toBe('friendRequest');
expect(ref.senderUserId).toBe('usr_abc');
expect(ref.message).toBe('');
});
it('should parse string details', () => {
const ref = createDefaultNotificationRef({
details: '{"worldId":"wrld_1"}'
});
expect(ref.details).toEqual({ worldId: 'wrld_1' });
});
it('should keep object details', () => {
const details = { worldId: 'wrld_1' };
const ref = createDefaultNotificationRef({ details });
expect(ref.details).toBe(details);
});
});
describe('createDefaultNotificationV2Ref', () => {
it('should create a ref with all default V2 fields', () => {
const ref = createDefaultNotificationV2Ref({});
expect(ref).toMatchObject({
id: '',
createdAt: '',
updatedAt: '',
expiresAt: '',
type: '',
link: '',
linkText: '',
message: '',
title: '',
imageUrl: '',
seen: false,
senderUserId: '',
senderUsername: '',
version: 2
});
expect(ref.data).toEqual({});
expect(ref.responses).toEqual([]);
expect(ref.details).toEqual({});
});
it('should merge json over defaults', () => {
const ref = createDefaultNotificationV2Ref({
id: 'noti_v2',
type: 'boop',
seen: true
});
expect(ref.id).toBe('noti_v2');
expect(ref.type).toBe('boop');
expect(ref.seen).toBe(true);
});
});
describe('applyBoopLegacyHandling', () => {
it('should not modify non-boop notifications', () => {
const ref = {
type: 'friendRequest',
title: 'Hello',
message: '',
imageUrl: ''
};
applyBoopLegacyHandling(ref, 'https://api.example.com');
expect(ref.title).toBe('Hello');
expect(ref.message).toBe('');
});
it('should not modify boop without title', () => {
const ref = {
type: 'boop',
title: '',
message: 'existing',
imageUrl: ''
};
applyBoopLegacyHandling(ref, 'https://api.example.com');
expect(ref.message).toBe('existing');
});
it('should handle default emoji boops', () => {
const ref = {
type: 'boop',
title: 'Boop!',
message: '',
imageUrl: '',
details: { emojiId: 'default_wave', emojiVersion: '1' }
};
applyBoopLegacyHandling(ref, 'https://api.example.com');
expect(ref.title).toBe('');
expect(ref.message).toBe('Boop! wave');
expect(ref.imageUrl).toBe('default_wave');
});
it('should handle custom emoji boops', () => {
const ref = {
type: 'boop',
title: 'Boop!',
message: '',
imageUrl: '',
details: { emojiId: 'emj_123', emojiVersion: '5' }
};
applyBoopLegacyHandling(ref, 'https://api.example.com');
expect(ref.title).toBe('');
expect(ref.message).toBe('Boop!');
expect(ref.imageUrl).toBe('https://api.example.com/file/emj_123/5');
});
});
+19 -1
View File
@@ -145,6 +145,23 @@ function replaceBioSymbols(text) {
return newText.replace(/ {1,}/g, ' ').trimRight(); return newText.replace(/ {1,}/g, ' ').trimRight();
} }
/**
* @param {string} text
* @returns {string}
*/
function removeEmojis(text) {
if (!text) {
return '';
}
return text
.replace(
/([\u2700-\u27BF]|[\uE000-\uF8FF]|\uD83C[\uDC00-\uDFFF]|\uD83D[\uDC00-\uDFFF]|[\u2011-\u26FF]|\uD83E[\uDD10-\uDDFF])/g,
''
)
.replace(/\s+/g, ' ')
.trim();
}
export { export {
escapeTag, escapeTag,
escapeTagRecursive, escapeTagRecursive,
@@ -152,5 +169,6 @@ export {
commaNumber, commaNumber,
localeIncludes, localeIncludes,
changeLogRemoveLinks, changeLogRemoveLinks,
replaceBioSymbols replaceBioSymbols,
removeEmojis
}; };
+67
View File
@@ -0,0 +1,67 @@
/**
* Evict entries from a Map cache when it exceeds maxSize.
* Entries matching isRetainedFn are kept; the rest are evicted oldest-first
* (or by the provided sortFn).
* @param {Map} cache - The cache Map to evict from
* @param {number} maxSize - Maximum allowed size
* @param {(value: any, key: string) => boolean} isRetainedFn - Return true to keep the entry
* @param {object} [opts] - Options
* @param {(a: {key: string, value: any}, b: {key: string, value: any}) => number} [opts.sortFn] -
* Custom sort for eviction order (entries sorted ascending; first entries evicted first).
* If not provided, entries are evicted in insertion order.
* @param {string} [opts.logLabel] - Label for console.log output
* @returns {{ deletedCount: number }}
*/
export function evictMapCache(cache, maxSize, isRetainedFn, opts = {}) {
if (cache.size <= maxSize) {
return { deletedCount: 0 };
}
const { sortFn, logLabel } = opts;
const overBy = cache.size - maxSize;
if (sortFn) {
// Collect removable entries, sort, then evict
const removable = [];
for (const [key, value] of cache) {
if (isRetainedFn(value, key)) {
continue;
}
removable.push({ key, value });
}
removable.sort(sortFn);
const toDelete = Math.min(overBy, removable.length);
for (let i = 0; i < toDelete; i++) {
cache.delete(removable[i].key);
}
if (logLabel) {
console.log(
`${logLabel}: Deleted ${toDelete}. Current cache size: ${cache.size}`
);
}
return { deletedCount: toDelete };
}
// Default: evict in insertion order (skip retained entries)
let deletedCount = 0;
const keysToDelete = [];
for (const [key, value] of cache) {
if (isRetainedFn(value, key)) {
continue;
}
if (deletedCount >= overBy) {
break;
}
keysToDelete.push(key);
deletedCount++;
}
for (const key of keysToDelete) {
cache.delete(key);
}
if (logLabel) {
console.log(
`${logLabel}: Deleted ${deletedCount}. Current cache size: ${cache.size}`
);
}
return { deletedCount };
}
+228
View File
@@ -0,0 +1,228 @@
import { ActivityType, StatusDisplayType } from '../constants/discord';
/**
* RPC world configuration table.
* Maps worldId → { activityType, statusDisplayType, appId, bigIcon }.
*/
const RPC_WORLD_CONFIGS = new Map([
// PyPyDance
[
'wrld_f20326da-f1ac-45fc-a062-609723b097b1',
{
activityType: ActivityType.Listening,
statusDisplayType: StatusDisplayType.Details,
appId: '784094509008551956',
bigIcon: 'pypy'
}
],
[
'wrld_10e5e467-fc65-42ed-8957-f02cace1398c',
{
activityType: ActivityType.Listening,
statusDisplayType: StatusDisplayType.Details,
appId: '784094509008551956',
bigIcon: 'pypy'
}
],
[
'wrld_04899f23-e182-4a8d-b2c7-2c74c7c15534',
{
activityType: ActivityType.Listening,
statusDisplayType: StatusDisplayType.Details,
appId: '784094509008551956',
bigIcon: 'pypy'
}
],
// VR Dancing
[
'wrld_42377cf1-c54f-45ed-8996-5875b0573a83',
{
activityType: ActivityType.Listening,
statusDisplayType: StatusDisplayType.Details,
appId: '846232616054030376',
bigIcon: 'vr_dancing'
}
],
[
'wrld_dd6d2888-dbdc-47c2-bc98-3d631b2acd7c',
{
activityType: ActivityType.Listening,
statusDisplayType: StatusDisplayType.Details,
appId: '846232616054030376',
bigIcon: 'vr_dancing'
}
],
// ZuwaZuwa Dance
[
'wrld_52bdcdab-11cd-4325-9655-0fb120846945',
{
activityType: ActivityType.Listening,
statusDisplayType: StatusDisplayType.Details,
appId: '939473404808007731',
bigIcon: 'zuwa_zuwa_dance'
}
],
[
'wrld_2d40da63-8f1f-4011-8a9e-414eb8530acd',
{
activityType: ActivityType.Listening,
statusDisplayType: StatusDisplayType.Details,
appId: '939473404808007731',
bigIcon: 'zuwa_zuwa_dance'
}
],
// LS Media
[
'wrld_74970324-58e8-4239-a17b-2c59dfdf00db',
{
activityType: ActivityType.Watching,
statusDisplayType: StatusDisplayType.Details,
appId: '968292722391785512',
bigIcon: 'ls_media'
}
],
[
'wrld_db9d878f-6e76-4776-8bf2-15bcdd7fc445',
{
activityType: ActivityType.Watching,
statusDisplayType: StatusDisplayType.Details,
appId: '968292722391785512',
bigIcon: 'ls_media'
}
],
[
'wrld_435bbf25-f34f-4b8b-82c6-cd809057eb8e',
{
activityType: ActivityType.Watching,
statusDisplayType: StatusDisplayType.Details,
appId: '968292722391785512',
bigIcon: 'ls_media'
}
],
[
'wrld_f767d1c8-b249-4ecc-a56f-614e433682c8',
{
activityType: ActivityType.Watching,
statusDisplayType: StatusDisplayType.Details,
appId: '968292722391785512',
bigIcon: 'ls_media'
}
],
// Popcorn Palace
[
'wrld_266523e8-9161-40da-acd0-6bd82e075833',
{
activityType: ActivityType.Watching,
statusDisplayType: StatusDisplayType.Details,
appId: '1095440531821170820',
bigIcon: 'popcorn_palace'
}
],
[
'wrld_27c7e6b2-d938-447e-a270-3d1a873e2cf3',
{
activityType: ActivityType.Watching,
statusDisplayType: StatusDisplayType.Details,
appId: '1095440531821170820',
bigIcon: 'popcorn_palace'
}
]
]);
/** Set of Popcorn Palace world IDs (big icon can be overridden by thumbnail) */
const POPCORN_PALACE_WORLD_IDS = new Set([
'wrld_266523e8-9161-40da-acd0-6bd82e075833',
'wrld_27c7e6b2-d938-447e-a270-3d1a873e2cf3'
]);
/**
* Get custom world rpc configuration for a specific world ID.
* @param {string} worldId
* @returns {{ activityType: number, statusDisplayType: number, appId: string, bigIcon: string } | null}
*/
export function getRpcWorldConfig(worldId) {
const config = RPC_WORLD_CONFIGS.get(worldId);
if (!config) {
return null;
}
return { ...config };
}
/**
* Check if a world ID is a Popcorn Palace world.
* @param {string} worldId
* @returns {boolean}
*/
export function isPopcornPalaceWorld(worldId) {
return POPCORN_PALACE_WORLD_IDS.has(worldId);
}
/**
* Get the platform display label for Discord RPC.
* @param {string} platform - VRC platform string (e.g. 'standalonewindows', 'android')
* @param {boolean} isGameRunning
* @param {boolean} isGameNoVR
* @param {Function} t - i18n translate function
* @returns {string} Platform label string (e.g. ' (VR)', ' (PC)'), or empty string
*/
export function getPlatformLabel(platform, isGameRunning, isGameNoVR, t) {
if (isGameRunning) {
return isGameNoVR
? ` (${t('view.settings.discord_presence.rpc.desktop')})`
: ` (${t('view.settings.discord_presence.rpc.vr')})`;
}
switch (platform) {
case 'web':
return '';
case 'standalonewindows':
return ` (PC)`;
case 'android':
return ` (Android)`;
case 'ios':
return ` (iOS)`;
default:
return platform ? ` (${platform})` : '';
}
}
/**
* Get Discord status info from VRC user status.
* @param {string} status - VRC user status ('active', 'join me', 'ask me', 'busy')
* @param {boolean} discordHideInvite - Whether invite-hiding is enabled
* @param {Function} t - i18n translate function
* @returns {{ statusName: string, statusImage: string, hidePrivate: boolean }}
*/
export function getStatusInfo(status, discordHideInvite, t) {
switch (status) {
case 'active':
return {
statusName: t('dialog.user.status.active'),
statusImage: 'active',
hidePrivate: false
};
case 'join me':
return {
statusName: t('dialog.user.status.join_me'),
statusImage: 'joinme',
hidePrivate: false
};
case 'ask me':
return {
statusName: t('dialog.user.status.ask_me'),
statusImage: 'askme',
hidePrivate: discordHideInvite
};
case 'busy':
return {
statusName: t('dialog.user.status.busy'),
statusImage: 'busy',
hidePrivate: true
};
default:
return {
statusName: t('dialog.user.status.offline'),
statusImage: 'offline',
hidePrivate: true
};
}
}
+532
View File
@@ -0,0 +1,532 @@
import { removeEmojis, replaceBioSymbols } from './base/string';
/**
* Sanitize user JSON fields before applying to cache.
* Applies replaceBioSymbols to statusDescription, bio, note;
* removeEmojis to statusDescription;
* strips robot avatar URL.
* @param {object} json - Raw user API response
* @param {string} robotUrl - The robot/default avatar URL to strip
* @returns {object} The mutated json (same reference)
*/
export function sanitizeUserJson(json, robotUrl) {
if (json.statusDescription) {
json.statusDescription = replaceBioSymbols(json.statusDescription);
json.statusDescription = removeEmojis(json.statusDescription);
}
if (json.bio) {
json.bio = replaceBioSymbols(json.bio);
}
if (json.note) {
json.note = replaceBioSymbols(json.note);
}
if (robotUrl && json.currentAvatarImageUrl === robotUrl) {
delete json.currentAvatarImageUrl;
delete json.currentAvatarThumbnailImageUrl;
}
return json;
}
/**
* Sanitize arbitrary entity JSON fields via replaceBioSymbols.
* @param {object} json - Raw API response
* @param {string[]} fields - Field names to sanitize
* @returns {object} The mutated json
*/
export function sanitizeEntityJson(json, fields) {
for (const field of fields) {
if (json[field]) {
json[field] = replaceBioSymbols(json[field]);
}
}
return json;
}
/**
* Compute trust level, moderator status, and troll status from user tags.
* Pure function — no store dependencies.
* @param {string[]} tags - User tags array
* @param {string} developerType - User's developerType field
* @returns {{
* trustLevel: string,
* trustClass: string,
* trustSortNum: number,
* isModerator: boolean,
* isTroll: boolean,
* isProbableTroll: boolean,
* trustColorKey: string
* }}
*/
export function computeTrustLevel(tags, developerType) {
let isModerator = Boolean(developerType) && developerType !== 'none';
let isTroll = false;
let isProbableTroll = false;
let trustLevel = 'Visitor';
let trustClass = 'x-tag-untrusted';
let trustColorKey = 'untrusted';
let trustSortNum = 1;
if (tags.includes('admin_moderator')) {
isModerator = true;
}
if (tags.includes('system_troll')) {
isTroll = true;
}
if (tags.includes('system_probable_troll') && !isTroll) {
isProbableTroll = true;
}
if (tags.includes('system_trust_veteran')) {
trustLevel = 'Trusted User';
trustClass = 'x-tag-veteran';
trustColorKey = 'veteran';
trustSortNum = 5;
} else if (tags.includes('system_trust_trusted')) {
trustLevel = 'Known User';
trustClass = 'x-tag-trusted';
trustColorKey = 'trusted';
trustSortNum = 4;
} else if (tags.includes('system_trust_known')) {
trustLevel = 'User';
trustClass = 'x-tag-known';
trustColorKey = 'known';
trustSortNum = 3;
} else if (tags.includes('system_trust_basic')) {
trustLevel = 'New User';
trustClass = 'x-tag-basic';
trustColorKey = 'basic';
trustSortNum = 2;
}
if (isTroll || isProbableTroll) {
trustColorKey = 'troll';
trustSortNum += 0.1;
}
if (isModerator) {
trustColorKey = 'vip';
trustSortNum += 0.3;
}
return {
trustLevel,
trustClass,
trustSortNum,
isModerator,
isTroll,
isProbableTroll,
trustColorKey
};
}
/**
* Determine the effective user platform.
* @param {string} platform - Current platform
* @param {string} lastPlatform - Last known platform
* @returns {string} Resolved platform
*/
export function computeUserPlatform(platform, lastPlatform) {
if (platform && platform !== 'offline' && platform !== 'web') {
return platform;
}
return lastPlatform || '';
}
/**
* Compute which content settings are disabled for an instance.
* @param {object} contentSettings - The instance's contentSettings object
* @param {string[]} settingsList - List of all possible content setting keys
* @returns {string[]} Array of disabled setting keys
*/
export function computeDisabledContentSettings(contentSettings, settingsList) {
const disabled = [];
if (!contentSettings || Object.keys(contentSettings).length === 0) {
return disabled;
}
for (const setting of settingsList) {
if (
typeof contentSettings[setting] === 'undefined' ||
contentSettings[setting] === true
) {
continue;
}
disabled.push(setting);
}
return disabled;
}
/**
* Detect which properties changed between an existing ref and incoming JSON.
* Compares primitives directly; arrays via arraysMatchFn.
* @param {object} ref - The existing cached object
* @param {object} json - The incoming update
* @param {(a: any[], b: any[]) => boolean} arraysMatchFn - Function to compare arrays
* @returns {{ hasPropChanged: boolean, changedProps: object }}
*/
export function diffObjectProps(ref, json, arraysMatchFn) {
const changedProps = {};
let hasPropChanged = false;
// Only compare primitive values
for (const prop in ref) {
if (typeof json[prop] === 'undefined') {
continue;
}
if (ref[prop] === null || typeof ref[prop] !== 'object') {
changedProps[prop] = true;
}
}
// Check json props against ref (including array comparison)
for (const prop in json) {
if (typeof ref[prop] === 'undefined') {
continue;
}
if (Array.isArray(json[prop]) && Array.isArray(ref[prop])) {
if (!arraysMatchFn(json[prop], ref[prop])) {
changedProps[prop] = true;
}
} else if (json[prop] === null || typeof json[prop] !== 'object') {
changedProps[prop] = true;
}
}
// Resolve actual changes
for (const prop in changedProps) {
const asIs = ref[prop];
const toBe = json[prop];
if (asIs === toBe) {
delete changedProps[prop];
} else {
hasPropChanged = true;
changedProps[prop] = [toBe, asIs];
}
}
return { hasPropChanged, changedProps };
}
/**
* Create a default user ref object with all expected fields.
* Returns a plain object (caller wraps in reactive() if needed).
* @param {object} json - API response to merge
* @returns {object} Default user object with json spread on top
*/
export function createDefaultUserRef(json) {
return {
ageVerificationStatus: '',
ageVerified: false,
allowAvatarCopying: false,
badges: [],
bio: '',
bioLinks: [],
currentAvatarImageUrl: '',
currentAvatarTags: [],
currentAvatarThumbnailImageUrl: '',
date_joined: '',
developerType: '',
discordId: '',
displayName: '',
friendKey: '',
friendRequestStatus: '',
id: '',
instanceId: '',
isFriend: false,
last_activity: '',
last_login: '',
last_mobile: null,
last_platform: '',
location: '',
platform: '',
note: null,
profilePicOverride: '',
profilePicOverrideThumbnail: '',
pronouns: '',
state: '',
status: '',
statusDescription: '',
tags: [],
travelingToInstance: '',
travelingToLocation: '',
travelingToWorld: '',
userIcon: '',
worldId: '',
// only in bulk request
fallbackAvatar: '',
// VRCX
$location: {},
$location_at: Date.now(),
$online_for: Date.now(),
$travelingToTime: Date.now(),
$offline_for: null,
$active_for: Date.now(),
$isVRCPlus: false,
$isModerator: false,
$isTroll: false,
$isProbableTroll: false,
$trustLevel: 'Visitor',
$trustClass: 'x-tag-untrusted',
$userColour: '',
$trustSortNum: 1,
$languages: [],
$joinCount: 0,
$timeSpent: 0,
$lastSeen: '',
$mutualCount: 0,
$nickName: '',
$previousLocation: '',
$customTag: '',
$customTagColour: '',
$friendNumber: 0,
$platform: '',
$moderations: {},
//
...json
};
}
/**
* Create a default world ref object.
* @param {object} json - API response to merge
* @returns {object}
*/
export function createDefaultWorldRef(json) {
return {
id: '',
name: '',
description: '',
defaultContentSettings: {},
authorId: '',
authorName: '',
capacity: 0,
recommendedCapacity: 0,
tags: [],
releaseStatus: '',
imageUrl: '',
thumbnailImageUrl: '',
assetUrl: '',
assetUrlObject: {},
pluginUrl: '',
pluginUrlObject: {},
unityPackageUrl: '',
unityPackageUrlObject: {},
unityPackages: [],
version: 0,
favorites: 0,
created_at: '',
updated_at: '',
publicationDate: '',
labsPublicationDate: '',
visits: 0,
popularity: 0,
heat: 0,
publicOccupants: 0,
privateOccupants: 0,
occupants: 0,
instances: [],
featured: false,
organization: '',
previewYoutubeId: '',
// VRCX
$isLabs: false,
//
...json
};
}
/**
* Create a default avatar ref object.
* @param {object} json - API response to merge
* @returns {object}
*/
export function createDefaultAvatarRef(json) {
return {
acknowledgements: '',
authorId: '',
authorName: '',
created_at: '',
description: '',
featured: false,
highestPrice: null,
id: '',
imageUrl: '',
listingDate: null,
lock: false,
lowestPrice: null,
name: '',
pendingUpload: false,
performance: {},
productId: null,
publishedListings: [],
releaseStatus: '',
searchable: false,
styles: [],
tags: [],
thumbnailImageUrl: '',
unityPackageUrl: '',
unityPackageUrlObject: {},
unityPackages: [],
updated_at: '',
version: 0,
...json
};
}
/**
* Create a default group ref object.
* @param {object} json - API response to merge
* @returns {object}
*/
export function createDefaultGroupRef(json) {
return {
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
};
}
/**
* Create a default instance ref object.
* @param {object} json - API response to merge
* @returns {object}
*/
export function createDefaultInstanceRef(json) {
return {
id: '',
location: '',
instanceId: '',
name: '',
worldId: '',
type: '',
ownerId: '',
tags: [],
active: false,
full: false,
n_users: 0,
hasCapacityForYou: true, // not present depending on endpoint
capacity: 0,
recommendedCapacity: 0,
userCount: 0,
queueEnabled: false, // only present with group instance type
queueSize: 0, // only present when queuing is enabled
platforms: {},
gameServerVersion: 0,
hardClose: null, // boolean or null
closedAt: null, // string or null
secureName: '',
shortName: '',
world: {},
users: [], // only present when you're the owner
clientNumber: '',
contentSettings: {},
photonRegion: '',
region: '',
canRequestInvite: false,
permanent: false,
private: '', // part of instance tag
hidden: '', // part of instance tag
nonce: '', // only present when you're the owner
strict: false, // deprecated
displayName: null,
groupAccessType: null, // only present with group instance type
roleRestricted: false, // only present with group instance type
instancePersistenceEnabled: null,
playerPersistenceEnabled: null,
ageGate: null,
// VRCX
$fetchedAt: '',
$disabledContentSettings: [],
...json
};
}
/**
* Build a default favorite group ref from JSON data.
* @param {object} json
* @returns {object}
*/
export function createDefaultFavoriteGroupRef(json) {
return {
id: '',
ownerId: '',
ownerDisplayName: '',
name: '',
displayName: '',
type: '',
visibility: '',
tags: [],
...json
};
}
/**
* Build a default cached favorite ref from JSON data.
* Computes $groupKey from type and first tag.
* @param {object} json
* @returns {object}
*/
export function createDefaultFavoriteCachedRef(json) {
const ref = {
id: '',
type: '',
favoriteId: '',
tags: [],
// VRCX
$groupKey: '',
//
...json
};
ref.$groupKey = `${ref.type}:${String(ref.tags[0])}`;
return ref;
}
+133 -2
View File
@@ -76,7 +76,6 @@ function gameLogSearchFilter(row, searchQuery) {
/** /**
* Extract a millisecond timestamp from a game log row. * Extract a millisecond timestamp from a game log row.
* Handles numeric (seconds or millis), ISO string, and dayjs-parseable formats. * Handles numeric (seconds or millis), ISO string, and dayjs-parseable formats.
*
* @param {object} row * @param {object} row
* @returns {number} millisecond timestamp, or 0 if unparseable * @returns {number} millisecond timestamp, or 0 if unparseable
*/ */
@@ -105,7 +104,6 @@ function getGameLogCreatedAtTs(row) {
* Primary key: created_at timestamp (newest first). * Primary key: created_at timestamp (newest first).
* Secondary: rowId (highest first). * Secondary: rowId (highest first).
* Tertiary: uid string (reverse lexicographic). * Tertiary: uid string (reverse lexicographic).
*
* @param {object} a * @param {object} a
* @param {object} b * @param {object} b
* @returns {number} negative if a should come first, positive if b first * @returns {number} negative if a should come first, positive if b first
@@ -129,3 +127,136 @@ function compareGameLogRows(a, b) {
} }
export { gameLogSearchFilter, getGameLogCreatedAtTs, compareGameLogRows }; export { gameLogSearchFilter, getGameLogCreatedAtTs, compareGameLogRows };
/**
* Create a Location game log entry.
* @param {string} dt
* @param {string} location
* @param {string} worldId
* @param {string} worldName
* @returns {object}
*/
export function createLocationEntry(dt, location, worldId, worldName) {
return {
created_at: dt,
type: 'Location',
location,
worldId,
worldName,
groupName: '',
time: 0
};
}
/**
* Create a player join or leave game log entry.
* @param {'OnPlayerJoined'|'OnPlayerLeft'} type
* @param {string} dt
* @param {string} displayName
* @param {string} location
* @param {string} userId
* @param {number} [time]
* @returns {object}
*/
export function createJoinLeaveEntry(
type,
dt,
displayName,
location,
userId,
time = 0
) {
return {
created_at: dt,
type,
displayName,
location,
userId,
time
};
}
/**
* Create a PortalSpawn game log entry.
* @param {string} dt
* @param {string} location
* @returns {object}
*/
export function createPortalSpawnEntry(dt, location) {
return {
created_at: dt,
type: 'PortalSpawn',
location,
displayName: '',
userId: '',
instanceId: '',
worldName: ''
};
}
/**
* Create a resource load game log entry.
* @param {string} rawType - 'resource-load-string' or 'resource-load-image'
* @param {string} dt
* @param {string} resourceUrl
* @param {string} location
* @returns {object}
*/
export function createResourceLoadEntry(rawType, dt, resourceUrl, location) {
return {
created_at: dt,
type: rawType === 'resource-load-string' ? 'StringLoad' : 'ImageLoad',
resourceUrl,
location
};
}
/**
* Parse an API request URL for inventory info.
* Matches: /api/1/user/{userId}/inventory/{inventoryId}
* @example
* // https://api.vrchat.cloud/api/1/user/usr_032383a7-748c-4fb2-94e4-bcb928e5de6b/inventory/inv_75781d65-92fe-4a80-a1ff-27ee6e843b08
* @param {string} url
* @returns {{ userId: string, inventoryId: string } | null}
*/
export function parseInventoryFromUrl(url) {
try {
const parsed = new URL(url);
if (
parsed.pathname.substring(0, 12) === '/api/1/user/' &&
parsed.pathname.includes('/inventory/inv_')
) {
const pathArray = parsed.pathname.split('/');
const userId = pathArray[4];
const inventoryId = pathArray[6];
if (userId && inventoryId && inventoryId.length === 40) {
return { userId, inventoryId };
}
}
} catch {
// invalid URL
}
return null;
}
/**
* Parse an API request URL for print info.
* Matches: /api/1/prints/{printId}
* @param {string} url
* @returns {string|null} printId or null
*/
export function parsePrintFromUrl(url) {
try {
const parsed = new URL(url);
if (parsed.pathname.substring(0, 14) === '/api/1/prints/') {
const pathArray = parsed.pathname.split('/');
const printId = pathArray[4];
if (printId && printId.length === 41) {
return printId;
}
}
} catch {
// invalid URL
}
return null;
}
+4
View File
@@ -24,3 +24,7 @@ export * from './memos';
export * from './throttle'; export * from './throttle';
export * from './retry'; export * from './retry';
export * from './gameLog'; export * from './gameLog';
export * from './entityTransforms';
export * from './cacheUtils';
export * from './notificationTransforms';
export * from './discordPresence';
+33
View File
@@ -37,3 +37,36 @@ function getFriendsLocations(friendsArr) {
} }
export { getFriendsLocations }; export { getFriendsLocations };
/**
* Get the display text for a location — synchronous, pure function.
* Does NOT handle async world name lookups (those stay in the component).
* @param {object} L - Parsed location object from parseLocation()
* @param {object} options
* @param {string} [options.hint] - Hint string (e.g. from props)
* @param {string|undefined} [options.worldName] - Cached world name, if available
* @param {string} options.accessTypeLabel - Translated access type label
* @param {Function} options.t - i18n translate function
* @returns {string} Display text for the location
*/
function getLocationText(L, { hint, worldName, accessTypeLabel, t }) {
if (L.isOffline) {
return t('location.offline');
}
if (L.isPrivate) {
return t('location.private');
}
if (L.isTraveling) {
return t('location.traveling');
}
if (typeof hint === 'string' && hint !== '') {
return L.instanceId ? `${hint} · ${accessTypeLabel}` : hint;
}
if (L.worldId) {
const name = worldName || L.worldId;
return L.instanceId ? `${name} · ${accessTypeLabel}` : name;
}
return '';
}
export { getLocationText };
+120
View File
@@ -0,0 +1,120 @@
import { replaceBioSymbols } from './base/string';
/**
* Remove null/undefined keys from a notification JSON object
* and sanitize message/title fields with replaceBioSymbols.
* @param {object} json - notification data (mutated in place)
* @returns {object} the same json reference
*/
export function sanitizeNotificationJson(json) {
for (const key in json) {
if (json[key] === null || typeof json[key] === 'undefined') {
delete json[key];
}
}
if (json.message) {
json.message = replaceBioSymbols(json.message);
}
if (json.title) {
json.title = replaceBioSymbols(json.title);
}
return json;
}
/**
* Parse a notification's details field from string to object if needed.
* @param {*} details - raw details value
* @returns {object} parsed details object
*/
export function parseNotificationDetails(details) {
if (details === Object(details)) {
return details;
}
if (details !== '{}' && typeof details === 'string') {
try {
const object = JSON.parse(details);
if (object === Object(object)) {
return object;
}
} catch (err) {
console.log(err);
}
}
return {};
}
/**
* Build a default V1 notification ref from JSON data.
* Does NOT perform cache lookup — caller is responsible for
* checking existing refs and merging.
* @param {object} json - sanitized notification JSON
* @returns {object} default notification ref
*/
export function createDefaultNotificationRef(json) {
const ref = {
id: '',
senderUserId: '',
senderUsername: '',
type: '',
message: '',
details: {},
seen: false,
created_at: '',
// VRCX
$isExpired: false,
//
...json
};
ref.details = parseNotificationDetails(ref.details);
return ref;
}
/**
* Build a default V2 notification ref from JSON data.
* Handles boop legacy formatting.
* @param {object} json - sanitized notification JSON
* @param {string} endpointDomain - API endpoint domain for emoji URLs
* @returns {object} default notification V2 ref
*/
export function createDefaultNotificationV2Ref(json) {
return {
id: '',
createdAt: '',
updatedAt: '',
expiresAt: '',
type: '',
link: '',
linkText: '',
message: '',
title: '',
imageUrl: '',
seen: false,
senderUserId: '',
senderUsername: '',
data: {},
responses: [],
details: {},
version: 2,
...json
};
}
/**
* Apply legacy boop formatting to a V2 notification ref.
* Mutates the ref in place.
* @param {object} ref - notification V2 ref
* @param {string} endpointDomain - API endpoint domain for emoji URLs
*/
export function applyBoopLegacyHandling(ref, endpointDomain) {
if (ref.type !== 'boop' || !ref.title) {
return;
}
ref.message = ref.title;
ref.title = '';
if (ref.details?.emojiId?.startsWith('default_')) {
ref.imageUrl = ref.details.emojiId;
ref.message += ` ${ref.details.emojiId.replace('default_', '')}`;
} else {
ref.imageUrl = `${endpointDomain}/file/${ref.details.emojiId}/${ref.details.emojiVersion}`;
}
}
+1 -18
View File
@@ -2,6 +2,7 @@ import { useAppearanceSettingsStore, useUserStore } from '../../stores';
import { HueToHex } from './base/ui'; import { HueToHex } from './base/ui';
import { convertFileUrlToImageUrl } from './common'; import { convertFileUrlToImageUrl } from './common';
import { languageMappings } from '../constants'; import { languageMappings } from '../constants';
import { removeEmojis } from './base/string';
import { timeToText } from './base/format'; import { timeToText } from './base/format';
/** /**
@@ -46,24 +47,6 @@ async function getNameColour(userId) {
return HueToHex(hue); return HueToHex(hue);
} }
/**
*
* @param {string} text
* @returns
*/
function removeEmojis(text) {
if (!text) {
return '';
}
return text
.replace(
/([\u2700-\u27BF]|[\uE000-\uF8FF]|\uD83C[\uDC00-\uDFFF]|\uD83D[\uDC00-\uDFFF]|[\u2011-\u26FF]|\uD83E[\uDD10-\uDDFF])/g,
''
)
.replace(/\s+/g, ' ')
.trim();
}
/** /**
* *
* @param {object} user * @param {object} user
+6 -33
View File
@@ -5,17 +5,19 @@ import { useI18n } from 'vue-i18n';
import { import {
checkVRChatCache, checkVRChatCache,
createDefaultAvatarRef,
extractFileId, extractFileId,
getAvailablePlatforms, getAvailablePlatforms,
getBundleDateSize, getBundleDateSize,
getPlatformInfo, getPlatformInfo,
replaceBioSymbols, replaceBioSymbols,
sanitizeEntityJson,
storeAvatarImage storeAvatarImage
} from '../shared/utils'; } from '../shared/utils';
import { avatarRequest, miscRequest } from '../api'; import { avatarRequest, miscRequest } from '../api';
import { patchAvatarFromEvent } from '../query';
import { AppDebug } from '../service/appConfig'; import { AppDebug } from '../service/appConfig';
import { database } from '../service/database'; import { database } from '../service/database';
import { patchAvatarFromEvent } from '../query';
import { processBulk } from '../service/request'; import { processBulk } from '../service/request';
import { useAdvancedSettingsStore } from './settings/advanced'; import { useAdvancedSettingsStore } from './settings/advanced';
import { useAvatarProviderStore } from './avatarProvider'; import { useAvatarProviderStore } from './avatarProvider';
@@ -92,40 +94,10 @@ export const useAvatarStore = defineStore('Avatar', () => {
* @returns {object} ref * @returns {object} ref
*/ */
function applyAvatar(json) { function applyAvatar(json) {
json.name = replaceBioSymbols(json.name); sanitizeEntityJson(json, ['name', 'description']);
json.description = replaceBioSymbols(json.description);
let ref = cachedAvatars.get(json.id); let ref = cachedAvatars.get(json.id);
if (typeof ref === 'undefined') { if (typeof ref === 'undefined') {
ref = { ref = createDefaultAvatarRef(json);
acknowledgements: '',
authorId: '',
authorName: '',
created_at: '',
description: '',
featured: false,
highestPrice: null,
id: '',
imageUrl: '',
listingDate: null,
lock: false,
lowestPrice: null,
name: '',
pendingUpload: false,
performance: {},
productId: null,
publishedListings: [],
releaseStatus: '',
searchable: false,
styles: [],
tags: [],
thumbnailImageUrl: '',
unityPackageUrl: '',
unityPackageUrlObject: {},
unityPackages: [],
updated_at: '',
version: 0,
...json
};
cachedAvatars.set(ref.id, ref); cachedAvatars.set(ref.id, ref);
} else { } else {
const { unityPackages } = ref; const { unityPackages } = ref;
@@ -178,6 +150,7 @@ export const useAvatarStore = defineStore('Avatar', () => {
/** /**
* *
* @param {string} avatarId * @param {string} avatarId
* @param options
* @returns * @returns
*/ */
function showAvatarDialog(avatarId, options = {}) { function showAvatarDialog(avatarId, options = {}) {
+92 -22
View File
@@ -5,6 +5,8 @@ import { useI18n } from 'vue-i18n';
import { import {
compareByName, compareByName,
createDefaultFavoriteCachedRef,
createDefaultFavoriteGroupRef,
removeFromArray, removeFromArray,
replaceReactiveObject replaceReactiveObject
} from '../shared/utils'; } from '../shared/utils';
@@ -209,6 +211,11 @@ export const useFavoriteStore = defineStore('Favorite', () => {
return favoriteGroup.length; return favoriteGroup.length;
}); });
/**
*
* @param list
* @param selectionRef
*/
function syncFavoriteSelection(list, selectionRef) { function syncFavoriteSelection(list, selectionRef) {
if (!Array.isArray(list)) { if (!Array.isArray(list)) {
selectionRef.value = []; selectionRef.value = [];
@@ -255,6 +262,9 @@ export const useFavoriteStore = defineStore('Favorite', () => {
{ flush: 'sync' } { flush: 'sync' }
); );
/**
*
*/
function getCachedFavoriteGroupsByTypeName() { function getCachedFavoriteGroupsByTypeName() {
const group = {}; const group = {};
@@ -274,10 +284,18 @@ export const useFavoriteStore = defineStore('Favorite', () => {
return group; return group;
} }
/**
*
* @param objectId
*/
function getCachedFavoritesByObjectId(objectId) { function getCachedFavoritesByObjectId(objectId) {
return cachedFavoritesByObjectId.get(objectId); return cachedFavoritesByObjectId.get(objectId);
} }
/**
*
* @param args
*/
function handleFavoriteAdd(args) { function handleFavoriteAdd(args) {
handleFavorite({ handleFavorite({
json: args.json, json: args.json,
@@ -310,6 +328,10 @@ export const useFavoriteStore = defineStore('Favorite', () => {
updateFavoriteDialog(args.params.objectId); updateFavoriteDialog(args.params.objectId);
} }
/**
*
* @param args
*/
function handleFavorite(args) { function handleFavorite(args) {
args.ref = applyFavoriteCached(args.json); args.ref = applyFavoriteCached(args.json);
applyFavorite(args.ref.type, args.ref.favoriteId); applyFavorite(args.ref.type, args.ref.favoriteId);
@@ -329,6 +351,10 @@ export const useFavoriteStore = defineStore('Favorite', () => {
} }
} }
/**
*
* @param objectId
*/
function handleFavoriteDelete(objectId) { function handleFavoriteDelete(objectId) {
const ref = getCachedFavoritesByObjectId(objectId); const ref = getCachedFavoritesByObjectId(objectId);
if (typeof ref === 'undefined') { if (typeof ref === 'undefined') {
@@ -337,10 +363,18 @@ export const useFavoriteStore = defineStore('Favorite', () => {
handleFavoriteAtDelete(ref); handleFavoriteAtDelete(ref);
} }
/**
*
* @param args
*/
function handleFavoriteGroup(args) { function handleFavoriteGroup(args) {
args.ref = applyFavoriteGroup(args.json); args.ref = applyFavoriteGroup(args.json);
} }
/**
*
* @param args
*/
function handleFavoriteGroupClear(args) { function handleFavoriteGroupClear(args) {
const key = `${args.params.type}:${args.params.group}`; const key = `${args.params.type}:${args.params.group}`;
for (const ref of cachedFavorites.values()) { for (const ref of cachedFavorites.values()) {
@@ -351,6 +385,10 @@ export const useFavoriteStore = defineStore('Favorite', () => {
} }
} }
/**
*
* @param args
*/
function handleFavoriteWorldList(args) { function handleFavoriteWorldList(args) {
for (const json of args.json) { for (const json of args.json) {
if (json.id === '???') { if (json.id === '???') {
@@ -360,6 +398,10 @@ export const useFavoriteStore = defineStore('Favorite', () => {
} }
} }
/**
*
* @param args
*/
function handleFavoriteAvatarList(args) { function handleFavoriteAvatarList(args) {
for (const json of args.json) { for (const json of args.json) {
if (json.releaseStatus === 'hidden') { if (json.releaseStatus === 'hidden') {
@@ -369,6 +411,10 @@ export const useFavoriteStore = defineStore('Favorite', () => {
} }
} }
/**
*
* @param ref
*/
function handleFavoriteAtDelete(ref) { function handleFavoriteAtDelete(ref) {
const favorite = state.favoriteObjects.get(ref.favoriteId); const favorite = state.favoriteObjects.get(ref.favoriteId);
removeFromArray(state.favoriteFriends_, favorite); removeFromArray(state.favoriteFriends_, favorite);
@@ -536,6 +582,9 @@ export const useFavoriteStore = defineStore('Favorite', () => {
} }
} }
/**
*
*/
function refreshFavoriteGroups() { function refreshFavoriteGroups() {
if (isFavoriteGroupLoading.value) { if (isFavoriteGroupLoading.value) {
return; return;
@@ -567,6 +616,9 @@ export const useFavoriteStore = defineStore('Favorite', () => {
}); });
} }
/**
*
*/
function buildFavoriteGroups() { function buildFavoriteGroups() {
let group; let group;
let groups; let groups;
@@ -683,6 +735,9 @@ export const useFavoriteStore = defineStore('Favorite', () => {
countFavoriteGroups(); countFavoriteGroups();
} }
/**
*
*/
function countFavoriteGroups() { function countFavoriteGroups() {
const cachedFavoriteGroups = getCachedFavoriteGroupsByTypeName(); const cachedFavoriteGroups = getCachedFavoriteGroupsByTypeName();
for (const key in cachedFavoriteGroups) { for (const key in cachedFavoriteGroups) {
@@ -764,17 +819,7 @@ export const useFavoriteStore = defineStore('Favorite', () => {
function applyFavoriteGroup(json) { function applyFavoriteGroup(json) {
let ref = cachedFavoriteGroups.value[json.id]; let ref = cachedFavoriteGroups.value[json.id];
if (typeof ref === 'undefined') { if (typeof ref === 'undefined') {
ref = { ref = createDefaultFavoriteGroupRef(json);
id: '',
ownerId: '',
ownerDisplayName: '',
name: '',
displayName: '',
type: '',
visibility: '',
tags: [],
...json
};
cachedFavoriteGroups.value[ref.id] = ref; cachedFavoriteGroups.value[ref.id] = ref;
} else { } else {
Object.assign(ref, json); Object.assign(ref, json);
@@ -790,19 +835,9 @@ export const useFavoriteStore = defineStore('Favorite', () => {
function applyFavoriteCached(json) { function applyFavoriteCached(json) {
let ref = cachedFavorites.get(json.id); let ref = cachedFavorites.get(json.id);
if (typeof ref === 'undefined') { if (typeof ref === 'undefined') {
ref = { ref = createDefaultFavoriteCachedRef(json);
id: '',
type: '',
favoriteId: '',
tags: [],
// VRCX
$groupKey: '',
//
...json
};
cachedFavorites.set(ref.id, ref); cachedFavorites.set(ref.id, ref);
cachedFavoritesByObjectId.set(ref.favoriteId, ref); cachedFavoritesByObjectId.set(ref.favoriteId, ref);
ref.$groupKey = `${ref.type}:${String(ref.tags[0])}`;
if ( if (
ref.type === 'friend' && ref.type === 'friend' &&
(!generalSettingsStore.localFavoriteFriendsGroups.some( (!generalSettingsStore.localFavoriteFriendsGroups.some(
@@ -893,14 +928,23 @@ export const useFavoriteStore = defineStore('Favorite', () => {
} }
} }
/**
*
*/
function showWorldImportDialog() { function showWorldImportDialog() {
worldImportDialogVisible.value = true; worldImportDialogVisible.value = true;
} }
/**
*
*/
function showAvatarImportDialog() { function showAvatarImportDialog() {
avatarImportDialogVisible.value = true; avatarImportDialogVisible.value = true;
} }
/**
*
*/
function showFriendImportDialog() { function showFriendImportDialog() {
friendImportDialogVisible.value = true; friendImportDialogVisible.value = true;
} }
@@ -1016,6 +1060,10 @@ export const useFavoriteStore = defineStore('Favorite', () => {
return false; return false;
} }
/**
*
* @param objectId
*/
function updateFavoriteDialog(objectId) { function updateFavoriteDialog(objectId) {
const D = favoriteDialog.value; const D = favoriteDialog.value;
if (!D.visible || D.objectId !== objectId) { if (!D.visible || D.objectId !== objectId) {
@@ -1108,6 +1156,9 @@ export const useFavoriteStore = defineStore('Favorite', () => {
}); });
} }
/**
*
*/
function sortLocalAvatarFavorites() { function sortLocalAvatarFavorites() {
if (!appearanceSettingsStore.sortFavorites) { if (!appearanceSettingsStore.sortFavorites) {
for (let i = 0; i < localAvatarFavoriteGroups.value.length; ++i) { for (let i = 0; i < localAvatarFavoriteGroups.value.length; ++i) {
@@ -1294,6 +1345,9 @@ export const useFavoriteStore = defineStore('Favorite', () => {
}); });
} }
/**
*
*/
function sortLocalWorldFavorites() { function sortLocalWorldFavorites() {
if (!appearanceSettingsStore.sortFavorites) { if (!appearanceSettingsStore.sortFavorites) {
for (let i = 0; i < localWorldFavoriteGroups.value.length; ++i) { for (let i = 0; i < localWorldFavoriteGroups.value.length; ++i) {
@@ -1718,6 +1772,11 @@ export const useFavoriteStore = defineStore('Favorite', () => {
}); });
} }
/**
*
* @param type
* @param objectId
*/
function showFavoriteDialog(type, objectId) { function showFavoriteDialog(type, objectId) {
const D = favoriteDialog.value; const D = favoriteDialog.value;
D.type = type; D.type = type;
@@ -1726,12 +1785,18 @@ export const useFavoriteStore = defineStore('Favorite', () => {
updateFavoriteDialog(objectId); updateFavoriteDialog(objectId);
} }
/**
*
*/
async function saveSortFavoritesOption() { async function saveSortFavoritesOption() {
getLocalWorldFavorites(); getLocalWorldFavorites();
getLocalFriendFavorites(); getLocalFriendFavorites();
appearanceSettingsStore.setSortFavorites(); appearanceSettingsStore.setSortFavorites();
} }
/**
*
*/
async function initFavorites() { async function initFavorites() {
refreshFavorites(); refreshFavorites();
getLocalWorldFavorites(); getLocalWorldFavorites();
@@ -1739,6 +1804,11 @@ export const useFavoriteStore = defineStore('Favorite', () => {
getLocalFriendFavorites(); getLocalFriendFavorites();
} }
/**
*
* @param a
* @param b
*/
function compareByFavoriteSortOrder(a, b) { function compareByFavoriteSortOrder(a, b) {
const indexA = favoritesSortOrder.value.indexOf(a.id); const indexA = favoritesSortOrder.value.indexOf(a.id);
const indexB = favoritesSortOrder.value.indexOf(b.id); const indexB = favoritesSortOrder.value.indexOf(b.id);
+106 -71
View File
@@ -8,11 +8,17 @@ import dayjs from 'dayjs';
import { import {
compareGameLogRows, compareGameLogRows,
createJoinLeaveEntry,
createLocationEntry,
createPortalSpawnEntry,
createResourceLoadEntry,
findUserByDisplayName, findUserByDisplayName,
formatSeconds, formatSeconds,
gameLogSearchFilter, gameLogSearchFilter,
getGroupName, getGroupName,
parseInventoryFromUrl,
parseLocation, parseLocation,
parsePrintFromUrl,
replaceBioSymbols replaceBioSymbols
} from '../../shared/utils'; } from '../../shared/utils';
import { AppDebug } from '../../service/appConfig'; import { AppDebug } from '../../service/appConfig';
@@ -131,6 +137,9 @@ export const useGameLogStore = defineStore('GameLog', () => {
{ flush: 'sync' } { flush: 'sync' }
); );
/**
*
*/
async function init() { async function init() {
gameLogTable.value.filter = JSON.parse( gameLogTable.value.filter = JSON.parse(
await configRepository.getString('VRCX_gameLogTableFilters', '[]') await configRepository.getString('VRCX_gameLogTableFilters', '[]')
@@ -143,6 +152,10 @@ export const useGameLogStore = defineStore('GameLog', () => {
init(); init();
/**
*
* @param entry
*/
function insertGameLogSorted(entry) { function insertGameLogSorted(entry) {
const arr = gameLogTableData.value; const arr = gameLogTableData.value;
if (arr.length === 0) { if (arr.length === 0) {
@@ -170,6 +183,9 @@ export const useGameLogStore = defineStore('GameLog', () => {
gameLogTableData.value = [...arr, entry]; gameLogTableData.value = [...arr, entry];
} }
/**
*
*/
function clearNowPlaying() { function clearNowPlaying() {
nowPlaying.value = { nowPlaying.value = {
url: '', url: '',
@@ -186,6 +202,10 @@ export const useGameLogStore = defineStore('GameLog', () => {
vrStore.updateVrNowPlaying(); vrStore.updateVrNowPlaying();
} }
/**
*
* @param data
*/
function setNowPlaying(data) { function setNowPlaying(data) {
const ctx = structuredClone(data); const ctx = structuredClone(data);
if (nowPlaying.value.url !== ctx.videoUrl) { if (nowPlaying.value.url !== ctx.videoUrl) {
@@ -257,6 +277,9 @@ export const useGameLogStore = defineStore('GameLog', () => {
advancedSettingsStore advancedSettingsStore
}); });
/**
*
*/
function updateNowPlaying() { function updateNowPlaying() {
const np = nowPlaying.value; const np = nowPlaying.value;
if (!nowPlaying.value.playing) { if (!nowPlaying.value.playing) {
@@ -275,6 +298,9 @@ export const useGameLogStore = defineStore('GameLog', () => {
workerTimers.setTimeout(() => updateNowPlaying(), 1000); workerTimers.setTimeout(() => updateNowPlaying(), 1000);
} }
/**
*
*/
async function tryLoadPlayerList() { async function tryLoadPlayerList() {
// TODO: make this work again // TODO: make this work again
if (!gameStore.isGameRunning) { if (!gameStore.isGameRunning) {
@@ -355,6 +381,10 @@ export const useGameLogStore = defineStore('GameLog', () => {
} }
} }
/**
*
* @param row
*/
function gameLogIsFriend(row) { function gameLogIsFriend(row) {
if (typeof row.isFriend !== 'undefined') { if (typeof row.isFriend !== 'undefined') {
return row.isFriend; return row.isFriend;
@@ -365,6 +395,10 @@ export const useGameLogStore = defineStore('GameLog', () => {
return friendStore.friends.has(row.userId); return friendStore.friends.has(row.userId);
} }
/**
*
* @param row
*/
function gameLogIsFavorite(row) { function gameLogIsFavorite(row) {
if (typeof row.isFavorite !== 'undefined') { if (typeof row.isFavorite !== 'undefined') {
return row.isFavorite; return row.isFavorite;
@@ -375,6 +409,9 @@ export const useGameLogStore = defineStore('GameLog', () => {
return friendStore.localFavoriteFriends.has(row.userId); return friendStore.localFavoriteFriends.has(row.userId);
} }
/**
*
*/
async function gameLogTableLookup() { async function gameLogTableLookup() {
await configRepository.setString( await configRepository.setString(
'VRCX_gameLogTableFilters', 'VRCX_gameLogTableFilters',
@@ -416,6 +453,10 @@ export const useGameLogStore = defineStore('GameLog', () => {
} }
} }
/**
*
* @param entry
*/
function addGameLog(entry) { function addGameLog(entry) {
entry.isFriend = gameLogIsFriend(entry); entry.isFriend = gameLogIsFriend(entry);
entry.isFavorite = gameLogIsFavorite(entry); entry.isFavorite = gameLogIsFavorite(entry);
@@ -456,6 +497,10 @@ export const useGameLogStore = defineStore('GameLog', () => {
uiStore.notifyMenu('game-log'); uiStore.notifyMenu('game-log');
} }
/**
*
* @param input
*/
async function addGamelogLocationToDatabase(input) { async function addGamelogLocationToDatabase(input) {
const groupName = await getGroupName(input.location); const groupName = await getGroupName(input.location);
const entry = { const entry = {
@@ -465,10 +510,17 @@ export const useGameLogStore = defineStore('GameLog', () => {
database.addGamelogLocationToDatabase(entry); database.addGamelogLocationToDatabase(entry);
} }
/**
*
* @param row
*/
function gameLogSearch(row) { function gameLogSearch(row) {
return gameLogSearchFilter(row, gameLogTable.value.search); return gameLogSearchFilter(row, gameLogTable.value.search);
} }
/**
*
*/
function sweepGameLog() { function sweepGameLog() {
const j = gameLogTableData.value.length; const j = gameLogTableData.value.length;
if (j > vrcxStore.maxTableSize + 50) { if (j > vrcxStore.maxTableSize + 50) {
@@ -476,6 +528,11 @@ export const useGameLogStore = defineStore('GameLog', () => {
} }
} }
/**
*
* @param gameLog
* @param location
*/
function addGameLogEntry(gameLog, location) { function addGameLogEntry(gameLog, location) {
let entry = undefined; let entry = undefined;
if (advancedSettingsStore.gameLogDisabled) { if (advancedSettingsStore.gameLogDisabled) {
@@ -543,15 +600,12 @@ export const useGameLogStore = defineStore('GameLog', () => {
gameLog.dt gameLog.dt
); );
const L = parseLocation(gameLog.location); const L = parseLocation(gameLog.location);
entry = { entry = createLocationEntry(
created_at: gameLog.dt, gameLog.dt,
type: 'Location', gameLog.location,
location: gameLog.location, L.worldId,
worldId: L.worldId, worldName
worldName, );
groupName: '',
time: 0
};
getGroupName(gameLog.location).then((groupName) => { getGroupName(gameLog.location).then((groupName) => {
entry.groupName = groupName; entry.groupName = groupName;
}); });
@@ -595,14 +649,13 @@ export const useGameLogStore = defineStore('GameLog', () => {
} }
vrStore.updateVRLastLocation(); vrStore.updateVRLastLocation();
instanceStore.getCurrentInstanceUserList(); instanceStore.getCurrentInstanceUserList();
entry = { entry = createJoinLeaveEntry(
created_at: gameLog.dt, 'OnPlayerJoined',
type: 'OnPlayerJoined', gameLog.dt,
displayName: gameLog.displayName, gameLog.displayName,
location, location,
userId, userId
time: 0 );
};
database.addGamelogJoinLeaveToDatabase(entry); database.addGamelogJoinLeaveToDatabase(entry);
break; break;
case 'player-left': case 'player-left':
@@ -617,29 +670,21 @@ export const useGameLogStore = defineStore('GameLog', () => {
photonStore.photonLobbyAvatars.delete(userId); photonStore.photonLobbyAvatars.delete(userId);
vrStore.updateVRLastLocation(); vrStore.updateVRLastLocation();
instanceStore.getCurrentInstanceUserList(); instanceStore.getCurrentInstanceUserList();
entry = { entry = createJoinLeaveEntry(
created_at: gameLog.dt, 'OnPlayerLeft',
type: 'OnPlayerLeft', gameLog.dt,
displayName: gameLog.displayName, gameLog.displayName,
location, location,
userId, userId,
time time
}; );
database.addGamelogJoinLeaveToDatabase(entry); database.addGamelogJoinLeaveToDatabase(entry);
break; break;
case 'portal-spawn': case 'portal-spawn':
if (vrcxStore.ipcEnabled && gameStore.isGameRunning) { if (vrcxStore.ipcEnabled && gameStore.isGameRunning) {
break; break;
} }
entry = { entry = createPortalSpawnEntry(gameLog.dt, location);
created_at: gameLog.dt,
type: 'PortalSpawn',
location,
displayName: '',
userId: '',
instanceId: '',
worldName: ''
};
database.addGamelogPortalSpawnToDatabase(entry); database.addGamelogPortalSpawnToDatabase(entry);
break; break;
case 'video-play': case 'video-play':
@@ -665,15 +710,12 @@ export const useGameLogStore = defineStore('GameLog', () => {
break; break;
} }
lastResourceloadUrl.value = gameLog.resourceUrl; lastResourceloadUrl.value = gameLog.resourceUrl;
entry = { entry = createResourceLoadEntry(
created_at: gameLog.dt, gameLog.type,
type: gameLog.dt,
gameLog.type === 'resource-load-string' gameLog.resourceUrl,
? 'StringLoad'
: 'ImageLoad',
resourceUrl: gameLog.resourceUrl,
location location
}; );
database.addGamelogResourceLoadToDatabase(entry); database.addGamelogResourceLoadToDatabase(entry);
break; break;
case 'screenshot': case 'screenshot':
@@ -711,42 +753,18 @@ export const useGameLogStore = defineStore('GameLog', () => {
// } // }
if (advancedSettingsStore.saveInstanceEmoji) { if (advancedSettingsStore.saveInstanceEmoji) {
try { const inv = parseInventoryFromUrl(gameLog.url);
// https://api.vrchat.cloud/api/1/user/usr_032383a7-748c-4fb2-94e4-bcb928e5de6b/inventory/inv_75781d65-92fe-4a80-a1ff-27ee6e843b08 if (inv) {
const url = new URL(gameLog.url); galleryStore.queueCheckInstanceInventory(
if ( inv.inventoryId,
url.pathname.substring(0, 12) === '/api/1/user/' && inv.userId
url.pathname.includes('/inventory/inv_') );
) {
const pathArray = url.pathname.split('/');
const userId = pathArray[4];
const inventoryId = pathArray[6];
if (userId && inventoryId.length === 40) {
galleryStore.queueCheckInstanceInventory(
inventoryId,
userId
);
}
}
} catch (err) {
console.error(err);
} }
} }
if (advancedSettingsStore.saveInstancePrints) { if (advancedSettingsStore.saveInstancePrints) {
try { const printId = parsePrintFromUrl(gameLog.url);
let printId = ''; if (printId) {
const url1 = new URL(gameLog.url); galleryStore.queueSavePrintToFile(printId);
if (
url1.pathname.substring(0, 14) === '/api/1/prints/'
) {
const pathArray = url1.pathname.split('/');
printId = pathArray[4];
}
if (printId && printId.length === 41) {
galleryStore.queueSavePrintToFile(printId);
}
} catch (err) {
console.error(err);
} }
} }
break; break;
@@ -902,12 +920,19 @@ export const useGameLogStore = defineStore('GameLog', () => {
} }
} }
/**
*
*/
async function getGameLogTable() { async function getGameLogTable() {
await database.initTables(); await database.initTables();
const dateTill = await database.getLastDateGameLogDatabase(); const dateTill = await database.getLastDateGameLogDatabase();
updateGameLog(dateTill); updateGameLog(dateTill);
} }
/**
*
* @param dateTill
*/
async function updateGameLog(dateTill) { async function updateGameLog(dateTill) {
await gameLogService.setDateTill(dateTill); await gameLogService.setDateTill(dateTill);
await new Promise((resolve) => { await new Promise((resolve) => {
@@ -923,6 +948,10 @@ export const useGameLogStore = defineStore('GameLog', () => {
} }
// use in C# // use in C#
/**
*
* @param json
*/
function addGameLogEvent(json) { function addGameLogEvent(json) {
const rawLogs = JSON.parse(json); const rawLogs = JSON.parse(json);
const gameLog = gameLogService.parseRawGameLog( const gameLog = gameLogService.parseRawGameLog(
@@ -941,6 +970,9 @@ export const useGameLogStore = defineStore('GameLog', () => {
addGameLogEntry(gameLog, locationStore.lastLocation.location); addGameLogEntry(gameLog, locationStore.lastLocation.location);
} }
/**
*
*/
async function disableGameLogDialog() { async function disableGameLogDialog() {
if (gameStore.isGameRunning) { if (gameStore.isGameRunning) {
toast.error(t('message.gamelog.vrchat_must_be_closed')); toast.error(t('message.gamelog.vrchat_must_be_closed'));
@@ -962,6 +994,9 @@ export const useGameLogStore = defineStore('GameLog', () => {
} }
} }
/**
*
*/
async function initGameLogTable() { async function initGameLogTable() {
gameLogTable.value.loading = true; gameLogTable.value.loading = true;
const rows = await database.lookupGameLogDatabase( const rows = await database.lookupGameLogDatabase(
+137 -96
View File
@@ -3,20 +3,22 @@ import { defineStore } from 'pinia';
import { toast } from 'vue-sonner'; import { toast } from 'vue-sonner';
import { useI18n } from 'vue-i18n'; import { useI18n } from 'vue-i18n';
import {
convertFileUrlToImageUrl,
createDefaultGroupRef,
hasGroupPermission,
replaceBioSymbols,
sanitizeEntityJson
} from '../shared/utils';
import { import {
groupRequest, groupRequest,
instanceRequest, instanceRequest,
userRequest, userRequest,
worldRequest worldRequest
} from '../api'; } from '../api';
import { patchGroupFromEvent } from '../query';
import {
convertFileUrlToImageUrl,
hasGroupPermission,
replaceBioSymbols
} from '../shared/utils';
import { database } from '../service/database'; import { database } from '../service/database';
import { groupDialogFilterOptions } from '../shared/constants/'; import { groupDialogFilterOptions } from '../shared/constants/';
import { patchGroupFromEvent } from '../query';
import { useGameStore } from './game'; import { useGameStore } from './game';
import { useInstanceStore } from './instance'; import { useInstanceStore } from './instance';
import { useModalStore } from './modal'; import { useModalStore } from './modal';
@@ -128,6 +130,11 @@ export const useGroupStore = defineStore('Group', () => {
{ flush: 'sync' } { flush: 'sync' }
); );
/**
*
* @param groupId
* @param options
*/
function showGroupDialog(groupId, options = {}) { function showGroupDialog(groupId, options = {}) {
if (!groupId) { if (!groupId) {
return; return;
@@ -232,6 +239,11 @@ export const useGroupStore = defineStore('Group', () => {
); );
} }
/**
*
* @param ref
* @param message
*/
function groupChange(ref, message) { function groupChange(ref, message) {
if (!currentUserGroupsInit.value) { if (!currentUserGroupsInit.value) {
return; return;
@@ -260,6 +272,9 @@ export const useGroupStore = defineStore('Group', () => {
workerTimers.setTimeout(saveCurrentUserGroups, 100); workerTimers.setTimeout(saveCurrentUserGroups, 100);
} }
/**
*
*/
function saveCurrentUserGroups() { function saveCurrentUserGroups() {
if (!currentUserGroupsInit.value) { if (!currentUserGroupsInit.value) {
return; return;
@@ -284,10 +299,10 @@ export const useGroupStore = defineStore('Group', () => {
/** /**
* *
* @param {object} ref * @param {object} ref
* @param {array} oldRoles * @param {Array} oldRoles
* @param {array} newRoles * @param {Array} newRoles
* @param {array} oldRoleIds * @param {Array} oldRoleIds
* @param {array} newRoleIds * @param {Array} newRoleIds
*/ */
function groupRoleChange(ref, oldRoles, newRoles, oldRoleIds, newRoleIds) { function groupRoleChange(ref, oldRoles, newRoles, oldRoleIds, newRoleIds) {
// check for removed/added roleIds // check for removed/added roleIds
@@ -401,7 +416,7 @@ export const useGroupStore = defineStore('Group', () => {
/** /**
* *
* @param {{ groupId: string }} params * @param {{ groupId: string }} params
* @return { Promise<{posts: any, params}> } * @returns { Promise<{posts: any, params}> }
*/ */
async function getAllGroupPosts(params) { async function getAllGroupPosts(params) {
const n = 100; const n = 100;
@@ -442,6 +457,10 @@ export const useGroupStore = defineStore('Group', () => {
return returnArgs; return returnArgs;
} }
/**
*
* @param groupId
*/
function getGroupDialogGroup(groupId) { function getGroupDialogGroup(groupId) {
const D = groupDialog.value; const D = groupDialog.value;
D.isGetGroupDialogGroupLoading = false; D.isGetGroupDialogGroupLoading = false;
@@ -497,32 +516,38 @@ export const useGroupStore = defineStore('Group', () => {
}); });
} }
}); });
groupRequest.getCachedGroupCalendar(groupId).then((args) => { groupRequest
if (groupDialog.value.id === args.params.groupId) { .getCachedGroupCalendar(groupId)
D.calendar = args.json.results; .then((args) => {
for (const event of D.calendar) { if (groupDialog.value.id === args.params.groupId) {
applyGroupEvent(event); D.calendar = args.json.results;
// fetch again for isFollowing for (const event of D.calendar) {
groupRequest applyGroupEvent(event);
.getCachedGroupCalendarEvent({ // fetch again for isFollowing
groupId, groupRequest
eventId: event.id .getCachedGroupCalendarEvent({
}) groupId,
.then((args) => { eventId: event.id
Object.assign( })
event, .then((args) => {
applyGroupEvent(args.json) Object.assign(
); event,
}); applyGroupEvent(args.json)
);
});
}
} }
} });
});
} }
nextTick(() => (D.isGetGroupDialogGroupLoading = false)); nextTick(() => (D.isGetGroupDialogGroupLoading = false));
return args; return args;
}); });
} }
/**
*
* @param event
*/
function applyGroupEvent(event) { function applyGroupEvent(event) {
return { return {
userInterest: { userInterest: {
@@ -536,6 +561,9 @@ export const useGroupStore = defineStore('Group', () => {
}; };
} }
/**
*
*/
async function updateInGameGroupOrder() { async function updateInGameGroupOrder() {
inGameGroupOrder.value = []; inGameGroupOrder.value = [];
try { try {
@@ -551,6 +579,11 @@ export const useGroupStore = defineStore('Group', () => {
} }
} }
/**
*
* @param a
* @param b
*/
function sortGroupInstancesByInGame(a, b) { function sortGroupInstancesByInGame(a, b) {
const aIndex = inGameGroupOrder.value.indexOf(a?.group?.id); const aIndex = inGameGroupOrder.value.indexOf(a?.group?.id);
const bIndex = inGameGroupOrder.value.indexOf(b?.group?.id); const bIndex = inGameGroupOrder.value.indexOf(b?.group?.id);
@@ -566,6 +599,10 @@ export const useGroupStore = defineStore('Group', () => {
return aIndex - bIndex; return aIndex - bIndex;
} }
/**
*
* @param groupId
*/
function leaveGroup(groupId) { function leaveGroup(groupId) {
groupRequest groupRequest
.leaveGroup({ .leaveGroup({
@@ -590,6 +627,10 @@ export const useGroupStore = defineStore('Group', () => {
}); });
} }
/**
*
* @param groupId
*/
function leaveGroupPrompt(groupId) { function leaveGroupPrompt(groupId) {
modalStore modalStore
.confirm({ .confirm({
@@ -603,6 +644,9 @@ export const useGroupStore = defineStore('Group', () => {
.catch(() => {}); .catch(() => {});
} }
/**
*
*/
function updateGroupPostSearch() { function updateGroupPostSearch() {
const D = groupDialog.value; const D = groupDialog.value;
const search = D.postsSearch.toLowerCase(); const search = D.postsSearch.toLowerCase();
@@ -620,6 +664,11 @@ export const useGroupStore = defineStore('Group', () => {
}); });
} }
/**
*
* @param groupId
* @param visibility
*/
function setGroupVisibility(groupId, visibility) { function setGroupVisibility(groupId, visibility) {
return groupRequest return groupRequest
.setGroupMemberProps(userStore.currentUser.id, groupId, { .setGroupMemberProps(userStore.currentUser.id, groupId, {
@@ -632,6 +681,11 @@ export const useGroupStore = defineStore('Group', () => {
}); });
} }
/**
*
* @param groupId
* @param subscribe
*/
function setGroupSubscription(groupId, subscribe) { function setGroupSubscription(groupId, subscribe) {
return groupRequest return groupRequest
.setGroupMemberProps(userStore.currentUser.id, groupId, { .setGroupMemberProps(userStore.currentUser.id, groupId, {
@@ -651,73 +705,9 @@ export const useGroupStore = defineStore('Group', () => {
*/ */
function applyGroup(json) { function applyGroup(json) {
let ref = cachedGroups.get(json.id); let ref = cachedGroups.get(json.id);
if (json.rules) { sanitizeEntityJson(json, ['rules', 'name', 'description']);
json.rules = replaceBioSymbols(json.rules);
}
if (json.name) {
json.name = replaceBioSymbols(json.name);
}
if (json.description) {
json.description = replaceBioSymbols(json.description);
}
if (typeof ref === 'undefined') { if (typeof ref === 'undefined') {
ref = { ref = createDefaultGroupRef(json);
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
};
cachedGroups.set(ref.id, ref); cachedGroups.set(ref.id, ref);
} else { } else {
if (currentUserGroups.has(ref.id)) { if (currentUserGroups.has(ref.id)) {
@@ -796,6 +786,10 @@ export const useGroupStore = defineStore('Group', () => {
return ref; return ref;
} }
/**
*
* @param args
*/
function handleGroupRepresented(args) { function handleGroupRepresented(args) {
const D = userStore.userDialog; const D = userStore.userDialog;
const json = args.json; const json = args.json;
@@ -819,6 +813,10 @@ export const useGroupStore = defineStore('Group', () => {
applyGroup(json); applyGroup(json);
} }
/**
*
* @param args
*/
function handleGroupList(args) { function handleGroupList(args) {
for (const json of args.json) { for (const json of args.json) {
json.$memberId = json.id; json.$memberId = json.id;
@@ -827,6 +825,10 @@ export const useGroupStore = defineStore('Group', () => {
} }
} }
/**
*
* @param args
*/
function handleGroupMemberProps(args) { function handleGroupMemberProps(args) {
if (args.userId === userStore.currentUser.id) { if (args.userId === userStore.currentUser.id) {
const json = args.json; const json = args.json;
@@ -873,6 +875,10 @@ export const useGroupStore = defineStore('Group', () => {
} }
} }
/**
*
* @param args
*/
function handleGroupPermissions(args) { function handleGroupPermissions(args) {
if (args.params.userId !== userStore.currentUser.id) { if (args.params.userId !== userStore.currentUser.id) {
return; return;
@@ -919,10 +925,18 @@ export const useGroupStore = defineStore('Group', () => {
updateGroupPostSearch(); updateGroupPostSearch();
} }
/**
*
* @param args
*/
function handleGroupMember(args) { function handleGroupMember(args) {
args.ref = applyGroupMember(args.json); args.ref = applyGroupMember(args.json);
} }
/**
*
* @param args
*/
async function handleGroupUserInstances(args) { async function handleGroupUserInstances(args) {
groupInstances.value = []; groupInstances.value = [];
for (const json of args.json.instances) { for (const json of args.json.instances) {
@@ -990,6 +1004,10 @@ export const useGroupStore = defineStore('Group', () => {
return json; return json;
} }
/**
*
* @param ref
*/
function applyGroupLanguage(ref) { function applyGroupLanguage(ref) {
ref.$languages = []; ref.$languages = [];
const { languages } = ref; const { languages } = ref;
@@ -1008,6 +1026,11 @@ export const useGroupStore = defineStore('Group', () => {
} }
} }
/**
*
* @param userId
* @param groups
*/
async function loadCurrentUserGroups(userId, groups) { async function loadCurrentUserGroups(userId, groups) {
const savedGroups = JSON.parse( const savedGroups = JSON.parse(
await configRepository.getString( await configRepository.getString(
@@ -1063,6 +1086,9 @@ export const useGroupStore = defineStore('Group', () => {
getCurrentUserGroups(); getCurrentUserGroups();
} }
/**
*
*/
async function getCurrentUserGroups() { async function getCurrentUserGroups() {
const args = await groupRequest.getGroups({ const args = await groupRequest.getGroups({
userId: userStore.currentUser.id userId: userStore.currentUser.id
@@ -1082,6 +1108,9 @@ export const useGroupStore = defineStore('Group', () => {
saveCurrentUserGroups(); saveCurrentUserGroups();
} }
/**
*
*/
function getCurrentUserRepresentedGroup() { function getCurrentUserRepresentedGroup() {
return groupRequest return groupRequest
.getRepresentedGroup({ .getRepresentedGroup({
@@ -1093,6 +1122,9 @@ export const useGroupStore = defineStore('Group', () => {
}); });
} }
/**
*
*/
async function initUserGroups() { async function initUserGroups() {
updateInGameGroupOrder(); updateInGameGroupOrder();
loadCurrentUserGroups( loadCurrentUserGroups(
@@ -1101,6 +1133,10 @@ export const useGroupStore = defineStore('Group', () => {
); );
} }
/**
*
* @param userId
*/
function showModerateGroupDialog(userId) { function showModerateGroupDialog(userId) {
const D = moderateGroupDialog.value; const D = moderateGroupDialog.value;
D.userId = userId; D.userId = userId;
@@ -1108,6 +1144,11 @@ export const useGroupStore = defineStore('Group', () => {
D.visible = true; D.visible = true;
} }
/**
*
* @param groupId
* @param userId
*/
function showGroupMemberModerationDialog(groupId, userId = '') { function showGroupMemberModerationDialog(groupId, userId = '') {
const D = groupMemberModeration.value; const D = groupMemberModeration.value;
D.id = groupId; D.id = groupId;
+93 -82
View File
@@ -8,8 +8,11 @@ import {
compareByDisplayName, compareByDisplayName,
compareById, compareById,
compareByLocationAt, compareByLocationAt,
computeDisabledContentSettings,
createDefaultInstanceRef,
debounce, debounce,
displayLocation, displayLocation,
evictMapCache,
getAvailablePlatforms, getAvailablePlatforms,
getBundleDateSize, getBundleDateSize,
getGroupName, getGroupName,
@@ -25,12 +28,12 @@ import {
userRequest, userRequest,
worldRequest worldRequest
} from '../api'; } from '../api';
import { patchInstanceFromEvent } from '../query';
import { import {
accessTypeLocaleKeyMap, accessTypeLocaleKeyMap,
instanceContentSettings instanceContentSettings
} from '../shared/constants'; } from '../shared/constants';
import { database } from '../service/database'; import { database } from '../service/database';
import { patchInstanceFromEvent } from '../query';
import { resolveRef } from '../shared/utils/resolveRef'; import { resolveRef } from '../shared/utils/resolveRef';
import { useAppearanceSettingsStore } from './settings/appearance'; import { useAppearanceSettingsStore } from './settings/appearance';
import { useFriendStore } from './friend'; import { useFriendStore } from './friend';
@@ -66,30 +69,26 @@ export const useInstanceStore = defineStore('Instance', () => {
let cachedInstances = new Map(); let cachedInstances = new Map();
/**
*
*/
function cleanInstanceCache() { function cleanInstanceCache() {
const maxSize = 200; const friendLocationTags = new Set(
if (cachedInstances.size <= maxSize) { [...friendStore.friends.values()]
return; .map((f) => f.$location?.tag)
} .filter(Boolean)
const removable = []; );
cachedInstances.forEach((ref, id) => { evictMapCache(
if ( cachedInstances,
[...friendStore.friends.values()].some( 200,
(f) => f.$location?.tag === id (_value, key) => friendLocationTags.has(key),
) {
) { sortFn: (a, b) =>
return; (Date.parse(a.value.$fetchedAt) || 0) -
(Date.parse(b.value.$fetchedAt) || 0),
logLabel: 'Instance cache cleanup'
} }
removable.push({ );
id,
fetchedAt: Date.parse(ref.$fetchedAt) || 0
});
});
removable.sort((a, b) => a.fetchedAt - b.fetchedAt);
const overBy = cachedInstances.size - maxSize;
for (let i = 0; i < overBy && i < removable.length; i++) {
cachedInstances.delete(removable[i].id);
}
} }
const lastInstanceApplied = ref(''); const lastInstanceApplied = ref('');
@@ -183,6 +182,9 @@ export const useInstanceStore = defineStore('Instance', () => {
{ flush: 'sync' } { flush: 'sync' }
); );
/**
*
*/
async function getInstanceJoinHistory() { async function getInstanceJoinHistory() {
try { try {
const data = await database.getInstanceJoinHistory(); const data = await database.getInstanceJoinHistory();
@@ -195,6 +197,11 @@ export const useInstanceStore = defineStore('Instance', () => {
} }
} }
/**
*
* @param location
* @param dateTime
*/
function addInstanceJoinHistory(location, dateTime) { function addInstanceJoinHistory(location, dateTime) {
if (!location || !dateTime) { if (!location || !dateTime) {
return; return;
@@ -208,11 +215,18 @@ export const useInstanceStore = defineStore('Instance', () => {
instanceJoinHistory.set(location, epoch); instanceJoinHistory.set(location, epoch);
} }
/**
*
*/
function hidePreviousInstancesDialogs() { function hidePreviousInstancesDialogs() {
previousInstancesInfoDialog.value.visible = false; previousInstancesInfoDialog.value.visible = false;
previousInstancesListDialog.value.visible = false; previousInstancesListDialog.value.visible = false;
} }
/**
*
* @param input
*/
function resolveUserRef(input) { function resolveUserRef(input) {
return resolveRef(input, { return resolveRef(input, {
emptyDefault: { id: '', displayName: '' }, emptyDefault: { id: '', displayName: '' },
@@ -222,6 +236,10 @@ export const useInstanceStore = defineStore('Instance', () => {
}); });
} }
/**
*
* @param input
*/
function resolveWorldRef(input) { function resolveWorldRef(input) {
return resolveRef(input, { return resolveRef(input, {
emptyDefault: { id: '', name: '' }, emptyDefault: { id: '', name: '' },
@@ -231,6 +249,10 @@ export const useInstanceStore = defineStore('Instance', () => {
}); });
} }
/**
*
* @param input
*/
function resolveGroupRef(input) { function resolveGroupRef(input) {
return resolveRef(input, { return resolveRef(input, {
emptyDefault: { id: '', name: '' }, emptyDefault: { id: '', name: '' },
@@ -240,6 +262,10 @@ export const useInstanceStore = defineStore('Instance', () => {
}); });
} }
/**
*
* @param accessTypeNameRaw
*/
function translateAccessType(accessTypeNameRaw) { function translateAccessType(accessTypeNameRaw) {
const key = accessTypeLocaleKeyMap[accessTypeNameRaw]; const key = accessTypeLocaleKeyMap[accessTypeNameRaw];
if (!key) { if (!key) {
@@ -255,6 +281,11 @@ export const useInstanceStore = defineStore('Instance', () => {
return t(key); return t(key);
} }
/**
*
* @param instanceId
* @param worldNameOverride
*/
function formatPreviousInstancesInfoLabel( function formatPreviousInstancesInfoLabel(
instanceId, instanceId,
worldNameOverride = '' worldNameOverride = ''
@@ -275,6 +306,10 @@ export const useInstanceStore = defineStore('Instance', () => {
return `${baseLabel} · ${accessTypeLabel}`; return `${baseLabel} · ${accessTypeLabel}`;
} }
/**
*
* @param instanceId
*/
function showPreviousInstancesInfoDialog(instanceId) { function showPreviousInstancesInfoDialog(instanceId) {
previousInstancesInfoDialog.value.visible = true; previousInstancesInfoDialog.value.visible = true;
previousInstancesInfoDialog.value.instanceId = instanceId; previousInstancesInfoDialog.value.instanceId = instanceId;
@@ -308,6 +343,11 @@ export const useInstanceStore = defineStore('Instance', () => {
} }
} }
/**
*
* @param variant
* @param targetRef
*/
async function showPreviousInstancesListDialog(variant, targetRef) { async function showPreviousInstancesListDialog(variant, targetRef) {
previousInstancesListDialog.value.variant = variant; previousInstancesListDialog.value.variant = variant;
let resolved = null; let resolved = null;
@@ -335,6 +375,9 @@ export const useInstanceStore = defineStore('Instance', () => {
}); });
} }
/**
*
*/
function updateCurrentInstanceWorld() { function updateCurrentInstanceWorld() {
let L; let L;
let instanceId = locationStore.lastLocation.location; let instanceId = locationStore.lastLocation.location;
@@ -472,53 +515,7 @@ export const useInstanceStore = defineStore('Instance', () => {
} }
let ref = cachedInstances.get(json.id); let ref = cachedInstances.get(json.id);
if (typeof ref === 'undefined') { if (typeof ref === 'undefined') {
ref = { ref = createDefaultInstanceRef(json);
id: '',
location: '',
instanceId: '',
name: '',
worldId: '',
type: '',
ownerId: '',
tags: [],
active: false,
full: false,
n_users: 0,
hasCapacityForYou: true, // not present depending on endpoint
capacity: 0,
recommendedCapacity: 0,
userCount: 0,
queueEnabled: false, // only present with group instance type
queueSize: 0, // only present when queuing is enabled
platforms: {},
gameServerVersion: 0,
hardClose: null, // boolean or null
closedAt: null, // string or null
secureName: '',
shortName: '',
world: {},
users: [], // only present when you're the owner
clientNumber: '',
contentSettings: {},
photonRegion: '',
region: '',
canRequestInvite: false,
permanent: false,
private: '', // part of instance tag
hidden: '', // part of instance tag
nonce: '', // only present when you're the owner
strict: false, // deprecated
displayName: null,
groupAccessType: null, // only present with group instance type
roleRestricted: false, // only present with group instance type
instancePersistenceEnabled: null,
playerPersistenceEnabled: null,
ageGate: null,
// VRCX
$fetchedAt: '',
$disabledContentSettings: [],
...json
};
cachedInstances.set(ref.id, ref); cachedInstances.set(ref.id, ref);
cleanInstanceCache(); cleanInstanceCache();
} else { } else {
@@ -535,18 +532,10 @@ export const useInstanceStore = defineStore('Instance', () => {
return args; return args;
}); });
} }
ref.$disabledContentSettings = []; ref.$disabledContentSettings = computeDisabledContentSettings(
if (json.contentSettings && Object.keys(json.contentSettings).length) { json.contentSettings,
for (const setting of instanceContentSettings) { instanceContentSettings
if ( );
typeof json.contentSettings[setting] === 'undefined' ||
json.contentSettings[setting] === true
) {
continue;
}
ref.$disabledContentSettings.push(setting);
}
}
if (ref.displayName) { if (ref.displayName) {
ref.displayName = replaceBioSymbols(ref.displayName); ref.displayName = replaceBioSymbols(ref.displayName);
} }
@@ -578,6 +567,10 @@ export const useInstanceStore = defineStore('Instance', () => {
return ref; return ref;
} }
/**
*
* @param location
*/
async function getInstanceName(location) { async function getInstanceName(location) {
let instanceName = ''; let instanceName = '';
@@ -701,10 +694,16 @@ export const useInstanceStore = defineStore('Instance', () => {
} }
} }
/**
*
*/
function applyWorldDialogInstances() { function applyWorldDialogInstances() {
debounce(applyWorldDialogInstancesDebounced, 100)(); debounce(applyWorldDialogInstancesDebounced, 100)();
} }
/**
*
*/
function applyWorldDialogInstancesDebounced() { function applyWorldDialogInstancesDebounced() {
let ref; let ref;
let instance; let instance;
@@ -1079,6 +1078,9 @@ export const useInstanceStore = defineStore('Instance', () => {
D.instances = rooms; D.instances = rooms;
} }
/**
*
*/
function removeAllQueuedInstances() { function removeAllQueuedInstances() {
queuedInstances.forEach((ref) => { queuedInstances.forEach((ref) => {
toast.info(`Removed instance ${ref.$worldName} from queue`); toast.info(`Removed instance ${ref.$worldName} from queue`);
@@ -1233,6 +1235,9 @@ export const useInstanceStore = defineStore('Instance', () => {
// workerTimers.setTimeout(this.instanceQueueTimeout, 3600000); // workerTimers.setTimeout(this.instanceQueueTimeout, 3600000);
} }
/**
*
*/
function getCurrentInstanceUserList() { function getCurrentInstanceUserList() {
if (!watchState.isFriendsLoaded) { if (!watchState.isFriendsLoaded) {
return; return;
@@ -1250,6 +1255,9 @@ export const useInstanceStore = defineStore('Instance', () => {
} }
} }
/**
*
*/
function updatePlayerListExecute() { function updatePlayerListExecute() {
try { try {
updatePlayerListDebounce(); updatePlayerListDebounce();
@@ -1260,6 +1268,9 @@ export const useInstanceStore = defineStore('Instance', () => {
state.updatePlayerListPending = false; state.updatePlayerListPending = false;
} }
/**
*
*/
function updatePlayerListDebounce() { function updatePlayerListDebounce() {
const users = []; const users = [];
const pushUser = function (ref) { const pushUser = function (ref) {
+125 -84
View File
@@ -7,14 +7,18 @@ import Noty from 'noty';
import dayjs from 'dayjs'; import dayjs from 'dayjs';
import { import {
applyBoopLegacyHandling,
checkCanInvite, checkCanInvite,
createDefaultNotificationRef,
createDefaultNotificationV2Ref,
escapeTag, escapeTag,
executeWithBackoff, executeWithBackoff,
findUserByDisplayName, findUserByDisplayName,
getUserMemo, getUserMemo,
parseLocation, parseLocation,
parseNotificationDetails,
removeFromArray, removeFromArray,
replaceBioSymbols sanitizeNotificationJson
} from '../../shared/utils'; } from '../../shared/utils';
import { import {
friendRequest, friendRequest,
@@ -163,6 +167,9 @@ export const useNotificationStore = defineStore('Notification', () => {
{ flush: 'sync' } { flush: 'sync' }
); );
/**
*
*/
async function init() { async function init() {
notificationTable.value.filters[0].value = JSON.parse( notificationTable.value.filters[0].value = JSON.parse(
await configRepository.getString( await configRepository.getString(
@@ -174,6 +181,10 @@ export const useNotificationStore = defineStore('Notification', () => {
init(); init();
/**
*
* @param args
*/
function handleNotification(args) { function handleNotification(args) {
args.ref = applyNotification(args.json); args.ref = applyNotification(args.json);
const { ref } = args; const { ref } = args;
@@ -234,6 +245,10 @@ export const useNotificationStore = defineStore('Notification', () => {
D.incomingRequest = true; D.incomingRequest = true;
} }
/**
*
* @param notificationId
*/
function handleNotificationHide(notificationId) { function handleNotificationHide(notificationId) {
const ref = notificationTable.value.data.find( const ref = notificationTable.value.data.find(
(n) => n.id === notificationId (n) => n.id === notificationId
@@ -259,6 +274,10 @@ export const useNotificationStore = defineStore('Notification', () => {
}); });
} }
/**
*
* @param args
*/
function handlePipelineNotification(args) { function handlePipelineNotification(args) {
const ref = args.json; const ref = args.json;
if ( if (
@@ -373,6 +392,10 @@ export const useNotificationStore = defineStore('Notification', () => {
}); });
} }
/**
*
* @param notificationId
*/
function handleNotificationSee(notificationId) { function handleNotificationSee(notificationId) {
removeFromArray(unseenNotifications.value, notificationId); removeFromArray(unseenNotifications.value, notificationId);
if (unseenNotifications.value.length === 0) { if (unseenNotifications.value.length === 0) {
@@ -392,6 +415,9 @@ export const useNotificationStore = defineStore('Notification', () => {
let seeProcessing = false; let seeProcessing = false;
const SEE_CONCURRENCY = 2; const SEE_CONCURRENCY = 2;
/**
*
*/
async function processSeeQueue() { async function processSeeQueue() {
if (seeProcessing) return; if (seeProcessing) return;
seeProcessing = true; seeProcessing = true;
@@ -443,7 +469,7 @@ export const useNotificationStore = defineStore('Notification', () => {
/** /**
* Queue a notification to be marked as seen. * Queue a notification to be marked as seen.
* @param {string} notificationId * @param {string} notificationId
* @param {number} [version=1] * @param {number} [version]
*/ */
function queueMarkAsSeen(notificationId, version = 1) { function queueMarkAsSeen(notificationId, version = 1) {
if (seenIds.has(notificationId)) return; if (seenIds.has(notificationId)) return;
@@ -452,6 +478,10 @@ export const useNotificationStore = defineStore('Notification', () => {
processSeeQueue(); processSeeQueue();
} }
/**
*
* @param args
*/
function handleNotificationAccept(args) { function handleNotificationAccept(args) {
let ref; let ref;
const array = notificationTable.value.data; const array = notificationTable.value.data;
@@ -490,6 +520,10 @@ export const useNotificationStore = defineStore('Notification', () => {
D.isFriend = true; D.isFriend = true;
} }
/**
*
* @param args
*/
function handleNotificationExpire(args) { function handleNotificationExpire(args) {
const { ref } = args; const { ref } = args;
const D = userStore.userDialog; const D = userStore.userDialog;
@@ -509,10 +543,7 @@ export const useNotificationStore = defineStore('Notification', () => {
* @returns {object} * @returns {object}
*/ */
function applyNotification(data) { function applyNotification(data) {
const json = { ...data }; const json = sanitizeNotificationJson({ ...data });
if (json.message) {
json.message = replaceBioSymbols(json.message);
}
let ref; let ref;
const array = notificationTable.value.data; const array = notificationTable.value.data;
for (let i = array.length - 1; i >= 0; i--) { for (let i = array.length - 1; i >= 0; i--) {
@@ -521,102 +552,37 @@ export const useNotificationStore = defineStore('Notification', () => {
break; break;
} }
} }
// delete any null in json
for (const key in json) {
if (json[key] === null) {
delete json[key];
}
}
if (typeof ref === 'undefined') { if (typeof ref === 'undefined') {
ref = { ref = createDefaultNotificationRef(json);
id: '',
senderUserId: '',
senderUsername: '',
type: '',
message: '',
details: {},
seen: false,
created_at: '',
// VRCX
$isExpired: false,
//
...json
};
} else { } else {
Object.assign(ref, json); Object.assign(ref, json);
ref.$isExpired = false; ref.$isExpired = false;
} }
if (ref.details !== Object(ref.details)) { ref.details = parseNotificationDetails(ref.details);
let details = {};
if (ref.details !== '{}') {
try {
const object = JSON.parse(ref.details);
if (object === Object(object)) {
details = object;
}
} catch (err) {
console.log(err);
}
}
ref.details = details;
}
return ref; return ref;
} }
/**
*
* @param data
*/
function applyNotificationV2(data) { function applyNotificationV2(data) {
const json = { ...data }; const json = sanitizeNotificationJson({ ...data });
// delete any null in json
for (const key in json) {
if (json[key] === null || typeof json[key] === 'undefined') {
delete json[key];
}
}
if (json.message) {
json.message = replaceBioSymbols(json.message);
}
if (json.title) {
json.title = replaceBioSymbols(json.title);
}
let ref = notificationTable.value.data.find((n) => n.id === json.id); let ref = notificationTable.value.data.find((n) => n.id === json.id);
if (typeof ref === 'undefined') { if (typeof ref === 'undefined') {
ref = { ref = createDefaultNotificationV2Ref(json);
id: '',
createdAt: '',
updatedAt: '',
expiresAt: '',
type: '',
link: '',
linkText: '',
message: '',
title: '',
imageUrl: '',
seen: false,
senderUserId: '',
senderUsername: '',
data: {},
responses: [],
details: {},
version: 2,
...json
};
} else { } else {
Object.assign(ref, json); Object.assign(ref, json);
} }
ref.created_at = ref.createdAt; // for table ref.created_at = ref.createdAt; // for table
// legacy handling of boops applyBoopLegacyHandling(ref, AppDebug.endpointDomain);
if (ref.type === 'boop' && ref.title) {
ref.message = ref.title;
ref.title = '';
if (ref.details?.emojiId?.startsWith('default_')) {
ref.imageUrl = ref.details.emojiId;
ref.message += ` ${ref.details.emojiId.replace('default_', '')}`;
} else {
ref.imageUrl = `${AppDebug.endpointDomain}/file/${ref.details.emojiId}/${ref.details.emojiVersion}`;
}
}
return ref; return ref;
} }
/**
*
* @param args
*/
function handleNotificationV2(args) { function handleNotificationV2(args) {
const ref = applyNotificationV2(args.json); const ref = applyNotificationV2(args.json);
if (ref.seen) { if (ref.seen) {
@@ -645,6 +611,10 @@ export const useNotificationStore = defineStore('Notification', () => {
sharedFeedStore.addEntry(ref); sharedFeedStore.addEntry(ref);
} }
/**
*
* @param args
*/
function handleNotificationV2Update(args) { function handleNotificationV2Update(args) {
const notificationId = args.params.notificationId; const notificationId = args.params.notificationId;
const json = { ...args.json }; const json = { ...args.json };
@@ -663,6 +633,10 @@ export const useNotificationStore = defineStore('Notification', () => {
} }
} }
/**
*
* @param notificationId
*/
function handleNotificationV2Hide(notificationId) { function handleNotificationV2Hide(notificationId) {
database.expireNotificationV2(notificationId); database.expireNotificationV2(notificationId);
const ref = notificationTable.value.data.find( const ref = notificationTable.value.data.find(
@@ -674,6 +648,9 @@ export const useNotificationStore = defineStore('Notification', () => {
} }
} }
/**
*
*/
function expireFriendRequestNotifications() { function expireFriendRequestNotifications() {
const array = notificationTable.value.data; const array = notificationTable.value.data;
for (let i = array.length - 1; i >= 0; i--) { for (let i = array.length - 1; i >= 0; i--) {
@@ -813,6 +790,10 @@ export const useNotificationStore = defineStore('Notification', () => {
} }
} }
/**
*
* @param noty
*/
function playNoty(noty) { function playNoty(noty) {
if ( if (
userStore.currentUser.status === 'busy' || userStore.currentUser.status === 'busy' ||
@@ -1014,6 +995,10 @@ export const useNotificationStore = defineStore('Notification', () => {
return ''; return '';
} }
/**
*
* @param gamelog
*/
function queueGameLogNoty(gamelog) { function queueGameLogNoty(gamelog) {
const noty = structuredClone(gamelog); const noty = structuredClone(gamelog);
let bias; let bias;
@@ -1090,6 +1075,10 @@ export const useNotificationStore = defineStore('Notification', () => {
} }
} }
/**
*
* @param feed
*/
function queueFeedNoty(feed) { function queueFeedNoty(feed) {
const noty = { ...feed }; const noty = { ...feed };
if (noty.type === 'Avatar') { if (noty.type === 'Avatar') {
@@ -1116,6 +1105,10 @@ export const useNotificationStore = defineStore('Notification', () => {
} }
} }
/**
*
* @param noty
*/
function queueFriendLogNoty(noty) { function queueFriendLogNoty(noty) {
if (noty.type === 'FriendRequest') { if (noty.type === 'FriendRequest') {
return; return;
@@ -1133,6 +1126,10 @@ export const useNotificationStore = defineStore('Notification', () => {
} }
} }
/**
*
* @param noty
*/
function queueModerationNoty(noty) { function queueModerationNoty(noty) {
noty.isFriend = false; noty.isFriend = false;
noty.isFavorite = false; noty.isFavorite = false;
@@ -1146,6 +1143,9 @@ export const useNotificationStore = defineStore('Notification', () => {
} }
} }
/**
*
*/
async function initNotifications() { async function initNotifications() {
notificationInitStatus.value = false; notificationInitStatus.value = false;
let tableData = await database.getNotificationsV2(); let tableData = await database.getNotificationsV2();
@@ -1161,6 +1161,9 @@ export const useNotificationStore = defineStore('Notification', () => {
refreshNotifications(); refreshNotifications();
} }
/**
*
*/
function testNotification() { function testNotification() {
playNoty({ playNoty({
type: 'Event', type: 'Event',
@@ -1169,6 +1172,10 @@ export const useNotificationStore = defineStore('Notification', () => {
}); });
} }
/**
*
* @param row
*/
function acceptFriendRequestNotification(row) { function acceptFriendRequestNotification(row) {
modalStore modalStore
.confirm({ .confirm({
@@ -1193,6 +1200,10 @@ export const useNotificationStore = defineStore('Notification', () => {
.catch(() => {}); .catch(() => {});
} }
/**
*
* @param row
*/
async function hideNotification(row) { async function hideNotification(row) {
if (row.type === 'ignoredFriendRequest') { if (row.type === 'ignoredFriendRequest') {
await friendRequest.deleteHiddenFriendRequest( await friendRequest.deleteHiddenFriendRequest(
@@ -1211,6 +1222,10 @@ export const useNotificationStore = defineStore('Notification', () => {
} }
} }
/**
*
* @param row
*/
function hideNotificationPrompt(row) { function hideNotificationPrompt(row) {
modalStore modalStore
.confirm({ .confirm({
@@ -1223,6 +1238,10 @@ export const useNotificationStore = defineStore('Notification', () => {
.catch(() => {}); .catch(() => {});
} }
/**
*
* @param row
*/
function acceptRequestInvite(row) { function acceptRequestInvite(row) {
modalStore modalStore
.confirm({ .confirm({
@@ -1268,6 +1287,12 @@ export const useNotificationStore = defineStore('Notification', () => {
.catch(() => {}); .catch(() => {});
} }
/**
*
* @param notificationId
* @param responses
* @param responseType
*/
function sendNotificationResponse(notificationId, responses, responseType) { function sendNotificationResponse(notificationId, responses, responseType) {
if (!Array.isArray(responses) || responses.length === 0) return; if (!Array.isArray(responses) || responses.length === 0) return;
let responseData = ''; let responseData = '';
@@ -1295,6 +1320,10 @@ export const useNotificationStore = defineStore('Notification', () => {
}); });
} }
/**
*
* @param row
*/
function deleteNotificationLog(row) { function deleteNotificationLog(row) {
const idx = notificationTable.value.data.findIndex( const idx = notificationTable.value.data.findIndex(
(e) => e.id === row.id (e) => e.id === row.id
@@ -1314,6 +1343,10 @@ export const useNotificationStore = defineStore('Notification', () => {
} }
} }
/**
*
* @param row
*/
function deleteNotificationLogPrompt(row) { function deleteNotificationLogPrompt(row) {
modalStore modalStore
.confirm({ .confirm({
@@ -1327,6 +1360,10 @@ export const useNotificationStore = defineStore('Notification', () => {
.catch(() => {}); .catch(() => {});
} }
/**
*
* @param notification
*/
function isNotificationExpired(notification) { function isNotificationExpired(notification) {
if (notification.$isExpired !== undefined) { if (notification.$isExpired !== undefined) {
return notification.$isExpired; return notification.$isExpired;
@@ -1338,6 +1375,10 @@ export const useNotificationStore = defineStore('Notification', () => {
return expiresAt.isValid() && dayjs().isSameOrAfter(expiresAt); return expiresAt.isValid() && dayjs().isSameOrAfter(expiresAt);
} }
/**
*
* @param link
*/
function openNotificationLink(link) { function openNotificationLink(link) {
if (!link) { if (!link) {
return; return;
+133 -51
View File
@@ -20,8 +20,8 @@ import {
getThemeMode, getThemeMode,
updateTrustColorClasses updateTrustColorClasses
} from '../../shared/utils/base/ui'; } from '../../shared/utils/base/ui';
import { computeTrustLevel, getNameColour } from '../../shared/utils';
import { database } from '../../service/database'; import { database } from '../../service/database';
import { getNameColour } from '../../shared/utils';
import { languageCodes } from '../../localization'; import { languageCodes } from '../../localization';
import { loadLocalizedStrings } from '../../plugin'; import { loadLocalizedStrings } from '../../plugin';
import { useFeedStore } from '../feed'; import { useFeedStore } from '../feed';
@@ -127,6 +127,9 @@ export const useAppearanceSettingsStore = defineStore(
: fallback; : fallback;
}; };
/**
*
*/
async function initAppearanceSettings() { async function initAppearanceSettings() {
const { initThemeMode, isDarkMode: initDarkMode } = const { initThemeMode, isDarkMode: initDarkMode } =
await getThemeMode(configRepository); await getThemeMode(configRepository);
@@ -410,6 +413,10 @@ export const useAppearanceSettingsStore = defineStore(
updateTrustColorClasses(trustColor.value); updateTrustColorClasses(trustColor.value);
} }
/**
*
* @param customFunc
*/
async function userColourInit(customFunc) { async function userColourInit(customFunc) {
let dictObject = null; let dictObject = null;
if (typeof customFunc === 'function') { if (typeof customFunc === 'function') {
@@ -440,55 +447,13 @@ export const useAppearanceSettingsStore = defineStore(
* @param {object} ref * @param {object} ref
*/ */
function applyUserTrustLevel(ref) { function applyUserTrustLevel(ref) {
ref.$isModerator = const trust = computeTrustLevel(ref.tags, ref.developerType);
ref.developerType && ref.developerType !== 'none'; ref.$isModerator = trust.isModerator;
ref.$isTroll = false; ref.$isTroll = trust.isTroll;
ref.$isProbableTroll = false; ref.$isProbableTroll = trust.isProbableTroll;
let trustColorTemp = ''; ref.$trustLevel = trust.trustLevel;
const { tags } = ref; ref.$trustClass = trust.trustClass;
if (tags.includes('admin_moderator')) { ref.$trustSortNum = trust.trustSortNum;
ref.$isModerator = true;
}
if (tags.includes('system_troll')) {
ref.$isTroll = true;
}
if (tags.includes('system_probable_troll') && !ref.$isTroll) {
ref.$isProbableTroll = true;
}
if (tags.includes('system_trust_veteran')) {
ref.$trustLevel = 'Trusted User';
ref.$trustClass = 'x-tag-veteran';
trustColorTemp = 'veteran';
ref.$trustSortNum = 5;
} else if (tags.includes('system_trust_trusted')) {
ref.$trustLevel = 'Known User';
ref.$trustClass = 'x-tag-trusted';
trustColorTemp = 'trusted';
ref.$trustSortNum = 4;
} else if (tags.includes('system_trust_known')) {
ref.$trustLevel = 'User';
ref.$trustClass = 'x-tag-known';
trustColorTemp = 'known';
ref.$trustSortNum = 3;
} else if (tags.includes('system_trust_basic')) {
ref.$trustLevel = 'New User';
ref.$trustClass = 'x-tag-basic';
trustColorTemp = 'basic';
ref.$trustSortNum = 2;
} else {
ref.$trustLevel = 'Visitor';
ref.$trustClass = 'x-tag-untrusted';
trustColorTemp = 'untrusted';
ref.$trustSortNum = 1;
}
if (ref.$isTroll || ref.$isProbableTroll) {
trustColorTemp = 'troll';
ref.$trustSortNum += 0.1;
}
if (ref.$isModerator) {
trustColorTemp = 'vip';
ref.$trustSortNum += 0.3;
}
if (randomUserColours.value && watchState.isFriendsLoaded) { if (randomUserColours.value && watchState.isFriendsLoaded) {
if (!ref.$userColour) { if (!ref.$userColour) {
getNameColour(ref.id).then((colour) => { getNameColour(ref.id).then((colour) => {
@@ -496,7 +461,7 @@ export const useAppearanceSettingsStore = defineStore(
}); });
} }
} else { } else {
ref.$userColour = trustColor.value[trustColorTemp]; ref.$userColour = trustColor.value[trust.trustColorKey];
} }
} }
@@ -525,6 +490,9 @@ export const useAppearanceSettingsStore = defineStore(
updateTrustColor(undefined, undefined); updateTrustColor(undefined, undefined);
} }
/**
*
*/
function toggleThemeMode() { function toggleThemeMode() {
const nextMode = isDarkMode.value const nextMode = isDarkMode.value
? 'light' ? 'light'
@@ -532,12 +500,20 @@ export const useAppearanceSettingsStore = defineStore(
setThemeMode(nextMode); setThemeMode(nextMode);
} }
/**
*
* @param value
*/
function normalizeAppFontFamily(value) { function normalizeAppFontFamily(value) {
return APP_FONT_FAMILIES.includes(value) return APP_FONT_FAMILIES.includes(value)
? value ? value
: APP_FONT_DEFAULT_KEY; : APP_FONT_DEFAULT_KEY;
} }
/**
*
* @param value
*/
function setAppFontFamily(value) { function setAppFontFamily(value) {
const normalized = normalizeAppFontFamily(value); const normalized = normalizeAppFontFamily(value);
appFontFamily.value = normalized; appFontFamily.value = normalized;
@@ -545,6 +521,9 @@ export const useAppearanceSettingsStore = defineStore(
applyAppFontFamily(normalized); applyAppFontFamily(normalized);
} }
/**
*
*/
function setDisplayVRCPlusIconsAsAvatar() { function setDisplayVRCPlusIconsAsAvatar() {
displayVRCPlusIconsAsAvatar.value = displayVRCPlusIconsAsAvatar.value =
!displayVRCPlusIconsAsAvatar.value; !displayVRCPlusIconsAsAvatar.value;
@@ -553,6 +532,9 @@ export const useAppearanceSettingsStore = defineStore(
displayVRCPlusIconsAsAvatar.value displayVRCPlusIconsAsAvatar.value
); );
} }
/**
*
*/
function setNotificationIconDot() { function setNotificationIconDot() {
notificationIconDot.value = !notificationIconDot.value; notificationIconDot.value = !notificationIconDot.value;
configRepository.setBool( configRepository.setBool(
@@ -561,10 +543,16 @@ export const useAppearanceSettingsStore = defineStore(
); );
uiStore.updateTrayIconNotify(); uiStore.updateTrayIconNotify();
} }
/**
*
*/
function setHideNicknames() { function setHideNicknames() {
hideNicknames.value = !hideNicknames.value; hideNicknames.value = !hideNicknames.value;
configRepository.setBool('VRCX_hideNicknames', hideNicknames.value); configRepository.setBool('VRCX_hideNicknames', hideNicknames.value);
} }
/**
*
*/
function setShowInstanceIdInLocation() { function setShowInstanceIdInLocation() {
showInstanceIdInLocation.value = !showInstanceIdInLocation.value; showInstanceIdInLocation.value = !showInstanceIdInLocation.value;
configRepository.setBool( configRepository.setBool(
@@ -572,6 +560,9 @@ export const useAppearanceSettingsStore = defineStore(
showInstanceIdInLocation.value showInstanceIdInLocation.value
); );
} }
/**
*
*/
function setIsAgeGatedInstancesVisible() { function setIsAgeGatedInstancesVisible() {
isAgeGatedInstancesVisible.value = isAgeGatedInstancesVisible.value =
!isAgeGatedInstancesVisible.value; !isAgeGatedInstancesVisible.value;
@@ -580,10 +571,16 @@ export const useAppearanceSettingsStore = defineStore(
isAgeGatedInstancesVisible.value isAgeGatedInstancesVisible.value
); );
} }
/**
*
*/
function setSortFavorites() { function setSortFavorites() {
sortFavorites.value = !sortFavorites.value; sortFavorites.value = !sortFavorites.value;
configRepository.setBool('VRCX_sortFavorites', sortFavorites.value); configRepository.setBool('VRCX_sortFavorites', sortFavorites.value);
} }
/**
*
*/
function setInstanceUsersSortAlphabetical() { function setInstanceUsersSortAlphabetical() {
instanceUsersSortAlphabetical.value = instanceUsersSortAlphabetical.value =
!instanceUsersSortAlphabetical.value; !instanceUsersSortAlphabetical.value;
@@ -593,6 +590,10 @@ export const useAppearanceSettingsStore = defineStore(
); );
} }
/**
*
* @param size
*/
function setTablePageSize(size) { function setTablePageSize(size) {
const processedSize = clampInt(size, 1, MAX_TABLE_PAGE_SIZE); const processedSize = clampInt(size, 1, MAX_TABLE_PAGE_SIZE);
tablePageSize.value = processedSize; tablePageSize.value = processedSize;
@@ -601,6 +602,10 @@ export const useAppearanceSettingsStore = defineStore(
return processedSize; return processedSize;
} }
/**
*
* @param input
*/
function normalizeTablePageSizes(input) { function normalizeTablePageSizes(input) {
const values = ( const values = (
Array.isArray(input) ? input : DEFAULT_TABLE_PAGE_SIZES Array.isArray(input) ? input : DEFAULT_TABLE_PAGE_SIZES
@@ -629,10 +634,16 @@ export const useAppearanceSettingsStore = defineStore(
setTablePageSize(tablePageSizes.value[0]); setTablePageSize(tablePageSizes.value[0]);
} }
} }
/**
*
*/
function setDtHour12() { function setDtHour12() {
dtHour12.value = !dtHour12.value; dtHour12.value = !dtHour12.value;
configRepository.setBool('VRCX_dtHour12', dtHour12.value); configRepository.setBool('VRCX_dtHour12', dtHour12.value);
} }
/**
*
*/
function setDtIsoFormat() { function setDtIsoFormat() {
dtIsoFormat.value = !dtIsoFormat.value; dtIsoFormat.value = !dtIsoFormat.value;
configRepository.setBool('VRCX_dtIsoFormat', dtIsoFormat.value); configRepository.setBool('VRCX_dtIsoFormat', dtIsoFormat.value);
@@ -668,13 +679,24 @@ export const useAppearanceSettingsStore = defineStore(
JSON.stringify(methods) JSON.stringify(methods)
); );
} }
/**
*
* @param collapsed
*/
function setNavCollapsed(collapsed) { function setNavCollapsed(collapsed) {
isNavCollapsed.value = collapsed; isNavCollapsed.value = collapsed;
configRepository.setBool('VRCX_navIsCollapsed', collapsed); configRepository.setBool('VRCX_navIsCollapsed', collapsed);
} }
/**
*
*/
function toggleNavCollapsed() { function toggleNavCollapsed() {
setNavCollapsed(!isNavCollapsed.value); setNavCollapsed(!isNavCollapsed.value);
} }
/**
*
* @param widthOrArray
*/
function setNavWidth(widthOrArray) { function setNavWidth(widthOrArray) {
let width = null; let width = null;
if (Array.isArray(widthOrArray) && widthOrArray.length) { if (Array.isArray(widthOrArray) && widthOrArray.length) {
@@ -692,6 +714,9 @@ export const useAppearanceSettingsStore = defineStore(
}); });
} }
} }
/**
*
*/
function setIsSidebarGroupByInstance() { function setIsSidebarGroupByInstance() {
isSidebarGroupByInstance.value = !isSidebarGroupByInstance.value; isSidebarGroupByInstance.value = !isSidebarGroupByInstance.value;
configRepository.setBool( configRepository.setBool(
@@ -699,6 +724,9 @@ export const useAppearanceSettingsStore = defineStore(
isSidebarGroupByInstance.value isSidebarGroupByInstance.value
); );
} }
/**
*
*/
function setIsHideFriendsInSameInstance() { function setIsHideFriendsInSameInstance() {
isHideFriendsInSameInstance.value = isHideFriendsInSameInstance.value =
!isHideFriendsInSameInstance.value; !isHideFriendsInSameInstance.value;
@@ -707,6 +735,9 @@ export const useAppearanceSettingsStore = defineStore(
isHideFriendsInSameInstance.value isHideFriendsInSameInstance.value
); );
} }
/**
*
*/
function setIsSidebarDivideByFriendGroup() { function setIsSidebarDivideByFriendGroup() {
isSidebarDivideByFriendGroup.value = isSidebarDivideByFriendGroup.value =
!isSidebarDivideByFriendGroup.value; !isSidebarDivideByFriendGroup.value;
@@ -735,18 +766,30 @@ export const useAppearanceSettingsStore = defineStore(
JSON.stringify(value) JSON.stringify(value)
); );
} }
/**
*
*/
function setHideUserNotes() { function setHideUserNotes() {
hideUserNotes.value = !hideUserNotes.value; hideUserNotes.value = !hideUserNotes.value;
configRepository.setBool('VRCX_hideUserNotes', hideUserNotes.value); configRepository.setBool('VRCX_hideUserNotes', hideUserNotes.value);
} }
/**
*
*/
function setHideUserMemos() { function setHideUserMemos() {
hideUserMemos.value = !hideUserMemos.value; hideUserMemos.value = !hideUserMemos.value;
configRepository.setBool('VRCX_hideUserMemos', hideUserMemos.value); configRepository.setBool('VRCX_hideUserMemos', hideUserMemos.value);
} }
/**
*
*/
function setHideUnfriends() { function setHideUnfriends() {
hideUnfriends.value = !hideUnfriends.value; hideUnfriends.value = !hideUnfriends.value;
configRepository.setBool('VRCX_hideUnfriends', hideUnfriends.value); configRepository.setBool('VRCX_hideUnfriends', hideUnfriends.value);
} }
/**
*
*/
function setRandomUserColours() { function setRandomUserColours() {
randomUserColours.value = !randomUserColours.value; randomUserColours.value = !randomUserColours.value;
configRepository.setBool( configRepository.setBool(
@@ -754,6 +797,10 @@ export const useAppearanceSettingsStore = defineStore(
randomUserColours.value randomUserColours.value
); );
} }
/**
*
* @param value
*/
function normalizeTableDensity(value) { function normalizeTableDensity(value) {
if ( if (
value === 'compact' || value === 'compact' ||
@@ -765,6 +812,10 @@ export const useAppearanceSettingsStore = defineStore(
return 'standard'; return 'standard';
} }
/**
*
* @param density
*/
function setTableDensity(density) { function setTableDensity(density) {
const normalized = normalizeTableDensity(density); const normalized = normalizeTableDensity(density);
tableDensity.value = normalized; tableDensity.value = normalized;
@@ -772,6 +823,9 @@ export const useAppearanceSettingsStore = defineStore(
configRepository.setString('VRCX_tableDensity', tableDensity.value); configRepository.setString('VRCX_tableDensity', tableDensity.value);
} }
/**
*
*/
function toggleStripedDataTable() { function toggleStripedDataTable() {
isDataTableStriped.value = !isDataTableStriped.value; isDataTableStriped.value = !isDataTableStriped.value;
configRepository.setBool( configRepository.setBool(
@@ -781,6 +835,9 @@ export const useAppearanceSettingsStore = defineStore(
} }
// FIXME: this is nasty, there should be a better way of doing this // FIXME: this is nasty, there should be a better way of doing this
/**
*
*/
function applyPointerHoverClass() { function applyPointerHoverClass() {
const classList = document.documentElement.classList; const classList = document.documentElement.classList;
classList.remove('force-pointer-on-hover'); classList.remove('force-pointer-on-hover');
@@ -790,6 +847,9 @@ export const useAppearanceSettingsStore = defineStore(
} }
} }
/**
*
*/
function togglePointerOnHover() { function togglePointerOnHover() {
showPointerOnHover.value = !showPointerOnHover.value; showPointerOnHover.value = !showPointerOnHover.value;
configRepository.setBool( configRepository.setBool(
@@ -811,6 +871,9 @@ export const useAppearanceSettingsStore = defineStore(
); );
} }
/**
*
*/
function handleSaveSidebarSortOrder() { function handleSaveSidebarSortOrder() {
if (sidebarSortMethod1.value === sidebarSortMethod2.value) { if (sidebarSortMethod1.value === sidebarSortMethod2.value) {
sidebarSortMethod2.value = ''; sidebarSortMethod2.value = '';
@@ -835,6 +898,9 @@ export const useAppearanceSettingsStore = defineStore(
setSidebarSortMethods(sidebarSortMethods); setSidebarSortMethods(sidebarSortMethods);
} }
/**
*
*/
async function mergeOldSortMethodsSettings() { async function mergeOldSortMethodsSettings() {
const orderFriendsGroupPrivate = await configRepository.getBool( const orderFriendsGroupPrivate = await configRepository.getBool(
'orderFriendGroupPrivate' 'orderFriendGroupPrivate'
@@ -897,6 +963,9 @@ export const useAppearanceSettingsStore = defineStore(
return n; return n;
}; };
/**
*
*/
function showTableLimitsDialog() { function showTableLimitsDialog() {
tableLimitsDialog.value.maxTableSize = Number( tableLimitsDialog.value.maxTableSize = Number(
vrcxStore.maxTableSize ?? 500 vrcxStore.maxTableSize ?? 500
@@ -907,10 +976,16 @@ export const useAppearanceSettingsStore = defineStore(
tableLimitsDialog.value.visible = true; tableLimitsDialog.value.visible = true;
} }
/**
*
*/
function closeTableLimitsDialog() { function closeTableLimitsDialog() {
tableLimitsDialog.value.visible = false; tableLimitsDialog.value.visible = false;
} }
/**
*
*/
async function saveTableLimitsDialog() { async function saveTableLimitsDialog() {
const nextMaxTableSize = clampLimit( const nextMaxTableSize = clampLimit(
tableLimitsDialog.value.maxTableSize, tableLimitsDialog.value.maxTableSize,
@@ -949,6 +1024,9 @@ export const useAppearanceSettingsStore = defineStore(
tableLimitsDialog.value.visible = false; tableLimitsDialog.value.visible = false;
} }
/**
*
*/
async function tryInitUserColours() { async function tryInitUserColours() {
if (!randomUserColours.value) { if (!randomUserColours.value) {
return; return;
@@ -958,6 +1036,10 @@ export const useAppearanceSettingsStore = defineStore(
await userColourInit(); await userColourInit();
} }
/**
*
* @param density
*/
function applyTableDensity(density) { function applyTableDensity(density) {
const classList = document.documentElement.classList; const classList = document.documentElement.classList;
classList.remove('is-compact-table', 'is-comfortable-table'); classList.remove('is-compact-table', 'is-comfortable-table');
+71 -113
View File
@@ -9,6 +9,12 @@ import {
isRpcWorld, isRpcWorld,
parseLocation parseLocation
} from '../../shared/utils'; } from '../../shared/utils';
import {
getPlatformLabel,
getRpcWorldConfig,
getStatusInfo,
isPopcornPalaceWorld
} from '../../shared/utils/discordPresence';
import { import {
ActivityType, ActivityType,
StatusDisplayType StatusDisplayType
@@ -59,14 +65,23 @@ export const useDiscordPresenceSettingsStore = defineStore(
const discordWorldIntegration = ref(true); const discordWorldIntegration = ref(true);
const discordWorldNameAsDiscordStatus = ref(false); const discordWorldNameAsDiscordStatus = ref(false);
/**
*
*/
function setDiscordActive() { function setDiscordActive() {
discordActive.value = !discordActive.value; discordActive.value = !discordActive.value;
configRepository.setBool('discordActive', discordActive.value); configRepository.setBool('discordActive', discordActive.value);
} }
/**
*
*/
function setDiscordInstance() { function setDiscordInstance() {
discordInstance.value = !discordInstance.value; discordInstance.value = !discordInstance.value;
configRepository.setBool('discordInstance', discordInstance.value); configRepository.setBool('discordInstance', discordInstance.value);
} }
/**
*
*/
function setDiscordHideInvite() { function setDiscordHideInvite() {
discordHideInvite.value = !discordHideInvite.value; discordHideInvite.value = !discordHideInvite.value;
configRepository.setBool( configRepository.setBool(
@@ -74,6 +89,9 @@ export const useDiscordPresenceSettingsStore = defineStore(
discordHideInvite.value discordHideInvite.value
); );
} }
/**
*
*/
function setDiscordJoinButton() { function setDiscordJoinButton() {
discordJoinButton.value = !discordJoinButton.value; discordJoinButton.value = !discordJoinButton.value;
configRepository.setBool( configRepository.setBool(
@@ -81,6 +99,9 @@ export const useDiscordPresenceSettingsStore = defineStore(
discordJoinButton.value discordJoinButton.value
); );
} }
/**
*
*/
function setDiscordHideImage() { function setDiscordHideImage() {
discordHideImage.value = !discordHideImage.value; discordHideImage.value = !discordHideImage.value;
configRepository.setBool( configRepository.setBool(
@@ -88,6 +109,9 @@ export const useDiscordPresenceSettingsStore = defineStore(
discordHideImage.value discordHideImage.value
); );
} }
/**
*
*/
function setDiscordShowPlatform() { function setDiscordShowPlatform() {
discordShowPlatform.value = !discordShowPlatform.value; discordShowPlatform.value = !discordShowPlatform.value;
configRepository.setBool( configRepository.setBool(
@@ -95,6 +119,9 @@ export const useDiscordPresenceSettingsStore = defineStore(
discordShowPlatform.value discordShowPlatform.value
); );
} }
/**
*
*/
function setDiscordWorldIntegration() { function setDiscordWorldIntegration() {
discordWorldIntegration.value = !discordWorldIntegration.value; discordWorldIntegration.value = !discordWorldIntegration.value;
configRepository.setBool( configRepository.setBool(
@@ -102,6 +129,9 @@ export const useDiscordPresenceSettingsStore = defineStore(
discordWorldIntegration.value discordWorldIntegration.value
); );
} }
/**
*
*/
function setDiscordWorldNameAsDiscordStatus() { function setDiscordWorldNameAsDiscordStatus() {
discordWorldNameAsDiscordStatus.value = discordWorldNameAsDiscordStatus.value =
!discordWorldNameAsDiscordStatus.value; !discordWorldNameAsDiscordStatus.value;
@@ -111,6 +141,9 @@ export const useDiscordPresenceSettingsStore = defineStore(
); );
} }
/**
*
*/
async function initDiscordPresenceSettings() { async function initDiscordPresenceSettings() {
const [ const [
discordActiveConfig, discordActiveConfig,
@@ -148,6 +181,9 @@ export const useDiscordPresenceSettingsStore = defineStore(
initDiscordPresenceSettings(); initDiscordPresenceSettings();
/**
*
*/
async function updateDiscord() { async function updateDiscord() {
let currentLocation = locationStore.lastLocation.location; let currentLocation = locationStore.lastLocation.location;
let startTime = locationStore.lastLocation.date; let startTime = locationStore.lastLocation.date;
@@ -204,27 +240,12 @@ export const useDiscordPresenceSettingsStore = defineStore(
let platform = ''; let platform = '';
if (discordShowPlatform.value) { if (discordShowPlatform.value) {
if (gameStore.isGameRunning) { platform = getPlatformLabel(
platform = gameStore.isGameNoVR userStore.currentUser.presence.platform,
? ` (${t('view.settings.discord_presence.rpc.desktop')})` gameStore.isGameRunning,
: ` (${t('view.settings.discord_presence.rpc.vr')})`; gameStore.isGameNoVR,
} else { t
switch (userStore.currentUser.presence.platform) { );
case 'web':
break;
case 'standalonewindows':
platform = ` (PC)`;
break;
case 'android':
platform = ` (Android)`;
break;
case 'ios':
platform = ` (iOS)`;
break;
default:
platform = ` (${userStore.currentUser.presence.platform})`;
}
}
} }
state.lastLocationDetails.groupAccessType = L.groupAccessType; state.lastLocationDetails.groupAccessType = L.groupAccessType;
if (L.groupAccessType) { if (L.groupAccessType) {
@@ -281,34 +302,14 @@ export const useDiscordPresenceSettingsStore = defineStore(
) { ) {
hidePrivate = true; hidePrivate = true;
} }
let statusName = ''; const statusInfo = getStatusInfo(
let statusImage = ''; userStore.currentUser.status,
switch (userStore.currentUser.status) { discordHideInvite.value,
case 'active': t
statusName = t('dialog.user.status.active'); );
statusImage = 'active'; const { statusName, statusImage } = statusInfo;
break; if (statusInfo.hidePrivate) {
case 'join me': hidePrivate = true;
statusName = t('dialog.user.status.join_me');
statusImage = 'joinme';
break;
case 'ask me':
statusName = t('dialog.user.status.ask_me');
statusImage = 'askme';
if (discordHideInvite.value) {
hidePrivate = true;
}
break;
case 'busy':
statusName = t('dialog.user.status.busy');
statusImage = 'busy';
hidePrivate = true;
break;
default:
statusName = t('dialog.user.status.offline');
statusImage = 'offline';
hidePrivate = true;
break;
} }
let details = state.lastLocationDetails.worldName; let details = state.lastLocationDetails.worldName;
let stateText = state.lastLocationDetails.accessName; let stateText = state.lastLocationDetails.accessName;
@@ -345,74 +346,23 @@ export const useDiscordPresenceSettingsStore = defineStore(
buttonUrl = ''; buttonUrl = '';
} }
if ( const rpcConfig =
isRpcWorld(state.lastLocationDetails.tag) && isRpcWorld(state.lastLocationDetails.tag) &&
discordWorldIntegration.value discordWorldIntegration.value
) { ? getRpcWorldConfig(state.lastLocationDetails.worldId)
// custom world rpc : null;
if (rpcConfig) {
activityType = rpcConfig.activityType;
statusDisplayType = rpcConfig.statusDisplayType;
appId = rpcConfig.appId;
bigIcon = rpcConfig.bigIcon;
if ( if (
state.lastLocationDetails.worldId === isPopcornPalaceWorld(state.lastLocationDetails.worldId) &&
'wrld_f20326da-f1ac-45fc-a062-609723b097b1' || !discordHideImage.value &&
state.lastLocationDetails.worldId === gameLogStore.nowPlaying.thumbnailUrl
'wrld_10e5e467-fc65-42ed-8957-f02cace1398c' ||
state.lastLocationDetails.worldId ===
'wrld_04899f23-e182-4a8d-b2c7-2c74c7c15534'
) { ) {
activityType = ActivityType.Listening; bigIcon = gameLogStore.nowPlaying.thumbnailUrl;
statusDisplayType = StatusDisplayType.Details;
appId = '784094509008551956';
bigIcon = 'pypy';
} else if (
state.lastLocationDetails.worldId ===
'wrld_42377cf1-c54f-45ed-8996-5875b0573a83' ||
state.lastLocationDetails.worldId ===
'wrld_dd6d2888-dbdc-47c2-bc98-3d631b2acd7c'
) {
activityType = ActivityType.Listening;
statusDisplayType = StatusDisplayType.Details;
appId = '846232616054030376';
bigIcon = 'vr_dancing';
} else if (
state.lastLocationDetails.worldId ===
'wrld_52bdcdab-11cd-4325-9655-0fb120846945' ||
state.lastLocationDetails.worldId ===
'wrld_2d40da63-8f1f-4011-8a9e-414eb8530acd'
) {
activityType = ActivityType.Listening;
statusDisplayType = StatusDisplayType.Details;
appId = '939473404808007731';
bigIcon = 'zuwa_zuwa_dance';
} else if (
state.lastLocationDetails.worldId ===
'wrld_74970324-58e8-4239-a17b-2c59dfdf00db' ||
state.lastLocationDetails.worldId ===
'wrld_db9d878f-6e76-4776-8bf2-15bcdd7fc445' ||
state.lastLocationDetails.worldId ===
'wrld_435bbf25-f34f-4b8b-82c6-cd809057eb8e' ||
state.lastLocationDetails.worldId ===
'wrld_f767d1c8-b249-4ecc-a56f-614e433682c8'
) {
activityType = ActivityType.Watching;
statusDisplayType = StatusDisplayType.Details;
appId = '968292722391785512';
bigIcon = 'ls_media';
} else if (
state.lastLocationDetails.worldId ===
'wrld_266523e8-9161-40da-acd0-6bd82e075833' ||
state.lastLocationDetails.worldId ===
'wrld_27c7e6b2-d938-447e-a270-3d1a873e2cf3'
) {
activityType = ActivityType.Watching;
statusDisplayType = StatusDisplayType.Details;
appId = '1095440531821170820';
if (
!discordHideImage.value &&
gameLogStore.nowPlaying.thumbnailUrl
) {
bigIcon = gameLogStore.nowPlaying.thumbnailUrl;
} else {
bigIcon = 'popcorn_palace';
}
} }
if (gameLogStore.nowPlaying.name) { if (gameLogStore.nowPlaying.name) {
details = gameLogStore.nowPlaying.name; details = gameLogStore.nowPlaying.name;
@@ -476,12 +426,20 @@ export const useDiscordPresenceSettingsStore = defineStore(
); );
} }
/**
*
* @param active
*/
async function setIsDiscordActive(active) { async function setIsDiscordActive(active) {
if (active !== state.isDiscordActive) { if (active !== state.isDiscordActive) {
state.isDiscordActive = await Discord.SetActive(active); state.isDiscordActive = await Discord.SetActive(active);
} }
} }
/**
*
* @param configLabel
*/
async function saveDiscordOption(configLabel = '') { async function saveDiscordOption(configLabel = '') {
state.lastLocationDetails.tag = ''; state.lastLocationDetails.tag = '';
updateLoopStore.nextDiscordUpdate = 3; updateLoopStore.nextDiscordUpdate = 3;
+71 -176
View File
@@ -12,6 +12,10 @@ import {
compareByLocationAt, compareByLocationAt,
compareByName, compareByName,
compareByUpdatedAt, compareByUpdatedAt,
computeUserPlatform,
createDefaultUserRef,
diffObjectProps,
evictMapCache,
extractFileId, extractFileId,
findUserByDisplayName, findUserByDisplayName,
getAllUserMemos, getAllUserMemos,
@@ -20,8 +24,8 @@ import {
getWorldName, getWorldName,
isRealInstance, isRealInstance,
parseLocation, parseLocation,
removeEmojis, replaceBioSymbols,
replaceBioSymbols sanitizeUserJson
} from '../shared/utils'; } from '../shared/utils';
import { import {
avatarRequest, avatarRequest,
@@ -29,10 +33,10 @@ import {
instanceRequest, instanceRequest,
userRequest userRequest
} from '../api'; } from '../api';
import { patchUserFromEvent } from '../query';
import { processBulk, request } from '../service/request'; import { processBulk, request } from '../service/request';
import { AppDebug } from '../service/appConfig'; import { AppDebug } from '../service/appConfig';
import { database } from '../service/database'; import { database } from '../service/database';
import { patchUserFromEvent } from '../query';
import { useAppearanceSettingsStore } from './settings/appearance'; import { useAppearanceSettingsStore } from './settings/appearance';
import { useAuthStore } from './auth'; import { useAuthStore } from './auth';
import { useAvatarStore } from './avatar'; import { useAvatarStore } from './avatar';
@@ -344,6 +348,10 @@ export const useUserStore = defineStore('User', () => {
{ flush: 'sync' } { flush: 'sync' }
); );
/**
*
* @param args
*/
function handleConfig(args) { function handleConfig(args) {
const authStore = useAuthStore(); const authStore = useAuthStore();
const ref = { const ref = {
@@ -419,143 +427,18 @@ export const useUserStore = defineStore('User', () => {
} }
const robotUrl = `${AppDebug.endpointDomain}/file/file_0e8c4e32-7444-44ea-ade4-313c010d4bae/1/file`; const robotUrl = `${AppDebug.endpointDomain}/file/file_0e8c4e32-7444-44ea-ade4-313c010d4bae/1/file`;
/**
*
* @param {Map<string, any>} userCache
* @param {Map<string, any>} friendMap
*/
function cleanupUserCache(userCache, friendMap) {
const bufferSize = 300;
const currentFriendCount = friendMap.size;
const currentTotalSize = userCache.size;
const effectiveMaxSize = currentFriendCount + bufferSize;
if (currentTotalSize <= effectiveMaxSize) {
return;
}
const targetDeleteCount = currentTotalSize - effectiveMaxSize;
let deletedCount = 0;
const keysToDelete = [];
for (const userId of userCache.keys()) {
if (friendMap.has(userId)) {
continue;
}
if (deletedCount >= targetDeleteCount) {
break;
}
keysToDelete.push(userId);
deletedCount++;
}
for (const id of keysToDelete) {
userCache.delete(id);
}
console.log(
`User cache cleanup: Deleted ${deletedCount}. Current cache size: ${userCache.size}`
);
}
/** /**
* *
* @param {import('../types/api/user').GetUserResponse} json * @param {import('../types/api/user').GetUserResponse} json
* @returns {import('../types/api/user').VrcxUser} * @returns {import('../types/api/user').VrcxUser}
*/ */
function applyUser(json) { function applyUser(json) {
let hasPropChanged = false;
const changedProps = {};
let ref = cachedUsers.get(json.id); let ref = cachedUsers.get(json.id);
if (json.statusDescription) { let hasPropChanged = false;
json.statusDescription = replaceBioSymbols(json.statusDescription); let changedProps = {};
json.statusDescription = removeEmojis(json.statusDescription); sanitizeUserJson(json, robotUrl);
}
if (json.bio) {
json.bio = replaceBioSymbols(json.bio);
}
if (json.note) {
json.note = replaceBioSymbols(json.note);
}
if (json.currentAvatarImageUrl === robotUrl) {
delete json.currentAvatarImageUrl;
delete json.currentAvatarThumbnailImageUrl;
}
if (typeof ref === 'undefined') { if (typeof ref === 'undefined') {
ref = reactive({ ref = reactive(createDefaultUserRef(json));
ageVerificationStatus: '',
ageVerified: false,
allowAvatarCopying: false,
badges: [],
bio: '',
bioLinks: [],
currentAvatarImageUrl: '',
currentAvatarTags: [],
currentAvatarThumbnailImageUrl: '',
date_joined: '',
developerType: '',
discordId: '',
displayName: '',
friendKey: '',
friendRequestStatus: '',
id: '',
instanceId: '',
isFriend: false,
last_activity: '',
last_login: '',
last_mobile: null,
last_platform: '',
location: '',
platform: '',
note: null,
profilePicOverride: '',
profilePicOverrideThumbnail: '',
pronouns: '',
state: '',
status: '',
statusDescription: '',
tags: [],
travelingToInstance: '',
travelingToLocation: '',
travelingToWorld: '',
userIcon: '',
worldId: '',
// only in bulk request
fallbackAvatar: '',
// VRCX
$location: {},
$location_at: Date.now(),
$online_for: Date.now(),
$travelingToTime: Date.now(),
$offline_for: null,
$active_for: Date.now(),
$isVRCPlus: false,
$isModerator: false,
$isTroll: false,
$isProbableTroll: false,
$trustLevel: 'Visitor',
$trustClass: 'x-tag-untrusted',
$userColour: '',
$trustSortNum: 1,
$languages: [],
$joinCount: 0,
$timeSpent: 0,
$lastSeen: '',
$mutualCount: 0,
$nickName: '',
$previousLocation: '',
$customTag: '',
$customTagColour: '',
$friendNumber: 0,
$platform: '',
$moderations: {},
//
...json
});
if (locationStore.lastLocation.playerList.has(json.id)) { if (locationStore.lastLocation.playerList.has(json.id)) {
// update $location_at from instance join time // update $location_at from instance join time
const player = locationStore.lastLocation.playerList.get( const player = locationStore.lastLocation.playerList.get(
@@ -581,7 +464,12 @@ export const useUserStore = defineStore('User', () => {
ref.$customTag = ''; ref.$customTag = '';
ref.$customTagColour = ''; ref.$customTagColour = '';
} }
cleanupUserCache(cachedUsers, friendStore.friends); evictMapCache(
cachedUsers,
friendStore.friends.size + 300,
(_value, key) => friendStore.friends.has(key),
{ logLabel: 'User cache cleanup' }
);
cachedUsers.set(ref.id, ref); cachedUsers.set(ref.id, ref);
friendStore.updateFriend(ref.id); friendStore.updateFriend(ref.id);
} else { } else {
@@ -589,59 +477,23 @@ export const useUserStore = defineStore('User', () => {
// offline event before GPS to offline location // offline event before GPS to offline location
friendStore.updateFriend(ref.id, json.state); friendStore.updateFriend(ref.id, json.state);
} }
for (const prop in ref) { const {
if (typeof json[prop] === 'undefined') { hasPropChanged: _hasPropChanged,
continue; changedProps: _changedProps
} } = diffObjectProps(ref, json, arraysMatch);
// Only compare primitive values
if (ref[prop] === null || typeof ref[prop] !== 'object') {
changedProps[prop] = true;
}
}
for (const prop in json) {
if (typeof ref[prop] === 'undefined') {
continue;
}
if (Array.isArray(json[prop]) && Array.isArray(ref[prop])) {
if (!arraysMatch(json[prop], ref[prop])) {
changedProps[prop] = true;
}
} else if (
json[prop] === null ||
typeof json[prop] !== 'object'
) {
changedProps[prop] = true;
}
}
for (const prop in changedProps) {
const asIs = ref[prop];
const toBe = json[prop];
if (asIs === toBe) {
delete changedProps[prop];
} else {
hasPropChanged = true;
changedProps[prop] = [toBe, asIs];
}
}
for (const prop in json) { for (const prop in json) {
if (typeof json[prop] !== 'undefined') { if (typeof json[prop] !== 'undefined') {
ref[prop] = json[prop]; ref[prop] = json[prop];
} }
} }
hasPropChanged = _hasPropChanged;
changedProps = _changedProps;
} }
ref.$moderations = moderationStore.getUserModerations(ref.id); ref.$moderations = moderationStore.getUserModerations(ref.id);
ref.$isVRCPlus = ref.tags.includes('system_supporter'); ref.$isVRCPlus = ref.tags.includes('system_supporter');
appearanceSettingsStore.applyUserTrustLevel(ref); appearanceSettingsStore.applyUserTrustLevel(ref);
applyUserLanguage(ref); applyUserLanguage(ref);
if ( ref.$platform = computeUserPlatform(ref.platform, ref.last_platform);
ref.platform &&
ref.platform !== 'offline' &&
ref.platform !== 'web'
) {
ref.$platform = ref.platform;
} else {
ref.$platform = ref.last_platform;
}
// traveling // traveling
if (ref.location === 'traveling') { if (ref.location === 'traveling') {
ref.$location = parseLocation(ref.travelingToLocation); ref.$location = parseLocation(ref.travelingToLocation);
@@ -1180,6 +1032,10 @@ export const useUserStore = defineStore('User', () => {
D.instance.friendCount = friendCount; D.instance.friendCount = friendCount;
} }
/**
*
* @param array
*/
function sortUserDialogAvatars(array) { function sortUserDialogAvatars(array) {
const D = userDialog.value; const D = userDialog.value;
if (D.avatarSorting === 'update') { if (D.avatarSorting === 'update') {
@@ -1192,6 +1048,10 @@ export const useUserStore = defineStore('User', () => {
D.avatars = array; D.avatars = array;
} }
/**
*
* @param fileId
*/
async function refreshUserDialogAvatars(fileId) { async function refreshUserDialogAvatars(fileId) {
const D = userDialog.value; const D = userDialog.value;
const userId = D.id; const userId = D.id;
@@ -1248,6 +1108,10 @@ export const useUserStore = defineStore('User', () => {
}); });
} }
/**
*
* @param ref
*/
async function lookupUser(ref) { async function lookupUser(ref) {
let ctx; let ctx;
if (ref.userId) { if (ref.userId) {
@@ -1577,6 +1441,9 @@ export const useUserStore = defineStore('User', () => {
} }
} }
/**
*
*/
function updateAutoStateChange() { function updateAutoStateChange() {
if ( if (
!generalSettingsStore.autoStateChangeEnabled || !generalSettingsStore.autoStateChangeEnabled ||
@@ -1683,6 +1550,10 @@ export const useUserStore = defineStore('User', () => {
}); });
} }
/**
*
* @param data
*/
function addCustomTag(data) { function addCustomTag(data) {
if (data.Tag) { if (data.Tag) {
customUserTags.set(data.UserId, { customUserTags.set(data.UserId, {
@@ -1708,6 +1579,9 @@ export const useUserStore = defineStore('User', () => {
sharedFeedStore.addTag(data.UserId, data.TagColour); sharedFeedStore.addTag(data.UserId, data.TagColour);
} }
/**
*
*/
async function initUserNotes() { async function initUserNotes() {
state.lastNoteCheck = new Date(); state.lastNoteCheck = new Date();
state.lastDbNoteDate = null; state.lastDbNoteDate = null;
@@ -1735,6 +1609,9 @@ export const useUserStore = defineStore('User', () => {
} }
} }
/**
*
*/
async function getLatestUserNotes() { async function getLatestUserNotes() {
state.lastNoteCheck = new Date(); state.lastNoteCheck = new Date();
const params = { const params = {
@@ -1793,6 +1670,11 @@ export const useUserStore = defineStore('User', () => {
} }
} }
/**
*
* @param userId
* @param newNote
*/
async function checkNote(userId, newNote) { async function checkNote(userId, newNote) {
// last check was more than than 5 minutes ago // last check was more than than 5 minutes ago
if ( if (
@@ -1814,6 +1696,9 @@ export const useUserStore = defineStore('User', () => {
} }
} }
/**
*
*/
function getCurrentUser() { function getCurrentUser() {
return request('auth/user', { return request('auth/user', {
method: 'GET' method: 'GET'
@@ -2079,11 +1964,18 @@ export const useUserStore = defineStore('User', () => {
return ref; return ref;
} }
/**
*
* @param userId
*/
function showSendBoopDialog(userId) { function showSendBoopDialog(userId) {
sendBoopDialog.value.userId = userId; sendBoopDialog.value.userId = userId;
sendBoopDialog.value.visible = true; sendBoopDialog.value.visible = true;
} }
/**
*
*/
function toggleSharedConnectionsOptOut() { function toggleSharedConnectionsOptOut() {
userRequest.saveCurrentUser({ userRequest.saveCurrentUser({
hasSharedConnectionsOptOut: hasSharedConnectionsOptOut:
@@ -2091,6 +1983,9 @@ export const useUserStore = defineStore('User', () => {
}); });
} }
/**
*
*/
function toggleDiscordFriendsOptOut() { function toggleDiscordFriendsOptOut() {
userRequest.saveCurrentUser({ userRequest.saveCurrentUser({
hasDiscordFriendsOptOut: !currentUser.value.hasDiscordFriendsOptOut hasDiscordFriendsOptOut: !currentUser.value.hasDiscordFriendsOptOut
+10 -64
View File
@@ -5,16 +5,18 @@ import { useI18n } from 'vue-i18n';
import { import {
checkVRChatCache, checkVRChatCache,
createDefaultWorldRef,
evictMapCache,
getAvailablePlatforms, getAvailablePlatforms,
getBundleDateSize, getBundleDateSize,
getWorldMemo, getWorldMemo,
isRealInstance, isRealInstance,
parseLocation, parseLocation,
replaceBioSymbols sanitizeEntityJson
} from '../shared/utils'; } from '../shared/utils';
import { instanceRequest, miscRequest, worldRequest } from '../api'; import { instanceRequest, miscRequest, worldRequest } from '../api';
import { patchWorldFromEvent } from '../query';
import { database } from '../service/database'; import { database } from '../service/database';
import { patchWorldFromEvent } from '../query';
import { processBulk } from '../service/request'; import { processBulk } from '../service/request';
import { useFavoriteStore } from './favorite'; import { useFavoriteStore } from './favorite';
import { useInstanceStore } from './instance'; import { useInstanceStore } from './instance';
@@ -76,6 +78,7 @@ export const useWorldStore = defineStore('World', () => {
* *
* @param {string} tag * @param {string} tag
* @param {string} shortName * @param {string} shortName
* @param options
*/ */
function showWorldDialog(tag, shortName = null, options = {}) { function showWorldDialog(tag, shortName = null, options = {}) {
const D = worldDialog; const D = worldDialog;
@@ -204,7 +207,6 @@ export const useWorldStore = defineStore('World', () => {
args.json !== false; args.json !== false;
} }
}); });
} }
}); });
} }
@@ -235,20 +237,9 @@ export const useWorldStore = defineStore('World', () => {
* @param WorldCache * @param WorldCache
*/ */
function cleanupWorldCache(WorldCache) { function cleanupWorldCache(WorldCache) {
const maxCacheSize = 10000; evictMapCache(WorldCache, 10000, () => false, {
logLabel: 'World cache cleanup'
if (WorldCache.size <= maxCacheSize) { });
return;
}
const deletedCount = WorldCache.size - maxCacheSize;
while (WorldCache.size > maxCacheSize) {
const deletedKey = WorldCache.keys().next().value;
WorldCache.delete(deletedKey);
}
console.log(
`World cache cleanup: Deleted ${deletedCount}. Current cache size: ${WorldCache.size}`
);
} }
/** /**
@@ -257,55 +248,10 @@ export const useWorldStore = defineStore('World', () => {
* @returns {object} ref * @returns {object} ref
*/ */
function applyWorld(json) { function applyWorld(json) {
if (json.name) { sanitizeEntityJson(json, ['name', 'description']);
json.name = replaceBioSymbols(json.name);
}
if (json.description) {
json.description = replaceBioSymbols(json.description);
}
let ref = cachedWorlds.get(json.id); let ref = cachedWorlds.get(json.id);
if (typeof ref === 'undefined') { if (typeof ref === 'undefined') {
ref = { ref = createDefaultWorldRef(json);
id: '',
name: '',
description: '',
defaultContentSettings: {},
authorId: '',
authorName: '',
capacity: 0,
recommendedCapacity: 0,
tags: [],
releaseStatus: '',
imageUrl: '',
thumbnailImageUrl: '',
assetUrl: '',
assetUrlObject: {},
pluginUrl: '',
pluginUrlObject: {},
unityPackageUrl: '',
unityPackageUrlObject: {},
unityPackages: [],
version: 0,
favorites: 0,
created_at: '',
updated_at: '',
publicationDate: '',
labsPublicationDate: '',
visits: 0,
popularity: 0,
heat: 0,
publicOccupants: 0,
privateOccupants: 0,
occupants: 0,
instances: [],
featured: false,
organization: '',
previewYoutubeId: '',
// VRCX
$isLabs: false,
//
...json
};
cleanupWorldCache(cachedWorlds); cleanupWorldCache(cachedWorlds);
cachedWorlds.set(ref.id, ref); cachedWorlds.set(ref.id, ref);
} else { } else {