diff --git a/CHANGELOG.md b/CHANGELOG.md
index d7df4a3465..c7a0fbdada 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,20 +1,30 @@
-## 2025.9.0
+## Unreleased
### General
-
+### Client
+- Enhance: 時刻計算のための基準値を一か所で管理するようにし、パフォーマンスを向上
+
+### Server
+-
+
+
+## 2025.9.0
+
### Client
- Enhance: AiScriptAppウィジェットで構文エラーを検知してもダイアログではなくウィジェット内にエラーを表示するように
- Enhance: /flushページでサイトキャッシュをクリアできるようになりました
- Enhance: クリップ/リスト/アンテナ/ロール追加系メニュー項目において、表示件数を拡張
+- Enhance: 「キャッシュを削除」ボタンでブラウザの内部キャッシュの削除も行えるように
+- Enhance: Ctrlキー(Commandキー)を押下しながらリンクをクリックすると新しいタブで開くように
- Fix: プッシュ通知を有効にできない問題を修正
- Fix: RSSティッカーウィジェットが正しく動作しない問題を修正
- Fix: プロファイルを復元後アカウントの切り替えができない問題を修正
- Fix: エラー画像が横に引き伸ばされてしまう問題に対応
### Server
--
-
+- Fix: webpなどの画像に対してセンシティブなメディアの検出が適用されていなかった問題を修正
## 2025.8.0
diff --git a/locales/ca-ES.yml b/locales/ca-ES.yml
index 4b6c8b97c3..63878bf1b7 100644
--- a/locales/ca-ES.yml
+++ b/locales/ca-ES.yml
@@ -1644,7 +1644,7 @@ _serverSettings:
reactionsBufferingDescription: "Quan s'activa aquesta opció millora bastant el rendiment en recuperar les línies de temps reduint la càrrega de la base. Com a contrapunt, augmentarà l'ús de memòria de Redís. Desactiva aquesta opció en cas de tenir un servidor amb poca memòria o si tens problemes d'inestabilitat."
remoteNotesCleaning: "Neteja automàtica de notes remotes"
remoteNotesCleaning_description: "Quan activis aquesta opció, periòdicament es netejaran les notes remotes que no es consultin, això evitarà que la base de dades se"
- remoteNotesCleaningMaxProcessingDuration: "D'oració màxima del temps de funcionament del procés de neteja"
+ remoteNotesCleaningMaxProcessingDuration: "Duració màxima del temps de funcionament del procés de neteja"
remoteNotesCleaningExpiryDaysForEachNotes: "Duració mínima de conservació de les notes"
inquiryUrl: "URL de consulta "
inquiryUrlDescription: "Escriu adreça URL per al formulari de consulta per al mantenidor del servidor o una pàgina web amb el contacte d'informació."
diff --git a/locales/es-ES.yml b/locales/es-ES.yml
index ac983aae37..8a1d2c458b 100644
--- a/locales/es-ES.yml
+++ b/locales/es-ES.yml
@@ -2137,7 +2137,7 @@ _aboutMisskey:
_displayOfSensitiveMedia:
respect: "Esconder medios marcados como sensibles"
ignore: "Mostrar medios marcados como sensibles"
- force: "Esconder todala multimedia"
+ force: "Esconder toda la multimedia"
_instanceTicker:
none: "No mostrar"
remote: "Mostrar a usuarios remotos"
diff --git a/locales/ru-RU.yml b/locales/ru-RU.yml
index aea69f6f24..5e52143f83 100644
--- a/locales/ru-RU.yml
+++ b/locales/ru-RU.yml
@@ -1215,6 +1215,7 @@ privacyPolicyUrl: "Ссылка на Политику Конфиденциаль
tosAndPrivacyPolicy: "Условия использования и политика конфиденциальности"
avatarDecorations: "Украшения для аватара"
attach: "Прикрепить"
+detachAll: "Убрать всё"
angle: "Угол"
flip: "Переворот"
showAvatarDecorations: "Показать украшения для аватара"
@@ -1253,7 +1254,7 @@ clipNoteLimitExceeded: "К этому клипу больше нельзя до
performance: "Производительность"
modified: "Изменено"
signinWithPasskey: "Войдите в систему, используя свой пароль"
-unknownWebAuthnKey: "Не известный ключ "
+unknownWebAuthnKey: "Неизвестный ключ"
passkeyVerificationFailed: "Ошибка проверка ключа доступа "
messageToFollower: "Сообщение подписчикам"
testCaptchaWarning: "Эта функция предназначена для тестирования CAPTCHA. Не использовать это в рабочей среде"
@@ -1268,8 +1269,11 @@ availableRoles: "Доступные роли"
federationDisabled: "Федерация отключена для этого сервера. Вы не можете взаимодействовать с пользователями на других серверах."
draft: "Черновик"
markAsSensitiveConfirm: "Отметить контент как чувствительный?"
+preferences: "Основное"
resetToDefaultValue: "Сбросить настройки до стандартных"
+syncBetweenDevices: "Синхронизировать между устройствами"
postForm: "Форма отправки"
+textCount: "Количество символов"
information: "Описание"
inMinutes: "мин"
inDays: "сут"
@@ -1281,6 +1285,11 @@ _chat:
send: "Отправить"
_settings:
webhook: "Вебхук"
+ preferencesBanner: "Вы можете настроить общее поведение клиента по вашим предпочтениям"
+ timelineAndNote: "Лента и заметки"
+ _chat:
+ showSenderName: "Показывать имя отправителя"
+ sendOnEnter: "Использовать Enter для отправки"
_delivery:
stop: "Заморожено"
_type:
@@ -1529,7 +1538,7 @@ _achievements:
description: "Нажато здесь"
_justPlainLucky:
title: "Чистая удача"
- description: "Может достаться с вероятностью 0,01% каждые 10 секунд."
+ description: "Может достаться с вероятностью 0,005% каждые 10 секунд."
_setNameToSyuilo:
title: "Комплекс бога"
description: "Установлено «syuilo» в качестве имени"
@@ -1557,6 +1566,12 @@ _achievements:
title: "Brain Diver"
description: "Опубликована ссылка на песню «Brain Diver»"
flavor: "Мисски-Мисски Ла-Ту-Ма"
+ _bubbleGameExplodingHead:
+ title: "🤯"
+ description: "Самый большой объект в Bubble game"
+ _bubbleGameDoubleExplodingHead:
+ title: "Двойной🤯"
+ description: "Два самых больших объекта в Bubble game одновременно!"
_role:
new: "Новая роль"
edit: "Изменить роль"
diff --git a/locales/tr-TR.yml b/locales/tr-TR.yml
index bde027ba69..5ca2b18fac 100644
--- a/locales/tr-TR.yml
+++ b/locales/tr-TR.yml
@@ -360,7 +360,7 @@ whenServerDisconnected: "Sunucu ile bağlantı kesildiğinde"
disconnectedFromServer: "Sunucu bağlantısı kesildi"
reload: "Yenile"
doNothing: "Yoksay"
-reloadConfirm: "Zaman çizelgesini yenilemek ister misin?"
+reloadConfirm: "Panoyu yenilemek ister misin?"
watch: "İzle"
unwatch: "İzlemeyi bırak"
accept: "Kabul et"
@@ -573,9 +573,9 @@ objectStorageSetPublicRead: "Yükleme sırasında \"genel-okuma\" ayarını yap
s3ForcePathStyleDesc: "s3ForcePathStyle etkinleştirilirse, kova adı URL'nin ana bilgisayar adı yerine URL yoluna eklenmelidir. Kendi kendine barındırılan bir Minio örneği gibi hizmetleri kullanırken bu ayarı etkinleştirmen gerekebilir."
serverLogs: "Sunucu log kayıtları"
deleteAll: "Tümünü sil"
-showFixedPostForm: "Gönderi formunu zaman çizelgesinin en üstünde görüntüle"
-showFixedPostFormInChannel: "Gönderi formunu zaman çizelgesinin en üstünde görüntüle (Kanallar)"
-withRepliesByDefaultForNewlyFollowed: "Yeni takip edilen kullanıcıların yanıtlarını varsayılan olarak zaman çizelgesine dahil et"
+showFixedPostForm: "Gönderi formunu pano üstünde görüntüle"
+showFixedPostFormInChannel: "Gönderi formunu pano üstünde görüntüle (Kanallar)"
+withRepliesByDefaultForNewlyFollowed: "Yeni takip edilen kullanıcıların yanıtlarını varsayılan olarak panoya dahil et"
newNoteRecived: "Yeni Not'lar var"
newNote: "Yeni Not"
sounds: "Sesler"
@@ -1059,7 +1059,7 @@ achievements: "Başarılar"
gotInvalidResponseError: "Geçersiz sunucu yanıtı"
gotInvalidResponseErrorDescription: "Sunucu erişilemez durumda olabilir veya bakım çalışması yapılmaktadır. Lütfen daha sonra tekrar dene."
thisPostMayBeAnnoying: "Bu not başkalarını rahatsız edebilir."
-thisPostMayBeAnnoyingHome: "Ana zaman çizelgesine gönder"
+thisPostMayBeAnnoyingHome: "Ana panoya gönder"
thisPostMayBeAnnoyingCancel: "İptal"
thisPostMayBeAnnoyingIgnore: "Yine de gönder"
collapseRenotes: "Daha önce görüntülenen Renote'lari kısaltılmış olarak göster"
@@ -1218,8 +1218,8 @@ showRepliesToOthersInTimeline: "Pano'da diğer kişilere verilen yanıtları gö
hideRepliesToOthersInTimeline: "Pano'dan diğer kişilerin yanıtlarını gizle"
showRepliesToOthersInTimelineAll: "Pano'da takip ettiğin herkesin diğerlerine verdiği yanıtları göster"
hideRepliesToOthersInTimelineAll: "Pano'da takip ettiğin herkesten diğer kişilere verilen yanıtları gizle"
-confirmShowRepliesAll: "Bu işlem geri alınamaz. Takip ettiğin herkesin yanıtlarını zaman çizelgende diğer kullanıcılara göstermek istiyor musun?"
-confirmHideRepliesAll: "Bu işlem geri alınamaz. Şu anda takip ettiğin tüm kullanıcıların yanıtlarını zaman tünelinde cidden göstermeyecek misin?"
+confirmShowRepliesAll: "Bu işlem geri alınamaz. Takip ettiğin herkesin yanıtlarını panoda diğer kullanıcılara göstermek istiyor musun?"
+confirmHideRepliesAll: "Bu işlem geri alınamaz. Şu anda takip ettiğin tüm kullanıcıların yanıtlarını panoda cidden göstermeyecek misin?"
externalServices: "Dış Hizmetler"
sourceCode: "Kaynak kodu"
sourceCodeIsNotYetProvided: "Kaynak kodu henüz mevcut değildir. Bu sorunu gidermek için yöneticiyle iletişime geçin."
@@ -1570,9 +1570,9 @@ _initialTutorial:
description: "Burada, Misskey'i kullanmanın temellerini ve özelliklerini öğrenebilirsin."
_note:
title: "Not nedir?"
- description: "Misskey'deki gönderiler “Notlar” olarak adlandırılır. Notlar zaman çizelgesinde kronolojik olarak düzenlenir ve gerçek zamanlı olarak güncellenir."
+ description: "Misskey'deki gönderiler “Notlar” olarak adlandırılır. Notlar panoda kronolojik olarak düzenlenir ve gerçek zamanlı olarak güncellenir."
reply: "Bir mesaja yanıt vermek için bu düğmeye tıklayın. Yanıtlara yanıt vermek de mümkündür, böylece konuşma bir konu başlığı gibi devam eder."
- renote: "Bu notu kendi zaman çizelgende paylaşabilirsiniz. Ayrıca yorumlarınızla birlikte alıntı da yapabilirsin."
+ renote: "Bu notu kendi panonda paylaşabilirsin. Ayrıca yorumlarınla birlikte alıntı da yapabilirsin."
reaction: "Not'a tepkiler ekleyebilirsin. Daha fazla ayrıntı bir sonraki sayfada açıklanacak."
menu: "Not ayrıntılarını görüntüleyebilir, bağlantıları kopyalayabilir ve çeşitli diğer işlemleri gerçekleştirebilirsin."
_reaction:
@@ -1640,7 +1640,7 @@ _serverSettings:
shortNameDescription: "Resmi adın uzun olması durumunda görüntülenebilen, örneğin adının kısaltması."
fanoutTimelineDescription: "Etkinleştirildiğinde Pano alma performansını büyük ölçüde artırır ve veritabanı yükünü azaltır. Bunun karşılığında Redis'in bellek kullanımı artacaktır. Sunucu belleği düşükse veya sunucu kararsızsa bunu devre dışı bırakmayı düşün."
fanoutTimelineDbFallback: "Veritabanına geri dön"
- fanoutTimelineDbFallbackDescription: "Etkinleştirildiğinde, Pano önbelleğe alınmamışsa ek sorgular için veritabanına geri döner. Bu özelliği devre dışı bırakmak, geri dönüş sürecini ortadan kaldırarak sunucu yükünü daha da azaltır, ancak alınabilecek zaman çizelgelerinin aralığını sınırlar."
+ fanoutTimelineDbFallbackDescription: "Etkinleştirildiğinde, Pano önbelleğe alınmamışsa ek sorgular için veritabanına geri döner. Bu özelliği devre dışı bırakmak, geri dönüş sürecini ortadan kaldırarak sunucu yükünü daha da azaltır, ancak alınabilecek panoların aralığını sınırlar."
reactionsBufferingDescription: "Etkinleştirildiğinde, reaksiyon oluşturma sırasında performans büyük ölçüde artacak ve veritabanı üzerindeki yük azalacaktır. Ancak, Redis bellek kullanımı artacakt."
remoteNotesCleaning: "Uzak notların otomatik olarak temizlenmesi"
remoteNotesCleaning_description: "Etkinleştirildiğinde, kullanılmayan ve güncelliğini yitirmiş uzak notlar, veritabanının şişmesini önlemek için periyodik olarak temizlenecek."
@@ -1668,6 +1668,7 @@ _serverSettings:
restartServerSetupWizardConfirm_text: "Bazı mevcut ayarlar sıfırlanacaktır."
entrancePageStyle: "Giriş sayfası stili"
showTimelineForVisitor: "Panoyu göster"
+ showActivitiesForVisitor: "Aktiviteleri göster"
_userGeneratedContentsVisibilityForVisitor:
all: "Her şey halka açıktır."
localOnly: "Yalnızca yerel içerik yayınlanır, uzak içerik gizli tutulur."
@@ -1876,7 +1877,7 @@ _achievements:
title: "Öz Referans"
description: "Kendi notunuzu alıntı yapın"
_htl20npm:
- title: "Akış Zaman Çizelgesi"
+ title: "Akış Panosu"
description: "Ev zaman çizelgenizin hızı 20 npm'yi (dakika başına not sayısı) aşıyor mu?"
_viewInstanceChart:
title: "Analist"
@@ -1965,7 +1966,7 @@ _role:
asBadge: "Rozet olarak göster"
descriptionOfAsBadge: "This role's icon will be displayed next to the username of users with this role if turned on."
isExplorable: "Rolü keşfedilebilir hale getir"
- descriptionOfIsExplorable: "Bu rolün zaman çizelgesi ve bu role sahip kullanıcıların listesi, etkinleştirilirse kamuya açık hale getirilecek."
+ descriptionOfIsExplorable: "Bu rolün panosu ve bu role sahip kullanıcıların listesi, etkinleştirilirse kamuya açık hale getirilecek."
displayOrder: "Pozisyon"
descriptionOfDisplayOrder: "Sayı ne kadar yüksekse, UI pozisyonu da o kadar yüksek olur."
preserveAssignmentOnMoveAccount: "Geçiş sırasında rol atamalarını koruyun"
@@ -1979,7 +1980,7 @@ _role:
high: "Yüksek"
_options:
gtlAvailable: "Global Pano'yu görüntüleyebilir"
- ltlAvailable: "Yerel zaman çizelgesini görüntüleyebilir"
+ ltlAvailable: "Yerel panoyu görüntüleyebilir"
canPublicNote: "Halka açık notlar gönderebilir"
mentionMax: "Bir notta maksimum bahsetme sayısı"
canInvite: "Sunucu davet kodları oluşturabilir"
@@ -2484,7 +2485,7 @@ _visibility:
public: "Halka açık"
publicDescription: "Notunuz tüm kullanıcılar tarafından görülebilir olacaktır."
home: "Pano"
- homeDescription: "Yalnızca ana zaman çizelgesine gönder"
+ homeDescription: "Yalnızca ana panoya gönder"
followers: "Takipçiler"
followersDescription: "Sadece takipçilerine görünür hale getir"
specified: "Doğrudan"
@@ -2531,7 +2532,7 @@ _exportOrImport:
userLists: "Kullanıcı listeleri"
excludeMutingUsers: "Sessize alınan kullanıcıları hariç tut"
excludeInactiveUsers: "Etkin olmayan kullanıcıları hariç tut"
- withReplies: "İçe aktarılan kullanıcıların yanıtlarını zaman çizelgesine dahil edin"
+ withReplies: "İçe aktarılan kullanıcıların yanıtlarını panoya dahil edin"
_charts:
federation: "Federasyon"
apRequest: "Talepler"
@@ -2925,7 +2926,7 @@ _reversi:
freeMatch: "Ücretsiz Eşleştirme"
lookingForPlayer: "Rakip aranıyor..."
gameCanceled: "Oyun iptal edildi."
- shareToTlTheGameWhenStart: "Oyun başlatıldığında zaman çizelgesinde paylaş"
+ shareToTlTheGameWhenStart: "Oyun başlatıldığında panoda paylaş"
iStartedAGame: "Oyun başladı! #MisskeyReversi"
opponentHasSettingsChanged: "Rakip ayarlarını değiştirmiş."
allowIrregularRules: "Düzensiz kurallar (tamamen ücretsiz)"
@@ -3153,7 +3154,7 @@ _clientPerformanceIssueTip:
_clip:
tip: "Klip, notları gruplandırmanıza olanak tanıyan bir özelliktir."
_userLists:
- tip: "Listeler, oluşturulurken belirttiğin herhangi bir kullanıcıyı içerebilir. Oluşturulan liste, yalnızca belirtilen kullanıcıları gösteren bir zaman çizelgesi olarak görüntülenebilir."
+ tip: "Listeler, oluşturulurken belirttiğin herhangi bir kullanıcıyı içerebilir. Oluşturulan liste, yalnızca belirtilen kullanıcıları gösteren bir pano olarak görüntülenebilir."
watermark: "Filigran"
defaultPreset: "Varsayılan Ön Ayar"
_watermarkEditor:
diff --git a/package.json b/package.json
index faafb9c264..7f19734453 100644
--- a/package.json
+++ b/package.json
@@ -1,12 +1,12 @@
{
"name": "misskey",
- "version": "2025.9.0-alpha.1",
+ "version": "2025.9.0",
"codename": "nasubi",
"repository": {
"type": "git",
"url": "https://github.com/misskey-dev/misskey.git"
},
- "packageManager": "pnpm@10.15.0",
+ "packageManager": "pnpm@10.15.1",
"workspaces": [
"packages/frontend-shared",
"packages/frontend",
@@ -62,22 +62,22 @@
"js-yaml": "4.1.0",
"postcss": "8.5.6",
"tar": "7.4.3",
- "terser": "5.43.1",
+ "terser": "5.44.0",
"typescript": "5.9.2"
},
"devDependencies": {
"@misskey-dev/eslint-plugin": "2.1.0",
"@types/js-yaml": "4.0.9",
- "@types/node": "22.17.2",
- "@typescript-eslint/eslint-plugin": "8.40.0",
- "@typescript-eslint/parser": "8.40.0",
+ "@types/node": "22.18.1",
+ "@typescript-eslint/eslint-plugin": "8.42.0",
+ "@typescript-eslint/parser": "8.42.0",
"cross-env": "7.0.3",
"cypress": "14.5.4",
- "eslint": "9.34.0",
+ "eslint": "9.35.0",
"globals": "16.3.0",
"ncp": "2.0.0",
- "pnpm": "10.15.0",
- "start-server-and-test": "2.0.13"
+ "pnpm": "10.15.1",
+ "start-server-and-test": "2.1.0"
},
"optionalDependencies": {
"@tensorflow/tfjs-core": "4.22.0"
diff --git a/packages/backend/package.json b/packages/backend/package.json
index 89e04f4da4..5114912769 100644
--- a/packages/backend/package.json
+++ b/packages/backend/package.json
@@ -39,17 +39,17 @@
},
"optionalDependencies": {
"@swc/core-android-arm64": "1.3.11",
- "@swc/core-darwin-arm64": "1.13.4",
- "@swc/core-darwin-x64": "1.13.4",
+ "@swc/core-darwin-arm64": "1.13.5",
+ "@swc/core-darwin-x64": "1.13.5",
"@swc/core-freebsd-x64": "1.3.11",
- "@swc/core-linux-arm-gnueabihf": "1.13.4",
- "@swc/core-linux-arm64-gnu": "1.13.4",
- "@swc/core-linux-arm64-musl": "1.13.4",
- "@swc/core-linux-x64-gnu": "1.13.4",
- "@swc/core-linux-x64-musl": "1.13.4",
- "@swc/core-win32-arm64-msvc": "1.13.4",
- "@swc/core-win32-ia32-msvc": "1.13.4",
- "@swc/core-win32-x64-msvc": "1.13.4",
+ "@swc/core-linux-arm-gnueabihf": "1.13.5",
+ "@swc/core-linux-arm64-gnu": "1.13.5",
+ "@swc/core-linux-arm64-musl": "1.13.5",
+ "@swc/core-linux-x64-gnu": "1.13.5",
+ "@swc/core-linux-x64-musl": "1.13.5",
+ "@swc/core-win32-arm64-msvc": "1.13.5",
+ "@swc/core-win32-ia32-msvc": "1.13.5",
+ "@swc/core-win32-x64-msvc": "1.13.5",
"@tensorflow/tfjs": "4.22.0",
"@tensorflow/tfjs-node": "4.22.0",
"bufferutil": "4.0.9",
@@ -69,20 +69,20 @@
"utf-8-validate": "6.0.5"
},
"dependencies": {
- "@aws-sdk/client-s3": "3.873.0",
- "@aws-sdk/lib-storage": "3.873.0",
+ "@aws-sdk/client-s3": "3.883.0",
+ "@aws-sdk/lib-storage": "3.883.0",
"@discordapp/twemoji": "16.0.1",
"@fastify/accepts": "5.0.2",
"@fastify/cookie": "11.0.2",
"@fastify/cors": "10.1.0",
"@fastify/express": "4.0.2",
"@fastify/http-proxy": "10.0.2",
- "@fastify/multipart": "9.0.3",
+ "@fastify/multipart": "9.2.1",
"@fastify/static": "8.2.0",
"@fastify/view": "10.0.2",
"@misskey-dev/sharp-read-bmp": "1.2.0",
"@misskey-dev/summaly": "5.2.3",
- "@napi-rs/canvas": "0.1.77",
+ "@napi-rs/canvas": "0.1.79",
"@nestjs/common": "11.1.6",
"@nestjs/core": "11.1.6",
"@nestjs/testing": "11.1.6",
@@ -93,7 +93,7 @@
"@sinonjs/fake-timers": "11.3.1",
"@smithy/node-http-handler": "2.5.0",
"@swc/cli": "0.7.8",
- "@swc/core": "1.13.4",
+ "@swc/core": "1.13.5",
"@twemoji/parser": "16.0.0",
"@types/redis-info": "3.0.3",
"accepts": "1.3.8",
@@ -103,7 +103,7 @@
"bcryptjs": "2.4.3",
"blurhash": "2.0.5",
"body-parser": "1.20.3",
- "bullmq": "5.58.1",
+ "bullmq": "5.58.5",
"cacheable-lookup": "7.0.0",
"cbor": "9.0.2",
"chalk": "5.6.0",
@@ -114,13 +114,13 @@
"content-disposition": "0.5.4",
"date-fns": "2.30.0",
"deep-email-validator": "0.1.21",
- "fastify": "5.5.0",
+ "fastify": "5.6.0",
"fastify-raw-body": "5.0.0",
"feed": "4.2.2",
"file-type": "19.6.0",
"fluent-ffmpeg": "2.1.3",
"form-data": "4.0.4",
- "got": "14.4.7",
+ "got": "14.4.8",
"happy-dom": "16.8.1",
"hpagent": "1.2.0",
"htmlescape": "1.1.1",
@@ -141,7 +141,7 @@
"mime-types": "2.1.35",
"misskey-js": "workspace:*",
"misskey-reversi": "workspace:*",
- "ms": "3.0.0-canary.1",
+ "ms": "3.0.0-canary.202508261828",
"nanoid": "5.1.5",
"nested-property": "4.0.0",
"node-fetch": "3.3.2",
@@ -175,7 +175,7 @@
"slacc": "0.0.10",
"strict-event-emitter-types": "2.0.0",
"stringz": "2.1.0",
- "systeminformation": "5.27.7",
+ "systeminformation": "5.27.8",
"tinycolor2": "1.6.0",
"tmp": "0.2.5",
"tsc-alias": "1.8.16",
@@ -210,7 +210,7 @@
"@types/jsrsasign": "10.5.15",
"@types/mime-types": "2.1.4",
"@types/ms": "0.7.34",
- "@types/node": "22.17.2",
+ "@types/node": "22.18.1",
"@types/nodemailer": "6.4.19",
"@types/oauth": "0.9.6",
"@types/oauth2orize": "1.11.5",
@@ -222,7 +222,7 @@
"@types/ratelimiter": "3.4.6",
"@types/rename": "1.0.7",
"@types/sanitize-html": "2.16.0",
- "@types/semver": "7.7.0",
+ "@types/semver": "7.7.1",
"@types/simple-oauth2": "5.0.7",
"@types/sinonjs__fake-timers": "8.1.5",
"@types/supertest": "6.0.3",
@@ -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.40.0",
- "@typescript-eslint/parser": "8.40.0",
+ "@typescript-eslint/eslint-plugin": "8.42.0",
+ "@typescript-eslint/parser": "8.42.0",
"aws-sdk-client-mock": "4.1.0",
"cross-env": "7.0.3",
"eslint-plugin-import": "2.32.0",
diff --git a/packages/backend/src/core/AiService.ts b/packages/backend/src/core/AiService.ts
index 248a9b8979..23ab8082ed 100644
--- a/packages/backend/src/core/AiService.ts
+++ b/packages/backend/src/core/AiService.ts
@@ -29,7 +29,7 @@ export class AiService {
}
@bindThis
- public async detectSensitive(path: string): Promise {
+ public async detectSensitive(source: string | Buffer): Promise {
try {
if (isSupportedCpu === undefined) {
isSupportedCpu = await this.computeIsSupportedCpu();
@@ -51,7 +51,7 @@ export class AiService {
});
}
- const buffer = await fs.promises.readFile(path);
+ const buffer = source instanceof Buffer ? source : await fs.promises.readFile(source);
const image = await tf.node.decodeImage(buffer, 3) as any;
try {
const predictions = await this.model.classify(image);
diff --git a/packages/backend/src/core/FileInfoService.ts b/packages/backend/src/core/FileInfoService.ts
index 6250d4d3a1..62a7d24afb 100644
--- a/packages/backend/src/core/FileInfoService.ts
+++ b/packages/backend/src/core/FileInfoService.ts
@@ -21,6 +21,7 @@ import { LoggerService } from '@/core/LoggerService.js';
import type Logger from '@/logger.js';
import { bindThis } from '@/decorators.js';
import type { PredictionType } from 'nsfwjs';
+import { isMimeImage } from '@/misc/is-mime-image.js';
export type FileInfo = {
size: number;
@@ -204,16 +205,7 @@ export class FileInfoService {
return [sensitive, porn];
}
- if ([
- 'image/jpeg',
- 'image/png',
- 'image/webp',
- ].includes(mime)) {
- const result = await this.aiService.detectSensitive(source);
- if (result) {
- [sensitive, porn] = judgePrediction(result);
- }
- } else if (analyzeVideo && (mime === 'image/apng' || mime.startsWith('video/'))) {
+ if (analyzeVideo && (mime === 'image/apng' || mime.startsWith('video/'))) {
const [outDir, disposeOutDir] = await createTempDir();
try {
const command = FFmpeg()
@@ -281,6 +273,23 @@ export class FileInfoService {
} finally {
disposeOutDir();
}
+ } else if (isMimeImage(mime, 'sharp-convertible-image-with-bmp')) {
+ /*
+ * tfjs-node は限られた画像形式しか受け付けないため、sharp で PNG に変換する
+ * せっかくなので内部処理で使われる最大サイズの299x299に事前にリサイズする
+ */
+ const png = await (await sharpBmp(source, mime))
+ .resize(299, 299, {
+ withoutEnlargement: false,
+ })
+ .rotate()
+ .flatten({ background: { r: 119, g: 119, b: 119 } }) // 透過部分を18%グレーで塗りつぶす
+ .png()
+ .toBuffer();
+ const result = await this.aiService.detectSensitive(png);
+ if (result) {
+ [sensitive, porn] = judgePrediction(result);
+ }
}
return [sensitive, porn];
diff --git a/packages/backend/src/core/QueueService.ts b/packages/backend/src/core/QueueService.ts
index 0f225a8242..2d0e7b5d83 100644
--- a/packages/backend/src/core/QueueService.ts
+++ b/packages/backend/src/core/QueueService.ts
@@ -756,8 +756,8 @@ export class QueueService {
@bindThis
public async queueRetryJob(queueType: typeof QUEUE_TYPES[number], jobId: string) {
const queue = this.getQueue(queueType);
- const job: Bull.Job | null = await queue.getJob(jobId);
- if (job) {
+ const job = await queue.getJob(jobId);
+ if (job != null) {
if (job.finishedOn != null) {
await job.retry();
} else {
@@ -769,8 +769,8 @@ export class QueueService {
@bindThis
public async queueRemoveJob(queueType: typeof QUEUE_TYPES[number], jobId: string) {
const queue = this.getQueue(queueType);
- const job: Bull.Job | null = await queue.getJob(jobId);
- if (job) {
+ const job = await queue.getJob(jobId);
+ if (job != null) {
await job.remove();
}
}
@@ -803,8 +803,8 @@ export class QueueService {
@bindThis
public async queueGetJob(queueType: typeof QUEUE_TYPES[number], jobId: string) {
const queue = this.getQueue(queueType);
- const job: Bull.Job | null = await queue.getJob(jobId);
- if (job) {
+ const job = await queue.getJob(jobId);
+ if (job != null) {
return this.packJobData(job);
} else {
throw new Error(`Job not found: ${jobId}`);
diff --git a/packages/backend/src/server/api/ApiServerService.ts b/packages/backend/src/server/api/ApiServerService.ts
index 32818003ad..57d74ef2b1 100644
--- a/packages/backend/src/server/api/ApiServerService.ts
+++ b/packages/backend/src/server/api/ApiServerService.ts
@@ -176,6 +176,17 @@ export class ApiServerService {
}
});
+ fastify.all('/clear-browser-cache', (request, reply) => {
+ if (['GET', 'POST'].includes(request.method)) {
+ reply.header('Clear-Site-Data', '"cache", "prefetchCache", "prerenderCache", "executionContexts"');
+ reply.code(204);
+ reply.send();
+ } else {
+ reply.code(405);
+ reply.send();
+ }
+ });
+
// Make sure any unknown path under /api returns HTTP 404 Not Found,
// because otherwise ClientServerService will return the base client HTML
// page with HTTP 200.
diff --git a/packages/backend/src/server/api/endpoints/admin/accounts/create.ts b/packages/backend/src/server/api/endpoints/admin/accounts/create.ts
index 06047b58a6..6606202118 100644
--- a/packages/backend/src/server/api/endpoints/admin/accounts/create.ts
+++ b/packages/backend/src/server/api/endpoints/admin/accounts/create.ts
@@ -34,13 +34,22 @@ export const meta = {
res: {
type: 'object',
optional: false, nullable: false,
- ref: 'MeDetailed',
- properties: {
- token: {
- type: 'string',
- optional: false, nullable: false,
+ allOf: [
+ {
+ type: 'object',
+ ref: 'MeDetailed',
},
- },
+ {
+ type: 'object',
+ optional: false, nullable: false,
+ properties: {
+ token: {
+ type: 'string',
+ optional: false, nullable: false,
+ },
+ },
+ }
+ ],
},
} as const;
diff --git a/packages/backend/src/server/api/endpoints/users/lists/show.ts b/packages/backend/src/server/api/endpoints/users/lists/show.ts
index ed5952d4c5..c6d477a92f 100644
--- a/packages/backend/src/server/api/endpoints/users/lists/show.ts
+++ b/packages/backend/src/server/api/endpoints/users/lists/show.ts
@@ -22,17 +22,26 @@ export const meta = {
res: {
type: 'object',
optional: false, nullable: false,
- ref: 'UserList',
- properties: {
- likedCount: {
- type: 'number',
- optional: true, nullable: false,
+ allOf: [
+ {
+ type: 'object',
+ ref: 'UserList',
},
- isLiked: {
- type: 'boolean',
- optional: true, nullable: false,
+ {
+ type: 'object',
+ optional: false, nullable: false,
+ properties: {
+ likedCount: {
+ type: 'number',
+ optional: true, nullable: false,
+ },
+ isLiked: {
+ type: 'boolean',
+ optional: true, nullable: false,
+ },
+ },
},
- },
+ ],
},
errors: {
diff --git a/packages/backend/test-federation/test/utils.ts b/packages/backend/test-federation/test/utils.ts
index 7e24bb7904..056a16ba15 100644
--- a/packages/backend/test-federation/test/utils.ts
+++ b/packages/backend/test-federation/test/utils.ts
@@ -68,7 +68,6 @@ async function createAdmin(host: Host): Promise {
ADMIN_CACHE.set(host, {
id: res.id,
- // @ts-expect-error FIXME: openapi-typescript generates incorrect response type for this endpoint, so ignore this
i: res.token,
});
return res as Misskey.entities.SignupResponse;
diff --git a/packages/frontend-builder/package.json b/packages/frontend-builder/package.json
index 5fdd25b32d..f03efac6d0 100644
--- a/packages/frontend-builder/package.json
+++ b/packages/frontend-builder/package.json
@@ -20,6 +20,6 @@
"dependencies": {
"estree-walker": "3.0.3",
"magic-string": "0.30.17",
- "vite": "7.0.6"
+ "vite": "7.0.7"
}
}
diff --git a/packages/frontend-embed/eslint.config.js b/packages/frontend-embed/eslint.config.js
index 179d811e77..46247e40d5 100644
--- a/packages/frontend-embed/eslint.config.js
+++ b/packages/frontend-embed/eslint.config.js
@@ -46,9 +46,71 @@ export default [
allowSingleExtends: true,
}],
'import/consistent-type-specifier-style': ['error', 'prefer-top-level'],
- // window の禁止理由: グローバルスコープと衝突し、予期せぬ結果を招くため
- // e の禁止理由: error や event など、複数のキーワードの頭文字であり分かりにくいため
- 'id-denylist': ['error', 'window', 'e'],
+ // window ... グローバルスコープと衝突し、予期せぬ結果を招くため
+ // e ... error や event など、複数のキーワードの頭文字であり分かりにくいため
+ // close ... window.closeと衝突 or 紛らわしい
+ // open ... window.openと衝突 or 紛らわしい
+ // fetch ... window.fetchと衝突 or 紛らわしい
+ // location ... window.locationと衝突 or 紛らわしい
+ // document ... window.documentと衝突 or 紛らわしい
+ // history ... window.historyと衝突 or 紛らわしい
+ // scroll ... window.scrollと衝突 or 紛らわしい
+ // setTimeout ... window.setTimeoutと衝突 or 紛らわしい
+ // setInterval ... window.setIntervalと衝突 or 紛らわしい
+ // clearTimeout ... window.clearTimeoutと衝突 or 紛らわしい
+ // clearInterval ... window.clearIntervalと衝突 or 紛らわしい
+ 'id-denylist': ['error', 'window', 'e', 'close', 'open', 'fetch', 'location', 'document', 'history', 'scroll', 'setTimeout', 'setInterval', 'clearTimeout', 'clearInterval'],
+ 'no-restricted-globals': [
+ 'error',
+ {
+ 'name': 'open',
+ 'message': 'Use `window.open`.',
+ },
+ {
+ 'name': 'close',
+ 'message': 'Use `window.close`.',
+ },
+ {
+ 'name': 'fetch',
+ 'message': 'Use `window.fetch`.',
+ },
+ {
+ 'name': 'location',
+ 'message': 'Use `window.location`.',
+ },
+ {
+ 'name': 'document',
+ 'message': 'Use `window.document`.',
+ },
+ {
+ 'name': 'history',
+ 'message': 'Use `window.history`.',
+ },
+ {
+ 'name': 'scroll',
+ 'message': 'Use `window.scroll`.',
+ },
+ {
+ 'name': 'setTimeout',
+ 'message': 'Use `window.setTimeout`.',
+ },
+ {
+ 'name': 'setInterval',
+ 'message': 'Use `window.setInterval`.',
+ },
+ {
+ 'name': 'clearTimeout',
+ 'message': 'Use `window.clearTimeout`.',
+ },
+ {
+ 'name': 'clearInterval',
+ 'message': 'Use `window.clearInterval`.',
+ },
+ {
+ 'name': 'name',
+ 'message': 'Use `window.name`. もしくは name という変数名を定義し忘れている',
+ },
+ ],
'no-shadow': ['warn'],
'vue/attributes-order': ['error', {
alphabetical: false,
diff --git a/packages/frontend-embed/package.json b/packages/frontend-embed/package.json
index 73bcd798f0..6a3392c021 100644
--- a/packages/frontend-embed/package.json
+++ b/packages/frontend-embed/package.json
@@ -13,10 +13,10 @@
"@discordapp/twemoji": "16.0.1",
"@rollup/plugin-json": "6.1.0",
"@rollup/plugin-replace": "6.0.2",
- "@rollup/pluginutils": "5.2.0",
+ "@rollup/pluginutils": "5.3.0",
"@twemoji/parser": "16.0.0",
"@vitejs/plugin-vue": "6.0.1",
- "@vue/compiler-sfc": "3.5.19",
+ "@vue/compiler-sfc": "3.5.21",
"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.48.0",
- "sass": "1.90.0",
- "shiki": "3.11.0",
+ "rollup": "4.50.1",
+ "sass": "1.92.1",
+ "shiki": "3.12.2",
"tinycolor2": "1.6.0",
"tsc-alias": "1.8.16",
"tsconfig-paths": "4.2.0",
"typescript": "5.9.2",
"uuid": "11.1.0",
- "vite": "7.1.3",
- "vue": "3.5.19"
+ "vite": "7.1.5",
+ "vue": "3.5.21"
},
"devDependencies": {
"@misskey-dev/summaly": "5.2.3",
@@ -43,14 +43,14 @@
"@testing-library/vue": "8.1.0",
"@types/estree": "1.0.8",
"@types/micromatch": "4.0.9",
- "@types/node": "22.17.2",
+ "@types/node": "22.18.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.40.0",
- "@typescript-eslint/parser": "8.40.0",
+ "@typescript-eslint/eslint-plugin": "8.42.0",
+ "@typescript-eslint/parser": "8.42.0",
"@vitest/coverage-v8": "3.2.4",
- "@vue/runtime-core": "3.5.19",
+ "@vue/runtime-core": "3.5.21",
"acorn": "8.15.0",
"cross-env": "10.0.0",
"eslint-plugin-import": "2.32.0",
@@ -59,11 +59,11 @@
"happy-dom": "18.0.1",
"intersection-observer": "0.12.2",
"micromatch": "4.0.8",
- "msw": "2.10.5",
+ "msw": "2.11.1",
"nodemon": "3.1.10",
"prettier": "3.6.2",
- "start-server-and-test": "2.0.13",
- "tsx": "4.20.4",
+ "start-server-and-test": "2.1.0",
+ "tsx": "4.20.5",
"vite-plugin-turbosnap": "1.0.3",
"vue-component-type-helpers": "3.0.6",
"vue-eslint-parser": "10.2.0",
diff --git a/packages/frontend-embed/src/boot.ts b/packages/frontend-embed/src/boot.ts
index 9d69437c30..961cbcef66 100644
--- a/packages/frontend-embed/src/boot.ts
+++ b/packages/frontend-embed/src/boot.ts
@@ -33,7 +33,7 @@ import type { Theme } from '@/theme.js';
console.log('Misskey Embed');
//#region Embedパラメータの取得・パース
-const params = new URLSearchParams(location.search);
+const params = new URLSearchParams(window.location.search);
const embedParams = parseEmbedParams(params);
if (_DEV_) console.log(embedParams);
//#endregion
@@ -81,7 +81,7 @@ storeBootloaderErrors({ ...i18n.ts._bootErrors, reload: i18n.ts.reload });
//#endregion
// サイズの制限
-document.documentElement.style.maxWidth = '500px';
+window.document.documentElement.style.maxWidth = '500px';
// iframeIdの設定
function setIframeIdHandler(event: MessageEvent) {
@@ -114,16 +114,16 @@ app.provide(DI.embedParams, embedParams);
const rootEl = ((): HTMLElement => {
const MISSKEY_MOUNT_DIV_ID = 'misskey_app';
- const currentRoot = document.getElementById(MISSKEY_MOUNT_DIV_ID);
+ const currentRoot = window.document.getElementById(MISSKEY_MOUNT_DIV_ID);
if (currentRoot) {
console.warn('multiple import detected');
return currentRoot;
}
- const root = document.createElement('div');
+ const root = window.document.createElement('div');
root.id = MISSKEY_MOUNT_DIV_ID;
- document.body.appendChild(root);
+ window.document.body.appendChild(root);
return root;
})();
@@ -159,7 +159,7 @@ console.log(i18n.tsx._selfXssPrevention.description3({ link: 'https://misskey-hu
//#endregion
function removeSplash() {
- const splash = document.getElementById('splash');
+ const splash = window.document.getElementById('splash');
if (splash) {
splash.style.opacity = '0';
splash.style.pointerEvents = 'none';
diff --git a/packages/frontend-embed/src/components/EmImgWithBlurhash.vue b/packages/frontend-embed/src/components/EmImgWithBlurhash.vue
index 0bff048ce4..71f0ee9294 100644
--- a/packages/frontend-embed/src/components/EmImgWithBlurhash.vue
+++ b/packages/frontend-embed/src/components/EmImgWithBlurhash.vue
@@ -19,7 +19,7 @@ import { extractAvgColorFromBlurhash } from '@@/js/extract-avg-color-from-blurha
const canvasPromise = new Promise(resolve => {
// テスト環境で Web Worker インスタンスは作成できない
if (import.meta.env.MODE === 'test') {
- const canvas = document.createElement('canvas');
+ const canvas = window.document.createElement('canvas');
canvas.width = 64;
canvas.height = 64;
resolve(canvas);
@@ -34,7 +34,7 @@ const canvasPromise = new Promise(resol
);
resolve(workers);
} else {
- const canvas = document.createElement('canvas');
+ const canvas = window.document.createElement('canvas');
canvas.width = 64;
canvas.height = 64;
resolve(canvas);
diff --git a/packages/frontend-embed/src/components/EmInstanceTicker.vue b/packages/frontend-embed/src/components/EmInstanceTicker.vue
index 4a116e317a..7add3bb53f 100644
--- a/packages/frontend-embed/src/components/EmInstanceTicker.vue
+++ b/packages/frontend-embed/src/components/EmInstanceTicker.vue
@@ -29,7 +29,7 @@ const props = defineProps<{
// if no instance data is given, this is for the local instance
const instance = props.instance ?? {
name: serverMetadata.name,
- themeColor: (document.querySelector('meta[name="theme-color-orig"]') as HTMLMetaElement)?.content,
+ themeColor: (window.document.querySelector('meta[name="theme-color-orig"]') as HTMLMetaElement)?.content,
};
const faviconUrl = computed(() => props.instance ? mediaProxy.getProxiedImageUrlNullable(props.instance.faviconUrl, 'preview') : mediaProxy.getProxiedImageUrlNullable(serverMetadata.iconUrl, 'preview') ?? '/favicon.ico');
diff --git a/packages/frontend-embed/src/components/EmMention.vue b/packages/frontend-embed/src/components/EmMention.vue
index b5aaa95894..0a8ac9c05a 100644
--- a/packages/frontend-embed/src/components/EmMention.vue
+++ b/packages/frontend-embed/src/components/EmMention.vue
@@ -27,7 +27,7 @@ const canonical = props.host === localHost ? `@${props.username}` : `@${props.us
const url = `/${canonical}`;
-const bg = tinycolor(getComputedStyle(document.documentElement).getPropertyValue('--MI_THEME-mention'));
+const bg = tinycolor(getComputedStyle(window.document.documentElement).getPropertyValue('--MI_THEME-mention'));
bg.setAlpha(0.1);
const bgCss = bg.toRgbString();
diff --git a/packages/frontend-embed/src/components/EmPagination.vue b/packages/frontend-embed/src/components/EmPagination.vue
index 94a91305f4..bd49d127a9 100644
--- a/packages/frontend-embed/src/components/EmPagination.vue
+++ b/packages/frontend-embed/src/components/EmPagination.vue
@@ -134,7 +134,7 @@ const isBackTop = ref(false);
const empty = computed(() => items.value.size === 0);
const error = ref(false);
-const scrollableElement = computed(() => rootEl.value ? getScrollContainer(rootEl.value) : document.body);
+const scrollableElement = computed(() => rootEl.value ? getScrollContainer(rootEl.value) : window.document.body);
const visibility = useDocumentVisibility();
@@ -353,7 +353,7 @@ watch(visibility, () => {
BACKGROUND_PAUSE_WAIT_SEC * 1000);
} else { // 'visible'
if (timerForSetPause) {
- clearTimeout(timerForSetPause);
+ window.clearTimeout(timerForSetPause);
timerForSetPause = null;
} else {
isPausingUpdate = false;
@@ -447,11 +447,11 @@ onBeforeMount(() => {
init().then(() => {
if (props.pagination.reversed) {
nextTick(() => {
- setTimeout(toBottom, 800);
+ window.setTimeout(toBottom, 800);
// scrollToBottomでmoreFetchingボタンが画面外まで出るまで
// more = trueを遅らせる
- setTimeout(() => {
+ window.setTimeout(() => {
moreFetching.value = false;
}, 2000);
});
@@ -461,11 +461,11 @@ onBeforeMount(() => {
onBeforeUnmount(() => {
if (timerForSetPause) {
- clearTimeout(timerForSetPause);
+ window.clearTimeout(timerForSetPause);
timerForSetPause = null;
}
if (preventAppearFetchMoreTimer.value) {
- clearTimeout(preventAppearFetchMoreTimer.value);
+ window.clearTimeout(preventAppearFetchMoreTimer.value);
preventAppearFetchMoreTimer.value = null;
}
scrollObserver.value?.disconnect();
diff --git a/packages/frontend-embed/src/server-context.ts b/packages/frontend-embed/src/server-context.ts
index a84a1a726a..c061d5a6f1 100644
--- a/packages/frontend-embed/src/server-context.ts
+++ b/packages/frontend-embed/src/server-context.ts
@@ -4,7 +4,7 @@
*/
import * as Misskey from 'misskey-js';
-const providedContextEl = document.getElementById('misskey_embedCtx');
+const providedContextEl = window.document.getElementById('misskey_embedCtx');
export type ServerContext = {
clip?: Misskey.entities.Clip;
diff --git a/packages/frontend-embed/src/server-metadata.ts b/packages/frontend-embed/src/server-metadata.ts
index 6c94aacd48..ad9b5a1a91 100644
--- a/packages/frontend-embed/src/server-metadata.ts
+++ b/packages/frontend-embed/src/server-metadata.ts
@@ -6,7 +6,7 @@
import * as Misskey from 'misskey-js';
import { misskeyApi } from '@/misskey-api.js';
-const providedMetaEl = document.getElementById('misskey_meta');
+const providedMetaEl = window.document.getElementById('misskey_meta');
const _serverMetadata: Misskey.entities.MetaDetailed | null = (providedMetaEl && providedMetaEl.textContent) ? JSON.parse(providedMetaEl.textContent) : null;
diff --git a/packages/frontend-embed/src/theme.ts b/packages/frontend-embed/src/theme.ts
index c9b1c0d0c6..c7bc5df85d 100644
--- a/packages/frontend-embed/src/theme.ts
+++ b/packages/frontend-embed/src/theme.ts
@@ -35,15 +35,15 @@ export function assertIsTheme(theme: Record): theme is Theme {
export function applyTheme(theme: Theme, persist = true) {
if (timeout) window.clearTimeout(timeout);
- document.documentElement.classList.add('_themeChanging_');
+ window.document.documentElement.classList.add('_themeChanging_');
timeout = window.setTimeout(() => {
- document.documentElement.classList.remove('_themeChanging_');
+ window.document.documentElement.classList.remove('_themeChanging_');
}, 1000);
const colorScheme = theme.base === 'dark' ? 'dark' : 'light';
- document.documentElement.dataset.colorScheme = colorScheme;
+ window.document.documentElement.dataset.colorScheme = colorScheme;
// Deep copy
const _theme = JSON.parse(JSON.stringify(theme));
@@ -55,7 +55,7 @@ export function applyTheme(theme: Theme, persist = true) {
const props = compile(_theme);
- for (const tag of document.head.children) {
+ for (const tag of window.document.head.children) {
if (tag.tagName === 'META' && tag.getAttribute('name') === 'theme-color') {
tag.setAttribute('content', props['htmlThemeColor']);
break;
@@ -63,7 +63,7 @@ export function applyTheme(theme: Theme, persist = true) {
}
for (const [k, v] of Object.entries(props)) {
- document.documentElement.style.setProperty(`--MI_THEME-${k}`, v.toString());
+ window.document.documentElement.style.setProperty(`--MI_THEME-${k}`, v.toString());
}
// iframeを正常に透過させるために、cssのcolor-schemeは `light dark;` 固定にしてある。style.scss参照
diff --git a/packages/frontend-embed/src/ui.vue b/packages/frontend-embed/src/ui.vue
index 4ba5968a91..711d0eae6d 100644
--- a/packages/frontend-embed/src/ui.vue
+++ b/packages/frontend-embed/src/ui.vue
@@ -52,8 +52,8 @@ function safeURIDecode(str: string): string {
}
}
-const page = location.pathname.split('/')[2];
-const contentId = safeURIDecode(location.pathname.split('/')[3]);
+const page = window.location.pathname.split('/')[2];
+const contentId = safeURIDecode(window.location.pathname.split('/')[3]);
if (_DEV_) console.log(page, contentId);
const embedParams = inject(DI.embedParams, defaultEmbedParams);
diff --git a/packages/frontend-shared/eslint.config.js b/packages/frontend-shared/eslint.config.js
index 6453be0042..b972cfdb27 100644
--- a/packages/frontend-shared/eslint.config.js
+++ b/packages/frontend-shared/eslint.config.js
@@ -51,9 +51,71 @@ export default [
allowSingleExtends: true,
}],
'import/consistent-type-specifier-style': ['error', 'prefer-top-level'],
- // window の禁止理由: グローバルスコープと衝突し、予期せぬ結果を招くため
- // e の禁止理由: error や event など、複数のキーワードの頭文字であり分かりにくいため
- 'id-denylist': ['error', 'window', 'e'],
+ // window ... グローバルスコープと衝突し、予期せぬ結果を招くため
+ // e ... error や event など、複数のキーワードの頭文字であり分かりにくいため
+ // close ... window.closeと衝突 or 紛らわしい
+ // open ... window.openと衝突 or 紛らわしい
+ // fetch ... window.fetchと衝突 or 紛らわしい
+ // location ... window.locationと衝突 or 紛らわしい
+ // document ... window.documentと衝突 or 紛らわしい
+ // history ... window.historyと衝突 or 紛らわしい
+ // scroll ... window.scrollと衝突 or 紛らわしい
+ // setTimeout ... window.setTimeoutと衝突 or 紛らわしい
+ // setInterval ... window.setIntervalと衝突 or 紛らわしい
+ // clearTimeout ... window.clearTimeoutと衝突 or 紛らわしい
+ // clearInterval ... window.clearIntervalと衝突 or 紛らわしい
+ 'id-denylist': ['error', 'window', 'e', 'close', 'open', 'fetch', 'location', 'document', 'history', 'scroll', 'setTimeout', 'setInterval', 'clearTimeout', 'clearInterval'],
+ 'no-restricted-globals': [
+ 'error',
+ {
+ 'name': 'open',
+ 'message': 'Use `window.open`.',
+ },
+ {
+ 'name': 'close',
+ 'message': 'Use `window.close`.',
+ },
+ {
+ 'name': 'fetch',
+ 'message': 'Use `window.fetch`.',
+ },
+ {
+ 'name': 'location',
+ 'message': 'Use `window.location`.',
+ },
+ {
+ 'name': 'document',
+ 'message': 'Use `window.document`.',
+ },
+ {
+ 'name': 'history',
+ 'message': 'Use `window.history`.',
+ },
+ {
+ 'name': 'scroll',
+ 'message': 'Use `window.scroll`.',
+ },
+ {
+ 'name': 'setTimeout',
+ 'message': 'Use `window.setTimeout`.',
+ },
+ {
+ 'name': 'setInterval',
+ 'message': 'Use `window.setInterval`.',
+ },
+ {
+ 'name': 'clearTimeout',
+ 'message': 'Use `window.clearTimeout`.',
+ },
+ {
+ 'name': 'clearInterval',
+ 'message': 'Use `window.clearInterval`.',
+ },
+ {
+ 'name': 'name',
+ 'message': 'Use `window.name`. もしくは name という変数名を定義し忘れている',
+ },
+ ],
'no-shadow': ['warn'],
'vue/attributes-order': ['error', {
alphabetical: false,
diff --git a/packages/frontend-shared/js/config.ts b/packages/frontend-shared/js/config.ts
index ac5c5629f3..6272d3f6b9 100644
--- a/packages/frontend-shared/js/config.ts
+++ b/packages/frontend-shared/js/config.ts
@@ -4,15 +4,15 @@
*/
// eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing
-const address = new URL(document.querySelector('meta[property="instance_url"]')?.content || location.href);
-const siteName = document.querySelector('meta[property="og:site_name"]')?.content;
+const address = new URL(window.document.querySelector('meta[property="instance_url"]')?.content || window.location.href);
+const siteName = window.document.querySelector('meta[property="og:site_name"]')?.content;
export const host = address.host;
export const hostname = address.hostname;
export const url = address.origin;
export const port = address.port;
-export const apiUrl = location.origin + '/api';
-export const wsOrigin = location.origin;
+export const apiUrl = window.location.origin + '/api';
+export const wsOrigin = window.location.origin;
export const lang = localStorage.getItem('lang') ?? 'en-US';
export const langs = _LANGS_;
export const version = _VERSION_;
diff --git a/packages/frontend-shared/js/scroll.ts b/packages/frontend-shared/js/scroll.ts
index 9057b896c6..5578cffdec 100644
--- a/packages/frontend-shared/js/scroll.ts
+++ b/packages/frontend-shared/js/scroll.ts
@@ -51,7 +51,7 @@ export function onScrollTop(el: HTMLElement, cb: (topVisible: boolean) => unknow
// - toleranceの範囲内に収まる程度の微量なスクロールが発生した
let prevTopVisible = firstTopVisible;
const onScroll = () => {
- if (!document.body.contains(el)) return;
+ if (!window.document.body.contains(el)) return;
const topVisible = isHeadVisible(el, tolerance);
if (topVisible !== prevTopVisible) {
@@ -78,7 +78,7 @@ export function onScrollBottom(el: HTMLElement, cb: () => unknown, tolerance = 1
const containerOrWindow = container ?? window;
const onScroll = () => {
- if (!document.body.contains(el)) return;
+ if (!window.document.body.contains(el)) return;
if (isTailVisible(el, 1, container)) {
cb();
if (once) removeListener();
@@ -145,8 +145,8 @@ export function isTailVisible(el: HTMLElement, tolerance = 1, container = getScr
// https://ja.javascript.info/size-and-scroll-window#ref-932
export function getBodyScrollHeight() {
return Math.max(
- document.body.scrollHeight, document.documentElement.scrollHeight,
- document.body.offsetHeight, document.documentElement.offsetHeight,
- document.body.clientHeight, document.documentElement.clientHeight,
+ window.document.body.scrollHeight, window.document.documentElement.scrollHeight,
+ window.document.body.offsetHeight, window.document.documentElement.offsetHeight,
+ window.document.body.clientHeight, window.document.documentElement.clientHeight,
);
}
diff --git a/packages/frontend-shared/js/use-document-visibility.ts b/packages/frontend-shared/js/use-document-visibility.ts
index b1197e68da..a87c1f1bab 100644
--- a/packages/frontend-shared/js/use-document-visibility.ts
+++ b/packages/frontend-shared/js/use-document-visibility.ts
@@ -7,18 +7,18 @@ import { onMounted, onUnmounted, ref } from 'vue';
import type { Ref } from 'vue';
export function useDocumentVisibility(): Ref {
- const visibility = ref(document.visibilityState);
+ const visibility = ref(window.document.visibilityState);
const onChange = (): void => {
- visibility.value = document.visibilityState;
+ visibility.value = window.document.visibilityState;
};
onMounted(() => {
- document.addEventListener('visibilitychange', onChange);
+ window.document.addEventListener('visibilitychange', onChange);
});
onUnmounted(() => {
- document.removeEventListener('visibilitychange', onChange);
+ window.document.removeEventListener('visibilitychange', onChange);
});
return visibility;
diff --git a/packages/frontend-shared/package.json b/packages/frontend-shared/package.json
index ea2d66a7c7..aebc418e3c 100644
--- a/packages/frontend-shared/package.json
+++ b/packages/frontend-shared/package.json
@@ -21,9 +21,9 @@
"lint": "pnpm typecheck && pnpm eslint"
},
"devDependencies": {
- "@types/node": "22.17.2",
- "@typescript-eslint/eslint-plugin": "8.40.0",
- "@typescript-eslint/parser": "8.40.0",
+ "@types/node": "22.18.1",
+ "@typescript-eslint/eslint-plugin": "8.42.0",
+ "@typescript-eslint/parser": "8.42.0",
"esbuild": "0.25.9",
"eslint-plugin-vue": "10.4.0",
"nodemon": "3.1.10",
@@ -35,6 +35,6 @@
],
"dependencies": {
"misskey-js": "workspace:*",
- "vue": "3.5.19"
+ "vue": "3.5.21"
}
}
diff --git a/packages/frontend/package.json b/packages/frontend/package.json
index 108fae7305..f207d04b96 100644
--- a/packages/frontend/package.json
+++ b/packages/frontend/package.json
@@ -23,13 +23,13 @@
"@misskey-dev/browser-image-resizer": "2024.1.0",
"@rollup/plugin-json": "6.1.0",
"@rollup/plugin-replace": "6.0.2",
- "@rollup/pluginutils": "5.2.0",
- "@sentry/vue": "10.5.0",
+ "@rollup/pluginutils": "5.3.0",
+ "@sentry/vue": "10.10.0",
"@syuilo/aiscript": "1.1.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.19",
+ "@vue/compiler-sfc": "3.5.21",
"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.1.3",
+ "chromatic": "13.1.4",
"compare-versions": "6.1.1",
"cropperjs": "2.0.1",
"date-fns": "4.1.0",
@@ -63,21 +63,21 @@
"misskey-reversi": "workspace:*",
"photoswipe": "5.4.4",
"punycode.js": "2.3.1",
- "rollup": "4.48.0",
+ "rollup": "4.50.1",
"sanitize-html": "2.17.0",
- "sass": "1.90.0",
- "shiki": "3.11.0",
+ "sass": "1.92.1",
+ "shiki": "3.12.2",
"strict-event-emitter-types": "2.0.0",
"textarea-caret": "3.1.0",
- "three": "0.179.1",
+ "three": "0.180.0",
"throttle-debounce": "5.0.2",
"tinycolor2": "1.6.0",
"tsc-alias": "1.8.16",
"tsconfig-paths": "4.2.0",
"typescript": "5.9.2",
"v-code-diff": "1.13.1",
- "vite": "7.1.3",
- "vue": "3.5.19",
+ "vite": "7.1.5",
+ "vue": "3.5.21",
"vuedraggable": "next",
"wanakana": "5.3.1"
},
@@ -85,7 +85,7 @@
"@misskey-dev/summaly": "5.2.3",
"@storybook/addon-essentials": "8.6.14",
"@storybook/addon-interactions": "8.6.14",
- "@storybook/addon-links": "9.1.3",
+ "@storybook/addon-links": "9.1.5",
"@storybook/addon-mdx-gfm": "8.6.14",
"@storybook/addon-storysource": "8.6.14",
"@storybook/blocks": "8.6.14",
@@ -93,31 +93,31 @@
"@storybook/core-events": "8.6.14",
"@storybook/manager-api": "8.6.14",
"@storybook/preview-api": "8.6.14",
- "@storybook/react": "9.1.3",
- "@storybook/react-vite": "9.1.3",
+ "@storybook/react": "9.1.5",
+ "@storybook/react-vite": "9.1.5",
"@storybook/test": "8.6.14",
"@storybook/theming": "8.6.14",
"@storybook/types": "8.6.14",
- "@storybook/vue3": "9.1.3",
- "@storybook/vue3-vite": "9.1.3",
+ "@storybook/vue3": "9.1.5",
+ "@storybook/vue3-vite": "9.1.5",
"@tabler/icons-webfont": "3.34.1",
"@testing-library/vue": "8.1.0",
"@types/canvas-confetti": "1.9.0",
"@types/estree": "1.0.8",
"@types/matter-js": "0.20.0",
"@types/micromatch": "4.0.9",
- "@types/node": "22.17.2",
+ "@types/node": "22.18.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.40.0",
- "@typescript-eslint/parser": "8.40.0",
+ "@typescript-eslint/eslint-plugin": "8.42.0",
+ "@typescript-eslint/parser": "8.42.0",
"@vitest/coverage-v8": "3.2.4",
- "@vue/compiler-core": "3.5.19",
- "@vue/runtime-core": "3.5.19",
+ "@vue/compiler-core": "3.5.21",
+ "@vue/runtime-core": "3.5.21",
"acorn": "8.15.0",
"cross-env": "10.0.0",
"cypress": "14.5.4",
@@ -128,17 +128,17 @@
"intersection-observer": "0.12.2",
"micromatch": "4.0.8",
"minimatch": "10.0.3",
- "msw": "2.10.5",
+ "msw": "2.11.1",
"msw-storybook-addon": "2.0.5",
"nodemon": "3.1.10",
"prettier": "3.6.2",
"react": "19.1.1",
"react-dom": "19.1.1",
"seedrandom": "3.0.5",
- "start-server-and-test": "2.0.13",
- "storybook": "9.1.3",
+ "start-server-and-test": "2.1.0",
+ "storybook": "9.1.5",
"storybook-addon-misskey-theme": "github:misskey-dev/storybook-addon-misskey-theme",
- "tsx": "4.20.4",
+ "tsx": "4.20.5",
"vite-plugin-turbosnap": "1.0.3",
"vitest": "3.2.4",
"vitest-fetch-mock": "0.4.5",
diff --git a/packages/frontend/src/components/MkAvatars.vue b/packages/frontend/src/components/MkAvatars.vue
index 1c44ed60d8..4bd6c62a5f 100644
--- a/packages/frontend/src/components/MkAvatars.vue
+++ b/packages/frontend/src/components/MkAvatars.vue
@@ -29,6 +29,6 @@ const users = ref([]);
onMounted(async () => {
users.value = await misskeyApi('users/show', {
userIds: props.userIds,
- }) as unknown as Misskey.entities.UserLite[];
+ });
});
diff --git a/packages/frontend/src/components/MkPoll.vue b/packages/frontend/src/components/MkPoll.vue
index 359ee08812..76c65397ae 100644
--- a/packages/frontend/src/components/MkPoll.vue
+++ b/packages/frontend/src/components/MkPoll.vue
@@ -27,16 +27,16 @@ SPDX-License-Identifier: AGPL-3.0-only
diff --git a/packages/frontend/src/composables/use-lowres-time.ts b/packages/frontend/src/composables/use-lowres-time.ts
new file mode 100644
index 0000000000..3c5b561f51
--- /dev/null
+++ b/packages/frontend/src/composables/use-lowres-time.ts
@@ -0,0 +1,34 @@
+/*
+ * SPDX-FileCopyrightText: syuilo and misskey-project
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+import { ref, readonly, computed } from 'vue';
+
+const time = ref(Date.now());
+
+export const TIME_UPDATE_INTERVAL = 10000; // 10秒
+
+/**
+ * 精度が求められないが定期的に更新しないといけない時計で使用(10秒に一度更新)。
+ * tickを各コンポーネントで行うのではなく、ここで一括して行うことでパフォーマンスを改善する。
+ *
+ * ※ マウント前の時刻を返す可能性があるため、通常は`useLowresTime`を使用する
+*/
+export const lowresTime = readonly(time);
+
+/**
+ * 精度が求められないが定期的に更新しないといけない時計で使用(10秒に一度更新)。
+ * tickを各コンポーネントで行うのではなく、ここで一括して行うことでパフォーマンスを改善する。
+ *
+ * 必ず現在時刻以降を返すことを保証するコンポーサブル
+ */
+export function useLowresTime() {
+ // lowresTime自体はマウント前の時刻を返す可能性があるため、必ず現在時刻以降を返すことを保証する
+ const now = Date.now();
+ return computed(() => Math.max(time.value, now));
+}
+
+window.setInterval(() => {
+ time.value = Date.now();
+}, TIME_UPDATE_INTERVAL);
diff --git a/packages/frontend/src/events.ts b/packages/frontend/src/events.ts
index 649561cd75..8cac1b6d2a 100644
--- a/packages/frontend/src/events.ts
+++ b/packages/frontend/src/events.ts
@@ -24,7 +24,7 @@ export const globalEvents = new EventEmitter();
export function useGlobalEvent(
event: T,
- callback: Events[T],
+ callback: EventEmitter.EventListener,
): void {
globalEvents.on(event, callback);
onBeforeUnmount(() => {
diff --git a/packages/frontend/src/lib/pizzax.ts b/packages/frontend/src/lib/pizzax.ts
index 20d44032df..6dffcf9478 100644
--- a/packages/frontend/src/lib/pizzax.ts
+++ b/packages/frontend/src/lib/pizzax.ts
@@ -94,7 +94,7 @@ export class Pizzax {
private mergeState(value: X, def: X): X {
if (this.isPureObject(value) && this.isPureObject(def)) {
- const merged = deepMerge(value, def);
+ const merged = deepMerge>(value, def);
if (_DEV_) console.log('Merging state. Incoming: ', value, ' Default: ', def, ' Result: ', merged);
diff --git a/packages/frontend/src/os.ts b/packages/frontend/src/os.ts
index a38de4576e..6c5f04c6b5 100644
--- a/packages/frontend/src/os.ts
+++ b/packages/frontend/src/os.ts
@@ -36,9 +36,9 @@ import { focusParent } from '@/utility/focus.js';
export const openingWindowsCount = ref(0);
export type ApiWithDialogCustomErrors = Record;
-export const apiWithDialog = ((
+export const apiWithDialog = ((
endpoint: E,
- data: P,
+ data: Misskey.Endpoints[E]['req'],
token?: string | null | undefined,
customErrors?: ApiWithDialogCustomErrors,
) => {
diff --git a/packages/frontend/src/pages/about.emojis.vue b/packages/frontend/src/pages/about.emojis.vue
index 7e514c5a73..4640812756 100644
--- a/packages/frontend/src/pages/about.emojis.vue
+++ b/packages/frontend/src/pages/about.emojis.vue
@@ -11,12 +11,6 @@ SPDX-License-Identifier: AGPL-3.0-only
-
-
@@ -42,51 +36,33 @@ import XEmoji from './emojis.emoji.vue';
import MkButton from '@/components/MkButton.vue';
import MkInput from '@/components/MkInput.vue';
import MkFoldableSection from '@/components/MkFoldableSection.vue';
-import { customEmojis, customEmojiCategories, getCustomEmojiTags } from '@/custom-emojis.js';
+import { customEmojis, customEmojiCategories } from '@/custom-emojis.js';
import { i18n } from '@/i18n.js';
import { $i } from '@/i.js';
-const customEmojiTags = getCustomEmojiTags();
const q = ref('');
const searchEmojis = ref(null);
-const selectedTags = ref(new Set());
function search() {
- if ((q.value === '' || q.value == null) && selectedTags.value.size === 0) {
+ if (q.value === '' || q.value == null) {
searchEmojis.value = null;
return;
}
- if (selectedTags.value.size === 0) {
- const queryarry = q.value.match(/\:([a-z0-9_]*)\:/g);
+ const queryarry = q.value.match(/\:([a-z0-9_]*)\:/g);
- if (queryarry) {
- searchEmojis.value = customEmojis.value.filter(emoji =>
- queryarry.includes(`:${emoji.name}:`),
- );
- } else {
- searchEmojis.value = customEmojis.value.filter(emoji => emoji.name.includes(q.value) || emoji.aliases.includes(q.value));
- }
+ if (queryarry) {
+ searchEmojis.value = customEmojis.value.filter(emoji =>
+ queryarry.includes(`:${emoji.name}:`),
+ );
} else {
- searchEmojis.value = customEmojis.value.filter(emoji => (emoji.name.includes(q.value) || emoji.aliases.includes(q.value)) && [...selectedTags.value].every(t => emoji.aliases.includes(t)));
- }
-}
-
-function toggleTag(tag) {
- if (selectedTags.value.has(tag)) {
- selectedTags.value.delete(tag);
- } else {
- selectedTags.value.add(tag);
+ searchEmojis.value = customEmojis.value.filter(emoji => emoji.name.includes(q.value) || emoji.aliases.includes(q.value));
}
}
watch(q, () => {
search();
});
-
-watch(selectedTags, () => {
- search();
-}, { deep: true });