Compare commits

...

25 Commits

Author SHA1 Message Date
CGsama 00ff7bb82c
Merge 0636de6a52 into 42a2ed8b67 2025-05-04 06:08:58 +00:00
renovate[bot] 42a2ed8b67
fix(deps): update dependency vite to v6.3.4 [security] (#15918)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-05-04 14:54:35 +09:00
zyoshoka a5fa9a2cef
chore(workflow): use `node-version-file` instead of hardcoded `node-version` (#15658)
* chore(workflow): use `node-version-file` instead of hardcoded `node-version`

* chore: test min version
2025-05-04 14:14:09 +09:00
かっこかり 0884605b62
fix(frontend): MkFolderのアニメーションにフォールバックを追加 (#15937)
* fix(frontend): Chrome系以外のブラウザでMkFolderがアニメーションしない問題を修正

* fix

* Update Changelog

* fix
2025-05-04 14:06:43 +09:00
かっこかり 8dbd8ff4cf
enhance(frontend): タイムラインpush時のアニメーションを調整 (#15936)
* fix(frontend): タイムラインpush時のアニメーションを復元

* 🎨

* enhance: 消える際のアニメーションも追加

* better support for non-supported browsers

* fix: Skip Note Rendering有効時、TransitionGroupでnoteを追加するときに一瞬がくっとなる問題を修正

* 🎨

* fix
2025-05-04 13:52:01 +09:00
syuilo 5aeedf59ff enhance(frontend): 中クリックでPullToRefreshするように
Close #15931
2025-05-04 11:04:51 +09:00
github-actions[bot] 3eaa05a5d9 Bump version to 2025.5.0-alpha.0 2025-05-04 00:41:28 +00:00
syuilo b0b6962661
New Crowdin updates (#15922)
* New translations ja-jp.yml (Catalan)

* New translations ja-jp.yml (English)

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

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

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

* New translations ja-jp.yml (Catalan)

* New translations ja-jp.yml (German)

* New translations ja-jp.yml (English)
2025-05-04 09:40:06 +09:00
おさむのひと 8c0c503c6c
refactor: ファイルアップロード時のテストを追加 (#15928)
* refactor: ファイルアップロード時のテストを追加

* なぜかsemverが消えてた
2025-05-04 09:38:35 +09:00
syuilo 590cb5f4dd perf(frontend): improve MkInstanceTicker rendering performance 2025-05-04 06:43:53 +09:00
syuilo 90e69f4d10
add note 2025-05-03 21:51:58 +09:00
syuilo e76e2534d7 perf(frontend): improve MkPullToRefresh render performance 2025-05-03 21:40:18 +09:00
syuilo 27682b980c
tweak MkPullToRefresh.vue 2025-05-03 21:14:59 +09:00
syuilo ef79cc290f perf(frontend): tweak PageWithHeader 2025-05-03 20:15:18 +09:00
syuilo e7c170cf0c tweak MkPullToRefresh 2025-05-03 18:35:43 +09:00
syuilo f0544ede87 tweak MkPullToRefresh 2025-05-03 16:51:23 +09:00
syuilo 00008d3763
Update CHANGELOG.md 2025-05-03 16:25:09 +09:00
おさむのひと 526057cc61
Revert "fix: 添付ファイルのあるリクエストを受けたときの初動を改善 (#15896)" (#15927)
* Revert "fix: 添付ファイルのあるリクエストを受けたときの初動を改善 (#15896)"

This reverts commit 7e8cc4d7c0.

* fix CHANGELOG.md
2025-05-03 16:23:06 +09:00
おさむのひと c13aa0c224
fix(backend): チャンネルフォロー一覧のsinceId/untilIdによる絞り込みが上手く動いていないのを修正 (#13698)
* fix(backend): チャンネルフォロー一覧のsinceId/untilIdによる絞り込みが上手く動いていないのを修正

* fix CHANGELOG.md

* docs(changelog): fix mistaken changelog insertion (restore newline)

* docs(changelog): update insertion position

---------

Co-authored-by: Sayamame-beans <61457993+Sayamame-beans@users.noreply.github.com>
2025-05-03 15:40:57 +09:00
anatawa12 1af98b690b
feat: CREATE INDEX CONCURRENTLY for "userId" "id" composite note index if admin wish. (#15915)
* chore: CREATE INDEX CONCURRENTLY for "userId" "id" composite note index

* chore: remove { concurrent: true } and comment why

* update comment

* feat: add MISSKEY_MIGRATION_CREATE_INDEX_CONCURRENTLY option

* fix: spdx license header

* alter comment

* chore: improve behavior when migration failure

* docs(changelog): 2025.4.1 で追加されたインデックスの再生成をノートの追加しながら行えるようになりました

* ちょっと表現を変更

---------

Co-authored-by: 饺子w (Yumechi) <35571479+eternal-flame-ad@users.noreply.github.com>
2025-05-03 12:57:50 +09:00
syuilo d25af911cf fix(frontend): tweak universal ui rendering 2025-05-03 11:19:55 +09:00
syuilo df1a3742dd feat(frontend): マウスでもタイムラインを引っ張って更新できるように & MkPullToRefreshのパフォーマンス向上 2025-05-03 10:26:40 +09:00
syuilo c5235a7b2f perf(frontend): improve timeline page performance 2025-05-02 20:25:51 +09:00
CGsama 0636de6a52 Fix 2025-01-26 18:10:45 -05:00
CGsama fb8312f989 Embedded all attachment, renotes and discussion history into rss feed content & improve title 2025-01-26 15:39:24 -05:00
52 changed files with 707 additions and 418 deletions

1
.github/min.node-version vendored Normal file
View File

@ -0,0 +1 @@
20.10.0

View File

@ -17,7 +17,6 @@ jobs:
strategy:
matrix:
node-version: [22.11.0]
api-json-name: [api-base.json, api-head.json]
include:
- api-json-name: api-base.json
@ -32,10 +31,10 @@ jobs:
submodules: true
- name: Setup pnpm
uses: pnpm/action-setup@v4.1.0
- name: Use Node.js ${{ matrix.node-version }}
- name: Use Node.js
uses: actions/setup-node@v4.4.0
with:
node-version: ${{ matrix.node-version }}
node-version-file: '.node-version'
cache: 'pnpm'
- run: pnpm i --frozen-lockfile
- name: Check pnpm-lock.yaml

View File

@ -15,22 +15,17 @@ jobs:
contents: read
id-token: write
strategy:
matrix:
node-version: [22.11.0]
steps:
- uses: actions/checkout@v4.2.2
with:
submodules: true
- name: Setup pnpm
uses: pnpm/action-setup@v4.1.0
- name: Use Node.js ${{ matrix.node-version }}
- name: Use Node.js
uses: actions/setup-node@v4.4.0
with:
node-version: ${{ matrix.node-version }}
node-version-file: '.node-version'
cache: 'pnpm'
registry-url: 'https://registry.npmjs.org'
- name: Publish package
run: |
pnpm i --frozen-lockfile

View File

@ -38,7 +38,7 @@ jobs:
run: git checkout "$(git rev-list --parents -n1 HEAD | cut -d" " -f3)"
- name: Setup pnpm
uses: pnpm/action-setup@v4.1.0
- name: Use Node.js 20.x
- name: Use Node.js
uses: actions/setup-node@v4.4.0
with:
node-version-file: '.node-version'

View File

@ -22,10 +22,11 @@ jobs:
unit:
name: Unit tests (backend)
runs-on: ubuntu-latest
strategy:
matrix:
node-version: [22.11.0]
node-version-file:
- .node-version
- .github/min.node-version
services:
postgres:
@ -61,10 +62,10 @@ jobs:
exit 1
fi
done
- name: Use Node.js ${{ matrix.node-version }}
- name: Use Node.js
uses: actions/setup-node@v4.4.0
with:
node-version: ${{ matrix.node-version }}
node-version-file: ${{ matrix.node-version-file }}
cache: 'pnpm'
- run: pnpm i --frozen-lockfile
- name: Check pnpm-lock.yaml
@ -84,10 +85,11 @@ jobs:
e2e:
name: E2E tests (backend)
runs-on: ubuntu-latest
strategy:
matrix:
node-version: [22.11.0]
node-version-file:
- .node-version
- .github/min.node-version
services:
postgres:
@ -108,10 +110,10 @@ jobs:
submodules: true
- name: Setup pnpm
uses: pnpm/action-setup@v4.1.0
- name: Use Node.js ${{ matrix.node-version }}
- name: Use Node.js
uses: actions/setup-node@v4.4.0
with:
node-version: ${{ matrix.node-version }}
node-version-file: ${{ matrix.node-version-file }}
cache: 'pnpm'
- run: pnpm i --frozen-lockfile
- name: Check pnpm-lock.yaml

View File

@ -21,7 +21,9 @@ jobs:
runs-on: ubuntu-latest
strategy:
matrix:
node-version: [22.11.0]
node-version-file:
- .node-version
- .github/min.node-version
steps:
- uses: actions/checkout@v4
with:
@ -43,10 +45,10 @@ jobs:
exit 1
fi
done
- name: Use Node.js ${{ matrix.node-version }}
- name: Use Node.js
uses: actions/setup-node@v4.4.0
with:
node-version: ${{ matrix.node-version }}
node-version-file: ${{ matrix.node-version-file }}
cache: 'pnpm'
- name: Build Misskey
run: |
@ -54,6 +56,7 @@ jobs:
pnpm build
- name: Setup
run: |
echo "NODE_VERSION=$(cat ${{ matrix.node-version-file }})" >> $GITHUB_ENV
cd packages/backend/test-federation
bash ./setup.sh
sudo chmod 644 ./certificates/*.test.key

View File

@ -27,20 +27,16 @@ jobs:
name: Unit tests (frontend)
runs-on: ubuntu-latest
strategy:
matrix:
node-version: [22.11.0]
steps:
- uses: actions/checkout@v4.2.2
with:
submodules: true
- name: Setup pnpm
uses: pnpm/action-setup@v4.1.0
- name: Use Node.js ${{ matrix.node-version }}
- name: Use Node.js
uses: actions/setup-node@v4.4.0
with:
node-version: ${{ matrix.node-version }}
node-version-file: '.node-version'
cache: 'pnpm'
- run: pnpm i --frozen-lockfile
- name: Check pnpm-lock.yaml
@ -64,7 +60,6 @@ jobs:
strategy:
fail-fast: false
matrix:
node-version: [22.11.0]
browser: [chrome]
services:
@ -92,10 +87,10 @@ jobs:
# if: ${{ matrix.browser == 'firefox' }}
- name: Setup pnpm
uses: pnpm/action-setup@v4.1.0
- name: Use Node.js ${{ matrix.node-version }}
- name: Use Node.js
uses: actions/setup-node@v4.4.0
with:
node-version: ${{ matrix.node-version }}
node-version-file: '.node-version'
cache: 'pnpm'
- run: pnpm i --frozen-lockfile
- name: Copy Configure

View File

@ -20,11 +20,6 @@ jobs:
runs-on: ubuntu-latest
strategy:
matrix:
node-version: [22.11.0]
# See supported Node.js release schedule at https://nodejs.org/en/about/releases/
steps:
- name: Checkout
uses: actions/checkout@v4.2.2
@ -32,10 +27,10 @@ jobs:
- name: Setup pnpm
uses: pnpm/action-setup@v4.1.0
- name: Setup Node.js ${{ matrix.node-version }}
- name: Setup Node.js
uses: actions/setup-node@v4.4.0
with:
node-version: ${{ matrix.node-version }}
node-version-file: '.node-version'
cache: 'pnpm'
- name: Install dependencies

View File

@ -15,20 +15,16 @@ jobs:
name: Production build
runs-on: ubuntu-latest
strategy:
matrix:
node-version: [22.11.0]
steps:
- uses: actions/checkout@v4.2.2
with:
submodules: true
- name: Setup pnpm
uses: pnpm/action-setup@v4.1.0
- name: Use Node.js ${{ matrix.node-version }}
- name: Use Node.js
uses: actions/setup-node@v4.4.0
with:
node-version: ${{ matrix.node-version }}
node-version-file: '.node-version'
cache: 'pnpm'
- run: pnpm i --frozen-lockfile
- name: Check pnpm-lock.yaml

View File

@ -16,20 +16,16 @@ jobs:
validate-api-json:
runs-on: ubuntu-latest
strategy:
matrix:
node-version: [22.11.0]
steps:
- uses: actions/checkout@v4.2.2
with:
submodules: true
- name: Setup pnpm
uses: pnpm/action-setup@v4.1.0
- name: Use Node.js ${{ matrix.node-version }}
- name: Use Node.js
uses: actions/setup-node@v4.4.0
with:
node-version: ${{ matrix.node-version }}
node-version-file: '.node-version'
cache: 'pnpm'
- name: Install Redocly CLI
run: npm i -g @redocly/cli

View File

@ -1,14 +1,25 @@
## Unreleased
## 2025.5.0
### General
-
### Client
-
- Feat: マウスでもタイムラインを引っ張って更新できるように
- アクセシビリティ設定からオフにすることもできます
- Enhance: タイムラインのパフォーマンスを向上
- Fix: 一部のブラウザでアコーディオンメニューのアニメーションが動作しない問題を修正
### Server
- Enhance: 凍結されたユーザのノートが各種タイムラインで表示されないように `#15775`
- Enhance: 連合先のソフトウェア及びバージョン名により配信停止を行えるように `#15727`
- Enhance: 2025.4.1 で追加されたインデックスの再生成をノートの追加しながら行えるようになりました。 `#15915`
- `MISSKEY_MIGRATION_CREATE_INDEX_CONCURRENTLY` 環境変数を `1` にセットしていると、巨大なテーブルの既存のカラムに関するインデックス再生成が`CREATE INDEX CONCURRENTLY`を使用するようになりました。
- 複数のサーバープロセスをクラスタリングしているサーバーにおいて、一部のプロセスが起動している状態でこのオプションを有効にしてマイグレーションすることにより、ダウンタイムを削減することができます。
- ただし、このオプションを有効にする場合、インデックスの作成にかかる時間が倍~3倍以上になることがあります。
- また、大きなインスタンスである場合にはインデックスの作成に失敗し、複数回再試行する必要がある可能性があります。
- Fix: チャンネルのフォロー一覧の結果が一部正しくないのを修正 (#12175)
- Fix: ファイルをアップロードした際にファイル名が常に untitled になる問題を修正
- Fix: ファイルのアップロードに失敗することがある問題を修正
## 2025.4.1

View File

@ -220,6 +220,7 @@ silenceThisInstance: "Silencia aquesta instància "
mediaSilenceThisInstance: "Silenciar els arxius d'aquesta instància "
operations: "Accions"
software: "Programari"
softwareName: "Nom del programari"
version: "Versió"
metadata: "Metadades"
withNFiles: "{n} fitxer(s)"
@ -1423,6 +1424,7 @@ _settings:
ifOn: "Quan s'activa"
ifOff: "Quan es desactiva"
enableSyncThemesBetweenDevices: "Sincronitzar els temes instal·lats entre dispositius"
enablePullToRefresh: "Lliscar i actualitzar "
_chat:
showSenderName: "Mostrar el nom del remitent"
sendOnEnter: "Introdueix per enviar"
@ -1468,6 +1470,7 @@ _delivery:
manuallySuspended: "Suspendre manualment"
goneSuspended: "Servidor suspès perquè el servidor s'ha esborrat"
autoSuspendedForNotResponding: "Servidor suspès perquè el servidor no respon"
softwareSuspended: "Suspès perquè el programari ha deixat de desenvolupar-se "
_bubbleGame:
howToPlay: "Com es juga"
hold: "Mantenir"
@ -1599,6 +1602,8 @@ _serverSettings:
openRegistration: "Registres oberts"
openRegistrationWarning: "Obrir els registres és arriscat. Es recomana obrir-los només si el servidor és monitorat constantment i per respondre immediatament davant qualsevol problema."
thisSettingWillAutomaticallyOffWhenModeratorsInactive: "Si no es detecta activitat per part del moderador durant un període de temps, aquesta opció es desactiva automàticament per evitar el correu brossa."
deliverSuspendedSoftware: "Programari que ja no es distribueix"
deliverSuspendedSoftwareDescription: "Pots especificar un rang de noms i versions del programari del servidor per detenir l'entrega, per exemple, degut a vulnerabilitats. Aquesta informació la proporciona el servidor i la seva fiabilitat no es garantitzada. Es pot fer servir una especificació de rang sencer per especificar una versió, però es recomana especificar una versió anterior, com >= 2024.3.1-0, perquè especificar >= 2024.3.1 no incloure versions personalitzades com 2024.3.1-custom.0."
_accountMigration:
moveFrom: "Migrar un altre compte a aquest"
moveFromSub: "Crear un àlies per un altre compte"

View File

@ -220,6 +220,7 @@ silenceThisInstance: "Instanz stummschalten"
mediaSilenceThisInstance: "Medien dieses Servers stummschalten"
operations: "Aktionen"
software: "Software"
softwareName: "Software Name"
version: "Version"
metadata: "Metadaten"
withNFiles: "{n} Datei(en)"
@ -1423,6 +1424,7 @@ _settings:
ifOn: "Wenn eingeschaltet"
ifOff: "Wenn ausgeschaltet"
enableSyncThemesBetweenDevices: "Synchronisierung von installierten Themen auf verschiedenen Endgeräten"
enablePullToRefresh: "Ziehen zum Aktualisieren"
_chat:
showSenderName: "Name des Absenders anzeigen"
sendOnEnter: "Eingabetaste sendet Nachricht"
@ -1468,6 +1470,7 @@ _delivery:
manuallySuspended: "Manuell gesperrt"
goneSuspended: "Gesperrt wegen Löschung des Servers"
autoSuspendedForNotResponding: "Gesperrt, weil der Server nicht antwortet"
softwareSuspended: "Ausgesetzt, weil die Software nicht mehr beliefert wird"
_bubbleGame:
howToPlay: "Wie man spielt"
hold: "Halten"
@ -1599,6 +1602,8 @@ _serverSettings:
openRegistration: "Registrierung von Konten aktivieren"
openRegistrationWarning: "Das Aktivieren von Registrierungen ist riskant. Es wird empfohlen, sie nur dann zu aktivieren, wenn der Server ständig überwacht wird und im Falle eines Problems sofort reagiert werden kann."
thisSettingWillAutomaticallyOffWhenModeratorsInactive: "Wenn über einen bestimmten Zeitraum keine Moderatorenaktivität festgestellt wird, wird diese Einstellung automatisch deaktiviert, um Spam zu verhindern."
deliverSuspendedSoftware: "Software, die nicht mehr beliefert wird"
deliverSuspendedSoftwareDescription: "Sie können eine Auswahl von Namen und Versionen verschiedener Serversoftware angeben, um die Zustellung zu stoppen, z. B. aufgrund von Sicherheitslücken. Diese Versionsinformationen werden vom Server bereitgestellt und ihre Zuverlässigkeit ist nicht garantiert. Es wird jedoch empfohlen, eine Vorabversion anzugeben, wie z. B. >= 2024.3.1-0, da die Angabe >= 2024.3.1 keine benutzerdefinierten Versionen wie 2024.3.1-custom.0 einschließt."
_accountMigration:
moveFrom: "Von einem anderen Konto zu diesem migrieren"
moveFromSub: "Alias für ein anderes Konto erstellen"

View File

@ -220,6 +220,7 @@ silenceThisInstance: "Silence this instance"
mediaSilenceThisInstance: "Media-silence this server"
operations: "Operations"
software: "Software"
softwareName: "Software"
version: "Version"
metadata: "Metadata"
withNFiles: "{n} file(s)"
@ -1423,6 +1424,7 @@ _settings:
ifOn: "When turned on"
ifOff: "When turned off"
enableSyncThemesBetweenDevices: "Synchronize installed themes across devices"
enablePullToRefresh: "Pull to Refresh"
_chat:
showSenderName: "Show sender's name"
sendOnEnter: "Press Enter to send"
@ -1468,6 +1470,7 @@ _delivery:
manuallySuspended: "Manually suspended"
goneSuspended: "Server is suspended due to server deletion"
autoSuspendedForNotResponding: "Server is suspended due to no responding"
softwareSuspended: "Suspended as this software is no longer being distributed to"
_bubbleGame:
howToPlay: "How to play"
hold: "Hold"
@ -1599,6 +1602,8 @@ _serverSettings:
openRegistration: "Make the account creation open"
openRegistrationWarning: "Opening registration carries risks. It is recommended to only enable it if you have a system in place to continuously monitor the server and respond immediately in case of any issues."
thisSettingWillAutomaticallyOffWhenModeratorsInactive: "If no moderator activity is detected for a while, this setting will be automatically turned off to prevent spam."
deliverSuspendedSoftware: "Suspended Software"
deliverSuspendedSoftwareDescription: "You can specify a range of names and versions of the server's software to stop delivery for vulnerability or other reasons. This version information is provided by the server and is not guaranteed to be reliable. A semver range specification can be used to specify the version, but specifying >= 2024.3.1 will not include custom versions such as 2024.3.1-custom.0, so it is recommended that a prerelease specification be used, such as >= 2024.3.1-0"
_accountMigration:
moveFrom: "Migrate another account to this one"
moveFromSub: "Create alias to another account"

8
locales/index.d.ts vendored
View File

@ -5709,6 +5709,14 @@ export interface Locale extends ILocale {
*
*/
"enableSyncThemesBetweenDevices": string;
/**
*
*/
"enablePullToRefresh": string;
/**
*
*/
"enablePullToRefresh_description": string;
"_chat": {
/**
*

View File

@ -1427,6 +1427,8 @@ _settings:
ifOn: "オンのとき"
ifOff: "オフのとき"
enableSyncThemesBetweenDevices: "デバイス間でインストールしたテーマを同期"
enablePullToRefresh: "ひっぱって更新"
enablePullToRefresh_description: "マウスでは、ホイールを押し込みながらドラッグします。"
_chat:
showSenderName: "送信者の名前を表示"

View File

@ -220,6 +220,7 @@ silenceThisInstance: "静音此服务器"
mediaSilenceThisInstance: "隐藏此服务器的媒体文件"
operations: "操作"
software: "软件"
softwareName: "软件名"
version: "版本"
metadata: "元数据"
withNFiles: "{n} 个文件"
@ -1422,6 +1423,7 @@ _settings:
showNavbarSubButtons: "在导航栏中显示副按钮"
ifOn: "启用时"
ifOff: "关闭时"
enablePullToRefresh: "开启下拉刷新"
_chat:
showSenderName: "显示发送者的名字"
sendOnEnter: "回车键发送"
@ -1467,6 +1469,7 @@ _delivery:
manuallySuspended: "手动停止中"
goneSuspended: "因服务器被删除而停止"
autoSuspendedForNotResponding: "因服务器无应答而停止"
softwareSuspended: "因有不可用的软件而停止"
_bubbleGame:
howToPlay: "游戏说明"
hold: "抓住"
@ -1598,6 +1601,7 @@ _serverSettings:
openRegistration: "开放注册"
openRegistrationWarning: "开放注册有风险。建议仅当能够持续监控服务器并在出现问题时能够立即响应时才打开它。"
thisSettingWillAutomaticallyOffWhenModeratorsInactive: "若在一段时间内没有检测到管理活动,为防止垃圾信息,此设定将自动关闭。"
deliverSuspendedSoftware: "不可用的软件"
_accountMigration:
moveFrom: "从别的账号迁移到此账户"
moveFromSub: "为另一个账户建立别名"

View File

@ -220,6 +220,7 @@ silenceThisInstance: "禁言此伺服器"
mediaSilenceThisInstance: "將這個伺服器的媒體設為禁言"
operations: "操作"
software: "軟體"
softwareName: "軟體名稱"
version: "版本"
metadata: "詮釋資料"
withNFiles: "{n} 個檔案"
@ -1423,6 +1424,7 @@ _settings:
ifOn: "開啟時"
ifOff: "關閉時"
enableSyncThemesBetweenDevices: "在裝置之間同步已安裝的主題"
enablePullToRefresh: "下拉更新"
_chat:
showSenderName: "顯示發送者的名稱"
sendOnEnter: "按下 Enter 發送訊息"
@ -1468,6 +1470,7 @@ _delivery:
manuallySuspended: "手動暫停中"
goneSuspended: "因為伺服器刪除所以暫停中"
autoSuspendedForNotResponding: "因為伺服器沒有回應所以暫停中"
softwareSuspended: "此軟體因已停止發佈,目前無法使用"
_bubbleGame:
howToPlay: "玩法說明"
hold: "保留"
@ -1599,6 +1602,8 @@ _serverSettings:
openRegistration: "允許建立帳戶"
openRegistrationWarning: "開放註冊伴隨著風險。 建議只有在伺服器受到持續監控,並準備好在出現問題時能立即處理的情況下才開放註冊。"
thisSettingWillAutomaticallyOffWhenModeratorsInactive: "如果在一段期間內沒有偵測到任何審查員活動,此設定將自動關閉,以防止垃圾內容。"
deliverSuspendedSoftware: "已停止發佈的軟體"
deliverSuspendedSoftwareDescription: "由於脆弱性等原因,可以指定伺服器軟體的名稱與版本範圍來停止其發佈。這些版本資訊是由伺服器所提供,其可靠性無法保證。版本的指定可以使用 semver語意化版本控制 的範圍語法,但如果指定為 >= 2024.3.1,則像 2024.3.1-custom.0 這樣的自訂版本將不會被包含在內,因此建議使用 >= 2024.3.1-0 的方式來同時包含預發佈版本。"
_accountMigration:
moveFrom: "從其他帳戶遷移到這個帳戶"
moveFromSub: "為另一個帳戶建立別名"

View File

@ -1,6 +1,6 @@
{
"name": "misskey",
"version": "2025.4.1",
"version": "2025.5.0-alpha.0",
"codename": "nasubi",
"repository": {
"type": "git",

View File

@ -3,11 +3,25 @@
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { isConcurrentIndexMigrationEnabled } from "./js/migration-config.js";
export class CompositeNoteIndex1745378064470 {
name = 'CompositeNoteIndex1745378064470';
transaction = isConcurrentIndexMigrationEnabled() ? false : undefined;
async up(queryRunner) {
await queryRunner.query(`CREATE INDEX "IDX_724b311e6f883751f261ebe378" ON "note" ("userId", "id" DESC)`);
const concurrently = isConcurrentIndexMigrationEnabled();
if (concurrently) {
const hasValidIndex = await queryRunner.query(`SELECT indisvalid FROM pg_index INNER JOIN pg_class ON pg_index.indexrelid = pg_class.oid WHERE pg_class.relname = 'IDX_724b311e6f883751f261ebe378'`);
if (!hasValidIndex || hasValidIndex[0].indisvalid !== true) {
await queryRunner.query(`DROP INDEX IF EXISTS "IDX_724b311e6f883751f261ebe378"`);
await queryRunner.query(`CREATE INDEX CONCURRENTLY "IDX_724b311e6f883751f261ebe378" ON "note" ("userId", "id" DESC)`);
}
} else {
await queryRunner.query(`CREATE INDEX IF NOT EXISTS "IDX_724b311e6f883751f261ebe378" ON "note" ("userId", "id" DESC)`);
}
await queryRunner.query(`DROP INDEX IF EXISTS "IDX_5b87d9d19127bd5d92026017a7"`);
// Flush all cached Linear Scan Plans and redo statistics for composite index
// this is important for Postgres to learn that even in highly complex queries, using this index first can reduce the result set significantly
@ -15,7 +29,8 @@ export class CompositeNoteIndex1745378064470 {
}
async down(queryRunner) {
const mayConcurrently = isConcurrentIndexMigrationEnabled() ? 'CONCURRENTLY' : '';
await queryRunner.query(`DROP INDEX IF EXISTS "IDX_724b311e6f883751f261ebe378"`);
await queryRunner.query(`CREATE INDEX "IDX_5b87d9d19127bd5d92026017a7" ON "note" ("userId")`);
await queryRunner.query(`CREATE INDEX ${mayConcurrently} "IDX_5b87d9d19127bd5d92026017a7" ON "note" ("userId")`);
}
}

View File

@ -0,0 +1,8 @@
/*
* SPDX-FileCopyrightText: syuilo and misskey-project
* SPDX-License-Identifier: AGPL-3.0-only
*/
export function isConcurrentIndexMigrationEnabled() {
return process.env.MISSKEY_MIGRATION_CREATE_INDEX_CONCURRENTLY === '1';
}

View File

@ -1,6 +1,7 @@
import { DataSource } from 'typeorm';
import { loadConfig } from './built/config.js';
import { entities } from './built/postgres.js';
import { isConcurrentIndexMigrationEnabled } from "./migration/js/migration-config.js";
const config = loadConfig();
@ -14,4 +15,5 @@ export default new DataSource({
extra: config.db.extra,
entities: entities,
migrations: ['migration/*.js'],
migrationsTransactionMode: isConcurrentIndexMigrationEnabled() ? 'each' : 'all',
});

View File

@ -24,8 +24,13 @@ const $config: Provider = {
const $db: Provider = {
provide: DI.db,
useFactory: async (config) => {
const db = createPostgresDataSource(config);
return await db.initialize();
try {
const db = createPostgresDataSource(config);
return await db.initialize();
} catch (e) {
console.log(e);
throw e;
}
},
inject: [DI.config],
};

View File

@ -43,29 +43,36 @@ export class QueryService {
) {
}
public makePaginationQuery<T extends ObjectLiteral>(q: SelectQueryBuilder<T>, sinceId?: string | null, untilId?: string | null, sinceDate?: number | null, untilDate?: number | null): SelectQueryBuilder<T> {
public makePaginationQuery<T extends ObjectLiteral>(
q: SelectQueryBuilder<T>,
sinceId?: string | null,
untilId?: string | null,
sinceDate?: number | null,
untilDate?: number | null,
targetColumn = 'id',
): SelectQueryBuilder<T> {
if (sinceId && untilId) {
q.andWhere(`${q.alias}.id > :sinceId`, { sinceId: sinceId });
q.andWhere(`${q.alias}.id < :untilId`, { untilId: untilId });
q.orderBy(`${q.alias}.id`, 'DESC');
q.andWhere(`${q.alias}.${targetColumn} > :sinceId`, { sinceId: sinceId });
q.andWhere(`${q.alias}.${targetColumn} < :untilId`, { untilId: untilId });
q.orderBy(`${q.alias}.${targetColumn}`, 'DESC');
} else if (sinceId) {
q.andWhere(`${q.alias}.id > :sinceId`, { sinceId: sinceId });
q.orderBy(`${q.alias}.id`, 'ASC');
q.andWhere(`${q.alias}.${targetColumn} > :sinceId`, { sinceId: sinceId });
q.orderBy(`${q.alias}.${targetColumn}`, 'ASC');
} else if (untilId) {
q.andWhere(`${q.alias}.id < :untilId`, { untilId: untilId });
q.orderBy(`${q.alias}.id`, 'DESC');
q.andWhere(`${q.alias}.${targetColumn} < :untilId`, { untilId: untilId });
q.orderBy(`${q.alias}.${targetColumn}`, 'DESC');
} else if (sinceDate && untilDate) {
q.andWhere(`${q.alias}.id > :sinceId`, { sinceId: this.idService.gen(sinceDate) });
q.andWhere(`${q.alias}.id < :untilId`, { untilId: this.idService.gen(untilDate) });
q.orderBy(`${q.alias}.id`, 'DESC');
q.andWhere(`${q.alias}.${targetColumn} > :sinceId`, { sinceId: this.idService.gen(sinceDate) });
q.andWhere(`${q.alias}.${targetColumn} < :untilId`, { untilId: this.idService.gen(untilDate) });
q.orderBy(`${q.alias}.${targetColumn}`, 'DESC');
} else if (sinceDate) {
q.andWhere(`${q.alias}.id > :sinceId`, { sinceId: this.idService.gen(sinceDate) });
q.orderBy(`${q.alias}.id`, 'ASC');
q.andWhere(`${q.alias}.${targetColumn} > :sinceId`, { sinceId: this.idService.gen(sinceDate) });
q.orderBy(`${q.alias}.${targetColumn}`, 'ASC');
} else if (untilDate) {
q.andWhere(`${q.alias}.id < :untilId`, { untilId: this.idService.gen(untilDate) });
q.orderBy(`${q.alias}.id`, 'DESC');
q.andWhere(`${q.alias}.${targetColumn} < :untilId`, { untilId: this.idService.gen(untilDate) });
q.orderBy(`${q.alias}.${targetColumn}`, 'DESC');
} else {
q.orderBy(`${q.alias}.id`, 'DESC');
q.orderBy(`${q.alias}.${targetColumn}`, 'DESC');
}
return q;
}

View File

@ -10,6 +10,16 @@ import { MiUser } from './User.js';
import { MiChannel } from './Channel.js';
import type { MiDriveFile } from './DriveFile.js';
// Note: When you create a new index for existing column of this table,
// it might be better to index concurrently under isConcurrentIndexMigrationEnabled flag
// by editing generated migration file since this table is very large,
// and it will make a long lock to create index in most cases.
// Please note that `CREATE INDEX CONCURRENTLY` is not supported in transaction,
// so you need to set `transaction = false` in migration if isConcurrentIndexMigrationEnabled() is true.
// Please refer 1745378064470-composite-note-index.js for example.
// You should not use `@Index({ concurrent: true })` decorator because database initialization for test will fail
// because it will always run CREATE INDEX in transaction based on decorators.
// Not appending `{ concurrent: true }` to `@Index` will not cause any problem in production,
@Index(['userId', 'id'])
@Entity('note')
export class MiNote {

View File

@ -7,7 +7,7 @@ import cluster from 'node:cluster';
import * as fs from 'node:fs';
import { fileURLToPath } from 'node:url';
import { Inject, Injectable, OnApplicationShutdown } from '@nestjs/common';
import Fastify, { FastifyInstance } from 'fastify';
import Fastify, { type FastifyInstance } from 'fastify';
import fastifyStatic from '@fastify/static';
import fastifyRawBody from 'fastify-raw-body';
import { IsNull } from 'typeorm';
@ -73,7 +73,7 @@ export class ServerService implements OnApplicationShutdown {
}
@bindThis
public async launch() {
public async launch(): Promise<void> {
const fastify = Fastify({
trustProxy: true,
logger: false,
@ -133,8 +133,8 @@ export class ServerService implements OnApplicationShutdown {
reply.header('content-type', 'text/plain; charset=utf-8');
reply.header('link', `<${encodeURI(location)}>; rel="canonical"`);
done(null, [
'Refusing to relay remote ActivityPub object lookup.',
'',
"Refusing to relay remote ActivityPub object lookup.",
"",
`Please remove 'application/activity+json' and 'application/ld+json' from the Accept header or fetch using the authoritative URL at ${location}.`,
].join('\n'));
});
@ -301,7 +301,6 @@ export class ServerService implements OnApplicationShutdown {
}
await fastify.ready();
return fastify;
}
@bindThis
@ -310,6 +309,13 @@ export class ServerService implements OnApplicationShutdown {
await this.#fastify.close();
}
/**
* Get the Fastify instance for testing.
*/
public get fastify(): FastifyInstance {
return this.#fastify;
}
@bindThis
async onApplicationShutdown(signal: string): Promise<void> {
await this.dispose();

View File

@ -6,11 +6,8 @@
import { randomUUID } from 'node:crypto';
import * as fs from 'node:fs';
import * as stream from 'node:stream/promises';
import { Transform } from 'node:stream';
import { type MultipartFile } from '@fastify/multipart';
import { Inject, Injectable } from '@nestjs/common';
import * as Sentry from '@sentry/node';
import { AttachmentFile } from '@/server/api/endpoint-base.js';
import { DI } from '@/di-symbols.js';
import { getIpHash } from '@/misc/get-ip-hash.js';
import type { MiLocalUser, MiUser } from '@/models/User.js';
@ -19,7 +16,7 @@ import type Logger from '@/logger.js';
import type { MiMeta, UserIpsRepository } from '@/models/_.js';
import { createTemp } from '@/misc/create-temp.js';
import { bindThis } from '@/decorators.js';
import { type RolePolicies, RoleService } from '@/core/RoleService.js';
import { RoleService } from '@/core/RoleService.js';
import type { Config } from '@/config.js';
import { ApiError } from './error.js';
import { RateLimiterService } from './RateLimiterService.js';
@ -203,6 +200,18 @@ export class ApiCallService implements OnApplicationShutdown {
return;
}
const [path, cleanup] = await createTemp();
await stream.pipeline(multipartData.file, fs.createWriteStream(path));
// ファイルサイズが制限を超えていた場合
// なお 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;
@ -217,7 +226,10 @@ export class ApiCallService implements OnApplicationShutdown {
return;
}
this.authenticateService.authenticate(token).then(([user, app]) => {
this.call(endpoint, user, app, fields, multipartData, request).then((res) => {
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);
@ -282,7 +294,10 @@ export class ApiCallService implements OnApplicationShutdown {
user: MiLocalUser | null | undefined,
token: MiAccessToken | null | undefined,
data: any,
multipartFile: MultipartFile | null,
file: {
name: string;
path: string;
} | null,
request: FastifyRequest<{ Body: Record<string, unknown> | undefined, Querystring: Record<string, unknown> }>,
) {
const isSecure = user != null && token == null;
@ -356,37 +371,6 @@ export class ApiCallService implements OnApplicationShutdown {
}
}
// Cast non JSON input
if ((ep.meta.requireFile || request.method === 'GET') && ep.params.properties) {
for (const k of Object.keys(ep.params.properties)) {
const param = ep.params.properties![k];
if (['boolean', 'number', 'integer'].includes(param.type ?? '') && typeof data[k] === 'string') {
try {
data[k] = JSON.parse(data[k]);
} catch (e) {
throw new ApiError({
message: 'Invalid param.',
code: 'INVALID_PARAM',
id: '0b5f1631-7c1a-41a6-b399-cce335f34d85',
}, {
param: k,
reason: `cannot cast to ${param.type}`,
});
}
}
}
}
if (token && ((ep.meta.kind && !token.permission.some(p => p === ep.meta.kind))
|| (!ep.meta.kind && (ep.meta.requireCredential || ep.meta.requireModerator || ep.meta.requireAdmin)))) {
throw new ApiError({
message: 'Your app does not have the necessary permissions to use this endpoint.',
code: 'PERMISSION_DENIED',
kind: 'permission',
id: '1370e5b7-d4eb-4566-bb1d-7748ee6a1838',
});
}
if ((ep.meta.requireModerator || ep.meta.requireAdmin) && (this.meta.rootUserId !== user!.id)) {
const myRoles = await this.roleService.getUserRoles(user!.id);
if (ep.meta.requireModerator && !myRoles.some(r => r.isModerator || r.isAdministrator)) {
@ -420,91 +404,49 @@ export class ApiCallService implements OnApplicationShutdown {
}
}
let attachmentFile: AttachmentFile | null = null;
let cleanup = () => {};
if (ep.meta.requireFile && request.method === 'POST' && multipartFile) {
const policies = await this.roleService.getUserPolicies(user!.id);
const result = await this.handleAttachmentFile(
Math.min((policies.maxFileSizeMb * 1024 * 1024), this.config.maxFileSize),
multipartFile,
);
attachmentFile = result.attachmentFile;
cleanup = result.cleanup;
if (token && ((ep.meta.kind && !token.permission.some(p => p === ep.meta.kind))
|| (!ep.meta.kind && (ep.meta.requireCredential || ep.meta.requireModerator || ep.meta.requireAdmin)))) {
throw new ApiError({
message: 'Your app does not have the necessary permissions to use this endpoint.',
code: 'PERMISSION_DENIED',
kind: 'permission',
id: '1370e5b7-d4eb-4566-bb1d-7748ee6a1838',
});
}
// Cast non JSON input
if ((ep.meta.requireFile || request.method === 'GET') && ep.params.properties) {
for (const k of Object.keys(ep.params.properties)) {
const param = ep.params.properties![k];
if (['boolean', 'number', 'integer'].includes(param.type ?? '') && typeof data[k] === 'string') {
try {
data[k] = JSON.parse(data[k]);
} catch (e) {
throw new ApiError({
message: 'Invalid param.',
code: 'INVALID_PARAM',
id: '0b5f1631-7c1a-41a6-b399-cce335f34d85',
}, {
param: k,
reason: `cannot cast to ${param.type}`,
});
}
}
}
}
// API invoking
if (this.config.sentryForBackend) {
return await Sentry.startSpan({
name: 'API: ' + ep.name,
}, () => {
return ep.exec(data, user, token, attachmentFile, request.ip, request.headers)
.catch((err: Error) => this.#onExecError(ep, data, err, user?.id))
.finally(() => cleanup());
});
}, () => ep.exec(data, user, token, file, request.ip, request.headers)
.catch((err: Error) => this.#onExecError(ep, data, err, user?.id)));
} else {
return await ep.exec(data, user, token, attachmentFile, request.ip, request.headers)
.catch((err: Error) => this.#onExecError(ep, data, err, user?.id))
.finally(() => cleanup());
return await ep.exec(data, user, token, file, request.ip, request.headers)
.catch((err: Error) => this.#onExecError(ep, data, err, user?.id));
}
}
@bindThis
private async handleAttachmentFile(
fileSizeLimit: number,
multipartFile: MultipartFile,
) {
function createTooLongError() {
return new ApiError({
httpStatusCode: 413,
kind: 'client',
message: 'File size is too large.',
code: 'FILE_SIZE_TOO_LARGE',
id: 'ff827ce8-9b4b-4808-8511-422222a3362f',
});
}
function createLimitStream(limit: number) {
let total = 0;
return new Transform({
transform(chunk, _, callback) {
total += chunk.length;
if (total > limit) {
callback(createTooLongError());
} else {
callback(null, chunk);
}
},
});
}
const [path, cleanup] = await createTemp();
try {
await stream.pipeline(
multipartFile.file,
createLimitStream(fileSizeLimit),
fs.createWriteStream(path),
);
// ファイルサイズが制限を超えていた場合
// なお truncated はストリームを読み切ってからでないと機能しないため、stream.pipeline より後にある必要がある
if (multipartFile.file.truncated) {
throw createTooLongError();
}
} catch (err) {
cleanup();
throw err;
}
return {
attachmentFile: {
name: multipartFile.filename,
path,
},
cleanup,
};
}
@bindThis
public dispose(): void {
clearInterval(this.userIpHistoriesClearIntervalId);

View File

@ -21,23 +21,23 @@ ajv.addFormat('misskey:id', /^[a-zA-Z0-9]+$/);
export type Response = Record<string, any> | void;
export type AttachmentFile = {
type File = {
name: string | null;
path: string;
};
// TODO: paramsの型をT['params']のスキーマ定義から推論する
type Executor<T extends IEndpointMeta, Ps extends Schema> =
(params: SchemaType<Ps>, user: T['requireCredential'] extends true ? MiLocalUser : MiLocalUser | null, token: MiAccessToken | null, file?: AttachmentFile, cleanup?: () => any, ip?: string | null, headers?: Record<string, string> | null) =>
Promise<T['res'] extends undefined ? Response : SchemaType<NonNullable<T['res']>>>;
(params: SchemaType<Ps>, user: T['requireCredential'] extends true ? MiLocalUser : MiLocalUser | null, token: MiAccessToken | null, file?: File, cleanup?: () => any, ip?: string | null, headers?: Record<string, string> | null) =>
Promise<T['res'] extends undefined ? Response : SchemaType<NonNullable<T['res']>>>;
export abstract class Endpoint<T extends IEndpointMeta, Ps extends Schema> {
public exec: (params: any, user: T['requireCredential'] extends true ? MiLocalUser : MiLocalUser | null, token: MiAccessToken | null, file?: AttachmentFile, ip?: string | null, headers?: Record<string, string> | null) => Promise<any>;
public exec: (params: any, user: T['requireCredential'] extends true ? MiLocalUser : MiLocalUser | null, token: MiAccessToken | null, file?: File, ip?: string | null, headers?: Record<string, string> | null) => Promise<any>;
constructor(meta: T, paramDef: Ps, cb: Executor<T, Ps>) {
const validate = ajv.compile(paramDef);
this.exec = (params: any, user: T['requireCredential'] extends true ? MiLocalUser : MiLocalUser | null, token: MiAccessToken | null, file?: AttachmentFile, ip?: string | null, headers?: Record<string, string> | null) => {
this.exec = (params: any, user: T['requireCredential'] extends true ? MiLocalUser : MiLocalUser | null, token: MiAccessToken | null, file?: File, ip?: string | null, headers?: Record<string, string> | null) => {
let cleanup: undefined | (() => void) = undefined;
if (meta.requireFile) {

View File

@ -48,7 +48,15 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
private queryService: QueryService,
) {
super(meta, paramDef, async (ps, me) => {
const query = this.queryService.makePaginationQuery(this.channelFollowingsRepository.createQueryBuilder(), ps.sinceId, ps.untilId)
const query = this.queryService
.makePaginationQuery(
this.channelFollowingsRepository.createQueryBuilder(),
ps.sinceId,
ps.untilId,
null,
null,
'followeeId',
)
.andWhere({ followerId: me.id });
const followings = await query

View File

@ -61,6 +61,7 @@ export const meta = {
message: 'Cannot upload the file because it exceeds the maximum file size.',
code: 'MAX_FILE_SIZE_EXCEEDED',
id: 'b9d8c348-33f0-4673-b9a9-5d4da058977a',
httpStatusCode: 413,
},
},
} as const;

View File

@ -7,7 +7,7 @@ import { Inject, Injectable } from '@nestjs/common';
import { In, IsNull } from 'typeorm';
import { Feed } from 'feed';
import { DI } from '@/di-symbols.js';
import type { DriveFilesRepository, NotesRepository, UserProfilesRepository } from '@/models/_.js';
import type { DriveFilesRepository, NotesRepository, UserProfilesRepository, UsersRepository } from '@/models/_.js';
import type { Config } from '@/config.js';
import type { MiUser } from '@/models/User.js';
import { UserEntityService } from '@/core/entities/UserEntityService.js';
@ -16,13 +16,23 @@ import { bindThis } from '@/decorators.js';
import { IdService } from '@/core/IdService.js';
import { MfmService } from "@/core/MfmService.js";
import { parse as mfmParse } from 'mfm-js';
import { MiNote } from '@/models/Note.js';
import { isQuote, isRenote } from '@/misc/is-renote.js';
import { getNoteSummary } from '@/misc/get-note-summary.js';
import Logger from '@/logger.js';
import { LoggerService } from '@/core/LoggerService.js';
@Injectable()
export class FeedService {
private readonly logger: Logger;
constructor(
@Inject(DI.config)
private config: Config,
@Inject(DI.usersRepository)
private usersRepository: UsersRepository,
@Inject(DI.userProfilesRepository)
private userProfilesRepository: UserProfilesRepository,
@ -36,13 +46,17 @@ export class FeedService {
private driveFileEntityService: DriveFileEntityService,
private idService: IdService,
private mfmService: MfmService,
loggerService: LoggerService,
) {
this.logger = loggerService.getLogger('feed');
}
@bindThis
public async packFeed(user: MiUser) {
const author = {
link: `${this.config.url}/@${user.username}`,
email: `${user.username}@${this.config.host}`,
name: user.name ?? user.username,
};
@ -51,7 +65,6 @@ export class FeedService {
const notes = await this.notesRepository.find({
where: {
userId: user.id,
renoteId: IsNull(),
visibility: In(['public', 'home']),
},
order: { id: -1 },
@ -75,22 +88,111 @@ export class FeedService {
});
for (const note of notes) {
const files = note.fileIds.length > 0 ? await this.driveFilesRepository.findBy({
id: In(note.fileIds),
}) : [];
const file = files.find(file => file.type.startsWith('image/'));
const text = note.text;
let contentStr = await this.noteToString(note, true);
let next = note.renoteId ? note.renoteId : note.replyId;
let depth = 10;
const noteintitle = true;
let title = `Post by ${author.name}`;
while (depth > 0 && next) {
const finding = await this.findById(next);
contentStr += finding.text;
next = finding.next;
depth -= 1;
}
if (noteintitle) {
if (note.renoteId) {
title = `Boost by ${author.name}`;
} else if (note.replyId) {
title = `Reply by ${author.name}`;
} else {
title = `Post by ${author.name}`;
}
const effectiveNote =
!isQuote(note) && note.renote != null ? note.renote : note;
const content = getNoteSummary(effectiveNote);
if (content) {
title += `: ${content}`;
}
}
feed.addItem({
title: `New note by ${author.name}`,
title: this.escapeCDATA(title).substring(0, 100),
link: `${this.config.url}/notes/${note.id}`,
date: this.idService.parse(note.id).date,
description: note.cw ?? undefined,
content: text ? this.mfmService.toHtml(mfmParse(text), JSON.parse(note.mentionedRemoteUsers)) ?? undefined : undefined,
image: file ? this.driveFileEntityService.getPublicUrl(file) : undefined,
description: this.escapeCDATA(note.cw) ?? undefined,
content: this.escapeCDATA(contentStr) || undefined,
});
}
return feed;
}
private escapeCDATA(str: string) {
return str?.replaceAll("]]>", "]]]]><![CDATA[>").replace(/[\x00-\x08\x0b\x0c\x0e-\x1f\x7f]/g, "");
}
private async noteToString(note: MiNote, isTheNote = false) {
const author = isTheNote
? null
: await this.usersRepository.findOneByOrFail({ id: note.userId });
let outstr = author
? `${author.name}(@${author.username}@${
author.host ? author.host : this.config.host
}) ${
note.renoteId ? "renotes" : note.replyId ? "replies" : "says"
}: <br>`
: "";
const files = note.fileIds?.length ? await this.driveFilesRepository.findBy({
id: In(note.fileIds),
}) : [];
let fileEle = "";
for (const file of files) {
if (file.type.startsWith("image/")) {
fileEle += ` <br><img src="${this.driveFileEntityService.getPublicUrl(file)}">`;
} else if (file.type.startsWith("audio/")) {
fileEle += ` <br><audio controls src="${this.driveFileEntityService.getPublicUrl(
file,
)}" type="${file.type}">`;
} else if (file.type.startsWith("video/")) {
fileEle += ` <br><video controls src="${this.driveFileEntityService.getPublicUrl(
file,
)}" type="${file.type}">`;
} else {
fileEle += ` <br><a href="${this.driveFileEntityService.getPublicUrl(file)}" download="${
file.name
}">${file.name}</a>`;
}
}
outstr += `${note.cw ? note.cw + "<br>" : ""}${
(note.text ? this.mfmService.toHtml(mfmParse(note.text), JSON.parse(note.mentionedRemoteUsers)) ?? undefined : undefined) || ""
}${fileEle}`;
if (isTheNote) {
outstr += ` <span class="${
note.renoteId ? "renote_note" : note.replyId ? "reply_note" : "new_note"
} ${
fileEle.indexOf("img src") !== -1 ? "with_img" : "without_img"
}"></span>`;
}
return outstr;
}
private async findById(id : string) {
let text = "";
let next = null;
const findings = await this.notesRepository.findOneBy({
id: id,
visibility: In(['public', 'home']),
});
if (findings) {
text += `<hr>`;
text += await this.noteToString(findings);
next = findings.renoteId ? findings.renoteId : findings.replyId;
} else {
this.logger.info(`Note ${id} not in scope`);
}
return { text, next };
}
}

View File

@ -10,15 +10,15 @@ cd packages/backend/test-federation
First, you need to start servers by executing following commands:
```sh
bash ./setup.sh
docker compose up --scale tester=0
NODE_VERSION=22 docker compose up --scale tester=0
```
Then you can run all tests by a following command:
```sh
docker compose run --no-deps --rm tester
NODE_VERSION=22 docker compose run --no-deps --rm tester
```
For testing a specific file, run a following command:
```sh
docker compose run --no-deps --rm tester -- pnpm -F backend test:fed packages/backend/test-federation/test/user.test.ts
NODE_VERSION=22 docker compose run --no-deps --rm tester -- pnpm -F backend test:fed packages/backend/test-federation/test/user.test.ts
```

View File

@ -12,7 +12,7 @@ services:
retries: 20
misskey:
image: node:20
image: node:${NODE_VERSION}
env_file:
- ./.config/docker.env
environment:

View File

@ -16,7 +16,7 @@ services:
"
tester:
image: node:20
image: node:${NODE_VERSION}
depends_on:
a.test:
condition: service_healthy
@ -85,7 +85,7 @@ services:
command: pnpm -F backend test:fed
daemon:
image: node:20
image: node:${NODE_VERSION}
depends_on:
redis.test:
condition: service_healthy

View File

@ -159,8 +159,8 @@ describe('API', () => {
user: { token: application3 },
}, {
status: 403,
code: 'PERMISSION_DENIED',
id: '1370e5b7-d4eb-4566-bb1d-7748ee6a1838',
code: 'ROLE_PERMISSION_DENIED',
id: 'c3d38592-54c0-429d-be96-5636b0431a61',
});
await failedApiCall({

View File

@ -3,29 +3,31 @@
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { S3Client } from '@aws-sdk/client-s3';
import { Test, TestingModule } from '@nestjs/testing';
import { mockClient } from 'aws-sdk-client-mock';
import { FastifyInstance } from 'fastify';
import request from 'supertest';
import { randomString } from '../../../../../utils.js';
import { CoreModule } from '@/core/CoreModule.js';
import { RoleService } from '@/core/RoleService.js';
import { DI } from '@/di-symbols.js';
import { GlobalModule } from '@/GlobalModule.js';
import { MiRole, UserProfilesRepository, UsersRepository } from '@/models/_.js';
import { DriveFoldersRepository, MiDriveFolder, MiRole, UserProfilesRepository, UsersRepository } from '@/models/_.js';
import { MiUser } from '@/models/User.js';
import { ServerModule } from '@/server/ServerModule.js';
import { ServerService } from '@/server/ServerService.js';
import { IdService } from '@/core/IdService.js';
describe('/drive/files/create', () => {
let module: TestingModule;
let server: FastifyInstance;
const s3Mock = mockClient(S3Client);
let roleService: RoleService;
let idService: IdService;
let root: MiUser;
let role_tinyAttachment: MiRole;
let folder: MiDriveFolder;
beforeAll(async () => {
module = await Test.createTestingModule({
imports: [GlobalModule, CoreModule, ServerModule],
@ -33,21 +35,34 @@ describe('/drive/files/create', () => {
module.enableShutdownHooks();
const serverService = module.get<ServerService>(ServerService);
server = await serverService.launch();
await serverService.launch();
server = serverService.fastify;
idService = module.get(IdService);
const usersRepository = module.get<UsersRepository>(DI.usersRepository);
await usersRepository.delete({});
root = await usersRepository.insert({
id: 'root',
id: idService.gen(),
username: 'root',
usernameLower: 'root',
token: '1234567890123456',
}).then(x => usersRepository.findOneByOrFail(x.identifiers[0]));
const userProfilesRepository = module.get<UserProfilesRepository>(DI.userProfilesRepository);
await userProfilesRepository.delete({});
await userProfilesRepository.insert({
userId: root.id,
});
const driveFoldersRepository = module.get<DriveFoldersRepository>(DI.driveFoldersRepository);
folder = await driveFoldersRepository.insertOne({
id: idService.gen(),
name: 'root-folder',
parentId: null,
userId: root.id,
});
roleService = module.get<RoleService>(RoleService);
role_tinyAttachment = await roleService.create({
name: 'test-role001',
@ -65,8 +80,8 @@ describe('/drive/files/create', () => {
});
beforeEach(async () => {
s3Mock.reset();
await roleService.unassign(root.id, role_tinyAttachment.id).catch(() => {});
await roleService.unassign(root.id, role_tinyAttachment.id).catch(() => {
});
});
afterAll(async () => {
@ -74,35 +89,76 @@ describe('/drive/files/create', () => {
await module.close();
});
test('200 ok', async () => {
const result = await request(server.server)
async function postFile(props: {
name: string,
comment: string,
isSensitive: boolean,
force: boolean,
fileContent: Buffer | string,
}) {
const { name, comment, isSensitive, force, fileContent } = props;
return await request(server.server)
.post('/api/drive/files/create')
.set('Content-Type', 'multipart/form-data')
.set('Authorization', `Bearer ${root.token}`)
.attach('file', Buffer.from('a'.repeat(1024 * 1024)));
.attach('file', fileContent)
.field('name', name)
.field('comment', comment)
.field('isSensitive', isSensitive)
.field('force', force)
.field('folderId', folder.id)
.field('i', root.token ?? '');
}
test('200 ok', async () => {
const name = randomString();
const comment = randomString();
const result = await postFile({
name: name,
comment: comment,
isSensitive: true,
force: true,
fileContent: Buffer.from('a'.repeat(1000 * 1000)),
});
expect(result.statusCode).toBe(200);
expect(result.body.name).toBe(name + '.unknown');
expect(result.body.comment).toBe(comment);
expect(result.body.isSensitive).toBe(true);
expect(result.body.folderId).toBe(folder.id);
});
test('200 ok(with role)', async () => {
await roleService.assign(root.id, role_tinyAttachment.id);
const result = await request(server.server)
.post('/api/drive/files/create')
.set('Content-Type', 'multipart/form-data')
.set('Authorization', `Bearer ${root.token}`)
.attach('file', Buffer.from('a'.repeat(10)));
const name = randomString();
const comment = randomString();
const result = await postFile({
name: name,
comment: comment,
isSensitive: true,
force: true,
fileContent: Buffer.from('a'.repeat(10)),
});
expect(result.statusCode).toBe(200);
expect(result.body.name).toBe(name + '.unknown');
expect(result.body.comment).toBe(comment);
expect(result.body.isSensitive).toBe(true);
expect(result.body.folderId).toBe(folder.id);
});
test('413 too large', async () => {
await roleService.assign(root.id, role_tinyAttachment.id);
const result = await request(server.server)
.post('/api/drive/files/create')
.set('Content-Type', 'multipart/form-data')
.set('Authorization', `Bearer ${root.token}`)
.attach('file', Buffer.from('a'.repeat(11)));
const name = randomString();
const comment = randomString();
const result = await postFile({
name: name,
comment: comment,
isSensitive: true,
force: true,
fileContent: Buffer.from('a'.repeat(11)),
});
expect(result.statusCode).toBe(413);
expect(result.body.error.code).toBe('FILE_SIZE_TOO_LARGE');
expect(result.body.error.code).toBe('MAX_FILE_SIZE_EXCEEDED');
});
});

View File

@ -34,7 +34,7 @@
"tsconfig-paths": "4.2.0",
"typescript": "5.8.3",
"uuid": "11.1.0",
"vite": "6.3.3",
"vite": "6.3.4",
"vue": "3.5.13"
},
"devDependencies": {

View File

@ -74,7 +74,7 @@
"typescript": "5.8.3",
"uuid": "11.1.0",
"v-code-diff": "1.13.1",
"vite": "6.3.3",
"vite": "6.3.4",
"vue": "3.5.13",
"vuedraggable": "next",
"wanakana": "5.3.1"

View File

@ -31,6 +31,10 @@ SPDX-License-Identifier: AGPL-3.0-only
:leaveActiveClass="prefer.s.animation ? $style.transition_toggle_leaveActive : ''"
:enterFromClass="prefer.s.animation ? $style.transition_toggle_enterFrom : ''"
:leaveToClass="prefer.s.animation ? $style.transition_toggle_leaveTo : ''"
@enter="enter"
@afterEnter="afterEnter"
@leave="leave"
@afterLeave="afterLeave"
>
<KeepAlive>
<div v-show="opened">
@ -86,6 +90,42 @@ const bgSame = ref(false);
const opened = ref(props.defaultOpen);
const openedAtLeastOnce = ref(props.defaultOpen);
//#region interpolate-sizeTODO:
function enter(el: Element) {
if (CSS.supports('interpolate-size', 'allow-keywords')) return;
if (!(el instanceof HTMLElement)) return;
const elementHeight = el.getBoundingClientRect().height;
el.style.height = '0';
el.offsetHeight; // reflow
el.style.height = `${Math.min(elementHeight, props.maxHeight ?? Infinity)}px`;
}
function afterEnter(el: Element) {
if (CSS.supports('interpolate-size', 'allow-keywords')) return;
if (!(el instanceof HTMLElement)) return;
el.style.height = '';
}
function leave(el: Element) {
if (CSS.supports('interpolate-size', 'allow-keywords')) return;
if (!(el instanceof HTMLElement)) return;
const elementHeight = el.getBoundingClientRect().height;
el.style.height = `${elementHeight}px`;
el.offsetHeight; // reflow
el.style.height = '0';
}
function afterLeave(el: Element) {
if (CSS.supports('interpolate-size', 'allow-keywords')) return;
if (!(el instanceof HTMLElement)) return;
el.style.height = '';
}
//#endregion
function toggle() {
if (!opened.value) {
openedAtLeastOnce.value = true;
@ -108,17 +148,27 @@ onMounted(() => {
.transition_toggle_enterActive,
.transition_toggle_leaveActive {
overflow-y: hidden; // margin clip 使
transition: opacity 0.3s, height 0.3s !important;
transition: opacity 0.3s, height 0.3s;
}
@supports (interpolate-size: allow-keywords) {
.transition_toggle_enterFrom,
.transition_toggle_leaveTo {
height: 0;
}
.root {
interpolate-size: allow-keywords; // heighttransition
}
}
.transition_toggle_enterFrom,
.transition_toggle_leaveTo {
opacity: 0;
height: 0;
}
.root {
display: block;
interpolate-size: allow-keywords; // heighttransition
}
.header {

View File

@ -12,8 +12,8 @@ SPDX-License-Identifier: AGPL-3.0-only
<script lang="ts" setup>
import { computed } from 'vue';
import type { CSSProperties } from 'vue';
import { instanceName as localInstanceName } from '@@/js/config.js';
import type { CSSProperties } from 'vue';
import { instance as localInstance } from '@/instance.js';
import { getProxiedImageUrlNullable } from '@/utility/media-proxy.js';
@ -61,19 +61,9 @@ $height: 2ex;
border-radius: 4px 0 0 4px;
overflow: clip;
color: #fff;
text-shadow: /* .866 ≈ sin(60deg) */
1px 0 1px #000,
.866px .5px 1px #000,
.5px .866px 1px #000,
0 1px 1px #000,
-.5px .866px 1px #000,
-.866px .5px 1px #000,
-1px 0 1px #000,
-.866px -.5px 1px #000,
-.5px -.866px 1px #000,
0 -1px 1px #000,
.5px -.866px 1px #000,
.866px -.5px 1px #000;
// text-shadow使
mask-image: linear-gradient(90deg,
rgb(0,0,0),
rgb(0,0,0) calc(100% - 16px),

View File

@ -4,7 +4,7 @@ SPDX-License-Identifier: AGPL-3.0-only
-->
<template>
<MkPullToRefresh :refresher="() => reload()">
<component :is="prefer.s.enablePullToRefresh ? MkPullToRefresh : 'div'" :refresher="() => reload()">
<MkPagination ref="pagingComponent" :pagination="pagination">
<template #empty>
<div class="_fullinfo">
@ -30,7 +30,7 @@ SPDX-License-Identifier: AGPL-3.0-only
</component>
</template>
</MkPagination>
</MkPullToRefresh>
</component>
</template>
<script lang="ts" setup>
@ -103,18 +103,38 @@ defineExpose({
</script>
<style lang="scss" module>
.transition_x_move,
.transition_x_enterActive,
.transition_x_leaveActive {
transition: opacity 0.3s cubic-bezier(0,.5,.5,1), transform 0.3s cubic-bezier(0,.5,.5,1) !important;
.transition_x_move {
transition: transform 0.7s cubic-bezier(0.23, 1, 0.32, 1);
}
.transition_x_enterFrom,
.transition_x_enterActive {
transition: transform 0.7s cubic-bezier(0.23, 1, 0.32, 1), opacity 0.7s cubic-bezier(0.23, 1, 0.32, 1);
&.item,
.item {
/* Skip Note Rendering有効時、TransitionGroupで通知を追加するときに一瞬がくっとなる問題を抑制する */
content-visibility: visible !important;
}
}
.transition_x_leaveActive {
transition: height 0.2s cubic-bezier(0,.5,.5,1), opacity 0.2s cubic-bezier(0,.5,.5,1);
}
.transition_x_enterFrom {
opacity: 0;
transform: translateY(max(-64px, -100%));
}
@supports (interpolate-size: allow-keywords) {
.transition_x_enterFrom {
interpolate-size: allow-keywords; // heighttransition
height: 0;
}
}
.transition_x_leaveTo {
opacity: 0;
transform: translateY(-50%);
}
.transition_x_leaveActive {
position: absolute;
}
.notifications {

View File

@ -4,13 +4,14 @@ SPDX-License-Identifier: AGPL-3.0-only
-->
<template>
<div ref="rootEl">
<div v-if="isPullStart" :class="$style.frame" :style="`--frame-min-height: ${pullDistance / (PULL_BRAKE_BASE + (pullDistance / PULL_BRAKE_FACTOR))}px;`">
<div ref="rootEl" :class="isPulling ? $style.isPulling : null">
<!-- 小数が含まれるとレンダリングが高頻度になりすぎパフォーマンスが悪化するためround -->
<div v-if="isPulling" :class="$style.frame" :style="`--frame-min-height: ${Math.round(pullDistance / (PULL_BRAKE_BASE + (pullDistance / PULL_BRAKE_FACTOR)))}px;`">
<div :class="$style.frameContent">
<MkLoading v-if="isRefreshing" :class="$style.loader" :em="true"/>
<i v-else class="ti ti-arrow-bar-to-down" :class="[$style.icon, { [$style.refresh]: isPullEnd }]"></i>
<i v-else class="ti ti-arrow-bar-to-down" :class="[$style.icon, { [$style.refresh]: isPulledEnough }]"></i>
<div :class="$style.text">
<template v-if="isPullEnd">{{ i18n.ts.releaseToRefresh }}</template>
<template v-if="isPulledEnough">{{ i18n.ts.releaseToRefresh }}</template>
<template v-else-if="isRefreshing">{{ i18n.ts.refreshing }}</template>
<template v-else>{{ i18n.ts.pullDownToRefresh }}</template>
</div>
@ -29,24 +30,21 @@ import { isHorizontalSwipeSwiping } from '@/utility/touch.js';
const SCROLL_STOP = 10;
const MAX_PULL_DISTANCE = Infinity;
const FIRE_THRESHOLD = 230;
const FIRE_THRESHOLD = 200;
const RELEASE_TRANSITION_DURATION = 200;
const PULL_BRAKE_BASE = 1.5;
const PULL_BRAKE_FACTOR = 170;
const isPullStart = ref(false);
const isPullEnd = ref(false);
const isPulling = ref(false);
const isPulledEnough = ref(false);
const isRefreshing = ref(false);
const pullDistance = ref(0);
let supportPointerDesktop = false;
let startScreenY: number | null = null;
const rootEl = useTemplateRef('rootEl');
let scrollEl: HTMLElement | null = null;
let disabled = false;
const props = withDefaults(defineProps<{
refresher: () => Promise<void>;
}>(), {
@ -57,18 +55,59 @@ const emit = defineEmits<{
(ev: 'refresh'): void;
}>();
function getScreenY(event) {
if (supportPointerDesktop) {
function getScreenY(event: TouchEvent | MouseEvent | PointerEvent): number {
if (event.touches && event.touches[0] && event.touches[0].screenY != null) {
return event.touches[0].screenY;
} else {
return event.screenY;
}
return event.touches[0].screenY;
}
function moveStart(event) {
if (!isPullStart.value && !isRefreshing.value && !disabled) {
isPullStart.value = true;
startScreenY = getScreenY(event);
pullDistance.value = 0;
// When at the top of the page, disable vertical overscroll so passive touch listeners can take over.
function lockDownScroll() {
if (scrollEl == null) return;
scrollEl.style.touchAction = 'pan-x pan-down pinch-zoom';
scrollEl.style.overscrollBehavior = 'none';
}
function unlockDownScroll() {
if (scrollEl == null) return;
scrollEl.style.touchAction = 'auto';
scrollEl.style.overscrollBehavior = 'contain';
}
function moveStart(event: PointerEvent) {
if (event.pointerType === 'mouse' && event.button !== 1) return;
if (isRefreshing.value) return;
const scrollPos = scrollEl!.scrollTop;
if (scrollPos !== 0) {
unlockDownScroll();
return;
}
lockDownScroll();
// pull
window.document.body.setAttribute('inert', 'true');
isPulling.value = true;
startScreenY = getScreenY(event);
pullDistance.value = 0;
// PointerEvent使TouchEventMouseEvent使
if (event.pointerType === 'mouse') {
window.addEventListener('mousemove', moving, { passive: true });
window.addEventListener('mouseup', () => {
window.removeEventListener('mousemove', moving);
onPullRelease();
}, { passive: true, once: true });
} else {
window.addEventListener('touchmove', moving, { passive: true });
window.addEventListener('touchend', () => {
window.removeEventListener('touchmove', moving);
onPullRelease();
}, { passive: true, once: true });
}
}
@ -108,31 +147,37 @@ async function closeContent() {
}
}
function moveEnd() {
if (isPullStart.value && !isRefreshing.value) {
startScreenY = null;
if (isPullEnd.value) {
isPullEnd.value = false;
isRefreshing.value = true;
fixOverContent().then(() => {
emit('refresh');
props.refresher().then(() => {
refreshFinished();
});
function onPullRelease() {
window.document.body.removeAttribute('inert');
startScreenY = null;
if (isPulledEnough.value) {
isPulledEnough.value = false;
isRefreshing.value = true;
fixOverContent().then(() => {
emit('refresh');
props.refresher().then(() => {
refreshFinished();
});
} else {
closeContent().then(() => isPullStart.value = false);
}
});
} else {
closeContent().then(() => isPulling.value = false);
}
}
function moving(event: TouchEvent | PointerEvent) {
if (!isPullStart.value || isRefreshing.value || disabled) return;
function toggleScrollLockOnTouchEnd() {
const scrollPos = scrollEl!.scrollTop;
if (scrollPos === 0) {
lockDownScroll();
} else {
unlockDownScroll();
}
}
if ((scrollEl?.scrollTop ?? 0) > (supportPointerDesktop ? SCROLL_STOP : SCROLL_STOP + pullDistance.value) || isHorizontalSwipeSwiping.value) {
function moving(event: MouseEvent | TouchEvent) {
if ((scrollEl?.scrollTop ?? 0) > SCROLL_STOP + pullDistance.value || isHorizontalSwipeSwiping.value) {
pullDistance.value = 0;
isPullEnd.value = false;
moveEnd();
isPulledEnough.value = false;
onPullRelease();
return;
}
@ -144,15 +189,7 @@ function moving(event: TouchEvent | PointerEvent) {
const moveHeight = moveScreenY - startScreenY!;
pullDistance.value = Math.min(Math.max(moveHeight, 0), MAX_PULL_DISTANCE);
if (pullDistance.value > 0) {
if (event.cancelable) event.preventDefault();
}
if (pullDistance.value > SCROLL_STOP) {
event.stopPropagation();
}
isPullEnd.value = pullDistance.value >= FIRE_THRESHOLD;
isPulledEnough.value = pullDistance.value >= FIRE_THRESHOLD;
}
/**
@ -162,65 +199,31 @@ function moving(event: TouchEvent | PointerEvent) {
*/
function refreshFinished() {
closeContent().then(() => {
isPullStart.value = false;
isPulling.value = false;
isRefreshing.value = false;
});
}
function setDisabled(value) {
disabled = value;
}
function onScrollContainerScroll() {
const scrollPos = scrollEl!.scrollTop;
// When at the top of the page, disable vertical overscroll so passive touch listeners can take over.
if (scrollPos === 0) {
scrollEl!.style.touchAction = 'pan-x pan-down pinch-zoom';
registerEventListenersForReadyToPull();
} else {
scrollEl!.style.touchAction = 'auto';
unregisterEventListenersForReadyToPull();
}
}
function registerEventListenersForReadyToPull() {
if (rootEl.value == null) return;
rootEl.value.addEventListener('touchstart', moveStart, { passive: true });
rootEl.value.addEventListener('touchmove', moving, { passive: false }); // passive: falsepreventDefault使
}
function unregisterEventListenersForReadyToPull() {
if (rootEl.value == null) return;
rootEl.value.removeEventListener('touchstart', moveStart);
rootEl.value.removeEventListener('touchmove', moving);
}
onMounted(() => {
if (rootEl.value == null) return;
scrollEl = getScrollContainer(rootEl.value);
if (scrollEl == null) return;
scrollEl.addEventListener('scroll', onScrollContainerScroll, { passive: true });
rootEl.value.addEventListener('touchend', moveEnd, { passive: true });
registerEventListenersForReadyToPull();
lockDownScroll();
rootEl.value.addEventListener('pointerdown', moveStart, { passive: true });
rootEl.value.addEventListener('touchend', toggleScrollLockOnTouchEnd, { passive: true });
});
onUnmounted(() => {
if (scrollEl) scrollEl.removeEventListener('scroll', onScrollContainerScroll);
unregisterEventListenersForReadyToPull();
});
defineExpose({
setDisabled,
unlockDownScroll();
if (rootEl.value) rootEl.value.removeEventListener('pointerdown', moveStart);
if (rootEl.value) rootEl.value.removeEventListener('touchend', toggleScrollLockOnTouchEnd);
});
</script>
<style lang="scss" module>
.isPulling {
will-change: contents;
}
.frame {
position: relative;
overflow: clip;
@ -242,7 +245,6 @@ defineExpose({
display: flex;
flex-direction: column;
align-items: center;
font-size: 14px;
> .icon, > .loader {
margin: 6px 0;
@ -258,6 +260,7 @@ defineExpose({
> .text {
margin: 5px 0;
font-size: 90%;
}
}
</style>

View File

@ -4,8 +4,8 @@ SPDX-License-Identifier: AGPL-3.0-only
-->
<template>
<MkPullToRefresh ref="prComponent" :refresher="() => reloadTimeline()">
<MkPagination v-if="paginationQuery" ref="pagingComponent" :pagination="paginationQuery" @queue="emit('queue', $event)" @status="prComponent?.setDisabled($event)">
<component :is="prefer.s.enablePullToRefresh ? MkPullToRefresh : 'div'" :refresher="() => reloadTimeline()">
<MkPagination v-if="paginationQuery" ref="pagingComponent" :pagination="paginationQuery" @queue="emit('queue', $event)">
<template #empty>
<div class="_fullinfo">
<img :src="infoImageUrl" draggable="false"/>
@ -21,7 +21,7 @@ SPDX-License-Identifier: AGPL-3.0-only
:leaveActiveClass="$style.transition_x_leaveActive"
:enterFromClass="$style.transition_x_enterFrom"
:leaveToClass="$style.transition_x_leaveTo"
:moveClass=" $style.transition_x_move"
:moveClass="$style.transition_x_move"
tag="div"
>
<template v-for="(note, i) in notes" :key="note.id">
@ -36,7 +36,7 @@ SPDX-License-Identifier: AGPL-3.0-only
</component>
</template>
</MkPagination>
</MkPullToRefresh>
</component>
</template>
<script lang="ts" setup>
@ -93,7 +93,6 @@ type TimelineQueryType = {
roleId?: string
};
const prComponent = useTemplateRef('prComponent');
const pagingComponent = useTemplateRef('pagingComponent');
let tlNotesCount = 0;
@ -306,18 +305,38 @@ defineExpose({
</script>
<style lang="scss" module>
.transition_x_move,
.transition_x_enterActive,
.transition_x_leaveActive {
transition: opacity 0.3s cubic-bezier(0,.5,.5,1), transform 0.3s cubic-bezier(0,.5,.5,1) !important;
.transition_x_move {
transition: transform 0.7s cubic-bezier(0.23, 1, 0.32, 1);
}
.transition_x_enterFrom,
.transition_x_enterActive {
transition: transform 0.7s cubic-bezier(0.23, 1, 0.32, 1), opacity 0.7s cubic-bezier(0.23, 1, 0.32, 1);
&.note,
.note {
/* Skip Note Rendering有効時、TransitionGroupでnoteを追加するときに一瞬がくっとなる問題を抑制する */
content-visibility: visible !important;
}
}
.transition_x_leaveActive {
transition: height 0.2s cubic-bezier(0,.5,.5,1), opacity 0.2s cubic-bezier(0,.5,.5,1);
}
.transition_x_enterFrom {
opacity: 0;
transform: translateY(max(-64px, -100%));
}
@supports (interpolate-size: allow-keywords) {
.transition_x_leaveTo {
interpolate-size: allow-keywords; // heighttransition
height: 0;
}
}
.transition_x_leaveTo {
opacity: 0;
transform: translateY(-50%);
}
.transition_x_leaveActive {
position: absolute;
}
.reverse {

View File

@ -8,7 +8,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<MkStickyContainer>
<template #header><MkPageHeader v-model:tab="tab" v-bind="pageHeaderProps"/></template>
<div :class="$style.body">
<MkSwiper v-if="swipable && (props.tabs?.length ?? 1) > 1" v-model:tab="tab" :class="$style.swiper" :tabs="props.tabs">
<MkSwiper v-if="prefer.s.enableHorizontalSwipe && swipable && (props.tabs?.length ?? 1) > 1" v-model:tab="tab" :class="$style.swiper" :tabs="props.tabs">
<slot></slot>
</MkSwiper>
<slot v-else></slot>
@ -25,6 +25,7 @@ import type { PageHeaderProps } from './MkPageHeader.vue';
import { useScrollPositionKeeper } from '@/use/use-scroll-position-keeper.js';
import MkSwiper from '@/components/MkSwiper.vue';
import { useRouter } from '@/router.js';
import { prefer } from '@/preferences.js';
const props = withDefaults(defineProps<PageHeaderProps & {
reversed?: boolean;

View File

@ -471,6 +471,15 @@ SPDX-License-Identifier: AGPL-3.0-only
</MkPreferenceContainer>
</SearchMarker>
<SearchMarker :keywords="['swipe', 'pull', 'refresh']">
<MkPreferenceContainer k="enablePullToRefresh">
<MkSwitch v-model="enablePullToRefresh">
<template #label><SearchLabel>{{ i18n.ts._settings.enablePullToRefresh }}</SearchLabel></template>
<template #caption><SearchKeyword>{{ i18n.ts._settings.enablePullToRefresh_description }}</SearchKeyword></template>
</MkSwitch>
</MkPreferenceContainer>
</SearchMarker>
<SearchMarker :keywords="['keep', 'screen', 'display', 'on']">
<MkPreferenceContainer k="keepScreenOn">
<MkSwitch v-model="keepScreenOn">
@ -800,6 +809,7 @@ const animatedMfm = prefer.model('animatedMfm');
const disableShowingAnimatedImages = prefer.model('disableShowingAnimatedImages');
const keepScreenOn = prefer.model('keepScreenOn');
const enableHorizontalSwipe = prefer.model('enableHorizontalSwipe');
const enablePullToRefresh = prefer.model('enablePullToRefresh');
const useNativeUiForVideoAudioPlayer = prefer.model('useNativeUiForVideoAudioPlayer');
const contextMenu = prefer.model('contextMenu');
const menuStyle = prefer.model('menuStyle');
@ -857,6 +867,8 @@ watch([
fontSize,
useSystemFont,
makeEveryTextElementsSelectable,
enableHorizontalSwipe,
enablePullToRefresh,
], async () => {
await reloadAsk({ reason: i18n.ts.reloadToApplySetting, unison: true });
});

View File

@ -300,6 +300,9 @@ export const PREF_DEF = {
enableHorizontalSwipe: {
default: true,
},
enablePullToRefresh: {
default: true,
},
useNativeUiForVideoAudioPlayer: {
default: false,
},

View File

@ -9,6 +9,7 @@ import type { RouteDef } from '@/lib/nirax.js';
import { $i, iAmModerator } from '@/i.js';
import MkLoading from '@/pages/_loading_.vue';
import MkError from '@/pages/_error_.vue';
import PageTimeline from '@/pages/timeline.vue';
export const page = (loader: AsyncComponentLoader) => defineAsyncComponent({
loader: loader,
@ -21,6 +22,13 @@ function chatPage(...args: Parameters<typeof page>) {
}
export const ROUTE_DEF = [{
name: 'index',
path: '/',
component: $i ? PageTimeline : page(() => import('@/pages/welcome.vue')),
}, {
path: '/timeline',
component: PageTimeline,
}, {
path: '/@:username/pages/:pageName(*)',
component: page(() => import('@/pages/page.vue')),
}, {
@ -579,13 +587,6 @@ export const ROUTE_DEF = [{
path: '/reversi/g/:gameId',
component: page(() => import('@/pages/reversi/game.vue')),
loginRequired: false,
}, {
path: '/timeline',
component: page(() => import('@/pages/timeline.vue')),
}, {
name: 'index',
path: '/',
component: $i ? page(() => import('@/pages/timeline.vue')) : page(() => import('@/pages/welcome.vue')),
}, {
// テスト用リダイレクト設定。ログイン中ユーザのプロフィールにリダイレクトする
path: '/redirect-test',

View File

@ -39,6 +39,7 @@ import type { PageMetadata } from '@/page.js';
import XMobileFooterMenu from '@/ui/_common_/mobile-footer-menu.vue';
import XPreferenceRestore from '@/ui/_common_/PreferenceRestore.vue';
import XTitlebar from '@/ui/_common_/titlebar.vue';
import XSidebar from '@/ui/_common_/navbar.vue';
import * as os from '@/os.js';
import { i18n } from '@/i18n.js';
import { $i } from '@/i.js';
@ -51,7 +52,6 @@ import { shouldSuggestRestoreBackup } from '@/preferences/utility.js';
import { DI } from '@/di.js';
const XWidgets = defineAsyncComponent(() => import('./_common_/widgets.vue'));
const XSidebar = defineAsyncComponent(() => import('@/ui/_common_/navbar.vue'));
const XStatusBars = defineAsyncComponent(() => import('@/ui/_common_/statusbars.vue'));
const XAnnouncements = defineAsyncComponent(() => import('@/ui/_common_/announcements.vue'));

View File

@ -1,7 +1,7 @@
{
"type": "module",
"name": "misskey-js",
"version": "2025.4.1",
"version": "2025.5.0-alpha.0",
"description": "Misskey SDK for JavaScript",
"license": "MIT",
"main": "./built/index.js",

View File

@ -736,7 +736,7 @@ importers:
version: 15.1.1
'@vitejs/plugin-vue':
specifier: 5.2.3
version: 5.2.3(vite@6.3.3(@types/node@22.15.2)(sass@1.87.0)(terser@5.39.0)(tsx@4.19.3))(vue@3.5.13(typescript@5.8.3))
version: 5.2.3(vite@6.3.4(@types/node@22.15.2)(sass@1.87.0)(terser@5.39.0)(tsx@4.19.3))(vue@3.5.13(typescript@5.8.3))
'@vue/compiler-sfc':
specifier: 3.5.13
version: 3.5.13
@ -873,8 +873,8 @@ importers:
specifier: 1.13.1
version: 1.13.1(vue@3.5.13(typescript@5.8.3))
vite:
specifier: 6.3.3
version: 6.3.3(@types/node@22.15.2)(sass@1.87.0)(terser@5.39.0)(tsx@4.19.3)
specifier: 6.3.4
version: 6.3.4(@types/node@22.15.2)(sass@1.87.0)(terser@5.39.0)(tsx@4.19.3)
vue:
specifier: 3.5.13
version: 3.5.13(typescript@5.8.3)
@ -926,7 +926,7 @@ importers:
version: 8.6.12(@storybook/test@8.6.12(storybook@8.6.12(bufferutil@4.0.9)(prettier@3.5.3)(utf-8-validate@6.0.5)))(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(storybook@8.6.12(bufferutil@4.0.9)(prettier@3.5.3)(utf-8-validate@6.0.5))(typescript@5.8.3)
'@storybook/react-vite':
specifier: 8.6.12
version: 8.6.12(@storybook/test@8.6.12(storybook@8.6.12(bufferutil@4.0.9)(prettier@3.5.3)(utf-8-validate@6.0.5)))(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(rollup@4.40.0)(storybook@8.6.12(bufferutil@4.0.9)(prettier@3.5.3)(utf-8-validate@6.0.5))(typescript@5.8.3)(vite@6.3.3(@types/node@22.15.2)(sass@1.87.0)(terser@5.39.0)(tsx@4.19.3))
version: 8.6.12(@storybook/test@8.6.12(storybook@8.6.12(bufferutil@4.0.9)(prettier@3.5.3)(utf-8-validate@6.0.5)))(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(rollup@4.40.0)(storybook@8.6.12(bufferutil@4.0.9)(prettier@3.5.3)(utf-8-validate@6.0.5))(typescript@5.8.3)(vite@6.3.4(@types/node@22.15.2)(sass@1.87.0)(terser@5.39.0)(tsx@4.19.3))
'@storybook/test':
specifier: 8.6.12
version: 8.6.12(storybook@8.6.12(bufferutil@4.0.9)(prettier@3.5.3)(utf-8-validate@6.0.5))
@ -941,7 +941,7 @@ importers:
version: 8.6.12(storybook@8.6.12(bufferutil@4.0.9)(prettier@3.5.3)(utf-8-validate@6.0.5))(vue@3.5.13(typescript@5.8.3))
'@storybook/vue3-vite':
specifier: 8.6.12
version: 8.6.12(storybook@8.6.12(bufferutil@4.0.9)(prettier@3.5.3)(utf-8-validate@6.0.5))(vite@6.3.3(@types/node@22.15.2)(sass@1.87.0)(terser@5.39.0)(tsx@4.19.3))(vue@3.5.13(typescript@5.8.3))
version: 8.6.12(storybook@8.6.12(bufferutil@4.0.9)(prettier@3.5.3)(utf-8-validate@6.0.5))(vite@6.3.4(@types/node@22.15.2)(sass@1.87.0)(terser@5.39.0)(tsx@4.19.3))(vue@3.5.13(typescript@5.8.3))
'@testing-library/vue':
specifier: 8.1.0
version: 8.1.0(@vue/compiler-sfc@3.5.13)(@vue/server-renderer@3.5.13(vue@3.5.13(typescript@5.8.3)))(vue@3.5.13(typescript@5.8.3))
@ -1094,7 +1094,7 @@ importers:
version: 15.1.1
'@vitejs/plugin-vue':
specifier: 5.2.3
version: 5.2.3(vite@6.3.3(@types/node@22.15.2)(sass@1.87.0)(terser@5.39.0)(tsx@4.19.3))(vue@3.5.13(typescript@5.8.3))
version: 5.2.3(vite@6.3.4(@types/node@22.15.2)(sass@1.87.0)(terser@5.39.0)(tsx@4.19.3))(vue@3.5.13(typescript@5.8.3))
'@vue/compiler-sfc':
specifier: 3.5.13
version: 3.5.13
@ -1147,8 +1147,8 @@ importers:
specifier: 11.1.0
version: 11.1.0
vite:
specifier: 6.3.3
version: 6.3.3(@types/node@22.15.2)(sass@1.87.0)(terser@5.39.0)(tsx@4.19.3)
specifier: 6.3.4
version: 6.3.4(@types/node@22.15.2)(sass@1.87.0)(terser@5.39.0)(tsx@4.19.3)
vue:
specifier: 3.5.13
version: 3.5.13(typescript@5.8.3)
@ -4114,6 +4114,7 @@ packages:
'@swc/core@1.11.22':
resolution: {integrity: sha512-mjPYbqq8XjwqSE0hEPT9CzaJDyxql97LgK4iyvYlwVSQhdN1uK0DBG4eP9PxYzCS2MUGAXB34WFLegdUj5HGpg==}
engines: {node: '>=10'}
deprecated: It has a bug. See https://github.com/swc-project/swc/issues/10413
peerDependencies:
'@swc/helpers': '>=0.5.17'
peerDependenciesMeta:
@ -10624,8 +10625,8 @@ packages:
vite-plugin-turbosnap@1.0.3:
resolution: {integrity: sha512-p4D8CFVhZS412SyQX125qxyzOgIFouwOcvjZWk6bQbNPR1wtaEzFT6jZxAjf1dejlGqa6fqHcuCvQea6EWUkUA==}
vite@6.3.3:
resolution: {integrity: sha512-5nXH+QsELbFKhsEfWLkHrvgRpTdGJzqOZ+utSdmPTvwHmvU6ITTm3xx+mRusihkcI8GeC7lCDyn3kDtiki9scw==}
vite@6.3.4:
resolution: {integrity: sha512-BiReIiMS2fyFqbqNT/Qqt4CVITDU9M9vE+DKcVAsB+ZV0wvTKd+3hMbkpxz1b+NmEDMegpVbisKiAZOnvO92Sw==}
engines: {node: ^18.0.0 || ^20.0.0 || >=22.0.0}
hasBin: true
peerDependencies:
@ -12619,12 +12620,12 @@ snapshots:
'@types/yargs': 17.0.19
chalk: 4.1.2
'@joshwooding/vite-plugin-react-docgen-typescript@0.5.0(typescript@5.8.3)(vite@6.3.3(@types/node@22.15.2)(sass@1.87.0)(terser@5.39.0)(tsx@4.19.3))':
'@joshwooding/vite-plugin-react-docgen-typescript@0.5.0(typescript@5.8.3)(vite@6.3.4(@types/node@22.15.2)(sass@1.87.0)(terser@5.39.0)(tsx@4.19.3))':
dependencies:
glob: 10.4.5
magic-string: 0.27.0
react-docgen-typescript: 2.2.2(typescript@5.8.3)
vite: 6.3.3(@types/node@22.15.2)(sass@1.87.0)(terser@5.39.0)(tsx@4.19.3)
vite: 6.3.4(@types/node@22.15.2)(sass@1.87.0)(terser@5.39.0)(tsx@4.19.3)
optionalDependencies:
typescript: 5.8.3
@ -14151,13 +14152,13 @@ snapshots:
react: 19.1.0
react-dom: 19.1.0(react@19.1.0)
'@storybook/builder-vite@8.6.12(storybook@8.6.12(bufferutil@4.0.9)(prettier@3.5.3)(utf-8-validate@6.0.5))(vite@6.3.3(@types/node@22.15.2)(sass@1.87.0)(terser@5.39.0)(tsx@4.19.3))':
'@storybook/builder-vite@8.6.12(storybook@8.6.12(bufferutil@4.0.9)(prettier@3.5.3)(utf-8-validate@6.0.5))(vite@6.3.4(@types/node@22.15.2)(sass@1.87.0)(terser@5.39.0)(tsx@4.19.3))':
dependencies:
'@storybook/csf-plugin': 8.6.12(storybook@8.6.12(bufferutil@4.0.9)(prettier@3.5.3)(utf-8-validate@6.0.5))
browser-assert: 1.2.1
storybook: 8.6.12(bufferutil@4.0.9)(prettier@3.5.3)(utf-8-validate@6.0.5)
ts-dedent: 2.2.0
vite: 6.3.3(@types/node@22.15.2)(sass@1.87.0)(terser@5.39.0)(tsx@4.19.3)
vite: 6.3.4(@types/node@22.15.2)(sass@1.87.0)(terser@5.39.0)(tsx@4.19.3)
'@storybook/components@8.6.12(storybook@8.6.12(bufferutil@4.0.9)(prettier@3.5.3)(utf-8-validate@6.0.5))':
dependencies:
@ -14220,11 +14221,11 @@ snapshots:
react-dom: 19.1.0(react@19.1.0)
storybook: 8.6.12(bufferutil@4.0.9)(prettier@3.5.3)(utf-8-validate@6.0.5)
'@storybook/react-vite@8.6.12(@storybook/test@8.6.12(storybook@8.6.12(bufferutil@4.0.9)(prettier@3.5.3)(utf-8-validate@6.0.5)))(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(rollup@4.40.0)(storybook@8.6.12(bufferutil@4.0.9)(prettier@3.5.3)(utf-8-validate@6.0.5))(typescript@5.8.3)(vite@6.3.3(@types/node@22.15.2)(sass@1.87.0)(terser@5.39.0)(tsx@4.19.3))':
'@storybook/react-vite@8.6.12(@storybook/test@8.6.12(storybook@8.6.12(bufferutil@4.0.9)(prettier@3.5.3)(utf-8-validate@6.0.5)))(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(rollup@4.40.0)(storybook@8.6.12(bufferutil@4.0.9)(prettier@3.5.3)(utf-8-validate@6.0.5))(typescript@5.8.3)(vite@6.3.4(@types/node@22.15.2)(sass@1.87.0)(terser@5.39.0)(tsx@4.19.3))':
dependencies:
'@joshwooding/vite-plugin-react-docgen-typescript': 0.5.0(typescript@5.8.3)(vite@6.3.3(@types/node@22.15.2)(sass@1.87.0)(terser@5.39.0)(tsx@4.19.3))
'@joshwooding/vite-plugin-react-docgen-typescript': 0.5.0(typescript@5.8.3)(vite@6.3.4(@types/node@22.15.2)(sass@1.87.0)(terser@5.39.0)(tsx@4.19.3))
'@rollup/pluginutils': 5.1.4(rollup@4.40.0)
'@storybook/builder-vite': 8.6.12(storybook@8.6.12(bufferutil@4.0.9)(prettier@3.5.3)(utf-8-validate@6.0.5))(vite@6.3.3(@types/node@22.15.2)(sass@1.87.0)(terser@5.39.0)(tsx@4.19.3))
'@storybook/builder-vite': 8.6.12(storybook@8.6.12(bufferutil@4.0.9)(prettier@3.5.3)(utf-8-validate@6.0.5))(vite@6.3.4(@types/node@22.15.2)(sass@1.87.0)(terser@5.39.0)(tsx@4.19.3))
'@storybook/react': 8.6.12(@storybook/test@8.6.12(storybook@8.6.12(bufferutil@4.0.9)(prettier@3.5.3)(utf-8-validate@6.0.5)))(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(storybook@8.6.12(bufferutil@4.0.9)(prettier@3.5.3)(utf-8-validate@6.0.5))(typescript@5.8.3)
find-up: 5.0.0
magic-string: 0.30.17
@ -14234,7 +14235,7 @@ snapshots:
resolve: 1.22.8
storybook: 8.6.12(bufferutil@4.0.9)(prettier@3.5.3)(utf-8-validate@6.0.5)
tsconfig-paths: 4.2.0
vite: 6.3.3(@types/node@22.15.2)(sass@1.87.0)(terser@5.39.0)(tsx@4.19.3)
vite: 6.3.4(@types/node@22.15.2)(sass@1.87.0)(terser@5.39.0)(tsx@4.19.3)
optionalDependencies:
'@storybook/test': 8.6.12(storybook@8.6.12(bufferutil@4.0.9)(prettier@3.5.3)(utf-8-validate@6.0.5))
transitivePeerDependencies:
@ -14283,15 +14284,15 @@ snapshots:
dependencies:
storybook: 8.6.12(bufferutil@4.0.9)(prettier@3.5.3)(utf-8-validate@6.0.5)
'@storybook/vue3-vite@8.6.12(storybook@8.6.12(bufferutil@4.0.9)(prettier@3.5.3)(utf-8-validate@6.0.5))(vite@6.3.3(@types/node@22.15.2)(sass@1.87.0)(terser@5.39.0)(tsx@4.19.3))(vue@3.5.13(typescript@5.8.3))':
'@storybook/vue3-vite@8.6.12(storybook@8.6.12(bufferutil@4.0.9)(prettier@3.5.3)(utf-8-validate@6.0.5))(vite@6.3.4(@types/node@22.15.2)(sass@1.87.0)(terser@5.39.0)(tsx@4.19.3))(vue@3.5.13(typescript@5.8.3))':
dependencies:
'@storybook/builder-vite': 8.6.12(storybook@8.6.12(bufferutil@4.0.9)(prettier@3.5.3)(utf-8-validate@6.0.5))(vite@6.3.3(@types/node@22.15.2)(sass@1.87.0)(terser@5.39.0)(tsx@4.19.3))
'@storybook/builder-vite': 8.6.12(storybook@8.6.12(bufferutil@4.0.9)(prettier@3.5.3)(utf-8-validate@6.0.5))(vite@6.3.4(@types/node@22.15.2)(sass@1.87.0)(terser@5.39.0)(tsx@4.19.3))
'@storybook/vue3': 8.6.12(storybook@8.6.12(bufferutil@4.0.9)(prettier@3.5.3)(utf-8-validate@6.0.5))(vue@3.5.13(typescript@5.8.3))
find-package-json: 1.2.0
magic-string: 0.30.17
storybook: 8.6.12(bufferutil@4.0.9)(prettier@3.5.3)(utf-8-validate@6.0.5)
typescript: 5.8.3
vite: 6.3.3(@types/node@22.15.2)(sass@1.87.0)(terser@5.39.0)(tsx@4.19.3)
vite: 6.3.4(@types/node@22.15.2)(sass@1.87.0)(terser@5.39.0)(tsx@4.19.3)
vue-component-meta: 2.0.16(typescript@5.8.3)
vue-docgen-api: 4.75.1(vue@3.5.13(typescript@5.8.3))
transitivePeerDependencies:
@ -14996,9 +14997,9 @@ snapshots:
'@ungap/structured-clone@1.2.0': {}
'@vitejs/plugin-vue@5.2.3(vite@6.3.3(@types/node@22.15.2)(sass@1.87.0)(terser@5.39.0)(tsx@4.19.3))(vue@3.5.13(typescript@5.8.3))':
'@vitejs/plugin-vue@5.2.3(vite@6.3.4(@types/node@22.15.2)(sass@1.87.0)(terser@5.39.0)(tsx@4.19.3))(vue@3.5.13(typescript@5.8.3))':
dependencies:
vite: 6.3.3(@types/node@22.15.2)(sass@1.87.0)(terser@5.39.0)(tsx@4.19.3)
vite: 6.3.4(@types/node@22.15.2)(sass@1.87.0)(terser@5.39.0)(tsx@4.19.3)
vue: 3.5.13(typescript@5.8.3)
'@vitest/coverage-v8@3.1.2(vitest@3.1.2(@types/debug@4.1.12)(@types/node@22.15.2)(happy-dom@17.4.4)(jsdom@26.1.0(bufferutil@4.0.9)(canvas@3.1.0)(utf-8-validate@6.0.5))(msw@2.7.5(@types/node@22.15.2)(typescript@5.8.3))(sass@1.87.0)(terser@5.39.0)(tsx@4.19.3))':
@ -15033,14 +15034,14 @@ snapshots:
chai: 5.2.0
tinyrainbow: 2.0.0
'@vitest/mocker@3.1.2(msw@2.7.5(@types/node@22.15.2)(typescript@5.8.3))(vite@6.3.3(@types/node@22.15.2)(sass@1.87.0)(terser@5.39.0)(tsx@4.19.3))':
'@vitest/mocker@3.1.2(msw@2.7.5(@types/node@22.15.2)(typescript@5.8.3))(vite@6.3.4(@types/node@22.15.2)(sass@1.87.0)(terser@5.39.0)(tsx@4.19.3))':
dependencies:
'@vitest/spy': 3.1.2
estree-walker: 3.0.3
magic-string: 0.30.17
optionalDependencies:
msw: 2.7.5(@types/node@22.15.2)(typescript@5.8.3)
vite: 6.3.3(@types/node@22.15.2)(sass@1.87.0)(terser@5.39.0)(tsx@4.19.3)
vite: 6.3.4(@types/node@22.15.2)(sass@1.87.0)(terser@5.39.0)(tsx@4.19.3)
'@vitest/pretty-format@2.0.5':
dependencies:
@ -22120,7 +22121,7 @@ snapshots:
debug: 4.4.0(supports-color@8.1.1)
es-module-lexer: 1.6.0
pathe: 2.0.3
vite: 6.3.3(@types/node@22.15.2)(sass@1.87.0)(terser@5.39.0)(tsx@4.19.3)
vite: 6.3.4(@types/node@22.15.2)(sass@1.87.0)(terser@5.39.0)(tsx@4.19.3)
transitivePeerDependencies:
- '@types/node'
- jiti
@ -22137,7 +22138,7 @@ snapshots:
vite-plugin-turbosnap@1.0.3: {}
vite@6.3.3(@types/node@22.15.2)(sass@1.87.0)(terser@5.39.0)(tsx@4.19.3):
vite@6.3.4(@types/node@22.15.2)(sass@1.87.0)(terser@5.39.0)(tsx@4.19.3):
dependencies:
esbuild: 0.25.3
fdir: 6.4.4(picomatch@4.0.2)
@ -22159,7 +22160,7 @@ snapshots:
vitest@3.1.2(@types/debug@4.1.12)(@types/node@22.15.2)(happy-dom@17.4.4)(jsdom@26.1.0(bufferutil@4.0.9)(canvas@3.1.0)(utf-8-validate@6.0.5))(msw@2.7.5(@types/node@22.15.2)(typescript@5.8.3))(sass@1.87.0)(terser@5.39.0)(tsx@4.19.3):
dependencies:
'@vitest/expect': 3.1.2
'@vitest/mocker': 3.1.2(msw@2.7.5(@types/node@22.15.2)(typescript@5.8.3))(vite@6.3.3(@types/node@22.15.2)(sass@1.87.0)(terser@5.39.0)(tsx@4.19.3))
'@vitest/mocker': 3.1.2(msw@2.7.5(@types/node@22.15.2)(typescript@5.8.3))(vite@6.3.4(@types/node@22.15.2)(sass@1.87.0)(terser@5.39.0)(tsx@4.19.3))
'@vitest/pretty-format': 3.1.2
'@vitest/runner': 3.1.2
'@vitest/snapshot': 3.1.2
@ -22176,7 +22177,7 @@ snapshots:
tinyglobby: 0.2.13
tinypool: 1.0.2
tinyrainbow: 2.0.0
vite: 6.3.3(@types/node@22.15.2)(sass@1.87.0)(terser@5.39.0)(tsx@4.19.3)
vite: 6.3.4(@types/node@22.15.2)(sass@1.87.0)(terser@5.39.0)(tsx@4.19.3)
vite-node: 3.1.2(@types/node@22.15.2)(sass@1.87.0)(terser@5.39.0)(tsx@4.19.3)
why-is-node-running: 2.3.0
optionalDependencies:

View File

@ -16,7 +16,7 @@
"remark-parse": "11.0.0",
"typescript": "5.8.3",
"unified": "11.0.5",
"vite": "6.3.3",
"vite": "6.3.4",
"vite-node": "3.1.2",
"vitest": "3.1.2"
}
@ -2785,9 +2785,9 @@
}
},
"node_modules/vite": {
"version": "6.3.3",
"resolved": "https://registry.npmjs.org/vite/-/vite-6.3.3.tgz",
"integrity": "sha512-5nXH+QsELbFKhsEfWLkHrvgRpTdGJzqOZ+utSdmPTvwHmvU6ITTm3xx+mRusihkcI8GeC7lCDyn3kDtiki9scw==",
"version": "6.3.4",
"resolved": "https://registry.npmjs.org/vite/-/vite-6.3.4.tgz",
"integrity": "sha512-BiReIiMS2fyFqbqNT/Qqt4CVITDU9M9vE+DKcVAsB+ZV0wvTKd+3hMbkpxz1b+NmEDMegpVbisKiAZOnvO92Sw==",
"dev": true,
"license": "MIT",
"dependencies": {

View File

@ -17,7 +17,7 @@
"remark-parse": "11.0.0",
"typescript": "5.8.3",
"unified": "11.0.5",
"vite": "6.3.3",
"vite": "6.3.4",
"vite-node": "3.1.2",
"vitest": "3.1.2"
}