1
0
mirror of https://github.com/misskey-dev/misskey.git synced 2026-05-07 08:45:33 +02:00

Compare commits

..

18 Commits

Author SHA1 Message Date
かっこかり
b73ac26612 Update CHANGELOG.md 2026-05-07 13:37:36 +09:00
かっこかり
b528ff9c59 enhance(frontend): テーマの適用管理を改善 (#17376)
* wip

* add test

* use themeManager.currentCompiledTheme for obtaining theme variables / reduce getComputedStyle usage

* fix

* fix: better error handling on theme installation

* Update Changelog

* chore: remove frontend-shared builds as it is currently working as a stub package

* fix: broken lockfile

* fix

* fix lint

* fix
2026-05-07 11:42:45 +09:00
github-actions[bot]
a82ba0d775 [skip ci] Update CHANGELOG.md (prepend template) 2026-05-06 10:44:25 +00:00
github-actions[bot]
b78e0168b0 Release: 2026.5.1 2026-05-06 10:44:17 +00:00
かっこかり
33f59b3469 Update CHANGELOG.md 2026-05-06 15:08:22 +09:00
syuilo
5b478dda9d New Crowdin updates (#17372)
* New translations ja-jp.yml (Turkish)

[ci skip]

* New translations ja-jp.yml (Thai)

[ci skip]

* New translations ja-jp.yml (Thai)

[ci skip]

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

[ci skip]

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

[ci skip]
2026-05-06 11:12:51 +09:00
かっこかり
90725d6a8c enhance(frontend): MkNoteDetailedの公開範囲表示を改善 (#17374)
* enhance(frontend): 노트 상세 페이지에서 공개 범위를 자세히 표시하도록 개선됨

* Update Changelog

* fix

---------

Co-authored-by: NoriDev <m1nthing2322@gmail.com>
2026-05-05 20:53:27 +09:00
github-actions[bot]
86542f07d3 Bump version to 2026.5.1-beta.0 2026-05-04 14:22:27 +00:00
syuilo
45022bc766 New Crowdin updates (#17324)
* New translations ja-jp.yml (Russian)

* New translations ja-jp.yml (Spanish)

* New translations ja-jp.yml (English)

* New translations ja-jp.yml (Thai)

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

[ci skip]

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

[ci skip]

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

[ci skip]

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

[ci skip]

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

[ci skip]

* New translations ja-jp.yml (Korean)

[ci skip]

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

[ci skip]

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

[ci skip]

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

[ci skip]

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

[ci skip]
2026-05-04 20:32:06 +09:00
Wonwoo Choi
35711fc8e1 fix(backend): Acquire lock of Announce object in announceNote even if it is from a relay actor (#17356)
fix(backend): Always acquire lock of Announce object in announceNote
2026-05-03 21:03:25 +09:00
かっこかり
45f140aa86 deps: Update dependencies [ci skip] (#17368)
* update deps

* update deps

* rollback got to v14

* Revert "rollback got to v14"

This reverts commit 780abdf7b6.

* rollback rolldown to v1.0.0-rc.15
2026-05-03 18:24:53 +09:00
renovate[bot]
22ce7b58ca chore(deps): update [docker] update dependencies [ci skip] (#17369)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-05-03 18:22:37 +09:00
renovate[bot]
37107c9818 chore(deps): update [github actions] update dependencies [ci skip] (#17370)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-05-03 18:22:02 +09:00
renovate[bot]
a5a43c8c06 chore(deps): update [github actions] update dependencies (major) (#17204)
chore(deps): update [github actions] update dependencies

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-05-03 17:45:12 +09:00
かっこかり
723d8add2f refactor: パスキーまわりのライブラリを更新 (#17354)
* refactor: パスキーまわりのライブラリを更新

* fix
2026-05-03 17:16:06 +09:00
かっこかり
9d20152e05 Update CHANGELOG.md (follow-up of #17121) [ci skip 2026-05-03 17:15:29 +09:00
Copilot
37412f0e1b enhance: Add canCreateChannel role policy (#17121)
* Initial plan

* Add canCreateChannel role policy to control channel creation

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

* Add canCreateChannel to getUserPolicies return value

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

* Add canCreateChannel translations for en-US and ja-JP

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

* Add canCreateChannel to misskey-js rolePolicies array

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

* Add frontend UI for canCreateChannel policy configuration

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

* fix: build autogen files

* 🎨

* migrate

* fix: unnecessary changes to non-Japanese locales

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: syuilo <4439005+syuilo@users.noreply.github.com>
Co-authored-by: kakkokari-gtyih <67428053+kakkokari-gtyih@users.noreply.github.com>
2026-05-03 17:10:17 +09:00
kami8
712b51c142 Fix(frontend): ロール設定画面でロールをアサイン/アサイン解除した際、リロードしなくても画面に反映されるよう修正 (#17365)
* ロールの付与、剥奪後にPaginatorのリロードを行って表示を更新する処理を追加

* CHANGELOGを更新
2026-05-03 16:15:03 +09:00
114 changed files with 3221 additions and 3908 deletions

View File

@@ -19,10 +19,10 @@ jobs:
uses: actions/checkout@v6.0.2
- name: Setup pnpm
uses: pnpm/action-setup@v4.4.0
uses: pnpm/action-setup@v6.0.3
- name: Setup Node.js
uses: actions/setup-node@v6.3.0
uses: actions/setup-node@v6.4.0
with:
node-version-file: '.node-version'
cache: 'pnpm'

View File

@@ -14,7 +14,7 @@ jobs:
- name: Checkout head
uses: actions/checkout@v6.0.2
- name: Setup Node.js
uses: actions/setup-node@v6.3.0
uses: actions/setup-node@v6.4.0
with:
node-version-file: '.node-version'

View File

@@ -25,11 +25,11 @@ jobs:
ref: refs/pull/${{ github.event.pull_request.number }}/merge
- name: setup pnpm
uses: pnpm/action-setup@v4
uses: pnpm/action-setup@v6
- name: setup node
id: setup-node
uses: actions/setup-node@v6.3.0
uses: actions/setup-node@v6.4.0
with:
node-version-file: '.node-version'
cache: pnpm
@@ -53,7 +53,7 @@ jobs:
# packages/misskey-js/generator/built/autogen
- name: Upload Generated
uses: actions/upload-artifact@v6
uses: actions/upload-artifact@v7
with:
name: generated-misskey-js
path: packages/misskey-js/generator/built/autogen
@@ -73,7 +73,7 @@ jobs:
ref: refs/pull/${{ github.event.pull_request.number }}/merge
- name: Upload From Merged
uses: actions/upload-artifact@v6
uses: actions/upload-artifact@v7
with:
name: actual-misskey-js
path: packages/misskey-js/src/autogen
@@ -86,13 +86,13 @@ jobs:
pull-requests: write
steps:
- name: download generated-misskey-js
uses: actions/download-artifact@v7
uses: actions/download-artifact@v8
with:
name: generated-misskey-js
path: misskey-js-generated
- name: download actual-misskey-js
uses: actions/download-artifact@v7
uses: actions/download-artifact@v8
with:
name: actual-misskey-js
path: misskey-js-actual

View File

@@ -29,15 +29,15 @@ jobs:
- name: Check out the repo
uses: actions/checkout@v6.0.2
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
uses: docker/setup-buildx-action@v4
- name: Log in to Docker Hub
uses: docker/login-action@v3
uses: docker/login-action@v4
with:
username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_PASSWORD }}
- name: Build and push by digest
id: build
uses: docker/build-push-action@v6
uses: docker/build-push-action@v7
with:
context: .
push: true
@@ -53,7 +53,7 @@ jobs:
digest="${{ steps.build.outputs.digest }}"
touch "/tmp/digests/${digest#sha256:}"
- name: Upload digest
uses: actions/upload-artifact@v6
uses: actions/upload-artifact@v7
with:
name: digests-${{ env.PLATFORM_PAIR }}
path: /tmp/digests/*
@@ -66,15 +66,15 @@ jobs:
- build
steps:
- name: Download digests
uses: actions/download-artifact@v7
uses: actions/download-artifact@v8
with:
path: /tmp/digests
pattern: digests-*
merge-multiple: true
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
uses: docker/setup-buildx-action@v4
- name: Login to Docker Hub
uses: docker/login-action@v3
uses: docker/login-action@v4
with:
username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_PASSWORD }}

View File

@@ -34,21 +34,21 @@ jobs:
- name: Check out the repo
uses: actions/checkout@v6.0.2
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
uses: docker/setup-buildx-action@v4
- name: Docker meta
id: meta
uses: docker/metadata-action@v5
uses: docker/metadata-action@v6
with:
images: ${{ env.REGISTRY_IMAGE }}
tags: ${{ env.TAGS }}
- name: Log in to Docker Hub
uses: docker/login-action@v3
uses: docker/login-action@v4
with:
username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_PASSWORD }}
- name: Build and Push to Docker Hub
id: build
uses: docker/build-push-action@v6
uses: docker/build-push-action@v7
with:
context: .
push: true
@@ -64,7 +64,7 @@ jobs:
digest="${{ steps.build.outputs.digest }}"
touch "/tmp/digests/${digest#sha256:}"
- name: Upload digest
uses: actions/upload-artifact@v6
uses: actions/upload-artifact@v7
with:
name: digests-${{ env.PLATFORM_PAIR }}
path: /tmp/digests/*
@@ -77,21 +77,21 @@ jobs:
- build
steps:
- name: Download digests
uses: actions/download-artifact@v7
uses: actions/download-artifact@v8
with:
path: /tmp/digests
pattern: digests-*
merge-multiple: true
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
uses: docker/setup-buildx-action@v4
- name: Docker meta
id: meta
uses: docker/metadata-action@v5
uses: docker/metadata-action@v6
with:
images: ${{ env.REGISTRY_IMAGE }}
tags: ${{ env.TAGS }}
- name: Login to Docker Hub
uses: docker/login-action@v3
uses: docker/login-action@v4
with:
username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_PASSWORD }}

View File

@@ -30,9 +30,9 @@ jobs:
ref: ${{ matrix.ref }}
submodules: true
- name: Setup pnpm
uses: pnpm/action-setup@v4.4.0
uses: pnpm/action-setup@v6.0.3
- name: Use Node.js
uses: actions/setup-node@v6.3.0
uses: actions/setup-node@v6.4.0
with:
node-version-file: '.node-version'
cache: 'pnpm'
@@ -48,7 +48,7 @@ jobs:
- name: Copy API.json
run: cp packages/backend/built/api.json ${{ matrix.api-json-name }}
- name: Upload Artifact
uses: actions/upload-artifact@v6
uses: actions/upload-artifact@v7
with:
name: api-artifact-${{ matrix.api-json-name }}
path: ${{ matrix.api-json-name }}
@@ -61,7 +61,7 @@ jobs:
PR_NUMBER: ${{ github.event.number }}
run: |
echo "$PR_NUMBER" > ./pr_number
- uses: actions/upload-artifact@v6
- uses: actions/upload-artifact@v7
with:
name: api-artifact-pr-number
path: pr_number

View File

@@ -35,7 +35,7 @@ jobs:
POSTGRES_DB: test-misskey
POSTGRES_HOST_AUTH_METHOD: trust
redis:
image: redis:7
image: redis:8
ports:
- 56312:6379
@@ -45,9 +45,9 @@ jobs:
ref: ${{ matrix.ref }}
submodules: true
- name: Setup pnpm
uses: pnpm/action-setup@v4.4.0
uses: pnpm/action-setup@v6.0.3
- name: Use Node.js
uses: actions/setup-node@v6.3.0
uses: actions/setup-node@v6.4.0
with:
node-version-file: '.node-version'
cache: 'pnpm'
@@ -67,7 +67,7 @@ jobs:
# Start the server and measure memory usage
node packages/backend/scripts/measure-memory.mjs > ${{ matrix.memory-json-name }}
- name: Upload Artifact
uses: actions/upload-artifact@v6
uses: actions/upload-artifact@v7
with:
name: memory-artifact-${{ matrix.memory-json-name }}
path: ${{ matrix.memory-json-name }}
@@ -81,7 +81,7 @@ jobs:
PR_NUMBER: ${{ github.event.number }}
run: |
echo "$PR_NUMBER" > ./pr_number
- uses: actions/upload-artifact@v6
- uses: actions/upload-artifact@v7
with:
name: memory-artifact-pr-number
path: pr_number

View File

@@ -41,8 +41,8 @@ jobs:
fetch-depth: 0
submodules: true
- name: Setup pnpm
uses: pnpm/action-setup@v4.4.0
- uses: actions/setup-node@v6.3.0
uses: pnpm/action-setup@v6.0.3
- uses: actions/setup-node@v6.4.0
with:
node-version-file: '.node-version'
cache: 'pnpm'
@@ -74,14 +74,14 @@ jobs:
fetch-depth: 0
submodules: true
- name: Setup pnpm
uses: pnpm/action-setup@v4.4.0
- uses: actions/setup-node@v6.3.0
uses: pnpm/action-setup@v6.0.3
- uses: actions/setup-node@v6.4.0
with:
node-version-file: '.node-version'
cache: 'pnpm'
- run: pnpm i --frozen-lockfile
- name: Restore eslint cache
uses: actions/cache@v4.3.0
uses: actions/cache@v5.0.5
with:
path: ${{ env.eslint-cache-path }}
key: eslint-${{ env.eslint-cache-version }}-${{ matrix.workspace }}-${{ hashFiles('**/pnpm-lock.yaml') }}-${{ github.ref_name }}-${{ github.sha }}
@@ -105,8 +105,8 @@ jobs:
fetch-depth: 0
submodules: true
- name: Setup pnpm
uses: pnpm/action-setup@v4.4.0
- uses: actions/setup-node@v6.3.0
uses: pnpm/action-setup@v6.0.3
- uses: actions/setup-node@v6.4.0
with:
node-version-file: '.node-version'
cache: 'pnpm'

View File

@@ -21,8 +21,8 @@ jobs:
fetch-depth: 0
submodules: true
- name: Setup pnpm
uses: pnpm/action-setup@v4.4.0
- uses: actions/setup-node@v6.3.0
uses: pnpm/action-setup@v6.0.3
- uses: actions/setup-node@v6.4.0
with:
node-version-file: ".node-version"
cache: "pnpm"

View File

@@ -20,9 +20,9 @@ jobs:
with:
submodules: true
- name: Setup pnpm
uses: pnpm/action-setup@v4.4.0
uses: pnpm/action-setup@v6.0.3
- name: Use Node.js
uses: actions/setup-node@v6.3.0
uses: actions/setup-node@v6.4.0
with:
node-version-file: '.node-version'
cache: 'pnpm'

View File

@@ -16,7 +16,7 @@ jobs:
# api-artifact
steps:
- name: Download artifact
uses: actions/github-script@v8.0.0
uses: actions/github-script@v9
with:
script: |
const fs = require('fs');
@@ -60,7 +60,7 @@ jobs:
- name: Echo full diff
run: cat ./api-full.json.diff
- name: Upload full diff to Artifact
uses: actions/upload-artifact@v6
uses: actions/upload-artifact@v7
with:
name: api-artifact
path: |

View File

@@ -15,7 +15,7 @@ jobs:
steps:
- name: Download artifact
uses: actions/github-script@v8.0.0
uses: actions/github-script@v9
with:
script: |
const fs = require('fs');

View File

@@ -14,7 +14,7 @@ jobs:
pull-requests: write
steps:
- name: Reply
uses: actions/github-script@v8
uses: actions/github-script@v9
with:
script: |
const body = `To dev team (@misskey-dev/dev):

View File

@@ -37,9 +37,9 @@ jobs:
if: github.event_name == 'pull_request_target'
run: git checkout "$(git rev-list --parents -n1 HEAD | cut -d" " -f3)"
- name: Setup pnpm
uses: pnpm/action-setup@v4.4.0
uses: pnpm/action-setup@v6.0.3
- name: Use Node.js
uses: actions/setup-node@v6.3.0
uses: actions/setup-node@v6.4.0
with:
node-version-file: '.node-version'
cache: 'pnpm'
@@ -90,7 +90,7 @@ jobs:
env:
CHROMATIC_PROJECT_TOKEN: ${{ secrets.CHROMATIC_PROJECT_TOKEN }}
- name: Notify that Chromatic detects changes
uses: actions/github-script@v8.0.0
uses: actions/github-script@v9
if: github.event_name != 'pull_request_target' && steps.chromatic_push.outputs.success == 'false'
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
@@ -102,7 +102,7 @@ jobs:
body: 'Chromatic detects changes. Please [review the changes on Chromatic](https://www.chromatic.com/builds?appId=6428f7d7b962f0b79f97d6e4).'
})
- name: Upload Artifacts
uses: actions/upload-artifact@v6
uses: actions/upload-artifact@v7
with:
name: storybook
path: packages/frontend/storybook-static

View File

@@ -45,11 +45,11 @@ jobs:
POSTGRES_DB: test-misskey
POSTGRES_HOST_AUTH_METHOD: trust
redis:
image: redis:7
image: redis:8
ports:
- 56312:6379
meilisearch:
image: getmeili/meilisearch:v1.38.2
image: getmeili/meilisearch:v1.42.1
ports:
- 57712:7700
env:
@@ -61,13 +61,13 @@ jobs:
with:
submodules: true
- name: Setup pnpm
uses: pnpm/action-setup@v4.4.0
uses: pnpm/action-setup@v6.0.3
- name: Get current date
id: current-date
run: echo "today=$(date +'%Y-%m-%d')" >> $GITHUB_OUTPUT
- name: Setup and Restore ffmpeg/ffprobe Cache
id: cache-ffmpeg
uses: actions/cache@v4
uses: actions/cache@v5
with:
path: |
/usr/local/bin/ffmpeg
@@ -93,7 +93,7 @@ jobs:
fi
done
- name: Use Node.js
uses: actions/setup-node@v6.3.0
uses: actions/setup-node@v6.4.0
with:
node-version-file: ${{ matrix.node-version-file }}
cache: 'pnpm'
@@ -107,7 +107,7 @@ jobs:
- name: Test
run: pnpm --filter backend test-and-coverage
- name: Upload to Codecov
uses: codecov/codecov-action@v5
uses: codecov/codecov-action@v6
with:
token: ${{ secrets.CODECOV_TOKEN }}
files: ./packages/backend/coverage/coverage-final.json
@@ -131,7 +131,7 @@ jobs:
POSTGRES_DB: test-misskey
POSTGRES_HOST_AUTH_METHOD: trust
redis:
image: redis:7
image: redis:8
ports:
- 56312:6379
@@ -140,9 +140,9 @@ jobs:
with:
submodules: true
- name: Setup pnpm
uses: pnpm/action-setup@v4.4.0
uses: pnpm/action-setup@v6.0.3
- name: Use Node.js
uses: actions/setup-node@v6.3.0
uses: actions/setup-node@v6.4.0
with:
node-version-file: ${{ matrix.node-version-file }}
cache: 'pnpm'
@@ -156,7 +156,7 @@ jobs:
- name: Test
run: pnpm --filter backend test-and-coverage:e2e
- name: Upload to Codecov
uses: codecov/codecov-action@v5
uses: codecov/codecov-action@v6
with:
token: ${{ secrets.CODECOV_TOKEN }}
files: ./packages/backend/coverage/coverage-final.json
@@ -184,12 +184,12 @@ jobs:
with:
submodules: true
- name: Setup pnpm
uses: pnpm/action-setup@v4.4.0
uses: pnpm/action-setup@v6.0.3
- name: Get current date
id: current-date
run: echo "today=$(date +'%Y-%m-%d')" >> $GITHUB_OUTPUT
- name: Use Node.js
uses: actions/setup-node@v6.3.0
uses: actions/setup-node@v6.4.0
with:
node-version-file: ${{ matrix.node-version-file }}
cache: 'pnpm'

View File

@@ -36,13 +36,13 @@ jobs:
with:
submodules: true
- name: Setup pnpm
uses: pnpm/action-setup@v4.4.0
uses: pnpm/action-setup@v6.0.3
- name: Get current date
id: current-date
run: echo "today=$(date +'%Y-%m-%d')" >> $GITHUB_OUTPUT
- name: Setup and Restore ffmpeg/ffprobe Cache
id: cache-ffmpeg
uses: actions/cache@v4
uses: actions/cache@v5
with:
path: |
/usr/local/bin/ffmpeg
@@ -68,7 +68,7 @@ jobs:
fi
done
- name: Use Node.js
uses: actions/setup-node@v6.3.0
uses: actions/setup-node@v6.4.0
with:
node-version-file: ${{ matrix.node-version-file }}
cache: 'pnpm'

View File

@@ -32,9 +32,9 @@ jobs:
with:
submodules: true
- name: Setup pnpm
uses: pnpm/action-setup@v4.4.0
uses: pnpm/action-setup@v6.0.3
- name: Use Node.js
uses: actions/setup-node@v6.3.0
uses: actions/setup-node@v6.4.0
with:
node-version-file: '.node-version'
cache: 'pnpm'
@@ -48,7 +48,7 @@ jobs:
- name: Test
run: pnpm --filter frontend test-and-coverage
- name: Upload Coverage
uses: codecov/codecov-action@v5
uses: codecov/codecov-action@v6
with:
token: ${{ secrets.CODECOV_TOKEN }}
files: ./packages/frontend/coverage/coverage-final.json
@@ -71,7 +71,7 @@ jobs:
POSTGRES_DB: test-misskey
POSTGRES_HOST_AUTH_METHOD: trust
redis:
image: redis:7
image: redis:8
ports:
- 56312:6379
@@ -86,9 +86,9 @@ jobs:
#- uses: browser-actions/setup-firefox@latest
# if: ${{ matrix.browser == 'firefox' }}
- name: Setup pnpm
uses: pnpm/action-setup@v4.4.0
uses: pnpm/action-setup@v6.0.3
- name: Use Node.js
uses: actions/setup-node@v6.3.0
uses: actions/setup-node@v6.4.0
with:
node-version-file: '.node-version'
cache: 'pnpm'
@@ -105,7 +105,7 @@ jobs:
- name: Cypress install
run: pnpm exec cypress install
- name: Cypress run
uses: cypress-io/github-action@v6
uses: cypress-io/github-action@v7.1.9
timeout-minutes: 15
with:
install: false
@@ -113,12 +113,12 @@ jobs:
wait-on: 'http://localhost:61812'
headed: true
browser: ${{ matrix.browser }}
- uses: actions/upload-artifact@v6
- uses: actions/upload-artifact@v7
if: failure()
with:
name: ${{ matrix.browser }}-cypress-screenshots
path: cypress/screenshots
- uses: actions/upload-artifact@v6
- uses: actions/upload-artifact@v7
if: always()
with:
name: ${{ matrix.browser }}-cypress-videos

View File

@@ -25,10 +25,10 @@ jobs:
uses: actions/checkout@v6.0.2
- name: Setup pnpm
uses: pnpm/action-setup@v4.4.0
uses: pnpm/action-setup@v6.0.3
- name: Setup Node.js
uses: actions/setup-node@v6.3.0
uses: actions/setup-node@v6.4.0
with:
node-version-file: '.node-version'
cache: 'pnpm'
@@ -48,7 +48,7 @@ jobs:
CI: true
- name: Upload Coverage
uses: codecov/codecov-action@v5
uses: codecov/codecov-action@v6
with:
token: ${{ secrets.CODECOV_TOKEN }}
files: ./packages/misskey-js/coverage/coverage-final.json

View File

@@ -20,9 +20,9 @@ jobs:
with:
submodules: true
- name: Setup pnpm
uses: pnpm/action-setup@v4.4.0
uses: pnpm/action-setup@v6.0.3
- name: Use Node.js
uses: actions/setup-node@v6.3.0
uses: actions/setup-node@v6.4.0
with:
node-version-file: '.node-version'
cache: 'pnpm'

View File

@@ -21,9 +21,9 @@ jobs:
with:
submodules: true
- name: Setup pnpm
uses: pnpm/action-setup@v4.4.0
uses: pnpm/action-setup@v6.0.3
- name: Use Node.js
uses: actions/setup-node@v6.3.0
uses: actions/setup-node@v6.4.0
with:
node-version-file: '.node-version'
cache: 'pnpm'

View File

@@ -1,10 +1,28 @@
## Unreleased
### General
-
### Client
- Enhance: テーマのプレビュー時、リロードせずにもとのテーマに戻せるように
- Fix: テーマエディター使用時に、最初の変更のみ適用される問題を修正
- Fix: テーマのプレビュー時、既存のテーマとIDが被っている場合にプレビューできない問題を修正
- Fix: テーマのインストールエラーの表示を改善
### Server
-
## 2026.5.1
### General
- Enhance: チャンネルの作成の可否をロールポリシーで制御できるように
- Fix: `.devcontainer/compose.yml`のvolumeのマウントパスを修正
### Client
-
- Enhance: ノートの詳細表示での公開範囲の表示を改善
(Cherry-picked from https://github.com/kokonect-link/cherrypick/commit/ecc75563f4e428b66adccc379bf317b5b21ed8e6)
- Fix: ロール設定画面でロールをアサイン/アサイン解除した際、リロードしなくても画面に反映されるよう修正
### Server
- Fix: ID生成アルゴリズムにULIDを使用している場合に通知が約10秒遅延する問題を修正

View File

@@ -1,6 +1,6 @@
# syntax = docker/dockerfile:1.21
# syntax = docker/dockerfile:1.23
ARG NODE_VERSION=22.22.0-bookworm
ARG NODE_VERSION=22.22.2-bookworm
# build assets & compile TypeScript

View File

@@ -1408,6 +1408,7 @@ frame: "Frame"
presets: "Preset"
zeroPadding: "Zero padding"
nothingToConfigure: "No configurable options available"
viewRenotedChannel: "Show renoted channel"
_imageEditing:
_vars:
caption: "File caption"
@@ -3401,6 +3402,8 @@ _imageEffector:
threshold: "Threshold"
centerX: "Center X"
centerY: "Center Y"
density: "Density"
zoomLinesOutlineThickness: "Outline shadow thickness"
zoomLinesMaskSize: "Center diameter"
circle: "Circular"
drafts: "Drafts"

View File

@@ -1408,6 +1408,7 @@ frame: "Marco"
presets: "Predefinido"
zeroPadding: "Relleno cero"
nothingToConfigure: "No hay nada que configurar"
viewRenotedChannel: "Ver el canal al que te has suscrito"
_imageEditing:
_vars:
caption: "Título del archivo"

View File

@@ -1409,6 +1409,8 @@ presets: "プリセット"
zeroPadding: "ゼロ埋め"
nothingToConfigure: "設定項目はありません"
viewRenotedChannel: "リノート先のチャンネルを見る"
previewingTheme: "テーマのプレビュー中"
previewingThemeRestore: "元に戻す"
_imageEditing:
_vars:
@@ -2122,6 +2124,7 @@ _role:
canSearchNotes: "ノート検索の利用"
canSearchUsers: "ユーザー検索の利用"
canUseTranslator: "翻訳機能の利用"
canCreateChannel: "チャンネルの作成"
avatarDecorationLimit: "アイコンデコレーションの最大取付個数"
canImportAntennas: "アンテナのインポートを許可"
canImportBlocking: "ブロックのインポートを許可"

View File

@@ -2098,6 +2098,7 @@ _role:
canSearchNotes: "노트 검색 이용 가능 여부"
canSearchUsers: "유저 검색 이용"
canUseTranslator: "번역 기능의 사용"
canCreateChannel: "패널 생성"
avatarDecorationLimit: "아바타 장식의 최대 붙임 개수"
canImportAntennas: "안테나 가져오기 허용"
canImportBlocking: "차단 목록 가져오기 허용"

View File

@@ -1332,7 +1332,9 @@ overrideByAccount: "Переопределить этим аккаунтом"
untitled: "Без названия"
noName: "Имя не указано"
skip: "Пропустить"
restore: "Восстановить"
syncBetweenDevices: "Синхронизировать между устройствами"
paste: "вставить"
postForm: "Форма отправки"
textCount: "Количество символов"
information: "Описание"
@@ -2395,6 +2397,8 @@ _imageEffector:
opacity: "Непрозрачность"
lightness: "Осветление"
drafts: "Черновик"
_drafts:
restore: "Восстановить"
_qr:
showTabTitle: "Отображение"
raw: "Текст"

View File

@@ -1335,7 +1335,7 @@ markAsSensitiveConfirm: "ต้องการตั้งค่าสื่อ
unmarkAsSensitiveConfirm: "ต้องการยกเลิกการระบุว่าสื่อนี้มีเนื้อหาละเอียดอ่อนหรือไม่?"
preferences: "การตั้งค่าสภาพแวดล้อม"
accessibility: "การช่วยการเข้าถึง"
preferencesProfile: "โปรไฟล์การกำหนดค่า"
preferencesProfile: "โปรไฟล์ของการตั้งค่า"
copyPreferenceId: "คัดลือก ID การตั้งค่า"
resetToDefaultValue: "คืนค่าเป็นค่าเริ่มต้น"
overrideByAccount: "เขียนทับด้วยบัญชี"
@@ -1345,10 +1345,10 @@ skip: "ข้าม"
restore: "กู้คืน"
syncBetweenDevices: "ซิงค์ระหว่างอุปกรณ์"
preferenceSyncConflictTitle: "การตั้งค่ามีอยู่บนเซิร์ฟเวอร์"
preferenceSyncConflictText: "การตั้งค่าที่เปิดใช้งานการซิง์จะบันทึกค่าลงในเซิร์ฟเวอร์ อย่างไรก็ดี พบว่ามีค่าการตั้งค่านี้ที่เคยบันทึกไว้ในเซิร์ฟเวอร์แล้ว ต้องการดำเนินการอย่างไร?"
preferenceSyncConflictText: "รายการตั้งค่าที่เปิดการซิง์จะถูกบันทึกลงเซิร์ฟเวอร์ แต่รายการตั้งค่านี้ได้ถูกบันทึกลงเซิร์ฟเวอร์ไว้อยู่แล้ว ต้องการดำเนินการอย่างไร?"
preferenceSyncConflictChoiceMerge: "รวมเข้าด้วยกัน"
preferenceSyncConflictChoiceServer: "เขียนทับด้วยค่าการตั้งค่าเซิร์ฟเวอร์"
preferenceSyncConflictChoiceDevice: "เขียนทับด้วยค่าการตั้งค่าอุปกรณ์"
preferenceSyncConflictChoiceServer: "เขียนทับด้วยค่าการตั้งค่าของเซิร์ฟเวอร์"
preferenceSyncConflictChoiceDevice: "เขียนทับด้วยค่าการตั้งค่าของอุปกรณ์"
preferenceSyncConflictChoiceCancel: "ยกเลิกการเปิดใช้งานการซิงค์"
paste: "วาง"
emojiPalette: "จานสีเอโมจิ"
@@ -1408,6 +1408,7 @@ frame: "เฟรม"
presets: "พรีเซ็ต"
zeroPadding: "ห่างเป็น 0"
nothingToConfigure: "ไม่มีอะไรให้ต้ังค่า"
viewRenotedChannel: "แสดงช่องที่ถูกรีโน้ต"
_imageEditing:
_vars:
caption: "แคปชั่นของไฟล์"
@@ -2097,6 +2098,7 @@ _role:
canSearchNotes: "การใช้การค้นหาโน้ต"
canSearchUsers: "ค้นหาผู้ใช้"
canUseTranslator: "การใช้งานแปล"
canCreateChannel: "สร้างช่องใหม่"
avatarDecorationLimit: "จำนวนของตกแต่งไอคอนสูงสุดที่สามารถติดตั้งได้"
canImportAntennas: "อนุญาตให้นำเข้าเสาอากาศ"
canImportBlocking: "อนุญาตให้นำเข้าการบล็อก"
@@ -2136,7 +2138,7 @@ _sensitiveMediaDetection:
sensitivityDescription: "เมื่อความไวต่ำ Misdetection (ผลบวกลวง) จะลดลง, เมื่อความไวสูง Missed detection (ผลลบลวง) จะลดลง"
setSensitiveFlagAutomatically: "ทำเครื่องหมายว่ามีเนื้อหาละเอียดอ่อน"
setSensitiveFlagAutomaticallyDescription: "ผลลัพธ์ของการตรวจจับภายในนั้นจะยังคงอยู่ ถึงแม้ว่าจะปิดตัวเลือกนี้"
analyzeVideos: "เปิดใช้งานวิเคราะห์ของวิดีโอ"
analyzeVideos: "เปิดใช้งานวิเคราะห์วิดีโอ"
analyzeVideosDescription: "การวิเคราะห์วิดีโอนอกเหนือจากรูปภาพนั้น การทำสิ่งนี้จะทำให้เพิ่มภาระบนเซิร์ฟเวอร์เล็กน้อย"
_emailUnavailable:
used: "ที่อยู่อีเมลนี้ได้ถูกใช้ไปแล้ว"
@@ -2586,7 +2588,7 @@ _widgetOptions:
period: "ระยะเวลา"
_cw:
hide: "ซ่อน"
show: "โหลดเพิ่มเติม"
show: "ดเพิ่มเติม"
chars: "{count} ตัวอักษร"
files: "{count} ไฟล์"
_poll:
@@ -3029,7 +3031,7 @@ _externalResourceInstaller:
description: "เกิดปัญหาระหว่างการติดตั้งธีม กรุณาลองอีกครั้ง. รายละเอียดข้อผิดพลาดสามารถดูได้ในคอนโซล Javascript"
_dataSaver:
_media:
title: "โหลดสื่อ"
title: "ปิดใช้งานการโหลดสื่ออัตโนมัติ"
description: "กันไม่ให้ภาพและวิดีโอโหลดโดยอัตโนมัติ แตะรูปภาพ/วิดีโอที่ซ่อนอยู่เพื่อโหลด"
_avatar:
title: "ปิดใช้งานภาพเคลื่อนไหวของไอคอนประจำตัว"
@@ -3111,7 +3113,7 @@ _urlPreviewSetting:
summaryProxyDescription: "สร้างการแสดงตัวอย่างด้วย summary Proxy แทนที่จะใช้เนื้อหา Misskey"
summaryProxyDescription2: "พารามิเตอร์ต่อไปนี้จะถูกใช้เป็นสตริงการสืบค้นเพื่อเชื่อมต่อกับพร็อกซี หากฝั่งพร็อกซีไม่รองรับการตั้งค่าเหล่านี้จะถูกละเว้น"
_mediaControls:
pip: "รูปภาพในรูปภาม"
pip: "ภาพซ้อนภาพ (PiP)"
playbackRate: "ความเร็วในการเล่น"
loop: "เล่นวนซ้ำ"
_contextMenu:

View File

@@ -1408,6 +1408,7 @@ frame: "Çerçeve"
presets: "Ön ayar"
zeroPadding: "Sıfır doldurma"
nothingToConfigure: "Ayarlar seçeneği bulunmamaktadır."
viewRenotedChannel: "Show renoted channel"
_imageEditing:
_vars:
caption: "Dosya başlığı"

View File

@@ -63,7 +63,7 @@ copyUserId: "复制用户 ID"
copyNoteId: "复制帖子 ID"
copyFileId: "复制文件ID"
copyFolderId: "复制文件夹ID"
copyProfileUrl: "复制个人资料URL"
copyProfileUrl: "复制个人资料链接"
searchUser: "搜索用户"
searchThisUsersNotes: "搜索用户帖子"
reply: "回复"
@@ -101,12 +101,12 @@ somethingHappened: "出错了"
retry: "重试"
pageLoadError: "页面加载失败。"
pageLoadErrorDescription: "这通常是由于网络或浏览器缓存的原因。请清除缓存或等待片刻后重试。"
serverIsDead: "没有服务器响应。 请稍后再试。"
serverIsDead: "服务器响应。 请稍后再试。"
youShouldUpgradeClient: "请重新加载并使用新版本的客户端查看此页面。"
enterListName: "输入列表名称"
privacy: "隐私"
makeFollowManuallyApprove: "关注请求需要批准"
defaultNoteVisibility: "默认可见"
defaultNoteVisibility: "默认可见范围"
follow: "关注"
followRequest: "申请关注"
followRequests: "关注请求"
@@ -137,10 +137,10 @@ pinnedEmojisForReactionSettingDescription: "可以设置发表回应时置顶显
pinnedEmojisSettingDescription: "可以设置输入表情符号时置顶显示的表情符号"
emojiPickerDisplay: "选择器显示设置"
overwriteFromPinnedEmojisForReaction: "使用「置顶(回应)」设置覆盖"
overwriteFromPinnedEmojis: "全局设置覆盖"
overwriteFromPinnedEmojis: "使用全局设置覆盖"
reactionSettingDescription2: "拖动重新排序,单击删除,点击 + 添加。"
rememberNoteVisibility: "保存上次设置的可见性"
attachCancel: "取消添加附件"
attachCancel: "移除附件"
deleteFile: "删除文件"
markAsSensitive: "标记为敏感内容"
unmarkAsSensitive: "取消标记为敏感内容"
@@ -149,12 +149,12 @@ mute: "屏蔽"
unmute: "取消隐藏"
renoteMute: "隐藏转帖"
renoteUnmute: "取消隐藏转帖"
block: "屏蔽"
unblock: "取消屏蔽"
block: "禁止对方与我互动"
unblock: "允许对方与我互动"
suspend: "冻结"
unsuspend: "解除冻结"
blockConfirm: "确定要屏蔽吗?"
unblockConfirm: "确定要取消屏蔽吗?"
blockConfirm: "确定要禁止对方与我互动吗?"
unblockConfirm: "确定要允许对方与我互动吗?"
suspendConfirm: "要冻结吗?"
unsuspendConfirm: "要解除冻结吗?"
selectList: "选择列表"
@@ -184,7 +184,7 @@ flagAsCat: "喵!!!!!!!!!!!!"
flagAsCatDescription: "喵喵喵??"
flagShowTimelineReplies: "在时间线上显示帖子的回复"
flagShowTimelineRepliesDescription: "启用时,时间线除了显示用户的帖子外,还会显示其他用户对帖子的回复。"
autoAcceptFollowed: "自动允许回关请求"
autoAcceptFollowed: "自动允许我关注的人的关注请求"
addAccount: "添加账户"
reloadAccountsList: "更新账户列表"
loginFailed: "登录失败"
@@ -207,7 +207,7 @@ selectSelf: "选择自己"
selectUser: "选择用户"
recipient: "收件人"
annotation: "注解"
federation: "联"
federation: "联"
instances: "服务器"
registeredAt: "初次观测"
latestRequestReceivedAt: "上次收到的请求"
@@ -244,11 +244,11 @@ silencedInstances: "被静音的服务器"
silencedInstancesDescription: "设置要静音的服务器,以换行分隔。被静音的服务器内所有的账户都被视为「静音」状态,且关注操作均需要被批准。已被屏蔽的实例不受影响。"
mediaSilencedInstances: "已隐藏媒体文件的服务器"
mediaSilencedInstancesDescription: "设置要隐藏媒体文件的服务器,以换行分隔。被设置的服务器内所有账号的文件均按照「敏感内容」处理,且将无法使用自定义表情符号。已被屏蔽的实例不受影响。"
federationAllowedHosts: "允许联的服务器"
federationAllowedHostsDescription: "设定允许联的服务器,以换行分隔。"
muteAndBlock: "隐藏/屏蔽"
mutedUsers: "已隐藏的用户"
blockedUsers: "已屏蔽的用户"
federationAllowedHosts: "允许联邦交互的服务器"
federationAllowedHostsDescription: "设定允许联邦通信的服务器,以换行分隔。"
muteAndBlock: "屏蔽用户/禁止用户与我互动"
mutedUsers: "已屏蔽的用户"
blockedUsers: "禁止与我互动的用户"
noUsers: "无用户"
editProfile: "编辑资料"
noteDeleteConfirm: "确定要删除该帖子吗?"
@@ -261,7 +261,7 @@ default: "默认"
defaultValueIs: "默认值: {value}"
noCustomEmojis: "没有自定义表情符号"
noJobs: "没有任务"
federating: "联中"
federating: "联邦通信中"
blocked: "已屏蔽"
suspended: "停止投递"
all: "全部"
@@ -277,7 +277,7 @@ retypedNotMatch: "两次输入不一致!"
currentPassword: "现在的密码"
newPassword: "新密码"
newPasswordRetype: "重新输入密码:"
attachFile: "插入附件"
attachFile: "添加附件"
more: "更多!"
featured: "热门"
usernameOrUserId: "用户名或用户 ID"
@@ -342,7 +342,7 @@ selectFolders: "选择多个文件夹"
fileNotSelected: "未选择文件"
renameFile: "重命名文件"
folderName: "文件夹名称"
createFolder: "建文件夹"
createFolder: "建文件夹"
renameFolder: "重命名文件夹"
deleteFolder: "删除文件夹"
folder: "文件夹"
@@ -353,7 +353,7 @@ emptyFolder: "此文件夹中无文件"
dropHereToUpload: "将文件拖动到这里来上传"
unableToDelete: "无法删除"
inputNewFileName: "请输入新文件名"
inputNewDescription: "请输入新标题"
inputNewDescription: "请输入新的描述文本"
inputNewFolderName: "请输入新文件夹名"
circularReferenceFolder: "目标文件夹是要移动的文件夹的子文件夹。"
hasChildFilesOrFolders: "此文件夹中有文件,无法删除。"
@@ -396,10 +396,10 @@ driveCapacityPerLocalAccount: "每个用户的网盘容量"
driveCapacityPerRemoteAccount: "每个远程用户的网盘容量"
inMb: "以兆字节(MegaByte)为单位"
bannerUrl: "横幅 URL"
backgroundImageUrl: "背景图 URL"
backgroundImageUrl: "背景图片的链接"
basicInfo: "基本信息"
pinnedUsers: "置顶用户"
pinnedUsersDescription: "输入您想要固定到“发现页面的用户,一行一个。"
pinnedUsersDescription: "在「发现页面中使用换行标记想要置顶的用户。"
pinnedPages: "固定页面"
pinnedPagesDescription: "输入您要固定到服务器首页的页面路径,以换行符分隔。"
pinnedClipId: "置顶的便签 ID"
@@ -432,7 +432,7 @@ antennaExcludeBots: "排除机器人账户"
antennaKeywordsDescription: "AND 条件用空格分隔OR 条件用换行符分隔。"
notifyAntenna: "开启通知"
withFileAntenna: "仅带有附件的帖子"
excludeNotesInSensitiveChannel: "排除敏感频道的帖子"
excludeNotesInSensitiveChannel: "排除敏感频道的帖子"
enableServiceworker: "启用 ServiceWorker"
antennaUsersDescription: "指定用户名,用换行符进行分隔"
caseSensitive: "区分大小写"
@@ -476,7 +476,7 @@ passwordLessLogin: "无密码登录"
passwordLessLoginDescription: "不使用密码,仅使用安全密钥或 Passkey 登录"
resetPassword: "重置密码"
newPasswordIs: "新的密码是「{password}」"
reduceUiAnimation: "减少 UI 动"
reduceUiAnimation: "减少 UI 动"
share: "分享"
notFound: "未找到"
notFoundDescription: "没有与指定 URL 对应的页面。"
@@ -543,7 +543,7 @@ regenerate: "重新生成"
fontSize: "字体大小"
mediaListWithOneImageAppearance: "仅一张图片的媒体列表高度"
limitTo: "上限为 {x}"
showMediaListByGridInWideArea: "在大屏幕上并排显示媒体列表"
showMediaListByGridInWideArea: "在宽屏上并排显示媒体列表"
noFollowRequests: "没有关注请求"
openImageInNewTab: "在新标签页中打开图片"
dashboard: "管理面板"
@@ -581,7 +581,7 @@ s3ForcePathStyleDesc: "启用 s3ForcePathStyle 会强制将存储桶名称指定
serverLogs: "服务器日志"
deleteAll: "全部删除"
showFixedPostForm: "在时间线顶部显示发帖框"
showFixedPostFormInChannel: "在时间线顶部显示发帖对话框(频道)"
showFixedPostFormInChannel: "在时间线顶部显示发帖框(频道)"
withRepliesByDefaultForNewlyFollowed: "在时间线中默认包含新关注用户的回复"
newNoteRecived: "有新的帖子"
newNote: "新帖子"
@@ -656,7 +656,7 @@ expandTweet: "展开帖子"
themeEditor: "主题编辑器"
description: "描述"
describeFile: "添加描述"
enterFileDescription: "输入标题"
enterFileDescription: "输入描述文本"
author: "作者"
leaveConfirm: "存在未保存的更改。要放弃更改吗?"
manage: "管理"
@@ -664,7 +664,7 @@ plugins: "插件"
preferencesBackups: "备份设置"
deck: "Deck"
undeck: "取消 Deck"
useBlurEffectForModal: "对话框使用模糊效果"
useBlurEffectForModal: "发帖背景使用模糊效果"
useFullReactionPicker: "使用全功能的回应工具栏"
width: "宽度"
height: "高度"
@@ -773,13 +773,13 @@ yes: "是"
no: "否"
driveFilesCount: "网盘的文件数"
driveUsage: "网盘的空间用量"
noCrawle: "要求搜索引擎索引该用户"
noCrawleDescription: "要求搜索引擎不要收录(索引)您的用户页面,帖子,页面等。"
noCrawle: "拒绝搜索引擎索引"
noCrawleDescription: "拒绝搜索引擎收录(索引)您的个人资料,帖子,页面等。"
lockedAccountInfo: "即使启用该功能,只要帖子可见范围不是「仅关注者」,任何人都可以看到您的帖子。"
alwaysMarkSensitive: "默认将媒体文件标记为敏感内容"
loadRawImages: "添加附件图像的缩略图时使用原始图像质量"
disableShowingAnimatedImages: "不播放动"
disableShowingAnimatedImages_caption: "如果即使关闭了此设置但动画仍无法播放,可能是浏览器或操作系统的辅助功能设置,又或者是省电设置等产生了干扰。"
disableShowingAnimatedImages: "不播放动态图像"
disableShowingAnimatedImages_caption: "如果即使禁用了此设置,动态图像仍无法播放,可能是由于浏览器或操作系统的辅助功能设置、省电设置或其他因素所致。"
highlightSensitiveMedia: "高亮显示敏感媒体"
verificationEmailSent: "已发送确认电子邮件。请访问电子邮件中的链接以完成设置。"
notSet: "未设置"
@@ -851,7 +851,7 @@ fullView: "全屏"
quitFullView: "退出全屏"
addDescription: "添加描述"
userPagePinTip: "在帖子的菜单中选择“置顶”,即可显示该条帖子。"
notSpecifiedMentionWarning: "有未指定的提及"
notSpecifiedMentionWarning: "有未添加到收件人的提及"
info: "关于"
userInfo: "用户信息"
unknown: "未知"
@@ -877,9 +877,9 @@ noMaintainerInformationWarning: "尚未设置管理员信息。"
noInquiryUrlWarning: "尚未设置联络地址。"
noBotProtectionWarning: "尚未设置 Bot 防御。"
configure: "设置"
postToGallery: "创建新图集"
postToGallery: "发表相册"
postToHashtag: "发布至该话题"
gallery: "图集"
gallery: "相册"
recentPosts: "最新发布"
popularPosts: "热门投稿"
shareWithNote: "分享到帖文"
@@ -1032,7 +1032,7 @@ browserPushNotificationDisabledDescription: "{serverName}无权限发送通知
windowMaximize: "最大化"
windowMinimize: "最小化"
windowRestore: "还原"
caption: "标题"
caption: "描述文本"
loggedInAsBot: "以 Bot 账户登录"
tools: "工具"
cannotLoad: "无法加载"
@@ -1073,7 +1073,7 @@ thisPostMayBeAnnoying: "这个帖子可能会让其他人感到困扰。"
thisPostMayBeAnnoyingHome: "发到首页"
thisPostMayBeAnnoyingCancel: "取消"
thisPostMayBeAnnoyingIgnore: "就这样发布"
collapseRenotes: "省略显示已经看过的转发内容"
collapseRenotes: "折叠已经看过的转"
collapseRenotesDescription: "折叠显示回应或转发过的帖文。"
internalServerError: "内部服务器错误"
internalServerErrorDescription: "内部服务器发生了预期外的错误"
@@ -1081,9 +1081,9 @@ copyErrorInfo: "复制错误信息"
joinThisServer: "在本服务器上注册"
exploreOtherServers: "探索其他服务器"
letsLookAtTimeline: "看看时间线"
disableFederationConfirm: "确定要禁用联"
disableFederationConfirmWarn: "禁用联不会将帖子设为私有。在大多数情况下,不需要禁用联。"
disableFederationOk: "联合禁用"
disableFederationConfirm: "确定要禁用联邦交互"
disableFederationConfirmWarn: "即使禁用联邦交互,也不会将帖子设为私有。在大多数情况下,没有必要禁用联邦交互。"
disableFederationOk: "禁用联邦"
invitationRequiredToRegister: "此服务器目前只允许拥有邀请码的人注册。"
emailNotSupported: "此服务器不支持发送邮件"
postToTheChannel: "发布到频道"
@@ -1093,7 +1093,7 @@ likeOnly: "仅点赞"
likeOnlyForRemote: "全部(远程仅点赞)"
nonSensitiveOnly: "仅限非敏感内容"
nonSensitiveOnlyForLocalLikeOnlyForRemote: "仅限非敏感内容(远程仅点赞)"
rolesAssignedToMe: "指派给自己的角色"
rolesAssignedToMe: "的角色"
resetPasswordConfirm: "确定重置密码?"
sensitiveWords: "敏感词"
sensitiveWordsDescription: "包含这些词的帖子将只在首页可见。可用换行来设定多个词。"
@@ -1148,7 +1148,7 @@ pleaseAgreeAllToContinue: "必须全部勾选「同意」才能够继续。"
continue: "继续"
preservedUsernames: "保留的用户名"
preservedUsernamesDescription: "列出需要保留的用户名,使用换行来作为分割。被指定的用户名在建立账户时无法使用,但由管理员所创建的账户不受该限制。此外,现有的账户也不会受到影响。"
createNoteFromTheFile: "从文件创建帖子"
createNoteFromTheFile: "使用该文件发帖"
archive: "归档"
archived: "已归档"
unarchive: "取消归档"
@@ -1158,7 +1158,7 @@ thisChannelArchived: "该频道已被归档。"
displayOfNote: "显示帖子"
initialAccountSetting: "初始设定"
youFollowing: "正在关注"
preventAiLearning: "拒绝接受生成式 AI 的学习"
preventAiLearning: "拒绝用于训练生成式 AI"
preventAiLearningDescription: "要求文章生成 AI 或图像生成 AI 不能够以发布的帖子和图像等内容作为学习对象。这是通过在 HTML 响应中包含 noai 标志来实现的,这不能完全阻止 AI 学习你的发布内容,并不是所有 AI 都会遵守这类请求。"
options: "选项"
specifyUser: "指定用户"
@@ -1226,8 +1226,8 @@ notificationRecieveConfig: "通知接收设置"
mutualFollow: "互相关注"
followingOrFollower: "关注中或关注者"
fileAttachedOnly: "仅限媒体"
showRepliesToOthersInTimeline: "在时间线中包含给别人的回复"
hideRepliesToOthersInTimeline: "在时间线中隐藏给别人的回复"
showRepliesToOthersInTimeline: "在时间线中显示对他人的回复"
hideRepliesToOthersInTimeline: "在时间线中隐藏对他人的回复"
showRepliesToOthersInTimelineAll: "在时间线中显示所有现在关注的人的回复"
hideRepliesToOthersInTimelineAll: "在时间线中隐藏所有现在关注的人的回复"
confirmShowRepliesAll: "此操作不可撤销。确认要在时间线中显示所有现在关注的人的回复吗?"
@@ -1325,22 +1325,22 @@ lockdown: "锁定"
pleaseSelectAccount: "请选择帐户"
availableRoles: "可用角色"
acknowledgeNotesAndEnable: "理解注意事项后再开启。"
federationSpecified: "此服务器已开启联白名单。只能与管理员指定的服务器通信。"
federationDisabled: "此服务器已禁用联。无法与其它服务器上的用户通信。"
federationSpecified: "此服务器已开启联白名单模式。只能与管理员指定的服务器通信。"
federationDisabled: "此服务器已禁用联邦功能。无法与其它服务器上的用户通信。"
draft: "草稿"
draftsAndScheduledNotes: "草稿和定时发送"
confirmOnReact: "发送回应前需要确认"
reactAreYouSure: "要用「{emoji}」进行回应吗?"
markAsSensitiveConfirm: "要将此媒体标记为敏感吗?"
unmarkAsSensitiveConfirm: "要将此媒体解除敏感标记吗?"
markAsSensitiveConfirm: "确定标记此媒体为敏感内容吗?"
unmarkAsSensitiveConfirm: "确定取消标记为敏感内容吗?"
preferences: "偏好设置"
accessibility: "辅助功能"
preferencesProfile: "设置的配置文件"
copyPreferenceId: "复制设置 ID"
resetToDefaultValue: "重置为默认值"
overrideByAccount: "覆盖账号"
overrideByAccount: "使用账户设置"
untitled: "未命名"
noName: "没有名字"
noName: "未命名"
skip: "跳过"
restore: "恢复"
syncBetweenDevices: "设备间同步"
@@ -1351,7 +1351,7 @@ preferenceSyncConflictChoiceServer: "服务器上的设定值"
preferenceSyncConflictChoiceDevice: "设备上的设定值"
preferenceSyncConflictChoiceCancel: "取消同步"
paste: "粘贴"
emojiPalette: "表情符号调色板"
emojiPalette: "表情符号选择器"
postForm: "发帖窗口"
textCount: "字数"
information: "关于"
@@ -1368,7 +1368,7 @@ embed: "嵌入"
settingsMigrating: "正在迁移设置,请稍候。(之后也可以在设置 → 其它 → 迁移旧设置来手动迁移)"
readonly: "只读"
goToDeck: "返回至 Deck"
federationJobs: "联作业"
federationJobs: "联作业"
driveAboutTip: "网盘可以显示以前上传的文件。<br>\n也可以在发布帖子时重复使用文件或在发布帖子前预先上传文件。<br>\n<b>删除文件时,其将从至今为止所有用到该文件的地方(如帖子、页面、头像、横幅)消失。</b><br>\n也可以新建文件夹来整理文件。"
scrollToClose: "滑动并关闭"
advice: "建议"
@@ -1382,7 +1382,7 @@ unmuteX: "取消对{x}的隐藏"
abort: "中止"
tip: "提示和技巧"
redisplayAllTips: "重新显示所有的提示和技巧"
hideAllTips: "隐藏所有的提示技巧"
hideAllTips: "隐藏所有的提示技巧"
defaultImageCompressionLevel: "默认图像压缩等级"
defaultImageCompressionLevel_description: "较低的等级可以保持画质,但会增加文件大小。<br>较高的等级可以减少文件大小,但相对应的画质将会降低。"
defaultCompressionLevel: "默认压缩等级"
@@ -1394,7 +1394,7 @@ pluginsAreDisabledBecauseSafeMode: "因启用了安全模式,所有插件均
customCssIsDisabledBecauseSafeMode: "因启用了安全模式,无法应用自定义 CSS。"
themeIsDefaultBecauseSafeMode: "启用安全模式时将使用默认主题。关闭安全模式后将还原。"
thankYouForTestingBeta: "感谢您协助测试 beta 版!"
createUserSpecifiedNote: "创建指定用户的帖子"
createUserSpecifiedNote: "提及该用户并发帖"
schedulePost: "定时发布"
scheduleToPostOnX: "预定在 {x} 发出"
scheduledToPostOnX: "已预定在 {x} 发出"
@@ -1511,10 +1511,10 @@ _chat:
mutual: "仅相互关注"
none: "没有人"
_emojiPalette:
palettes: "调色板"
enableSyncBetweenDevicesForPalettes: "启用调色板的设备间同步"
paletteForMain: "主调色板"
paletteForReaction: "回应用调色板"
palettes: "表情符号托盘"
enableSyncBetweenDevicesForPalettes: "在设备间同步表情符号托盘"
paletteForMain: "主表情符号托盘"
paletteForReaction: "回应时的表情符号托盘"
_settings:
driveBanner: "可在此管理和设置网盘、确认使用量及配置上传文件的设置。"
pluginBanner: "使用插件可以扩展客户端的功能。可以在此安装、单独管理插件。"
@@ -1535,9 +1535,9 @@ _settings:
timelineAndNote: "时间线和帖子"
makeEveryTextElementsSelectable: "使所有的文字均可选择"
makeEveryTextElementsSelectable_description: "若开启,在某些情况下可能降低用户体验。"
useStickyIcons: "使图标跟随滚动"
useStickyIcons: "用户头像跟随页面滚动"
enableHighQualityImagePlaceholders: "显示高质量图像的占位符"
uiAnimations: "UI 动"
uiAnimations: "UI 动"
showNavbarSubButtons: "在导航栏中显示副按钮"
ifOn: "启用时"
ifOff: "关闭时"
@@ -1552,7 +1552,7 @@ _settings:
showAvailableReactionsFirstInNote: "在顶部显示可用的回应"
showPageTabBarBottom: "在下方显示页面标签栏"
emojiPaletteBanner: "可以将固定显示在表情符号选择器中的预设注册为调色板,也可以自定义表情符号选择器的显示方式。"
enableAnimatedImages: "启用动图像"
enableAnimatedImages: "启用动图像"
settingsPersistence_title: "设置持久化"
settingsPersistence_description1: "启用设置持久化可防止设置信息丢失。"
settingsPersistence_description2: "根据环境不同,有可能无法开启。"
@@ -1580,12 +1580,12 @@ _accountSettings:
requireSigninToViewContents: "需要登录才能显示内容"
requireSigninToViewContentsDescription1: "您发布的所有帖子将变成需要登入后才会显示。有望防止爬虫收集各种信息。"
requireSigninToViewContentsDescription2: "没有 URL 预览OGP、内嵌网页、引用帖子的功能的服务器也将无法显示。"
requireSigninToViewContentsDescription3: "这些限制可能不适用于联合到远程服务器的内容。"
requireSigninToViewContentsDescription3: "对于已通过联邦分发到远程服务器的内容,这些限制可能不适用。"
makeNotesFollowersOnlyBefore: "可将过去的帖子设为仅关注者可见"
makeNotesFollowersOnlyBeforeDescription: "开启此设定时,超过设定的时间或日期后,帖子将变为仅关注者可见。关闭后帖子的公开状态将恢复成原本的设定。"
makeNotesHiddenBefore: "将过去的帖子设为私密"
makeNotesHiddenBeforeDescription: "开启此设定时,超过设定的时间或日期后,帖子将变为仅自己可见。关闭后帖子的公开状态将恢复成原本的设定。"
mayNotEffectForFederatedNotes: "远程服务器联合的帖子在远端可能会没有效果。"
mayNotEffectForFederatedNotes: "对于已通过联邦投递到远程服务器的帖子,此操作在远端可能无法生效。"
mayNotEffectSomeSituations: "此限制功能非常简单,在与远程服务器联合等情形时可能不适用。"
notesHavePassedSpecifiedPeriod: "超过指定时间的帖子"
notesOlderThanSpecifiedDateAndTime: "指定日期前的帖子"
@@ -1637,7 +1637,7 @@ _announcement:
_initialAccountSetting:
accountCreated: "账户创建完成了!"
letsStartAccountSetup: "马上来进行账户的初始设定吧。"
letsFillYourProfile: "首先,来设定你的个人档案吧!"
letsFillYourProfile: "首先,设置一下您的个人资料吧!"
profileSetting: "个人资料设置"
privacySetting: "隐私设置"
theseSettingsCanEditLater: "也可以在稍后修改这里的设置。"
@@ -1689,10 +1689,10 @@ _initialTutorial:
public: "向所有用户公开。\n"
home: "仅在首页时间线上发布。 关注者、从个人资料页查看过来的用户、以及通过转帖也能被别的用户看见。"
followers: "仅对关注者可见。 除了您自己之外,没有人可以转贴,并且只有您的关注者可以查看它。\n"
direct: "它将仅向指定用户公开,并且他们也会收到通知。 您可以使用它来代替私信。\n"
direct: "仅对指定用户公开,且收件人将收到通知。"
doNotSendConfidencialOnDirect1: "发送敏感信息时请注意。\n"
doNotSendConfidencialOnDirect2: "目标服务器的管理员可以看到发布的内容,因此如果您向不受信任的服务器上的用户发送私信,则在处理敏感信息时需要小心。"
localOnly: "不将帖子推送到其它服务器。 无论上述公开范围如何,其它服务器的用户将无法看到附加了此设定的帖子。\n"
localOnly: "不将帖子通过联邦推送到其它服务器。 无论上述公开范围如何,其它服务器的用户将无法看到附加了此设定的帖子。\n"
_cw:
title: "隐藏内容 (CW)\n"
description: "显示「注解」里的内容而不是正文。点击「查看更多」将会把正文显示出来。"
@@ -1701,7 +1701,7 @@ _initialTutorial:
note: "茨了带巧克力的甜甜圈🍩😋"
useCases: "用于服务器条款所规定的帖子,或对剧透内容和敏感内容进行自主规制。"
_howToMakeAttachmentsSensitive:
title: "如何将附件标注为敏感内容?"
title: "如何标记附件为敏感内容?"
description: "对于服务器方针所要求要求的,又或者不适合直接展示的附件,请添加「敏感」标记。\n"
tryThisFile: "试试看,将附加到此窗口的图像标注为敏感!"
_exampleNote:
@@ -1746,7 +1746,7 @@ _serverSettings:
singleUserMode: "单用户模式"
singleUserMode_description: "若此服务器只有自己使用,开启此模式将最佳化性能。"
signToActivityPubGet: "对 GET 请求签名"
signToActivityPubGet_description: "通常情况下请保持启用。若遇到联通信方面的问题,将其关闭可能会有所改善,但另一方面有可能会造成无法通信。"
signToActivityPubGet_description: "通常情况下请保持启用。若遇到联通信方面的问题,将其关闭可能会有所改善,但另一方面有可能会造成无法通信。"
proxyRemoteFiles: "代理远程文件"
proxyRemoteFiles_description: "如果启用,远程服务器的文件将由代理提供。可有效保护图像预览缩略图的生成与用户隐私。"
allowExternalApRedirect: "允许通过 ActivityPub 重定向查询"
@@ -1771,7 +1771,7 @@ _accountMigration:
moveTo: "把这个账户迁移到新的账户"
moveToLabel: "迁移后的账户"
moveCannotBeUndone: "一旦迁移账户,就无法撤销。"
moveAccountDescription: "\n迁移到新帐户。\n ・现有的关注者自动关注新帐户\n ・此帐户的所有关注者都将被删除\n ・您将无法再使用此帐户发帖。\n关注者迁移是自动的但关注中迁移必须手动完成。请在迁移前在此帐户上导出关注列表并在迁移后立即在目标帐户上执行导入。\n列表、隐藏、屏蔽也是如此,因此您必须手动迁移它。\n此描述适用于该服务器Misskey v13.12.0 或更高版本)。其他 ActivityPub 软件(例如 Mastodon的行为可能有所不同。"
moveAccountDescription: "\n迁移到新帐户。\n ・现有的关注者自动关注新帐户\n ・此帐户的所有关注者都将被删除\n ・您将无法再使用此帐户发帖。\n关注者迁移是自动的但关注中迁移必须手动完成。请在迁移前在此帐户上导出关注列表并在迁移后立即在目标帐户上执行导入。\n列表、屏蔽列表、禁止与我互动的列表也是如此,因此您必须手动迁移它。\n此描述适用于该服务器Misskey v13.12.0 或更高版本)。其他 ActivityPub 软件(例如 Mastodon的行为可能有所不同。"
moveAccountHowTo: "要进行账户迁移,请现在目标账户中为此账户建立一个别名。\n建立别名后请像这样输入目标账户@username@server.example.com"
startMigration: "迁移"
migrationConfirm: "确定要把此账户迁移到 {account} 吗?一旦确定后,此操作无法取消,此账户也无法以原来的状态使用。\n同时请确认迁移后的账户已创造别名。"
@@ -1889,7 +1889,7 @@ _achievements:
description: "第一次将帖子加入收藏"
_myNoteFavorited1:
title: "想要星星"
description: "自己的帖子被其他人加入收藏了"
description: "自己的帖子被其他人收藏了"
_profileFilled:
title: "整装待发"
description: "设置了个人资料"
@@ -2083,7 +2083,7 @@ _role:
maxFileSize: "可上传的最大文件大小"
maxFileSize_caption: "可能在反向代理或 CDN 等前端存在其它设定值。"
alwaysMarkNsfw: "总是将文件标记为 NSFW"
canUpdateBioMedia: "可以更新头像和横幅"
canUpdateBioMedia: "允许更新头像和横幅"
pinMax: "帖子置顶数量限制"
antennaMax: "可创建的最大天线数量"
wordMuteMax: "折叠词的字数限制"
@@ -2098,9 +2098,10 @@ _role:
canSearchNotes: "是否可以搜索帖子"
canSearchUsers: "使用用户检索"
canUseTranslator: "使用翻译功能"
canCreateChannel: "创建频道"
avatarDecorationLimit: "可添加头像挂件的最大个数"
canImportAntennas: "允许导入天线"
canImportBlocking: "允许导入屏蔽列表"
canImportBlocking: "允许导入禁止与我互动的列表"
canImportFollowing: "允许导入关注列表"
canImportMuting: "允许导入隐藏列表"
canImportUserLists: "允许导入用户列表"
@@ -2108,7 +2109,7 @@ _role:
uploadableFileTypes: "可上传的文件类型"
uploadableFileTypes_caption: "指定 MIME 类型。可用换行指定多个类型,也可以用星号(*)作为通配符。(如 image/*"
uploadableFileTypes_caption2: "文件根据文件的不同,可能无法判断其类型。若要允许此类文件,请在指定中添加 {x}。"
noteDraftLimit: "可在服务器上创建多少草稿"
noteDraftLimit: "可在服务器上创建的草稿数量"
scheduledNoteLimit: "可同时创建的定时帖子数量"
watermarkAvailable: "能否使用水印功能"
_condition:
@@ -2165,7 +2166,7 @@ _ad:
back: "返回"
reduceFrequencyOfThisAd: "减少此广告的频率"
hide: "不显示"
timezoneinfo: "星期几是服务器的时区所指定的。"
timezoneinfo: "星期几是根据服务器的时区定的。"
adsSettings: "广告设置"
notesPerOneAd: "在实时更新时间线中插入广告的间隔(帖子个数)"
setZeroToDisable: "设为 0 将不在实时更新时间线中投放广告"
@@ -2229,7 +2230,7 @@ _aboutMisskey:
_displayOfSensitiveMedia:
respect: "隐藏敏感媒体"
ignore: "显示敏感媒体"
force: "隐藏所有内容"
force: "隐藏所有媒体"
_instanceTicker:
none: "不显示"
remote: "仅远程用户"
@@ -2253,7 +2254,7 @@ _channel:
allowRenoteToExternal: "允许转发到频道外和引用"
_menuDisplay:
sideFull: "横向"
sideIcon: "横向(图标)"
sideIcon: "横向图标"
top: "顶部"
hide: "隐藏"
_wordMute:
@@ -2315,7 +2316,7 @@ _theme:
mention: "提及"
mentionMe: "提及"
renote: "转发"
modalBg: "对话框背景"
modalBg: "发帖背景"
divider: "分割线"
scrollbarHandle: "滚动条"
scrollbarHandleHover: "滚动条(悬停)"
@@ -2334,7 +2335,7 @@ _theme:
fgHighlighted: "高亮显示文本"
_sfx:
note: "帖子"
noteMy: "我的帖子"
noteMy: "发帖"
notification: "通知"
reaction: "选择回应时"
chatMessage: "私信"
@@ -2403,8 +2404,8 @@ _2fa:
_permissions:
"read:account": "查看账户信息"
"write:account": "更改帐户信息"
"read:blocks": "查看屏蔽列表"
"write:blocks": "编辑屏蔽列表"
"read:blocks": "查看禁止与我互动的列表"
"write:blocks": "编辑禁止与我互动的列表"
"read:drive": "查看网盘"
"write:drive": "管理网盘文件"
"read:favorites": "查看收藏夹"
@@ -2429,10 +2430,10 @@ _permissions:
"write:user-groups": "编辑用户组"
"read:channels": "查看频道"
"write:channels": "管理频道"
"read:gallery": "浏览图集"
"write:gallery": "编辑图集"
"read:gallery-likes": "浏览喜欢的图集"
"write:gallery-likes": "管理喜欢的图集"
"read:gallery": "浏览相册"
"write:gallery": "管理相册"
"read:gallery-likes": "浏览喜欢的相册"
"write:gallery-likes": "管理喜欢的相册"
"read:flash": "查看 Play"
"write:flash": "编辑 Play"
"read:flash-likes": "查看 Play 的点赞"
@@ -2456,7 +2457,7 @@ _permissions:
"write:admin:unsuspend-user": "解除用户冻结"
"write:admin:meta": "编辑实例元数据"
"write:admin:user-note": "编辑管理笔记"
"write:admin:roles": "编辑角色"
"write:admin:roles": "管理角色"
"read:admin:roles": "查看角色"
"write:admin:relays": "编辑中继"
"read:admin:relays": "查看中继"
@@ -2466,8 +2467,8 @@ _permissions:
"read:admin:announcements": "查看公告"
"write:admin:avatar-decorations": "编辑头像挂件"
"read:admin:avatar-decorations": "查看头像挂件"
"write:admin:federation": "编辑联相关信息"
"write:admin:account": "编辑用户账户"
"write:admin:federation": "编辑联相关信息"
"write:admin:account": "管理用户账户"
"read:admin:account": "查看用户相关情报"
"write:admin:emoji": "编辑表情符号"
"read:admin:emoji": "查看表情符号"
@@ -2483,7 +2484,7 @@ _permissions:
"read:invite-codes": "获取已发行的邀请码"
"write:clip-favorite": "管理喜欢的便签"
"read:clip-favorite": "查看便签的点赞"
"read:federation": "查看联相关信息"
"read:federation": "查看联相关信息"
"write:report-abuse": "举报用户"
"write:chat": "撰写或删除消息"
"read:chat": "查看私信"
@@ -2530,7 +2531,7 @@ _widgets:
photos: "照片"
digitalClock: "数字时钟"
unixClock: "UNIX 时钟"
federation: "联"
federation: "联"
instanceCloud: "服务器球状列表"
postForm: "发帖窗口"
slideshow: "幻灯片展示"
@@ -2563,7 +2564,7 @@ _widgetOptions:
graduationDots: "点"
graduationArabic: "阿拉伯数字"
fadeGraduations: "淡化表盘"
sAnimation: "秒针动"
sAnimation: "秒针动"
sAnimationElastic: "跳动"
sAnimationEaseOut: "平滑"
twentyFour: "24 小时制"
@@ -2621,11 +2622,11 @@ _visibility:
followersDescription: "仅发送至关注者"
specified: "指定用户"
specifiedDescription: "仅发送至指定用户"
disableFederation: "不参与联合"
disableFederation: "仅限本地"
disableFederationDescription: "不发送到其他服务器"
_postForm:
quitInspiteOfThereAreUnuploadedFilesConfirm: "还有未上传的文件,要丢弃并关闭窗口吗?"
uploaderTip: "文件未上传。可以在文件菜单中进行重命名、裁剪、添加水印、设置是否压缩等操作。文件将在帖时自动上传。"
uploaderTip: "文件未上传。可以在文件菜单中设置重命名、裁剪图片、添加水印以及是否压缩等功能。文件将在帖子发布时自动上传。"
replyPlaceholder: "回复这个帖子..."
quotePlaceholder: "引用这个帖子..."
channelPlaceholder: "发布到频道…"
@@ -2660,12 +2661,12 @@ _profile:
metadataDescription: "最多可以在个人资料中以表格形式显示四条其他信息。"
metadataLabel: "标签"
metadataContent: "内容"
changeAvatar: "修改头像"
changeBanner: "修改横幅"
changeAvatar: "更换头像"
changeBanner: "更换横幅"
verifiedLinkDescription: "如果将内容设置为 URL当链接所指向的网页内包含自己的个人资料链接时可以显示一个已验证图标。"
avatarDecorationMax: "最多可添加 {max} 个挂件"
followedMessage: "被关注时显示的消息"
followedMessageDescription: "可以设置被关注时向对方显示的短消息。"
followedMessage: "被关注时的信息"
followedMessageDescription: "被关注时,可设置向关注者显示的息。"
followedMessageDescriptionForLockedAccount: "需要批准才能关注的情况下,消息会在请求被批准后显示。"
_exportOrImport:
allNotes: "所有帖子"
@@ -2673,13 +2674,13 @@ _exportOrImport:
clips: "便签"
followingList: "关注中"
muteList: "隐藏"
blockingList: "屏蔽"
blockingList: "禁止与我互动的列表"
userLists: "列表"
excludeMutingUsers: "排除已隐藏用户"
excludeInactiveUsers: "排除不活跃用户"
withReplies: "在时间线中包含导入用户的回复"
_charts:
federation: "联"
federation: "联"
apRequest: "请求"
usersIncDec: "用户数量:增加/减少"
usersTotal: "用户总数"
@@ -2977,7 +2978,7 @@ _moderationLogTypes:
deleteAccount: "删除帐户"
deletePage: "删除页面"
deleteFlash: "删除 Play"
deleteGalleryPost: "删除图集内容"
deleteGalleryPost: "删除相册内容"
deleteChatRoom: "删除群聊"
updateProxyAccountDescription: "更新代理账户的简介"
_fileViewer:
@@ -3034,7 +3035,7 @@ _dataSaver:
description: "防止自动加载图像和视频。 点击隐藏的图像/视频即可加载它们。\n"
_avatar:
title: "头像"
description: "停止播放头像的动画。 由于动画图片的文件大小可能比普通图像大,这可以进一步减少数据流量。"
description: "播放头像的动画。 由于动态图像的文件大小远大于一般图像,停止播放能够进一步节省数据流量。"
_urlPreviewThumbnail:
title: "不显示 URL预览缩略图"
description: "将不再加载 URL 预览缩略图。"
@@ -3053,7 +3054,7 @@ _reversi:
gameSettings: "对局设置"
chooseBoard: "选择棋盘"
blackOrWhite: "先手/后手"
blackIs: "{name}执黑(先手)"
blackIs: "{name}执黑先手"
rules: "规则"
thisGameIsStartedSoon: "对局即将开始"
waitingForOther: "等待对手准备"
@@ -3069,7 +3070,7 @@ _reversi:
surrendered: "已认输"
timeout: "超时"
drawn: "平局"
won: "{name}获胜"
won: "{name} 获胜"
black: "黑"
white: "白"
total: "总计"
@@ -3078,7 +3079,7 @@ _reversi:
allGames: "所有对局"
ended: "结束"
playing: "对局中"
isLlotheo: "落子少的一方获胜(又名奥赛罗"
isLlotheo: "落子少的一方获胜(黑白棋规则"
loopedMap: "循环棋盘"
canPutEverywhere: "无限制放置模式"
timeLimitForEachTurn: "1回合的时间限制"
@@ -3088,8 +3089,8 @@ _reversi:
shareToTlTheGameWhenStart: "开始时在时间线发布对局"
iStartedAGame: "对局开始!#MisskeyReversi"
opponentHasSettingsChanged: "对手更改了设定"
allowIrregularRules: "允许非常规规则(完全自由)"
disallowIrregularRules: "禁止非常规规则"
allowIrregularRules: "允许特殊规则(完全自由)"
disallowIrregularRules: "禁止特殊规则"
showBoardLabels: "显示行号和列号"
useAvatarAsStone: "用头像作为棋子"
_offlineScreen:
@@ -3246,7 +3247,7 @@ _search:
searchScopeLocal: "本地"
searchScopeServer: "指定服务器"
searchScopeUser: "指定用户"
pleaseEnterServerHost: "请填写服务器主机名"
pleaseEnterServerHost: "请填写服务器主机名"
pleaseSelectUser: "请选择用户"
serverHostPlaceholder: "如misskey.example.com"
_serverSetupWizard:
@@ -3275,13 +3276,13 @@ _serverSetupWizard:
largeScaleServerAdvice: "运营大规模服务器可能需要高级基础设施知识,如负载均衡和数据库复制。"
doYouConnectToFediverse: "要加入 Fediverse 吗?"
doYouConnectToFediverse_description1: "若加入由分散性服务器所构成的网络Fediverse将能与其它服务器交换内容。"
doYouConnectToFediverse_description2: "加入 Fediverse 在这里被称为「联」。"
youCanConfigureMoreFederationSettingsLater: "可在之后进行如哪些服务器可以进行联等高级设置。"
doYouConnectToFediverse_description2: "加入 Fediverse 在这里被称为「联」。"
youCanConfigureMoreFederationSettingsLater: "可在之后进行如哪些服务器允许进行联邦交互等高级设置。"
remoteContentsCleaning: "自动清理传入内容"
remoteContentsCleaning_description: "加入联合后,服务器将持续接收大量内容。打开自动清理后,将自动删除无法找到的旧内容,可节省存储空间。"
remoteContentsCleaning_description: "开启联邦互通后,服务器将持续接收大量内容。打开自动清理后,将自动删除无法找到的旧内容,可节省存储空间。"
adminInfo: "管理员信息"
adminInfo_description: "设置用于接受询问的管理员信息。"
adminInfo_mustBeFilled: "开放服务器或启了联的情况下必须输入。"
adminInfo_mustBeFilled: "开放服务器或启了联的情况下必须输入。"
followingSettingsAreRecommended: "推荐以下设置"
applyTheseSettings: "使用此设置"
skipSettings: "跳过设置"
@@ -3301,7 +3302,7 @@ _uploader:
doneConfirm: "部分文件尚未上传,是否继续?"
maxFileSizeIsX: "可上传最大 {x} 的文件。"
allowedTypes: "可上传的文件类型"
tip: "文件还没有被上传。在此对话框中进行上传前确认、重命名、压缩裁剪等操作。准备完成后,点击上传即可开始上传。"
tip: "文件尚未上传。在此对话框中,您可以进行上传前确认、重命名、压缩裁剪等操作。准备就绪后,点击上传”按钮即可开始上传。"
_clientPerformanceIssueTip:
title: "如果觉得电池耗电过高"
makeSureDisabledAdBlocker: "请关闭广告拦截器"

View File

@@ -1,12 +1,12 @@
{
"name": "misskey",
"version": "2026.5.1-alpha.0",
"version": "2026.5.1",
"codename": "nasubi",
"repository": {
"type": "git",
"url": "https://github.com/misskey-dev/misskey.git"
},
"packageManager": "pnpm@10.33.0",
"packageManager": "pnpm@10.33.2",
"workspaces": [
"packages/misskey-js",
"packages/i18n",
@@ -53,29 +53,29 @@
"cleanall": "pnpm clean-all"
},
"dependencies": {
"cssnano": "7.1.5",
"cssnano": "7.1.7",
"esbuild": "0.28.0",
"execa": "9.6.1",
"ignore-walk": "8.0.0",
"js-yaml": "4.1.1",
"postcss": "8.5.9",
"postcss": "8.5.10",
"tar": "7.5.13",
"terser": "5.46.1"
"terser": "5.46.2"
},
"devDependencies": {
"@eslint/js": "9.39.4",
"@misskey-dev/eslint-plugin": "2.1.0",
"@types/js-yaml": "4.0.9",
"@types/node": "24.12.2",
"@typescript-eslint/eslint-plugin": "8.58.2",
"@typescript-eslint/parser": "8.58.2",
"@typescript/native-preview": "7.0.0-dev.20260421.2",
"@typescript-eslint/eslint-plugin": "8.59.0",
"@typescript-eslint/parser": "8.59.0",
"@typescript/native-preview": "7.0.0-dev.20260426.1",
"cross-env": "10.1.0",
"cypress": "15.13.1",
"cypress": "15.14.1",
"eslint": "9.39.4",
"globals": "17.5.0",
"ncp": "2.0.0",
"pnpm": "10.33.0",
"pnpm": "10.33.2",
"start-server-and-test": "3.0.2",
"typescript": "5.9.3"
},

View File

@@ -53,8 +53,8 @@
"utf-8-validate": "6.0.6"
},
"dependencies": {
"@aws-sdk/client-s3": "3.1030.0",
"@aws-sdk/lib-storage": "3.1030.0",
"@aws-sdk/client-s3": "3.1037.0",
"@aws-sdk/lib-storage": "3.1037.0",
"@discordapp/twemoji": "16.0.1",
"@fastify/accepts": "5.0.4",
"@fastify/cors": "11.2.0",
@@ -65,26 +65,26 @@
"@kitajs/html": "4.2.13",
"@misskey-dev/sharp-read-bmp": "1.2.0",
"@misskey-dev/summaly": "5.3.0",
"@napi-rs/canvas": "0.1.97",
"@napi-rs/canvas": "0.1.100",
"@nestjs/common": "11.1.19",
"@nestjs/core": "11.1.19",
"@nestjs/testing": "11.1.19",
"@oxc-project/runtime": "0.125.0",
"@oxc-project/runtime": "0.127.0",
"@peertube/http-signature": "1.7.0",
"@sentry/node": "10.48.0",
"@sentry/profiling-node": "10.48.0",
"@sentry/node": "10.50.0",
"@sentry/profiling-node": "10.50.0",
"@simplewebauthn/server": "13.3.0",
"@sinonjs/fake-timers": "15.3.2",
"@smithy/node-http-handler": "4.5.2",
"@smithy/node-http-handler": "4.6.1",
"@twemoji/parser": "16.0.0",
"accepts": "1.3.8",
"ajv": "8.18.0",
"ajv": "8.20.0",
"archiver": "7.0.1",
"async-mutex": "0.5.0",
"bcryptjs": "3.0.3",
"blurhash": "2.0.5",
"body-parser": "2.2.2",
"bullmq": "5.73.5",
"bullmq": "5.76.2",
"cacheable-lookup": "7.0.0",
"chalk": "5.6.2",
"chalk-template": "1.1.2",
@@ -92,14 +92,14 @@
"color-convert": "3.1.3",
"content-disposition": "1.1.0",
"date-fns": "4.1.0",
"deep-email-validator": "0.1.21",
"deep-email-validator": "0.1.27",
"fastify": "5.8.5",
"fastify-raw-body": "5.0.0",
"feed": "5.2.0",
"feed": "5.2.1",
"file-type": "22.0.1",
"fluent-ffmpeg": "2.1.3",
"form-data": "4.0.5",
"got": "14.6.6",
"got": "15.0.3",
"hpagent": "1.2.0",
"http-link-header": "1.1.3",
"i18n": "workspace:*",
@@ -116,16 +116,16 @@
"misskey-js": "workspace:*",
"misskey-reversi": "workspace:*",
"ms": "3.0.0-canary.202508261828",
"nanoid": "5.1.7",
"nanoid": "5.1.9",
"nested-property": "4.0.0",
"node-fetch": "3.3.2",
"node-html-parser": "7.1.0",
"nodemailer": "8.0.5",
"nodemailer": "8.0.6",
"nsfwjs": "4.3.0",
"oauth2orize": "1.12.0",
"oauth2orize-pkce": "0.1.2",
"os-utils": "0.0.14",
"otpauth": "9.5.0",
"otpauth": "9.5.1",
"pg": "8.20.0",
"pkce-challenge": "6.0.0",
"probe-image-size": "7.2.3",
@@ -160,8 +160,7 @@
"@kitajs/ts-html-plugin": "4.1.4",
"@nestjs/platform-express": "11.1.19",
"@rollup/plugin-esm-shim": "0.1.8",
"@sentry/vue": "10.48.0",
"@simplewebauthn/types": "12.0.0",
"@sentry/vue": "10.50.0",
"@types/accepts": "1.3.7",
"@types/archiver": "7.0.0",
"@types/body-parser": "1.19.6",
@@ -192,9 +191,9 @@
"@types/vary": "1.1.3",
"@types/web-push": "3.6.4",
"@types/ws": "8.18.1",
"@typescript-eslint/eslint-plugin": "8.58.2",
"@typescript-eslint/parser": "8.58.2",
"@vitest/coverage-v8": "4.1.4",
"@typescript-eslint/eslint-plugin": "8.59.0",
"@typescript-eslint/parser": "8.59.0",
"@vitest/coverage-v8": "4.1.5",
"aws-sdk-client-mock": "4.1.0",
"cbor": "10.0.12",
"cross-env": "10.1.0",
@@ -206,8 +205,8 @@
"rolldown": "1.0.0-rc.15",
"simple-oauth2": "5.1.0",
"supertest": "7.2.2",
"vite": "8.0.8",
"vitest": "4.1.4",
"vite": "8.0.10",
"vitest": "4.1.5",
"vitest-mock-extended": "4.0.0"
}
}

View File

@@ -47,6 +47,7 @@ export type RolePolicies = {
canSearchUsers: boolean;
canUseTranslator: boolean;
canHideAds: boolean;
canCreateChannel: boolean;
driveCapacityMb: number;
maxFileSizeMb: number;
alwaysMarkNsfw: boolean;
@@ -88,6 +89,7 @@ export const DEFAULT_POLICIES: RolePolicies = {
canSearchUsers: true,
canUseTranslator: true,
canHideAds: false,
canCreateChannel: true,
driveCapacityMb: 100,
maxFileSizeMb: 30,
alwaysMarkNsfw: false,
@@ -410,6 +412,7 @@ export class RoleService implements OnApplicationShutdown, OnModuleInit {
canSearchUsers: calc('canSearchUsers', vs => vs.some(v => v === true)),
canUseTranslator: calc('canUseTranslator', vs => vs.some(v => v === true)),
canHideAds: calc('canHideAds', vs => vs.some(v => v === true)),
canCreateChannel: calc('canCreateChannel', vs => vs.some(v => v === true)),
driveCapacityMb: calc('driveCapacityMb', vs => Math.max(...vs)),
maxFileSizeMb: calc('maxFileSizeMb', vs => Math.max(...vs)),
alwaysMarkNsfw: calc('alwaysMarkNsfw', vs => vs.some(v => v === true)),

View File

@@ -24,7 +24,7 @@ import type {
PublicKeyCredentialCreationOptionsJSON,
PublicKeyCredentialRequestOptionsJSON,
RegistrationResponseJSON,
} from '@simplewebauthn/types';
} from '@simplewebauthn/server';
@Injectable()
export class WebAuthnService {

View File

@@ -313,7 +313,8 @@ export class ApInboxService {
// アナウンス先が許可されているかチェック
if (!this.utilityService.isFederationAllowedUri(uri)) return;
const unlock = await acquireApObjectLock(this.redisClient, uri);
const activityUri = getApId(activity);
const unlock = await acquireApObjectLock(this.redisClient, activityUri);
try {
// 既に同じURIを持つものが登録されていないかチェック

View File

@@ -224,6 +224,10 @@ export const packedRolePoliciesSchema = {
type: 'boolean',
optional: false, nullable: false,
},
canCreateChannel: {
type: 'boolean',
optional: false, nullable: false,
},
driveCapacityMb: {
type: 'integer',
optional: false, nullable: false,

View File

@@ -7,7 +7,7 @@ import { Inject, Injectable } from '@nestjs/common';
import cors from '@fastify/cors';
import multipart from '@fastify/multipart';
import { ModuleRef } from '@nestjs/core';
import { AuthenticationResponseJSON } from '@simplewebauthn/types';
import type { AuthenticationResponseJSON } from '@simplewebauthn/server';
import type { Config } from '@/config.js';
import type { InstancesRepository, AccessTokensRepository } from '@/models/_.js';
import { DI } from '@/di-symbols.js';

View File

@@ -28,7 +28,7 @@ import { LoggerService } from '@/core/LoggerService.js';
import { FastifyReplyError } from '@/misc/fastify-reply-error.js';
import { RateLimiterService } from './RateLimiterService.js';
import { SigninService } from './SigninService.js';
import type { AuthenticationResponseJSON } from '@simplewebauthn/types';
import type { AuthenticationResponseJSON } from '@simplewebauthn/server';
import type { FastifyReply, FastifyRequest } from 'fastify';
@Injectable()

View File

@@ -23,7 +23,7 @@ import { LoggerService } from '@/core/LoggerService.js';
import type { IdentifiableError } from '@/misc/identifiable-error.js';
import { RateLimiterService } from './RateLimiterService.js';
import { SigninService } from './SigninService.js';
import type { AuthenticationResponseJSON } from '@simplewebauthn/types';
import type { AuthenticationResponseJSON } from '@simplewebauthn/server';
import type { FastifyReply, FastifyRequest } from 'fastify';
@Injectable()

View File

@@ -22,6 +22,8 @@ export const meta = {
kind: 'write:channels',
requiredRolePolicy: 'canCreateChannel',
limit: {
duration: ms('1hour'),
max: 10,

View File

@@ -39,137 +39,6 @@ export const meta = {
res: {
type: 'object',
nullable: false,
optional: false,
properties: {
rp: {
type: 'object',
properties: {
id: {
type: 'string',
optional: true,
},
},
},
user: {
type: 'object',
properties: {
id: {
type: 'string',
},
name: {
type: 'string',
},
displayName: {
type: 'string',
},
},
},
challenge: {
type: 'string',
},
pubKeyCredParams: {
type: 'array',
items: {
type: 'object',
properties: {
type: {
type: 'string',
},
alg: {
type: 'number',
},
},
},
},
timeout: {
type: 'number',
nullable: true,
},
excludeCredentials: {
type: 'array',
nullable: true,
items: {
type: 'object',
properties: {
id: {
type: 'string',
},
type: {
type: 'string',
},
transports: {
type: 'array',
items: {
type: 'string',
enum: [
'ble',
'cable',
'hybrid',
'internal',
'nfc',
'smart-card',
'usb',
],
},
},
},
},
},
authenticatorSelection: {
type: 'object',
nullable: true,
properties: {
authenticatorAttachment: {
type: 'string',
enum: [
'cross-platform',
'platform',
],
},
requireResidentKey: {
type: 'boolean',
},
userVerification: {
type: 'string',
enum: [
'discouraged',
'preferred',
'required',
],
},
},
},
attestation: {
type: 'string',
nullable: true,
enum: [
'direct',
'enterprise',
'indirect',
'none',
null,
],
},
extensions: {
type: 'object',
nullable: true,
properties: {
appid: {
type: 'string',
nullable: true,
},
credProps: {
type: 'boolean',
nullable: true,
},
hmacCreateSecret: {
type: 'boolean',
nullable: true,
},
},
},
},
},
} as const;

View File

@@ -1,6 +1,6 @@
services:
nginx:
image: nginx:1.29
image: nginx:1.30
volumes:
- type: bind
source: ./certificates/rootCA.crt

View File

@@ -18,7 +18,7 @@ import type {
PublicKeyCredentialCreationOptionsJSON,
PublicKeyCredentialRequestOptionsJSON,
RegistrationResponseJSON,
} from '@simplewebauthn/types';
} from '@simplewebauthn/server';
import type * as misskey from 'misskey-js';
import { describe, beforeAll, test } from 'vitest';

View File

@@ -8,7 +8,7 @@ import { afterAll, beforeAll, beforeEach, describe, expect, it, vi } from 'vites
import { mockDeep } from 'vitest-mock-extended';
import { Test, TestingModule } from '@nestjs/testing';
import { FastifyReply, FastifyRequest } from 'fastify';
import { AuthenticationResponseJSON } from '@simplewebauthn/types';
import type { AuthenticationResponseJSON } from '@simplewebauthn/server';
import { HttpHeader } from 'fastify/types/utils.js';
import { MiUser } from '@/models/User.js';
import { MiUserProfile, UserProfilesRepository, UsersRepository } from '@/models/_.js';

View File

@@ -12,15 +12,15 @@
"devDependencies": {
"@types/estree": "1.0.8",
"@types/node": "24.12.2",
"@typescript-eslint/eslint-plugin": "8.58.2",
"@typescript-eslint/parser": "8.58.2",
"rollup": "4.60.1"
"@typescript-eslint/eslint-plugin": "8.59.0",
"@typescript-eslint/parser": "8.59.0",
"rollup": "4.60.2"
},
"dependencies": {
"estree-walker": "3.0.3",
"i18n": "workspace:*",
"magic-string": "0.30.21",
"rolldown": "1.0.0-rc.15",
"vite": "8.0.8"
"vite": "8.0.10"
}
}

View File

@@ -24,11 +24,11 @@
"mfm-js": "0.25.0",
"misskey-js": "workspace:*",
"punycode.js": "2.3.1",
"rollup": "4.60.1",
"rollup": "4.60.2",
"shiki": "4.0.2",
"tinycolor2": "1.6.0",
"uuid": "13.0.0",
"vue": "3.5.32"
"uuid": "14.0.0",
"vue": "3.5.33"
},
"devDependencies": {
"@misskey-dev/summaly": "5.3.0",
@@ -40,26 +40,26 @@
"@types/punycode.js": "npm:@types/punycode@2.1.4",
"@types/tinycolor2": "1.4.6",
"@types/ws": "8.18.1",
"@typescript-eslint/eslint-plugin": "8.58.2",
"@typescript-eslint/parser": "8.58.2",
"@vitest/coverage-v8": "4.1.4",
"@vue/runtime-core": "3.5.32",
"@typescript-eslint/eslint-plugin": "8.59.0",
"@typescript-eslint/parser": "8.59.0",
"@vitest/coverage-v8": "4.1.5",
"@vue/runtime-core": "3.5.33",
"acorn": "8.16.0",
"cross-env": "10.1.0",
"eslint-plugin-import": "2.32.0",
"eslint-plugin-vue": "10.8.0",
"eslint-plugin-vue": "10.9.0",
"happy-dom": "20.9.0",
"intersection-observer": "0.12.2",
"micromatch": "4.0.8",
"msw": "2.13.3",
"msw": "2.13.6",
"prettier": "3.8.3",
"sass-embedded": "1.99.0",
"start-server-and-test": "3.0.2",
"tsx": "4.21.0",
"vite": "8.0.8",
"vite": "8.0.10",
"vite-plugin-turbosnap": "1.0.3",
"vue-component-type-helpers": "3.2.6",
"vue-component-type-helpers": "3.2.7",
"vue-eslint-parser": "10.4.0",
"vue-tsc": "3.2.6"
"vue-tsc": "3.2.7"
}
}

View File

@@ -28,7 +28,7 @@ import { postMessageToParentWindow, setIframeId } from '@/post-message.js';
import { serverContext } from '@/server-context.js';
import { i18n } from '@/i18n.js';
import type { Theme } from '@/theme.js';
import type { Theme } from '@@/js/theme.js';
console.log('Misskey Embed');

View File

@@ -5,26 +5,10 @@
// TODO: (可能な部分を)sharedに抽出して frontend と共通化
import tinycolor from 'tinycolor2';
import lightTheme from '@@/themes/_light.json5';
import darkTheme from '@@/themes/_dark.json5';
import type { BundledTheme } from 'shiki/themes';
export type Theme = {
id: string;
name: string;
author: string;
desc?: string;
base?: 'dark' | 'light';
props: Record<string, string>;
codeHighlighter?: {
base: BundledTheme;
overrides?: Record<string, any>;
} | {
base: '_none_';
overrides: Record<string, any>;
};
};
import { compile } from '@@/js/theme.js';
import type { Theme } from '@@/js/theme.js';
let timeout: number | null = null;
@@ -32,7 +16,7 @@ export function assertIsTheme(theme: Record<string, unknown>): theme is Theme {
return typeof theme === 'object' && theme !== null && 'id' in theme && 'name' in theme && 'author' in theme && 'props' in theme;
}
export function applyTheme(theme: Theme, persist = true) {
export function applyTheme(theme: Theme) {
if (timeout) window.clearTimeout(timeout);
window.document.documentElement.classList.add('_themeChanging_');
@@ -68,48 +52,3 @@ export function applyTheme(theme: Theme, persist = true) {
// iframeを正常に透過させるために、cssのcolor-schemeは `light dark;` 固定にしてある。style.scss参照
}
function compile(theme: Theme): Record<string, string> {
function getColor(val: string): tinycolor.Instance {
if (val[0] === '@') { // ref (prop)
return getColor(theme.props[val.substring(1)]);
} else if (val[0] === '$') { // ref (const)
return getColor(theme.props[val]);
} else if (val[0] === ':') { // func
const parts = val.split('<');
const funcTxt = parts.shift();
const argTxt = parts.shift();
if (funcTxt && argTxt) {
const func = funcTxt.substring(1);
const arg = parseFloat(argTxt);
const color = getColor(parts.join('<'));
switch (func) {
case 'darken': return color.darken(arg);
case 'lighten': return color.lighten(arg);
case 'alpha': return color.setAlpha(arg);
case 'hue': return color.spin(arg);
case 'saturate': return color.saturate(arg);
}
}
}
// other case
return tinycolor(val);
}
const props = {};
for (const [k, v] of Object.entries(theme.props)) {
if (k.startsWith('$')) continue; // ignore const
props[k] = v.startsWith('"') ? v.replace(/^"\s*/, '') : genValue(getColor(v));
}
return props;
}
function genValue(c: tinycolor.Instance): string {
return c.toRgbString();
}

View File

@@ -0,0 +1,13 @@
/*
* SPDX-FileCopyrightText: syuilo and misskey-project
* SPDX-License-Identifier: AGPL-3.0-only
*/
declare module '@@/themes/*.json5' {
import { Theme } from '@@/js/theme.js';
const theme: Theme;
// eslint-disable-next-line import/no-default-export
export default theme;
}

View File

@@ -1,109 +0,0 @@
import fs from 'node:fs';
import { fileURLToPath } from 'node:url';
import { dirname } from 'node:path';
import * as esbuild from 'esbuild';
import { build } from 'esbuild';
import { execa } from 'execa';
const _filename = fileURLToPath(import.meta.url);
const _dirname = dirname(_filename);
const _package = JSON.parse(fs.readFileSync(_dirname + '/package.json', 'utf-8'));
const entryPoints = fs.globSync('./js/**/**.{ts,tsx}');
/** @type {import('esbuild').BuildOptions} */
const options = {
entryPoints,
minify: process.env.NODE_ENV === 'production',
outdir: './js-built',
target: 'es2022',
platform: 'browser',
format: 'esm',
sourcemap: 'linked',
};
const args = process.argv.slice(2).map(arg => arg.toLowerCase());
// js-built配下をすべて削除する
if (!args.includes('--no-clean')) {
fs.rmSync('./js-built', { recursive: true, force: true });
}
if (args.includes('--watch')) {
await watchSrc();
} else {
await buildSrc();
}
async function buildSrc() {
console.log(`[${_package.name}] start building...`);
await build(options)
.then(() => {
console.log(`[${_package.name}] build succeeded.`);
})
.catch((err) => {
process.stderr.write(err.stderr);
process.exit(1);
});
if (process.env.NODE_ENV === 'production') {
console.log(`[${_package.name}] skip building d.ts because NODE_ENV is production.`);
} else {
await buildDts();
}
fs.copyFileSync('./js/emojilist.json', './js-built/emojilist.json');
console.log(`[${_package.name}] finish building.`);
}
function buildDts() {
return execa(
'tsgo',
[
'--project', 'tsconfig.json',
'--outDir', 'js-built',
'--declaration', 'true',
'--emitDeclarationOnly', 'true',
],
{
stdout: process.stdout,
stderr: process.stderr,
},
);
}
async function watchSrc() {
const plugins = [{
name: 'gen-dts',
setup(build) {
build.onStart(() => {
console.log(`[${_package.name}] detect changed...`);
});
build.onEnd(async result => {
if (result.errors.length > 0) {
console.error(`[${_package.name}] watch build failed:`, result);
return;
}
await buildDts();
});
},
}];
console.log(`[${_package.name}] start watching...`);
const context = await esbuild.context({ ...options, plugins });
await context.watch();
await new Promise((resolve, reject) => {
process.on('SIGHUP', resolve);
process.on('SIGINT', resolve);
process.on('SIGTERM', resolve);
process.on('uncaughtException', reject);
process.on('exit', resolve);
}).finally(async () => {
await context.dispose();
console.log(`[${_package.name}] finish watching.`);
});
}

View File

@@ -0,0 +1,126 @@
/*
* SPDX-FileCopyrightText: syuilo and misskey-project
* SPDX-License-Identifier: AGPL-3.0-only
*/
import tinycolor from 'tinycolor2';
import JSON5 from 'json5';
import lightTheme from '@@/themes/_light.json5';
import type { BundledTheme } from 'shiki/themes';
export type Theme = {
id: string;
name: string;
author: string;
desc?: string;
base?: 'dark' | 'light';
kind?: 'dark' | 'light'; // legacy
props: Record<string, string>;
codeHighlighter?: {
base: BundledTheme;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
overrides?: Record<string, any>;
} | {
base: '_none_';
// eslint-disable-next-line @typescript-eslint/no-explicit-any
overrides: Record<string, any>;
};
};
export type CompiledTheme = Record<string, string>;
export const themeProps = Object.keys(lightTheme.props).filter(key => !key.startsWith('X'));
export const getBuiltinThemes = () => Promise.all(
[
'l-light',
'l-coffee',
'l-apricot',
'l-rainy',
'l-botanical',
'l-vivid',
'l-cherry',
'l-sushi',
'l-u0',
'd-dark',
'd-persimmon',
'd-astro',
'd-future',
'd-botanical',
'd-green-lime',
'd-green-orange',
'd-cherry',
'd-ice',
'd-u0',
].map(name => import(`@@/themes/${name}.json5`).then(({ default: _default }): Theme => _default)),
);
export function compile(theme: Theme): CompiledTheme {
function getColor(val: string): tinycolor.Instance {
if (val[0] === '@') { // ref (prop)
return getColor(theme.props[val.substring(1)]);
} else if (val[0] === '$') { // ref (const)
return getColor(theme.props[val]);
} else if (val[0] === ':') { // func
const parts = val.split('<');
const funcTxt = parts.shift();
const argTxt = parts.shift();
if (funcTxt && argTxt) {
const func = funcTxt.substring(1);
const arg = parseFloat(argTxt);
const color = getColor(parts.join('<'));
switch (func) {
case 'darken': return color.darken(arg);
case 'lighten': return color.lighten(arg);
case 'alpha': return color.setAlpha(arg);
case 'hue': return color.spin(arg);
case 'saturate': return color.saturate(arg);
}
}
}
// other case
return tinycolor(val);
}
const props = {} as CompiledTheme;
for (const [k, v] of Object.entries(theme.props)) {
if (k.startsWith('$')) continue; // ignore const
props[k] = v.startsWith('"') ? v.replace(/^"\s*/, '') : genValue(getColor(v));
}
return props;
}
function genValue(c: tinycolor.Instance): string {
return c.toRgbString();
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
export function validateTheme(theme: Record<string, any>): boolean {
if (theme.id == null || typeof theme.id !== 'string') return false;
if (theme.name == null || typeof theme.name !== 'string') return false;
if (theme.base == null || !['light', 'dark'].includes(theme.base)) return false;
if (theme.props == null || typeof theme.props !== 'object') return false;
return true;
}
export function parseThemeCode(code: string): Theme {
let theme;
try {
theme = JSON5.parse(code);
} catch (_) {
throw new Error('Failed to parse theme json');
}
if (!validateTheme(theme)) {
throw new Error('This theme is invaild');
}
return theme;
}

View File

@@ -1,32 +1,18 @@
{
"name": "frontend-shared",
"type": "module",
"main": "./js-built/index.js",
"types": "./js-built/index.d.ts",
"exports": {
".": {
"import": "./js-built/index.js",
"types": "./js-built/index.d.ts"
},
"./*": {
"import": "./js-built/*",
"types": "./js-built/*"
}
},
"private": true,
"scripts": {
"build": "node ./build.js",
"watch": "nodemon -w package.json -e json --exec \"node ./build.js --watch\"",
"eslint": "eslint './**/*.{js,jsx,ts,tsx}'",
"typecheck": "tsgo --noEmit",
"lint": "pnpm typecheck && pnpm eslint"
},
"devDependencies": {
"@types/node": "24.12.2",
"@typescript-eslint/eslint-plugin": "8.58.2",
"@typescript-eslint/parser": "8.58.2",
"esbuild": "0.28.0",
"eslint-plugin-vue": "10.8.0",
"nodemon": "3.1.14",
"@types/tinycolor2": "1.4.6",
"@typescript-eslint/eslint-plugin": "8.59.0",
"@typescript-eslint/parser": "8.59.0",
"eslint-plugin-vue": "10.9.0",
"vue-eslint-parser": "10.4.0"
},
"files": [
@@ -34,7 +20,10 @@
],
"dependencies": {
"i18n": "workspace:*",
"json5": "2.2.3",
"misskey-js": "workspace:*",
"vue": "3.5.32"
"shiki": "4.0.2",
"tinycolor2": "1.6.0",
"vue": "3.5.33"
}
}

View File

@@ -20,13 +20,13 @@ let moduleInitialized = false;
let unobserve = () => {};
let misskeyOS = null;
function loadTheme(applyTheme: typeof import('../src/theme')['applyTheme']) {
function loadTheme(themeMaganer: typeof import('../src/theme')['themeManager']) {
unobserve();
const theme = themes[window.document.documentElement.dataset.misskeyTheme];
if (theme) {
applyTheme(themes[window.document.documentElement.dataset.misskeyTheme]);
themeMaganer.updateTheme(themes[window.document.documentElement.dataset.misskeyTheme]);
} else {
applyTheme(themes['l-light']);
themeMaganer.updateTheme(themes['l-light']);
}
const observer = new MutationObserver((entries) => {
for (const entry of entries) {
@@ -34,7 +34,7 @@ function loadTheme(applyTheme: typeof import('../src/theme')['applyTheme']) {
const target = entry.target as HTMLElement;
const theme = themes[target.dataset.misskeyTheme];
if (theme) {
applyTheme(themes[target.dataset.misskeyTheme]);
themeMaganer.updateTheme(themes[target.dataset.misskeyTheme]);
} else {
target.removeAttribute('style');
}

View File

@@ -18,10 +18,10 @@
"dependencies": {
"@analytics/google-analytics": "1.1.0",
"@discordapp/twemoji": "16.0.1",
"@github/webauthn-json": "2.1.1",
"@mcaptcha/core-glue": "0.1.0-alpha-5",
"@misskey-dev/browser-image-resizer": "2024.1.0",
"@sentry/vue": "10.48.0",
"@sentry/vue": "10.50.0",
"@simplewebauthn/browser": "13.3.0",
"@syuilo/aiscript": "1.2.1",
"@syuilo/aiscript-0-19-0": "npm:@syuilo/aiscript@^0.19.0",
"@twemoji/parser": "16.0.0",
@@ -36,7 +36,7 @@
"chartjs-chart-matrix": "3.0.0",
"chartjs-plugin-gradient": "0.6.1",
"chartjs-plugin-zoom": "2.2.0",
"chromatic": "15.3.1",
"chromatic": "16.6.0",
"compare-versions": "6.1.1",
"cropperjs": "2.1.1",
"date-fns": "4.1.0",
@@ -52,7 +52,7 @@
"is-file-animated": "1.0.2",
"json5": "2.2.3",
"matter-js": "0.20.0",
"mediabunny": "1.40.1",
"mediabunny": "1.41.0",
"mfm-js": "0.25.0",
"misskey-bubble-game": "workspace:*",
"misskey-js": "workspace:*",
@@ -64,11 +64,11 @@
"sanitize-html": "2.17.3",
"shiki": "4.0.2",
"textarea-caret": "3.1.0",
"three": "0.183.2",
"three": "0.184.0",
"throttle-debounce": "5.0.2",
"tinycolor2": "1.6.0",
"v-code-diff": "1.13.1",
"vue": "3.5.32",
"vue": "3.5.33",
"wanakana": "5.3.1"
},
"devDependencies": {
@@ -106,22 +106,22 @@
"@types/textarea-caret": "3.0.4",
"@types/throttle-debounce": "5.0.2",
"@types/tinycolor2": "1.4.6",
"@typescript-eslint/eslint-plugin": "8.58.2",
"@typescript-eslint/parser": "8.58.2",
"@vitest/coverage-v8": "4.1.4",
"@vue/compiler-core": "3.5.32",
"@typescript-eslint/eslint-plugin": "8.59.0",
"@typescript-eslint/parser": "8.59.0",
"@vitest/coverage-v8": "4.1.5",
"@vue/compiler-core": "3.5.33",
"acorn": "8.16.0",
"astring": "1.9.0",
"cross-env": "10.1.0",
"cypress": "15.13.1",
"cypress": "15.14.1",
"eslint-plugin-import": "2.32.0",
"eslint-plugin-vue": "10.8.0",
"eslint-plugin-vue": "10.9.0",
"estree-walker": "3.0.3",
"happy-dom": "20.9.0",
"intersection-observer": "0.12.2",
"micromatch": "4.0.8",
"minimatch": "10.2.5",
"msw": "2.13.3",
"msw": "2.13.6",
"msw-storybook-addon": "2.0.7",
"nodemon": "3.1.14",
"prettier": "3.8.3",
@@ -134,13 +134,13 @@
"storybook": "10.3.5",
"storybook-addon-misskey-theme": "github:misskey-dev/storybook-addon-misskey-theme",
"tsx": "4.21.0",
"vite": "8.0.8",
"vite": "8.0.10",
"vite-plugin-glsl": "1.6.0",
"vite-plugin-turbosnap": "1.0.3",
"vitest": "4.1.4",
"vitest": "4.1.5",
"vitest-fetch-mock": "0.4.5",
"vue-component-type-helpers": "3.2.6",
"vue-component-type-helpers": "3.2.7",
"vue-eslint-parser": "10.4.0",
"vue-tsc": "3.2.6"
"vue-tsc": "3.2.7"
}
}

View File

@@ -13,7 +13,7 @@ import type { App } from 'vue';
import widgets from '@/widgets/index.js';
import directives from '@/directives/index.js';
import components from '@/components/index.js';
import { applyTheme } from '@/theme.js';
import { themeManager } from '@/theme.js';
import { isDeviceDarkmode } from '@/utility/is-device-darkmode.js';
import { i18n } from '@/i18n.js';
import { refreshCurrentAccount, login } from '@/accounts.js';
@@ -161,7 +161,7 @@ export async function common(createVue: () => Promise<App<Element>>) {
// NOTE: この処理は必ずクライアント更新チェック処理より後に来ること(テーマ再構築のため)
// NOTE: この処理は必ずダークモード判定処理より後に来ること(初回のテーマ適用のため)
// NOTE: この処理は必ずサーバーテーマ適用処理より後に来ること(二重applyTheme発火を防ぐため)
// NOTE: この処理は必ずサーバーテーマ適用処理より後に来ること(二重発火を防ぐため)
// see: https://github.com/misskey-dev/misskey/issues/16562
watch(store.r.darkMode, (darkMode) => {
const theme = (() => {
@@ -172,7 +172,7 @@ export async function common(createVue: () => Promise<App<Element>>) {
}
})();
applyTheme(theme);
themeManager.updateTheme(theme);
}, { immediate: true });
window.document.documentElement.dataset.colorScheme = store.s.darkMode ? 'dark' : 'light';
@@ -180,13 +180,13 @@ export async function common(createVue: () => Promise<App<Element>>) {
if (!isSafeMode) {
watch(prefer.r.darkTheme, (theme) => {
if (store.s.darkMode) {
applyTheme(theme ?? defaultDarkTheme);
themeManager.updateTheme(theme ?? defaultDarkTheme);
}
});
watch(prefer.r.lightTheme, (theme) => {
if (!store.s.darkMode) {
applyTheme(theme ?? defaultLightTheme);
themeManager.updateTheme(theme ?? defaultLightTheme);
}
});
}

View File

@@ -81,7 +81,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<script lang="ts" setup>
import { computed, onMounted, onBeforeUnmount, ref } from 'vue';
import tinycolor from 'tinycolor2';
import { globalEvents } from '@/events.js';
import { themeManager } from '@/theme.js';
import { defaultIdlingRenderScheduler } from '@/utility/idle-render.js';
// https://stackoverflow.com/questions/1878907/how-can-i-find-the-difference-between-two-angles
@@ -192,13 +192,13 @@ function tick() {
tick();
function calcColors() {
const computedStyle = getComputedStyle(window.document.documentElement);
const dark = tinycolor(computedStyle.getPropertyValue('--MI_THEME-bg')).isDark();
const accent = tinycolor(computedStyle.getPropertyValue('--MI_THEME-accent')).toHexString();
const themeValue = themeManager.currentCompiledTheme!;
const dark = tinycolor(themeValue.bg).isDark();
const accent = tinycolor(themeValue.accent).toHexString();
majorGraduationColor.value = dark ? 'rgba(255, 255, 255, 0.3)' : 'rgba(0, 0, 0, 0.3)';
//minorGraduationColor = dark ? 'rgba(255, 255, 255, 0.2)' : 'rgba(0, 0, 0, 0.2)';
sHandColor.value = dark ? 'rgba(255, 255, 255, 0.5)' : 'rgba(0, 0, 0, 0.3)';
mHandColor.value = tinycolor(computedStyle.getPropertyValue('--MI_THEME-fg')).toHexString();
mHandColor.value = tinycolor(themeValue.fg).toHexString();
hHandColor.value = accent;
nowColor.value = accent;
}
@@ -207,13 +207,13 @@ calcColors();
onMounted(() => {
defaultIdlingRenderScheduler.add(tick);
globalEvents.on('themeChanged', calcColors);
themeManager.on('themeChanged', calcColors);
});
onBeforeUnmount(() => {
enabled = false;
defaultIdlingRenderScheduler.delete(tick);
globalEvents.off('themeChanged', calcColors);
themeManager.off('themeChanged', calcColors);
});
</script>

View File

@@ -34,7 +34,7 @@ import * as Misskey from 'misskey-js';
import Cropper from 'cropperjs';
import tinycolor from 'tinycolor2';
import MkModalWindow from '@/components/MkModalWindow.vue';
import * as os from '@/os.js';
import { themeManager } from '@/theme.js';
import { i18n } from '@/i18n.js';
const props = defineProps<{
@@ -105,10 +105,10 @@ onMounted(() => {
cropper = new Cropper(imgEl.value, {
});
const computedStyle = getComputedStyle(window.document.documentElement);
const themeValue = themeManager.currentCompiledTheme!;
const selection = cropper.getCropperSelection()!;
selection.themeColor = tinycolor(computedStyle.getPropertyValue('--MI_THEME-accent')).toHexString();
selection.themeColor = tinycolor(themeValue.accent).toHexString();
if (props.aspectRatio != null) selection.aspectRatio = props.aspectRatio;
selection.initialAspectRatio = props.aspectRatio ?? 1;
selection.outlined = true;

View File

@@ -34,7 +34,7 @@ SPDX-License-Identifier: AGPL-3.0-only
import { onBeforeUnmount, onMounted, ref, useTemplateRef, watch } from 'vue';
import { miLocalStorage } from '@/local-storage.js';
import { prefer } from '@/preferences.js';
import { globalEvents } from '@/events.js';
import { themeManager } from '@/theme.js';
import { getBgColor } from '@/utility/get-bg-color.js';
const miLocalStoragePrefix = 'ui:folder:' as const;
@@ -92,11 +92,11 @@ function updateBgColor() {
onMounted(() => {
updateBgColor();
globalEvents.on('themeChanging', updateBgColor);
themeManager.on('themeChanging', updateBgColor);
});
onBeforeUnmount(() => {
globalEvents.off('themeChanging', updateBgColor);
themeManager.off('themeChanging', updateBgColor);
});
</script>

View File

@@ -100,6 +100,7 @@ import { nextTick, onMounted, ref, useTemplateRef, watch } from 'vue';
import { prefer } from '@/preferences.js';
import { getBgColor } from '@/utility/get-bg-color.js';
import { pageFolderTeleportCount, popup } from '@/os.js';
import { themeManager } from '@/theme.js';
import MkFolderPage from '@/components/MkFolderPage.vue';
import { deviceKind } from '@/utility/device-kind.js';
@@ -192,9 +193,9 @@ async function toggle(ev: PointerEvent) {
}
onMounted(() => {
const computedStyle = getComputedStyle(window.document.documentElement);
const themeValue = themeManager.currentCompiledTheme!;
const parentBg = getBgColor(rootEl.value?.parentElement) ?? 'transparent';
const myBg = computedStyle.getPropertyValue('--MI_THEME-panel');
const myBg = themeValue.panel;
bgSame.value = parentBg === myBg;
});

View File

@@ -73,6 +73,7 @@ import MkRetentionHeatmap from '@/components/MkRetentionHeatmap.vue';
import MkRetentionLineChart from '@/components/MkRetentionLineChart.vue';
import { initChart } from '@/utility/init-chart.js';
import { useMkSelect } from '@/composables/use-mkselect.js';
import { themeManager } from '@/theme.js';
initChart();
@@ -186,7 +187,7 @@ function createDoughnut(chartEl: HTMLCanvasElement, tooltip: ReturnType<typeof u
labels: data.map(x => x.name),
datasets: [{
backgroundColor: data.map(x => x.color),
borderColor: getComputedStyle(window.document.documentElement).getPropertyValue('--MI_THEME-panel'),
borderColor: themeManager.currentCompiledTheme!.panel,
borderWidth: 2,
hoverOffset: 0,
data: data.map(x => x.value),

View File

@@ -33,6 +33,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<script lang="ts" setup>
import { watch, ref } from 'vue';
import { genId } from '@/utility/id.js';
import { themeManager } from '@/theme.js';
import tinycolor from 'tinycolor2';
import { useInterval } from '@@/js/use-interval.js';
@@ -47,8 +48,7 @@ const polylinePoints = ref('');
const polygonPoints = ref('');
const headX = ref<number | null>(null);
const headY = ref<number | null>(null);
const clock = ref<number | null>(null);
const accent = tinycolor(getComputedStyle(window.document.documentElement).getPropertyValue('--MI_THEME-accent'));
const accent = tinycolor(themeManager.currentCompiledTheme!.accent);
const color = accent.toRgbString();
function draw(): void {

View File

@@ -135,6 +135,14 @@ SPDX-License-Identifier: AGPL-3.0-only
<MkA :to="notePage(appearNote)">
<MkTime :time="appearNote.createdAt" mode="detail" colored/>
</MkA>
<span style="margin-left: 0.5em;">
<span style="border: 1px solid var(--MI_THEME-divider); margin-right: 0.5em;"></span>
<i v-if="appearNote.visibility === 'public'" class="ti ti-world"></i>
<i v-else-if="appearNote.visibility === 'home'" class="ti ti-home"></i>
<i v-else-if="appearNote.visibility === 'followers'" class="ti ti-lock"></i>
<i v-else-if="appearNote.visibility === 'specified'" ref="specified" class="ti ti-mail"></i>
<span style="margin-left: 0.3em;">{{ i18n.ts._visibility[appearNote.visibility] }}</span>
</span>
</div>
<MkReactionsViewer
v-if="appearNote.reactionAcceptance !== 'likeOnly'"

View File

@@ -13,6 +13,7 @@ import { Chart } from 'chart.js';
import type { ScatterDataPoint } from 'chart.js';
import tinycolor from 'tinycolor2';
import { store } from '@/store.js';
import { themeManager } from '@/theme.js';
import { useChartTooltip } from '@/composables/use-chart-tooltip.js';
import { chartVLine } from '@/utility/chart-vline.js';
import { alpha } from '@/utility/color.js';
@@ -51,7 +52,7 @@ onMounted(async () => {
const vLineColor = store.s.darkMode ? 'rgba(255, 255, 255, 0.2)' : 'rgba(0, 0, 0, 0.2)';
const accent = tinycolor(getComputedStyle(window.document.documentElement).getPropertyValue('--MI_THEME-accent'));
const accent = tinycolor(themeManager.currentCompiledTheme!.accent);
const color = accent.toHex();
if (chartEl.value == null) return;

View File

@@ -22,21 +22,21 @@ SPDX-License-Identifier: AGPL-3.0-only
<script setup lang="ts">
import { ref, onMounted } from 'vue';
import { get as webAuthnRequest } from '@github/webauthn-json/browser-ponyfill';
import { startAuthentication } from '@simplewebauthn/browser';
import { i18n } from '@/i18n.js';
import MkButton from '@/components/MkButton.vue';
import type { AuthenticationPublicKeyCredential } from '@github/webauthn-json/browser-ponyfill';
import type { PublicKeyCredentialRequestOptionsJSON, AuthenticationResponseJSON } from '@simplewebauthn/browser';
const props = defineProps<{
credentialRequest: CredentialRequestOptions;
credentialRequest: PublicKeyCredentialRequestOptionsJSON;
isPerformingPasswordlessLogin?: boolean;
}>();
const emit = defineEmits<{
(ev: 'done', credential: AuthenticationPublicKeyCredential): void;
(ev: 'done', credential: AuthenticationResponseJSON): void;
(ev: 'useTotp'): void;
}>();
@@ -44,7 +44,7 @@ const queryingKey = ref(true);
async function queryKey() {
queryingKey.value = true;
await webAuthnRequest(props.credentialRequest)
await startAuthentication({ optionsJSON: props.credentialRequest })
.catch(() => {
return Promise.reject(null);
})

View File

@@ -67,8 +67,8 @@ SPDX-License-Identifier: AGPL-3.0-only
<script setup lang="ts">
import { nextTick, onBeforeUnmount, ref, shallowRef, useTemplateRef } from 'vue';
import * as Misskey from 'misskey-js';
import { supported as webAuthnSupported, parseRequestOptionsFromJSON } from '@github/webauthn-json/browser-ponyfill';
import type { AuthenticationPublicKeyCredential } from '@github/webauthn-json/browser-ponyfill';
import { browserSupportsWebAuthn } from '@simplewebauthn/browser';
import type { PublicKeyCredentialRequestOptionsJSON, AuthenticationResponseJSON } from '@simplewebauthn/browser';
import type { OpenOnRemoteOptions } from '@/utility/please-login.js';
import type { PwResponse } from '@/components/MkSignin.password.vue';
import { misskeyApi } from '@/utility/misskey-api.js';
@@ -108,21 +108,18 @@ const userInfo = ref<null | Misskey.entities.UserDetailed>(null);
const password = ref('');
//#region Passkey Passwordless
const credentialRequest = shallowRef<CredentialRequestOptions | null>(null);
const credentialRequest = shallowRef<PublicKeyCredentialRequestOptionsJSON | null>(null);
const passkeyContext = ref('');
const doingPasskeyFromInputPage = ref(false);
function onPasskeyLogin(): void {
if (webAuthnSupported()) {
if (browserSupportsWebAuthn()) {
doingPasskeyFromInputPage.value = true;
waiting.value = true;
misskeyApi('signin-with-passkey', {})
.then((res) => {
passkeyContext.value = res.context ?? '';
credentialRequest.value = parseRequestOptionsFromJSON({
// @ts-expect-error TODO: misskey-js由来の型@simplewebauthn/typesとフロントエンド由来の型@github/webauthn-jsonが合わない
publicKey: res.option,
});
credentialRequest.value = res.option;
page.value = 'passkey';
waiting.value = false;
@@ -131,12 +128,12 @@ function onPasskeyLogin(): void {
}
}
function onPasskeyDone(credential: AuthenticationPublicKeyCredential): void {
function onPasskeyDone(credential: AuthenticationResponseJSON): void {
waiting.value = true;
if (doingPasskeyFromInputPage.value) {
misskeyApi<Misskey.entities.SigninWithPasskeyResponse>('signin-with-passkey', {
credential: credential.toJSON(),
misskeyApi('signin-with-passkey', {
credential: credential,
context: passkeyContext.value,
}).then((res) => {
if (res.signinResponse == null) {
@@ -150,8 +147,7 @@ function onPasskeyDone(credential: AuthenticationPublicKeyCredential): void {
tryLogin({
username: userInfo.value.username,
password: password.value,
// @ts-expect-error TODO: misskey-js由来の型@simplewebauthn/typesとフロントエンド由来の型@github/webauthn-jsonが合わない
credential: credential.toJSON(),
credential: credential,
});
}
}
@@ -253,11 +249,8 @@ async function tryLogin(req: Partial<Misskey.entities.SigninFlowRequest>): Promi
break;
}
case 'passkey': {
if (webAuthnSupported()) {
credentialRequest.value = parseRequestOptionsFromJSON({
// @ts-expect-error TODO: misskey-js由来の型@simplewebauthn/typesとフロントエンド由来の型@github/webauthn-jsonが合わない
publicKey: res.authRequest,
});
if (browserSupportsWebAuthn()) {
credentialRequest.value = res.authRequest;
page.value = 'passkey';
} else {
page.value = 'totp';

View File

@@ -16,11 +16,11 @@ SPDX-License-Identifier: AGPL-3.0-only
<script lang="ts" setup>
import { onMounted, watch, onBeforeUnmount, ref, useTemplateRef } from 'vue';
import { themeManager } from '@/theme.js';
import tinycolor from 'tinycolor2';
const loaded = !!window.TagCanvas;
const SAFE_FOR_HTML_ID = 'abcdefghijklmnopqrstuvwxyz';
const computedStyle = getComputedStyle(window.document.documentElement);
const idForCanvas = Array.from({ length: 16 }, () => SAFE_FOR_HTML_ID[Math.floor(Math.random() * SAFE_FOR_HTML_ID.length)]).join('');
const idForTags = Array.from({ length: 16 }, () => SAFE_FOR_HTML_ID[Math.floor(Math.random() * SAFE_FOR_HTML_ID.length)]).join('');
const available = ref(false);
@@ -33,7 +33,7 @@ watch(available, () => {
try {
window.TagCanvas.Start(idForCanvas, idForTags, {
textColour: '#ffffff',
outlineColour: tinycolor(computedStyle.getPropertyValue('--MI_THEME-accent')).toHexString(),
outlineColour: tinycolor(themeManager.currentCompiledTheme!.accent).toHexString(),
outlineRadius: 10,
initial: [-0.030, -0.010],
frontSelect: true,

View File

@@ -43,8 +43,8 @@ SPDX-License-Identifier: AGPL-3.0-only
import { ref, watch } from 'vue';
import lightTheme from '@@/themes/_light.json5';
import darkTheme from '@@/themes/_dark.json5';
import type { Theme } from '@/theme.js';
import { compile } from '@/theme.js';
import type { Theme } from '@@/js/theme.js';
import { compile } from '@@/js/theme.js';
import { deepClone } from '@/utility/clone.js';
const props = defineProps<{

View File

@@ -15,9 +15,9 @@ SPDX-License-Identifier: AGPL-3.0-only
<script lang="ts" setup>
import { onMounted, useTemplateRef, ref, nextTick } from 'vue';
import { Chart } from 'chart.js';
import gradient from 'chartjs-plugin-gradient';
import tinycolor from 'tinycolor2';
import { misskeyApi } from '@/utility/misskey-api.js';
import { themeManager } from '@/theme.js';
import { store } from '@/store.js';
import { useChartTooltip } from '@/composables/use-chart-tooltip.js';
import { chartVLine } from '@/utility/chart-vline.js';
@@ -61,8 +61,7 @@ async function renderChart() {
const vLineColor = store.s.darkMode ? 'rgba(255, 255, 255, 0.2)' : 'rgba(0, 0, 0, 0.2)';
const computedStyle = getComputedStyle(window.document.documentElement);
const accent = tinycolor(computedStyle.getPropertyValue('--MI_THEME-accent')).toHexString();
const accent = tinycolor(themeManager.currentCompiledTheme!.accent).toHexString();
const colorRead = accent;
const colorWrite = '#2ecc71';

View File

@@ -324,8 +324,9 @@ function onTopHandlePointerdown(evt: PointerEvent) {
if (main == null) return;
const base = getPositionY(evt);
const height = parseInt(getComputedStyle(main, '').height, 10);
const top = parseInt(getComputedStyle(main, '').top, 10);
const computedStyle = getComputedStyle(main, '');
const height = parseInt(computedStyle.height, 10);
const top = parseInt(computedStyle.top, 10);
// 動かした時
dragListen(me => {
@@ -353,8 +354,9 @@ function onRightHandlePointerdown(evt: PointerEvent) {
if (main == null) return;
const base = getPositionX(evt);
const width = parseInt(getComputedStyle(main, '').width, 10);
const left = parseInt(getComputedStyle(main, '').left, 10);
const computedStyle = getComputedStyle(main, '');
const width = parseInt(computedStyle.width, 10);
const left = parseInt(computedStyle.left, 10);
const browserWidth = window.innerWidth;
// 動かした時
@@ -380,8 +382,9 @@ function onBottomHandlePointerdown(evt: PointerEvent) {
if (main == null) return;
const base = getPositionY(evt);
const height = parseInt(getComputedStyle(main, '').height, 10);
const top = parseInt(getComputedStyle(main, '').top, 10);
const computedStyle = getComputedStyle(main, '');
const height = parseInt(computedStyle.height, 10);
const top = parseInt(computedStyle.top, 10);
const browserHeight = window.innerHeight;
// 動かした時
@@ -407,8 +410,9 @@ function onLeftHandlePointerdown(evt: PointerEvent) {
if (main == null) return;
const base = getPositionX(evt);
const width = parseInt(getComputedStyle(main, '').width, 10);
const left = parseInt(getComputedStyle(main, '').left, 10);
const computedStyle = getComputedStyle(main, '');
const width = parseInt(computedStyle.width, 10);
const left = parseInt(computedStyle.left, 10);
// 動かした時
dragListen(me => {

View File

@@ -5,7 +5,7 @@
import type { Directive } from 'vue';
import { getBgColor } from '@/utility/get-bg-color.js';
import { globalEvents } from '@/events.js';
import { themeManager } from '@/theme.js';
const handlerMap = new WeakMap<HTMLElement, () => void>();
@@ -27,10 +27,10 @@ export const adaptiveBorderDirective = {
calc();
globalEvents.on('themeChanged', calc);
themeManager.on('themeChanged', calc);
},
unmounted(src) {
globalEvents.off('themeChanged', handlerMap.get(src));
themeManager.off('themeChanged', handlerMap.get(src));
},
} as Directive<HTMLElement>;

View File

@@ -4,13 +4,14 @@
*/
import type { Directive } from 'vue';
import { themeManager } from '@/theme.js';
import { getBgColor } from '@/utility/get-bg-color.js';
export const panelDirective = {
mounted(src) {
const parentBg = getBgColor(src.parentElement) ?? 'transparent';
const myBg = getComputedStyle(window.document.documentElement).getPropertyValue('--MI_THEME-panel');
const myBg = themeManager.currentCompiledTheme!.panel;
if (parentBg === myBg) {
src.style.backgroundColor = 'var(--MI_THEME-bg)';

View File

@@ -8,8 +8,6 @@ import * as Misskey from 'misskey-js';
import { onBeforeUnmount } from 'vue';
type Events = {
themeChanging: () => void;
themeChanged: () => void;
clientNotification: (notification: Misskey.entities.Notification) => void;
notePosted: (note: Misskey.entities.Note) => void;
noteDeleted: (noteId: Misskey.entities.Note['id']) => void;

View File

@@ -10,6 +10,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<script lang="ts" setup>
import { onMounted, useTemplateRef } from 'vue';
import { Chart } from 'chart.js';
import { themeManager } from '@/theme.js';
import { useChartTooltip } from '@/composables/use-chart-tooltip.js';
import { initChart } from '@/utility/init-chart.js';
@@ -43,7 +44,7 @@ onMounted(() => {
labels: props.data.map(x => x.name),
datasets: [{
backgroundColor: props.data.map(x => x.color ?? '#000'),
borderColor: getComputedStyle(window.document.documentElement).getPropertyValue('--MI_THEME-panel'),
borderColor: themeManager.currentCompiledTheme!.panel,
borderWidth: 2,
hoverOffset: 0,
data: props.data.map(x => x.value),

View File

@@ -161,6 +161,16 @@ SPDX-License-Identifier: AGPL-3.0-only
</template>
</XFolder>
<XFolder v-if="matchQuery([i18n.ts._role._options.canCreateChannel, 'canCreateChannel'])" v-model:policyMeta="policyMetaModel.canCreateChannel" :isBaseRole="isBaseRole" :readonly="readonly">
<template #label>{{ i18n.ts._role._options.canCreateChannel }}</template>
<template #suffix>{{ valuesModel.canCreateChannel ? i18n.ts.yes : i18n.ts.no }}</template>
<template #default="{ disabled }">
<MkSwitch v-model="valuesModel.canCreateChannel" :disabled="disabled">
<template #label>{{ i18n.ts.enable }}</template>
</MkSwitch>
</template>
</XFolder>
<XFolder v-if="matchQuery([i18n.ts._role._options.driveCapacity, 'driveCapacityMb'])" v-model:policyMeta="policyMetaModel.driveCapacityMb" :isBaseRole="isBaseRole" :readonly="readonly">
<template #label>{{ i18n.ts._role._options.driveCapacity }}</template>
<template #valueText>{{ valuesModel.driveCapacityMb }}MB</template>

View File

@@ -139,6 +139,7 @@ async function assign() {
await os.apiWithDialog('admin/roles/assign', { roleId: role.id, userId: user.id, expiresAt });
//role.users.push(user);
usersPaginator.reload();
}
async function unassign(userId: Misskey.entities.User['id'], ev: PointerEvent) {
@@ -149,6 +150,7 @@ async function unassign(userId: Misskey.entities.User['id'], ev: PointerEvent) {
action: async () => {
await os.apiWithDialog('admin/roles/unassign', { roleId: role.id, userId: userId });
//role.users = role.users.filter(u => u.id !== userId);
usersPaginator.reload();
},
}], ev.currentTarget ?? ev.target);
}

View File

@@ -50,7 +50,7 @@ SPDX-License-Identifier: AGPL-3.0-only
</MkPagination>
</div>
<div v-else-if="tab === 'owned'" class="_gaps">
<MkButton type="routerLink" primary rounded to="/channels/new"><i class="ti ti-plus"></i> {{ i18n.ts.createNew }}</MkButton>
<MkButton v-if="$i?.policies.canCreateChannel" type="routerLink" primary rounded to="/channels/new"><i class="ti ti-plus"></i> {{ i18n.ts.createNew }}</MkButton>
<MkPagination v-slot="{items}" :paginator="ownedPaginator">
<div :class="$style.root">
<MkChannelPreview v-for="channel in items" :key="channel.id" :channel="channel"/>
@@ -74,6 +74,7 @@ import { definePage } from '@/page.js';
import { i18n } from '@/i18n.js';
import { useRouter } from '@/router.js';
import { Paginator } from '@/utility/paginator.js';
import { $i } from '@/i.js';
const router = useRouter();

View File

@@ -53,7 +53,8 @@ import FormSection from '@/components/form/section.vue';
import * as os from '@/os.js';
import { misskeyApi } from '@/utility/misskey-api.js';
import { parsePluginMeta, installPlugin } from '@/plugin.js';
import { parseThemeCode, installTheme } from '@/theme.js';
import { installTheme } from '@/theme.js';
import { parseThemeCode } from '@@/js/theme.js';
import { unisonReload } from '@/utility/unison-reload.js';
import { i18n } from '@/i18n.js';
import { definePage } from '@/page.js';

View File

@@ -48,11 +48,11 @@ SPDX-License-Identifier: AGPL-3.0-only
{{ i18n.ts._2fa.securityKeyInfo }}
</MkInfo>
<MkInfo v-if="!webAuthnSupported()" warn>
<MkInfo v-if="!browserSupportsWebAuthn()" warn>
{{ i18n.ts._2fa.securityKeyNotSupported }}
</MkInfo>
<MkInfo v-else-if="webAuthnSupported() && !$i.twoFactorEnabled" warn>
<MkInfo v-else-if="browserSupportsWebAuthn() && !$i.twoFactorEnabled" warn>
{{ i18n.ts._2fa.registerTOTPBeforeKey }}
</MkInfo>
@@ -83,8 +83,8 @@ SPDX-License-Identifier: AGPL-3.0-only
</template>
<script lang="ts" setup>
import { defineAsyncComponent, computed } from 'vue';
import { supported as webAuthnSupported, create as webAuthnCreate, parseCreationOptionsFromJSON } from '@github/webauthn-json/browser-ponyfill';
import { computed } from 'vue';
import { browserSupportsWebAuthn, startRegistration } from '@simplewebauthn/browser';
import * as Misskey from 'misskey-js';
import MkButton from '@/components/MkButton.vue';
import MkInfo from '@/components/MkInfo.vue';
@@ -196,12 +196,9 @@ async function addSecurityKey() {
const auth = await os.authenticateDialog();
if (auth.canceled) return;
const registrationOptions = parseCreationOptionsFromJSON({
// @ts-expect-error misskey-js側に型がない
publicKey: await os.apiWithDialog('i/2fa/register-key', {
password: auth.result.password,
token: auth.result.token,
}),
const registrationOptions = await os.apiWithDialog('i/2fa/register-key', {
password: auth.result.password,
token: auth.result.token,
});
const name = await os.inputText({
@@ -214,7 +211,7 @@ async function addSecurityKey() {
if (name.canceled) return;
const credential = await os.promiseDialog(
webAuthnCreate(registrationOptions),
startRegistration({ optionsJSON: registrationOptions }),
null,
() => {}, // ユーザーのキャンセルはrejectなのでエラーダイアログを出さない
i18n.ts._2fa.tapSecurityKey,
@@ -228,8 +225,7 @@ async function addSecurityKey() {
password: auth.result.password,
token: auth.result.token,
name: name.result,
// @ts-expect-error misskey-js側に型がない
credential: credential.toJSON(),
credential: credential,
});
}

View File

@@ -20,7 +20,8 @@ SPDX-License-Identifier: AGPL-3.0-only
import { ref, computed } from 'vue';
import MkCodeEditor from '@/components/MkCodeEditor.vue';
import MkButton from '@/components/MkButton.vue';
import { parseThemeCode, previewTheme, installTheme } from '@/theme.js';
import { themeManager, installTheme, handleThemeInstallError } from '@/theme.js';
import { parseThemeCode } from '@@/js/theme.js';
import * as os from '@/os.js';
import { i18n } from '@/i18n.js';
import { definePage } from '@/page.js';
@@ -29,6 +30,19 @@ import { useRouter } from '@/router.js';
const router = useRouter();
const installThemeCode = ref<string | null>(null);
function previewTheme(code: string): void {
try {
const theme = parseThemeCode(code);
themeManager.previewTheme(theme);
} catch (err) {
os.alert({
type: 'error',
text: i18n.ts._theme.invalid,
});
console.error(err);
}
}
async function install(code: string): Promise<void> {
try {
const theme = parseThemeCode(code);
@@ -40,22 +54,7 @@ async function install(code: string): Promise<void> {
installThemeCode.value = null;
router.push('/settings/theme');
} catch (err: any) {
switch (err.message.toLowerCase()) {
case 'this theme is already installed':
os.alert({
type: 'info',
text: i18n.ts._theme.alreadyInstalled,
});
break;
default:
os.alert({
type: 'error',
text: i18n.ts._theme.invalid,
});
break;
}
console.error(err);
handleThemeInstallError(err);
}
}

View File

@@ -27,21 +27,27 @@ SPDX-License-Identifier: AGPL-3.0-only
<script lang="ts" setup>
import { computed, ref } from 'vue';
import JSON5 from 'json5';
import type { Theme } from '@/theme.js';
import type { Theme } from '@@/js/theme.js';
import MkTextarea from '@/components/MkTextarea.vue';
import MkSelect from '@/components/MkSelect.vue';
import MkInput from '@/components/MkInput.vue';
import MkButton from '@/components/MkButton.vue';
import { getBuiltinThemesRef, getThemesRef, removeTheme } from '@/theme.js';
import { removeTheme } from '@/theme.js';
import { getBuiltinThemes } from '@@/js/theme.js';
import { copyToClipboard } from '@/utility/copy-to-clipboard.js';
import * as os from '@/os.js';
import { i18n } from '@/i18n.js';
import { definePage } from '@/page.js';
import { useMkSelect } from '@/composables/use-mkselect.js';
import type { MkSelectItem } from '@/components/MkSelect.vue';
import { prefer } from '@/preferences';
const installedThemes = prefer.r.themes;
const builtinThemes = ref<Theme[]>([]);
getBuiltinThemes().then(themes => {
builtinThemes.value = themes;
});
const installedThemes = getThemesRef();
const builtinThemes = getBuiltinThemesRef();
const {
model: selectedThemeId,
def: selectedThemeIdDef,

View File

@@ -210,7 +210,7 @@ import JSON5 from 'json5';
import defaultLightTheme from '@@/themes/l-light.json5';
import defaultDarkTheme from '@@/themes/d-green-lime.json5';
import { isSafeMode } from '@@/js/config.js';
import type { Theme } from '@/theme.js';
import type { Theme } from '@@/js/theme.js';
import * as os from '@/os.js';
import MkSwitch from '@/components/MkSwitch.vue';
import FormSection from '@/components/form/section.vue';
@@ -218,7 +218,8 @@ import FormLink from '@/components/form/link.vue';
import MkFolder from '@/components/MkFolder.vue';
import MkThemePreview from '@/components/MkThemePreview.vue';
import MkInfo from '@/components/MkInfo.vue';
import { getBuiltinThemesRef, getThemesRef, installTheme, parseThemeCode, removeTheme } from '@/theme.js';
import { handleThemeInstallError, installTheme, removeTheme } from '@/theme.js';
import { getBuiltinThemes } from '@@/js/theme.js';
import { isDeviceDarkmode } from '@/utility/is-device-darkmode.js';
import { store } from '@/store.js';
import { i18n } from '@/i18n.js';
@@ -229,8 +230,11 @@ import { prefer } from '@/preferences.js';
import { copyToClipboard } from '@/utility/copy-to-clipboard.js';
import { checkDragDataType, getDragData, getPlainDragData, setDragData, setPlainDragData } from '@/drag-and-drop.js';
const installedThemes = getThemesRef();
const builtinThemes = getBuiltinThemesRef();
const installedThemes = prefer.r.themes;
const builtinThemes = ref<Theme[]>([]);
getBuiltinThemes().then(themes => {
builtinThemes.value = themes;
});
const instanceDarkTheme = computed<Theme | null>(() => instance.defaultDarkTheme ? JSON5.parse(instance.defaultDarkTheme) : null);
const installedDarkThemes = computed(() => installedThemes.value.filter(t => t.base === 'dark' || t.kind === 'dark'));
@@ -353,7 +357,7 @@ async function onDrop(ev: DragEvent) {
try {
await installTheme(code);
} catch (err) {
// nop
handleThemeInstallError(err);
}
}
}

View File

@@ -79,14 +79,15 @@ import JSON5 from 'json5';
import lightTheme from '@@/themes/_light.json5';
import darkTheme from '@@/themes/_dark.json5';
import { host } from '@@/js/config.js';
import type { Theme } from '@/theme.js';
import type { Theme } from '@@/js/theme.js';
import { genId } from '@/utility/id.js';
import MkButton from '@/components/MkButton.vue';
import MkCodeEditor from '@/components/MkCodeEditor.vue';
import MkTextarea from '@/components/MkTextarea.vue';
import MkFolder from '@/components/MkFolder.vue';
import { ensureSignin } from '@/i.js';
import { addTheme, applyTheme } from '@/theme.js';
import { addTheme, themeManager } from '@/theme.js';
import { deepClone } from '@/utility/clone.js';
import * as os from '@/os.js';
import { store } from '@/store.js';
import { i18n } from '@/i18n.js';
@@ -130,7 +131,7 @@ const theme = ref<Theme>({
name: 'untitled',
author: `@${$i.username}@${toUnicode(host)}`,
base: 'light',
props: lightTheme.props,
props: deepClone(lightTheme.props),
});
const description = ref<string | null>(null);
const themeCode = ref<string>('');
@@ -170,7 +171,7 @@ function setFgColor(color: typeof fgColors[number]) {
function apply() {
themeCode.value = JSON5.stringify(theme.value, null, '\t');
applyTheme(theme.value, false);
themeManager.previewTheme(theme.value);
changed.value = true;
}
@@ -201,7 +202,7 @@ async function saveAs() {
theme.value.name = name;
if (description.value) theme.value.desc = description.value;
await addTheme(theme.value);
applyTheme(theme.value);
themeManager.updateTheme(theme.value);
if (store.s.darkMode) {
prefer.commit('darkTheme', theme.value);
} else {

View File

@@ -8,7 +8,7 @@ import { hemisphere } from '@@/js/intl-const.js';
import { DEFAULT_EMOJIS } from '@@/js/const.js';
import { prefersReducedMotion } from '@@/js/config.js';
import { definePreferences } from './manager.js';
import type { Theme } from '@/theme.js';
import type { Theme } from '@@/js/theme.js';
import type { SoundType } from '@/utility/sound.js';
import type { Plugin } from '@/plugin.js';
import type { DeviceKind } from '@/utility/device-kind.js';

View File

@@ -6,73 +6,183 @@
// TODO: (可能な部分を)sharedに抽出して frontend-embed と共通化
import { ref, nextTick } from 'vue';
import tinycolor from 'tinycolor2';
import { EventEmitter } from 'eventemitter3';
import lightTheme from '@@/themes/_light.json5';
import darkTheme from '@@/themes/_dark.json5';
import JSON5 from 'json5';
import { version } from '@@/js/config.js';
import type { Ref } from 'vue';
import type { BundledTheme } from 'shiki/themes';
import { getBuiltinThemes, parseThemeCode, themeProps, compile } from '@@/js/theme.js';
import type { Theme, CompiledTheme } from '@@/js/theme.js';
import { deepClone } from '@/utility/clone.js';
import { globalEvents } from '@/events.js';
import { miLocalStorage } from '@/local-storage.js';
import { $i } from '@/i.js';
import { i18n } from '@/i18n.js';
import * as os from '@/os.js';
import { prefer } from '@/preferences.js';
import { deepEqual } from '@/utility/deep-equal.js';
export type Theme = {
id: string;
name: string;
author: string;
desc?: string;
base?: 'dark' | 'light';
kind?: 'dark' | 'light'; // legacy
props: Record<string, string>;
codeHighlighter?: {
base: BundledTheme;
overrides?: Record<string, any>;
} | {
base: '_none_';
overrides: Record<string, any>;
};
type ThemeManagerEvents = {
'themeChanging': () => void;
'themeChanged': () => void;
'previewStateChanged': (isPreview: boolean) => void;
'requestUpdateThemeCache': (theme: Theme, compiled: CompiledTheme) => void;
};
export const themeProps = Object.keys(lightTheme.props).filter(key => !key.startsWith('X'));
class ThemeManager extends EventEmitter<ThemeManagerEvents> {
/** 現在常用しているテーマ */
private _theme: Theme | null = null;
get theme() { return this._theme; }
private _compiledTheme: CompiledTheme | null = null;
get compiledTheme() { return this._compiledTheme; }
export const getBuiltinThemes = () => Promise.all(
[
'l-light',
'l-coffee',
'l-apricot',
'l-rainy',
'l-botanical',
'l-vivid',
'l-cherry',
'l-sushi',
'l-u0',
/** 現在適用中のテーマ */
private _currentTheme: Theme | null = null;
get currentTheme() { return this._currentTheme; }
get currentThemeId() { return this._currentTheme?.id; }
private _currentCompiledTheme: CompiledTheme | null = null;
get currentCompiledTheme() { return this._currentCompiledTheme; }
'd-dark',
'd-persimmon',
'd-astro',
'd-future',
'd-botanical',
'd-green-lime',
'd-green-orange',
'd-cherry',
'd-ice',
'd-u0',
].map(name => import(`@@/themes/${name}.json5`).then(({ default: _default }): Theme => _default)),
);
/** プレビュー中かどうか */
private _isPreviewMode = false;
get isPreviewMode() { return this._isPreviewMode; }
set isPreviewMode(value: boolean) {
if (this._isPreviewMode !== value) {
this._isPreviewMode = value;
this.emit('previewStateChanged', value);
}
}
export function getBuiltinThemesRef() {
const builtinThemes = ref<Theme[]>([]);
getBuiltinThemes().then(themes => builtinThemes.value = themes);
return builtinThemes;
constructor() {
super();
}
/** テーマを更新し、同時に適用します。 */
public updateTheme(newTheme: Theme) {
if (newTheme.id === this.theme?.id && version === miLocalStorage.getItem('themeCachedVersion')) return; // 変更なし
this.isPreviewMode = false;
// テーマを更新
this._theme = deepClone(newTheme);
const compiled = this.compile(newTheme);
this._compiledTheme = compiled;
// 適用中のテーマも更新
this._currentTheme = deepClone(this.theme);
this._currentCompiledTheme = deepClone(compiled);
this.applyTheme();
}
/** プレビュー用のテーマを適用します。 */
public previewTheme(theme: Theme) {
this.isPreviewMode = true;
// 適用中のテーマを更新
this._currentTheme = deepClone(theme);
this._currentCompiledTheme = this.compile(theme);
this.applyTheme();
}
/** プレビュー状態を解除し、適用中のテーマを常用しているテーマに戻します。 */
public clearPreview() {
this.isPreviewMode = false;
// 適用中のテーマを常用しているテーマに戻す
this._currentTheme = deepClone(this.theme);
this._currentCompiledTheme = deepClone(this.compiledTheme);
this.applyTheme();
}
/** 通常のテーマのコンパイルに加え、ベースとなるテーマの値を解決し代入します。 */
private compile(theme: Theme) {
const _theme = deepClone(theme);
if (_theme.base != null) {
const base = [lightTheme, darkTheme].find(x => x.id === _theme.base);
if (base) _theme.props = Object.assign({}, base.props, _theme.props);
}
return compile(_theme);
}
/** currentThemeを適用します。 */
private applyTheme() {
if (this.currentTheme == null || this.currentCompiledTheme == null) return;
// visibilityStateがhiddenな状態でstartViewTransitionするとブラウザによってはエラーになる
// 通常hiddenな時に呼ばれることはないが、iOSのPWAだとアプリ切り替え時に(何故か)hiddenな状態で(何故か)一瞬デバイスのダークモード判定が変わりapplyThemeが呼ばれる場合がある
if (window.document.startViewTransition != null && window.document.visibilityState === 'visible') {
window.document.documentElement.classList.add('_themeChanging_');
try {
window.document.startViewTransition(async () => {
this.updateAttributes();
await nextTick();
}).finished.then(() => {
window.document.documentElement.classList.remove('_themeChanging_');
this.emit('themeChanged');
});
} catch (err) {
// 様々な理由により startViewTransition は失敗することがある
// ref. https://github.com/misskey-dev/misskey/issues/16562
// FIXME: viewTransitonエラーはtry~catch貫通してそうな気配がする
console.error(err);
window.document.documentElement.classList.remove('_themeChanging_');
this.updateAttributes();
this.emit('themeChanged');
}
} else {
this.updateAttributes();
this.emit('themeChanged');
}
if (!this.isPreviewMode) {
this.emit('requestUpdateThemeCache', this.currentTheme, this.currentCompiledTheme);
}
}
private updateAttributes() {
if (!this.currentTheme || !this.currentCompiledTheme) return;
const colorScheme = this.currentTheme.base === 'dark' ? 'dark' : 'light';
window.document.documentElement.dataset.colorScheme = colorScheme;
for (const tag of window.document.head.children) {
if (tag.tagName === 'META' && tag.getAttribute('name') === 'theme-color') {
tag.setAttribute('content', this.currentCompiledTheme['htmlThemeColor']);
break;
}
}
for (const key of themeProps) {
const value = this.currentCompiledTheme[key];
if (value) {
window.document.documentElement.style.setProperty(`--MI_THEME-${key}`, value.toString());
} else {
window.document.documentElement.style.removeProperty(`--MI_THEME-${key}`);
}
}
window.document.documentElement.style.setProperty('color-scheme', colorScheme);
this.emit('themeChanging');
}
}
export function getThemesRef(): Ref<Theme[]> {
return prefer.r.themes;
}
export const themeManager = new ThemeManager();
export const isPreviewMode = ref(false);
themeManager.on('requestUpdateThemeCache', (theme, props) => {
miLocalStorage.setItem('theme', JSON.stringify(props));
miLocalStorage.setItem('themeId', theme.id);
miLocalStorage.setItem('themeCachedVersion', version);
});
themeManager.on('previewStateChanged', (preview) => {
isPreviewMode.value = preview;
});
export async function addTheme(theme: Theme): Promise<void> {
if ($i == null) return;
@@ -93,163 +203,6 @@ export async function removeTheme(theme: Theme): Promise<void> {
prefer.commit('themes', themes);
}
function applyThemeInternal(theme: Theme, persist: boolean) {
const colorScheme = theme.base === 'dark' ? 'dark' : 'light';
window.document.documentElement.dataset.colorScheme = colorScheme;
// Deep copy
const _theme = deepClone(theme);
if (_theme.base) {
const base = [lightTheme, darkTheme].find(x => x.id === _theme.base);
if (base) _theme.props = Object.assign({}, base.props, _theme.props);
}
const props = compile(_theme);
for (const tag of window.document.head.children) {
if (tag.tagName === 'META' && tag.getAttribute('name') === 'theme-color') {
tag.setAttribute('content', props['htmlThemeColor']);
break;
}
}
for (const [k, v] of Object.entries(props)) {
window.document.documentElement.style.setProperty(`--MI_THEME-${k}`, v.toString());
}
window.document.documentElement.style.setProperty('color-scheme', colorScheme);
if (persist) {
miLocalStorage.setItem('theme', JSON.stringify(props));
miLocalStorage.setItem('themeId', theme.id);
miLocalStorage.setItem('themeCachedVersion', version);
miLocalStorage.setItem('colorScheme', colorScheme);
}
// 色計算など再度行えるようにクライアント全体に通知
globalEvents.emit('themeChanging');
}
let timeout: number | null = null;
let currentThemeId = miLocalStorage.getItem('themeId');
export function applyTheme(theme: Theme, persist = true) {
if (timeout) {
window.clearTimeout(timeout);
timeout = null;
}
if (theme.id === currentThemeId && miLocalStorage.getItem('themeCachedVersion') === version) return;
currentThemeId = theme.id;
// visibilityStateがhiddenな状態でstartViewTransitionするとブラウザによってはエラーになる
// 通常hiddenな時に呼ばれることはないが、iOSのPWAだとアプリ切り替え時に(何故か)hiddenな状態で(何故か)一瞬デバイスのダークモード判定が変わりapplyThemeが呼ばれる場合がある
if (window.document.startViewTransition != null && window.document.visibilityState === 'visible') {
window.document.documentElement.classList.add('_themeChanging_');
try {
window.document.startViewTransition(async () => {
applyThemeInternal(theme, persist);
await nextTick();
}).finished.then(() => {
window.document.documentElement.classList.remove('_themeChanging_');
globalEvents.emit('themeChanged');
});
} catch (err) {
// 様々な理由により startViewTransition は失敗することがある
// ref. https://github.com/misskey-dev/misskey/issues/16562
// FIXME: viewTransitonエラーはtry~catch貫通してそうな気配がする
console.error(err);
window.document.documentElement.classList.remove('_themeChanging_');
applyThemeInternal(theme, persist);
globalEvents.emit('themeChanged');
}
} else {
applyThemeInternal(theme, persist);
globalEvents.emit('themeChanged');
}
}
export function compile(theme: Theme): Record<string, string> {
function getColor(val: string): tinycolor.Instance {
if (val[0] === '@') { // ref (prop)
return getColor(theme.props[val.substring(1)]);
} else if (val[0] === '$') { // ref (const)
return getColor(theme.props[val]);
} else if (val[0] === ':') { // func
const parts = val.split('<');
const funcTxt = parts.shift();
const argTxt = parts.shift();
if (funcTxt && argTxt) {
const func = funcTxt.substring(1);
const arg = parseFloat(argTxt);
const color = getColor(parts.join('<'));
switch (func) {
case 'darken': return color.darken(arg);
case 'lighten': return color.lighten(arg);
case 'alpha': return color.setAlpha(arg);
case 'hue': return color.spin(arg);
case 'saturate': return color.saturate(arg);
}
}
}
// other case
return tinycolor(val);
}
const props = {} as Record<string, string>;
for (const [k, v] of Object.entries(theme.props)) {
if (k.startsWith('$')) continue; // ignore const
props[k] = v.startsWith('"') ? v.replace(/^"\s*/, '') : genValue(getColor(v));
}
return props;
}
function genValue(c: tinycolor.Instance): string {
return c.toRgbString();
}
export function validateTheme(theme: Record<string, any>): boolean {
if (theme.id == null || typeof theme.id !== 'string') return false;
if (theme.name == null || typeof theme.name !== 'string') return false;
if (theme.base == null || !['light', 'dark'].includes(theme.base)) return false;
if (theme.props == null || typeof theme.props !== 'object') return false;
return true;
}
export function parseThemeCode(code: string): Theme {
let theme;
try {
theme = JSON5.parse(code);
} catch (_) {
throw new Error('Failed to parse theme json');
}
if (!validateTheme(theme)) {
throw new Error('This theme is invaild');
}
if (prefer.s.themes.some(t => t.id === theme.id)) {
throw new Error('This theme is already installed');
}
return theme;
}
export function previewTheme(code: string): void {
const theme = parseThemeCode(code);
if (theme != null) applyTheme(theme, false);
}
export async function installTheme(code: string): Promise<void> {
const theme = parseThemeCode(code);
if (theme == null) return;
@@ -261,3 +214,26 @@ export function clearAppliedThemeCache() {
miLocalStorage.removeItem('themeId');
miLocalStorage.removeItem('themeCachedVersion');
}
export function handleThemeInstallError(err: unknown) {
if (err instanceof Error) {
let message = '';
switch (err.message.toLowerCase()) {
case 'this theme is already installed':
case 'already exists':
case 'builtin theme':
message = i18n.ts._theme.alreadyInstalled;
break;
default:
message = i18n.ts._theme.invalid;
break;
}
os.alert({
type: 'error',
text: message,
});
}
console.error(err);
}

View File

@@ -0,0 +1,57 @@
<!--
SPDX-FileCopyrightText: syuilo and misskey-project
SPDX-License-Identifier: AGPL-3.0-only
-->
<template>
<div :class="$style.root">
<span :class="$style.icon">
<i class="ti ti-info-circle"></i>
</span>
<span :class="$style.title">{{ i18n.ts.previewingTheme }}</span>
<span :class="$style.body"><button class="_textButton" style="color: var(--MI_THEME-fgOnAccent);" @click="restore">{{ i18n.ts.previewingThemeRestore }}</button> | <MkA class="_textButton" style="color: var(--MI_THEME-fgOnAccent);" to="/settings/theme">{{ i18n.ts.settings }}</MkA></span>
</div>
</template>
<script lang="ts" setup>
import { i18n } from '@/i18n.js';
import { themeManager } from '@/theme.js';
function restore() {
themeManager.clearPreview();
}
</script>
<style lang="scss" module>
.root {
--height: 24px;
font-size: 0.85em;
display: flex;
vertical-align: bottom;
width: 100%;
line-height: var(--height);
height: var(--height);
overflow: clip;
contain: strict;
background: var(--MI_THEME-accent);
color: var(--MI_THEME-fgOnAccent);
}
.icon {
margin-left: 10px;
animation: blink 2s infinite;
}
.title {
padding: 0 10px;
font-weight: bold;
}
.body {
min-width: 0;
flex: 1;
overflow: clip;
white-space: nowrap;
text-overflow: ellipsis;
}
</style>

View File

@@ -15,6 +15,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<XReloadSuggestion v-if="shouldSuggestReload"/>
<XPreferenceRestore v-if="shouldSuggestRestoreBackup"/>
<XThemePreviewing v-if="isThemePreviewMode"/>
<XAnnouncements v-if="$i"/>
<XStatusBars/>
<div :class="$style.columnsWrapper">
@@ -94,12 +95,14 @@ import XMobileFooterMenu from '@/ui/_common_/mobile-footer-menu.vue';
import XTitlebar from '@/ui/_common_/titlebar.vue';
import XPreferenceRestore from '@/ui/_common_/PreferenceRestore.vue';
import XReloadSuggestion from '@/ui/_common_/ReloadSuggestion.vue';
import XThemePreviewing from '@/ui/_common_/ThemePreviewing.vue';
import * as os from '@/os.js';
import { $i } from '@/i.js';
import { i18n } from '@/i18n.js';
import { deviceKind } from '@/utility/device-kind.js';
import { prefer } from '@/preferences.js';
import { store } from '@/store.js';
import { isPreviewMode as isThemePreviewMode } from '@/theme.js';
import XMainColumn from '@/ui/deck/main-column.vue';
import XTlColumn from '@/ui/deck/tl-column.vue';
import XAntennaColumn from '@/ui/deck/antenna-column.vue';

View File

@@ -14,6 +14,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<div>
<XReloadSuggestion v-if="shouldSuggestReload"/>
<XPreferenceRestore v-if="shouldSuggestRestoreBackup"/>
<XThemePreviewing v-if="isThemePreviewMode"/>
<XAnnouncements v-if="$i"/>
<XStatusBars :class="$style.statusbars"/>
</div>
@@ -40,8 +41,10 @@ import type { PageMetadata } from '@/page.js';
import XMobileFooterMenu from '@/ui/_common_/mobile-footer-menu.vue';
import XPreferenceRestore from '@/ui/_common_/PreferenceRestore.vue';
import XReloadSuggestion from '@/ui/_common_/ReloadSuggestion.vue';
import XThemePreviewing from '@/ui/_common_/ThemePreviewing.vue';
import XTitlebar from '@/ui/_common_/titlebar.vue';
import XSidebar from '@/ui/_common_/navbar.vue';
import { isPreviewMode as isThemePreviewMode } from '@/theme.js';
import * as os from '@/os.js';
import { i18n } from '@/i18n.js';
import { $i } from '@/i.js';

View File

@@ -44,7 +44,7 @@ export async function getTheme(mode: 'light' | 'dark', getName = false): Promise
_res.type = mode;
if (getName) {
return _res.name;
return _res.name!;
}
return _res;
}

View File

@@ -24,6 +24,7 @@ import {
import gradient from 'chartjs-plugin-gradient';
import zoomPlugin from 'chartjs-plugin-zoom';
import { MatrixController, MatrixElement } from 'chartjs-chart-matrix';
import { themeManager } from '@/theme.js';
import { store } from '@/store.js';
import 'chartjs-adapter-date-fns';
@@ -50,7 +51,7 @@ export function initChart() {
);
// フォントカラー
Chart.defaults.color = getComputedStyle(window.document.documentElement).getPropertyValue('--MI_THEME-fg');
Chart.defaults.color = themeManager.currentCompiledTheme!.fg;
Chart.defaults.borderColor = store.s.darkMode ? 'rgba(255, 255, 255, 0.1)' : 'rgba(0, 0, 0, 0.1)';

View File

@@ -11,8 +11,9 @@ export function popout(path: string, w?: HTMLElement) {
url = appendQuery(url, 'zen');
if (w) {
const position = w.getBoundingClientRect();
const width = parseInt(getComputedStyle(w, '').width, 10);
const height = parseInt(getComputedStyle(w, '').height, 10);
const computedStyle = getComputedStyle(w, '');
const width = parseInt(computedStyle.width, 10);
const height = parseInt(computedStyle.height, 10);
const x = window.screenX + position.left;
const y = window.screenY + position.top;
window.open(url, url,

View File

@@ -5,8 +5,8 @@
import { genId } from '@/utility/id.js';
import type { Theme } from '@/theme.js';
import { themeProps } from '@/theme.js';
import type { Theme } from '@@/js/theme.js';
import { themeProps } from '@@/js/theme.js';
export type Default = null;
export type Color = string;

View File

@@ -0,0 +1,188 @@
/*
* SPDX-FileCopyrightText: syuilo and misskey-project
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { afterEach, assert, beforeEach, describe, test, vi } from 'vitest';
import type { Theme } from '@@/js/theme.js';
import lightTheme from '@@/themes/_light.json5';
import darkTheme from '@@/themes/_dark.json5';
import './init';
vi.mock('@/i18n.js', () => ({
i18n: {
ts: {
_theme: {
alreadyInstalled: 'already installed',
invalid: 'invalid',
},
},
},
updateI18n: vi.fn(),
}));
vi.mock('@/os.js', () => ({
alert: vi.fn(),
}));
const cloneTheme = <T>(value: T): T => structuredClone(value);
const createTheme = (base: 'light' | 'dark', options: {
id: string;
name: string;
accent: string;
bg: string;
fg: string;
}): Theme => {
const builtin = base === 'dark' ? darkTheme : lightTheme;
return {
id: options.id,
name: options.name,
author: 'tester',
base,
props: {
...cloneTheme(builtin.props),
accent: options.accent,
bg: options.bg,
fg: options.fg,
},
};
};
const primaryTheme = createTheme('light', {
id: 'primary-theme',
name: 'Primary Theme',
accent: '#224488',
bg: '#faf7f2',
fg: '#1a1a1a',
});
const previewTheme = createTheme('dark', {
id: 'preview-theme',
name: 'Preview Theme',
accent: '#55aa33',
bg: '#101820',
fg: '#f4f4f4',
});
const replacementTheme = createTheme('dark', {
id: 'replacement-theme',
name: 'Replacement Theme',
accent: '#bb5500',
bg: '#18110f',
fg: '#f6e7df',
});
const loadThemeModule = async () => {
vi.resetModules();
return await import('@/theme.js');
};
const resetDocument = () => {
window.localStorage.clear();
document.head.innerHTML = '<meta name="theme-color" content="#000000">';
document.documentElement.className = '';
document.documentElement.removeAttribute('data-color-scheme');
document.documentElement.style.cssText = '';
Reflect.deleteProperty(document, 'startViewTransition');
Object.defineProperty(document, 'visibilityState', {
configurable: true,
value: 'visible',
});
};
describe('ThemeManager', () => {
beforeEach(() => {
resetDocument();
});
afterEach(() => {
window.localStorage.clear();
});
test('通常テーマ適用後のプレビューは現在テーマのみを切り替え、キャッシュは保持する', async () => {
const { themeManager, isPreviewMode } = await loadThemeModule();
themeManager.updateTheme(primaryTheme);
const cachedTheme = window.localStorage.getItem('theme');
const cachedThemeId = window.localStorage.getItem('themeId');
themeManager.previewTheme(previewTheme);
assert.strictEqual(themeManager.theme?.id, primaryTheme.id);
assert.strictEqual(themeManager.currentTheme?.id, previewTheme.id);
assert.strictEqual(themeManager.currentThemeId, previewTheme.id);
assert.strictEqual(themeManager.isPreviewMode, true);
assert.strictEqual(isPreviewMode.value, true);
assert.strictEqual(document.documentElement.dataset.colorScheme, 'dark');
assert.strictEqual(document.documentElement.style.getPropertyValue('--MI_THEME-accent'), themeManager.currentCompiledTheme?.accent);
assert.strictEqual(window.localStorage.getItem('theme'), cachedTheme);
assert.strictEqual(window.localStorage.getItem('themeId'), cachedThemeId);
});
test('プレビュー解除で元のテーマと DOM 状態が復元される', async () => {
const { themeManager, isPreviewMode } = await loadThemeModule();
themeManager.updateTheme(primaryTheme);
const originalCompiledThemeColor = themeManager.currentCompiledTheme?.htmlThemeColor;
themeManager.previewTheme(previewTheme);
const previewCompiledThemeColor = themeManager.currentCompiledTheme?.htmlThemeColor;
assert.strictEqual(themeManager.currentTheme?.id, previewTheme.id);
assert.notStrictEqual(previewCompiledThemeColor, originalCompiledThemeColor);
themeManager.clearPreview();
assert.strictEqual(themeManager.theme?.id, primaryTheme.id);
assert.strictEqual(themeManager.currentTheme?.id, primaryTheme.id);
assert.strictEqual(themeManager.currentCompiledTheme?.htmlThemeColor, originalCompiledThemeColor);
assert.strictEqual(themeManager.isPreviewMode, false);
assert.strictEqual(isPreviewMode.value, false);
assert.strictEqual(document.documentElement.dataset.colorScheme, 'light');
assert.strictEqual(document.documentElement.style.getPropertyValue('--MI_THEME-accent'), themeManager.currentCompiledTheme?.accent);
assert.strictEqual(document.head.querySelector('meta[name="theme-color"]')?.getAttribute('content'), originalCompiledThemeColor);
assert.strictEqual(window.localStorage.getItem('themeId'), primaryTheme.id);
});
test('プレビュー中に通常テーマを更新するとプレビューを抜けて新しい通常テーマが適用される', async () => {
const { themeManager, isPreviewMode } = await loadThemeModule();
themeManager.updateTheme(primaryTheme);
themeManager.previewTheme(previewTheme);
themeManager.updateTheme(replacementTheme);
assert.strictEqual(themeManager.theme?.id, replacementTheme.id);
assert.strictEqual(themeManager.currentTheme?.id, replacementTheme.id);
assert.strictEqual(themeManager.isPreviewMode, false);
assert.strictEqual(isPreviewMode.value, false);
assert.strictEqual(document.documentElement.dataset.colorScheme, 'dark');
assert.strictEqual(document.documentElement.style.getPropertyValue('--MI_THEME-accent'), themeManager.currentCompiledTheme?.accent);
assert.strictEqual(window.localStorage.getItem('themeId'), replacementTheme.id);
});
test('themeChanging と themeChanged はプレビュー適用と復帰のたびに発火する', async () => {
const { themeManager } = await loadThemeModule();
const events: string[] = [];
themeManager.on('themeChanging', () => {
events.push('themeChanging');
});
themeManager.on('themeChanged', () => {
events.push('themeChanged');
});
themeManager.updateTheme(primaryTheme);
themeManager.previewTheme(previewTheme);
themeManager.clearPreview();
assert.deepStrictEqual(events, [
'themeChanging',
'themeChanged',
'themeChanging',
'themeChanged',
'themeChanging',
'themeChanged',
]);
});
});

View File

@@ -23,7 +23,8 @@
"resolveJsonModule": true,
"isolatedModules": true,
"paths": {
"@/*": ["../src/*"]
"@/*": ["../src/*"],
"@@/*": ["../../frontend-shared/*"]
},
"typeRoots": [
"../node_modules/@types"
@@ -37,6 +38,7 @@
"compileOnSave": false,
"include": [
"./**/*.ts",
"../src/**/*.vue"
"../src/**/*.vue",
"../@types/**/*.d.ts"
]
}

View File

@@ -30,8 +30,8 @@
"devDependencies": {
"@types/js-yaml": "4.0.9",
"@types/node": "24.12.2",
"@typescript-eslint/eslint-plugin": "8.58.2",
"@typescript-eslint/parser": "8.58.2",
"@typescript-eslint/eslint-plugin": "8.59.0",
"@typescript-eslint/parser": "8.59.0",
"chokidar": "5.0.0",
"esbuild": "0.28.0",
"execa": "9.6.1",

View File

@@ -5651,6 +5651,14 @@ export interface Locale extends ILocale {
* リノート先のチャンネルを見る
*/
"viewRenotedChannel": string;
/**
* テーマのプレビュー中
*/
"previewingTheme": string;
/**
* 元に戻す
*/
"previewingThemeRestore": string;
"_imageEditing": {
"_vars": {
/**
@@ -8177,6 +8185,10 @@ export interface Locale extends ILocale {
* 翻訳機能の利用
*/
"canUseTranslator": string;
/**
* チャンネルの作成
*/
"canCreateChannel": string;
/**
* アイコンデコレーションの最大取付個数
*/

View File

@@ -13,8 +13,8 @@
"devDependencies": {
"@types/node": "24.12.2",
"@types/wawoff2": "1.0.2",
"@typescript-eslint/eslint-plugin": "8.58.2",
"@typescript-eslint/parser": "8.58.2"
"@typescript-eslint/eslint-plugin": "8.59.0",
"@typescript-eslint/parser": "8.59.0"
},
"dependencies": {
"@tabler/icons-webfont": "3.35.0",

View File

@@ -27,8 +27,8 @@
"@types/matter-js": "0.20.2",
"@types/node": "24.12.2",
"@types/seedrandom": "3.0.8",
"@typescript-eslint/eslint-plugin": "8.58.2",
"@typescript-eslint/parser": "8.58.2",
"@typescript-eslint/eslint-plugin": "8.59.0",
"@typescript-eslint/parser": "8.59.0",
"esbuild": "0.28.0",
"execa": "9.6.1",
"nodemon": "3.1.14"

View File

@@ -4,11 +4,13 @@
```ts
import type { AuthenticationResponseJSON } from '@simplewebauthn/types';
import type { AuthenticationResponseJSON } from '@simplewebauthn/browser';
import { EventEmitter } from 'eventemitter3';
import { Options } from 'reconnecting-websocket';
import type { PublicKeyCredentialRequestOptionsJSON as PublicKeyCredentialRequestOptionsJSON_2 } from '@simplewebauthn/types';
import type { PublicKeyCredentialCreationOptionsJSON as PublicKeyCredentialCreationOptionsJSON_2 } from '@simplewebauthn/browser';
import type { PublicKeyCredentialRequestOptionsJSON as PublicKeyCredentialRequestOptionsJSON_2 } from '@simplewebauthn/browser';
import _ReconnectingWebSocket from 'reconnecting-websocket';
import type { RegistrationResponseJSON } from '@simplewebauthn/browser';
// Warning: (ae-forgotten-export) The symbol "components" needs to be exported by the entry point index.d.ts
//
@@ -1471,6 +1473,14 @@ export type Endpoints = Overwrite<Endpoints_2, {
};
};
};
'i/2fa/register-key': {
req: I2faRegisterKeyRequest;
res: I2faRegisterKeyResponse_2;
};
'i/2fa/key-done': {
req: I2faKeyDoneRequest_2;
res: I2faKeyDoneResponse;
};
'admin/roles/create': {
req: Overwrite<AdminRolesCreateRequest, {
policies: PartialRolePolicyOverride;
@@ -1510,6 +1520,8 @@ declare namespace entities {
SigninWithPasskeyRequest,
SigninWithPasskeyInitResponse,
SigninWithPasskeyResponse,
I2faRegisterKeyResponse_2 as I2faRegisterKeyResponse,
I2faKeyDoneRequest_2 as I2faKeyDoneRequest,
PartialRolePolicyOverride,
EmptyRequest,
EmptyResponse,
@@ -1911,13 +1923,11 @@ declare namespace entities {
IResponse,
I2faDoneRequest,
I2faDoneResponse,
I2faKeyDoneRequest,
I2faKeyDoneResponse,
I2faPasswordLessRequest,
I2faRegisterRequest,
I2faRegisterResponse,
I2faRegisterKeyRequest,
I2faRegisterKeyResponse,
I2faRemoveKeyRequest,
I2faUnregisterRequest,
I2faUpdateKeyRequest,
@@ -2515,7 +2525,12 @@ type I2faDoneRequest = operations['i___2fa___done']['requestBody']['content']['a
type I2faDoneResponse = operations['i___2fa___done']['responses']['200']['content']['application/json'];
// @public (undocumented)
type I2faKeyDoneRequest = operations['i___2fa___key-done']['requestBody']['content']['application/json'];
type I2faKeyDoneRequest_2 = {
password: string;
token?: string | null;
name: string;
credential: RegistrationResponseJSON;
};
// @public (undocumented)
type I2faKeyDoneResponse = operations['i___2fa___key-done']['responses']['200']['content']['application/json'];
@@ -2527,7 +2542,7 @@ type I2faPasswordLessRequest = operations['i___2fa___password-less']['requestBod
type I2faRegisterKeyRequest = operations['i___2fa___register-key']['requestBody']['content']['application/json'];
// @public (undocumented)
type I2faRegisterKeyResponse = operations['i___2fa___register-key']['responses']['200']['content']['application/json'];
type I2faRegisterKeyResponse_2 = PublicKeyCredentialCreationOptionsJSON_2;
// @public (undocumented)
type I2faRegisterRequest = operations['i___2fa___register']['requestBody']['content']['application/json'];
@@ -3466,7 +3481,7 @@ type RoleLite = components['schemas']['RoleLite'];
type RolePolicies = components['schemas']['RolePolicies'];
// @public (undocumented)
export const rolePolicies: readonly ["gtlAvailable", "ltlAvailable", "canPublicNote", "mentionLimit", "canInvite", "inviteLimit", "inviteLimitCycle", "inviteExpirationTime", "canManageCustomEmojis", "canManageAvatarDecorations", "canSearchNotes", "canSearchUsers", "canUseTranslator", "canHideAds", "driveCapacityMb", "maxFileSizeMb", "alwaysMarkNsfw", "canUpdateBioMedia", "pinLimit", "antennaLimit", "wordMuteLimit", "webhookLimit", "clipLimit", "noteEachClipsLimit", "userListLimit", "userEachUserListsLimit", "rateLimitFactor", "avatarDecorationLimit", "canImportAntennas", "canImportBlocking", "canImportFollowing", "canImportMuting", "canImportUserLists", "chatAvailability", "uploadableFileTypes", "noteDraftLimit", "scheduledNoteLimit", "watermarkAvailable"];
export const rolePolicies: readonly ["gtlAvailable", "ltlAvailable", "canPublicNote", "mentionLimit", "canInvite", "inviteLimit", "inviteLimitCycle", "inviteExpirationTime", "canManageCustomEmojis", "canManageAvatarDecorations", "canSearchNotes", "canSearchUsers", "canUseTranslator", "canHideAds", "canCreateChannel", "driveCapacityMb", "maxFileSizeMb", "alwaysMarkNsfw", "canUpdateBioMedia", "pinLimit", "antennaLimit", "wordMuteLimit", "webhookLimit", "clipLimit", "noteEachClipsLimit", "userListLimit", "userEachUserListsLimit", "rateLimitFactor", "avatarDecorationLimit", "canImportAntennas", "canImportBlocking", "canImportFollowing", "canImportMuting", "canImportUserLists", "chatAvailability", "uploadableFileTypes", "noteDraftLimit", "scheduledNoteLimit", "watermarkAvailable"];
// @public (undocumented)
type RolesListResponse = operations['roles___list']['responses']['200']['content']['application/json'];
@@ -3880,7 +3895,7 @@ type VerifyEmailRequest = operations['verify-email']['requestBody']['content']['
// Warnings were encountered during analysis:
//
// src/entities.ts:55:2 - (ae-forgotten-export) The symbol "ModerationLogPayloads" needs to be exported by the entry point index.d.ts
// src/entities.ts:60:2 - (ae-forgotten-export) The symbol "ModerationLogPayloads" needs to be exported by the entry point index.d.ts
// src/streaming.ts:57:3 - (ae-forgotten-export) The symbol "ReconnectingWebSocket" needs to be exported by the entry point index.d.ts
// src/streaming.types.ts:226:4 - (ae-forgotten-export) The symbol "ReversiUpdateKey" needs to be exported by the entry point index.d.ts
// src/streaming.types.ts:241:4 - (ae-forgotten-export) The symbol "ReversiUpdateSettings" needs to be exported by the entry point index.d.ts

View File

@@ -9,8 +9,8 @@
"devDependencies": {
"@readme/openapi-parser": "6.0.1",
"@types/node": "24.12.2",
"@typescript-eslint/eslint-plugin": "8.58.2",
"@typescript-eslint/parser": "8.58.2",
"@typescript-eslint/eslint-plugin": "8.59.0",
"@typescript-eslint/parser": "8.59.0",
"openapi-types": "12.1.3",
"openapi-typescript": "7.13.0",
"ts-case-convert": "2.1.0",

View File

@@ -414,7 +414,7 @@ async function main() {
await generateEndpoints(openApiDocs, typeFileName, entitiesFileName, endpointFileName);
const apiClientWarningFileName = `${generatePath}/apiClientJSDoc.ts`;
await generateApiClientJSDoc(openApiDocs, '../api.ts', endpointFileName, apiClientWarningFileName);
await generateApiClientJSDoc(openApiDocs, '../api.ts', '../api.types.ts', apiClientWarningFileName);
}
main();

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