Compare commits

..

23 Commits

Author SHA1 Message Date
Sayamame-beans cc66d2c585 Merge branch 'develop' into copilot/add-user-mute-settings 2026-07-18 12:00:29 +09:00
kakkokari-gtyih cc418615ba fix 2026-07-08 09:05:41 +09:00
かっこかり 1565c0f844 Merge branch 'develop' into copilot/add-user-mute-settings 2026-07-08 08:59:15 +09:00
kakkokari-gtyih 411e5faedb Merge remote-tracking branch 'msky/develop' into copilot/add-user-mute-settings 2026-05-03 16:50:34 +09:00
かっこかり 066bf42fc5 Merge branch 'develop' into copilot/add-user-mute-settings 2026-04-02 11:30:10 +09:00
kakkokari-gtyih 368c40c653 Merge remote-tracking branch 'msky/develop' into copilot/add-user-mute-settings 2025-12-16 22:09:06 +09:00
かっこかり 68d8445fe7 Update CHANGELOG.md 2025-12-02 16:52:45 +09:00
kakkokari-gtyih 6d80f9ef2f Merge branch 'develop' into copilot/add-user-mute-settings 2025-12-02 14:19:48 +09:00
Sayamame-beans 4a16d7f354 Merge branch 'develop' into copilot/add-user-mute-settings 2025-11-27 22:58:26 +09:00
kakkokari-gtyih cdaf57d4c2 Update Changelog 2025-11-24 00:50:07 +09:00
kakkokari-gtyih 9fe09c041f Merge branch 'develop' into copilot/add-user-mute-settings 2025-11-24 00:49:31 +09:00
かっこかり 6ef38a4cf9 Merge branch 'develop' into copilot/add-user-mute-settings 2025-11-10 18:38:04 +09:00
かっこかり 855a652439 enhance(frontend): ミュート・ロール付与期間を任意の長さに設定できるように (#16766)
* enhance(frontend): ミュートの長さを自由に設定できるように

* enhance(frontend): ロールアサインの長さを自由に設定できるように

* Update Changelog
2025-11-10 15:45:57 +09:00
copilot-swe-agent[bot] 50bbc71098 Apply code review suggestions - use explicit null checks
Co-authored-by: kakkokari-gtyih <67428053+kakkokari-gtyih@users.noreply.github.com>
2025-11-09 15:03:53 +00:00
かっこかり 4762dfd5c7 Merge branch 'develop' into copilot/add-user-mute-settings 2025-11-09 13:05:32 +09:00
kakkokari-gtyih 3e6150174a Update Changelog 2025-11-08 15:19:56 +09:00
kakkokari-gtyih 6e2360e9b1 fix 2025-11-08 13:39:12 +09:00
かっこかり 7d1da29c48 enhance(frontend): implement mute setting dialog for enhanced mute (#16763)
* enhance(frontend): implement mute setting dialog for enhanced mute

* remove unnecessary defs

* fix description

* fix lint
2025-11-08 13:11:52 +09:00
kakkokari-gtyih 2b75b575ac build misskey-js with types 2025-11-08 11:56:42 +09:00
copilot-swe-agent[bot] 8d1b2f2089 Fix code review issues - update comments to English
Co-authored-by: syuilo <4439005+syuilo@users.noreply.github.com>
2025-11-07 12:11:46 +00:00
copilot-swe-agent[bot] 18ba94c909 Add test for timeline-only muting feature
Co-authored-by: syuilo <4439005+syuilo@users.noreply.github.com>
2025-11-07 12:09:26 +00:00
copilot-swe-agent[bot] 685847e1b6 Add mutingType field to Muting model and update related code
Co-authored-by: syuilo <4439005+syuilo@users.noreply.github.com>
2025-11-07 12:04:42 +00:00
copilot-swe-agent[bot] 1810d6e837 Initial plan 2025-11-07 11:52:31 +00:00
77 changed files with 3373 additions and 3085 deletions
+1 -1
View File
@@ -19,7 +19,7 @@
|---|---|---|---|
| `skills/context-budget/SKILL.md` | `skills/context-budget/SKILL.md` | ECC | description を日本語化、Misskey 固有メモを追記 |
| `commands/harness-audit.md` | `commands/harness-audit.md` | ECC | scripts 依存の自動採点を、Claude が `pnpm`/`git`/`grep` で手動採点する版に書き換え。Misskey 固有の評価軸 (SPDX / endpoint-list / migration / locales) を組み込み |
| `commands/quality-gate.md` | `commands/quality-gate.md` | ECC | 言語自動判定を排除し Misskey 固定 pipeline (`pnpm` + tsc + ESLint + Vitest) に。Prettier/Biome フェーズを削除 |
| `commands/quality-gate.md` | `commands/quality-gate.md` | ECC | 言語自動判定を排除し Misskey 固定 pipeline (`pnpm` + tsgo + ESLint + Vitest) に。Prettier/Biome フェーズを削除 |
### MIT License (full text)
+4 -4
View File
@@ -12,9 +12,9 @@ upstream path: commands/quality-gate.md
upstream license: MIT — https://github.com/affaan-m/everything-claude-code/blob/main/LICENSE
project-level notice: see .claude/THIRD_PARTY_LICENSES.md (Misskey 内サードパーティ一覧 + MIT 全文)
Imported into Misskey .claude/ on 2026-05-10. Pipeline 概念 (lint → typecheck → test) は upstream ECC 版から借用 (MIT)。実コマンド層は Misskey の pnpm + tsc + ESLint + Vitest に固定し、formatter (Prettier/Biome) フェーズは削除した。
Imported into Misskey .claude/ on 2026-05-10. Pipeline 概念 (lint → typecheck → test) は upstream ECC 版から借用 (MIT)。実コマンド層は Misskey の pnpm + tsgo + ESLint + Vitest に固定し、formatter (Prettier/Biome) フェーズは削除した。
note: 元 ECC 版は言語自動判定 + format/lint/type のジェネリック版だったが、Misskey 専用に pnpm + tsc + ESLint + Vitest の組み合わせに固定。重い test:e2e / test:fed は含まない (CI 側で実行される)。
note: 元 ECC 版は言語自動判定 + format/lint/type のジェネリック版だったが、Misskey 専用に pnpm + tsgo + ESLint + Vitest の組み合わせに固定。重い test:e2e / test:fed は含まない (CI 側で実行される)。
-->
# /quality-gate — Misskey 軽量品質ゲート
@@ -50,7 +50,7 @@ pnpm --filter frontend test
lint がまとめて失敗していて typecheck の結果だけ単独で見たい場合は、以下を個別に回す。**通常は不要** (lint の出力を読めば足りる):
```bash
pnpm --filter backend typecheck # tsc 単体
pnpm --filter backend typecheck # tsgo 単体
pnpm --filter frontend typecheck # vue-tsc 単体 (Vue SFC の型を見るため)
```
@@ -63,7 +63,7 @@ pnpm --filter backend lint
pnpm --filter backend test
```
`tsc` の出力を単独で見たい時のみ optional で `pnpm --filter backend typecheck` を別途回す。
`tsgo` の出力を単独で見たい時のみ optional で `pnpm --filter backend typecheck` を別途回す。
### Frontend scope
@@ -6,7 +6,7 @@ Misskey の backend は NestJS 11 + Fastify 5 + TypeORM 1 (PostgreSQL) + Redis
- **DI コンテナ**: NestJS の `@Injectable()` サービス + Repository (TypeORM) パターン
- **DI トークン**: [`@/di-symbols.js`](../../../../../packages/backend/src/di-symbols.ts) の `DI` から `@Inject(DI.xxx)` で注入
- **ビルド**: `rolldown -c``built/` にバンドル。型チェックは `tsc`
- **ビルド**: `rolldown -c``built/` にバンドル。型チェックは `tsgo`
## エンドポイント内での DI
@@ -236,7 +236,7 @@ pnpm --filter backend test:e2e
```bash
# 個別ファイルを高速にチェック
pnpm exec eslint --fix packages/backend/src/server/api/endpoints/<category>/<name>.ts
pnpm --filter backend typecheck # tsc --noEmit (backend のみ)
pnpm --filter backend typecheck # tsgo --noEmit (backend のみ)
# 一括 (PR 提出前)
pnpm --filter backend lint
-6
View File
@@ -9,9 +9,3 @@ reviews:
drafts: false
base_branches:
- "develop"
ignore_title_keywords:
- "New Crowdin updates"
ignore_usernames:
- "dependabot[bot]"
- "renovate[bot]"
- "github-actions[bot]"
@@ -317,7 +317,7 @@ function renderVisualizerSummaryTable(before: ReturnType<typeof collectVisualize
`</tr>`,
`</tbody>`,
`</table>`,
].join('\n');
];
}
function getChunkComparisonRows(keys: string[], before: Record<string, FileEntry>, after: Record<string, FileEntry>) {
@@ -71,6 +71,6 @@ jobs:
if: failure() && steps.load-pr-num.outputs.pr-number
with:
pr-number: ${{ steps.load-pr-num.outputs.pr-number }}
comment-tag: show_memory_diff
comment-tag: show_memory_diff_error
message: |
An error occurred while comparing backend memory usage. See [workflow logs](https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}) for details.
+6
View File
@@ -0,0 +1,6 @@
build:
misskey:
args:
- NODE_ENV=development
deploy:
- helm upgrade --install misskey chart --set image=${OKTETO_BUILD_MISSKEY_IMAGE} --set url="https://misskey-$(kubectl config view --minify -o jsonpath='{..namespace}').cloud.okteto.net" --set environment=development
+4 -4
View File
@@ -17,6 +17,8 @@
- Feat: コントロールパネルから二要素認証を解除できるように
- Feat: 条件に一致したURLプレビューのサムネイルを隠すことができるように
(Based on https://github.com/MisskeyIO/misskey/pull/214)
- Feat: ユーザーミュートの適用範囲から通知を除外できるように
- タイムラインや検索等でノートが見えないようにしつつ、通知は引き続き受け取れるように設定することができるようになりました
### Client
- 2025.4.0 以前の設定情報の移行処理が削除されました
@@ -30,14 +32,14 @@
- Fix: 一部の画像のみセンシティブなとき、ビューワー内で画像を切り替えるとセンシティブな画像がそのまま表示される問題を修正
- Fix: 一部の画像をビューワーで読み込んだ際に正しく表示されない問題を修正
- Enhance: タイムラインの読み込みパフォーマンスを改善
- Enhance: ミュートの付与期間を自由に設定できるように
- Enhance: ロールの付与期間を自由に設定できるように
- Fix: 「画像を新しいタブで開く」が機能しなくなっていた問題を修正
- Fix: デバイスタイプをスマートフォンに固定している状態で画面幅が広いとき、画面左上のアイコンが表示されない問題を修正
- Fix: チャットでIMEの変換を確定するEnterでメッセージが送信されてしまうことがある問題を修正
- Fix: 自分へのメンションに対する色分けで、判定が大文字/小文字を区別していた問題を修正
- Fix: いくつかのイベントリスナーが正しく解除されない問題を修正(メモリ使用量の改善)
- Fix: 非ログイン時トップページをスクロール操作できないことがある問題を修正
- Fix: ローカルユーザーへのホスト付きメンションが本文に含まれる指名ノートの作成時、投稿フォームにて、当該ユーザーが宛先に含まれていても正しく認識されない問題を修正
- Fix: ドライブの「このファイルからノートを作成」やギャラリーの「ノートで共有」、誕生日ウィジェットからのノート作成において、通常投稿の下書きが表示される問題を修正
### Server
- Feat: OpenTelemetryサポート
@@ -61,8 +63,6 @@
- Fix: ハッシュタグに関連するデータを更新する際のエラーハンドリングを修正
- Fix: Sentry 使用環境下にて、Misskey が発行した SQL クエリが span に含まれない問題を修正
- Fix: Sentry 使用環境下にて、外部送信リクエストへ `sentry-trace` / `baggage` ヘッダーが既定で付与されないように
- Fix: フォロワー限定投稿へのリプライをホーム投稿に出来る問題を修正
- Fix: ファイルをアップロードするAPIにて、処理終了後に一時ファイルが削除されないことがある問題を修正
## 2026.6.0
+4
View File
@@ -0,0 +1,4 @@
apiVersion: v2
name: misskey
version: 0.0.0
description: This chart is created for the purpose of previewing Pull Requests. Do not use this for production use.
+231
View File
@@ -0,0 +1,231 @@
#━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
# Misskey configuration
#━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
# ┌─────┐
#───┘ URL └─────────────────────────────────────────────────────
# Final accessible URL seen by a user.
# url: https://example.tld/
# ONCE YOU HAVE STARTED THE INSTANCE, DO NOT CHANGE THE
# URL SETTINGS AFTER THAT!
# ┌───────────────────────┐
#───┘ Port and TLS settings └───────────────────────────────────
#
# Misskey supports two deployment options for public.
#
# Option 1: With Reverse Proxy
#
# +----- https://example.tld/ ------------+
# +------+ |+-------------+ +----------------+|
# | User | ---> || Proxy (443) | ---> | Misskey (3000) ||
# +------+ |+-------------+ +----------------+|
# +---------------------------------------+
#
# You need to setup reverse proxy. (eg. nginx)
# You do not define 'https' section.
# Option 2: Standalone
#
# +- https://example.tld/ -+
# +------+ | +---------------+ |
# | User | ---> | | Misskey (443) | |
# +------+ | +---------------+ |
# +------------------------+
#
# You need to run Misskey as root.
# You need to set Certificate in 'https' section.
# To use option 1, uncomment below line.
port: 3000 # A port that your Misskey server should listen.
# To use option 2, uncomment below lines.
#port: 443
#https:
# # path for certification
# key: /etc/letsencrypt/live/example.tld/privkey.pem
# cert: /etc/letsencrypt/live/example.tld/fullchain.pem
# ┌──────────────────────────┐
#───┘ PostgreSQL configuration └────────────────────────────────
db:
host: localhost
port: 5432
# Database name
db: misskey
# Auth
user: example-misskey-user
pass: example-misskey-pass
# Whether disable Caching queries
#disableCache: true
# Extra Connection options
#extra:
# ssl: true
dbReplications: false
# You can configure any number of replicas here
#dbSlaves:
# -
# host:
# port:
# db:
# user:
# pass:
# -
# host:
# port:
# db:
# user:
# pass:
# ┌─────────────────────┐
#───┘ Redis configuration └─────────────────────────────────────
redis:
host: localhost
port: 6379
#family: 0 # 0=Both, 4=IPv4, 6=IPv6
#pass: example-pass
#prefix: example-prefix
#db: 1
#redisForPubsub:
# host: localhost
# port: 6379
# #family: 0 # 0=Both, 4=IPv4, 6=IPv6
# #pass: example-pass
# #prefix: example-prefix
# #db: 1
#redisForJobQueue:
# host: localhost
# port: 6379
# #family: 0 # 0=Both, 4=IPv4, 6=IPv6
# #pass: example-pass
# #prefix: example-prefix
# #db: 1
#redisForTimelines:
# host: redis
# port: 6379
# #family: 0 # 0=Both, 4=IPv4, 6=IPv6
# #pass: example-pass
# #prefix: example-prefix
# #db: 1
#redisForReactions:
# host: redis
# port: 6379
# #family: 0 # 0=Both, 4=IPv4, 6=IPv6
# #pass: example-pass
# #prefix: example-prefix
# #db: 1
# ┌───────────────────────────┐
#───┘ MeiliSearch configuration └─────────────────────────────
#meilisearch:
# host: localhost
# port: 7700
# apiKey: ''
# ssl: true
# index: ''
# ┌───────────────┐
#───┘ ID generation └───────────────────────────────────────────
# You can select the ID generation method.
# You don't usually need to change this setting, but you can
# change it according to your preferences.
# Available methods:
# aid ... Short, Millisecond accuracy
# aidx ... Millisecond accuracy
# meid ... Similar to ObjectID, Millisecond accuracy
# ulid ... Millisecond accuracy
# objectid ... This is left for backward compatibility
# ONCE YOU HAVE STARTED THE INSTANCE, DO NOT CHANGE THE
# ID SETTINGS AFTER THAT!
id: "aidx"
# ┌────────────────┐
#───┘ Error tracking └──────────────────────────────────────────
# Sentry is available for error tracking.
# See the Sentry documentation for more details on options.
#sentryForBackend:
# enableNodeProfiling: true
# options:
# dsn: 'https://examplePublicKey@o0.ingest.sentry.io/0'
#sentryForFrontend:
# vueIntegration:
# tracingOptions:
# trackComponents: true
# browserTracingIntegration:
# replayIntegration:
# options:
# dsn: 'https://examplePublicKey@o0.ingest.sentry.io/0'
# ┌─────────────────────┐
#───┘ Other configuration └─────────────────────────────────────
# Whether disable HSTS
#disableHsts: true
# Number of worker processes
#clusterLimit: 1
# Number of threads of extra thread pool for CPU-intensive tasks (per worker)
#threadPoolSize: 1
# Job concurrency per worker
# deliverJobConcurrency: 128
# inboxJobConcurrency: 16
# Job rate limiter
# deliverJobPerSec: 128
# inboxJobPerSec: 32
# Job attempts
# deliverJobMaxAttempts: 12
# inboxJobMaxAttempts: 8
# IP address family used for outgoing request (ipv4, ipv6 or dual)
#outgoingAddressFamily: ipv4
# Proxy for HTTP/HTTPS
#proxy: http://127.0.0.1:3128
#proxyBypassHosts: [
# 'example.com',
# '192.0.2.8'
#]
# Proxy for SMTP/SMTPS
#proxySmtp: http://127.0.0.1:3128 # use HTTP/1.1 CONNECT
#proxySmtp: socks4://127.0.0.1:1080 # use SOCKS4
#proxySmtp: socks5://127.0.0.1:1080 # use SOCKS5
# Media Proxy
#mediaProxy: https://example.com/proxy
allowedPrivateNetworks:
- '127.0.0.1/32'
# Upload or download file size limits (bytes)
#maxFileSize: 262144000
+8
View File
@@ -0,0 +1,8 @@
apiVersion: v1
kind: ConfigMap
metadata:
name: {{ include "misskey.fullname" . }}-configuration
data:
default.yml: |-
{{ .Files.Get "files/default.yml"|nindent 4 }}
url: {{ .Values.url }}
+47
View File
@@ -0,0 +1,47 @@
apiVersion: apps/v1
kind: Deployment
metadata:
name: {{ include "misskey.fullname" . }}
labels:
{{- include "misskey.labels" . | nindent 4 }}
spec:
selector:
matchLabels:
{{- include "misskey.selectorLabels" . | nindent 6 }}
replicas: 1
template:
metadata:
labels:
{{- include "misskey.selectorLabels" . | nindent 8 }}
spec:
containers:
- name: misskey
image: {{ .Values.image }}
env:
- name: NODE_ENV
value: {{ .Values.environment }}
volumeMounts:
- name: {{ include "misskey.fullname" . }}-configuration
mountPath: /misskey/.config
readOnly: true
ports:
- containerPort: 3000
- name: postgres
image: postgres:18-alpine
env:
- name: POSTGRES_USER
value: "example-misskey-user"
- name: POSTGRES_PASSWORD
value: "example-misskey-pass"
- name: POSTGRES_DB
value: "misskey"
ports:
- containerPort: 5432
- name: redis
image: redis:7-alpine
ports:
- containerPort: 6379
volumes:
- name: {{ include "misskey.fullname" . }}-configuration
configMap:
name: {{ include "misskey.fullname" . }}-configuration
+14
View File
@@ -0,0 +1,14 @@
apiVersion: v1
kind: Service
metadata:
name: {{ include "misskey.fullname" . }}
annotations:
dev.okteto.com/auto-ingress: "true"
spec:
type: ClusterIP
ports:
- port: 3000
protocol: TCP
name: http
selector:
{{- include "misskey.selectorLabels" . | nindent 4 }}
+62
View File
@@ -0,0 +1,62 @@
{{/*
Expand the name of the chart.
*/}}
{{- define "misskey.name" -}}
{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" }}
{{- end }}
{{/*
Create a default fully qualified app name.
We truncate at 63 chars because some Kubernetes name fields are limited to this (by the DNS naming spec).
If release name contains chart name it will be used as a full name.
*/}}
{{- define "misskey.fullname" -}}
{{- if .Values.fullnameOverride }}
{{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" }}
{{- else }}
{{- $name := default .Chart.Name .Values.nameOverride }}
{{- if contains $name .Release.Name }}
{{- .Release.Name | trunc 63 | trimSuffix "-" }}
{{- else }}
{{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" }}
{{- end }}
{{- end }}
{{- end }}
{{/*
Create chart name and version as used by the chart label.
*/}}
{{- define "misskey.chart" -}}
{{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" }}
{{- end }}
{{/*
Common labels
*/}}
{{- define "misskey.labels" -}}
helm.sh/chart: {{ include "misskey.chart" . }}
{{ include "misskey.selectorLabels" . }}
{{- if .Chart.AppVersion }}
app.kubernetes.io/version: {{ .Chart.AppVersion | quote }}
{{- end }}
app.kubernetes.io/managed-by: {{ .Release.Service }}
{{- end }}
{{/*
Selector labels
*/}}
{{- define "misskey.selectorLabels" -}}
app.kubernetes.io/name: {{ include "misskey.name" . }}
app.kubernetes.io/instance: {{ .Release.Name }}
{{- end }}
{{/*
Create the name of the service account to use
*/}}
{{- define "misskey.serviceAccountName" -}}
{{- if .Values.serviceAccount.create }}
{{- default (include "misskey.fullname" .) .Values.serviceAccount.name }}
{{- else }}
{{- default "default" .Values.serviceAccount.name }}
{{- end }}
{{- end }}
+3
View File
@@ -0,0 +1,3 @@
url: https://example.tld/
image: okteto.dev/misskey
environment: production
+5
View File
@@ -146,6 +146,9 @@ markAsSensitive: "センシティブとして設定"
unmarkAsSensitive: "センシティブを解除する"
enterFileName: "ファイル名を入力"
mute: "ミュート"
muteType: "ミュートする範囲"
muteTypeDescription: "ミュートを適用する範囲を設定できます。「タイムラインのみ」に設定すると、タイムラインや検索結果上からは見えなくなりますが、通知は受け取ります。"
muteTypeTimeline: "タイムラインのみ"
unmute: "ミュート解除"
renoteMute: "リノートをミュート"
renoteUnmute: "リノートのミュートを解除"
@@ -961,6 +964,7 @@ instanceDefaultLightTheme: "サーバーデフォルトのライトテーマ"
instanceDefaultDarkTheme: "サーバーデフォルトのダークテーマ"
instanceDefaultThemeDescription: "オブジェクト形式のテーマコードを記入します。"
mutePeriod: "ミュートする期限"
mutePeriodDescription: "期限はあくまで目安です。反映が数分遅れる場合があります。"
period: "期限"
indefinitely: "無期限"
tenMinutes: "10分"
@@ -1409,6 +1413,7 @@ youAreAdmin: "あなたは管理者です"
frame: "フレーム"
presets: "プリセット"
zeroPadding: "ゼロ埋め"
muteConfirm: "ミュートしますか?"
nothingToConfigure: "設定項目はありません"
viewRenotedChannel: "リノート先のチャンネルを見る"
previewingTheme: "テーマのプレビュー中"
+12 -12
View File
@@ -1,12 +1,12 @@
{
"name": "misskey",
"version": "2026.7.0-beta.1",
"version": "2026.7.0-alpha.6",
"codename": "nasubi",
"repository": {
"type": "git",
"url": "https://github.com/misskey-dev/misskey.git"
},
"packageManager": "pnpm@11.11.0",
"packageManager": "pnpm@11.9.0",
"workspaces": [
"packages/misskey-js",
"packages/i18n",
@@ -55,22 +55,22 @@
"esbuild": "0.28.1",
"execa": "9.6.1",
"ignore-walk": "9.0.0",
"js-yaml": "5.2.1",
"tar": "7.5.20"
"js-yaml": "5.2.0",
"tar": "7.5.19"
},
"devDependencies": {
"@eslint/js": "9.39.5",
"@eslint/js": "9.39.4",
"@misskey-dev/eslint-plugin": "2.1.0",
"@types/node": "26.1.1",
"@typescript-eslint/eslint-plugin": "8.63.0",
"@typescript-eslint/parser": "8.63.0",
"@typescript/native": "npm:typescript@7.0.2",
"@types/node": "26.0.1",
"@typescript-eslint/eslint-plugin": "8.62.0",
"@typescript-eslint/parser": "8.62.0",
"@typescript/native-preview": "7.0.0-dev.20260426.1",
"cross-env": "10.1.0",
"eslint": "9.39.5",
"eslint": "9.39.4",
"globals": "17.7.0",
"ncp": "2.0.0",
"pnpm": "11.11.0",
"pnpm": "11.9.0",
"start-server-and-test": "3.0.11",
"typescript": "npm:@typescript/typescript6@6.0.2"
"typescript": "5.9.3"
}
}
@@ -0,0 +1,16 @@
/*
* SPDX-FileCopyrightText: syuilo and misskey-project
* SPDX-License-Identifier: AGPL-3.0-only
*/
export class AddMutingType1762516776421 {
name = 'AddMutingType1762516776421'
async up(queryRunner) {
await queryRunner.query(`ALTER TABLE "muting" ADD "mutingType" varchar(128) NOT NULL DEFAULT 'all'`);
}
async down(queryRunner) {
await queryRunner.query(`ALTER TABLE "muting" DROP COLUMN "mutingType"`);
}
}
+45 -45
View File
@@ -18,11 +18,11 @@
"build": "rolldown -c",
"build:unit": "rolldown -c --sourcemap",
"build:e2e": "rolldown -c --e2e",
"build:tsc": "tsc -p tsconfig.json && tsc-alias -p tsconfig.json",
"build:tsc": "tsgo -p tsconfig.json && tsc-alias -p tsconfig.json",
"watch": "pnpm compile-config && node ./scripts/watch.mjs",
"restart": "pnpm build && pnpm start",
"dev": "pnpm compile-config && rolldown -c --watch",
"typecheck": "tsc --noEmit && tsc -p test --noEmit && tsc -p test-federation --noEmit",
"typecheck": "tsgo --noEmit && tsgo -p test --noEmit && tsgo -p test-federation --noEmit",
"eslint": "eslint --quiet \"{src,test-federation}/**/*.ts\"",
"lint": "pnpm typecheck && pnpm eslint",
"test": "pnpm build:unit && cross-env NODE_ENV=test pnpm compile-config && vitest --config vitest.config.unit.ts",
@@ -51,43 +51,43 @@
"utf-8-validate": "6.0.6"
},
"dependencies": {
"@aws-sdk/client-s3": "3.1085.0",
"@aws-sdk/lib-storage": "3.1085.0",
"@aws-sdk/client-s3": "3.1075.0",
"@aws-sdk/lib-storage": "3.1075.0",
"@fastify/accepts": "5.0.4",
"@fastify/cors": "11.3.0",
"@fastify/cors": "11.2.0",
"@fastify/http-proxy": "11.5.0",
"@fastify/multipart": "10.1.0",
"@fastify/otel": "0.20.1",
"@fastify/static": "10.1.0",
"@fastify/multipart": "10.0.0",
"@fastify/otel": "0.19.0",
"@fastify/static": "9.1.3",
"@kitajs/html": "4.2.13",
"@misskey-dev/emoji-assets": "17.0.3",
"@misskey-dev/emoji-data": "17.0.3",
"@misskey-dev/sharp-read-bmp": "1.3.1",
"@misskey-dev/summaly": "5.5.1",
"@napi-rs/canvas": "1.0.2",
"@nestjs/common": "11.1.28",
"@nestjs/core": "11.1.28",
"@nestjs/testing": "11.1.28",
"@napi-rs/canvas": "1.0.1",
"@nestjs/common": "11.1.27",
"@nestjs/core": "11.1.27",
"@nestjs/testing": "11.1.27",
"@opentelemetry/api": "1.9.1",
"@opentelemetry/core": "2.9.0",
"@opentelemetry/exporter-trace-otlp-proto": "0.220.0",
"@opentelemetry/core": "2.8.0",
"@opentelemetry/exporter-trace-otlp-proto": "0.214.0",
"@opentelemetry/instrumentation-pg": "0.72.0",
"@opentelemetry/resources": "2.9.0",
"@opentelemetry/sdk-trace-base": "2.9.0",
"@opentelemetry/sdk-trace-node": "2.9.0",
"@opentelemetry/semantic-conventions": "1.43.0",
"@oxc-project/runtime": "0.139.0",
"@opentelemetry/resources": "2.7.1",
"@opentelemetry/sdk-trace-base": "2.7.1",
"@opentelemetry/sdk-trace-node": "2.7.1",
"@opentelemetry/semantic-conventions": "1.40.0",
"@oxc-project/runtime": "0.137.0",
"@peertube/http-signature": "1.7.0",
"@sentry/node": "10.65.0",
"@sentry/profiling-node": "10.65.0",
"@sentry/node": "10.62.0",
"@sentry/profiling-node": "10.62.0",
"@simplewebauthn/server": "13.3.2",
"@smithy/node-http-handler": "4.9.5",
"@smithy/node-http-handler": "4.8.2",
"accepts": "1.3.8",
"ajv": "8.20.0",
"archiver": "8.0.0",
"bcryptjs": "3.0.3",
"blurhash": "2.0.5",
"bullmq": "5.80.2",
"bullmq": "5.79.2",
"cacheable-lookup": "7.0.0",
"chalk": "5.6.2",
"chalk-template": "1.1.2",
@@ -96,12 +96,12 @@
"content-disposition": "2.0.1",
"date-fns": "4.4.0",
"deep-email-validator": "0.1.27",
"fastify": "5.10.0",
"fastify": "5.9.0",
"fastify-raw-body": "5.0.0",
"feed": "6.0.0",
"feed": "5.2.1",
"file-type": "22.0.1",
"fluent-ffmpeg": "2.1.3",
"got": "15.1.0",
"got": "15.0.7",
"hpagent": "1.2.0",
"http-link-header": "1.1.3",
"i18n": "workspace:*",
@@ -112,17 +112,17 @@
"json5": "2.2.3",
"jsonld": "9.0.0",
"juice": "12.1.1",
"meilisearch": "0.59.0",
"meilisearch": "0.58.0",
"mfm-js": "0.26.0",
"mime-types": "3.0.2",
"misskey-js": "workspace:*",
"misskey-reversi": "workspace:*",
"ms": "3.0.0-canary.202508261828",
"nanoid": "6.0.0",
"nanoid": "5.1.16",
"nested-property": "4.0.0",
"node-fetch": "3.3.2",
"node-html-parser": "9.0.0",
"nodemailer": "9.0.3",
"node-html-parser": "8.0.3",
"nodemailer": "9.0.1",
"os-utils": "0.0.14",
"otpauth": "9.5.1",
"pg": "8.22.0",
@@ -132,21 +132,21 @@
"qrcode": "1.5.4",
"random-seed": "0.3.0",
"ratelimiter": "3.4.1",
"re2": "1.26.0",
"re2": "1.25.0",
"reflect-metadata": "0.2.2",
"rename": "1.0.4",
"rss-parser": "3.13.0",
"sanitize-html": "2.17.6",
"sanitize-html": "2.17.5",
"secure-json-parse": "4.1.0",
"semver": "7.8.5",
"sharp": "0.35.3",
"sharp": "0.35.2",
"slacc": "0.1.5",
"strict-event-emitter-types": "2.0.0",
"stringz": "2.1.0",
"systeminformation": "5.31.16",
"systeminformation": "5.31.11",
"tinycolor2": "1.6.0",
"tmp": "0.2.7",
"tsc-alias": "1.9.0",
"tsc-alias": "1.8.17",
"typeorm": "1.0.0",
"ulid": "3.0.2",
"vary": "1.1.2",
@@ -156,9 +156,9 @@
},
"devDependencies": {
"@kitajs/ts-html-plugin": "4.1.4",
"@nestjs/platform-express": "11.1.28",
"@nestjs/platform-express": "11.1.27",
"@rollup/plugin-esm-shim": "0.1.8",
"@sentry/vue": "10.65.0",
"@sentry/vue": "10.62.0",
"@sinonjs/fake-timers": "15.4.0",
"@types/accepts": "1.3.7",
"@types/archiver": "8.0.0",
@@ -167,7 +167,7 @@
"@types/jsonld": "1.5.15",
"@types/mime-types": "3.0.1",
"@types/ms": "2.1.0",
"@types/node": "26.1.1",
"@types/node": "26.0.1",
"@types/nodemailer": "8.0.1",
"@types/pg": "8.20.0",
"@types/qrcode": "1.5.6",
@@ -183,21 +183,21 @@
"@types/vary": "1.1.3",
"@types/web-push": "3.6.4",
"@types/ws": "8.18.1",
"@typescript-eslint/eslint-plugin": "8.63.0",
"@typescript-eslint/parser": "8.63.0",
"@vitest/coverage-v8": "4.1.10",
"@typescript-eslint/eslint-plugin": "8.62.0",
"@typescript-eslint/parser": "8.62.0",
"@vitest/coverage-v8": "4.1.9",
"aws-sdk-client-mock": "4.1.0",
"cbor2": "2.3.0",
"cross-env": "10.1.0",
"eslint-plugin-import": "2.32.0",
"execa": "9.6.1",
"fkill": "10.0.3",
"js-yaml": "5.2.1",
"js-yaml": "5.2.0",
"pid-port": "2.1.1",
"rolldown": "1.1.5",
"rolldown": "1.1.3",
"simple-oauth2": "5.1.0",
"vite": "8.1.4",
"vitest": "4.1.10",
"vite": "8.1.0",
"vitest": "4.1.9",
"vitest-mock-extended": "4.0.0"
}
}
+1 -1
View File
@@ -160,7 +160,7 @@ export default defineConfig((args) => {
clearScreen: false,
},
// ビルドの高速化のために、watchモードのときは外部モジュールは全てバンドルしないようにする
external: isWatchMode ? /^(?!@\/|\0)[^.\/](?!:[\/\\])/ : externalModules,
external: isWatchMode ? /^(?!@\/)[^.\/](?!:[\/\\])/ : externalModules,
};
}
});
+1 -1
View File
@@ -21,7 +21,7 @@ import { execa } from 'execa';
});
}, 3000);
execa('tsc', ['-w', '-p', 'tsconfig.json'], {
execa('tsgo', ['-w', '-p', 'tsconfig.json'], {
stdout: process.stdout,
stderr: process.stderr,
});
+7 -7
View File
@@ -5,7 +5,7 @@
import { Inject, Injectable } from '@nestjs/common';
import * as Redis from 'ioredis';
import type { BlockingsRepository, FollowingsRepository, MutingsRepository, RenoteMutingsRepository, MiUserProfile, UserProfilesRepository, UsersRepository, MiFollowing } from '@/models/_.js';
import type { BlockingsRepository, FollowingsRepository, MutingsRepository, RenoteMutingsRepository, MiUserProfile, UserProfilesRepository, UsersRepository, MiFollowing, MiMuting } from '@/models/_.js';
import { MemoryKVCache, RedisKVCache } from '@/misc/cache.js';
import type { MiLocalUser, MiUser } from '@/models/User.js';
import { DI } from '@/di-symbols.js';
@@ -21,7 +21,7 @@ export class CacheService implements OnApplicationShutdown {
public localUserByIdCache: MemoryKVCache<MiLocalUser>;
public uriPersonCache: MemoryKVCache<MiUser | null>;
public userProfileCache: RedisKVCache<MiUserProfile>;
public userMutingsCache: RedisKVCache<Set<string>>;
public userMutingsCache: RedisKVCache<Map<string, Pick<MiMuting, 'mutingType'>>>;
public userBlockingCache: RedisKVCache<Set<string>>;
public userBlockedCache: RedisKVCache<Set<string>>; // NOTE: 「被」Blockキャッシュ
public renoteMutingsCache: RedisKVCache<Set<string>>;
@@ -69,15 +69,15 @@ export class CacheService implements OnApplicationShutdown {
fromRedisConverter: (value) => JSON.parse(value), // TODO: date型の考慮
});
this.userMutingsCache = new RedisKVCache<Set<string>>(this.redisClient, 'userMutings', {
this.userMutingsCache = new RedisKVCache<Map<string, Pick<MiMuting, 'mutingType'>>>(this.redisClient, 'userMutings', {
lifetime: 1000 * 60 * 30, // 30m
memoryCacheLifetime: 1000 * 60, // 1m
fetcher: (key) => this.mutingsRepository.find({
where: { muterId: key },
select: { muteeId: true },
}).then(xs => new Set(xs.map(x => x.muteeId))),
toRedisConverter: (value) => JSON.stringify(Array.from(value)),
fromRedisConverter: (value) => new Set(JSON.parse(value)),
select: { muteeId: true, mutingType: true },
}).then(xs => new Map(xs.map(x => [x.muteeId, { mutingType: x.mutingType }]))),
toRedisConverter: (value) => JSON.stringify(Array.from(value.entries())),
fromRedisConverter: (value) => new Map(JSON.parse(value)),
});
this.userBlockingCache = new RedisKVCache<Set<string>>(this.redisClient, 'userBlocking', {
@@ -111,7 +111,7 @@ export class FanoutTimelineEndpointService {
if (ps.me) {
const me = ps.me;
const [
userIdsWhoMeMuting,
userIdsWhoMeMutingMap,
userIdsWhoMeMutingRenotes,
userIdsWhoBlockingMe,
userMutedInstances,
@@ -124,6 +124,8 @@ export class FanoutTimelineEndpointService {
this.channelMutingService.mutingChannelsCache.fetch(me.id),
]);
const userIdsWhoMeMuting = new Set(userIdsWhoMeMutingMap.keys());
const parentFilter = filter;
filter = (note) => {
if (isUserRelated(note, userIdsWhoBlockingMe, ps.ignoreAuthorFromBlock)) return false;
+12 -33
View File
@@ -520,11 +520,6 @@ export class NoteCreateService implements OnApplicationShutdown {
// specified / direct noteはreject
throw new Error('Renote target is not public or home');
}
// ローカルのみをRenoteしたらローカルのみにする
if (data.renote.localOnly && data.channel == null) {
data.localOnly = true;
}
}
// Check blocking
@@ -539,35 +534,19 @@ export class NoteCreateService implements OnApplicationShutdown {
}
}
if (data.reply) {
switch (data.reply.visibility) {
case 'public':
// public noteは無条件にreply可能
break;
case 'home':
// home noteはhome以下にreply可能
if (data.visibility === 'public') {
data.visibility = 'home';
}
break;
case 'followers':
// followers noteはfollowers以下にreply可能
if (data.visibility === 'public' || data.visibility === 'home') {
data.visibility = 'followers';
}
break;
case 'specified':
// specified / direct noteはspecifiedのみreply可能
if (data.visibility !== 'specified') {
data.visibility = 'specified';
}
break;
}
// 返信対象がpublicではないならhomeにする
if (data.reply && data.reply.visibility !== 'public' && data.visibility === 'public') {
data.visibility = 'home';
}
// ローカルのみにリプライしたらローカルのみにする
if (data.reply.localOnly && data.channel == null) {
data.localOnly = true;
}
// ローカルのみをRenoteしたらローカルのみにする
if (data.renote && data.renote.localOnly && data.channel == null) {
data.localOnly = true;
}
// ローカルのみにリプライしたらローカルのみにする
if (data.reply && data.reply.localOnly && data.channel == null) {
data.localOnly = true;
}
if (data.text) {
@@ -108,7 +108,8 @@ export class NotificationService implements OnApplicationShutdown {
}
const mutings = await this.cacheService.userMutingsCache.fetch(notifieeId);
if (mutings.has(notifierId)) {
const muting = mutings.get(notifierId);
if (muting != null && muting.mutingType === 'all') {
return null;
}
+4 -2
View File
@@ -307,14 +307,16 @@ export class SearchService {
}
const [
userIdsWhoMeMuting,
userIdsWhoMeMutingMap,
userIdsWhoBlockingMe,
] = me
? await Promise.all([
this.cacheService.userMutingsCache.fetch(me.id),
this.cacheService.userBlockedCache.fetch(me.id),
])
: [new Set<string>(), new Set<string>()];
: [new Map<string, { mutingType: 'all' | 'timelineOnly' }>(), new Set<string>()];
const userIdsWhoMeMuting = new Set(userIdsWhoMeMutingMap.keys());
const query = this.notesRepository.createQueryBuilder('note')
.innerJoinAndSelect('note.user', 'user')
@@ -24,12 +24,13 @@ export class UserMutingService {
}
@bindThis
public async mute(user: MiUser, target: MiUser, expiresAt: Date | null = null): Promise<void> {
public async mute(user: MiUser, target: MiUser, expiresAt: Date | null = null, mutingType: 'all' | 'timelineOnly' = 'all'): Promise<void> {
await this.mutingsRepository.insert({
id: this.idService.gen(),
expiresAt: expiresAt ?? null,
muterId: user.id,
muteeId: target.id,
mutingType: mutingType,
});
this.cacheService.userMutingsCache.refresh(user.id);
@@ -40,6 +40,7 @@ export class MutingEntityService {
id: muting.id,
createdAt: this.idService.parse(muting.id).date.toISOString(),
expiresAt: muting.expiresAt ? muting.expiresAt.toISOString() : null,
mutingType: muting.mutingType,
muteeId: muting.muteeId,
mutee: hints?.packedMutee ?? this.userEntityService.pack(muting.muteeId, me, {
schema: 'UserDetailedNotMe',
@@ -297,12 +297,13 @@ export class NotificationEntityService implements OnModuleInit {
*/
#validateNotifier <T extends MiNotification | MiGroupedNotification> (
notification: T,
userIdsWhoMeMuting: Set<MiUser['id']>,
userIdsWhoMeMutingMap: Map<MiUser['id'], { mutingType: 'all' | 'timelineOnly' }>,
userMutedInstances: Set<string>,
notifiers: MiUser[],
): boolean {
if (!('notifierId' in notification)) return true;
if (userIdsWhoMeMuting.has(notification.notifierId)) return false;
const muting = userIdsWhoMeMutingMap.get(notification.notifierId);
if (muting != null && muting.mutingType === 'all') return false;
const notifier = notifiers.find(x => x.id === notification.notifierId) ?? null;
@@ -332,7 +333,7 @@ export class NotificationEntityService implements OnModuleInit {
meId: MiUser['id'],
): Promise<T[]> {
const [
userIdsWhoMeMuting,
userIdsWhoMeMutingMap,
userMutedInstances,
] = await Promise.all([
this.cacheService.userMutingsCache.fetch(meId),
@@ -345,7 +346,7 @@ export class NotificationEntityService implements OnModuleInit {
}) : [];
const filteredNotifications = ((await Promise.all(notifications.map(async (notification) => {
const isValid = this.#validateNotifier(notification, userIdsWhoMeMuting, userMutedInstances, notifiers);
const isValid = this.#validateNotifier(notification, userIdsWhoMeMutingMap, userMutedInstances, notifiers);
return isValid ? notification : null;
}))) as [T | null] ).filter(x => x != null);
+6
View File
@@ -19,6 +19,12 @@ export class MiMuting {
})
public expiresAt: Date | null;
@Column('varchar', {
length: 128,
default: 'all',
})
public mutingType: 'all' | 'timelineOnly';
@Index()
@Column({
...id(),
@@ -22,6 +22,11 @@ export const packedMutingSchema = {
optional: false, nullable: true,
format: 'date-time',
},
mutingType: {
type: 'string',
optional: false, nullable: false,
enum: ['all', 'timelineOnly'],
},
muteeId: {
type: 'string',
optional: false, nullable: false,
@@ -199,54 +199,51 @@ export class ApiCallService implements OnApplicationShutdown {
reply.send();
return;
}
const [path, cleanup] = await createTemp();
await stream.pipeline(multipartData.file, fs.createWriteStream(path));
try {
await stream.pipeline(multipartData.file, fs.createWriteStream(path));
// ファイルサイズが制限を超えていた場合
// なお truncated はストリームを読み切ってからでないと機能しないため、stream.pipeline より後にある必要がある
if (multipartData.file.truncated) {
reply.code(413);
reply.send();
return;
}
const fields = {} as Record<string, unknown>;
for (const [k, v] of Object.entries(multipartData.fields)) {
fields[k] = typeof v === 'object' && 'value' in v ? v.value : undefined;
}
// https://datatracker.ietf.org/doc/html/rfc6750.html#section-2.1 (case sensitive)
const token = request.headers.authorization?.startsWith('Bearer ')
? request.headers.authorization.slice(7)
: fields['i'];
if (token != null && typeof token !== 'string') {
reply.code(400);
return;
}
return await this.telemetryService.startSpan('API: ' + endpoint.name, () => this.authenticateService.authenticate(token).then(([user, app]) => {
const call = this.call(endpoint, user, app, fields, {
name: multipartData.filename,
path: path,
}, request).then((res) => {
this.send(reply, res);
}).catch((err: ApiError) => {
this.#sendApiError(reply, err);
});
if (user) {
this.logIp(request, user);
}
return call;
}).catch(err => {
this.#sendAuthenticationError(reply, err);
}));
} finally {
// ファイルサイズが制限を超えていた場合
// なお truncated はストリームを読み切ってからでないと機能しないため、stream.pipeline より後にある必要がある
if (multipartData.file.truncated) {
cleanup();
reply.code(413);
reply.send();
return;
}
const fields = {} as Record<string, unknown>;
for (const [k, v] of Object.entries(multipartData.fields)) {
fields[k] = typeof v === 'object' && 'value' in v ? v.value : undefined;
}
// https://datatracker.ietf.org/doc/html/rfc6750.html#section-2.1 (case sensitive)
const token = request.headers.authorization?.startsWith('Bearer ')
? request.headers.authorization.slice(7)
: fields['i'];
if (token != null && typeof token !== 'string') {
reply.code(400);
return;
}
return await this.telemetryService.startSpan('API: ' + endpoint.name, () => this.authenticateService.authenticate(token).then(([user, app]) => {
const call = this.call(endpoint, user, app, fields, {
name: multipartData.filename,
path: path,
}, request).then((res) => {
this.send(reply, res);
}).catch((err: ApiError) => {
this.#sendApiError(reply, err);
});
if (user) {
this.logIp(request, user);
}
return call;
}).catch(err => {
this.#sendAuthenticationError(reply, err);
}));
}
@bindThis
@@ -55,6 +55,12 @@ export const paramDef = {
nullable: true,
description: 'A Unix Epoch timestamp that must lie in the future. `null` means an indefinite mute.',
},
mutingType: {
type: 'string',
enum: ['all', 'timelineOnly'],
default: 'all',
description: 'Type of muting. `all` mutes everything including notifications. `timelineOnly` mutes only timeline and search, but allows notifications.',
},
},
required: ['userId'],
} as const;
@@ -98,7 +104,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
return;
}
await this.userMutingService.mute(muter, mutee, ps.expiresAt ? new Date(ps.expiresAt) : null);
await this.userMutingService.mute(muter, mutee, ps.expiresAt ? new Date(ps.expiresAt) : null, ps.mutingType ?? 'all');
});
}
}
@@ -80,12 +80,14 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
}
const [
userIdsWhoMeMuting,
userIdsWhoMeMutingMap,
userIdsWhoBlockingMe,
] = me ? await Promise.all([
this.cacheService.userMutingsCache.fetch(me.id),
this.cacheService.userBlockedCache.fetch(me.id),
]) : [new Set<string>(), new Set<string>()];
]) : [new Map<string, { mutingType: 'all' | 'timelineOnly' }>(), new Set<string>()];
const userIdsWhoMeMuting = new Set(userIdsWhoMeMutingMap.keys());
const query = this.notesRepository.createQueryBuilder('note')
.where('note.id IN (:...noteIds)', { noteIds: noteIds })
@@ -73,10 +73,13 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
}
const [
userIdsWhoMeMuting,
userIdsWhoMeMutingMap,
] = me ? await Promise.all([
this.cacheService.userMutingsCache.fetch(me.id),
]) : [new Set<string>()];
]) : [new Map<string, { mutingType: 'all' | 'timelineOnly' }>()];
// Convert to Set for backward compatibility with isUserRelated
const userIdsWhoMeMuting = new Set(userIdsWhoMeMutingMap.keys());
const query = this.notesRepository.createQueryBuilder('note')
.where('note.id IN (:...noteIds)', { noteIds: noteIds })
@@ -94,7 +94,8 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
}
}
const userIdsWhoMeMuting = me ? await this.cacheService.userMutingsCache.fetch(me.id) : new Set<string>();
const userIdsWhoMeMutingMap = me ? await this.cacheService.userMutingsCache.fetch(me.id) : new Map<string, { mutingType: 'all' | 'timelineOnly' }>();
const userIdsWhoMeMuting = new Set(userIdsWhoMeMutingMap.keys());
const query = this.queryService.makePaginationQuery(this.noteReactionsRepository.createQueryBuilder('reaction'),
ps.sinceId, ps.untilId, ps.sinceDate, ps.untilDate)
@@ -59,6 +59,7 @@ export default class Connection {
public followingChannels: Set<string> = new Set();
public mutingChannels: Set<string> = new Set();
public userIdsWhoMeMuting: Set<string> = new Set();
public userIdsWhoMeMutingMap: Map<string, { mutingType: 'all' | 'timelineOnly' }> = new Map();
public userIdsWhoBlockingMe: Set<string> = new Set();
public userIdsWhoMeMutingRenotes: Set<string> = new Set();
public userMutedInstances: Set<string> = new Set();
@@ -85,7 +86,7 @@ export default class Connection {
following,
followingChannels,
mutingChannels,
userIdsWhoMeMuting,
userIdsWhoMeMutingMap,
userIdsWhoBlockingMe,
userIdsWhoMeMutingRenotes,
] = await Promise.all([
@@ -101,7 +102,8 @@ export default class Connection {
this.following = following;
this.followingChannels = followingChannels;
this.mutingChannels = mutingChannels;
this.userIdsWhoMeMuting = userIdsWhoMeMuting;
this.userIdsWhoMeMutingMap = userIdsWhoMeMutingMap;
this.userIdsWhoMeMuting = new Set(userIdsWhoMeMutingMap.keys());
this.userIdsWhoBlockingMe = userIdsWhoBlockingMe;
this.userIdsWhoMeMutingRenotes = userIdsWhoMeMutingRenotes;
this.userMutedInstances = new Set(userProfile.mutedInstances);
+1 -1
View File
@@ -139,7 +139,7 @@ services:
bash -c "
npm install -g pnpm
pnpm -F backend i --frozen-lockfile
pnpm exec tsc -p ./packages/backend/test-federation
pnpm exec tsgo -p ./packages/backend/test-federation
node ./packages/backend/test-federation/built/daemon.js
"
@@ -0,0 +1,131 @@
/*
* SPDX-FileCopyrightText: syuilo and misskey-project
* SPDX-License-Identifier: AGPL-3.0-only
*/
process.env.NODE_ENV = 'test';
import * as assert from 'assert';
import { describe, test, beforeAll } from 'vitest';
import { api, post, react, signup, waitFire } from '../utils.js';
import type * as misskey from 'misskey-js';
describe('Timeline-only Mute', () => {
// alice timeline-only mutes carol
let alice: misskey.entities.SignupResponse;
let bob: misskey.entities.SignupResponse;
let carol: misskey.entities.SignupResponse;
beforeAll(async () => {
alice = await signup({ username: 'alice' });
bob = await signup({ username: 'bob' });
carol = await signup({ username: 'carol' });
// Timeline-only mute: alice ==> carol
await api('mute/create', {
userId: carol.id,
mutingType: 'timelineOnly',
}, alice);
}, 1000 * 60 * 2);
test('タイムラインオンリーミュート作成', async () => {
const res = await api('mute/create', {
userId: bob.id,
mutingType: 'timelineOnly',
}, alice);
assert.strictEqual(res.status, 204);
// Clean up side effects so tests can run independently
await api('mute/delete', {
userId: bob.id,
}, alice);
});
describe('Timeline', () => {
test('タイムラインにtimelineOnlyミュートしているユーザーの投稿が含まれない', async () => {
const aliceNote = await post(alice, { text: 'hi' });
const bobNote = await post(bob, { text: 'hi' });
const carolNote = await post(carol, { text: 'hi' });
const res = await api('notes/local-timeline', {}, alice);
assert.strictEqual(res.status, 200);
assert.strictEqual(Array.isArray(res.body), true);
assert.strictEqual(res.body.some(note => note.id === aliceNote.id), true);
assert.strictEqual(res.body.some(note => note.id === bobNote.id), true);
assert.strictEqual(res.body.some(note => note.id === carolNote.id), false);
});
});
describe('Notification', () => {
test('timelineOnlyミュートしているユーザーからの通知は届く(リアクション)', async () => {
const aliceNote = await post(alice, { text: 'hi' });
await react(bob, aliceNote, 'like');
await react(carol, aliceNote, 'like');
const res = await api('i/notifications', {}, alice);
assert.strictEqual(res.status, 200);
assert.strictEqual(Array.isArray(res.body), true);
assert.strictEqual(res.body.some(notification => 'userId' in notification && notification.userId === bob.id), true);
// carol is timelineOnly muted, so notifications should still come through
assert.strictEqual(res.body.some(notification => 'userId' in notification && notification.userId === carol.id), true);
});
test('timelineOnlyミュートしているユーザーからのリプライ通知は届く', async () => {
const aliceNote = await post(alice, { text: 'hi' });
await post(bob, { text: '@alice hi', replyId: aliceNote.id });
await post(carol, { text: '@alice hi', replyId: aliceNote.id });
const res = await api('i/notifications', {}, alice);
assert.strictEqual(res.status, 200);
assert.strictEqual(Array.isArray(res.body), true);
assert.strictEqual(res.body.some(notification => 'userId' in notification && notification.userId === bob.id), true);
// carol is timelineOnly muted, so reply notifications should still come through
assert.strictEqual(res.body.some(notification => 'userId' in notification && notification.userId === carol.id), true);
});
test('timelineOnlyミュートしているユーザーからのメンション通知は届く', async () => {
await post(alice, { text: 'hi' });
await post(bob, { text: '@alice hi' });
await post(carol, { text: '@alice hi' });
const res = await api('i/notifications', {}, alice);
assert.strictEqual(res.status, 200);
assert.strictEqual(Array.isArray(res.body), true);
assert.strictEqual(res.body.some(notification => 'userId' in notification && notification.userId === bob.id), true);
// carol is timelineOnly muted, so mention notifications should still come through
assert.strictEqual(res.body.some(notification => 'userId' in notification && notification.userId === carol.id), true);
});
test('timelineOnlyミュートしているユーザーからの引用リノート通知は届く', async () => {
const aliceNote = await post(alice, { text: 'hi' });
await post(bob, { text: 'hi', renoteId: aliceNote.id });
await post(carol, { text: 'hi', renoteId: aliceNote.id });
const res = await api('i/notifications', {}, alice);
assert.strictEqual(res.status, 200);
assert.strictEqual(Array.isArray(res.body), true);
assert.strictEqual(res.body.some(notification => 'userId' in notification && notification.userId === bob.id), true);
// carol is timelineOnly muted, so quote notifications should still come through
assert.strictEqual(res.body.some(notification => 'userId' in notification && notification.userId === carol.id), true);
});
test('timelineOnlyミュートしているユーザーからメンションされても、ストリームに unreadNotification イベントが流れてくる', async () => {
// Reset state
await api('notifications/mark-all-as-read', {}, alice);
const fired = await waitFire(alice, 'main', () => post(carol, { text: '@alice hi' }), msg => msg.type === 'unreadNotification');
// carol is timelineOnly muted, so notification stream should fire
assert.strictEqual(fired, true);
});
});
});
+6 -6
View File
@@ -3,7 +3,7 @@
"type": "module",
"scripts": {
"eslint": "eslint './**/*.{js,jsx,ts,tsx}'",
"typecheck": "tsc --noEmit",
"typecheck": "tsgo --noEmit",
"lint": "pnpm typecheck && pnpm eslint"
},
"exports": {
@@ -11,16 +11,16 @@
},
"devDependencies": {
"@types/estree": "1.0.9",
"@types/node": "26.1.1",
"@typescript-eslint/eslint-plugin": "8.63.0",
"@typescript-eslint/parser": "8.63.0",
"@types/node": "26.0.1",
"@typescript-eslint/eslint-plugin": "8.62.0",
"@typescript-eslint/parser": "8.62.0",
"rollup": "4.62.2"
},
"dependencies": {
"i18n": "workspace:*",
"magic-string": "0.30.21",
"oxc-walker": "1.0.0",
"rolldown": "1.1.5",
"vite": "8.1.4"
"rolldown": "1.1.3",
"vite": "8.1.0"
}
}
+9 -10
View File
@@ -18,7 +18,7 @@
"mfm-js": "0.26.0",
"misskey-js": "workspace:*",
"punycode.js": "2.3.1",
"shiki": "4.3.1",
"shiki": "4.3.0",
"tinycolor2": "1.6.0",
"uuid": "14.0.1",
"vue": "3.5.39"
@@ -30,13 +30,13 @@
"@rollup/pluginutils": "5.4.0",
"@tabler/icons-webfont": "3.35.0",
"@testing-library/vue": "8.1.0",
"@types/node": "26.1.1",
"@types/node": "26.0.1",
"@types/punycode.js": "npm:@types/punycode@2.1.4",
"@types/tinycolor2": "1.4.6",
"@types/ws": "8.18.1",
"@typescript-eslint/eslint-plugin": "8.63.0",
"@typescript-eslint/parser": "8.63.0",
"@vitest/coverage-v8": "4.1.10",
"@typescript-eslint/eslint-plugin": "8.62.0",
"@typescript-eslint/parser": "8.62.0",
"@vitest/coverage-v8": "4.1.9",
"@vitejs/plugin-vue": "6.0.7",
"@vue/runtime-core": "3.5.39",
"eslint-plugin-import": "2.32.0",
@@ -44,11 +44,10 @@
"intersection-observer": "0.12.2",
"lightningcss": "1.32.0",
"sass-embedded": "1.100.0",
"tsx": "4.23.0",
"typescript": "6.0.2",
"vite": "8.1.4",
"vue-component-type-helpers": "3.3.7",
"tsx": "4.22.4",
"vite": "8.1.0",
"vue-component-type-helpers": "3.3.5",
"vue-eslint-parser": "10.4.1",
"vue-tsc": "3.3.7"
"vue-tsc": "3.3.5"
}
}
+5 -5
View File
@@ -4,14 +4,14 @@
"private": true,
"scripts": {
"eslint": "eslint './**/*.{js,jsx,ts,tsx}'",
"typecheck": "tsc --noEmit",
"typecheck": "tsgo --noEmit",
"lint": "pnpm typecheck && pnpm eslint"
},
"devDependencies": {
"@types/node": "26.1.1",
"@types/node": "26.0.1",
"@types/tinycolor2": "1.4.6",
"@typescript-eslint/eslint-plugin": "8.63.0",
"@typescript-eslint/parser": "8.63.0",
"@typescript-eslint/eslint-plugin": "8.62.0",
"@typescript-eslint/parser": "8.62.0",
"eslint-plugin-vue": "10.9.2",
"vue-eslint-parser": "10.4.1"
},
@@ -23,7 +23,7 @@
"i18n": "workspace:*",
"json5": "2.2.3",
"misskey-js": "workspace:*",
"shiki": "4.3.1",
"shiki": "4.3.0",
"tinycolor2": "1.6.0",
"vue": "3.5.39"
}
+25 -26
View File
@@ -6,7 +6,7 @@
"watch": "vite",
"build": "tsx build.ts",
"storybook-dev": "nodemon --verbose --watch src --ext \"mdx,ts,vue\" --ignore \"*.stories.ts\" --exec \"pnpm build-storybook-pre && pnpm exec storybook dev -p 6006 --ci\"",
"build-storybook-pre": "(tsc -p .storybook || echo done.) && node .storybook/generate.js && node .storybook/preload-locale.js && node .storybook/preload-theme.js",
"build-storybook-pre": "(tsgo -p .storybook || echo done.) && node .storybook/generate.js && node .storybook/preload-locale.js && node .storybook/preload-theme.js",
"build-storybook": "pnpm build-storybook-pre && storybook build --webpack-stats-json storybook-static",
"chromatic": "chromatic",
"test": "vitest --run --globals --config vitest.config.unit.ts",
@@ -22,7 +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",
"@sentry/vue": "10.65.0",
"@sentry/vue": "10.62.0",
"@simplewebauthn/browser": "13.3.0",
"@syuilo/aiscript": "1.2.1",
"@syuilo/aiscript-0-19-0": "npm:@syuilo/aiscript@^0.19.0",
@@ -34,7 +34,7 @@
"canvas-confetti": "1.9.4",
"chart.js": "4.5.1",
"chartjs-adapter-date-fns": "3.0.0",
"chartjs-chart-matrix": "3.0.5",
"chartjs-chart-matrix": "3.0.4",
"chartjs-plugin-gradient": "0.6.1",
"chartjs-plugin-zoom": "2.2.0",
"compare-versions": "6.1.1",
@@ -45,13 +45,13 @@
"frontend-shared": "workspace:*",
"i18n": "workspace:*",
"icons-subsetter": "workspace:*",
"idb-keyval": "6.3.0",
"idb-keyval": "6.2.5",
"insert-text-at-cursor": "0.3.0",
"ios-haptics": "0.1.5",
"is-file-animated": "1.0.2",
"json5": "2.2.3",
"matter-js": "0.20.0",
"mediabunny": "1.50.8",
"mediabunny": "1.49.0",
"mfm-js": "0.26.0",
"misskey-bubble-game": "workspace:*",
"misskey-js": "workspace:*",
@@ -59,9 +59,9 @@
"punycode.js": "2.3.1",
"qr-code-styling": "1.9.2",
"qr-scanner": "1.4.2",
"sanitize-html": "2.17.6",
"sanitize-html": "2.17.5",
"seedrandom": "3.0.5",
"shiki": "4.3.1",
"shiki": "4.3.0",
"textarea-caret": "3.1.0",
"throttle-debounce": "5.0.2",
"tinycolor2": "1.6.0",
@@ -77,7 +77,7 @@
"@rollup/pluginutils": "5.4.0",
"@storybook/addon-essentials": "8.6.18",
"@storybook/addon-interactions": "8.6.18",
"@storybook/addon-links": "10.5.0",
"@storybook/addon-links": "10.4.6",
"@storybook/addon-mdx-gfm": "8.6.18",
"@storybook/addon-storysource": "8.6.18",
"@storybook/blocks": "8.6.18",
@@ -88,8 +88,8 @@
"@storybook/test": "8.6.18",
"@storybook/theming": "8.6.18",
"@storybook/types": "8.6.18",
"@storybook/vue3": "10.5.0",
"@storybook/vue3-vite": "10.5.0",
"@storybook/vue3": "10.4.6",
"@storybook/vue3-vite": "10.4.6",
"@tabler/icons-webfont": "3.35.0",
"@testing-library/vue": "8.1.0",
"@types/canvas-confetti": "1.9.0",
@@ -97,20 +97,20 @@
"@types/insert-text-at-cursor": "0.3.2",
"@types/matter-js": "0.20.2",
"@types/micromatch": "4.0.10",
"@types/node": "26.1.1",
"@types/node": "26.0.1",
"@types/punycode.js": "npm:@types/punycode@2.1.4",
"@types/sanitize-html": "2.16.1",
"@types/seedrandom": "3.0.8",
"@types/textarea-caret": "3.0.4",
"@types/throttle-debounce": "5.0.2",
"@types/tinycolor2": "1.4.6",
"@typescript-eslint/eslint-plugin": "8.63.0",
"@typescript-eslint/parser": "8.63.0",
"@vitest/browser-playwright": "4.1.10",
"@vitest/coverage-v8": "4.1.10",
"@typescript-eslint/eslint-plugin": "8.62.0",
"@typescript-eslint/parser": "8.62.0",
"@vitest/browser-playwright": "4.1.9",
"@vitest/coverage-v8": "4.1.9",
"@vue/compiler-core": "3.5.39",
"astring": "1.9.0",
"chromatic": "18.0.1",
"chromatic": "17.7.2",
"eslint-plugin-import": "2.32.0",
"eslint-plugin-vue": "10.9.2",
"execa": "9.6.1",
@@ -119,28 +119,27 @@
"lightningcss": "1.32.0",
"micromatch": "4.0.8",
"minimatch": "10.2.5",
"msw": "2.15.0",
"msw": "2.14.6",
"msw-storybook-addon": "2.0.7",
"nodemon": "3.1.14",
"oxc-walker": "1.0.0",
"playwright": "1.61.1",
"prettier": "3.9.5",
"prettier": "3.9.1",
"react": "19.2.7",
"react-dom": "19.2.7",
"rolldown": "1.1.5",
"rolldown": "1.1.3",
"rollup-plugin-visualizer": "7.0.1",
"sass-embedded": "1.100.0",
"storybook": "10.5.0",
"storybook": "10.4.6",
"storybook-addon-misskey-theme": "github:misskey-dev/storybook-addon-misskey-theme",
"tsx": "4.23.0",
"typescript": "6.0.2",
"vite": "8.1.4",
"tsx": "4.22.4",
"vite": "8.1.0",
"vite-plugin-glsl": "1.6.0",
"vite-plugin-turbosnap": "1.0.3",
"vitest": "4.1.10",
"vitest": "4.1.9",
"vitest-fetch-mock": "0.4.5",
"vue-component-type-helpers": "3.3.7",
"vue-component-type-helpers": "3.3.5",
"vue-eslint-parser": "10.4.1",
"vue-tsc": "3.3.7"
"vue-tsc": "3.3.5"
}
}
@@ -127,6 +127,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<script lang="ts">
import * as Misskey from 'misskey-js';
import MkBlurhash from './MkBlurhash.vue';
type Size = {
width: number;
@@ -177,9 +178,8 @@ export function calculateSourceTransform({
</script>
<script lang="ts" setup>
import { computed, nextTick, ref, useTemplateRef, markRaw, watch, provide, onBeforeUnmount } from 'vue';
import MkVideoControl from '@/components/MkVideoControl.vue';
import MkBlurhash from '@/components/MkBlurhash.vue';
import { computed, nextTick, ref, useTemplateRef, markRaw, watch, provide } from 'vue';
import MkVideoControl from './MkVideoControl.vue';
import XFileInfo from './MkLightbox.item.fileinfo.vue';
import type { MenuItem } from '@/types/menu.js';
import { DI } from '@/di.js';
@@ -451,17 +451,6 @@ function onZoomGestureEnd() {
transform.value = clampedTransform;
}
/** 縦・横どちらのスワイプかを確定させるのに必要な、開始点からの移動量 (px) */
const AXIS_SWIPE_HYSTERISIS = 10;
/** スワイプが成立する速度 (px/ms) */
const MIN_VELOCITY_TO_SWIPE = 0.5;
/**縦スワイプで閉じるのに必要な移動量の、ビューポート高の1/3に対する比 */
const MIN_RATIO_TO_CLOSE = 0.4;
/** 横スワイプで前後のコンテンツに移動するのに必要な移動量 (px) */
const HORIZONTAL_SWIPE_DISTANCE_THRESHOLD = 150;
/** 速度を平均する時間窓 (ms) */
const VELOCITY_WINDOW = 100;
let isDragging = false;
let isClick = false;
let clickAction: 'hidden' | 'video' | null = null;
@@ -471,43 +460,12 @@ let currentPointerId: number | null = null;
let currentPointerStartOffset = { x: 0, y: 0 };
let isVerticalSwiping = false;
let isHorizontalSwiping = false;
let verticalSwipeDelta = 0;
let horizontalSwipeDelta = 0;
/** 軸が確定した時点のポインタ位置。ここを基準にスワイプ量を測ることで、確定時に描画が飛ぶのを防ぐ */
let swipeOrigin = { x: 0, y: 0 };
const pointerEventCache = new Map<number, PointerEvent>();
let pointerVec = { x: 0, y: 0 };
// 単発のpointermoveの増分から速度を求めると、表示のリフレッシュレートを超える頻度で
// pointermoveを発火する環境では値が暴れるため、直近VELOCITY_WINDOW msの平均を取る
let velocitySamples: { time: number; x: number; y: number }[] = [];
function pushVelocitySample(time: number, x: number, y: number) {
velocitySamples.push({ time, x, y });
while (velocitySamples.length > 2 && time - velocitySamples[0].time > VELOCITY_WINDOW) {
velocitySamples.shift();
}
}
function getVelocity(now: number): { x: number; y: number } {
if (velocitySamples.length < 2) return { x: 0, y: 0 };
const latest = velocitySamples[velocitySamples.length - 1];
// 指を止めたまま離した場合、pointermoveが発火しなくなる環境 (iOS・マウス) では直前のフリックの速度が
// 残り続けてしまい、静止状態で離したのにスワイプが成立してしまう。最後のサンプルが古ければ速度なしとして扱う
if (now - latest.time > VELOCITY_WINDOW) return { x: 0, y: 0 };
const oldest = velocitySamples[0];
const duration = latest.time - oldest.time;
if (duration <= 0) return { x: 0, y: 0 };
return {
x: (latest.x - oldest.x) / duration,
y: (latest.y - oldest.y) / duration,
};
}
function resolveClickAction(target: EventTarget | null): 'hidden' | 'video' | null {
if (!(target instanceof Element)) return null;
@@ -530,29 +488,22 @@ function onPointerdown(ev: PointerEvent) {
lastX = ev.clientX;
lastY = ev.clientY;
pointerVec = { x: 0, y: 0 };
velocitySamples = [];
horizontalSwipeDelta = 0;
// スワイプで閉じた直後はitemがv-showで残ったままなので (MkLightbox.vue参照)、閉じるアニメーション中に
// 触るとロック済みの軸が引き継がれてヒステリシスを経ずにスワイプが始まってしまう。ここで必ず解除する
isVerticalSwiping = false;
isHorizontalSwiping = false;
swipeOrigin = { x: ev.clientX, y: ev.clientY };
if (currentPointerId == null) {
currentPointerId = ev.pointerId;
currentPointerStartOffset = {
x: ev.clientX,
y: ev.clientY,
};
// pointerdown自体を最初のサンプルとして積む。これがないとpointermoveが1回しか発生しないような
// 素早いフリックでサンプルが1つしか溜まらず、速度が常に0と評価されてスワイプが成立しない
pushVelocitySample(ev.timeStamp, ev.clientX, ev.clientY);
}
}
let prevTwoTouchPointsDistance = 0;
let lastMoveTimeStamp = 0;
function onPointermove(ev: PointerEvent) {
const currentTime = ev.timeStamp;
const currentTime = performance.now();
const dt = currentTime - lastMoveTimeStamp;
lastMoveTimeStamp = currentTime;
if (pointerEventCache.size === 0) {
return;
@@ -596,35 +547,23 @@ function onPointermove(ev: PointerEvent) {
} else {
if (isVerticalSwiping) {
transform.value.y += deltaY;
verticalSwipeDelta += deltaY;
} else if (isHorizontalSwiping) {
horizontalSwipeDelta = ev.clientX - swipeOrigin.x;
horizontalSwipeDelta = ev.clientX - currentPointerStartOffset.x;
emit('horizontalSwipe', horizontalSwipeDelta);
} else {
// 軸を確定させるまでは、開始点からの累積移動量で毎回評価し直す。
const totalX = ev.clientX - currentPointerStartOffset.x;
const totalY = ev.clientY - currentPointerStartOffset.y;
const diff = Math.abs(totalX) - Math.abs(totalY);
if (diff !== 0) {
// 完全に同値の場合はどちらにもロックしない
const isVerticalVector = diff < 0;
const dominantDelta = isVerticalVector ? totalY : totalX;
// 意図が読み取れる程度に動いてからロックする
if (Math.abs(dominantDelta) >= AXIS_SWIPE_HYSTERISIS) {
swipeOrigin = { x: ev.clientX, y: ev.clientY };
if (isVerticalVector) {
isVerticalSwiping = true;
} else {
isHorizontalSwiping = true;
}
}
const isVerticalVector = Math.abs(deltaY) > Math.abs(deltaX);
if (isVerticalVector) {
isVerticalSwiping = true;
} else {
isHorizontalSwiping = true;
}
}
}
pushVelocitySample(currentTime, ev.clientX, ev.clientY);
pointerVec = getVelocity(currentTime);
if (dt > 0) {
pointerVec = { x: deltaX / dt, y: deltaY / dt };
}
lastX = ev.clientX;
lastY = ev.clientY;
@@ -642,20 +581,9 @@ function onPointerup(ev: PointerEvent) {
if (currentPointerId === ev.pointerId) {
currentPointerId = null;
// 離した時点で取り直す。指を止めたまま離した場合はここで速度が0になり、
// 直前のフリックの速度で誤ってスワイプが成立するのを防ぐことができる
pointerVec = getVelocity(ev.timeStamp);
// 判定にはジェスチャー開始点からの総移動量を使う。描画用のdelta (transform.y / horizontalSwipeDelta) は
// 軸が確定した地点を基準にしており、確定までのヒステリシス分と確定したpointermove自体の移動量を含まないため、
// pointermoveが1回しか発生しないような素早いフリックがそのまま握り潰されてしまう
const totalSwipeX = ev.clientX - currentPointerStartOffset.x;
const totalSwipeY = ev.clientY - currentPointerStartOffset.y;
if (isVerticalSwiping) {
const closeThreshold = (window.innerHeight / 3) * MIN_RATIO_TO_CLOSE;
const shouldCloseByUpwardSwipe = totalSwipeY < -closeThreshold || (totalSwipeY < 0 && pointerVec.y < -MIN_VELOCITY_TO_SWIPE); // の方で離された、またはに向かって強めに弾かれた
const shouldCloseByDownwardSwipe = totalSwipeY > closeThreshold || (totalSwipeY > 0 && pointerVec.y > MIN_VELOCITY_TO_SWIPE); // 下の方で離された、または下に向かって強めに弾かれた
const shouldCloseByUpwardSwipe = verticalSwipeDelta < -200 || (verticalSwipeDelta < 0 && pointerVec.y < -3); // 上の方で離された、または上に向かって強めに弾かれた
const shouldCloseByDownwardSwipe = verticalSwipeDelta > 200 || (verticalSwipeDelta > 0 && pointerVec.y > 3); // の方で離された、またはに向かって強めに弾かれた
if (shouldCloseByUpwardSwipe || shouldCloseByDownwardSwipe) {
closeThis();
return;
@@ -663,8 +591,8 @@ function onPointerup(ev: PointerEvent) {
resetToNeutral();
} else if (isHorizontalSwiping) {
const shouldNext = totalSwipeX < -HORIZONTAL_SWIPE_DISTANCE_THRESHOLD || (totalSwipeX < 0 && pointerVec.x < -MIN_VELOCITY_TO_SWIPE); // 左の方で離された、または左に向かって強めに弾かれた
const shouldPrev = totalSwipeX > HORIZONTAL_SWIPE_DISTANCE_THRESHOLD || (totalSwipeX > 0 && pointerVec.x > MIN_VELOCITY_TO_SWIPE); // 右の方で離された、または右に向かって強めに弾かれた
const shouldNext = horizontalSwipeDelta < -150 || (horizontalSwipeDelta < 0 && pointerVec.x < -1); // 左の方で離された、または左に向かって強めに弾かれた
const shouldPrev = horizontalSwipeDelta > 150 || (horizontalSwipeDelta > 0 && pointerVec.x > 1); // 右の方で離された、または右に向かって強めに弾かれた
if (shouldNext) {
emit('next');
} else if (shouldPrev) {
@@ -681,6 +609,8 @@ function onPointerup(ev: PointerEvent) {
}
const doubleTapDetector = makeDoubleTapDetector((ev) => {
ev.preventDefault();
ev.stopPropagation();
pointerVec = { x: 0, y: 0 };
if (isZooming.value) {
@@ -704,9 +634,8 @@ function cancelPointerGesture() {
isClick = false;
clickAction = null;
pointerVec = { x: 0, y: 0 };
velocitySamples = [];
verticalSwipeDelta = 0;
horizontalSwipeDelta = 0;
swipeOrigin = { x: 0, y: 0 };
isVerticalSwiping = false;
isHorizontalSwiping = false;
doubleTapDetector.reset();
@@ -892,12 +821,6 @@ function onDeactive() {
}
}
onBeforeUnmount(() => {
if (rafHandle) {
window.cancelAnimationFrame(rafHandle);
}
});
defineExpose({
onActive,
onDeactive,
@@ -67,7 +67,7 @@ const emit = defineEmits<{
}>();
const activatedIndexes = ref(new Set<number>());
const items = new Map<number, InstanceType<typeof XItem> | null>();
const items = new Map<number, InstanceType<typeof XItem>>();
const currentIndex = ref(props.defaultIndex ?? 0);
watch(currentIndex, (newIndex, oldIndex) => {
@@ -75,10 +75,10 @@ watch(currentIndex, (newIndex, oldIndex) => {
nextTick(() => {
if (oldIndex != null && items.has(oldIndex)) {
items.get(oldIndex)?.onDeactive();
items.get(oldIndex)!.onDeactive();
}
if (items.has(newIndex)) {
items.get(newIndex)?.onActive();
items.get(newIndex)!.onActive();
}
});
}, { immediate: true });
@@ -142,9 +142,8 @@ function closeGallery() {
}
function close() {
const item = items.get(currentIndex.value);
if (item != null) {
item.closeThis();
if (items.has(currentIndex.value)) {
items.get(currentIndex.value)!.closeThis();
} else {
closeGallery();
}
@@ -210,7 +209,6 @@ const keymap = {
} as const satisfies Keymap;
onBeforeUnmount(() => {
items.clear();
window.removeEventListener('resize', onResize);
window.removeEventListener('popstate', onPopState);
});
@@ -4,7 +4,7 @@ SPDX-License-Identifier: AGPL-3.0-only
-->
<template>
<div :class="[hide ? $style.hidden : $style.visible, (image.isSensitive && prefer.s.highlightSensitiveMedia) && $style.sensitive]" @pointerup="onClick" @contextmenu.stop="onContextmenu">
<div :class="[hide ? $style.hidden : $style.visible, (image.isSensitive && prefer.s.highlightSensitiveMedia) && $style.sensitive]" @click="onClick" @contextmenu.stop="onContextmenu">
<component
:is="disableImageLink ? 'div' : 'a'"
v-bind="disableImageLink ? {
@@ -61,8 +61,8 @@ SPDX-License-Identifier: AGPL-3.0-only
<div v-if="image.comment" :class="$style.indicator">ALT</div>
<div v-if="image.isSensitive" :class="$style.indicator" style="color: var(--MI_THEME-warn);" :title="i18n.ts.sensitive"><i class="ti ti-eye-exclamation"></i></div>
</div>
<button :class="[$style.menu, $style.menuBottom]" class="_button" @click.stop="showMenu" @pointerup.stop><i class="ti ti-dots" style="vertical-align: middle;" aria-hidden="true"></i></button>
<button :class="[$style.menu, $style.menuTop]" class="_button" @click.stop="hide = true" @pointerup.stop><i class="ti ti-eye-off" style="vertical-align: middle;" aria-hidden="true"></i></button>
<button :class="$style.menu" class="_button" @click.stop="showMenu"><i class="ti ti-dots" style="vertical-align: middle;"></i></button>
<i class="ti ti-eye-off" :class="$style.hide" @click.stop="hide = true"></i>
</template>
</div>
</template>
@@ -174,6 +174,23 @@ function onContextmenu(ev: PointerEvent) {
cursor: pointer;
}
.hide {
display: block;
position: absolute;
background-color: rgba(0, 0, 0, 0.3);
-webkit-backdrop-filter: var(--MI-blur, blur(15px));
backdrop-filter: var(--MI-blur, blur(15px));
border-radius: 0 0 0 9px;
color: #fff;
font-size: 12px;
opacity: .5;
padding: 5px 8px;
text-align: center;
cursor: pointer;
top: 0;
right: 0;
}
.hiddenTextWrapper {
display: table-cell;
text-align: center;
@@ -204,25 +221,16 @@ html[data-color-scheme=light] .visible {
background-color: rgba(0, 0, 0, 0.3);
-webkit-backdrop-filter: var(--MI-blur, blur(15px));
backdrop-filter: var(--MI-blur, blur(15px));
border-radius: 9px 0 0 0;
color: #fff;
font-size: 0.8em;
width: 28px;
height: 28px;
text-align: center;
}
.menuBottom {
border-radius: 8px 0 8px 0;
bottom: 0;
right: 0;
}
.menuTop {
border-radius: 0 8px 0 8px;
top: 0;
right: 0;
}
.imageContainer {
display: block;
overflow: hidden;
@@ -12,6 +12,7 @@ SPDX-License-Identifier: AGPL-3.0-only
(video.isSensitive && prefer.s.highlightSensitiveMedia) && $style.sensitive,
]"
@contextmenu.stop="onContextmenu"
@keydown.stop
>
<button v-if="hide" :class="$style.hidden" @click="reveal">
<div :class="$style.hiddenTextWrapper">
@@ -21,7 +22,7 @@ SPDX-License-Identifier: AGPL-3.0-only
</div>
</button>
<div v-else :class="$style.videoRoot" @pointerup="emit('mediaClick', $event)">
<div v-else :class="$style.videoRoot" @click="emit('mediaClick', $event)">
<img
v-if="video.thumbnailUrl"
:class="$style.video"
@@ -41,8 +42,8 @@ SPDX-License-Identifier: AGPL-3.0-only
<i class="ti ti-player-play"></i>
</div>
</div>
<button :class="[$style.menu, $style.menuBottom]" class="_button" @click.stop="showMenu" @pointerup.stop><i class="ti ti-dots" style="vertical-align: middle;" aria-hidden="true"></i></button>
<button :class="[$style.menu, $style.menuTop]" class="_button" @click.stop="hide = true" @pointerup.stop><i class="ti ti-eye-off" style="vertical-align: middle;" aria-hidden="true"></i></button>
<button :class="$style.menu" class="_button" @click.stop="showMenu"><i class="ti ti-dots" style="vertical-align: middle;"></i></button>
<i class="ti ti-eye-off" :class="$style.hide" @click.stop="hide = true"></i>
</div>
</div>
</template>
@@ -183,21 +184,29 @@ function onContextmenu(ev: PointerEvent) {
background-color: rgba(0, 0, 0, 0.3);
-webkit-backdrop-filter: var(--MI-blur, blur(15px));
backdrop-filter: var(--MI-blur, blur(15px));
border-radius: 9px 0 0 0;
color: #fff;
font-size: 0.8em;
width: 28px;
height: 28px;
text-align: center;
}
.menuBottom {
border-radius: 8px 0 8px 0;
bottom: 0;
right: 0;
}
.menuTop {
border-radius: 0 8px 0 8px;
.hide {
display: block;
position: absolute;
background-color: rgba(0, 0, 0, 0.3);
-webkit-backdrop-filter: var(--MI-blur, blur(15px));
backdrop-filter: var(--MI-blur, blur(15px));
border-radius: 0 0 0 9px;
color: #fff;
font-size: 12px;
opacity: .5;
padding: 5px 8px;
text-align: center;
cursor: pointer;
top: 0;
right: 0;
}
@@ -0,0 +1,217 @@
<!--
SPDX-FileCopyrightText: syuilo and misskey-project
SPDX-License-Identifier: AGPL-3.0-only
-->
<template>
<MkModal ref="modal" :preferType="'dialog'" :zPriority="'high'" @click="done(true)" @closed="emit('closed')">
<div :class="$style.root" class="_gaps_m">
<div class="_gaps_s">
<div :class="$style.header">
<div :class="$style.icon">
<i class="ti ti-alert-triangle"></i>
</div>
<div :class="$style.title">{{ i18n.ts.muteConfirm }}</div>
</div>
</div>
<div class="_gaps">
<FormSlot>
<div class="_gaps_s">
<MkSelect v-model="periodModel" :items="periodDef">
<template #label>{{ i18n.ts.mutePeriod }}</template>
</MkSelect>
<MkInput
v-if="periodModel === 'custom'"
v-model="manualExpiresAt"
type="datetime-local"
></MkInput>
</div>
<template #caption>{{ i18n.ts.mutePeriodDescription }}</template>
</FormSlot>
<MkSelect v-if="withMuteType" v-model="muteTypeModel" :items="muteTypeDef">
<template #label>{{ i18n.ts.muteType }}</template>
<template #caption>{{ i18n.ts.muteTypeDescription }}</template>
</MkSelect>
</div>
<div :class="$style.buttons">
<MkButton inline rounded @click="cancel">{{ i18n.ts.cancel }}</MkButton>
<MkButton inline primary rounded :disabled="!canSave" @click="ok">{{ i18n.ts.ok }}</MkButton>
</div>
</div>
</MkModal>
</template>
<script lang="ts">
import { i18n } from '@/i18n.js';
import type { MkSelectItem, GetMkSelectValueTypesFromDef } from '@/components/MkSelect.vue';
import MkInput from './MkInput.vue';
const periodItems = [{
value: 'indefinitely', label: i18n.ts.indefinitely,
}, {
value: 'tenMinutes', label: i18n.ts.tenMinutes,
}, {
value: 'oneHour', label: i18n.ts.oneHour,
}, {
value: 'oneDay', label: i18n.ts.oneDay,
}, {
value: 'oneWeek', label: i18n.ts.oneWeek,
}, {
value: 'custom', label: i18n.ts.custom,
}] as const satisfies MkSelectItem[];
const muteTypeItems = [{
value: 'all', label: i18n.ts.all,
}, {
value: 'timelineOnly', label: i18n.ts.muteTypeTimeline,
}] as const satisfies MkSelectItem[];
export type MkMuteSettingDialogDoneEvent = { canceled: true } | { canceled: false, expiresAt: number | null, type: GetMkSelectValueTypesFromDef<typeof muteTypeItems> };
</script>
<script lang="ts" setup>
import { onBeforeUnmount, onMounted, useTemplateRef, ref, computed } from 'vue';
import FormSlot from '@/components/form/slot.vue';
import MkModal from '@/components/MkModal.vue';
import MkButton from '@/components/MkButton.vue';
import MkSelect from '@/components/MkSelect.vue';
import { useMkSelect } from '@/composables/use-mkselect.js';
withDefaults(defineProps<{
withMuteType?: boolean;
}>(), {
withMuteType: false,
});
const emit = defineEmits<{
(ev: 'done', v: MkMuteSettingDialogDoneEvent): void;
(ev: 'closed'): void;
}>();
const modal = useTemplateRef('modal');
const {
def: periodDef,
model: periodModel,
} = useMkSelect({
items: periodItems,
initialValue: 'indefinitely',
});
const now = Date.now();
const manualExpiresAt = ref<string | null>(null);
const canSave = computed(() => {
if (periodModel.value === 'custom') {
return manualExpiresAt.value != null && new Date(manualExpiresAt.value).getTime() > now;
}
return true;
});
const {
def: muteTypeDef,
model: muteTypeModel,
} = useMkSelect({
items: muteTypeItems,
initialValue: 'all',
});
// overload function を使いたいので lint エラーを無視する
function done(canceled: true): void;
function done(canceled: false, period: typeof periodModel.value, type: typeof muteTypeModel.value): void; // eslint-disable-line no-redeclare
function done(canceled: boolean, period?: typeof periodModel.value, type?: typeof muteTypeModel.value) { // eslint-disable-line no-redeclare
const expiresAt = (() => {
if (canceled) return null;
if (period === 'custom' && manualExpiresAt.value != null) {
return new Date(manualExpiresAt.value!).getTime();
}
const now = Date.now();
switch (period) {
case 'indefinitely':
return null;
case 'tenMinutes':
return now + 10 * 60 * 1000;
case 'oneHour':
return now + 60 * 60 * 1000;
case 'oneDay':
return now + 24 * 60 * 60 * 1000;
case 'oneWeek':
return now + 7 * 24 * 60 * 60 * 1000;
default:
return null;
}
})();
if (canceled) {
emit('done', { canceled: true });
} else {
emit('done', { canceled: false, expiresAt, type: type! });
}
modal.value?.close();
}
async function ok() {
done(false, periodModel.value, muteTypeModel.value);
}
function cancel() {
done(true);
}
/*
function onBgClick() {
if (props.cancelableByBgClick) cancel();
}
*/
function onKeydown(evt: KeyboardEvent) {
if (evt.key === 'Escape') cancel();
}
onMounted(() => {
window.document.addEventListener('keydown', onKeydown);
});
onBeforeUnmount(() => {
window.document.removeEventListener('keydown', onKeydown);
});
</script>
<style lang="scss" module>
.root {
position: relative;
margin: auto;
padding: 32px;
width: 100%;
min-width: 320px;
max-width: 480px;
box-sizing: border-box;
background: var(--MI_THEME-panel);
border-radius: 16px;
}
.header {
display: flex;
align-items: center;
gap: 0.75em;
}
.icon {
font-size: 18px;
color: var(--MI_THEME-warn);
}
.title {
font-weight: bold;
font-size: 1.1em;
}
.buttons {
display: flex;
gap: 8px;
flex-wrap: wrap;
justify-content: right;
}
</style>
@@ -0,0 +1,187 @@
<!--
SPDX-FileCopyrightText: syuilo and misskey-project
SPDX-License-Identifier: AGPL-3.0-only
-->
<template>
<MkModal ref="modal" :preferType="'dialog'" :zPriority="'high'" @click="done(true)" @closed="emit('closed')">
<div :class="$style.root">
<div v-if="title" class="_selectable" :class="$style.header">
<Mfm :text="title"/>
</div>
<div v-if="text" :class="$style.text" class="_selectable">
<Mfm :text="text"/>
</div>
<div class="_gaps_s">
<MkSelect v-model="periodModel" :items="periodDef"></MkSelect>
<MkInput
v-if="periodModel === 'custom'"
v-model="manualExpiresAt"
type="datetime-local"
></MkInput>
</div>
<div :class="$style.buttons">
<MkButton inline rounded @click="cancel">{{ i18n.ts.cancel }}</MkButton>
<MkButton inline primary rounded :disabled="!canSave" @click="ok">{{ i18n.ts.ok }}</MkButton>
</div>
</div>
</MkModal>
</template>
<script lang="ts">
import { i18n } from '@/i18n.js';
import type { MkSelectItem } from '@/components/MkSelect.vue';
import MkInput from './MkInput.vue';
const periodItems = [{
value: 'indefinitely', label: i18n.ts.indefinitely,
}, {
value: 'tenMinutes', label: i18n.ts.tenMinutes,
}, {
value: 'oneHour', label: i18n.ts.oneHour,
}, {
value: 'oneDay', label: i18n.ts.oneDay,
}, {
value: 'oneWeek', label: i18n.ts.oneWeek,
}, {
value: 'custom', label: i18n.ts.custom,
}] as const satisfies MkSelectItem[];
export type MkPeriodDialogDoneEvent = { canceled: true } | { canceled: false, expiresAt: number | null };
</script>
<script lang="ts" setup>
import { onBeforeUnmount, onMounted, useTemplateRef, ref, computed } from 'vue';
import MkModal from '@/components/MkModal.vue';
import MkButton from '@/components/MkButton.vue';
import MkSelect from '@/components/MkSelect.vue';
import { useMkSelect } from '@/composables/use-mkselect.js';
defineProps<{
title?: string;
text?: string;
}>();
const emit = defineEmits<{
(ev: 'done', v: MkPeriodDialogDoneEvent): void;
(ev: 'closed'): void;
}>();
const modal = useTemplateRef('modal');
const {
def: periodDef,
model: periodModel,
} = useMkSelect({
items: periodItems,
initialValue: 'indefinitely',
});
const now = Date.now();
const manualExpiresAt = ref<string | null>(null);
const canSave = computed(() => {
if (periodModel.value === 'custom') {
return manualExpiresAt.value != null && new Date(manualExpiresAt.value).getTime() > now;
}
return true;
});
// overload function を使いたいので lint エラーを無視する
function done(canceled: true): void;
function done(canceled: false, period: typeof periodModel.value): void; // eslint-disable-line no-redeclare
function done(canceled: boolean, period?: typeof periodModel.value) { // eslint-disable-line no-redeclare
const expiresAt = (() => {
if (canceled) return null;
if (period === 'custom' && manualExpiresAt.value != null) {
return new Date(manualExpiresAt.value!).getTime();
}
const now = Date.now();
switch (period) {
case 'indefinitely':
return null;
case 'tenMinutes':
return now + 10 * 60 * 1000;
case 'oneHour':
return now + 60 * 60 * 1000;
case 'oneDay':
return now + 24 * 60 * 60 * 1000;
case 'oneWeek':
return now + 7 * 24 * 60 * 60 * 1000;
default:
return null;
}
})();
if (canceled) {
emit('done', { canceled: true });
} else {
emit('done', { canceled: false, expiresAt });
}
modal.value?.close();
}
async function ok() {
done(false, periodModel.value);
}
function cancel() {
done(true);
}
/*
function onBgClick() {
if (props.cancelableByBgClick) cancel();
}
*/
function onKeydown(evt: KeyboardEvent) {
if (evt.key === 'Escape') cancel();
}
onMounted(() => {
window.document.addEventListener('keydown', onKeydown);
});
onBeforeUnmount(() => {
window.document.removeEventListener('keydown', onKeydown);
});
</script>
<style lang="scss" module>
.root {
position: relative;
margin: auto;
padding: 32px;
min-width: 320px;
max-width: 480px;
box-sizing: border-box;
background: var(--MI_THEME-panel);
border-radius: 16px;
}
.header {
margin: 0 0 8px 0;
text-align: center;
font-weight: bold;
font-size: 1.1em;
& + .text {
margin-top: 8px;
}
}
.text {
margin: 16px 0 0 0;
}
.buttons {
margin-top: 16px;
display: flex;
gap: 8px;
flex-wrap: wrap;
justify-content: center;
}
</style>
@@ -445,7 +445,7 @@ function checkMissingMention() {
const ast = mfm.parse(text.value);
for (const x of extractMentions(ast)) {
if (!visibleUsers.value.some(u => (u.username === x.username) && ((u.host === x.host) || (x.host === host && u.host == null)))) {
if (!visibleUsers.value.some(u => (u.username === x.username) && (u.host === x.host))) {
hasNotSpecifiedMentions.value = true;
return;
}
+14
View File
@@ -13,6 +13,7 @@ import type { Form, GetFormResultType } from '@/utility/form.js';
import type { MenuItem } from '@/types/menu.js';
import type { PostFormProps } from '@/types/post-form.js';
import type { UploaderFeatures } from '@/composables/use-uploader.js';
import type { MkPeriodDialogDoneEvent } from '@/components/MkPeriodDialog.vue';
import type { MkSelectItem } from '@/components/MkSelect.vue';
import type { OptionValue } from '@/types/option-value.js';
import type { MkDialogReturnType } from '@/components/MkDialog.vue';
@@ -493,6 +494,19 @@ export function select<C extends OptionValue, D extends C | null = null>(props:
});
}
export function selectPeriod(options: { title?: string } = {}): Promise<MkPeriodDialogDoneEvent> {
return new Promise((resolve) => {
const { dispose } = popup(defineAsyncComponent(() => import('@/components/MkPeriodDialog.vue')), {
title: options.title,
}, {
done: result => {
resolve(result ? result : { canceled: true });
},
closed: () => dispose(),
});
});
}
export function success(): Promise<void> {
return new Promise(resolve => {
const showing = ref(true);
+4 -22
View File
@@ -462,31 +462,13 @@ async function assignRole() {
});
if (canceled || roleId == null) return;
const { canceled: canceled2, result: period } = await os.select({
title: i18n.ts.period + ': ' + roles.find(r => r.id === roleId)!.name,
items: [{
value: 'indefinitely', label: i18n.ts.indefinitely,
}, {
value: 'oneHour', label: i18n.ts.oneHour,
}, {
value: 'oneDay', label: i18n.ts.oneDay,
}, {
value: 'oneWeek', label: i18n.ts.oneWeek,
}, {
value: 'oneMonth', label: i18n.ts.oneMonth,
}],
default: 'indefinitely',
const res = await os.selectPeriod({
title: `${i18n.ts.period}: ${roles.find(r => r.id === roleId)!.name}`,
});
if (canceled2) return;
const expiresAt = period === 'indefinitely' ? null
: period === 'oneHour' ? Date.now() + (1000 * 60 * 60)
: period === 'oneDay' ? Date.now() + (1000 * 60 * 60 * 24)
: period === 'oneWeek' ? Date.now() + (1000 * 60 * 60 * 24 * 7)
: period === 'oneMonth' ? Date.now() + (1000 * 60 * 60 * 24 * 30)
: null;
if (res.canceled) return;
await os.apiWithDialog('admin/roles/assign', { roleId, userId: user.value.id, expiresAt });
await os.apiWithDialog('admin/roles/assign', { roleId, userId: user.value.id, expiresAt: res.expiresAt });
refreshUser();
}
@@ -113,31 +113,12 @@ async function del() {
async function assign() {
const user = await os.selectUser({ includeSelf: true });
const { canceled: canceled2, result: period } = await os.select({
title: i18n.ts.period + ': ' + role.name,
items: [{
value: 'indefinitely', label: i18n.ts.indefinitely,
}, {
value: 'oneHour', label: i18n.ts.oneHour,
}, {
value: 'oneDay', label: i18n.ts.oneDay,
}, {
value: 'oneWeek', label: i18n.ts.oneWeek,
}, {
value: 'oneMonth', label: i18n.ts.oneMonth,
}],
default: 'indefinitely',
const res = await os.selectPeriod({
title: `${i18n.ts.period}: ${role.name}`,
});
if (canceled2) return;
if (res.canceled) return;
const expiresAt = period === 'indefinitely' ? null
: period === 'oneHour' ? Date.now() + (1000 * 60 * 60)
: period === 'oneDay' ? Date.now() + (1000 * 60 * 60 * 24)
: period === 'oneWeek' ? Date.now() + (1000 * 60 * 60 * 24 * 7)
: period === 'oneMonth' ? Date.now() + (1000 * 60 * 60 * 24 * 30)
: null;
await os.apiWithDialog('admin/roles/assign', { roleId: role.id, userId: user.id, expiresAt });
await os.apiWithDialog('admin/roles/assign', { roleId: role.id, userId: user.id, expiresAt: res.expiresAt });
//role.users.push(user);
usersPaginator.reload();
}
+5 -23
View File
@@ -81,6 +81,7 @@ import MkStreamingNotesTimeline from '@/components/MkStreamingNotesTimeline.vue'
import XChannelFollowButton from '@/components/MkChannelFollowButton.vue';
import * as os from '@/os.js';
import { misskeyApi } from '@/utility/misskey-api.js';
import { openMuteSettingDialog } from '@/utility/mute-confirm.js';
import { $i, iAmModerator } from '@/i.js';
import { i18n } from '@/i18n.js';
import { definePage } from '@/page.js';
@@ -195,33 +196,14 @@ async function mute() {
if (!channel.value) return;
const _channel = channel.value;
const { canceled, result: period } = await os.select({
title: i18n.ts.mutePeriod,
items: [{
value: 'indefinitely', label: i18n.ts.indefinitely,
}, {
value: 'tenMinutes', label: i18n.ts.tenMinutes,
}, {
value: 'oneHour', label: i18n.ts.oneHour,
}, {
value: 'oneDay', label: i18n.ts.oneDay,
}, {
value: 'oneWeek', label: i18n.ts.oneWeek,
}],
default: 'indefinitely',
const res = await openMuteSettingDialog({
withMuteType: false,
});
if (canceled) return;
const expiresAt = period === 'indefinitely' ? null
: period === 'tenMinutes' ? Date.now() + (1000 * 60 * 10)
: period === 'oneHour' ? Date.now() + (1000 * 60 * 60)
: period === 'oneDay' ? Date.now() + (1000 * 60 * 60 * 24)
: period === 'oneWeek' ? Date.now() + (1000 * 60 * 60 * 24 * 7)
: null;
if (res.canceled) return;
os.apiWithDialog('channels/mute/create', {
channelId: _channel.id,
expiresAt,
expiresAt: res.expiresAt,
}).then(() => {
_channel.isMuting = true;
});
@@ -123,7 +123,6 @@ function postThis() {
os.post({
initialFiles: [file.value],
instant: true,
});
}
@@ -126,7 +126,6 @@ function shareWithNote() {
if (!post.value) return;
os.post({
initialText: `${post.value.title} ${url}/gallery/${post.value.id}`,
instant: true,
});
}
@@ -131,7 +131,6 @@ export function getDriveFileMenu(file: Misskey.entities.DriveFile, folder?: Miss
icon: 'ti ti-pencil',
action: () => os.post({
initialFiles: [file],
instant: true,
}),
}, {
text: i18n.ts.copyUrl,
+10 -45
View File
@@ -18,6 +18,7 @@ import { notesSearchAvailable, canSearchNonLocalNotes } from '@/utility/check-pe
import { antennasCache, rolesCache, userListsCache } from '@/cache.js';
import { mainRouter } from '@/router.js';
import { genEmbedCode } from '@/utility/get-embed-code.js';
import { openMuteSettingDialog } from '@/utility/mute-confirm.js';
import { prefer } from '@/preferences.js';
import { getPluginHandlers } from '@/plugin.js';
@@ -34,33 +35,15 @@ export function getUserMenu(user: Misskey.entities.UserDetailed, router: Router
user.isMuted = false;
});
} else {
const { canceled, result: period } = await os.select({
title: i18n.ts.mutePeriod,
items: [{
value: 'indefinitely', label: i18n.ts.indefinitely,
}, {
value: 'tenMinutes', label: i18n.ts.tenMinutes,
}, {
value: 'oneHour', label: i18n.ts.oneHour,
}, {
value: 'oneDay', label: i18n.ts.oneDay,
}, {
value: 'oneWeek', label: i18n.ts.oneWeek,
}],
default: 'indefinitely',
const res = await openMuteSettingDialog({
withMuteType: true,
});
if (canceled) return;
const expiresAt = period === 'indefinitely' ? null
: period === 'tenMinutes' ? Date.now() + (1000 * 60 * 10)
: period === 'oneHour' ? Date.now() + (1000 * 60 * 60)
: period === 'oneDay' ? Date.now() + (1000 * 60 * 60 * 24)
: period === 'oneWeek' ? Date.now() + (1000 * 60 * 60 * 24 * 7)
: null;
if (res.canceled) return;
os.apiWithDialog('mute/create', {
userId: user.id,
expiresAt,
expiresAt: res.expiresAt,
mutingType: res.type,
}).then(() => {
user.isMuted = true;
});
@@ -324,31 +307,13 @@ export function getUserMenu(user: Misskey.entities.UserDetailed, router: Router
return roles.filter(r => r.target === 'manual').map(r => ({
text: r.name,
action: async () => {
const { canceled, result: period } = await os.select({
title: i18n.ts.period + ': ' + r.name,
items: [{
value: 'indefinitely', label: i18n.ts.indefinitely,
}, {
value: 'oneHour', label: i18n.ts.oneHour,
}, {
value: 'oneDay', label: i18n.ts.oneDay,
}, {
value: 'oneWeek', label: i18n.ts.oneWeek,
}, {
value: 'oneMonth', label: i18n.ts.oneMonth,
}],
default: 'indefinitely',
const res = await os.selectPeriod({
title: `${i18n.ts.period}: ${r.name}`,
});
if (canceled) return;
const expiresAt = period === 'indefinitely' ? null
: period === 'oneHour' ? Date.now() + (1000 * 60 * 60)
: period === 'oneDay' ? Date.now() + (1000 * 60 * 60 * 24)
: period === 'oneWeek' ? Date.now() + (1000 * 60 * 60 * 24 * 7)
: period === 'oneMonth' ? Date.now() + (1000 * 60 * 60 * 24 * 30)
: null;
if (res.canceled) return;
os.apiWithDialog('admin/roles/assign', { roleId: r.id, userId: user.id, expiresAt });
os.apiWithDialog('admin/roles/assign', { roleId: r.id, userId: user.id, expiresAt: res.expiresAt });
},
}));
},
@@ -0,0 +1,21 @@
/*
* SPDX-FileCopyrightText: syuilo and misskey-project
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { defineAsyncComponent } from 'vue';
import * as os from '@/os.js';
import type { MkMuteSettingDialogDoneEvent } from '@/components/MkMuteSettingDialog.vue';
export function openMuteSettingDialog(opts?: { withMuteType?: boolean }): Promise<MkMuteSettingDialogDoneEvent> {
return new Promise(resolve => {
const { dispose } = os.popup(defineAsyncComponent(() => import('@/components/MkMuteSettingDialog.vue')), opts ?? {}, {
done: result => {
resolve(result ? result : { canceled: true });
},
closed: () => {
dispose();
},
});
});
}
@@ -14,7 +14,7 @@ SPDX-License-Identifier: AGPL-3.0-only
</template>
</MkUserCardMini>
</MkA>
<button v-tooltip.noDelay="i18n.ts.note" class="_button" :class="$style.post" @click="os.post({initialText: `@${item.user.username}${item.user.host ? `@${item.user.host}` : ''} `, instant: true})">
<button v-tooltip.noDelay="i18n.ts.note" class="_button" :class="$style.post" @click="os.post({initialText: `@${item.user.username}${item.user.host ? `@${item.user.host}` : ''} `})">
<i class="ti-fw ti ti-confetti" :class="$style.postIcon"></i>
</button>
</div>
+1 -1
View File
@@ -100,7 +100,7 @@ async function buildSrc(): Promise<void> {
function buildDts(): Promise<unknown> {
return execa(
'tsc',
'tsgo',
[
'--project', 'tsconfig.json',
'--rootDir', 'src',
+6 -6
View File
@@ -20,7 +20,7 @@
"build": "tsx ./build.ts",
"watch": "nodemon -w package.json -e json --exec \"tsx ./build.ts --watch\"",
"tsd": "tsd",
"typecheck": "tsc --noEmit",
"typecheck": "tsgo --noEmit",
"lint": "pnpm typecheck && pnpm eslint",
"lint:fix": "pnpm eslint --fix"
},
@@ -28,16 +28,16 @@
"built"
],
"devDependencies": {
"@types/node": "26.1.1",
"@typescript-eslint/eslint-plugin": "8.63.0",
"@typescript-eslint/parser": "8.63.0",
"@types/node": "26.0.1",
"@typescript-eslint/eslint-plugin": "8.62.0",
"@typescript-eslint/parser": "8.62.0",
"chokidar": "5.0.0",
"esbuild": "0.28.1",
"execa": "9.6.1",
"nodemon": "3.1.14",
"tsx": "4.23.0"
"tsx": "4.22.4"
},
"dependencies": {
"js-yaml": "5.2.1"
"js-yaml": "5.2.0"
}
}
+20
View File
@@ -596,6 +596,18 @@ export interface Locale extends ILocale {
*
*/
"mute": string;
/**
*
*/
"muteType": string;
/**
*
*/
"muteTypeDescription": string;
/**
*
*/
"muteTypeTimeline": string;
/**
*
*/
@@ -3856,6 +3868,10 @@ export interface Locale extends ILocale {
*
*/
"mutePeriod": string;
/**
*
*/
"mutePeriodDescription": string;
/**
*
*/
@@ -5651,6 +5667,10 @@ export interface Locale extends ILocale {
*
*/
"zeroPadding": string;
/**
*
*/
"muteConfirm": string;
/**
*
*/
+5 -5
View File
@@ -7,19 +7,19 @@
"scripts": {
"build": "tsx src/generator.ts",
"eslint": "eslint src/**/*.ts",
"typecheck": "tsc --noEmit",
"typecheck": "tsgo --noEmit",
"lint": "pnpm typecheck && pnpm eslint"
},
"devDependencies": {
"@types/node": "26.1.1",
"@types/node": "26.0.1",
"@types/wawoff2": "1.0.2",
"@typescript-eslint/eslint-plugin": "8.63.0",
"@typescript-eslint/parser": "8.63.0"
"@typescript-eslint/eslint-plugin": "8.62.0",
"@typescript-eslint/parser": "8.62.0"
},
"dependencies": {
"@tabler/icons-webfont": "3.35.0",
"harfbuzzjs": "1.4.0",
"tsx": "4.23.0",
"tsx": "4.22.4",
"wawoff2": "2.0.1"
},
"files": [
+1 -1
View File
@@ -58,7 +58,7 @@ async function buildSrc() {
function buildDts() {
return execa(
'tsc',
'tsgo',
[
'--project', 'tsconfig.json',
'--outDir', 'built',
+4 -4
View File
@@ -20,15 +20,15 @@
"build": "node ./build.js",
"watch": "nodemon -w package.json -e json --exec \"node ./build.js --watch\"",
"eslint": "eslint './**/*.{js,jsx,ts,tsx}'",
"typecheck": "tsc --noEmit",
"typecheck": "tsgo --noEmit",
"lint": "pnpm typecheck && pnpm eslint"
},
"devDependencies": {
"@types/matter-js": "0.20.2",
"@types/node": "26.1.1",
"@types/node": "26.0.1",
"@types/seedrandom": "3.0.8",
"@typescript-eslint/eslint-plugin": "8.63.0",
"@typescript-eslint/parser": "8.63.0",
"@typescript-eslint/eslint-plugin": "8.62.0",
"@typescript-eslint/parser": "8.62.0",
"esbuild": "0.28.1",
"execa": "9.6.1",
"nodemon": "3.1.14"
+1 -1
View File
@@ -59,7 +59,7 @@ async function buildSrc() {
function buildDts() {
return execa(
'tsc',
'tsgo',
[
'--project', 'tsconfig.json',
'--outDir', 'built',
+6 -6
View File
@@ -7,15 +7,15 @@
"generate": "tsx src/generator.ts && eslint ./built/**/*.ts --fix"
},
"devDependencies": {
"@readme/openapi-parser": "6.2.1",
"@types/node": "26.1.1",
"@typescript-eslint/eslint-plugin": "8.63.0",
"@typescript-eslint/parser": "8.63.0",
"@readme/openapi-parser": "6.1.3",
"@types/node": "26.0.1",
"@typescript-eslint/eslint-plugin": "8.62.0",
"@typescript-eslint/parser": "8.62.0",
"openapi-types": "12.1.3",
"openapi-typescript": "7.13.0",
"ts-case-convert": "2.3.1",
"tsx": "4.23.0",
"eslint": "9.39.5"
"tsx": "4.22.4",
"eslint": "9.39.4"
},
"files": [
"built"
+8 -8
View File
@@ -1,7 +1,7 @@
{
"type": "module",
"name": "misskey-js",
"version": "2026.7.0-beta.1",
"version": "2026.7.0-alpha.6",
"description": "Misskey SDK for JavaScript",
"license": "MIT",
"main": "./built/index.js",
@@ -25,7 +25,7 @@
"api": "pnpm api-extractor run --local --verbose",
"api-prod": "pnpm api-extractor run --verbose",
"eslint": "eslint './**/*.{js,jsx,ts,tsx}'",
"typecheck": "tsc --noEmit",
"typecheck": "tsgo --noEmit",
"lint": "pnpm typecheck && pnpm eslint",
"vitest": "vitest run --coverage",
"test": "pnpm vitest && pnpm tsd",
@@ -38,17 +38,17 @@
},
"devDependencies": {
"@microsoft/api-extractor": "7.58.9",
"@types/node": "26.1.1",
"@typescript-eslint/eslint-plugin": "8.63.0",
"@typescript-eslint/parser": "8.63.0",
"@vitest/coverage-v8": "4.1.10",
"@types/node": "26.0.1",
"@typescript-eslint/eslint-plugin": "8.62.0",
"@typescript-eslint/parser": "8.62.0",
"@vitest/coverage-v8": "4.1.9",
"esbuild": "0.28.1",
"execa": "9.6.1",
"ncp": "2.0.0",
"nodemon": "3.1.14",
"tsd": "0.33.0",
"vitest": "4.1.10",
"vitest-websocket-mock": "0.7.0"
"vitest": "4.1.9",
"vitest-websocket-mock": "0.5.0"
},
"files": [
"built"
+8
View File
@@ -4905,6 +4905,8 @@ export type components = {
createdAt: string;
/** Format: date-time */
expiresAt: string | null;
/** @enum {string} */
mutingType: 'all' | 'timelineOnly';
/** Format: id */
muteeId: string;
mutee: components['schemas']['UserDetailedNotMe'];
@@ -29009,6 +29011,12 @@ export interface operations {
userId: string;
/** @description A Unix Epoch timestamp that must lie in the future. `null` means an indefinite mute. */
expiresAt?: number | null;
/**
* @description Type of muting. `all` mutes everything including notifications. `timelineOnly` mutes only timeline and search, but allows notifications.
* @default all
* @enum {string}
*/
mutingType?: 'all' | 'timelineOnly';
};
};
};
+1 -1
View File
@@ -58,7 +58,7 @@ async function buildSrc() {
function buildDts() {
return execa(
'tsc',
'tsgo',
[
'--project', 'tsconfig.json',
'--outDir', 'built',
+4 -4
View File
@@ -20,13 +20,13 @@
"build": "node ./build.js",
"watch": "nodemon -w package.json -e json --exec \"node ./build.js --watch\"",
"eslint": "eslint './**/*.{js,jsx,ts,tsx}'",
"typecheck": "tsc --noEmit",
"typecheck": "tsgo --noEmit",
"lint": "pnpm typecheck && pnpm eslint"
},
"devDependencies": {
"@types/node": "26.1.1",
"@typescript-eslint/eslint-plugin": "8.63.0",
"@typescript-eslint/parser": "8.63.0",
"@types/node": "26.0.1",
"@typescript-eslint/eslint-plugin": "8.62.0",
"@typescript-eslint/parser": "8.62.0",
"esbuild": "0.28.1",
"execa": "9.6.1",
"nodemon": "3.1.14"
+3 -3
View File
@@ -4,18 +4,18 @@
"scripts": {
"watch": "nodemon -w ../../package.json -e json --exec \"node build.js watch\"",
"build": "node build.js",
"typecheck": "tsc --noEmit",
"typecheck": "tsgo --noEmit",
"eslint": "eslint --quiet src/**/*.ts",
"lint": "pnpm typecheck && pnpm eslint"
},
"dependencies": {
"i18n": "workspace:*",
"esbuild": "0.28.1",
"idb-keyval": "6.3.0",
"idb-keyval": "6.2.5",
"misskey-js": "workspace:*"
},
"devDependencies": {
"@typescript-eslint/parser": "8.63.0",
"@typescript-eslint/parser": "8.62.0",
"@typescript/lib-webworker": "npm:@types/serviceworker@0.0.74",
"eslint-plugin-import": "2.32.0",
"nodemon": "3.1.14"
+1862 -2053
View File
File diff suppressed because it is too large Load Diff
+150 -511
View File
@@ -9,16 +9,16 @@
"version": "1.0.0",
"devDependencies": {
"@types/mdast": "4.0.4",
"@types/node": "26.1.1",
"@vitest/coverage-v8": "4.1.10",
"@types/node": "26.0.1",
"@vitest/coverage-v8": "4.1.9",
"mdast-util-to-string": "4.0.0",
"remark": "15.0.1",
"remark-parse": "11.0.0",
"typescript": "7.0.2",
"typescript": "5.9.3",
"unified": "11.0.5",
"vite": "8.1.4",
"vite": "8.1.0",
"vite-node": "6.0.0",
"vitest": "4.1.10"
"vitest": "4.1.9"
}
},
"node_modules/@babel/helper-string-parser": {
@@ -163,9 +163,9 @@
}
},
"node_modules/@oxc-project/types": {
"version": "0.139.0",
"resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.139.0.tgz",
"integrity": "sha512-r9gHphtCs+1M7J0pw6Sn/hh/Wpa/iQrOOkrNAlVLF/gHq+/CJmHIWKKUUhdWjcD6CIa8idarspCsASiXCXvFUw==",
"version": "0.137.0",
"resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.137.0.tgz",
"integrity": "sha512-WT+Gb24i8hmvo85AIv2oEYouEXkRlKAlT9WaCa3TfLgNCN+GhrJOGZuIlMouAh38Qe4QOx26eUOVsq70qXrywA==",
"dev": true,
"license": "MIT",
"funding": {
@@ -173,9 +173,9 @@
}
},
"node_modules/@rolldown/binding-android-arm64": {
"version": "1.1.5",
"resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.1.5.tgz",
"integrity": "sha512-lZg8fqIv2v7FF237bwMgzGZEJvGL79/s5knJ/i6FmsGF4XXlzccZ4jb+TrFIxtSSxFtIpdsgrPZeMk1I9AFcyQ==",
"version": "1.1.3",
"resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.1.3.tgz",
"integrity": "sha512-DT6Z3PhvioeHMvxo+xHc3KtqggrI7CCTXCmC2h/5zUlp5jVitv7XEy+9q5/7v8IolhlioawpMo8Kg0EEBy7J0g==",
"cpu": [
"arm64"
],
@@ -190,9 +190,9 @@
}
},
"node_modules/@rolldown/binding-darwin-arm64": {
"version": "1.1.5",
"resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.1.5.tgz",
"integrity": "sha512-51Bnx9pNiMRKSUNtBfySkNJ9vMU9Hh3I1ozDd6gyPPYzaXCfnptUcEZxXGYFn+ul2dtcMUiqGR1Yai2K10uoTw==",
"version": "1.1.3",
"resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.1.3.tgz",
"integrity": "sha512-0NwgwsjM7LrsuVnXMK3koTpagBNOhloc/BNjKqZjv4V5zI5r13qx69uVhRx+o5Z0yy4Hzq+lpy7TAgUG/ocvrw==",
"cpu": [
"arm64"
],
@@ -207,9 +207,9 @@
}
},
"node_modules/@rolldown/binding-darwin-x64": {
"version": "1.1.5",
"resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.1.5.tgz",
"integrity": "sha512-Tm+gbfC0aHu1tBA/JvKQh32S0K6YgCHkiAF4/W6xX0K0RmNuc94VeK419dJoE65R5aRxmo+noZQSWrAMF6yb6g==",
"version": "1.1.3",
"resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.1.3.tgz",
"integrity": "sha512-YtiBp4disu6V560loT6PjMdiRaWmVvDNrUunAalbiFx2ggeJwxdAsgZMcoGP17uyAsTwAj5V1niksxlHnVQ1Sw==",
"cpu": [
"x64"
],
@@ -224,9 +224,9 @@
}
},
"node_modules/@rolldown/binding-freebsd-x64": {
"version": "1.1.5",
"resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.1.5.tgz",
"integrity": "sha512-JMzDKCCXq93YccG5gz3hvOs1oXRKAf0XYpfOS88e+wZrC8Iugj6j68867vrYZkvpDDpKn/KoKORThmchMpF6TA==",
"version": "1.1.3",
"resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.1.3.tgz",
"integrity": "sha512-yD3EkEdXk2LypPxnf/kSZHirarsI8gcPzc62SukhR9VJTyvV+F9Q/GxWNuCojc7sXyuVC4DxRGhdDK4X8VSsbw==",
"cpu": [
"x64"
],
@@ -241,9 +241,9 @@
}
},
"node_modules/@rolldown/binding-linux-arm-gnueabihf": {
"version": "1.1.5",
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.1.5.tgz",
"integrity": "sha512-uML21j2K5TfPGutKxub+M+nLjZIrWjXQ5Grx4lCe/nimTj9B4L63zHpjXLl4y0L3mcm2htEQIb06oCG/szerNw==",
"version": "1.1.3",
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.1.3.tgz",
"integrity": "sha512-c+8vieQbsD7HNAHKIA34w0GJ9FedFFuJGD+7E6vz7Q3uqAIugL5p45fhlsj4UaAsHpcmlqugBWMhA0/j7o0sIg==",
"cpu": [
"arm"
],
@@ -258,9 +258,9 @@
}
},
"node_modules/@rolldown/binding-linux-arm64-gnu": {
"version": "1.1.5",
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.1.5.tgz",
"integrity": "sha512-navSiuTMogvnQoZoM/v+l3ZWo50/NTwSHSzheABx/RCnmUPaKwq9qSo4Br2OYRs21+Fz8uFqITZM3H4opOB0/Q==",
"version": "1.1.3",
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.1.3.tgz",
"integrity": "sha512-50jD0uUwLvur7Zz9LHz17kaAdTPjn5wN93hEgjvmYFRZwiR7ZJYovTd5ipyWJDAnXKvZ+wgc+/Ika6dwSF5OcA==",
"cpu": [
"arm64"
],
@@ -275,9 +275,9 @@
}
},
"node_modules/@rolldown/binding-linux-arm64-musl": {
"version": "1.1.5",
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.1.5.tgz",
"integrity": "sha512-lAryqH7IteztmCXQXk0etKj4wBQ7Gx5S6LjKhsgp9zb8I5bsuvU/2llH1hDQcjsFeqIsovMVN339/8pUDDBXxA==",
"version": "1.1.3",
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.1.3.tgz",
"integrity": "sha512-BO9+oPL8K9poZJBfYPsXNtYjPE5uM3qeehT3aFcW4LITOl+iSqhp0abzjR2nWBUNjIZeKXjAEWBZ64WjNoHd6w==",
"cpu": [
"arm64"
],
@@ -292,9 +292,9 @@
}
},
"node_modules/@rolldown/binding-linux-ppc64-gnu": {
"version": "1.1.5",
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.1.5.tgz",
"integrity": "sha512-fsK/sNBnxzBlL4O1JNrZakVQxPspqpED5dLtNsZS9oOKmtSpdNIzxH2kkol5HYTWJN47sE20ztMJPxfZ89qGOg==",
"version": "1.1.3",
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.1.3.tgz",
"integrity": "sha512-f3VpLB1vQ0Eo6ecr/6cekLnvYMFF4YBFoVGkfkvPLq1bAkbAwHYQPZKoAmG6OJyTcxxoC+AvezGx/S1obNC0Mw==",
"cpu": [
"ppc64"
],
@@ -309,9 +309,9 @@
}
},
"node_modules/@rolldown/binding-linux-s390x-gnu": {
"version": "1.1.5",
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.1.5.tgz",
"integrity": "sha512-gLYb4BIadlfTOYT5gO503n8zQjXflgzpD0FcyKh0Mzx3rqCZKnHoJWV9xe1KXUJ5lx2JfcSHr/mhzS0PC/McAA==",
"version": "1.1.3",
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.1.3.tgz",
"integrity": "sha512-AmurZ26Pqx/RI9N1gzEOCklkKXl927yjfXWUUS0O7Puh8ARM/Ob8qfrD3qnWksScdw6cSrW5PSHE9DyLu7+PtA==",
"cpu": [
"s390x"
],
@@ -326,9 +326,9 @@
}
},
"node_modules/@rolldown/binding-linux-x64-gnu": {
"version": "1.1.5",
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.1.5.tgz",
"integrity": "sha512-FjcpEKUyJygHgs1o50VYNvkt5+7Le/VEdYt0AkRpkL33MnyQfwr8l5mXwMmfmTbyMPr5vJLC+8/Gd9gXnwU1QQ==",
"version": "1.1.3",
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.1.3.tgz",
"integrity": "sha512-JJpqs8bRGITDOdbkNKnlojzBabbOHrqjSvDr0IVsZObE1lBcPjxItUEY9eWIDbxaJ3cGrXPWGfGkIxFijg/URg==",
"cpu": [
"x64"
],
@@ -343,9 +343,9 @@
}
},
"node_modules/@rolldown/binding-linux-x64-musl": {
"version": "1.1.5",
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.1.5.tgz",
"integrity": "sha512-Me+PfPI2TMeOQk0gYWfLQZtTktrmzbr8cDboqX83XKc7UrgAi55gF+2dUkWdxd19n55Essp2yeca+O9N5rBxHg==",
"version": "1.1.3",
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.1.3.tgz",
"integrity": "sha512-rSJcdjPxzA/by/6/rYs+v+bXU7UjvnbUWz8MJb6kh6+knqB1dCrtHg0uu7C/4haqJvqdkYHQ5IGn+tCH9GLW/g==",
"cpu": [
"x64"
],
@@ -360,9 +360,9 @@
}
},
"node_modules/@rolldown/binding-openharmony-arm64": {
"version": "1.1.5",
"resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.1.5.tgz",
"integrity": "sha512-yc5WrLzXks6zCQfn9Oxr8pORKyl/pF+QjHmW/Qx3qu0oyrrNC+y2JLTU1E2rcWYAmzlnqngWXHQjy51VzW70Vw==",
"version": "1.1.3",
"resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.1.3.tgz",
"integrity": "sha512-hQ3/PYkDJICgevvyNcVrihVeqq7k1Pp3VZ9lY+dauAYUJKO+auqApvANhvR1An9BhmqYKvW2Mu1F9u4DXSMLxQ==",
"cpu": [
"arm64"
],
@@ -377,9 +377,9 @@
}
},
"node_modules/@rolldown/binding-wasm32-wasi": {
"version": "1.1.5",
"resolved": "https://registry.npmjs.org/@rolldown/binding-wasm32-wasi/-/binding-wasm32-wasi-1.1.5.tgz",
"integrity": "sha512-VbQGPX2b4r48TAMIM2cjgluIM1HYutm4pcTEJsle7iEP7sB1dFqtPLBVbdLAZCxy1txCcPxf4QFf4v8uvltPqA==",
"version": "1.1.3",
"resolved": "https://registry.npmjs.org/@rolldown/binding-wasm32-wasi/-/binding-wasm32-wasi-1.1.3.tgz",
"integrity": "sha512-Elcv/BtML9lXrV6JuKITc/grN2kYV9gjsQpW8Jfw4ioK0TOkjBjye0nnyqQNy9STNaI20lXNaQBRrD5gSgR0Yg==",
"cpu": [
"wasm32"
],
@@ -396,9 +396,9 @@
}
},
"node_modules/@rolldown/binding-win32-arm64-msvc": {
"version": "1.1.5",
"resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.1.5.tgz",
"integrity": "sha512-gHv82k63z4qpV5+Q1y/12KrK0ltWBukVDI8nZcbT7Tt/ZlOIVwppazneq0F93oDxTo3IgAMEDIoQh3E2n6mVsw==",
"version": "1.1.3",
"resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.1.3.tgz",
"integrity": "sha512-2DrEfhluH9yhiaFApmsjsjwrSYbNcY1oFTzYSP1a535jDbV98zCFanA/96TBUd0iDFcxGmw9QRExwGCXz3U+/g==",
"cpu": [
"arm64"
],
@@ -413,9 +413,9 @@
}
},
"node_modules/@rolldown/binding-win32-x64-msvc": {
"version": "1.1.5",
"resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.1.5.tgz",
"integrity": "sha512-tTZuDBPw85tEN5PQi1pnEBzDy0Z49HtScLAbD5t6hyeU92A95pRWaSMw1GZZi/RwgSgUIl0xrSlXIT/9QzvYSA==",
"version": "1.1.3",
"resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.1.3.tgz",
"integrity": "sha512-OL4OMk7UPXOeVGGd3qo5zJyPIljf4AFgk5QAkPPS+OoLuOOozhuaQGC18MxVTnw/06q93gShAJzlwnSCY9YtqA==",
"cpu": [
"x64"
],
@@ -505,9 +505,9 @@
"dev": true
},
"node_modules/@types/node": {
"version": "26.1.1",
"resolved": "https://registry.npmjs.org/@types/node/-/node-26.1.1.tgz",
"integrity": "sha512-nxAkRSVkN1Y0JC1W8ky/fTfkGsMmcrRsbx+3XoZE+rMOX71kLYTV7fLXpqud1GpbpP5TuffXFqfX7fH2GgZREw==",
"version": "26.0.1",
"resolved": "https://registry.npmjs.org/@types/node/-/node-26.0.1.tgz",
"integrity": "sha512-fc3KiUoBt6kie0N9bIW3E47vZsuaMf0PM2AaUpLCLT0s/LvX1nxAim6Fc049cNxODPpGm6qRAuUOB86SkRuPQw==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -520,355 +520,15 @@
"integrity": "sha512-dqId9J8K/vGi5Zr7oo212BGii5m3q5Hxlkwy3WpYuKPklmBEvsbMYYyLxAQpSffdLl/gdW0XUpKWFvYmyoWCoQ==",
"dev": true
},
"node_modules/@typescript/typescript-aix-ppc64": {
"version": "7.0.2",
"resolved": "https://registry.npmjs.org/@typescript/typescript-aix-ppc64/-/typescript-aix-ppc64-7.0.2.tgz",
"integrity": "sha512-MTKKkWB7p/0E9xi1d1tHtZ5PiLkGEMIq88pK2CubZjOsLtYTLqhgIgi6zepFa+9GHZ6h05NMCkQxGKiPXMxXtQ==",
"cpu": [
"ppc64"
],
"dev": true,
"license": "Apache-2.0",
"optional": true,
"os": [
"aix"
],
"engines": {
"node": ">=16.20.0"
}
},
"node_modules/@typescript/typescript-darwin-arm64": {
"version": "7.0.2",
"resolved": "https://registry.npmjs.org/@typescript/typescript-darwin-arm64/-/typescript-darwin-arm64-7.0.2.tgz",
"integrity": "sha512-gowzar9MwS/aRWp6f3a4KUqzRjAZjOsmGNCM6LcTgXum+dBfgsBVMN+AgvOCCbguXyick6LJhpBszxMebJ8syA==",
"cpu": [
"arm64"
],
"dev": true,
"license": "Apache-2.0",
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": ">=16.20.0"
}
},
"node_modules/@typescript/typescript-darwin-x64": {
"version": "7.0.2",
"resolved": "https://registry.npmjs.org/@typescript/typescript-darwin-x64/-/typescript-darwin-x64-7.0.2.tgz",
"integrity": "sha512-SZ9xZInqApNlNGc9s0W1VSsktYSOe9cFqNOIqmN1Gs8SmkjKZYFt017G4VwPxASInODuAdbTW7sXiFUf893RgA==",
"cpu": [
"x64"
],
"dev": true,
"license": "Apache-2.0",
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": ">=16.20.0"
}
},
"node_modules/@typescript/typescript-freebsd-arm64": {
"version": "7.0.2",
"resolved": "https://registry.npmjs.org/@typescript/typescript-freebsd-arm64/-/typescript-freebsd-arm64-7.0.2.tgz",
"integrity": "sha512-W5NH4y/J0plIIS5b2xvTEkU7JFxyqdMAOgf+Ilhl0vHQXKO5dZoxd+C/jEtq56c4F3wk71RB4BMRQ2XdI+bwYQ==",
"cpu": [
"arm64"
],
"dev": true,
"license": "Apache-2.0",
"optional": true,
"os": [
"freebsd"
],
"engines": {
"node": ">=16.20.0"
}
},
"node_modules/@typescript/typescript-freebsd-x64": {
"version": "7.0.2",
"resolved": "https://registry.npmjs.org/@typescript/typescript-freebsd-x64/-/typescript-freebsd-x64-7.0.2.tgz",
"integrity": "sha512-UMGDx5sTpzNw3WiPebH7l90IWfJggEd+egHt/q6p7/Cm3zqoV7VxkGXt+3DxPIw8CcmvAB0j3sVVfbhX+M4Tpw==",
"cpu": [
"x64"
],
"dev": true,
"license": "Apache-2.0",
"optional": true,
"os": [
"freebsd"
],
"engines": {
"node": ">=16.20.0"
}
},
"node_modules/@typescript/typescript-linux-arm": {
"version": "7.0.2",
"resolved": "https://registry.npmjs.org/@typescript/typescript-linux-arm/-/typescript-linux-arm-7.0.2.tgz",
"integrity": "sha512-gffT3xPz9sR7j/YJExkyPntrI0P2EP9XbOyWzth2/Gs0RstK+90RBcO0ncXoXy/beYll1SXw846Nf2zdnEz0QQ==",
"cpu": [
"arm"
],
"dev": true,
"license": "Apache-2.0",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=16.20.0"
}
},
"node_modules/@typescript/typescript-linux-arm64": {
"version": "7.0.2",
"resolved": "https://registry.npmjs.org/@typescript/typescript-linux-arm64/-/typescript-linux-arm64-7.0.2.tgz",
"integrity": "sha512-Qh4eU4/y3yDjnfjjyPYihMj5/ODIlmt+Bzu17OI+fiSRDW57QmU5SiN63exPRNJPKUzcc1INa1NXdrJ+MqHjUQ==",
"cpu": [
"arm64"
],
"dev": true,
"license": "Apache-2.0",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=16.20.0"
}
},
"node_modules/@typescript/typescript-linux-loong64": {
"version": "7.0.2",
"resolved": "https://registry.npmjs.org/@typescript/typescript-linux-loong64/-/typescript-linux-loong64-7.0.2.tgz",
"integrity": "sha512-uEHck9i8hoAzXPiYRib1O7miOnz23SxIeVl6F4LXox+qov1K35jHcEW6VHKvZI+pyvl7fZEP4MCU5LYvIq1GuQ==",
"cpu": [
"loong64"
],
"dev": true,
"license": "Apache-2.0",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=16.20.0"
}
},
"node_modules/@typescript/typescript-linux-mips64el": {
"version": "7.0.2",
"resolved": "https://registry.npmjs.org/@typescript/typescript-linux-mips64el/-/typescript-linux-mips64el-7.0.2.tgz",
"integrity": "sha512-R4KvAMnE43W5Qeqb0Ly56O3mWMWIAgsMyz36DCaycd5nbg/9kzm0liw3JocfRqyJY0KPmzFjbswozXyW0DnIYA==",
"cpu": [
"mips64el"
],
"dev": true,
"license": "Apache-2.0",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=16.20.0"
}
},
"node_modules/@typescript/typescript-linux-ppc64": {
"version": "7.0.2",
"resolved": "https://registry.npmjs.org/@typescript/typescript-linux-ppc64/-/typescript-linux-ppc64-7.0.2.tgz",
"integrity": "sha512-DORx5b3sd/4S7eayxm4FQv+A7CrkUIGRaHiwI8oiHTAI1fAPWhF4J0vAlkC8biAlHSVVwxMQ3tjZ2/DVbnQiiA==",
"cpu": [
"ppc64"
],
"dev": true,
"license": "Apache-2.0",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=16.20.0"
}
},
"node_modules/@typescript/typescript-linux-riscv64": {
"version": "7.0.2",
"resolved": "https://registry.npmjs.org/@typescript/typescript-linux-riscv64/-/typescript-linux-riscv64-7.0.2.tgz",
"integrity": "sha512-wf0jqEDOjrPRnKwYRyyJDRo11KMbvMFrU+q4zqKyChODBzvlkbhNQfKvLxQCcwTpdDaXSHZTVuh0JoCrKCUMHQ==",
"cpu": [
"riscv64"
],
"dev": true,
"license": "Apache-2.0",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=16.20.0"
}
},
"node_modules/@typescript/typescript-linux-s390x": {
"version": "7.0.2",
"resolved": "https://registry.npmjs.org/@typescript/typescript-linux-s390x/-/typescript-linux-s390x-7.0.2.tgz",
"integrity": "sha512-IkwJc3L7yhytWd/ewjyxNDfOmswCm9GWMJT/ue/dU4aZNbwZeYAetq42VyLmsmSjvoX7z74X6ZaYCtzAr0EuGw==",
"cpu": [
"s390x"
],
"dev": true,
"license": "Apache-2.0",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=16.20.0"
}
},
"node_modules/@typescript/typescript-linux-x64": {
"version": "7.0.2",
"resolved": "https://registry.npmjs.org/@typescript/typescript-linux-x64/-/typescript-linux-x64-7.0.2.tgz",
"integrity": "sha512-EYdf2cNg7rgCWJnxCdJ+F3V39O8ihb37eHAu1LK8oAFizgTQbPOK7zHHXbPt8rX24COqODXeI3sIf0fCXG7H/A==",
"cpu": [
"x64"
],
"dev": true,
"license": "Apache-2.0",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=16.20.0"
}
},
"node_modules/@typescript/typescript-netbsd-arm64": {
"version": "7.0.2",
"resolved": "https://registry.npmjs.org/@typescript/typescript-netbsd-arm64/-/typescript-netbsd-arm64-7.0.2.tgz",
"integrity": "sha512-+polYF4MF04aPpO5FTkHran9yUQDSXqy5GiSDKpsll5jy3l3+g9QLhpf39T+ePtefhXLOGrLl0QIjkQP6VnelA==",
"cpu": [
"arm64"
],
"dev": true,
"license": "Apache-2.0",
"optional": true,
"os": [
"netbsd"
],
"engines": {
"node": ">=16.20.0"
}
},
"node_modules/@typescript/typescript-netbsd-x64": {
"version": "7.0.2",
"resolved": "https://registry.npmjs.org/@typescript/typescript-netbsd-x64/-/typescript-netbsd-x64-7.0.2.tgz",
"integrity": "sha512-8YIT0EHM/3dq10ZOVF/A7pc/YSMtbcecct4rWtexrnSCHOPcpC2KTLXfTCR6vDpnSiY12heNb1GiN/wu+T/FyA==",
"cpu": [
"x64"
],
"dev": true,
"license": "Apache-2.0",
"optional": true,
"os": [
"netbsd"
],
"engines": {
"node": ">=16.20.0"
}
},
"node_modules/@typescript/typescript-openbsd-arm64": {
"version": "7.0.2",
"resolved": "https://registry.npmjs.org/@typescript/typescript-openbsd-arm64/-/typescript-openbsd-arm64-7.0.2.tgz",
"integrity": "sha512-APT8+ClYnuYm1u9+kgGXoMj2VzWzcymwh2gNSQVySHfkRDGOTVkoWLjCmOQSaO+PoqQ57B0flRp9SA+7GnnkzQ==",
"cpu": [
"arm64"
],
"dev": true,
"license": "Apache-2.0",
"optional": true,
"os": [
"openbsd"
],
"engines": {
"node": ">=16.20.0"
}
},
"node_modules/@typescript/typescript-openbsd-x64": {
"version": "7.0.2",
"resolved": "https://registry.npmjs.org/@typescript/typescript-openbsd-x64/-/typescript-openbsd-x64-7.0.2.tgz",
"integrity": "sha512-yX7s+Q0Dln0Dt9tEzZsAjXXR/+ytBM7AlglaqyeMPxQszJ1JhlJdZ6jLA+IzldHtflX81em7lDao1xXu+aRRkg==",
"cpu": [
"x64"
],
"dev": true,
"license": "Apache-2.0",
"optional": true,
"os": [
"openbsd"
],
"engines": {
"node": ">=16.20.0"
}
},
"node_modules/@typescript/typescript-sunos-x64": {
"version": "7.0.2",
"resolved": "https://registry.npmjs.org/@typescript/typescript-sunos-x64/-/typescript-sunos-x64-7.0.2.tgz",
"integrity": "sha512-dLJDGaLZ1D4HPQn62u1n8mBDkJREwMsAkCdkwd4Ieqw+x3TUyTsqY0YiBCtE6H6OzzgGk3iuZ3vFWRS+E8/d1g==",
"cpu": [
"x64"
],
"dev": true,
"license": "Apache-2.0",
"optional": true,
"os": [
"sunos"
],
"engines": {
"node": ">=16.20.0"
}
},
"node_modules/@typescript/typescript-win32-arm64": {
"version": "7.0.2",
"resolved": "https://registry.npmjs.org/@typescript/typescript-win32-arm64/-/typescript-win32-arm64-7.0.2.tgz",
"integrity": "sha512-Gyl1Vy6OsWesLzmq+EP0Fb7b4Nid5232AvcA2SFcdYreldpNtYFFofPjnt62y9hQy7VTaZp65ICJjuAQRaVcIQ==",
"cpu": [
"arm64"
],
"dev": true,
"license": "Apache-2.0",
"optional": true,
"os": [
"win32"
],
"engines": {
"node": ">=16.20.0"
}
},
"node_modules/@typescript/typescript-win32-x64": {
"version": "7.0.2",
"resolved": "https://registry.npmjs.org/@typescript/typescript-win32-x64/-/typescript-win32-x64-7.0.2.tgz",
"integrity": "sha512-0BQ3HkAHHlKLSp1qRvf3SUhGpGsDuhB/jgFw75guyqbxJqEaS0Cw/VFO8i2nHglJUzQCRtMMR/IBAKE3ETMC4g==",
"cpu": [
"x64"
],
"dev": true,
"license": "Apache-2.0",
"optional": true,
"os": [
"win32"
],
"engines": {
"node": ">=16.20.0"
}
},
"node_modules/@vitest/coverage-v8": {
"version": "4.1.10",
"resolved": "https://registry.npmjs.org/@vitest/coverage-v8/-/coverage-v8-4.1.10.tgz",
"integrity": "sha512-IM49HmthevbgAO4anp1hwtoT9wYe59w0LR00gr+eagHE+ZJ5lK4sLPeO0ubgoJcwLk6dehU3R24N+FbEEKDc8g==",
"version": "4.1.9",
"resolved": "https://registry.npmjs.org/@vitest/coverage-v8/-/coverage-v8-4.1.9.tgz",
"integrity": "sha512-G9/lgqibheLVBDRuya45EbsEXTYcWoSG+TLg7i2axuzx0Eq62eXn+aWXyaVdV5vKvFSWd6ywcX8hA7la9Pvu8g==",
"dev": true,
"license": "MIT",
"dependencies": {
"@bcoe/v8-coverage": "^1.0.2",
"@vitest/utils": "4.1.10",
"@vitest/utils": "4.1.9",
"ast-v8-to-istanbul": "^1.0.0",
"istanbul-lib-coverage": "^3.2.2",
"istanbul-lib-report": "^3.0.1",
@@ -882,8 +542,8 @@
"url": "https://opencollective.com/vitest"
},
"peerDependencies": {
"@vitest/browser": "4.1.10",
"vitest": "4.1.10"
"@vitest/browser": "4.1.9",
"vitest": "4.1.9"
},
"peerDependenciesMeta": {
"@vitest/browser": {
@@ -892,16 +552,16 @@
}
},
"node_modules/@vitest/expect": {
"version": "4.1.10",
"resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-4.1.10.tgz",
"integrity": "sha512-YsCn+qAk1GWjQOWFEsEcL2gNQ0zmVmQu3T03qP6UyjhtmdtwtbuI+DASn/7iQB3HGTXkdBwGddzxPlmiql5vlA==",
"version": "4.1.9",
"resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-4.1.9.tgz",
"integrity": "sha512-vl/rYsUKcBr3SnQn166+XR5ZQcgMx3DQhFWdfli/cWpLnLUmbxZvyrJZotLFUryib+LtArYMSTJ5RbQ57ZqrlA==",
"dev": true,
"license": "MIT",
"dependencies": {
"@standard-schema/spec": "^1.1.0",
"@types/chai": "^5.2.2",
"@vitest/spy": "4.1.10",
"@vitest/utils": "4.1.10",
"@vitest/spy": "4.1.9",
"@vitest/utils": "4.1.9",
"chai": "^6.2.2",
"tinyrainbow": "^3.1.0"
},
@@ -910,13 +570,13 @@
}
},
"node_modules/@vitest/mocker": {
"version": "4.1.10",
"resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-4.1.10.tgz",
"integrity": "sha512-v0xaezt+DKEmKfaxg133ldzADrwLGd7Ze1MfQQTYfvs8OqZIwbxyxaYURivwV7sWy5fqn3rH5uOrSp07bp44Ow==",
"version": "4.1.9",
"resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-4.1.9.tgz",
"integrity": "sha512-EVkXzBjrPGM+cK8/ANWgBrkUCfJfb38/EfTSO8h7pWvKkyPkpWxvR7BkD2MyItMF62C97zAEoqdpUixwR/e+Rw==",
"dev": true,
"license": "MIT",
"dependencies": {
"@vitest/spy": "4.1.10",
"@vitest/spy": "4.1.9",
"estree-walker": "^3.0.3",
"magic-string": "^0.30.21"
},
@@ -937,9 +597,9 @@
}
},
"node_modules/@vitest/pretty-format": {
"version": "4.1.10",
"resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-4.1.10.tgz",
"integrity": "sha512-W1HsjSH4MXQ9YfmmhLAoIYf1HRfekQCGngeIgcei6MP5QQGWUe0gkopdZQaVCFO+JDJMrAJGwa5pRpNpvy4P8Q==",
"version": "4.1.9",
"resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-4.1.9.tgz",
"integrity": "sha512-s0iufns3iIFitdgm+YR7g1whCAaGtXz459VS9/PqyKDEEFgYIhsHOQmXgIgDuYCt7DeQmiZT0Qe2OA2p4ZPu5A==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -950,13 +610,13 @@
}
},
"node_modules/@vitest/runner": {
"version": "4.1.10",
"resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-4.1.10.tgz",
"integrity": "sha512-IKI6kpIH+LmpROplyLwBBaCfMgOZOMsygVa6BARD6ahA04VRuJSa6OaVG7kRvSEMD870Vd91rSSw0eegtWyLGg==",
"version": "4.1.9",
"resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-4.1.9.tgz",
"integrity": "sha512-KXLMDtc7oe70+3mJfGrPUWPesswH+3sTxAMAMl8DG7I8IUQT4XW718dY5ID3vPUcmlu27CcKfY4P3h3I29SLJg==",
"dev": true,
"license": "MIT",
"dependencies": {
"@vitest/utils": "4.1.10",
"@vitest/utils": "4.1.9",
"pathe": "^2.0.3"
},
"funding": {
@@ -964,14 +624,14 @@
}
},
"node_modules/@vitest/snapshot": {
"version": "4.1.10",
"resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-4.1.10.tgz",
"integrity": "sha512-xRkfOT1qpTAi/Ti4Y1LtfRc3kEuqxGw59eN2jN9pRWMtS/XDevekhcFSqvQqjUNGksfjMJu3Y+oJ+4Ypn2OaJw==",
"version": "4.1.9",
"resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-4.1.9.tgz",
"integrity": "sha512-Jc7RKGNBo8Z28WYIm0Niej4xdSPByRf6mU58VpHQkd6Zh05rlnA+twjbK5HyeIGHxrzsc3mJgS43uM0CZKzaIA==",
"dev": true,
"license": "MIT",
"dependencies": {
"@vitest/pretty-format": "4.1.10",
"@vitest/utils": "4.1.10",
"@vitest/pretty-format": "4.1.9",
"@vitest/utils": "4.1.9",
"magic-string": "^0.30.21",
"pathe": "^2.0.3"
},
@@ -980,9 +640,9 @@
}
},
"node_modules/@vitest/spy": {
"version": "4.1.10",
"resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-4.1.10.tgz",
"integrity": "sha512-PLf/Ugvoq5wO/b4rwYCR1h2PSIdXz7wnkQFMiUpLdtM7l6pqVFcQIBEHyT1+l+cj7mNwAfZHzqXqDyjvOuwbDw==",
"version": "4.1.9",
"resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-4.1.9.tgz",
"integrity": "sha512-fHpsS6mIi+PiEW+vcRVOMkX1oSaPKne3VOclSFICPcGOmfKgXPU5iAah+wcNcj2xPrCCmfq99IDGf+EojhhvhA==",
"dev": true,
"license": "MIT",
"funding": {
@@ -990,13 +650,13 @@
}
},
"node_modules/@vitest/utils": {
"version": "4.1.10",
"resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-4.1.10.tgz",
"integrity": "sha512-fy9am/HWxbaGt/Sawrp90vt6Y6jQwf1RX77cz3uwoJwJVMli/e1IEwRPnMNJ7vKfPTwo0diXifkpPvwH9v7nGA==",
"version": "4.1.9",
"resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-4.1.9.tgz",
"integrity": "sha512-A51o8ymO5PpqlWNnBP9ZHPXDIpuMtTLlGSjN7la4US+LJzoUMyhwjA5QXlm39JexgwHKW4Xjs8Z2d3dLCXOeuA==",
"dev": true,
"license": "MIT",
"dependencies": {
"@vitest/pretty-format": "4.1.10",
"@vitest/pretty-format": "4.1.9",
"convert-source-map": "^2.0.0",
"tinyrainbow": "^3.1.0"
},
@@ -2114,9 +1774,9 @@
"license": "MIT"
},
"node_modules/nanoid": {
"version": "3.3.16",
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.16.tgz",
"integrity": "sha512-bzlKTyNJ7+LdGIIwy8ijFpIqEQIvafahV7eYykJ8Cvh42EdJeODoJ6gUJXpQJvej1BddH8OqTXZNE/KfbWAu8Q==",
"version": "3.3.12",
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.12.tgz",
"integrity": "sha512-ZB9RH/39qpq5Vu6Y+NmUaFhQR6pp+M2Xt76XBnEwDaGcVAqhlvxrl3B2bKS5D3NH3QR76v3aSrKaF/Kiy7lEtQ==",
"dev": true,
"funding": [
{
@@ -2158,9 +1818,9 @@
"license": "ISC"
},
"node_modules/picomatch": {
"version": "4.0.5",
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.5.tgz",
"integrity": "sha512-RvwwcruNjI1ncT5xRakeyS9Lf8lcItv34KD+aif+VH9kduAyfYBipGh12274xtenIPZ119/R9BdTBa8gAwSh0A==",
"version": "4.0.4",
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz",
"integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==",
"dev": true,
"license": "MIT",
"engines": {
@@ -2171,9 +1831,9 @@
}
},
"node_modules/postcss": {
"version": "8.5.20",
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.20.tgz",
"integrity": "sha512-lW616l85ucIQL+FocMmL7pQFPqBmwejrCMg+iPxyImlrANNJG9NHq/RkyCZopDhd8C3LA03PHRJDjkbGu8vvug==",
"version": "8.5.15",
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.15.tgz",
"integrity": "sha512-FfR8sjd4em2T6fb3I2MwAJU7HWVMr9zba+enmQeeWFfCbm+UOC/0X4DS8XtpUTMwWMGbjKYP7xjfNekzyGmB3A==",
"dev": true,
"funding": [
{
@@ -2191,7 +1851,7 @@
],
"license": "MIT",
"dependencies": {
"nanoid": "^3.3.16",
"nanoid": "^3.3.12",
"picocolors": "^1.1.1",
"source-map-js": "^1.2.1"
},
@@ -2247,13 +1907,13 @@
}
},
"node_modules/rolldown": {
"version": "1.1.5",
"resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.1.5.tgz",
"integrity": "sha512-t9z29cJjXf/vxQ8dyhCSpt6H6aSwHTk8cT5I3iy6SMXuFpk5mB6PL6XfC8PCwrPTx93udwKUm9HRteAlTGBLiA==",
"version": "1.1.3",
"resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.1.3.tgz",
"integrity": "sha512-1F1eEtUBtFvcGm1HQ9TiUIUHPQG7mSAODrhIzjxoUEFuo8OcbrGLiVLkevNgj84TE4lnHvnumwFjhJO5Eu135g==",
"dev": true,
"license": "MIT",
"dependencies": {
"@oxc-project/types": "=0.139.0",
"@oxc-project/types": "=0.137.0",
"@rolldown/pluginutils": "^1.0.0"
},
"bin": {
@@ -2263,21 +1923,21 @@
"node": "^20.19.0 || >=22.12.0"
},
"optionalDependencies": {
"@rolldown/binding-android-arm64": "1.1.5",
"@rolldown/binding-darwin-arm64": "1.1.5",
"@rolldown/binding-darwin-x64": "1.1.5",
"@rolldown/binding-freebsd-x64": "1.1.5",
"@rolldown/binding-linux-arm-gnueabihf": "1.1.5",
"@rolldown/binding-linux-arm64-gnu": "1.1.5",
"@rolldown/binding-linux-arm64-musl": "1.1.5",
"@rolldown/binding-linux-ppc64-gnu": "1.1.5",
"@rolldown/binding-linux-s390x-gnu": "1.1.5",
"@rolldown/binding-linux-x64-gnu": "1.1.5",
"@rolldown/binding-linux-x64-musl": "1.1.5",
"@rolldown/binding-openharmony-arm64": "1.1.5",
"@rolldown/binding-wasm32-wasi": "1.1.5",
"@rolldown/binding-win32-arm64-msvc": "1.1.5",
"@rolldown/binding-win32-x64-msvc": "1.1.5"
"@rolldown/binding-android-arm64": "1.1.3",
"@rolldown/binding-darwin-arm64": "1.1.3",
"@rolldown/binding-darwin-x64": "1.1.3",
"@rolldown/binding-freebsd-x64": "1.1.3",
"@rolldown/binding-linux-arm-gnueabihf": "1.1.3",
"@rolldown/binding-linux-arm64-gnu": "1.1.3",
"@rolldown/binding-linux-arm64-musl": "1.1.3",
"@rolldown/binding-linux-ppc64-gnu": "1.1.3",
"@rolldown/binding-linux-s390x-gnu": "1.1.3",
"@rolldown/binding-linux-x64-gnu": "1.1.3",
"@rolldown/binding-linux-x64-musl": "1.1.3",
"@rolldown/binding-openharmony-arm64": "1.1.3",
"@rolldown/binding-wasm32-wasi": "1.1.3",
"@rolldown/binding-win32-arm64-msvc": "1.1.3",
"@rolldown/binding-win32-x64-msvc": "1.1.3"
}
},
"node_modules/semver": {
@@ -2319,9 +1979,9 @@
"license": "MIT"
},
"node_modules/std-env": {
"version": "4.2.0",
"resolved": "https://registry.npmjs.org/std-env/-/std-env-4.2.0.tgz",
"integrity": "sha512-oCUKSupKTHX53EyjDtuZQ64pjLJ6yYCtpmEw0goYxtjG9KpbRe8KAsl2tBUGU9DyMcJ0RwJ8GqJAFzMXcXW1Rw==",
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/std-env/-/std-env-4.1.0.tgz",
"integrity": "sha512-Rq7ybcX2RuC55r9oaPVEW7/xu3tj8u4GeBYHBWCychFtzMIr86A7e3PPEBPT37sHStKX3+TiX/Fr/ACmJLVlLQ==",
"dev": true,
"license": "MIT"
},
@@ -2400,38 +2060,17 @@
"optional": true
},
"node_modules/typescript": {
"version": "7.0.2",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-7.0.2.tgz",
"integrity": "sha512-8FYau96o3NKOhbjKi/qNvG/W5jhzxkbdm5sj9AbZ/5T5sWqn3hJgLfGx27sRKZWTvyzCP8dLRBTf5tBTSRVUNA==",
"version": "5.9.3",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz",
"integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
"dev": true,
"license": "Apache-2.0",
"bin": {
"tsc": "bin/tsc"
"tsc": "bin/tsc",
"tsserver": "bin/tsserver"
},
"engines": {
"node": ">=16.20.0"
},
"optionalDependencies": {
"@typescript/typescript-aix-ppc64": "7.0.2",
"@typescript/typescript-darwin-arm64": "7.0.2",
"@typescript/typescript-darwin-x64": "7.0.2",
"@typescript/typescript-freebsd-arm64": "7.0.2",
"@typescript/typescript-freebsd-x64": "7.0.2",
"@typescript/typescript-linux-arm": "7.0.2",
"@typescript/typescript-linux-arm64": "7.0.2",
"@typescript/typescript-linux-loong64": "7.0.2",
"@typescript/typescript-linux-mips64el": "7.0.2",
"@typescript/typescript-linux-ppc64": "7.0.2",
"@typescript/typescript-linux-riscv64": "7.0.2",
"@typescript/typescript-linux-s390x": "7.0.2",
"@typescript/typescript-linux-x64": "7.0.2",
"@typescript/typescript-netbsd-arm64": "7.0.2",
"@typescript/typescript-netbsd-x64": "7.0.2",
"@typescript/typescript-openbsd-arm64": "7.0.2",
"@typescript/typescript-openbsd-x64": "7.0.2",
"@typescript/typescript-sunos-x64": "7.0.2",
"@typescript/typescript-win32-arm64": "7.0.2",
"@typescript/typescript-win32-x64": "7.0.2"
"node": ">=14.17"
}
},
"node_modules/undici-types": {
@@ -2546,16 +2185,16 @@
}
},
"node_modules/vite": {
"version": "8.1.4",
"resolved": "https://registry.npmjs.org/vite/-/vite-8.1.4.tgz",
"integrity": "sha512-bTT9PsdWO+MQMNG9ZXIP/qM9wGh37DFxTV/sPq9cFpHr3w4jkgef032PkAL9jAqhk3Nz8NQw3O8n6/xFkqO4QQ==",
"version": "8.1.0",
"resolved": "https://registry.npmjs.org/vite/-/vite-8.1.0.tgz",
"integrity": "sha512-BuJcQK/56NQTWDGn4ABea3q4SSBdNPWwNZKTkkUpcMPnLoquSYH8llRtSUIgoL1KSCpHt5eghLShn50mH36y7Q==",
"dev": true,
"license": "MIT",
"dependencies": {
"lightningcss": "^1.32.0",
"picomatch": "^4.0.5",
"postcss": "^8.5.16",
"rolldown": "~1.1.4",
"picomatch": "^4.0.4",
"postcss": "^8.5.15",
"rolldown": "~1.1.2",
"tinyglobby": "^0.2.17"
},
"bin": {
@@ -2647,19 +2286,19 @@
}
},
"node_modules/vitest": {
"version": "4.1.10",
"resolved": "https://registry.npmjs.org/vitest/-/vitest-4.1.10.tgz",
"integrity": "sha512-R9jUTe5S4Qb0HCd4TNqpC7oGcrMssMRGXLW80ubjWsW9VH5GF8y1Y0SFLY9AbqSk6nt0PnOx4H4WNJYZ13GUPw==",
"version": "4.1.9",
"resolved": "https://registry.npmjs.org/vitest/-/vitest-4.1.9.tgz",
"integrity": "sha512-nE3/LEyc0z87uHYLZebqCUOaJr2hdtuPp7BQ4BosVFnfltxgAvMG08NyrSGlPpOUWvR27c5flSmYFTNr78L9GQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"@vitest/expect": "4.1.10",
"@vitest/mocker": "4.1.10",
"@vitest/pretty-format": "4.1.10",
"@vitest/runner": "4.1.10",
"@vitest/snapshot": "4.1.10",
"@vitest/spy": "4.1.10",
"@vitest/utils": "4.1.10",
"@vitest/expect": "4.1.9",
"@vitest/mocker": "4.1.9",
"@vitest/pretty-format": "4.1.9",
"@vitest/runner": "4.1.9",
"@vitest/snapshot": "4.1.9",
"@vitest/spy": "4.1.9",
"@vitest/utils": "4.1.9",
"es-module-lexer": "^2.0.0",
"expect-type": "^1.3.0",
"magic-string": "^0.30.21",
@@ -2687,12 +2326,12 @@
"@edge-runtime/vm": "*",
"@opentelemetry/api": "^1.9.0",
"@types/node": "^20.0.0 || ^22.0.0 || >=24.0.0",
"@vitest/browser-playwright": "4.1.10",
"@vitest/browser-preview": "4.1.10",
"@vitest/browser-webdriverio": "4.1.10",
"@vitest/coverage-istanbul": "4.1.10",
"@vitest/coverage-v8": "4.1.10",
"@vitest/ui": "4.1.10",
"@vitest/browser-playwright": "4.1.9",
"@vitest/browser-preview": "4.1.9",
"@vitest/browser-webdriverio": "4.1.9",
"@vitest/coverage-istanbul": "4.1.9",
"@vitest/coverage-v8": "4.1.9",
"@vitest/ui": "4.1.9",
"happy-dom": "*",
"jsdom": "*",
"vite": "^6.0.0 || ^7.0.0 || ^8.0.0"
+5 -5
View File
@@ -10,15 +10,15 @@
},
"devDependencies": {
"@types/mdast": "4.0.4",
"@types/node": "26.1.1",
"@vitest/coverage-v8": "4.1.10",
"@types/node": "26.0.1",
"@vitest/coverage-v8": "4.1.9",
"mdast-util-to-string": "4.0.0",
"remark": "15.0.1",
"remark-parse": "11.0.0",
"typescript": "7.0.2",
"typescript": "5.9.3",
"unified": "11.0.5",
"vite": "8.1.4",
"vite": "8.1.0",
"vite-node": "6.0.0",
"vitest": "4.1.10"
"vitest": "4.1.9"
}
}