diff options
| author | misskey-release-bot[bot] <157398866+misskey-release-bot[bot]@users.noreply.github.com> | 2025-11-28 10:04:09 +0000 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2025-11-28 10:04:09 +0000 |
| commit | 994fc062cf3c60559d5730e9897f6c47b296f254 (patch) | |
| tree | ce75fcbd2f55ffbd9a83e81af6117fd94eb24321 /packages | |
| parent | Merge pull request #16759 from misskey-dev/develop (diff) | |
| parent | Release: 2025.11.1 (diff) | |
| download | misskey-994fc062cf3c60559d5730e9897f6c47b296f254.tar.gz misskey-994fc062cf3c60559d5730e9897f6c47b296f254.tar.bz2 misskey-994fc062cf3c60559d5730e9897f6c47b296f254.zip | |
Merge pull request #16840 from misskey-dev/develop
Release: 2025.11.1
Diffstat (limited to 'packages')
47 files changed, 601 insertions, 458 deletions
diff --git a/packages/backend/package.json b/packages/backend/package.json index e69dd86dc0..95ebdbdd3c 100644 --- a/packages/backend/package.json +++ b/packages/backend/package.json @@ -8,6 +8,7 @@ }, "scripts": { "start": "node ./built/boot/entry.js", + "start:inspect": "node --inspect ./built/boot/entry.js", "start:test": "cross-env NODE_ENV=test node ./built/boot/entry.js", "migrate": "pnpm typeorm migration:run -d ormconfig.js", "revert": "pnpm typeorm migration:revert -d ormconfig.js", @@ -39,17 +40,17 @@ }, "optionalDependencies": { "@swc/core-android-arm64": "1.3.11", - "@swc/core-darwin-arm64": "1.14.0", - "@swc/core-darwin-x64": "1.14.0", + "@swc/core-darwin-arm64": "1.15.2", + "@swc/core-darwin-x64": "1.15.2", "@swc/core-freebsd-x64": "1.3.11", - "@swc/core-linux-arm-gnueabihf": "1.14.0", - "@swc/core-linux-arm64-gnu": "1.14.0", - "@swc/core-linux-arm64-musl": "1.14.0", - "@swc/core-linux-x64-gnu": "1.14.0", - "@swc/core-linux-x64-musl": "1.14.0", - "@swc/core-win32-arm64-msvc": "1.14.0", - "@swc/core-win32-ia32-msvc": "1.14.0", - "@swc/core-win32-x64-msvc": "1.14.0", + "@swc/core-linux-arm-gnueabihf": "1.15.2", + "@swc/core-linux-arm64-gnu": "1.15.2", + "@swc/core-linux-arm64-musl": "1.15.2", + "@swc/core-linux-x64-gnu": "1.15.2", + "@swc/core-linux-x64-musl": "1.15.2", + "@swc/core-win32-arm64-msvc": "1.15.2", + "@swc/core-win32-ia32-msvc": "1.15.2", + "@swc/core-win32-x64-msvc": "1.15.2", "@tensorflow/tfjs": "4.22.0", "@tensorflow/tfjs-node": "4.22.0", "bufferutil": "4.0.9", @@ -69,8 +70,8 @@ "utf-8-validate": "6.0.5" }, "dependencies": { - "@aws-sdk/client-s3": "3.922.0", - "@aws-sdk/lib-storage": "3.922.0", + "@aws-sdk/client-s3": "3.936.0", + "@aws-sdk/lib-storage": "3.936.0", "@discordapp/twemoji": "16.0.1", "@fastify/accepts": "5.0.3", "@fastify/cookie": "11.0.2", @@ -82,18 +83,18 @@ "@fastify/view": "10.0.2", "@misskey-dev/sharp-read-bmp": "1.2.0", "@misskey-dev/summaly": "5.2.5", - "@napi-rs/canvas": "0.1.81", - "@nestjs/common": "11.1.8", - "@nestjs/core": "11.1.8", - "@nestjs/testing": "11.1.8", + "@napi-rs/canvas": "0.1.82", + "@nestjs/common": "11.1.9", + "@nestjs/core": "11.1.9", + "@nestjs/testing": "11.1.9", "@peertube/http-signature": "1.7.0", - "@sentry/node": "10.22.0", - "@sentry/profiling-node": "10.22.0", + "@sentry/node": "10.26.0", + "@sentry/profiling-node": "10.26.0", "@simplewebauthn/server": "12.0.0", "@sinonjs/fake-timers": "11.3.1", "@smithy/node-http-handler": "2.5.0", - "@swc/cli": "0.7.8", - "@swc/core": "1.14.0", + "@swc/cli": "0.7.9", + "@swc/core": "1.15.2", "@twemoji/parser": "16.0.0", "@types/redis-info": "3.0.3", "accepts": "1.3.8", @@ -103,24 +104,23 @@ "bcryptjs": "2.4.3", "blurhash": "2.0.5", "body-parser": "1.20.3", - "bullmq": "5.63.0", + "bullmq": "5.63.2", "cacheable-lookup": "7.0.0", "cbor": "9.0.2", "chalk": "5.6.2", "chalk-template": "1.1.2", "chokidar": "4.0.3", - "cli-highlight": "2.1.11", "color-convert": "2.0.1", "content-disposition": "0.5.4", "date-fns": "2.30.0", "deep-email-validator": "0.1.21", - "fastify": "5.6.1", + "fastify": "5.6.2", "fastify-raw-body": "5.0.0", "feed": "4.2.2", - "file-type": "21.0.0", + "file-type": "21.1.1", "fluent-ffmpeg": "2.1.3", - "form-data": "4.0.4", - "got": "14.6.1", + "form-data": "4.0.5", + "got": "14.6.4", "happy-dom": "20.0.10", "hpagent": "1.2.0", "htmlescape": "1.1.1", @@ -129,7 +129,7 @@ "ip-cidr": "4.0.2", "ipaddr.js": "2.2.0", "is-svg": "5.1.0", - "js-yaml": "4.1.0", + "js-yaml": "4.1.1", "jsdom": "26.1.0", "json5": "2.2.3", "jsonld": "8.3.3", @@ -161,7 +161,7 @@ "qrcode": "1.5.4", "random-seed": "0.3.0", "ratelimiter": "3.4.1", - "re2": "1.22.1", + "re2": "1.22.3", "redis-info": "3.1.0", "redis-lock": "0.1.4", "reflect-metadata": "0.2.2", @@ -170,8 +170,8 @@ "rxjs": "7.8.2", "sanitize-html": "2.17.0", "secure-json-parse": "3.0.2", - "sharp": "0.33.5", "semver": "7.7.3", + "sharp": "0.33.5", "slacc": "0.0.10", "strict-event-emitter-types": "2.0.0", "stringz": "2.1.0", @@ -191,7 +191,7 @@ "devDependencies": { "@jest/globals": "29.7.0", "@nestjs/platform-express": "10.4.20", - "@sentry/vue": "10.22.0", + "@sentry/vue": "10.26.0", "@simplewebauthn/types": "12.0.0", "@swc/jest": "0.2.39", "@types/accepts": "1.3.7", @@ -210,7 +210,7 @@ "@types/jsrsasign": "10.5.15", "@types/mime-types": "2.1.4", "@types/ms": "0.7.34", - "@types/node": "24.9.2", + "@types/node": "24.10.1", "@types/nodemailer": "6.4.21", "@types/oauth": "0.9.6", "@types/oauth2orize": "1.11.5", @@ -231,8 +231,8 @@ "@types/vary": "1.1.3", "@types/web-push": "3.6.4", "@types/ws": "8.18.1", - "@typescript-eslint/eslint-plugin": "8.46.2", - "@typescript-eslint/parser": "8.46.2", + "@typescript-eslint/eslint-plugin": "8.47.0", + "@typescript-eslint/parser": "8.47.0", "aws-sdk-client-mock": "4.1.0", "cross-env": "7.0.3", "eslint-plugin-import": "2.32.0", @@ -240,7 +240,7 @@ "fkill": "9.0.0", "jest": "29.7.0", "jest-mock": "29.7.0", - "nodemon": "3.1.10", + "nodemon": "3.1.11", "pid-port": "1.0.2", "simple-oauth2": "5.1.0", "supertest": "7.1.4" diff --git a/packages/backend/src/boot/master.ts b/packages/backend/src/boot/master.ts index d1fb3858db..5ec362fb34 100644 --- a/packages/backend/src/boot/master.ts +++ b/packages/backend/src/boot/master.ts @@ -41,7 +41,7 @@ function greet() { //#endregion console.log(' Misskey is an open-source decentralized microblogging platform.'); - console.log(chalk.rgb(255, 136, 0)(' If you like Misskey, please donate to support development. https://www.patreon.com/syuilo')); + console.log(chalk.rgb(255, 136, 0)(' If you like Misskey, please consider donating to support dev. https://misskey-hub.net/docs/donate/')); console.log(''); console.log(chalkTemplate`--- ${os.hostname()} {gray (PID: ${process.pid.toString()})} ---`); diff --git a/packages/backend/src/core/AiService.ts b/packages/backend/src/core/AiService.ts index 23ab8082ed..7a005400bb 100644 --- a/packages/backend/src/core/AiService.ts +++ b/packages/backend/src/core/AiService.ts @@ -7,11 +7,11 @@ import * as fs from 'node:fs'; import { fileURLToPath } from 'node:url'; import { dirname } from 'node:path'; import { Injectable } from '@nestjs/common'; -import * as nsfw from 'nsfwjs'; import si from 'systeminformation'; import { Mutex } from 'async-mutex'; import fetch from 'node-fetch'; import { bindThis } from '@/decorators.js'; +import type { NSFWJS, PredictionType } from 'nsfwjs'; const _filename = fileURLToPath(import.meta.url); const _dirname = dirname(_filename); @@ -21,7 +21,7 @@ let isSupportedCpu: undefined | boolean = undefined; @Injectable() export class AiService { - private model: nsfw.NSFWJS; + private model: NSFWJS; private modelLoadMutex: Mutex = new Mutex(); constructor( @@ -29,7 +29,7 @@ export class AiService { } @bindThis - public async detectSensitive(source: string | Buffer): Promise<nsfw.PredictionType[] | null> { + public async detectSensitive(source: string | Buffer): Promise<PredictionType[] | null> { try { if (isSupportedCpu === undefined) { isSupportedCpu = await this.computeIsSupportedCpu(); @@ -44,6 +44,7 @@ export class AiService { tf.env().global.fetch = fetch; if (this.model == null) { + const nsfw = await import('nsfwjs'); await this.modelLoadMutex.runExclusive(async () => { if (this.model == null) { this.model = await nsfw.load(`file://${_dirname}/../../nsfw-model/`, { size: 299 }); diff --git a/packages/backend/src/models/_.ts b/packages/backend/src/models/_.ts index 8c77a225c8..c4528e3a77 100644 --- a/packages/backend/src/models/_.ts +++ b/packages/backend/src/models/_.ts @@ -5,18 +5,9 @@ import { FindOneOptions, - InsertQueryBuilder, ObjectLiteral, - QueryRunner, Repository, - SelectQueryBuilder, } from 'typeorm'; -import { PostgresConnectionOptions } from 'typeorm/driver/postgres/PostgresConnectionOptions.js'; -import { RelationCountLoader } from 'typeorm/query-builder/relation-count/RelationCountLoader.js'; -import { RelationIdLoader } from 'typeorm/query-builder/relation-id/RelationIdLoader.js'; -import { - RawSqlResultsToEntityTransformer, -} from 'typeorm/query-builder/transformer/RawSqlResultsToEntityTransformer.js'; import { MiAbuseReportNotificationRecipient } from '@/models/AbuseReportNotificationRecipient.js'; import { MiAbuseUserReport } from '@/models/AbuseUserReport.js'; import { MiAccessToken } from '@/models/AccessToken.js'; @@ -96,66 +87,12 @@ import { MiWebhook } from '@/models/Webhook.js'; import type { QueryDeepPartialEntity } from 'typeorm/query-builder/QueryPartialEntity.js'; export interface MiRepository<T extends ObjectLiteral> { - createTableColumnNames(this: Repository<T> & MiRepository<T>): string[]; - insertOne(this: Repository<T> & MiRepository<T>, entity: QueryDeepPartialEntity<T>, findOptions?: Pick<FindOneOptions<T>, 'relations'>): Promise<T>; - - insertOneImpl(this: Repository<T> & MiRepository<T>, entity: QueryDeepPartialEntity<T>, findOptions?: Pick<FindOneOptions<T>, 'relations'>, queryRunner?: QueryRunner): Promise<T>; - - selectAliasColumnNames(this: Repository<T> & MiRepository<T>, queryBuilder: InsertQueryBuilder<T>, builder: SelectQueryBuilder<T>): void; } export const miRepository = { - createTableColumnNames() { - return this.metadata.columns.filter(column => column.isSelect && !column.isVirtual).map(column => column.databaseName); - }, async insertOne(entity, findOptions?) { - const opt = this.manager.connection.options as PostgresConnectionOptions; - if (opt.replication) { - const queryRunner = this.manager.connection.createQueryRunner('master'); - try { - return this.insertOneImpl(entity, findOptions, queryRunner); - } finally { - await queryRunner.release(); - } - } else { - return this.insertOneImpl(entity, findOptions); - } - }, - async insertOneImpl(entity, findOptions?, queryRunner?) { - // ---- insert + returningの結果を共通テーブル式(CTE)に保持するクエリを生成 ---- - - const queryBuilder = this.createQueryBuilder().insert().values(entity); - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - const mainAlias = queryBuilder.expressionMap.mainAlias!; - const name = mainAlias.name; - mainAlias.name = 't'; - const columnNames = this.createTableColumnNames(); - queryBuilder.returning(columnNames.reduce((a, c) => `${a}, ${queryBuilder.escape(c)}`, '').slice(2)); - - // ---- 共通テーブル式(CTE)から結果を取得 ---- - const builder = this.createQueryBuilder(undefined, queryRunner).addCommonTableExpression(queryBuilder, 'cte', { columnNames }); - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - builder.expressionMap.mainAlias!.tablePath = 'cte'; - this.selectAliasColumnNames(queryBuilder, builder); - if (findOptions) { - builder.setFindOptions(findOptions); - } - const raw = await builder.execute(); - mainAlias.name = name; - const relationId = await new RelationIdLoader(builder.connection, this.queryRunner, builder.expressionMap.relationIdAttributes).load(raw); - const relationCount = await new RelationCountLoader(builder.connection, this.queryRunner, builder.expressionMap.relationCountAttributes).load(raw); - const result = new RawSqlResultsToEntityTransformer(builder.expressionMap, builder.connection.driver, relationId, relationCount, this.queryRunner).transform(raw, mainAlias); - return result[0]; - }, - selectAliasColumnNames(queryBuilder, builder) { - let selectOrAddSelect = (selection: string, selectionAliasName?: string) => { - selectOrAddSelect = (selection, selectionAliasName) => builder.addSelect(selection, selectionAliasName); - return builder.select(selection, selectionAliasName); - }; - for (const columnName of this.createTableColumnNames()) { - selectOrAddSelect(`${builder.alias}.${columnName}`, `${builder.alias}_${columnName}`); - } + return await this.insert(entity).then(x => this.findOneOrFail({ where: x.identifiers[0], ...findOptions })); }, } satisfies MiRepository<ObjectLiteral>; diff --git a/packages/backend/src/postgres.ts b/packages/backend/src/postgres.ts index a3e659c110..3dcd3f0965 100644 --- a/packages/backend/src/postgres.ts +++ b/packages/backend/src/postgres.ts @@ -6,7 +6,6 @@ // https://github.com/typeorm/typeorm/issues/2400 import pg from 'pg'; import { DataSource, Logger, type QueryRunner } from 'typeorm'; -import * as highlight from 'cli-highlight'; import { entities as charts } from '@/core/chart/entities.js'; import { Config } from '@/config.js'; import MisskeyLogger from '@/logger.js'; @@ -25,7 +24,7 @@ import { MiAuthSession } from '@/models/AuthSession.js'; import { MiBlocking } from '@/models/Blocking.js'; import { MiChannelFollowing } from '@/models/ChannelFollowing.js'; import { MiChannelFavorite } from '@/models/ChannelFavorite.js'; -import { MiChannelMuting } from "@/models/ChannelMuting.js"; +import { MiChannelMuting } from '@/models/ChannelMuting.js'; import { MiClip } from '@/models/Clip.js'; import { MiClipNote } from '@/models/ClipNote.js'; import { MiClipFavorite } from '@/models/ClipFavorite.js'; @@ -101,12 +100,6 @@ export type LoggerProps = { printReplicationMode?: boolean, }; -function highlightSql(sql: string) { - return highlight.highlight(sql, { - language: 'sql', ignoreIllegals: true, - }); -} - function truncateSql(sql: string) { return sql.length > 100 ? `${sql.substring(0, 100)}...` : sql; } @@ -132,7 +125,7 @@ class MyCustomLogger implements Logger { modded = truncateSql(modded); } - return highlightSql(modded); + return modded; } @bindThis diff --git a/packages/backend/src/server/api/endpoints/i/update.ts b/packages/backend/src/server/api/endpoints/i/update.ts index 5c7958fc1c..113a09cb14 100644 --- a/packages/backend/src/server/api/endpoints/i/update.ts +++ b/packages/backend/src/server/api/endpoints/i/update.ts @@ -295,8 +295,20 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint- if (ps.chatScope !== undefined) updates.chatScope = ps.chatScope; function checkMuteWordCount(mutedWords: (string[] | string)[], limit: number) { - // TODO: ちゃんと数える - const length = JSON.stringify(mutedWords).length; + const count = (arr: (string[] | string)[]) => { + let length = 0; + for (const item of arr) { + if (typeof item === 'string') { + length += item.length; + } else if (Array.isArray(item)) { + for (const subItem of item) { + length += subItem.length; + } + } + } + return length; + }; + const length = count(mutedWords); if (length > limit) { throw new ApiError(meta.errors.tooManyMutedWords); } diff --git a/packages/backend/src/server/api/endpoints/notes/drafts/create.ts b/packages/backend/src/server/api/endpoints/notes/drafts/create.ts index 880f4964a1..efb5ee01d1 100644 --- a/packages/backend/src/server/api/endpoints/notes/drafts/create.ts +++ b/packages/backend/src/server/api/endpoints/notes/drafts/create.ts @@ -135,6 +135,18 @@ export const meta = { code: 'CANNOT_RENOTE_TO_EXTERNAL', id: 'ed1952ac-2d26-4957-8b30-2deda76bedf7', }, + + scheduledAtRequired: { + message: 'scheduledAt is required when isActuallyScheduled is true.', + code: 'SCHEDULED_AT_REQUIRED', + id: '15e28a55-e74c-4d65-89b7-8880cdaaa87d', + }, + + scheduledAtMustBeInFuture: { + message: 'scheduledAt must be in the future.', + code: 'SCHEDULED_AT_MUST_BE_IN_FUTURE', + id: 'e4bed6c9-017e-4934-aed0-01c22cc60ec1', + }, }, limit: { @@ -252,6 +264,10 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint- throw new ApiError(meta.errors.cannotReplyToSpecifiedVisibilityNoteWithExtendedVisibility); case 'c3275f19-4558-4c59-83e1-4f684b5fab66': throw new ApiError(meta.errors.tooManyScheduledNotes); + case '94a89a43-3591-400a-9c17-dd166e71fdfa': + throw new ApiError(meta.errors.scheduledAtRequired); + case 'b34d0c1b-996f-4e34-a428-c636d98df457': + throw new ApiError(meta.errors.scheduledAtMustBeInFuture); default: throw err; } diff --git a/packages/backend/src/server/api/endpoints/notes/drafts/update.ts b/packages/backend/src/server/api/endpoints/notes/drafts/update.ts index 9a2e2ca415..2900e0cb0d 100644 --- a/packages/backend/src/server/api/endpoints/notes/drafts/update.ts +++ b/packages/backend/src/server/api/endpoints/notes/drafts/update.ts @@ -165,6 +165,18 @@ export const meta = { code: 'TOO_MANY_SCHEDULED_NOTES', id: '02f5df79-08ae-4a33-8524-f1503c8f6212', }, + + scheduledAtRequired: { + message: 'scheduledAt is required when isActuallyScheduled is true.', + code: 'SCHEDULED_AT_REQUIRED', + id: 'fe9737d5-cc41-498c-af9d-149207307530', + }, + + scheduledAtMustBeInFuture: { + message: 'scheduledAt must be in the future.', + code: 'SCHEDULED_AT_MUST_BE_IN_FUTURE', + id: 'ed1a6673-d0d1-4364-aaae-9bf3f139cbc5', + }, }, limit: { @@ -295,6 +307,10 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint- throw new ApiError(meta.errors.containsTooManyMentions); case 'bacdf856-5c51-4159-b88a-804fa5103be5': throw new ApiError(meta.errors.tooManyScheduledNotes); + case '94a89a43-3591-400a-9c17-dd166e71fdfa': + throw new ApiError(meta.errors.scheduledAtRequired); + case 'b34d0c1b-996f-4e34-a428-c636d98df457': + throw new ApiError(meta.errors.scheduledAtMustBeInFuture); default: throw err; } diff --git a/packages/backend/src/server/api/endpoints/notes/translate.ts b/packages/backend/src/server/api/endpoints/notes/translate.ts index e9a6a36b02..cd7d46007c 100644 --- a/packages/backend/src/server/api/endpoints/notes/translate.ts +++ b/packages/backend/src/server/api/endpoints/notes/translate.ts @@ -95,7 +95,6 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint- if (targetLang.includes('-')) targetLang = targetLang.split('-')[0]; const params = new URLSearchParams(); - params.append('auth_key', this.serverSettings.deeplAuthKey); params.append('text', note.text); params.append('target_lang', targetLang); @@ -104,6 +103,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint- const res = await this.httpRequestService.send(endpoint, { method: 'POST', headers: { + 'Authorization': `DeepL-Auth-Key ${this.serverSettings.deeplAuthKey}`, 'Content-Type': 'application/x-www-form-urlencoded', Accept: 'application/json, */*', }, diff --git a/packages/backend/src/server/api/stream/channels/channel.ts b/packages/backend/src/server/api/stream/channels/channel.ts index 9a0da5b66b..c07eaac98d 100644 --- a/packages/backend/src/server/api/stream/channels/channel.ts +++ b/packages/backend/src/server/api/stream/channels/channel.ts @@ -41,6 +41,10 @@ class ChannelChannel extends Channel { private async onNote(note: Packed<'Note'>) { if (note.channelId !== this.channelId) return; + if (note.user.requireSigninToViewContents && this.user == null) return; + if (note.renote && note.renote.user.requireSigninToViewContents && this.user == null) return; + if (note.reply && note.reply.user.requireSigninToViewContents && this.user == null) return; + if (this.isNoteMutedOrBlocked(note)) return; if (this.user && isRenotePacked(note) && !isQuotePacked(note)) { diff --git a/packages/backend/src/server/web/ClientServerService.ts b/packages/backend/src/server/web/ClientServerService.ts index 3cd83efa1a..f9d904f3cd 100644 --- a/packages/backend/src/server/web/ClientServerService.ts +++ b/packages/backend/src/server/web/ClientServerService.ts @@ -50,7 +50,7 @@ import { AnnouncementEntityService } from '@/core/entities/AnnouncementEntitySer import { FeedService } from './FeedService.js'; import { UrlPreviewService } from './UrlPreviewService.js'; import { ClientLoggerService } from './ClientLoggerService.js'; -import type { FastifyInstance, FastifyPluginOptions, FastifyReply } from 'fastify'; +import type { FastifyError, FastifyInstance, FastifyPluginOptions, FastifyReply } from 'fastify'; const _filename = fileURLToPath(import.meta.url); const _dirname = dirname(_filename); @@ -918,7 +918,7 @@ export class ClientServerService { return await renderBase(reply); }); - fastify.setErrorHandler(async (error, request, reply) => { + fastify.setErrorHandler<FastifyError>(async (error, request, reply) => { const errId = randomUUID(); this.clientLoggerService.logger.error(`Internal error occurred in ${request.routeOptions.url}: ${error.message}`, { path: request.routeOptions.url, diff --git a/packages/backend/src/server/web/boot.embed.js b/packages/backend/src/server/web/boot.embed.js index 022ff064ad..ba6366b3db 100644 --- a/packages/backend/src/server/web/boot.embed.js +++ b/packages/backend/src/server/web/boot.embed.js @@ -55,7 +55,7 @@ //#region Script async function importAppScript() { - await import(CLIENT_ENTRY ? `/embed_vite/${CLIENT_ENTRY.replace('scripts', lang)}` : '/embed_vite/src/_boot_.ts') + await import(CLIENT_ENTRY ? `/embed_vite/${CLIENT_ENTRY.replace('scripts', lang)}` : '/embed_vite/src/boot.ts') .catch(async e => { console.error(e); renderError('APP_IMPORT'); diff --git a/packages/backend/src/server/web/views/base.pug b/packages/backend/src/server/web/views/base.pug index a76c75fe5c..46b365a9c7 100644 --- a/packages/backend/src/server/web/views/base.pug +++ b/packages/backend/src/server/web/views/base.pug @@ -28,7 +28,7 @@ html meta(name='theme-color-orig' content= themeColor || '#86b300') meta(property='og:site_name' content= instanceName || 'Misskey') meta(property='instance_url' content= instanceUrl) - meta(name='viewport' content='width=device-width, initial-scale=1') + meta(name='viewport' content='width=device-width, initial-scale=1, minimum-scale=1, maximum-scale=1, user-scalable=no, viewport-fit=cover') meta(name='format-detection' content='telephone=no,date=no,address=no,email=no,url=no') link(rel='icon' href= icon || '/favicon.ico') link(rel='apple-touch-icon' href= appleTouchIcon || '/apple-touch-icon.png') diff --git a/packages/backend/test-federation/compose.tpl.yml b/packages/backend/test-federation/compose.tpl.yml index 3d2ed21337..92b986736d 100644 --- a/packages/backend/test-federation/compose.tpl.yml +++ b/packages/backend/test-federation/compose.tpl.yml @@ -95,7 +95,7 @@ services: retries: 20 db: - image: postgres:15-alpine + image: postgres:18-alpine env_file: - ./.config/docker.env volumes: diff --git a/packages/backend/test/compose.yml b/packages/backend/test/compose.yml index 6593fc33dd..fe96616fc0 100644 --- a/packages/backend/test/compose.yml +++ b/packages/backend/test/compose.yml @@ -5,7 +5,7 @@ services: - "127.0.0.1:56312:6379" dbtest: - image: postgres:15 + image: postgres:18 ports: - "127.0.0.1:54312:5432" environment: diff --git a/packages/frontend-builder/package.json b/packages/frontend-builder/package.json index 5e1ddee1ec..ef5c8e0367 100644 --- a/packages/frontend-builder/package.json +++ b/packages/frontend-builder/package.json @@ -11,15 +11,15 @@ }, "devDependencies": { "@types/estree": "1.0.8", - "@types/node": "24.9.2", - "@typescript-eslint/eslint-plugin": "8.46.2", - "@typescript-eslint/parser": "8.46.2", - "rollup": "4.52.5", + "@types/node": "24.10.1", + "@typescript-eslint/eslint-plugin": "8.47.0", + "@typescript-eslint/parser": "8.47.0", + "rollup": "4.53.3", "typescript": "5.9.3" }, "dependencies": { "estree-walker": "3.0.3", "magic-string": "0.30.21", - "vite": "7.1.11" + "vite": "7.2.2" } } diff --git a/packages/frontend-embed/package.json b/packages/frontend-embed/package.json index c5bcb13cf7..7bfd32686c 100644 --- a/packages/frontend-embed/package.json +++ b/packages/frontend-embed/package.json @@ -15,8 +15,8 @@ "@rollup/plugin-replace": "6.0.3", "@rollup/pluginutils": "5.3.0", "@twemoji/parser": "16.0.0", - "@vitejs/plugin-vue": "6.0.1", - "@vue/compiler-sfc": "3.5.22", + "@vitejs/plugin-vue": "6.0.2", + "@vue/compiler-sfc": "3.5.24", "astring": "1.9.0", "buraha": "0.0.1", "estree-walker": "3.0.3", @@ -26,16 +26,16 @@ "mfm-js": "0.25.0", "misskey-js": "workspace:*", "punycode.js": "2.3.1", - "rollup": "4.52.5", - "sass": "1.93.3", - "shiki": "3.14.0", + "rollup": "4.53.3", + "sass": "1.94.1", + "shiki": "3.15.0", "tinycolor2": "1.6.0", "tsc-alias": "1.8.16", "tsconfig-paths": "4.2.0", "typescript": "5.9.3", "uuid": "13.0.0", - "vite": "7.1.11", - "vue": "3.5.22" + "vite": "7.2.2", + "vue": "3.5.24" }, "devDependencies": { "@misskey-dev/summaly": "5.2.5", @@ -43,14 +43,14 @@ "@testing-library/vue": "8.1.0", "@types/estree": "1.0.8", "@types/micromatch": "4.0.10", - "@types/node": "24.9.2", + "@types/node": "24.10.1", "@types/punycode.js": "npm:@types/punycode@2.1.4", "@types/tinycolor2": "1.4.6", "@types/ws": "8.18.1", - "@typescript-eslint/eslint-plugin": "8.46.2", - "@typescript-eslint/parser": "8.46.2", + "@typescript-eslint/eslint-plugin": "8.47.0", + "@typescript-eslint/parser": "8.47.0", "@vitest/coverage-v8": "3.2.4", - "@vue/runtime-core": "3.5.22", + "@vue/runtime-core": "3.5.24", "acorn": "8.15.0", "cross-env": "10.1.0", "eslint-plugin-import": "2.32.0", @@ -59,14 +59,14 @@ "happy-dom": "20.0.10", "intersection-observer": "0.12.2", "micromatch": "4.0.8", - "msw": "2.11.6", - "nodemon": "3.1.10", + "msw": "2.12.2", + "nodemon": "3.1.11", "prettier": "3.6.2", "start-server-and-test": "2.1.2", "tsx": "4.20.6", "vite-plugin-turbosnap": "1.0.3", - "vue-component-type-helpers": "3.1.2", + "vue-component-type-helpers": "3.1.4", "vue-eslint-parser": "10.2.0", - "vue-tsc": "3.1.2" + "vue-tsc": "3.1.4" } } diff --git a/packages/frontend-shared/package.json b/packages/frontend-shared/package.json index 52feb3401b..af4a6cb67e 100644 --- a/packages/frontend-shared/package.json +++ b/packages/frontend-shared/package.json @@ -21,12 +21,12 @@ "lint": "pnpm typecheck && pnpm eslint" }, "devDependencies": { - "@types/node": "24.9.2", - "@typescript-eslint/eslint-plugin": "8.46.2", - "@typescript-eslint/parser": "8.46.2", - "esbuild": "0.25.11", + "@types/node": "24.10.1", + "@typescript-eslint/eslint-plugin": "8.47.0", + "@typescript-eslint/parser": "8.47.0", + "esbuild": "0.27.0", "eslint-plugin-vue": "10.5.1", - "nodemon": "3.1.10", + "nodemon": "3.1.11", "typescript": "5.9.3", "vue-eslint-parser": "10.2.0" }, @@ -35,6 +35,6 @@ ], "dependencies": { "misskey-js": "workspace:*", - "vue": "3.5.22" + "vue": "3.5.24" } } diff --git a/packages/frontend/package.json b/packages/frontend/package.json index 776822978d..b6906e130a 100644 --- a/packages/frontend/package.json +++ b/packages/frontend/package.json @@ -24,12 +24,12 @@ "@rollup/plugin-json": "6.1.0", "@rollup/plugin-replace": "6.0.3", "@rollup/pluginutils": "5.3.0", - "@sentry/vue": "10.22.0", - "@syuilo/aiscript": "1.1.2", + "@sentry/vue": "10.26.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.1", - "@vue/compiler-sfc": "3.5.22", + "@vitejs/plugin-vue": "6.0.2", + "@vue/compiler-sfc": "3.5.24", "aiscript-vscode": "github:aiscript-dev/aiscript-vscode#v0.1.15", "analytics": "0.8.19", "astring": "1.9.0", @@ -41,7 +41,7 @@ "chartjs-chart-matrix": "3.0.0", "chartjs-plugin-gradient": "0.6.1", "chartjs-plugin-zoom": "2.2.0", - "chromatic": "13.3.3", + "chromatic": "13.3.4", "compare-versions": "6.1.1", "cropperjs": "2.1.0", "date-fns": "4.1.0", @@ -58,7 +58,7 @@ "json5": "2.2.3", "magic-string": "0.30.21", "matter-js": "0.20.0", - "mediabunny": "1.24.2", + "mediabunny": "1.25.0", "mfm-js": "0.25.0", "misskey-bubble-game": "workspace:*", "misskey-js": "workspace:*", @@ -67,21 +67,21 @@ "punycode.js": "2.3.1", "qr-code-styling": "1.9.2", "qr-scanner": "1.4.2", - "rollup": "4.52.5", + "rollup": "4.53.3", "sanitize-html": "2.17.0", - "sass": "1.93.3", - "shiki": "3.14.0", + "sass": "1.94.1", + "shiki": "3.15.0", "strict-event-emitter-types": "2.0.0", "textarea-caret": "3.1.0", - "three": "0.181.0", + "three": "0.181.2", "throttle-debounce": "5.0.2", "tinycolor2": "1.6.0", "tsc-alias": "1.8.16", "tsconfig-paths": "4.2.0", "typescript": "5.9.3", "v-code-diff": "1.13.1", - "vite": "7.1.11", - "vue": "3.5.22", + "vite": "7.2.2", + "vue": "3.5.24", "vuedraggable": "next", "wanakana": "5.3.1" }, @@ -110,21 +110,21 @@ "@types/estree": "1.0.8", "@types/matter-js": "0.20.2", "@types/micromatch": "4.0.10", - "@types/node": "24.9.2", + "@types/node": "24.10.1", "@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", "@types/ws": "8.18.1", - "@typescript-eslint/eslint-plugin": "8.46.2", - "@typescript-eslint/parser": "8.46.2", + "@typescript-eslint/eslint-plugin": "8.47.0", + "@typescript-eslint/parser": "8.47.0", "@vitest/coverage-v8": "3.2.4", - "@vue/compiler-core": "3.5.22", - "@vue/runtime-core": "3.5.22", + "@vue/compiler-core": "3.5.24", + "@vue/runtime-core": "3.5.24", "acorn": "8.15.0", "cross-env": "10.1.0", - "cypress": "15.5.0", + "cypress": "15.6.0", "eslint-plugin-import": "2.32.0", "eslint-plugin-vue": "10.5.1", "fast-glob": "3.3.3", @@ -132,9 +132,9 @@ "intersection-observer": "0.12.2", "micromatch": "4.0.8", "minimatch": "10.1.1", - "msw": "2.11.6", + "msw": "2.12.2", "msw-storybook-addon": "2.0.6", - "nodemon": "3.1.10", + "nodemon": "3.1.11", "prettier": "3.6.2", "react": "19.2.0", "react-dom": "19.2.0", @@ -147,8 +147,8 @@ "vite-plugin-turbosnap": "1.0.3", "vitest": "3.2.4", "vitest-fetch-mock": "0.4.5", - "vue-component-type-helpers": "3.1.2", + "vue-component-type-helpers": "3.1.4", "vue-eslint-parser": "10.2.0", - "vue-tsc": "3.1.2" + "vue-tsc": "3.1.4" } } diff --git a/packages/frontend/src/aiscript/api.ts b/packages/frontend/src/aiscript/api.ts index dc84925375..3a476787fe 100644 --- a/packages/frontend/src/aiscript/api.ts +++ b/packages/frontend/src/aiscript/api.ts @@ -40,29 +40,77 @@ export function createAiScriptEnv(opts: { storageKey: string, token?: string }) CUSTOM_EMOJIS: utils.jsToVal(customEmojis.value), LOCALE: values.STR(lang), SERVER_URL: values.STR(url), - 'Mk:dialog': values.FN_NATIVE(async ([title, text, type]) => { - utils.assertString(title); - utils.assertString(text); - if (type != null) { - assertStringAndIsIn(type, DIALOG_TYPES); + 'Mk:dialog': values.FN_NATIVE(async ([_title, _text, _type]) => { + let title: string | undefined = undefined; + let text: string | undefined = undefined; + let type: typeof DIALOG_TYPES[number] = 'info'; + + if (_title != null) { + if (utils.isString(_title)) { + title = _title.value; + } else { + utils.assertNull(_title); + } + } + + if (_text != null) { + if (utils.isString(_text)) { + text = _text.value; + } else { + utils.assertNull(_text); + } + } + + if (_type != null) { + if (utils.isString(_type)) { + assertStringAndIsIn(_type, DIALOG_TYPES); + type = _type.value; + } else { + utils.assertNull(_type); + } } + await os.alert({ - type: type ? type.value : 'info', - title: title.value, - text: text.value, + type, + title, + text, }); return values.NULL; }), - 'Mk:confirm': values.FN_NATIVE(async ([title, text, type]) => { - utils.assertString(title); - utils.assertString(text); - if (type != null) { - assertStringAndIsIn(type, DIALOG_TYPES); + 'Mk:confirm': values.FN_NATIVE(async ([_title, _text, _type]) => { + let title: string | undefined = undefined; + let text: string | undefined = undefined; + let type: typeof DIALOG_TYPES[number] = 'question'; + + if (_title != null) { + if (utils.isString(_title)) { + title = _title.value; + } else { + utils.assertNull(_title); + } } + + if (_text != null) { + if (utils.isString(_text)) { + text = _text.value; + } else { + utils.assertNull(_text); + } + } + + if (_type != null) { + if (utils.isString(_type)) { + assertStringAndIsIn(_type, DIALOG_TYPES); + type = _type.value; + } else { + utils.assertNull(_type); + } + } + const confirm = await os.confirm({ - type: type ? type.value : 'question', - title: title.value, - text: text.value, + type, + title, + text, }); return confirm.canceled ? values.FALSE : values.TRUE; }), @@ -76,15 +124,23 @@ export function createAiScriptEnv(opts: { storageKey: string, token?: string }) if (ep.value.includes('://') || ep.value.includes('..')) { throw new errors.AiScriptRuntimeError('invalid endpoint'); } - if (token) { + + let actualToken: string | null = null; + if (token != null && !utils.isNull(token)) { utils.assertString(token); // バグがあればundefinedもあり得るため念のため - if (typeof token.value !== 'string') throw new Error('invalid token'); + if (typeof token.value !== 'string') throw new errors.AiScriptRuntimeError('invalid token'); + actualToken = token.value; } - const actualToken: string | null = token?.value ?? opts.token ?? null; + + if (actualToken == null) { + actualToken = opts.token ?? null; + } + if (param == null) { throw new errors.AiScriptRuntimeError('expected param'); } + utils.assertObject(param); return misskeyApi(ep.value as keyof Misskey.Endpoints, utils.valToJs(param) as object, actualToken).then(res => { return utils.jsToVal(res); diff --git a/packages/frontend/src/boot/common.ts b/packages/frontend/src/boot/common.ts index f9783cb65c..2b522d3f10 100644 --- a/packages/frontend/src/boot/common.ts +++ b/packages/frontend/src/boot/common.ts @@ -3,7 +3,7 @@ * SPDX-License-Identifier: AGPL-3.0-only */ -import { computed, watch, version as vueVersion } from 'vue'; +import { watch, version as vueVersion } from 'vue'; import { compareVersions } from 'compare-versions'; import { version, lang, apiUrl, isSafeMode } from '@@/js/config.js'; import defaultLightTheme from '@@/themes/l-light.json5'; @@ -15,11 +15,11 @@ import directives from '@/directives/index.js'; import components from '@/components/index.js'; import { applyTheme } from '@/theme.js'; import { isDeviceDarkmode } from '@/utility/is-device-darkmode.js'; -import { updateI18n, i18n } from '@/i18n.js'; +import { i18n } from '@/i18n.js'; import { refreshCurrentAccount, login } from '@/accounts.js'; import { store } from '@/store.js'; import { fetchInstance, instance } from '@/instance.js'; -import { deviceKind, updateDeviceKind } from '@/utility/device-kind.js'; +import { updateDeviceKind } from '@/utility/device-kind.js'; import { reloadChannel } from '@/utility/unison-reload.js'; import { getUrlWithoutLoginId } from '@/utility/login-id.js'; import { getAccountFromId } from '@/utility/get-account-from-id.js'; @@ -109,13 +109,6 @@ export async function common(createVue: () => Promise<App<Element>>) { else window.location.reload(); }); - // If mobile, insert the viewport meta tag - if (['smartphone', 'tablet'].includes(deviceKind)) { - const viewport = window.document.getElementsByName('viewport').item(0); - viewport.setAttribute('content', - `${viewport.getAttribute('content')}, minimum-scale=1, maximum-scale=1, user-scalable=no, viewport-fit=cover`); - } - //#region Set lang attr const html = window.document.documentElement; html.setAttribute('lang', lang); @@ -160,8 +153,15 @@ export async function common(createVue: () => Promise<App<Element>>) { }); //#endregion + if (!isSafeMode) { + // TODO: instance.defaultLightTheme/instance.defaultDarkThemeが不正な形式だった場合のケア + if (prefer.s.lightTheme == null && instance.defaultLightTheme != null) prefer.commit('lightTheme', JSON.parse(instance.defaultLightTheme)); + if (prefer.s.darkTheme == null && instance.defaultDarkTheme != null) prefer.commit('darkTheme', JSON.parse(instance.defaultDarkTheme)); + } + // NOTE: この処理は必ずクライアント更新チェック処理より後に来ること(テーマ再構築のため) // NOTE: この処理は必ずダークモード判定処理より後に来ること(初回のテーマ適用のため) + // NOTE: この処理は必ずサーバーテーマ適用処理より後に来ること(二重applyTheme発火を防ぐため) // see: https://github.com/misskey-dev/misskey/issues/16562 watch(store.r.darkMode, (darkMode) => { const theme = (() => { @@ -178,26 +178,17 @@ export async function common(createVue: () => Promise<App<Element>>) { window.document.documentElement.dataset.colorScheme = store.s.darkMode ? 'dark' : 'light'; if (!isSafeMode) { - const darkTheme = prefer.model('darkTheme'); - const lightTheme = prefer.model('lightTheme'); - - watch(darkTheme, (theme) => { + watch(prefer.r.darkTheme, (theme) => { if (store.s.darkMode) { applyTheme(theme ?? defaultDarkTheme); } }); - watch(lightTheme, (theme) => { + watch(prefer.r.lightTheme, (theme) => { if (!store.s.darkMode) { applyTheme(theme ?? defaultLightTheme); } }); - - fetchInstanceMetaPromise.then(() => { - // TODO: instance.defaultLightTheme/instance.defaultDarkThemeが不正な形式だった場合のケア - if (prefer.s.lightTheme == null && instance.defaultLightTheme != null) prefer.commit('lightTheme', JSON.parse(instance.defaultLightTheme)); - if (prefer.s.darkTheme == null && instance.defaultDarkTheme != null) prefer.commit('darkTheme', JSON.parse(instance.defaultDarkTheme)); - }); } watch(prefer.r.overridedDeviceKind, (kind) => { diff --git a/packages/frontend/src/components/MkFollowButton.vue b/packages/frontend/src/components/MkFollowButton.vue index c7361a19c6..ba21fe82e4 100644 --- a/packages/frontend/src/components/MkFollowButton.vue +++ b/packages/frontend/src/components/MkFollowButton.vue @@ -102,6 +102,21 @@ async function onClick() { await misskeyApi('following/delete', { userId: props.user.id, }); + } else if (hasPendingFollowRequestFromYou.value) { + const { canceled } = await os.confirm({ + type: 'question', + text: i18n.tsx.cancelFollowRequestConfirm({ name: props.user.name || props.user.username }), + }); + + if (canceled) { + wait.value = false; + return; + } + + await misskeyApi('following/requests/cancel', { + userId: props.user.id, + }); + hasPendingFollowRequestFromYou.value = false; } else { if (prefer.s.alwaysConfirmFollow) { const { canceled } = await os.confirm({ @@ -115,41 +130,34 @@ async function onClick() { } } - if (hasPendingFollowRequestFromYou.value) { - await misskeyApi('following/requests/cancel', { - userId: props.user.id, - }); - hasPendingFollowRequestFromYou.value = false; - } else { - await misskeyApi('following/create', { - userId: props.user.id, - withReplies: prefer.s.defaultFollowWithReplies, - }); - emit('update:user', { - ...props.user, - withReplies: prefer.s.defaultFollowWithReplies, - }); - hasPendingFollowRequestFromYou.value = true; + await misskeyApi('following/create', { + userId: props.user.id, + withReplies: prefer.s.defaultFollowWithReplies, + }); + emit('update:user', { + ...props.user, + withReplies: prefer.s.defaultFollowWithReplies, + }); + hasPendingFollowRequestFromYou.value = true; - if ($i == null) { - wait.value = false; - return; - } + if ($i == null) { + wait.value = false; + return; + } - claimAchievement('following1'); + claimAchievement('following1'); - if ($i.followingCount >= 10) { - claimAchievement('following10'); - } - if ($i.followingCount >= 50) { - claimAchievement('following50'); - } - if ($i.followingCount >= 100) { - claimAchievement('following100'); - } - if ($i.followingCount >= 300) { - claimAchievement('following300'); - } + if ($i.followingCount >= 10) { + claimAchievement('following10'); + } + if ($i.followingCount >= 50) { + claimAchievement('following50'); + } + if ($i.followingCount >= 100) { + claimAchievement('following100'); + } + if ($i.followingCount >= 300) { + claimAchievement('following300'); } } } catch (err) { diff --git a/packages/frontend/src/components/MkPostForm.vue b/packages/frontend/src/components/MkPostForm.vue index 50340b21c2..86557b12df 100644 --- a/packages/frontend/src/components/MkPostForm.vue +++ b/packages/frontend/src/components/MkPostForm.vue @@ -608,11 +608,30 @@ async function toggleReactionAcceptance() { //#region その他の設定メニューpopup function showOtherSettings() { let reactionAcceptanceIcon = 'ti ti-icons'; + let reactionAcceptanceCaption = ''; - if (reactionAcceptance.value === 'likeOnly') { - reactionAcceptanceIcon = 'ti ti-heart _love'; - } else if (reactionAcceptance.value === 'likeOnlyForRemote') { - reactionAcceptanceIcon = 'ti ti-heart-plus'; + switch (reactionAcceptance.value) { + case 'likeOnly': + reactionAcceptanceIcon = 'ti ti-heart _love'; + reactionAcceptanceCaption = i18n.ts.likeOnly; + break; + + case 'likeOnlyForRemote': + reactionAcceptanceIcon = 'ti ti-heart-plus'; + reactionAcceptanceCaption = i18n.ts.likeOnlyForRemote; + break; + + case 'nonSensitiveOnly': + reactionAcceptanceCaption = i18n.ts.nonSensitiveOnly; + break; + + case 'nonSensitiveOnlyForLocalLikeOnlyForRemote': + reactionAcceptanceCaption = i18n.ts.nonSensitiveOnlyForLocalLikeOnlyForRemote; + break; + + default: + reactionAcceptanceCaption = i18n.ts.all; + break; } const menuItems = [{ @@ -624,6 +643,7 @@ function showOtherSettings() { }, { type: 'divider' }, { icon: reactionAcceptanceIcon, text: i18n.ts.reactionAcceptance, + caption: reactionAcceptanceCaption, action: () => { toggleReactionAcceptance(); }, @@ -692,6 +712,7 @@ function removeVisibleUser(user) { function clear() { text.value = ''; + cw.value = null; files.value = []; poll.value = null; quoteId.value = null; diff --git a/packages/frontend/src/components/MkRadios.vue b/packages/frontend/src/components/MkRadios.vue index 8b641d0f93..426a1d2c2b 100644 --- a/packages/frontend/src/components/MkRadios.vue +++ b/packages/frontend/src/components/MkRadios.vue @@ -4,7 +4,7 @@ SPDX-License-Identifier: AGPL-3.0-only --> <script lang="ts"> -import { defineComponent, h, ref, watch } from 'vue'; +import { Comment, defineComponent, h, ref, watch } from 'vue'; import MkRadio from './MkRadio.vue'; import type { VNode } from 'vue'; @@ -35,7 +35,7 @@ export default defineComponent({ if (options.length === 1 && options[0].props == null) options = options[0].children as VNode[]; // vnodeのうちv-if=falseなものを除外する(trueになるものはoptionなど他typeになる) - options = options.filter(vnode => !(typeof vnode.type === 'symbol' && vnode.type.description === 'v-cmt' && vnode.children === 'v-if')); + options = options.filter(vnode => vnode.type !== Comment); return () => h('div', { class: [ diff --git a/packages/frontend/src/components/MkSpot.vue b/packages/frontend/src/components/MkSpot.vue index 07699644aa..4a8ebb5f94 100644 --- a/packages/frontend/src/components/MkSpot.vue +++ b/packages/frontend/src/components/MkSpot.vue @@ -110,7 +110,11 @@ onUnmounted(() => { <style lang="scss" module> .root { - position: absolute; + position: fixed; + top: 0; + left: 0; + width: 100%; + height: 100%; } .bg { diff --git a/packages/frontend/src/components/global/MkSystemIcon.vue b/packages/frontend/src/components/global/MkSystemIcon.vue index d2ef0fb2d8..971c13478e 100644 --- a/packages/frontend/src/components/global/MkSystemIcon.vue +++ b/packages/frontend/src/components/global/MkSystemIcon.vue @@ -5,31 +5,31 @@ SPDX-License-Identifier: AGPL-3.0-only <template> <svg v-if="type === 'info'" :class="[$style.icon, $style.info]" viewBox="0 0 160 160"> - <path d="M80,108L80,72" style="--l:37;" :class="[$style.line, $style.animLine]"/> + <path d="M80,108L80,72" pathLength="1" :class="[$style.line, $style.animLine]"/> <path d="M80,52L80,52" :class="[$style.line, $style.animFade]"/> - <circle cx="80" cy="80" r="56" style="--l:350;" :class="[$style.line, $style.animCircle]"/> + <circle cx="80" cy="80" r="56" pathLength="1" :class="[$style.line, $style.animCircle]"/> </svg> <svg v-else-if="type === 'question'" :class="[$style.icon, $style.question]" viewBox="0 0 160 160"> - <path d="M80,92L79.991,84C88.799,83.98 96,76.962 96,68C96,59.038 88.953,52 79.991,52C71.03,52 64,59.038 64,68" style="--l:85;" :class="[$style.line, $style.animLine]"/> + <path d="M80,92L79.991,84C88.799,83.98 96,76.962 96,68C96,59.038 88.953,52 79.991,52C71.03,52 64,59.038 64,68" pathLength="1" :class="[$style.line, $style.animLine]"/> <path d="M80,108L80,108" :class="[$style.line, $style.animFade]"/> - <circle cx="80" cy="80" r="56" style="--l:350;" :class="[$style.line, $style.animCircle]"/> + <circle cx="80" cy="80" r="56" pathLength="1" :class="[$style.line, $style.animCircle]"/> </svg> <svg v-else-if="type === 'success'" :class="[$style.icon, $style.success]" viewBox="0 0 160 160"> - <path d="M62,80L74,92L98,68" style="--l:50;" :class="[$style.line, $style.animLine]"/> - <circle cx="80" cy="80" r="56" style="--l:350;" :class="[$style.line, $style.animCircle]"/> + <path d="M62,80L74,92L98,68" pathLength="1" :class="[$style.line, $style.animLine]"/> + <circle cx="80" cy="80" r="56" pathLength="1" :class="[$style.line, $style.animCircle]"/> </svg> <svg v-else-if="type === 'warn'" :class="[$style.icon, $style.warn]" viewBox="0 0 160 160"> - <path d="M80,64L80,88" style="--l:27;" :class="[$style.line, $style.animLine]"/> + <path d="M80,64L80,88" pathLength="1" :class="[$style.line, $style.animLine]"/> <path d="M80,108L80,108" :class="[$style.line, $style.animFade]"/> - <path d="M92,28L144,116C148.709,124.65 144.083,135.82 136,136L24,136C15.917,135.82 11.291,124.65 16,116L68,28C73.498,19.945 86.771,19.945 92,28Z" style="--l:395;" :class="[$style.line, $style.animLine]"/> + <path d="M92,28L144,116C148.709,124.65 144.083,135.82 136,136L24,136C15.917,135.82 11.291,124.65 16,116L68,28C73.498,19.945 86.771,19.945 92,28Z" pathLength="1" :class="[$style.line, $style.animLine]"/> </svg> <svg v-else-if="type === 'error'" :class="[$style.icon, $style.error]" viewBox="0 0 160 160"> - <path d="M63,63L96,96" style="--l:47;--duration:0.3s;" :class="[$style.line, $style.animLine]"/> - <path d="M96,63L63,96" style="--l:47;--duration:0.3s;--delay:0.2s;" :class="[$style.line, $style.animLine]"/> - <circle cx="80" cy="80" r="56" style="--l:350;" :class="[$style.line, $style.animCircle]"/> + <path d="M63,63L96,96" pathLength="1" style="--duration:0.3s;" :class="[$style.line, $style.animLine]"/> + <path d="M96,63L63,96" pathLength="1" style="--duration:0.3s;--delay:0.2s;" :class="[$style.line, $style.animLine]"/> + <circle cx="80" cy="80" r="56" pathLength="1" :class="[$style.line, $style.animCircle]"/> </svg> <svg v-else-if="type === 'waiting'" :class="[$style.icon, $style.waiting]" viewBox="0 0 160 160"> - <circle cx="80" cy="80" r="56" style="--l:350;" :class="[$style.line, $style.animCircleWaiting]"/> + <circle cx="80" cy="80" r="56" pathLength="1" :class="[$style.line, $style.animCircleWaiting]"/> <circle cx="80" cy="80" r="56" style="opacity: 0.25;" :class="[$style.line]"/> </svg> </template> @@ -80,15 +80,15 @@ const props = defineProps<{ } .animLine { - stroke-dasharray: var(--l); - stroke-dashoffset: var(--l); + stroke-dasharray: 1; + stroke-dashoffset: 1; animation: line var(--duration, 0.5s) cubic-bezier(0,0,.25,1) 1 forwards; animation-delay: var(--delay, 0s); } .animCircle { - stroke-dasharray: var(--l); - stroke-dashoffset: var(--l); + stroke-dasharray: 1; + stroke-dashoffset: 1; animation: line var(--duration, 0.5s) cubic-bezier(0,0,.25,1) 1 forwards; animation-delay: var(--delay, 0s); transform-origin: center; @@ -96,8 +96,8 @@ const props = defineProps<{ } .animCircleWaiting { - stroke-dasharray: var(--l); - stroke-dashoffset: calc(var(--l) / 1.5); + stroke-dasharray: 1; + stroke-dashoffset: calc(1 / 1.5); animation: waiting 0.75s linear infinite; transform-origin: center; } @@ -110,7 +110,7 @@ const props = defineProps<{ @keyframes line { 0% { - stroke-dashoffset: var(--l); + stroke-dashoffset: 1; opacity: 0; } 100% { diff --git a/packages/frontend/src/pages/chat/room.vue b/packages/frontend/src/pages/chat/room.vue index ef9205d86e..ef9191b4a5 100644 --- a/packages/frontend/src/pages/chat/room.vue +++ b/packages/frontend/src/pages/chat/room.vue @@ -447,7 +447,6 @@ const headerTabs = computed(() => room.value ? [{ const headerActions = computed<PageHeaderItem[]>(() => [{ icon: 'ti ti-dots', - text: '', handler: showMenu, }]); diff --git a/packages/frontend/src/pages/flash/flash.vue b/packages/frontend/src/pages/flash/flash.vue index f318a9f817..2913aaae64 100644 --- a/packages/frontend/src/pages/flash/flash.vue +++ b/packages/frontend/src/pages/flash/flash.vue @@ -63,6 +63,8 @@ SPDX-License-Identifier: AGPL-3.0-only <script lang="ts" setup> import { computed, onDeactivated, onUnmounted, ref, watch, shallowRef, defineAsyncComponent } from 'vue'; import * as Misskey from 'misskey-js'; +import { utils } from '@syuilo/aiscript'; +import { compareVersions } from 'compare-versions'; import { url } from '@@/js/config.js'; import type { Ref } from 'vue'; import type { AsUiComponent, AsUiRoot } from '@/aiscript/ui.js'; @@ -190,11 +192,21 @@ function start() { run(); } +function getIsLegacy(version: string | null): boolean { + if (version == null) return false; + try { + return compareVersions(version, '1.0.0') < 0; + } catch { + return false; + } +} + async function run() { if (aiscript.value) aiscript.value.abort(); if (!flash.value) return; - const isLegacy = !flash.value.script.replaceAll(' ', '').startsWith('///@1.0.0'); + const version = utils.getLangVersion(flash.value.script); + const isLegacy = version != null && 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/follow-requests.vue b/packages/frontend/src/pages/follow-requests.vue index 35e259a571..ba24d7abc6 100644 --- a/packages/frontend/src/pages/follow-requests.vue +++ b/packages/frontend/src/pages/follow-requests.vue @@ -63,14 +63,28 @@ function accept(user: Misskey.entities.UserLite) { }); } -function reject(user: Misskey.entities.UserLite) { - os.apiWithDialog('following/requests/reject', { userId: user.id }).then(() => { +async function reject(user: Misskey.entities.UserLite) { + const { canceled } = await os.confirm({ + type: 'question', + text: i18n.tsx.rejectFollowRequestConfirm({ name: user.name || user.username }), + }); + + if (canceled) return; + + await os.apiWithDialog('following/requests/reject', { userId: user.id }).then(() => { paginator.reload(); }); } -function cancel(user: Misskey.entities.UserLite) { - os.apiWithDialog('following/requests/cancel', { userId: user.id }).then(() => { +async function cancel(user: Misskey.entities.UserLite) { + const { canceled } = await os.confirm({ + type: 'question', + text: i18n.tsx.cancelFollowRequestConfirm({ name: user.name || user.username }), + }); + + if (canceled) return; + + await os.apiWithDialog('following/requests/cancel', { userId: user.id }).then(() => { paginator.reload(); }); } diff --git a/packages/frontend/src/pages/my-clips/index.vue b/packages/frontend/src/pages/my-clips/index.vue index f48dc5be4d..45faae48be 100644 --- a/packages/frontend/src/pages/my-clips/index.vue +++ b/packages/frontend/src/pages/my-clips/index.vue @@ -43,6 +43,8 @@ const paginator = markRaw(new Paginator('clips/list', { })); const favoritesPaginator = markRaw(new Paginator('clips/my-favorites', { + // ページネーションに対応していない + noPaging: true, })); async function create() { diff --git a/packages/frontend/src/pages/page.vue b/packages/frontend/src/pages/page.vue index 465167cadb..c3b52a24fd 100644 --- a/packages/frontend/src/pages/page.vue +++ b/packages/frontend/src/pages/page.vue @@ -465,6 +465,7 @@ definePage(() => ({ } .pageContent { + contain: content; margin-bottom: 1.5rem; } diff --git a/packages/frontend/src/pages/timeline.vue b/packages/frontend/src/pages/timeline.vue index f72549df07..89d0991bc0 100644 --- a/packages/frontend/src/pages/timeline.vue +++ b/packages/frontend/src/pages/timeline.vue @@ -163,7 +163,7 @@ async function chooseChannel(ev: MouseEvent): Promise<void> { type: 'link', icon: 'ti ti-plus', text: i18n.ts.createNew, - to: '/channels', + to: '/channels/new', }, ]; os.popupMenu(items.filter(i => i != null), ev.currentTarget ?? ev.target); diff --git a/packages/frontend/src/preferences.ts b/packages/frontend/src/preferences.ts index b8d5e66bf6..7b03823407 100644 --- a/packages/frontend/src/preferences.ts +++ b/packages/frontend/src/preferences.ts @@ -3,6 +3,7 @@ * SPDX-License-Identifier: AGPL-3.0-only */ +import { BroadcastChannel } from 'broadcast-channel'; import type { StorageProvider } from '@/preferences/manager.js'; import { cloudBackup } from '@/preferences/utility.js'; import { miLocalStorage } from '@/local-storage.js'; @@ -12,6 +13,7 @@ import { $i } from '@/i.js'; import { misskeyApi } from '@/utility/misskey-api.js'; import { TAB_ID } from '@/tab-id.js'; +// クラウド同期用グループ名 const syncGroup = 'default'; const io: StorageProvider = { @@ -26,7 +28,6 @@ const io: StorageProvider = { save: (ctx) => { miLocalStorage.setItem('preferences', JSON.stringify(ctx.profile)); - miLocalStorage.setItem('latestPreferencesUpdate', `${TAB_ID}/${Date.now()}`); }, cloudGet: async (ctx) => { @@ -99,33 +100,47 @@ const io: StorageProvider = { export const prefer = new PreferencesManager(io, $i); -let latestSyncedAt = Date.now(); +//#region タブ間同期 +let latestPreferencesUpdate: { + tabId: string; + timestamp: number; +} | null = null; -function syncBetweenTabs() { - const latest = miLocalStorage.getItem('latestPreferencesUpdate'); - if (latest == null) return; +const preferencesChannel = new BroadcastChannel<{ + type: 'preferencesUpdate'; + tabId: string; + timestamp: number; +}>('preferences'); - const latestTab = latest.split('/')[0]; - const latestAt = parseInt(latest.split('/')[1]); - - if (latestTab === TAB_ID) return; - if (latestAt <= latestSyncedAt) return; - - prefer.reloadProfile(); - - latestSyncedAt = Date.now(); - - if (_DEV_) console.log('prefer:synced'); -} - -window.setInterval(syncBetweenTabs, 5000); +prefer.on('committed', () => { + latestPreferencesUpdate = { + tabId: TAB_ID, + timestamp: Date.now(), + }; + preferencesChannel.postMessage({ + type: 'preferencesUpdate', + tabId: TAB_ID, + timestamp: latestPreferencesUpdate.timestamp, + }); +}); -window.document.addEventListener('visibilitychange', () => { - if (window.document.visibilityState === 'visible') { - syncBetweenTabs(); +preferencesChannel.addEventListener('message', (msg) => { + if (msg.type === 'preferencesUpdate') { + if (msg.tabId === TAB_ID) return; + if (latestPreferencesUpdate != null) { + if (msg.timestamp <= latestPreferencesUpdate.timestamp) return; + } + prefer.reloadProfile(); + if (_DEV_) console.log('prefer:received update from other tab'); + latestPreferencesUpdate = { + tabId: msg.tabId, + timestamp: msg.timestamp, + }; } }); +//#endregion +//#region 定期クラウドバックアップ let latestBackupAt = 0; window.setInterval(() => { @@ -138,6 +153,7 @@ window.setInterval(() => { latestBackupAt = Date.now(); }); }, 1000 * 60 * 3); +//#endregion if (_DEV_) { (window as any).prefer = prefer; diff --git a/packages/frontend/src/preferences/def.ts b/packages/frontend/src/preferences/def.ts index 2f2107d9ed..8d1d33977d 100644 --- a/packages/frontend/src/preferences/def.ts +++ b/packages/frontend/src/preferences/def.ts @@ -234,7 +234,7 @@ export const PREF_DEF = definePreferences({ default: false, }, disableShowingAnimatedImages: { - default: prefersReducedMotion, + default: false, }, emojiStyle: { default: 'twemoji', // twemoji / fluentEmoji / native diff --git a/packages/frontend/src/preferences/manager.ts b/packages/frontend/src/preferences/manager.ts index b6d3d55a5f..5949ee71eb 100644 --- a/packages/frontend/src/preferences/manager.ts +++ b/packages/frontend/src/preferences/manager.ts @@ -4,6 +4,7 @@ */ import { computed, onUnmounted, ref, watch } from 'vue'; +import { EventEmitter } from 'eventemitter3'; import { host, version } from '@@/js/config.js'; import { PREF_DEF } from './def.js'; import type { Ref, WritableComputedRef } from 'vue'; @@ -100,6 +101,14 @@ type PreferencesDefinitionRecord<Default, T = Default extends (...args: any) => export type PreferencesDefinition = Record<string, PreferencesDefinitionRecord<any>>; +type PreferencesManagerEvents = { + 'committed': <K extends keyof PREF>(ctx: { + key: K; + value: ValueOf<K>; + oldValue: ValueOf<K>; + }) => void; +}; + export function definePreferences<T extends Record<string, unknown>>(x: { [K in keyof T]: PreferencesDefinitionRecord<T[K]> }): { @@ -180,7 +189,7 @@ function normalizePreferences(preferences: PossiblyNonNormalizedPreferencesProfi // TODO: PreferencesManagerForGuest のような非ログイン専用のクラスを分離すればthis.currentAccountのnullチェックやaccountがnullであるスコープのレコード挿入などが不要になり綺麗になるかもしれない // と思ったけど操作アカウントが存在しない場合も考慮する現在の設計の方が汎用的かつ堅牢かもしれない // NOTE: accountDependentな設定は初期状態であってもアカウントごとのスコープでレコードを作成しておかないと、サーバー同期する際に正しく動作しなくなる -export class PreferencesManager { +export class PreferencesManager extends EventEmitter<PreferencesManagerEvents> { private io: StorageProvider; private currentAccount: { id: string } | null; public profile: PreferencesProfile; @@ -201,6 +210,8 @@ export class PreferencesManager { }; constructor(io: StorageProvider, currentAccount: { id: string } | null) { + super(); + this.io = io; this.currentAccount = currentAccount; @@ -246,6 +257,12 @@ export class PreferencesManager { this.rewriteRawState(key, v); + this.emit('committed', { + key, + value: v, + oldValue: this.s[key], + }); + const record = this.getMatchedRecordOf(key); if (parseScope(record[0]).account == null && isAccountDependentKey(key) && currentAccount != null) { diff --git a/packages/frontend/src/tab-id.ts b/packages/frontend/src/tab-id.ts index 6525763582..db8a5b147f 100644 --- a/packages/frontend/src/tab-id.ts +++ b/packages/frontend/src/tab-id.ts @@ -5,7 +5,5 @@ import { genId } from '@/utility/id.js'; -// HMR有効時にバグか知らんけど複数回実行されるのでその対策 -export const TAB_ID = window.sessionStorage.getItem('TAB_ID') ?? genId(); -window.sessionStorage.setItem('TAB_ID', TAB_ID); +export const TAB_ID = genId(); if (_DEV_) console.log('TAB_ID', TAB_ID); diff --git a/packages/frontend/src/theme.ts b/packages/frontend/src/theme.ts index 2fca4e79ae..fc3f5f55f2 100644 --- a/packages/frontend/src/theme.ts +++ b/packages/frontend/src/theme.ts @@ -158,6 +158,8 @@ export function applyTheme(theme: Theme, persist = true) { // 様々な理由により startViewTransition は失敗することがある // ref. https://github.com/misskey-dev/misskey/issues/16562 + // FIXME: viewTransitonエラーはtry~catch貫通してそうな気配がする + console.error(err); window.document.documentElement.classList.remove('_themeChanging_'); diff --git a/packages/frontend/src/types/page-header.ts b/packages/frontend/src/types/page-header.ts index e9807a2939..7232f17d47 100644 --- a/packages/frontend/src/types/page-header.ts +++ b/packages/frontend/src/types/page-header.ts @@ -4,8 +4,8 @@ */ export type PageHeaderItem = { - text: string; - icon: string; - highlighted?: boolean; - handler: (ev: MouseEvent) => void; + text?: string; + icon: string; + highlighted?: boolean; + handler: (ev: MouseEvent) => void; }; diff --git a/packages/frontend/src/ui/_common_/navbar-h.vue b/packages/frontend/src/ui/_common_/navbar-h.vue index b025dd4858..64da4647b6 100644 --- a/packages/frontend/src/ui/_common_/navbar-h.vue +++ b/packages/frontend/src/ui/_common_/navbar-h.vue @@ -4,40 +4,40 @@ SPDX-License-Identifier: AGPL-3.0-only --> <template> -<div class="azykntjl"> - <div class="body"> - <div class="left"> - <button v-click-anime class="item _button instance" @click="openInstanceMenu"> - <img :src="instance.iconUrl ?? '/favicon.ico'" draggable="false"/> +<div :class="[$style.root, acrylic ? $style.acrylic : null]"> + <div :class="$style.body"> + <div :class="$style.left"> + <button v-click-anime :class="[$style.item, $style.instance]" class="_button" @click="openInstanceMenu"> + <img :class="$style.instanceIcon" :src="instance.iconUrl ?? '/favicon.ico'" draggable="false"/> </button> - <MkA v-click-anime v-tooltip="i18n.ts.timeline" class="item index" activeClass="active" to="/" exact> - <i class="ti ti-home ti-fw"></i> + <MkA v-click-anime v-tooltip="i18n.ts.timeline" :class="$style.item" activeClass="active" to="/" exact> + <i :class="$style.itemIcon" class="ti ti-home ti-fw"></i> </MkA> <template v-for="item in menu"> - <div v-if="item === '-'" class="divider"></div> - <component :is="navbarItemDef[item].to ? 'MkA' : 'button'" v-else-if="navbarItemDef[item] && (navbarItemDef[item].show !== false)" v-click-anime v-tooltip="navbarItemDef[item].title" class="item _button" :class="item" activeClass="active" :to="navbarItemDef[item].to" v-on="navbarItemDef[item].action ? { click: navbarItemDef[item].action } : {}"> - <i class="ti-fw" :class="navbarItemDef[item].icon"></i> - <span v-if="navbarItemDef[item].indicated" class="indicator _blink"><i class="_indicatorCircle"></i></span> + <div v-if="item === '-'" :class="$style.divider"></div> + <component :is="navbarItemDef[item].to ? 'MkA' : 'button'" v-else-if="navbarItemDef[item] && (navbarItemDef[item].show !== false)" v-click-anime v-tooltip="navbarItemDef[item].title" class="_button" :class="$style.item" activeClass="active" :to="navbarItemDef[item].to" v-on="navbarItemDef[item].action ? { click: navbarItemDef[item].action } : {}"> + <i :class="[$style.itemIcon, navbarItemDef[item].icon]" class="ti-fw"></i> + <span v-if="navbarItemDef[item].indicated" :class="$style.indicator" class="_blink"><i class="_indicatorCircle"></i></span> </component> </template> - <div class="divider"></div> + <div :class="$style.divider"></div> <MkA v-if="$i && ($i.isAdmin || $i.isModerator)" v-click-anime v-tooltip="i18n.ts.controlPanel" class="item" activeClass="active" to="/admin" :behavior="settingsWindowed ? 'window' : null"> - <i class="ti ti-dashboard ti-fw"></i> + <i :class="$style.itemIcon" class="ti ti-dashboard ti-fw"></i> </MkA> - <button v-click-anime class="item _button" @click="more"> - <i class="ti ti-dots ti-fw"></i> - <span v-if="otherNavItemIndicated" class="indicator _blink"><i class="_indicatorCircle"></i></span> + <button v-click-anime :class="$style.item" class="_button" @click="more"> + <i :class="$style.itemIcon" class="ti ti-dots ti-fw"></i> + <span v-if="otherNavItemIndicated" :class="$style.indicator" class="_blink"><i class="_indicatorCircle"></i></span> </button> </div> - <div class="right"> - <MkA v-click-anime v-tooltip="i18n.ts.settings" class="item" activeClass="active" to="/settings" :behavior="settingsWindowed ? 'window' : null"> - <i class="ti ti-settings ti-fw"></i> + <div :class="$style.right"> + <MkA v-click-anime v-tooltip="i18n.ts.settings" :class="$style.item" activeClass="active" to="/settings" :behavior="settingsWindowed ? 'window' : null"> + <i :class="$style.itemIcon" class="ti ti-settings ti-fw"></i> </MkA> - <button v-if="$i" v-click-anime class="item _button account" @click="openAccountMenu"> - <MkAvatar :user="$i" class="avatar"/><MkAcct class="acct" :user="$i"/> + <button v-if="$i" v-click-anime :class="[$style.item, $style.account]" class="_button" @click="openAccountMenu"> + <MkAvatar :user="$i" :class="$style.avatar"/><MkAcct :class="$style.acct" :user="$i"/> </button> - <div class="post" @click="os.post()"> - <MkButton class="button" gradate full rounded> + <div :class="$style.post" @click="os.post()"> + <MkButton :class="$style.postButton" gradate rounded> <i class="ti ti-pencil ti-fw"></i> </MkButton> </div> @@ -61,6 +61,10 @@ import { getHTMLElementOrNull } from '@/utility/get-dom-node-or-null.js'; const WINDOW_THRESHOLD = 1400; +const props = defineProps<{ + acrylic?: boolean; +}>(); + const settingsWindowed = ref(window.innerWidth > WINDOW_THRESHOLD); const menu = ref(prefer.s.menu); // const menuDisplay = computed(store.makeGetterSetter('menuDisplay')); @@ -100,121 +104,140 @@ onMounted(() => { </script> -<style lang="scss" scoped> -.azykntjl { - $height: 60px; - $avatar-size: 32px; - $avatar-margin: 8px; +<style lang="scss" module> +.root { + --height: 60px; position: sticky; top: 0; z-index: 1000; width: 100%; - height: $height; - background: color(from var(--MI_THEME-bg) srgb r g b / 0.75); - -webkit-backdrop-filter: var(--MI-blur, blur(15px)); - backdrop-filter: var(--MI-blur, blur(15px)); + height: var(--height); + contain: strict; + background: var(--MI_THEME-navBg); - > .body { - max-width: 1380px; - margin: 0 auto; - display: flex; + &.acrylic { + background: color(from var(--MI_THEME-bg) srgb r g b / 0.75); + -webkit-backdrop-filter: var(--MI-blur, blur(15px)); + backdrop-filter: var(--MI-blur, blur(15px)); + } +} - > .right, - > .left { +.body { + max-width: 1380px; + margin: 0 auto; + display: flex; + overflow: auto; + overflow-y: clip; + white-space: nowrap; +} - > .item { - position: relative; - font-size: 0.9em; - display: inline-block; - padding: 0 12px; - line-height: $height; +.item { + position: relative; + font-size: 0.9em; + display: inline-block; + padding: 0 12px; + line-height: var(--height); - > i, - > .avatar { - margin-right: 0; - } + &:hover { + text-decoration: none; + color: light-dark(hsl(from var(--MI_THEME-navFg) h s calc(l - 17)), hsl(from var(--MI_THEME-navFg) h s calc(l + 17))); + } - > i { - left: 10px; - } + &.active { + color: var(--MI_THEME-navActive); + } +} - > .avatar { - width: $avatar-size; - height: $avatar-size; - vertical-align: middle; - } +.itemIcon { + margin-right: 0; + left: 10px; +} - > .indicator { - position: absolute; - top: 0; - left: 0; - color: var(--MI_THEME-navIndicator); - font-size: 8px; - } +.avatar { + margin-right: 0; + width: 32px; + height: 32px; + vertical-align: middle; +} - &:hover { - text-decoration: none; - color: light-dark(hsl(from var(--MI_THEME-navFg) h s calc(l - 17)), hsl(from var(--MI_THEME-navFg) h s calc(l + 17))); - } +.acct { + margin-left: 8px; - &.active { - color: var(--MI_THEME-navActive); - } - } + @media (max-width: 1200px) { + display: none; + } +} - > .divider { - display: inline-block; - height: 16px; - margin: 0 10px; - border-right: solid 0.5px var(--MI_THEME-divider); - } +.indicator { + position: absolute; + top: 0; + left: 0; + color: var(--MI_THEME-navIndicator); + font-size: 8px; +} - > .instance { - display: inline-block; - position: relative; - width: 56px; - height: 100%; - vertical-align: bottom; +.divider { + display: inline-block; + height: 16px; + margin: 0 10px; + border-right: solid 0.5px var(--MI_THEME-divider); +} - > img { - display: inline-block; - width: 24px; - position: absolute; - top: 0; - right: 0; - bottom: 0; - left: 0; - margin: auto; - } - } +.instance { + display: inline-block; + position: relative; + width: 56px; + height: 100%; + vertical-align: bottom; + position: sticky; + top: 0; + left: 0; + z-index: 1; +} - > .post { - display: inline-block; +.instanceIcon { + display: inline-block; + width: 24px; + position: absolute; + top: 0; + right: 0; + bottom: 0; + left: 0; + margin: auto; +} - > .button { - width: 40px; - height: 40px; - padding: 0; - min-width: 0; - } - } +.right { + display: flex; + align-items: center; + margin-left: auto; + position: sticky; + top: 0; + right: 0; + z-index: 1; + contain: content; + background: var(--MI_THEME-navBg); +} +.acrylic .right { + background: transparent; +} - > .account { - display: inline-flex; - align-items: center; - vertical-align: top; - margin-right: 8px; +.post { + display: inline-block; + margin-right: 8px; +} - > .acct { - margin-left: 8px; - } - } - } +.postButton { + width: 40px; + height: 40px; + padding: 0; + min-width: 0; +} - > .right { - margin-left: auto; - } - } +.account { + display: inline-flex; + align-items: center; + vertical-align: top; + margin-right: 8px; } </style> diff --git a/packages/frontend/src/ui/deck.vue b/packages/frontend/src/ui/deck.vue index ff8e91663a..0941c25467 100644 --- a/packages/frontend/src/ui/deck.vue +++ b/packages/frontend/src/ui/deck.vue @@ -11,7 +11,7 @@ SPDX-License-Identifier: AGPL-3.0-only <XSidebar v-if="!isMobile && prefer.r['deck.navbarPosition'].value === 'left'"/> <div :class="[$style.main, { [$style.withWallpaper]: withWallpaper, [$style.withSidebarAndTitlebar]: !isMobile && prefer.r['deck.navbarPosition'].value === 'left' && prefer.r.showTitlebar.value }]" :style="{ backgroundImage: prefer.s['deck.wallpaper'] != null ? `url(${ prefer.s['deck.wallpaper'] })` : '' }"> - <XNavbarH v-if="!isMobile && prefer.r['deck.navbarPosition'].value === 'top'"/> + <XNavbarH v-if="!isMobile && prefer.r['deck.navbarPosition'].value === 'top'" :acrylic="withWallpaper"/> <XReloadSuggestion v-if="shouldSuggestReload"/> <XPreferenceRestore v-if="shouldSuggestRestoreBackup"/> @@ -71,7 +71,7 @@ SPDX-License-Identifier: AGPL-3.0-only </div> </div> - <XNavbarH v-if="!isMobile && prefer.r['deck.navbarPosition'].value === 'bottom'"/> + <XNavbarH v-if="!isMobile && prefer.r['deck.navbarPosition'].value === 'bottom'" :acrylic="withWallpaper"/> <XMobileFooterMenu v-if="isMobile" v-model:drawerMenuShowing="drawerMenuShowing" v-model:widgetsShowing="widgetsShowing"/> </div> diff --git a/packages/frontend/src/utility/tour.ts b/packages/frontend/src/utility/tour.ts index 8ab3474ddc..c6bfa35a66 100644 --- a/packages/frontend/src/utility/tour.ts +++ b/packages/frontend/src/utility/tour.ts @@ -3,7 +3,7 @@ * SPDX-License-Identifier: AGPL-3.0-only */ -import { computed, ref, shallowRef, watch } from 'vue'; +import { computed, ref, shallowRef, watch, defineAsyncComponent } from 'vue'; import * as os from '@/os.js'; type TourStep = { @@ -26,7 +26,7 @@ export function startTour(steps: TourStep[]) { anchorElementRef.value = step.element; }); - const { dispose } = os.popup(await import('@/components/MkSpot.vue').then(x => x.default), { + const { dispose } = os.popup(defineAsyncComponent(() => import('@/components/MkSpot.vue')), { title: titleRef, description: descriptionRef, anchorElement: anchorElementRef, diff --git a/packages/icons-subsetter/package.json b/packages/icons-subsetter/package.json index 88dcc5b7ba..9ca6ee35cb 100644 --- a/packages/icons-subsetter/package.json +++ b/packages/icons-subsetter/package.json @@ -11,10 +11,10 @@ "lint": "pnpm typecheck && pnpm eslint" }, "devDependencies": { - "@types/node": "24.9.2", + "@types/node": "24.10.1", "@types/wawoff2": "1.0.2", - "@typescript-eslint/eslint-plugin": "8.46.2", - "@typescript-eslint/parser": "8.46.2" + "@typescript-eslint/eslint-plugin": "8.47.0", + "@typescript-eslint/parser": "8.47.0" }, "dependencies": { "@tabler/icons-webfont": "3.35.0", diff --git a/packages/misskey-bubble-game/package.json b/packages/misskey-bubble-game/package.json index 032a11add7..5d3f386c96 100644 --- a/packages/misskey-bubble-game/package.json +++ b/packages/misskey-bubble-game/package.json @@ -25,14 +25,14 @@ }, "devDependencies": { "@types/matter-js": "0.20.2", - "@types/node": "24.9.2", + "@types/node": "24.10.1", "@types/seedrandom": "3.0.8", - "@typescript-eslint/eslint-plugin": "8.46.2", - "@typescript-eslint/parser": "8.46.2", - "esbuild": "0.25.11", + "@typescript-eslint/eslint-plugin": "8.47.0", + "@typescript-eslint/parser": "8.47.0", + "esbuild": "0.27.0", "execa": "9.6.0", - "glob": "11.0.3", - "nodemon": "3.1.10", + "glob": "11.1.0", + "nodemon": "3.1.11", "typescript": "5.9.3" }, "files": [ diff --git a/packages/misskey-js/generator/package.json b/packages/misskey-js/generator/package.json index c5cfdaaf79..7c4a12552d 100644 --- a/packages/misskey-js/generator/package.json +++ b/packages/misskey-js/generator/package.json @@ -7,16 +7,16 @@ "generate": "tsx src/generator.ts && eslint ./built/**/*.ts --fix" }, "devDependencies": { - "@readme/openapi-parser": "5.2.0", - "@types/node": "24.9.2", - "@typescript-eslint/eslint-plugin": "8.46.2", - "@typescript-eslint/parser": "8.46.2", + "@readme/openapi-parser": "5.2.1", + "@types/node": "24.10.1", + "@typescript-eslint/eslint-plugin": "8.47.0", + "@typescript-eslint/parser": "8.47.0", "openapi-types": "12.1.3", "openapi-typescript": "7.10.1", "ts-case-convert": "2.1.0", "tsx": "4.20.6", "typescript": "5.9.3", - "eslint": "9.39.0" + "eslint": "9.39.1" }, "files": [ "built" diff --git a/packages/misskey-js/package.json b/packages/misskey-js/package.json index a716d17ec9..a961f2a25c 100644 --- a/packages/misskey-js/package.json +++ b/packages/misskey-js/package.json @@ -1,7 +1,7 @@ { "type": "module", "name": "misskey-js", - "version": "2025.11.0", + "version": "2025.11.1", "description": "Misskey SDK for JavaScript", "license": "MIT", "main": "./built/index.js", @@ -37,19 +37,19 @@ "directory": "packages/misskey-js" }, "devDependencies": { - "@microsoft/api-extractor": "7.53.3", - "@types/node": "24.9.2", - "@typescript-eslint/eslint-plugin": "8.46.2", - "@typescript-eslint/parser": "8.46.2", - "@vitest/coverage-v8": "3.2.4", - "esbuild": "0.25.11", + "@microsoft/api-extractor": "7.55.0", + "@types/node": "24.10.1", + "@typescript-eslint/eslint-plugin": "8.47.0", + "@typescript-eslint/parser": "8.47.0", + "@vitest/coverage-v8": "4.0.10", + "esbuild": "0.27.0", "execa": "9.6.0", - "glob": "11.0.3", + "glob": "13.0.0", "ncp": "2.0.0", - "nodemon": "3.1.10", + "nodemon": "3.1.11", "tsd": "0.33.0", "typescript": "5.9.3", - "vitest": "3.2.4", + "vitest": "4.0.10", "vitest-websocket-mock": "0.5.0" }, "files": [ diff --git a/packages/misskey-reversi/package.json b/packages/misskey-reversi/package.json index 7848ccddd0..1e733ecbf4 100644 --- a/packages/misskey-reversi/package.json +++ b/packages/misskey-reversi/package.json @@ -24,13 +24,13 @@ "lint": "pnpm typecheck && pnpm eslint" }, "devDependencies": { - "@types/node": "24.9.2", - "@typescript-eslint/eslint-plugin": "8.46.2", - "@typescript-eslint/parser": "8.46.2", - "esbuild": "0.25.11", + "@types/node": "24.10.1", + "@typescript-eslint/eslint-plugin": "8.47.0", + "@typescript-eslint/parser": "8.47.0", + "esbuild": "0.27.0", "execa": "9.6.0", - "glob": "11.0.3", - "nodemon": "3.1.10", + "glob": "11.1.0", + "nodemon": "3.1.11", "typescript": "5.9.3" }, "files": [ diff --git a/packages/sw/package.json b/packages/sw/package.json index 4e35e17b7a..51d78511c2 100644 --- a/packages/sw/package.json +++ b/packages/sw/package.json @@ -9,15 +9,15 @@ "lint": "pnpm typecheck && pnpm eslint" }, "dependencies": { - "esbuild": "0.25.11", + "esbuild": "0.27.0", "idb-keyval": "6.2.2", "misskey-js": "workspace:*" }, "devDependencies": { - "@typescript-eslint/parser": "8.46.2", + "@typescript-eslint/parser": "8.47.0", "@typescript/lib-webworker": "npm:@types/serviceworker@0.0.74", "eslint-plugin-import": "2.32.0", - "nodemon": "3.1.10", + "nodemon": "3.1.11", "typescript": "5.9.3" }, "type": "module" |