Merge branch 'develop' into sw-file

This commit is contained in:
tamaina 2025-08-01 21:12:37 +09:00
commit 7de7ecb566
75 changed files with 1104 additions and 209 deletions

View File

@ -1 +1 @@
20.18.1 22.15.0

View File

@ -1,12 +1,29 @@
## Unreleased ## 2025.8.0
### Note
- サポートされるNode.jsの最小バージョンが**22.15.0**になりました
### General ### General
- ノートを削除した際、関連するノートが同時に削除されないようになりました - ノートを削除した際、関連するノートが同時に削除されないようになりました
- APIで、「replyIdが存在しているのにreplyがnull」や「renoteIdが存在しているのにrenoteがnull」であるという、今までにはなかったパターンが表れることになります - APIで、「replyIdが存在しているのにreplyがnull」や「renoteIdが存在しているのにrenoteがnull」であるという、今までにはなかったパターンが表れることになります
- 定期的に参照されていない古いリモートの投稿を削除する機能が実装されました(コントロールパネル→パフォーマンス→Remote Notes Cleaning)
- 既存のサーバーでは**デフォルトでオフ**、新規サーバーでは**デフォルトでオン**になります
- データベースの肥大化を防止することが可能です
- 既存のサーバーで当機能を有効化した場合は、処理量が多くなるため、一時的にストレージ使用量が増加する可能性があります。
- 増加量を抑えるには、最大処理継続時間をデフォルトより短くしてください。
- サーバーの初期設定が完了するまでは連合がオンにならないようになりました
- 日本語における公開範囲名称の「ダイレクト」が「指名」に改称されました
### Client ### Client
- Feat: セーフモード
- プラグイン・テーマ・カスタムCSSの使用でクライアントの起動に問題が発生した際に、これらを無効にして起動できます
- 以下の方法でセーフモードを起動できます
- `g` キーを連打する
- URLに`?safemode=true`を付ける
- PWAのショートカットで Safemode を選択して起動する
- Fix: 一部の設定検索結果が存在しないパスになる問題を修正 - Fix: 一部の設定検索結果が存在しないパスになる問題を修正
(Cherry-picked from https://activitypub.software/TransFem-org/Sharkey/-/merge_requests/1171) (Cherry-picked from https://activitypub.software/TransFem-org/Sharkey/-/merge_requests/1171)
- Fix: テーマエディタが動作しない問題を修正
### Server ### Server
- Enhance: ノートの削除処理の効率化 - Enhance: ノートの削除処理の効率化

View File

@ -1008,6 +1008,8 @@ lastNDays: "آخر {n} أيام"
surrender: "ألغِ" surrender: "ألغِ"
postForm: "أنشئ ملاحظة" postForm: "أنشئ ملاحظة"
information: "عن" information: "عن"
inMinutes: "د"
inDays: "ي"
_chat: _chat:
invitations: "دعوة" invitations: "دعوة"
noHistory: "السجل فارغ" noHistory: "السجل فارغ"

View File

@ -848,6 +848,8 @@ sourceCode: "সোর্স কোড"
flip: "উল্টান" flip: "উল্টান"
postForm: "নোট লিখুন" postForm: "নোট লিখুন"
information: "আপনার সম্পর্কে" information: "আপনার সম্পর্কে"
inMinutes: "মিনিট"
inDays: "দিন"
_chat: _chat:
invitations: "আমন্ত্রণ" invitations: "আমন্ত্রণ"
noHistory: "কোনো ইতিহাস নেই" noHistory: "কোনো ইতিহাস নেই"

View File

@ -896,7 +896,7 @@ searchResult: "Resultats de la cerca"
hashtags: "Etiquetes" hashtags: "Etiquetes"
troubleshooting: "Solucionar problemes" troubleshooting: "Solucionar problemes"
useBlurEffect: "Fes servir efectes de desenfocament a la interfície" useBlurEffect: "Fes servir efectes de desenfocament a la interfície"
learnMore: "Saber més " learnMore: "Saber-ne més "
misskeyUpdated: "Misskey s'ha actualitzat " misskeyUpdated: "Misskey s'ha actualitzat "
whatIsNew: "Mostra canvis" whatIsNew: "Mostra canvis"
translate: "Traduir " translate: "Traduir "
@ -1368,6 +1368,8 @@ redisplayAllTips: "Torna ha mostrat tots els trucs i consells"
hideAllTips: "Amagar tots els trucs i consells" hideAllTips: "Amagar tots els trucs i consells"
defaultImageCompressionLevel: "Nivell de comprensió de la imatge per defecte" defaultImageCompressionLevel: "Nivell de comprensió de la imatge per defecte"
defaultImageCompressionLevel_description: "Baixa, conserva la qualitat de la imatge però la mida de l'arxiu és més gran. <br>Alta, redueix la mida de l'arxiu però també la qualitat de la imatge." defaultImageCompressionLevel_description: "Baixa, conserva la qualitat de la imatge però la mida de l'arxiu és més gran. <br>Alta, redueix la mida de l'arxiu però també la qualitat de la imatge."
inMinutes: "Minut(s)"
inDays: "Di(a)(es)"
_order: _order:
newest: "Més recent" newest: "Més recent"
oldest: "Cronològic" oldest: "Cronològic"

View File

@ -1107,6 +1107,8 @@ lastNDays: "Posledních {n} dnů"
surrender: "Zrušit" surrender: "Zrušit"
postForm: "Formulář pro odeslání" postForm: "Formulář pro odeslání"
information: "Informace" information: "Informace"
inMinutes: "Minut"
inDays: "Dnů"
_chat: _chat:
invitations: "Pozvat" invitations: "Pozvat"
noHistory: "Žádná historie" noHistory: "Žádná historie"

View File

@ -1368,6 +1368,8 @@ redisplayAllTips: "Alle „Tipps und Tricks“ wieder anzeigen"
hideAllTips: "Alle „Tipps und Tricks“ ausblenden" hideAllTips: "Alle „Tipps und Tricks“ ausblenden"
defaultImageCompressionLevel: "Standard-Bildkomprimierungsstufe" defaultImageCompressionLevel: "Standard-Bildkomprimierungsstufe"
defaultImageCompressionLevel_description: "Ein niedrigerer Wert erhält die Bildqualität, erhöht aber die Dateigröße. <br>Höhere Werte reduzieren die Dateigröße, verringern aber die Bildqualität." defaultImageCompressionLevel_description: "Ein niedrigerer Wert erhält die Bildqualität, erhöht aber die Dateigröße. <br>Höhere Werte reduzieren die Dateigröße, verringern aber die Bildqualität."
inMinutes: "Minute(n)"
inDays: "Tag(en)"
_order: _order:
newest: "Neueste zuerst" newest: "Neueste zuerst"
oldest: "Älteste zuerst" oldest: "Älteste zuerst"

View File

@ -1368,6 +1368,8 @@ redisplayAllTips: "Show all “Tips & Tricks” again"
hideAllTips: "Hide all \"Tips & Tricks\"" hideAllTips: "Hide all \"Tips & Tricks\""
defaultImageCompressionLevel: "Default image compression level" defaultImageCompressionLevel: "Default image compression level"
defaultImageCompressionLevel_description: "Lower level preserves image quality but increases file size.<br>Higher level reduce file size, but reduce image quality." defaultImageCompressionLevel_description: "Lower level preserves image quality but increases file size.<br>Higher level reduce file size, but reduce image quality."
inMinutes: "Minute(s)"
inDays: "Day(s)"
_order: _order:
newest: "Newest First" newest: "Newest First"
oldest: "Oldest First" oldest: "Oldest First"

View File

@ -280,8 +280,8 @@ featured: "Destacados"
usernameOrUserId: "Nombre o ID del usuario" usernameOrUserId: "Nombre o ID del usuario"
noSuchUser: "No se encuentra el usuario" noSuchUser: "No se encuentra el usuario"
lookup: "Búsqueda" lookup: "Búsqueda"
announcements: "Anuncios" announcements: "Avisos"
imageUrl: "URL de la imágen" imageUrl: "URL de la imagen."
remove: "Borrar" remove: "Borrar"
removed: "Borrado" removed: "Borrado"
removeAreYouSure: "¿Desea borrar \"{x}\"?" removeAreYouSure: "¿Desea borrar \"{x}\"?"
@ -842,7 +842,7 @@ unlikeConfirm: "¿Quitar como favorito?"
fullView: "Vista completa" fullView: "Vista completa"
quitFullView: "quitar vista completa" quitFullView: "quitar vista completa"
addDescription: "Agregar descripción" addDescription: "Agregar descripción"
userPagePinTip: "Puede mantener sus notas visibles aquí seleccionando Pin en el menú de notas individuales" userPagePinTip: "Puede mantener sus notas visibles aquí seleccionando 'Fijar al perfil' en el menú de notas individuales"
notSpecifiedMentionWarning: "Algunas menciones no están incluidas en el destino" notSpecifiedMentionWarning: "Algunas menciones no están incluidas en el destino"
info: "Información" info: "Información"
userInfo: "Información del usuario" userInfo: "Información del usuario"
@ -877,7 +877,7 @@ popularPosts: "Más vistos"
shareWithNote: "Compartir con una nota" shareWithNote: "Compartir con una nota"
ads: "Anuncios" ads: "Anuncios"
expiration: "Termina el" expiration: "Termina el"
startingperiod: "periodo de inicio" startingperiod: "Comienzo"
memo: "Notas" memo: "Notas"
priority: "Prioridad" priority: "Prioridad"
high: "Alta" high: "Alta"
@ -1143,7 +1143,7 @@ channelArchiveConfirmTitle: "¿Seguro de archivar {name}?"
channelArchiveConfirmDescription: "Un canal archivado no aparecerá en la lista de canales ni en los resultados. Las nuevas publicaciones tampoco serán añadidas." channelArchiveConfirmDescription: "Un canal archivado no aparecerá en la lista de canales ni en los resultados. Las nuevas publicaciones tampoco serán añadidas."
thisChannelArchived: "El canal ha sido archivado." thisChannelArchived: "El canal ha sido archivado."
displayOfNote: "Mostrar notas" displayOfNote: "Mostrar notas"
initialAccountSetting: "Configración inicial de su cuenta\nか\nConfigración de inicio" initialAccountSetting: "Configración inicial de su cuenta"
youFollowing: "Siguiendo" youFollowing: "Siguiendo"
preventAiLearning: "Rechazar el uso en el Aprendizaje de Máquinas. (IA Generativa)" preventAiLearning: "Rechazar el uso en el Aprendizaje de Máquinas. (IA Generativa)"
preventAiLearningDescription: "Pedirle a las arañas (crawlers) no usar los textos publicados o imágenes en el aprendizaje automático (IA Predictiva / Generativa). Ésto se logra añadiendo una marca respuesta HTML con la cadena \"noai\" al cantenido. Una prevención total no podría lograrse sólo usando ésta marca, ya que puede ser simplemente ignorada." preventAiLearningDescription: "Pedirle a las arañas (crawlers) no usar los textos publicados o imágenes en el aprendizaje automático (IA Predictiva / Generativa). Ésto se logra añadiendo una marca respuesta HTML con la cadena \"noai\" al cantenido. Una prevención total no podría lograrse sólo usando ésta marca, ya que puede ser simplemente ignorada."
@ -1358,8 +1358,8 @@ advice: "Consejos"
realtimeMode: "Modo en tiempo real" realtimeMode: "Modo en tiempo real"
turnItOn: "Activar" turnItOn: "Activar"
turnItOff: "Desactivar" turnItOff: "Desactivar"
emojiMute: "Silenciar emojis" emojiMute: "Silenciar emoji"
emojiUnmute: "No Silenciar emojis" emojiUnmute: "No silenciar emoji"
muteX: "Silenciar {x}" muteX: "Silenciar {x}"
unmuteX: "Dejar de silenciar {x}" unmuteX: "Dejar de silenciar {x}"
abort: "Abortar" abort: "Abortar"
@ -1368,6 +1368,8 @@ redisplayAllTips: "Volver a mostrar todos \"Trucos y consejos\""
hideAllTips: "Ocultar todos los \"Trucos y consejos\"" hideAllTips: "Ocultar todos los \"Trucos y consejos\""
defaultImageCompressionLevel: "Nivel de compresión de la imagen por defecto" defaultImageCompressionLevel: "Nivel de compresión de la imagen por defecto"
defaultImageCompressionLevel_description: "Baja, conserva la calidad de la imagen pero la medida del archivo es más grande. <br>Alta, reduce la medida del archivo pero también la calidad de la imagen." defaultImageCompressionLevel_description: "Baja, conserva la calidad de la imagen pero la medida del archivo es más grande. <br>Alta, reduce la medida del archivo pero también la calidad de la imagen."
inMinutes: "Minutos"
inDays: "Días"
_order: _order:
newest: "Los más recientes primero" newest: "Los más recientes primero"
oldest: "Los más antiguos primero" oldest: "Los más antiguos primero"
@ -1530,7 +1532,7 @@ _announcement:
tooManyActiveAnnouncementDescription: "Tener demasiados anuncios activos empeora la experiencia de usuario. Por favor, considera archivar aquellos anuncios que hayan quedado obsoletos." tooManyActiveAnnouncementDescription: "Tener demasiados anuncios activos empeora la experiencia de usuario. Por favor, considera archivar aquellos anuncios que hayan quedado obsoletos."
readConfirmTitle: "¿Marcar como leído?" readConfirmTitle: "¿Marcar como leído?"
readConfirmText: "Esto marcará el contenido de \"{title}\" como leído." readConfirmText: "Esto marcará el contenido de \"{title}\" como leído."
shouldNotBeUsedToPresentPermanentInfo: "Dado que puede impactar en la experiencia de usuario de forma significativa, es recomendable usar notificaciones en el flujo de información en vez de información persistente." shouldNotBeUsedToPresentPermanentInfo: "Se recomienda utilizar los avisos para publicar información que requiera inmediatez, en lugar de hacerlo constantemente, ya que esto perjudica especialmente la UX de los nuevos usuarios."
dialogAnnouncementUxWarn: "Mostrar dos o más notificaciones en formato diálogo a la vez puede impactar en la experiencia de usuario de forma significativa, úsalos con cuidado." dialogAnnouncementUxWarn: "Mostrar dos o más notificaciones en formato diálogo a la vez puede impactar en la experiencia de usuario de forma significativa, úsalos con cuidado."
silence: "Silenciar notificaciones" silence: "Silenciar notificaciones"
silenceDescription: "Si lo activas, no enviarás notificación sobre este anuncio y el usuario no tendrá que leerlo." silenceDescription: "Si lo activas, no enviarás notificación sobre este anuncio y el usuario no tendrá que leerlo."
@ -3121,7 +3123,7 @@ _uploader:
tip: "El archivo aún no se ha cargado, por lo que este cuadro de diálogo te permite confirmar, renombrar, comprimir y recortar el archivo antes de cargarlo. Cuando esté listo, puedes iniciar la carga pulsando el botón \"Cargar\"." tip: "El archivo aún no se ha cargado, por lo que este cuadro de diálogo te permite confirmar, renombrar, comprimir y recortar el archivo antes de cargarlo. Cuando esté listo, puedes iniciar la carga pulsando el botón \"Cargar\"."
_clientPerformanceIssueTip: _clientPerformanceIssueTip:
title: "Si crees que el consumo de batería es demasiado alto" title: "Si crees que el consumo de batería es demasiado alto"
makeSureDisabledAdBlocker: "Por favor, desactive el bloqueador de publicidad." makeSureDisabledAdBlocker: "Por favor, desactiva el bloqueador de publicidad."
makeSureDisabledAdBlocker_description: "Los bloqueadores de anuncios pueden afectar al rendimiento. Asegúrate de que no están activados en tu sistema o en las funciones/extensiones de tu navegador." makeSureDisabledAdBlocker_description: "Los bloqueadores de anuncios pueden afectar al rendimiento. Asegúrate de que no están activados en tu sistema o en las funciones/extensiones de tu navegador."
makeSureDisabledCustomCss: "Desactiva el CSS personalizado" makeSureDisabledCustomCss: "Desactiva el CSS personalizado"
makeSureDisabledCustomCss_description: "Anular estilos puede afectar al rendimiento. Asegúrate de que el CSS personalizado o las extensiones que sobrescriben estilos no están activados." makeSureDisabledCustomCss_description: "Anular estilos puede afectar al rendimiento. Asegúrate de que el CSS personalizado o las extensiones que sobrescriben estilos no están activados."

View File

@ -1272,6 +1272,8 @@ pleaseSelectAccount: "Sélectionner un compte"
availableRoles: "Rôles disponibles" availableRoles: "Rôles disponibles"
postForm: "Formulaire de publication" postForm: "Formulaire de publication"
information: "Informations" information: "Informations"
inMinutes: "min"
inDays: "j"
_chat: _chat:
invitations: "Inviter" invitations: "Inviter"
noHistory: "Pas d'historique" noHistory: "Pas d'historique"

View File

@ -1263,6 +1263,8 @@ thereAreNChanges: "Ada {n} perubahan"
prohibitedWordsForNameOfUser: "Kata yang dilarang untuk nama pengguna" prohibitedWordsForNameOfUser: "Kata yang dilarang untuk nama pengguna"
postForm: "Buat catatan" postForm: "Buat catatan"
information: "Informasi" information: "Informasi"
inMinutes: "menit"
inDays: "hari"
_chat: _chat:
invitations: "Undang" invitations: "Undang"
noHistory: "Tidak ada riwayat" noHistory: "Tidak ada riwayat"

74
locales/index.d.ts vendored
View File

@ -315,11 +315,11 @@ export interface Locale extends ILocale {
*/ */
"mention": string; "mention": string;
/** /**
* *
*/ */
"mentions": string; "mentions": string;
/** /**
* 稿 *
*/ */
"directNotes": string; "directNotes": string;
/** /**
@ -5493,6 +5493,30 @@ export interface Locale extends ILocale {
* <br> * <br>
*/ */
"defaultImageCompressionLevel_description": string; "defaultImageCompressionLevel_description": string;
/**
*
*/
"inMinutes": string;
/**
*
*/
"inDays": string;
/**
*
*/
"safeModeEnabled": string;
/**
*
*/
"pluginsAreDisabledBecauseSafeMode": string;
/**
* CSSは適用されていません
*/
"customCssIsDisabledBecauseSafeMode": string;
/**
* 使
*/
"themeIsDefaultBecauseSafeMode": string;
"_order": { "_order": {
/** /**
* *
@ -6329,7 +6353,7 @@ export interface Locale extends ILocale {
*/ */
"followers": string; "followers": string;
/** /**
* 使 *
*/ */
"direct": string; "direct": string;
/** /**
@ -6337,7 +6361,7 @@ export interface Locale extends ILocale {
*/ */
"doNotSendConfidencialOnDirect1": string; "doNotSendConfidencialOnDirect1": string;
/** /**
* 稿稿 * 稿
*/ */
"doNotSendConfidencialOnDirect2": string; "doNotSendConfidencialOnDirect2": string;
/** /**
@ -6486,6 +6510,22 @@ export interface Locale extends ILocale {
* Redisのメモリ使用量は増加します * Redisのメモリ使用量は増加します
*/ */
"reactionsBufferingDescription": string; "reactionsBufferingDescription": string;
/**
* 稿
*/
"remoteNotesCleaning": string;
/**
* 稿
*/
"remoteNotesCleaning_description": string;
/**
*
*/
"remoteNotesCleaningMaxProcessingDuration": string;
/**
*
*/
"remoteNotesCleaningExpiryDaysForEachNotes": string;
/** /**
* URL * URL
*/ */
@ -6558,6 +6598,14 @@ export interface Locale extends ILocale {
* *
*/ */
"userGeneratedContentsVisibilityForVisitor_description2": string; "userGeneratedContentsVisibilityForVisitor_description2": string;
/**
*
*/
"restartServerSetupWizardConfirm_title": string;
/**
*
*/
"restartServerSetupWizardConfirm_text": string;
"_userGeneratedContentsVisibilityForVisitor": { "_userGeneratedContentsVisibilityForVisitor": {
/** /**
* *
@ -9593,7 +9641,7 @@ export interface Locale extends ILocale {
*/ */
"followersDescription": string; "followersDescription": string;
/** /**
* *
*/ */
"specified": string; "specified": string;
/** /**
@ -10482,11 +10530,11 @@ export interface Locale extends ILocale {
*/ */
"channel": string; "channel": string;
/** /**
* *
*/ */
"mentions": string; "mentions": string;
/** /**
* *
*/ */
"direct": string; "direct": string;
/** /**
@ -11807,6 +11855,10 @@ export interface Locale extends ILocale {
* *
*/ */
"otherOption3": string; "otherOption3": string;
/**
* Misskeyをセーフモードで起動
*/
"otherOption4": string;
}; };
"_search": { "_search": {
/** /**
@ -11943,6 +11995,14 @@ export interface Locale extends ILocale {
* *
*/ */
"youCanConfigureMoreFederationSettingsLater": string; "youCanConfigureMoreFederationSettingsLater": string;
/**
*
*/
"remoteContentsCleaning": string;
/**
*
*/
"remoteContentsCleaning_description": string;
/** /**
* *
*/ */

View File

@ -1313,6 +1313,7 @@ availableRoles: "Ruoli disponibili"
acknowledgeNotesAndEnable: "Attivare dopo averne compreso il comportamento." acknowledgeNotesAndEnable: "Attivare dopo averne compreso il comportamento."
federationSpecified: "Questo server è federato solo con istanze specifiche del Fediverso. Puoi interagire solo con quelle scelte dall'amministrazione." federationSpecified: "Questo server è federato solo con istanze specifiche del Fediverso. Puoi interagire solo con quelle scelte dall'amministrazione."
federationDisabled: "Questo server ha la federazione disabilitata. Non puoi interagire con profili provenienti da altri server." federationDisabled: "Questo server ha la federazione disabilitata. Non puoi interagire con profili provenienti da altri server."
draft: "Bozza"
confirmOnReact: "Confermare le reazioni" confirmOnReact: "Confermare le reazioni"
reactAreYouSure: "Vuoi davvero reagire con {emoji} ?" reactAreYouSure: "Vuoi davvero reagire con {emoji} ?"
markAsSensitiveConfirm: "Vuoi davvero indicare questo contenuto multimediale come esplicito?" markAsSensitiveConfirm: "Vuoi davvero indicare questo contenuto multimediale come esplicito?"
@ -1367,6 +1368,11 @@ redisplayAllTips: "Mostra tutti i suggerimenti"
hideAllTips: "Nascondi tutti i suggerimenti" hideAllTips: "Nascondi tutti i suggerimenti"
defaultImageCompressionLevel: "Livello predefinito di compressione immagini" defaultImageCompressionLevel: "Livello predefinito di compressione immagini"
defaultImageCompressionLevel_description: "La compressione diminuisce la qualità dell'immagine, poca compressione mantiene alta qualità delle immagini. Aumentandola, si riducono le dimensioni del file, a discapito della qualità dell'immagine." defaultImageCompressionLevel_description: "La compressione diminuisce la qualità dell'immagine, poca compressione mantiene alta qualità delle immagini. Aumentandola, si riducono le dimensioni del file, a discapito della qualità dell'immagine."
inMinutes: "min"
inDays: "giorni"
_order:
newest: "Prima i più recenti"
oldest: "Meno recenti prima"
_chat: _chat:
noMessagesYet: "Ancora nessun messaggio" noMessagesYet: "Ancora nessun messaggio"
newMessage: "Nuovo messaggio" newMessage: "Nuovo messaggio"
@ -1993,6 +1999,8 @@ _role:
uploadableFileTypes: "Tipi di file caricabili" uploadableFileTypes: "Tipi di file caricabili"
uploadableFileTypes_caption: "Specifica il tipo MIME. Puoi specificare più valori separandoli andando a capo, oppure indicare caratteri jolly con un asterisco (*). Ad esempio: image/*" uploadableFileTypes_caption: "Specifica il tipo MIME. Puoi specificare più valori separandoli andando a capo, oppure indicare caratteri jolly con un asterisco (*). Ad esempio: image/*"
uploadableFileTypes_caption2: "A seconda del file, il tipo potrebbe non essere determinato. Se si desidera consentire tali file, aggiungere {x} alla specifica." uploadableFileTypes_caption2: "A seconda del file, il tipo potrebbe non essere determinato. Se si desidera consentire tali file, aggiungere {x} alla specifica."
noteDraftLimit: "Numero massimo di Note in bozza, lato server"
watermarkAvailable: "Disponibilità della funzione filigrana"
_condition: _condition:
roleAssignedTo: "Assegnato a ruoli manualmente" roleAssignedTo: "Assegnato a ruoli manualmente"
isLocal: "Profilo locale" isLocal: "Profilo locale"
@ -2152,6 +2160,7 @@ _theme:
install: "Installa un tema" install: "Installa un tema"
manage: "Gestione dei temi" manage: "Gestione dei temi"
code: "Codice tema" code: "Codice tema"
copyThemeCode: "Copia il codice del Tema"
description: "Descrizione" description: "Descrizione"
installed: "{name} è installato" installed: "{name} è installato"
installedThemes: "Temi installati" installedThemes: "Temi installati"
@ -2800,6 +2809,7 @@ _fileViewer:
url: "URL" url: "URL"
uploadedAt: "Caricato il" uploadedAt: "Caricato il"
attachedNotes: "Note a cui è allegato" attachedNotes: "Note a cui è allegato"
usage: "In uso"
thisPageCanBeSeenFromTheAuthor: "Questa pagina può essere vista solo da chi ha caricato il file." thisPageCanBeSeenFromTheAuthor: "Questa pagina può essere vista solo da chi ha caricato il file."
_externalResourceInstaller: _externalResourceInstaller:
title: "Installa da sito esterno" title: "Installa da sito esterno"
@ -3103,6 +3113,7 @@ _serverSetupWizard:
text2: "Se puoi, ti preghiamo di prendere in considerazione l'idea di fare una donazione, così potremo continuare a sviluppare." text2: "Se puoi, ti preghiamo di prendere in considerazione l'idea di fare una donazione, così potremo continuare a sviluppare."
text3: "Sono previsti anche dei vantaggi speciali per i sostenitori!" text3: "Sono previsti anche dei vantaggi speciali per i sostenitori!"
_uploader: _uploader:
editImage: "Modifica immagine"
compressedToX: "Compresso in {x}" compressedToX: "Compresso in {x}"
savedXPercent: "{x}% risparmiati" savedXPercent: "{x}% risparmiati"
abortConfirm: "Alcuni file non sono stati caricati. Vuoi annullare l'operazione?" abortConfirm: "Alcuni file non sono stati caricati. Vuoi annullare l'operazione?"
@ -3169,5 +3180,20 @@ _imageEffector:
stripe: "Strisce" stripe: "Strisce"
polkadot: "A pallini" polkadot: "A pallini"
checker: "revisore" checker: "revisore"
blockNoise: "Attenua rumore"
tearing: "Strappa immagine"
drafts: "Bozza"
_drafts: _drafts:
select: "Selezionare bozza"
cannotCreateDraftAnymore: "Hai superato il numero massimo di bozze ammissibili."
cannotCreateDraft: "Impossibile creare una bozza di questo contenuto."
delete: "Elimina bozza"
deleteAreYouSure: "Vuoi davvero eliminare la bozza?"
noDrafts: "Non c'è nessuna bozza."
replyTo: "Rispondere a {user}"
quoteOf: "Citare la nota di {user}"
postTo: "Inserire in {channel}"
saveToDraft: "Salva come bozza"
restoreFromDraft: "Recuperare dalle bozze"
restore: "Ripristina" restore: "Ripristina"
listDrafts: "Elenco bozze"

View File

@ -74,8 +74,8 @@ youGotNewFollower: "フォローされました"
receiveFollowRequest: "フォローリクエストされました" receiveFollowRequest: "フォローリクエストされました"
followRequestAccepted: "フォローが承認されました" followRequestAccepted: "フォローが承認されました"
mention: "メンション" mention: "メンション"
mentions: "あなた宛て" mentions: "メンション"
directNotes: "ダイレクト投稿" directNotes: "指名"
importAndExport: "インポートとエクスポート" importAndExport: "インポートとエクスポート"
import: "インポート" import: "インポート"
export: "エクスポート" export: "エクスポート"
@ -1368,6 +1368,12 @@ redisplayAllTips: "全ての「ヒントとコツ」を再表示"
hideAllTips: "全ての「ヒントとコツ」を非表示" hideAllTips: "全ての「ヒントとコツ」を非表示"
defaultImageCompressionLevel: "デフォルトの画像圧縮度" defaultImageCompressionLevel: "デフォルトの画像圧縮度"
defaultImageCompressionLevel_description: "低くすると画質を保てますが、ファイルサイズは増加します。<br>高くするとファイルサイズを減らせますが、画質は低下します。" defaultImageCompressionLevel_description: "低くすると画質を保てますが、ファイルサイズは増加します。<br>高くするとファイルサイズを減らせますが、画質は低下します。"
inMinutes: "分"
inDays: "日"
safeModeEnabled: "セーフモードが有効です"
pluginsAreDisabledBecauseSafeMode: "セーフモードが有効なため、プラグインはすべて無効化されています。"
customCssIsDisabledBecauseSafeMode: "セーフモードが有効なため、カスタムCSSは適用されていません。"
themeIsDefaultBecauseSafeMode: "セーフモードが有効な間はデフォルトのテーマが使用されます。セーフモードをオフにすると元に戻ります。"
_order: _order:
newest: "新しい順" newest: "新しい順"
@ -1603,9 +1609,9 @@ _initialTutorial:
public: "すべてのユーザーに公開。" public: "すべてのユーザーに公開。"
home: "ホームタイムラインのみに公開。フォロワー・プロフィールを見に来た人・リノートから、他のユーザーも見ることができます。" home: "ホームタイムラインのみに公開。フォロワー・プロフィールを見に来た人・リノートから、他のユーザーも見ることができます。"
followers: "フォロワーにのみ公開。本人以外がリノートすることはできず、またフォロワー以外は閲覧できません。" followers: "フォロワーにのみ公開。本人以外がリノートすることはできず、またフォロワー以外は閲覧できません。"
direct: "指定したユーザーにのみ公開され、また相手に通知が入ります。ダイレクトメッセージのかわりにお使いいただけます。" direct: "指定したユーザーにのみ公開され、また相手に通知が入ります。"
doNotSendConfidencialOnDirect1: "機密情報は送信する際は注意してください。" doNotSendConfidencialOnDirect1: "機密情報は送信する際は注意してください。"
doNotSendConfidencialOnDirect2: "送信先のサーバーの管理者は投稿内容を見ることが可能なので、信頼できないサーバーのユーザーにダイレクト投稿を送信する場合は、機密情報の扱いに注意が必要です。" doNotSendConfidencialOnDirect2: "送信先のサーバーの管理者は投稿内容を見ることが可能なので、信頼できないサーバーのユーザーが含まれる限定公開のノートを作成する際は、機密情報の扱いに注意が必要です。"
localOnly: "他のサーバーに投稿を連合しません。上記の公開範囲に関わらず、他のサーバーのユーザーは、この設定がついたノートを直接閲覧することができなくなります。" localOnly: "他のサーバーに投稿を連合しません。上記の公開範囲に関わらず、他のサーバーのユーザーは、この設定がついたノートを直接閲覧することができなくなります。"
_cw: _cw:
title: "内容を隠すCW" title: "内容を隠すCW"
@ -1649,6 +1655,10 @@ _serverSettings:
fanoutTimelineDbFallback: "データベースへのフォールバック" fanoutTimelineDbFallback: "データベースへのフォールバック"
fanoutTimelineDbFallbackDescription: "有効にすると、タイムラインがキャッシュされていない場合にDBへ追加で問い合わせを行うフォールバック処理を行います。無効にすると、フォールバック処理を行わないことでさらにサーバーの負荷を軽減することができますが、タイムラインが取得できる範囲に制限が生じます。" fanoutTimelineDbFallbackDescription: "有効にすると、タイムラインがキャッシュされていない場合にDBへ追加で問い合わせを行うフォールバック処理を行います。無効にすると、フォールバック処理を行わないことでさらにサーバーの負荷を軽減することができますが、タイムラインが取得できる範囲に制限が生じます。"
reactionsBufferingDescription: "有効にすると、リアクション作成時のパフォーマンスが大幅に向上し、データベースへの負荷を軽減することが可能です。ただし、Redisのメモリ使用量は増加します。" reactionsBufferingDescription: "有効にすると、リアクション作成時のパフォーマンスが大幅に向上し、データベースへの負荷を軽減することが可能です。ただし、Redisのメモリ使用量は増加します。"
remoteNotesCleaning: "リモート投稿の自動クリーニング"
remoteNotesCleaning_description: "有効にすると、参照されていない古いリモートの投稿を定期的にクリーンアップしてデータベースの肥大化を抑制します。"
remoteNotesCleaningMaxProcessingDuration: "最大クリーニング処理継続時間"
remoteNotesCleaningExpiryDaysForEachNotes: "最低ノート保持日数"
inquiryUrl: "問い合わせ先URL" inquiryUrl: "問い合わせ先URL"
inquiryUrlDescription: "サーバー運営者へのお問い合わせフォームのURLや、運営者の連絡先等が記載されたWebページのURLを指定します。" inquiryUrlDescription: "サーバー運営者へのお問い合わせフォームのURLや、運営者の連絡先等が記載されたWebページのURLを指定します。"
openRegistration: "アカウントの作成をオープンにする" openRegistration: "アカウントの作成をオープンにする"
@ -1667,6 +1677,8 @@ _serverSettings:
userGeneratedContentsVisibilityForVisitor: "非利用者に対するユーザー作成コンテンツの公開範囲" userGeneratedContentsVisibilityForVisitor: "非利用者に対するユーザー作成コンテンツの公開範囲"
userGeneratedContentsVisibilityForVisitor_description: "モデレーションが行き届きにくい不適切なリモートコンテンツなどが、自サーバー経由で図らずもインターネットに公開されてしまうことによるトラブル防止などに役立ちます。" userGeneratedContentsVisibilityForVisitor_description: "モデレーションが行き届きにくい不適切なリモートコンテンツなどが、自サーバー経由で図らずもインターネットに公開されてしまうことによるトラブル防止などに役立ちます。"
userGeneratedContentsVisibilityForVisitor_description2: "サーバーで受信したリモートのコンテンツを含め、サーバー内の全てのコンテンツを無条件でインターネットに公開することはリスクが伴います。特に、分散型の特性を知らない閲覧者にとっては、リモートのコンテンツであってもサーバー内で作成されたコンテンツであると誤って認識してしまう可能性があるため、注意が必要です。" userGeneratedContentsVisibilityForVisitor_description2: "サーバーで受信したリモートのコンテンツを含め、サーバー内の全てのコンテンツを無条件でインターネットに公開することはリスクが伴います。特に、分散型の特性を知らない閲覧者にとっては、リモートのコンテンツであってもサーバー内で作成されたコンテンツであると誤って認識してしまう可能性があるため、注意が必要です。"
restartServerSetupWizardConfirm_title: "サーバーの初期設定ウィザードをやり直しますか?"
restartServerSetupWizardConfirm_text: "現在の一部の設定はリセットされます。"
_userGeneratedContentsVisibilityForVisitor: _userGeneratedContentsVisibilityForVisitor:
all: "全て公開" all: "全て公開"
@ -2524,7 +2536,7 @@ _visibility:
homeDescription: "ホームタイムラインのみに公開" homeDescription: "ホームタイムラインのみに公開"
followers: "フォロワー" followers: "フォロワー"
followersDescription: "自分のフォロワーのみに公開" followersDescription: "自分のフォロワーのみに公開"
specified: "ダイレクト" specified: "指名"
specifiedDescription: "指定したユーザーのみに公開" specifiedDescription: "指定したユーザーのみに公開"
disableFederation: "連合なし" disableFederation: "連合なし"
disableFederationDescription: "他サーバーへの配信を行いません" disableFederationDescription: "他サーバーへの配信を行いません"
@ -2770,8 +2782,8 @@ _deck:
antenna: "アンテナ" antenna: "アンテナ"
list: "リスト" list: "リスト"
channel: "チャンネル" channel: "チャンネル"
mentions: "あなた宛て" mentions: "メンション"
direct: "ダイレクト" direct: "指名"
roleTimeline: "ロールタイムライン" roleTimeline: "ロールタイムライン"
chat: "チャット" chat: "チャット"
@ -3156,6 +3168,7 @@ _bootErrors:
otherOption1: "クライアント設定とキャッシュを削除" otherOption1: "クライアント設定とキャッシュを削除"
otherOption2: "簡易クライアントを起動" otherOption2: "簡易クライアントを起動"
otherOption3: "修復ツールを起動" otherOption3: "修復ツールを起動"
otherOption4: "Misskeyをセーフモードで起動"
_search: _search:
searchScopeAll: "全て" searchScopeAll: "全て"
@ -3194,6 +3207,8 @@ _serverSetupWizard:
doYouConnectToFediverse_description1: "分散型サーバーで構成されるネットワーク(Fediverse)に接続すると、他のサーバーと相互にコンテンツのやり取りが可能です。" doYouConnectToFediverse_description1: "分散型サーバーで構成されるネットワーク(Fediverse)に接続すると、他のサーバーと相互にコンテンツのやり取りが可能です。"
doYouConnectToFediverse_description2: "Fediverseと接続することは「連合」とも呼ばれます。" doYouConnectToFediverse_description2: "Fediverseと接続することは「連合」とも呼ばれます。"
youCanConfigureMoreFederationSettingsLater: "連合可能なサーバーの指定など、高度な設定も後ほど可能です。" youCanConfigureMoreFederationSettingsLater: "連合可能なサーバーの指定など、高度な設定も後ほど可能です。"
remoteContentsCleaning: "受信コンテンツの自動クリーニング"
remoteContentsCleaning_description: "連合を行うと、継続して多くのコンテンツを受信します。自動クリーニングを有効にすると、参照されていない古くなったコンテンツを自動でサーバーから削除し、ストレージを節約できます。"
adminInfo: "管理者情報" adminInfo: "管理者情報"
adminInfo_description: "問い合わせを受け付けるために使用される管理者情報を設定します。" adminInfo_description: "問い合わせを受け付けるために使用される管理者情報を設定します。"
adminInfo_mustBeFilled: "オープンサーバー、または連合がオンの場合は必ず入力が必要です。" adminInfo_mustBeFilled: "オープンサーバー、または連合がオンの場合は必ず入力が必要です。"

View File

@ -300,6 +300,7 @@ uploadFromUrlMayTakeTime: "アップロード終わるんにちょい時間か
explore: "みつける" explore: "みつける"
messageRead: "もう読んだ" messageRead: "もう読んだ"
noMoreHistory: "これより昔のんはあらへんで" noMoreHistory: "これより昔のんはあらへんで"
startChat: "チャットを始めよか"
nUsersRead: "{n}人が読んでもうた" nUsersRead: "{n}人が読んでもうた"
agreeTo: "{0}に同意したで" agreeTo: "{0}に同意したで"
agree: "せやな" agree: "せやな"
@ -324,6 +325,7 @@ dark: "ダーク"
lightThemes: "デイゲーム" lightThemes: "デイゲーム"
darkThemes: "ナイトゲーム" darkThemes: "ナイトゲーム"
syncDeviceDarkMode: "デバイスのダークモードと一緒にする" syncDeviceDarkMode: "デバイスのダークモードと一緒にする"
switchDarkModeManuallyWhenSyncEnabledConfirm: "「{x}」がオンになってるで。同期をオフにして手動でモードを切り替えることにします?"
drive: "ドライブ" drive: "ドライブ"
fileName: "ファイル名" fileName: "ファイル名"
selectFile: "ファイル選んでや" selectFile: "ファイル選んでや"
@ -422,6 +424,7 @@ antennaExcludeBots: "Botアカウントを除外"
antennaKeywordsDescription: "スペースで区切ったるとAND指定で、改行で区切ったるとOR指定や" antennaKeywordsDescription: "スペースで区切ったるとAND指定で、改行で区切ったるとOR指定や"
notifyAntenna: "新しいノートを通知すんで" notifyAntenna: "新しいノートを通知すんで"
withFileAntenna: "なんか添付されたノートだけ" withFileAntenna: "なんか添付されたノートだけ"
excludeNotesInSensitiveChannel: "センシティブなチャンネルのノートは入れんとくわ"
enableServiceworker: "ブラウザにプッシュ通知が行くようにする" enableServiceworker: "ブラウザにプッシュ通知が行くようにする"
antennaUsersDescription: "ユーザー名を改行で区切ったってな" antennaUsersDescription: "ユーザー名を改行で区切ったってな"
caseSensitive: "大文字と小文字は別もんや" caseSensitive: "大文字と小文字は別もんや"
@ -693,6 +696,7 @@ userSaysSomethingAbout: "{name}が「{word}」についてなんか言うてた
makeActive: "使うで" makeActive: "使うで"
display: "表示" display: "表示"
copy: "コピー" copy: "コピー"
copiedToClipboard: "クリップボードにコピーされたで"
metrics: "メトリクス" metrics: "メトリクス"
overview: "概要" overview: "概要"
logs: "ログ" logs: "ログ"
@ -787,6 +791,7 @@ wide: "広い"
narrow: "狭い" narrow: "狭い"
reloadToApplySetting: "設定はページリロード後に反映されるで。今リロードしとくか?" reloadToApplySetting: "設定はページリロード後に反映されるで。今リロードしとくか?"
needReloadToApply: "反映には再起動せなあかんで" needReloadToApply: "反映には再起動せなあかんで"
needToRestartServerToApply: "反映にはサーバーを再起動せなあかんのよ。"
showTitlebar: "タイトルバーを見せる" showTitlebar: "タイトルバーを見せる"
clearCache: "キャッシュをほかす" clearCache: "キャッシュをほかす"
onlineUsersCount: "{n}人が起きとるで" onlineUsersCount: "{n}人が起きとるで"
@ -974,6 +979,7 @@ document: "ドキュメント"
numberOfPageCache: "ページ、どんだけキャッシュすんの?" numberOfPageCache: "ページ、どんだけキャッシュすんの?"
numberOfPageCacheDescription: "増やすと使いやすくなるけど、負荷とメモリ使用量が増えてくで。一長一短やな。" numberOfPageCacheDescription: "増やすと使いやすくなるけど、負荷とメモリ使用量が増えてくで。一長一短やな。"
logoutConfirm: "ログアウトしまっか?" logoutConfirm: "ログアウトしまっか?"
logoutWillClearClientData: "ログアウトするとクライアントの設定情報がブラウザから消されてまうで。再ログイン時に設定情報を復元できるようにするためには、設定の自動バックアップを有効にするとええで。"
lastActiveDate: "最後に使った日時" lastActiveDate: "最後に使った日時"
statusbar: "ステータスバー" statusbar: "ステータスバー"
pleaseSelect: "選んだってやー" pleaseSelect: "選んだってやー"
@ -992,6 +998,7 @@ failedToUpload: "アップロードに失敗してもうたわ…"
cannotUploadBecauseInappropriate: "きわどい内容を含むかもしれへんって言われたからアップロードできへんわ。" cannotUploadBecauseInappropriate: "きわどい内容を含むかもしれへんって言われたからアップロードできへんわ。"
cannotUploadBecauseNoFreeSpace: "ドライブがもうパンパンやからアップロードできへんわ。" cannotUploadBecauseNoFreeSpace: "ドライブがもうパンパンやからアップロードできへんわ。"
cannotUploadBecauseExceedsFileSizeLimit: "ファイルが思うたよりも大きいさかいアップロードできへんでこれ。" cannotUploadBecauseExceedsFileSizeLimit: "ファイルが思うたよりも大きいさかいアップロードできへんでこれ。"
cannotUploadBecauseUnallowedFileType: "許可されてへんファイル種別やからアップロードできへんっぽい。"
beta: "ベータ" beta: "ベータ"
enableAutoSensitive: "自動できわどいか判断する" enableAutoSensitive: "自動できわどいか判断する"
enableAutoSensitiveDescription: "使える時は、機械学習を使って自動でメディアにNSFWフラグを設定するで。この機能をオフにしても、サーバーによっては自動で設定されることがあるで。" enableAutoSensitiveDescription: "使える時は、機械学習を使って自動でメディアにNSFWフラグを設定するで。この機能をオフにしても、サーバーによっては自動で設定されることがあるで。"
@ -1304,11 +1311,37 @@ federationSpecified: "このサーバーはホワイトリスト連合で運用
federationDisabled: "このサーバーは連合が無効化されてるで。他のサーバーのユーザーとやり取りすることはできひんで。" federationDisabled: "このサーバーは連合が無効化されてるで。他のサーバーのユーザーとやり取りすることはできひんで。"
confirmOnReact: "ツッコむときに確認とる" confirmOnReact: "ツッコむときに確認とる"
reactAreYouSure: "\" {emoji} \" でツッコむ?" reactAreYouSure: "\" {emoji} \" でツッコむ?"
markAsSensitiveConfirm: "このメディアをきわどい扱いしときますか?"
unmarkAsSensitiveConfirm: "このメディアはやっぱきわどくなかったってことでええんか?"
noName: "名前はあらへんで"
preferenceSyncConflictTitle: "サーバーに設定値があるみたいやわ"
preferenceSyncConflictText: "同期が有効にされた設定項目は設定値をサーバーに保存するねんけど、この設定項目はサーバーに保存されたやつがあるみたいやわ。どないするん?"
preferenceSyncConflictChoiceMerge: "ガッチャンコしよか"
preferenceSyncConflictChoiceCancel: "同期の有効化はやめとくわ"
postForm: "投稿フォーム" postForm: "投稿フォーム"
information: "情報" information: "情報"
migrateOldSettings: "旧設定情報をお引っ越し"
migrateOldSettings_description: "通常これは自動で行われるはずなんやけど、なんかの理由で上手く移行できへんかったときは手動で移行処理をポチっとできるで。今の設定情報は上書きされるで。"
settingsMigrating: "設定を移行しとるで。ちょっと待っとってな... (後で、設定→その他→旧設定情報を移行 で手動で移行することもできるで)"
driveAboutTip: "ドライブでは、今までアップロードしたファイルがずらーっと表示されるで。<br>\nートにファイルをもっかいのっけたり、あとで投稿するファイルをその辺に置いとくこともできるねん。<br>\n<b>ファイルをほかすと、前にそのファイルをのっけた全部の場所(ノート、ページ、アバター、バナー等)からも見えんくなるから気いつけてな。</b><br>\nフォルダを作って整理することもできるで。"
turnItOn: "オンにしとこ"
turnItOff: "オフでええわ"
emojiUnmute: "絵文字ミュートやめたる"
unmuteX: "{x}のミュートやめたる"
redisplayAllTips: "全部の「ヒントとコツ」をもっかい見して"
hideAllTips: "「ヒントとコツ」は全部表示せんでええ"
defaultImageCompressionLevel_description: "低くすると画質は保てるんやけど、ファイルサイズが増えるで。<br>高くするとファイルサイズは減らせるんやけど、画質が落ちるで。"
inMinutes: "分"
inDays: "日"
_chat: _chat:
noMessagesYet: "まだメッセージはあらへんで"
individualChat_description: "特定のユーザーと一対一でチャットができるで。"
roomChat_description: "複数人でチャットできるで。\nあと、個人チャットを許可してへんユーザーとでも、相手がええって言うならチャットできるで。"
inviteUserToChat: "ユーザーを招待してチャットを始めてみ"
invitations: "来てや" invitations: "来てや"
noInvitations: "招待はあらへんで"
noHistory: "履歴はないわ。" noHistory: "履歴はないわ。"
noRooms: "ルームはあらへんで"
members: "メンバーはん" members: "メンバーはん"
home: "ホーム" home: "ホーム"
send: "送信" send: "送信"
@ -2617,7 +2650,7 @@ _externalResourceInstaller:
_errors: _errors:
_invalidParams: _invalidParams:
title: "" title: ""
description: "" description: "外部サイトからデータを持ってくるのに欲しい情報が足らへんみたいやわ。URLは合っとる"
_resourceTypeNotSupported: _resourceTypeNotSupported:
title: "" title: ""
description: "" description: ""
@ -2648,7 +2681,7 @@ _dataSaver:
title: "アイコンの絵" title: "アイコンの絵"
description: "アイコン画像のアニメが止まるで。普通の画像よりもデータ量がでかいから、もっと通信量を節約できるねん。" description: "アイコン画像のアニメが止まるで。普通の画像よりもデータ量がでかいから、もっと通信量を節約できるねん。"
_code: _code:
title: "コードハイライト" title: "コードハイライトは表示せんでええ"
description: "MFMとかでコードハイライト記法が使われてるとき、タップするまで読み込まれへんくなるで。コードハイライトではハイライトする言語ごとにその決めてるファイルを読む必要はあんねんな。けどな、それは自動で読み込まれなくなるから、通信量を少なくできることができるねん。" description: "MFMとかでコードハイライト記法が使われてるとき、タップするまで読み込まれへんくなるで。コードハイライトではハイライトする言語ごとにその決めてるファイルを読む必要はあんねんな。けどな、それは自動で読み込まれなくなるから、通信量を少なくできることができるねん。"
_hemisphere: _hemisphere:
N: "北半球" N: "北半球"
@ -2858,3 +2891,8 @@ _watermarkEditor:
image: "画像" image: "画像"
advanced: "高度" advanced: "高度"
angle: "角度" angle: "角度"
_imageEffector:
discardChangesConfirm: "変更をせんで終わるか?"
_drafts:
deleteAreYouSure: "下書きをほかしてもええか?"
noDrafts: "下書きはあらへん"

View File

@ -1368,6 +1368,8 @@ redisplayAllTips: "모든 '팁과 유용한 정보'를 재표시"
hideAllTips: "모든 '팁과 유용한 정보'를 비표시" hideAllTips: "모든 '팁과 유용한 정보'를 비표시"
defaultImageCompressionLevel: "기본 이미지 압축 정도" defaultImageCompressionLevel: "기본 이미지 압축 정도"
defaultImageCompressionLevel_description: "낮추면 화질을 유지합니다만 파일 크기는 증가합니다. <br>높이면 파일 크기를 줄일 수 있습니다만 화질은 저하됩니다." defaultImageCompressionLevel_description: "낮추면 화질을 유지합니다만 파일 크기는 증가합니다. <br>높이면 파일 크기를 줄일 수 있습니다만 화질은 저하됩니다."
inMinutes: "분"
inDays: "일"
_order: _order:
newest: "최신 순" newest: "최신 순"
oldest: "오래된 순" oldest: "오래된 순"

View File

@ -461,6 +461,8 @@ replies: "Svar"
renotes: "Renote" renotes: "Renote"
surrender: "Avbryt" surrender: "Avbryt"
information: "Informasjon" information: "Informasjon"
inMinutes: "Minutter"
inDays: "Dager"
_chat: _chat:
invitations: "Inviter" invitations: "Inviter"
members: "Medlemmer" members: "Medlemmer"

View File

@ -1040,6 +1040,8 @@ surrender: "Odrzuć"
gameRetry: "Spróbuj ponownie" gameRetry: "Spróbuj ponownie"
postForm: "Formularz tworzenia wpisu" postForm: "Formularz tworzenia wpisu"
information: "Informacje" information: "Informacje"
inMinutes: "minuta"
inDays: "dzień"
_chat: _chat:
invitations: "Zaproś" invitations: "Zaproś"
noHistory: "Brak historii" noHistory: "Brak historii"

View File

@ -1368,6 +1368,8 @@ redisplayAllTips: "Mostrar todas as \"Dicas e Truques\" novamente"
hideAllTips: "Ocultas todas as \"Dicas e Truques\"" hideAllTips: "Ocultas todas as \"Dicas e Truques\""
defaultImageCompressionLevel: "Nível de compressão de imagem padrão" defaultImageCompressionLevel: "Nível de compressão de imagem padrão"
defaultImageCompressionLevel_description: "Alto, reduz o tamanho do arquivo mas, também, a qualidade da imagem.<br>Alto, reduz o tamanho do arquivo mas, também, a qualidade da imagem." defaultImageCompressionLevel_description: "Alto, reduz o tamanho do arquivo mas, também, a qualidade da imagem.<br>Alto, reduz o tamanho do arquivo mas, também, a qualidade da imagem."
inMinutes: "Minuto(s)"
inDays: "Dia(s)"
_order: _order:
newest: "Priorizar Mais Novos" newest: "Priorizar Mais Novos"
oldest: "Priorizar Mais Antigos" oldest: "Priorizar Mais Antigos"

View File

@ -2,7 +2,7 @@
_lang_: "Русский" _lang_: "Русский"
headlineMisskey: "Сеть, сплетённая из заметок" headlineMisskey: "Сеть, сплетённая из заметок"
introMisskey: "Добро пожаловать! Misskey — это децентрализованный сервис микроблогов с открытым исходным кодом.\nПишите «заметки» — делитесь со всеми происходящим вокруг или рассказывайте о себе 📡\nСтавьте «реакции» — выражайте свои чувства и эмоции от заметок других 👍\nОткройте для себя новый мир 🚀" introMisskey: "Добро пожаловать! Misskey — это децентрализованный сервис микроблогов с открытым исходным кодом.\nПишите «заметки» — делитесь со всеми происходящим вокруг или рассказывайте о себе 📡\nСтавьте «реакции» — выражайте свои чувства и эмоции от заметок других 👍\nОткройте для себя новый мир 🚀"
poweredByMisskeyDescription: "{name} сервис на платформе с открытым исходным кодом <b>Misskey</b>, называемый экземпляром Misskey." poweredByMisskeyDescription: "{name} один из инстансов (также называемый экземпляром Misskey), использующий платформу с открытым исходным кодом <b>Misskey</b>."
monthAndDay: "{day}.{month}" monthAndDay: "{day}.{month}"
search: "Поиск" search: "Поиск"
reset: "Сброс" reset: "Сброс"
@ -82,7 +82,7 @@ export: "Экспорт"
files: "Файлы" files: "Файлы"
download: "Скачать" download: "Скачать"
driveFileDeleteConfirm: "Удалить файл «{name}»? Заметки с ним также будут удалены." driveFileDeleteConfirm: "Удалить файл «{name}»? Заметки с ним также будут удалены."
unfollowConfirm: "Удалить из подписок пользователя {name}?" unfollowConfirm: "Отписаться от {name} ?"
exportRequested: "Вы запросили экспорт. Это может занять некоторое время. Результат будет добавлен на «Диск»." exportRequested: "Вы запросили экспорт. Это может занять некоторое время. Результат будет добавлен на «Диск»."
importRequested: "Вы запросили импорт. Это может занять некоторое время." importRequested: "Вы запросили импорт. Это может занять некоторое время."
lists: "Списки" lists: "Списки"
@ -298,6 +298,7 @@ uploadFromUrl: "Загрузить по ссылке"
uploadFromUrlDescription: "Ссылка на файл, который хотите загрузить" uploadFromUrlDescription: "Ссылка на файл, который хотите загрузить"
uploadFromUrlRequested: "Загрузка выбранного" uploadFromUrlRequested: "Загрузка выбранного"
uploadFromUrlMayTakeTime: "Загрузка может занять некоторое время." uploadFromUrlMayTakeTime: "Загрузка может занять некоторое время."
uploadNFiles: "Загрузить {n} файл"
explore: "Обзор" explore: "Обзор"
messageRead: "Прочитали" messageRead: "Прочитали"
noMoreHistory: "История закончилась" noMoreHistory: "История закончилась"
@ -575,8 +576,10 @@ showFixedPostForm: "Показывать поле для ввода новой
showFixedPostFormInChannel: "Показывать поле для ввода новой заметки наверху ленты (каналы)" showFixedPostFormInChannel: "Показывать поле для ввода новой заметки наверху ленты (каналы)"
withRepliesByDefaultForNewlyFollowed: "По умолчанию включайте ответы новых пользователей, на которых вы подписались, во временную шкалу" withRepliesByDefaultForNewlyFollowed: "По умолчанию включайте ответы новых пользователей, на которых вы подписались, во временную шкалу"
newNoteRecived: "Появилась новая заметка" newNoteRecived: "Появилась новая заметка"
newNote: "Новая заметка"
sounds: "Звуки" sounds: "Звуки"
sound: "Звуки" sound: "Звуки"
notificationSoundSettings: "Настройки звука уведомлений"
listen: "Слушать" listen: "Слушать"
none: "Ничего" none: "Ничего"
showInPage: "Показать страницу" showInPage: "Показать страницу"
@ -791,6 +794,7 @@ wide: "Толстый"
narrow: "Тонкий" narrow: "Тонкий"
reloadToApplySetting: "Это настройка вступает в силу при загрузке страницы. Перезагрузить сейчас?" reloadToApplySetting: "Это настройка вступает в силу при загрузке страницы. Перезагрузить сейчас?"
needReloadToApply: "Изменения вступят в силу после перезагрузки страницы." needReloadToApply: "Изменения вступят в силу после перезагрузки страницы."
needToRestartServerToApply: "Для вступления изменений в силу необходимо перезапустить сервер."
showTitlebar: "Показать заголовок" showTitlebar: "Показать заголовок"
clearCache: "Очистить кэш" clearCache: "Очистить кэш"
onlineUsersCount: "Пользователей сейчас в сети: {n}" onlineUsersCount: "Пользователей сейчас в сети: {n}"
@ -1176,13 +1180,25 @@ unused: "Неиспользованное"
used: "Использован" used: "Использован"
expired: "Срок действия приглашения истёк" expired: "Срок действия приглашения истёк"
doYouAgree: "Согласны?" doYouAgree: "Согласны?"
beSureToReadThisAsItIsImportant: "Это важно, поэтому, пожалуйста, прочтите это."
iHaveReadXCarefullyAndAgree: "Я прочитал(а) и согласен(сна) с условиями \"{x}"
dialog: "Диалог"
icon: "Аватар" icon: "Аватар"
currentAnnouncements: "Текущие новости"
pastAnnouncements: "Предыдущие новости"
youHaveUnreadAnnouncements: "У вас есть непрочитанные уведомления"
replies: "Ответы" replies: "Ответы"
renotes: "Репост" renotes: "Репост"
loadReplies: "Показать ответы" loadReplies: "Показать ответы"
loadConversation: "Загрузить беседу"
pinnedList: "Закреплённый список" pinnedList: "Закреплённый список"
keepScreenOn: "Держать экран включённым" keepScreenOn: "Держать экран включённым"
unnotifyNotes: "Отписаться от сообщений"
authentication: "Аутентификация"
authenticationRequiredToContinue: "Пожалуйста, пройдите аутентификацию, чтобы продолжить"
dateAndTime: "Дата и время"
showRenotes: "Показывать репосты" showRenotes: "Показывать репосты"
edited: "Изменено"
mutualFollow: "Взаимные подписки" mutualFollow: "Взаимные подписки"
followingOrFollower: "Подписки или подписчики" followingOrFollower: "Подписки или подписчики"
fileAttachedOnly: "Только заметки с файлами" fileAttachedOnly: "Только заметки с файлами"
@ -1193,30 +1209,71 @@ sourceCode: "Исходный код"
sourceCodeIsNotYetProvided: "Исходный код пока не доступен. Свяжитесь с администратором, чтобы исправить эту проблему." sourceCodeIsNotYetProvided: "Исходный код пока не доступен. Свяжитесь с администратором, чтобы исправить эту проблему."
repositoryUrl: "Ссылка на репозиторий" repositoryUrl: "Ссылка на репозиторий"
repositoryUrlDescription: "Если вы используете Misskey как есть (без изменений в исходном коде), введите https://github.com/misskey-dev/misskey" repositoryUrlDescription: "Если вы используете Misskey как есть (без изменений в исходном коде), введите https://github.com/misskey-dev/misskey"
feedback: "Обратная связь"
privacyPolicy: "Политика Конфиденциальности" privacyPolicy: "Политика Конфиденциальности"
privacyPolicyUrl: "Ссылка на Политику Конфиденциальности" privacyPolicyUrl: "Ссылка на Политику Конфиденциальности"
tosAndPrivacyPolicy: "Условия использования и политика конфиденциальности"
avatarDecorations: "Украшения для аватара"
attach: "Прикрепить" attach: "Прикрепить"
angle: "Угол" angle: "Угол"
flip: "Переворот" flip: "Переворот"
showAvatarDecorations: "Показать украшения для аватара"
pullDownToRefresh: "Опустите что бы обновить"
useGroupedNotifications: "Отображать уведомления сгруппировано" useGroupedNotifications: "Отображать уведомления сгруппировано"
signupPendingError: "Возникла проблема с подтверждением вашего адреса электронной почты. Возможно, срок действия ссылки истёк."
cwNotationRequired: "Если включена опция «Скрыть содержимое», необходимо написать аннотацию."
doReaction: "Добавить реакцию" doReaction: "Добавить реакцию"
code: "Код" code: "Код"
reloadRequiredToApplySettings: "Для применения настроек необходима обновить страницу."
remainingN: "Остаётся: {n}" remainingN: "Остаётся: {n}"
overwriteContentConfirm: "Текущее содержимое будет перезаписано. Вы уверены?"
seasonalScreenEffect: "Эффект времени года на экране" seasonalScreenEffect: "Эффект времени года на экране"
decorate: "Украсить" decorate: "Украсить"
addMfmFunction: "Добавить MFM" addMfmFunction: "Добавить MFM"
bubbleGame: "BubbleGame"
sfx: "Звуковые эффекты"
soundWillBePlayed: "Будет воспроизведен звук"
showReplay: "Показать повтор"
endReplay: "Конец повтора"
lastNDays: "Последние {n} сут" lastNDays: "Последние {n} сут"
hemisphere: "Место проживания" hemisphere: "Место проживания"
userSaysSomethingSensitive: "Сообщение, содержит конфиденциальные файлы от {name}"
enableHorizontalSwipe: "Смахните в сторону, чтобы сменить вкладки" enableHorizontalSwipe: "Смахните в сторону, чтобы сменить вкладки"
surrender: "Этот пост не может быть отменен." surrender: "Этот пост не может быть отменен."
gameRetry: "Повторить попытку"
notUsePleaseLeaveBlank: "Если не используется, оставьте пустым"
useNativeUIForVideoAudioPlayer: "Использовать интерфейс браузера при проигрывании видео и звука" useNativeUIForVideoAudioPlayer: "Использовать интерфейс браузера при проигрывании видео и звука"
keepOriginalFilename: "Сохранять исходное имя файла" keepOriginalFilename: "Сохранять исходное имя файла"
keepOriginalFilenameDescription: "Если вы выключите данную настройку, имена файлов будут автоматически заменены случайной строкой при загрузке." keepOriginalFilenameDescription: "Если вы выключите данную настройку, имена файлов будут автоматически заменены случайной строкой при загрузке."
alwaysConfirmFollow: "Всегда подтверждать подписку" alwaysConfirmFollow: "Всегда подтверждать подписку"
inquiry: "Связаться" inquiry: "Связаться"
fromX: "Из {x}"
genEmbedCode: "Сгенерировать код для "
noteOfThisUser: "Список заметок этого пользователя"
clipNoteLimitExceeded: "К этому клипу больше нельзя добавить заметки"
performance: "Производительность"
modified: "Изменено"
signinWithPasskey: "Войдите в систему, используя свой пароль"
unknownWebAuthnKey: "Не известный ключ "
passkeyVerificationFailed: "Ошибка проверка ключа доступа "
messageToFollower: "Сообщение подписчикам" messageToFollower: "Сообщение подписчикам"
testCaptchaWarning: "Эта функция предназначена для тестирования CAPTCHA. <strong>Не использовать это в рабочей среде</strong>"
prohibitedWordsForNameOfUser: "Запрещенные слова (имя пользователя)"
prohibitedWordsForNameOfUserDescription: "Если имя пользователя содержит строку из этого списка, изменение имени пользователя будет запрещено. На пользователей с правами модератора это ограничение не распространяется. Имена пользователей также проверяются путём замены всех букв в нижнем регистре"
yourNameContainsProhibitedWords: "Имя, которое вы пытаетесь изменить, содержит запрещенную строку символов"
yourNameContainsProhibitedWordsDescription: "Имя содержит запрещённую строку символов. Если вы хотите использовать это имя, обратитесь к администратору сервера"
thisContentsAreMarkedAsSigninRequiredByAuthor: "Автор сообщения установил требование в виде авторизации для просмотра"
lockdown: "Доступ ограничен"
pleaseSelectAccount: "Выберите свой аккаунт"
availableRoles: "Доступные роли"
federationDisabled: "Федерация отключена для этого сервера. Вы не можете взаимодействовать с пользователями на других серверах."
draft: "Черновик"
markAsSensitiveConfirm: "Отметить контент как чувствительный?"
resetToDefaultValue: "Сбросить настройки до стандартных"
postForm: "Форма отправки" postForm: "Форма отправки"
information: "Описание" information: "Описание"
inMinutes: "мин"
inDays: "сут"
_chat: _chat:
invitations: "Пригласить" invitations: "Пригласить"
noHistory: "История пока пуста" noHistory: "История пока пуста"
@ -2200,3 +2257,4 @@ _watermarkEditor:
image: "Изображения" image: "Изображения"
advanced: "Для продвинутых" advanced: "Для продвинутых"
angle: "Угол" angle: "Угол"
drafts: "Черновик"

View File

@ -913,6 +913,8 @@ flip: "Preklopiť"
lastNDays: "Posledných {n} dní" lastNDays: "Posledných {n} dní"
postForm: "Napísať poznámku" postForm: "Napísať poznámku"
information: "Informácie" information: "Informácie"
inMinutes: "min"
inDays: "dní"
_chat: _chat:
invitations: "Pozvať" invitations: "Pozvať"
noHistory: "Žiadna história" noHistory: "Žiadna história"

View File

@ -1368,6 +1368,8 @@ redisplayAllTips: "แสดงคำแนะนำและเคล็ดล
hideAllTips: "ซ่อนคำแนะนำและเคล็ดลับทั้งหมด" hideAllTips: "ซ่อนคำแนะนำและเคล็ดลับทั้งหมด"
defaultImageCompressionLevel: "ความละเอียดเริ่มต้นสำหรับการบีบอัดภาพ" defaultImageCompressionLevel: "ความละเอียดเริ่มต้นสำหรับการบีบอัดภาพ"
defaultImageCompressionLevel_description: "หากตั้งค่าต่ำ จะรักษาคุณภาพภาพได้ดีขึ้นแต่ขนาดไฟล์จะเพิ่มขึ้น<br>หากตั้งค่าสูง จะลดขนาดไฟล์ได้ แต่คุณภาพภาพจะลดลง" defaultImageCompressionLevel_description: "หากตั้งค่าต่ำ จะรักษาคุณภาพภาพได้ดีขึ้นแต่ขนาดไฟล์จะเพิ่มขึ้น<br>หากตั้งค่าสูง จะลดขนาดไฟล์ได้ แต่คุณภาพภาพจะลดลง"
inMinutes: "นาที"
inDays: "วัน"
_order: _order:
newest: "เรียงจากใหม่ไปเก่า" newest: "เรียงจากใหม่ไปเก่า"
oldest: "เรียงจากเก่าไปใหม่" oldest: "เรียงจากเก่าไปใหม่"

View File

@ -919,6 +919,8 @@ flip: "Перевернути"
lastNDays: "Останні {n} днів" lastNDays: "Останні {n} днів"
postForm: "Створення нотатки" postForm: "Створення нотатки"
information: "Інформація" information: "Інформація"
inMinutes: "х"
inDays: "д"
_chat: _chat:
invitations: "Запросити" invitations: "Запросити"
noHistory: "Історія порожня" noHistory: "Історія порожня"

View File

@ -1221,6 +1221,8 @@ information: "Giới thiệu"
chat: "Trò chuyện" chat: "Trò chuyện"
migrateOldSettings: "Di chuyển cài đặt cũ" migrateOldSettings: "Di chuyển cài đặt cũ"
migrateOldSettings_description: "Thông thường, quá trình này diễn ra tự động, nhưng nếu vì lý do nào đó mà quá trình di chuyển không thành công, bạn có thể kích hoạt thủ công quy trình di chuyển, quá trình này sẽ ghi đè lên thông tin cấu hình hiện tại của bạn." migrateOldSettings_description: "Thông thường, quá trình này diễn ra tự động, nhưng nếu vì lý do nào đó mà quá trình di chuyển không thành công, bạn có thể kích hoạt thủ công quy trình di chuyển, quá trình này sẽ ghi đè lên thông tin cấu hình hiện tại của bạn."
inMinutes: "phút"
inDays: "ngày"
_chat: _chat:
invitations: "Mời" invitations: "Mời"
noHistory: "Không có dữ liệu" noHistory: "Không có dữ liệu"

View File

@ -1318,7 +1318,7 @@ confirmOnReact: "发送回应前需要确认"
reactAreYouSure: "要用「{emoji}」进行回应吗?" reactAreYouSure: "要用「{emoji}」进行回应吗?"
markAsSensitiveConfirm: "要将此媒体标记为敏感吗?" markAsSensitiveConfirm: "要将此媒体标记为敏感吗?"
unmarkAsSensitiveConfirm: "要将此媒体解除敏感标记吗?" unmarkAsSensitiveConfirm: "要将此媒体解除敏感标记吗?"
preferences: "设置" preferences: "偏好设置"
accessibility: "辅助功能" accessibility: "辅助功能"
preferencesProfile: "设置的配置" preferencesProfile: "设置的配置"
copyPreferenceId: "复制设置 ID" copyPreferenceId: "复制设置 ID"
@ -1368,6 +1368,8 @@ redisplayAllTips: "重新显示所有的提示和技巧"
hideAllTips: "隐藏所有的提示和技巧" hideAllTips: "隐藏所有的提示和技巧"
defaultImageCompressionLevel: "默认图像压缩等级" defaultImageCompressionLevel: "默认图像压缩等级"
defaultImageCompressionLevel_description: "较低的等级可以保持画质,但会增加文件大小。<br>较高的等级可以减少文件大小,但相对应的画质将会降低。" defaultImageCompressionLevel_description: "较低的等级可以保持画质,但会增加文件大小。<br>较高的等级可以减少文件大小,但相对应的画质将会降低。"
inMinutes: "分"
inDays: "日"
_order: _order:
newest: "从新到旧" newest: "从新到旧"
oldest: "从旧到新" oldest: "从旧到新"
@ -1927,7 +1929,7 @@ _role:
name: "角色名称" name: "角色名称"
description: "角色描述" description: "角色描述"
permission: "角色权限" permission: "角色权限"
descriptionOfPermission: "<b>监察员</b>可以执行基本地审核操作。\n<b>管理员</b>可以更改服务器的所有设置。" descriptionOfPermission: "<b>监察员</b>可以执行基本的审核操作。\n<b>管理员</b>可以更改实例的所有设置。"
assignTarget: "授权对象" assignTarget: "授权对象"
descriptionOfAssignTarget: "<b>手动</b>指手动选择谁被包括在这个角色中。\n<b>符合条件</b>指设置条件以自动包括符合条件的用户。" descriptionOfAssignTarget: "<b>手动</b>指手动选择谁被包括在这个角色中。\n<b>符合条件</b>指设置条件以自动包括符合条件的用户。"
manual: "手动" manual: "手动"

View File

@ -638,7 +638,7 @@ inboxUrl: "收件夾 URL"
addedRelays: "已加入的中繼器" addedRelays: "已加入的中繼器"
serviceworkerInfo: "如要使用推播通知,需要啟用此選項並設定金鑰。" serviceworkerInfo: "如要使用推播通知,需要啟用此選項並設定金鑰。"
deletedNote: "已刪除的貼文" deletedNote: "已刪除的貼文"
invisibleNote: "私貼文" invisibleNote: "私密的貼文"
enableInfiniteScroll: "啟用自動滾動頁面模式" enableInfiniteScroll: "啟用自動滾動頁面模式"
visibility: "可見性" visibility: "可見性"
poll: "票選活動" poll: "票選活動"
@ -1368,6 +1368,8 @@ redisplayAllTips: "重新顯示所有「提示與技巧」"
hideAllTips: "隱藏所有「提示與技巧」" hideAllTips: "隱藏所有「提示與技巧」"
defaultImageCompressionLevel: "預設的影像壓縮程度" defaultImageCompressionLevel: "預設的影像壓縮程度"
defaultImageCompressionLevel_description: "低的話可以保留畫質,但是會增加檔案的大小。<br>高的話可以減少檔案大小,但是會降低畫質。" defaultImageCompressionLevel_description: "低的話可以保留畫質,但是會增加檔案的大小。<br>高的話可以減少檔案大小,但是會降低畫質。"
inMinutes: "分鐘"
inDays: "日"
_order: _order:
newest: "最新的在前" newest: "最新的在前"
oldest: "最舊的在前" oldest: "最舊的在前"

View File

@ -1,6 +1,6 @@
{ {
"name": "misskey", "name": "misskey",
"version": "2025.7.0", "version": "2025.8.0-alpha.2",
"codename": "nasubi", "codename": "nasubi",
"repository": { "repository": {
"type": "git", "type": "git",

View File

@ -0,0 +1,20 @@
/*
* SPDX-FileCopyrightText: syuilo and misskey-project
* SPDX-License-Identifier: AGPL-3.0-only
*/
export class RemoteNotesCleaning1753863104203 {
name = 'RemoteNotesCleaning1753863104203'
async up(queryRunner) {
await queryRunner.query(`ALTER TABLE "meta" ADD "enableRemoteNotesCleaning" boolean NOT NULL DEFAULT true`);
await queryRunner.query('ALTER TABLE "meta" ADD "remoteNotesCleaningMaxProcessingDurationInMinutes" integer NOT NULL DEFAULT \'60\'');
await queryRunner.query('ALTER TABLE "meta" ADD "remoteNotesCleaningExpiryDaysForEachNotes" integer NOT NULL DEFAULT \'90\'');
}
async down(queryRunner) {
await queryRunner.query('ALTER TABLE "meta" DROP COLUMN "remoteNotesCleaningExpiryDaysForEachNotes"');
await queryRunner.query('ALTER TABLE "meta" DROP COLUMN "remoteNotesCleaningMaxProcessingDurationInMinutes"');
await queryRunner.query(`ALTER TABLE "meta" DROP COLUMN "enableRemoteNotesCleaning"`);
}
}

View File

@ -0,0 +1,18 @@
/*
* SPDX-FileCopyrightText: syuilo and misskey-project
* SPDX-License-Identifier: AGPL-3.0-only
*/
export class TweakDefaultFederationSettings1754019326356 {
name = 'TweakDefaultFederationSettings1754019326356'
async up(queryRunner) {
await queryRunner.query(`ALTER TABLE "meta" ALTER COLUMN "federation" SET DEFAULT 'none'`);
await queryRunner.query(`ALTER TABLE "meta" ALTER COLUMN "enableRemoteNotesCleaning" SET DEFAULT false`);
}
async down(queryRunner) {
await queryRunner.query(`ALTER TABLE "meta" ALTER COLUMN "enableRemoteNotesCleaning" SET DEFAULT true`);
await queryRunner.query(`ALTER TABLE "meta" ALTER COLUMN "federation" SET DEFAULT 'all'`);
}
}

View File

@ -4,7 +4,7 @@
"private": true, "private": true,
"type": "module", "type": "module",
"engines": { "engines": {
"node": "^20.18.1 || ^22.0.0" "node": "^22.15.0"
}, },
"scripts": { "scripts": {
"start": "node ./built/boot/entry.js", "start": "node ./built/boot/entry.js",

View File

@ -53,6 +53,37 @@ export const QUEUE_TYPES = [
'systemWebhookDeliver', 'systemWebhookDeliver',
] as const; ] as const;
const REPEATABLE_SYSTEM_JOB_DEF = [{
name: 'tickCharts',
pattern: '55 * * * *',
}, {
name: 'resyncCharts',
pattern: '0 0 * * *',
}, {
name: 'cleanCharts',
pattern: '0 0 * * *',
}, {
name: 'aggregateRetention',
pattern: '0 0 * * *',
}, {
name: 'clean',
pattern: '0 0 * * *',
}, {
name: 'checkExpiredMutings',
pattern: '*/5 * * * *',
}, {
name: 'bakeBufferedReactions',
pattern: '0 0 * * *',
}, {
name: 'checkModeratorsActivity',
// 毎時30分に起動
pattern: '30 * * * *',
}, {
name: 'cleanRemoteNotes',
// 毎日午前4時に起動(最も人の少ない時間帯)
pattern: '0 4 * * *',
}];
@Injectable() @Injectable()
export class QueueService { export class QueueService {
constructor( constructor(
@ -69,85 +100,30 @@ export class QueueService {
@Inject('queue:userWebhookDeliver') public userWebhookDeliverQueue: UserWebhookDeliverQueue, @Inject('queue:userWebhookDeliver') public userWebhookDeliverQueue: UserWebhookDeliverQueue,
@Inject('queue:systemWebhookDeliver') public systemWebhookDeliverQueue: SystemWebhookDeliverQueue, @Inject('queue:systemWebhookDeliver') public systemWebhookDeliverQueue: SystemWebhookDeliverQueue,
) { ) {
this.systemQueue.upsertJobScheduler('tickCharts', { for (const def of REPEATABLE_SYSTEM_JOB_DEF) {
pattern: '55 * * * *', this.systemQueue.upsertJobScheduler(def.name, {
pattern: def.pattern,
}, { }, {
name: 'tickCharts', name: def.name,
opts: { opts: {
removeOnComplete: 10, // 期限ではなくcountで設定したいが、ジョブごとではなくキュー全体でカウントされるため、高頻度で実行されるジョブによって低頻度で実行されるジョブのログが消えることになる
removeOnFail: 30, removeOnComplete: {
age: 3600 * 24 * 7, // keep up to 7 days
},
removeOnFail: {
age: 3600 * 24 * 7, // keep up to 7 days
},
}, },
}); });
}
this.systemQueue.upsertJobScheduler('resyncCharts', { // 古いバージョンで作成され現在使われなくなったrepeatableジョブをクリーンアップ
pattern: '0 0 * * *', this.systemQueue.getJobSchedulers().then(schedulers => {
}, { for (const scheduler of schedulers) {
name: 'resyncCharts', if (!REPEATABLE_SYSTEM_JOB_DEF.some(def => def.name === scheduler.key)) {
opts: { this.systemQueue.removeJobScheduler(scheduler.key);
removeOnComplete: 10, }
removeOnFail: 30, }
},
});
this.systemQueue.upsertJobScheduler('cleanCharts', {
pattern: '0 0 * * *',
}, {
name: 'cleanCharts',
opts: {
removeOnComplete: 10,
removeOnFail: 30,
},
});
this.systemQueue.upsertJobScheduler('aggregateRetention', {
pattern: '0 0 * * *',
}, {
name: 'aggregateRetention',
opts: {
removeOnComplete: 10,
removeOnFail: 30,
},
});
this.systemQueue.upsertJobScheduler('clean', {
pattern: '0 0 * * *',
}, {
name: 'clean',
opts: {
removeOnComplete: 10,
removeOnFail: 30,
},
});
this.systemQueue.upsertJobScheduler('checkExpiredMutings', {
pattern: '*/5 * * * *',
}, {
name: 'checkExpiredMutings',
opts: {
removeOnComplete: 10,
removeOnFail: 30,
},
});
this.systemQueue.upsertJobScheduler('bakeBufferedReactions', {
pattern: '0 0 * * *',
}, {
name: 'bakeBufferedReactions',
opts: {
removeOnComplete: 10,
removeOnFail: 30,
},
});
this.systemQueue.upsertJobScheduler('checkModeratorsActivity', {
// 毎時30分に起動
pattern: '30 * * * *',
}, {
name: 'checkModeratorsActivity',
opts: {
removeOnComplete: 10,
removeOnFail: 30,
},
}); });
} }
@ -834,6 +810,13 @@ export class QueueService {
} }
} }
@bindThis
public async queueGetJobLogs(queueType: typeof QUEUE_TYPES[number], jobId: string) {
const queue = this.getQueue(queueType);
const result = await queue.getJobLogs(jobId);
return result.logs;
}
@bindThis @bindThis
public async queueGetJobs(queueType: typeof QUEUE_TYPES[number], jobTypes: JobType[], search?: string) { public async queueGetJobs(queueType: typeof QUEUE_TYPES[number], jobTypes: JobType[], search?: string) {
const RETURN_LIMIT = 100; const RETURN_LIMIT = 100;

View File

@ -227,9 +227,9 @@ export class SearchService {
if (opts.host) { if (opts.host) {
if (opts.host === '.') { if (opts.host === '.') {
query.andWhere('user.host IS NULL'); query.andWhere('note.userHost IS NULL');
} else { } else {
query.andWhere('user.host = :host', { host: opts.host }); query.andWhere('note.userHost = :host', { host: opts.host });
} }
} }

View File

@ -654,7 +654,7 @@ export class MiMeta {
@Column('varchar', { @Column('varchar', {
length: 128, length: 128,
default: 'all', default: 'none',
}) })
public federation: 'all' | 'specified' | 'none'; public federation: 'all' | 'specified' | 'none';
@ -701,6 +701,21 @@ export class MiMeta {
default: true, default: true,
}) })
public allowExternalApRedirect: boolean; public allowExternalApRedirect: boolean;
@Column('boolean', {
default: false,
})
public enableRemoteNotesCleaning: boolean;
@Column('integer', {
default: 60, // minutes
})
public remoteNotesCleaningMaxProcessingDurationInMinutes: number;
@Column('integer', {
default: 90, // days
})
public remoteNotesCleaningExpiryDaysForEachNotes: number;
} }
export type SoftwareSuspension = { export type SoftwareSuspension = {

View File

@ -6,7 +6,6 @@
import { Module } from '@nestjs/common'; import { Module } from '@nestjs/common';
import { CoreModule } from '@/core/CoreModule.js'; import { CoreModule } from '@/core/CoreModule.js';
import { GlobalModule } from '@/GlobalModule.js'; import { GlobalModule } from '@/GlobalModule.js';
import { CheckModeratorsActivityProcessorService } from '@/queue/processors/CheckModeratorsActivityProcessorService.js';
import { QueueLoggerService } from './QueueLoggerService.js'; import { QueueLoggerService } from './QueueLoggerService.js';
import { QueueProcessorService } from './QueueProcessorService.js'; import { QueueProcessorService } from './QueueProcessorService.js';
import { DeliverProcessorService } from './processors/DeliverProcessorService.js'; import { DeliverProcessorService } from './processors/DeliverProcessorService.js';
@ -18,6 +17,8 @@ import { CheckExpiredMutingsProcessorService } from './processors/CheckExpiredMu
import { BakeBufferedReactionsProcessorService } from './processors/BakeBufferedReactionsProcessorService.js'; import { BakeBufferedReactionsProcessorService } from './processors/BakeBufferedReactionsProcessorService.js';
import { CleanChartsProcessorService } from './processors/CleanChartsProcessorService.js'; import { CleanChartsProcessorService } from './processors/CleanChartsProcessorService.js';
import { CleanProcessorService } from './processors/CleanProcessorService.js'; import { CleanProcessorService } from './processors/CleanProcessorService.js';
import { CheckModeratorsActivityProcessorService } from './processors/CheckModeratorsActivityProcessorService.js';
import { CleanRemoteNotesProcessorService } from './processors/CleanRemoteNotesProcessorService.js';
import { CleanRemoteFilesProcessorService } from './processors/CleanRemoteFilesProcessorService.js'; import { CleanRemoteFilesProcessorService } from './processors/CleanRemoteFilesProcessorService.js';
import { DeleteAccountProcessorService } from './processors/DeleteAccountProcessorService.js'; import { DeleteAccountProcessorService } from './processors/DeleteAccountProcessorService.js';
import { DeleteDriveFilesProcessorService } from './processors/DeleteDriveFilesProcessorService.js'; import { DeleteDriveFilesProcessorService } from './processors/DeleteDriveFilesProcessorService.js';
@ -83,6 +84,7 @@ import { RelationshipProcessorService } from './processors/RelationshipProcessor
AggregateRetentionProcessorService, AggregateRetentionProcessorService,
CheckExpiredMutingsProcessorService, CheckExpiredMutingsProcessorService,
CheckModeratorsActivityProcessorService, CheckModeratorsActivityProcessorService,
CleanRemoteNotesProcessorService,
QueueProcessorService, QueueProcessorService,
], ],
exports: [ exports: [

View File

@ -43,6 +43,7 @@ import { CheckExpiredMutingsProcessorService } from './processors/CheckExpiredMu
import { BakeBufferedReactionsProcessorService } from './processors/BakeBufferedReactionsProcessorService.js'; import { BakeBufferedReactionsProcessorService } from './processors/BakeBufferedReactionsProcessorService.js';
import { CleanProcessorService } from './processors/CleanProcessorService.js'; import { CleanProcessorService } from './processors/CleanProcessorService.js';
import { AggregateRetentionProcessorService } from './processors/AggregateRetentionProcessorService.js'; import { AggregateRetentionProcessorService } from './processors/AggregateRetentionProcessorService.js';
import { CleanRemoteNotesProcessorService } from './processors/CleanRemoteNotesProcessorService.js';
import { QueueLoggerService } from './QueueLoggerService.js'; import { QueueLoggerService } from './QueueLoggerService.js';
import { QUEUE, baseWorkerOptions } from './const.js'; import { QUEUE, baseWorkerOptions } from './const.js';
@ -123,6 +124,7 @@ export class QueueProcessorService implements OnApplicationShutdown {
private bakeBufferedReactionsProcessorService: BakeBufferedReactionsProcessorService, private bakeBufferedReactionsProcessorService: BakeBufferedReactionsProcessorService,
private checkModeratorsActivityProcessorService: CheckModeratorsActivityProcessorService, private checkModeratorsActivityProcessorService: CheckModeratorsActivityProcessorService,
private cleanProcessorService: CleanProcessorService, private cleanProcessorService: CleanProcessorService,
private cleanRemoteNotesProcessorService: CleanRemoteNotesProcessorService,
) { ) {
this.logger = this.queueLoggerService.logger; this.logger = this.queueLoggerService.logger;
@ -164,6 +166,7 @@ export class QueueProcessorService implements OnApplicationShutdown {
case 'bakeBufferedReactions': return this.bakeBufferedReactionsProcessorService.process(); case 'bakeBufferedReactions': return this.bakeBufferedReactionsProcessorService.process();
case 'checkModeratorsActivity': return this.checkModeratorsActivityProcessorService.process(); case 'checkModeratorsActivity': return this.checkModeratorsActivityProcessorService.process();
case 'clean': return this.cleanProcessorService.process(); case 'clean': return this.cleanProcessorService.process();
case 'cleanRemoteNotes': return this.cleanRemoteNotesProcessorService.process(job);
default: throw new Error(`unrecognized job type ${job.name} for system`); default: throw new Error(`unrecognized job type ${job.name} for system`);
} }
}; };

View File

@ -0,0 +1,174 @@
/*
* SPDX-FileCopyrightText: syuilo and misskey-project
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { setTimeout } from 'node:timers/promises';
import { Inject, Injectable } from '@nestjs/common';
import { And, In, IsNull, LessThan, MoreThan, Not } from 'typeorm';
import { DI } from '@/di-symbols.js';
import type { MiMeta, MiNote, NoteFavoritesRepository, NotesRepository, UserNotePiningsRepository } from '@/models/_.js';
import type Logger from '@/logger.js';
import { bindThis } from '@/decorators.js';
import { IdService } from '@/core/IdService.js';
import { QueueLoggerService } from '../QueueLoggerService.js';
import type * as Bull from 'bullmq';
@Injectable()
export class CleanRemoteNotesProcessorService {
private logger: Logger;
constructor(
@Inject(DI.meta)
private meta: MiMeta,
@Inject(DI.notesRepository)
private notesRepository: NotesRepository,
@Inject(DI.noteFavoritesRepository)
private noteFavoritesRepository: NoteFavoritesRepository,
@Inject(DI.userNotePiningsRepository)
private userNotePiningsRepository: UserNotePiningsRepository,
private idService: IdService,
private queueLoggerService: QueueLoggerService,
) {
this.logger = this.queueLoggerService.logger.createSubLogger('clean-remote-notes');
}
@bindThis
public async process(job: Bull.Job<Record<string, unknown>>): Promise<{
deletedCount: number;
oldest: number | null;
newest: number | null;
skipped?: boolean;
}> {
if (!this.meta.enableRemoteNotesCleaning) {
this.logger.info('Remote notes cleaning is disabled, skipping...');
return {
deletedCount: 0,
oldest: null,
newest: null,
skipped: true,
};
}
this.logger.info('cleaning remote notes...');
const maxDuration = this.meta.remoteNotesCleaningMaxProcessingDurationInMinutes * 60 * 1000; // Convert minutes to milliseconds
const startAt = Date.now();
const MAX_NOTE_COUNT_PER_QUERY = 50;
const stats = {
deletedCount: 0,
oldest: null as number | null,
newest: null as number | null,
};
let cursor: MiNote['id'] = this.idService.gen(Date.now() - (1000 * 60 * 60 * 24 * this.meta.remoteNotesCleaningExpiryDaysForEachNotes));
while (true) {
const batchBeginAt = Date.now();
let notes: Pick<MiNote, 'id'>[] = await this.notesRepository.find({
where: {
id: LessThan(cursor),
userHost: Not(IsNull()),
clippedCount: 0,
renoteCount: 0,
},
take: MAX_NOTE_COUNT_PER_QUERY,
order: {
// 新しい順
// https://github.com/misskey-dev/misskey/pull/16292#issuecomment-3139376314
id: -1,
},
select: ['id'],
});
const fetchedCount = notes.length;
for (const note of notes) {
if (note.id < cursor) {
cursor = note.id;
}
}
const pinings = notes.length === 0 ? [] : await this.userNotePiningsRepository.find({
where: {
noteId: In(notes.map(note => note.id)),
},
select: ['noteId'],
});
notes = notes.filter(note => {
return !pinings.some(pining => pining.noteId === note.id);
});
const favorites = notes.length === 0 ? [] : await this.noteFavoritesRepository.find({
where: {
noteId: In(notes.map(note => note.id)),
},
select: ['noteId'],
});
notes = notes.filter(note => {
return !favorites.some(favorite => favorite.noteId === note.id);
});
const replies = notes.length === 0 ? [] : await this.notesRepository.find({
where: {
replyId: In(notes.map(note => note.id)),
userHost: IsNull(),
},
select: ['replyId'],
});
notes = notes.filter(note => {
return !replies.some(reply => reply.replyId === note.id);
});
if (notes.length > 0) {
await this.notesRepository.delete(notes.map(note => note.id));
for (const note of notes) {
const t = this.idService.parse(note.id).date.getTime();
if (stats.oldest === null || t < stats.oldest) {
stats.oldest = t;
}
if (stats.newest === null || t > stats.newest) {
stats.newest = t;
}
}
stats.deletedCount += notes.length;
}
job.log(`Deleted ${notes.length} of ${fetchedCount}; ${Date.now() - batchBeginAt}ms`);
const elapsed = Date.now() - startAt;
if (elapsed >= maxDuration) {
this.logger.info(`Reached maximum duration of ${maxDuration}ms, stopping...`);
job.log('Reached maximum duration, stopping cleaning.');
job.updateProgress(100);
break;
}
job.updateProgress((elapsed / maxDuration) * 100);
await setTimeout(1000 * 5); // Wait a moment to avoid overwhelming the db
}
this.logger.succ('cleaning of remote notes completed.');
return {
deletedCount: stats.deletedCount,
oldest: stats.oldest,
newest: stats.newest,
skipped: false,
};
}
}

View File

@ -70,6 +70,7 @@ export * as 'admin/queue/inbox-delayed' from './endpoints/admin/queue/inbox-dela
export * as 'admin/queue/retry-job' from './endpoints/admin/queue/retry-job.js'; export * as 'admin/queue/retry-job' from './endpoints/admin/queue/retry-job.js';
export * as 'admin/queue/remove-job' from './endpoints/admin/queue/remove-job.js'; export * as 'admin/queue/remove-job' from './endpoints/admin/queue/remove-job.js';
export * as 'admin/queue/show-job' from './endpoints/admin/queue/show-job.js'; export * as 'admin/queue/show-job' from './endpoints/admin/queue/show-job.js';
export * as 'admin/queue/show-job-logs' from './endpoints/admin/queue/show-job-logs.js';
export * as 'admin/queue/promote-jobs' from './endpoints/admin/queue/promote-jobs.js'; export * as 'admin/queue/promote-jobs' from './endpoints/admin/queue/promote-jobs.js';
export * as 'admin/queue/jobs' from './endpoints/admin/queue/jobs.js'; export * as 'admin/queue/jobs' from './endpoints/admin/queue/jobs.js';
export * as 'admin/queue/stats' from './endpoints/admin/queue/stats.js'; export * as 'admin/queue/stats' from './endpoints/admin/queue/stats.js';

View File

@ -571,6 +571,18 @@ export const meta = {
type: 'boolean', type: 'boolean',
optional: false, nullable: false, optional: false, nullable: false,
}, },
enableRemoteNotesCleaning: {
type: 'boolean',
optional: false, nullable: false,
},
remoteNotesCleaningExpiryDaysForEachNotes: {
type: 'number',
optional: false, nullable: false,
},
remoteNotesCleaningMaxProcessingDurationInMinutes: {
type: 'number',
optional: false, nullable: false,
},
}, },
}, },
} as const; } as const;
@ -722,6 +734,9 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
proxyRemoteFiles: instance.proxyRemoteFiles, proxyRemoteFiles: instance.proxyRemoteFiles,
signToActivityPubGet: instance.signToActivityPubGet, signToActivityPubGet: instance.signToActivityPubGet,
allowExternalApRedirect: instance.allowExternalApRedirect, allowExternalApRedirect: instance.allowExternalApRedirect,
enableRemoteNotesCleaning: instance.enableRemoteNotesCleaning,
remoteNotesCleaningExpiryDaysForEachNotes: instance.remoteNotesCleaningExpiryDaysForEachNotes,
remoteNotesCleaningMaxProcessingDurationInMinutes: instance.remoteNotesCleaningMaxProcessingDurationInMinutes,
}; };
}); });
} }

View File

@ -0,0 +1,45 @@
/*
* SPDX-FileCopyrightText: syuilo and misskey-project
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { Injectable } from '@nestjs/common';
import { Endpoint } from '@/server/api/endpoint-base.js';
import { QUEUE_TYPES, QueueService } from '@/core/QueueService.js';
export const meta = {
tags: ['admin'],
requireCredential: true,
requireModerator: true,
kind: 'read:admin:queue',
res: {
type: 'array',
optional: false, nullable: false,
items: {
optional: false, nullable: false,
type: 'string',
},
},
} as const;
export const paramDef = {
type: 'object',
properties: {
queue: { type: 'string', enum: QUEUE_TYPES },
jobId: { type: 'string' },
},
required: ['queue', 'jobId'],
} as const;
@Injectable()
export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export
constructor(
private queueService: QueueService,
) {
super(meta, paramDef, async (ps, me) => {
return this.queueService.queueGetJobLogs(ps.queue, ps.jobId);
});
}
}

View File

@ -205,6 +205,9 @@ export const paramDef = {
proxyRemoteFiles: { type: 'boolean' }, proxyRemoteFiles: { type: 'boolean' },
signToActivityPubGet: { type: 'boolean' }, signToActivityPubGet: { type: 'boolean' },
allowExternalApRedirect: { type: 'boolean' }, allowExternalApRedirect: { type: 'boolean' },
enableRemoteNotesCleaning: { type: 'boolean' },
remoteNotesCleaningExpiryDaysForEachNotes: { type: 'number' },
remoteNotesCleaningMaxProcessingDurationInMinutes: { type: 'number' },
}, },
required: [], required: [],
} as const; } as const;
@ -723,6 +726,18 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
set.allowExternalApRedirect = ps.allowExternalApRedirect; set.allowExternalApRedirect = ps.allowExternalApRedirect;
} }
if (ps.enableRemoteNotesCleaning !== undefined) {
set.enableRemoteNotesCleaning = ps.enableRemoteNotesCleaning;
}
if (ps.remoteNotesCleaningExpiryDaysForEachNotes !== undefined) {
set.remoteNotesCleaningExpiryDaysForEachNotes = ps.remoteNotesCleaningExpiryDaysForEachNotes;
}
if (ps.remoteNotesCleaningMaxProcessingDurationInMinutes !== undefined) {
set.remoteNotesCleaningMaxProcessingDurationInMinutes = ps.remoteNotesCleaningMaxProcessingDurationInMinutes;
}
const before = await this.metaService.fetch(true); const before = await this.metaService.fetch(true);
await this.metaService.update(set); await this.metaService.update(set);

View File

@ -194,6 +194,10 @@ export class ClientServerService {
], ],
}, },
}, },
'shortcuts': [{
'name': 'Safemode',
'url': '/?safemode=true',
}],
}; };
manifest = { manifest = {

View File

@ -94,7 +94,19 @@
} }
//#endregion //#endregion
let isSafeMode = (localStorage.getItem('isSafeMode') === 'true');
if (!isSafeMode) {
const urlParams = new URLSearchParams(window.location.search);
if (urlParams.has('safemode') && urlParams.get('safemode') === 'true') {
localStorage.setItem('isSafeMode', 'true');
isSafeMode = true;
}
}
//#region Theme //#region Theme
if (!isSafeMode) {
const theme = localStorage.getItem('theme'); const theme = localStorage.getItem('theme');
if (theme) { if (theme) {
for (const [k, v] of Object.entries(JSON.parse(theme))) { for (const [k, v] of Object.entries(JSON.parse(theme))) {
@ -111,6 +123,8 @@
} }
} }
} }
}
const colorScheme = localStorage.getItem('colorScheme'); const colorScheme = localStorage.getItem('colorScheme');
if (colorScheme) { if (colorScheme) {
document.documentElement.style.setProperty('color-scheme', colorScheme); document.documentElement.style.setProperty('color-scheme', colorScheme);
@ -127,12 +141,14 @@
document.documentElement.classList.add('useSystemFont'); document.documentElement.classList.add('useSystemFont');
} }
if (!isSafeMode) {
const customCss = localStorage.getItem('customCss'); const customCss = localStorage.getItem('customCss');
if (customCss && customCss.length > 0) { if (customCss && customCss.length > 0) {
const style = document.createElement('style'); const style = document.createElement('style');
style.innerHTML = customCss; style.innerHTML = customCss;
document.head.appendChild(style); document.head.appendChild(style);
} }
}
async function addStyle(styleText) { async function addStyle(styleText) {
let css = document.createElement('style'); let css = document.createElement('style');
@ -159,9 +175,13 @@
otherOption1: 'Clear preferences and cache', otherOption1: 'Clear preferences and cache',
otherOption2: 'Start the simple client', otherOption2: 'Start the simple client',
otherOption3: 'Start the repair tool', otherOption3: 'Start the repair tool',
otherOption4: 'Start Misskey in safe mode',
}, locale?._bootErrors || {}); }, locale?._bootErrors || {});
const reload = locale?.reload || 'Reload'; const reload = locale?.reload || 'Reload';
const safeModeUrl = new URL(window.location.href);
safeModeUrl.searchParams.set('safemode', 'true');
let errorsElement = document.getElementById('errors'); let errorsElement = document.getElementById('errors');
if (!errorsElement) { if (!errorsElement) {
@ -182,6 +202,12 @@
<p>${messages.solution4}</p> <p>${messages.solution4}</p>
<details style="color: #86b300;"> <details style="color: #86b300;">
<summary>${messages.otherOption}</summary> <summary>${messages.otherOption}</summary>
<a href="${safeModeUrl}">
<button class="button-small">
<span class="button-label-small">${messages.otherOption4}</span>
</button>
</a>
<br>
<a href="/flush"> <a href="/flush">
<button class="button-small"> <button class="button-small">
<span class="button-label-small">${messages.otherOption1}</span> <span class="button-label-small">${messages.otherOption1}</span>

View File

@ -40,5 +40,11 @@
} }
] ]
} }
},
"shortcuts": [
{
"name": "Safemode",
"url": "/?safemode=true"
} }
]
} }

View File

@ -23,6 +23,7 @@ export const version = _VERSION_;
export const instanceName = (siteName === 'Misskey' || siteName == null) ? host : siteName; export const instanceName = (siteName === 'Misskey' || siteName == null) ? host : siteName;
export const ui = localStorage.getItem('ui'); export const ui = localStorage.getItem('ui');
export const debug = localStorage.getItem('debug') === 'true'; export const debug = localStorage.getItem('debug') === 'true';
export const isSafeMode = localStorage.getItem('isSafeMode') === 'true';
export function updateLocale(newLocale: Locale): void { export function updateLocale(newLocale: Locale): void {
locale = newLocale; locale = newLocale;

View File

@ -5,7 +5,7 @@
import { computed, watch, version as vueVersion } from 'vue'; import { computed, watch, version as vueVersion } from 'vue';
import { compareVersions } from 'compare-versions'; import { compareVersions } from 'compare-versions';
import { version, lang, updateLocale, locale, apiUrl } from '@@/js/config.js'; import { version, lang, updateLocale, locale, apiUrl, isSafeMode } from '@@/js/config.js';
import defaultLightTheme from '@@/themes/l-light.json5'; import defaultLightTheme from '@@/themes/l-light.json5';
import defaultDarkTheme from '@@/themes/d-green-lime.json5'; import defaultDarkTheme from '@@/themes/d-green-lime.json5';
import type { App } from 'vue'; import type { App } from 'vue';
@ -168,14 +168,20 @@ export async function common(createVue: () => Promise<App<Element>>) {
// NOTE: この処理は必ずクライアント更新チェック処理より後に来ること(テーマ再構築のため) // NOTE: この処理は必ずクライアント更新チェック処理より後に来ること(テーマ再構築のため)
watch(store.r.darkMode, (darkMode) => { watch(store.r.darkMode, (darkMode) => {
applyTheme(darkMode const theme = (() => {
? (prefer.s.darkTheme ?? defaultDarkTheme) if (darkMode) {
: (prefer.s.lightTheme ?? defaultLightTheme), return isSafeMode ? defaultDarkTheme : (prefer.s.darkTheme ?? defaultDarkTheme);
); } else {
}, { immediate: miLocalStorage.getItem('theme') == null }); return isSafeMode ? defaultLightTheme : (prefer.s.lightTheme ?? defaultLightTheme);
}
})();
applyTheme(theme);
}, { immediate: isSafeMode || miLocalStorage.getItem('theme') == null });
window.document.documentElement.dataset.colorScheme = store.s.darkMode ? 'dark' : 'light'; window.document.documentElement.dataset.colorScheme = store.s.darkMode ? 'dark' : 'light';
if (!isSafeMode) {
const darkTheme = prefer.model('darkTheme'); const darkTheme = prefer.model('darkTheme');
const lightTheme = prefer.model('lightTheme'); const lightTheme = prefer.model('lightTheme');
@ -190,6 +196,7 @@ export async function common(createVue: () => Promise<App<Element>>) {
applyTheme(theme ?? defaultLightTheme); applyTheme(theme ?? defaultLightTheme);
} }
}); });
}
//#region Sync dark mode //#region Sync dark mode
if (prefer.s.syncDeviceDarkMode) { if (prefer.s.syncDeviceDarkMode) {
@ -203,6 +210,7 @@ export async function common(createVue: () => Promise<App<Element>>) {
}); });
//#endregion //#endregion
if (!isSafeMode) {
if (prefer.s.darkTheme && store.s.darkMode) { if (prefer.s.darkTheme && store.s.darkMode) {
if (miLocalStorage.getItem('themeId') !== prefer.s.darkTheme.id) applyTheme(prefer.s.darkTheme); if (miLocalStorage.getItem('themeId') !== prefer.s.darkTheme.id) applyTheme(prefer.s.darkTheme);
} else if (prefer.s.lightTheme && !store.s.darkMode) { } else if (prefer.s.lightTheme && !store.s.darkMode) {
@ -214,6 +222,7 @@ export async function common(createVue: () => Promise<App<Element>>) {
if (prefer.s.lightTheme == null && instance.defaultLightTheme != null) prefer.commit('lightTheme', JSON.parse(instance.defaultLightTheme)); if (prefer.s.lightTheme == null && instance.defaultLightTheme != null) prefer.commit('lightTheme', JSON.parse(instance.defaultLightTheme));
if (prefer.s.darkTheme == null && instance.defaultDarkTheme != null) prefer.commit('darkTheme', JSON.parse(instance.defaultDarkTheme)); if (prefer.s.darkTheme == null && instance.defaultDarkTheme != null) prefer.commit('darkTheme', JSON.parse(instance.defaultDarkTheme));
}); });
}
watch(prefer.r.overridedDeviceKind, (kind) => { watch(prefer.r.overridedDeviceKind, (kind) => {
updateDeviceKind(kind); updateDeviceKind(kind);

View File

@ -28,8 +28,8 @@ import { addCustomEmoji, removeCustomEmojis, updateCustomEmojis } from '@/custom
import { prefer } from '@/preferences.js'; import { prefer } from '@/preferences.js';
import { launchPlugins } from '@/plugin.js'; import { launchPlugins } from '@/plugin.js';
import { updateCurrentAccountPartial } from '@/accounts.js'; import { updateCurrentAccountPartial } from '@/accounts.js';
import { signout } from '@/signout.js';
import { migrateOldSettings } from '@/pref-migrate.js'; import { migrateOldSettings } from '@/pref-migrate.js';
import { unisonReload } from '@/utility/unison-reload.js';
export async function mainBoot() { export async function mainBoot() {
const { isClientUpdated, lastVersion } = await common(async () => { const { isClientUpdated, lastVersion } = await common(async () => {
@ -391,6 +391,8 @@ export async function mainBoot() {
} }
// shortcut // shortcut
let safemodeRequestCount = 0;
let safemodeRequestTimer: number | null = null;
const keymap = { const keymap = {
'p|n': () => { 'p|n': () => {
if ($i == null) return; if ($i == null) return;
@ -402,6 +404,24 @@ export async function mainBoot() {
's': () => { 's': () => {
mainRouter.push('/search'); mainRouter.push('/search');
}, },
'g': {
callback: () => {
// mを5回押すとセーフモードに入る
safemodeRequestCount++;
if (safemodeRequestCount >= 5) {
miLocalStorage.setItem('isSafeMode', 'true');
unisonReload();
} else {
if (safemodeRequestTimer != null) {
window.clearTimeout(safemodeRequestTimer);
}
safemodeRequestTimer = window.setTimeout(() => {
safemodeRequestCount = 0;
}, 300);
}
},
allowRepeat: true,
}
} as const satisfies Keymap; } as const satisfies Keymap;
window.document.addEventListener('keydown', makeHotkey(keymap), { passive: false }); window.document.addEventListener('keydown', makeHotkey(keymap), { passive: false });

View File

@ -145,7 +145,7 @@ import { claimAchievement } from '@/utility/achievements.js';
import { prefer } from '@/preferences.js'; import { prefer } from '@/preferences.js';
import { chooseFileFromPcAndUpload, selectDriveFolder } from '@/utility/drive.js'; import { chooseFileFromPcAndUpload, selectDriveFolder } from '@/utility/drive.js';
import { store } from '@/store.js'; import { store } from '@/store.js';
import { isSeparatorNeeded, getSeparatorInfo, makeDateGroupedTimelineComputedRef } from '@/utility/timeline-date-separate.js'; import { makeDateGroupedTimelineComputedRef } from '@/utility/timeline-date-separate.js';
import { globalEvents, useGlobalEvent } from '@/events.js'; import { globalEvents, useGlobalEvent } from '@/events.js';
import { checkDragDataType, getDragData, setDragData } from '@/drag-and-drop.js'; import { checkDragDataType, getDragData, setDragData } from '@/drag-and-drop.js';
import { getDriveFileMenu } from '@/utility/get-drive-file-menu.js'; import { getDriveFileMenu } from '@/utility/get-drive-file-menu.js';

View File

@ -15,7 +15,7 @@ SPDX-License-Identifier: AGPL-3.0-only
@contextmenu.stop @contextmenu.stop
@keydown.stop @keydown.stop
> >
<button v-if="hide" :class="$style.hidden" @click="show"> <button v-if="hide" :class="$style.hidden" @click="reveal">
<div :class="$style.hiddenTextWrapper"> <div :class="$style.hiddenTextWrapper">
<b v-if="audio.isSensitive" style="display: block;"><i class="ti ti-eye-exclamation"></i> {{ i18n.ts.sensitive }}{{ prefer.s.dataSaver.media ? ` (${i18n.ts.audio}${audio.size ? ' ' + bytes(audio.size) : ''})` : '' }}</b> <b v-if="audio.isSensitive" style="display: block;"><i class="ti ti-eye-exclamation"></i> {{ i18n.ts.sensitive }}{{ prefer.s.dataSaver.media ? ` (${i18n.ts.audio}${audio.size ? ' ' + bytes(audio.size) : ''})` : '' }}</b>
<b v-else style="display: block;"><i class="ti ti-music"></i> {{ prefer.s.dataSaver.media && audio.size ? bytes(audio.size) : i18n.ts.audio }}</b> <b v-else style="display: block;"><i class="ti ti-music"></i> {{ prefer.s.dataSaver.media && audio.size ? bytes(audio.size) : i18n.ts.audio }}</b>
@ -157,7 +157,7 @@ const audioEl = useTemplateRef('audioEl');
// eslint-disable-next-line vue/no-setup-props-reactivity-loss // eslint-disable-next-line vue/no-setup-props-reactivity-loss
const hide = ref((prefer.s.nsfw === 'force' || prefer.s.dataSaver.media) ? true : (props.audio.isSensitive && prefer.s.nsfw !== 'ignore')); const hide = ref((prefer.s.nsfw === 'force' || prefer.s.dataSaver.media) ? true : (props.audio.isSensitive && prefer.s.nsfw !== 'ignore'));
async function show() { async function reveal() {
if (props.audio.isSensitive && prefer.s.confirmWhenRevealingSensitiveMedia) { if (props.audio.isSensitive && prefer.s.confirmWhenRevealingSensitiveMedia) {
const { canceled } = await os.confirm({ const { canceled } = await os.confirm({
type: 'question', type: 'question',

View File

@ -6,7 +6,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<template> <template>
<div :class="$style.root"> <div :class="$style.root">
<MkMediaAudio v-if="media.type.startsWith('audio') && media.type !== 'audio/midi'" :audio="media"/> <MkMediaAudio v-if="media.type.startsWith('audio') && media.type !== 'audio/midi'" :audio="media"/>
<div v-else-if="media.isSensitive && hide" :class="$style.sensitive" @click="show"> <div v-else-if="media.isSensitive && hide" :class="$style.sensitive" @click="reveal">
<span style="font-size: 1.6em;"><i class="ti ti-alert-triangle"></i></span> <span style="font-size: 1.6em;"><i class="ti ti-alert-triangle"></i></span>
<b>{{ i18n.ts.sensitive }}</b> <b>{{ i18n.ts.sensitive }}</b>
<span>{{ i18n.ts.clickToShow }}</span> <span>{{ i18n.ts.clickToShow }}</span>
@ -37,7 +37,7 @@ const props = defineProps<{
const hide = ref(true); const hide = ref(true);
async function show() { async function reveal() {
if (props.media.isSensitive && prefer.s.confirmWhenRevealingSensitiveMedia) { if (props.media.isSensitive && prefer.s.confirmWhenRevealingSensitiveMedia) {
const { canceled } = await os.confirm({ const { canceled } = await os.confirm({
type: 'question', type: 'question',

View File

@ -4,7 +4,7 @@ SPDX-License-Identifier: AGPL-3.0-only
--> -->
<template> <template>
<div :class="[hide ? $style.hidden : $style.visible, (image.isSensitive && prefer.s.highlightSensitiveMedia) && $style.sensitive]" @click="onclick"> <div :class="[hide ? $style.hidden : $style.visible, (image.isSensitive && prefer.s.highlightSensitiveMedia) && $style.sensitive]" @click="reveal">
<component <component
:is="disableImageLink ? 'div' : 'a'" :is="disableImageLink ? 'div' : 'a'"
v-bind="disableImageLink ? { v-bind="disableImageLink ? {
@ -96,10 +96,10 @@ const url = computed(() => (props.raw || prefer.s.loadRawImages)
? props.image.url ? props.image.url
: prefer.s.disableShowingAnimatedImages : prefer.s.disableShowingAnimatedImages
? getStaticImageUrl(props.image.url) ? getStaticImageUrl(props.image.url)
: props.image.thumbnailUrl, : props.image.thumbnailUrl!,
); );
async function onclick(ev: MouseEvent) { async function reveal(ev: MouseEvent) {
if (!props.controls) { if (!props.controls) {
return; return;
} }

View File

@ -19,7 +19,7 @@ SPDX-License-Identifier: AGPL-3.0-only
@contextmenu.stop @contextmenu.stop
@keydown.stop @keydown.stop
> >
<button v-if="hide" :class="$style.hidden" @click="show"> <button v-if="hide" :class="$style.hidden" @click="reveal">
<div :class="$style.hiddenTextWrapper"> <div :class="$style.hiddenTextWrapper">
<b v-if="video.isSensitive" style="display: block;"><i class="ti ti-eye-exclamation"></i> {{ i18n.ts.sensitive }}{{ prefer.s.dataSaver.media ? ` (${i18n.ts.video}${video.size ? ' ' + bytes(video.size) : ''})` : '' }}</b> <b v-if="video.isSensitive" style="display: block;"><i class="ti ti-eye-exclamation"></i> {{ i18n.ts.sensitive }}{{ prefer.s.dataSaver.media ? ` (${i18n.ts.video}${video.size ? ' ' + bytes(video.size) : ''})` : '' }}</b>
<b v-else style="display: block;"><i class="ti ti-movie"></i> {{ prefer.s.dataSaver.media && video.size ? bytes(video.size) : i18n.ts.video }}</b> <b v-else style="display: block;"><i class="ti ti-movie"></i> {{ prefer.s.dataSaver.media && video.size ? bytes(video.size) : i18n.ts.video }}</b>
@ -178,7 +178,7 @@ function hasFocus() {
// eslint-disable-next-line vue/no-setup-props-reactivity-loss // eslint-disable-next-line vue/no-setup-props-reactivity-loss
const hide = ref((prefer.s.nsfw === 'force' || prefer.s.dataSaver.media) ? true : (props.video.isSensitive && prefer.s.nsfw !== 'ignore')); const hide = ref((prefer.s.nsfw === 'force' || prefer.s.dataSaver.media) ? true : (props.video.isSensitive && prefer.s.nsfw !== 'ignore'));
async function show() { async function reveal() {
if (props.video.isSensitive && prefer.s.confirmWhenRevealingSensitiveMedia) { if (props.video.isSensitive && prefer.s.confirmWhenRevealingSensitiveMedia) {
const { canceled } = await os.confirm({ const { canceled } = await os.confirm({
type: 'question', type: 'question',

View File

@ -10,15 +10,22 @@ SPDX-License-Identifier: AGPL-3.0-only
<template #default="{ items: notes }"> <template #default="{ items: notes }">
<div :class="[$style.root, { [$style.noGap]: noGap, '_gaps': !noGap }]"> <div :class="[$style.root, { [$style.noGap]: noGap, '_gaps': !noGap }]">
<template v-for="(note, i) in notes" :key="note.id"> <template v-for="(note, i) in notes" :key="note.id">
<div v-if="i > 0 && isSeparatorNeeded(paginator.items.value[i -1].createdAt, note.createdAt)" :data-scroll-anchor="note.id"> <div
<div :class="$style.date"> v-if="i > 0 && isSeparatorNeeded(paginator.items.value[i - 1].createdAt, note.createdAt)"
<span><i class="ti ti-chevron-up"></i> {{ getSeparatorInfo(paginator.items.value[i -1].createdAt, note.createdAt).prevText }}</span> :data-scroll-anchor="note.id"
:class="{ '_gaps': !noGap }"
>
<div :class="[$style.date, { [$style.noGap]: noGap }]">
<span><i class="ti ti-chevron-up"></i> {{ getSeparatorInfo(paginator.items.value[i - 1].createdAt, note.createdAt)?.prevText }}</span>
<span style="height: 1em; width: 1px; background: var(--MI_THEME-divider);"></span> <span style="height: 1em; width: 1px; background: var(--MI_THEME-divider);"></span>
<span>{{ getSeparatorInfo(paginator.items.value[i -1].createdAt, note.createdAt).nextText }} <i class="ti ti-chevron-down"></i></span> <span>{{ getSeparatorInfo(paginator.items.value[i - 1].createdAt, note.createdAt)?.nextText }} <i class="ti ti-chevron-down"></i></span>
</div> </div>
<MkNote :class="$style.note" :note="note" :withHardMute="true"/> <MkNote :class="$style.note" :note="note" :withHardMute="true"/>
<div v-if="note._shouldInsertAd_" :class="$style.ad">
<MkAd :preferForms="['horizontal', 'horizontal-big']"/>
</div> </div>
<div v-else-if="note._shouldInsertAd_" :class="[$style.noteWithAd, { '_gaps': !noGap }]" :data-scroll-anchor="note.id"> </div>
<div v-else-if="note._shouldInsertAd_" :class="{ '_gaps': !noGap }" :data-scroll-anchor="note.id">
<MkNote :class="$style.note" :note="note" :withHardMute="true"/> <MkNote :class="$style.note" :note="note" :withHardMute="true"/>
<div :class="$style.ad"> <div :class="$style.ad">
<MkAd :preferForms="['horizontal', 'horizontal-big']"/> <MkAd :preferForms="['horizontal', 'horizontal-big']"/>
@ -103,8 +110,11 @@ defineExpose({
opacity: 0.75; opacity: 0.75;
padding: 8px 8px; padding: 8px 8px;
margin: 0 auto; margin: 0 auto;
&.noGap {
border-bottom: solid 0.5px var(--MI_THEME-divider); border-bottom: solid 0.5px var(--MI_THEME-divider);
} }
}
.ad:empty { .ad:empty {
display: none; display: none;

View File

@ -55,7 +55,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<template #icon><i class="ti ti-planet"></i></template> <template #icon><i class="ti ti-planet"></i></template>
<div class="_gaps_s"> <div class="_gaps_s">
<div>{{ i18n.ts._serverSetupWizard.doYouConnectToFediverse_description1 }}<br>{{ i18n.ts._serverSetupWizard.doYouConnectToFediverse_description2 }}</div> <div>{{ i18n.ts._serverSetupWizard.doYouConnectToFediverse_description1 }}<br>{{ i18n.ts._serverSetupWizard.doYouConnectToFediverse_description2 }}<br><MkLink target="_blank" url="https://wikipedia.org/wiki/Fediverse">{{ i18n.ts.learnMore }}</MkLink></div>
<MkRadios v-model="q_federation" :vertical="true"> <MkRadios v-model="q_federation" :vertical="true">
<option value="yes">{{ i18n.ts.yes }}</option> <option value="yes">{{ i18n.ts.yes }}</option>
@ -63,6 +63,11 @@ SPDX-License-Identifier: AGPL-3.0-only
</MkRadios> </MkRadios>
<MkInfo v-if="q_federation === 'yes'">{{ i18n.ts._serverSetupWizard.youCanConfigureMoreFederationSettingsLater }}</MkInfo> <MkInfo v-if="q_federation === 'yes'">{{ i18n.ts._serverSetupWizard.youCanConfigureMoreFederationSettingsLater }}</MkInfo>
<MkSwitch v-if="q_federation === 'yes'" v-model="q_remoteContentsCleaning">
<template #label>{{ i18n.ts._serverSetupWizard.remoteContentsCleaning }}</template>
<template #caption>{{ i18n.ts._serverSetupWizard.remoteContentsCleaning_description }}</template>
</MkSwitch>
</div> </div>
</MkFolder> </MkFolder>
@ -110,6 +115,10 @@ SPDX-License-Identifier: AGPL-3.0-only
<div><b>{{ i18n.ts.federation }}:</b></div> <div><b>{{ i18n.ts.federation }}:</b></div>
<div>{{ serverSettings.federation === 'none' ? i18n.ts.no : i18n.ts.all }}</div> <div>{{ serverSettings.federation === 'none' ? i18n.ts.no : i18n.ts.all }}</div>
</div> </div>
<div>
<div><b>{{ i18n.ts._serverSettings.remoteNotesCleaning }}:</b></div>
<div>{{ serverSettings.enableRemoteNotesCleaning ? i18n.ts.yes : i18n.ts.no }}</div>
</div>
<div> <div>
<div><b>FTT:</b></div> <div><b>FTT:</b></div>
<div>{{ serverSettings.enableFanoutTimeline ? i18n.ts.yes : i18n.ts.no }}</div> <div>{{ serverSettings.enableFanoutTimeline ? i18n.ts.yes : i18n.ts.no }}</div>
@ -185,7 +194,9 @@ import { misskeyApi } from '@/utility/misskey-api.js';
import { i18n } from '@/i18n.js'; import { i18n } from '@/i18n.js';
import MkFolder from '@/components/MkFolder.vue'; import MkFolder from '@/components/MkFolder.vue';
import MkRadios from '@/components/MkRadios.vue'; import MkRadios from '@/components/MkRadios.vue';
import MkSwitch from '@/components/MkSwitch.vue';
import MkInfo from '@/components/MkInfo.vue'; import MkInfo from '@/components/MkInfo.vue';
import MkLink from '@/components/MkLink.vue';
const emit = defineEmits<{ const emit = defineEmits<{
(ev: 'finished'): void; (ev: 'finished'): void;
@ -200,6 +211,7 @@ const q_name = ref('');
const q_use = ref('single'); const q_use = ref('single');
const q_scale = ref('small'); const q_scale = ref('small');
const q_federation = ref('yes'); const q_federation = ref('yes');
const q_remoteContentsCleaning = ref(true);
const q_adminName = ref(''); const q_adminName = ref('');
const q_adminEmail = ref(''); const q_adminEmail = ref('');
@ -217,6 +229,7 @@ const serverSettings = computed<Misskey.entities.AdminUpdateMetaRequest>(() => {
emailRequiredForSignup: q_use.value === 'open', emailRequiredForSignup: q_use.value === 'open',
enableIpLogging: q_use.value === 'open', enableIpLogging: q_use.value === 'open',
federation: q_federation.value === 'yes' ? 'all' : 'none', federation: q_federation.value === 'yes' ? 'all' : 'none',
enableRemoteNotesCleaning: q_remoteContentsCleaning.value,
enableFanoutTimeline: true, enableFanoutTimeline: true,
enableFanoutTimelineDbFallback: q_use.value === 'single', enableFanoutTimelineDbFallback: q_use.value === 'single',
enableReactionsBuffering, enableReactionsBuffering,

View File

@ -0,0 +1,57 @@
<!--
SPDX-FileCopyrightText: syuilo and other misskey contributors
SPDX-License-Identifier: AGPL-3.0-only
-->
<template>
<MkModalWindow
ref="windowEl"
:withOkButton="false"
:okButtonDisabled="false"
:width="500"
:height="600"
@close="onCloseModalWindow"
@closed="emit('closed')"
>
<template #header>Server setup wizard</template>
<div class="_spacer" style="--MI_SPACER-min: 20px; --MI_SPACER-max: 28px;">
<Suspense>
<template #default>
<MkServerSetupWizard @finished="onWizardFinished"/>
</template>
<template #fallback>
<MkLoading/>
</template>
</Suspense>
</div>
</MkModalWindow>
</template>
<script setup lang="ts">
import { useTemplateRef } from 'vue';
import MkModalWindow from '@/components/MkModalWindow.vue';
import MkServerSetupWizard from '@/components/MkServerSetupWizard.vue';
const emit = defineEmits<{
(ev: 'closed'),
}>();
const windowEl = useTemplateRef('windowEl');
function onWizardFinished() {
windowEl.value?.close();
}
function onCloseModalWindow() {
windowEl.value?.close();
}
</script>
<style module lang="scss">
.root {
max-height: 410px;
height: 410px;
display: flex;
flex-direction: column;
}
</style>

View File

@ -32,9 +32,9 @@ SPDX-License-Identifier: AGPL-3.0-only
<template v-for="(note, i) in paginator.items.value" :key="note.id"> <template v-for="(note, i) in paginator.items.value" :key="note.id">
<div v-if="i > 0 && isSeparatorNeeded(paginator.items.value[i -1].createdAt, note.createdAt)" :data-scroll-anchor="note.id"> <div v-if="i > 0 && isSeparatorNeeded(paginator.items.value[i -1].createdAt, note.createdAt)" :data-scroll-anchor="note.id">
<div :class="$style.date"> <div :class="$style.date">
<span><i class="ti ti-chevron-up"></i> {{ getSeparatorInfo(paginator.items.value[i -1].createdAt, note.createdAt).prevText }}</span> <span><i class="ti ti-chevron-up"></i> {{ getSeparatorInfo(paginator.items.value[i -1].createdAt, note.createdAt)?.prevText }}</span>
<span style="height: 1em; width: 1px; background: var(--MI_THEME-divider);"></span> <span style="height: 1em; width: 1px; background: var(--MI_THEME-divider);"></span>
<span>{{ getSeparatorInfo(paginator.items.value[i -1].createdAt, note.createdAt).nextText }} <i class="ti ti-chevron-down"></i></span> <span>{{ getSeparatorInfo(paginator.items.value[i -1].createdAt, note.createdAt)?.nextText }} <i class="ti ti-chevron-down"></i></span>
</div> </div>
<MkNote :class="$style.note" :note="note" :withHardMute="true"/> <MkNote :class="$style.note" :note="note" :withHardMute="true"/>
</div> </div>

View File

@ -25,11 +25,11 @@ SPDX-License-Identifier: AGPL-3.0-only
> >
<div v-for="(notification, i) in paginator.items.value" :key="notification.id" :data-scroll-anchor="notification.id" :class="$style.item"> <div v-for="(notification, i) in paginator.items.value" :key="notification.id" :data-scroll-anchor="notification.id" :class="$style.item">
<div v-if="i > 0 && isSeparatorNeeded(paginator.items.value[i -1].createdAt, notification.createdAt)" :class="$style.date"> <div v-if="i > 0 && isSeparatorNeeded(paginator.items.value[i -1].createdAt, notification.createdAt)" :class="$style.date">
<span><i class="ti ti-chevron-up"></i> {{ getSeparatorInfo(paginator.items.value[i -1].createdAt, notification.createdAt).prevText }}</span> <span><i class="ti ti-chevron-up"></i> {{ getSeparatorInfo(paginator.items.value[i -1].createdAt, notification.createdAt)?.prevText }}</span>
<span style="height: 1em; width: 1px; background: var(--MI_THEME-divider);"></span> <span style="height: 1em; width: 1px; background: var(--MI_THEME-divider);"></span>
<span>{{ getSeparatorInfo(paginator.items.value[i -1].createdAt, notification.createdAt).nextText }} <i class="ti ti-chevron-down"></i></span> <span>{{ getSeparatorInfo(paginator.items.value[i -1].createdAt, notification.createdAt)?.nextText }} <i class="ti ti-chevron-down"></i></span>
</div> </div>
<MkNote v-if="['reply', 'quote', 'mention'].includes(notification.type)" :class="$style.content" :note="notification.note" :withHardMute="true"/> <MkNote v-if="['reply', 'quote', 'mention'].includes(notification.type) && 'note' in notification" :class="$style.content" :note="notification.note" :withHardMute="true"/>
<XNotification v-else :class="$style.content" :notification="notification" :withTime="true" :full="true"/> <XNotification v-else :class="$style.content" :notification="notification" :withTime="true" :full="true"/>
</div> </div>
</component> </component>

View File

@ -33,6 +33,7 @@ export type Keys = (
'preferences' | 'preferences' |
'latestPreferencesUpdate' | 'latestPreferencesUpdate' |
'hidePreferencesRestoreSuggestion' | 'hidePreferencesRestoreSuggestion' |
'isSafeMode' |
`miux:${string}` | `miux:${string}` |
`ui:folder:${string}` | `ui:folder:${string}` |
`themes:${string}` | // DEPRECATED `themes:${string}` | // DEPRECATED

View File

@ -98,7 +98,7 @@ SPDX-License-Identifier: AGPL-3.0-only
</MkKeyValue> </MkKeyValue>
<MkKeyValue v-if="job.progress != null && typeof job.progress === 'number' && job.progress > 0"> <MkKeyValue v-if="job.progress != null && typeof job.progress === 'number' && job.progress > 0">
<template #key>Progress</template> <template #key>Progress</template>
<template #value>{{ Math.floor(job.progress * 100) }}%</template> <template #value>{{ Math.floor(job.progress) }}%</template>
</MkKeyValue> </MkKeyValue>
</div> </div>
<MkFolder :withSpacer="false"> <MkFolder :withSpacer="false">
@ -150,11 +150,15 @@ SPDX-License-Identifier: AGPL-3.0-only
<MkButton><i class="ti ti-device-floppy"></i> Update</MkButton> <MkButton><i class="ti ti-device-floppy"></i> Update</MkButton>
</div> </div>
<div v-else-if="tab === 'result'"> <div v-else-if="tab === 'result'">
<MkCode :code="String(job.returnValue)"/> <MkCode :code="JSON5.stringify(job.returnValue, null, '\t')" lang="json5"/>
</div> </div>
<div v-else-if="tab === 'error'" class="_gaps_s"> <div v-else-if="tab === 'error'" class="_gaps_s">
<MkCode v-for="log in job.stacktrace" :code="log" lang="stacktrace"/> <MkCode v-for="log in job.stacktrace" :code="log" lang="stacktrace"/>
</div> </div>
<div v-else-if="tab === 'logs'">
<MkButton primary rounded @click="loadLogs()"><i class="ti ti-refresh"></i> Load logs</MkButton>
<div v-for="log in logs">{{ log }}</div>
</div>
</MkFolder> </MkFolder>
</template> </template>
@ -198,6 +202,7 @@ const emit = defineEmits<{
const tab = ref('info'); const tab = ref('info');
const editData = ref(JSON5.stringify(props.job.data, null, '\t')); const editData = ref(JSON5.stringify(props.job.data, null, '\t'));
const canEdit = true; const canEdit = true;
const logs = ref<string[]>([]);
type TlType = TlEvent<{ type TlType = TlEvent<{
type: 'created' | 'processed' | 'finished'; type: 'created' | 'processed' | 'finished';
@ -268,6 +273,10 @@ async function removeJob() {
os.apiWithDialog('admin/queue/remove-job', { queue: props.queueType, jobId: props.job.id }); os.apiWithDialog('admin/queue/remove-job', { queue: props.queueType, jobId: props.job.id });
} }
async function loadLogs() {
logs.value = await os.apiWithDialog('admin/queue/show-job-logs', { queue: props.queueType, jobId: props.job.id });
}
// TODO // TODO
// function moveJob() { // function moveJob() {
// //

View File

@ -101,6 +101,35 @@ SPDX-License-Identifier: AGPL-3.0-only
</MkSwitch> </MkSwitch>
</div> </div>
</MkFolder> </MkFolder>
<MkFolder :defaultOpen="true">
<template #icon><i class="ti ti-recycle"></i></template>
<template #label>Remote Notes Cleaning ()</template>
<template v-if="remoteNotesCleaningForm.savedState.enableRemoteNotesCleaning" #suffix>Enabled</template>
<template v-else #suffix>Disabled</template>
<template v-if="remoteNotesCleaningForm.modified.value" #footer>
<MkFormFooter :form="remoteNotesCleaningForm"/>
</template>
<div class="_gaps_m">
<MkSwitch v-model="remoteNotesCleaningForm.state.enableRemoteNotesCleaning">
<template #label>{{ i18n.ts.enable }}<span v-if="remoteNotesCleaningForm.modifiedStates.enableRemoteNotesCleaning" class="_modified">{{ i18n.ts.modified }}</span></template>
<template #caption>{{ i18n.ts._serverSettings.remoteNotesCleaning_description }}</template>
</MkSwitch>
<template v-if="remoteNotesCleaningForm.state.enableRemoteNotesCleaning">
<MkInput v-model="remoteNotesCleaningForm.state.remoteNotesCleaningExpiryDaysForEachNotes" type="number">
<template #label>{{ i18n.ts._serverSettings.remoteNotesCleaningExpiryDaysForEachNotes }} ({{ i18n.ts.inDays }})<span v-if="remoteNotesCleaningForm.modifiedStates.remoteNotesCleaningExpiryDaysForEachNotes" class="_modified">{{ i18n.ts.modified }}</span></template>
<template #suffix>{{ i18n.ts._time.day }}</template>
</MkInput>
<MkInput v-model="remoteNotesCleaningForm.state.remoteNotesCleaningMaxProcessingDurationInMinutes" type="number">
<template #label>{{ i18n.ts._serverSettings.remoteNotesCleaningMaxProcessingDuration }} ({{ i18n.ts.inMinutes }})<span v-if="remoteNotesCleaningForm.modifiedStates.remoteNotesCleaningMaxProcessingDurationInMinutes" class="_modified">{{ i18n.ts.modified }}</span></template>
<template #suffix>{{ i18n.ts._time.minute }}</template>
</MkInput>
</template>
</div>
</MkFolder>
</div> </div>
</div> </div>
</PageWithHeader> </PageWithHeader>
@ -196,6 +225,19 @@ const rbtForm = useForm({
fetchInstance(true); fetchInstance(true);
}); });
const remoteNotesCleaningForm = useForm({
enableRemoteNotesCleaning: meta.enableRemoteNotesCleaning,
remoteNotesCleaningExpiryDaysForEachNotes: meta.remoteNotesCleaningExpiryDaysForEachNotes,
remoteNotesCleaningMaxProcessingDurationInMinutes: meta.remoteNotesCleaningMaxProcessingDurationInMinutes,
}, async (state) => {
await os.apiWithDialog('admin/update-meta', {
enableRemoteNotesCleaning: state.enableRemoteNotesCleaning,
remoteNotesCleaningExpiryDaysForEachNotes: state.remoteNotesCleaningExpiryDaysForEachNotes,
remoteNotesCleaningMaxProcessingDurationInMinutes: state.remoteNotesCleaningMaxProcessingDurationInMinutes,
});
fetchInstance(true);
});
const headerActions = computed(() => []); const headerActions = computed(() => []);
const headerTabs = computed(() => []); const headerTabs = computed(() => []);

View File

@ -287,6 +287,10 @@ SPDX-License-Identifier: AGPL-3.0-only
</MkTextarea> </MkTextarea>
</div> </div>
</MkFolder> </MkFolder>
<MkButton primary @click="openSetupWizard">
Open setup wizard
</MkButton>
</div> </div>
</div> </div>
</PageWithHeader> </PageWithHeader>
@ -425,6 +429,20 @@ const proxyAccountForm = useForm({
fetchInstance(true); fetchInstance(true);
}); });
async function openSetupWizard() {
const { canceled } = await os.confirm({
type: 'warning',
title: i18n.ts._serverSettings.restartServerSetupWizardConfirm_title,
text: i18n.ts._serverSettings.restartServerSetupWizardConfirm_text,
});
if (canceled) return;
const { dispose } = await os.popupAsyncWithDialog(import('@/components/MkServerSetupWizardDialog.vue').then(x => x.default), {
}, {
closed: () => dispose(),
});
}
const headerTabs = computed(() => []); const headerTabs = computed(() => []);
definePage(() => ({ definePage(() => ({

View File

@ -366,6 +366,7 @@ definePage(() => ({
> .items { > .items {
display: flex; display: flex;
flex-wrap: wrap;
justify-content: center; justify-content: center;
gap: 12px; gap: 12px;
padding: 16px; padding: 16px;

View File

@ -7,6 +7,8 @@ SPDX-License-Identifier: AGPL-3.0-only
<div class="_gaps_m"> <div class="_gaps_m">
<FormInfo warn>{{ i18n.ts.customCssWarn }}</FormInfo> <FormInfo warn>{{ i18n.ts.customCssWarn }}</FormInfo>
<FormInfo v-if="isSafeMode" warn>{{ i18n.ts.customCssIsDisabledBecauseSafeMode }}</FormInfo>
<MkCodeEditor v-model="localCustomCss" manualSave lang="css"> <MkCodeEditor v-model="localCustomCss" manualSave lang="css">
<template #label>CSS</template> <template #label>CSS</template>
</MkCodeEditor> </MkCodeEditor>
@ -17,6 +19,7 @@ SPDX-License-Identifier: AGPL-3.0-only
import { ref, watch, computed } from 'vue'; import { ref, watch, computed } from 'vue';
import MkCodeEditor from '@/components/MkCodeEditor.vue'; import MkCodeEditor from '@/components/MkCodeEditor.vue';
import FormInfo from '@/components/MkInfo.vue'; import FormInfo from '@/components/MkInfo.vue';
import { isSafeMode } from '@@/js/config.js';
import * as os from '@/os.js'; import * as os from '@/os.js';
import { unisonReload } from '@/utility/unison-reload.js'; import { unisonReload } from '@/utility/unison-reload.js';
import { i18n } from '@/i18n.js'; import { i18n } from '@/i18n.js';

View File

@ -10,7 +10,9 @@ SPDX-License-Identifier: AGPL-3.0-only
<SearchKeyword>{{ i18n.ts._settings.pluginBanner }}</SearchKeyword> <SearchKeyword>{{ i18n.ts._settings.pluginBanner }}</SearchKeyword>
</MkFeatureBanner> </MkFeatureBanner>
<FormLink to="/settings/plugin/install"><template #icon><i class="ti ti-download"></i></template>{{ i18n.ts._plugin.install }}</FormLink> <MkInfo v-if="isSafeMode" warn>{{ i18n.ts.pluginsAreDisabledBecauseSafeMode }}</MkInfo>
<FormLink v-else to="/settings/plugin/install"><template #icon><i class="ti ti-download"></i></template>{{ i18n.ts._plugin.install }}</FormLink>
<FormSection> <FormSection>
<template #label>{{ i18n.ts.manage }}</template> <template #label>{{ i18n.ts.manage }}</template>
@ -103,10 +105,12 @@ import MkCode from '@/components/MkCode.vue';
import MkFolder from '@/components/MkFolder.vue'; import MkFolder from '@/components/MkFolder.vue';
import MkKeyValue from '@/components/MkKeyValue.vue'; import MkKeyValue from '@/components/MkKeyValue.vue';
import MkFeatureBanner from '@/components/MkFeatureBanner.vue'; import MkFeatureBanner from '@/components/MkFeatureBanner.vue';
import MkInfo from '@/components/MkInfo.vue';
import { i18n } from '@/i18n.js'; import { i18n } from '@/i18n.js';
import { definePage } from '@/page.js'; import { definePage } from '@/page.js';
import { changePluginActive, configPlugin, pluginLogs, uninstallPlugin, reloadPlugin } from '@/plugin.js'; import { changePluginActive, configPlugin, pluginLogs, uninstallPlugin, reloadPlugin } from '@/plugin.js';
import { prefer } from '@/preferences.js'; import { prefer } from '@/preferences.js';
import { isSafeMode } from '@@/js/config.js';
import * as os from '@/os.js'; import * as os from '@/os.js';
const plugins = prefer.r.plugins; const plugins = prefer.r.plugins;

View File

@ -35,7 +35,9 @@ SPDX-License-Identifier: AGPL-3.0-only
</div> </div>
</div> </div>
<div class="_gaps"> <MkInfo v-if="isSafeMode" warn>{{ i18n.ts.themeIsDefaultBecauseSafeMode }}</MkInfo>
<div v-else class="_gaps">
<template v-if="!store.r.darkMode.value"> <template v-if="!store.r.darkMode.value">
<SearchMarker :keywords="['light', 'theme']"> <SearchMarker :keywords="['light', 'theme']">
<MkFolder :defaultOpen="true" :max-height="500"> <MkFolder :defaultOpen="true" :max-height="500">
@ -204,12 +206,14 @@ import JSON5 from 'json5';
import defaultLightTheme from '@@/themes/l-light.json5'; import defaultLightTheme from '@@/themes/l-light.json5';
import defaultDarkTheme from '@@/themes/d-green-lime.json5'; import defaultDarkTheme from '@@/themes/d-green-lime.json5';
import type { Theme } from '@/theme.js'; import type { Theme } from '@/theme.js';
import { isSafeMode } from '@@/js/config.js';
import * as os from '@/os.js'; import * as os from '@/os.js';
import MkSwitch from '@/components/MkSwitch.vue'; import MkSwitch from '@/components/MkSwitch.vue';
import FormSection from '@/components/form/section.vue'; import FormSection from '@/components/form/section.vue';
import FormLink from '@/components/form/link.vue'; import FormLink from '@/components/form/link.vue';
import MkFolder from '@/components/MkFolder.vue'; import MkFolder from '@/components/MkFolder.vue';
import MkThemePreview from '@/components/MkThemePreview.vue'; import MkThemePreview from '@/components/MkThemePreview.vue';
import MkInfo from '@/components/MkInfo.vue';
import { getBuiltinThemesRef, getThemesRef, removeTheme } from '@/theme.js'; import { getBuiltinThemesRef, getThemesRef, removeTheme } from '@/theme.js';
import { isDeviceDarkmode } from '@/utility/is-device-darkmode.js'; import { isDeviceDarkmode } from '@/utility/is-device-darkmode.js';
import { store } from '@/store.js'; import { store } from '@/store.js';

View File

@ -87,7 +87,14 @@ SPDX-License-Identifier: AGPL-3.0-only
<div>{{ i18n.ts._serverSetupWizard.settingsYouMakeHereCanBeChangedLater }}</div> <div>{{ i18n.ts._serverSetupWizard.settingsYouMakeHereCanBeChangedLater }}</div>
</div> </div>
<Suspense>
<template #default>
<MkServerSetupWizard :token="token" @finished="onWizardFinished"/> <MkServerSetupWizard :token="token" @finished="onWizardFinished"/>
</template>
<template #fallback>
<MkLoading/>
</template>
</Suspense>
<MkButton rounded style="margin: 0 auto;" @click="skipSettings"> <MkButton rounded style="margin: 0 auto;" @click="skipSettings">
{{ i18n.ts._serverSetupWizard.skipSettings }} {{ i18n.ts._serverSetupWizard.skipSettings }}

View File

@ -6,6 +6,7 @@
import { ref, defineAsyncComponent } from 'vue'; import { ref, defineAsyncComponent } from 'vue';
import { Interpreter, Parser, utils, values } from '@syuilo/aiscript'; import { Interpreter, Parser, utils, values } from '@syuilo/aiscript';
import { compareVersions } from 'compare-versions'; import { compareVersions } from 'compare-versions';
import { isSafeMode } from '@@/js/config.js';
import { genId } from '@/utility/id.js'; import { genId } from '@/utility/id.js';
import * as Misskey from 'misskey-js'; import * as Misskey from 'misskey-js';
import { aiScriptReadline, createAiScriptEnv } from '@/aiscript/api.js'; import { aiScriptReadline, createAiScriptEnv } from '@/aiscript/api.js';
@ -232,6 +233,7 @@ export function launchPlugins() {
} }
async function launchPlugin(id: Plugin['installId']): Promise<void> { async function launchPlugin(id: Plugin['installId']): Promise<void> {
if (isSafeMode) return;
const plugin = prefer.s.plugins.find(x => x.installId === id); const plugin = prefer.s.plugins.find(x => x.installId === id);
if (!plugin) return; if (!plugin) return;

View File

@ -64,12 +64,6 @@ html {
} }
} }
html._themeChangingFallback_ {
&, * {
transition: background 0.5s ease, border 0.5s ease !important;
}
}
html._themeChanging_ { html._themeChanging_ {
view-transition-name: theme-changing; view-transition-name: theme-changing;
} }

View File

@ -137,9 +137,10 @@ export function applyTheme(theme: Theme, persist = true) {
} }
if (deepEqual(currentTheme, theme)) return; if (deepEqual(currentTheme, theme)) return;
currentTheme = theme; // リアクティビティ解除
currentTheme = deepClone(theme);
if (window.document.startViewTransition != null && prefer.s.animation) { if (window.document.startViewTransition != null) {
window.document.documentElement.classList.add('_themeChanging_'); window.document.documentElement.classList.add('_themeChanging_');
window.document.startViewTransition(async () => { window.document.startViewTransition(async () => {
applyThemeInternal(theme, persist); applyThemeInternal(theme, persist);
@ -150,15 +151,9 @@ export function applyTheme(theme: Theme, persist = true) {
globalEvents.emit('themeChanged'); globalEvents.emit('themeChanged');
}); });
} else { } else {
// TODO: ViewTransition API が主要ブラウザで対応したら消す applyThemeInternal(theme, persist);
window.document.documentElement.classList.add('_themeChangingFallback_');
timeout = window.setTimeout(() => {
window.document.documentElement.classList.remove('_themeChangingFallback_');
// 色計算など再度行えるようにクライアント全体に通知 // 色計算など再度行えるようにクライアント全体に通知
globalEvents.emit('themeChanged'); globalEvents.emit('themeChanged');
}, 500);
applyThemeInternal(theme, persist);
} }
} }

View File

@ -94,6 +94,11 @@ SPDX-License-Identifier: AGPL-3.0-only
<div v-if="dev" id="devTicker"><span style="animation: dev-ticker-blink 2s infinite;">DEV BUILD</span></div> <div v-if="dev" id="devTicker"><span style="animation: dev-ticker-blink 2s infinite;">DEV BUILD</span></div>
<div v-if="$i && $i.isBot" id="botWarn"><span style="animation: dev-ticker-blink 2s infinite;">{{ i18n.ts.loggedInAsBot }}</span></div> <div v-if="$i && $i.isBot" id="botWarn"><span style="animation: dev-ticker-blink 2s infinite;">{{ i18n.ts.loggedInAsBot }}</span></div>
<div v-if="isSafeMode" id="safemodeWarn">
<span style="animation: dev-ticker-blink 2s infinite;">{{ i18n.ts.safeModeEnabled }}</span>&nbsp;
<button class="_textButton" style="pointer-events: all;" @click="exitSafeMode">{{ i18n.ts.turnItOff }}</button>
</div>
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
@ -101,7 +106,10 @@ import { defineAsyncComponent, ref, TransitionGroup } from 'vue';
import * as Misskey from 'misskey-js'; import * as Misskey from 'misskey-js';
import { swInject } from './sw-inject.js'; import { swInject } from './sw-inject.js';
import XNotification from './notification.vue'; import XNotification from './notification.vue';
import { isSafeMode } from '@@/js/config.js';
import { popups } from '@/os.js'; import { popups } from '@/os.js';
import { unisonReload } from '@/utility/unison-reload.js';
import { miLocalStorage } from '@/local-storage.js';
import { pendingApiRequestsCount } from '@/utility/misskey-api.js'; import { pendingApiRequestsCount } from '@/utility/misskey-api.js';
import * as sound from '@/utility/sound.js'; import * as sound from '@/utility/sound.js';
import { $i } from '@/i.js'; import { $i } from '@/i.js';
@ -144,6 +152,13 @@ function onNotification(notification: Misskey.entities.Notification, isClient =
sound.playMisskeySfx('notification'); sound.playMisskeySfx('notification');
} }
function exitSafeMode() {
miLocalStorage.removeItem('isSafeMode');
const url = new URL(window.location.href);
url.searchParams.delete('safemode');
unisonReload(url.toString());
}
if ($i) { if ($i) {
if (store.s.realtimeMode) { if (store.s.realtimeMode) {
const connection = useStream().useChannel('main'); const connection = useStream().useChannel('main');
@ -396,7 +411,7 @@ if ($i) {
width: 100%; width: 100%;
height: max-content; height: max-content;
text-align: center; text-align: center;
z-index: 2147483647; z-index: 2147483646;
color: #ff0; color: #ff0;
background: rgba(0, 0, 0, 0.5); background: rgba(0, 0, 0, 0.5);
padding: 4px 7px; padding: 4px 7px;
@ -405,6 +420,11 @@ if ($i) {
user-select: none; user-select: none;
} }
#safemodeWarn {
@extend #botWarn;
z-index: 2147483647;
}
#devTicker { #devTicker {
position: fixed; position: fixed;
bottom: 0; bottom: 0;

View File

@ -296,6 +296,12 @@ type AdminQueueRemoveJobRequest = operations['admin___queue___remove-job']['requ
// @public (undocumented) // @public (undocumented)
type AdminQueueRetryJobRequest = operations['admin___queue___retry-job']['requestBody']['content']['application/json']; type AdminQueueRetryJobRequest = operations['admin___queue___retry-job']['requestBody']['content']['application/json'];
// @public (undocumented)
type AdminQueueShowJobLogsRequest = operations['admin___queue___show-job-logs']['requestBody']['content']['application/json'];
// @public (undocumented)
type AdminQueueShowJobLogsResponse = operations['admin___queue___show-job-logs']['responses']['200']['content']['application/json'];
// @public (undocumented) // @public (undocumented)
type AdminQueueShowJobRequest = operations['admin___queue___show-job']['requestBody']['content']['application/json']; type AdminQueueShowJobRequest = operations['admin___queue___show-job']['requestBody']['content']['application/json'];
@ -1559,6 +1565,8 @@ declare namespace entities {
AdminQueueRetryJobRequest, AdminQueueRetryJobRequest,
AdminQueueShowJobRequest, AdminQueueShowJobRequest,
AdminQueueShowJobResponse, AdminQueueShowJobResponse,
AdminQueueShowJobLogsRequest,
AdminQueueShowJobLogsResponse,
AdminQueueStatsResponse, AdminQueueStatsResponse,
AdminRelaysAddRequest, AdminRelaysAddRequest,
AdminRelaysAddResponse, AdminRelaysAddResponse,

View File

@ -1,7 +1,7 @@
{ {
"type": "module", "type": "module",
"name": "misskey-js", "name": "misskey-js",
"version": "2025.7.0", "version": "2025.8.0-alpha.2",
"description": "Misskey SDK for JavaScript", "description": "Misskey SDK for JavaScript",
"license": "MIT", "license": "MIT",
"main": "./built/index.js", "main": "./built/index.js",

View File

@ -713,6 +713,17 @@ declare module '../api.js' {
credential?: string | null, credential?: string | null,
): Promise<SwitchCaseResponseType<E, P>>; ): Promise<SwitchCaseResponseType<E, P>>;
/**
* No description provided.
*
* **Credential required**: *Yes* / **Permission**: *read:admin:queue*
*/
request<E extends 'admin/queue/show-job-logs', P extends Endpoints[E]['req']>(
endpoint: E,
params: P,
credential?: string | null,
): Promise<SwitchCaseResponseType<E, P>>;
/** /**
* No description provided. * No description provided.
* *

View File

@ -88,6 +88,8 @@ import type {
AdminQueueRetryJobRequest, AdminQueueRetryJobRequest,
AdminQueueShowJobRequest, AdminQueueShowJobRequest,
AdminQueueShowJobResponse, AdminQueueShowJobResponse,
AdminQueueShowJobLogsRequest,
AdminQueueShowJobLogsResponse,
AdminQueueStatsResponse, AdminQueueStatsResponse,
AdminRelaysAddRequest, AdminRelaysAddRequest,
AdminRelaysAddResponse, AdminRelaysAddResponse,
@ -717,6 +719,7 @@ export type Endpoints = {
'admin/queue/remove-job': { req: AdminQueueRemoveJobRequest; res: EmptyResponse }; 'admin/queue/remove-job': { req: AdminQueueRemoveJobRequest; res: EmptyResponse };
'admin/queue/retry-job': { req: AdminQueueRetryJobRequest; res: EmptyResponse }; 'admin/queue/retry-job': { req: AdminQueueRetryJobRequest; res: EmptyResponse };
'admin/queue/show-job': { req: AdminQueueShowJobRequest; res: AdminQueueShowJobResponse }; 'admin/queue/show-job': { req: AdminQueueShowJobRequest; res: AdminQueueShowJobResponse };
'admin/queue/show-job-logs': { req: AdminQueueShowJobLogsRequest; res: AdminQueueShowJobLogsResponse };
'admin/queue/stats': { req: EmptyRequest; res: AdminQueueStatsResponse }; 'admin/queue/stats': { req: EmptyRequest; res: AdminQueueStatsResponse };
'admin/relays/add': { req: AdminRelaysAddRequest; res: AdminRelaysAddResponse }; 'admin/relays/add': { req: AdminRelaysAddRequest; res: AdminRelaysAddResponse };
'admin/relays/list': { req: EmptyRequest; res: AdminRelaysListResponse }; 'admin/relays/list': { req: EmptyRequest; res: AdminRelaysListResponse };

View File

@ -91,6 +91,8 @@ export type AdminQueueRemoveJobRequest = operations['admin___queue___remove-job'
export type AdminQueueRetryJobRequest = operations['admin___queue___retry-job']['requestBody']['content']['application/json']; export type AdminQueueRetryJobRequest = operations['admin___queue___retry-job']['requestBody']['content']['application/json'];
export type AdminQueueShowJobRequest = operations['admin___queue___show-job']['requestBody']['content']['application/json']; export type AdminQueueShowJobRequest = operations['admin___queue___show-job']['requestBody']['content']['application/json'];
export type AdminQueueShowJobResponse = operations['admin___queue___show-job']['responses']['200']['content']['application/json']; export type AdminQueueShowJobResponse = operations['admin___queue___show-job']['responses']['200']['content']['application/json'];
export type AdminQueueShowJobLogsRequest = operations['admin___queue___show-job-logs']['requestBody']['content']['application/json'];
export type AdminQueueShowJobLogsResponse = operations['admin___queue___show-job-logs']['responses']['200']['content']['application/json'];
export type AdminQueueStatsResponse = operations['admin___queue___stats']['responses']['200']['content']['application/json']; export type AdminQueueStatsResponse = operations['admin___queue___stats']['responses']['200']['content']['application/json'];
export type AdminRelaysAddRequest = operations['admin___relays___add']['requestBody']['content']['application/json']; export type AdminRelaysAddRequest = operations['admin___relays___add']['requestBody']['content']['application/json'];
export type AdminRelaysAddResponse = operations['admin___relays___add']['responses']['200']['content']['application/json']; export type AdminRelaysAddResponse = operations['admin___relays___add']['responses']['200']['content']['application/json'];

View File

@ -584,6 +584,15 @@ export type paths = {
*/ */
post: operations['admin___queue___show-job']; post: operations['admin___queue___show-job'];
}; };
'/admin/queue/show-job-logs': {
/**
* admin/queue/show-job-logs
* @description No description provided.
*
* **Credential required**: *Yes* / **Permission**: *read:admin:queue*
*/
post: operations['admin___queue___show-job-logs'];
};
'/admin/queue/stats': { '/admin/queue/stats': {
/** /**
* admin/queue/stats * admin/queue/stats
@ -9370,6 +9379,9 @@ export interface operations {
proxyRemoteFiles: boolean; proxyRemoteFiles: boolean;
signToActivityPubGet: boolean; signToActivityPubGet: boolean;
allowExternalApRedirect: boolean; allowExternalApRedirect: boolean;
enableRemoteNotesCleaning: boolean;
remoteNotesCleaningExpiryDaysForEachNotes: number;
remoteNotesCleaningMaxProcessingDurationInMinutes: number;
}; };
}; };
}; };
@ -10164,6 +10176,73 @@ export interface operations {
}; };
}; };
}; };
'admin___queue___show-job-logs': {
requestBody: {
content: {
'application/json': {
/** @enum {string} */
queue: 'system' | 'endedPollNotification' | 'deliver' | 'inbox' | 'db' | 'relationship' | 'objectStorage' | 'userWebhookDeliver' | 'systemWebhookDeliver';
jobId: string;
};
};
};
responses: {
/** @description OK (with results) */
200: {
headers: {
[name: string]: unknown;
};
content: {
'application/json': string[];
};
};
/** @description Client error */
400: {
headers: {
[name: string]: unknown;
};
content: {
'application/json': components['schemas']['Error'];
};
};
/** @description Authentication error */
401: {
headers: {
[name: string]: unknown;
};
content: {
'application/json': components['schemas']['Error'];
};
};
/** @description Forbidden error */
403: {
headers: {
[name: string]: unknown;
};
content: {
'application/json': components['schemas']['Error'];
};
};
/** @description I'm Ai */
418: {
headers: {
[name: string]: unknown;
};
content: {
'application/json': components['schemas']['Error'];
};
};
/** @description Internal server error */
500: {
headers: {
[name: string]: unknown;
};
content: {
'application/json': components['schemas']['Error'];
};
};
};
};
admin___queue___stats: { admin___queue___stats: {
responses: { responses: {
/** @description OK (with results) */ /** @description OK (with results) */
@ -12599,6 +12678,9 @@ export interface operations {
proxyRemoteFiles?: boolean; proxyRemoteFiles?: boolean;
signToActivityPubGet?: boolean; signToActivityPubGet?: boolean;
allowExternalApRedirect?: boolean; allowExternalApRedirect?: boolean;
enableRemoteNotesCleaning?: boolean;
remoteNotesCleaningExpiryDaysForEachNotes?: number;
remoteNotesCleaningMaxProcessingDurationInMinutes?: number;
}; };
}; };
}; };