diff --git a/.config/example.yml b/.config/example.yml index c127eaae22..489cceec34 100644 --- a/.config/example.yml +++ b/.config/example.yml @@ -105,6 +105,16 @@ port: 3000 # socket: /path/to/misskey.sock # chmodSocket: '777' +# Proxy trust settings +# +# Changes how the server interpret the origin IP of the request. +# +# Any format supported by Fastify is accepted. +# Default: trust all proxies (i.e. trustProxy: true) +# See: https://fastify.dev/docs/latest/reference/server/#trustproxy +# +# trustProxy: 1 + # ┌──────────────────────────┐ #───┘ PostgreSQL configuration └──────────────────────────────── diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index 91813cebc3..fba3cc4920 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -81,7 +81,7 @@ jobs: cache: 'pnpm' - run: pnpm i --frozen-lockfile - name: Restore eslint cache - uses: actions/cache@v4.2.4 + uses: actions/cache@v4.3.0 with: path: ${{ env.eslint-cache-path }} key: eslint-${{ env.eslint-cache-version }}-${{ matrix.workspace }}-${{ hashFiles('**/pnpm-lock.yaml') }}-${{ github.ref_name }}-${{ github.sha }} diff --git a/.github/workflows/report-api-diff.yml b/.github/workflows/report-api-diff.yml index 1170f898ce..f24cd7d30e 100644 --- a/.github/workflows/report-api-diff.yml +++ b/.github/workflows/report-api-diff.yml @@ -16,7 +16,7 @@ jobs: # api-artifact steps: - name: Download artifact - uses: actions/github-script@v7.0.1 + uses: actions/github-script@v7.1.0 with: script: | const fs = require('fs'); diff --git a/.github/workflows/storybook.yml b/.github/workflows/storybook.yml index 7f964ef1d7..1611706ac5 100644 --- a/.github/workflows/storybook.yml +++ b/.github/workflows/storybook.yml @@ -90,7 +90,7 @@ jobs: env: CHROMATIC_PROJECT_TOKEN: ${{ secrets.CHROMATIC_PROJECT_TOKEN }} - name: Notify that Chromatic detects changes - uses: actions/github-script@v7.0.1 + uses: actions/github-script@v7.1.0 if: github.event_name != 'pull_request_target' && steps.chromatic_push.outputs.success == 'false' with: github-token: ${{ secrets.GITHUB_TOKEN }} diff --git a/CHANGELOG.md b/CHANGELOG.md index 3b6a1f6f66..01213bd8c3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,32 @@ +## 2025.10.0 + +### NOTE +- pnpm 10.16.0 が必要です + +### General +- Feat: 予約投稿ができるようになりました + - デフォルトで作成可能数は1になっています。適宜ロールのポリシーで設定を行ってください。 +- Enhance: 広告ごとにセンシティブフラグを設定できるようになりました +- Enhance: 依存関係の更新 +- Enhance: 翻訳の更新 + +### Client +- Feat: アカウントのQRコードを表示・読み取りできるようになりました +- Feat: 動画を圧縮してアップロードできるようになりました +- Feat: (実験的) ブラウザ上でノートの翻訳を行えるように +- Enhance: チャットの日本語名称がダイレクトメッセージに戻るとともに、ベータ版機能ではなくなりました +- Enhance: 画像編集にマスクエフェクト(塗りつぶし、ぼかし、モザイク)を追加 +- Enhance: 画像編集の集中線エフェクトを強化 +- Enhance: ウォーターマークにアカウントのQRコードを追加できるように +- Enhance: テーマをドラッグ&ドロップできるように +- Enhance: 絵文字ピッカーのサイズをより大きくできるように +- Enhance: 時刻計算のための基準値を一か所で管理するようにし、パフォーマンスを向上 +- Fix: iOSで、デバイスがダークモードだと初回読み込み時にエラーになる問題を修正 +- Fix: アクティビティウィジェットのグラフモードが動作しない問題を修正 + +### Server +- Enhance: ユーザーIPを確実に取得できるために設定ファイルにFastifyOptions.trustProxyを追加しました + ## 2025.9.0 ### Client diff --git a/idea/MkAnimatedBg.dotted-ripples.vue b/idea/MkAnimatedBg.dotted-ripples.vue new file mode 100644 index 0000000000..f8f809c8ce --- /dev/null +++ b/idea/MkAnimatedBg.dotted-ripples.vue @@ -0,0 +1,232 @@ + + + + + + + diff --git a/idea/MkAnimatedBg.dotted.vue b/idea/MkAnimatedBg.dotted.vue new file mode 100644 index 0000000000..7f68b8972a --- /dev/null +++ b/idea/MkAnimatedBg.dotted.vue @@ -0,0 +1,190 @@ + + + + + + + diff --git a/locales/ar-SA.yml b/locales/ar-SA.yml index 24a2caea35..117c38b677 100644 --- a/locales/ar-SA.yml +++ b/locales/ar-SA.yml @@ -1603,5 +1603,9 @@ _imageEffector: _fxProps: scale: "الحجم" size: "الحجم" + offset: "الموضع" color: "اللون" opacity: "الشفافية" +_qr: + showTabTitle: "المظهر" + raw: "نص" diff --git a/locales/bn-BD.yml b/locales/bn-BD.yml index 2b0645b77d..395cee114a 100644 --- a/locales/bn-BD.yml +++ b/locales/bn-BD.yml @@ -1364,3 +1364,6 @@ _imageEffector: color: "রং" opacity: "অস্বচ্ছতা" lightness: "উজ্জ্বল করুন" +_qr: + showTabTitle: "প্রদর্শন" + raw: "লেখা" diff --git a/locales/ca-ES.yml b/locales/ca-ES.yml index 63878bf1b7..bb1a42232d 100644 --- a/locales/ca-ES.yml +++ b/locales/ca-ES.yml @@ -253,6 +253,7 @@ noteDeleteConfirm: "Segur que voleu eliminar aquesta publicació?" pinLimitExceeded: "No podeu fixar més publicacions" done: "Fet" processing: "S'està processant..." +preprocessing: "Preparant" preview: "Vista prèvia" default: "Per defecte" defaultValueIs: "Per defecte: {value}" @@ -1316,6 +1317,7 @@ acknowledgeNotesAndEnable: "Activa'l després de comprendre els possibles perill federationSpecified: "Aquest servidor treballa amb una federació de llistes blanques. No pot interactuar amb altres servidors que no siguin els especificats per l'administrador." federationDisabled: "La unió es troba deshabilitada en aquest servidor. No es pot interactuar amb usuaris d'altres servidors." draft: "Esborrany " +draftsAndScheduledNotes: "Esborranys i publicacions programades" confirmOnReact: "Confirmar en reaccionar" reactAreYouSure: "Vols reaccionar amb \"{emoji}\"?" markAsSensitiveConfirm: "Vols marcar aquest contingut com a sensible?" @@ -1343,6 +1345,8 @@ postForm: "Formulari de publicació" textCount: "Nombre de caràcters " information: "Informació" chat: "Xat" +directMessage: "Xateja amb aquest usuari" +directMessage_short: "Missatge" migrateOldSettings: "Migrar la configuració anterior" migrateOldSettings_description: "Normalment això es fa automàticament, però si la transició no es fa, el procés es pot iniciar manualment. S'esborrarà la configuració actual." compress: "Comprimir " @@ -1370,6 +1374,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.
Alta, redueix la mida de l'arxiu però també la qualitat de la imatge." +defaultCompressionLevel: "Nivell de compressió predeterminat" +defaultCompressionLevel_description: "Si el redueixes augmentaràs la qualitat de la imatge, però la mida de l'arxiu serà més gran.
Si augmentes l'opció redueixes la mida de l'arxiu i la qualitat de la imatge és pitjor." inMinutes: "Minut(s)" inDays: "Di(a)(es)" safeModeEnabled: "Mode segur activat" @@ -1377,10 +1383,26 @@ pluginsAreDisabledBecauseSafeMode: "Els afegits no estan activats perquè el mod customCssIsDisabledBecauseSafeMode: "El CSS personalitzat no s'aplica perquè el mode segur es troba activat." themeIsDefaultBecauseSafeMode: "El tema predeterminat es farà servir mentre el mode segur estigui activat. Una vegada es desactivi el mode segur es restablirà el tema escollit." thankYouForTestingBeta: "Gràcies per ajudar-nos a provar la versió beta!" +createUserSpecifiedNote: "Crear notes especificades per l'usuari " +schedulePost: "Programar una nota" +scheduleToPostOnX: "Programar una nota per {x}" +scheduledToPostOnX: "S'ha programat la nota per {x}" +schedule: "Programa" +scheduled: "Programat" +_compression: + _quality: + high: "Qualitat alta" + medium: "Qualitat mitjana" + low: "Qualitat baixa" + _size: + large: "Mida gran" + medium: "Mida mitjana" + small: "Mida petita" _order: newest: "Més recent" oldest: "Antigues primer" _chat: + messages: "Missatge" noMessagesYet: "Encara no tens missatges " newMessage: "Missatge nou" individualChat: "Xat individual " @@ -1537,7 +1559,7 @@ _announcement: needConfirmationToRead: "Es necessita confirmació de lectura de la notificació " needConfirmationToReadDescription: "Si s'activa es mostrarà un diàleg per confirmar la lectura d'aquesta notificació. A més aquesta notificació serà exclosa de qualsevol funcionalitat com \"Marcar tot com a llegit\"." end: "Final de la notificació " - tooManyActiveAnnouncementDescription: "Tenir massa notificacions actives pot empitjorar l'experiència de l'usuari. Considera finalitzar els avisos que siguin antics." + tooManyActiveAnnouncementDescription: "Tenir masses notificacions actives pot empitjorar l'experiència de l'usuari. Considera finalitzar els avisos que siguin antics." readConfirmTitle: "Marcar com llegida?" readConfirmText: "Això marcarà el contingut de \"{title}\" com llegit." shouldNotBeUsedToPresentPermanentInfo: "Ja que l'ús de notificacions pot impactar l'experiència dels nous usuaris, és recomanable fer servir les notificacions amb el flux d'informació en comptes de fer-les servir en un únic bloc." @@ -2018,6 +2040,7 @@ _role: uploadableFileTypes_caption: "Especifica el tipus MIME. Es poden especificar diferents tipus MIME separats amb una nova línia, i es poden especificar comodins amb asteriscs (*). (Per exemple: image/*)" uploadableFileTypes_caption2: "Pot que no sigui possible determinar el tipus MIME d'alguns arxius. Per permetre aquests tipus d'arxius afegeix {x} a les especificacions." noteDraftLimit: "Nombre possible d'esborranys de notes al servidor" + scheduledNoteLimit: "Màxim nombre de notes programades que es poden crear simultàniament" watermarkAvailable: "Pots fer servir la marca d'aigua" _condition: roleAssignedTo: "Assignat a rols manuals" @@ -2076,7 +2099,7 @@ _ad: timezoneinfo: "El dia de la setmana ve determinat del fus horari del servidor." adsSettings: "Configurar la publicitat" notesPerOneAd: "Interval d'emplaçament publicitari en temps real (Notes per anuncis)" - setZeroToDisable: "Ajusta aquest valor a 0 per deshabilitar l'actualització d'anuncis en temps real" + setZeroToDisable: "Ajusta aquest valor a 0 per deshabilitar l'actualització de publicitat en temps real" adsTooClose: "L'interval actual pot fer que l'experiència de l'usuari sigui dolenta perquè l'interval és molt baix." _forgotPassword: enterEmail: "Escriu l'adreça de correu electrònic amb la que et vas registrar. S'enviarà un correu electrònic amb un enllaç perquè puguis canviar-la." @@ -2453,7 +2476,7 @@ _widgets: chooseList: "Tria una llista" clicker: "Clicker" birthdayFollowings: "Usuaris que fan l'aniversari avui" - chat: "Xat" + chat: "Xateja amb aquest usuari" _cw: hide: "Amagar" show: "Carregar més" @@ -2643,6 +2666,8 @@ _notification: youReceivedFollowRequest: "Has rebut una petició de seguiment" yourFollowRequestAccepted: "La teva petició de seguiment ha sigut acceptada" pollEnded: "Ja pots veure els resultats de l'enquesta " + scheduledNotePosted: "Una nota programada ha sigut publicada" + scheduledNotePostFailed: "Ha fallat la publicació d'una nota programada" newNote: "Nota nova" unreadAntennaNote: "Antena {name}" roleAssigned: "Rol assignat " @@ -2722,7 +2747,7 @@ _deck: mentions: "Mencions" direct: "Publicacions directes" roleTimeline: "Línia de temps dels rols" - chat: "Xat" + chat: "Xateja amb aquest usuari" _dialog: charactersExceeded: "Has arribat al màxim de caràcters! Actualment és {current} de {max}" charactersBelow: "Ets per sota del mínim de caràcters! Actualment és {current} de {min}" @@ -3168,7 +3193,9 @@ _watermarkEditor: opacity: "Opacitat" scale: "Mida" text: "Text" + qr: "Codi QR" position: "Posició " + margin: "Marge" type: "Tipus" image: "Imatges" advanced: "Avançat" @@ -3183,6 +3210,7 @@ _watermarkEditor: polkadotSubDotOpacity: "Opacitat del lunar secundari" polkadotSubDotRadius: "Mida del lunar secundari" polkadotSubDotDivisions: "Nombre de punts secundaris" + leaveBlankToAccountUrl: "Si deixes aquest camp buit, es farà servir l'URL del teu compte" _imageEffector: title: "Efecte" addEffect: "Afegeix un efecte" @@ -3194,6 +3222,8 @@ _imageEffector: mirror: "Mirall" invert: "Inversió cromàtica " grayscale: "Monocrom " + blur: "Desenfocament" + pixelate: "Mosaic" colorAdjust: "Correcció de color" colorClamp: "Compressió cromàtica " colorClampAdvanced: "Compressió de cromàtica avançada " @@ -3205,10 +3235,14 @@ _imageEffector: checker: "Escacs" blockNoise: "Bloqueig de soroll" tearing: "Trencament d'imatge " + fill: "Omplir" _fxProps: angle: "Angle" scale: "Mida" size: "Mida" + radius: "Radi" + samples: "Mida de la mostra" + offset: "Posició " color: "Color" opacity: "Opacitat" normalize: "Normalitzar" @@ -3237,6 +3271,7 @@ _imageEffector: zoomLinesThreshold: "Amplada de línia a l'augmentar " zoomLinesMaskSize: "Diàmetre del centre" zoomLinesBlack: "Obscurir" + circle: "Cercle" drafts: "Esborrany " _drafts: select: "Seleccionar esborrany" @@ -3252,3 +3287,22 @@ _drafts: restoreFromDraft: "Restaurar des dels esborranys" restore: "Restaurar esborrany" listDrafts: "Llistat d'esborranys" + schedule: "Programació esborranys" + listScheduledNotes: "Llista de notes programades" + cancelSchedule: "Cancel·lar la programació" +qr: "Codi QR" +_qr: + showTabTitle: "Veure" + readTabTitle: "Escanejar " + shareTitle: "{name} {acct}" + shareText: "Segueix-me al Fediverse" + chooseCamera: "Seleccionar càmera " + cannotToggleFlash: "No es pot activar el flaix" + turnOnFlash: "Activar el flaix" + turnOffFlash: "Apagar el flaix" + startQr: "Reiniciar el lector de codis QR" + stopQr: "Parar el lector de codis QR" + noQrCodeFound: "No s'ha trobat cap codi QR" + scanFile: "Escanejar la imatge des del dispositiu" + raw: "Text" + mfm: "MFM" diff --git a/locales/cs-CZ.yml b/locales/cs-CZ.yml index 21be386e26..0d2ddccc60 100644 --- a/locales/cs-CZ.yml +++ b/locales/cs-CZ.yml @@ -2057,6 +2057,10 @@ _imageEffector: _fxProps: scale: "Velikost" size: "Velikost" + offset: "Pozice" color: "Barva" opacity: "Průhlednost" lightness: "Zesvětlit" +_qr: + showTabTitle: "Zobrazit" + raw: "Text" diff --git a/locales/de-DE.yml b/locales/de-DE.yml index 2d7d41bfdd..ba74564fc3 100644 --- a/locales/de-DE.yml +++ b/locales/de-DE.yml @@ -1340,6 +1340,7 @@ postForm: "Notizfenster" textCount: "Zeichenanzahl" information: "Über" chat: "Chat" +directMessage: "Mit dem Benutzer chatten" migrateOldSettings: "Alte Client-Einstellungen migrieren" migrateOldSettings_description: "Dies sollte normalerweise automatisch geschehen, aber wenn die Migration aus irgendeinem Grund nicht erfolgreich war, kannst du den Migrationsprozess selbst manuell auslösen. Die aktuellen Konfigurationsinformationen werden dabei überschrieben." compress: "Komprimieren" @@ -2433,7 +2434,7 @@ _widgets: chooseList: "Liste auswählen" clicker: "Klickzähler" birthdayFollowings: "Nutzer, die heute Geburtstag haben" - chat: "Chat" + chat: "Mit dem Benutzer chatten" _cw: hide: "Inhalt verbergen" show: "Inhalt anzeigen" @@ -2702,7 +2703,7 @@ _deck: mentions: "Erwähnungen" direct: "Direktnachrichten" roleTimeline: "Rollenchronik" - chat: "Chat" + chat: "Mit dem Benutzer chatten" _dialog: charactersExceeded: "Maximallänge überschritten! Momentan {current} von {max}" charactersBelow: "Minimallänge unterschritten! Momentan {current} von {min}" @@ -3176,6 +3177,7 @@ _imageEffector: angle: "Winkel" scale: "Größe" size: "Größe" + offset: "Position" color: "Farbe" opacity: "Transparenz" lightness: "Erhellen" @@ -3193,3 +3195,6 @@ _drafts: restoreFromDraft: "Aus Entwurf wiederherstellen" restore: "Wiederherstellen" listDrafts: "Liste der Entwürfe" +_qr: + showTabTitle: "Anzeigeart" + raw: "Text" diff --git a/locales/en-US.yml b/locales/en-US.yml index 9c02e83021..049ad54d82 100644 --- a/locales/en-US.yml +++ b/locales/en-US.yml @@ -253,6 +253,7 @@ noteDeleteConfirm: "Are you sure you want to delete this note?" pinLimitExceeded: "You cannot pin any more notes" done: "Done" processing: "Processing..." +preprocessing: "Preparing..." preview: "Preview" default: "Default" defaultValueIs: "Default: {value}" @@ -1316,6 +1317,7 @@ acknowledgeNotesAndEnable: "Turn on after understanding the precautions." federationSpecified: "This server is operated in a whitelist federation. Interacting with servers other than those designated by the administrator is not allowed." federationDisabled: "Federation is disabled on this server. You cannot interact with users on other servers." draft: "Drafts" +draftsAndScheduledNotes: "Drafts and scheduled notes" confirmOnReact: "Confirm when reacting" reactAreYouSure: "Would you like to add a \"{emoji}\" reaction?" markAsSensitiveConfirm: "Do you want to set this media as sensitive?" @@ -1343,6 +1345,8 @@ postForm: "Posting form" textCount: "Character count" information: "About" chat: "Chat" +directMessage: "Chat with user" +directMessage_short: "Message" migrateOldSettings: "Migrate old client settings" migrateOldSettings_description: "This should be done automatically but if for some reason the migration was not successful, you can trigger the migration process yourself manually. The current configuration information will be overwritten." compress: "Compress" @@ -1370,6 +1374,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.
Higher level reduce file size, but reduce image quality." +defaultCompressionLevel: "Default compression level" +defaultCompressionLevel_description: "Lower compression preserves quality but increases file size.
Higher compression reduces file size but lowers quality." inMinutes: "Minute(s)" inDays: "Day(s)" safeModeEnabled: "Safe mode is enabled" @@ -1377,10 +1383,26 @@ pluginsAreDisabledBecauseSafeMode: "All plugins are disabled because safe mode i customCssIsDisabledBecauseSafeMode: "Custom CSS is not applied because safe mode is enabled." themeIsDefaultBecauseSafeMode: "While safe mode is active, the default theme is used. Disabling safe mode will revert these changes." thankYouForTestingBeta: "Thank you for helping us test the beta version!" +createUserSpecifiedNote: "Create a direct note" +schedulePost: "Schedule note" +scheduleToPostOnX: "Scheduled to note on {x}" +scheduledToPostOnX: "Note is scheduled for {x}" +schedule: "Schedule" +scheduled: "Scheduled" +_compression: + _quality: + high: "High quality" + medium: "Medium quality" + low: "Low quality" + _size: + large: "Large size" + medium: "Medium size" + small: "Small size" _order: newest: "Newest First" oldest: "Oldest First" _chat: + messages: "Messages" noMessagesYet: "No messages yet" newMessage: "New message" individualChat: "Private Chat" @@ -2018,6 +2040,7 @@ _role: uploadableFileTypes_caption: "Specifies the allowed MIME/file types. Multiple MIME types can be specified by separating them with a new line, and wildcards can be specified with an asterisk (*). (e.g., image/*)" uploadableFileTypes_caption2: "Some files types might fail to be detected. To allow such files, add {x} to the specification." noteDraftLimit: "Number of possible drafts of server notes" + scheduledNoteLimit: "Maximum number of simultaneous scheduled notes" watermarkAvailable: "Watermark function" _condition: roleAssignedTo: "Assigned to manual roles" @@ -2453,7 +2476,7 @@ _widgets: chooseList: "Select a list" clicker: "Clicker" birthdayFollowings: "Today's Birthdays" - chat: "Chat" + chat: "Chat with user" _cw: hide: "Hide" show: "Show content" @@ -2643,6 +2666,8 @@ _notification: youReceivedFollowRequest: "You've received a follow request" yourFollowRequestAccepted: "Your follow request was accepted" pollEnded: "Poll results have become available" + scheduledNotePosted: "Scheduled note has been posted" + scheduledNotePostFailed: "Failed to post scheduled note" newNote: "New note" unreadAntennaNote: "Antenna {name}" roleAssigned: "Role given" @@ -2722,7 +2747,7 @@ _deck: mentions: "Mentions" direct: "Direct notes" roleTimeline: "Role Timeline" - chat: "Chat" + chat: "Chat with user" _dialog: charactersExceeded: "You've exceeded the maximum character limit! Currently at {current} of {max}." charactersBelow: "You're below the minimum character limit! Currently at {current} of {min}." @@ -3168,7 +3193,9 @@ _watermarkEditor: opacity: "Opacity" scale: "Size" text: "Text" + qr: "QR Code" position: "Position" + margin: "Margin" type: "Type" image: "Images" advanced: "Advanced" @@ -3183,6 +3210,7 @@ _watermarkEditor: polkadotSubDotOpacity: "Opacity of the secondary dot" polkadotSubDotRadius: "Size of the secondary dot" polkadotSubDotDivisions: "Number of sub-dots." + leaveBlankToAccountUrl: "Leave blank to use account URL" _imageEffector: title: "Effects" addEffect: "Add Effects" @@ -3194,6 +3222,8 @@ _imageEffector: mirror: "Mirror" invert: "Invert Colors" grayscale: "Grayscale" + blur: "Blur" + pixelate: "Pixelate" colorAdjust: "Color Correction" colorClamp: "Color Compression" colorClampAdvanced: "Color Compression (Advanced)" @@ -3205,10 +3235,14 @@ _imageEffector: checker: "Checker" blockNoise: "Block Noise" tearing: "Tearing" + fill: "Fill" _fxProps: angle: "Angle" scale: "Size" size: "Size" + radius: "Radius" + samples: "Sample count" + offset: "Position" color: "Color" opacity: "Opacity" normalize: "Normalize" @@ -3237,6 +3271,7 @@ _imageEffector: zoomLinesThreshold: "Zoom line width" zoomLinesMaskSize: "Center diameter" zoomLinesBlack: "Make black" + circle: "Circular" drafts: "Drafts" _drafts: select: "Select Draft" @@ -3252,3 +3287,22 @@ _drafts: restoreFromDraft: "Restore from Draft" restore: "Restore" listDrafts: "List of Drafts" + schedule: "Schedule note" + listScheduledNotes: "Scheduled notes list" + cancelSchedule: "Cancel schedule" +qr: "QR Code" +_qr: + showTabTitle: "Display" + readTabTitle: "Scan" + shareTitle: "{name} {acct}" + shareText: "Follow me on the Fediverse!" + chooseCamera: "Choose camera" + cannotToggleFlash: "Unable to toggle flashlight" + turnOnFlash: "Turn on flashlight" + turnOffFlash: "Turn off flashlight" + startQr: "Resume QR code reader" + stopQr: "Stop QR code reader" + noQrCodeFound: "No QR code found" + scanFile: "Scan image from device" + raw: "Text" + mfm: "MFM" diff --git a/locales/es-ES.yml b/locales/es-ES.yml index 8a1d2c458b..5ba924c78a 100644 --- a/locales/es-ES.yml +++ b/locales/es-ES.yml @@ -253,6 +253,7 @@ noteDeleteConfirm: "¿Quieres borrar esta nota?" pinLimitExceeded: "Ya no se pueden fijar más notas" done: "Terminado" processing: "Procesando..." +preprocessing: "Preparando" preview: "Vista previa" default: "Predeterminado" defaultValueIs: "Por defecto: {value}" @@ -910,7 +911,7 @@ pubSub: "Cuentas Pub/Sub" lastCommunication: "Última comunicación" resolved: "Resuelto" unresolved: "Sin resolver" -breakFollow: "Dejar de seguir" +breakFollow: "Eliminar seguidor" breakFollowConfirm: "¿Quieres dejar de seguir?" itsOn: "¡Está encendido!" itsOff: "¡Está apagado!" @@ -1316,6 +1317,7 @@ acknowledgeNotesAndEnable: "Activar después de comprender las precauciones" federationSpecified: "Este servidor opera en una federación de listas blancas. No puede interactuar con otros servidores que no sean los especificados por el administrador." federationDisabled: "La federación está desactivada en este servidor. No puede interactuar con usuarios de otros servidores" draft: "Borrador" +draftsAndScheduledNotes: "Borradores y notas programadas" confirmOnReact: "Confirmar la reacción" reactAreYouSure: "¿Quieres añadir una reacción «{emoji}»?" markAsSensitiveConfirm: "¿Desea establecer este medio multimedia(Imagen,vídeo...) como sensible?" @@ -1343,6 +1345,8 @@ postForm: "Formulario" textCount: "caracteres" information: "Información" chat: "Chat" +directMessage: "Chatear" +directMessage_short: "Mensaje" migrateOldSettings: "Migrar la configuración anterior" migrateOldSettings_description: "Esto debería hacerse automáticamente, pero si por alguna razón la migración no ha tenido éxito, puede activar usted mismo el proceso de migración manualmente. Se sobrescribirá la información de configuración actual." compress: "Comprimir" @@ -1370,6 +1374,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.
Alta, reduce la medida del archivo pero también la calidad de la imagen." +defaultCompressionLevel: "Nivel de compresión predeterminado" +defaultCompressionLevel_description: "Al reducir el ajuste se conserva la calidad, pero aumenta el tamaño del archivo.
Al aumentar el ajuste se reduce el tamaño del archivo, pero disminuye la calidad." inMinutes: "Minutos" inDays: "Días" safeModeEnabled: "El modo seguro está activado" @@ -1377,10 +1383,26 @@ pluginsAreDisabledBecauseSafeMode: "El modo seguro está activado, por lo que to customCssIsDisabledBecauseSafeMode: "El modo seguro está activado, por lo que no se aplica el CSS personalizado." themeIsDefaultBecauseSafeMode: "Mientras el modo seguro esté activado, se utilizará el tema predeterminado. Cuando se desactive el modo seguro, se volverá al tema original." thankYouForTestingBeta: "¡Gracias por tu colaboración en la prueba de la versión beta!" +createUserSpecifiedNote: "Crear notas especificadas por el usuario" +schedulePost: "Programar una nota" +scheduleToPostOnX: "Programar una nota para {x}" +scheduledToPostOnX: "La nota está programada para {x}." +schedule: "Programado" +scheduled: "Programado" +_compression: + _quality: + high: "Calidad alta" + medium: "Calidad media" + low: "Calidad baja" + _size: + large: "Tamaño grande" + medium: "Tamaño mediano" + small: "Tamaño pequeño" _order: newest: "Los más recientes primero" oldest: "Los más antiguos primero" _chat: + messages: "Mensaje" noMessagesYet: "Aún no hay mensajes" newMessage: "Mensajes nuevos" individualChat: "Chat individual" @@ -2018,6 +2040,7 @@ _role: uploadableFileTypes_caption: "Especifica los tipos MIME/archivos permitidos. Se pueden especificar varios tipos MIME separándolos con una nueva línea, y se pueden especificar comodines con un asterisco (*). (por ejemplo, image/*)" uploadableFileTypes_caption2: "Es posible que no se detecten algunos tipos de archivos. Para permitir estos archivos, añade {x} a la especificación." noteDraftLimit: "Número de posibles borradores de notas del servidor" + scheduledNoteLimit: "Máximo número de notas programadas que se pueden crear simultáneamente." watermarkAvailable: "Disponibilidad de la función de marca de agua" _condition: roleAssignedTo: "Asignado a roles manuales" @@ -2453,7 +2476,7 @@ _widgets: chooseList: "Seleccione una lista" clicker: "Cliqueador" birthdayFollowings: "Hoy cumplen años" - chat: "Chat" + chat: "Chatear" _cw: hide: "Ocultar" show: "Ver más" @@ -2643,6 +2666,8 @@ _notification: youReceivedFollowRequest: "Has mandado una solicitud de seguimiento" yourFollowRequestAccepted: "Tu solicitud de seguimiento fue aceptada" pollEnded: "Estan disponibles los resultados de la encuesta" + scheduledNotePosted: "Una nota programada ha sido publicada" + scheduledNotePostFailed: "Ha fallado la publicación de una nota programada" newNote: "Nueva nota" unreadAntennaNote: "Antena {name}" roleAssigned: "Rol asignado" @@ -2722,7 +2747,7 @@ _deck: mentions: "Menciones" direct: "Notas directas" roleTimeline: "Linea de tiempo del rol" - chat: "Chat" + chat: "Chatear" _dialog: charactersExceeded: "¡Has excedido el límite de caracteres! Actualmente {current} de {max}." charactersBelow: "¡Estás por debajo del límite de caracteres! Actualmente {current} de {min}." @@ -3168,7 +3193,9 @@ _watermarkEditor: opacity: "Opacidad" scale: "Tamaño" text: "Texto" + qr: "Código QR" position: "Posición" + margin: "Margen" type: "Tipo" image: "Imágenes" advanced: "Avanzado" @@ -3183,6 +3210,7 @@ _watermarkEditor: polkadotSubDotOpacity: "Opacidad del círculo secundario" polkadotSubDotRadius: "Tamaño del círculo secundario." polkadotSubDotDivisions: "Número de subpuntos." + leaveBlankToAccountUrl: "Si dejas este campo en blanco, se utilizará la URL de tu cuenta." _imageEffector: title: "Efecto" addEffect: "Añadir Efecto" @@ -3194,6 +3222,8 @@ _imageEffector: mirror: "Espejo" invert: "Invertir colores" grayscale: "Blanco y negro" + blur: "Difuminar" + pixelate: "Pixelar" colorAdjust: "Corrección de Color" colorClamp: "Compresión cromática" colorClampAdvanced: "Compresión cromática avanzada" @@ -3205,10 +3235,14 @@ _imageEffector: checker: "Corrector" blockNoise: "Bloquear Ruido" tearing: "Rasgado de Imagen (Tearing)" + fill: "Relleno de color" _fxProps: angle: "Ángulo" scale: "Tamaño" size: "Tamaño" + radius: "Radio" + samples: "Tamaño de muestra" + offset: "Posición" color: "Color" opacity: "Opacidad" normalize: "Normalización" @@ -3237,6 +3271,7 @@ _imageEffector: zoomLinesThreshold: "Ancho de línea del zoom" zoomLinesMaskSize: "Diámetro del centro" zoomLinesBlack: "Hacer oscuro" + circle: "Círculo" drafts: "Borrador" _drafts: select: "Seleccionar borradores" @@ -3252,3 +3287,22 @@ _drafts: restoreFromDraft: "Restaurar desde los borradores" restore: "Restaurar" listDrafts: "Listar los borradores" + schedule: "Programar Nota" + listScheduledNotes: "Lista de notas programadas" + cancelSchedule: "Cancelar programación" +qr: "Código QR" +_qr: + showTabTitle: "Apariencia" + readTabTitle: "Escanear" + shareTitle: "{name} {acct}" + shareText: "¡Sígueme en el Fediverso!" + chooseCamera: "Seleccione cámara" + cannotToggleFlash: "No se puede activar el flash" + turnOnFlash: "Encender el flash" + turnOffFlash: "Apagar el flash" + startQr: "Reiniciar el lector de códigos QR" + stopQr: "Detener el lector de códigos QR" + noQrCodeFound: "No se encontró el código QR" + scanFile: "Escanear imagen desde un dispositivo" + raw: "Texto" + mfm: "MFM" diff --git a/locales/fr-FR.yml b/locales/fr-FR.yml index 8ea21bdb46..23c7ba97bb 100644 --- a/locales/fr-FR.yml +++ b/locales/fr-FR.yml @@ -2376,6 +2376,10 @@ _imageEffector: angle: "Angle" scale: "Taille" size: "Taille" + offset: "Position" color: "Couleur" opacity: "Transparence" lightness: "Clair" +_qr: + showTabTitle: "Affichage" + raw: "Texte" diff --git a/locales/id-ID.yml b/locales/id-ID.yml index ac6aefa4b4..abb0720c6b 100644 --- a/locales/id-ID.yml +++ b/locales/id-ID.yml @@ -2631,6 +2631,10 @@ _imageEffector: angle: "Sudut" scale: "Ukuran" size: "Ukuran" + offset: "Posisi" color: "Warna" opacity: "Opasitas" lightness: "Menerangkan" +_qr: + showTabTitle: "Tampilkan" + raw: "Teks" diff --git a/locales/index.d.ts b/locales/index.d.ts index 0cee5b27e5..fe3e07fcfa 100644 --- a/locales/index.d.ts +++ b/locales/index.d.ts @@ -1030,6 +1030,10 @@ export interface Locale extends ILocale { * 処理中 */ "processing": string; + /** + * 準備中 + */ + "preprocessing": string; /** * プレビュー */ @@ -1227,7 +1231,7 @@ export interface Locale extends ILocale { */ "noMoreHistory": string; /** - * チャットを始める + * メッセージを送る */ "startChat": string; /** @@ -1927,7 +1931,7 @@ export interface Locale extends ILocale { */ "markAsReadAllUnreadNotes": string; /** - * すべてのチャットを既読にする + * すべてのダイレクトメッセージを既読にする */ "markAsReadAllTalkMessages": string; /** @@ -5282,6 +5286,10 @@ export interface Locale extends ILocale { * 下書き */ "draft": string; + /** + * 下書きと予約投稿 + */ + "draftsAndScheduledNotes": string; /** * リアクションする際に確認する */ @@ -5390,6 +5398,14 @@ export interface Locale extends ILocale { * チャット */ "chat": string; + /** + * ダイレクトメッセージ + */ + "directMessage": string; + /** + * メッセージ + */ + "directMessage_short": string; /** * 旧設定情報を移行 */ @@ -5501,6 +5517,14 @@ export interface Locale extends ILocale { * 低くすると画質を保てますが、ファイルサイズは増加します。
高くするとファイルサイズを減らせますが、画質は低下します。 */ "defaultImageCompressionLevel_description": string; + /** + * デフォルトの圧縮度 + */ + "defaultCompressionLevel": string; + /** + * 低くすると品質を保てますが、ファイルサイズは増加します。
高くするとファイルサイズを減らせますが、品質は低下します。 + */ + "defaultCompressionLevel_description": string; /** * 分 */ @@ -5529,6 +5553,64 @@ export interface Locale extends ILocale { * ベータ版の検証にご協力いただきありがとうございます! */ "thankYouForTestingBeta": string; + /** + * ユーザー指定ノートを作成 + */ + "createUserSpecifiedNote": string; + /** + * 投稿を予約 + */ + "schedulePost": string; + /** + * {x}に投稿を予約します + */ + "scheduleToPostOnX": ParameterizedString<"x">; + /** + * {x}に投稿が予約されています + */ + "scheduledToPostOnX": ParameterizedString<"x">; + /** + * 予約 + */ + "schedule": string; + /** + * 予約 + */ + "scheduled": string; + /** + * ウィジェット + */ + "widgets": string; + "_compression": { + "_quality": { + /** + * 高品質 + */ + "high": string; + /** + * 中品質 + */ + "medium": string; + /** + * 低品質 + */ + "low": string; + }; + "_size": { + /** + * サイズ大 + */ + "large": string; + /** + * サイズ中 + */ + "medium": string; + /** + * サイズ小 + */ + "small": string; + }; + }; "_order": { /** * 新しい順 @@ -5540,6 +5622,10 @@ export interface Locale extends ILocale { "oldest": string; }; "_chat": { + /** + * メッセージ + */ + "messages": string; /** * まだメッセージはありません */ @@ -5549,36 +5635,36 @@ export interface Locale extends ILocale { */ "newMessage": string; /** - * 個人チャット + * 個別 */ "individualChat": string; /** - * 特定ユーザーとの一対一のチャットができます。 + * 特定ユーザーと個別にメッセージのやりとりができます。 */ "individualChat_description": string; /** - * ルームチャット + * グループ */ "roomChat": string; /** - * 複数人でのチャットができます。 - * また、個人チャットを許可していないユーザーとでも、相手が受け入れればチャットができます。 + * 複数人でメッセージのやりとりができます。 + * また、個別のメッセージを許可していないユーザーとでも、相手が受け入れればやりとりできます。 */ "roomChat_description": string; /** - * ルームを作成 + * グループを作成 */ "createRoom": string; /** - * ユーザーを招待してチャットを始めましょう + * ユーザーを招待してメッセージを送信しましょう */ "inviteUserToChat": string; /** - * 作成したルーム + * 作成したグループ */ "yourRooms": string; /** - * 参加中のルーム + * 参加中のグループ */ "joiningRooms": string; /** @@ -5598,7 +5684,7 @@ export interface Locale extends ILocale { */ "noHistory": string; /** - * ルームはありません + * グループはありません */ "noRooms": string; /** @@ -5618,7 +5704,7 @@ export interface Locale extends ILocale { */ "ignore": string; /** - * ルームから退出 + * グループから退出 */ "leave": string; /** @@ -5642,35 +5728,35 @@ export interface Locale extends ILocale { */ "newline": string; /** - * このルームをミュート + * このグループをミュート */ "muteThisRoom": string; /** - * ルームを削除 + * グループを削除 */ "deleteRoom": string; /** - * このサーバー、またはこのアカウントでチャットは有効化されていません。 + * このサーバー、またはこのアカウントでダイレクトメッセージは有効化されていません。 */ "chatNotAvailableForThisAccountOrServer": string; /** - * このサーバー、またはこのアカウントでチャットは読み取り専用となっています。新たに書き込んだり、チャットルームを作成・参加したりすることはできません。 + * このサーバー、またはこのアカウントでダイレクトメッセージは読み取り専用となっています。新たに書き込んだり、グループを作成・参加したりすることはできません。 */ "chatIsReadOnlyForThisAccountOrServer": string; /** - * 相手のアカウントでチャット機能が使えない状態になっています。 + * 相手のアカウントでダイレクトメッセージが使えない状態になっています。 */ "chatNotAvailableInOtherAccount": string; /** - * このユーザーとのチャットを開始できません + * このユーザーとのダイレクトメッセージを開始できません */ "cannotChatWithTheUser": string; /** - * チャットが使えない状態になっているか、相手がチャットを開放していません。 + * ダイレクトメッセージが使えない状態になっているか、相手がダイレクトメッセージを開放していません。 */ "cannotChatWithTheUser_description": string; /** - * あなたはこのルームの参加者ではありませんが、招待が届いています。参加するには、招待を承認してください。 + * あなたはこのグループの参加者ではありませんが、招待が届いています。参加するには、招待を承認してください。 */ "youAreNotAMemberOfThisRoomButInvited": string; /** @@ -5678,31 +5764,31 @@ export interface Locale extends ILocale { */ "doYouAcceptInvitation": string; /** - * チャットする + * ダイレクトメッセージ */ "chatWithThisUser": string; /** - * このユーザーはフォロワーからのみチャットを受け付けています。 + * このユーザーはフォロワーからのみメッセージを受け付けています。 */ "thisUserAllowsChatOnlyFromFollowers": string; /** - * このユーザーは、このユーザーがフォローしているユーザーからのみチャットを受け付けています。 + * このユーザーは、このユーザーがフォローしているユーザーからのみメッセージを受け付けています。 */ "thisUserAllowsChatOnlyFromFollowing": string; /** - * このユーザーは相互フォローのユーザーからのみチャットを受け付けています。 + * このユーザーは相互フォローのユーザーからのみメッセージを受け付けています。 */ "thisUserAllowsChatOnlyFromMutualFollowing": string; /** - * このユーザーは誰からもチャットを受け付けていません。 + * このユーザーは誰からもメッセージを受け付けていません。 */ "thisUserNotAllowedChatAnyone": string; /** - * チャットを許可する相手 + * メッセージを許可する相手 */ "chatAllowedUsers": string; /** - * 自分からチャットメッセージを送った相手とはこの設定に関わらずチャットが可能です。 + * 自分からメッセージを送った相手とはこの設定に関わらずメッセージの送受信が可能です。 */ "chatAllowedUsers_note": string; "_chatAllowedUsers": { @@ -7856,7 +7942,7 @@ export interface Locale extends ILocale { */ "canImportUserLists": string; /** - * チャットを許可 + * ダイレクトメッセージを許可 */ "chatAvailability": string; /** @@ -7875,6 +7961,10 @@ export interface Locale extends ILocale { * サーバーサイドのノートの下書きの作成可能数 */ "noteDraftLimit": string; + /** + * 予約投稿の同時作成可能数 + */ + "scheduledNoteLimit": string; /** * ウォーターマーク機能の使用可否 */ @@ -8706,7 +8796,7 @@ export interface Locale extends ILocale { */ "badge": string; /** - * チャットの背景 + * メッセージの背景 */ "messageBg": string; /** @@ -8733,7 +8823,7 @@ export interface Locale extends ILocale { */ "reaction": string; /** - * チャットのメッセージ + * ダイレクトメッセージ */ "chatMessage": string; }; @@ -9017,11 +9107,11 @@ export interface Locale extends ILocale { */ "write:following": string; /** - * チャットを見る + * ダイレクトメッセージを見る */ "read:messaging": string; /** - * チャットを操作する + * ダイレクトメッセージを操作する */ "write:messaging": string; /** @@ -9313,11 +9403,11 @@ export interface Locale extends ILocale { */ "write:report-abuse": string; /** - * チャットを操作する + * ダイレクトメッセージを操作する */ "write:chat": string; /** - * チャットを閲覧する + * ダイレクトメッセージを閲覧する */ "read:chat": string; }; @@ -9543,7 +9633,7 @@ export interface Locale extends ILocale { */ "birthdayFollowings": string; /** - * チャット + * ダイレクトメッセージ */ "chat": string; }; @@ -10270,6 +10360,14 @@ export interface Locale extends ILocale { * アンケートの結果が出ました */ "pollEnded": string; + /** + * 予約ノートが投稿されました + */ + "scheduledNotePosted": string; + /** + * 予約ノートの投稿に失敗しました + */ + "scheduledNotePostFailed": string; /** * 新しい投稿 */ @@ -10283,7 +10381,7 @@ export interface Locale extends ILocale { */ "roleAssigned": string; /** - * チャットルームへ招待されました + * ダイレクトメッセージのグループへ招待されました */ "chatRoomInvitationReceived": string; /** @@ -10396,7 +10494,7 @@ export interface Locale extends ILocale { */ "roleAssigned": string; /** - * チャットルームへ招待された + * ダイレクトメッセージのグループへ招待された */ "chatRoomInvitationReceived": string; /** @@ -10578,7 +10676,7 @@ export interface Locale extends ILocale { */ "roleTimeline": string; /** - * チャット + * ダイレクトメッセージ */ "chat": string; }; @@ -10945,7 +11043,7 @@ export interface Locale extends ILocale { */ "deleteGalleryPost": string; /** - * チャットルームを削除 + * ダイレクトメッセージのグループを削除 */ "deleteChatRoom": string; /** @@ -12219,10 +12317,18 @@ export interface Locale extends ILocale { * テキスト */ "text": string; + /** + * 二次元コード + */ + "qr": string; /** * 位置 */ "position": string; + /** + * マージン + */ + "margin": string; /** * タイプ */ @@ -12279,6 +12385,10 @@ export interface Locale extends ILocale { * サブドットの数 */ "polkadotSubDotDivisions": string; + /** + * 空欄にするとアカウントのURLになります + */ + "leaveBlankToAccountUrl": string; }; "_imageEffector": { /** @@ -12318,6 +12428,14 @@ export interface Locale extends ILocale { * 白黒 */ "grayscale": string; + /** + * ぼかし + */ + "blur": string; + /** + * モザイク + */ + "pixelate": string; /** * 色調補正 */ @@ -12362,6 +12480,10 @@ export interface Locale extends ILocale { * ティアリング */ "tearing": string; + /** + * 塗りつぶし + */ + "fill": string; }; "_fxProps": { /** @@ -12376,6 +12498,18 @@ export interface Locale extends ILocale { * サイズ */ "size": string; + /** + * 半径 + */ + "radius": string; + /** + * サンプル数 + */ + "samples": string; + /** + * 位置 + */ + "offset": string; /** * 色 */ @@ -12488,6 +12622,10 @@ export interface Locale extends ILocale { * 黒色にする */ "zoomLinesBlack": string; + /** + * 円形 + */ + "circle": string; }; }; /** @@ -12547,6 +12685,80 @@ export interface Locale extends ILocale { * 下書き一覧 */ "listDrafts": string; + /** + * 投稿予約 + */ + "schedule": string; + /** + * 予約投稿一覧 + */ + "listScheduledNotes": string; + /** + * 予約解除 + */ + "cancelSchedule": string; + }; + /** + * 二次元コード + */ + "qr": string; + "_qr": { + /** + * 表示 + */ + "showTabTitle": string; + /** + * 読み取る + */ + "readTabTitle": string; + /** + * {name} {acct} + */ + "shareTitle": ParameterizedString<"name" | "acct">; + /** + * Fediverseで私をフォローしてください! + */ + "shareText": string; + /** + * カメラを選択 + */ + "chooseCamera": string; + /** + * ライト選択不可 + */ + "cannotToggleFlash": string; + /** + * ライトをオンにする + */ + "turnOnFlash": string; + /** + * ライトをオフにする + */ + "turnOffFlash": string; + /** + * コードリーダーを再開 + */ + "startQr": string; + /** + * コードリーダーを停止 + */ + "stopQr": string; + /** + * QRコードが見つかりません + */ + "noQrCodeFound": string; + /** + * 端末の画像をスキャン + */ + "scanFile": string; + /** + * テキスト + */ + "raw": string; + /** + * MFM + */ + "mfm": string; }; } declare const locales: { diff --git a/locales/it-IT.yml b/locales/it-IT.yml index 8ea11f81c9..8fa481afe8 100644 --- a/locales/it-IT.yml +++ b/locales/it-IT.yml @@ -253,6 +253,7 @@ noteDeleteConfirm: "Vuoi davvero eliminare questa Nota?" pinLimitExceeded: "Non puoi fissare altre note " done: "Fine" processing: "In elaborazione" +preprocessing: "In preparazione" preview: "Anteprima" default: "Predefinito" defaultValueIs: "Predefinito: {value}" @@ -577,7 +578,7 @@ showFixedPostForm: "Visualizzare la finestra di pubblicazione in cima alla timel showFixedPostFormInChannel: "Per i canali, mostra il modulo di pubblicazione in cima alla timeline" withRepliesByDefaultForNewlyFollowed: "Quando segui nuovi profili, includi le risposte in TL come impostazione predefinita" newNoteRecived: "Nuove Note da leggere" -newNote: "Nuova Nota" +newNote: "Nuove Note" sounds: "Impostazioni suoni" sound: "Suono" notificationSoundSettings: "Preferenze di notifica" @@ -1316,13 +1317,14 @@ 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" +draftsAndScheduledNotes: "Bozze e Note pianificate" confirmOnReact: "Confermare le reazioni" reactAreYouSure: "Vuoi davvero reagire con {emoji} ?" markAsSensitiveConfirm: "Vuoi davvero indicare questo contenuto multimediale come esplicito?" unmarkAsSensitiveConfirm: "Vuoi davvero indicare come non esplicito il contenuto multimediale?" preferences: "Preferenze" accessibility: "Accessibilità" -preferencesProfile: "Profilo preferenze" +preferencesProfile: "Preferenze del profilo" copyPreferenceId: "Copia ID preferenze" resetToDefaultValue: "Ripristina a predefinito" overrideByAccount: "Sovrascrivere col profilo" @@ -1343,6 +1345,8 @@ postForm: "Finestra di pubblicazione" textCount: "Il numero di caratteri" information: "Informazioni" chat: "Chat" +directMessage: "Chatta con questa persona" +directMessage_short: "Messaggio" migrateOldSettings: "Migrare le vecchie impostazioni" migrateOldSettings_description: "Di solito, viene fatto automaticamente. Se per qualche motivo non fossero migrate con successo, è possibile avviare il processo di migrazione manualmente, sovrascrivendo le configurazioni attuali." compress: "Compressione" @@ -1370,6 +1374,8 @@ 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." +defaultCompressionLevel: "Compressione predefinita" +defaultCompressionLevel_description: "Diminuisci per mantenere la qualità aumentando le dimensioni del file.
Aumenta per ridurre le dimensioni del file e anche la qualità." inMinutes: "min" inDays: "giorni" safeModeEnabled: "La modalità sicura è attiva" @@ -1377,10 +1383,26 @@ pluginsAreDisabledBecauseSafeMode: "Tutti i plugin sono disattivati, poiché la customCssIsDisabledBecauseSafeMode: "Il CSS personalizzato non è stato applicato, poiché la modalità sicura è attiva." themeIsDefaultBecauseSafeMode: "Quando la modalità sicura è attiva, viene utilizzato il tema predefinito. Quando la modalità sicura viene disattivata, il tema torna a essere quello precedente." thankYouForTestingBeta: "Grazie per la tua collaborazione nella verifica delle versioni beta!" +createUserSpecifiedNote: "Creare Nota personalizzata" +schedulePost: "Pianificare la pubblicazione" +scheduleToPostOnX: "Pianificare la pubblicazione {x}" +scheduledToPostOnX: "Pubblicazione pianificata {x}" +schedule: "Pianificare" +scheduled: "Pianificata" +_compression: + _quality: + high: "Alta qualità" + medium: "Media qualità" + low: "Bassa qualità" + _size: + large: "Taglia grande" + medium: "Taglia media" + small: "Taglia piccola" _order: newest: "Prima i più recenti" oldest: "Meno recenti prima" _chat: + messages: "Messaggi" noMessagesYet: "Ancora nessun messaggio" newMessage: "Nuovo messaggio" individualChat: "Chat individuale" @@ -2018,6 +2040,7 @@ _role: 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" + scheduledNoteLimit: "Quantità di Note pianificabili contemporaneamente" watermarkAvailable: "Disponibilità della funzione filigrana" _condition: roleAssignedTo: "Assegnato a ruoli manualmente" @@ -2453,7 +2476,7 @@ _widgets: chooseList: "Seleziona una lista" clicker: "Cliccheria" birthdayFollowings: "Compleanni del giorno" - chat: "Chat" + chat: "Chatta con questa persona" _cw: hide: "Nascondere" show: "Continua la lettura..." @@ -2643,6 +2666,8 @@ _notification: youReceivedFollowRequest: "Hai ricevuto una richiesta di follow" yourFollowRequestAccepted: "La tua richiesta di follow è stata accettata" pollEnded: "Risultati del sondaggio." + scheduledNotePosted: "Pubblicazione Nota pianificata" + scheduledNotePostFailed: "Impossibile pubblicare la Nota pianificata" newNote: "Nuove Note" unreadAntennaNote: "Antenna {name}" roleAssigned: "Ruolo assegnato" @@ -2687,7 +2712,7 @@ _notification: reply: "Rispondi" renote: "Rinota" _deck: - alwaysShowMainColumn: "Mostra sempre la colonna principale" + alwaysShowMainColumn: "Mostrare sempre la colonna Principale" columnAlign: "Allineamento delle colonne" columnGap: "Spessore del margine tra colonne" deckMenuPosition: "Posizione del menu Deck" @@ -2704,8 +2729,8 @@ _deck: profile: "Profilo" newProfile: "Nuovo profilo" deleteProfile: "Cancellare il profilo." - introduction: "Combinate le colonne per creare la vostra interfaccia!" - introduction2: "È possibile aggiungere colonne in qualsiasi momento premendo + sulla destra dello schermo." + introduction: "Crea la tua interfaccia combinando le colonne!" + introduction2: "Per aggiungere una colonna, cliccare il bottone + (più) visibile al margine dello schermo." widgetsIntroduction: "Dal menu della colonna, selezionare \"Modifica i riquadri\" per aggiungere un un riquadro con funzionalità" useSimpleUiForNonRootPages: "Visualizza sotto pagine con interfaccia web semplice" usedAsMinWidthWhenFlexible: "Se \"larghezza flessibile\" è abilitato, questa diventa la larghezza minima" @@ -2722,7 +2747,7 @@ _deck: mentions: "Menzioni" direct: "Note Dirette" roleTimeline: "Timeline Ruolo" - chat: "Chat" + chat: "Chatta con questa persona" _dialog: charactersExceeded: "Hai superato il limite di {max} caratteri! ({current})" charactersBelow: "Sei al di sotto del minimo di {min} caratteri! ({current})" @@ -3168,7 +3193,9 @@ _watermarkEditor: opacity: "Opacità" scale: "Dimensioni" text: "Testo" + qr: "QR Code" position: "Posizione" + margin: "Margine" type: "Tipo" image: "Immagini" advanced: "Avanzato" @@ -3183,6 +3210,7 @@ _watermarkEditor: polkadotSubDotOpacity: "Opacità del punto secondario" polkadotSubDotRadius: "Dimensione del punto secondario" polkadotSubDotDivisions: "Quantità di punti secondari" + leaveBlankToAccountUrl: "Il valore vuoto indica la URL dell'account" _imageEffector: title: "Effetto" addEffect: "Aggiungi effetto" @@ -3194,6 +3222,8 @@ _imageEffector: mirror: "Specchio" invert: "Inversione colore" grayscale: "Bianco e nero" + blur: "Sfocatura" + pixelate: "Mosaico" colorAdjust: "Correzione Colore" colorClamp: "Compressione del colore" colorClampAdvanced: "Compressione del colore (avanzata)" @@ -3205,10 +3235,14 @@ _imageEffector: checker: "revisore" blockNoise: "Attenua rumore" tearing: "Strappa immagine" + fill: "Riempimento" _fxProps: angle: "Angolo" scale: "Dimensioni" size: "Dimensioni" + radius: "Raggio" + samples: "Quantità di campioni" + offset: "Posizione" color: "Colore" opacity: "Opacità" normalize: "Normalizza" @@ -3237,6 +3271,7 @@ _imageEffector: zoomLinesThreshold: "Limite delle linee zoom" zoomLinesMaskSize: "Ampiezza del diametro" zoomLinesBlack: "Bande nere" + circle: "Circolare" drafts: "Bozze" _drafts: select: "Selezionare bozza" @@ -3252,3 +3287,22 @@ _drafts: restoreFromDraft: "Recuperare dalle bozze" restore: "Ripristina" listDrafts: "Elenco bozze" + schedule: "Pianifica pubblicazione" + listScheduledNotes: "Elenca Note pianificate" + cancelSchedule: "Annulla pianificazione" +qr: "QR Code" +_qr: + showTabTitle: "Visualizza" + readTabTitle: "Leggere" + shareTitle: "{name} {acct}" + shareText: "Seguimi nel Fediverso!" + chooseCamera: "Seleziona fotocamera" + cannotToggleFlash: "Flash non controllabile" + turnOnFlash: "Accendi il flash" + turnOffFlash: "Spegni il flash" + startQr: "Inizia lettura QR Code" + stopQr: "Interrompi lettura QR Code" + noQrCodeFound: "Non trovo alcun QR Code" + scanFile: "Scansiona immagine nel dispositivo" + raw: "Testo" + mfm: "MFM" diff --git a/locales/ja-JP.yml b/locales/ja-JP.yml index 3cb8248948..8306a862e1 100644 --- a/locales/ja-JP.yml +++ b/locales/ja-JP.yml @@ -253,6 +253,7 @@ noteDeleteConfirm: "このノートを削除しますか?" pinLimitExceeded: "これ以上ピン留めできません" done: "完了" processing: "処理中" +preprocessing: "準備中" preview: "プレビュー" default: "デフォルト" defaultValueIs: "デフォルト: {value}" @@ -302,7 +303,7 @@ uploadNFiles: "{n}個のファイルをアップロード" explore: "みつける" messageRead: "既読" noMoreHistory: "これより過去の履歴はありません" -startChat: "チャットを始める" +startChat: "メッセージを送る" nUsersRead: "{n}人が読みました" agreeTo: "{0}に同意" agree: "同意する" @@ -477,7 +478,7 @@ notFoundDescription: "指定されたURLに該当するページはありませ uploadFolder: "既定アップロード先" markAsReadAllNotifications: "すべての通知を既読にする" markAsReadAllUnreadNotes: "すべての投稿を既読にする" -markAsReadAllTalkMessages: "すべてのチャットを既読にする" +markAsReadAllTalkMessages: "すべてのダイレクトメッセージを既読にする" help: "ヘルプ" inputMessageHere: "ここにメッセージを入力" close: "閉じる" @@ -1316,6 +1317,7 @@ acknowledgeNotesAndEnable: "注意事項を理解した上でオンにします federationSpecified: "このサーバーはホワイトリスト連合で運用されています。管理者が指定したサーバー以外とやり取りすることはできません。" federationDisabled: "このサーバーは連合が無効化されています。他のサーバーのユーザーとやり取りすることはできません。" draft: "下書き" +draftsAndScheduledNotes: "下書きと予約投稿" confirmOnReact: "リアクションする際に確認する" reactAreYouSure: "\" {emoji} \" をリアクションしますか?" markAsSensitiveConfirm: "このメディアをセンシティブとして設定しますか?" @@ -1343,6 +1345,8 @@ postForm: "投稿フォーム" textCount: "文字数" information: "情報" chat: "チャット" +directMessage: "ダイレクトメッセージ" +directMessage_short: "メッセージ" migrateOldSettings: "旧設定情報を移行" migrateOldSettings_description: "通常これは自動で行われていますが、何らかの理由により上手く移行されなかった場合は手動で移行処理をトリガーできます。現在の設定情報は上書きされます。" compress: "圧縮" @@ -1370,6 +1374,8 @@ redisplayAllTips: "全ての「ヒントとコツ」を再表示" hideAllTips: "全ての「ヒントとコツ」を非表示" defaultImageCompressionLevel: "デフォルトの画像圧縮度" defaultImageCompressionLevel_description: "低くすると画質を保てますが、ファイルサイズは増加します。
高くするとファイルサイズを減らせますが、画質は低下します。" +defaultCompressionLevel: "デフォルトの圧縮度" +defaultCompressionLevel_description: "低くすると品質を保てますが、ファイルサイズは増加します。
高くするとファイルサイズを減らせますが、品質は低下します。" inMinutes: "分" inDays: "日" safeModeEnabled: "セーフモードが有効です" @@ -1377,53 +1383,71 @@ pluginsAreDisabledBecauseSafeMode: "セーフモードが有効なため、プ customCssIsDisabledBecauseSafeMode: "セーフモードが有効なため、カスタムCSSは適用されていません。" themeIsDefaultBecauseSafeMode: "セーフモードが有効な間はデフォルトのテーマが使用されます。セーフモードをオフにすると元に戻ります。" thankYouForTestingBeta: "ベータ版の検証にご協力いただきありがとうございます!" +createUserSpecifiedNote: "ユーザー指定ノートを作成" +schedulePost: "投稿を予約" +scheduleToPostOnX: "{x}に投稿を予約します" +scheduledToPostOnX: "{x}に投稿が予約されています" +schedule: "予約" +scheduled: "予約" +widgets: "ウィジェット" + +_compression: + _quality: + high: "高品質" + medium: "中品質" + low: "低品質" + _size: + large: "サイズ大" + medium: "サイズ中" + small: "サイズ小" _order: newest: "新しい順" oldest: "古い順" _chat: + messages: "メッセージ" noMessagesYet: "まだメッセージはありません" newMessage: "新しいメッセージ" - individualChat: "個人チャット" - individualChat_description: "特定ユーザーとの一対一のチャットができます。" - roomChat: "ルームチャット" - roomChat_description: "複数人でのチャットができます。\nまた、個人チャットを許可していないユーザーとでも、相手が受け入れればチャットができます。" - createRoom: "ルームを作成" - inviteUserToChat: "ユーザーを招待してチャットを始めましょう" - yourRooms: "作成したルーム" - joiningRooms: "参加中のルーム" + individualChat: "個別" + individualChat_description: "特定ユーザーと個別にメッセージのやりとりができます。" + roomChat: "グループ" + roomChat_description: "複数人でメッセージのやりとりができます。\nまた、個別のメッセージを許可していないユーザーとでも、相手が受け入れればやりとりできます。" + createRoom: "グループを作成" + inviteUserToChat: "ユーザーを招待してメッセージを送信しましょう" + yourRooms: "作成したグループ" + joiningRooms: "参加中のグループ" invitations: "招待" noInvitations: "招待はありません" history: "履歴" noHistory: "履歴はありません" - noRooms: "ルームはありません" + noRooms: "グループはありません" inviteUser: "ユーザーを招待" sentInvitations: "送信した招待" join: "参加" ignore: "無視" - leave: "ルームから退出" + leave: "グループから退出" members: "メンバー" searchMessages: "メッセージを検索" home: "ホーム" send: "送信" newline: "改行" - muteThisRoom: "このルームをミュート" - deleteRoom: "ルームを削除" - chatNotAvailableForThisAccountOrServer: "このサーバー、またはこのアカウントでチャットは有効化されていません。" - chatIsReadOnlyForThisAccountOrServer: "このサーバー、またはこのアカウントでチャットは読み取り専用となっています。新たに書き込んだり、チャットルームを作成・参加したりすることはできません。" - chatNotAvailableInOtherAccount: "相手のアカウントでチャット機能が使えない状態になっています。" - cannotChatWithTheUser: "このユーザーとのチャットを開始できません" - cannotChatWithTheUser_description: "チャットが使えない状態になっているか、相手がチャットを開放していません。" - youAreNotAMemberOfThisRoomButInvited: "あなたはこのルームの参加者ではありませんが、招待が届いています。参加するには、招待を承認してください。" + muteThisRoom: "このグループをミュート" + deleteRoom: "グループを削除" + chatNotAvailableForThisAccountOrServer: "このサーバー、またはこのアカウントでダイレクトメッセージは有効化されていません。" + chatIsReadOnlyForThisAccountOrServer: "このサーバー、またはこのアカウントでダイレクトメッセージは読み取り専用となっています。新たに書き込んだり、グループを作成・参加したりすることはできません。" + chatNotAvailableInOtherAccount: "相手のアカウントでダイレクトメッセージが使えない状態になっています。" + cannotChatWithTheUser: "このユーザーとのダイレクトメッセージを開始できません" + cannotChatWithTheUser_description: "ダイレクトメッセージが使えない状態になっているか、相手がダイレクトメッセージを開放していません。" + youAreNotAMemberOfThisRoomButInvited: "あなたはこのグループの参加者ではありませんが、招待が届いています。参加するには、招待を承認してください。" doYouAcceptInvitation: "招待を承認しますか?" - chatWithThisUser: "チャットする" - thisUserAllowsChatOnlyFromFollowers: "このユーザーはフォロワーからのみチャットを受け付けています。" - thisUserAllowsChatOnlyFromFollowing: "このユーザーは、このユーザーがフォローしているユーザーからのみチャットを受け付けています。" - thisUserAllowsChatOnlyFromMutualFollowing: "このユーザーは相互フォローのユーザーからのみチャットを受け付けています。" - thisUserNotAllowedChatAnyone: "このユーザーは誰からもチャットを受け付けていません。" - chatAllowedUsers: "チャットを許可する相手" - chatAllowedUsers_note: "自分からチャットメッセージを送った相手とはこの設定に関わらずチャットが可能です。" + chatWithThisUser: "ダイレクトメッセージ" + thisUserAllowsChatOnlyFromFollowers: "このユーザーはフォロワーからのみメッセージを受け付けています。" + thisUserAllowsChatOnlyFromFollowing: "このユーザーは、このユーザーがフォローしているユーザーからのみメッセージを受け付けています。" + thisUserAllowsChatOnlyFromMutualFollowing: "このユーザーは相互フォローのユーザーからのみメッセージを受け付けています。" + thisUserNotAllowedChatAnyone: "このユーザーは誰からもメッセージを受け付けていません。" + chatAllowedUsers: "メッセージを許可する相手" + chatAllowedUsers_note: "自分からメッセージを送った相手とはこの設定に関わらずメッセージの送受信が可能です。" _chatAllowedUsers: everyone: "誰でも" followers: "自分のフォロワーのみ" @@ -2034,11 +2058,12 @@ _role: canImportFollowing: "フォローのインポートを許可" canImportMuting: "ミュートのインポートを許可" canImportUserLists: "リストのインポートを許可" - chatAvailability: "チャットを許可" + chatAvailability: "ダイレクトメッセージを許可" uploadableFileTypes: "アップロード可能なファイル種別" uploadableFileTypes_caption: "MIMEタイプを指定します。改行で区切って複数指定できるほか、アスタリスク(*)でワイルドカード指定できます。(例: image/*)" uploadableFileTypes_caption2: "ファイルによっては種別を判定できないことがあります。そのようなファイルを許可する場合は {x} を指定に追加してください。" noteDraftLimit: "サーバーサイドのノートの下書きの作成可能数" + scheduledNoteLimit: "予約投稿の同時作成可能数" watermarkAvailable: "ウォーターマーク機能の使用可否" _condition: roleAssignedTo: "マニュアルロールにアサイン済み" @@ -2281,7 +2306,7 @@ _theme: buttonHoverBg: "ボタンの背景 (ホバー)" inputBorder: "入力ボックスの縁取り" badge: "バッジ" - messageBg: "チャットの背景" + messageBg: "メッセージの背景" fgHighlighted: "強調された文字" _sfx: @@ -2289,7 +2314,7 @@ _sfx: noteMy: "ノート(自分)" notification: "通知" reaction: "リアクション選択時" - chatMessage: "チャットのメッセージ" + chatMessage: "ダイレクトメッセージ" _soundSettings: driveFile: "ドライブの音声を使用" @@ -2369,8 +2394,8 @@ _permissions: "write:favorites": "お気に入りを操作する" "read:following": "フォローの情報を見る" "write:following": "フォロー・フォロー解除する" - "read:messaging": "チャットを見る" - "write:messaging": "チャットを操作する" + "read:messaging": "ダイレクトメッセージを見る" + "write:messaging": "ダイレクトメッセージを操作する" "read:mutes": "ミュートを見る" "write:mutes": "ミュートを操作する" "write:notes": "ノートを作成・削除する" @@ -2443,8 +2468,8 @@ _permissions: "read:clip-favorite": "クリップのいいねを見る" "read:federation": "連合に関する情報を取得する" "write:report-abuse": "違反を報告する" - "write:chat": "チャットを操作する" - "read:chat": "チャットを閲覧する" + "write:chat": "ダイレクトメッセージを操作する" + "read:chat": "ダイレクトメッセージを閲覧する" _auth: shareAccessTitle: "アプリへのアクセス許可" @@ -2507,7 +2532,7 @@ _widgets: chooseList: "リストを選択" clicker: "クリッカー" birthdayFollowings: "今日誕生日のユーザー" - chat: "チャット" + chat: "ダイレクトメッセージ" _cw: hide: "隠す" @@ -2711,10 +2736,12 @@ _notification: youReceivedFollowRequest: "フォローリクエストが来ました" yourFollowRequestAccepted: "フォローリクエストが承認されました" pollEnded: "アンケートの結果が出ました" + scheduledNotePosted: "予約ノートが投稿されました" + scheduledNotePostFailed: "予約ノートの投稿に失敗しました" newNote: "新しい投稿" unreadAntennaNote: "アンテナ {name}" roleAssigned: "ロールが付与されました" - chatRoomInvitationReceived: "チャットルームへ招待されました" + chatRoomInvitationReceived: "ダイレクトメッセージのグループへ招待されました" emptyPushNotificationMessage: "プッシュ通知の更新をしました" achievementEarned: "実績を獲得" testNotification: "通知テスト" @@ -2744,7 +2771,7 @@ _notification: receiveFollowRequest: "フォロー申請を受け取った" followRequestAccepted: "フォローが受理された" roleAssigned: "ロールが付与された" - chatRoomInvitationReceived: "チャットルームへ招待された" + chatRoomInvitationReceived: "ダイレクトメッセージのグループへ招待された" achievementEarned: "実績の獲得" exportCompleted: "エクスポートが完了した" login: "ログイン" @@ -2794,7 +2821,7 @@ _deck: mentions: "メンション" direct: "指名" roleTimeline: "ロールタイムライン" - chat: "チャット" + chat: "ダイレクトメッセージ" _dialog: charactersExceeded: "最大文字数を超えています! 現在 {current} / 制限 {max}" @@ -2897,7 +2924,7 @@ _moderationLogTypes: deletePage: "ページを削除" deleteFlash: "Playを削除" deleteGalleryPost: "ギャラリーの投稿を削除" - deleteChatRoom: "チャットルームを削除" + deleteChatRoom: "ダイレクトメッセージのグループを削除" updateProxyAccountDescription: "プロキシアカウントの説明を更新" _fileViewer: @@ -3271,7 +3298,9 @@ _watermarkEditor: opacity: "不透明度" scale: "サイズ" text: "テキスト" + qr: "二次元コード" position: "位置" + margin: "マージン" type: "タイプ" image: "画像" advanced: "高度" @@ -3286,6 +3315,7 @@ _watermarkEditor: polkadotSubDotOpacity: "サブドットの不透明度" polkadotSubDotRadius: "サブドットの大きさ" polkadotSubDotDivisions: "サブドットの数" + leaveBlankToAccountUrl: "空欄にするとアカウントのURLになります" _imageEffector: title: "エフェクト" @@ -3299,6 +3329,8 @@ _imageEffector: mirror: "ミラー" invert: "色の反転" grayscale: "白黒" + blur: "ぼかし" + pixelate: "モザイク" colorAdjust: "色調補正" colorClamp: "色の圧縮" colorClampAdvanced: "色の圧縮(高度)" @@ -3310,11 +3342,15 @@ _imageEffector: checker: "チェッカー" blockNoise: "ブロックノイズ" tearing: "ティアリング" + fill: "塗りつぶし" _fxProps: angle: "角度" scale: "サイズ" size: "サイズ" + radius: "半径" + samples: "サンプル数" + offset: "位置" color: "色" opacity: "不透明度" normalize: "正規化" @@ -3343,6 +3379,7 @@ _imageEffector: zoomLinesThreshold: "集中線の幅" zoomLinesMaskSize: "中心径" zoomLinesBlack: "黒色にする" + circle: "円形" drafts: "下書き" _drafts: @@ -3359,3 +3396,23 @@ _drafts: restoreFromDraft: "下書きから復元" restore: "復元" listDrafts: "下書き一覧" + schedule: "投稿予約" + listScheduledNotes: "予約投稿一覧" + cancelSchedule: "予約解除" + +qr: "二次元コード" +_qr: + showTabTitle: "表示" + readTabTitle: "読み取る" + shareTitle: "{name} {acct}" + shareText: "Fediverseで私をフォローしてください!" + chooseCamera: "カメラを選択" + cannotToggleFlash: "ライト選択不可" + turnOnFlash: "ライトをオンにする" + turnOffFlash: "ライトをオフにする" + startQr: "コードリーダーを再開" + stopQr: "コードリーダーを停止" + noQrCodeFound: "QRコードが見つかりません" + scanFile: "端末の画像をスキャン" + raw: "テキスト" + mfm: "MFM" diff --git a/locales/ja-KS.yml b/locales/ja-KS.yml index d44ed1c3fc..d781a14b85 100644 --- a/locales/ja-KS.yml +++ b/locales/ja-KS.yml @@ -1319,6 +1319,7 @@ preferenceSyncConflictChoiceMerge: "ガッチャンコしよか" preferenceSyncConflictChoiceCancel: "同期の有効化はやめとくわ" postForm: "投稿フォーム" information: "情報" +directMessage: "チャットしよか" migrateOldSettings: "旧設定情報をお引っ越し" migrateOldSettings_description: "通常これは自動で行われるはずなんやけど、なんかの理由で上手く移行できへんかったときは手動で移行処理をポチっとできるで。今の設定情報は上書きされるで。" settingsMigrating: "設定を移行しとるで。ちょっと待っとってな... (後で、設定→その他→旧設定情報を移行 で手動で移行することもできるで)" @@ -1409,7 +1410,7 @@ _accountSettings: makeNotesFollowersOnlyBefore: "昔のノートをフォロワーだけに見てもらう" makeNotesFollowersOnlyBeforeDescription: "この機能が有効になってる間は、設定された日時より前、それか設定された時間が経ったノートがフォロワーのみ見れるようになるで。無効に戻すと、ノートの公開状態も戻るで。" makeNotesHiddenBefore: "昔のノートを見れんようにする" - makeNotesHiddenBeforeDescription: "この機能が有効になってる間は、設定された日時より前、それか設定された時間が経ったノートがフォロワーのみ見れるようになるで。無効に戻すと、ノートの公開状態も戻るで。" + makeNotesHiddenBeforeDescription: "この機能が有効になってる間は、設定された日時より前、それか設定された時間が経ったノートがあんただけ見れるようになるで。無効に戻すと、ノートの公開状態も戻るで。" mayNotEffectForFederatedNotes: "リモートサーバーに連合されたノートには効果が及ばんかもしれん。" mayNotEffectSomeSituations: "これらの制限は簡易的なものやで。リモートサーバーでの閲覧とかモデレーション時とか、一部のシチュエーションでは適用されへんかもしれん。" notesHavePassedSpecifiedPeriod: "決めた時間が経ったノート" @@ -2135,6 +2136,7 @@ _sfx: noteMy: "ノート(自分)" notification: "通知" reaction: "ツッコミ選んどるとき" + chatMessage: "チャットしよか" _soundSettings: driveFile: "ドライブん中の音使う" driveFileWarn: "ドライブん中のファイル選びや" @@ -2340,6 +2342,7 @@ _widgets: chooseList: "リストを選ぶ" clicker: "クリッカー" birthdayFollowings: "今日誕生日のツレ" + chat: "チャットしよか" _cw: hide: "隠す" show: "続き見して!" @@ -2602,6 +2605,7 @@ _deck: mentions: "あんた宛て" direct: "ダイレクト" roleTimeline: "ロールタイムライン" + chat: "チャットしよか" _dialog: charactersExceeded: "最大の文字数を上回っとるで!今は {current} / 最大でも {max}" charactersBelow: "最小の文字数を下回っとるで!今は {current} / 最低でも {min}" @@ -3023,6 +3027,7 @@ _imageEffector: angle: "角度" scale: "大きさ" size: "大きさ" + offset: "位置" color: "色" opacity: "不透明度" lightness: "明るさ" @@ -3032,3 +3037,6 @@ _drafts: delete: "下書きをほかす" deleteAreYouSure: "下書きをほかしてもええか?" noDrafts: "下書きはあらへん" +_qr: + showTabTitle: "表示" + raw: "テキスト" diff --git a/locales/ko-GS.yml b/locales/ko-GS.yml index 59fe63be6c..2712586e65 100644 --- a/locales/ko-GS.yml +++ b/locales/ko-GS.yml @@ -852,3 +852,5 @@ _search: searchScopeUser: "사용자 지정" _watermarkEditor: image: "이미지" +_qr: + showTabTitle: "보기" diff --git a/locales/ko-KR.yml b/locales/ko-KR.yml index 8809d3b4d3..a29744bad8 100644 --- a/locales/ko-KR.yml +++ b/locales/ko-KR.yml @@ -253,6 +253,7 @@ noteDeleteConfirm: "이 노트를 삭제하시겠습니까?" pinLimitExceeded: "더 이상 고정할 수 없습니다." done: "완료" processing: "처리중" +preprocessing: "준비중" preview: "미리보기" default: "기본값" defaultValueIs: "기본값: {value}" @@ -1316,6 +1317,7 @@ acknowledgeNotesAndEnable: "활성화 하기 전에 주의 사항을 확인했 federationSpecified: "이 서버는 화이트 리스트 제도로 운영 중 입니다. 정해진 리모트 서버가 아닌 경우 연합되지 않습니다." federationDisabled: "이 서버는 연합을 하지 않고 있습니다. 리모트 서버 유저와 통신을 할 수 없습니다." draft: "초안" +draftsAndScheduledNotes: "초안과 예약 게시물" confirmOnReact: "리액션할 때 확인" reactAreYouSure: "\" {emoji} \"로 리액션하시겠습니까?" markAsSensitiveConfirm: "이 미디어를 민감한 미디어로 설정하시겠습니까?" @@ -1343,6 +1345,8 @@ postForm: "글 입력란" textCount: "문자 수" information: "정보" chat: "채팅" +directMessage: "채팅하기" +directMessage_short: "메시지" migrateOldSettings: "기존 설정 정보를 이전" migrateOldSettings_description: "보통은 자동으로 이루어지지만, 어떤 이유로 인해 성공적으로 이전이 이루어지지 않는 경우 수동으로 이전을 실행할 수 있습니다. 현재 설정 정보는 덮어쓰게 됩니다." compress: "압축" @@ -1370,6 +1374,8 @@ redisplayAllTips: "모든 '팁과 유용한 정보'를 재표시" hideAllTips: "모든 '팁과 유용한 정보'를 비표시" defaultImageCompressionLevel: "기본 이미지 압축 정도" defaultImageCompressionLevel_description: "낮추면 화질을 유지합니다만 파일 크기는 증가합니다.
높이면 파일 크기를 줄일 수 있습니다만 화질은 저하됩니다." +defaultCompressionLevel: "기본 압축 정도 " +defaultCompressionLevel_description: "낮추면 품질을 유지합니다만 파일 크기는 증가합니다.
높이면 파일 크기를 줄일 수 있습니다만 품질은 저하됩니다." inMinutes: "분" inDays: "일" safeModeEnabled: "세이프 모드가 활성화돼있습니다" @@ -1377,10 +1383,26 @@ pluginsAreDisabledBecauseSafeMode: "세이프 모드가 활성화돼있기에 customCssIsDisabledBecauseSafeMode: "세이프 모드가 활성화돼있기에 커스텀 CSS는 적용되지 않습니다." themeIsDefaultBecauseSafeMode: "세이프 모드가 활성화돼있는 동안에는 기본 테마가 사용됩니다. 세이프 모드를 끄면 원래대로 돌아옵니다." thankYouForTestingBeta: "베타 버전의 검증에 협력해 주셔서 감사합니다!" +createUserSpecifiedNote: "사용자 지정 노트를 작성" +schedulePost: "게시 예약" +scheduleToPostOnX: "{x}에 게시를 예약합니다." +scheduledToPostOnX: "{x}에 게시가 예약돼있습니다." +schedule: "예약" +scheduled: "예약" +_compression: + _quality: + high: "고품질" + medium: "중간 품질" + low: "저품질" + _size: + large: "대형" + medium: "중형" + small: "소형" _order: newest: "최신 순" oldest: "오래된 순" _chat: + messages: "메시지" noMessagesYet: "아직 메시지가 없습니다" newMessage: "새로운 메시지" individualChat: "개인 대화" @@ -2018,6 +2040,7 @@ _role: uploadableFileTypes_caption: "MIME 유형을 " uploadableFileTypes_caption2: "파일에 따라서는 유형을 검사하지 못하는 경우가 있습니다. 그러한 파일을 허가하는 경우에는 {x}를 지정으로 추가해주십시오." noteDraftLimit: "서버측 노트 초안 작성 가능 수" + scheduledNoteLimit: "예약 게시물의 동시 작성 가능 수" watermarkAvailable: "워터마크 기능의 사용 여부" _condition: roleAssignedTo: "수동 역할에 이미 할당됨" @@ -2453,7 +2476,7 @@ _widgets: chooseList: "리스트 선택" clicker: "클리커" birthdayFollowings: "오늘이 생일인 유저" - chat: "채팅" + chat: "채팅하기" _cw: hide: "숨기기" show: "더 보기" @@ -2643,6 +2666,8 @@ _notification: youReceivedFollowRequest: "새로운 팔로우 요청이 있습니다" yourFollowRequestAccepted: "팔로우 요청이 수락되었습니다" pollEnded: "투표 결과가 발표되었습니다" + scheduledNotePosted: "예약 노트가 게시됐습니다." + scheduledNotePostFailed: "예약 노트의 게시에 실패했습니다." newNote: "새 게시물" unreadAntennaNote: "안테나 {name}" roleAssigned: "역할이 부여 되었습니다." @@ -2722,7 +2747,7 @@ _deck: mentions: "받은 멘션" direct: "다이렉트" roleTimeline: "역할 타임라인" - chat: "채팅" + chat: "채팅하기" _dialog: charactersExceeded: "최대 글자수를 초과하였습니다! 현재 {current} / 최대 {max}" charactersBelow: "최소 글자수 미만입니다! 현재 {current} / 최소 {min}" @@ -3168,7 +3193,9 @@ _watermarkEditor: opacity: "불투명도" scale: "크기" text: "텍스트" + qr: "QR 코드" position: "위치" + margin: "여백" type: "종류" image: "이미지" advanced: "고급" @@ -3183,6 +3210,7 @@ _watermarkEditor: polkadotSubDotOpacity: "서브 물방울의 불투명도" polkadotSubDotRadius: "서브 물방울의 크기" polkadotSubDotDivisions: "서브 물방울의 수" + leaveBlankToAccountUrl: "빈칸일 경우 계정의 URL로 됩니다." _imageEffector: title: "이펙트" addEffect: "이펙트를 추가" @@ -3194,6 +3222,8 @@ _imageEffector: mirror: "미러" invert: "색 반전" grayscale: "흑백" + blur: "흐림 효과" + pixelate: "모자이크" colorAdjust: "색조 보정" colorClamp: "색 압축" colorClampAdvanced: "색 압축(고급)" @@ -3205,10 +3235,14 @@ _imageEffector: checker: "체크 무늬" blockNoise: "노이즈 방지" tearing: "티어링" + fill: "채우기" _fxProps: angle: "각도" scale: "크기" size: "크기" + radius: "반지름" + samples: "샘플 수" + offset: "위치" color: "색" opacity: "불투명도" normalize: "노멀라이즈" @@ -3237,6 +3271,7 @@ _imageEffector: zoomLinesThreshold: "집중선 폭" zoomLinesMaskSize: "중앙 값" zoomLinesBlack: "검은색으로 하기" + circle: "원형" drafts: "초안" _drafts: select: "초안 선택" @@ -3252,3 +3287,22 @@ _drafts: restoreFromDraft: "초안에서 복원\n" restore: "복원" listDrafts: "초안 목록" + schedule: "게시 예약" + listScheduledNotes: "예약 게시물 목록" + cancelSchedule: "예약 해제" +qr: "QR 코드" +_qr: + showTabTitle: "보기" + readTabTitle: "읽어들이기" + shareTitle: "{name} {acct}" + shareText: "Fediverse로 저를 팔로우해 주세요!" + chooseCamera: "카메라 선택" + cannotToggleFlash: "플래시 선택 불가" + turnOnFlash: "플래시 켜기" + turnOffFlash: "플래시 끄기" + startQr: "코드 리더 재개" + stopQr: "코드 리더 정지" + noQrCodeFound: "QR 코드를 찾을 수 없습니다." + scanFile: "단말기의 이미지 스캔" + raw: "텍스트" + mfm: "MFM" diff --git a/locales/nl-NL.yml b/locales/nl-NL.yml index e4efbc7e39..87a805429e 100644 --- a/locales/nl-NL.yml +++ b/locales/nl-NL.yml @@ -1083,3 +1083,5 @@ _search: _watermarkEditor: image: "Afbeeldingen" advanced: "Geavanceerd" +_qr: + showTabTitle: "Weergave" diff --git a/locales/no-NO.yml b/locales/no-NO.yml index 1eafd31c4f..2df475bffd 100644 --- a/locales/no-NO.yml +++ b/locales/no-NO.yml @@ -747,3 +747,5 @@ _imageEffector: scale: "Størrelse" size: "Størrelse" color: "Farge" +_qr: + raw: "Tekst" diff --git a/locales/pl-PL.yml b/locales/pl-PL.yml index fbd898016e..40f7aad9fa 100644 --- a/locales/pl-PL.yml +++ b/locales/pl-PL.yml @@ -1600,3 +1600,6 @@ _imageEffector: color: "Kolor" opacity: "Przezroczystość" lightness: "Rozjaśnij" +_qr: + showTabTitle: "Wyświetlanie" + raw: "Tekst" diff --git a/locales/pt-PT.yml b/locales/pt-PT.yml index 013d2ef549..0c365068e6 100644 --- a/locales/pt-PT.yml +++ b/locales/pt-PT.yml @@ -253,6 +253,7 @@ noteDeleteConfirm: "Deseja excluir esta nota?" pinLimitExceeded: "Não é possível fixar novas notas" done: "Concluído" processing: "Em Progresso" +preprocessing: "Preparando..." preview: "Pré-visualizar" default: "Predefinição" defaultValueIs: "Predefinição: {value}" @@ -1054,6 +1055,7 @@ permissionDeniedError: "Operação recusada" permissionDeniedErrorDescription: "Esta conta não tem permissão para executar esta ação." preset: "Predefinições" selectFromPresets: "Escolher de predefinições" +custom: "Personalizado" achievements: "Conquistas" gotInvalidResponseError: "Resposta do servidor inválida" gotInvalidResponseErrorDescription: "Servidor fora do ar ou em manutenção. Favor tentar mais tarde." @@ -1092,6 +1094,7 @@ prohibitedWordsDescription2: "Utilizar espaços irá criar expressões aditivas hiddenTags: "Hashtags escondidas" hiddenTagsDescription: "Selecione tags que não serão exibidas na lista de destaques. Várias tags podem ser escolhidas, separadas por linha." notesSearchNotAvailable: "A pesquisa de notas está indisponível." +usersSearchNotAvailable: "Pesquisa de usuário está indisponível." license: "Licença" unfavoriteConfirm: "Deseja realmente remover dos favoritos?" myClips: "Meus clipes" @@ -1243,6 +1246,7 @@ releaseToRefresh: "Solte para atualizar" refreshing: "Atualizando..." pullDownToRefresh: "Puxe para baixo para atualizar" useGroupedNotifications: "Agrupar notificações" +emailVerificationFailedError: "Houve um problema ao verificar seu endereço de email. O link pode ter expirado." cwNotationRequired: "Se \"Esconder conteúdo\" está habilitado, uma descrição deve ser adicionada." doReaction: "Adicionar reação" code: "Código" @@ -1313,6 +1317,7 @@ acknowledgeNotesAndEnable: "Ative após compreender as precauções." federationSpecified: "Esse servidor opera com uma lista branca de federação. Interagir com servidores diferentes daqueles designados pela administração não é permitido." federationDisabled: "Federação está desabilitada nesse servidor. Você não pode interagir com usuários de outros servidores." draft: "Rascunhos" +draftsAndScheduledNotes: "Rascunhos e notas agendadas." confirmOnReact: "Confirmar ao reagir" reactAreYouSure: "Você deseja adicionar uma reação \"{emoji}\"?" markAsSensitiveConfirm: "Você deseja definir essa mídia como sensível?" @@ -1340,6 +1345,8 @@ postForm: "Campo de postagem" textCount: "Contagem de caracteres" information: "Sobre" chat: "Conversas" +directMessage: "Conversar com usuário" +directMessage_short: "Mensagem" migrateOldSettings: "Migrar configurações antigas de cliente" migrateOldSettings_description: "Isso deve ser feito automaticamente. Caso o processo de migração tenha falhado, você pode acioná-lo manualmente. As informações atuais de migração serão substituídas." compress: "Comprimir" @@ -1367,12 +1374,35 @@ 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.
Alto, reduz o tamanho do arquivo mas, também, a qualidade da imagem." +defaultCompressionLevel: "Nível padrão de compressão" +defaultCompressionLevel_description: "Menor compressão preserva a qualidade mas aumenta o tamanho do arquivo.
Maior compressão reduz o tamanho do arquivo mas diminui a qualidade." inMinutes: "Minuto(s)" inDays: "Dia(s)" +safeModeEnabled: "Modo seguro está habilitado" +pluginsAreDisabledBecauseSafeMode: "Todos os plugins estão desabilitados porque o modo seguro está habilitado." +customCssIsDisabledBecauseSafeMode: "CSS personalizado não está aplicado porque o modo seguro está habilitado." +themeIsDefaultBecauseSafeMode: "Enquanto o modo seguro estiver ativo, o tema padrão é utilizado. Desabilitar o modo seguro reverterá essas mudanças." +thankYouForTestingBeta: "Obrigado por nos ajudar a testar a versão beta!" +createUserSpecifiedNote: "Criar uma nota direta" +schedulePost: "Agendar publicação" +scheduleToPostOnX: "Agendar nota para {x}" +scheduledToPostOnX: "A nota está agendada para {x}" +schedule: "Agendar" +scheduled: "Agendado" +_compression: + _quality: + high: "Qualidade alta" + medium: "Qualidade média" + low: "Qualidade baixa" + _size: + large: "Tamanho grande" + medium: "Tamanho médio" + small: "Tamanho pequeno" _order: newest: "Priorizar Mais Novos" oldest: "Priorizar Mais Antigos" _chat: + messages: "Mensagem" noMessagesYet: "Ainda não há mensagens" newMessage: "Nova mensagem" individualChat: "Conversa Particular" @@ -1460,6 +1490,7 @@ _settings: contentsUpdateFrequency_description2: "Quando o modo tempo-real está ativado, o conteúdo é atualizado em tempo real, ignorando essa opção." showUrlPreview: "Exibir prévia de URL" showAvailableReactionsFirstInNote: "Exibir reações disponíveis no topo." + showPageTabBarBottom: "Mostrar barra de aba da página inferiormente" _chat: showSenderName: "Exibir nome de usuário do remetente" sendOnEnter: "Pressionar Enter para enviar" @@ -1633,6 +1664,10 @@ _serverSettings: fanoutTimelineDbFallback: "\"Fallback\" ao banco de dados" fanoutTimelineDbFallbackDescription: "Quando habilitado, a linha do tempo irá recuar ao banco de dados caso consultas adicionais sejam feitas e ela não estiver em cache. Quando desabilitado, o impacto no servidor será reduzido ao eliminar o recuo, mas limita a quantidade de linhas do tempo que podem ser recebidas." reactionsBufferingDescription: "Quando ativado, o desempenho durante a criação de uma reação será melhorado substancialmente, reduzindo a carga do banco de dados. Porém, a o uso de memória do Redis irá aumentar." + remoteNotesCleaning: "Limpeza automática de notas remotas" + remoteNotesCleaning_description: "Quando habilitado, notas remotas obsoletas e não utilizadas serão periodicamente limpadas para previnir sobrecarga no banco de dados." + remoteNotesCleaningMaxProcessingDuration: "Maximizar tempo de processamento da limpeza" + remoteNotesCleaningExpiryDaysForEachNotes: "Mínimo de dias para retenção de notas" inquiryUrl: "URL de inquérito" inquiryUrlDescription: "Especifique um URL para um formulário de inquérito para a administração ou uma página web com informações de contato." openRegistration: "Abrir a criação de contas" @@ -1651,6 +1686,11 @@ _serverSettings: userGeneratedContentsVisibilityForVisitor: "Visibilidade de conteúdo dos usuários para visitantes" userGeneratedContentsVisibilityForVisitor_description: "Isso é útil para prevenir problemas causados por conteúdo inapropriado de usuários remotos de servidores com pouca ou nenhuma moderação, que pode ser hospedado na internet a partir desse servidor." userGeneratedContentsVisibilityForVisitor_description2: "Publicar todo o conteúdo do servidor para a internet pode ser arriscado. Isso é especialmente importante para visitantes que desconhecem a natureza distribuída do conteúdo na internet, pois eles podem acreditar que o conteúdo remoto é criado por usuários desse servidor." + restartServerSetupWizardConfirm_title: "Reiniciar o assistente de configuração?" + restartServerSetupWizardConfirm_text: "Algumas configurações atuais serão reiniciadas." + entrancePageStyle: "Estilo da página de entrada" + showTimelineForVisitor: "Mostrar linha do tempo" + showActivitiesForVisitor: "Mostrar atividades" _userGeneratedContentsVisibilityForVisitor: all: "Tudo é público" localOnly: "Conteúdo local é publicado, conteúdo remoto é privado" @@ -1987,6 +2027,7 @@ _role: descriptionOfRateLimitFactor: "Valores menores são menos restritivos, valores maiores são mais restritivos." canHideAds: "Permitir ocultar anúncios" canSearchNotes: "Permitir a busca de notas" + canSearchUsers: "Busca de usuário" canUseTranslator: "Uso do tradutor" avatarDecorationLimit: "Número máximo de decorações de avatar que podem ser aplicadas" canImportAntennas: "Permitir importação de antenas" @@ -1999,6 +2040,7 @@ _role: uploadableFileTypes_caption: "Especifica tipos MIME permitidos. Múltiplos tipos MIME podem ser especificados separando-os por linha. Curingas podem ser especificados com um asterisco (*). (exemplo, image/*)" uploadableFileTypes_caption2: "Alguns tipos de arquivos podem não ser detectados. Para permiti-los, adicione {x} à especificação." noteDraftLimit: "Limite de rascunhos possíveis" + scheduledNoteLimit: "Número máximo de notas agendadas simultâneas" watermarkAvailable: "Disponibilidade da função de marca d'água" _condition: roleAssignedTo: "Atribuído a cargos manuais" @@ -2259,6 +2301,7 @@ _time: minute: "Minuto(s)" hour: "Hora(s)" day: "Dia(s)" + month: "Mês(es)" _2fa: alreadyRegistered: "Você já cadastrou um dispositivo de autenticação de dois fatores." registerTOTP: "Cadastrar aplicativo autenticador" @@ -2433,7 +2476,7 @@ _widgets: chooseList: "Selecione uma lista" clicker: "Clicker" birthdayFollowings: "Usuários de aniversário hoje" - chat: "Conversas" + chat: "Conversar com usuário" _cw: hide: "Esconder" show: "Carregar mais" @@ -2623,6 +2666,8 @@ _notification: youReceivedFollowRequest: "Você recebeu um pedido de seguidor" yourFollowRequestAccepted: "Seu pedido de seguidor foi aceito" pollEnded: "Os resultados da enquete agora estão disponíveis" + scheduledNotePosted: "Nota agendada foi publicada" + scheduledNotePostFailed: "Não foi possível publicar nota agendada" newNote: "Nova nota" unreadAntennaNote: "Antena {name}" roleAssigned: "Cargo dado" @@ -2702,7 +2747,7 @@ _deck: mentions: "Menções" direct: "Notas diretas" roleTimeline: "Linha do tempo do cargo" - chat: "Conversas" + chat: "Conversar com usuário" _dialog: charactersExceeded: "Você excedeu o limite de caracteres! Atualmente em {current} de {max}." charactersBelow: "Você está abaixo do limite mínimo de caracteres! Atualmente em {current} of {min}." @@ -3061,6 +3106,7 @@ _bootErrors: otherOption1: "Excluir ajustes de cliente e cache" otherOption2: "Iniciar o cliente simples" otherOption3: "Iniciar ferramenta de reparo" + otherOption4: "Abrir Misskey no modo seguro" _search: searchScopeAll: "Todos" searchScopeLocal: "Local" @@ -3097,6 +3143,8 @@ _serverSetupWizard: doYouConnectToFediverse_description1: "Quando conectado com uma rede distribuída de servidores (Fediverso), o conteúdo pode ser trocado com outros servidores." doYouConnectToFediverse_description2: "Conectar com o Fediverso também é chamado de \"federação\"" youCanConfigureMoreFederationSettingsLater: "Configurações adicionais como especificar servidores para conectar-se com podem ser feitas posteriormente" + remoteContentsCleaning: "Limpeza automática de conteúdos recebidos" + remoteContentsCleaning_description: "A federação pode resultar em uma entrada contínua de conteúdo. Habilitar a limpeza automática removerá conteúdo obsoleto e não referenciado do servidor para economizar armazenamento." adminInfo: "Informações da administração" adminInfo_description: "Define as informações do administrador usadas para receber consultas." adminInfo_mustBeFilled: "Deve ser preenchido se o servidor é público ou se a federação está ativa." @@ -3145,7 +3193,9 @@ _watermarkEditor: opacity: "Opacidade" scale: "Tamanho" text: "Texto" + qr: "Código QR" position: "Posição" + margin: "Margem" type: "Tipo" image: "imagem" advanced: "Avançado" @@ -3160,16 +3210,20 @@ _watermarkEditor: polkadotSubDotOpacity: "Opacidade da bolinha secundária" polkadotSubDotRadius: "Raio das bolinhas adicionais" polkadotSubDotDivisions: "Número de bolinhas adicionais" + leaveBlankToAccountUrl: "Deixe em branco para utilizar URL da conta" _imageEffector: title: "Efeitos" addEffect: "Adicionar efeitos" discardChangesConfirm: "Tem certeza que deseja sair? Há mudanças não salvas." + nothingToConfigure: "Não há nada para configurar" _fxs: chromaticAberration: "Aberração cromática" glitch: "Glitch" mirror: "Espelho" invert: "Inverter Cores" grayscale: "Tons de Cinza" + blur: "Desfoque" + pixelate: "Pixelizar" colorAdjust: "Correção de Cores" colorClamp: "Compressão de Cores" colorClampAdvanced: "Compressão Avançada de Cores" @@ -3181,13 +3235,43 @@ _imageEffector: checker: "Xadrez" blockNoise: "Bloquear Ruído" tearing: "Descontinuidade" + fill: "Preencher" _fxProps: angle: "Ângulo" scale: "Tamanho" size: "Tamanho" + radius: "Raio" + samples: "Número de amostras" + offset: "Posição" color: "Cor" opacity: "Opacidade" + normalize: "Normalizar" + amount: "Quantidade" lightness: "Esclarecer" + contrast: "Contraste" + hue: "Matiz" + brightness: "Brilho" + saturation: "Saturação" + max: "Máximo" + min: "Mínimo" + direction: "Direção" + phase: "Fase" + frequency: "Frequência" + strength: "Força" + glitchChannelShift: "Mudança de canal" + seed: "Valor da semente" + redComponent: "Componente vermelho" + greenComponent: "Componente verde" + blueComponent: "Componente azul" + 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: select: "Selecionar Rascunho" @@ -3203,3 +3287,22 @@ _drafts: restoreFromDraft: "Restaurar de Rascunho" restore: "Redefinir" listDrafts: "Lista de Rascunhos" + schedule: "Agendar nota" + listScheduledNotes: "Lista de notas agendadas" + cancelSchedule: "Cancelar agendamento" +qr: "Código QR" +_qr: + showTabTitle: "Visualizar" + readTabTitle: "Escanear" + shareTitle: "{name} {acct}" + shareText: "Siga-me no Fediverso!" + chooseCamera: "Escolher câmera" + cannotToggleFlash: "Não foi possível ligar a lanterna" + turnOnFlash: "Ligar a lanterna" + turnOffFlash: "Desligar a lanterna" + startQr: "Retornar ao leitor de códigos QR" + stopQr: "Deixar o leitor de códigos QR" + noQrCodeFound: "Nenhum código QR encontrado" + scanFile: "Escanear imagem de dispositivo" + raw: "Texto" + mfm: "MFM" diff --git a/locales/ro-RO.yml b/locales/ro-RO.yml index b08341711a..cc37b531f5 100644 --- a/locales/ro-RO.yml +++ b/locales/ro-RO.yml @@ -1404,3 +1404,7 @@ _imageEffector: _fxProps: scale: "Dimensiune" size: "Dimensiune" + offset: "Poziție" +_qr: + showTabTitle: "Arată" + raw: "Text" diff --git a/locales/ru-RU.yml b/locales/ru-RU.yml index 5e52143f83..27fc425e3d 100644 --- a/locales/ru-RU.yml +++ b/locales/ru-RU.yml @@ -2276,7 +2276,11 @@ _imageEffector: angle: "Угол" scale: "Размер" size: "Размер" + offset: "Позиция" color: "Цвет" opacity: "Непрозрачность" lightness: "Осветление" drafts: "Черновик" +_qr: + showTabTitle: "Отображение" + raw: "Текст" diff --git a/locales/sk-SK.yml b/locales/sk-SK.yml index c9fd8e6ae9..2b86826703 100644 --- a/locales/sk-SK.yml +++ b/locales/sk-SK.yml @@ -1466,3 +1466,6 @@ _imageEffector: color: "Farba" opacity: "Priehľadnosť" lightness: "Zosvetliť" +_qr: + showTabTitle: "Zobraziť" + raw: "Text" diff --git a/locales/th-TH.yml b/locales/th-TH.yml index f70b0d5be8..b945decbab 100644 --- a/locales/th-TH.yml +++ b/locales/th-TH.yml @@ -253,6 +253,7 @@ noteDeleteConfirm: "ต้องการลบโน้ตนี้ใช่ไ pinLimitExceeded: "คุณไม่สามารถปักหมุดโน้ตเพิ่มเติมใดๆได้อีก" done: "เสร็จสิ้น" processing: "กำลังประมวลผล..." +preprocessing: "กำลังจัดเตรียม..." preview: "แสดงตัวอย่าง" default: "ค่าเริ่มต้น" defaultValueIs: "ค่าเริ่มต้น: {value}" @@ -1245,6 +1246,7 @@ releaseToRefresh: "ปล่อยเพื่อรีเฟรช" refreshing: "กำลังรีเฟรช..." pullDownToRefresh: "ดึงลงเพื่อรีเฟรช" useGroupedNotifications: "แสดงผลการแจ้งเตือนแบบกลุ่มแล้ว" +emailVerificationFailedError: "เกิดปัญหาในขณะตรวจสอบอีเมล อาจเป็นไปได้ว่าลิงก์หมดอายุแล้ว" cwNotationRequired: "หากเปิดใช้งาน “ซ่อนเนื้อหา” จะต้องระบุคำอธิบาย" doReaction: "เพิ่มรีแอคชั่น" code: "โค้ด" @@ -1315,6 +1317,7 @@ acknowledgeNotesAndEnable: "เปิดใช้งานหลังจาก federationSpecified: "เซิร์ฟเวอร์นี้ดำเนินงานในระบบกลุ่มไวท์ลิสต์ ไม่สามารถติดต่อกับเซิร์ฟเวอร์อื่นที่ไม่ได้รับอนุญาตจากผู้ดูแลระบบได้" federationDisabled: "เซิร์ฟเวอร์นี้ปิดใช้งานสหพันธ์ ไม่สามารถติดต่อหรือแลกเปลี่ยนข้อมูลกับผู้ใช้จากเซิร์ฟเวอร์อื่นได้" draft: "ร่าง" +draftsAndScheduledNotes: "ร่างและกำหนดเวลาโพสต์" confirmOnReact: "ยืนยันเมื่อทำการรีแอคชั่น" reactAreYouSure: "ต้องการใส่รีแอคชั่นด้วย \"{emoji}\" หรือไม่?" markAsSensitiveConfirm: "ต้องการตั้งค่าสื่อนี้ว่าเป็นเนื้อหาละเอียดอ่อนหรือไม่?" @@ -1342,6 +1345,8 @@ postForm: "แบบฟอร์มการโพสต์" textCount: "จำนวนอักขระ" information: "เกี่ยวกับ" chat: "แชต" +directMessage: "แชตเลย" +directMessage_short: "ข้อความ" migrateOldSettings: "ย้ายข้อมูลการตั้งค่าเก่า" migrateOldSettings_description: "โดยปกติจะทำโดยอัตโนมัติ แต่หากด้วยเหตุผลบางประการที่ไม่สามารถย้ายได้สำเร็จ สามารถสั่งย้ายด้วยตนเองได้ การตั้งค่าปัจจุบันจะถูกเขียนทับ" compress: "บีบอัด" @@ -1367,8 +1372,10 @@ abort: "หยุดและยกเลิก" tip: "คำแนะนำและเคล็ดลับ" redisplayAllTips: "แสดงคำแนะนำและเคล็ดลับทั้งหมดอีกครั้ง" hideAllTips: "ซ่อนคำแนะนำและเคล็ดลับทั้งหมด" -defaultImageCompressionLevel: "ความละเอียดเริ่มต้นสำหรับการบีบอัดภาพ" +defaultImageCompressionLevel: "ค่าการบีบอัดภาพเริ่มต้น" defaultImageCompressionLevel_description: "หากตั้งค่าต่ำ จะรักษาคุณภาพภาพได้ดีขึ้นแต่ขนาดไฟล์จะเพิ่มขึ้น
หากตั้งค่าสูง จะลดขนาดไฟล์ได้ แต่คุณภาพภาพจะลดลง" +defaultCompressionLevel: "ค่าการบีบอัดเริ่มต้น" +defaultCompressionLevel_description: "ถ้าต่ำ จะรักษาคุณภาพได้ แต่ขนาดไฟล์จะเพิ่มขึ้น
ถ้าสูง จะลดขนาดไฟล์ได้ แต่คุณภาพจะลดลง" inMinutes: "นาที" inDays: "วัน" safeModeEnabled: "โหมดปลอดภัยถูกเปิดใช้งาน" @@ -1376,10 +1383,26 @@ pluginsAreDisabledBecauseSafeMode: "เนื่องจากโหมดป customCssIsDisabledBecauseSafeMode: "เนื่องจากโหมดปลอดภัยถูกเปิดใช้งาน CSS แบบกำหนดเองจึงไม่ได้ถูกนำมาใช้" themeIsDefaultBecauseSafeMode: "ในระหว่างที่โหมดปลอดภัยถูกเปิดใช้งาน จะใช้ธีมเริ่มต้น เมื่อปิดโหมดปลอดภัยจะกลับคืนดังเดิม" thankYouForTestingBeta: "ขอบคุณที่ให้ความร่วมมือในการทดสอบเวอร์ชันเบต้า!" +createUserSpecifiedNote: "สร้างโน้ตแบบไดเร็กต์" +schedulePost: "กำหนดเวลาให้โพสต์" +scheduleToPostOnX: "กำหนดเวลาให้โพสต์ไว้ที่ {x}" +scheduledToPostOnX: "มีการกำหนดเวลาให้โพสต์ไว้ที่ {x}" +schedule: "กำหนดเวลา" +scheduled: "กำหนดเวลา" +_compression: + _quality: + high: "คุณภาพสูง" + medium: "คุณภาพปานกลาง" + low: "คุณภาพต่ำ" + _size: + large: "ขนาดใหญ่" + medium: "ขนาดปานกลาง" + small: "ขนาดเล็ก" _order: newest: "เรียงจากใหม่ไปเก่า" oldest: "เรียงจากเก่าไปใหม่" _chat: + messages: "ข้อความ" noMessagesYet: "ยังไม่มีข้อความ" newMessage: "ข้อความใหม่" individualChat: "แชตส่วนตัว" @@ -1665,6 +1688,9 @@ _serverSettings: userGeneratedContentsVisibilityForVisitor_description2: "การเปิดเผยเนื้อหาทั้งหมดในเซิร์ฟเวอร์รวมทั้งเนื้อหาที่รับมาจากระยะไกลสู่สาธารณะบนอินเทอร์เน็ตโดยไม่มีข้อจำกัดใดๆ มีความเสี่ยงโดยเฉพาะอย่างยิ่งสำหรับผู้ชมที่ไม่เข้าใจลักษณะของระบบแบบกระจาย อาจทำให้เกิดความเข้าใจผิดคิดว่าเนื้อหาที่มาจากระยะไกลนั้นเป็นเนื้อหาที่สร้างขึ้นภายในเซิร์ฟเวอร์นี้ จึงควรใช้ความระมัดระวังอย่างมาก" restartServerSetupWizardConfirm_title: "ต้องการเริ่มวิซาร์ดการตั้งค่าเซิร์ฟเวอร์ใหม่หรือไม่?" restartServerSetupWizardConfirm_text: "การตั้งค่าบางส่วนในปัจจุบันจะถูกรีเซ็ต" + entrancePageStyle: "สไตล์ของหน้าเพจทางเข้า" + showTimelineForVisitor: "แสดงไทม์ไลน์" + showActivitiesForVisitor: "แสดงกิจกรรม" _userGeneratedContentsVisibilityForVisitor: all: "ทั้งหมดสาธารณะ" localOnly: "เผยแพร่เป็นสาธารณะเฉพาะเนื้อหาท้องถิ่น เนื้อหาระยะไกลให้เป็นส่วนตัว" @@ -2014,6 +2040,7 @@ _role: uploadableFileTypes_caption: "สามารถระบุ MIME type ได้ โดยใช้การขึ้นบรรทัดใหม่เพื่อแยกหลายรายการ และสามารถใช้ดอกจัน (*) เพื่อระบุแบบไวลด์การ์ดได้ (เช่น: image/*)" uploadableFileTypes_caption2: "ไฟล์บางประเภทอาจไม่สามารถระบุชนิดได้ หากต้องการอนุญาตไฟล์ลักษณะนั้น กรุณาเพิ่ม {x} ลงในรายการที่อนุญาต" noteDraftLimit: "จำนวนโน้ตฉบับร่างที่สามารถสร้างได้บนฝั่งเซิร์ฟเวอร์" + scheduledNoteLimit: "จำนวนโพสต์กำหนดเวลาที่สร้างพร้อมกันได้" watermarkAvailable: "มีฟังก์ชั่นลายน้ำให้เลือกใช้" _condition: roleAssignedTo: "มอบหมายให้มีบทบาทแบบทำมือ" @@ -2449,7 +2476,7 @@ _widgets: chooseList: "เลือกรายชื่อ" clicker: "คลิกเกอร์" birthdayFollowings: "วันเกิดผู้ใช้ในวันนี้" - chat: "แชต" + chat: "แชตเลย" _cw: hide: "ซ่อน" show: "โหลดเพิ่มเติม" @@ -2639,6 +2666,8 @@ _notification: youReceivedFollowRequest: "ได้รับคำขอติดตาม" yourFollowRequestAccepted: "คำขอติดตามได้รับการอนุมัติแล้ว" pollEnded: "ผลโพลออกมาแล้ว" + scheduledNotePosted: "โน้ตที่กำหนดเวลาไว้ได้ถูกโพสต์แล้ว" + scheduledNotePostFailed: "ล้มเหลวในการโพสต์โน้ตที่กำหนดเวลาไว้" newNote: "โพสต์ใหม่" unreadAntennaNote: "เสาอากาศ {name}" roleAssigned: "ได้รับบทบาท" @@ -2718,7 +2747,7 @@ _deck: mentions: "กล่าวถึงคุณ" direct: "ไดเร็กต์" roleTimeline: "บทบาทไทม์ไลน์" - chat: "แชต" + chat: "แชตเลย" _dialog: charactersExceeded: "คุณกำลังมีตัวอักขระเกินขีดจำกัดสูงสุดแล้วนะ! ปัจจุบันอยู่ที่ {current} จาก {max}" charactersBelow: "คุณกำลังใช้อักขระต่ำกว่าขีดจำกัดขั้นต่ำเลยนะ! ปัจจุบันอยู่ที่ {current} จาก {min}" @@ -3164,7 +3193,9 @@ _watermarkEditor: opacity: "ความทึบแสง" scale: "ขนาด" text: "ข้อความ" + qr: "QR โค้ด" position: "ตำแหน่ง" + margin: "ระยะขอบ" type: "รูปแบบ" image: "รูปภาพ" advanced: "ขั้นสูง" @@ -3179,6 +3210,7 @@ _watermarkEditor: polkadotSubDotOpacity: "ความทึบของจุดรอง" polkadotSubDotRadius: "ขนาดของจุดรอง" polkadotSubDotDivisions: "จำนวนจุดรอง" + leaveBlankToAccountUrl: "เว้นว่างไว้หากต้องการใช้ URL ของบัญชีแทน" _imageEffector: title: "เอฟเฟกต์" addEffect: "เพิ่มเอฟเฟกต์" @@ -3190,6 +3222,8 @@ _imageEffector: mirror: "กระจก" invert: "กลับสี" grayscale: "ขาวดำเทา" + blur: "มัว" + pixelate: "โมเสก" colorAdjust: "ปรับแก้สี" colorClamp: "บีบอัดสี" colorClampAdvanced: "บีบอัดสี (ขั้นสูง)" @@ -3201,10 +3235,14 @@ _imageEffector: checker: "ช่องตาราง" blockNoise: "บล็อกที่มีการรบกวน" tearing: "ฉีกขาด" + fill: "เติมเต็ม" _fxProps: angle: "แองเกิล" scale: "ขนาด" size: "ขนาด" + radius: "รัศสี" + samples: "จำนวนตัวอย่าง" + offset: "ตำแหน่ง" color: "สี" opacity: "ความทึบแสง" normalize: "นอร์มัลไลซ์" @@ -3233,6 +3271,7 @@ _imageEffector: zoomLinesThreshold: "ความกว้างเส้นรวมศูนย์" zoomLinesMaskSize: "ขนาดพื้นที่ตรงกลาง" zoomLinesBlack: "ทำให้ดำ" + circle: "ทรงกลม" drafts: "ร่าง" _drafts: select: "เลือกฉบับร่าง" @@ -3248,3 +3287,22 @@ _drafts: restoreFromDraft: "คืนค่าจากฉบับร่าง" restore: "กู้คืน" listDrafts: "รายการฉบับร่าง" + schedule: "โพสต์กำหนดเวลา" + listScheduledNotes: "รายการโน้ตที่กำหนดเวลาไว้" + cancelSchedule: "ยกเลิกกำหนดเวลา" +qr: "QR โค้ด" +_qr: + showTabTitle: "แสดงผล" + readTabTitle: "แสกน" + shareTitle: "{name}{acct}" + shareText: "โปรดติดตามฉันบน Fediverse ด้วย!" + chooseCamera: "เลือกกล้อง" + cannotToggleFlash: "ไม่สามารถเลือกแสงแฟลชได้" + turnOnFlash: "ปิดแสงแฟลช" + turnOffFlash: "เปิดแสงแฟลช" + startQr: "เริ่มตัวอ่าน QR โค้ด" + stopQr: "หยุดตัวอ่าน QR โค้ด" + noQrCodeFound: "ไม่พบ QR โค้ด" + scanFile: "สแกนภาพจากอุปกรณ์" + raw: "ข้อความ" + mfm: "MFM" diff --git a/locales/tr-TR.yml b/locales/tr-TR.yml index 5ca2b18fac..f73ebafa89 100644 --- a/locales/tr-TR.yml +++ b/locales/tr-TR.yml @@ -1343,6 +1343,7 @@ postForm: "Gönderim formu" textCount: "Karakter sayısı" information: "Hakkında" chat: "Sohbet" +directMessage: "Kullanıcıyla sohbet et" 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" @@ -3209,6 +3210,7 @@ _imageEffector: angle: "Açı" scale: "Boyut" size: "Boyut" + offset: "Pozisyon" color: "Renk" opacity: "Opaklık" normalize: "Normalize" @@ -3252,3 +3254,6 @@ _drafts: restoreFromDraft: "Taslaktan geri yükle" restore: "Geri yükle" listDrafts: "Taslaklar Listesi" +_qr: + showTabTitle: "Ekran" + raw: "Metin" diff --git a/locales/uk-UA.yml b/locales/uk-UA.yml index 46d9ab95ff..e33eff637c 100644 --- a/locales/uk-UA.yml +++ b/locales/uk-UA.yml @@ -1655,3 +1655,6 @@ _imageEffector: color: "Колір" opacity: "Непрозорість" lightness: "Яскравість" +_qr: + showTabTitle: "Відображення" + raw: "Текст" diff --git a/locales/uz-UZ.yml b/locales/uz-UZ.yml index cfa2e26fd5..a17df99ad5 100644 --- a/locales/uz-UZ.yml +++ b/locales/uz-UZ.yml @@ -1106,3 +1106,6 @@ _imageEffector: _fxProps: color: "Rang" lightness: "Yoritish" +_qr: + showTabTitle: "Displey" + raw: "Matn" diff --git a/locales/vi-VN.yml b/locales/vi-VN.yml index cb2f37bed7..639cf92954 100644 --- a/locales/vi-VN.yml +++ b/locales/vi-VN.yml @@ -1816,7 +1816,6 @@ _widgets: _userList: chooseList: "Chọn danh sách" clicker: "clicker" - chat: "Trò chuyện" _cw: hide: "Ẩn" show: "Tải thêm" @@ -2042,7 +2041,6 @@ _deck: channel: "Kênh" mentions: "Lượt nhắc" direct: "Nhắn riêng" - chat: "Trò chuyện" _dialog: charactersExceeded: "Bạn nhắn quá giới hạn ký tự!! Hiện nay {current} / giới hạn {max}" charactersBelow: "Bạn nhắn quá ít tối thiểu ký tự!! Hiện nay {current} / Tối thiểu {min}" @@ -2095,6 +2093,10 @@ _imageEffector: angle: "Góc" scale: "Kích thước" size: "Kích thước" + offset: "Vị trí" color: "Màu sắc" opacity: "Độ trong suốt" lightness: "Độ sáng" +_qr: + showTabTitle: "Hiển thị" + raw: "Văn bản" diff --git a/locales/zh-CN.yml b/locales/zh-CN.yml index 6c62c80f0d..bf59023952 100644 --- a/locales/zh-CN.yml +++ b/locales/zh-CN.yml @@ -87,7 +87,7 @@ exportRequested: "导出请求已提交,这可能需要花一些时间,导 importRequested: "导入请求已提交,这可能需要花一点时间。" lists: "列表" noLists: "列表为空" -note: "帖子" +note: "发帖" notes: "帖子" following: "关注中" followers: "关注者" @@ -144,15 +144,15 @@ markAsSensitive: "标记为敏感内容" unmarkAsSensitive: "取消标记为敏感内容" enterFileName: "输入文件名" mute: "屏蔽" -unmute: "取消隐藏" -renoteMute: "隐藏转帖" -renoteUnmute: "解除隐藏转帖" -block: "屏蔽" -unblock: "取消屏蔽" +unmute: "取消屏蔽" +renoteMute: "屏蔽转帖" +renoteUnmute: "取消屏蔽转帖" +block: "拉黑" +unblock: "取消拉黑" suspend: "冻结" unsuspend: "解除冻结" -blockConfirm: "确定要屏蔽吗?" -unblockConfirm: "确定要取消屏蔽吗?" +blockConfirm: "确定要拉黑吗?" +unblockConfirm: "确定要取消拉黑吗?" suspendConfirm: "要冻结吗?" unsuspendConfirm: "要解除冻结吗?" selectList: "选择列表" @@ -244,22 +244,23 @@ mediaSilencedInstances: "已隐藏媒体文件的服务器" mediaSilencedInstancesDescription: "设置要隐藏媒体文件的服务器,以换行分隔。被设置的服务器内所有账号的文件均按照「敏感内容」处理,且将无法使用自定义表情符号。被阻止的实例不受影响。" federationAllowedHosts: "允许联合的服务器" federationAllowedHostsDescription: "设定允许联合的服务器,以换行分隔。" -muteAndBlock: "隐藏和屏蔽" -mutedUsers: "已隐藏用户" -blockedUsers: "已屏蔽的用户" +muteAndBlock: "屏蔽/拉黑" +mutedUsers: "已屏蔽用户" +blockedUsers: "已拉黑的用户" noUsers: "无用户" editProfile: "编辑资料" noteDeleteConfirm: "确定要删除该帖子吗?" pinLimitExceeded: "无法置顶更多了" done: "完成" processing: "正在处理" +preprocessing: "准备中" preview: "预览" default: "默认" defaultValueIs: "默认值: {value}" noCustomEmojis: "没有自定义表情符号" noJobs: "没有任务" federating: "联合中" -blocked: "已屏蔽" +blocked: "已拉黑" suspended: "停止投递" all: "全部" subscribing: "已订阅" @@ -303,7 +304,7 @@ explore: "发现" messageRead: "已读" noMoreHistory: "没有更多的历史记录" startChat: "开始聊天" -nUsersRead: "{n} 人已读" +nUsersRead: "{n}人已读" agreeTo: "勾选则表示已阅读并同意 {0}" agree: "同意" agreeBelow: "同意以下内容" @@ -395,7 +396,7 @@ basicInfo: "基本信息" pinnedUsers: "置顶用户" pinnedUsersDescription: "输入您想要固定到“发现”页面的用户,一行一个。" pinnedPages: "固定页面" -pinnedPagesDescription: "输入您要固定到服务器首页的页面路径,一行一个。" +pinnedPagesDescription: "输入您要固定到服务器首页的页面路径,以换行符分隔。" pinnedClipId: "置顶的便签 ID" pinnedNotes: "已置顶的帖子" hcaptcha: "hCaptcha" @@ -428,7 +429,7 @@ notifyAntenna: "开启通知" withFileAntenna: "仅带有附件的帖子" excludeNotesInSensitiveChannel: "排除敏感频道内的帖子" enableServiceworker: "启用 ServiceWorker" -antennaUsersDescription: "指定用户名,一行一个" +antennaUsersDescription: "指定用户名,用换行符进行分隔" caseSensitive: "区分大小写" withReplies: "包括回复" connectedTo: "您的账号已连到接以下第三方账号" @@ -460,7 +461,7 @@ moderationNote: "管理笔记" moderationNoteDescription: "可以用来记录仅在管理员之间共享的笔记。" addModerationNote: "添加管理笔记" moderationLogs: "管理日志" -nUsersMentioned: "{n} 被提到" +nUsersMentioned: "{n}人投稿" securityKeyAndPasskey: "安全密钥或 Passkey" securityKey: "安全密钥" lastUsed: "最后使用:" @@ -477,7 +478,7 @@ notFoundDescription: "没有与指定 URL 对应的页面。" uploadFolder: "默认上传文件夹" markAsReadAllNotifications: "将所有通知标为已读" markAsReadAllUnreadNotes: "将所有帖子标记为已读" -markAsReadAllTalkMessages: "将所有聊天标记为已读" +markAsReadAllTalkMessages: "将所有私信标记为已读" help: "帮助" inputMessageHere: "在此键入信息" close: "关闭" @@ -597,7 +598,7 @@ recentUsed: "最近使用" install: "安装" uninstall: "卸载" installedApps: "已授权的应用" -nothing: "没有" +nothing: "无" installedDate: "授权日期" lastUsedDate: "最近使用" state: "状态" @@ -687,10 +688,10 @@ emptyToDisableSmtpAuth: "用户名和密码留空可以禁用 SMTP 验证" smtpSecure: "在 SMTP 连接中使用隐式 SSL / TLS" smtpSecureInfo: "使用 STARTTLS 时关闭。" testEmail: "邮件发送测试" -wordMute: "隐藏关键词" +wordMute: "屏蔽关键词" wordMuteDescription: "折叠包含指定关键词的帖子。被折叠的帖子可单击展开。" -hardWordMute: "隐藏硬关键词" -showMutedWord: "显示已隐藏的关键词" +hardWordMute: "强屏蔽关键词" +showMutedWord: "显示屏蔽关键词" hardWordMuteDescription: "隐藏包含指定关键词的帖子。与隐藏关键词不同,帖子将完全不会显示。" regexpError: "正则表达式错误" regexpErrorDescription: "{tab} 隐藏文字的第 {line} 行的正则表达式有错误:" @@ -779,7 +780,7 @@ emailVerified: "电子邮件地址已验证" noteFavoritesCount: "收藏的帖子数" pageLikesCount: "页面点赞次数" pageLikedCount: "页面被点赞次数" -contact: "联系人" +contact: "联系方式" useSystemFont: "使用系统默认字体" clips: "便签" experimentalFeatures: "实验性功能" @@ -800,7 +801,7 @@ showTitlebar: "显示标题栏" clearCache: "清除缓存" onlineUsersCount: "{n} 人在线" nUsers: "{n} 用户" -nNotes: "{n} 帖子" +nNotes: "{n}帖子" sendErrorReports: "发送错误报告" sendErrorReportsDescription: "启用后,如果出现问题,可以与 Misskey 共享详细的错误信息,从而帮助提高软件的质量。错误信息包括操作系统版本、浏览器类型、行为历史记录等。" myTheme: "我的主题" @@ -824,7 +825,7 @@ youAreRunningUpToDateClient: "您所使用的客户端已经是最新的。" newVersionOfClientAvailable: "新版本的客户端可用。" usageAmount: "使用量" capacity: "容量" -inUse: "已使用" +inUse: "使用中" editCode: "编辑代码" apply: "应用" receiveAnnouncementFromInstance: "从服务器接收通知" @@ -869,12 +870,12 @@ noMaintainerInformationWarning: "尚未设置管理员信息。" noInquiryUrlWarning: "尚未设置联络地址。" noBotProtectionWarning: "尚未设置 Bot 防御。" configure: "设置" -postToGallery: "发送到图库" +postToGallery: "创建新相册" postToHashtag: "投稿到这个标签" -gallery: "图库" +gallery: "相册" recentPosts: "最新发布" popularPosts: "热门投稿" -shareWithNote: "在帖子中分享" +shareWithNote: "分享到贴文" ads: "广告" expiration: "截止时间" startingperiod: "开始时间" @@ -885,7 +886,7 @@ middle: "中" low: "低" emailNotConfiguredWarning: "尚未设置电子邮件地址。" ratio: "比率" -previewNoteText: "预览文本" +previewNoteText: "预览正文" customCss: "自定义 CSS" customCssWarn: "这些设置必须有相关的基础知识,不当的配置可能导致客户端无法正常使用。" global: "全局" @@ -924,8 +925,8 @@ manageAccounts: "管理账户" makeReactionsPublic: "将回应设置为公开" makeReactionsPublicDescription: "将您发表过的回应设置成公开可见。" classic: "经典" -muteThread: "隐藏帖子列表" -unmuteThread: "取消隐藏帖子列表" +muteThread: "屏蔽帖文串" +unmuteThread: "取消屏蔽帖文串" followingVisibility: "关注的人的公开范围" followersVisibility: "关注者的公开范围" continueThread: "查看更多帖子" @@ -948,17 +949,17 @@ searchByGoogle: "Google" instanceDefaultLightTheme: "服务器默认浅色主题" instanceDefaultDarkTheme: "服务器默认深色主题" instanceDefaultThemeDescription: "以对象格式输入主题代码" -mutePeriod: "隐藏期限" +mutePeriod: "屏蔽期限" period: "截止时间" indefinitely: "永久" -tenMinutes: "10 分钟" +tenMinutes: "10分钟" oneHour: "1 小时" -oneDay: "1 天" +oneDay: "1天" oneWeek: "1 周" -oneMonth: "1 个月" -threeMonths: "3 个月" +oneMonth: "1个月" +threeMonths: "3个月" oneYear: "1 年" -threeDays: "3 天" +threeDays: "3天" reflectMayTakeTime: "可能需要一些时间才能体现出效果。" failedToFetchAccountInformation: "获取账户信息失败" rateLimitExceeded: "已超过速率限制" @@ -967,8 +968,8 @@ cropImageAsk: "是否要裁剪图像?" cropYes: "去裁剪" cropNo: "就这样吧!" file: "文件" -recentNHours: "最近 {n} 小时" -recentNDays: "最近 {n} 天" +recentNHours: "最近{n}小时" +recentNDays: "最近{n}天" noEmailServerWarning: "电子邮件服务器未设置。" thereIsUnresolvedAbuseReportWarning: "有未解决的报告" recommended: "推荐" @@ -1079,7 +1080,7 @@ postToTheChannel: "发布到频道" cannotBeChangedLater: "之后不能再更改。" reactionAcceptance: "接受表情回应" likeOnly: "仅点赞" -likeOnlyForRemote: "远程仅点赞" +likeOnlyForRemote: "全部(远程仅点赞)" nonSensitiveOnly: "仅限非敏感内容" nonSensitiveOnlyForLocalLikeOnlyForRemote: "仅限非敏感内容(远程仅点赞)" rolesAssignedToMe: "指派给自己的角色" @@ -1150,7 +1151,7 @@ youFollowing: "正在关注" preventAiLearning: "拒绝接受生成式 AI 的学习" preventAiLearningDescription: "要求文章生成 AI 或图像生成 AI 不能够以发布的帖子和图像等内容作为学习对象。这是通过在 HTML 响应中包含 noai 标志来实现的,这不能完全阻止 AI 学习你的发布内容,并不是所有 AI 都会遵守这类请求。" options: "选项" -specifyUser: "用户指定" +specifyUser: "指定用户" lookupConfirm: "确定查询?" openTagPageConfirm: "确定打开话题标签页面?" specifyHost: "指定主机名" @@ -1265,7 +1266,7 @@ replaying: "重播中" endReplay: "结束回放" copyReplayData: "复制回放数据" ranking: "排行榜" -lastNDays: "最近 {n} 天" +lastNDays: "最近{n}天" backToTitle: "返回标题" hemisphere: "居住地区" withSensitive: "显示包含敏感媒体的帖子" @@ -1316,6 +1317,7 @@ acknowledgeNotesAndEnable: "理解注意事项后再开启。" federationSpecified: "此服务器已开启联合白名单。只能与管理员指定的服务器通信。" federationDisabled: "此服务器已禁用联合。无法与其它服务器上的用户通信。" draft: "草稿" +draftsAndScheduledNotes: "草稿和定时发送" confirmOnReact: "发送回应前需要确认" reactAreYouSure: "要用「{emoji}」进行回应吗?" markAsSensitiveConfirm: "要将此媒体标记为敏感吗?" @@ -1343,6 +1345,8 @@ postForm: "投稿窗口" textCount: "字数" information: "关于" chat: "聊天" +directMessage: "私信" +directMessage_short: "消息" migrateOldSettings: "迁移旧设置信息" migrateOldSettings_description: "通常设置信息将自动迁移。但如果由于某种原因迁移不成功,则可以手动触发迁移过程。当前的配置信息将被覆盖。" compress: "压缩" @@ -1360,42 +1364,60 @@ advice: "建议" realtimeMode: "实时模式" turnItOn: "开启" turnItOff: "关闭" -emojiMute: "隐藏表情符号" -emojiUnmute: "解除隐藏表情符号" -muteX: "隐藏{x}" -unmuteX: "解除隐藏{x}" +emojiMute: "屏蔽表情符号" +emojiUnmute: "取消屏蔽表情符号" +muteX: "屏蔽{x}" +unmuteX: "取消屏蔽{x}" abort: "中止" tip: "提示和技巧" redisplayAllTips: "重新显示所有的提示和技巧" hideAllTips: "隐藏所有的提示和技巧" defaultImageCompressionLevel: "默认图像压缩等级" defaultImageCompressionLevel_description: "较低的等级可以保持画质,但会增加文件大小。
较高的等级可以减少文件大小,但相对应的画质将会降低。" -inMinutes: "分" -inDays: "日" +defaultCompressionLevel: "默认压缩等级" +defaultCompressionLevel_description: "较低的等级可以保持质量,但会增加文件大小。
较高的等级可以减少文件大小,但相对应的质量将会降低。" +inMinutes: "分钟" +inDays: "天" safeModeEnabled: "已启用安全模式" pluginsAreDisabledBecauseSafeMode: "因启用了安全模式,所有插件均已被禁用。" customCssIsDisabledBecauseSafeMode: "因启用了安全模式,无法应用自定义 CSS。" themeIsDefaultBecauseSafeMode: "启用安全模式时将使用默认主题。关闭安全模式后将还原。" thankYouForTestingBeta: "感谢您协助测试 beta 版!" +createUserSpecifiedNote: "创建指定用户的帖子" +schedulePost: "定时发布" +scheduleToPostOnX: "预定在 {x} 发出" +scheduledToPostOnX: "已预定在 {x} 发出" +schedule: "定时" +scheduled: "定时" +_compression: + _quality: + high: "高质量" + medium: "中质量" + low: "低质量" + _size: + large: "大" + medium: "中" + small: "小" _order: newest: "从新到旧" oldest: "从旧到新" _chat: + messages: "消息" noMessagesYet: "还没有消息" newMessage: "新消息" individualChat: "私聊" individualChat_description: "可以与特定用户进行一对一聊天。" roomChat: "群聊" - roomChat_description: "可以进行多人聊天。\n就算用户未允许私聊,只要接受了邀请,仍可以聊天。" - createRoom: "创建房间" + roomChat_description: "支持多人同时进行消息交流。\n即使部分用户未开放私信权限,只要接受了邀请,仍可进行聊天。" + createRoom: "创建群组" inviteUserToChat: "邀请用户来开始聊天" - yourRooms: "已创建的房间" - joiningRooms: "已加入的房间" + yourRooms: "创建的群组" + joiningRooms: "已加入的群组" invitations: "邀请" noInvitations: "没有邀请" history: "历史" noHistory: "没有历史记录" - noRooms: "没有房间" + noRooms: "没有群组" inviteUser: "邀请用户" sentInvitations: "已发送的邀请" join: "加入" @@ -1406,16 +1428,16 @@ _chat: home: "首页" send: "发送" newline: "换行" - muteThisRoom: "静音此房间" - deleteRoom: "删除房间" + muteThisRoom: "屏蔽该群组" + deleteRoom: "删除群组" chatNotAvailableForThisAccountOrServer: "此服务器或者账户还未开启聊天功能。" chatIsReadOnlyForThisAccountOrServer: "此服务器或者账户内的聊天为只读。无法发布新信息或创建及加入群聊。" - chatNotAvailableInOtherAccount: "对方账户目前处于无法使用聊天的状态。" - cannotChatWithTheUser: "无法与此用户聊天" + chatNotAvailableInOtherAccount: "对方的账户当前无法使用私信。" + cannotChatWithTheUser: "无法私信该用户" cannotChatWithTheUser_description: "可能现在无法使用聊天,或者对方未开启聊天。" youAreNotAMemberOfThisRoomButInvited: "您还未加入此房间,但已收到邀请。如要加入,请接受邀请。" doYouAcceptInvitation: "要接受邀请吗?" - chatWithThisUser: "聊天" + chatWithThisUser: "私信" thisUserAllowsChatOnlyFromFollowers: "此用户仅接受关注者发起的聊天。" thisUserAllowsChatOnlyFromFollowing: "此用户仅接受关注的人发起的聊天。" thisUserAllowsChatOnlyFromMutualFollowing: "此用户仅接受互相关注的人发起的聊天。" @@ -1662,7 +1684,7 @@ _serverSettings: allowExternalApRedirect: "允许通过 ActivityPub 重定向查询" allowExternalApRedirect_description: "启用时,将允许其它服务器通过此服务器查询第三方内容,但有可能导致内容欺骗。" userGeneratedContentsVisibilityForVisitor: "用户生成内容对非用户的可见性" - userGeneratedContentsVisibilityForVisitor_description: "对于防止难以审核的不适当的远程内容等,通过自己的服务器无意中在互联网上公开等问题很有用。" + userGeneratedContentsVisibilityForVisitor_description: "对于防止诸如难以管理的不适当的远程内容通过自己的服务器意外地在互联网上公开等问题很有用。" userGeneratedContentsVisibilityForVisitor_description2: "包含服务器接收到的远程内容在内,无条件将服务器上的所有内容公开在互联网上存在风险。特别是对去中心化的特性不是很了解的访问者有可能将远程服务器上的内容误认为是在此服务器内生成的,需要特别留意。" restartServerSetupWizardConfirm_title: "要重新开始服务器初始设定向导吗?" restartServerSetupWizardConfirm_text: "现有的部分设定将重置。" @@ -1893,7 +1915,7 @@ _achievements: description: "试图对网盘中的文件夹进行循环嵌套" _reactWithoutRead: title: "有好好读过吗?" - description: "在含有 100 字以上的帖子被发出三秒内做出回应" + description: "在含有100字以上的帖子被发出三秒内做出回应" _clickedClickHere: title: "点这里" description: "点了这里" @@ -1995,7 +2017,7 @@ _role: canUpdateBioMedia: "可以更新头像和横幅" pinMax: "帖子置顶数量限制" antennaMax: "可创建的最大天线数量" - wordMuteMax: "隐藏词的字数限制" + wordMuteMax: "屏蔽词的字数限制" webhookMax: "Webhook 创建数量限制" clipMax: "便签创建数量限制" noteEachClipsMax: "单个便签内的贴文数量限制" @@ -2013,11 +2035,12 @@ _role: canImportFollowing: "允许导入关注列表" canImportMuting: "允许导入隐藏列表" canImportUserLists: "允许导入用户列表" - chatAvailability: "允许聊天" + chatAvailability: "允许私信" uploadableFileTypes: "可上传的文件类型" uploadableFileTypes_caption: "指定 MIME 类型。可用换行指定多个类型,也可以用星号(*)作为通配符。(如 image/*)" uploadableFileTypes_caption2: "文件根据文件的不同,可能无法判断其类型。若要允许此类文件,请在指定中添加 {x}。" noteDraftLimit: "可在服务器上创建多少草稿" + scheduledNoteLimit: "可同时创建的定时帖子数量" watermarkAvailable: "能否使用水印功能" _condition: roleAssignedTo: "已分配给手动角色" @@ -2083,9 +2106,9 @@ _forgotPassword: ifNoEmail: "如果您没有设置电子邮件地址,请联系管理员。" contactAdmin: "该服务器不支持发送电子邮件。如果您想重设密码,请联系管理员。" _gallery: - my: "我的图库" - liked: "喜欢的图片" - like: "喜欢" + my: "我的相册" + liked: "喜欢的相册" + like: "喜欢!" unlike: "取消喜欢" _email: _follow: @@ -2151,14 +2174,14 @@ _channel: edit: "编辑频道" setBanner: "设置横幅" removeBanner: "删除横幅" - featured: "热点" - owned: "管理中" + featured: "热门" + owned: "正在管理" following: "正在关注" - usersCount: "有 {n} 人参与" - notesCount: "有 {n} 个帖子" + usersCount: "有{n}人参与" + notesCount: "有{n}个帖子" nameAndDescription: "名称与描述" nameOnly: "仅名称" - allowRenoteToExternal: "允许在频道外转帖及引用" + allowRenoteToExternal: "允许转发到频道外和引用" _menuDisplay: sideFull: "横向" sideIcon: "横向(图标)" @@ -2169,10 +2192,10 @@ _wordMute: muteWordsDescription: "AND 条件用空格分隔,OR 条件用换行符分隔。" muteWordsDescription2: "正则表达式用斜线包裹" _instanceMute: - instanceMuteDescription: "隐藏服务器中所有的帖子和转帖,包括这些服务器上用户的回复。" - instanceMuteDescription2: "一行一个" + instanceMuteDescription: "屏蔽服务器中所有的帖子和转帖,包括该服务器内用户的回复。" + instanceMuteDescription2: "通过换行符分隔进行设置" title: "下面实例中的帖子将被隐藏。" - heading: "已隐藏的服务器" + heading: "已屏蔽的服务器" _theme: explore: "寻找主题" install: "安装主题" @@ -2245,7 +2268,7 @@ _sfx: noteMy: "我的帖子" notification: "通知" reaction: "选择回应时" - chatMessage: "聊天信息" + chatMessage: "私信" _soundSettings: driveFile: "使用网盘内的音频" driveFileWarn: "选择网盘上的文件" @@ -2256,28 +2279,28 @@ _soundSettings: driveFileError: "无法读取声音。请更改设置。" _ago: future: "未来" - justNow: "最近" - secondsAgo: "{n} 秒前" - minutesAgo: "{n} 分前" - hoursAgo: "{n} 小时前" - daysAgo: "{n} 日前" - weeksAgo: "{n} 周前" - monthsAgo: "{n} 月前" - yearsAgo: "{n} 年前" + justNow: "刚刚" + secondsAgo: "{n}秒前" + minutesAgo: "{n}分钟前" + hoursAgo: "{n}小时前" + daysAgo: "{n}天前" + weeksAgo: "{n}周前" + monthsAgo: "{n}个月前" + yearsAgo: "{n}年前" invalid: "没有" _timeIn: seconds: "{n}秒后" - minutes: "{n} 分后" - hours: "{n} 小时后" + minutes: "{n}分钟后" + hours: "{n}小时后" days: "{n}天后" - weeks: "{n} 周后" - months: "{n} 月后" - years: "{n} 年后" + weeks: "{n}周后" + months: "{n}个月后" + years: "{n}年后" _time: second: "秒" - minute: "分" + minute: "分钟" hour: "小时" - day: "日" + day: "天" month: "个月" _2fa: alreadyRegistered: "此设备已被注册" @@ -2311,36 +2334,36 @@ _2fa: _permissions: "read:account": "查看账户信息" "write:account": "更改帐户信息" - "read:blocks": "查看屏蔽列表" - "write:blocks": "编辑屏蔽列表" + "read:blocks": "查看黑名单" + "write:blocks": "编辑黑名单" "read:drive": "查看网盘" "write:drive": "管理网盘文件" "read:favorites": "查看收藏夹" "write:favorites": "编辑收藏夹" "read:following": "查看关注信息" "write:following": "关注/取消关注" - "read:messaging": "查看消息" + "read:messaging": "查看私信" "write:messaging": "撰写或删除消息" - "read:mutes": "查看隐藏列表" - "write:mutes": "编辑隐藏列表" + "read:mutes": "查看屏蔽列表" + "write:mutes": "编辑屏蔽列表" "write:notes": "撰写或删除帖子" "read:notifications": "查看通知" "write:notifications": "管理通知" "read:reactions": "查看回应" - "write:reactions": "回应操作" + "write:reactions": "编辑回应" "write:votes": "投票" "read:pages": "查看页面" - "write:pages": "操作页面" + "write:pages": "编辑页面" "read:page-likes": "查看喜欢的页面" - "write:page-likes": "操作喜欢的页面" + "write:page-likes": "管理喜欢的页面" "read:user-groups": "查看用户组" - "write:user-groups": "操作用户组" + "write:user-groups": "编辑用户组" "read:channels": "查看频道" "write:channels": "管理频道" - "read:gallery": "浏览图库" - "write:gallery": "操作图库" - "read:gallery-likes": "读取喜欢的图片" - "write:gallery-likes": "操作喜欢的图片" + "read:gallery": "浏览相册" + "write:gallery": "编辑相册" + "read:gallery-likes": "浏览喜欢的相册" + "write:gallery-likes": "管理喜欢的相册" "read:flash": "查看 Play" "write:flash": "编辑 Play" "read:flash-likes": "查看 Play 的点赞" @@ -2368,33 +2391,33 @@ _permissions: "read:admin:roles": "查看角色" "write:admin:relays": "编辑中继" "read:admin:relays": "查看中继" - "write:admin:invite-codes": "编辑邀请码" + "write:admin:invite-codes": "管理邀请码" "read:admin:invite-codes": "查看邀请码" - "write:admin:announcements": "编辑公告" + "write:admin:announcements": "管理公告" "read:admin:announcements": "查看公告" "write:admin:avatar-decorations": "编辑头像挂件" "read:admin:avatar-decorations": "查看头像挂件" "write:admin:federation": "编辑联合相关信息" "write:admin:account": "编辑用户账户" "read:admin:account": "查看用户相关情报" - "write:admin:emoji": "编辑表情文字" - "read:admin:emoji": "查看表情文字" + "write:admin:emoji": "编辑表情符号" + "read:admin:emoji": "查看表情符号" "write:admin:queue": "编辑作业队列" "read:admin:queue": "查看作业队列相关情报" "write:admin:promo": "运营推广说明" - "write:admin:drive": "编辑用户网盘" + "write:admin:drive": "管理用户网盘" "read:admin:drive": "查看用户网盘相关情报" "read:admin:stream": "使用管理员用的 Websocket API" - "write:admin:ad": "编辑广告" + "write:admin:ad": "管理广告" "read:admin:ad": "查看广告" "write:invite-codes": "生成邀请码" "read:invite-codes": "获取已发行的邀请码" - "write:clip-favorite": "编辑便签的点赞" + "write:clip-favorite": "管理喜欢的便签" "read:clip-favorite": "查看便签的点赞" "read:federation": "查看联合相关信息" "write:report-abuse": "举报用户" "write:chat": "撰写或删除消息" - "read:chat": "查看聊天" + "read:chat": "查看私信" _auth: shareAccessTitle: "应用程序授权许可" shareAccess: "您要授权允许 “{name}” 访问您的帐户吗?" @@ -2413,7 +2436,7 @@ _antennaSources: homeTimeline: "已关注用户的帖子" users: "来自指定用户的帖子" userList: "来自指定列表中的帖子" - userBlacklist: "除掉已选择用户后所有的帖子" + userBlacklist: "过滤指定用户后的所有帖子" _weekday: sunday: "星期日" monday: "星期一" @@ -2453,7 +2476,7 @@ _widgets: chooseList: "选择列表" clicker: "点击器" birthdayFollowings: "今天是他们的生日" - chat: "聊天" + chat: "私信" _cw: hide: "隐藏" show: "查看更多" @@ -2461,26 +2484,26 @@ _cw: files: "{count} 个文件" _poll: noOnlyOneChoice: "需要至少两个选项" - choiceN: "选择 {n}" + choiceN: "选项{n}" noMore: "无法再添加更多了" - canMultipleVote: "允许多个投票" + canMultipleVote: "允许选择多个选项" expiration: "截止时间" infinite: "永久" at: "指定日期" after: "指定时间" deadlineDate: "截止日期" - deadlineTime: "小时" - duration: "时长" - votesCount: "{n} 票" + deadlineTime: "时间" + duration: "期限" + votesCount: "{n}票" totalVotes: "总票数 {n}" vote: "投票" showResult: "显示结果" voted: "已投票" closed: "已截止" - remainingDays: "{d} 天 {h} 小时后截止" + remainingDays: "{d}天{h}小时后截止" remainingHours: "{h} 小时 {m} 分后截止" - remainingMinutes: "{m} 分 {s} 秒后截止" - remainingSeconds: "{s} 秒后截止" + remainingMinutes: "{m}分{s}秒后截止" + remainingSeconds: "{s}秒后截止" _visibility: public: "公开" publicDescription: "您的帖子将出现在全局时间线上" @@ -2499,9 +2522,9 @@ _postForm: quotePlaceholder: "引用这个帖子..." channelPlaceholder: "发布到频道…" _placeholders: - a: "现在如何?" - b: "发生了什么?" - c: "你有什么想法?" + a: "现在怎么样?" + b: "想好发些什么了吗?" + c: "在想些什么呢?" d: "你想要发布些什么吗?" e: "请写下来吧" f: "等待您的发布..." @@ -2527,8 +2550,8 @@ _exportOrImport: favoritedNotes: "收藏的帖子" clips: "便签" followingList: "关注中" - muteList: "隐藏" - blockingList: "屏蔽" + muteList: "屏蔽" + blockingList: "拉黑" userLists: "列表" excludeMutingUsers: "排除屏蔽用户" excludeInactiveUsers: "排除不活跃用户" @@ -2574,7 +2597,7 @@ _play: editThisPage: "编辑此 Play" viewSource: "查看源代码" my: "我的 Play" - liked: "点赞的 Play" + liked: "喜欢的 Play" featured: "热门" title: "标题" script: "脚本" @@ -2591,7 +2614,7 @@ _pages: editThisPage: "编辑此页面" viewSource: "查看源代码" viewPage: "查看页面" - like: "赞" + like: "喜欢" unlike: "取消喜欢" my: "我的页面" liked: "喜欢的页面" @@ -2639,10 +2662,12 @@ _notification: youGotReply: "来自{name}的回复" youGotQuote: "来自{name}的引用" youRenoted: "来自{name}的转发" - youWereFollowed: "关注了你。" + youWereFollowed: "关注了你" youReceivedFollowRequest: "您有新的关注请求" yourFollowRequestAccepted: "您的关注请求已通过" pollEnded: "问卷调查结果已生成。" + scheduledNotePosted: "定时帖子已发布" + scheduledNotePostFailed: "定时帖子发布失败" newNote: "新的帖子" unreadAntennaNote: "天线 {name}" roleAssigned: "授予的角色" @@ -2722,7 +2747,7 @@ _deck: mentions: "提及" direct: "指定用户" roleTimeline: "角色时间线" - chat: "聊天" + chat: "私信" _dialog: charactersExceeded: "已经超过了最大字符数! 当前字符数 {current} / 限制字符数 {max}" charactersBelow: "低于最小字符数!当前字符数 {current} / 限制字符数 {min}" @@ -2818,7 +2843,7 @@ _moderationLogTypes: deleteAccount: "删除了账户" deletePage: "删除了页面" deleteFlash: "删除了 Play" - deleteGalleryPost: "删除了图库稿件" + deleteGalleryPost: "删除相册内容" deleteChatRoom: "删除聊天室" updateProxyAccountDescription: "更新代理账户的简介" _fileViewer: @@ -3074,7 +3099,7 @@ _bootErrors: serverError: "请稍等片刻再重试。若问题仍无法解决,请将以下 Error ID 一起发送给管理员。" solution: "以下方法或许可以解决问题:" solution1: "将浏览器及操作系统更新到最新版本" - solution2: "禁用广告屏蔽插件" + solution2: "禁用广告拦截插件" solution3: "清除浏览器缓存" solution4: "(Tor Browser)将 dom.webaudio.enabled 设定为 true" otherOption: "其它选项" @@ -3168,7 +3193,9 @@ _watermarkEditor: opacity: "不透明度" scale: "大小" text: "文本" + qr: "二维码" position: "位置" + margin: "边距" type: "类型" image: "图片" advanced: "高级" @@ -3183,6 +3210,7 @@ _watermarkEditor: polkadotSubDotOpacity: "副波点的不透明度" polkadotSubDotRadius: "副波点的大小" polkadotSubDotDivisions: "副波点的数量" + leaveBlankToAccountUrl: "留空则为账户 URL" _imageEffector: title: "效果" addEffect: "添加效果" @@ -3194,6 +3222,8 @@ _imageEffector: mirror: "镜像" invert: "反转颜色" grayscale: "黑白" + blur: "模糊" + pixelate: "马赛克" colorAdjust: "色彩校正" colorClamp: "颜色限制" colorClampAdvanced: "颜色限制(高级)" @@ -3205,10 +3235,14 @@ _imageEffector: checker: "检查" blockNoise: "块状噪点" tearing: "撕裂" + fill: "填充" _fxProps: angle: "角度" scale: "大小" size: "大小" + radius: "半径" + samples: "采样数" + offset: "位置" color: "颜色" opacity: "不透明度" normalize: "标准化" @@ -3237,6 +3271,7 @@ _imageEffector: zoomLinesThreshold: "集中线宽度" zoomLinesMaskSize: "中心直径" zoomLinesBlack: "变成黑色" + circle: "圆形" drafts: "草稿" _drafts: select: "选择草稿" @@ -3252,3 +3287,22 @@ _drafts: restoreFromDraft: "从草稿恢复" restore: "恢复" listDrafts: "草稿一览" + schedule: "定时发布" + listScheduledNotes: "定时发布列表" + cancelSchedule: "取消定时" +qr: "二维码" +_qr: + showTabTitle: "显示" + readTabTitle: "读取" + shareTitle: "{name} {acct}" + shareText: "请在 Fediverse 上关注我!" + chooseCamera: "选择相机" + cannotToggleFlash: "无法开关闪光灯" + turnOnFlash: "打开闪光灯" + turnOffFlash: "关闭闪光灯" + startQr: "重新打开二维码扫描器" + stopQr: "关闭二维码扫描器" + noQrCodeFound: "未找到二维码" + scanFile: "扫描设备上的图像" + raw: "文本" + mfm: "MFM" diff --git a/locales/zh-TW.yml b/locales/zh-TW.yml index 65b7f9bfba..6f67be9741 100644 --- a/locales/zh-TW.yml +++ b/locales/zh-TW.yml @@ -75,7 +75,7 @@ receiveFollowRequest: "您有新的追隨請求" followRequestAccepted: "追隨請求已被接受" mention: "提及" mentions: "提及" -directNotes: "私訊" +directNotes: "指定使用者" importAndExport: "匯入與匯出" import: "匯入" export: "匯出" @@ -253,6 +253,7 @@ noteDeleteConfirm: "確定刪除此貼文嗎?" pinLimitExceeded: "不能置頂更多貼文了" done: "完成" processing: "處理中" +preprocessing: "準備中" preview: "預覽" default: "預設" defaultValueIs: "預設值:{value}" @@ -1316,6 +1317,7 @@ acknowledgeNotesAndEnable: "了解注意事項後再開啟。" federationSpecified: "此伺服器以白名單聯邦的方式運作。除了管理員指定的伺服器外,它無法與其他伺服器互動。" federationDisabled: "此伺服器未開啟站台聯邦。無法與其他伺服器上的使用者互動。" draft: "草稿\n" +draftsAndScheduledNotes: "草稿與排定發布" confirmOnReact: "在做出反應前先確認" reactAreYouSure: "用「 {emoji} 」反應嗎?" markAsSensitiveConfirm: "要將這個媒體設定為敏感嗎?" @@ -1343,6 +1345,8 @@ postForm: "發文視窗" textCount: "字數" information: "關於" chat: "聊天" +directMessage: "直接訊息" +directMessage_short: "訊息" migrateOldSettings: "遷移舊設定資訊" migrateOldSettings_description: "通常情況下,這會自動進行,但若因某些原因未能順利遷移,您可以手動觸發遷移處理。請注意,當前的設定資訊將會被覆寫。" compress: "壓縮" @@ -1370,6 +1374,8 @@ redisplayAllTips: "重新顯示所有「提示與技巧」" hideAllTips: "隱藏所有「提示與技巧」" defaultImageCompressionLevel: "預設的影像壓縮程度" defaultImageCompressionLevel_description: "低的話可以保留畫質,但是會增加檔案的大小。
高的話可以減少檔案大小,但是會降低畫質。" +defaultCompressionLevel: "預設的壓縮程度" +defaultCompressionLevel_description: "低的話可以保留品質,但是會增加檔案的大小。
高的話可以減少檔案大小,但是會降低品質。" inMinutes: "分鐘" inDays: "日" safeModeEnabled: "啟用安全模式" @@ -1377,10 +1383,26 @@ pluginsAreDisabledBecauseSafeMode: "由於啟用安全模式,所有的外掛 customCssIsDisabledBecauseSafeMode: "由於啟用安全模式,所有的客製 CSS 都被停用。" themeIsDefaultBecauseSafeMode: "在安全模式啟用期間將使用預設主題。關閉安全模式後會恢復原本的設定。" thankYouForTestingBeta: "感謝您協助驗證 beta 版!" +createUserSpecifiedNote: "建立使用者指定的筆記" +schedulePost: "排定發布" +scheduleToPostOnX: "排定在 {x} 發布" +scheduledToPostOnX: "已排定在 {x} 發布貼文" +schedule: "排定" +scheduled: "排定" +_compression: + _quality: + high: "高品質" + medium: "中品質" + low: "低品質" + _size: + large: "大" + medium: "中" + small: "小" _order: newest: "最新的在前" oldest: "最舊的在前" _chat: + messages: "訊息" noMessagesYet: "尚無訊息" newMessage: "新訊息" individualChat: "ㄧ對一聊天室" @@ -2018,6 +2040,7 @@ _role: uploadableFileTypes_caption: "請指定 MIME 類型。可以用換行區隔多個類型,也可以使用星號(*)作為萬用字元進行指定。(例如:image/*)\n" uploadableFileTypes_caption2: "有些檔案可能無法判斷其類型。若要允許這類檔案,請在指定中加入 {x}。" noteDraftLimit: "伺服器端可建立的貼文草稿數量上限\n" + scheduledNoteLimit: "同時建立的排定發布數量" watermarkAvailable: "浮水印功能是否可用" _condition: roleAssignedTo: "手動指派角色完成" @@ -2238,7 +2261,7 @@ _theme: buttonHoverBg: "按鈕背景 (漂浮)" inputBorder: "輸入框邊框" badge: "徽章" - messageBg: "私訊背景" + messageBg: "聊天的背景" fgHighlighted: "突顯文字" _sfx: note: "貼文" @@ -2643,6 +2666,8 @@ _notification: youReceivedFollowRequest: "您有新的追隨請求" yourFollowRequestAccepted: "您的追隨請求已被核准" pollEnded: "問卷調查已產生結果" + scheduledNotePosted: "已排定發布貼文" + scheduledNotePostFailed: "排定發布貼文失敗了" newNote: "新的貼文" unreadAntennaNote: "天線 {name}" roleAssigned: "已授予角色" @@ -3168,7 +3193,9 @@ _watermarkEditor: opacity: "透明度" scale: "大小" text: "文字" + qr: "二維條碼" position: "位置" + margin: "邊界" type: "類型" image: "圖片" advanced: "進階" @@ -3183,6 +3210,7 @@ _watermarkEditor: polkadotSubDotOpacity: "子圓點的不透明度" polkadotSubDotRadius: "子圓點的尺寸" polkadotSubDotDivisions: "子圓點的數量" + leaveBlankToAccountUrl: "若留空則使用帳戶的 URL" _imageEffector: title: "特效" addEffect: "新增特效" @@ -3194,6 +3222,8 @@ _imageEffector: mirror: "鏡像" invert: "反轉色彩" grayscale: "黑白" + blur: "模糊" + pixelate: "馬賽克" colorAdjust: "色彩校正" colorClamp: "壓縮色彩" colorClampAdvanced: "壓縮色彩(進階)" @@ -3205,10 +3235,14 @@ _imageEffector: checker: "棋盤格" blockNoise: "阻擋雜訊" tearing: "撕裂" + fill: "填充" _fxProps: angle: "角度" scale: "大小" size: "大小" + radius: "半徑" + samples: "取樣數" + offset: "位置" color: "顏色" opacity: "透明度" normalize: "正規化" @@ -3237,6 +3271,7 @@ _imageEffector: zoomLinesThreshold: "集中線的寬度" zoomLinesMaskSize: "中心直徑" zoomLinesBlack: "變成黑色" + circle: "圓形" drafts: "草稿\n" _drafts: select: "選擇草槁" @@ -3252,3 +3287,22 @@ _drafts: restoreFromDraft: "從草稿復原\n" restore: "還原" listDrafts: "草稿清單" + schedule: "排定發布" + listScheduledNotes: "排定發布列表" + cancelSchedule: "解除排定" +qr: "二維條碼" +_qr: + showTabTitle: "檢視" + readTabTitle: "讀取" + shareTitle: "{name} {acct}" + shareText: "請在聯邦宇宙追隨我吧!" + chooseCamera: "選擇相機" + cannotToggleFlash: "無法切換閃光燈" + turnOnFlash: "開啟閃光燈" + turnOffFlash: "關閉閃光燈" + startQr: "啟動條碼掃描器" + stopQr: "停止條碼掃描器" + noQrCodeFound: "找不到 QR code" + scanFile: "掃描在裝置上的影像" + raw: "文字" + mfm: "MFM" diff --git a/package.json b/package.json index 7f19734453..772b822dd3 100644 --- a/package.json +++ b/package.json @@ -1,12 +1,12 @@ { "name": "misskey", - "version": "2025.9.0", + "version": "2025.10.0-beta.0", "codename": "nasubi", "repository": { "type": "git", "url": "https://github.com/misskey-dev/misskey.git" }, - "packageManager": "pnpm@10.15.1", + "packageManager": "pnpm@10.17.1", "workspaces": [ "packages/frontend-shared", "packages/frontend", @@ -54,30 +54,30 @@ }, "dependencies": { "cssnano": "7.1.1", - "esbuild": "0.25.9", + "esbuild": "0.25.10", "execa": "9.6.0", "fast-glob": "3.3.3", "glob": "11.0.3", "ignore-walk": "7.0.0", "js-yaml": "4.1.0", "postcss": "8.5.6", - "tar": "7.4.3", + "tar": "7.5.1", "terser": "5.44.0", "typescript": "5.9.2" }, "devDependencies": { "@misskey-dev/eslint-plugin": "2.1.0", "@types/js-yaml": "4.0.9", - "@types/node": "22.18.1", - "@typescript-eslint/eslint-plugin": "8.42.0", - "@typescript-eslint/parser": "8.42.0", + "@types/node": "22.18.6", + "@typescript-eslint/eslint-plugin": "8.44.1", + "@typescript-eslint/parser": "8.44.1", "cross-env": "7.0.3", "cypress": "14.5.4", - "eslint": "9.35.0", - "globals": "16.3.0", + "eslint": "9.36.0", + "globals": "16.4.0", "ncp": "2.0.0", - "pnpm": "10.15.1", - "start-server-and-test": "2.1.0" + "pnpm": "10.17.1", + "start-server-and-test": "2.1.2" }, "optionalDependencies": { "@tensorflow/tfjs-core": "4.22.0" diff --git a/packages/backend/migration/1757823175259-sensitive-ad.js b/packages/backend/migration/1757823175259-sensitive-ad.js new file mode 100644 index 0000000000..46f0f270ab --- /dev/null +++ b/packages/backend/migration/1757823175259-sensitive-ad.js @@ -0,0 +1,16 @@ +/* + * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-License-Identifier: AGPL-3.0-only + */ + +export class SensitiveAd1757823175259 { + name = 'SensitiveAd1757823175259' + + async up(queryRunner) { + await queryRunner.query(`ALTER TABLE "ad" ADD "isSensitive" boolean NOT NULL DEFAULT false`); + } + + async down(queryRunner) { + await queryRunner.query(`ALTER TABLE "ad" DROP COLUMN "isSensitive"`); + } +} diff --git a/packages/backend/migration/1758677617888-scheduled-post.js b/packages/backend/migration/1758677617888-scheduled-post.js new file mode 100644 index 0000000000..b31313d9db --- /dev/null +++ b/packages/backend/migration/1758677617888-scheduled-post.js @@ -0,0 +1,24 @@ +/* + * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-License-Identifier: AGPL-3.0-only + */ + +export class ScheduledPost1758677617888 { + name = 'ScheduledPost1758677617888' + + /** + * @param {QueryRunner} queryRunner + */ + async up(queryRunner) { + await queryRunner.query(`ALTER TABLE "note_draft" ADD "scheduledAt" TIMESTAMP WITH TIME ZONE`); + await queryRunner.query(`ALTER TABLE "note_draft" ADD "isActuallyScheduled" boolean NOT NULL DEFAULT false`); + } + + /** + * @param {QueryRunner} queryRunner + */ + async down(queryRunner) { + await queryRunner.query(`ALTER TABLE "note_draft" DROP COLUMN "isActuallyScheduled"`); + await queryRunner.query(`ALTER TABLE "note_draft" DROP COLUMN "scheduledAt"`); + } +} diff --git a/packages/backend/package.json b/packages/backend/package.json index 5114912769..07a80abc0f 100644 --- a/packages/backend/package.json +++ b/packages/backend/package.json @@ -39,17 +39,17 @@ }, "optionalDependencies": { "@swc/core-android-arm64": "1.3.11", - "@swc/core-darwin-arm64": "1.13.5", - "@swc/core-darwin-x64": "1.13.5", + "@swc/core-darwin-arm64": "1.13.19", + "@swc/core-darwin-x64": "1.13.19", "@swc/core-freebsd-x64": "1.3.11", - "@swc/core-linux-arm-gnueabihf": "1.13.5", - "@swc/core-linux-arm64-gnu": "1.13.5", - "@swc/core-linux-arm64-musl": "1.13.5", - "@swc/core-linux-x64-gnu": "1.13.5", - "@swc/core-linux-x64-musl": "1.13.5", - "@swc/core-win32-arm64-msvc": "1.13.5", - "@swc/core-win32-ia32-msvc": "1.13.5", - "@swc/core-win32-x64-msvc": "1.13.5", + "@swc/core-linux-arm-gnueabihf": "1.13.19", + "@swc/core-linux-arm64-gnu": "1.13.19", + "@swc/core-linux-arm64-musl": "1.13.19", + "@swc/core-linux-x64-gnu": "1.13.19", + "@swc/core-linux-x64-musl": "1.13.19", + "@swc/core-win32-arm64-msvc": "1.13.19", + "@swc/core-win32-ia32-msvc": "1.13.19", + "@swc/core-win32-x64-msvc": "1.13.19", "@tensorflow/tfjs": "4.22.0", "@tensorflow/tfjs-node": "4.22.0", "bufferutil": "4.0.9", @@ -69,8 +69,8 @@ "utf-8-validate": "6.0.5" }, "dependencies": { - "@aws-sdk/client-s3": "3.883.0", - "@aws-sdk/lib-storage": "3.883.0", + "@aws-sdk/client-s3": "3.896.0", + "@aws-sdk/lib-storage": "3.895.0", "@discordapp/twemoji": "16.0.1", "@fastify/accepts": "5.0.2", "@fastify/cookie": "11.0.2", @@ -82,7 +82,7 @@ "@fastify/view": "10.0.2", "@misskey-dev/sharp-read-bmp": "1.2.0", "@misskey-dev/summaly": "5.2.3", - "@napi-rs/canvas": "0.1.79", + "@napi-rs/canvas": "0.1.80", "@nestjs/common": "11.1.6", "@nestjs/core": "11.1.6", "@nestjs/testing": "11.1.6", @@ -103,29 +103,29 @@ "bcryptjs": "2.4.3", "blurhash": "2.0.5", "body-parser": "1.20.3", - "bullmq": "5.58.5", + "bullmq": "5.58.8", "cacheable-lookup": "7.0.0", "cbor": "9.0.2", - "chalk": "5.6.0", - "chalk-template": "1.1.0", + "chalk": "5.6.2", + "chalk-template": "1.1.2", "chokidar": "4.0.3", "cli-highlight": "2.1.11", "color-convert": "2.0.1", "content-disposition": "0.5.4", "date-fns": "2.30.0", "deep-email-validator": "0.1.21", - "fastify": "5.6.0", + "fastify": "5.6.1", "fastify-raw-body": "5.0.0", "feed": "4.2.2", "file-type": "19.6.0", "fluent-ffmpeg": "2.1.3", "form-data": "4.0.4", - "got": "14.4.8", + "got": "14.4.9", "happy-dom": "16.8.1", "hpagent": "1.2.0", "htmlescape": "1.1.1", "http-link-header": "1.1.3", - "ioredis": "5.7.0", + "ioredis": "5.8.0", "ip-cidr": "4.0.2", "ipaddr.js": "2.2.0", "is-svg": "5.1.0", @@ -135,14 +135,14 @@ "jsonld": "8.3.3", "jsrsasign": "11.1.0", "juice": "11.0.1", - "meilisearch": "0.52.0", + "meilisearch": "0.53.0", "mfm-js": "0.25.0", "microformats-parser": "2.0.4", "mime-types": "2.1.35", "misskey-js": "workspace:*", "misskey-reversi": "workspace:*", "ms": "3.0.0-canary.202508261828", - "nanoid": "5.1.5", + "nanoid": "5.1.6", "nested-property": "4.0.0", "node-fetch": "3.3.2", "nodemailer": "6.10.1", @@ -175,12 +175,12 @@ "slacc": "0.0.10", "strict-event-emitter-types": "2.0.0", "stringz": "2.1.0", - "systeminformation": "5.27.8", + "systeminformation": "5.27.10", "tinycolor2": "1.6.0", "tmp": "0.2.5", "tsc-alias": "1.8.16", "tsconfig-paths": "4.2.0", - "typeorm": "0.3.26", + "typeorm": "0.3.27", "typescript": "5.9.2", "ulid": "2.4.0", "vary": "1.1.2", @@ -210,7 +210,7 @@ "@types/jsrsasign": "10.5.15", "@types/mime-types": "2.1.4", "@types/ms": "0.7.34", - "@types/node": "22.18.1", + "@types/node": "22.18.6", "@types/nodemailer": "6.4.19", "@types/oauth": "0.9.6", "@types/oauth2orize": "1.11.5", @@ -231,8 +231,8 @@ "@types/vary": "1.1.3", "@types/web-push": "3.6.4", "@types/ws": "8.18.1", - "@typescript-eslint/eslint-plugin": "8.42.0", - "@typescript-eslint/parser": "8.42.0", + "@typescript-eslint/eslint-plugin": "8.44.1", + "@typescript-eslint/parser": "8.44.1", "aws-sdk-client-mock": "4.1.0", "cross-env": "7.0.3", "eslint-plugin-import": "2.32.0", diff --git a/packages/backend/src/config.ts b/packages/backend/src/config.ts index f71f1d7e34..fdf6fe18e2 100644 --- a/packages/backend/src/config.ts +++ b/packages/backend/src/config.ts @@ -7,6 +7,7 @@ import * as fs from 'node:fs'; import { fileURLToPath } from 'node:url'; import { dirname, resolve } from 'node:path'; import * as yaml from 'js-yaml'; +import { type FastifyServerOptions } from 'fastify'; import type * as Sentry from '@sentry/node'; import type * as SentryVue from '@sentry/vue'; import type { RedisOptions } from 'ioredis'; @@ -27,6 +28,7 @@ type Source = { url?: string; port?: number; socket?: string; + trustProxy?: FastifyServerOptions['trustProxy']; chmodSocket?: string; disableHsts?: boolean; db: { @@ -118,6 +120,7 @@ export type Config = { url: string; port: number; socket: string | undefined; + trustProxy: FastifyServerOptions['trustProxy']; chmodSocket: string | undefined; disableHsts: boolean | undefined; db: { @@ -266,6 +269,7 @@ export function loadConfig(): Config { url: url.origin, port: config.port ?? parseInt(process.env.PORT ?? '', 10), socket: config.socket, + trustProxy: config.trustProxy, chmodSocket: config.chmodSocket, disableHsts: config.disableHsts, host, diff --git a/packages/backend/src/core/HttpRequestService.ts b/packages/backend/src/core/HttpRequestService.ts index f7973cbb66..5714bde8bf 100644 --- a/packages/backend/src/core/HttpRequestService.ts +++ b/packages/backend/src/core/HttpRequestService.ts @@ -37,17 +37,23 @@ class HttpRequestServiceAgent extends http.Agent { @bindThis public createConnection(options: http.ClientRequestArgs, callback?: (err: Error | null, stream: stream.Duplex) => void): stream.Duplex { - const socket = super.createConnection(options, callback) - .on('connect', () => { - if (socket instanceof net.Socket && process.env.NODE_ENV === 'production') { - const address = socket.remoteAddress; - if (address && ipaddr.isValid(address)) { - if (this.isPrivateIp(address)) { - socket.destroy(new Error(`Blocked address: ${address}`)); - } + const socket = super.createConnection(options, callback); + + if (socket == null) { + throw new Error('Failed to create socket'); + } + + socket.on('connect', () => { + if (socket instanceof net.Socket && process.env.NODE_ENV === 'production') { + const address = socket.remoteAddress; + if (address && ipaddr.isValid(address)) { + if (this.isPrivateIp(address)) { + socket.destroy(new Error(`Blocked address: ${address}`)); } } - }); + } + }); + return socket; } @@ -76,17 +82,23 @@ class HttpsRequestServiceAgent extends https.Agent { @bindThis public createConnection(options: http.ClientRequestArgs, callback?: (err: Error | null, stream: stream.Duplex) => void): stream.Duplex { - const socket = super.createConnection(options, callback) - .on('connect', () => { - if (socket instanceof net.Socket && process.env.NODE_ENV === 'production') { - const address = socket.remoteAddress; - if (address && ipaddr.isValid(address)) { - if (this.isPrivateIp(address)) { - socket.destroy(new Error(`Blocked address: ${address}`)); - } + const socket = super.createConnection(options, callback); + + if (socket == null) { + throw new Error('Failed to create socket'); + } + + socket.on('connect', () => { + if (socket instanceof net.Socket && process.env.NODE_ENV === 'production') { + const address = socket.remoteAddress; + if (address && ipaddr.isValid(address)) { + if (this.isPrivateIp(address)) { + socket.destroy(new Error(`Blocked address: ${address}`)); } } - }); + } + }); + return socket; } diff --git a/packages/backend/src/core/NoteCreateService.ts b/packages/backend/src/core/NoteCreateService.ts index 1eefcfa054..b6acf4c5fb 100644 --- a/packages/backend/src/core/NoteCreateService.ts +++ b/packages/backend/src/core/NoteCreateService.ts @@ -13,7 +13,7 @@ import { extractCustomEmojisFromMfm } from '@/misc/extract-custom-emojis-from-mf import { extractHashtags } from '@/misc/extract-hashtags.js'; import type { IMentionedRemoteUsers } from '@/models/Note.js'; import { MiNote } from '@/models/Note.js'; -import type { ChannelFollowingsRepository, ChannelsRepository, FollowingsRepository, InstancesRepository, MiFollowing, MiMeta, MutingsRepository, NotesRepository, NoteThreadMutingsRepository, UserListMembershipsRepository, UserProfilesRepository, UsersRepository } from '@/models/_.js'; +import type { BlockingsRepository, ChannelFollowingsRepository, ChannelsRepository, DriveFilesRepository, FollowingsRepository, InstancesRepository, MiFollowing, MiMeta, MutingsRepository, NotesRepository, NoteThreadMutingsRepository, UserListMembershipsRepository, UserProfilesRepository, UsersRepository } from '@/models/_.js'; import type { MiDriveFile } from '@/models/DriveFile.js'; import type { MiApp } from '@/models/App.js'; import { concat } from '@/misc/prelude/array.js'; @@ -56,6 +56,7 @@ import { trackPromise } from '@/misc/promise-tracker.js'; import { IdentifiableError } from '@/misc/identifiable-error.js'; import { CollapsedQueue } from '@/misc/collapsed-queue.js'; import { CacheService } from '@/core/CacheService.js'; +import { isQuote, isRenote } from '@/misc/is-renote.js'; type NotificationType = 'reply' | 'renote' | 'quote' | 'mention'; @@ -192,6 +193,12 @@ export class NoteCreateService implements OnApplicationShutdown { @Inject(DI.channelFollowingsRepository) private channelFollowingsRepository: ChannelFollowingsRepository, + @Inject(DI.blockingsRepository) + private blockingsRepository: BlockingsRepository, + + @Inject(DI.driveFilesRepository) + private driveFilesRepository: DriveFilesRepository, + private userEntityService: UserEntityService, private noteEntityService: NoteEntityService, private idService: IdService, @@ -221,6 +228,167 @@ export class NoteCreateService implements OnApplicationShutdown { this.updateNotesCountQueue = new CollapsedQueue(process.env.NODE_ENV !== 'test' ? 60 * 1000 * 5 : 0, this.collapseNotesCount, this.performUpdateNotesCount); } + @bindThis + public async fetchAndCreate(user: { + id: MiUser['id']; + username: MiUser['username']; + host: MiUser['host']; + isBot: MiUser['isBot']; + isCat: MiUser['isCat']; + }, data: { + createdAt: Date; + replyId: MiNote['id'] | null; + renoteId: MiNote['id'] | null; + fileIds: MiDriveFile['id'][]; + text: string | null; + cw: string | null; + visibility: string; + visibleUserIds: MiUser['id'][]; + channelId: MiChannel['id'] | null; + localOnly: boolean; + reactionAcceptance: MiNote['reactionAcceptance']; + poll: IPoll | null; + apMentions?: MinimumUser[] | null; + apHashtags?: string[] | null; + apEmojis?: string[] | null; + }): Promise { + const visibleUsers = data.visibleUserIds.length > 0 ? await this.usersRepository.findBy({ + id: In(data.visibleUserIds), + }) : []; + + let files: MiDriveFile[] = []; + if (data.fileIds.length > 0) { + files = await this.driveFilesRepository.createQueryBuilder('file') + .where('file.userId = :userId AND file.id IN (:...fileIds)', { + userId: user.id, + fileIds: data.fileIds, + }) + .orderBy('array_position(ARRAY[:...fileIds], "id"::text)') + .setParameters({ fileIds: data.fileIds }) + .getMany(); + + if (files.length !== data.fileIds.length) { + throw new IdentifiableError('801c046c-5bf5-4234-ad2b-e78fc20a2ac7', 'No such file'); + } + } + + let renote: MiNote | null = null; + if (data.renoteId != null) { + // Fetch renote to note + renote = await this.notesRepository.findOne({ + where: { id: data.renoteId }, + relations: ['user', 'renote', 'reply'], + }); + + if (renote == null) { + throw new IdentifiableError('53983c56-e163-45a6-942f-4ddc485d4290', 'No such renote target'); + } else if (isRenote(renote) && !isQuote(renote)) { + throw new IdentifiableError('bde24c37-121f-4e7d-980d-cec52f599f02', 'Cannot renote pure renote'); + } + + // Check blocking + if (renote.userId !== user.id) { + const blockExist = await this.blockingsRepository.exists({ + where: { + blockerId: renote.userId, + blockeeId: user.id, + }, + }); + if (blockExist) { + throw new IdentifiableError('2b4fe776-4414-4a2d-ae39-f3418b8fd4d3', 'You have been blocked by the user'); + } + } + + if (renote.visibility === 'followers' && renote.userId !== user.id) { + // 他人のfollowers noteはreject + throw new IdentifiableError('90b9d6f0-893a-4fef-b0f1-e9a33989f71a', 'Renote target visibility'); + } else if (renote.visibility === 'specified') { + // specified / direct noteはreject + throw new IdentifiableError('48d7a997-da5c-4716-b3c3-92db3f37bf7d', 'Renote target visibility'); + } + + if (renote.channelId && renote.channelId !== data.channelId) { + // チャンネルのノートに対しリノート要求がきたとき、チャンネル外へのリノート可否をチェック + // リノートのユースケースのうち、チャンネル内→チャンネル外は少数だと考えられるため、JOINはせず必要な時に都度取得する + const renoteChannel = await this.channelsRepository.findOneBy({ id: renote.channelId }); + if (renoteChannel == null) { + // リノートしたいノートが書き込まれているチャンネルが無い + throw new IdentifiableError('b060f9a6-8909-4080-9e0b-94d9fa6f6a77', 'No such channel'); + } else if (!renoteChannel.allowRenoteToExternal) { + // リノート作成のリクエストだが、対象チャンネルがリノート禁止だった場合 + throw new IdentifiableError('7e435f4a-780d-4cfc-a15a-42519bd6fb67', 'Channel does not allow renote to external'); + } + } + } + + let reply: MiNote | null = null; + if (data.replyId != null) { + // Fetch reply + reply = await this.notesRepository.findOne({ + where: { id: data.replyId }, + relations: ['user'], + }); + + if (reply == null) { + throw new IdentifiableError('60142edb-1519-408e-926d-4f108d27bee0', 'No such reply target'); + } else if (isRenote(reply) && !isQuote(reply)) { + throw new IdentifiableError('f089e4e2-c0e7-4f60-8a23-e5a6bf786b36', 'Cannot reply to pure renote'); + } else if (!await this.noteEntityService.isVisibleForMe(reply, user.id)) { + throw new IdentifiableError('11cd37b3-a411-4f77-8633-c580ce6a8dce', 'No such reply target'); + } else if (reply.visibility === 'specified' && data.visibility !== 'specified') { + throw new IdentifiableError('ced780a1-2012-4caf-bc7e-a95a291294cb', 'Cannot reply to specified note with different visibility'); + } + + // Check blocking + if (reply.userId !== user.id) { + const blockExist = await this.blockingsRepository.exists({ + where: { + blockerId: reply.userId, + blockeeId: user.id, + }, + }); + if (blockExist) { + throw new IdentifiableError('b0df6025-f2e8-44b4-a26a-17ad99104612', 'You have been blocked by the user'); + } + } + } + + if (data.poll) { + if (data.poll.expiresAt != null) { + if (data.poll.expiresAt.getTime() < Date.now()) { + throw new IdentifiableError('0c11c11e-0c8d-48e7-822c-76ccef660068', 'Poll expiration must be future time'); + } + } + } + + let channel: MiChannel | null = null; + if (data.channelId != null) { + channel = await this.channelsRepository.findOneBy({ id: data.channelId, isArchived: false }); + + if (channel == null) { + throw new IdentifiableError('bfa3905b-25f5-4894-b430-da331a490e4b', 'No such channel'); + } + } + + return this.create(user, { + createdAt: data.createdAt, + files: files, + poll: data.poll, + text: data.text, + reply, + renote, + cw: data.cw, + localOnly: data.localOnly, + reactionAcceptance: data.reactionAcceptance, + visibility: data.visibility, + visibleUsers, + channel, + apMentions: data.apMentions, + apHashtags: data.apHashtags, + apEmojis: data.apEmojis, + }); + } + @bindThis public async create(user: { id: MiUser['id']; diff --git a/packages/backend/src/core/NoteDraftService.ts b/packages/backend/src/core/NoteDraftService.ts index c43be96efa..7666407c1e 100644 --- a/packages/backend/src/core/NoteDraftService.ts +++ b/packages/backend/src/core/NoteDraftService.ts @@ -5,32 +5,18 @@ import { Inject, Injectable } from '@nestjs/common'; import { In } from 'typeorm'; -import type { noteVisibilities, noteReactionAcceptances } from '@/types.js'; import { DI } from '@/di-symbols.js'; import type { MiNoteDraft, NoteDraftsRepository, MiNote, MiDriveFile, MiChannel, UsersRepository, DriveFilesRepository, NotesRepository, BlockingsRepository, ChannelsRepository } from '@/models/_.js'; import { bindThis } from '@/decorators.js'; import { RoleService } from '@/core/RoleService.js'; import { IdService } from '@/core/IdService.js'; import type { MiLocalUser, MiUser } from '@/models/User.js'; -import { IPoll } from '@/models/Poll.js'; import { IdentifiableError } from '@/misc/identifiable-error.js'; import { isRenote, isQuote } from '@/misc/is-renote.js'; import { NoteEntityService } from '@/core/entities/NoteEntityService.js'; +import { QueueService } from '@/core/QueueService.js'; -export type NoteDraftOptions = { - replyId?: MiNote['id'] | null; - renoteId?: MiNote['id'] | null; - text?: string | null; - cw?: string | null; - localOnly?: boolean | null; - reactionAcceptance?: typeof noteReactionAcceptances[number]; - visibility?: typeof noteVisibilities[number]; - fileIds?: MiDriveFile['id'][]; - visibleUserIds?: MiUser['id'][]; - hashtag?: string; - channelId?: MiChannel['id'] | null; - poll?: (IPoll & { expiredAfter?: number | null }) | null; -}; +export type NoteDraftOptions = Omit; @Injectable() export class NoteDraftService { @@ -56,6 +42,7 @@ export class NoteDraftService { private roleService: RoleService, private idService: IdService, private noteEntityService: NoteEntityService, + private queueService: QueueService, ) { } @@ -72,36 +59,43 @@ export class NoteDraftService { @bindThis public async create(me: MiLocalUser, data: NoteDraftOptions): Promise { //#region check draft limit + const policies = await this.roleService.getUserPolicies(me.id); const currentCount = await this.noteDraftsRepository.countBy({ userId: me.id, }); - if (currentCount >= (await this.roleService.getUserPolicies(me.id)).noteDraftLimit) { + if (currentCount >= policies.noteDraftLimit) { throw new IdentifiableError('9ee33bbe-fde3-4c71-9b51-e50492c6b9c8', 'Too many drafts'); } + + if (data.isActuallyScheduled) { + const currentScheduledCount = await this.noteDraftsRepository.countBy({ + userId: me.id, + isActuallyScheduled: true, + }); + if (currentScheduledCount >= policies.scheduledNoteLimit) { + throw new IdentifiableError('c3275f19-4558-4c59-83e1-4f684b5fab66', 'Too many scheduled notes'); + } + } //#endregion - if (data.poll) { - if (typeof data.poll.expiresAt === 'number') { - if (data.poll.expiresAt < Date.now()) { - throw new IdentifiableError('04da457d-b083-4055-9082-955525eda5a5', 'Cannot create expired poll'); - } - } else if (typeof data.poll.expiredAfter === 'number') { - data.poll.expiresAt = new Date(Date.now() + data.poll.expiredAfter); - } + await this.validate(me, data); + + const draft = await this.noteDraftsRepository.insertOne({ + ...data, + id: this.idService.gen(), + userId: me.id, + }); + + if (draft.scheduledAt && draft.isActuallyScheduled) { + this.schedule(draft); } - const appliedDraft = await this.checkAndSetDraftNoteOptions(me, this.noteDraftsRepository.create(), data); - - appliedDraft.id = this.idService.gen(); - appliedDraft.userId = me.id; - const draft = this.noteDraftsRepository.save(appliedDraft); - return draft; } @bindThis - public async update(me: MiLocalUser, draftId: MiNoteDraft['id'], data: NoteDraftOptions): Promise { + public async update(me: MiLocalUser, draftId: MiNoteDraft['id'], data: Partial): Promise { const draft = await this.noteDraftsRepository.findOneBy({ id: draftId, userId: me.id, @@ -111,19 +105,36 @@ export class NoteDraftService { throw new IdentifiableError('49cd6b9d-848e-41ee-b0b9-adaca711a6b1', 'No such note draft'); } - if (data.poll) { - if (typeof data.poll.expiresAt === 'number') { - if (data.poll.expiresAt < Date.now()) { - throw new IdentifiableError('04da457d-b083-4055-9082-955525eda5a5', 'Cannot create expired poll'); - } - } else if (typeof data.poll.expiredAfter === 'number') { - data.poll.expiresAt = new Date(Date.now() + data.poll.expiredAfter); + //#region check draft limit + const policies = await this.roleService.getUserPolicies(me.id); + + if (!draft.isActuallyScheduled && data.isActuallyScheduled) { + const currentScheduledCount = await this.noteDraftsRepository.countBy({ + userId: me.id, + isActuallyScheduled: true, + }); + if (currentScheduledCount >= policies.scheduledNoteLimit) { + throw new IdentifiableError('bacdf856-5c51-4159-b88a-804fa5103be5', 'Too many scheduled notes'); } } + //#endregion - const appliedDraft = await this.checkAndSetDraftNoteOptions(me, draft, data); + await this.validate(me, data); - return await this.noteDraftsRepository.save(appliedDraft); + const updatedDraft = await this.noteDraftsRepository.createQueryBuilder().update() + .set(data) + .where('id = :id', { id: draftId }) + .returning('*') + .execute() + .then((response) => response.raw[0]); + + this.clearSchedule(draftId).then(() => { + if (updatedDraft.scheduledAt != null && updatedDraft.isActuallyScheduled) { + this.schedule(updatedDraft); + } + }); + + return updatedDraft; } @bindThis @@ -138,6 +149,8 @@ export class NoteDraftService { } await this.noteDraftsRepository.delete(draft.id); + + this.clearSchedule(draftId); } @bindThis @@ -154,27 +167,20 @@ export class NoteDraftService { return draft; } - // 関連エンティティを取得し紐づける部分を共通化する @bindThis - public async checkAndSetDraftNoteOptions( + public async validate( me: MiLocalUser, - draft: MiNoteDraft, - data: NoteDraftOptions, - ): Promise { - data.visibility ??= 'public'; - data.localOnly ??= false; - if (data.reactionAcceptance === undefined) data.reactionAcceptance = null; - if (data.channelId != null) { - data.visibility = 'public'; - data.visibleUserIds = []; - data.localOnly = true; + data: Partial, + ): Promise { + if (data.pollExpiresAt != null) { + if (data.pollExpiresAt.getTime() < Date.now()) { + throw new IdentifiableError('04da457d-b083-4055-9082-955525eda5a5', 'Cannot create expired poll'); + } } - let appliedDraft = draft; - //#region visibleUsers let visibleUsers: MiUser[] = []; - if (data.visibleUserIds != null) { + if (data.visibleUserIds != null && data.visibleUserIds.length > 0) { visibleUsers = await this.usersRepository.findBy({ id: In(data.visibleUserIds), }); @@ -184,7 +190,7 @@ export class NoteDraftService { //#region files let files: MiDriveFile[] = []; const fileIds = data.fileIds ?? null; - if (fileIds != null) { + if (fileIds != null && fileIds.length > 0) { files = await this.driveFilesRepository.createQueryBuilder('file') .where('file.userId = :userId AND file.id IN (:...fileIds)', { userId: me.id, @@ -288,27 +294,37 @@ export class NoteDraftService { } } //#endregion + } - appliedDraft = { - ...appliedDraft, - visibility: data.visibility, - cw: data.cw ?? null, - fileIds: fileIds ?? [], - replyId: data.replyId ?? null, - renoteId: data.renoteId ?? null, - channelId: data.channelId ?? null, - text: data.text ?? null, - hashtag: data.hashtag ?? null, - hasPoll: data.poll != null, - pollChoices: data.poll ? data.poll.choices : [], - pollMultiple: data.poll ? data.poll.multiple : false, - pollExpiresAt: data.poll ? data.poll.expiresAt : null, - pollExpiredAfter: data.poll ? data.poll.expiredAfter ?? null : null, - visibleUserIds: data.visibleUserIds ?? [], - localOnly: data.localOnly, - reactionAcceptance: data.reactionAcceptance, - } satisfies MiNoteDraft; + @bindThis + public async schedule(draft: MiNoteDraft): Promise { + if (!draft.isActuallyScheduled) return; + if (draft.scheduledAt == null) return; + if (draft.scheduledAt.getTime() <= Date.now()) return; - return appliedDraft; + const delay = draft.scheduledAt.getTime() - Date.now(); + this.queueService.postScheduledNoteQueue.add(draft.id, { + noteDraftId: draft.id, + }, { + delay, + removeOnComplete: { + age: 3600 * 24 * 7, // keep up to 7 days + count: 30, + }, + removeOnFail: { + age: 3600 * 24 * 7, // keep up to 7 days + count: 100, + }, + }); + } + + @bindThis + public async clearSchedule(draftId: MiNoteDraft['id']): Promise { + const jobs = await this.queueService.postScheduledNoteQueue.getJobs(['delayed', 'waiting', 'active']); + for (const job of jobs) { + if (job.data.noteDraftId === draftId) { + await job.remove(); + } + } } } diff --git a/packages/backend/src/core/QueueModule.ts b/packages/backend/src/core/QueueModule.ts index b10b8e5899..ecd96261e0 100644 --- a/packages/backend/src/core/QueueModule.ts +++ b/packages/backend/src/core/QueueModule.ts @@ -16,11 +16,13 @@ import { RelationshipJobData, UserWebhookDeliverJobData, SystemWebhookDeliverJobData, + PostScheduledNoteJobData, } from '../queue/types.js'; import type { Provider } from '@nestjs/common'; export type SystemQueue = Bull.Queue>; export type EndedPollNotificationQueue = Bull.Queue; +export type PostScheduledNoteQueue = Bull.Queue; export type DeliverQueue = Bull.Queue; export type InboxQueue = Bull.Queue; export type DbQueue = Bull.Queue; @@ -41,6 +43,12 @@ const $endedPollNotification: Provider = { inject: [DI.config], }; +const $postScheduledNote: Provider = { + provide: 'queue:postScheduledNote', + useFactory: (config: Config) => new Bull.Queue(QUEUE.POST_SCHEDULED_NOTE, baseQueueOptions(config, QUEUE.POST_SCHEDULED_NOTE)), + inject: [DI.config], +}; + const $deliver: Provider = { provide: 'queue:deliver', useFactory: (config: Config) => new Bull.Queue(QUEUE.DELIVER, baseQueueOptions(config, QUEUE.DELIVER)), @@ -89,6 +97,7 @@ const $systemWebhookDeliver: Provider = { providers: [ $system, $endedPollNotification, + $postScheduledNote, $deliver, $inbox, $db, @@ -100,6 +109,7 @@ const $systemWebhookDeliver: Provider = { exports: [ $system, $endedPollNotification, + $postScheduledNote, $deliver, $inbox, $db, @@ -113,6 +123,7 @@ export class QueueModule implements OnApplicationShutdown { constructor( @Inject('queue:system') public systemQueue: SystemQueue, @Inject('queue:endedPollNotification') public endedPollNotificationQueue: EndedPollNotificationQueue, + @Inject('queue:postScheduledNote') public postScheduledNoteQueue: PostScheduledNoteQueue, @Inject('queue:deliver') public deliverQueue: DeliverQueue, @Inject('queue:inbox') public inboxQueue: InboxQueue, @Inject('queue:db') public dbQueue: DbQueue, @@ -129,6 +140,7 @@ export class QueueModule implements OnApplicationShutdown { await Promise.all([ this.systemQueue.close(), this.endedPollNotificationQueue.close(), + this.postScheduledNoteQueue.close(), this.deliverQueue.close(), this.inboxQueue.close(), this.dbQueue.close(), diff --git a/packages/backend/src/core/QueueService.ts b/packages/backend/src/core/QueueService.ts index 2d0e7b5d83..42782167bb 100644 --- a/packages/backend/src/core/QueueService.ts +++ b/packages/backend/src/core/QueueService.ts @@ -31,6 +31,7 @@ import type { DbQueue, DeliverQueue, EndedPollNotificationQueue, + PostScheduledNoteQueue, InboxQueue, ObjectStorageQueue, RelationshipQueue, @@ -44,6 +45,7 @@ import type * as Bull from 'bullmq'; export const QUEUE_TYPES = [ 'system', 'endedPollNotification', + 'postScheduledNote', 'deliver', 'inbox', 'db', @@ -92,6 +94,7 @@ export class QueueService { @Inject('queue:system') public systemQueue: SystemQueue, @Inject('queue:endedPollNotification') public endedPollNotificationQueue: EndedPollNotificationQueue, + @Inject('queue:postScheduledNote') public postScheduledNoteQueue: PostScheduledNoteQueue, @Inject('queue:deliver') public deliverQueue: DeliverQueue, @Inject('queue:inbox') public inboxQueue: InboxQueue, @Inject('queue:db') public dbQueue: DbQueue, @@ -717,6 +720,7 @@ export class QueueService { switch (type) { case 'system': return this.systemQueue; case 'endedPollNotification': return this.endedPollNotificationQueue; + case 'postScheduledNote': return this.postScheduledNoteQueue; case 'deliver': return this.deliverQueue; case 'inbox': return this.inboxQueue; case 'db': return this.dbQueue; diff --git a/packages/backend/src/core/RoleService.ts b/packages/backend/src/core/RoleService.ts index 7dc07ef4dd..6e4ac66e81 100644 --- a/packages/backend/src/core/RoleService.ts +++ b/packages/backend/src/core/RoleService.ts @@ -69,6 +69,7 @@ export type RolePolicies = { chatAvailability: 'available' | 'readonly' | 'unavailable'; uploadableFileTypes: string[]; noteDraftLimit: number; + scheduledNoteLimit: number; watermarkAvailable: boolean; }; @@ -101,20 +102,22 @@ export const DEFAULT_POLICIES: RolePolicies = { userEachUserListsLimit: 50, rateLimitFactor: 1, avatarDecorationLimit: 1, - canImportAntennas: true, - canImportBlocking: true, - canImportFollowing: true, - canImportMuting: true, - canImportUserLists: true, + canImportAntennas: false, + canImportBlocking: false, + canImportFollowing: false, + canImportMuting: false, + canImportUserLists: false, chatAvailability: 'available', uploadableFileTypes: [ 'text/plain', + 'text/csv', 'application/json', 'image/*', 'video/*', 'audio/*', ], noteDraftLimit: 10, + scheduledNoteLimit: 1, watermarkAvailable: true, }; @@ -439,6 +442,7 @@ export class RoleService implements OnApplicationShutdown, OnModuleInit { return [...set]; }), noteDraftLimit: calc('noteDraftLimit', vs => Math.max(...vs)), + scheduledNoteLimit: calc('scheduledNoteLimit', vs => Math.max(...vs)), watermarkAvailable: calc('watermarkAvailable', vs => vs.some(v => v === true)), }; } diff --git a/packages/backend/src/core/entities/MetaEntityService.ts b/packages/backend/src/core/entities/MetaEntityService.ts index f8abfb2f98..2da614a120 100644 --- a/packages/backend/src/core/entities/MetaEntityService.ts +++ b/packages/backend/src/core/entities/MetaEntityService.ts @@ -117,6 +117,7 @@ export class MetaEntityService { ratio: ad.ratio, imageUrl: ad.imageUrl, dayOfWeek: ad.dayOfWeek, + isSensitive: ad.isSensitive ? true : undefined, })), notesPerOneAd: instance.notesPerOneAd, enableEmail: instance.enableEmail, diff --git a/packages/backend/src/core/entities/NoteDraftEntityService.ts b/packages/backend/src/core/entities/NoteDraftEntityService.ts index 3ef8cdaa12..71e41a588d 100644 --- a/packages/backend/src/core/entities/NoteDraftEntityService.ts +++ b/packages/backend/src/core/entities/NoteDraftEntityService.ts @@ -105,6 +105,8 @@ export class NoteDraftEntityService implements OnModuleInit { const packed: Packed<'NoteDraft'> = await awaitAll({ id: noteDraft.id, createdAt: this.idService.parse(noteDraft.id).date.toISOString(), + scheduledAt: noteDraft.scheduledAt?.getTime() ?? null, + isActuallyScheduled: noteDraft.isActuallyScheduled, userId: noteDraft.userId, user: packedUsers?.get(noteDraft.userId) ?? this.userEntityService.pack(noteDraft.user ?? noteDraft.userId, me), text: text, @@ -112,13 +114,13 @@ export class NoteDraftEntityService implements OnModuleInit { visibility: noteDraft.visibility, localOnly: noteDraft.localOnly, reactionAcceptance: noteDraft.reactionAcceptance, - visibleUserIds: noteDraft.visibility === 'specified' ? noteDraft.visibleUserIds : undefined, - hashtag: noteDraft.hashtag ?? undefined, + visibleUserIds: noteDraft.visibleUserIds, + hashtag: noteDraft.hashtag, fileIds: noteDraft.fileIds, files: packedFiles != null ? this.packAttachedFiles(noteDraft.fileIds, packedFiles) : this.driveFileEntityService.packManyByIds(noteDraft.fileIds), replyId: noteDraft.replyId, renoteId: noteDraft.renoteId, - channelId: noteDraft.channelId ?? undefined, + channelId: noteDraft.channelId, channel: channel ? { id: channel.id, name: channel.name, @@ -127,6 +129,12 @@ export class NoteDraftEntityService implements OnModuleInit { allowRenoteToExternal: channel.allowRenoteToExternal, userId: channel.userId, } : undefined, + poll: noteDraft.hasPoll ? { + choices: noteDraft.pollChoices, + multiple: noteDraft.pollMultiple, + expiresAt: noteDraft.pollExpiresAt?.toISOString(), + expiredAfter: noteDraft.pollExpiredAfter, + } : null, ...(opts.detail ? { reply: noteDraft.replyId ? nullIfEntityNotFound(this.noteEntityService.pack(noteDraft.replyId, me, { @@ -138,13 +146,6 @@ export class NoteDraftEntityService implements OnModuleInit { detail: true, skipHide: opts.skipHide, })) : undefined, - - poll: noteDraft.hasPoll ? { - choices: noteDraft.pollChoices, - multiple: noteDraft.pollMultiple, - expiresAt: noteDraft.pollExpiresAt?.toISOString(), - expiredAfter: noteDraft.pollExpiredAfter, - } : undefined, } : {} ), }); diff --git a/packages/backend/src/core/entities/NotificationEntityService.ts b/packages/backend/src/core/entities/NotificationEntityService.ts index e91fb9eb51..0e96237d32 100644 --- a/packages/backend/src/core/entities/NotificationEntityService.ts +++ b/packages/backend/src/core/entities/NotificationEntityService.ts @@ -21,7 +21,18 @@ import type { OnModuleInit } from '@nestjs/common'; import type { UserEntityService } from './UserEntityService.js'; import type { NoteEntityService } from './NoteEntityService.js'; -const NOTE_REQUIRED_NOTIFICATION_TYPES = new Set(['note', 'mention', 'reply', 'renote', 'renote:grouped', 'quote', 'reaction', 'reaction:grouped', 'pollEnded'] as (typeof groupedNotificationTypes[number])[]); +const NOTE_REQUIRED_NOTIFICATION_TYPES = new Set([ + 'note', + 'mention', + 'reply', + 'renote', + 'renote:grouped', + 'quote', + 'reaction', + 'reaction:grouped', + 'pollEnded', + 'scheduledNotePosted', +] as (typeof groupedNotificationTypes[number])[]); @Injectable() export class NotificationEntityService implements OnModuleInit { diff --git a/packages/backend/src/models/Ad.ts b/packages/backend/src/models/Ad.ts index 108e991c70..0d402fcbe8 100644 --- a/packages/backend/src/models/Ad.ts +++ b/packages/backend/src/models/Ad.ts @@ -54,10 +54,17 @@ export class MiAd { length: 8192, nullable: false, }) public memo: string; + @Column('integer', { default: 0, nullable: false, }) public dayOfWeek: number; + + @Column('boolean', { + default: false, + }) + public isSensitive: boolean; + constructor(data: Partial) { if (data == null) return; diff --git a/packages/backend/src/models/NoteDraft.ts b/packages/backend/src/models/NoteDraft.ts index 6483748bc2..0ece02c943 100644 --- a/packages/backend/src/models/NoteDraft.ts +++ b/packages/backend/src/models/NoteDraft.ts @@ -126,7 +126,7 @@ export class MiNoteDraft { @JoinColumn() public channel: MiChannel | null; - // 以下、Pollについて追加 + //#region 以下、Pollについて追加 @Column('boolean', { default: false, @@ -151,13 +151,15 @@ export class MiNoteDraft { }) public pollExpiredAfter: number | null; - // ここまで追加 + //#endregion - constructor(data: Partial) { - if (data == null) return; + @Column('timestamp with time zone', { + nullable: true, + }) + public scheduledAt: Date | null; - for (const [k, v] of Object.entries(data)) { - (this as any)[k] = v; - } - } + @Column('boolean', { + default: false, + }) + public isActuallyScheduled: boolean; } diff --git a/packages/backend/src/models/Notification.ts b/packages/backend/src/models/Notification.ts index 0b4eeb3455..7fa17e20fa 100644 --- a/packages/backend/src/models/Notification.ts +++ b/packages/backend/src/models/Notification.ts @@ -9,6 +9,7 @@ import { MiNote } from './Note.js'; import { MiAccessToken } from './AccessToken.js'; import { MiRole } from './Role.js'; import { MiDriveFile } from './DriveFile.js'; +import { MiNoteDraft } from './NoteDraft.js'; // misskey-js の notificationTypes と同期すべし export type MiNotification = { @@ -60,6 +61,16 @@ export type MiNotification = { createdAt: string; notifierId: MiUser['id']; noteId: MiNote['id']; +} | { + type: 'scheduledNotePosted'; + id: string; + createdAt: string; + noteId: MiNote['id']; +} | { + type: 'scheduledNotePostFailed'; + id: string; + createdAt: string; + noteDraftId: MiNoteDraft['id']; } | { type: 'receiveFollowRequest'; id: string; diff --git a/packages/backend/src/models/json-schema/ad.ts b/packages/backend/src/models/json-schema/ad.ts index b01b39a38b..d88ac23894 100644 --- a/packages/backend/src/models/json-schema/ad.ts +++ b/packages/backend/src/models/json-schema/ad.ts @@ -60,5 +60,10 @@ export const packedAdSchema = { optional: false, nullable: false, }, + isSensitive: { + type: 'boolean', + optional: false, + nullable: false, + }, }, } as const; diff --git a/packages/backend/src/models/json-schema/meta.ts b/packages/backend/src/models/json-schema/meta.ts index 357ff26041..a0e7d490b3 100644 --- a/packages/backend/src/models/json-schema/meta.ts +++ b/packages/backend/src/models/json-schema/meta.ts @@ -195,6 +195,10 @@ export const packedMetaLiteSchema = { type: 'integer', optional: false, nullable: false, }, + isSensitive: { + type: 'boolean', + optional: true, nullable: false, + }, }, }, }, diff --git a/packages/backend/src/models/json-schema/note-draft.ts b/packages/backend/src/models/json-schema/note-draft.ts index 504b263a6d..8144ac7b3b 100644 --- a/packages/backend/src/models/json-schema/note-draft.ts +++ b/packages/backend/src/models/json-schema/note-draft.ts @@ -23,7 +23,7 @@ export const packedNoteDraftSchema = { }, cw: { type: 'string', - optional: true, nullable: true, + optional: false, nullable: true, }, userId: { type: 'string', @@ -37,27 +37,23 @@ export const packedNoteDraftSchema = { }, replyId: { type: 'string', - optional: true, nullable: true, + optional: false, nullable: true, format: 'id', - example: 'xxxxxxxxxx', }, renoteId: { type: 'string', - optional: true, nullable: true, + optional: false, nullable: true, format: 'id', - example: 'xxxxxxxxxx', }, reply: { type: 'object', optional: true, nullable: true, ref: 'Note', - description: 'The reply target note contents if exists. If the reply target has been deleted since the draft was created, this will be null while replyId is not null.', }, renote: { type: 'object', optional: true, nullable: true, ref: 'Note', - description: 'The renote target note contents if exists. If the renote target has been deleted since the draft was created, this will be null while renoteId is not null.', }, visibility: { type: 'string', @@ -66,7 +62,7 @@ export const packedNoteDraftSchema = { }, visibleUserIds: { type: 'array', - optional: true, nullable: false, + optional: false, nullable: false, items: { type: 'string', optional: false, nullable: false, @@ -75,7 +71,7 @@ export const packedNoteDraftSchema = { }, fileIds: { type: 'array', - optional: true, nullable: false, + optional: false, nullable: false, items: { type: 'string', optional: false, nullable: false, @@ -93,11 +89,11 @@ export const packedNoteDraftSchema = { }, hashtag: { type: 'string', - optional: true, nullable: false, + optional: false, nullable: true, }, poll: { type: 'object', - optional: true, nullable: true, + optional: false, nullable: true, properties: { expiresAt: { type: 'string', @@ -124,9 +120,8 @@ export const packedNoteDraftSchema = { }, channelId: { type: 'string', - optional: true, nullable: true, + optional: false, nullable: true, format: 'id', - example: 'xxxxxxxxxx', }, channel: { type: 'object', @@ -160,12 +155,20 @@ export const packedNoteDraftSchema = { }, localOnly: { type: 'boolean', - optional: true, nullable: false, + optional: false, nullable: false, }, reactionAcceptance: { type: 'string', optional: false, nullable: true, enum: ['likeOnly', 'likeOnlyForRemote', 'nonSensitiveOnly', 'nonSensitiveOnlyForLocalLikeOnlyForRemote', null], }, + scheduledAt: { + type: 'number', + optional: false, nullable: true, + }, + isActuallyScheduled: { + type: 'boolean', + optional: false, nullable: false, + }, }, } as const; diff --git a/packages/backend/src/models/json-schema/notification.ts b/packages/backend/src/models/json-schema/notification.ts index 6de120c8d7..30e9c9327a 100644 --- a/packages/backend/src/models/json-schema/notification.ts +++ b/packages/backend/src/models/json-schema/notification.ts @@ -207,6 +207,36 @@ export const packedNotificationSchema = { optional: false, nullable: false, }, }, + }, { + type: 'object', + properties: { + ...baseSchema.properties, + type: { + type: 'string', + optional: false, nullable: false, + enum: ['scheduledNotePosted'], + }, + note: { + type: 'object', + ref: 'Note', + optional: false, nullable: false, + }, + }, + }, { + type: 'object', + properties: { + ...baseSchema.properties, + type: { + type: 'string', + optional: false, nullable: false, + enum: ['scheduledNotePostFailed'], + }, + noteDraft: { + type: 'object', + ref: 'NoteDraft', + optional: false, nullable: false, + }, + }, }, { type: 'object', properties: { diff --git a/packages/backend/src/models/json-schema/role.ts b/packages/backend/src/models/json-schema/role.ts index 0b9234cb81..b9000152d4 100644 --- a/packages/backend/src/models/json-schema/role.ts +++ b/packages/backend/src/models/json-schema/role.ts @@ -317,6 +317,10 @@ export const packedRolePoliciesSchema = { type: 'integer', optional: false, nullable: false, }, + scheduledNoteLimit: { + type: 'integer', + optional: false, nullable: false, + }, watermarkAvailable: { type: 'boolean', optional: false, nullable: false, diff --git a/packages/backend/src/models/json-schema/user.ts b/packages/backend/src/models/json-schema/user.ts index c507d8d5c6..b5fd38a7d7 100644 --- a/packages/backend/src/models/json-schema/user.ts +++ b/packages/backend/src/models/json-schema/user.ts @@ -609,6 +609,8 @@ export const packedMeDetailedOnlySchema = { quote: { optional: true, ...notificationRecieveConfig }, reaction: { optional: true, ...notificationRecieveConfig }, pollEnded: { optional: true, ...notificationRecieveConfig }, + scheduledNotePosted: { optional: true, ...notificationRecieveConfig }, + scheduledNotePostFailed: { optional: true, ...notificationRecieveConfig }, receiveFollowRequest: { optional: true, ...notificationRecieveConfig }, followRequestAccepted: { optional: true, ...notificationRecieveConfig }, roleAssigned: { optional: true, ...notificationRecieveConfig }, diff --git a/packages/backend/src/queue/QueueProcessorModule.ts b/packages/backend/src/queue/QueueProcessorModule.ts index e01414cd53..e64882c4df 100644 --- a/packages/backend/src/queue/QueueProcessorModule.ts +++ b/packages/backend/src/queue/QueueProcessorModule.ts @@ -10,6 +10,7 @@ import { QueueLoggerService } from './QueueLoggerService.js'; import { QueueProcessorService } from './QueueProcessorService.js'; import { DeliverProcessorService } from './processors/DeliverProcessorService.js'; import { EndedPollNotificationProcessorService } from './processors/EndedPollNotificationProcessorService.js'; +import { PostScheduledNoteProcessorService } from './processors/PostScheduledNoteProcessorService.js'; import { InboxProcessorService } from './processors/InboxProcessorService.js'; import { UserWebhookDeliverProcessorService } from './processors/UserWebhookDeliverProcessorService.js'; import { SystemWebhookDeliverProcessorService } from './processors/SystemWebhookDeliverProcessorService.js'; @@ -79,6 +80,7 @@ import { RelationshipProcessorService } from './processors/RelationshipProcessor UserWebhookDeliverProcessorService, SystemWebhookDeliverProcessorService, EndedPollNotificationProcessorService, + PostScheduledNoteProcessorService, DeliverProcessorService, InboxProcessorService, AggregateRetentionProcessorService, diff --git a/packages/backend/src/queue/QueueProcessorService.ts b/packages/backend/src/queue/QueueProcessorService.ts index 7b64182754..642d3fc8ad 100644 --- a/packages/backend/src/queue/QueueProcessorService.ts +++ b/packages/backend/src/queue/QueueProcessorService.ts @@ -14,6 +14,7 @@ import { CheckModeratorsActivityProcessorService } from '@/queue/processors/Chec import { UserWebhookDeliverProcessorService } from './processors/UserWebhookDeliverProcessorService.js'; import { SystemWebhookDeliverProcessorService } from './processors/SystemWebhookDeliverProcessorService.js'; import { EndedPollNotificationProcessorService } from './processors/EndedPollNotificationProcessorService.js'; +import { PostScheduledNoteProcessorService } from './processors/PostScheduledNoteProcessorService.js'; import { DeliverProcessorService } from './processors/DeliverProcessorService.js'; import { InboxProcessorService } from './processors/InboxProcessorService.js'; import { DeleteDriveFilesProcessorService } from './processors/DeleteDriveFilesProcessorService.js'; @@ -85,6 +86,7 @@ export class QueueProcessorService implements OnApplicationShutdown { private relationshipQueueWorker: Bull.Worker; private objectStorageQueueWorker: Bull.Worker; private endedPollNotificationQueueWorker: Bull.Worker; + private postScheduledNoteQueueWorker: Bull.Worker; constructor( @Inject(DI.config) @@ -94,6 +96,7 @@ export class QueueProcessorService implements OnApplicationShutdown { private userWebhookDeliverProcessorService: UserWebhookDeliverProcessorService, private systemWebhookDeliverProcessorService: SystemWebhookDeliverProcessorService, private endedPollNotificationProcessorService: EndedPollNotificationProcessorService, + private postScheduledNoteProcessorService: PostScheduledNoteProcessorService, private deliverProcessorService: DeliverProcessorService, private inboxProcessorService: InboxProcessorService, private deleteDriveFilesProcessorService: DeleteDriveFilesProcessorService, @@ -520,6 +523,21 @@ export class QueueProcessorService implements OnApplicationShutdown { }); } //#endregion + + //#region post scheduled note + { + this.postScheduledNoteQueueWorker = new Bull.Worker(QUEUE.POST_SCHEDULED_NOTE, async (job) => { + if (this.config.sentryForBackend) { + return Sentry.startSpan({ name: 'Queue: PostScheduledNote' }, () => this.postScheduledNoteProcessorService.process(job)); + } else { + return this.postScheduledNoteProcessorService.process(job); + } + }, { + ...baseWorkerOptions(this.config, QUEUE.POST_SCHEDULED_NOTE), + autorun: false, + }); + } + //#endregion } @bindThis @@ -534,6 +552,7 @@ export class QueueProcessorService implements OnApplicationShutdown { this.relationshipQueueWorker.run(), this.objectStorageQueueWorker.run(), this.endedPollNotificationQueueWorker.run(), + this.postScheduledNoteQueueWorker.run(), ]); } @@ -549,6 +568,7 @@ export class QueueProcessorService implements OnApplicationShutdown { this.relationshipQueueWorker.close(), this.objectStorageQueueWorker.close(), this.endedPollNotificationQueueWorker.close(), + this.postScheduledNoteQueueWorker.close(), ]); } diff --git a/packages/backend/src/queue/const.ts b/packages/backend/src/queue/const.ts index 7e146a7e03..625204b7ad 100644 --- a/packages/backend/src/queue/const.ts +++ b/packages/backend/src/queue/const.ts @@ -12,6 +12,7 @@ export const QUEUE = { INBOX: 'inbox', SYSTEM: 'system', ENDED_POLL_NOTIFICATION: 'endedPollNotification', + POST_SCHEDULED_NOTE: 'postScheduledNote', DB: 'db', RELATIONSHIP: 'relationship', OBJECT_STORAGE: 'objectStorage', diff --git a/packages/backend/src/queue/processors/PostScheduledNoteProcessorService.ts b/packages/backend/src/queue/processors/PostScheduledNoteProcessorService.ts new file mode 100644 index 0000000000..d0eaeee090 --- /dev/null +++ b/packages/backend/src/queue/processors/PostScheduledNoteProcessorService.ts @@ -0,0 +1,72 @@ +/* + * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-License-Identifier: AGPL-3.0-only + */ + +import { Inject, Injectable } from '@nestjs/common'; +import { DI } from '@/di-symbols.js'; +import type { NoteDraftsRepository } from '@/models/_.js'; +import type Logger from '@/logger.js'; +import { NotificationService } from '@/core/NotificationService.js'; +import { bindThis } from '@/decorators.js'; +import { NoteCreateService } from '@/core/NoteCreateService.js'; +import { QueueLoggerService } from '../QueueLoggerService.js'; +import type * as Bull from 'bullmq'; +import type { PostScheduledNoteJobData } from '../types.js'; + +@Injectable() +export class PostScheduledNoteProcessorService { + private logger: Logger; + + constructor( + @Inject(DI.noteDraftsRepository) + private noteDraftsRepository: NoteDraftsRepository, + + private noteCreateService: NoteCreateService, + private notificationService: NotificationService, + private queueLoggerService: QueueLoggerService, + ) { + this.logger = this.queueLoggerService.logger.createSubLogger('post-scheduled-note'); + } + + @bindThis + public async process(job: Bull.Job): Promise { + const draft = await this.noteDraftsRepository.findOne({ where: { id: job.data.noteDraftId }, relations: ['user'] }); + if (draft == null || draft.user == null || draft.scheduledAt == null || !draft.isActuallyScheduled) { + return; + } + + try { + const note = await this.noteCreateService.fetchAndCreate(draft.user, { + createdAt: new Date(), + fileIds: draft.fileIds, + poll: draft.hasPoll ? { + choices: draft.pollChoices, + multiple: draft.pollMultiple, + expiresAt: draft.pollExpiredAfter ? new Date(Date.now() + draft.pollExpiredAfter) : draft.pollExpiresAt ? new Date(draft.pollExpiresAt) : null, + } : null, + text: draft.text ?? null, + replyId: draft.replyId, + renoteId: draft.renoteId, + cw: draft.cw, + localOnly: draft.localOnly, + reactionAcceptance: draft.reactionAcceptance, + visibility: draft.visibility, + visibleUserIds: draft.visibleUserIds, + channelId: draft.channelId, + }); + + // await不要 + this.noteDraftsRepository.remove(draft); + + // await不要 + this.notificationService.createNotification(draft.userId, 'scheduledNotePosted', { + noteId: note.id, + }); + } catch (err) { + this.notificationService.createNotification(draft.userId, 'scheduledNotePostFailed', { + noteDraftId: draft.id, + }); + } + } +} diff --git a/packages/backend/src/queue/types.ts b/packages/backend/src/queue/types.ts index 757daea88b..1cb2b93918 100644 --- a/packages/backend/src/queue/types.ts +++ b/packages/backend/src/queue/types.ts @@ -109,6 +109,10 @@ export type EndedPollNotificationJobData = { noteId: MiNote['id']; }; +export type PostScheduledNoteJobData = { + noteDraftId: string; +}; + export type SystemWebhookDeliverJobData = { type: T; content: SystemWebhookPayload; diff --git a/packages/backend/src/server/ServerService.ts b/packages/backend/src/server/ServerService.ts index 7325c53df0..1286b4dad6 100644 --- a/packages/backend/src/server/ServerService.ts +++ b/packages/backend/src/server/ServerService.ts @@ -75,7 +75,7 @@ export class ServerService implements OnApplicationShutdown { @bindThis public async launch(): Promise { const fastify = Fastify({ - trustProxy: true, + trustProxy: this.config.trustProxy ?? true, logger: false, }); this.#fastify = fastify; diff --git a/packages/backend/src/server/api/endpoints/admin/accounts/create.ts b/packages/backend/src/server/api/endpoints/admin/accounts/create.ts index 06047b58a6..6606202118 100644 --- a/packages/backend/src/server/api/endpoints/admin/accounts/create.ts +++ b/packages/backend/src/server/api/endpoints/admin/accounts/create.ts @@ -34,13 +34,22 @@ export const meta = { res: { type: 'object', optional: false, nullable: false, - ref: 'MeDetailed', - properties: { - token: { - type: 'string', - optional: false, nullable: false, + allOf: [ + { + type: 'object', + ref: 'MeDetailed', }, - }, + { + type: 'object', + optional: false, nullable: false, + properties: { + token: { + type: 'string', + optional: false, nullable: false, + }, + }, + } + ], }, } as const; diff --git a/packages/backend/src/server/api/endpoints/admin/ad/create.ts b/packages/backend/src/server/api/endpoints/admin/ad/create.ts index 955154f4fb..01697ae185 100644 --- a/packages/backend/src/server/api/endpoints/admin/ad/create.ts +++ b/packages/backend/src/server/api/endpoints/admin/ad/create.ts @@ -36,6 +36,7 @@ export const paramDef = { startsAt: { type: 'integer' }, imageUrl: { type: 'string', minLength: 1 }, dayOfWeek: { type: 'integer' }, + isSensitive: { type: 'boolean' }, }, required: ['url', 'memo', 'place', 'priority', 'ratio', 'expiresAt', 'startsAt', 'imageUrl', 'dayOfWeek'], } as const; @@ -55,6 +56,7 @@ export default class extends Endpoint { // eslint- expiresAt: new Date(ps.expiresAt), startsAt: new Date(ps.startsAt), dayOfWeek: ps.dayOfWeek, + isSensitive: ps.isSensitive, url: ps.url, imageUrl: ps.imageUrl, priority: ps.priority, @@ -73,6 +75,7 @@ export default class extends Endpoint { // eslint- expiresAt: ad.expiresAt.toISOString(), startsAt: ad.startsAt.toISOString(), dayOfWeek: ad.dayOfWeek, + isSensitive: ad.isSensitive, url: ad.url, imageUrl: ad.imageUrl, priority: ad.priority, diff --git a/packages/backend/src/server/api/endpoints/admin/ad/list.ts b/packages/backend/src/server/api/endpoints/admin/ad/list.ts index 4f897d98e4..f67cad5bd2 100644 --- a/packages/backend/src/server/api/endpoints/admin/ad/list.ts +++ b/packages/backend/src/server/api/endpoints/admin/ad/list.ts @@ -63,6 +63,7 @@ export default class extends Endpoint { // eslint- expiresAt: ad.expiresAt.toISOString(), startsAt: ad.startsAt.toISOString(), dayOfWeek: ad.dayOfWeek, + isSensitive: ad.isSensitive, url: ad.url, imageUrl: ad.imageUrl, memo: ad.memo, diff --git a/packages/backend/src/server/api/endpoints/admin/ad/update.ts b/packages/backend/src/server/api/endpoints/admin/ad/update.ts index 4e3d731aca..a3d9aaddc6 100644 --- a/packages/backend/src/server/api/endpoints/admin/ad/update.ts +++ b/packages/backend/src/server/api/endpoints/admin/ad/update.ts @@ -39,6 +39,7 @@ export const paramDef = { expiresAt: { type: 'integer' }, startsAt: { type: 'integer' }, dayOfWeek: { type: 'integer' }, + isSensitive: { type: 'boolean' }, }, required: ['id'], } as const; @@ -66,6 +67,7 @@ export default class extends Endpoint { // eslint- expiresAt: ps.expiresAt ? new Date(ps.expiresAt) : undefined, startsAt: ps.startsAt ? new Date(ps.startsAt) : undefined, dayOfWeek: ps.dayOfWeek, + isSensitive: ps.isSensitive, }); const updatedAd = await this.adsRepository.findOneByOrFail({ id: ad.id }); diff --git a/packages/backend/src/server/api/endpoints/admin/queue/stats.ts b/packages/backend/src/server/api/endpoints/admin/queue/stats.ts index d7f9e4eaa3..b69699c338 100644 --- a/packages/backend/src/server/api/endpoints/admin/queue/stats.ts +++ b/packages/backend/src/server/api/endpoints/admin/queue/stats.ts @@ -5,7 +5,7 @@ import { Inject, Injectable } from '@nestjs/common'; import { Endpoint } from '@/server/api/endpoint-base.js'; -import type { DbQueue, DeliverQueue, EndedPollNotificationQueue, InboxQueue, ObjectStorageQueue, SystemQueue, UserWebhookDeliverQueue, SystemWebhookDeliverQueue } from '@/core/QueueModule.js'; +import type { DbQueue, DeliverQueue, EndedPollNotificationQueue, PostScheduledNoteQueue, InboxQueue, ObjectStorageQueue, SystemQueue, UserWebhookDeliverQueue, SystemWebhookDeliverQueue } from '@/core/QueueModule.js'; export const meta = { tags: ['admin'], @@ -49,6 +49,7 @@ export default class extends Endpoint { // eslint- constructor( @Inject('queue:system') public systemQueue: SystemQueue, @Inject('queue:endedPollNotification') public endedPollNotificationQueue: EndedPollNotificationQueue, + @Inject('queue:postScheduledNote') public postScheduledNoteQueue: PostScheduledNoteQueue, @Inject('queue:deliver') public deliverQueue: DeliverQueue, @Inject('queue:inbox') public inboxQueue: InboxQueue, @Inject('queue:db') public dbQueue: DbQueue, diff --git a/packages/backend/src/server/api/endpoints/admin/show-user.ts b/packages/backend/src/server/api/endpoints/admin/show-user.ts index 1ba6853dbe..2fd7ab8ca2 100644 --- a/packages/backend/src/server/api/endpoints/admin/show-user.ts +++ b/packages/backend/src/server/api/endpoints/admin/show-user.ts @@ -103,6 +103,8 @@ export const meta = { quote: { optional: true, ...notificationRecieveConfig }, reaction: { optional: true, ...notificationRecieveConfig }, pollEnded: { optional: true, ...notificationRecieveConfig }, + scheduledNotePosted: { optional: true, ...notificationRecieveConfig }, + scheduledNotePostFailed: { optional: true, ...notificationRecieveConfig }, receiveFollowRequest: { optional: true, ...notificationRecieveConfig }, followRequestAccepted: { optional: true, ...notificationRecieveConfig }, roleAssigned: { optional: true, ...notificationRecieveConfig }, diff --git a/packages/backend/src/server/api/endpoints/ap/show.ts b/packages/backend/src/server/api/endpoints/ap/show.ts index 4afed7dc5c..fe48e7497a 100644 --- a/packages/backend/src/server/api/endpoints/ap/show.ts +++ b/packages/backend/src/server/api/endpoints/ap/show.ts @@ -18,9 +18,9 @@ import { UserEntityService } from '@/core/entities/UserEntityService.js'; import { NoteEntityService } from '@/core/entities/NoteEntityService.js'; import { UtilityService } from '@/core/UtilityService.js'; import { bindThis } from '@/decorators.js'; -import { ApiError } from '../../error.js'; import { IdentifiableError } from '@/misc/identifiable-error.js'; import { FetchAllowSoftFailMask } from '@/core/activitypub/misc/check-against-url.js'; +import { ApiError } from '../../error.js'; export const meta = { tags: ['federation'], diff --git a/packages/backend/src/server/api/endpoints/i/update.ts b/packages/backend/src/server/api/endpoints/i/update.ts index 082d97f5d4..5c7958fc1c 100644 --- a/packages/backend/src/server/api/endpoints/i/update.ts +++ b/packages/backend/src/server/api/endpoints/i/update.ts @@ -209,6 +209,8 @@ export const paramDef = { quote: notificationRecieveConfig, reaction: notificationRecieveConfig, pollEnded: notificationRecieveConfig, + scheduledNotePosted: notificationRecieveConfig, + scheduledNotePostFailed: notificationRecieveConfig, receiveFollowRequest: notificationRecieveConfig, followRequestAccepted: notificationRecieveConfig, roleAssigned: notificationRecieveConfig, diff --git a/packages/backend/src/server/api/endpoints/notes/create.ts b/packages/backend/src/server/api/endpoints/notes/create.ts index 7caea8eedc..e48aa69d0f 100644 --- a/packages/backend/src/server/api/endpoints/notes/create.ts +++ b/packages/backend/src/server/api/endpoints/notes/create.ts @@ -6,17 +6,10 @@ import ms from 'ms'; import { In } from 'typeorm'; import { Inject, Injectable } from '@nestjs/common'; -import type { MiUser } from '@/models/User.js'; -import type { UsersRepository, NotesRepository, BlockingsRepository, DriveFilesRepository, ChannelsRepository } from '@/models/_.js'; -import type { MiDriveFile } from '@/models/DriveFile.js'; -import type { MiNote } from '@/models/Note.js'; -import type { MiChannel } from '@/models/Channel.js'; import { MAX_NOTE_TEXT_LENGTH } from '@/const.js'; import { Endpoint } from '@/server/api/endpoint-base.js'; import { NoteEntityService } from '@/core/entities/NoteEntityService.js'; import { NoteCreateService } from '@/core/NoteCreateService.js'; -import { DI } from '@/di-symbols.js'; -import { isQuote, isRenote } from '@/misc/is-renote.js'; import { IdentifiableError } from '@/misc/identifiable-error.js'; import { ApiError } from '../../error.js'; @@ -223,168 +216,28 @@ export const paramDef = { @Injectable() export default class extends Endpoint { // eslint-disable-line import/no-default-export constructor( - @Inject(DI.usersRepository) - private usersRepository: UsersRepository, - - @Inject(DI.notesRepository) - private notesRepository: NotesRepository, - - @Inject(DI.blockingsRepository) - private blockingsRepository: BlockingsRepository, - - @Inject(DI.driveFilesRepository) - private driveFilesRepository: DriveFilesRepository, - - @Inject(DI.channelsRepository) - private channelsRepository: ChannelsRepository, - private noteEntityService: NoteEntityService, private noteCreateService: NoteCreateService, ) { super(meta, paramDef, async (ps, me) => { - let visibleUsers: MiUser[] = []; - if (ps.visibleUserIds) { - visibleUsers = await this.usersRepository.findBy({ - id: In(ps.visibleUserIds), - }); - } - - let files: MiDriveFile[] = []; - const fileIds = ps.fileIds ?? ps.mediaIds ?? null; - if (fileIds != null) { - files = await this.driveFilesRepository.createQueryBuilder('file') - .where('file.userId = :userId AND file.id IN (:...fileIds)', { - userId: me.id, - fileIds, - }) - .orderBy('array_position(ARRAY[:...fileIds], "id"::text)') - .setParameters({ fileIds }) - .getMany(); - - if (files.length !== fileIds.length) { - throw new ApiError(meta.errors.noSuchFile); - } - } - - let renote: MiNote | null = null; - if (ps.renoteId != null) { - // Fetch renote to note - renote = await this.notesRepository.findOne({ - where: { id: ps.renoteId }, - relations: ['user', 'renote', 'reply'], - }); - - if (renote == null) { - throw new ApiError(meta.errors.noSuchRenoteTarget); - } else if (isRenote(renote) && !isQuote(renote)) { - throw new ApiError(meta.errors.cannotReRenote); - } - - // Check blocking - if (renote.userId !== me.id) { - const blockExist = await this.blockingsRepository.exists({ - where: { - blockerId: renote.userId, - blockeeId: me.id, - }, - }); - if (blockExist) { - throw new ApiError(meta.errors.youHaveBeenBlocked); - } - } - - if (renote.visibility === 'followers' && renote.userId !== me.id) { - // 他人のfollowers noteはreject - throw new ApiError(meta.errors.cannotRenoteDueToVisibility); - } else if (renote.visibility === 'specified') { - // specified / direct noteはreject - throw new ApiError(meta.errors.cannotRenoteDueToVisibility); - } - - if (renote.channelId && renote.channelId !== ps.channelId) { - // チャンネルのノートに対しリノート要求がきたとき、チャンネル外へのリノート可否をチェック - // リノートのユースケースのうち、チャンネル内→チャンネル外は少数だと考えられるため、JOINはせず必要な時に都度取得する - const renoteChannel = await this.channelsRepository.findOneBy({ id: renote.channelId }); - if (renoteChannel == null) { - // リノートしたいノートが書き込まれているチャンネルが無い - throw new ApiError(meta.errors.noSuchChannel); - } else if (!renoteChannel.allowRenoteToExternal) { - // リノート作成のリクエストだが、対象チャンネルがリノート禁止だった場合 - throw new ApiError(meta.errors.cannotRenoteOutsideOfChannel); - } - } - } - - let reply: MiNote | null = null; - if (ps.replyId != null) { - // Fetch reply - reply = await this.notesRepository.findOne({ - where: { id: ps.replyId }, - relations: ['user'], - }); - - if (reply == null) { - throw new ApiError(meta.errors.noSuchReplyTarget); - } else if (isRenote(reply) && !isQuote(reply)) { - throw new ApiError(meta.errors.cannotReplyToPureRenote); - } else if (!await this.noteEntityService.isVisibleForMe(reply, me.id)) { - throw new ApiError(meta.errors.cannotReplyToInvisibleNote); - } else if (reply.visibility === 'specified' && ps.visibility !== 'specified') { - throw new ApiError(meta.errors.cannotReplyToSpecifiedVisibilityNoteWithExtendedVisibility); - } - - // Check blocking - if (reply.userId !== me.id) { - const blockExist = await this.blockingsRepository.exists({ - where: { - blockerId: reply.userId, - blockeeId: me.id, - }, - }); - if (blockExist) { - throw new ApiError(meta.errors.youHaveBeenBlocked); - } - } - } - - if (ps.poll) { - if (typeof ps.poll.expiresAt === 'number') { - if (ps.poll.expiresAt < Date.now()) { - throw new ApiError(meta.errors.cannotCreateAlreadyExpiredPoll); - } - } else if (typeof ps.poll.expiredAfter === 'number') { - ps.poll.expiresAt = Date.now() + ps.poll.expiredAfter; - } - } - - let channel: MiChannel | null = null; - if (ps.channelId != null) { - channel = await this.channelsRepository.findOneBy({ id: ps.channelId, isArchived: false }); - - if (channel == null) { - throw new ApiError(meta.errors.noSuchChannel); - } - } - - // 投稿を作成 try { - const note = await this.noteCreateService.create(me, { + const note = await this.noteCreateService.fetchAndCreate(me, { createdAt: new Date(), - files: files, + fileIds: ps.fileIds ?? ps.mediaIds ?? [], poll: ps.poll ? { choices: ps.poll.choices, multiple: ps.poll.multiple ?? false, - expiresAt: ps.poll.expiresAt ? new Date(ps.poll.expiresAt) : null, - } : undefined, - text: ps.text ?? undefined, - reply, - renote, - cw: ps.cw, + expiresAt: ps.poll.expiredAfter ? new Date(Date.now() + ps.poll.expiredAfter) : ps.poll.expiresAt ? new Date(ps.poll.expiresAt) : null, + } : null, + text: ps.text ?? null, + replyId: ps.replyId ?? null, + renoteId: ps.renoteId ?? null, + cw: ps.cw ?? null, localOnly: ps.localOnly, reactionAcceptance: ps.reactionAcceptance, visibility: ps.visibility, - visibleUsers, - channel, + visibleUserIds: ps.visibleUserIds ?? [], + channelId: ps.channelId ?? null, apMentions: ps.noExtractMentions ? [] : undefined, apHashtags: ps.noExtractHashtags ? [] : undefined, apEmojis: ps.noExtractEmojis ? [] : undefined, @@ -393,16 +246,46 @@ export default class extends Endpoint { // eslint- return { createdNote: await this.noteEntityService.pack(note, me), }; - } catch (e) { + } catch (err) { // TODO: 他のErrorもここでキャッチしてエラーメッセージを当てるようにしたい - if (e instanceof IdentifiableError) { - if (e.id === '689ee33f-f97c-479a-ac49-1b9f8140af99') { + if (err instanceof IdentifiableError) { + if (err.id === '689ee33f-f97c-479a-ac49-1b9f8140af99') { throw new ApiError(meta.errors.containsProhibitedWords); - } else if (e.id === '9f466dab-c856-48cd-9e65-ff90ff750580') { + } else if (err.id === '9f466dab-c856-48cd-9e65-ff90ff750580') { throw new ApiError(meta.errors.containsTooManyMentions); + } else if (err.id === '801c046c-5bf5-4234-ad2b-e78fc20a2ac7') { + throw new ApiError(meta.errors.noSuchFile); + } else if (err.id === '53983c56-e163-45a6-942f-4ddc485d4290') { + throw new ApiError(meta.errors.noSuchRenoteTarget); + } else if (err.id === 'bde24c37-121f-4e7d-980d-cec52f599f02') { + throw new ApiError(meta.errors.cannotReRenote); + } else if (err.id === '2b4fe776-4414-4a2d-ae39-f3418b8fd4d3') { + throw new ApiError(meta.errors.youHaveBeenBlocked); + } else if (err.id === '90b9d6f0-893a-4fef-b0f1-e9a33989f71a') { + throw new ApiError(meta.errors.cannotRenoteDueToVisibility); + } else if (err.id === '48d7a997-da5c-4716-b3c3-92db3f37bf7d') { + throw new ApiError(meta.errors.cannotRenoteDueToVisibility); + } else if (err.id === 'b060f9a6-8909-4080-9e0b-94d9fa6f6a77') { + throw new ApiError(meta.errors.noSuchChannel); + } else if (err.id === '7e435f4a-780d-4cfc-a15a-42519bd6fb67') { + throw new ApiError(meta.errors.cannotRenoteOutsideOfChannel); + } else if (err.id === '60142edb-1519-408e-926d-4f108d27bee0') { + throw new ApiError(meta.errors.noSuchReplyTarget); + } else if (err.id === 'f089e4e2-c0e7-4f60-8a23-e5a6bf786b36') { + throw new ApiError(meta.errors.cannotReplyToPureRenote); + } else if (err.id === '11cd37b3-a411-4f77-8633-c580ce6a8dce') { + throw new ApiError(meta.errors.cannotReplyToInvisibleNote); + } else if (err.id === 'ced780a1-2012-4caf-bc7e-a95a291294cb') { + throw new ApiError(meta.errors.cannotReplyToSpecifiedVisibilityNoteWithExtendedVisibility); + } else if (err.id === 'b0df6025-f2e8-44b4-a26a-17ad99104612') { + throw new ApiError(meta.errors.youHaveBeenBlocked); + } else if (err.id === '0c11c11e-0c8d-48e7-822c-76ccef660068') { + throw new ApiError(meta.errors.cannotCreateAlreadyExpiredPoll); + } else if (err.id === 'bfa3905b-25f5-4894-b430-da331a490e4b') { + throw new ApiError(meta.errors.noSuchChannel); } } - throw e; + throw err; } }); } diff --git a/packages/backend/src/server/api/endpoints/notes/drafts/create.ts b/packages/backend/src/server/api/endpoints/notes/drafts/create.ts index 1c28ec22d0..8f2fbf9197 100644 --- a/packages/backend/src/server/api/endpoints/notes/drafts/create.ts +++ b/packages/backend/src/server/api/endpoints/notes/drafts/create.ts @@ -124,6 +124,12 @@ export const meta = { id: '9ee33bbe-fde3-4c71-9b51-e50492c6b9c8', }, + tooManyScheduledNotes: { + message: 'You cannot create scheduled notes any more.', + code: 'TOO_MANY_SCHEDULED_NOTES', + id: '22ae69eb-09e3-4541-a850-773cfa45e693', + }, + cannotRenoteToExternal: { message: 'Cannot Renote to External.', code: 'CANNOT_RENOTE_TO_EXTERNAL', @@ -162,7 +168,7 @@ export const paramDef = { fileIds: { type: 'array', uniqueItems: true, - minItems: 1, + minItems: 0, maxItems: 16, items: { type: 'string', format: 'misskey:id' }, }, @@ -183,8 +189,10 @@ export const paramDef = { }, required: ['choices'], }, + scheduledAt: { type: 'integer', nullable: true }, + isActuallyScheduled: { type: 'boolean', default: false }, }, - required: [], + required: ['visibility', 'visibleUserIds', 'cw', 'hashtag', 'localOnly', 'reactionAcceptance', 'replyId', 'renoteId', 'channelId', 'text', 'fileIds', 'poll', 'scheduledAt', 'isActuallyScheduled'], } as const; @Injectable() @@ -196,22 +204,23 @@ export default class extends Endpoint { // eslint- super(meta, paramDef, async (ps, me) => { const draft = await this.noteDraftService.create(me, { fileIds: ps.fileIds, - poll: ps.poll ? { - choices: ps.poll.choices, - multiple: ps.poll.multiple ?? false, - expiresAt: ps.poll.expiresAt ? new Date(ps.poll.expiresAt) : null, - expiredAfter: ps.poll.expiredAfter ?? null, - } : undefined, - text: ps.text ?? null, - replyId: ps.replyId ?? undefined, - renoteId: ps.renoteId ?? undefined, - cw: ps.cw ?? null, - ...(ps.hashtag ? { hashtag: ps.hashtag } : {}), + pollChoices: ps.poll?.choices ?? [], + pollMultiple: ps.poll?.multiple ?? false, + pollExpiresAt: ps.poll?.expiresAt ? new Date(ps.poll.expiresAt) : null, + pollExpiredAfter: ps.poll?.expiredAfter ?? null, + hasPoll: ps.poll != null, + text: ps.text, + replyId: ps.replyId, + renoteId: ps.renoteId, + cw: ps.cw, + hashtag: ps.hashtag, localOnly: ps.localOnly, reactionAcceptance: ps.reactionAcceptance, visibility: ps.visibility, - visibleUserIds: ps.visibleUserIds ?? [], - channelId: ps.channelId ?? undefined, + visibleUserIds: ps.visibleUserIds, + channelId: ps.channelId, + scheduledAt: ps.scheduledAt ? new Date(ps.scheduledAt) : null, + isActuallyScheduled: ps.isActuallyScheduled, }).catch((err) => { if (err instanceof IdentifiableError) { switch (err.id) { @@ -241,6 +250,8 @@ export default class extends Endpoint { // eslint- throw new ApiError(meta.errors.cannotReplyToInvisibleNote); case '215dbc76-336c-4d2a-9605-95766ba7dab0': throw new ApiError(meta.errors.cannotReplyToSpecifiedVisibilityNoteWithExtendedVisibility); + case 'c3275f19-4558-4c59-83e1-4f684b5fab66': + throw new ApiError(meta.errors.tooManyScheduledNotes); default: throw err; } diff --git a/packages/backend/src/server/api/endpoints/notes/drafts/list.ts b/packages/backend/src/server/api/endpoints/notes/drafts/list.ts index f24f9b8fb2..0774f09228 100644 --- a/packages/backend/src/server/api/endpoints/notes/drafts/list.ts +++ b/packages/backend/src/server/api/endpoints/notes/drafts/list.ts @@ -41,6 +41,7 @@ export const paramDef = { untilId: { type: 'string', format: 'misskey:id' }, sinceDate: { type: 'integer' }, untilDate: { type: 'integer' }, + scheduled: { type: 'boolean', nullable: true }, }, required: [], } as const; @@ -58,6 +59,12 @@ export default class extends Endpoint { // eslint- const query = this.queryService.makePaginationQuery(this.noteDraftsRepository.createQueryBuilder('drafts'), ps.sinceId, ps.untilId, ps.sinceDate, ps.untilDate) .andWhere('drafts.userId = :meId', { meId: me.id }); + if (ps.scheduled === true) { + query.andWhere('drafts.isActuallyScheduled = true'); + } else if (ps.scheduled === false) { + query.andWhere('drafts.isActuallyScheduled = false'); + } + const drafts = await query .limit(ps.limit) .getMany(); diff --git a/packages/backend/src/server/api/endpoints/notes/drafts/update.ts b/packages/backend/src/server/api/endpoints/notes/drafts/update.ts index ee221fb765..9a2e2ca415 100644 --- a/packages/backend/src/server/api/endpoints/notes/drafts/update.ts +++ b/packages/backend/src/server/api/endpoints/notes/drafts/update.ts @@ -159,6 +159,12 @@ export const meta = { code: 'CANNOT_REPLY_TO_SPECIFIED_VISIBILITY_NOTE_WITH_EXTENDED_VISIBILITY', id: '215dbc76-336c-4d2a-9605-95766ba7dab0', }, + + tooManyScheduledNotes: { + message: 'You cannot create scheduled notes any more.', + code: 'TOO_MANY_SCHEDULED_NOTES', + id: '02f5df79-08ae-4a33-8524-f1503c8f6212', + }, }, limit: { @@ -171,14 +177,14 @@ export const paramDef = { type: 'object', properties: { draftId: { type: 'string', nullable: false, format: 'misskey:id' }, - visibility: { type: 'string', enum: ['public', 'home', 'followers', 'specified'], default: 'public' }, + visibility: { type: 'string', enum: ['public', 'home', 'followers', 'specified'] }, visibleUserIds: { type: 'array', uniqueItems: true, items: { type: 'string', format: 'misskey:id', } }, cw: { type: 'string', nullable: true, minLength: 1, maxLength: 100 }, hashtag: { type: 'string', nullable: true, maxLength: 200 }, - localOnly: { type: 'boolean', default: false }, - reactionAcceptance: { type: 'string', nullable: true, enum: [null, 'likeOnly', 'likeOnlyForRemote', 'nonSensitiveOnly', 'nonSensitiveOnlyForLocalLikeOnlyForRemote'], default: null }, + localOnly: { type: 'boolean' }, + reactionAcceptance: { type: 'string', nullable: true, enum: [null, 'likeOnly', 'likeOnlyForRemote', 'nonSensitiveOnly', 'nonSensitiveOnlyForLocalLikeOnlyForRemote'] }, replyId: { type: 'string', format: 'misskey:id', nullable: true }, renoteId: { type: 'string', format: 'misskey:id', nullable: true }, channelId: { type: 'string', format: 'misskey:id', nullable: true }, @@ -194,7 +200,7 @@ export const paramDef = { fileIds: { type: 'array', uniqueItems: true, - minItems: 1, + minItems: 0, maxItems: 16, items: { type: 'string', format: 'misskey:id' }, }, @@ -215,6 +221,8 @@ export const paramDef = { }, required: ['choices'], }, + scheduledAt: { type: 'integer', nullable: true }, + isActuallyScheduled: { type: 'boolean' }, }, required: ['draftId'], } as const; @@ -228,22 +236,22 @@ export default class extends Endpoint { // eslint- super(meta, paramDef, async (ps, me) => { const draft = await this.noteDraftService.update(me, ps.draftId, { fileIds: ps.fileIds, - poll: ps.poll ? { - choices: ps.poll.choices, - multiple: ps.poll.multiple ?? false, - expiresAt: ps.poll.expiresAt ? new Date(ps.poll.expiresAt) : null, - expiredAfter: ps.poll.expiredAfter ?? null, - } : undefined, - text: ps.text ?? null, - replyId: ps.replyId ?? undefined, - renoteId: ps.renoteId ?? undefined, - cw: ps.cw ?? null, - ...(ps.hashtag ? { hashtag: ps.hashtag } : {}), + pollChoices: ps.poll?.choices, + pollMultiple: ps.poll?.multiple, + pollExpiresAt: ps.poll?.expiresAt ? new Date(ps.poll.expiresAt) : null, + pollExpiredAfter: ps.poll?.expiredAfter, + text: ps.text, + replyId: ps.replyId, + renoteId: ps.renoteId, + cw: ps.cw, + hashtag: ps.hashtag, localOnly: ps.localOnly, reactionAcceptance: ps.reactionAcceptance, visibility: ps.visibility, - visibleUserIds: ps.visibleUserIds ?? [], - channelId: ps.channelId ?? undefined, + visibleUserIds: ps.visibleUserIds, + channelId: ps.channelId, + scheduledAt: ps.scheduledAt ? new Date(ps.scheduledAt) : null, + isActuallyScheduled: ps.isActuallyScheduled, }).catch((err) => { if (err instanceof IdentifiableError) { switch (err.id) { @@ -285,6 +293,8 @@ export default class extends Endpoint { // eslint- throw new ApiError(meta.errors.containsProhibitedWords); case '4de0363a-3046-481b-9b0f-feff3e211025': throw new ApiError(meta.errors.containsTooManyMentions); + case 'bacdf856-5c51-4159-b88a-804fa5103be5': + throw new ApiError(meta.errors.tooManyScheduledNotes); default: throw err; } diff --git a/packages/backend/src/server/api/endpoints/notes/show.ts b/packages/backend/src/server/api/endpoints/notes/show.ts index cae0e752da..a41de25ddf 100644 --- a/packages/backend/src/server/api/endpoints/notes/show.ts +++ b/packages/backend/src/server/api/endpoints/notes/show.ts @@ -29,10 +29,16 @@ export const meta = { id: '24fcbfc6-2e37-42b6-8388-c29b3861a08d', }, - signinRequired: { - message: 'Signin required.', - code: 'SIGNIN_REQUIRED', - id: '8e75455b-738c-471d-9f80-62693f33372e', + contentRestrictedByUser: { + message: 'Content restricted by user. Please sign in to view.', + code: 'CONTENT_RESTRICTED_BY_USER', + id: 'fbcc002d-37d9-4944-a6b0-d9e29f2d33ab', + }, + + contentRestrictedByServer: { + message: 'Content restricted by server settings. Please sign in to view.', + code: 'CONTENT_RESTRICTED_BY_SERVER', + id: '145f88d2-b03d-4087-8143-a78928883c4b', }, }, } as const; @@ -61,15 +67,15 @@ export default class extends Endpoint { // eslint- }); if (note.user!.requireSigninToViewContents && me == null) { - throw new ApiError(meta.errors.signinRequired); + throw new ApiError(meta.errors.contentRestrictedByUser); } if (this.serverSettings.ugcVisibilityForVisitor === 'none' && me == null) { - throw new ApiError(meta.errors.signinRequired); + throw new ApiError(meta.errors.contentRestrictedByServer); } if (this.serverSettings.ugcVisibilityForVisitor === 'local' && note.userHost != null && me == null) { - throw new ApiError(meta.errors.signinRequired); + throw new ApiError(meta.errors.contentRestrictedByServer); } return await this.noteEntityService.pack(note, me, { diff --git a/packages/backend/src/server/api/endpoints/users/lists/show.ts b/packages/backend/src/server/api/endpoints/users/lists/show.ts index ed5952d4c5..c6d477a92f 100644 --- a/packages/backend/src/server/api/endpoints/users/lists/show.ts +++ b/packages/backend/src/server/api/endpoints/users/lists/show.ts @@ -22,17 +22,26 @@ export const meta = { res: { type: 'object', optional: false, nullable: false, - ref: 'UserList', - properties: { - likedCount: { - type: 'number', - optional: true, nullable: false, + allOf: [ + { + type: 'object', + ref: 'UserList', }, - isLiked: { - type: 'boolean', - optional: true, nullable: false, + { + type: 'object', + optional: false, nullable: false, + properties: { + likedCount: { + type: 'number', + optional: true, nullable: false, + }, + isLiked: { + type: 'boolean', + optional: true, nullable: false, + }, + }, }, - }, + ], }, errors: { diff --git a/packages/backend/src/server/web/boot.js b/packages/backend/src/server/web/boot.js index 0c0b46f82b..ab4b158287 100644 --- a/packages/backend/src/server/web/boot.js +++ b/packages/backend/src/server/web/boot.js @@ -13,7 +13,7 @@ }; window.onunhandledrejection = (e) => { console.error(e); - renderError('SOMETHING_HAPPENED_IN_PROMISE', e); + renderError('SOMETHING_HAPPENED_IN_PROMISE', e.reason || e); }; let forceError = localStorage.getItem('forceError'); diff --git a/packages/backend/src/types.ts b/packages/backend/src/types.ts index b20f2a2179..24654b0017 100644 --- a/packages/backend/src/types.ts +++ b/packages/backend/src/types.ts @@ -12,6 +12,8 @@ * quote - 投稿が引用Renoteされた * reaction - 投稿にリアクションされた * pollEnded - 自分のアンケートもしくは自分が投票したアンケートが終了した + * scheduledNotePosted - 予約したノートが投稿された + * scheduledNotePostFailed - 予約したノートの投稿に失敗した * receiveFollowRequest - フォローリクエストされた * followRequestAccepted - 自分の送ったフォローリクエストが承認された * roleAssigned - ロールが付与された @@ -32,6 +34,8 @@ export const notificationTypes = [ 'quote', 'reaction', 'pollEnded', + 'scheduledNotePosted', + 'scheduledNotePostFailed', 'receiveFollowRequest', 'followRequestAccepted', 'roleAssigned', diff --git a/packages/backend/test-federation/test/utils.ts b/packages/backend/test-federation/test/utils.ts index 7e24bb7904..056a16ba15 100644 --- a/packages/backend/test-federation/test/utils.ts +++ b/packages/backend/test-federation/test/utils.ts @@ -68,7 +68,6 @@ async function createAdmin(host: Host): Promise { ADMIN_CACHE.set(host, { id: res.id, - // @ts-expect-error FIXME: openapi-typescript generates incorrect response type for this endpoint, so ignore this i: res.token, }); return res as Misskey.entities.SignupResponse; diff --git a/packages/frontend-builder/package.json b/packages/frontend-builder/package.json index 5fdd25b32d..bdaf0d4027 100644 --- a/packages/frontend-builder/package.json +++ b/packages/frontend-builder/package.json @@ -11,15 +11,15 @@ }, "devDependencies": { "@types/estree": "1.0.8", - "@types/node": "22.17.0", - "@typescript-eslint/eslint-plugin": "8.38.0", - "@typescript-eslint/parser": "8.38.0", - "rollup": "4.46.2", + "@types/node": "22.18.6", + "@typescript-eslint/eslint-plugin": "8.44.1", + "@typescript-eslint/parser": "8.44.1", + "rollup": "4.52.2", "typescript": "5.9.2" }, "dependencies": { "estree-walker": "3.0.3", - "magic-string": "0.30.17", - "vite": "7.0.6" + "magic-string": "0.30.19", + "vite": "7.1.7" } } diff --git a/packages/frontend-embed/eslint.config.js b/packages/frontend-embed/eslint.config.js index 179d811e77..46247e40d5 100644 --- a/packages/frontend-embed/eslint.config.js +++ b/packages/frontend-embed/eslint.config.js @@ -46,9 +46,71 @@ export default [ allowSingleExtends: true, }], 'import/consistent-type-specifier-style': ['error', 'prefer-top-level'], - // window の禁止理由: グローバルスコープと衝突し、予期せぬ結果を招くため - // e の禁止理由: error や event など、複数のキーワードの頭文字であり分かりにくいため - 'id-denylist': ['error', 'window', 'e'], + // window ... グローバルスコープと衝突し、予期せぬ結果を招くため + // e ... error や event など、複数のキーワードの頭文字であり分かりにくいため + // close ... window.closeと衝突 or 紛らわしい + // open ... window.openと衝突 or 紛らわしい + // fetch ... window.fetchと衝突 or 紛らわしい + // location ... window.locationと衝突 or 紛らわしい + // document ... window.documentと衝突 or 紛らわしい + // history ... window.historyと衝突 or 紛らわしい + // scroll ... window.scrollと衝突 or 紛らわしい + // setTimeout ... window.setTimeoutと衝突 or 紛らわしい + // setInterval ... window.setIntervalと衝突 or 紛らわしい + // clearTimeout ... window.clearTimeoutと衝突 or 紛らわしい + // clearInterval ... window.clearIntervalと衝突 or 紛らわしい + 'id-denylist': ['error', 'window', 'e', 'close', 'open', 'fetch', 'location', 'document', 'history', 'scroll', 'setTimeout', 'setInterval', 'clearTimeout', 'clearInterval'], + 'no-restricted-globals': [ + 'error', + { + 'name': 'open', + 'message': 'Use `window.open`.', + }, + { + 'name': 'close', + 'message': 'Use `window.close`.', + }, + { + 'name': 'fetch', + 'message': 'Use `window.fetch`.', + }, + { + 'name': 'location', + 'message': 'Use `window.location`.', + }, + { + 'name': 'document', + 'message': 'Use `window.document`.', + }, + { + 'name': 'history', + 'message': 'Use `window.history`.', + }, + { + 'name': 'scroll', + 'message': 'Use `window.scroll`.', + }, + { + 'name': 'setTimeout', + 'message': 'Use `window.setTimeout`.', + }, + { + 'name': 'setInterval', + 'message': 'Use `window.setInterval`.', + }, + { + 'name': 'clearTimeout', + 'message': 'Use `window.clearTimeout`.', + }, + { + 'name': 'clearInterval', + 'message': 'Use `window.clearInterval`.', + }, + { + 'name': 'name', + 'message': 'Use `window.name`. もしくは name という変数名を定義し忘れている', + }, + ], 'no-shadow': ['warn'], 'vue/attributes-order': ['error', { alphabetical: false, diff --git a/packages/frontend-embed/package.json b/packages/frontend-embed/package.json index 2f5a3fc369..cd5e5071a6 100644 --- a/packages/frontend-embed/package.json +++ b/packages/frontend-embed/package.json @@ -16,7 +16,7 @@ "@rollup/pluginutils": "5.3.0", "@twemoji/parser": "16.0.0", "@vitejs/plugin-vue": "6.0.1", - "@vue/compiler-sfc": "3.5.21", + "@vue/compiler-sfc": "3.5.22", "astring": "1.9.0", "buraha": "0.0.1", "estree-walker": "3.0.3", @@ -26,47 +26,47 @@ "mfm-js": "0.25.0", "misskey-js": "workspace:*", "punycode.js": "2.3.1", - "rollup": "4.50.1", - "sass": "1.92.1", - "shiki": "3.12.2", + "rollup": "4.52.2", + "sass": "1.93.2", + "shiki": "3.13.0", "tinycolor2": "1.6.0", "tsc-alias": "1.8.16", "tsconfig-paths": "4.2.0", "typescript": "5.9.2", "uuid": "11.1.0", - "vite": "7.1.4", - "vue": "3.5.21" + "vite": "7.1.7", + "vue": "3.5.22" }, "devDependencies": { "@misskey-dev/summaly": "5.2.3", - "@tabler/icons-webfont": "3.34.1", + "@tabler/icons-webfont": "3.35.0", "@testing-library/vue": "8.1.0", "@types/estree": "1.0.8", "@types/micromatch": "4.0.9", - "@types/node": "22.18.1", + "@types/node": "22.18.6", "@types/punycode.js": "npm:@types/punycode@2.1.4", "@types/tinycolor2": "1.4.6", "@types/ws": "8.18.1", - "@typescript-eslint/eslint-plugin": "8.42.0", - "@typescript-eslint/parser": "8.42.0", + "@typescript-eslint/eslint-plugin": "8.44.1", + "@typescript-eslint/parser": "8.44.1", "@vitest/coverage-v8": "3.2.4", - "@vue/runtime-core": "3.5.21", + "@vue/runtime-core": "3.5.22", "acorn": "8.15.0", "cross-env": "10.0.0", "eslint-plugin-import": "2.32.0", - "eslint-plugin-vue": "10.4.0", + "eslint-plugin-vue": "10.5.0", "fast-glob": "3.3.3", "happy-dom": "18.0.1", "intersection-observer": "0.12.2", "micromatch": "4.0.8", - "msw": "2.11.1", + "msw": "2.11.3", "nodemon": "3.1.10", "prettier": "3.6.2", - "start-server-and-test": "2.1.0", - "tsx": "4.20.5", + "start-server-and-test": "2.1.2", + "tsx": "4.20.6", "vite-plugin-turbosnap": "1.0.3", - "vue-component-type-helpers": "3.0.6", + "vue-component-type-helpers": "3.0.8", "vue-eslint-parser": "10.2.0", - "vue-tsc": "3.0.6" + "vue-tsc": "3.0.8" } } diff --git a/packages/frontend-embed/src/boot.ts b/packages/frontend-embed/src/boot.ts index 9d69437c30..961cbcef66 100644 --- a/packages/frontend-embed/src/boot.ts +++ b/packages/frontend-embed/src/boot.ts @@ -33,7 +33,7 @@ import type { Theme } from '@/theme.js'; console.log('Misskey Embed'); //#region Embedパラメータの取得・パース -const params = new URLSearchParams(location.search); +const params = new URLSearchParams(window.location.search); const embedParams = parseEmbedParams(params); if (_DEV_) console.log(embedParams); //#endregion @@ -81,7 +81,7 @@ storeBootloaderErrors({ ...i18n.ts._bootErrors, reload: i18n.ts.reload }); //#endregion // サイズの制限 -document.documentElement.style.maxWidth = '500px'; +window.document.documentElement.style.maxWidth = '500px'; // iframeIdの設定 function setIframeIdHandler(event: MessageEvent) { @@ -114,16 +114,16 @@ app.provide(DI.embedParams, embedParams); const rootEl = ((): HTMLElement => { const MISSKEY_MOUNT_DIV_ID = 'misskey_app'; - const currentRoot = document.getElementById(MISSKEY_MOUNT_DIV_ID); + const currentRoot = window.document.getElementById(MISSKEY_MOUNT_DIV_ID); if (currentRoot) { console.warn('multiple import detected'); return currentRoot; } - const root = document.createElement('div'); + const root = window.document.createElement('div'); root.id = MISSKEY_MOUNT_DIV_ID; - document.body.appendChild(root); + window.document.body.appendChild(root); return root; })(); @@ -159,7 +159,7 @@ console.log(i18n.tsx._selfXssPrevention.description3({ link: 'https://misskey-hu //#endregion function removeSplash() { - const splash = document.getElementById('splash'); + const splash = window.document.getElementById('splash'); if (splash) { splash.style.opacity = '0'; splash.style.pointerEvents = 'none'; diff --git a/packages/frontend-embed/src/components/EmImgWithBlurhash.vue b/packages/frontend-embed/src/components/EmImgWithBlurhash.vue index 0bff048ce4..71f0ee9294 100644 --- a/packages/frontend-embed/src/components/EmImgWithBlurhash.vue +++ b/packages/frontend-embed/src/components/EmImgWithBlurhash.vue @@ -19,7 +19,7 @@ import { extractAvgColorFromBlurhash } from '@@/js/extract-avg-color-from-blurha const canvasPromise = new Promise(resolve => { // テスト環境で Web Worker インスタンスは作成できない if (import.meta.env.MODE === 'test') { - const canvas = document.createElement('canvas'); + const canvas = window.document.createElement('canvas'); canvas.width = 64; canvas.height = 64; resolve(canvas); @@ -34,7 +34,7 @@ const canvasPromise = new Promise(resol ); resolve(workers); } else { - const canvas = document.createElement('canvas'); + const canvas = window.document.createElement('canvas'); canvas.width = 64; canvas.height = 64; resolve(canvas); diff --git a/packages/frontend-embed/src/components/EmInstanceTicker.vue b/packages/frontend-embed/src/components/EmInstanceTicker.vue index 4a116e317a..7add3bb53f 100644 --- a/packages/frontend-embed/src/components/EmInstanceTicker.vue +++ b/packages/frontend-embed/src/components/EmInstanceTicker.vue @@ -29,7 +29,7 @@ const props = defineProps<{ // if no instance data is given, this is for the local instance const instance = props.instance ?? { name: serverMetadata.name, - themeColor: (document.querySelector('meta[name="theme-color-orig"]') as HTMLMetaElement)?.content, + themeColor: (window.document.querySelector('meta[name="theme-color-orig"]') as HTMLMetaElement)?.content, }; const faviconUrl = computed(() => props.instance ? mediaProxy.getProxiedImageUrlNullable(props.instance.faviconUrl, 'preview') : mediaProxy.getProxiedImageUrlNullable(serverMetadata.iconUrl, 'preview') ?? '/favicon.ico'); diff --git a/packages/frontend-embed/src/components/EmMention.vue b/packages/frontend-embed/src/components/EmMention.vue index b5aaa95894..0a8ac9c05a 100644 --- a/packages/frontend-embed/src/components/EmMention.vue +++ b/packages/frontend-embed/src/components/EmMention.vue @@ -27,7 +27,7 @@ const canonical = props.host === localHost ? `@${props.username}` : `@${props.us const url = `/${canonical}`; -const bg = tinycolor(getComputedStyle(document.documentElement).getPropertyValue('--MI_THEME-mention')); +const bg = tinycolor(getComputedStyle(window.document.documentElement).getPropertyValue('--MI_THEME-mention')); bg.setAlpha(0.1); const bgCss = bg.toRgbString(); diff --git a/packages/frontend-embed/src/components/EmPagination.vue b/packages/frontend-embed/src/components/EmPagination.vue index 94a91305f4..bd49d127a9 100644 --- a/packages/frontend-embed/src/components/EmPagination.vue +++ b/packages/frontend-embed/src/components/EmPagination.vue @@ -134,7 +134,7 @@ const isBackTop = ref(false); const empty = computed(() => items.value.size === 0); const error = ref(false); -const scrollableElement = computed(() => rootEl.value ? getScrollContainer(rootEl.value) : document.body); +const scrollableElement = computed(() => rootEl.value ? getScrollContainer(rootEl.value) : window.document.body); const visibility = useDocumentVisibility(); @@ -353,7 +353,7 @@ watch(visibility, () => { BACKGROUND_PAUSE_WAIT_SEC * 1000); } else { // 'visible' if (timerForSetPause) { - clearTimeout(timerForSetPause); + window.clearTimeout(timerForSetPause); timerForSetPause = null; } else { isPausingUpdate = false; @@ -447,11 +447,11 @@ onBeforeMount(() => { init().then(() => { if (props.pagination.reversed) { nextTick(() => { - setTimeout(toBottom, 800); + window.setTimeout(toBottom, 800); // scrollToBottomでmoreFetchingボタンが画面外まで出るまで // more = trueを遅らせる - setTimeout(() => { + window.setTimeout(() => { moreFetching.value = false; }, 2000); }); @@ -461,11 +461,11 @@ onBeforeMount(() => { onBeforeUnmount(() => { if (timerForSetPause) { - clearTimeout(timerForSetPause); + window.clearTimeout(timerForSetPause); timerForSetPause = null; } if (preventAppearFetchMoreTimer.value) { - clearTimeout(preventAppearFetchMoreTimer.value); + window.clearTimeout(preventAppearFetchMoreTimer.value); preventAppearFetchMoreTimer.value = null; } scrollObserver.value?.disconnect(); diff --git a/packages/frontend-embed/src/server-context.ts b/packages/frontend-embed/src/server-context.ts index a84a1a726a..c061d5a6f1 100644 --- a/packages/frontend-embed/src/server-context.ts +++ b/packages/frontend-embed/src/server-context.ts @@ -4,7 +4,7 @@ */ import * as Misskey from 'misskey-js'; -const providedContextEl = document.getElementById('misskey_embedCtx'); +const providedContextEl = window.document.getElementById('misskey_embedCtx'); export type ServerContext = { clip?: Misskey.entities.Clip; diff --git a/packages/frontend-embed/src/server-metadata.ts b/packages/frontend-embed/src/server-metadata.ts index 6c94aacd48..ad9b5a1a91 100644 --- a/packages/frontend-embed/src/server-metadata.ts +++ b/packages/frontend-embed/src/server-metadata.ts @@ -6,7 +6,7 @@ import * as Misskey from 'misskey-js'; import { misskeyApi } from '@/misskey-api.js'; -const providedMetaEl = document.getElementById('misskey_meta'); +const providedMetaEl = window.document.getElementById('misskey_meta'); const _serverMetadata: Misskey.entities.MetaDetailed | null = (providedMetaEl && providedMetaEl.textContent) ? JSON.parse(providedMetaEl.textContent) : null; diff --git a/packages/frontend-embed/src/theme.ts b/packages/frontend-embed/src/theme.ts index c9b1c0d0c6..c7bc5df85d 100644 --- a/packages/frontend-embed/src/theme.ts +++ b/packages/frontend-embed/src/theme.ts @@ -35,15 +35,15 @@ export function assertIsTheme(theme: Record): theme is Theme { export function applyTheme(theme: Theme, persist = true) { if (timeout) window.clearTimeout(timeout); - document.documentElement.classList.add('_themeChanging_'); + window.document.documentElement.classList.add('_themeChanging_'); timeout = window.setTimeout(() => { - document.documentElement.classList.remove('_themeChanging_'); + window.document.documentElement.classList.remove('_themeChanging_'); }, 1000); const colorScheme = theme.base === 'dark' ? 'dark' : 'light'; - document.documentElement.dataset.colorScheme = colorScheme; + window.document.documentElement.dataset.colorScheme = colorScheme; // Deep copy const _theme = JSON.parse(JSON.stringify(theme)); @@ -55,7 +55,7 @@ export function applyTheme(theme: Theme, persist = true) { const props = compile(_theme); - for (const tag of document.head.children) { + for (const tag of window.document.head.children) { if (tag.tagName === 'META' && tag.getAttribute('name') === 'theme-color') { tag.setAttribute('content', props['htmlThemeColor']); break; @@ -63,7 +63,7 @@ export function applyTheme(theme: Theme, persist = true) { } for (const [k, v] of Object.entries(props)) { - document.documentElement.style.setProperty(`--MI_THEME-${k}`, v.toString()); + window.document.documentElement.style.setProperty(`--MI_THEME-${k}`, v.toString()); } // iframeを正常に透過させるために、cssのcolor-schemeは `light dark;` 固定にしてある。style.scss参照 diff --git a/packages/frontend-embed/src/ui.vue b/packages/frontend-embed/src/ui.vue index 4ba5968a91..711d0eae6d 100644 --- a/packages/frontend-embed/src/ui.vue +++ b/packages/frontend-embed/src/ui.vue @@ -52,8 +52,8 @@ function safeURIDecode(str: string): string { } } -const page = location.pathname.split('/')[2]; -const contentId = safeURIDecode(location.pathname.split('/')[3]); +const page = window.location.pathname.split('/')[2]; +const contentId = safeURIDecode(window.location.pathname.split('/')[3]); if (_DEV_) console.log(page, contentId); const embedParams = inject(DI.embedParams, defaultEmbedParams); diff --git a/packages/frontend-embed/vite.config.ts b/packages/frontend-embed/vite.config.ts index 3ddee9b8a9..db4afb43a7 100644 --- a/packages/frontend-embed/vite.config.ts +++ b/packages/frontend-embed/vite.config.ts @@ -64,6 +64,8 @@ function toBase62(n: number): string { } export function getConfig(): UserConfig { + const localesHash = toBase62(hash(JSON.stringify(locales))); + return { base: '/embed_vite/', @@ -148,9 +150,9 @@ export function getConfig(): UserConfig { // dependencies of i18n.ts 'config': ['@@/js/config.js'], }, - entryFileNames: 'scripts/[hash:8].js', - chunkFileNames: 'scripts/[hash:8].js', - assetFileNames: 'assets/[hash:8][extname]', + entryFileNames: `scripts/${localesHash}-[hash:8].js`, + chunkFileNames: `scripts/${localesHash}-[hash:8].js`, + assetFileNames: `assets/${localesHash}-[hash:8][extname]`, paths(id) { for (const p of externalPackages) { if (p.match.test(id)) { diff --git a/packages/frontend-shared/eslint.config.js b/packages/frontend-shared/eslint.config.js index 6453be0042..b972cfdb27 100644 --- a/packages/frontend-shared/eslint.config.js +++ b/packages/frontend-shared/eslint.config.js @@ -51,9 +51,71 @@ export default [ allowSingleExtends: true, }], 'import/consistent-type-specifier-style': ['error', 'prefer-top-level'], - // window の禁止理由: グローバルスコープと衝突し、予期せぬ結果を招くため - // e の禁止理由: error や event など、複数のキーワードの頭文字であり分かりにくいため - 'id-denylist': ['error', 'window', 'e'], + // window ... グローバルスコープと衝突し、予期せぬ結果を招くため + // e ... error や event など、複数のキーワードの頭文字であり分かりにくいため + // close ... window.closeと衝突 or 紛らわしい + // open ... window.openと衝突 or 紛らわしい + // fetch ... window.fetchと衝突 or 紛らわしい + // location ... window.locationと衝突 or 紛らわしい + // document ... window.documentと衝突 or 紛らわしい + // history ... window.historyと衝突 or 紛らわしい + // scroll ... window.scrollと衝突 or 紛らわしい + // setTimeout ... window.setTimeoutと衝突 or 紛らわしい + // setInterval ... window.setIntervalと衝突 or 紛らわしい + // clearTimeout ... window.clearTimeoutと衝突 or 紛らわしい + // clearInterval ... window.clearIntervalと衝突 or 紛らわしい + 'id-denylist': ['error', 'window', 'e', 'close', 'open', 'fetch', 'location', 'document', 'history', 'scroll', 'setTimeout', 'setInterval', 'clearTimeout', 'clearInterval'], + 'no-restricted-globals': [ + 'error', + { + 'name': 'open', + 'message': 'Use `window.open`.', + }, + { + 'name': 'close', + 'message': 'Use `window.close`.', + }, + { + 'name': 'fetch', + 'message': 'Use `window.fetch`.', + }, + { + 'name': 'location', + 'message': 'Use `window.location`.', + }, + { + 'name': 'document', + 'message': 'Use `window.document`.', + }, + { + 'name': 'history', + 'message': 'Use `window.history`.', + }, + { + 'name': 'scroll', + 'message': 'Use `window.scroll`.', + }, + { + 'name': 'setTimeout', + 'message': 'Use `window.setTimeout`.', + }, + { + 'name': 'setInterval', + 'message': 'Use `window.setInterval`.', + }, + { + 'name': 'clearTimeout', + 'message': 'Use `window.clearTimeout`.', + }, + { + 'name': 'clearInterval', + 'message': 'Use `window.clearInterval`.', + }, + { + 'name': 'name', + 'message': 'Use `window.name`. もしくは name という変数名を定義し忘れている', + }, + ], 'no-shadow': ['warn'], 'vue/attributes-order': ['error', { alphabetical: false, diff --git a/packages/frontend-shared/js/config.ts b/packages/frontend-shared/js/config.ts index ac5c5629f3..6272d3f6b9 100644 --- a/packages/frontend-shared/js/config.ts +++ b/packages/frontend-shared/js/config.ts @@ -4,15 +4,15 @@ */ // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing -const address = new URL(document.querySelector('meta[property="instance_url"]')?.content || location.href); -const siteName = document.querySelector('meta[property="og:site_name"]')?.content; +const address = new URL(window.document.querySelector('meta[property="instance_url"]')?.content || window.location.href); +const siteName = window.document.querySelector('meta[property="og:site_name"]')?.content; export const host = address.host; export const hostname = address.hostname; export const url = address.origin; export const port = address.port; -export const apiUrl = location.origin + '/api'; -export const wsOrigin = location.origin; +export const apiUrl = window.location.origin + '/api'; +export const wsOrigin = window.location.origin; export const lang = localStorage.getItem('lang') ?? 'en-US'; export const langs = _LANGS_; export const version = _VERSION_; diff --git a/packages/frontend-shared/js/scroll.ts b/packages/frontend-shared/js/scroll.ts index 9057b896c6..5578cffdec 100644 --- a/packages/frontend-shared/js/scroll.ts +++ b/packages/frontend-shared/js/scroll.ts @@ -51,7 +51,7 @@ export function onScrollTop(el: HTMLElement, cb: (topVisible: boolean) => unknow // - toleranceの範囲内に収まる程度の微量なスクロールが発生した let prevTopVisible = firstTopVisible; const onScroll = () => { - if (!document.body.contains(el)) return; + if (!window.document.body.contains(el)) return; const topVisible = isHeadVisible(el, tolerance); if (topVisible !== prevTopVisible) { @@ -78,7 +78,7 @@ export function onScrollBottom(el: HTMLElement, cb: () => unknown, tolerance = 1 const containerOrWindow = container ?? window; const onScroll = () => { - if (!document.body.contains(el)) return; + if (!window.document.body.contains(el)) return; if (isTailVisible(el, 1, container)) { cb(); if (once) removeListener(); @@ -145,8 +145,8 @@ export function isTailVisible(el: HTMLElement, tolerance = 1, container = getScr // https://ja.javascript.info/size-and-scroll-window#ref-932 export function getBodyScrollHeight() { return Math.max( - document.body.scrollHeight, document.documentElement.scrollHeight, - document.body.offsetHeight, document.documentElement.offsetHeight, - document.body.clientHeight, document.documentElement.clientHeight, + window.document.body.scrollHeight, window.document.documentElement.scrollHeight, + window.document.body.offsetHeight, window.document.documentElement.offsetHeight, + window.document.body.clientHeight, window.document.documentElement.clientHeight, ); } diff --git a/packages/frontend-shared/js/use-document-visibility.ts b/packages/frontend-shared/js/use-document-visibility.ts index b1197e68da..a87c1f1bab 100644 --- a/packages/frontend-shared/js/use-document-visibility.ts +++ b/packages/frontend-shared/js/use-document-visibility.ts @@ -7,18 +7,18 @@ import { onMounted, onUnmounted, ref } from 'vue'; import type { Ref } from 'vue'; export function useDocumentVisibility(): Ref { - const visibility = ref(document.visibilityState); + const visibility = ref(window.document.visibilityState); const onChange = (): void => { - visibility.value = document.visibilityState; + visibility.value = window.document.visibilityState; }; onMounted(() => { - document.addEventListener('visibilitychange', onChange); + window.document.addEventListener('visibilitychange', onChange); }); onUnmounted(() => { - document.removeEventListener('visibilitychange', onChange); + window.document.removeEventListener('visibilitychange', onChange); }); return visibility; diff --git a/packages/frontend-shared/package.json b/packages/frontend-shared/package.json index aebc418e3c..46f39496b1 100644 --- a/packages/frontend-shared/package.json +++ b/packages/frontend-shared/package.json @@ -21,11 +21,11 @@ "lint": "pnpm typecheck && pnpm eslint" }, "devDependencies": { - "@types/node": "22.18.1", - "@typescript-eslint/eslint-plugin": "8.42.0", - "@typescript-eslint/parser": "8.42.0", - "esbuild": "0.25.9", - "eslint-plugin-vue": "10.4.0", + "@types/node": "22.18.6", + "@typescript-eslint/eslint-plugin": "8.44.1", + "@typescript-eslint/parser": "8.44.1", + "esbuild": "0.25.10", + "eslint-plugin-vue": "10.5.0", "nodemon": "3.1.10", "typescript": "5.9.2", "vue-eslint-parser": "10.2.0" @@ -35,6 +35,6 @@ ], "dependencies": { "misskey-js": "workspace:*", - "vue": "3.5.21" + "vue": "3.5.22" } } diff --git a/packages/frontend/package.json b/packages/frontend/package.json index d701189a7e..0200269fcd 100644 --- a/packages/frontend/package.json +++ b/packages/frontend/package.json @@ -24,12 +24,12 @@ "@rollup/plugin-json": "6.1.0", "@rollup/plugin-replace": "6.0.2", "@rollup/pluginutils": "5.3.0", - "@sentry/vue": "10.10.0", - "@syuilo/aiscript": "1.1.0", + "@sentry/vue": "10.15.0", + "@syuilo/aiscript": "1.1.2", "@syuilo/aiscript-0-19-0": "npm:@syuilo/aiscript@^0.19.0", "@twemoji/parser": "16.0.0", "@vitejs/plugin-vue": "6.0.1", - "@vue/compiler-sfc": "3.5.21", + "@vue/compiler-sfc": "3.5.22", "aiscript-vscode": "github:aiscript-dev/aiscript-vscode#v0.1.15", "analytics": "0.8.19", "astring": "1.9.0", @@ -41,7 +41,7 @@ "chartjs-chart-matrix": "3.0.0", "chartjs-plugin-gradient": "0.6.1", "chartjs-plugin-zoom": "2.2.0", - "chromatic": "13.1.4", + "chromatic": "13.2.1", "compare-versions": "6.1.1", "cropperjs": "2.0.1", "date-fns": "4.1.0", @@ -52,21 +52,24 @@ "icons-subsetter": "workspace:*", "idb-keyval": "6.2.2", "insert-text-at-cursor": "0.3.0", - "ios-haptics": "0.1.0", + "ios-haptics": "0.1.4", "is-file-animated": "1.0.2", "json5": "2.2.3", - "magic-string": "0.30.18", + "magic-string": "0.30.19", "matter-js": "0.20.0", + "mediabunny": "1.21.0", "mfm-js": "0.25.0", "misskey-bubble-game": "workspace:*", "misskey-js": "workspace:*", "misskey-reversi": "workspace:*", "photoswipe": "5.4.4", "punycode.js": "2.3.1", - "rollup": "4.50.1", + "qr-code-styling": "1.9.2", + "qr-scanner": "1.4.2", + "rollup": "4.52.2", "sanitize-html": "2.17.0", - "sass": "1.92.1", - "shiki": "3.12.2", + "sass": "1.93.2", + "shiki": "3.13.0", "strict-event-emitter-types": "2.0.0", "textarea-caret": "3.1.0", "three": "0.180.0", @@ -76,8 +79,8 @@ "tsconfig-paths": "4.2.0", "typescript": "5.9.2", "v-code-diff": "1.13.1", - "vite": "7.1.4", - "vue": "3.5.21", + "vite": "7.1.7", + "vue": "3.5.22", "vuedraggable": "next", "wanakana": "5.3.1" }, @@ -85,7 +88,7 @@ "@misskey-dev/summaly": "5.2.3", "@storybook/addon-essentials": "8.6.14", "@storybook/addon-interactions": "8.6.14", - "@storybook/addon-links": "9.1.5", + "@storybook/addon-links": "9.1.8", "@storybook/addon-mdx-gfm": "8.6.14", "@storybook/addon-storysource": "8.6.14", "@storybook/blocks": "8.6.14", @@ -93,57 +96,57 @@ "@storybook/core-events": "8.6.14", "@storybook/manager-api": "8.6.14", "@storybook/preview-api": "8.6.14", - "@storybook/react": "9.1.5", - "@storybook/react-vite": "9.1.5", + "@storybook/react": "9.1.8", + "@storybook/react-vite": "9.1.8", "@storybook/test": "8.6.14", "@storybook/theming": "8.6.14", "@storybook/types": "8.6.14", - "@storybook/vue3": "9.1.5", - "@storybook/vue3-vite": "9.1.5", - "@tabler/icons-webfont": "3.34.1", + "@storybook/vue3": "9.1.8", + "@storybook/vue3-vite": "9.1.8", + "@tabler/icons-webfont": "3.35.0", "@testing-library/vue": "8.1.0", "@types/canvas-confetti": "1.9.0", "@types/estree": "1.0.8", - "@types/matter-js": "0.20.0", + "@types/matter-js": "0.20.2", "@types/micromatch": "4.0.9", - "@types/node": "22.18.1", + "@types/node": "22.18.6", "@types/punycode.js": "npm:@types/punycode@2.1.4", "@types/sanitize-html": "2.16.0", "@types/seedrandom": "3.0.8", "@types/throttle-debounce": "5.0.2", "@types/tinycolor2": "1.4.6", "@types/ws": "8.18.1", - "@typescript-eslint/eslint-plugin": "8.42.0", - "@typescript-eslint/parser": "8.42.0", + "@typescript-eslint/eslint-plugin": "8.44.1", + "@typescript-eslint/parser": "8.44.1", "@vitest/coverage-v8": "3.2.4", - "@vue/compiler-core": "3.5.21", - "@vue/runtime-core": "3.5.21", + "@vue/compiler-core": "3.5.22", + "@vue/runtime-core": "3.5.22", "acorn": "8.15.0", "cross-env": "10.0.0", "cypress": "14.5.4", "eslint-plugin-import": "2.32.0", - "eslint-plugin-vue": "10.4.0", + "eslint-plugin-vue": "10.5.0", "fast-glob": "3.3.3", "happy-dom": "18.0.1", "intersection-observer": "0.12.2", "micromatch": "4.0.8", "minimatch": "10.0.3", - "msw": "2.11.1", + "msw": "2.11.3", "msw-storybook-addon": "2.0.5", "nodemon": "3.1.10", "prettier": "3.6.2", "react": "19.1.1", "react-dom": "19.1.1", "seedrandom": "3.0.5", - "start-server-and-test": "2.1.0", - "storybook": "9.1.5", + "start-server-and-test": "2.1.2", + "storybook": "9.1.8", "storybook-addon-misskey-theme": "github:misskey-dev/storybook-addon-misskey-theme", - "tsx": "4.20.5", + "tsx": "4.20.6", "vite-plugin-turbosnap": "1.0.3", "vitest": "3.2.4", "vitest-fetch-mock": "0.4.5", - "vue-component-type-helpers": "3.0.6", + "vue-component-type-helpers": "3.0.8", "vue-eslint-parser": "10.2.0", - "vue-tsc": "3.0.6" + "vue-tsc": "3.0.8" } } diff --git a/packages/frontend/src/boot/common.ts b/packages/frontend/src/boot/common.ts index 574012ff78..4becf32ab5 100644 --- a/packages/frontend/src/boot/common.ts +++ b/packages/frontend/src/boot/common.ts @@ -151,7 +151,21 @@ export async function common(createVue: () => Promise>) { } //#endregion + //#region Sync dark mode + if (prefer.s.syncDeviceDarkMode) { + store.set('darkMode', isDeviceDarkmode()); + } + + window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', (mql) => { + if (prefer.s.syncDeviceDarkMode) { + store.set('darkMode', mql.matches); + } + }); + //#endregion + // NOTE: この処理は必ずクライアント更新チェック処理より後に来ること(テーマ再構築のため) + // NOTE: この処理は必ずダークモード判定処理より後に来ること(初回のテーマ適用のため) + // see: https://github.com/misskey-dev/misskey/issues/16562 watch(store.r.darkMode, (darkMode) => { const theme = (() => { if (darkMode) { @@ -183,18 +197,6 @@ export async function common(createVue: () => Promise>) { }); } - //#region Sync dark mode - if (prefer.s.syncDeviceDarkMode) { - store.set('darkMode', isDeviceDarkmode()); - } - - window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', (mql) => { - if (prefer.s.syncDeviceDarkMode) { - store.set('darkMode', mql.matches); - } - }); - //#endregion - if (!isSafeMode) { if (prefer.s.darkTheme && store.s.darkMode) { if (miLocalStorage.getItem('themeId') !== prefer.s.darkTheme.id) applyTheme(prefer.s.darkTheme); diff --git a/packages/frontend/src/components/MkAnimBg.vue b/packages/frontend/src/components/MkAnimBg.vue index 19a21f6e24..0e1018dcbf 100644 --- a/packages/frontend/src/components/MkAnimBg.vue +++ b/packages/frontend/src/components/MkAnimBg.vue @@ -10,6 +10,7 @@ SPDX-License-Identifier: AGPL-3.0-only diff --git a/packages/frontend/src/components/MkDialog.vue b/packages/frontend/src/components/MkDialog.vue index 3f7519a43f..705301a6a6 100644 --- a/packages/frontend/src/components/MkDialog.vue +++ b/packages/frontend/src/components/MkDialog.vue @@ -29,16 +29,7 @@ SPDX-License-Identifier: AGPL-3.0-only - - - +
{{ okText ?? ((showCancelButton || input || select) ? i18n.ts.ok : i18n.ts.gotIt) }} {{ cancelText ?? i18n.ts.cancel }} @@ -56,6 +47,8 @@ import MkModal from '@/components/MkModal.vue'; import MkButton from '@/components/MkButton.vue'; import MkInput from '@/components/MkInput.vue'; import MkSelect from '@/components/MkSelect.vue'; +import type { MkSelectItem, OptionValue } from '@/components/MkSelect.vue'; +import { useMkSelect } from '@/composables/use-mkselect.js'; import { i18n } from '@/i18n.js'; type Input = { @@ -67,17 +60,9 @@ type Input = { maxLength?: number; }; -type SelectItem = { - value: any; - text: string; -}; - type Select = { - items: (SelectItem | { - sectionTitle: string; - items: SelectItem[]; - })[]; - default: string | null; + items: MkSelectItem[]; + default: OptionValue | null; }; type Result = string | number | true | null; @@ -115,7 +100,6 @@ const emit = defineEmits<{ const modal = useTemplateRef('modal'); const inputValue = ref(props.input?.default ?? null); -const selectedValue = ref(props.select?.default ?? null); const okButtonDisabledReason = computed(() => { if (props.input) { @@ -134,6 +118,14 @@ const okButtonDisabledReason = computed props.select?.items ?? []), + initialValue: props.select?.default ?? null, +}); + // overload function を使いたいので lint エラーを無視する function done(canceled: true): void; function done(canceled: false, result: Result): void; // eslint-disable-line no-redeclare diff --git a/packages/frontend/src/components/MkEmbedCodeGenDialog.vue b/packages/frontend/src/components/MkEmbedCodeGenDialog.vue index 17823deb85..0cb8499699 100644 --- a/packages/frontend/src/components/MkEmbedCodeGenDialog.vue +++ b/packages/frontend/src/components/MkEmbedCodeGenDialog.vue @@ -52,11 +52,8 @@ SPDX-License-Identifier: AGPL-3.0-only - + - - - {{ i18n.ts._embedCodeGen.header }} {{ i18n.ts._embedCodeGen.rounded }} @@ -105,6 +102,7 @@ import MkInfo from '@/components/MkInfo.vue'; import * as os from '@/os.js'; import { i18n } from '@/i18n.js'; +import { useMkSelect } from '@/composables/use-mkselect.js'; import { copyToClipboard } from '@/utility/copy-to-clipboard.js'; import { normalizeEmbedParams, getEmbedCode } from '@/utility/get-embed-code.js'; @@ -162,7 +160,18 @@ const isEmbedWithScrollbar = computed(() => embedRouteWithScrollbar.includes(pro const header = ref(props.params?.header ?? true); const maxHeight = ref(props.params?.maxHeight !== 0 ? props.params?.maxHeight ?? null : 500); -const colorMode = ref<'light' | 'dark' | 'auto'>(props.params?.colorMode ?? 'auto'); +const { + model: colorMode, + def: colorModeDef, +} = useMkSelect({ + items: [ + { value: 'auto', label: i18n.ts.syncDeviceDarkMode }, + { value: 'light', label: i18n.ts.light }, + { value: 'dark', label: i18n.ts.dark }, + ], + initialValue: props.params?.colorMode ?? 'auto', +}); + const rounded = ref(props.params?.rounded ?? true); const border = ref(props.params?.border ?? true); diff --git a/packages/frontend/src/components/MkEmojiPicker.vue b/packages/frontend/src/components/MkEmojiPicker.vue index 6904c417ce..4ac65a5f45 100644 --- a/packages/frontend/src/components/MkEmojiPicker.vue +++ b/packages/frontend/src/components/MkEmojiPicker.vue @@ -530,6 +530,14 @@ defineExpose({ --eachSize: 50px; } + &.s4 { + --eachSize: 55px; + } + + &.s5 { + --eachSize: 60px; + } + &.w1 { width: calc((var(--eachSize) * 5) + (#{$pad} * 2)); --columns: 1fr 1fr 1fr 1fr 1fr; diff --git a/packages/frontend/src/components/MkFormDialog.vue b/packages/frontend/src/components/MkFormDialog.vue index 8d697499a5..142ccb12a3 100644 --- a/packages/frontend/src/components/MkFormDialog.vue +++ b/packages/frontend/src/components/MkFormDialog.vue @@ -39,9 +39,8 @@ SPDX-License-Identifier: AGPL-3.0-only - + - @@ -77,7 +76,8 @@ import MkRange from './MkRange.vue'; import MkButton from './MkButton.vue'; import MkRadios from './MkRadios.vue'; import XFile from './MkFormDialog.file.vue'; -import type { EnumItem, Form, RadioFormItem } from '@/utility/form.js'; +import type { MkSelectItem } from '@/components/MkSelect.vue'; +import type { Form, EnumFormItem, RadioFormItem } from '@/utility/form.js'; import MkModalWindow from '@/components/MkModalWindow.vue'; import { i18n } from '@/i18n.js'; @@ -120,16 +120,14 @@ function cancel() { dialog.value?.close(); } -function getEnumLabel(e: EnumItem) { - return typeof e === 'string' ? e : e.label; -} - -function getEnumValue(e: EnumItem) { - return typeof e === 'string' ? e : e.value; -} - -function getEnumKey(e: EnumItem) { - return typeof e === 'string' ? e : typeof e.value === 'string' ? e.value : JSON.stringify(e.value); +function getMkSelectDef(def: EnumFormItem): MkSelectItem[] { + return def.enum.map((v) => { + if (typeof v === 'string') { + return { value: v, label: v }; + } else { + return { value: v.value, label: v.label }; + } + }); } function getRadioKey(e: RadioFormItem['options'][number]) { diff --git a/packages/frontend/src/components/MkImageEffectorDialog.vue b/packages/frontend/src/components/MkImageEffectorDialog.vue index 2c6185fd33..5ce514f93e 100644 --- a/packages/frontend/src/components/MkImageEffectorDialog.vue +++ b/packages/frontend/src/components/MkImageEffectorDialog.vue @@ -19,9 +19,12 @@ SPDX-License-Identifier: AGPL-3.0-only
- +
{{ i18n.ts.preview }}
+
+ +
@@ -212,6 +215,147 @@ watch(enabled, () => { renderer.render(); } }); + +const penMode = ref<'fill' | 'blur' | 'pixelate' | null>(null); + +function showPenMenu(ev: MouseEvent) { + os.popupMenu([{ + text: i18n.ts._imageEffector._fxs.fill, + action: () => { + penMode.value = 'fill'; + }, + }, { + text: i18n.ts._imageEffector._fxs.blur, + action: () => { + penMode.value = 'blur'; + }, + }, { + text: i18n.ts._imageEffector._fxs.pixelate, + action: () => { + penMode.value = 'pixelate'; + }, + }], ev.currentTarget ?? ev.target); +} + +function onImagePointerdown(ev: PointerEvent) { + if (canvasEl.value == null || imageBitmap == null || penMode.value == null) return; + + const AW = canvasEl.value.clientWidth; + const AH = canvasEl.value.clientHeight; + const BW = imageBitmap.width; + const BH = imageBitmap.height; + + let xOffset = 0; + let yOffset = 0; + + if (AW / AH < BW / BH) { // 横長 + yOffset = AH - BH * (AW / BW); + } else { // 縦長 + xOffset = AW - BW * (AH / BH); + } + + xOffset /= 2; + yOffset /= 2; + + let startX = ev.offsetX - xOffset; + let startY = ev.offsetY - yOffset; + + if (AW / AH < BW / BH) { // 横長 + startX = startX / (Math.max(AW, AH) / Math.max(BH / BW, 1)); + startY = startY / (Math.max(AW, AH) / Math.max(BW / BH, 1)); + } else { // 縦長 + startX = startX / (Math.min(AW, AH) / Math.max(BH / BW, 1)); + startY = startY / (Math.min(AW, AH) / Math.max(BW / BH, 1)); + } + + const id = genId(); + if (penMode.value === 'fill') { + layers.push({ + id, + fxId: 'fill', + params: { + offsetX: 0, + offsetY: 0, + scaleX: 0.1, + scaleY: 0.1, + angle: 0, + opacity: 1, + color: [1, 1, 1], + }, + }); + } else if (penMode.value === 'blur') { + layers.push({ + id, + fxId: 'blur', + params: { + offsetX: 0, + offsetY: 0, + scaleX: 0.1, + scaleY: 0.1, + angle: 0, + radius: 3, + }, + }); + } else if (penMode.value === 'pixelate') { + layers.push({ + id, + fxId: 'pixelate', + params: { + offsetX: 0, + offsetY: 0, + scaleX: 0.1, + scaleY: 0.1, + angle: 0, + strength: 0.2, + }, + }); + } + + _move(ev.offsetX, ev.offsetY); + + function _move(pointerX: number, pointerY: number) { + let x = pointerX - xOffset; + let y = pointerY - yOffset; + + if (AW / AH < BW / BH) { // 横長 + x = x / (Math.max(AW, AH) / Math.max(BH / BW, 1)); + y = y / (Math.max(AW, AH) / Math.max(BW / BH, 1)); + } else { // 縦長 + x = x / (Math.min(AW, AH) / Math.max(BH / BW, 1)); + y = y / (Math.min(AW, AH) / Math.max(BW / BH, 1)); + } + + const scaleX = Math.abs(x - startX); + const scaleY = Math.abs(y - startY); + + const layerIndex = layers.findIndex((l) => l.id === id); + const layer = layerIndex !== -1 ? layers[layerIndex] : null; + if (layer != null) { + layer.params.offsetX = (x + startX) - 1; + layer.params.offsetY = (y + startY) - 1; + layer.params.scaleX = scaleX; + layer.params.scaleY = scaleY; + layers[layerIndex] = layer; + } + } + + function move(ev: PointerEvent) { + _move(ev.offsetX, ev.offsetY); + } + + function up() { + canvasEl.value?.removeEventListener('pointermove', move); + canvasEl.value?.removeEventListener('pointerup', up); + canvasEl.value?.removeEventListener('pointercancel', up); + canvasEl.value?.releasePointerCapture(ev.pointerId); + + penMode.value = null; + } + + canvasEl.value.addEventListener('pointermove', move); + canvasEl.value.addEventListener('pointerup', up); + canvasEl.value.setPointerCapture(ev.pointerId); +} diff --git a/packages/frontend/src/components/MkNotification.vue b/packages/frontend/src/components/MkNotification.vue index 21104b41df..45a74e3f02 100644 --- a/packages/frontend/src/components/MkNotification.vue +++ b/packages/frontend/src/components/MkNotification.vue @@ -7,7 +7,7 @@ SPDX-License-Identifier: AGPL-3.0-only
- +
@@ -23,6 +23,8 @@ SPDX-License-Identifier: AGPL-3.0-only [$style.t_mention]: notification.type === 'mention', [$style.t_quote]: notification.type === 'quote', [$style.t_pollEnded]: notification.type === 'pollEnded', + [$style.t_scheduledNotePosted]: notification.type === 'scheduledNotePosted', + [$style.t_scheduledNotePostFailed]: notification.type === 'scheduledNotePostFailed', [$style.t_achievementEarned]: notification.type === 'achievementEarned', [$style.t_exportCompleted]: notification.type === 'exportCompleted', [$style.t_login]: notification.type === 'login', @@ -39,6 +41,8 @@ SPDX-License-Identifier: AGPL-3.0-only + + @@ -60,6 +64,8 @@ SPDX-License-Identifier: AGPL-3.0-only
{{ i18n.ts._notification.pollEnded }} + {{ i18n.ts._notification.scheduledNotePosted }} + {{ i18n.ts._notification.scheduledNotePostFailed }} {{ i18n.ts._notification.newNote }}: {{ i18n.ts._notification.roleAssigned }} {{ i18n.ts._notification.chatRoomInvitationReceived }} @@ -103,6 +109,11 @@ SPDX-License-Identifier: AGPL-3.0-only + + + + +
{{ notification.role.name }}
@@ -338,6 +349,16 @@ function getActualReactedUsersCount(notification: Misskey.entities.Notification) pointer-events: none; } +.t_scheduledNotePosted { + background: var(--eventOther); + pointer-events: none; +} + +.t_scheduledNotePostFailed { + background: var(--eventOther); + pointer-events: none; +} + .t_achievementEarned { background: var(--eventAchievement); pointer-events: none; diff --git a/packages/frontend/src/components/MkPaginationControl.vue b/packages/frontend/src/components/MkPaginationControl.vue index 10bed575a4..55aa3f2dc2 100644 --- a/packages/frontend/src/components/MkPaginationControl.vue +++ b/packages/frontend/src/components/MkPaginationControl.vue @@ -6,7 +6,7 @@ SPDX-License-Identifier: AGPL-3.0-only @@ -1516,6 +1588,10 @@ html[data-color-scheme=light] .preview { margin: 0 20px 16px 20px; } +.scheduledAt { + margin: 0 20px 16px 20px; +} + .cw, .hashtags, .text { diff --git a/packages/frontend/src/components/MkPostFormDialog.vue b/packages/frontend/src/components/MkPostFormDialog.vue index bf332e706e..ba8d3a7210 100644 --- a/packages/frontend/src/components/MkPostFormDialog.vue +++ b/packages/frontend/src/components/MkPostFormDialog.vue @@ -54,6 +54,7 @@ function onPosted() { async function _close() { const canClose = await form.value?.canClose(); if (!canClose) return; + form.value?.abortUploader(); modal.value?.close(); } diff --git a/packages/frontend/src/components/MkPushNotificationAllowButton.vue b/packages/frontend/src/components/MkPushNotificationAllowButton.vue index 9c37eb5e72..697346020c 100644 --- a/packages/frontend/src/components/MkPushNotificationAllowButton.vue +++ b/packages/frontend/src/components/MkPushNotificationAllowButton.vue @@ -90,7 +90,7 @@ function subscribe() { publickey: encode(subscription.getKey('p256dh')), }); }, async err => { // When subscribe failed - // 通知が許可されていなかったとき + // 通知が許可されていなかったとき if (err?.name === 'NotAllowedError') { console.info('User denied the notification permission request.'); return; @@ -114,14 +114,13 @@ async function unsubscribe() { if ($i && accounts.length >= 2) { apiWithDialog('sw/unregister', { - i: $i.token, endpoint, - }); + }, $i.token); } else { pushSubscription.value.unsubscribe(); apiWithDialog('sw/unregister', { endpoint, - }); + }, null); pushSubscription.value = null; } } @@ -134,7 +133,7 @@ function encode(buffer: ArrayBuffer | null) { * Convert the URL safe base64 string to a Uint8Array * @param base64String base64 string */ -function urlBase64ToUint8Array(base64String: string): Uint8Array { +function urlBase64ToUint8Array(base64String: string): BufferSource { const padding = '='.repeat((4 - base64String.length % 4) % 4); const base64 = (base64String + padding) .replace(/-/g, '+') diff --git a/packages/frontend/src/components/MkRolePreview.vue b/packages/frontend/src/components/MkRolePreview.vue index 15149b3f0c..8e5cbde8c3 100644 --- a/packages/frontend/src/components/MkRolePreview.vue +++ b/packages/frontend/src/components/MkRolePreview.vue @@ -6,7 +6,7 @@ SPDX-License-Identifier: AGPL-3.0-only {{ role.name }} - - - diff --git a/packages/frontend/src/components/MkTabs.vue b/packages/frontend/src/components/MkTabs.vue index 57fb6548ba..9798e2c3b3 100644 --- a/packages/frontend/src/components/MkTabs.vue +++ b/packages/frontend/src/components/MkTabs.vue @@ -4,12 +4,20 @@ SPDX-License-Identifier: AGPL-3.0-only --> - diff --git a/packages/frontend/src/pages/qr.show.vue b/packages/frontend/src/pages/qr.show.vue new file mode 100644 index 0000000000..28f80e0963 --- /dev/null +++ b/packages/frontend/src/pages/qr.show.vue @@ -0,0 +1,234 @@ + + + + + + + + + diff --git a/packages/frontend/src/pages/qr.vue b/packages/frontend/src/pages/qr.vue new file mode 100644 index 0000000000..2e5629f232 --- /dev/null +++ b/packages/frontend/src/pages/qr.vue @@ -0,0 +1,57 @@ + + + + + + + diff --git a/packages/frontend/src/pages/reversi/game.board.vue b/packages/frontend/src/pages/reversi/game.board.vue index 69429728d0..aae638641a 100644 --- a/packages/frontend/src/pages/reversi/game.board.vue +++ b/packages/frontend/src/pages/reversi/game.board.vue @@ -164,7 +164,7 @@ const $i = ensureSignin(); const props = defineProps<{ game: Misskey.entities.ReversiGameDetailed; - connection?: Misskey.ChannelConnection | null; + connection?: Misskey.IChannelConnection | null; }>(); const showBoardLabels = ref(false); diff --git a/packages/frontend/src/pages/reversi/game.setting.vue b/packages/frontend/src/pages/reversi/game.setting.vue index 8392384963..1e01496bbb 100644 --- a/packages/frontend/src/pages/reversi/game.setting.vue +++ b/packages/frontend/src/pages/reversi/game.setting.vue @@ -132,7 +132,7 @@ const mapCategories = Array.from(new Set(Object.values(Reversi.maps).map(x => x. const props = defineProps<{ game: Misskey.entities.ReversiGameDetailed; - connection: Misskey.ChannelConnection; + connection: Misskey.IChannelConnection; }>(); const shareWhenStart = defineModel('shareWhenStart', { default: false }); diff --git a/packages/frontend/src/pages/reversi/game.vue b/packages/frontend/src/pages/reversi/game.vue index a447572cc0..b1ba4da247 100644 --- a/packages/frontend/src/pages/reversi/game.vue +++ b/packages/frontend/src/pages/reversi/game.vue @@ -33,7 +33,7 @@ const props = defineProps<{ }>(); const game = shallowRef(null); -const connection = shallowRef(null); +const connection = shallowRef | null>(null); const shareWhenStart = ref(false); watch(() => props.gameId, () => { diff --git a/packages/frontend/src/pages/settings/2fa.vue b/packages/frontend/src/pages/settings/2fa.vue index ca404b43c4..2cc13744b1 100644 --- a/packages/frontend/src/pages/settings/2fa.vue +++ b/packages/frontend/src/pages/settings/2fa.vue @@ -196,6 +196,7 @@ async function addSecurityKey() { if (auth.canceled) return; const registrationOptions = parseCreationOptionsFromJSON({ + // @ts-expect-error misskey-js側に型がない publicKey: await os.apiWithDialog('i/2fa/register-key', { password: auth.result.password, token: auth.result.token, @@ -226,6 +227,7 @@ async function addSecurityKey() { password: auth.result.password, token: auth.result.token, name: name.result, + // @ts-expect-error misskey-js側に型がない credential: credential.toJSON(), }); } diff --git a/packages/frontend/src/pages/settings/drive-cleaner.vue b/packages/frontend/src/pages/settings/drive-cleaner.vue index 63b3c95233..57192c0fb7 100644 --- a/packages/frontend/src/pages/settings/drive-cleaner.vue +++ b/packages/frontend/src/pages/settings/drive-cleaner.vue @@ -5,9 +5,8 @@ SPDX-License-Identifier: AGPL-3.0-only