Compare commits

..

29 Commits

Author SHA1 Message Date
syuilo
7f9180e045 Merge branch 'develop' into lowpowermode 2025-08-01 15:20:01 +09:00
かっこかり
62f68de800 fix(frontend); Playのボタンがはみ出している問題を修正 (#16303) 2025-08-01 14:31:49 +09:00
syuilo
5bf13c4cc2 Update CHANGELOG.md 2025-08-01 13:44:06 +09:00
syuilo
16f47adcc6 Update CHANGELOG.md 2025-08-01 13:43:09 +09:00
github-actions[bot]
8eba8c7218 Bump version to 2025.8.0-alpha.1 2025-08-01 04:06:20 +00:00
syuilo
b214a19d5f New Crowdin updates (#16300)
* New translations ja-jp.yml (Italian)

* New translations ja-jp.yml (Spanish)

* New translations ja-jp.yml (Spanish)

* New translations ja-jp.yml (Spanish)

* New translations ja-jp.yml (Spanish)

* New translations ja-jp.yml (English)

* New translations ja-jp.yml (Catalan)

* New translations ja-jp.yml (Russian)

* New translations ja-jp.yml (Russian)

* New translations ja-jp.yml (Japanese, Kansai)

* New translations ja-jp.yml (Russian)

* New translations ja-jp.yml (Chinese Simplified)

* New translations ja-jp.yml (Chinese Traditional)

* New translations ja-jp.yml (Japanese, Kansai)

* New translations ja-jp.yml (Japanese, Kansai)

* New translations ja-jp.yml (Spanish)

* New translations ja-jp.yml (Catalan)

* New translations ja-jp.yml (German)

* New translations ja-jp.yml (Italian)

* New translations ja-jp.yml (Russian)

* New translations ja-jp.yml (English)

* New translations ja-jp.yml (Japanese, Kansai)

* New translations ja-jp.yml (Chinese Simplified)

* New translations ja-jp.yml (French)

* New translations ja-jp.yml (Arabic)

* New translations ja-jp.yml (Czech)

* New translations ja-jp.yml (Korean)

* New translations ja-jp.yml (Norwegian)

* New translations ja-jp.yml (Polish)

* New translations ja-jp.yml (Portuguese)

* New translations ja-jp.yml (Slovak)

* New translations ja-jp.yml (Ukrainian)

* New translations ja-jp.yml (Chinese Traditional)

* New translations ja-jp.yml (Vietnamese)

* New translations ja-jp.yml (Indonesian)

* New translations ja-jp.yml (Bengali)

* New translations ja-jp.yml (Thai)
2025-08-01 13:04:32 +09:00
syuilo
1082145c74 enhance: ジョブのログを表示できるように 2025-08-01 12:54:33 +09:00
syuilo
2a836047e3 Update CHANGELOG.md 2025-08-01 12:38:50 +09:00
syuilo
b2b07e5f21 enhance(backend): 連合関係のサーバー設定のデフォルト値をウィザード側に移動
- サーバー初期設定ウィザードでデフォルト値を設定できるため、データベース上のデフォルト値でオンにしておく必要がない
- 連合は初期設定が終わるまで閉じられている方が安全
2025-08-01 12:36:25 +09:00
github-actions[bot]
da06f75455 Bump version to 2025.8.0-alpha.0 2025-08-01 02:50:01 +00:00
syuilo
d624da9c1a feat: remote notes cleaning (#16292)
* Create CleanRemoteNotesProcessorService.ts

* Update CleanRemoteNotesProcessorService.ts

* Update CleanRemoteNotesProcessorService.ts

* wip

* Update CleanRemoteNotesProcessorService.ts

* Update CleanRemoteNotesProcessorService.ts

* Update CleanRemoteNotesProcessorService.ts

* Update CleanRemoteNotesProcessorService.ts

* Update CleanRemoteNotesProcessorService.ts

* Update CleanRemoteNotesProcessorService.ts

* Update CleanRemoteNotesProcessorService.ts

* Update CleanRemoteNotesProcessorService.ts

* Update job-queue.job.vue

* wip

* Update CleanRemoteNotesProcessorService.ts

* wip

* wip

* wip

* Update CleanRemoteNotesProcessorService.ts

* wip

* Update CHANGELOG.md

* Revert "wip"

This reverts commit 89d455d302c1106c421bcec309fd7bf02509465e.

* wip

* woip

* Update QueueService.ts

* Update QueueService.ts

* ピン留め考慮

* Update CleanRemoteNotesProcessorService.ts

* Update QueueService.ts

* Update CleanRemoteNotesProcessorService.ts

* add log

* Update CHANGELOG.md

* wip

* Update MkServerSetupWizard.vue
2025-08-01 11:49:12 +09:00
syuilo
4c520fa693 enhance(frontend): サーバーの初期設定ウィザードをやり直せるように 2025-08-01 11:07:09 +09:00
syuilo
a7d1c94f48 enhance(backend): tweak system job log 2025-08-01 09:51:43 +09:00
かっこかり
4f5d3f6f7d fix(frontend): MkNotesTimelineの日付dividerのスタイル修正 (#16306) 2025-07-31 21:45:34 +09:00
syuilo
4be0045826 update minimum nodejs version 2025-07-31 21:21:44 +09:00
syuilo
18daf43f70 clean up
ワイルドカードセレクタはexpensive
2025-07-31 21:12:07 +09:00
syuilo
862a6fae79 enhance(backend): 古いバージョンで作成され現在使われなくなったrepeatableジョブをクリーンアップするように 2025-07-31 20:57:36 +09:00
かっこかり
a45e89c300 fix(frontend): 適用中のテーマを保持する際にリアクティビティも保持される問題を修正 (#16304)
* fix(frontend): 現在のテーマを保持する際にリアクティビティが保持される問題を修正

* Update Changelog

* Update theme.ts
2025-07-31 18:47:22 +09:00
syuilo
35888eb8f4 enhance(backend): BullMQの廃止されたRepeatableからJob Schedulersに移行 2025-07-31 18:16:21 +09:00
syuilo
f2a23fb55e ノートの脱CASCADE削除 (#16332)
* wip

* Update CHANGELOG.md

* Update QueryService.ts

* Update QueryService.ts

* wip

* Update MkNoteDetailed.vue

* Update NoteEntityService.ts

* wip

* Update antennas.ts

* Update create.ts

* Update NoteEntityService.ts

* wip

* Update CHANGELOG.md

* Update NoteEntityService.ts

* Update NoteCreateService.ts

* Update note.test.ts

* Update note.test.ts

* Update ClientServerService.ts

* Update ClientServerService.ts

* add error handling

* Update NoteDeleteService.ts

* Update CHANGELOG.md

* Update entities.ts

* Update entities.ts

* Update misskey-js.api.md
2025-07-31 14:40:51 +09:00
tamaina
414d5958c1 fix(test): Fix name of a test in e2e/timelines.ts (#16334) 2025-07-31 14:22:32 +09:00
tamaina
8c65d8d020 test(backend): e2e/timelines.ts: 非FTT時のテストを追加, 凍結のテストを追加, これにかかる幾つかのバグ修正 (#16284)
* test(backend): 非FTT時のテストを追加

* clean up

* skip test about reply

* Fix #16289

* clean up

* cherry pick

* add renote test

* Fix https://github.com/misskey-dev/misskey/issues/16293

* remove debug log
2025-07-30 21:41:46 +09:00
かっこかり
927aa9dc3d fix(frontend): inline な SearchMarker のパスが正しくない問題を修正 (#16301)
* replace URL path for inlined SearchMarkers

The search index looks like:

```ts
[
 {
   id: 'foo', label: 'security',
   path: '/settings/security', inlining: ['2fa'],
 },
 {
   id: '2fa',
   label: 'two-factor auth',
   path: '/settings/2fa', // guessed wrong by the index generation
 },
 {
   id: 'aaaa',
   parentId: '2fa',
   label: 'totp',
 },
 …
]
```

This file post-processes that index and re-parents the inlined
sections. Problem was, it left the (wrong) `path` untouched.

Replacing the `path` makes the search work fine.

* Update Changelog

---------

Co-authored-by: dakkar <dakkar@thenautilus.net>
2025-07-30 14:39:55 +09:00
かっこかり
1dec8b2329 fix(frontend/test): Cypressが失敗する問題を修正 (#16307)
* attempt to fix test

* fix(frontend/test): Cypressが失敗する問題を修正
2025-07-30 14:12:59 +09:00
syuilo
e8b5aa5c2f wip 2025-07-30 13:52:52 +09:00
zyoshoka
b0493abe93 chore: continue backend E2E test even if fail with minimum Node.js version (#16324)
* chore: continue backend E2E test even if fail with minimum Node.js version

* chore: disable `fail-fast`
2025-07-30 12:32:24 +09:00
かっこかり
4f653f2fbc enhance(frontend): typed nirax (#16309)
* enhance(frontend): typed nirax

* migrate router.replace

* fix
2025-07-30 12:30:35 +09:00
tamaina
b660769288 perf(frontend): draw-blurhash workerの結果をpostMessageする際にImageBitmapを移譲する (#16330) 2025-07-30 09:30:07 +09:00
かっこかり
48246bd166 fix(deps): regenerate lockfile (#16302) 2025-07-19 14:00:19 +09:00
117 changed files with 3953 additions and 3563 deletions

View File

@@ -1 +1 @@
20.18.1
22.15.0

View File

@@ -109,6 +109,7 @@ jobs:
name: E2E tests (backend)
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
node-version-file:
- .node-version

View File

@@ -1,13 +1,26 @@
## Unreleased
## 2025.8.0
### Note
- サポートされるNode.jsの最小バージョンが**22.15.0**になりました
### General
-
- ノートを削除した際、関連するノートが同時に削除されないようになりました
- APIで、「replyIdが存在しているのにreplyがnull」や「renoteIdが存在しているのにrenoteがnull」であるという、今までにはなかったパターンが表れることになります
- 定期的に参照されていない古いリモートの投稿を削除する機能が実装されました(コントロールパネル→パフォーマンス→Remote Notes Cleaning)
- 既存のサーバーでは**デフォルトでオフ**、新規サーバーでは**デフォルトでオン**になります
- データベースの肥大化を防止することが可能です
- 既存のサーバーで当機能を有効化した場合は、処理量が多くなるため、一時的にストレージ使用量が増加する可能性があります。
- 増加量を抑えるには、最大処理継続時間をデフォルトより短くしてください。
- サーバーの初期設定が完了するまでは連合がオンにならないようになりました
### Client
-
- Fix: 一部の設定検索結果が存在しないパスになる問題を修正
(Cherry-picked from https://activitypub.software/TransFem-org/Sharkey/-/merge_requests/1171)
- Fix: テーマエディタが動作しない問題を修正
### Server
-
- Enhance: ノートの削除処理の効率化
- Enhance: 全体的なパフォーマンスの向上
## 2025.7.0

View File

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

View File

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

View File

@@ -896,7 +896,7 @@ searchResult: "Resultats de la cerca"
hashtags: "Etiquetes"
troubleshooting: "Solucionar problemes"
useBlurEffect: "Fes servir efectes de desenfocament a la interfície"
learnMore: "Saber més "
learnMore: "Saber-ne més "
misskeyUpdated: "Misskey s'ha actualitzat "
whatIsNew: "Mostra canvis"
translate: "Traduir "
@@ -1368,6 +1368,8 @@ redisplayAllTips: "Torna ha mostrat tots els trucs i consells"
hideAllTips: "Amagar tots els trucs i consells"
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)"
_order:
newest: "Més recent"
oldest: "Cronològic"

View File

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

View File

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

View File

@@ -1302,7 +1302,7 @@ passkeyVerificationSucceededButPasswordlessLoginDisabled: "Passkey verification
messageToFollower: "Message to followers"
target: "Target"
testCaptchaWarning: "This function is intended for CAPTCHA testing purposes.\n<strong>Do not use in a production environment.</strong>"
prohibitedWordsForNameOfUser: "Prohibited words for user names"
prohibitedWordsForNameOfUser: "Prohibited words for usernames"
prohibitedWordsForNameOfUserDescription: "If any of the strings in this list are included in the user's name, the name will be denied. Users with moderator privileges are not affected by this restriction."
yourNameContainsProhibitedWords: "Your name contains prohibited words"
yourNameContainsProhibitedWordsDescription: "If you wish to use this name, please contact your server administrator."
@@ -1368,6 +1368,8 @@ redisplayAllTips: "Show all “Tips & Tricks” again"
hideAllTips: "Hide all \"Tips & Tricks\""
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)"
_order:
newest: "Newest First"
oldest: "Oldest First"

View File

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

View File

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

View File

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

52
locales/index.d.ts vendored
View File

@@ -2567,11 +2567,11 @@ export interface Locale extends ILocale {
*/
"serviceworkerInfo": string;
/**
* 削除された投稿
* 削除されたノート
*/
"deletedNote": string;
/**
* 非公開の投稿
* 非公開のノート
*/
"invisibleNote": string;
/**
@@ -5493,6 +5493,18 @@ export interface Locale extends ILocale {
* 低くすると画質を保てますが、ファイルサイズは増加します。<br>高くするとファイルサイズを減らせますが、画質は低下します。
*/
"defaultImageCompressionLevel_description": string;
/**
* 低電力モード
*/
"lowPowerMode": string;
/**
* 分
*/
"inMinutes": string;
/**
* 日
*/
"inDays": string;
"_order": {
/**
* 新しい順
@@ -5799,6 +5811,10 @@ export interface Locale extends ILocale {
* UIのアニメーション
*/
"uiAnimations": string;
/**
* アニメーション画像を再生
*/
"playAnimatedImages": string;
/**
* ナビゲーションバーに副ボタンを表示
*/
@@ -6486,6 +6502,22 @@ export interface Locale extends ILocale {
* 有効にすると、リアクション作成時のパフォーマンスが大幅に向上し、データベースへの負荷を軽減することが可能です。ただし、Redisのメモリ使用量は増加します。
*/
"reactionsBufferingDescription": string;
/**
* リモート投稿の自動クリーニング
*/
"remoteNotesCleaning": string;
/**
* 有効にすると、参照されていない古いリモートの投稿を定期的にクリーンアップしてデータベースの肥大化を抑制します。
*/
"remoteNotesCleaning_description": string;
/**
* 最大クリーニング処理継続時間
*/
"remoteNotesCleaningMaxProcessingDuration": string;
/**
* 最低ノート保持日数
*/
"remoteNotesCleaningExpiryDaysForEachNotes": string;
/**
* 問い合わせ先URL
*/
@@ -6558,6 +6590,14 @@ export interface Locale extends ILocale {
* サーバーで受信したリモートのコンテンツを含め、サーバー内の全てのコンテンツを無条件でインターネットに公開することはリスクが伴います。特に、分散型の特性を知らない閲覧者にとっては、リモートのコンテンツであってもサーバー内で作成されたコンテンツであると誤って認識してしまう可能性があるため、注意が必要です。
*/
"userGeneratedContentsVisibilityForVisitor_description2": string;
/**
* サーバーの初期設定ウィザードをやり直しますか?
*/
"restartServerSetupWizardConfirm_title": string;
/**
* 現在の一部の設定はリセットされます。
*/
"restartServerSetupWizardConfirm_text": string;
"_userGeneratedContentsVisibilityForVisitor": {
/**
* 全て公開
@@ -11943,6 +11983,14 @@ export interface Locale extends ILocale {
* 連合可能なサーバーの指定など、高度な設定も後ほど可能です。
*/
"youCanConfigureMoreFederationSettingsLater": string;
/**
* 受信コンテンツの自動クリーニング
*/
"remoteContentsCleaning": string;
/**
* 連合を行うと、継続して多くのコンテンツを受信します。自動クリーニングを有効にすると、参照されていない古くなったコンテンツを自動でサーバーから削除し、ストレージを節約できます。
*/
"remoteContentsCleaning_description": string;
/**
* 管理者情報
*/

View File

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

View File

@@ -637,8 +637,8 @@ addRelay: "リレーの追加"
inboxUrl: "inboxのURL"
addedRelays: "追加済みのリレー"
serviceworkerInfo: "プッシュ通知を行うには有効にする必要があります。"
deletedNote: "削除された投稿"
invisibleNote: "非公開の投稿"
deletedNote: "削除されたノート"
invisibleNote: "非公開のノート"
enableInfiniteScroll: "自動でもっと見る"
visibility: "公開範囲"
poll: "アンケート"
@@ -1368,6 +1368,9 @@ redisplayAllTips: "全ての「ヒントとコツ」を再表示"
hideAllTips: "全ての「ヒントとコツ」を非表示"
defaultImageCompressionLevel: "デフォルトの画像圧縮度"
defaultImageCompressionLevel_description: "低くすると画質を保てますが、ファイルサイズは増加します。<br>高くするとファイルサイズを減らせますが、画質は低下します。"
lowPowerMode: "低電力モード"
inMinutes: "分"
inDays: "日"
_order:
newest: "新しい順"
@@ -1451,6 +1454,7 @@ _settings:
useStickyIcons: "アイコンをスクロールに追従させる"
enableHighQualityImagePlaceholders: "高品質な画像のプレースホルダを表示"
uiAnimations: "UIのアニメーション"
playAnimatedImages: "アニメーション画像を再生"
showNavbarSubButtons: "ナビゲーションバーに副ボタンを表示"
ifOn: "オンのとき"
ifOff: "オフのとき"
@@ -1649,6 +1653,10 @@ _serverSettings:
fanoutTimelineDbFallback: "データベースへのフォールバック"
fanoutTimelineDbFallbackDescription: "有効にすると、タイムラインがキャッシュされていない場合にDBへ追加で問い合わせを行うフォールバック処理を行います。無効にすると、フォールバック処理を行わないことでさらにサーバーの負荷を軽減することができますが、タイムラインが取得できる範囲に制限が生じます。"
reactionsBufferingDescription: "有効にすると、リアクション作成時のパフォーマンスが大幅に向上し、データベースへの負荷を軽減することが可能です。ただし、Redisのメモリ使用量は増加します。"
remoteNotesCleaning: "リモート投稿の自動クリーニング"
remoteNotesCleaning_description: "有効にすると、参照されていない古いリモートの投稿を定期的にクリーンアップしてデータベースの肥大化を抑制します。"
remoteNotesCleaningMaxProcessingDuration: "最大クリーニング処理継続時間"
remoteNotesCleaningExpiryDaysForEachNotes: "最低ノート保持日数"
inquiryUrl: "問い合わせ先URL"
inquiryUrlDescription: "サーバー運営者へのお問い合わせフォームのURLや、運営者の連絡先等が記載されたWebページのURLを指定します。"
openRegistration: "アカウントの作成をオープンにする"
@@ -1667,6 +1675,8 @@ _serverSettings:
userGeneratedContentsVisibilityForVisitor: "非利用者に対するユーザー作成コンテンツの公開範囲"
userGeneratedContentsVisibilityForVisitor_description: "モデレーションが行き届きにくい不適切なリモートコンテンツなどが、自サーバー経由で図らずもインターネットに公開されてしまうことによるトラブル防止などに役立ちます。"
userGeneratedContentsVisibilityForVisitor_description2: "サーバーで受信したリモートのコンテンツを含め、サーバー内の全てのコンテンツを無条件でインターネットに公開することはリスクが伴います。特に、分散型の特性を知らない閲覧者にとっては、リモートのコンテンツであってもサーバー内で作成されたコンテンツであると誤って認識してしまう可能性があるため、注意が必要です。"
restartServerSetupWizardConfirm_title: "サーバーの初期設定ウィザードをやり直しますか?"
restartServerSetupWizardConfirm_text: "現在の一部の設定はリセットされます。"
_userGeneratedContentsVisibilityForVisitor:
all: "全て公開"
@@ -3194,6 +3204,8 @@ _serverSetupWizard:
doYouConnectToFediverse_description1: "分散型サーバーで構成されるネットワーク(Fediverse)に接続すると、他のサーバーと相互にコンテンツのやり取りが可能です。"
doYouConnectToFediverse_description2: "Fediverseと接続することは「連合」とも呼ばれます。"
youCanConfigureMoreFederationSettingsLater: "連合可能なサーバーの指定など、高度な設定も後ほど可能です。"
remoteContentsCleaning: "受信コンテンツの自動クリーニング"
remoteContentsCleaning_description: "連合を行うと、継続して多くのコンテンツを受信します。自動クリーニングを有効にすると、参照されていない古くなったコンテンツを自動でサーバーから削除し、ストレージを節約できます。"
adminInfo: "管理者情報"
adminInfo_description: "問い合わせを受け付けるために使用される管理者情報を設定します。"
adminInfo_mustBeFilled: "オープンサーバー、または連合がオンの場合は必ず入力が必要です。"

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,18 @@
/*
* SPDX-FileCopyrightText: syuilo and misskey-project
* SPDX-License-Identifier: AGPL-3.0-only
*/
export class RemoveNoteConstraints1753868431598 {
name = 'RemoveNoteConstraints1753868431598'
async up(queryRunner) {
await queryRunner.query(`ALTER TABLE "note" DROP CONSTRAINT "FK_52ccc804d7c69037d558bac4c96"`);
await queryRunner.query(`ALTER TABLE "note" DROP CONSTRAINT "FK_17cb3553c700a4985dff5a30ff5"`);
}
async down(queryRunner) {
await queryRunner.query(`ALTER TABLE "note" ADD CONSTRAINT "FK_17cb3553c700a4985dff5a30ff5" FOREIGN KEY ("replyId") REFERENCES "note"("id") ON DELETE CASCADE ON UPDATE NO ACTION`);
await queryRunner.query(`ALTER TABLE "note" ADD CONSTRAINT "FK_52ccc804d7c69037d558bac4c96" FOREIGN KEY ("renoteId") REFERENCES "note"("id") ON DELETE CASCADE ON UPDATE NO ACTION`);
}
}

View File

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

View File

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

View File

@@ -20,6 +20,8 @@ import { CacheService } from '@/core/CacheService.js';
import { isReply } from '@/misc/is-reply.js';
import { isInstanceMuted } from '@/misc/is-instance-muted.js';
type NoteFilter = (note: MiNote) => boolean;
type TimelineOptions = {
untilId: string | null,
sinceId: string | null,
@@ -28,7 +30,7 @@ type TimelineOptions = {
me?: { id: MiUser['id'] } | undefined | null,
useDbFallback: boolean,
redisTimelines: FanoutTimelineName[],
noteFilter?: (note: MiNote) => boolean,
noteFilter?: NoteFilter,
alwaysIncludeMyNotes?: boolean;
ignoreAuthorFromBlock?: boolean;
ignoreAuthorFromMute?: boolean;
@@ -79,7 +81,7 @@ export class FanoutTimelineEndpointService {
const shouldFallbackToDb = noteIds.length === 0 || ps.sinceId != null && ps.sinceId < oldestNoteId;
if (!shouldFallbackToDb) {
let filter = ps.noteFilter ?? (_note => true);
let filter = ps.noteFilter ?? (_note => true) as NoteFilter;
if (ps.alwaysIncludeMyNotes && ps.me) {
const me = ps.me;
@@ -145,15 +147,11 @@ export class FanoutTimelineEndpointService {
{
const parentFilter = filter;
filter = (note) => {
const noteJoined = note as MiNote & {
renoteUser: MiUser | null;
replyUser: MiUser | null;
};
if (!ps.ignoreAuthorFromUserSuspension) {
if (note.user!.isSuspended) return false;
}
if (note.userId !== note.renoteUserId && noteJoined.renoteUser?.isSuspended) return false;
if (note.userId !== note.replyUserId && noteJoined.replyUser?.isSuspended) return false;
if (note.userId !== note.renoteUserId && note.renote?.user?.isSuspended) return false;
if (note.userId !== note.replyUserId && note.reply?.user?.isSuspended) return false;
return parentFilter(note);
};
@@ -200,7 +198,7 @@ export class FanoutTimelineEndpointService {
return await ps.dbFallback(ps.untilId, ps.sinceId, ps.limit);
}
private async getAndFilterFromDb(noteIds: string[], noteFilter: (note: MiNote) => boolean, idCompare: (a: string, b: string) => number): Promise<MiNote[]> {
private async getAndFilterFromDb(noteIds: string[], noteFilter: NoteFilter, idCompare: (a: string, b: string) => number): Promise<MiNote[]> {
const query = this.notesRepository.createQueryBuilder('note')
.where('note.id IN (:...noteIds)', { noteIds: noteIds })
.innerJoinAndSelect('note.user', 'user')

View File

@@ -421,7 +421,7 @@ export class NoteCreateService implements OnApplicationShutdown {
emojis,
userId: user.id,
localOnly: data.localOnly!,
reactionAcceptance: data.reactionAcceptance,
reactionAcceptance: data.reactionAcceptance ?? null,
visibility: data.visibility as any,
visibleUserIds: data.visibility === 'specified'
? data.visibleUsers
@@ -483,7 +483,11 @@ export class NoteCreateService implements OnApplicationShutdown {
await this.notesRepository.insert(insert);
}
return insert;
return {
...insert,
reply: data.reply ?? null,
renote: data.renote ?? null,
};
} catch (e) {
// duplicate key error
if (isDuplicateKeyValueError(e)) {

View File

@@ -62,7 +62,6 @@ export class NoteDeleteService {
*/
async delete(user: { id: MiUser['id']; uri: MiUser['uri']; host: MiUser['host']; isBot: MiUser['isBot']; }, note: MiNote, quiet = false, deleter?: MiUser) {
const deletedAt = new Date();
const cascadingNotes = await this.findCascadingNotes(note);
if (note.replyId) {
await this.notesRepository.decrement({ id: note.replyId }, 'repliesCount', 1);
@@ -90,15 +89,6 @@ export class NoteDeleteService {
this.deliverToConcerned(user, note, content);
}
// also deliver delete activity to cascaded notes
const federatedLocalCascadingNotes = (cascadingNotes).filter(note => !note.localOnly && note.userHost == null); // filter out local-only notes
for (const cascadingNote of federatedLocalCascadingNotes) {
if (!cascadingNote.user) continue;
if (!this.userEntityService.isLocalUser(cascadingNote.user)) continue;
const content = this.apRendererService.addContext(this.apRendererService.renderDelete(this.apRendererService.renderTombstone(`${this.config.url}/notes/${cascadingNote.id}`), cascadingNote.user));
this.deliverToConcerned(cascadingNote.user, cascadingNote, content);
}
//#endregion
this.notesChart.update(note, false);
@@ -118,9 +108,6 @@ export class NoteDeleteService {
}
}
for (const cascadingNote of cascadingNotes) {
this.searchService.unindexNote(cascadingNote);
}
this.searchService.unindexNote(note);
await this.notesRepository.delete({
@@ -140,29 +127,6 @@ export class NoteDeleteService {
}
}
@bindThis
private async findCascadingNotes(note: MiNote): Promise<MiNote[]> {
const recursive = async (noteId: string): Promise<MiNote[]> => {
const query = this.notesRepository.createQueryBuilder('note')
.where('note.replyId = :noteId', { noteId })
.orWhere(new Brackets(q => {
q.where('note.renoteId = :noteId', { noteId })
.andWhere('note.text IS NOT NULL');
}))
.leftJoinAndSelect('note.user', 'user');
const replies = await query.getMany();
return [
replies,
...await Promise.all(replies.map(reply => recursive(reply.id))),
].flat();
};
const cascadingNotes: MiNote[] = await recursive(note.id);
return cascadingNotes;
}
@bindThis
private async getMentionedRemoteUsers(note: MiNote) {
const where = [] as any[];

View File

@@ -360,7 +360,7 @@ export class QueryService {
public generateSuspendedUserQueryForNote(q: SelectQueryBuilder<any>, excludeAuthor?: boolean): void {
if (excludeAuthor) {
const brakets = (user: string) => new Brackets(qb => qb
.where(`note.${user}Id IS NULL`)
.where(`${user}.id IS NULL`) // そもそもreplyやrenoteではない、もしくはleftjoinなどでuserが存在しなかった場合を考慮
.orWhere(`user.id = ${user}.id`)
.orWhere(`${user}.isSuspended = FALSE`));
q
@@ -368,7 +368,7 @@ export class QueryService {
.andWhere(brakets('renoteUser'));
} else {
const brakets = (user: string) => new Brackets(qb => qb
.where(`note.${user}Id IS NULL`)
.where(`${user}.id IS NULL`) // そもそもreplyやrenoteではない、もしくはleftjoinなどでuserが存在しなかった場合を考慮
.orWhere(`${user}.isSuspended = FALSE`));
q
.andWhere('user.isSuspended = FALSE')

View File

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

View File

@@ -4,7 +4,7 @@
*/
import { Inject, Injectable } from '@nestjs/common';
import { In } from 'typeorm';
import { EntityNotFoundError, In } from 'typeorm';
import { ModuleRef } from '@nestjs/core';
import { DI } from '@/di-symbols.js';
import type { Packed } from '@/misc/json-schema.js';
@@ -46,6 +46,17 @@ function getAppearNoteIds(notes: MiNote[]): Set<string> {
return appearNoteIds;
}
async function nullIfEntityNotFound<T>(promise: Promise<T>): Promise<T | null> {
try {
return await promise;
} catch (err) {
if (err instanceof EntityNotFoundError) {
return null;
}
throw err;
}
}
@Injectable()
export class NoteEntityService implements OnModuleInit {
private userEntityService: UserEntityService;
@@ -436,19 +447,21 @@ export class NoteEntityService implements OnModuleInit {
...(opts.detail ? {
clippedCount: note.clippedCount,
reply: note.replyId ? this.pack(note.reply ?? note.replyId, me, {
// そもそもJOINしていない場合はundefined、JOINしたけど存在していなかった場合はnullで区別される
reply: (note.replyId && note.reply === null) ? null : note.replyId ? nullIfEntityNotFound(this.pack(note.reply ?? note.replyId, me, {
detail: false,
skipHide: opts.skipHide,
withReactionAndUserPairCache: opts.withReactionAndUserPairCache,
_hint_: options?._hint_,
}) : undefined,
})) : undefined,
renote: note.renoteId ? this.pack(note.renote ?? note.renoteId, me, {
// そもそもJOINしていない場合はundefined、JOINしたけど存在していなかった場合はnullで区別される
renote: (note.renoteId && note.renote === null) ? null : note.renoteId ? nullIfEntityNotFound(this.pack(note.renote ?? note.renoteId, me, {
detail: true,
skipHide: opts.skipHide,
withReactionAndUserPairCache: opts.withReactionAndUserPairCache,
_hint_: options?._hint_,
}) : undefined,
})) : undefined,
poll: note.hasPoll ? this.populatePoll(note, meId) : undefined,
@@ -591,7 +604,7 @@ export class NoteEntityService implements OnModuleInit {
private findNoteOrFail(id: string): Promise<MiNote> {
return this.notesRepository.findOneOrFail({
where: { id },
relations: ['user'],
relations: ['user', 'renote', 'reply'],
});
}

View File

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

View File

@@ -36,7 +36,7 @@ export class MiNote {
public replyId: MiNote['id'] | null;
@ManyToOne(type => MiNote, {
onDelete: 'CASCADE',
createForeignKeyConstraints: false,
})
@JoinColumn()
public reply: MiNote | null;
@@ -50,7 +50,7 @@ export class MiNote {
public renoteId: MiNote['id'] | null;
@ManyToOne(type => MiNote, {
onDelete: 'CASCADE',
createForeignKeyConstraints: false,
})
@JoinColumn()
public renote: MiNote | null;

View File

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

View File

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

View File

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

View File

@@ -40,8 +40,8 @@ export class GetterService {
}
@bindThis
public async getNoteWithUser(noteId: MiNote['id']) {
const note = await this.notesRepository.findOne({ where: { id: noteId }, relations: ['user'] });
public async getNoteWithRelations(noteId: MiNote['id']) {
const note = await this.notesRepository.findOne({ where: { id: noteId }, relations: ['user', 'reply', 'renote', 'reply.user', 'renote.user'] });
if (note == null) {
throw new IdentifiableError('9725d0ce-ba28-4dde-95a7-2cbb2c15de24', 'No such note.');

View File

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

View File

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

View File

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

View File

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

View File

@@ -269,7 +269,10 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
let renote: MiNote | null = null;
if (ps.renoteId != null) {
// Fetch renote to note
renote = await this.notesRepository.findOneBy({ id: ps.renoteId });
renote = await this.notesRepository.findOne({
where: { id: ps.renoteId },
relations: ['user', 'renote', 'reply'],
});
if (renote == null) {
throw new ApiError(meta.errors.noSuchRenoteTarget);
@@ -315,7 +318,10 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
let reply: MiNote | null = null;
if (ps.replyId != null) {
// Fetch reply
reply = await this.notesRepository.findOneBy({ id: ps.replyId });
reply = await this.notesRepository.findOne({
where: { id: ps.replyId },
relations: ['user'],
});
if (reply == null) {
throw new ApiError(meta.errors.noSuchReplyTarget);

View File

@@ -55,7 +55,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
private getterService: GetterService,
) {
super(meta, paramDef, async (ps, me) => {
const note = await this.getterService.getNoteWithUser(ps.noteId).catch(err => {
const note = await this.getterService.getNoteWithRelations(ps.noteId).catch(err => {
if (err.id === '9725d0ce-ba28-4dde-95a7-2cbb2c15de24') throw new ApiError(meta.errors.noSuchNote);
throw err;
});

View File

@@ -237,7 +237,13 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
}
if (ps.withRenotes === false) {
query.andWhere('note.renoteId IS NULL');
query.andWhere(new Brackets(qb => {
qb.orWhere('note.renoteId IS NULL');
qb.orWhere(new Brackets(qb => {
qb.orWhere('note.text IS NOT NULL');
qb.orWhere('note.fileIds != \'{}\'');
}));
}));
}
//#endregion

View File

@@ -580,7 +580,7 @@ export class ClientServerService {
id: request.params.note,
visibility: In(['public', 'home']),
},
relations: ['user'],
relations: ['user', 'reply', 'renote'],
});
if (
@@ -821,8 +821,11 @@ export class ClientServerService {
fastify.get<{ Params: { note: string; } }>('/embed/notes/:note', async (request, reply) => {
reply.removeHeader('X-Frame-Options');
const note = await this.notesRepository.findOneBy({
id: request.params.note,
const note = await this.notesRepository.findOne({
where: {
id: request.params.note,
},
relations: ['user', 'reply', 'renote'],
});
if (note == null) return;

View File

@@ -63,7 +63,6 @@ describe('Note', () => {
deepStrictEqualWithExcludedFields(note, resolvedNote, [
'id',
'emojis',
'reactionAcceptance',
'replyId',
'reply',
'userId',
@@ -105,7 +104,6 @@ describe('Note', () => {
deepStrictEqualWithExcludedFields(note, resolvedNote, [
'id',
'emojis',
'reactionAcceptance',
'renoteId',
'renote',
'userId',

View File

@@ -673,7 +673,6 @@ describe('アンテナ', () => {
assert.deepStrictEqual(response, expected);
});
test.skip('が取得でき、日付指定のPaginationに一貫性があること', async () => { });
test.each([
{ label: 'ID指定', offsetBy: 'id' },

File diff suppressed because it is too large Load Diff

View File

@@ -18,5 +18,5 @@ onmessage = (event) => {
render(event.data.hash, canvas);
const bitmap = canvas.transferToImageBitmap();
postMessage({ id: event.data.id, bitmap });
postMessage({ id: event.data.id, bitmap }, [bitmap]);
};

View File

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

View File

@@ -495,7 +495,7 @@ function done(query?: string): boolean | void {
function settings() {
emit('esc');
router.push('settings/emoji-palette');
router.push('/settings/emoji-palette');
}
onMounted(() => {

View File

@@ -52,15 +52,20 @@ import TestWebGL2 from '@/workers/test-webgl2?worker';
import { WorkerMultiDispatch } from '@@/js/worker-multi-dispatch.js';
import { extractAvgColorFromBlurhash } from '@@/js/extract-avg-color-from-blurhash.js';
// テスト環境で Web Worker インスタンスは作成できない
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-expect-error
const isTest = (import.meta.env.MODE === 'test' || window.Cypress != null);
const canvasPromise = new Promise<WorkerMultiDispatch | HTMLCanvasElement>(resolve => {
// テスト環境で Web Worker インスタンスは作成できない
if (import.meta.env.MODE === 'test') {
if (isTest) {
const canvas = window.document.createElement('canvas');
canvas.width = 64;
canvas.height = 64;
resolve(canvas);
return;
}
const testWorker = new TestWebGL2();
testWorker.addEventListener('message', event => {
if (event.data.result) {
@@ -189,7 +194,7 @@ function drawAvg() {
}
async function draw() {
if (import.meta.env.MODE === 'test' && props.hash == null) return;
if (isTest && props.hash == null) return;
drawAvg();

View File

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

View File

@@ -17,7 +17,7 @@ SPDX-License-Identifier: AGPL-3.0-only
</div>
<MkNoteSub v-for="note in conversation" :key="note.id" :class="$style.replyToMore" :note="note"/>
</div>
<MkNoteSub v-if="appearNote.reply" :note="appearNote.reply" :class="$style.replyTo"/>
<MkNoteSub v-if="appearNote.replyId" :note="appearNote.reply" :class="$style.replyTo"/>
<div v-if="isRenote" :class="$style.renote">
<MkAvatar :class="$style.renoteAvatar" :user="note.user" link preview/>
<i class="ti ti-repeat" style="margin-right: 4px;"></i>

View File

@@ -4,7 +4,7 @@ SPDX-License-Identifier: AGPL-3.0-only
-->
<template>
<div :class="$style.root">
<div v-if="note" :class="$style.root">
<MkAvatar :class="$style.avatar" :user="note.user" link preview/>
<div :class="$style.main">
<MkNoteHeader :class="$style.header" :note="note" :mini="true"/>
@@ -19,6 +19,9 @@ SPDX-License-Identifier: AGPL-3.0-only
</div>
</div>
</div>
<div v-else :class="$style.deleted">
{{ i18n.ts.deletedNote }}
</div>
</template>
<script lang="ts" setup>
@@ -27,9 +30,10 @@ import * as Misskey from 'misskey-js';
import MkNoteHeader from '@/components/MkNoteHeader.vue';
import MkSubNoteContent from '@/components/MkSubNoteContent.vue';
import MkCwButton from '@/components/MkCwButton.vue';
import { i18n } from '@/i18n.js';
const props = defineProps<{
note: Misskey.entities.Note;
note: Misskey.entities.Note | null;
}>();
const showContent = ref(false);
@@ -101,4 +105,14 @@ const showContent = ref(false);
height: 48px;
}
}
.deleted {
text-align: center;
padding: 8px !important;
margin: 8px 8px 0 8px;
--color: light-dark(rgba(0, 0, 0, 0.05), rgba(0, 0, 0, 0.15));
background-size: auto auto;
background-image: repeating-linear-gradient(135deg, transparent, transparent 10px, var(--color) 4px, var(--color) 14px);
border-radius: 8px;
}
</style>

View File

@@ -4,7 +4,10 @@ SPDX-License-Identifier: AGPL-3.0-only
-->
<template>
<div v-if="!muted" :class="[$style.root, { [$style.children]: depth > 1 }]">
<div v-if="note == null" :class="$style.deleted">
{{ i18n.ts.deletedNote }}
</div>
<div v-else-if="!muted" :class="[$style.root, { [$style.children]: depth > 1 }]">
<div :class="$style.main">
<div v-if="note.channel" :class="$style.colorBar" :style="{ background: note.channel.color }"></div>
<MkAvatar :class="$style.avatar" :user="note.user" link preview/>
@@ -53,7 +56,7 @@ import { userPage } from '@/filters/user.js';
import { checkWordMute } from '@/utility/check-word-mute.js';
const props = withDefaults(defineProps<{
note: Misskey.entities.Note;
note: Misskey.entities.Note | null;
detail?: boolean;
// how many notes are in between this one and the note being viewed in detail
@@ -62,12 +65,12 @@ const props = withDefaults(defineProps<{
depth: 1,
});
const muted = ref($i ? checkWordMute(props.note, $i, $i.mutedWords) : false);
const muted = ref(props.note && $i ? checkWordMute(props.note, $i, $i.mutedWords) : false);
const showContent = ref(false);
const replies = ref<Misskey.entities.Note[]>([]);
if (props.detail) {
if (props.detail && props.note) {
misskeyApi('notes/children', {
noteId: props.note.id,
limit: 5,
@@ -160,4 +163,14 @@ if (props.detail) {
margin: 8px 8px 0 8px;
border-radius: 8px;
}
.deleted {
text-align: center;
padding: 8px !important;
margin: 8px 8px 0 8px;
--color: light-dark(rgba(0, 0, 0, 0.05), rgba(0, 0, 0, 0.15));
background-size: auto auto;
background-image: repeating-linear-gradient(135deg, transparent, transparent 10px, var(--color) 4px, var(--color) 14px);
border-radius: 8px;
}
</style>

View File

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

View File

@@ -151,7 +151,7 @@ const contextmenu = computed(() => ([{
function back() {
history.value.pop();
windowRouter.replace(history.value.at(-1)!.path);
windowRouter.replaceByPath(history.value.at(-1)!.path);
}
function reload() {
@@ -163,7 +163,7 @@ function close() {
}
function expand() {
mainRouter.push(windowRouter.getCurrentFullPath(), 'forcePage');
mainRouter.pushByPath(windowRouter.getCurrentFullPath(), 'forcePage');
windowEl.value?.close();
}

View File

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

View File

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

View File

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

View File

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

View File

@@ -186,7 +186,7 @@ function searchOnKeyDown(ev: KeyboardEvent) {
if (ev.key === 'Enter' && searchSelectedIndex.value != null) {
ev.preventDefault();
router.push(searchResult.value[searchSelectedIndex.value].path + '#' + searchResult.value[searchSelectedIndex.value].id);
router.pushByPath(searchResult.value[searchSelectedIndex.value].path + '#' + searchResult.value[searchSelectedIndex.value].id);
} else if (ev.key === 'ArrowDown') {
ev.preventDefault();
const current = searchSelectedIndex.value ?? -1;

View File

@@ -64,7 +64,7 @@ function onContextmenu(ev) {
icon: 'ti ti-player-eject',
text: i18n.ts.showInPage,
action: () => {
router.push(props.to, 'forcePage');
router.pushByPath(props.to, 'forcePage');
},
}, { type: 'divider' }, {
icon: 'ti ti-external-link',
@@ -99,6 +99,6 @@ function nav(ev: MouseEvent) {
return openWindow();
}
router.push(props.to, ev.ctrlKey ? 'forcePage' : null);
router.pushByPath(props.to, ev.ctrlKey ? 'forcePage' : null);
}
</script>

View File

@@ -76,7 +76,7 @@ function mount() {
function back() {
const prev = tabs.value[tabs.value.length - 2];
tabs.value = [...tabs.value.slice(0, tabs.value.length - 1)];
router.replace(prev.fullPath);
router?.replaceByPath(prev.fullPath);
}
router.useListener('change', ({ resolved }) => {

View File

@@ -58,7 +58,7 @@ export type RouterEvents = {
beforeFullPath: string;
fullPath: string;
route: RouteDef | null;
props: Map<string, string> | null;
props: Map<string, string | boolean> | null;
}) => void;
same: () => void;
};
@@ -77,6 +77,110 @@ export type PathResolvedResult = {
};
};
//#region Path Types
type Prettify<T> = {
[K in keyof T]: T[K]
} & {};
type RemoveNever<T> = {
[K in keyof T as T[K] extends never ? never : K]: T[K];
} & {};
type IsPathParameter<Part extends string> = Part extends `${string}:${infer Parameter}` ? Parameter : never;
type GetPathParamKeys<Path extends string> =
Path extends `${infer A}/${infer B}`
? IsPathParameter<A> | GetPathParamKeys<B>
: IsPathParameter<Path>;
type GetPathParams<Path extends string> = Prettify<{
[Param in GetPathParamKeys<Path> as Param extends `${string}?` ? never : Param]: string;
} & {
[Param in GetPathParamKeys<Path> as Param extends `${infer OptionalParam}?` ? OptionalParam : never]?: string;
}>;
type UnwrapReadOnly<T> = T extends ReadonlyArray<infer U>
? U
: T extends Readonly<infer U>
? U
: T;
type GetPaths<Def extends RouteDef> = Def extends { path: infer Path }
? Path extends string
? Def extends { children: infer Children }
? Children extends RouteDef[]
? Path | `${Path}${FlattenAllPaths<Children>}`
: Path
: Path
: never
: never;
type FlattenAllPaths<Defs extends RouteDef[]> = GetPaths<Defs[number]>;
type GetSinglePathQuery<Def extends RouteDef, Path extends FlattenAllPaths<RouteDef[]>> = RemoveNever<
Def extends { path: infer BasePath, children: infer Children }
? BasePath extends string
? Path extends `${BasePath}${infer ChildPath}`
? Children extends RouteDef[]
? ChildPath extends FlattenAllPaths<Children>
? GetPathQuery<Children, ChildPath>
: Record<string, never>
: never
: never
: never
: Def['path'] extends Path
? Def extends { query: infer Query }
? Query extends Record<string, string>
? UnwrapReadOnly<{ [Key in keyof Query]?: string; }>
: Record<string, never>
: Record<string, never>
: Record<string, never>
>;
type GetPathQuery<Defs extends RouteDef[], Path extends FlattenAllPaths<Defs>> = GetSinglePathQuery<Defs[number], Path>;
type RequiredIfNotEmpty<K extends string, T extends Record<string, unknown>> = T extends Record<string, never>
? { [Key in K]?: T }
: { [Key in K]: T };
type NotRequiredIfEmpty<T extends Record<string, unknown>> = T extends Record<string, never> ? T | undefined : T;
type GetRouterOperationProps<Defs extends RouteDef[], Path extends FlattenAllPaths<Defs>> = NotRequiredIfEmpty<RequiredIfNotEmpty<'params', GetPathParams<Path>> & {
query?: GetPathQuery<Defs, Path>;
hash?: string;
}>;
//#endregion
function buildFullPath(args: {
path: string;
params?: Record<string, string>;
query?: Record<string, string>;
hash?: string;
}) {
let fullPath = args.path;
if (args.params) {
for (const key in args.params) {
const value = args.params[key];
const replaceRegex = new RegExp(`:${key}(\\?)?`, 'g');
fullPath = fullPath.replace(replaceRegex, value ? encodeURIComponent(value) : '');
}
}
if (args.query) {
const queryString = new URLSearchParams(args.query).toString();
if (queryString) {
fullPath += '?' + queryString;
}
}
if (args.hash) {
fullPath += '#' + encodeURIComponent(args.hash);
}
return fullPath;
}
function parsePath(path: string): ParsedPath {
const res = [] as ParsedPath;
@@ -282,7 +386,7 @@ export class Nirax<DEF extends RouteDef[]> extends EventEmitter<RouterEvents> {
}
}
if (res.route.loginRequired && !this.isLoggedIn) {
if (res.route.loginRequired && !this.isLoggedIn && 'component' in res.route) {
res.route.component = this.notFoundPageComponent;
res.props.set('showLoginPopup', true);
}
@@ -310,14 +414,35 @@ export class Nirax<DEF extends RouteDef[]> extends EventEmitter<RouterEvents> {
return this.currentFullPath;
}
public push(fullPath: string, flag?: RouterFlag) {
public push<P extends FlattenAllPaths<DEF>>(path: P, props?: GetRouterOperationProps<DEF, P>, flag?: RouterFlag | null) {
const fullPath = buildFullPath({
path,
params: props?.params,
query: props?.query,
hash: props?.hash,
});
this.pushByPath(fullPath, flag);
}
public replace<P extends FlattenAllPaths<DEF>>(path: P, props?: GetRouterOperationProps<DEF, P>) {
const fullPath = buildFullPath({
path,
params: props?.params,
query: props?.query,
hash: props?.hash,
});
this.replaceByPath(fullPath);
}
/** どうしても必要な場合に使用(パスが確定している場合は `Nirax.push` を使用すること) */
public pushByPath(fullPath: string, flag?: RouterFlag | null) {
const beforeFullPath = this.currentFullPath;
if (fullPath === beforeFullPath) {
this.emit('same');
return;
}
if (this.navHook) {
const cancel = this.navHook(fullPath, flag);
const cancel = this.navHook(fullPath, flag ?? undefined);
if (cancel) return;
}
const res = this.navigate(fullPath);
@@ -333,14 +458,15 @@ export class Nirax<DEF extends RouteDef[]> extends EventEmitter<RouterEvents> {
}
}
public replace(fullPath: string) {
/** どうしても必要な場合に使用(パスが確定している場合は `Nirax.replace` を使用すること) */
public replaceByPath(fullPath: string) {
const res = this.navigate(fullPath);
this.emit('replace', {
fullPath: res._parsedRoute.fullPath,
});
}
public useListener<E extends keyof RouterEvents, L = RouterEvents[E]>(event: E, listener: L) {
public useListener<E extends keyof RouterEvents>(event: E, listener: EventEmitter.EventListener<RouterEvents, E>) {
this.addListener(event, listener);
onBeforeUnmount(() => {

View File

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

View File

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

View File

@@ -72,12 +72,20 @@ async function save() {
roleId: role.value.id,
...data.value,
});
router.push('/admin/roles/' + role.value.id);
router.push('/admin/roles/:id', {
params: {
id: role.value.id,
}
});
} else {
const created = await os.apiWithDialog('admin/roles/create', {
...data.value,
});
router.push('/admin/roles/' + created.id);
router.push('/admin/roles/:id', {
params: {
id: created.id,
}
});
}
}

View File

@@ -88,7 +88,11 @@ const role = reactive(await misskeyApi('admin/roles/show', {
}));
function edit() {
router.push('/admin/roles/' + role.id + '/edit');
router.push('/admin/roles/:id/edit', {
params: {
id: role.id,
}
});
}
async function del() {

View File

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

View File

@@ -47,7 +47,11 @@ async function timetravel() {
}
function settings() {
router.push(`/my/antennas/${props.antennaId}`);
router.push('/my/antennas/:antennaId', {
params: {
antennaId: props.antennaId,
}
});
}
function focus() {

View File

@@ -165,7 +165,11 @@ function save() {
os.apiWithDialog('channels/update', params);
} else {
os.apiWithDialog('channels/create', params).then(created => {
router.push(`/channels/${created.id}`);
router.push('/channels/:channelId', {
params: {
channelId: created.id,
},
});
});
}
}

View File

@@ -147,7 +147,11 @@ watch(() => props.channelId, async () => {
}, { immediate: true });
function edit() {
router.push(`/channels/${channel.value?.id}/edit`);
router.push('/channels/:channelId/edit', {
params: {
channelId: props.channelId,
}
});
}
function openPostForm() {

View File

@@ -86,7 +86,11 @@ function start(ev: MouseEvent) {
async function startUser() {
// TODO: localOnly は連合に対応したら消す
os.selectUser({ localOnly: true }).then(user => {
router.push(`/chat/user/${user.id}`);
router.push('/chat/user/:userId', {
params: {
userId: user.id,
}
});
});
}
@@ -101,7 +105,11 @@ async function createRoom() {
name: result,
});
router.push(`/chat/room/${room.id}`);
router.push('/chat/room/:roomId', {
params: {
roomId: room.id,
}
});
}
async function search() {

View File

@@ -61,7 +61,11 @@ async function join(invitation: Misskey.entities.ChatRoomInvitation) {
roomId: invitation.room.id,
});
router.push(`/chat/room/${invitation.room.id}`);
router.push('/chat/room/:roomId', {
params: {
roomId: invitation.room.id,
},
});
}
async function ignore(invitation: Misskey.entities.ChatRoomInvitation) {

View File

@@ -429,7 +429,11 @@ async function save() {
script: script.value,
visibility: visibility.value,
});
router.push('/play/' + created.id + '/edit');
router.push('/play/:id/edit', {
params: {
id: created.id,
},
});
}
}

View File

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

View File

@@ -85,7 +85,11 @@ async function save() {
fileIds: files.value.map(file => file.id),
isSensitive: isSensitive.value,
});
router.push(`/gallery/${props.postId}`);
router.push('/gallery/:postId', {
params: {
postId: props.postId,
}
});
} else {
const created = await os.apiWithDialog('gallery/posts/create', {
title: title.value,
@@ -93,7 +97,11 @@ async function save() {
fileIds: files.value.map(file => file.id),
isSensitive: isSensitive.value,
});
router.push(`/gallery/${created.id}`);
router.push('/gallery/:postId', {
params: {
postId: created.id,
}
});
}
}

View File

@@ -150,7 +150,11 @@ async function unlike() {
}
function edit() {
router.push(`/gallery/${post.value.id}/edit`);
router.push('/gallery/:postId/edit', {
params: {
postId: props.postId,
},
});
}
async function reportAbuse() {

View File

@@ -45,11 +45,20 @@ function fetch() {
promise = misskeyApi('ap/show', {
uri,
});
promise.then(res => {
if (res.type === 'User') {
mainRouter.replace(res.object.host ? `/@${res.object.username}@${res.object.host}` : `/@${res.object.username}`);
mainRouter.replace('/@:acct/:page?', {
params: {
acct: res.host != null ? `${res.object.username}@${res.object.host}` : res.object.username,
}
});
} else if (res.type === 'Note') {
mainRouter.replace(`/notes/${res.object.id}`);
mainRouter.replace('/notes/:noteId/:initialTab?', {
params: {
noteId: res.object.id,
}
});
} else {
os.alert({
type: 'error',
@@ -63,7 +72,11 @@ function fetch() {
}
promise = misskeyApi('users/show', Misskey.acct.parse(uri));
promise.then(user => {
mainRouter.replace(user.host ? `/@${user.username}@${user.host}` : `/@${user.username}`);
mainRouter.replace('/@:acct/:page?', {
params: {
acct: user.host != null ? `${user.username}@${user.host}` : user.username,
}
});
});
}

View File

@@ -154,7 +154,11 @@ async function save() {
pageId.value = created.id;
currentName.value = name.value.trim();
mainRouter.replace(`/pages/edit/${pageId.value}`);
mainRouter.replace('/pages/edit/:initPageId', {
params: {
initPageId: pageId.value,
},
});
}
}
@@ -189,7 +193,11 @@ async function duplicate() {
pageId.value = created.id;
currentName.value = name.value.trim();
mainRouter.push(`/pages/edit/${pageId.value}`);
mainRouter.push('/pages/edit/:initPageId', {
params: {
initPageId: pageId.value,
},
});
}
async function add() {

View File

@@ -267,7 +267,11 @@ function showMenu(ev: MouseEvent) {
menuItems.push({
icon: 'ti ti-pencil',
text: i18n.ts.edit,
action: () => router.push(`/pages/edit/${page.value.id}`),
action: () => router.push('/pages/edit/:initPageId', {
params: {
initPageId: page.value!.id,
},
}),
});
if ($i.pinnedPageId === page.value.id) {

View File

@@ -168,7 +168,11 @@ function startGame(game: Misskey.entities.ReversiGameDetailed) {
playbackRate: 1,
});
router.push(`/reversi/g/${game.id}`);
router.push('/reversi/g/:gameId', {
params: {
gameId: game.id,
},
});
}
async function matchHeatbeat() {

View File

@@ -264,10 +264,18 @@ async function search() {
const res = await apLookup(searchParams.value.query);
if (res.type === 'User') {
router.push(`/@${res.object.username}@${res.object.host}`);
router.push('/@:acct/:page?', {
params: {
acct: `${res.object.username}@${res.object.host}`,
},
});
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
} else if (res.type === 'Note') {
router.push(`/notes/${res.object.id}`);
router.push('/notes/:noteId/:initialTab?', {
params: {
noteId: res.object.id,
},
});
}
return;
@@ -282,7 +290,7 @@ async function search() {
text: i18n.ts.lookupConfirm,
});
if (!confirm.canceled) {
router.push(`/${searchParams.value.query}`);
router.pushByPath(`/${searchParams.value.query}`);
return;
}
}
@@ -293,7 +301,11 @@ async function search() {
text: i18n.ts.openTagPageConfirm,
});
if (!confirm.canceled) {
router.push(`/tags/${encodeURIComponent(searchParams.value.query.substring(1))}`);
router.push('/tags/:tag', {
params: {
tag: searchParams.value.query.substring(1),
},
});
return;
}
}

View File

@@ -77,10 +77,18 @@ async function search() {
const res = await promise;
if (res.type === 'User') {
router.push(`/@${res.object.username}@${res.object.host}`);
router.push('/@:acct/:page?', {
params: {
acct: `${res.object.username}@${res.object.host}`,
},
});
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
} else if (res.type === 'Note') {
router.push(`/notes/${res.object.id}`);
router.push('/notes/:noteId/:initialTab?', {
params: {
noteId: res.object.id,
},
});
}
return;
@@ -95,7 +103,7 @@ async function search() {
text: i18n.ts.lookupConfirm,
});
if (!confirm.canceled) {
router.push(`/${query}`);
router.pushByPath(`/${query}`);
return;
}
}
@@ -106,7 +114,11 @@ async function search() {
text: i18n.ts.openTagPageConfirm,
});
if (!confirm.canceled) {
router.push(`/user-tags/${encodeURIComponent(query.substring(1))}`);
router.push('/user-tags/:tag', {
params: {
tag: query.substring(1),
},
});
return;
}
}

View File

@@ -567,50 +567,73 @@ SPDX-License-Identifier: AGPL-3.0-only
<template #icon><SearchIcon><i class="ti ti-battery-vertical-eco"></i></SearchIcon></template>
<div class="_gaps_s">
<SearchMarker :keywords="['animation', 'motion', 'reduce']">
<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>
<SearchMarker :keywords="['lowpowermode', 'battery', 'eco', 'save']">
<MkPreferenceContainer k="lowPowerMode">
<MkSwitch v-model="lowPowerMode">
<template #label><SearchLabel>{{ i18n.ts.lowPowerMode }}</SearchLabel></template>
</MkSwitch>
</MkPreferenceContainer>
</SearchMarker>
<SearchMarker :keywords="['blur']">
<MkPreferenceContainer k="useBlurEffect">
<MkSwitch v-model="useBlurEffect">
<template #label><SearchLabel>{{ i18n.ts.useBlurEffect }}</SearchLabel></template>
<template #caption><SearchKeyword>{{ i18n.ts.turnOffToImprovePerformance }}</SearchKeyword></template>
</MkSwitch>
</MkPreferenceContainer>
</SearchMarker>
<hr>
<SearchMarker :keywords="['blur', 'modal']">
<MkPreferenceContainer k="useBlurEffectForModal">
<MkSwitch v-model="useBlurEffectForModal">
<template #label><SearchLabel>{{ i18n.ts.useBlurEffectForModal }}</SearchLabel></template>
<template #caption><SearchKeyword>{{ i18n.ts.turnOffToImprovePerformance }}</SearchKeyword></template>
</MkSwitch>
</MkPreferenceContainer>
</SearchMarker>
<MkDisableSection :disabled="lowPowerMode">
<div class="_gaps_s">
<SearchMarker :keywords="['animation', 'image', 'gif']">
<MkPreferenceContainer k="disableShowingAnimatedImages">
<MkSwitch :modelValue="!disableShowingAnimatedImages" @update:modelValue="v => disableShowingAnimatedImages = !v">
<template #label><SearchLabel>{{ i18n.ts._settings.playAnimatedImages }}</SearchLabel></template>
<template #caption><SearchKeyword>{{ i18n.ts.turnOffToImprovePerformance }}</SearchKeyword></template>
</MkSwitch>
</MkPreferenceContainer>
</SearchMarker>
<SearchMarker :keywords="['blurhash', 'image', 'photo', 'picture', 'thumbnail', 'placeholder']">
<MkPreferenceContainer k="enableHighQualityImagePlaceholders">
<MkSwitch v-model="enableHighQualityImagePlaceholders">
<template #label><SearchLabel>{{ i18n.ts._settings.enableHighQualityImagePlaceholders }}</SearchLabel></template>
<template #caption><SearchKeyword>{{ i18n.ts.turnOffToImprovePerformance }}</SearchKeyword></template>
</MkSwitch>
</MkPreferenceContainer>
</SearchMarker>
<SearchMarker :keywords="['animation', 'motion', 'reduce']">
<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>
</MkSwitch>
</MkPreferenceContainer>
</SearchMarker>
<SearchMarker :keywords="['sticky']">
<MkPreferenceContainer k="useStickyIcons">
<MkSwitch v-model="useStickyIcons">
<template #label><SearchLabel>{{ i18n.ts._settings.useStickyIcons }}</SearchLabel></template>
<template #caption><SearchKeyword>{{ i18n.ts.turnOffToImprovePerformance }}</SearchKeyword></template>
</MkSwitch>
</MkPreferenceContainer>
</SearchMarker>
<SearchMarker :keywords="['blur']">
<MkPreferenceContainer k="useBlurEffect">
<MkSwitch v-model="useBlurEffect">
<template #label><SearchLabel>{{ i18n.ts.useBlurEffect }}</SearchLabel></template>
<template #caption><SearchKeyword>{{ i18n.ts.turnOffToImprovePerformance }}</SearchKeyword></template>
</MkSwitch>
</MkPreferenceContainer>
</SearchMarker>
<SearchMarker :keywords="['blur', 'modal']">
<MkPreferenceContainer k="useBlurEffectForModal">
<MkSwitch v-model="useBlurEffectForModal">
<template #label><SearchLabel>{{ i18n.ts.useBlurEffectForModal }}</SearchLabel></template>
<template #caption><SearchKeyword>{{ i18n.ts.turnOffToImprovePerformance }}</SearchKeyword></template>
</MkSwitch>
</MkPreferenceContainer>
</SearchMarker>
<SearchMarker :keywords="['blurhash', 'image', 'photo', 'picture', 'thumbnail', 'placeholder']">
<MkPreferenceContainer k="enableHighQualityImagePlaceholders">
<MkSwitch v-model="enableHighQualityImagePlaceholders">
<template #label><SearchLabel>{{ i18n.ts._settings.enableHighQualityImagePlaceholders }}</SearchLabel></template>
<template #caption><SearchKeyword>{{ i18n.ts.turnOffToImprovePerformance }}</SearchKeyword></template>
</MkSwitch>
</MkPreferenceContainer>
</SearchMarker>
<SearchMarker :keywords="['sticky']">
<MkPreferenceContainer k="useStickyIcons">
<MkSwitch v-model="useStickyIcons">
<template #label><SearchLabel>{{ i18n.ts._settings.useStickyIcons }}</SearchLabel></template>
<template #caption><SearchKeyword>{{ i18n.ts.turnOffToImprovePerformance }}</SearchKeyword></template>
</MkSwitch>
</MkPreferenceContainer>
</SearchMarker>
</div>
</MkDisableSection>
<MkInfo>
<div class="_gaps_s">
@@ -871,6 +894,7 @@ const useNativeUiForVideoAudioPlayer = prefer.model('useNativeUiForVideoAudioPla
const contextMenu = prefer.model('contextMenu');
const menuStyle = prefer.model('menuStyle');
const makeEveryTextElementsSelectable = prefer.model('makeEveryTextElementsSelectable');
const lowPowerMode = prefer.model('lowPowerMode');
const fontSize = ref(miLocalStorage.getItem('fontSize'));
const useSystemFont = ref(miLocalStorage.getItem('useSystemFont') != null);
@@ -928,6 +952,7 @@ watch([
enablePullToRefresh,
reduceAnimation,
showAvailableReactionsFirstInNote,
lowPowerMode,
], async () => {
await reloadAsk({ reason: i18n.ts.reloadToApplySetting, unison: true });
});

View File

@@ -135,7 +135,7 @@ async function del(): Promise<void> {
webhookId: props.webhookId,
});
router.push('/settings/webhook');
router.push('/settings/connect');
}
async function test(type: Misskey.entities.UserWebhook['on'][number]): Promise<void> {

View File

@@ -42,7 +42,11 @@ watch(() => props.listId, async () => {
}, { immediate: true });
function settings() {
router.push(`/my/lists/${props.listId}`);
router.push('/my/lists/:listId', {
params: {
listId: props.listId,
}
});
}
const headerActions = computed(() => list.value ? [{

View File

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

View File

@@ -35,464 +35,472 @@ export type SoundStore = {
// NOTE: デフォルト値は他の設定の状態に依存してはならない(依存していた場合、ユーザーがその設定項目単体で「初期値にリセット」した場合不具合の原因になる)
export const PREF_DEF = definePreferences({
accounts: {
default: [] as [host: string, user: {
id: string;
username: string;
}][],
},
pinnedUserLists: {
accountDependent: true,
default: [] as Misskey.entities.UserList[],
},
uploadFolder: {
accountDependent: true,
default: null as string | null,
},
widgets: {
accountDependent: true,
default: () => [{
name: 'calendar',
id: genId(), place: 'right', data: {},
}, {
name: 'notifications',
id: genId(), place: 'right', data: {},
}, {
name: 'trends',
id: genId(), place: 'right', data: {},
}] as {
name: string;
id: string;
place: string | null;
data: Record<string, any>;
}[],
},
'deck.profile': {
accountDependent: true,
default: null as string | null,
},
'deck.profiles': {
accountDependent: true,
default: [] as DeckProfile[],
},
emojiPalettes: {
serverDependent: true,
default: () => [{
id: genId(),
name: '',
emojis: ['👍', '❤️', '😆', '🤔', '😮', '🎉', '💢', '😥', '😇', '🍮'],
}] as {
id: string;
name: string;
emojis: string[];
}[],
mergeStrategy: (a, b) => {
const mergedItems = [] as typeof a;
for (const x of a.concat(b)) {
const sameIdItem = mergedItems.find(y => y.id === x.id);
if (sameIdItem != null) {
if (deepEqual(x, sameIdItem)) { // 完全な重複は無視
continue;
} else { // IDは同じなのに内容が違う場合はマージ不可とする
throw new Error();
}
} else {
mergedItems.push(x);
}
}
return mergedItems;
states: {
accounts: {
default: [] as [host: string, user: {
id: string;
username: string;
}][],
},
},
emojiPaletteForReaction: {
serverDependent: true,
default: null as string | null,
},
emojiPaletteForMain: {
serverDependent: true,
default: null as string | null,
},
overridedDeviceKind: {
default: null as DeviceKind | null,
},
themes: {
default: [] as Theme[],
mergeStrategy: (a, b) => {
const mergedItems = [] as typeof a;
for (const x of a.concat(b)) {
const sameIdItem = mergedItems.find(y => y.id === x.id);
if (sameIdItem != null) {
if (deepEqual(x, sameIdItem)) { // 完全な重複は無視
continue;
} else { // IDは同じなのに内容が違う場合はマージ不可とする
throw new Error();
}
} else {
mergedItems.push(x);
}
}
return mergedItems;
pinnedUserLists: {
accountDependent: true,
default: [] as Misskey.entities.UserList[],
},
},
lightTheme: {
default: null as Theme | null,
},
darkTheme: {
default: null as Theme | null,
},
syncDeviceDarkMode: {
default: true,
},
defaultNoteVisibility: {
default: 'public' as (typeof Misskey.noteVisibilities)[number],
},
defaultNoteLocalOnly: {
default: false,
},
keepCw: {
default: true,
},
rememberNoteVisibility: {
default: false,
},
reportError: {
default: false,
},
collapseRenotes: {
default: true,
},
menu: {
default: [
'notifications',
'clips',
'drive',
'followRequests',
'chat',
'-',
'explore',
'announcements',
'channels',
'search',
'-',
'ui',
],
},
statusbars: {
default: [] as {
name: string;
id: string;
type: string;
size: 'verySmall' | 'small' | 'medium' | 'large' | 'veryLarge';
black: boolean;
props: Record<string, any>;
}[],
},
serverDisconnectedBehavior: {
default: 'quiet' as 'quiet' | 'reload' | 'dialog',
},
nsfw: {
default: 'respect' as 'respect' | 'force' | 'ignore',
},
highlightSensitiveMedia: {
default: false,
},
animation: {
default: !window.matchMedia('(prefers-reduced-motion)').matches,
},
animatedMfm: {
default: !window.matchMedia('(prefers-reduced-motion)').matches,
},
advancedMfm: {
default: true,
},
showReactionsCount: {
default: false,
},
enableQuickAddMfmFunction: {
default: false,
},
loadRawImages: {
default: false,
},
imageNewTab: {
default: false,
},
disableShowingAnimatedImages: {
default: window.matchMedia('(prefers-reduced-motion)').matches,
},
emojiStyle: {
default: 'twemoji', // twemoji / fluentEmoji / native
},
menuStyle: {
default: 'auto' as 'auto' | 'popup' | 'drawer',
},
useBlurEffectForModal: {
default: true,
},
useBlurEffect: {
default: true,
},
useStickyIcons: {
default: true,
},
enableHighQualityImagePlaceholders: {
default: true,
},
showFixedPostForm: {
default: false,
},
showFixedPostFormInChannel: {
default: false,
},
enableInfiniteScroll: {
default: true,
},
useReactionPickerForContextMenu: {
default: false,
},
instanceTicker: {
default: 'remote' as 'none' | 'remote' | 'always',
},
emojiPickerScale: {
default: 2,
},
emojiPickerWidth: {
default: 2,
},
emojiPickerHeight: {
default: 3,
},
emojiPickerStyle: {
default: 'auto' as 'auto' | 'popup' | 'drawer',
},
squareAvatars: {
default: false,
},
showAvatarDecorations: {
default: true,
},
numberOfPageCache: {
default: 3,
},
pollingInterval: {
uploadFolder: {
accountDependent: true,
default: null as string | null,
},
widgets: {
accountDependent: true,
default: () => [{
name: 'calendar',
id: genId(), place: 'right', data: {},
}, {
name: 'notifications',
id: genId(), place: 'right', data: {},
}, {
name: 'trends',
id: genId(), place: 'right', data: {},
}] as {
name: string;
id: string;
place: string | null;
data: Record<string, any>;
}[],
},
'deck.profile': {
accountDependent: true,
default: null as string | null,
},
'deck.profiles': {
accountDependent: true,
default: [] as DeckProfile[],
},
emojiPalettes: {
serverDependent: true,
default: () => [{
id: genId(),
name: '',
emojis: ['👍', '❤️', '😆', '🤔', '😮', '🎉', '💢', '😥', '😇', '🍮'],
}] as {
id: string;
name: string;
emojis: string[];
}[],
mergeStrategy: (a, b) => {
const mergedItems = [] as typeof a;
for (const x of a.concat(b)) {
const sameIdItem = mergedItems.find(y => y.id === x.id);
if (sameIdItem != null) {
if (deepEqual(x, sameIdItem)) { // 完全な重複は無視
continue;
} else { // IDは同じなのに内容が違う場合はマージ不可とする
throw new Error();
}
} else {
mergedItems.push(x);
}
}
return mergedItems;
},
},
emojiPaletteForReaction: {
serverDependent: true,
default: null as string | null,
},
emojiPaletteForMain: {
serverDependent: true,
default: null as string | null,
},
overridedDeviceKind: {
default: null as DeviceKind | null,
},
themes: {
default: [] as Theme[],
mergeStrategy: (a, b) => {
const mergedItems = [] as typeof a;
for (const x of a.concat(b)) {
const sameIdItem = mergedItems.find(y => y.id === x.id);
if (sameIdItem != null) {
if (deepEqual(x, sameIdItem)) { // 完全な重複は無視
continue;
} else { // IDは同じなのに内容が違う場合はマージ不可とする
throw new Error();
}
} else {
mergedItems.push(x);
}
}
return mergedItems;
},
},
lightTheme: {
default: null as Theme | null,
},
darkTheme: {
default: null as Theme | null,
},
syncDeviceDarkMode: {
default: true,
},
defaultNoteVisibility: {
default: 'public' as (typeof Misskey.noteVisibilities)[number],
},
defaultNoteLocalOnly: {
default: false,
},
keepCw: {
default: true,
},
rememberNoteVisibility: {
default: false,
},
reportError: {
default: false,
},
collapseRenotes: {
default: true,
},
menu: {
default: [
'notifications',
'clips',
'drive',
'followRequests',
'chat',
'-',
'explore',
'announcements',
'channels',
'search',
'-',
'ui',
],
},
statusbars: {
default: [] as {
name: string;
id: string;
type: string;
size: 'verySmall' | 'small' | 'medium' | 'large' | 'veryLarge';
black: boolean;
props: Record<string, any>;
}[],
},
serverDisconnectedBehavior: {
default: 'quiet' as 'quiet' | 'reload' | 'dialog',
},
nsfw: {
default: 'respect' as 'respect' | 'force' | 'ignore',
},
highlightSensitiveMedia: {
default: false,
},
animation: {
default: !window.matchMedia('(prefers-reduced-motion)').matches,
},
animatedMfm: {
default: !window.matchMedia('(prefers-reduced-motion)').matches,
},
advancedMfm: {
default: true,
},
showReactionsCount: {
default: false,
},
enableQuickAddMfmFunction: {
default: false,
},
loadRawImages: {
default: false,
},
imageNewTab: {
default: false,
},
disableShowingAnimatedImages: {
default: window.matchMedia('(prefers-reduced-motion)').matches,
},
emojiStyle: {
default: 'twemoji', // twemoji / fluentEmoji / native
},
menuStyle: {
default: 'auto' as 'auto' | 'popup' | 'drawer',
},
useBlurEffectForModal: {
default: true,
},
useBlurEffect: {
default: true,
},
useStickyIcons: {
default: true,
},
enableHighQualityImagePlaceholders: {
default: true,
},
showFixedPostForm: {
default: false,
},
showFixedPostFormInChannel: {
default: false,
},
enableInfiniteScroll: {
default: true,
},
useReactionPickerForContextMenu: {
default: false,
},
instanceTicker: {
default: 'remote' as 'none' | 'remote' | 'always',
},
emojiPickerScale: {
default: 2,
},
emojiPickerWidth: {
default: 2,
},
emojiPickerHeight: {
default: 3,
},
emojiPickerStyle: {
default: 'auto' as 'auto' | 'popup' | 'drawer',
},
squareAvatars: {
default: false,
},
showAvatarDecorations: {
default: true,
},
numberOfPageCache: {
default: 3,
},
pollingInterval: {
// 1 ... 低
// 2 ... 中
// 3 ... 高
default: 2,
},
showNoteActionsOnlyHover: {
default: false,
},
showClipButtonInNoteFooter: {
default: false,
},
reactionsDisplaySize: {
default: 'medium' as 'small' | 'medium' | 'large',
},
limitWidthOfReaction: {
default: true,
},
forceShowAds: {
default: false,
},
aiChanMode: {
default: false,
},
devMode: {
default: false,
},
mediaListWithOneImageAppearance: {
default: 'expand' as 'expand' | '16_9' | '1_1' | '2_3',
},
notificationPosition: {
default: 'rightBottom' as 'leftTop' | 'leftBottom' | 'rightTop' | 'rightBottom',
},
notificationStackAxis: {
default: 'horizontal' as 'vertical' | 'horizontal',
},
enableCondensedLine: {
default: true,
},
keepScreenOn: {
default: false,
},
useGroupedNotifications: {
default: true,
},
dataSaver: {
default: {
media: false,
avatar: false,
urlPreviewThumbnail: false,
disableUrlPreview: false,
code: false,
} satisfies Record<string, boolean>,
},
hemisphere: {
default: hemisphere as 'N' | 'S',
},
enableSeasonalScreenEffect: {
default: false,
},
enableHorizontalSwipe: {
default: false,
},
enablePullToRefresh: {
default: true,
},
useNativeUiForVideoAudioPlayer: {
default: false,
},
keepOriginalFilename: {
default: true,
},
alwaysConfirmFollow: {
default: true,
},
confirmWhenRevealingSensitiveMedia: {
default: false,
},
contextMenu: {
default: 'app' as 'app' | 'appWithShift' | 'native',
},
skipNoteRender: {
default: true,
},
showSoftWordMutedWord: {
default: false,
},
confirmOnReact: {
default: false,
},
defaultFollowWithReplies: {
default: false,
},
makeEveryTextElementsSelectable: {
default: DEFAULT_DEVICE_KIND === 'desktop',
},
showNavbarSubButtons: {
default: true,
},
showTitlebar: {
default: false,
},
showAvailableReactionsFirstInNote: {
default: false,
},
plugins: {
default: [] as Plugin[],
mergeStrategy: (a, b) => {
const sameIdExists = a.some(x => b.some(y => x.installId === y.installId));
if (sameIdExists) throw new Error();
const sameNameExists = a.some(x => b.some(y => x.name === y.name));
if (sameNameExists) throw new Error();
return a.concat(b);
default: 2,
},
},
mutingEmojis: {
default: [] as string[],
mergeStrategy: (a, b) => {
return [...new Set(a.concat(b))];
showNoteActionsOnlyHover: {
default: false,
},
},
watermarkPresets: {
accountDependent: true,
default: [] as WatermarkPreset[],
mergeStrategy: (a, b) => {
const mergedItems = [] as typeof a;
for (const x of a.concat(b)) {
const sameIdItem = mergedItems.find(y => y.id === x.id);
if (sameIdItem != null) {
if (deepEqual(x, sameIdItem)) { // 完全な重複は無視
continue;
} else { // IDは同じなのに内容が違う場合はマージ不可とする
throw new Error();
showClipButtonInNoteFooter: {
default: false,
},
reactionsDisplaySize: {
default: 'medium' as 'small' | 'medium' | 'large',
},
limitWidthOfReaction: {
default: true,
},
forceShowAds: {
default: false,
},
aiChanMode: {
default: false,
},
devMode: {
default: false,
},
mediaListWithOneImageAppearance: {
default: 'expand' as 'expand' | '16_9' | '1_1' | '2_3',
},
notificationPosition: {
default: 'rightBottom' as 'leftTop' | 'leftBottom' | 'rightTop' | 'rightBottom',
},
notificationStackAxis: {
default: 'horizontal' as 'vertical' | 'horizontal',
},
enableCondensedLine: {
default: true,
},
keepScreenOn: {
default: false,
},
useGroupedNotifications: {
default: true,
},
dataSaver: {
default: {
media: false,
avatar: false,
urlPreviewThumbnail: false,
disableUrlPreview: false,
code: false,
} satisfies Record<string, boolean>,
},
hemisphere: {
default: hemisphere as 'N' | 'S',
},
enableSeasonalScreenEffect: {
default: false,
},
enableHorizontalSwipe: {
default: false,
},
enablePullToRefresh: {
default: true,
},
useNativeUiForVideoAudioPlayer: {
default: false,
},
keepOriginalFilename: {
default: true,
},
alwaysConfirmFollow: {
default: true,
},
confirmWhenRevealingSensitiveMedia: {
default: false,
},
contextMenu: {
default: 'app' as 'app' | 'appWithShift' | 'native',
},
skipNoteRender: {
default: true,
},
showSoftWordMutedWord: {
default: false,
},
confirmOnReact: {
default: false,
},
defaultFollowWithReplies: {
default: false,
},
makeEveryTextElementsSelectable: {
default: DEFAULT_DEVICE_KIND === 'desktop',
},
showNavbarSubButtons: {
default: true,
},
showTitlebar: {
default: false,
},
showAvailableReactionsFirstInNote: {
default: false,
},
plugins: {
default: [] as Plugin[],
mergeStrategy: (a, b) => {
const sameIdExists = a.some(x => b.some(y => x.installId === y.installId));
if (sameIdExists) throw new Error();
const sameNameExists = a.some(x => b.some(y => x.name === y.name));
if (sameNameExists) throw new Error();
return a.concat(b);
},
},
mutingEmojis: {
default: [] as string[],
mergeStrategy: (a, b) => {
return [...new Set(a.concat(b))];
},
},
watermarkPresets: {
accountDependent: true,
default: [] as WatermarkPreset[],
mergeStrategy: (a, b) => {
const mergedItems = [] as typeof a;
for (const x of a.concat(b)) {
const sameIdItem = mergedItems.find(y => y.id === x.id);
if (sameIdItem != null) {
if (deepEqual(x, sameIdItem)) { // 完全な重複は無視
continue;
} else { // IDは同じなのに内容が違う場合はマージ不可とする
throw new Error();
}
} else {
mergedItems.push(x);
}
} else {
mergedItems.push(x);
}
}
return mergedItems;
return mergedItems;
},
},
defaultWatermarkPresetId: {
accountDependent: true,
default: null as WatermarkPreset['id'] | null,
},
defaultImageCompressionLevel: {
default: 2 as 0 | 1 | 2 | 3,
},
lowPowerMode: {
default: false,
},
'sound.masterVolume': {
default: 0.5,
},
'sound.notUseSound': {
default: false,
},
'sound.useSoundOnlyWhenActive': {
default: false,
},
'sound.on.note': {
default: { type: 'syuilo/n-aec', volume: 1 } as SoundStore,
},
'sound.on.noteMy': {
default: { type: 'syuilo/n-cea-4va', volume: 1 } as SoundStore,
},
'sound.on.notification': {
default: { type: 'syuilo/n-ea', volume: 1 } as SoundStore,
},
'sound.on.reaction': {
default: { type: 'syuilo/bubble2', volume: 1 } as SoundStore,
},
'sound.on.chatMessage': {
default: { type: 'syuilo/waon', volume: 1 } as SoundStore,
},
'deck.alwaysShowMainColumn': {
default: true,
},
'deck.navWindow': {
default: true,
},
'deck.useSimpleUiForNonRootPages': {
default: true,
},
'deck.columnAlign': {
default: 'center' as 'left' | 'right' | 'center',
},
'deck.columnGap': {
default: 6,
},
'deck.menuPosition': {
default: 'bottom' as 'right' | 'bottom',
},
'deck.navbarPosition': {
default: 'left' as 'left' | 'top' | 'bottom',
},
'deck.wallpaper': {
default: null as string | null,
},
'chat.showSenderName': {
default: false,
},
'chat.sendOnEnter': {
default: false,
},
'game.dropAndFusion': {
default: {
bgmVolume: 0.25,
sfxVolume: 1,
},
},
'experimental.stackingRouterView': {
default: false,
},
'experimental.enableFolderPageView': {
default: false,
},
},
defaultWatermarkPresetId: {
accountDependent: true,
default: null as WatermarkPreset['id'] | null,
},
defaultImageCompressionLevel: {
default: 2 as 0 | 1 | 2 | 3,
},
'sound.masterVolume': {
default: 0.5,
},
'sound.notUseSound': {
default: false,
},
'sound.useSoundOnlyWhenActive': {
default: false,
},
'sound.on.note': {
default: { type: 'syuilo/n-aec', volume: 1 } as SoundStore,
},
'sound.on.noteMy': {
default: { type: 'syuilo/n-cea-4va', volume: 1 } as SoundStore,
},
'sound.on.notification': {
default: { type: 'syuilo/n-ea', volume: 1 } as SoundStore,
},
'sound.on.reaction': {
default: { type: 'syuilo/bubble2', volume: 1 } as SoundStore,
},
'sound.on.chatMessage': {
default: { type: 'syuilo/waon', volume: 1 } as SoundStore,
},
'deck.alwaysShowMainColumn': {
default: true,
},
'deck.navWindow': {
default: true,
},
'deck.useSimpleUiForNonRootPages': {
default: true,
},
'deck.columnAlign': {
default: 'center' as 'left' | 'right' | 'center',
},
'deck.columnGap': {
default: 6,
},
'deck.menuPosition': {
default: 'bottom' as 'right' | 'bottom',
},
'deck.navbarPosition': {
default: 'left' as 'left' | 'top' | 'bottom',
},
'deck.wallpaper': {
default: null as string | null,
},
'chat.showSenderName': {
default: false,
},
'chat.sendOnEnter': {
default: false,
},
'game.dropAndFusion': {
default: {
bgmVolume: 0.25,
sfxVolume: 1,
},
},
'experimental.stackingRouterView': {
default: false,
},
'experimental.enableFolderPageView': {
default: false,
computed: {
disableShowingAnimatedImages: (s) => s.disableShowingAnimatedImages || s.lowPowerMode,
},
});

View File

@@ -101,9 +101,19 @@ type PreferencesDefinitionRecord<Default, T = Default extends (...args: any) =>
export type PreferencesDefinition = Record<string, PreferencesDefinitionRecord<any>>;
export function definePreferences<T extends Record<string, unknown>>(x: {
[K in keyof T]: PreferencesDefinitionRecord<T[K]>
states: {
[K in keyof T]: PreferencesDefinitionRecord<T[K]>;
};
computed: {
[K in keyof T]: PreferencesDefinitionRecord<T[K]>;
};
}): {
[K in keyof T]: PreferencesDefinitionRecord<T[K]>
states: {
[K in keyof T]: PreferencesDefinitionRecord<T[K]>;
};
computed: {
[K in keyof T]: PreferencesDefinitionRecord<T[K]>;
};
} {
return x;
}

View File

@@ -603,4 +603,4 @@ export const ROUTE_DEF = [{
}, {
path: '/:(*)',
component: page(() => import('@/pages/not-found.vue')),
}] satisfies RouteDef[];
}] as const satisfies RouteDef[];

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