Merge pull request #16521 from misskey-dev/develop

Release: 2025.9.0
This commit is contained in:
misskey-release-bot[bot] 2025-09-08 12:29:29 +00:00 committed by GitHub
commit 1eab314b17
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
105 changed files with 2858 additions and 2609 deletions

View File

@ -1,3 +1,19 @@
## 2025.9.0
### Client
- Enhance: AiScriptAppウィジェットで構文エラーを検知してもダイアログではなくウィジェット内にエラーを表示するように
- Enhance: /flushページでサイトキャッシュをクリアできるようになりました
- Enhance: クリップ/リスト/アンテナ/ロール追加系メニュー項目において、表示件数を拡張
- Enhance: 「キャッシュを削除」ボタンでブラウザの内部キャッシュの削除も行えるように
- Enhance: CtrlキーCommandキーを押下しながらリンクをクリックすると新しいタブで開くように
- Fix: プッシュ通知を有効にできない問題を修正
- Fix: RSSティッカーウィジェットが正しく動作しない問題を修正
- Fix: プロファイルを復元後アカウントの切り替えができない問題を修正
- Fix: エラー画像が横に引き伸ばされてしまう問題に対応
### Server
- Fix: webpなどの画像に対してセンシティブなメディアの検出が適用されていなかった問題を修正
## 2025.8.0
### Note

View File

@ -1644,7 +1644,7 @@ _serverSettings:
reactionsBufferingDescription: "Quan s'activa aquesta opció millora bastant el rendiment en recuperar les línies de temps reduint la càrrega de la base. Com a contrapunt, augmentarà l'ús de memòria de Redís. Desactiva aquesta opció en cas de tenir un servidor amb poca memòria o si tens problemes d'inestabilitat."
remoteNotesCleaning: "Neteja automàtica de notes remotes"
remoteNotesCleaning_description: "Quan activis aquesta opció, periòdicament es netejaran les notes remotes que no es consultin, això evitarà que la base de dades se"
remoteNotesCleaningMaxProcessingDuration: "D'oració màxima del temps de funcionament del procés de neteja"
remoteNotesCleaningMaxProcessingDuration: "Duració màxima del temps de funcionament del procés de neteja"
remoteNotesCleaningExpiryDaysForEachNotes: "Duració mínima de conservació de les notes"
inquiryUrl: "URL de consulta "
inquiryUrlDescription: "Escriu adreça URL per al formulari de consulta per al mantenidor del servidor o una pàgina web amb el contacte d'informació."

View File

@ -2137,7 +2137,7 @@ _aboutMisskey:
_displayOfSensitiveMedia:
respect: "Esconder medios marcados como sensibles"
ignore: "Mostrar medios marcados como sensibles"
force: "Esconder todala multimedia"
force: "Esconder toda la multimedia"
_instanceTicker:
none: "No mostrar"
remote: "Mostrar a usuarios remotos"

View File

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

View File

@ -360,7 +360,7 @@ whenServerDisconnected: "Sunucu ile bağlantı kesildiğinde"
disconnectedFromServer: "Sunucu bağlantısı kesildi"
reload: "Yenile"
doNothing: "Yoksay"
reloadConfirm: "Zaman çizelgesini yenilemek ister misin?"
reloadConfirm: "Panoyu yenilemek ister misin?"
watch: "İzle"
unwatch: "İzlemeyi bırak"
accept: "Kabul et"
@ -573,9 +573,9 @@ objectStorageSetPublicRead: "Yükleme sırasında \"genel-okuma\" ayarını yap
s3ForcePathStyleDesc: "s3ForcePathStyle etkinleştirilirse, kova adı URL'nin ana bilgisayar adı yerine URL yoluna eklenmelidir. Kendi kendine barındırılan bir Minio örneği gibi hizmetleri kullanırken bu ayarı etkinleştirmen gerekebilir."
serverLogs: "Sunucu log kayıtları"
deleteAll: "Tümünü sil"
showFixedPostForm: "Gönderi formunu zaman çizelgesinin en üstünde görüntüle"
showFixedPostFormInChannel: "Gönderi formunu zaman çizelgesinin en üstünde görüntüle (Kanallar)"
withRepliesByDefaultForNewlyFollowed: "Yeni takip edilen kullanıcıların yanıtlarını varsayılan olarak zaman çizelgesine dahil et"
showFixedPostForm: "Gönderi formunu pano üstünde görüntüle"
showFixedPostFormInChannel: "Gönderi formunu pano üstünde görüntüle (Kanallar)"
withRepliesByDefaultForNewlyFollowed: "Yeni takip edilen kullanıcıların yanıtlarını varsayılan olarak panoya dahil et"
newNoteRecived: "Yeni Not'lar var"
newNote: "Yeni Not"
sounds: "Sesler"
@ -1059,7 +1059,7 @@ achievements: "Başarılar"
gotInvalidResponseError: "Geçersiz sunucu yanıtı"
gotInvalidResponseErrorDescription: "Sunucu erişilemez durumda olabilir veya bakım çalışması yapılmaktadır. Lütfen daha sonra tekrar dene."
thisPostMayBeAnnoying: "Bu not başkalarını rahatsız edebilir."
thisPostMayBeAnnoyingHome: "Ana zaman çizelgesine gönder"
thisPostMayBeAnnoyingHome: "Ana panoya gönder"
thisPostMayBeAnnoyingCancel: "İptal"
thisPostMayBeAnnoyingIgnore: "Yine de gönder"
collapseRenotes: "Daha önce görüntülenen Renote'lari kısaltılmış olarak göster"
@ -1218,8 +1218,8 @@ showRepliesToOthersInTimeline: "Pano'da diğer kişilere verilen yanıtları
hideRepliesToOthersInTimeline: "Pano'dan diğer kişilerin yanıtlarını gizle"
showRepliesToOthersInTimelineAll: "Pano'da takip ettiğin herkesin diğerlerine verdiği yanıtları göster"
hideRepliesToOthersInTimelineAll: "Pano'da takip ettiğin herkesten diğer kişilere verilen yanıtları gizle"
confirmShowRepliesAll: "Bu işlem geri alınamaz. Takip ettiğin herkesin yanıtlarını zaman çizelgende diğer kullanıcılara göstermek istiyor musun?"
confirmHideRepliesAll: "Bu işlem geri alınamaz. Şu anda takip ettiğin tüm kullanıcıların yanıtlarını zaman tünelinde cidden göstermeyecek misin?"
confirmShowRepliesAll: "Bu işlem geri alınamaz. Takip ettiğin herkesin yanıtlarını panoda diğer kullanıcılara göstermek istiyor musun?"
confirmHideRepliesAll: "Bu işlem geri alınamaz. Şu anda takip ettiğin tüm kullanıcıların yanıtlarını panoda cidden göstermeyecek misin?"
externalServices: "Dış Hizmetler"
sourceCode: "Kaynak kodu"
sourceCodeIsNotYetProvided: "Kaynak kodu henüz mevcut değildir. Bu sorunu gidermek için yöneticiyle iletişime geçin."
@ -1570,9 +1570,9 @@ _initialTutorial:
description: "Burada, Misskey'i kullanmanın temellerini ve özelliklerini öğrenebilirsin."
_note:
title: "Not nedir?"
description: "Misskey'deki gönderiler “Notlar” olarak adlandırılır. Notlar zaman çizelgesinde kronolojik olarak düzenlenir ve gerçek zamanlı olarak güncellenir."
description: "Misskey'deki gönderiler “Notlar” olarak adlandırılır. Notlar panoda kronolojik olarak düzenlenir ve gerçek zamanlı olarak güncellenir."
reply: "Bir mesaja yanıt vermek için bu düğmeye tıklayın. Yanıtlara yanıt vermek de mümkündür, böylece konuşma bir konu başlığı gibi devam eder."
renote: "Bu notu kendi zaman çizelgende paylaşabilirsiniz. Ayrıca yorumlarınızla birlikte alıntı da yapabilirsin."
renote: "Bu notu kendi panonda paylaşabilirsin. Ayrıca yorumlarınla birlikte alıntı da yapabilirsin."
reaction: "Not'a tepkiler ekleyebilirsin. Daha fazla ayrıntı bir sonraki sayfada açıklanacak."
menu: "Not ayrıntılarını görüntüleyebilir, bağlantıları kopyalayabilir ve çeşitli diğer işlemleri gerçekleştirebilirsin."
_reaction:
@ -1640,7 +1640,7 @@ _serverSettings:
shortNameDescription: "Resmi adın uzun olması durumunda görüntülenebilen, örneğin adının kısaltması."
fanoutTimelineDescription: "Etkinleştirildiğinde Pano alma performansını büyük ölçüde artırır ve veritabanı yükünü azaltır. Bunun karşılığında Redis'in bellek kullanımı artacaktır. Sunucu belleği düşükse veya sunucu kararsızsa bunu devre dışı bırakmayı düşün."
fanoutTimelineDbFallback: "Veritabanına geri dön"
fanoutTimelineDbFallbackDescription: "Etkinleştirildiğinde, Pano önbelleğe alınmamışsa ek sorgular için veritabanına geri döner. Bu özelliği devre dışı bırakmak, geri dönüş sürecini ortadan kaldırarak sunucu yükünü daha da azaltır, ancak alınabilecek zaman çizelgelerinin aralığını sınırlar."
fanoutTimelineDbFallbackDescription: "Etkinleştirildiğinde, Pano önbelleğe alınmamışsa ek sorgular için veritabanına geri döner. Bu özelliği devre dışı bırakmak, geri dönüş sürecini ortadan kaldırarak sunucu yükünü daha da azaltır, ancak alınabilecek panoların aralığını sınırlar."
reactionsBufferingDescription: "Etkinleştirildiğinde, reaksiyon oluşturma sırasında performans büyük ölçüde artacak ve veritabanı üzerindeki yük azalacaktır. Ancak, Redis bellek kullanımı artacakt."
remoteNotesCleaning: "Uzak notların otomatik olarak temizlenmesi"
remoteNotesCleaning_description: "Etkinleştirildiğinde, kullanılmayan ve güncelliğini yitirmiş uzak notlar, veritabanının şişmesini önlemek için periyodik olarak temizlenecek."
@ -1668,6 +1668,7 @@ _serverSettings:
restartServerSetupWizardConfirm_text: "Bazı mevcut ayarlar sıfırlanacaktır."
entrancePageStyle: "Giriş sayfası stili"
showTimelineForVisitor: "Panoyu göster"
showActivitiesForVisitor: "Aktiviteleri göster"
_userGeneratedContentsVisibilityForVisitor:
all: "Her şey halka açıktır."
localOnly: "Yalnızca yerel içerik yayınlanır, uzak içerik gizli tutulur."
@ -1876,7 +1877,7 @@ _achievements:
title: "Öz Referans"
description: "Kendi notunuzu alıntı yapın"
_htl20npm:
title: "Akış Zaman Çizelgesi"
title: "Akış Panosu"
description: "Ev zaman çizelgenizin hızı 20 npm'yi (dakika başına not sayısı) aşıyor mu?"
_viewInstanceChart:
title: "Analist"
@ -1965,7 +1966,7 @@ _role:
asBadge: "Rozet olarak göster"
descriptionOfAsBadge: "This role's icon will be displayed next to the username of users with this role if turned on."
isExplorable: "Rolü keşfedilebilir hale getir"
descriptionOfIsExplorable: "Bu rolün zaman çizelgesi ve bu role sahip kullanıcıların listesi, etkinleştirilirse kamuya açık hale getirilecek."
descriptionOfIsExplorable: "Bu rolün panosu ve bu role sahip kullanıcıların listesi, etkinleştirilirse kamuya açık hale getirilecek."
displayOrder: "Pozisyon"
descriptionOfDisplayOrder: "Sayı ne kadar yüksekse, UI pozisyonu da o kadar yüksek olur."
preserveAssignmentOnMoveAccount: "Geçiş sırasında rol atamalarını koruyun"
@ -1979,7 +1980,7 @@ _role:
high: "Yüksek"
_options:
gtlAvailable: "Global Pano'yu görüntüleyebilir"
ltlAvailable: "Yerel zaman çizelgesini görüntüleyebilir"
ltlAvailable: "Yerel panoyu görüntüleyebilir"
canPublicNote: "Halka açık notlar gönderebilir"
mentionMax: "Bir notta maksimum bahsetme sayısı"
canInvite: "Sunucu davet kodları oluşturabilir"
@ -2484,7 +2485,7 @@ _visibility:
public: "Halka açık"
publicDescription: "Notunuz tüm kullanıcılar tarafından görülebilir olacaktır."
home: "Pano"
homeDescription: "Yalnızca ana zaman çizelgesine gönder"
homeDescription: "Yalnızca ana panoya gönder"
followers: "Takipçiler"
followersDescription: "Sadece takipçilerine görünür hale getir"
specified: "Doğrudan"
@ -2531,7 +2532,7 @@ _exportOrImport:
userLists: "Kullanıcı listeleri"
excludeMutingUsers: "Sessize alınan kullanıcıları hariç tut"
excludeInactiveUsers: "Etkin olmayan kullanıcıları hariç tut"
withReplies: "İçe aktarılan kullanıcıların yanıtlarını zaman çizelgesine dahil edin"
withReplies: "İçe aktarılan kullanıcıların yanıtlarını panoya dahil edin"
_charts:
federation: "Federasyon"
apRequest: "Talepler"
@ -2925,7 +2926,7 @@ _reversi:
freeMatch: "Ücretsiz Eşleştirme"
lookingForPlayer: "Rakip aranıyor..."
gameCanceled: "Oyun iptal edildi."
shareToTlTheGameWhenStart: "Oyun başlatıldığında zaman çizelgesinde paylaş"
shareToTlTheGameWhenStart: "Oyun başlatıldığında panoda paylaş"
iStartedAGame: "Oyun başladı! #MisskeyReversi"
opponentHasSettingsChanged: "Rakip ayarlarını değiştirmiş."
allowIrregularRules: "Düzensiz kurallar (tamamen ücretsiz)"
@ -3153,7 +3154,7 @@ _clientPerformanceIssueTip:
_clip:
tip: "Klip, notları gruplandırmanıza olanak tanıyan bir özelliktir."
_userLists:
tip: "Listeler, oluşturulurken belirttiğin herhangi bir kullanıcıyı içerebilir. Oluşturulan liste, yalnızca belirtilen kullanıcıları gösteren bir zaman çizelgesi olarak görüntülenebilir."
tip: "Listeler, oluşturulurken belirttiğin herhangi bir kullanıcıyı içerebilir. Oluşturulan liste, yalnızca belirtilen kullanıcıları gösteren bir pano olarak görüntülenebilir."
watermark: "Filigran"
defaultPreset: "Varsayılan Ön Ayar"
_watermarkEditor:

