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 ### General
- -
### Client
- Enhance: 時刻計算のための基準値を一か所で管理するようにし、パフォーマンスを向上
### Server
-
## 2025.9.0
### Client ### Client
- Enhance: AiScriptAppウィジェットで構文エラーを検知してもダイアログではなくウィジェット内にエラーを表示するように - Enhance: AiScriptAppウィジェットで構文エラーを検知してもダイアログではなくウィジェット内にエラーを表示するように
- Enhance: /flushページでサイトキャッシュをクリアできるようになりました - Enhance: /flushページでサイトキャッシュをクリアできるようになりました
- Enhance: クリップ/リスト/アンテナ/ロール追加系メニュー項目において、表示件数を拡張 - Enhance: クリップ/リスト/アンテナ/ロール追加系メニュー項目において、表示件数を拡張
- Enhance: 「キャッシュを削除」ボタンでブラウザの内部キャッシュの削除も行えるように
- Enhance: CtrlキーCommandキーを押下しながらリンクをクリックすると新しいタブで開くように
- Fix: プッシュ通知を有効にできない問題を修正 - Fix: プッシュ通知を有効にできない問題を修正
- Fix: RSSティッカーウィジェットが正しく動作しない問題を修正 - Fix: RSSティッカーウィジェットが正しく動作しない問題を修正
- Fix: プロファイルを復元後アカウントの切り替えができない問題を修正 - Fix: プロファイルを復元後アカウントの切り替えができない問題を修正
- Fix: エラー画像が横に引き伸ばされてしまう問題に対応 - Fix: エラー画像が横に引き伸ばされてしまう問題に対応
### Server ### Server
- - Fix: webpなどの画像に対してセンシティブなメディアの検出が適用されていなかった問題を修正
## 2025.8.0 ## 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." 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: "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" 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" remoteNotesCleaningExpiryDaysForEachNotes: "Duració mínima de conservació de les notes"
inquiryUrl: "URL de consulta " 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ó." 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

@ -1215,6 +1215,7 @@ privacyPolicyUrl: "Ссылка на Политику Конфиденциаль
tosAndPrivacyPolicy: "Условия использования и политика конфиденциальности" tosAndPrivacyPolicy: "Условия использования и политика конфиденциальности"
avatarDecorations: "Украшения для аватара" avatarDecorations: "Украшения для аватара"
attach: "Прикрепить" attach: "Прикрепить"
detachAll: "Убрать всё"
angle: "Угол" angle: "Угол"
flip: "Переворот" flip: "Переворот"
showAvatarDecorations: "Показать украшения для аватара" showAvatarDecorations: "Показать украшения для аватара"
@ -1268,8 +1269,11 @@ availableRoles: "Доступные роли"
federationDisabled: "Федерация отключена для этого сервера. Вы не можете взаимодействовать с пользователями на других серверах." federationDisabled: "Федерация отключена для этого сервера. Вы не можете взаимодействовать с пользователями на других серверах."
draft: "Черновик" draft: "Черновик"
markAsSensitiveConfirm: "Отметить контент как чувствительный?" markAsSensitiveConfirm: "Отметить контент как чувствительный?"
preferences: "Основное"
resetToDefaultValue: "Сбросить настройки до стандартных" resetToDefaultValue: "Сбросить настройки до стандартных"
syncBetweenDevices: "Синхронизировать между устройствами"
postForm: "Форма отправки" postForm: "Форма отправки"
textCount: "Количество символов"
information: "Описание" information: "Описание"
inMinutes: "мин" inMinutes: "мин"
inDays: "сут" inDays: "сут"
@ -1281,6 +1285,11 @@ _chat:
send: "Отправить" send: "Отправить"
_settings: _settings:
webhook: "Вебхук" webhook: "Вебхук"
preferencesBanner: "Вы можете настроить общее поведение клиента по вашим предпочтениям"
timelineAndNote: "Лента и заметки"
_chat:
showSenderName: "Показывать имя отправителя"
sendOnEnter: "Использовать Enter для отправки"
_delivery: _delivery:
stop: "Заморожено" stop: "Заморожено"
_type: _type:
@ -1529,7 +1538,7 @@ _achievements:
description: "Нажато здесь" description: "Нажато здесь"
_justPlainLucky: _justPlainLucky:
title: "Чистая удача" title: "Чистая удача"
description: "Может достаться с вероятностью 0,01% каждые 10 секунд." description: "Может достаться с вероятностью 0,005% каждые 10 секунд."
_setNameToSyuilo: _setNameToSyuilo:
title: "Комплекс бога" title: "Комплекс бога"
description: "Установлено «syuilo» в качестве имени" description: "Установлено «syuilo» в качестве имени"
@ -1557,6 +1566,12 @@ _achievements:
title: "Brain Diver" title: "Brain Diver"
description: "Опубликована ссылка на песню «Brain Diver»" description: "Опубликована ссылка на песню «Brain Diver»"
flavor: "Мисски-Мисски Ла-Ту-Ма" flavor: "Мисски-Мисски Ла-Ту-Ма"
_bubbleGameExplodingHead:
title: "🤯"
description: "Самый большой объект в Bubble game"
_bubbleGameDoubleExplodingHead:
title: "Двойной🤯"
description: "Два самых больших объекта в Bubble game одновременно!"
_role: _role:
new: "Новая роль" new: "Новая роль"
edit: "Изменить роль" edit: "Изменить роль"

View File

@ -360,7 +360,7 @@ whenServerDisconnected: "Sunucu ile bağlantı kesildiğinde"
disconnectedFromServer: "Sunucu bağlantısı kesildi" disconnectedFromServer: "Sunucu bağlantısı kesildi"
reload: "Yenile" reload: "Yenile"
doNothing: "Yoksay" doNothing: "Yoksay"
reloadConfirm: "Zaman çizelgesini yenilemek ister misin?" reloadConfirm: "Panoyu yenilemek ister misin?"
watch: "İzle" watch: "İzle"
unwatch: "İzlemeyi bırak" unwatch: "İzlemeyi bırak"
accept: "Kabul et" 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." 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ı" serverLogs: "Sunucu log kayıtları"
deleteAll: "Tümünü sil" deleteAll: "Tümünü sil"
showFixedPostForm: "Gönderi formunu zaman çizelgesinin en üstünde görüntüle" showFixedPostForm: "Gönderi formunu pano üstünde görüntüle"
showFixedPostFormInChannel: "Gönderi formunu zaman çizelgesinin en üstünde görüntüle (Kanallar)" 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 zaman çizelgesine dahil et" withRepliesByDefaultForNewlyFollowed: "Yeni takip edilen kullanıcıların yanıtlarını varsayılan olarak panoya dahil et"
newNoteRecived: "Yeni Not'lar var" newNoteRecived: "Yeni Not'lar var"
newNote: "Yeni Not" newNote: "Yeni Not"
sounds: "Sesler" sounds: "Sesler"
@ -1059,7 +1059,7 @@ achievements: "Başarılar"
gotInvalidResponseError: "Geçersiz sunucu yanıtı" 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." 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." thisPostMayBeAnnoying: "Bu not başkalarını rahatsız edebilir."
thisPostMayBeAnnoyingHome: "Ana zaman çizelgesine gönder" thisPostMayBeAnnoyingHome: "Ana panoya gönder"
thisPostMayBeAnnoyingCancel: "İptal" thisPostMayBeAnnoyingCancel: "İptal"
thisPostMayBeAnnoyingIgnore: "Yine de gönder" thisPostMayBeAnnoyingIgnore: "Yine de gönder"
collapseRenotes: "Daha önce görüntülenen Renote'lari kısaltılmış olarak göster" 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" 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" 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" 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?" 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ı zaman tünelinde cidden göstermeyecek misin?" 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" externalServices: "Dış Hizmetler"
sourceCode: "Kaynak kodu" sourceCode: "Kaynak kodu"
sourceCodeIsNotYetProvided: "Kaynak kodu henüz mevcut değildir. Bu sorunu gidermek için yöneticiyle iletişime geçin." 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." description: "Burada, Misskey'i kullanmanın temellerini ve özelliklerini öğrenebilirsin."
_note: _note:
title: "Not nedir?" 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." 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." 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." 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: _reaction:
@ -1640,7 +1640,7 @@ _serverSettings:
shortNameDescription: "Resmi adın uzun olması durumunda görüntülenebilen, örneğin adının kısaltması." 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." 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" 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." 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: "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." 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." restartServerSetupWizardConfirm_text: "Bazı mevcut ayarlar sıfırlanacaktır."
entrancePageStyle: "Giriş sayfası stili" entrancePageStyle: "Giriş sayfası stili"
showTimelineForVisitor: "Panoyu göster" showTimelineForVisitor: "Panoyu göster"
showActivitiesForVisitor: "Aktiviteleri göster"
_userGeneratedContentsVisibilityForVisitor: _userGeneratedContentsVisibilityForVisitor:
all: "Her şey halka açıktır." all: "Her şey halka açıktır."
localOnly: "Yalnızca yerel içerik yayınlanır, uzak içerik gizli tutulur." localOnly: "Yalnızca yerel içerik yayınlanır, uzak içerik gizli tutulur."
@ -1876,7 +1877,7 @@ _achievements:
title: "Öz Referans" title: "Öz Referans"
description: "Kendi notunuzu alıntı yapın" description: "Kendi notunuzu alıntı yapın"
_htl20npm: _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?" description: "Ev zaman çizelgenizin hızı 20 npm'yi (dakika başına not sayısı) aşıyor mu?"
_viewInstanceChart: _viewInstanceChart:
title: "Analist" title: "Analist"
@ -1965,7 +1966,7 @@ _role:
asBadge: "Rozet olarak göster" 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." 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" 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" displayOrder: "Pozisyon"
descriptionOfDisplayOrder: "Sayı ne kadar yüksekse, UI pozisyonu da o kadar yüksek olur." descriptionOfDisplayOrder: "Sayı ne kadar yüksekse, UI pozisyonu da o kadar yüksek olur."
preserveAssignmentOnMoveAccount: "Geçiş sırasında rol atamalarını koruyun" preserveAssignmentOnMoveAccount: "Geçiş sırasında rol atamalarını koruyun"
@ -1979,7 +1980,7 @@ _role:
high: "Yüksek" high: "Yüksek"
_options: _options:
gtlAvailable: "Global Pano'yu görüntüleyebilir" 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" canPublicNote: "Halka açık notlar gönderebilir"
mentionMax: "Bir notta maksimum bahsetme sayısı" mentionMax: "Bir notta maksimum bahsetme sayısı"
canInvite: "Sunucu davet kodları oluşturabilir" canInvite: "Sunucu davet kodları oluşturabilir"
@ -2484,7 +2485,7 @@ _visibility:
public: "Halka açık" public: "Halka açık"
publicDescription: "Notunuz tüm kullanıcılar tarafından görülebilir olacaktır." publicDescription: "Notunuz tüm kullanıcılar tarafından görülebilir olacaktır."
home: "Pano" home: "Pano"
homeDescription: "Yalnızca ana zaman çizelgesine gönder" homeDescription: "Yalnızca ana panoya gönder"
followers: "Takipçiler" followers: "Takipçiler"
followersDescription: "Sadece takipçilerine görünür hale getir" followersDescription: "Sadece takipçilerine görünür hale getir"
specified: "Doğrudan" specified: "Doğrudan"
@ -2531,7 +2532,7 @@ _exportOrImport:
userLists: "Kullanıcı listeleri" userLists: "Kullanıcı listeleri"
excludeMutingUsers: "Sessize alınan kullanıcıları hariç tut" excludeMutingUsers: "Sessize alınan kullanıcıları hariç tut"
excludeInactiveUsers: "Etkin olmayan 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: _charts:
federation: "Federasyon" federation: "Federasyon"
apRequest: "Talepler" apRequest: "Talepler"
@ -2925,7 +2926,7 @@ _reversi:
freeMatch: "Ücretsiz Eşleştirme" freeMatch: "Ücretsiz Eşleştirme"
lookingForPlayer: "Rakip aranıyor..." lookingForPlayer: "Rakip aranıyor..."
gameCanceled: "Oyun iptal edildi." 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" iStartedAGame: "Oyun başladı! #MisskeyReversi"
opponentHasSettingsChanged: "Rakip ayarlarını değiştirmiş." opponentHasSettingsChanged: "Rakip ayarlarını değiştirmiş."
allowIrregularRules: "Düzensiz kurallar (tamamen ücretsiz)" allowIrregularRules: "Düzensiz kurallar (tamamen ücretsiz)"
@ -3153,7 +3154,7 @@ _clientPerformanceIssueTip:
_clip: _clip:
tip: "Klip, notları gruplandırmanıza olanak tanıyan bir özelliktir." tip: "Klip, notları gruplandırmanıza olanak tanıyan bir özelliktir."
_userLists: _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" watermark: "Filigran"
defaultPreset: "Varsayılan Ön Ayar" defaultPreset: "Varsayılan Ön Ayar"
_watermarkEditor: _watermarkEditor:

View File

@ -1,12 +1,12 @@
{ {
"name": "misskey", "name": "misskey",
"version": "2025.9.0-alpha.1", "version": "2025.9.0",
"codename": "nasubi", "codename": "nasubi",
"repository": { "repository": {
"type": "git", "type": "git",
"url": "https://github.com/misskey-dev/misskey.git" "url": "https://github.com/misskey-dev/misskey.git"
}, },
"packageManager": "pnpm@10.15.0", "packageManager": "pnpm@10.15.1",
"workspaces": [ "workspaces": [
"packages/frontend-shared", "packages/frontend-shared",
"packages/frontend", "packages/frontend",
@ -62,22 +62,22 @@
"js-yaml": "4.1.0", "js-yaml": "4.1.0",
"postcss": "8.5.6", "postcss": "8.5.6",
"tar": "7.4.3", "tar": "7.4.3",
"terser": "5.43.1", "terser": "5.44.0",
"typescript": "5.9.2" "typescript": "5.9.2"
}, },
"devDependencies": { "devDependencies": {
"@misskey-dev/eslint-plugin": "2.1.0", "@misskey-dev/eslint-plugin": "2.1.0",
"@types/js-yaml": "4.0.9", "@types/js-yaml": "4.0.9",
"@types/node": "22.17.2", "@types/node": "22.18.1",
"@typescript-eslint/eslint-plugin": "8.40.0", "@typescript-eslint/eslint-plugin": "8.42.0",
"@typescript-eslint/parser": "8.40.0", "@typescript-eslint/parser": "8.42.0",
"cross-env": "7.0.3", "cross-env": "7.0.3",
"cypress": "14.5.4", "cypress": "14.5.4",
"eslint": "9.34.0", "eslint": "9.35.0",
"globals": "16.3.0", "globals": "16.3.0",
"ncp": "2.0.0", "ncp": "2.0.0",
"pnpm": "10.15.0", "pnpm": "10.15.1",
"start-server-and-test": "2.0.13" "start-server-and-test": "2.1.0"
}, },
"optionalDependencies": { "optionalDependencies": {
"@tensorflow/tfjs-core": "4.22.0" "@tensorflow/tfjs-core": "4.22.0"

View File

@ -39,17 +39,17 @@
}, },
"optionalDependencies": { "optionalDependencies": {
"@swc/core-android-arm64": "1.3.11", "@swc/core-android-arm64": "1.3.11",
"@swc/core-darwin-arm64": "1.13.4", "@swc/core-darwin-arm64": "1.13.5",
"@swc/core-darwin-x64": "1.13.4", "@swc/core-darwin-x64": "1.13.5",
"@swc/core-freebsd-x64": "1.3.11", "@swc/core-freebsd-x64": "1.3.11",
"@swc/core-linux-arm-gnueabihf": "1.13.4", "@swc/core-linux-arm-gnueabihf": "1.13.5",
"@swc/core-linux-arm64-gnu": "1.13.4", "@swc/core-linux-arm64-gnu": "1.13.5",
"@swc/core-linux-arm64-musl": "1.13.4", "@swc/core-linux-arm64-musl": "1.13.5",
"@swc/core-linux-x64-gnu": "1.13.4", "@swc/core-linux-x64-gnu": "1.13.5",
"@swc/core-linux-x64-musl": "1.13.4", "@swc/core-linux-x64-musl": "1.13.5",
"@swc/core-win32-arm64-msvc": "1.13.4", "@swc/core-win32-arm64-msvc": "1.13.5",
"@swc/core-win32-ia32-msvc": "1.13.4", "@swc/core-win32-ia32-msvc": "1.13.5",
"@swc/core-win32-x64-msvc": "1.13.4", "@swc/core-win32-x64-msvc": "1.13.5",
"@tensorflow/tfjs": "4.22.0", "@tensorflow/tfjs": "4.22.0",
"@tensorflow/tfjs-node": "4.22.0", "@tensorflow/tfjs-node": "4.22.0",
"bufferutil": "4.0.9", "bufferutil": "4.0.9",
@ -69,20 +69,20 @@
"utf-8-validate": "6.0.5" "utf-8-validate": "6.0.5"
}, },
"dependencies": { "dependencies": {
"@aws-sdk/client-s3": "3.873.0", "@aws-sdk/client-s3": "3.883.0",
"@aws-sdk/lib-storage": "3.873.0", "@aws-sdk/lib-storage": "3.883.0",
"@discordapp/twemoji": "16.0.1", "@discordapp/twemoji": "16.0.1",
"@fastify/accepts": "5.0.2", "@fastify/accepts": "5.0.2",
"@fastify/cookie": "11.0.2", "@fastify/cookie": "11.0.2",
"@fastify/cors": "10.1.0", "@fastify/cors": "10.1.0",
"@fastify/express": "4.0.2", "@fastify/express": "4.0.2",
"@fastify/http-proxy": "10.0.2", "@fastify/http-proxy": "10.0.2",
"@fastify/multipart": "9.0.3", "@fastify/multipart": "9.2.1",
"@fastify/static": "8.2.0", "@fastify/static": "8.2.0",
"@fastify/view": "10.0.2", "@fastify/view": "10.0.2",
"@misskey-dev/sharp-read-bmp": "1.2.0", "@misskey-dev/sharp-read-bmp": "1.2.0",
"@misskey-dev/summaly": "5.2.3", "@misskey-dev/summaly": "5.2.3",
"@napi-rs/canvas": "0.1.77", "@napi-rs/canvas": "0.1.79",
"@nestjs/common": "11.1.6", "@nestjs/common": "11.1.6",
"@nestjs/core": "11.1.6", "@nestjs/core": "11.1.6",
"@nestjs/testing": "11.1.6", "@nestjs/testing": "11.1.6",
@ -93,7 +93,7 @@
"@sinonjs/fake-timers": "11.3.1", "@sinonjs/fake-timers": "11.3.1",
"@smithy/node-http-handler": "2.5.0", "@smithy/node-http-handler": "2.5.0",
"@swc/cli": "0.7.8", "@swc/cli": "0.7.8",
"@swc/core": "1.13.4", "@swc/core": "1.13.5",
"@twemoji/parser": "16.0.0", "@twemoji/parser": "16.0.0",
"@types/redis-info": "3.0.3", "@types/redis-info": "3.0.3",
"accepts": "1.3.8", "accepts": "1.3.8",
@ -103,7 +103,7 @@
"bcryptjs": "2.4.3", "bcryptjs": "2.4.3",
"blurhash": "2.0.5", "blurhash": "2.0.5",
"body-parser": "1.20.3", "body-parser": "1.20.3",
"bullmq": "5.58.1", "bullmq": "5.58.5",
"cacheable-lookup": "7.0.0", "cacheable-lookup": "7.0.0",
"cbor": "9.0.2", "cbor": "9.0.2",
"chalk": "5.6.0", "chalk": "5.6.0",
@ -114,13 +114,13 @@
"content-disposition": "0.5.4", "content-disposition": "0.5.4",
"date-fns": "2.30.0", "date-fns": "2.30.0",
"deep-email-validator": "0.1.21", "deep-email-validator": "0.1.21",
"fastify": "5.5.0", "fastify": "5.6.0",
"fastify-raw-body": "5.0.0", "fastify-raw-body": "5.0.0",
"feed": "4.2.2", "feed": "4.2.2",
"file-type": "19.6.0", "file-type": "19.6.0",
"fluent-ffmpeg": "2.1.3", "fluent-ffmpeg": "2.1.3",
"form-data": "4.0.4", "form-data": "4.0.4",
"got": "14.4.7", "got": "14.4.8",
"happy-dom": "16.8.1", "happy-dom": "16.8.1",
"hpagent": "1.2.0", "hpagent": "1.2.0",
"htmlescape": "1.1.1", "htmlescape": "1.1.1",
@ -141,7 +141,7 @@
"mime-types": "2.1.35", "mime-types": "2.1.35",
"misskey-js": "workspace:*", "misskey-js": "workspace:*",
"misskey-reversi": "workspace:*", "misskey-reversi": "workspace:*",
"ms": "3.0.0-canary.1", "ms": "3.0.0-canary.202508261828",
"nanoid": "5.1.5", "nanoid": "5.1.5",
"nested-property": "4.0.0", "nested-property": "4.0.0",
"node-fetch": "3.3.2", "node-fetch": "3.3.2",
@ -175,7 +175,7 @@
"slacc": "0.0.10", "slacc": "0.0.10",
"strict-event-emitter-types": "2.0.0", "strict-event-emitter-types": "2.0.0",
"stringz": "2.1.0", "stringz": "2.1.0",
"systeminformation": "5.27.7", "systeminformation": "5.27.8",
"tinycolor2": "1.6.0", "tinycolor2": "1.6.0",
"tmp": "0.2.5", "tmp": "0.2.5",
"tsc-alias": "1.8.16", "tsc-alias": "1.8.16",
@ -210,7 +210,7 @@
"@types/jsrsasign": "10.5.15", "@types/jsrsasign": "10.5.15",
"@types/mime-types": "2.1.4", "@types/mime-types": "2.1.4",
"@types/ms": "0.7.34", "@types/ms": "0.7.34",
"@types/node": "22.17.2", "@types/node": "22.18.1",
"@types/nodemailer": "6.4.19", "@types/nodemailer": "6.4.19",
"@types/oauth": "0.9.6", "@types/oauth": "0.9.6",
"@types/oauth2orize": "1.11.5", "@types/oauth2orize": "1.11.5",
@ -222,7 +222,7 @@
"@types/ratelimiter": "3.4.6", "@types/ratelimiter": "3.4.6",
"@types/rename": "1.0.7", "@types/rename": "1.0.7",
"@types/sanitize-html": "2.16.0", "@types/sanitize-html": "2.16.0",
"@types/semver": "7.7.0", "@types/semver": "7.7.1",
"@types/simple-oauth2": "5.0.7", "@types/simple-oauth2": "5.0.7",
"@types/sinonjs__fake-timers": "8.1.5", "@types/sinonjs__fake-timers": "8.1.5",
"@types/supertest": "6.0.3", "@types/supertest": "6.0.3",
@ -231,8 +231,8 @@
"@types/vary": "1.1.3", "@types/vary": "1.1.3",
"@types/web-push": "3.6.4", "@types/web-push": "3.6.4",
"@types/ws": "8.18.1", "@types/ws": "8.18.1",
"@typescript-eslint/eslint-plugin": "8.40.0", "@typescript-eslint/eslint-plugin": "8.42.0",
"@typescript-eslint/parser": "8.40.0", "@typescript-eslint/parser": "8.42.0",
"aws-sdk-client-mock": "4.1.0", "aws-sdk-client-mock": "4.1.0",
"cross-env": "7.0.3", "cross-env": "7.0.3",
"eslint-plugin-import": "2.32.0", "eslint-plugin-import": "2.32.0",

View File

@ -29,7 +29,7 @@ export class AiService {
} }
@bindThis @bindThis
public async detectSensitive(path: string): Promise<nsfw.PredictionType[] | null> { public async detectSensitive(source: string | Buffer): Promise<nsfw.PredictionType[] | null> {
try { try {
if (isSupportedCpu === undefined) { if (isSupportedCpu === undefined) {
isSupportedCpu = await this.computeIsSupportedCpu(); 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; const image = await tf.node.decodeImage(buffer, 3) as any;
try { try {
const predictions = await this.model.classify(image); 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 type Logger from '@/logger.js';
import { bindThis } from '@/decorators.js'; import { bindThis } from '@/decorators.js';
import type { PredictionType } from 'nsfwjs'; import type { PredictionType } from 'nsfwjs';
import { isMimeImage } from '@/misc/is-mime-image.js';
export type FileInfo = { export type FileInfo = {
size: number; size: number;
@ -204,16 +205,7 @@ export class FileInfoService {
return [sensitive, porn]; return [sensitive, porn];
} }
if ([ if (analyzeVideo && (mime === 'image/apng' || mime.startsWith('video/'))) {
'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/'))) {
const [outDir, disposeOutDir] = await createTempDir(); const [outDir, disposeOutDir] = await createTempDir();
try { try {
const command = FFmpeg() const command = FFmpeg()
@ -281,6 +273,23 @@ export class FileInfoService {
} finally { } finally {
disposeOutDir(); 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]; return [sensitive, porn];

View File

@ -756,8 +756,8 @@ export class QueueService {
@bindThis @bindThis
public async queueRetryJob(queueType: typeof QUEUE_TYPES[number], jobId: string) { public async queueRetryJob(queueType: typeof QUEUE_TYPES[number], jobId: string) {
const queue = this.getQueue(queueType); const queue = this.getQueue(queueType);
const job: Bull.Job | null = await queue.getJob(jobId); const job = await queue.getJob(jobId);
if (job) { if (job != null) {
if (job.finishedOn != null) { if (job.finishedOn != null) {
await job.retry(); await job.retry();
} else { } else {
@ -769,8 +769,8 @@ export class QueueService {
@bindThis @bindThis
public async queueRemoveJob(queueType: typeof QUEUE_TYPES[number], jobId: string) { public async queueRemoveJob(queueType: typeof QUEUE_TYPES[number], jobId: string) {
const queue = this.getQueue(queueType); const queue = this.getQueue(queueType);
const job: Bull.Job | null = await queue.getJob(jobId); const job = await queue.getJob(jobId);
if (job) { if (job != null) {
await job.remove(); await job.remove();
} }
} }
@ -803,8 +803,8 @@ export class QueueService {
@bindThis @bindThis
public async queueGetJob(queueType: typeof QUEUE_TYPES[number], jobId: string) { public async queueGetJob(queueType: typeof QUEUE_TYPES[number], jobId: string) {
const queue = this.getQueue(queueType); const queue = this.getQueue(queueType);
const job: Bull.Job | null = await queue.getJob(jobId); const job = await queue.getJob(jobId);
if (job) { if (job != null) {
return this.packJobData(job); return this.packJobData(job);
} else { } else {
throw new Error(`Job not found: ${jobId}`); 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, // Make sure any unknown path under /api returns HTTP 404 Not Found,
// because otherwise ClientServerService will return the base client HTML // because otherwise ClientServerService will return the base client HTML
// page with HTTP 200. // page with HTTP 200.

View File

@ -34,13 +34,22 @@ export const meta = {
res: { res: {
type: 'object', type: 'object',
optional: false, nullable: false, optional: false, nullable: false,
allOf: [
{
type: 'object',
ref: 'MeDetailed', ref: 'MeDetailed',
},
{
type: 'object',
optional: false, nullable: false,
properties: { properties: {
token: { token: {
type: 'string', type: 'string',
optional: false, nullable: false, optional: false, nullable: false,
}, },
}, },
}
],
}, },
} as const; } as const;

View File

@ -22,7 +22,14 @@ export const meta = {
res: { res: {
type: 'object', type: 'object',
optional: false, nullable: false, optional: false, nullable: false,
allOf: [
{
type: 'object',
ref: 'UserList', ref: 'UserList',
},
{
type: 'object',
optional: false, nullable: false,
properties: { properties: {
likedCount: { likedCount: {
type: 'number', type: 'number',
@ -34,6 +41,8 @@ export const meta = {
}, },
}, },
}, },
],
},
errors: { errors: {
noSuchList: { noSuchList: {

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 => { return await client.request('admin/accounts/create', ADMIN_PARAMS).then(res => {
ADMIN_CACHE.set(host, { ADMIN_CACHE.set(host, {
id: res.id, id: res.id,
// @ts-expect-error FIXME: openapi-typescript generates incorrect response type for this endpoint, so ignore this
i: res.token, i: res.token,
}); });
return res as Misskey.entities.SignupResponse; return res as Misskey.entities.SignupResponse;

View File

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

View File

@ -46,9 +46,71 @@ export default [
allowSingleExtends: true, allowSingleExtends: true,
}], }],
'import/consistent-type-specifier-style': ['error', 'prefer-top-level'], 'import/consistent-type-specifier-style': ['error', 'prefer-top-level'],
// window の禁止理由: グローバルスコープと衝突し、予期せぬ結果を招くため // window ... グローバルスコープと衝突し、予期せぬ結果を招くため
// e の禁止理由: error や event など、複数のキーワードの頭文字であり分かりにくいため // e ... error や event など、複数のキーワードの頭文字であり分かりにくいため
'id-denylist': ['error', 'window', 'e'], // 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'], 'no-shadow': ['warn'],
'vue/attributes-order': ['error', { 'vue/attributes-order': ['error', {
alphabetical: false, alphabetical: false,

View File

@ -13,10 +13,10 @@
"@discordapp/twemoji": "16.0.1", "@discordapp/twemoji": "16.0.1",
"@rollup/plugin-json": "6.1.0", "@rollup/plugin-json": "6.1.0",
"@rollup/plugin-replace": "6.0.2", "@rollup/plugin-replace": "6.0.2",
"@rollup/pluginutils": "5.2.0", "@rollup/pluginutils": "5.3.0",
"@twemoji/parser": "16.0.0", "@twemoji/parser": "16.0.0",
"@vitejs/plugin-vue": "6.0.1", "@vitejs/plugin-vue": "6.0.1",
"@vue/compiler-sfc": "3.5.19", "@vue/compiler-sfc": "3.5.21",
"astring": "1.9.0", "astring": "1.9.0",
"buraha": "0.0.1", "buraha": "0.0.1",
"estree-walker": "3.0.3", "estree-walker": "3.0.3",
@ -26,16 +26,16 @@
"mfm-js": "0.25.0", "mfm-js": "0.25.0",
"misskey-js": "workspace:*", "misskey-js": "workspace:*",
"punycode.js": "2.3.1", "punycode.js": "2.3.1",
"rollup": "4.48.0", "rollup": "4.50.1",
"sass": "1.90.0", "sass": "1.92.1",
"shiki": "3.11.0", "shiki": "3.12.2",
"tinycolor2": "1.6.0", "tinycolor2": "1.6.0",
"tsc-alias": "1.8.16", "tsc-alias": "1.8.16",
"tsconfig-paths": "4.2.0", "tsconfig-paths": "4.2.0",
"typescript": "5.9.2", "typescript": "5.9.2",
"uuid": "11.1.0", "uuid": "11.1.0",
"vite": "7.1.3", "vite": "7.1.5",
"vue": "3.5.19" "vue": "3.5.21"
}, },
"devDependencies": { "devDependencies": {
"@misskey-dev/summaly": "5.2.3", "@misskey-dev/summaly": "5.2.3",
@ -43,14 +43,14 @@
"@testing-library/vue": "8.1.0", "@testing-library/vue": "8.1.0",
"@types/estree": "1.0.8", "@types/estree": "1.0.8",
"@types/micromatch": "4.0.9", "@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/punycode.js": "npm:@types/punycode@2.1.4",
"@types/tinycolor2": "1.4.6", "@types/tinycolor2": "1.4.6",
"@types/ws": "8.18.1", "@types/ws": "8.18.1",
"@typescript-eslint/eslint-plugin": "8.40.0", "@typescript-eslint/eslint-plugin": "8.42.0",
"@typescript-eslint/parser": "8.40.0", "@typescript-eslint/parser": "8.42.0",
"@vitest/coverage-v8": "3.2.4", "@vitest/coverage-v8": "3.2.4",
"@vue/runtime-core": "3.5.19", "@vue/runtime-core": "3.5.21",
"acorn": "8.15.0", "acorn": "8.15.0",
"cross-env": "10.0.0", "cross-env": "10.0.0",
"eslint-plugin-import": "2.32.0", "eslint-plugin-import": "2.32.0",
@ -59,11 +59,11 @@
"happy-dom": "18.0.1", "happy-dom": "18.0.1",
"intersection-observer": "0.12.2", "intersection-observer": "0.12.2",
"micromatch": "4.0.8", "micromatch": "4.0.8",
"msw": "2.10.5", "msw": "2.11.1",
"nodemon": "3.1.10", "nodemon": "3.1.10",
"prettier": "3.6.2", "prettier": "3.6.2",
"start-server-and-test": "2.0.13", "start-server-and-test": "2.1.0",
"tsx": "4.20.4", "tsx": "4.20.5",
"vite-plugin-turbosnap": "1.0.3", "vite-plugin-turbosnap": "1.0.3",
"vue-component-type-helpers": "3.0.6", "vue-component-type-helpers": "3.0.6",
"vue-eslint-parser": "10.2.0", "vue-eslint-parser": "10.2.0",

View File

@ -33,7 +33,7 @@ import type { Theme } from '@/theme.js';
console.log('Misskey Embed'); console.log('Misskey Embed');
//#region Embedパラメータの取得・パース //#region Embedパラメータの取得・パース
const params = new URLSearchParams(location.search); const params = new URLSearchParams(window.location.search);
const embedParams = parseEmbedParams(params); const embedParams = parseEmbedParams(params);
if (_DEV_) console.log(embedParams); if (_DEV_) console.log(embedParams);
//#endregion //#endregion
@ -81,7 +81,7 @@ storeBootloaderErrors({ ...i18n.ts._bootErrors, reload: i18n.ts.reload });
//#endregion //#endregion
// サイズの制限 // サイズの制限
document.documentElement.style.maxWidth = '500px'; window.document.documentElement.style.maxWidth = '500px';
// iframeIdの設定 // iframeIdの設定
function setIframeIdHandler(event: MessageEvent) { function setIframeIdHandler(event: MessageEvent) {
@ -114,16 +114,16 @@ app.provide(DI.embedParams, embedParams);
const rootEl = ((): HTMLElement => { const rootEl = ((): HTMLElement => {
const MISSKEY_MOUNT_DIV_ID = 'misskey_app'; 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) { if (currentRoot) {
console.warn('multiple import detected'); console.warn('multiple import detected');
return currentRoot; return currentRoot;
} }
const root = document.createElement('div'); const root = window.document.createElement('div');
root.id = MISSKEY_MOUNT_DIV_ID; root.id = MISSKEY_MOUNT_DIV_ID;
document.body.appendChild(root); window.document.body.appendChild(root);
return root; return root;
})(); })();
@ -159,7 +159,7 @@ console.log(i18n.tsx._selfXssPrevention.description3({ link: 'https://misskey-hu
//#endregion //#endregion
function removeSplash() { function removeSplash() {
const splash = document.getElementById('splash'); const splash = window.document.getElementById('splash');
if (splash) { if (splash) {
splash.style.opacity = '0'; splash.style.opacity = '0';
splash.style.pointerEvents = 'none'; 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 => { const canvasPromise = new Promise<WorkerMultiDispatch | HTMLCanvasElement>(resolve => {
// Web Worker // Web Worker
if (import.meta.env.MODE === 'test') { if (import.meta.env.MODE === 'test') {
const canvas = document.createElement('canvas'); const canvas = window.document.createElement('canvas');
canvas.width = 64; canvas.width = 64;
canvas.height = 64; canvas.height = 64;
resolve(canvas); resolve(canvas);
@ -34,7 +34,7 @@ const canvasPromise = new Promise<WorkerMultiDispatch | HTMLCanvasElement>(resol
); );
resolve(workers); resolve(workers);
} else { } else {
const canvas = document.createElement('canvas'); const canvas = window.document.createElement('canvas');
canvas.width = 64; canvas.width = 64;
canvas.height = 64; canvas.height = 64;
resolve(canvas); resolve(canvas);

View File

@ -29,7 +29,7 @@ const props = defineProps<{
// if no instance data is given, this is for the local instance // if no instance data is given, this is for the local instance
const instance = props.instance ?? { const instance = props.instance ?? {
name: serverMetadata.name, 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'); 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 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); bg.setAlpha(0.1);
const bgCss = bg.toRgbString(); const bgCss = bg.toRgbString();
</script> </script>

View File

@ -134,7 +134,7 @@ const isBackTop = ref(false);
const empty = computed(() => items.value.size === 0); const empty = computed(() => items.value.size === 0);
const error = ref(false); 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(); const visibility = useDocumentVisibility();
@ -353,7 +353,7 @@ watch(visibility, () => {
BACKGROUND_PAUSE_WAIT_SEC * 1000); BACKGROUND_PAUSE_WAIT_SEC * 1000);
} else { // 'visible' } else { // 'visible'
if (timerForSetPause) { if (timerForSetPause) {
clearTimeout(timerForSetPause); window.clearTimeout(timerForSetPause);
timerForSetPause = null; timerForSetPause = null;
} else { } else {
isPausingUpdate = false; isPausingUpdate = false;
@ -447,11 +447,11 @@ onBeforeMount(() => {
init().then(() => { init().then(() => {
if (props.pagination.reversed) { if (props.pagination.reversed) {
nextTick(() => { nextTick(() => {
setTimeout(toBottom, 800); window.setTimeout(toBottom, 800);
// scrollToBottommoreFetching // scrollToBottommoreFetching
// more = true // more = true
setTimeout(() => { window.setTimeout(() => {
moreFetching.value = false; moreFetching.value = false;
}, 2000); }, 2000);
}); });
@ -461,11 +461,11 @@ onBeforeMount(() => {
onBeforeUnmount(() => { onBeforeUnmount(() => {
if (timerForSetPause) { if (timerForSetPause) {
clearTimeout(timerForSetPause); window.clearTimeout(timerForSetPause);
timerForSetPause = null; timerForSetPause = null;
} }
if (preventAppearFetchMoreTimer.value) { if (preventAppearFetchMoreTimer.value) {
clearTimeout(preventAppearFetchMoreTimer.value); window.clearTimeout(preventAppearFetchMoreTimer.value);
preventAppearFetchMoreTimer.value = null; preventAppearFetchMoreTimer.value = null;
} }
scrollObserver.value?.disconnect(); scrollObserver.value?.disconnect();

View File

@ -4,7 +4,7 @@
*/ */
import * as Misskey from 'misskey-js'; import * as Misskey from 'misskey-js';
const providedContextEl = document.getElementById('misskey_embedCtx'); const providedContextEl = window.document.getElementById('misskey_embedCtx');
export type ServerContext = { export type ServerContext = {
clip?: Misskey.entities.Clip; clip?: Misskey.entities.Clip;

View File

@ -6,7 +6,7 @@
import * as Misskey from 'misskey-js'; import * as Misskey from 'misskey-js';
import { misskeyApi } from '@/misskey-api.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; 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) { export function applyTheme(theme: Theme, persist = true) {
if (timeout) window.clearTimeout(timeout); if (timeout) window.clearTimeout(timeout);
document.documentElement.classList.add('_themeChanging_'); window.document.documentElement.classList.add('_themeChanging_');
timeout = window.setTimeout(() => { timeout = window.setTimeout(() => {
document.documentElement.classList.remove('_themeChanging_'); window.document.documentElement.classList.remove('_themeChanging_');
}, 1000); }, 1000);
const colorScheme = theme.base === 'dark' ? 'dark' : 'light'; const colorScheme = theme.base === 'dark' ? 'dark' : 'light';
document.documentElement.dataset.colorScheme = colorScheme; window.document.documentElement.dataset.colorScheme = colorScheme;
// Deep copy // Deep copy
const _theme = JSON.parse(JSON.stringify(theme)); const _theme = JSON.parse(JSON.stringify(theme));
@ -55,7 +55,7 @@ export function applyTheme(theme: Theme, persist = true) {
const props = compile(_theme); 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') { if (tag.tagName === 'META' && tag.getAttribute('name') === 'theme-color') {
tag.setAttribute('content', props['htmlThemeColor']); tag.setAttribute('content', props['htmlThemeColor']);
break; break;
@ -63,7 +63,7 @@ export function applyTheme(theme: Theme, persist = true) {
} }
for (const [k, v] of Object.entries(props)) { 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参照 // 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 page = window.location.pathname.split('/')[2];
const contentId = safeURIDecode(location.pathname.split('/')[3]); const contentId = safeURIDecode(window.location.pathname.split('/')[3]);
if (_DEV_) console.log(page, contentId); if (_DEV_) console.log(page, contentId);
const embedParams = inject(DI.embedParams, defaultEmbedParams); const embedParams = inject(DI.embedParams, defaultEmbedParams);

View File

@ -51,9 +51,71 @@ export default [
allowSingleExtends: true, allowSingleExtends: true,
}], }],
'import/consistent-type-specifier-style': ['error', 'prefer-top-level'], 'import/consistent-type-specifier-style': ['error', 'prefer-top-level'],
// window の禁止理由: グローバルスコープと衝突し、予期せぬ結果を招くため // window ... グローバルスコープと衝突し、予期せぬ結果を招くため
// e の禁止理由: error や event など、複数のキーワードの頭文字であり分かりにくいため // e ... error や event など、複数のキーワードの頭文字であり分かりにくいため
'id-denylist': ['error', 'window', 'e'], // 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'], 'no-shadow': ['warn'],
'vue/attributes-order': ['error', { 'vue/attributes-order': ['error', {
alphabetical: false, alphabetical: false,

View File

@ -4,15 +4,15 @@
*/ */
// eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing
const address = new URL(document.querySelector<HTMLMetaElement>('meta[property="instance_url"]')?.content || location.href); const address = new URL(window.document.querySelector<HTMLMetaElement>('meta[property="instance_url"]')?.content || window.location.href);
const siteName = document.querySelector<HTMLMetaElement>('meta[property="og:site_name"]')?.content; const siteName = window.document.querySelector<HTMLMetaElement>('meta[property="og:site_name"]')?.content;
export const host = address.host; export const host = address.host;
export const hostname = address.hostname; export const hostname = address.hostname;
export const url = address.origin; export const url = address.origin;
export const port = address.port; export const port = address.port;
export const apiUrl = location.origin + '/api'; export const apiUrl = window.location.origin + '/api';
export const wsOrigin = location.origin; export const wsOrigin = window.location.origin;
export const lang = localStorage.getItem('lang') ?? 'en-US'; export const lang = localStorage.getItem('lang') ?? 'en-US';
export const langs = _LANGS_; export const langs = _LANGS_;
export const version = _VERSION_; export const version = _VERSION_;

View File

@ -51,7 +51,7 @@ export function onScrollTop(el: HTMLElement, cb: (topVisible: boolean) => unknow
// - toleranceの範囲内に収まる程度の微量なスクロールが発生した // - toleranceの範囲内に収まる程度の微量なスクロールが発生した
let prevTopVisible = firstTopVisible; let prevTopVisible = firstTopVisible;
const onScroll = () => { const onScroll = () => {
if (!document.body.contains(el)) return; if (!window.document.body.contains(el)) return;
const topVisible = isHeadVisible(el, tolerance); const topVisible = isHeadVisible(el, tolerance);
if (topVisible !== prevTopVisible) { if (topVisible !== prevTopVisible) {
@ -78,7 +78,7 @@ export function onScrollBottom(el: HTMLElement, cb: () => unknown, tolerance = 1
const containerOrWindow = container ?? window; const containerOrWindow = container ?? window;
const onScroll = () => { const onScroll = () => {
if (!document.body.contains(el)) return; if (!window.document.body.contains(el)) return;
if (isTailVisible(el, 1, container)) { if (isTailVisible(el, 1, container)) {
cb(); cb();
if (once) removeListener(); 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 // https://ja.javascript.info/size-and-scroll-window#ref-932
export function getBodyScrollHeight() { export function getBodyScrollHeight() {
return Math.max( return Math.max(
document.body.scrollHeight, document.documentElement.scrollHeight, window.document.body.scrollHeight, window.document.documentElement.scrollHeight,
document.body.offsetHeight, document.documentElement.offsetHeight, window.document.body.offsetHeight, window.document.documentElement.offsetHeight,
document.body.clientHeight, document.documentElement.clientHeight, 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'; import type { Ref } from 'vue';
export function useDocumentVisibility(): Ref<DocumentVisibilityState> { export function useDocumentVisibility(): Ref<DocumentVisibilityState> {
const visibility = ref(document.visibilityState); const visibility = ref(window.document.visibilityState);
const onChange = (): void => { const onChange = (): void => {
visibility.value = document.visibilityState; visibility.value = window.document.visibilityState;
}; };
onMounted(() => { onMounted(() => {
document.addEventListener('visibilitychange', onChange); window.document.addEventListener('visibilitychange', onChange);
}); });
onUnmounted(() => { onUnmounted(() => {
document.removeEventListener('visibilitychange', onChange); window.document.removeEventListener('visibilitychange', onChange);
}); });
return visibility; return visibility;

View File

@ -21,9 +21,9 @@
"lint": "pnpm typecheck && pnpm eslint" "lint": "pnpm typecheck && pnpm eslint"
}, },
"devDependencies": { "devDependencies": {
"@types/node": "22.17.2", "@types/node": "22.18.1",
"@typescript-eslint/eslint-plugin": "8.40.0", "@typescript-eslint/eslint-plugin": "8.42.0",
"@typescript-eslint/parser": "8.40.0", "@typescript-eslint/parser": "8.42.0",
"esbuild": "0.25.9", "esbuild": "0.25.9",
"eslint-plugin-vue": "10.4.0", "eslint-plugin-vue": "10.4.0",
"nodemon": "3.1.10", "nodemon": "3.1.10",
@ -35,6 +35,6 @@
], ],
"dependencies": { "dependencies": {
"misskey-js": "workspace:*", "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", "@misskey-dev/browser-image-resizer": "2024.1.0",
"@rollup/plugin-json": "6.1.0", "@rollup/plugin-json": "6.1.0",
"@rollup/plugin-replace": "6.0.2", "@rollup/plugin-replace": "6.0.2",
"@rollup/pluginutils": "5.2.0", "@rollup/pluginutils": "5.3.0",
"@sentry/vue": "10.5.0", "@sentry/vue": "10.10.0",
"@syuilo/aiscript": "1.1.0", "@syuilo/aiscript": "1.1.0",
"@syuilo/aiscript-0-19-0": "npm:@syuilo/aiscript@^0.19.0", "@syuilo/aiscript-0-19-0": "npm:@syuilo/aiscript@^0.19.0",
"@twemoji/parser": "16.0.0", "@twemoji/parser": "16.0.0",
"@vitejs/plugin-vue": "6.0.1", "@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", "aiscript-vscode": "github:aiscript-dev/aiscript-vscode#v0.1.15",
"analytics": "0.8.19", "analytics": "0.8.19",
"astring": "1.9.0", "astring": "1.9.0",
@ -41,7 +41,7 @@
"chartjs-chart-matrix": "3.0.0", "chartjs-chart-matrix": "3.0.0",
"chartjs-plugin-gradient": "0.6.1", "chartjs-plugin-gradient": "0.6.1",
"chartjs-plugin-zoom": "2.2.0", "chartjs-plugin-zoom": "2.2.0",
"chromatic": "13.1.3", "chromatic": "13.1.4",
"compare-versions": "6.1.1", "compare-versions": "6.1.1",
"cropperjs": "2.0.1", "cropperjs": "2.0.1",
"date-fns": "4.1.0", "date-fns": "4.1.0",
@ -63,21 +63,21 @@
"misskey-reversi": "workspace:*", "misskey-reversi": "workspace:*",
"photoswipe": "5.4.4", "photoswipe": "5.4.4",
"punycode.js": "2.3.1", "punycode.js": "2.3.1",
"rollup": "4.48.0", "rollup": "4.50.1",
"sanitize-html": "2.17.0", "sanitize-html": "2.17.0",
"sass": "1.90.0", "sass": "1.92.1",
"shiki": "3.11.0", "shiki": "3.12.2",
"strict-event-emitter-types": "2.0.0", "strict-event-emitter-types": "2.0.0",
"textarea-caret": "3.1.0", "textarea-caret": "3.1.0",
"three": "0.179.1", "three": "0.180.0",
"throttle-debounce": "5.0.2", "throttle-debounce": "5.0.2",
"tinycolor2": "1.6.0", "tinycolor2": "1.6.0",
"tsc-alias": "1.8.16", "tsc-alias": "1.8.16",
"tsconfig-paths": "4.2.0", "tsconfig-paths": "4.2.0",
"typescript": "5.9.2", "typescript": "5.9.2",
"v-code-diff": "1.13.1", "v-code-diff": "1.13.1",
"vite": "7.1.3", "vite": "7.1.5",
"vue": "3.5.19", "vue": "3.5.21",
"vuedraggable": "next", "vuedraggable": "next",
"wanakana": "5.3.1" "wanakana": "5.3.1"
}, },
@ -85,7 +85,7 @@
"@misskey-dev/summaly": "5.2.3", "@misskey-dev/summaly": "5.2.3",
"@storybook/addon-essentials": "8.6.14", "@storybook/addon-essentials": "8.6.14",
"@storybook/addon-interactions": "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-mdx-gfm": "8.6.14",
"@storybook/addon-storysource": "8.6.14", "@storybook/addon-storysource": "8.6.14",
"@storybook/blocks": "8.6.14", "@storybook/blocks": "8.6.14",
@ -93,31 +93,31 @@
"@storybook/core-events": "8.6.14", "@storybook/core-events": "8.6.14",
"@storybook/manager-api": "8.6.14", "@storybook/manager-api": "8.6.14",
"@storybook/preview-api": "8.6.14", "@storybook/preview-api": "8.6.14",
"@storybook/react": "9.1.3", "@storybook/react": "9.1.5",
"@storybook/react-vite": "9.1.3", "@storybook/react-vite": "9.1.5",
"@storybook/test": "8.6.14", "@storybook/test": "8.6.14",
"@storybook/theming": "8.6.14", "@storybook/theming": "8.6.14",
"@storybook/types": "8.6.14", "@storybook/types": "8.6.14",
"@storybook/vue3": "9.1.3", "@storybook/vue3": "9.1.5",
"@storybook/vue3-vite": "9.1.3", "@storybook/vue3-vite": "9.1.5",
"@tabler/icons-webfont": "3.34.1", "@tabler/icons-webfont": "3.34.1",
"@testing-library/vue": "8.1.0", "@testing-library/vue": "8.1.0",
"@types/canvas-confetti": "1.9.0", "@types/canvas-confetti": "1.9.0",
"@types/estree": "1.0.8", "@types/estree": "1.0.8",
"@types/matter-js": "0.20.0", "@types/matter-js": "0.20.0",
"@types/micromatch": "4.0.9", "@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/punycode.js": "npm:@types/punycode@2.1.4",
"@types/sanitize-html": "2.16.0", "@types/sanitize-html": "2.16.0",
"@types/seedrandom": "3.0.8", "@types/seedrandom": "3.0.8",
"@types/throttle-debounce": "5.0.2", "@types/throttle-debounce": "5.0.2",
"@types/tinycolor2": "1.4.6", "@types/tinycolor2": "1.4.6",
"@types/ws": "8.18.1", "@types/ws": "8.18.1",
"@typescript-eslint/eslint-plugin": "8.40.0", "@typescript-eslint/eslint-plugin": "8.42.0",
"@typescript-eslint/parser": "8.40.0", "@typescript-eslint/parser": "8.42.0",
"@vitest/coverage-v8": "3.2.4", "@vitest/coverage-v8": "3.2.4",
"@vue/compiler-core": "3.5.19", "@vue/compiler-core": "3.5.21",
"@vue/runtime-core": "3.5.19", "@vue/runtime-core": "3.5.21",
"acorn": "8.15.0", "acorn": "8.15.0",
"cross-env": "10.0.0", "cross-env": "10.0.0",
"cypress": "14.5.4", "cypress": "14.5.4",
@ -128,17 +128,17 @@
"intersection-observer": "0.12.2", "intersection-observer": "0.12.2",
"micromatch": "4.0.8", "micromatch": "4.0.8",
"minimatch": "10.0.3", "minimatch": "10.0.3",
"msw": "2.10.5", "msw": "2.11.1",
"msw-storybook-addon": "2.0.5", "msw-storybook-addon": "2.0.5",
"nodemon": "3.1.10", "nodemon": "3.1.10",
"prettier": "3.6.2", "prettier": "3.6.2",
"react": "19.1.1", "react": "19.1.1",
"react-dom": "19.1.1", "react-dom": "19.1.1",
"seedrandom": "3.0.5", "seedrandom": "3.0.5",
"start-server-and-test": "2.0.13", "start-server-and-test": "2.1.0",
"storybook": "9.1.3", "storybook": "9.1.5",
"storybook-addon-misskey-theme": "github:misskey-dev/storybook-addon-misskey-theme", "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", "vite-plugin-turbosnap": "1.0.3",
"vitest": "3.2.4", "vitest": "3.2.4",
"vitest-fetch-mock": "0.4.5", "vitest-fetch-mock": "0.4.5",

View File

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

View File

@ -27,16 +27,16 @@ SPDX-License-Identifier: AGPL-3.0-only
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import { computed, ref } from 'vue'; import { computed, ref, watch } from 'vue';
import * as Misskey from 'misskey-js'; import * as Misskey from 'misskey-js';
import { host } from '@@/js/config.js'; import { host } from '@@/js/config.js';
import { useInterval } from '@@/js/use-interval.js';
import type { OpenOnRemoteOptions } from '@/utility/please-login.js'; import type { OpenOnRemoteOptions } from '@/utility/please-login.js';
import { sum } from '@/utility/array.js'; import { sum } from '@/utility/array.js';
import { pleaseLogin } from '@/utility/please-login.js'; import { pleaseLogin } from '@/utility/please-login.js';
import * as os from '@/os.js'; import * as os from '@/os.js';
import { misskeyApi } from '@/utility/misskey-api.js'; import { misskeyApi } from '@/utility/misskey-api.js';
import { i18n } from '@/i18n.js'; import { i18n } from '@/i18n.js';
import { useLowresTime } from '@/composables/use-lowres-time.js';
const props = defineProps<{ const props = defineProps<{
noteId: string; noteId: string;
@ -48,7 +48,21 @@ const props = defineProps<{
author?: Misskey.entities.UserLite; 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 total = computed(() => sum(props.choices.map(x => x.votes)));
const closed = computed(() => remaining.value === 0); const closed = computed(() => remaining.value === 0);
@ -71,22 +85,7 @@ const pleaseLoginContext = computed<OpenOnRemoteOptions>(() => ({
url: `https://${host}/notes/${props.noteId}`, url: `https://${host}/notes/${props.noteId}`,
})); }));
// const vote = async (id: number) => {
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) => {
if (props.readOnly || closed.value || isVoted.value) return; if (props.readOnly || closed.value || isVoted.value) return;
pleaseLogin({ openOnRemote: pleaseLoginContext.value }); 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', { return await os.apiWithDialog(serverDraftId.value == null ? 'notes/drafts/create' : 'notes/drafts/update', {
...(serverDraftId.value == null ? {} : { draftId: serverDraftId.value }), ...(serverDraftId.value == null ? {} : { draftId: serverDraftId.value }),
text: text.value, text: text.value,
useCw: useCw.value, cw: useCw.value ? cw.value || null : null,
cw: cw.value,
visibility: visibility.value, visibility: visibility.value,
localOnly: localOnly.value, localOnly: localOnly.value,
hashtag: hashtags.value, hashtag: hashtags.value,
...(files.value.length > 0 ? { fileIds: files.value.map(f => f.id) } : {}), ...(files.value.length > 0 ? { fileIds: files.value.map(f => f.id) } : {}),
poll: poll.value, poll: poll.value,
...(visibleUsers.value.length > 0 ? { visibleUserIds: visibleUsers.value.map(x => x.id) } : {}), ...(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, replyId: replyTargetNote.value ? replyTargetNote.value.id : undefined,
quoteId: quoteId.value,
channelId: targetChannel.value ? targetChannel.value.id : undefined, channelId: targetChannel.value ? targetChannel.value.id : undefined,
reactionAcceptance: reactionAcceptance.value, reactionAcceptance: reactionAcceptance.value,
}).then(() => { }).then(() => {

View File

@ -114,14 +114,13 @@ async function unsubscribe() {
if ($i && accounts.length >= 2) { if ($i && accounts.length >= 2) {
apiWithDialog('sw/unregister', { apiWithDialog('sw/unregister', {
i: $i.token,
endpoint, endpoint,
}); }, $i.token);
} else { } else {
pushSubscription.value.unsubscribe(); pushSubscription.value.unsubscribe();
apiWithDialog('sw/unregister', { apiWithDialog('sw/unregister', {
endpoint, endpoint,
}); }, null);
pushSubscription.value = null; pushSubscription.value = null;
} }
} }
@ -134,7 +133,7 @@ function encode(buffer: ArrayBuffer | null) {
* Convert the URL safe base64 string to a Uint8Array * Convert the URL safe base64 string to a Uint8Array
* @param base64String base64 string * @param base64String base64 string
*/ */
function urlBase64ToUint8Array(base64String: string): Uint8Array { function urlBase64ToUint8Array(base64String: string): BufferSource {
const padding = '='.repeat((4 - base64String.length % 4) % 4); const padding = '='.repeat((4 - base64String.length % 4) % 4);
const base64 = (base64String + padding) const base64 = (base64String + padding)
.replace(/-/g, '+') .replace(/-/g, '+')

View File

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

View File

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

View File

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

View File

@ -4,7 +4,7 @@ SPDX-License-Identifier: AGPL-3.0-only
--> -->
<template> <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> <slot></slot>
</a> </a>
</template> </template>
@ -86,6 +86,11 @@ function openWindow() {
} }
function nav(ev: MouseEvent) { function nav(ev: MouseEvent) {
// shift
if (ev.metaKey || ev.altKey || ev.ctrlKey) return;
ev.preventDefault();
if (behavior === 'browser') { if (behavior === 'browser') {
window.location.href = props.to; window.location.href = props.to;
return; return;

View File

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

View File

@ -14,9 +14,10 @@ SPDX-License-Identifier: AGPL-3.0-only
<script lang="ts" setup> <script lang="ts" setup>
import isChromatic from 'chromatic/isChromatic'; import isChromatic from 'chromatic/isChromatic';
import { onMounted, onUnmounted, ref, computed } from 'vue'; import { computed } from 'vue';
import { i18n } from '@/i18n.js'; import { i18n } from '@/i18n.js';
import { dateTimeFormat } from '@@/js/intl-const.js'; import { dateTimeFormat } from '@@/js/intl-const.js';
import { useLowresTime } from '@/composables/use-lowres-time.js';
const props = withDefaults(defineProps<{ const props = withDefaults(defineProps<{
time: Date | string | number | null; 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 invalid = Number.isNaN(_time);
const absolute = !invalid ? dateTimeFormat.format(_time) : i18n.ts._ago.invalid; 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 // 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 ago = computed(() => (now.value - _time) / 1000/*ms*/);
const relative = computed<string>(() => { const relative = computed<string>(() => {
@ -72,29 +75,6 @@ const relative = computed<string>(() => {
i18n.tsx._timeIn.seconds({ n: (~~(-ago.value % 60)).toString() }) 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> </script>
<style lang="scss" module> <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"/> <MkPageHeader v-else v-model:tab="tab" v-bind="pageHeaderProps"/>
</template> </template>
<div :class="$style.body"> <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> <slot></slot>
</MkSwiper> </MkSwiper>
<slot v-else></slot> <slot v-else></slot>
@ -45,7 +45,7 @@ const props = withDefaults(defineProps<PageHeaderProps & {
}); });
const pageHeaderProps = computed(() => { const pageHeaderProps = computed(() => {
const { reversed, ...rest } = props; const { reversed, tab, ...rest } = props;
return rest; return rest;
}); });
@ -75,10 +75,6 @@ defineExpose({
</script> </script>
<style lang="scss" module> <style lang="scss" module>
.root {
}
.body, .swiper { .body, .swiper {
min-height: calc(100cqh - (var(--MI-stickyTop, 0px) + var(--MI-stickyBottom, 0px))); min-height: calc(100cqh - (var(--MI-stickyTop, 0px) + var(--MI-stickyBottom, 0px)));
} }

View File

@ -65,5 +65,12 @@ router.useListener('change', ({ resolved }) => {
.root { .root {
height: 100%; height: 100%;
background-color: var(--MI_THEME-bg); 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> </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>( export function useGlobalEvent<T extends keyof Events>(
event: T, event: T,
callback: Events[T], callback: EventEmitter.EventListener<Events, T>,
): void { ): void {
globalEvents.on(event, callback); globalEvents.on(event, callback);
onBeforeUnmount(() => { onBeforeUnmount(() => {

View File

@ -94,7 +94,7 @@ export class Pizzax<T extends StateDef> {
private mergeState<X>(value: X, def: X): X { private mergeState<X>(value: X, def: X): X {
if (this.isPureObject(value) && this.isPureObject(def)) { 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); 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 const openingWindowsCount = ref(0);
export type ApiWithDialogCustomErrors = Record<string, { title?: string; text: string; }>; 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, endpoint: E,
data: P, data: Misskey.Endpoints[E]['req'],
token?: string | null | undefined, token?: string | null | undefined,
customErrors?: ApiWithDialogCustomErrors, 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"> <MkInput v-model="q" class="" :placeholder="i18n.ts.search" autocapitalize="off">
<template #prefix><i class="ti ti-search"></i></template> <template #prefix><i class="ti ti-search"></i></template>
</MkInput> </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> </div>
<MkFoldableSection v-if="searchEmojis"> <MkFoldableSection v-if="searchEmojis">
@ -42,22 +36,19 @@ import XEmoji from './emojis.emoji.vue';
import MkButton from '@/components/MkButton.vue'; import MkButton from '@/components/MkButton.vue';
import MkInput from '@/components/MkInput.vue'; import MkInput from '@/components/MkInput.vue';
import MkFoldableSection from '@/components/MkFoldableSection.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 { i18n } from '@/i18n.js';
import { $i } from '@/i.js'; import { $i } from '@/i.js';
const customEmojiTags = getCustomEmojiTags();
const q = ref(''); const q = ref('');
const searchEmojis = ref<Misskey.entities.EmojiSimple[] | null>(null); const searchEmojis = ref<Misskey.entities.EmojiSimple[] | null>(null);
const selectedTags = ref(new Set());
function search() { function search() {
if ((q.value === '' || q.value == null) && selectedTags.value.size === 0) { if (q.value === '' || q.value == null) {
searchEmojis.value = null; searchEmojis.value = null;
return; 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) { if (queryarry) {
@ -67,26 +58,11 @@ function search() {
} else { } else {
searchEmojis.value = customEmojis.value.filter(emoji => emoji.name.includes(q.value) || emoji.aliases.includes(q.value)); searchEmojis.value = customEmojis.value.filter(emoji => emoji.name.includes(q.value) || emoji.aliases.includes(q.value));
} }
} 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);
}
} }
watch(q, () => { watch(q, () => {
search(); search();
}); });
watch(selectedTags, () => {
search();
}, { deep: true });
</script> </script>
<style lang="scss" module> <style lang="scss" module>

View File

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

View File

@ -307,8 +307,8 @@ async function onFileSelectClicked() {
const driveFiles = await chooseFileFromPcAndUpload({ const driveFiles = await chooseFileFromPcAndUpload({
multiple: true, multiple: true,
folderId: selectedFolderId.value, 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)); gridItems.value.push(...driveFiles.map(fromDriveFile));

View File

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

View File

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

View File

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

View File

@ -23,9 +23,9 @@ SPDX-License-Identifier: AGPL-3.0-only
<div class="item _panel sub"> <div class="item _panel sub">
<div class="icon"><i class="ti ti-world-download"></i></div> <div class="icon"><i class="ti ti-world-download"></i></div>
<div class="body"> <div class="body">
<div class="value"> <div v-if="federationSubActive != null" class="value">
{{ number(federationSubActive) }} {{ 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>
<div class="label">Sub</div> <div class="label">Sub</div>
</div> </div>
@ -33,9 +33,9 @@ SPDX-License-Identifier: AGPL-3.0-only
<div class="item _panel pub"> <div class="item _panel pub">
<div class="icon"><i class="ti ti-world-upload"></i></div> <div class="icon"><i class="ti ti-world-upload"></i></div>
<div class="body"> <div class="body">
<div class="value"> <div v-if="federationPubActive != null" class="value">
{{ number(federationPubActive) }} {{ 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>
<div class="label">Pub</div> <div class="label">Pub</div>
</div> </div>

View File

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

View File

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

View File

@ -26,10 +26,10 @@ const chartEl = useTemplateRef('chartEl');
const { handler: externalTooltipHandler } = useChartTooltip(); const { handler: externalTooltipHandler } = useChartTooltip();
let chartInstance: Chart; let chartInstance: Chart | null = null;
function setData(values) { function setData(values: number[]) {
if (chartInstance == null) return; if (chartInstance == null || chartInstance.data.labels == null) return;
for (const value of values) { for (const value of values) {
chartInstance.data.labels.push(''); chartInstance.data.labels.push('');
chartInstance.data.datasets[0].data.push(value); chartInstance.data.datasets[0].data.push(value);
@ -41,8 +41,8 @@ function setData(values) {
chartInstance.update(); chartInstance.update();
} }
function pushData(value) { function pushData(value: number) {
if (chartInstance == null) return; if (chartInstance == null || chartInstance.data.labels == null) return;
chartInstance.data.labels.push(''); chartInstance.data.labels.push('');
chartInstance.data.datasets[0].data.push(value); chartInstance.data.datasets[0].data.push(value);
if (chartInstance.data.datasets[0].data.length > 100) { if (chartInstance.data.datasets[0].data.length > 100) {
@ -67,6 +67,8 @@ const color =
'?' as never; '?' as never;
onMounted(() => { onMounted(() => {
if (chartEl.value == null) return;
const vLineColor = store.s.darkMode ? 'rgba(255, 255, 255, 0.2)' : 'rgba(0, 0, 0, 0.2)'; const vLineColor = store.s.darkMode ? 'rgba(255, 255, 255, 0.2)' : 'rgba(0, 0, 0, 0.2)';
chartInstance = new Chart(chartEl.value, { 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 { markRaw, onMounted, onUnmounted, ref, useTemplateRef } from 'vue';
import * as Misskey from 'misskey-js'; import * as Misskey from 'misskey-js';
import XChart from './overview.queue.chart.vue'; 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 number from '@/filters/number.js';
import { useStream } from '@/stream.js'; import { useStream } from '@/stream.js';
import { genId } from '@/utility/id.js'; import { genId } from '@/utility/id.js';
@ -64,10 +64,10 @@ function onStats(stats: Misskey.entities.QueueStats) {
delayed.value = stats[props.domain].delayed; delayed.value = stats[props.domain].delayed;
waiting.value = stats[props.domain].waiting; waiting.value = stats[props.domain].waiting;
chartProcess.value.pushData(stats[props.domain].activeSincePrevTick); chartProcess.value?.pushData(stats[props.domain].activeSincePrevTick);
chartActive.value.pushData(stats[props.domain].active); chartActive.value?.pushData(stats[props.domain].active);
chartDelayed.value.pushData(stats[props.domain].delayed); chartDelayed.value?.pushData(stats[props.domain].delayed);
chartWaiting.value.pushData(stats[props.domain].waiting); chartWaiting.value?.pushData(stats[props.domain].waiting);
} }
function onStatsLog(statsLog: Misskey.entities.QueueStatsLog) { function onStatsLog(statsLog: Misskey.entities.QueueStatsLog) {
@ -83,10 +83,10 @@ function onStatsLog(statsLog: Misskey.entities.QueueStatsLog) {
dataWaiting.push(stats[props.domain].waiting); dataWaiting.push(stats[props.domain].waiting);
} }
chartProcess.value.setData(dataProcess); chartProcess.value?.setData(dataProcess);
chartActive.value.setData(dataActive); chartActive.value?.setData(dataActive);
chartDelayed.value.setData(dataDelayed); chartDelayed.value?.setData(dataDelayed);
chartWaiting.value.setData(dataWaiting); chartWaiting.value?.setData(dataWaiting);
} }
onMounted(() => { onMounted(() => {

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -19,7 +19,7 @@ SPDX-License-Identifier: AGPL-3.0-only
</div> </div>
</div> </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 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> <MkButton inline @click="create()"><i class="ti ti-download" :class="$style.import"></i>{{ i18n.ts.import }}</MkButton>
</div> </div>
@ -41,7 +41,7 @@ const props = defineProps<{
listId: string; 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 error = ref<unknown | null>(null);
const users = ref<Misskey.entities.UserDetailed[]>([]); const users = ref<Misskey.entities.UserDetailed[]>([]);
@ -51,8 +51,9 @@ function fetchList(): void {
forPublic: true, forPublic: true,
}).then(_list => { }).then(_list => {
list.value = _list; list.value = _list;
if (_list.userIds == null || _list.userIds.length === 0) return;
misskeyApi('users/show', { misskeyApi('users/show', {
userIds: list.value.userIds, userIds: _list.userIds,
}).then(_users => { }).then(_users => {
users.value = _users; users.value = _users;
}); });
@ -68,7 +69,7 @@ function like() {
}).then(() => { }).then(() => {
if (list.value == null) return; if (list.value == null) return;
list.value.isLiked = true; 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(() => { }).then(() => {
if (list.value == null) return; if (list.value == null) return;
list.value.isLiked = false; 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({ const { canceled, result: name } = await os.inputText({
title: i18n.ts.enterListName, 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 }); 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<{ const emit = defineEmits<{
(ev: 'update:modelValue', value: Misskey.entities.PageBlock & { type: 'note' }): void; (ev: 'update:modelValue', value: Misskey.entities.PageBlock & { type: 'note' }): void;
(ev: 'remove'): void;
}>(); }>();
const id = ref(props.modelValue.note); const id = ref(props.modelValue.note);

View File

@ -27,6 +27,7 @@ const props = defineProps<{
const emit = defineEmits<{ const emit = defineEmits<{
(ev: 'update:modelValue', value: Misskey.entities.PageBlock & { type: 'text' }): void; (ev: 'update:modelValue', value: Misskey.entities.PageBlock & { type: 'text' }): void;
(ev: 'remove'): void;
}>(); }>();
let autocomplete: Autocomplete; let autocomplete: Autocomplete;
@ -42,6 +43,7 @@ watch(text, () => {
}); });
onMounted(() => { onMounted(() => {
if (inputEl.value == null) return;
autocomplete = new Autocomplete(inputEl.value, text); 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"> <PageWithHeader v-model:tab="tab" :actions="headerActions" :tabs="headerTabs">
<div class="_spacer" style="--MI_SPACER-w: 700px;"> <div class="_spacer" style="--MI_SPACER-w: 700px;">
<div class="jqqmcavi"> <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="!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" 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> <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>
<MkInput v-model="name"> <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> <template #label>{{ i18n.ts._pages.url }}</template>
</MkInput> </MkInput>
@ -84,7 +84,7 @@ const props = defineProps<{
}>(); }>();
const tab = ref('settings'); const tab = ref('settings');
const author = ref($i); const author = ref<Misskey.entities.User | null>($i);
const readonly = ref(false); const readonly = ref(false);
const page = ref<Misskey.entities.Page | null>(null); const page = ref<Misskey.entities.Page | null>(null);
const pageId = ref<string | null>(null); const pageId = ref<string | null>(null);

View File

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

View File

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

View File

@ -131,6 +131,10 @@ SPDX-License-Identifier: AGPL-3.0-only
<hr> <hr>
<MkButton @click="forceCloudBackup">Force cloud backup</MkButton>
<hr>
<template v-if="$i.policies.chatAvailability !== 'unavailable'"> <template v-if="$i.policies.chatAvailability !== 'unavailable'">
<MkButton @click="readAllChatMessages">Read all chat messages</MkButton> <MkButton @click="readAllChatMessages">Read all chat messages</MkButton>
@ -167,6 +171,7 @@ import { signout } from '@/signout.js';
import { migrateOldSettings } from '@/pref-migrate.js'; import { migrateOldSettings } from '@/pref-migrate.js';
import { hideAllTips as _hideAllTips, resetAllTips as _resetAllTips } from '@/tips.js'; import { hideAllTips as _hideAllTips, resetAllTips as _resetAllTips } from '@/tips.js';
import { suggestReload } from '@/utility/reload-suggest.js'; import { suggestReload } from '@/utility/reload-suggest.js';
import { cloudBackup } from '@/preferences/utility.js';
const $i = ensureSignin(); const $i = ensureSignin();
@ -224,6 +229,11 @@ function readAllChatMessages() {
os.apiWithDialog('chat/read-all', {}); os.apiWithDialog('chat/read-all', {});
} }
async function forceCloudBackup() {
await cloudBackup();
os.success();
}
const headerActions = computed(() => []); const headerActions = computed(() => []);
const headerTabs = 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 { misskeyApi } from '@/utility/misskey-api.js';
import { playMisskeySfxFile, soundsTypes, getSoundDuration } from '@/utility/sound.js'; import { playMisskeySfxFile, soundsTypes, getSoundDuration } from '@/utility/sound.js';
import { selectFile } from '@/utility/drive.js'; import { selectFile } from '@/utility/drive.js';
import type { SoundStore } from '@/preferences/def.js';
const props = defineProps<{ const props = defineProps<{
type: SoundType; def: SoundStore;
fileId?: string;
fileUrl?: string;
volume: number;
}>(); }>();
const emit = defineEmits<{ const emit = defineEmits<{
@ -61,14 +59,14 @@ const {
label: getSoundTypeName(x), label: getSoundTypeName(x),
value: x, value: x,
})), })),
initialValue: props.type, initialValue: props.def.type,
}); });
const fileId = ref(props.fileId); const fileId = ref('fileId' in props.def ? props.def.fileId : undefined);
const fileUrl = ref(props.fileUrl); const fileUrl = ref('fileUrl' in props.def ? props.def.fileUrl : undefined);
const fileName = ref<string>(''); const fileName = ref<string>('');
const driveFileError = ref(false); const driveFileError = ref(false);
const hasChanged = ref(false); const hasChanged = ref(false);
const volume = ref(props.volume); const volume = ref(props.def.volume);
if (type.value === '_driveFile_' && fileId.value) { if (type.value === '_driveFile_' && fileId.value) {
await misskeyApi('drive/files/show', { 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> <template #suffix>{{ getSoundTypeName(sounds[type].type) }}</template>
<Suspense> <Suspense>
<template #default> <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>
<template #fallback> <template #fallback>
<MkLoading/> <MkLoading/>

View File

@ -112,8 +112,7 @@ async function init() {
...(visibleUserIds ? visibleUserIds.split(',').map(userId => ({ userId })) : []), ...(visibleUserIds ? visibleUserIds.split(',').map(userId => ({ userId })) : []),
...(visibleAccts ? visibleAccts.split(',').map(Misskey.acct.parse) : []), ...(visibleAccts ? visibleAccts.split(',').map(Misskey.acct.parse) : []),
] ]
// TypeScript // @ts-expect-error payload
.map(q => 'username' in q ? { username: q.username, host: q.host === null ? undefined : q.host } : q)
.map(q => misskeyApi('users/show', q) .map(q => misskeyApi('users/show', q)
.then(user => { .then(user => {
visibleUsers.value.push(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> <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 }`"> <MkA v-for="list in items" :key="list.id" class="_panel" :class="$style.list" :to="`/list/${ list.id }`">
<div>{{ list.name }}</div> <div>{{ list.name }}</div>
<MkAvatars :userIds="list.userIds"/> <MkAvatars v-if="list.userIds != null" :userIds="list.userIds"/>
</MkA> </MkA>
</MkPagination> </MkPagination>
</div> </div>

View File

@ -7,7 +7,7 @@ import { ref } from 'vue';
import { compareVersions } from 'compare-versions'; import { compareVersions } from 'compare-versions';
import { isSafeMode } from '@@/js/config.js'; import { isSafeMode } from '@@/js/config.js';
import * as Misskey from 'misskey-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 type { FormWithDefault } from '@/utility/form.js';
import { genId } from '@/utility/id.js'; import { genId } from '@/utility/id.js';
import { store } from '@/store.js'; import { store } from '@/store.js';
@ -82,22 +82,23 @@ export async function parsePluginMeta(code: string): Promise<AiScriptPluginMeta>
} }
const metadata = meta.get(null); const metadata = meta.get(null);
if (metadata == null) { if (metadata == null || typeof metadata !== 'object' || Array.isArray(metadata)) {
throw new Error('Metadata not found'); throw new Error('Metadata not found or invalid');
} }
const { name, version, author, description, permissions, config } = metadata; const { name, version, author, description, permissions, config } = metadata;
if (name == null || version == null || author == null) { if (name == null || version == null || author == null) {
throw new Error('Required property not found'); throw new Error('Required property not found');
} }
return { return {
name, name: name as string,
version, version: version as string,
author, author: author as string,
description, description: description as string | undefined,
permissions, permissions: permissions as string[] | undefined,
config, config: config as Record<string, any> | undefined,
}; };
} }
@ -110,7 +111,7 @@ export async function authorizePlugin(plugin: Plugin) {
title: i18n.ts.tokenRequested, title: i18n.ts.tokenRequested,
information: i18n.ts.pluginTokenRequestedDescription, information: i18n.ts.pluginTokenRequestedDescription,
initialName: plugin.name, initialName: plugin.name,
initialPermissions: plugin.permissions, initialPermissions: plugin.permissions as typeof Misskey.permissions[number][],
}, { }, {
done: async result => { done: async result => {
const { name, permissions } = result; const { name, permissions } = result;
@ -149,6 +150,7 @@ export async function installPlugin(code: string, meta?: AiScriptPluginMeta) {
const plugin = { const plugin = {
...realMeta, ...realMeta,
config: realMeta.config ?? {},
installId, installId,
active: true, active: true,
configData: {}, configData: {},
@ -205,7 +207,7 @@ type HandlerDef = {
handler: (note: Misskey.entities.Note) => void; handler: (note: Misskey.entities.Note) => void;
}; };
note_view_interruptor: { note_view_interruptor: {
handler: (note: Misskey.entities.Note) => Misskey.entities.Note; handler: (note: Misskey.entities.Note) => Misskey.entities.Note | null;
}; };
note_post_interruptor: { note_post_interruptor: {
handler: (note: FIXME) => unknown; 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>> { async function createPluginEnv(opts: { plugin: Plugin; storageKey: string }): Promise<Record<string, values.Value>> {
const id = opts.plugin.installId; 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 { createAiScriptEnv } = await import('@/aiscript/api.js');
const config = new Map<string, values.Value>(); const config = new Map<string, values.Value>();
@ -375,7 +379,7 @@ async function createPluginEnv(opts: { plugin: Plugin; storageKey: string }): Pr
utils.assertFunction(handler); utils.assertFunction(handler);
addPluginHandler(id, 'post_form_action', { addPluginHandler(id, 'post_form_action', {
title: title.value, title: title.value,
handler: withContext(ctx => (form, update) => { handler: (form, update) => withContext(ctx => {
ctx.execFn(handler, [utils.jsToVal(form), values.FN_NATIVE(([key, value]) => { ctx.execFn(handler, [utils.jsToVal(form), values.FN_NATIVE(([key, value]) => {
if (!key || !value) { if (!key || !value) {
return; return;
@ -391,7 +395,7 @@ async function createPluginEnv(opts: { plugin: Plugin; storageKey: string }): Pr
utils.assertFunction(handler); utils.assertFunction(handler);
addPluginHandler(id, 'user_action', { addPluginHandler(id, 'user_action', {
title: title.value, title: title.value,
handler: withContext(ctx => (user) => { handler: (user) => withContext(ctx => {
ctx.execFn(handler, [utils.jsToVal(user)]); ctx.execFn(handler, [utils.jsToVal(user)]);
}), }),
}); });
@ -402,7 +406,7 @@ async function createPluginEnv(opts: { plugin: Plugin; storageKey: string }): Pr
utils.assertFunction(handler); utils.assertFunction(handler);
addPluginHandler(id, 'note_action', { addPluginHandler(id, 'note_action', {
title: title.value, title: title.value,
handler: withContext(ctx => (note) => { handler: (note) => withContext(ctx => {
ctx.execFn(handler, [utils.jsToVal(note)]); 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]) => { 'Plugin:register:note_view_interruptor': values.FN_NATIVE(([handler]) => {
utils.assertFunction(handler); utils.assertFunction(handler);
addPluginHandler(id, 'note_view_interruptor', { addPluginHandler(id, 'note_view_interruptor', {
handler: withContext(ctx => (note) => { handler: (note) => withContext(ctx => {
return utils.valToJs(ctx.execFnSync(handler, [utils.jsToVal(note)])); 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]) => { 'Plugin:register:note_post_interruptor': values.FN_NATIVE(([handler]) => {
utils.assertFunction(handler); utils.assertFunction(handler);
addPluginHandler(id, 'note_post_interruptor', { addPluginHandler(id, 'note_post_interruptor', {
handler: withContext(ctx => async (note) => { handler: (note) => withContext(ctx => {
return utils.valToJs(await ctx.execFn(handler, [utils.jsToVal(note)])); 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]) => { 'Plugin:register:page_view_interruptor': values.FN_NATIVE(([handler]) => {
utils.assertFunction(handler); utils.assertFunction(handler);
addPluginHandler(id, 'page_view_interruptor', { addPluginHandler(id, 'page_view_interruptor', {
handler: withContext(ctx => async (page) => { handler: (page) => withContext(ctx => {
return utils.valToJs(await ctx.execFn(handler, [utils.jsToVal(page)])); 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'); const plugins = ColdDeviceStorage.get('plugins');
prefer.commit('plugins', plugins.map(p => ({ prefer.commit('plugins', plugins.map(p => {
...p, const { id, ...rest } = p;
installId: (p as any).id, return {
id: undefined, ...rest,
}))); config: rest.config ?? {},
installId: id,
};
}));
prefer.commit('deck.profile', deckStore.s.profile); prefer.commit('deck.profile', deckStore.s.profile);
misskeyApi('i/registry/keys', { misskeyApi('i/registry/keys', {
@ -115,7 +118,13 @@ export function migrateOldSettings() {
prefer.commit('enableCondensedLine', store.s.enableCondensedLine); prefer.commit('enableCondensedLine', store.s.enableCondensedLine);
prefer.commit('keepScreenOn', store.s.keepScreenOn); prefer.commit('keepScreenOn', store.s.keepScreenOn);
prefer.commit('useGroupedNotifications', store.s.useGroupedNotifications); 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('enableSeasonalScreenEffect', store.s.enableSeasonalScreenEffect);
prefer.commit('enableHorizontalSwipe', store.s.enableHorizontalSwipe); prefer.commit('enableHorizontalSwipe', store.s.enableHorizontalSwipe);
prefer.commit('useNativeUiForVideoAudioPlayer', store.s.useNativeUIForVideoAudioPlayer); prefer.commit('useNativeUiForVideoAudioPlayer', store.s.useNativeUIForVideoAudioPlayer);

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -38,12 +38,12 @@ SPDX-License-Identifier: AGPL-3.0-only
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import { ref } from 'vue'; import { ref, watch } from 'vue';
import { useWidgetPropsManager } from './widget.js'; import { useWidgetPropsManager } from './widget.js';
import type { WidgetComponentEmits, WidgetComponentExpose, WidgetComponentProps } from './widget.js'; import type { WidgetComponentEmits, WidgetComponentExpose, WidgetComponentProps } from './widget.js';
import type { FormWithDefault, GetFormResultType } from '@/utility/form.js'; import type { FormWithDefault, GetFormResultType } from '@/utility/form.js';
import { i18n } from '@/i18n.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'; const name = 'calendar';
@ -65,6 +65,7 @@ const { widgetProps, configure } = useWidgetPropsManager(name,
emit, emit,
); );
const fNow = useLowresTime();
const year = ref(0); const year = ref(0);
const month = ref(0); const month = ref(0);
const day = ref(0); const day = ref(0);
@ -73,8 +74,14 @@ const yearP = ref(0);
const monthP = ref(0); const monthP = ref(0);
const dayP = ref(0); const dayP = ref(0);
const isHoliday = ref(false); 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 nd = now.getDate();
const nm = now.getMonth(); const nm = now.getMonth();
const ny = now.getFullYear(); const ny = now.getFullYear();
@ -104,11 +111,28 @@ const tick = () => {
yearP.value = yearNumer / yearDenom * 100; yearP.value = yearNumer / yearDenom * 100;
isHoliday.value = now.getDay() === 0 || now.getDay() === 6; isHoliday.value = now.getDay() === 0 || now.getDay() === 6;
}; }
useInterval(tick, 1000, { watch(fNow, (to) => {
immediate: true, update(to);
afterMounted: false,
//
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>({ defineExpose<WidgetComponentExpose>({

View File

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

View File

@ -11,16 +11,16 @@
"lint": "pnpm typecheck && pnpm eslint" "lint": "pnpm typecheck && pnpm eslint"
}, },
"devDependencies": { "devDependencies": {
"@types/node": "22.17.2", "@types/node": "22.18.1",
"@types/wawoff2": "1.0.2", "@types/wawoff2": "1.0.2",
"@typescript-eslint/eslint-plugin": "8.40.0", "@typescript-eslint/eslint-plugin": "8.42.0",
"@typescript-eslint/parser": "8.40.0" "@typescript-eslint/parser": "8.42.0"
}, },
"dependencies": { "dependencies": {
"@tabler/icons-webfont": "3.34.1", "@tabler/icons-webfont": "3.34.1",
"harfbuzzjs": "0.4.9", "harfbuzzjs": "0.4.11",
"tiny-glob": "0.2.9", "tiny-glob": "0.2.9",
"tsx": "4.20.4", "tsx": "4.20.5",
"typescript": "5.9.2", "typescript": "5.9.2",
"wawoff2": "2.0.1" "wawoff2": "2.0.1"
}, },

View File

@ -24,9 +24,9 @@
"devDependencies": { "devDependencies": {
"@types/matter-js": "0.20.0", "@types/matter-js": "0.20.0",
"@types/seedrandom": "3.0.8", "@types/seedrandom": "3.0.8",
"@types/node": "22.17.2", "@types/node": "22.18.1",
"@typescript-eslint/eslint-plugin": "8.40.0", "@typescript-eslint/eslint-plugin": "8.42.0",
"@typescript-eslint/parser": "8.40.0", "@typescript-eslint/parser": "8.42.0",
"nodemon": "3.1.10", "nodemon": "3.1.10",
"execa": "9.6.0", "execa": "9.6.0",
"typescript": "5.9.2", "typescript": "5.9.2",

View File

@ -818,6 +818,18 @@ export type Channels = {
}; };
receives: null; receives: null;
}; };
reversi: {
params: null;
events: {
matched: (payload: {
game: ReversiGameDetailed;
}) => void;
invited: (payload: {
user: User;
}) => void;
};
receives: null;
};
reversiGame: { reversiGame: {
params: { params: {
gameId: string; gameId: string;
@ -1449,6 +1461,10 @@ export type Endpoints = Overwrite<Endpoints_2, {
}>; }>;
res: AdminRolesCreateResponse; res: AdminRolesCreateResponse;
}; };
'clear-browser-cache': {
req: EmptyRequest;
res: EmptyResponse;
};
}>; }>;
// @public (undocumented) // @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/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.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:226: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: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) // (No @packageDocumentation comment for this package)

View File

@ -8,9 +8,9 @@
}, },
"devDependencies": { "devDependencies": {
"@readme/openapi-parser": "5.0.1", "@readme/openapi-parser": "5.0.1",
"@types/node": "22.17.2", "@types/node": "22.18.1",
"@typescript-eslint/eslint-plugin": "8.40.0", "@typescript-eslint/eslint-plugin": "8.42.0",
"@typescript-eslint/parser": "8.40.0", "@typescript-eslint/parser": "8.42.0",
"openapi-types": "12.1.3", "openapi-types": "12.1.3",
"openapi-typescript": "7.9.1", "openapi-typescript": "7.9.1",
"ts-case-convert": "2.1.0", "ts-case-convert": "2.1.0",

View File

@ -1,7 +1,7 @@
{ {
"type": "module", "type": "module",
"name": "misskey-js", "name": "misskey-js",
"version": "2025.9.0-alpha.1", "version": "2025.9.0",
"description": "Misskey SDK for JavaScript", "description": "Misskey SDK for JavaScript",
"license": "MIT", "license": "MIT",
"main": "./built/index.js", "main": "./built/index.js",
@ -36,9 +36,9 @@
}, },
"devDependencies": { "devDependencies": {
"@microsoft/api-extractor": "7.52.11", "@microsoft/api-extractor": "7.52.11",
"@types/node": "22.17.2", "@types/node": "22.18.1",
"@typescript-eslint/eslint-plugin": "8.40.0", "@typescript-eslint/eslint-plugin": "8.42.0",
"@typescript-eslint/parser": "8.40.0", "@typescript-eslint/parser": "8.42.0",
"@vitest/coverage-v8": "3.2.4", "@vitest/coverage-v8": "3.2.4",
"esbuild": "0.25.9", "esbuild": "0.25.9",
"execa": "9.6.0", "execa": "9.6.0",

View File

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

View File

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

View File

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

View File

@ -22,9 +22,9 @@
"lint": "pnpm typecheck && pnpm eslint" "lint": "pnpm typecheck && pnpm eslint"
}, },
"devDependencies": { "devDependencies": {
"@types/node": "22.17.2", "@types/node": "22.18.1",
"@typescript-eslint/eslint-plugin": "8.40.0", "@typescript-eslint/eslint-plugin": "8.42.0",
"@typescript-eslint/parser": "8.40.0", "@typescript-eslint/parser": "8.42.0",
"execa": "9.6.0", "execa": "9.6.0",
"nodemon": "3.1.10", "nodemon": "3.1.10",
"typescript": "5.9.2", "typescript": "5.9.2",

View File

@ -14,7 +14,7 @@
"misskey-js": "workspace:*" "misskey-js": "workspace:*"
}, },
"devDependencies": { "devDependencies": {
"@typescript-eslint/parser": "8.40.0", "@typescript-eslint/parser": "8.42.0",
"@typescript/lib-webworker": "npm:@types/serviceworker@0.0.74", "@typescript/lib-webworker": "npm:@types/serviceworker@0.0.74",
"eslint-plugin-import": "2.32.0", "eslint-plugin-import": "2.32.0",
"nodemon": "3.1.10", "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/**/package.json',
'packages/frontend-embed/**/package.json', 'packages/frontend-embed/**/package.json',
'packages/frontend-shared/**/package.json', 'packages/frontend-shared/**/package.json',
'packages/frontend-builder/**/package.json',
'packages/misskey-bubble-game/**/package.json', 'packages/misskey-bubble-game/**/package.json',
'packages/misskey-reversi/**/package.json', 'packages/misskey-reversi/**/package.json',
'packages/sw/**/package.json', 'packages/sw/**/package.json',

View File

@ -16,7 +16,7 @@
"remark-parse": "11.0.0", "remark-parse": "11.0.0",
"typescript": "5.9.2", "typescript": "5.9.2",
"unified": "11.0.5", "unified": "11.0.5",
"vite": "6.3.5", "vite": "6.3.6",
"vite-node": "3.2.4", "vite-node": "3.2.4",
"vitest": "3.2.4" "vitest": "3.2.4"
} }
@ -2837,9 +2837,9 @@
} }
}, },
"node_modules/vite": { "node_modules/vite": {
"version": "6.3.5", "version": "6.3.6",
"resolved": "https://registry.npmjs.org/vite/-/vite-6.3.5.tgz", "resolved": "https://registry.npmjs.org/vite/-/vite-6.3.6.tgz",
"integrity": "sha512-cZn6NDFE7wdTpINgs++ZJ4N49W2vRp8LCKrn3Ob1kYNtOo21vfDoaV5GzBfLU4MovSAB8uNRm4jgzVQZ+mBzPQ==", "integrity": "sha512-0msEVHJEScQbhkbVTb/4iHZdJ6SXp/AvxL2sjwYQFfBqleHtnCqv1J3sa9zbWz/6kW1m9Tfzn92vW+kZ1WV6QA==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {

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