Compare commits
19 Commits
3b00c0c730
...
62cc16c415
Author | SHA1 | Date |
---|---|---|
|
62cc16c415 | |
|
90e69f4d10 | |
|
e76e2534d7 | |
|
27682b980c | |
|
ef79cc290f | |
|
e7c170cf0c | |
|
f0544ede87 | |
|
00008d3763 | |
|
526057cc61 | |
|
c13aa0c224 | |
|
1af98b690b | |
|
d25af911cf | |
|
df1a3742dd | |
|
479b40deee | |
|
dce3925e81 | |
|
8f609ec106 | |
|
0a3e8ca2d0 | |
|
ca4cf0ec5e | |
|
ac8161c50e |
13
CHANGELOG.md
13
CHANGELOG.md
|
@ -4,11 +4,21 @@
|
|||
-
|
||||
|
||||
### Client
|
||||
-
|
||||
- Feat: マウスでもタイムラインを引っ張って更新できるように
|
||||
- アクセシビリティ設定からオフにすることもできます
|
||||
- Enhance: タイムラインのパフォーマンスを向上
|
||||
|
||||
### Server
|
||||
- Enhance: 凍結されたユーザのノートが各種タイムラインで表示されないように `#15775`
|
||||
- Enhance: 連合先のソフトウェア及びバージョン名により配信停止を行えるように `#15727`
|
||||
- Enhance: 2025.4.1 で追加されたインデックスの再生成をノートの追加しながら行えるようになりました。 `#15915`
|
||||
- `MISSKEY_MIGRATION_CREATE_INDEX_CONCURRENTLY` 環境変数を `1` にセットしていると、巨大なテーブルの既存のカラムに関するインデックス再生成が`CREATE INDEX CONCURRENTLY`を使用するようになりました。
|
||||
- 複数のサーバープロセスをクラスタリングしているサーバーにおいて、一部のプロセスが起動している状態でこのオプションを有効にしてマイグレーションすることにより、ダウンタイムを削減することができます。
|
||||
- ただし、このオプションを有効にする場合、インデックスの作成にかかる時間が倍~3倍以上になることがあります。
|
||||
- また、大きなインスタンスである場合にはインデックスの作成に失敗し、複数回再試行する必要がある可能性があります。
|
||||
- Fix: チャンネルのフォロー一覧の結果が一部正しくないのを修正 (#12175)
|
||||
- Fix: ファイルをアップロードした際にファイル名が常に untitled になる問題を修正
|
||||
- Fix: ファイルのアップロードに失敗することがある問題を修正
|
||||
|
||||
## 2025.4.1
|
||||
|
||||
|
@ -38,6 +48,7 @@
|
|||
- Fix: ノートの直後のノートを表示する機能で表示が逆順になっていた問題を修正 #15841
|
||||
- Fix: アカウントの移行時にアンテナのフィルターのユーザが更新されない問題を修正 #15843
|
||||
- Fix: タイムラインでノートが重複して表示されることがあるのを修正
|
||||
- Fix: ダイアログのお知らせが画面からはみ出ることがある問題を修正
|
||||
|
||||
### Server
|
||||
- Enhance: ジョブキューの成功/失敗したジョブも一定数・一定期間保存するようにし、後から問題を調査することを容易に
|
||||
|
|
|
@ -5413,6 +5413,10 @@ export interface Locale extends ILocale {
|
|||
* フォルダを作って整理することもできます。
|
||||
*/
|
||||
"driveAboutTip": string;
|
||||
/**
|
||||
* スクロールして閉じる
|
||||
*/
|
||||
"scrollToClose": string;
|
||||
"_chat": {
|
||||
/**
|
||||
* まだメッセージはありません
|
||||
|
@ -5709,6 +5713,10 @@ export interface Locale extends ILocale {
|
|||
* デバイス間でインストールしたテーマを同期
|
||||
*/
|
||||
"enableSyncThemesBetweenDevices": string;
|
||||
/**
|
||||
* ひっぱって更新
|
||||
*/
|
||||
"enablePullToRefresh": string;
|
||||
"_chat": {
|
||||
/**
|
||||
* 送信者の名前を表示
|
||||
|
|
|
@ -1348,6 +1348,7 @@ readonly: "読み取り専用"
|
|||
goToDeck: "デッキへ戻る"
|
||||
federationJobs: "連合ジョブ"
|
||||
driveAboutTip: "ドライブでは、過去にアップロードしたファイルの一覧が表示されます。<br>\nノートに添付する際に再利用したり、あとで投稿するファイルを予めアップロードしておくこともできます。<br>\n<b>ファイルを削除すると、今までそのファイルを使用した全ての場所(ノート、ページ、アバター、バナー等)からも見えなくなるので注意してください。</b><br>\nフォルダを作って整理することもできます。"
|
||||
scrollToClose: "スクロールして閉じる"
|
||||
|
||||
_chat:
|
||||
noMessagesYet: "まだメッセージはありません"
|
||||
|
@ -1427,6 +1428,7 @@ _settings:
|
|||
ifOn: "オンのとき"
|
||||
ifOff: "オフのとき"
|
||||
enableSyncThemesBetweenDevices: "デバイス間でインストールしたテーマを同期"
|
||||
enablePullToRefresh: "ひっぱって更新"
|
||||
|
||||
_chat:
|
||||
showSenderName: "送信者の名前を表示"
|
||||
|
|
|
@ -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',
|
||||
});
|
||||
|
|
|
@ -223,7 +223,6 @@
|
|||
"@types/semver": "7.7.0",
|
||||
"@types/simple-oauth2": "5.0.7",
|
||||
"@types/sinonjs__fake-timers": "8.1.5",
|
||||
"@types/supertest": "6.0.3",
|
||||
"@types/tinycolor2": "1.4.6",
|
||||
"@types/tmp": "0.2.6",
|
||||
"@types/vary": "1.1.3",
|
||||
|
@ -240,7 +239,6 @@
|
|||
"jest-mock": "29.7.0",
|
||||
"nodemon": "3.1.10",
|
||||
"pid-port": "1.0.2",
|
||||
"simple-oauth2": "5.1.0",
|
||||
"supertest": "7.1.0"
|
||||
"simple-oauth2": "5.1.0"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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({
|
||||
|
|
|
@ -1,108 +0,0 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: syuilo and misskey-project
|
||||
* 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 { 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 { MiUser } from '@/models/User.js';
|
||||
import { ServerModule } from '@/server/ServerModule.js';
|
||||
import { ServerService } from '@/server/ServerService.js';
|
||||
|
||||
describe('/drive/files/create', () => {
|
||||
let module: TestingModule;
|
||||
let server: FastifyInstance;
|
||||
const s3Mock = mockClient(S3Client);
|
||||
let roleService: RoleService;
|
||||
|
||||
let root: MiUser;
|
||||
let role_tinyAttachment: MiRole;
|
||||
|
||||
beforeAll(async () => {
|
||||
module = await Test.createTestingModule({
|
||||
imports: [GlobalModule, CoreModule, ServerModule],
|
||||
}).compile();
|
||||
module.enableShutdownHooks();
|
||||
|
||||
const serverService = module.get<ServerService>(ServerService);
|
||||
server = await serverService.launch();
|
||||
|
||||
const usersRepository = module.get<UsersRepository>(DI.usersRepository);
|
||||
root = await usersRepository.insert({
|
||||
id: 'root',
|
||||
username: 'root',
|
||||
usernameLower: 'root',
|
||||
token: '1234567890123456',
|
||||
}).then(x => usersRepository.findOneByOrFail(x.identifiers[0]));
|
||||
|
||||
const userProfilesRepository = module.get<UserProfilesRepository>(DI.userProfilesRepository);
|
||||
await userProfilesRepository.insert({
|
||||
userId: root.id,
|
||||
});
|
||||
|
||||
roleService = module.get<RoleService>(RoleService);
|
||||
role_tinyAttachment = await roleService.create({
|
||||
name: 'test-role001',
|
||||
description: 'Test role001 description',
|
||||
target: 'manual',
|
||||
policies: {
|
||||
maxFileSizeMb: {
|
||||
useDefault: false,
|
||||
priority: 1,
|
||||
// 10byte
|
||||
value: 10 / 1024 / 1024,
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
beforeEach(async () => {
|
||||
s3Mock.reset();
|
||||
await roleService.unassign(root.id, role_tinyAttachment.id).catch(() => {});
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
await server.close();
|
||||
await module.close();
|
||||
});
|
||||
|
||||
test('200 ok', async () => {
|
||||
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(1024 * 1024)));
|
||||
expect(result.statusCode).toBe(200);
|
||||
});
|
||||
|
||||
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)));
|
||||
expect(result.statusCode).toBe(200);
|
||||
});
|
||||
|
||||
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)));
|
||||
expect(result.statusCode).toBe(413);
|
||||
expect(result.body.error.code).toBe('FILE_SIZE_TOO_LARGE');
|
||||
});
|
||||
});
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -4,13 +4,14 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
-->
|
||||
|
||||
<template>
|
||||
<div ref="rootEl">
|
||||
<div v-if="isPullStart" :class="$style.frame" :style="`--frame-min-height: ${pullDistance / (PULL_BRAKE_BASE + (pullDistance / PULL_BRAKE_FACTOR))}px;`">
|
||||
<div ref="rootEl" :class="isPulling ? $style.isPulling : null">
|
||||
<!-- 小数が含まれるとレンダリングが高頻度になりすぎパフォーマンスが悪化するためround -->
|
||||
<div v-if="isPulling" :class="$style.frame" :style="`--frame-min-height: ${Math.round(pullDistance / (PULL_BRAKE_BASE + (pullDistance / PULL_BRAKE_FACTOR)))}px;`">
|
||||
<div :class="$style.frameContent">
|
||||
<MkLoading v-if="isRefreshing" :class="$style.loader" :em="true"/>
|
||||
<i v-else class="ti ti-arrow-bar-to-down" :class="[$style.icon, { [$style.refresh]: isPullEnd }]"></i>
|
||||
<i v-else class="ti ti-arrow-bar-to-down" :class="[$style.icon, { [$style.refresh]: isPulledEnough }]"></i>
|
||||
<div :class="$style.text">
|
||||
<template v-if="isPullEnd">{{ i18n.ts.releaseToRefresh }}</template>
|
||||
<template v-if="isPulledEnough">{{ i18n.ts.releaseToRefresh }}</template>
|
||||
<template v-else-if="isRefreshing">{{ i18n.ts.refreshing }}</template>
|
||||
<template v-else>{{ i18n.ts.pullDownToRefresh }}</template>
|
||||
</div>
|
||||
|
@ -29,24 +30,21 @@ import { isHorizontalSwipeSwiping } from '@/utility/touch.js';
|
|||
|
||||
const SCROLL_STOP = 10;
|
||||
const MAX_PULL_DISTANCE = Infinity;
|
||||
const FIRE_THRESHOLD = 230;
|
||||
const FIRE_THRESHOLD = 200;
|
||||
const RELEASE_TRANSITION_DURATION = 200;
|
||||
const PULL_BRAKE_BASE = 1.5;
|
||||
const PULL_BRAKE_FACTOR = 170;
|
||||
|
||||
const isPullStart = ref(false);
|
||||
const isPullEnd = ref(false);
|
||||
const isPulling = ref(false);
|
||||
const isPulledEnough = ref(false);
|
||||
const isRefreshing = ref(false);
|
||||
const pullDistance = ref(0);
|
||||
|
||||
let supportPointerDesktop = false;
|
||||
let startScreenY: number | null = null;
|
||||
|
||||
const rootEl = useTemplateRef('rootEl');
|
||||
let scrollEl: HTMLElement | null = null;
|
||||
|
||||
let disabled = false;
|
||||
|
||||
const props = withDefaults(defineProps<{
|
||||
refresher: () => Promise<void>;
|
||||
}>(), {
|
||||
|
@ -57,18 +55,53 @@ const emit = defineEmits<{
|
|||
(ev: 'refresh'): void;
|
||||
}>();
|
||||
|
||||
function getScreenY(event) {
|
||||
if (supportPointerDesktop) {
|
||||
function getScreenY(event: TouchEvent | MouseEvent | PointerEvent): number {
|
||||
if (event.touches && event.touches[0] && event.touches[0].screenY != null) {
|
||||
return event.touches[0].screenY;
|
||||
} else {
|
||||
return event.screenY;
|
||||
}
|
||||
return event.touches[0].screenY;
|
||||
}
|
||||
|
||||
function moveStart(event) {
|
||||
if (!isPullStart.value && !isRefreshing.value && !disabled) {
|
||||
isPullStart.value = true;
|
||||
startScreenY = getScreenY(event);
|
||||
pullDistance.value = 0;
|
||||
// When at the top of the page, disable vertical overscroll so passive touch listeners can take over.
|
||||
function lockDownScroll() {
|
||||
if (scrollEl == null) return;
|
||||
scrollEl.style.touchAction = 'pan-x pan-down pinch-zoom';
|
||||
scrollEl.style.overscrollBehavior = 'none';
|
||||
}
|
||||
|
||||
function unlockDownScroll() {
|
||||
if (scrollEl == null) return;
|
||||
scrollEl.style.touchAction = 'auto';
|
||||
scrollEl.style.overscrollBehavior = 'contain';
|
||||
}
|
||||
|
||||
function moveStart(event: PointerEvent) {
|
||||
const scrollPos = scrollEl!.scrollTop;
|
||||
if (scrollPos === 0) {
|
||||
lockDownScroll();
|
||||
if (!isPulling.value && !isRefreshing.value) {
|
||||
isPulling.value = true;
|
||||
startScreenY = getScreenY(event);
|
||||
pullDistance.value = 0;
|
||||
|
||||
// タッチデバイスでPointerEventを使うとなんか挙動がおかしいので、TouchEventとMouseEventを使い分ける
|
||||
if (event.pointerType === 'mouse') {
|
||||
window.addEventListener('mousemove', moving, { passive: true });
|
||||
window.addEventListener('mouseup', () => {
|
||||
window.removeEventListener('mousemove', moving);
|
||||
onPullRelease();
|
||||
}, { passive: true, once: true });
|
||||
} else {
|
||||
window.addEventListener('touchmove', moving, { passive: true });
|
||||
window.addEventListener('touchend', () => {
|
||||
window.removeEventListener('touchmove', moving);
|
||||
onPullRelease();
|
||||
}, { passive: true, once: true });
|
||||
}
|
||||
}
|
||||
} else {
|
||||
unlockDownScroll();
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -108,31 +141,39 @@ async function closeContent() {
|
|||
}
|
||||
}
|
||||
|
||||
function moveEnd() {
|
||||
if (isPullStart.value && !isRefreshing.value) {
|
||||
startScreenY = null;
|
||||
if (isPullEnd.value) {
|
||||
isPullEnd.value = false;
|
||||
isRefreshing.value = true;
|
||||
fixOverContent().then(() => {
|
||||
emit('refresh');
|
||||
props.refresher().then(() => {
|
||||
refreshFinished();
|
||||
});
|
||||
function onPullRelease() {
|
||||
window.document.body.removeAttribute('inert');
|
||||
startScreenY = null;
|
||||
if (isPulledEnough.value) {
|
||||
isPulledEnough.value = false;
|
||||
isRefreshing.value = true;
|
||||
fixOverContent().then(() => {
|
||||
emit('refresh');
|
||||
props.refresher().then(() => {
|
||||
refreshFinished();
|
||||
});
|
||||
} else {
|
||||
closeContent().then(() => isPullStart.value = false);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
closeContent().then(() => isPulling.value = false);
|
||||
}
|
||||
}
|
||||
|
||||
function moving(event: TouchEvent | PointerEvent) {
|
||||
if (!isPullStart.value || isRefreshing.value || disabled) return;
|
||||
function toggleScrollLockOnTouchEnd() {
|
||||
const scrollPos = scrollEl!.scrollTop;
|
||||
if (scrollPos === 0) {
|
||||
lockDownScroll();
|
||||
} else {
|
||||
unlockDownScroll();
|
||||
}
|
||||
}
|
||||
|
||||
if ((scrollEl?.scrollTop ?? 0) > (supportPointerDesktop ? SCROLL_STOP : SCROLL_STOP + pullDistance.value) || isHorizontalSwipeSwiping.value) {
|
||||
function moving(event: MouseEvent | TouchEvent) {
|
||||
if (!isPulling.value || isRefreshing.value) return;
|
||||
|
||||
if ((scrollEl?.scrollTop ?? 0) > SCROLL_STOP + pullDistance.value || isHorizontalSwipeSwiping.value) {
|
||||
pullDistance.value = 0;
|
||||
isPullEnd.value = false;
|
||||
moveEnd();
|
||||
isPulledEnough.value = false;
|
||||
onPullRelease();
|
||||
return;
|
||||
}
|
||||
|
||||
|
@ -144,15 +185,12 @@ 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();
|
||||
// マウスでのpull時、画面上のテキスト選択が発生して画面がスクロールされたりするのを防ぐ
|
||||
if (pullDistance.value > 3) { // ある程度遊びを持たせないと通常のクリックでも発火しクリックできなくなる
|
||||
window.document.body.setAttribute('inert', 'true');
|
||||
}
|
||||
|
||||
if (pullDistance.value > SCROLL_STOP) {
|
||||
event.stopPropagation();
|
||||
}
|
||||
|
||||
isPullEnd.value = pullDistance.value >= FIRE_THRESHOLD;
|
||||
isPulledEnough.value = pullDistance.value >= FIRE_THRESHOLD;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -162,65 +200,31 @@ function moving(event: TouchEvent | PointerEvent) {
|
|||
*/
|
||||
function refreshFinished() {
|
||||
closeContent().then(() => {
|
||||
isPullStart.value = false;
|
||||
isPulling.value = false;
|
||||
isRefreshing.value = false;
|
||||
});
|
||||
}
|
||||
|
||||
function setDisabled(value) {
|
||||
disabled = value;
|
||||
}
|
||||
|
||||
function onScrollContainerScroll() {
|
||||
const scrollPos = scrollEl!.scrollTop;
|
||||
|
||||
// When at the top of the page, disable vertical overscroll so passive touch listeners can take over.
|
||||
if (scrollPos === 0) {
|
||||
scrollEl!.style.touchAction = 'pan-x pan-down pinch-zoom';
|
||||
registerEventListenersForReadyToPull();
|
||||
} else {
|
||||
scrollEl!.style.touchAction = 'auto';
|
||||
unregisterEventListenersForReadyToPull();
|
||||
}
|
||||
}
|
||||
|
||||
function registerEventListenersForReadyToPull() {
|
||||
if (rootEl.value == null) return;
|
||||
rootEl.value.addEventListener('touchstart', moveStart, { passive: true });
|
||||
rootEl.value.addEventListener('touchmove', moving, { passive: false }); // passive: 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('pointerdown', moveStart, { passive: true });
|
||||
rootEl.value.addEventListener('touchend', toggleScrollLockOnTouchEnd, { passive: true });
|
||||
});
|
||||
|
||||
onUnmounted(() => {
|
||||
if (scrollEl) scrollEl.removeEventListener('scroll', onScrollContainerScroll);
|
||||
|
||||
unregisterEventListenersForReadyToPull();
|
||||
});
|
||||
|
||||
defineExpose({
|
||||
setDisabled,
|
||||
unlockDownScroll();
|
||||
if (rootEl.value) rootEl.value.removeEventListener('pointerdown', moveStart);
|
||||
if (rootEl.value) rootEl.value.removeEventListener('touchend', toggleScrollLockOnTouchEnd);
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" module>
|
||||
.isPulling {
|
||||
will-change: contents;
|
||||
}
|
||||
|
||||
.frame {
|
||||
position: relative;
|
||||
overflow: clip;
|
||||
|
@ -242,7 +246,6 @@ defineExpose({
|
|||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
font-size: 14px;
|
||||
|
||||
> .icon, > .loader {
|
||||
margin: 6px 0;
|
||||
|
@ -258,6 +261,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"/>
|
||||
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -471,6 +471,14 @@ 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>
|
||||
</MkSwitch>
|
||||
</MkPreferenceContainer>
|
||||
</SearchMarker>
|
||||
|
||||
<SearchMarker :keywords="['keep', 'screen', 'display', 'on']">
|
||||
<MkPreferenceContainer k="keepScreenOn">
|
||||
<MkSwitch v-model="keepScreenOn">
|
||||
|
@ -800,6 +808,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 +866,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,
|
||||
},
|
||||
|
|
|
@ -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'));
|
||||
|
||||
|
|
158
pnpm-lock.yaml
158
pnpm-lock.yaml
|
@ -550,9 +550,6 @@ importers:
|
|||
'@types/sinonjs__fake-timers':
|
||||
specifier: 8.1.5
|
||||
version: 8.1.5
|
||||
'@types/supertest':
|
||||
specifier: 6.0.3
|
||||
version: 6.0.3
|
||||
'@types/tinycolor2':
|
||||
specifier: 1.4.6
|
||||
version: 1.4.6
|
||||
|
@ -604,9 +601,6 @@ importers:
|
|||
simple-oauth2:
|
||||
specifier: 5.1.0
|
||||
version: 5.1.0
|
||||
supertest:
|
||||
specifier: 7.1.0
|
||||
version: 7.1.0
|
||||
optionalDependencies:
|
||||
'@swc/core-android-arm64':
|
||||
specifier: 1.3.11
|
||||
|
@ -3148,9 +3142,6 @@ packages:
|
|||
peerDependencies:
|
||||
'@opentelemetry/api': ^1.1.0
|
||||
|
||||
'@paralleldrive/cuid2@2.2.2':
|
||||
resolution: {integrity: sha512-ZOBkgDwEdoYVlSeRbYYXs0S9MejQofiVYoTbKzy/6GQa39/q5tQU2IX46+shYnUkpEl3wc+J6wRlar7r2EK2xA==}
|
||||
|
||||
'@parcel/watcher-android-arm64@2.5.0':
|
||||
resolution: {integrity: sha512-qlX4eS28bUcQCdribHkg/herLe+0A9RyYC+mm2PXpncit8z5b3nSqGVzMNR3CmtAOgRutiZ02eIJJgP/b1iEFQ==}
|
||||
engines: {node: '>= 10.0.0'}
|
||||
|
@ -4294,9 +4285,6 @@ packages:
|
|||
'@types/cookie@0.6.0':
|
||||
resolution: {integrity: sha512-4Kh9a6B2bQciAhf7FSuMRRkUWecJgJu9nPnx3yzpsfXX/c50REIqpHY4C82bXP90qrLtXtkDxTZosYO3UpOwlA==}
|
||||
|
||||
'@types/cookiejar@2.1.5':
|
||||
resolution: {integrity: sha512-he+DHOWReW0nghN24E1WUqM0efK4kI9oTqDm6XmK8ZPe2djZ90BSNdGnIyCLzCPw7/pogPlGbzI2wHGGmi4O/Q==}
|
||||
|
||||
'@types/debug@4.1.12':
|
||||
resolution: {integrity: sha512-vIChWdVG3LG1SMxEvI/AK+FWJthlrqlTu7fbrlywTkkaONwk/UAGaULXRlf8vkzFBLVm0zkMdCquhL5aOjhXPQ==}
|
||||
|
||||
|
@ -4384,9 +4372,6 @@ packages:
|
|||
'@types/mdx@2.0.3':
|
||||
resolution: {integrity: sha512-IgHxcT3RC8LzFLhKwP3gbMPeaK7BM9eBH46OdapPA7yvuIUJ8H6zHZV53J8hGZcTSnt95jANt+rTBNUUc22ACQ==}
|
||||
|
||||
'@types/methods@1.1.4':
|
||||
resolution: {integrity: sha512-ymXWVrDiCxTBE3+RIrrP533E70eA+9qu7zdWoHuOmGujkYtzf4HQF96b8nwHLqhuf4ykX61IGRIB38CC6/sImQ==}
|
||||
|
||||
'@types/micromatch@4.0.9':
|
||||
resolution: {integrity: sha512-7V+8ncr22h4UoYRLnLXSpTxjQrNUXtWHGeMPRJt1nULXI57G9bIcpyrHlmrQ7QK24EyyuXvYcSSWAM8GA9nqCg==}
|
||||
|
||||
|
@ -4525,12 +4510,6 @@ packages:
|
|||
'@types/statuses@2.0.4':
|
||||
resolution: {integrity: sha512-eqNDvZsCNY49OAXB0Firg/Sc2BgoWsntsLUdybGFOhAfCD6QJ2n9HXUIHGqt5qjrxmMv4wS8WLAw43ZkKcJ8Pw==}
|
||||
|
||||
'@types/superagent@8.1.9':
|
||||
resolution: {integrity: sha512-pTVjI73witn+9ILmoJdajHGW2jkSaOzhiFYF1Rd3EQ94kymLqB9PjD9ISg7WaALC7+dCHT0FGe9T2LktLq/3GQ==}
|
||||
|
||||
'@types/supertest@6.0.3':
|
||||
resolution: {integrity: sha512-8WzXq62EXFhJ7QsH3Ocb/iKQ/Ty9ZVWnVzoTKc9tyyFRRF3a74Tk2+TLFgaFFw364Ere+npzHKEJ6ga2LzIL7w==}
|
||||
|
||||
'@types/tedious@4.0.14':
|
||||
resolution: {integrity: sha512-KHPsfX/FoVbUGbyYvk1q9MMQHLPeRZhRJZdO45Q4YjvFkv4hMNghCWTvy7rdKessBsmtz4euWCWAB6/tVpI1Iw==}
|
||||
|
||||
|
@ -5596,9 +5575,6 @@ packages:
|
|||
compare-versions@6.1.1:
|
||||
resolution: {integrity: sha512-4hm4VPpIecmlg59CHXnRDnqGplJFrbLG4aFEl5vl6cK1u76ws3LLvX7ikFnTDl5vo39sjWD6AaDPYodJp/NNHg==}
|
||||
|
||||
component-emitter@1.3.1:
|
||||
resolution: {integrity: sha512-T0+barUSQRTUQASh8bx02dl+DhF54GtIDY13Y3m9oWTklKbb3Wv974meRpeZ3lp1JpLVECWWNHC4vaG2XHXouQ==}
|
||||
|
||||
compress-commons@6.0.2:
|
||||
resolution: {integrity: sha512-6FqVXeETqWPoGcfzrXb37E50NP0LXT8kAMu5ooZayhWWdgEY4lBEEcbQNXtkuKQsGduxiIcI4gOTsxTmuq/bSg==}
|
||||
engines: {node: '>= 14'}
|
||||
|
@ -5656,9 +5632,6 @@ packages:
|
|||
resolution: {integrity: sha512-Xd8lFX4LM9QEEwxQpF9J9NTUh8pmdJO0cyRJhFiDoLTk2eH8FXlRv2IFGYVadZpqI3j8fhNrSdKCeYPxiAhLXw==}
|
||||
engines: {node: '>=18'}
|
||||
|
||||
cookiejar@2.1.4:
|
||||
resolution: {integrity: sha512-LDx6oHrK+PhzLKJU9j5S7/Y3jM/mUHvD/DeI1WQmJn652iPC5Y4TBzC9l+5OMOXlyTTA+SmVUPm0HQUwpD5Jqw==}
|
||||
|
||||
core-js@3.29.1:
|
||||
resolution: {integrity: sha512-+jwgnhg6cQxKYIIjGtAHq2nwUOolo9eoFZ4sHfUH09BLXBgxnH4gA0zEd+t+BO2cNB8idaBtZFcFTRjQJRJmAw==}
|
||||
|
||||
|
@ -5968,9 +5941,6 @@ packages:
|
|||
devlop@1.1.0:
|
||||
resolution: {integrity: sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA==}
|
||||
|
||||
dezalgo@1.0.4:
|
||||
resolution: {integrity: sha512-rXSP0bf+5n0Qonsb+SVVfNfIsimO4HEtmnIpPHY8Q1UCzKlQrDMfdobr8nJOOsRgWCyMRqeSBQzmWUMq7zvVig==}
|
||||
|
||||
diff-match-patch@1.0.5:
|
||||
resolution: {integrity: sha512-IayShXAgj/QMXgB0IWmKx+rOPuGMhqm5w6jvFxmVenXKIzRqTAAsbBPT3kWQeGANj3jGgvcvv4yK6SxqYmikgw==}
|
||||
|
||||
|
@ -6632,10 +6602,6 @@ packages:
|
|||
resolution: {integrity: sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g==}
|
||||
engines: {node: '>=12.20.0'}
|
||||
|
||||
formidable@3.5.4:
|
||||
resolution: {integrity: sha512-YikH+7CUTOtP44ZTnUhR7Ic2UASBPOqmaRkRKxRbywPTe5VxF7RRCck4af9wutiZ/QKM5nME9Bie2fFaPz5Gug==}
|
||||
engines: {node: '>=14.0.0'}
|
||||
|
||||
forwarded-parse@2.1.2:
|
||||
resolution: {integrity: sha512-alTFZZQDKMporBH77856pXgzhEzaUVmLCDk+egLgIgHst3Tpndzz8MnKe+GzRJRfvVdn69HhpW7cmXzvtLvJAw==}
|
||||
|
||||
|
@ -10049,14 +10015,6 @@ packages:
|
|||
peerDependencies:
|
||||
postcss: ^8.4.31
|
||||
|
||||
superagent@9.0.2:
|
||||
resolution: {integrity: sha512-xuW7dzkUpcJq7QnhOsnNUgtYp3xRwpt2F7abdRYIpCsAt0hhUqia0EdxyXZQQpNmGtsCzYHryaKSV3q3GJnq7w==}
|
||||
engines: {node: '>=14.18.0'}
|
||||
|
||||
supertest@7.1.0:
|
||||
resolution: {integrity: sha512-5QeSO8hSrKghtcWEoPiO036fxH0Ii2wVQfFZSP0oqQhmjk8bOLhDFXr4JrvaFmPuEWUoq4znY3uSi8UzLKxGqw==}
|
||||
engines: {node: '>=14.18.0'}
|
||||
|
||||
supports-color@5.5.0:
|
||||
resolution: {integrity: sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==}
|
||||
engines: {node: '>=4'}
|
||||
|
@ -11570,7 +11528,7 @@ snapshots:
|
|||
'@babel/traverse': 7.24.7
|
||||
'@babel/types': 7.25.6
|
||||
convert-source-map: 2.0.0
|
||||
debug: 4.4.0(supports-color@8.1.1)
|
||||
debug: 4.4.0(supports-color@5.5.0)
|
||||
gensync: 1.0.0-beta.2
|
||||
json5: 2.2.3
|
||||
semver: 6.3.1
|
||||
|
@ -11590,7 +11548,7 @@ snapshots:
|
|||
'@babel/traverse': 7.24.7
|
||||
'@babel/types': 7.25.6
|
||||
convert-source-map: 2.0.0
|
||||
debug: 4.4.0(supports-color@8.1.1)
|
||||
debug: 4.4.0(supports-color@5.5.0)
|
||||
gensync: 1.0.0-beta.2
|
||||
json5: 2.2.3
|
||||
semver: 6.3.1
|
||||
|
@ -11812,7 +11770,7 @@ snapshots:
|
|||
'@babel/helper-split-export-declaration': 7.24.7
|
||||
'@babel/parser': 7.25.6
|
||||
'@babel/types': 7.25.6
|
||||
debug: 4.4.0(supports-color@8.1.1)
|
||||
debug: 4.4.0(supports-color@5.5.0)
|
||||
globals: 11.12.0
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
@ -12148,7 +12106,7 @@ snapshots:
|
|||
'@eslint/config-array@0.20.0':
|
||||
dependencies:
|
||||
'@eslint/object-schema': 2.1.6
|
||||
debug: 4.4.0(supports-color@8.1.1)
|
||||
debug: 4.4.0(supports-color@5.5.0)
|
||||
minimatch: 3.1.2
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
@ -12162,7 +12120,7 @@ snapshots:
|
|||
'@eslint/eslintrc@3.3.1':
|
||||
dependencies:
|
||||
ajv: 6.12.6
|
||||
debug: 4.4.0(supports-color@8.1.1)
|
||||
debug: 4.4.0(supports-color@5.5.0)
|
||||
espree: 10.3.0
|
||||
globals: 14.0.0
|
||||
ignore: 5.3.1
|
||||
|
@ -13201,10 +13159,6 @@ snapshots:
|
|||
'@opentelemetry/api': 1.9.0
|
||||
'@opentelemetry/core': 1.30.1(@opentelemetry/api@1.9.0)
|
||||
|
||||
'@paralleldrive/cuid2@2.2.2':
|
||||
dependencies:
|
||||
'@noble/hashes': 1.7.1
|
||||
|
||||
'@parcel/watcher-android-arm64@2.5.0':
|
||||
optional: true
|
||||
|
||||
|
@ -14552,7 +14506,7 @@ snapshots:
|
|||
|
||||
'@tokenizer/inflate@0.2.7':
|
||||
dependencies:
|
||||
debug: 4.4.0(supports-color@8.1.1)
|
||||
debug: 4.4.0(supports-color@5.5.0)
|
||||
fflate: 0.8.2
|
||||
token-types: 6.0.0
|
||||
transitivePeerDependencies:
|
||||
|
@ -14632,8 +14586,6 @@ snapshots:
|
|||
|
||||
'@types/cookie@0.6.0': {}
|
||||
|
||||
'@types/cookiejar@2.1.5': {}
|
||||
|
||||
'@types/debug@4.1.12':
|
||||
dependencies:
|
||||
'@types/ms': 0.7.34
|
||||
|
@ -14727,8 +14679,6 @@ snapshots:
|
|||
|
||||
'@types/mdx@2.0.3': {}
|
||||
|
||||
'@types/methods@1.1.4': {}
|
||||
|
||||
'@types/micromatch@4.0.9':
|
||||
dependencies:
|
||||
'@types/braces': 3.0.1
|
||||
|
@ -14864,18 +14814,6 @@ snapshots:
|
|||
|
||||
'@types/statuses@2.0.4': {}
|
||||
|
||||
'@types/superagent@8.1.9':
|
||||
dependencies:
|
||||
'@types/cookiejar': 2.1.5
|
||||
'@types/methods': 1.1.4
|
||||
'@types/node': 22.15.2
|
||||
form-data: 4.0.2
|
||||
|
||||
'@types/supertest@6.0.3':
|
||||
dependencies:
|
||||
'@types/methods': 1.1.4
|
||||
'@types/superagent': 8.1.9
|
||||
|
||||
'@types/tedious@4.0.14':
|
||||
dependencies:
|
||||
'@types/node': 22.15.2
|
||||
|
@ -14940,7 +14878,7 @@ snapshots:
|
|||
'@typescript-eslint/types': 8.31.0
|
||||
'@typescript-eslint/typescript-estree': 8.31.0(typescript@5.8.3)
|
||||
'@typescript-eslint/visitor-keys': 8.31.0
|
||||
debug: 4.4.0(supports-color@8.1.1)
|
||||
debug: 4.4.0(supports-color@5.5.0)
|
||||
eslint: 9.25.1
|
||||
typescript: 5.8.3
|
||||
transitivePeerDependencies:
|
||||
|
@ -14955,7 +14893,7 @@ snapshots:
|
|||
dependencies:
|
||||
'@typescript-eslint/typescript-estree': 8.31.0(typescript@5.8.3)
|
||||
'@typescript-eslint/utils': 8.31.0(eslint@9.25.1)(typescript@5.8.3)
|
||||
debug: 4.4.0(supports-color@8.1.1)
|
||||
debug: 4.4.0(supports-color@5.5.0)
|
||||
eslint: 9.25.1
|
||||
ts-api-utils: 2.1.0(typescript@5.8.3)
|
||||
typescript: 5.8.3
|
||||
|
@ -14968,7 +14906,7 @@ snapshots:
|
|||
dependencies:
|
||||
'@typescript-eslint/types': 8.31.0
|
||||
'@typescript-eslint/visitor-keys': 8.31.0
|
||||
debug: 4.4.0(supports-color@8.1.1)
|
||||
debug: 4.4.0(supports-color@5.5.0)
|
||||
fast-glob: 3.3.3
|
||||
is-glob: 4.0.3
|
||||
minimatch: 9.0.5
|
||||
|
@ -15005,7 +14943,7 @@ snapshots:
|
|||
dependencies:
|
||||
'@ampproject/remapping': 2.3.0
|
||||
'@bcoe/v8-coverage': 1.0.2
|
||||
debug: 4.4.0(supports-color@8.1.1)
|
||||
debug: 4.4.0(supports-color@5.5.0)
|
||||
istanbul-lib-coverage: 3.2.2
|
||||
istanbul-lib-report: 3.0.1
|
||||
istanbul-lib-source-maps: 5.0.6
|
||||
|
@ -15316,14 +15254,14 @@ snapshots:
|
|||
|
||||
agent-base@6.0.2:
|
||||
dependencies:
|
||||
debug: 4.4.0(supports-color@8.1.1)
|
||||
debug: 4.4.0(supports-color@5.5.0)
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
optional: true
|
||||
|
||||
agent-base@7.1.0:
|
||||
dependencies:
|
||||
debug: 4.4.0(supports-color@8.1.1)
|
||||
debug: 4.4.0(supports-color@5.5.0)
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
|
@ -16160,8 +16098,6 @@ snapshots:
|
|||
|
||||
compare-versions@6.1.1: {}
|
||||
|
||||
component-emitter@1.3.1: {}
|
||||
|
||||
compress-commons@6.0.2:
|
||||
dependencies:
|
||||
crc-32: 1.2.2
|
||||
|
@ -16214,8 +16150,6 @@ snapshots:
|
|||
|
||||
cookie@1.0.1: {}
|
||||
|
||||
cookiejar@2.1.4: {}
|
||||
|
||||
core-js@3.29.1: {}
|
||||
|
||||
core-util-is@1.0.2: {}
|
||||
|
@ -16598,11 +16532,6 @@ snapshots:
|
|||
dependencies:
|
||||
dequal: 2.0.3
|
||||
|
||||
dezalgo@1.0.4:
|
||||
dependencies:
|
||||
asap: 2.0.6
|
||||
wrappy: 1.0.2
|
||||
|
||||
diff-match-patch@1.0.5: {}
|
||||
|
||||
diff-sequences@29.6.3: {}
|
||||
|
@ -16905,7 +16834,7 @@ snapshots:
|
|||
|
||||
esbuild-register@3.5.0(esbuild@0.25.3):
|
||||
dependencies:
|
||||
debug: 4.4.0(supports-color@8.1.1)
|
||||
debug: 4.4.0(supports-color@5.5.0)
|
||||
esbuild: 0.25.3
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
@ -17082,7 +17011,7 @@ snapshots:
|
|||
ajv: 6.12.6
|
||||
chalk: 4.1.2
|
||||
cross-spawn: 7.0.6
|
||||
debug: 4.4.0(supports-color@8.1.1)
|
||||
debug: 4.4.0(supports-color@5.5.0)
|
||||
escape-string-regexp: 4.0.0
|
||||
eslint-scope: 8.3.0
|
||||
eslint-visitor-keys: 4.2.0
|
||||
|
@ -17533,7 +17462,7 @@ snapshots:
|
|||
|
||||
follow-redirects@1.15.9(debug@4.4.0):
|
||||
optionalDependencies:
|
||||
debug: 4.4.0(supports-color@8.1.1)
|
||||
debug: 4.4.0(supports-color@5.5.0)
|
||||
|
||||
for-each@0.3.3:
|
||||
dependencies:
|
||||
|
@ -17561,12 +17490,6 @@ snapshots:
|
|||
dependencies:
|
||||
fetch-blob: 3.2.0
|
||||
|
||||
formidable@3.5.4:
|
||||
dependencies:
|
||||
'@paralleldrive/cuid2': 2.2.2
|
||||
dezalgo: 1.0.4
|
||||
once: 1.4.0
|
||||
|
||||
forwarded-parse@2.1.2: {}
|
||||
|
||||
forwarded@0.2.0: {}
|
||||
|
@ -17981,7 +17904,7 @@ snapshots:
|
|||
http-proxy-agent@7.0.2:
|
||||
dependencies:
|
||||
agent-base: 7.1.3
|
||||
debug: 4.4.0(supports-color@8.1.1)
|
||||
debug: 4.4.0(supports-color@5.5.0)
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
|
@ -18009,7 +17932,7 @@ snapshots:
|
|||
https-proxy-agent@5.0.1:
|
||||
dependencies:
|
||||
agent-base: 6.0.2
|
||||
debug: 4.4.0(supports-color@8.1.1)
|
||||
debug: 4.4.0(supports-color@5.5.0)
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
optional: true
|
||||
|
@ -18017,14 +17940,14 @@ snapshots:
|
|||
https-proxy-agent@7.0.2:
|
||||
dependencies:
|
||||
agent-base: 7.1.0
|
||||
debug: 4.4.0(supports-color@8.1.1)
|
||||
debug: 4.4.0(supports-color@5.5.0)
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
https-proxy-agent@7.0.6:
|
||||
dependencies:
|
||||
agent-base: 7.1.3
|
||||
debug: 4.4.0(supports-color@8.1.1)
|
||||
debug: 4.4.0(supports-color@5.5.0)
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
|
@ -18122,7 +18045,7 @@ snapshots:
|
|||
dependencies:
|
||||
'@ioredis/commands': 1.2.0
|
||||
cluster-key-slot: 1.1.2
|
||||
debug: 4.4.0(supports-color@8.1.1)
|
||||
debug: 4.4.0(supports-color@5.5.0)
|
||||
denque: 2.1.0
|
||||
lodash.defaults: 4.2.0
|
||||
lodash.isarguments: 3.1.0
|
||||
|
@ -18356,7 +18279,7 @@ snapshots:
|
|||
|
||||
istanbul-lib-source-maps@4.0.1:
|
||||
dependencies:
|
||||
debug: 4.4.0(supports-color@8.1.1)
|
||||
debug: 4.4.0(supports-color@5.5.0)
|
||||
istanbul-lib-coverage: 3.2.2
|
||||
source-map: 0.6.1
|
||||
transitivePeerDependencies:
|
||||
|
@ -18365,7 +18288,7 @@ snapshots:
|
|||
istanbul-lib-source-maps@5.0.6:
|
||||
dependencies:
|
||||
'@jridgewell/trace-mapping': 0.3.25
|
||||
debug: 4.4.0(supports-color@8.1.1)
|
||||
debug: 4.4.0(supports-color@5.5.0)
|
||||
istanbul-lib-coverage: 3.2.2
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
@ -19389,7 +19312,7 @@ snapshots:
|
|||
micromark@4.0.0:
|
||||
dependencies:
|
||||
'@types/debug': 4.1.12
|
||||
debug: 4.4.0(supports-color@8.1.1)
|
||||
debug: 4.4.0(supports-color@5.5.0)
|
||||
decode-named-character-reference: 1.0.2
|
||||
devlop: 1.1.0
|
||||
micromark-core-commonmark: 2.0.0
|
||||
|
@ -20794,7 +20717,7 @@ snapshots:
|
|||
|
||||
require-in-the-middle@7.3.0:
|
||||
dependencies:
|
||||
debug: 4.4.0(supports-color@8.1.1)
|
||||
debug: 4.4.0(supports-color@5.5.0)
|
||||
module-details-from-path: 1.0.3
|
||||
resolve: 1.22.8
|
||||
transitivePeerDependencies:
|
||||
|
@ -21244,7 +21167,7 @@ snapshots:
|
|||
socks-proxy-agent@8.0.2:
|
||||
dependencies:
|
||||
agent-base: 7.1.3
|
||||
debug: 4.4.0(supports-color@8.1.1)
|
||||
debug: 4.4.0(supports-color@5.5.0)
|
||||
socks: 2.7.1
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
@ -21353,7 +21276,7 @@ snapshots:
|
|||
arg: 5.0.2
|
||||
bluebird: 3.7.2
|
||||
check-more-types: 2.24.0
|
||||
debug: 4.4.0(supports-color@8.1.1)
|
||||
debug: 4.4.0(supports-color@5.5.0)
|
||||
execa: 5.1.1
|
||||
lazy-ass: 1.6.0
|
||||
ps-tree: 1.2.0
|
||||
|
@ -21545,27 +21468,6 @@ snapshots:
|
|||
postcss: 8.5.3
|
||||
postcss-selector-parser: 6.1.2
|
||||
|
||||
superagent@9.0.2:
|
||||
dependencies:
|
||||
component-emitter: 1.3.1
|
||||
cookiejar: 2.1.4
|
||||
debug: 4.4.0(supports-color@8.1.1)
|
||||
fast-safe-stringify: 2.1.1
|
||||
form-data: 4.0.2
|
||||
formidable: 3.5.4
|
||||
methods: 1.1.2
|
||||
mime: 2.6.0
|
||||
qs: 6.14.0
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
supertest@7.1.0:
|
||||
dependencies:
|
||||
methods: 1.1.2
|
||||
superagent: 9.0.2
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
supports-color@5.5.0:
|
||||
dependencies:
|
||||
has-flag: 3.0.0
|
||||
|
@ -21923,7 +21825,7 @@ snapshots:
|
|||
app-root-path: 3.1.0
|
||||
buffer: 6.0.3
|
||||
dayjs: 1.11.13
|
||||
debug: 4.4.0(supports-color@8.1.1)
|
||||
debug: 4.4.0(supports-color@5.5.0)
|
||||
dotenv: 16.4.7
|
||||
glob: 10.4.5
|
||||
reflect-metadata: 0.2.2
|
||||
|
@ -22117,7 +22019,7 @@ snapshots:
|
|||
vite-node@3.1.2(@types/node@22.15.2)(sass@1.87.0)(terser@5.39.0)(tsx@4.19.3):
|
||||
dependencies:
|
||||
cac: 6.7.14
|
||||
debug: 4.4.0(supports-color@8.1.1)
|
||||
debug: 4.4.0(supports-color@5.5.0)
|
||||
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)
|
||||
|
@ -22166,7 +22068,7 @@ snapshots:
|
|||
'@vitest/spy': 3.1.2
|
||||
'@vitest/utils': 3.1.2
|
||||
chai: 5.2.0
|
||||
debug: 4.4.0(supports-color@8.1.1)
|
||||
debug: 4.4.0(supports-color@5.5.0)
|
||||
expect-type: 1.2.1
|
||||
magic-string: 0.30.17
|
||||
pathe: 2.0.3
|
||||
|
@ -22253,7 +22155,7 @@ snapshots:
|
|||
|
||||
vue-eslint-parser@10.1.3(eslint@9.25.1):
|
||||
dependencies:
|
||||
debug: 4.4.0(supports-color@8.1.1)
|
||||
debug: 4.4.0(supports-color@5.5.0)
|
||||
eslint: 9.25.1
|
||||
eslint-scope: 8.3.0
|
||||
eslint-visitor-keys: 4.2.0
|
||||
|
|
Loading…
Reference in New Issue