View File

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

View File

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

View File

@ -29,7 +29,7 @@ export class AiService {
}
@bindThis
public async detectSensitive(path: string): Promise<nsfw.PredictionType[] | null> {
public async detectSensitive(source: string | Buffer): Promise<nsfw.PredictionType[] | null> {
try {
if (isSupportedCpu === undefined) {
isSupportedCpu = await this.computeIsSupportedCpu();
@ -51,7 +51,7 @@ export class AiService {
});
}
const buffer = await fs.promises.readFile(path);
const buffer = source instanceof Buffer ? source : await fs.promises.readFile(source);
const image = await tf.node.decodeImage(buffer, 3) as any;
try {
const predictions = await this.model.classify(image);

View File

@ -21,6 +21,7 @@ import { LoggerService } from '@/core/LoggerService.js';
import type Logger from '@/logger.js';
import { bindThis } from '@/decorators.js';
import type { PredictionType } from 'nsfwjs';
import { isMimeImage } from '@/misc/is-mime-image.js';
export type FileInfo = {
size: number;
@ -204,16 +205,7 @@ export class FileInfoService {
return [sensitive, porn];
}
if ([
'image/jpeg',
'image/png',
'image/webp',
].includes(mime)) {
const result = await this.aiService.detectSensitive(source);
if (result) {
[sensitive, porn] = judgePrediction(result);
}
} else if (analyzeVideo && (mime === 'image/apng' || mime.startsWith('video/'))) {
if (analyzeVideo && (mime === 'image/apng' || mime.startsWith('video/'))) {
const [outDir, disposeOutDir] = await createTempDir();
try {
const command = FFmpeg()
@ -281,6 +273,23 @@ export class FileInfoService {
} finally {
disposeOutDir();
}
} else if (isMimeImage(mime, 'sharp-convertible-image-with-bmp')) {
/*
* tfjs-node sharp PNG
* 使299x299に事前にリサイズする
*/
const png = await (await sharpBmp(source, mime))
.resize(299, 299, {
withoutEnlargement: false,
})
.rotate()
.flatten({ background: { r: 119, g: 119, b: 119 } }) // 透過部分を18%グレーで塗りつぶす
.png()
.toBuffer();
const result = await this.aiService.detectSensitive(png);
if (result) {
[sensitive, porn] = judgePrediction(result);
}
}
return [sensitive, porn];

View File

@ -756,8 +756,8 @@ export class QueueService {
@bindThis
public async queueRetryJob(queueType: typeof QUEUE_TYPES[number], jobId: string) {
const queue = this.getQueue(queueType);
const job: Bull.Job | null = await queue.getJob(jobId);
if (job) {
const job = await queue.getJob(jobId);
if (job != null) {
if (job.finishedOn != null) {
await job.retry();
} else {
@ -769,8 +769,8 @@ export class QueueService {
@bindThis
public async queueRemoveJob(queueType: typeof QUEUE_TYPES[number], jobId: string) {
const queue = this.getQueue(queueType);
const job: Bull.Job | null = await queue.getJob(jobId);
if (job) {
const job = await queue.getJob(jobId);
if (job != null) {
await job.remove();
}
}
@ -803,8 +803,8 @@ export class QueueService {
@bindThis
public async queueGetJob(queueType: typeof QUEUE_TYPES[number], jobId: string) {
const queue = this.getQueue(queueType);
const job: Bull.Job | null = await queue.getJob(jobId);
if (job) {
const job = await queue.getJob(jobId);
if (job != null) {
return this.packJobData(job);
} else {
throw new Error(`Job not found: ${jobId}`);

View File

@ -31,6 +31,7 @@ import { FanoutTimelineService } from '@/core/FanoutTimelineService.js';
import { NotificationService } from '@/core/NotificationService.js';
import type { OnApplicationShutdown, OnModuleInit } from '@nestjs/common';
// misskey-js の rolePolicies と同期すべし
export type RolePolicies = {
gtlAvailable: boolean;
ltlAvailable: boolean;

View File

@ -10,6 +10,7 @@ import { MiAccessToken } from './AccessToken.js';
import { MiRole } from './Role.js';
import { MiDriveFile } from './DriveFile.js';
// misskey-js の notificationTypes と同期すべし
export type MiNotification = {
type: 'note';
id: string;

View File

@ -176,6 +176,17 @@ export class ApiServerService {
}
});
fastify.all('/clear-browser-cache', (request, reply) => {
if (['GET', 'POST'].includes(request.method)) {
reply.header('Clear-Site-Data', '"cache", "prefetchCache", "prerenderCache", "executionContexts"');
reply.code(204);
reply.send();
} else {
reply.code(405);
reply.send();
}
});
// Make sure any unknown path under /api returns HTTP 404 Not Found,
// because otherwise ClientServerService will return the base client HTML
// page with HTTP 200.

View File

@ -201,6 +201,8 @@ export class ClientServerService {
@bindThis
public createServer(fastify: FastifyInstance, options: FastifyPluginOptions, done: (err?: Error) => void) {
const configUrl = new URL(this.config.url);
fastify.register(fastifyView, {
root: _dirname + '/views',
engine: {
@ -239,7 +241,6 @@ export class ClientServerService {
done();
});
} else {
const configUrl = new URL(this.config.url);
const urlOriginWithoutPort = configUrl.origin.replace(/:\d+$/, '');
const port = (process.env.VITE_PORT ?? '5173');
@ -887,6 +888,22 @@ export class ClientServerService {
[, ...target.split('/').filter(x => x), ...source.split('/').filter(x => x).splice(depth)].join('/');
fastify.get('/flush', async (request, reply) => {
let sendHeader = true;
if (request.headers['origin']) {
const originURL = new URL(request.headers['origin']);
if (originURL.protocol !== 'https:') { // Clear-Site-Data only supports https
sendHeader = false;
}
if (originURL.host !== configUrl.host) {
sendHeader = false;
}
}
if (sendHeader) {
reply.header('Clear-Site-Data', '"*"');
}
reply.header('Set-Cookie', 'http-flush-failed=1; Path=/flush; Max-Age=60');
return await reply.view('flush');
});

View File

@ -6,8 +6,11 @@ html
const msg = document.getElementById('msg');
const successText = `\nSuccess Flush! <a href="/">Back to Misskey</a>\n成功しました。<a href="/">Misskeyを開き直してください。</a>`;
message('Start flushing.');
if (!document.cookie) {
message('Your site data is fully cleared by your browser.');
message(successText);
} else {
message('Your browser does not support Clear-Site-Data header. Start opportunistic flushing.');
(async function() {
try {
localStorage.clear();
@ -33,7 +36,7 @@ html
message(successText);
} catch (e) {
message(`\n${e}\n\nFlush Failed. <a href="/flush">Please retry.</a>\n失敗しました。<a href="/flush">もう一度試してみてください。</a>`);
message(`\nIf you retry more than 3 times, clear the browser cache or contact to instance admin.\n3回以上試しても失敗する場合、ブラウザのキャッシュを消去し、それでもだめならインスタンス管理者に連絡してみてください。\n`)
message(`\nIf you retry more than 3 times, try manually clearing the browser cache or contact to instance admin.\n3回以上試しても失敗する場合、ブラウザのキャッシュを手動で消去し、それでもだめならインスタンス管理者に連絡してみてください。\n`)
console.error(e);
setTimeout(() => {
@ -41,6 +44,7 @@ html
}, 10000)
}
})();
}
function message(text) {
msg.insertAdjacentHTML('beforeend', `<p>[${(new Date()).toString()}] ${text.replace(/\n/g,'<br>')}</p>`)

View File

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

View File

@ -54,68 +54,6 @@ https://github.com/sindresorhus/file-type/blob/main/core.js
https://developer.mozilla.org/en-US/docs/Web/Media/Formats/Containers
*/
export const notificationTypes = [
'note',
'follow',
'mention',
'reply',
'renote',
'quote',
'reaction',
'pollEnded',
'receiveFollowRequest',
'followRequestAccepted',
'roleAssigned',
'chatRoomInvitationReceived',
'achievementEarned',
'exportCompleted',
'login',
'createToken',
'test',
'app',
] as const;
export const obsoleteNotificationTypes = ['pollVote', 'groupInvited'] as const;
export const ROLE_POLICIES = [
'gtlAvailable',
'ltlAvailable',
'canPublicNote',
'mentionLimit',
'canInvite',
'inviteLimit',
'inviteLimitCycle',
'inviteExpirationTime',
'canManageCustomEmojis',
'canManageAvatarDecorations',
'canSearchNotes',
'canSearchUsers',
'canUseTranslator',
'canHideAds',
'driveCapacityMb',
'maxFileSizeMb',
'alwaysMarkNsfw',
'canUpdateBioMedia',
'pinLimit',
'antennaLimit',
'wordMuteLimit',
'webhookLimit',
'clipLimit',
'noteEachClipsLimit',
'userListLimit',
'userEachUserListsLimit',
'rateLimitFactor',
'avatarDecorationLimit',
'canImportAntennas',
'canImportBlocking',
'canImportFollowing',
'canImportMuting',
'canImportUserLists',
'chatAvailability',
'uploadableFileTypes',
'noteDraftLimit',
'watermarkAvailable',
] as const;
export const MFM_TAGS = ['tada', 'jelly', 'twitch', 'shake', 'spin', 'jump', 'bounce', 'flip', 'x2', 'x3', 'x4', 'scale', 'position', 'fg', 'bg', 'border', 'font', 'blur', 'rainbow', 'sparkle', 'rotate', 'ruby', 'unixtime'];
export const MFM_PARAMS: Record<typeof MFM_TAGS[number], string[]> = {
tada: ['speed=', 'delay='],

View File

@ -21,9 +21,9 @@
"lint": "pnpm typecheck && pnpm eslint"
},
"devDependencies": {
"@types/node": "22.17.2",
"@typescript-eslint/eslint-plugin": "8.40.0",
"@typescript-eslint/parser": "8.40.0",
"@types/node": "22.18.1",
"@typescript-eslint/eslint-plugin": "8.42.0",
"@typescript-eslint/parser": "8.42.0",
"esbuild": "0.25.9",
"eslint-plugin-vue": "10.4.0",
"nodemon": "3.1.10",
@ -35,6 +35,6 @@
],
"dependencies": {
"misskey-js": "workspace:*",
"vue": "3.5.19"
"vue": "3.5.21"
}
}

View File

@ -127,7 +127,7 @@ export function galleryPost(isSensitive = false) {
}
}
export function file(isSensitive = false) {
export function file(isSensitive = false): entities.DriveFile {
return {
id: 'somefileid',
createdAt: '2016-12-28T22:49:51.000Z',
@ -207,6 +207,7 @@ export function federationInstance(): entities.FederationInstance {
isSuspended: false,
suspensionState: 'none',
isBlocked: false,
isMediaSilenced: false,
softwareName: 'misskey',
softwareVersion: '2024.5.0',
openRegistrations: false,
@ -311,6 +312,8 @@ export function userDetailed(id = 'someuserid', username = 'miskist', host: enti
alsoKnownAs: null,
notify: 'none',
memo: null,
canChat: true,
chatScope: 'everyone',
};
}
@ -378,6 +381,7 @@ export function role(params: {
asBadge: params.asBadge ?? true,
canEditMembersByModerator: params.canEditMembersByModerator ?? false,
usersCount: params.usersCount ?? 10,
preserveAssignmentOnMoveAccount: false,
condFormula: {
id: '',
type: 'or',

View File

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

View File

@ -251,13 +251,30 @@ export async function openAccountMenu(opts: {
}
},
};
} else {
} else { // プロファイルを復元した場合などはアカウントのトークンや詳細情報はstoreにキャッシュされていない
return {
type: 'button' as const,
text: username,
active: opts.active != null ? opts.active === id : false,
action: async () => {
// TODO
const { dispose } = popup(defineAsyncComponent(() => import('@/components/MkSigninDialog.vue')), {
initialUsername: username,
}, {
done: async (res: Misskey.entities.SigninFlowResponse & { finished: true }) => {
store.set('accountTokens', { ...store.s.accountTokens, [host + '/' + res.id]: res.i });
if (callback) {
fetchAccount(res.i, id).then(account => {
callback(account);
});
} else {
switchAccount(host, id);
}
},
closed: () => {
dispose();
},
});
},
};
}

View File

@ -86,7 +86,7 @@ export function createAiScriptEnv(opts: { storageKey: string, token?: string })
throw new errors.AiScriptRuntimeError('expected param');
}
utils.assertObject(param);
return misskeyApi(ep.value, utils.valToJs(param) as object, actualToken).then(res => {
return misskeyApi(ep.value as keyof Misskey.Endpoints, utils.valToJs(param) as object, actualToken).then(res => {
return utils.jsToVal(res);
}, err => {
return values.ERROR('request_failed', utils.jsToVal(err));

View File

@ -7,8 +7,8 @@ import * as Misskey from 'misskey-js';
import { Cache } from '@/utility/cache.js';
import { misskeyApi } from '@/utility/misskey-api.js';
export const clipsCache = new Cache<Misskey.entities.Clip[]>(1000 * 60 * 30, () => misskeyApi('clips/list'));
export const rolesCache = new Cache(1000 * 60 * 30, () => misskeyApi('admin/roles/list'));
export const clipsCache = new Cache<Misskey.entities.Clip[]>(1000 * 60 * 30, () => misskeyApi('clips/list', { limit: 30 }));
export const rolesCache = new Cache(1000 * 60 * 30, () => misskeyApi('admin/roles/list', { limit: 30 }));
export const userListsCache = new Cache<Misskey.entities.UserList[]>(1000 * 60 * 30, () => misskeyApi('users/lists/list'));
export const antennasCache = new Cache<Misskey.entities.Antenna[]>(1000 * 60 * 30, () => misskeyApi('antennas/list'));
export const antennasCache = new Cache<Misskey.entities.Antenna[]>(1000 * 60 * 30, () => misskeyApi('antennas/list', { limit: 30 }));
export const favoritedChannelsCache = new Cache<Misskey.entities.Channel[]>(1000 * 60 * 30, () => misskeyApi('channels/my-favorites', { limit: 100 }));

View File

@ -167,9 +167,13 @@ async function init() {
for (const user of usersRes) {
if (users.value.has(user.id)) continue;
const account = accounts.find(a => a.id === user.id);
if (!account || account.token == null) continue;
users.value.set(user.id, {
...user,
token: accounts.find(a => a.id === user.id)!.token,
token: account.token,
});
}
}

View File

@ -25,7 +25,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<MkCustomEmoji v-if="'isCustomEmoji' in emoji && emoji.isCustomEmoji" :name="emoji.emoji" :class="$style.emoji" :fallbackToImage="true"/>
<MkEmoji v-else :emoji="emoji.emoji" :class="$style.emoji"/>
<!-- eslint-disable-next-line vue/no-v-html -->
<span v-if="q" :class="$style.emojiName" v-html="sanitizeHtml(emoji.name.replace(q, `<b>${q}</b>`))"></span>
<span v-if="q != null && typeof q === 'string'" :class="$style.emojiName" v-html="sanitizeHtml(emoji.name.replace(q, `<b>${q}</b>`))"></span>
<span v-else v-text="emoji.name"></span>
<span v-if="emoji.aliasOf" :class="$style.emojiAlias">({{ emoji.aliasOf }})</span>
</li>
@ -36,7 +36,7 @@ SPDX-License-Identifier: AGPL-3.0-only
</li>
</ol>
<ol v-else-if="type === 'mfmParam' && mfmParams.length > 0" ref="suggests" :class="$style.list">
<li v-for="param in mfmParams" tabindex="-1" :class="$style.item" @click="complete(type, q.params.toSpliced(-1, 1, param).join(','))" @keydown="onKeydown">
<li v-for="param in mfmParams" tabindex="-1" :class="$style.item" @click="completeMfmParam(param)" @keydown="onKeydown">
<span>{{ param }}</span>
</li>
</ol>
@ -194,6 +194,11 @@ const mfmParams = ref<string[]>([]);
const select = ref(-1);
const zIndex = os.claimZIndex('high');
function completeMfmParam(param: string) {
if (props.type !== 'mfmParam') throw new Error('Invalid type');
complete('mfmParam', props.q.params.toSpliced(-1, 1, param).join(','));
}
function complete<T extends keyof CompleteInfo>(type: T, value: CompleteInfo[T]['payload']) {
emit('done', { type, value });
emit('closed');

View File

@ -25,12 +25,12 @@ defineProps<{
showing: boolean;
x: number;
y: number;
title?: string;
title?: string | null;
series?: {
backgroundColor: string;
borderColor: string;
text: string;
}[];
}[] | null;
}>();
const emit = defineEmits<{

View File

@ -38,7 +38,7 @@ export const Default = {
};
},
args: {
file: file(),
imageFile: file(),
aspectRatio: NaN,
},
parameters: {

View File

@ -699,7 +699,7 @@ useGlobalEvent('driveFoldersDeleted', (folders) => {
}
});
let connection: Misskey.ChannelConnection<Misskey.Channels['drive']> | null = null;
let connection: Misskey.IChannelConnection<Misskey.Channels['drive']> | null = null;
onMounted(() => {
if (store.s.realtimeMode) {

View File

@ -160,7 +160,7 @@ const embedPreviewUrl = computed(() => {
const isEmbedWithScrollbar = computed(() => embedRouteWithScrollbar.includes(props.entity));
const header = ref(props.params?.header ?? true);
const maxHeight = ref(props.params?.maxHeight !== 0 ? props.params?.maxHeight ?? undefined : 500);
const maxHeight = ref(props.params?.maxHeight !== 0 ? props.params?.maxHeight ?? null : 500);
const colorMode = ref<'light' | 'dark' | 'auto'>(props.params?.colorMode ?? 'auto');
const rounded = ref(props.params?.rounded ?? true);

View File

@ -41,11 +41,11 @@ SPDX-License-Identifier: AGPL-3.0-only
</MkSwitch>
<MkSelect v-else-if="v.type === 'enum'" v-model="values[k]">
<template #label><span v-text="v.label || k"></span><span v-if="v.required === false"> ({{ i18n.ts.optional }})</span></template>
<option v-for="option in v.enum" :key="option.value" :value="option.value">{{ option.label }}</option>
<option v-for="option in v.enum" :key="getEnumKey(option)" :value="getEnumValue(option)">{{ getEnumLabel(option) }}</option>
</MkSelect>
<MkRadios v-else-if="v.type === 'radio'" v-model="values[k]">
<template #label><span v-text="v.label || k"></span><span v-if="v.required === false"> ({{ i18n.ts.optional }})</span></template>
<option v-for="option in v.options" :key="option.value" :value="option.value">{{ option.label }}</option>
<option v-for="option in v.options" :key="getRadioKey(option)" :value="option.value">{{ option.label }}</option>
</MkRadios>
<MkRange v-else-if="v.type === 'range'" v-model="values[k]" :min="v.min" :max="v.max" :step="v.step" :textConverter="v.textConverter">
<template #label><span v-text="v.label || k"></span><span v-if="v.required === false"> ({{ i18n.ts.optional }})</span></template>
@ -77,7 +77,7 @@ import MkRange from './MkRange.vue';
import MkButton from './MkButton.vue';
import MkRadios from './MkRadios.vue';
import XFile from './MkFormDialog.file.vue';
import type { Form } from '@/utility/form.js';
import type { EnumItem, Form, RadioFormItem } from '@/utility/form.js';
import MkModalWindow from '@/components/MkModalWindow.vue';
import { i18n } from '@/i18n.js';
@ -99,7 +99,11 @@ const dialog = useTemplateRef('dialog');
const values = reactive({});
for (const item in props.form) {
if ('default' in props.form[item]) {
values[item] = props.form[item].default ?? null;
} else {
values[item] = null;
}
}
function ok() {
@ -115,4 +119,20 @@ function cancel() {
});
dialog.value?.close();
}
function getEnumLabel(e: EnumItem) {
return typeof e === 'string' ? e : e.label;
}
function getEnumValue(e: EnumItem) {
return typeof e === 'string' ? e : e.value;
}
function getEnumKey(e: EnumItem) {
return typeof e === 'string' ? e : typeof e.value === 'string' ? e.value : JSON.stringify(e.value);
}
function getRadioKey(e: RadioFormItem['options'][number]) {
return typeof e.value === 'string' ? e.value : JSON.stringify(e.value);
}
</script>

View File

@ -8,7 +8,8 @@ SPDX-License-Identifier: AGPL-3.0-only
<div v-for="v, k in paramDefs" :key="k">
<MkSwitch
v-if="v.type === 'boolean'"
v-model="params[k]">
v-model="params[k]"
>
<template #label>{{ v.label ?? k }}</template>
<template v-if="v.caption != null" #caption>{{ v.caption }}</template>
</MkSwitch>
@ -53,12 +54,12 @@ SPDX-License-Identifier: AGPL-3.0-only
</template>
<script setup lang="ts">
import type { ImageEffectorRGB, ImageEffectorFxParamDefs } from '@/utility/image-effector/ImageEffector.js';
import MkInput from '@/components/MkInput.vue';
import MkRadios from '@/components/MkRadios.vue';
import MkSwitch from '@/components/MkSwitch.vue';
import MkRange from '@/components/MkRange.vue';
import { i18n } from '@/i18n.js';
import type { ImageEffectorRGB, ImageEffectorFxParamDefs } from '@/utility/image-effector/ImageEffector.js';
defineProps<{
paramDefs: ImageEffectorFxParamDefs;

View File

@ -43,7 +43,15 @@ SPDX-License-Identifier: AGPL-3.0-only
</div>
</template>
<script lang="ts" setup>
<script lang="ts">
type SupportedTypes = 'text' | 'password' | 'email' | 'url' | 'tel' | 'number' | 'search' | 'date' | 'time' | 'datetime-local' | 'color';
type ModelValueType<T extends SupportedTypes> =
T extends 'number' ? number :
T extends 'text' | 'password' | 'email' | 'url' | 'tel' | 'search' | 'date' | 'time' | 'datetime-local' | 'color' ? string :
never;
</script>
<script lang="ts" setup generic="T extends SupportedTypes = 'text'">
import { onMounted, onUnmounted, nextTick, ref, useTemplateRef, watch, computed, toRefs } from 'vue';
import { debounce } from 'throttle-debounce';
import { useInterval } from '@@/js/use-interval.js';
@ -55,8 +63,8 @@ import { Autocomplete } from '@/utility/autocomplete.js';
import { genId } from '@/utility/id.js';
const props = defineProps<{
modelValue: string | number | null;
type?: InputHTMLAttributes['type'];
modelValue: ModelValueType<T> | null;
type?: T;
required?: boolean;
readonly?: boolean;
disabled?: boolean;
@ -83,11 +91,11 @@ const emit = defineEmits<{
(ev: 'change', _ev: KeyboardEvent): void;
(ev: 'keydown', _ev: KeyboardEvent): void;
(ev: 'enter', _ev: KeyboardEvent): void;
(ev: 'update:modelValue', value: string | number): void;
(ev: 'update:modelValue', value: ModelValueType<T>): void;
}>();
const { modelValue, type, autofocus } = toRefs(props);
const v = ref(modelValue.value);
const { modelValue } = toRefs(props);
const v = ref<ModelValueType<T> | null>(modelValue.value);
const id = genId();
const focused = ref(false);
const changed = ref(false);
@ -120,8 +128,8 @@ const onKeydown = (ev: KeyboardEvent) => {
const updated = () => {
changed.value = false;
if (type.value === 'number') {
emit('update:modelValue', typeof v.value === 'number' ? v.value : parseFloat(v.value ?? '0'));
if (props.type === 'number') {
emit('update:modelValue', typeof v.value === 'number' ? v.value as ModelValueType<T> : parseFloat(v.value ?? '0') as ModelValueType<T>);
} else {
emit('update:modelValue', v.value ?? '');
}
@ -167,7 +175,7 @@ useInterval(() => {
onMounted(() => {
nextTick(() => {
if (autofocus.value) {
if (props.autofocus) {
focus();
}
});

View File

@ -89,6 +89,7 @@ import { Chart } from 'chart.js';
import type { HeatmapSource } from '@/components/MkHeatmap.vue';
import MkSelect from '@/components/MkSelect.vue';
import MkChart from '@/components/MkChart.vue';
import type { ChartSrc } from '@/components/MkChart.vue';
import { useChartTooltip } from '@/composables/use-chart-tooltip.js';
import { $i } from '@/i.js';
import * as os from '@/os.js';
@ -107,7 +108,7 @@ const shouldShowFederation = computed(() => instance.federation !== 'none' || $i
const chartLimit = 500;
const chartSpan = ref<'hour' | 'day'>('hour');
const chartSrc = ref('active-users');
const chartSrc = ref<ChartSrc>('active-users');
const heatmapSrc = ref<HeatmapSource>('active-users');
const subDoughnutEl = useTemplateRef('subDoughnutEl');
const pubDoughnutEl = useTemplateRef('pubDoughnutEl');

View File

@ -11,7 +11,7 @@ SPDX-License-Identifier: AGPL-3.0-only
:class="[$style.root, { [$style.showActionsOnlyHover]: prefer.s.showNoteActionsOnlyHover, [$style.skipRender]: prefer.s.skipNoteRender }]"
tabindex="0"
>
<MkNoteSub v-if="appearNote.replyId && !renoteCollapsed" :note="appearNote.reply" :class="$style.replyTo"/>
<MkNoteSub v-if="appearNote.replyId && !renoteCollapsed" :note="appearNote?.reply ?? null" :class="$style.replyTo"/>
<div v-if="pinned" :class="$style.tip"><i class="ti ti-pin"></i> {{ i18n.ts.pinnedNote }}</div>
<div v-if="isRenote" :class="$style.renote">
<div v-if="note.channel" :class="$style.colorBar" :style="{ background: note.channel.color }"></div>
@ -99,7 +99,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<div v-if="isEnabledUrlPreview">
<MkUrlPreview v-for="url in urls" :key="url" :url="url" :compact="true" :detail="false" :class="$style.urlPreview"/>
</div>
<div v-if="appearNote.renoteId" :class="$style.quote"><MkNoteSimple :note="appearNote.renote" :class="$style.quoteNote"/></div>
<div v-if="appearNote.renoteId" :class="$style.quote"><MkNoteSimple :note="appearNote?.renote ?? null" :class="$style.quoteNote"/></div>
<button v-if="isLong && collapsed" :class="$style.collapsed" class="_button" @click="collapsed = false">
<span :class="$style.collapsedLabel">{{ i18n.ts.showMore }}</span>
</button>

View File

@ -31,7 +31,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<script lang="ts" setup>
import { ref, useTemplateRef } from 'vue';
import { notificationTypes } from '@@/js/const.js';
import { notificationTypes } from 'misskey-js';
import MkSwitch from './MkSwitch.vue';
import MkInfo from './MkInfo.vue';
import MkButton from './MkButton.vue';

View File

@ -31,7 +31,7 @@ SPDX-License-Identifier: AGPL-3.0-only
</MkButton>
<MkLoading v-else/>
</div>
<slot :items="unref(paginator.items)" :fetching="paginator.fetching.value || paginator.fetchingOlder.value"></slot>
<slot :items="getValue(paginator.items)" :fetching="paginator.fetching.value || paginator.fetchingOlder.value"></slot>
<div v-if="direction === 'down' || direction === 'both'" v-show="downButtonVisible">
<MkButton v-if="!downButtonLoading" :class="$style.more" primary rounded @click="downButtonClick">
{{ i18n.ts.loadMore }}
@ -90,6 +90,10 @@ function onContextmenu(ev: MouseEvent) {
}], ev);
}
function getValue(v: IPaginator['items']) {
return unref(v) as UnwrapRef<T['items']>;
}
if (props.autoLoad) {
onMounted(() => {
props.paginator.init();
@ -134,7 +138,7 @@ function downButtonClick() {
defineSlots<{
empty: () => void;
default: (props: { items: UnwrapRef<T['items']> }) => void;
default: (props: { items: UnwrapRef<T['items']>, fetching: boolean }) => void;
}>();
</script>

View File

@ -78,7 +78,7 @@ function subscribe() {
// SEE: https://developer.mozilla.org/en-US/docs/Web/API/PushManager/subscribe#Parameters
return promiseDialog(registration.value.pushManager.subscribe({
userVisibleOnly: true,
applicationServerKey: urlBase64ToBase64(instance.swPublickey),
applicationServerKey: urlBase64ToUint8Array(instance.swPublickey),
})
.then(async subscription => {
pushSubscription.value = subscription;
@ -131,16 +131,22 @@ function encode(buffer: ArrayBuffer | null) {
}
/**
* Convert the URL safe base64 string to a base64 string
* Convert the URL safe base64 string to a Uint8Array
* @param base64String base64 string
*/
function urlBase64ToBase64(base64String: string): string {
function urlBase64ToUint8Array(base64String: string): Uint8Array {
const padding = '='.repeat((4 - base64String.length % 4) % 4);
const base64 = (base64String + padding)
.replace(/-/g, '+')
.replace(/_/g, '/');
return base64;
const rawData = window.atob(base64);
const outputArray = new Uint8Array(rawData.length);
for (let i = 0; i < rawData.length; ++i) {
outputArray[i] = rawData.charCodeAt(i);
}
return outputArray;
}
if (navigator.serviceWorker == null) {

View File

@ -134,7 +134,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<div>
<div><b>{{ i18n.ts._serverSettings.entrancePageStyle }}:</b></div>
<div>{{ serverSettings.clientOptions.entrancePageStyle }}</div>
<div>{{ serverSettings.clientOptions?.entrancePageStyle }}</div>
</div>
<div>
@ -191,7 +191,6 @@ SPDX-License-Identifier: AGPL-3.0-only
<script setup lang="ts">
import { computed, ref } from 'vue';
import * as Misskey from 'misskey-js';
import { ROLE_POLICIES } from '@@/js/const.js';
import MkButton from '@/components/MkButton.vue';
import MkInput from '@/components/MkInput.vue';
import * as os from '@/os.js';
@ -240,12 +239,12 @@ const serverSettings = computed<Misskey.entities.AdminUpdateMetaRequest>(() => {
enableReactionsBuffering,
clientOptions: {
entrancePageStyle: q_use.value === 'open' ? 'classic' : 'simple',
},
} as any,
};
});
const defaultPolicies = computed<Partial<Record<typeof ROLE_POLICIES[number], any>>>(() => {
let driveCapacityMb;
const defaultPolicies = computed<Partial<Misskey.entities.RolePolicies>>(() => {
let driveCapacityMb: Misskey.entities.RolePolicies['driveCapacityMb'] | undefined;
if (q_use.value === 'single') {
driveCapacityMb = 8192;
} else if (q_use.value === 'group') {
@ -254,7 +253,7 @@ const defaultPolicies = computed<Partial<Record<typeof ROLE_POLICIES[number], an
driveCapacityMb = 100;
}
let rateLimitFactor;
let rateLimitFactor: Misskey.entities.RolePolicies['rateLimitFactor'] | undefined;
if (q_use.value === 'single') {
rateLimitFactor = 0.3;
} else if (q_use.value === 'group') {
@ -269,7 +268,7 @@ const defaultPolicies = computed<Partial<Record<typeof ROLE_POLICIES[number], an
}
}
let userListLimit;
let userListLimit: Misskey.entities.RolePolicies['userListLimit'] | undefined;
if (q_use.value === 'single') {
userListLimit = 100;
} else if (q_use.value === 'group') {
@ -278,7 +277,7 @@ const defaultPolicies = computed<Partial<Record<typeof ROLE_POLICIES[number], an
userListLimit = 3;
}
let antennaLimit;
let antennaLimit: Misskey.entities.RolePolicies['antennaLimit'] | undefined;
if (q_use.value === 'single') {
antennaLimit = 100;
} else if (q_use.value === 'group') {
@ -287,7 +286,7 @@ const defaultPolicies = computed<Partial<Record<typeof ROLE_POLICIES[number], an
antennaLimit = 0;
}
let webhookLimit;
let webhookLimit: Misskey.entities.RolePolicies['webhookLimit'] | undefined;
if (q_use.value === 'single') {
webhookLimit = 100;
} else if (q_use.value === 'group') {
@ -296,35 +295,35 @@ const defaultPolicies = computed<Partial<Record<typeof ROLE_POLICIES[number], an
webhookLimit = 0;
}
let canImportFollowing;
let canImportFollowing: Misskey.entities.RolePolicies['canImportFollowing'];
if (q_use.value === 'single') {
canImportFollowing = true;
} else {
canImportFollowing = false;
}
let canImportMuting;
let canImportMuting: Misskey.entities.RolePolicies['canImportMuting'];
if (q_use.value === 'single') {
canImportMuting = true;
} else {
canImportMuting = false;
}
let canImportBlocking;
let canImportBlocking: Misskey.entities.RolePolicies['canImportBlocking'];
if (q_use.value === 'single') {
canImportBlocking = true;
} else {
canImportBlocking = false;
}
let canImportUserLists;
let canImportUserLists: Misskey.entities.RolePolicies['canImportUserLists'];
if (q_use.value === 'single') {
canImportUserLists = true;
} else {
canImportUserLists = false;
}
let canImportAntennas;
let canImportAntennas: Misskey.entities.RolePolicies['canImportAntennas'];
if (q_use.value === 'single') {
canImportAntennas = true;
} else {
@ -355,6 +354,7 @@ function applySettings() {
maintainerEmail: q_adminEmail.value === '' ? undefined : q_adminEmail.value,
}, props.token),
misskeyApi('admin/roles/update-default-policies', {
// @ts-expect-error
policies: defaultPolicies.value,
}, props.token),
]).then(() => {

View File

@ -69,9 +69,11 @@ import MkInfo from '@/components/MkInfo.vue';
const props = withDefaults(defineProps<{
message?: string,
openOnRemote?: OpenOnRemoteOptions,
initialUsername?: string;
}>(), {
message: '',
openOnRemote: undefined,
initialUsername: undefined,
});
const emit = defineEmits<{
@ -81,7 +83,7 @@ const emit = defineEmits<{
const host = toUnicode(configHost);
const username = ref('');
const username = ref(props.initialUsername ?? '');
//#region Open on remote
function openRemote(options: OpenOnRemoteOptions, targetHost?: string): void {

View File

@ -20,6 +20,7 @@ SPDX-License-Identifier: AGPL-3.0-only
key="input"
:message="message"
:openOnRemote="openOnRemote"
:initialUsername="initialUsername"
@usernameSubmitted="onUsernameSubmitted"
@passkeyClick="onPasskeyLogin"
@ -89,10 +90,12 @@ const props = withDefaults(defineProps<{
autoSet?: boolean;
message?: string,
openOnRemote?: OpenOnRemoteOptions,
initialUsername?: string;
}>(), {
autoSet: false,
message: '',
openOnRemote: undefined,
initialUsername: undefined,
});
const page = ref<'input' | 'password' | 'totp' | 'passkey'>('input');

View File

@ -16,7 +16,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<button :class="$style.closeButton" class="_button" @click="onClose"><i class="ti ti-x"></i></button>
</div>
<div :class="$style.content">
<MkSignin :autoSet="autoSet" :message="message" :openOnRemote="openOnRemote" @login="onLogin"/>
<MkSignin :autoSet="autoSet" :message="message" :openOnRemote="openOnRemote" :initialUsername="initialUsername" @login="onLogin"/>
</div>
</div>
</MkModal>
@ -34,10 +34,12 @@ withDefaults(defineProps<{
autoSet?: boolean;
message?: string,
openOnRemote?: OpenOnRemoteOptions,
initialUsername?: string;
}>(), {
autoSet: false,
message: '',
openOnRemote: undefined,
initialUsername: undefined,
});
const emit = defineEmits<{

View File

@ -66,7 +66,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<MkCaptcha v-if="instance.enableMcaptcha" ref="mcaptcha" v-model="mCaptchaResponse" :class="$style.captcha" provider="mcaptcha" :sitekey="instance.mcaptchaSiteKey" :instanceUrl="instance.mcaptchaInstanceUrl"/>
<MkCaptcha v-if="instance.enableRecaptcha" ref="recaptcha" v-model="reCaptchaResponse" :class="$style.captcha" provider="recaptcha" :sitekey="instance.recaptchaSiteKey"/>
<MkCaptcha v-if="instance.enableTurnstile" ref="turnstile" v-model="turnstileResponse" :class="$style.captcha" provider="turnstile" :sitekey="instance.turnstileSiteKey"/>
<MkCaptcha v-if="instance.enableTestcaptcha" ref="testcaptcha" v-model="testcaptchaResponse" :class="$style.captcha" provider="testcaptcha"/>
<MkCaptcha v-if="instance.enableTestcaptcha" ref="testcaptcha" v-model="testcaptchaResponse" :class="$style.captcha" provider="testcaptcha" :sitekey="null"/>
<MkButton type="submit" :disabled="shouldDisableSubmitting" large gradate rounded data-cy-signup-submit style="margin: 0 auto;">
<template v-if="submitting">
<MkLoading :em="true" :colored="false"/>

View File

@ -44,10 +44,10 @@ SPDX-License-Identifier: AGPL-3.0-only
<script lang="ts" setup>
import { onUnmounted, onMounted, computed, useTemplateRef, TransitionGroup, markRaw, watch } from 'vue';
import * as Misskey from 'misskey-js';
import { notificationTypes } from 'misskey-js';
import { useInterval } from '@@/js/use-interval.js';
import { useDocumentVisibility } from '@@/js/use-document-visibility.js';
import { getScrollContainer, scrollToTop } from '@@/js/scroll.js';
import type { notificationTypes } from '@@/js/const.js';
import XNotification from '@/components/MkNotification.vue';
import MkNote from '@/components/MkNote.vue';
import { useStream } from '@/stream.js';

View File

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

View File

@ -41,8 +41,7 @@ const props = defineProps<{
.img {
vertical-align: bottom;
height: 128px;
aspect-ratio: 1;
margin-bottom: 16px;
margin: auto auto 16px;
border-radius: 16px;
}

View File

@ -12,8 +12,11 @@ SPDX-License-Identifier: AGPL-3.0-only
</div>
<div v-else>
<div :class="$style.error">
<slot name="error" :error="error">
<div><i class="ti ti-alert-triangle"></i> {{ i18n.ts.somethingHappened }}</div>
<div v-if="error">{{ JSON.stringify(error) }}</div>
<MkButton inline style="margin-top: 16px;" @click="retry"><i class="ti ti-reload"></i> {{ i18n.ts.retry }}</MkButton>
</slot>
</div>
</div>
</template>
@ -27,15 +30,17 @@ const props = defineProps<{
p: () => Promise<T>;
}>();
const emit = defineEmits<{
(ev: 'resolved', result: T): void;
}>();
const pending = ref(true);
const resolved = ref(false);
const rejected = ref(false);
const result = ref<T | null>(null);
const error = ref<any | null>(null);
const process = () => {
if (props.p == null) {
return;
}
const promise = props.p();
pending.value = true;
resolved.value = false;
@ -44,10 +49,12 @@ const process = () => {
pending.value = false;
resolved.value = true;
result.value = _result;
emit('resolved', _result);
});
promise.catch(() => {
promise.catch((_error) => {
pending.value = false;
rejected.value = true;
error.value = _error;
});
};

View File

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

View File

@ -48,6 +48,7 @@ SPDX-License-Identifier: AGPL-3.0-only
</div>
<div v-else-if="cellType === 'image'">
<img
v-if="cell.value && typeof cell.value === 'string'"
:src="cell.value"
:alt="cell.value"
:class="$style.viewImage"

View File

@ -20,6 +20,7 @@ import NestedRouterView from './global/NestedRouterView.vue';
import StackingRouterView from './global/StackingRouterView.vue';
import MkLoading from './global/MkLoading.vue';
import MkError from './global/MkError.vue';
import MkSuspense from './global/MkSuspense.vue';
import MkAd from './global/MkAd.vue';
import MkPageHeader from './global/MkPageHeader.vue';
import MkStickyContainer from './global/MkStickyContainer.vue';
@ -60,6 +61,7 @@ export const components = {
MkUrl: MkUrl,
MkLoading: MkLoading,
MkError: MkError,
MkSuspense: MkSuspense,
MkAd: MkAd,
MkPageHeader: MkPageHeader,
MkStickyContainer: MkStickyContainer,
@ -94,6 +96,7 @@ declare module '@vue/runtime-core' {
MkUrl: typeof MkUrl;
MkLoading: typeof MkLoading;
MkError: typeof MkError;
MkSuspense: typeof MkSuspense;
MkAd: typeof MkAd;
MkPageHeader: typeof MkPageHeader;
MkStickyContainer: typeof MkStickyContainer;

View File

@ -15,7 +15,7 @@ import * as Misskey from 'misskey-js';
import MkMediaList from '@/components/MkMediaList.vue';
const props = defineProps<{
block: Misskey.entities.PageBlock,
block: Extract<Misskey.entities.PageBlock, { type: 'image' }>,
page: Misskey.entities.Page,
}>();

View File

@ -18,7 +18,7 @@ import MkNoteDetailed from '@/components/MkNoteDetailed.vue';
import { misskeyApi } from '@/utility/misskey-api.js';
const props = defineProps<{
block: Misskey.entities.PageBlock,
block: Extract<Misskey.entities.PageBlock, { type: 'note' }>,
page: Misskey.entities.Page,
}>();

View File

@ -29,7 +29,7 @@ import * as Misskey from 'misskey-js';
const XBlock = defineAsyncComponent(() => import('./page.block.vue'));
defineProps<{
block: Misskey.entities.PageBlock,
block: Extract<Misskey.entities.PageBlock, { type: 'section' }>,
h: number,
page: Misskey.entities.Page,
}>();

View File

@ -22,7 +22,7 @@ import { isEnabledUrlPreview } from '@/utility/url-preview.js';
const MkUrlPreview = defineAsyncComponent(() => import('@/components/MkUrlPreview.vue'));
const props = defineProps<{
block: Misskey.entities.PageBlock,
block: Extract<Misskey.entities.PageBlock, { type: 'text' }>,
page: Misskey.entities.Page,
}>();

View File

@ -51,3 +51,9 @@ export async function fetchInstance(force = false): Promise<Misskey.entities.Met
return instance;
}
export type ClientOptions = {
entrancePageStyle: 'classic' | 'simple';
showTimelineForVisitor: boolean;
showActivitiesForVisitor: boolean;
};

View File

@ -96,7 +96,7 @@ SPDX-License-Identifier: AGPL-3.0-only
</div>
</FormSection>
<FormSuspense v-slot="{ result: stats }" :p="initStats">
<MkSuspense v-slot="{ result: stats }" :p="initStats">
<FormSection>
<template #label>{{ i18n.ts.statistics }}</template>
<FormSplit>
@ -110,7 +110,7 @@ SPDX-License-Identifier: AGPL-3.0-only
</MkKeyValue>
</FormSplit>
</FormSection>
</FormSuspense>
</MkSuspense>
<FormSection>
<template #label>Well-known resources</template>
@ -134,7 +134,6 @@ import { misskeyApi } from '@/utility/misskey-api.js';
import FormLink from '@/components/form/link.vue';
import FormSection from '@/components/form/section.vue';
import FormSplit from '@/components/form/split.vue';
import FormSuspense from '@/components/form/suspense.vue';
import MkFolder from '@/components/MkFolder.vue';
import MkKeyValue from '@/components/MkKeyValue.vue';
import MkLink from '@/components/MkLink.vue';

View File

@ -0,0 +1,182 @@
<!--
SPDX-FileCopyrightText: syuilo and misskey-project
SPDX-License-Identifier: AGPL-3.0-only
-->
<template>
<PageWithHeader v-model:tab="tab" :actions="headerActions" :tabs="headerTabs">
<div v-if="file" class="_spacer" style="--MI_SPACER-w: 600px; --MI_SPACER-min: 16px; --MI_SPACER-max: 32px;">
<div v-if="tab === 'overview'" class="cxqhhsmd _gaps_m">
<a class="thumbnail" :href="file.url" target="_blank">
<MkDriveFileThumbnail class="thumbnail" :file="file" fit="contain"/>
</a>
<div>
<MkKeyValue :copy="file.type" oneline style="margin: 1em 0;">
<template #key>MIME Type</template>
<template #value><span class="_monospace">{{ file.type }}</span></template>
</MkKeyValue>
<MkKeyValue oneline style="margin: 1em 0;">
<template #key>Size</template>
<template #value><span class="_monospace">{{ bytes(file.size) }}</span></template>
</MkKeyValue>
<MkKeyValue :copy="file.id" oneline style="margin: 1em 0;">
<template #key>ID</template>
<template #value><span class="_monospace">{{ file.id }}</span></template>
</MkKeyValue>
<MkKeyValue :copy="file.md5" oneline style="margin: 1em 0;">
<template #key>MD5</template>
<template #value><span class="_monospace">{{ file.md5 }}</span></template>
</MkKeyValue>
<MkKeyValue oneline style="margin: 1em 0;">
<template #key>{{ i18n.ts.createdAt }}</template>
<template #value><span class="_monospace"><MkTime :time="file.createdAt" mode="detail" style="display: block;"/></span></template>
</MkKeyValue>
</div>
<MkA v-if="file.user" class="user" :to="`/admin/user/${file.user.id}`">
<MkUserCardMini :user="file.user"/>
</MkA>
<div>
<MkSwitch :modelValue="isSensitive" @update:modelValue="toggleSensitive">{{ i18n.ts.sensitive }}</MkSwitch>
</div>
<div>
<MkButton danger @click="del"><i class="ti ti-trash"></i> {{ i18n.ts.delete }}</MkButton>
</div>
</div>
<div v-else-if="tab === 'usage' && info" class="_gaps_m">
<MkTabs
v-model:tab="usageTab"
:tabs="[{
key: 'note',
title: 'Note',
}, {
key: 'chat',
title: 'Chat',
}]"
/>
<XNotes v-if="usageTab === 'note'" :fileId="props.file.id"/>
<XChat v-else-if="usageTab === 'chat'" :fileId="props.file.id"/>
</div>
<div v-else-if="tab === 'ip' && info" class="_gaps_m">
<MkInfo v-if="!iAmAdmin" warn>{{ i18n.ts.requireAdminForView }}</MkInfo>
<MkKeyValue v-if="info.requestIp" class="_monospace" :copy="info.requestIp" oneline>
<template #key>IP</template>
<template #value>{{ info.requestIp }}</template>
</MkKeyValue>
<FormSection v-if="info.requestHeaders">
<template #label>Headers</template>
<MkKeyValue v-for="(v, k) in info.requestHeaders" :key="k" class="_monospace">
<template #key>{{ k }}</template>
<template #value>{{ v }}</template>
</MkKeyValue>
</FormSection>
</div>
<div v-else-if="tab === 'raw'" class="_gaps_m">
<MkObjectView v-if="info" tall :value="info">
</MkObjectView>
</div>
</div>
</PageWithHeader>
</template>
<script lang="ts" setup>
import { computed, defineAsyncComponent, ref } from 'vue';
import * as Misskey from 'misskey-js';
import MkButton from '@/components/MkButton.vue';
import MkSwitch from '@/components/MkSwitch.vue';
import MkObjectView from '@/components/MkObjectView.vue';
import MkDriveFileThumbnail from '@/components/MkDriveFileThumbnail.vue';
import MkKeyValue from '@/components/MkKeyValue.vue';
import FormSection from '@/components/form/section.vue';
import MkUserCardMini from '@/components/MkUserCardMini.vue';
import MkInfo from '@/components/MkInfo.vue';
import bytes from '@/filters/bytes.js';
import * as os from '@/os.js';
import { i18n } from '@/i18n.js';
import { iAmAdmin, iAmModerator } from '@/i.js';
import MkTabs from '@/components/MkTabs.vue';
const props = defineProps<{
file: Misskey.entities.DriveFile,
info: Misskey.entities.AdminDriveShowFileResponse,
}>();
const tab = ref('overview');
const isSensitive = ref(props.file.isSensitive);
const usageTab = ref<'note' | 'chat'>('note');
const XNotes = defineAsyncComponent(() => import('./drive.file.notes.vue'));
const XChat = defineAsyncComponent(() => import('./admin-file.chat.vue'));
async function del() {
const { canceled } = await os.confirm({
type: 'warning',
text: i18n.tsx.removeAreYouSure({ x: props.file.name }),
});
if (canceled) return;
os.apiWithDialog('drive/files/delete', {
fileId: props.file.id,
});
}
async function toggleSensitive() {
const { canceled } = await os.confirm({
type: 'warning',
text: isSensitive.value ? i18n.ts.unmarkAsSensitiveConfirm : i18n.ts.markAsSensitiveConfirm,
});
if (canceled) return;
isSensitive.value = !isSensitive.value;
os.apiWithDialog('drive/files/update', {
fileId: props.file.id,
isSensitive: !props.file.isSensitive,
});
}
const headerActions = computed(() => [{
text: i18n.ts.openInNewTab,
icon: 'ti ti-external-link',
handler: () => {
window.open(props.file.url, '_blank', 'noopener');
},
}]);
const headerTabs = computed(() => [{
key: 'overview',
title: i18n.ts.overview,
icon: 'ti ti-info-circle',
}, iAmModerator ? {
key: 'usage',
title: i18n.ts._fileViewer.usage,
icon: 'ti ti-plus',
} : null, iAmModerator ? {
key: 'ip',
title: 'IP',
icon: 'ti ti-password',
} : null, {
key: 'raw',
title: 'Raw data',
icon: 'ti ti-code',
}].filter(x => x != null));
</script>
<style lang="scss" scoped>
.cxqhhsmd {
> .thumbnail {
display: block;
> .thumbnail {
height: 300px;
max-width: 100%;
}
}
> .user {
&:hover {
text-decoration: none;
}
}
}
</style>

View File

@ -4,197 +4,37 @@ SPDX-License-Identifier: AGPL-3.0-only
-->
<template>
<PageWithHeader v-model:tab="tab" :actions="headerActions" :tabs="headerTabs">
<div v-if="file" class="_spacer" style="--MI_SPACER-w: 600px; --MI_SPACER-min: 16px; --MI_SPACER-max: 32px;">
<div v-if="tab === 'overview'" class="cxqhhsmd _gaps_m">
<a class="thumbnail" :href="file.url" target="_blank">
<MkDriveFileThumbnail class="thumbnail" :file="file" fit="contain"/>
</a>
<div>
<MkKeyValue :copy="file.type" oneline style="margin: 1em 0;">
<template #key>MIME Type</template>
<template #value><span class="_monospace">{{ file.type }}</span></template>
</MkKeyValue>
<MkKeyValue oneline style="margin: 1em 0;">
<template #key>Size</template>
<template #value><span class="_monospace">{{ bytes(file.size) }}</span></template>
</MkKeyValue>
<MkKeyValue :copy="file.id" oneline style="margin: 1em 0;">
<template #key>ID</template>
<template #value><span class="_monospace">{{ file.id }}</span></template>
</MkKeyValue>
<MkKeyValue :copy="file.md5" oneline style="margin: 1em 0;">
<template #key>MD5</template>
<template #value><span class="_monospace">{{ file.md5 }}</span></template>
</MkKeyValue>
<MkKeyValue oneline style="margin: 1em 0;">
<template #key>{{ i18n.ts.createdAt }}</template>
<template #value><span class="_monospace"><MkTime :time="file.createdAt" mode="detail" style="display: block;"/></span></template>
</MkKeyValue>
</div>
<MkA v-if="file.user" class="user" :to="`/admin/user/${file.user.id}`">
<MkUserCardMini :user="file.user"/>
</MkA>
<div>
<MkSwitch :modelValue="isSensitive" @update:modelValue="toggleSensitive">{{ i18n.ts.sensitive }}</MkSwitch>
</div>
<div>
<MkButton danger @click="del"><i class="ti ti-trash"></i> {{ i18n.ts.delete }}</MkButton>
</div>
</div>
<div v-else-if="tab === 'usage' && info" class="_gaps_m">
<MkTabs
v-model:tab="usageTab"
:tabs="[{
key: 'note',
title: 'Note',
}, {
key: 'chat',
title: 'Chat',
}]"
/>
<XNotes v-if="usageTab === 'note'" :fileId="fileId"/>
<XChat v-else-if="usageTab === 'chat'" :fileId="fileId"/>
</div>
<div v-else-if="tab === 'ip' && info" class="_gaps_m">
<MkInfo v-if="!iAmAdmin" warn>{{ i18n.ts.requireAdminForView }}</MkInfo>
<MkKeyValue v-if="info.requestIp" class="_monospace" :copy="info.requestIp" oneline>
<template #key>IP</template>
<template #value>{{ info.requestIp }}</template>
</MkKeyValue>
<FormSection v-if="info.requestHeaders">
<template #label>Headers</template>
<MkKeyValue v-for="(v, k) in info.requestHeaders" :key="k" class="_monospace">
<template #key>{{ k }}</template>
<template #value>{{ v }}</template>
</MkKeyValue>
</FormSection>
</div>
<div v-else-if="tab === 'raw'" class="_gaps_m">
<MkObjectView v-if="info" tall :value="info">
</MkObjectView>
</div>
</div>
</PageWithHeader>
<MkSuspense v-slot="{ result }" :p="_fetch_" @resolved="(result) => file = result.file">
<XRoot v-if="result.file != null && result.info != null" :file="result.file" :info="result.info"/>
</MkSuspense>
</template>
<script lang="ts" setup>
import { computed, defineAsyncComponent, ref } from 'vue';
import { ref } from 'vue';
import * as Misskey from 'misskey-js';
import MkButton from '@/components/MkButton.vue';
import MkSwitch from '@/components/MkSwitch.vue';
import MkObjectView from '@/components/MkObjectView.vue';
import MkDriveFileThumbnail from '@/components/MkDriveFileThumbnail.vue';
import MkKeyValue from '@/components/MkKeyValue.vue';
import FormSection from '@/components/form/section.vue';
import MkUserCardMini from '@/components/MkUserCardMini.vue';
import MkInfo from '@/components/MkInfo.vue';
import bytes from '@/filters/bytes.js';
import * as os from '@/os.js';
import XRoot from './admin-file.root.vue';
import { misskeyApi } from '@/utility/misskey-api.js';
import { i18n } from '@/i18n.js';
import { definePage } from '@/page.js';
import { iAmAdmin, iAmModerator } from '@/i.js';
import MkTabs from '@/components/MkTabs.vue';
const tab = ref('overview');
const file = ref<Misskey.entities.DriveFile | null>(null);
const info = ref<Misskey.entities.AdminDriveShowFileResponse | null>(null);
const isSensitive = ref<boolean>(false);
const usageTab = ref<'note' | 'chat'>('note');
const XNotes = defineAsyncComponent(() => import('./drive.file.notes.vue'));
const XChat = defineAsyncComponent(() => import('./admin-file.chat.vue'));
const props = defineProps<{
fileId: string,
}>();
async function _fetch_() {
file.value = await misskeyApi('drive/files/show', { fileId: props.fileId });
info.value = await misskeyApi('admin/drive/show-file', { fileId: props.fileId });
isSensitive.value = file.value.isSensitive;
function _fetch_() {
return Promise.all([
misskeyApi('drive/files/show', { fileId: props.fileId }),
misskeyApi('admin/drive/show-file', { fileId: props.fileId }),
]).then((result) => ({
file: result[0],
info: result[1],
}));
}
_fetch_();
async function del() {
const { canceled } = await os.confirm({
type: 'warning',
text: i18n.tsx.removeAreYouSure({ x: file.value.name }),
});
if (canceled) return;
os.apiWithDialog('drive/files/delete', {
fileId: file.value.id,
});
}
async function toggleSensitive() {
if (!file.value) return;
const { canceled } = await os.confirm({
type: 'warning',
text: isSensitive.value ? i18n.ts.unmarkAsSensitiveConfirm : i18n.ts.markAsSensitiveConfirm,
});
if (canceled) return;
isSensitive.value = !isSensitive.value;
os.apiWithDialog('drive/files/update', {
fileId: file.value.id,
isSensitive: !file.value.isSensitive,
});
}
const headerActions = computed(() => [{
text: i18n.ts.openInNewTab,
icon: 'ti ti-external-link',
handler: () => {
window.open(file.value.url, '_blank', 'noopener');
},
}]);
const headerTabs = computed(() => [{
key: 'overview',
title: i18n.ts.overview,
icon: 'ti ti-info-circle',
}, iAmModerator ? {
key: 'usage',
title: i18n.ts._fileViewer.usage,
icon: 'ti ti-plus',
} : null, iAmModerator ? {
key: 'ip',
title: 'IP',
icon: 'ti ti-password',
} : null, {
key: 'raw',
title: 'Raw data',
icon: 'ti ti-code',
}].filter(x => x != null));
const file = ref<Misskey.entities.DriveFile | null>(null);
definePage(() => ({
title: file.value ? `${i18n.ts.file}: ${file.value.name}` : i18n.ts.file,
icon: 'ti ti-file',
}));
</script>
<style lang="scss" scoped>
.cxqhhsmd {
> .thumbnail {
display: block;
> .thumbnail {
height: 300px;
max-width: 100%;
}
}
> .user {
&:hover {
text-decoration: none;
}
}
}
</style>

View File

@ -152,6 +152,7 @@ SPDX-License-Identifier: AGPL-3.0-only
import { ref, computed } from 'vue';
import JSON5 from 'json5';
import { host } from '@@/js/config.js';
import type { ClientOptions } from '@/instance.js';
import MkInput from '@/components/MkInput.vue';
import MkTextarea from '@/components/MkTextarea.vue';
import * as os from '@/os.js';
@ -166,9 +167,13 @@ import MkSwitch from '@/components/MkSwitch.vue';
const meta = await misskeyApi('admin/meta');
const entrancePageStyle = ref(meta.clientOptions.entrancePageStyle ?? 'classic');
const showTimelineForVisitor = ref(meta.clientOptions.showTimelineForVisitor ?? true);
const showActivitiesForVisitor = ref(meta.clientOptions.showActivitiesForVisitor ?? true);
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
const entrancePageStyle = ref<ClientOptions['entrancePageStyle']>(meta.clientOptions.entrancePageStyle ?? 'classic');
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
const showTimelineForVisitor = ref<ClientOptions['showTimelineForVisitor']>(meta.clientOptions.showTimelineForVisitor ?? true);
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
const showActivitiesForVisitor = ref<ClientOptions['showActivitiesForVisitor']>(meta.clientOptions.showActivitiesForVisitor ?? true);
const iconUrl = ref(meta.iconUrl);
const app192IconUrl = ref(meta.app192IconUrl);
const app512IconUrl = ref(meta.app512IconUrl);
@ -186,11 +191,11 @@ const manifestJsonOverride = ref(meta.manifestJsonOverride === '' ? '{}' : JSON.
function save() {
os.apiWithDialog('admin/update-meta', {
clientOptions: {
clientOptions: ({
entrancePageStyle: entrancePageStyle.value,
showTimelineForVisitor: showTimelineForVisitor.value,
showActivitiesForVisitor: showActivitiesForVisitor.value,
},
} as ClientOptions) as any,
iconUrl: iconUrl.value,
app192IconUrl: app192IconUrl.value,
app512IconUrl: app512IconUrl.value,

View File

@ -6,19 +6,18 @@ SPDX-License-Identifier: AGPL-3.0-only
<template>
<PageWithHeader :actions="headerActions" :tabs="headerTabs">
<div class="_spacer" style="--MI_SPACER-w: 800px; --MI_SPACER-min: 16px; --MI_SPACER-max: 32px;">
<FormSuspense v-slot="{ result: database }" :p="databasePromiseFactory">
<MkSuspense v-slot="{ result: database }" :p="databasePromiseFactory">
<MkKeyValue v-for="table in database" :key="table[0]" oneline style="margin: 1em 0;">
<template #key>{{ table[0] }}</template>
<template #value>{{ bytes(table[1].size) }} ({{ number(table[1].count) }} recs)</template>
</MkKeyValue>
</FormSuspense>
</MkSuspense>
</div>
</PageWithHeader>
</template>
<script lang="ts" setup>
import { computed } from 'vue';
import FormSuspense from '@/components/form/suspense.vue';
import MkKeyValue from '@/components/MkKeyValue.vue';
import { misskeyApi } from '@/utility/misskey-api.js';
import bytes from '@/filters/bytes.js';

View File

@ -107,7 +107,7 @@ const smtpPass = ref(meta.smtpPass);
async function testEmail() {
const { canceled, result: destination } = await os.inputText({
title: i18n.ts.destination,
title: 'To',
type: 'email',
default: instance.maintainerEmail ?? '',
placeholder: 'test@example.com',

View File

@ -81,10 +81,10 @@ function onStats(stats: Misskey.entities.QueueStats) {
delayed.value = stats[props.domain].delayed;
waiting.value = stats[props.domain].waiting;
chartProcess.value.pushData(stats[props.domain].activeSincePrevTick);
chartActive.value.pushData(stats[props.domain].active);
chartDelayed.value.pushData(stats[props.domain].delayed);
chartWaiting.value.pushData(stats[props.domain].waiting);
if (chartProcess.value != null) chartProcess.value.pushData(stats[props.domain].activeSincePrevTick);
if (chartActive.value != null) chartActive.value.pushData(stats[props.domain].active);
if (chartDelayed.value != null) chartDelayed.value.pushData(stats[props.domain].delayed);
if (chartWaiting.value != null) chartWaiting.value.pushData(stats[props.domain].waiting);
}
function onStatsLog(statsLog: Misskey.entities.QueueStatsLog) {
@ -100,10 +100,10 @@ function onStatsLog(statsLog: Misskey.entities.QueueStatsLog) {
dataWaiting.push(stats[props.domain].waiting);
}
chartProcess.value.setData(dataProcess);
chartActive.value.setData(dataActive);
chartDelayed.value.setData(dataDelayed);
chartWaiting.value.setData(dataWaiting);
if (chartProcess.value != null) chartProcess.value.setData(dataProcess);
if (chartActive.value != null) chartActive.value.setData(dataActive);
if (chartDelayed.value != null) chartDelayed.value.setData(dataDelayed);
if (chartWaiting.value != null) chartWaiting.value.setData(dataWaiting);
}
onMounted(() => {

View File

@ -109,7 +109,6 @@ SPDX-License-Identifier: AGPL-3.0-only
import { ref, computed } from 'vue';
import MkSwitch from '@/components/MkSwitch.vue';
import MkInput from '@/components/MkInput.vue';
import FormSuspense from '@/components/form/suspense.vue';
import FormSplit from '@/components/form/split.vue';
import * as os from '@/os.js';
import { misskeyApi } from '@/utility/misskey-api.js';

View File

@ -37,6 +37,8 @@ async function renderChart() {
chartInstance.destroy();
}
if (chartEl.value == null) return;
const getDate = (ago: number) => {
const y = now.getFullYear();
const m = now.getMonth();
@ -105,7 +107,6 @@ async function renderChart() {
type: 'time',
offset: true,
time: {
stepSize: 1,
unit: 'day',
displayFormats: {
day: 'M/d',
@ -149,7 +150,9 @@ async function renderChart() {
},
external: externalTooltipHandler,
},
...({ // TS
gradient,
}),
},
},
plugins: [chartVLine(vLineColor)],

View File

@ -42,6 +42,9 @@ const { handler: externalTooltipHandler } = useChartTooltip();
const { handler: externalTooltipHandler2 } = useChartTooltip();
onMounted(async () => {
if (chartEl.value == null) return;
if (chartEl2.value == null) return;
const now = isChromatic() ? new Date('2024-08-31T10:00:00Z') : new Date();
const getDate = (ago: number) => {
@ -122,7 +125,6 @@ onMounted(async () => {
stacked: true,
offset: false,
time: {
stepSize: 1,
unit: 'day',
},
grid: {
@ -144,7 +146,7 @@ onMounted(async () => {
ticks: {
display: true,
//mirror: true,
callback: (value, index, values) => value < 0 ? -value : value,
callback: (value, index, values) => (value as number) < 0 ? -value : value,
},
},
},
@ -173,7 +175,9 @@ onMounted(async () => {
label: context => `${context.dataset.label}: ${Math.abs(context.parsed.y)}`,
},
},
...({ // TS
gradient,
}),
},
},
plugins: [chartVLine(vLineColor)],
@ -213,7 +217,6 @@ onMounted(async () => {
type: 'time',
offset: false,
time: {
stepSize: 1,
unit: 'day',
displayFormats: {
day: 'M/d',
@ -260,7 +263,9 @@ onMounted(async () => {
},
external: externalTooltipHandler2,
},
...({ // TS
gradient,
}),
},
},
plugins: [chartVLine(vLineColor)],

View File

@ -828,8 +828,9 @@ SPDX-License-Identifier: AGPL-3.0-only
<script lang="ts" setup>
import { watch, ref, computed } from 'vue';
import { throttle } from 'throttle-debounce';
import { ROLE_POLICIES } from '@@/js/const.js';
import * as Misskey from 'misskey-js';
import RolesEditorFormula from './RolesEditorFormula.vue';
import type { GetMkSelectValueTypesFromDef, MkSelectItem } from '@/components/MkSelect.vue';
import MkInput from '@/components/MkInput.vue';
import MkColorInput from '@/components/MkColorInput.vue';
import MkSelect from '@/components/MkSelect.vue';
@ -854,7 +855,7 @@ const props = defineProps<{
const role = ref(deepClone(props.modelValue));
// fill missing policy
for (const ROLE_POLICY of ROLE_POLICIES) {
for (const ROLE_POLICY of Misskey.rolePolicies) {
if (role.value.policies[ROLE_POLICY] == null) {
role.value.policies[ROLE_POLICY] = {
useDefault: true,

View File

@ -331,7 +331,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<script lang="ts" setup>
import { computed, reactive, ref } from 'vue';
import { ROLE_POLICIES } from '@@/js/const.js';
import * as Misskey from 'misskey-js';
import MkInput from '@/components/MkInput.vue';
import MkFolder from '@/components/MkFolder.vue';
import MkSwitch from '@/components/MkSwitch.vue';
@ -353,8 +353,8 @@ const baseRoleQ = ref('');
const roles = await misskeyApi('admin/roles/list');
const policies = reactive<Record<typeof ROLE_POLICIES[number], any>>({});
for (const ROLE_POLICY of ROLE_POLICIES) {
const policies = reactive<Record<typeof Misskey.rolePolicies[number], any>>({});
for (const ROLE_POLICY of Misskey.rolePolicies) {
policies[ROLE_POLICY] = instance.policies[ROLE_POLICY];
}

View File

@ -194,7 +194,7 @@ const sensitiveMediaDetectionForm = useForm({
state.sensitiveMediaDetectionSensitivity === 2 ? 'medium' :
state.sensitiveMediaDetectionSensitivity === 3 ? 'high' :
state.sensitiveMediaDetectionSensitivity === 4 ? 'veryHigh' :
0,
null as never,
setSensitiveFlagAutomatically: state.setSensitiveFlagAutomatically,
enableSensitiveMediaDetectionForVideos: state.enableSensitiveMediaDetectionForVideos,
});

View File

@ -138,9 +138,10 @@ async function addPinnedNote() {
const { canceled, result: value } = await os.inputText({
title: i18n.ts.noteIdOrUrl,
});
if (canceled) return;
if (canceled || value == null) return;
const fromUrl = value.includes('/') ? value.split('/').pop() : null;
const note = await os.apiWithDialog('notes/show', {
noteId: value.includes('/') ? value.split('/').pop() : value,
noteId: fromUrl ?? value,
});
pinnedNotes.value = [{
id: note.id,

View File

@ -48,8 +48,8 @@ SPDX-License-Identifier: AGPL-3.0-only
<div><b>{{ i18n.tsx.lastNDays({ n: 7 }) }} {{ i18n.ts.ranking }}</b> ({{ gameMode.toUpperCase() }})</div>
<div v-if="ranking" class="_gaps_s">
<div v-for="r in ranking" :key="r.id" :class="$style.rankingRecord">
<MkAvatar :link="true" style="width: 24px; height: 24px; margin-right: 4px;" :user="r.user"/>
<MkUserName :user="r.user" :nowrap="true"/>
<MkAvatar v-if="r.user" :link="true" style="width: 24px; height: 24px; margin-right: 4px;" :user="r.user"/>
<MkUserName v-if="r.user" :user="r.user" :nowrap="true"/>
<b style="margin-left: auto;">{{ r.score.toLocaleString() }} {{ getScoreUnit(gameMode) }}</b>
</div>
</div>
@ -87,6 +87,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<script lang="ts" setup>
import { computed, ref, watch } from 'vue';
import * as Misskey from 'misskey-js';
import XGame from './drop-and-fusion.game.vue';
import { definePage } from '@/page.js';
import MkButton from '@/components/MkButton.vue';
@ -98,7 +99,7 @@ import { misskeyApiGet } from '@/utility/misskey-api.js';
const gameMode = ref<'normal' | 'square' | 'yen' | 'sweets' | 'space'>('normal');
const gameStarted = ref(false);
const mute = ref(false);
const ranking = ref(null);
const ranking = ref<Misskey.entities.BubbleGameRankingResponse | null>(null);
watch(gameMode, async () => {
ranking.value = await misskeyApiGet('bubble-game/ranking', { gameMode: gameMode.value });

View File

@ -36,7 +36,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<MkInput v-model="name" pattern="[a-z0-9_]" autocapitalize="off">
<template #label>{{ i18n.ts.name }}</template>
</MkInput>
<MkInput v-model="category" :datalist="customEmojiCategories">
<MkInput v-model="category" :datalist="customEmojiCategories.filter(x => x != null)">
<template #label>{{ i18n.ts.category }}</template>
</MkInput>
<MkInput v-model="aliases" autocapitalize="off">

View File

@ -0,0 +1,145 @@
<!--
SPDX-FileCopyrightText: syuilo and misskey-project
SPDX-License-Identifier: AGPL-3.0-only
-->
<template>
<PageWithHeader :actions="headerActions" :tabs="headerTabs">
<div class="_spacer" style="--MI_SPACER-w: 800px; --MI_SPACER-min: 16px; --MI_SPACER-max: 32px;">
<MkInput v-model="title">
<template #label>{{ i18n.ts.title }}</template>
</MkInput>
<MkTextarea v-model="description" :max="500">
<template #label>{{ i18n.ts.description }}</template>
</MkTextarea>
<div class="_gaps_s">
<div v-for="file in files" :key="file.id" class="wqugxsfx" :style="{ backgroundImage: file ? `url(${ file.thumbnailUrl })` : '' }">
<div class="name">{{ file.name }}</div>
<button v-tooltip="i18n.ts.remove" class="remove _button" @click="remove(file)"><i class="ti ti-x"></i></button>
</div>
<MkButton primary @click="chooseFile"><i class="ti ti-plus"></i> {{ i18n.ts.attachFile }}</MkButton>
</div>
<MkSwitch v-model="isSensitive">{{ i18n.ts.markAsSensitive }}</MkSwitch>
<div class="_buttons">
<MkButton v-if="props.post != null" primary @click="save"><i class="ti ti-device-floppy"></i> {{ i18n.ts.save }}</MkButton>
<MkButton v-else primary @click="save"><i class="ti ti-device-floppy"></i> {{ i18n.ts.publish }}</MkButton>
<MkButton v-if="props.post != null" danger @click="del"><i class="ti ti-trash"></i> {{ i18n.ts.delete }}</MkButton>
</div>
</div>
</PageWithHeader>
</template>
<script lang="ts" setup>
import { computed, watch, ref } from 'vue';
import * as Misskey from 'misskey-js';
import MkButton from '@/components/MkButton.vue';
import MkInput from '@/components/MkInput.vue';
import MkTextarea from '@/components/MkTextarea.vue';
import MkSwitch from '@/components/MkSwitch.vue';
import { selectFile } from '@/utility/drive.js';
import * as os from '@/os.js';
import { misskeyApi } from '@/utility/misskey-api.js';
import { i18n } from '@/i18n.js';
import { useRouter } from '@/router.js';
const router = useRouter();
const props = defineProps<{
post: Misskey.entities.GalleryPost | null;
}>();
const files = ref(props.post?.files ?? []);
const description = ref(props.post?.description ?? null);
const title = ref(props.post?.title ?? '');
const isSensitive = ref(props.post?.isSensitive ?? false);
function chooseFile(evt) {
selectFile({
anchorElement: evt.currentTarget ?? evt.target,
multiple: true,
}).then(selected => {
files.value = files.value.concat(selected);
});
}
function remove(file) {
files.value = files.value.filter(f => f.id !== file.id);
}
async function save() {
if (props.post != null) {
await os.apiWithDialog('gallery/posts/update', {
postId: props.post.id,
title: title.value,
description: description.value,
fileIds: files.value.map(file => file.id),
isSensitive: isSensitive.value,
});
router.push('/gallery/:postId', {
params: {
postId: props.post.id,
},
});
} else {
const created = await os.apiWithDialog('gallery/posts/create', {
title: title.value,
description: description.value,
fileIds: files.value.map(file => file.id),
isSensitive: isSensitive.value,
});
router.push('/gallery/:postId', {
params: {
postId: created.id,
},
});
}
}
async function del() {
if (props.post == null) return;
const { canceled } = await os.confirm({
type: 'warning',
text: i18n.ts.deleteConfirm,
});
if (canceled) return;
await os.apiWithDialog('gallery/posts/delete', {
postId: props.post.id,
});
router.push('/gallery');
}
const headerActions = computed(() => []);
const headerTabs = computed(() => []);
</script>
<style lang="scss" scoped>
.wqugxsfx {
height: 200px;
background-size: contain;
background-position: center;
background-repeat: no-repeat;
position: relative;
> .name {
position: absolute;
top: 8px;
left: 9px;
padding: 8px;
background: var(--MI_THEME-panel);
}
> .remove {
position: absolute;
top: 8px;
right: 9px;
padding: 8px;
background: var(--MI_THEME-panel);
}
}
</style>

View File

@ -4,162 +4,35 @@ SPDX-License-Identifier: AGPL-3.0-only
-->
<template>
<PageWithHeader :actions="headerActions" :tabs="headerTabs">
<div class="_spacer" style="--MI_SPACER-w: 800px; --MI_SPACER-min: 16px; --MI_SPACER-max: 32px;">
<FormSuspense :p="init" class="_gaps">
<MkInput v-model="title">
<template #label>{{ i18n.ts.title }}</template>
</MkInput>
<MkTextarea v-model="description" :max="500">
<template #label>{{ i18n.ts.description }}</template>
</MkTextarea>
<div class="_gaps_s">
<div v-for="file in files" :key="file.id" class="wqugxsfx" :style="{ backgroundImage: file ? `url(${ file.thumbnailUrl })` : '' }">
<div class="name">{{ file.name }}</div>
<button v-tooltip="i18n.ts.remove" class="remove _button" @click="remove(file)"><i class="ti ti-x"></i></button>
</div>
<MkButton primary @click="chooseFile"><i class="ti ti-plus"></i> {{ i18n.ts.attachFile }}</MkButton>
</div>
<MkSwitch v-model="isSensitive">{{ i18n.ts.markAsSensitive }}</MkSwitch>
<div class="_buttons">
<MkButton v-if="postId" primary @click="save"><i class="ti ti-device-floppy"></i> {{ i18n.ts.save }}</MkButton>
<MkButton v-else primary @click="save"><i class="ti ti-device-floppy"></i> {{ i18n.ts.publish }}</MkButton>
<MkButton v-if="postId" danger @click="del"><i class="ti ti-trash"></i> {{ i18n.ts.delete }}</MkButton>
</div>
</FormSuspense>
</div>
</PageWithHeader>
<MkSuspense v-slot="{ result }" :p="_fetch_">
<XRoot :post="result"/>
</MkSuspense>
</template>
<script lang="ts" setup>
import { computed, watch, ref } from 'vue';
import { ref } from 'vue';
import * as Misskey from 'misskey-js';
import MkButton from '@/components/MkButton.vue';
import MkInput from '@/components/MkInput.vue';
import MkTextarea from '@/components/MkTextarea.vue';
import MkSwitch from '@/components/MkSwitch.vue';
import FormSuspense from '@/components/form/suspense.vue';
import { selectFile } from '@/utility/drive.js';
import * as os from '@/os.js';
import XRoot from './edit.root.vue';
import { misskeyApi } from '@/utility/misskey-api.js';
import { definePage } from '@/page.js';
import { i18n } from '@/i18n.js';
import { useRouter } from '@/router.js';
const router = useRouter();
import { definePage } from '@/page.js';
const props = defineProps<{
postId?: string;
postId?: string,
}>();
const init = ref<(() => Promise<any>) | null>(null);
const files = ref<Misskey.entities.DriveFile[]>([]);
const description = ref<string | null>(null);
const title = ref<string | null>(null);
const isSensitive = ref(false);
function chooseFile(evt) {
selectFile({
anchorElement: evt.currentTarget ?? evt.target,
multiple: true,
}).then(selected => {
files.value = files.value.concat(selected);
});
}
function remove(file) {
files.value = files.value.filter(f => f.id !== file.id);
}
async function save() {
if (props.postId) {
await os.apiWithDialog('gallery/posts/update', {
postId: props.postId,
title: title.value,
description: description.value,
fileIds: files.value.map(file => file.id),
isSensitive: isSensitive.value,
});
router.push('/gallery/:postId', {
params: {
postId: props.postId,
},
});
function _fetch_() {
if (props.postId == null) {
return Promise.resolve(null);
} else {
const created = await os.apiWithDialog('gallery/posts/create', {
title: title.value,
description: description.value,
fileIds: files.value.map(file => file.id),
isSensitive: isSensitive.value,
});
router.push('/gallery/:postId', {
params: {
postId: created.id,
},
return misskeyApi('gallery/posts/show', {
postId: props.postId,
});
}
}
async function del() {
const { canceled } = await os.confirm({
type: 'warning',
text: i18n.ts.deleteConfirm,
});
if (canceled) return;
await os.apiWithDialog('gallery/posts/delete', {
postId: props.postId,
});
router.push('/gallery');
}
watch(() => props.postId, () => {
init.value = () => props.postId ? misskeyApi('gallery/posts/show', {
postId: props.postId,
}).then(post => {
files.value = post.files ?? [];
title.value = post.title;
description.value = post.description;
isSensitive.value = post.isSensitive;
}) : Promise.resolve(null);
}, { immediate: true });
const headerActions = computed(() => []);
const headerTabs = computed(() => []);
definePage(() => ({
title: props.postId ? i18n.ts.edit : i18n.ts.postToGallery,
icon: 'ti ti-pencil',
}));
</script>
<style lang="scss" scoped>
.wqugxsfx {
height: 200px;
background-size: contain;
background-position: center;
background-repeat: no-repeat;
position: relative;
> .name {
position: absolute;
top: 8px;
left: 9px;
padding: 8px;
background: var(--MI_THEME-panel);
}
> .remove {
position: absolute;
top: 8px;
right: 9px;
padding: 8px;
background: var(--MI_THEME-panel);
}
}
</style>

View File

@ -21,7 +21,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<script lang="ts" setup>
import { computed, markRaw, ref } from 'vue';
import { notificationTypes } from '@@/js/const.js';
import { notificationTypes } from 'misskey-js';
import MkStreamingNotificationsTimeline from '@/components/MkStreamingNotificationsTimeline.vue';
import MkNotesTimeline from '@/components/MkNotesTimeline.vue';
import * as os from '@/os.js';

View File

@ -24,8 +24,8 @@ SPDX-License-Identifier: AGPL-3.0-only
import { defineAsyncComponent, inject, onMounted, watch, ref } from 'vue';
import * as Misskey from 'misskey-js';
import { genId } from '@/utility/id.js';
import XContainer from '../page-editor.container.vue';
import { genId } from '@/utility/id.js';
import * as os from '@/os.js';
import { i18n } from '@/i18n.js';
import { deepClone } from '@/utility/clone.js';
@ -35,11 +35,11 @@ import { getPageBlockList } from '@/pages/page-editor/common.js';
const XBlocks = defineAsyncComponent(() => import('../page-editor.blocks.vue'));
const props = defineProps<{
modelValue: Misskey.entities.PageBlock & { type: 'section'; },
modelValue: Extract<Misskey.entities.PageBlock, { type: 'section'; }>,
}>();
const emit = defineEmits<{
(ev: 'update:modelValue', value: Misskey.entities.PageBlock & { type: 'section' }): void;
(ev: 'update:modelValue', value: Extract<Misskey.entities.PageBlock, { type: 'section'; }>): void;
(ev: 'remove'): void;
}>();
@ -59,7 +59,7 @@ async function rename() {
title: i18n.ts._pages.enterSectionTitle,
default: props.modelValue.title,
});
if (canceled) return;
if (canceled || title == null) return;
emit('update:modelValue', {
...props.modelValue,
title,

View File

@ -135,7 +135,7 @@ const emit = defineEmits<{
const dialog = useTemplateRef('dialog');
const page = ref(0);
const token = ref<string | number | null>(null);
const token = ref<string | null>(null);
const backupCodes = ref<string[]>();
function cancel() {
@ -145,7 +145,7 @@ function cancel() {
async function tokenDone() {
if (token.value == null) return;
const res = await os.apiWithDialog('i/2fa/done', {
token: typeof token.value === 'string' ? token.value : token.value.toString(),
token: token.value.toString(), // numbertoString
});
backupCodes.value = res.backupCodes;

View File

@ -65,7 +65,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<script lang="ts" setup>
import { useTemplateRef, computed } from 'vue';
import { notificationTypes } from '@@/js/const.js';
import { notificationTypes } from 'misskey-js';
import XNotificationConfig from './notifications.notification-config.vue';
import type { NotificationConfig } from './notifications.notification-config.vue';
import FormLink from '@/components/form/link.vue';

View File

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

View File

@ -110,7 +110,6 @@ SPDX-License-Identifier: AGPL-3.0-only
</MkPreferenceContainer>
</SearchMarker>
<!--
<SearchMarker :keywords="['auto', 'load', 'auto', 'more', 'scroll']">
<MkPreferenceContainer k="enableInfiniteScroll">
<MkSwitch v-model="enableInfiniteScroll">
@ -118,7 +117,6 @@ SPDX-License-Identifier: AGPL-3.0-only
</MkSwitch>
</MkPreferenceContainer>
</SearchMarker>
-->
</div>
<SearchMarker :keywords="['emoji', 'style', 'native', 'system', 'fluent', 'twemoji']">

View File

@ -17,8 +17,8 @@ SPDX-License-Identifier: AGPL-3.0-only
<script lang="ts" setup>
import { onMounted, ref, computed } from 'vue';
import * as Misskey from 'misskey-js';
import { genId } from '@/utility/id.js';
import XStatusbar from './statusbar.statusbar.vue';
import { genId } from '@/utility/id.js';
import MkFolder from '@/components/MkFolder.vue';
import MkButton from '@/components/MkButton.vue';
import { misskeyApi } from '@/utility/misskey-api.js';
@ -39,6 +39,7 @@ onMounted(() => {
async function add() {
prefer.commit('statusbars', [...statusbars.value, {
id: genId(),
name: null,
type: null,
black: false,
size: 'medium',

View File

@ -117,7 +117,6 @@ async function renderChart() {
offset: true,
stacked: true,
time: {
stepSize: 1,
unit: 'day',
displayFormats: {
day: 'M/d',
@ -162,7 +161,9 @@ async function renderChart() {
},
external: externalTooltipHandler,
},
...({ // TS
gradient,
}),
},
},
plugins: [chartVLine(vLineColor), chartLegend(legendEl.value)],

View File

@ -116,7 +116,6 @@ async function renderChart() {
offset: true,
stacked: true,
time: {
stepSize: 1,
unit: 'day',
displayFormats: {
day: 'M/d',
@ -161,7 +160,9 @@ async function renderChart() {
},
external: externalTooltipHandler,
},
...({ // TS
gradient,
}),
},
},
plugins: [chartVLine(vLineColor), chartLegend(legendEl.value)],

View File

@ -117,7 +117,6 @@ async function renderChart() {
offset: true,
stacked: true,
time: {
stepSize: 1,
unit: 'day',
displayFormats: {
day: 'M/d',
@ -155,8 +154,6 @@ async function renderChart() {
display: true,
text: 'Unique/Natural PV',
padding: {
left: 0,
right: 0,
top: 0,
bottom: 12,
},
@ -172,7 +169,9 @@ async function renderChart() {
},
external: externalTooltipHandler,
},
...({ // TS
gradient,
}),
},
},
plugins: [chartVLine(vLineColor), chartLegend(legendEl.value)],

View File

@ -209,7 +209,7 @@ const XTimeline = defineAsyncComponent(() => import('./index.timeline.vue'));
const props = withDefaults(defineProps<{
user: Misskey.entities.UserDetailed;
/** Test only; MkNotesTimeline currently causes problems in vitest */
disableNotes: boolean;
disableNotes?: boolean;
}>(), {
disableNotes: false,
});

View File

@ -34,7 +34,7 @@ import { instance as meta } from '@/instance.js';
position: fixed;
top: 0;
right: 0;
width: 80vw; // 100%shape
width: 100vw;
height: 100vh;
}

View File

@ -205,13 +205,13 @@ type HandlerDef = {
handler: (note: Misskey.entities.Note) => void;
};
note_view_interruptor: {
handler: (note: Misskey.entities.Note) => unknown;
handler: (note: Misskey.entities.Note) => Misskey.entities.Note | null;
};
note_post_interruptor: {
handler: (note: FIXME) => unknown;
};
page_view_interruptor: {
handler: (page: Misskey.entities.Page) => unknown;
handler: (page: Misskey.entities.Page) => Misskey.entities.Page;
};
};

View File

@ -32,6 +32,15 @@ export type SoundStore = {
volume: number;
};
export type StatusbarStore = {
name: string | null;
id: string;
type: string | null;
size: 'verySmall' | 'small' | 'medium' | 'large' | 'veryLarge';
black: boolean;
props: Record<string, any>;
};
type OmitStrict<T, K extends keyof T> = T extends any ? Pick<T, Exclude<keyof T, K>> : never;
// NOTE: デフォルト値は他の設定の状態に依存してはならない(依存していた場合、ユーザーがその設定項目単体で「初期値にリセット」した場合不具合の原因になる)
@ -182,14 +191,7 @@ export const PREF_DEF = definePreferences({
],
},
statusbars: {
default: [] as {
name: string;
id: string;
type: string;
size: 'verySmall' | 'small' | 'medium' | 'large' | 'veryLarge';
black: boolean;
props: Record<string, any>;
}[],
default: [] as StatusbarStore[],
},
serverDisconnectedBehavior: {
default: 'quiet' as 'quiet' | 'reload' | 'dialog',

View File

@ -6,9 +6,11 @@
import type { Plugin } from 'chart.js';
import MkChartLegend from '@/components/MkChartLegend.vue';
export const chartLegend = (legend: InstanceType<typeof MkChartLegend>) => ({
export const chartLegend = (legend: InstanceType<typeof MkChartLegend> | null | undefined) => ({
id: 'htmlLegend',
afterUpdate(chart, args, options) {
if (legend == null) return;
// Reuse the built-in legendItems generator
const items = chart.options.plugins!.legend!.labels!.generateLabels!(chart);

View File

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

View File

@ -7,25 +7,26 @@ SPDX-License-Identifier: AGPL-3.0-only
<MkContainer :showHeader="widgetProps.showHeader" class="mkw-aiscriptApp">
<template #header>App</template>
<div :class="$style.root">
<MkAsUi v-if="root" :component="root" :components="components" size="small"/>
<div v-if="isSyntaxError">Syntax error :(</div>
<MkAsUi v-else-if="root" :component="root" :components="components" size="small"/>
</div>
</MkContainer>
</template>
<script lang="ts" setup>
import { onMounted, ref, watch } from 'vue';
import type { Ref } from 'vue';
import { Interpreter, Parser } from '@syuilo/aiscript';
import { useWidgetPropsManager } from './widget.js';
import type { Ref } from 'vue';
import type { WidgetComponentEmits, WidgetComponentExpose, WidgetComponentProps } from './widget.js';
import type { FormWithDefault, GetFormResultType } from '@/utility/form.js';
import type { AsUiComponent, AsUiRoot } from '@/aiscript/ui.js';
import * as os from '@/os.js';
import { aiScriptReadline, createAiScriptEnv } from '@/aiscript/api.js';
import { $i } from '@/i.js';
import MkAsUi from '@/components/MkAsUi.vue';
import MkContainer from '@/components/MkContainer.vue';
import { registerAsUiLib } from '@/aiscript/ui.js';
import type { AsUiComponent, AsUiRoot } from '@/aiscript/ui.js';
const name = 'aiscriptApp';
@ -56,8 +57,11 @@ const parser = new Parser();
const root = ref<AsUiRoot>();
const components = ref<Ref<AsUiComponent>[]>([]);
const isSyntaxError = ref(false);
async function run() {
isSyntaxError.value = false;
const aiscript = new Interpreter({
...createAiScriptEnv({
storageKey: 'widget',
@ -80,10 +84,7 @@ async function run() {
try {
ast = parser.parse(widgetProps.script);
} catch (err) {
os.alert({
type: 'error',
text: 'Syntax error :(',
});
isSyntaxError.value = true;
return;
}
try {

View File

@ -52,7 +52,7 @@ const { widgetProps, configure } = useWidgetPropsManager(name,
const parser = new Parser();
const run = async () => {
async function run() {
const aiscript = new Interpreter(createAiScriptEnv({
storageKey: 'widget',
token: $i?.token,
@ -84,7 +84,7 @@ const run = async () => {
text: err instanceof Error ? err.message : String(err),
});
}
};
}
defineExpose<WidgetComponentExpose>({
name,

View File

@ -17,8 +17,8 @@ SPDX-License-Identifier: AGPL-3.0-only
<script lang="ts" setup>
import { defineAsyncComponent } from 'vue';
import type { notificationTypes as notificationTypes_typeReferenceOnly } from '@@/js/const.js';
import { useWidgetPropsManager } from './widget.js';
import type { notificationTypes as notificationTypes_typeReferenceOnly } from 'misskey-js';
import type { WidgetComponentEmits, WidgetComponentExpose, WidgetComponentProps } from './widget.js';
import type { FormWithDefault, GetFormResultType } from '@/utility/form.js';
import MkContainer from '@/components/MkContainer.vue';

View File

@ -31,7 +31,7 @@ import { ref, watch, computed } from 'vue';
import * as Misskey from 'misskey-js';
import { useWidgetPropsManager } from './widget.js';
import type { WidgetComponentEmits, WidgetComponentExpose, WidgetComponentProps } from './widget.js';
import MarqueeText from '@/components/MkMarqueeText.vue';
import MkMarqueeText from '@/components/MkMarqueeText.vue';
import type { FormWithDefault, GetFormResultType } from '@/utility/form.js';
import MkContainer from '@/components/MkContainer.vue';
import { shuffle } from '@/utility/shuffle.js';

View File

@ -22,6 +22,7 @@
"isolatedModules": true,
"useDefineForClassFields": true,
"verbatimModuleSyntax": true,
"skipLibCheck": true,
"baseUrl": ".",
"paths": {
"@/*": ["./src/*"],
@ -46,8 +47,6 @@
},
"compileOnSave": false,
"include": [
"./build.ts",
"./lib/**/*.ts",
"./src/**/*.ts",
"./src/**/*.vue",
"./test/**/*.ts",
@ -55,7 +54,6 @@
"./@types/**/*.ts"
],
"exclude": [
".storybook/**/*",
"./src/**/*.stories.ts"
]
}

View File

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

View File

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

View File

@ -1449,6 +1449,10 @@ export type Endpoints = Overwrite<Endpoints_2, {
}>;
res: AdminRolesCreateResponse;
};
'clear-browser-cache': {
req: EmptyRequest;
res: EmptyResponse;
};
}>;
// @public (undocumented)
@ -3210,7 +3214,7 @@ type Notification_2 = components['schemas']['Notification'];
type NotificationsCreateRequest = operations['notifications___create']['requestBody']['content']['application/json'];
// @public (undocumented)
export const notificationTypes: readonly ["note", "follow", "mention", "reply", "renote", "quote", "reaction", "pollVote", "pollEnded", "receiveFollowRequest", "followRequestAccepted", "groupInvited", "app", "roleAssigned", "chatRoomInvitationReceived", "achievementEarned"];
export const notificationTypes: readonly ["note", "follow", "mention", "reply", "renote", "quote", "reaction", "pollEnded", "receiveFollowRequest", "followRequestAccepted", "groupInvited", "app", "roleAssigned", "chatRoomInvitationReceived", "achievementEarned", "exportCompleted", "test", "login", "createToken"];
// @public (undocumented)
export function nyaize(text: string): string;
@ -3424,6 +3428,9 @@ type RoleLite = components['schemas']['RoleLite'];
// @public (undocumented)
type RolePolicies = components['schemas']['RolePolicies'];
// @public (undocumented)
export const rolePolicies: readonly ["gtlAvailable", "ltlAvailable", "canPublicNote", "mentionLimit", "canInvite", "inviteLimit", "inviteLimitCycle", "inviteExpirationTime", "canManageCustomEmojis", "canManageAvatarDecorations", "canSearchNotes", "canSearchUsers", "canUseTranslator", "canHideAds", "driveCapacityMb", "maxFileSizeMb", "alwaysMarkNsfw", "canUpdateBioMedia", "pinLimit", "antennaLimit", "wordMuteLimit", "webhookLimit", "clipLimit", "noteEachClipsLimit", "userListLimit", "userEachUserListsLimit", "rateLimitFactor", "avatarDecorationLimit", "canImportAntennas", "canImportBlocking", "canImportFollowing", "canImportMuting", "canImportUserLists", "chatAvailability", "uploadableFileTypes", "noteDraftLimit", "watermarkAvailable"];
// @public (undocumented)
type RolesListResponse = operations['roles___list']['responses']['200']['content']['application/json'];
@ -3538,6 +3545,7 @@ type SignupRequest = {
'g-recaptcha-response'?: string | null;
'turnstile-response'?: string | null;
'm-captcha-response'?: string | null;
'testcaptcha-response'?: string | null;
};
// @public (undocumented)

View File

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

View File

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

View File

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

View File

@ -17,7 +17,27 @@ import type {
ChatRoom,
} from './autogen/models.js';
export const notificationTypes = ['note', 'follow', 'mention', 'reply', 'renote', 'quote', 'reaction', 'pollVote', 'pollEnded', 'receiveFollowRequest', 'followRequestAccepted', 'groupInvited', 'app', 'roleAssigned', 'chatRoomInvitationReceived', 'achievementEarned'] as const;
export const notificationTypes = [
'note',
'follow',
'mention',
'reply',
'renote',
'quote',
'reaction',
'pollEnded',
'receiveFollowRequest',
'followRequestAccepted',
'groupInvited',
'app',
'roleAssigned',
'chatRoomInvitationReceived',
'achievementEarned',
'exportCompleted',
'test',
'login',
'createToken',
] as const;
export const noteVisibilities = ['public', 'home', 'followers', 'specified'] as const;
@ -170,6 +190,46 @@ export const moderationLogTypes = [
'updateProxyAccountDescription',
] as const;
export const rolePolicies = [
'gtlAvailable',
'ltlAvailable',
'canPublicNote',
'mentionLimit',
'canInvite',
'inviteLimit',
'inviteLimitCycle',
'inviteExpirationTime',
'canManageCustomEmojis',
'canManageAvatarDecorations',
'canSearchNotes',
'canSearchUsers',
'canUseTranslator',
'canHideAds',
'driveCapacityMb',
'maxFileSizeMb',
'alwaysMarkNsfw',
'canUpdateBioMedia',
'pinLimit',
'antennaLimit',
'wordMuteLimit',
'webhookLimit',
'clipLimit',
'noteEachClipsLimit',
'userListLimit',
'userEachUserListsLimit',
'rateLimitFactor',
'avatarDecorationLimit',
'canImportAntennas',
'canImportBlocking',
'canImportFollowing',
'canImportMuting',
'canImportUserLists',
'chatAvailability',
'uploadableFileTypes',
'noteDraftLimit',
'watermarkAvailable',
] as const;
export const queueTypes = [
'system',
'endedPollNotification',

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