summaryrefslogtreecommitdiff
path: root/packages/frontend
diff options
context:
space:
mode:
authormisskey-release-bot[bot] <157398866+misskey-release-bot[bot]@users.noreply.github.com>2025-12-06 12:22:58 +0000
committerGitHub <noreply@github.com>2025-12-06 12:22:58 +0000
commite40c84f31df0202351c5585d3edbca000846b73b (patch)
tree548eafb27b758c55de2e750a0ef9efbe3f056357 /packages/frontend
parentMerge pull request #16840 from misskey-dev/develop (diff)
parentRelease: 2025.12.0 (diff)
downloadmisskey-e40c84f31df0202351c5585d3edbca000846b73b.tar.gz
misskey-e40c84f31df0202351c5585d3edbca000846b73b.tar.bz2
misskey-e40c84f31df0202351c5585d3edbca000846b73b.zip
Merge pull request #16916 from misskey-dev/develop
Release: 2025.12.0
Diffstat (limited to 'packages/frontend')
-rw-r--r--packages/frontend/.storybook/preload-locale.ts2
-rw-r--r--packages/frontend/build.ts2
-rw-r--r--packages/frontend/lib/vite-plugin-watch-locales.ts18
-rw-r--r--packages/frontend/package.json55
-rw-r--r--packages/frontend/public/loader/boot.js336
-rw-r--r--packages/frontend/public/loader/style.css78
-rw-r--r--packages/frontend/src/components/MkMediaImage.vue18
-rw-r--r--packages/frontend/src/components/global/I18n.vue4
-rw-r--r--packages/frontend/src/components/global/StackingRouterView.vue1
-rw-r--r--packages/frontend/src/i18n.ts2
-rw-r--r--packages/frontend/src/theme.ts4
-rw-r--r--packages/frontend/test/aiscript/api.test.ts15
-rw-r--r--packages/frontend/test/i18n.test.ts2
-rw-r--r--packages/frontend/test/init.ts6
-rw-r--r--packages/frontend/vite.config.ts6
15 files changed, 486 insertions, 63 deletions
diff --git a/packages/frontend/.storybook/preload-locale.ts b/packages/frontend/.storybook/preload-locale.ts
index c823ff9bee..42918b44f3 100644
--- a/packages/frontend/.storybook/preload-locale.ts
+++ b/packages/frontend/.storybook/preload-locale.ts
@@ -4,7 +4,7 @@
*/
import { writeFile } from 'node:fs/promises';
-import locales from '../../../locales/index.js';
+import locales from 'i18n';
await writeFile(
new URL('locale.ts', import.meta.url),
diff --git a/packages/frontend/build.ts b/packages/frontend/build.ts
index 0401c2b9ba..0f605c745b 100644
--- a/packages/frontend/build.ts
+++ b/packages/frontend/build.ts
@@ -2,7 +2,7 @@ import * as fs from 'fs/promises';
import url from 'node:url';
import path from 'node:path';
import { execa } from 'execa';
-import locales from '../../locales/index.js';
+import locales from 'i18n';
import { LocaleInliner } from '../frontend-builder/locale-inliner.js'
import { createLogger } from '../frontend-builder/logger';
diff --git a/packages/frontend/lib/vite-plugin-watch-locales.ts b/packages/frontend/lib/vite-plugin-watch-locales.ts
index 8e209d27bd..372e9039d5 100644
--- a/packages/frontend/lib/vite-plugin-watch-locales.ts
+++ b/packages/frontend/lib/vite-plugin-watch-locales.ts
@@ -4,7 +4,7 @@
*/
import path from 'node:path'
-import locales from '../../../locales/index.js';
+import locales from 'i18n';
const localesDir = path.resolve(__dirname, '../../../locales')
@@ -13,14 +13,14 @@ const localesDir = path.resolve(__dirname, '../../../locales')
* @returns {import('vite').Plugin}
*/
export default function pluginWatchLocales() {
- return {
- name: 'watch-locales',
+ return {
+ name: 'watch-locales',
- configureServer(server) {
- const localeYmlPaths = Object.keys(locales).map(locale => path.join(localesDir, `${locale}.yml`));
+ configureServer(server) {
+ const localeYmlPaths = Object.keys(locales).map(locale => path.join(localesDir, `${locale}.yml`));
- // watcherにパスを追加
- server.watcher.add(localeYmlPaths);
+ // watcherにパスを追加
+ server.watcher.add(localeYmlPaths);
server.watcher.on('change', (filePath) => {
if (localeYmlPaths.includes(filePath)) {
@@ -31,6 +31,6 @@ export default function pluginWatchLocales() {
})
}
});
- },
- };
+ },
+ };
}
diff --git a/packages/frontend/package.json b/packages/frontend/package.json
index b6906e130a..c9d49201c4 100644
--- a/packages/frontend/package.json
+++ b/packages/frontend/package.json
@@ -20,16 +20,17 @@
"@discordapp/twemoji": "16.0.1",
"@github/webauthn-json": "2.1.1",
"@mcaptcha/vanilla-glue": "0.1.0-alpha-3",
+ "i18n": "workspace:*",
"@misskey-dev/browser-image-resizer": "2024.1.0",
"@rollup/plugin-json": "6.1.0",
"@rollup/plugin-replace": "6.0.3",
"@rollup/pluginutils": "5.3.0",
- "@sentry/vue": "10.26.0",
+ "@sentry/vue": "10.27.0",
"@syuilo/aiscript": "1.2.0",
"@syuilo/aiscript-0-19-0": "npm:@syuilo/aiscript@^0.19.0",
"@twemoji/parser": "16.0.0",
"@vitejs/plugin-vue": "6.0.2",
- "@vue/compiler-sfc": "3.5.24",
+ "@vue/compiler-sfc": "3.5.25",
"aiscript-vscode": "github:aiscript-dev/aiscript-vscode#v0.1.15",
"analytics": "0.8.19",
"astring": "1.9.0",
@@ -58,7 +59,7 @@
"json5": "2.2.3",
"magic-string": "0.30.21",
"matter-js": "0.20.0",
- "mediabunny": "1.25.0",
+ "mediabunny": "1.25.3",
"mfm-js": "0.25.0",
"misskey-bubble-game": "workspace:*",
"misskey-js": "workspace:*",
@@ -69,8 +70,8 @@
"qr-scanner": "1.4.2",
"rollup": "4.53.3",
"sanitize-html": "2.17.0",
- "sass": "1.94.1",
- "shiki": "3.15.0",
+ "sass": "1.94.2",
+ "shiki": "3.17.0",
"strict-event-emitter-types": "2.0.0",
"textarea-caret": "3.1.0",
"three": "0.181.2",
@@ -80,8 +81,8 @@
"tsconfig-paths": "4.2.0",
"typescript": "5.9.3",
"v-code-diff": "1.13.1",
- "vite": "7.2.2",
- "vue": "3.5.24",
+ "vite": "7.2.4",
+ "vue": "3.5.25",
"vuedraggable": "next",
"wanakana": "5.3.1"
},
@@ -89,7 +90,7 @@
"@misskey-dev/summaly": "5.2.5",
"@storybook/addon-essentials": "8.6.14",
"@storybook/addon-interactions": "8.6.14",
- "@storybook/addon-links": "9.1.16",
+ "@storybook/addon-links": "10.1.0",
"@storybook/addon-mdx-gfm": "8.6.14",
"@storybook/addon-storysource": "8.6.14",
"@storybook/blocks": "8.6.14",
@@ -97,13 +98,13 @@
"@storybook/core-events": "8.6.14",
"@storybook/manager-api": "8.6.14",
"@storybook/preview-api": "8.6.14",
- "@storybook/react": "9.1.16",
- "@storybook/react-vite": "9.1.16",
+ "@storybook/react": "10.1.0",
+ "@storybook/react-vite": "10.1.0",
"@storybook/test": "8.6.14",
"@storybook/theming": "8.6.14",
"@storybook/types": "8.6.14",
- "@storybook/vue3": "9.1.16",
- "@storybook/vue3-vite": "9.1.16",
+ "@storybook/vue3": "10.1.0",
+ "@storybook/vue3-vite": "10.1.0",
"@tabler/icons-webfont": "3.35.0",
"@testing-library/vue": "8.1.0",
"@types/canvas-confetti": "1.9.0",
@@ -117,38 +118,38 @@
"@types/throttle-debounce": "5.0.2",
"@types/tinycolor2": "1.4.6",
"@types/ws": "8.18.1",
- "@typescript-eslint/eslint-plugin": "8.47.0",
- "@typescript-eslint/parser": "8.47.0",
- "@vitest/coverage-v8": "3.2.4",
- "@vue/compiler-core": "3.5.24",
- "@vue/runtime-core": "3.5.24",
+ "@typescript-eslint/eslint-plugin": "8.48.0",
+ "@typescript-eslint/parser": "8.48.0",
+ "@vitest/coverage-v8": "4.0.14",
+ "@vue/compiler-core": "3.5.25",
+ "@vue/runtime-core": "3.5.25",
"acorn": "8.15.0",
"cross-env": "10.1.0",
- "cypress": "15.6.0",
+ "cypress": "15.7.0",
"eslint-plugin-import": "2.32.0",
- "eslint-plugin-vue": "10.5.1",
+ "eslint-plugin-vue": "10.6.2",
"fast-glob": "3.3.3",
- "happy-dom": "20.0.10",
+ "happy-dom": "20.0.11",
"intersection-observer": "0.12.2",
"micromatch": "4.0.8",
"minimatch": "10.1.1",
- "msw": "2.12.2",
+ "msw": "2.12.3",
"msw-storybook-addon": "2.0.6",
"nodemon": "3.1.11",
- "prettier": "3.6.2",
+ "prettier": "3.7.1",
"react": "19.2.0",
"react-dom": "19.2.0",
"seedrandom": "3.0.5",
- "start-server-and-test": "2.1.2",
- "storybook": "9.1.16",
+ "start-server-and-test": "2.1.3",
+ "storybook": "10.1.0",
"storybook-addon-misskey-theme": "github:misskey-dev/storybook-addon-misskey-theme",
"tsx": "4.20.6",
"vite-plugin-glsl": "1.5.4",
"vite-plugin-turbosnap": "1.0.3",
- "vitest": "3.2.4",
+ "vitest": "4.0.14",
"vitest-fetch-mock": "0.4.5",
- "vue-component-type-helpers": "3.1.4",
+ "vue-component-type-helpers": "3.1.5",
"vue-eslint-parser": "10.2.0",
- "vue-tsc": "3.1.4"
+ "vue-tsc": "3.1.5"
}
}
diff --git a/packages/frontend/public/loader/boot.js b/packages/frontend/public/loader/boot.js
new file mode 100644
index 0000000000..ab4b158287
--- /dev/null
+++ b/packages/frontend/public/loader/boot.js
@@ -0,0 +1,336 @@
+/*
+ * SPDX-FileCopyrightText: syuilo and misskey-project
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+'use strict';
+
+// ブロックの中に入れないと、定義した変数がブラウザのグローバルスコープに登録されてしまい邪魔なので
+(async () => {
+ window.onerror = (e) => {
+ console.error(e);
+ renderError('SOMETHING_HAPPENED', e);
+ };
+ window.onunhandledrejection = (e) => {
+ console.error(e);
+ renderError('SOMETHING_HAPPENED_IN_PROMISE', e.reason || e);
+ };
+
+ let forceError = localStorage.getItem('forceError');
+ if (forceError != null) {
+ renderError('FORCED_ERROR', 'This error is forced by having forceError in local storage.');
+ return;
+ }
+
+ //#region Detect language
+ const supportedLangs = LANGS;
+ /** @type { string } */
+ let lang = localStorage.getItem('lang');
+ if (lang == null || !supportedLangs.includes(lang)) {
+ if (supportedLangs.includes(navigator.language)) {
+ lang = navigator.language;
+ } else {
+ lang = supportedLangs.find(x => x.split('-')[0] === navigator.language);
+
+ // Fallback
+ if (lang == null) lang = 'en-US';
+ }
+ }
+
+ // for https://github.com/misskey-dev/misskey/issues/10202
+ if (lang == null || lang.toString == null || lang.toString() === 'null') {
+ console.error('invalid lang value detected!!!', typeof lang, lang);
+ lang = 'en-US';
+ }
+ //#endregion
+
+ //#region Script
+ async function importAppScript() {
+ await import(CLIENT_ENTRY ? `/vite/${CLIENT_ENTRY.replace('scripts', lang)}` : '/vite/src/_boot_.ts')
+ .catch(async e => {
+ console.error(e);
+ renderError('APP_IMPORT', e);
+ });
+ }
+
+ // タイミングによっては、この時点でDOMの構築が済んでいる場合とそうでない場合とがある
+ if (document.readyState !== 'loading') {
+ importAppScript();
+ } else {
+ window.addEventListener('DOMContentLoaded', () => {
+ importAppScript();
+ });
+ }
+ //#endregion
+
+ let isSafeMode = (localStorage.getItem('isSafeMode') === 'true');
+
+ if (!isSafeMode) {
+ const urlParams = new URLSearchParams(window.location.search);
+
+ if (urlParams.has('safemode') && urlParams.get('safemode') === 'true') {
+ localStorage.setItem('isSafeMode', 'true');
+ isSafeMode = true;
+ }
+ }
+
+ //#region Theme
+ if (!isSafeMode) {
+ const theme = localStorage.getItem('theme');
+ if (theme) {
+ for (const [k, v] of Object.entries(JSON.parse(theme))) {
+ document.documentElement.style.setProperty(`--MI_THEME-${k}`, v.toString());
+
+ // HTMLの theme-color 適用
+ if (k === 'htmlThemeColor') {
+ for (const tag of document.head.children) {
+ if (tag.tagName === 'META' && tag.getAttribute('name') === 'theme-color') {
+ tag.setAttribute('content', v);
+ break;
+ }
+ }
+ }
+ }
+ }
+ }
+
+ const colorScheme = localStorage.getItem('colorScheme');
+ if (colorScheme) {
+ document.documentElement.style.setProperty('color-scheme', colorScheme);
+ }
+ //#endregion
+
+ const fontSize = localStorage.getItem('fontSize');
+ if (fontSize) {
+ document.documentElement.classList.add('f-' + fontSize);
+ }
+
+ const useSystemFont = localStorage.getItem('useSystemFont');
+ if (useSystemFont) {
+ document.documentElement.classList.add('useSystemFont');
+ }
+
+ if (!isSafeMode) {
+ const customCss = localStorage.getItem('customCss');
+ if (customCss && customCss.length > 0) {
+ const style = document.createElement('style');
+ style.innerHTML = customCss;
+ document.head.appendChild(style);
+ }
+ }
+
+ async function addStyle(styleText) {
+ let css = document.createElement('style');
+ css.appendChild(document.createTextNode(styleText));
+ document.head.appendChild(css);
+ }
+
+ async function renderError(code, details) {
+ // Cannot set property 'innerHTML' of null を回避
+ if (document.readyState === 'loading') {
+ await new Promise(resolve => window.addEventListener('DOMContentLoaded', resolve));
+ }
+
+ let messages = null;
+ const bootloaderLocales = localStorage.getItem('bootloaderLocales');
+ if (bootloaderLocales) {
+ messages = JSON.parse(bootloaderLocales);
+ }
+ if (!messages) {
+ // older version of misskey does not store bootloaderLocales, stores locale as a whole
+ const legacyLocale = localStorage.getItem('locale');
+ if (legacyLocale) {
+ const parsed = JSON.parse(legacyLocale);
+ messages = {
+ ...(parsed._bootErrors ?? {}),
+ reload: parsed.reload,
+ };
+ }
+ }
+ if (!messages) messages = {};
+
+ messages = Object.assign({
+ title: 'Failed to initialize Misskey',
+ solution: 'The following actions may solve the problem.',
+ solution1: 'Update your os and browser',
+ solution2: 'Disable an adblocker',
+ solution3: 'Clear the browser cache',
+ solution4: '(Tor Browser) Set dom.webaudio.enabled to true',
+ otherOption: 'Other options',
+ otherOption1: 'Clear preferences and cache',
+ otherOption2: 'Start the simple client',
+ otherOption3: 'Start the repair tool',
+ otherOption4: 'Start Misskey in safe mode',
+ reload: 'Reload',
+ }, messages);
+
+ const safeModeUrl = new URL(window.location.href);
+ safeModeUrl.searchParams.set('safemode', 'true');
+
+ let errorsElement = document.getElementById('errors');
+
+ if (!errorsElement) {
+ document.body.innerHTML = `
+ <svg class="icon-warning" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round">
+ <path stroke="none" d="M0 0h24v24H0z" fill="none"></path>
+ <path d="M12 9v2m0 4v.01"></path>
+ <path d="M5 19h14a2 2 0 0 0 1.84 -2.75l-7.1 -12.25a2 2 0 0 0 -3.5 0l-7.1 12.25a2 2 0 0 0 1.75 2.75"></path>
+ </svg>
+ <h1>${messages.title}</h1>
+ <button class="button-big" onclick="location.reload(true);">
+ <span class="button-label-big">${messages?.reload}</span>
+ </button>
+ <p><b>${messages.solution}</b></p>
+ <p>${messages.solution1}</p>
+ <p>${messages.solution2}</p>
+ <p>${messages.solution3}</p>
+ <p>${messages.solution4}</p>
+ <details style="color: #86b300;">
+ <summary>${messages.otherOption}</summary>
+ <a href="${safeModeUrl}">
+ <button class="button-small">
+ <span class="button-label-small">${messages.otherOption4}</span>
+ </button>
+ </a>
+ <br>
+ <a href="/flush">
+ <button class="button-small">
+ <span class="button-label-small">${messages.otherOption1}</span>
+ </button>
+ </a>
+ <br>
+ <a href="/cli">
+ <button class="button-small">
+ <span class="button-label-small">${messages.otherOption2}</span>
+ </button>
+ </a>
+ <br>
+ <a href="/bios">
+ <button class="button-small">
+ <span class="button-label-small">${messages.otherOption3}</span>
+ </button>
+ </a>
+ </details>
+ <br>
+ <div id="errors"></div>
+ `;
+ errorsElement = document.getElementById('errors');
+ }
+ const detailsElement = document.createElement('details');
+ detailsElement.id = 'errorInfo';
+ detailsElement.innerHTML = `
+ <br>
+ <summary>
+ <code>ERROR CODE: ${code}</code>
+ </summary>
+ <code>${details.toString()} ${JSON.stringify(details)}</code>`;
+ errorsElement.appendChild(detailsElement);
+ addStyle(`
+ * {
+ font-family: BIZ UDGothic, Roboto, HelveticaNeue, Arial, sans-serif;
+ }
+
+ #misskey_app,
+ #splash {
+ display: none !important;
+ }
+
+ body,
+ html {
+ background-color: #222;
+ color: #dfddcc;
+ justify-content: center;
+ margin: auto;
+ padding: 10px;
+ text-align: center;
+ }
+
+ button {
+ border-radius: 999px;
+ padding: 0px 12px 0px 12px;
+ border: none;
+ cursor: pointer;
+ margin-bottom: 12px;
+ }
+
+ .button-big {
+ background: linear-gradient(90deg, rgb(134, 179, 0), rgb(74, 179, 0));
+ line-height: 50px;
+ }
+
+ .button-big:hover {
+ background: rgb(153, 204, 0);
+ }
+
+ .button-small {
+ background: #444;
+ line-height: 40px;
+ }
+
+ .button-small:hover {
+ background: #555;
+ }
+
+ .button-label-big {
+ color: #222;
+ font-weight: bold;
+ font-size: 1.2em;
+ padding: 12px;
+ }
+
+ .button-label-small {
+ color: rgb(153, 204, 0);
+ font-size: 16px;
+ padding: 12px;
+ }
+
+ a {
+ color: rgb(134, 179, 0);
+ text-decoration: none;
+ }
+
+ p,
+ li {
+ font-size: 16px;
+ }
+
+ .icon-warning {
+ color: #dec340;
+ height: 4rem;
+ padding-top: 2rem;
+ }
+
+ h1 {
+ font-size: 1.5em;
+ margin: 1em;
+ }
+
+ code {
+ font-family: Fira, FiraCode, monospace;
+ }
+
+ #errorInfo {
+ background: #333;
+ margin-bottom: 2rem;
+ padding: 0.5rem 1rem;
+ width: 40rem;
+ border-radius: 10px;
+ justify-content: center;
+ margin: auto;
+ }
+
+ #errorInfo summary {
+ cursor: pointer;
+ }
+
+ #errorInfo summary > * {
+ display: inline;
+ }
+
+ @media screen and (max-width: 500px) {
+ #errorInfo {
+ width: 50%;
+ }
+ }`);
+ }
+})();
diff --git a/packages/frontend/public/loader/style.css b/packages/frontend/public/loader/style.css
new file mode 100644
index 0000000000..8e63a2ea66
--- /dev/null
+++ b/packages/frontend/public/loader/style.css
@@ -0,0 +1,78 @@
+/*
+ * SPDX-FileCopyrightText: syuilo and misskey-project
+ *
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+html {
+ background-color: var(--MI_THEME-bg);
+ color: var(--MI_THEME-fg);
+}
+
+#splash {
+ position: fixed;
+ z-index: 10000;
+ top: 0;
+ left: 0;
+ width: 100vw;
+ height: 100vh;
+ cursor: wait;
+ background-color: var(--MI_THEME-bg);
+ opacity: 1;
+ transition: opacity 0.5s ease;
+}
+
+#splashIcon {
+ position: absolute;
+ top: 0;
+ right: 0;
+ bottom: 0;
+ left: 0;
+ margin: auto;
+ width: 64px;
+ height: 64px;
+ border-radius: 10px;
+ pointer-events: none;
+}
+
+#splashSpinner {
+ position: absolute;
+ top: 0;
+ right: 0;
+ bottom: 0;
+ left: 0;
+ margin: auto;
+ display: inline-block;
+ width: 28px;
+ height: 28px;
+ transform: translateY(70px);
+ color: var(--MI_THEME-accent);
+}
+
+#splashSpinner > .spinner {
+ position: absolute;
+ top: 0;
+ left: 0;
+ width: 28px;
+ height: 28px;
+ fill-rule: evenodd;
+ clip-rule: evenodd;
+ stroke-linecap: round;
+ stroke-linejoin: round;
+ stroke-miterlimit: 1.5;
+}
+#splashSpinner > .spinner.bg {
+ opacity: 0.275;
+}
+#splashSpinner > .spinner.fg {
+ animation: splashSpinner 0.5s linear infinite;
+}
+
+@keyframes splashSpinner {
+ 0% {
+ transform: rotate(0deg);
+ }
+ 100% {
+ transform: rotate(360deg);
+ }
+}
diff --git a/packages/frontend/src/components/MkMediaImage.vue b/packages/frontend/src/components/MkMediaImage.vue
index 99ea606a11..f59d15d9a2 100644
--- a/packages/frontend/src/components/MkMediaImage.vue
+++ b/packages/frontend/src/components/MkMediaImage.vue
@@ -233,16 +233,18 @@ function showMenu(ev: MouseEvent) {
.hide {
display: block;
position: absolute;
- border-radius: 6px;
- background-color: var(--MI_THEME-fg);
- color: hsl(from var(--MI_THEME-accent) h s calc(l + 10));
+ background-color: rgba(0, 0, 0, 0.3);
+ -webkit-backdrop-filter: var(--MI-blur, blur(15px));
+ backdrop-filter: var(--MI-blur, blur(15px));
+ border-radius: 0 0 0 9px;
+ color: #fff;
font-size: 12px;
opacity: .5;
padding: 5px 8px;
text-align: center;
cursor: pointer;
- top: 12px;
- right: 12px;
+ top: 0;
+ right: 0;
}
.hiddenTextWrapper {
@@ -272,17 +274,17 @@ html[data-color-scheme=light] .visible {
.menu {
display: block;
position: absolute;
- border-radius: 999px;
background-color: rgba(0, 0, 0, 0.3);
-webkit-backdrop-filter: var(--MI-blur, blur(15px));
backdrop-filter: var(--MI-blur, blur(15px));
+ border-radius: 9px 0 0 0;
color: #fff;
font-size: 0.8em;
width: 28px;
height: 28px;
text-align: center;
- bottom: 10px;
- right: 10px;
+ bottom: 0;
+ right: 0;
}
.imageContainer {
diff --git a/packages/frontend/src/components/global/I18n.vue b/packages/frontend/src/components/global/I18n.vue
index 6b7723e6ac..9866e50958 100644
--- a/packages/frontend/src/components/global/I18n.vue
+++ b/packages/frontend/src/components/global/I18n.vue
@@ -9,7 +9,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<script setup lang="ts" generic="T extends string | ParameterizedString">
import { computed, h } from 'vue';
-import type { ParameterizedString } from '../../../../../locales/index.js';
+import type { ParameterizedString } from 'i18n';
const props = withDefaults(defineProps<{
src: T;
@@ -25,7 +25,7 @@ const slots = defineSlots<T extends ParameterizedString<infer R> ? { [K in R]: (
const parsed = computed(() => {
let str = props.src as string;
const value: (string | { arg: string; })[] = [];
- for (;;) {
+ for (; ;) {
const nextBracketOpen = str.indexOf('{');
const nextBracketClose = str.indexOf('}');
diff --git a/packages/frontend/src/components/global/StackingRouterView.vue b/packages/frontend/src/components/global/StackingRouterView.vue
index 4c56767608..d52dd9b89d 100644
--- a/packages/frontend/src/components/global/StackingRouterView.vue
+++ b/packages/frontend/src/components/global/StackingRouterView.vue
@@ -74,6 +74,7 @@ function mount() {
}
function back() {
+ if (tabs.value.length <= 1) return; // transitionの関係でタブが1つの状態でbackが呼ばれることがある
const prev = tabs.value[tabs.value.length - 2];
tabs.value = [...tabs.value.slice(0, tabs.value.length - 1)];
router?.replaceByPath(prev.fullPath);
diff --git a/packages/frontend/src/i18n.ts b/packages/frontend/src/i18n.ts
index 0b2b206b7e..6a3a18df17 100644
--- a/packages/frontend/src/i18n.ts
+++ b/packages/frontend/src/i18n.ts
@@ -6,7 +6,7 @@
import { markRaw } from 'vue';
import { I18n } from '@@/js/i18n.js';
import { locale } from '@@/js/locale.js';
-import type { Locale } from '../../../locales/index.js';
+import type { Locale } from 'i18n';
export const i18n = markRaw(new I18n<Locale>(locale, _DEV_));
diff --git a/packages/frontend/src/theme.ts b/packages/frontend/src/theme.ts
index fc3f5f55f2..e001bed8f3 100644
--- a/packages/frontend/src/theme.ts
+++ b/packages/frontend/src/theme.ts
@@ -144,7 +144,9 @@ export function applyTheme(theme: Theme, persist = true) {
if (theme.id === currentThemeId && miLocalStorage.getItem('themeCachedVersion') === version) return;
currentThemeId = theme.id;
- if (window.document.startViewTransition != null) {
+ // visibilityStateがhiddenな状態でstartViewTransitionするとブラウザによってはエラーになる
+ // 通常hiddenな時に呼ばれることはないが、iOSのPWAだとアプリ切り替え時に(何故か)hiddenな状態で(何故か)一瞬デバイスのダークモード判定が変わりapplyThemeが呼ばれる場合がある
+ if (window.document.startViewTransition != null && window.document.visibilityState === 'visible') {
window.document.documentElement.classList.add('_themeChanging_');
try {
window.document.startViewTransition(async () => {
diff --git a/packages/frontend/test/aiscript/api.test.ts b/packages/frontend/test/aiscript/api.test.ts
index 34f880286c..292ec7ca77 100644
--- a/packages/frontend/test/aiscript/api.test.ts
+++ b/packages/frontend/test/aiscript/api.test.ts
@@ -8,7 +8,6 @@ import { aiScriptReadline, createAiScriptEnv } from '@/aiscript/api.js';
import { errors, Interpreter, Parser, values } from '@syuilo/aiscript';
import {
afterAll,
- afterEach,
beforeAll,
beforeEach,
describe,
@@ -80,8 +79,9 @@ describe('AiScript common API', () => {
});
describe('readline', () => {
- afterEach(() => {
+ beforeEach(() => {
vi.restoreAllMocks();
+ vi.clearAllMocks();
});
test.sequential('ok', async () => {
@@ -176,8 +176,9 @@ describe('AiScript common API', () => {
});
describe('dialog', () => {
- afterEach(() => {
+ beforeEach(() => {
vi.restoreAllMocks();
+ vi.clearAllMocks();
});
test.sequential('ok', async () => {
@@ -215,8 +216,9 @@ describe('AiScript common API', () => {
});
describe('confirm', () => {
- afterEach(() => {
+ beforeEach(() => {
vi.restoreAllMocks();
+ vi.clearAllMocks();
});
test.sequential('ok', async () => {
@@ -272,8 +274,9 @@ describe('AiScript common API', () => {
});
describe('api', () => {
- afterEach(() => {
+ beforeEach(() => {
vi.restoreAllMocks();
+ vi.clearAllMocks();
});
test.sequential('successful', async () => {
@@ -347,7 +350,7 @@ describe('AiScript common API', () => {
miLocalStorage.removeItem('aiscript:widget:key');
});
- afterEach(() => {
+ beforeEach(() => {
miLocalStorage.removeItem('aiscript:widget:key');
});
diff --git a/packages/frontend/test/i18n.test.ts b/packages/frontend/test/i18n.test.ts
index a51dfc6c4e..14b89f898d 100644
--- a/packages/frontend/test/i18n.test.ts
+++ b/packages/frontend/test/i18n.test.ts
@@ -5,7 +5,7 @@
import { describe, expect, it } from 'vitest';
import { I18n } from '../../frontend-shared/js/i18n.js'; // @@で参照できなかったので
-import type { ParameterizedString } from '../../../locales/index.js';
+import type { ParameterizedString } from 'i18n';
// TODO: このテストはfrontend-sharedに移動する
diff --git a/packages/frontend/test/init.ts b/packages/frontend/test/init.ts
index e38338cf95..28848f6c2f 100644
--- a/packages/frontend/test/init.ts
+++ b/packages/frontend/test/init.ts
@@ -7,13 +7,13 @@ import { vi } from 'vitest';
import createFetchMock from 'vitest-fetch-mock';
import type { Ref } from 'vue';
import { ref } from 'vue';
+// Set i18n
+import locales from 'i18n';
+import { updateI18n } from '@/i18n.js';
const fetchMocker = createFetchMock(vi);
fetchMocker.enableMocks();
-// Set i18n
-import locales from '../../../locales/index.js';
-import { updateI18n } from '@/i18n.js';
updateI18n(locales['en-US']);
// XXX: misskey-js panics if WebSocket is not defined
diff --git a/packages/frontend/vite.config.ts b/packages/frontend/vite.config.ts
index 6f320e99c9..c9c20b23ea 100644
--- a/packages/frontend/vite.config.ts
+++ b/packages/frontend/vite.config.ts
@@ -2,18 +2,18 @@ import path from 'path';
import pluginReplace from '@rollup/plugin-replace';
import pluginVue from '@vitejs/plugin-vue';
import pluginGlsl from 'vite-plugin-glsl';
-import { defineConfig } from 'vite';
import type { UserConfig } from 'vite';
+import { defineConfig } from 'vite';
import * as yaml from 'js-yaml';
import { promises as fsp } from 'fs';
-import locales from '../../locales/index.js';
+import locales from 'i18n';
import meta from '../../package.json';
import packageInfo from './package.json' with { type: 'json' };
import pluginUnwindCssModuleClassName from './lib/rollup-plugin-unwind-css-module-class-name.js';
import pluginJson5 from './vite.json5.js';
-import pluginCreateSearchIndex from './lib/vite-plugin-create-search-index.js';
import type { Options as SearchIndexOptions } from './lib/vite-plugin-create-search-index.js';
+import pluginCreateSearchIndex from './lib/vite-plugin-create-search-index.js';
import pluginWatchLocales from './lib/vite-plugin-watch-locales.js';
import { pluginRemoveUnrefI18n } from '../frontend-builder/rollup-plugin-remove-unref-i18n.js';