diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index bacad34f70..3f888504b7 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -5,7 +5,7 @@ "workspaceFolder": "/workspace", "features": { "ghcr.io/devcontainers/features/node:1": { - "version": "24.10.0" + "version": "22.15.0" }, "ghcr.io/devcontainers-extra/features/pnpm:2": { "version": "10.10.0" diff --git a/.node-version b/.node-version index 21651351e2..b8ffd70759 100644 --- a/.node-version +++ b/.node-version @@ -1 +1 @@ -24.10.0 +22.15.0 diff --git a/CHANGELOG.md b/CHANGELOG.md index a30bf8c441..a829f3032c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,8 +1,34 @@ -## 2025.12.0 +## Unreleased ### General - +### Client +- + +### Server +- + + +## 2025.12.1 + +### Client +- Fix: 特定の条件下でMisskeyが起動せず空白のページが表示されることがある問題を軽減 +- Fix: 初回読み込み時などに、言語設定で不整合が発生することがある問題を修正 +- Fix: 削除されたノートのリノートが正しく動作されない問題を修正 +- Fix: チャンネルオーナーが削除済みの時にチャンネルのヘッダーメニューが表示されない不具合を修正 +- Fix: ドライブで登録日以外でソートする場合は月でグループ化して表示しないように +- Fix: `null` を返す note_view_intrruptor プラグインが動作しない問題を修正 + +### Server +- Fix: ジョブキューでSentryが有効にならない問題を修正 + + +## 2025.12.0 + +### Note +- configの`trustProxy`のデフォルト値を`false`に変更しました。アップデート前に現在のconfigをご確認の上、必要に応じて値を変更してください。 + ### Client - Fix: stacking router viewで連続して戻る操作を行うと何も表示されなくなる問題を修正 @@ -10,7 +36,7 @@ - Enhance: メモリ使用量を削減しました - Enhance: ActivityPubアクティビティを送信する際のパフォーマンス向上 - Enhance: 依存関係の更新 - +- Fix: セキュリティに関する修正 ## 2025.11.1 diff --git a/Dockerfile b/Dockerfile index a071970927..02739d9ca2 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,6 +1,6 @@ # syntax = docker/dockerfile:1.4 -ARG NODE_VERSION=24.10.0-bookworm +ARG NODE_VERSION=22.15.0-bookworm # build assets & compile TypeScript diff --git a/compose.local-db.yml b/compose.local-db.yml index 4703b16fc5..2b69cabf5a 100644 --- a/compose.local-db.yml +++ b/compose.local-db.yml @@ -21,7 +21,7 @@ services: env_file: - .config/docker.env volumes: - - ./db:/var/lib/postgresql/data + - ./db:/var/lib/postgresql healthcheck: test: "pg_isready -U $$POSTGRES_USER -d $$POSTGRES_DB" interval: 5s diff --git a/compose_example.yml b/compose_example.yml index 70de5bba7b..efc7d6624d 100644 --- a/compose_example.yml +++ b/compose_example.yml @@ -43,7 +43,7 @@ services: env_file: - .config/docker.env volumes: - - ./db:/var/lib/postgresql/data + - ./db:/var/lib/postgresql healthcheck: test: "pg_isready -U $$POSTGRES_USER -d $$POSTGRES_DB" interval: 5s diff --git a/locales/de-DE.yml b/locales/de-DE.yml index ba7ca27e66..33e19f1cc1 100644 --- a/locales/de-DE.yml +++ b/locales/de-DE.yml @@ -83,6 +83,7 @@ files: "Dateien" download: "Herunterladen" driveFileDeleteConfirm: "Möchtest du die Datei „{name}“ wirklich löschen? Einige Inhalte, die diese Datei verwenden, werden auch verschwinden." unfollowConfirm: "Möchtest du {name} wirklich nicht mehr folgen?" +rejectFollowRequestConfirm: "Möchtest du die Follow-Anfrage von {name} ablehnen?" exportRequested: "Du hast einen Export angefragt. Dies kann etwas Zeit in Anspruch nehmen. Sobald der Export abgeschlossen ist, wird er deiner Drive hinzugefügt." importRequested: "Du hast einen Import angefragt. Dies kann etwas Zeit in Anspruch nehmen." lists: "Listen" @@ -1018,6 +1019,7 @@ pushNotificationAlreadySubscribed: "Push-Benachrichtigungen sind bereits aktivie pushNotificationNotSupported: "Entweder dein Browser oder deine Instanz unterstützt Push-Benachrichtigungen nicht" sendPushNotificationReadMessage: "Push-Benachrichtigungen löschen, sobald sie gelesen wurden" sendPushNotificationReadMessageCaption: "Dies kann gegebenenfalls den Batterieverbrauch deines Gerätes erhöhen." +pleaseAllowPushNotification: "Bitte erlauben Sie Benachrichtigungen in Ihrem Browser." windowMaximize: "Maximieren" windowMinimize: "Minimieren" windowRestore: "Wiederherstellen" @@ -1054,6 +1056,7 @@ permissionDeniedError: "Aktion verweigert" permissionDeniedErrorDescription: "Dieses Benutzerkonto besitzt nicht die Berechtigung, um diese Aktion auszuführen." preset: "Vorlage" selectFromPresets: "Aus Vorlagen wählen" +custom: "Benutzerdefiniert" achievements: "Errungenschaften" gotInvalidResponseError: "Ungültige Antwort des Servers" gotInvalidResponseErrorDescription: "Eventuell ist der Server momentan nicht erreichbar oder untergeht Wartungsarbeiten. Bitte versuche es später noch einmal." @@ -1243,6 +1246,7 @@ releaseToRefresh: "Zum Aktualisieren loslassen" refreshing: "Wird aktualisiert..." pullDownToRefresh: "Zum Aktualisieren ziehen" useGroupedNotifications: "Benachrichtigungen gruppieren" +emailVerificationFailedError: "Es gab ein Problem bei der Überprüfung Ihrer E-Mail-Adresse. Der Link ist möglicherweise abgelaufen." cwNotationRequired: "Ist \"Inhaltswarnung verwenden\" aktiviert, muss eine Beschreibung gegeben werden." doReaction: "Reagieren" code: "Code" @@ -1370,7 +1374,12 @@ defaultImageCompressionLevel: "Standard-Bildkomprimierungsstufe" defaultImageCompressionLevel_description: "Ein niedrigerer Wert erhält die Bildqualität, erhöht aber die Dateigröße.
Höhere Werte reduzieren die Dateigröße, verringern aber die Bildqualität." inMinutes: "Minute(n)" inDays: "Tag(en)" +safeModeEnabled: "Der abgesicherte Modus ist aktiviert." +schedule: "Planen" +scheduled: "Geplant" widgets: "Widgets" +deviceInfo: "Geräteinformation" +youAreAdmin: "Sie sind ein Administrator" presets: "Vorlage" _imageEditing: _vars: diff --git a/locales/en-US.yml b/locales/en-US.yml index fc979ef2b0..22e9d6eeb9 100644 --- a/locales/en-US.yml +++ b/locales/en-US.yml @@ -83,6 +83,8 @@ files: "Files" download: "Download" driveFileDeleteConfirm: "Are you sure you want to delete \"{name}\"? All notes with this file attached will also be deleted." unfollowConfirm: "Are you sure you want to unfollow {name}?" +cancelFollowRequestConfirm: "Are you sure that you want to cancel your follow request to {name}?" +rejectFollowRequestConfirm: "Are you sure that you want to reject the follow request from {name}?" exportRequested: "You've requested an export. This may take a while. It will be added to your Drive once completed." importRequested: "You've requested an import. This may take a while." lists: "Lists" diff --git a/locales/es-ES.yml b/locales/es-ES.yml index 259dcadd2c..dddfa4d57b 100644 --- a/locales/es-ES.yml +++ b/locales/es-ES.yml @@ -1252,7 +1252,7 @@ detachAll: "Quitar todo" angle: "Ángulo" flip: "Echar de un capirotazo" showAvatarDecorations: "Mostrar decoraciones de avatar" -releaseToRefresh: "Soltar para recargar" +releaseToRefresh: "Suelta para recargar" refreshing: "Recargando..." pullDownToRefresh: "Tira hacia abajo para recargar" useGroupedNotifications: "Mostrar notificaciones agrupadas" diff --git a/locales/zh-CN.yml b/locales/zh-CN.yml index c5eb96fbb3..caae928605 100644 --- a/locales/zh-CN.yml +++ b/locales/zh-CN.yml @@ -53,7 +53,7 @@ copyRemoteLink: "复制远程链接" copyLinkRenote: "复制转帖链接" delete: "删除" deleteAndEdit: "删除并编辑" -deleteAndEditConfirm: "要删除此帖并再次编辑吗?对此帖的所有回应、转发和回复也将被删除。" +deleteAndEditConfirm: "要删除此帖并再次编辑吗?此帖下所有的回应、转发和回复也将被删除。" addToList: "添加至列表" addToAntenna: "添加到天线" sendMessage: "发送消息" @@ -136,7 +136,7 @@ emojiPicker: "表情符号选择器" pinnedEmojisForReactionSettingDescription: "可以设置发表回应时置顶显示的表情符号" pinnedEmojisSettingDescription: "可以设置输入表情符号时置顶显示的表情符号" emojiPickerDisplay: "选择器显示设置" -overwriteFromPinnedEmojisForReaction: "从「置顶(回应)」设置覆盖" +overwriteFromPinnedEmojisForReaction: "使用「置顶(回应)」设置覆盖" overwriteFromPinnedEmojis: "从全局设置覆盖" reactionSettingDescription2: "拖动重新排序,单击删除,点击 + 添加。" rememberNoteVisibility: "保存上次设置的可见性" @@ -153,8 +153,8 @@ block: "拉黑" unblock: "取消拉黑" suspend: "冻结" unsuspend: "解除冻结" -blockConfirm: "确定要拉黑吗?" -unblockConfirm: "确定要取消拉黑吗?" +blockConfirm: "确定要屏蔽吗?" +unblockConfirm: "确定要取消屏蔽吗?" suspendConfirm: "要冻结吗?" unsuspendConfirm: "要解除冻结吗?" selectList: "选择列表" @@ -174,7 +174,7 @@ emojiUrl: "emoji 地址" addEmoji: "添加表情符号" settingGuide: "推荐配置" cacheRemoteFiles: "缓存远程文件" -cacheRemoteFilesDescription: "启用此设定时,将在此服务器上缓存远程文件。虽然可以加快图片显示的速度,但是相对的会消耗大量的服务器存储空间。用户角色内的网盘容量决定了这个远程用户能在服务器上保留多少缓存。当超出了这个限制时,旧的文件将从缓存中被删除,成为链接。当禁用此设定时,则是从一开始就将远程文件保留为链接。此时推荐将 default.yml 的 proxyRemoteFiles 设置为 true 以优化缩略图生成及保护用户隐私。" +cacheRemoteFilesDescription: "启用此设定时,将在此服务器上缓存远程文件。虽然可以加快图片显示的速度,但是相对的会消耗大量的服务器存储空间。用户角色内的网盘容量决定了这个远程用户能在服务器上保留多少缓存。当超出了这个限制时,旧的文件将从缓存中被删除,成为链接。当禁用此设定时,则是从一开始就将远程文件保留为链接。此时推荐将 的 proxyRemoteFiles 设置为 true 以优化缩略图生成及保护用户隐私。" youCanCleanRemoteFilesCache: "可以使用文件管理的🗑️按钮来删除所有的缓存。" cacheRemoteSensitiveFiles: "缓存远程敏感媒体文件" cacheRemoteSensitiveFilesDescription: "如果禁用这项设定,远程服务器的敏感媒体将不会被缓存,而是直接链接。" @@ -184,7 +184,7 @@ flagAsCat: "喵!!!!!!!!!!!!" flagAsCatDescription: "喵喵喵??" flagShowTimelineReplies: "在时间线上显示帖子的回复" flagShowTimelineRepliesDescription: "启用时,时间线除了显示用户的帖子外,还会显示其他用户对帖子的回复。" -autoAcceptFollowed: "自动允许来自我关注的用户对我的关注请求" +autoAcceptFollowed: "自动允许回关请求" addAccount: "添加账户" reloadAccountsList: "更新账户列表" loginFailed: "登录失败" @@ -247,8 +247,8 @@ mediaSilencedInstancesDescription: "设置要隐藏媒体文件的服务器, federationAllowedHosts: "允许联合的服务器" federationAllowedHostsDescription: "设定允许联合的服务器,以换行分隔。" muteAndBlock: "屏蔽/拉黑" -mutedUsers: "已屏蔽用户" -blockedUsers: "已拉黑的用户" +mutedUsers: "已静音的用户" +blockedUsers: "已屏蔽的用户" noUsers: "无用户" editProfile: "编辑资料" noteDeleteConfirm: "确定要删除该帖子吗?" @@ -1344,7 +1344,7 @@ skip: "跳过" restore: "恢复" syncBetweenDevices: "设备间同步" preferenceSyncConflictTitle: "服务器上已存在设定值" -preferenceSyncConflictText: "服务器上已有此设置的设定值。要覆盖哪个设定值?" +preferenceSyncConflictText: "即将保存设定值到服务器,但检测到服务器上已有此设置的设定值。要使用哪个设定值?" preferenceSyncConflictChoiceMerge: "合并" preferenceSyncConflictChoiceServer: "服务器上的设定值" preferenceSyncConflictChoiceDevice: "设备上的设定值" @@ -3270,7 +3270,7 @@ _watermarkEditor: driveFileTypeWarn: "不支持此文件" driveFileTypeWarnDescription: "请选择图像文件" title: "编辑水印" - cover: "覆盖全体" + cover: "覆盖所有" repeat: "平铺" preserveBoundingRect: "调整为旋转时不超出范围" opacity: "不透明度" diff --git a/package.json b/package.json index 2753b63bb8..edbea6830a 100644 --- a/package.json +++ b/package.json @@ -1,12 +1,12 @@ { "name": "misskey", - "version": "2025.12.0-alpha.2", + "version": "2025.12.1", "codename": "nasubi", "repository": { "type": "git", "url": "https://github.com/misskey-dev/misskey.git" }, - "packageManager": "pnpm@10.23.0", + "packageManager": "pnpm@10.24.0", "workspaces": [ "packages/misskey-js", "packages/i18n", @@ -60,8 +60,6 @@ "cssnano": "7.1.2", "esbuild": "0.27.0", "execa": "9.6.0", - "fast-glob": "3.3.3", - "glob": "13.0.0", "ignore-walk": "8.0.0", "js-yaml": "4.1.1", "postcss": "8.5.6", @@ -81,7 +79,7 @@ "eslint": "9.39.1", "globals": "16.5.0", "ncp": "2.0.0", - "pnpm": "10.23.0", + "pnpm": "10.24.0", "start-server-and-test": "2.1.3" }, "optionalDependencies": { diff --git a/packages/backend/package.json b/packages/backend/package.json index 3f5ce22617..8fea771eaf 100644 --- a/packages/backend/package.json +++ b/packages/backend/package.json @@ -65,11 +65,10 @@ "utf-8-validate": "6.0.5" }, "dependencies": { - "@aws-sdk/client-s3": "3.937.0", - "@aws-sdk/lib-storage": "3.937.0", + "@aws-sdk/client-s3": "3.940.0", + "@aws-sdk/lib-storage": "3.940.0", "@discordapp/twemoji": "16.0.1", "@fastify/accepts": "5.0.3", - "@fastify/cookie": "11.0.2", "@fastify/cors": "11.1.0", "@fastify/express": "4.0.2", "@fastify/http-proxy": "11.3.0", @@ -78,13 +77,13 @@ "@kitajs/html": "4.2.11", "@misskey-dev/sharp-read-bmp": "1.2.0", "@misskey-dev/summaly": "5.2.5", - "@napi-rs/canvas": "0.1.82", + "@napi-rs/canvas": "0.1.83", "@nestjs/common": "11.1.9", "@nestjs/core": "11.1.9", "@nestjs/testing": "11.1.9", "@peertube/http-signature": "1.7.0", - "@sentry/node": "10.26.0", - "@sentry/profiling-node": "10.26.0", + "@sentry/node": "10.27.0", + "@sentry/profiling-node": "10.27.0", "@simplewebauthn/server": "13.2.2", "@sinonjs/fake-timers": "15.0.0", "@smithy/node-http-handler": "4.4.5", @@ -99,9 +98,8 @@ "bcryptjs": "3.0.3", "blurhash": "2.0.5", "body-parser": "2.2.1", - "bullmq": "5.64.1", + "bullmq": "5.65.0", "cacheable-lookup": "7.0.0", - "cbor": "10.0.11", "chalk": "5.6.2", "chalk-template": "1.1.2", "chokidar": "4.0.3", @@ -115,17 +113,16 @@ "file-type": "21.1.1", "fluent-ffmpeg": "2.1.3", "form-data": "4.0.5", - "got": "14.6.4", + "got": "14.6.5", "hpagent": "1.2.0", "http-link-header": "1.1.3", "i18n": "workspace:*", "ioredis": "5.8.2", "ip-cidr": "4.0.2", - "ipaddr.js": "2.2.0", + "ipaddr.js": "2.3.0", "is-svg": "6.1.0", "json5": "2.2.3", "jsonld": "9.0.0", - "jsrsasign": "11.1.0", "juice": "11.0.3", "meilisearch": "0.54.0", "mfm-js": "0.25.0", @@ -137,15 +134,14 @@ "nested-property": "4.0.0", "node-fetch": "3.3.2", "node-html-parser": "7.0.1", - "nodemailer": "7.0.10", + "nodemailer": "7.0.11", "nsfwjs": "4.2.0", - "oauth": "0.10.2", "oauth2orize": "1.12.0", "oauth2orize-pkce": "0.1.2", "os-utils": "0.0.14", "otpauth": "9.4.1", "pg": "8.16.3", - "pkce-challenge": "5.0.0", + "pkce-challenge": "5.0.1", "probe-image-size": "7.2.3", "promise-limit": "2.7.0", "qrcode": "1.5.4", @@ -168,7 +164,6 @@ "tinycolor2": "1.6.0", "tmp": "0.2.5", "tsc-alias": "1.8.16", - "tsconfig-paths": "4.2.0", "typeorm": "0.3.27", "typescript": "5.9.3", "ulid": "3.0.1", @@ -180,7 +175,7 @@ "devDependencies": { "@kitajs/ts-html-plugin": "4.1.3", "@nestjs/platform-express": "11.1.9", - "@sentry/vue": "10.26.0", + "@sentry/vue": "10.27.0", "@simplewebauthn/types": "12.0.0", "@types/accepts": "1.3.7", "@types/archiver": "7.0.0", @@ -191,12 +186,10 @@ "@types/http-link-header": "1.0.7", "@types/js-yaml": "4.0.9", "@types/jsonld": "1.5.15", - "@types/jsrsasign": "10.5.15", "@types/mime-types": "3.0.1", "@types/ms": "2.1.0", "@types/node": "24.10.1", "@types/nodemailer": "7.0.4", - "@types/oauth": "0.9.6", "@types/oauth2orize": "1.11.5", "@types/oauth2orize-pkce": "0.1.2", "@types/pg": "8.15.6", @@ -214,10 +207,10 @@ "@types/vary": "1.1.3", "@types/web-push": "3.6.4", "@types/ws": "8.18.1", - "@typescript-eslint/eslint-plugin": "8.47.0", - "@typescript-eslint/parser": "8.47.0", - "@vitest/coverage-v8": "4.0.13", + "@typescript-eslint/eslint-plugin": "8.48.0", + "@typescript-eslint/parser": "8.48.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", diff --git a/packages/backend/src/core/entities/NoteEntityService.ts b/packages/backend/src/core/entities/NoteEntityService.ts index 6871ba2c72..e7847ba74e 100644 --- a/packages/backend/src/core/entities/NoteEntityService.ts +++ b/packages/backend/src/core/entities/NoteEntityService.ts @@ -15,6 +15,7 @@ import type { UsersRepository, NotesRepository, FollowingsRepository, PollsRepos import { bindThis } from '@/decorators.js'; import { DebounceLoader } from '@/misc/loader.js'; import { IdService } from '@/core/IdService.js'; +import { shouldHideNoteByTime } from '@/misc/should-hide-note-by-time.js'; import { ReactionsBufferingService } from '@/core/ReactionsBufferingService.js'; import type { OnModuleInit } from '@nestjs/common'; import type { CustomEmojiService } from '../CustomEmojiService.js'; @@ -116,12 +117,7 @@ export class NoteEntityService implements OnModuleInit { private treatVisibility(packedNote: Packed<'Note'>): Packed<'Note'>['visibility'] { if (packedNote.visibility === 'public' || packedNote.visibility === 'home') { const followersOnlyBefore = packedNote.user.makeNotesFollowersOnlyBefore; - if ((followersOnlyBefore != null) - && ( - (followersOnlyBefore <= 0 && (Date.now() - new Date(packedNote.createdAt).getTime() > 0 - (followersOnlyBefore * 1000))) - || (followersOnlyBefore > 0 && (new Date(packedNote.createdAt).getTime() < followersOnlyBefore * 1000)) - ) - ) { + if (shouldHideNoteByTime(followersOnlyBefore, packedNote.createdAt)) { packedNote.visibility = 'followers'; } } @@ -141,12 +137,7 @@ export class NoteEntityService implements OnModuleInit { if (!hide) { const hiddenBefore = packedNote.user.makeNotesHiddenBefore; - if ((hiddenBefore != null) - && ( - (hiddenBefore <= 0 && (Date.now() - new Date(packedNote.createdAt).getTime() > 0 - (hiddenBefore * 1000))) - || (hiddenBefore > 0 && (new Date(packedNote.createdAt).getTime() < hiddenBefore * 1000)) - ) - ) { + if (shouldHideNoteByTime(hiddenBefore, packedNote.createdAt)) { hide = true; } } diff --git a/packages/backend/src/misc/should-hide-note-by-time.ts b/packages/backend/src/misc/should-hide-note-by-time.ts new file mode 100644 index 0000000000..ea1951e66c --- /dev/null +++ b/packages/backend/src/misc/should-hide-note-by-time.ts @@ -0,0 +1,29 @@ +/* + * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-License-Identifier: AGPL-3.0-only + */ + +/** + * ノートが指定された時間条件に基づいて非表示対象かどうかを判定する + * @param hiddenBefore 非表示条件(負の値: 作成からの経過秒数、正の値: UNIXタイムスタンプ秒、null: 判定しない) + * @param createdAt ノートの作成日時(ISO 8601形式の文字列 または Date オブジェクト) + * @returns 非表示にすべき場合は true + */ +export function shouldHideNoteByTime(hiddenBefore: number | null | undefined, createdAt: string | Date): boolean { + if (hiddenBefore == null) { + return false; + } + + const createdAtTime = typeof createdAt === 'string' ? new Date(createdAt).getTime() : createdAt.getTime(); + + if (hiddenBefore <= 0) { + // 負の値: 作成からの経過時間(秒)で判定 + const elapsedSeconds = (Date.now() - createdAtTime) / 1000; + const hideAfterSeconds = Math.abs(hiddenBefore); + return elapsedSeconds >= hideAfterSeconds; + } else { + // 正の値: 絶対的なタイムスタンプ(秒)で判定 + const createdAtSeconds = createdAtTime / 1000; + return createdAtSeconds <= hiddenBefore; + } +} diff --git a/packages/backend/src/queue/QueueProcessorService.ts b/packages/backend/src/queue/QueueProcessorService.ts index 306fdb41f6..2b3b3fc0ad 100644 --- a/packages/backend/src/queue/QueueProcessorService.ts +++ b/packages/backend/src/queue/QueueProcessorService.ts @@ -157,7 +157,7 @@ export class QueueProcessorService implements OnApplicationShutdown { } let Sentry: typeof import('@sentry/node') | undefined; - if (Sentry != null) { + if (this.config.sentryForBackend) { import('@sentry/node').then((mod) => { Sentry = mod; }); diff --git a/packages/backend/src/queue/processors/ExportClipsProcessorService.ts b/packages/backend/src/queue/processors/ExportClipsProcessorService.ts index 486dc4c01f..be7d4e9e21 100644 --- a/packages/backend/src/queue/processors/ExportClipsProcessorService.ts +++ b/packages/backend/src/queue/processors/ExportClipsProcessorService.ts @@ -5,21 +5,20 @@ import * as fs from 'node:fs'; import { Writable } from 'node:stream'; -import { Inject, Injectable, StreamableFile } from '@nestjs/common'; -import { MoreThan } from 'typeorm'; +import { Inject, Injectable } from '@nestjs/common'; import { format as dateFormat } from 'date-fns'; import { DI } from '@/di-symbols.js'; -import type { ClipNotesRepository, ClipsRepository, MiClip, MiClipNote, MiUser, NotesRepository, PollsRepository, UsersRepository } from '@/models/_.js'; +import type { ClipNotesRepository, ClipsRepository, MiClip, MiClipNote, MiUser, PollsRepository, UsersRepository } from '@/models/_.js'; import type Logger from '@/logger.js'; import { DriveService } from '@/core/DriveService.js'; import { createTemp } from '@/misc/create-temp.js'; import type { MiPoll } from '@/models/Poll.js'; import type { MiNote } from '@/models/Note.js'; import { bindThis } from '@/decorators.js'; -import { DriveFileEntityService } from '@/core/entities/DriveFileEntityService.js'; -import { Packed } from '@/misc/json-schema.js'; import { IdService } from '@/core/IdService.js'; import { NotificationService } from '@/core/NotificationService.js'; +import { QueryService } from '@/core/QueryService.js'; +import { shouldHideNoteByTime } from '@/misc/should-hide-note-by-time.js'; import { QueueLoggerService } from '../QueueLoggerService.js'; import type * as Bull from 'bullmq'; import type { DbJobDataWithUser } from '../types.js'; @@ -43,6 +42,7 @@ export class ExportClipsProcessorService { private driveService: DriveService, private queueLoggerService: QueueLoggerService, + private queryService: QueryService, private idService: IdService, private notificationService: NotificationService, ) { @@ -100,16 +100,16 @@ export class ExportClipsProcessorService { }); while (true) { - const clips = await this.clipsRepository.find({ - where: { - userId: user.id, - ...(cursor ? { id: MoreThan(cursor) } : {}), - }, - take: 100, - order: { - id: 1, - }, - }); + const query = this.clipsRepository.createQueryBuilder('clip') + .where('clip.userId = :userId', { userId: user.id }) + .orderBy('clip.id', 'ASC') + .take(100); + + if (cursor) { + query.andWhere('clip.id > :cursor', { cursor }); + } + + const clips = await query.getMany(); if (clips.length === 0) { job.updateProgress(100); @@ -124,7 +124,7 @@ export class ExportClipsProcessorService { const isFirst = exportedClipsCount === 0; await writer.write(isFirst ? content : ',\n' + content); - await this.processClipNotes(writer, clip.id); + await this.processClipNotes(writer, clip.id, user.id); await writer.write(']}'); exportedClipsCount++; @@ -134,22 +134,25 @@ export class ExportClipsProcessorService { } } - async processClipNotes(writer: WritableStreamDefaultWriter, clipId: string): Promise { + async processClipNotes(writer: WritableStreamDefaultWriter, clipId: string, userId: string): Promise { let exportedClipNotesCount = 0; let cursor: MiClipNote['id'] | null = null; while (true) { - const clipNotes = await this.clipNotesRepository.find({ - where: { - clipId, - ...(cursor ? { id: MoreThan(cursor) } : {}), - }, - take: 100, - order: { - id: 1, - }, - relations: ['note', 'note.user'], - }) as (MiClipNote & { note: MiNote & { user: MiUser } })[]; + const query = this.clipNotesRepository.createQueryBuilder('clipNote') + .leftJoinAndSelect('clipNote.note', 'note') + .leftJoinAndSelect('note.user', 'user') + .where('clipNote.clipId = :clipId', { clipId }) + .orderBy('clipNote.id', 'ASC') + .take(100); + + if (cursor) { + query.andWhere('clipNote.id > :cursor', { cursor }); + } + + this.queryService.generateVisibilityQuery(query, { id: userId }); + + const clipNotes = await query.getMany() as (MiClipNote & { note: MiNote & { user: MiUser } })[]; if (clipNotes.length === 0) { break; @@ -158,6 +161,11 @@ export class ExportClipsProcessorService { cursor = clipNotes.at(-1)?.id ?? null; for (const clipNote of clipNotes) { + const noteCreatedAt = this.idService.parse(clipNote.note.id).date; + if (shouldHideNoteByTime(clipNote.note.user.makeNotesHiddenBefore, noteCreatedAt)) { + continue; + } + let poll: MiPoll | undefined; if (clipNote.note.hasPoll) { poll = await this.pollsRepository.findOneByOrFail({ noteId: clipNote.note.id }); diff --git a/packages/backend/src/queue/processors/ExportFavoritesProcessorService.ts b/packages/backend/src/queue/processors/ExportFavoritesProcessorService.ts index 7918c8ccb5..87a8ded307 100644 --- a/packages/backend/src/queue/processors/ExportFavoritesProcessorService.ts +++ b/packages/backend/src/queue/processors/ExportFavoritesProcessorService.ts @@ -5,7 +5,6 @@ import * as fs from 'node:fs'; import { Inject, Injectable } from '@nestjs/common'; -import { MoreThan } from 'typeorm'; import { format as dateFormat } from 'date-fns'; import { DI } from '@/di-symbols.js'; import type { MiNoteFavorite, NoteFavoritesRepository, PollsRepository, MiUser, UsersRepository } from '@/models/_.js'; @@ -17,6 +16,8 @@ import type { MiNote } from '@/models/Note.js'; import { bindThis } from '@/decorators.js'; import { IdService } from '@/core/IdService.js'; import { NotificationService } from '@/core/NotificationService.js'; +import { QueryService } from '@/core/QueryService.js'; +import { shouldHideNoteByTime } from '@/misc/should-hide-note-by-time.js'; import { QueueLoggerService } from '../QueueLoggerService.js'; import type * as Bull from 'bullmq'; import type { DbJobDataWithUser } from '../types.js'; @@ -37,6 +38,7 @@ export class ExportFavoritesProcessorService { private driveService: DriveService, private queueLoggerService: QueueLoggerService, + private queryService: QueryService, private idService: IdService, private notificationService: NotificationService, ) { @@ -83,17 +85,20 @@ export class ExportFavoritesProcessorService { }); while (true) { - const favorites = await this.noteFavoritesRepository.find({ - where: { - userId: user.id, - ...(cursor ? { id: MoreThan(cursor) } : {}), - }, - take: 100, - order: { - id: 1, - }, - relations: ['note', 'note.user'], - }) as (MiNoteFavorite & { note: MiNote & { user: MiUser } })[]; + const query = this.noteFavoritesRepository.createQueryBuilder('favorite') + .leftJoinAndSelect('favorite.note', 'note') + .leftJoinAndSelect('note.user', 'user') + .where('favorite.userId = :userId', { userId: user.id }) + .orderBy('favorite.id', 'ASC') + .take(100); + + if (cursor) { + query.andWhere('favorite.id > :cursor', { cursor }); + } + + this.queryService.generateVisibilityQuery(query, { id: user.id }); + + const favorites = await query.getMany() as (MiNoteFavorite & { note: MiNote & { user: MiUser } })[]; if (favorites.length === 0) { job.updateProgress(100); @@ -103,6 +108,11 @@ export class ExportFavoritesProcessorService { cursor = favorites.at(-1)?.id ?? null; for (const favorite of favorites) { + const noteCreatedAt = this.idService.parse(favorite.note.id).date; + if (shouldHideNoteByTime(favorite.note.user.makeNotesHiddenBefore, noteCreatedAt)) { + continue; + } + let poll: MiPoll | undefined; if (favorite.note.hasPoll) { poll = await this.pollsRepository.findOneByOrFail({ noteId: favorite.note.id }); diff --git a/packages/backend/test-federation/compose.a.yml b/packages/backend/test-federation/compose.a.yml index 4fd4eb3851..ec9a2cf2af 100644 --- a/packages/backend/test-federation/compose.a.yml +++ b/packages/backend/test-federation/compose.a.yml @@ -50,7 +50,7 @@ services: volumes: - type: bind source: ./volumes/db.a - target: /var/lib/postgresql/data + target: /var/lib/postgresql bind: create_host_path: true diff --git a/packages/backend/test-federation/compose.b.yml b/packages/backend/test-federation/compose.b.yml index 753da22822..9221934406 100644 --- a/packages/backend/test-federation/compose.b.yml +++ b/packages/backend/test-federation/compose.b.yml @@ -50,7 +50,7 @@ services: volumes: - type: bind source: ./volumes/db.b - target: /var/lib/postgresql/data + target: /var/lib/postgresql bind: create_host_path: true diff --git a/packages/backend/test/e2e/exports.ts b/packages/backend/test/e2e/exports.ts index 5bdb7e79a3..1742506306 100644 --- a/packages/backend/test/e2e/exports.ts +++ b/packages/backend/test/e2e/exports.ts @@ -169,7 +169,36 @@ describe('export-clips', () => { assert.strictEqual(exported[1].clipNotes[0].note.text, 'baz2'); }); - test('Clipping other user\'s note', async () => { + test('Clipping other user\'s note (followers only notes are excluded when not following)', async () => { + const res = await api('clips/create', { + name: 'kawaii', + description: 'kawaii', + }, alice); + assert.strictEqual(res.status, 200); + const clip = res.body; + + const note = await post(bob, { + text: 'baz', + visibility: 'followers', + }); + + const res2 = await api('clips/add-note', { + clipId: clip.id, + noteId: note.id, + }, alice); + assert.strictEqual(res2.status, 204); + + const res3 = await api('i/export-clips', {}, alice); + assert.strictEqual(res3.status, 204); + + const exported = await pollFirstDriveFile(); + assert.strictEqual(exported[0].clipNotes.length, 0); + }); + + test('Clipping other user\'s note (followers only notes are included when following)', async () => { + // Alice follows Bob + await api('following/create', { userId: bob.id }, alice); + const res = await api('clips/create', { name: 'kawaii', description: 'kawaii', diff --git a/packages/backend/test/unit/misc/should-hide-note-by-time.ts b/packages/backend/test/unit/misc/should-hide-note-by-time.ts new file mode 100644 index 0000000000..1c463c82c6 --- /dev/null +++ b/packages/backend/test/unit/misc/should-hide-note-by-time.ts @@ -0,0 +1,136 @@ +/* + * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-License-Identifier: AGPL-3.0-only + */ + +import { describe, expect, test, beforeEach, afterEach } from '@jest/globals'; +import * as lolex from '@sinonjs/fake-timers'; +import { shouldHideNoteByTime } from '@/misc/should-hide-note-by-time.js'; + +describe('misc:should-hide-note-by-time', () => { + let clock: lolex.InstalledClock; + const epoch = Date.UTC(2000, 0, 1, 0, 0, 0); + + beforeEach(() => { + clock = lolex.install({ + // https://github.com/sinonjs/sinon/issues/2620 + toFake: Object.keys(lolex.timers).filter((key) => !['nextTick', 'queueMicrotask'].includes(key)) as lolex.FakeMethod[], + now: new Date(epoch), + shouldClearNativeTimers: true, + }); + }); + + afterEach(() => { + clock.uninstall(); + }); + + describe('hiddenBefore が null または undefined の場合', () => { + test('hiddenBefore が null のときは false を返す(非表示機能が有効でない)', () => { + const createdAt = new Date(epoch - 86400000); // 1 day ago + expect(shouldHideNoteByTime(null, createdAt)).toBe(false); + }); + + test('hiddenBefore が undefined のときは false を返す(非表示機能が有効でない)', () => { + const createdAt = new Date(epoch - 86400000); // 1 day ago + expect(shouldHideNoteByTime(undefined, createdAt)).toBe(false); + }); + }); + + describe('相対時間モード (hiddenBefore <= 0)', () => { + test('閾値内に作成されたノートは false を返す(作成からの経過時間がまだ短い→表示)', () => { + const hiddenBefore = -86400; // 1 day in seconds + const createdAt = new Date(epoch - 3600000); // 1 hour ago + expect(shouldHideNoteByTime(hiddenBefore, createdAt)).toBe(false); + }); + + test('閾値を超えて作成されたノートは true を返す(指定期間以上経過している→非表示)', () => { + const hiddenBefore = -86400; // 1 day in seconds + const createdAt = new Date(epoch - 172800000); // 2 days ago + expect(shouldHideNoteByTime(hiddenBefore, createdAt)).toBe(true); + }); + + test('ちょうど閾値で作成されたノートは true を返す(閾値に達したら非表示)', () => { + const hiddenBefore = -86400; // 1 day in seconds + const createdAt = new Date(epoch - 86400000); // exactly 1 day ago + expect(shouldHideNoteByTime(hiddenBefore, createdAt)).toBe(true); + }); + + test('異なる相対時間値で判定できる(1時間設定と3時間設定の異なる結果)', () => { + const createdAt = new Date(epoch - 7200000); // 2 hours ago + expect(shouldHideNoteByTime(-3600, createdAt)).toBe(true); // 1時間経過→非表示 + expect(shouldHideNoteByTime(-10800, createdAt)).toBe(false); // 3時間未経過→表示 + }); + + test('ISO 8601 形式の文字列の createdAt に対応できる(文字列でも正しく判定)', () => { + const createdAtString = new Date(epoch - 86400000).toISOString(); + const hiddenBefore = -86400; // 1 day in seconds + expect(shouldHideNoteByTime(hiddenBefore, createdAtString)).toBe(true); + }); + + test('hiddenBefore が 0 の場合に対応できる(0秒以上経過で非表示→ほぼ全て非表示)', () => { + const hiddenBefore = 0; + const createdAt = new Date(epoch - 1); // 1ms ago + expect(shouldHideNoteByTime(hiddenBefore, createdAt)).toBe(true); + }); + }); + + describe('絶対時間モード (hiddenBefore > 0)', () => { + test('閾値タイムスタンプより後に作成されたノートは false を返す(指定日時より後→表示)', () => { + const thresholdSeconds = Math.floor(epoch / 1000); + const createdAt = new Date(epoch + 3600000); // 1 hour from epoch + expect(shouldHideNoteByTime(thresholdSeconds, createdAt)).toBe(false); + }); + + test('閾値タイムスタンプより前に作成されたノートは true を返す(指定日時より前→非表示)', () => { + const thresholdSeconds = Math.floor(epoch / 1000); + const createdAt = new Date(epoch - 3600000); // 1 hour ago + expect(shouldHideNoteByTime(thresholdSeconds, createdAt)).toBe(true); + }); + + test('ちょうど閾値タイムスタンプで作成されたノートは true を返す(指定日時に達したら非表示)', () => { + const thresholdSeconds = Math.floor(epoch / 1000); + const createdAt = new Date(epoch); // exactly epoch + expect(shouldHideNoteByTime(thresholdSeconds, createdAt)).toBe(true); + }); + + test('ISO 8601 形式の文字列の createdAt に対応できる(文字列でも正しく判定)', () => { + const thresholdSeconds = Math.floor(epoch / 1000); + const createdAtString = new Date(epoch - 3600000).toISOString(); + expect(shouldHideNoteByTime(thresholdSeconds, createdAtString)).toBe(true); + }); + + test('異なる閾値タイムスタンプで判定できる(2021年設定と現在より1時間前設定の異なる結果)', () => { + const thresholdSeconds = Math.floor((epoch - 86400000) / 1000); // 1 day ago + const createdAtBefore = new Date(epoch - 172800000); // 2 days ago + const createdAtAfter = new Date(epoch - 3600000); // 1 hour ago + expect(shouldHideNoteByTime(thresholdSeconds, createdAtBefore)).toBe(true); // 閾値より前→非表示 + expect(shouldHideNoteByTime(thresholdSeconds, createdAtAfter)).toBe(false); // 閾値より後→表示 + }); + }); + + describe('エッジケース', () => { + test('相対時間モードで非常に古いノートに対応できる(非常に古い→閾値超→非表示)', () => { + const hiddenBefore = -1; // hide notes older than 1 second + const createdAt = new Date(epoch - 1000000); // very old + expect(shouldHideNoteByTime(hiddenBefore, createdAt)).toBe(true); + }); + + test('相対時間モードで非常に新しいノートに対応できる(非常に新しい→閾値未満→表示)', () => { + const hiddenBefore = -86400; // 1 day + const createdAt = new Date(epoch - 1); // 1ms ago + expect(shouldHideNoteByTime(hiddenBefore, createdAt)).toBe(false); + }); + + test('大きなタイムスタンプ値に対応できる(未来の日時を指定→現在のノートは全て非表示)', () => { + const thresholdSeconds = Math.floor(epoch / 1000) + 86400; // 1 day from epoch + const createdAt = new Date(epoch); // created epoch + expect(shouldHideNoteByTime(thresholdSeconds, createdAt)).toBe(true); + }); + + test('小さな相対時間値に対応できる(1秒設定で2秒前→非表示)', () => { + const hiddenBefore = -1; // 1 second + const createdAt = new Date(epoch - 2000); // 2 seconds ago + expect(shouldHideNoteByTime(hiddenBefore, createdAt)).toBe(true); + }); + }); +}); diff --git a/packages/frontend-builder/package.json b/packages/frontend-builder/package.json index 37dd133fe6..36c32b915d 100644 --- a/packages/frontend-builder/package.json +++ b/packages/frontend-builder/package.json @@ -12,8 +12,8 @@ "devDependencies": { "@types/estree": "1.0.8", "@types/node": "24.10.1", - "@typescript-eslint/eslint-plugin": "8.47.0", - "@typescript-eslint/parser": "8.47.0", + "@typescript-eslint/eslint-plugin": "8.48.0", + "@typescript-eslint/parser": "8.48.0", "rollup": "4.53.3", "typescript": "5.9.3" }, diff --git a/packages/frontend-embed/package.json b/packages/frontend-embed/package.json index c27583cf86..e82cdc1f27 100644 --- a/packages/frontend-embed/package.json +++ b/packages/frontend-embed/package.json @@ -17,8 +17,6 @@ "@rollup/pluginutils": "5.3.0", "@twemoji/parser": "16.0.0", "@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", "frontend-shared": "workspace:*", @@ -29,14 +27,11 @@ "punycode.js": "2.3.1", "rollup": "4.53.3", "sass": "1.94.2", - "shiki": "3.15.0", + "shiki": "3.17.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.2.4", - "vue": "3.5.24" + "vue": "3.5.25" }, "devDependencies": { "@misskey-dev/summaly": "5.2.5", @@ -48,23 +43,23 @@ "@types/punycode.js": "npm:@types/punycode@2.1.4", "@types/tinycolor2": "1.4.6", "@types/ws": "8.18.1", - "@typescript-eslint/eslint-plugin": "8.47.0", - "@typescript-eslint/parser": "8.47.0", - "@vitest/coverage-v8": "4.0.13", - "@vue/runtime-core": "3.5.24", + "@typescript-eslint/eslint-plugin": "8.48.0", + "@typescript-eslint/parser": "8.48.0", + "@vitest/coverage-v8": "4.0.14", + "@vue/runtime-core": "3.5.25", "acorn": "8.15.0", "cross-env": "10.1.0", "eslint-plugin-import": "2.32.0", - "eslint-plugin-vue": "10.6.0", - "fast-glob": "3.3.3", - "happy-dom": "20.0.10", + "eslint-plugin-vue": "10.6.2", + "happy-dom": "20.0.11", "intersection-observer": "0.12.2", "micromatch": "4.0.8", - "msw": "2.12.2", + "msw": "2.12.3", "nodemon": "3.1.11", - "prettier": "3.6.2", + "prettier": "3.7.1", "start-server-and-test": "2.1.3", "tsx": "4.20.6", + "typescript": "5.9.3", "vite-plugin-turbosnap": "1.0.3", "vue-component-type-helpers": "3.1.5", "vue-eslint-parser": "10.2.0", diff --git a/packages/frontend-embed/public/loader/boot.js b/packages/frontend-embed/public/loader/boot.js index ba6366b3db..9b3d27873b 100644 --- a/packages/frontend-embed/public/loader/boot.js +++ b/packages/frontend-embed/public/loader/boot.js @@ -70,6 +70,8 @@ importAppScript(); }); } + + localStorage.setItem('lang', lang); //#endregion async function addStyle(styleText) { diff --git a/packages/frontend-shared/build.js b/packages/frontend-shared/build.js index 9941114757..07e98ad182 100644 --- a/packages/frontend-shared/build.js +++ b/packages/frontend-shared/build.js @@ -3,14 +3,13 @@ import { fileURLToPath } from 'node:url'; import { dirname } from 'node:path'; import * as esbuild from 'esbuild'; import { build } from 'esbuild'; -import { globSync } from 'glob'; import { execa } from 'execa'; const _filename = fileURLToPath(import.meta.url); const _dirname = dirname(_filename); const _package = JSON.parse(fs.readFileSync(_dirname + '/package.json', 'utf-8')); -const entryPoints = globSync('./js/**/**.{ts,tsx}'); +const entryPoints = fs.globSync('./js/**/**.{ts,tsx}'); /** @type {import('esbuild').BuildOptions} */ const options = { diff --git a/packages/frontend-shared/package.json b/packages/frontend-shared/package.json index 5806414930..baf5c561d8 100644 --- a/packages/frontend-shared/package.json +++ b/packages/frontend-shared/package.json @@ -22,10 +22,10 @@ }, "devDependencies": { "@types/node": "24.10.1", - "@typescript-eslint/eslint-plugin": "8.47.0", - "@typescript-eslint/parser": "8.47.0", + "@typescript-eslint/eslint-plugin": "8.48.0", + "@typescript-eslint/parser": "8.48.0", "esbuild": "0.27.0", - "eslint-plugin-vue": "10.6.0", + "eslint-plugin-vue": "10.6.2", "nodemon": "3.1.11", "typescript": "5.9.3", "vue-eslint-parser": "10.2.0" @@ -36,6 +36,6 @@ "dependencies": { "i18n": "workspace:*", "misskey-js": "workspace:*", - "vue": "3.5.24" + "vue": "3.5.25" } } diff --git a/packages/frontend/.storybook/generate.tsx b/packages/frontend/.storybook/generate.tsx index 89d4214141..6005049dde 100644 --- a/packages/frontend/.storybook/generate.tsx +++ b/packages/frontend/.storybook/generate.tsx @@ -3,12 +3,11 @@ * SPDX-License-Identifier: AGPL-3.0-only */ -import { existsSync, readFileSync } from 'node:fs'; +import { existsSync, readFileSync, globSync } from 'node:fs'; import { writeFile } from 'node:fs/promises'; import { basename, dirname } from 'node:path/posix'; import { GENERATOR, type State, generate } from 'astring'; import type * as estree from 'estree'; -import glob from 'fast-glob'; import { format } from 'prettier'; interface SatisfiesExpression extends estree.BaseExpression { @@ -439,38 +438,37 @@ function toStories(component: string): Promise { // glob('src/{components,pages,ui,widgets}/**/*.vue') (async () => { - const globs = await Promise.all([ - glob('src/components/global/Mk*.vue'), - glob('src/components/global/RouterView.vue'), - glob('src/components/MkAbuseReportWindow.vue'), - glob('src/components/MkAccountMoved.vue'), - glob('src/components/MkAchievements.vue'), - glob('src/components/MkAnalogClock.vue'), - glob('src/components/MkAnimBg.vue'), - glob('src/components/MkAnnouncementDialog.vue'), - glob('src/components/MkAntennaEditor.vue'), - glob('src/components/MkAntennaEditorDialog.vue'), - glob('src/components/MkAsUi.vue'), - glob('src/components/MkAutocomplete.vue'), - glob('src/components/MkAvatars.vue'), - glob('src/components/Mk[B-E]*.vue'), - glob('src/components/MkFlashPreview.vue'), - glob('src/components/MkGalleryPostPreview.vue'), - glob('src/components/MkSignupServerRules.vue'), - glob('src/components/MkUserSetupDialog.vue'), - glob('src/components/MkUserSetupDialog.*.vue'), - glob('src/components/MkImgPreviewDialog.vue'), - glob('src/components/MkInstanceCardMini.vue'), - glob('src/components/MkInviteCode.vue'), - glob('src/components/MkTagItem.vue'), - glob('src/components/MkRoleSelectDialog.vue'), - glob('src/components/grid/MkGrid.vue'), - glob('src/pages/admin/custom-emojis-manager2.vue'), - glob('src/pages/admin/overview.ap-requests.vue'), - glob('src/pages/user/home.vue'), - glob('src/pages/search.vue'), - ]); - const components = globs.flat(); + const components = [ + globSync('src/components/global/Mk*.vue'), + globSync('src/components/global/RouterView.vue'), + globSync('src/components/MkAbuseReportWindow.vue'), + globSync('src/components/MkAccountMoved.vue'), + globSync('src/components/MkAchievements.vue'), + globSync('src/components/MkAnalogClock.vue'), + globSync('src/components/MkAnimBg.vue'), + globSync('src/components/MkAnnouncementDialog.vue'), + globSync('src/components/MkAntennaEditor.vue'), + globSync('src/components/MkAntennaEditorDialog.vue'), + globSync('src/components/MkAsUi.vue'), + globSync('src/components/MkAutocomplete.vue'), + globSync('src/components/MkAvatars.vue'), + globSync('src/components/Mk[B-E]*.vue'), + globSync('src/components/MkFlashPreview.vue'), + globSync('src/components/MkGalleryPostPreview.vue'), + globSync('src/components/MkSignupServerRules.vue'), + globSync('src/components/MkUserSetupDialog.vue'), + globSync('src/components/MkUserSetupDialog.*.vue'), + globSync('src/components/MkImgPreviewDialog.vue'), + globSync('src/components/MkInstanceCardMini.vue'), + globSync('src/components/MkInviteCode.vue'), + globSync('src/components/MkTagItem.vue'), + globSync('src/components/MkRoleSelectDialog.vue'), + globSync('src/components/grid/MkGrid.vue'), + globSync('src/pages/admin/custom-emojis-manager2.vue'), + globSync('src/pages/admin/overview.ap-requests.vue'), + globSync('src/pages/user/home.vue'), + globSync('src/pages/search.vue'), + ].flat(); await Promise.all(components.map(async (component) => { const stories = component.replace(/\.vue$/, '.stories.ts'); await writeFile(stories, await toStories(component)); diff --git a/packages/frontend/lib/vite-plugin-create-search-index.ts b/packages/frontend/lib/vite-plugin-create-search-index.ts index f17b43b0e3..cfbba0823c 100644 --- a/packages/frontend/lib/vite-plugin-create-search-index.ts +++ b/packages/frontend/lib/vite-plugin-create-search-index.ts @@ -16,7 +16,6 @@ import { type PluginOption } from 'vite'; import fs from 'node:fs'; -import { glob } from 'glob'; import JSON5 from 'json5'; import MagicString, { SourceMap } from 'magic-string'; import path from 'node:path' @@ -724,7 +723,7 @@ export function pluginCreateSearchIndexVirtualModule(options: Options, asigner: async load(id) { if (id == '\0' + allSearchIndexFile) { - const files = await Promise.all(options.targetFilePaths.map(async (filePathPattern) => await glob(filePathPattern))).then(paths => paths.flat()); + const files = options.targetFilePaths.map((filePathPattern) => fs.globSync(filePathPattern)).flat(); let generatedFile = ''; let arrayElements = ''; for (let file of files) { diff --git a/packages/frontend/package.json b/packages/frontend/package.json index c3baf5ea74..68dc5bd656 100644 --- a/packages/frontend/package.json +++ b/packages/frontend/package.json @@ -25,15 +25,13 @@ "@rollup/plugin-json": "6.1.0", "@rollup/plugin-replace": "6.0.3", "@rollup/pluginutils": "5.3.0", - "@sentry/vue": "10.26.0", + "@sentry/vue": "10.27.0", "@syuilo/aiscript": "1.2.0", "@syuilo/aiscript-0-19-0": "npm:@syuilo/aiscript@^0.19.0", "@twemoji/parser": "16.0.0", "@vitejs/plugin-vue": "6.0.2", - "@vue/compiler-sfc": "3.5.24", "aiscript-vscode": "github:aiscript-dev/aiscript-vscode#v0.1.15", "analytics": "0.8.19", - "astring": "1.9.0", "broadcast-channel": "7.2.0", "buraha": "0.0.1", "canvas-confetti": "1.9.4", @@ -46,7 +44,6 @@ "compare-versions": "6.1.1", "cropperjs": "2.1.0", "date-fns": "4.1.0", - "estree-walker": "3.0.3", "eventemitter3": "5.0.1", "execa": "9.6.0", "exifreader": "4.32.0", @@ -57,9 +54,8 @@ "ios-haptics": "0.1.4", "is-file-animated": "1.0.2", "json5": "2.2.3", - "magic-string": "0.30.21", "matter-js": "0.20.0", - "mediabunny": "1.25.1", + "mediabunny": "1.25.3", "mfm-js": "0.25.0", "misskey-bubble-game": "workspace:*", "misskey-js": "workspace:*", @@ -71,18 +67,14 @@ "rollup": "4.53.3", "sanitize-html": "2.17.0", "sass": "1.94.2", - "shiki": "3.15.0", - "strict-event-emitter-types": "2.0.0", + "shiki": "3.17.0", "textarea-caret": "3.1.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.2.4", - "vue": "3.5.24", + "vue": "3.5.25", "vuedraggable": "next", "wanakana": "5.3.1" }, @@ -90,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.0.8", + "@storybook/addon-links": "10.1.0", "@storybook/addon-mdx-gfm": "8.6.14", "@storybook/addon-storysource": "8.6.14", "@storybook/blocks": "8.6.14", @@ -98,13 +90,13 @@ "@storybook/core-events": "8.6.14", "@storybook/manager-api": "8.6.14", "@storybook/preview-api": "8.6.14", - "@storybook/react": "10.0.8", - "@storybook/react-vite": "10.0.8", + "@storybook/react": "10.1.0", + "@storybook/react-vite": "10.1.0", "@storybook/test": "8.6.14", "@storybook/theming": "8.6.14", "@storybook/types": "8.6.14", - "@storybook/vue3": "10.0.8", - "@storybook/vue3-vite": "10.0.8", + "@storybook/vue3": "10.1.0", + "@storybook/vue3-vite": "10.1.0", "@tabler/icons-webfont": "3.35.0", "@testing-library/vue": "8.1.0", "@types/canvas-confetti": "1.9.0", @@ -117,36 +109,37 @@ "@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.47.0", - "@typescript-eslint/parser": "8.47.0", - "@vitest/coverage-v8": "4.0.13", - "@vue/compiler-core": "3.5.24", - "@vue/runtime-core": "3.5.24", + "@typescript-eslint/eslint-plugin": "8.48.0", + "@typescript-eslint/parser": "8.48.0", + "@vitest/coverage-v8": "4.0.14", + "@vue/compiler-core": "3.5.25", "acorn": "8.15.0", + "astring": "1.9.0", "cross-env": "10.1.0", "cypress": "15.7.0", "eslint-plugin-import": "2.32.0", - "eslint-plugin-vue": "10.6.0", - "fast-glob": "3.3.3", - "happy-dom": "20.0.10", + "eslint-plugin-vue": "10.6.2", + "estree-walker": "3.0.3", + "happy-dom": "20.0.11", "intersection-observer": "0.12.2", + "magic-string": "0.30.21", "micromatch": "4.0.8", "minimatch": "10.1.1", - "msw": "2.12.2", + "msw": "2.12.3", "msw-storybook-addon": "2.0.6", "nodemon": "3.1.11", - "prettier": "3.6.2", + "prettier": "3.7.1", "react": "19.2.0", "react-dom": "19.2.0", "seedrandom": "3.0.5", "start-server-and-test": "2.1.3", - "storybook": "10.0.8", + "storybook": "10.1.0", "storybook-addon-misskey-theme": "github:misskey-dev/storybook-addon-misskey-theme", "tsx": "4.20.6", + "typescript": "5.9.3", "vite-plugin-glsl": "1.5.4", "vite-plugin-turbosnap": "1.0.3", - "vitest": "4.0.13", + "vitest": "4.0.14", "vitest-fetch-mock": "0.4.5", "vue-component-type-helpers": "3.1.5", "vue-eslint-parser": "10.2.0", diff --git a/packages/frontend/public/loader/boot.js b/packages/frontend/public/loader/boot.js index ab4b158287..8aafb282aa 100644 --- a/packages/frontend/public/loader/boot.js +++ b/packages/frontend/public/loader/boot.js @@ -42,6 +42,8 @@ console.error('invalid lang value detected!!!', typeof lang, lang); lang = 'en-US'; } + + localStorage.setItem('lang', lang); //#endregion //#region Script diff --git a/packages/frontend/src/components/MkDrive.vue b/packages/frontend/src/components/MkDrive.vue index b84532b40b..d8c949d8eb 100644 --- a/packages/frontend/src/components/MkDrive.vue +++ b/packages/frontend/src/components/MkDrive.vue @@ -83,34 +83,58 @@ SPDX-License-Identifier: AGPL-3.0-only {{ i18n.ts.loadMore }} - - + + + + - - - - {{ i18n.ts.loadMore }}
@@ -217,6 +241,7 @@ const foldersPaginator = markRaw(new Paginator('drive/folders', { })); const filesTimeline = makeDateGroupedTimelineComputedRef(filesPaginator.items, 'month'); +const shouldBeGroupedByDate = computed(() => ['+createdAt', '-createdAt'].includes(sortModeSelect.value)); watch(folder, () => emit('cd', folder.value)); watch(sortModeSelect, () => { diff --git a/packages/frontend/src/components/MkMediaImage.vue b/packages/frontend/src/components/MkMediaImage.vue index 99ea606a11..f59d15d9a2 100644 --- a/packages/frontend/src/components/MkMediaImage.vue +++ b/packages/frontend/src/components/MkMediaImage.vue @@ -233,16 +233,18 @@ function showMenu(ev: MouseEvent) { .hide { display: block; position: absolute; - border-radius: 6px; - background-color: var(--MI_THEME-fg); - color: hsl(from var(--MI_THEME-accent) h s calc(l + 10)); + background-color: rgba(0, 0, 0, 0.3); + -webkit-backdrop-filter: var(--MI-blur, blur(15px)); + backdrop-filter: var(--MI-blur, blur(15px)); + border-radius: 0 0 0 9px; + color: #fff; font-size: 12px; opacity: .5; padding: 5px 8px; text-align: center; cursor: pointer; - top: 12px; - right: 12px; + top: 0; + right: 0; } .hiddenTextWrapper { @@ -272,17 +274,17 @@ html[data-color-scheme=light] .visible { .menu { display: block; position: absolute; - border-radius: 999px; background-color: rgba(0, 0, 0, 0.3); -webkit-backdrop-filter: var(--MI-blur, blur(15px)); backdrop-filter: var(--MI-blur, blur(15px)); + border-radius: 9px 0 0 0; color: #fff; font-size: 0.8em; width: 28px; height: 28px; text-align: center; - bottom: 10px; - right: 10px; + bottom: 0; + right: 0; } .imageContainer { diff --git a/packages/frontend/src/components/MkNote.vue b/packages/frontend/src/components/MkNote.vue index 1b0f25a1a2..a7299d2961 100644 --- a/packages/frontend/src/components/MkNote.vue +++ b/packages/frontend/src/components/MkNote.vue @@ -5,7 +5,7 @@ SPDX-License-Identifier: AGPL-3.0-only