mirror of
https://github.com/misskey-dev/misskey.git
synced 2026-07-30 11:55:49 +00:00
Compare commits
37 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 9b2f90f681 | |||
| 7daec62680 | |||
| 440b1e442a | |||
| 1057295d5d | |||
| dad093c6f8 | |||
| 4d85ea8657 | |||
| b1980f1287 | |||
| f42840ad03 | |||
| a01b6af7c1 | |||
| 5b8fdabcf2 | |||
| 41c20ce140 | |||
| 6aad11766a | |||
| 66498639bf | |||
| 76389b684c | |||
| 4b82eab304 | |||
| 3ead8d840e | |||
| c93b093bff | |||
| b33ec08cbf | |||
| 0d677f288a | |||
| 8a3e33218e | |||
| c70fb20b9e | |||
| cde2fe24c4 | |||
| e8951b7aa1 | |||
| d2973ecdca | |||
| cf7a6b6efc | |||
| 7c29a18d61 | |||
| 16ca5a72a6 | |||
| db8ee16e03 | |||
| 521f6006fe | |||
| 19fa9a24ab | |||
| f68da80ebd | |||
| fcb4ced33d | |||
| 85e810533b | |||
| 661838c89e | |||
| 80d14231c5 | |||
| 28153f7b5f | |||
| 6d81c81ab2 |
@@ -16,13 +16,13 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v6.0.3
|
||||
uses: actions/checkout@v7.0.0
|
||||
|
||||
- name: Setup pnpm
|
||||
uses: pnpm/action-setup@v6.0.9
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v6.4.0
|
||||
uses: actions/setup-node@v7.0.0
|
||||
with:
|
||||
node-version-file: '.node-version'
|
||||
cache: 'pnpm'
|
||||
|
||||
@@ -17,12 +17,12 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v6.0.3
|
||||
uses: actions/checkout@v7.0.0
|
||||
|
||||
- name: Setup pnpm
|
||||
uses: pnpm/action-setup@v6.0.9
|
||||
- name: Use Node.js
|
||||
uses: actions/setup-node@v6.4.0
|
||||
uses: actions/setup-node@v7.0.0
|
||||
with:
|
||||
node-version-file: '.node-version'
|
||||
cache: 'pnpm'
|
||||
|
||||
@@ -35,13 +35,13 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout base
|
||||
uses: actions/checkout@v6.0.3
|
||||
uses: actions/checkout@v7.0.0
|
||||
with:
|
||||
ref: ${{ github.base_ref }}
|
||||
path: base
|
||||
submodules: true
|
||||
- name: Checkout head
|
||||
uses: actions/checkout@v6.0.3
|
||||
uses: actions/checkout@v7.0.0
|
||||
with:
|
||||
ref: refs/pull/${{ github.event.number }}/merge
|
||||
path: head
|
||||
@@ -51,7 +51,7 @@ jobs:
|
||||
with:
|
||||
package_json_file: head/package.json
|
||||
- name: Use Node.js
|
||||
uses: actions/setup-node@v6.4.0
|
||||
uses: actions/setup-node@v7.0.0
|
||||
with:
|
||||
node-version-file: 'head/.node-version'
|
||||
cache: 'pnpm'
|
||||
|
||||
@@ -12,13 +12,13 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout head
|
||||
uses: actions/checkout@v6.0.3
|
||||
uses: actions/checkout@v7.0.0
|
||||
|
||||
- name: setup pnpm
|
||||
uses: pnpm/action-setup@v6
|
||||
|
||||
- name: setup node
|
||||
uses: actions/setup-node@v6.4.0
|
||||
uses: actions/setup-node@v7.0.0
|
||||
with:
|
||||
node-version-file: '.node-version'
|
||||
cache: pnpm
|
||||
|
||||
@@ -18,7 +18,7 @@ jobs:
|
||||
if: ${{ github.event.pull_request.mergeable == null || github.event.pull_request.mergeable == true }}
|
||||
steps:
|
||||
- name: checkout
|
||||
uses: actions/checkout@v6.0.3
|
||||
uses: actions/checkout@v7.0.0
|
||||
with:
|
||||
submodules: true
|
||||
persist-credentials: false
|
||||
@@ -29,7 +29,7 @@ jobs:
|
||||
|
||||
- name: setup node
|
||||
id: setup-node
|
||||
uses: actions/setup-node@v6.4.0
|
||||
uses: actions/setup-node@v7.0.0
|
||||
with:
|
||||
node-version-file: '.node-version'
|
||||
cache: pnpm
|
||||
@@ -66,7 +66,7 @@ jobs:
|
||||
if: ${{ github.event.pull_request.mergeable == null || github.event.pull_request.mergeable == true }}
|
||||
steps:
|
||||
- name: checkout
|
||||
uses: actions/checkout@v6.0.3
|
||||
uses: actions/checkout@v7.0.0
|
||||
with:
|
||||
submodules: true
|
||||
persist-credentials: false
|
||||
|
||||
@@ -20,7 +20,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v6.0.3
|
||||
uses: actions/checkout@v7.0.0
|
||||
- name: Check version
|
||||
run: |
|
||||
if [ "$(jq -r '.version' package.json)" != "$(jq -r '.version' packages/misskey-js/package.json)" ]; then
|
||||
|
||||
@@ -12,7 +12,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v6.0.3
|
||||
uses: actions/checkout@v7.0.0
|
||||
- name: Check
|
||||
run: |
|
||||
counter=0
|
||||
|
||||
@@ -10,7 +10,7 @@ jobs:
|
||||
check_copyright_year:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v6.0.3
|
||||
- uses: actions/checkout@v7.0.0
|
||||
- run: |
|
||||
if [ "$(grep Copyright COPYING | sed -e 's/.*2014-\([0-9]*\) .*/\1/g')" -ne "$(date +%Y)" ]; then
|
||||
echo "Please change copyright year!"
|
||||
|
||||
@@ -27,7 +27,7 @@ jobs:
|
||||
platform=${{ matrix.platform }}
|
||||
echo "PLATFORM_PAIR=${platform//\//-}" >> $GITHUB_ENV
|
||||
- name: Check out the repo
|
||||
uses: actions/checkout@v6.0.3
|
||||
uses: actions/checkout@v7.0.0
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v4
|
||||
- name: Log in to Docker Hub
|
||||
|
||||
@@ -32,7 +32,7 @@ jobs:
|
||||
platform=${{ matrix.platform }}
|
||||
echo "PLATFORM_PAIR=${platform//\//-}" >> $GITHUB_ENV
|
||||
- name: Check out the repo
|
||||
uses: actions/checkout@v6.0.3
|
||||
uses: actions/checkout@v7.0.0
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v4
|
||||
- name: Docker meta
|
||||
|
||||
@@ -17,7 +17,7 @@ jobs:
|
||||
DOCKLE_VERSION: 0.4.15
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v6.0.3
|
||||
- uses: actions/checkout@v7.0.0
|
||||
|
||||
- name: Download and install dockle v${{ env.DOCKLE_VERSION }}
|
||||
run: |
|
||||
|
||||
@@ -56,7 +56,7 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout base
|
||||
uses: actions/checkout@v6.0.3
|
||||
uses: actions/checkout@v7.0.0
|
||||
with:
|
||||
persist-credentials: false
|
||||
repository: ${{ github.event.pull_request.base.repo.full_name }}
|
||||
@@ -65,7 +65,7 @@ jobs:
|
||||
submodules: true
|
||||
|
||||
- name: Checkout head
|
||||
uses: actions/checkout@v6.0.3
|
||||
uses: actions/checkout@v7.0.0
|
||||
with:
|
||||
persist-credentials: false
|
||||
repository: ${{ github.event.pull_request.head.repo.full_name }}
|
||||
@@ -79,7 +79,7 @@ jobs:
|
||||
package_json_file: head/package.json
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v6.4.0
|
||||
uses: actions/setup-node@v7.0.0
|
||||
with:
|
||||
node-version-file: head/.node-version
|
||||
cache: pnpm
|
||||
|
||||
@@ -25,14 +25,14 @@ jobs:
|
||||
ref: refs/pull/${{ github.event.number }}/merge
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v6.0.3
|
||||
- uses: actions/checkout@v7.0.0
|
||||
with:
|
||||
ref: ${{ matrix.ref }}
|
||||
submodules: true
|
||||
- name: Setup pnpm
|
||||
uses: pnpm/action-setup@v6.0.9
|
||||
- name: Use Node.js
|
||||
uses: actions/setup-node@v6.4.0
|
||||
uses: actions/setup-node@v7.0.0
|
||||
with:
|
||||
node-version-file: '.node-version'
|
||||
cache: 'pnpm'
|
||||
|
||||
@@ -11,6 +11,6 @@ jobs:
|
||||
pull-requests: write
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/labeler@v6
|
||||
- uses: actions/labeler@v7
|
||||
with:
|
||||
repo-token: "${{ secrets.GITHUB_TOKEN }}"
|
||||
|
||||
@@ -40,13 +40,13 @@ jobs:
|
||||
pnpm_install:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v6.0.3
|
||||
- uses: actions/checkout@v7.0.0
|
||||
with:
|
||||
fetch-depth: 0
|
||||
submodules: true
|
||||
- name: Setup pnpm
|
||||
uses: pnpm/action-setup@v6.0.9
|
||||
- uses: actions/setup-node@v6.4.0
|
||||
- uses: actions/setup-node@v7.0.0
|
||||
with:
|
||||
node-version-file: '.node-version'
|
||||
cache: 'pnpm'
|
||||
@@ -73,19 +73,19 @@ jobs:
|
||||
eslint-cache-version: v1
|
||||
eslint-cache-path: ${{ github.workspace }}/node_modules/.cache/eslint-${{ matrix.workspace }}
|
||||
steps:
|
||||
- uses: actions/checkout@v6.0.3
|
||||
- uses: actions/checkout@v7.0.0
|
||||
with:
|
||||
fetch-depth: 0
|
||||
submodules: true
|
||||
- name: Setup pnpm
|
||||
uses: pnpm/action-setup@v6.0.9
|
||||
- uses: actions/setup-node@v6.4.0
|
||||
- uses: actions/setup-node@v7.0.0
|
||||
with:
|
||||
node-version-file: '.node-version'
|
||||
cache: 'pnpm'
|
||||
- run: pnpm i --frozen-lockfile
|
||||
- name: Restore eslint cache
|
||||
uses: actions/cache@v5.0.5
|
||||
uses: actions/cache@v6.1.0
|
||||
with:
|
||||
path: ${{ env.eslint-cache-path }}
|
||||
key: eslint-${{ env.eslint-cache-version }}-${{ matrix.workspace }}-${{ hashFiles('**/pnpm-lock.yaml') }}-${{ github.ref_name }}-${{ github.sha }}
|
||||
@@ -104,13 +104,13 @@ jobs:
|
||||
- sw
|
||||
- misskey-js
|
||||
steps:
|
||||
- uses: actions/checkout@v6.0.3
|
||||
- uses: actions/checkout@v7.0.0
|
||||
with:
|
||||
fetch-depth: 0
|
||||
submodules: true
|
||||
- name: Setup pnpm
|
||||
uses: pnpm/action-setup@v6.0.9
|
||||
- uses: actions/setup-node@v6.4.0
|
||||
- uses: actions/setup-node@v7.0.0
|
||||
with:
|
||||
node-version-file: '.node-version'
|
||||
cache: 'pnpm'
|
||||
@@ -123,13 +123,13 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
continue-on-error: true
|
||||
steps:
|
||||
- uses: actions/checkout@v6.0.3
|
||||
- uses: actions/checkout@v7.0.0
|
||||
with:
|
||||
fetch-depth: 0
|
||||
submodules: true
|
||||
- name: Setup pnpm
|
||||
uses: pnpm/action-setup@v6.0.9
|
||||
- uses: actions/setup-node@v6.4.0
|
||||
- uses: actions/setup-node@v7.0.0
|
||||
with:
|
||||
node-version-file: '.node-version'
|
||||
cache: 'pnpm'
|
||||
|
||||
@@ -16,13 +16,13 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
continue-on-error: true
|
||||
steps:
|
||||
- uses: actions/checkout@v6.0.3
|
||||
- uses: actions/checkout@v7.0.0
|
||||
with:
|
||||
fetch-depth: 0
|
||||
submodules: true
|
||||
- name: Setup pnpm
|
||||
uses: pnpm/action-setup@v6.0.9
|
||||
- uses: actions/setup-node@v6.4.0
|
||||
- uses: actions/setup-node@v7.0.0
|
||||
with:
|
||||
node-version-file: ".node-version"
|
||||
cache: "pnpm"
|
||||
|
||||
@@ -16,13 +16,13 @@ jobs:
|
||||
id-token: write
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v6.0.3
|
||||
- uses: actions/checkout@v7.0.0
|
||||
with:
|
||||
submodules: true
|
||||
- name: Setup pnpm
|
||||
uses: pnpm/action-setup@v6.0.9
|
||||
- name: Use Node.js
|
||||
uses: actions/setup-node@v6.4.0
|
||||
uses: actions/setup-node@v7.0.0
|
||||
with:
|
||||
node-version-file: '.node-version'
|
||||
cache: 'pnpm'
|
||||
|
||||
@@ -36,12 +36,12 @@ jobs:
|
||||
- diagnostics-frontend
|
||||
- changelog-checker
|
||||
steps:
|
||||
- uses: actions/checkout@v6.0.3
|
||||
- uses: actions/checkout@v7.0.0
|
||||
with:
|
||||
persist-credentials: false
|
||||
- name: Setup pnpm
|
||||
uses: pnpm/action-setup@v6.0.9
|
||||
- uses: actions/setup-node@v6.4.0
|
||||
- uses: actions/setup-node@v7.0.0
|
||||
with:
|
||||
node-version-file: '.node-version'
|
||||
cache: 'pnpm'
|
||||
|
||||
@@ -19,7 +19,7 @@ jobs:
|
||||
edit:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
- uses: actions/checkout@v7
|
||||
# headが$GITHUB_REF_NAME, baseが$STABLE_BRANCHかつopenのPRを1つ取得
|
||||
- name: Get PR
|
||||
run: |
|
||||
|
||||
@@ -36,7 +36,7 @@ jobs:
|
||||
outputs:
|
||||
pr_number: ${{ steps.get_pr.outputs.pr_number }}
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
- uses: actions/checkout@v7
|
||||
# headが$GITHUB_REF_NAME, baseが$STABLE_BRANCHかつopenのPRを1つ取得
|
||||
- name: Get PRs
|
||||
run: |
|
||||
|
||||
@@ -22,12 +22,12 @@ jobs:
|
||||
NODE_OPTIONS: "--max_old_space_size=7168"
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v6.0.3
|
||||
- uses: actions/checkout@v7.0.0
|
||||
if: github.event_name != 'pull_request_target'
|
||||
with:
|
||||
fetch-depth: 0
|
||||
submodules: true
|
||||
- uses: actions/checkout@v6.0.3
|
||||
- uses: actions/checkout@v7.0.0
|
||||
if: github.event_name == 'pull_request_target'
|
||||
with:
|
||||
fetch-depth: 0
|
||||
@@ -39,7 +39,7 @@ jobs:
|
||||
- name: Setup pnpm
|
||||
uses: pnpm/action-setup@v6.0.9
|
||||
- name: Use Node.js
|
||||
uses: actions/setup-node@v6.4.0
|
||||
uses: actions/setup-node@v7.0.0
|
||||
with:
|
||||
node-version-file: '.node-version'
|
||||
cache: 'pnpm'
|
||||
|
||||
@@ -43,7 +43,7 @@ jobs:
|
||||
ports:
|
||||
- 56312:6379
|
||||
meilisearch:
|
||||
image: getmeili/meilisearch:v1.48.1
|
||||
image: getmeili/meilisearch:v1.49.0
|
||||
ports:
|
||||
- 57712:7700
|
||||
env:
|
||||
@@ -51,7 +51,7 @@ jobs:
|
||||
MEILI_ENV: development
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v6.0.3
|
||||
- uses: actions/checkout@v7.0.0
|
||||
with:
|
||||
submodules: true
|
||||
- name: Setup pnpm
|
||||
@@ -60,7 +60,7 @@ jobs:
|
||||
run: |
|
||||
sudo apt install -y ffmpeg
|
||||
- name: Use Node.js
|
||||
uses: actions/setup-node@v6.4.0
|
||||
uses: actions/setup-node@v7.0.0
|
||||
with:
|
||||
node-version-file: ${{ matrix.node-version-file }}
|
||||
cache: 'pnpm'
|
||||
@@ -74,7 +74,7 @@ jobs:
|
||||
- name: Test
|
||||
run: pnpm --filter backend test-and-coverage
|
||||
- name: Upload to Codecov
|
||||
uses: codecov/codecov-action@v6
|
||||
uses: codecov/codecov-action@v7
|
||||
with:
|
||||
token: ${{ secrets.CODECOV_TOKEN }}
|
||||
files: ./packages/backend/coverage/coverage-final.json
|
||||
@@ -103,13 +103,13 @@ jobs:
|
||||
- 56312:6379
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v6.0.3
|
||||
- uses: actions/checkout@v7.0.0
|
||||
with:
|
||||
submodules: true
|
||||
- name: Setup pnpm
|
||||
uses: pnpm/action-setup@v6.0.9
|
||||
- name: Use Node.js
|
||||
uses: actions/setup-node@v6.4.0
|
||||
uses: actions/setup-node@v7.0.0
|
||||
with:
|
||||
node-version-file: ${{ matrix.node-version-file }}
|
||||
cache: 'pnpm'
|
||||
@@ -123,7 +123,7 @@ jobs:
|
||||
- name: Test
|
||||
run: pnpm --filter backend test-and-coverage:e2e
|
||||
- name: Upload to Codecov
|
||||
uses: codecov/codecov-action@v6
|
||||
uses: codecov/codecov-action@v7
|
||||
with:
|
||||
token: ${{ secrets.CODECOV_TOKEN }}
|
||||
files: ./packages/backend/coverage/coverage-final.json
|
||||
@@ -147,7 +147,7 @@ jobs:
|
||||
POSTGRES_HOST_AUTH_METHOD: trust
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v6.0.3
|
||||
- uses: actions/checkout@v7.0.0
|
||||
with:
|
||||
submodules: true
|
||||
- name: Setup pnpm
|
||||
@@ -156,7 +156,7 @@ jobs:
|
||||
id: current-date
|
||||
run: echo "today=$(date +'%Y-%m-%d')" >> $GITHUB_OUTPUT
|
||||
- name: Use Node.js
|
||||
uses: actions/setup-node@v6.4.0
|
||||
uses: actions/setup-node@v7.0.0
|
||||
with:
|
||||
node-version-file: ${{ matrix.node-version-file }}
|
||||
cache: 'pnpm'
|
||||
|
||||
@@ -26,7 +26,7 @@ jobs:
|
||||
- .node-version
|
||||
- .github/min.node-version
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
- uses: actions/checkout@v7
|
||||
with:
|
||||
submodules: true
|
||||
- name: Setup pnpm
|
||||
@@ -35,7 +35,7 @@ jobs:
|
||||
run: |
|
||||
sudo apt install -y ffmpeg
|
||||
- name: Use Node.js
|
||||
uses: actions/setup-node@v6.4.0
|
||||
uses: actions/setup-node@v7.0.0
|
||||
with:
|
||||
node-version-file: ${{ matrix.node-version-file }}
|
||||
cache: 'pnpm'
|
||||
|
||||
@@ -28,13 +28,13 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v6.0.3
|
||||
- uses: actions/checkout@v7.0.0
|
||||
with:
|
||||
submodules: true
|
||||
- name: Setup pnpm
|
||||
uses: pnpm/action-setup@v6.0.9
|
||||
- name: Use Node.js
|
||||
uses: actions/setup-node@v6.4.0
|
||||
uses: actions/setup-node@v7.0.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@v6
|
||||
uses: codecov/codecov-action@v7
|
||||
with:
|
||||
token: ${{ secrets.CODECOV_TOKEN }}
|
||||
files: ./packages/frontend/coverage/coverage-final.json
|
||||
@@ -71,13 +71,13 @@ jobs:
|
||||
- 56312:6379
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v6.0.3
|
||||
- uses: actions/checkout@v7.0.0
|
||||
with:
|
||||
submodules: true
|
||||
- name: Setup pnpm
|
||||
uses: pnpm/action-setup@v6.0.9
|
||||
- name: Use Node.js
|
||||
uses: actions/setup-node@v6.4.0
|
||||
uses: actions/setup-node@v7.0.0
|
||||
with:
|
||||
node-version-file: '.node-version'
|
||||
cache: 'pnpm'
|
||||
|
||||
@@ -22,13 +22,13 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v6.0.3
|
||||
uses: actions/checkout@v7.0.0
|
||||
|
||||
- name: Setup pnpm
|
||||
uses: pnpm/action-setup@v6.0.9
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v6.4.0
|
||||
uses: actions/setup-node@v7.0.0
|
||||
with:
|
||||
node-version-file: '.node-version'
|
||||
cache: 'pnpm'
|
||||
@@ -48,7 +48,7 @@ jobs:
|
||||
CI: true
|
||||
|
||||
- name: Upload Coverage
|
||||
uses: codecov/codecov-action@v6
|
||||
uses: codecov/codecov-action@v7
|
||||
with:
|
||||
token: ${{ secrets.CODECOV_TOKEN }}
|
||||
files: ./packages/misskey-js/coverage/coverage-final.json
|
||||
|
||||
@@ -16,13 +16,13 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v6.0.3
|
||||
- uses: actions/checkout@v7.0.0
|
||||
with:
|
||||
submodules: true
|
||||
- name: Setup pnpm
|
||||
uses: pnpm/action-setup@v6.0.9
|
||||
- name: Use Node.js
|
||||
uses: actions/setup-node@v6.4.0
|
||||
uses: actions/setup-node@v7.0.0
|
||||
with:
|
||||
node-version-file: '.node-version'
|
||||
cache: 'pnpm'
|
||||
|
||||
@@ -17,13 +17,13 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v6.0.3
|
||||
- uses: actions/checkout@v7.0.0
|
||||
with:
|
||||
submodules: true
|
||||
- name: Setup pnpm
|
||||
uses: pnpm/action-setup@v6.0.9
|
||||
- name: Use Node.js
|
||||
uses: actions/setup-node@v6.4.0
|
||||
uses: actions/setup-node@v7.0.0
|
||||
with:
|
||||
node-version-file: '.node-version'
|
||||
cache: 'pnpm'
|
||||
|
||||
+3
-2
@@ -21,7 +21,7 @@
|
||||
### Client
|
||||
- 2025.4.0 以前の設定情報の移行処理が削除されました
|
||||
- 2025.4.0 から直接 2026.6.0 以上にアップデートする場合は設定が移行されませんので注意してください。移行したい場合は一度 2026.5.1 を経由してください。
|
||||
- Enhance: 画像ビューワーを独自実装に変更・動画プレイヤーを統合
|
||||
- Enhance: 画像ビューワーを刷新・動画プレイヤーを統合
|
||||
- 操作性の改善
|
||||
- ビューワーとMisskeyの各種機能との統合を強化
|
||||
- パフォーマンスの向上
|
||||
@@ -29,7 +29,7 @@
|
||||
- Fix: 幅が狭い画面で動画の再生が困難な問題を修正
|
||||
- Fix: 一部の画像のみセンシティブなとき、ビューワー内で画像を切り替えるとセンシティブな画像がそのまま表示される問題を修正
|
||||
- Fix: 一部の画像をビューワーで読み込んだ際に正しく表示されない問題を修正
|
||||
- Enhance: タイムラインの読み込みパフォーマンスを改善
|
||||
- Enhance: ファイルアップロード前にプレビューできるように
|
||||
- Enhance: タブがバックグラウンドの間は必要ない定期更新処理を停止するように
|
||||
- Fix: 「画像を新しいタブで開く」が機能しなくなっていた問題を修正
|
||||
- Fix: デバイスタイプをスマートフォンに固定している状態で画面幅が広いとき、画面左上のアイコンが表示されない問題を修正
|
||||
@@ -70,6 +70,7 @@
|
||||
- Fix: Sentry 使用環境下にて、外部送信リクエストへ `sentry-trace` / `baggage` ヘッダーが既定で付与されないように
|
||||
- Fix: フォロワー限定投稿へのリプライをホーム投稿に出来る問題を修正
|
||||
- Fix: ファイルをアップロードするAPIにて、処理終了後に一時ファイルが削除されないことがある問題を修正
|
||||
- Fix: 初期設定で作成したアカウント以外でアカウント作成APIが使用できない問題を修正
|
||||
|
||||
## 2026.6.0
|
||||
|
||||
|
||||
@@ -619,6 +619,8 @@ output: "Sortida"
|
||||
script: "Script"
|
||||
disablePagesScript: "Desactivar AiScript a les pàgines "
|
||||
updateRemoteUser: "Actualitzar la informació de l'usuari remot"
|
||||
unsetMfa: "Desactiva l'autenticació de dos factors"
|
||||
unsetMfaConfirm: "Voldries desactivar l'autenticació de dos factors?"
|
||||
unsetUserAvatar: "Desactiva l'avatar "
|
||||
unsetUserAvatarConfirm: "Segur que vols desactivar l'avatar?"
|
||||
unsetUserBanner: "Desactiva el bàner "
|
||||
@@ -1417,6 +1419,8 @@ addToEmojiPalette: "Afegeix al calaix d'emojis"
|
||||
emojiPaletteAlreadyAddedConfirm: "Aquest emoji ja està inclòs en aquest calaix d'emojis. Vols afegir-lo de nou?"
|
||||
append: "Afegeix al final"
|
||||
prepend: "Afegeix al principi"
|
||||
urlPreviewSensitiveList: "Llista d'URLs per restringir la visualització de miniatures"
|
||||
urlPreviewSensitiveListDescription: "Si separeu els termes amb un espai, s'interpretarà com una condició 'AND'; si els separeu amb un salt de línia, s'interpretarà com una condició 'OR'. Si envolupeu els termes amb barres obliqües, s'interpretarà com una expressió regular. Si es troba una coincidència, la miniatura ja no es mostrarà."
|
||||
_imageEditing:
|
||||
_vars:
|
||||
caption: "Títol de l'arxiu"
|
||||
@@ -2149,6 +2153,15 @@ _sensitiveMediaDetection:
|
||||
setSensitiveFlagAutomaticallyDescription: "Els resultats de la detecció interna seran desats, inclòs si aquesta opció es troba desactivada."
|
||||
analyzeVideos: "Activar anàlisis de vídeos "
|
||||
analyzeVideosDescription: "Analitzar els vídeos a més de les imatges. Això incrementarà lleugerament la càrrega del servidor."
|
||||
externalServiceInfo: "La detecció de mitjans sensibles s'ha delegat a un servei extern (sensitive-detector). Per utilitzar aquesta funció, cal configurar un servei sidecar separat i establir els detalls de connexió que es detallen a continuació. Si no es configuren els detalls de connexió, no es durà a terme cap detecció (el contingut es tractarà com a no sensible)."
|
||||
apiUrl: "URL per connectar-se al servei de verificació"
|
||||
apiUrlDescription: "L'URL base del servei de detector de sensibilitat (per exemple, http://localhost:3009). Si us connecteu a un servei en una xarxa privada, afegiu la xarxa de destinació a la configuració `allowedPrivateNetworks` del fitxer de configuració. Si utilitzeu un proxy, configureu també `proxyBypassHosts`. Si es deixa en blanc, no es realitzaran comprovacions de sensibilitat."
|
||||
apiKey: "Clau de l'API"
|
||||
apiKeyDescription: "Introduïu això si l'autenticació (token Bearer) està configurada al costat del servei d'autenticació. Si no està configurada, deixeu aquest camp en blanc."
|
||||
timeout: "Temps d'espera (mil·lisegons)"
|
||||
timeoutDescription: "1 Aquesta és la durada del temps mort per sol·licitud de validació."
|
||||
maxImagesPerRequest: "1 Nombre màxim d'imatges per sol·licitud"
|
||||
maxImagesPerRequestDescription: "1 Quan s'analitzen múltiples fotogrames, com en un vídeo, aquest és el nombre màxim d'imatges que es poden enviar en una única petició. Qualsevol imatge que superi aquest límit es dividirà en parts i s'enviarà seqüencialment. Assegureu-vos que aquest ajust no superi el valor de `maxParts` al costat del `sensitive-detector` (per defecte: 10). Si es supera aquest límit, totes les imatges d'aquest lot es consideraran no sensibles."
|
||||
_emailUnavailable:
|
||||
used: "Aquest correu electrònic ja s'està fent servir"
|
||||
format: "El format del correu electrònic és invàlid "
|
||||
@@ -2461,6 +2474,7 @@ _permissions:
|
||||
"read:admin:show-moderation-log": "Veure registre de moderació "
|
||||
"read:admin:show-user": "Veure informació privada de l'usuari "
|
||||
"write:admin:suspend-user": "Suspendre usuari"
|
||||
"write:admin:unset-mfa": "Desactiva l'autenticació de dos factors per a un usuari"
|
||||
"write:admin:unset-user-avatar": "Esborrar avatar d'usuari "
|
||||
"write:admin:unset-user-banner": "Esborrar bàner de l'usuari "
|
||||
"write:admin:unsuspend-user": "Treure la suspensió d'un usuari"
|
||||
@@ -2976,6 +2990,7 @@ _moderationLogTypes:
|
||||
createAvatarDecoration: "Decoració de l'avatar creada"
|
||||
updateAvatarDecoration: "S'ha actualitzat la decoració de l'avatar "
|
||||
deleteAvatarDecoration: "S'ha esborrat la decoració de l'avatar "
|
||||
unsetMfa: "Desactiva l'autenticació de dos factors per a l'usuari"
|
||||
unsetUserAvatar: "Esborrar l'avatar d'aquest usuari"
|
||||
unsetUserBanner: "Esborrar el bàner d'aquest usuari"
|
||||
createSystemWebhook: "Crear un SystemWebhook"
|
||||
|
||||
@@ -619,6 +619,8 @@ output: "Output"
|
||||
script: "Script"
|
||||
disablePagesScript: "Disable AiScript on Pages"
|
||||
updateRemoteUser: "Update remote user information"
|
||||
unsetMfa: "Reset two-factor authentication"
|
||||
unsetMfaConfirm: "Are you sure you want to reset two-factor authentication?"
|
||||
unsetUserAvatar: "Unset avatar"
|
||||
unsetUserAvatarConfirm: "Are you sure you want to unset the avatar?"
|
||||
unsetUserBanner: "Unset banner"
|
||||
@@ -1417,6 +1419,8 @@ addToEmojiPalette: "Add to emoji palette"
|
||||
emojiPaletteAlreadyAddedConfirm: "This emoji is already included in this emoji palette. Do you want to add it again?"
|
||||
append: "Append to end"
|
||||
prepend: "Append to beginning"
|
||||
urlPreviewSensitiveList: "URL to restrict thumbnail display"
|
||||
urlPreviewSensitiveListDescription: "Use spaces to specify AND conditions, and line breaks to specify OR conditions. Enclose text in slashes to use regular expressions. If a match is found, the thumbnail will be hidden."
|
||||
_imageEditing:
|
||||
_vars:
|
||||
caption: "File caption"
|
||||
@@ -2149,6 +2153,15 @@ _sensitiveMediaDetection:
|
||||
setSensitiveFlagAutomaticallyDescription: "The results of the internal detection will be retained even if this option is turned off."
|
||||
analyzeVideos: "Enable analysis of videos"
|
||||
analyzeVideosDescription: "Analyzes videos in addition to images. This will slightly increase the load on the server."
|
||||
externalServiceInfo: "The detection of sensitive media has been offloaded to an external service (sensitive-detector). To use this feature, you must set up a separate service and configure the connection details provided below. If no connection details are configured, no detection will be performed (it will be treated as non-sensitive)."
|
||||
apiUrl: "Detection service endpoint URL"
|
||||
apiUrlDescription: "The base URL for the sensitive-detector service (e.g., http://localhost:3009). If you are connecting to a service on a private network, please allow the target network in the allowedPrivateNetworks setting in the configuration file. If you are using a proxy, please also configure proxyBypassHosts. If left blank, sensitive media detection will not be performed."
|
||||
apiKey: "API key"
|
||||
apiKeyDescription: "Enter this if authentication (Bearer token) is configured on the detector service. If it is not configured, please leave it blank."
|
||||
timeout: "Timeout (Milliseconds)"
|
||||
timeoutDescription: "Timeout duration for each judgment request."
|
||||
maxImagesPerRequest: "Max images per request"
|
||||
maxImagesPerRequestDescription: "Maximum number of images that can be sent in a single request when processing multi-frame at once, such as videos. Any images exceeding this limit will be split and sent sequentially. Please ensure this is set to not exceed the maxParts setting (default: 10) on the detector service. If it exceeds this limit, all items in that chunk will be treated as non-sensitive."
|
||||
_emailUnavailable:
|
||||
used: "This email address is already being used"
|
||||
format: "The format of this email address is invalid"
|
||||
@@ -2461,6 +2474,7 @@ _permissions:
|
||||
"read:admin:show-moderation-log": "View moderation log"
|
||||
"read:admin:show-user": "View private user info"
|
||||
"write:admin:suspend-user": "Suspend user"
|
||||
"write:admin:unset-mfa": "Reset two-factor authentication for the user"
|
||||
"write:admin:unset-user-avatar": "Remove user avatar"
|
||||
"write:admin:unset-user-banner": "Remove user banner"
|
||||
"write:admin:unsuspend-user": "Unsuspend user"
|
||||
@@ -2976,6 +2990,7 @@ _moderationLogTypes:
|
||||
createAvatarDecoration: "Avatar decoration created"
|
||||
updateAvatarDecoration: "Avatar decoration updated"
|
||||
deleteAvatarDecoration: "Avatar decoration deleted"
|
||||
unsetMfa: "Reset two-factor authentication for the user"
|
||||
unsetUserAvatar: "User avatar unset"
|
||||
unsetUserBanner: "User banner unset"
|
||||
createSystemWebhook: "System Webhook created"
|
||||
|
||||
@@ -619,6 +619,8 @@ output: "Salida"
|
||||
script: "Script"
|
||||
disablePagesScript: "Deshabilitar AiScript en Páginas"
|
||||
updateRemoteUser: "Actualizar información de usuario remoto"
|
||||
unsetMfa: "Desactivar la autenticación de dos factores"
|
||||
unsetMfaConfirm: "¿Desea desactivar la autenticación de dos factores?"
|
||||
unsetUserAvatar: "Quitar avatar"
|
||||
unsetUserAvatarConfirm: "¿Confirmas que quieres quitar tu avatar?"
|
||||
unsetUserBanner: "Quitar banner"
|
||||
@@ -1417,6 +1419,8 @@ addToEmojiPalette: "Añadir a la paleta de emojis"
|
||||
emojiPaletteAlreadyAddedConfirm: "Este emoji ya está incluido en esta paleta de emojis. ¿Quieres volver a añadirlo?"
|
||||
append: "Añadir al final"
|
||||
prepend: "Añadir al principio"
|
||||
urlPreviewSensitiveList: "URL para restringir la visualización de miniaturas"
|
||||
urlPreviewSensitiveListDescription: "Si se separan con un espacio, se interpretará como una condición «AND»; si se separan con un salto de línea, se interpretará como una condición «OR». Si se escriben entre barras, se interpretarán como expresiones regulares. Si se encuentra una coincidencia, no se mostrará la miniatura."
|
||||
_imageEditing:
|
||||
_vars:
|
||||
caption: "Título del archivo"
|
||||
@@ -2149,6 +2153,15 @@ _sensitiveMediaDetection:
|
||||
setSensitiveFlagAutomaticallyDescription: "Los resultados de la detección interna pueden ser retenidos incluso si la opción está desactivada."
|
||||
analyzeVideos: "Habilitar el análisis de videos"
|
||||
analyzeVideosDescription: "Analizar videos en adición a las imágenes. Esto puede incrementar ligeramente la carga del servidor."
|
||||
externalServiceInfo: "La detección de contenidos sensibles se ha externalizado a un servicio externo (sensitive-detector). Para utilizar esta función, es necesario configurar por separado un servicio «sidecar» y establecer los datos de conexión que se indican a continuación. Si no se configuran los datos de conexión, no se realizará la detección (se tratará como contenido no sensible)."
|
||||
apiUrl: "URL de conexión al servicio de verificación"
|
||||
apiUrlDescription: "URL base del servicio «sensitive-detector» (por ejemplo: http://localhost:3009). Si te conectas a un servicio situado en una red privada, debes permitir la red de destino en el parámetro «allowedPrivateNetworks» del archivo de configuración. Si utilizas un proxy, configura también el parámetro «proxyBypassHosts». Si se deja en blanco, no se realizará la evaluación de datos sensibles."
|
||||
apiKey: "Clave API"
|
||||
apiKeyDescription: "Introduce este dato si se ha configurado la autenticación (token Bearer) en el servicio de validación. Si no se ha configurado, déjalo en blanco."
|
||||
timeout: "Tiempo de espera (milisegundos)"
|
||||
timeoutDescription: "Duración del tiempo de espera para cada solicitud de resolución."
|
||||
maxImagesPerRequest: "Número máximo de imágenes por solicitud"
|
||||
maxImagesPerRequestDescription: "Es el número máximo de imágenes que se pueden enviar en una sola solicitud al analizar varios fotogramas, como en un vídeo. Las imágenes que superen este límite se dividirán y se enviarán de forma secuencial. Configúralo de manera que no se supere el valor de «maxParts» de sensitive-detector (por defecto: 10). Si se supera este límite, todas las imágenes de ese fragmento se considerarán no sensibles."
|
||||
_emailUnavailable:
|
||||
used: "Ya fue usado"
|
||||
format: "Formato no válido."
|
||||
@@ -2461,6 +2474,7 @@ _permissions:
|
||||
"read:admin:show-moderation-log": "Ver log de moderación"
|
||||
"read:admin:show-user": "Ver información privada de usuario"
|
||||
"write:admin:suspend-user": "Suspender cuentas de usuario"
|
||||
"write:admin:unset-mfa": "Desactivar la autenticación de dos factores del usuario"
|
||||
"write:admin:unset-user-avatar": "Quitar avatares de usuario"
|
||||
"write:admin:unset-user-banner": "Quitar banner de usuarios"
|
||||
"write:admin:unsuspend-user": "Quitar suspensión de cuentas de usuario"
|
||||
@@ -2976,6 +2990,7 @@ _moderationLogTypes:
|
||||
createAvatarDecoration: "Decoración de avatar creada"
|
||||
updateAvatarDecoration: "Decoración de avatar actualizada"
|
||||
deleteAvatarDecoration: "Decoración de avatar eliminada"
|
||||
unsetMfa: "Desactivar la autenticación de dos factores del usuario"
|
||||
unsetUserAvatar: "Quitar decoración de avatar de este usuario"
|
||||
unsetUserBanner: "Quitar banner de este usuario"
|
||||
createSystemWebhook: "Crear un SystemWebhook"
|
||||
|
||||
@@ -619,6 +619,8 @@ output: "Output"
|
||||
script: "Script"
|
||||
disablePagesScript: "Disabilitare AiScript nelle pagine"
|
||||
updateRemoteUser: "Aggiorna dati dal profilo remoto"
|
||||
unsetMfa: "Rimuovere l'autenticazione a due fattori (2FA/MFA)"
|
||||
unsetMfaConfirm: "Vuoi davvero rimuovere l'autenticazione a due fattori?"
|
||||
unsetUserAvatar: "Rimozione foto profilo"
|
||||
unsetUserAvatarConfirm: "Vuoi davvero rimuovere la foto profilo?"
|
||||
unsetUserBanner: "Rimuovi intestazione profilo"
|
||||
@@ -1417,6 +1419,8 @@ addToEmojiPalette: "Aggiungi alla tavolozza emoji"
|
||||
emojiPaletteAlreadyAddedConfirm: "Questa emoji è già inclusa in nella tavolozza. Vuoi davvero aggiungerla?"
|
||||
append: "Accodare"
|
||||
prepend: "Anteporre"
|
||||
urlPreviewSensitiveList: "URL da impedire alla vista delle anteprime"
|
||||
urlPreviewSensitiveListDescription: "Separando con uno spazio si indica E, separando con una linea si indica O. Circondando con barre / si indica una Espressione Regolare.\nLe URL che coincidono con le indicazioni non verranno visualizzate."
|
||||
_imageEditing:
|
||||
_vars:
|
||||
caption: "Didascalia dell'immagine"
|
||||
@@ -2149,6 +2153,15 @@ _sensitiveMediaDetection:
|
||||
setSensitiveFlagAutomaticallyDescription: "Anche se questa impostazione è disattivata, il risultato della decisione viene conservato internamente."
|
||||
analyzeVideos: "Abilitazione dell'analisi video."
|
||||
analyzeVideosDescription: "Assicuratevi che vengano analizzati anche i video oltre alle immagini fisse. Il carico del server aumenterà leggermente."
|
||||
externalServiceInfo: "Abbiamo spostato esternamente il riconoscimento di media espliciti (sensitive-detector). Per usufruirne devi impostare un servizio separato e indicare di seguito la destinazione. Se non è impostata, non viene emesso alcun giudizio (contenuto NON esplicito)."
|
||||
apiUrl: "URL di connessione a Sensitive-Detector"
|
||||
apiUrlDescription: "L'URL di base del servizio (ad esempio http://localhost:3009). Collegandosi a una rete privata, autorizzare la rete nel parametro allowedPrivateNetworks nel file di configurazione.\nCollegandosi con un proxy, è anche necessario impostare proxyBypassHosts. Nel caso il campo sia vuoto, non vengono emessi giudizi di sensibilità."
|
||||
apiKey: "Chiave API"
|
||||
apiKeyDescription: "Indicare il token di autenticazione (Bearer token), soltanto se occorre, altrimenti lasciare vuoto."
|
||||
timeout: "Timeout (ms)"
|
||||
timeoutDescription: "Tempo limite per ogni richiesta"
|
||||
maxImagesPerRequest: "Numero massimo di media per ogni richiesta"
|
||||
maxImagesPerRequestDescription: "In caso ci siano più fotogrammi, come nei video, questo è il numero massimo di immagini da inviare in una richiesta. Oltre questo numero, il totale sarà diviso e inviato in sequenza.\nImpostare in modo che non superi il valore maxParts (default: 10) in Sensitive-Detector. Se viene superato, il media sarà considerato non esplicito."
|
||||
_emailUnavailable:
|
||||
used: "Email già in uso"
|
||||
format: "Formato email non valido"
|
||||
@@ -2461,6 +2474,7 @@ _permissions:
|
||||
"read:admin:show-moderation-log": "Vedere lo storico di moderazione"
|
||||
"read:admin:show-user": "Vedere le informazioni private dei profili"
|
||||
"write:admin:suspend-user": "Sospendere i profili"
|
||||
"write:admin:unset-mfa": "Può rimuovere l'autenticazione a due fattori (2FA/MFA)"
|
||||
"write:admin:unset-user-avatar": "Rimuovere la foto profilo dai profili"
|
||||
"write:admin:unset-user-banner": "Rimuovere l'immagine testata dai profili"
|
||||
"write:admin:unsuspend-user": "Rimuovere la sospensione ai profili"
|
||||
@@ -2976,6 +2990,7 @@ _moderationLogTypes:
|
||||
createAvatarDecoration: "Crea una decorazione della foto profilo"
|
||||
updateAvatarDecoration: "Modifica una decorazione della foto profilo"
|
||||
deleteAvatarDecoration: "Elimina una decorazione della foto profilo"
|
||||
unsetMfa: "Rimossa l'autenticazione a due fattori (2FA7MFA)"
|
||||
unsetUserAvatar: "Toglie una foto profilo"
|
||||
unsetUserBanner: "Toglie una immagine di intestazione profilo"
|
||||
createSystemWebhook: "Aggiunge un System Webhook"
|
||||
|
||||
+11
-1
@@ -1312,6 +1312,7 @@ noteOfThisUser: "このユーザーのノート一覧"
|
||||
clipNoteLimitExceeded: "これ以上このクリップにノートを追加できません。"
|
||||
performance: "パフォーマンス"
|
||||
modified: "変更あり"
|
||||
modifiedAt: "変更日時"
|
||||
discard: "破棄"
|
||||
thereAreNChanges: "{n}件の変更があります"
|
||||
signinWithPasskey: "パスキーでログイン"
|
||||
@@ -1421,6 +1422,8 @@ append: "末尾に追加"
|
||||
prepend: "先頭に追加"
|
||||
urlPreviewSensitiveList: "サムネイルの表示を制限するURL"
|
||||
urlPreviewSensitiveListDescription: "スペースで区切るとAND指定になり、改行で区切るとOR指定になります。スラッシュで囲むと正規表現になります。一致した場合、サムネイルが表示されなくなります。"
|
||||
latestBackupAt: "最終バックアップ"
|
||||
latestSyncAt: "最終同期"
|
||||
|
||||
_imageEditing:
|
||||
_vars:
|
||||
@@ -1589,7 +1592,9 @@ _preferencesProfile:
|
||||
useSyncBetweenDevicesOptionIfYouWantToSyncSetting: "複数のデバイスで同期したい設定項目が存在する場合は、個別に「複数のデバイスで同期」オプションを有効にしてください。"
|
||||
|
||||
_preferencesBackup:
|
||||
backupAndSync: "バックアップと同期"
|
||||
autoBackup: "自動バックアップ"
|
||||
autoBackup_description: "設定を自動でサーバーに保存し、いつでも復元できるようにします"
|
||||
restoreFromBackup: "バックアップから復元"
|
||||
noBackupsFoundTitle: "バックアップが見つかりませんでした"
|
||||
noBackupsFoundDescription: "自動で作成されたバックアップは見つかりませんでしたが、バックアップファイルを手動で保存している場合、それをインポートして復元することはできます。"
|
||||
@@ -1597,7 +1602,12 @@ _preferencesBackup:
|
||||
youNeedToNameYourProfileToEnableAutoBackup: "自動バックアップを有効にするにはプロファイル名の設定が必要です。"
|
||||
autoPreferencesBackupIsNotEnabledForThisDevice: "このデバイスで設定の自動バックアップは有効になっていません。"
|
||||
backupFound: "設定のバックアップが見つかりました"
|
||||
forceBackup: "設定の強制バックアップ"
|
||||
forceBackup: "今すぐバックアップ"
|
||||
autoSync: "デバイス間同期"
|
||||
autoSync_description: "サーバーに保存された設定を自動で取得し、別のデバイスでの変更と同期できるようにします"
|
||||
forceSync: "今すぐ同期"
|
||||
autoSyncAreYouSure: "デバイス間の同期をオンにしますか?"
|
||||
autoSyncAreYouSure_description: "通信量が増えるため、他のデバイスとこのプロファイルを共有する予定がない場合はオンにしないでください。"
|
||||
|
||||
_accountSettings:
|
||||
requireSigninToViewContents: "コンテンツの表示にログインを必須にする"
|
||||
|
||||
@@ -619,6 +619,8 @@ output: "출력"
|
||||
script: "스크립트"
|
||||
disablePagesScript: "Pages 에서 AiScript 를 사용하지 않음"
|
||||
updateRemoteUser: "리모트 유저 정보 갱신"
|
||||
unsetMfa: "2단계 인증 해제"
|
||||
unsetMfaConfirm: "2단계 인증을 해제하시겠습니까?"
|
||||
unsetUserAvatar: "아바타 제거"
|
||||
unsetUserAvatarConfirm: "아바타를 제거할까요?"
|
||||
unsetUserBanner: "배너 제거"
|
||||
@@ -1417,6 +1419,8 @@ addToEmojiPalette: "이모지 팔레트에 추가"
|
||||
emojiPaletteAlreadyAddedConfirm: "이 이모지는 이미 이 이모지 팔레트에 포함돼있습니다. 다시 추가하시겠습니까?"
|
||||
append: "맨뒤에 추가"
|
||||
prepend: "맨앞에 추가"
|
||||
urlPreviewSensitiveList: "썸네일 표시 제한 URL"
|
||||
urlPreviewSensitiveListDescription: "공백으로 구분하면 AND 지정으로 되고, 줄내림으로 구분하면 OR 지정으로 됩니다. 슬래시로 감싸면 정규 표현으로 됩니다. 일치한 경우에 썸네일이 표시되지 않게 됩니다."
|
||||
_imageEditing:
|
||||
_vars:
|
||||
caption: "파일 설명"
|
||||
@@ -2149,6 +2153,15 @@ _sensitiveMediaDetection:
|
||||
setSensitiveFlagAutomaticallyDescription: "이 설정을 해제해도 탐지 결과는 유지됩니다."
|
||||
analyzeVideos: "동영상도 같이 확인하기"
|
||||
analyzeVideosDescription: "사진 뿐만 아니라 동영상의 NSFW 여부도 탐지합니다. 서버의 부하를 약간 증가시킵니다."
|
||||
externalServiceInfo: "민감한 미디어 판정은 외부 서비스(sensitive-detector)로 분리됐습니다. 이 기능을 이용하려면 별도 사이드카 서비스를 설정하고, 아래의 접속 위치를 설정해야 합니다. 접속 위치가 설정되지 않은 경우에는 판정이 이루어지지 않습니다. (민감하지 않음 처리)"
|
||||
apiUrl: "판정 서비스의 접속 위치 URL"
|
||||
apiUrlDescription: "sensitive-detector 서비스의 베이스 URL(예시: http://localhost:3009). 프라이빗 네트워크상의 서비스에 접속하는 경우에는 설정 파일의 allowedPrivateNetworks로 접속 위치 네트워크를 허가해 주십시오. 프록시를 사용하고 있는 경우에는 proxyBypassHosts도 설정해 주십시오. 비어있으면 민감함 판정은 이루어지지 않습니다."
|
||||
apiKey: "API 키"
|
||||
apiKeyDescription: "판정 서비스 측에서 인증(Bearer 토큰)을 설정하고 있는 경우에 입력합니다. 설정하고 있지 않은 경우에는 빈칸으로 둬주십시오."
|
||||
timeout: "타임아웃 (밀리초)"
|
||||
timeoutDescription: "판정 요청 1회당 타임아웃 시간입니다."
|
||||
maxImagesPerRequest: "한 요청당 최대 이미지 수"
|
||||
maxImagesPerRequestDescription: "동영상 등 여러 프레임을 판정할 때, 한 번의 요청에 모아서 보내는 이미지의 최대 장수입니다. 이를 넘으면 분할해 순차적으로 송신됩니다. sensitive-detector 측의 maxParts 설정(기본: 10)을 남지 않도록 설정해 주십시오. 넘은 경우에는 그 청크는 전부 민감하지 않음 처리로 됩니다."
|
||||
_emailUnavailable:
|
||||
used: "이 메일 주소는 사용중입니다"
|
||||
format: "형식이 올바르지 않습니다"
|
||||
@@ -2461,6 +2474,7 @@ _permissions:
|
||||
"read:admin:show-moderation-log": "조정 기록 보기"
|
||||
"read:admin:show-user": "유저 개인정보 보기"
|
||||
"write:admin:suspend-user": "유저 정지하기"
|
||||
"write:admin:unset-mfa": "사용자의 2단계 인증 해제"
|
||||
"write:admin:unset-user-avatar": "유저 아바타 삭제하기"
|
||||
"write:admin:unset-user-banner": "유저 배너 삭제하기"
|
||||
"write:admin:unsuspend-user": "유저 정지 해제하기"
|
||||
@@ -2976,6 +2990,7 @@ _moderationLogTypes:
|
||||
createAvatarDecoration: "아바타 장식 만들기"
|
||||
updateAvatarDecoration: "아바타 장식 수정"
|
||||
deleteAvatarDecoration: "아바타 장식 삭제"
|
||||
unsetMfa: "사용자의 2단계 인증 해제"
|
||||
unsetUserAvatar: "유저 아바타 제거"
|
||||
unsetUserBanner: "유저 배너 제거"
|
||||
createSystemWebhook: "SystemWebhook을 생성"
|
||||
|
||||
+20
-5
@@ -57,7 +57,7 @@ deleteAndEditConfirm: "要删除该帖并重新编辑吗?该帖下的所有回
|
||||
addToList: "添加至列表"
|
||||
addToAntenna: "添加到天线"
|
||||
sendMessage: "发送消息"
|
||||
copyRSS: "复制RSS"
|
||||
copyRSS: "复制 RSS"
|
||||
copyUsername: "复制用户名"
|
||||
copyUserId: "复制用户 ID"
|
||||
copyNoteId: "复制帖子 ID"
|
||||
@@ -456,8 +456,8 @@ about: "关于"
|
||||
aboutMisskey: "关于 Misskey"
|
||||
administrator: "管理员"
|
||||
token: "Token (令牌)"
|
||||
2fa: "双因素认证"
|
||||
setupOf2fa: "设置双因素认证"
|
||||
2fa: "双重验证"
|
||||
setupOf2fa: "设置双重验证"
|
||||
totp: "验证器"
|
||||
totpDescription: "使用验证器输入一次性密码"
|
||||
moderator: "监察员"
|
||||
@@ -485,7 +485,7 @@ markAsReadAllNotifications: "将所有通知标为已读"
|
||||
markAsReadAllUnreadNotes: "将所有帖子标记为已读"
|
||||
markAsReadAllTalkMessages: "将所有私信标记为已读"
|
||||
help: "帮助"
|
||||
inputMessageHere: "在此键入信息"
|
||||
inputMessageHere: "在此输入信息"
|
||||
close: "关闭"
|
||||
invites: "邀请"
|
||||
members: "成员"
|
||||
@@ -619,6 +619,8 @@ output: "输出"
|
||||
script: "脚本"
|
||||
disablePagesScript: "禁用页面脚本"
|
||||
updateRemoteUser: "更新远程用户信息"
|
||||
unsetMfa: "解除双重验证"
|
||||
unsetMfaConfirm: "确认解除双重验证吗?"
|
||||
unsetUserAvatar: "清除头像"
|
||||
unsetUserAvatarConfirm: "要清除头像吗?"
|
||||
unsetUserBanner: "清除横幅"
|
||||
@@ -1417,6 +1419,8 @@ addToEmojiPalette: "添加至表情符号选择器"
|
||||
emojiPaletteAlreadyAddedConfirm: "此表情符号已存在于此表情符号选择器中。要再次添加吗?"
|
||||
append: "加到最后"
|
||||
prepend: "加到最前"
|
||||
urlPreviewSensitiveList: "限制显示缩略图的 URL"
|
||||
urlPreviewSensitiveListDescription: "AND 条件用空格分隔,OR 条件用换行符分隔,正则表达式用斜线包裹。成功匹配则不再显示缩略图。"
|
||||
_imageEditing:
|
||||
_vars:
|
||||
caption: "文件标题"
|
||||
@@ -2149,6 +2153,15 @@ _sensitiveMediaDetection:
|
||||
setSensitiveFlagAutomaticallyDescription: "即使关闭此配置,识别结果也会在内部保存。"
|
||||
analyzeVideos: "启用对视频的检测"
|
||||
analyzeVideosDescription: "除了静止图像之外,还对视频进行分析。服务器负载会略微增加。"
|
||||
externalServiceInfo: "检测敏感媒体已分离至外部服务 (sensitive-detector)。若要使用,需额外部署 Sidecar 服务,并设置下方的连接 URL。未设定时将不会进行检测(视为非敏感媒体)。"
|
||||
apiUrl: "检测服务的连接 URL"
|
||||
apiUrlDescription: "sensitive-detector 服务的 base URL(如:http://localhost:3009)。若是连接至部署在专用网络上的服务,请在配置文件中的 allowedPrivateNetworks 里允许目标网络。若是使用了代理,请一并设置 proxyBypassHosts。留空则不进行敏感媒体检测。"
|
||||
apiKey: "API 密钥"
|
||||
apiKeyDescription: "若服务端有设置验证(Bearer token)则填写,未设置则留空。"
|
||||
timeout: "超时(毫秒)"
|
||||
timeoutDescription: "此为单次检测请求的超时时长。"
|
||||
maxImagesPerRequest: "单次检测请求最大图像数量"
|
||||
maxImagesPerRequestDescription: "此为在检测动画等多帧图像时,单次请求中可发送的图像数量上限。超出此值时动画将被拆分并按序发送。请勿将此值设为超出 sensitive-detector 侧的 maxParts 的值(默认:10),否则对应的分块将全被视为非敏感媒体。"
|
||||
_emailUnavailable:
|
||||
used: "已经被使用过"
|
||||
format: "无效的格式"
|
||||
@@ -2461,6 +2474,7 @@ _permissions:
|
||||
"read:admin:show-moderation-log": "查看管理日志"
|
||||
"read:admin:show-user": "查看用户的非公开信息"
|
||||
"write:admin:suspend-user": "冻结用户"
|
||||
"write:admin:unset-mfa": "解除用户的双重验证"
|
||||
"write:admin:unset-user-avatar": "删除用户头像"
|
||||
"write:admin:unset-user-banner": "删除用户横幅"
|
||||
"write:admin:unsuspend-user": "解除用户冻结"
|
||||
@@ -2586,7 +2600,7 @@ _widgetOptions:
|
||||
_jobQueue:
|
||||
sound: "播放音效"
|
||||
_rss:
|
||||
url: "RSS feed 的 URL"
|
||||
url: "RSS 订阅源网址"
|
||||
refreshIntervalSec: "更新间隔(秒)"
|
||||
maxEntries: "最大显示个数"
|
||||
_rssTicker:
|
||||
@@ -2976,6 +2990,7 @@ _moderationLogTypes:
|
||||
createAvatarDecoration: "新建头像挂件"
|
||||
updateAvatarDecoration: "更新头像挂件"
|
||||
deleteAvatarDecoration: "删除头像挂件"
|
||||
unsetMfa: "解除用户的双重验证"
|
||||
unsetUserAvatar: "清除用户头像"
|
||||
unsetUserBanner: "清除用户横幅"
|
||||
createSystemWebhook: "新建了 SystemWebhook"
|
||||
|
||||
+2
-2
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "misskey",
|
||||
"version": "2026.7.0-beta.4",
|
||||
"version": "2026.7.0-beta.6",
|
||||
"codename": "nasubi",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
@@ -58,7 +58,7 @@
|
||||
"execa": "9.6.1",
|
||||
"ignore-walk": "9.0.0",
|
||||
"js-yaml": "5.2.1",
|
||||
"tar": "7.5.20"
|
||||
"tar": "7.5.21"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@eslint/js": "9.39.5",
|
||||
|
||||
@@ -64,7 +64,7 @@ test('throws when filtering leaves fewer than two heap snapshots per side', asyn
|
||||
})).toThrow('At least two samples per side are required');
|
||||
});
|
||||
|
||||
test('reports the difference of medians and leaves a paired-looking PSS delta uncoloured', async () => {
|
||||
test('omits a paired-looking PSS delta that stays within observed noise', async () => {
|
||||
const base = replacePssSamples(await loadFixture('base'), [290_000, 292_900, 295_800, 298_700, 301_600]);
|
||||
const head = replacePssSamples(await loadFixture('head'), [292_900, 296_300, 298_700, 301_600, 290_000]);
|
||||
|
||||
@@ -78,16 +78,10 @@ test('reports the difference of medians and leaves a paired-looking PSS delta un
|
||||
baseHeapSnapshotUrl: 'https://example.invalid/base',
|
||||
headHeapSnapshotUrl: 'https://example.invalid/head',
|
||||
});
|
||||
const row = findMetricRow(markdown, 'PSS');
|
||||
|
||||
expect(row).toContain('295.8 MB <br> ± 2.9 MB');
|
||||
expect(row).toContain('296.3 MB <br> ± 3.4 MB');
|
||||
expect(row).toContain('$\\text{+0.5 MB}$');
|
||||
expect(row).toContain('4.5 MB');
|
||||
expect(row.split('|')).toHaveLength(7);
|
||||
expect(row).not.toMatch(/within noise|increase|decrease|inconclusive/);
|
||||
expect(row).not.toContain('\\color{orange}');
|
||||
expect(findMetricRow(markdown, 'USS')).not.toContain('\\color{orange}');
|
||||
expect(markdown).not.toContain('| **PSS** |');
|
||||
expect(markdown).toContain('<small><i>Only metrics showing significant changes are displayed.</i></small>');
|
||||
expect(markdown).not.toContain('⚠️ **Warning**: Memory usage (PSS)');
|
||||
});
|
||||
|
||||
test('keeps the convergence warning without suppressing table colour or the PSS warning', async () => {
|
||||
|
||||
@@ -251,7 +251,7 @@ test('renders a directional encoded byte delta at the absolute threshold', async
|
||||
|
||||
const row = requireMetricRow(await renderReport(null, { base, head }), 'Encoded network');
|
||||
|
||||
expect(row).toContain('$\\color{orange}{\\text{+10 KB}}$');
|
||||
expect(row).toContain('$\\color{orange}{\\text{+10 KB}}$');
|
||||
});
|
||||
|
||||
test('colours absolute and relative deltas together when the row is significant', async () => {
|
||||
@@ -268,9 +268,9 @@ test('colours absolute and relative deltas together when the row is significant'
|
||||
|
||||
const row = requireMetricRow(await renderReport(null, { base, head }), 'Encoded network');
|
||||
|
||||
expect(row).toContain('100 MB <br> ± 0 B');
|
||||
expect(row).toContain('$\\color{orange}{\\text{+20 KB}}$<br>$\\color{orange}{\\text{+0\\\\%}}$');
|
||||
expect(row).toContain('| 0 B |');
|
||||
expect(row).toContain('100 MB <br> ± 0 B');
|
||||
expect(row).toContain('$\\color{orange}{\\text{+20 KB}}$<br>$\\color{orange}{\\text{+0\\\\%}}$');
|
||||
expect(row).toContain('| 0 B |');
|
||||
expect(row.split('|')).toHaveLength(7);
|
||||
});
|
||||
|
||||
|
||||
@@ -20,15 +20,15 @@ import {
|
||||
describe('formatBytes', () => {
|
||||
// 1024ではなく1000区切り。単位が2桁以上なら小数を落とす
|
||||
test.each([
|
||||
[0, '0 B'],
|
||||
[999, '999 B'],
|
||||
[1_000, '1 KB'],
|
||||
[1_500, '1.5 KB'],
|
||||
[15_000, '15 KB'],
|
||||
[1_234_567, '1.2 MB'],
|
||||
[12_345_678, '12 MB'],
|
||||
[1_500_000_000, '1.5 GB'],
|
||||
[1_500_000_000_000, '1,500 GB'],
|
||||
[0, '0 B'],
|
||||
[999, '999 B'],
|
||||
[1_000, '1 KB'],
|
||||
[1_500, '1.5 KB'],
|
||||
[15_000, '15 KB'],
|
||||
[1_234_567, '1.2 MB'],
|
||||
[12_345_678, '12 MB'],
|
||||
[1_500_000_000, '1.5 GB'],
|
||||
[1_500_000_000_000, '1,500 GB'],
|
||||
])('formats %i as %s', (input, expected) => {
|
||||
expect(formatBytes(input)).toBe(expected);
|
||||
});
|
||||
|
||||
@@ -45,7 +45,7 @@ describe('renderHeapSnapshotTable', () => {
|
||||
report([1_100_000, 1_200_000, 1_300_000]),
|
||||
));
|
||||
|
||||
expect(row).toContain('$\\text{+100 KB}$');
|
||||
expect(row).toContain('$\\text{+100 KB}$');
|
||||
expect(row).not.toContain('within noise');
|
||||
expect(row).not.toContain('\\color{orange}');
|
||||
});
|
||||
@@ -56,7 +56,7 @@ describe('renderHeapSnapshotTable', () => {
|
||||
report([1_200_000, 1_200_000, 1_200_000]),
|
||||
));
|
||||
|
||||
expect(row).toContain('$\\color{orange}{\\text{+200 KB}}$');
|
||||
expect(row).toContain('$\\color{orange}{\\text{+200 KB}}$');
|
||||
expect(row).toContain('$\\color{orange}{\\text{+20\\\\%}}$');
|
||||
expect(row).not.toContain('increase');
|
||||
});
|
||||
@@ -67,7 +67,7 @@ describe('renderHeapSnapshotTable', () => {
|
||||
report([1_050_000, 1_050_000, 1_050_000]),
|
||||
));
|
||||
|
||||
expect(row).toContain('$\\text{+50 KB}$<br>$\\text{+5\\\\%}$');
|
||||
expect(row).toContain('$\\text{+50 KB}$<br>$\\text{+5\\\\%}$');
|
||||
expect(row.split('|')[4]).not.toContain('\\color{');
|
||||
});
|
||||
|
||||
|
||||
@@ -33,7 +33,7 @@ describe('renderMetricComparisonTable', () => {
|
||||
expect(table.split('\n')).toStrictEqual([
|
||||
'| Metric | @ Base | @ Head | Δ | MAD |',
|
||||
'| --- | ---: | ---: | ---: | ---: |',
|
||||
'| <strong>Metric</strong> | 100 units <br> ± 0 units | 120 units <br> ± 0 units | $\\color{orange}{\\text{+20 units}}$<br>$\\color{orange}{\\text{+20\\\\%}}$ | 0 units |',
|
||||
'| <strong>Metric</strong> | 100 units <br> ± 0 units | 120 units <br> ± 0 units | $\\color{orange}{\\text{+20~units}}$<br>$\\color{orange}{\\text{+20\\\\%}}$ | 0 units |',
|
||||
]);
|
||||
});
|
||||
|
||||
@@ -52,7 +52,7 @@ describe('renderMetricComparisonTable', () => {
|
||||
expect(table.split('\n')).toStrictEqual([
|
||||
'| Metric | @ Base | @ Head | Δ | MAD |',
|
||||
'| --- | ---: | ---: | ---: | ---: |',
|
||||
'| <strong>Metric</strong> | 100 units | 120 units | $\\color{orange}{\\text{+20 units}}$ | 0 units |',
|
||||
'| <strong>Metric</strong> | 100 units | 120 units | $\\color{orange}{\\text{+20~units}}$ | 0 units |',
|
||||
'| | | | | |',
|
||||
]);
|
||||
});
|
||||
@@ -62,7 +62,7 @@ describe('renderMetricComparisonTable', () => {
|
||||
const head = samples(109, 109, 109);
|
||||
const table = renderMetricComparisonTable(base, head, [defaultRow]);
|
||||
|
||||
expect(table).toContain('$\\text{+9 units}$<br>$\\text{+9\\\\%}$');
|
||||
expect(table).toContain('$\\text{+9~units}$<br>$\\text{+9\\\\%}$');
|
||||
expect(table).not.toContain('\\color{');
|
||||
expect(renderMetricComparisonTable(base, head, [defaultRow], {
|
||||
onlySignificantChanges: true,
|
||||
@@ -85,7 +85,7 @@ describe('renderMetricComparisonTable', () => {
|
||||
{ onlySignificantChanges: true },
|
||||
);
|
||||
|
||||
expect(table).toContain('$\\color{orange}{\\text{+10 units}}$');
|
||||
expect(table).toContain('$\\color{orange}{\\text{+10~units}}$');
|
||||
expect(table).toContain('$\\color{orange}{\\text{+10\\\\%}}$');
|
||||
});
|
||||
|
||||
@@ -106,11 +106,11 @@ describe('renderMetricComparisonTable', () => {
|
||||
[defaultRow],
|
||||
);
|
||||
|
||||
expect(inside).toContain('$\\text{+9 units}$');
|
||||
expect(inside).toContain('$\\text{+9~units}$');
|
||||
expect(inside).not.toContain('\\color{');
|
||||
expect(boundary).toContain('$\\text{+30 units}$');
|
||||
expect(boundary).toContain('$\\text{+30~units}$');
|
||||
expect(boundary).not.toContain('\\color{');
|
||||
expect(outside).toContain('$\\color{orange}{\\text{+31 units}}$');
|
||||
expect(outside).toContain('$\\color{orange}{\\text{+31~units}}$');
|
||||
expect(outside).toContain('$\\color{orange}{\\text{+31\\\\%}}$');
|
||||
});
|
||||
|
||||
@@ -121,7 +121,7 @@ describe('renderMetricComparisonTable', () => {
|
||||
[defaultRow],
|
||||
);
|
||||
|
||||
expect(table).toContain('$\\color{green}{\\text{-20 units}}$');
|
||||
expect(table).toContain('$\\color{green}{\\text{-20~units}}$');
|
||||
expect(table).toContain('$\\color{green}{\\text{-16.7\\\\%}}$');
|
||||
});
|
||||
|
||||
@@ -132,7 +132,7 @@ describe('renderMetricComparisonTable', () => {
|
||||
[defaultRow],
|
||||
);
|
||||
|
||||
expect(table).toContain('$\\color{orange}{\\text{+20 units}}$<br>-');
|
||||
expect(table).toContain('$\\color{orange}{\\text{+20~units}}$<br>-');
|
||||
});
|
||||
|
||||
test('propagates the minimum sample contract', () => {
|
||||
|
||||
@@ -58,7 +58,7 @@
|
||||
"@fastify/http-proxy": "11.5.0",
|
||||
"@fastify/multipart": "10.1.0",
|
||||
"@fastify/otel": "0.20.1",
|
||||
"@fastify/static": "10.1.0",
|
||||
"@fastify/static": "10.1.2",
|
||||
"@kitajs/html": "4.2.13",
|
||||
"@misskey-dev/emoji-assets": "17.0.3",
|
||||
"@misskey-dev/emoji-data": "17.0.3",
|
||||
|
||||
@@ -5,9 +5,12 @@
|
||||
|
||||
import { bindThis } from '@/decorators.js';
|
||||
import { logManager } from './logging/logging-runtime.js';
|
||||
import type { LogLevel, LoggerContext, LogWriteInput } from './logging/types.js';
|
||||
import type { LogEntryInput, LogLevel, LoggerContext, LogWriteInput } from './logging/types.js';
|
||||
import type { Keyword } from 'color-convert';
|
||||
|
||||
// 旧APIのdataは表示用の任意値を受け取り、Errorや配列も既存呼び出しで使用されています。
|
||||
type LegacyData = Record<string, any> | null;
|
||||
|
||||
/**
|
||||
* ロガー名の階層と従来の公開APIを提供する薄い窓口です。
|
||||
* 出力条件の判断や整形はLogManagerとLogBackendへ委譲します。
|
||||
@@ -52,6 +55,15 @@ export default class Logger {
|
||||
});
|
||||
}
|
||||
|
||||
/** level別メソッドの構造化入力にlevelとLoggerのcontextを付けて渡します。 */
|
||||
@bindThis
|
||||
private logStructured(level: LogLevel, input: LogEntryInput): void {
|
||||
this.write({
|
||||
...input,
|
||||
level,
|
||||
});
|
||||
}
|
||||
|
||||
/** 構造化ログをLoggerのcontext付きでLogManagerへ渡します。 */
|
||||
@bindThis
|
||||
public write(input: LogWriteInput): void {
|
||||
@@ -62,24 +74,35 @@ export default class Logger {
|
||||
}
|
||||
|
||||
/** 処理を継続できない状況を記録します。 */
|
||||
public error(input: LogEntryInput): void;
|
||||
public error(error: Error, data?: LegacyData, important?: boolean): void;
|
||||
public error(message: string, data?: LegacyData, important?: boolean): void;
|
||||
public error(errorOrMessage: string | Error, data?: LegacyData, important?: boolean): void;
|
||||
@bindThis
|
||||
public error(x: string | Error, data?: Record<string, any> | null, important = false): void {
|
||||
public error(x: LogEntryInput | string | Error, data?: LegacyData, important = false): void {
|
||||
if (x instanceof Error) {
|
||||
// エラー本体も第2引数へ残し、従来どおりスタックなどを確認できるようにします。
|
||||
data = data ?? {};
|
||||
data.e = x;
|
||||
this.log('error', x.toString(), data, important, undefined, x);
|
||||
} else if (typeof x === 'object') {
|
||||
this.log('error', `${(x as any).message ?? (x as any).name ?? x}`, data, important);
|
||||
} else if (typeof x === 'string') {
|
||||
this.log('error', x, data, important);
|
||||
} else {
|
||||
this.log('error', `${x}`, data, important);
|
||||
this.logStructured('error', x);
|
||||
}
|
||||
}
|
||||
|
||||
/** 処理は継続できるものの、改善が必要な状況を記録します。 */
|
||||
public warn(input: LogEntryInput): void;
|
||||
public warn(message: string): void;
|
||||
public warn(message: string, data?: LegacyData, important?: boolean): void;
|
||||
@bindThis
|
||||
public warn(message: string, data?: Record<string, any> | null, important = false): void {
|
||||
this.log('warn', message, data, important);
|
||||
public warn(inputOrMessage: LogEntryInput | string, data?: LegacyData, important = false): void {
|
||||
if (typeof inputOrMessage === 'string') {
|
||||
this.log('warn', inputOrMessage, data, important);
|
||||
} else {
|
||||
this.logStructured('warn', inputOrMessage);
|
||||
}
|
||||
}
|
||||
|
||||
/** 処理が成功したことを、従来のDONE表示で記録します。 */
|
||||
@@ -89,14 +112,40 @@ export default class Logger {
|
||||
}
|
||||
|
||||
/** 開発者向けの調査情報を記録します。 */
|
||||
public debug(input: LogEntryInput): void;
|
||||
public debug(message: string): void;
|
||||
public debug(message: string, data?: LegacyData, important?: boolean): void;
|
||||
@bindThis
|
||||
public debug(message: string, data?: Record<string, any> | null, important = false): void {
|
||||
this.log('debug', message, data, important);
|
||||
public debug(inputOrMessage: LogEntryInput | string, data?: LegacyData, important = false): void {
|
||||
if (typeof inputOrMessage === 'string') {
|
||||
this.log('debug', inputOrMessage, data, important);
|
||||
} else {
|
||||
this.logStructured('debug', inputOrMessage);
|
||||
}
|
||||
}
|
||||
|
||||
/** 通常の動作状況を記録します。 */
|
||||
public info(input: LogEntryInput): void;
|
||||
public info(message: string): void;
|
||||
public info(message: string, data?: LegacyData, important?: boolean): void;
|
||||
@bindThis
|
||||
public info(message: string, data?: Record<string, any> | null, important = false): void {
|
||||
this.log('info', message, data, important);
|
||||
public info(inputOrMessage: LogEntryInput | string, data?: LegacyData, important = false): void {
|
||||
if (typeof inputOrMessage === 'string') {
|
||||
this.log('info', inputOrMessage, data, important);
|
||||
} else {
|
||||
this.logStructured('info', inputOrMessage);
|
||||
}
|
||||
}
|
||||
|
||||
/** 致命的な状況を構造化ログとして記録します。 */
|
||||
public fatal(input: LogEntryInput): void;
|
||||
public fatal(message: string): void;
|
||||
@bindThis
|
||||
public fatal(inputOrMessage: LogEntryInput | string): void {
|
||||
if (typeof inputOrMessage === 'string') {
|
||||
this.logStructured('fatal', { message: inputOrMessage });
|
||||
} else {
|
||||
this.logStructured('fatal', inputOrMessage);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -52,15 +52,19 @@ export type LogTraceContext = {
|
||||
/** LogManagerが出力直前に呼び出すTrace Context取得処理です。 */
|
||||
export type LogTraceContextProvider = () => LogTraceContext | undefined;
|
||||
|
||||
/** ロガーの呼び出し側が構造化ログとして指定する入力です。 */
|
||||
export type LogWriteInput = {
|
||||
readonly level: LogLevel;
|
||||
/** level別のLoggerメソッドが受け取る構造化ログの共通入力です。 */
|
||||
export type LogEntryInput = {
|
||||
readonly message: string;
|
||||
readonly eventName?: string;
|
||||
readonly attributes?: Readonly<Record<string, unknown>>;
|
||||
readonly error?: unknown;
|
||||
};
|
||||
|
||||
/** 動的なlevelを含めてLogManagerへ渡す構造化ログの入力です。 */
|
||||
export type LogWriteInput = LogEntryInput & {
|
||||
readonly level: LogLevel;
|
||||
};
|
||||
|
||||
/**
|
||||
* ロガー名を構成する一要素です。
|
||||
* 色は見やすい形式での表示だけに使い、ログの意味には影響させません。
|
||||
|
||||
@@ -7,6 +7,7 @@ import { Inject, Injectable } from '@nestjs/common';
|
||||
import { Endpoint } from '@/server/api/endpoint-base.js';
|
||||
import type { MiMeta, UsersRepository } from '@/models/_.js';
|
||||
import { SignupService } from '@/core/SignupService.js';
|
||||
import { RoleService } from '@/core/RoleService.js';
|
||||
import { UserEntityService } from '@/core/entities/UserEntityService.js';
|
||||
import { localUsernameSchema, passwordSchema } from '@/models/User.js';
|
||||
import { DI } from '@/di-symbols.js';
|
||||
@@ -77,6 +78,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
||||
|
||||
private userEntityService: UserEntityService,
|
||||
private signupService: SignupService,
|
||||
private roleService: RoleService,
|
||||
) {
|
||||
super(meta, paramDef, async (ps, _me, token) => {
|
||||
const me = _me ? await this.usersRepository.findOneByOrFail({ id: _me.id }) : null;
|
||||
@@ -93,7 +95,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
||||
// 初期パスワードが設定されていないのに初期パスワードが入力された場合
|
||||
throw new ApiError(meta.errors.wrongInitialPassword);
|
||||
}
|
||||
} else if ((this.serverSettings.rootUserId != null && (this.serverSettings.rootUserId !== me?.id)) || token !== null) {
|
||||
} else if (token !== null || !(await this.roleService.isAdministrator(me))) {
|
||||
// 初回セットアップではなく、管理者でない場合 or 外部トークンを使用している場合
|
||||
throw new ApiError(meta.errors.accessDenied);
|
||||
}
|
||||
|
||||
@@ -76,6 +76,91 @@ describe('Logger', () => {
|
||||
}));
|
||||
});
|
||||
|
||||
test('supports structured input through every level-specific method', () => {
|
||||
const logger = new Logger('root').createSubLogger('child');
|
||||
const error = new Error('broken');
|
||||
const input = {
|
||||
eventName: 'example.failed',
|
||||
message: 'failed',
|
||||
attributes: { id: 'id' },
|
||||
error,
|
||||
};
|
||||
|
||||
logger.debug(input);
|
||||
logger.info(input);
|
||||
logger.warn(input);
|
||||
logger.error(input);
|
||||
logger.fatal(input);
|
||||
|
||||
expect(mocks.write.mock.calls.map(([entry]) => entry.level)).toEqual([
|
||||
'debug',
|
||||
'info',
|
||||
'warn',
|
||||
'error',
|
||||
'fatal',
|
||||
]);
|
||||
for (const [entry] of mocks.write.mock.calls) {
|
||||
expect(entry).toMatchObject({
|
||||
...input,
|
||||
context: [
|
||||
{ name: 'root', color: undefined },
|
||||
{ name: 'child', color: undefined },
|
||||
],
|
||||
});
|
||||
expect(entry).not.toHaveProperty('compatibility');
|
||||
}
|
||||
});
|
||||
|
||||
test('level-specific methods own the level even for runtime-invalid input', () => {
|
||||
const logger = new Logger('root');
|
||||
|
||||
// @ts-expect-error level is selected by the method rather than the input object
|
||||
logger.warn({ level: 'error', message: 'warning' });
|
||||
|
||||
expect(mocks.write.mock.calls[0][0]).toMatchObject({
|
||||
level: 'warn',
|
||||
message: 'warning',
|
||||
});
|
||||
});
|
||||
|
||||
test('preserves the legacy string signatures for non-error levels', () => {
|
||||
const logger = new Logger('root');
|
||||
logger.debug('debug', { source: 'debug' }, true);
|
||||
logger.info('info', null, true);
|
||||
logger.warn('warn', { source: 'warn' });
|
||||
|
||||
expect(mocks.write.mock.calls.map(([entry]) => entry.compatibility)).toEqual([
|
||||
{ legacyLevel: undefined, important: true, data: { source: 'debug' } },
|
||||
{ legacyLevel: undefined, important: true, data: null },
|
||||
{ legacyLevel: undefined, important: false, data: { source: 'warn' } },
|
||||
]);
|
||||
});
|
||||
|
||||
test('records a fatal string through the structured API', () => {
|
||||
new Logger('root').fatal('fatal message');
|
||||
|
||||
expect(mocks.write).toHaveBeenCalledWith({
|
||||
level: 'fatal',
|
||||
message: 'fatal message',
|
||||
context: [{ name: 'root', color: undefined }],
|
||||
});
|
||||
});
|
||||
|
||||
test('preserves the legacy error string signature', () => {
|
||||
new Logger('root').error('failed', { requestId: 'request' }, true);
|
||||
|
||||
expect(mocks.write).toHaveBeenCalledWith({
|
||||
level: 'error',
|
||||
message: 'failed',
|
||||
context: [{ name: 'root', color: undefined }],
|
||||
compatibility: {
|
||||
legacyLevel: undefined,
|
||||
important: true,
|
||||
data: { requestId: 'request' },
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
test('uses Error.toString and adds the Error to existing data', () => {
|
||||
const logger = new Logger('root');
|
||||
const error = new TypeError('broken');
|
||||
|
||||
-5
@@ -13,8 +13,3 @@ declare const _PERF_PREFIX_: string;
|
||||
|
||||
// for dev-mode
|
||||
declare const _LANGS_FULL_: string[][];
|
||||
|
||||
// TagCanvas
|
||||
interface Window {
|
||||
TagCanvas: any;
|
||||
}
|
||||
|
||||
-6
@@ -14,9 +14,3 @@ declare const _PERF_PREFIX_: string;
|
||||
|
||||
// for dev-mode
|
||||
declare const _LANGS_FULL_: string[][];
|
||||
|
||||
// TagCanvas
|
||||
interface Window {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
TagCanvas: any;
|
||||
}
|
||||
|
||||
@@ -458,7 +458,6 @@ function toStories(component: string): Promise<string> {
|
||||
globSync('src/components/MkSignupServerRules.vue'),
|
||||
globSync('src/components/MkUserSetupDialog.vue'),
|
||||
globSync('src/components/MkUserSetupDialog.*.vue'),
|
||||
globSync('src/components/MkImgPreviewDialog.vue'),
|
||||
globSync('src/components/MkInstanceCardMini.vue'),
|
||||
globSync('src/components/MkInviteCode.vue'),
|
||||
globSync('src/components/MkTagItem.vue'),
|
||||
|
||||
Vendored
-5
@@ -13,8 +13,3 @@ declare const _PERF_PREFIX_: string;
|
||||
|
||||
// for dev-mode
|
||||
declare const _LANGS_FULL_: string[][];
|
||||
|
||||
// TagCanvas
|
||||
interface Window {
|
||||
TagCanvas: any;
|
||||
}
|
||||
|
||||
-21
File diff suppressed because one or more lines are too long
@@ -0,0 +1,52 @@
|
||||
# Preferences system
|
||||
|
||||
ユーザーの環境設定を管理するシステム。
|
||||
|
||||
## 指針
|
||||
|
||||
実装上のミスで、**設定値が意図せず失われる(古い値で上書きされる)ことが絶対にあってはならない。**
|
||||
|
||||
設定値が失われる、考えられるシナリオの例:
|
||||
|
||||
- 複数のタブでMisskeyを開いていて、タブAで設定を編集した後、タブBを開いたところ、タブBの古い状態の内容で設定が保存され、タブAで行った編集が巻き戻ってしまった。
|
||||
- プロファイルの同期機能をオンにしたところ、サーバーに保存されていた古い設定でローカルが上書きされてしまった。
|
||||
|
||||
上記のシナリオが絶対に発生しない設計・実装にしなければならない。
|
||||
|
||||
上記以外にも考えられるシナリオがあれば、必ず適切な対処を行う設計・実装にしなければならない。
|
||||
|
||||
## 仕様
|
||||
|
||||
### タブ間同期
|
||||
|
||||
ブラウザで複数タブを開いているとき、あるタブで変更した設定が他のタブでも反映されるようにし、UXを向上させ、古い設定情報が上書き保存されることを防ぐ。
|
||||
|
||||
### 自動バックアップ(cloudBackup)
|
||||
|
||||
定期的、または特定のタイミングなどで、プロファイルをサーバーに自動的にアップロードする。
|
||||
|
||||
### デバイス間同期(cloudSync)
|
||||
|
||||
定期的、または特定のタイミングなどで、サーバーにアップロードされたプロファイルをダウンロードし、ローカルに適用する。
|
||||
|
||||
### 設定項目: デバイス間で同期(syncBetweenDevices, ValueMeta.sync)
|
||||
|
||||
デバイス間・プロファイル横断でその設定項目の設定値を同期する。
|
||||
|
||||
当オプションを有効にするかどうかは設定項目ごとに設定可能。
|
||||
|
||||
## ユーザーストーリー(ユースケース)
|
||||
|
||||
### 同じプロファイルを複数のデバイスで使いたい(同期したい)
|
||||
|
||||
autoBackupとautoSyncをオンにする。
|
||||
|
||||
### (新しいデバイスなどで)既存のプロファイルを継承した新しいプロファイルを作りたい
|
||||
|
||||
継承したいプロファイルをバックアップから復元した後、プロファイルの名前を変える。
|
||||
|
||||
## メモ
|
||||
|
||||
autoBackupとautoSyncの違いがユーザーにとって分かりにくい可能性があるので、機能を一本化するか、個別にオンオフを切り替えられるのではなく「自動バックアップ:『しない/有効/有効+同期』」みたいな選択方式にするなどがいいかもしれない?
|
||||
|
||||
autoBackupはするがautoSyncしたくないケースはあまりないと思うが、一本化すると複数のデバイスで同一のプロファイルを共有しない人(大多数だと思われる)にとっては無駄に通信量が増えるだけになる
|
||||
@@ -22,6 +22,7 @@
|
||||
"@mcaptcha/core-glue": "0.1.0-alpha-5",
|
||||
"@misskey-dev/browser-image-resizer": "2024.1.0",
|
||||
"@misskey-dev/emoji-data": "17.0.3",
|
||||
"@misskey-dev/tagcanvas-es": "0.1.2",
|
||||
"@sentry/vue": "10.65.0",
|
||||
"@simplewebauthn/browser": "13.3.0",
|
||||
"@syuilo/aiscript": "1.2.1",
|
||||
|
||||
@@ -1,40 +0,0 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: syuilo and misskey-project
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import type { StoryObj } from '@storybook/vue3';
|
||||
import { file } from '../../.storybook/fakes.js';
|
||||
import MkImgPreviewDialog from './MkImgPreviewDialog.vue';
|
||||
export const Default = {
|
||||
render(args) {
|
||||
return {
|
||||
components: {
|
||||
MkImgPreviewDialog,
|
||||
},
|
||||
setup() {
|
||||
return {
|
||||
args,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
props() {
|
||||
return {
|
||||
...this.args,
|
||||
};
|
||||
},
|
||||
},
|
||||
template: '<MkImgPreviewDialog v-bind="props" />',
|
||||
};
|
||||
},
|
||||
args: {
|
||||
file: file(),
|
||||
},
|
||||
parameters: {
|
||||
chromatic: {
|
||||
// NOTE: ロードが終わるまで待つ
|
||||
delay: 3000,
|
||||
},
|
||||
layout: 'centered',
|
||||
},
|
||||
} satisfies StoryObj<typeof MkImgPreviewDialog>;
|
||||
@@ -1,63 +0,0 @@
|
||||
<!--
|
||||
SPDX-FileCopyrightText: syuilo and misskey-project
|
||||
SPDX-License-Identifier: AGPL-3.0-only
|
||||
-->
|
||||
|
||||
<template>
|
||||
<MkModalWindow
|
||||
ref="modal"
|
||||
:width="1800"
|
||||
:height="900"
|
||||
@close="close"
|
||||
@esc="close"
|
||||
@click="close"
|
||||
@closed="emit('closed')"
|
||||
>
|
||||
<template #header>{{ file.name }}</template>
|
||||
<div :class="$style.container">
|
||||
<img :src="file.url" :alt="file.comment || file.name" :class="$style.img"/>
|
||||
</div>
|
||||
</MkModalWindow>
|
||||
</template>
|
||||
<script lang="ts" setup>
|
||||
import { ref } from 'vue';
|
||||
import MkModalWindow from './MkModalWindow.vue';
|
||||
import type * as Misskey from 'misskey-js';
|
||||
|
||||
defineProps<{
|
||||
file: Misskey.entities.DriveFile;
|
||||
}>();
|
||||
|
||||
const emit = defineEmits<{
|
||||
(ev: 'closed'): void;
|
||||
}>();
|
||||
|
||||
const modal = ref<typeof MkModalWindow | null>(null);
|
||||
|
||||
function close() {
|
||||
modal.value?.close();
|
||||
}
|
||||
|
||||
</script>
|
||||
<style lang="scss" module>
|
||||
.container {
|
||||
box-sizing: border-box;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
min-height: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
overflow: hidden;
|
||||
|
||||
background-color: var(--MI_THEME-bg);
|
||||
background-size: auto auto;
|
||||
background-image: repeating-linear-gradient(135deg, transparent, transparent 6px, var(--MI_THEME-panel) 6px, var(--MI_THEME-panel) 12px);
|
||||
}
|
||||
|
||||
.img {
|
||||
width: 100%;
|
||||
max-height: 100%;
|
||||
object-fit: contain;
|
||||
}
|
||||
</style>
|
||||
@@ -16,7 +16,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||
@pointerup.passive="onPointerup"
|
||||
@pointercancel.passive="cancelPointerGesture"
|
||||
@touchstart.passive="onTouchstart"
|
||||
@touchmove.passive="onTouchmove"
|
||||
@touchmove="onTouchmove"
|
||||
@touchcancel.passive="cancelPointerGesture"
|
||||
@contextmenu="cancelPointerGesture"
|
||||
@wheel="onWheel"
|
||||
@@ -721,6 +721,13 @@ function onTouchstart(ev: TouchEvent) {
|
||||
|
||||
function onTouchmove(ev: TouchEvent) {
|
||||
doubleTapDetector.onTouchmove(ev);
|
||||
|
||||
// スワイプ操作中は、ブラウザがタッチを慣性スクロールとして認識するのを防ぐためにpreventDefaultする必要がある
|
||||
// touch-action: noneが指定されているが、それだけではブラウザによっては慣性スクロールが発生した扱いになり、
|
||||
// その後のタップが慣性スクロールを止めるためのタップという扱いで握りつぶされてしまうことがあるので必要
|
||||
if (isVerticalSwiping || isHorizontalSwiping || isZooming.value || pointerEventCache.size > 1) {
|
||||
ev.preventDefault();
|
||||
}
|
||||
}
|
||||
|
||||
//#region inertia
|
||||
|
||||
@@ -48,13 +48,14 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref, watch, nextTick, onBeforeUnmount, onMounted } from 'vue';
|
||||
import { ref, watch, nextTick, onBeforeUnmount, onMounted, useTemplateRef } from 'vue';
|
||||
import XItem from './MkLightbox.item.vue';
|
||||
import type { Content } from './MkLightbox.item.vue';
|
||||
import type { Keymap } from '@/utility/hotkey.js';
|
||||
import * as os from '@/os.js';
|
||||
import { prefer } from '@/preferences.js';
|
||||
import { isTouchUsing } from '@/utility/touch.js';
|
||||
import { focusTrap } from '@/utility/focus-trap.js';
|
||||
|
||||
const props = withDefaults(defineProps<{
|
||||
defaultIndex?: number;
|
||||
@@ -66,6 +67,7 @@ const emit = defineEmits<{
|
||||
(ev: 'closed'): void;
|
||||
}>();
|
||||
|
||||
const rootEl = useTemplateRef('rootEl');
|
||||
const activatedIndexes = ref(new Set<number>());
|
||||
const items = new Map<number, InstanceType<typeof XItem> | null>();
|
||||
const currentIndex = ref(props.defaultIndex ?? 0);
|
||||
@@ -192,7 +194,22 @@ function onPopState() {
|
||||
}
|
||||
}
|
||||
|
||||
let releaseFocusTrap: (() => void) | null = null;
|
||||
|
||||
onMounted(() => {
|
||||
watch([showing], ([showing]) => {
|
||||
if (showing === true) {
|
||||
if (rootEl.value != null) {
|
||||
const { release } = focusTrap(rootEl.value);
|
||||
|
||||
releaseFocusTrap = release;
|
||||
rootEl.value.focus();
|
||||
}
|
||||
} else {
|
||||
releaseFocusTrap?.();
|
||||
}
|
||||
}, { immediate: true });
|
||||
|
||||
window.history.pushState(null, '', '#pswp');
|
||||
window.addEventListener('popstate', onPopState);
|
||||
});
|
||||
|
||||
@@ -137,8 +137,8 @@ async function openGallery(id?: string) {
|
||||
type: media.type.startsWith('video') ? 'video' : 'image',
|
||||
url: media.url,
|
||||
thumbnailUrl: media.thumbnailUrl,
|
||||
width: media.properties.width ?? 0,
|
||||
height: media.properties.height ?? 0,
|
||||
width: media.properties.width,
|
||||
height: media.properties.height,
|
||||
filename: media.name,
|
||||
file: media,
|
||||
sourceElement: getElementByMarker(`${markerId}:${media.id}`),
|
||||
|
||||
@@ -191,6 +191,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||
role="menuitem"
|
||||
tabindex="0"
|
||||
:class="['_button', $style.item, { [$style.danger]: item.danger, [$style.active]: unref(item.active) }]"
|
||||
:disabled="unref(item.disabled)"
|
||||
@click.prevent="unref(item.active) ? close(false) : clicked(item.action, $event)"
|
||||
@mouseenter.passive="onItemMouseEnter"
|
||||
@mouseleave.passive="onItemMouseLeave"
|
||||
@@ -629,6 +630,7 @@ function guardMouseMove(ev: MouseEvent) {
|
||||
box-sizing: border-box;
|
||||
max-width: 100vw;
|
||||
min-width: 200px;
|
||||
width: max-content;
|
||||
overflow: auto;
|
||||
overscroll-behavior: contain;
|
||||
|
||||
@@ -701,6 +703,7 @@ function guardMouseMove(ev: MouseEvent) {
|
||||
|
||||
&:disabled {
|
||||
cursor: not-allowed;
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
&.danger {
|
||||
|
||||
@@ -153,6 +153,7 @@ function showFileMenu(file: Misskey.entities.DriveFile, ev: PointerEvent | Keybo
|
||||
if (menuShowing) return;
|
||||
|
||||
const isImage = file.type.startsWith('image/');
|
||||
const isVideo = file.type.startsWith('video/');
|
||||
|
||||
const menuItems: MenuItem[] = [];
|
||||
|
||||
@@ -170,13 +171,25 @@ function showFileMenu(file: Misskey.entities.DriveFile, ev: PointerEvent | Keybo
|
||||
action: () => { describe(file); },
|
||||
});
|
||||
|
||||
if (isImage) {
|
||||
if (isImage || isVideo) {
|
||||
menuItems.push({
|
||||
text: i18n.ts.preview,
|
||||
icon: 'ti ti-photo-search',
|
||||
action: async () => {
|
||||
const { dispose } = await os.popupAsyncWithDialog(import('@/components/MkImgPreviewDialog.vue').then(x => x.default), {
|
||||
file: file,
|
||||
const constents = props.modelValue.filter(item => item.type.startsWith('image') || item.type.startsWith('video')).map(item => ({
|
||||
id: item.id,
|
||||
type: item.type.startsWith('video') ? 'video' as const : 'image' as const,
|
||||
url: item.url,
|
||||
thumbnailUrl: item.thumbnailUrl,
|
||||
width: item.properties.width,
|
||||
height: item.properties.height,
|
||||
filename: item.name,
|
||||
file: item,
|
||||
//sourceElement: TODO
|
||||
}));
|
||||
const { dispose } = await os.popupAsyncWithDialog(import('@/components/MkLightbox.vue').then(x => x.default), {
|
||||
defaultIndex: constents.findIndex(content => content.id === file.id),
|
||||
contents: constents,
|
||||
}, {
|
||||
closed: () => dispose(),
|
||||
});
|
||||
|
||||
@@ -19,15 +19,18 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||
<div :class="$style.newBg2"></div>
|
||||
<button class="_button" :class="$style.newButton" @click="releaseQueue()"><i class="ti ti-circle-arrow-up"></i> {{ i18n.ts.newNote }}</button>
|
||||
</div>
|
||||
<div :class="$style.notes">
|
||||
<component
|
||||
:is="prefer.s.animation ? MkStreamingTimelineItem : 'div'"
|
||||
v-for="(note, i) in paginator.items.value"
|
||||
v-bind="prefer.s.animation ? { animatingIn: note._shouldAnimateIn_, animatingOut: note._shouldAnimateOut_ } : {}"
|
||||
:key="note.id"
|
||||
:data-scroll-anchor="note.id"
|
||||
>
|
||||
<div v-if="i > 0 && isSeparatorNeeded(paginator.items.value[i -1].createdAt, note.createdAt) && paginator.items.value[i -1]._shouldAnimateOut_ !== true">
|
||||
<component
|
||||
:is="prefer.s.animation ? TransitionGroup : 'div'"
|
||||
:class="$style.notes"
|
||||
:enterActiveClass="$style.transition_x_enterActive"
|
||||
:leaveActiveClass="$style.transition_x_leaveActive"
|
||||
:enterFromClass="$style.transition_x_enterFrom"
|
||||
:leaveToClass="$style.transition_x_leaveTo"
|
||||
:moveClass="$style.transition_x_move"
|
||||
tag="div"
|
||||
>
|
||||
<template v-for="(note, i) in paginator.items.value" :key="note.id">
|
||||
<div v-if="i > 0 && isSeparatorNeeded(paginator.items.value[i -1].createdAt, note.createdAt)" :data-scroll-anchor="note.id">
|
||||
<div :class="$style.date">
|
||||
<span><i class="ti ti-chevron-up"></i> {{ getSeparatorInfo(paginator.items.value[i -1].createdAt, note.createdAt)?.prevText }}</span>
|
||||
<span style="height: 1em; width: 1px; background: var(--MI_THEME-divider);"></span>
|
||||
@@ -35,15 +38,15 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||
</div>
|
||||
<MkNote :class="$style.note" :note="note" :withHardMute="true"/>
|
||||
</div>
|
||||
<div v-else-if="note._shouldInsertAd_">
|
||||
<div v-else-if="note._shouldInsertAd_" :data-scroll-anchor="note.id">
|
||||
<MkNote :class="$style.note" :note="note" :withHardMute="true"/>
|
||||
<div :class="$style.ad">
|
||||
<MkAd :preferForms="['horizontal', 'horizontal-big']"/>
|
||||
</div>
|
||||
</div>
|
||||
<MkNote v-else :class="$style.note" :note="note" :withHardMute="true"/>
|
||||
</component>
|
||||
</div>
|
||||
<MkNote v-else :class="$style.note" :note="note" :withHardMute="true" :data-scroll-anchor="note.id"/>
|
||||
</template>
|
||||
</component>
|
||||
<button v-show="paginator.canFetchOlder.value" key="_more_" v-appear="prefer.s.enableInfiniteScroll ? paginator.fetchOlder : null" :disabled="paginator.fetchingOlder.value" class="_button" :class="$style.more" @click="paginator.fetchOlder">
|
||||
<div v-if="!paginator.fetchingOlder.value">{{ i18n.ts.loadMore }}</div>
|
||||
<MkLoading v-else :inline="true"/>
|
||||
@@ -53,7 +56,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { computed, watch, onUnmounted, provide, useTemplateRef, onMounted, markRaw } from 'vue';
|
||||
import { computed, watch, onUnmounted, provide, useTemplateRef, TransitionGroup, onMounted, shallowRef, ref, markRaw } from 'vue';
|
||||
import * as Misskey from 'misskey-js';
|
||||
import { useInterval } from '@@/js/use-interval.js';
|
||||
import { useDocumentVisibility } from '@@/js/use-document-visibility.js';
|
||||
@@ -69,10 +72,10 @@ import { instance } from '@/instance.js';
|
||||
import { prefer } from '@/preferences.js';
|
||||
import { store } from '@/store.js';
|
||||
import MkNote from '@/components/MkNote.vue';
|
||||
import MkStreamingTimelineItem, { ITEM_REMOVAL_MS } from '@/components/MkStreamingTimelineItem.vue';
|
||||
import MkButton from '@/components/MkButton.vue';
|
||||
import { i18n } from '@/i18n.js';
|
||||
import { DI } from '@/di.js';
|
||||
import { useGlobalEvent } from '@/events.js';
|
||||
import { globalEvents, useGlobalEvent } from '@/events.js';
|
||||
import { isSeparatorNeeded, getSeparatorInfo } from '@/utility/timeline-date-separate.js';
|
||||
import { Paginator } from '@/utility/paginator.js';
|
||||
|
||||
@@ -101,8 +104,6 @@ provide('inTimeline', true);
|
||||
provide('tl_withSensitive', computed(() => props.withSensitive));
|
||||
provide(DI.inChannel, computed(() => props.src === 'channel' ? props.channel ?? null : null));
|
||||
|
||||
const itemRemovalDelay = prefer.s.animation ? ITEM_REMOVAL_MS : false;
|
||||
|
||||
let paginator: IPaginator<Misskey.entities.Note>;
|
||||
|
||||
if (props.src === 'antenna') {
|
||||
@@ -111,7 +112,6 @@ if (props.src === 'antenna') {
|
||||
antennaId: props.antenna!,
|
||||
})),
|
||||
useShallowRef: true,
|
||||
itemRemovalDelay,
|
||||
}));
|
||||
} else if (props.src === 'home') {
|
||||
paginator = markRaw(new Paginator('notes/timeline', {
|
||||
@@ -120,7 +120,6 @@ if (props.src === 'antenna') {
|
||||
withFiles: props.onlyFiles ? true : undefined,
|
||||
})),
|
||||
useShallowRef: true,
|
||||
itemRemovalDelay,
|
||||
}));
|
||||
} else if (props.src === 'local') {
|
||||
paginator = markRaw(new Paginator('notes/local-timeline', {
|
||||
@@ -130,7 +129,6 @@ if (props.src === 'antenna') {
|
||||
withFiles: props.onlyFiles ? true : undefined,
|
||||
})),
|
||||
useShallowRef: true,
|
||||
itemRemovalDelay,
|
||||
}));
|
||||
} else if (props.src === 'social') {
|
||||
paginator = markRaw(new Paginator('notes/hybrid-timeline', {
|
||||
@@ -140,7 +138,6 @@ if (props.src === 'antenna') {
|
||||
withFiles: props.onlyFiles ? true : undefined,
|
||||
})),
|
||||
useShallowRef: true,
|
||||
itemRemovalDelay,
|
||||
}));
|
||||
} else if (props.src === 'global') {
|
||||
paginator = markRaw(new Paginator('notes/global-timeline', {
|
||||
@@ -149,12 +146,10 @@ if (props.src === 'antenna') {
|
||||
withFiles: props.onlyFiles ? true : undefined,
|
||||
})),
|
||||
useShallowRef: true,
|
||||
itemRemovalDelay,
|
||||
}));
|
||||
} else if (props.src === 'mentions') {
|
||||
paginator = markRaw(new Paginator('notes/mentions', {
|
||||
useShallowRef: true,
|
||||
itemRemovalDelay,
|
||||
}));
|
||||
} else if (props.src === 'directs') {
|
||||
paginator = markRaw(new Paginator('notes/mentions', {
|
||||
@@ -162,7 +157,6 @@ if (props.src === 'antenna') {
|
||||
visibility: 'specified',
|
||||
},
|
||||
useShallowRef: true,
|
||||
itemRemovalDelay,
|
||||
}));
|
||||
} else if (props.src === 'list') {
|
||||
paginator = markRaw(new Paginator('notes/user-list-timeline', {
|
||||
@@ -172,7 +166,6 @@ if (props.src === 'antenna') {
|
||||
listId: props.list!,
|
||||
})),
|
||||
useShallowRef: true,
|
||||
itemRemovalDelay,
|
||||
}));
|
||||
} else if (props.src === 'channel') {
|
||||
paginator = markRaw(new Paginator('channels/timeline', {
|
||||
@@ -180,7 +173,6 @@ if (props.src === 'antenna') {
|
||||
channelId: props.channel!,
|
||||
})),
|
||||
useShallowRef: true,
|
||||
itemRemovalDelay,
|
||||
}));
|
||||
} else if (props.src === 'role') {
|
||||
paginator = markRaw(new Paginator('roles/notes', {
|
||||
@@ -188,7 +180,6 @@ if (props.src === 'antenna') {
|
||||
roleId: props.role!,
|
||||
})),
|
||||
useShallowRef: true,
|
||||
itemRemovalDelay,
|
||||
}));
|
||||
} else {
|
||||
throw new Error('Unrecognized timeline type: ' + props.src);
|
||||
@@ -437,16 +428,45 @@ defineExpose({
|
||||
</script>
|
||||
|
||||
<style lang="scss" module>
|
||||
.transition_x_move {
|
||||
transition: transform 0.7s cubic-bezier(0.23, 1, 0.32, 1);
|
||||
}
|
||||
|
||||
.transition_x_enterActive {
|
||||
transition: transform 0.7s cubic-bezier(0.23, 1, 0.32, 1), opacity 0.7s cubic-bezier(0.23, 1, 0.32, 1);
|
||||
|
||||
&.note,
|
||||
.note {
|
||||
/* Skip Note Rendering有効時、TransitionGroupでnoteを追加するときに一瞬がくっとなる問題を抑制する */
|
||||
content-visibility: visible !important;
|
||||
}
|
||||
}
|
||||
|
||||
.transition_x_leaveActive {
|
||||
transition: height 0.2s cubic-bezier(0,.5,.5,1), opacity 0.2s cubic-bezier(0,.5,.5,1);
|
||||
}
|
||||
|
||||
.transition_x_enterFrom {
|
||||
opacity: 0;
|
||||
transform: translateY(max(-64px, -100%));
|
||||
}
|
||||
|
||||
@supports (interpolate-size: allow-keywords) {
|
||||
.transition_x_leaveTo {
|
||||
interpolate-size: allow-keywords; // heightのtransitionを動作させるために必要
|
||||
height: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.transition_x_leaveTo {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.notes {
|
||||
container-type: inline-size;
|
||||
background: var(--MI_THEME-panel);
|
||||
}
|
||||
|
||||
.date,
|
||||
.note {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.note:not(:empty) {
|
||||
border-bottom: solid 0.5px var(--MI_THEME-divider);
|
||||
}
|
||||
|
||||
@@ -14,15 +14,16 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||
</div>
|
||||
|
||||
<div v-else ref="rootEl">
|
||||
<div :class="$style.notifications">
|
||||
<component
|
||||
:is="prefer.s.animation ? MkStreamingTimelineItem : 'div'"
|
||||
v-for="(notification, i) in paginator.items.value"
|
||||
v-bind="prefer.s.animation ? { animatingIn: notification._shouldAnimateIn_, animatingOut: notification._shouldAnimateOut_ } : {}"
|
||||
:key="notification.id"
|
||||
:data-scroll-anchor="notification.id"
|
||||
:class="$style.item"
|
||||
>
|
||||
<component
|
||||
:is="prefer.s.animation ? TransitionGroup : 'div'" :class="[$style.notifications]"
|
||||
:enterActiveClass="$style.transition_x_enterActive"
|
||||
:leaveActiveClass="$style.transition_x_leaveActive"
|
||||
:enterFromClass="$style.transition_x_enterFrom"
|
||||
:leaveToClass="$style.transition_x_leaveTo"
|
||||
:moveClass="$style.transition_x_move"
|
||||
tag="div"
|
||||
>
|
||||
<div v-for="(notification, i) in paginator.items.value" :key="notification.id" :data-scroll-anchor="notification.id" :class="$style.item">
|
||||
<div v-if="i > 0 && isSeparatorNeeded(paginator.items.value[i -1].createdAt, notification.createdAt)" :class="$style.date">
|
||||
<span><i class="ti ti-chevron-up"></i> {{ getSeparatorInfo(paginator.items.value[i -1].createdAt, notification.createdAt)?.prevText }}</span>
|
||||
<span style="height: 1em; width: 1px; background: var(--MI_THEME-divider);"></span>
|
||||
@@ -30,8 +31,8 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||
</div>
|
||||
<MkNote v-if="['reply', 'quote', 'mention'].includes(notification.type) && 'note' in notification" :class="$style.content" :note="notification.note" :withHardMute="true"/>
|
||||
<XNotification v-else :class="$style.content" :notification="notification" :withTime="true" :full="true"/>
|
||||
</component>
|
||||
</div>
|
||||
</div>
|
||||
</component>
|
||||
<button v-show="paginator.canFetchOlder.value" key="_more_" v-appear="prefer.s.enableInfiniteScroll ? paginator.fetchOlder : null" :disabled="paginator.fetchingOlder.value" class="_button" :class="$style.more" @click="paginator.fetchOlder">
|
||||
<div v-if="!paginator.fetchingOlder.value">{{ i18n.ts.loadMore }}</div>
|
||||
<MkLoading v-else/>
|
||||
@@ -41,7 +42,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { onUnmounted, onMounted, computed, useTemplateRef, markRaw, watch } from 'vue';
|
||||
import { onUnmounted, onMounted, computed, useTemplateRef, TransitionGroup, markRaw, watch } from 'vue';
|
||||
import * as Misskey from 'misskey-js';
|
||||
import { notificationTypes } from 'misskey-js';
|
||||
import { useInterval } from '@@/js/use-interval.js';
|
||||
@@ -52,7 +53,6 @@ import MkNote from '@/components/MkNote.vue';
|
||||
import { useStream } from '@/stream.js';
|
||||
import { i18n } from '@/i18n.js';
|
||||
import MkPullToRefresh from '@/components/MkPullToRefresh.vue';
|
||||
import MkStreamingTimelineItem, { ITEM_REMOVAL_MS } from '@/components/MkStreamingTimelineItem.vue';
|
||||
import { prefer } from '@/preferences.js';
|
||||
import { store } from '@/store.js';
|
||||
import { isSeparatorNeeded, getSeparatorInfo } from '@/utility/timeline-date-separate.js';
|
||||
@@ -64,20 +64,16 @@ const props = defineProps<{
|
||||
|
||||
const rootEl = useTemplateRef('rootEl');
|
||||
|
||||
const itemRemovalDelay = prefer.s.animation ? ITEM_REMOVAL_MS : false;
|
||||
|
||||
const paginator = prefer.s.useGroupedNotifications ? markRaw(new Paginator('i/notifications-grouped', {
|
||||
limit: 20,
|
||||
computedParams: computed(() => ({
|
||||
excludeTypes: props.excludeTypes ?? undefined,
|
||||
})),
|
||||
itemRemovalDelay,
|
||||
})) : markRaw(new Paginator('i/notifications', {
|
||||
limit: 20,
|
||||
computedParams: computed(() => ({
|
||||
excludeTypes: props.excludeTypes ?? undefined,
|
||||
})),
|
||||
itemRemovalDelay,
|
||||
}));
|
||||
|
||||
const MIN_POLLING_INTERVAL = 1000 * 10;
|
||||
@@ -193,8 +189,38 @@ defineExpose({
|
||||
</script>
|
||||
|
||||
<style lang="scss" module>
|
||||
.item {
|
||||
border-bottom: solid 0.5px var(--MI_THEME-divider);
|
||||
.transition_x_move {
|
||||
transition: transform 0.7s cubic-bezier(0.23, 1, 0.32, 1);
|
||||
}
|
||||
|
||||
.transition_x_enterActive {
|
||||
transition: transform 0.7s cubic-bezier(0.23, 1, 0.32, 1), opacity 0.7s cubic-bezier(0.23, 1, 0.32, 1);
|
||||
|
||||
&.content,
|
||||
.content {
|
||||
/* Skip Note Rendering有効時、TransitionGroupで通知を追加するときに一瞬がくっとなる問題を抑制する */
|
||||
content-visibility: visible !important;
|
||||
}
|
||||
}
|
||||
|
||||
.transition_x_leaveActive {
|
||||
transition: height 0.2s cubic-bezier(0,.5,.5,1), opacity 0.2s cubic-bezier(0,.5,.5,1);
|
||||
}
|
||||
|
||||
.transition_x_enterFrom {
|
||||
opacity: 0;
|
||||
transform: translateY(max(-64px, -100%));
|
||||
}
|
||||
|
||||
@supports (interpolate-size: allow-keywords) {
|
||||
.transition_x_enterFrom {
|
||||
interpolate-size: allow-keywords; // heightのtransitionを動作させるために必要
|
||||
height: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.transition_x_leaveTo {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.notifications {
|
||||
@@ -202,6 +228,10 @@ defineExpose({
|
||||
background: var(--MI_THEME-panel);
|
||||
}
|
||||
|
||||
.item {
|
||||
border-bottom: solid 0.5px var(--MI_THEME-divider);
|
||||
}
|
||||
|
||||
.date {
|
||||
display: flex;
|
||||
font-size: 85%;
|
||||
|
||||
@@ -1,127 +0,0 @@
|
||||
<!--
|
||||
SPDX-FileCopyrightText: syuilo and misskey-project
|
||||
SPDX-License-Identifier: AGPL-3.0-only
|
||||
-->
|
||||
|
||||
<template>
|
||||
<div
|
||||
ref="rootEl"
|
||||
:class="[$style.root, {
|
||||
[$style.enter]: animatingIn && animating && !animatingOut,
|
||||
[$style.leave]: animatingOut,
|
||||
[$style.animating]: animating,
|
||||
}]"
|
||||
@animationend.self="onAnimationEnd"
|
||||
@animationcancel.self="onAnimationEnd"
|
||||
>
|
||||
<div ref="innerEl">
|
||||
<slot></slot>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
export const ITEM_REMOVAL_MS = 200;
|
||||
|
||||
const supportsInterpolateSize = CSS.supports('interpolate-size: allow-keywords');
|
||||
let resizeObserver: ResizeObserver | null = null;
|
||||
|
||||
if (!supportsInterpolateSize) {
|
||||
resizeObserver = new ResizeObserver((entries) => {
|
||||
for (const entry of entries) {
|
||||
const target = entry.target as HTMLElement;
|
||||
const root = target.parentElement;
|
||||
if (root != null) {
|
||||
root.style.setProperty('--child-height', `${entry.contentRect.height}px`);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
</script>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { useTemplateRef, onMounted, onBeforeUnmount, ref, watch } from 'vue';
|
||||
|
||||
const props = defineProps<{
|
||||
animatingIn?: boolean;
|
||||
animatingOut?: boolean;
|
||||
}>();
|
||||
|
||||
const rootEl = useTemplateRef('rootEl');
|
||||
const innerEl = useTemplateRef('innerEl');
|
||||
|
||||
const animating = ref(false);
|
||||
|
||||
watch([() => props.animatingIn, () => props.animatingOut], ([animatingIn, animatingOut]) => {
|
||||
if (animatingIn === true || animatingOut === true) animating.value = true;
|
||||
}, { immediate: true });
|
||||
|
||||
function onAnimationEnd() {
|
||||
// 削除アニメーション中(enterから差し替わった場合を含む)は、要素が消えるまで再生中扱いのままにする
|
||||
if (props.animatingOut === true) return;
|
||||
animating.value = false;
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
if (resizeObserver != null && rootEl.value != null && innerEl.value != null) {
|
||||
resizeObserver.observe(innerEl.value);
|
||||
rootEl.value.style.setProperty('--child-height', `${innerEl.value.getBoundingClientRect().height}px`);
|
||||
}
|
||||
});
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
if (resizeObserver != null && innerEl.value != null) {
|
||||
resizeObserver.unobserve(innerEl.value);
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<style module lang="scss">
|
||||
.animating {
|
||||
overflow: clip;
|
||||
}
|
||||
|
||||
.inner {
|
||||
display: flow-root;
|
||||
}
|
||||
|
||||
.enter {
|
||||
animation: enterAnim 0.7s cubic-bezier(0.23, 1, 0.32, 1) both;
|
||||
}
|
||||
|
||||
.leave {
|
||||
animation: leaveAnim 0.2s cubic-bezier(0,.5,.5,1) both;
|
||||
}
|
||||
|
||||
@supports (interpolate-size: allow-keywords) {
|
||||
.root {
|
||||
interpolate-size: allow-keywords;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes enterAnim {
|
||||
from {
|
||||
height: 0;
|
||||
opacity: 0;
|
||||
transform: translateY(max(-64px, -100%));
|
||||
}
|
||||
|
||||
to {
|
||||
height: var(--child-height, auto);
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes leaveAnim {
|
||||
from {
|
||||
height: var(--child-height, auto);
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
to {
|
||||
height: 0;
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -5,8 +5,8 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
<template>
|
||||
<div ref="rootEl" :class="$style.root">
|
||||
<canvas :id="idForCanvas" ref="canvasEl" style="display: block;" :width="width" height="300" @contextmenu.prevent="() => {}"></canvas>
|
||||
<div :id="idForTags" ref="tagsEl" :class="$style.tags">
|
||||
<canvas ref="canvasEl" style="display: block;" :width="width" height="300" @contextmenu.prevent="() => {}"></canvas>
|
||||
<div ref="tagsEl" :class="$style.tags">
|
||||
<ul>
|
||||
<slot></slot>
|
||||
</ul>
|
||||
@@ -15,62 +15,68 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { onMounted, watch, onBeforeUnmount, ref, useTemplateRef } from 'vue';
|
||||
import { onMounted, onBeforeUnmount, nextTick, ref, shallowRef, useTemplateRef } from 'vue';
|
||||
import { themeManager } from '@/theme.js';
|
||||
import tinycolor from 'tinycolor2';
|
||||
import { TagCanvas } from '@misskey-dev/tagcanvas-es';
|
||||
|
||||
const loaded = !!window.TagCanvas;
|
||||
const SAFE_FOR_HTML_ID = 'abcdefghijklmnopqrstuvwxyz';
|
||||
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);
|
||||
const rootEl = useTemplateRef('rootEl');
|
||||
const canvasEl = useTemplateRef('canvasEl');
|
||||
const tagsEl = useTemplateRef('tagsEl');
|
||||
const tagCanvas = shallowRef<TagCanvas | null>(null);
|
||||
const width = ref(300);
|
||||
|
||||
watch(available, () => {
|
||||
try {
|
||||
window.TagCanvas.Start(idForCanvas, idForTags, {
|
||||
textColour: '#ffffff',
|
||||
outlineColour: tinycolor(themeManager.currentCompiledTheme!.accent).toHexString(),
|
||||
outlineRadius: 10,
|
||||
initial: [-0.030, -0.010],
|
||||
frontSelect: true,
|
||||
imageRadius: 8,
|
||||
//dragControl: true,
|
||||
dragThreshold: 3,
|
||||
wheelZoom: false,
|
||||
reverse: true,
|
||||
depth: 0.5,
|
||||
maxSpeed: 0.2,
|
||||
minSpeed: 0.003,
|
||||
stretchX: 0.8,
|
||||
stretchY: 0.8,
|
||||
});
|
||||
} catch (err) {}
|
||||
});
|
||||
function createTagCanvas() {
|
||||
if (tagCanvas.value) {
|
||||
tagCanvas.value.destroy();
|
||||
tagCanvas.value = null;
|
||||
}
|
||||
|
||||
if (tagsEl.value == null || canvasEl.value == null) return;
|
||||
if (tagsEl.value.children[0].children.length === 0) return;
|
||||
|
||||
tagCanvas.value = new TagCanvas(canvasEl.value, {
|
||||
tagContainer: tagsEl.value,
|
||||
textColor: '#ffffff',
|
||||
outlineColor: tinycolor(themeManager.currentCompiledTheme!.accent).toHexString(),
|
||||
outlineRadius: 10,
|
||||
initial: [-0.030, -0.010],
|
||||
frontSelect: true,
|
||||
imageRadius: 8,
|
||||
// dragControl: true,
|
||||
dragThreshold: 3,
|
||||
wheelZoom: false,
|
||||
reverse: true,
|
||||
depth: 0.5,
|
||||
maxSpeed: 0.2,
|
||||
minSpeed: 0.003,
|
||||
stretchX: 0.8,
|
||||
stretchY: 0.8,
|
||||
});
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
if (rootEl.value) width.value = rootEl.value.offsetWidth;
|
||||
|
||||
if (loaded) {
|
||||
available.value = true;
|
||||
} else {
|
||||
window.document.head.appendChild(Object.assign(window.document.createElement('script'), {
|
||||
async: true,
|
||||
src: '/client-assets/tagcanvas.min.js',
|
||||
})).addEventListener('load', () => available.value = true);
|
||||
}
|
||||
nextTick(() => {
|
||||
createTagCanvas();
|
||||
});
|
||||
});
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
if (window.TagCanvas) window.TagCanvas.Delete(idForCanvas);
|
||||
if (tagCanvas.value) {
|
||||
tagCanvas.value.destroy();
|
||||
tagCanvas.value = null;
|
||||
}
|
||||
});
|
||||
|
||||
defineExpose({
|
||||
update: () => {
|
||||
window.TagCanvas.Update(idForCanvas);
|
||||
if (tagCanvas.value) {
|
||||
tagCanvas.value.update();
|
||||
} else {
|
||||
createTagCanvas();
|
||||
}
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
@@ -49,13 +49,14 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { computed } from 'vue';
|
||||
import { computed, onUnmounted, watch } from 'vue';
|
||||
import { isLink } from '@@/js/is-link.js';
|
||||
import { getUploadName } from '@/composables/use-uploader.js';
|
||||
import type { UploaderItem } from '@/composables/use-uploader.js';
|
||||
import { getUploadName } from '@/composables/use-uploader.js';
|
||||
import { i18n } from '@/i18n.js';
|
||||
import MkButton from '@/components/MkButton.vue';
|
||||
import bytes from '@/filters/bytes.js';
|
||||
import * as os from '@/os.js';
|
||||
|
||||
const props = defineProps<{
|
||||
items: UploaderItem[];
|
||||
@@ -71,6 +72,32 @@ const emit = defineEmits<{
|
||||
(ev: 'showMenuViaContextmenu', item: UploaderItem, event: PointerEvent): void;
|
||||
}>();
|
||||
|
||||
//#region objectUrlMap
|
||||
const objectUrlMap = new Map<UploaderItem, string>();
|
||||
|
||||
watch(() => props.items, () => {
|
||||
for (const item of props.items) {
|
||||
if (!objectUrlMap.has(item)) {
|
||||
objectUrlMap.set(item, URL.createObjectURL(item.file));
|
||||
}
|
||||
}
|
||||
|
||||
for (const [item, url] of objectUrlMap.entries()) {
|
||||
if (!props.items.includes(item)) {
|
||||
URL.revokeObjectURL(url);
|
||||
objectUrlMap.delete(item);
|
||||
}
|
||||
}
|
||||
}, { immediate: true, deep: true });
|
||||
|
||||
onUnmounted(() => {
|
||||
for (const url of objectUrlMap.values()) {
|
||||
URL.revokeObjectURL(url);
|
||||
}
|
||||
objectUrlMap.clear();
|
||||
});
|
||||
//#endregion
|
||||
|
||||
function getUploadNameParts(item: UploaderItem): {
|
||||
baseName: string;
|
||||
extension: string | null;
|
||||
@@ -98,8 +125,23 @@ function onContextmenu(item: UploaderItem, ev: PointerEvent) {
|
||||
emit('showMenuViaContextmenu', item, ev);
|
||||
}
|
||||
|
||||
function onThumbnailClick(item: UploaderItem, ev: PointerEvent) {
|
||||
// TODO: preview when item is image
|
||||
async function onThumbnailClick(item: UploaderItem, ev: PointerEvent) {
|
||||
if (item.file.type.startsWith('image') || item.file.type.startsWith('video')) {
|
||||
const contents = props.items.filter(item => item.file.type.startsWith('image') || item.file.type.startsWith('video')).map(item => ({
|
||||
id: item.id,
|
||||
type: (item.file.type.startsWith('video') ? 'video' as const : 'image' as const),
|
||||
url: objectUrlMap.get(item)!,
|
||||
thumbnailUrl: item.thumbnail,
|
||||
filename: getUploadName(item),
|
||||
}));
|
||||
|
||||
const { dispose } = await os.popupAsyncWithDialog(import('@/components/MkLightbox.vue').then(x => x.default), {
|
||||
defaultIndex: contents.findIndex(content => content.id === item.id),
|
||||
contents: contents,
|
||||
}, {
|
||||
closed: () => dispose(),
|
||||
});
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
|
||||
@@ -144,7 +144,17 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
<MkButton v-if="storagePersistenceSupported && !storagePersisted" @click="enableStoragePersistence">{{ i18n.ts._settings.settingsPersistence_title }}</MkButton>
|
||||
|
||||
<MkButton @click="forceCloudBackup">{{ i18n.ts._preferencesBackup.forceBackup }}</MkButton>
|
||||
<SearchMarker :keywords="['profile', 'preferences']">
|
||||
<MkFolder>
|
||||
<template #icon><SearchIcon><i class="ti ti-cogs"></i></SearchIcon></template>
|
||||
<template #label><SearchLabel>{{ i18n.ts.preferencesProfile }}</SearchLabel></template>
|
||||
|
||||
<div class="_buttons">
|
||||
<MkButton @click="forceCloudBackup">{{ i18n.ts._preferencesBackup.forceBackup }}</MkButton>
|
||||
<MkButton @click="forceCloudSync">{{ i18n.ts._preferencesBackup.forceSync }}</MkButton>
|
||||
</div>
|
||||
</MkFolder>
|
||||
</SearchMarker>
|
||||
</div>
|
||||
</SearchMarker>
|
||||
</template>
|
||||
@@ -170,7 +180,7 @@ import MkRolePreview from '@/components/MkRolePreview.vue';
|
||||
import { signout } from '@/signout.js';
|
||||
import { hideAllTips as _hideAllTips, resetAllTips as _resetAllTips } from '@/tips.js';
|
||||
import { suggestReload } from '@/utility/reload-suggest.js';
|
||||
import { cloudBackup } from '@/preferences/utility.js';
|
||||
import { cloudBackup, cloudSync } from '@/preferences/utility.js';
|
||||
|
||||
const $i = ensureSignin();
|
||||
|
||||
@@ -232,6 +242,11 @@ async function forceCloudBackup() {
|
||||
os.success();
|
||||
}
|
||||
|
||||
async function forceCloudSync() {
|
||||
await cloudSync();
|
||||
os.success();
|
||||
}
|
||||
|
||||
const headerActions = computed(() => []);
|
||||
|
||||
const headerTabs = computed(() => []);
|
||||
|
||||
@@ -3,16 +3,14 @@
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import { BroadcastChannel } from 'broadcast-channel';
|
||||
import { createVisibilityAwareInterval } from '@@/js/interval.js';
|
||||
import type { StorageProvider } from '@/preferences/manager.js';
|
||||
import { cloudBackup } from '@/preferences/utility.js';
|
||||
import { cloudBackup, cloudSync } from '@/preferences/utility.js';
|
||||
import { miLocalStorage } from '@/local-storage.js';
|
||||
import { isSameScope, PreferencesManager } from '@/preferences/manager.js';
|
||||
import { store } from '@/store.js';
|
||||
import { $i } from '@/i.js';
|
||||
import { misskeyApi } from '@/utility/misskey-api.js';
|
||||
import { TAB_ID } from '@/tab-id.js';
|
||||
|
||||
// クラウド同期用グループ名
|
||||
const syncGroup = 'default';
|
||||
@@ -38,11 +36,12 @@ const io: StorageProvider = {
|
||||
const cloudData = await misskeyApi('i/registry/get', {
|
||||
scope: ['client', 'preferences', 'sync'],
|
||||
key: syncGroup + ':' + ctx.key,
|
||||
}) as [any, any][];
|
||||
}) as [any, any, any][];
|
||||
const target = cloudData.find(([scope]) => isSameScope(scope, ctx.scope));
|
||||
if (target == null) return null;
|
||||
return {
|
||||
value: target[1],
|
||||
meta: target[2],
|
||||
};
|
||||
} catch (err: any) {
|
||||
if (err.code === 'NO_SUCH_KEY') { // TODO: いちいちエラーキャッチするのは面倒なのでキーが無くてもエラーにならない maybe-get のようなエンドポイントをバックエンドに実装する
|
||||
@@ -54,12 +53,12 @@ const io: StorageProvider = {
|
||||
},
|
||||
|
||||
cloudSet: async (ctx) => {
|
||||
let cloudData: [any, any][] = [];
|
||||
let cloudData: [any, any, any][] = [];
|
||||
try {
|
||||
cloudData = await misskeyApi('i/registry/get', {
|
||||
scope: ['client', 'preferences', 'sync'],
|
||||
key: syncGroup + ':' + ctx.key,
|
||||
}) as [any, any][];
|
||||
}) as [any, any, any][];
|
||||
} catch (err: any) {
|
||||
if (err.code === 'NO_SUCH_KEY') { // TODO: いちいちエラーキャッチするのは面倒なのでキーが無くてもエラーにならない maybe-get のようなエンドポイントをバックエンドに実装する
|
||||
cloudData = [];
|
||||
@@ -71,9 +70,9 @@ const io: StorageProvider = {
|
||||
const i = cloudData.findIndex(([scope]) => isSameScope(scope, ctx.scope));
|
||||
|
||||
if (i === -1) {
|
||||
cloudData.push([ctx.scope, ctx.value]);
|
||||
cloudData.push([ctx.scope, ctx.value, ctx.meta]);
|
||||
} else {
|
||||
cloudData[i] = [ctx.scope, ctx.value];
|
||||
cloudData[i] = [ctx.scope, ctx.value, ctx.meta];
|
||||
}
|
||||
|
||||
await misskeyApi('i/registry/set', {
|
||||
@@ -88,10 +87,10 @@ const io: StorageProvider = {
|
||||
const fetchings = ctx.needs.map(need => io.cloudGet(need).then(res => [need.key, res] as const));
|
||||
const cloudDatas = await Promise.all(fetchings);
|
||||
|
||||
const res = {} as Partial<Record<string, any>>;
|
||||
const res = {} as Partial<Record<string, { value: any; meta: any; }>>;
|
||||
for (const cloudData of cloudDatas) {
|
||||
if (cloudData[1] != null) {
|
||||
res[cloudData[0]] = cloudData[1].value;
|
||||
res[cloudData[0]] = cloudData[1];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -102,41 +101,10 @@ const io: StorageProvider = {
|
||||
export const prefer = new PreferencesManager(io, $i);
|
||||
|
||||
//#region タブ間同期
|
||||
let latestPreferencesUpdate: {
|
||||
tabId: string;
|
||||
timestamp: number;
|
||||
} | null = null;
|
||||
|
||||
const preferencesChannel = new BroadcastChannel<{
|
||||
type: 'preferencesUpdate';
|
||||
tabId: string;
|
||||
timestamp: number;
|
||||
}>('preferences');
|
||||
|
||||
prefer.on('committed', () => {
|
||||
latestPreferencesUpdate = {
|
||||
tabId: TAB_ID,
|
||||
timestamp: Date.now(),
|
||||
};
|
||||
preferencesChannel.postMessage({
|
||||
type: 'preferencesUpdate',
|
||||
tabId: TAB_ID,
|
||||
timestamp: latestPreferencesUpdate.timestamp,
|
||||
});
|
||||
});
|
||||
|
||||
preferencesChannel.addEventListener('message', (msg) => {
|
||||
if (msg.type === 'preferencesUpdate') {
|
||||
if (msg.tabId === TAB_ID) return;
|
||||
if (latestPreferencesUpdate != null) {
|
||||
if (msg.timestamp <= latestPreferencesUpdate.timestamp) return;
|
||||
}
|
||||
window.addEventListener('storage', (ev) => {
|
||||
if (ev.key === 'preferences') {
|
||||
prefer.reloadProfile();
|
||||
if (_DEV_) console.log('prefer:received update from other tab');
|
||||
latestPreferencesUpdate = {
|
||||
tabId: msg.tabId,
|
||||
timestamp: msg.timestamp,
|
||||
};
|
||||
if (_DEV_) console.log('prefer: received update from other tab');
|
||||
}
|
||||
});
|
||||
//#endregion
|
||||
@@ -156,6 +124,13 @@ createVisibilityAwareInterval(() => {
|
||||
}, 1000 * 60 * 3);
|
||||
//#endregion
|
||||
|
||||
store.loaded.then(() => {
|
||||
if (store.s.enablePreferencesAutoCloudSync) {
|
||||
// TODO: 前回同期してから10分以上経過している場合のみ
|
||||
cloudSync();
|
||||
}
|
||||
});
|
||||
|
||||
if (_DEV_) {
|
||||
(window as any).prefer = prefer;
|
||||
(window as any).cloudBackup = cloudBackup;
|
||||
|
||||
@@ -36,6 +36,7 @@ type Scope = Partial<{
|
||||
|
||||
type ValueMeta = Partial<{
|
||||
sync: boolean;
|
||||
modifiedAt?: number; // 設定値を変更した日時。同期した日時などではない。つまり別のデバイスでA日に変更したものをB日に同期して取得したとしてもmodifiedAtはA日である必要がある
|
||||
}>;
|
||||
|
||||
type PrefRecord<K extends keyof PREF> = [scope: Scope, value: ValueOf<K>, meta: ValueMeta];
|
||||
@@ -74,7 +75,7 @@ export type PreferencesProfile = {
|
||||
id: string;
|
||||
version: string;
|
||||
type: 'main';
|
||||
modifiedAt: number;
|
||||
modifiedAt: number; // 仕様が若干直感的ではない(syncされた値が降ってきたときは更新されないなど)ため、一応残してはいるが積極的な利用はしない方が無難。設定値の新旧比較が必要なら項目ごとのmodifiedAtを使うべし
|
||||
name: string;
|
||||
preferences: {
|
||||
[K in keyof PREF]: PrefRecord<K>[];
|
||||
@@ -88,9 +89,9 @@ export type PossiblyNonNormalizedPreferencesProfile = Omit<PreferencesProfile, '
|
||||
export type StorageProvider = {
|
||||
load: () => PossiblyNonNormalizedPreferencesProfile | null;
|
||||
save: (ctx: { profile: PreferencesProfile; }) => void;
|
||||
cloudGetBulk: <K extends keyof PREF>(ctx: { needs: { key: K; scope: Scope; }[] }) => Promise<Partial<Record<K, ValueOf<K>>>>;
|
||||
cloudGet: <K extends keyof PREF>(ctx: { key: K; scope: Scope; }) => Promise<{ value: ValueOf<K>; } | null>;
|
||||
cloudSet: <K extends keyof PREF>(ctx: { key: K; scope: Scope; value: ValueOf<K>; }) => Promise<void>;
|
||||
cloudGetBulk: <K extends keyof PREF>(ctx: { needs: { key: K; scope: Scope; }[] }) => Promise<Partial<Record<K, { value: ValueOf<K>; meta: { modifiedAt: ValueMeta['modifiedAt'] }; }>>>;
|
||||
cloudGet: <K extends keyof PREF>(ctx: { key: K; scope: Scope; }) => Promise<{ value: ValueOf<K>; meta: { modifiedAt: ValueMeta['modifiedAt'] }; } | null>;
|
||||
cloudSet: <K extends keyof PREF>(ctx: { key: K; scope: Scope; value: ValueOf<K>; meta: { modifiedAt: ValueMeta['modifiedAt'] }; }) => Promise<void>;
|
||||
};
|
||||
|
||||
type PreferencesDefinitionRecord<Default, T = Default extends (...args: any) => infer R ? R : Default> = {
|
||||
@@ -102,14 +103,6 @@ type PreferencesDefinitionRecord<Default, T = Default extends (...args: any) =>
|
||||
|
||||
export type PreferencesDefinition = Record<string, PreferencesDefinitionRecord<any>>;
|
||||
|
||||
type PreferencesManagerEvents = {
|
||||
'committed': <K extends keyof PREF>(ctx: {
|
||||
key: K;
|
||||
value: ValueOf<K>;
|
||||
oldValue: ValueOf<K>;
|
||||
}) => void;
|
||||
};
|
||||
|
||||
export function definePreferences<T extends Record<string, unknown>>(x: {
|
||||
[K in keyof T]: PreferencesDefinitionRecord<T[K]>
|
||||
}): {
|
||||
@@ -188,6 +181,44 @@ function normalizePreferences(preferences: PossiblyNonNormalizedPreferencesProfi
|
||||
return data as PreferencesProfile['preferences'];
|
||||
}
|
||||
|
||||
// 各recordについて、modifiedAtが大きい方を採用する
|
||||
// 引数の参照をmutateしないように注意すること
|
||||
export function mergeProfiles(a: PreferencesProfile, b: PreferencesProfile): PreferencesProfile {
|
||||
const merged = {
|
||||
...a,
|
||||
modifiedAt: Math.max(a.modifiedAt, b.modifiedAt),
|
||||
preferences: {},
|
||||
} as PreferencesProfile;
|
||||
|
||||
for (const _key in PREF_DEF) {
|
||||
const key = _key as keyof PREF;
|
||||
const aRecords = a.preferences[key];
|
||||
const bRecords = b.preferences[key];
|
||||
|
||||
const mergedRecords = [...aRecords];
|
||||
|
||||
for (const bRecord of bRecords) {
|
||||
const existingIndex = mergedRecords.findIndex(([scope]) => isSameScope(scope, bRecord[0]));
|
||||
if (existingIndex === -1) {
|
||||
mergedRecords.push(bRecord);
|
||||
} else {
|
||||
const aRecord = mergedRecords[existingIndex];
|
||||
if ((bRecord[2].modifiedAt ?? 0) > (aRecord[2].modifiedAt ?? 0)) {
|
||||
mergedRecords[existingIndex] = bRecord;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
(merged.preferences[key] as PrefRecord<typeof key>[]) = mergedRecords;
|
||||
}
|
||||
|
||||
return merged;
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-empty-object-type
|
||||
type PreferencesManagerEvents = {
|
||||
};
|
||||
|
||||
// TODO: PreferencesManagerForGuest のような非ログイン専用のクラスを分離すればthis.currentAccountのnullチェックやaccountがnullであるスコープのレコード挿入などが不要になり綺麗になるかもしれない
|
||||
// と思ったけど操作アカウントが存在しない場合も考慮する現在の設計の方が汎用的かつ堅牢かもしれない
|
||||
// NOTE: accountDependentな設定は初期状態であってもアカウントごとのスコープでレコードを作成しておかないと、サーバー同期する際に正しく動作しなくなる
|
||||
@@ -247,13 +278,13 @@ export class PreferencesManager extends EventEmitter<PreferencesManagerEvents> {
|
||||
}
|
||||
|
||||
// TODO: desync対策 cloudの値のfetchが正常に完了していない状態でcommitすると多分値が上書きされる
|
||||
public commit<K extends keyof PREF>(key: K, value: ValueOf<K>) {
|
||||
public commit<K extends keyof PREF>(key: K, value: ValueOf<K>): PrefRecord<K> | null {
|
||||
const currentAccount = this.currentAccount; // TSを黙らせるため
|
||||
const v = JSON.parse(JSON.stringify(value)); // deep copy 兼 vueのプロキシ解除
|
||||
|
||||
if (deepEqual(this.s[key], v)) {
|
||||
if (_DEV_) console.log('(skip) prefer:commit', key, v);
|
||||
return;
|
||||
return null;
|
||||
}
|
||||
|
||||
if (_DEV_) console.log('prefer:commit', key, v);
|
||||
@@ -262,40 +293,40 @@ export class PreferencesManager extends EventEmitter<PreferencesManagerEvents> {
|
||||
|
||||
const record = this.getMatchedRecordOf(key);
|
||||
|
||||
const _save = () => {
|
||||
this.save();
|
||||
this.emit('committed', {
|
||||
key,
|
||||
value: v,
|
||||
oldValue: this.s[key],
|
||||
});
|
||||
};
|
||||
|
||||
if (parseScope(record[0]).account == null && isAccountDependentKey(key) && currentAccount != null) {
|
||||
this.profile.preferences[key].push([makeScope({
|
||||
const newRecord = [makeScope({
|
||||
server: host,
|
||||
account: currentAccount.id,
|
||||
}), v, {}]);
|
||||
_save();
|
||||
return;
|
||||
}), v, {
|
||||
modifiedAt: Date.now(),
|
||||
}] as PrefRecord<K>;
|
||||
this.profile.preferences[key].push(newRecord);
|
||||
this.save();
|
||||
return newRecord;
|
||||
}
|
||||
|
||||
if (parseScope(record[0]).server == null && isServerDependentKey(key)) {
|
||||
this.profile.preferences[key].push([makeScope({
|
||||
const newRecord = [makeScope({
|
||||
server: host,
|
||||
}), v, {}]);
|
||||
_save();
|
||||
return;
|
||||
}), v, {
|
||||
modifiedAt: Date.now(),
|
||||
}] as PrefRecord<K>;
|
||||
this.profile.preferences[key].push(newRecord);
|
||||
this.save();
|
||||
return newRecord;
|
||||
}
|
||||
|
||||
record[1] = v;
|
||||
_save();
|
||||
record[2].modifiedAt = Date.now();
|
||||
this.save();
|
||||
|
||||
if (record[2].sync) {
|
||||
// awaitの必要なし
|
||||
// TODO: リクエストを間引く
|
||||
this.io.cloudSet({ key, scope: record[0], value: record[1] });
|
||||
this.io.cloudSet({ key, scope: record[0], value: record[1], meta: { modifiedAt: record[2].modifiedAt } });
|
||||
}
|
||||
|
||||
return record;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -364,24 +395,29 @@ export class PreferencesManager extends EventEmitter<PreferencesManagerEvents> {
|
||||
|
||||
const cloudValues = await this.io.cloudGetBulk({ needs });
|
||||
|
||||
let modified = false;
|
||||
|
||||
for (const _key in PREF_DEF) {
|
||||
const key = _key as keyof PREF;
|
||||
const record = this.getMatchedRecordOf(key);
|
||||
if (record[2].sync && Object.hasOwn(cloudValues, key) && cloudValues[key] !== undefined) {
|
||||
const cloudValue = cloudValues[key];
|
||||
if (!deepEqual(cloudValue, record[1])) {
|
||||
this.rewriteRawState(key, cloudValue);
|
||||
record[1] = cloudValue;
|
||||
this.rewriteRawState(key, cloudValue.value);
|
||||
record[1] = cloudValue.value;
|
||||
record[2].modifiedAt = cloudValue.meta.modifiedAt;
|
||||
modified = true;
|
||||
if (_DEV_) console.log('cloud fetched', key, cloudValue);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
this.save();
|
||||
if (modified) this.save();
|
||||
|
||||
if (_DEV_) console.log('cloud fetch completed');
|
||||
}
|
||||
|
||||
public save() {
|
||||
private save() {
|
||||
this.profile.modifiedAt = Date.now();
|
||||
this.profile.version = version;
|
||||
this.io.save({ profile: this.profile });
|
||||
@@ -511,12 +547,12 @@ export class PreferencesManager extends EventEmitter<PreferencesManagerEvents> {
|
||||
newValue = resolvedValue;
|
||||
}
|
||||
|
||||
this.commit(key, newValue);
|
||||
const commitedRecord = this.commit(key, newValue);
|
||||
|
||||
const done = os.waiting();
|
||||
|
||||
try {
|
||||
await this.io.cloudSet({ key, scope: record[0], value: newValue });
|
||||
await this.io.cloudSet({ key, scope: record[0], value: newValue, meta: { modifiedAt: record[2].modifiedAt } });
|
||||
} catch (err) {
|
||||
done();
|
||||
|
||||
@@ -548,24 +584,24 @@ export class PreferencesManager extends EventEmitter<PreferencesManagerEvents> {
|
||||
|
||||
public renameProfile(name: string) {
|
||||
this.profile.name = name;
|
||||
// TODO: バックアップのキーが名前ベースであることを考えると名前=IDであるから、idも再生成する方が自然かもしれない
|
||||
// (将来名前ではなくIDをキーにするようになったとした場合、名前を変えたのにバックアップのキーが変わらないことになってしまう)
|
||||
this.save();
|
||||
}
|
||||
|
||||
public reloadProfile() {
|
||||
const newProfile = this.io.load();
|
||||
if (newProfile == null) return;
|
||||
const freshProfile = this.io.load();
|
||||
if (freshProfile == null) return;
|
||||
|
||||
this.profile = {
|
||||
...newProfile,
|
||||
preferences: normalizePreferences(newProfile.preferences, this.currentAccount),
|
||||
...freshProfile,
|
||||
preferences: normalizePreferences(freshProfile.preferences, this.currentAccount),
|
||||
};
|
||||
const states = this.genStates();
|
||||
for (const _key in states) {
|
||||
const key = _key as keyof PREF;
|
||||
this.rewriteRawState(key, states[key]);
|
||||
}
|
||||
|
||||
this.fetchCloudValues();
|
||||
}
|
||||
|
||||
public getPerPrefMenu<K extends keyof PREF>(key: K): MenuItem[] {
|
||||
@@ -615,6 +651,11 @@ export class PreferencesManager extends EventEmitter<PreferencesManagerEvents> {
|
||||
icon: 'ti ti-cloud-cog',
|
||||
text: i18n.ts.syncBetweenDevices,
|
||||
ref: sync,
|
||||
}, {
|
||||
type: 'divider',
|
||||
}, {
|
||||
type: 'label',
|
||||
text: i18n.ts.modifiedAt + ': ' + (this.getMatchedRecordOf(key)[2].modifiedAt ? new Date(this.getMatchedRecordOf(key)[2].modifiedAt!).toLocaleString() : '-'),
|
||||
}];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,7 +3,8 @@
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import { ref, watch } from 'vue';
|
||||
import { computed, ref, watch } from 'vue';
|
||||
import { mergeProfiles } from './manager.js';
|
||||
import type { PreferencesProfile } from './manager.js';
|
||||
import type { MenuItem } from '@/types/menu.js';
|
||||
import { copyToClipboard } from '@/utility/copy-to-clipboard.js';
|
||||
@@ -39,6 +40,28 @@ export function getPreferencesProfileMenu(): MenuItem[] {
|
||||
cloudBackup();
|
||||
} else {
|
||||
store.set('enablePreferencesAutoCloudBackup', false);
|
||||
|
||||
autoSyncEnabled.value = false;
|
||||
}
|
||||
});
|
||||
|
||||
const autoSyncEnabled = ref(store.s.enablePreferencesAutoCloudSync);
|
||||
|
||||
watch(autoSyncEnabled, async () => {
|
||||
if (autoSyncEnabled.value) {
|
||||
const confirm = await os.confirm({
|
||||
type: 'warning',
|
||||
title: i18n.ts._preferencesBackup.autoSyncAreYouSure,
|
||||
text: i18n.ts._preferencesBackup.autoSyncAreYouSure_description,
|
||||
});
|
||||
if (confirm.canceled) {
|
||||
autoSyncEnabled.value = false;
|
||||
return;
|
||||
}
|
||||
|
||||
store.set('enablePreferencesAutoCloudSync', true);
|
||||
} else {
|
||||
store.set('enablePreferencesAutoCloudSync', false);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -52,10 +75,42 @@ export function getPreferencesProfileMenu(): MenuItem[] {
|
||||
renameProfile();
|
||||
},
|
||||
}, {
|
||||
type: 'switch',
|
||||
icon: 'ti ti-cloud-up',
|
||||
text: i18n.ts._preferencesBackup.autoBackup,
|
||||
ref: autoBackupEnabled,
|
||||
type: 'parent',
|
||||
text: i18n.ts._preferencesBackup.backupAndSync,
|
||||
caption: i18n.ts.latestBackupAt + ': ' + (store.s.latestPreferencesBackupAt !== 0 ? new Date(store.s.latestPreferencesBackupAt).toLocaleString() : '-'),
|
||||
icon: 'ti ti-cloud',
|
||||
children: [{
|
||||
type: 'switch',
|
||||
icon: 'ti ti-cloud-up',
|
||||
text: i18n.ts._preferencesBackup.autoBackup,
|
||||
caption: i18n.ts._preferencesBackup.autoBackup_description,
|
||||
ref: autoBackupEnabled,
|
||||
}, {
|
||||
type: 'button',
|
||||
icon: 'ti ti-cloud-up',
|
||||
text: i18n.ts._preferencesBackup.forceBackup,
|
||||
disabled: computed(() => !autoBackupEnabled.value),
|
||||
action: () => {
|
||||
cloudBackup();
|
||||
},
|
||||
}, {
|
||||
type: 'divider',
|
||||
}, {
|
||||
type: 'switch',
|
||||
icon: 'ti ti-cloud-down',
|
||||
text: i18n.ts._preferencesBackup.autoSync,
|
||||
caption: i18n.ts._preferencesBackup.autoSync_description,
|
||||
ref: autoSyncEnabled,
|
||||
disabled: computed(() => !autoBackupEnabled.value),
|
||||
}, {
|
||||
type: 'button',
|
||||
icon: 'ti ti-cloud-down',
|
||||
text: i18n.ts._preferencesBackup.forceSync,
|
||||
disabled: computed(() => !autoSyncEnabled.value),
|
||||
action: () => {
|
||||
cloudSync();
|
||||
},
|
||||
}],
|
||||
}, {
|
||||
text: i18n.ts.export,
|
||||
icon: 'ti ti-download',
|
||||
@@ -139,17 +194,55 @@ function importProfile() {
|
||||
input.click();
|
||||
}
|
||||
|
||||
export async function cloudSync() {
|
||||
if ($i == null) return;
|
||||
|
||||
const cloudProfile = await misskeyApi('i/registry/get', {
|
||||
scope: ['client', 'preferences', 'backups'],
|
||||
key: prefer.profile.name,
|
||||
}) as PreferencesProfile | null;
|
||||
|
||||
if (cloudProfile == null) {
|
||||
if (_DEV_) console.log('no backuped profile found, skipping sync');
|
||||
return;
|
||||
}
|
||||
|
||||
if (_DEV_) console.log('backuped profile found, syncing', cloudProfile);
|
||||
|
||||
miLocalStorage.setItem('preferences', JSON.stringify(mergeProfiles(prefer.profile, cloudProfile)));
|
||||
|
||||
prefer.reloadProfile();
|
||||
|
||||
store.set('latestPreferencesSyncAt', Date.now());
|
||||
}
|
||||
|
||||
export async function cloudBackup() {
|
||||
if ($i == null) return;
|
||||
if (!canAutoBackup()) {
|
||||
throw new Error('cannot auto backup for this profile');
|
||||
}
|
||||
|
||||
let currentProfile = prefer.profile;
|
||||
|
||||
if (_DEV_) console.log('cloud backup', currentProfile);
|
||||
|
||||
const backupedProfile = await misskeyApi('i/registry/get', {
|
||||
scope: ['client', 'preferences', 'backups'],
|
||||
key: prefer.profile.name,
|
||||
}) as PreferencesProfile | null;
|
||||
|
||||
// 古い設定で新しいバックアップを上書きしないようにマージ
|
||||
if (backupedProfile != null) {
|
||||
currentProfile = mergeProfiles(currentProfile, backupedProfile);
|
||||
}
|
||||
|
||||
await misskeyApi('i/registry/set', {
|
||||
scope: ['client', 'preferences', 'backups'],
|
||||
key: prefer.profile.name,
|
||||
value: prefer.profile,
|
||||
value: currentProfile,
|
||||
});
|
||||
|
||||
store.set('latestPreferencesBackupAt', Date.now());
|
||||
}
|
||||
|
||||
export async function listCloudBackups() {
|
||||
@@ -186,7 +279,6 @@ export async function restoreFromCloudBackup() {
|
||||
|
||||
const select = await os.select({
|
||||
title: i18n.ts._preferencesBackup.selectBackupToRestore,
|
||||
text: 'ℹ️ ' + i18n.ts._preferencesProfile.shareSameProfileBetweenDevicesIsNotRecommended + ' ' + i18n.ts._preferencesProfile.useSyncBetweenDevicesOptionIfYouWantToSyncSetting,
|
||||
items: backups.map(backup => ({
|
||||
label: backup.name,
|
||||
value: backup.name,
|
||||
|
||||
@@ -110,6 +110,10 @@ export const store = markRaw(new Pizzax('base', {
|
||||
where: 'device',
|
||||
default: false,
|
||||
},
|
||||
enablePreferencesAutoCloudSync: {
|
||||
where: 'device',
|
||||
default: false,
|
||||
},
|
||||
showPreferencesAutoCloudBackupSuggestion: {
|
||||
where: 'device',
|
||||
default: true,
|
||||
@@ -118,6 +122,14 @@ export const store = markRaw(new Pizzax('base', {
|
||||
where: 'device',
|
||||
default: true,
|
||||
},
|
||||
latestPreferencesBackupAt: {
|
||||
where: 'device',
|
||||
default: 0,
|
||||
},
|
||||
latestPreferencesSyncAt: {
|
||||
where: 'device',
|
||||
default: 0,
|
||||
},
|
||||
}));
|
||||
|
||||
// TODO: 他のタブと永続化されたstateを同期
|
||||
|
||||
@@ -23,6 +23,7 @@ export interface MenuButton {
|
||||
danger?: boolean;
|
||||
active?: boolean | ComputedRef<boolean>;
|
||||
avatar?: Misskey.entities.User;
|
||||
disabled?: boolean | Ref<boolean>;
|
||||
action: MenuAction;
|
||||
}
|
||||
|
||||
|
||||
@@ -17,8 +17,6 @@ export type MisskeyEntity = {
|
||||
id: string;
|
||||
createdAt: string;
|
||||
_shouldInsertAd_?: boolean;
|
||||
_shouldAnimateIn_?: boolean;
|
||||
_shouldAnimateOut_?: boolean;
|
||||
};
|
||||
|
||||
type AbsEndpointType = {
|
||||
@@ -109,8 +107,6 @@ export class Paginator<
|
||||
private canFetchDetection: 'safe' | 'limit' | null = null;
|
||||
private aheadQueue: T[] = [];
|
||||
private useShallowRef: SRef;
|
||||
private itemRemovalDelay: number | false;
|
||||
private removalTimers = new Map<string, number>();
|
||||
|
||||
// 配列内の要素をどのような順序で並べるか
|
||||
// newest: 新しいものが先頭 (default)
|
||||
@@ -142,9 +138,6 @@ export class Paginator<
|
||||
|
||||
useShallowRef?: SRef;
|
||||
|
||||
// アイテム削除時にアニメーションを待つ時間 (ms)
|
||||
itemRemovalDelay?: number | false;
|
||||
|
||||
canSearch?: boolean;
|
||||
searchParamName?: keyof E['req'];
|
||||
}) {
|
||||
@@ -167,7 +160,6 @@ export class Paginator<
|
||||
this.noPaging = props.noPaging ?? false;
|
||||
this.offsetMode = props.offsetMode ?? false;
|
||||
this.canSearch = props.canSearch ?? false;
|
||||
this.itemRemovalDelay = props.itemRemovalDelay ?? false;
|
||||
this.searchParamName = props.searchParamName ?? 'search';
|
||||
|
||||
this.getNewestId = this.getNewestId.bind(this);
|
||||
@@ -199,7 +191,6 @@ export class Paginator<
|
||||
}
|
||||
|
||||
public async init(): Promise<void> {
|
||||
this.clearRemovalTimers();
|
||||
this.items.value = [];
|
||||
this.aheadQueue = [];
|
||||
this.queuedAheadItemsCount.value = 0;
|
||||
@@ -393,8 +384,6 @@ export class Paginator<
|
||||
|
||||
public prepend(item: T): void {
|
||||
if (this.items.value.some(x => x.id === item.id)) return;
|
||||
item._shouldAnimateIn_ = true;
|
||||
item._shouldAnimateOut_ = false;
|
||||
this.items.value.unshift(item);
|
||||
this.trim(false);
|
||||
if (this.useShallowRef) triggerRef(this.items);
|
||||
@@ -410,10 +399,6 @@ export class Paginator<
|
||||
|
||||
public releaseQueue(): void {
|
||||
if (this.aheadQueue.length === 0) return; // これやらないと余計なre-renderが走る
|
||||
for (const item of this.aheadQueue) {
|
||||
item._shouldAnimateIn_ = false; // 一気に入るときは挿入アニメーションさせない
|
||||
item._shouldAnimateOut_ = false;
|
||||
}
|
||||
this.unshiftItems(this.aheadQueue);
|
||||
this.aheadQueue = [];
|
||||
this.queuedAheadItemsCount.value = 0;
|
||||
@@ -422,43 +407,13 @@ export class Paginator<
|
||||
public removeItem(id: string): void {
|
||||
// TODO: queueからも消す
|
||||
|
||||
if (this.itemRemovalDelay === false) {
|
||||
const index = this.items.value.findIndex(x => x.id === id);
|
||||
if (index !== -1) {
|
||||
this.items.value.splice(index, 1);
|
||||
if (this.useShallowRef) triggerRef(this.items);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
const index = this.items.value.findIndex(x => x.id === id);
|
||||
if (index !== -1) {
|
||||
this.items.value.splice(index, 1);
|
||||
if (this.useShallowRef) triggerRef(this.items);
|
||||
|
||||
const item = this.items.value[index]!;
|
||||
item._shouldAnimateOut_ = true;
|
||||
if (this.useShallowRef) triggerRef(this.items);
|
||||
|
||||
if (this.removalTimers.has(id)) return;
|
||||
|
||||
this.removalTimers.set(id, window.setTimeout(() => {
|
||||
this.removalTimers.delete(id);
|
||||
const currentIndex = this.items.value.findIndex(x => x.id === id);
|
||||
if (currentIndex !== -1) {
|
||||
this.items.value.splice(currentIndex, 1);
|
||||
if (this.useShallowRef) triggerRef(this.items);
|
||||
}
|
||||
}, this.itemRemovalDelay + 20)); // アニメーション終了からやや余裕をもたせる
|
||||
}
|
||||
}
|
||||
|
||||
private clearRemovalTimers(): void {
|
||||
for (const timer of this.removalTimers.values()) {
|
||||
window.clearTimeout(timer);
|
||||
}
|
||||
this.removalTimers.clear();
|
||||
}
|
||||
|
||||
public updateItem(id: string, updater: (item: T) => T): void {
|
||||
// TODO: queueのも更新
|
||||
|
||||
|
||||
@@ -5260,6 +5260,10 @@ export interface Locale extends ILocale {
|
||||
* 変更あり
|
||||
*/
|
||||
"modified": string;
|
||||
/**
|
||||
* 変更日時
|
||||
*/
|
||||
"modifiedAt": string;
|
||||
/**
|
||||
* 破棄
|
||||
*/
|
||||
@@ -5699,6 +5703,14 @@ export interface Locale extends ILocale {
|
||||
* スペースで区切るとAND指定になり、改行で区切るとOR指定になります。スラッシュで囲むと正規表現になります。一致した場合、サムネイルが表示されなくなります。
|
||||
*/
|
||||
"urlPreviewSensitiveListDescription": string;
|
||||
/**
|
||||
* 最終バックアップ
|
||||
*/
|
||||
"latestBackupAt": string;
|
||||
/**
|
||||
* 最終同期
|
||||
*/
|
||||
"latestSyncAt": string;
|
||||
"_imageEditing": {
|
||||
"_vars": {
|
||||
/**
|
||||
@@ -6303,10 +6315,18 @@ export interface Locale extends ILocale {
|
||||
"useSyncBetweenDevicesOptionIfYouWantToSyncSetting": string;
|
||||
};
|
||||
"_preferencesBackup": {
|
||||
/**
|
||||
* バックアップと同期
|
||||
*/
|
||||
"backupAndSync": string;
|
||||
/**
|
||||
* 自動バックアップ
|
||||
*/
|
||||
"autoBackup": string;
|
||||
/**
|
||||
* 設定を自動でサーバーに保存し、いつでも復元できるようにします
|
||||
*/
|
||||
"autoBackup_description": string;
|
||||
/**
|
||||
* バックアップから復元
|
||||
*/
|
||||
@@ -6336,9 +6356,29 @@ export interface Locale extends ILocale {
|
||||
*/
|
||||
"backupFound": string;
|
||||
/**
|
||||
* 設定の強制バックアップ
|
||||
* 今すぐバックアップ
|
||||
*/
|
||||
"forceBackup": string;
|
||||
/**
|
||||
* デバイス間同期
|
||||
*/
|
||||
"autoSync": string;
|
||||
/**
|
||||
* サーバーに保存された設定を自動で取得し、別のデバイスでの変更と同期できるようにします
|
||||
*/
|
||||
"autoSync_description": string;
|
||||
/**
|
||||
* 今すぐ同期
|
||||
*/
|
||||
"forceSync": string;
|
||||
/**
|
||||
* デバイス間の同期をオンにしますか?
|
||||
*/
|
||||
"autoSyncAreYouSure": string;
|
||||
/**
|
||||
* 通信量が増えるため、他のデバイスとこのプロファイルを共有する予定がない場合はオンにしないでください。
|
||||
*/
|
||||
"autoSyncAreYouSure_description": string;
|
||||
};
|
||||
"_accountSettings": {
|
||||
/**
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"type": "module",
|
||||
"name": "misskey-js",
|
||||
"version": "2026.7.0-beta.4",
|
||||
"version": "2026.7.0-beta.6",
|
||||
"description": "Misskey SDK for JavaScript",
|
||||
"license": "MIT",
|
||||
"main": "./built/index.js",
|
||||
|
||||
Generated
+19
-11
@@ -27,8 +27,8 @@ importers:
|
||||
specifier: 5.2.1
|
||||
version: 5.2.1
|
||||
tar:
|
||||
specifier: 7.5.20
|
||||
version: 7.5.20
|
||||
specifier: 7.5.21
|
||||
version: 7.5.21
|
||||
devDependencies:
|
||||
'@eslint/js':
|
||||
specifier: 9.39.5
|
||||
@@ -192,8 +192,8 @@ importers:
|
||||
specifier: 0.20.1
|
||||
version: 0.20.1(@opentelemetry/api@1.9.1)
|
||||
'@fastify/static':
|
||||
specifier: 10.1.0
|
||||
version: 10.1.0
|
||||
specifier: 10.1.2
|
||||
version: 10.1.2
|
||||
'@kitajs/html':
|
||||
specifier: 4.2.13
|
||||
version: 4.2.13
|
||||
@@ -673,6 +673,9 @@ importers:
|
||||
'@misskey-dev/emoji-data':
|
||||
specifier: 17.0.3
|
||||
version: 17.0.3
|
||||
'@misskey-dev/tagcanvas-es':
|
||||
specifier: 0.1.2
|
||||
version: 0.1.2
|
||||
'@sentry/vue':
|
||||
specifier: 10.65.0
|
||||
version: 10.65.0(vue@3.5.39(typescript@6.0.2))
|
||||
@@ -1952,8 +1955,8 @@ packages:
|
||||
'@fastify/send@4.1.0':
|
||||
resolution: {integrity: sha512-TMYeQLCBSy2TOFmV95hQWkiTYgC/SEx7vMdV+wnZVX4tt8VBLKzmH8vV9OzJehV0+XBfg+WxPMt5wp+JBUKsVw==}
|
||||
|
||||
'@fastify/static@10.1.0':
|
||||
resolution: {integrity: sha512-iK/8TvRM/EgNOyQL+EpWu+x3aR6o4GWt+UI+27zmE7w6t/6d80mXqOtWLdEKQ13vL/g1Jry0ae2icj6GP7tGzA==}
|
||||
'@fastify/static@10.1.2':
|
||||
resolution: {integrity: sha512-G/g18cG9tLutT/OVyN1AIsHIl9L1UwmJ+S3dkyhVpplIx0nEMicd7RGQ+uJLyhKKF4a3tTcQydccn3Mop1fX+Q==}
|
||||
|
||||
'@file-type/xml@0.4.4':
|
||||
resolution: {integrity: sha512-NhCyXoHlVZ8TqM476hyzwGJ24+D5IPSaZhmrPj7qXnEVb3q6jrFzA3mM9TBpknKSI9EuQeGTKRg2DXGUwvBBoQ==}
|
||||
@@ -2447,6 +2450,9 @@ packages:
|
||||
'@misskey-dev/summaly@5.5.1':
|
||||
resolution: {integrity: sha512-CfpVtmFgltjNS1FAS5+bOluKAF/irZuQnUxAw66ydnp/uesiNNu62ltf9gEgwutT5S+itGtlnP87AOx6DDn/1A==}
|
||||
|
||||
'@misskey-dev/tagcanvas-es@0.1.2':
|
||||
resolution: {integrity: sha512-SGqyu30/CCQyIC95Zcmj3k1vYXwfX66/arsxpl1UUulUqvlRpTpuhB7kEAAps1lqXin0rQzXknb31EeveNKsaw==}
|
||||
|
||||
'@msgpackr-extract/msgpackr-extract-darwin-arm64@3.0.4':
|
||||
resolution: {integrity: sha512-LCkGo6JDfaBhgST7UpPWgNgLINpcpabaHfyz5OBx75nUYxBsaEPxjnyNjWpeb/xBup/682QnBfRBy2/LvPutZQ==}
|
||||
cpu: [arm64]
|
||||
@@ -8607,8 +8613,8 @@ packages:
|
||||
tar-stream@3.2.0:
|
||||
resolution: {integrity: sha512-ojzvCvVaNp6aOTFmG7jaRD0meowIAuPc3cMMhSgKiVWws1GyHbGd/xvnyuRKcKlMpt3qvxx6r0hreCNITP9hIg==}
|
||||
|
||||
tar@7.5.20:
|
||||
resolution: {integrity: sha512-9FcyK4PA6+WbzlTM9WhQm6vB5W7cP7dUiPsv1g7YDwEQnQ1CGpK3MGlKk/ITVWMk05kHZuBhmVhiv8LZoy/PFQ==}
|
||||
tar@7.5.21:
|
||||
resolution: {integrity: sha512-XdhtCvlMywwxpCW8YEq3lOXBJpUPTR2OHHcwLPO3HwsJqOHa2Ok/oJ7ruGzp+JrKoRPVCzJwAdEjqLW/vNRPHA==}
|
||||
engines: {node: '>=18'}
|
||||
|
||||
taskkill@5.0.0:
|
||||
@@ -10082,7 +10088,7 @@ snapshots:
|
||||
http-errors: 2.0.1
|
||||
mime: 3.0.0
|
||||
|
||||
'@fastify/static@10.1.0':
|
||||
'@fastify/static@10.1.2':
|
||||
dependencies:
|
||||
'@fastify/accept-negotiator': 2.0.1
|
||||
'@fastify/error': 4.2.0
|
||||
@@ -10505,6 +10511,8 @@ snapshots:
|
||||
optionalDependencies:
|
||||
fastify: 5.8.5
|
||||
|
||||
'@misskey-dev/tagcanvas-es@0.1.2': {}
|
||||
|
||||
'@msgpackr-extract/msgpackr-extract-darwin-arm64@3.0.4':
|
||||
optional: true
|
||||
|
||||
@@ -15920,7 +15928,7 @@ snapshots:
|
||||
nopt: 10.0.1
|
||||
proc-log: 7.0.0
|
||||
semver: 7.8.5
|
||||
tar: 7.5.20
|
||||
tar: 7.5.21
|
||||
tinyglobby: 0.2.17
|
||||
undici: 6.25.0
|
||||
which: 7.0.0
|
||||
@@ -17578,7 +17586,7 @@ snapshots:
|
||||
- bare-buffer
|
||||
- react-native-b4a
|
||||
|
||||
tar@7.5.20:
|
||||
tar@7.5.21:
|
||||
dependencies:
|
||||
'@isaacs/fs-minipass': 4.0.1
|
||||
chownr: 3.0.0
|
||||
|
||||
@@ -58,6 +58,10 @@ minimumReleaseAgeExclude:
|
||||
- "@opentelemetry/core@2.8.0"
|
||||
# Renovate security update: typeorm@1.1.0
|
||||
- typeorm@1.1.0
|
||||
# Renovate security update: @fastify/static@10.1.2
|
||||
- "@fastify/static@10.1.2"
|
||||
# Renovate security update: tar@7.5.21
|
||||
- tar@7.5.21
|
||||
overrides:
|
||||
'@aiscript-dev/aiscript-languageserver': '-'
|
||||
'bullmq>ioredis': 5.11.1
|
||||
|
||||
Reference in New Issue
Block a user