Compare commits

...

93 Commits

Author SHA1 Message Date
syuilo f8bb504e9f
Merge b59cd38190 into 7ea4cad12e 2025-10-04 15:57:51 +09:00
renovate[bot] 7ea4cad12e
chore(deps): update [misskey-js] update dependencies [skip ci] (#16543)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-10-04 15:44:08 +09:00
github-actions[bot] d864e9a269 Bump version to 2025.10.0-beta.0 2025-10-04 06:40:01 +00:00
syuilo 4e0434c275
Update CHANGELOG with new features and enhancements 2025-10-04 15:38:05 +09:00
renovate[bot] e2f939080a
fix(deps): update [frontend] update dependencies [ci skip] (#16548)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-10-04 15:26:26 +09:00
renovate[bot] 6956f44d1f
chore(deps): update [github actions] update dependencies (#16545)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-10-04 15:18:21 +09:00
renovate[bot] a393d5a87e
fix(deps): update [backend] update dependencies (#16547)
* fix(deps): update [backend] update dependencies

* chore: update typeorm.patch

* fix: handle socket creation failure in HttpRequestServiceAgent

---------

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: anatawa12 <anatawa12@icloud.com>
Co-authored-by: kakkokari-gtyih <67428053+kakkokari-gtyih@users.noreply.github.com>
2025-10-04 15:04:28 +09:00
syuilo b59cd38190 New translations ja-jp.yml (Chinese Simplified) 2025-10-04 14:38:35 +09:00
syuilo bbd87bbd01
Merge branch 'develop' into l10n_develop 2025-10-04 14:23:59 +09:00
syuilo b9552c253e New translations ja-jp.yml (Chinese Simplified) 2025-10-04 13:24:03 +09:00
syuilo 6c634de482
Bump version to 2025.10.0 in CHANGELOG
Updated version number and note for pnpm requirement.
2025-10-04 09:50:58 +09:00
syuilo fc02e0d34d chore(frontend): make enableFolderPageView false by default
see #16553
2025-10-04 08:54:49 +09:00
syuilo cb1a90ddad chore(frontend): improve usability 2025-10-04 08:53:19 +09:00
syuilo f76c3dd078 New translations ja-jp.yml (Portuguese) 2025-10-03 23:21:54 +09:00
syuilo a98b1349a6 New translations ja-jp.yml (Portuguese) 2025-10-03 21:56:25 +09:00
syuilo 6f1bbad3d8 New translations ja-jp.yml (Thai) 2025-10-03 00:47:55 +09:00
syuilo 4953ed7c10 New translations ja-jp.yml (Thai) 2025-10-02 22:41:41 +09:00
syuilo c587ba8094 New translations ja-jp.yml (Italian) 2025-10-02 06:00:21 +09:00
syuilo 5e123049d6 New translations ja-jp.yml (Italian) 2025-10-02 04:41:36 +09:00
github-actions[bot] f0fb3a56a8 Bump version to 2025.10.0-alpha.0 2025-10-01 04:57:00 +00:00
syuilo eb67e0552c New translations ja-jp.yml (Chinese Simplified) 2025-09-30 00:58:10 +09:00
syuilo 56ef212147 New translations ja-jp.yml (Spanish) 2025-09-30 00:58:08 +09:00
syuilo c22844c367 New translations ja-jp.yml (English) 2025-09-29 23:13:50 +09:00
syuilo 1f9659802b New translations ja-jp.yml (English) 2025-09-29 22:03:27 +09:00
syuilo b599b1bfa0 New translations ja-jp.yml (Chinese Simplified) 2025-09-29 16:44:06 +09:00
syuilo e9fb4a7625 New translations ja-jp.yml (Chinese Simplified) 2025-09-29 14:25:06 +09:00
syuilo 7b7512f798 New translations ja-jp.yml (Chinese Simplified) 2025-09-29 03:36:43 +09:00
syuilo 2dea0989bc New translations ja-jp.yml (Chinese Simplified) 2025-09-29 02:25:00 +09:00
syuilo f82c1f1092 New translations ja-jp.yml (Chinese Simplified) 2025-09-29 01:17:18 +09:00
syuilo 298be1a093 New translations ja-jp.yml (Chinese Simplified) 2025-09-29 00:17:49 +09:00
syuilo 2fb58a6394 New translations ja-jp.yml (Chinese Simplified) 2025-09-28 23:12:31 +09:00
かっこかり b8ae7edcec
fix(gh): add minimumReleaseAge settings to renovate [ci skip] 2025-09-28 18:28:37 +09:00
syuilo e24233c1c7 add ideas 2025-09-27 20:53:21 +09:00
syuilo 1fd7387f14 New translations ja-jp.yml (Catalan) 2025-09-27 20:37:51 +09:00
syuilo 225154d76d enhance(frontend): improve zoomLines image effect 2025-09-27 18:46:26 +09:00
syuilo 396bcbdb0f New translations ja-jp.yml (Chinese Traditional) 2025-09-27 01:44:44 +09:00
syuilo 9664408b4c New translations ja-jp.yml (Chinese Simplified) 2025-09-26 23:26:54 +09:00
syuilo 3de7a0bf4e New translations ja-jp.yml (Korean) 2025-09-26 23:26:52 +09:00
syuilo f2e3154319 New translations ja-jp.yml (Chinese Traditional) 2025-09-26 18:56:40 +09:00
syuilo c5f9c0ce5c enhance(frontend): add pixelate mask effect 2025-09-26 18:27:53 +09:00
github-actions[bot] cce302ae4f Bump version to 2025.9.1-alpha.2 2025-09-26 06:44:58 +00:00
かっこかり e0d210e15b
fix(frontend): アクティビティウィジェットのグラフモードが動作しない問題を修正 (#16579)
* fix(frontend): アクティビティウィジェットのグラフモードが動作しない問題を修正

* 🎨

* Update Changelog

* fix
2025-09-26 15:36:50 +09:00
syuilo 0b7634b126 enhance(frontend): テーマをドラッグ&ドロップできるように 2025-09-26 15:36:25 +09:00
syuilo d1446d195a
feat: scheduled post (#16577)
* Update NoteDraft.ts

* Update NoteDraft.ts

* wip

* Update CHANGELOG.md

* wip

* Update PostScheduledNoteProcessorService.ts

* Update PostScheduledNoteProcessorService.ts

* Update Notification.ts

* wip

* Update NoteDraftService.ts

* Update NoteDraftService.ts

* Update NoteDraftService.ts

* wip

* Create 1758677617888-scheduled-post.js

* Update index.d.ts

* Update stats.ts

* wip

* wip

* wip

* wip

* wip

* Update MkNotification.vue

* wip

* wip

* wip

* Update NoteDraftService.ts

* Update NoteDraftService.ts

* wip

* wip

* Update NoteDraftEntityService.ts

* wip

* Update index.d.ts

* Update MkPostForm.vue

* wip

* wip

* wip

* Update NoteCreateService.ts

* wip

* wip

* wip

* Update NoteDraftEntityService.ts

* Update NoteCreateService.ts

* Update NoteDraftService.ts

* wip

* Update NoteDraftService.ts

* wip

* wip

* Update MkPostForm.vue

* wip

* Update MkPostForm.vue

* Update os.ts

* wip

* Update MkNoteDraftsDialog.vue
2025-09-26 15:29:52 +09:00
syuilo 94ee22e9a3 New translations ja-jp.yml (Spanish) 2025-09-25 07:29:05 +09:00
syuilo a0b95a48d0 New translations ja-jp.yml (Catalan) 2025-09-24 18:55:19 +09:00
syuilo ff7e669e70 New translations ja-jp.yml (Chinese Traditional) 2025-09-24 18:55:17 +09:00
かっこかり 218070eb13
fix(frontend): ビルド成果物のファイル名にlocalesのhashを含めるように (#16580) 2025-09-24 17:01:48 +09:00
syuilo fa97ab984c New translations ja-jp.yml (Chinese Simplified) 2025-09-24 16:10:49 +09:00
syuilo 742c452716 New translations ja-jp.yml (Korean) 2025-09-24 14:31:29 +09:00
syuilo 0f8c068e84
feat(frontend): Video compression (#16574)
* wip

* Update CHANGELOG.md

* wip

* wip

* wip

* wip

* Update use-uploader.ts

* Update use-uploader.ts
2025-09-24 09:01:06 +09:00
github-actions[bot] 69d66b89f2 Bump version to 2025.9.1-alpha.1 2025-09-22 10:52:25 +00:00
饺子w (Yumechi) 211365de64
enhance(backend): 設定ファイルにFastifyOptions.trustProxyを追加 (#16567)
* enhance(backend): 設定ファイルにFastifyOptions.trustProxyを追加

Signed-off-by: eternal-flame-AD <yume@yumechi.jp>

* try harder

Signed-off-by: eternal-flame-AD <yume@yumechi.jp>

---------

Signed-off-by: eternal-flame-AD <yume@yumechi.jp>
2025-09-22 19:45:01 +09:00
syuilo 966127c63e enhance(frontend): 絵文字ピッカーのサイズをより大きくできるように 2025-09-22 19:43:31 +09:00
果物リン 54800971eb
プロフィールの「ユーザーのノートを検索」でローカルのユーザーを検索できないのを修正 (#16570)
* プロフィールの「ユーザーのノートを検索」でローカルのユーザーを検索できないのを修正

* fix lint
2025-09-22 19:21:01 +09:00
syuilo 13d5c6d2b2 refactor MkAnimBg 2025-09-22 19:00:47 +09:00
syuilo 2cff00eedd update pnpm 2025-09-22 18:27:39 +09:00
syuilo 3fc2261041 dev(pnpm): set minimumReleaseAge to 7days
to mitigate supply-chain attack
Resolve #16572
2025-09-22 18:23:43 +09:00
syuilo 050b67bb02 New translations ja-jp.yml (Japanese, Kansai) 2025-09-21 18:53:19 +09:00
syuilo 56fe87e560 New translations ja-jp.yml (Korean) 2025-09-21 14:36:43 +09:00
syuilo beaf1882c9 New translations ja-jp.yml (Spanish) 2025-09-21 02:45:33 +09:00
syuilo 5791b3b03d New translations ja-jp.yml (Spanish) 2025-09-21 01:32:21 +09:00
syuilo cd29664664 New translations ja-jp.yml (Chinese Traditional) 2025-09-20 21:16:29 +09:00
syuilo 13018434b8 New translations ja-jp.yml (Chinese Traditional) 2025-09-20 19:57:34 +09:00
syuilo 3c10839770 New translations ja-jp.yml (Chinese Simplified) 2025-09-20 16:32:56 +09:00
syuilo 693b9f5374 New translations ja-jp.yml (Catalan) 2025-09-20 16:32:55 +09:00
syuilo 1fd2c652d4 New translations ja-jp.yml (English) 2025-09-20 15:22:27 +09:00
syuilo 9a1be19bf0 New translations ja-jp.yml (Chinese Simplified) 2025-09-20 15:22:25 +09:00
syuilo 4d9c8c870c New translations ja-jp.yml (Korean) 2025-09-20 15:22:17 +09:00
syuilo 425ffe97a4 New translations ja-jp.yml (Catalan) 2025-09-20 15:22:10 +09:00
syuilo 0727929ef7 New translations ja-jp.yml (Chinese Traditional) 2025-09-20 15:22:05 +09:00
syuilo 1f42b9d0aa New translations ja-jp.yml (Italian) 2025-09-20 15:22:01 +09:00
syuilo 18d66c0233 Update CHANGELOG.md 2025-09-20 14:21:51 +09:00
Copilot 2f52c20150
Implement professional-grade Gaussian-approximated blur effect with resolution independence and configurable quality for image effector system (#16571)
* Initial plan

* Implement blur effect for image effector system

Co-authored-by: syuilo <4439005+syuilo@users.noreply.github.com>

* Improve blur quality with configurable sample count

Co-authored-by: syuilo <4439005+syuilo@users.noreply.github.com>

* Revert to simpler blur implementation with configurable sample count

Co-authored-by: syuilo <4439005+syuilo@users.noreply.github.com>

* Fix blur size independence from sample count

Co-authored-by: syuilo <4439005+syuilo@users.noreply.github.com>

* Make blur radius resolution-independent

Co-authored-by: syuilo <4439005+syuilo@users.noreply.github.com>

* Update blur.ts

* Enhance blur quality with explicit diagonal sampling and fix parameter configuration

Co-authored-by: syuilo <4439005+syuilo@users.noreply.github.com>

* Implement Gaussian-approximated blur with distance-based weighting for superior quality

Co-authored-by: syuilo <4439005+syuilo@users.noreply.github.com>

* Revert "Implement Gaussian-approximated blur with distance-based weighting for superior quality"

This reverts commit c739e9f55b.

* Revert "Enhance blur quality with explicit diagonal sampling and fix parameter configuration"

This reverts commit ffbc6eeba7.

* wip

* tweak

* ellipse

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: syuilo <4439005+syuilo@users.noreply.github.com>
2025-09-20 14:19:35 +09:00
github-actions[bot] 9d70c9ad78 Bump version to 2025.9.1-alpha.0 2025-09-19 13:51:21 +00:00
tamaina 42b2aea533
feat(frontend): 自分のプロフィールページの二次元コード(QRコード)を表示し、他の人のコードを読み取りするページを追加 (#16456)
* wip (qr.show.vue)

* added to navbar

* qr.show.vue

* fix

* added to navbar

* fix size

* 🎨

* 🎨

* fix div warn

* fix

* use * 0.25

* fix??

* fix lint

* clean up

* ???

* ?

* fix

* 🎨

* 🎨

* refactor

* 🎨

* 🎨

* :ar:t

* 🎨

* iphone flip

* no lazy import

* 🎨

* 🎨

* 🎨

* ユーザー全部flipでいいや

* ✌️

* fix

* fix

* fix lint

* 🎨

* fix type

* fix: local user profile url cannot be resolved with ap/show

* fix: local user url with hostname could not be resolved

* chore: use common utility for checking self host

* wip

* 🎨

* 🎨

* fix imports

* fix

* fix

* fix

* 🎨

* fix...

* set spacer-w

* ✌️

* 全画面でQRを読むように

* fix

* 🎨

* modify navbar.ts

* start/stop on vue activation

* display raw content read from qr

* 端末のQRをスキャンするボタンを追加

* chore

* やっぱりmfmを先に表示する

* 🎨

* fix 18n

* QRの内容は/users/:userIdにする

* add spdx

* use cqh

* `defineProps` is a compiler macro and no longer needs to be imported.

* use MkUserName

* 🎨

* 🎨

* refactor

* clean up

* refactor

* 🎨

* Update qr.show.vue

* Misskeyロゴにdrop-shadowを追加

* clean up: do not use empty css

* fix os.select usage

* Update qr.vue

* Update qr.show.vue

* Update qr.show.vue

* Update get-user-menu.ts

* ✌️

* Update show.ts

* Update ja-JP.yml

* watermark

* Update CHANGELOG.md

* Update qr.read.vue

* Update qr.read.vue

* wip

* Update MkWatermarkEditorDialog.Layer.vue

---------

Co-authored-by: anatawa12 <anatawa12@icloud.com>
Co-authored-by: syuilo <4439005+syuilo@users.noreply.github.com>
2025-09-19 21:02:30 +09:00
syuilo 97adf6f2cc 🎨 2025-09-19 14:23:34 +09:00
tamaina 93ff209c51
enhance(frontend): bootでonunhandledrejectionでrenderErrorする場合、PromiseRejectionEvent.reasonを渡すように (#16563) 2025-09-18 19:35:23 +09:00
syuilo 5fe08d0bbb fix(frontend): iOSで、デバイスがダークモードだと初回読み込み時にエラーになる問題を修正
Fix #16562
2025-09-18 19:18:31 +09:00
syuilo 8c413d01e6
enhance(frontend): マスクエフェクト (#16556)
* wip

* wip

* Update MkImageEffectorDialog.vue

* Update MkImageEffectorDialog.vue

* Update MkImageEffectorDialog.vue

* Update MkImageEffectorDialog.vue

* Update MkImageEffectorDialog.vue

* Update fillSquare.ts

* Update CHANGELOG.md

* Update fillSquare.ts
2025-09-17 18:38:56 +09:00
syuilo b231da7c7c enhance(frontend): チャットの日本語名称をダイレクトメッセージに & ベータを外す 2025-09-16 16:24:10 +09:00
syuilo df3e44f62e enhance(backend): allow upload csv by default
Related #16541
2025-09-16 12:16:18 +09:00
かっこかり e504560477
fix: サーバー設定によりコンテンツの閲覧が制限されている場合のメッセージを区別するように (#16527) 2025-09-16 11:53:20 +09:00
syuilo bcb2073715 enhance(backend): 初期設定をスキップした場合の初期状態ポリシーでインポート系をオプトインに
Resolve #16540
2025-09-16 11:26:35 +09:00
syuilo 6a80c23a50
chore(frontend): enable enableFolderPageView by default 2025-09-15 14:33:32 +09:00
syuilo 2621f468ff enhance: 広告ごとにセンシティブフラグを設定できるように 2025-09-14 15:25:22 +09:00
かっこかり d4654dd7bd
refactor(frontend): os.select, MkSelectのitem指定をオブジェクトによる定義に統一し、型を狭める (#16475)
* refactor(frontend): MkSelectのitem指定をオブジェクトによる定義に統一

* fix

* spdx

* fix

* fix os.select

* fix lint

* add comment

* fix

* fix: os.select対応漏れを修正

* fix

* fix

* fix: MkSelectのmodelに対する型チェックを厳格化

* fix

* fix

* fix

* Update packages/frontend/src/components/MkEmbedCodeGenDialog.vue

Co-authored-by: syuilo <4439005+syuilo@users.noreply.github.com>

* fix

* fix types

* fix

* fix

* Update packages/frontend/src/pages/admin/roles.editor.vue

Co-authored-by: syuilo <4439005+syuilo@users.noreply.github.com>

* fix: MkSelectに直接配列を指定している場合に正常に型が解決されるように

---------

Co-authored-by: syuilo <4439005+syuilo@users.noreply.github.com>
2025-09-13 21:00:33 +09:00
renovate[bot] b7da6cad87
fix(deps): update dependency vite [security] (#16535)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-09-13 10:32:49 +09:00
かっこかり 5b4115e21a
refactor(frontend): フロントエンドの型エラー解消(途中まで) (#16539)
* fix(frontend): FormLinkをボタンとして使用した際にエラーが出る問題を修正

* refactor(frontend): フロントエンドの型エラー解消

* remove unused ts-expect-error

* migrate

* remove unrelated changes

* fix lint

* more type fixes
2025-09-13 08:33:14 +09:00
syuilo c174c5c144
Update CHANGELOG.md 2025-09-12 17:13:13 +09:00
かっこかり aebc3f781e
perf(frontend): 低精度な現在時刻を一か所で管理するように (#16479)
* perf(frontend): 低精度な現在時刻を一か所で管理するように

* lint

* fix

* remove unused imports

* fix

* Update Changelog

* [ci skip] typo

* enhance: カレンダーウィジェットの日付変更は時間通りに行うように

* [ci skip] fix
2025-09-12 17:12:50 +09:00
かっこかり f60b6291d7
chore(gh): add frontend-builder to renovate 2025-09-10 10:01:25 +09:00
taiy 7673874675
fix(eslint): add window prefix rules to frontend-embed & frontend-shared (#16531) 2025-09-10 09:22:12 +09:00
241 changed files with 8891 additions and 4132 deletions

View File

@ -105,6 +105,16 @@ port: 3000
# socket: /path/to/misskey.sock # socket: /path/to/misskey.sock
# chmodSocket: '777' # 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 └──────────────────────────────── #───┘ PostgreSQL configuration └────────────────────────────────

View File

@ -81,7 +81,7 @@ jobs:
cache: 'pnpm' cache: 'pnpm'
- run: pnpm i --frozen-lockfile - run: pnpm i --frozen-lockfile
- name: Restore eslint cache - name: Restore eslint cache
uses: actions/cache@v4.2.4 uses: actions/cache@v4.3.0
with: with:
path: ${{ env.eslint-cache-path }} path: ${{ env.eslint-cache-path }}
key: eslint-${{ env.eslint-cache-version }}-${{ matrix.workspace }}-${{ hashFiles('**/pnpm-lock.yaml') }}-${{ github.ref_name }}-${{ github.sha }} key: eslint-${{ env.eslint-cache-version }}-${{ matrix.workspace }}-${{ hashFiles('**/pnpm-lock.yaml') }}-${{ github.ref_name }}-${{ github.sha }}

View File

@ -16,7 +16,7 @@ jobs:
# api-artifact # api-artifact
steps: steps:
- name: Download artifact - name: Download artifact
uses: actions/github-script@v7.0.1 uses: actions/github-script@v7.1.0
with: with:
script: | script: |
const fs = require('fs'); const fs = require('fs');

View File

@ -90,7 +90,7 @@ jobs:
env: env:
CHROMATIC_PROJECT_TOKEN: ${{ secrets.CHROMATIC_PROJECT_TOKEN }} CHROMATIC_PROJECT_TOKEN: ${{ secrets.CHROMATIC_PROJECT_TOKEN }}
- name: Notify that Chromatic detects changes - 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' if: github.event_name != 'pull_request_target' && steps.chromatic_push.outputs.success == 'false'
with: with:
github-token: ${{ secrets.GITHUB_TOKEN }} github-token: ${{ secrets.GITHUB_TOKEN }}

View File

@ -1,14 +1,30 @@
## Unreleased ## 2025.10.0
### NOTE
- pnpm 10.16.0 が必要です
### General ### General
- - Feat: 予約投稿ができるようになりました
- デフォルトで作成可能数は1になっています。適宜ロールのポリシーで設定を行ってください。
- Enhance: 広告ごとにセンシティブフラグを設定できるようになりました
- Enhance: 依存関係の更新
- Enhance: 翻訳の更新
### Client ### Client
- - Feat: アカウントのQRコードを表示・読み取りできるようになりました
- Feat: 動画を圧縮してアップロードできるようになりました
- Enhance: チャットの日本語名称がダイレクトメッセージに戻るとともに、ベータ版機能ではなくなりました
- Enhance: 画像編集にマスクエフェクト(塗りつぶし、ぼかし、モザイク)を追加
- Enhance: 画像編集の集中線エフェクトを強化
- Enhance: ウォーターマークにアカウントのQRコードを追加できるように
- Enhance: テーマをドラッグ&ドロップできるように
- Enhance: 絵文字ピッカーのサイズをより大きくできるように
- Enhance: 時刻計算のための基準値を一か所で管理するようにし、パフォーマンスを向上
- Fix: iOSで、デバイスがダークモードだと初回読み込み時にエラーになる問題を修正
- Fix: アクティビティウィジェットのグラフモードが動作しない問題を修正
### Server ### Server
- - Enhance: ユーザーIPを確実に取得できるために設定ファイルにFastifyOptions.trustProxyを追加しました
## 2025.9.0 ## 2025.9.0

View File

@ -0,0 +1,232 @@
<!--
SPDX-FileCopyrightText: syuilo and misskey-project
SPDX-License-Identifier: AGPL-3.0-only
-->
<template>
<canvas ref="canvasEl" style="display: block; width: 100%; height: 100%; pointer-events: none;"></canvas>
</template>
<script lang="ts" setup>
import { onMounted, onUnmounted, useTemplateRef } from 'vue';
import isChromatic from 'chromatic/isChromatic';
import { initShaderProgram } from '@/utility/webgl.js';
const VERTEX_SHADER = `#version 300 es
in vec2 position;
out vec2 in_uv;
void main() {
in_uv = (position + 1.0) / 2.0;
gl_Position = vec4(position, 0.0, 1.0);
}
`;
const FRAGMENT_SHADER = `#version 300 es
precision mediump float;
const float PI = 3.141592653589793;
const float TWO_PI = 6.283185307179586;
const float HALF_PI = 1.5707963267948966;
in vec2 in_uv;
uniform vec2 in_resolution;
uniform float u_scale;
uniform float u_time;
uniform float u_seed;
uniform float u_angle;
uniform float u_radius;
uniform vec3 u_color;
uniform vec2 u_ripplePositions[16];
uniform float u_rippleRadiuses[16];
out vec4 out_color;
float getRipple(vec2 uv) {
float strength = 0.0;
float thickness = 0.05;
for (int i = 0; i < 16; i++) {
if (u_rippleRadiuses[i] <= 0.0) continue;
float d = distance(uv, u_ripplePositions[i]);
//
if (d < u_rippleRadiuses[i] + thickness && d > u_rippleRadiuses[i] - thickness) {
float gradate = abs(d - u_rippleRadiuses[i] + thickness) / thickness;
strength += (1.0 - u_rippleRadiuses[i]) * gradate;
}
//
if (d < u_rippleRadiuses[i] + thickness) {
strength += 0.25 * (1.0 - u_rippleRadiuses[i]);
}
}
return strength;
}
void main() {
float x_ratio = min(in_resolution.x / in_resolution.y, 1.0);
float y_ratio = min(in_resolution.y / in_resolution.x, 1.0);
float angle = -(u_angle * PI);
vec2 centeredUv = (in_uv - vec2(0.5, 0.5)) * vec2(x_ratio, y_ratio);
vec2 rotatedUV = vec2(
centeredUv.x * cos(angle) - centeredUv.y * sin(angle),
centeredUv.x * sin(angle) + centeredUv.y * cos(angle)
);
vec2 uv = rotatedUV;
float time = u_time * 0.00025;
float size = 1.0 / u_scale;
float size_half = size / 2.0;
float modX = mod(uv.x, size);
float modY = mod(uv.y, size);
vec2 pixelated_uv = vec2(
(size * (floor((uv.x - 0.5 - size) / size) + 0.5)),
(size * (floor((uv.y - 0.5 - size) / size) + 0.5))
) + vec2(0.5 + size, 0.5 + size);
float strength = getRipple(pixelated_uv);
float opacity = min(max(strength, 0.0), 1.0);
float threshold = ((u_radius / 2.0) / u_scale);
if (length(vec2(modX - size_half, modY - size_half)) < threshold) {
out_color = vec4(u_color.r, u_color.g, u_color.b, opacity);
//out_color = vec4(1.0);
return;
}
// debug
//float a = min(max(getRipple(uv), 0.0), 1.0);
//out_color = vec4(u_color.r, u_color.g, u_color.b, (opacity + a) / 2.0);
out_color = vec4(0.0, 0.0, 0.0, 0.0);
}
`;
const canvasEl = useTemplateRef('canvasEl');
const props = withDefaults(defineProps<{
scale?: number;
}>(), {
scale: 48,
});
let handle: ReturnType<typeof window['requestAnimationFrame']> | null = null;
onMounted(() => {
const canvas = canvasEl.value!;
let width = canvas.offsetWidth;
let height = canvas.offsetHeight;
canvas.width = width;
canvas.height = height;
const maybeGl = canvas.getContext('webgl2', { preserveDrawingBuffer: false, alpha: true, premultipliedAlpha: false, antialias: true });
if (maybeGl == null) return;
const gl = maybeGl;
const VERTICES = new Float32Array([-1, -1, -1, 1, 1, 1, -1, -1, 1, 1, 1, -1]);
const vertexBuffer = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, vertexBuffer);
gl.bufferData(gl.ARRAY_BUFFER, VERTICES, gl.STATIC_DRAW);
//gl.clearColor(0.0, 0.0, 0.0, 0.0);
//gl.clear(gl.COLOR_BUFFER_BIT);
const shaderProgram = initShaderProgram(gl, VERTEX_SHADER, FRAGMENT_SHADER);
gl.useProgram(shaderProgram);
const positionLocation = gl.getAttribLocation(shaderProgram, 'position');
gl.vertexAttribPointer(positionLocation, 2, gl.FLOAT, false, 0, 0);
gl.enableVertexAttribArray(positionLocation);
const in_resolution = gl.getUniformLocation(shaderProgram, 'in_resolution');
gl.uniform2fv(in_resolution, [canvas.width, canvas.height]);
const u_time = gl.getUniformLocation(shaderProgram, 'u_time');
const u_seed = gl.getUniformLocation(shaderProgram, 'u_seed');
const u_scale = gl.getUniformLocation(shaderProgram, 'u_scale');
const u_angle = gl.getUniformLocation(shaderProgram, 'u_angle');
const u_radius = gl.getUniformLocation(shaderProgram, 'u_radius');
const u_color = gl.getUniformLocation(shaderProgram, 'u_color');
gl.uniform1f(u_seed, Math.random() * 1000);
gl.uniform1f(u_scale, props.scale);
gl.uniform1f(u_angle, 0.0);
gl.uniform1f(u_radius, 0.15);
gl.uniform3fv(u_color, [0.5, 1.0, 0]);
if (isChromatic()) {
gl.uniform1f(u_time, 0);
gl.drawArrays(gl.TRIANGLES, 0, 6);
} else {
let ripples = [] as { position: [number, number]; startTime: number; }[];
const LIFE_TIME = 1000 * 4;
function render(timeStamp: number) {
let sizeChanged = false;
if (Math.abs(height - canvas.offsetHeight) > 2) {
height = canvas.offsetHeight;
canvas.height = height;
sizeChanged = true;
}
if (Math.abs(width - canvas.offsetWidth) > 2) {
width = canvas.offsetWidth;
canvas.width = width;
sizeChanged = true;
}
if (sizeChanged && gl) {
gl.uniform2fv(in_resolution, [width, height]);
gl.viewport(0, 0, width, height);
}
gl.uniform1f(u_time, timeStamp);
if (Math.random() < 0.01 && ripples.length < 16) {
ripples.push({ position: [(Math.random() * 2) - 1, (Math.random() * 2) - 1], startTime: timeStamp });
}
for (let i = 0; i < 16; i++) {
const o = gl.getUniformLocation(shaderProgram, `u_ripplePositions[${i.toString()}]`);
const r = gl.getUniformLocation(shaderProgram, `u_rippleRadiuses[${i.toString()}]`);
const ripple = ripples[i];
if (ripple == null) {
gl.uniform2f(o, 0, 0);
gl.uniform1f(r, 0.0);
continue;
}
const delta = timeStamp - ripple.startTime;
gl.uniform2f(o, ripple.position[0], ripple.position[1]);
gl.uniform1f(r, delta / LIFE_TIME);
}
ripples = ripples.filter(r => (timeStamp - r.startTime) < LIFE_TIME);
if (ripples.length === 0) {
ripples.push({ position: [(Math.random() * 2) - 1, (Math.random() * 2) - 1], startTime: timeStamp });
}
gl.drawArrays(gl.TRIANGLES, 0, 6);
handle = window.requestAnimationFrame(render);
}
handle = window.requestAnimationFrame(render);
}
});
onUnmounted(() => {
if (handle) {
window.cancelAnimationFrame(handle);
}
// TODO: WebGL
});
</script>
<style lang="scss" module>
</style>

View File

@ -0,0 +1,190 @@
<!--
SPDX-FileCopyrightText: syuilo and misskey-project
SPDX-License-Identifier: AGPL-3.0-only
-->
<template>
<canvas ref="canvasEl" style="display: block; width: 100%; height: 100%; pointer-events: none;"></canvas>
</template>
<script lang="ts" setup>
import { onMounted, onUnmounted, useTemplateRef } from 'vue';
import isChromatic from 'chromatic/isChromatic';
import { GLSL_LIB_SNOISE, initShaderProgram } from '@/utility/webgl.js';
const VERTEX_SHADER = `#version 300 es
in vec2 position;
out vec2 in_uv;
void main() {
in_uv = (position + 1.0) / 2.0;
gl_Position = vec4(position, 0.0, 1.0);
}
`;
const FRAGMENT_SHADER = `#version 300 es
precision mediump float;
const float PI = 3.141592653589793;
const float TWO_PI = 6.283185307179586;
const float HALF_PI = 1.5707963267948966;
${GLSL_LIB_SNOISE}
in vec2 in_uv;
uniform vec2 in_resolution;
uniform float u_scale;
uniform float u_time;
uniform float u_seed;
uniform float u_angle;
uniform float u_radius;
uniform vec3 u_color;
out vec4 out_color;
void main() {
float x_ratio = min(in_resolution.x / in_resolution.y, 1.0);
float y_ratio = min(in_resolution.y / in_resolution.x, 1.0);
float size = 1.0 / u_scale;
float size_half = size / 2.0;
float angle = -(u_angle * PI);
vec2 centeredUv = (in_uv - vec2(0.5, 0.5)) * vec2(x_ratio, y_ratio);
vec2 rotatedUV = vec2(
centeredUv.x * cos(angle) - centeredUv.y * sin(angle),
centeredUv.x * sin(angle) + centeredUv.y * cos(angle)
);
vec2 uv = rotatedUV;
float modX = mod(uv.x, size);
float modY = mod(uv.y, size);
vec2 pixelated_uv = vec2(
(size * (floor((uv.x - 0.5 - size) / size) + 0.5)),
(size * (floor((uv.y - 0.5 - size) / size) + 0.5))
) + vec2(0.5 + size, 0.5 + size);
float time = u_time * 0.00025;
float noiseAScale = 1.0;
float noiseAX = (pixelated_uv.x + u_seed) * (u_scale / noiseAScale);
float noiseAY = (pixelated_uv.y + u_seed) * (u_scale / noiseAScale);
float noiseA = snoise(vec3(noiseAX, noiseAY, time * 2.0));
float noiseBScale = 32.0;
float noiseBX = (pixelated_uv.x + u_seed) * (u_scale / noiseBScale);
float noiseBY = (pixelated_uv.y + u_seed) * (u_scale / noiseBScale);
float noiseB = snoise(vec3(noiseBX, noiseBY, time));
float strength = 0.0;
strength += noiseA * 0.2;
strength += noiseB * 0.8;
float opacity = min(max(strength, 0.0), 1.0);
float threshold = ((u_radius / 2.0) / u_scale);
if (length(vec2(modX - size_half, modY - size_half)) < threshold) {
out_color = vec4(u_color.r, u_color.g, u_color.b, opacity);
return;
}
out_color = vec4(0.0, 0.0, 0.0, 0.0);
}
`;
const canvasEl = useTemplateRef('canvasEl');
const props = withDefaults(defineProps<{
scale?: number;
}>(), {
scale: 48,
});
let handle: ReturnType<typeof window['requestAnimationFrame']> | null = null;
onMounted(() => {
const canvas = canvasEl.value!;
let width = canvas.offsetWidth;
let height = canvas.offsetHeight;
canvas.width = width;
canvas.height = height;
const maybeGl = canvas.getContext('webgl2', { preserveDrawingBuffer: false, alpha: true, premultipliedAlpha: false, antialias: true });
if (maybeGl == null) return;
const gl = maybeGl;
const VERTICES = new Float32Array([-1, -1, -1, 1, 1, 1, -1, -1, 1, 1, 1, -1]);
const vertexBuffer = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, vertexBuffer);
gl.bufferData(gl.ARRAY_BUFFER, VERTICES, gl.STATIC_DRAW);
//gl.clearColor(0.0, 0.0, 0.0, 0.0);
//gl.clear(gl.COLOR_BUFFER_BIT);
const shaderProgram = initShaderProgram(gl, VERTEX_SHADER, FRAGMENT_SHADER);
gl.useProgram(shaderProgram);
const positionLocation = gl.getAttribLocation(shaderProgram, 'position');
gl.vertexAttribPointer(positionLocation, 2, gl.FLOAT, false, 0, 0);
gl.enableVertexAttribArray(positionLocation);
const in_resolution = gl.getUniformLocation(shaderProgram, 'in_resolution');
gl.uniform2fv(in_resolution, [canvas.width, canvas.height]);
const u_time = gl.getUniformLocation(shaderProgram, 'u_time');
const u_seed = gl.getUniformLocation(shaderProgram, 'u_seed');
const u_scale = gl.getUniformLocation(shaderProgram, 'u_scale');
const u_angle = gl.getUniformLocation(shaderProgram, 'u_angle');
const u_radius = gl.getUniformLocation(shaderProgram, 'u_radius');
const u_color = gl.getUniformLocation(shaderProgram, 'u_color');
gl.uniform1f(u_seed, Math.random() * 1000);
gl.uniform1f(u_scale, props.scale);
gl.uniform1f(u_angle, 0.0);
gl.uniform1f(u_radius, 0.15);
gl.uniform3fv(u_color, [0.5, 1.0, 0]);
if (isChromatic()) {
gl.uniform1f(u_time, 0);
gl.drawArrays(gl.TRIANGLES, 0, 6);
} else {
function render(timeStamp: number) {
let sizeChanged = false;
if (Math.abs(height - canvas.offsetHeight) > 2) {
height = canvas.offsetHeight;
canvas.height = height;
sizeChanged = true;
}
if (Math.abs(width - canvas.offsetWidth) > 2) {
width = canvas.offsetWidth;
canvas.width = width;
sizeChanged = true;
}
if (sizeChanged && gl) {
gl.uniform2fv(in_resolution, [width, height]);
gl.viewport(0, 0, width, height);
}
gl.uniform1f(u_time, timeStamp);
gl.drawArrays(gl.TRIANGLES, 0, 6);
handle = window.requestAnimationFrame(render);
}
handle = window.requestAnimationFrame(render);
}
});
onUnmounted(() => {
if (handle) {
window.cancelAnimationFrame(handle);
}
// TODO: WebGL
});
</script>
<style lang="scss" module>
</style>

View File

@ -253,6 +253,7 @@ noteDeleteConfirm: "Segur que voleu eliminar aquesta publicació?"
pinLimitExceeded: "No podeu fixar més publicacions" pinLimitExceeded: "No podeu fixar més publicacions"
done: "Fet" done: "Fet"
processing: "S'està processant..." processing: "S'està processant..."
preprocessing: "Preparant"
preview: "Vista prèvia" preview: "Vista prèvia"
default: "Per defecte" default: "Per defecte"
defaultValueIs: "Per defecte: {value}" 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." 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." federationDisabled: "La unió es troba deshabilitada en aquest servidor. No es pot interactuar amb usuaris d'altres servidors."
draft: "Esborrany " draft: "Esborrany "
draftsAndScheduledNotes: "Esborranys i publicacions programades"
confirmOnReact: "Confirmar en reaccionar" confirmOnReact: "Confirmar en reaccionar"
reactAreYouSure: "Vols reaccionar amb \"{emoji}\"?" reactAreYouSure: "Vols reaccionar amb \"{emoji}\"?"
markAsSensitiveConfirm: "Vols marcar aquest contingut com a sensible?" markAsSensitiveConfirm: "Vols marcar aquest contingut com a sensible?"
@ -1372,6 +1374,8 @@ redisplayAllTips: "Torna ha mostrat tots els trucs i consells"
hideAllTips: "Amagar tots els trucs i consells" hideAllTips: "Amagar tots els trucs i consells"
defaultImageCompressionLevel: "Nivell de comprensió de la imatge per defecte" defaultImageCompressionLevel: "Nivell de comprensió de la imatge per defecte"
defaultImageCompressionLevel_description: "Baixa, conserva la qualitat de la imatge però la mida de l'arxiu és més gran. <br>Alta, redueix la mida de l'arxiu però també la qualitat de la imatge." defaultImageCompressionLevel_description: "Baixa, conserva la qualitat de la imatge però la mida de l'arxiu és més gran. <br>Alta, redueix la mida de l'arxiu però també la qualitat de la imatge."
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. <br>Si augmentes l'opció redueixes la mida de l'arxiu i la qualitat de la imatge és pitjor."
inMinutes: "Minut(s)" inMinutes: "Minut(s)"
inDays: "Di(a)(es)" inDays: "Di(a)(es)"
safeModeEnabled: "Mode segur activat" safeModeEnabled: "Mode segur activat"
@ -1380,6 +1384,20 @@ customCssIsDisabledBecauseSafeMode: "El CSS personalitzat no s'aplica perquè el
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." 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!" thankYouForTestingBeta: "Gràcies per ajudar-nos a provar la versió beta!"
createUserSpecifiedNote: "Crear notes especificades per l'usuari " 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: _order:
newest: "Més recent" newest: "Més recent"
oldest: "Antigues primer" oldest: "Antigues primer"
@ -2022,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_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." 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" 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" watermarkAvailable: "Pots fer servir la marca d'aigua"
_condition: _condition:
roleAssignedTo: "Assignat a rols manuals" roleAssignedTo: "Assignat a rols manuals"
@ -2647,6 +2666,8 @@ _notification:
youReceivedFollowRequest: "Has rebut una petició de seguiment" youReceivedFollowRequest: "Has rebut una petició de seguiment"
yourFollowRequestAccepted: "La teva petició de seguiment ha sigut acceptada" yourFollowRequestAccepted: "La teva petició de seguiment ha sigut acceptada"
pollEnded: "Ja pots veure els resultats de l'enquesta " 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" newNote: "Nota nova"
unreadAntennaNote: "Antena {name}" unreadAntennaNote: "Antena {name}"
roleAssigned: "Rol assignat " roleAssigned: "Rol assignat "
@ -3201,6 +3222,8 @@ _imageEffector:
mirror: "Mirall" mirror: "Mirall"
invert: "Inversió cromàtica " invert: "Inversió cromàtica "
grayscale: "Monocrom " grayscale: "Monocrom "
blur: "Desenfocament"
pixelate: "Mosaic"
colorAdjust: "Correcció de color" colorAdjust: "Correcció de color"
colorClamp: "Compressió cromàtica " colorClamp: "Compressió cromàtica "
colorClampAdvanced: "Compressió de cromàtica avançada " colorClampAdvanced: "Compressió de cromàtica avançada "
@ -3212,11 +3235,13 @@ _imageEffector:
checker: "Escacs" checker: "Escacs"
blockNoise: "Bloqueig de soroll" blockNoise: "Bloqueig de soroll"
tearing: "Trencament d'imatge " tearing: "Trencament d'imatge "
fillSquare: "Omplir quadrat" fill: "Omplir"
_fxProps: _fxProps:
angle: "Angle" angle: "Angle"
scale: "Mida" scale: "Mida"
size: "Mida" size: "Mida"
radius: "Radi"
samples: "Mida de la mostra"
offset: "Posició " offset: "Posició "
color: "Color" color: "Color"
opacity: "Opacitat" opacity: "Opacitat"
@ -3246,6 +3271,7 @@ _imageEffector:
zoomLinesThreshold: "Amplada de línia a l'augmentar " zoomLinesThreshold: "Amplada de línia a l'augmentar "
zoomLinesMaskSize: "Diàmetre del centre" zoomLinesMaskSize: "Diàmetre del centre"
zoomLinesBlack: "Obscurir" zoomLinesBlack: "Obscurir"
circle: "Cercle"
drafts: "Esborrany " drafts: "Esborrany "
_drafts: _drafts:
select: "Seleccionar esborrany" select: "Seleccionar esborrany"
@ -3261,6 +3287,9 @@ _drafts:
restoreFromDraft: "Restaurar des dels esborranys" restoreFromDraft: "Restaurar des dels esborranys"
restore: "Restaurar esborrany" restore: "Restaurar esborrany"
listDrafts: "Llistat d'esborranys" listDrafts: "Llistat d'esborranys"
schedule: "Programació esborranys"
listScheduledNotes: "Llista de notes programades"
cancelSchedule: "Cancel·lar la programació"
qr: "Codi QR" qr: "Codi QR"
_qr: _qr:
showTabTitle: "Veure" showTabTitle: "Veure"

View File

@ -253,6 +253,7 @@ noteDeleteConfirm: "Are you sure you want to delete this note?"
pinLimitExceeded: "You cannot pin any more notes" pinLimitExceeded: "You cannot pin any more notes"
done: "Done" done: "Done"
processing: "Processing..." processing: "Processing..."
preprocessing: "Preparing..."
preview: "Preview" preview: "Preview"
default: "Default" default: "Default"
defaultValueIs: "Default: {value}" 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." 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." federationDisabled: "Federation is disabled on this server. You cannot interact with users on other servers."
draft: "Drafts" draft: "Drafts"
draftsAndScheduledNotes: "Drafts and scheduled notes"
confirmOnReact: "Confirm when reacting" confirmOnReact: "Confirm when reacting"
reactAreYouSure: "Would you like to add a \"{emoji}\" reaction?" reactAreYouSure: "Would you like to add a \"{emoji}\" reaction?"
markAsSensitiveConfirm: "Do you want to set this media as sensitive?" markAsSensitiveConfirm: "Do you want to set this media as sensitive?"
@ -1372,6 +1374,8 @@ redisplayAllTips: "Show all “Tips & Tricks” again"
hideAllTips: "Hide all \"Tips & Tricks\"" hideAllTips: "Hide all \"Tips & Tricks\""
defaultImageCompressionLevel: "Default image compression level" defaultImageCompressionLevel: "Default image compression level"
defaultImageCompressionLevel_description: "Lower level preserves image quality but increases file size.<br>Higher level reduce file size, but reduce image quality." defaultImageCompressionLevel_description: "Lower level preserves image quality but increases file size.<br>Higher level reduce file size, but reduce image quality."
defaultCompressionLevel: "Default compression level"
defaultCompressionLevel_description: "Lower compression preserves quality but increases file size.<br>Higher compression reduces file size but lowers quality."
inMinutes: "Minute(s)" inMinutes: "Minute(s)"
inDays: "Day(s)" inDays: "Day(s)"
safeModeEnabled: "Safe mode is enabled" safeModeEnabled: "Safe mode is enabled"
@ -1380,6 +1384,20 @@ customCssIsDisabledBecauseSafeMode: "Custom CSS is not applied because safe mode
themeIsDefaultBecauseSafeMode: "While safe mode is active, the default theme is used. Disabling safe mode will revert these changes." 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!" thankYouForTestingBeta: "Thank you for helping us test the beta version!"
createUserSpecifiedNote: "Create a direct note" 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: _order:
newest: "Newest First" newest: "Newest First"
oldest: "Oldest First" oldest: "Oldest First"
@ -2022,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_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." 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" noteDraftLimit: "Number of possible drafts of server notes"
scheduledNoteLimit: "Maximum number of simultaneous scheduled notes"
watermarkAvailable: "Watermark function" watermarkAvailable: "Watermark function"
_condition: _condition:
roleAssignedTo: "Assigned to manual roles" roleAssignedTo: "Assigned to manual roles"
@ -2647,6 +2666,8 @@ _notification:
youReceivedFollowRequest: "You've received a follow request" youReceivedFollowRequest: "You've received a follow request"
yourFollowRequestAccepted: "Your follow request was accepted" yourFollowRequestAccepted: "Your follow request was accepted"
pollEnded: "Poll results have become available" pollEnded: "Poll results have become available"
scheduledNotePosted: "Scheduled note has been posted"
scheduledNotePostFailed: "Failed to post scheduled note"
newNote: "New note" newNote: "New note"
unreadAntennaNote: "Antenna {name}" unreadAntennaNote: "Antenna {name}"
roleAssigned: "Role given" roleAssigned: "Role given"
@ -3201,6 +3222,8 @@ _imageEffector:
mirror: "Mirror" mirror: "Mirror"
invert: "Invert Colors" invert: "Invert Colors"
grayscale: "Grayscale" grayscale: "Grayscale"
blur: "Blur"
pixelate: "Pixelate"
colorAdjust: "Color Correction" colorAdjust: "Color Correction"
colorClamp: "Color Compression" colorClamp: "Color Compression"
colorClampAdvanced: "Color Compression (Advanced)" colorClampAdvanced: "Color Compression (Advanced)"
@ -3212,11 +3235,13 @@ _imageEffector:
checker: "Checker" checker: "Checker"
blockNoise: "Block Noise" blockNoise: "Block Noise"
tearing: "Tearing" tearing: "Tearing"
fillSquare: "Fill (Square)" fill: "Fill"
_fxProps: _fxProps:
angle: "Angle" angle: "Angle"
scale: "Size" scale: "Size"
size: "Size" size: "Size"
radius: "Radius"
samples: "Sample count"
offset: "Position" offset: "Position"
color: "Color" color: "Color"
opacity: "Opacity" opacity: "Opacity"
@ -3246,6 +3271,7 @@ _imageEffector:
zoomLinesThreshold: "Zoom line width" zoomLinesThreshold: "Zoom line width"
zoomLinesMaskSize: "Center diameter" zoomLinesMaskSize: "Center diameter"
zoomLinesBlack: "Make black" zoomLinesBlack: "Make black"
circle: "Circular"
drafts: "Drafts" drafts: "Drafts"
_drafts: _drafts:
select: "Select Draft" select: "Select Draft"
@ -3261,6 +3287,9 @@ _drafts:
restoreFromDraft: "Restore from Draft" restoreFromDraft: "Restore from Draft"
restore: "Restore" restore: "Restore"
listDrafts: "List of Drafts" listDrafts: "List of Drafts"
schedule: "Schedule note"
listScheduledNotes: "Scheduled notes list"
cancelSchedule: "Cancel schedule"
qr: "QR Code" qr: "QR Code"
_qr: _qr:
showTabTitle: "Display" showTabTitle: "Display"

View File

@ -253,6 +253,7 @@ noteDeleteConfirm: "¿Quieres borrar esta nota?"
pinLimitExceeded: "Ya no se pueden fijar más notas" pinLimitExceeded: "Ya no se pueden fijar más notas"
done: "Terminado" done: "Terminado"
processing: "Procesando..." processing: "Procesando..."
preprocessing: "Preparando"
preview: "Vista previa" preview: "Vista previa"
default: "Predeterminado" default: "Predeterminado"
defaultValueIs: "Por defecto: {value}" defaultValueIs: "Por defecto: {value}"
@ -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." 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" federationDisabled: "La federación está desactivada en este servidor. No puede interactuar con usuarios de otros servidores"
draft: "Borrador" draft: "Borrador"
draftsAndScheduledNotes: "Borradores y notas programadas"
confirmOnReact: "Confirmar la reacción" confirmOnReact: "Confirmar la reacción"
reactAreYouSure: "¿Quieres añadir una reacción «{emoji}»?" reactAreYouSure: "¿Quieres añadir una reacción «{emoji}»?"
markAsSensitiveConfirm: "¿Desea establecer este medio multimedia(Imagen,vídeo...) como sensible?" markAsSensitiveConfirm: "¿Desea establecer este medio multimedia(Imagen,vídeo...) como sensible?"
@ -1372,6 +1374,8 @@ redisplayAllTips: "Volver a mostrar todos \"Trucos y consejos\""
hideAllTips: "Ocultar todos los \"Trucos y consejos\"" hideAllTips: "Ocultar todos los \"Trucos y consejos\""
defaultImageCompressionLevel: "Nivel de compresión de la imagen por defecto" defaultImageCompressionLevel: "Nivel de compresión de la imagen por defecto"
defaultImageCompressionLevel_description: "Baja, conserva la calidad de la imagen pero la medida del archivo es más grande. <br>Alta, reduce la medida del archivo pero también la calidad de la imagen." defaultImageCompressionLevel_description: "Baja, conserva la calidad de la imagen pero la medida del archivo es más grande. <br>Alta, reduce la medida del archivo pero también la calidad de la imagen."
defaultCompressionLevel: "Nivel de compresión predeterminado"
defaultCompressionLevel_description: "Al reducir el ajuste se conserva la calidad, pero aumenta el tamaño del archivo.<br>Al aumentar el ajuste se reduce el tamaño del archivo, pero disminuye la calidad."
inMinutes: "Minutos" inMinutes: "Minutos"
inDays: "Días" inDays: "Días"
safeModeEnabled: "El modo seguro está activado" safeModeEnabled: "El modo seguro está activado"
@ -1380,6 +1384,20 @@ customCssIsDisabledBecauseSafeMode: "El modo seguro está activado, por lo que n
themeIsDefaultBecauseSafeMode: "Mientras el modo seguro esté activado, se utilizará el tema predeterminado. Cuando se desactive el modo seguro, se volverá al tema original." 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!" thankYouForTestingBeta: "¡Gracias por tu colaboración en la prueba de la versión beta!"
createUserSpecifiedNote: "Crear notas especificadas por el usuario" 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: _order:
newest: "Los más recientes primero" newest: "Los más recientes primero"
oldest: "Los más antiguos primero" oldest: "Los más antiguos primero"
@ -2022,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_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." 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" 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" watermarkAvailable: "Disponibilidad de la función de marca de agua"
_condition: _condition:
roleAssignedTo: "Asignado a roles manuales" roleAssignedTo: "Asignado a roles manuales"
@ -2647,6 +2666,8 @@ _notification:
youReceivedFollowRequest: "Has mandado una solicitud de seguimiento" youReceivedFollowRequest: "Has mandado una solicitud de seguimiento"
yourFollowRequestAccepted: "Tu solicitud de seguimiento fue aceptada" yourFollowRequestAccepted: "Tu solicitud de seguimiento fue aceptada"
pollEnded: "Estan disponibles los resultados de la encuesta" 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" newNote: "Nueva nota"
unreadAntennaNote: "Antena {name}" unreadAntennaNote: "Antena {name}"
roleAssigned: "Rol asignado" roleAssigned: "Rol asignado"
@ -3172,7 +3193,9 @@ _watermarkEditor:
opacity: "Opacidad" opacity: "Opacidad"
scale: "Tamaño" scale: "Tamaño"
text: "Texto" text: "Texto"
qr: "Código QR"
position: "Posición" position: "Posición"
margin: "Margen"
type: "Tipo" type: "Tipo"
image: "Imágenes" image: "Imágenes"
advanced: "Avanzado" advanced: "Avanzado"
@ -3187,6 +3210,7 @@ _watermarkEditor:
polkadotSubDotOpacity: "Opacidad del círculo secundario" polkadotSubDotOpacity: "Opacidad del círculo secundario"
polkadotSubDotRadius: "Tamaño del círculo secundario." polkadotSubDotRadius: "Tamaño del círculo secundario."
polkadotSubDotDivisions: "Número de subpuntos." polkadotSubDotDivisions: "Número de subpuntos."
leaveBlankToAccountUrl: "Si dejas este campo en blanco, se utilizará la URL de tu cuenta."
_imageEffector: _imageEffector:
title: "Efecto" title: "Efecto"
addEffect: "Añadir Efecto" addEffect: "Añadir Efecto"
@ -3198,6 +3222,8 @@ _imageEffector:
mirror: "Espejo" mirror: "Espejo"
invert: "Invertir colores" invert: "Invertir colores"
grayscale: "Blanco y negro" grayscale: "Blanco y negro"
blur: "Difuminar"
pixelate: "Pixelar"
colorAdjust: "Corrección de Color" colorAdjust: "Corrección de Color"
colorClamp: "Compresión cromática" colorClamp: "Compresión cromática"
colorClampAdvanced: "Compresión cromática avanzada" colorClampAdvanced: "Compresión cromática avanzada"
@ -3209,10 +3235,13 @@ _imageEffector:
checker: "Corrector" checker: "Corrector"
blockNoise: "Bloquear Ruido" blockNoise: "Bloquear Ruido"
tearing: "Rasgado de Imagen (Tearing)" tearing: "Rasgado de Imagen (Tearing)"
fill: "Relleno de color"
_fxProps: _fxProps:
angle: "Ángulo" angle: "Ángulo"
scale: "Tamaño" scale: "Tamaño"
size: "Tamaño" size: "Tamaño"
radius: "Radio"
samples: "Tamaño de muestra"
offset: "Posición" offset: "Posición"
color: "Color" color: "Color"
opacity: "Opacidad" opacity: "Opacidad"
@ -3242,6 +3271,7 @@ _imageEffector:
zoomLinesThreshold: "Ancho de línea del zoom" zoomLinesThreshold: "Ancho de línea del zoom"
zoomLinesMaskSize: "Diámetro del centro" zoomLinesMaskSize: "Diámetro del centro"
zoomLinesBlack: "Hacer oscuro" zoomLinesBlack: "Hacer oscuro"
circle: "Círculo"
drafts: "Borrador" drafts: "Borrador"
_drafts: _drafts:
select: "Seleccionar borradores" select: "Seleccionar borradores"
@ -3257,6 +3287,22 @@ _drafts:
restoreFromDraft: "Restaurar desde los borradores" restoreFromDraft: "Restaurar desde los borradores"
restore: "Restaurar" restore: "Restaurar"
listDrafts: "Listar los borradores" listDrafts: "Listar los borradores"
schedule: "Programar Nota"
listScheduledNotes: "Lista de notas programadas"
cancelSchedule: "Cancelar programación"
qr: "Código QR"
_qr: _qr:
showTabTitle: "Apariencia" 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" raw: "Texto"
mfm: "MFM"

288
locales/index.d.ts vendored
View File

@ -1030,6 +1030,10 @@ export interface Locale extends ILocale {
* *
*/ */
"processing": string; "processing": string;
/**
*
*/
"preprocessing": string;
/** /**
* *
*/ */
@ -1227,7 +1231,7 @@ export interface Locale extends ILocale {
*/ */
"noMoreHistory": string; "noMoreHistory": string;
/** /**
* *
*/ */
"startChat": string; "startChat": string;
/** /**
@ -1927,7 +1931,7 @@ export interface Locale extends ILocale {
*/ */
"markAsReadAllUnreadNotes": string; "markAsReadAllUnreadNotes": string;
/** /**
* *
*/ */
"markAsReadAllTalkMessages": string; "markAsReadAllTalkMessages": string;
/** /**
@ -5282,6 +5286,10 @@ export interface Locale extends ILocale {
* *
*/ */
"draft": string; "draft": string;
/**
* 稿
*/
"draftsAndScheduledNotes": string;
/** /**
* *
*/ */
@ -5390,6 +5398,14 @@ export interface Locale extends ILocale {
* *
*/ */
"chat": string; "chat": string;
/**
*
*/
"directMessage": string;
/**
*
*/
"directMessage_short": string;
/** /**
* *
*/ */
@ -5501,6 +5517,14 @@ export interface Locale extends ILocale {
* <br> * <br>
*/ */
"defaultImageCompressionLevel_description": string; "defaultImageCompressionLevel_description": string;
/**
*
*/
"defaultCompressionLevel": string;
/**
* <br>
*/
"defaultCompressionLevel_description": string;
/** /**
* *
*/ */
@ -5529,6 +5553,60 @@ export interface Locale extends ILocale {
* *
*/ */
"thankYouForTestingBeta": string; "thankYouForTestingBeta": string;
/**
*
*/
"createUserSpecifiedNote": string;
/**
* 稿
*/
"schedulePost": string;
/**
* {x}稿
*/
"scheduleToPostOnX": ParameterizedString<"x">;
/**
* {x}稿
*/
"scheduledToPostOnX": ParameterizedString<"x">;
/**
*
*/
"schedule": string;
/**
*
*/
"scheduled": string;
"_compression": {
"_quality": {
/**
*
*/
"high": string;
/**
*
*/
"medium": string;
/**
*
*/
"low": string;
};
"_size": {
/**
*
*/
"large": string;
/**
*
*/
"medium": string;
/**
*
*/
"small": string;
};
};
"_order": { "_order": {
/** /**
* *
@ -5540,6 +5618,10 @@ export interface Locale extends ILocale {
"oldest": string; "oldest": string;
}; };
"_chat": { "_chat": {
/**
*
*/
"messages": string;
/** /**
* *
*/ */
@ -5549,36 +5631,36 @@ export interface Locale extends ILocale {
*/ */
"newMessage": string; "newMessage": string;
/** /**
* *
*/ */
"individualChat": string; "individualChat": string;
/** /**
* *
*/ */
"individualChat_description": string; "individualChat_description": string;
/** /**
* *
*/ */
"roomChat": string; "roomChat": string;
/** /**
* *
* *
*/ */
"roomChat_description": string; "roomChat_description": string;
/** /**
* *
*/ */
"createRoom": string; "createRoom": string;
/** /**
* *
*/ */
"inviteUserToChat": string; "inviteUserToChat": string;
/** /**
* *
*/ */
"yourRooms": string; "yourRooms": string;
/** /**
* *
*/ */
"joiningRooms": string; "joiningRooms": string;
/** /**
@ -5598,7 +5680,7 @@ export interface Locale extends ILocale {
*/ */
"noHistory": string; "noHistory": string;
/** /**
* *
*/ */
"noRooms": string; "noRooms": string;
/** /**
@ -5618,7 +5700,7 @@ export interface Locale extends ILocale {
*/ */
"ignore": string; "ignore": string;
/** /**
* 退 * 退
*/ */
"leave": string; "leave": string;
/** /**
@ -5642,35 +5724,35 @@ export interface Locale extends ILocale {
*/ */
"newline": string; "newline": string;
/** /**
* *
*/ */
"muteThisRoom": string; "muteThisRoom": string;
/** /**
* *
*/ */
"deleteRoom": string; "deleteRoom": string;
/** /**
* *
*/ */
"chatNotAvailableForThisAccountOrServer": string; "chatNotAvailableForThisAccountOrServer": string;
/** /**
* *
*/ */
"chatIsReadOnlyForThisAccountOrServer": string; "chatIsReadOnlyForThisAccountOrServer": string;
/** /**
* 使 * 使
*/ */
"chatNotAvailableInOtherAccount": string; "chatNotAvailableInOtherAccount": string;
/** /**
* *
*/ */
"cannotChatWithTheUser": string; "cannotChatWithTheUser": string;
/** /**
* 使 * 使
*/ */
"cannotChatWithTheUser_description": string; "cannotChatWithTheUser_description": string;
/** /**
* *
*/ */
"youAreNotAMemberOfThisRoomButInvited": string; "youAreNotAMemberOfThisRoomButInvited": string;
/** /**
@ -5678,31 +5760,31 @@ export interface Locale extends ILocale {
*/ */
"doYouAcceptInvitation": string; "doYouAcceptInvitation": string;
/** /**
* *
*/ */
"chatWithThisUser": string; "chatWithThisUser": string;
/** /**
* *
*/ */
"thisUserAllowsChatOnlyFromFollowers": string; "thisUserAllowsChatOnlyFromFollowers": string;
/** /**
* *
*/ */
"thisUserAllowsChatOnlyFromFollowing": string; "thisUserAllowsChatOnlyFromFollowing": string;
/** /**
* *
*/ */
"thisUserAllowsChatOnlyFromMutualFollowing": string; "thisUserAllowsChatOnlyFromMutualFollowing": string;
/** /**
* *
*/ */
"thisUserNotAllowedChatAnyone": string; "thisUserNotAllowedChatAnyone": string;
/** /**
* *
*/ */
"chatAllowedUsers": string; "chatAllowedUsers": string;
/** /**
* *
*/ */
"chatAllowedUsers_note": string; "chatAllowedUsers_note": string;
"_chatAllowedUsers": { "_chatAllowedUsers": {
@ -7856,7 +7938,7 @@ export interface Locale extends ILocale {
*/ */
"canImportUserLists": string; "canImportUserLists": string;
/** /**
* *
*/ */
"chatAvailability": string; "chatAvailability": string;
/** /**
@ -7875,6 +7957,10 @@ export interface Locale extends ILocale {
* *
*/ */
"noteDraftLimit": string; "noteDraftLimit": string;
/**
* 稿
*/
"scheduledNoteLimit": string;
/** /**
* 使 * 使
*/ */
@ -8706,7 +8792,7 @@ export interface Locale extends ILocale {
*/ */
"badge": string; "badge": string;
/** /**
* *
*/ */
"messageBg": string; "messageBg": string;
/** /**
@ -8733,7 +8819,7 @@ export interface Locale extends ILocale {
*/ */
"reaction": string; "reaction": string;
/** /**
* *
*/ */
"chatMessage": string; "chatMessage": string;
}; };
@ -9017,11 +9103,11 @@ export interface Locale extends ILocale {
*/ */
"write:following": string; "write:following": string;
/** /**
* *
*/ */
"read:messaging": string; "read:messaging": string;
/** /**
* *
*/ */
"write:messaging": string; "write:messaging": string;
/** /**
@ -9313,11 +9399,11 @@ export interface Locale extends ILocale {
*/ */
"write:report-abuse": string; "write:report-abuse": string;
/** /**
* *
*/ */
"write:chat": string; "write:chat": string;
/** /**
* *
*/ */
"read:chat": string; "read:chat": string;
}; };
@ -9543,7 +9629,7 @@ export interface Locale extends ILocale {
*/ */
"birthdayFollowings": string; "birthdayFollowings": string;
/** /**
* *
*/ */
"chat": string; "chat": string;
}; };
@ -10270,6 +10356,14 @@ export interface Locale extends ILocale {
* *
*/ */
"pollEnded": string; "pollEnded": string;
/**
* 稿
*/
"scheduledNotePosted": string;
/**
* 稿
*/
"scheduledNotePostFailed": string;
/** /**
* 稿 * 稿
*/ */
@ -10283,7 +10377,7 @@ export interface Locale extends ILocale {
*/ */
"roleAssigned": string; "roleAssigned": string;
/** /**
* *
*/ */
"chatRoomInvitationReceived": string; "chatRoomInvitationReceived": string;
/** /**
@ -10396,7 +10490,7 @@ export interface Locale extends ILocale {
*/ */
"roleAssigned": string; "roleAssigned": string;
/** /**
* *
*/ */
"chatRoomInvitationReceived": string; "chatRoomInvitationReceived": string;
/** /**
@ -10578,7 +10672,7 @@ export interface Locale extends ILocale {
*/ */
"roleTimeline": string; "roleTimeline": string;
/** /**
* *
*/ */
"chat": string; "chat": string;
}; };
@ -10945,7 +11039,7 @@ export interface Locale extends ILocale {
*/ */
"deleteGalleryPost": string; "deleteGalleryPost": string;
/** /**
* *
*/ */
"deleteChatRoom": string; "deleteChatRoom": string;
/** /**
@ -12219,10 +12313,18 @@ export interface Locale extends ILocale {
* *
*/ */
"text": string; "text": string;
/**
*
*/
"qr": string;
/** /**
* *
*/ */
"position": string; "position": string;
/**
*
*/
"margin": string;
/** /**
* *
*/ */
@ -12279,6 +12381,10 @@ export interface Locale extends ILocale {
* *
*/ */
"polkadotSubDotDivisions": string; "polkadotSubDotDivisions": string;
/**
* URLになります
*/
"leaveBlankToAccountUrl": string;
}; };
"_imageEffector": { "_imageEffector": {
/** /**
@ -12318,6 +12424,14 @@ export interface Locale extends ILocale {
* *
*/ */
"grayscale": string; "grayscale": string;
/**
*
*/
"blur": string;
/**
*
*/
"pixelate": string;
/** /**
* 調 * 調
*/ */
@ -12362,6 +12476,10 @@ export interface Locale extends ILocale {
* *
*/ */
"tearing": string; "tearing": string;
/**
*
*/
"fill": string;
}; };
"_fxProps": { "_fxProps": {
/** /**
@ -12376,6 +12494,18 @@ export interface Locale extends ILocale {
* *
*/ */
"size": string; "size": string;
/**
*
*/
"radius": string;
/**
*
*/
"samples": string;
/**
*
*/
"offset": string;
/** /**
* *
*/ */
@ -12488,6 +12618,10 @@ export interface Locale extends ILocale {
* *
*/ */
"zoomLinesBlack": string; "zoomLinesBlack": string;
/**
*
*/
"circle": string;
}; };
}; };
/** /**
@ -12547,6 +12681,80 @@ export interface Locale extends ILocale {
* *
*/ */
"listDrafts": string; "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: { declare const locales: {

View File

@ -253,6 +253,7 @@ noteDeleteConfirm: "Vuoi davvero eliminare questa Nota?"
pinLimitExceeded: "Non puoi fissare altre note " pinLimitExceeded: "Non puoi fissare altre note "
done: "Fine" done: "Fine"
processing: "In elaborazione" processing: "In elaborazione"
preprocessing: "In preparazione"
preview: "Anteprima" preview: "Anteprima"
default: "Predefinito" default: "Predefinito"
defaultValueIs: "Predefinito: {value}" defaultValueIs: "Predefinito: {value}"
@ -1316,6 +1317,7 @@ acknowledgeNotesAndEnable: "Attivare dopo averne compreso il comportamento."
federationSpecified: "Questo server è federato solo con istanze specifiche del Fediverso. Puoi interagire solo con quelle scelte dall'amministrazione." federationSpecified: "Questo server è federato solo con istanze specifiche del Fediverso. Puoi interagire solo con quelle scelte dall'amministrazione."
federationDisabled: "Questo server ha la federazione disabilitata. Non puoi interagire con profili provenienti da altri server." federationDisabled: "Questo server ha la federazione disabilitata. Non puoi interagire con profili provenienti da altri server."
draft: "Bozza" draft: "Bozza"
draftsAndScheduledNotes: "Bozze e Note pianificate"
confirmOnReact: "Confermare le reazioni" confirmOnReact: "Confermare le reazioni"
reactAreYouSure: "Vuoi davvero reagire con {emoji} ?" reactAreYouSure: "Vuoi davvero reagire con {emoji} ?"
markAsSensitiveConfirm: "Vuoi davvero indicare questo contenuto multimediale come esplicito?" markAsSensitiveConfirm: "Vuoi davvero indicare questo contenuto multimediale come esplicito?"
@ -1372,6 +1374,8 @@ redisplayAllTips: "Mostra tutti i suggerimenti"
hideAllTips: "Nascondi tutti i suggerimenti" hideAllTips: "Nascondi tutti i suggerimenti"
defaultImageCompressionLevel: "Livello predefinito di compressione immagini" defaultImageCompressionLevel: "Livello predefinito di compressione immagini"
defaultImageCompressionLevel_description: "La compressione diminuisce la qualità dell'immagine, poca compressione mantiene alta qualità delle immagini. Aumentandola, si riducono le dimensioni del file, a discapito della qualità dell'immagine." defaultImageCompressionLevel_description: "La compressione diminuisce la qualità dell'immagine, poca compressione mantiene alta qualità delle immagini. Aumentandola, si riducono le dimensioni del file, a discapito della qualità dell'immagine."
defaultCompressionLevel: "Compressione predefinita"
defaultCompressionLevel_description: "Diminuisci per mantenere la qualità aumentando le dimensioni del file.<br> Aumenta per ridurre le dimensioni del file e anche la qualità."
inMinutes: "min" inMinutes: "min"
inDays: "giorni" inDays: "giorni"
safeModeEnabled: "La modalità sicura è attiva" safeModeEnabled: "La modalità sicura è attiva"
@ -1380,6 +1384,20 @@ customCssIsDisabledBecauseSafeMode: "Il CSS personalizzato non è stato applicat
themeIsDefaultBecauseSafeMode: "Quando la modalità sicura è attiva, viene utilizzato il tema predefinito. Quando la modalità sicura viene disattivata, il tema torna a essere quello precedente." 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!" thankYouForTestingBeta: "Grazie per la tua collaborazione nella verifica delle versioni beta!"
createUserSpecifiedNote: "Creare Nota personalizzata" 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: _order:
newest: "Prima i più recenti" newest: "Prima i più recenti"
oldest: "Meno recenti prima" oldest: "Meno recenti prima"
@ -2022,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_caption: "Specifica il tipo MIME. Puoi specificare più valori separandoli andando a capo, oppure indicare caratteri jolly con un asterisco (*). Ad esempio: image/*"
uploadableFileTypes_caption2: "A seconda del file, il tipo potrebbe non essere determinato. Se si desidera consentire tali file, aggiungere {x} alla specifica." uploadableFileTypes_caption2: "A seconda del file, il tipo potrebbe non essere determinato. Se si desidera consentire tali file, aggiungere {x} alla specifica."
noteDraftLimit: "Numero massimo di Note in bozza, lato server" noteDraftLimit: "Numero massimo di Note in bozza, lato server"
scheduledNoteLimit: "Quantità di Note pianificabili contemporaneamente"
watermarkAvailable: "Disponibilità della funzione filigrana" watermarkAvailable: "Disponibilità della funzione filigrana"
_condition: _condition:
roleAssignedTo: "Assegnato a ruoli manualmente" roleAssignedTo: "Assegnato a ruoli manualmente"
@ -2647,6 +2666,8 @@ _notification:
youReceivedFollowRequest: "Hai ricevuto una richiesta di follow" youReceivedFollowRequest: "Hai ricevuto una richiesta di follow"
yourFollowRequestAccepted: "La tua richiesta di follow è stata accettata" yourFollowRequestAccepted: "La tua richiesta di follow è stata accettata"
pollEnded: "Risultati del sondaggio." pollEnded: "Risultati del sondaggio."
scheduledNotePosted: "Pubblicazione Nota pianificata"
scheduledNotePostFailed: "Impossibile pubblicare la Nota pianificata"
newNote: "Nuove Note" newNote: "Nuove Note"
unreadAntennaNote: "Antenna {name}" unreadAntennaNote: "Antenna {name}"
roleAssigned: "Ruolo assegnato" roleAssigned: "Ruolo assegnato"
@ -3172,7 +3193,9 @@ _watermarkEditor:
opacity: "Opacità" opacity: "Opacità"
scale: "Dimensioni" scale: "Dimensioni"
text: "Testo" text: "Testo"
qr: "QR Code"
position: "Posizione" position: "Posizione"
margin: "Margine"
type: "Tipo" type: "Tipo"
image: "Immagini" image: "Immagini"
advanced: "Avanzato" advanced: "Avanzato"
@ -3187,6 +3210,7 @@ _watermarkEditor:
polkadotSubDotOpacity: "Opacità del punto secondario" polkadotSubDotOpacity: "Opacità del punto secondario"
polkadotSubDotRadius: "Dimensione del punto secondario" polkadotSubDotRadius: "Dimensione del punto secondario"
polkadotSubDotDivisions: "Quantità di punti secondari" polkadotSubDotDivisions: "Quantità di punti secondari"
leaveBlankToAccountUrl: "Il valore vuoto indica la URL dell'account"
_imageEffector: _imageEffector:
title: "Effetto" title: "Effetto"
addEffect: "Aggiungi effetto" addEffect: "Aggiungi effetto"
@ -3198,6 +3222,8 @@ _imageEffector:
mirror: "Specchio" mirror: "Specchio"
invert: "Inversione colore" invert: "Inversione colore"
grayscale: "Bianco e nero" grayscale: "Bianco e nero"
blur: "Sfocatura"
pixelate: "Mosaico"
colorAdjust: "Correzione Colore" colorAdjust: "Correzione Colore"
colorClamp: "Compressione del colore" colorClamp: "Compressione del colore"
colorClampAdvanced: "Compressione del colore (avanzata)" colorClampAdvanced: "Compressione del colore (avanzata)"
@ -3209,11 +3235,13 @@ _imageEffector:
checker: "revisore" checker: "revisore"
blockNoise: "Attenua rumore" blockNoise: "Attenua rumore"
tearing: "Strappa immagine" tearing: "Strappa immagine"
fillSquare: "Riempi (quadrato)" fill: "Riempimento"
_fxProps: _fxProps:
angle: "Angolo" angle: "Angolo"
scale: "Dimensioni" scale: "Dimensioni"
size: "Dimensioni" size: "Dimensioni"
radius: "Raggio"
samples: "Quantità di campioni"
offset: "Posizione" offset: "Posizione"
color: "Colore" color: "Colore"
opacity: "Opacità" opacity: "Opacità"
@ -3243,6 +3271,7 @@ _imageEffector:
zoomLinesThreshold: "Limite delle linee zoom" zoomLinesThreshold: "Limite delle linee zoom"
zoomLinesMaskSize: "Ampiezza del diametro" zoomLinesMaskSize: "Ampiezza del diametro"
zoomLinesBlack: "Bande nere" zoomLinesBlack: "Bande nere"
circle: "Circolare"
drafts: "Bozze" drafts: "Bozze"
_drafts: _drafts:
select: "Selezionare bozza" select: "Selezionare bozza"
@ -3258,6 +3287,22 @@ _drafts:
restoreFromDraft: "Recuperare dalle bozze" restoreFromDraft: "Recuperare dalle bozze"
restore: "Ripristina" restore: "Ripristina"
listDrafts: "Elenco bozze" listDrafts: "Elenco bozze"
schedule: "Pianifica pubblicazione"
listScheduledNotes: "Elenca Note pianificate"
cancelSchedule: "Annulla pianificazione"
qr: "QR Code"
_qr: _qr:
showTabTitle: "Visualizza" 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" raw: "Testo"
mfm: "MFM"

View File

@ -253,6 +253,7 @@ noteDeleteConfirm: "このノートを削除しますか?"
pinLimitExceeded: "これ以上ピン留めできません" pinLimitExceeded: "これ以上ピン留めできません"
done: "完了" done: "完了"
processing: "処理中" processing: "処理中"
preprocessing: "準備中"
preview: "プレビュー" preview: "プレビュー"
default: "デフォルト" default: "デフォルト"
defaultValueIs: "デフォルト: {value}" defaultValueIs: "デフォルト: {value}"
@ -302,7 +303,7 @@ uploadNFiles: "{n}個のファイルをアップロード"
explore: "みつける" explore: "みつける"
messageRead: "既読" messageRead: "既読"
noMoreHistory: "これより過去の履歴はありません" noMoreHistory: "これより過去の履歴はありません"
startChat: "チャットを始める" startChat: "メッセージを送る"
nUsersRead: "{n}人が読みました" nUsersRead: "{n}人が読みました"
agreeTo: "{0}に同意" agreeTo: "{0}に同意"
agree: "同意する" agree: "同意する"
@ -477,7 +478,7 @@ notFoundDescription: "指定されたURLに該当するページはありませ
uploadFolder: "既定アップロード先" uploadFolder: "既定アップロード先"
markAsReadAllNotifications: "すべての通知を既読にする" markAsReadAllNotifications: "すべての通知を既読にする"
markAsReadAllUnreadNotes: "すべての投稿を既読にする" markAsReadAllUnreadNotes: "すべての投稿を既読にする"
markAsReadAllTalkMessages: "すべてのチャットを既読にする" markAsReadAllTalkMessages: "すべてのダイレクトメッセージを既読にする"
help: "ヘルプ" help: "ヘルプ"
inputMessageHere: "ここにメッセージを入力" inputMessageHere: "ここにメッセージを入力"
close: "閉じる" close: "閉じる"
@ -1316,6 +1317,7 @@ acknowledgeNotesAndEnable: "注意事項を理解した上でオンにします
federationSpecified: "このサーバーはホワイトリスト連合で運用されています。管理者が指定したサーバー以外とやり取りすることはできません。" federationSpecified: "このサーバーはホワイトリスト連合で運用されています。管理者が指定したサーバー以外とやり取りすることはできません。"
federationDisabled: "このサーバーは連合が無効化されています。他のサーバーのユーザーとやり取りすることはできません。" federationDisabled: "このサーバーは連合が無効化されています。他のサーバーのユーザーとやり取りすることはできません。"
draft: "下書き" draft: "下書き"
draftsAndScheduledNotes: "下書きと予約投稿"
confirmOnReact: "リアクションする際に確認する" confirmOnReact: "リアクションする際に確認する"
reactAreYouSure: "\" {emoji} \" をリアクションしますか?" reactAreYouSure: "\" {emoji} \" をリアクションしますか?"
markAsSensitiveConfirm: "このメディアをセンシティブとして設定しますか?" markAsSensitiveConfirm: "このメディアをセンシティブとして設定しますか?"
@ -1343,6 +1345,8 @@ postForm: "投稿フォーム"
textCount: "文字数" textCount: "文字数"
information: "情報" information: "情報"
chat: "チャット" chat: "チャット"
directMessage: "ダイレクトメッセージ"
directMessage_short: "メッセージ"
migrateOldSettings: "旧設定情報を移行" migrateOldSettings: "旧設定情報を移行"
migrateOldSettings_description: "通常これは自動で行われていますが、何らかの理由により上手く移行されなかった場合は手動で移行処理をトリガーできます。現在の設定情報は上書きされます。" migrateOldSettings_description: "通常これは自動で行われていますが、何らかの理由により上手く移行されなかった場合は手動で移行処理をトリガーできます。現在の設定情報は上書きされます。"
compress: "圧縮" compress: "圧縮"
@ -1370,6 +1374,8 @@ redisplayAllTips: "全ての「ヒントとコツ」を再表示"
hideAllTips: "全ての「ヒントとコツ」を非表示" hideAllTips: "全ての「ヒントとコツ」を非表示"
defaultImageCompressionLevel: "デフォルトの画像圧縮度" defaultImageCompressionLevel: "デフォルトの画像圧縮度"
defaultImageCompressionLevel_description: "低くすると画質を保てますが、ファイルサイズは増加します。<br>高くするとファイルサイズを減らせますが、画質は低下します。" defaultImageCompressionLevel_description: "低くすると画質を保てますが、ファイルサイズは増加します。<br>高くするとファイルサイズを減らせますが、画質は低下します。"
defaultCompressionLevel: "デフォルトの圧縮度"
defaultCompressionLevel_description: "低くすると品質を保てますが、ファイルサイズは増加します。<br>高くするとファイルサイズを減らせますが、品質は低下します。"
inMinutes: "分" inMinutes: "分"
inDays: "日" inDays: "日"
safeModeEnabled: "セーフモードが有効です" safeModeEnabled: "セーフモードが有効です"
@ -1377,53 +1383,70 @@ pluginsAreDisabledBecauseSafeMode: "セーフモードが有効なため、プ
customCssIsDisabledBecauseSafeMode: "セーフモードが有効なため、カスタムCSSは適用されていません。" customCssIsDisabledBecauseSafeMode: "セーフモードが有効なため、カスタムCSSは適用されていません。"
themeIsDefaultBecauseSafeMode: "セーフモードが有効な間はデフォルトのテーマが使用されます。セーフモードをオフにすると元に戻ります。" themeIsDefaultBecauseSafeMode: "セーフモードが有効な間はデフォルトのテーマが使用されます。セーフモードをオフにすると元に戻ります。"
thankYouForTestingBeta: "ベータ版の検証にご協力いただきありがとうございます!" thankYouForTestingBeta: "ベータ版の検証にご協力いただきありがとうございます!"
createUserSpecifiedNote: "ユーザー指定ノートを作成"
schedulePost: "投稿を予約"
scheduleToPostOnX: "{x}に投稿を予約します"
scheduledToPostOnX: "{x}に投稿が予約されています"
schedule: "予約"
scheduled: "予約"
_compression:
_quality:
high: "高品質"
medium: "中品質"
low: "低品質"
_size:
large: "サイズ大"
medium: "サイズ中"
small: "サイズ小"
_order: _order:
newest: "新しい順" newest: "新しい順"
oldest: "古い順" oldest: "古い順"
_chat: _chat:
messages: "メッセージ"
noMessagesYet: "まだメッセージはありません" noMessagesYet: "まだメッセージはありません"
newMessage: "新しいメッセージ" newMessage: "新しいメッセージ"
individualChat: "個人チャット" individualChat: "個"
individualChat_description: "特定ユーザーとの一対一のチャットができます。" individualChat_description: "特定ユーザーと個別にメッセージのやりとりができます。"
roomChat: "ルームチャット" roomChat: "グループ"
roomChat_description: "複数人でのチャットができます。\nまた、個人チャットを許可していないユーザーとでも、相手が受け入れればチャットができます。" roomChat_description: "複数人でメッセージのやりとりができます。\nまた、個別のメッセージを許可していないユーザーとでも、相手が受け入れればやりとりできます。"
createRoom: "ルームを作成" createRoom: "グループを作成"
inviteUserToChat: "ユーザーを招待してチャットを始めましょう" inviteUserToChat: "ユーザーを招待してメッセージを送信しましょう"
yourRooms: "作成したルーム" yourRooms: "作成したグループ"
joiningRooms: "参加中のルーム" joiningRooms: "参加中のグループ"
invitations: "招待" invitations: "招待"
noInvitations: "招待はありません" noInvitations: "招待はありません"
history: "履歴" history: "履歴"
noHistory: "履歴はありません" noHistory: "履歴はありません"
noRooms: "ルームはありません" noRooms: "グループはありません"
inviteUser: "ユーザーを招待" inviteUser: "ユーザーを招待"
sentInvitations: "送信した招待" sentInvitations: "送信した招待"
join: "参加" join: "参加"
ignore: "無視" ignore: "無視"
leave: "ルームから退出" leave: "グループから退出"
members: "メンバー" members: "メンバー"
searchMessages: "メッセージを検索" searchMessages: "メッセージを検索"
home: "ホーム" home: "ホーム"
send: "送信" send: "送信"
newline: "改行" newline: "改行"
muteThisRoom: "このルームをミュート" muteThisRoom: "このグループをミュート"
deleteRoom: "ルームを削除" deleteRoom: "グループを削除"
chatNotAvailableForThisAccountOrServer: "このサーバー、またはこのアカウントでチャットは有効化されていません。" chatNotAvailableForThisAccountOrServer: "このサーバー、またはこのアカウントでダイレクトメッセージは有効化されていません。"
chatIsReadOnlyForThisAccountOrServer: "このサーバー、またはこのアカウントでチャットは読み取り専用となっています。新たに書き込んだり、チャットルームを作成・参加したりすることはできません。" chatIsReadOnlyForThisAccountOrServer: "このサーバー、またはこのアカウントでダイレクトメッセージは読み取り専用となっています。新たに書き込んだり、グループを作成・参加したりすることはできません。"
chatNotAvailableInOtherAccount: "相手のアカウントでチャット機能が使えない状態になっています。" chatNotAvailableInOtherAccount: "相手のアカウントでダイレクトメッセージが使えない状態になっています。"
cannotChatWithTheUser: "このユーザーとのチャットを開始できません" cannotChatWithTheUser: "このユーザーとのダイレクトメッセージを開始できません"
cannotChatWithTheUser_description: "チャットが使えない状態になっているか、相手がチャットを開放していません。" cannotChatWithTheUser_description: "ダイレクトメッセージが使えない状態になっているか、相手がダイレクトメッセージを開放していません。"
youAreNotAMemberOfThisRoomButInvited: "あなたはこのルームの参加者ではありませんが、招待が届いています。参加するには、招待を承認してください。" youAreNotAMemberOfThisRoomButInvited: "あなたはこのグループの参加者ではありませんが、招待が届いています。参加するには、招待を承認してください。"
doYouAcceptInvitation: "招待を承認しますか?" doYouAcceptInvitation: "招待を承認しますか?"
chatWithThisUser: "チャットする" chatWithThisUser: "ダイレクトメッセージ"
thisUserAllowsChatOnlyFromFollowers: "このユーザーはフォロワーからのみチャットを受け付けています。" thisUserAllowsChatOnlyFromFollowers: "このユーザーはフォロワーからのみメッセージを受け付けています。"
thisUserAllowsChatOnlyFromFollowing: "このユーザーは、このユーザーがフォローしているユーザーからのみチャットを受け付けています。" thisUserAllowsChatOnlyFromFollowing: "このユーザーは、このユーザーがフォローしているユーザーからのみメッセージを受け付けています。"
thisUserAllowsChatOnlyFromMutualFollowing: "このユーザーは相互フォローのユーザーからのみチャットを受け付けています。" thisUserAllowsChatOnlyFromMutualFollowing: "このユーザーは相互フォローのユーザーからのみメッセージを受け付けています。"
thisUserNotAllowedChatAnyone: "このユーザーは誰からもチャットを受け付けていません。" thisUserNotAllowedChatAnyone: "このユーザーは誰からもメッセージを受け付けていません。"
chatAllowedUsers: "チャットを許可する相手" chatAllowedUsers: "メッセージを許可する相手"
chatAllowedUsers_note: "自分からチャットメッセージを送った相手とはこの設定に関わらずチャットが可能です。" chatAllowedUsers_note: "自分からメッセージを送った相手とはこの設定に関わらずメッセージの送受信が可能です。"
_chatAllowedUsers: _chatAllowedUsers:
everyone: "誰でも" everyone: "誰でも"
followers: "自分のフォロワーのみ" followers: "自分のフォロワーのみ"
@ -2034,11 +2057,12 @@ _role:
canImportFollowing: "フォローのインポートを許可" canImportFollowing: "フォローのインポートを許可"
canImportMuting: "ミュートのインポートを許可" canImportMuting: "ミュートのインポートを許可"
canImportUserLists: "リストのインポートを許可" canImportUserLists: "リストのインポートを許可"
chatAvailability: "チャットを許可" chatAvailability: "ダイレクトメッセージを許可"
uploadableFileTypes: "アップロード可能なファイル種別" uploadableFileTypes: "アップロード可能なファイル種別"
uploadableFileTypes_caption: "MIMEタイプを指定します。改行で区切って複数指定できるほか、アスタリスク(*)でワイルドカード指定できます。(例: image/*)" uploadableFileTypes_caption: "MIMEタイプを指定します。改行で区切って複数指定できるほか、アスタリスク(*)でワイルドカード指定できます。(例: image/*)"
uploadableFileTypes_caption2: "ファイルによっては種別を判定できないことがあります。そのようなファイルを許可する場合は {x} を指定に追加してください。" uploadableFileTypes_caption2: "ファイルによっては種別を判定できないことがあります。そのようなファイルを許可する場合は {x} を指定に追加してください。"
noteDraftLimit: "サーバーサイドのノートの下書きの作成可能数" noteDraftLimit: "サーバーサイドのノートの下書きの作成可能数"
scheduledNoteLimit: "予約投稿の同時作成可能数"
watermarkAvailable: "ウォーターマーク機能の使用可否" watermarkAvailable: "ウォーターマーク機能の使用可否"
_condition: _condition:
roleAssignedTo: "マニュアルロールにアサイン済み" roleAssignedTo: "マニュアルロールにアサイン済み"
@ -2281,7 +2305,7 @@ _theme:
buttonHoverBg: "ボタンの背景 (ホバー)" buttonHoverBg: "ボタンの背景 (ホバー)"
inputBorder: "入力ボックスの縁取り" inputBorder: "入力ボックスの縁取り"
badge: "バッジ" badge: "バッジ"
messageBg: "チャットの背景" messageBg: "メッセージの背景"
fgHighlighted: "強調された文字" fgHighlighted: "強調された文字"
_sfx: _sfx:
@ -2289,7 +2313,7 @@ _sfx:
noteMy: "ノート(自分)" noteMy: "ノート(自分)"
notification: "通知" notification: "通知"
reaction: "リアクション選択時" reaction: "リアクション選択時"
chatMessage: "チャットのメッセージ" chatMessage: "ダイレクトメッセージ"
_soundSettings: _soundSettings:
driveFile: "ドライブの音声を使用" driveFile: "ドライブの音声を使用"
@ -2369,8 +2393,8 @@ _permissions:
"write:favorites": "お気に入りを操作する" "write:favorites": "お気に入りを操作する"
"read:following": "フォローの情報を見る" "read:following": "フォローの情報を見る"
"write:following": "フォロー・フォロー解除する" "write:following": "フォロー・フォロー解除する"
"read:messaging": "チャットを見る" "read:messaging": "ダイレクトメッセージを見る"
"write:messaging": "チャットを操作する" "write:messaging": "ダイレクトメッセージを操作する"
"read:mutes": "ミュートを見る" "read:mutes": "ミュートを見る"
"write:mutes": "ミュートを操作する" "write:mutes": "ミュートを操作する"
"write:notes": "ノートを作成・削除する" "write:notes": "ノートを作成・削除する"
@ -2443,8 +2467,8 @@ _permissions:
"read:clip-favorite": "クリップのいいねを見る" "read:clip-favorite": "クリップのいいねを見る"
"read:federation": "連合に関する情報を取得する" "read:federation": "連合に関する情報を取得する"
"write:report-abuse": "違反を報告する" "write:report-abuse": "違反を報告する"
"write:chat": "チャットを操作する" "write:chat": "ダイレクトメッセージを操作する"
"read:chat": "チャットを閲覧する" "read:chat": "ダイレクトメッセージを閲覧する"
_auth: _auth:
shareAccessTitle: "アプリへのアクセス許可" shareAccessTitle: "アプリへのアクセス許可"
@ -2507,7 +2531,7 @@ _widgets:
chooseList: "リストを選択" chooseList: "リストを選択"
clicker: "クリッカー" clicker: "クリッカー"
birthdayFollowings: "今日誕生日のユーザー" birthdayFollowings: "今日誕生日のユーザー"
chat: "チャット" chat: "ダイレクトメッセージ"
_cw: _cw:
hide: "隠す" hide: "隠す"
@ -2711,10 +2735,12 @@ _notification:
youReceivedFollowRequest: "フォローリクエストが来ました" youReceivedFollowRequest: "フォローリクエストが来ました"
yourFollowRequestAccepted: "フォローリクエストが承認されました" yourFollowRequestAccepted: "フォローリクエストが承認されました"
pollEnded: "アンケートの結果が出ました" pollEnded: "アンケートの結果が出ました"
scheduledNotePosted: "予約ノートが投稿されました"
scheduledNotePostFailed: "予約ノートの投稿に失敗しました"
newNote: "新しい投稿" newNote: "新しい投稿"
unreadAntennaNote: "アンテナ {name}" unreadAntennaNote: "アンテナ {name}"
roleAssigned: "ロールが付与されました" roleAssigned: "ロールが付与されました"
chatRoomInvitationReceived: "チャットルームへ招待されました" chatRoomInvitationReceived: "ダイレクトメッセージのグループへ招待されました"
emptyPushNotificationMessage: "プッシュ通知の更新をしました" emptyPushNotificationMessage: "プッシュ通知の更新をしました"
achievementEarned: "実績を獲得" achievementEarned: "実績を獲得"
testNotification: "通知テスト" testNotification: "通知テスト"
@ -2744,7 +2770,7 @@ _notification:
receiveFollowRequest: "フォロー申請を受け取った" receiveFollowRequest: "フォロー申請を受け取った"
followRequestAccepted: "フォローが受理された" followRequestAccepted: "フォローが受理された"
roleAssigned: "ロールが付与された" roleAssigned: "ロールが付与された"
chatRoomInvitationReceived: "チャットルームへ招待された" chatRoomInvitationReceived: "ダイレクトメッセージのグループへ招待された"
achievementEarned: "実績の獲得" achievementEarned: "実績の獲得"
exportCompleted: "エクスポートが完了した" exportCompleted: "エクスポートが完了した"
login: "ログイン" login: "ログイン"
@ -2794,7 +2820,7 @@ _deck:
mentions: "メンション" mentions: "メンション"
direct: "指名" direct: "指名"
roleTimeline: "ロールタイムライン" roleTimeline: "ロールタイムライン"
chat: "チャット" chat: "ダイレクトメッセージ"
_dialog: _dialog:
charactersExceeded: "最大文字数を超えています! 現在 {current} / 制限 {max}" charactersExceeded: "最大文字数を超えています! 現在 {current} / 制限 {max}"
@ -2897,7 +2923,7 @@ _moderationLogTypes:
deletePage: "ページを削除" deletePage: "ページを削除"
deleteFlash: "Playを削除" deleteFlash: "Playを削除"
deleteGalleryPost: "ギャラリーの投稿を削除" deleteGalleryPost: "ギャラリーの投稿を削除"
deleteChatRoom: "チャットルームを削除" deleteChatRoom: "ダイレクトメッセージのグループを削除"
updateProxyAccountDescription: "プロキシアカウントの説明を更新" updateProxyAccountDescription: "プロキシアカウントの説明を更新"
_fileViewer: _fileViewer:
@ -3271,7 +3297,9 @@ _watermarkEditor:
opacity: "不透明度" opacity: "不透明度"
scale: "サイズ" scale: "サイズ"
text: "テキスト" text: "テキスト"
qr: "二次元コード"
position: "位置" position: "位置"
margin: "マージン"
type: "タイプ" type: "タイプ"
image: "画像" image: "画像"
advanced: "高度" advanced: "高度"
@ -3286,6 +3314,7 @@ _watermarkEditor:
polkadotSubDotOpacity: "サブドットの不透明度" polkadotSubDotOpacity: "サブドットの不透明度"
polkadotSubDotRadius: "サブドットの大きさ" polkadotSubDotRadius: "サブドットの大きさ"
polkadotSubDotDivisions: "サブドットの数" polkadotSubDotDivisions: "サブドットの数"
leaveBlankToAccountUrl: "空欄にするとアカウントのURLになります"
_imageEffector: _imageEffector:
title: "エフェクト" title: "エフェクト"
@ -3299,6 +3328,8 @@ _imageEffector:
mirror: "ミラー" mirror: "ミラー"
invert: "色の反転" invert: "色の反転"
grayscale: "白黒" grayscale: "白黒"
blur: "ぼかし"
pixelate: "モザイク"
colorAdjust: "色調補正" colorAdjust: "色調補正"
colorClamp: "色の圧縮" colorClamp: "色の圧縮"
colorClampAdvanced: "色の圧縮(高度)" colorClampAdvanced: "色の圧縮(高度)"
@ -3310,11 +3341,15 @@ _imageEffector:
checker: "チェッカー" checker: "チェッカー"
blockNoise: "ブロックノイズ" blockNoise: "ブロックノイズ"
tearing: "ティアリング" tearing: "ティアリング"
fill: "塗りつぶし"
_fxProps: _fxProps:
angle: "角度" angle: "角度"
scale: "サイズ" scale: "サイズ"
size: "サイズ" size: "サイズ"
radius: "半径"
samples: "サンプル数"
offset: "位置"
color: "色" color: "色"
opacity: "不透明度" opacity: "不透明度"
normalize: "正規化" normalize: "正規化"
@ -3343,6 +3378,7 @@ _imageEffector:
zoomLinesThreshold: "集中線の幅" zoomLinesThreshold: "集中線の幅"
zoomLinesMaskSize: "中心径" zoomLinesMaskSize: "中心径"
zoomLinesBlack: "黒色にする" zoomLinesBlack: "黒色にする"
circle: "円形"
drafts: "下書き" drafts: "下書き"
_drafts: _drafts:
@ -3359,3 +3395,23 @@ _drafts:
restoreFromDraft: "下書きから復元" restoreFromDraft: "下書きから復元"
restore: "復元" restore: "復元"
listDrafts: "下書き一覧" 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"

View File

@ -1410,7 +1410,7 @@ _accountSettings:
makeNotesFollowersOnlyBefore: "昔のノートをフォロワーだけに見てもらう" makeNotesFollowersOnlyBefore: "昔のノートをフォロワーだけに見てもらう"
makeNotesFollowersOnlyBeforeDescription: "この機能が有効になってる間は、設定された日時より前、それか設定された時間が経ったノートがフォロワーのみ見れるようになるで。無効に戻すと、ノートの公開状態も戻るで。" makeNotesFollowersOnlyBeforeDescription: "この機能が有効になってる間は、設定された日時より前、それか設定された時間が経ったノートがフォロワーのみ見れるようになるで。無効に戻すと、ノートの公開状態も戻るで。"
makeNotesHiddenBefore: "昔のノートを見れんようにする" makeNotesHiddenBefore: "昔のノートを見れんようにする"
makeNotesHiddenBeforeDescription: "この機能が有効になってる間は、設定された日時より前、それか設定された時間が経ったノートがフォロワーのみ見れるようになるで。無効に戻すと、ノートの公開状態も戻るで。" makeNotesHiddenBeforeDescription: "この機能が有効になってる間は、設定された日時より前、それか設定された時間が経ったノートがあんただけ見れるようになるで。無効に戻すと、ノートの公開状態も戻るで。"
mayNotEffectForFederatedNotes: "リモートサーバーに連合されたノートには効果が及ばんかもしれん。" mayNotEffectForFederatedNotes: "リモートサーバーに連合されたノートには効果が及ばんかもしれん。"
mayNotEffectSomeSituations: "これらの制限は簡易的なものやで。リモートサーバーでの閲覧とかモデレーション時とか、一部のシチュエーションでは適用されへんかもしれん。" mayNotEffectSomeSituations: "これらの制限は簡易的なものやで。リモートサーバーでの閲覧とかモデレーション時とか、一部のシチュエーションでは適用されへんかもしれん。"
notesHavePassedSpecifiedPeriod: "決めた時間が経ったノート" notesHavePassedSpecifiedPeriod: "決めた時間が経ったノート"

View File

@ -253,6 +253,7 @@ noteDeleteConfirm: "이 노트를 삭제하시겠습니까?"
pinLimitExceeded: "더 이상 고정할 수 없습니다." pinLimitExceeded: "더 이상 고정할 수 없습니다."
done: "완료" done: "완료"
processing: "처리중" processing: "처리중"
preprocessing: "준비중"
preview: "미리보기" preview: "미리보기"
default: "기본값" default: "기본값"
defaultValueIs: "기본값: {value}" defaultValueIs: "기본값: {value}"
@ -1316,6 +1317,7 @@ acknowledgeNotesAndEnable: "활성화 하기 전에 주의 사항을 확인했
federationSpecified: "이 서버는 화이트 리스트 제도로 운영 중 입니다. 정해진 리모트 서버가 아닌 경우 연합되지 않습니다." federationSpecified: "이 서버는 화이트 리스트 제도로 운영 중 입니다. 정해진 리모트 서버가 아닌 경우 연합되지 않습니다."
federationDisabled: "이 서버는 연합을 하지 않고 있습니다. 리모트 서버 유저와 통신을 할 수 없습니다." federationDisabled: "이 서버는 연합을 하지 않고 있습니다. 리모트 서버 유저와 통신을 할 수 없습니다."
draft: "초안" draft: "초안"
draftsAndScheduledNotes: "초안과 예약 게시물"
confirmOnReact: "리액션할 때 확인" confirmOnReact: "리액션할 때 확인"
reactAreYouSure: "\" {emoji} \"로 리액션하시겠습니까?" reactAreYouSure: "\" {emoji} \"로 리액션하시겠습니까?"
markAsSensitiveConfirm: "이 미디어를 민감한 미디어로 설정하시겠습니까?" markAsSensitiveConfirm: "이 미디어를 민감한 미디어로 설정하시겠습니까?"
@ -1372,6 +1374,8 @@ redisplayAllTips: "모든 '팁과 유용한 정보'를 재표시"
hideAllTips: "모든 '팁과 유용한 정보'를 비표시" hideAllTips: "모든 '팁과 유용한 정보'를 비표시"
defaultImageCompressionLevel: "기본 이미지 압축 정도" defaultImageCompressionLevel: "기본 이미지 압축 정도"
defaultImageCompressionLevel_description: "낮추면 화질을 유지합니다만 파일 크기는 증가합니다. <br>높이면 파일 크기를 줄일 수 있습니다만 화질은 저하됩니다." defaultImageCompressionLevel_description: "낮추면 화질을 유지합니다만 파일 크기는 증가합니다. <br>높이면 파일 크기를 줄일 수 있습니다만 화질은 저하됩니다."
defaultCompressionLevel: "기본 압축 정도 "
defaultCompressionLevel_description: "낮추면 품질을 유지합니다만 파일 크기는 증가합니다. <br>높이면 파일 크기를 줄일 수 있습니다만 품질은 저하됩니다."
inMinutes: "분" inMinutes: "분"
inDays: "일" inDays: "일"
safeModeEnabled: "세이프 모드가 활성화돼있습니다" safeModeEnabled: "세이프 모드가 활성화돼있습니다"
@ -1380,6 +1384,20 @@ customCssIsDisabledBecauseSafeMode: "세이프 모드가 활성화돼있기에
themeIsDefaultBecauseSafeMode: "세이프 모드가 활성화돼있는 동안에는 기본 테마가 사용됩니다. 세이프 모드를 끄면 원래대로 돌아옵니다." themeIsDefaultBecauseSafeMode: "세이프 모드가 활성화돼있는 동안에는 기본 테마가 사용됩니다. 세이프 모드를 끄면 원래대로 돌아옵니다."
thankYouForTestingBeta: "베타 버전의 검증에 협력해 주셔서 감사합니다!" thankYouForTestingBeta: "베타 버전의 검증에 협력해 주셔서 감사합니다!"
createUserSpecifiedNote: "사용자 지정 노트를 작성" createUserSpecifiedNote: "사용자 지정 노트를 작성"
schedulePost: "게시 예약"
scheduleToPostOnX: "{x}에 게시를 예약합니다."
scheduledToPostOnX: "{x}에 게시가 예약돼있습니다."
schedule: "예약"
scheduled: "예약"
_compression:
_quality:
high: "고품질"
medium: "중간 품질"
low: "저품질"
_size:
large: "대형"
medium: "중형"
small: "소형"
_order: _order:
newest: "최신 순" newest: "최신 순"
oldest: "오래된 순" oldest: "오래된 순"
@ -2022,6 +2040,7 @@ _role:
uploadableFileTypes_caption: "MIME 유형을 " uploadableFileTypes_caption: "MIME 유형을 "
uploadableFileTypes_caption2: "파일에 따라서는 유형을 검사하지 못하는 경우가 있습니다. 그러한 파일을 허가하는 경우에는 {x}를 지정으로 추가해주십시오." uploadableFileTypes_caption2: "파일에 따라서는 유형을 검사하지 못하는 경우가 있습니다. 그러한 파일을 허가하는 경우에는 {x}를 지정으로 추가해주십시오."
noteDraftLimit: "서버측 노트 초안 작성 가능 수" noteDraftLimit: "서버측 노트 초안 작성 가능 수"
scheduledNoteLimit: "예약 게시물의 동시 작성 가능 수"
watermarkAvailable: "워터마크 기능의 사용 여부" watermarkAvailable: "워터마크 기능의 사용 여부"
_condition: _condition:
roleAssignedTo: "수동 역할에 이미 할당됨" roleAssignedTo: "수동 역할에 이미 할당됨"
@ -2647,6 +2666,8 @@ _notification:
youReceivedFollowRequest: "새로운 팔로우 요청이 있습니다" youReceivedFollowRequest: "새로운 팔로우 요청이 있습니다"
yourFollowRequestAccepted: "팔로우 요청이 수락되었습니다" yourFollowRequestAccepted: "팔로우 요청이 수락되었습니다"
pollEnded: "투표 결과가 발표되었습니다" pollEnded: "투표 결과가 발표되었습니다"
scheduledNotePosted: "예약 노트가 게시됐습니다."
scheduledNotePostFailed: "예약 노트의 게시에 실패했습니다."
newNote: "새 게시물" newNote: "새 게시물"
unreadAntennaNote: "안테나 {name}" unreadAntennaNote: "안테나 {name}"
roleAssigned: "역할이 부여 되었습니다." roleAssigned: "역할이 부여 되었습니다."
@ -3201,6 +3222,8 @@ _imageEffector:
mirror: "미러" mirror: "미러"
invert: "색 반전" invert: "색 반전"
grayscale: "흑백" grayscale: "흑백"
blur: "흐림 효과"
pixelate: "모자이크"
colorAdjust: "색조 보정" colorAdjust: "색조 보정"
colorClamp: "색 압축" colorClamp: "색 압축"
colorClampAdvanced: "색 압축(고급)" colorClampAdvanced: "색 압축(고급)"
@ -3212,11 +3235,13 @@ _imageEffector:
checker: "체크 무늬" checker: "체크 무늬"
blockNoise: "노이즈 방지" blockNoise: "노이즈 방지"
tearing: "티어링" tearing: "티어링"
fillSquare: "칠하기(사각형)" fill: "채우기"
_fxProps: _fxProps:
angle: "각도" angle: "각도"
scale: "크기" scale: "크기"
size: "크기" size: "크기"
radius: "반지름"
samples: "샘플 수"
offset: "위치" offset: "위치"
color: "색" color: "색"
opacity: "불투명도" opacity: "불투명도"
@ -3246,6 +3271,7 @@ _imageEffector:
zoomLinesThreshold: "집중선 폭" zoomLinesThreshold: "집중선 폭"
zoomLinesMaskSize: "중앙 값" zoomLinesMaskSize: "중앙 값"
zoomLinesBlack: "검은색으로 하기" zoomLinesBlack: "검은색으로 하기"
circle: "원형"
drafts: "초안" drafts: "초안"
_drafts: _drafts:
select: "초안 선택" select: "초안 선택"
@ -3261,6 +3287,9 @@ _drafts:
restoreFromDraft: "초안에서 복원\n" restoreFromDraft: "초안에서 복원\n"
restore: "복원" restore: "복원"
listDrafts: "초안 목록" listDrafts: "초안 목록"
schedule: "게시 예약"
listScheduledNotes: "예약 게시물 목록"
cancelSchedule: "예약 해제"
qr: "QR 코드" qr: "QR 코드"
_qr: _qr:
showTabTitle: "보기" showTabTitle: "보기"

View File

@ -253,6 +253,7 @@ noteDeleteConfirm: "Deseja excluir esta nota?"
pinLimitExceeded: "Não é possível fixar novas notas" pinLimitExceeded: "Não é possível fixar novas notas"
done: "Concluído" done: "Concluído"
processing: "Em Progresso" processing: "Em Progresso"
preprocessing: "Preparando..."
preview: "Pré-visualizar" preview: "Pré-visualizar"
default: "Predefinição" default: "Predefinição"
defaultValueIs: "Predefinição: {value}" 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." permissionDeniedErrorDescription: "Esta conta não tem permissão para executar esta ação."
preset: "Predefinições" preset: "Predefinições"
selectFromPresets: "Escolher de predefinições" selectFromPresets: "Escolher de predefinições"
custom: "Personalizado"
achievements: "Conquistas" achievements: "Conquistas"
gotInvalidResponseError: "Resposta do servidor inválida" gotInvalidResponseError: "Resposta do servidor inválida"
gotInvalidResponseErrorDescription: "Servidor fora do ar ou em manutenção. Favor tentar mais tarde." 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" 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." 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." notesSearchNotAvailable: "A pesquisa de notas está indisponível."
usersSearchNotAvailable: "Pesquisa de usuário está indisponível."
license: "Licença" license: "Licença"
unfavoriteConfirm: "Deseja realmente remover dos favoritos?" unfavoriteConfirm: "Deseja realmente remover dos favoritos?"
myClips: "Meus clipes" myClips: "Meus clipes"
@ -1243,6 +1246,7 @@ releaseToRefresh: "Solte para atualizar"
refreshing: "Atualizando..." refreshing: "Atualizando..."
pullDownToRefresh: "Puxe para baixo para atualizar" pullDownToRefresh: "Puxe para baixo para atualizar"
useGroupedNotifications: "Agrupar notificações" 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." cwNotationRequired: "Se \"Esconder conteúdo\" está habilitado, uma descrição deve ser adicionada."
doReaction: "Adicionar reação" doReaction: "Adicionar reação"
code: "Código" 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." 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." federationDisabled: "Federação está desabilitada nesse servidor. Você não pode interagir com usuários de outros servidores."
draft: "Rascunhos" draft: "Rascunhos"
draftsAndScheduledNotes: "Rascunhos e notas agendadas."
confirmOnReact: "Confirmar ao reagir" confirmOnReact: "Confirmar ao reagir"
reactAreYouSure: "Você deseja adicionar uma reação \"{emoji}\"?" reactAreYouSure: "Você deseja adicionar uma reação \"{emoji}\"?"
markAsSensitiveConfirm: "Você deseja definir essa mídia como sensível?" markAsSensitiveConfirm: "Você deseja definir essa mídia como sensível?"
@ -1341,6 +1346,7 @@ textCount: "Contagem de caracteres"
information: "Sobre" information: "Sobre"
chat: "Conversas" chat: "Conversas"
directMessage: "Conversar com usuário" directMessage: "Conversar com usuário"
directMessage_short: "Mensagem"
migrateOldSettings: "Migrar configurações antigas de cliente" 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." 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" compress: "Comprimir"
@ -1368,12 +1374,35 @@ redisplayAllTips: "Mostrar todas as \"Dicas e Truques\" novamente"
hideAllTips: "Ocultas todas as \"Dicas e Truques\"" hideAllTips: "Ocultas todas as \"Dicas e Truques\""
defaultImageCompressionLevel: "Nível de compressão de imagem padrão" defaultImageCompressionLevel: "Nível de compressão de imagem padrão"
defaultImageCompressionLevel_description: "Alto, reduz o tamanho do arquivo mas, também, a qualidade da imagem.<br>Alto, reduz o tamanho do arquivo mas, também, a qualidade da imagem." defaultImageCompressionLevel_description: "Alto, reduz o tamanho do arquivo mas, também, a qualidade da imagem.<br>Alto, reduz o tamanho do arquivo mas, também, a qualidade da imagem."
defaultCompressionLevel: "Nível padrão de compressão"
defaultCompressionLevel_description: "Menor compressão preserva a qualidade mas aumenta o tamanho do arquivo.<br>Maior compressão reduz o tamanho do arquivo mas diminui a qualidade."
inMinutes: "Minuto(s)" inMinutes: "Minuto(s)"
inDays: "Dia(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: _order:
newest: "Priorizar Mais Novos" newest: "Priorizar Mais Novos"
oldest: "Priorizar Mais Antigos" oldest: "Priorizar Mais Antigos"
_chat: _chat:
messages: "Mensagem"
noMessagesYet: "Ainda não há mensagens" noMessagesYet: "Ainda não há mensagens"
newMessage: "Nova mensagem" newMessage: "Nova mensagem"
individualChat: "Conversa Particular" individualChat: "Conversa Particular"
@ -1461,6 +1490,7 @@ _settings:
contentsUpdateFrequency_description2: "Quando o modo tempo-real está ativado, o conteúdo é atualizado em tempo real, ignorando essa opção." 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" showUrlPreview: "Exibir prévia de URL"
showAvailableReactionsFirstInNote: "Exibir reações disponíveis no topo." showAvailableReactionsFirstInNote: "Exibir reações disponíveis no topo."
showPageTabBarBottom: "Mostrar barra de aba da página inferiormente"
_chat: _chat:
showSenderName: "Exibir nome de usuário do remetente" showSenderName: "Exibir nome de usuário do remetente"
sendOnEnter: "Pressionar Enter para enviar" sendOnEnter: "Pressionar Enter para enviar"
@ -1634,6 +1664,10 @@ _serverSettings:
fanoutTimelineDbFallback: "\"Fallback\" ao banco de dados" 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." 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." 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" 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." 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" openRegistration: "Abrir a criação de contas"
@ -1652,6 +1686,11 @@ _serverSettings:
userGeneratedContentsVisibilityForVisitor: "Visibilidade de conteúdo dos usuários para visitantes" 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_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." 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: _userGeneratedContentsVisibilityForVisitor:
all: "Tudo é público" all: "Tudo é público"
localOnly: "Conteúdo local é publicado, conteúdo remoto é privado" localOnly: "Conteúdo local é publicado, conteúdo remoto é privado"
@ -1988,6 +2027,7 @@ _role:
descriptionOfRateLimitFactor: "Valores menores são menos restritivos, valores maiores são mais restritivos." descriptionOfRateLimitFactor: "Valores menores são menos restritivos, valores maiores são mais restritivos."
canHideAds: "Permitir ocultar anúncios" canHideAds: "Permitir ocultar anúncios"
canSearchNotes: "Permitir a busca de notas" canSearchNotes: "Permitir a busca de notas"
canSearchUsers: "Busca de usuário"
canUseTranslator: "Uso do tradutor" canUseTranslator: "Uso do tradutor"
avatarDecorationLimit: "Número máximo de decorações de avatar que podem ser aplicadas" avatarDecorationLimit: "Número máximo de decorações de avatar que podem ser aplicadas"
canImportAntennas: "Permitir importação de antenas" canImportAntennas: "Permitir importação de antenas"
@ -2000,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_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." uploadableFileTypes_caption2: "Alguns tipos de arquivos podem não ser detectados. Para permiti-los, adicione {x} à especificação."
noteDraftLimit: "Limite de rascunhos possíveis" 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" watermarkAvailable: "Disponibilidade da função de marca d'água"
_condition: _condition:
roleAssignedTo: "Atribuído a cargos manuais" roleAssignedTo: "Atribuído a cargos manuais"
@ -2260,6 +2301,7 @@ _time:
minute: "Minuto(s)" minute: "Minuto(s)"
hour: "Hora(s)" hour: "Hora(s)"
day: "Dia(s)" day: "Dia(s)"
month: "Mês(es)"
_2fa: _2fa:
alreadyRegistered: "Você já cadastrou um dispositivo de autenticação de dois fatores." alreadyRegistered: "Você já cadastrou um dispositivo de autenticação de dois fatores."
registerTOTP: "Cadastrar aplicativo autenticador" registerTOTP: "Cadastrar aplicativo autenticador"
@ -2624,6 +2666,8 @@ _notification:
youReceivedFollowRequest: "Você recebeu um pedido de seguidor" youReceivedFollowRequest: "Você recebeu um pedido de seguidor"
yourFollowRequestAccepted: "Seu pedido de seguidor foi aceito" yourFollowRequestAccepted: "Seu pedido de seguidor foi aceito"
pollEnded: "Os resultados da enquete agora estão disponíveis" 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" newNote: "Nova nota"
unreadAntennaNote: "Antena {name}" unreadAntennaNote: "Antena {name}"
roleAssigned: "Cargo dado" roleAssigned: "Cargo dado"
@ -3062,6 +3106,7 @@ _bootErrors:
otherOption1: "Excluir ajustes de cliente e cache" otherOption1: "Excluir ajustes de cliente e cache"
otherOption2: "Iniciar o cliente simples" otherOption2: "Iniciar o cliente simples"
otherOption3: "Iniciar ferramenta de reparo" otherOption3: "Iniciar ferramenta de reparo"
otherOption4: "Abrir Misskey no modo seguro"
_search: _search:
searchScopeAll: "Todos" searchScopeAll: "Todos"
searchScopeLocal: "Local" searchScopeLocal: "Local"
@ -3098,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_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\"" 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" 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: "Informações da administração"
adminInfo_description: "Define as informações do administrador usadas para receber consultas." 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." adminInfo_mustBeFilled: "Deve ser preenchido se o servidor é público ou se a federação está ativa."
@ -3146,7 +3193,9 @@ _watermarkEditor:
opacity: "Opacidade" opacity: "Opacidade"
scale: "Tamanho" scale: "Tamanho"
text: "Texto" text: "Texto"
qr: "Código QR"
position: "Posição" position: "Posição"
margin: "Margem"
type: "Tipo" type: "Tipo"
image: "imagem" image: "imagem"
advanced: "Avançado" advanced: "Avançado"
@ -3161,16 +3210,20 @@ _watermarkEditor:
polkadotSubDotOpacity: "Opacidade da bolinha secundária" polkadotSubDotOpacity: "Opacidade da bolinha secundária"
polkadotSubDotRadius: "Raio das bolinhas adicionais" polkadotSubDotRadius: "Raio das bolinhas adicionais"
polkadotSubDotDivisions: "Número de bolinhas adicionais" polkadotSubDotDivisions: "Número de bolinhas adicionais"
leaveBlankToAccountUrl: "Deixe em branco para utilizar URL da conta"
_imageEffector: _imageEffector:
title: "Efeitos" title: "Efeitos"
addEffect: "Adicionar efeitos" addEffect: "Adicionar efeitos"
discardChangesConfirm: "Tem certeza que deseja sair? Há mudanças não salvas." discardChangesConfirm: "Tem certeza que deseja sair? Há mudanças não salvas."
nothingToConfigure: "Não há nada para configurar"
_fxs: _fxs:
chromaticAberration: "Aberração cromática" chromaticAberration: "Aberração cromática"
glitch: "Glitch" glitch: "Glitch"
mirror: "Espelho" mirror: "Espelho"
invert: "Inverter Cores" invert: "Inverter Cores"
grayscale: "Tons de Cinza" grayscale: "Tons de Cinza"
blur: "Desfoque"
pixelate: "Pixelizar"
colorAdjust: "Correção de Cores" colorAdjust: "Correção de Cores"
colorClamp: "Compressão de Cores" colorClamp: "Compressão de Cores"
colorClampAdvanced: "Compressão Avançada de Cores" colorClampAdvanced: "Compressão Avançada de Cores"
@ -3182,14 +3235,43 @@ _imageEffector:
checker: "Xadrez" checker: "Xadrez"
blockNoise: "Bloquear Ruído" blockNoise: "Bloquear Ruído"
tearing: "Descontinuidade" tearing: "Descontinuidade"
fill: "Preencher"
_fxProps: _fxProps:
angle: "Ângulo" angle: "Ângulo"
scale: "Tamanho" scale: "Tamanho"
size: "Tamanho" size: "Tamanho"
radius: "Raio"
samples: "Número de amostras"
offset: "Posição" offset: "Posição"
color: "Cor" color: "Cor"
opacity: "Opacidade" opacity: "Opacidade"
normalize: "Normalizar"
amount: "Quantidade"
lightness: "Esclarecer" 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: "Rascunhos"
_drafts: _drafts:
select: "Selecionar Rascunho" select: "Selecionar Rascunho"
@ -3205,6 +3287,22 @@ _drafts:
restoreFromDraft: "Restaurar de Rascunho" restoreFromDraft: "Restaurar de Rascunho"
restore: "Redefinir" restore: "Redefinir"
listDrafts: "Lista de Rascunhos" listDrafts: "Lista de Rascunhos"
schedule: "Agendar nota"
listScheduledNotes: "Lista de notas agendadas"
cancelSchedule: "Cancelar agendamento"
qr: "Código QR"
_qr: _qr:
showTabTitle: "Visualizar" 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" raw: "Texto"
mfm: "MFM"

View File

@ -253,6 +253,7 @@ noteDeleteConfirm: "ต้องการลบโน้ตนี้ใช่ไ
pinLimitExceeded: "คุณไม่สามารถปักหมุดโน้ตเพิ่มเติมใดๆได้อีก" pinLimitExceeded: "คุณไม่สามารถปักหมุดโน้ตเพิ่มเติมใดๆได้อีก"
done: "เสร็จสิ้น" done: "เสร็จสิ้น"
processing: "กำลังประมวลผล..." processing: "กำลังประมวลผล..."
preprocessing: "กำลังจัดเตรียม..."
preview: "แสดงตัวอย่าง" preview: "แสดงตัวอย่าง"
default: "ค่าเริ่มต้น" default: "ค่าเริ่มต้น"
defaultValueIs: "ค่าเริ่มต้น: {value}" defaultValueIs: "ค่าเริ่มต้น: {value}"
@ -1245,6 +1246,7 @@ releaseToRefresh: "ปล่อยเพื่อรีเฟรช"
refreshing: "กำลังรีเฟรช..." refreshing: "กำลังรีเฟรช..."
pullDownToRefresh: "ดึงลงเพื่อรีเฟรช" pullDownToRefresh: "ดึงลงเพื่อรีเฟรช"
useGroupedNotifications: "แสดงผลการแจ้งเตือนแบบกลุ่มแล้ว" useGroupedNotifications: "แสดงผลการแจ้งเตือนแบบกลุ่มแล้ว"
emailVerificationFailedError: "เกิดปัญหาในขณะตรวจสอบอีเมล อาจเป็นไปได้ว่าลิงก์หมดอายุแล้ว"
cwNotationRequired: "หากเปิดใช้งาน “ซ่อนเนื้อหา” จะต้องระบุคำอธิบาย" cwNotationRequired: "หากเปิดใช้งาน “ซ่อนเนื้อหา” จะต้องระบุคำอธิบาย"
doReaction: "เพิ่มรีแอคชั่น" doReaction: "เพิ่มรีแอคชั่น"
code: "โค้ด" code: "โค้ด"
@ -1315,6 +1317,7 @@ acknowledgeNotesAndEnable: "เปิดใช้งานหลังจาก
federationSpecified: "เซิร์ฟเวอร์นี้ดำเนินงานในระบบกลุ่มไวท์ลิสต์ ไม่สามารถติดต่อกับเซิร์ฟเวอร์อื่นที่ไม่ได้รับอนุญาตจากผู้ดูแลระบบได้" federationSpecified: "เซิร์ฟเวอร์นี้ดำเนินงานในระบบกลุ่มไวท์ลิสต์ ไม่สามารถติดต่อกับเซิร์ฟเวอร์อื่นที่ไม่ได้รับอนุญาตจากผู้ดูแลระบบได้"
federationDisabled: "เซิร์ฟเวอร์นี้ปิดใช้งานสหพันธ์ ไม่สามารถติดต่อหรือแลกเปลี่ยนข้อมูลกับผู้ใช้จากเซิร์ฟเวอร์อื่นได้" federationDisabled: "เซิร์ฟเวอร์นี้ปิดใช้งานสหพันธ์ ไม่สามารถติดต่อหรือแลกเปลี่ยนข้อมูลกับผู้ใช้จากเซิร์ฟเวอร์อื่นได้"
draft: "ร่าง" draft: "ร่าง"
draftsAndScheduledNotes: "ร่างและกำหนดเวลาโพสต์"
confirmOnReact: "ยืนยันเมื่อทำการรีแอคชั่น" confirmOnReact: "ยืนยันเมื่อทำการรีแอคชั่น"
reactAreYouSure: "ต้องการใส่รีแอคชั่นด้วย \"{emoji}\" หรือไม่?" reactAreYouSure: "ต้องการใส่รีแอคชั่นด้วย \"{emoji}\" หรือไม่?"
markAsSensitiveConfirm: "ต้องการตั้งค่าสื่อนี้ว่าเป็นเนื้อหาละเอียดอ่อนหรือไม่?" markAsSensitiveConfirm: "ต้องการตั้งค่าสื่อนี้ว่าเป็นเนื้อหาละเอียดอ่อนหรือไม่?"
@ -1343,6 +1346,7 @@ textCount: "จำนวนอักขระ"
information: "เกี่ยวกับ" information: "เกี่ยวกับ"
chat: "แชต" chat: "แชต"
directMessage: "แชตเลย" directMessage: "แชตเลย"
directMessage_short: "ข้อความ"
migrateOldSettings: "ย้ายข้อมูลการตั้งค่าเก่า" migrateOldSettings: "ย้ายข้อมูลการตั้งค่าเก่า"
migrateOldSettings_description: "โดยปกติจะทำโดยอัตโนมัติ แต่หากด้วยเหตุผลบางประการที่ไม่สามารถย้ายได้สำเร็จ สามารถสั่งย้ายด้วยตนเองได้ การตั้งค่าปัจจุบันจะถูกเขียนทับ" migrateOldSettings_description: "โดยปกติจะทำโดยอัตโนมัติ แต่หากด้วยเหตุผลบางประการที่ไม่สามารถย้ายได้สำเร็จ สามารถสั่งย้ายด้วยตนเองได้ การตั้งค่าปัจจุบันจะถูกเขียนทับ"
compress: "บีบอัด" compress: "บีบอัด"
@ -1368,8 +1372,10 @@ abort: "หยุดและยกเลิก"
tip: "คำแนะนำและเคล็ดลับ" tip: "คำแนะนำและเคล็ดลับ"
redisplayAllTips: "แสดงคำแนะนำและเคล็ดลับทั้งหมดอีกครั้ง" redisplayAllTips: "แสดงคำแนะนำและเคล็ดลับทั้งหมดอีกครั้ง"
hideAllTips: "ซ่อนคำแนะนำและเคล็ดลับทั้งหมด" hideAllTips: "ซ่อนคำแนะนำและเคล็ดลับทั้งหมด"
defaultImageCompressionLevel: "ความละเอียดเริ่มต้นสำหรับการบีบอัดภาพ" defaultImageCompressionLevel: "ค่าการบีบอัดภาพเริ่มต้น"
defaultImageCompressionLevel_description: "หากตั้งค่าต่ำ จะรักษาคุณภาพภาพได้ดีขึ้นแต่ขนาดไฟล์จะเพิ่มขึ้น<br>หากตั้งค่าสูง จะลดขนาดไฟล์ได้ แต่คุณภาพภาพจะลดลง" defaultImageCompressionLevel_description: "หากตั้งค่าต่ำ จะรักษาคุณภาพภาพได้ดีขึ้นแต่ขนาดไฟล์จะเพิ่มขึ้น<br>หากตั้งค่าสูง จะลดขนาดไฟล์ได้ แต่คุณภาพภาพจะลดลง"
defaultCompressionLevel: "ค่าการบีบอัดเริ่มต้น"
defaultCompressionLevel_description: "ถ้าต่ำ จะรักษาคุณภาพได้ แต่ขนาดไฟล์จะเพิ่มขึ้น<br>ถ้าสูง จะลดขนาดไฟล์ได้ แต่คุณภาพจะลดลง"
inMinutes: "นาที" inMinutes: "นาที"
inDays: "วัน" inDays: "วัน"
safeModeEnabled: "โหมดปลอดภัยถูกเปิดใช้งาน" safeModeEnabled: "โหมดปลอดภัยถูกเปิดใช้งาน"
@ -1377,10 +1383,26 @@ pluginsAreDisabledBecauseSafeMode: "เนื่องจากโหมดป
customCssIsDisabledBecauseSafeMode: "เนื่องจากโหมดปลอดภัยถูกเปิดใช้งาน CSS แบบกำหนดเองจึงไม่ได้ถูกนำมาใช้" customCssIsDisabledBecauseSafeMode: "เนื่องจากโหมดปลอดภัยถูกเปิดใช้งาน CSS แบบกำหนดเองจึงไม่ได้ถูกนำมาใช้"
themeIsDefaultBecauseSafeMode: "ในระหว่างที่โหมดปลอดภัยถูกเปิดใช้งาน จะใช้ธีมเริ่มต้น เมื่อปิดโหมดปลอดภัยจะกลับคืนดังเดิม" themeIsDefaultBecauseSafeMode: "ในระหว่างที่โหมดปลอดภัยถูกเปิดใช้งาน จะใช้ธีมเริ่มต้น เมื่อปิดโหมดปลอดภัยจะกลับคืนดังเดิม"
thankYouForTestingBeta: "ขอบคุณที่ให้ความร่วมมือในการทดสอบเวอร์ชันเบต้า!" thankYouForTestingBeta: "ขอบคุณที่ให้ความร่วมมือในการทดสอบเวอร์ชันเบต้า!"
createUserSpecifiedNote: "สร้างโน้ตแบบไดเร็กต์"
schedulePost: "กำหนดเวลาให้โพสต์"
scheduleToPostOnX: "กำหนดเวลาให้โพสต์ไว้ที่ {x}"
scheduledToPostOnX: "มีการกำหนดเวลาให้โพสต์ไว้ที่ {x}"
schedule: "กำหนดเวลา"
scheduled: "กำหนดเวลา"
_compression:
_quality:
high: "คุณภาพสูง"
medium: "คุณภาพปานกลาง"
low: "คุณภาพต่ำ"
_size:
large: "ขนาดใหญ่"
medium: "ขนาดปานกลาง"
small: "ขนาดเล็ก"
_order: _order:
newest: "เรียงจากใหม่ไปเก่า" newest: "เรียงจากใหม่ไปเก่า"
oldest: "เรียงจากเก่าไปใหม่" oldest: "เรียงจากเก่าไปใหม่"
_chat: _chat:
messages: "ข้อความ"
noMessagesYet: "ยังไม่มีข้อความ" noMessagesYet: "ยังไม่มีข้อความ"
newMessage: "ข้อความใหม่" newMessage: "ข้อความใหม่"
individualChat: "แชตส่วนตัว" individualChat: "แชตส่วนตัว"
@ -1666,6 +1688,9 @@ _serverSettings:
userGeneratedContentsVisibilityForVisitor_description2: "การเปิดเผยเนื้อหาทั้งหมดในเซิร์ฟเวอร์รวมทั้งเนื้อหาที่รับมาจากระยะไกลสู่สาธารณะบนอินเทอร์เน็ตโดยไม่มีข้อจำกัดใดๆ มีความเสี่ยงโดยเฉพาะอย่างยิ่งสำหรับผู้ชมที่ไม่เข้าใจลักษณะของระบบแบบกระจาย อาจทำให้เกิดความเข้าใจผิดคิดว่าเนื้อหาที่มาจากระยะไกลนั้นเป็นเนื้อหาที่สร้างขึ้นภายในเซิร์ฟเวอร์นี้ จึงควรใช้ความระมัดระวังอย่างมาก" userGeneratedContentsVisibilityForVisitor_description2: "การเปิดเผยเนื้อหาทั้งหมดในเซิร์ฟเวอร์รวมทั้งเนื้อหาที่รับมาจากระยะไกลสู่สาธารณะบนอินเทอร์เน็ตโดยไม่มีข้อจำกัดใดๆ มีความเสี่ยงโดยเฉพาะอย่างยิ่งสำหรับผู้ชมที่ไม่เข้าใจลักษณะของระบบแบบกระจาย อาจทำให้เกิดความเข้าใจผิดคิดว่าเนื้อหาที่มาจากระยะไกลนั้นเป็นเนื้อหาที่สร้างขึ้นภายในเซิร์ฟเวอร์นี้ จึงควรใช้ความระมัดระวังอย่างมาก"
restartServerSetupWizardConfirm_title: "ต้องการเริ่มวิซาร์ดการตั้งค่าเซิร์ฟเวอร์ใหม่หรือไม่?" restartServerSetupWizardConfirm_title: "ต้องการเริ่มวิซาร์ดการตั้งค่าเซิร์ฟเวอร์ใหม่หรือไม่?"
restartServerSetupWizardConfirm_text: "การตั้งค่าบางส่วนในปัจจุบันจะถูกรีเซ็ต" restartServerSetupWizardConfirm_text: "การตั้งค่าบางส่วนในปัจจุบันจะถูกรีเซ็ต"
entrancePageStyle: "สไตล์ของหน้าเพจทางเข้า"
showTimelineForVisitor: "แสดงไทม์ไลน์"
showActivitiesForVisitor: "แสดงกิจกรรม"
_userGeneratedContentsVisibilityForVisitor: _userGeneratedContentsVisibilityForVisitor:
all: "ทั้งหมดสาธารณะ" all: "ทั้งหมดสาธารณะ"
localOnly: "เผยแพร่เป็นสาธารณะเฉพาะเนื้อหาท้องถิ่น เนื้อหาระยะไกลให้เป็นส่วนตัว" localOnly: "เผยแพร่เป็นสาธารณะเฉพาะเนื้อหาท้องถิ่น เนื้อหาระยะไกลให้เป็นส่วนตัว"
@ -2015,6 +2040,7 @@ _role:
uploadableFileTypes_caption: "สามารถระบุ MIME type ได้ โดยใช้การขึ้นบรรทัดใหม่เพื่อแยกหลายรายการ และสามารถใช้ดอกจัน (*) เพื่อระบุแบบไวลด์การ์ดได้ (เช่น: image/*)" uploadableFileTypes_caption: "สามารถระบุ MIME type ได้ โดยใช้การขึ้นบรรทัดใหม่เพื่อแยกหลายรายการ และสามารถใช้ดอกจัน (*) เพื่อระบุแบบไวลด์การ์ดได้ (เช่น: image/*)"
uploadableFileTypes_caption2: "ไฟล์บางประเภทอาจไม่สามารถระบุชนิดได้ หากต้องการอนุญาตไฟล์ลักษณะนั้น กรุณาเพิ่ม {x} ลงในรายการที่อนุญาต" uploadableFileTypes_caption2: "ไฟล์บางประเภทอาจไม่สามารถระบุชนิดได้ หากต้องการอนุญาตไฟล์ลักษณะนั้น กรุณาเพิ่ม {x} ลงในรายการที่อนุญาต"
noteDraftLimit: "จำนวนโน้ตฉบับร่างที่สามารถสร้างได้บนฝั่งเซิร์ฟเวอร์" noteDraftLimit: "จำนวนโน้ตฉบับร่างที่สามารถสร้างได้บนฝั่งเซิร์ฟเวอร์"
scheduledNoteLimit: "จำนวนโพสต์กำหนดเวลาที่สร้างพร้อมกันได้"
watermarkAvailable: "มีฟังก์ชั่นลายน้ำให้เลือกใช้" watermarkAvailable: "มีฟังก์ชั่นลายน้ำให้เลือกใช้"
_condition: _condition:
roleAssignedTo: "มอบหมายให้มีบทบาทแบบทำมือ" roleAssignedTo: "มอบหมายให้มีบทบาทแบบทำมือ"
@ -2640,6 +2666,8 @@ _notification:
youReceivedFollowRequest: "ได้รับคำขอติดตาม" youReceivedFollowRequest: "ได้รับคำขอติดตาม"
yourFollowRequestAccepted: "คำขอติดตามได้รับการอนุมัติแล้ว" yourFollowRequestAccepted: "คำขอติดตามได้รับการอนุมัติแล้ว"
pollEnded: "ผลโพลออกมาแล้ว" pollEnded: "ผลโพลออกมาแล้ว"
scheduledNotePosted: "โน้ตที่กำหนดเวลาไว้ได้ถูกโพสต์แล้ว"
scheduledNotePostFailed: "ล้มเหลวในการโพสต์โน้ตที่กำหนดเวลาไว้"
newNote: "โพสต์ใหม่" newNote: "โพสต์ใหม่"
unreadAntennaNote: "เสาอากาศ {name}" unreadAntennaNote: "เสาอากาศ {name}"
roleAssigned: "ได้รับบทบาท" roleAssigned: "ได้รับบทบาท"
@ -3165,7 +3193,9 @@ _watermarkEditor:
opacity: "ความทึบแสง" opacity: "ความทึบแสง"
scale: "ขนาด" scale: "ขนาด"
text: "ข้อความ" text: "ข้อความ"
qr: "QR โค้ด"
position: "ตำแหน่ง" position: "ตำแหน่ง"
margin: "ระยะขอบ"
type: "รูปแบบ" type: "รูปแบบ"
image: "รูปภาพ" image: "รูปภาพ"
advanced: "ขั้นสูง" advanced: "ขั้นสูง"
@ -3180,6 +3210,7 @@ _watermarkEditor:
polkadotSubDotOpacity: "ความทึบของจุดรอง" polkadotSubDotOpacity: "ความทึบของจุดรอง"
polkadotSubDotRadius: "ขนาดของจุดรอง" polkadotSubDotRadius: "ขนาดของจุดรอง"
polkadotSubDotDivisions: "จำนวนจุดรอง" polkadotSubDotDivisions: "จำนวนจุดรอง"
leaveBlankToAccountUrl: "เว้นว่างไว้หากต้องการใช้ URL ของบัญชีแทน"
_imageEffector: _imageEffector:
title: "เอฟเฟกต์" title: "เอฟเฟกต์"
addEffect: "เพิ่มเอฟเฟกต์" addEffect: "เพิ่มเอฟเฟกต์"
@ -3191,6 +3222,8 @@ _imageEffector:
mirror: "กระจก" mirror: "กระจก"
invert: "กลับสี" invert: "กลับสี"
grayscale: "ขาวดำเทา" grayscale: "ขาวดำเทา"
blur: "มัว"
pixelate: "โมเสก"
colorAdjust: "ปรับแก้สี" colorAdjust: "ปรับแก้สี"
colorClamp: "บีบอัดสี" colorClamp: "บีบอัดสี"
colorClampAdvanced: "บีบอัดสี (ขั้นสูง)" colorClampAdvanced: "บีบอัดสี (ขั้นสูง)"
@ -3202,10 +3235,13 @@ _imageEffector:
checker: "ช่องตาราง" checker: "ช่องตาราง"
blockNoise: "บล็อกที่มีการรบกวน" blockNoise: "บล็อกที่มีการรบกวน"
tearing: "ฉีกขาด" tearing: "ฉีกขาด"
fill: "เติมเต็ม"
_fxProps: _fxProps:
angle: "แองเกิล" angle: "แองเกิล"
scale: "ขนาด" scale: "ขนาด"
size: "ขนาด" size: "ขนาด"
radius: "รัศสี"
samples: "จำนวนตัวอย่าง"
offset: "ตำแหน่ง" offset: "ตำแหน่ง"
color: "สี" color: "สี"
opacity: "ความทึบแสง" opacity: "ความทึบแสง"
@ -3235,6 +3271,7 @@ _imageEffector:
zoomLinesThreshold: "ความกว้างเส้นรวมศูนย์" zoomLinesThreshold: "ความกว้างเส้นรวมศูนย์"
zoomLinesMaskSize: "ขนาดพื้นที่ตรงกลาง" zoomLinesMaskSize: "ขนาดพื้นที่ตรงกลาง"
zoomLinesBlack: "ทำให้ดำ" zoomLinesBlack: "ทำให้ดำ"
circle: "ทรงกลม"
drafts: "ร่าง" drafts: "ร่าง"
_drafts: _drafts:
select: "เลือกฉบับร่าง" select: "เลือกฉบับร่าง"
@ -3250,6 +3287,22 @@ _drafts:
restoreFromDraft: "คืนค่าจากฉบับร่าง" restoreFromDraft: "คืนค่าจากฉบับร่าง"
restore: "กู้คืน" restore: "กู้คืน"
listDrafts: "รายการฉบับร่าง" listDrafts: "รายการฉบับร่าง"
schedule: "โพสต์กำหนดเวลา"
listScheduledNotes: "รายการโน้ตที่กำหนดเวลาไว้"
cancelSchedule: "ยกเลิกกำหนดเวลา"
qr: "QR โค้ด"
_qr: _qr:
showTabTitle: "แสดงผล" showTabTitle: "แสดงผล"
readTabTitle: "แสกน"
shareTitle: "{name}{acct}"
shareText: "โปรดติดตามฉันบน Fediverse ด้วย!"
chooseCamera: "เลือกกล้อง"
cannotToggleFlash: "ไม่สามารถเลือกแสงแฟลชได้"
turnOnFlash: "ปิดแสงแฟลช"
turnOffFlash: "เปิดแสงแฟลช"
startQr: "เริ่มตัวอ่าน QR โค้ด"
stopQr: "หยุดตัวอ่าน QR โค้ด"
noQrCodeFound: "ไม่พบ QR โค้ด"
scanFile: "สแกนภาพจากอุปกรณ์"
raw: "ข้อความ" raw: "ข้อความ"
mfm: "MFM"

View File

@ -87,7 +87,7 @@ exportRequested: "导出请求已提交,这可能需要花一些时间,导
importRequested: "导入请求已提交,这可能需要花一点时间。" importRequested: "导入请求已提交,这可能需要花一点时间。"
lists: "列表" lists: "列表"
noLists: "列表为空" noLists: "列表为空"
note: "" note: "帖"
notes: "帖子" notes: "帖子"
following: "关注中" following: "关注中"
followers: "关注者" followers: "关注者"
@ -144,15 +144,15 @@ markAsSensitive: "标记为敏感内容"
unmarkAsSensitive: "取消标记为敏感内容" unmarkAsSensitive: "取消标记为敏感内容"
enterFileName: "输入文件名" enterFileName: "输入文件名"
mute: "屏蔽" mute: "屏蔽"
unmute: "取消隐藏" unmute: "取消屏蔽"
renoteMute: "隐藏转帖" renoteMute: "屏蔽转帖"
renoteUnmute: "解除隐藏转帖" renoteUnmute: "取消屏蔽转帖"
block: "屏蔽" block: "拉黑"
unblock: "取消屏蔽" unblock: "取消拉黑"
suspend: "冻结" suspend: "冻结"
unsuspend: "解除冻结" unsuspend: "解除冻结"
blockConfirm: "确定要屏蔽吗?" blockConfirm: "确定要拉黑吗?"
unblockConfirm: "确定要取消屏蔽吗?" unblockConfirm: "确定要取消拉黑吗?"
suspendConfirm: "要冻结吗?" suspendConfirm: "要冻结吗?"
unsuspendConfirm: "要解除冻结吗?" unsuspendConfirm: "要解除冻结吗?"
selectList: "选择列表" selectList: "选择列表"
@ -244,22 +244,23 @@ mediaSilencedInstances: "已隐藏媒体文件的服务器"
mediaSilencedInstancesDescription: "设置要隐藏媒体文件的服务器,以换行分隔。被设置的服务器内所有账号的文件均按照「敏感内容」处理,且将无法使用自定义表情符号。被阻止的实例不受影响。" mediaSilencedInstancesDescription: "设置要隐藏媒体文件的服务器,以换行分隔。被设置的服务器内所有账号的文件均按照「敏感内容」处理,且将无法使用自定义表情符号。被阻止的实例不受影响。"
federationAllowedHosts: "允许联合的服务器" federationAllowedHosts: "允许联合的服务器"
federationAllowedHostsDescription: "设定允许联合的服务器,以换行分隔。" federationAllowedHostsDescription: "设定允许联合的服务器,以换行分隔。"
muteAndBlock: "隐藏和屏蔽" muteAndBlock: "屏蔽/拉黑"
mutedUsers: "已隐藏用户" mutedUsers: "已屏蔽用户"
blockedUsers: "已屏蔽的用户" blockedUsers: "已拉黑的用户"
noUsers: "无用户" noUsers: "无用户"
editProfile: "编辑资料" editProfile: "编辑资料"
noteDeleteConfirm: "确定要删除该帖子吗?" noteDeleteConfirm: "确定要删除该帖子吗?"
pinLimitExceeded: "无法置顶更多了" pinLimitExceeded: "无法置顶更多了"
done: "完成" done: "完成"
processing: "正在处理" processing: "正在处理"
preprocessing: "准备中"
preview: "预览" preview: "预览"
default: "默认" default: "默认"
defaultValueIs: "默认值: {value}" defaultValueIs: "默认值: {value}"
noCustomEmojis: "没有自定义表情符号" noCustomEmojis: "没有自定义表情符号"
noJobs: "没有任务" noJobs: "没有任务"
federating: "联合中" federating: "联合中"
blocked: "已屏蔽" blocked: "已拉黑"
suspended: "停止投递" suspended: "停止投递"
all: "全部" all: "全部"
subscribing: "已订阅" subscribing: "已订阅"
@ -303,7 +304,7 @@ explore: "发现"
messageRead: "已读" messageRead: "已读"
noMoreHistory: "没有更多的历史记录" noMoreHistory: "没有更多的历史记录"
startChat: "开始聊天" startChat: "开始聊天"
nUsersRead: "{n} 人已读" nUsersRead: "{n}人已读"
agreeTo: "勾选则表示已阅读并同意 {0}" agreeTo: "勾选则表示已阅读并同意 {0}"
agree: "同意" agree: "同意"
agreeBelow: "同意以下内容" agreeBelow: "同意以下内容"
@ -395,7 +396,7 @@ basicInfo: "基本信息"
pinnedUsers: "置顶用户" pinnedUsers: "置顶用户"
pinnedUsersDescription: "输入您想要固定到“发现”页面的用户,一行一个。" pinnedUsersDescription: "输入您想要固定到“发现”页面的用户,一行一个。"
pinnedPages: "固定页面" pinnedPages: "固定页面"
pinnedPagesDescription: "输入您要固定到服务器首页的页面路径,一行一个。" pinnedPagesDescription: "输入您要固定到服务器首页的页面路径,以换行符分隔。"
pinnedClipId: "置顶的便签 ID" pinnedClipId: "置顶的便签 ID"
pinnedNotes: "已置顶的帖子" pinnedNotes: "已置顶的帖子"
hcaptcha: "hCaptcha" hcaptcha: "hCaptcha"
@ -428,7 +429,7 @@ notifyAntenna: "开启通知"
withFileAntenna: "仅带有附件的帖子" withFileAntenna: "仅带有附件的帖子"
excludeNotesInSensitiveChannel: "排除敏感频道内的帖子" excludeNotesInSensitiveChannel: "排除敏感频道内的帖子"
enableServiceworker: "启用 ServiceWorker" enableServiceworker: "启用 ServiceWorker"
antennaUsersDescription: "指定用户名,一行一个" antennaUsersDescription: "指定用户名,用换行符进行分隔"
caseSensitive: "区分大小写" caseSensitive: "区分大小写"
withReplies: "包括回复" withReplies: "包括回复"
connectedTo: "您的账号已连到接以下第三方账号" connectedTo: "您的账号已连到接以下第三方账号"
@ -460,7 +461,7 @@ moderationNote: "管理笔记"
moderationNoteDescription: "可以用来记录仅在管理员之间共享的笔记。" moderationNoteDescription: "可以用来记录仅在管理员之间共享的笔记。"
addModerationNote: "添加管理笔记" addModerationNote: "添加管理笔记"
moderationLogs: "管理日志" moderationLogs: "管理日志"
nUsersMentioned: "{n} 被提到" nUsersMentioned: "{n}人投稿"
securityKeyAndPasskey: "安全密钥或 Passkey" securityKeyAndPasskey: "安全密钥或 Passkey"
securityKey: "安全密钥" securityKey: "安全密钥"
lastUsed: "最后使用:" lastUsed: "最后使用:"
@ -477,7 +478,7 @@ notFoundDescription: "没有与指定 URL 对应的页面。"
uploadFolder: "默认上传文件夹" uploadFolder: "默认上传文件夹"
markAsReadAllNotifications: "将所有通知标为已读" markAsReadAllNotifications: "将所有通知标为已读"
markAsReadAllUnreadNotes: "将所有帖子标记为已读" markAsReadAllUnreadNotes: "将所有帖子标记为已读"
markAsReadAllTalkMessages: "将所有聊天标记为已读" markAsReadAllTalkMessages: "将所有私信标记为已读"
help: "帮助" help: "帮助"
inputMessageHere: "在此键入信息" inputMessageHere: "在此键入信息"
close: "关闭" close: "关闭"
@ -687,10 +688,10 @@ emptyToDisableSmtpAuth: "用户名和密码留空可以禁用 SMTP 验证"
smtpSecure: "在 SMTP 连接中使用隐式 SSL / TLS" smtpSecure: "在 SMTP 连接中使用隐式 SSL / TLS"
smtpSecureInfo: "使用 STARTTLS 时关闭。" smtpSecureInfo: "使用 STARTTLS 时关闭。"
testEmail: "邮件发送测试" testEmail: "邮件发送测试"
wordMute: "隐藏关键词" wordMute: "屏蔽关键词"
wordMuteDescription: "折叠包含指定关键词的帖子。被折叠的帖子可单击展开。" wordMuteDescription: "折叠包含指定关键词的帖子。被折叠的帖子可单击展开。"
hardWordMute: "隐藏硬关键词" hardWordMute: "强屏蔽关键词"
showMutedWord: "显示已隐藏的关键词" showMutedWord: "显示屏蔽关键词"
hardWordMuteDescription: "隐藏包含指定关键词的帖子。与隐藏关键词不同,帖子将完全不会显示。" hardWordMuteDescription: "隐藏包含指定关键词的帖子。与隐藏关键词不同,帖子将完全不会显示。"
regexpError: "正则表达式错误" regexpError: "正则表达式错误"
regexpErrorDescription: "{tab} 隐藏文字的第 {line} 行的正则表达式有错误:" regexpErrorDescription: "{tab} 隐藏文字的第 {line} 行的正则表达式有错误:"
@ -779,7 +780,7 @@ emailVerified: "电子邮件地址已验证"
noteFavoritesCount: "收藏的帖子数" noteFavoritesCount: "收藏的帖子数"
pageLikesCount: "页面点赞次数" pageLikesCount: "页面点赞次数"
pageLikedCount: "页面被点赞次数" pageLikedCount: "页面被点赞次数"
contact: "联系" contact: "联系方式"
useSystemFont: "使用系统默认字体" useSystemFont: "使用系统默认字体"
clips: "便签" clips: "便签"
experimentalFeatures: "实验性功能" experimentalFeatures: "实验性功能"
@ -800,7 +801,7 @@ showTitlebar: "显示标题栏"
clearCache: "清除缓存" clearCache: "清除缓存"
onlineUsersCount: "{n} 人在线" onlineUsersCount: "{n} 人在线"
nUsers: "{n} 用户" nUsers: "{n} 用户"
nNotes: "{n} 帖子" nNotes: "{n}帖子"
sendErrorReports: "发送错误报告" sendErrorReports: "发送错误报告"
sendErrorReportsDescription: "启用后,如果出现问题,可以与 Misskey 共享详细的错误信息,从而帮助提高软件的质量。错误信息包括操作系统版本、浏览器类型、行为历史记录等。" sendErrorReportsDescription: "启用后,如果出现问题,可以与 Misskey 共享详细的错误信息,从而帮助提高软件的质量。错误信息包括操作系统版本、浏览器类型、行为历史记录等。"
myTheme: "我的主题" myTheme: "我的主题"
@ -824,7 +825,7 @@ youAreRunningUpToDateClient: "您所使用的客户端已经是最新的。"
newVersionOfClientAvailable: "新版本的客户端可用。" newVersionOfClientAvailable: "新版本的客户端可用。"
usageAmount: "使用量" usageAmount: "使用量"
capacity: "容量" capacity: "容量"
inUse: "使用" inUse: "使用"
editCode: "编辑代码" editCode: "编辑代码"
apply: "应用" apply: "应用"
receiveAnnouncementFromInstance: "从服务器接收通知" receiveAnnouncementFromInstance: "从服务器接收通知"
@ -869,12 +870,12 @@ noMaintainerInformationWarning: "尚未设置管理员信息。"
noInquiryUrlWarning: "尚未设置联络地址。" noInquiryUrlWarning: "尚未设置联络地址。"
noBotProtectionWarning: "尚未设置 Bot 防御。" noBotProtectionWarning: "尚未设置 Bot 防御。"
configure: "设置" configure: "设置"
postToGallery: "发送到图库" postToGallery: "添加至相册"
postToHashtag: "投稿到这个标签" postToHashtag: "投稿到这个标签"
gallery: "图库" gallery: "相册"
recentPosts: "最新发布" recentPosts: "最新发布"
popularPosts: "热门投稿" popularPosts: "热门投稿"
shareWithNote: "在帖子中分享" shareWithNote: "分享到贴文"
ads: "广告" ads: "广告"
expiration: "截止时间" expiration: "截止时间"
startingperiod: "开始时间" startingperiod: "开始时间"
@ -885,7 +886,7 @@ middle: "中"
low: "低" low: "低"
emailNotConfiguredWarning: "尚未设置电子邮件地址。" emailNotConfiguredWarning: "尚未设置电子邮件地址。"
ratio: "比率" ratio: "比率"
previewNoteText: "预览" previewNoteText: "预览文"
customCss: "自定义 CSS" customCss: "自定义 CSS"
customCssWarn: "这些设置必须有相关的基础知识,不当的配置可能导致客户端无法正常使用。" customCssWarn: "这些设置必须有相关的基础知识,不当的配置可能导致客户端无法正常使用。"
global: "全局" global: "全局"
@ -924,8 +925,8 @@ manageAccounts: "管理账户"
makeReactionsPublic: "将回应设置为公开" makeReactionsPublic: "将回应设置为公开"
makeReactionsPublicDescription: "将您发表过的回应设置成公开可见。" makeReactionsPublicDescription: "将您发表过的回应设置成公开可见。"
classic: "经典" classic: "经典"
muteThread: "隐藏帖子列表" muteThread: "屏蔽帖文串"
unmuteThread: "取消隐藏帖子列表" unmuteThread: "取消屏蔽帖文串"
followingVisibility: "关注的人的公开范围" followingVisibility: "关注的人的公开范围"
followersVisibility: "关注者的公开范围" followersVisibility: "关注者的公开范围"
continueThread: "查看更多帖子" continueThread: "查看更多帖子"
@ -948,17 +949,17 @@ searchByGoogle: "Google"
instanceDefaultLightTheme: "服务器默认浅色主题" instanceDefaultLightTheme: "服务器默认浅色主题"
instanceDefaultDarkTheme: "服务器默认深色主题" instanceDefaultDarkTheme: "服务器默认深色主题"
instanceDefaultThemeDescription: "以对象格式输入主题代码" instanceDefaultThemeDescription: "以对象格式输入主题代码"
mutePeriod: "隐藏期限" mutePeriod: "屏蔽期限"
period: "截止时间" period: "截止时间"
indefinitely: "永久" indefinitely: "永久"
tenMinutes: "10 分钟" tenMinutes: "10分钟"
oneHour: "1 小时" oneHour: "1 小时"
oneDay: "1 天" oneDay: "1天"
oneWeek: "1 周" oneWeek: "1 周"
oneMonth: "1 个月" oneMonth: "1个月"
threeMonths: "3 个月" threeMonths: "3个月"
oneYear: "1 年" oneYear: "1 年"
threeDays: "3 天" threeDays: "3天"
reflectMayTakeTime: "可能需要一些时间才能体现出效果。" reflectMayTakeTime: "可能需要一些时间才能体现出效果。"
failedToFetchAccountInformation: "获取账户信息失败" failedToFetchAccountInformation: "获取账户信息失败"
rateLimitExceeded: "已超过速率限制" rateLimitExceeded: "已超过速率限制"
@ -967,8 +968,8 @@ cropImageAsk: "是否要裁剪图像?"
cropYes: "去裁剪" cropYes: "去裁剪"
cropNo: "就这样吧!" cropNo: "就这样吧!"
file: "文件" file: "文件"
recentNHours: "最近 {n} 小时" recentNHours: "最近{n}小时"
recentNDays: "最近 {n} 天" recentNDays: "最近{n}天"
noEmailServerWarning: "电子邮件服务器未设置。" noEmailServerWarning: "电子邮件服务器未设置。"
thereIsUnresolvedAbuseReportWarning: "有未解决的报告" thereIsUnresolvedAbuseReportWarning: "有未解决的报告"
recommended: "推荐" recommended: "推荐"
@ -1079,7 +1080,7 @@ postToTheChannel: "发布到频道"
cannotBeChangedLater: "之后不能再更改。" cannotBeChangedLater: "之后不能再更改。"
reactionAcceptance: "接受表情回应" reactionAcceptance: "接受表情回应"
likeOnly: "仅点赞" likeOnly: "仅点赞"
likeOnlyForRemote: "远程仅点赞" likeOnlyForRemote: "全部(远程仅点赞"
nonSensitiveOnly: "仅限非敏感内容" nonSensitiveOnly: "仅限非敏感内容"
nonSensitiveOnlyForLocalLikeOnlyForRemote: "仅限非敏感内容(远程仅点赞)" nonSensitiveOnlyForLocalLikeOnlyForRemote: "仅限非敏感内容(远程仅点赞)"
rolesAssignedToMe: "指派给自己的角色" rolesAssignedToMe: "指派给自己的角色"
@ -1150,7 +1151,7 @@ youFollowing: "正在关注"
preventAiLearning: "拒绝接受生成式 AI 的学习" preventAiLearning: "拒绝接受生成式 AI 的学习"
preventAiLearningDescription: "要求文章生成 AI 或图像生成 AI 不能够以发布的帖子和图像等内容作为学习对象。这是通过在 HTML 响应中包含 noai 标志来实现的,这不能完全阻止 AI 学习你的发布内容,并不是所有 AI 都会遵守这类请求。" preventAiLearningDescription: "要求文章生成 AI 或图像生成 AI 不能够以发布的帖子和图像等内容作为学习对象。这是通过在 HTML 响应中包含 noai 标志来实现的,这不能完全阻止 AI 学习你的发布内容,并不是所有 AI 都会遵守这类请求。"
options: "选项" options: "选项"
specifyUser: "用户指定" specifyUser: "指定用户"
lookupConfirm: "确定查询?" lookupConfirm: "确定查询?"
openTagPageConfirm: "确定打开话题标签页面?" openTagPageConfirm: "确定打开话题标签页面?"
specifyHost: "指定主机名" specifyHost: "指定主机名"
@ -1265,7 +1266,7 @@ replaying: "重播中"
endReplay: "结束回放" endReplay: "结束回放"
copyReplayData: "复制回放数据" copyReplayData: "复制回放数据"
ranking: "排行榜" ranking: "排行榜"
lastNDays: "最近 {n} 天" lastNDays: "最近{n}天"
backToTitle: "返回标题" backToTitle: "返回标题"
hemisphere: "居住地区" hemisphere: "居住地区"
withSensitive: "显示包含敏感媒体的帖子" withSensitive: "显示包含敏感媒体的帖子"
@ -1316,6 +1317,7 @@ acknowledgeNotesAndEnable: "理解注意事项后再开启。"
federationSpecified: "此服务器已开启联合白名单。只能与管理员指定的服务器通信。" federationSpecified: "此服务器已开启联合白名单。只能与管理员指定的服务器通信。"
federationDisabled: "此服务器已禁用联合。无法与其它服务器上的用户通信。" federationDisabled: "此服务器已禁用联合。无法与其它服务器上的用户通信。"
draft: "草稿" draft: "草稿"
draftsAndScheduledNotes: "草稿和定时发送"
confirmOnReact: "发送回应前需要确认" confirmOnReact: "发送回应前需要确认"
reactAreYouSure: "要用「{emoji}」进行回应吗?" reactAreYouSure: "要用「{emoji}」进行回应吗?"
markAsSensitiveConfirm: "要将此媒体标记为敏感吗?" markAsSensitiveConfirm: "要将此媒体标记为敏感吗?"
@ -1343,7 +1345,7 @@ postForm: "投稿窗口"
textCount: "字数" textCount: "字数"
information: "关于" information: "关于"
chat: "聊天" chat: "聊天"
directMessage: "聊天" directMessage: "私信"
directMessage_short: "消息" directMessage_short: "消息"
migrateOldSettings: "迁移旧设置信息" migrateOldSettings: "迁移旧设置信息"
migrateOldSettings_description: "通常设置信息将自动迁移。但如果由于某种原因迁移不成功,则可以手动触发迁移过程。当前的配置信息将被覆盖。" migrateOldSettings_description: "通常设置信息将自动迁移。但如果由于某种原因迁移不成功,则可以手动触发迁移过程。当前的配置信息将被覆盖。"
@ -1362,24 +1364,40 @@ advice: "建议"
realtimeMode: "实时模式" realtimeMode: "实时模式"
turnItOn: "开启" turnItOn: "开启"
turnItOff: "关闭" turnItOff: "关闭"
emojiMute: "隐藏表情符号" emojiMute: "屏蔽表情符号"
emojiUnmute: "解除隐藏表情符号" emojiUnmute: "取消屏蔽表情符号"
muteX: "隐藏{x}" muteX: "屏蔽{x}"
unmuteX: "解除隐藏{x}" unmuteX: "取消屏蔽{x}"
abort: "中止" abort: "中止"
tip: "提示和技巧" tip: "提示和技巧"
redisplayAllTips: "重新显示所有的提示和技巧" redisplayAllTips: "重新显示所有的提示和技巧"
hideAllTips: "隐藏所有的提示和技巧" hideAllTips: "隐藏所有的提示和技巧"
defaultImageCompressionLevel: "默认图像压缩等级" defaultImageCompressionLevel: "默认图像压缩等级"
defaultImageCompressionLevel_description: "较低的等级可以保持画质,但会增加文件大小。<br>较高的等级可以减少文件大小,但相对应的画质将会降低。" defaultImageCompressionLevel_description: "较低的等级可以保持画质,但会增加文件大小。<br>较高的等级可以减少文件大小,但相对应的画质将会降低。"
inMinutes: "分" defaultCompressionLevel: "默认压缩等级"
inDays: "日" defaultCompressionLevel_description: "较低的等级可以保持质量,但会增加文件大小。<br>较高的等级可以减少文件大小,但相对应的质量将会降低。"
inMinutes: "分钟"
inDays: "天"
safeModeEnabled: "已启用安全模式" safeModeEnabled: "已启用安全模式"
pluginsAreDisabledBecauseSafeMode: "因启用了安全模式,所有插件均已被禁用。" pluginsAreDisabledBecauseSafeMode: "因启用了安全模式,所有插件均已被禁用。"
customCssIsDisabledBecauseSafeMode: "因启用了安全模式,无法应用自定义 CSS。" customCssIsDisabledBecauseSafeMode: "因启用了安全模式,无法应用自定义 CSS。"
themeIsDefaultBecauseSafeMode: "启用安全模式时将使用默认主题。关闭安全模式后将还原。" themeIsDefaultBecauseSafeMode: "启用安全模式时将使用默认主题。关闭安全模式后将还原。"
thankYouForTestingBeta: "感谢您协助测试 beta 版!" thankYouForTestingBeta: "感谢您协助测试 beta 版!"
createUserSpecifiedNote: "创建指定用户的帖子" createUserSpecifiedNote: "创建指定用户的帖子"
schedulePost: "定时发布"
scheduleToPostOnX: "预定在 {x} 发出"
scheduledToPostOnX: "已预定在 {x} 发出"
schedule: "定时"
scheduled: "定时"
_compression:
_quality:
high: "高质量"
medium: "中质量"
low: "低质量"
_size:
large: "大"
medium: "中"
small: "小"
_order: _order:
newest: "从新到旧" newest: "从新到旧"
oldest: "从旧到新" oldest: "从旧到新"
@ -1390,16 +1408,16 @@ _chat:
individualChat: "私聊" individualChat: "私聊"
individualChat_description: "可以与特定用户进行一对一聊天。" individualChat_description: "可以与特定用户进行一对一聊天。"
roomChat: "群聊" roomChat: "群聊"
roomChat_description: "可以进行多人聊天。\n就算用户未允许私聊只要接受了邀请仍可以聊天。" roomChat_description: "支持多人同时进行消息交流。\n即使部分用户未开放私信权限只要接受了邀请仍可进行聊天。"
createRoom: "创建房间" createRoom: "创建群组"
inviteUserToChat: "邀请用户来开始聊天" inviteUserToChat: "邀请用户来开始聊天"
yourRooms: "已创建的房间" yourRooms: "创建的群组"
joiningRooms: "已加入的房间" joiningRooms: "已加入的群组"
invitations: "邀请" invitations: "邀请"
noInvitations: "没有邀请" noInvitations: "没有邀请"
history: "历史" history: "历史"
noHistory: "没有历史记录" noHistory: "没有历史记录"
noRooms: "没有房间" noRooms: "没有群组"
inviteUser: "邀请用户" inviteUser: "邀请用户"
sentInvitations: "已发送的邀请" sentInvitations: "已发送的邀请"
join: "加入" join: "加入"
@ -1410,16 +1428,16 @@ _chat:
home: "首页" home: "首页"
send: "发送" send: "发送"
newline: "换行" newline: "换行"
muteThisRoom: "静音此房间" muteThisRoom: "屏蔽该群组"
deleteRoom: "删除房间" deleteRoom: "删除群组"
chatNotAvailableForThisAccountOrServer: "此服务器或者账户还未开启聊天功能。" chatNotAvailableForThisAccountOrServer: "此服务器或者账户还未开启聊天功能。"
chatIsReadOnlyForThisAccountOrServer: "此服务器或者账户内的聊天为只读。无法发布新信息或创建及加入群聊。" chatIsReadOnlyForThisAccountOrServer: "此服务器或者账户内的聊天为只读。无法发布新信息或创建及加入群聊。"
chatNotAvailableInOtherAccount: "对方账户目前处于无法使用聊天的状态。" chatNotAvailableInOtherAccount: "对方的账户当前无法使用私信。"
cannotChatWithTheUser: "无法与此用户聊天" cannotChatWithTheUser: "无法私信该用户"
cannotChatWithTheUser_description: "可能现在无法使用聊天,或者对方未开启聊天。" cannotChatWithTheUser_description: "可能现在无法使用聊天,或者对方未开启聊天。"
youAreNotAMemberOfThisRoomButInvited: "您还未加入此房间,但已收到邀请。如要加入,请接受邀请。" youAreNotAMemberOfThisRoomButInvited: "您还未加入此房间,但已收到邀请。如要加入,请接受邀请。"
doYouAcceptInvitation: "要接受邀请吗?" doYouAcceptInvitation: "要接受邀请吗?"
chatWithThisUser: "聊天" chatWithThisUser: "私信"
thisUserAllowsChatOnlyFromFollowers: "此用户仅接受关注者发起的聊天。" thisUserAllowsChatOnlyFromFollowers: "此用户仅接受关注者发起的聊天。"
thisUserAllowsChatOnlyFromFollowing: "此用户仅接受关注的人发起的聊天。" thisUserAllowsChatOnlyFromFollowing: "此用户仅接受关注的人发起的聊天。"
thisUserAllowsChatOnlyFromMutualFollowing: "此用户仅接受互相关注的人发起的聊天。" thisUserAllowsChatOnlyFromMutualFollowing: "此用户仅接受互相关注的人发起的聊天。"
@ -1897,7 +1915,7 @@ _achievements:
description: "试图对网盘中的文件夹进行循环嵌套" description: "试图对网盘中的文件夹进行循环嵌套"
_reactWithoutRead: _reactWithoutRead:
title: "有好好读过吗?" title: "有好好读过吗?"
description: "在含有 100 字以上的帖子被发出三秒内做出回应" description: "在含有100字以上的帖子被发出三秒内做出回应"
_clickedClickHere: _clickedClickHere:
title: "点这里" title: "点这里"
description: "点了这里" description: "点了这里"
@ -1999,7 +2017,7 @@ _role:
canUpdateBioMedia: "可以更新头像和横幅" canUpdateBioMedia: "可以更新头像和横幅"
pinMax: "帖子置顶数量限制" pinMax: "帖子置顶数量限制"
antennaMax: "可创建的最大天线数量" antennaMax: "可创建的最大天线数量"
wordMuteMax: "隐藏词的字数限制" wordMuteMax: "屏蔽词的字数限制"
webhookMax: "Webhook 创建数量限制" webhookMax: "Webhook 创建数量限制"
clipMax: "便签创建数量限制" clipMax: "便签创建数量限制"
noteEachClipsMax: "单个便签内的贴文数量限制" noteEachClipsMax: "单个便签内的贴文数量限制"
@ -2017,11 +2035,12 @@ _role:
canImportFollowing: "允许导入关注列表" canImportFollowing: "允许导入关注列表"
canImportMuting: "允许导入隐藏列表" canImportMuting: "允许导入隐藏列表"
canImportUserLists: "允许导入用户列表" canImportUserLists: "允许导入用户列表"
chatAvailability: "允许聊天" chatAvailability: "允许私信"
uploadableFileTypes: "可上传的文件类型" uploadableFileTypes: "可上传的文件类型"
uploadableFileTypes_caption: "指定 MIME 类型。可用换行指定多个类型,也可以用星号(*)作为通配符。(如 image/*" uploadableFileTypes_caption: "指定 MIME 类型。可用换行指定多个类型,也可以用星号(*)作为通配符。(如 image/*"
uploadableFileTypes_caption2: "文件根据文件的不同,可能无法判断其类型。若要允许此类文件,请在指定中添加 {x}。" uploadableFileTypes_caption2: "文件根据文件的不同,可能无法判断其类型。若要允许此类文件,请在指定中添加 {x}。"
noteDraftLimit: "可在服务器上创建多少草稿" noteDraftLimit: "可在服务器上创建多少草稿"
scheduledNoteLimit: "可同时创建的定时帖子数量"
watermarkAvailable: "能否使用水印功能" watermarkAvailable: "能否使用水印功能"
_condition: _condition:
roleAssignedTo: "已分配给手动角色" roleAssignedTo: "已分配给手动角色"
@ -2087,9 +2106,9 @@ _forgotPassword:
ifNoEmail: "如果您没有设置电子邮件地址,请联系管理员。" ifNoEmail: "如果您没有设置电子邮件地址,请联系管理员。"
contactAdmin: "该服务器不支持发送电子邮件。如果您想重设密码,请联系管理员。" contactAdmin: "该服务器不支持发送电子邮件。如果您想重设密码,请联系管理员。"
_gallery: _gallery:
my: "我的图库" my: "我的相册"
liked: "喜欢的图片" liked: "喜欢的相册"
like: "喜欢" like: "喜欢"
unlike: "取消喜欢" unlike: "取消喜欢"
_email: _email:
_follow: _follow:
@ -2155,14 +2174,14 @@ _channel:
edit: "编辑频道" edit: "编辑频道"
setBanner: "设置横幅" setBanner: "设置横幅"
removeBanner: "删除横幅" removeBanner: "删除横幅"
featured: "热" featured: "热"
owned: "管理" owned: "正在管理"
following: "正在关注" following: "正在关注"
usersCount: "有 {n} 人参与" usersCount: "有{n}人参与"
notesCount: "有 {n} 个帖子" notesCount: "有{n}个帖子"
nameAndDescription: "名称与描述" nameAndDescription: "名称与描述"
nameOnly: "仅名称" nameOnly: "仅名称"
allowRenoteToExternal: "允许在频道外转帖及引用" allowRenoteToExternal: "允许转发到频道外和引用"
_menuDisplay: _menuDisplay:
sideFull: "横向" sideFull: "横向"
sideIcon: "横向(图标)" sideIcon: "横向(图标)"
@ -2173,10 +2192,10 @@ _wordMute:
muteWordsDescription: "AND 条件用空格分隔OR 条件用换行符分隔。" muteWordsDescription: "AND 条件用空格分隔OR 条件用换行符分隔。"
muteWordsDescription2: "正则表达式用斜线包裹" muteWordsDescription2: "正则表达式用斜线包裹"
_instanceMute: _instanceMute:
instanceMuteDescription: "隐藏服务器中所有的帖子和转帖,包括这些服务器上用户的回复。" instanceMuteDescription: "屏蔽服务器中所有的帖子和转帖,包括该服务器内用户的回复。"
instanceMuteDescription2: "一行一个" instanceMuteDescription2: "通过换行符分隔进行设置"
title: "下面实例中的帖子将被隐藏。" title: "下面实例中的帖子将被隐藏。"
heading: "已隐藏的服务器" heading: "已屏蔽的服务器"
_theme: _theme:
explore: "寻找主题" explore: "寻找主题"
install: "安装主题" install: "安装主题"
@ -2249,7 +2268,7 @@ _sfx:
noteMy: "我的帖子" noteMy: "我的帖子"
notification: "通知" notification: "通知"
reaction: "选择回应时" reaction: "选择回应时"
chatMessage: "聊天信息" chatMessage: "私信"
_soundSettings: _soundSettings:
driveFile: "使用网盘内的音频" driveFile: "使用网盘内的音频"
driveFileWarn: "选择网盘上的文件" driveFileWarn: "选择网盘上的文件"
@ -2260,28 +2279,28 @@ _soundSettings:
driveFileError: "无法读取声音。请更改设置。" driveFileError: "无法读取声音。请更改设置。"
_ago: _ago:
future: "未来" future: "未来"
justNow: "最近" justNow: "刚刚"
secondsAgo: "{n} 秒前" secondsAgo: "{n}秒前"
minutesAgo: "{n} 分前" minutesAgo: "{n}前"
hoursAgo: "{n} 小时前" hoursAgo: "{n}小时前"
daysAgo: "{n}前" daysAgo: "{n}前"
weeksAgo: "{n} 周前" weeksAgo: "{n}周前"
monthsAgo: "{n} 月前" monthsAgo: "{n}月前"
yearsAgo: "{n} 年前" yearsAgo: "{n}年前"
invalid: "没有" invalid: "没有"
_timeIn: _timeIn:
seconds: "{n}秒后" seconds: "{n}秒后"
minutes: "{n} 分后" minutes: "{n}后"
hours: "{n} 小时后" hours: "{n}小时后"
days: "{n}天后" days: "{n}天后"
weeks: "{n} 周后" weeks: "{n}周后"
months: "{n} 月后" months: "{n}月后"
years: "{n} 年后" years: "{n}年后"
_time: _time:
second: "秒" second: "秒"
minute: "分" minute: "分"
hour: "小时" hour: "小时"
day: "" day: ""
month: "个月" month: "个月"
_2fa: _2fa:
alreadyRegistered: "此设备已被注册" alreadyRegistered: "此设备已被注册"
@ -2315,36 +2334,36 @@ _2fa:
_permissions: _permissions:
"read:account": "查看账户信息" "read:account": "查看账户信息"
"write:account": "更改帐户信息" "write:account": "更改帐户信息"
"read:blocks": "查看屏蔽列表" "read:blocks": "查看黑名单"
"write:blocks": "编辑屏蔽列表" "write:blocks": "编辑黑名单"
"read:drive": "查看网盘" "read:drive": "查看网盘"
"write:drive": "管理网盘文件" "write:drive": "管理网盘文件"
"read:favorites": "查看收藏夹" "read:favorites": "查看收藏夹"
"write:favorites": "编辑收藏夹" "write:favorites": "编辑收藏夹"
"read:following": "查看关注信息" "read:following": "查看关注信息"
"write:following": "关注/取消关注" "write:following": "关注/取消关注"
"read:messaging": "查看消息" "read:messaging": "查看私信"
"write:messaging": "撰写或删除消息" "write:messaging": "撰写或删除消息"
"read:mutes": "查看隐藏列表" "read:mutes": "查看屏蔽列表"
"write:mutes": "编辑隐藏列表" "write:mutes": "编辑屏蔽列表"
"write:notes": "撰写或删除帖子" "write:notes": "撰写或删除帖子"
"read:notifications": "查看通知" "read:notifications": "查看通知"
"write:notifications": "管理通知" "write:notifications": "管理通知"
"read:reactions": "查看回应" "read:reactions": "查看回应"
"write:reactions": "回应操作" "write:reactions": "编辑回应"
"write:votes": "投票" "write:votes": "投票"
"read:pages": "查看页面" "read:pages": "查看页面"
"write:pages": "操作页面" "write:pages": "编辑页面"
"read:page-likes": "查看喜欢的页面" "read:page-likes": "查看喜欢的页面"
"write:page-likes": "操作喜欢的页面" "write:page-likes": "管理喜欢的页面"
"read:user-groups": "查看用户组" "read:user-groups": "查看用户组"
"write:user-groups": "操作用户组" "write:user-groups": "编辑用户组"
"read:channels": "查看频道" "read:channels": "查看频道"
"write:channels": "管理频道" "write:channels": "管理频道"
"read:gallery": "浏览图库" "read:gallery": "浏览相册"
"write:gallery": "操作图库" "write:gallery": "编辑相册"
"read:gallery-likes": "读取喜欢的图片" "read:gallery-likes": "浏览喜欢的相册"
"write:gallery-likes": "操作喜欢的图片" "write:gallery-likes": "管理喜欢的相册"
"read:flash": "查看 Play" "read:flash": "查看 Play"
"write:flash": "编辑 Play" "write:flash": "编辑 Play"
"read:flash-likes": "查看 Play 的点赞" "read:flash-likes": "查看 Play 的点赞"
@ -2372,33 +2391,33 @@ _permissions:
"read:admin:roles": "查看角色" "read:admin:roles": "查看角色"
"write:admin:relays": "编辑中继" "write:admin:relays": "编辑中继"
"read:admin:relays": "查看中继" "read:admin:relays": "查看中继"
"write:admin:invite-codes": "编辑邀请码" "write:admin:invite-codes": "管理邀请码"
"read:admin:invite-codes": "查看邀请码" "read:admin:invite-codes": "查看邀请码"
"write:admin:announcements": "编辑公告" "write:admin:announcements": "管理公告"
"read:admin:announcements": "查看公告" "read:admin:announcements": "查看公告"
"write:admin:avatar-decorations": "编辑头像挂件" "write:admin:avatar-decorations": "编辑头像挂件"
"read:admin:avatar-decorations": "查看头像挂件" "read:admin:avatar-decorations": "查看头像挂件"
"write:admin:federation": "编辑联合相关信息" "write:admin:federation": "编辑联合相关信息"
"write:admin:account": "编辑用户账户" "write:admin:account": "编辑用户账户"
"read:admin:account": "查看用户相关情报" "read:admin:account": "查看用户相关情报"
"write:admin:emoji": "编辑表情文字" "write:admin:emoji": "编辑表情符号"
"read:admin:emoji": "查看表情文字" "read:admin:emoji": "查看表情符号"
"write:admin:queue": "编辑作业队列" "write:admin:queue": "编辑作业队列"
"read:admin:queue": "查看作业队列相关情报" "read:admin:queue": "查看作业队列相关情报"
"write:admin:promo": "运营推广说明" "write:admin:promo": "运营推广说明"
"write:admin:drive": "编辑用户网盘" "write:admin:drive": "管理用户网盘"
"read:admin:drive": "查看用户网盘相关情报" "read:admin:drive": "查看用户网盘相关情报"
"read:admin:stream": "使用管理员用的 Websocket API" "read:admin:stream": "使用管理员用的 Websocket API"
"write:admin:ad": "编辑广告" "write:admin:ad": "管理广告"
"read:admin:ad": "查看广告" "read:admin:ad": "查看广告"
"write:invite-codes": "生成邀请码" "write:invite-codes": "生成邀请码"
"read:invite-codes": "获取已发行的邀请码" "read:invite-codes": "获取已发行的邀请码"
"write:clip-favorite": "编辑便签的点赞" "write:clip-favorite": "管理喜欢的便签"
"read:clip-favorite": "查看便签的点赞" "read:clip-favorite": "查看便签的点赞"
"read:federation": "查看联合相关信息" "read:federation": "查看联合相关信息"
"write:report-abuse": "举报用户" "write:report-abuse": "举报用户"
"write:chat": "撰写或删除消息" "write:chat": "撰写或删除消息"
"read:chat": "查看聊天" "read:chat": "查看私信"
_auth: _auth:
shareAccessTitle: "应用程序授权许可" shareAccessTitle: "应用程序授权许可"
shareAccess: "您要授权允许 “{name}” 访问您的帐户吗?" shareAccess: "您要授权允许 “{name}” 访问您的帐户吗?"
@ -2417,7 +2436,7 @@ _antennaSources:
homeTimeline: "已关注用户的帖子" homeTimeline: "已关注用户的帖子"
users: "来自指定用户的帖子" users: "来自指定用户的帖子"
userList: "来自指定列表中的帖子" userList: "来自指定列表中的帖子"
userBlacklist: "除掉已选择用户后所有的帖子" userBlacklist: "过滤指定用户后的所有帖子"
_weekday: _weekday:
sunday: "星期日" sunday: "星期日"
monday: "星期一" monday: "星期一"
@ -2457,7 +2476,7 @@ _widgets:
chooseList: "选择列表" chooseList: "选择列表"
clicker: "点击器" clicker: "点击器"
birthdayFollowings: "今天是他们的生日" birthdayFollowings: "今天是他们的生日"
chat: "聊天" chat: "私信"
_cw: _cw:
hide: "隐藏" hide: "隐藏"
show: "查看更多" show: "查看更多"
@ -2465,26 +2484,26 @@ _cw:
files: "{count} 个文件" files: "{count} 个文件"
_poll: _poll:
noOnlyOneChoice: "需要至少两个选项" noOnlyOneChoice: "需要至少两个选项"
choiceN: "选{n}" choiceN: "选{n}"
noMore: "无法再添加更多了" noMore: "无法再添加更多了"
canMultipleVote: "允许多个投票" canMultipleVote: "允许选择多个选项"
expiration: "截止时间" expiration: "截止时间"
infinite: "永久" infinite: "永久"
at: "指定日期" at: "指定日期"
after: "指定时间" after: "指定时间"
deadlineDate: "截止日期" deadlineDate: "截止日期"
deadlineTime: "时" deadlineTime: ""
duration: "时长" duration: "期限"
votesCount: "{n} 票" votesCount: "{n}票"
totalVotes: "总票数 {n}" totalVotes: "总票数 {n}"
vote: "投票" vote: "投票"
showResult: "显示结果" showResult: "显示结果"
voted: "已投票" voted: "已投票"
closed: "已截止" closed: "已截止"
remainingDays: "{d} {h} 小时后截止" remainingDays: "{d}天{h}小时后截止"
remainingHours: "{h} 小时 {m} 分后截止" remainingHours: "{h} 小时 {m} 分后截止"
remainingMinutes: "{m} {s} 秒后截止" remainingMinutes: "{m}分{s}秒后截止"
remainingSeconds: "{s} 秒后截止" remainingSeconds: "{s}秒后截止"
_visibility: _visibility:
public: "公开" public: "公开"
publicDescription: "您的帖子将出现在全局时间线上" publicDescription: "您的帖子将出现在全局时间线上"
@ -2503,9 +2522,9 @@ _postForm:
quotePlaceholder: "引用这个帖子..." quotePlaceholder: "引用这个帖子..."
channelPlaceholder: "发布到频道…" channelPlaceholder: "发布到频道…"
_placeholders: _placeholders:
a: "现在如何" a: "现在怎么样"
b: "发生了什么" b: "想好发些什么了吗"
c: "你有什么想法" c: "在想些什么呢"
d: "你想要发布些什么吗?" d: "你想要发布些什么吗?"
e: "请写下来吧" e: "请写下来吧"
f: "等待您的发布..." f: "等待您的发布..."
@ -2531,8 +2550,8 @@ _exportOrImport:
favoritedNotes: "收藏的帖子" favoritedNotes: "收藏的帖子"
clips: "便签" clips: "便签"
followingList: "关注中" followingList: "关注中"
muteList: "隐藏" muteList: "屏蔽"
blockingList: "屏蔽" blockingList: "拉黑"
userLists: "列表" userLists: "列表"
excludeMutingUsers: "排除屏蔽用户" excludeMutingUsers: "排除屏蔽用户"
excludeInactiveUsers: "排除不活跃用户" excludeInactiveUsers: "排除不活跃用户"
@ -2578,7 +2597,7 @@ _play:
editThisPage: "编辑此 Play" editThisPage: "编辑此 Play"
viewSource: "查看源代码" viewSource: "查看源代码"
my: "我的 Play" my: "我的 Play"
liked: "点赞的 Play" liked: "喜欢的 Play"
featured: "热门" featured: "热门"
title: "标题" title: "标题"
script: "脚本" script: "脚本"
@ -2595,7 +2614,7 @@ _pages:
editThisPage: "编辑此页面" editThisPage: "编辑此页面"
viewSource: "查看源代码" viewSource: "查看源代码"
viewPage: "查看页面" viewPage: "查看页面"
like: "" like: "喜欢"
unlike: "取消喜欢" unlike: "取消喜欢"
my: "我的页面" my: "我的页面"
liked: "喜欢的页面" liked: "喜欢的页面"
@ -2643,10 +2662,12 @@ _notification:
youGotReply: "来自{name}的回复" youGotReply: "来自{name}的回复"
youGotQuote: "来自{name}的引用" youGotQuote: "来自{name}的引用"
youRenoted: "来自{name}的转发" youRenoted: "来自{name}的转发"
youWereFollowed: "关注了你" youWereFollowed: "关注了你"
youReceivedFollowRequest: "您有新的关注请求" youReceivedFollowRequest: "您有新的关注请求"
yourFollowRequestAccepted: "您的关注请求已通过" yourFollowRequestAccepted: "您的关注请求已通过"
pollEnded: "问卷调查结果已生成。" pollEnded: "问卷调查结果已生成。"
scheduledNotePosted: "定时帖子已发布"
scheduledNotePostFailed: "定时帖子发布失败"
newNote: "新的帖子" newNote: "新的帖子"
unreadAntennaNote: "天线 {name}" unreadAntennaNote: "天线 {name}"
roleAssigned: "授予的角色" roleAssigned: "授予的角色"
@ -2726,7 +2747,7 @@ _deck:
mentions: "提及" mentions: "提及"
direct: "指定用户" direct: "指定用户"
roleTimeline: "角色时间线" roleTimeline: "角色时间线"
chat: "聊天" chat: "私信"
_dialog: _dialog:
charactersExceeded: "已经超过了最大字符数! 当前字符数 {current} / 限制字符数 {max}" charactersExceeded: "已经超过了最大字符数! 当前字符数 {current} / 限制字符数 {max}"
charactersBelow: "低于最小字符数!当前字符数 {current} / 限制字符数 {min}" charactersBelow: "低于最小字符数!当前字符数 {current} / 限制字符数 {min}"
@ -2822,7 +2843,7 @@ _moderationLogTypes:
deleteAccount: "删除了账户" deleteAccount: "删除了账户"
deletePage: "删除了页面" deletePage: "删除了页面"
deleteFlash: "删除了 Play" deleteFlash: "删除了 Play"
deleteGalleryPost: "删除了图库稿件" deleteGalleryPost: "删除相册内容"
deleteChatRoom: "删除聊天室" deleteChatRoom: "删除聊天室"
updateProxyAccountDescription: "更新代理账户的简介" updateProxyAccountDescription: "更新代理账户的简介"
_fileViewer: _fileViewer:
@ -3078,7 +3099,7 @@ _bootErrors:
serverError: "请稍等片刻再重试。若问题仍无法解决,请将以下 Error ID 一起发送给管理员。" serverError: "请稍等片刻再重试。若问题仍无法解决,请将以下 Error ID 一起发送给管理员。"
solution: "以下方法或许可以解决问题:" solution: "以下方法或许可以解决问题:"
solution1: "将浏览器及操作系统更新到最新版本" solution1: "将浏览器及操作系统更新到最新版本"
solution2: "禁用广告屏蔽插件" solution2: "禁用广告拦截插件"
solution3: "清除浏览器缓存" solution3: "清除浏览器缓存"
solution4: "Tor Browser将 dom.webaudio.enabled 设定为 true" solution4: "Tor Browser将 dom.webaudio.enabled 设定为 true"
otherOption: "其它选项" otherOption: "其它选项"
@ -3201,6 +3222,8 @@ _imageEffector:
mirror: "镜像" mirror: "镜像"
invert: "反转颜色" invert: "反转颜色"
grayscale: "黑白" grayscale: "黑白"
blur: "模糊"
pixelate: "马赛克"
colorAdjust: "色彩校正" colorAdjust: "色彩校正"
colorClamp: "颜色限制" colorClamp: "颜色限制"
colorClampAdvanced: "颜色限制(高级)" colorClampAdvanced: "颜色限制(高级)"
@ -3212,11 +3235,13 @@ _imageEffector:
checker: "检查" checker: "检查"
blockNoise: "块状噪点" blockNoise: "块状噪点"
tearing: "撕裂" tearing: "撕裂"
fillSquare: "填充(四角)" fill: "填充"
_fxProps: _fxProps:
angle: "角度" angle: "角度"
scale: "大小" scale: "大小"
size: "大小" size: "大小"
radius: "半径"
samples: "采样数"
offset: "位置" offset: "位置"
color: "颜色" color: "颜色"
opacity: "不透明度" opacity: "不透明度"
@ -3246,6 +3271,7 @@ _imageEffector:
zoomLinesThreshold: "集中线宽度" zoomLinesThreshold: "集中线宽度"
zoomLinesMaskSize: "中心直径" zoomLinesMaskSize: "中心直径"
zoomLinesBlack: "变成黑色" zoomLinesBlack: "变成黑色"
circle: "圆形"
drafts: "草稿" drafts: "草稿"
_drafts: _drafts:
select: "选择草稿" select: "选择草稿"
@ -3261,6 +3287,9 @@ _drafts:
restoreFromDraft: "从草稿恢复" restoreFromDraft: "从草稿恢复"
restore: "恢复" restore: "恢复"
listDrafts: "草稿一览" listDrafts: "草稿一览"
schedule: "定时发布"
listScheduledNotes: "定时发布列表"
cancelSchedule: "取消定时"
qr: "二维码" qr: "二维码"
_qr: _qr:
showTabTitle: "显示" showTabTitle: "显示"

View File

@ -253,6 +253,7 @@ noteDeleteConfirm: "確定刪除此貼文嗎?"
pinLimitExceeded: "不能置頂更多貼文了" pinLimitExceeded: "不能置頂更多貼文了"
done: "完成" done: "完成"
processing: "處理中" processing: "處理中"
preprocessing: "準備中"
preview: "預覽" preview: "預覽"
default: "預設" default: "預設"
defaultValueIs: "預設值:{value}" defaultValueIs: "預設值:{value}"
@ -1316,6 +1317,7 @@ acknowledgeNotesAndEnable: "了解注意事項後再開啟。"
federationSpecified: "此伺服器以白名單聯邦的方式運作。除了管理員指定的伺服器外,它無法與其他伺服器互動。" federationSpecified: "此伺服器以白名單聯邦的方式運作。除了管理員指定的伺服器外,它無法與其他伺服器互動。"
federationDisabled: "此伺服器未開啟站台聯邦。無法與其他伺服器上的使用者互動。" federationDisabled: "此伺服器未開啟站台聯邦。無法與其他伺服器上的使用者互動。"
draft: "草稿\n" draft: "草稿\n"
draftsAndScheduledNotes: "草稿與排定發布"
confirmOnReact: "在做出反應前先確認" confirmOnReact: "在做出反應前先確認"
reactAreYouSure: "用「 {emoji} 」反應嗎?" reactAreYouSure: "用「 {emoji} 」反應嗎?"
markAsSensitiveConfirm: "要將這個媒體設定為敏感嗎?" markAsSensitiveConfirm: "要將這個媒體設定為敏感嗎?"
@ -1372,6 +1374,8 @@ redisplayAllTips: "重新顯示所有「提示與技巧」"
hideAllTips: "隱藏所有「提示與技巧」" hideAllTips: "隱藏所有「提示與技巧」"
defaultImageCompressionLevel: "預設的影像壓縮程度" defaultImageCompressionLevel: "預設的影像壓縮程度"
defaultImageCompressionLevel_description: "低的話可以保留畫質,但是會增加檔案的大小。<br>高的話可以減少檔案大小,但是會降低畫質。" defaultImageCompressionLevel_description: "低的話可以保留畫質,但是會增加檔案的大小。<br>高的話可以減少檔案大小,但是會降低畫質。"
defaultCompressionLevel: "預設的壓縮程度"
defaultCompressionLevel_description: "低的話可以保留品質,但是會增加檔案的大小。<br>高的話可以減少檔案大小,但是會降低品質。"
inMinutes: "分鐘" inMinutes: "分鐘"
inDays: "日" inDays: "日"
safeModeEnabled: "啟用安全模式" safeModeEnabled: "啟用安全模式"
@ -1380,6 +1384,20 @@ customCssIsDisabledBecauseSafeMode: "由於啟用安全模式,所有的客製
themeIsDefaultBecauseSafeMode: "在安全模式啟用期間將使用預設主題。關閉安全模式後會恢復原本的設定。" themeIsDefaultBecauseSafeMode: "在安全模式啟用期間將使用預設主題。關閉安全模式後會恢復原本的設定。"
thankYouForTestingBeta: "感謝您協助驗證 beta 版!" thankYouForTestingBeta: "感謝您協助驗證 beta 版!"
createUserSpecifiedNote: "建立使用者指定的筆記" createUserSpecifiedNote: "建立使用者指定的筆記"
schedulePost: "排定發布"
scheduleToPostOnX: "排定在 {x} 發布"
scheduledToPostOnX: "已排定在 {x} 發布貼文"
schedule: "排定"
scheduled: "排定"
_compression:
_quality:
high: "高品質"
medium: "中品質"
low: "低品質"
_size:
large: "大"
medium: "中"
small: "小"
_order: _order:
newest: "最新的在前" newest: "最新的在前"
oldest: "最舊的在前" oldest: "最舊的在前"
@ -2022,6 +2040,7 @@ _role:
uploadableFileTypes_caption: "請指定 MIME 類型。可以用換行區隔多個類型,也可以使用星號(*作為萬用字元進行指定。例如image/*\n" uploadableFileTypes_caption: "請指定 MIME 類型。可以用換行區隔多個類型,也可以使用星號(*作為萬用字元進行指定。例如image/*\n"
uploadableFileTypes_caption2: "有些檔案可能無法判斷其類型。若要允許這類檔案,請在指定中加入 {x}。" uploadableFileTypes_caption2: "有些檔案可能無法判斷其類型。若要允許這類檔案,請在指定中加入 {x}。"
noteDraftLimit: "伺服器端可建立的貼文草稿數量上限\n" noteDraftLimit: "伺服器端可建立的貼文草稿數量上限\n"
scheduledNoteLimit: "同時建立的排定發布數量"
watermarkAvailable: "浮水印功能是否可用" watermarkAvailable: "浮水印功能是否可用"
_condition: _condition:
roleAssignedTo: "手動指派角色完成" roleAssignedTo: "手動指派角色完成"
@ -2647,6 +2666,8 @@ _notification:
youReceivedFollowRequest: "您有新的追隨請求" youReceivedFollowRequest: "您有新的追隨請求"
yourFollowRequestAccepted: "您的追隨請求已被核准" yourFollowRequestAccepted: "您的追隨請求已被核准"
pollEnded: "問卷調查已產生結果" pollEnded: "問卷調查已產生結果"
scheduledNotePosted: "已排定發布貼文"
scheduledNotePostFailed: "排定發布貼文失敗了"
newNote: "新的貼文" newNote: "新的貼文"
unreadAntennaNote: "天線 {name}" unreadAntennaNote: "天線 {name}"
roleAssigned: "已授予角色" roleAssigned: "已授予角色"
@ -3172,7 +3193,9 @@ _watermarkEditor:
opacity: "透明度" opacity: "透明度"
scale: "大小" scale: "大小"
text: "文字" text: "文字"
qr: "二維條碼"
position: "位置" position: "位置"
margin: "邊界"
type: "類型" type: "類型"
image: "圖片" image: "圖片"
advanced: "進階" advanced: "進階"
@ -3187,6 +3210,7 @@ _watermarkEditor:
polkadotSubDotOpacity: "子圓點的不透明度" polkadotSubDotOpacity: "子圓點的不透明度"
polkadotSubDotRadius: "子圓點的尺寸" polkadotSubDotRadius: "子圓點的尺寸"
polkadotSubDotDivisions: "子圓點的數量" polkadotSubDotDivisions: "子圓點的數量"
leaveBlankToAccountUrl: "若留空則使用帳戶的 URL"
_imageEffector: _imageEffector:
title: "特效" title: "特效"
addEffect: "新增特效" addEffect: "新增特效"
@ -3198,6 +3222,8 @@ _imageEffector:
mirror: "鏡像" mirror: "鏡像"
invert: "反轉色彩" invert: "反轉色彩"
grayscale: "黑白" grayscale: "黑白"
blur: "模糊"
pixelate: "馬賽克"
colorAdjust: "色彩校正" colorAdjust: "色彩校正"
colorClamp: "壓縮色彩" colorClamp: "壓縮色彩"
colorClampAdvanced: "壓縮色彩(進階)" colorClampAdvanced: "壓縮色彩(進階)"
@ -3209,11 +3235,13 @@ _imageEffector:
checker: "棋盤格" checker: "棋盤格"
blockNoise: "阻擋雜訊" blockNoise: "阻擋雜訊"
tearing: "撕裂" tearing: "撕裂"
fillSquare: "填充(方形)" fill: "填充"
_fxProps: _fxProps:
angle: "角度" angle: "角度"
scale: "大小" scale: "大小"
size: "大小" size: "大小"
radius: "半徑"
samples: "取樣數"
offset: "位置" offset: "位置"
color: "顏色" color: "顏色"
opacity: "透明度" opacity: "透明度"
@ -3243,6 +3271,7 @@ _imageEffector:
zoomLinesThreshold: "集中線的寬度" zoomLinesThreshold: "集中線的寬度"
zoomLinesMaskSize: "中心直徑" zoomLinesMaskSize: "中心直徑"
zoomLinesBlack: "變成黑色" zoomLinesBlack: "變成黑色"
circle: "圓形"
drafts: "草稿\n" drafts: "草稿\n"
_drafts: _drafts:
select: "選擇草槁" select: "選擇草槁"
@ -3258,6 +3287,22 @@ _drafts:
restoreFromDraft: "從草稿復原\n" restoreFromDraft: "從草稿復原\n"
restore: "還原" restore: "還原"
listDrafts: "草稿清單" listDrafts: "草稿清單"
schedule: "排定發布"
listScheduledNotes: "排定發布列表"
cancelSchedule: "解除排定"
qr: "二維條碼"
_qr: _qr:
showTabTitle: "檢視" showTabTitle: "檢視"
readTabTitle: "讀取"
shareTitle: "{name} {acct}"
shareText: "請在聯邦宇宙追隨我吧!"
chooseCamera: "選擇相機"
cannotToggleFlash: "無法切換閃光燈"
turnOnFlash: "開啟閃光燈"
turnOffFlash: "關閉閃光燈"
startQr: "啟動條碼掃描器"
stopQr: "停止條碼掃描器"
noQrCodeFound: "找不到 QR code"
scanFile: "掃描在裝置上的影像"
raw: "文字" raw: "文字"
mfm: "MFM"

View File

@ -1,12 +1,12 @@
{ {
"name": "misskey", "name": "misskey",
"version": "2025.9.0", "version": "2025.10.0-beta.0",
"codename": "nasubi", "codename": "nasubi",
"repository": { "repository": {
"type": "git", "type": "git",
"url": "https://github.com/misskey-dev/misskey.git" "url": "https://github.com/misskey-dev/misskey.git"
}, },
"packageManager": "pnpm@10.15.1", "packageManager": "pnpm@10.16.0",
"workspaces": [ "workspaces": [
"packages/frontend-shared", "packages/frontend-shared",
"packages/frontend", "packages/frontend",
@ -76,7 +76,7 @@
"eslint": "9.35.0", "eslint": "9.35.0",
"globals": "16.3.0", "globals": "16.3.0",
"ncp": "2.0.0", "ncp": "2.0.0",
"pnpm": "10.15.1", "pnpm": "10.16.0",
"start-server-and-test": "2.1.0" "start-server-and-test": "2.1.0"
}, },
"optionalDependencies": { "optionalDependencies": {

View File

@ -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"`);
}
}

View File

@ -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"`);
}
}

View File

@ -39,17 +39,17 @@
}, },
"optionalDependencies": { "optionalDependencies": {
"@swc/core-android-arm64": "1.3.11", "@swc/core-android-arm64": "1.3.11",
"@swc/core-darwin-arm64": "1.13.5", "@swc/core-darwin-arm64": "1.13.19",
"@swc/core-darwin-x64": "1.13.5", "@swc/core-darwin-x64": "1.13.19",
"@swc/core-freebsd-x64": "1.3.11", "@swc/core-freebsd-x64": "1.3.11",
"@swc/core-linux-arm-gnueabihf": "1.13.5", "@swc/core-linux-arm-gnueabihf": "1.13.19",
"@swc/core-linux-arm64-gnu": "1.13.5", "@swc/core-linux-arm64-gnu": "1.13.19",
"@swc/core-linux-arm64-musl": "1.13.5", "@swc/core-linux-arm64-musl": "1.13.19",
"@swc/core-linux-x64-gnu": "1.13.5", "@swc/core-linux-x64-gnu": "1.13.19",
"@swc/core-linux-x64-musl": "1.13.5", "@swc/core-linux-x64-musl": "1.13.19",
"@swc/core-win32-arm64-msvc": "1.13.5", "@swc/core-win32-arm64-msvc": "1.13.19",
"@swc/core-win32-ia32-msvc": "1.13.5", "@swc/core-win32-ia32-msvc": "1.13.19",
"@swc/core-win32-x64-msvc": "1.13.5", "@swc/core-win32-x64-msvc": "1.13.19",
"@tensorflow/tfjs": "4.22.0", "@tensorflow/tfjs": "4.22.0",
"@tensorflow/tfjs-node": "4.22.0", "@tensorflow/tfjs-node": "4.22.0",
"bufferutil": "4.0.9", "bufferutil": "4.0.9",
@ -69,8 +69,8 @@
"utf-8-validate": "6.0.5" "utf-8-validate": "6.0.5"
}, },
"dependencies": { "dependencies": {
"@aws-sdk/client-s3": "3.883.0", "@aws-sdk/client-s3": "3.896.0",
"@aws-sdk/lib-storage": "3.883.0", "@aws-sdk/lib-storage": "3.895.0",
"@discordapp/twemoji": "16.0.1", "@discordapp/twemoji": "16.0.1",
"@fastify/accepts": "5.0.2", "@fastify/accepts": "5.0.2",
"@fastify/cookie": "11.0.2", "@fastify/cookie": "11.0.2",
@ -82,7 +82,7 @@
"@fastify/view": "10.0.2", "@fastify/view": "10.0.2",
"@misskey-dev/sharp-read-bmp": "1.2.0", "@misskey-dev/sharp-read-bmp": "1.2.0",
"@misskey-dev/summaly": "5.2.3", "@misskey-dev/summaly": "5.2.3",
"@napi-rs/canvas": "0.1.79", "@napi-rs/canvas": "0.1.80",
"@nestjs/common": "11.1.6", "@nestjs/common": "11.1.6",
"@nestjs/core": "11.1.6", "@nestjs/core": "11.1.6",
"@nestjs/testing": "11.1.6", "@nestjs/testing": "11.1.6",
@ -103,29 +103,29 @@
"bcryptjs": "2.4.3", "bcryptjs": "2.4.3",
"blurhash": "2.0.5", "blurhash": "2.0.5",
"body-parser": "1.20.3", "body-parser": "1.20.3",
"bullmq": "5.58.5", "bullmq": "5.58.8",
"cacheable-lookup": "7.0.0", "cacheable-lookup": "7.0.0",
"cbor": "9.0.2", "cbor": "9.0.2",
"chalk": "5.6.0", "chalk": "5.6.2",
"chalk-template": "1.1.0", "chalk-template": "1.1.2",
"chokidar": "4.0.3", "chokidar": "4.0.3",
"cli-highlight": "2.1.11", "cli-highlight": "2.1.11",
"color-convert": "2.0.1", "color-convert": "2.0.1",
"content-disposition": "0.5.4", "content-disposition": "0.5.4",
"date-fns": "2.30.0", "date-fns": "2.30.0",
"deep-email-validator": "0.1.21", "deep-email-validator": "0.1.21",
"fastify": "5.6.0", "fastify": "5.6.1",
"fastify-raw-body": "5.0.0", "fastify-raw-body": "5.0.0",
"feed": "4.2.2", "feed": "4.2.2",
"file-type": "19.6.0", "file-type": "19.6.0",
"fluent-ffmpeg": "2.1.3", "fluent-ffmpeg": "2.1.3",
"form-data": "4.0.4", "form-data": "4.0.4",
"got": "14.4.8", "got": "14.4.9",
"happy-dom": "16.8.1", "happy-dom": "16.8.1",
"hpagent": "1.2.0", "hpagent": "1.2.0",
"htmlescape": "1.1.1", "htmlescape": "1.1.1",
"http-link-header": "1.1.3", "http-link-header": "1.1.3",
"ioredis": "5.7.0", "ioredis": "5.8.0",
"ip-cidr": "4.0.2", "ip-cidr": "4.0.2",
"ipaddr.js": "2.2.0", "ipaddr.js": "2.2.0",
"is-svg": "5.1.0", "is-svg": "5.1.0",
@ -135,14 +135,14 @@
"jsonld": "8.3.3", "jsonld": "8.3.3",
"jsrsasign": "11.1.0", "jsrsasign": "11.1.0",
"juice": "11.0.1", "juice": "11.0.1",
"meilisearch": "0.52.0", "meilisearch": "0.53.0",
"mfm-js": "0.25.0", "mfm-js": "0.25.0",
"microformats-parser": "2.0.4", "microformats-parser": "2.0.4",
"mime-types": "2.1.35", "mime-types": "2.1.35",
"misskey-js": "workspace:*", "misskey-js": "workspace:*",
"misskey-reversi": "workspace:*", "misskey-reversi": "workspace:*",
"ms": "3.0.0-canary.202508261828", "ms": "3.0.0-canary.202508261828",
"nanoid": "5.1.5", "nanoid": "5.1.6",
"nested-property": "4.0.0", "nested-property": "4.0.0",
"node-fetch": "3.3.2", "node-fetch": "3.3.2",
"nodemailer": "6.10.1", "nodemailer": "6.10.1",
@ -175,12 +175,12 @@
"slacc": "0.0.10", "slacc": "0.0.10",
"strict-event-emitter-types": "2.0.0", "strict-event-emitter-types": "2.0.0",
"stringz": "2.1.0", "stringz": "2.1.0",
"systeminformation": "5.27.8", "systeminformation": "5.27.10",
"tinycolor2": "1.6.0", "tinycolor2": "1.6.0",
"tmp": "0.2.5", "tmp": "0.2.5",
"tsc-alias": "1.8.16", "tsc-alias": "1.8.16",
"tsconfig-paths": "4.2.0", "tsconfig-paths": "4.2.0",
"typeorm": "0.3.26", "typeorm": "0.3.27",
"typescript": "5.9.2", "typescript": "5.9.2",
"ulid": "2.4.0", "ulid": "2.4.0",
"vary": "1.1.2", "vary": "1.1.2",
@ -210,7 +210,7 @@
"@types/jsrsasign": "10.5.15", "@types/jsrsasign": "10.5.15",
"@types/mime-types": "2.1.4", "@types/mime-types": "2.1.4",
"@types/ms": "0.7.34", "@types/ms": "0.7.34",
"@types/node": "22.18.1", "@types/node": "22.18.6",
"@types/nodemailer": "6.4.19", "@types/nodemailer": "6.4.19",
"@types/oauth": "0.9.6", "@types/oauth": "0.9.6",
"@types/oauth2orize": "1.11.5", "@types/oauth2orize": "1.11.5",
@ -231,8 +231,8 @@
"@types/vary": "1.1.3", "@types/vary": "1.1.3",
"@types/web-push": "3.6.4", "@types/web-push": "3.6.4",
"@types/ws": "8.18.1", "@types/ws": "8.18.1",
"@typescript-eslint/eslint-plugin": "8.42.0", "@typescript-eslint/eslint-plugin": "8.44.1",
"@typescript-eslint/parser": "8.42.0", "@typescript-eslint/parser": "8.44.1",
"aws-sdk-client-mock": "4.1.0", "aws-sdk-client-mock": "4.1.0",
"cross-env": "7.0.3", "cross-env": "7.0.3",
"eslint-plugin-import": "2.32.0", "eslint-plugin-import": "2.32.0",

View File

@ -7,6 +7,7 @@ import * as fs from 'node:fs';
import { fileURLToPath } from 'node:url'; import { fileURLToPath } from 'node:url';
import { dirname, resolve } from 'node:path'; import { dirname, resolve } from 'node:path';
import * as yaml from 'js-yaml'; import * as yaml from 'js-yaml';
import { type FastifyServerOptions } from 'fastify';
import type * as Sentry from '@sentry/node'; import type * as Sentry from '@sentry/node';
import type * as SentryVue from '@sentry/vue'; import type * as SentryVue from '@sentry/vue';
import type { RedisOptions } from 'ioredis'; import type { RedisOptions } from 'ioredis';
@ -27,6 +28,7 @@ type Source = {
url?: string; url?: string;
port?: number; port?: number;
socket?: string; socket?: string;
trustProxy?: FastifyServerOptions['trustProxy'];
chmodSocket?: string; chmodSocket?: string;
disableHsts?: boolean; disableHsts?: boolean;
db: { db: {
@ -118,6 +120,7 @@ export type Config = {
url: string; url: string;
port: number; port: number;
socket: string | undefined; socket: string | undefined;
trustProxy: FastifyServerOptions['trustProxy'];
chmodSocket: string | undefined; chmodSocket: string | undefined;
disableHsts: boolean | undefined; disableHsts: boolean | undefined;
db: { db: {
@ -266,6 +269,7 @@ export function loadConfig(): Config {
url: url.origin, url: url.origin,
port: config.port ?? parseInt(process.env.PORT ?? '', 10), port: config.port ?? parseInt(process.env.PORT ?? '', 10),
socket: config.socket, socket: config.socket,
trustProxy: config.trustProxy,
chmodSocket: config.chmodSocket, chmodSocket: config.chmodSocket,
disableHsts: config.disableHsts, disableHsts: config.disableHsts,
host, host,

View File

@ -37,17 +37,23 @@ class HttpRequestServiceAgent extends http.Agent {
@bindThis @bindThis
public createConnection(options: http.ClientRequestArgs, callback?: (err: Error | null, stream: stream.Duplex) => void): stream.Duplex { public createConnection(options: http.ClientRequestArgs, callback?: (err: Error | null, stream: stream.Duplex) => void): stream.Duplex {
const socket = super.createConnection(options, callback) const socket = super.createConnection(options, callback);
.on('connect', () => {
if (socket instanceof net.Socket && process.env.NODE_ENV === 'production') { if (socket == null) {
const address = socket.remoteAddress; throw new Error('Failed to create socket');
if (address && ipaddr.isValid(address)) { }
if (this.isPrivateIp(address)) {
socket.destroy(new Error(`Blocked address: ${address}`)); 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; return socket;
} }
@ -76,17 +82,23 @@ class HttpsRequestServiceAgent extends https.Agent {
@bindThis @bindThis
public createConnection(options: http.ClientRequestArgs, callback?: (err: Error | null, stream: stream.Duplex) => void): stream.Duplex { public createConnection(options: http.ClientRequestArgs, callback?: (err: Error | null, stream: stream.Duplex) => void): stream.Duplex {
const socket = super.createConnection(options, callback) const socket = super.createConnection(options, callback);
.on('connect', () => {
if (socket instanceof net.Socket && process.env.NODE_ENV === 'production') { if (socket == null) {
const address = socket.remoteAddress; throw new Error('Failed to create socket');
if (address && ipaddr.isValid(address)) { }
if (this.isPrivateIp(address)) {
socket.destroy(new Error(`Blocked address: ${address}`)); 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; return socket;
} }

View File

@ -13,7 +13,7 @@ import { extractCustomEmojisFromMfm } from '@/misc/extract-custom-emojis-from-mf
import { extractHashtags } from '@/misc/extract-hashtags.js'; import { extractHashtags } from '@/misc/extract-hashtags.js';
import type { IMentionedRemoteUsers } from '@/models/Note.js'; import type { IMentionedRemoteUsers } from '@/models/Note.js';
import { MiNote } 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 { MiDriveFile } from '@/models/DriveFile.js';
import type { MiApp } from '@/models/App.js'; import type { MiApp } from '@/models/App.js';
import { concat } from '@/misc/prelude/array.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 { IdentifiableError } from '@/misc/identifiable-error.js';
import { CollapsedQueue } from '@/misc/collapsed-queue.js'; import { CollapsedQueue } from '@/misc/collapsed-queue.js';
import { CacheService } from '@/core/CacheService.js'; import { CacheService } from '@/core/CacheService.js';
import { isQuote, isRenote } from '@/misc/is-renote.js';
type NotificationType = 'reply' | 'renote' | 'quote' | 'mention'; type NotificationType = 'reply' | 'renote' | 'quote' | 'mention';
@ -192,6 +193,12 @@ export class NoteCreateService implements OnApplicationShutdown {
@Inject(DI.channelFollowingsRepository) @Inject(DI.channelFollowingsRepository)
private channelFollowingsRepository: ChannelFollowingsRepository, private channelFollowingsRepository: ChannelFollowingsRepository,
@Inject(DI.blockingsRepository)
private blockingsRepository: BlockingsRepository,
@Inject(DI.driveFilesRepository)
private driveFilesRepository: DriveFilesRepository,
private userEntityService: UserEntityService, private userEntityService: UserEntityService,
private noteEntityService: NoteEntityService, private noteEntityService: NoteEntityService,
private idService: IdService, 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); 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<MiNote> {
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 @bindThis
public async create(user: { public async create(user: {
id: MiUser['id']; id: MiUser['id'];

View File

@ -5,32 +5,18 @@
import { Inject, Injectable } from '@nestjs/common'; import { Inject, Injectable } from '@nestjs/common';
import { In } from 'typeorm'; import { In } from 'typeorm';
import type { noteVisibilities, noteReactionAcceptances } from '@/types.js';
import { DI } from '@/di-symbols.js'; import { DI } from '@/di-symbols.js';
import type { MiNoteDraft, NoteDraftsRepository, MiNote, MiDriveFile, MiChannel, UsersRepository, DriveFilesRepository, NotesRepository, BlockingsRepository, ChannelsRepository } from '@/models/_.js'; import type { MiNoteDraft, NoteDraftsRepository, MiNote, MiDriveFile, MiChannel, UsersRepository, DriveFilesRepository, NotesRepository, BlockingsRepository, ChannelsRepository } from '@/models/_.js';
import { bindThis } from '@/decorators.js'; import { bindThis } from '@/decorators.js';
import { RoleService } from '@/core/RoleService.js'; import { RoleService } from '@/core/RoleService.js';
import { IdService } from '@/core/IdService.js'; import { IdService } from '@/core/IdService.js';
import type { MiLocalUser, MiUser } from '@/models/User.js'; import type { MiLocalUser, MiUser } from '@/models/User.js';
import { IPoll } from '@/models/Poll.js';
import { IdentifiableError } from '@/misc/identifiable-error.js'; import { IdentifiableError } from '@/misc/identifiable-error.js';
import { isRenote, isQuote } from '@/misc/is-renote.js'; import { isRenote, isQuote } from '@/misc/is-renote.js';
import { NoteEntityService } from '@/core/entities/NoteEntityService.js'; import { NoteEntityService } from '@/core/entities/NoteEntityService.js';
import { QueueService } from '@/core/QueueService.js';
export type NoteDraftOptions = { export type NoteDraftOptions = Omit<MiNoteDraft, 'id' | 'userId' | 'user' | 'reply' | 'renote' | 'channel'>;
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;
};
@Injectable() @Injectable()
export class NoteDraftService { export class NoteDraftService {
@ -56,6 +42,7 @@ export class NoteDraftService {
private roleService: RoleService, private roleService: RoleService,
private idService: IdService, private idService: IdService,
private noteEntityService: NoteEntityService, private noteEntityService: NoteEntityService,
private queueService: QueueService,
) { ) {
} }
@ -72,36 +59,43 @@ export class NoteDraftService {
@bindThis @bindThis
public async create(me: MiLocalUser, data: NoteDraftOptions): Promise<MiNoteDraft> { public async create(me: MiLocalUser, data: NoteDraftOptions): Promise<MiNoteDraft> {
//#region check draft limit //#region check draft limit
const policies = await this.roleService.getUserPolicies(me.id);
const currentCount = await this.noteDraftsRepository.countBy({ const currentCount = await this.noteDraftsRepository.countBy({
userId: me.id, 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'); 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 //#endregion
if (data.poll) { await this.validate(me, data);
if (typeof data.poll.expiresAt === 'number') {
if (data.poll.expiresAt < Date.now()) { const draft = await this.noteDraftsRepository.insertOne({
throw new IdentifiableError('04da457d-b083-4055-9082-955525eda5a5', 'Cannot create expired poll'); ...data,
} id: this.idService.gen(),
} else if (typeof data.poll.expiredAfter === 'number') { userId: me.id,
data.poll.expiresAt = new Date(Date.now() + data.poll.expiredAfter); });
}
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; return draft;
} }
@bindThis @bindThis
public async update(me: MiLocalUser, draftId: MiNoteDraft['id'], data: NoteDraftOptions): Promise<MiNoteDraft> { public async update(me: MiLocalUser, draftId: MiNoteDraft['id'], data: Partial<NoteDraftOptions>): Promise<MiNoteDraft> {
const draft = await this.noteDraftsRepository.findOneBy({ const draft = await this.noteDraftsRepository.findOneBy({
id: draftId, id: draftId,
userId: me.id, userId: me.id,
@ -111,19 +105,36 @@ export class NoteDraftService {
throw new IdentifiableError('49cd6b9d-848e-41ee-b0b9-adaca711a6b1', 'No such note draft'); throw new IdentifiableError('49cd6b9d-848e-41ee-b0b9-adaca711a6b1', 'No such note draft');
} }
if (data.poll) { //#region check draft limit
if (typeof data.poll.expiresAt === 'number') { const policies = await this.roleService.getUserPolicies(me.id);
if (data.poll.expiresAt < Date.now()) {
throw new IdentifiableError('04da457d-b083-4055-9082-955525eda5a5', 'Cannot create expired poll'); if (!draft.isActuallyScheduled && data.isActuallyScheduled) {
} const currentScheduledCount = await this.noteDraftsRepository.countBy({
} else if (typeof data.poll.expiredAfter === 'number') { userId: me.id,
data.poll.expiresAt = new Date(Date.now() + data.poll.expiredAfter); 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 @bindThis
@ -138,6 +149,8 @@ export class NoteDraftService {
} }
await this.noteDraftsRepository.delete(draft.id); await this.noteDraftsRepository.delete(draft.id);
this.clearSchedule(draftId);
} }
@bindThis @bindThis
@ -154,27 +167,20 @@ export class NoteDraftService {
return draft; return draft;
} }
// 関連エンティティを取得し紐づける部分を共通化する
@bindThis @bindThis
public async checkAndSetDraftNoteOptions( public async validate(
me: MiLocalUser, me: MiLocalUser,
draft: MiNoteDraft, data: Partial<NoteDraftOptions>,
data: NoteDraftOptions, ): Promise<void> {
): Promise<MiNoteDraft> { if (data.pollExpiresAt != null) {
data.visibility ??= 'public'; if (data.pollExpiresAt.getTime() < Date.now()) {
data.localOnly ??= false; throw new IdentifiableError('04da457d-b083-4055-9082-955525eda5a5', 'Cannot create expired poll');
if (data.reactionAcceptance === undefined) data.reactionAcceptance = null; }
if (data.channelId != null) {
data.visibility = 'public';
data.visibleUserIds = [];
data.localOnly = true;
} }
let appliedDraft = draft;
//#region visibleUsers //#region visibleUsers
let visibleUsers: MiUser[] = []; let visibleUsers: MiUser[] = [];
if (data.visibleUserIds != null) { if (data.visibleUserIds != null && data.visibleUserIds.length > 0) {
visibleUsers = await this.usersRepository.findBy({ visibleUsers = await this.usersRepository.findBy({
id: In(data.visibleUserIds), id: In(data.visibleUserIds),
}); });
@ -184,7 +190,7 @@ export class NoteDraftService {
//#region files //#region files
let files: MiDriveFile[] = []; let files: MiDriveFile[] = [];
const fileIds = data.fileIds ?? null; const fileIds = data.fileIds ?? null;
if (fileIds != null) { if (fileIds != null && fileIds.length > 0) {
files = await this.driveFilesRepository.createQueryBuilder('file') files = await this.driveFilesRepository.createQueryBuilder('file')
.where('file.userId = :userId AND file.id IN (:...fileIds)', { .where('file.userId = :userId AND file.id IN (:...fileIds)', {
userId: me.id, userId: me.id,
@ -288,27 +294,37 @@ export class NoteDraftService {
} }
} }
//#endregion //#endregion
}
appliedDraft = { @bindThis
...appliedDraft, public async schedule(draft: MiNoteDraft): Promise<void> {
visibility: data.visibility, if (!draft.isActuallyScheduled) return;
cw: data.cw ?? null, if (draft.scheduledAt == null) return;
fileIds: fileIds ?? [], if (draft.scheduledAt.getTime() <= Date.now()) return;
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;
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<void> {
const jobs = await this.queueService.postScheduledNoteQueue.getJobs(['delayed', 'waiting', 'active']);
for (const job of jobs) {
if (job.data.noteDraftId === draftId) {
await job.remove();
}
}
} }
} }

View File

@ -16,11 +16,13 @@ import {
RelationshipJobData, RelationshipJobData,
UserWebhookDeliverJobData, UserWebhookDeliverJobData,
SystemWebhookDeliverJobData, SystemWebhookDeliverJobData,
PostScheduledNoteJobData,
} from '../queue/types.js'; } from '../queue/types.js';
import type { Provider } from '@nestjs/common'; import type { Provider } from '@nestjs/common';
export type SystemQueue = Bull.Queue<Record<string, unknown>>; export type SystemQueue = Bull.Queue<Record<string, unknown>>;
export type EndedPollNotificationQueue = Bull.Queue<EndedPollNotificationJobData>; export type EndedPollNotificationQueue = Bull.Queue<EndedPollNotificationJobData>;
export type PostScheduledNoteQueue = Bull.Queue<PostScheduledNoteJobData>;
export type DeliverQueue = Bull.Queue<DeliverJobData>; export type DeliverQueue = Bull.Queue<DeliverJobData>;
export type InboxQueue = Bull.Queue<InboxJobData>; export type InboxQueue = Bull.Queue<InboxJobData>;
export type DbQueue = Bull.Queue; export type DbQueue = Bull.Queue;
@ -41,6 +43,12 @@ const $endedPollNotification: Provider = {
inject: [DI.config], 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 = { const $deliver: Provider = {
provide: 'queue:deliver', provide: 'queue:deliver',
useFactory: (config: Config) => new Bull.Queue(QUEUE.DELIVER, baseQueueOptions(config, QUEUE.DELIVER)), useFactory: (config: Config) => new Bull.Queue(QUEUE.DELIVER, baseQueueOptions(config, QUEUE.DELIVER)),
@ -89,6 +97,7 @@ const $systemWebhookDeliver: Provider = {
providers: [ providers: [
$system, $system,
$endedPollNotification, $endedPollNotification,
$postScheduledNote,
$deliver, $deliver,
$inbox, $inbox,
$db, $db,
@ -100,6 +109,7 @@ const $systemWebhookDeliver: Provider = {
exports: [ exports: [
$system, $system,
$endedPollNotification, $endedPollNotification,
$postScheduledNote,
$deliver, $deliver,
$inbox, $inbox,
$db, $db,
@ -113,6 +123,7 @@ export class QueueModule implements OnApplicationShutdown {
constructor( constructor(
@Inject('queue:system') public systemQueue: SystemQueue, @Inject('queue:system') public systemQueue: SystemQueue,
@Inject('queue:endedPollNotification') public endedPollNotificationQueue: EndedPollNotificationQueue, @Inject('queue:endedPollNotification') public endedPollNotificationQueue: EndedPollNotificationQueue,
@Inject('queue:postScheduledNote') public postScheduledNoteQueue: PostScheduledNoteQueue,
@Inject('queue:deliver') public deliverQueue: DeliverQueue, @Inject('queue:deliver') public deliverQueue: DeliverQueue,
@Inject('queue:inbox') public inboxQueue: InboxQueue, @Inject('queue:inbox') public inboxQueue: InboxQueue,
@Inject('queue:db') public dbQueue: DbQueue, @Inject('queue:db') public dbQueue: DbQueue,
@ -129,6 +140,7 @@ export class QueueModule implements OnApplicationShutdown {
await Promise.all([ await Promise.all([
this.systemQueue.close(), this.systemQueue.close(),
this.endedPollNotificationQueue.close(), this.endedPollNotificationQueue.close(),
this.postScheduledNoteQueue.close(),
this.deliverQueue.close(), this.deliverQueue.close(),
this.inboxQueue.close(), this.inboxQueue.close(),
this.dbQueue.close(), this.dbQueue.close(),

View File

@ -31,6 +31,7 @@ import type {
DbQueue, DbQueue,
DeliverQueue, DeliverQueue,
EndedPollNotificationQueue, EndedPollNotificationQueue,
PostScheduledNoteQueue,
InboxQueue, InboxQueue,
ObjectStorageQueue, ObjectStorageQueue,
RelationshipQueue, RelationshipQueue,
@ -44,6 +45,7 @@ import type * as Bull from 'bullmq';
export const QUEUE_TYPES = [ export const QUEUE_TYPES = [
'system', 'system',
'endedPollNotification', 'endedPollNotification',
'postScheduledNote',
'deliver', 'deliver',
'inbox', 'inbox',
'db', 'db',
@ -92,6 +94,7 @@ export class QueueService {
@Inject('queue:system') public systemQueue: SystemQueue, @Inject('queue:system') public systemQueue: SystemQueue,
@Inject('queue:endedPollNotification') public endedPollNotificationQueue: EndedPollNotificationQueue, @Inject('queue:endedPollNotification') public endedPollNotificationQueue: EndedPollNotificationQueue,
@Inject('queue:postScheduledNote') public postScheduledNoteQueue: PostScheduledNoteQueue,
@Inject('queue:deliver') public deliverQueue: DeliverQueue, @Inject('queue:deliver') public deliverQueue: DeliverQueue,
@Inject('queue:inbox') public inboxQueue: InboxQueue, @Inject('queue:inbox') public inboxQueue: InboxQueue,
@Inject('queue:db') public dbQueue: DbQueue, @Inject('queue:db') public dbQueue: DbQueue,
@ -717,6 +720,7 @@ export class QueueService {
switch (type) { switch (type) {
case 'system': return this.systemQueue; case 'system': return this.systemQueue;
case 'endedPollNotification': return this.endedPollNotificationQueue; case 'endedPollNotification': return this.endedPollNotificationQueue;
case 'postScheduledNote': return this.postScheduledNoteQueue;
case 'deliver': return this.deliverQueue; case 'deliver': return this.deliverQueue;
case 'inbox': return this.inboxQueue; case 'inbox': return this.inboxQueue;
case 'db': return this.dbQueue; case 'db': return this.dbQueue;

View File

@ -69,6 +69,7 @@ export type RolePolicies = {
chatAvailability: 'available' | 'readonly' | 'unavailable'; chatAvailability: 'available' | 'readonly' | 'unavailable';
uploadableFileTypes: string[]; uploadableFileTypes: string[];
noteDraftLimit: number; noteDraftLimit: number;
scheduledNoteLimit: number;
watermarkAvailable: boolean; watermarkAvailable: boolean;
}; };
@ -101,20 +102,22 @@ export const DEFAULT_POLICIES: RolePolicies = {
userEachUserListsLimit: 50, userEachUserListsLimit: 50,
rateLimitFactor: 1, rateLimitFactor: 1,
avatarDecorationLimit: 1, avatarDecorationLimit: 1,
canImportAntennas: true, canImportAntennas: false,
canImportBlocking: true, canImportBlocking: false,
canImportFollowing: true, canImportFollowing: false,
canImportMuting: true, canImportMuting: false,
canImportUserLists: true, canImportUserLists: false,
chatAvailability: 'available', chatAvailability: 'available',
uploadableFileTypes: [ uploadableFileTypes: [
'text/plain', 'text/plain',
'text/csv',
'application/json', 'application/json',
'image/*', 'image/*',
'video/*', 'video/*',
'audio/*', 'audio/*',
], ],
noteDraftLimit: 10, noteDraftLimit: 10,
scheduledNoteLimit: 1,
watermarkAvailable: true, watermarkAvailable: true,
}; };
@ -439,6 +442,7 @@ export class RoleService implements OnApplicationShutdown, OnModuleInit {
return [...set]; return [...set];
}), }),
noteDraftLimit: calc('noteDraftLimit', vs => Math.max(...vs)), noteDraftLimit: calc('noteDraftLimit', vs => Math.max(...vs)),
scheduledNoteLimit: calc('scheduledNoteLimit', vs => Math.max(...vs)),
watermarkAvailable: calc('watermarkAvailable', vs => vs.some(v => v === true)), watermarkAvailable: calc('watermarkAvailable', vs => vs.some(v => v === true)),
}; };
} }

View File

@ -117,6 +117,7 @@ export class MetaEntityService {
ratio: ad.ratio, ratio: ad.ratio,
imageUrl: ad.imageUrl, imageUrl: ad.imageUrl,
dayOfWeek: ad.dayOfWeek, dayOfWeek: ad.dayOfWeek,
isSensitive: ad.isSensitive ? true : undefined,
})), })),
notesPerOneAd: instance.notesPerOneAd, notesPerOneAd: instance.notesPerOneAd,
enableEmail: instance.enableEmail, enableEmail: instance.enableEmail,

View File

@ -105,6 +105,8 @@ export class NoteDraftEntityService implements OnModuleInit {
const packed: Packed<'NoteDraft'> = await awaitAll({ const packed: Packed<'NoteDraft'> = await awaitAll({
id: noteDraft.id, id: noteDraft.id,
createdAt: this.idService.parse(noteDraft.id).date.toISOString(), createdAt: this.idService.parse(noteDraft.id).date.toISOString(),
scheduledAt: noteDraft.scheduledAt?.getTime() ?? null,
isActuallyScheduled: noteDraft.isActuallyScheduled,
userId: noteDraft.userId, userId: noteDraft.userId,
user: packedUsers?.get(noteDraft.userId) ?? this.userEntityService.pack(noteDraft.user ?? noteDraft.userId, me), user: packedUsers?.get(noteDraft.userId) ?? this.userEntityService.pack(noteDraft.user ?? noteDraft.userId, me),
text: text, text: text,
@ -112,13 +114,13 @@ export class NoteDraftEntityService implements OnModuleInit {
visibility: noteDraft.visibility, visibility: noteDraft.visibility,
localOnly: noteDraft.localOnly, localOnly: noteDraft.localOnly,
reactionAcceptance: noteDraft.reactionAcceptance, reactionAcceptance: noteDraft.reactionAcceptance,
visibleUserIds: noteDraft.visibility === 'specified' ? noteDraft.visibleUserIds : undefined, visibleUserIds: noteDraft.visibleUserIds,
hashtag: noteDraft.hashtag ?? undefined, hashtag: noteDraft.hashtag,
fileIds: noteDraft.fileIds, fileIds: noteDraft.fileIds,
files: packedFiles != null ? this.packAttachedFiles(noteDraft.fileIds, packedFiles) : this.driveFileEntityService.packManyByIds(noteDraft.fileIds), files: packedFiles != null ? this.packAttachedFiles(noteDraft.fileIds, packedFiles) : this.driveFileEntityService.packManyByIds(noteDraft.fileIds),
replyId: noteDraft.replyId, replyId: noteDraft.replyId,
renoteId: noteDraft.renoteId, renoteId: noteDraft.renoteId,
channelId: noteDraft.channelId ?? undefined, channelId: noteDraft.channelId,
channel: channel ? { channel: channel ? {
id: channel.id, id: channel.id,
name: channel.name, name: channel.name,
@ -127,6 +129,12 @@ export class NoteDraftEntityService implements OnModuleInit {
allowRenoteToExternal: channel.allowRenoteToExternal, allowRenoteToExternal: channel.allowRenoteToExternal,
userId: channel.userId, userId: channel.userId,
} : undefined, } : undefined,
poll: noteDraft.hasPoll ? {
choices: noteDraft.pollChoices,
multiple: noteDraft.pollMultiple,
expiresAt: noteDraft.pollExpiresAt?.toISOString(),
expiredAfter: noteDraft.pollExpiredAfter,
} : null,
...(opts.detail ? { ...(opts.detail ? {
reply: noteDraft.replyId ? nullIfEntityNotFound(this.noteEntityService.pack(noteDraft.replyId, me, { reply: noteDraft.replyId ? nullIfEntityNotFound(this.noteEntityService.pack(noteDraft.replyId, me, {
@ -138,13 +146,6 @@ export class NoteDraftEntityService implements OnModuleInit {
detail: true, detail: true,
skipHide: opts.skipHide, skipHide: opts.skipHide,
})) : undefined, })) : undefined,
poll: noteDraft.hasPoll ? {
choices: noteDraft.pollChoices,
multiple: noteDraft.pollMultiple,
expiresAt: noteDraft.pollExpiresAt?.toISOString(),
expiredAfter: noteDraft.pollExpiredAfter,
} : undefined,
} : {} ), } : {} ),
}); });

View File

@ -21,7 +21,18 @@ import type { OnModuleInit } from '@nestjs/common';
import type { UserEntityService } from './UserEntityService.js'; import type { UserEntityService } from './UserEntityService.js';
import type { NoteEntityService } from './NoteEntityService.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() @Injectable()
export class NotificationEntityService implements OnModuleInit { export class NotificationEntityService implements OnModuleInit {

View File

@ -54,10 +54,17 @@ export class MiAd {
length: 8192, nullable: false, length: 8192, nullable: false,
}) })
public memo: string; public memo: string;
@Column('integer', { @Column('integer', {
default: 0, nullable: false, default: 0, nullable: false,
}) })
public dayOfWeek: number; public dayOfWeek: number;
@Column('boolean', {
default: false,
})
public isSensitive: boolean;
constructor(data: Partial<MiAd>) { constructor(data: Partial<MiAd>) {
if (data == null) return; if (data == null) return;

View File

@ -126,7 +126,7 @@ export class MiNoteDraft {
@JoinColumn() @JoinColumn()
public channel: MiChannel | null; public channel: MiChannel | null;
// 以下、Pollについて追加 //#region 以下、Pollについて追加
@Column('boolean', { @Column('boolean', {
default: false, default: false,
@ -151,13 +151,15 @@ export class MiNoteDraft {
}) })
public pollExpiredAfter: number | null; public pollExpiredAfter: number | null;
// ここまで追加 //#endregion
constructor(data: Partial<MiNoteDraft>) { @Column('timestamp with time zone', {
if (data == null) return; nullable: true,
})
public scheduledAt: Date | null;
for (const [k, v] of Object.entries(data)) { @Column('boolean', {
(this as any)[k] = v; default: false,
} })
} public isActuallyScheduled: boolean;
} }

View File

@ -9,6 +9,7 @@ import { MiNote } from './Note.js';
import { MiAccessToken } from './AccessToken.js'; import { MiAccessToken } from './AccessToken.js';
import { MiRole } from './Role.js'; import { MiRole } from './Role.js';
import { MiDriveFile } from './DriveFile.js'; import { MiDriveFile } from './DriveFile.js';
import { MiNoteDraft } from './NoteDraft.js';
// misskey-js の notificationTypes と同期すべし // misskey-js の notificationTypes と同期すべし
export type MiNotification = { export type MiNotification = {
@ -60,6 +61,16 @@ export type MiNotification = {
createdAt: string; createdAt: string;
notifierId: MiUser['id']; notifierId: MiUser['id'];
noteId: MiNote['id']; noteId: MiNote['id'];
} | {
type: 'scheduledNotePosted';
id: string;
createdAt: string;
noteId: MiNote['id'];
} | {
type: 'scheduledNotePostFailed';
id: string;
createdAt: string;
noteDraftId: MiNoteDraft['id'];
} | { } | {
type: 'receiveFollowRequest'; type: 'receiveFollowRequest';
id: string; id: string;

View File

@ -60,5 +60,10 @@ export const packedAdSchema = {
optional: false, optional: false,
nullable: false, nullable: false,
}, },
isSensitive: {
type: 'boolean',
optional: false,
nullable: false,
},
}, },
} as const; } as const;

View File

@ -195,6 +195,10 @@ export const packedMetaLiteSchema = {
type: 'integer', type: 'integer',
optional: false, nullable: false, optional: false, nullable: false,
}, },
isSensitive: {
type: 'boolean',
optional: true, nullable: false,
},
}, },
}, },
}, },

View File

@ -23,7 +23,7 @@ export const packedNoteDraftSchema = {
}, },
cw: { cw: {
type: 'string', type: 'string',
optional: true, nullable: true, optional: false, nullable: true,
}, },
userId: { userId: {
type: 'string', type: 'string',
@ -37,27 +37,23 @@ export const packedNoteDraftSchema = {
}, },
replyId: { replyId: {
type: 'string', type: 'string',
optional: true, nullable: true, optional: false, nullable: true,
format: 'id', format: 'id',
example: 'xxxxxxxxxx',
}, },
renoteId: { renoteId: {
type: 'string', type: 'string',
optional: true, nullable: true, optional: false, nullable: true,
format: 'id', format: 'id',
example: 'xxxxxxxxxx',
}, },
reply: { reply: {
type: 'object', type: 'object',
optional: true, nullable: true, optional: true, nullable: true,
ref: 'Note', 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: { renote: {
type: 'object', type: 'object',
optional: true, nullable: true, optional: true, nullable: true,
ref: 'Note', 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: { visibility: {
type: 'string', type: 'string',
@ -66,7 +62,7 @@ export const packedNoteDraftSchema = {
}, },
visibleUserIds: { visibleUserIds: {
type: 'array', type: 'array',
optional: true, nullable: false, optional: false, nullable: false,
items: { items: {
type: 'string', type: 'string',
optional: false, nullable: false, optional: false, nullable: false,
@ -75,7 +71,7 @@ export const packedNoteDraftSchema = {
}, },
fileIds: { fileIds: {
type: 'array', type: 'array',
optional: true, nullable: false, optional: false, nullable: false,
items: { items: {
type: 'string', type: 'string',
optional: false, nullable: false, optional: false, nullable: false,
@ -93,11 +89,11 @@ export const packedNoteDraftSchema = {
}, },
hashtag: { hashtag: {
type: 'string', type: 'string',
optional: true, nullable: false, optional: false, nullable: true,
}, },
poll: { poll: {
type: 'object', type: 'object',
optional: true, nullable: true, optional: false, nullable: true,
properties: { properties: {
expiresAt: { expiresAt: {
type: 'string', type: 'string',
@ -124,9 +120,8 @@ export const packedNoteDraftSchema = {
}, },
channelId: { channelId: {
type: 'string', type: 'string',
optional: true, nullable: true, optional: false, nullable: true,
format: 'id', format: 'id',
example: 'xxxxxxxxxx',
}, },
channel: { channel: {
type: 'object', type: 'object',
@ -160,12 +155,20 @@ export const packedNoteDraftSchema = {
}, },
localOnly: { localOnly: {
type: 'boolean', type: 'boolean',
optional: true, nullable: false, optional: false, nullable: false,
}, },
reactionAcceptance: { reactionAcceptance: {
type: 'string', type: 'string',
optional: false, nullable: true, optional: false, nullable: true,
enum: ['likeOnly', 'likeOnlyForRemote', 'nonSensitiveOnly', 'nonSensitiveOnlyForLocalLikeOnlyForRemote', null], enum: ['likeOnly', 'likeOnlyForRemote', 'nonSensitiveOnly', 'nonSensitiveOnlyForLocalLikeOnlyForRemote', null],
}, },
scheduledAt: {
type: 'number',
optional: false, nullable: true,
},
isActuallyScheduled: {
type: 'boolean',
optional: false, nullable: false,
},
}, },
} as const; } as const;

View File

@ -207,6 +207,36 @@ export const packedNotificationSchema = {
optional: false, nullable: false, 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', type: 'object',
properties: { properties: {

View File

@ -317,6 +317,10 @@ export const packedRolePoliciesSchema = {
type: 'integer', type: 'integer',
optional: false, nullable: false, optional: false, nullable: false,
}, },
scheduledNoteLimit: {
type: 'integer',
optional: false, nullable: false,
},
watermarkAvailable: { watermarkAvailable: {
type: 'boolean', type: 'boolean',
optional: false, nullable: false, optional: false, nullable: false,

View File

@ -609,6 +609,8 @@ export const packedMeDetailedOnlySchema = {
quote: { optional: true, ...notificationRecieveConfig }, quote: { optional: true, ...notificationRecieveConfig },
reaction: { optional: true, ...notificationRecieveConfig }, reaction: { optional: true, ...notificationRecieveConfig },
pollEnded: { optional: true, ...notificationRecieveConfig }, pollEnded: { optional: true, ...notificationRecieveConfig },
scheduledNotePosted: { optional: true, ...notificationRecieveConfig },
scheduledNotePostFailed: { optional: true, ...notificationRecieveConfig },
receiveFollowRequest: { optional: true, ...notificationRecieveConfig }, receiveFollowRequest: { optional: true, ...notificationRecieveConfig },
followRequestAccepted: { optional: true, ...notificationRecieveConfig }, followRequestAccepted: { optional: true, ...notificationRecieveConfig },
roleAssigned: { optional: true, ...notificationRecieveConfig }, roleAssigned: { optional: true, ...notificationRecieveConfig },

View File

@ -10,6 +10,7 @@ import { QueueLoggerService } from './QueueLoggerService.js';
import { QueueProcessorService } from './QueueProcessorService.js'; import { QueueProcessorService } from './QueueProcessorService.js';
import { DeliverProcessorService } from './processors/DeliverProcessorService.js'; import { DeliverProcessorService } from './processors/DeliverProcessorService.js';
import { EndedPollNotificationProcessorService } from './processors/EndedPollNotificationProcessorService.js'; import { EndedPollNotificationProcessorService } from './processors/EndedPollNotificationProcessorService.js';
import { PostScheduledNoteProcessorService } from './processors/PostScheduledNoteProcessorService.js';
import { InboxProcessorService } from './processors/InboxProcessorService.js'; import { InboxProcessorService } from './processors/InboxProcessorService.js';
import { UserWebhookDeliverProcessorService } from './processors/UserWebhookDeliverProcessorService.js'; import { UserWebhookDeliverProcessorService } from './processors/UserWebhookDeliverProcessorService.js';
import { SystemWebhookDeliverProcessorService } from './processors/SystemWebhookDeliverProcessorService.js'; import { SystemWebhookDeliverProcessorService } from './processors/SystemWebhookDeliverProcessorService.js';
@ -79,6 +80,7 @@ import { RelationshipProcessorService } from './processors/RelationshipProcessor
UserWebhookDeliverProcessorService, UserWebhookDeliverProcessorService,
SystemWebhookDeliverProcessorService, SystemWebhookDeliverProcessorService,
EndedPollNotificationProcessorService, EndedPollNotificationProcessorService,
PostScheduledNoteProcessorService,
DeliverProcessorService, DeliverProcessorService,
InboxProcessorService, InboxProcessorService,
AggregateRetentionProcessorService, AggregateRetentionProcessorService,

View File

@ -14,6 +14,7 @@ import { CheckModeratorsActivityProcessorService } from '@/queue/processors/Chec
import { UserWebhookDeliverProcessorService } from './processors/UserWebhookDeliverProcessorService.js'; import { UserWebhookDeliverProcessorService } from './processors/UserWebhookDeliverProcessorService.js';
import { SystemWebhookDeliverProcessorService } from './processors/SystemWebhookDeliverProcessorService.js'; import { SystemWebhookDeliverProcessorService } from './processors/SystemWebhookDeliverProcessorService.js';
import { EndedPollNotificationProcessorService } from './processors/EndedPollNotificationProcessorService.js'; import { EndedPollNotificationProcessorService } from './processors/EndedPollNotificationProcessorService.js';
import { PostScheduledNoteProcessorService } from './processors/PostScheduledNoteProcessorService.js';
import { DeliverProcessorService } from './processors/DeliverProcessorService.js'; import { DeliverProcessorService } from './processors/DeliverProcessorService.js';
import { InboxProcessorService } from './processors/InboxProcessorService.js'; import { InboxProcessorService } from './processors/InboxProcessorService.js';
import { DeleteDriveFilesProcessorService } from './processors/DeleteDriveFilesProcessorService.js'; import { DeleteDriveFilesProcessorService } from './processors/DeleteDriveFilesProcessorService.js';
@ -85,6 +86,7 @@ export class QueueProcessorService implements OnApplicationShutdown {
private relationshipQueueWorker: Bull.Worker; private relationshipQueueWorker: Bull.Worker;
private objectStorageQueueWorker: Bull.Worker; private objectStorageQueueWorker: Bull.Worker;
private endedPollNotificationQueueWorker: Bull.Worker; private endedPollNotificationQueueWorker: Bull.Worker;
private postScheduledNoteQueueWorker: Bull.Worker;
constructor( constructor(
@Inject(DI.config) @Inject(DI.config)
@ -94,6 +96,7 @@ export class QueueProcessorService implements OnApplicationShutdown {
private userWebhookDeliverProcessorService: UserWebhookDeliverProcessorService, private userWebhookDeliverProcessorService: UserWebhookDeliverProcessorService,
private systemWebhookDeliverProcessorService: SystemWebhookDeliverProcessorService, private systemWebhookDeliverProcessorService: SystemWebhookDeliverProcessorService,
private endedPollNotificationProcessorService: EndedPollNotificationProcessorService, private endedPollNotificationProcessorService: EndedPollNotificationProcessorService,
private postScheduledNoteProcessorService: PostScheduledNoteProcessorService,
private deliverProcessorService: DeliverProcessorService, private deliverProcessorService: DeliverProcessorService,
private inboxProcessorService: InboxProcessorService, private inboxProcessorService: InboxProcessorService,
private deleteDriveFilesProcessorService: DeleteDriveFilesProcessorService, private deleteDriveFilesProcessorService: DeleteDriveFilesProcessorService,
@ -520,6 +523,21 @@ export class QueueProcessorService implements OnApplicationShutdown {
}); });
} }
//#endregion //#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 @bindThis
@ -534,6 +552,7 @@ export class QueueProcessorService implements OnApplicationShutdown {
this.relationshipQueueWorker.run(), this.relationshipQueueWorker.run(),
this.objectStorageQueueWorker.run(), this.objectStorageQueueWorker.run(),
this.endedPollNotificationQueueWorker.run(), this.endedPollNotificationQueueWorker.run(),
this.postScheduledNoteQueueWorker.run(),
]); ]);
} }
@ -549,6 +568,7 @@ export class QueueProcessorService implements OnApplicationShutdown {
this.relationshipQueueWorker.close(), this.relationshipQueueWorker.close(),
this.objectStorageQueueWorker.close(), this.objectStorageQueueWorker.close(),
this.endedPollNotificationQueueWorker.close(), this.endedPollNotificationQueueWorker.close(),
this.postScheduledNoteQueueWorker.close(),
]); ]);
} }

View File

@ -12,6 +12,7 @@ export const QUEUE = {
INBOX: 'inbox', INBOX: 'inbox',
SYSTEM: 'system', SYSTEM: 'system',
ENDED_POLL_NOTIFICATION: 'endedPollNotification', ENDED_POLL_NOTIFICATION: 'endedPollNotification',
POST_SCHEDULED_NOTE: 'postScheduledNote',
DB: 'db', DB: 'db',
RELATIONSHIP: 'relationship', RELATIONSHIP: 'relationship',
OBJECT_STORAGE: 'objectStorage', OBJECT_STORAGE: 'objectStorage',

View File

@ -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<PostScheduledNoteJobData>): Promise<void> {
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,
});
}
}
}

View File

@ -109,6 +109,10 @@ export type EndedPollNotificationJobData = {
noteId: MiNote['id']; noteId: MiNote['id'];
}; };
export type PostScheduledNoteJobData = {
noteDraftId: string;
};
export type SystemWebhookDeliverJobData<T extends SystemWebhookEventType = SystemWebhookEventType> = { export type SystemWebhookDeliverJobData<T extends SystemWebhookEventType = SystemWebhookEventType> = {
type: T; type: T;
content: SystemWebhookPayload<T>; content: SystemWebhookPayload<T>;

View File

@ -75,7 +75,7 @@ export class ServerService implements OnApplicationShutdown {
@bindThis @bindThis
public async launch(): Promise<void> { public async launch(): Promise<void> {
const fastify = Fastify({ const fastify = Fastify({
trustProxy: true, trustProxy: this.config.trustProxy ?? true,
logger: false, logger: false,
}); });
this.#fastify = fastify; this.#fastify = fastify;

View File

@ -34,13 +34,22 @@ export const meta = {
res: { res: {
type: 'object', type: 'object',
optional: false, nullable: false, optional: false, nullable: false,
ref: 'MeDetailed', allOf: [
properties: { {
token: { type: 'object',
type: 'string', ref: 'MeDetailed',
optional: false, nullable: false,
}, },
}, {
type: 'object',
optional: false, nullable: false,
properties: {
token: {
type: 'string',
optional: false, nullable: false,
},
},
}
],
}, },
} as const; } as const;

View File

@ -36,6 +36,7 @@ export const paramDef = {
startsAt: { type: 'integer' }, startsAt: { type: 'integer' },
imageUrl: { type: 'string', minLength: 1 }, imageUrl: { type: 'string', minLength: 1 },
dayOfWeek: { type: 'integer' }, dayOfWeek: { type: 'integer' },
isSensitive: { type: 'boolean' },
}, },
required: ['url', 'memo', 'place', 'priority', 'ratio', 'expiresAt', 'startsAt', 'imageUrl', 'dayOfWeek'], required: ['url', 'memo', 'place', 'priority', 'ratio', 'expiresAt', 'startsAt', 'imageUrl', 'dayOfWeek'],
} as const; } as const;
@ -55,6 +56,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
expiresAt: new Date(ps.expiresAt), expiresAt: new Date(ps.expiresAt),
startsAt: new Date(ps.startsAt), startsAt: new Date(ps.startsAt),
dayOfWeek: ps.dayOfWeek, dayOfWeek: ps.dayOfWeek,
isSensitive: ps.isSensitive,
url: ps.url, url: ps.url,
imageUrl: ps.imageUrl, imageUrl: ps.imageUrl,
priority: ps.priority, priority: ps.priority,
@ -73,6 +75,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
expiresAt: ad.expiresAt.toISOString(), expiresAt: ad.expiresAt.toISOString(),
startsAt: ad.startsAt.toISOString(), startsAt: ad.startsAt.toISOString(),
dayOfWeek: ad.dayOfWeek, dayOfWeek: ad.dayOfWeek,
isSensitive: ad.isSensitive,
url: ad.url, url: ad.url,
imageUrl: ad.imageUrl, imageUrl: ad.imageUrl,
priority: ad.priority, priority: ad.priority,

View File

@ -63,6 +63,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
expiresAt: ad.expiresAt.toISOString(), expiresAt: ad.expiresAt.toISOString(),
startsAt: ad.startsAt.toISOString(), startsAt: ad.startsAt.toISOString(),
dayOfWeek: ad.dayOfWeek, dayOfWeek: ad.dayOfWeek,
isSensitive: ad.isSensitive,
url: ad.url, url: ad.url,
imageUrl: ad.imageUrl, imageUrl: ad.imageUrl,
memo: ad.memo, memo: ad.memo,

View File

@ -39,6 +39,7 @@ export const paramDef = {
expiresAt: { type: 'integer' }, expiresAt: { type: 'integer' },
startsAt: { type: 'integer' }, startsAt: { type: 'integer' },
dayOfWeek: { type: 'integer' }, dayOfWeek: { type: 'integer' },
isSensitive: { type: 'boolean' },
}, },
required: ['id'], required: ['id'],
} as const; } as const;
@ -66,6 +67,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
expiresAt: ps.expiresAt ? new Date(ps.expiresAt) : undefined, expiresAt: ps.expiresAt ? new Date(ps.expiresAt) : undefined,
startsAt: ps.startsAt ? new Date(ps.startsAt) : undefined, startsAt: ps.startsAt ? new Date(ps.startsAt) : undefined,
dayOfWeek: ps.dayOfWeek, dayOfWeek: ps.dayOfWeek,
isSensitive: ps.isSensitive,
}); });
const updatedAd = await this.adsRepository.findOneByOrFail({ id: ad.id }); const updatedAd = await this.adsRepository.findOneByOrFail({ id: ad.id });

View File

@ -5,7 +5,7 @@
import { Inject, Injectable } from '@nestjs/common'; import { Inject, Injectable } from '@nestjs/common';
import { Endpoint } from '@/server/api/endpoint-base.js'; 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 = { export const meta = {
tags: ['admin'], tags: ['admin'],
@ -49,6 +49,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
constructor( constructor(
@Inject('queue:system') public systemQueue: SystemQueue, @Inject('queue:system') public systemQueue: SystemQueue,
@Inject('queue:endedPollNotification') public endedPollNotificationQueue: EndedPollNotificationQueue, @Inject('queue:endedPollNotification') public endedPollNotificationQueue: EndedPollNotificationQueue,
@Inject('queue:postScheduledNote') public postScheduledNoteQueue: PostScheduledNoteQueue,
@Inject('queue:deliver') public deliverQueue: DeliverQueue, @Inject('queue:deliver') public deliverQueue: DeliverQueue,
@Inject('queue:inbox') public inboxQueue: InboxQueue, @Inject('queue:inbox') public inboxQueue: InboxQueue,
@Inject('queue:db') public dbQueue: DbQueue, @Inject('queue:db') public dbQueue: DbQueue,

View File

@ -103,6 +103,8 @@ export const meta = {
quote: { optional: true, ...notificationRecieveConfig }, quote: { optional: true, ...notificationRecieveConfig },
reaction: { optional: true, ...notificationRecieveConfig }, reaction: { optional: true, ...notificationRecieveConfig },
pollEnded: { optional: true, ...notificationRecieveConfig }, pollEnded: { optional: true, ...notificationRecieveConfig },
scheduledNotePosted: { optional: true, ...notificationRecieveConfig },
scheduledNotePostFailed: { optional: true, ...notificationRecieveConfig },
receiveFollowRequest: { optional: true, ...notificationRecieveConfig }, receiveFollowRequest: { optional: true, ...notificationRecieveConfig },
followRequestAccepted: { optional: true, ...notificationRecieveConfig }, followRequestAccepted: { optional: true, ...notificationRecieveConfig },
roleAssigned: { optional: true, ...notificationRecieveConfig }, roleAssigned: { optional: true, ...notificationRecieveConfig },

View File

@ -18,9 +18,9 @@ import { UserEntityService } from '@/core/entities/UserEntityService.js';
import { NoteEntityService } from '@/core/entities/NoteEntityService.js'; import { NoteEntityService } from '@/core/entities/NoteEntityService.js';
import { UtilityService } from '@/core/UtilityService.js'; import { UtilityService } from '@/core/UtilityService.js';
import { bindThis } from '@/decorators.js'; import { bindThis } from '@/decorators.js';
import { ApiError } from '../../error.js';
import { IdentifiableError } from '@/misc/identifiable-error.js'; import { IdentifiableError } from '@/misc/identifiable-error.js';
import { FetchAllowSoftFailMask } from '@/core/activitypub/misc/check-against-url.js'; import { FetchAllowSoftFailMask } from '@/core/activitypub/misc/check-against-url.js';
import { ApiError } from '../../error.js';
export const meta = { export const meta = {
tags: ['federation'], tags: ['federation'],

View File

@ -209,6 +209,8 @@ export const paramDef = {
quote: notificationRecieveConfig, quote: notificationRecieveConfig,
reaction: notificationRecieveConfig, reaction: notificationRecieveConfig,
pollEnded: notificationRecieveConfig, pollEnded: notificationRecieveConfig,
scheduledNotePosted: notificationRecieveConfig,
scheduledNotePostFailed: notificationRecieveConfig,
receiveFollowRequest: notificationRecieveConfig, receiveFollowRequest: notificationRecieveConfig,
followRequestAccepted: notificationRecieveConfig, followRequestAccepted: notificationRecieveConfig,
roleAssigned: notificationRecieveConfig, roleAssigned: notificationRecieveConfig,

View File

@ -6,17 +6,10 @@
import ms from 'ms'; import ms from 'ms';
import { In } from 'typeorm'; import { In } from 'typeorm';
import { Inject, Injectable } from '@nestjs/common'; 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 { MAX_NOTE_TEXT_LENGTH } from '@/const.js';
import { Endpoint } from '@/server/api/endpoint-base.js'; import { Endpoint } from '@/server/api/endpoint-base.js';
import { NoteEntityService } from '@/core/entities/NoteEntityService.js'; import { NoteEntityService } from '@/core/entities/NoteEntityService.js';
import { NoteCreateService } from '@/core/NoteCreateService.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 { IdentifiableError } from '@/misc/identifiable-error.js';
import { ApiError } from '../../error.js'; import { ApiError } from '../../error.js';
@ -223,168 +216,28 @@ export const paramDef = {
@Injectable() @Injectable()
export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export
constructor( 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 noteEntityService: NoteEntityService,
private noteCreateService: NoteCreateService, private noteCreateService: NoteCreateService,
) { ) {
super(meta, paramDef, async (ps, me) => { 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 { try {
const note = await this.noteCreateService.create(me, { const note = await this.noteCreateService.fetchAndCreate(me, {
createdAt: new Date(), createdAt: new Date(),
files: files, fileIds: ps.fileIds ?? ps.mediaIds ?? [],
poll: ps.poll ? { poll: ps.poll ? {
choices: ps.poll.choices, choices: ps.poll.choices,
multiple: ps.poll.multiple ?? false, multiple: ps.poll.multiple ?? false,
expiresAt: ps.poll.expiresAt ? new Date(ps.poll.expiresAt) : null, expiresAt: ps.poll.expiredAfter ? new Date(Date.now() + ps.poll.expiredAfter) : ps.poll.expiresAt ? new Date(ps.poll.expiresAt) : null,
} : undefined, } : null,
text: ps.text ?? undefined, text: ps.text ?? null,
reply, replyId: ps.replyId ?? null,
renote, renoteId: ps.renoteId ?? null,
cw: ps.cw, cw: ps.cw ?? null,
localOnly: ps.localOnly, localOnly: ps.localOnly,
reactionAcceptance: ps.reactionAcceptance, reactionAcceptance: ps.reactionAcceptance,
visibility: ps.visibility, visibility: ps.visibility,
visibleUsers, visibleUserIds: ps.visibleUserIds ?? [],
channel, channelId: ps.channelId ?? null,
apMentions: ps.noExtractMentions ? [] : undefined, apMentions: ps.noExtractMentions ? [] : undefined,
apHashtags: ps.noExtractHashtags ? [] : undefined, apHashtags: ps.noExtractHashtags ? [] : undefined,
apEmojis: ps.noExtractEmojis ? [] : undefined, apEmojis: ps.noExtractEmojis ? [] : undefined,
@ -393,16 +246,46 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
return { return {
createdNote: await this.noteEntityService.pack(note, me), createdNote: await this.noteEntityService.pack(note, me),
}; };
} catch (e) { } catch (err) {
// TODO: 他のErrorもここでキャッチしてエラーメッセージを当てるようにしたい // TODO: 他のErrorもここでキャッチしてエラーメッセージを当てるようにしたい
if (e instanceof IdentifiableError) { if (err instanceof IdentifiableError) {
if (e.id === '689ee33f-f97c-479a-ac49-1b9f8140af99') { if (err.id === '689ee33f-f97c-479a-ac49-1b9f8140af99') {
throw new ApiError(meta.errors.containsProhibitedWords); 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); 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;
} }
}); });
} }

View File

@ -124,6 +124,12 @@ export const meta = {
id: '9ee33bbe-fde3-4c71-9b51-e50492c6b9c8', 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: { cannotRenoteToExternal: {
message: 'Cannot Renote to External.', message: 'Cannot Renote to External.',
code: 'CANNOT_RENOTE_TO_EXTERNAL', code: 'CANNOT_RENOTE_TO_EXTERNAL',
@ -162,7 +168,7 @@ export const paramDef = {
fileIds: { fileIds: {
type: 'array', type: 'array',
uniqueItems: true, uniqueItems: true,
minItems: 1, minItems: 0,
maxItems: 16, maxItems: 16,
items: { type: 'string', format: 'misskey:id' }, items: { type: 'string', format: 'misskey:id' },
}, },
@ -183,8 +189,10 @@ export const paramDef = {
}, },
required: ['choices'], 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; } as const;
@Injectable() @Injectable()
@ -196,22 +204,23 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
super(meta, paramDef, async (ps, me) => { super(meta, paramDef, async (ps, me) => {
const draft = await this.noteDraftService.create(me, { const draft = await this.noteDraftService.create(me, {
fileIds: ps.fileIds, fileIds: ps.fileIds,
poll: ps.poll ? { pollChoices: ps.poll?.choices ?? [],
choices: ps.poll.choices, pollMultiple: ps.poll?.multiple ?? false,
multiple: ps.poll.multiple ?? false, pollExpiresAt: ps.poll?.expiresAt ? new Date(ps.poll.expiresAt) : null,
expiresAt: ps.poll.expiresAt ? new Date(ps.poll.expiresAt) : null, pollExpiredAfter: ps.poll?.expiredAfter ?? null,
expiredAfter: ps.poll.expiredAfter ?? null, hasPoll: ps.poll != null,
} : undefined, text: ps.text,
text: ps.text ?? null, replyId: ps.replyId,
replyId: ps.replyId ?? undefined, renoteId: ps.renoteId,
renoteId: ps.renoteId ?? undefined, cw: ps.cw,
cw: ps.cw ?? null, hashtag: ps.hashtag,
...(ps.hashtag ? { hashtag: ps.hashtag } : {}),
localOnly: ps.localOnly, localOnly: ps.localOnly,
reactionAcceptance: ps.reactionAcceptance, reactionAcceptance: ps.reactionAcceptance,
visibility: ps.visibility, visibility: ps.visibility,
visibleUserIds: ps.visibleUserIds ?? [], visibleUserIds: ps.visibleUserIds,
channelId: ps.channelId ?? undefined, channelId: ps.channelId,
scheduledAt: ps.scheduledAt ? new Date(ps.scheduledAt) : null,
isActuallyScheduled: ps.isActuallyScheduled,
}).catch((err) => { }).catch((err) => {
if (err instanceof IdentifiableError) { if (err instanceof IdentifiableError) {
switch (err.id) { switch (err.id) {
@ -241,6 +250,8 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
throw new ApiError(meta.errors.cannotReplyToInvisibleNote); throw new ApiError(meta.errors.cannotReplyToInvisibleNote);
case '215dbc76-336c-4d2a-9605-95766ba7dab0': case '215dbc76-336c-4d2a-9605-95766ba7dab0':
throw new ApiError(meta.errors.cannotReplyToSpecifiedVisibilityNoteWithExtendedVisibility); throw new ApiError(meta.errors.cannotReplyToSpecifiedVisibilityNoteWithExtendedVisibility);
case 'c3275f19-4558-4c59-83e1-4f684b5fab66':
throw new ApiError(meta.errors.tooManyScheduledNotes);
default: default:
throw err; throw err;
} }

View File

@ -41,6 +41,7 @@ export const paramDef = {
untilId: { type: 'string', format: 'misskey:id' }, untilId: { type: 'string', format: 'misskey:id' },
sinceDate: { type: 'integer' }, sinceDate: { type: 'integer' },
untilDate: { type: 'integer' }, untilDate: { type: 'integer' },
scheduled: { type: 'boolean', nullable: true },
}, },
required: [], required: [],
} as const; } as const;
@ -58,6 +59,12 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
const query = this.queryService.makePaginationQuery<MiNoteDraft>(this.noteDraftsRepository.createQueryBuilder('drafts'), ps.sinceId, ps.untilId, ps.sinceDate, ps.untilDate) const query = this.queryService.makePaginationQuery<MiNoteDraft>(this.noteDraftsRepository.createQueryBuilder('drafts'), ps.sinceId, ps.untilId, ps.sinceDate, ps.untilDate)
.andWhere('drafts.userId = :meId', { meId: me.id }); .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 const drafts = await query
.limit(ps.limit) .limit(ps.limit)
.getMany(); .getMany();

View File

@ -159,6 +159,12 @@ export const meta = {
code: 'CANNOT_REPLY_TO_SPECIFIED_VISIBILITY_NOTE_WITH_EXTENDED_VISIBILITY', code: 'CANNOT_REPLY_TO_SPECIFIED_VISIBILITY_NOTE_WITH_EXTENDED_VISIBILITY',
id: '215dbc76-336c-4d2a-9605-95766ba7dab0', 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: { limit: {
@ -171,14 +177,14 @@ export const paramDef = {
type: 'object', type: 'object',
properties: { properties: {
draftId: { type: 'string', nullable: false, format: 'misskey:id' }, 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: { visibleUserIds: { type: 'array', uniqueItems: true, items: {
type: 'string', format: 'misskey:id', type: 'string', format: 'misskey:id',
} }, } },
cw: { type: 'string', nullable: true, minLength: 1, maxLength: 100 }, cw: { type: 'string', nullable: true, minLength: 1, maxLength: 100 },
hashtag: { type: 'string', nullable: true, maxLength: 200 }, hashtag: { type: 'string', nullable: true, maxLength: 200 },
localOnly: { type: 'boolean', default: false }, localOnly: { type: 'boolean' },
reactionAcceptance: { type: 'string', nullable: true, enum: [null, 'likeOnly', 'likeOnlyForRemote', 'nonSensitiveOnly', 'nonSensitiveOnlyForLocalLikeOnlyForRemote'], default: null }, reactionAcceptance: { type: 'string', nullable: true, enum: [null, 'likeOnly', 'likeOnlyForRemote', 'nonSensitiveOnly', 'nonSensitiveOnlyForLocalLikeOnlyForRemote'] },
replyId: { type: 'string', format: 'misskey:id', nullable: true }, replyId: { type: 'string', format: 'misskey:id', nullable: true },
renoteId: { type: 'string', format: 'misskey:id', nullable: true }, renoteId: { type: 'string', format: 'misskey:id', nullable: true },
channelId: { type: 'string', format: 'misskey:id', nullable: true }, channelId: { type: 'string', format: 'misskey:id', nullable: true },
@ -194,7 +200,7 @@ export const paramDef = {
fileIds: { fileIds: {
type: 'array', type: 'array',
uniqueItems: true, uniqueItems: true,
minItems: 1, minItems: 0,
maxItems: 16, maxItems: 16,
items: { type: 'string', format: 'misskey:id' }, items: { type: 'string', format: 'misskey:id' },
}, },
@ -215,6 +221,8 @@ export const paramDef = {
}, },
required: ['choices'], required: ['choices'],
}, },
scheduledAt: { type: 'integer', nullable: true },
isActuallyScheduled: { type: 'boolean' },
}, },
required: ['draftId'], required: ['draftId'],
} as const; } as const;
@ -228,22 +236,22 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
super(meta, paramDef, async (ps, me) => { super(meta, paramDef, async (ps, me) => {
const draft = await this.noteDraftService.update(me, ps.draftId, { const draft = await this.noteDraftService.update(me, ps.draftId, {
fileIds: ps.fileIds, fileIds: ps.fileIds,
poll: ps.poll ? { pollChoices: ps.poll?.choices,
choices: ps.poll.choices, pollMultiple: ps.poll?.multiple,
multiple: ps.poll.multiple ?? false, pollExpiresAt: ps.poll?.expiresAt ? new Date(ps.poll.expiresAt) : null,
expiresAt: ps.poll.expiresAt ? new Date(ps.poll.expiresAt) : null, pollExpiredAfter: ps.poll?.expiredAfter,
expiredAfter: ps.poll.expiredAfter ?? null, text: ps.text,
} : undefined, replyId: ps.replyId,
text: ps.text ?? null, renoteId: ps.renoteId,
replyId: ps.replyId ?? undefined, cw: ps.cw,
renoteId: ps.renoteId ?? undefined, hashtag: ps.hashtag,
cw: ps.cw ?? null,
...(ps.hashtag ? { hashtag: ps.hashtag } : {}),
localOnly: ps.localOnly, localOnly: ps.localOnly,
reactionAcceptance: ps.reactionAcceptance, reactionAcceptance: ps.reactionAcceptance,
visibility: ps.visibility, visibility: ps.visibility,
visibleUserIds: ps.visibleUserIds ?? [], visibleUserIds: ps.visibleUserIds,
channelId: ps.channelId ?? undefined, channelId: ps.channelId,
scheduledAt: ps.scheduledAt ? new Date(ps.scheduledAt) : null,
isActuallyScheduled: ps.isActuallyScheduled,
}).catch((err) => { }).catch((err) => {
if (err instanceof IdentifiableError) { if (err instanceof IdentifiableError) {
switch (err.id) { switch (err.id) {
@ -285,6 +293,8 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
throw new ApiError(meta.errors.containsProhibitedWords); throw new ApiError(meta.errors.containsProhibitedWords);
case '4de0363a-3046-481b-9b0f-feff3e211025': case '4de0363a-3046-481b-9b0f-feff3e211025':
throw new ApiError(meta.errors.containsTooManyMentions); throw new ApiError(meta.errors.containsTooManyMentions);
case 'bacdf856-5c51-4159-b88a-804fa5103be5':
throw new ApiError(meta.errors.tooManyScheduledNotes);
default: default:
throw err; throw err;
} }

View File

@ -29,10 +29,16 @@ export const meta = {
id: '24fcbfc6-2e37-42b6-8388-c29b3861a08d', id: '24fcbfc6-2e37-42b6-8388-c29b3861a08d',
}, },
signinRequired: { contentRestrictedByUser: {
message: 'Signin required.', message: 'Content restricted by user. Please sign in to view.',
code: 'SIGNIN_REQUIRED', code: 'CONTENT_RESTRICTED_BY_USER',
id: '8e75455b-738c-471d-9f80-62693f33372e', 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; } as const;
@ -61,15 +67,15 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
}); });
if (note.user!.requireSigninToViewContents && me == null) { 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) { 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) { 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, { return await this.noteEntityService.pack(note, me, {

View File

@ -22,17 +22,26 @@ export const meta = {
res: { res: {
type: 'object', type: 'object',
optional: false, nullable: false, optional: false, nullable: false,
ref: 'UserList', allOf: [
properties: { {
likedCount: { type: 'object',
type: 'number', ref: 'UserList',
optional: true, nullable: false,
}, },
isLiked: { {
type: 'boolean', type: 'object',
optional: true, nullable: false, optional: false, nullable: false,
properties: {
likedCount: {
type: 'number',
optional: true, nullable: false,
},
isLiked: {
type: 'boolean',
optional: true, nullable: false,
},
},
}, },
}, ],
}, },
errors: { errors: {

View File

@ -13,7 +13,7 @@
}; };
window.onunhandledrejection = (e) => { window.onunhandledrejection = (e) => {
console.error(e); console.error(e);
renderError('SOMETHING_HAPPENED_IN_PROMISE', e); renderError('SOMETHING_HAPPENED_IN_PROMISE', e.reason || e);
}; };
let forceError = localStorage.getItem('forceError'); let forceError = localStorage.getItem('forceError');

View File

@ -12,6 +12,8 @@
* quote - 稿Renoteされた * quote - 稿Renoteされた
* reaction - 稿 * reaction - 稿
* pollEnded - * pollEnded -
* scheduledNotePosted - 稿
* scheduledNotePostFailed - 稿
* receiveFollowRequest - * receiveFollowRequest -
* followRequestAccepted - * followRequestAccepted -
* roleAssigned - * roleAssigned -
@ -32,6 +34,8 @@ export const notificationTypes = [
'quote', 'quote',
'reaction', 'reaction',
'pollEnded', 'pollEnded',
'scheduledNotePosted',
'scheduledNotePostFailed',
'receiveFollowRequest', 'receiveFollowRequest',
'followRequestAccepted', 'followRequestAccepted',
'roleAssigned', 'roleAssigned',

View File

@ -68,7 +68,6 @@ async function createAdmin(host: Host): Promise<Misskey.entities.SignupResponse
return await client.request('admin/accounts/create', ADMIN_PARAMS).then(res => { return await client.request('admin/accounts/create', ADMIN_PARAMS).then(res => {
ADMIN_CACHE.set(host, { ADMIN_CACHE.set(host, {
id: res.id, id: res.id,
// @ts-expect-error FIXME: openapi-typescript generates incorrect response type for this endpoint, so ignore this
i: res.token, i: res.token,
}); });
return res as Misskey.entities.SignupResponse; return res as Misskey.entities.SignupResponse;

View File

@ -11,15 +11,15 @@
}, },
"devDependencies": { "devDependencies": {
"@types/estree": "1.0.8", "@types/estree": "1.0.8",
"@types/node": "22.17.0", "@types/node": "22.18.6",
"@typescript-eslint/eslint-plugin": "8.38.0", "@typescript-eslint/eslint-plugin": "8.44.1",
"@typescript-eslint/parser": "8.38.0", "@typescript-eslint/parser": "8.44.1",
"rollup": "4.46.2", "rollup": "4.52.2",
"typescript": "5.9.2" "typescript": "5.9.2"
}, },
"dependencies": { "dependencies": {
"estree-walker": "3.0.3", "estree-walker": "3.0.3",
"magic-string": "0.30.17", "magic-string": "0.30.19",
"vite": "7.0.6" "vite": "7.1.7"
} }
} }

View File

@ -46,9 +46,71 @@ export default [
allowSingleExtends: true, allowSingleExtends: true,
}], }],
'import/consistent-type-specifier-style': ['error', 'prefer-top-level'], 'import/consistent-type-specifier-style': ['error', 'prefer-top-level'],
// window の禁止理由: グローバルスコープと衝突し、予期せぬ結果を招くため // window ... グローバルスコープと衝突し、予期せぬ結果を招くため
// e の禁止理由: error や event など、複数のキーワードの頭文字であり分かりにくいため // e ... error や event など、複数のキーワードの頭文字であり分かりにくいため
'id-denylist': ['error', 'window', 'e'], // 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'], 'no-shadow': ['warn'],
'vue/attributes-order': ['error', { 'vue/attributes-order': ['error', {
alphabetical: false, alphabetical: false,

View File

@ -16,7 +16,7 @@
"@rollup/pluginutils": "5.3.0", "@rollup/pluginutils": "5.3.0",
"@twemoji/parser": "16.0.0", "@twemoji/parser": "16.0.0",
"@vitejs/plugin-vue": "6.0.1", "@vitejs/plugin-vue": "6.0.1",
"@vue/compiler-sfc": "3.5.21", "@vue/compiler-sfc": "3.5.22",
"astring": "1.9.0", "astring": "1.9.0",
"buraha": "0.0.1", "buraha": "0.0.1",
"estree-walker": "3.0.3", "estree-walker": "3.0.3",
@ -26,47 +26,47 @@
"mfm-js": "0.25.0", "mfm-js": "0.25.0",
"misskey-js": "workspace:*", "misskey-js": "workspace:*",
"punycode.js": "2.3.1", "punycode.js": "2.3.1",
"rollup": "4.50.1", "rollup": "4.52.2",
"sass": "1.92.1", "sass": "1.93.2",
"shiki": "3.12.2", "shiki": "3.13.0",
"tinycolor2": "1.6.0", "tinycolor2": "1.6.0",
"tsc-alias": "1.8.16", "tsc-alias": "1.8.16",
"tsconfig-paths": "4.2.0", "tsconfig-paths": "4.2.0",
"typescript": "5.9.2", "typescript": "5.9.2",
"uuid": "11.1.0", "uuid": "11.1.0",
"vite": "7.1.4", "vite": "7.1.7",
"vue": "3.5.21" "vue": "3.5.22"
}, },
"devDependencies": { "devDependencies": {
"@misskey-dev/summaly": "5.2.3", "@misskey-dev/summaly": "5.2.3",
"@tabler/icons-webfont": "3.34.1", "@tabler/icons-webfont": "3.35.0",
"@testing-library/vue": "8.1.0", "@testing-library/vue": "8.1.0",
"@types/estree": "1.0.8", "@types/estree": "1.0.8",
"@types/micromatch": "4.0.9", "@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/punycode.js": "npm:@types/punycode@2.1.4",
"@types/tinycolor2": "1.4.6", "@types/tinycolor2": "1.4.6",
"@types/ws": "8.18.1", "@types/ws": "8.18.1",
"@typescript-eslint/eslint-plugin": "8.42.0", "@typescript-eslint/eslint-plugin": "8.44.1",
"@typescript-eslint/parser": "8.42.0", "@typescript-eslint/parser": "8.44.1",
"@vitest/coverage-v8": "3.2.4", "@vitest/coverage-v8": "3.2.4",
"@vue/runtime-core": "3.5.21", "@vue/runtime-core": "3.5.22",
"acorn": "8.15.0", "acorn": "8.15.0",
"cross-env": "10.0.0", "cross-env": "10.0.0",
"eslint-plugin-import": "2.32.0", "eslint-plugin-import": "2.32.0",
"eslint-plugin-vue": "10.4.0", "eslint-plugin-vue": "10.5.0",
"fast-glob": "3.3.3", "fast-glob": "3.3.3",
"happy-dom": "18.0.1", "happy-dom": "18.0.1",
"intersection-observer": "0.12.2", "intersection-observer": "0.12.2",
"micromatch": "4.0.8", "micromatch": "4.0.8",
"msw": "2.11.1", "msw": "2.11.3",
"nodemon": "3.1.10", "nodemon": "3.1.10",
"prettier": "3.6.2", "prettier": "3.6.2",
"start-server-and-test": "2.1.0", "start-server-and-test": "2.1.2",
"tsx": "4.20.5", "tsx": "4.20.6",
"vite-plugin-turbosnap": "1.0.3", "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-eslint-parser": "10.2.0",
"vue-tsc": "3.0.6" "vue-tsc": "3.0.8"
} }
} }

View File

@ -33,7 +33,7 @@ import type { Theme } from '@/theme.js';
console.log('Misskey Embed'); console.log('Misskey Embed');
//#region Embedパラメータの取得・パース //#region Embedパラメータの取得・パース
const params = new URLSearchParams(location.search); const params = new URLSearchParams(window.location.search);
const embedParams = parseEmbedParams(params); const embedParams = parseEmbedParams(params);
if (_DEV_) console.log(embedParams); if (_DEV_) console.log(embedParams);
//#endregion //#endregion
@ -81,7 +81,7 @@ storeBootloaderErrors({ ...i18n.ts._bootErrors, reload: i18n.ts.reload });
//#endregion //#endregion
// サイズの制限 // サイズの制限
document.documentElement.style.maxWidth = '500px'; window.document.documentElement.style.maxWidth = '500px';
// iframeIdの設定 // iframeIdの設定
function setIframeIdHandler(event: MessageEvent) { function setIframeIdHandler(event: MessageEvent) {
@ -114,16 +114,16 @@ app.provide(DI.embedParams, embedParams);
const rootEl = ((): HTMLElement => { const rootEl = ((): HTMLElement => {
const MISSKEY_MOUNT_DIV_ID = 'misskey_app'; 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) { if (currentRoot) {
console.warn('multiple import detected'); console.warn('multiple import detected');
return currentRoot; return currentRoot;
} }
const root = document.createElement('div'); const root = window.document.createElement('div');
root.id = MISSKEY_MOUNT_DIV_ID; root.id = MISSKEY_MOUNT_DIV_ID;
document.body.appendChild(root); window.document.body.appendChild(root);
return root; return root;
})(); })();
@ -159,7 +159,7 @@ console.log(i18n.tsx._selfXssPrevention.description3({ link: 'https://misskey-hu
//#endregion //#endregion
function removeSplash() { function removeSplash() {
const splash = document.getElementById('splash'); const splash = window.document.getElementById('splash');
if (splash) { if (splash) {
splash.style.opacity = '0'; splash.style.opacity = '0';
splash.style.pointerEvents = 'none'; splash.style.pointerEvents = 'none';

View File

@ -19,7 +19,7 @@ import { extractAvgColorFromBlurhash } from '@@/js/extract-avg-color-from-blurha
const canvasPromise = new Promise<WorkerMultiDispatch | HTMLCanvasElement>(resolve => { const canvasPromise = new Promise<WorkerMultiDispatch | HTMLCanvasElement>(resolve => {
// Web Worker // Web Worker
if (import.meta.env.MODE === 'test') { if (import.meta.env.MODE === 'test') {
const canvas = document.createElement('canvas'); const canvas = window.document.createElement('canvas');
canvas.width = 64; canvas.width = 64;
canvas.height = 64; canvas.height = 64;
resolve(canvas); resolve(canvas);
@ -34,7 +34,7 @@ const canvasPromise = new Promise<WorkerMultiDispatch | HTMLCanvasElement>(resol
); );
resolve(workers); resolve(workers);
} else { } else {
const canvas = document.createElement('canvas'); const canvas = window.document.createElement('canvas');
canvas.width = 64; canvas.width = 64;
canvas.height = 64; canvas.height = 64;
resolve(canvas); resolve(canvas);

View File

@ -29,7 +29,7 @@ const props = defineProps<{
// if no instance data is given, this is for the local instance // if no instance data is given, this is for the local instance
const instance = props.instance ?? { const instance = props.instance ?? {
name: serverMetadata.name, 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'); const faviconUrl = computed(() => props.instance ? mediaProxy.getProxiedImageUrlNullable(props.instance.faviconUrl, 'preview') : mediaProxy.getProxiedImageUrlNullable(serverMetadata.iconUrl, 'preview') ?? '/favicon.ico');

View File

@ -27,7 +27,7 @@ const canonical = props.host === localHost ? `@${props.username}` : `@${props.us
const url = `/${canonical}`; 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); bg.setAlpha(0.1);
const bgCss = bg.toRgbString(); const bgCss = bg.toRgbString();
</script> </script>

View File

@ -134,7 +134,7 @@ const isBackTop = ref(false);
const empty = computed(() => items.value.size === 0); const empty = computed(() => items.value.size === 0);
const error = ref(false); 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(); const visibility = useDocumentVisibility();
@ -353,7 +353,7 @@ watch(visibility, () => {
BACKGROUND_PAUSE_WAIT_SEC * 1000); BACKGROUND_PAUSE_WAIT_SEC * 1000);
} else { // 'visible' } else { // 'visible'
if (timerForSetPause) { if (timerForSetPause) {
clearTimeout(timerForSetPause); window.clearTimeout(timerForSetPause);
timerForSetPause = null; timerForSetPause = null;
} else { } else {
isPausingUpdate = false; isPausingUpdate = false;
@ -447,11 +447,11 @@ onBeforeMount(() => {
init().then(() => { init().then(() => {
if (props.pagination.reversed) { if (props.pagination.reversed) {
nextTick(() => { nextTick(() => {
setTimeout(toBottom, 800); window.setTimeout(toBottom, 800);
// scrollToBottommoreFetching // scrollToBottommoreFetching
// more = true // more = true
setTimeout(() => { window.setTimeout(() => {
moreFetching.value = false; moreFetching.value = false;
}, 2000); }, 2000);
}); });
@ -461,11 +461,11 @@ onBeforeMount(() => {
onBeforeUnmount(() => { onBeforeUnmount(() => {
if (timerForSetPause) { if (timerForSetPause) {
clearTimeout(timerForSetPause); window.clearTimeout(timerForSetPause);
timerForSetPause = null; timerForSetPause = null;
} }
if (preventAppearFetchMoreTimer.value) { if (preventAppearFetchMoreTimer.value) {
clearTimeout(preventAppearFetchMoreTimer.value); window.clearTimeout(preventAppearFetchMoreTimer.value);
preventAppearFetchMoreTimer.value = null; preventAppearFetchMoreTimer.value = null;
} }
scrollObserver.value?.disconnect(); scrollObserver.value?.disconnect();

View File

@ -4,7 +4,7 @@
*/ */
import * as Misskey from 'misskey-js'; import * as Misskey from 'misskey-js';
const providedContextEl = document.getElementById('misskey_embedCtx'); const providedContextEl = window.document.getElementById('misskey_embedCtx');
export type ServerContext = { export type ServerContext = {
clip?: Misskey.entities.Clip; clip?: Misskey.entities.Clip;

View File

@ -6,7 +6,7 @@
import * as Misskey from 'misskey-js'; import * as Misskey from 'misskey-js';
import { misskeyApi } from '@/misskey-api.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; const _serverMetadata: Misskey.entities.MetaDetailed | null = (providedMetaEl && providedMetaEl.textContent) ? JSON.parse(providedMetaEl.textContent) : null;

View File

@ -35,15 +35,15 @@ export function assertIsTheme(theme: Record<string, unknown>): theme is Theme {
export function applyTheme(theme: Theme, persist = true) { export function applyTheme(theme: Theme, persist = true) {
if (timeout) window.clearTimeout(timeout); if (timeout) window.clearTimeout(timeout);
document.documentElement.classList.add('_themeChanging_'); window.document.documentElement.classList.add('_themeChanging_');
timeout = window.setTimeout(() => { timeout = window.setTimeout(() => {
document.documentElement.classList.remove('_themeChanging_'); window.document.documentElement.classList.remove('_themeChanging_');
}, 1000); }, 1000);
const colorScheme = theme.base === 'dark' ? 'dark' : 'light'; const colorScheme = theme.base === 'dark' ? 'dark' : 'light';
document.documentElement.dataset.colorScheme = colorScheme; window.document.documentElement.dataset.colorScheme = colorScheme;
// Deep copy // Deep copy
const _theme = JSON.parse(JSON.stringify(theme)); const _theme = JSON.parse(JSON.stringify(theme));
@ -55,7 +55,7 @@ export function applyTheme(theme: Theme, persist = true) {
const props = compile(_theme); 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') { if (tag.tagName === 'META' && tag.getAttribute('name') === 'theme-color') {
tag.setAttribute('content', props['htmlThemeColor']); tag.setAttribute('content', props['htmlThemeColor']);
break; break;
@ -63,7 +63,7 @@ export function applyTheme(theme: Theme, persist = true) {
} }
for (const [k, v] of Object.entries(props)) { 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参照 // iframeを正常に透過させるために、cssのcolor-schemeは `light dark;` 固定にしてある。style.scss参照

View File

@ -52,8 +52,8 @@ function safeURIDecode(str: string): string {
} }
} }
const page = location.pathname.split('/')[2]; const page = window.location.pathname.split('/')[2];
const contentId = safeURIDecode(location.pathname.split('/')[3]); const contentId = safeURIDecode(window.location.pathname.split('/')[3]);
if (_DEV_) console.log(page, contentId); if (_DEV_) console.log(page, contentId);
const embedParams = inject(DI.embedParams, defaultEmbedParams); const embedParams = inject(DI.embedParams, defaultEmbedParams);

View File

@ -64,6 +64,8 @@ function toBase62(n: number): string {
} }
export function getConfig(): UserConfig { export function getConfig(): UserConfig {
const localesHash = toBase62(hash(JSON.stringify(locales)));
return { return {
base: '/embed_vite/', base: '/embed_vite/',
@ -148,9 +150,9 @@ export function getConfig(): UserConfig {
// dependencies of i18n.ts // dependencies of i18n.ts
'config': ['@@/js/config.js'], 'config': ['@@/js/config.js'],
}, },
entryFileNames: 'scripts/[hash:8].js', entryFileNames: `scripts/${localesHash}-[hash:8].js`,
chunkFileNames: 'scripts/[hash:8].js', chunkFileNames: `scripts/${localesHash}-[hash:8].js`,
assetFileNames: 'assets/[hash:8][extname]', assetFileNames: `assets/${localesHash}-[hash:8][extname]`,
paths(id) { paths(id) {
for (const p of externalPackages) { for (const p of externalPackages) {
if (p.match.test(id)) { if (p.match.test(id)) {

View File

@ -51,9 +51,71 @@ export default [
allowSingleExtends: true, allowSingleExtends: true,
}], }],
'import/consistent-type-specifier-style': ['error', 'prefer-top-level'], 'import/consistent-type-specifier-style': ['error', 'prefer-top-level'],
// window の禁止理由: グローバルスコープと衝突し、予期せぬ結果を招くため // window ... グローバルスコープと衝突し、予期せぬ結果を招くため
// e の禁止理由: error や event など、複数のキーワードの頭文字であり分かりにくいため // e ... error や event など、複数のキーワードの頭文字であり分かりにくいため
'id-denylist': ['error', 'window', 'e'], // 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'], 'no-shadow': ['warn'],
'vue/attributes-order': ['error', { 'vue/attributes-order': ['error', {
alphabetical: false, alphabetical: false,

View File

@ -4,15 +4,15 @@
*/ */
// eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing
const address = new URL(document.querySelector<HTMLMetaElement>('meta[property="instance_url"]')?.content || location.href); const address = new URL(window.document.querySelector<HTMLMetaElement>('meta[property="instance_url"]')?.content || window.location.href);
const siteName = document.querySelector<HTMLMetaElement>('meta[property="og:site_name"]')?.content; const siteName = window.document.querySelector<HTMLMetaElement>('meta[property="og:site_name"]')?.content;
export const host = address.host; export const host = address.host;
export const hostname = address.hostname; export const hostname = address.hostname;
export const url = address.origin; export const url = address.origin;
export const port = address.port; export const port = address.port;
export const apiUrl = location.origin + '/api'; export const apiUrl = window.location.origin + '/api';
export const wsOrigin = location.origin; export const wsOrigin = window.location.origin;
export const lang = localStorage.getItem('lang') ?? 'en-US'; export const lang = localStorage.getItem('lang') ?? 'en-US';
export const langs = _LANGS_; export const langs = _LANGS_;
export const version = _VERSION_; export const version = _VERSION_;

View File

@ -51,7 +51,7 @@ export function onScrollTop(el: HTMLElement, cb: (topVisible: boolean) => unknow
// - toleranceの範囲内に収まる程度の微量なスクロールが発生した // - toleranceの範囲内に収まる程度の微量なスクロールが発生した
let prevTopVisible = firstTopVisible; let prevTopVisible = firstTopVisible;
const onScroll = () => { const onScroll = () => {
if (!document.body.contains(el)) return; if (!window.document.body.contains(el)) return;
const topVisible = isHeadVisible(el, tolerance); const topVisible = isHeadVisible(el, tolerance);
if (topVisible !== prevTopVisible) { if (topVisible !== prevTopVisible) {
@ -78,7 +78,7 @@ export function onScrollBottom(el: HTMLElement, cb: () => unknown, tolerance = 1
const containerOrWindow = container ?? window; const containerOrWindow = container ?? window;
const onScroll = () => { const onScroll = () => {
if (!document.body.contains(el)) return; if (!window.document.body.contains(el)) return;
if (isTailVisible(el, 1, container)) { if (isTailVisible(el, 1, container)) {
cb(); cb();
if (once) removeListener(); 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 // https://ja.javascript.info/size-and-scroll-window#ref-932
export function getBodyScrollHeight() { export function getBodyScrollHeight() {
return Math.max( return Math.max(
document.body.scrollHeight, document.documentElement.scrollHeight, window.document.body.scrollHeight, window.document.documentElement.scrollHeight,
document.body.offsetHeight, document.documentElement.offsetHeight, window.document.body.offsetHeight, window.document.documentElement.offsetHeight,
document.body.clientHeight, document.documentElement.clientHeight, window.document.body.clientHeight, window.document.documentElement.clientHeight,
); );
} }

View File

@ -7,18 +7,18 @@ import { onMounted, onUnmounted, ref } from 'vue';
import type { Ref } from 'vue'; import type { Ref } from 'vue';
export function useDocumentVisibility(): Ref<DocumentVisibilityState> { export function useDocumentVisibility(): Ref<DocumentVisibilityState> {
const visibility = ref(document.visibilityState); const visibility = ref(window.document.visibilityState);
const onChange = (): void => { const onChange = (): void => {
visibility.value = document.visibilityState; visibility.value = window.document.visibilityState;
}; };
onMounted(() => { onMounted(() => {
document.addEventListener('visibilitychange', onChange); window.document.addEventListener('visibilitychange', onChange);
}); });
onUnmounted(() => { onUnmounted(() => {
document.removeEventListener('visibilitychange', onChange); window.document.removeEventListener('visibilitychange', onChange);
}); });
return visibility; return visibility;

View File

@ -21,11 +21,11 @@
"lint": "pnpm typecheck && pnpm eslint" "lint": "pnpm typecheck && pnpm eslint"
}, },
"devDependencies": { "devDependencies": {
"@types/node": "22.18.1", "@types/node": "22.18.6",
"@typescript-eslint/eslint-plugin": "8.42.0", "@typescript-eslint/eslint-plugin": "8.44.1",
"@typescript-eslint/parser": "8.42.0", "@typescript-eslint/parser": "8.44.1",
"esbuild": "0.25.9", "esbuild": "0.25.10",
"eslint-plugin-vue": "10.4.0", "eslint-plugin-vue": "10.5.0",
"nodemon": "3.1.10", "nodemon": "3.1.10",
"typescript": "5.9.2", "typescript": "5.9.2",
"vue-eslint-parser": "10.2.0" "vue-eslint-parser": "10.2.0"
@ -35,6 +35,6 @@
], ],
"dependencies": { "dependencies": {
"misskey-js": "workspace:*", "misskey-js": "workspace:*",
"vue": "3.5.21" "vue": "3.5.22"
} }
} }

View File

@ -24,12 +24,12 @@
"@rollup/plugin-json": "6.1.0", "@rollup/plugin-json": "6.1.0",
"@rollup/plugin-replace": "6.0.2", "@rollup/plugin-replace": "6.0.2",
"@rollup/pluginutils": "5.3.0", "@rollup/pluginutils": "5.3.0",
"@sentry/vue": "10.10.0", "@sentry/vue": "10.15.0",
"@syuilo/aiscript": "1.1.0", "@syuilo/aiscript": "1.1.2",
"@syuilo/aiscript-0-19-0": "npm:@syuilo/aiscript@^0.19.0", "@syuilo/aiscript-0-19-0": "npm:@syuilo/aiscript@^0.19.0",
"@twemoji/parser": "16.0.0", "@twemoji/parser": "16.0.0",
"@vitejs/plugin-vue": "6.0.1", "@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", "aiscript-vscode": "github:aiscript-dev/aiscript-vscode#v0.1.15",
"analytics": "0.8.19", "analytics": "0.8.19",
"astring": "1.9.0", "astring": "1.9.0",
@ -41,7 +41,7 @@
"chartjs-chart-matrix": "3.0.0", "chartjs-chart-matrix": "3.0.0",
"chartjs-plugin-gradient": "0.6.1", "chartjs-plugin-gradient": "0.6.1",
"chartjs-plugin-zoom": "2.2.0", "chartjs-plugin-zoom": "2.2.0",
"chromatic": "13.1.4", "chromatic": "13.2.1",
"compare-versions": "6.1.1", "compare-versions": "6.1.1",
"cropperjs": "2.0.1", "cropperjs": "2.0.1",
"date-fns": "4.1.0", "date-fns": "4.1.0",
@ -52,21 +52,24 @@
"icons-subsetter": "workspace:*", "icons-subsetter": "workspace:*",
"idb-keyval": "6.2.2", "idb-keyval": "6.2.2",
"insert-text-at-cursor": "0.3.0", "insert-text-at-cursor": "0.3.0",
"ios-haptics": "0.1.0", "ios-haptics": "0.1.4",
"is-file-animated": "1.0.2", "is-file-animated": "1.0.2",
"json5": "2.2.3", "json5": "2.2.3",
"magic-string": "0.30.18", "magic-string": "0.30.19",
"matter-js": "0.20.0", "matter-js": "0.20.0",
"mediabunny": "1.21.0",
"mfm-js": "0.25.0", "mfm-js": "0.25.0",
"misskey-bubble-game": "workspace:*", "misskey-bubble-game": "workspace:*",
"misskey-js": "workspace:*", "misskey-js": "workspace:*",
"misskey-reversi": "workspace:*", "misskey-reversi": "workspace:*",
"photoswipe": "5.4.4", "photoswipe": "5.4.4",
"punycode.js": "2.3.1", "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", "sanitize-html": "2.17.0",
"sass": "1.92.1", "sass": "1.93.2",
"shiki": "3.12.2", "shiki": "3.13.0",
"strict-event-emitter-types": "2.0.0", "strict-event-emitter-types": "2.0.0",
"textarea-caret": "3.1.0", "textarea-caret": "3.1.0",
"three": "0.180.0", "three": "0.180.0",
@ -76,8 +79,8 @@
"tsconfig-paths": "4.2.0", "tsconfig-paths": "4.2.0",
"typescript": "5.9.2", "typescript": "5.9.2",
"v-code-diff": "1.13.1", "v-code-diff": "1.13.1",
"vite": "7.1.4", "vite": "7.1.7",
"vue": "3.5.21", "vue": "3.5.22",
"vuedraggable": "next", "vuedraggable": "next",
"wanakana": "5.3.1" "wanakana": "5.3.1"
}, },
@ -85,7 +88,7 @@
"@misskey-dev/summaly": "5.2.3", "@misskey-dev/summaly": "5.2.3",
"@storybook/addon-essentials": "8.6.14", "@storybook/addon-essentials": "8.6.14",
"@storybook/addon-interactions": "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-mdx-gfm": "8.6.14",
"@storybook/addon-storysource": "8.6.14", "@storybook/addon-storysource": "8.6.14",
"@storybook/blocks": "8.6.14", "@storybook/blocks": "8.6.14",
@ -93,57 +96,57 @@
"@storybook/core-events": "8.6.14", "@storybook/core-events": "8.6.14",
"@storybook/manager-api": "8.6.14", "@storybook/manager-api": "8.6.14",
"@storybook/preview-api": "8.6.14", "@storybook/preview-api": "8.6.14",
"@storybook/react": "9.1.5", "@storybook/react": "9.1.8",
"@storybook/react-vite": "9.1.5", "@storybook/react-vite": "9.1.8",
"@storybook/test": "8.6.14", "@storybook/test": "8.6.14",
"@storybook/theming": "8.6.14", "@storybook/theming": "8.6.14",
"@storybook/types": "8.6.14", "@storybook/types": "8.6.14",
"@storybook/vue3": "9.1.5", "@storybook/vue3": "9.1.8",
"@storybook/vue3-vite": "9.1.5", "@storybook/vue3-vite": "9.1.8",
"@tabler/icons-webfont": "3.34.1", "@tabler/icons-webfont": "3.35.0",
"@testing-library/vue": "8.1.0", "@testing-library/vue": "8.1.0",
"@types/canvas-confetti": "1.9.0", "@types/canvas-confetti": "1.9.0",
"@types/estree": "1.0.8", "@types/estree": "1.0.8",
"@types/matter-js": "0.20.0", "@types/matter-js": "0.20.2",
"@types/micromatch": "4.0.9", "@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/punycode.js": "npm:@types/punycode@2.1.4",
"@types/sanitize-html": "2.16.0", "@types/sanitize-html": "2.16.0",
"@types/seedrandom": "3.0.8", "@types/seedrandom": "3.0.8",
"@types/throttle-debounce": "5.0.2", "@types/throttle-debounce": "5.0.2",
"@types/tinycolor2": "1.4.6", "@types/tinycolor2": "1.4.6",
"@types/ws": "8.18.1", "@types/ws": "8.18.1",
"@typescript-eslint/eslint-plugin": "8.42.0", "@typescript-eslint/eslint-plugin": "8.44.1",
"@typescript-eslint/parser": "8.42.0", "@typescript-eslint/parser": "8.44.1",
"@vitest/coverage-v8": "3.2.4", "@vitest/coverage-v8": "3.2.4",
"@vue/compiler-core": "3.5.21", "@vue/compiler-core": "3.5.22",
"@vue/runtime-core": "3.5.21", "@vue/runtime-core": "3.5.22",
"acorn": "8.15.0", "acorn": "8.15.0",
"cross-env": "10.0.0", "cross-env": "10.0.0",
"cypress": "14.5.4", "cypress": "14.5.4",
"eslint-plugin-import": "2.32.0", "eslint-plugin-import": "2.32.0",
"eslint-plugin-vue": "10.4.0", "eslint-plugin-vue": "10.5.0",
"fast-glob": "3.3.3", "fast-glob": "3.3.3",
"happy-dom": "18.0.1", "happy-dom": "18.0.1",
"intersection-observer": "0.12.2", "intersection-observer": "0.12.2",
"micromatch": "4.0.8", "micromatch": "4.0.8",
"minimatch": "10.0.3", "minimatch": "10.0.3",
"msw": "2.11.1", "msw": "2.11.3",
"msw-storybook-addon": "2.0.5", "msw-storybook-addon": "2.0.5",
"nodemon": "3.1.10", "nodemon": "3.1.10",
"prettier": "3.6.2", "prettier": "3.6.2",
"react": "19.1.1", "react": "19.1.1",
"react-dom": "19.1.1", "react-dom": "19.1.1",
"seedrandom": "3.0.5", "seedrandom": "3.0.5",
"start-server-and-test": "2.1.0", "start-server-and-test": "2.1.2",
"storybook": "9.1.5", "storybook": "9.1.8",
"storybook-addon-misskey-theme": "github:misskey-dev/storybook-addon-misskey-theme", "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", "vite-plugin-turbosnap": "1.0.3",
"vitest": "3.2.4", "vitest": "3.2.4",
"vitest-fetch-mock": "0.4.5", "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-eslint-parser": "10.2.0",
"vue-tsc": "3.0.6" "vue-tsc": "3.0.8"
} }
} }

View File

@ -151,7 +151,21 @@ export async function common(createVue: () => Promise<App<Element>>) {
} }
//#endregion //#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: この処理は必ずクライアント更新チェック処理より後に来ること(テーマ再構築のため)
// NOTE: この処理は必ずダークモード判定処理より後に来ること(初回のテーマ適用のため)
// see: https://github.com/misskey-dev/misskey/issues/16562
watch(store.r.darkMode, (darkMode) => { watch(store.r.darkMode, (darkMode) => {
const theme = (() => { const theme = (() => {
if (darkMode) { if (darkMode) {
@ -183,18 +197,6 @@ export async function common(createVue: () => Promise<App<Element>>) {
}); });
} }
//#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 (!isSafeMode) {
if (prefer.s.darkTheme && store.s.darkMode) { if (prefer.s.darkTheme && store.s.darkMode) {
if (miLocalStorage.getItem('themeId') !== prefer.s.darkTheme.id) applyTheme(prefer.s.darkTheme); if (miLocalStorage.getItem('themeId') !== prefer.s.darkTheme.id) applyTheme(prefer.s.darkTheme);

View File

@ -10,6 +10,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<script lang="ts" setup> <script lang="ts" setup>
import { onMounted, onUnmounted, useTemplateRef } from 'vue'; import { onMounted, onUnmounted, useTemplateRef } from 'vue';
import isChromatic from 'chromatic/isChromatic'; import isChromatic from 'chromatic/isChromatic';
import { initShaderProgram } from '@/utility/webgl.js';
const canvasEl = useTemplateRef('canvasEl'); const canvasEl = useTemplateRef('canvasEl');
@ -21,47 +22,6 @@ const props = withDefaults(defineProps<{
focus: 1.0, focus: 1.0,
}); });
function loadShader(gl: WebGLRenderingContext, type: number, source: string) {
const shader = gl.createShader(type);
if (shader == null) return null;
gl.shaderSource(shader, source);
gl.compileShader(shader);
if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) {
alert(
`falied to compile shader: ${gl.getShaderInfoLog(shader)}`,
);
gl.deleteShader(shader);
return null;
}
return shader;
}
function initShaderProgram(gl: WebGLRenderingContext, vsSource: string, fsSource: string) {
const vertexShader = loadShader(gl, gl.VERTEX_SHADER, vsSource);
const fragmentShader = loadShader(gl, gl.FRAGMENT_SHADER, fsSource);
const shaderProgram = gl.createProgram();
if (vertexShader == null || fragmentShader == null) return null;
gl.attachShader(shaderProgram, vertexShader);
gl.attachShader(shaderProgram, fragmentShader);
gl.linkProgram(shaderProgram);
if (!gl.getProgramParameter(shaderProgram, gl.LINK_STATUS)) {
alert(
`failed to init shader: ${gl.getProgramInfoLog(
shaderProgram,
)}`,
);
return null;
}
return shaderProgram;
}
let handle: ReturnType<typeof window['requestAnimationFrame']> | null = null; let handle: ReturnType<typeof window['requestAnimationFrame']> | null = null;
onMounted(() => { onMounted(() => {
@ -71,7 +31,7 @@ onMounted(() => {
canvas.width = width; canvas.width = width;
canvas.height = height; canvas.height = height;
const maybeGl = canvas.getContext('webgl', { premultipliedAlpha: true }); const maybeGl = canvas.getContext('webgl2', { premultipliedAlpha: true });
if (maybeGl == null) return; if (maybeGl == null) return;
const gl = maybeGl; const gl = maybeGl;
@ -82,18 +42,16 @@ onMounted(() => {
const positionBuffer = gl.createBuffer(); const positionBuffer = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, positionBuffer); gl.bindBuffer(gl.ARRAY_BUFFER, positionBuffer);
const shaderProgram = initShaderProgram(gl, ` const shaderProgram = initShaderProgram(gl, `#version 300 es
attribute vec2 vertex; in vec2 position;
uniform vec2 u_scale; uniform vec2 u_scale;
out vec2 in_uv;
varying vec2 v_pos;
void main() { void main() {
gl_Position = vec4(vertex, 0.0, 1.0); gl_Position = vec4(position, 0.0, 1.0);
v_pos = vertex / u_scale; in_uv = position / u_scale;
} }
`, ` `, `#version 300 es
precision mediump float; precision mediump float;
vec3 mod289(vec3 x) { vec3 mod289(vec3 x) {
@ -143,6 +101,7 @@ onMounted(() => {
return 130.0 * dot(m, g); return 130.0 * dot(m, g);
} }
in vec2 in_uv;
uniform float u_time; uniform float u_time;
uniform vec2 u_resolution; uniform vec2 u_resolution;
uniform float u_spread; uniform float u_spread;
@ -150,8 +109,7 @@ onMounted(() => {
uniform float u_warp; uniform float u_warp;
uniform float u_focus; uniform float u_focus;
uniform float u_itensity; uniform float u_itensity;
out vec4 out_color;
varying vec2 v_pos;
float circle( in vec2 _pos, in vec2 _origin, in float _radius ) { float circle( in vec2 _pos, in vec2 _origin, in float _radius ) {
float SPREAD = 0.7 * u_spread; float SPREAD = 0.7 * u_spread;
@ -182,13 +140,13 @@ onMounted(() => {
float ratio = u_resolution.x / u_resolution.y; float ratio = u_resolution.x / u_resolution.y;
vec2 uv = vec2( v_pos.x, v_pos.y / ratio ) * 0.5 + 0.5; vec2 uv = vec2( in_uv.x, in_uv.y / ratio ) * 0.5 + 0.5;
vec3 color = vec3( 0.0 ); vec3 color = vec3( 0.0 );
float greenMix = snoise( v_pos * 1.31 + u_time * 0.8 * 0.00017 ) * 0.5 + 0.5; float greenMix = snoise( in_uv * 1.31 + u_time * 0.8 * 0.00017 ) * 0.5 + 0.5;
float purpleMix = snoise( v_pos * 1.26 + u_time * 0.8 * -0.0001 ) * 0.5 + 0.5; float purpleMix = snoise( in_uv * 1.26 + u_time * 0.8 * -0.0001 ) * 0.5 + 0.5;
float orangeMix = snoise( v_pos * 1.34 + u_time * 0.8 * 0.00015 ) * 0.5 + 0.5; float orangeMix = snoise( in_uv * 1.34 + u_time * 0.8 * 0.00015 ) * 0.5 + 0.5;
float alphaOne = 0.35 + 0.65 * pow( snoise( vec2( u_time * 0.00012, uv.x ) ) * 0.5 + 0.5, 1.2 ); float alphaOne = 0.35 + 0.65 * pow( snoise( vec2( u_time * 0.00012, uv.x ) ) * 0.5 + 0.5, 1.2 );
float alphaTwo = 0.35 + 0.65 * pow( snoise( vec2( ( u_time + 1561.0 ) * 0.00014, uv.x ) ) * 0.5 + 0.5, 1.2 ); float alphaTwo = 0.35 + 0.65 * pow( snoise( vec2( ( u_time + 1561.0 ) * 0.00014, uv.x ) ) * 0.5 + 0.5, 1.2 );
@ -198,10 +156,10 @@ onMounted(() => {
color += vec3( circle( uv, vec2( 0.90 + cos( u_time * 0.000166 ) * 0.06, 0.42 + sin( u_time * 0.000138 ) * 0.06 ), 0.18 ) ) * alphaTwo * ( green * greenMix + purple * purpleMix ); color += vec3( circle( uv, vec2( 0.90 + cos( u_time * 0.000166 ) * 0.06, 0.42 + sin( u_time * 0.000138 ) * 0.06 ), 0.18 ) ) * alphaTwo * ( green * greenMix + purple * purpleMix );
color += vec3( circle( uv, vec2( 0.19 + sin( u_time * 0.000112 ) * 0.06, 0.25 + sin( u_time * 0.000192 ) * 0.06 ), 0.09 ) ) * alphaThree * ( orange * orangeMix ); color += vec3( circle( uv, vec2( 0.19 + sin( u_time * 0.000112 ) * 0.06, 0.25 + sin( u_time * 0.000192 ) * 0.06 ), 0.09 ) ) * alphaThree * ( orange * orangeMix );
color *= u_itensity + 1.0 * pow( snoise( vec2( v_pos.y + u_time * 0.00013, v_pos.x + u_time * -0.00009 ) ) * 0.5 + 0.5, 2.0 ); color *= u_itensity + 1.0 * pow( snoise( vec2( in_uv.y + u_time * 0.00013, in_uv.x + u_time * -0.00009 ) ) * 0.5 + 0.5, 2.0 );
vec3 inverted = vec3( 1.0 ) - color; vec3 inverted = vec3( 1.0 ) - color;
gl_FragColor = vec4( color, max(max(color.x, color.y), color.z) ); out_color = vec4(color, max(max(color.x, color.y), color.z));
} }
`); `);
if (shaderProgram == null) return; if (shaderProgram == null) return;
@ -223,7 +181,7 @@ onMounted(() => {
gl.uniform1f(u_itensity, 0.5); gl.uniform1f(u_itensity, 0.5);
gl.uniform2fv(u_scale, [props.scale, props.scale]); gl.uniform2fv(u_scale, [props.scale, props.scale]);
const vertex = gl.getAttribLocation(shaderProgram, 'vertex'); const vertex = gl.getAttribLocation(shaderProgram, 'position');
gl.enableVertexAttribArray(vertex); gl.enableVertexAttribArray(vertex);
gl.vertexAttribPointer(vertex, 2, gl.FLOAT, false, 0, 0); gl.vertexAttribPointer(vertex, 2, gl.FLOAT, false, 0, 0);

View File

@ -10,17 +10,11 @@ SPDX-License-Identifier: AGPL-3.0-only
<MkInput v-model="name"> <MkInput v-model="name">
<template #label>{{ i18n.ts.name }}</template> <template #label>{{ i18n.ts.name }}</template>
</MkInput> </MkInput>
<MkSelect v-model="src"> <MkSelect v-model="src" :items="antennaSourcesSelectDef">
<template #label>{{ i18n.ts.antennaSource }}</template> <template #label>{{ i18n.ts.antennaSource }}</template>
<option value="all">{{ i18n.ts._antennaSources.all }}</option>
<!--<option value="home">{{ i18n.ts._antennaSources.homeTimeline }}</option>-->
<option value="users">{{ i18n.ts._antennaSources.users }}</option>
<!--<option value="list">{{ i18n.ts._antennaSources.userList }}</option>-->
<option value="users_blacklist">{{ i18n.ts._antennaSources.userBlacklist }}</option>
</MkSelect> </MkSelect>
<MkSelect v-if="src === 'list'" v-model="userListId"> <MkSelect v-if="src === 'list'" v-model="userListId" :items="userListsSelectDef">
<template #label>{{ i18n.ts.userList }}</template> <template #label>{{ i18n.ts.userList }}</template>
<option v-for="list in userLists" :key="list.id" :value="list.id">{{ list.name }}</option>
</MkSelect> </MkSelect>
<MkTextarea v-else-if="src === 'users' || src === 'users_blacklist'" v-model="users"> <MkTextarea v-else-if="src === 'users' || src === 'users_blacklist'" v-model="users">
<template #label>{{ i18n.ts.users }}</template> <template #label>{{ i18n.ts.users }}</template>
@ -52,7 +46,7 @@ SPDX-License-Identifier: AGPL-3.0-only
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import { watch, ref } from 'vue'; import { watch, ref, computed } from 'vue';
import * as Misskey from 'misskey-js'; import * as Misskey from 'misskey-js';
import type { DeepPartial } from '@/utility/merge.js'; import type { DeepPartial } from '@/utility/merge.js';
import MkButton from '@/components/MkButton.vue'; import MkButton from '@/components/MkButton.vue';
@ -64,6 +58,7 @@ import * as os from '@/os.js';
import { misskeyApi } from '@/utility/misskey-api.js'; import { misskeyApi } from '@/utility/misskey-api.js';
import { i18n } from '@/i18n.js'; import { i18n } from '@/i18n.js';
import { deepMerge } from '@/utility/merge.js'; import { deepMerge } from '@/utility/merge.js';
import { useMkSelect } from '@/composables/use-mkselect.js';
type PartialAllowedAntenna = Omit<Misskey.entities.Antenna, 'id' | 'createdAt' | 'updatedAt'> & { type PartialAllowedAntenna = Omit<Misskey.entities.Antenna, 'id' | 'createdAt' | 'updatedAt'> & {
id?: string; id?: string;
@ -99,9 +94,35 @@ const emit = defineEmits<{
(ev: 'deleted'): void, (ev: 'deleted'): void,
}>(); }>();
const {
model: src,
def: antennaSourcesSelectDef,
} = useMkSelect({
items: [
{ value: 'all', label: i18n.ts._antennaSources.all },
//{ value: 'home', label: i18n.ts._antennaSources.homeTimeline },
{ value: 'users', label: i18n.ts._antennaSources.users },
//{ value: 'list', label: i18n.ts._antennaSources.userList },
{ value: 'users_blacklist', label: i18n.ts._antennaSources.userBlacklist },
],
initialValue: initialAntenna.src,
});
const {
model: userListId,
def: userListsSelectDef,
} = useMkSelect({
items: computed(() => {
if (userLists.value == null) return [];
return userLists.value.map(list => ({
value: list.id,
label: list.name,
}));
}),
initialValue: initialAntenna.userListId,
});
const name = ref<string>(initialAntenna.name); const name = ref<string>(initialAntenna.name);
const src = ref<Misskey.entities.AntennasCreateRequest['src']>(initialAntenna.src);
const userListId = ref<string | null>(initialAntenna.userListId);
const users = ref<string>(initialAntenna.users.join('\n')); const users = ref<string>(initialAntenna.users.join('\n'));
const keywords = ref<string>(initialAntenna.keywords.map(x => x.join(' ')).join('\n')); const keywords = ref<string>(initialAntenna.keywords.map(x => x.join(' ')).join('\n'));
const excludeKeywords = ref<string>(initialAntenna.excludeKeywords.map(x => x.join(' ')).join('\n')); const excludeKeywords = ref<string>(initialAntenna.excludeKeywords.map(x => x.join(' ')).join('\n'));

View File

@ -32,10 +32,9 @@ SPDX-License-Identifier: AGPL-3.0-only
<template v-if="c.label" #label>{{ c.label }}</template> <template v-if="c.label" #label>{{ c.label }}</template>
<template v-if="c.caption" #caption>{{ c.caption }}</template> <template v-if="c.caption" #caption>{{ c.caption }}</template>
</MkInput> </MkInput>
<MkSelect v-else-if="c.type === 'select'" :small="size === 'small'" :modelValue="valueForSelect" @update:modelValue="onSelectUpdate"> <MkSelect v-else-if="c.type === 'select'" :small="size === 'small'" :modelValue="valueForSelect" :items="selectDef" @update:modelValue="onSelectUpdate">
<template v-if="c.label" #label>{{ c.label }}</template> <template v-if="c.label" #label>{{ c.label }}</template>
<template v-if="c.caption" #caption>{{ c.caption }}</template> <template v-if="c.caption" #caption>{{ c.caption }}</template>
<option v-for="item in c.items" :key="item.value" :value="item.value">{{ item.text }}</option>
</MkSelect> </MkSelect>
<MkButton v-else-if="c.type === 'postFormButton'" :primary="c.primary" :rounded="c.rounded" :small="size === 'small'" inline @click="openPostForm">{{ c.text }}</MkButton> <MkButton v-else-if="c.type === 'postFormButton'" :primary="c.primary" :rounded="c.rounded" :small="size === 'small'" inline @click="openPostForm">{{ c.text }}</MkButton>
<div v-else-if="c.type === 'postForm'" :class="$style.postForm"> <div v-else-if="c.type === 'postForm'" :class="$style.postForm">
@ -74,6 +73,7 @@ import MkSelect from '@/components/MkSelect.vue';
import type { AsUiComponent, AsUiRoot, AsUiPostFormButton } from '@/aiscript/ui.js'; import type { AsUiComponent, AsUiRoot, AsUiPostFormButton } from '@/aiscript/ui.js';
import MkFolder from '@/components/MkFolder.vue'; import MkFolder from '@/components/MkFolder.vue';
import MkPostForm from '@/components/MkPostForm.vue'; import MkPostForm from '@/components/MkPostForm.vue';
import { useMkSelect } from '@/composables/use-mkselect.js';
const props = withDefaults(defineProps<{ const props = withDefaults(defineProps<{
component: AsUiComponent; component: AsUiComponent;
@ -130,7 +130,19 @@ function onSwitchUpdate(v: boolean) {
} }
} }
const valueForSelect = ref('default' in c && typeof c.default !== 'boolean' ? c.default ?? null : null); const {
model: valueForSelect,
def: selectDef,
} = useMkSelect({
items: computed(() => {
if (c.type !== 'select') return [];
return (c.items ?? []).map(item => ({
value: item.value,
label: item.text,
}));
}),
initialValue: (c.type === 'select' && 'default' in c && typeof c.default !== 'boolean') ? c.default ?? null : null,
});
function onSelectUpdate(v) { function onSelectUpdate(v) {
valueForSelect.value = v; valueForSelect.value = v;

View File

@ -29,6 +29,6 @@ const users = ref<Misskey.entities.UserLite[]>([]);
onMounted(async () => { onMounted(async () => {
users.value = await misskeyApi('users/show', { users.value = await misskeyApi('users/show', {
userIds: props.userIds, userIds: props.userIds,
}) as unknown as Misskey.entities.UserLite[]; });
}); });
</script> </script>

View File

@ -29,16 +29,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<span v-else-if="okButtonDisabledReason === 'charactersBelow'" v-text="i18n.tsx._dialog.charactersBelow({ current: (inputValue as string)?.length ?? 0, min: input.minLength ?? 'NaN' })"/> <span v-else-if="okButtonDisabledReason === 'charactersBelow'" v-text="i18n.tsx._dialog.charactersBelow({ current: (inputValue as string)?.length ?? 0, min: input.minLength ?? 'NaN' })"/>
</template> </template>
</MkInput> </MkInput>
<MkSelect v-if="select" v-model="selectedValue" autofocus> <MkSelect v-if="select" v-model="selectedValue" :items="selectDef" autofocus></MkSelect>
<template v-if="select.items">
<template v-for="item in select.items">
<optgroup v-if="'sectionTitle' in item" :label="item.sectionTitle">
<option v-for="subItem in item.items" :value="subItem.value">{{ subItem.text }}</option>
</optgroup>
<option v-else :value="item.value">{{ item.text }}</option>
</template>
</template>
</MkSelect>
<div v-if="(showOkButton || showCancelButton) && !actions" :class="$style.buttons"> <div v-if="(showOkButton || showCancelButton) && !actions" :class="$style.buttons">
<MkButton v-if="showOkButton" data-cy-modal-dialog-ok inline primary rounded :autofocus="!input && !select" :disabled="okButtonDisabledReason != null" @click="ok">{{ okText ?? ((showCancelButton || input || select) ? i18n.ts.ok : i18n.ts.gotIt) }}</MkButton> <MkButton v-if="showOkButton" data-cy-modal-dialog-ok inline primary rounded :autofocus="!input && !select" :disabled="okButtonDisabledReason != null" @click="ok">{{ okText ?? ((showCancelButton || input || select) ? i18n.ts.ok : i18n.ts.gotIt) }}</MkButton>
<MkButton v-if="showCancelButton || input || select" data-cy-modal-dialog-cancel inline rounded @click="cancel">{{ cancelText ?? i18n.ts.cancel }}</MkButton> <MkButton v-if="showCancelButton || input || select" data-cy-modal-dialog-cancel inline rounded @click="cancel">{{ cancelText ?? i18n.ts.cancel }}</MkButton>
@ -56,6 +47,8 @@ import MkModal from '@/components/MkModal.vue';
import MkButton from '@/components/MkButton.vue'; import MkButton from '@/components/MkButton.vue';
import MkInput from '@/components/MkInput.vue'; import MkInput from '@/components/MkInput.vue';
import MkSelect from '@/components/MkSelect.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'; import { i18n } from '@/i18n.js';
type Input = { type Input = {
@ -67,17 +60,9 @@ type Input = {
maxLength?: number; maxLength?: number;
}; };
type SelectItem = {
value: any;
text: string;
};
type Select = { type Select = {
items: (SelectItem | { items: MkSelectItem[];
sectionTitle: string; default: OptionValue | null;
items: SelectItem[];
})[];
default: string | null;
}; };
type Result = string | number | true | null; type Result = string | number | true | null;
@ -115,7 +100,6 @@ const emit = defineEmits<{
const modal = useTemplateRef('modal'); const modal = useTemplateRef('modal');
const inputValue = ref<string | number | null>(props.input?.default ?? null); const inputValue = ref<string | number | null>(props.input?.default ?? null);
const selectedValue = ref(props.select?.default ?? null);
const okButtonDisabledReason = computed<null | 'charactersExceeded' | 'charactersBelow'>(() => { const okButtonDisabledReason = computed<null | 'charactersExceeded' | 'charactersBelow'>(() => {
if (props.input) { if (props.input) {
@ -134,6 +118,14 @@ const okButtonDisabledReason = computed<null | 'charactersExceeded' | 'character
return null; return null;
}); });
const {
def: selectDef,
model: selectedValue,
} = useMkSelect({
items: computed(() => props.select?.items ?? []),
initialValue: props.select?.default ?? null,
});
// overload function 使 lint // overload function 使 lint
function done(canceled: true): void; function done(canceled: true): void;
function done(canceled: false, result: Result): void; // eslint-disable-line no-redeclare function done(canceled: false, result: Result): void; // eslint-disable-line no-redeclare

View File

@ -52,11 +52,8 @@ SPDX-License-Identifier: AGPL-3.0-only
<template #suffix>px</template> <template #suffix>px</template>
<template #caption>{{ i18n.ts._embedCodeGen.maxHeightDescription }}</template> <template #caption>{{ i18n.ts._embedCodeGen.maxHeightDescription }}</template>
</MkInput> </MkInput>
<MkSelect v-model="colorMode"> <MkSelect v-model="colorMode" :items="colorModeDef">
<template #label>{{ i18n.ts.theme }}</template> <template #label>{{ i18n.ts.theme }}</template>
<option value="auto">{{ i18n.ts.syncDeviceDarkMode }}</option>
<option value="light">{{ i18n.ts.light }}</option>
<option value="dark">{{ i18n.ts.dark }}</option>
</MkSelect> </MkSelect>
<MkSwitch v-if="isEmbedWithScrollbar" v-model="header">{{ i18n.ts._embedCodeGen.header }}</MkSwitch> <MkSwitch v-if="isEmbedWithScrollbar" v-model="header">{{ i18n.ts._embedCodeGen.header }}</MkSwitch>
<MkSwitch v-model="rounded">{{ i18n.ts._embedCodeGen.rounded }}</MkSwitch> <MkSwitch v-model="rounded">{{ i18n.ts._embedCodeGen.rounded }}</MkSwitch>
@ -105,6 +102,7 @@ import MkInfo from '@/components/MkInfo.vue';
import * as os from '@/os.js'; import * as os from '@/os.js';
import { i18n } from '@/i18n.js'; import { i18n } from '@/i18n.js';
import { useMkSelect } from '@/composables/use-mkselect.js';
import { copyToClipboard } from '@/utility/copy-to-clipboard.js'; import { copyToClipboard } from '@/utility/copy-to-clipboard.js';
import { normalizeEmbedParams, getEmbedCode } from '@/utility/get-embed-code.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 header = ref(props.params?.header ?? true);
const maxHeight = ref(props.params?.maxHeight !== 0 ? props.params?.maxHeight ?? null : 500); 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 rounded = ref(props.params?.rounded ?? true);
const border = ref(props.params?.border ?? true); const border = ref(props.params?.border ?? true);

View File

@ -530,6 +530,14 @@ defineExpose({
--eachSize: 50px; --eachSize: 50px;
} }
&.s4 {
--eachSize: 55px;
}
&.s5 {
--eachSize: 60px;
}
&.w1 { &.w1 {
width: calc((var(--eachSize) * 5) + (#{$pad} * 2)); width: calc((var(--eachSize) * 5) + (#{$pad} * 2));
--columns: 1fr 1fr 1fr 1fr 1fr; --columns: 1fr 1fr 1fr 1fr 1fr;

View File

@ -39,9 +39,8 @@ SPDX-License-Identifier: AGPL-3.0-only
<span v-text="v.label || k"></span> <span v-text="v.label || k"></span>
<template v-if="v.description" #caption>{{ v.description }}</template> <template v-if="v.description" #caption>{{ v.description }}</template>
</MkSwitch> </MkSwitch>
<MkSelect v-else-if="v.type === 'enum'" v-model="values[k]"> <MkSelect v-else-if="v.type === 'enum'" v-model="values[k]" :items="getMkSelectDef(v)">
<template #label><span v-text="v.label || k"></span><span v-if="v.required === false"> ({{ i18n.ts.optional }})</span></template> <template #label><span v-text="v.label || k"></span><span v-if="v.required === false"> ({{ i18n.ts.optional }})</span></template>
<option v-for="option in v.enum" :key="getEnumKey(option)" :value="getEnumValue(option)">{{ getEnumLabel(option) }}</option>
</MkSelect> </MkSelect>
<MkRadios v-else-if="v.type === 'radio'" v-model="values[k]"> <MkRadios v-else-if="v.type === 'radio'" v-model="values[k]">
<template #label><span v-text="v.label || k"></span><span v-if="v.required === false"> ({{ i18n.ts.optional }})</span></template> <template #label><span v-text="v.label || k"></span><span v-if="v.required === false"> ({{ i18n.ts.optional }})</span></template>
@ -77,7 +76,8 @@ import MkRange from './MkRange.vue';
import MkButton from './MkButton.vue'; import MkButton from './MkButton.vue';
import MkRadios from './MkRadios.vue'; import MkRadios from './MkRadios.vue';
import XFile from './MkFormDialog.file.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 MkModalWindow from '@/components/MkModalWindow.vue';
import { i18n } from '@/i18n.js'; import { i18n } from '@/i18n.js';
@ -120,16 +120,14 @@ function cancel() {
dialog.value?.close(); dialog.value?.close();
} }
function getEnumLabel(e: EnumItem) { function getMkSelectDef(def: EnumFormItem): MkSelectItem[] {
return typeof e === 'string' ? e : e.label; return def.enum.map((v) => {
} if (typeof v === 'string') {
return { value: v, label: v };
function getEnumValue(e: EnumItem) { } else {
return typeof e === 'string' ? e : e.value; return { value: v.value, label: v.label };
} }
});
function getEnumKey(e: EnumItem) {
return typeof e === 'string' ? e : typeof e.value === 'string' ? e.value : JSON.stringify(e.value);
} }
function getRadioKey(e: RadioFormItem['options'][number]) { function getRadioKey(e: RadioFormItem['options'][number]) {

View File

@ -19,9 +19,12 @@ SPDX-License-Identifier: AGPL-3.0-only
<div :class="$style.root"> <div :class="$style.root">
<div :class="$style.container"> <div :class="$style.container">
<div :class="$style.preview"> <div :class="$style.preview">
<canvas ref="canvasEl" :class="$style.previewCanvas"></canvas> <canvas ref="canvasEl" :class="$style.previewCanvas" @pointerdown="onImagePointerdown"></canvas>
<div :class="$style.previewContainer"> <div :class="$style.previewContainer">
<div class="_acrylic" :class="$style.previewTitle">{{ i18n.ts.preview }}</div> <div class="_acrylic" :class="$style.previewTitle">{{ i18n.ts.preview }}</div>
<div class="_acrylic" :class="$style.editControls">
<button class="_button" :class="[$style.previewControlsButton, penMode != null ? $style.active : null]" @click="showPenMenu"><i class="ti ti-pencil"></i></button>
</div>
<div class="_acrylic" :class="$style.previewControls"> <div class="_acrylic" :class="$style.previewControls">
<button class="_button" :class="[$style.previewControlsButton, !enabled ? $style.active : null]" @click="enabled = false">Before</button> <button class="_button" :class="[$style.previewControlsButton, !enabled ? $style.active : null]" @click="enabled = false">Before</button>
<button class="_button" :class="[$style.previewControlsButton, enabled ? $style.active : null]" @click="enabled = true">After</button> <button class="_button" :class="[$style.previewControlsButton, enabled ? $style.active : null]" @click="enabled = true">After</button>
@ -212,6 +215,147 @@ watch(enabled, () => {
renderer.render(); 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);
}
</script> </script>
<style module> <style module>
@ -251,6 +395,18 @@ watch(enabled, () => {
font-size: 85%; font-size: 85%;
} }
.editControls {
position: absolute;
z-index: 100;
bottom: 8px;
left: 8px;
display: flex;
align-items: center;
gap: 8px;
padding: 6px 10px;
border-radius: 6px;
}
.previewControls { .previewControls {
position: absolute; position: absolute;
z-index: 100; z-index: 100;
@ -283,9 +439,13 @@ watch(enabled, () => {
position: absolute; position: absolute;
top: 0; top: 0;
left: 0; left: 0;
width: 100%; /* iOS
height: 100%; width: stretch;
padding: 20px; height: stretch;
*/
width: calc(100% - 40px);
height: calc(100% - 40px);
margin: 20px;
box-sizing: border-box; box-sizing: border-box;
object-fit: contain; object-fit: contain;
} }

View File

@ -9,31 +9,8 @@ SPDX-License-Identifier: AGPL-3.0-only
<template #header>Chart</template> <template #header>Chart</template>
<div :class="$style.chart"> <div :class="$style.chart">
<div class="selects"> <div class="selects">
<MkSelect v-model="chartSrc" style="margin: 0; flex: 1;"> <MkSelect v-model="chartSrc" :items="chartSrcDef" style="margin: 0; flex: 1;"></MkSelect>
<optgroup v-if="shouldShowFederation" :label="i18n.ts.federation"> <MkSelect v-model="chartSpan" :items="chartSpanDef" style="margin: 0 0 0 10px;"></MkSelect>
<option value="federation">{{ i18n.ts._charts.federation }}</option>
<option value="ap-request">{{ i18n.ts._charts.apRequest }}</option>
</optgroup>
<optgroup :label="i18n.ts.users">
<option value="users">{{ i18n.ts._charts.usersIncDec }}</option>
<option value="users-total">{{ i18n.ts._charts.usersTotal }}</option>
<option value="active-users">{{ i18n.ts._charts.activeUsers }}</option>
</optgroup>
<optgroup :label="i18n.ts.notes">
<option value="notes">{{ i18n.ts._charts.notesIncDec }}</option>
<option value="local-notes">{{ i18n.ts._charts.localNotesIncDec }}</option>
<option v-if="shouldShowFederation" value="remote-notes">{{ i18n.ts._charts.remoteNotesIncDec }}</option>
<option value="notes-total">{{ i18n.ts._charts.notesTotal }}</option>
</optgroup>
<optgroup :label="i18n.ts.drive">
<option value="drive-files">{{ i18n.ts._charts.filesIncDec }}</option>
<option value="drive">{{ i18n.ts._charts.storageUsageIncDec }}</option>
</optgroup>
</MkSelect>
<MkSelect v-model="chartSpan" style="margin: 0 0 0 10px;">
<option value="hour">{{ i18n.ts.perHour }}</option>
<option value="day">{{ i18n.ts.perDay }}</option>
</MkSelect>
</div> </div>
<div class="chart _panel"> <div class="chart _panel">
<MkChart :src="chartSrc" :span="chartSpan" :limit="chartLimit" :detailed="true"></MkChart> <MkChart :src="chartSrc" :span="chartSpan" :limit="chartLimit" :detailed="true"></MkChart>
@ -43,13 +20,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<MkFoldableSection class="item"> <MkFoldableSection class="item">
<template #header>Active users heatmap</template> <template #header>Active users heatmap</template>
<MkSelect v-model="heatmapSrc" style="margin: 0 0 12px 0;"> <MkSelect v-model="heatmapSrc" :items="heatmapSrcDef" style="margin: 0 0 12px 0;"></MkSelect>
<option value="active-users">Active users</option>
<option value="notes">Notes</option>
<option v-if="shouldShowFederation" value="ap-requests-inbox-received">AP Requests: inboxReceived</option>
<option v-if="shouldShowFederation" value="ap-requests-deliver-succeeded">AP Requests: deliverSucceeded</option>
<option v-if="shouldShowFederation" value="ap-requests-deliver-failed">AP Requests: deliverFailed</option>
</MkSelect>
<div class="_panel" :class="$style.heatmap"> <div class="_panel" :class="$style.heatmap">
<MkHeatmap :src="heatmapSrc" :label="'Read & Write'"/> <MkHeatmap :src="heatmapSrc" :label="'Read & Write'"/>
</div> </div>
@ -84,10 +55,10 @@ SPDX-License-Identifier: AGPL-3.0-only
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import { onMounted, ref, computed, useTemplateRef } from 'vue'; import { onMounted, computed, useTemplateRef } from 'vue';
import { Chart } from 'chart.js'; import { Chart } from 'chart.js';
import type { HeatmapSource } from '@/components/MkHeatmap.vue';
import MkSelect from '@/components/MkSelect.vue'; import MkSelect from '@/components/MkSelect.vue';
import type { MkSelectItem, ItemOption } from '@/components/MkSelect.vue';
import MkChart from '@/components/MkChart.vue'; import MkChart from '@/components/MkChart.vue';
import type { ChartSrc } from '@/components/MkChart.vue'; import type { ChartSrc } from '@/components/MkChart.vue';
import { useChartTooltip } from '@/composables/use-chart-tooltip.js'; import { useChartTooltip } from '@/composables/use-chart-tooltip.js';
@ -101,15 +72,96 @@ import MkFoldableSection from '@/components/MkFoldableSection.vue';
import MkRetentionHeatmap from '@/components/MkRetentionHeatmap.vue'; import MkRetentionHeatmap from '@/components/MkRetentionHeatmap.vue';
import MkRetentionLineChart from '@/components/MkRetentionLineChart.vue'; import MkRetentionLineChart from '@/components/MkRetentionLineChart.vue';
import { initChart } from '@/utility/init-chart.js'; import { initChart } from '@/utility/init-chart.js';
import { useMkSelect } from '@/composables/use-mkselect.js';
initChart(); initChart();
const shouldShowFederation = computed(() => instance.federation !== 'none' || $i?.isModerator); const shouldShowFederation = computed(() => instance.federation !== 'none' || $i?.isModerator);
const chartLimit = 500; const chartLimit = 500;
const chartSpan = ref<'hour' | 'day'>('hour'); const {
const chartSrc = ref<ChartSrc>('active-users'); model: chartSpan,
const heatmapSrc = ref<HeatmapSource>('active-users'); def: chartSpanDef,
} = useMkSelect({
items: [
{ value: 'hour', label: i18n.ts.perHour },
{ value: 'day', label: i18n.ts.perDay },
],
initialValue: 'hour',
});
const {
model: chartSrc,
def: chartSrcDef,
} = useMkSelect({
items: computed<MkSelectItem<ChartSrc>[]>(() => {
const items: MkSelectItem<ChartSrc>[] = [];
if (shouldShowFederation.value) {
items.push({
type: 'group',
label: i18n.ts.federation,
items: [
{ value: 'federation', label: i18n.ts._charts.federation },
{ value: 'ap-request', label: i18n.ts._charts.apRequest },
],
});
}
items.push({
type: 'group',
label: i18n.ts.users,
items: [
{ value: 'users', label: i18n.ts._charts.usersIncDec },
{ value: 'users-total', label: i18n.ts._charts.usersTotal },
{ value: 'active-users', label: i18n.ts._charts.activeUsers },
],
});
const notesItems: ItemOption<ChartSrc>[] = [
{ value: 'notes', label: i18n.ts._charts.notesIncDec },
{ value: 'local-notes', label: i18n.ts._charts.localNotesIncDec },
];
if (shouldShowFederation.value) notesItems.push({ value: 'remote-notes', label: i18n.ts._charts.remoteNotesIncDec });
notesItems.push(
{ value: 'notes-total', label: i18n.ts._charts.notesTotal },
);
items.push({
type: 'group',
label: i18n.ts.notes,
items: notesItems,
});
items.push({
type: 'group',
label: i18n.ts.drive,
items: [
{ value: 'drive-files', label: i18n.ts._charts.filesIncDec },
{ value: 'drive', label: i18n.ts._charts.storageUsageIncDec },
],
});
return items;
}),
initialValue: 'active-users',
});
const {
model: heatmapSrc,
def: heatmapSrcDef,
} = useMkSelect({
items: computed(() => [
{ value: 'active-users' as const, label: 'Active Users' },
{ value: 'notes' as const, label: 'Notes' },
...(shouldShowFederation.value ? [
{ value: 'ap-requests-inbox-received' as const, label: 'AP Requests: inboxReceived' },
{ value: 'ap-requests-deliver-succeeded' as const, label: 'AP Requests: deliverSucceeded' },
{ value: 'ap-requests-deliver-failed' as const, label: 'AP Requests: deliverFailed' },
] : []),
]),
initialValue: 'active-users',
});
const subDoughnutEl = useTemplateRef('subDoughnutEl'); const subDoughnutEl = useTemplateRef('subDoughnutEl');
const pubDoughnutEl = useTemplateRef('pubDoughnutEl'); const pubDoughnutEl = useTemplateRef('pubDoughnutEl');

View File

@ -15,101 +15,151 @@ SPDX-License-Identifier: AGPL-3.0-only
@esc="cancel()" @esc="cancel()"
> >
<template #header> <template #header>
{{ i18n.ts.drafts }} ({{ currentDraftsCount }}/{{ $i?.policies.noteDraftLimit }}) {{ i18n.ts.draftsAndScheduledNotes }} ({{ currentDraftsCount }}/{{ $i?.policies.noteDraftLimit }})
</template> </template>
<div class="_spacer">
<MkPagination :paginator="paginator" withControl>
<template #empty>
<MkResult type="empty" :text="i18n.ts._drafts.noDrafts"/>
</template>
<template #default="{ items }"> <MkStickyContainer>
<div class="_gaps_s"> <template #header>
<div <MkTabs
v-for="draft in (items as unknown as Misskey.entities.NoteDraft[])" v-model:tab="tab"
:key="draft.id" centered
v-panel :class="$style.tabs"
:class="[$style.draft]" :tabs="[
> {
<div :class="$style.draftBody" class="_gaps_s"> key: 'drafts',
<div :class="$style.draftInfo"> title: i18n.ts.drafts,
<div :class="$style.draftMeta"> icon: 'ti ti-pencil-question',
<div v-if="draft.reply" class="_nowrap"> },
<i class="ti ti-arrow-back-up"></i> <I18n :src="i18n.ts._drafts.replyTo" tag="span"> {
<template #user> key: 'scheduled',
<Mfm v-if="draft.reply.user.name != null" :text="draft.reply.user.name" :plain="true" :nowrap="true"/> title: i18n.ts.scheduled,
<MkAcct v-else :user="draft.reply.user"/> icon: 'ti ti-calendar-clock',
</template> },
</I18n> ]"
</div> />
<div v-else-if="draft.replyId" class="_nowrap"> </template>
<i class="ti ti-arrow-back-up"></i> <I18n :src="i18n.ts._drafts.replyTo" tag="span">
<template #user> <div class="_spacer">
{{ i18n.ts.deletedNote }} <MkPagination :key="tab" :paginator="tab === 'scheduled' ? scheduledPaginator : draftsPaginator" withControl>
</template> <template #empty>
</I18n> <MkResult type="empty" :text="i18n.ts._drafts.noDrafts"/>
</div> </template>
<div v-if="draft.renote && draft.text != null" class="_nowrap">
<i class="ti ti-quote"></i> <I18n :src="i18n.ts._drafts.quoteOf" tag="span"> <template #default="{ items }">
<template #user> <div class="_gaps_s">
<Mfm v-if="draft.renote.user.name != null" :text="draft.renote.user.name" :plain="true" :nowrap="true"/> <div
<MkAcct v-else :user="draft.renote.user"/> v-for="draft in (items as unknown as Misskey.entities.NoteDraft[])"
</template> :key="draft.id"
</I18n> v-panel
</div> :class="[$style.draft]"
<div v-else-if="draft.renoteId" class="_nowrap"> >
<i class="ti ti-quote"></i> <I18n :src="i18n.ts._drafts.quoteOf" tag="span"> <div :class="$style.draftBody" class="_gaps_s">
<template #user> <MkInfo v-if="draft.scheduledAt != null && draft.isActuallyScheduled">
{{ i18n.ts.deletedNote }} <I18n :src="i18n.ts.scheduledToPostOnX" tag="span">
</template> <template #x>
</I18n> <MkTime :time="draft.scheduledAt" :mode="'detail'" style="font-weight: bold;"/>
</div> </template>
<div v-if="draft.channel" class="_nowrap"> </I18n>
<i class="ti ti-device-tv"></i> {{ i18n.tsx._drafts.postTo({ channel: draft.channel.name }) }} </MkInfo>
<div :class="$style.draftInfo">
<div :class="$style.draftMeta">
<div v-if="draft.reply" class="_nowrap">
<i class="ti ti-arrow-back-up"></i> <I18n :src="i18n.ts._drafts.replyTo" tag="span">
<template #user>
<Mfm v-if="draft.reply.user.name != null" :text="draft.reply.user.name" :plain="true" :nowrap="true"/>
<MkAcct v-else :user="draft.reply.user"/>
</template>
</I18n>
</div>
<div v-else-if="draft.replyId" class="_nowrap">
<i class="ti ti-arrow-back-up"></i> <I18n :src="i18n.ts._drafts.replyTo" tag="span">
<template #user>
{{ i18n.ts.deletedNote }}
</template>
</I18n>
</div>
<div v-if="draft.renote && draft.text != null" class="_nowrap">
<i class="ti ti-quote"></i> <I18n :src="i18n.ts._drafts.quoteOf" tag="span">
<template #user>
<Mfm v-if="draft.renote.user.name != null" :text="draft.renote.user.name" :plain="true" :nowrap="true"/>
<MkAcct v-else :user="draft.renote.user"/>
</template>
</I18n>
</div>
<div v-else-if="draft.renoteId" class="_nowrap">
<i class="ti ti-quote"></i> <I18n :src="i18n.ts._drafts.quoteOf" tag="span">
<template #user>
{{ i18n.ts.deletedNote }}
</template>
</I18n>
</div>
<div v-if="draft.channel" class="_nowrap">
<i class="ti ti-device-tv"></i> {{ i18n.tsx._drafts.postTo({ channel: draft.channel.name }) }}
</div>
</div> </div>
</div> </div>
</div> <div :class="$style.draftContent">
<div :class="$style.draftContent"> <Mfm :text="getNoteSummary(draft, { showRenote: false, showReply: false })" :plain="true" :author="draft.user"/>
<Mfm :text="getNoteSummary(draft, { showRenote: false, showReply: false })" :plain="true" :author="draft.user"/> </div>
</div> <div :class="$style.draftFooter">
<div :class="$style.draftFooter"> <div :class="$style.draftVisibility">
<div :class="$style.draftVisibility"> <span :title="i18n.ts._visibility[draft.visibility]">
<span :title="i18n.ts._visibility[draft.visibility]"> <i v-if="draft.visibility === 'public'" class="ti ti-world"></i>
<i v-if="draft.visibility === 'public'" class="ti ti-world"></i> <i v-else-if="draft.visibility === 'home'" class="ti ti-home"></i>
<i v-else-if="draft.visibility === 'home'" class="ti ti-home"></i> <i v-else-if="draft.visibility === 'followers'" class="ti ti-lock"></i>
<i v-else-if="draft.visibility === 'followers'" class="ti ti-lock"></i> <i v-else-if="draft.visibility === 'specified'" class="ti ti-mail"></i>
<i v-else-if="draft.visibility === 'specified'" class="ti ti-mail"></i> </span>
</span> <span v-if="draft.localOnly" :title="i18n.ts._visibility['disableFederation']"><i class="ti ti-rocket-off"></i></span>
<span v-if="draft.localOnly" :title="i18n.ts._visibility['disableFederation']"><i class="ti ti-rocket-off"></i></span> </div>
<MkTime :time="draft.createdAt" :class="$style.draftCreatedAt" mode="detail" colored/>
</div> </div>
<MkTime :time="draft.createdAt" :class="$style.draftCreatedAt" mode="detail" colored/>
</div> </div>
</div>
<div :class="$style.draftActions" class="_buttons"> <div :class="$style.draftActions" class="_buttons">
<MkButton <template v-if="draft.scheduledAt != null && draft.isActuallyScheduled">
:class="$style.itemButton" <MkButton
small :class="$style.itemButton"
@click="restoreDraft(draft)" small
> @click="cancelSchedule(draft)"
<i class="ti ti-corner-up-left"></i> >
{{ i18n.ts._drafts.restore }} <i class="ti ti-calendar-x"></i> {{ i18n.ts._drafts.cancelSchedule }}
</MkButton> </MkButton>
<MkButton <!-- TODO
v-tooltip="i18n.ts._drafts.delete" <MkButton
danger :class="$style.itemButton"
small small
:iconOnly="true" @click="reSchedule(draft)"
:class="$style.itemButton" >
@click="deleteDraft(draft)" <i class="ti ti-calendar-time"></i> {{ i18n.ts._drafts.reSchedule }}
> </MkButton>
<i class="ti ti-trash"></i> -->
</MkButton> </template>
<MkButton
v-else
:class="$style.itemButton"
small
@click="restoreDraft(draft)"
>
<i class="ti ti-corner-up-left"></i> {{ i18n.ts._drafts.restore }}
</MkButton>
<MkButton
v-tooltip="i18n.ts._drafts.delete"
danger
small
:iconOnly="true"
:class="$style.itemButton"
style="margin-left: auto;"
@click="deleteDraft(draft)"
>
<i class="ti ti-trash"></i>
</MkButton>
</div>
</div> </div>
</div> </div>
</div> </template>
</template> </MkPagination>
</MkPagination> </div>
</div> </MkStickyContainer>
</MkModalWindow> </MkModalWindow>
</template> </template>
@ -125,6 +175,12 @@ import * as os from '@/os.js';
import { $i } from '@/i.js'; import { $i } from '@/i.js';
import { misskeyApi } from '@/utility/misskey-api'; import { misskeyApi } from '@/utility/misskey-api';
import { Paginator } from '@/utility/paginator.js'; import { Paginator } from '@/utility/paginator.js';
import MkTabs from '@/components/MkTabs.vue';
import MkInfo from '@/components/MkInfo.vue';
const props = defineProps<{
scheduled?: boolean;
}>();
const emit = defineEmits<{ const emit = defineEmits<{
(ev: 'restore', draft: Misskey.entities.NoteDraft): void; (ev: 'restore', draft: Misskey.entities.NoteDraft): void;
@ -132,8 +188,20 @@ const emit = defineEmits<{
(ev: 'closed'): void; (ev: 'closed'): void;
}>(); }>();
const paginator = markRaw(new Paginator('notes/drafts/list', { const tab = ref<'drafts' | 'scheduled'>(props.scheduled ? 'scheduled' : 'drafts');
const draftsPaginator = markRaw(new Paginator('notes/drafts/list', {
limit: 10, limit: 10,
params: {
scheduled: false,
},
}));
const scheduledPaginator = markRaw(new Paginator('notes/drafts/list', {
limit: 10,
params: {
scheduled: true,
},
})); }));
const currentDraftsCount = ref(0); const currentDraftsCount = ref(0);
@ -162,7 +230,17 @@ async function deleteDraft(draft: Misskey.entities.NoteDraft) {
if (canceled) return; if (canceled) return;
os.apiWithDialog('notes/drafts/delete', { draftId: draft.id }).then(() => { os.apiWithDialog('notes/drafts/delete', { draftId: draft.id }).then(() => {
paginator.reload(); draftsPaginator.reload();
});
}
async function cancelSchedule(draft: Misskey.entities.NoteDraft) {
os.apiWithDialog('notes/drafts/update', {
draftId: draft.id,
isActuallyScheduled: false,
scheduledAt: null,
}).then(() => {
scheduledPaginator.reload();
}); });
} }
</script> </script>
@ -220,4 +298,11 @@ async function deleteDraft(draft: Misskey.entities.NoteDraft) {
padding-top: 16px; padding-top: 16px;
border-top: solid 1px var(--MI_THEME-divider); border-top: solid 1px var(--MI_THEME-divider);
} }
.tabs {
background: color(from var(--MI_THEME-bg) srgb r g b / 0.75);
-webkit-backdrop-filter: var(--MI-blur, blur(15px));
backdrop-filter: var(--MI-blur, blur(15px));
border-bottom: solid 0.5px var(--MI_THEME-divider);
}
</style> </style>

View File

@ -7,7 +7,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<div :class="$style.root"> <div :class="$style.root">
<div :class="$style.head"> <div :class="$style.head">
<MkAvatar v-if="['pollEnded', 'note'].includes(notification.type) && 'note' in notification" :class="$style.icon" :user="notification.note.user" link preview/> <MkAvatar v-if="['pollEnded', 'note'].includes(notification.type) && 'note' in notification" :class="$style.icon" :user="notification.note.user" link preview/>
<MkAvatar v-else-if="['roleAssigned', 'achievementEarned', 'exportCompleted', 'login', 'createToken'].includes(notification.type)" :class="$style.icon" :user="$i" link preview/> <MkAvatar v-else-if="['roleAssigned', 'achievementEarned', 'exportCompleted', 'login', 'createToken', 'scheduledNotePosted', 'scheduledNotePostFailed'].includes(notification.type)" :class="$style.icon" :user="$i" link preview/>
<div v-else-if="notification.type === 'reaction:grouped' && notification.note.reactionAcceptance === 'likeOnly'" :class="[$style.icon, $style.icon_reactionGroupHeart]"><i class="ti ti-heart" style="line-height: 1;"></i></div> <div v-else-if="notification.type === 'reaction:grouped' && notification.note.reactionAcceptance === 'likeOnly'" :class="[$style.icon, $style.icon_reactionGroupHeart]"><i class="ti ti-heart" style="line-height: 1;"></i></div>
<div v-else-if="notification.type === 'reaction:grouped'" :class="[$style.icon, $style.icon_reactionGroup]"><i class="ti ti-plus" style="line-height: 1;"></i></div> <div v-else-if="notification.type === 'reaction:grouped'" :class="[$style.icon, $style.icon_reactionGroup]"><i class="ti ti-plus" style="line-height: 1;"></i></div>
<div v-else-if="notification.type === 'renote:grouped'" :class="[$style.icon, $style.icon_renoteGroup]"><i class="ti ti-repeat" style="line-height: 1;"></i></div> <div v-else-if="notification.type === 'renote:grouped'" :class="[$style.icon, $style.icon_renoteGroup]"><i class="ti ti-repeat" style="line-height: 1;"></i></div>
@ -23,6 +23,8 @@ SPDX-License-Identifier: AGPL-3.0-only
[$style.t_mention]: notification.type === 'mention', [$style.t_mention]: notification.type === 'mention',
[$style.t_quote]: notification.type === 'quote', [$style.t_quote]: notification.type === 'quote',
[$style.t_pollEnded]: notification.type === 'pollEnded', [$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_achievementEarned]: notification.type === 'achievementEarned',
[$style.t_exportCompleted]: notification.type === 'exportCompleted', [$style.t_exportCompleted]: notification.type === 'exportCompleted',
[$style.t_login]: notification.type === 'login', [$style.t_login]: notification.type === 'login',
@ -39,6 +41,8 @@ SPDX-License-Identifier: AGPL-3.0-only
<i v-else-if="notification.type === 'mention'" class="ti ti-at"></i> <i v-else-if="notification.type === 'mention'" class="ti ti-at"></i>
<i v-else-if="notification.type === 'quote'" class="ti ti-quote"></i> <i v-else-if="notification.type === 'quote'" class="ti ti-quote"></i>
<i v-else-if="notification.type === 'pollEnded'" class="ti ti-chart-arrows"></i> <i v-else-if="notification.type === 'pollEnded'" class="ti ti-chart-arrows"></i>
<i v-else-if="notification.type === 'scheduledNotePosted'" class="ti ti-send"></i>
<i v-else-if="notification.type === 'scheduledNotePostFailed'" class="ti ti-alert-triangle"></i>
<i v-else-if="notification.type === 'achievementEarned'" class="ti ti-medal"></i> <i v-else-if="notification.type === 'achievementEarned'" class="ti ti-medal"></i>
<i v-else-if="notification.type === 'exportCompleted'" class="ti ti-archive"></i> <i v-else-if="notification.type === 'exportCompleted'" class="ti ti-archive"></i>
<i v-else-if="notification.type === 'login'" class="ti ti-login-2"></i> <i v-else-if="notification.type === 'login'" class="ti ti-login-2"></i>
@ -60,6 +64,8 @@ SPDX-License-Identifier: AGPL-3.0-only
<div :class="$style.tail"> <div :class="$style.tail">
<header :class="$style.header"> <header :class="$style.header">
<span v-if="notification.type === 'pollEnded'">{{ i18n.ts._notification.pollEnded }}</span> <span v-if="notification.type === 'pollEnded'">{{ i18n.ts._notification.pollEnded }}</span>
<span v-else-if="notification.type === 'scheduledNotePosted'">{{ i18n.ts._notification.scheduledNotePosted }}</span>
<span v-else-if="notification.type === 'scheduledNotePostFailed'">{{ i18n.ts._notification.scheduledNotePostFailed }}</span>
<span v-else-if="notification.type === 'note'">{{ i18n.ts._notification.newNote }}: <MkUserName :user="notification.note.user"/></span> <span v-else-if="notification.type === 'note'">{{ i18n.ts._notification.newNote }}: <MkUserName :user="notification.note.user"/></span>
<span v-else-if="notification.type === 'roleAssigned'">{{ i18n.ts._notification.roleAssigned }}</span> <span v-else-if="notification.type === 'roleAssigned'">{{ i18n.ts._notification.roleAssigned }}</span>
<span v-else-if="notification.type === 'chatRoomInvitationReceived'">{{ i18n.ts._notification.chatRoomInvitationReceived }}</span> <span v-else-if="notification.type === 'chatRoomInvitationReceived'">{{ i18n.ts._notification.chatRoomInvitationReceived }}</span>
@ -103,6 +109,11 @@ SPDX-License-Identifier: AGPL-3.0-only
<Mfm :text="getNoteSummary(notification.note)" :plain="true" :nowrap="true" :author="notification.note.user"/> <Mfm :text="getNoteSummary(notification.note)" :plain="true" :nowrap="true" :author="notification.note.user"/>
<i class="ti ti-quote" :class="$style.quote"></i> <i class="ti ti-quote" :class="$style.quote"></i>
</MkA> </MkA>
<MkA v-else-if="notification.type === 'scheduledNotePosted'" :class="$style.text" :to="notePage(notification.note)" :title="getNoteSummary(notification.note)">
<i class="ti ti-quote" :class="$style.quote"></i>
<Mfm :text="getNoteSummary(notification.note)" :plain="true" :nowrap="true" :author="notification.note.user"/>
<i class="ti ti-quote" :class="$style.quote"></i>
</MkA>
<div v-else-if="notification.type === 'roleAssigned'" :class="$style.text"> <div v-else-if="notification.type === 'roleAssigned'" :class="$style.text">
{{ notification.role.name }} {{ notification.role.name }}
</div> </div>
@ -338,6 +349,16 @@ function getActualReactedUsersCount(notification: Misskey.entities.Notification)
pointer-events: none; pointer-events: none;
} }
.t_scheduledNotePosted {
background: var(--eventOther);
pointer-events: none;
}
.t_scheduledNotePostFailed {
background: var(--eventOther);
pointer-events: none;
}
.t_achievementEarned { .t_achievementEarned {
background: var(--eventAchievement); background: var(--eventAchievement);
pointer-events: none; pointer-events: none;

View File

@ -6,7 +6,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<template> <template>
<div :class="$style.root"> <div :class="$style.root">
<div :class="$style.control"> <div :class="$style.control">
<MkSelect v-model="order" :class="$style.order" :items="[{ label: i18n.ts._order.newest, value: 'newest' }, { label: i18n.ts._order.oldest, value: 'oldest' }]"> <MkSelect v-model="order" :class="$style.order" :items="orderDef">
<template #prefix><i class="ti ti-arrows-sort"></i></template> <template #prefix><i class="ti ti-arrows-sort"></i></template>
</MkSelect> </MkSelect>
<MkButton v-if="paginator.canSearch" v-tooltip="i18n.ts.search" iconOnly transparent rounded :active="searchOpened" @click="searchOpened = !searchOpened"><i class="ti ti-search"></i></MkButton> <MkButton v-if="paginator.canSearch" v-tooltip="i18n.ts.search" iconOnly transparent rounded :active="searchOpened" @click="searchOpened = !searchOpened"><i class="ti ti-search"></i></MkButton>
@ -45,6 +45,7 @@ import { i18n } from '@/i18n.js';
import MkSelect from '@/components/MkSelect.vue'; import MkSelect from '@/components/MkSelect.vue';
import MkInput from '@/components/MkInput.vue'; import MkInput from '@/components/MkInput.vue';
import { formatDateTimeString } from '@/utility/format-time-string.js'; import { formatDateTimeString } from '@/utility/format-time-string.js';
import { useMkSelect } from '@/composables/use-mkselect.js';
const props = withDefaults(defineProps<{ const props = withDefaults(defineProps<{
paginator: T; paginator: T;
@ -58,7 +59,16 @@ const props = withDefaults(defineProps<{
const searchOpened = ref(false); const searchOpened = ref(false);
const filterOpened = ref(props.filterOpened); const filterOpened = ref(props.filterOpened);
const order = ref<'newest' | 'oldest'>('newest'); const {
model: order,
def: orderDef,
} = useMkSelect({
items: [
{ label: i18n.ts._order.newest, value: 'newest' },
{ label: i18n.ts._order.oldest, value: 'oldest' },
],
initialValue: 'newest',
});
const date = ref<number | null>(null); const date = ref<number | null>(null);
const q = ref<string | null>(null); const q = ref<string | null>(null);

View File

@ -4,14 +4,18 @@ SPDX-License-Identifier: AGPL-3.0-only
--> -->
<template> <template>
<div :class="[$style.root, accented ? $style.accented : null]"></div> <div :class="[$style.root, accented ? $style.accented : null, revered ? $style.revered : null]"/>
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
const props = withDefaults(defineProps<{ const props = withDefaults(defineProps<{
accented?: boolean; accented?: boolean;
revered?: boolean;
height?: number;
}>(), { }>(), {
accented: false, accented: false,
revered: false,
height: 200,
}); });
</script> </script>
@ -27,14 +31,17 @@ const props = withDefaults(defineProps<{
--dot-size: 2px; --dot-size: 2px;
--gap-size: 40px; --gap-size: 40px;
--offset: calc(var(--gap-size) / 2); --offset: calc(var(--gap-size) / 2);
--height: v-bind('props.height + "px"');
height: 200px; height: var(--height);
margin-bottom: -200px;
background-image: linear-gradient(transparent 60%, transparent 100%), radial-gradient(var(--c) var(--dot-size), transparent var(--dot-size)), radial-gradient(var(--c) var(--dot-size), transparent var(--dot-size)); background-image: linear-gradient(transparent 60%, transparent 100%), radial-gradient(var(--c) var(--dot-size), transparent var(--dot-size)), radial-gradient(var(--c) var(--dot-size), transparent var(--dot-size));
background-position: 0 0, 0 0, var(--offset) var(--offset); background-position: 0 0, 0 0, var(--offset) var(--offset);
background-size: 100% 100%, var(--gap-size) var(--gap-size), var(--gap-size) var(--gap-size); background-size: 100% 100%, var(--gap-size) var(--gap-size), var(--gap-size) var(--gap-size);
mask-image: linear-gradient(to bottom, black 0%, transparent 100%); mask-image: linear-gradient(to bottom, black 0%, transparent 100%);
pointer-events: none; pointer-events: none;
&.revered {
mask-image: linear-gradient(to top, black 0%, transparent 100%);
}
} }
</style> </style>

View File

@ -27,16 +27,16 @@ SPDX-License-Identifier: AGPL-3.0-only
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import { computed, ref } from 'vue'; import { computed, ref, watch } from 'vue';
import * as Misskey from 'misskey-js'; import * as Misskey from 'misskey-js';
import { host } from '@@/js/config.js'; import { host } from '@@/js/config.js';
import { useInterval } from '@@/js/use-interval.js';
import type { OpenOnRemoteOptions } from '@/utility/please-login.js'; import type { OpenOnRemoteOptions } from '@/utility/please-login.js';
import { sum } from '@/utility/array.js'; import { sum } from '@/utility/array.js';
import { pleaseLogin } from '@/utility/please-login.js'; import { pleaseLogin } from '@/utility/please-login.js';
import * as os from '@/os.js'; import * as os from '@/os.js';
import { misskeyApi } from '@/utility/misskey-api.js'; import { misskeyApi } from '@/utility/misskey-api.js';
import { i18n } from '@/i18n.js'; import { i18n } from '@/i18n.js';
import { useLowresTime } from '@/composables/use-lowres-time.js';
const props = defineProps<{ const props = defineProps<{
noteId: string; noteId: string;
@ -48,7 +48,21 @@ const props = defineProps<{
author?: Misskey.entities.UserLite; author?: Misskey.entities.UserLite;
}>(); }>();
const remaining = ref(-1); const now = useLowresTime();
const expiresAtTime = computed(() => props.expiresAt ? new Date(props.expiresAt).getTime() : null);
const remaining = computed(() => {
if (expiresAtTime.value == null) return -1;
return Math.floor(Math.max(expiresAtTime.value - now.value, 0) / 1000);
});
const remainingWatchStop = watch(remaining, (to) => {
if (to <= 0) {
showResult.value = true;
remainingWatchStop();
}
}, { immediate: true });
const total = computed(() => sum(props.choices.map(x => x.votes))); const total = computed(() => sum(props.choices.map(x => x.votes)));
const closed = computed(() => remaining.value === 0); const closed = computed(() => remaining.value === 0);
@ -71,22 +85,7 @@ const pleaseLoginContext = computed<OpenOnRemoteOptions>(() => ({
url: `https://${host}/notes/${props.noteId}`, url: `https://${host}/notes/${props.noteId}`,
})); }));
// const vote = async (id: number) => {
if (props.expiresAt) {
const tick = () => {
remaining.value = Math.floor(Math.max(new Date(props.expiresAt!).getTime() - Date.now(), 0) / 1000);
if (remaining.value === 0) {
showResult.value = true;
}
};
useInterval(tick, 3000, {
immediate: true,
afterMounted: false,
});
}
const vote = async (id) => {
if (props.readOnly || closed.value || isVoted.value) return; if (props.readOnly || closed.value || isVoted.value) return;
pleaseLogin({ openOnRemote: pleaseLoginContext.value }); pleaseLogin({ openOnRemote: pleaseLoginContext.value });

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