1
0
mirror of https://github.com/misskey-dev/misskey.git synced 2026-05-20 12:55:45 +02:00

Merge branch 'develop' into copilot/add-user-mute-settings

This commit is contained in:
kakkokari-gtyih
2025-12-02 14:19:48 +09:00
180 changed files with 4058 additions and 3445 deletions

View File

@@ -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),

View File

@@ -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';

View File

@@ -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() {
})
}
});
},
};
},
};
}

View File

@@ -20,6 +20,7 @@
"@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",
@@ -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.1",
"mfm-js": "0.25.0",
"misskey-bubble-game": "workspace:*",
"misskey-js": "workspace:*",
@@ -69,7 +70,7 @@
"qr-scanner": "1.4.2",
"rollup": "4.53.3",
"sanitize-html": "2.17.0",
"sass": "1.94.1",
"sass": "1.94.2",
"shiki": "3.15.0",
"strict-event-emitter-types": "2.0.0",
"textarea-caret": "3.1.0",
@@ -80,7 +81,7 @@
"tsconfig-paths": "4.2.0",
"typescript": "5.9.3",
"v-code-diff": "1.13.1",
"vite": "7.2.2",
"vite": "7.2.4",
"vue": "3.5.24",
"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.0.8",
"@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.0.8",
"@storybook/react-vite": "10.0.8",
"@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.0.8",
"@storybook/vue3-vite": "10.0.8",
"@tabler/icons-webfont": "3.35.0",
"@testing-library/vue": "8.1.0",
"@types/canvas-confetti": "1.9.0",
@@ -119,14 +120,14 @@
"@types/ws": "8.18.1",
"@typescript-eslint/eslint-plugin": "8.47.0",
"@typescript-eslint/parser": "8.47.0",
"@vitest/coverage-v8": "3.2.4",
"@vitest/coverage-v8": "4.0.13",
"@vue/compiler-core": "3.5.24",
"@vue/runtime-core": "3.5.24",
"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.0",
"fast-glob": "3.3.3",
"happy-dom": "20.0.10",
"intersection-observer": "0.12.2",
@@ -139,16 +140,16 @@
"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.0.8",
"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.13",
"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"
}
}

View File

@@ -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%;
}
}`);
}
})();

View File

@@ -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);
}
}

View File

@@ -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('}');

View File

@@ -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_));

View File

@@ -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 () => {

View File

@@ -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');
});

View File

@@ -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に移動する

View File

@@ -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

View File

@@ -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';