Merge remote-tracking branch 'upstream/develop' into removed-note-metadata

# Conflicts:
#	packages/backend/src/queue/processors/CleanRemoteNotesProcessorService.ts
This commit is contained in:
anatawa12 2025-08-04 23:13:59 +09:00
commit e4da21684b
No known key found for this signature in database
GPG Key ID: 9CA909848B8E4EA6
87 changed files with 6908 additions and 3150 deletions

View File

@ -25,6 +25,9 @@
- `g` キーを連打する
- URLに`?safemode=true`を付ける
- PWAのショートカットで Safemode を選択して起動する
- Feat: ページのタブバーを下部に表示できるように
- Enhance: コントロールパネルを検索できるように
- Enhance: トルコ語 (tr-TR) に対応
- Fix: 一部の設定検索結果が存在しないパスになる問題を修正
(Cherry-picked from https://activitypub.software/TransFem-org/Sharkey/-/merge_requests/1171)
- Fix: テーマエディタが動作しない問題を修正

View File

@ -68,7 +68,7 @@ receiveFollowRequest: "تلقيت طلب متابعة"
followRequestAccepted: "قُبل طلب المتابعة"
mention: "أشر الى"
mentions: "الإشارات"
directNotes: "الملاحظات المباشرة"
directNotes: "رسالة خاصة"
importAndExport: "إستورد / صدر"
import: "استيراد"
export: "تصدير"

View File

@ -1370,9 +1370,13 @@ 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."
inMinutes: "Minut(s)"
inDays: "Di(a)(es)"
safeModeEnabled: "Mode segur activat"
pluginsAreDisabledBecauseSafeMode: "Els afegits no estan activats perquè el mode segur està activat."
customCssIsDisabledBecauseSafeMode: "El CSS personalitzat no s'aplica perquè el mode segur es troba activat."
themeIsDefaultBecauseSafeMode: "El tema predeterminat es farà servir mentre el mode segur estigui activat. Una vegada es desactivi el mode segur es restablirà el tema escollit."
_order:
newest: "Més recent"
oldest: "Cronològic"
oldest: "Antigues primer"
_chat:
noMessagesYet: "Encara no tens missatges "
newMessage: "Missatge nou"
@ -1634,6 +1638,10 @@ _serverSettings:
fanoutTimelineDbFallback: "Carregar de la base de dades"
fanoutTimelineDbFallbackDescription: "Quan s'activa, la línia de temps fa servir la base de dades per consultes adicionals si la línia de temps no es troba a la memòria cau. Si és desactiva la càrrega del servidor és veure reduïda, però també és reduirà el nombre de línies de temps que és poden obtenir."
reactionsBufferingDescription: "Quan s'activa aquesta opció millora bastant el rendiment en recuperar les línies de temps reduint la càrrega de la base. Com a contrapunt, augmentarà l'ús de memòria de Redís. Desactiva aquesta opció en cas de tenir un servidor amb poca memòria o si tens problemes d'inestabilitat."
remoteNotesCleaning: "Neteja automàtica de notes remotes"
remoteNotesCleaning_description: "Quan activis aquesta opció, periòdicament es netejaran les notes remotes que no es consultin, això evitarà que la base de dades se"
remoteNotesCleaningMaxProcessingDuration: "D'oració màxima del temps de funcionament del procés de neteja"
remoteNotesCleaningExpiryDaysForEachNotes: "Duració mínima de conservació de les notes"
inquiryUrl: "URL de consulta "
inquiryUrlDescription: "Escriu adreça URL per al formulari de consulta per al mantenidor del servidor o una pàgina web amb el contacte d'informació."
openRegistration: "Registres oberts"
@ -1652,6 +1660,8 @@ _serverSettings:
userGeneratedContentsVisibilityForVisitor: "L'abast de la publicació del contingut generat per l'usuari"
userGeneratedContentsVisibilityForVisitor_description: "Això ajuda a evitar problemes com que continguts remots inadequats que no hagin estat moderats correctament es publiquin a internet mitjançant el teu servidor."
userGeneratedContentsVisibilityForVisitor_description2: "La publicació incondicional de tots els continguts del servidor a internet, incloent-hi els continguts remots rebuts pel servidor, comporta riscos. Això és extremadament important per els espectadors que desconeixen el caràcter descentralitzat dels continguts, ja que poden percebre erroneament els continguts remots com contingut generat per el propi servidor."
restartServerSetupWizardConfirm_title: "Vols tornar a executar l'assistent de configuració inicial del servidor?"
restartServerSetupWizardConfirm_text: "Algunes configuracions actuals seran restablertes."
_userGeneratedContentsVisibilityForVisitor:
all: "Tot obert al públic "
localOnly: "Només es publiquen els continguts locals, el contingut remot es manté privat"
@ -3062,6 +3072,7 @@ _bootErrors:
otherOption1: "Esborrar la configuració i la memòria cau del client"
otherOption2: "Iniciar client senzill"
otherOption3: "Iniciar l'eina de reparació "
otherOption4: "Iniciar Misskey en mode segur"
_search:
searchScopeAll: "Tot"
searchScopeLocal: "Local"
@ -3098,6 +3109,8 @@ _serverSetupWizard:
doYouConnectToFediverse_description1: "Quan es connecta amb una xarxa de servidors distribuïts (Fedivers), els continguts poden intercanviar-se amb altres servidors i entre ells."
doYouConnectToFediverse_description2: "La connexió amb el Fedivers també es coneix com a \"federació\"."
youCanConfigureMoreFederationSettingsLater: "Les configuracions avançades, com especificar els servidors amb els quals es pot federar, es poden fer més tard."
remoteContentsCleaning: "Neteja automàtica del contingut rebut"
remoteContentsCleaning_description: "Quan es comença a federar es rep un munt de contingut, quan s'activa la neteja automàtica el contingut antic que no es consulta serà eliminat del servidor, el que permet estalviar espai d'emmagatzematge."
adminInfo: "Informació de l'administrador "
adminInfo_description: "Estableix la informació de l'administrador que es farà servir per rebre consultes."
adminInfo_mustBeFilled: "Aquesta informació ha de ser omplerta si el servidor té els registres oberts o la federació es troba activada."

View File

@ -2004,7 +2004,7 @@ _deck:
list: "Seznamy"
channel: "Kanály"
mentions: "Zmínění"
direct: "Přímý"
direct: "Přímé poznámky"
roleTimeline: "Časová osa role"
_dialog:
charactersExceeded: "Překročili jste maximální počet znaků! V současné době je na hodnotě {current} z {max}."

View File

@ -353,6 +353,7 @@ _visibility:
home: "Κεντρικό"
homeDescription: "Δημοσίευση στο κεντρικό χρονολόγιο μόνο"
followers: "Ακολουθούν"
specified: "Απευθείας σημειώματα"
_profile:
name: "Όνομα"
username: "Όνομα μέλους"
@ -395,6 +396,7 @@ _deck:
antenna: "Αντένες"
list: "Λίστα"
mentions: "Επισημάνσεις"
direct: "Απευθείας σημειώματα"
_webhookSettings:
name: "Όνομα"
_moderationLogTypes:

View File

@ -81,7 +81,7 @@ import: "Import"
export: "Export"
files: "Files"
download: "Download"
driveFileDeleteConfirm: "Do you want to remove the file \"{name}\"? Some content using this file will also be removed."
driveFileDeleteConfirm: "Are you sure you want to delete \"{name}\"? All notes with this file attached will also be deleted."
unfollowConfirm: "Are you sure you want to unfollow {name}?"
exportRequested: "You've requested an export. This may take a while. It will be added to your Drive once completed."
importRequested: "You've requested an import. This may take a while."
@ -1370,6 +1370,10 @@ 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."
inMinutes: "Minute(s)"
inDays: "Day(s)"
safeModeEnabled: "Safe mode is enabled"
pluginsAreDisabledBecauseSafeMode: "All plugins are disabled because safe mode is enabled."
customCssIsDisabledBecauseSafeMode: "Custom CSS is not applied because safe mode is enabled."
themeIsDefaultBecauseSafeMode: "While safe mode is active, the default theme is used. Disabling safe mode will revert these changes."
_order:
newest: "Newest First"
oldest: "Oldest First"
@ -1402,7 +1406,7 @@ _chat:
muteThisRoom: "Mute room"
deleteRoom: "Delete room"
chatNotAvailableForThisAccountOrServer: "Chat is not enabled on this server or for this account."
chatIsReadOnlyForThisAccountOrServer: "Chat is read-only on this instance or this account. You cannot write new messages or create/join chat rooms."
chatIsReadOnlyForThisAccountOrServer: "Chat is read-only on this server or this account. You cannot write new messages or create/join chat rooms."
chatNotAvailableInOtherAccount: "The chat function is disabled for the other user."
cannotChatWithTheUser: "Cannot start a chat with this user"
cannotChatWithTheUser_description: "Chat is either unavailable or the other party has not enabled chat."
@ -1500,7 +1504,7 @@ _abuseUserReport:
resolveTutorial: "If the report's content is legitimate, select \"Accept\" to mark it as resolved.\nIf the report's content is illegitimate, select \"Reject\" to ignore it."
_delivery:
status: "Delivery status"
stop: "Suspended"
stop: "Suspend"
resume: "Delivery resume"
_type:
none: "Publishing"
@ -1634,6 +1638,10 @@ _serverSettings:
fanoutTimelineDbFallback: "Fallback to database"
fanoutTimelineDbFallbackDescription: "When enabled, the timeline will fall back to the database for additional queries if the timeline is not cached. Disabling it further reduces the server load by eliminating the fallback process, but limits the range of timelines that can be retrieved."
reactionsBufferingDescription: "When enabled, performance during reaction creation will be greatly improved, reducing the load on the database. However, Redis memory usage will increase."
remoteNotesCleaning: "Automatic cleanup of remote notes"
remoteNotesCleaning_description: "When enabled, unused and outdated remote notes will be periodically cleaned up to prevent database bloat."
remoteNotesCleaningMaxProcessingDuration: "Maximum cleanup processing time"
remoteNotesCleaningExpiryDaysForEachNotes: "Minimum days to retain notes"
inquiryUrl: "Inquiry URL"
inquiryUrlDescription: "Specify a URL for the inquiry form to the server maintainer or a web page for the contact information."
openRegistration: "Make the account creation open"
@ -1652,6 +1660,8 @@ _serverSettings:
userGeneratedContentsVisibilityForVisitor: "Visibility of user-generated content to guests"
userGeneratedContentsVisibilityForVisitor_description: "This is useful for preventing problems caused by inappropriate remote content that is not well moderated from being unintentionally published on the Internet via your own server."
userGeneratedContentsVisibilityForVisitor_description2: "Unconditionally publishing all content on the server to the Internet, including remote content received by the server is risky. This is especially important for guests who are unaware of the distributed nature of the content, as they may mistakenly believe that even remote content is content created by users on the server."
restartServerSetupWizardConfirm_title: "Restart server setup wizard?"
restartServerSetupWizardConfirm_text: "Some current settings will be reset."
_userGeneratedContentsVisibilityForVisitor:
all: "Everything is public"
localOnly: "Only local content is published, remote content is kept private"
@ -2332,7 +2342,7 @@ _permissions:
"read:admin:index-stats": "View database index stats"
"read:admin:table-stats": "View database table stats"
"read:admin:user-ips": "View user IP addresses"
"read:admin:meta": "View instance metadata"
"read:admin:meta": "View server metadata"
"write:admin:reset-password": "Reset user password"
"write:admin:resolve-abuse-user-report": "Resolve user report"
"write:admin:send-email": "Send email"
@ -2343,7 +2353,7 @@ _permissions:
"write:admin:unset-user-avatar": "Remove user avatar"
"write:admin:unset-user-banner": "Remove user banner"
"write:admin:unsuspend-user": "Unsuspend user"
"write:admin:meta": "Manage instance metadata"
"write:admin:meta": "Manage server metadata"
"write:admin:user-note": "Manage moderation note"
"write:admin:roles": "Manage roles"
"read:admin:roles": "View roles"
@ -2775,7 +2785,7 @@ _moderationLogTypes:
resetPassword: "Password reset"
suspendRemoteInstance: "Remote instance suspended"
unsuspendRemoteInstance: "Remote instance unsuspended"
updateRemoteInstanceNote: "Moderation note updated for remote instance."
updateRemoteInstanceNote: "Updated moderation note for remote servers"
markSensitiveDriveFile: "File marked as sensitive"
unmarkSensitiveDriveFile: "File unmarked as sensitive"
resolveAbuseReport: "Report resolved"
@ -3062,6 +3072,7 @@ _bootErrors:
otherOption1: "Delete client settings and cache"
otherOption2: "Start the simple client"
otherOption3: "Launch the repair tool"
otherOption4: "Launch Misskey in safe mode"
_search:
searchScopeAll: "All"
searchScopeLocal: "Local"
@ -3098,6 +3109,8 @@ _serverSetupWizard:
doYouConnectToFediverse_description1: "When connected to a network of distributed servers (Fediverse) content can be exchanged with other servers."
doYouConnectToFediverse_description2: "Connecting with the Fediverse is also called \"federation\""
youCanConfigureMoreFederationSettingsLater: "Advanced settings such as specifying federated servers can be configured later."
remoteContentsCleaning: "Automatic cleanup of received contents"
remoteContentsCleaning_description: "Federation may result in a continuous inflow of content. Enabling automatic cleanup will remove outdated and unreferenced content from the server to save storage."
adminInfo: "Administrator information"
adminInfo_description: "Sets the administrator information used to receive inquiries."
adminInfo_mustBeFilled: "Must be entered if public server or federation is on."

View File

@ -1370,6 +1370,10 @@ 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."
inMinutes: "Minutos"
inDays: "Días"
safeModeEnabled: "El modo seguro está activado"
pluginsAreDisabledBecauseSafeMode: "El modo seguro está activado, por lo que todos los plugins están desactivados."
customCssIsDisabledBecauseSafeMode: "El modo seguro está activado, por lo que no se aplica el CSS personalizado."
themeIsDefaultBecauseSafeMode: "Mientras el modo seguro esté activado, se utilizará el tema predeterminado. Cuando se desactive el modo seguro, se volverá al tema original."
_order:
newest: "Los más recientes primero"
oldest: "Los más antiguos primero"
@ -1634,6 +1638,10 @@ _serverSettings:
fanoutTimelineDbFallback: "Cargar desde la base de datos"
fanoutTimelineDbFallbackDescription: "Cuando esta opción está habilitada, la carga de peticiones adicionales de la línea de tiempo se hará desde la base de datos cuando éstas no se encuentren en la caché. Al deshabilitar esta opción se reduce la carga del servidor, pero limita el número de líneas de tiempo que pueden obtenerse."
reactionsBufferingDescription: "Cuando se activa, el rendimiento durante la creación de reacciones mejorará considerablemente, reduciendo la carga de la base de datos. Sin embargo, aumentará el uso de memoria de Redis."
remoteNotesCleaning: "Limpieza automática de notas (publicaciones) remotas"
remoteNotesCleaning_description: "Al habilitar esta opción, se limpiarán periódicamente las entradas remotas antiguas que no se consultan, lo que evitará que la base de datos se sature."
remoteNotesCleaningMaxProcessingDuration: "Tiempo máximo de funcionamiento continuo del proceso de limpieza"
remoteNotesCleaningExpiryDaysForEachNotes: "Días mínimos para conservar las notas"
inquiryUrl: "URL de consulta "
inquiryUrlDescription: "Especifica una URL para el formulario de consulta al responsable del servidor o una página web para la información de contacto."
openRegistration: "Registros Abiertos"
@ -1652,6 +1660,8 @@ _serverSettings:
userGeneratedContentsVisibilityForVisitor: "Visibilidad de contenido generado por un usuario a invitados"
userGeneratedContentsVisibilityForVisitor_description: "Esto es útil para evitar problemas causados por contenidos remotos inapropiados que no estén bien moderados y que se publiquen involuntariamente en Internet a través de su propio servidor."
userGeneratedContentsVisibilityForVisitor_description2: "Publicar incondicionalmente todo el contenido del servidor en Internet, incluido el contenido remoto recibido por el servidor, es arriesgado. Esto es especialmente importante para los invitados que desconocen la naturaleza distribuida del contenido, ya que pueden creer erróneamente que incluso el contenido remoto es contenido creado por usuarios en el servidor."
restartServerSetupWizardConfirm_title: "¿Reiniciar el asistente de configuración del servidor?"
restartServerSetupWizardConfirm_text: "Algunas configuraciones actuales se restablecerán"
_userGeneratedContentsVisibilityForVisitor:
all: "Todo es público."
localOnly: "Sólo se publica el contenido local, el remoto se mantiene privado"
@ -3062,6 +3072,7 @@ _bootErrors:
otherOption1: "Borra la configuración y la memoria caché del cliente"
otherOption2: "Iniciar el cliente simple"
otherOption3: "Iniciar la herramienta de reparación"
otherOption4: "Iniciar Misskey en modo seguro"
_search:
searchScopeAll: "Todo"
searchScopeLocal: "Local"
@ -3098,6 +3109,8 @@ _serverSetupWizard:
doYouConnectToFediverse_description1: "Cuando se conecta a una red de servidores distribuidos (Fediverso), el contenido puede intercambiarse con otros servidores."
doYouConnectToFediverse_description2: "Conectarse con el Fediverso también se conoce como \"federación\"."
youCanConfigureMoreFederationSettingsLater: "Los ajustes avanzados, como la especificación de servidores federados, pueden configurarse más adelante."
remoteContentsCleaning: "Limpieza automática de los contenidos recibidos"
remoteContentsCleaning_description: "La federación puede dar lugar a un flujo continuo de contenido. Al habilitar la limpieza automática, se eliminará del servidor el contenido obsoleto y sin referencias para ahorrar espacio de almacenamiento."
adminInfo: "Información del administrador"
adminInfo_description: "Establece la información del administrador para recibir consultas."
adminInfo_mustBeFilled: "Esta información debe ser introducida en el caso de registros abiertos o la federación esté activada."

4
locales/index.d.ts vendored
View File

@ -5871,6 +5871,10 @@ export interface Locale extends ILocale {
*
*/
"showAvailableReactionsFirstInNote": string;
/**
*
*/
"showPageTabBarBottom": string;
"_chat": {
/**
*

View File

@ -36,6 +36,7 @@ const languages = [
'ru-RU',
'sk-SK',
'th-TH',
'tr-TR',
'ug-CN',
'uk-UA',
'vi-VN',

View File

@ -1469,6 +1469,7 @@ _settings:
contentsUpdateFrequency_description2: "リアルタイムモードがオンのときは、この設定に関わらずリアルタイムでコンテンツが更新されます。"
showUrlPreview: "URLプレビューを表示する"
showAvailableReactionsFirstInNote: "利用できるリアクションを先頭に表示"
showPageTabBarBottom: "ページのタブバーを下部に表示"
_chat:
showSenderName: "送信者の名前を表示"

View File

@ -1333,6 +1333,10 @@ hideAllTips: "「ヒントとコツ」は全部表示せんでええ"
defaultImageCompressionLevel_description: "低くすると画質は保てるんやけど、ファイルサイズが増えるで。<br>高くするとファイルサイズは減らせるんやけど、画質が落ちるで。"
inMinutes: "分"
inDays: "日"
safeModeEnabled: "セーフモードがオンになってるで"
pluginsAreDisabledBecauseSafeMode: "セーフモードがオンやから、プラグインは全部無効化されてるで。"
customCssIsDisabledBecauseSafeMode: "セーフモードがオンやから、カスタムCSSは適用されてへんで。"
themeIsDefaultBecauseSafeMode: "セーフモードがオンの間はデフォルトのテーマを使うで。セーフモードをオフにれば元に戻るで。"
_chat:
noMessagesYet: "まだメッセージはあらへんで"
individualChat_description: "特定のユーザーと一対一でチャットができるで。"
@ -1345,8 +1349,59 @@ _chat:
members: "メンバーはん"
home: "ホーム"
send: "送信"
deleteRoom: "ルームをほかす"
chatNotAvailableForThisAccountOrServer: "このサーバー、もしくはこのアカウントでチャットが有効にされてへんで。"
chatIsReadOnlyForThisAccountOrServer: "このサーバー、もしくはこのアカウントでチャットが読み取り専用になっとるわ。新しく書き込んだり、チャットルームを作ったり参加したりはできへんで。"
chatNotAvailableInOtherAccount: "相手のアカウントでチャット機能が使えんくなっとるみたいやわ。"
cannotChatWithTheUser: "このユーザーとのチャットを開始できへんみたいやわ"
cannotChatWithTheUser_description: "チャットが使えん状態になっとるか、相手がチャットを開放してへんみたいやわ。"
youAreNotAMemberOfThisRoomButInvited: "あんたはこのルームの参加者ちゃうけど、招待が届いとるで。参加するんやったら、招待を承認してな。"
doYouAcceptInvitation: "招待を承認してもええんか?"
chatWithThisUser: "チャットしよか"
thisUserAllowsChatOnlyFromFollowers: "このユーザーはフォロワーからのチャットしか受け付けとらんみたいやわ。"
thisUserAllowsChatOnlyFromFollowing: "このユーザーは、このユーザーがフォローしとるユーザーからのチャットしか受け付けとらんみたいやわ。"
thisUserAllowsChatOnlyFromMutualFollowing: "このユーザーは相互フォローのユーザーからのチャットしか受け付けとらんみたいやわ。"
thisUserNotAllowedChatAnyone: "このユーザーは誰からのチャットも受け付けとらんみたいやわ。"
chatAllowedUsers: "チャットしてもええ相手"
chatAllowedUsers_note: "自分からチャットメッセージを送った相手やったらこの設定に関わらずチャットできるで。"
_chatAllowedUsers:
followers: "自分のフォロワーだけ"
following: "自分がフォローしとるユーザーだけ"
mutual: "相互フォローのユーザーだけ"
none: "誰もかもあかん"
_emojiPalette:
enableSyncBetweenDevicesForPalettes: "パレットのデバイス間同期をつけとく"
paletteForMain: "メインで使うパレット"
paletteForReaction: "リアクションで使うパレット"
_settings:
driveBanner: "ドライブの管理と設定、使用量の確認、ファイルをアップロードするときの設定ができるで。"
pluginBanner: "プラグインを使うとクライアントの機能を拡張できるねん。プラグインのインストール、個別の設定と管理ができるで。"
notificationsBanner: "サーバーから受け取る通知の種類とか範囲、プッシュ通知の設定ができるで。"
webhook: "Webhook"
serviceConnectionBanner: "外部のアプリ・サービスと連携するのに使うとるアクセストークンとかWebhookの管理と設定ができるで。"
accountDataBanner: "アカウントデータのアーカイブをエクスポート/インポートして管理できるで。"
muteAndBlockBanner: "見せんでええコンテンツの設定とか、特定のユーザーからのアクションを制限する設定と管理ができるで。"
accessibilityBanner: "クライアントの視覚や動作に関わるパーソナライズをして、よりええ感じに使えるように設定できるで。"
privacyBanner: "コンテンツの公開範囲、見つけやすさ、フォローの承認制とかアカウントのプライバシーに関わる設定ができるで。"
securityBanner: "パスワード、ログイン方法、認証アプリ、パスキーとかアカウントのセキュリティに関わる設定ができるで。"
preferencesBanner: "好みに応じた、クライアントの全体的な動作の設定ができるで。"
appearanceBanner: "好みに応じた、クライアントの見た目・表示方法に関わる設定ができるで。"
soundsBanner: "クライアントで流すサウンドの設定ができるで。"
makeEveryTextElementsSelectable: "全部のテキスト要素を選択できるようにする"
makeEveryTextElementsSelectable_description: "これをつけると、一部のシチュエーションでユーザビリティが低下するかもしれん。"
enablePullToRefresh_description: "マウスやったら、ホイールを押し込みながらドラッグしてな。"
realtimeMode_description: "サーバーと接続を確立して、リアルタイムでコンテンツを更新するで。通信量とバッテリーの消費が多くなるかもしれへん。"
contentsUpdateFrequency_description: "高いほどリアルタイムにコンテンツが更新されるんやけど、そのぶんパフォーマンスが低くなるし、通信量とバッテリーの消費も増えるねん。"
contentsUpdateFrequency_description2: "リアルタイムモードをつけてるんやったら、この設定がどうであれリアルタイムでコンテンツが更新されるで。"
_preferencesProfile:
profileNameDescription: "このデバイスはなんて呼んだらええんや?"
_preferencesBackup:
noBackupsFoundTitle: "バックアップが見つからへんね"
noBackupsFoundDescription: "自動で作られたバックアップは見つからんかったけど、バックアップファイルを手動で保存してるんやったら、それをインポートして復元できるで。"
selectBackupToRestore: "復元するバックアップを選んでや"
youNeedToNameYourProfileToEnableAutoBackup: "自動バックアップを有効するんやったらプロファイル名の設定が必要やな。"
autoPreferencesBackupIsNotEnabledForThisDevice: "このデバイスで設定の自動バックアップは有効になってへんで。"
backupFound: "設定のバックアップがあるみたいやわ"
_accountSettings:
requireSigninToViewContents: "ログインしてもらってからコンテンツ見てもらう"
requireSigninToViewContentsDescription1: "あなたが作成した全部のノートとかのコンテンツを見れるようにするのにログインがいるようにするで。クローラーにいろいろ収集されるんを防げるかもしれん。"
@ -1357,6 +1412,7 @@ _accountSettings:
makeNotesHiddenBefore: "昔のノートを見れんようにする"
makeNotesHiddenBeforeDescription: "この機能が有効になってる間は、設定された日時より前、それか設定された時間が経ったノートがフォロワーのみ見れるようになるで。無効に戻すと、ノートの公開状態も戻るで。"
mayNotEffectForFederatedNotes: "リモートサーバーに連合されたノートには効果が及ばんかもしれん。"
mayNotEffectSomeSituations: "これらの制限は簡易的なものやで。リモートサーバーでの閲覧とかモデレーション時とか、一部のシチュエーションでは適用されへんかもしれん。"
notesHavePassedSpecifiedPeriod: "決めた時間が経ったノート"
notesOlderThanSpecifiedDateAndTime: "決めた日時より前のノート"
_abuseUserReport:
@ -1375,6 +1431,7 @@ _delivery:
manuallySuspended: "手動停止中"
goneSuspended: "サーバー削除のため停止中"
autoSuspendedForNotResponding: "サーバー応答せえへんから停止中"
softwareSuspended: "配信停止中のソフトウェアやから停止中"
_bubbleGame:
howToPlay: "遊び方"
hold: "ホールド"
@ -1501,11 +1558,21 @@ _serverSettings:
fanoutTimelineDbFallback: "データベースにフォールバックする"
fanoutTimelineDbFallbackDescription: "有効にしたら、タイムラインがキャッシュん中に入ってないときにDBにもっかい問い合わせるフォールバック処理ってのをやっとくで。切ったらフォールバック処理をやらんからサーバーはもっと軽くなんねんけど、タイムラインの取得範囲がちょっと減るで。"
reactionsBufferingDescription: "有効にしたら、リアクション作るときのパフォーマンスがすっごい上がって、データベースへの負荷が減るで。代わりに、Redisのメモリ使用は増えるで。"
remoteNotesCleaning_description: "つけると、参照されてへん古いリモートの投稿を定期的にクリーンアップしてデータベースの肥大化を抑えてくれるで。"
inquiryUrl: "問い合わせ先URL"
inquiryUrlDescription: "サーバー運営者へのお問い合わせフォームのURLや、運営者の連絡先等が記載されたWebページのURLを指定するで。"
openRegistration: "アカウントの作成をオープンにする"
openRegistrationWarning: "登録を解放するのはリスクが伴うで。サーバーをいっつも監視して、なんか起きたらすぐに対応できるんやったら、オンにしてもええと思う。"
thisSettingWillAutomaticallyOffWhenModeratorsInactive: "一定期間モデレーターがおらんかったら、スパムを防ぐためにこの設定は勝手に切られるで。"
deliverSuspendedSoftwareDescription: "脆弱性とかの理由で、サーバーのソフトウェアの名前とバージョンの範囲を決めて配信を止められるで。このバージョン情報はサーバーが提供したものやから、信頼性は保証されへん。バージョン指定には semver の範囲指定が使えるねんけど、>= 2024.3.1と指定すると 2024.3.1-custom.0 みたいなカスタムバージョンが含まれへんから、>= 2024.3.1-0 みたいに prerelease を指定するとええかもしれへんな。"
singleUserMode_description: "このサーバーを使うとるんが自分だけなんやったら、このモードを有効にすると動作がええ感じになるで。"
signToActivityPubGet_description: "通常はつけといてな。連合の通信に関わる問題があるんやったら、無効にすると改善するかもしれへんけど、逆にサーバーによっては通信ができんくなることがあるで。"
proxyRemoteFiles_description: "つけると、リモートのファイルをプロキシして提供するで。画像のサムネイル生成とかユーザーのプライバシー保護にええな。"
allowExternalApRedirect_description: "つけると、他のサーバーがうちのサーバーを通して第三者のコンテンツを照会できるようになるんやけど、コンテンツのなりすましが発生するかもしれへん。"
userGeneratedContentsVisibilityForVisitor_description: "モデレーションが行き届きにくい不適切なリモートコンテンツとかが、うちのサーバー経由で図らずもインターネットに公開されてまうことによるトラブルを防止できたりするで。"
userGeneratedContentsVisibilityForVisitor_description2: "サーバーで受け取ったリモートのコンテンツを含め、サーバー内の全部のコンテンツを何でもかんでもインターネットに公開するのはリスクを伴うねん。特に、分散型の特性を知らん閲覧者にとっては、リモートのコンテンツやったとしてもサーバー内で作られたコンテンツやと誤認してまうかもしれへんから、注意が必要やな。"
restartServerSetupWizardConfirm_title: "サーバーの初期設定ウィザードをやり直すん?"
restartServerSetupWizardConfirm_text: "現在の一部の設定はリセットされるで。"
_accountMigration:
moveFrom: "別のアカウントからこのアカウントに引っ越す"
moveFromSub: "別のアカウントへエイリアスを作る"
@ -1802,6 +1869,7 @@ _role:
descriptionOfIsExplorable: "オンにしたらロールの面子一覧が「みつける」で公開されるし、ロールのタイムラインが使えるようになるで。"
displayOrder: "表示順"
descriptionOfDisplayOrder: "数がでかいほど、UI上で先に表示されるで。"
preserveAssignmentOnMoveAccount_description: "つけると、このロールがのっかったアカウントが引っ越したときに、引っ越し先アカウントにもこのロールがのっかるようになるで。"
canEditMembersByModerator: "モデレーターがメンバーいじるのを許す"
descriptionOfCanEditMembersByModerator: "オンにすると、管理者だけやなくてモデレーターもこのロールにユーザーを入れたり抜いたりできるで。オフにすると管理者だけしかやれへんくなるで。"
priority: "優先度"
@ -1842,6 +1910,8 @@ _role:
canImportFollowing: "フォローのインポートを許す"
canImportMuting: "ミュートのインポートを許す"
canImportUserLists: "リストのインポートを許す"
uploadableFileTypes_caption: "MIMEタイプを指定してや。改行で区切って複数指定もできるし、アスタリスク(*)でワイルドカード指定もできるで。(例: image/*)"
uploadableFileTypes_caption2: "ファイルによっては種別がわからんこともあるで。そないなファイルを許可するんやったら {x} を指定に追加してな。"
_condition:
roleAssignedTo: "マニュアルロールにアサイン済み"
isLocal: "ローカルユーザー"
@ -2041,7 +2111,7 @@ _theme:
navIndicator: "サイドバーのインジケーター"
link: "リンク"
hashtag: "ハッシュタグ"
mention: "メンション"
mention: "あんた宛て"
mentionMe: "うち宛てのメンション"
renote: "Renote"
modalBg: "モーダルの背景"
@ -2310,6 +2380,8 @@ _visibility:
disableFederation: "連合なし"
disableFederationDescription: "他サーバーへは送らんとくわ"
_postForm:
quitInspiteOfThereAreUnuploadedFilesConfirm: "アップロードされてへんファイルがあるんやけど、ほかしてフォームを閉じてもええんか?"
uploaderTip: "ファイルはまだアップロードされてへんで。ファイルのメニューから、リネームとか画像のクロップ、ウォーターマークをのっける、圧縮するかどうかなんかを設定できるで。ファイルはノートを投稿するときに自動でアップロードされるで。"
replyPlaceholder: "このノートに返信..."
quotePlaceholder: "このノートを引用..."
channelPlaceholder: "チャンネルに投稿..."
@ -2461,6 +2533,7 @@ _notification:
newNote: "さらの投稿"
unreadAntennaNote: "アンテナ {name}"
roleAssigned: "ロールが付与されたで"
chatRoomInvitationReceived: "チャットルームへ招待されたで"
emptyPushNotificationMessage: "プッシュ通知の更新をしといたで"
achievementEarned: "実績を獲得しとるで"
testNotification: "通知テスト"
@ -2480,7 +2553,7 @@ _notification:
all: "すべて"
note: "あんたらの新規投稿"
follow: "フォロー"
mention: "メンション"
mention: "あんた宛て"
reply: "リプライ"
renote: "リノート"
quote: "引用"
@ -2680,6 +2753,10 @@ _dataSaver:
_avatar:
title: "アイコンの絵"
description: "アイコン画像のアニメが止まるで。普通の画像よりもデータ量がでかいから、もっと通信量を節約できるねん。"
_urlPreviewThumbnail:
description: "URLプレビューのサムネイル画像が読み込まれへんくなるで。"
_disableUrlPreview:
description: "URLプレビュー機能を切るで。サムネイル画像だけと違って、リンク先の情報の読み込み自体を削減できるで。"
_code:
title: "コードハイライトは表示せんでええ"
description: "MFMとかでコードハイライト記法が使われてるとき、タップするまで読み込まれへんくなるで。コードハイライトではハイライトする言語ごとにその決めてるファイルを読む必要はあんねんな。けどな、それは自動で読み込まれなくなるから、通信量を少なくできることができるねん。"
@ -2737,6 +2814,7 @@ _offlineScreen:
_urlPreviewSetting:
title: "URLプレビューの設定"
enable: "URLプレビューを有効にする"
allowRedirectDescription: "入力されたURLがリダイレクトされるとき、そのリダイレクト先をたどってプレビューを表示するかどうかを設定できるで。無効にするとサーバーリソースを節約できるんやけど、リダイレクト先の内容は表示されへんくなるで。"
timeout: "プレビュー取得時のタイムアウト(ms)"
timeoutDescription: "プレビュー取得の所要時間がこの値を超えた場合、プレビューは生成されへんで。"
maximumContentLength: "Content-Lengthの最大値(byte)"
@ -2881,8 +2959,57 @@ _search:
searchScopeAll: "みんな"
searchScopeLocal: "ローカル"
searchScopeUser: "ユーザー指定"
pleaseEnterServerHost: "サーバーのホストはどないするん?"
pleaseSelectUser: "ユーザーを選んでや"
_serverSetupWizard:
installCompleted: "Misskeyのインストールが終わったで"
firstCreateAccount: "最初は、管理者アカウントを作成しよか。"
accountCreated: "管理者アカウントができたで!"
youCanEasilyConfigureOptimalServerSettingsWithThisWizard: "このウィザードで簡単にええ感じのサーバーの設定ができるで。"
settingsYouMakeHereCanBeChangedLater: "ここでの設定は、あとからでも変えられるで。"
howWillYouUseMisskey: "Misskeyをどんな感じに使うん"
_use:
single_youCanCreateMultipleAccounts: "お一人様サーバーとして運用するとしても、アカウントは必要に応じて複数作れるで。"
openServerAdvice: "不特定多数の利用者を受け入れるには相応のリスクがあるで。トラブルに対処できるよう、ちゃんとしたモデレーション体制で運営しいや。"
openServerAntiSpamAdvice: "うちのサーバーがスパムの踏み台にならへんように、reCAPTCHAとかのアンチボット機能を使う、みたいなセキュリティ対策もしっかり考えてな。"
howManyUsersDoYouExpect: "どれくらいの人数を考えとるん?"
largeScaleServerAdvice: "大規模なサーバーやったら、ロードバランシングとかデータベースのレプリケーションみたいな、高度なインフラストラクチャーの知識が必要になるかもしれへんわ。"
doYouConnectToFediverse: "Fediverseと接続するんやっけ"
doYouConnectToFediverse_description1: "分散型サーバーでできたネットワーク(Fediverse)に繋げると、他のサーバーと相互にコンテンツのやり取りができるようになるで。"
doYouConnectToFediverse_description2: "Fediverseと接続することは「連合」とも呼ばれるな。"
youCanConfigureMoreFederationSettingsLater: "連合してもええサーバーの指定とか、高度な設定も後でできるで。"
remoteContentsCleaning_description: "連合すると、ぎょうさんコンテンツを受け取り続けることになるねん。自動クリーニングをつけると、参照されてない古いコンテンツを自動でサーバーからほかして、ストレージを節約できるで。"
adminInfo_description: "問い合わせを受け付けるのに使う管理者情報を設定しよか。"
adminInfo_mustBeFilled: "オープンサーバー、もしくは連合を入れとるんやったら必ず入力せなあかんで。"
followingSettingsAreRecommended: "こういう設定がええかもな"
settingsCompleted: "設定が終わったで!"
settingsCompleted_description: "お疲れさん。準備ができたから、さっそくサーバーを使い始められるで。"
settingsCompleted_description2: "細かいサーバー設定は、「コントロールパネル」を見てみてな。"
_donationRequest:
text1: "Misskeyは有志で開発されとる無料のソフトウェアやで。"
text2: "今後も開発を続けられるように、よかったらぜひカンパをお願いするわ。"
text3: "支援者向け特典もあるで!"
_uploader:
abortConfirm: "アップロードされてへんファイルがあるんやけど、やめてもええんか?"
doneConfirm: "アップロードされてへんファイルがあるんやけど、完了してもええんか?"
maxFileSizeIsX: "アップロードできるファイルサイズは{x}までやで。"
tip: "ファイルはまだアップロードされてへんで。このダイアログで、アップロードする前に確認・リネーム・圧縮・クロッピングとかをできるで。準備が出来たら、「アップロード」ボタンを押してアップロードしてな。"
_clientPerformanceIssueTip:
makeSureDisabledAdBlocker: "アドブロッカーを切ってみてや"
makeSureDisabledAdBlocker_description: "アドブロッカーはパフォーマンスに影響があるかもしれへん。OSの機能とかブラウザの機能・アドオンとかでアドブロッカーが有効になってないか確認してや。"
makeSureDisabledCustomCss: "カスタムCSSを無効にしてみてや"
makeSureDisabledCustomCss_description: "スタイルを上書きするとパフォーマンスに影響があるかもしれへん。カスタムCSSとか、スタイルを上書きする拡張機能が有効になってないか確認してや。"
makeSureDisabledAddons: "拡張機能を無効にしてみてや"
makeSureDisabledAddons_description: "なんかの拡張機能がクライアントの動作にちょっかいをかけてパフォーマンスに影響を与えてるかもしれへん。ブラウザの拡張機能を無効にして良くなるか確認してや。"
_clip:
tip: "クリップは、ノートをまとめられる機能やで。"
_userLists:
tip: "好きなユーザーを含むリストを作れるねん。作ったリストはタイムラインとして表示できるで。"
_watermarkEditor:
tip: "画像にクレジット情報とかのウォーターマークをのっけられるで。"
quitWithoutSaveConfirm: "保存せずに終わってもええんか?"
driveFileTypeWarn: "このファイルは対応しとらへん"
driveFileTypeWarnDescription: "画像ファイルを選んでや"
opacity: "不透明度"
scale: "大きさ"
text: "テキスト"
@ -2894,5 +3021,8 @@ _watermarkEditor:
_imageEffector:
discardChangesConfirm: "変更をせんで終わるか?"
_drafts:
cannotCreateDraftAnymore: "下書きはこれ以上は作れへんな。"
cannotCreateDraft: "この内容で下書きは作れへんな。"
delete: "下書きをほかす"
deleteAreYouSure: "下書きをほかしてもええか?"
noDrafts: "下書きはあらへん"

View File

@ -44,6 +44,7 @@ showMore: "ಇನ್ನಷ್ಟು ನೋಡು"
youGotNewFollower: "ಹಿಂಬಾಲಿಸಿದರು"
receiveFollowRequest: "ಹಿಂಬಾಲನೆ ವಿನಂತಿ ಬಂದಿದೆ"
followRequestAccepted: "ಹಿಂಬಾಲನೆ ವಿನಂತಿ ಸ್ವೀಕರಿಸಲಾಯಿತು"
mention: "ಹೆಸರಿಸಿದ"
mentions: "ಹೆಸರಿಸಿದ"
directNotes: "ನೇರ ಟಿಪ್ಪಣಿಗಳು"
importAndExport: "ಆಮದು/ರಫ್ತು"
@ -65,6 +66,9 @@ replies: "ಉತ್ತರಿಸು"
_email:
_follow:
title: "ಹಿಂಬಾಲಿಸಿದರು"
_theme:
keys:
mention: "ಹೆಸರಿಸಿದ"
_sfx:
notification: "ಅಧಿಸೂಚನೆಗಳು"
_widgets:
@ -73,11 +77,14 @@ _widgets:
timeline: "ಸಮಯಸಾಲು"
_cw:
show: "ಇನ್ನಷ್ಟು ನೋಡು"
_visibility:
specified: "ನೇರ ಟಿಪ್ಪಣಿಗಳು"
_profile:
username: "ಬಳಕೆಹೆಸರು"
_notification:
youWereFollowed: "ಹಿಂಬಾಲಿಸಿದರು"
_types:
mention: "ಹೆಸರಿಸಿದ"
login: "ಪ್ರವೇಶ"
_actions:
reply: "ಉತ್ತರಿಸು"
@ -86,3 +93,4 @@ _deck:
notifications: "ಅಧಿಸೂಚನೆಗಳು"
tl: "ಸಮಯಸಾಲು"
mentions: "ಹೆಸರಿಸಿದ"
direct: "ನೇರ ಟಿಪ್ಪಣಿಗಳು"

View File

@ -745,7 +745,7 @@ _menuDisplay:
_theme:
description: "설멩"
keys:
mention: "멘션"
mention: "받언 멘션"
renote: "리노트"
_sfx:
note: "새 노트"
@ -775,6 +775,7 @@ _cw:
_visibility:
home: "덜머리"
followers: "팔로워"
specified: "쪽지 서기"
_postForm:
_placeholders:
e: "옇다 서 주이소"
@ -809,7 +810,7 @@ _notification:
newNote: "새 걸"
_types:
follow: "팔로잉"
mention: "멘션"
mention: "받언 멘션"
renote: "리노트"
quote: "따오기"
reaction: "반엉"
@ -824,6 +825,7 @@ _deck:
antenna: "안테나"
list: "리스트"
mentions: "받언 멘션"
direct: "쪽지 서기"
_webhookSettings:
name: "이럼"
_abuseReport:

View File

@ -1370,6 +1370,10 @@ defaultImageCompressionLevel: "기본 이미지 압축 정도"
defaultImageCompressionLevel_description: "낮추면 화질을 유지합니다만 파일 크기는 증가합니다. <br>높이면 파일 크기를 줄일 수 있습니다만 화질은 저하됩니다."
inMinutes: "분"
inDays: "일"
safeModeEnabled: "세이프 모드가 활성화돼있습니다"
pluginsAreDisabledBecauseSafeMode: "세이프 모드가 활성화돼있기에 플러그인은 전부 비활성화됩니다."
customCssIsDisabledBecauseSafeMode: "세이프 모드가 활성화돼있기에 커스텀 CSS는 적용되지 않습니다."
themeIsDefaultBecauseSafeMode: "세이프 모드가 활성화돼있는 동안에는 기본 테마가 사용됩니다. 세이프 모드를 끄면 원래대로 돌아옵니다."
_order:
newest: "최신 순"
oldest: "오래된 순"
@ -1634,6 +1638,10 @@ _serverSettings:
fanoutTimelineDbFallback: "데이터베이스를 예비로 사용하기"
fanoutTimelineDbFallbackDescription: "활성화하면 타임라인의 캐시되어 있지 않은 부분에 대해 DB에 질의하여 정보를 가져옵니다. 비활성화하면 이를 실행하지 않음으로써 서버의 부하를 줄일 수 있지만, 타임라인에서 가져올 수 있는 게시물 범위가 한정됩니다."
reactionsBufferingDescription: "활성화 한 경우, 리액션 작성 퍼포먼스가 대폭 향상되어 DB의 부하를 줄일 수 있으나, Redis의 메모리 사용량이 많아집니다."
remoteNotesCleaning: "리모트 서버 노트 자동 정리 "
remoteNotesCleaning_description: "더 이상 사용되지 않는 오래된 리모트 노트를 정기적으로 정리하여, 데이터 베이스의 사용량을 절약할 수 있습니다."
remoteNotesCleaningMaxProcessingDuration: "리모트 노트 자동 정리 최대 실행 시간"
remoteNotesCleaningExpiryDaysForEachNotes: "리모트 노트 저장 최소 일수"
inquiryUrl: "문의처 URL"
inquiryUrlDescription: "서버 운영자에게 보내는 문의 양식의 URL이나 운영자의 연락처 등이 적힌 웹 페이지의 URL을 설정합니다."
openRegistration: "회원 가입을 활성화 하기"
@ -1652,6 +1660,8 @@ _serverSettings:
userGeneratedContentsVisibilityForVisitor: "비이용자에 대한 유저 작성 콘텐츠의 공개 범위"
userGeneratedContentsVisibilityForVisitor_description: "조정을 하기 힘든 부적절한 리모트 콘텐츠 등이 자신의 서버 경유로 의도치 않게 인터넷에 공개되는 문제의 방지 등에 도움을 줍니다."
userGeneratedContentsVisibilityForVisitor_description2: "서버에서 받은 리모트 콘텐츠를 포함해 서버 내의 모든 콘텐츠를 무조건 인터넷에 공개하는 것에는 위험이 따릅니다. 특히, 분산형 특성에 대해 모르는 열람자에게는 리모트 콘텐츠여도 서버 내에서 작성된 콘텐츠라고 잘못 인식할 수 있기에 주의가 필요합니다."
restartServerSetupWizardConfirm_title: "서버의 초기 설정 위자드를 재시도하시겠습니까?"
restartServerSetupWizardConfirm_text: "현재 일부 설정은 리셋됩니다."
_userGeneratedContentsVisibilityForVisitor:
all: "모두 공개"
localOnly: "로컬 콘텐츠만 공개하고 리모트 콘텐츠는 비공개"
@ -3062,6 +3072,7 @@ _bootErrors:
otherOption1: "클라이언트 설정 및 캐시 삭제"
otherOption2: "간편 클라이언트 실행"
otherOption3: "복구 툴 실행"
otherOption4: "Misskey를 세이프 모드로 열기"
_search:
searchScopeAll: "전체"
searchScopeLocal: "로컬"
@ -3098,6 +3109,8 @@ _serverSetupWizard:
doYouConnectToFediverse_description1: "분산형 서버로 구성된 네트워크(Fediverse)에 접속하면 다른 서버와 서로 콘텐츠의 주고받기를 할 수 있습니다."
doYouConnectToFediverse_description2: "Fediverse에 접속하는 것을 '연합'이라고도 부릅니다."
youCanConfigureMoreFederationSettingsLater: "나중에 연합 가능한 서버의 지정 등 고급 설정을 할 수 있습니다."
remoteContentsCleaning: "리모트 콘텐츠 자동 정리"
remoteContentsCleaning_description: "연합 중인 서버가 있는 경우, 리모트 서버에서 대단히 많은 콘텐츠를 받아오게 됩니다. 자동 정리 기능을 활성화하면, 오래되고 서버에서 더 이상 조회되지 않는 콘텐츠를 자동으로 서버에서 삭제하여, 스토리지를 절약할 수 있습니다."
adminInfo: "관리자 정보"
adminInfo_description: "문의 접수를 위해 사용되는 관리자 정보를 설정합니다."
adminInfo_mustBeFilled: "오픈 서버 혹은 연합이 켜져 있는 경우 반드시 입력해야 합니다."

View File

@ -433,6 +433,7 @@ _cw:
_visibility:
home: "ໜ້າຫຼັກ"
followers: "ຜູ້ຕິດຕາມ"
specified: "ໂພສ Direct note"
_profile:
name: "ຊື່"
username: "ຊື່ຜູ້ໃຊ້"
@ -470,6 +471,7 @@ _deck:
list: "ລາຍການ"
channel: "ຊ່ອງ"
mentions: "ກ່າວເຖິງເຈົ້າ"
direct: "ໂພສ Direct note"
_webhookSettings:
name: "ຊື່"
_abuseReport:

View File

@ -1019,6 +1019,7 @@ _cw:
_visibility:
home: "Startpagina"
followers: "Volgers"
specified: "Directe notities"
_profile:
name: "Naam"
username: "Gebruikersnaam"
@ -1061,6 +1062,7 @@ _deck:
list: "Lijsten"
channel: "Kanalen"
mentions: "Vermeldingen"
direct: "Directe notities"
_webhookSettings:
name: "Naam"
active: "Ingeschakeld"

View File

@ -1302,6 +1302,7 @@ _cw:
_visibility:
home: "Acasă"
followers: "Urmăritori"
specified: "Note directe"
_postForm:
replyPlaceholder: "Răspunde la această notă..."
quotePlaceholder: "Citează aceasta nota..."
@ -1356,6 +1357,7 @@ _deck:
list: "Liste"
channel: "Canale"
mentions: "Mențiuni"
direct: "Note directe"
roleTimeline: "Cronologia rolului"
_webhookSettings:
name: "Nume"

View File

@ -646,6 +646,7 @@ _poll:
_visibility:
home: "Hem"
followers: "Följare"
specified: "Direktnoter"
_profile:
name: "Namn"
username: "Användarnamn"
@ -692,6 +693,7 @@ _deck:
list: "Listor"
channel: "kanal"
mentions: "Omnämningar"
direct: "Direktnoter"
_webhookSettings:
name: "Namn"
active: "Aktiverad"

View File

@ -776,7 +776,7 @@ highlightSensitiveMedia: "ไฮไลท์สื่อที่มีเนื
verificationEmailSent: "ได้ส่งอีเมลยืนยันแล้ว กรุณาเข้าลิงก์ที่ระบุในอีเมลเพื่อทำการตั้งค่าให้เสร็จสิ้น"
notSet: "ไม่ได้ตั้งค่า"
emailVerified: "อีเมลได้รับการยืนยันแล้ว"
noteFavoritesCount: "จำนวนโน้ตที่ชื่นชอบ"
noteFavoritesCount: "จำนวนโน้ตโปรด"
pageLikesCount: "จำนวนเพจที่ถูกใจ"
pageLikedCount: "จำนวนการกดถูกใจเพจที่ได้รับแล้ว"
contact: "ติดต่อ"
@ -1433,7 +1433,7 @@ _settings:
api: "API"
webhook: "Webhook"
serviceConnection: "การเชื่อมต่อกับบริการ"
serviceConnectionBanner: "สามารถจัดการและตั้งค่า Access Token และ Webhook เพื่อเชื่อมต่อกับแอปหรือบริการภายนอกได้"
serviceConnectionBanner: "สามารถจัดการและตั้งค่าโทเค็นการเข้าถึงและ Webhook เพื่อเชื่อมต่อกับแอปหรือบริการภายนอกได้"
accountData: "ข้อมูลบัญชี"
accountDataBanner: "สามารถจัดการข้อมูลบัญชีได้โดยส่งออกหรือนำเข้าไฟล์เก็บถาวร"
muteAndBlockBanner: "สามารถตั้งค่าการซ่อนเนื้อหา และจำกัดการกระทำจากผู้ใช้เฉพาะรายได้"
@ -1634,6 +1634,10 @@ _serverSettings:
fanoutTimelineDbFallback: "ฟอลแบ๊กกลับฐานข้อมูล"
fanoutTimelineDbFallbackDescription: "เมื่อเปิดใช้งาน หากไม่ได้แคชไทม์ไลน์ ไทม์ไลน์จะฟอลแบ๊กไปยังฐานข้อมูลสำหรับการ query เพิ่มเติม การปิดใช้งานจะช่วยลดภาระของเซิร์ฟเวอร์ด้วยการกำจัดกระบวนฟอลแบ๊ก แต่มันก็จะจำกัดช่วงเวลาไทม์ไลน์ที่สามารถดึงข้อมูลได้"
reactionsBufferingDescription: "เมื่อเปิดใช้งานฟังก์ชันนี้ก็จะช่วยลด latency ในการสร้างปฏิกิริยา แต่อาจจะส่งผลให้ memory footprint ของ Redis เพิ่มขึ้นนะ"
remoteNotesCleaning: "การล้างข้อมูลโพสต์จากระยะไกลโดยอัตโนมัติ"
remoteNotesCleaning_description: "เมื่อเปิดใช้งาน จะทำการล้างโพสต์จากระยะไกลเก่าที่ไม่ถูกอ้างอิง เป็นระยะ เพื่อลดการขยายตัวของฐานข้อมูล"
remoteNotesCleaningMaxProcessingDuration: "ระยะเวลาสูงสุดของการประมวลผลการล้างข้อมูล"
remoteNotesCleaningExpiryDaysForEachNotes: "จำนวนวันที่ต้องเก็บโน้ตไว้อย่างน้อย"
inquiryUrl: "URL สำหรับการติดต่อสอบถาม"
inquiryUrlDescription: "ระบุ URL ของหน้าเว็บที่มีแบบฟอร์มสำหรับติดต่อผู้ดูแลเซิร์ฟเวอร์ หรือข้อมูลการติดต่อของผู้ดูแลเซิร์ฟเวอร์"
openRegistration: "เปิดให้สร้างบัญชีได้"
@ -1652,6 +1656,8 @@ _serverSettings:
userGeneratedContentsVisibilityForVisitor: "ขอบเขตการเปิดเผยเนื้อหาที่ผู้ใช้สร้างต่อบุคคลที่ไม่ได้เข้าร่วม (แขก)"
userGeneratedContentsVisibilityForVisitor_description: "ช่วยป้องกันปัญหาที่อาจเกิดขึ้นจากเนื้อหาระยะไกลที่ไม่เหมาะสม ซึ่งอาจถูกเผยแพร่ออกสู่อินเทอร์เน็ตโดยไม่ตั้งใจผ่านเซิร์ฟเวอร์ของตนเอง โดยเฉพาะในกรณีที่การดูแลควบคุมไม่ทั่วถึง"
userGeneratedContentsVisibilityForVisitor_description2: "การเปิดเผยเนื้อหาทั้งหมดในเซิร์ฟเวอร์รวมทั้งเนื้อหาที่รับมาจากระยะไกลสู่สาธารณะบนอินเทอร์เน็ตโดยไม่มีข้อจำกัดใดๆ มีความเสี่ยงโดยเฉพาะอย่างยิ่งสำหรับผู้ชมที่ไม่เข้าใจลักษณะของระบบแบบกระจาย อาจทำให้เกิดความเข้าใจผิดคิดว่าเนื้อหาที่มาจากระยะไกลนั้นเป็นเนื้อหาที่สร้างขึ้นภายในเซิร์ฟเวอร์นี้ จึงควรใช้ความระมัดระวังอย่างมาก"
restartServerSetupWizardConfirm_title: "ต้องการเริ่มวิซาร์ดการตั้งค่าเซิร์ฟเวอร์ใหม่หรือไม่?"
restartServerSetupWizardConfirm_text: "การตั้งค่าบางส่วนในปัจจุบันจะถูกรีเซ็ต"
_userGeneratedContentsVisibilityForVisitor:
all: "ทั้งหมดสาธารณะ"
localOnly: "เผยแพร่เป็นสาธารณะเฉพาะเนื้อหาท้องถิ่น เนื้อหาระยะไกลให้เป็นส่วนตัว"
@ -3098,6 +3104,8 @@ _serverSetupWizard:
doYouConnectToFediverse_description1: "หากเชื่อมต่อกับเครือข่ายที่ประกอบด้วยเซิร์ฟเวอร์แบบกระจาย (Fediverse) จะสามารถแลกเปลี่ยนเนื้อหากับเซิร์ฟเวอร์อื่นๆ ได้"
doYouConnectToFediverse_description2: "การเชื่อมต่อกับ Fediverse เรียกว่า “สหพันธ์”"
youCanConfigureMoreFederationSettingsLater: "หลังจากนี้ยังสามารถตั้งค่าแบบขั้นสูง เช่น การกำหนดเซิร์ฟเวอร์ที่อนุญาตให้สหพันธ์ต่อกันได้เพิ่มเติม"
remoteContentsCleaning: "การล้างข้อมูลเนื้อหาที่ได้รับโดยอัตโนมัติ"
remoteContentsCleaning_description: "เมื่อมีการเชื่อมโยงสหพันธ์ จะได้รับเนื้อหาเป็นจำนวนมากอย่างต่อเนื่อง เมื่อเปิดใช้งานการล้างข้อมูลอัตโนมัติ จะทำการลบเนื้อหาเก่าที่ไม่ถูกอ้างอิง ไปจากเซิร์ฟเวอร์โดยอัตโนมัติ เพื่อประหยัดพื้นที่จัดเก็บข้อมูล"
adminInfo: "ข้อมูลผู้ดูแลระบ"
adminInfo_description: "ตั้งค่าข้อมูลผู้ดูแลระบบที่จะใช้รับคำถามและติดต่อ"
adminInfo_mustBeFilled: "หากเปิดใช้เซิร์ฟเวอร์สาธารณะ หรือเปิดใช้งานสหพันธ์ จะต้องกรอกข้อมูลนี้"

File diff suppressed because it is too large Load Diff

View File

@ -903,7 +903,7 @@ _theme:
header: "Sarlavha"
navBg: "Yon panel foni"
navFg: "Yon panel matni"
mention: "Murojat"
mention: "Eslatib o'tish"
renote: "Qayta qayd etish"
divider: "Ajratrmoq"
fgHighlighted: "Belgilangan matn"
@ -1045,7 +1045,7 @@ _notification:
_types:
all: "Barchasi"
follow: "Obuna bolish"
mention: "Murojat"
mention: "Eslatib o'tish"
renote: "Qayta qayd etish"
quote: "Iqtibos keltirish"
reaction: "Reaktsiyalar"

View File

@ -1143,7 +1143,7 @@ channelArchiveConfirmTitle: "要将 {name} 归档吗?"
channelArchiveConfirmDescription: "归档后,在频道列表与搜索结果中不会显示,也无法发布新的贴文。"
thisChannelArchived: "该频道已被归档。"
displayOfNote: "显示帖子"
initialAccountSetting: "初始设"
initialAccountSetting: "初始设"
youFollowing: "正在关注"
preventAiLearning: "拒绝接受生成式 AI 的学习"
preventAiLearningDescription: "要求文章生成 AI 或图像生成 AI 不能够以发布的帖子和图像等内容作为学习对象。这是通过在 HTML 响应中包含 noai 标志来实现的,这不能完全阻止 AI 学习你的发布内容,并不是所有 AI 都会遵守这类请求。"
@ -1370,6 +1370,10 @@ defaultImageCompressionLevel: "默认图像压缩等级"
defaultImageCompressionLevel_description: "较低的等级可以保持画质,但会增加文件大小。<br>较高的等级可以减少文件大小,但相对应的画质将会降低。"
inMinutes: "分"
inDays: "日"
safeModeEnabled: "已启用安全模式"
pluginsAreDisabledBecauseSafeMode: "因启用了安全模式,所有插件均已被禁用。"
customCssIsDisabledBecauseSafeMode: "因启用了安全模式,无法应用自定义 CSS。"
themeIsDefaultBecauseSafeMode: "启用安全模式时将使用默认主题。关闭安全模式后将还原。"
_order:
newest: "从新到旧"
oldest: "从旧到新"
@ -1538,7 +1542,7 @@ _announcement:
silenceDescription: "开启后,此条公告将不会发送通知,也不强制用户阅读。"
_initialAccountSetting:
accountCreated: "账户创建完成了!"
letsStartAccountSetup: "来进行帐户的初始设置吧。"
letsStartAccountSetup: "马上来进行账户的初始设定吧。"
letsFillYourProfile: "首先,来设定你的个人档案吧!"
profileSetting: "个人资料设置"
privacySetting: "隐私设置"
@ -1550,7 +1554,7 @@ _initialAccountSetting:
haveFun: "希望 {name} 在这里玩得开心!"
youCanContinueTutorial: "您可以继续了解 {name}(Misskey) 的使用教程,也可以在此停止教程并立即开始使用它。\n"
startTutorial: "开始教学"
skipAreYouSure: "要跳过初始设吗?"
skipAreYouSure: "要跳过初始设吗?"
laterAreYouSure: "要稍后再进行初始设定吗?"
_initialTutorial:
launchTutorial: "观看教学"
@ -1634,6 +1638,10 @@ _serverSettings:
fanoutTimelineDbFallback: "回退到数据库"
fanoutTimelineDbFallbackDescription: "当启用时,若时间线未被缓存,则将额外查询数据库。禁用该功能可通过不执行回退处理进一步减少服务器负载,但会限制可检索的时间线范围。"
reactionsBufferingDescription: "开启时可显著提高发送回应时的性能,及减轻数据库负荷。但 Redis 的内存用量会相应增加。"
remoteNotesCleaning: "自动清理远程投稿"
remoteNotesCleaning_description: "启用后,将自动清理已无法找到的旧的远程投稿,可减缓数据库的增长。"
remoteNotesCleaningMaxProcessingDuration: "最长清理持续时间"
remoteNotesCleaningExpiryDaysForEachNotes: "最短帖子保留期限"
inquiryUrl: "联络地址"
inquiryUrlDescription: "用来指定诸如向服务运营商咨询的论坛地址,或记载了运营商联系方式之类的网页地址。"
openRegistration: "开放注册"
@ -1652,6 +1660,8 @@ _serverSettings:
userGeneratedContentsVisibilityForVisitor: "用户生成内容对非用户的可见性"
userGeneratedContentsVisibilityForVisitor_description: "对于防止难以审核的不适当的远程内容等,通过自己的服务器无意中在互联网上公开等问题很有用。"
userGeneratedContentsVisibilityForVisitor_description2: "包含服务器接收到的远程内容在内,无条件将服务器上的所有内容公开在互联网上存在风险。特别是对去中心化的特性不是很了解的访问者有可能将远程服务器上的内容误认为是在此服务器内生成的,需要特别留意。"
restartServerSetupWizardConfirm_title: "要重新开始服务器初始设定向导吗?"
restartServerSetupWizardConfirm_text: "现有的部分设定将重置。"
_userGeneratedContentsVisibilityForVisitor:
all: "全部公开"
localOnly: "仅公开本地内容,隐藏远程内容"
@ -3062,6 +3072,7 @@ _bootErrors:
otherOption1: "清除客户端设定与缓存"
otherOption2: "使用简易客户端"
otherOption3: "启动修复工具"
otherOption4: "以安全模式启动 Misskey"
_search:
searchScopeAll: "全部"
searchScopeLocal: "本地"
@ -3098,6 +3109,8 @@ _serverSetupWizard:
doYouConnectToFediverse_description1: "若加入由分散性服务器所构成的网络Fediverse将能与其它服务器交换内容。"
doYouConnectToFediverse_description2: "加入 Fediverse 在这里被称为「联合」。"
youCanConfigureMoreFederationSettingsLater: "可在之后进行如哪些服务器可以进行联合等高级设置。"
remoteContentsCleaning: "自动清理传入内容"
remoteContentsCleaning_description: "加入联合后,服务器将持续接收大量内容。打开自动清理后,将自动删除无法找到的旧内容,可节省存储空间。"
adminInfo: "管理员信息"
adminInfo_description: "设置用于接受询问的管理员信息。"
adminInfo_mustBeFilled: "开放服务器或开启了联合的情况下必须输入。"

View File

@ -1370,6 +1370,10 @@ defaultImageCompressionLevel: "預設的影像壓縮程度"
defaultImageCompressionLevel_description: "低的話可以保留畫質,但是會增加檔案的大小。<br>高的話可以減少檔案大小,但是會降低畫質。"
inMinutes: "分鐘"
inDays: "日"
safeModeEnabled: "啟用安全模式"
pluginsAreDisabledBecauseSafeMode: "由於啟用安全模式,所有的外掛都被停用。"
customCssIsDisabledBecauseSafeMode: "由於啟用安全模式,所有的客製 CSS 都被停用。"
themeIsDefaultBecauseSafeMode: "啟用安全模式時將使用預設主題,關閉安全模式時將恢復預設主題。"
_order:
newest: "最新的在前"
oldest: "最舊的在前"
@ -1634,6 +1638,10 @@ _serverSettings:
fanoutTimelineDbFallback: "資料庫的回退"
fanoutTimelineDbFallbackDescription: "若啟用,在時間軸沒有快取的情況下將執行回退處理以額外查詢資料庫。若停用,可以透過不執行回退處理來進一步減少伺服器的負荷,但會限制可取得的時間軸範圍。"
reactionsBufferingDescription: "啟用時,可以顯著提高建立反應時的效能並減少資料庫的負載。 但是Redis 記憶體使用量會增加。"
remoteNotesCleaning: "自動清除遠端發佈內容"
remoteNotesCleaning_description: "啟用後,系統會定期清理未被參照的舊遠端貼文,以抑制資料庫的膨脹。"
remoteNotesCleaningMaxProcessingDuration: "清理作業的最長持續時間"
remoteNotesCleaningExpiryDaysForEachNotes: "貼文最短保留天數"
inquiryUrl: "聯絡表單網址"
inquiryUrlDescription: "指定伺服器運營者的聯絡表單網址,或包含運營者聯絡資訊網頁的網址。"
openRegistration: "允許建立帳戶"
@ -1652,6 +1660,8 @@ _serverSettings:
userGeneratedContentsVisibilityForVisitor: "使用者建立的內容對訪客的公開範圍"
userGeneratedContentsVisibilityForVisitor_description: "這有助於防止一些問題的發生,例如未經適當審核的不適當遠端內容無意中透過您自己的伺服器發佈到網際網路上。"
userGeneratedContentsVisibilityForVisitor_description2: "包括伺服器接收到的遠端內容在內,無條件地將伺服器內所有內容公開到網際網路上是具有風險的。特別是對於不了解分散式架構特性的瀏覽者來說,他們可能會誤以為這些遠端內容是由該伺服器所創建的,因此需要特別留意。"
restartServerSetupWizardConfirm_title: "要重新執行伺服器的初始設定精靈嗎?"
restartServerSetupWizardConfirm_text: "當前的部分設定將會被重設。"
_userGeneratedContentsVisibilityForVisitor:
all: "全部公開\n"
localOnly: "僅公開本地內容,遠端內容則不公開\n"
@ -3062,6 +3072,7 @@ _bootErrors:
otherOption1: "刪除用戶端設定和快取"
otherOption2: "啟動簡易用戶端"
otherOption3: "啟動修復工具"
otherOption4: "以安全模式啟動 Misskey"
_search:
searchScopeAll: "全部"
searchScopeLocal: "本地"
@ -3098,6 +3109,8 @@ _serverSetupWizard:
doYouConnectToFediverse_description1: "連接到由分散型伺服器構成的網絡(聯邦宇宙)後,您可以與其他伺服器進行內容的互相交流。\n"
doYouConnectToFediverse_description2: "連接到聯邦宇宙被稱為「聯邦」。\n"
youCanConfigureMoreFederationSettingsLater: "您可以在稍後進行更高級的設定,例如指定可以聯繫的伺服器等。\n"
remoteContentsCleaning: "自動清理接收的內容"
remoteContentsCleaning_description: "進行聯邦後,會持續接收大量內容。啟用自動清理功能後,系統會自動從伺服器中刪除未被參照的過時內容,以節省儲存空間。"
adminInfo: "管理員資訊"
adminInfo_description: "設定用於接收查詢的管理者資訊。\n"
adminInfo_mustBeFilled: "當設置為開放伺服器或啟用聯邦時,必須填寫此資訊。\n"

View File

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

View File

@ -5,7 +5,7 @@
import { setTimeout } from 'node:timers/promises';
import { Inject, Injectable } from '@nestjs/common';
import { DataSource, In, IsNull, LessThan, Not } from 'typeorm';
import { DataSource } from 'typeorm';
import { DI } from '@/di-symbols.js';
import { MiNote } from '@/models/Note.js';
import { MiDeletedNote } from '@/models/DeletedNote.js';
@ -72,92 +72,79 @@ export class CleanRemoteNotesProcessorService {
newest: null as number | null,
};
let cursor: MiNote['id'] = this.idService.gen(Date.now() - (1000 * 60 * 60 * 24 * this.meta.remoteNotesCleaningExpiryDaysForEachNotes));
// The date limit for the newest note to be considered for deletion.
// All notes newer than this limit will always be retained.
const newestLimit = this.idService.gen(Date.now() - (1000 * 60 * 60 * 24 * this.meta.remoteNotesCleaningExpiryDaysForEachNotes));
let cursor = '0'; // oldest note ID to start from
while (true) {
const batchBeginAt = Date.now();
const selectColumns = [...[
'id',
'replyId',
'renoteId',
'userId',
'localOnly',
'uri',
'url',
'channelId',
'replyUserId',
'renoteUserId',
] as const];
let notes: Pick<MiNote, typeof selectColumns[number]>[] = 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: selectColumns,
});
// We use string literals instead of query builder for several reasons:
// - for removeCondition, we need to use it in having clause, which is not supported by Brackets.
// - for recursive part, we need to preserve the order of columns, but typeorm query builder does not guarantee the order of columns in the result query
// The condition for removing the notes.
// The note must be:
// - old enough (older than the newestLimit)
// - a remote note (userHost is not null).
// - not have clipped
// - not have pinned on the user profile
// - not has been favorite by any user
const removeCondition = 'note.id < :newestLimit'
+ ' AND note."clippedCount" = 0'
+ ' AND note."userHost" IS NOT NULL'
// using both userId and noteId instead of just noteId to use index on user_note_pining table.
// This is safe because notes are only pinned by the user who created them.
+ ' AND NOT EXISTS(SELECT 1 FROM "user_note_pining" WHERE "noteId" = note."id" AND "userId" = note."userId")'
// We cannot use userId trick because users can favorite notes from other users.
+ ' AND NOT EXISTS(SELECT 1 FROM "note_favorite" WHERE "noteId" = note."id")'
;
// The initiator query contains the oldest ${MAX_NOTE_COUNT_PER_QUERY} remote non-clipped notes
const initiatorQuery = `
SELECT "note"."id" AS "id", "note"."replyId" AS "replyId", "note"."renoteId" AS "renoteId", "note"."id" AS "initiatorId"
FROM "note" "note" WHERE ${removeCondition} AND "note"."id" > :cursor ORDER BY "note"."id" ASC LIMIT ${MAX_NOTE_COUNT_PER_QUERY}`;
// The union query queries the related notes and replies related to the initiator query
const unionQuery = `
SELECT "note"."id", "note"."replyId", "note"."renoteId", rn."initiatorId"
FROM "note" "note"
INNER JOIN "related_notes" "rn"
ON "note"."replyId" = rn.id
OR "note"."renoteId" = rn.id
OR "note"."id" = rn."replyId"
OR "note"."id" = rn."renoteId"
`;
const recursiveQuery = `(${initiatorQuery}) UNION (${unionQuery})`;
const removableInitiatorNotesQuery = this.notesRepository.createQueryBuilder('note')
.select('rn."initiatorId"')
.innerJoin('related_notes', 'rn', 'note.id = rn.id')
.groupBy('rn."initiatorId"')
.having(`bool_and(${removeCondition})`);
const notesQuery = this.notesRepository.createQueryBuilder('note')
.addCommonTableExpression(recursiveQuery, 'related_notes', { recursive: true })
.select('note.id', 'id')
.addSelect('rn."initiatorId"')
.innerJoin('related_notes', 'rn', 'note.id = rn.id')
.where(`rn."initiatorId" IN (${ removableInitiatorNotesQuery.getQuery() })`)
.setParameters({ cursor, newestLimit });
const notes: { id: MiNote['id'], initiatorId: MiNote['id'] }[] = await notesQuery.getRawMany();
const fetchedCount = notes.length;
// update the cursor to the newest initiatorId found in the fetched notes.
// We don't use 'id' since the note can be newer than the initiator note.
for (const note of notes) {
if (note.id < cursor) {
cursor = note.id;
if (cursor < note.initiatorId) {
cursor = note.initiatorId;
}
}
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)),
},
select: ['replyId', 'userHost'],
});
const noteIdsWithReplies = new Set(replies.map(reply => reply.replyId));
notes = notes.filter(note => {
return !replies.some(reply => reply.userHost == null && reply.replyId === note.id);
});
// find self renotes and quotes to determine if we should keep deleted notes
const renotes = notes.length === 0 ? [] : await this.notesRepository.find({
where: {
renoteId: In(notes.map(note => note.id)),
},
select: ['renoteId'],
});
const noteIdsWithRenotes = new Set(renotes.map(reply => reply.replyId));
if (notes.length > 0) {
await this.db.transaction(async (transaction) => {
await transaction.save(MiDeletedNote, notes.filter(x => x.replyId != null || x.renoteId != null || noteIdsWithReplies.has(x.id) || noteIdsWithRenotes.has(x.id)).map(note => ({

View File

@ -20,17 +20,6 @@ import type { Config } from '@/config.js';
import { getNoteSummary } from '@/misc/get-note-summary.js';
import { DI } from '@/di-symbols.js';
import * as Acct from '@/misc/acct.js';
import type {
DbQueue,
DeliverQueue,
EndedPollNotificationQueue,
InboxQueue,
ObjectStorageQueue,
RelationshipQueue,
SystemQueue,
UserWebhookDeliverQueue,
SystemWebhookDeliverQueue,
} from '@/core/QueueModule.js';
import { UserEntityService } from '@/core/entities/UserEntityService.js';
import { NoteEntityService } from '@/core/entities/NoteEntityService.js';
import { PageEntityService } from '@/core/entities/PageEntityService.js';
@ -129,16 +118,6 @@ export class ClientServerService {
private feedService: FeedService,
private roleService: RoleService,
private clientLoggerService: ClientLoggerService,
@Inject('queue:system') public systemQueue: SystemQueue,
@Inject('queue:endedPollNotification') public endedPollNotificationQueue: EndedPollNotificationQueue,
@Inject('queue:deliver') public deliverQueue: DeliverQueue,
@Inject('queue:inbox') public inboxQueue: InboxQueue,
@Inject('queue:db') public dbQueue: DbQueue,
@Inject('queue:relationship') public relationshipQueue: RelationshipQueue,
@Inject('queue:objectStorage') public objectStorageQueue: ObjectStorageQueue,
@Inject('queue:userWebhookDeliver') public userWebhookDeliverQueue: UserWebhookDeliverQueue,
@Inject('queue:systemWebhookDeliver') public systemWebhookDeliverQueue: SystemWebhookDeliverQueue,
) {
//this.createServer = this.createServer.bind(this);
}

View File

@ -15,8 +15,8 @@
"@rollup/plugin-replace": "6.0.2",
"@rollup/pluginutils": "5.2.0",
"@twemoji/parser": "16.0.0",
"@vitejs/plugin-vue": "5.2.4",
"@vue/compiler-sfc": "3.5.17",
"@vitejs/plugin-vue": "6.0.1",
"@vue/compiler-sfc": "3.5.18",
"astring": "1.9.0",
"buraha": "0.0.1",
"estree-walker": "3.0.3",
@ -26,37 +26,37 @@
"mfm-js": "0.25.0",
"misskey-js": "workspace:*",
"punycode.js": "2.3.1",
"rollup": "4.45.1",
"rollup": "4.46.2",
"sass": "1.89.2",
"shiki": "3.8.0",
"shiki": "3.9.1",
"tinycolor2": "1.6.0",
"tsc-alias": "1.8.16",
"tsconfig-paths": "4.2.0",
"typescript": "5.8.3",
"typescript": "5.9.2",
"uuid": "11.1.0",
"vite": "6.3.5",
"vue": "3.5.17"
"vite": "7.0.6",
"vue": "3.5.18"
},
"devDependencies": {
"@misskey-dev/summaly": "5.2.2",
"@tabler/icons-webfont": "3.34.0",
"@misskey-dev/summaly": "5.2.3",
"@tabler/icons-webfont": "3.34.1",
"@testing-library/vue": "8.1.0",
"@types/estree": "1.0.8",
"@types/micromatch": "4.0.9",
"@types/node": "22.16.4",
"@types/node": "22.17.0",
"@types/punycode.js": "npm:@types/punycode@2.1.4",
"@types/tinycolor2": "1.4.6",
"@types/ws": "8.18.1",
"@typescript-eslint/eslint-plugin": "8.37.0",
"@typescript-eslint/parser": "8.37.0",
"@typescript-eslint/eslint-plugin": "8.38.0",
"@typescript-eslint/parser": "8.38.0",
"@vitest/coverage-v8": "3.2.4",
"@vue/runtime-core": "3.5.17",
"@vue/runtime-core": "3.5.18",
"acorn": "8.15.0",
"cross-env": "7.0.3",
"cross-env": "10.0.0",
"eslint-plugin-import": "2.32.0",
"eslint-plugin-vue": "10.3.0",
"eslint-plugin-vue": "10.4.0",
"fast-glob": "3.3.3",
"happy-dom": "17.6.3",
"happy-dom": "18.0.1",
"intersection-observer": "0.12.2",
"micromatch": "4.0.8",
"msw": "2.10.4",
@ -64,8 +64,8 @@
"prettier": "3.6.2",
"start-server-and-test": "2.0.12",
"vite-plugin-turbosnap": "1.0.3",
"vue-component-type-helpers": "2.2.12",
"vue-component-type-helpers": "3.0.5",
"vue-eslint-parser": "10.2.0",
"vue-tsc": "2.2.12"
"vue-tsc": "3.0.5"
}
}

View File

@ -21,13 +21,13 @@
"lint": "pnpm typecheck && pnpm eslint"
},
"devDependencies": {
"@types/node": "22.16.4",
"@typescript-eslint/eslint-plugin": "8.37.0",
"@typescript-eslint/parser": "8.37.0",
"esbuild": "0.25.6",
"eslint-plugin-vue": "10.3.0",
"@types/node": "22.17.0",
"@typescript-eslint/eslint-plugin": "8.38.0",
"@typescript-eslint/parser": "8.38.0",
"esbuild": "0.25.8",
"eslint-plugin-vue": "10.4.0",
"nodemon": "3.1.10",
"typescript": "5.8.3",
"typescript": "5.9.2",
"vue-eslint-parser": "10.2.0"
},
"files": [
@ -35,6 +35,6 @@
],
"dependencies": {
"misskey-js": "workspace:*",
"vue": "3.5.17"
"vue": "3.5.18"
}
}

View File

@ -39,6 +39,7 @@ export interface SearchIndexItem {
path?: string;
label: string;
keywords: string[];
texts: string[];
icon?: string;
inlining?: string[];
}
@ -227,14 +228,14 @@ function extractElementText2Inner(node: TemplateChildNode, processingNodeName: s
// region extractUsageInfoFromTemplateAst
/**
* SearchLabel/SearchKeyword/SearchIconを探して抽出する関数
* SearchLabel/SearchText/SearchIconを探して抽出する関数
*/
function extractSugarTags(nodes: TemplateChildNode[], id: string): { label: string | null, keywords: string[], icon: string | null } {
function extractSugarTags(nodes: TemplateChildNode[], id: string): { label: string | null; texts: string[]; icon: string | null; } {
let label: string | null | undefined = undefined;
let icon: string | null | undefined = undefined;
const keywords: string[] = [];
const texts: string[] = [];
logger.info(`Extracting labels and keywords from ${nodes.length} nodes`);
logger.info(`Extracting labels and texts from ${nodes.length} nodes`);
walkVueElements(nodes, null, (node) => {
switch (node.tag) {
@ -248,10 +249,10 @@ function extractSugarTags(nodes: TemplateChildNode[], id: string): { label: stri
label = extractElementText(node, id);
return;
case 'SearchKeyword':
case 'SearchText':
const content = extractElementText(node, id);
if (content) {
keywords.push(content);
texts.push(content);
}
return;
case 'SearchIcon':
@ -278,8 +279,8 @@ function extractSugarTags(nodes: TemplateChildNode[], id: string): { label: stri
});
// デバッグ情報
logger.info(`Extraction completed: label=${label}, keywords=[${keywords.join(', ')}, icon=${icon}]`);
return { label: label ?? null, keywords, icon: icon ?? null };
logger.info(`Extraction completed: label=${label}, text=[${texts.join(', ')}, icon=${icon}]`);
return { label: label ?? null, texts, icon: icon ?? null };
}
function getStringProp(attr: AttributeNode | DirectiveNode | null, id: string): string | null {
@ -351,33 +352,36 @@ function extractUsageInfoFromTemplateAst(
parentId: parentId ?? undefined,
label: '', // デフォルト値
keywords: [],
texts: [],
};
// バインドプロパティを取得
const path = getStringProp(findAttribute(node.props, 'path'), id)
const icon = getStringProp(findAttribute(node.props, 'icon'), id)
const label = getStringProp(findAttribute(node.props, 'label'), id)
const inlining = getStringArrayProp(findAttribute(node.props, 'inlining'), id)
const keywords = getStringArrayProp(findAttribute(node.props, 'keywords'), id)
const path = getStringProp(findAttribute(node.props, 'path'), id);
const icon = getStringProp(findAttribute(node.props, 'icon'), id);
const label = getStringProp(findAttribute(node.props, 'label'), id);
const inlining = getStringArrayProp(findAttribute(node.props, 'inlining'), id);
const keywords = getStringArrayProp(findAttribute(node.props, 'keywords'), id);
const texts = getStringArrayProp(findAttribute(node.props, 'texts'), id);
if (path) markerInfo.path = path;
if (icon) markerInfo.icon = icon;
if (label) markerInfo.label = label;
if (inlining) markerInfo.inlining = inlining;
if (keywords) markerInfo.keywords = keywords;
if (texts) markerInfo.texts = texts;
//pathがない場合はファイルパスを設定
// pathがない場合はファイルパスを設定
if (markerInfo.path == null && parentId == null) {
markerInfo.path = id.match(/.*(\/(admin|settings)\/[^\/]+)\.vue$/)?.[1];
}
// SearchLabelとSearchKeywordを抽出 (AST全体を探索)
// SearchLabelとSearchTextを抽出 (AST全体を探索)
{
const extracted = extractSugarTags(node.children, id);
if (extracted.label && markerInfo.label) logger.warn(`Duplicate label found for ${markerId} at ${id}:${node.loc.start.line}`);
if (extracted.icon && markerInfo.icon) logger.warn(`Duplicate icon found for ${markerId} at ${id}:${node.loc.start.line}`);
markerInfo.label = extracted.label ?? markerInfo.label ?? '';
markerInfo.keywords = [...extracted.keywords, ...markerInfo.keywords];
markerInfo.texts = [...extracted.texts, ...markerInfo.texts];
markerInfo.icon = extracted.icon ?? markerInfo.icon ?? undefined;
}

View File

@ -24,11 +24,11 @@
"@rollup/plugin-json": "6.1.0",
"@rollup/plugin-replace": "6.0.2",
"@rollup/pluginutils": "5.2.0",
"@sentry/vue": "9.39.0",
"@sentry/vue": "10.0.0",
"@syuilo/aiscript": "0.19.0",
"@twemoji/parser": "16.0.0",
"@vitejs/plugin-vue": "5.2.4",
"@vue/compiler-sfc": "3.5.17",
"@vitejs/plugin-vue": "6.0.1",
"@vue/compiler-sfc": "3.5.18",
"aiscript-vscode": "github:aiscript-dev/aiscript-vscode#v0.1.15",
"analytics": "0.8.16",
"astring": "1.9.0",
@ -37,12 +37,12 @@
"canvas-confetti": "1.9.3",
"chart.js": "4.5.0",
"chartjs-adapter-date-fns": "3.0.0",
"chartjs-chart-matrix": "2.1.1",
"chartjs-chart-matrix": "3.0.0",
"chartjs-plugin-gradient": "0.6.1",
"chartjs-plugin-zoom": "2.2.0",
"chromatic": "11.29.0",
"chromatic": "13.1.3",
"compare-versions": "6.1.1",
"cropperjs": "2.0.0",
"cropperjs": "2.0.1",
"date-fns": "4.1.0",
"estree-walker": "3.0.3",
"eventemitter3": "5.0.1",
@ -60,30 +60,30 @@
"misskey-reversi": "workspace:*",
"photoswipe": "5.4.4",
"punycode.js": "2.3.1",
"rollup": "4.45.1",
"rollup": "4.46.2",
"sanitize-html": "2.17.0",
"sass": "1.89.2",
"shiki": "3.8.0",
"shiki": "3.9.1",
"strict-event-emitter-types": "2.0.0",
"textarea-caret": "3.1.0",
"three": "0.178.0",
"three": "0.179.1",
"throttle-debounce": "5.0.2",
"tinycolor2": "1.6.0",
"tsc-alias": "1.8.16",
"tsconfig-paths": "4.2.0",
"typescript": "5.8.3",
"typescript": "5.9.2",
"v-code-diff": "1.13.1",
"vite": "6.3.5",
"vue": "3.5.17",
"vite": "7.0.6",
"vue": "3.5.18",
"vuedraggable": "next",
"wanakana": "5.3.1"
},
"devDependencies": {
"@misskey-dev/summaly": "5.2.2",
"@storybook/addon-actions": "8.6.14",
"@misskey-dev/summaly": "5.2.3",
"@storybook/addon-actions": "9.0.8",
"@storybook/addon-essentials": "8.6.14",
"@storybook/addon-interactions": "8.6.14",
"@storybook/addon-links": "8.6.14",
"@storybook/addon-links": "9.1.0",
"@storybook/addon-mdx-gfm": "8.6.14",
"@storybook/addon-storysource": "8.6.14",
"@storybook/blocks": "8.6.14",
@ -91,38 +91,38 @@
"@storybook/core-events": "8.6.14",
"@storybook/manager-api": "8.6.14",
"@storybook/preview-api": "8.6.14",
"@storybook/react": "8.6.14",
"@storybook/react-vite": "8.6.14",
"@storybook/react": "9.1.0",
"@storybook/react-vite": "9.1.0",
"@storybook/test": "8.6.14",
"@storybook/theming": "8.6.14",
"@storybook/types": "8.6.14",
"@storybook/vue3": "8.6.14",
"@storybook/vue3-vite": "8.6.14",
"@tabler/icons-webfont": "3.34.0",
"@storybook/vue3": "9.1.0",
"@storybook/vue3-vite": "9.1.0",
"@tabler/icons-webfont": "3.34.1",
"@testing-library/vue": "8.1.0",
"@types/canvas-confetti": "1.9.0",
"@types/estree": "1.0.8",
"@types/matter-js": "0.19.8",
"@types/micromatch": "4.0.9",
"@types/node": "22.16.4",
"@types/node": "22.17.0",
"@types/punycode.js": "npm:@types/punycode@2.1.4",
"@types/sanitize-html": "2.16.0",
"@types/seedrandom": "3.0.8",
"@types/throttle-debounce": "5.0.2",
"@types/tinycolor2": "1.4.6",
"@types/ws": "8.18.1",
"@typescript-eslint/eslint-plugin": "8.37.0",
"@typescript-eslint/parser": "8.37.0",
"@typescript-eslint/eslint-plugin": "8.38.0",
"@typescript-eslint/parser": "8.38.0",
"@vitest/coverage-v8": "3.2.4",
"@vue/compiler-core": "3.5.17",
"@vue/runtime-core": "3.5.17",
"@vue/compiler-core": "3.5.18",
"@vue/runtime-core": "3.5.18",
"acorn": "8.15.0",
"cross-env": "7.0.3",
"cypress": "14.5.2",
"cross-env": "10.0.0",
"cypress": "14.5.3",
"eslint-plugin-import": "2.32.0",
"eslint-plugin-vue": "10.3.0",
"eslint-plugin-vue": "10.4.0",
"fast-glob": "3.3.3",
"happy-dom": "17.6.3",
"happy-dom": "18.0.1",
"intersection-observer": "0.12.2",
"micromatch": "4.0.8",
"minimatch": "10.0.3",
@ -130,17 +130,17 @@
"msw-storybook-addon": "2.0.5",
"nodemon": "3.1.10",
"prettier": "3.6.2",
"react": "19.1.0",
"react-dom": "19.1.0",
"react": "19.1.1",
"react-dom": "19.1.1",
"seedrandom": "3.0.5",
"start-server-and-test": "2.0.12",
"storybook": "8.6.14",
"storybook": "9.1.0",
"storybook-addon-misskey-theme": "github:misskey-dev/storybook-addon-misskey-theme",
"vite-plugin-turbosnap": "1.0.3",
"vitest": "3.2.4",
"vitest-fetch-mock": "0.4.5",
"vue-component-type-helpers": "2.2.12",
"vue-component-type-helpers": "3.0.5",
"vue-eslint-parser": "10.2.0",
"vue-tsc": "2.2.12"
"vue-tsc": "3.0.5"
}
}

View File

@ -52,9 +52,9 @@ SPDX-License-Identifier: AGPL-3.0-only
{{ item.label }}
</template>
<template v-else>
<span style="opacity: 0.7; font-size: 90%;">{{ item.parentLabels.join(' > ') }}</span>
<span style="opacity: 0.7; font-size: 90%; word-break: break-word;">{{ item.parentLabels.join(' > ') }}</span>
<br>
<span>{{ item.label }}</span>
<span style="word-break: break-word;">{{ item.label }}</span>
</template>
</span>
</MkA>
@ -95,7 +95,7 @@ export type SuperMenuDef = {
<script lang="ts" setup>
import { useTemplateRef, ref, watch, nextTick, computed } from 'vue';
import { getScrollContainer } from '@@/js/scroll.js';
import type { SearchIndexItem } from '@/utility/settings-search-index.js';
import type { SearchIndexItem } from '@/utility/inapp-search.js';
import MkInput from '@/components/MkInput.vue';
import { i18n } from '@/i18n.js';
import { useRouter } from '@/router.js';
@ -165,12 +165,28 @@ watch(rawSearchQuery, (value) => {
});
};
for (const item of searchIndexItemById.values()) {
if (
compareStringIncludes(item.label, value) ||
item.keywords.some((x) => compareStringIncludes(x, value))
) {
// label, keywords, texts
let items = Array.from(searchIndexItemById.values());
for (const item of items) {
if (compareStringIncludes(item.label, value)) {
addSearchResult(item);
items = items.filter(i => i.id !== item.id);
}
}
for (const item of items) {
if (item.keywords.some((x) => compareStringIncludes(x, value))) {
addSearchResult(item);
items = items.filter(i => i.id !== item.id);
}
}
for (const item of items) {
if (item.texts.some((x) => compareStringIncludes(x, value))) {
addSearchResult(item);
items = items.filter(i => i.id !== item.id);
}
}
}

View File

@ -4,7 +4,7 @@ SPDX-License-Identifier: AGPL-3.0-only
-->
<template>
<div :class="$style.tabs">
<div :class="[$style.tabs, { [$style.centered]: props.centered }]">
<div :class="$style.tabsInner">
<button
v-for="t in tabs" :ref="(el) => tabRefs[t.key] = (el as HTMLElement)" v-tooltip.noDelay="t.title"
@ -30,7 +30,7 @@ SPDX-License-Identifier: AGPL-3.0-only
</div>
<div
ref="tabHighlightEl"
:class="[$style.tabHighlight, { [$style.animate]: prefer.s.animation }]"
:class="[$style.tabHighlight, { [$style.animate]: prefer.s.animation, [$style.tabHighlightUpper]: tabHighlightUpper }]"
></div>
</div>
</template>
@ -59,6 +59,8 @@ import { prefer } from '@/preferences.js';
const props = withDefaults(defineProps<{
tabs?: Tab[];
tab?: string;
centered?: boolean;
tabHighlightUpper?: boolean;
}>(), {
tabs: () => ([] as Tab[]),
});
@ -169,6 +171,16 @@ onUnmounted(() => {
overflow-x: auto;
overflow-y: hidden;
scrollbar-width: none;
&.centered {
text-align: center;
}
}
@container (max-width: 450px) {
.tabs {
font-size: 80%;
}
}
.tabsInner {
@ -227,5 +239,10 @@ onUnmounted(() => {
&.animate {
transition: width 0.15s ease, left 0.15s ease;
}
&.tabHighlightUpper {
top: 0;
bottom: auto;
}
}
</style>

View File

@ -6,14 +6,22 @@ SPDX-License-Identifier: AGPL-3.0-only
<template>
<div ref="rootEl" :class="[$style.root, reversed ? '_pageScrollableReversed' : '_pageScrollable']">
<MkStickyContainer>
<template #header><MkPageHeader v-model:tab="tab" v-bind="pageHeaderProps"/></template>
<template #header>
<MkPageHeader v-if="prefer.s.showPageTabBarBottom && (props.tabs?.length ?? 0) > 0" v-bind="pageHeaderPropsWithoutTabs"/>
<MkPageHeader v-else v-model:tab="tab" v-bind="pageHeaderProps"/>
</template>
<div :class="$style.body">
<MkSwiper v-if="prefer.s.enableHorizontalSwipe && swipable && (props.tabs?.length ?? 1) > 1" v-model:tab="tab" :class="$style.swiper" :tabs="props.tabs">
<slot></slot>
</MkSwiper>
<slot v-else></slot>
</div>
<template #footer><slot name="footer"></slot></template>
<template #footer>
<slot name="footer"></slot>
<div v-if="prefer.s.showPageTabBarBottom && (props.tabs?.length ?? 0) > 0" :class="$style.footerTabs">
<MkTabs v-model:tab="tab" :tabs="props.tabs" :centered="true" :tabHighlightUpper="true"/>
</div>
</template>
</MkStickyContainer>
</div>
</template>
@ -26,6 +34,7 @@ import { useScrollPositionKeeper } from '@/composables/use-scroll-position-keepe
import MkSwiper from '@/components/MkSwiper.vue';
import { useRouter } from '@/router.js';
import { prefer } from '@/preferences.js';
import MkTabs from '@/components/MkTabs.vue';
const props = withDefaults(defineProps<PageHeaderProps & {
reversed?: boolean;
@ -40,6 +49,11 @@ const pageHeaderProps = computed(() => {
return rest;
});
const pageHeaderPropsWithoutTabs = computed(() => {
const { reversed, tabs, ...rest } = props;
return rest;
});
const tab = defineModel<string>('tab');
const rootEl = useTemplateRef('rootEl');
@ -68,4 +82,11 @@ defineExpose({
.body, .swiper {
min-height: calc(100cqh - (var(--MI-stickyTop, 0px) + var(--MI-stickyBottom, 0px)));
}
.footerTabs {
background: color(from var(--MI_THEME-pageHeaderBg) srgb r g b / 0.75);
-webkit-backdrop-filter: var(--MI-blur, blur(15px));
backdrop-filter: var(--MI-blur, blur(15px));
border-top: solid 0.5px var(--MI_THEME-divider);
}
</style>

View File

@ -0,0 +1,14 @@
<!--
SPDX-FileCopyrightText: syuilo and misskey-project
SPDX-License-Identifier: AGPL-3.0-only
-->
<template>
<slot></slot>
</template>
<script lang="ts" setup>
</script>
<style lang="scss" module>
</style>

View File

@ -31,7 +31,7 @@ import PageWithHeader from './global/PageWithHeader.vue';
import PageWithAnimBg from './global/PageWithAnimBg.vue';
import SearchMarker from './global/SearchMarker.vue';
import SearchLabel from './global/SearchLabel.vue';
import SearchKeyword from './global/SearchKeyword.vue';
import SearchText from './global/SearchText.vue';
import SearchIcon from './global/SearchIcon.vue';
import type { App } from 'vue';
@ -71,7 +71,7 @@ export const components = {
PageWithAnimBg: PageWithAnimBg,
SearchMarker: SearchMarker,
SearchLabel: SearchLabel,
SearchKeyword: SearchKeyword,
SearchText: SearchText,
SearchIcon: SearchIcon,
};
@ -105,7 +105,7 @@ declare module '@vue/runtime-core' {
PageWithAnimBg: typeof PageWithAnimBg;
SearchMarker: typeof SearchMarker;
SearchLabel: typeof SearchLabel;
SearchKeyword: typeof SearchKeyword;
SearchText: typeof SearchText;
SearchIcon: typeof SearchIcon;
}
}

View File

@ -165,6 +165,8 @@ function buildFullPath(args: {
const replaceRegex = new RegExp(`:${key}(\\?)?`, 'g');
fullPath = fullPath.replace(replaceRegex, value ? encodeURIComponent(value) : '');
}
// remove any optional parameters that are not provided
fullPath = fullPath.replace(/\/:\w+\?(?=\/|$)/g, '');
}
if (args.query) {

View File

@ -4,9 +4,10 @@ SPDX-License-Identifier: AGPL-3.0-only
-->
<template>
<MkFolder>
<template #icon><i class="ti ti-shield"></i></template>
<template #label>{{ i18n.ts.botProtection }}</template>
<SearchMarker markerId="botProtection" :keywords="['bot', 'protection', 'captcha', 'hcaptcha', 'mcaptcha', 'recaptcha', 'turnstile']">
<MkFolder>
<template #icon><SearchIcon><i class="ti ti-shield"></i></SearchIcon></template>
<template #label><SearchLabel>{{ i18n.ts.botProtection }}</SearchLabel></template>
<template v-if="botProtectionForm.savedState.provider === 'hcaptcha'" #suffix>hCaptcha</template>
<template v-else-if="botProtectionForm.savedState.provider === 'mcaptcha'" #suffix>mCaptcha</template>
<template v-else-if="botProtectionForm.savedState.provider === 'recaptcha'" #suffix>reCAPTCHA</template>
@ -150,12 +151,14 @@ SPDX-License-Identifier: AGPL-3.0-only
</FormSlot>
</template>
</div>
</MkFolder>
</MkFolder>
</SearchMarker>
</template>
<script lang="ts" setup>
import { computed, defineAsyncComponent, ref, watch } from 'vue';
import * as Misskey from 'misskey-js';
import type { ApiWithDialogCustomErrors } from '@/os.js';
import MkRadios from '@/components/MkRadios.vue';
import MkInput from '@/components/MkInput.vue';
import FormSlot from '@/components/form/slot.vue';
@ -167,7 +170,6 @@ import { useForm } from '@/composables/use-form.js';
import MkFormFooter from '@/components/MkFormFooter.vue';
import MkFolder from '@/components/MkFolder.vue';
import MkInfo from '@/components/MkInfo.vue';
import type { ApiWithDialogCustomErrors } from '@/os.js';
const MkCaptcha = defineAsyncComponent(() => import('@/components/MkCaptcha.vue'));

View File

@ -6,16 +6,19 @@ SPDX-License-Identifier: AGPL-3.0-only
<template>
<PageWithHeader :tabs="headerTabs">
<div class="_spacer" style="--MI_SPACER-w: 700px; --MI_SPACER-min: 16px; --MI_SPACER-max: 32px;">
<FormSuspense :p="init">
<SearchMarker path="/admin/branding" :label="i18n.ts.branding" :keywords="['branding']" icon="ti ti-paint">
<div class="_gaps_m">
<SearchMarker :keywords="['icon', 'image']">
<MkInput v-model="iconUrl" type="url">
<template #prefix><i class="ti ti-link"></i></template>
<template #label>{{ i18n.ts._serverSettings.iconUrl }}</template>
<template #label><SearchLabel>{{ i18n.ts._serverSettings.iconUrl }}</SearchLabel></template>
</MkInput>
</SearchMarker>
<SearchMarker :keywords="['icon', 'image']">
<MkInput v-model="app192IconUrl" type="url">
<template #prefix><i class="ti ti-link"></i></template>
<template #label>{{ i18n.ts._serverSettings.iconUrl }} (App/192px)</template>
<template #label><SearchLabel>{{ i18n.ts._serverSettings.iconUrl }} (App/192px)</SearchLabel></template>
<template #caption>
<div>{{ i18n.tsx._serverSettings.appIconDescription({ host: instance.name ?? host }) }}</div>
<div>({{ i18n.ts._serverSettings.appIconUsageExample }})</div>
@ -23,10 +26,12 @@ SPDX-License-Identifier: AGPL-3.0-only
<div><strong>{{ i18n.tsx._serverSettings.appIconResolutionMustBe({ resolution: '192x192px' }) }}</strong></div>
</template>
</MkInput>
</SearchMarker>
<SearchMarker :keywords="['icon', 'image']">
<MkInput v-model="app512IconUrl" type="url">
<template #prefix><i class="ti ti-link"></i></template>
<template #label>{{ i18n.ts._serverSettings.iconUrl }} (App/512px)</template>
<template #label><SearchLabel>{{ i18n.ts._serverSettings.iconUrl }} (App/512px)</SearchLabel></template>
<template #caption>
<div>{{ i18n.tsx._serverSettings.appIconDescription({ host: instance.name ?? host }) }}</div>
<div>({{ i18n.ts._serverSettings.appIconUsageExample }})</div>
@ -34,61 +39,84 @@ SPDX-License-Identifier: AGPL-3.0-only
<div><strong>{{ i18n.tsx._serverSettings.appIconResolutionMustBe({ resolution: '512x512px' }) }}</strong></div>
</template>
</MkInput>
</SearchMarker>
<SearchMarker :keywords="['banner', 'image']">
<MkInput v-model="bannerUrl" type="url">
<template #prefix><i class="ti ti-link"></i></template>
<template #label>{{ i18n.ts.bannerUrl }}</template>
<template #label><SearchLabel>{{ i18n.ts.bannerUrl }}</SearchLabel></template>
</MkInput>
</SearchMarker>
<SearchMarker :keywords="['background', 'image']">
<MkInput v-model="backgroundImageUrl" type="url">
<template #prefix><i class="ti ti-link"></i></template>
<template #label>{{ i18n.ts.backgroundImageUrl }}</template>
<template #label><SearchLabel>{{ i18n.ts.backgroundImageUrl }}</SearchLabel></template>
</MkInput>
</SearchMarker>
<SearchMarker :keywords="['image']">
<MkInput v-model="notFoundImageUrl" type="url">
<template #prefix><i class="ti ti-link"></i></template>
<template #label>{{ i18n.ts.notFoundDescription }}</template>
<template #label><SearchLabel>{{ i18n.ts.notFoundDescription }}</SearchLabel></template>
</MkInput>
</SearchMarker>
<SearchMarker :keywords="['image']">
<MkInput v-model="infoImageUrl" type="url">
<template #prefix><i class="ti ti-link"></i></template>
<template #label>{{ i18n.ts.nothing }}</template>
<template #label><SearchLabel>{{ i18n.ts.nothing }}</SearchLabel></template>
</MkInput>
</SearchMarker>
<SearchMarker :keywords="['image']">
<MkInput v-model="serverErrorImageUrl" type="url">
<template #prefix><i class="ti ti-link"></i></template>
<template #label>{{ i18n.ts.somethingHappened }}</template>
<template #label><SearchLabel>{{ i18n.ts.somethingHappened }}</SearchLabel></template>
</MkInput>
</SearchMarker>
<SearchMarker :keywords="['theme', 'color']">
<MkColorInput v-model="themeColor">
<template #label>{{ i18n.ts.themeColor }}</template>
<template #label><SearchLabel>{{ i18n.ts.themeColor }}</SearchLabel></template>
</MkColorInput>
</SearchMarker>
<SearchMarker :keywords="['theme', 'default', 'light']">
<MkTextarea v-model="defaultLightTheme">
<template #label>{{ i18n.ts.instanceDefaultLightTheme }}</template>
<template #label><SearchLabel>{{ i18n.ts.instanceDefaultLightTheme }}</SearchLabel></template>
<template #caption>{{ i18n.ts.instanceDefaultThemeDescription }}</template>
</MkTextarea>
</SearchMarker>
<SearchMarker :keywords="['theme', 'default', 'dark']">
<MkTextarea v-model="defaultDarkTheme">
<template #label>{{ i18n.ts.instanceDefaultDarkTheme }}</template>
<template #label><SearchLabel>{{ i18n.ts.instanceDefaultDarkTheme }}</SearchLabel></template>
<template #caption>{{ i18n.ts.instanceDefaultThemeDescription }}</template>
</MkTextarea>
</SearchMarker>
<SearchMarker>
<MkInput v-model="repositoryUrl" type="url">
<template #prefix><i class="ti ti-link"></i></template>
<template #label>{{ i18n.ts.repositoryUrl }}</template>
<template #label><SearchLabel>{{ i18n.ts.repositoryUrl }}</SearchLabel></template>
</MkInput>
</SearchMarker>
<SearchMarker>
<MkInput v-model="feedbackUrl" type="url">
<template #prefix><i class="ti ti-link"></i></template>
<template #label>{{ i18n.ts.feedbackUrl }}</template>
<template #label><SearchLabel>{{ i18n.ts.feedbackUrl }}</SearchLabel></template>
</MkInput>
</SearchMarker>
<SearchMarker>
<MkTextarea v-model="manifestJsonOverride">
<template #label>{{ i18n.ts._serverSettings.manifestJsonOverride }}</template>
<template #label><SearchLabel>{{ i18n.ts._serverSettings.manifestJsonOverride }}</SearchLabel></template>
</MkTextarea>
</SearchMarker>
</div>
</FormSuspense>
</SearchMarker>
</div>
<template #footer>
<div :class="$style.footer">
@ -106,7 +134,6 @@ import JSON5 from 'json5';
import { host } from '@@/js/config.js';
import MkInput from '@/components/MkInput.vue';
import MkTextarea from '@/components/MkTextarea.vue';
import FormSuspense from '@/components/form/suspense.vue';
import * as os from '@/os.js';
import { misskeyApi } from '@/utility/misskey-api.js';
import { instance, fetchInstance } from '@/instance.js';
@ -115,38 +142,22 @@ import { definePage } from '@/page.js';
import MkButton from '@/components/MkButton.vue';
import MkColorInput from '@/components/MkColorInput.vue';
const iconUrl = ref<string | null>(null);
const app192IconUrl = ref<string | null>(null);
const app512IconUrl = ref<string | null>(null);
const bannerUrl = ref<string | null>(null);
const backgroundImageUrl = ref<string | null>(null);
const themeColor = ref<string | null>(null);
const defaultLightTheme = ref<string | null>(null);
const defaultDarkTheme = ref<string | null>(null);
const serverErrorImageUrl = ref<string | null>(null);
const infoImageUrl = ref<string | null>(null);
const notFoundImageUrl = ref<string | null>(null);
const repositoryUrl = ref<string | null>(null);
const feedbackUrl = ref<string | null>(null);
const manifestJsonOverride = ref<string>('{}');
const meta = await misskeyApi('admin/meta');
async function init() {
const meta = await misskeyApi('admin/meta');
iconUrl.value = meta.iconUrl;
app192IconUrl.value = meta.app192IconUrl;
app512IconUrl.value = meta.app512IconUrl;
bannerUrl.value = meta.bannerUrl;
backgroundImageUrl.value = meta.backgroundImageUrl;
themeColor.value = meta.themeColor;
defaultLightTheme.value = meta.defaultLightTheme;
defaultDarkTheme.value = meta.defaultDarkTheme;
serverErrorImageUrl.value = meta.serverErrorImageUrl;
infoImageUrl.value = meta.infoImageUrl;
notFoundImageUrl.value = meta.notFoundImageUrl;
repositoryUrl.value = meta.repositoryUrl;
feedbackUrl.value = meta.feedbackUrl;
manifestJsonOverride.value = meta.manifestJsonOverride === '' ? '{}' : JSON.stringify(JSON.parse(meta.manifestJsonOverride), null, '\t');
}
const iconUrl = ref(meta.iconUrl);
const app192IconUrl = ref(meta.app192IconUrl);
const app512IconUrl = ref(meta.app512IconUrl);
const bannerUrl = ref(meta.bannerUrl);
const backgroundImageUrl = ref(meta.backgroundImageUrl);
const themeColor = ref(meta.themeColor);
const defaultLightTheme = ref(meta.defaultLightTheme);
const defaultDarkTheme = ref(meta.defaultDarkTheme);
const serverErrorImageUrl = ref(meta.serverErrorImageUrl);
const infoImageUrl = ref(meta.infoImageUrl);
const notFoundImageUrl = ref(meta.notFoundImageUrl);
const repositoryUrl = ref(meta.repositoryUrl);
const feedbackUrl = ref(meta.feedbackUrl);
const manifestJsonOverride = ref(meta.manifestJsonOverride === '' ? '{}' : JSON.stringify(JSON.parse(meta.manifestJsonOverride), null, '\t'));
function save() {
os.apiWithDialog('admin/update-meta', {

View File

@ -6,48 +6,67 @@ SPDX-License-Identifier: AGPL-3.0-only
<template>
<PageWithHeader :tabs="headerTabs">
<div class="_spacer" style="--MI_SPACER-w: 700px; --MI_SPACER-min: 16px; --MI_SPACER-max: 32px;">
<FormSuspense :p="init">
<SearchMarker path="/admin/email-settings" :label="i18n.ts.emailServer" :keywords="['email']" icon="ti ti-mail">
<div class="_gaps_m">
<SearchMarker>
<MkSwitch v-model="enableEmail">
<template #label>{{ i18n.ts.enableEmail }} ({{ i18n.ts.recommended }})</template>
<template #caption>{{ i18n.ts.emailConfigInfo }}</template>
<template #label><SearchLabel>{{ i18n.ts.enableEmail }}</SearchLabel> ({{ i18n.ts.recommended }})</template>
<template #caption><SearchText>{{ i18n.ts.emailConfigInfo }}</SearchText></template>
</MkSwitch>
</SearchMarker>
<template v-if="enableEmail">
<SearchMarker>
<MkInput v-model="email" type="email">
<template #label>{{ i18n.ts.emailAddress }}</template>
<template #label><SearchLabel>{{ i18n.ts.emailAddress }}</SearchLabel></template>
</MkInput>
</SearchMarker>
<SearchMarker>
<FormSection>
<template #label>{{ i18n.ts.smtpConfig }}</template>
<template #label><SearchLabel>{{ i18n.ts.smtpConfig }}</SearchLabel></template>
<div class="_gaps_m">
<FormSplit :minWidth="280">
<SearchMarker>
<MkInput v-model="smtpHost">
<template #label>{{ i18n.ts.smtpHost }}</template>
<template #label><SearchLabel>{{ i18n.ts.smtpHost }}</SearchLabel></template>
</MkInput>
</SearchMarker>
<SearchMarker>
<MkInput v-model="smtpPort" type="number">
<template #label>{{ i18n.ts.smtpPort }}</template>
<template #label><SearchLabel>{{ i18n.ts.smtpPort }}</SearchLabel></template>
</MkInput>
</SearchMarker>
</FormSplit>
<FormSplit :minWidth="280">
<SearchMarker>
<MkInput v-model="smtpUser">
<template #label>{{ i18n.ts.smtpUser }}</template>
<template #label><SearchLabel>{{ i18n.ts.smtpUser }}</SearchLabel></template>
</MkInput>
</SearchMarker>
<SearchMarker>
<MkInput v-model="smtpPass" type="password">
<template #label>{{ i18n.ts.smtpPass }}</template>
<template #label><SearchLabel>{{ i18n.ts.smtpPass }}</SearchLabel></template>
</MkInput>
</SearchMarker>
</FormSplit>
<FormInfo>{{ i18n.ts.emptyToDisableSmtpAuth }}</FormInfo>
<SearchMarker>
<MkSwitch v-model="smtpSecure">
<template #label>{{ i18n.ts.smtpSecure }}</template>
<template #caption>{{ i18n.ts.smtpSecureInfo }}</template>
<template #label><SearchLabel>{{ i18n.ts.smtpSecure }}</SearchLabel></template>
<template #caption><SearchText>{{ i18n.ts.smtpSecureInfo }}</SearchText></template>
</MkSwitch>
</SearchMarker>
</div>
</FormSection>
</SearchMarker>
</template>
</div>
</FormSuspense>
</SearchMarker>
</div>
<template #footer>
<div :class="$style.footer">
@ -67,7 +86,6 @@ import { ref, computed } from 'vue';
import MkSwitch from '@/components/MkSwitch.vue';
import MkInput from '@/components/MkInput.vue';
import FormInfo from '@/components/MkInfo.vue';
import FormSuspense from '@/components/form/suspense.vue';
import FormSplit from '@/components/form/split.vue';
import FormSection from '@/components/form/section.vue';
import * as os from '@/os.js';
@ -77,24 +95,15 @@ import { i18n } from '@/i18n.js';
import { definePage } from '@/page.js';
import MkButton from '@/components/MkButton.vue';
const enableEmail = ref<boolean>(false);
const email = ref<string | null>(null);
const smtpSecure = ref<boolean>(false);
const smtpHost = ref<string>('');
const smtpPort = ref<number>(0);
const smtpUser = ref<string>('');
const smtpPass = ref<string>('');
const meta = await misskeyApi('admin/meta');
async function init() {
const meta = await misskeyApi('admin/meta');
enableEmail.value = meta.enableEmail;
email.value = meta.email;
smtpSecure.value = meta.smtpSecure;
smtpHost.value = meta.smtpHost;
smtpPort.value = meta.smtpPort;
smtpUser.value = meta.smtpUser;
smtpPass.value = meta.smtpPass;
}
const enableEmail = ref(meta.enableEmail);
const email = ref(meta.email);
const smtpSecure = ref(meta.smtpSecure);
const smtpHost = ref(meta.smtpHost);
const smtpPort = ref(meta.smtpPort);
const smtpUser = ref(meta.smtpUser);
const smtpPass = ref(meta.smtpPass);
async function testEmail() {
const { canceled, result: destination } = await os.inputText({

View File

@ -6,36 +6,49 @@ SPDX-License-Identifier: AGPL-3.0-only
<template>
<PageWithHeader :actions="headerActions" :tabs="headerTabs">
<div class="_spacer" style="--MI_SPACER-w: 700px; --MI_SPACER-min: 16px; --MI_SPACER-max: 32px;">
<FormSuspense :p="init">
<SearchMarker path="/admin/external-services" :label="i18n.ts.externalServices" :keywords="['external', 'services', 'thirdparty']" icon="ti ti-link">
<div class="_gaps_m">
<MkFolder>
<template #label>Google Analytics<span class="_beta">{{ i18n.ts.beta }}</span></template>
<SearchMarker v-slot="slotProps">
<MkFolder :defaultOpen="slotProps.isParentOfTarget">
<template #label><SearchLabel>Google Analytics</SearchLabel><span class="_beta">{{ i18n.ts.beta }}</span></template>
<div class="_gaps_m">
<SearchMarker>
<MkInput v-model="googleAnalyticsMeasurementId">
<template #prefix><i class="ti ti-key"></i></template>
<template #label>Measurement ID</template>
<template #label><SearchLabel>Measurement ID</SearchLabel></template>
</MkInput>
</SearchMarker>
<MkButton primary @click="save_googleAnalytics">Save</MkButton>
</div>
</MkFolder>
</SearchMarker>
<MkFolder>
<template #label>DeepL Translation</template>
<SearchMarker v-slot="slotProps">
<MkFolder :defaultOpen="slotProps.isParentOfTarget">
<template #label><SearchLabel>DeepL Translation</SearchLabel></template>
<div class="_gaps_m">
<SearchMarker>
<MkInput v-model="deeplAuthKey">
<template #prefix><i class="ti ti-key"></i></template>
<template #label>DeepL Auth Key</template>
<template #label><SearchLabel>Auth Key</SearchLabel></template>
</MkInput>
</SearchMarker>
<SearchMarker>
<MkSwitch v-model="deeplIsPro">
<template #label>Pro account</template>
<template #label><SearchLabel>Pro account</SearchLabel></template>
</MkSwitch>
</SearchMarker>
<MkButton primary @click="save_deepl">Save</MkButton>
</div>
</MkFolder>
</SearchMarker>
</div>
</FormSuspense>
</SearchMarker>
</div>
</PageWithHeader>
</template>
@ -45,7 +58,6 @@ import { ref, computed } from 'vue';
import MkInput from '@/components/MkInput.vue';
import MkButton from '@/components/MkButton.vue';
import MkSwitch from '@/components/MkSwitch.vue';
import FormSuspense from '@/components/form/suspense.vue';
import * as os from '@/os.js';
import { misskeyApi } from '@/utility/misskey-api.js';
import { fetchInstance } from '@/instance.js';
@ -53,17 +65,11 @@ import { i18n } from '@/i18n.js';
import { definePage } from '@/page.js';
import MkFolder from '@/components/MkFolder.vue';
const deeplAuthKey = ref<string>('');
const deeplIsPro = ref<boolean>(false);
const meta = await misskeyApi('admin/meta');
const googleAnalyticsMeasurementId = ref<string>('');
async function init() {
const meta = await misskeyApi('admin/meta');
deeplAuthKey.value = meta.deeplAuthKey ?? '';
deeplIsPro.value = meta.deeplIsPro;
googleAnalyticsMeasurementId.value = meta.googleAnalyticsMeasurementId ?? '';
}
const deeplAuthKey = ref(meta.deeplAuthKey ?? '');
const deeplIsPro = ref(meta.deeplIsPro);
const googleAnalyticsMeasurementId = ref(meta.googleAnalyticsMeasurementId ?? '');
function save_deepl() {
os.apiWithDialog('admin/update-meta', {

View File

@ -20,7 +20,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<MkInfo v-if="noEmailServer" warn>{{ i18n.ts.noEmailServerWarning }} <MkA to="/admin/email-settings" class="_link">{{ i18n.ts.configure }}</MkA></MkInfo>
</div>
<MkSuperMenu :def="menuDef" :grid="narrow"></MkSuperMenu>
<MkSuperMenu :def="menuDef" :searchIndex="searchIndex" :grid="narrow"></MkSuperMenu>
</div>
</div>
</div>
@ -44,6 +44,9 @@ import { misskeyApi } from '@/utility/misskey-api.js';
import { lookupUser, lookupUserByEmail, lookupFile } from '@/utility/admin-lookup.js';
import { definePage, provideMetadataReceiver, provideReactiveMetadata } from '@/page.js';
import { useRouter } from '@/router.js';
import { genSearchIndexes } from '@/utility/inapp-search.js';
const searchIndex = await import('search-index:admin').then(({ searchIndexes }) => genSearchIndexes(searchIndexes));
const isEmpty = (x: string | null) => x == null || x === '';
@ -324,12 +327,6 @@ const headerActions = computed(() => []);
const headerTabs = computed(() => []);
definePage(() => INFO.value);
defineExpose({
header: {
title: i18n.ts.controlPanel,
},
});
</script>
<style lang="scss" scoped>

View File

@ -6,36 +6,43 @@ SPDX-License-Identifier: AGPL-3.0-only
<template>
<PageWithHeader :tabs="headerTabs">
<div class="_spacer" style="--MI_SPACER-w: 700px; --MI_SPACER-min: 16px; --MI_SPACER-max: 32px;">
<FormSuspense :p="init">
<SearchMarker path="/admin/moderation" :label="i18n.ts.moderation" :keywords="['moderation']" icon="ti ti-shield" :inlining="['serverRules']">
<div class="_gaps_m">
<SearchMarker :keywords="['open', 'registration']">
<MkSwitch :modelValue="enableRegistration" @update:modelValue="onChange_enableRegistration">
<template #label>{{ i18n.ts._serverSettings.openRegistration }}</template>
<template #label><SearchLabel>{{ i18n.ts._serverSettings.openRegistration }}</SearchLabel></template>
<template #caption>
<div>{{ i18n.ts._serverSettings.thisSettingWillAutomaticallyOffWhenModeratorsInactive }}</div>
<div><i class="ti ti-alert-triangle" style="color: var(--MI_THEME-warn);"></i> {{ i18n.ts._serverSettings.openRegistrationWarning }}</div>
<div><SearchText>{{ i18n.ts._serverSettings.thisSettingWillAutomaticallyOffWhenModeratorsInactive }}</SearchText></div>
<div><i class="ti ti-alert-triangle" style="color: var(--MI_THEME-warn);"></i> <SearchText>{{ i18n.ts._serverSettings.openRegistrationWarning }}</SearchText></div>
</template>
</MkSwitch>
</SearchMarker>
<SearchMarker :keywords="['email', 'required', 'signup']">
<MkSwitch v-model="emailRequiredForSignup" @change="onChange_emailRequiredForSignup">
<template #label>{{ i18n.ts.emailRequiredForSignup }} ({{ i18n.ts.recommended }})</template>
<template #label><SearchLabel>{{ i18n.ts.emailRequiredForSignup }}</SearchLabel> ({{ i18n.ts.recommended }})</template>
</MkSwitch>
</SearchMarker>
<SearchMarker :keywords="['ugc', 'content', 'visibility', 'visitor', 'guest']">
<MkSelect v-model="ugcVisibilityForVisitor" @update:modelValue="onChange_ugcVisibilityForVisitor">
<template #label>{{ i18n.ts._serverSettings.userGeneratedContentsVisibilityForVisitor }}</template>
<template #label><SearchLabel>{{ i18n.ts._serverSettings.userGeneratedContentsVisibilityForVisitor }}</SearchLabel></template>
<option value="all">{{ i18n.ts._serverSettings._userGeneratedContentsVisibilityForVisitor.all }}</option>
<option value="local">{{ i18n.ts._serverSettings._userGeneratedContentsVisibilityForVisitor.localOnly }} ({{ i18n.ts.recommended }})</option>
<option value="none">{{ i18n.ts._serverSettings._userGeneratedContentsVisibilityForVisitor.none }}</option>
<template #caption>
<div>{{ i18n.ts._serverSettings.userGeneratedContentsVisibilityForVisitor_description }}</div>
<div><i class="ti ti-alert-triangle" style="color: var(--MI_THEME-warn);"></i> {{ i18n.ts._serverSettings.userGeneratedContentsVisibilityForVisitor_description2 }}</div>
<div><SearchText>{{ i18n.ts._serverSettings.userGeneratedContentsVisibilityForVisitor_description }}</SearchText></div>
<div><i class="ti ti-alert-triangle" style="color: var(--MI_THEME-warn);"></i> <SearchText>{{ i18n.ts._serverSettings.userGeneratedContentsVisibilityForVisitor_description2 }}</SearchText></div>
</template>
</MkSelect>
</SearchMarker>
<FormLink to="/admin/server-rules">{{ i18n.ts.serverRules }}</FormLink>
<XServerRules/>
<SearchMarker :keywords="['preserved', 'usernames']">
<MkFolder>
<template #icon><i class="ti ti-lock-star"></i></template>
<template #label>{{ i18n.ts.preservedUsernames }}</template>
<template #icon><SearchIcon><i class="ti ti-lock-star"></i></SearchIcon></template>
<template #label><SearchLabel>{{ i18n.ts.preservedUsernames }}</SearchLabel></template>
<div class="_gaps">
<MkTextarea v-model="preservedUsernames">
@ -44,10 +51,12 @@ SPDX-License-Identifier: AGPL-3.0-only
<MkButton primary @click="save_preservedUsernames">{{ i18n.ts.save }}</MkButton>
</div>
</MkFolder>
</SearchMarker>
<SearchMarker :keywords="['sensitive', 'words']">
<MkFolder>
<template #icon><i class="ti ti-message-exclamation"></i></template>
<template #label>{{ i18n.ts.sensitiveWords }}</template>
<template #icon><SearchIcon><i class="ti ti-message-exclamation"></i></SearchIcon></template>
<template #label><SearchLabel>{{ i18n.ts.sensitiveWords }}</SearchLabel></template>
<div class="_gaps">
<MkTextarea v-model="sensitiveWords">
@ -56,10 +65,12 @@ SPDX-License-Identifier: AGPL-3.0-only
<MkButton primary @click="save_sensitiveWords">{{ i18n.ts.save }}</MkButton>
</div>
</MkFolder>
</SearchMarker>
<SearchMarker :keywords="['prohibited', 'words']">
<MkFolder>
<template #icon><i class="ti ti-message-x"></i></template>
<template #label>{{ i18n.ts.prohibitedWords }}</template>
<template #icon><SearchIcon><i class="ti ti-message-x"></i></SearchIcon></template>
<template #label><SearchLabel>{{ i18n.ts.prohibitedWords }}</SearchLabel></template>
<div class="_gaps">
<MkTextarea v-model="prohibitedWords">
@ -68,10 +79,12 @@ SPDX-License-Identifier: AGPL-3.0-only
<MkButton primary @click="save_prohibitedWords">{{ i18n.ts.save }}</MkButton>
</div>
</MkFolder>
</SearchMarker>
<SearchMarker :keywords="['prohibited', 'name', 'user']">
<MkFolder>
<template #icon><i class="ti ti-user-x"></i></template>
<template #label>{{ i18n.ts.prohibitedWordsForNameOfUser }}</template>
<template #icon><SearchIcon><i class="ti ti-user-x"></i></SearchIcon></template>
<template #label><SearchLabel>{{ i18n.ts.prohibitedWordsForNameOfUser }}</SearchLabel></template>
<div class="_gaps">
<MkTextarea v-model="prohibitedWordsForNameOfUser">
@ -80,10 +93,12 @@ SPDX-License-Identifier: AGPL-3.0-only
<MkButton primary @click="save_prohibitedWordsForNameOfUser">{{ i18n.ts.save }}</MkButton>
</div>
</MkFolder>
</SearchMarker>
<SearchMarker :keywords="['hidden', 'tags', 'hashtags']">
<MkFolder>
<template #icon><i class="ti ti-eye-off"></i></template>
<template #label>{{ i18n.ts.hiddenTags }}</template>
<template #icon><SearchIcon><i class="ti ti-eye-off"></i></SearchIcon></template>
<template #label><SearchLabel>{{ i18n.ts.hiddenTags }}</SearchLabel></template>
<div class="_gaps">
<MkTextarea v-model="hiddenTags">
@ -92,10 +107,12 @@ SPDX-License-Identifier: AGPL-3.0-only
<MkButton primary @click="save_hiddenTags">{{ i18n.ts.save }}</MkButton>
</div>
</MkFolder>
</SearchMarker>
<SearchMarker :keywords="['silenced', 'servers', 'hosts']">
<MkFolder>
<template #icon><i class="ti ti-eye-off"></i></template>
<template #label>{{ i18n.ts.silencedInstances }}</template>
<template #icon><SearchIcon><i class="ti ti-eye-off"></i></SearchIcon></template>
<template #label><SearchLabel>{{ i18n.ts.silencedInstances }}</SearchLabel></template>
<div class="_gaps">
<MkTextarea v-model="silencedHosts">
@ -104,10 +121,12 @@ SPDX-License-Identifier: AGPL-3.0-only
<MkButton primary @click="save_silencedHosts">{{ i18n.ts.save }}</MkButton>
</div>
</MkFolder>
</SearchMarker>
<SearchMarker :keywords="['media', 'silenced', 'servers', 'hosts']">
<MkFolder>
<template #icon><i class="ti ti-eye-off"></i></template>
<template #label>{{ i18n.ts.mediaSilencedInstances }}</template>
<template #icon><SearchIcon><i class="ti ti-eye-off"></i></SearchIcon></template>
<template #label><SearchLabel>{{ i18n.ts.mediaSilencedInstances }}</SearchLabel></template>
<div class="_gaps">
<MkTextarea v-model="mediaSilencedHosts">
@ -116,10 +135,12 @@ SPDX-License-Identifier: AGPL-3.0-only
<MkButton primary @click="save_mediaSilencedHosts">{{ i18n.ts.save }}</MkButton>
</div>
</MkFolder>
</SearchMarker>
<SearchMarker :keywords="['blocked', 'servers', 'hosts']">
<MkFolder>
<template #icon><i class="ti ti-ban"></i></template>
<template #label>{{ i18n.ts.blockedInstances }}</template>
<template #icon><SearchIcon><i class="ti ti-ban"></i></SearchIcon></template>
<template #label><SearchLabel>{{ i18n.ts.blockedInstances }}</SearchLabel></template>
<div class="_gaps">
<MkTextarea v-model="blockedHosts">
@ -128,18 +149,19 @@ SPDX-License-Identifier: AGPL-3.0-only
<MkButton primary @click="save_blockedHosts">{{ i18n.ts.save }}</MkButton>
</div>
</MkFolder>
</SearchMarker>
</div>
</FormSuspense>
</SearchMarker>
</div>
</PageWithHeader>
</template>
<script lang="ts" setup>
import { ref, computed } from 'vue';
import XServerRules from './server-rules.vue';
import MkSwitch from '@/components/MkSwitch.vue';
import MkInput from '@/components/MkInput.vue';
import MkTextarea from '@/components/MkTextarea.vue';
import FormSuspense from '@/components/form/suspense.vue';
import * as os from '@/os.js';
import { misskeyApi } from '@/utility/misskey-api.js';
import { fetchInstance } from '@/instance.js';
@ -150,32 +172,19 @@ import FormLink from '@/components/form/link.vue';
import MkFolder from '@/components/MkFolder.vue';
import MkSelect from '@/components/MkSelect.vue';
const enableRegistration = ref<boolean>(false);
const emailRequiredForSignup = ref<boolean>(false);
const ugcVisibilityForVisitor = ref<string>('all');
const sensitiveWords = ref<string>('');
const prohibitedWords = ref<string>('');
const prohibitedWordsForNameOfUser = ref<string>('');
const hiddenTags = ref<string>('');
const preservedUsernames = ref<string>('');
const blockedHosts = ref<string>('');
const silencedHosts = ref<string>('');
const mediaSilencedHosts = ref<string>('');
const meta = await misskeyApi('admin/meta');
async function init() {
const meta = await misskeyApi('admin/meta');
enableRegistration.value = !meta.disableRegistration;
emailRequiredForSignup.value = meta.emailRequiredForSignup;
ugcVisibilityForVisitor.value = meta.ugcVisibilityForVisitor;
sensitiveWords.value = meta.sensitiveWords.join('\n');
prohibitedWords.value = meta.prohibitedWords.join('\n');
prohibitedWordsForNameOfUser.value = meta.prohibitedWordsForNameOfUser.join('\n');
hiddenTags.value = meta.hiddenTags.join('\n');
preservedUsernames.value = meta.preservedUsernames.join('\n');
blockedHosts.value = meta.blockedHosts.join('\n');
silencedHosts.value = meta.silencedHosts?.join('\n') ?? '';
mediaSilencedHosts.value = meta.mediaSilencedHosts.join('\n');
}
const enableRegistration = ref(!meta.disableRegistration);
const emailRequiredForSignup = ref(meta.emailRequiredForSignup);
const ugcVisibilityForVisitor = ref(meta.ugcVisibilityForVisitor);
const sensitiveWords = ref(meta.sensitiveWords.join('\n'));
const prohibitedWords = ref(meta.prohibitedWords.join('\n'));
const prohibitedWordsForNameOfUser = ref(meta.prohibitedWordsForNameOfUser.join('\n'));
const hiddenTags = ref(meta.hiddenTags.join('\n'));
const preservedUsernames = ref(meta.preservedUsernames.join('\n'));
const blockedHosts = ref(meta.blockedHosts.join('\n'));
const silencedHosts = ref(meta.silencedHosts?.join('\n') ?? '');
const mediaSilencedHosts = ref(meta.mediaSilencedHosts.join('\n'));
async function onChange_enableRegistration(value: boolean) {
if (value) {

View File

@ -6,70 +6,94 @@ SPDX-License-Identifier: AGPL-3.0-only
<template>
<PageWithHeader :tabs="headerTabs">
<div class="_spacer" style="--MI_SPACER-w: 700px; --MI_SPACER-min: 16px; --MI_SPACER-max: 32px;">
<FormSuspense :p="init">
<SearchMarker path="/admin/object-storage" :label="i18n.ts.objectStorage" :keywords="['objectStorage']" icon="ti ti-cloud">
<div class="_gaps_m">
<MkSwitch v-model="useObjectStorage">{{ i18n.ts.useObjectStorage }}</MkSwitch>
<SearchMarker>
<MkSwitch v-model="useObjectStorage"><SearchLabel>{{ i18n.ts.useObjectStorage }}</SearchLabel></MkSwitch>
</SearchMarker>
<template v-if="useObjectStorage">
<SearchMarker>
<MkInput v-model="objectStorageBaseUrl" :placeholder="'https://example.com'" type="url">
<template #label>{{ i18n.ts.objectStorageBaseUrl }}</template>
<template #caption>{{ i18n.ts.objectStorageBaseUrlDesc }}</template>
<template #label><SearchLabel>{{ i18n.ts.objectStorageBaseUrl }}</SearchLabel></template>
<template #caption><SearchText>{{ i18n.ts.objectStorageBaseUrlDesc }}</SearchText></template>
</MkInput>
</SearchMarker>
<SearchMarker>
<MkInput v-model="objectStorageBucket">
<template #label>{{ i18n.ts.objectStorageBucket }}</template>
<template #caption>{{ i18n.ts.objectStorageBucketDesc }}</template>
<template #label><SearchLabel>{{ i18n.ts.objectStorageBucket }}</SearchLabel></template>
<template #caption><SearchText>{{ i18n.ts.objectStorageBucketDesc }}</SearchText></template>
</MkInput>
</SearchMarker>
<SearchMarker>
<MkInput v-model="objectStoragePrefix">
<template #label>{{ i18n.ts.objectStoragePrefix }}</template>
<template #caption>{{ i18n.ts.objectStoragePrefixDesc }}</template>
<template #label><SearchLabel>{{ i18n.ts.objectStoragePrefix }}</SearchLabel></template>
<template #caption><SearchText>{{ i18n.ts.objectStoragePrefixDesc }}</SearchText></template>
</MkInput>
</SearchMarker>
<SearchMarker>
<MkInput v-model="objectStorageEndpoint" :placeholder="'example.com'">
<template #label>{{ i18n.ts.objectStorageEndpoint }}</template>
<template #label><SearchLabel>{{ i18n.ts.objectStorageEndpoint }}</SearchLabel></template>
<template #prefix>https://</template>
<template #caption>{{ i18n.ts.objectStorageEndpointDesc }}</template>
<template #caption><SearchText>{{ i18n.ts.objectStorageEndpointDesc }}</SearchText></template>
</MkInput>
</SearchMarker>
<SearchMarker>
<MkInput v-model="objectStorageRegion">
<template #label>{{ i18n.ts.objectStorageRegion }}</template>
<template #caption>{{ i18n.ts.objectStorageRegionDesc }}</template>
<template #label><SearchLabel>{{ i18n.ts.objectStorageRegion }}</SearchLabel></template>
<template #caption><SearchText>{{ i18n.ts.objectStorageRegionDesc }}</SearchText></template>
</MkInput>
</SearchMarker>
<FormSplit :minWidth="280">
<SearchMarker>
<MkInput v-model="objectStorageAccessKey">
<template #prefix><i class="ti ti-key"></i></template>
<template #label>Access key</template>
<template #label><SearchLabel>Access key</SearchLabel></template>
</MkInput>
</SearchMarker>
<SearchMarker>
<MkInput v-model="objectStorageSecretKey" type="password">
<template #prefix><i class="ti ti-key"></i></template>
<template #label>Secret key</template>
<template #label><SearchLabel>Secret key</SearchLabel></template>
</MkInput>
</SearchMarker>
</FormSplit>
<SearchMarker>
<MkSwitch v-model="objectStorageUseSSL">
<template #label>{{ i18n.ts.objectStorageUseSSL }}</template>
<template #caption>{{ i18n.ts.objectStorageUseSSLDesc }}</template>
<template #label><SearchLabel>{{ i18n.ts.objectStorageUseSSL }}</SearchLabel></template>
<template #caption><SearchText>{{ i18n.ts.objectStorageUseSSLDesc }}</SearchText></template>
</MkSwitch>
</SearchMarker>
<SearchMarker>
<MkSwitch v-model="objectStorageUseProxy">
<template #label>{{ i18n.ts.objectStorageUseProxy }}</template>
<template #caption>{{ i18n.ts.objectStorageUseProxyDesc }}</template>
<template #label><SearchLabel>{{ i18n.ts.objectStorageUseProxy }}</SearchLabel></template>
<template #caption><SearchText>{{ i18n.ts.objectStorageUseProxyDesc }}</SearchText></template>
</MkSwitch>
</SearchMarker>
<SearchMarker>
<MkSwitch v-model="objectStorageSetPublicRead">
<template #label>{{ i18n.ts.objectStorageSetPublicRead }}</template>
<template #label><SearchLabel>{{ i18n.ts.objectStorageSetPublicRead }}</SearchLabel></template>
</MkSwitch>
</SearchMarker>
<SearchMarker>
<MkSwitch v-model="objectStorageS3ForcePathStyle">
<template #label>s3ForcePathStyle</template>
<template #caption>{{ i18n.ts.s3ForcePathStyleDesc }}</template>
<template #label><SearchLabel>s3ForcePathStyle</SearchLabel></template>
<template #caption><SearchText>{{ i18n.ts.s3ForcePathStyleDesc }}</SearchText></template>
</MkSwitch>
</SearchMarker>
</template>
</div>
</FormSuspense>
</SearchMarker>
</div>
<template #footer>
<div :class="$style.footer">
@ -94,36 +118,21 @@ import { i18n } from '@/i18n.js';
import { definePage } from '@/page.js';
import MkButton from '@/components/MkButton.vue';
const useObjectStorage = ref<boolean>(false);
const objectStorageBaseUrl = ref<string | null>(null);
const objectStorageBucket = ref<string | null>(null);
const objectStoragePrefix = ref<string | null>(null);
const objectStorageEndpoint = ref<string | null>(null);
const objectStorageRegion = ref<string | null>(null);
const objectStoragePort = ref<number | null>(null);
const objectStorageAccessKey = ref<string | null>(null);
const objectStorageSecretKey = ref<string | null>(null);
const objectStorageUseSSL = ref<boolean>(false);
const objectStorageUseProxy = ref<boolean>(false);
const objectStorageSetPublicRead = ref<boolean>(false);
const objectStorageS3ForcePathStyle = ref<boolean>(true);
const meta = await misskeyApi('admin/meta');
async function init() {
const meta = await misskeyApi('admin/meta');
useObjectStorage.value = meta.useObjectStorage;
objectStorageBaseUrl.value = meta.objectStorageBaseUrl;
objectStorageBucket.value = meta.objectStorageBucket;
objectStoragePrefix.value = meta.objectStoragePrefix;
objectStorageEndpoint.value = meta.objectStorageEndpoint;
objectStorageRegion.value = meta.objectStorageRegion;
objectStoragePort.value = meta.objectStoragePort;
objectStorageAccessKey.value = meta.objectStorageAccessKey;
objectStorageSecretKey.value = meta.objectStorageSecretKey;
objectStorageUseSSL.value = meta.objectStorageUseSSL;
objectStorageUseProxy.value = meta.objectStorageUseProxy;
objectStorageSetPublicRead.value = meta.objectStorageSetPublicRead;
objectStorageS3ForcePathStyle.value = meta.objectStorageS3ForcePathStyle;
}
const useObjectStorage = ref(meta.useObjectStorage);
const objectStorageBaseUrl = ref(meta.objectStorageBaseUrl);
const objectStorageBucket = ref(meta.objectStorageBucket);
const objectStoragePrefix = ref(meta.objectStoragePrefix);
const objectStorageEndpoint = ref(meta.objectStorageEndpoint);
const objectStorageRegion = ref(meta.objectStorageRegion);
const objectStoragePort = ref(meta.objectStoragePort);
const objectStorageAccessKey = ref(meta.objectStorageAccessKey);
const objectStorageSecretKey = ref(meta.objectStorageSecretKey);
const objectStorageUseSSL = ref(meta.objectStorageUseSSL);
const objectStorageUseProxy = ref(meta.objectStorageUseProxy);
const objectStorageSetPublicRead = ref(meta.objectStorageSetPublicRead);
const objectStorageS3ForcePathStyle = ref(meta.objectStorageS3ForcePathStyle);
function save() {
os.apiWithDialog('admin/update-meta', {

View File

@ -6,45 +6,57 @@ SPDX-License-Identifier: AGPL-3.0-only
<template>
<PageWithHeader :actions="headerActions" :tabs="headerTabs">
<div class="_spacer" style="--MI_SPACER-w: 700px; --MI_SPACER-min: 16px; --MI_SPACER-max: 32px;">
<SearchMarker path="/admin/performance" :label="i18n.ts.performance" :keywords="['performance']" icon="ti ti-bolt">
<div class="_gaps">
<SearchMarker>
<div class="_panel" style="padding: 16px;">
<MkSwitch v-model="enableServerMachineStats" @change="onChange_enableServerMachineStats">
<template #label>{{ i18n.ts.enableServerMachineStats }}</template>
<template #label><SearchLabel>{{ i18n.ts.enableServerMachineStats }}</SearchLabel></template>
<template #caption>{{ i18n.ts.turnOffToImprovePerformance }}</template>
</MkSwitch>
</div>
</SearchMarker>
<SearchMarker>
<div class="_panel" style="padding: 16px;">
<MkSwitch v-model="enableIdenticonGeneration" @change="onChange_enableIdenticonGeneration">
<template #label>{{ i18n.ts.enableIdenticonGeneration }}</template>
<template #label><SearchLabel>{{ i18n.ts.enableIdenticonGeneration }}</SearchLabel></template>
<template #caption>{{ i18n.ts.turnOffToImprovePerformance }}</template>
</MkSwitch>
</div>
</SearchMarker>
<SearchMarker>
<div class="_panel" style="padding: 16px;">
<MkSwitch v-model="enableChartsForRemoteUser" @change="onChange_enableChartsForRemoteUser">
<template #label>{{ i18n.ts.enableChartsForRemoteUser }}</template>
<template #label><SearchLabel>{{ i18n.ts.enableChartsForRemoteUser }}</SearchLabel></template>
<template #caption>{{ i18n.ts.turnOffToImprovePerformance }}</template>
</MkSwitch>
</div>
</SearchMarker>
<SearchMarker>
<div class="_panel" style="padding: 16px;">
<MkSwitch v-model="enableStatsForFederatedInstances" @change="onChange_enableStatsForFederatedInstances">
<template #label>{{ i18n.ts.enableStatsForFederatedInstances }}</template>
<template #label><SearchLabel>{{ i18n.ts.enableStatsForFederatedInstances }}</SearchLabel></template>
<template #caption>{{ i18n.ts.turnOffToImprovePerformance }}</template>
</MkSwitch>
</div>
</SearchMarker>
<SearchMarker>
<div class="_panel" style="padding: 16px;">
<MkSwitch v-model="enableChartsForFederatedInstances" @change="onChange_enableChartsForFederatedInstances">
<template #label>{{ i18n.ts.enableChartsForFederatedInstances }}</template>
<template #label><SearchLabel>{{ i18n.ts.enableChartsForFederatedInstances }}</SearchLabel></template>
<template #caption>{{ i18n.ts.turnOffToImprovePerformance }}</template>
</MkSwitch>
</div>
</SearchMarker>
<SearchMarker>
<MkFolder :defaultOpen="true">
<template #icon><i class="ti ti-bolt"></i></template>
<template #label>Misskey® Fan-out Timeline Technology (FTT)</template>
<template #icon><SearchIcon><i class="ti ti-bolt"></i></SearchIcon></template>
<template #label><SearchLabel>Misskey® Fan-out Timeline Technology (FTT)</SearchLabel></template>
<template v-if="fttForm.savedState.enableFanoutTimeline" #suffix>Enabled</template>
<template v-else #suffix>Disabled</template>
<template v-if="fttForm.modified.value" #footer>
@ -52,42 +64,56 @@ SPDX-License-Identifier: AGPL-3.0-only
</template>
<div class="_gaps">
<SearchMarker>
<MkSwitch v-model="fttForm.state.enableFanoutTimeline">
<template #label>{{ i18n.ts.enable }}<span v-if="fttForm.modifiedStates.enableFanoutTimeline" class="_modified">{{ i18n.ts.modified }}</span></template>
<template #label><SearchLabel>{{ i18n.ts.enable }}</SearchLabel><span v-if="fttForm.modifiedStates.enableFanoutTimeline" class="_modified">{{ i18n.ts.modified }}</span></template>
<template #caption>
<div>{{ i18n.ts._serverSettings.fanoutTimelineDescription }}</div>
<div><SearchText>{{ i18n.ts._serverSettings.fanoutTimelineDescription }}</SearchText></div>
<div><MkLink target="_blank" url="https://misskey-hub.net/docs/for-admin/features/ftt/">{{ i18n.ts.details }}</MkLink></div>
</template>
</MkSwitch>
</SearchMarker>
<template v-if="fttForm.state.enableFanoutTimeline">
<SearchMarker :keywords="['db', 'database', 'fallback']">
<MkSwitch v-model="fttForm.state.enableFanoutTimelineDbFallback">
<template #label>{{ i18n.ts._serverSettings.fanoutTimelineDbFallback }}<span v-if="fttForm.modifiedStates.enableFanoutTimelineDbFallback" class="_modified">{{ i18n.ts.modified }}</span></template>
<template #caption>{{ i18n.ts._serverSettings.fanoutTimelineDbFallbackDescription }}</template>
<template #label><SearchLabel>{{ i18n.ts._serverSettings.fanoutTimelineDbFallback }}</SearchLabel><span v-if="fttForm.modifiedStates.enableFanoutTimelineDbFallback" class="_modified">{{ i18n.ts.modified }}</span></template>
<template #caption><SearchText>{{ i18n.ts._serverSettings.fanoutTimelineDbFallbackDescription }}</SearchText></template>
</MkSwitch>
</SearchMarker>
<SearchMarker>
<MkInput v-model="fttForm.state.perLocalUserUserTimelineCacheMax" type="number">
<template #label>perLocalUserUserTimelineCacheMax<span v-if="fttForm.modifiedStates.perLocalUserUserTimelineCacheMax" class="_modified">{{ i18n.ts.modified }}</span></template>
<template #label><SearchLabel>perLocalUserUserTimelineCacheMax</SearchLabel><span v-if="fttForm.modifiedStates.perLocalUserUserTimelineCacheMax" class="_modified">{{ i18n.ts.modified }}</span></template>
</MkInput>
</SearchMarker>
<SearchMarker>
<MkInput v-model="fttForm.state.perRemoteUserUserTimelineCacheMax" type="number">
<template #label>perRemoteUserUserTimelineCacheMax<span v-if="fttForm.modifiedStates.perRemoteUserUserTimelineCacheMax" class="_modified">{{ i18n.ts.modified }}</span></template>
<template #label><SearchLabel>perRemoteUserUserTimelineCacheMax</SearchLabel><span v-if="fttForm.modifiedStates.perRemoteUserUserTimelineCacheMax" class="_modified">{{ i18n.ts.modified }}</span></template>
</MkInput>
</SearchMarker>
<SearchMarker>
<MkInput v-model="fttForm.state.perUserHomeTimelineCacheMax" type="number">
<template #label>perUserHomeTimelineCacheMax<span v-if="fttForm.modifiedStates.perUserHomeTimelineCacheMax" class="_modified">{{ i18n.ts.modified }}</span></template>
<template #label><SearchLabel>perUserHomeTimelineCacheMax</SearchLabel><span v-if="fttForm.modifiedStates.perUserHomeTimelineCacheMax" class="_modified">{{ i18n.ts.modified }}</span></template>
</MkInput>
</SearchMarker>
<SearchMarker>
<MkInput v-model="fttForm.state.perUserListTimelineCacheMax" type="number">
<template #label>perUserListTimelineCacheMax<span v-if="fttForm.modifiedStates.perUserListTimelineCacheMax" class="_modified">{{ i18n.ts.modified }}</span></template>
<template #label><SearchLabel>perUserListTimelineCacheMax</SearchLabel><span v-if="fttForm.modifiedStates.perUserListTimelineCacheMax" class="_modified">{{ i18n.ts.modified }}</span></template>
</MkInput>
</SearchMarker>
</template>
</div>
</MkFolder>
</SearchMarker>
<SearchMarker>
<MkFolder :defaultOpen="true">
<template #icon><i class="ti ti-bolt"></i></template>
<template #label>Misskey® Reactions Boost Technology (RBT)<span class="_beta">{{ i18n.ts.beta }}</span></template>
<template #icon><SearchIcon><i class="ti ti-bolt"></i></SearchIcon></template>
<template #label><SearchLabel>Misskey® Reactions Boost Technology (RBT)</SearchLabel><span class="_beta">{{ i18n.ts.beta }}</span></template>
<template v-if="rbtForm.savedState.enableReactionsBuffering" #suffix>Enabled</template>
<template v-else #suffix>Disabled</template>
<template v-if="rbtForm.modified.value" #footer>
@ -95,16 +121,20 @@ SPDX-License-Identifier: AGPL-3.0-only
</template>
<div class="_gaps_m">
<SearchMarker>
<MkSwitch v-model="rbtForm.state.enableReactionsBuffering">
<template #label>{{ i18n.ts.enable }}<span v-if="rbtForm.modifiedStates.enableReactionsBuffering" class="_modified">{{ i18n.ts.modified }}</span></template>
<template #caption>{{ i18n.ts._serverSettings.reactionsBufferingDescription }}</template>
<template #label><SearchLabel>{{ i18n.ts.enable }}</SearchLabel><span v-if="rbtForm.modifiedStates.enableReactionsBuffering" class="_modified">{{ i18n.ts.modified }}</span></template>
<template #caption><SearchText>{{ i18n.ts._serverSettings.reactionsBufferingDescription }}</SearchText></template>
</MkSwitch>
</SearchMarker>
</div>
</MkFolder>
</SearchMarker>
<SearchMarker>
<MkFolder :defaultOpen="true">
<template #icon><i class="ti ti-recycle"></i></template>
<template #label>Remote Notes Cleaning ()</template>
<template #icon><SearchIcon><i class="ti ti-recycle"></i></SearchIcon></template>
<template #label><SearchLabel>Remote Notes Cleaning ()</SearchLabel></template>
<template v-if="remoteNotesCleaningForm.savedState.enableRemoteNotesCleaning" #suffix>Enabled</template>
<template v-else #suffix>Disabled</template>
<template v-if="remoteNotesCleaningForm.modified.value" #footer>
@ -113,24 +143,26 @@ SPDX-License-Identifier: AGPL-3.0-only
<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>
<template #label><SearchLabel>{{ i18n.ts.enable }}</SearchLabel><span v-if="remoteNotesCleaningForm.modifiedStates.enableRemoteNotesCleaning" class="_modified">{{ i18n.ts.modified }}</span></template>
<template #caption><SearchText>{{ i18n.ts._serverSettings.remoteNotesCleaning_description }}</SearchText></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 #label><SearchLabel>{{ i18n.ts._serverSettings.remoteNotesCleaningExpiryDaysForEachNotes }}</SearchLabel> ({{ 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 #label><SearchLabel>{{ i18n.ts._serverSettings.remoteNotesCleaningMaxProcessingDuration }}</SearchLabel> ({{ 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>
</SearchMarker>
</div>
</SearchMarker>
</div>
</PageWithHeader>
</template>
@ -243,7 +275,7 @@ const headerActions = computed(() => []);
const headerTabs = computed(() => []);
definePage(() => ({
title: i18n.ts.other,
icon: 'ti ti-adjustments',
title: i18n.ts.performance,
icon: 'ti ti-bolt',
}));
</script>

View File

@ -6,6 +6,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<template>
<PageWithHeader :actions="headerActions" :tabs="headerTabs">
<div class="_spacer" style="--MI_SPACER-w: 800px;">
<SearchMarker path="/admin/relays" :label="i18n.ts.relays" :keywords="['relays']" icon="ti ti-planet">
<div class="_gaps">
<div v-for="relay in relays" :key="relay.inbox" class="relaycxt _panel" style="padding: 16px;">
<div>{{ relay.inbox }}</div>
@ -18,6 +19,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<MkButton class="button" inline danger @click="remove(relay.inbox)"><i class="ti ti-trash"></i> {{ i18n.ts.remove }}</MkButton>
</div>
</div>
</SearchMarker>
</div>
</PageWithHeader>
</template>

View File

@ -6,12 +6,14 @@ SPDX-License-Identifier: AGPL-3.0-only
<template>
<PageWithHeader :actions="headerActions" :tabs="headerTabs">
<div class="_spacer" style="--MI_SPACER-w: 700px; --MI_SPACER-min: 16px; --MI_SPACER-max: 32px;">
<SearchMarker path="/admin/security" :label="i18n.ts.security" :keywords="['security']" icon="ti ti-lock" :inlining="['botProtection']">
<div class="_gaps_m">
<XBotProtection/>
<MkFolder>
<template #icon><i class="ti ti-eye-off"></i></template>
<template #label>{{ i18n.ts.sensitiveMediaDetection }}</template>
<SearchMarker v-slot="slotProps" :keywords="['sensitive', 'media', 'detection']">
<MkFolder :defaultOpen="slotProps.isParentOfTarget">
<template #icon><SearchIcon><i class="ti ti-eye-off"></i></SearchIcon></template>
<template #label><SearchLabel>{{ i18n.ts.sensitiveMediaDetection }}</SearchLabel></template>
<template v-if="sensitiveMediaDetectionForm.savedState.sensitiveMediaDetection === 'all'" #suffix>{{ i18n.ts.all }}</template>
<template v-else-if="sensitiveMediaDetectionForm.savedState.sensitiveMediaDetection === 'local'" #suffix>{{ i18n.ts.localOnly }}</template>
<template v-else-if="sensitiveMediaDetectionForm.savedState.sensitiveMediaDetection === 'remote'" #suffix>{{ i18n.ts.remoteOnly }}</template>
@ -21,7 +23,7 @@ SPDX-License-Identifier: AGPL-3.0-only
</template>
<div class="_gaps_m">
<span>{{ i18n.ts._sensitiveMediaDetection.description }}</span>
<div><SearchText>{{ i18n.ts._sensitiveMediaDetection.description }}</SearchText></div>
<MkRadios v-model="sensitiveMediaDetectionForm.state.sensitiveMediaDetection">
<option value="none">{{ i18n.ts.none }}</option>
@ -30,20 +32,26 @@ SPDX-License-Identifier: AGPL-3.0-only
<option value="remote">{{ i18n.ts.remoteOnly }}</option>
</MkRadios>
<SearchMarker :keywords="['sensitivity']">
<MkRange v-model="sensitiveMediaDetectionForm.state.sensitiveMediaDetectionSensitivity" :min="0" :max="4" :step="1" :textConverter="(v) => `${v + 1}`">
<template #label>{{ i18n.ts._sensitiveMediaDetection.sensitivity }}</template>
<template #caption>{{ i18n.ts._sensitiveMediaDetection.sensitivityDescription }}</template>
<template #label><SearchLabel>{{ i18n.ts._sensitiveMediaDetection.sensitivity }}</SearchLabel></template>
<template #caption><SearchText>{{ i18n.ts._sensitiveMediaDetection.sensitivityDescription }}</SearchText></template>
</MkRange>
</SearchMarker>
<SearchMarker :keywords="['video', 'analyze']">
<MkSwitch v-model="sensitiveMediaDetectionForm.state.enableSensitiveMediaDetectionForVideos">
<template #label>{{ i18n.ts._sensitiveMediaDetection.analyzeVideos }}<span class="_beta">{{ i18n.ts.beta }}</span></template>
<template #caption>{{ i18n.ts._sensitiveMediaDetection.analyzeVideosDescription }}</template>
<template #label><SearchLabel>{{ i18n.ts._sensitiveMediaDetection.analyzeVideos }}</SearchLabel><span class="_beta">{{ i18n.ts.beta }}</span></template>
<template #caption><SearchText>{{ i18n.ts._sensitiveMediaDetection.analyzeVideosDescription }}</SearchText></template>
</MkSwitch>
</SearchMarker>
<SearchMarker :keywords="['flag', 'automatically']">
<MkSwitch v-model="sensitiveMediaDetectionForm.state.setSensitiveFlagAutomatically">
<template #label>{{ i18n.ts._sensitiveMediaDetection.setSensitiveFlagAutomatically }} ({{ i18n.ts.notRecommended }})</template>
<template #caption>{{ i18n.ts._sensitiveMediaDetection.setSensitiveFlagAutomaticallyDescription }}</template>
<template #label><SearchLabel>{{ i18n.ts._sensitiveMediaDetection.setSensitiveFlagAutomatically }}</SearchLabel> ({{ i18n.ts.notRecommended }})</template>
<template #caption><SearchText>{{ i18n.ts._sensitiveMediaDetection.setSensitiveFlagAutomaticallyDescription }}</SearchText></template>
</MkSwitch>
</SearchMarker>
<!-- 現状 false positive が多すぎて実用に耐えない
<MkSwitch v-model="disallowUploadWhenPredictedAsPorn">
@ -52,9 +60,11 @@ SPDX-License-Identifier: AGPL-3.0-only
-->
</div>
</MkFolder>
</SearchMarker>
<MkFolder>
<template #label>Active Email Validation</template>
<SearchMarker v-slot="slotProps" :keywords="['email', 'validation']">
<MkFolder :defaultOpen="slotProps.isParentOfTarget">
<template #label><SearchLabel>Active Email Validation</SearchLabel></template>
<template v-if="emailValidationForm.savedState.enableActiveEmailValidation" #suffix>Enabled</template>
<template v-else #suffix>Disabled</template>
<template v-if="emailValidationForm.modified.value" #footer>
@ -62,46 +72,70 @@ SPDX-License-Identifier: AGPL-3.0-only
</template>
<div class="_gaps_m">
<span>{{ i18n.ts.activeEmailValidationDescription }}</span>
<div><SearchText>{{ i18n.ts.activeEmailValidationDescription }}</SearchText></div>
<SearchMarker>
<MkSwitch v-model="emailValidationForm.state.enableActiveEmailValidation">
<template #label>Enable</template>
<template #label><SearchLabel>Enable</SearchLabel></template>
</MkSwitch>
</SearchMarker>
<SearchMarker>
<MkSwitch v-model="emailValidationForm.state.enableVerifymailApi">
<template #label>Use Verifymail.io API</template>
<template #label><SearchLabel>Use Verifymail.io API</SearchLabel></template>
</MkSwitch>
</SearchMarker>
<SearchMarker>
<MkInput v-model="emailValidationForm.state.verifymailAuthKey">
<template #prefix><i class="ti ti-key"></i></template>
<template #label>Verifymail.io API Auth Key</template>
<template #label><SearchLabel>Verifymail.io API Auth Key</SearchLabel></template>
</MkInput>
</SearchMarker>
<SearchMarker>
<MkSwitch v-model="emailValidationForm.state.enableTruemailApi">
<template #label>Use TrueMail API</template>
<template #label><SearchLabel>Use TrueMail API</SearchLabel></template>
</MkSwitch>
</SearchMarker>
<SearchMarker>
<MkInput v-model="emailValidationForm.state.truemailInstance">
<template #prefix><i class="ti ti-key"></i></template>
<template #label>TrueMail API Instance</template>
<template #label><SearchLabel>TrueMail API Instance</SearchLabel></template>
</MkInput>
</SearchMarker>
<SearchMarker>
<MkInput v-model="emailValidationForm.state.truemailAuthKey">
<template #prefix><i class="ti ti-key"></i></template>
<template #label>TrueMail API Auth Key</template>
<template #label><SearchLabel>TrueMail API Auth Key</SearchLabel></template>
</MkInput>
</SearchMarker>
</div>
</MkFolder>
</SearchMarker>
<MkFolder>
<template #label>Banned Email Domains</template>
<SearchMarker v-slot="slotProps" :keywords="['banned', 'email', 'domains', 'blacklist']">
<MkFolder :defaultOpen="slotProps.isParentOfTarget">
<template #label><SearchLabel>Banned Email Domains</SearchLabel></template>
<template v-if="bannedEmailDomainsForm.modified.value" #footer>
<MkFormFooter :form="bannedEmailDomainsForm"/>
</template>
<div class="_gaps_m">
<SearchMarker>
<MkTextarea v-model="bannedEmailDomainsForm.state.bannedEmailDomains">
<template #label>Banned Email Domains List</template>
<template #label><SearchLabel>Banned Email Domains List</SearchLabel></template>
</MkTextarea>
</SearchMarker>
</div>
</MkFolder>
</SearchMarker>
<MkFolder>
<template #label>Log IP address</template>
<SearchMarker v-slot="slotProps" :keywords="['log', 'ipAddress']">
<MkFolder :defaultOpen="slotProps.isParentOfTarget">
<template #label><SearchLabel>Log IP address</SearchLabel></template>
<template v-if="ipLoggingForm.savedState.enableIpLogging" #suffix>Enabled</template>
<template v-else #suffix>Disabled</template>
<template v-if="ipLoggingForm.modified.value" #footer>
@ -109,12 +143,16 @@ SPDX-License-Identifier: AGPL-3.0-only
</template>
<div class="_gaps_m">
<SearchMarker>
<MkSwitch v-model="ipLoggingForm.state.enableIpLogging">
<template #label>Enable</template>
<template #label><SearchLabel>Enable</SearchLabel></template>
</MkSwitch>
</SearchMarker>
</div>
</MkFolder>
</SearchMarker>
</div>
</SearchMarker>
</div>
</PageWithHeader>
</template>

View File

@ -4,10 +4,14 @@ SPDX-License-Identifier: AGPL-3.0-only
-->
<template>
<PageWithHeader :tabs="headerTabs">
<div class="_spacer" style="--MI_SPACER-w: 700px; --MI_SPACER-min: 16px; --MI_SPACER-max: 32px;">
<SearchMarker markerId="serverRules" :keywords="['rules']">
<MkFolder>
<template #icon><SearchIcon><i class="ti ti-checkbox"></i></SearchIcon></template>
<template #label><SearchLabel>{{ i18n.ts.serverRules }}</SearchLabel></template>
<div class="_gaps_m">
<div>{{ i18n.ts._serverRules.description }}</div>
<div><SearchText>{{ i18n.ts._serverRules.description }}</SearchText></div>
<Sortable
v-model="serverRules"
class="_gaps_m"
@ -33,8 +37,8 @@ SPDX-License-Identifier: AGPL-3.0-only
<MkButton primary rounded @click="save"><i class="ti ti-check"></i> {{ i18n.ts.save }}</MkButton>
</div>
</div>
</div>
</PageWithHeader>
</MkFolder>
</SearchMarker>
</template>
<script lang="ts" setup>
@ -42,9 +46,9 @@ import { defineAsyncComponent, ref, computed } from 'vue';
import * as os from '@/os.js';
import { fetchInstance, instance } from '@/instance.js';
import { i18n } from '@/i18n.js';
import { definePage } from '@/page.js';
import MkButton from '@/components/MkButton.vue';
import MkInput from '@/components/MkInput.vue';
import MkFolder from '@/components/MkFolder.vue';
const Sortable = defineAsyncComponent(() => import('vuedraggable').then(x => x.default));
@ -60,13 +64,6 @@ const save = async () => {
const remove = (index: number): void => {
serverRules.value.splice(index, 1);
};
const headerTabs = computed(() => []);
definePage(() => ({
title: i18n.ts.serverRules,
icon: 'ti ti-checkbox',
}));
</script>
<style lang="scss" module>

View File

@ -6,176 +6,229 @@ SPDX-License-Identifier: AGPL-3.0-only
<template>
<PageWithHeader :tabs="headerTabs">
<div class="_spacer" style="--MI_SPACER-w: 700px; --MI_SPACER-min: 16px; --MI_SPACER-max: 32px;">
<SearchMarker path="/admin/settings" :label="i18n.ts.general" :keywords="['general', 'settings']" icon="ti ti-settings">
<div class="_gaps_m">
<SearchMarker v-slot="slotProps" :keywords="['information', 'meta']">
<MkFolder :defaultOpen="true">
<template #icon><i class="ti ti-info-circle"></i></template>
<template #label>{{ i18n.ts.info }}</template>
<template #icon><SearchIcon><i class="ti ti-info-circle"></i></SearchIcon></template>
<template #label><SearchLabel>{{ i18n.ts.info }}</SearchLabel></template>
<template v-if="infoForm.modified.value" #footer>
<MkFormFooter :form="infoForm"/>
</template>
<div class="_gaps">
<SearchMarker :keywords="['name']">
<MkInput v-model="infoForm.state.name">
<template #label>{{ i18n.ts.instanceName }}<span v-if="infoForm.modifiedStates.name" class="_modified">{{ i18n.ts.modified }}</span></template>
<template #label><SearchLabel>{{ i18n.ts.instanceName }}</SearchLabel><span v-if="infoForm.modifiedStates.name" class="_modified">{{ i18n.ts.modified }}</span></template>
</MkInput>
</SearchMarker>
<SearchMarker :keywords="['shortName']">
<MkInput v-model="infoForm.state.shortName">
<template #label>{{ i18n.ts._serverSettings.shortName }} ({{ i18n.ts.optional }})<span v-if="infoForm.modifiedStates.shortName" class="_modified">{{ i18n.ts.modified }}</span></template>
<template #caption>{{ i18n.ts._serverSettings.shortNameDescription }}</template>
<template #label><SearchLabel>{{ i18n.ts._serverSettings.shortName }}</SearchLabel> ({{ i18n.ts.optional }})<span v-if="infoForm.modifiedStates.shortName" class="_modified">{{ i18n.ts.modified }}</span></template>
<template #caption><SearchText>{{ i18n.ts._serverSettings.shortNameDescription }}</SearchText></template>
</MkInput>
</SearchMarker>
<SearchMarker :keywords="['description']">
<MkTextarea v-model="infoForm.state.description">
<template #label>{{ i18n.ts.instanceDescription }}<span v-if="infoForm.modifiedStates.description" class="_modified">{{ i18n.ts.modified }}</span></template>
<template #label><SearchLabel>{{ i18n.ts.instanceDescription }}</SearchLabel><span v-if="infoForm.modifiedStates.description" class="_modified">{{ i18n.ts.modified }}</span></template>
</MkTextarea>
</SearchMarker>
<FormSplit :minWidth="300">
<SearchMarker :keywords="['maintainer', 'name']">
<MkInput v-model="infoForm.state.maintainerName">
<template #label>{{ i18n.ts.maintainerName }}<span v-if="infoForm.modifiedStates.maintainerName" class="_modified">{{ i18n.ts.modified }}</span></template>
<template #label><SearchLabel>{{ i18n.ts.maintainerName }}</SearchLabel><span v-if="infoForm.modifiedStates.maintainerName" class="_modified">{{ i18n.ts.modified }}</span></template>
</MkInput>
</SearchMarker>
<SearchMarker :keywords="['maintainer', 'email', 'contact']">
<MkInput v-model="infoForm.state.maintainerEmail" type="email">
<template #label>{{ i18n.ts.maintainerEmail }}<span v-if="infoForm.modifiedStates.maintainerEmail" class="_modified">{{ i18n.ts.modified }}</span></template>
<template #label><SearchLabel>{{ i18n.ts.maintainerEmail }}</SearchLabel><span v-if="infoForm.modifiedStates.maintainerEmail" class="_modified">{{ i18n.ts.modified }}</span></template>
<template #prefix><i class="ti ti-mail"></i></template>
</MkInput>
</SearchMarker>
</FormSplit>
<SearchMarker :keywords="['tos', 'termsOfService']">
<MkInput v-model="infoForm.state.tosUrl" type="url">
<template #label>{{ i18n.ts.tosUrl }}<span v-if="infoForm.modifiedStates.tosUrl" class="_modified">{{ i18n.ts.modified }}</span></template>
<template #label><SearchLabel>{{ i18n.ts.tosUrl }}</SearchLabel><span v-if="infoForm.modifiedStates.tosUrl" class="_modified">{{ i18n.ts.modified }}</span></template>
<template #prefix><i class="ti ti-link"></i></template>
</MkInput>
</SearchMarker>
<SearchMarker :keywords="['privacyPolicy']">
<MkInput v-model="infoForm.state.privacyPolicyUrl" type="url">
<template #label>{{ i18n.ts.privacyPolicyUrl }}<span v-if="infoForm.modifiedStates.privacyPolicyUrl" class="_modified">{{ i18n.ts.modified }}</span></template>
<template #label><SearchLabel>{{ i18n.ts.privacyPolicyUrl }}</SearchLabel><span v-if="infoForm.modifiedStates.privacyPolicyUrl" class="_modified">{{ i18n.ts.modified }}</span></template>
<template #prefix><i class="ti ti-link"></i></template>
</MkInput>
</SearchMarker>
<SearchMarker :keywords="['inquiry', 'contact']">
<MkInput v-model="infoForm.state.inquiryUrl" type="url">
<template #label>{{ i18n.ts._serverSettings.inquiryUrl }}<span v-if="infoForm.modifiedStates.inquiryUrl" class="_modified">{{ i18n.ts.modified }}</span></template>
<template #caption>{{ i18n.ts._serverSettings.inquiryUrlDescription }}</template>
<template #label><SearchLabel>{{ i18n.ts._serverSettings.inquiryUrl }}</SearchLabel><span v-if="infoForm.modifiedStates.inquiryUrl" class="_modified">{{ i18n.ts.modified }}</span></template>
<template #caption><SearchText>{{ i18n.ts._serverSettings.inquiryUrlDescription }}</SearchText></template>
<template #prefix><i class="ti ti-link"></i></template>
</MkInput>
</SearchMarker>
<SearchMarker :keywords="['repository', 'url']">
<MkInput v-model="infoForm.state.repositoryUrl" type="url">
<template #label>{{ i18n.ts.repositoryUrl }}<span v-if="infoForm.modifiedStates.repositoryUrl" class="_modified">{{ i18n.ts.modified }}</span></template>
<template #caption>{{ i18n.ts.repositoryUrlDescription }}</template>
<template #label><SearchLabel>{{ i18n.ts.repositoryUrl }}</SearchLabel><span v-if="infoForm.modifiedStates.repositoryUrl" class="_modified">{{ i18n.ts.modified }}</span></template>
<template #caption><SearchText>{{ i18n.ts.repositoryUrlDescription }}</SearchText></template>
<template #prefix><i class="ti ti-link"></i></template>
</MkInput>
</SearchMarker>
<MkInfo v-if="!instance.providesTarball && !infoForm.state.repositoryUrl" warn>
{{ i18n.ts.repositoryUrlOrTarballRequired }}
</MkInfo>
<SearchMarker :keywords="['impressum', 'legalNotice']">
<MkInput v-model="infoForm.state.impressumUrl" type="url">
<template #label>{{ i18n.ts.impressumUrl }}<span v-if="infoForm.modifiedStates.impressumUrl" class="_modified">{{ i18n.ts.modified }}</span></template>
<template #caption>{{ i18n.ts.impressumDescription }}</template>
<template #label><SearchLabel>{{ i18n.ts.impressumUrl }}</SearchLabel><span v-if="infoForm.modifiedStates.impressumUrl" class="_modified">{{ i18n.ts.modified }}</span></template>
<template #caption><SearchText>{{ i18n.ts.impressumDescription }}</SearchText></template>
<template #prefix><i class="ti ti-link"></i></template>
</MkInput>
</SearchMarker>
</div>
</MkFolder>
</SearchMarker>
<MkFolder>
<template #icon><i class="ti ti-user-star"></i></template>
<template #label>{{ i18n.ts.pinnedUsers }}</template>
<SearchMarker v-slot="slotProps" :keywords="['pinned', 'users']">
<MkFolder :defaultOpen="slotProps.isParentOfTarget">
<template #icon><SearchIcon><i class="ti ti-user-star"></i></SearchIcon></template>
<template #label><SearchLabel>{{ i18n.ts.pinnedUsers }}</SearchLabel></template>
<template v-if="pinnedUsersForm.modified.value" #footer>
<MkFormFooter :form="pinnedUsersForm"/>
</template>
<MkTextarea v-model="pinnedUsersForm.state.pinnedUsers">
<template #label>{{ i18n.ts.pinnedUsers }}<span v-if="pinnedUsersForm.modifiedStates.pinnedUsers" class="_modified">{{ i18n.ts.modified }}</span></template>
<template #caption>{{ i18n.ts.pinnedUsersDescription }}</template>
<template #caption><SearchText>{{ i18n.ts.pinnedUsersDescription }}</SearchText></template>
</MkTextarea>
</MkFolder>
</SearchMarker>
<MkFolder>
<template #icon><i class="ti ti-world-cog"></i></template>
<template #label>ServiceWorker</template>
<SearchMarker v-slot="slotProps" :keywords="['serviceWorker']">
<MkFolder :defaultOpen="slotProps.isParentOfTarget">
<template #icon><SearchIcon><i class="ti ti-world-cog"></i></SearchIcon></template>
<template #label><SearchLabel>ServiceWorker</SearchLabel></template>
<template v-if="serviceWorkerForm.modified.value" #footer>
<MkFormFooter :form="serviceWorkerForm"/>
</template>
<div class="_gaps">
<SearchMarker>
<MkSwitch v-model="serviceWorkerForm.state.enableServiceWorker">
<template #label>{{ i18n.ts.enableServiceworker }}<span v-if="serviceWorkerForm.modifiedStates.enableServiceWorker" class="_modified">{{ i18n.ts.modified }}</span></template>
<template #caption>{{ i18n.ts.serviceworkerInfo }}</template>
<template #label><SearchLabel>{{ i18n.ts.enableServiceworker }}</SearchLabel><span v-if="serviceWorkerForm.modifiedStates.enableServiceWorker" class="_modified">{{ i18n.ts.modified }}</span></template>
<template #caption><SearchText>{{ i18n.ts.serviceworkerInfo }}</SearchText></template>
</MkSwitch>
</SearchMarker>
<template v-if="serviceWorkerForm.state.enableServiceWorker">
<SearchMarker>
<MkInput v-model="serviceWorkerForm.state.swPublicKey">
<template #label>Public key<span v-if="serviceWorkerForm.modifiedStates.swPublicKey" class="_modified">{{ i18n.ts.modified }}</span></template>
<template #label><SearchLabel>Public key</SearchLabel><span v-if="serviceWorkerForm.modifiedStates.swPublicKey" class="_modified">{{ i18n.ts.modified }}</span></template>
<template #prefix><i class="ti ti-key"></i></template>
</MkInput>
</SearchMarker>
<SearchMarker>
<MkInput v-model="serviceWorkerForm.state.swPrivateKey">
<template #label>Private key<span v-if="serviceWorkerForm.modifiedStates.swPrivateKey" class="_modified">{{ i18n.ts.modified }}</span></template>
<template #label><SearchLabel>Private key</SearchLabel><span v-if="serviceWorkerForm.modifiedStates.swPrivateKey" class="_modified">{{ i18n.ts.modified }}</span></template>
<template #prefix><i class="ti ti-key"></i></template>
</MkInput>
</SearchMarker>
</template>
</div>
</MkFolder>
</SearchMarker>
<MkFolder>
<template #icon><i class="ti ti-ad"></i></template>
<template #label>{{ i18n.ts._ad.adsSettings }}</template>
<SearchMarker v-slot="slotProps" :keywords="['ads']">
<MkFolder :defaultOpen="slotProps.isParentOfTarget">
<template #icon><SearchIcon><i class="ti ti-ad"></i></SearchIcon></template>
<template #label><SearchLabel>{{ i18n.ts._ad.adsSettings }}</SearchLabel></template>
<template v-if="adForm.modified.value" #footer>
<MkFormFooter :form="adForm"/>
</template>
<div class="_gaps">
<div class="_gaps_s">
<SearchMarker>
<MkInput v-model="adForm.state.notesPerOneAd" :min="0" type="number">
<template #label>{{ i18n.ts._ad.notesPerOneAd }}<span v-if="adForm.modifiedStates.notesPerOneAd" class="_modified">{{ i18n.ts.modified }}</span></template>
<template #label><SearchLabel>{{ i18n.ts._ad.notesPerOneAd }}</SearchLabel><span v-if="adForm.modifiedStates.notesPerOneAd" class="_modified">{{ i18n.ts.modified }}</span></template>
<template #caption>{{ i18n.ts._ad.setZeroToDisable }}</template>
</MkInput>
</SearchMarker>
<MkInfo v-if="adForm.state.notesPerOneAd > 0 && adForm.state.notesPerOneAd < 20" :warn="true">
{{ i18n.ts._ad.adsTooClose }}
</MkInfo>
</div>
</div>
</MkFolder>
</SearchMarker>
<MkFolder>
<template #icon><i class="ti ti-world-search"></i></template>
<template #label>{{ i18n.ts._urlPreviewSetting.title }}</template>
<SearchMarker v-slot="slotProps" :keywords="['url', 'preview']">
<MkFolder :defaultOpen="slotProps.isParentOfTarget">
<template #icon><SearchIcon><i class="ti ti-world-search"></i></SearchIcon></template>
<template #label><SearchLabel>{{ i18n.ts._urlPreviewSetting.title }}</SearchLabel></template>
<template v-if="urlPreviewForm.modified.value" #footer>
<MkFormFooter :form="urlPreviewForm"/>
</template>
<div class="_gaps">
<SearchMarker>
<MkSwitch v-model="urlPreviewForm.state.urlPreviewEnabled">
<template #label>{{ i18n.ts._urlPreviewSetting.enable }}<span v-if="urlPreviewForm.modifiedStates.urlPreviewEnabled" class="_modified">{{ i18n.ts.modified }}</span></template>
<template #label><SearchLabel>{{ i18n.ts._urlPreviewSetting.enable }}</SearchLabel><span v-if="urlPreviewForm.modifiedStates.urlPreviewEnabled" class="_modified">{{ i18n.ts.modified }}</span></template>
</MkSwitch>
</SearchMarker>
<template v-if="urlPreviewForm.state.urlPreviewEnabled">
<SearchMarker :keywords="['allow', 'redirect']">
<MkSwitch v-model="urlPreviewForm.state.urlPreviewAllowRedirect">
<template #label>{{ i18n.ts._urlPreviewSetting.allowRedirect }}<span v-if="urlPreviewForm.modifiedStates.urlPreviewAllowRedirect" class="_modified">{{ i18n.ts.modified }}</span></template>
<template #label><SearchLabel>{{ i18n.ts._urlPreviewSetting.allowRedirect }}</SearchLabel><span v-if="urlPreviewForm.modifiedStates.urlPreviewAllowRedirect" class="_modified">{{ i18n.ts.modified }}</span></template>
<template #caption>{{ i18n.ts._urlPreviewSetting.allowRedirectDescription }}</template>
</MkSwitch>
</SearchMarker>
<SearchMarker :keywords="['contentLength']">
<MkSwitch v-model="urlPreviewForm.state.urlPreviewRequireContentLength">
<template #label>{{ i18n.ts._urlPreviewSetting.requireContentLength }}<span v-if="urlPreviewForm.modifiedStates.urlPreviewRequireContentLength" class="_modified">{{ i18n.ts.modified }}</span></template>
<template #label><SearchLabel>{{ i18n.ts._urlPreviewSetting.requireContentLength }}</SearchLabel><span v-if="urlPreviewForm.modifiedStates.urlPreviewRequireContentLength" class="_modified">{{ i18n.ts.modified }}</span></template>
<template #caption>{{ i18n.ts._urlPreviewSetting.requireContentLengthDescription }}</template>
</MkSwitch>
</SearchMarker>
<SearchMarker :keywords="['contentLength']">
<MkInput v-model="urlPreviewForm.state.urlPreviewMaximumContentLength" type="number">
<template #label>{{ i18n.ts._urlPreviewSetting.maximumContentLength }}<span v-if="urlPreviewForm.modifiedStates.urlPreviewMaximumContentLength" class="_modified">{{ i18n.ts.modified }}</span></template>
<template #label><SearchLabel>{{ i18n.ts._urlPreviewSetting.maximumContentLength }}</SearchLabel><span v-if="urlPreviewForm.modifiedStates.urlPreviewMaximumContentLength" class="_modified">{{ i18n.ts.modified }}</span></template>
<template #caption>{{ i18n.ts._urlPreviewSetting.maximumContentLengthDescription }}</template>
</MkInput>
</SearchMarker>
<SearchMarker :keywords="['timeout']">
<MkInput v-model="urlPreviewForm.state.urlPreviewTimeout" type="number">
<template #label>{{ i18n.ts._urlPreviewSetting.timeout }}<span v-if="urlPreviewForm.modifiedStates.urlPreviewTimeout" class="_modified">{{ i18n.ts.modified }}</span></template>
<template #label><SearchLabel>{{ i18n.ts._urlPreviewSetting.timeout }}</SearchLabel><span v-if="urlPreviewForm.modifiedStates.urlPreviewTimeout" class="_modified">{{ i18n.ts.modified }}</span></template>
<template #caption>{{ i18n.ts._urlPreviewSetting.timeoutDescription }}</template>
</MkInput>
</SearchMarker>
<SearchMarker :keywords="['userAgent']">
<MkInput v-model="urlPreviewForm.state.urlPreviewUserAgent" type="text">
<template #label>{{ i18n.ts._urlPreviewSetting.userAgent }}<span v-if="urlPreviewForm.modifiedStates.urlPreviewUserAgent" class="_modified">{{ i18n.ts.modified }}</span></template>
<template #label><SearchLabel>{{ i18n.ts._urlPreviewSetting.userAgent }}</SearchLabel><span v-if="urlPreviewForm.modifiedStates.urlPreviewUserAgent" class="_modified">{{ i18n.ts.modified }}</span></template>
<template #caption>{{ i18n.ts._urlPreviewSetting.userAgentDescription }}</template>
</MkInput>
</SearchMarker>
<div>
<SearchMarker :keywords="['proxy']">
<MkInput v-model="urlPreviewForm.state.urlPreviewSummaryProxyUrl" type="text">
<template #label>{{ i18n.ts._urlPreviewSetting.summaryProxy }}<span v-if="urlPreviewForm.modifiedStates.urlPreviewSummaryProxyUrl" class="_modified">{{ i18n.ts.modified }}</span></template>
<template #label><SearchLabel>{{ i18n.ts._urlPreviewSetting.summaryProxy }}</SearchLabel><span v-if="urlPreviewForm.modifiedStates.urlPreviewSummaryProxyUrl" class="_modified">{{ i18n.ts.modified }}</span></template>
<template #caption>[{{ i18n.ts.notUsePleaseLeaveBlank }}] {{ i18n.ts._urlPreviewSetting.summaryProxyDescription }}</template>
</MkInput>
</SearchMarker>
<div :class="$style.subCaption">
{{ i18n.ts._urlPreviewSetting.summaryProxyDescription2 }}
@ -190,10 +243,12 @@ SPDX-License-Identifier: AGPL-3.0-only
</template>
</div>
</MkFolder>
</SearchMarker>
<MkFolder>
<template #icon><i class="ti ti-planet"></i></template>
<template #label>{{ i18n.ts.federation }}</template>
<SearchMarker v-slot="slotProps" :keywords="['federation']">
<MkFolder :defaultOpen="slotProps.isParentOfTarget">
<template #icon><SearchIcon><i class="ti ti-planet"></i></SearchIcon></template>
<template #label><SearchLabel>{{ i18n.ts.federation }}</SearchLabel></template>
<template v-if="federationForm.savedState.federation === 'all'" #suffix>{{ i18n.ts.all }}</template>
<template v-else-if="federationForm.savedState.federation === 'specified'" #suffix>{{ i18n.ts.specifyHost }}</template>
<template v-else-if="federationForm.savedState.federation === 'none'" #suffix>{{ i18n.ts.none }}</template>
@ -202,18 +257,23 @@ SPDX-License-Identifier: AGPL-3.0-only
</template>
<div class="_gaps">
<SearchMarker>
<MkRadios v-model="federationForm.state.federation">
<template #label>{{ i18n.ts.behavior }}<span v-if="federationForm.modifiedStates.federation" class="_modified">{{ i18n.ts.modified }}</span></template>
<template #label><SearchLabel>{{ i18n.ts.behavior }}</SearchLabel><span v-if="federationForm.modifiedStates.federation" class="_modified">{{ i18n.ts.modified }}</span></template>
<option value="all">{{ i18n.ts.all }}</option>
<option value="specified">{{ i18n.ts.specifyHost }}</option>
<option value="none">{{ i18n.ts.none }}</option>
</MkRadios>
</SearchMarker>
<SearchMarker :keywords="['hosts']">
<MkTextarea v-if="federationForm.state.federation === 'specified'" v-model="federationForm.state.federationHosts">
<template #label>{{ i18n.ts.federationAllowedHosts }}<span v-if="federationForm.modifiedStates.federationHosts" class="_modified">{{ i18n.ts.modified }}</span></template>
<template #label><SearchLabel>{{ i18n.ts.federationAllowedHosts }}</SearchLabel><span v-if="federationForm.modifiedStates.federationHosts" class="_modified">{{ i18n.ts.modified }}</span></template>
<template #caption>{{ i18n.ts.federationAllowedHostsDescription }}</template>
</MkTextarea>
</SearchMarker>
<SearchMarker :keywords="['suspended', 'software']">
<MkFolder>
<template #icon><i class="ti ti-list"></i></template>
<template #label><SearchLabel>{{ i18n.ts._serverSettings.deliverSuspendedSoftware }}</SearchLabel></template>
@ -238,42 +298,55 @@ SPDX-License-Identifier: AGPL-3.0-only
</div>
</div>
</MkFolder>
</SearchMarker>
<SearchMarker :keywords="['sign', 'get']">
<MkSwitch v-model="federationForm.state.signToActivityPubGet">
<template #label>{{ i18n.ts._serverSettings.signToActivityPubGet }}<span v-if="federationForm.modifiedStates.signToActivityPubGet" class="_modified">{{ i18n.ts.modified }}</span></template>
<template #caption>{{ i18n.ts._serverSettings.signToActivityPubGet_description }}</template>
<template #label><SearchLabel>{{ i18n.ts._serverSettings.signToActivityPubGet }}</SearchLabel><span v-if="federationForm.modifiedStates.signToActivityPubGet" class="_modified">{{ i18n.ts.modified }}</span></template>
<template #caption><SearchText>{{ i18n.ts._serverSettings.signToActivityPubGet_description }}</SearchText></template>
</MkSwitch>
</SearchMarker>
<SearchMarker :keywords="['proxy', 'remote', 'files']">
<MkSwitch v-model="federationForm.state.proxyRemoteFiles">
<template #label>{{ i18n.ts._serverSettings.proxyRemoteFiles }}<span v-if="federationForm.modifiedStates.proxyRemoteFiles" class="_modified">{{ i18n.ts.modified }}</span></template>
<template #caption>{{ i18n.ts._serverSettings.proxyRemoteFiles_description }}</template>
<template #label><SearchLabel>{{ i18n.ts._serverSettings.proxyRemoteFiles }}</SearchLabel><span v-if="federationForm.modifiedStates.proxyRemoteFiles" class="_modified">{{ i18n.ts.modified }}</span></template>
<template #caption><SearchText>{{ i18n.ts._serverSettings.proxyRemoteFiles_description }}</SearchText></template>
</MkSwitch>
</SearchMarker>
<SearchMarker :keywords="['allow', 'external', 'redirect']">
<MkSwitch v-model="federationForm.state.allowExternalApRedirect">
<template #label>{{ i18n.ts._serverSettings.allowExternalApRedirect }}<span v-if="federationForm.modifiedStates.allowExternalApRedirect" class="_modified">{{ i18n.ts.modified }}</span></template>
<template #label><SearchLabel>{{ i18n.ts._serverSettings.allowExternalApRedirect }}</SearchLabel><span v-if="federationForm.modifiedStates.allowExternalApRedirect" class="_modified">{{ i18n.ts.modified }}</span></template>
<template #caption>
<div>{{ i18n.ts._serverSettings.allowExternalApRedirect_description }}</div>
<div><SearchText>{{ i18n.ts._serverSettings.allowExternalApRedirect_description }}</SearchText></div>
<div>{{ i18n.ts.needToRestartServerToApply }}</div>
</template>
</MkSwitch>
</SearchMarker>
<SearchMarker :keywords="['cache', 'remote', 'files']">
<MkSwitch v-model="federationForm.state.cacheRemoteFiles">
<template #label>{{ i18n.ts.cacheRemoteFiles }}<span v-if="federationForm.modifiedStates.cacheRemoteFiles" class="_modified">{{ i18n.ts.modified }}</span></template>
<template #caption>{{ i18n.ts.cacheRemoteFilesDescription }}{{ i18n.ts.youCanCleanRemoteFilesCache }}</template>
<template #label><SearchLabel>{{ i18n.ts.cacheRemoteFiles }}</SearchLabel><span v-if="federationForm.modifiedStates.cacheRemoteFiles" class="_modified">{{ i18n.ts.modified }}</span></template>
<template #caption><SearchText>{{ i18n.ts.cacheRemoteFilesDescription }}</SearchText>{{ i18n.ts.youCanCleanRemoteFilesCache }}</template>
</MkSwitch>
</SearchMarker>
<template v-if="federationForm.state.cacheRemoteFiles">
<SearchMarker :keywords="['cache', 'remote', 'sensitive', 'files']">
<MkSwitch v-model="federationForm.state.cacheRemoteSensitiveFiles">
<template #label>{{ i18n.ts.cacheRemoteSensitiveFiles }}<span v-if="federationForm.modifiedStates.cacheRemoteSensitiveFiles" class="_modified">{{ i18n.ts.modified }}</span></template>
<template #caption>{{ i18n.ts.cacheRemoteSensitiveFilesDescription }}</template>
<template #label><SearchLabel>{{ i18n.ts.cacheRemoteSensitiveFiles }}</SearchLabel><span v-if="federationForm.modifiedStates.cacheRemoteSensitiveFiles" class="_modified">{{ i18n.ts.modified }}</span></template>
<template #caption><SearchText>{{ i18n.ts.cacheRemoteSensitiveFilesDescription }}</SearchText></template>
</MkSwitch>
</SearchMarker>
</template>
</div>
</MkFolder>
</SearchMarker>
<MkFolder>
<template #icon><i class="ti ti-ghost"></i></template>
<template #label>{{ i18n.ts.proxyAccount }}</template>
<SearchMarker v-slot="slotProps" :keywords="['proxy', 'account']">
<MkFolder :defaultOpen="slotProps.isParentOfTarget">
<template #icon><SearchIcon><i class="ti ti-ghost"></i></SearchIcon></template>
<template #label><SearchLabel>{{ i18n.ts.proxyAccount }}</SearchLabel></template>
<template v-if="proxyAccountForm.modified.value" #footer>
<MkFormFooter :form="proxyAccountForm"/>
</template>
@ -281,17 +354,21 @@ SPDX-License-Identifier: AGPL-3.0-only
<div class="_gaps">
<MkInfo>{{ i18n.ts.proxyAccountDescription }}</MkInfo>
<SearchMarker :keywords="['description']">
<MkTextarea v-model="proxyAccountForm.state.description" :max="500" tall mfmAutocomplete :mfmPreview="true">
<template #label>{{ i18n.ts._profile.description }}</template>
<template #label><SearchLabel>{{ i18n.ts._profile.description }}</SearchLabel></template>
<template #caption>{{ i18n.ts._profile.youCanIncludeHashtags }}</template>
</MkTextarea>
</SearchMarker>
</div>
</MkFolder>
</SearchMarker>
<MkButton primary @click="openSetupWizard">
Open setup wizard
</MkButton>
</div>
</SearchMarker>
</div>
</PageWithHeader>
</template>

View File

@ -6,10 +6,13 @@ SPDX-License-Identifier: AGPL-3.0-only
<template>
<PageWithHeader :actions="headerActions" :tabs="headerTabs">
<div class="_spacer" style="--MI_SPACER-w: 900px;">
<SearchMarker path="/admin/system-webhook" label="SystemWebhook" :keywords="['webhook']" icon="ti ti-webhook">
<div class="_gaps_m">
<SearchMarker>
<MkButton primary @click="onCreateWebhookClicked">
<i class="ti ti-plus"></i> {{ i18n.ts._webhookSettings.createWebhook }}
<i class="ti ti-plus"></i> <SearchLabel>{{ i18n.ts._webhookSettings.createWebhook }}</SearchLabel>
</MkButton>
</SearchMarker>
<FormSection>
<div class="_gaps">
@ -17,6 +20,7 @@ SPDX-License-Identifier: AGPL-3.0-only
</div>
</FormSection>
</div>
</SearchMarker>
</div>
</PageWithHeader>
</template>

View File

@ -20,7 +20,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<MkFolder :defaultOpen="true">
<template #icon><i class="ti ti-shield-lock"></i></template>
<template #label><SearchLabel>{{ i18n.ts.totp }}</SearchLabel></template>
<template #caption><SearchKeyword>{{ i18n.ts.totpDescription }}</SearchKeyword></template>
<template #caption><SearchText>{{ i18n.ts.totpDescription }}</SearchText></template>
<template #suffix><i v-if="$i.twoFactorEnabled" class="ti ti-check" style="color: var(--MI_THEME-success)"></i></template>
<div v-if="$i.twoFactorEnabled" class="_gaps_s">
@ -74,7 +74,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<SearchMarker :keywords="['password', 'less', 'key', 'passkey', 'login', 'signin']">
<MkSwitch :disabled="!$i.twoFactorEnabled || $i.securityKeysList.length === 0" :modelValue="usePasswordLessLogin" @update:modelValue="v => updatePasswordLessLogin(v)">
<template #label><SearchLabel>{{ i18n.ts.passwordLessLogin }}</SearchLabel></template>
<template #caption><SearchKeyword>{{ i18n.ts.passwordLessLoginDescription }}</SearchKeyword></template>
<template #caption><SearchText>{{ i18n.ts.passwordLessLoginDescription }}</SearchText></template>
</MkSwitch>
</SearchMarker>
</div>

View File

@ -7,7 +7,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<SearchMarker path="/settings/account-data" :label="i18n.ts._settings.accountData" :keywords="['import', 'export', 'data', 'archive']" icon="ti ti-package">
<div class="_gaps_m">
<MkFeatureBanner icon="/client-assets/package_3d.png" color="#ff9100">
<SearchKeyword>{{ i18n.ts._settings.accountDataBanner }}</SearchKeyword>
<SearchText>{{ i18n.ts._settings.accountDataBanner }}</SearchText>
</MkFeatureBanner>
<div class="_gaps_s">

View File

@ -7,7 +7,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<SearchMarker path="/settings/connect" :label="i18n.ts._settings.serviceConnection" :keywords="['app', 'service', 'connect', 'webhook', 'api', 'token']" icon="ti ti-link">
<div class="_gaps_m">
<MkFeatureBanner icon="/client-assets/link_3d.png" color="#ff0088">
<SearchKeyword>{{ i18n.ts._settings.serviceConnectionBanner }}</SearchKeyword>
<SearchText>{{ i18n.ts._settings.serviceConnectionBanner }}</SearchText>
</MkFeatureBanner>
<SearchMarker :keywords="['api', 'app', 'token', 'accessToken']">

View File

@ -7,7 +7,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<SearchMarker path="/settings/drive" :label="i18n.ts.drive" :keywords="['drive']" icon="ti ti-cloud">
<div class="_gaps_m">
<MkFeatureBanner icon="/client-assets/cloud_3d.png" color="#0059ff">
<SearchKeyword>{{ i18n.ts._settings.driveBanner }}</SearchKeyword>
<SearchText>{{ i18n.ts._settings.driveBanner }}</SearchText>
</MkFeatureBanner>
<SearchMarker :keywords="['capacity', 'usage']">
@ -60,7 +60,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<MkPreferenceContainer k="keepOriginalFilename">
<MkSwitch v-model="keepOriginalFilename">
<template #label><SearchLabel>{{ i18n.ts.keepOriginalFilename }}</SearchLabel></template>
<template #caption><SearchKeyword>{{ i18n.ts.keepOriginalFilenameDescription }}</SearchKeyword></template>
<template #caption><SearchText>{{ i18n.ts.keepOriginalFilenameDescription }}</SearchText></template>
</MkSwitch>
</MkPreferenceContainer>
</SearchMarker>
@ -74,7 +74,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<SearchMarker :keywords="['auto', 'nsfw', 'sensitive', 'media', 'file']">
<MkSwitch v-model="autoSensitive" @update:modelValue="saveProfile()">
<template #label><SearchLabel>{{ i18n.ts.enableAutoSensitive }}</SearchLabel><span class="_beta">{{ i18n.ts.beta }}</span></template>
<template #caption><SearchKeyword>{{ i18n.ts.enableAutoSensitiveDescription }}</SearchKeyword></template>
<template #caption><SearchText>{{ i18n.ts.enableAutoSensitiveDescription }}</SearchText></template>
</MkSwitch>
</SearchMarker>
</div>

View File

@ -15,7 +15,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<div>{{ i18n.ts._preferencesBackup.autoPreferencesBackupIsNotEnabledForThisDevice }}</div>
<div><button class="_textButton" @click="enableAutoBackup">{{ i18n.ts.enable }}</button> | <button class="_textButton" @click="skipAutoBackup">{{ i18n.ts.skip }}</button></div>
</MkInfo>
<MkSuperMenu :def="menuDef" :grid="narrow" :searchIndex="SETTING_INDEX"></MkSuperMenu>
<MkSuperMenu :def="menuDef" :grid="narrow" :searchIndex="searchIndex"></MkSuperMenu>
</div>
</div>
<div v-if="!(narrow && currentPage?.route.name == null)" class="main">
@ -42,12 +42,12 @@ import { instance } from '@/instance.js';
import { definePage, provideMetadataReceiver, provideReactiveMetadata } from '@/page.js';
import * as os from '@/os.js';
import { useRouter } from '@/router.js';
import { searchIndexes } from '@/utility/settings-search-index.js';
import { enableAutoBackup, getPreferencesProfileMenu } from '@/preferences/utility.js';
import { store } from '@/store.js';
import { signout } from '@/signout.js';
import { genSearchIndexes } from '@/utility/inapp-search.js';
const SETTING_INDEX = searchIndexes; // TODO: lazy load
const searchIndex = await import('search-index:settings').then(({ searchIndexes }) => genSearchIndexes(searchIndexes));
const indexInfo = {
title: i18n.ts.settings,

View File

@ -7,7 +7,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<SearchMarker path="/settings/mute-block" :label="i18n.ts.muteAndBlock" icon="ti ti-ban" :keywords="['mute', 'block']">
<div class="_gaps_m">
<MkFeatureBanner icon="/client-assets/prohibited_3d.png" color="#ff2600">
<SearchKeyword>{{ i18n.ts._settings.muteAndBlockBanner }}</SearchKeyword>
<SearchText>{{ i18n.ts._settings.muteAndBlockBanner }}</SearchText>
</MkFeatureBanner>
<div class="_gaps_s">

View File

@ -7,7 +7,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<SearchMarker path="/settings/notifications" :label="i18n.ts.notifications" :keywords="['notifications']" icon="ti ti-bell">
<div class="_gaps_m">
<MkFeatureBanner icon="/client-assets/bell_3d.png" color="#ffff00">
<SearchKeyword>{{ i18n.ts._settings.notificationsBanner }}</SearchKeyword>
<SearchText>{{ i18n.ts._settings.notificationsBanner }}</SearchText>
</MkFeatureBanner>
<FormSection first>

View File

@ -75,7 +75,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<div class="_gaps_m">
<FormInfo warn>{{ i18n.ts._accountDelete.mayTakeTime }}</FormInfo>
<FormInfo>{{ i18n.ts._accountDelete.sendEmail }}</FormInfo>
<MkButton v-if="!$i.isDeleted" danger @click="deleteAccount"><SearchKeyword>{{ i18n.ts._accountDelete.requestAccountDelete }}</SearchKeyword></MkButton>
<MkButton v-if="!$i.isDeleted" danger @click="deleteAccount"><SearchText>{{ i18n.ts._accountDelete.requestAccountDelete }}</SearchText></MkButton>
<MkButton v-else disabled>{{ i18n.ts._accountDelete.inProgress }}</MkButton>
</div>
</MkFolder>

View File

@ -7,7 +7,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<SearchMarker path="/settings/plugin" :label="i18n.ts.plugins" :keywords="['plugin', 'addon', 'extension']" icon="ti ti-plug">
<div class="_gaps_m">
<MkFeatureBanner icon="/client-assets/electric_plug_3d.png" color="#ffbb00">
<SearchKeyword>{{ i18n.ts._settings.pluginBanner }}</SearchKeyword>
<SearchText>{{ i18n.ts._settings.pluginBanner }}</SearchText>
</MkFeatureBanner>
<MkInfo v-if="isSafeMode" warn>{{ i18n.ts.pluginsAreDisabledBecauseSafeMode }}</MkInfo>

View File

@ -7,7 +7,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<SearchMarker path="/settings/preferences" :label="i18n.ts.preferences" :keywords="['general', 'preferences']" icon="ti ti-adjustments">
<div class="_gaps_m">
<MkFeatureBanner icon="/client-assets/gear_3d.png" color="#00ff9d">
<SearchKeyword>{{ i18n.ts._settings.preferencesBanner }}</SearchKeyword>
<SearchText>{{ i18n.ts._settings.preferencesBanner }}</SearchText>
</MkFeatureBanner>
<div class="_gaps_s">
@ -44,7 +44,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<SearchMarker :keywords="['realtimemode']">
<MkSwitch v-model="realtimeMode">
<template #label><i class="ti ti-bolt"></i> <SearchLabel>{{ i18n.ts.realtimeMode }}</SearchLabel></template>
<template #caption><SearchKeyword>{{ i18n.ts._settings.realtimeMode_description }}</SearchKeyword></template>
<template #caption><SearchText>{{ i18n.ts._settings.realtimeMode_description }}</SearchText></template>
</MkSwitch>
</SearchMarker>
@ -53,7 +53,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<MkPreferenceContainer k="pollingInterval">
<MkRange v-model="pollingInterval" :min="1" :max="3" :step="1" easing :showTicks="true" :textConverter="(v) => v === 1 ? i18n.ts.low : v === 2 ? i18n.ts.middle : v === 3 ? i18n.ts.high : ''">
<template #label><SearchLabel>{{ i18n.ts._settings.contentsUpdateFrequency }}</SearchLabel></template>
<template #caption><SearchKeyword>{{ i18n.ts._settings.contentsUpdateFrequency_description }}</SearchKeyword><br><SearchKeyword>{{ i18n.ts._settings.contentsUpdateFrequency_description2 }}</SearchKeyword></template>
<template #caption><SearchText>{{ i18n.ts._settings.contentsUpdateFrequency_description }}</SearchText><br><SearchText>{{ i18n.ts._settings.contentsUpdateFrequency_description2 }}</SearchText></template>
<template #prefix><i class="ti ti-player-play"></i></template>
<template #suffix><i class="ti ti-player-track-next"></i></template>
</MkRange>
@ -165,7 +165,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<MkPreferenceContainer k="collapseRenotes">
<MkSwitch v-model="collapseRenotes">
<template #label><SearchLabel>{{ i18n.ts.collapseRenotes }}</SearchLabel></template>
<template #caption><SearchKeyword>{{ i18n.ts.collapseRenotesDescription }}</SearchKeyword></template>
<template #caption><SearchText>{{ i18n.ts.collapseRenotesDescription }}</SearchText></template>
</MkSwitch>
</MkPreferenceContainer>
</SearchMarker>
@ -449,7 +449,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<div class="_gaps_m">
<MkFeatureBanner icon="/client-assets/mens_room_3d.png" color="#0011ff">
<SearchKeyword>{{ i18n.ts._settings.accessibilityBanner }}</SearchKeyword>
<SearchText>{{ i18n.ts._settings.accessibilityBanner }}</SearchText>
</MkFeatureBanner>
<div class="_gaps_s">
@ -477,6 +477,14 @@ SPDX-License-Identifier: AGPL-3.0-only
</MkPreferenceContainer>
</SearchMarker>
<SearchMarker :keywords="['tabs', 'tabbar', 'bottom', 'under']">
<MkPreferenceContainer k="showPageTabBarBottom">
<MkSwitch v-model="showPageTabBarBottom">
<template #label><SearchLabel>{{ i18n.ts._settings.showPageTabBarBottom }}</SearchLabel></template>
</MkSwitch>
</MkPreferenceContainer>
</SearchMarker>
<SearchMarker :keywords="['swipe', 'horizontal', 'tab']">
<MkPreferenceContainer k="enableHorizontalSwipe">
<MkSwitch v-model="enableHorizontalSwipe">
@ -489,7 +497,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<MkPreferenceContainer k="enablePullToRefresh">
<MkSwitch v-model="enablePullToRefresh">
<template #label><SearchLabel>{{ i18n.ts._settings.enablePullToRefresh }}</SearchLabel></template>
<template #caption><SearchKeyword>{{ i18n.ts._settings.enablePullToRefresh_description }}</SearchKeyword></template>
<template #caption><SearchText>{{ i18n.ts._settings.enablePullToRefresh_description }}</SearchText></template>
</MkSwitch>
</MkPreferenceContainer>
</SearchMarker>
@ -571,7 +579,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<MkPreferenceContainer k="animation">
<MkSwitch :modelValue="!reduceAnimation" @update:modelValue="v => reduceAnimation = !v">
<template #label><SearchLabel>{{ i18n.ts._settings.uiAnimations }}</SearchLabel></template>
<template #caption><SearchKeyword>{{ i18n.ts.turnOffToImprovePerformance }}</SearchKeyword></template>
<template #caption><SearchText>{{ i18n.ts.turnOffToImprovePerformance }}</SearchText></template>
</MkSwitch>
</MkPreferenceContainer>
</SearchMarker>
@ -580,7 +588,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<MkPreferenceContainer k="useBlurEffect">
<MkSwitch v-model="useBlurEffect">
<template #label><SearchLabel>{{ i18n.ts.useBlurEffect }}</SearchLabel></template>
<template #caption><SearchKeyword>{{ i18n.ts.turnOffToImprovePerformance }}</SearchKeyword></template>
<template #caption><SearchText>{{ i18n.ts.turnOffToImprovePerformance }}</SearchText></template>
</MkSwitch>
</MkPreferenceContainer>
</SearchMarker>
@ -589,7 +597,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<MkPreferenceContainer k="useBlurEffectForModal">
<MkSwitch v-model="useBlurEffectForModal">
<template #label><SearchLabel>{{ i18n.ts.useBlurEffectForModal }}</SearchLabel></template>
<template #caption><SearchKeyword>{{ i18n.ts.turnOffToImprovePerformance }}</SearchKeyword></template>
<template #caption><SearchText>{{ i18n.ts.turnOffToImprovePerformance }}</SearchText></template>
</MkSwitch>
</MkPreferenceContainer>
</SearchMarker>
@ -598,7 +606,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<MkPreferenceContainer k="enableHighQualityImagePlaceholders">
<MkSwitch v-model="enableHighQualityImagePlaceholders">
<template #label><SearchLabel>{{ i18n.ts._settings.enableHighQualityImagePlaceholders }}</SearchLabel></template>
<template #caption><SearchKeyword>{{ i18n.ts.turnOffToImprovePerformance }}</SearchKeyword></template>
<template #caption><SearchText>{{ i18n.ts.turnOffToImprovePerformance }}</SearchText></template>
</MkSwitch>
</MkPreferenceContainer>
</SearchMarker>
@ -607,7 +615,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<MkPreferenceContainer k="useStickyIcons">
<MkSwitch v-model="useStickyIcons">
<template #label><SearchLabel>{{ i18n.ts._settings.useStickyIcons }}</SearchLabel></template>
<template #caption><SearchKeyword>{{ i18n.ts.turnOffToImprovePerformance }}</SearchKeyword></template>
<template #caption><SearchText>{{ i18n.ts.turnOffToImprovePerformance }}</SearchText></template>
</MkSwitch>
</MkPreferenceContainer>
</SearchMarker>
@ -866,6 +874,7 @@ const animatedMfm = prefer.model('animatedMfm');
const disableShowingAnimatedImages = prefer.model('disableShowingAnimatedImages');
const keepScreenOn = prefer.model('keepScreenOn');
const enableHorizontalSwipe = prefer.model('enableHorizontalSwipe');
const showPageTabBarBottom = prefer.model('showPageTabBarBottom');
const enablePullToRefresh = prefer.model('enablePullToRefresh');
const useNativeUiForVideoAudioPlayer = prefer.model('useNativeUiForVideoAudioPlayer');
const contextMenu = prefer.model('contextMenu');
@ -925,6 +934,7 @@ watch([
useSystemFont,
makeEveryTextElementsSelectable,
enableHorizontalSwipe,
showPageTabBarBottom,
enablePullToRefresh,
reduceAnimation,
showAvailableReactionsFirstInNote,

View File

@ -7,13 +7,13 @@ SPDX-License-Identifier: AGPL-3.0-only
<SearchMarker path="/settings/privacy" :label="i18n.ts.privacy" :keywords="['privacy']" icon="ti ti-lock-open">
<div class="_gaps_m">
<MkFeatureBanner icon="/client-assets/unlocked_3d.png" color="#aeff00">
<SearchKeyword>{{ i18n.ts._settings.privacyBanner }}</SearchKeyword>
<SearchText>{{ i18n.ts._settings.privacyBanner }}</SearchText>
</MkFeatureBanner>
<SearchMarker :keywords="['follow', 'lock']">
<MkSwitch v-model="isLocked" @update:modelValue="save()">
<template #label><SearchLabel>{{ i18n.ts.makeFollowManuallyApprove }}</SearchLabel></template>
<template #caption><SearchKeyword>{{ i18n.ts.lockedAccountInfo }}</SearchKeyword></template>
<template #caption><SearchText>{{ i18n.ts.lockedAccountInfo }}</SearchText></template>
</MkSwitch>
</SearchMarker>
@ -28,7 +28,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<SearchMarker :keywords="['reaction', 'public']">
<MkSwitch v-model="publicReactions" @update:modelValue="save()">
<template #label><SearchLabel>{{ i18n.ts.makeReactionsPublic }}</SearchLabel></template>
<template #caption><SearchKeyword>{{ i18n.ts.makeReactionsPublicDescription }}</SearchKeyword></template>
<template #caption><SearchText>{{ i18n.ts.makeReactionsPublicDescription }}</SearchText></template>
</MkSwitch>
</SearchMarker>
@ -53,28 +53,28 @@ SPDX-License-Identifier: AGPL-3.0-only
<SearchMarker :keywords="['online', 'status']">
<MkSwitch v-model="hideOnlineStatus" @update:modelValue="save()">
<template #label><SearchLabel>{{ i18n.ts.hideOnlineStatus }}</SearchLabel></template>
<template #caption><SearchKeyword>{{ i18n.ts.hideOnlineStatusDescription }}</SearchKeyword></template>
<template #caption><SearchText>{{ i18n.ts.hideOnlineStatusDescription }}</SearchText></template>
</MkSwitch>
</SearchMarker>
<SearchMarker :keywords="['crawle', 'index', 'search']">
<MkSwitch v-model="noCrawle" @update:modelValue="save()">
<template #label><SearchLabel>{{ i18n.ts.noCrawle }}</SearchLabel></template>
<template #caption><SearchKeyword>{{ i18n.ts.noCrawleDescription }}</SearchKeyword></template>
<template #caption><SearchText>{{ i18n.ts.noCrawleDescription }}</SearchText></template>
</MkSwitch>
</SearchMarker>
<SearchMarker :keywords="['crawle', 'ai']">
<MkSwitch v-model="preventAiLearning" @update:modelValue="save()">
<template #label><SearchLabel>{{ i18n.ts.preventAiLearning }}</SearchLabel></template>
<template #caption><SearchKeyword>{{ i18n.ts.preventAiLearningDescription }}</SearchKeyword></template>
<template #caption><SearchText>{{ i18n.ts.preventAiLearningDescription }}</SearchText></template>
</MkSwitch>
</SearchMarker>
<SearchMarker :keywords="['explore']">
<MkSwitch v-model="isExplorable" @update:modelValue="save()">
<template #label><SearchLabel>{{ i18n.ts.makeExplorable }}</SearchLabel></template>
<template #caption><SearchKeyword>{{ i18n.ts.makeExplorableDescription }}</SearchKeyword></template>
<template #caption><SearchText>{{ i18n.ts.makeExplorableDescription }}</SearchText></template>
</MkSwitch>
</SearchMarker>
@ -146,7 +146,7 @@ SPDX-License-Identifier: AGPL-3.0-only
</div>
<template #caption>
<div><SearchKeyword>{{ i18n.ts._accountSettings.makeNotesFollowersOnlyBeforeDescription }}</SearchKeyword></div>
<div><SearchText>{{ i18n.ts._accountSettings.makeNotesFollowersOnlyBeforeDescription }}</SearchText></div>
</template>
</FormSlot>
</SearchMarker>
@ -183,7 +183,7 @@ SPDX-License-Identifier: AGPL-3.0-only
</div>
<template #caption>
<div><SearchKeyword>{{ i18n.ts._accountSettings.makeNotesHiddenBeforeDescription }}</SearchKeyword></div>
<div><SearchText>{{ i18n.ts._accountSettings.makeNotesHiddenBeforeDescription }}</SearchText></div>
</template>
</FormSlot>
</SearchMarker>

View File

@ -110,7 +110,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<MkInput v-model="profile.followedMessage" :max="200" manualSave :mfmPreview="false">
<template #label><SearchLabel>{{ i18n.ts._profile.followedMessage }}</SearchLabel><span class="_beta">{{ i18n.ts.beta }}</span></template>
<template #caption>
<div><SearchKeyword>{{ i18n.ts._profile.followedMessageDescription }}</SearchKeyword></div>
<div><SearchText>{{ i18n.ts._profile.followedMessageDescription }}</SearchText></div>
<div>{{ i18n.ts._profile.followedMessageDescriptionForLockedAccount }}</div>
</template>
</MkInput>

View File

@ -7,7 +7,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<SearchMarker path="/settings/security" :label="i18n.ts.security" :keywords="['security']" icon="ti ti-lock" :inlining="['2fa']">
<div class="_gaps_m">
<MkFeatureBanner icon="/client-assets/locked_with_key_3d.png" color="#ffbf00">
<SearchKeyword>{{ i18n.ts._settings.securityBanner }}</SearchKeyword>
<SearchText>{{ i18n.ts._settings.securityBanner }}</SearchText>
</MkFeatureBanner>
<SearchMarker :keywords="['password']">
@ -24,8 +24,9 @@ SPDX-License-Identifier: AGPL-3.0-only
<X2fa/>
<SearchMarker :keywords="['signin', 'login', 'history', 'log']">
<FormSection>
<template #label>{{ i18n.ts.signinHistory }}</template>
<template #label><SearchLabel>{{ i18n.ts.signinHistory }}</SearchLabel></template>
<MkPagination :paginator="paginator" withControl>
<template #default="{items}">
<div>
@ -41,13 +42,16 @@ SPDX-License-Identifier: AGPL-3.0-only
</template>
</MkPagination>
</FormSection>
</SearchMarker>
<SearchMarker :keywords="['regenerate', 'refresh', 'reset', 'token']">
<FormSection>
<FormSlot>
<MkButton danger @click="regenerateToken"><i class="ti ti-refresh"></i> {{ i18n.ts.regenerateLoginToken }}</MkButton>
<MkButton danger @click="regenerateToken"><i class="ti ti-refresh"></i> <SearchLabel>{{ i18n.ts.regenerateLoginToken }}</SearchLabel></MkButton>
<template #caption>{{ i18n.ts.regenerateLoginTokenDescription }}</template>
</FormSlot>
</FormSection>
</SearchMarker>
</div>
</SearchMarker>
</template>

View File

@ -7,7 +7,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<SearchMarker path="/settings/sounds" :label="i18n.ts.sounds" :keywords="['sounds']" icon="ti ti-music">
<div class="_gaps_m">
<MkFeatureBanner icon="/client-assets/speaker_high_volume_3d.png" color="#ff006f">
<SearchKeyword>{{ i18n.ts._settings.soundsBanner }}</SearchKeyword>
<SearchText>{{ i18n.ts._settings.soundsBanner }}</SearchText>
</MkFeatureBanner>
<SearchMarker :keywords="['mute']">

View File

@ -381,6 +381,9 @@ export const PREF_DEF = definePreferences({
showAvailableReactionsFirstInNote: {
default: false,
},
showPageTabBarBottom: {
default: false,
},
plugins: {
default: [] as Plugin[],
mergeStrategy: (a, b) => {

View File

@ -491,10 +491,6 @@ export const ROUTE_DEF = [{
path: '/performance',
name: 'performance',
component: page(() => import('@/pages/admin/performance.vue')),
}, {
path: '/server-rules',
name: 'server-rules',
component: page(() => import('@/pages/admin/server-rules.vue')),
}, {
path: '/invites',
name: 'invites',

View File

@ -37,11 +37,6 @@ html {
color: var(--MI_THEME-fg);
accent-color: var(--MI_THEME-accent);
&, * {
scrollbar-color: var(--MI_THEME-scrollbarHandle) transparent;
scrollbar-width: thin;
}
&.f-1 {
font-size: 15px;
}
@ -91,7 +86,11 @@ html::selection {
100% {
opacity: 0;
}
}
html, body, main, div {
scrollbar-color: var(--MI_THEME-scrollbarHandle) transparent;
scrollbar-width: thin;
}
html,

View File

@ -86,7 +86,7 @@ watch(rootEl, () => {
box-sizing: border-box;
background: var(--MI_THEME-navBg);
color: var(--MI_THEME-navFg);
box-shadow: 0px 0px 6px 6px #0000000f;
border-top: solid 0.5px var(--MI_THEME-divider);
}
.item {

View File

@ -0,0 +1,37 @@
/*
* SPDX-FileCopyrightText: syuilo and misskey-project
* SPDX-License-Identifier: AGPL-3.0-only
*/
import type { GeneratedSearchIndexItem } from 'search-index';
export type SearchIndexItem = {
id: string;
parentId?: string;
path?: string;
label: string;
keywords: string[];
texts: string[];
icon?: string;
};
export function genSearchIndexes(generated: GeneratedSearchIndexItem[]): SearchIndexItem[] {
const rootMods = new Map(generated.map(item => [item.id, item]));
// link inlining here
for (const item of generated) {
if (item.inlining) {
for (const id of item.inlining) {
const inline = rootMods.get(id);
if (inline) {
inline.parentId = item.id;
inline.path = item.path;
} else {
console.log('[Settings Search Index] Failed to inline', id);
}
}
}
}
return generated;
}

View File

@ -1,36 +0,0 @@
/*
* SPDX-FileCopyrightText: syuilo and misskey-project
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { searchIndexes as generated } from 'search-index:settings';
import type { GeneratedSearchIndexItem } from 'search-index:settings';
export type SearchIndexItem = {
id: string;
parentId?: string;
path?: string;
label: string;
keywords: string[];
icon?: string;
};
const rootMods = new Map(generated.map(item => [item.id, item]));
// link inlining here
for (const item of generated) {
if (item.inlining) {
for (const id of item.inlining) {
const inline = rootMods.get(id);
if (inline) {
inline.parentId = item.id;
inline.path = item.path;
} else {
console.log('[Settings Search Index] Failed to inline', id);
}
}
}
}
export const searchIndexes: SearchIndexItem[] = generated;

View File

@ -3,16 +3,25 @@
* SPDX-License-Identifier: AGPL-3.0-only
*/
declare module 'search-index:settings' {
export type GeneratedSearchIndexItem = {
type XGeneratedSearchIndexItem = {
id: string;
parentId?: string;
path?: string;
label: string;
keywords: string[];
texts: string[];
icon?: string;
inlining?: string[];
};
};
export const searchIndexes: GeneratedSearchIndexItem[];
declare module 'search-index' {
export type GeneratedSearchIndexItem = XGeneratedSearchIndexItem;
}
declare module 'search-index:settings' {
export const searchIndexes: XGeneratedSearchIndexItem[];
}
declare module 'search-index:admin' {
export const searchIndexes: XGeneratedSearchIndexItem[];
}

View File

@ -28,6 +28,11 @@ export const searchIndexes = [{
mainVirtualModule: 'search-index:settings',
modulesToHmrOnUpdate: ['src/pages/settings/index.vue'],
verbose: process.env.FRONTEND_SEARCH_INDEX_VERBOSE === 'true',
}, {
targetFilePaths: ['src/pages/admin/*.vue'],
mainVirtualModule: 'search-index:admin',
modulesToHmrOnUpdate: ['src/pages/admin/index.vue'],
verbose: process.env.FRONTEND_SEARCH_INDEX_VERBOSE === 'true',
}] satisfies SearchIndexOptions[];
/**

View File

@ -11,17 +11,17 @@
"lint": "pnpm typecheck && pnpm eslint"
},
"devDependencies": {
"@types/node": "22.16.4",
"@types/node": "22.17.0",
"@types/wawoff2": "1.0.2",
"@typescript-eslint/eslint-plugin": "8.37.0",
"@typescript-eslint/parser": "8.37.0"
"@typescript-eslint/eslint-plugin": "8.38.0",
"@typescript-eslint/parser": "8.38.0"
},
"dependencies": {
"@tabler/icons-webfont": "3.34.0",
"harfbuzzjs": "0.4.7",
"@tabler/icons-webfont": "3.34.1",
"harfbuzzjs": "0.4.8",
"tiny-glob": "0.2.9",
"tsx": "4.20.3",
"typescript": "5.8.3",
"typescript": "5.9.2",
"wawoff2": "2.0.1"
},
"files": [

View File

@ -24,13 +24,13 @@
"devDependencies": {
"@types/matter-js": "0.19.8",
"@types/seedrandom": "3.0.8",
"@types/node": "22.16.4",
"@typescript-eslint/eslint-plugin": "8.37.0",
"@typescript-eslint/parser": "8.37.0",
"@types/node": "22.17.0",
"@typescript-eslint/eslint-plugin": "8.38.0",
"@typescript-eslint/parser": "8.38.0",
"nodemon": "3.1.10",
"execa": "9.6.0",
"typescript": "5.8.3",
"esbuild": "0.25.6",
"typescript": "5.9.2",
"esbuild": "0.25.8",
"glob": "11.0.3"
},
"files": [

View File

@ -1,23 +0,0 @@
{
"$schema": "https://swc.rs/schema.json",
"jsc": {
"parser": {
"syntax": "typescript",
"dynamicImport": true,
"decorators": true
},
"transform": {
"legacyDecorator": true,
"decoratorMetadata": true
},
"experimental": {
"keepImportAssertions": true
},
"baseUrl": "src",
"paths": {
"@/*": ["*"]
},
"target": "es2022"
},
"minify": false
}

View File

@ -9,7 +9,7 @@ export default [
'**/node_modules',
'built',
'coverage',
'jest.config.ts',
'vitest.config.ts',
'test',
'test-d',
'generator',

View File

@ -7,7 +7,7 @@
"generate": "tsx src/generator.ts && eslint ./built/**/*.ts --fix"
},
"devDependencies": {
"@readme/openapi-parser": "2.7.0",
"@readme/openapi-parser": "5.0.0",
"@types/node": "22.16.4",
"@typescript-eslint/eslint-plugin": "8.37.0",
"@typescript-eslint/parser": "8.37.0",

View File

@ -2,7 +2,7 @@ import assert from 'assert';
import { mkdir, readFile, writeFile } from 'fs/promises';
import type { OpenAPIV3_1 } from 'openapi-types';
import { toPascal } from 'ts-case-convert';
import OpenAPIParser from '@readme/openapi-parser';
import { parse } from '@readme/openapi-parser';
import openapiTS, { astToString } from 'openapi-typescript';
import type { OpenAPI3, OperationObject, PathItemObject } from 'openapi-typescript';
import ts from 'typescript';
@ -401,7 +401,7 @@ async function main() {
await mkdir(generatePath, { recursive: true });
const openApiJsonPath = './api.json';
const openApiDocs = await OpenAPIParser.parse(openApiJsonPath) as OpenAPIV3_1.Document;
const openApiDocs = await parse(openApiJsonPath) as OpenAPIV3_1.Document;
const typeFileName = './built/autogen/types.ts';
await generateBaseTypes(openApiDocs, openApiJsonPath, typeFileName);

View File

@ -1,207 +0,0 @@
/*
* For a detailed explanation regarding each configuration property and type check, visit:
* https://jestjs.io/docs/en/configuration.html
*/
module.exports = {
// All imported modules in your tests should be mocked automatically
// automock: false,
// Stop running tests after `n` failures
// bail: 0,
// The directory where Jest should store its cached dependency information
// cacheDirectory: "C:\\Users\\ai\\AppData\\Local\\Temp\\jest",
// Automatically clear mock calls and instances between every test
// clearMocks: false,
// Indicates whether the coverage information should be collected while executing the test
// collectCoverage: false,
// An array of glob patterns indicating a set of files for which coverage information should be collected
// collectCoverageFrom: undefined,
// The directory where Jest should output its coverage files
coverageDirectory: "coverage",
// An array of regexp pattern strings used to skip coverage collection
// coveragePathIgnorePatterns: [
// "\\\\node_modules\\\\"
// ],
// Indicates which provider should be used to instrument code for coverage
coverageProvider: "v8",
// A list of reporter names that Jest uses when writing coverage reports
// coverageReporters: [
// "json",
// "text",
// "lcov",
// "clover"
// ],
// An object that configures minimum threshold enforcement for coverage results
// coverageThreshold: undefined,
// A path to a custom dependency extractor
// dependencyExtractor: undefined,
// Make calling deprecated APIs throw helpful error messages
// errorOnDeprecated: false,
// Force coverage collection from ignored files using an array of glob patterns
// forceCoverageMatch: [],
// A path to a module which exports an async function that is triggered once before all test suites
// globalSetup: undefined,
// A path to a module which exports an async function that is triggered once after all test suites
// globalTeardown: undefined,
// A set of global variables that need to be available in all test environments
// globals: {},
// The maximum amount of workers used to run your tests. Can be specified as % or a number. E.g. maxWorkers: 10% will use 10% of your CPU amount + 1 as the maximum worker number. maxWorkers: 2 will use a maximum of 2 workers.
// maxWorkers: "50%",
// An array of directory names to be searched recursively up from the requiring module's location
// moduleDirectories: [
// "node_modules"
// ],
// An array of file extensions your modules use
// moduleFileExtensions: [
// "js",
// "json",
// "jsx",
// "ts",
// "tsx",
// "node"
// ],
// A map from regular expressions to module names or to arrays of module names that allow to stub out resources with a single module
moduleNameMapper: {
// Do not resolve .wasm.js to .wasm by the rule below
'^(.+)\\.wasm\\.js$': '$1.wasm.js',
// SWC converts @/foo/bar.js to `../../src/foo/bar.js`, and then this rule
// converts it again to `../../src/foo/bar` which then can be resolved to
// `.ts` files.
// See https://github.com/swc-project/jest/issues/64#issuecomment-1029753225
// TODO: Use `--allowImportingTsExtensions` on TypeScript 5.0 so that we can
// directly import `.ts` files without this hack.
'^((?:\\.{1,2}|[A-Z:])*/.*)\\.js$': '$1',
},
// An array of regexp pattern strings, matched against all module paths before considered 'visible' to the module loader
// modulePathIgnorePatterns: [],
// Activates notifications for test results
// notify: false,
// An enum that specifies notification mode. Requires { notify: true }
// notifyMode: "failure-change",
// A preset that is used as a base for Jest's configuration
// preset: undefined,
// Run tests from one or more projects
// projects: undefined,
// Use this configuration option to add custom reporters to Jest
// reporters: undefined,
// Automatically reset mock state between every test
// resetMocks: false,
// Reset the module registry before running each individual test
// resetModules: false,
// A path to a custom resolver
// resolver: undefined,
// Automatically restore mock state between every test
// restoreMocks: false,
// The root directory that Jest should scan for tests and modules within
// rootDir: undefined,
// A list of paths to directories that Jest should use to search for files in
roots: [
"<rootDir>"
],
// Allows you to use a custom runner instead of Jest's default test runner
// runner: "jest-runner",
// The paths to modules that run some code to configure or set up the testing environment before each test
// setupFiles: [],
// A list of paths to modules that run some code to configure or set up the testing framework before each test
// setupFilesAfterEnv: [],
// The number of seconds after which a test is considered as slow and reported as such in the results.
// slowTestThreshold: 5,
// A list of paths to snapshot serializer modules Jest should use for snapshot testing
// snapshotSerializers: [],
// The test environment that will be used for testing
testEnvironment: "node",
// Options that will be passed to the testEnvironment
// testEnvironmentOptions: {},
// Adds a location field to test results
// testLocationInResults: false,
// The glob patterns Jest uses to detect test files
testMatch: [
"**/__tests__/**/*.[jt]s?(x)",
"**/?(*.)+(spec|test).[tj]s?(x)",
"<rootDir>/test/**/*"
],
// An array of regexp pattern strings that are matched against all test paths, matched tests are skipped
// testPathIgnorePatterns: [
// "\\\\node_modules\\\\"
// ],
// The regexp pattern or array of patterns that Jest uses to detect test files
// testRegex: [],
// This option allows the use of a custom results processor
// testResultsProcessor: undefined,
// This option allows use of a custom test runner
// testRunner: "jasmine2",
// This option sets the URL for the jsdom environment. It is reflected in properties such as location.href
// testURL: "http://localhost",
// Setting this value to "fake" allows the use of fake timers for functions such as "setTimeout"
// timers: "real",
// A map from regular expressions to paths to transformers
transform: {
"^.+\\.(t|j)sx?$": ["@swc/jest"],
},
// An array of regexp pattern strings that are matched against all source file paths, matched files will skip transformation
// transformIgnorePatterns: [
// "\\\\node_modules\\\\",
// "\\.pnp\\.[^\\\\]+$"
// ],
// An array of regexp pattern strings that are matched against all modules before the module loader will automatically return a mock for them
// unmockedModulePathPatterns: undefined,
// Indicates whether each individual test should be reported during the run
// verbose: undefined,
// An array of regexp patterns that are matched against all source file paths before re-running tests in watch mode
// watchPathIgnorePatterns: [],
// Whether to use watchman for file crawling
// watchman: true,
};

View File

@ -1,7 +1,7 @@
{
"type": "module",
"name": "misskey-js",
"version": "2025.8.0-alpha.3",
"version": "2025.8.0-alpha.4",
"description": "Misskey SDK for JavaScript",
"license": "MIT",
"main": "./built/index.js",
@ -25,8 +25,8 @@
"eslint": "eslint './**/*.{js,jsx,ts,tsx}'",
"typecheck": "tsc --noEmit",
"lint": "pnpm typecheck && pnpm eslint",
"jest": "jest --coverage --detectOpenHandles",
"test": "pnpm jest && pnpm tsd",
"vitest": "vitest run --coverage",
"test": "pnpm vitest && pnpm tsd",
"update-autogen-code": "pnpm --filter misskey-js-type-generator generate && ncp generator/built/autogen src/autogen"
},
"repository": {
@ -36,22 +36,19 @@
},
"devDependencies": {
"@microsoft/api-extractor": "7.52.8",
"@swc/jest": "0.2.39",
"@types/jest": "29.5.14",
"@types/node": "22.16.4",
"@typescript-eslint/eslint-plugin": "8.37.0",
"@typescript-eslint/parser": "8.37.0",
"jest": "29.7.0",
"jest-fetch-mock": "3.0.3",
"jest-websocket-mock": "2.5.0",
"mock-socket": "9.3.1",
"@vitest/coverage-v8": "3.2.4",
"esbuild": "0.25.6",
"execa": "9.6.0",
"glob": "11.0.3",
"ncp": "2.0.0",
"nodemon": "3.1.10",
"execa": "8.0.1",
"tsd": "0.32.0",
"typescript": "5.8.3",
"esbuild": "0.25.6",
"glob": "11.0.3"
"vitest": "3.2.4",
"vitest-websocket-mock": "0.5.0"
},
"files": [
"built"

View File

@ -1,3 +1,4 @@
import { describe, test } from 'vitest';
import { expectType } from 'tsd';
import * as Misskey from '../src/index.js';

View File

@ -1,3 +1,4 @@
import { describe, test } from 'vitest';
import { expectType } from 'tsd';
import * as Misskey from '../src/index.js';

View File

@ -1,40 +1,23 @@
import { enableFetchMocks } from 'jest-fetch-mock';
import { vi, describe, test, expect } from 'vitest';
import { APIClient, isAPIError } from '../src/api.js';
enableFetchMocks();
function getFetchCall(call: any[]) {
const { body, method } = call[1];
const contentType = call[1].headers['Content-Type'];
if (
body == null ||
(contentType === 'application/json' && typeof body !== 'string') ||
(contentType === 'multipart/form-data' && !(body instanceof FormData))
) {
throw new Error('invalid body');
}
return {
url: call[0],
method: method,
contentType: contentType,
body: body instanceof FormData ? Object.fromEntries(body.entries()) : JSON.parse(body),
};
}
describe('API', () => {
test('success', async () => {
fetchMock.resetMocks();
fetchMock.mockResponse(async (req) => {
const body = await req.json();
if (req.method == 'POST' && req.url == 'https://misskey.test/api/i') {
const fetchMock = vi
.spyOn(globalThis, 'fetch')
.mockImplementation(async (url, options) => {
if (url === 'https://misskey.test/api/i' && options?.method === 'POST') {
if (options.body) {
const body = JSON.parse(options.body as string);
if (body.i === 'TOKEN') {
return JSON.stringify({ id: 'foo' });
} else {
return { status: 400 };
return new Response(JSON.stringify({ id: 'foo' }), { status: 200 });
}
} else {
return { status: 404 };
}
return new Response(null, { status: 400 });
}
return new Response(null, { status: 404 });
});
const cli = new APIClient({
@ -48,27 +31,37 @@ describe('API', () => {
id: 'foo'
});
expect(getFetchCall(fetchMock.mock.calls[0])).toEqual({
url: 'https://misskey.test/api/i',
fetch('https://misskey.test/api/i', {
method: 'POST',
contentType: 'application/json',
body: { i: 'TOKEN' }
})
expect(fetchMock).toHaveBeenCalledWith('https://misskey.test/api/i', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
credentials: 'omit',
cache: 'no-cache',
body: JSON.stringify({ i: 'TOKEN' }),
});
fetchMock.mockRestore();
});
test('with params', async () => {
fetchMock.resetMocks();
fetchMock.mockResponse(async (req) => {
const body = await req.json();
if (req.method == 'POST' && req.url == 'https://misskey.test/api/notes/show') {
const fetchMock = vi
.spyOn(globalThis, 'fetch')
.mockImplementation(async (url, options) => {
if (url === 'https://misskey.test/api/notes/show' && options?.method === 'POST') {
if (options.body) {
const body = JSON.parse(options.body as string);
if (body.i === 'TOKEN' && body.noteId === 'aaaaa') {
return JSON.stringify({ id: 'foo' });
} else {
return { status: 400 };
return new Response(JSON.stringify({ id: 'foo' }), { status: 200 });
}
} else {
return { status: 404 };
}
return new Response(null, { status: 400 });
}
return new Response(null, { status: 404 });
});
const cli = new APIClient({
@ -82,22 +75,33 @@ describe('API', () => {
id: 'foo'
});
expect(getFetchCall(fetchMock.mock.calls[0])).toEqual({
url: 'https://misskey.test/api/notes/show',
expect(fetchMock).toHaveBeenCalledWith('https://misskey.test/api/notes/show', {
method: 'POST',
contentType: 'application/json',
body: { i: 'TOKEN', noteId: 'aaaaa' }
headers: {
'Content-Type': 'application/json',
},
credentials: 'omit',
cache: 'no-cache',
body: JSON.stringify({ noteId: 'aaaaa', i: 'TOKEN' }),
});
fetchMock.mockRestore();
});
test('multipart/form-data', async () => {
fetchMock.resetMocks();
fetchMock.mockResponse(async (req) => {
if (req.method == 'POST' && req.url == 'https://misskey.test/api/drive/files/create') {
return JSON.stringify({ id: 'foo' });
} else {
return { status: 404 };
const fetchMock = vi
.spyOn(globalThis, 'fetch')
.mockImplementation(async (url, options) => {
if (url === 'https://misskey.test/api/drive/files/create' && options?.method === 'POST') {
if (options.body instanceof FormData) {
const file = options.body.get('file');
if (file instanceof File && file.name === 'foo.txt') {
return new Response(JSON.stringify({ id: 'foo' }), { status: 200 });
}
}
return new Response(null, { status: 400 });
}
return new Response(null, { status: 404 });
});
const cli = new APIClient({
@ -116,25 +120,25 @@ describe('API', () => {
id: 'foo'
});
expect(getFetchCall(fetchMock.mock.calls[0])).toEqual({
url: 'https://misskey.test/api/drive/files/create',
expect(fetchMock).toHaveBeenCalledWith('https://misskey.test/api/drive/files/create', {
method: 'POST',
contentType: undefined,
body: {
i: 'TOKEN',
file: testFile,
}
body: expect.any(FormData),
headers: {},
credentials: 'omit',
cache: 'no-cache',
});
fetchMock.mockRestore();
});
test('204 No Content で null が返る', async () => {
fetchMock.resetMocks();
fetchMock.mockResponse(async (req) => {
if (req.method == 'POST' && req.url == 'https://misskey.test/api/reset-password') {
return { status: 204 };
} else {
return { status: 404 };
const fetchMock = vi
.spyOn(globalThis, 'fetch')
.mockImplementation(async (url, options) => {
if (url === 'https://misskey.test/api/reset-password' && options?.method === 'POST') {
return new Response(null, { status: 204 });
}
return new Response(null, { status: 404 });
});
const cli = new APIClient({
@ -146,36 +150,41 @@ describe('API', () => {
expect(res).toEqual(null);
expect(getFetchCall(fetchMock.mock.calls[0])).toEqual({
url: 'https://misskey.test/api/reset-password',
expect(fetchMock).toHaveBeenCalledWith('https://misskey.test/api/reset-password', {
method: 'POST',
contentType: 'application/json',
body: { i: 'TOKEN', token: 'aaa', password: 'aaa' }
headers: {
'Content-Type': 'application/json',
},
credentials: 'omit',
cache: 'no-cache',
body: JSON.stringify({ token: 'aaa', password: 'aaa', i: 'TOKEN' }),
});
fetchMock.mockRestore();
});
test('インスタンスの credential が指定されていても引数で credential が null ならば null としてリクエストされる', async () => {
fetchMock.resetMocks();
fetchMock.mockResponse(async (req) => {
const body = await req.json();
if (req.method == 'POST' && req.url == 'https://misskey.test/api/i') {
const fetchMock = vi
.spyOn(globalThis, 'fetch')
.mockImplementation(async (url, options) => {
if (url === 'https://misskey.test/api/i' && options?.method === 'POST') {
if (options.body) {
const body = JSON.parse(options.body as string);
if (typeof body.i === 'string') {
return JSON.stringify({ id: 'foo' });
return new Response(JSON.stringify({ id: 'foo' }), { status: 200 });
} else {
return {
status: 401,
body: JSON.stringify({
return new Response(JSON.stringify({
error: {
message: 'Credential required.',
code: 'CREDENTIAL_REQUIRED',
id: '1384574d-a912-4b81-8601-c7b1c4085df1',
}
})
};
}), { status: 401 });
}
} else {
return { status: 404 };
}
return new Response(null, { status: 400 });
}
return new Response(null, { status: 404 });
});
try {
@ -187,23 +196,23 @@ describe('API', () => {
await cli.request('i', {}, null);
} catch (e) {
expect(isAPIError(e)).toEqual(true);
} finally {
fetchMock.mockRestore();
}
});
test('api error', async () => {
fetchMock.resetMocks();
fetchMock.mockResponse(async (req) => {
return {
status: 500,
body: JSON.stringify({
const fetchMock = vi
.spyOn(globalThis, 'fetch')
.mockImplementation(async () => {
return new Response(JSON.stringify({
error: {
message: 'Internal error occurred. Please contact us if the error persists.',
code: 'INTERNAL_ERROR',
id: '5d37dbcb-891e-41ca-a3d6-e690c97775ac',
kind: 'server',
},
})
};
}), { status: 500 });
});
try {
@ -216,12 +225,17 @@ describe('API', () => {
} catch (e: any) {
expect(isAPIError(e)).toEqual(true);
expect(e.id).toEqual('5d37dbcb-891e-41ca-a3d6-e690c97775ac');
} finally {
fetchMock.mockRestore();
}
});
test('network error', async () => {
fetchMock.resetMocks();
fetchMock.mockAbort();
const fetchMock = vi
.spyOn(globalThis, 'fetch')
.mockImplementation(async () => {
throw new Error('Network error');
});
try {
const cli = new APIClient({
@ -232,16 +246,16 @@ describe('API', () => {
await cli.request('i');
} catch (e) {
expect(isAPIError(e)).toEqual(false);
} finally {
fetchMock.mockRestore();
}
});
test('json parse error', async () => {
fetchMock.resetMocks();
fetchMock.mockResponse(async (req) => {
return {
status: 500,
body: '<html>I AM NOT JSON</html>'
};
const fetchMock = vi
.spyOn(globalThis, 'fetch')
.mockImplementation(async () => {
return new Response('<html>I AM NOT JSON</html>', { status: 500 });
});
try {
@ -253,17 +267,17 @@ describe('API', () => {
await cli.request('i');
} catch (e) {
expect(isAPIError(e)).toEqual(false);
} finally {
fetchMock.mockRestore();
}
});
test('admin/roles/create の型が合う', async() => {
fetchMock.resetMocks();
fetchMock.mockResponse(async () => {
return {
const fetchMock = vi
.spyOn(globalThis, 'fetch')
.mockImplementation(async () => {
// 本来返すべき値は`Role`型だが、テストなのでお茶を濁す
status: 200,
body: '{}'
};
return new Response('{}', { status: 200 });
});
const cli = new APIClient({
@ -292,5 +306,7 @@ describe('API', () => {
},
target: 'manual',
});
fetchMock.mockRestore();
})
});

View File

@ -1,4 +1,5 @@
import WS from 'jest-websocket-mock';
import { describe, test, expect } from 'vitest';
import WS from 'vitest-websocket-mock';
import Stream from '../src/streaming.js';
describe('Streaming', () => {

View File

@ -0,0 +1,17 @@
import { defineConfig, configDefaults } from 'vitest/config';
export default defineConfig({
test: {
include: ['test/**/*.ts'],
coverage: {
exclude: [
...configDefaults.coverage.exclude!,
'src/autogen/**/*',
'generator/**/*',
'built/**/*',
'test-d/**/*',
'build.js',
],
}
},
});

View File

@ -22,13 +22,13 @@
"lint": "pnpm typecheck && pnpm eslint"
},
"devDependencies": {
"@types/node": "22.16.4",
"@typescript-eslint/eslint-plugin": "8.37.0",
"@typescript-eslint/parser": "8.37.0",
"@types/node": "22.17.0",
"@typescript-eslint/eslint-plugin": "8.38.0",
"@typescript-eslint/parser": "8.38.0",
"execa": "9.6.0",
"nodemon": "3.1.10",
"typescript": "5.8.3",
"esbuild": "0.25.6",
"typescript": "5.9.2",
"esbuild": "0.25.8",
"glob": "11.0.3"
},
"files": [

View File

@ -9,16 +9,16 @@
"lint": "pnpm typecheck && pnpm eslint"
},
"dependencies": {
"esbuild": "0.25.6",
"esbuild": "0.25.8",
"idb-keyval": "6.2.2",
"misskey-js": "workspace:*"
},
"devDependencies": {
"@typescript-eslint/parser": "8.37.0",
"@typescript-eslint/parser": "8.38.0",
"@typescript/lib-webworker": "npm:@types/serviceworker@0.0.74",
"eslint-plugin-import": "2.32.0",
"nodemon": "3.1.10",
"typescript": "5.8.3"
"typescript": "5.9.2"
},
"type": "module"
}

File diff suppressed because it is too large Load Diff