summaryrefslogtreecommitdiff
path: root/packages
diff options
context:
space:
mode:
authormisskey-release-bot[bot] <157398866+misskey-release-bot[bot]@users.noreply.github.com>2025-12-22 05:30:45 +0000
committerGitHub <noreply@github.com>2025-12-22 05:30:45 +0000
commit0d46089f9a18abbb001fee2860dfaabf881831b3 (patch)
tree8315f33781b790084279680d05ea521f47fe1219 /packages
parentMerge pull request #16972 from misskey-dev/develop (diff)
parentRelease: 2025.12.2 (diff)
downloadmisskey-0d46089f9a18abbb001fee2860dfaabf881831b3.tar.gz
misskey-0d46089f9a18abbb001fee2860dfaabf881831b3.tar.bz2
misskey-0d46089f9a18abbb001fee2860dfaabf881831b3.zip
Merge pull request #16998 from misskey-dev/develop
Release: 2025.12.2
Diffstat (limited to 'packages')
-rw-r--r--packages/backend/package.json40
-rw-r--r--packages/backend/src/config.ts14
-rw-r--r--packages/backend/src/core/AiService.ts2
-rw-r--r--packages/backend/src/daemons/ServerStatsService.ts6
-rw-r--r--packages/backend/src/misc/show-machine-info.ts6
-rw-r--r--packages/backend/src/server/ServerService.ts2
-rw-r--r--packages/backend/src/server/api/ApiCallService.ts11
-rw-r--r--packages/backend/src/server/api/SigninApiService.ts31
-rw-r--r--packages/backend/src/server/api/SigninWithPasskeyApiService.ts28
-rw-r--r--packages/backend/src/server/api/endpoints/admin/queue/deliver-delayed.ts10
-rw-r--r--packages/backend/src/server/api/endpoints/admin/queue/inbox-delayed.ts10
-rw-r--r--packages/backend/src/server/api/endpoints/admin/server-info.ts3
-rw-r--r--packages/backend/src/server/api/endpoints/server-info.ts3
-rw-r--r--packages/frontend-builder/package.json8
-rw-r--r--packages/frontend-embed/package.json24
-rw-r--r--packages/frontend-shared/js/emojilist.ts25
-rw-r--r--packages/frontend-shared/package.json8
-rw-r--r--packages/frontend/package.json58
-rw-r--r--packages/frontend/src/components/MkImgPreviewDialog.vue2
-rw-r--r--packages/frontend/src/components/MkPostForm.vue16
-rw-r--r--packages/frontend/src/components/MkPostFormDialog.vue4
-rw-r--r--packages/frontend/src/components/global/MkAvatar.vue19
-rw-r--r--packages/frontend/src/pages/admin/performance.vue2
-rw-r--r--packages/frontend/src/pages/flash/flash.vue4
-rw-r--r--packages/frontend/src/pages/settings/index.vue6
-rw-r--r--packages/frontend/src/pages/settings/other.vue4
-rw-r--r--packages/frontend/src/pages/settings/privacy.vue4
-rw-r--r--packages/frontend/src/pages/settings/profile.vue2
-rw-r--r--packages/frontend/src/pages/settings/security.vue2
-rw-r--r--packages/frontend/src/preferences/manager.ts21
-rw-r--r--packages/frontend/src/store.ts4
-rw-r--r--packages/frontend/src/tips.ts1
-rw-r--r--packages/frontend/src/ui/_common_/navbar.vue11
-rw-r--r--packages/frontend/src/ui/deck.vue52
-rw-r--r--packages/frontend/src/ui/universal.vue15
-rw-r--r--packages/frontend/src/utility/storage.ts34
-rw-r--r--packages/i18n/package.json14
-rw-r--r--packages/i18n/src/autogen/locale.ts42
-rw-r--r--packages/icons-subsetter/package.json8
-rw-r--r--packages/misskey-bubble-game/package.json10
-rw-r--r--packages/misskey-js/generator/package.json8
-rw-r--r--packages/misskey-js/package.json18
-rw-r--r--packages/misskey-reversi/package.json10
-rw-r--r--packages/sw/package.json4
44 files changed, 387 insertions, 219 deletions
diff --git a/packages/backend/package.json b/packages/backend/package.json
index f49acff701..c7a8a6c223 100644
--- a/packages/backend/package.json
+++ b/packages/backend/package.json
@@ -71,25 +71,25 @@
"utf-8-validate": "6.0.5"
},
"dependencies": {
- "@aws-sdk/client-s3": "3.940.0",
- "@aws-sdk/lib-storage": "3.940.0",
+ "@aws-sdk/client-s3": "3.948.0",
+ "@aws-sdk/lib-storage": "3.948.0",
"@discordapp/twemoji": "16.0.1",
- "@fastify/accepts": "5.0.3",
- "@fastify/cors": "11.1.0",
+ "@fastify/accepts": "5.0.4",
+ "@fastify/cors": "11.2.0",
"@fastify/express": "4.0.2",
- "@fastify/http-proxy": "11.3.0",
+ "@fastify/http-proxy": "11.4.1",
"@fastify/multipart": "9.3.0",
"@fastify/static": "8.3.0",
"@kitajs/html": "4.2.11",
"@misskey-dev/sharp-read-bmp": "1.2.0",
"@misskey-dev/summaly": "5.2.5",
- "@napi-rs/canvas": "0.1.83",
+ "@napi-rs/canvas": "0.1.84",
"@nestjs/common": "11.1.9",
"@nestjs/core": "11.1.9",
"@nestjs/testing": "11.1.9",
"@peertube/http-signature": "1.7.0",
- "@sentry/node": "10.27.0",
- "@sentry/profiling-node": "10.27.0",
+ "@sentry/node": "10.29.0",
+ "@sentry/profiling-node": "10.29.0",
"@simplewebauthn/server": "13.2.2",
"@sinonjs/fake-timers": "15.0.0",
"@smithy/node-http-handler": "4.4.5",
@@ -104,11 +104,11 @@
"bcryptjs": "3.0.3",
"blurhash": "2.0.5",
"body-parser": "2.2.1",
- "bullmq": "5.65.0",
+ "bullmq": "5.65.1",
"cacheable-lookup": "7.0.0",
"chalk": "5.6.2",
"chalk-template": "1.1.2",
- "chokidar": "4.0.3",
+ "chokidar": "5.0.0",
"color-convert": "3.1.3",
"content-disposition": "1.0.1",
"date-fns": "4.1.0",
@@ -166,13 +166,13 @@
"slacc": "0.0.10",
"strict-event-emitter-types": "2.0.0",
"stringz": "2.1.0",
- "systeminformation": "5.27.11",
+ "systeminformation": "5.27.14",
"tinycolor2": "1.6.0",
"tmp": "0.2.5",
"tsc-alias": "1.8.16",
- "typeorm": "0.3.27",
+ "typeorm": "0.3.28",
"typescript": "5.9.3",
- "ulid": "3.0.1",
+ "ulid": "3.0.2",
"vary": "1.1.2",
"web-push": "3.6.7",
"ws": "8.18.3",
@@ -182,7 +182,7 @@
"@jest/globals": "29.7.0",
"@kitajs/ts-html-plugin": "4.1.3",
"@nestjs/platform-express": "11.1.9",
- "@sentry/vue": "10.27.0",
+ "@sentry/vue": "10.29.0",
"@simplewebauthn/types": "12.0.0",
"@swc/jest": "0.2.39",
"@types/accepts": "1.3.7",
@@ -196,7 +196,7 @@
"@types/jsonld": "1.5.15",
"@types/mime-types": "3.0.1",
"@types/ms": "2.1.0",
- "@types/node": "24.10.1",
+ "@types/node": "24.10.2",
"@types/nodemailer": "7.0.4",
"@types/oauth2orize": "1.11.5",
"@types/oauth2orize-pkce": "0.1.2",
@@ -207,7 +207,7 @@
"@types/rename": "1.0.7",
"@types/sanitize-html": "2.16.0",
"@types/semver": "7.7.1",
- "@types/simple-oauth2": "5.0.7",
+ "@types/simple-oauth2": "5.0.8",
"@types/sinonjs__fake-timers": "15.0.1",
"@types/supertest": "6.0.3",
"@types/tinycolor2": "1.4.6",
@@ -215,13 +215,13 @@
"@types/vary": "1.1.3",
"@types/web-push": "3.6.4",
"@types/ws": "8.18.1",
- "@typescript-eslint/eslint-plugin": "8.48.0",
- "@typescript-eslint/parser": "8.48.0",
+ "@typescript-eslint/eslint-plugin": "8.49.0",
+ "@typescript-eslint/parser": "8.49.0",
"aws-sdk-client-mock": "4.1.0",
"cbor": "10.0.11",
"cross-env": "10.1.0",
"eslint-plugin-import": "2.32.0",
- "execa": "9.6.0",
+ "execa": "9.6.1",
"fkill": "10.0.1",
"jest": "29.7.0",
"jest-mock": "29.7.0",
@@ -230,6 +230,6 @@
"pid-port": "2.0.0",
"simple-oauth2": "5.1.0",
"supertest": "7.1.4",
- "vite": "7.2.4"
+ "vite": "7.2.7"
}
}
diff --git a/packages/backend/src/config.ts b/packages/backend/src/config.ts
index f9852d3578..657d7869fa 100644
--- a/packages/backend/src/config.ts
+++ b/packages/backend/src/config.ts
@@ -30,6 +30,7 @@ type Source = {
socket?: string;
trustProxy?: FastifyServerOptions['trustProxy'];
chmodSocket?: string;
+ enableIpRateLimit?: boolean;
disableHsts?: boolean;
db: {
host: string;
@@ -120,8 +121,9 @@ export type Config = {
url: string;
port: number;
socket: string | undefined;
- trustProxy: FastifyServerOptions['trustProxy'];
+ trustProxy: NonNullable<FastifyServerOptions['trustProxy']>;
chmodSocket: string | undefined;
+ enableIpRateLimit: boolean;
disableHsts: boolean | undefined;
db: {
host: string;
@@ -263,9 +265,17 @@ export function loadConfig(): Config {
url: url.origin,
port: config.port ?? parseInt(process.env.PORT ?? '', 10),
socket: config.socket,
- trustProxy: config.trustProxy,
+ trustProxy: config.trustProxy ?? [
+ '10.0.0.0/8',
+ '172.16.0.0/12',
+ '192.168.0.0/16',
+ '127.0.0.1/32',
+ '::1/128',
+ 'fc00::/7',
+ ],
chmodSocket: config.chmodSocket,
disableHsts: config.disableHsts,
+ enableIpRateLimit: config.enableIpRateLimit ?? true,
host,
hostname,
scheme,
diff --git a/packages/backend/src/core/AiService.ts b/packages/backend/src/core/AiService.ts
index 7a005400bb..cbae280030 100644
--- a/packages/backend/src/core/AiService.ts
+++ b/packages/backend/src/core/AiService.ts
@@ -7,7 +7,6 @@ import * as fs from 'node:fs';
import { fileURLToPath } from 'node:url';
import { dirname } from 'node:path';
import { Injectable } from '@nestjs/common';
-import si from 'systeminformation';
import { Mutex } from 'async-mutex';
import fetch from 'node-fetch';
import { bindThis } from '@/decorators.js';
@@ -84,6 +83,7 @@ export class AiService {
@bindThis
private async getCpuFlags(): Promise<string[]> {
+ const si = await import('systeminformation');
const str = await si.cpuFlags();
return str.split(/\s+/);
}
diff --git a/packages/backend/src/daemons/ServerStatsService.ts b/packages/backend/src/daemons/ServerStatsService.ts
index d229efb123..a972e5861c 100644
--- a/packages/backend/src/daemons/ServerStatsService.ts
+++ b/packages/backend/src/daemons/ServerStatsService.ts
@@ -4,13 +4,12 @@
*/
import { Inject, Injectable } from '@nestjs/common';
-import si from 'systeminformation';
import Xev from 'xev';
import * as osUtils from 'os-utils';
import { bindThis } from '@/decorators.js';
-import type { OnApplicationShutdown } from '@nestjs/common';
import { MiMeta } from '@/models/_.js';
import { DI } from '@/di-symbols.js';
+import type { OnApplicationShutdown } from '@nestjs/common';
const ev = new Xev();
@@ -97,12 +96,14 @@ function cpuUsage(): Promise<number> {
// MEMORY STAT
async function mem() {
+ const si = await import('systeminformation');
const data = await si.mem();
return data;
}
// NETWORK STAT
async function net() {
+ const si = await import('systeminformation');
const iface = await si.networkInterfaceDefault();
const data = await si.networkStats(iface);
return data[0];
@@ -110,5 +111,6 @@ async function net() {
// FS STAT
async function fs() {
+ const si = await import('systeminformation');
return await si.disksIO().catch(() => ({ rIO_sec: 0, wIO_sec: 0 }));
}
diff --git a/packages/backend/src/misc/show-machine-info.ts b/packages/backend/src/misc/show-machine-info.ts
index 8ddec35f23..b279eb9546 100644
--- a/packages/backend/src/misc/show-machine-info.ts
+++ b/packages/backend/src/misc/show-machine-info.ts
@@ -4,15 +4,11 @@
*/
import * as os from 'node:os';
-import sysUtils from 'systeminformation';
import type Logger from '@/logger.js';
export async function showMachineInfo(parentLogger: Logger) {
const logger = parentLogger.createSubLogger('machine');
logger.debug(`Hostname: ${os.hostname()}`);
logger.debug(`Platform: ${process.platform} Arch: ${process.arch}`);
- const mem = await sysUtils.mem();
- const totalmem = (mem.total / 1024 / 1024 / 1024).toFixed(1);
- const availmem = (mem.available / 1024 / 1024 / 1024).toFixed(1);
- logger.debug(`CPU: ${os.cpus().length} core MEM: ${totalmem}GB (available: ${availmem}GB)`);
+ logger.debug(`CPU: ${os.cpus().length} core MEM: ${(os.totalmem() / 1024 / 1024 / 1024).toFixed(1)}GB (available: ${(os.freemem() / 1024 / 1024 / 1024).toFixed(1)}GB)`);
}
diff --git a/packages/backend/src/server/ServerService.ts b/packages/backend/src/server/ServerService.ts
index 4e05322b12..ef9ac81f95 100644
--- a/packages/backend/src/server/ServerService.ts
+++ b/packages/backend/src/server/ServerService.ts
@@ -75,7 +75,7 @@ export class ServerService implements OnApplicationShutdown {
@bindThis
public async launch(): Promise<void> {
const fastify = Fastify({
- trustProxy: this.config.trustProxy ?? false,
+ trustProxy: this.config.trustProxy,
logger: false,
});
this.#fastify = fastify;
diff --git a/packages/backend/src/server/api/ApiCallService.ts b/packages/backend/src/server/api/ApiCallService.ts
index 27c79ab438..8bae46d9fb 100644
--- a/packages/backend/src/server/api/ApiCallService.ts
+++ b/packages/backend/src/server/api/ApiCallService.ts
@@ -313,11 +313,14 @@ export class ApiCallService implements OnApplicationShutdown {
}
if (ep.meta.limit) {
- // koa will automatically load the `X-Forwarded-For` header if `proxy: true` is configured in the app.
- let limitActor: string;
+ let limitActor: string | null = null;
if (user) {
limitActor = user.id;
- } else {
+ } else if (this.config.enableIpRateLimit) {
+ if (process.env.NODE_ENV === 'production' && (request.ip === '::1' || request.ip === '127.0.0.1')) {
+ this.logger.warn('Recieved API request from localhost IP address for rate limiting in production environment. This is likely due to an improper trustProxy setting in the config file.');
+ }
+
limitActor = getIpHash(request.ip);
}
@@ -330,7 +333,7 @@ export class ApiCallService implements OnApplicationShutdown {
// TODO: 毎リクエスト計算するのもあれだしキャッシュしたい
const factor = user ? (await this.roleService.getUserPolicies(user.id)).rateLimitFactor : 1;
- if (factor > 0) {
+ if (limitActor != null && factor > 0) {
// Rate limit
const rateLimit = await this.rateLimiterService.limit(limit as IEndpointMeta['limit'] & { key: NonNullable<string> }, limitActor, factor);
if (rateLimit != null) {
diff --git a/packages/backend/src/server/api/SigninApiService.ts b/packages/backend/src/server/api/SigninApiService.ts
index 3e889372d8..00e8828242 100644
--- a/packages/backend/src/server/api/SigninApiService.ts
+++ b/packages/backend/src/server/api/SigninApiService.ts
@@ -15,6 +15,7 @@ import type {
UserSecurityKeysRepository,
UsersRepository,
} from '@/models/_.js';
+import type Logger from '@/logger.js';
import type { Config } from '@/config.js';
import { getIpHash } from '@/misc/get-ip-hash.js';
import type { MiLocalUser } from '@/models/User.js';
@@ -23,6 +24,7 @@ import { bindThis } from '@/decorators.js';
import { WebAuthnService } from '@/core/WebAuthnService.js';
import { UserAuthService } from '@/core/UserAuthService.js';
import { CaptchaService } from '@/core/CaptchaService.js';
+import { LoggerService } from '@/core/LoggerService.js';
import { FastifyReplyError } from '@/misc/fastify-reply-error.js';
import { RateLimiterService } from './RateLimiterService.js';
import { SigninService } from './SigninService.js';
@@ -31,6 +33,8 @@ import type { FastifyReply, FastifyRequest } from 'fastify';
@Injectable()
export class SigninApiService {
+ private logger: Logger;
+
constructor(
@Inject(DI.config)
private config: Config,
@@ -50,6 +54,7 @@ export class SigninApiService {
@Inject(DI.signinsRepository)
private signinsRepository: SigninsRepository,
+ private loggerService: LoggerService,
private idService: IdService,
private rateLimiterService: RateLimiterService,
private signinService: SigninService,
@@ -57,6 +62,7 @@ export class SigninApiService {
private webAuthnService: WebAuthnService,
private captchaService: CaptchaService,
) {
+ this.logger = this.loggerService.getLogger('Signin');
}
@bindThis
@@ -90,16 +96,21 @@ export class SigninApiService {
}
// not more than 1 attempt per second and not more than 10 attempts per hour
- const rateLimit = await this.rateLimiterService.limit({ key: 'signin', duration: 60 * 60 * 1000, max: 10, minInterval: 1000 }, getIpHash(request.ip));
- if (rateLimit != null) {
- reply.code(429);
- return {
- error: {
- message: 'Too many failed attempts to sign in. Try again later.',
- code: 'TOO_MANY_AUTHENTICATION_FAILURES',
- id: '22d05606-fbcf-421a-a2db-b32610dcfd1b',
- },
- };
+ if (this.config.enableIpRateLimit) {
+ if (process.env.NODE_ENV === 'production' && (request.ip === '::1' || request.ip === '127.0.0.1')) {
+ this.logger.warn('Recieved signin request from localhost IP address for rate limiting in production environment. This is likely due to an improper trustProxy setting in the config file.');
+ }
+ const rateLimit = await this.rateLimiterService.limit({ key: 'signin', duration: 60 * 60 * 1000, max: 10, minInterval: 1000 }, getIpHash(request.ip));
+ if (rateLimit != null) {
+ reply.code(429);
+ return {
+ error: {
+ message: 'Too many failed attempts to sign in. Try again later.',
+ code: 'TOO_MANY_AUTHENTICATION_FAILURES',
+ id: '22d05606-fbcf-421a-a2db-b32610dcfd1b',
+ },
+ };
+ }
}
if (typeof username !== 'string') {
diff --git a/packages/backend/src/server/api/SigninWithPasskeyApiService.ts b/packages/backend/src/server/api/SigninWithPasskeyApiService.ts
index 9ba23c54e2..920f9d0b3a 100644
--- a/packages/backend/src/server/api/SigninWithPasskeyApiService.ts
+++ b/packages/backend/src/server/api/SigninWithPasskeyApiService.ts
@@ -84,19 +84,25 @@ export class SigninWithPasskeyApiService {
return error(status ?? 500, failure ?? { id: '4e30e80c-e338-45a0-8c8f-44455efa3b76' });
};
- try {
+ if (this.config.enableIpRateLimit) {
+ if (process.env.NODE_ENV === 'production' && (request.ip === '::1' || request.ip === '127.0.0.1')) {
+ this.logger.warn('Recieved signin with passkey request from localhost IP address for rate limiting in production environment. This is likely due to an improper trustProxy setting in the config file.');
+ }
+
+ try {
// Not more than 1 API call per 250ms and not more than 100 attempts per 30min
// NOTE: 1 Sign-in require 2 API calls
- await this.rateLimiterService.limit({ key: 'signin-with-passkey', duration: 60 * 30 * 1000, max: 200, minInterval: 250 }, getIpHash(request.ip));
- } catch (err) {
- reply.code(429);
- return {
- error: {
- message: 'Too many failed attempts to sign in. Try again later.',
- code: 'TOO_MANY_AUTHENTICATION_FAILURES',
- id: '22d05606-fbcf-421a-a2db-b32610dcfd1b',
- },
- };
+ await this.rateLimiterService.limit({ key: 'signin-with-passkey', duration: 60 * 30 * 1000, max: 200, minInterval: 250 }, getIpHash(request.ip));
+ } catch (err) {
+ reply.code(429);
+ return {
+ error: {
+ message: 'Too many failed attempts to sign in. Try again later.',
+ code: 'TOO_MANY_AUTHENTICATION_FAILURES',
+ id: '22d05606-fbcf-421a-a2db-b32610dcfd1b',
+ },
+ };
+ }
}
// Initiate Passkey Auth challenge with context
diff --git a/packages/backend/src/server/api/endpoints/admin/queue/deliver-delayed.ts b/packages/backend/src/server/api/endpoints/admin/queue/deliver-delayed.ts
index f3e440b4cb..86158d7e22 100644
--- a/packages/backend/src/server/api/endpoints/admin/queue/deliver-delayed.ts
+++ b/packages/backend/src/server/api/endpoints/admin/queue/deliver-delayed.ts
@@ -52,18 +52,14 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
super(meta, paramDef, async (ps, me) => {
const jobs = await this.deliverQueue.getJobs(['delayed']);
- const res = [] as [string, number][];
+ const counts = new Map<string, number>();
for (const job of jobs) {
const host = new URL(job.data.to).host;
- if (res.find(x => x[0] === host)) {
- res.find(x => x[0] === host)![1]++;
- } else {
- res.push([host, 1]);
- }
+ counts.set(host, (counts.get(host) ?? 0) + 1);
}
- res.sort((a, b) => b[1] - a[1]);
+ const res = [...counts.entries()].sort((a, b) => b[1] - a[1]);
return res;
});
diff --git a/packages/backend/src/server/api/endpoints/admin/queue/inbox-delayed.ts b/packages/backend/src/server/api/endpoints/admin/queue/inbox-delayed.ts
index e7589cba81..ad6a823b8f 100644
--- a/packages/backend/src/server/api/endpoints/admin/queue/inbox-delayed.ts
+++ b/packages/backend/src/server/api/endpoints/admin/queue/inbox-delayed.ts
@@ -52,18 +52,14 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
super(meta, paramDef, async (ps, me) => {
const jobs = await this.inboxQueue.getJobs(['delayed']);
- const res = [] as [string, number][];
+ const counts = new Map<string, number>();
for (const job of jobs) {
const host = new URL(job.data.signature.keyId).host;
- if (res.find(x => x[0] === host)) {
- res.find(x => x[0] === host)![1]++;
- } else {
- res.push([host, 1]);
- }
+ counts.set(host, (counts.get(host) ?? 0) + 1);
}
- res.sort((a, b) => b[1] - a[1]);
+ const res = [...counts.entries()].sort((a, b) => b[1] - a[1]);
return res;
});
diff --git a/packages/backend/src/server/api/endpoints/admin/server-info.ts b/packages/backend/src/server/api/endpoints/admin/server-info.ts
index 80b6a4d32e..603be514c8 100644
--- a/packages/backend/src/server/api/endpoints/admin/server-info.ts
+++ b/packages/backend/src/server/api/endpoints/admin/server-info.ts
@@ -4,7 +4,6 @@
*/
import * as os from 'node:os';
-import si from 'systeminformation';
import { Inject, Injectable } from '@nestjs/common';
import { DataSource } from 'typeorm';
import * as Redis from 'ioredis';
@@ -112,6 +111,8 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
) {
super(meta, paramDef, async () => {
+ const si = await import('systeminformation');
+
const memStats = await si.mem();
const fsStats = await si.fsSize();
const netInterface = await si.networkInterfaceDefault();
diff --git a/packages/backend/src/server/api/endpoints/server-info.ts b/packages/backend/src/server/api/endpoints/server-info.ts
index 8301c85f2e..0e8dc73ad9 100644
--- a/packages/backend/src/server/api/endpoints/server-info.ts
+++ b/packages/backend/src/server/api/endpoints/server-info.ts
@@ -4,7 +4,6 @@
*/
import * as os from 'node:os';
-import si from 'systeminformation';
import { Inject, Injectable } from '@nestjs/common';
import { Endpoint } from '@/server/api/endpoint-base.js';
import { MiMeta } from '@/models/_.js';
@@ -93,6 +92,8 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
},
};
+ const si = await import('systeminformation');
+
const memStats = await si.mem();
const fsStats = await si.fsSize();
diff --git a/packages/frontend-builder/package.json b/packages/frontend-builder/package.json
index 36c32b915d..c1d9e316e6 100644
--- a/packages/frontend-builder/package.json
+++ b/packages/frontend-builder/package.json
@@ -11,9 +11,9 @@
},
"devDependencies": {
"@types/estree": "1.0.8",
- "@types/node": "24.10.1",
- "@typescript-eslint/eslint-plugin": "8.48.0",
- "@typescript-eslint/parser": "8.48.0",
+ "@types/node": "24.10.2",
+ "@typescript-eslint/eslint-plugin": "8.49.0",
+ "@typescript-eslint/parser": "8.49.0",
"rollup": "4.53.3",
"typescript": "5.9.3"
},
@@ -21,6 +21,6 @@
"i18n": "workspace:*",
"estree-walker": "3.0.3",
"magic-string": "0.30.21",
- "vite": "7.2.4"
+ "vite": "7.2.7"
}
}
diff --git a/packages/frontend-embed/package.json b/packages/frontend-embed/package.json
index e82cdc1f27..808559f44a 100644
--- a/packages/frontend-embed/package.json
+++ b/packages/frontend-embed/package.json
@@ -26,11 +26,11 @@
"misskey-js": "workspace:*",
"punycode.js": "2.3.1",
"rollup": "4.53.3",
- "sass": "1.94.2",
- "shiki": "3.17.0",
+ "sass": "1.95.1",
+ "shiki": "3.19.0",
"tinycolor2": "1.6.0",
"uuid": "13.0.0",
- "vite": "7.2.4",
+ "vite": "7.2.7",
"vue": "3.5.25"
},
"devDependencies": {
@@ -39,13 +39,13 @@
"@testing-library/vue": "8.1.0",
"@types/estree": "1.0.8",
"@types/micromatch": "4.0.10",
- "@types/node": "24.10.1",
+ "@types/node": "24.10.2",
"@types/punycode.js": "npm:@types/punycode@2.1.4",
"@types/tinycolor2": "1.4.6",
"@types/ws": "8.18.1",
- "@typescript-eslint/eslint-plugin": "8.48.0",
- "@typescript-eslint/parser": "8.48.0",
- "@vitest/coverage-v8": "4.0.14",
+ "@typescript-eslint/eslint-plugin": "8.49.0",
+ "@typescript-eslint/parser": "8.49.0",
+ "@vitest/coverage-v8": "4.0.15",
"@vue/runtime-core": "3.5.25",
"acorn": "8.15.0",
"cross-env": "10.1.0",
@@ -54,15 +54,15 @@
"happy-dom": "20.0.11",
"intersection-observer": "0.12.2",
"micromatch": "4.0.8",
- "msw": "2.12.3",
+ "msw": "2.12.4",
"nodemon": "3.1.11",
- "prettier": "3.7.1",
+ "prettier": "3.7.4",
"start-server-and-test": "2.1.3",
- "tsx": "4.20.6",
+ "tsx": "4.21.0",
"typescript": "5.9.3",
"vite-plugin-turbosnap": "1.0.3",
- "vue-component-type-helpers": "3.1.5",
+ "vue-component-type-helpers": "3.1.8",
"vue-eslint-parser": "10.2.0",
- "vue-tsc": "3.1.5"
+ "vue-tsc": "3.1.8"
}
}
diff --git a/packages/frontend-shared/js/emojilist.ts b/packages/frontend-shared/js/emojilist.ts
index 20ddd0f7d7..1cee7173bd 100644
--- a/packages/frontend-shared/js/emojilist.ts
+++ b/packages/frontend-shared/js/emojilist.ts
@@ -41,7 +41,7 @@ export const emojiCharByCategory = _charGroupByCategory;
export function getUnicodeEmojiOrNull(char: string): UnicodeEmojiDef | null {
// Colorize it because emojilist.json assumes that
- return unicodeEmojisMap.get(colorizeEmoji(char))
+ return unicodeEmojisMap.get(forceColorizeEmoji(char))
// カラースタイル絵文字がjsonに無い場合はテキストスタイル絵文字にフォールバックする
?? unicodeEmojisMap.get(char)
// それでも見つからない場合はnullを返す
@@ -54,12 +54,12 @@ export function getUnicodeEmoji(char: string): UnicodeEmojiDef | string {
}
export function isSupportedEmoji(char: string): boolean {
- return unicodeEmojisMap.has(colorizeEmoji(char)) || unicodeEmojisMap.has(char);
+ return unicodeEmojisMap.has(forceColorizeEmoji(char)) || unicodeEmojisMap.has(char);
}
export function getEmojiName(char: string): string {
// Colorize it because emojilist.json assumes that
- const idx = _indexByChar.get(colorizeEmoji(char)) ?? _indexByChar.get(char);
+ const idx = _indexByChar.get(forceColorizeEmoji(char)) ?? _indexByChar.get(char);
if (idx === undefined) {
// 絵文字情報がjsonに無い場合は名前の取得が出来ないのでそのまま返すしか無い
return char;
@@ -72,7 +72,24 @@ export function getEmojiName(char: string): string {
* テキストスタイル絵文字(U+260Eなどの1文字で表現される絵文字)をカラースタイル絵文字に変換します(VS16:U+FE0Fを付与)。
*/
export function colorizeEmoji(char: string) {
- return char.length === 1 ? `${char}\uFE0F` : char;
+ // <文字列>.length はコードポイント数ではなくUTF-16コードユニット数を返すため、サロゲートペアを含む絵文字で誤動作する。
+ // そのため、配列に変換してコードポイント数を数える方法を取る。
+ return Array.from(char).length === 1 ? `${char}\uFE0F` : char;
+}
+
+/**
+ * 文字種にかかわらず、カラースタイル絵文字への変換を試みます(本ファイルにある検索プログラム用・フォールバックが必須)。
+ */
+function forceColorizeEmoji(char: string) {
+ // <文字列>.length はコードポイント数ではなくUTF-16コードユニット数を返すため、サロゲートペアを含む絵文字で誤動作する。
+ // そのため、配列に変換してコードポイント数を数える方法を取る。
+ const chars = Array.from(char);
+ if (chars.includes('\uFE0F')) {
+ return char;
+ } else {
+ chars.splice(1, 0, '\uFE0F');
+ return chars.join('');
+ }
}
export interface CustomEmojiFolderTree {
diff --git a/packages/frontend-shared/package.json b/packages/frontend-shared/package.json
index baf5c561d8..49cce0d707 100644
--- a/packages/frontend-shared/package.json
+++ b/packages/frontend-shared/package.json
@@ -21,10 +21,10 @@
"lint": "pnpm typecheck && pnpm eslint"
},
"devDependencies": {
- "@types/node": "24.10.1",
- "@typescript-eslint/eslint-plugin": "8.48.0",
- "@typescript-eslint/parser": "8.48.0",
- "esbuild": "0.27.0",
+ "@types/node": "24.10.2",
+ "@typescript-eslint/eslint-plugin": "8.49.0",
+ "@typescript-eslint/parser": "8.49.0",
+ "esbuild": "0.27.1",
"eslint-plugin-vue": "10.6.2",
"nodemon": "3.1.11",
"typescript": "5.9.3",
diff --git a/packages/frontend/package.json b/packages/frontend/package.json
index 68dc5bd656..730bf71789 100644
--- a/packages/frontend/package.json
+++ b/packages/frontend/package.json
@@ -19,18 +19,18 @@
"@analytics/google-analytics": "1.1.0",
"@discordapp/twemoji": "16.0.1",
"@github/webauthn-json": "2.1.1",
- "@mcaptcha/vanilla-glue": "0.1.0-alpha-3",
+ "@mcaptcha/vanilla-glue": "0.1.0-rc2",
"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.27.0",
+ "@sentry/vue": "10.29.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",
- "aiscript-vscode": "github:aiscript-dev/aiscript-vscode#v0.1.15",
+ "aiscript-vscode": "github:aiscript-dev/aiscript-vscode#v0.1.16",
"analytics": "0.8.19",
"broadcast-channel": "7.2.0",
"buraha": "0.0.1",
@@ -45,8 +45,8 @@
"cropperjs": "2.1.0",
"date-fns": "4.1.0",
"eventemitter3": "5.0.1",
- "execa": "9.6.0",
- "exifreader": "4.32.0",
+ "execa": "9.6.1",
+ "exifreader": "4.33.1",
"frontend-shared": "workspace:*",
"icons-subsetter": "workspace:*",
"idb-keyval": "6.2.2",
@@ -55,7 +55,7 @@
"is-file-animated": "1.0.2",
"json5": "2.2.3",
"matter-js": "0.20.0",
- "mediabunny": "1.25.3",
+ "mediabunny": "1.25.8",
"mfm-js": "0.25.0",
"misskey-bubble-game": "workspace:*",
"misskey-js": "workspace:*",
@@ -66,14 +66,14 @@
"qr-scanner": "1.4.2",
"rollup": "4.53.3",
"sanitize-html": "2.17.0",
- "sass": "1.94.2",
- "shiki": "3.17.0",
+ "sass": "1.95.1",
+ "shiki": "3.19.0",
"textarea-caret": "3.1.0",
"three": "0.181.2",
"throttle-debounce": "5.0.2",
"tinycolor2": "1.6.0",
"v-code-diff": "1.13.1",
- "vite": "7.2.4",
+ "vite": "7.2.7",
"vue": "3.5.25",
"vuedraggable": "next",
"wanakana": "5.3.1"
@@ -82,7 +82,7 @@
"@misskey-dev/summaly": "5.2.5",
"@storybook/addon-essentials": "8.6.14",
"@storybook/addon-interactions": "8.6.14",
- "@storybook/addon-links": "10.1.0",
+ "@storybook/addon-links": "10.1.5",
"@storybook/addon-mdx-gfm": "8.6.14",
"@storybook/addon-storysource": "8.6.14",
"@storybook/blocks": "8.6.14",
@@ -90,33 +90,33 @@
"@storybook/core-events": "8.6.14",
"@storybook/manager-api": "8.6.14",
"@storybook/preview-api": "8.6.14",
- "@storybook/react": "10.1.0",
- "@storybook/react-vite": "10.1.0",
+ "@storybook/react": "10.1.5",
+ "@storybook/react-vite": "10.1.5",
"@storybook/test": "8.6.14",
"@storybook/theming": "8.6.14",
"@storybook/types": "8.6.14",
- "@storybook/vue3": "10.1.0",
- "@storybook/vue3-vite": "10.1.0",
+ "@storybook/vue3": "10.1.5",
+ "@storybook/vue3-vite": "10.1.5",
"@tabler/icons-webfont": "3.35.0",
"@testing-library/vue": "8.1.0",
"@types/canvas-confetti": "1.9.0",
"@types/estree": "1.0.8",
"@types/matter-js": "0.20.2",
"@types/micromatch": "4.0.10",
- "@types/node": "24.10.1",
+ "@types/node": "24.10.2",
"@types/punycode.js": "npm:@types/punycode@2.1.4",
"@types/sanitize-html": "2.16.0",
"@types/seedrandom": "3.0.8",
"@types/throttle-debounce": "5.0.2",
"@types/tinycolor2": "1.4.6",
- "@typescript-eslint/eslint-plugin": "8.48.0",
- "@typescript-eslint/parser": "8.48.0",
- "@vitest/coverage-v8": "4.0.14",
+ "@typescript-eslint/eslint-plugin": "8.49.0",
+ "@typescript-eslint/parser": "8.49.0",
+ "@vitest/coverage-v8": "4.0.15",
"@vue/compiler-core": "3.5.25",
"acorn": "8.15.0",
"astring": "1.9.0",
"cross-env": "10.1.0",
- "cypress": "15.7.0",
+ "cypress": "15.7.1",
"eslint-plugin-import": "2.32.0",
"eslint-plugin-vue": "10.6.2",
"estree-walker": "3.0.3",
@@ -125,24 +125,24 @@
"magic-string": "0.30.21",
"micromatch": "4.0.8",
"minimatch": "10.1.1",
- "msw": "2.12.3",
+ "msw": "2.12.4",
"msw-storybook-addon": "2.0.6",
"nodemon": "3.1.11",
- "prettier": "3.7.1",
- "react": "19.2.0",
- "react-dom": "19.2.0",
+ "prettier": "3.7.4",
+ "react": "19.2.1",
+ "react-dom": "19.2.1",
"seedrandom": "3.0.5",
"start-server-and-test": "2.1.3",
- "storybook": "10.1.0",
+ "storybook": "10.1.5",
"storybook-addon-misskey-theme": "github:misskey-dev/storybook-addon-misskey-theme",
- "tsx": "4.20.6",
+ "tsx": "4.21.0",
"typescript": "5.9.3",
- "vite-plugin-glsl": "1.5.4",
+ "vite-plugin-glsl": "1.5.5",
"vite-plugin-turbosnap": "1.0.3",
- "vitest": "4.0.14",
+ "vitest": "4.0.15",
"vitest-fetch-mock": "0.4.5",
- "vue-component-type-helpers": "3.1.5",
+ "vue-component-type-helpers": "3.1.8",
"vue-eslint-parser": "10.2.0",
- "vue-tsc": "3.1.5"
+ "vue-tsc": "3.1.8"
}
}
diff --git a/packages/frontend/src/components/MkImgPreviewDialog.vue b/packages/frontend/src/components/MkImgPreviewDialog.vue
index 3e6e4e0ec9..e17a1651cf 100644
--- a/packages/frontend/src/components/MkImgPreviewDialog.vue
+++ b/packages/frontend/src/components/MkImgPreviewDialog.vue
@@ -19,7 +19,7 @@ SPDX-License-Identifier: AGPL-3.0-only
</MkModalWindow>
</template>
<script lang="ts" setup>
-import { defineProps, ref } from 'vue';
+import { ref } from 'vue';
import MkModalWindow from './MkModalWindow.vue';
import type * as Misskey from 'misskey-js';
diff --git a/packages/frontend/src/components/MkPostForm.vue b/packages/frontend/src/components/MkPostForm.vue
index 86557b12df..b3bcfcc137 100644
--- a/packages/frontend/src/components/MkPostForm.vue
+++ b/packages/frontend/src/components/MkPostForm.vue
@@ -5,7 +5,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<template>
<div
- :class="[$style.root, { [$style.modal]: modal, _popup: modal }]"
+ :class="[$style.root]"
@dragover.stop="onDragover"
@dragenter="onDragenter"
@dragleave="onDragleave"
@@ -114,7 +114,7 @@ SPDX-License-Identifier: AGPL-3.0-only
</template>
<script lang="ts" setup>
-import { inject, watch, nextTick, onMounted, defineAsyncComponent, provide, shallowRef, ref, computed, useTemplateRef, onUnmounted } from 'vue';
+import { watch, nextTick, onMounted, defineAsyncComponent, provide, shallowRef, ref, computed, useTemplateRef, onUnmounted } from 'vue';
import * as mfm from 'mfm-js';
import * as Misskey from 'misskey-js';
import insertTextAtCursor from 'insert-text-at-cursor';
@@ -161,8 +161,6 @@ import { closeTip } from '@/tips.js';
const $i = ensureSignin();
-const modal = inject(DI.inModal, false);
-
const props = withDefaults(defineProps<PostFormProps & {
fixed?: boolean;
autofocus?: boolean;
@@ -1447,13 +1445,6 @@ defineExpose({
.root {
position: relative;
container-type: inline-size;
-
- &.modal {
- width: 100%;
- max-width: 520px;
- overflow-x: clip;
- overflow-y: auto;
- }
}
//#region header
@@ -1722,7 +1713,8 @@ html[data-color-scheme=light] .preview {
min-width: 100%;
width: 100%;
min-height: 90px;
- height: 100%;
+ max-height: 500px;
+ field-sizing: content;
}
.textCount {
diff --git a/packages/frontend/src/components/MkPostFormDialog.vue b/packages/frontend/src/components/MkPostFormDialog.vue
index ba8d3a7210..a7cf8a37cf 100644
--- a/packages/frontend/src/components/MkPostFormDialog.vue
+++ b/packages/frontend/src/components/MkPostFormDialog.vue
@@ -14,6 +14,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<MkPostForm
ref="form"
:class="$style.form"
+ class="_popup"
v-bind="props"
autofocus
freezeAfterPosted
@@ -73,7 +74,8 @@ function onModalClosed() {
<style lang="scss" module>
.form {
- max-height: 100%;
+ width: 100%;
+ max-width: 520px;
margin: 0 auto auto auto;
}
</style>
diff --git a/packages/frontend/src/components/global/MkAvatar.vue b/packages/frontend/src/components/global/MkAvatar.vue
index c2548cc7be..e7208ed574 100644
--- a/packages/frontend/src/components/global/MkAvatar.vue
+++ b/packages/frontend/src/components/global/MkAvatar.vue
@@ -27,7 +27,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<template v-if="showDecoration">
<img
v-for="decoration in decorations ?? user.avatarDecorations"
- :class="[$style.decoration, { [$style.decorationBlink]: decoration.blink }]"
+ :class="[$style.decoration, { [$style.decorationBlink]: getDecorationIsBrink(decoration) }]"
:src="getDecorationUrl(decoration)"
:style="{
rotate: getDecorationAngle(decoration),
@@ -56,13 +56,16 @@ import { prefer } from '@/preferences.js';
const animation = ref(prefer.s.animation);
const squareAvatars = ref(prefer.s.squareAvatars);
+type Decoration = Misskey.entities.UserDetailed['avatarDecorations'][number];
+type DecorationEditorDecoration = Omit<Misskey.entities.UserDetailed['avatarDecorations'][number], 'id'> & { blink?: boolean; };
+
const props = withDefaults(defineProps<{
user: Misskey.entities.User;
target?: string | null;
link?: boolean;
preview?: boolean;
indicator?: boolean;
- decorations?: (Omit<Misskey.entities.UserDetailed['avatarDecorations'][number], 'id'> & { blink?: boolean; })[];
+ decorations?: DecorationEditorDecoration[];
forceShowDecoration?: boolean;
}>(), {
target: null,
@@ -93,27 +96,31 @@ function onClick(ev: MouseEvent): void {
emit('click', ev);
}
-function getDecorationUrl(decoration: Omit<Misskey.entities.UserDetailed['avatarDecorations'][number], 'id'>) {
+function getDecorationUrl(decoration: Decoration | DecorationEditorDecoration) {
if (prefer.s.disableShowingAnimatedImages || prefer.s.dataSaver.avatar) return getStaticImageUrl(decoration.url);
return decoration.url;
}
-function getDecorationAngle(decoration: Omit<Misskey.entities.UserDetailed['avatarDecorations'][number], 'id'>) {
+function getDecorationAngle(decoration: Decoration | DecorationEditorDecoration) {
const angle = decoration.angle ?? 0;
return angle === 0 ? undefined : `${angle * 360}deg`;
}
-function getDecorationScale(decoration: Omit<Misskey.entities.UserDetailed['avatarDecorations'][number], 'id'>) {
+function getDecorationScale(decoration: Decoration | DecorationEditorDecoration) {
const scaleX = decoration.flipH ? -1 : 1;
return scaleX === 1 ? undefined : `${scaleX} 1`;
}
-function getDecorationOffset(decoration: Omit<Misskey.entities.UserDetailed['avatarDecorations'][number], 'id'>) {
+function getDecorationOffset(decoration: Decoration | DecorationEditorDecoration) {
const offsetX = decoration.offsetX ?? 0;
const offsetY = decoration.offsetY ?? 0;
return offsetX === 0 && offsetY === 0 ? undefined : `${offsetX * 100}% ${offsetY * 100}%`;
}
+function getDecorationIsBrink(decoration: Decoration | DecorationEditorDecoration) {
+ return 'blink' in decoration && decoration.blink === true;
+}
+
const color = ref<string | undefined>();
watch(() => props.user.avatarBlurhash, () => {
diff --git a/packages/frontend/src/pages/admin/performance.vue b/packages/frontend/src/pages/admin/performance.vue
index c5f3c2d4f0..3ce8e05982 100644
--- a/packages/frontend/src/pages/admin/performance.vue
+++ b/packages/frontend/src/pages/admin/performance.vue
@@ -122,7 +122,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<SearchMarker>
<MkFolder :defaultOpen="true">
<template #icon><SearchIcon><i class="ti ti-bolt"></i></SearchIcon></template>
- <template #label><SearchLabel>Misskey® Reactions Boost Technology™ (RBT)</SearchLabel><span class="_beta">{{ i18n.ts.beta }}</span></template>
+ <template #label><SearchLabel>Misskey® Reactions Boost Technology™ (RBT)</SearchLabel></template>
<template v-if="rbtForm.savedState.enableReactionsBuffering" #suffix>Enabled</template>
<template v-else #suffix>Disabled</template>
<template v-if="rbtForm.modified.value" #footer>
diff --git a/packages/frontend/src/pages/flash/flash.vue b/packages/frontend/src/pages/flash/flash.vue
index 2913aaae64..efc9ee014f 100644
--- a/packages/frontend/src/pages/flash/flash.vue
+++ b/packages/frontend/src/pages/flash/flash.vue
@@ -193,7 +193,7 @@ function start() {
}
function getIsLegacy(version: string | null): boolean {
- if (version == null) return false;
+ if (version == null) return true;
try {
return compareVersions(version, '1.0.0') < 0;
} catch {
@@ -206,7 +206,7 @@ async function run() {
if (!flash.value) return;
const version = utils.getLangVersion(flash.value.script);
- const isLegacy = version != null && getIsLegacy(version);
+ const isLegacy = getIsLegacy(version);
const { Interpreter, Parser, values } = isLegacy ? (await import('@syuilo/aiscript-0-19-0') as any) : await import('@syuilo/aiscript');
diff --git a/packages/frontend/src/pages/settings/index.vue b/packages/frontend/src/pages/settings/index.vue
index 250c1735be..39c32d347f 100644
--- a/packages/frontend/src/pages/settings/index.vue
+++ b/packages/frontend/src/pages/settings/index.vue
@@ -11,6 +11,11 @@ SPDX-License-Identifier: AGPL-3.0-only
<div v-if="!narrow || currentPage?.route.name == null" class="nav">
<div class="_gaps_s">
<MkInfo v-if="emailNotConfigured" warn class="info">{{ i18n.ts.emailNotConfiguredWarning }} <MkA to="/settings/email" class="_link">{{ i18n.ts.configure }}</MkA></MkInfo>
+ <MkInfo v-if="!storagePersisted && store.r.showStoragePersistenceSuggestion.value" class="info">
+ <div>{{ i18n.ts._settings.settingsPersistence_description1 }}</div>
+ <div>{{ i18n.ts._settings.settingsPersistence_description2 }}</div>
+ <div><button class="_textButton" @click="enableStoragePersistence">{{ i18n.ts.enable }}</button> | <button class="_textButton" @click="skipStoragePersistence">{{ i18n.ts.skip }}</button></div>
+ </MkInfo>
<MkInfo v-if="!store.r.enablePreferencesAutoCloudBackup.value && store.r.showPreferencesAutoCloudBackupSuggestion.value" class="info">
<div>{{ i18n.ts._preferencesBackup.autoPreferencesBackupIsNotEnabledForThisDevice }}</div>
<div><button class="_textButton" @click="enableAutoBackup">{{ i18n.ts.enable }}</button> | <button class="_textButton" @click="skipAutoBackup">{{ i18n.ts.skip }}</button></div>
@@ -46,6 +51,7 @@ import { enableAutoBackup, getPreferencesProfileMenu } from '@/preferences/utili
import { store } from '@/store.js';
import { signout } from '@/signout.js';
import { genSearchIndexes } from '@/utility/inapp-search.js';
+import { enableStoragePersistence, storagePersisted, skipStoragePersistence } from '@/utility/storage.js';
const searchIndex = await import('search-index:settings').then(({ searchIndexes }) => genSearchIndexes(searchIndexes));
diff --git a/packages/frontend/src/pages/settings/other.vue b/packages/frontend/src/pages/settings/other.vue
index e6ee3bfb1c..d4097bde94 100644
--- a/packages/frontend/src/pages/settings/other.vue
+++ b/packages/frontend/src/pages/settings/other.vue
@@ -142,6 +142,8 @@ SPDX-License-Identifier: AGPL-3.0-only
<hr>
</template>
+ <MkButton v-if="!storagePersisted" @click="enableStoragePersistence">{{ i18n.ts._settings.settingsPersistence_title }}</MkButton>
+
<MkButton @click="forceCloudBackup">{{ i18n.ts._preferencesBackup.forceBackup }}</MkButton>
<FormSlot>
@@ -163,7 +165,7 @@ import MkKeyValue from '@/components/MkKeyValue.vue';
import MkButton from '@/components/MkButton.vue';
import FormSlot from '@/components/form/slot.vue';
import * as os from '@/os.js';
-import { misskeyApi } from '@/utility/misskey-api.js';
+import { enableStoragePersistence, storagePersisted, skipStoragePersistence } from '@/utility/storage.js';
import { ensureSignin } from '@/i.js';
import { i18n } from '@/i18n.js';
import { definePage } from '@/page.js';
diff --git a/packages/frontend/src/pages/settings/privacy.vue b/packages/frontend/src/pages/settings/privacy.vue
index c2e0b3fe41..edc71e5156 100644
--- a/packages/frontend/src/pages/settings/privacy.vue
+++ b/packages/frontend/src/pages/settings/privacy.vue
@@ -90,7 +90,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<SearchMarker :keywords="['lockdown']">
<FormSection>
- <template #label><SearchLabel>{{ i18n.ts.lockdown }}</SearchLabel><span class="_beta">{{ i18n.ts.beta }}</span></template>
+ <template #label><SearchLabel>{{ i18n.ts.lockdown }}</SearchLabel></template>
<div class="_gaps_m">
<SearchMarker :keywords="['login', 'signin']">
@@ -213,9 +213,9 @@ SPDX-License-Identifier: AGPL-3.0-only
<script lang="ts" setup>
import { ref, computed, watch } from 'vue';
+import type { MkSelectItem } from '@/components/MkSelect.vue';
import MkSwitch from '@/components/MkSwitch.vue';
import MkSelect from '@/components/MkSelect.vue';
-import type { MkSelectItem } from '@/components/MkSelect.vue';
import FormSection from '@/components/form/section.vue';
import { misskeyApi } from '@/utility/misskey-api.js';
import { i18n } from '@/i18n.js';
diff --git a/packages/frontend/src/pages/settings/profile.vue b/packages/frontend/src/pages/settings/profile.vue
index 89325dee63..7d3da470d6 100644
--- a/packages/frontend/src/pages/settings/profile.vue
+++ b/packages/frontend/src/pages/settings/profile.vue
@@ -107,7 +107,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<SearchMarker :keywords="['follow', 'message']">
<MkInput v-model="profile.followedMessage" :max="200" manualSave :mfmPreview="false">
- <template #label><SearchLabel>{{ i18n.ts._profile.followedMessage }}</SearchLabel><span class="_beta">{{ i18n.ts.beta }}</span></template>
+ <template #label><SearchLabel>{{ i18n.ts._profile.followedMessage }}</SearchLabel></template>
<template #caption>
<div><SearchText>{{ i18n.ts._profile.followedMessageDescription }}</SearchText></div>
<div>{{ i18n.ts._profile.followedMessageDescriptionForLockedAccount }}</div>
diff --git a/packages/frontend/src/pages/settings/security.vue b/packages/frontend/src/pages/settings/security.vue
index bc77c1f0af..30f97b095e 100644
--- a/packages/frontend/src/pages/settings/security.vue
+++ b/packages/frontend/src/pages/settings/security.vue
@@ -27,7 +27,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<SearchMarker :keywords="['signin', 'login', 'history', 'log']">
<FormSection>
<template #label><SearchLabel>{{ i18n.ts.signinHistory }}</SearchLabel></template>
- <MkPagination :paginator="paginator" withControl>
+ <MkPagination :paginator="paginator" withControl :forceDisableInfiniteScroll="true">
<template #default="{items}">
<div>
<div v-for="item in items" :key="item.id" v-panel class="timnmucd">
diff --git a/packages/frontend/src/preferences/manager.ts b/packages/frontend/src/preferences/manager.ts
index 5949ee71eb..13ba0000e4 100644
--- a/packages/frontend/src/preferences/manager.ts
+++ b/packages/frontend/src/preferences/manager.ts
@@ -257,20 +257,23 @@ export class PreferencesManager extends EventEmitter<PreferencesManagerEvents> {
this.rewriteRawState(key, v);
- this.emit('committed', {
- key,
- value: v,
- oldValue: this.s[key],
- });
-
const record = this.getMatchedRecordOf(key);
+ const _save = () => {
+ this.save();
+ this.emit('committed', {
+ key,
+ value: v,
+ oldValue: this.s[key],
+ });
+ };
+
if (parseScope(record[0]).account == null && isAccountDependentKey(key) && currentAccount != null) {
this.profile.preferences[key].push([makeScope({
server: host,
account: currentAccount.id,
}), v, {}]);
- this.save();
+ _save();
return;
}
@@ -278,12 +281,12 @@ export class PreferencesManager extends EventEmitter<PreferencesManagerEvents> {
this.profile.preferences[key].push([makeScope({
server: host,
}), v, {}]);
- this.save();
+ _save();
return;
}
record[1] = v;
- this.save();
+ _save();
if (record[2].sync) {
// awaitの必要なし
diff --git a/packages/frontend/src/store.ts b/packages/frontend/src/store.ts
index 073fbba0fb..fb9349c42f 100644
--- a/packages/frontend/src/store.ts
+++ b/packages/frontend/src/store.ts
@@ -118,6 +118,10 @@ export const store = markRaw(new Pizzax('base', {
where: 'device',
default: true,
},
+ showStoragePersistenceSuggestion: {
+ where: 'device',
+ default: true,
+ },
//#region TODO: そのうち消す (preferに移行済み)
defaultWithReplies: {
diff --git a/packages/frontend/src/tips.ts b/packages/frontend/src/tips.ts
index 8a58e2aa63..6ee7130ee9 100644
--- a/packages/frontend/src/tips.ts
+++ b/packages/frontend/src/tips.ts
@@ -12,6 +12,7 @@ export const TIPS = [
'clips',
'userLists',
'postForm',
+ 'deck',
'tl.home',
'tl.local',
'tl.social',
diff --git a/packages/frontend/src/ui/_common_/navbar.vue b/packages/frontend/src/ui/_common_/navbar.vue
index f37e7ae85e..c679ee7a92 100644
--- a/packages/frontend/src/ui/_common_/navbar.vue
+++ b/packages/frontend/src/ui/_common_/navbar.vue
@@ -14,6 +14,9 @@ SPDX-License-Identifier: AGPL-3.0-only
<i v-if="store.r.realtimeMode.value" class="ti ti-bolt ti-fw"></i>
<i v-else class="ti ti-bolt-off ti-fw"></i>
</button>
+ <button v-if="!iconOnly && showWidgetButton" v-tooltip.noDelay.right="i18n.ts.widgets" class="_button" :class="[$style.widget]" @click="() => emit('widgetButtonClick')">
+ <i class="ti ti-apps ti-fw"></i>
+ </button>
</div>
<div :class="$style.middle">
<MkA v-tooltip.noDelay.right="i18n.ts.timeline" :class="$style.item" :activeClass="$style.active" to="/" exact>
@@ -51,7 +54,7 @@ SPDX-License-Identifier: AGPL-3.0-only
</MkA>
</div>
<div :class="$style.bottom">
- <button v-if="showWidgetButton" v-tooltip.noDelay.right="i18n.ts.widgets" class="_button" :class="[$style.widget]" @click="() => emit('widgetButtonClick')">
+ <button v-if="iconOnly && showWidgetButton" v-tooltip.noDelay.right="i18n.ts.widgets" class="_button" :class="[$style.widget]" @click="() => emit('widgetButtonClick')">
<i class="ti ti-apps ti-fw"></i>
</button>
<button v-if="iconOnly" v-tooltip.noDelay.right="i18n.ts.realtimeMode" class="_button" :class="[$style.realtimeMode, store.r.realtimeMode.value ? $style.on : null]" @click="toggleRealtimeMode">
@@ -436,6 +439,12 @@ function menuEdit() {
}
}
+ .widget {
+ display: inline-block;
+ width: var(--top-height);
+ margin-left: auto;
+ }
+
.bottom {
position: sticky;
bottom: 0;
diff --git a/packages/frontend/src/ui/deck.vue b/packages/frontend/src/ui/deck.vue
index 0941c25467..484b7f277a 100644
--- a/packages/frontend/src/ui/deck.vue
+++ b/packages/frontend/src/ui/deck.vue
@@ -38,36 +38,39 @@ SPDX-License-Identifier: AGPL-3.0-only
@headerWheel="onWheel"
/>
</section>
- <div v-if="layout.length === 0" class="_panel" :class="$style.onboarding">
+ <div v-if="layout.length === 0" class="_panel _gaps" :class="$style.onboarding">
<div>{{ i18n.ts._deck.introduction }}</div>
<div>{{ i18n.ts._deck.introduction2 }}</div>
+ <MkInfo v-if="!store.r.tips.value.deck" closable @close="closeTip('deck')">
+ <button class="_textButton" @click="showTour">{{ i18n.ts._deck.showHowToUse }}</button>
+ </MkInfo>
</div>
</div>
<div v-if="prefer.r['deck.menuPosition'].value === 'right'" :class="$style.sideMenu">
<div :class="$style.sideMenuTop">
- <button v-tooltip.noDelay.left="`${i18n.ts._deck.profile}: ${prefer.s['deck.profile']}`" :class="$style.sideMenuButton" class="_button" @click="switchProfileMenu"><i class="ti ti-caret-down"></i></button>
+ <button ref="swicthProfileButtonEl" v-tooltip.noDelay.left="`${i18n.ts._deck.profile}: ${prefer.s['deck.profile']}`" :class="$style.sideMenuButton" class="_button" @click="switchProfileMenu"><i class="ti ti-caret-down"></i></button>
<button v-tooltip.noDelay.left="i18n.ts._deck.deleteProfile" :class="$style.sideMenuButton" class="_button" @click="deleteProfile"><i class="ti ti-trash"></i></button>
</div>
<div :class="$style.sideMenuMiddle">
- <button v-tooltip.noDelay.left="i18n.ts._deck.addColumn" :class="$style.sideMenuButton" class="_button" @click="addColumn"><i class="ti ti-plus"></i></button>
+ <button ref="addColumnButtonEl" v-tooltip.noDelay.left="i18n.ts._deck.addColumn" :class="$style.sideMenuButton" class="_button" @click="addColumn"><i class="ti ti-plus"></i></button>
</div>
<div :class="$style.sideMenuBottom">
- <button v-tooltip.noDelay.left="i18n.ts.settings" :class="$style.sideMenuButton" class="_button" @click="showSettings"><i class="ti ti-settings-2"></i></button>
+ <button ref="settingsButtonEl" v-tooltip.noDelay.left="i18n.ts.settings" :class="$style.sideMenuButton" class="_button" @click="showSettings"><i class="ti ti-settings-2"></i></button>
</div>
</div>
</div>
<div v-if="prefer.r['deck.menuPosition'].value === 'bottom'" :class="$style.bottomMenu">
<div :class="$style.bottomMenuLeft">
- <button v-tooltip.noDelay.left="`${i18n.ts._deck.profile}: ${prefer.s['deck.profile']}`" :class="$style.bottomMenuButton" class="_button" @click="switchProfileMenu"><i class="ti ti-caret-down"></i></button>
- <button v-tooltip.noDelay.left="i18n.ts._deck.deleteProfile" :class="$style.bottomMenuButton" class="_button" @click="deleteProfile"><i class="ti ti-trash"></i></button>
+ <button ref="swicthProfileButtonEl" v-tooltip.noDelay.top="`${i18n.ts._deck.profile}: ${prefer.s['deck.profile']}`" :class="$style.bottomMenuButton" class="_button" @click="switchProfileMenu"><i class="ti ti-caret-down"></i></button>
+ <button v-tooltip.noDelay.top="i18n.ts._deck.deleteProfile" :class="$style.bottomMenuButton" class="_button" @click="deleteProfile"><i class="ti ti-trash"></i></button>
</div>
<div :class="$style.bottomMenuMiddle">
- <button v-tooltip.noDelay.left="i18n.ts._deck.addColumn" :class="$style.bottomMenuButton" class="_button" @click="addColumn"><i class="ti ti-plus"></i></button>
+ <button ref="addColumnButtonEl" v-tooltip.noDelay.top="i18n.ts._deck.addColumn" :class="$style.bottomMenuButton" class="_button" @click="addColumn"><i class="ti ti-plus"></i></button>
</div>
<div :class="$style.bottomMenuRight">
- <button v-tooltip.noDelay.left="i18n.ts.settings" :class="$style.bottomMenuButton" class="_button" @click="showSettings"><i class="ti ti-settings-2"></i></button>
+ <button ref="settingsButtonEl" v-tooltip.noDelay.top="i18n.ts.settings" :class="$style.bottomMenuButton" class="_button" @click="showSettings"><i class="ti ti-settings-2"></i></button>
</div>
</div>
@@ -96,6 +99,7 @@ import { $i } from '@/i.js';
import { i18n } from '@/i18n.js';
import { deviceKind } from '@/utility/device-kind.js';
import { prefer } from '@/preferences.js';
+import { store } from '@/store.js';
import XMainColumn from '@/ui/deck/main-column.vue';
import XTlColumn from '@/ui/deck/tl-column.vue';
import XAntennaColumn from '@/ui/deck/antenna-column.vue';
@@ -107,10 +111,13 @@ import XMentionsColumn from '@/ui/deck/mentions-column.vue';
import XDirectColumn from '@/ui/deck/direct-column.vue';
import XRoleTimelineColumn from '@/ui/deck/role-timeline-column.vue';
import XChatColumn from '@/ui/deck/chat-column.vue';
+import MkInfo from '@/components/MkInfo.vue';
import { mainRouter } from '@/router.js';
import { columns, layout, columnTypes, switchProfileMenu, addColumn as addColumnToStore, deleteProfile as deleteProfile_ } from '@/deck.js';
import { shouldSuggestRestoreBackup } from '@/preferences/utility.js';
import { shouldSuggestReload } from '@/utility/reload-suggest.js';
+import { startTour } from '@/utility/tour.js';
+import { closeTip } from '@/tips.js';
const XStatusBars = defineAsyncComponent(() => import('@/ui/_common_/statusbars.vue'));
const XAnnouncements = defineAsyncComponent(() => import('@/ui/_common_/announcements.vue'));
@@ -163,6 +170,9 @@ function showSettings() {
}
const columnsEl = useTemplateRef('columnsEl');
+const addColumnButtonEl = useTemplateRef('addColumnButtonEl');
+const settingsButtonEl = useTemplateRef('settingsButtonEl');
+const swicthProfileButtonEl = useTemplateRef('swicthProfileButtonEl');
const addColumn = async (ev) => {
const { canceled, result: column } = await os.select({
@@ -218,6 +228,30 @@ async function deleteProfile() {
os.success();
}
+function showTour() {
+ if (addColumnButtonEl.value == null ||
+ settingsButtonEl.value == null ||
+ swicthProfileButtonEl.value == null) {
+ return;
+ }
+
+ startTour([{
+ element: addColumnButtonEl.value,
+ title: i18n.ts._deck._howToUse.addColumn_title,
+ description: i18n.ts._deck._howToUse.addColumn_description,
+ }, {
+ element: settingsButtonEl.value,
+ title: i18n.ts._deck._howToUse.settings_title,
+ description: i18n.ts._deck._howToUse.settings_description,
+ }, {
+ element: swicthProfileButtonEl.value,
+ title: i18n.ts._deck._howToUse.switchProfile_title,
+ description: i18n.ts._deck._howToUse.switchProfile_description,
+ }]).then(() => {
+ closeTip('deck');
+ });
+}
+
window.document.documentElement.style.overflowY = 'hidden';
window.document.documentElement.style.scrollBehavior = 'auto';
</script>
@@ -345,7 +379,7 @@ window.document.documentElement.style.scrollBehavior = 'auto';
}
.bottomMenuButton {
- display: block;
+ display: inline-block;
height: 100%;
aspect-ratio: 1;
}
diff --git a/packages/frontend/src/ui/universal.vue b/packages/frontend/src/ui/universal.vue
index 727fe08989..497ef72d04 100644
--- a/packages/frontend/src/ui/universal.vue
+++ b/packages/frontend/src/ui/universal.vue
@@ -8,7 +8,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<XTitlebar v-if="prefer.r.showTitlebar.value" style="flex-shrink: 0;"/>
<div :class="$style.nonTitlebarArea">
- <XSidebar v-if="!isMobile" :class="$style.sidebar" :showWidgetButton="!isDesktop" @widgetButtonClick="widgetsShowing = true"/>
+ <XSidebar v-if="!isMobile" :class="$style.sidebar" :showWidgetButton="!showWidgetsSide" @widgetButtonClick="widgetsShowing = true"/>
<div :class="[$style.contents, !isMobile && prefer.r.showTitlebar.value ? $style.withSidebarAndTitlebar : null]" @contextmenu.stop="onContextmenu">
<div>
@@ -22,7 +22,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<XMobileFooterMenu v-if="isMobile" ref="navFooter" v-model:drawerMenuShowing="drawerMenuShowing" v-model:widgetsShowing="widgetsShowing"/>
</div>
- <div v-if="isDesktop && !pageMetadata?.needWideArea" :class="$style.widgets">
+ <div v-if="showWidgetsSide && !pageMetadata?.needWideArea" :class="$style.widgets">
<XWidgets/>
</div>
</div>
@@ -64,7 +64,8 @@ const DESKTOP_THRESHOLD = 1100;
const MOBILE_THRESHOLD = 500;
// デスクトップでウィンドウを狭くしたときモバイルUIが表示されて欲しいことはあるので deviceKind === 'desktop' の判定は行わない
-const isDesktop = ref(window.innerWidth >= DESKTOP_THRESHOLD);
+const showWidgetsSide = window.innerWidth >= DESKTOP_THRESHOLD;
+
const isMobile = ref(deviceKind === 'smartphone' || window.innerWidth <= MOBILE_THRESHOLD);
window.addEventListener('resize', () => {
isMobile.value = deviceKind === 'smartphone' || window.innerWidth <= MOBILE_THRESHOLD;
@@ -102,14 +103,6 @@ if (window.innerWidth > 1024) {
}
}
-onMounted(() => {
- if (!isDesktop.value) {
- window.addEventListener('resize', () => {
- if (window.innerWidth >= DESKTOP_THRESHOLD) isDesktop.value = true;
- }, { passive: true });
- }
-});
-
const onContextmenu = (ev) => {
if (isLink(ev.target)) return;
if (['INPUT', 'TEXTAREA', 'IMG', 'VIDEO', 'CANVAS'].includes(ev.target.tagName) || ev.target.attributes['contenteditable']) return;
diff --git a/packages/frontend/src/utility/storage.ts b/packages/frontend/src/utility/storage.ts
new file mode 100644
index 0000000000..9df3a251e6
--- /dev/null
+++ b/packages/frontend/src/utility/storage.ts
@@ -0,0 +1,34 @@
+/*
+ * SPDX-FileCopyrightText: syuilo and misskey-project
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+import { computed, ref, shallowRef, watch, defineAsyncComponent } from 'vue';
+import * as os from '@/os.js';
+import { store } from '@/store.js';
+import { i18n } from '@/i18n.js';
+
+export const storagePersisted = ref(await navigator.storage.persisted());
+
+export async function enableStoragePersistence() {
+ try {
+ const persisted = await navigator.storage.persist();
+ if (persisted) {
+ storagePersisted.value = true;
+ } else {
+ os.alert({
+ type: 'error',
+ text: i18n.ts.somethingHappened,
+ });
+ }
+ } catch (err) {
+ os.alert({
+ type: 'error',
+ text: i18n.ts.somethingHappened,
+ });
+ }
+}
+
+export function skipStoragePersistence() {
+ store.set('showStoragePersistenceSuggestion', false);
+}
diff --git a/packages/i18n/package.json b/packages/i18n/package.json
index 0354c26d15..ac6c386995 100644
--- a/packages/i18n/package.json
+++ b/packages/i18n/package.json
@@ -29,14 +29,14 @@
],
"devDependencies": {
"@types/js-yaml": "4.0.9",
- "@types/node": "24.10.1",
- "@typescript-eslint/eslint-plugin": "8.47.0",
- "@typescript-eslint/parser": "8.47.0",
- "chokidar": "4.0.3",
- "esbuild": "0.27.0",
- "execa": "9.6.0",
+ "@types/node": "24.10.2",
+ "@typescript-eslint/eslint-plugin": "8.49.0",
+ "@typescript-eslint/parser": "8.49.0",
+ "chokidar": "5.0.0",
+ "esbuild": "0.27.1",
+ "execa": "9.6.1",
"nodemon": "3.1.11",
- "tsx": "4.20.6",
+ "tsx": "4.21.0",
"typescript": "5.9.3"
},
"dependencies": {
diff --git a/packages/i18n/src/autogen/locale.ts b/packages/i18n/src/autogen/locale.ts
index 8f94aab555..96a728da63 100644
--- a/packages/i18n/src/autogen/locale.ts
+++ b/packages/i18n/src/autogen/locale.ts
@@ -6193,6 +6193,18 @@ export interface Locale extends ILocale {
* アニメーション画像を有効にする
*/
"enableAnimatedImages": string;
+ /**
+ * 設定の永続化
+ */
+ "settingsPersistence_title": string;
+ /**
+ * 設定の永続化を有効にすると、設定情報が失われるのを防止できます。
+ */
+ "settingsPersistence_description1": string;
+ /**
+ * 環境によっては有効化できない場合があります。
+ */
+ "settingsPersistence_description2": string;
"_chat": {
/**
* 送信者の名前を表示
@@ -10936,6 +10948,36 @@ export interface Locale extends ILocale {
* プロファイル情報のデバイス間同期を有効にする
*/
"enableSyncBetweenDevicesForProfiles": string;
+ /**
+ * UIの説明を見る
+ */
+ "showHowToUse": string;
+ "_howToUse": {
+ /**
+ * カラム追加
+ */
+ "addColumn_title": string;
+ /**
+ * カラムの種類を選んで追加できます。
+ */
+ "addColumn_description": string;
+ /**
+ * UI設定
+ */
+ "settings_title": string;
+ /**
+ * デッキUIの詳細設定を行えます。
+ */
+ "settings_description": string;
+ /**
+ * プロファイル切り替え
+ */
+ "switchProfile_title": string;
+ /**
+ * UIのレイアウトをプロファイルとして保存し、いつでも切り替えられるようにできます。
+ */
+ "switchProfile_description": string;
+ };
"_columns": {
/**
* メイン
diff --git a/packages/icons-subsetter/package.json b/packages/icons-subsetter/package.json
index 597520ff36..8d52555288 100644
--- a/packages/icons-subsetter/package.json
+++ b/packages/icons-subsetter/package.json
@@ -11,15 +11,15 @@
"lint": "pnpm typecheck && pnpm eslint"
},
"devDependencies": {
- "@types/node": "24.10.1",
+ "@types/node": "24.10.2",
"@types/wawoff2": "1.0.2",
- "@typescript-eslint/eslint-plugin": "8.48.0",
- "@typescript-eslint/parser": "8.48.0"
+ "@typescript-eslint/eslint-plugin": "8.49.0",
+ "@typescript-eslint/parser": "8.49.0"
},
"dependencies": {
"@tabler/icons-webfont": "3.35.0",
"harfbuzzjs": "0.4.13",
- "tsx": "4.20.6",
+ "tsx": "4.21.0",
"typescript": "5.9.3",
"wawoff2": "2.0.1"
},
diff --git a/packages/misskey-bubble-game/package.json b/packages/misskey-bubble-game/package.json
index 978d77e0e4..3844740bf2 100644
--- a/packages/misskey-bubble-game/package.json
+++ b/packages/misskey-bubble-game/package.json
@@ -25,12 +25,12 @@
},
"devDependencies": {
"@types/matter-js": "0.20.2",
- "@types/node": "24.10.1",
+ "@types/node": "24.10.2",
"@types/seedrandom": "3.0.8",
- "@typescript-eslint/eslint-plugin": "8.48.0",
- "@typescript-eslint/parser": "8.48.0",
- "esbuild": "0.27.0",
- "execa": "9.6.0",
+ "@typescript-eslint/eslint-plugin": "8.49.0",
+ "@typescript-eslint/parser": "8.49.0",
+ "esbuild": "0.27.1",
+ "execa": "9.6.1",
"nodemon": "3.1.11",
"typescript": "5.9.3"
},
diff --git a/packages/misskey-js/generator/package.json b/packages/misskey-js/generator/package.json
index 7c4a12552d..e9721911cc 100644
--- a/packages/misskey-js/generator/package.json
+++ b/packages/misskey-js/generator/package.json
@@ -8,13 +8,13 @@
},
"devDependencies": {
"@readme/openapi-parser": "5.2.1",
- "@types/node": "24.10.1",
- "@typescript-eslint/eslint-plugin": "8.47.0",
- "@typescript-eslint/parser": "8.47.0",
+ "@types/node": "24.10.2",
+ "@typescript-eslint/eslint-plugin": "8.49.0",
+ "@typescript-eslint/parser": "8.49.0",
"openapi-types": "12.1.3",
"openapi-typescript": "7.10.1",
"ts-case-convert": "2.1.0",
- "tsx": "4.20.6",
+ "tsx": "4.21.0",
"typescript": "5.9.3",
"eslint": "9.39.1"
},
diff --git a/packages/misskey-js/package.json b/packages/misskey-js/package.json
index 395c2e2353..226428af13 100644
--- a/packages/misskey-js/package.json
+++ b/packages/misskey-js/package.json
@@ -1,7 +1,7 @@
{
"type": "module",
"name": "misskey-js",
- "version": "2025.12.1",
+ "version": "2025.12.2",
"description": "Misskey SDK for JavaScript",
"license": "MIT",
"main": "./built/index.js",
@@ -37,18 +37,18 @@
"directory": "packages/misskey-js"
},
"devDependencies": {
- "@microsoft/api-extractor": "7.55.1",
- "@types/node": "24.10.1",
- "@typescript-eslint/eslint-plugin": "8.47.0",
- "@typescript-eslint/parser": "8.47.0",
- "@vitest/coverage-v8": "4.0.13",
- "esbuild": "0.27.0",
- "execa": "9.6.0",
+ "@microsoft/api-extractor": "7.55.2",
+ "@types/node": "24.10.2",
+ "@typescript-eslint/eslint-plugin": "8.49.0",
+ "@typescript-eslint/parser": "8.49.0",
+ "@vitest/coverage-v8": "4.0.15",
+ "esbuild": "0.27.1",
+ "execa": "9.6.1",
"ncp": "2.0.0",
"nodemon": "3.1.11",
"tsd": "0.33.0",
"typescript": "5.9.3",
- "vitest": "4.0.13",
+ "vitest": "4.0.15",
"vitest-websocket-mock": "0.5.0"
},
"files": [
diff --git a/packages/misskey-reversi/package.json b/packages/misskey-reversi/package.json
index 85c829204e..e22ccd1e02 100644
--- a/packages/misskey-reversi/package.json
+++ b/packages/misskey-reversi/package.json
@@ -24,11 +24,11 @@
"lint": "pnpm typecheck && pnpm eslint"
},
"devDependencies": {
- "@types/node": "24.10.1",
- "@typescript-eslint/eslint-plugin": "8.48.0",
- "@typescript-eslint/parser": "8.48.0",
- "esbuild": "0.27.0",
- "execa": "9.6.0",
+ "@types/node": "24.10.2",
+ "@typescript-eslint/eslint-plugin": "8.49.0",
+ "@typescript-eslint/parser": "8.49.0",
+ "esbuild": "0.27.1",
+ "execa": "9.6.1",
"nodemon": "3.1.11",
"typescript": "5.9.3"
},
diff --git a/packages/sw/package.json b/packages/sw/package.json
index b6b03adb54..1911524b8f 100644
--- a/packages/sw/package.json
+++ b/packages/sw/package.json
@@ -10,12 +10,12 @@
},
"dependencies": {
"i18n": "workspace:*",
- "esbuild": "0.27.0",
+ "esbuild": "0.27.1",
"idb-keyval": "6.2.2",
"misskey-js": "workspace:*"
},
"devDependencies": {
- "@typescript-eslint/parser": "8.48.0",
+ "@typescript-eslint/parser": "8.49.0",
"@typescript/lib-webworker": "npm:@types/serviceworker@0.0.74",
"eslint-plugin-import": "2.32.0",
"nodemon": "3.1.11",