diff --git a/.github/workflows/api-misskey-js.yml b/.github/workflows/api-misskey-js.yml index f362470b6a..1a35b86041 100644 --- a/.github/workflows/api-misskey-js.yml +++ b/.github/workflows/api-misskey-js.yml @@ -19,10 +19,10 @@ jobs: uses: actions/checkout@v6.0.2 - name: Setup pnpm - uses: pnpm/action-setup@v4.2.0 + uses: pnpm/action-setup@v4.4.0 - name: Setup Node.js - uses: actions/setup-node@v6.2.0 + uses: actions/setup-node@v6.3.0 with: node-version-file: '.node-version' cache: 'pnpm' diff --git a/.github/workflows/changelog-check.yml b/.github/workflows/changelog-check.yml index 3870b4b8a3..37664e950e 100644 --- a/.github/workflows/changelog-check.yml +++ b/.github/workflows/changelog-check.yml @@ -14,7 +14,7 @@ jobs: - name: Checkout head uses: actions/checkout@v6.0.2 - name: Setup Node.js - uses: actions/setup-node@v6.2.0 + uses: actions/setup-node@v6.3.0 with: node-version-file: '.node-version' diff --git a/.github/workflows/check-misskey-js-autogen.yml b/.github/workflows/check-misskey-js-autogen.yml index 0017f8dc7a..a31a4d85fa 100644 --- a/.github/workflows/check-misskey-js-autogen.yml +++ b/.github/workflows/check-misskey-js-autogen.yml @@ -29,7 +29,7 @@ jobs: - name: setup node id: setup-node - uses: actions/setup-node@v6.2.0 + uses: actions/setup-node@v6.3.0 with: node-version-file: '.node-version' cache: pnpm diff --git a/.github/workflows/get-api-diff.yml b/.github/workflows/get-api-diff.yml index 076d7400f2..c7ab3e2a29 100644 --- a/.github/workflows/get-api-diff.yml +++ b/.github/workflows/get-api-diff.yml @@ -30,9 +30,9 @@ jobs: ref: ${{ matrix.ref }} submodules: true - name: Setup pnpm - uses: pnpm/action-setup@v4.2.0 + uses: pnpm/action-setup@v4.4.0 - name: Use Node.js - uses: actions/setup-node@v6.2.0 + uses: actions/setup-node@v6.3.0 with: node-version-file: '.node-version' cache: 'pnpm' diff --git a/.github/workflows/get-backend-memory.yml b/.github/workflows/get-backend-memory.yml index b0f63954fa..0dcaaa8cb3 100644 --- a/.github/workflows/get-backend-memory.yml +++ b/.github/workflows/get-backend-memory.yml @@ -45,9 +45,9 @@ jobs: ref: ${{ matrix.ref }} submodules: true - name: Setup pnpm - uses: pnpm/action-setup@v4.2.0 + uses: pnpm/action-setup@v4.4.0 - name: Use Node.js - uses: actions/setup-node@v6.2.0 + uses: actions/setup-node@v6.3.0 with: node-version-file: '.node-version' cache: 'pnpm' diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index 54d741c8e4..0d9ac81314 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -41,8 +41,8 @@ jobs: fetch-depth: 0 submodules: true - name: Setup pnpm - uses: pnpm/action-setup@v4.2.0 - - uses: actions/setup-node@v6.2.0 + uses: pnpm/action-setup@v4.4.0 + - uses: actions/setup-node@v6.3.0 with: node-version-file: '.node-version' cache: 'pnpm' @@ -74,8 +74,8 @@ jobs: fetch-depth: 0 submodules: true - name: Setup pnpm - uses: pnpm/action-setup@v4.2.0 - - uses: actions/setup-node@v6.2.0 + uses: pnpm/action-setup@v4.4.0 + - uses: actions/setup-node@v6.3.0 with: node-version-file: '.node-version' cache: 'pnpm' @@ -105,8 +105,8 @@ jobs: fetch-depth: 0 submodules: true - name: Setup pnpm - uses: pnpm/action-setup@v4.2.0 - - uses: actions/setup-node@v6.2.0 + uses: pnpm/action-setup@v4.4.0 + - uses: actions/setup-node@v6.3.0 with: node-version-file: '.node-version' cache: 'pnpm' diff --git a/.github/workflows/locale.yml b/.github/workflows/locale.yml index cb8232024e..a965aae0d1 100644 --- a/.github/workflows/locale.yml +++ b/.github/workflows/locale.yml @@ -21,8 +21,8 @@ jobs: fetch-depth: 0 submodules: true - name: Setup pnpm - uses: pnpm/action-setup@v4.2.0 - - uses: actions/setup-node@v6.2.0 + uses: pnpm/action-setup@v4.4.0 + - uses: actions/setup-node@v6.3.0 with: node-version-file: ".node-version" cache: "pnpm" diff --git a/.github/workflows/on-release-created.yml b/.github/workflows/on-release-created.yml index 668d1fe2a3..7d19678574 100644 --- a/.github/workflows/on-release-created.yml +++ b/.github/workflows/on-release-created.yml @@ -20,9 +20,9 @@ jobs: with: submodules: true - name: Setup pnpm - uses: pnpm/action-setup@v4.2.0 + uses: pnpm/action-setup@v4.4.0 - name: Use Node.js - uses: actions/setup-node@v6.2.0 + uses: actions/setup-node@v6.3.0 with: node-version-file: '.node-version' cache: 'pnpm' diff --git a/.github/workflows/storybook.yml b/.github/workflows/storybook.yml index 57651810ee..0bfb7f4c9c 100644 --- a/.github/workflows/storybook.yml +++ b/.github/workflows/storybook.yml @@ -37,9 +37,9 @@ jobs: if: github.event_name == 'pull_request_target' run: git checkout "$(git rev-list --parents -n1 HEAD | cut -d" " -f3)" - name: Setup pnpm - uses: pnpm/action-setup@v4.2.0 + uses: pnpm/action-setup@v4.4.0 - name: Use Node.js - uses: actions/setup-node@v6.2.0 + uses: actions/setup-node@v6.3.0 with: node-version-file: '.node-version' cache: 'pnpm' diff --git a/.github/workflows/test-backend.yml b/.github/workflows/test-backend.yml index 0fab5d5f31..29e634f84b 100644 --- a/.github/workflows/test-backend.yml +++ b/.github/workflows/test-backend.yml @@ -49,7 +49,7 @@ jobs: ports: - 56312:6379 meilisearch: - image: getmeili/meilisearch:v1.36.0 + image: getmeili/meilisearch:v1.38.2 ports: - 57712:7700 env: @@ -61,7 +61,7 @@ jobs: with: submodules: true - name: Setup pnpm - uses: pnpm/action-setup@v4.2.0 + uses: pnpm/action-setup@v4.4.0 - name: Get current date id: current-date run: echo "today=$(date +'%Y-%m-%d')" >> $GITHUB_OUTPUT @@ -93,7 +93,7 @@ jobs: fi done - name: Use Node.js - uses: actions/setup-node@v6.2.0 + uses: actions/setup-node@v6.3.0 with: node-version-file: ${{ matrix.node-version-file }} cache: 'pnpm' @@ -140,9 +140,9 @@ jobs: with: submodules: true - name: Setup pnpm - uses: pnpm/action-setup@v4.2.0 + uses: pnpm/action-setup@v4.4.0 - name: Use Node.js - uses: actions/setup-node@v6.2.0 + uses: actions/setup-node@v6.3.0 with: node-version-file: ${{ matrix.node-version-file }} cache: 'pnpm' @@ -184,12 +184,12 @@ jobs: with: submodules: true - name: Setup pnpm - uses: pnpm/action-setup@v4.2.0 + uses: pnpm/action-setup@v4.4.0 - name: Get current date id: current-date run: echo "today=$(date +'%Y-%m-%d')" >> $GITHUB_OUTPUT - name: Use Node.js - uses: actions/setup-node@v6.2.0 + uses: actions/setup-node@v6.3.0 with: node-version-file: ${{ matrix.node-version-file }} cache: 'pnpm' diff --git a/.github/workflows/test-federation.yml b/.github/workflows/test-federation.yml index 1f0325991e..27049ecd42 100644 --- a/.github/workflows/test-federation.yml +++ b/.github/workflows/test-federation.yml @@ -36,7 +36,7 @@ jobs: with: submodules: true - name: Setup pnpm - uses: pnpm/action-setup@v4.2.0 + uses: pnpm/action-setup@v4.4.0 - name: Get current date id: current-date run: echo "today=$(date +'%Y-%m-%d')" >> $GITHUB_OUTPUT @@ -68,7 +68,7 @@ jobs: fi done - name: Use Node.js - uses: actions/setup-node@v6.2.0 + uses: actions/setup-node@v6.3.0 with: node-version-file: ${{ matrix.node-version-file }} cache: 'pnpm' diff --git a/.github/workflows/test-frontend.yml b/.github/workflows/test-frontend.yml index b5a292801d..1125565d8b 100644 --- a/.github/workflows/test-frontend.yml +++ b/.github/workflows/test-frontend.yml @@ -32,9 +32,9 @@ jobs: with: submodules: true - name: Setup pnpm - uses: pnpm/action-setup@v4.2.0 + uses: pnpm/action-setup@v4.4.0 - name: Use Node.js - uses: actions/setup-node@v6.2.0 + uses: actions/setup-node@v6.3.0 with: node-version-file: '.node-version' cache: 'pnpm' @@ -86,9 +86,9 @@ jobs: #- uses: browser-actions/setup-firefox@latest # if: ${{ matrix.browser == 'firefox' }} - name: Setup pnpm - uses: pnpm/action-setup@v4.2.0 + uses: pnpm/action-setup@v4.4.0 - name: Use Node.js - uses: actions/setup-node@v6.2.0 + uses: actions/setup-node@v6.3.0 with: node-version-file: '.node-version' cache: 'pnpm' diff --git a/.github/workflows/test-misskey-js.yml b/.github/workflows/test-misskey-js.yml index cdce5b9e19..54cf1c318a 100644 --- a/.github/workflows/test-misskey-js.yml +++ b/.github/workflows/test-misskey-js.yml @@ -25,10 +25,10 @@ jobs: uses: actions/checkout@v6.0.2 - name: Setup pnpm - uses: pnpm/action-setup@v4.2.0 + uses: pnpm/action-setup@v4.4.0 - name: Setup Node.js - uses: actions/setup-node@v6.2.0 + uses: actions/setup-node@v6.3.0 with: node-version-file: '.node-version' cache: 'pnpm' diff --git a/.github/workflows/test-production.yml b/.github/workflows/test-production.yml index 3501afb451..319ff6e5f8 100644 --- a/.github/workflows/test-production.yml +++ b/.github/workflows/test-production.yml @@ -20,9 +20,9 @@ jobs: with: submodules: true - name: Setup pnpm - uses: pnpm/action-setup@v4.2.0 + uses: pnpm/action-setup@v4.4.0 - name: Use Node.js - uses: actions/setup-node@v6.2.0 + uses: actions/setup-node@v6.3.0 with: node-version-file: '.node-version' cache: 'pnpm' diff --git a/.github/workflows/validate-api-json.yml b/.github/workflows/validate-api-json.yml index f617ff169b..f2e8381344 100644 --- a/.github/workflows/validate-api-json.yml +++ b/.github/workflows/validate-api-json.yml @@ -21,9 +21,9 @@ jobs: with: submodules: true - name: Setup pnpm - uses: pnpm/action-setup@v4.2.0 + uses: pnpm/action-setup@v4.4.0 - name: Use Node.js - uses: actions/setup-node@v6.2.0 + uses: actions/setup-node@v6.3.0 with: node-version-file: '.node-version' cache: 'pnpm' diff --git a/CHANGELOG.md b/CHANGELOG.md index 1e6a582700..9ab691e32d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,10 +4,16 @@ - 依存関係の更新 ### Client -- +- Enhance: アプリ内ウィンドウの初期サイズを画面サイズに応じて自動で調整するように +- Fix: 絵文字パレットが空の状態でMisskeyについてのページが閲覧できない問題を修正 +- Fix: ウィンドウのタイトルをクリックしても最前面に出ないことがある問題を修正 ### Server - Fix: 自分の行ったフォロワー限定投稿または指名投稿に自分自身でリアクションなどを行った場合のイベントが流れない問題を修正 +- Fix: 署名付きGETリクエストにおいてAcceptヘッダを署名の対象から除外(Acceptヘッダを正規化するCDNやリバースプロキシを使用している際に挙動がおかしくなる問題を修正) +- Fix: WebSocket接続におけるノートの非表示ロジックを修正 +- Fix: チャンネルミュートを有効にしている際に、一部のタイムラインやノート一覧が空になる問題を修正 +- Fix: 初期読込時に必要なフロントエンドのアセットがすべて読み込まれていない問題を修正 ## 2026.3.1 diff --git a/locales/ca-ES.yml b/locales/ca-ES.yml index 5bd4b01b27..f2867585c2 100644 --- a/locales/ca-ES.yml +++ b/locales/ca-ES.yml @@ -264,7 +264,7 @@ noJobs: "No hi ha feines" federating: "Federant" blocked: "Bloquejat" suspended: "Anul·lar subscripció " -all: "tot" +all: "Tot" subscribing: "Subscrit a" publishing: "S'està publicant" notResponding: "Sense resposta" @@ -3401,11 +3401,9 @@ _imageEffector: threshold: "Llindar" centerX: "Centre de X" centerY: "Centre de Y" - zoomLinesSmoothing: "Suavitzat" - zoomLinesSmoothingDescription: "Els paràmetres de suavitzat i amplada de línia en augmentar no es poden fer servir junts." - zoomLinesThreshold: "Amplada de línia a l'augmentar " + density: "Densitat" + zoomLinesOutlineThickness: "Amplada de les vores exteriors" zoomLinesMaskSize: "Diàmetre del centre" - zoomLinesBlack: "Obscurir" circle: "Cercle" drafts: "Esborrany " _drafts: diff --git a/locales/cs-CZ.yml b/locales/cs-CZ.yml index 22962a4243..c8d2ff6565 100644 --- a/locales/cs-CZ.yml +++ b/locales/cs-CZ.yml @@ -130,6 +130,7 @@ reactions: "Reakce" reactionSettingDescription2: "Přetažením změníte pořadí, kliknutím smažete, zmáčkněte \"+\" k přidání" rememberNoteVisibility: "Zapamatovat nastavení zobrazení poznámky" attachCancel: "Odstranit přílohu" +deleteFile: "Smazat soubor" markAsSensitive: "Označit jako NSFW" unmarkAsSensitive: "Odznačit jako NSFW" enterFileName: "Zadejte název souboru" @@ -205,6 +206,7 @@ blockThisInstance: "Blokovat tuto instanci" silenceThisInstance: "Utišit tuto instanci" operations: "Operace" software: "Software" +softwareName: "Software" version: "Verze" metadata: "Metadata" withNFiles: "{n} soubor(ů)" @@ -231,6 +233,7 @@ noteDeleteConfirm: "Jste si jistí že chcete smazat tuhle poznámku?" pinLimitExceeded: "Nemůžete připnout další poznámky." done: "Hotovo" processing: "Zpracovávám" +preprocessing: "Připravuji..." preview: "Náhled" default: "Výchozí" defaultValueIs: "Základní hodnota: {value}" @@ -265,6 +268,7 @@ removed: "Smazáno" removeAreYouSure: "Jste si jistí že chcete smazat \"{x}\"?" deleteAreYouSure: "Jste si jistí že chcete smazat \"{x}\"?" resetAreYouSure: "Opravdu resetovat?" +areYouSure: "Jste si jistí?" saved: "Uloženo" upload: "Nahrát soubory" keepOriginalUploading: "Ponechat originální obrázek" @@ -275,9 +279,12 @@ uploadFromUrl: "Nahrát z URL adresy" uploadFromUrlDescription: "URL adresa souboru, který chcete nahrát" uploadFromUrlRequested: "Upload zažádán" uploadFromUrlMayTakeTime: "Může trvat nějakou dobu, dokud nebude dokončeno nahrávání." +uploadNFiles: "Uploadovat {n} souborů" explore: "Objevovat" messageRead: "Přečtené" +readAllChatMessages: "Označit všechny zprávy za přečtené" noMoreHistory: "To je vše" +startChat: "Začít chat" nUsersRead: "přečteno {n} uživateli" agreeTo: "Souhlasím s {0}" agree: "Souhlasím" @@ -308,12 +315,15 @@ selectFile: "Vybrat soubor" selectFiles: "Vybrat soubory" selectFolder: "Vyberte složku" selectFolders: "Vyberte složky" +fileNotSelected: "Nebyl vybrán žádný soubor" renameFile: "Přejmenovat soubor" folderName: "Název složky" createFolder: "Vytvořit složku" renameFolder: "Přejmenovat složku" deleteFolder: "Odstranit složku" +folder: "Složka " addFile: "Přidat soubor" +showFile: "Procházet soubory" emptyDrive: "Váš disk je prázdný" emptyFolder: "Tato složka je prázdná" unableToDelete: "Nelze smazat" @@ -424,6 +434,7 @@ totp: "Ověřovací aplikace" totpDescription: "Použít ověřovací aplikaci pro použití jednorázových hesel" moderator: "Moderátor" moderation: "Moderování" +moderationNote: "Poznámka moderátora" nUsersMentioned: "{n} uživatelů zmínilo" securityKeyAndPasskey: "Bezpečnostní klíče a tokeny" securityKey: "Bezpečnostní klíč" @@ -479,7 +490,9 @@ uiLanguage: "Jazyk uživatelského rozhraní" aboutX: "O {x}" emojiStyle: "Styl emoji" native: "Výchozí" +menuStyle: "Styl nabídky" style: "Vzhled" +drawer: "Boční menu" popup: "Vyskakovací okno" showNoteActionsOnlyHover: "Zobrazit akce poznámky jenom při naběhnutí myši" noHistory: "Žádná historie" @@ -535,6 +548,7 @@ deleteAll: "Smazat vše" showFixedPostForm: "Zobrazit formulář pro nové příspěvky nad časovou osou" showFixedPostFormInChannel: "Zobrazit vkládací formulář na vrcholu časové osy (Kanály)" newNoteRecived: "Jsou k dispozici nové poznámky" +newNote: "Nová poznámka" sounds: "Zvuky" sound: "Zvuky" listen: "Poslouchat" @@ -614,6 +628,7 @@ medium: "Střední" small: "Malé" generateAccessToken: "Vygenerovat přístupový token" permission: "Oprávnění" +adminPermission: "Administrátorská práva" enableAll: "Povolit vše" disableAll: "Vypnout vše" tokenRequested: "Povolit přístup k účtu" @@ -889,6 +904,9 @@ oneHour: "1 hodina" oneDay: "1 den" oneWeek: "1 týden" oneMonth: "1 měsíc" +threeMonths: "3 měsíce" +oneYear: "1 rok" +threeDays: "3 dny" reflectMayTakeTime: "Může trvat nějakou dobu, než se projeví změny." failedToFetchAccountInformation: "Nepodařily se načíst informace o účtě" rateLimitExceeded: "Překročení rychlostního limitu" @@ -1026,6 +1044,8 @@ showClipButtonInNoteFooter: "Přidat \"Připnout\" do akčního menu poznámky" noteIdOrUrl: "ID nebo URL poznámky" video: "Video" videos: "Videa" +audio: "Zvuk" +audioFiles: "Zvuk" dataSaver: "Spořič dat" accountMigration: "Migrace účtu" accountMoved: "Tenhle uživatel se přesunul na nový účet:" @@ -1053,6 +1073,8 @@ preservedUsernames: "Rezervované uživatelské jména" preservedUsernamesDescription: "Seznam uživatelských jmén na rezervaci oddělené mezerama. Tyhle jména se potom nebudou moc použít při normálním procesu vytvoření účtu ale můžou být použiti manuálně administratorém. Existujících účtů se to nedotkne." createNoteFromTheFile: "Vytvořit poznámku z tohodle souboru" archive: "Archiv" +archived: "Archivované" +unarchive: "Obnovit" channelArchiveConfirmTitle: "Opravdu chcete archivovat {name}?" channelArchiveConfirmDescription: "Archivovaný kanál se objeví v seznamu kanálů nebo ve výsledcích hledání. Nové poznámky se nedají vložit do seznamu." thisChannelArchived: "Tenhle kanál je archivovaný" @@ -1099,6 +1121,7 @@ doYouAgree: "Souhlasíte?" beSureToReadThisAsItIsImportant: "Přečtěte si prosím tyto důležité informace." iHaveReadXCarefullyAndAgree: "Přečetl jsem si text \"{x}\" a souhlasím s ním." icon: "Avatar" +forYou: "Pro vás" replies: "Odpovědět" renotes: "Přeposlat" sourceCode: "Zdrojový kód" diff --git a/locales/de-DE.yml b/locales/de-DE.yml index a0591b4931..cc645d83cf 100644 --- a/locales/de-DE.yml +++ b/locales/de-DE.yml @@ -3401,11 +3401,7 @@ _imageEffector: threshold: "Schwellenwert" centerX: "Zentrum X" centerY: "Zentrum Y" - zoomLinesSmoothing: "Glättung" - zoomLinesSmoothingDescription: "Die Einstellungen für die Glättung und für die Breite der Konzentrationslinien können nicht gleichzeitig verwendet werden." - zoomLinesThreshold: "Breite der Konzentrationslinien" zoomLinesMaskSize: "Mitteldurchmesser" - zoomLinesBlack: "Schwarz machen" circle: "Kreisförmig" drafts: "Entwurf" _drafts: diff --git a/locales/en-US.yml b/locales/en-US.yml index d08611fd23..a9729b2ce3 100644 --- a/locales/en-US.yml +++ b/locales/en-US.yml @@ -3401,11 +3401,7 @@ _imageEffector: threshold: "Threshold" centerX: "Center X" centerY: "Center Y" - zoomLinesSmoothing: "Smoothing" - zoomLinesSmoothingDescription: "Smoothing and zoom line width cannot be used together." - zoomLinesThreshold: "Zoom line width" zoomLinesMaskSize: "Center diameter" - zoomLinesBlack: "Make black" circle: "Circular" drafts: "Drafts" _drafts: diff --git a/locales/es-ES.yml b/locales/es-ES.yml index 2d8c39d91a..72b7892128 100644 --- a/locales/es-ES.yml +++ b/locales/es-ES.yml @@ -3401,11 +3401,9 @@ _imageEffector: threshold: "Umbral" centerX: "Centrar X" centerY: "Centrar Y" - zoomLinesSmoothing: "Suavizado" - zoomLinesSmoothingDescription: "El suavizado y el ancho de línea de zoom no se pueden utilizar juntos." - zoomLinesThreshold: "Ancho de línea del zoom" + density: "Densidad" + zoomLinesOutlineThickness: "Grosor del borde" zoomLinesMaskSize: "Diámetro del centro" - zoomLinesBlack: "Cambiar color de las líneas de impacto a negro." circle: "Círculo" drafts: "Borrador" _drafts: diff --git a/locales/it-IT.yml b/locales/it-IT.yml index db9bb481c7..2401bd84aa 100644 --- a/locales/it-IT.yml +++ b/locales/it-IT.yml @@ -3401,11 +3401,9 @@ _imageEffector: threshold: "Soglia" centerX: "Centro orizzontale" centerY: "Centro verticale" - zoomLinesSmoothing: "Levigatura" - zoomLinesSmoothingDescription: "Non si possono usare insieme la levigatura e la larghezza della linea centrale." - zoomLinesThreshold: "Limite delle linee zoom" + density: "Densità" + zoomLinesOutlineThickness: "Spessore del bordo" zoomLinesMaskSize: "Ampiezza del diametro" - zoomLinesBlack: "Bande nere" circle: "Circolare" drafts: "Bozze" _drafts: diff --git a/locales/ko-KR.yml b/locales/ko-KR.yml index bf8a851884..52da6d071a 100644 --- a/locales/ko-KR.yml +++ b/locales/ko-KR.yml @@ -1466,17 +1466,17 @@ _chat: newMessage: "새로운 메시지" individualChat: "개인 대화" individualChat_description: "특정 유저와 일대일 채팅을 할 수 있습니다." - roomChat: "룸 채팅" + roomChat: "그룹 채팅" roomChat_description: "여러 명이 함께 채팅할 수 있습니다.\n또한, 개인 채팅을 허용하지 않은 유저와도 상대방이 수락하면 채팅을 할 수 있습니다." - createRoom: "룸을 생성" + createRoom: "방 만들기" inviteUserToChat: "유저를 초대하여 채팅을 시작하세요" - yourRooms: "생성한 룸" - joiningRooms: "참가 중인 룸" + yourRooms: "만들어진 방" + joiningRooms: "참가 중인 방" invitations: "초대" noInvitations: "초대장이 없습니다" history: "이력" noHistory: "기록이 없습니다" - noRooms: "룸이 없습니다" + noRooms: "방이 없습니다" inviteUser: "유저를 초대" sentInvitations: "초대를 보내기" join: "참여" @@ -1487,14 +1487,14 @@ _chat: home: "홈" send: "전송" newline: "줄바꿈" - muteThisRoom: "이 룸을 뮤트" - deleteRoom: "룸을 삭제" + muteThisRoom: "이 방을 뮤트하기" + deleteRoom: "방을 삭제하기" chatNotAvailableForThisAccountOrServer: "이 서버 또는 이 계정에서 채팅이 활성화되어 있지 않습니다." chatIsReadOnlyForThisAccountOrServer: "이 서버 또는 이 계정에서 채팅은 읽기 전용입니다. 새로 쓰거나 채팅 룸을 만들거나 참가할 수 없습니다." chatNotAvailableInOtherAccount: "상대방 계정에서 채팅 기능을 사용할 수 없는 상태입니다." cannotChatWithTheUser: "이 유저와 채팅을 시작할 수 없습니다" cannotChatWithTheUser_description: "채팅을 사용할 수 없는 상태이거나 상대방이 채팅을 열지 않은 상태입니다." - youAreNotAMemberOfThisRoomButInvited: "당신은 이 룸의 참가자가 아닙니다만 초대 신청을 받으셨습니다. 참가하려면 초대를 수락해주십시오." + youAreNotAMemberOfThisRoomButInvited: "이 방의 참가자가 아니지만 초대를 받았습니다. 참가하려면 초대를 수락하세요." doYouAcceptInvitation: "초대를 수락하시겠습니까?" chatWithThisUser: "채팅하기" thisUserAllowsChatOnlyFromFollowers: "이 유저는 팔로워만 채팅을 할 수 있습니다." @@ -2544,7 +2544,7 @@ _widgets: _userList: chooseList: "리스트 선택" clicker: "클리커" - birthdayFollowings: "오늘이 생일인 유저" + birthdayFollowings: "곧 생일인 사용자" chat: "채팅하기" _widgetOptions: showHeader: "해더를 표시" @@ -2792,7 +2792,7 @@ _notification: newNote: "새 게시물" unreadAntennaNote: "안테나 {name}" roleAssigned: "역할이 부여 되었습니다." - chatRoomInvitationReceived: "채팅 룸에 초대받았습니다" + chatRoomInvitationReceived: "채팅방에 초대되었습니다" emptyPushNotificationMessage: "푸시 알림이 갱신되었습니다" achievementEarned: "도전 과제를 달성했습니다" testNotification: "알림 테스트" @@ -2823,7 +2823,7 @@ _notification: receiveFollowRequest: "팔로우 요청을 받았을 때" followRequestAccepted: "팔로우 요청이 승인되었을 때" roleAssigned: "역할이 부여됨" - chatRoomInvitationReceived: "채팅 룸에 초대받음" + chatRoomInvitationReceived: "채팅방에 초대됨" achievementEarned: "도전 과제 획득" exportCompleted: "추출을 성공함" login: "로그인" @@ -2977,7 +2977,7 @@ _moderationLogTypes: deletePage: "페이지를 삭제" deleteFlash: "Play를 삭제" deleteGalleryPost: "갤러리 게시물을 삭제" - deleteChatRoom: "채팅 룸 삭제" + deleteChatRoom: "채팅방 삭제하기" updateProxyAccountDescription: "프록시 계정의 설명 업데이트" _fileViewer: title: "파일 상세" @@ -3401,11 +3401,9 @@ _imageEffector: threshold: "한계 값" centerX: "X축 중심" centerY: "Y축 중심" - zoomLinesSmoothing: "다듬기" - zoomLinesSmoothingDescription: "다듬기와 집중선 폭 설정은 같이 쓸 수 없습니다." - zoomLinesThreshold: "집중선 폭" + density: "밀도" + zoomLinesOutlineThickness: "선 그림자의 굵기" zoomLinesMaskSize: "중앙 값" - zoomLinesBlack: "검은색으로 하기" circle: "원형" drafts: "초안" _drafts: diff --git a/locales/pt-PT.yml b/locales/pt-PT.yml index 7552852c4e..0fba19df40 100644 --- a/locales/pt-PT.yml +++ b/locales/pt-PT.yml @@ -3289,11 +3289,7 @@ _imageEffector: threshold: "Limiar" centerX: "Centralizar X" centerY: "Centralizar Y" - zoomLinesSmoothing: "Suavização" - zoomLinesSmoothingDescription: "Suavização e largura das linhas de zoom não podem ser utilizados simultaneamente." - zoomLinesThreshold: "Largura das linhas de zoom" zoomLinesMaskSize: "Diâmetro do centro" - zoomLinesBlack: "Linhas pretas" circle: "Circular" drafts: "Rascunhos" _drafts: diff --git a/locales/th-TH.yml b/locales/th-TH.yml index 4a58abe521..6bcff59979 100644 --- a/locales/th-TH.yml +++ b/locales/th-TH.yml @@ -3401,11 +3401,7 @@ _imageEffector: threshold: "เทรชโฮลด์" centerX: "กลาง X" centerY: "กลาง Y" - zoomLinesSmoothing: "ทำให้สมูธ" - zoomLinesSmoothingDescription: "ตั้งให้สมูธไม่สามารถใช้ร่วมกับตั้งความกว้างเส้นรวมศูนย์ได้" - zoomLinesThreshold: "ความกว้างเส้นรวมศูนย์" zoomLinesMaskSize: "ขนาดพื้นที่ตรงกลาง" - zoomLinesBlack: "ทำให้ดำ" circle: "ทรงกลม" drafts: "ร่าง" _drafts: diff --git a/locales/tr-TR.yml b/locales/tr-TR.yml index 94e01df0fb..b05c62bb25 100644 --- a/locales/tr-TR.yml +++ b/locales/tr-TR.yml @@ -83,6 +83,8 @@ files: "Dosyalar" download: "İndir" driveFileDeleteConfirm: "“{name}” dosyasını silmek istediğinden emin misin? Bu dosyaya ekli tüm notlar da silinecek." unfollowConfirm: "{name} kullanıcısını cidden takipden çıkmak istiyor musun?" +cancelFollowRequestConfirm: "{name} adlı kişiye gönderdiğiniz takip isteğini iptal etmek ister misiniz?" +rejectFollowRequestConfirm: "{name} adlı kullanıcının takip isteğini reddetmek istiyor musunuz?" exportRequested: "Dışa aktarma işlemi talep ettin. Bu işlem biraz zaman alabilir. İşlem tamamlandığında Drive'ına eklenecek." importRequested: "İçe aktarma talebinde bulundun. Bu işlem biraz zaman alabilir." lists: "Listeler" @@ -253,6 +255,7 @@ noteDeleteConfirm: "Bu notu silmek istediğinden emin misin?" pinLimitExceeded: "Artık daha fazla not sabitleyemezsin" done: "Tamam" processing: "İşleniyor..." +preprocessing: "Hazırlık aşamasında" preview: "Önizleme" default: "Varsayılan" defaultValueIs: "Varsayılan: {value}" @@ -301,6 +304,7 @@ uploadFromUrlMayTakeTime: "Yükleme işleminin tamamlanması biraz zaman alabili uploadNFiles: "{n} dosya yükle" explore: "Keşfet" messageRead: "Oku" +readAllChatMessages: "Tüm mesajları okundu olarak işaretle" noMoreHistory: "Daha fazla geçmiş bilgisi yok." startChat: "Sohbete başla" nUsersRead: "{n} tarafından okundu" @@ -333,6 +337,7 @@ fileName: "Dosya adı" selectFile: "Dosya seçin" selectFiles: "Dosyaları seçin" selectFolder: "Klasör seçin" +unselectFolder: "Klasör seçimini kaldır" selectFolders: "Klasörleri seçin" fileNotSelected: "Hiç dosya seçilmedi" renameFile: "Dosyayı yeniden adlandır" @@ -345,6 +350,7 @@ addFile: "Bir dosya ekle" showFile: "Dosyaları göster" emptyDrive: "Drive boş" emptyFolder: "Bu klasör boş" +dropHereToUpload: "Yüklemek için dosyalarınızı buraya sürükleyin." unableToDelete: "Silinemiyor" inputNewFileName: "Yeni bir dosya adı girin" inputNewDescription: "Yeni alternatif metin girin" @@ -537,6 +543,7 @@ regenerate: "Yeniden oluştur" fontSize: "Yazı tipi boyutu" mediaListWithOneImageAppearance: "Tek bir resim içeren medya listelerinin yüksekliği" limitTo: "{x} ile sınırlandır" +showMediaListByGridInWideArea: "Ekran genişliği geniş olduğunda, medya listesi yatay olarak görüntülenecektir." noFollowRequests: "Bekleyen takip istekleri yok." openImageInNewTab: "Görüntüleri yeni sekmede aç" dashboard: "Gösterge paneli" @@ -772,6 +779,7 @@ lockedAccountInfo: "Notunuzun görünürlüğünü “Yalnızca takipçiler” o alwaysMarkSensitive: "Varsayılan olarak hassas olarak işaretle" loadRawImages: "Küçük resimleri göstermek yerine orijinal resimleri yükle" disableShowingAnimatedImages: "Animasyonlu görüntüleri oynatmayın" +disableShowingAnimatedImages_caption: "Bu ayara rağmen animasyonlu görüntüler oynatılmıyorsa, bunun nedeni tarayıcınızın veya işletim sisteminizin erişilebilirlik ayarları veya güç tasarrufu ayarlarından kaynaklanan parazit olabilir." highlightSensitiveMedia: "Hassas medyayı vurgulayın" verificationEmailSent: "Doğrulama e-postası gönderildi. Doğrulamayı tamamlamak için e-postadaki bağlantıyı takip edin." notSet: "Ayarlı değil" @@ -1018,6 +1026,9 @@ pushNotificationAlreadySubscribed: "Push bildirimleri zaten açık" pushNotificationNotSupported: "Push bildirimleri sunucu veya tarayıcı tarafından desteklenmiyor" sendPushNotificationReadMessage: "Okunduktan sonra push bildirimlerini silin" sendPushNotificationReadMessageCaption: "Bu, cihazınızın güç tüketimini artırabilir." +pleaseAllowPushNotification: "Lütfen tarayıcı ayarlarınızdan bildirimlere izin verin." +browserPushNotificationDisabled: "Bildirim gönderme izni alınamadı." +browserPushNotificationDisabledDescription: "{serverName} sunucusundan bildirim gönderme izniniz yok. Lütfen tarayıcı ayarlarınızdan bildirimlere izin verin ve tekrar deneyin." windowMaximize: "Maksimize et" windowMinimize: "Minimize et" windowRestore: "Geri yükle" @@ -1168,6 +1179,7 @@ installed: "Yüklendi" branding: "Markalaşma" enableServerMachineStats: "Sunucu donanım istatistiklerini yayınla" enableIdenticonGeneration: "Kullanıcı identicon oluşturmayı etkinleştir" +showRoleBadgesOfRemoteUsers: "Uzaktan kullanıcılara verilen rol rozetlerini görüntüle" turnOffToImprovePerformance: "Devre dışı bırakma, daha yüksek performansa yol açabilir." createInviteCode: "Davet Kodu oluştur" createWithOptions: "Seçeneklerle oluştur" @@ -1316,6 +1328,7 @@ acknowledgeNotesAndEnable: "Önlemleri anladıktan sonra açın." federationSpecified: "Bu sunucu, beyaz liste federasyonunda çalıştırılmaktadır. Yönetici tarafından belirlenen sunucular dışında diğer sunucularla etkileşim kurmak yasaktır." federationDisabled: "Bu sunucuda federasyon devre dışıdır. Diğer sunuculardaki kullanıcılarla etkileşim kuramazsınız." draft: "Taslaklar" +draftsAndScheduledNotes: "Taslaklar ve planlanmış gönderiler" confirmOnReact: "Tepki verirken onaylayın" reactAreYouSure: "“{emoji}” tepkisini eklemek ister misin?" markAsSensitiveConfirm: "Bu medyayı hassas olarak ayarlamak ister misin?" @@ -1344,6 +1357,7 @@ textCount: "Karakter sayısı" information: "Hakkında" chat: "Sohbet" directMessage: "Kullanıcıyla sohbet et" +directMessage_short: "Mesaj" migrateOldSettings: "Eski istemci ayarlarını taşıma" migrateOldSettings_description: "Bu işlem otomatik olarak yapılmalıdır, ancak herhangi bir nedenle geçiş başarısız olursa, geçiş işlemini manuel olarak kendin başlatabilirsin. Mevcut yapılandırma bilgileri üzerine yazılacaktır." compress: "Sıkıştır" @@ -1371,6 +1385,8 @@ redisplayAllTips: "Tüm “İpucu & Püf Nokta” tekrar göster" hideAllTips: "Tüm “İpucu & Püf Nokta” gizle" defaultImageCompressionLevel: "Varsayılan görüntü sıkıştırma düzeyi" defaultImageCompressionLevel_description: "Düşük seviye görüntü kalitesini korur ancak dosya boyutunu artırır.
Yüksek seviye dosya boyutunu azaltır ancak görüntü kalitesini düşürür." +defaultCompressionLevel: "Varsayılan sıkıştırma seviyesi" +defaultCompressionLevel_description: "Ayarı düşürmek kaliteyi koruyacak ancak dosya boyutunu artıracaktır.
Ayarı yükseltmek dosya boyutunu küçültecek ancak kaliteyi düşürecektir." inMinutes: "Dakika(lar)" inDays: "Gün(ler)" safeModeEnabled: "Güvenli mod etkinleştirildi" @@ -1378,21 +1394,74 @@ pluginsAreDisabledBecauseSafeMode: "Güvenli mod etkinleştirildiği için tüm customCssIsDisabledBecauseSafeMode: "Güvenli mod etkin olduğu için özel CSS uygulanmıyor." themeIsDefaultBecauseSafeMode: "Güvenli mod etkinken, varsayılan tema kullanılır. Güvenli modu devre dışı bırakmak bu değişiklikleri geri alır." thankYouForTestingBeta: "Beta sürümünü test ettiğin için teşekkür ederiz!" +createUserSpecifiedNote: "Kullanıcı tarafından belirtilen notlar oluşturun" +schedulePost: "Bir gönderi planla" +scheduleToPostOnX: "{x} için bir gönderi planla" +scheduledToPostOnX: "{x} için bir gönderi planlandı." +schedule: "rezervasyon" +scheduled: "rezervasyon" widgets: "Widget'lar" +deviceInfo: "Cihaz Bilgileri" +deviceInfoDescription: "Teknik bir sorunuz olduğunda, aşağıdaki bilgileri eklemeniz sorunun çözülmesine yardımcı olabilir." +youAreAdmin: "Siz yöneticisiniz." +frame: "Çerçeve" presets: "Ön ayar" +zeroPadding: "Sıfır doldurma" +nothingToConfigure: "Ayarlar seçeneği bulunmamaktadır." _imageEditing: _vars: + caption: "Dosya başlığı" filename: "Dosya adı" + filename_without_ext: "Uzantısız dosya adları" + year: "Çekim yılı" + month: "Çekim ayı" + day: "Çekim tarihi" + hour: "Fotoğrafın çekildiği zaman (saat)" + minute: "Çekim süresi (dakika)" + second: "Çekim süresi (saniye)" + camera_model: "Kamera Adı" + camera_lens_model: "Lens adı" + camera_mm: "Odak uzaklığı" + camera_mm_35: "Genişlik (35mm)" + camera_f: "açıklık" + camera_s: "Enstantane hızı" + camera_iso: "ISO hassasiyeti" + gps_lat: "Enlem" + gps_long: "Boylam" _imageFrameEditor: + title: "Düzenleme kareleri" + tip: "Görselleri, meta verileri içeren çerçeveler ve etiketler ekleyerek süsleyebilirsiniz." header: "Başlık" + footer: "Alt bilgi" + borderThickness: "jantın genişliği" + labelThickness: "Etiket genişliği" + labelScale: "Etiket ölçeği" + centered: "Merkezlenmiş" + captionMain: "Altyazı (büyük)" + captionSub: "Altyazı (küçük)" + availableVariables: "Mevcut değişkenler" + withQrCode: "2 boyutlu kod" + backgroundColor: "Arka Plan Rengi " + textColor: "Metin Rengi " font: "Yazı tipi" fontSerif: "Serif" fontSansSerif: "Sans Serif" quitWithoutSaveConfirm: "Kaydedilmemiş değişiklikleri silmek ister misin?" + failedToLoadImage: "Görüntü yükleme başarısız oldu " +_compression: + _quality: + high: "Yüksek Kalite " + medium: "Orta Kalite" + low: "Düşük Kalite " + _size: + large: "Büyük Boyut" + medium: "Orta Boyut" + small: "Küçük Boyut" _order: newest: "Önce yeni" oldest: "Önce eski" _chat: + messages: "Mesaj" noMessagesYet: "Henüz mesaj yok" newMessage: "Yeni mesaj" individualChat: "Özel Sohbet" @@ -1481,6 +1550,11 @@ _settings: showUrlPreview: "URL önizlemesi" showAvailableReactionsFirstInNote: "Mevcut tepkileri en üstte göster." showPageTabBarBottom: "Sayfa sekme çubuğunu aşağıda göster" + emojiPaletteBanner: "Emoji seçiciye kalıcı olarak bir palet olarak görüntülenecek ön ayarları kaydedebilir veya seçicinin nasıl görüntüleneceğini özelleştirebilirsiniz." + enableAnimatedImages: "Hareketli görüntüleri etkinleştirin" + settingsPersistence_title: "Ayarların kalıcılığı" + settingsPersistence_description1: "Ayarların kalıcı olarak saklanmasını etkinleştirmek, yapılandırma bilgilerinin kaybolmasını önler." + settingsPersistence_description2: "Ortamınıza bağlı olarak bu özelliği etkinleştirmek mümkün olmayabilir." _chat: showSenderName: "Gönderenin adını göster" sendOnEnter: "Enter tuşuna basarak gönderin" @@ -1489,6 +1563,8 @@ _preferencesProfile: profileNameDescription: "Bu cihazı tanımlayan bir ad belirle." profileNameDescription2: "Örnek: “Ana bilgisayar”, “Akıllı telefon”" manageProfiles: "Profilleri Yönet" + shareSameProfileBetweenDevicesIsNotRecommended: "Aynı profili birden fazla cihazda kullanmak önerilmez." + useSyncBetweenDevicesOptionIfYouWantToSyncSetting: "Birden fazla cihazda senkronize etmek istediğiniz ayarlarınız varsa, lütfen her bir ayar için \"Birden fazla cihazda senkronize et\" seçeneğini etkinleştirin." _preferencesBackup: autoBackup: "Otomatik yedekleme" restoreFromBackup: "Yedeklemeden geri yükle" @@ -1498,6 +1574,7 @@ _preferencesBackup: youNeedToNameYourProfileToEnableAutoBackup: "Otomatik yedeklemeyi etkinleştirmek için bir profil adı ayarlanmalıdır." autoPreferencesBackupIsNotEnabledForThisDevice: "Bu cihazda ayarların otomatik yedeklemesi etkinleştirilmemiş." backupFound: "Ayarların yedeği bulundu" + forceBackup: "Ayarların zorunlu yedeklenmesi" _accountSettings: requireSigninToViewContents: "İçeriği görüntülemek için oturum açmanız gerekir." requireSigninToViewContentsDescription1: "Oluşturduğun tüm notları ve diğer içeriği görüntülemek için oturum açman gerekir. Bu, tarayıcıların bilgilerini toplamasına engel olacaktır." @@ -2003,6 +2080,7 @@ _role: canManageAvatarDecorations: "Avatar süslerini yönet" driveCapacity: "Drive kapasitesi" maxFileSize: "Yükleyebileceğin maksimum dosya boyutu" + maxFileSize_caption: "Önceki aşamada ters proxy veya CDN gibi başka yapılandırma ayarları da olabilir." alwaysMarkNsfw: "Dosyaları her zaman NSFW olarak işaretle" canUpdateBioMedia: "Bir simge veya banner görüntüsünü düzenleyebilir" pinMax: "Sabitlenmiş notların maksimum sayısı" @@ -2030,6 +2108,7 @@ _role: uploadableFileTypes_caption: "İzin verilen MIME/dosya türlerini belirtir. Birden fazla MIME türü, yeni bir satırla ayırarak belirtilebilir ve joker karakterler yıldız işareti (*) ile belirtilebilir. (örneğin, image/*)" uploadableFileTypes_caption2: "Bazı dosya türleri algılanamayabilir. Bu tür dosyalara izin vermek için, spesifikasyona {x} ekle." noteDraftLimit: "Sunucu notlarının olası taslak sayısı" + scheduledNoteLimit: "Aynı anda oluşturulabilecek planlanmış gönderi sayısı" watermarkAvailable: "Filigran işlevinin kullanılabilirliği" _condition: roleAssignedTo: "Manuel rollere atanmış" @@ -2420,6 +2499,7 @@ _auth: scopeUser: "Aşağıdaki kullanıcı olarak çalıştırın" pleaseLogin: "Uygulamaları yetkilendirmek için lütfen giriş yapın." byClickingYouWillBeRedirectedToThisUrl: "Erişim izni verildiğinde, otomatik olarak aşağıdaki URL'ye yönlendirileceksin." + alreadyAuthorized: "Bu uygulamaya zaten erişim izinleri verilmiş durumda." _antennaSources: all: "Tüm notlar" homeTimeline: "Takip edilen kullanıcıların notları" @@ -2468,11 +2548,40 @@ _widgets: chat: "Sohbet" _widgetOptions: showHeader: "Başlığı göster" + transparent: "Arka planı şeffaf yapın" height: "Yükseklik" _button: colored: "Renkli" _clock: size: "Boyut" + thickness: "İğne kalınlığı" + thicknessThin: "İnce" + thicknessMedium: "Normal" + thicknessThick: "Kalın" + graduations: "Kadran ölçeği" + graduationDots: "Nokta" + graduationArabic: "Arap rakamları" + fadeGraduations: "ölçeği soluklaştır" + sAnimation: "İkinci el animasyon" + sAnimationElastic: "Gerçek" + sAnimationEaseOut: "Düz" + twentyFour: "24 saat ekran" + labelTime: "Zaman" + labelTz: "Zaman Dilimi" + labelTimeAndTz: "Zaman ve Saat Dilimi" + timezone: "Zaman Dilimi " + showMs: "Milisaniye cinsinden göster" + showLabel: "Etiketi Göster" + _jobQueue: + sound: "Sesleri Çal" + _rss: + url: "RSS beslemesi URL'si" + refreshIntervalSec: "Güncelleme aralığı (saniye)" + maxEntries: "Görüntülenecek maksimum öğe sayısı" + _rssTicker: + shuffle: "Görüntüleme sırasını karıştır" + duration: "Kaydırma yazısı hızı (saniye)" + reverse: "Geriye doğru kaydır" _birthdayFollowings: period: "Süre" _cw: @@ -2519,9 +2628,20 @@ _postForm: replyPlaceholder: "Bu notu yanıtla..." quotePlaceholder: "Bu notu alıntı yap..." channelPlaceholder: "Bir kanala gönder..." + showHowToUse: "Form açıklamasını göster" _howToUse: + content_title: "Metin" + content_description: "Yayınlamak istediğiniz içeriği girin." + toolbar_title: "Araç Çubuğu" + toolbar_description: "Dosya ve anket ekleyebilir, açıklamalar ve etiketler ekleyebilir, emoji ve bahsetme mesajları ekleyebilirsiniz." + account_title: "Hesap Menüsü" + account_description: "Paylaşım yaptığınız hesabı değiştirebilir ve hesabınıza kaydedilmiş taslak ve planlanmış paylaşımların listesini görüntüleyebilirsiniz." visibility_title: "Görünürlük" + visibility_description: "Notlarınıza kimlerin erişebileceğinin kapsamını belirleyebilirsiniz." menu_title: "Menü" + menu_description: "Taslak olarak kaydetme, gönderi planlama ve tepki ayarlama gibi diğer işlemleri de gerçekleştirebilirsiniz." + submit_title: "Gönder düğmesi" + submit_description: "Bir not paylaşacağım. Ctrl + Enter / Cmd + Enter tuşlarını kullanarak da paylaşım yapabilirsiniz." _placeholders: a: "Ne yapıyorsun?" b: "Çevrende neler oluyor?" @@ -2667,6 +2787,8 @@ _notification: youReceivedFollowRequest: "Bir takip isteği aldınız." yourFollowRequestAccepted: "Takip isteğin kabul edildi." pollEnded: "Anket sonuçları açıklandı." + scheduledNotePosted: "Rezervasyon defteri yayınlandı." + scheduledNotePostFailed: "Rezervasyon defterine gönderilemedi" newNote: "Yeni not" unreadAntennaNote: "{name} anteni" roleAssigned: "Verilen rol" @@ -2696,6 +2818,8 @@ _notification: quote: "Alıntılar" reaction: "Tepki" pollEnded: "Anketler sona eriyor" + scheduledNotePosted: "Planlanan gönderi başarılı" + scheduledNotePostFailed: "Planlanan gönderi başarısız oldu" receiveFollowRequest: "Takip istekleri alındı" followRequestAccepted: "Kabul edilen takip istekleri" roleAssigned: "Verilen rol" @@ -2735,6 +2859,14 @@ _deck: usedAsMinWidthWhenFlexible: "“Otomatik genişlik ayarı” seçeneği etkinleştirildiğinde, bunun için minimum genişlik kullanılacak." flexible: "Otomatik genişlik ayarı" enableSyncBetweenDevicesForProfiles: "Cihazlar arasında profil bilgilerinin senkronizasyonunu etkinleştir" + showHowToUse: "Kullanıcı arayüzü açıklamasını görüntüle" + _howToUse: + addColumn_title: "Sütun ekle" + addColumn_description: "Sütun türlerini seçip ekleyebilirsiniz." + settings_title: "Arayüz Yapılandırması" + settings_description: "Sekme kullanıcı arayüzünü ayrıntılı olarak yapılandırabilirsiniz." + switchProfile_title: "Profili Değiştir" + switchProfile_description: "Kullanıcı arayüzü düzenlerini profil olarak kaydedebilir ve istediğiniz zaman bunlar arasında geçiş yapabilirsiniz." _columns: main: "Ana" widgets: "Widget'lar" @@ -2795,6 +2927,8 @@ _abuseReport: notifiedWebhook: "Kullanılacak webhook" deleteConfirm: "Bildirim alıcısını silmek istediğinden emin misin?" _moderationLogTypes: + clearQueue: "Kuyruğu temizle" + promoteQueue: "Sıraya alınmış işi yeniden deneyin." createRole: "Rol oluşturuldu" deleteRole: "Rol silindi" updateRole: "Rol güncellendi" @@ -3189,10 +3323,13 @@ _watermarkEditor: title: "Filigranı Düzenle" cover: "Her şeyi örtün" repeat: "her yere yayılmış" + preserveBoundingRect: "Döndürme sırasında dışarı çıkmayacak şekilde ayarlayın." opacity: "Opaklık" scale: "Boyut" text: "Metin" + qr: "2 boyutlu kod" position: "Pozisyon" + margin: "Kenar" type: "Tür" image: "Görseller" advanced: "Gelişmiş" @@ -3207,16 +3344,21 @@ _watermarkEditor: polkadotSubDotOpacity: "İkincil noktanın opaklığı" polkadotSubDotRadius: "İkincil noktanın boyutu" polkadotSubDotDivisions: "Alt nokta sayısı." + leaveBlankToAccountUrl: "Boş bırakılması durumunda hesap URL'si görüntülenecektir." + failedToLoadImage: "Görüntü yükleme başarısız oldu " _imageEffector: title: "Effektler" addEffect: "Efektler Ekle" discardChangesConfirm: "Cidden çıkmak istiyor musun? Kaydedilmemiş değişikliklerin var." + failedToLoadImage: "Görüntü yükleme başarısız oldu " _fxs: chromaticAberration: "Renk Sapması" glitch: "Bozulma" mirror: "Ayna" invert: "Renkleri Ters Çevir" grayscale: "Gri tonlama" + blur: "Bulanıklık" + pixelate: "Mozaik" colorAdjust: "Renk Düzeltme" colorClamp: "Renk Sıkıştırma" colorClampAdvanced: "Renk Sıkıştırma (Gelişmiş)" @@ -3228,10 +3370,13 @@ _imageEffector: checker: "Denetleyici" blockNoise: "Gürültüyü Engelle" tearing: "Yırtılma" + fill: "Doldur" _fxProps: angle: "Açı" scale: "Boyut" size: "Boyut" + radius: "Yarıçap" + samples: "Örnek sayısı" offset: "Pozisyon" color: "Renk" opacity: "Opaklık" @@ -3256,11 +3401,10 @@ _imageEffector: threshold: "Eşik" centerX: "Merkez X" centerY: "Merkez Y" - zoomLinesSmoothing: "Düzeltme" - zoomLinesSmoothingDescription: "Düzeltme ve yakınlaştırma çizgi genişliği birlikte kullanılamaz." - zoomLinesThreshold: "Zoom çizgi genişliği" + density: "Yoğunluk" + zoomLinesOutlineThickness: "çizgi gölge kalınlığı" zoomLinesMaskSize: "Merkez çapı" - zoomLinesBlack: "Siyah yap" + circle: "Dairesel" drafts: "Taslaklar" _drafts: select: "Taslak Seç" @@ -3276,6 +3420,22 @@ _drafts: restoreFromDraft: "Taslaktan geri yükle" restore: "Geri yükle" listDrafts: "Taslaklar Listesi" + schedule: "Planlanmış Gönderi" + listScheduledNotes: "Planlanmış gönderilerin listesi" + cancelSchedule: "Rezervasyonu iptal et" +qr: "2 boyutlu kod" _qr: showTabTitle: "Ekran" + readTabTitle: "Okumak" + shareTitle: "{name}{acct}" + shareText: "Beni Fediverse'te takip edin!" + chooseCamera: "Kamera Seç" + cannotToggleFlash: "Işık seçeneği mevcut değil." + turnOnFlash: "Işığı açın" + turnOffFlash: "Işığı kapatın" + startQr: "Özgeçmiş Kodu Okuyucu" + stopQr: "Kod okuyucuyu durdurun" + noQrCodeFound: "QR kodu bulunamadı" + scanFile: "Cihazdaki görüntüyü tarayın" raw: "Metin" + mfm: "MFM" diff --git a/locales/vi-VN.yml b/locales/vi-VN.yml index 0bb1d54ae8..f4e6e568bb 100644 --- a/locales/vi-VN.yml +++ b/locales/vi-VN.yml @@ -1,5 +1,5 @@ --- -_lang_: "Tiếng Việt " +_lang_: "Tiếng Việt" headlineMisskey: "Mạng xã hội liên hợp" introMisskey: "Xin chào! Misskey là một nền tảng tiểu blog phi tập trung mã nguồn mở.\nViết \"tút\" để chia sẻ những suy nghĩ của bạn 📡\nBằng \"biểu cảm\", bạn có thể bày tỏ nhanh chóng cảm xúc của bạn với các tút 👍\nHãy khám phá một thế giới mới! 🚀" poweredByMisskeyDescription: "{name} là một trong những chủ máy của Misskey là nền tảng mã nguồn mở" @@ -576,6 +576,7 @@ showFixedPostForm: "Hiện khung soạn tút ở phía trên bảng tin" showFixedPostFormInChannel: "Hiển thị mẫu bài đăng ở phía trên bản tin" withRepliesByDefaultForNewlyFollowed: "Mặc định hiển thị trả lời từ những người dùng mới theo dõi trong dòng thời gian" newNoteRecived: "Đã nhận tút mới" +newNote: "Ghi chú mới" sounds: "Âm thanh" sound: "Âm thanh" notificationSoundSettings: "Cài đặt âm thanh thông báo" @@ -848,7 +849,7 @@ hideOnlineStatus: "Ẩn trạng thái online" hideOnlineStatusDescription: "Ẩn trạng thái online của bạn làm giảm sự tiện lợi của một số tính năng như tìm kiếm." online: "Online" active: "Hoạt động" -offline: "Offline" +offline: "Ngoại tuyến" notRecommended: "Không đề xuất" botProtection: "Bảo vệ Bot" instanceBlocking: "Máy chủ đã chặn" @@ -1220,6 +1221,7 @@ 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." +driveAboutTip: "Trong Drive, danh sách các tệp bạn đã tải lên trước đây sẽ được hiển thị.
\nBạn có thể sử dụng lại chúng khi đính kèm vào ghi chú, hoặc tải lên trước các tệp để đăng sau.
\nLưu ý rằng nếu bạn xóa một tệp, tệp đó cũng sẽ biến mất khỏi tất cả những nơi đã sử dụng tệp đó (ghi chú, trang, ảnh đại diện, biểu ngữ, v.v.).
\nBạn cũng có thể tạo các thư mục để sắp xếp chúng." inMinutes: "phút" inDays: "ngày" widgets: "Tiện ích" diff --git a/locales/zh-CN.yml b/locales/zh-CN.yml index b2f700379d..5cfa90e910 100644 --- a/locales/zh-CN.yml +++ b/locales/zh-CN.yml @@ -3401,11 +3401,9 @@ _imageEffector: threshold: "阈值" centerX: "中心 X " centerY: "中心 Y" - zoomLinesSmoothing: "平滑" - zoomLinesSmoothingDescription: "平滑和集中线宽度设置不能同时使用。" - zoomLinesThreshold: "集中线宽度" + density: "密度" + zoomLinesOutlineThickness: "线条阴影粗细" zoomLinesMaskSize: "中心直径" - zoomLinesBlack: "变成黑色" circle: "圆形" drafts: "草稿" _drafts: diff --git a/locales/zh-TW.yml b/locales/zh-TW.yml index 57612abd36..fa8a3eead8 100644 --- a/locales/zh-TW.yml +++ b/locales/zh-TW.yml @@ -3401,11 +3401,9 @@ _imageEffector: threshold: "閾值" centerX: "X中心座標" centerY: "Y中心座標" - zoomLinesSmoothing: "平滑化" - zoomLinesSmoothingDescription: "平滑化與集中線寬度設定不能同時使用。" - zoomLinesThreshold: "集中線的寬度" + density: "密度" + zoomLinesOutlineThickness: "線條陰影的粗細" zoomLinesMaskSize: "中心直徑" - zoomLinesBlack: "變成黑色" circle: "圓形" drafts: "草稿\n" _drafts: diff --git a/package.json b/package.json index 38170b5e21..9817c8f4f1 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "misskey", - "version": "2026.3.2-alpha.0", + "version": "2026.3.2-alpha.2", "codename": "nasubi", "repository": { "type": "git", @@ -53,29 +53,29 @@ "cleanall": "pnpm clean-all" }, "dependencies": { - "cssnano": "7.1.2", - "esbuild": "0.27.3", + "cssnano": "7.1.3", + "esbuild": "0.27.4", "execa": "9.6.1", "ignore-walk": "8.0.0", "js-yaml": "4.1.1", - "postcss": "8.5.6", - "tar": "7.5.10", + "postcss": "8.5.8", + "tar": "7.5.11", "terser": "5.46.0" }, "devDependencies": { - "@eslint/js": "9.39.3", + "@eslint/js": "9.39.4", "@misskey-dev/eslint-plugin": "2.1.0", "@types/js-yaml": "4.0.9", - "@types/node": "24.11.0", - "@typescript-eslint/eslint-plugin": "8.56.1", - "@typescript-eslint/parser": "8.56.1", + "@types/node": "24.12.0", + "@typescript-eslint/eslint-plugin": "8.57.0", + "@typescript-eslint/parser": "8.57.0", "@typescript/native-preview": "7.0.0-dev.20260116.1", "cross-env": "10.1.0", "cypress": "15.11.0", - "eslint": "9.39.3", + "eslint": "9.39.4", "globals": "17.4.0", "ncp": "2.0.0", - "pnpm": "10.30.3", + "pnpm": "10.32.1", "start-server-and-test": "2.1.5", "typescript": "5.9.3" }, diff --git a/packages/backend/package.json b/packages/backend/package.json index 2ec504be21..921e89eff9 100644 --- a/packages/backend/package.json +++ b/packages/backend/package.json @@ -71,8 +71,8 @@ "utf-8-validate": "6.0.6" }, "dependencies": { - "@aws-sdk/client-s3": "3.1000.0", - "@aws-sdk/lib-storage": "3.1000.0", + "@aws-sdk/client-s3": "3.1008.0", + "@aws-sdk/lib-storage": "3.1008.0", "@discordapp/twemoji": "16.0.1", "@fastify/accepts": "5.0.4", "@fastify/cors": "11.2.0", @@ -83,16 +83,16 @@ "@kitajs/html": "4.2.13", "@misskey-dev/sharp-read-bmp": "1.2.0", "@misskey-dev/summaly": "5.2.5", - "@napi-rs/canvas": "0.1.95", - "@nestjs/common": "11.1.14", - "@nestjs/core": "11.1.14", - "@nestjs/testing": "11.1.14", + "@napi-rs/canvas": "0.1.96", + "@nestjs/common": "11.1.16", + "@nestjs/core": "11.1.16", + "@nestjs/testing": "11.1.16", "@peertube/http-signature": "1.7.0", - "@sentry/node": "10.40.0", - "@sentry/profiling-node": "10.40.0", - "@simplewebauthn/server": "13.2.3", - "@sinonjs/fake-timers": "15.1.0", - "@smithy/node-http-handler": "4.4.12", + "@sentry/node": "10.43.0", + "@sentry/profiling-node": "10.43.0", + "@simplewebauthn/server": "13.3.0", + "@sinonjs/fake-timers": "15.1.1", + "@smithy/node-http-handler": "4.4.16", "@swc/cli": "0.8.0", "@swc/core": "1.15.18", "@twemoji/parser": "16.0.0", @@ -103,7 +103,7 @@ "bcryptjs": "3.0.3", "blurhash": "2.0.5", "body-parser": "2.2.2", - "bullmq": "5.70.1", + "bullmq": "5.71.0", "cacheable-lookup": "7.0.0", "chalk": "5.6.2", "chalk-template": "1.1.2", @@ -112,10 +112,10 @@ "content-disposition": "1.0.1", "date-fns": "4.1.0", "deep-email-validator": "0.1.21", - "fastify": "5.8.1", + "fastify": "5.8.2", "fastify-raw-body": "5.0.0", "feed": "5.2.0", - "file-type": "21.3.0", + "file-type": "21.3.2", "fluent-ffmpeg": "2.1.3", "form-data": "4.0.5", "got": "14.6.6", @@ -138,14 +138,14 @@ "nanoid": "5.1.6", "nested-property": "4.0.0", "node-fetch": "3.3.2", - "node-html-parser": "7.0.2", - "nodemailer": "8.0.1", + "node-html-parser": "7.1.0", + "nodemailer": "8.0.2", "nsfwjs": "4.2.0", "oauth2orize": "1.12.0", "oauth2orize-pkce": "0.1.2", "os-utils": "0.0.14", "otpauth": "9.5.0", - "pg": "8.19.0", + "pg": "8.20.0", "pkce-challenge": "6.0.0", "probe-image-size": "7.2.3", "promise-limit": "2.7.0", @@ -164,7 +164,7 @@ "slacc": "0.0.10", "strict-event-emitter-types": "2.0.0", "stringz": "2.1.0", - "systeminformation": "5.31.1", + "systeminformation": "5.31.4", "tinycolor2": "1.6.0", "tmp": "0.2.5", "tsc-alias": "1.8.16", @@ -178,8 +178,8 @@ "devDependencies": { "@jest/globals": "29.7.0", "@kitajs/ts-html-plugin": "4.1.4", - "@nestjs/platform-express": "11.1.14", - "@sentry/vue": "10.40.0", + "@nestjs/platform-express": "11.1.16", + "@sentry/vue": "10.43.0", "@simplewebauthn/types": "12.0.0", "@swc/jest": "0.2.39", "@types/accepts": "1.3.7", @@ -193,7 +193,7 @@ "@types/jsonld": "1.5.15", "@types/mime-types": "3.0.1", "@types/ms": "2.1.0", - "@types/node": "24.11.0", + "@types/node": "24.12.0", "@types/nodemailer": "7.0.11", "@types/oauth2orize": "1.11.5", "@types/oauth2orize-pkce": "0.1.2", @@ -202,7 +202,7 @@ "@types/random-seed": "0.3.5", "@types/ratelimiter": "3.4.6", "@types/rename": "1.0.7", - "@types/sanitize-html": "2.16.0", + "@types/sanitize-html": "2.16.1", "@types/semver": "7.7.1", "@types/simple-oauth2": "5.0.8", "@types/sinonjs__fake-timers": "15.0.1", @@ -212,10 +212,10 @@ "@types/vary": "1.1.3", "@types/web-push": "3.6.4", "@types/ws": "8.18.1", - "@typescript-eslint/eslint-plugin": "8.56.1", - "@typescript-eslint/parser": "8.56.1", + "@typescript-eslint/eslint-plugin": "8.57.0", + "@typescript-eslint/parser": "8.57.0", "aws-sdk-client-mock": "4.1.0", - "cbor": "10.0.11", + "cbor": "10.0.12", "cross-env": "10.1.0", "esbuild-plugin-swc": "1.0.1", "eslint-plugin-import": "2.32.0", diff --git a/packages/backend/src/config.ts b/packages/backend/src/config.ts index 4cd82bed87..6a83359d38 100644 --- a/packages/backend/src/config.ts +++ b/packages/backend/src/config.ts @@ -10,7 +10,6 @@ import { type FastifyServerOptions } from 'fastify'; import type * as Sentry from '@sentry/node'; import type * as SentryVue from '@sentry/vue'; import type { RedisOptions } from 'ioredis'; -import type { ManifestChunk } from 'vite'; type RedisOptionsSource = Partial & { host: string; @@ -189,9 +188,7 @@ export type Config = { authUrl: string; driveUrl: string; userAgent: string; - frontendEntry: ManifestChunk; frontendManifestExists: boolean; - frontendEmbedEntry: ManifestChunk; frontendEmbedManifestExists: boolean; mediaProxy: string; externalMediaProxyEnabled: boolean; @@ -250,12 +247,6 @@ export function loadConfig(): Config { const frontendManifestExists = fs.existsSync(resolve(projectBuiltDir, '_frontend_vite_/manifest.json')); const frontendEmbedManifestExists = fs.existsSync(resolve(projectBuiltDir, '_frontend_embed_vite_/manifest.json')); - const frontendManifest = frontendManifestExists ? - JSON.parse(fs.readFileSync(resolve(projectBuiltDir, '_frontend_vite_/manifest.json'), 'utf-8')) - : { 'src/_boot_.ts': { file: null } }; - const frontendEmbedManifest = frontendEmbedManifestExists ? - JSON.parse(fs.readFileSync(resolve(projectBuiltDir, '_frontend_embed_vite_/manifest.json'), 'utf-8')) - : { 'src/boot.ts': { file: null } }; const config = JSON.parse(fs.readFileSync(compiledConfigFilePath, 'utf-8')) as Source; @@ -337,9 +328,7 @@ export function loadConfig(): Config { config.videoThumbnailGenerator.endsWith('/') ? config.videoThumbnailGenerator.substring(0, config.videoThumbnailGenerator.length - 1) : config.videoThumbnailGenerator : null, userAgent: `Misskey/${version} (${config.url})`, - frontendEntry: frontendManifest['src/_boot_.ts'], frontendManifestExists: frontendManifestExists, - frontendEmbedEntry: frontendEmbedManifest['src/boot.ts'], frontendEmbedManifestExists: frontendEmbedManifestExists, perChannelMaxNoteCacheCount: config.perChannelMaxNoteCacheCount ?? 1000, perUserNotificationsMaxCount: config.perUserNotificationsMaxCount ?? 500, diff --git a/packages/backend/src/core/activitypub/ApRequestService.ts b/packages/backend/src/core/activitypub/ApRequestService.ts index d14b82dc92..0ad885a17c 100644 --- a/packages/backend/src/core/activitypub/ApRequestService.ts +++ b/packages/backend/src/core/activitypub/ApRequestService.ts @@ -81,7 +81,7 @@ export class ApRequestCreator { }, args.additionalHeaders), }; - const result = this.#signToRequest(request, args.key, ['(request-target)', 'date', 'host', 'accept']); + const result = this.#signToRequest(request, args.key, ['(request-target)', 'date', 'host']); return { request, diff --git a/packages/backend/src/server/api/endpoints/channels/timeline.ts b/packages/backend/src/server/api/endpoints/channels/timeline.ts index 4f56bc2110..e088869457 100644 --- a/packages/backend/src/server/api/endpoints/channels/timeline.ts +++ b/packages/backend/src/server/api/endpoints/channels/timeline.ts @@ -15,6 +15,7 @@ import { FanoutTimelineEndpointService } from '@/core/FanoutTimelineEndpointServ import { MiLocalUser } from '@/models/User.js'; import { ChannelMutingService } from '@/core/ChannelMutingService.js'; import { ApiError } from '../../error.js'; +import { Brackets } from 'typeorm'; export const meta = { tags: ['notes', 'channels'], @@ -132,7 +133,10 @@ export default class extends Endpoint { // eslint- .then(x => x.map(x => x.id).filter(x => x !== ps.channelId)); if (mutingChannelIds.length > 0) { query.andWhere('note.channelId NOT IN (:...mutingChannelIds)', { mutingChannelIds }); - query.andWhere('note.renoteChannelId NOT IN (:...mutingChannelIds)', { mutingChannelIds }); + query.andWhere(new Brackets(qb => { + qb.orWhere('note.renoteChannelId IS NULL'); + qb.orWhere('note.renoteChannelId NOT IN (:...mutingChannelIds)', { mutingChannelIds }); + })); } } //#endregion diff --git a/packages/backend/src/server/api/endpoints/notes/timeline.ts b/packages/backend/src/server/api/endpoints/notes/timeline.ts index fe9c412be4..b00247c69d 100644 --- a/packages/backend/src/server/api/endpoints/notes/timeline.ts +++ b/packages/backend/src/server/api/endpoints/notes/timeline.ts @@ -177,7 +177,10 @@ export default class extends Endpoint { // eslint- .andWhere('note.channelId IS NULL') .andWhere('note.userId IN (:...meOrFolloweeIds)', { meOrFolloweeIds: meOrFolloweeIds }); if (mutingChannelIds.length > 0) { - qb.andWhere('note.renoteChannelId NOT IN (:...mutingChannelIds)', { mutingChannelIds }); + qb.andWhere(new Brackets(qb2 => { + qb2.orWhere('note.renoteChannelId IS NULL'); + qb2.orWhere('note.renoteChannelId NOT IN (:...mutingChannelIds)', { mutingChannelIds }); + })); } })); } else if (followingChannelIds.length > 0) { diff --git a/packages/backend/src/server/api/endpoints/users/notes.ts b/packages/backend/src/server/api/endpoints/users/notes.ts index b9710250cf..e280b367f9 100644 --- a/packages/backend/src/server/api/endpoints/users/notes.ts +++ b/packages/backend/src/server/api/endpoints/users/notes.ts @@ -185,7 +185,10 @@ export default class extends Endpoint { // eslint- if (ps.withChannelNotes) { query.andWhere(new Brackets(qb => { if (mutingChannelIds.length > 0) { - qb.andWhere('note.channelId NOT IN (:...mutingChannelIds)', { mutingChannelIds: mutingChannelIds }); + qb.andWhere(new Brackets(qb2 => { + qb2.orWhere('note.channelId IS NULL'); + qb2.orWhere('note.channelId NOT IN (:...mutingChannelIds)', { mutingChannelIds }); + })); } if (!isSelf) { diff --git a/packages/backend/src/server/api/stream/NoteStreamingHidingService.ts b/packages/backend/src/server/api/stream/NoteStreamingHidingService.ts index 459fa30fa9..1b86c0ae20 100644 --- a/packages/backend/src/server/api/stream/NoteStreamingHidingService.ts +++ b/packages/backend/src/server/api/stream/NoteStreamingHidingService.ts @@ -6,16 +6,11 @@ import { Injectable } from '@nestjs/common'; import { bindThis } from '@/decorators.js'; import { NoteEntityService } from '@/core/entities/NoteEntityService.js'; +import { deepClone } from '@/misc/clone.js'; import { isRenotePacked, isQuotePacked } from '@/misc/is-renote.js'; import type { Packed } from '@/misc/json-schema.js'; import type { MiUser } from '@/models/User.js'; -type HiddenLayer = 'note' | 'renote' | 'renoteRenote'; - -type LockdownCheckResult = - | { shouldSkip: true } - | { shouldSkip: false; hiddenLayers: Set }; - /** Streamにおいて、ノートを隠す(hideNote)を適用するためのService */ @Injectable() export class NoteStreamingHidingService { @@ -23,110 +18,52 @@ export class NoteStreamingHidingService { private noteEntityService: NoteEntityService, ) {} - /** - * ノートの可視性を判定する - * - * @param note - 判定対象のノート - * @param meId - 閲覧者のユーザーID(未ログインの場合はnull) - * @returns shouldSkip: true の場合はノートを流さない、false の場合は hiddenLayers に基づいて隠す - */ - @bindThis - public async shouldHide( - note: Packed<'Note'>, - meId: MiUser['id'] | null, - ): Promise { - const hiddenLayers = new Set(); + private collectRenoteChain(note: Packed<'Note'>): Packed<'Note'>[] { + const renoteChain: Packed<'Note'>[] = []; - // 1階層目: note自体 - const shouldHideThisNote = await this.noteEntityService.shouldHideNote(note, meId); - if (shouldHideThisNote) { - if (isRenotePacked(note) && isQuotePacked(note)) { - // 引用リノートの場合、内容を隠して流す - hiddenLayers.add('note'); - } else if (isRenotePacked(note)) { - // 純粋リノートの場合、流さない - return { shouldSkip: true }; - } else { - // 通常ノートの場合、内容を隠して流す - hiddenLayers.add('note'); - } + for (let current: Packed<'Note'> | null | undefined = note; current != null; current = current.renote) { + renoteChain.push(current); } - // 2階層目: note.renote - if (isRenotePacked(note) && note.renote) { - const shouldHideRenote = await this.noteEntityService.shouldHideNote(note.renote, meId); - if (shouldHideRenote) { - if (isQuotePacked(note)) { - // noteが引用リノートの場合、renote部分だけ隠す - hiddenLayers.add('renote'); - } else { - // noteが純粋リノートの場合、流さない - return { shouldSkip: true }; - } - } - } - - // 3階層目: note.renote.renote - if (isRenotePacked(note) && note.renote && - isRenotePacked(note.renote) && note.renote.renote) { - const shouldHideRenoteRenote = await this.noteEntityService.shouldHideNote(note.renote.renote, meId); - if (shouldHideRenoteRenote) { - if (isQuotePacked(note.renote)) { - // note.renoteが引用リノートの場合、renote.renote部分だけ隠す - hiddenLayers.add('renoteRenote'); - } else { - // note.renoteが純粋リノートの場合、note.renoteの意味がなくなるので流さない - return { shouldSkip: true }; - } - } - } - - return { shouldSkip: false, hiddenLayers }; + return renoteChain; } /** - * hiddenLayersに基づいてノートの内容を隠す。 + * ストリーミング配信用にノートの内容を隠す(あるいはそもそも送信しない)判定及び処理を行う。 * - * この処理は渡された `note` を直接変更します。 + * 隠す処理が必要な場合は元のノートをクローンして変更を適用したものを返し、 + * 送信すべきでない場合は `null` を返す。 + * 変更が不要な場合は元のノートの参照をそのまま返す。 * * @param note - 処理対象のノート - * @param hiddenLayers - 隠す階層のセット + * @param meId - 閲覧者のユーザー ID (未ログインの場合は `null`) + * @returns 配信するノートオブジェクト、または配信スキップの場合は `null` */ @bindThis - public applyHiding( - note: Packed<'Note'>, - hiddenLayers: Set, - ): void { - if (hiddenLayers.has('note')) { - this.noteEntityService.hideNote(note); - } - if (hiddenLayers.has('renote') && note.renote) { - this.noteEntityService.hideNote(note.renote); - } - if (hiddenLayers.has('renoteRenote') && note.renote && note.renote.renote) { - this.noteEntityService.hideNote(note.renote.renote); - } - } + public async filter(note: Packed<'Note'>, meId: MiUser['id'] | null): Promise | null> { + const renoteChain = this.collectRenoteChain(note); + const shouldHide = await Promise.all(renoteChain.map(n => this.noteEntityService.shouldHideNote(n, meId))); - /** - * ストリーミング配信用にノートを隠す(あるいはそもそも送信しない)の判定及び処理を行う。 - * - * この処理は渡された `note` を直接変更します。 - * - * @param note - 処理対象のノート(必要に応じて内容が隠される) - * @param meId - 閲覧者のユーザーID(未ログインの場合はnull) - * @returns shouldSkip: true の場合はノートを流さない - */ - @bindThis - public async processHiding( - note: Packed<'Note'>, - meId: MiUser['id'] | null, - ): Promise<{ shouldSkip: boolean }> { - const result = await this.shouldHide(note, meId); - if (result.shouldSkip) { - return { shouldSkip: true }; + if (!shouldHide.some(h => h)) { + // 隠す必要がない場合は元のノートをそのまま返す + return note; } - this.applyHiding(note, result.hiddenLayers); - return { shouldSkip: false }; + + if (renoteChain.some(n => isRenotePacked(n) && !isQuotePacked(n))) { + // 純粋リノートの場合は配信をスキップする + return null; + } + + const clonedNote = deepClone(note); + let currentCloned = clonedNote; + + for (let i = 0; i < renoteChain.length; i++) { + if (shouldHide[i]) { + this.noteEntityService.hideNote(currentCloned); + } + currentCloned = currentCloned.renote!; + } + + return clonedNote; } } diff --git a/packages/backend/src/server/api/stream/channels/antenna.ts b/packages/backend/src/server/api/stream/channels/antenna.ts index 3fb1ad63d4..b7f863b355 100644 --- a/packages/backend/src/server/api/stream/channels/antenna.ts +++ b/packages/backend/src/server/api/stream/channels/antenna.ts @@ -62,13 +62,14 @@ export class AntennaChannel extends Channel { @bindThis private async onEvent(data: GlobalEvents['antenna']['payload']) { if (data.type === 'note') { - const note = await this.noteEntityService.pack(data.body.id, this.user, { detail: true }); + let note = await this.noteEntityService.pack(data.body.id, this.user, { detail: true }); if (!this.isNoteVisibleForMe(note)) return; if (this.isNoteMutedOrBlocked(note)) return; - const { shouldSkip } = await this.noteStreamingHidingService.processHiding(note, this.user?.id ?? null); - if (shouldSkip) return; + const filtered = await this.noteStreamingHidingService.filter(note, this.user?.id ?? null); + if (!filtered) return; + note = filtered; if (this.user) { if (isRenotePacked(note) && !isQuotePacked(note)) { diff --git a/packages/backend/src/server/api/stream/channels/channel.ts b/packages/backend/src/server/api/stream/channels/channel.ts index 80352df2c2..6b9159887b 100644 --- a/packages/backend/src/server/api/stream/channels/channel.ts +++ b/packages/backend/src/server/api/stream/channels/channel.ts @@ -53,8 +53,10 @@ export class ChannelChannel extends Channel { if (!this.isNoteVisibleForMe(note)) return; if (this.isNoteMutedOrBlocked(note)) return; - const { shouldSkip } = await this.noteStreamingHidingService.processHiding(note, this.user?.id ?? null); - if (shouldSkip) return; + const filtered = await this.noteStreamingHidingService.filter(note, this.user?.id ?? null); + if (!filtered) return; + // eslint-disable-next-line no-param-reassign -- これ以降元の Note オブジェクトは見てはいけないので、いっそ再代入した方が安全 + note = filtered; if (this.user) { if (isRenotePacked(note) && !isQuotePacked(note)) { diff --git a/packages/backend/src/server/api/stream/channels/global-timeline.ts b/packages/backend/src/server/api/stream/channels/global-timeline.ts index 2b971d93db..7d310bd875 100644 --- a/packages/backend/src/server/api/stream/channels/global-timeline.ts +++ b/packages/backend/src/server/api/stream/channels/global-timeline.ts @@ -62,8 +62,10 @@ export class GlobalTimelineChannel extends Channel { if (this.isNoteMutedOrBlocked(note)) return; - const { shouldSkip } = await this.noteStreamingHidingService.processHiding(note, this.user?.id ?? null); - if (shouldSkip) return; + const filtered = await this.noteStreamingHidingService.filter(note, this.user?.id ?? null); + if (!filtered) return; + // eslint-disable-next-line no-param-reassign -- これ以降元の Note オブジェクトは見てはいけないので、いっそ再代入した方が安全 + note = filtered; if (this.user) { if (isRenotePacked(note) && !isQuotePacked(note)) { diff --git a/packages/backend/src/server/api/stream/channels/hashtag.ts b/packages/backend/src/server/api/stream/channels/hashtag.ts index 6ba8963044..ccbe6a610c 100644 --- a/packages/backend/src/server/api/stream/channels/hashtag.ts +++ b/packages/backend/src/server/api/stream/channels/hashtag.ts @@ -59,8 +59,10 @@ export class HashtagChannel extends Channel { if (note.reply && note.reply.user.requireSigninToViewContents && this.user == null) return; if (this.isNoteMutedOrBlocked(note)) return; - const { shouldSkip } = await this.noteStreamingHidingService.processHiding(note, this.user?.id ?? null); - if (shouldSkip) return; + const filtered = await this.noteStreamingHidingService.filter(note, this.user?.id ?? null); + if (!filtered) return; + // eslint-disable-next-line no-param-reassign -- これ以降元の Note オブジェクトは見てはいけないので、いっそ再代入した方が安全 + note = filtered; if (this.user) { if (isRenotePacked(note) && !isQuotePacked(note)) { diff --git a/packages/backend/src/server/api/stream/channels/home-timeline.ts b/packages/backend/src/server/api/stream/channels/home-timeline.ts index 4742b0ed32..5b6dbb24b0 100644 --- a/packages/backend/src/server/api/stream/channels/home-timeline.ts +++ b/packages/backend/src/server/api/stream/channels/home-timeline.ts @@ -82,8 +82,10 @@ export class HomeTimelineChannel extends Channel { if (this.isNoteMutedOrBlocked(note)) return; - const { shouldSkip } = await this.noteStreamingHidingService.processHiding(note, this.user?.id ?? null); - if (shouldSkip) return; + const filtered = await this.noteStreamingHidingService.filter(note, this.user?.id ?? null); + if (!filtered) return; + // eslint-disable-next-line no-param-reassign -- これ以降元の Note オブジェクトは見てはいけないので、いっそ再代入した方が安全 + note = filtered; if (this.user) { if (isRenotePacked(note) && !isQuotePacked(note)) { diff --git a/packages/backend/src/server/api/stream/channels/hybrid-timeline.ts b/packages/backend/src/server/api/stream/channels/hybrid-timeline.ts index 7acd945f54..f81e880018 100644 --- a/packages/backend/src/server/api/stream/channels/hybrid-timeline.ts +++ b/packages/backend/src/server/api/stream/channels/hybrid-timeline.ts @@ -101,8 +101,10 @@ export class HybridTimelineChannel extends Channel { } } - const { shouldSkip } = await this.noteStreamingHidingService.processHiding(note, this.user?.id ?? null); - if (shouldSkip) return; + const filtered = await this.noteStreamingHidingService.filter(note, this.user?.id ?? null); + if (!filtered) return; + // eslint-disable-next-line no-param-reassign -- これ以降元の Note オブジェクトは見てはいけないので、いっそ再代入した方が安全 + note = filtered; if (this.user) { if (isRenotePacked(note) && !isQuotePacked(note)) { diff --git a/packages/backend/src/server/api/stream/channels/local-timeline.ts b/packages/backend/src/server/api/stream/channels/local-timeline.ts index 115b3a1415..5df9b7902b 100644 --- a/packages/backend/src/server/api/stream/channels/local-timeline.ts +++ b/packages/backend/src/server/api/stream/channels/local-timeline.ts @@ -72,8 +72,10 @@ export class LocalTimelineChannel extends Channel { if (this.isNoteMutedOrBlocked(note)) return; - const { shouldSkip } = await this.noteStreamingHidingService.processHiding(note, this.user?.id ?? null); - if (shouldSkip) return; + const filtered = await this.noteStreamingHidingService.filter(note, this.user?.id ?? null); + if (!filtered) return; + // eslint-disable-next-line no-param-reassign -- これ以降元の Note オブジェクトは見てはいけないので、いっそ再代入した方が安全 + note = filtered; if (this.user) { if (isRenotePacked(note) && !isQuotePacked(note)) { diff --git a/packages/backend/src/server/api/stream/channels/role-timeline.ts b/packages/backend/src/server/api/stream/channels/role-timeline.ts index a1be92a5c8..c0e054b3e7 100644 --- a/packages/backend/src/server/api/stream/channels/role-timeline.ts +++ b/packages/backend/src/server/api/stream/channels/role-timeline.ts @@ -44,7 +44,7 @@ export class RoleTimelineChannel extends Channel { @bindThis private async onEvent(data: GlobalEvents['roleTimeline']['payload']) { if (data.type === 'note') { - const note = data.body; + let note = data.body; if (!(await this.roleservice.isExplorable({ id: this.roleId }))) { return; @@ -56,8 +56,9 @@ export class RoleTimelineChannel extends Channel { if (this.isNoteMutedOrBlocked(note)) return; - const { shouldSkip } = await this.noteStreamingHidingService.processHiding(note, this.user?.id ?? null); - if (shouldSkip) return; + const filtered = await this.noteStreamingHidingService.filter(note, this.user?.id ?? null); + if (!filtered) return; + note = filtered; if (this.user) { if (isRenotePacked(note) && !isQuotePacked(note)) { diff --git a/packages/backend/src/server/api/stream/channels/user-list.ts b/packages/backend/src/server/api/stream/channels/user-list.ts index 6df7e32005..0a9d09d64a 100644 --- a/packages/backend/src/server/api/stream/channels/user-list.ts +++ b/packages/backend/src/server/api/stream/channels/user-list.ts @@ -117,8 +117,10 @@ export class UserListChannel extends Channel { if (this.isNoteMutedOrBlocked(note)) return; - const { shouldSkip } = await this.noteStreamingHidingService.processHiding(note, this.user?.id ?? null); - if (shouldSkip) return; + const filtered = await this.noteStreamingHidingService.filter(note, this.user?.id ?? null); + if (!filtered) return; + // eslint-disable-next-line no-param-reassign -- これ以降元の Note オブジェクトは見てはいけないので、いっそ再代入した方が安全 + note = filtered; if (this.user) { if (isRenotePacked(note) && !isQuotePacked(note)) { diff --git a/packages/backend/src/server/web/HtmlTemplateService.ts b/packages/backend/src/server/web/HtmlTemplateService.ts index 8ff985530d..36272c81d5 100644 --- a/packages/backend/src/server/web/HtmlTemplateService.ts +++ b/packages/backend/src/server/web/HtmlTemplateService.ts @@ -3,9 +3,9 @@ * SPDX-License-Identifier: AGPL-3.0-only */ -import { dirname } from 'node:path'; +import { dirname, resolve } from 'node:path'; import { fileURLToPath } from 'node:url'; -import { promises as fsp } from 'node:fs'; +import { promises as fsp, existsSync } from 'node:fs'; import { languages } from 'i18n/const'; import { Injectable, Inject } from '@nestjs/common'; import { DI } from '@/di-symbols.js'; @@ -13,21 +13,34 @@ import { bindThis } from '@/decorators.js'; import { htmlSafeJsonStringify } from '@/misc/json-stringify-html-safe.js'; import { MetaEntityService } from '@/core/entities/MetaEntityService.js'; import type { FastifyReply } from 'fastify'; +import type { Manifest } from 'vite'; import type { Config } from '@/config.js'; import type { MiMeta } from '@/models/Meta.js'; -import type { CommonData } from './views/_.js'; +import type { CommonData, ViteFiles } from './views/_.js'; const _filename = fileURLToPath(import.meta.url); const _dirname = dirname(_filename); -const frontendVitePublic = `${_dirname}/../../../../frontend/public/`; -const frontendEmbedVitePublic = `${_dirname}/../../../../frontend-embed/public/`; +let rootDir = _dirname; +// 見つかるまで上に遡る +while (!existsSync(resolve(rootDir, 'packages'))) { + const parentDir = dirname(rootDir); + if (parentDir === rootDir) { + throw new Error('Cannot find root directory'); + } + rootDir = parentDir; +} + +const frontendViteBuilt = resolve(rootDir, 'built/_frontend_vite_'); +const frontendEmbedViteBuilt = resolve(rootDir, 'built/_frontend_embed_vite_'); @Injectable() export class HtmlTemplateService { - private frontendBootloadersFetched = false; + private frontendAssetsFetched = false; + public frontendViteFiles: ViteFiles | null = null; public frontendBootloaderJs: string | null = null; public frontendBootloaderCss: string | null = null; + public frontendEmbedViteFiles: ViteFiles | null = null; public frontendEmbedBootloaderJs: string | null = null; public frontendEmbedBootloaderCss: string | null = null; @@ -42,18 +55,92 @@ export class HtmlTemplateService { ) { } + // 初期ロードで読み込むべきファイルのパスを収集する。 + // See https://ja.vite.dev/guide/backend-integration @bindThis - private async prepareFrontendBootloaders() { - if (this.frontendBootloadersFetched) return; - this.frontendBootloadersFetched = true; + private collectViteAssetFiles(manifest: Manifest): ViteFiles { + const entryFile = Object.values(manifest).find((chunk) => chunk.isEntry); + if (!entryFile) return { + entryJs: null, + css: [], + modulePreloads: [], + }; - const [bootJs, bootCss, embedBootJs, embedBootCss] = await Promise.all([ - fsp.readFile(`${frontendVitePublic}loader/boot.js`, 'utf-8').catch(() => null), - fsp.readFile(`${frontendVitePublic}loader/style.css`, 'utf-8').catch(() => null), - fsp.readFile(`${frontendEmbedVitePublic}loader/boot.js`, 'utf-8').catch(() => null), - fsp.readFile(`${frontendEmbedVitePublic}loader/style.css`, 'utf-8').catch(() => null), + const seenChunkIds = new Set(); + const cssFiles = new Set(); + const modulePreloads = new Set(); + + if (entryFile.css) { + entryFile.css.forEach((css) => cssFiles.add(css)); + } + + if (entryFile.imports != null && Array.isArray(entryFile.imports)) { + function collectImports(imports: string[], recursive = false) { + for (const importId of imports) { + if (seenChunkIds.has(importId)) continue; + seenChunkIds.add(importId); + + const importedChunk = manifest[importId]; + if (!importedChunk) return; + + if (importedChunk.css) { + importedChunk.css.forEach((css) => cssFiles.add(css)); + } + + if (importedChunk.imports != null && Array.isArray(importedChunk.imports)) { + collectImports(importedChunk.imports, true); + } + + if (!recursive) { + modulePreloads.add(importedChunk.file); + } + } + } + + collectImports(entryFile.imports); + } + + return { + entryJs: entryFile.file, + css: Array.from(cssFiles), + modulePreloads: Array.from(modulePreloads), + }; + } + + @bindThis + private async prepareFrontendAssets() { + if (this.frontendAssetsFetched) return; + this.frontendAssetsFetched = true; + + const [ + bootJs, + bootCss, + embedBootJs, + embedBootCss, + ] = await Promise.all([ + fsp.readFile(resolve(frontendViteBuilt, 'loader/boot.js'), 'utf-8').catch(() => null), + fsp.readFile(resolve(frontendViteBuilt, 'loader/style.css'), 'utf-8').catch(() => null), + fsp.readFile(resolve(frontendEmbedViteBuilt, 'loader/boot.js'), 'utf-8').catch(() => null), + fsp.readFile(resolve(frontendEmbedViteBuilt, 'loader/style.css'), 'utf-8').catch(() => null), ]); + let feViteManifest: Manifest | null = null; + let embedFeViteManifest: Manifest | null = null; + + if (this.config.frontendManifestExists) { + const manifestContent = await fsp.readFile(resolve(frontendViteBuilt, 'manifest.json'), 'utf-8').catch(() => null); + feViteManifest = manifestContent ? JSON.parse(manifestContent) : null; + } + + if (this.config.frontendEmbedManifestExists) { + const manifestContent = await fsp.readFile(resolve(frontendEmbedViteBuilt, 'manifest.json'), 'utf-8').catch(() => null); + embedFeViteManifest = manifestContent ? JSON.parse(manifestContent) : null; + } + + if (feViteManifest != null) { + this.frontendViteFiles = this.collectViteAssetFiles(feViteManifest); + } + if (bootJs != null) { this.frontendBootloaderJs = bootJs; } @@ -62,6 +149,10 @@ export class HtmlTemplateService { this.frontendBootloaderCss = bootCss; } + if (embedFeViteManifest != null) { + this.frontendEmbedViteFiles = this.collectViteAssetFiles(embedFeViteManifest); + } + if (embedBootJs != null) { this.frontendEmbedBootloaderJs = embedBootJs; } @@ -73,7 +164,7 @@ export class HtmlTemplateService { @bindThis public async getCommonData(): Promise { - await this.prepareFrontendBootloaders(); + await this.prepareFrontendAssets(); return { version: this.config.version, @@ -90,8 +181,10 @@ export class HtmlTemplateService { metaJson: htmlSafeJsonStringify(await this.metaEntityService.packDetailed(this.meta)), now: Date.now(), federationEnabled: this.meta.federation !== 'none', + frontendViteFiles: this.frontendViteFiles, frontendBootloaderJs: this.frontendBootloaderJs, frontendBootloaderCss: this.frontendBootloaderCss, + frontendEmbedViteFiles: this.frontendEmbedViteFiles, frontendEmbedBootloaderJs: this.frontendEmbedBootloaderJs, frontendEmbedBootloaderCss: this.frontendEmbedBootloaderCss, }; diff --git a/packages/backend/src/server/web/views/_.ts b/packages/backend/src/server/web/views/_.ts index ac7418f362..f9b290b13a 100644 --- a/packages/backend/src/server/web/views/_.ts +++ b/packages/backend/src/server/web/views/_.ts @@ -24,6 +24,12 @@ export type MinimumCommonData = { config: Config; }; +export type ViteFiles = { + entryJs: string | null; + css: string[]; + modulePreloads: string[]; +}; + export type CommonData = MinimumCommonData & { langs: string[]; instanceName: string; @@ -36,8 +42,10 @@ export type CommonData = MinimumCommonData & { instanceUrl: string; now: number; federationEnabled: boolean; + frontendViteFiles: ViteFiles | null; frontendBootloaderJs: string | null; frontendBootloaderCss: string | null; + frontendEmbedViteFiles: ViteFiles | null; frontendEmbedBootloaderJs: string | null; frontendEmbedBootloaderCss: string | null; metaJson?: string; diff --git a/packages/backend/src/server/web/views/base-embed.tsx b/packages/backend/src/server/web/views/base-embed.tsx index 011b66592e..a656bb28a7 100644 --- a/packages/backend/src/server/web/views/base-embed.tsx +++ b/packages/backend/src/server/web/views/base-embed.tsx @@ -46,11 +46,11 @@ export function BaseEmbed(props: PropsWithChildren