Merge branch 'develop' into refactor-mkselect

This commit is contained in:
kakkokari-gtyih 2025-09-13 18:39:56 +09:00
commit 94b2b21ddd
102 changed files with 2477 additions and 2002 deletions

View File

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

View File

@ -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ó."

View File

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

View File

@ -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. <strong>Не использовать это в рабочей среде</strong>"
@ -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: "Изменить роль"

View File

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

View File

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

View File

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

View File

@ -29,7 +29,7 @@ export class AiService {
}
@bindThis
public async detectSensitive(path: string): Promise<nsfw.PredictionType[] | null> {
public async detectSensitive(source: string | Buffer): Promise<nsfw.PredictionType[] | null> {
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);

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -68,7 +68,6 @@ async function createAdmin(host: Host): Promise<Misskey.entities.SignupResponse
return await client.request('admin/accounts/create', ADMIN_PARAMS).then(res => {
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;

View File

@ -20,6 +20,6 @@
"dependencies": {
"estree-walker": "3.0.3",
"magic-string": "0.30.17",
"vite": "7.0.6"
"vite": "7.0.7"
}
}

View File

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

View File

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

View File

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

View File

@ -19,7 +19,7 @@ import { extractAvgColorFromBlurhash } from '@@/js/extract-avg-color-from-blurha
const canvasPromise = new Promise<WorkerMultiDispatch | HTMLCanvasElement>(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<WorkerMultiDispatch | HTMLCanvasElement>(resol
);
resolve(workers);
} else {
const canvas = document.createElement('canvas');
const canvas = window.document.createElement('canvas');
canvas.width = 64;
canvas.height = 64;
resolve(canvas);

View File

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

View File

@ -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();
</script>

View File

@ -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);
// scrollToBottommoreFetching
// 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();

View File

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

View File

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

View File

@ -35,15 +35,15 @@ export function assertIsTheme(theme: Record<string, unknown>): 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参照

View File

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

View File

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

View File

@ -4,15 +4,15 @@
*/
// eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing
const address = new URL(document.querySelector<HTMLMetaElement>('meta[property="instance_url"]')?.content || location.href);
const siteName = document.querySelector<HTMLMetaElement>('meta[property="og:site_name"]')?.content;
const address = new URL(window.document.querySelector<HTMLMetaElement>('meta[property="instance_url"]')?.content || window.location.href);
const siteName = window.document.querySelector<HTMLMetaElement>('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_;

View File

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

View File

@ -7,18 +7,18 @@ import { onMounted, onUnmounted, ref } from 'vue';
import type { Ref } from 'vue';
export function useDocumentVisibility(): Ref<DocumentVisibilityState> {
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;

View File

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

View File

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

View File

@ -29,6 +29,6 @@ const users = ref<Misskey.entities.UserLite[]>([]);
onMounted(async () => {
users.value = await misskeyApi('users/show', {
userIds: props.userIds,
}) as unknown as Misskey.entities.UserLite[];
});
});
</script>

View File

@ -27,16 +27,16 @@ SPDX-License-Identifier: AGPL-3.0-only
</template>
<script lang="ts" setup>
import { computed, ref } from 'vue';
import { computed, ref, watch } from 'vue';
import * as Misskey from 'misskey-js';
import { host } from '@@/js/config.js';
import { useInterval } from '@@/js/use-interval.js';
import type { OpenOnRemoteOptions } from '@/utility/please-login.js';
import { sum } from '@/utility/array.js';
import { pleaseLogin } from '@/utility/please-login.js';
import * as os from '@/os.js';
import { misskeyApi } from '@/utility/misskey-api.js';
import { i18n } from '@/i18n.js';
import { useLowresTime } from '@/composables/use-lowres-time.js';
const props = defineProps<{
noteId: string;
@ -48,7 +48,21 @@ const props = defineProps<{
author?: Misskey.entities.UserLite;
}>();
const remaining = ref(-1);
const now = useLowresTime();
const expiresAtTime = computed(() => props.expiresAt ? new Date(props.expiresAt).getTime() : null);
const remaining = computed(() => {
if (expiresAtTime.value == null) return -1;
return Math.floor(Math.max(expiresAtTime.value - now.value, 0) / 1000);
});
const remainingWatchStop = watch(remaining, (to) => {
if (to <= 0) {
showResult.value = true;
remainingWatchStop();
}
}, { immediate: true });
const total = computed(() => sum(props.choices.map(x => x.votes)));
const closed = computed(() => remaining.value === 0);
@ -71,22 +85,7 @@ const pleaseLoginContext = computed<OpenOnRemoteOptions>(() => ({
url: `https://${host}/notes/${props.noteId}`,
}));
//
if (props.expiresAt) {
const tick = () => {
remaining.value = Math.floor(Math.max(new Date(props.expiresAt!).getTime() - Date.now(), 0) / 1000);
if (remaining.value === 0) {
showResult.value = true;
}
};
useInterval(tick, 3000, {
immediate: true,
afterMounted: false,
});
}
const vote = async (id) => {
const vote = async (id: number) => {
if (props.readOnly || closed.value || isVoted.value) return;
pleaseLogin({ openOnRemote: pleaseLoginContext.value });

View File

@ -823,17 +823,15 @@ async function saveServerDraft(clearLocal = false) {
return await os.apiWithDialog(serverDraftId.value == null ? 'notes/drafts/create' : 'notes/drafts/update', {
...(serverDraftId.value == null ? {} : { draftId: serverDraftId.value }),
text: text.value,
useCw: useCw.value,
cw: cw.value,
cw: useCw.value ? cw.value || null : null,
visibility: visibility.value,
localOnly: localOnly.value,
hashtag: hashtags.value,
...(files.value.length > 0 ? { fileIds: files.value.map(f => f.id) } : {}),
poll: poll.value,
...(visibleUsers.value.length > 0 ? { visibleUserIds: visibleUsers.value.map(x => x.id) } : {}),
renoteId: renoteTargetNote.value ? renoteTargetNote.value.id : undefined,
renoteId: renoteTargetNote.value ? renoteTargetNote.value.id : quoteId.value ? quoteId.value : undefined,
replyId: replyTargetNote.value ? replyTargetNote.value.id : undefined,
quoteId: quoteId.value,
channelId: targetChannel.value ? targetChannel.value.id : undefined,
reactionAcceptance: reactionAcceptance.value,
}).then(() => {

View File

@ -90,7 +90,7 @@ function subscribe() {
publickey: encode(subscription.getKey('p256dh')),
});
}, async err => { // When subscribe failed
//
//
if (err?.name === 'NotAllowedError') {
console.info('User denied the notification permission request.');
return;
@ -114,14 +114,13 @@ async function unsubscribe() {
if ($i && accounts.length >= 2) {
apiWithDialog('sw/unregister', {
i: $i.token,
endpoint,
});
}, $i.token);
} else {
pushSubscription.value.unsubscribe();
apiWithDialog('sw/unregister', {
endpoint,
});
}, null);
pushSubscription.value = null;
}
}
@ -134,7 +133,7 @@ function encode(buffer: ArrayBuffer | null) {
* Convert the URL safe base64 string to a Uint8Array
* @param base64String base64 string
*/
function urlBase64ToUint8Array(base64String: string): Uint8Array {
function urlBase64ToUint8Array(base64String: string): BufferSource {
const padding = '='.repeat((4 - base64String.length % 4) % 4);
const base64 = (base64String + padding)
.replace(/-/g, '+')

View File

@ -6,7 +6,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<template>
<MkA :to="forModeration ? `/admin/roles/${role.id}` : `/roles/${role.id}`" :class="$style.root" tabindex="-1" :style="{ '--color': role.color }">
<template v-if="forModeration">
<i v-if="role.isPublic" class="ti ti-world" :class="$style.icon" style="color: var(--MI_THEME-success)"></i>
<i v-if="'isPublic' in role && role.isPublic" class="ti ti-world" :class="$style.icon" style="color: var(--MI_THEME-success)"></i>
<i v-else class="ti ti-lock" :class="$style.icon" style="color: var(--MI_THEME-warn)"></i>
</template>
@ -23,7 +23,7 @@ SPDX-License-Identifier: AGPL-3.0-only
</template>
</span>
<span :class="$style.bodyName">{{ role.name }}</span>
<template v-if="detailed">
<template v-if="detailed && 'target' in role && 'usersCount' in role">
<span v-if="role.target === 'manual'" :class="$style.bodyUsers">{{ role.usersCount }} users</span>
<span v-else-if="role.target === 'conditional'" :class="$style.bodyUsers">? users</span>
</template>
@ -39,7 +39,7 @@ import * as Misskey from 'misskey-js';
import { i18n } from '@/i18n.js';
const props = withDefaults(defineProps<{
role: Misskey.entities.Role;
role: Misskey.entities.Role | Misskey.entities.IResponse['roles'][number];
forModeration: boolean;
detailed?: boolean;
}>(), {

View File

@ -72,7 +72,7 @@ import { getStaticImageUrl } from '@/utility/media-proxy.js';
const props = defineProps<{
showing: boolean;
q: string;
q: string | Misskey.entities.UserDetailed;
source: HTMLElement;
}>();
@ -99,10 +99,11 @@ async function fetchUser() {
user.value = props.q;
error.value = false;
} else {
const query: Omit<Misskey.entities.UsersShowRequest, 'userIds'> = props.q.startsWith('@') ?
const query: Misskey.entities.UsersShowRequest = props.q.startsWith('@') ?
Misskey.acct.parse(props.q.substring(1)) :
{ userId: props.q };
// @ts-expect-error payload
misskeyApi('users/show', query).then(res => {
if (!props.showing) return;
user.value = res;

View File

@ -4,31 +4,39 @@ SPDX-License-Identifier: AGPL-3.0-only
-->
<template>
<div :class="[$style.root, { [$style.inline]: inline }]">
<a v-if="external" :class="$style.main" class="_button" :href="to" target="_blank">
<component
:is="to ? 'div' : 'button'"
:class="[
$style.root,
{
[$style.inline]: inline,
'_button': !to,
},
]"
>
<component
:is="to ? (external ? 'a' : 'MkA') : 'div'"
:class="[$style.main, { [$style.active]: active }]"
class="_button"
v-bind="to ? (external ? { href: to, target: '_blank' } : { to, behavior }) : {}"
>
<span :class="$style.icon"><slot name="icon"></slot></span>
<span :class="$style.text"><slot></slot></span>
<div :class="$style.headerText">
<div>
<MkCondensedLine :minScale="2 / 3"><slot></slot></MkCondensedLine>
</div>
</div>
<span :class="$style.suffix">
<span :class="$style.suffixText"><slot name="suffix"></slot></span>
<i class="ti ti-external-link"></i>
<i :class="to && external ? 'ti ti-external-link' : 'ti ti-chevron-right'"></i>
</span>
</a>
<MkA v-else :class="[$style.main, { [$style.active]: active }]" class="_button" :to="to" :behavior="behavior">
<span :class="$style.icon"><slot name="icon"></slot></span>
<span :class="$style.text"><slot></slot></span>
<span :class="$style.suffix">
<span :class="$style.suffixText"><slot name="suffix"></slot></span>
<i class="ti ti-chevron-right"></i>
</span>
</MkA>
</div>
</component>
</component>
</template>
<script lang="ts" setup>
import { } from 'vue';
const props = defineProps<{
to: string;
defineProps<{
to?: string;
active?: boolean;
external?: boolean;
behavior?: null | 'window' | 'browser';
@ -75,17 +83,18 @@ const props = defineProps<{
&:empty {
display: none;
& + .text {
& + .headerText {
padding-left: 4px;
}
}
}
.text {
flex-shrink: 1;
white-space: normal;
.headerText {
white-space: nowrap;
text-overflow: ellipsis;
text-align: start;
overflow: hidden;
padding-right: 12px;
text-align: center;
}
.suffix {

View File

@ -4,7 +4,7 @@ SPDX-License-Identifier: AGPL-3.0-only
-->
<template>
<a ref="el" :href="to" :class="active ? activeClass : null" @click.prevent="nav" @contextmenu.prevent.stop="onContextmenu">
<a ref="el" :href="to" :class="active ? activeClass : null" @click="nav" @contextmenu.prevent.stop="onContextmenu">
<slot></slot>
</a>
</template>
@ -86,6 +86,11 @@ function openWindow() {
}
function nav(ev: MouseEvent) {
// shift
if (ev.metaKey || ev.altKey || ev.ctrlKey) return;
ev.preventDefault();
if (behavior === 'browser') {
window.location.href = props.to;
return;

View File

@ -1,4 +1,3 @@
@ -1,70 +0,0 @@
<!--
SPDX-FileCopyrightText: syuilo and misskey-project
SPDX-License-Identifier: AGPL-3.0-only

View File

@ -14,9 +14,10 @@ SPDX-License-Identifier: AGPL-3.0-only
<script lang="ts" setup>
import isChromatic from 'chromatic/isChromatic';
import { onMounted, onUnmounted, ref, computed } from 'vue';
import { computed } from 'vue';
import { i18n } from '@/i18n.js';
import { dateTimeFormat } from '@@/js/intl-const.js';
import { useLowresTime } from '@/composables/use-lowres-time.js';
const props = withDefaults(defineProps<{
time: Date | string | number | null;
@ -46,8 +47,10 @@ const _time = props.time == null ? NaN : getDateSafe(props.time).getTime();
const invalid = Number.isNaN(_time);
const absolute = !invalid ? dateTimeFormat.format(_time) : i18n.ts._ago.invalid;
const actualNow = useLowresTime();
const now = computed(() => (props.origin ? props.origin.getTime() : actualNow.value));
// eslint-disable-next-line vue/no-setup-props-reactivity-loss
const now = ref(props.origin?.getTime() ?? Date.now());
const ago = computed(() => (now.value - _time) / 1000/*ms*/);
const relative = computed<string>(() => {
@ -72,29 +75,6 @@ const relative = computed<string>(() => {
i18n.tsx._timeIn.seconds({ n: (~~(-ago.value % 60)).toString() })
);
});
let tickId: number;
let currentInterval: number;
function tick() {
now.value = Date.now();
const nextInterval = ago.value < 60 ? 10000 : ago.value < 3600 ? 60000 : 180000;
if (currentInterval !== nextInterval) {
if (tickId) window.clearInterval(tickId);
currentInterval = nextInterval;
tickId = window.setInterval(tick, nextInterval);
}
}
if (!invalid && props.origin === null && (props.mode === 'relative' || props.mode === 'detail')) {
onMounted(() => {
tick();
});
onUnmounted(() => {
if (tickId) window.clearInterval(tickId);
});
}
</script>
<style lang="scss" module>

View File

@ -11,7 +11,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<MkPageHeader v-else v-model:tab="tab" v-bind="pageHeaderProps"/>
</template>
<div :class="$style.body">
<MkSwiper v-if="prefer.s.enableHorizontalSwipe && swipable && (props.tabs?.length ?? 1) > 1" v-model:tab="tab" :class="$style.swiper" :tabs="props.tabs">
<MkSwiper v-if="prefer.s.enableHorizontalSwipe && swipable && (props.tabs?.length ?? 1) > 1" v-model:tab="tab" :class="$style.swiper" :tabs="props.tabs ?? []">
<slot></slot>
</MkSwiper>
<slot v-else></slot>
@ -45,7 +45,7 @@ const props = withDefaults(defineProps<PageHeaderProps & {
});
const pageHeaderProps = computed(() => {
const { reversed, ...rest } = props;
const { reversed, tab, ...rest } = props;
return rest;
});
@ -75,10 +75,6 @@ defineExpose({
</script>
<style lang="scss" module>
.root {
}
.body, .swiper {
min-height: calc(100cqh - (var(--MI-stickyTop, 0px) + var(--MI-stickyBottom, 0px)));
}

View File

@ -65,5 +65,12 @@ router.useListener('change', ({ resolved }) => {
.root {
height: 100%;
background-color: var(--MI_THEME-bg);
/**
* FIXME: Safari 26 contain: layout を指定するとバグるのでhotfixとして _pageContainer content: strict を上書き
* https://github.com/misskey-dev/misskey/issues/16204#issuecomment-3265404776
* https://bugs.webkit.org/show_bug.cgi?id=297186
*/
contain: size style paint !important;
}
</style>

View File

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

View File

@ -24,7 +24,7 @@ export const globalEvents = new EventEmitter<Events>();
export function useGlobalEvent<T extends keyof Events>(
event: T,
callback: Events[T],
callback: EventEmitter.EventListener<Events, T>,
): void {
globalEvents.on(event, callback);
onBeforeUnmount(() => {

View File

@ -94,7 +94,7 @@ export class Pizzax<T extends StateDef> {
private mergeState<X>(value: X, def: X): X {
if (this.isPureObject(value) && this.isPureObject(def)) {
const merged = deepMerge(value, def);
const merged = deepMerge<Record<PropertyKey, unknown>>(value, def);
if (_DEV_) console.log('Merging state. Incoming: ', value, ' Default: ', def, ' Result: ', merged);

View File

@ -36,9 +36,9 @@ import { focusParent } from '@/utility/focus.js';
export const openingWindowsCount = ref(0);
export type ApiWithDialogCustomErrors = Record<string, { title?: string; text: string; }>;
export const apiWithDialog = (<E extends keyof Misskey.Endpoints, P extends Misskey.Endpoints[E]['req'] = Misskey.Endpoints[E]['req']>(
export const apiWithDialog = (<E extends keyof Misskey.Endpoints>(
endpoint: E,
data: P,
data: Misskey.Endpoints[E]['req'],
token?: string | null | undefined,
customErrors?: ApiWithDialogCustomErrors,
) => {

View File

@ -11,12 +11,6 @@ SPDX-License-Identifier: AGPL-3.0-only
<MkInput v-model="q" class="" :placeholder="i18n.ts.search" autocapitalize="off">
<template #prefix><i class="ti ti-search"></i></template>
</MkInput>
<!-- たくさんあると邪魔
<div class="tags">
<span class="tag _button" v-for="tag in customEmojiTags" :class="{ active: selectedTags.has(tag) }" @click="toggleTag(tag)">{{ tag }}</span>
</div>
-->
</div>
<MkFoldableSection v-if="searchEmojis">
@ -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<Misskey.entities.EmojiSimple[] | null>(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 });
</script>
<style lang="scss" module>

View File

@ -231,6 +231,7 @@ import { ensureSignin, iAmAdmin, iAmModerator } from '@/i.js';
import MkRolePreview from '@/components/MkRolePreview.vue';
import MkPagination from '@/components/MkPagination.vue';
import { Paginator } from '@/utility/paginator.js';
import type { ChartSrc } from '@/components/MkChart.vue';
const $i = ensureSignin();

View File

@ -307,8 +307,8 @@ async function onFileSelectClicked() {
const driveFiles = await chooseFileFromPcAndUpload({
multiple: true,
folderId: selectedFolderId.value,
//
nameConverter: (file) => file.name.replace(/\.[a-zA-Z0-9]+$/, ''),
// //
// nameConverter: (file) => file.name.replace(/\.[a-zA-Z0-9]+$/, ''),
});
gridItems.value.push(...driveFiles.map(fromDriveFile));

View File

@ -26,10 +26,10 @@ const chartEl = useTemplateRef('chartEl');
const { handler: externalTooltipHandler } = useChartTooltip();
let chartInstance: Chart;
let chartInstance: Chart | null = null;
function setData(values) {
if (chartInstance == null) return;
if (chartInstance == null || chartInstance.data.labels == null) return;
for (const value of values) {
chartInstance.data.labels.push('');
chartInstance.data.datasets[0].data.push(value);
@ -42,7 +42,7 @@ function setData(values) {
}
function pushData(value) {
if (chartInstance == null) return;
if (chartInstance == null || chartInstance.data.labels == null) return;
chartInstance.data.labels.push('');
chartInstance.data.datasets[0].data.push(value);
if (chartInstance.data.datasets[0].data.length > 200) {
@ -69,6 +69,8 @@ const color =
onMounted(() => {
const vLineColor = store.s.darkMode ? 'rgba(255, 255, 255, 0.2)' : 'rgba(0, 0, 0, 0.2)';
if (chartEl.value == null) return;
chartInstance = new Chart(chartEl.value, {
type: 'line',
data: {

View File

@ -210,6 +210,7 @@ async function fetchCurrentQueue() {
}
async function fetchJobs() {
if (tab.value === '-') return;
jobsFetching.value = true;
const state = jobState.value;
jobs.value = await misskeyApi('admin/queue/jobs', {
@ -307,6 +308,7 @@ async function removeJobs() {
}
async function refreshJob(jobId: string) {
if (tab.value === '-') return;
const newJob = await misskeyApi('admin/queue/show-job', { queue: tab.value, jobId });
const index = jobs.value.findIndex((job) => job.id === jobId);
if (index !== -1) {

View File

@ -26,7 +26,7 @@ initChart();
const chartEl = useTemplateRef('chartEl');
const now = new Date();
let chartInstance: Chart = null;
let chartInstance: Chart | null = null;
const chartLimit = 7;
const fetching = ref(true);

View File

@ -23,9 +23,9 @@ SPDX-License-Identifier: AGPL-3.0-only
<div class="item _panel sub">
<div class="icon"><i class="ti ti-world-download"></i></div>
<div class="body">
<div class="value">
<div v-if="federationSubActive != null" class="value">
{{ number(federationSubActive) }}
<MkNumberDiff v-tooltip="i18n.ts.dayOverDayChanges" class="diff" :value="federationSubActiveDiff"></MkNumberDiff>
<MkNumberDiff v-if="federationSubActiveDiff != null" v-tooltip="i18n.ts.dayOverDayChanges" class="diff" :value="federationSubActiveDiff"></MkNumberDiff>
</div>
<div class="label">Sub</div>
</div>
@ -33,9 +33,9 @@ SPDX-License-Identifier: AGPL-3.0-only
<div class="item _panel pub">
<div class="icon"><i class="ti ti-world-upload"></i></div>
<div class="body">
<div class="value">
<div v-if="federationPubActive != null" class="value">
{{ number(federationPubActive) }}
<MkNumberDiff v-tooltip="i18n.ts.dayOverDayChanges" class="diff" :value="federationPubActiveDiff"></MkNumberDiff>
<MkNumberDiff v-if="federationPubActiveDiff != null" v-tooltip="i18n.ts.dayOverDayChanges" class="diff" :value="federationPubActiveDiff"></MkNumberDiff>
</div>
<div class="label">Pub</div>
</div>

View File

@ -12,7 +12,6 @@ SPDX-License-Identifier: AGPL-3.0-only
</template>
<script lang="ts" setup>
import { ref } from 'vue';
import MkHeatmap from '@/components/MkHeatmap.vue';
import MkSelect from '@/components/MkSelect.vue';
import { useMkSelect } from '@/composables/use-mkselect.js';

View File

@ -32,15 +32,17 @@ const { handler: externalTooltipHandler } = useChartTooltip({
position: 'middle',
});
let chartInstance: Chart;
let chartInstance: Chart | null = null;
onMounted(() => {
if (chartEl.value == null) return;
chartInstance = new Chart(chartEl.value, {
type: 'doughnut',
data: {
labels: props.data.map(x => x.name),
datasets: [{
backgroundColor: props.data.map(x => x.color),
backgroundColor: props.data.map(x => x.color ?? '#000'),
borderColor: getComputedStyle(window.document.documentElement).getPropertyValue('--MI_THEME-panel'),
borderWidth: 2,
hoverOffset: 0,
@ -57,9 +59,10 @@ onMounted(() => {
},
},
onClick: (ev) => {
const hit = chartInstance.getElementsAtEventForMode(ev, 'nearest', { intersect: true }, false)[0];
if (hit && props.data[hit.index].onClick) {
props.data[hit.index].onClick();
if (ev.native == null) return;
const hit = chartInstance!.getElementsAtEventForMode(ev.native, 'nearest', { intersect: true }, false)[0];
if (hit && props.data[hit.index].onClick != null) {
props.data[hit.index].onClick!();
}
},
plugins: {

View File

@ -26,10 +26,10 @@ const chartEl = useTemplateRef('chartEl');
const { handler: externalTooltipHandler } = useChartTooltip();
let chartInstance: Chart;
let chartInstance: Chart | null = null;
function setData(values) {
if (chartInstance == null) return;
function setData(values: number[]) {
if (chartInstance == null || chartInstance.data.labels == null) return;
for (const value of values) {
chartInstance.data.labels.push('');
chartInstance.data.datasets[0].data.push(value);
@ -41,8 +41,8 @@ function setData(values) {
chartInstance.update();
}
function pushData(value) {
if (chartInstance == null) return;
function pushData(value: number) {
if (chartInstance == null || chartInstance.data.labels == null) return;
chartInstance.data.labels.push('');
chartInstance.data.datasets[0].data.push(value);
if (chartInstance.data.datasets[0].data.length > 100) {
@ -67,6 +67,8 @@ const color =
'?' as never;
onMounted(() => {
if (chartEl.value == null) return;
const vLineColor = store.s.darkMode ? 'rgba(255, 255, 255, 0.2)' : 'rgba(0, 0, 0, 0.2)';
chartInstance = new Chart(chartEl.value, {

View File

@ -38,7 +38,7 @@ SPDX-License-Identifier: AGPL-3.0-only
import { markRaw, onMounted, onUnmounted, ref, useTemplateRef } from 'vue';
import * as Misskey from 'misskey-js';
import XChart from './overview.queue.chart.vue';
import type { ApQueueDomain } from '@/pages/admin/queue.vue';
import type { ApQueueDomain } from '@/pages/admin/federation-job-queue.vue';
import number from '@/filters/number.js';
import { useStream } from '@/stream.js';
import { genId } from '@/utility/id.js';
@ -64,10 +64,10 @@ function onStats(stats: Misskey.entities.QueueStats) {
delayed.value = stats[props.domain].delayed;
waiting.value = stats[props.domain].waiting;
chartProcess.value.pushData(stats[props.domain].activeSincePrevTick);
chartActive.value.pushData(stats[props.domain].active);
chartDelayed.value.pushData(stats[props.domain].delayed);
chartWaiting.value.pushData(stats[props.domain].waiting);
chartProcess.value?.pushData(stats[props.domain].activeSincePrevTick);
chartActive.value?.pushData(stats[props.domain].active);
chartDelayed.value?.pushData(stats[props.domain].delayed);
chartWaiting.value?.pushData(stats[props.domain].waiting);
}
function onStatsLog(statsLog: Misskey.entities.QueueStatsLog) {
@ -83,10 +83,10 @@ function onStatsLog(statsLog: Misskey.entities.QueueStatsLog) {
dataWaiting.push(stats[props.domain].waiting);
}
chartProcess.value.setData(dataProcess);
chartActive.value.setData(dataActive);
chartDelayed.value.setData(dataDelayed);
chartWaiting.value.setData(dataWaiting);
chartProcess.value?.setData(dataProcess);
chartActive.value?.setData(dataActive);
chartDelayed.value?.setData(dataDelayed);
chartWaiting.value?.setData(dataWaiting);
}
onMounted(() => {

View File

@ -7,13 +7,13 @@ SPDX-License-Identifier: AGPL-3.0-only
<div>
<Transition :name="prefer.s.animation ? '_transition_zoom' : ''" mode="out-in">
<MkLoading v-if="fetching"/>
<div v-else :class="$style.root">
<div v-else-if="stats != null" :class="$style.root">
<div class="item _panel users">
<div class="icon"><i class="ti ti-users"></i></div>
<div class="body">
<div class="value">
<MkNumber :value="stats.originalUsersCount" style="margin-right: 0.5em;"/>
<MkNumberDiff v-tooltip="i18n.ts.dayOverDayChanges" class="diff" :value="usersComparedToThePrevDay"></MkNumberDiff>
<MkNumberDiff v-if="usersComparedToThePrevDay != null" v-tooltip="i18n.ts.dayOverDayChanges" class="diff" :value="usersComparedToThePrevDay"></MkNumberDiff>
</div>
<div class="label">Users</div>
</div>
@ -23,7 +23,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<div class="body">
<div class="value">
<MkNumber :value="stats.originalNotesCount" style="margin-right: 0.5em;"/>
<MkNumberDiff v-tooltip="i18n.ts.dayOverDayChanges" class="diff" :value="notesComparedToThePrevDay"></MkNumberDiff>
<MkNumberDiff v-if="notesComparedToThePrevDay != null" v-tooltip="i18n.ts.dayOverDayChanges" class="diff" :value="notesComparedToThePrevDay"></MkNumberDiff>
</div>
<div class="label">Notes</div>
</div>
@ -56,6 +56,7 @@ SPDX-License-Identifier: AGPL-3.0-only
</div>
</div>
</div>
<MkError v-else/>
</Transition>
</div>
</template>
@ -71,8 +72,8 @@ import { customEmojis } from '@/custom-emojis.js';
import { prefer } from '@/preferences.js';
const stats = ref<Misskey.entities.StatsResponse | null>(null);
const usersComparedToThePrevDay = ref<number>();
const notesComparedToThePrevDay = ref<number>();
const usersComparedToThePrevDay = ref<number | null>(null);
const notesComparedToThePrevDay = ref<number | null>(null);
const onlineUsersCount = ref(0);
const fetching = ref(true);
@ -85,11 +86,11 @@ onMounted(async () => {
onlineUsersCount.value = _onlineUsersCount;
misskeyApiGet('charts/users', { limit: 2, span: 'day' }).then(chart => {
usersComparedToThePrevDay.value = stats.value.originalUsersCount - chart.local.total[1];
usersComparedToThePrevDay.value = _stats.originalUsersCount - chart.local.total[1];
});
misskeyApiGet('charts/notes', { limit: 2, span: 'day' }).then(chart => {
notesComparedToThePrevDay.value = stats.value.originalNotesCount - chart.local.total[1];
notesComparedToThePrevDay.value = _stats.originalNotesCount - chart.local.total[1];
});
fetching.value = false;

View File

@ -95,7 +95,7 @@ const federationPubActiveDiff = ref<number | null>(null);
const federationSubActive = ref<number | null>(null);
const federationSubActiveDiff = ref<number | null>(null);
const newUsers = ref<Misskey.entities.UserDetailed[] | null>(null);
const activeInstances = shallowRef<Misskey.entities.FederationInstance | null>(null);
const activeInstances = shallowRef<Misskey.entities.FederationInstancesResponse | null>(null);
const queueStatsConnection = markRaw(useStream().useChannel('queueStats'));
const now = new Date();
const filesPagination = {

View File

@ -831,7 +831,6 @@ import { watch, ref, computed } from 'vue';
import { throttle } from 'throttle-debounce';
import * as Misskey from 'misskey-js';
import RolesEditorFormula from './RolesEditorFormula.vue';
import type { GetMkSelectValueTypesFromDef, MkSelectItem } from '@/components/MkSelect.vue';
import MkInput from '@/components/MkInput.vue';
import MkColorInput from '@/components/MkColorInput.vue';
import MkSelect from '@/components/MkSelect.vue';

View File

@ -71,7 +71,7 @@ import { Paginator } from '@/utility/paginator.js';
const router = useRouter();
const props = defineProps<{
id?: string;
id: string;
}>();
const usersPaginator = markRaw(new Paginator('admin/roles/users', {

View File

@ -350,6 +350,7 @@ import { definePage } from '@/page.js';
import { instance, fetchInstance } from '@/instance.js';
import MkFoldableSection from '@/components/MkFoldableSection.vue';
import { useRouter } from '@/router.js';
import { deepClone } from '@/utility/clone.js';
import MkTextarea from '@/components/MkTextarea.vue';
const router = useRouter();
@ -357,10 +358,7 @@ const baseRoleQ = ref('');
const roles = await misskeyApi('admin/roles/list');
const policies = reactive<Record<typeof Misskey.rolePolicies[number], any>>({});
for (const ROLE_POLICY of Misskey.rolePolicies) {
policies[ROLE_POLICY] = instance.policies[ROLE_POLICY];
}
const policies = reactive(deepClone(instance.policies));
const avatarDecorationLimit = computed({
get: () => Math.min(16, Math.max(0, policies.avatarDecorationLimit)),
@ -380,6 +378,7 @@ function matchQuery(keywords: string[]): boolean {
async function updateBaseRole() {
await os.apiWithDialog('admin/roles/update-default-policies', {
//@ts-expect-error misskey-js
policies,
});
fetchInstance(true);

View File

@ -16,7 +16,7 @@ SPDX-License-Identifier: AGPL-3.0-only
</div>
<div class="body">
<div class="title">{{ post.title }}</div>
<div class="description"><Mfm :text="post.description"/></div>
<div class="description"><Mfm v-if="post.description != null" :text="post.description"/></div>
<div class="info">
<i class="ti ti-clock"></i> <MkTime :time="post.createdAt" mode="detail"/>
</div>
@ -93,7 +93,7 @@ const error = ref<any>(null);
const otherPostsPaginator = markRaw(new Paginator('users/gallery/posts', {
limit: 6,
computedParams: computed(() => ({
userId: post.value.user.id,
userId: post.value!.user.id,
})),
}));
@ -109,33 +109,38 @@ function fetchPost() {
}
function copyLink() {
if (!post.value) return;
copyToClipboard(`${url}/gallery/${post.value.id}`);
}
function share() {
if (!post.value) return;
navigator.share({
title: post.value.title,
text: post.value.description,
text: post.value.description ?? undefined,
url: `${url}/gallery/${post.value.id}`,
});
}
function shareWithNote() {
if (!post.value) return;
os.post({
initialText: `${post.value.title} ${url}/gallery/${post.value.id}`,
});
}
function like() {
if (!post.value) return;
os.apiWithDialog('gallery/posts/like', {
postId: props.postId,
}).then(() => {
post.value.isLiked = true;
post.value.likedCount++;
post.value!.isLiked = true;
post.value!.likedCount++;
});
}
async function unlike() {
if (!post.value) return;
const confirm = await os.confirm({
type: 'warning',
text: i18n.ts.unlikeConfirm,
@ -144,8 +149,8 @@ async function unlike() {
os.apiWithDialog('gallery/posts/unlike', {
postId: props.postId,
}).then(() => {
post.value.isLiked = false;
post.value.likedCount--;
post.value!.isLiked = false;
post.value!.likedCount--;
});
}

View File

@ -19,7 +19,7 @@ SPDX-License-Identifier: AGPL-3.0-only
</div>
</div>
</div>
<MkButton v-if="list.isLiked" v-tooltip="i18n.ts.unlike" inline :class="$style.button" asLike primary @click="unlike()"><i class="ti ti-heart-off"></i><span v-if="list.likedCount > 0" class="count">{{ list.likedCount }}</span></MkButton>
<MkButton v-if="list.isLiked" v-tooltip="i18n.ts.unlike" inline :class="$style.button" asLike primary @click="unlike()"><i class="ti ti-heart-off"></i><span v-if="list.likedCount != null && list.likedCount > 0" class="count">{{ list.likedCount }}</span></MkButton>
<MkButton v-if="!list.isLiked" v-tooltip="i18n.ts.like" inline :class="$style.button" asLike @click="like()"><i class="ti ti-heart"></i><span v-if="1 > 0" class="count">{{ list.likedCount }}</span></MkButton>
<MkButton inline @click="create()"><i class="ti ti-download" :class="$style.import"></i>{{ i18n.ts.import }}</MkButton>
</div>
@ -41,7 +41,7 @@ const props = defineProps<{
listId: string;
}>();
const list = ref<Misskey.entities.UserList | null>(null);
const list = ref<Misskey.entities.UsersListsShowResponse | null>(null);
const error = ref<unknown | null>(null);
const users = ref<Misskey.entities.UserDetailed[]>([]);
@ -51,8 +51,9 @@ function fetchList(): void {
forPublic: true,
}).then(_list => {
list.value = _list;
if (_list.userIds == null || _list.userIds.length === 0) return;
misskeyApi('users/show', {
userIds: list.value.userIds,
userIds: _list.userIds,
}).then(_users => {
users.value = _users;
});
@ -68,7 +69,7 @@ function like() {
}).then(() => {
if (list.value == null) return;
list.value.isLiked = true;
list.value.likedCount++;
list.value.likedCount = (list.value.likedCount != null ? list.value.likedCount + 1 : 1);
});
}
@ -79,7 +80,7 @@ function unlike() {
}).then(() => {
if (list.value == null) return;
list.value.isLiked = false;
list.value.likedCount--;
list.value.likedCount = (list.value.likedCount != null ? Math.max(0, list.value.likedCount - 1) : 0);
});
}
@ -88,7 +89,7 @@ async function create() {
const { canceled, result: name } = await os.inputText({
title: i18n.ts.enterListName,
});
if (canceled) return;
if (canceled || name == null) return;
await os.apiWithDialog('users/lists/create-from-public', { name: name, listId: list.value.id });
}

View File

@ -39,6 +39,7 @@ const props = defineProps<{
const emit = defineEmits<{
(ev: 'update:modelValue', value: Misskey.entities.PageBlock & { type: 'note' }): void;
(ev: 'remove'): void;
}>();
const id = ref(props.modelValue.note);

View File

@ -27,6 +27,7 @@ const props = defineProps<{
const emit = defineEmits<{
(ev: 'update:modelValue', value: Misskey.entities.PageBlock & { type: 'text' }): void;
(ev: 'remove'): void;
}>();
let autocomplete: Autocomplete;
@ -42,6 +43,7 @@ watch(text, () => {
});
onMounted(() => {
if (inputEl.value == null) return;
autocomplete = new Autocomplete(inputEl.value, text);
});

View File

@ -7,7 +7,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<PageWithHeader v-model:tab="tab" :actions="headerActions" :tabs="headerTabs">
<div class="_spacer" style="--MI_SPACER-w: 700px;">
<div class="jqqmcavi">
<MkButton v-if="pageId" class="button" inline link :to="`/@${ author.username }/pages/${ currentName }`"><i class="ti ti-external-link"></i> {{ i18n.ts._pages.viewPage }}</MkButton>
<MkButton v-if="pageId && author != null" class="button" inline link :to="`/@${ author.username }/pages/${ currentName }`"><i class="ti ti-external-link"></i> {{ i18n.ts._pages.viewPage }}</MkButton>
<MkButton v-if="!readonly" inline primary class="button" @click="save"><i class="ti ti-device-floppy"></i> {{ i18n.ts.save }}</MkButton>
<MkButton v-if="pageId" inline class="button" @click="duplicate"><i class="ti ti-copy"></i> {{ i18n.ts.duplicate }}</MkButton>
<MkButton v-if="pageId && !readonly" inline class="button" danger @click="del"><i class="ti ti-trash"></i> {{ i18n.ts.delete }}</MkButton>
@ -24,7 +24,7 @@ SPDX-License-Identifier: AGPL-3.0-only
</MkInput>
<MkInput v-model="name">
<template #prefix>{{ url }}/@{{ author.username }}/pages/</template>
<template #prefix>{{ url }}/@{{ author?.username ?? '???' }}/pages/</template>
<template #label>{{ i18n.ts._pages.url }}</template>
</MkInput>
@ -84,7 +84,7 @@ const props = defineProps<{
}>();
const tab = ref('settings');
const author = ref($i);
const author = ref<Misskey.entities.User | null>($i);
const readonly = ref(false);
const page = ref<Misskey.entities.Page | null>(null);
const pageId = ref<string | null>(null);

View File

@ -164,7 +164,7 @@ const $i = ensureSignin();
const props = defineProps<{
game: Misskey.entities.ReversiGameDetailed;
connection?: Misskey.ChannelConnection<Misskey.Channels['reversiGame']> | null;
connection?: Misskey.IChannelConnection<Misskey.Channels['reversiGame']> | null;
}>();
const showBoardLabels = ref<boolean>(false);

View File

@ -132,7 +132,7 @@ const mapCategories = Array.from(new Set(Object.values(Reversi.maps).map(x => x.
const props = defineProps<{
game: Misskey.entities.ReversiGameDetailed;
connection: Misskey.ChannelConnection<Misskey.Channels['reversiGame']>;
connection: Misskey.IChannelConnection<Misskey.Channels['reversiGame']>;
}>();
const shareWhenStart = defineModel<boolean>('shareWhenStart', { default: false });

View File

@ -33,7 +33,7 @@ const props = defineProps<{
}>();
const game = shallowRef<Misskey.entities.ReversiGameDetailed | null>(null);
const connection = shallowRef<Misskey.ChannelConnection | null>(null);
const connection = shallowRef<Misskey.IChannelConnection<Misskey.Channels['reversiGame']> | null>(null);
const shareWhenStart = ref(false);
watch(() => props.gameId, () => {

View File

@ -196,6 +196,7 @@ async function addSecurityKey() {
if (auth.canceled) return;
const registrationOptions = parseCreationOptionsFromJSON({
// @ts-expect-error misskey-js
publicKey: await os.apiWithDialog('i/2fa/register-key', {
password: auth.result.password,
token: auth.result.token,
@ -226,6 +227,7 @@ async function addSecurityKey() {
password: auth.result.password,
token: auth.result.token,
name: name.result,
// @ts-expect-error misskey-js
credential: credential.toJSON(),
});
}

View File

@ -131,6 +131,10 @@ SPDX-License-Identifier: AGPL-3.0-only
<hr>
<MkButton @click="forceCloudBackup">Force cloud backup</MkButton>
<hr>
<template v-if="$i.policies.chatAvailability !== 'unavailable'">
<MkButton @click="readAllChatMessages">Read all chat messages</MkButton>
@ -167,6 +171,7 @@ import { signout } from '@/signout.js';
import { migrateOldSettings } from '@/pref-migrate.js';
import { hideAllTips as _hideAllTips, resetAllTips as _resetAllTips } from '@/tips.js';
import { suggestReload } from '@/utility/reload-suggest.js';
import { cloudBackup } from '@/preferences/utility.js';
const $i = ensureSignin();
@ -224,6 +229,11 @@ function readAllChatMessages() {
os.apiWithDialog('chat/read-all', {});
}
async function forceCloudBackup() {
await cloudBackup();
os.success();
}
const headerActions = computed(() => []);
const headerTabs = computed(() => []);

View File

@ -41,12 +41,10 @@ import { useMkSelect } from '@/composables/use-mkselect.js';
import { misskeyApi } from '@/utility/misskey-api.js';
import { playMisskeySfxFile, soundsTypes, getSoundDuration } from '@/utility/sound.js';
import { selectFile } from '@/utility/drive.js';
import type { SoundStore } from '@/preferences/def.js';
const props = defineProps<{
type: SoundType;
fileId?: string;
fileUrl?: string;
volume: number;
def: SoundStore;
}>();
const emit = defineEmits<{
@ -61,14 +59,14 @@ const {
label: getSoundTypeName(x),
value: x,
})),
initialValue: props.type,
initialValue: props.def.type,
});
const fileId = ref(props.fileId);
const fileUrl = ref(props.fileUrl);
const fileId = ref('fileId' in props.def ? props.def.fileId : undefined);
const fileUrl = ref('fileUrl' in props.def ? props.def.fileUrl : undefined);
const fileName = ref<string>('');
const driveFileError = ref(false);
const hasChanged = ref(false);
const volume = ref(props.volume);
const volume = ref(props.def.volume);
if (type.value === '_driveFile_' && fileId.value) {
await misskeyApi('drive/files/show', {

View File

@ -42,7 +42,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<template #suffix>{{ getSoundTypeName(sounds[type].type) }}</template>
<Suspense>
<template #default>
<XSound :type="sounds[type].type" :volume="sounds[type].volume" :fileId="sounds[type].fileId" :fileUrl="sounds[type].fileUrl" @update="(res) => updated(type, res)"/>
<XSound :def="sounds[type]" @update="(res) => updated(type, res)"/>
</template>
<template #fallback>
<MkLoading/>

View File

@ -112,8 +112,7 @@ async function init() {
...(visibleUserIds ? visibleUserIds.split(',').map(userId => ({ userId })) : []),
...(visibleAccts ? visibleAccts.split(',').map(Misskey.acct.parse) : []),
]
// TypeScript
.map(q => 'username' in q ? { username: q.username, host: q.host === null ? undefined : q.host } : q)
// @ts-expect-error payload
.map(q => misskeyApi('users/show', q)
.then(user => {
visibleUsers.value.push(user);

View File

@ -10,7 +10,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<MkPagination v-slot="{items}" :paginator="paginator" withControl>
<MkA v-for="list in items" :key="list.id" class="_panel" :class="$style.list" :to="`/list/${ list.id }`">
<div>{{ list.name }}</div>
<MkAvatars :userIds="list.userIds"/>
<MkAvatars v-if="list.userIds != null" :userIds="list.userIds"/>
</MkA>
</MkPagination>
</div>

View File

@ -7,7 +7,7 @@ import { ref } from 'vue';
import { compareVersions } from 'compare-versions';
import { isSafeMode } from '@@/js/config.js';
import * as Misskey from 'misskey-js';
import type { Parser, Interpreter, values } from '@syuilo/aiscript';
import type { Parser, Interpreter, values, utils as utils_TypeReferenceOnly } from '@syuilo/aiscript';
import type { FormWithDefault } from '@/utility/form.js';
import { genId } from '@/utility/id.js';
import { store } from '@/store.js';
@ -82,22 +82,23 @@ export async function parsePluginMeta(code: string): Promise<AiScriptPluginMeta>
}
const metadata = meta.get(null);
if (metadata == null) {
throw new Error('Metadata not found');
if (metadata == null || typeof metadata !== 'object' || Array.isArray(metadata)) {
throw new Error('Metadata not found or invalid');
}
const { name, version, author, description, permissions, config } = metadata;
if (name == null || version == null || author == null) {
throw new Error('Required property not found');
}
return {
name,
version,
author,
description,
permissions,
config,
name: name as string,
version: version as string,
author: author as string,
description: description as string | undefined,
permissions: permissions as string[] | undefined,
config: config as Record<string, any> | undefined,
};
}
@ -110,7 +111,7 @@ export async function authorizePlugin(plugin: Plugin) {
title: i18n.ts.tokenRequested,
information: i18n.ts.pluginTokenRequestedDescription,
initialName: plugin.name,
initialPermissions: plugin.permissions,
initialPermissions: plugin.permissions as typeof Misskey.permissions[number][],
}, {
done: async result => {
const { name, permissions } = result;
@ -149,6 +150,7 @@ export async function installPlugin(code: string, meta?: AiScriptPluginMeta) {
const plugin = {
...realMeta,
config: realMeta.config ?? {},
installId,
active: true,
configData: {},
@ -205,7 +207,7 @@ type HandlerDef = {
handler: (note: Misskey.entities.Note) => void;
};
note_view_interruptor: {
handler: (note: Misskey.entities.Note) => Misskey.entities.Note;
handler: (note: Misskey.entities.Note) => Misskey.entities.Note | null;
};
note_post_interruptor: {
handler: (note: FIXME) => unknown;
@ -353,7 +355,9 @@ export function changePluginActive(plugin: Plugin, active: boolean) {
async function createPluginEnv(opts: { plugin: Plugin; storageKey: string }): Promise<Record<string, values.Value>> {
const id = opts.plugin.installId;
const { utils, values } = await import('@syuilo/aiscript');
const ais = await import('@syuilo/aiscript');
const values = ais.values;
const utils: typeof utils_TypeReferenceOnly = ais.utils;
const { createAiScriptEnv } = await import('@/aiscript/api.js');
const config = new Map<string, values.Value>();
@ -375,7 +379,7 @@ async function createPluginEnv(opts: { plugin: Plugin; storageKey: string }): Pr
utils.assertFunction(handler);
addPluginHandler(id, 'post_form_action', {
title: title.value,
handler: withContext(ctx => (form, update) => {
handler: (form, update) => withContext(ctx => {
ctx.execFn(handler, [utils.jsToVal(form), values.FN_NATIVE(([key, value]) => {
if (!key || !value) {
return;
@ -391,7 +395,7 @@ async function createPluginEnv(opts: { plugin: Plugin; storageKey: string }): Pr
utils.assertFunction(handler);
addPluginHandler(id, 'user_action', {
title: title.value,
handler: withContext(ctx => (user) => {
handler: (user) => withContext(ctx => {
ctx.execFn(handler, [utils.jsToVal(user)]);
}),
});
@ -402,7 +406,7 @@ async function createPluginEnv(opts: { plugin: Plugin; storageKey: string }): Pr
utils.assertFunction(handler);
addPluginHandler(id, 'note_action', {
title: title.value,
handler: withContext(ctx => (note) => {
handler: (note) => withContext(ctx => {
ctx.execFn(handler, [utils.jsToVal(note)]);
}),
});
@ -411,8 +415,8 @@ async function createPluginEnv(opts: { plugin: Plugin; storageKey: string }): Pr
'Plugin:register:note_view_interruptor': values.FN_NATIVE(([handler]) => {
utils.assertFunction(handler);
addPluginHandler(id, 'note_view_interruptor', {
handler: withContext(ctx => (note) => {
return utils.valToJs(ctx.execFnSync(handler, [utils.jsToVal(note)]));
handler: (note) => withContext(ctx => {
return utils.valToJs(ctx.execFnSync(handler, [utils.jsToVal(note)])) as Misskey.entities.Note | null;
}),
});
}),
@ -420,8 +424,8 @@ async function createPluginEnv(opts: { plugin: Plugin; storageKey: string }): Pr
'Plugin:register:note_post_interruptor': values.FN_NATIVE(([handler]) => {
utils.assertFunction(handler);
addPluginHandler(id, 'note_post_interruptor', {
handler: withContext(ctx => async (note) => {
return utils.valToJs(await ctx.execFn(handler, [utils.jsToVal(note)]));
handler: (note) => withContext(ctx => {
return utils.valToJs(ctx.execFnSync(handler, [utils.jsToVal(note)]));
}),
});
}),
@ -429,8 +433,8 @@ async function createPluginEnv(opts: { plugin: Plugin; storageKey: string }): Pr
'Plugin:register:page_view_interruptor': values.FN_NATIVE(([handler]) => {
utils.assertFunction(handler);
addPluginHandler(id, 'page_view_interruptor', {
handler: withContext(ctx => async (page) => {
return utils.valToJs(await ctx.execFn(handler, [utils.jsToVal(page)]));
handler: (page) => withContext(ctx => {
return utils.valToJs(ctx.execFnSync(handler, [utils.jsToVal(page)])) as Misskey.entities.Page;
}),
});
}),

View File

@ -25,11 +25,14 @@ export function migrateOldSettings() {
});
const plugins = ColdDeviceStorage.get('plugins');
prefer.commit('plugins', plugins.map(p => ({
...p,
installId: (p as any).id,
id: undefined,
})));
prefer.commit('plugins', plugins.map(p => {
const { id, ...rest } = p;
return {
...rest,
config: rest.config ?? {},
installId: id,
};
}));
prefer.commit('deck.profile', deckStore.s.profile);
misskeyApi('i/registry/keys', {
@ -115,7 +118,13 @@ export function migrateOldSettings() {
prefer.commit('enableCondensedLine', store.s.enableCondensedLine);
prefer.commit('keepScreenOn', store.s.keepScreenOn);
prefer.commit('useGroupedNotifications', store.s.useGroupedNotifications);
prefer.commit('dataSaver', store.s.dataSaver);
prefer.commit('dataSaver', {
...prefer.s.dataSaver,
media: store.s.dataSaver.media,
avatar: store.s.dataSaver.avatar,
urlPreviewThumbnail: store.s.dataSaver.urlPreview,
code: store.s.dataSaver.code,
});
prefer.commit('enableSeasonalScreenEffect', store.s.enableSeasonalScreenEffect);
prefer.commit('enableHorizontalSwipe', store.s.enableHorizontalSwipe);
prefer.commit('useNativeUiForVideoAudioPlayer', store.s.useNativeUIForVideoAudioPlayer);

View File

@ -41,6 +41,14 @@ export type StatusbarStore = {
props: Record<string, any>;
};
export type DataSaverStore = {
media: boolean;
avatar: boolean;
urlPreviewThumbnail: boolean;
disableUrlPreview: boolean;
code: boolean;
};
type OmitStrict<T, K extends keyof T> = T extends any ? Pick<T, Exclude<keyof T, K>> : never;
// NOTE: デフォルト値は他の設定の状態に依存してはならない(依存していた場合、ユーザーがその設定項目単体で「初期値にリセット」した場合不具合の原因になる)
@ -332,7 +340,7 @@ export const PREF_DEF = definePreferences({
urlPreviewThumbnail: false,
disableUrlPreview: false,
code: false,
} satisfies Record<string, boolean>,
} as DataSaverStore,
},
hemisphere: {
default: hemisphere as 'N' | 'S',

View File

@ -381,7 +381,7 @@ export const store = markRaw(new Pizzax('base', {
avatar: false,
urlPreview: false,
code: false,
} as Record<string, boolean>,
},
},
enableSeasonalScreenEffect: {
where: 'device',
@ -483,7 +483,7 @@ export class ColdDeviceStorage {
lightTheme, // TODO: 消す(preferに移行済みのため)
darkTheme, // TODO: 消す(preferに移行済みのため)
syncDeviceDarkMode: true, // TODO: 消す(preferに移行済みのため)
plugins: [] as Plugin[], // TODO: 消す(preferに移行済みのため)
plugins: [] as (Omit<Plugin, 'installId'> & { id: string })[], // TODO: 消す(preferに移行済みのため)
};
public static watchers: Watcher[] = [];

View File

@ -4,6 +4,7 @@
*/
import { unisonReload } from '@/utility/unison-reload.js';
import { misskeyApiGet } from '@/utility/misskey-api.js';
import * as os from '@/os.js';
import { miLocalStorage } from '@/local-storage.js';
import { fetchCustomEmojis } from '@/custom-emojis.js';
@ -16,6 +17,9 @@ export async function clearCache() {
miLocalStorage.removeItem('theme');
miLocalStorage.removeItem('emojis');
miLocalStorage.removeItem('lastEmojisFetchedAt');
await misskeyApiGet('clear-browser-cache', {}).catch(() => {
// ignore
});
await fetchInstance(true);
await fetchCustomEmojis(true);
unisonReload();

View File

@ -36,7 +36,7 @@ export async function getTheme(mode: 'light' | 'dark', getName = false): Promise
_res = deepClone(theme.codeHighlighter.overrides);
} else {
const base = await bundledThemesInfo.find(t => t.id === theme.codeHighlighter!.base)?.import() ?? darkPlus;
_res = deepMerge(theme.codeHighlighter.overrides ?? {}, 'default' in base ? base.default : base);
_res = deepMerge<ThemeRegistration>(theme.codeHighlighter.overrides ?? {}, 'default' in base ? base.default : base);
}
if (_res.name == null) {
_res.name = theme.id;

View File

@ -131,11 +131,11 @@ type GetItemType<Item extends FormItem> =
: Item extends RadioFormItem
? GetRadioItemType<Item>
: Item extends RangeFormItem
? NonNullableIfRequired<InferDefault<RangeFormItem, number>, Item>
? NonNullableIfRequired<InferDefault<Item, number>, Item>
: Item extends EnumFormItem
? GetEnumItemType<Item>
: Item extends ArrayFormItem
? NonNullableIfRequired<InferDefault<ArrayFormItem, unknown[]>, Item>
? NonNullableIfRequired<InferDefault<Item, unknown[]>, Item>
: Item extends ObjectFormItem
? NonNullableIfRequired<InferDefault<Item, Record<string, unknown>>, Item>
: Item extends DriveFileFormItem

View File

@ -289,7 +289,6 @@ export function getUserMenu(user: Misskey.entities.UserDetailed, router: Router
caseSensitive: antenna.caseSensitive,
withReplies: antenna.withReplies,
withFile: antenna.withFile,
notify: antenna.notify,
});
antennasCache.delete();
},

View File

@ -38,12 +38,12 @@ SPDX-License-Identifier: AGPL-3.0-only
</template>
<script lang="ts" setup>
import { ref } from 'vue';
import { ref, watch } from 'vue';
import { useWidgetPropsManager } from './widget.js';
import type { WidgetComponentEmits, WidgetComponentExpose, WidgetComponentProps } from './widget.js';
import type { FormWithDefault, GetFormResultType } from '@/utility/form.js';
import { i18n } from '@/i18n.js';
import { useInterval } from '@@/js/use-interval.js';
import { useLowresTime, TIME_UPDATE_INTERVAL } from '@/composables/use-lowres-time.js';
const name = 'calendar';
@ -65,6 +65,7 @@ const { widgetProps, configure } = useWidgetPropsManager(name,
emit,
);
const fNow = useLowresTime();
const year = ref(0);
const month = ref(0);
const day = ref(0);
@ -73,8 +74,14 @@ const yearP = ref(0);
const monthP = ref(0);
const dayP = ref(0);
const isHoliday = ref(false);
const tick = () => {
const now = new Date();
const nextDay = new Date();
nextDay.setHours(24, 0, 0, 0);
let nextDayMidnightTime = nextDay.getTime();
let nextDayTimer: number | null = null;
function update(time: number) {
const now = new Date(time);
const nd = now.getDate();
const nm = now.getMonth();
const ny = now.getFullYear();
@ -104,11 +111,28 @@ const tick = () => {
yearP.value = yearNumer / yearDenom * 100;
isHoliday.value = now.getDay() === 0 || now.getDay() === 6;
};
}
useInterval(tick, 1000, {
immediate: true,
afterMounted: false,
watch(fNow, (to) => {
update(to);
//
if (nextDayMidnightTime - to <= TIME_UPDATE_INTERVAL) {
if (nextDayTimer != null) {
window.clearTimeout(nextDayTimer);
nextDayTimer = null;
}
nextDayTimer = window.setTimeout(() => {
update(nextDayMidnightTime);
nextDayTimer = null;
}, nextDayMidnightTime - to);
}
}, { immediate: true });
watch(day, () => {
nextDay.setHours(24, 0, 0, 0);
nextDayMidnightTime = nextDay.getTime();
});
defineExpose<WidgetComponentExpose>({

View File

@ -35,7 +35,7 @@ describe('MkUrlPreview', () => {
});
const result = render(MkUrlPreview, {
props: { url: summary.url },
props: { url: summary.url! },
global: { directives, components },
});

View File

@ -11,16 +11,16 @@
"lint": "pnpm typecheck && pnpm eslint"
},
"devDependencies": {
"@types/node": "22.17.2",
"@types/node": "22.18.1",
"@types/wawoff2": "1.0.2",
"@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"
},
"dependencies": {
"@tabler/icons-webfont": "3.34.1",
"harfbuzzjs": "0.4.9",
"harfbuzzjs": "0.4.11",
"tiny-glob": "0.2.9",
"tsx": "4.20.4",
"tsx": "4.20.5",
"typescript": "5.9.2",
"wawoff2": "2.0.1"
},

View File

@ -24,9 +24,9 @@
"devDependencies": {
"@types/matter-js": "0.20.0",
"@types/seedrandom": "3.0.8",
"@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",
"nodemon": "3.1.10",
"execa": "9.6.0",
"typescript": "5.9.2",

View File

@ -818,6 +818,18 @@ export type Channels = {
};
receives: null;
};
reversi: {
params: null;
events: {
matched: (payload: {
game: ReversiGameDetailed;
}) => void;
invited: (payload: {
user: User;
}) => void;
};
receives: null;
};
reversiGame: {
params: {
gameId: string;
@ -1449,6 +1461,10 @@ export type Endpoints = Overwrite<Endpoints_2, {
}>;
res: AdminRolesCreateResponse;
};
'clear-browser-cache': {
req: EmptyRequest;
res: EmptyResponse;
};
}>;
// @public (undocumented)
@ -3834,8 +3850,8 @@ type VerifyEmailRequest = operations['verify-email']['requestBody']['content']['
//
// src/entities.ts:55:2 - (ae-forgotten-export) The symbol "ModerationLogPayloads" needs to be exported by the entry point index.d.ts
// src/streaming.ts:57:3 - (ae-forgotten-export) The symbol "ReconnectingWebSocket" needs to be exported by the entry point index.d.ts
// src/streaming.types.ts:218:4 - (ae-forgotten-export) The symbol "ReversiUpdateKey" needs to be exported by the entry point index.d.ts
// src/streaming.types.ts:228:4 - (ae-forgotten-export) The symbol "ReversiUpdateSettings" needs to be exported by the entry point index.d.ts
// src/streaming.types.ts:226:4 - (ae-forgotten-export) The symbol "ReversiUpdateKey" needs to be exported by the entry point index.d.ts
// src/streaming.types.ts:236:4 - (ae-forgotten-export) The symbol "ReversiUpdateSettings" needs to be exported by the entry point index.d.ts
// (No @packageDocumentation comment for this package)

View File

@ -8,9 +8,9 @@
},
"devDependencies": {
"@readme/openapi-parser": "5.0.1",
"@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",
"openapi-types": "12.1.3",
"openapi-typescript": "7.9.1",
"ts-case-convert": "2.1.0",

View File

@ -1,7 +1,7 @@
{
"type": "module",
"name": "misskey-js",
"version": "2025.9.0-alpha.1",
"version": "2025.9.0",
"description": "Misskey SDK for JavaScript",
"license": "MIT",
"main": "./built/index.js",
@ -36,9 +36,9 @@
},
"devDependencies": {
"@microsoft/api-extractor": "7.52.11",
"@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",
"@vitest/coverage-v8": "3.2.4",
"esbuild": "0.25.9",
"execa": "9.6.0",

View File

@ -1,6 +1,12 @@
import { Endpoints as Gen } from './autogen/endpoint.js';
import { UserDetailed } from './autogen/models.js';
import { AdminRolesCreateRequest, AdminRolesCreateResponse, UsersShowRequest } from './autogen/entities.js';
import {
AdminRolesCreateRequest,
AdminRolesCreateResponse,
EmptyRequest,
EmptyResponse,
UsersShowRequest,
} from './autogen/entities.js';
import {
PartialRolePolicyOverride,
SigninFlowRequest,
@ -106,6 +112,10 @@ export type Endpoints = Overwrite<
'admin/roles/create': {
req: Overwrite<AdminRolesCreateRequest, { policies: PartialRolePolicyOverride }>;
res: AdminRolesCreateResponse;
}
},
'clear-browser-cache': {
req: EmptyRequest;
res: EmptyResponse;
},
}
>;

View File

@ -6049,7 +6049,9 @@ export interface operations {
[name: string]: unknown;
};
content: {
'application/json': components['schemas']['MeDetailed'];
'application/json': components['schemas']['MeDetailed'] & {
token: string;
};
};
};
/** @description Client error */
@ -35333,7 +35335,10 @@ export interface operations {
[name: string]: unknown;
};
content: {
'application/json': components['schemas']['UserList'];
'application/json': components['schemas']['UserList'] & {
likedCount?: number;
isLiked?: boolean;
};
};
};
/** @description Client error */

View File

@ -206,6 +206,14 @@ export type Channels = {
};
receives: null;
};
reversi: {
params: null;
events: {
matched: (payload: { game: ReversiGameDetailed }) => void;
invited: (payload: { user: User }) => void;
};
receives: null;
};
reversiGame: {
params: {
gameId: string;

View File

@ -22,9 +22,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",
"execa": "9.6.0",
"nodemon": "3.1.10",
"typescript": "5.9.2",

View File

@ -14,7 +14,7 @@
"misskey-js": "workspace:*"
},
"devDependencies": {
"@typescript-eslint/parser": "8.40.0",
"@typescript-eslint/parser": "8.42.0",
"@typescript/lib-webworker": "npm:@types/serviceworker@0.0.74",
"eslint-plugin-import": "2.32.0",
"nodemon": "3.1.10",

File diff suppressed because it is too large Load Diff

View File

@ -37,6 +37,7 @@
'packages/frontend/**/package.json',
'packages/frontend-embed/**/package.json',
'packages/frontend-shared/**/package.json',
'packages/frontend-builder/**/package.json',
'packages/misskey-bubble-game/**/package.json',
'packages/misskey-reversi/**/package.json',
'packages/sw/**/package.json',

Some files were not shown because too many files have changed in this diff Show More