Compare commits
35 Commits
6155780679
...
870387bc66
Author | SHA1 | Date |
---|---|---|
|
870387bc66 | |
|
d522d1bf26 | |
|
080276e3e7 | |
|
619bb2214e | |
|
c23f2ff900 | |
|
14d6734cb1 | |
|
3bdb1dd558 | |
|
e75d749784 | |
|
42a2ed8b67 | |
|
a5fa9a2cef | |
|
0884605b62 | |
|
8dbd8ff4cf | |
|
5aeedf59ff | |
|
3eaa05a5d9 | |
|
b0b6962661 | |
|
8c0c503c6c | |
|
590cb5f4dd | |
|
90e69f4d10 | |
|
e76e2534d7 | |
|
27682b980c | |
|
ef79cc290f | |
|
e7c170cf0c | |
|
f0544ede87 | |
|
00008d3763 | |
|
526057cc61 | |
|
c13aa0c224 | |
|
1af98b690b | |
|
d25af911cf | |
|
df1a3742dd | |
|
c5235a7b2f | |
|
2e48608f67 | |
|
51a10e55d3 | |
|
5e6d44a265 | |
|
924f9ed899 | |
|
0435cc5f2d |
|
@ -5,7 +5,7 @@
|
|||
"workspaceFolder": "/workspace",
|
||||
"features": {
|
||||
"ghcr.io/devcontainers/features/node:1": {
|
||||
"version": "22.11.0"
|
||||
"version": "22.15.0"
|
||||
},
|
||||
"ghcr.io/devcontainers-extra/features/pnpm:2": {
|
||||
"version": "10.10.0"
|
||||
|
|
|
@ -0,0 +1 @@
|
|||
20.10.0
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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'
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -1 +1 @@
|
|||
22.11.0
|
||||
22.15.0
|
||||
|
|
21
CHANGELOG.md
21
CHANGELOG.md
|
@ -1,14 +1,31 @@
|
|||
## Unreleased
|
||||
## 2025.5.0
|
||||
|
||||
### Note
|
||||
- DockerのNode.jsが22.15.0に更新されました
|
||||
|
||||
### General
|
||||
-
|
||||
|
||||
### Client
|
||||
-
|
||||
- Feat: マウスでもタイムラインを引っ張って更新できるように
|
||||
- アクセシビリティ設定からオフにすることもできます
|
||||
- Enhance: タイムラインのパフォーマンスを向上
|
||||
- Fix: 一部のブラウザでアコーディオンメニューのアニメーションが動作しない問題を修正
|
||||
- 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: ファイルのアップロードに失敗することがある問題を修正
|
||||
- 投稿フォーム上で画像のクロップを行うと、`Invalid Param.`エラーでノートが投稿出来なくなる問題も解決されます。
|
||||
- この事象によって既にノートが投稿出来ない状態になっている場合は、投稿フォーム右上のメニューから、下書きデータの「リセット」を行ってください。
|
||||
|
||||
## 2025.4.1
|
||||
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
# syntax = docker/dockerfile:1.4
|
||||
|
||||
ARG NODE_VERSION=22.11.0-bookworm
|
||||
ARG NODE_VERSION=22.15.0-bookworm
|
||||
|
||||
# build assets & compile TypeScript
|
||||
|
||||
|
|
|
@ -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,8 @@ _settings:
|
|||
ifOn: "Quan s'activa"
|
||||
ifOff: "Quan es desactiva"
|
||||
enableSyncThemesBetweenDevices: "Sincronitzar els temes instal·lats entre dispositius"
|
||||
enablePullToRefresh: "Lliscar i actualitzar "
|
||||
enablePullToRefresh_description: "Amb el ratolí, llisca mentre prems la roda."
|
||||
_chat:
|
||||
showSenderName: "Mostrar el nom del remitent"
|
||||
sendOnEnter: "Introdueix per enviar"
|
||||
|
@ -1468,6 +1471,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 +1603,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"
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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)"
|
||||
|
@ -1347,6 +1348,7 @@ readonly: "Read only"
|
|||
goToDeck: "Return to Deck"
|
||||
federationJobs: "Federation Jobs"
|
||||
driveAboutTip: "In Drive, a list of files you've uploaded in the past will be displayed. <br> \nYou can reuse these files when attaching them to notes, or you can upload files in advance to post later. <br> \n<b>Be careful when deleting a file, as it will not be available in all places where it was used (such as notes, pages, avatars, banners, etc.).</b> <br> \nYou can also create folders to organize your files."
|
||||
scrollToClose: "Scroll to close"
|
||||
_chat:
|
||||
noMessagesYet: "No messages yet"
|
||||
newMessage: "New message"
|
||||
|
@ -1423,6 +1425,8 @@ _settings:
|
|||
ifOn: "When turned on"
|
||||
ifOff: "When turned off"
|
||||
enableSyncThemesBetweenDevices: "Synchronize installed themes across devices"
|
||||
enablePullToRefresh: "Pull to Refresh"
|
||||
enablePullToRefresh_description: "When using a mouse, drag while pressing in the scrolling wheel."
|
||||
_chat:
|
||||
showSenderName: "Show sender's name"
|
||||
sendOnEnter: "Press Enter to send"
|
||||
|
@ -1468,6 +1472,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 +1604,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"
|
||||
|
|
|
@ -5413,6 +5413,10 @@ export interface Locale extends ILocale {
|
|||
* フォルダを作って整理することもできます。
|
||||
*/
|
||||
"driveAboutTip": string;
|
||||
/**
|
||||
* スクロールして閉じる
|
||||
*/
|
||||
"scrollToClose": string;
|
||||
"_chat": {
|
||||
/**
|
||||
* まだメッセージはありません
|
||||
|
@ -5709,6 +5713,14 @@ export interface Locale extends ILocale {
|
|||
* デバイス間でインストールしたテーマを同期
|
||||
*/
|
||||
"enableSyncThemesBetweenDevices": string;
|
||||
/**
|
||||
* ひっぱって更新
|
||||
*/
|
||||
"enablePullToRefresh": string;
|
||||
/**
|
||||
* マウスでは、ホイールを押し込みながらドラッグします。
|
||||
*/
|
||||
"enablePullToRefresh_description": string;
|
||||
"_chat": {
|
||||
/**
|
||||
* 送信者の名前を表示
|
||||
|
|
|
@ -1348,6 +1348,7 @@ readonly: "読み取り専用"
|
|||
goToDeck: "デッキへ戻る"
|
||||
federationJobs: "連合ジョブ"
|
||||
driveAboutTip: "ドライブでは、過去にアップロードしたファイルの一覧が表示されます。<br>\nノートに添付する際に再利用したり、あとで投稿するファイルを予めアップロードしておくこともできます。<br>\n<b>ファイルを削除すると、今までそのファイルを使用した全ての場所(ノート、ページ、アバター、バナー等)からも見えなくなるので注意してください。</b><br>\nフォルダを作って整理することもできます。"
|
||||
scrollToClose: "スクロールして閉じる"
|
||||
|
||||
_chat:
|
||||
noMessagesYet: "まだメッセージはありません"
|
||||
|
@ -1427,6 +1428,8 @@ _settings:
|
|||
ifOn: "オンのとき"
|
||||
ifOff: "オフのとき"
|
||||
enableSyncThemesBetweenDevices: "デバイス間でインストールしたテーマを同期"
|
||||
enablePullToRefresh: "ひっぱって更新"
|
||||
enablePullToRefresh_description: "マウスでは、ホイールを押し込みながらドラッグします。"
|
||||
|
||||
_chat:
|
||||
showSenderName: "送信者の名前を表示"
|
||||
|
|
|
@ -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: "为另一个账户建立别名"
|
||||
|
|
|
@ -220,6 +220,7 @@ silenceThisInstance: "禁言此伺服器"
|
|||
mediaSilenceThisInstance: "將這個伺服器的媒體設為禁言"
|
||||
operations: "操作"
|
||||
software: "軟體"
|
||||
softwareName: "軟體名稱"
|
||||
version: "版本"
|
||||
metadata: "詮釋資料"
|
||||
withNFiles: "{n} 個檔案"
|
||||
|
@ -1347,6 +1348,7 @@ readonly: "唯讀"
|
|||
goToDeck: "回去甲板"
|
||||
federationJobs: "聯邦通訊作業"
|
||||
driveAboutTip: "在「雲端硬碟」中,會顯示過去上傳的檔案列表。<br>\n可以在附加到貼文時重新利用,或者事先上傳之後再用於發布。<br>\n<b>請注意,刪除檔案後,之前使用過該檔案的所有地方(貼文、頁面、大頭貼、橫幅等)也會一併無法顯示。</b><br>\n也可以建立資料夾來整理檔案。"
|
||||
scrollToClose: "用滾輪關閉"
|
||||
_chat:
|
||||
noMessagesYet: "尚無訊息"
|
||||
newMessage: "新訊息"
|
||||
|
@ -1423,6 +1425,8 @@ _settings:
|
|||
ifOn: "開啟時"
|
||||
ifOff: "關閉時"
|
||||
enableSyncThemesBetweenDevices: "在裝置之間同步已安裝的主題"
|
||||
enablePullToRefresh: "下拉更新"
|
||||
enablePullToRefresh_description: "使用滑鼠,按下並拖曳滾輪。"
|
||||
_chat:
|
||||
showSenderName: "顯示發送者的名稱"
|
||||
sendOnEnter: "按下 Enter 發送訊息"
|
||||
|
@ -1468,6 +1472,7 @@ _delivery:
|
|||
manuallySuspended: "手動暫停中"
|
||||
goneSuspended: "因為伺服器刪除所以暫停中"
|
||||
autoSuspendedForNotResponding: "因為伺服器沒有回應所以暫停中"
|
||||
softwareSuspended: "此軟體因已停止發佈,目前無法使用"
|
||||
_bubbleGame:
|
||||
howToPlay: "玩法說明"
|
||||
hold: "保留"
|
||||
|
@ -1599,6 +1604,8 @@ _serverSettings:
|
|||
openRegistration: "允許建立帳戶"
|
||||
openRegistrationWarning: "開放註冊伴隨著風險。 建議只有在伺服器受到持續監控,並準備好在出現問題時能立即處理的情況下才開放註冊。"
|
||||
thisSettingWillAutomaticallyOffWhenModeratorsInactive: "如果在一段期間內沒有偵測到任何審查員活動,此設定將自動關閉,以防止垃圾內容。"
|
||||
deliverSuspendedSoftware: "已停止發佈的軟體"
|
||||
deliverSuspendedSoftwareDescription: "由於脆弱性等原因,可以指定伺服器軟體的名稱與版本範圍來停止其發佈。這些版本資訊是由伺服器所提供,其可靠性無法保證。版本的指定可以使用 semver(語意化版本控制) 的範圍語法,但如果指定為 >= 2024.3.1,則像 2024.3.1-custom.0 這樣的自訂版本將不會被包含在內,因此建議使用 >= 2024.3.1-0 的方式來同時包含預發佈版本。"
|
||||
_accountMigration:
|
||||
moveFrom: "從其他帳戶遷移到這個帳戶"
|
||||
moveFromSub: "為另一個帳戶建立別名"
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "misskey",
|
||||
"version": "2025.4.1",
|
||||
"version": "2025.5.0-alpha.1",
|
||||
"codename": "nasubi",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
import tsParser from '@typescript-eslint/parser';
|
||||
import globals from 'globals';
|
||||
import sharedConfig from '../shared/eslint.config.js';
|
||||
|
||||
export default [
|
||||
|
@ -6,6 +7,13 @@ export default [
|
|||
{
|
||||
ignores: ['**/node_modules', 'built', '@types/**/*', 'migration'],
|
||||
},
|
||||
{
|
||||
languageOptions: {
|
||||
globals: {
|
||||
...globals.node,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
files: ['**/*.ts', '**/*.tsx'],
|
||||
languageOptions: {
|
||||
|
|
|
@ -0,0 +1,20 @@
|
|||
#!/usr/bin/env node
|
||||
import child_process from 'node:child_process';
|
||||
import path from 'node:path';
|
||||
import url from 'node:url';
|
||||
|
||||
import semver from 'semver';
|
||||
|
||||
const __filename = url.fileURLToPath(import.meta.url);
|
||||
const __dirname = path.dirname(__filename);
|
||||
|
||||
const args = [];
|
||||
args.push(...[
|
||||
...semver.satisfies(process.version, '^20.17.0 || ^22.0.0') ? ['--no-experimental-require-module'] : [],
|
||||
'--experimental-vm-modules',
|
||||
'--experimental-import-meta-resolve',
|
||||
path.join(__dirname, 'node_modules/jest/bin/jest.js'),
|
||||
...process.argv.slice(2),
|
||||
]);
|
||||
|
||||
child_process.spawn(process.execPath, args, { stdio: 'inherit' });
|
|
@ -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")`);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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';
|
||||
}
|
|
@ -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',
|
||||
});
|
||||
|
|
|
@ -22,12 +22,12 @@
|
|||
"typecheck": "tsc --noEmit && tsc -p test --noEmit && tsc -p test-federation --noEmit",
|
||||
"eslint": "eslint --quiet \"{src,test-federation}/**/*.ts\"",
|
||||
"lint": "pnpm typecheck && pnpm eslint",
|
||||
"jest": "cross-env NODE_ENV=test node --experimental-vm-modules --experimental-import-meta-resolve node_modules/jest/bin/jest.js --forceExit --config jest.config.unit.cjs",
|
||||
"jest:e2e": "cross-env NODE_ENV=test node --experimental-vm-modules --experimental-import-meta-resolve node_modules/jest/bin/jest.js --forceExit --config jest.config.e2e.cjs",
|
||||
"jest:fed": "node --experimental-vm-modules --experimental-import-meta-resolve node_modules/jest/bin/jest.js --forceExit --config jest.config.fed.cjs",
|
||||
"jest-and-coverage": "cross-env NODE_ENV=test node --experimental-vm-modules --experimental-import-meta-resolve node_modules/jest/bin/jest.js --coverage --forceExit --config jest.config.unit.cjs",
|
||||
"jest-and-coverage:e2e": "cross-env NODE_ENV=test node --experimental-vm-modules --experimental-import-meta-resolve node_modules/jest/bin/jest.js --coverage --forceExit --config jest.config.e2e.cjs",
|
||||
"jest-clear": "cross-env NODE_ENV=test node --experimental-vm-modules --experimental-import-meta-resolve node_modules/jest/bin/jest.js --clearCache",
|
||||
"jest": "cross-env NODE_ENV=test node ./jest.js --forceExit --config jest.config.unit.cjs",
|
||||
"jest:e2e": "cross-env NODE_ENV=test node ./jest.js --forceExit --config jest.config.e2e.cjs",
|
||||
"jest:fed": "node ./jest.js --forceExit --config jest.config.fed.cjs",
|
||||
"jest-and-coverage": "cross-env NODE_ENV=test node ./jest.js --coverage --forceExit --config jest.config.unit.cjs",
|
||||
"jest-and-coverage:e2e": "cross-env NODE_ENV=test node ./jest.js --coverage --forceExit --config jest.config.e2e.cjs",
|
||||
"jest-clear": "cross-env NODE_ENV=test node ./jest.js --clearCache",
|
||||
"test": "pnpm jest",
|
||||
"test:e2e": "pnpm build && pnpm build:test && pnpm jest:e2e",
|
||||
"test:fed": "pnpm jest:fed",
|
||||
|
|
|
@ -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],
|
||||
};
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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
|
||||
```
|
||||
|
|
|
@ -12,7 +12,7 @@ services:
|
|||
retries: 20
|
||||
|
||||
misskey:
|
||||
image: node:20
|
||||
image: node:${NODE_VERSION}
|
||||
env_file:
|
||||
- ./.config/docker.env
|
||||
environment:
|
||||
|
|
|
@ -16,7 +16,7 @@ services:
|
|||
"
|
||||
|
||||
tester:
|
||||
image: node:20
|
||||
image: node:${NODE_VERSION}
|
||||
depends_on:
|
||||
a.test:
|
||||
condition: service_healthy
|
||||
|
@ -50,6 +50,10 @@ services:
|
|||
source: ../jest.config.fed.cjs
|
||||
target: /misskey/packages/backend/jest.config.fed.cjs
|
||||
read_only: true
|
||||
- type: bind
|
||||
source: ../jest.js
|
||||
target: /misskey/packages/backend/jest.js
|
||||
read_only: true
|
||||
- type: bind
|
||||
source: ../../misskey-js/built
|
||||
target: /misskey/packages/misskey-js/built
|
||||
|
@ -85,7 +89,7 @@ services:
|
|||
command: pnpm -F backend test:fed
|
||||
|
||||
daemon:
|
||||
image: node:20
|
||||
image: node:${NODE_VERSION}
|
||||
depends_on:
|
||||
redis.test:
|
||||
condition: service_healthy
|
||||
|
|
|
@ -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({
|
||||
|
|
|
@ -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');
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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": {
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -4,7 +4,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
-->
|
||||
|
||||
<template>
|
||||
<MkModal ref="modal" :zPriority="'middle'" @closed="$emit('closed')" @click="onBgClick">
|
||||
<MkModal ref="modal" :zPriority="'middle'" :preferType="'dialog'" @closed="$emit('closed')" @click="onBgClick">
|
||||
<div ref="rootEl" :class="$style.root">
|
||||
<div :class="$style.header">
|
||||
<span :class="$style.icon">
|
||||
|
@ -16,13 +16,21 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
<span :class="$style.title">{{ announcement.title }}</span>
|
||||
</div>
|
||||
<div :class="$style.text"><Mfm :text="announcement.text"/></div>
|
||||
<MkButton primary full @click="ok">{{ i18n.ts.ok }}</MkButton>
|
||||
<div ref="bottomEl"></div>
|
||||
<div :class="$style.footer">
|
||||
<MkButton
|
||||
primary
|
||||
full
|
||||
:disabled="!hasReachedBottom"
|
||||
@click="ok"
|
||||
>{{ hasReachedBottom ? i18n.ts.close : i18n.ts.scrollToClose }}</MkButton>
|
||||
</div>
|
||||
</div>
|
||||
</MkModal>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { onMounted, useTemplateRef } from 'vue';
|
||||
import { onMounted, ref, useTemplateRef } from 'vue';
|
||||
import * as Misskey from 'misskey-js';
|
||||
import * as os from '@/os.js';
|
||||
import { misskeyApi } from '@/utility/misskey-api.js';
|
||||
|
@ -32,12 +40,12 @@ import { i18n } from '@/i18n.js';
|
|||
import { $i } from '@/i.js';
|
||||
import { updateCurrentAccountPartial } from '@/accounts.js';
|
||||
|
||||
const props = withDefaults(defineProps<{
|
||||
const props = defineProps<{
|
||||
announcement: Misskey.entities.Announcement;
|
||||
}>(), {
|
||||
});
|
||||
}>();
|
||||
|
||||
const rootEl = useTemplateRef('rootEl');
|
||||
const bottomEl = useTemplateRef('bottomEl');
|
||||
const modal = useTemplateRef('modal');
|
||||
|
||||
async function ok() {
|
||||
|
@ -72,7 +80,34 @@ function onBgClick() {
|
|||
});
|
||||
}
|
||||
|
||||
const hasReachedBottom = ref(false);
|
||||
|
||||
onMounted(() => {
|
||||
if (bottomEl.value && rootEl.value) {
|
||||
const bottomElRect = bottomEl.value.getBoundingClientRect();
|
||||
const rootElRect = rootEl.value.getBoundingClientRect();
|
||||
if (
|
||||
bottomElRect.top >= rootElRect.top &&
|
||||
bottomElRect.top <= (rootElRect.bottom - 66) // 66 ≒ 75 * 0.9 (modalのアニメーション分)
|
||||
) {
|
||||
hasReachedBottom.value = true;
|
||||
return;
|
||||
}
|
||||
|
||||
const observer = new IntersectionObserver(entries => {
|
||||
for (const entry of entries) {
|
||||
if (entry.isIntersecting) {
|
||||
hasReachedBottom.value = true;
|
||||
observer.disconnect();
|
||||
}
|
||||
}
|
||||
}, {
|
||||
root: rootEl.value,
|
||||
rootMargin: '0px 0px -75px 0px',
|
||||
});
|
||||
|
||||
observer.observe(bottomEl.value);
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
|
@ -80,9 +115,12 @@ onMounted(() => {
|
|||
.root {
|
||||
margin: auto;
|
||||
position: relative;
|
||||
padding: 32px;
|
||||
padding: 32px 32px 0;
|
||||
min-width: 320px;
|
||||
max-width: 480px;
|
||||
max-height: 100%;
|
||||
overflow-y: auto;
|
||||
overflow-x: hidden;
|
||||
box-sizing: border-box;
|
||||
background: var(--MI_THEME-panel);
|
||||
border-radius: var(--MI-radius);
|
||||
|
@ -103,4 +141,14 @@ onMounted(() => {
|
|||
.text {
|
||||
margin: 1em 0;
|
||||
}
|
||||
|
||||
.footer {
|
||||
position: sticky;
|
||||
bottom: 0;
|
||||
left: -32px;
|
||||
backdrop-filter: var(--MI-blur, blur(15px));
|
||||
background: color(from var(--MI_THEME-bg) srgb r g b / 0.5);
|
||||
margin: 0 -32px;
|
||||
padding: 24px 32px;
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -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-sizeに対応していないブラウザ向け(TODO: 主要ブラウザが対応したら消す)
|
||||
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; // heightのtransitionを動作させるために必要
|
||||
}
|
||||
}
|
||||
|
||||
.transition_toggle_enterFrom,
|
||||
.transition_toggle_leaveTo {
|
||||
opacity: 0;
|
||||
height: 0;
|
||||
}
|
||||
|
||||
.root {
|
||||
display: block;
|
||||
interpolate-size: allow-keywords; // heightのtransitionを動作させるために必要
|
||||
}
|
||||
|
||||
.header {
|
||||
|
|
|
@ -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),
|
||||
|
|
|
@ -61,7 +61,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
/>
|
||||
<MkCwButton v-model="showContent" :text="appearNote.text" :renote="appearNote.renote" :files="appearNote.files" :poll="appearNote.poll" style="margin: 4px 0;"/>
|
||||
</p>
|
||||
<div v-show="appearNote.cw == null || showContent" :class="[{ [$style.contentCollapsed]: collapsed }]">
|
||||
<div v-show="appearNote.cw == null || showContent" :class="[{ [$style.contentCollapsed]: collapsed }]"><div ref="collapsibleInner">
|
||||
<div :class="$style.text">
|
||||
<span v-if="appearNote.isHidden" style="opacity: 0.5">({{ i18n.ts.private }})</span>
|
||||
<MkA v-if="appearNote.replyId" :class="$style.replyIcon" :to="`/notes/${appearNote.replyId}`"><i class="ti ti-arrow-back-up"></i></MkA>
|
||||
|
@ -98,7 +98,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
<button v-else-if="isLong && !collapsed" :class="$style.showLess" class="_button" @click="collapsed = true">
|
||||
<span :class="$style.showLessLabel">{{ i18n.ts.showLess }}</span>
|
||||
</button>
|
||||
</div>
|
||||
</div></div>
|
||||
<MkA v-if="appearNote.channel && !inChannel" :class="$style.channel" :to="`/channels/${appearNote.channel.id}`"><i class="ti ti-device-tv"></i> {{ appearNote.channel.name }}</MkA>
|
||||
</div>
|
||||
<MkReactionsViewer v-if="appearNote.reactionAcceptance !== 'likeOnly'" style="margin-top: 6px;" :note="appearNote" :maxNumber="16" @mockUpdateMyReaction="emitUpdReaction">
|
||||
|
@ -247,6 +247,11 @@ const currentClip = inject<Ref<Misskey.entities.Clip> | null>('currentClip', nul
|
|||
|
||||
const note = ref(deepClone(props.note));
|
||||
|
||||
onMounted(() => {
|
||||
isLong.value = collapsibleInner.value.clientHeight > 9 * parseFloat(getComputedStyle(collapsibleInner.value).fontSize);
|
||||
collapsed.value &&= isLong.value;
|
||||
});
|
||||
|
||||
// plugin
|
||||
const noteViewInterruptors = getPluginHandlers('note_view_interruptor');
|
||||
if (noteViewInterruptors.length > 0) {
|
||||
|
@ -281,8 +286,9 @@ const isMyRenote = $i && ($i.id === note.value.userId);
|
|||
const showContent = ref(false);
|
||||
const parsed = computed(() => appearNote.value.text ? mfm.parse(appearNote.value.text) : null);
|
||||
const urls = computed(() => parsed.value ? extractUrlFromMfm(parsed.value).filter((url) => appearNote.value.renote?.url !== url && appearNote.value.renote?.uri !== url) : null);
|
||||
const isLong = shouldCollapsed(appearNote.value, urls.value ?? []);
|
||||
const collapsed = ref(appearNote.value.cw == null && isLong);
|
||||
const collapsibleInner = ref(null);
|
||||
const isLong = ref(false);
|
||||
const collapsed = ref(appearNote.value.cw == null);
|
||||
const isDeleted = ref(false);
|
||||
const muted = ref(checkMute(appearNote.value, $i?.mutedWords));
|
||||
const hardMuted = ref(props.withHardMute && checkMute(appearNote.value, $i?.hardMutedWords, true));
|
||||
|
|
|
@ -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; // heightのtransitionを動作させるために必要
|
||||
height: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.transition_x_leaveTo {
|
||||
opacity: 0;
|
||||
transform: translateY(-50%);
|
||||
}
|
||||
.transition_x_leaveActive {
|
||||
position: absolute;
|
||||
}
|
||||
|
||||
.notifications {
|
||||
|
|
|
@ -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,19 +55,72 @@ 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 moveStartByMouse(event: MouseEvent) {
|
||||
if (event.button !== 1) return;
|
||||
if (isRefreshing.value) return;
|
||||
|
||||
const scrollPos = scrollEl!.scrollTop;
|
||||
if (scrollPos !== 0) {
|
||||
unlockDownScroll();
|
||||
return;
|
||||
}
|
||||
|
||||
lockDownScroll();
|
||||
|
||||
event.preventDefault(); // 中クリックによるスクロール、テキスト選択などを防ぐ
|
||||
|
||||
isPulling.value = true;
|
||||
startScreenY = getScreenY(event);
|
||||
pullDistance.value = 0;
|
||||
|
||||
window.addEventListener('mousemove', moving, { passive: true });
|
||||
window.addEventListener('mouseup', () => {
|
||||
window.removeEventListener('mousemove', moving);
|
||||
onPullRelease();
|
||||
}, { passive: true, once: true });
|
||||
}
|
||||
|
||||
function moveStartByTouch(event: TouchEvent) {
|
||||
if (isRefreshing.value) return;
|
||||
|
||||
const scrollPos = scrollEl!.scrollTop;
|
||||
if (scrollPos !== 0) {
|
||||
unlockDownScroll();
|
||||
return;
|
||||
}
|
||||
|
||||
lockDownScroll();
|
||||
|
||||
isPulling.value = true;
|
||||
startScreenY = getScreenY(event);
|
||||
pullDistance.value = 0;
|
||||
|
||||
window.addEventListener('touchmove', moving, { passive: true });
|
||||
window.addEventListener('touchend', () => {
|
||||
window.removeEventListener('touchmove', moving);
|
||||
onPullRelease();
|
||||
}, { passive: true, once: true });
|
||||
}
|
||||
|
||||
function moveBySystem(to: number): Promise<void> {
|
||||
|
@ -108,31 +159,36 @@ 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() {
|
||||
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 +200,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 +210,33 @@ 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: falseにしないとpreventDefaultが使えない
|
||||
}
|
||||
|
||||
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('mousedown', moveStartByMouse, { passive: false }); // preventDefaultするため
|
||||
rootEl.value.addEventListener('touchstart', moveStartByTouch, { 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('mousedown', moveStartByMouse);
|
||||
if (rootEl.value) rootEl.value.removeEventListener('touchstart', moveStartByTouch);
|
||||
if (rootEl.value) rootEl.value.removeEventListener('touchend', toggleScrollLockOnTouchEnd);
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" module>
|
||||
.isPulling {
|
||||
will-change: contents;
|
||||
}
|
||||
|
||||
.frame {
|
||||
position: relative;
|
||||
overflow: clip;
|
||||
|
@ -242,7 +258,6 @@ defineExpose({
|
|||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
font-size: 14px;
|
||||
|
||||
> .icon, > .loader {
|
||||
margin: 6px 0;
|
||||
|
@ -258,6 +273,7 @@ defineExpose({
|
|||
|
||||
> .text {
|
||||
margin: 5px 0;
|
||||
font-size: 90%;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -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; // heightのtransitionを動作させるために必要
|
||||
height: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.transition_x_leaveTo {
|
||||
opacity: 0;
|
||||
transform: translateY(-50%);
|
||||
}
|
||||
.transition_x_leaveActive {
|
||||
position: absolute;
|
||||
}
|
||||
|
||||
.reverse {
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -21,7 +21,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
<MkTl :events="timeline">
|
||||
<template #left="{ event }">
|
||||
<div>
|
||||
<MkAvatar :user="event.user" style="width: 24px; height: 24px;"/>
|
||||
<MkAvatar :user="event.user" style="width: 26px; height: 26px;"/>
|
||||
</div>
|
||||
</template>
|
||||
<template #right="{ event, timestamp, delta }">
|
||||
|
|
|
@ -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 });
|
||||
});
|
||||
|
|
|
@ -300,6 +300,9 @@ export const PREF_DEF = {
|
|||
enableHorizontalSwipe: {
|
||||
default: true,
|
||||
},
|
||||
enablePullToRefresh: {
|
||||
default: true,
|
||||
},
|
||||
useNativeUiForVideoAudioPlayer: {
|
||||
default: false,
|
||||
},
|
||||
|
|
|
@ -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',
|
||||
|
|
|
@ -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'));
|
||||
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
{
|
||||
"type": "module",
|
||||
"name": "misskey-js",
|
||||
"version": "2025.4.1",
|
||||
"version": "2025.5.0-alpha.1",
|
||||
"description": "Misskey SDK for JavaScript",
|
||||
"license": "MIT",
|
||||
"main": "./built/index.js",
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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": {
|
||||
|
|
|
@ -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"
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue