Merge branch 'develop' into mahjong

This commit is contained in:
syuilo 2024-07-08 13:08:57 +09:00
commit 6b16b85203
126 changed files with 6662 additions and 4854 deletions

View File

@ -10,14 +10,14 @@ on:
- packages/frontend/** - packages/frontend/**
- packages/sw/** - packages/sw/**
- packages/misskey-js/** - packages/misskey-js/**
- packages/shared/.eslintrc.js - packages/shared/eslint.config.js
pull_request: pull_request:
paths: paths:
- packages/backend/** - packages/backend/**
- packages/frontend/** - packages/frontend/**
- packages/sw/** - packages/sw/**
- packages/misskey-js/** - packages/misskey-js/**
- packages/shared/.eslintrc.js - packages/shared/eslint.config.js
jobs: jobs:
pnpm_install: pnpm_install:

View File

@ -14,21 +14,26 @@
- Fix: テーマプレビューが見れない問題を修正 - Fix: テーマプレビューが見れない問題を修正
### Server ### Server
- チャート生成時にinstance.suspentionStateに置き換えられたinstance.isSuspendedが参照されてしまう問題を修正
- Feat: レートリミット制限に引っかかったときに`Retry-After`ヘッダーを返すように (#13949) - Feat: レートリミット制限に引っかかったときに`Retry-After`ヘッダーを返すように (#13949)
- Fix: ユーザーのフィードページのMFMをHTMLに展開するように (#14006)
- Fix: アンテナ・クリップ・リスト・ウェブフックがロールポリシーの上限より一つ多く作れてしまうのを修正 (#14036)
- Enhance: エンドポイント`clips/update`の必須項目を`clipId`のみに - Enhance: エンドポイント`clips/update`の必須項目を`clipId`のみに
- Enhance: エンドポイント`admin/roles/update`の必須項目を`roleId`のみに - Enhance: エンドポイント`admin/roles/update`の必須項目を`roleId`のみに
- Enhance: エンドポイント`pages/update`の必須項目を`pageId`のみに - Enhance: エンドポイント`pages/update`の必須項目を`pageId`のみに
- Enhance: エンドポイント`gallery/posts/update`の必須項目を`postId`のみに - Enhance: エンドポイント`gallery/posts/update`の必須項目を`postId`のみに
- Enhance: エンドポイント`i/webhook/update`の必須項目を`webhookId`のみに - Enhance: エンドポイント`i/webhook/update`の必須項目を`webhookId`のみに
- Enhance: エンドポイント`admin/ad/update`の必須項目を`id`のみに - Enhance: エンドポイント`admin/ad/update`の必須項目を`id`のみに
- Fix: チャート生成時にinstance.suspentionStateに置き換えられたinstance.isSuspendedが参照されてしまう問題を修正
- Fix: ユーザーのフィードページのMFMをHTMLに展開するように (#14006)
- Fix: アンテナ・クリップ・リスト・ウェブフックがロールポリシーの上限より一つ多く作れてしまうのを修正 (#14036)
- Fix: notRespondingSinceが実装される前に不通になったインスタンスが自動的に配信停止にならない (#14059) - Fix: notRespondingSinceが実装される前に不通になったインスタンスが自動的に配信停止にならない (#14059)
- Fix: FTT有効時、タイムライン用エンドポイントで`sinceId`にキャッシュ内最古のものより古いものを指定した場合に正しく結果が返ってこない問題を修正 - Fix: FTT有効時、タイムライン用エンドポイントで`sinceId`にキャッシュ内最古のものより古いものを指定した場合に正しく結果が返ってこない問題を修正
- Fix: 自分以外のクリップ内のノート個数が見えることがあるのを修正 - Fix: 自分以外のクリップ内のノート個数が見えることがあるのを修正
- Fix: 空文字列のリアクションはフォールバックされるように - Fix: 空文字列のリアクションはフォールバックされるように
- Fix: リノートにリアクションできないように - Fix: リノートにリアクションできないように
- Fix: ユーザー名の前後に空白文字列がある場合は省略するように
- Fix: プロフィール編集時に名前を空白文字列のみにできる問題を修正
### Misskey.js
- Feat: `/drive/files/create` のリクエストに対応(`multipart/form-data`に対応)
## 2024.5.0 ## 2024.5.0

View File

@ -52,7 +52,11 @@ const primaries = {
const clean = (text) => text.replace(new RegExp(String.fromCodePoint(0x08), 'g'), ''); const clean = (text) => text.replace(new RegExp(String.fromCodePoint(0x08), 'g'), '');
export function build() { export function build() {
const locales = languages.reduce((a, c) => (a[c] = yaml.load(clean(fs.readFileSync(new URL(`${c}.yml`, import.meta.url), 'utf-8'))) || {}, a), {}); // vitestの挙動を調整するため、一度ローカル変数化する必要がある
// https://github.com/vitest-dev/vitest/issues/3988#issuecomment-1686599577
// https://github.com/misskey-dev/misskey/pull/14057#issuecomment-2192833785
const metaUrl = import.meta.url;
const locales = languages.reduce((a, c) => (a[c] = yaml.load(clean(fs.readFileSync(new URL(`${c}.yml`, metaUrl), 'utf-8'))) || {}, a), {});
// 空文字列が入ることがあり、フォールバックが動作しなくなるのでプロパティごと消す // 空文字列が入ることがあり、フォールバックが動作しなくなるのでプロパティごと消す
const removeEmpty = (obj) => { const removeEmpty = (obj) => {

View File

@ -56,20 +56,22 @@
"js-yaml": "4.1.0", "js-yaml": "4.1.0",
"postcss": "8.4.38", "postcss": "8.4.38",
"tar": "6.2.1", "tar": "6.2.1",
"terser": "5.30.3", "terser": "5.31.1",
"typescript": "5.5.2", "typescript": "5.5.3",
"esbuild": "0.20.2", "esbuild": "0.22.0",
"glob": "10.3.12" "glob": "10.3.12"
}, },
"devDependencies": { "devDependencies": {
"@types/node": "20.12.7", "@misskey-dev/eslint-plugin": "2.0.2",
"@typescript-eslint/eslint-plugin": "7.7.1", "@types/node": "20.14.9",
"@typescript-eslint/parser": "7.7.1", "@typescript-eslint/eslint-plugin": "7.15.0",
"@typescript-eslint/parser": "7.15.0",
"cross-env": "7.0.3", "cross-env": "7.0.3",
"cypress": "13.7.3", "cypress": "13.13.0",
"eslint": "8.57.0", "eslint": "9.6.0",
"globals": "15.7.0",
"ncp": "2.0.0", "ncp": "2.0.0",
"start-server-and-test": "2.0.3" "start-server-and-test": "2.0.4"
}, },
"optionalDependencies": { "optionalDependencies": {
"@tensorflow/tfjs-core": "4.4.0" "@tensorflow/tfjs-core": "4.4.0"

View File

@ -1,4 +0,0 @@
node_modules
/built
/.eslintrc.js
/@types/**/*

View File

@ -1,32 +0,0 @@
module.exports = {
parserOptions: {
tsconfigRootDir: __dirname,
project: ['./tsconfig.json', './test/tsconfig.json'],
},
extends: [
'../shared/.eslintrc.js',
],
rules: {
'import/order': ['warn', {
'groups': ['builtin', 'external', 'internal', 'parent', 'sibling', 'index', 'object', 'type'],
'pathGroups': [
{
'pattern': '@/**',
'group': 'external',
'position': 'after'
}
],
}],
'no-restricted-globals': [
'error',
{
'name': '__dirname',
'message': 'Not in ESModule. Use `import.meta.url` instead.'
},
{
'name': '__filename',
'message': 'Not in ESModule. Use `import.meta.url` instead.'
}
]
},
};

View File

@ -0,0 +1,46 @@
import tsParser from '@typescript-eslint/parser';
import sharedConfig from '../shared/eslint.config.js';
export default [
...sharedConfig,
{
ignores: ['**/node_modules', 'built', '@types/**/*'],
},
{
files: ['**/*.ts', '**/*.tsx'],
languageOptions: {
parserOptions: {
parser: tsParser,
project: ['./tsconfig.json', './test/tsconfig.json'],
sourceType: 'module',
tsconfigRootDir: import.meta.dirname,
},
},
rules: {
'import/order': ['warn', {
groups: [
'builtin',
'external',
'internal',
'parent',
'sibling',
'index',
'object',
'type',
],
pathGroups: [{
pattern: '@/**',
group: 'external',
position: 'after',
}],
}],
'no-restricted-globals': ['error', {
name: '__dirname',
message: 'Not in ESModule. Use `import.meta.url` instead.',
}, {
name: '__filename',
message: 'Not in ESModule. Use `import.meta.url` instead.',
}],
},
},
];

View File

@ -65,43 +65,43 @@
"utf-8-validate": "6.0.3" "utf-8-validate": "6.0.3"
}, },
"dependencies": { "dependencies": {
"@aws-sdk/client-s3": "3.412.0", "@aws-sdk/client-s3": "3.600.0",
"@aws-sdk/lib-storage": "3.412.0", "@aws-sdk/lib-storage": "3.600.0",
"@bull-board/api": "5.17.0", "@bull-board/api": "5.20.5",
"@bull-board/fastify": "5.17.0", "@bull-board/fastify": "5.20.5",
"@bull-board/ui": "5.17.0", "@bull-board/ui": "5.20.5",
"@discordapp/twemoji": "15.0.3", "@discordapp/twemoji": "15.0.3",
"@fastify/accepts": "4.3.0", "@fastify/accepts": "4.3.0",
"@fastify/cookie": "9.3.1", "@fastify/cookie": "9.3.1",
"@fastify/cors": "9.0.1", "@fastify/cors": "9.0.1",
"@fastify/express": "3.0.0", "@fastify/express": "3.0.0",
"@fastify/http-proxy": "9.5.0", "@fastify/http-proxy": "9.5.0",
"@fastify/multipart": "8.2.0", "@fastify/multipart": "8.3.0",
"@fastify/static": "7.0.3", "@fastify/static": "7.0.4",
"@fastify/view": "9.1.0", "@fastify/view": "9.1.0",
"@misskey-dev/sharp-read-bmp": "1.2.0", "@misskey-dev/sharp-read-bmp": "1.2.0",
"@misskey-dev/summaly": "5.1.0", "@misskey-dev/summaly": "5.1.0",
"@napi-rs/canvas": "^0.1.52", "@napi-rs/canvas": "^0.1.53",
"@nestjs/common": "10.3.8", "@nestjs/common": "10.3.10",
"@nestjs/core": "10.3.8", "@nestjs/core": "10.3.10",
"@nestjs/testing": "10.3.8", "@nestjs/testing": "10.3.10",
"@peertube/http-signature": "1.7.0", "@peertube/http-signature": "1.7.0",
"@sentry/node": "^8.5.0", "@sentry/node": "8.13.0",
"@sentry/profiling-node": "^8.5.0", "@sentry/profiling-node": "8.13.0",
"@simplewebauthn/server": "10.0.0", "@simplewebauthn/server": "10.0.0",
"@sinonjs/fake-timers": "11.2.2", "@sinonjs/fake-timers": "11.2.2",
"@smithy/node-http-handler": "2.5.0", "@smithy/node-http-handler": "2.5.0",
"@swc/cli": "0.3.12", "@swc/cli": "0.3.12",
"@swc/core": "1.4.17", "@swc/core": "1.6.6",
"@twemoji/parser": "15.1.1", "@twemoji/parser": "15.1.1",
"accepts": "1.3.8", "accepts": "1.3.8",
"ajv": "8.13.0", "ajv": "8.16.0",
"archiver": "7.0.1", "archiver": "7.0.1",
"async-mutex": "0.5.0", "async-mutex": "0.5.0",
"bcryptjs": "2.4.3", "bcryptjs": "2.4.3",
"blurhash": "2.0.5", "blurhash": "2.0.5",
"body-parser": "1.20.2", "body-parser": "1.20.2",
"bullmq": "5.7.8", "bullmq": "5.8.3",
"cacheable-lookup": "7.0.0", "cacheable-lookup": "7.0.0",
"cbor": "9.0.2", "cbor": "9.0.2",
"chalk": "5.3.0", "chalk": "5.3.0",
@ -112,27 +112,27 @@
"content-disposition": "0.5.4", "content-disposition": "0.5.4",
"date-fns": "2.30.0", "date-fns": "2.30.0",
"deep-email-validator": "0.1.21", "deep-email-validator": "0.1.21",
"fastify": "4.26.2", "fastify": "4.28.1",
"fastify-raw-body": "4.3.0", "fastify-raw-body": "4.3.0",
"feed": "4.2.2", "feed": "4.2.2",
"file-type": "19.0.0", "file-type": "19.0.0",
"fluent-ffmpeg": "2.1.2", "fluent-ffmpeg": "2.1.3",
"form-data": "4.0.0", "form-data": "4.0.0",
"got": "14.2.1", "got": "14.4.1",
"happy-dom": "10.0.3", "happy-dom": "10.0.3",
"hpagent": "1.2.0", "hpagent": "1.2.0",
"htmlescape": "1.1.1", "htmlescape": "1.1.1",
"http-link-header": "1.1.3", "http-link-header": "1.1.3",
"ioredis": "5.4.1", "ioredis": "5.4.1",
"ip-cidr": "3.1.0", "ip-cidr": "4.0.1",
"ipaddr.js": "2.2.0", "ipaddr.js": "2.2.0",
"is-svg": "5.0.0", "is-svg": "5.0.1",
"js-yaml": "4.1.0", "js-yaml": "4.1.0",
"jsdom": "24.0.0", "jsdom": "24.1.0",
"json5": "2.2.3", "json5": "2.2.3",
"jsonld": "8.3.2", "jsonld": "8.3.2",
"jsrsasign": "11.1.0", "jsrsasign": "11.1.0",
"meilisearch": "0.38.0", "meilisearch": "0.41.0",
"mfm-js": "0.24.0", "mfm-js": "0.24.0",
"microformats-parser": "2.0.2", "microformats-parser": "2.0.2",
"mime-types": "2.1.35", "mime-types": "2.1.35",
@ -143,24 +143,24 @@
"nanoid": "5.0.7", "nanoid": "5.0.7",
"nested-property": "4.0.0", "nested-property": "4.0.0",
"node-fetch": "3.3.2", "node-fetch": "3.3.2",
"nodemailer": "6.9.13", "nodemailer": "6.9.14",
"nsfwjs": "2.4.2", "nsfwjs": "2.4.2",
"oauth": "0.10.0", "oauth": "0.10.0",
"oauth2orize": "1.12.0", "oauth2orize": "1.12.0",
"oauth2orize-pkce": "0.1.2", "oauth2orize-pkce": "0.1.2",
"os-utils": "0.0.14", "os-utils": "0.0.14",
"otpauth": "9.2.3", "otpauth": "9.3.1",
"parse5": "7.1.2", "parse5": "7.1.2",
"pg": "8.11.5", "pg": "8.12.0",
"pkce-challenge": "4.1.0", "pkce-challenge": "4.1.0",
"probe-image-size": "7.2.3", "probe-image-size": "7.2.3",
"promise-limit": "2.7.0", "promise-limit": "2.7.0",
"pug": "3.0.2", "pug": "3.0.3",
"punycode": "2.3.1", "punycode": "2.3.1",
"qrcode": "1.5.3", "qrcode": "1.5.3",
"random-seed": "0.3.0", "random-seed": "0.3.0",
"ratelimiter": "3.4.1", "ratelimiter": "3.4.1",
"re2": "1.21.2", "re2": "1.21.3",
"redis-lock": "0.1.4", "redis-lock": "0.1.4",
"reflect-metadata": "0.2.2", "reflect-metadata": "0.2.2",
"rename": "1.0.4", "rename": "1.0.4",
@ -168,27 +168,26 @@
"rxjs": "7.8.1", "rxjs": "7.8.1",
"sanitize-html": "2.13.0", "sanitize-html": "2.13.0",
"secure-json-parse": "2.7.0", "secure-json-parse": "2.7.0",
"sharp": "0.33.3", "sharp": "0.33.4",
"slacc": "0.0.10", "slacc": "0.0.10",
"strict-event-emitter-types": "2.0.0", "strict-event-emitter-types": "2.0.0",
"stringz": "2.1.0", "stringz": "2.1.0",
"systeminformation": "5.22.7", "systeminformation": "5.22.11",
"tinycolor2": "1.6.0", "tinycolor2": "1.6.0",
"tmp": "0.2.3", "tmp": "0.2.3",
"tsc-alias": "1.8.8", "tsc-alias": "1.8.10",
"tsconfig-paths": "4.2.0", "tsconfig-paths": "4.2.0",
"typeorm": "0.3.20", "typeorm": "0.3.20",
"typescript": "5.5.2", "typescript": "5.5.3",
"ulid": "2.3.0", "ulid": "2.3.0",
"vary": "1.1.2", "vary": "1.1.2",
"web-push": "3.6.7", "web-push": "3.6.7",
"ws": "8.17.0", "ws": "8.17.1",
"xev": "3.0.2" "xev": "3.0.2"
}, },
"devDependencies": { "devDependencies": {
"@jest/globals": "29.7.0", "@jest/globals": "29.7.0",
"@misskey-dev/eslint-plugin": "1.0.0", "@nestjs/platform-express": "10.3.10",
"@nestjs/platform-express": "10.3.8",
"@simplewebauthn/types": "10.0.0", "@simplewebauthn/types": "10.0.0",
"@swc/jest": "0.2.36", "@swc/jest": "0.2.36",
"@types/accepts": "1.3.7", "@types/accepts": "1.3.7",
@ -198,21 +197,21 @@
"@types/color-convert": "2.0.3", "@types/color-convert": "2.0.3",
"@types/content-disposition": "0.5.8", "@types/content-disposition": "0.5.8",
"@types/fluent-ffmpeg": "2.1.24", "@types/fluent-ffmpeg": "2.1.24",
"@types/htmlescape": "^1.1.3", "@types/htmlescape": "1.1.3",
"@types/http-link-header": "1.0.5", "@types/http-link-header": "1.0.7",
"@types/jest": "29.5.12", "@types/jest": "29.5.12",
"@types/js-yaml": "4.0.9", "@types/js-yaml": "4.0.9",
"@types/jsdom": "21.1.6", "@types/jsdom": "21.1.7",
"@types/jsonld": "1.5.13", "@types/jsonld": "1.5.14",
"@types/jsrsasign": "10.5.14", "@types/jsrsasign": "10.5.14",
"@types/mime-types": "2.1.4", "@types/mime-types": "2.1.4",
"@types/ms": "0.7.34", "@types/ms": "0.7.34",
"@types/node": "20.12.7", "@types/node": "20.14.9",
"@types/nodemailer": "6.4.15", "@types/nodemailer": "6.4.15",
"@types/oauth": "0.9.4", "@types/oauth": "0.9.5",
"@types/oauth2orize": "1.11.5", "@types/oauth2orize": "1.11.5",
"@types/oauth2orize-pkce": "0.1.2", "@types/oauth2orize-pkce": "0.1.2",
"@types/pg": "8.11.5", "@types/pg": "8.11.6",
"@types/pug": "2.0.10", "@types/pug": "2.0.10",
"@types/punycode": "2.1.4", "@types/punycode": "2.1.4",
"@types/qrcode": "1.5.5", "@types/qrcode": "1.5.5",
@ -228,18 +227,17 @@
"@types/vary": "1.1.3", "@types/vary": "1.1.3",
"@types/web-push": "3.6.3", "@types/web-push": "3.6.3",
"@types/ws": "8.5.10", "@types/ws": "8.5.10",
"@typescript-eslint/eslint-plugin": "7.7.1", "@typescript-eslint/eslint-plugin": "7.15.0",
"@typescript-eslint/parser": "7.7.1", "@typescript-eslint/parser": "7.15.0",
"aws-sdk-client-mock": "3.0.1", "aws-sdk-client-mock": "4.0.1",
"cross-env": "7.0.3", "cross-env": "7.0.3",
"eslint": "8.57.0",
"eslint-plugin-import": "2.29.1", "eslint-plugin-import": "2.29.1",
"execa": "8.0.1", "execa": "9.2.0",
"fkill": "^9.0.0", "fkill": "9.0.0",
"jest": "29.7.0", "jest": "29.7.0",
"jest-mock": "29.7.0", "jest-mock": "29.7.0",
"nodemon": "3.1.0", "nodemon": "3.1.4",
"pid-port": "1.0.0", "pid-port": "1.0.0",
"simple-oauth2": "5.0.0" "simple-oauth2": "5.0.1"
} }
} }

View File

@ -30,6 +30,7 @@ function execStart() {
async function killProc() { async function killProc() {
if (backendProcess) { if (backendProcess) {
backendProcess.catch(() => {}); // backendProcess.kill()によって発生する例外を無視するためにcatch()を呼び出す
backendProcess.kill(); backendProcess.kill();
await new Promise(resolve => backendProcess.on('exit', resolve)); await new Promise(resolve => backendProcess.on('exit', resolve));
backendProcess = undefined; backendProcess = undefined;
@ -46,6 +47,7 @@ async function killProc() {
], ],
{ {
stdio: [process.stdin, process.stdout, process.stderr, 'ipc'], stdio: [process.stdin, process.stdout, process.stderr, 'ipc'],
serialization: "json",
}) })
.on('message', async (message) => { .on('message', async (message) => {
if (message.type === 'exit') { if (message.type === 'exit') {

View File

@ -13,10 +13,12 @@ import { intersperse } from '@/misc/prelude/array.js';
import { normalizeForSearch } from '@/misc/normalize-for-search.js'; import { normalizeForSearch } from '@/misc/normalize-for-search.js';
import type { IMentionedRemoteUsers } from '@/models/Note.js'; import type { IMentionedRemoteUsers } from '@/models/Note.js';
import { bindThis } from '@/decorators.js'; import { bindThis } from '@/decorators.js';
import * as TreeAdapter from '../../node_modules/parse5/dist/tree-adapters/default.js'; import type { DefaultTreeAdapterMap } from 'parse5';
import type * as mfm from 'mfm-js'; import type * as mfm from 'mfm-js';
const treeAdapter = TreeAdapter.defaultTreeAdapter; const treeAdapter = parse5.defaultTreeAdapter;
type Node = DefaultTreeAdapterMap['node'];
type ChildNode = DefaultTreeAdapterMap['childNode'];
const urlRegex = /^https?:\/\/[\w\/:%#@$&?!()\[\]~.,=+\-]+/; const urlRegex = /^https?:\/\/[\w\/:%#@$&?!()\[\]~.,=+\-]+/;
const urlRegexFull = /^https?:\/\/[\w\/:%#@$&?!()\[\]~.,=+\-]+$/; const urlRegexFull = /^https?:\/\/[\w\/:%#@$&?!()\[\]~.,=+\-]+$/;
@ -46,7 +48,7 @@ export class MfmService {
return text.trim(); return text.trim();
function getText(node: TreeAdapter.Node): string { function getText(node: Node): string {
if (treeAdapter.isTextNode(node)) return node.value; if (treeAdapter.isTextNode(node)) return node.value;
if (!treeAdapter.isElementNode(node)) return ''; if (!treeAdapter.isElementNode(node)) return '';
if (node.nodeName === 'br') return '\n'; if (node.nodeName === 'br') return '\n';
@ -58,7 +60,7 @@ export class MfmService {
return ''; return '';
} }
function appendChildren(childNodes: TreeAdapter.ChildNode[]): void { function appendChildren(childNodes: ChildNode[]): void {
if (childNodes) { if (childNodes) {
for (const n of childNodes) { for (const n of childNodes) {
analyze(n); analyze(n);
@ -66,14 +68,16 @@ export class MfmService {
} }
} }
function analyze(node: TreeAdapter.Node) { function analyze(node: Node) {
if (treeAdapter.isTextNode(node)) { if (treeAdapter.isTextNode(node)) {
text += node.value; text += node.value;
return; return;
} }
// Skip comment or document type node // Skip comment or document type node
if (!treeAdapter.isElementNode(node)) return; if (!treeAdapter.isElementNode(node)) {
return;
}
switch (node.nodeName) { switch (node.nodeName) {
case 'br': { case 'br': {
@ -81,8 +85,7 @@ export class MfmService {
break; break;
} }
case 'a': case 'a': {
{
const txt = getText(node); const txt = getText(node);
const rel = node.attrs.find(x => x.name === 'rel'); const rel = node.attrs.find(x => x.name === 'rel');
const href = node.attrs.find(x => x.name === 'href'); const href = node.attrs.find(x => x.name === 'href');
@ -90,7 +93,7 @@ export class MfmService {
// ハッシュタグ // ハッシュタグ
if (normalizedHashtagNames && href && normalizedHashtagNames.has(normalizeForSearch(txt))) { if (normalizedHashtagNames && href && normalizedHashtagNames.has(normalizeForSearch(txt))) {
text += txt; text += txt;
// メンション // メンション
} else if (txt.startsWith('@') && !(rel && rel.value.startsWith('me '))) { } else if (txt.startsWith('@') && !(rel && rel.value.startsWith('me '))) {
const part = txt.split('@'); const part = txt.split('@');
@ -102,7 +105,7 @@ export class MfmService {
} else if (part.length === 3) { } else if (part.length === 3) {
text += txt; text += txt;
} }
// その他 // その他
} else { } else {
const generateLink = () => { const generateLink = () => {
if (!href && !txt) { if (!href && !txt) {
@ -130,8 +133,7 @@ export class MfmService {
break; break;
} }
case 'h1': case 'h1': {
{
text += '【'; text += '【';
appendChildren(node.childNodes); appendChildren(node.childNodes);
text += '】\n'; text += '】\n';
@ -139,16 +141,14 @@ export class MfmService {
} }
case 'b': case 'b':
case 'strong': case 'strong': {
{
text += '**'; text += '**';
appendChildren(node.childNodes); appendChildren(node.childNodes);
text += '**'; text += '**';
break; break;
} }
case 'small': case 'small': {
{
text += '<small>'; text += '<small>';
appendChildren(node.childNodes); appendChildren(node.childNodes);
text += '</small>'; text += '</small>';
@ -156,8 +156,7 @@ export class MfmService {
} }
case 's': case 's':
case 'del': case 'del': {
{
text += '~~'; text += '~~';
appendChildren(node.childNodes); appendChildren(node.childNodes);
text += '~~'; text += '~~';
@ -165,8 +164,7 @@ export class MfmService {
} }
case 'i': case 'i':
case 'em': case 'em': {
{
text += '<i>'; text += '<i>';
appendChildren(node.childNodes); appendChildren(node.childNodes);
text += '</i>'; text += '</i>';
@ -207,8 +205,7 @@ export class MfmService {
case 'h3': case 'h3':
case 'h4': case 'h4':
case 'h5': case 'h5':
case 'h6': case 'h6': {
{
text += '\n\n'; text += '\n\n';
appendChildren(node.childNodes); appendChildren(node.childNodes);
break; break;
@ -221,8 +218,7 @@ export class MfmService {
case 'article': case 'article':
case 'li': case 'li':
case 'dt': case 'dt':
case 'dd': case 'dd': {
{
text += '\n'; text += '\n';
appendChildren(node.childNodes); appendChildren(node.childNodes);
break; break;

View File

@ -20,7 +20,7 @@ export const packedDriveFileSchema = {
name: { name: {
type: 'string', type: 'string',
optional: false, nullable: false, optional: false, nullable: false,
example: 'lenna.jpg', example: '192.jpg',
}, },
type: { type: {
type: 'string', type: 'string',

View File

@ -61,7 +61,7 @@ export const meta = {
name: { name: {
type: 'string', type: 'string',
optional: false, nullable: false, optional: false, nullable: false,
example: 'lenna.jpg', example: '192.jpg',
}, },
type: { type: {
type: 'string', type: 'string',

View File

@ -257,7 +257,14 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
const profile = await this.userProfilesRepository.findOneByOrFail({ userId: user.id }); const profile = await this.userProfilesRepository.findOneByOrFail({ userId: user.id });
if (ps.name !== undefined) updates.name = ps.name; if (ps.name !== undefined) {
if (ps.name === null) {
updates.name = null;
} else {
const trimmedName = ps.name.trim();
updates.name = trimmedName === '' ? null : trimmedName;
}
}
if (ps.description !== undefined) profileUpdates.description = ps.description; if (ps.description !== undefined) profileUpdates.description = ps.description;
if (ps.lang !== undefined) profileUpdates.lang = ps.lang; if (ps.lang !== undefined) profileUpdates.lang = ps.lang;
if (ps.location !== undefined) profileUpdates.location = ps.location; if (ps.location !== undefined) profileUpdates.location = ps.location;

View File

@ -29,7 +29,8 @@
let forceError = localStorage.getItem('forceError'); let forceError = localStorage.getItem('forceError');
if (forceError != null) { if (forceError != null) {
renderError('FORCED_ERROR', 'This error is forced by having forceError in local storage.') renderError('FORCED_ERROR', 'This error is forced by having forceError in local storage.');
return;
} }
//#region Detect language & fetch translations //#region Detect language & fetch translations
@ -155,7 +156,12 @@
document.head.appendChild(css); document.head.appendChild(css);
} }
function renderError(code, details) { async function renderError(code, details) {
// Cannot set property 'innerHTML' of null を回避
if (document.readyState === 'loading') {
await new Promise(resolve => window.addEventListener('DOMContentLoaded', resolve));
}
let errorsElement = document.getElementById('errors'); let errorsElement = document.getElementById('errors');
if (!errorsElement) { if (!errorsElement) {
@ -314,6 +320,6 @@
#errorInfo { #errorInfo {
width: 50%; width: 50%;
} }
`) }`)
} }
})(); })();

View File

@ -1,32 +0,0 @@
module.exports = {
parserOptions: {
tsconfigRootDir: __dirname,
project: ['./tsconfig.json'],
},
extends: [
'../../shared/.eslintrc.js',
],
rules: {
'import/order': ['warn', {
'groups': ['builtin', 'external', 'internal', 'parent', 'sibling', 'index', 'object', 'type'],
'pathGroups': [
{
'pattern': '@/**',
'group': 'external',
'position': 'after'
}
],
}],
'no-restricted-globals': [
'error',
{
'name': '__dirname',
'message': 'Not in ESModule. Use `import.meta.url` instead.'
},
{
'name': '__filename',
'message': 'Not in ESModule. Use `import.meta.url` instead.'
}
]
},
};

View File

@ -0,0 +1,43 @@
import tsParser from '@typescript-eslint/parser';
import sharedConfig from '../../shared/eslint.config.js';
export default [
...sharedConfig,
{
files: ['**/*.ts', '**/*.tsx'],
languageOptions: {
parserOptions: {
parser: tsParser,
project: ['./tsconfig.json'],
sourceType: 'module',
tsconfigRootDir: import.meta.dirname,
},
},
rules: {
'import/order': ['warn', {
groups: [
'builtin',
'external',
'internal',
'parent',
'sibling',
'index',
'object',
'type',
],
pathGroups: [{
pattern: '@/**',
group: 'external',
position: 'after',
}],
}],
'no-restricted-globals': ['error', {
name: '__dirname',
message: 'Not in ESModule. Use `import.meta.url` instead.',
}, {
name: '__filename',
message: 'Not in ESModule. Use `import.meta.url` instead.',
}],
},
},
];

View File

@ -1,11 +0,0 @@
module.exports = {
parserOptions: {
tsconfigRootDir: __dirname,
project: ['./tsconfig.json'],
},
extends: ['../.eslintrc.cjs'],
env: {
node: true,
jest: true,
},
};

View File

@ -23,7 +23,7 @@ describe('Drive', () => {
const marker = Math.random().toString(); const marker = Math.random().toString();
const url = 'https://raw.githubusercontent.com/misskey-dev/misskey/develop/packages/backend/test/resources/Lenna.jpg'; const url = 'https://raw.githubusercontent.com/misskey-dev/misskey/develop/packages/backend/test/resources/192.jpg';
const catcher = makeStreamCatcher( const catcher = makeStreamCatcher(
alice, alice,
@ -41,14 +41,14 @@ describe('Drive', () => {
const file = await catcher; const file = await catcher;
assert.strictEqual(res.status, 204); assert.strictEqual(res.status, 204);
assert.strictEqual(file.name, 'Lenna.jpg'); assert.strictEqual(file.name, '192.jpg');
assert.strictEqual(file.type, 'image/jpeg'); assert.strictEqual(file.type, 'image/jpeg');
}); });
test('ローカルからアップロードできる', async () => { test('ローカルからアップロードできる', async () => {
// APIレスポンスを直接使用するので utils.js uploadFile が通過することで成功とする // APIレスポンスを直接使用するので utils.js uploadFile が通過することで成功とする
const res = await uploadFile(alice, { path: 'Lenna.jpg', name: 'テスト画像' }); const res = await uploadFile(alice, { path: '192.jpg', name: 'テスト画像' });
assert.strictEqual(res.body?.name, 'テスト画像.jpg'); assert.strictEqual(res.body?.name, 'テスト画像.jpg');
assert.strictEqual(res.body.type, 'image/jpeg'); assert.strictEqual(res.body.type, 'image/jpeg');

View File

@ -117,12 +117,21 @@ describe('Endpoints', () => {
assert.strictEqual(res.body.birthday, myBirthday); assert.strictEqual(res.body.birthday, myBirthday);
}); });
test('名前を空白にできる', async () => { test('名前を空白のみにした場合nullになる', async () => {
const res = await api('i/update', { const res = await api('i/update', {
name: ' ', name: ' ',
}, alice); }, alice);
assert.strictEqual(res.status, 200); assert.strictEqual(res.status, 200);
assert.strictEqual(res.body.name, ' '); assert.strictEqual(res.body.name, null);
});
test('名前の前後に空白(ホワイトスペース)を入れてもトリムされる', async () => {
const res = await api('i/update', {
// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Lexical_grammar#white_space
name: ' あ い う \u0009\u000b\u000c\u0020\u00a0\u1680\u2000\u2001\u2002\u2003\u2004\u2005\u2006\u2007\u2008\u2009\u200a\u202f\u205f\u3000\ufeff',
}, alice);
assert.strictEqual(res.status, 200);
assert.strictEqual(res.body.name, 'あ い う');
}); });
test('誕生日の設定を削除できる', async () => { test('誕生日の設定を削除できる', async () => {
@ -584,7 +593,7 @@ describe('Endpoints', () => {
assert.strictEqual(res.status, 200); assert.strictEqual(res.status, 200);
assert.strictEqual(typeof res.body === 'object' && !Array.isArray(res.body), true); assert.strictEqual(typeof res.body === 'object' && !Array.isArray(res.body), true);
assert.strictEqual(res.body!.name, 'Lenna.jpg'); assert.strictEqual(res.body!.name, '192.jpg');
}); });
test('ファイルに名前を付けられる', async () => { test('ファイルに名前を付けられる', async () => {

View File

@ -7,12 +7,13 @@ import { INestApplicationContext } from '@nestjs/common';
process.env.NODE_ENV = 'test'; process.env.NODE_ENV = 'test';
import { setTimeout } from 'node:timers/promises';
import * as assert from 'assert'; import * as assert from 'assert';
import { loadConfig } from '@/config.js'; import { loadConfig } from '@/config.js';
import { MiRepository, MiUser, UsersRepository, miRepository } from '@/models/_.js'; import { MiRepository, MiUser, UsersRepository, miRepository } from '@/models/_.js';
import { secureRndstr } from '@/misc/secure-rndstr.js'; import { secureRndstr } from '@/misc/secure-rndstr.js';
import { jobQueue } from '@/boot/common.js'; import { jobQueue } from '@/boot/common.js';
import { api, initTestDb, signup, sleep, successfulApiCall, uploadFile } from '../utils.js'; import { api, initTestDb, signup, successfulApiCall, uploadFile } from '../utils.js';
import type * as misskey from 'misskey-js'; import type * as misskey from 'misskey-js';
describe('Account Move', () => { describe('Account Move', () => {
@ -271,7 +272,7 @@ describe('Account Move', () => {
assert.strictEqual(move.status, 200); assert.strictEqual(move.status, 200);
await sleep(1000 * 3); // wait for jobs to finish await setTimeout(1000 * 3); // wait for jobs to finish
// Unfollow delayed? // Unfollow delayed?
const aliceFollowings = await api('users/following', { const aliceFollowings = await api('users/following', {
@ -330,7 +331,7 @@ describe('Account Move', () => {
}); });
test('Unfollowed after 10 sec (24 hours in production).', async () => { test('Unfollowed after 10 sec (24 hours in production).', async () => {
await sleep(1000 * 8); await setTimeout(1000 * 8);
const following = await api('users/following', { const following = await api('users/following', {
userId: alice.id, userId: alice.id,

View File

@ -41,7 +41,7 @@ describe('Note', () => {
}); });
test('ファイルを添付できる', async () => { test('ファイルを添付できる', async () => {
const file = await uploadUrl(alice, 'https://raw.githubusercontent.com/misskey-dev/misskey/develop/packages/backend/test/resources/Lenna.jpg'); const file = await uploadUrl(alice, 'https://raw.githubusercontent.com/misskey-dev/misskey/develop/packages/backend/test/resources/192.jpg');
const res = await api('notes/create', { const res = await api('notes/create', {
fileIds: [file.id], fileIds: [file.id],
@ -53,7 +53,7 @@ describe('Note', () => {
}, 1000 * 10); }, 1000 * 10);
test('他人のファイルで怒られる', async () => { test('他人のファイルで怒られる', async () => {
const file = await uploadUrl(bob, 'https://raw.githubusercontent.com/misskey-dev/misskey/develop/packages/backend/test/resources/Lenna.jpg'); const file = await uploadUrl(bob, 'https://raw.githubusercontent.com/misskey-dev/misskey/develop/packages/backend/test/resources/192.jpg');
const res = await api('notes/create', { const res = await api('notes/create', {
text: 'test', text: 'test',

View File

@ -6,7 +6,8 @@
process.env.NODE_ENV = 'test'; process.env.NODE_ENV = 'test';
import * as assert from 'assert'; import * as assert from 'assert';
import { api, post, signup, sleep, waitFire } from '../utils.js'; import { setTimeout } from 'node:timers/promises';
import { api, post, signup, waitFire } from '../utils.js';
import type * as misskey from 'misskey-js'; import type * as misskey from 'misskey-js';
describe('Renote Mute', () => { describe('Renote Mute', () => {
@ -35,7 +36,7 @@ describe('Renote Mute', () => {
const carolNote = await post(carol, { text: 'hi' }); const carolNote = await post(carol, { text: 'hi' });
// redisに追加されるのを待つ // redisに追加されるのを待つ
await sleep(100); await setTimeout(100);
const res = await api('notes/local-timeline', {}, alice); const res = await api('notes/local-timeline', {}, alice);
@ -52,7 +53,7 @@ describe('Renote Mute', () => {
const carolNote = await post(carol, { text: 'hi' }); const carolNote = await post(carol, { text: 'hi' });
// redisに追加されるのを待つ // redisに追加されるのを待つ
await sleep(100); await setTimeout(100);
const res = await api('notes/local-timeline', {}, alice); const res = await api('notes/local-timeline', {}, alice);
@ -69,7 +70,7 @@ describe('Renote Mute', () => {
const bobRenote = await post(bob, { renoteId: carolNote.id }); const bobRenote = await post(bob, { renoteId: carolNote.id });
// redisに追加されるのを待つ // redisに追加されるのを待つ
await sleep(100); await setTimeout(100);
const res = await api('notes/local-timeline', {}, alice); const res = await api('notes/local-timeline', {}, alice);

View File

@ -7,16 +7,17 @@
// pnpm jest -- e2e/timelines.ts // pnpm jest -- e2e/timelines.ts
import * as assert from 'assert'; import * as assert from 'assert';
import { setTimeout } from 'node:timers/promises';
import { Redis } from 'ioredis'; import { Redis } from 'ioredis';
import { loadConfig } from '@/config.js'; import { loadConfig } from '@/config.js';
import { api, post, randomString, sendEnvUpdateRequest, signup, sleep, uploadUrl } from '../utils.js'; import { api, post, randomString, sendEnvUpdateRequest, signup, uploadUrl } from '../utils.js';
function genHost() { function genHost() {
return randomString() + '.example.com'; return randomString() + '.example.com';
} }
function waitForPushToTl() { function waitForPushToTl() {
return sleep(500); return setTimeout(500);
} }
let redisForTimelines: Redis; let redisForTimelines: Redis;
@ -44,7 +45,7 @@ describe('Timelines', () => {
const [alice, bob, carol] = await Promise.all([signup(), signup(), signup()]); const [alice, bob, carol] = await Promise.all([signup(), signup(), signup()]);
await api('following/create', { userId: bob.id }, alice); await api('following/create', { userId: bob.id }, alice);
await sleep(1000); await setTimeout(1000);
const bobNote = await post(bob, { text: 'hi' }); const bobNote = await post(bob, { text: 'hi' });
const carolNote = await post(carol, { text: 'hi' }); const carolNote = await post(carol, { text: 'hi' });
@ -60,7 +61,7 @@ describe('Timelines', () => {
const [alice, bob, carol] = await Promise.all([signup(), signup(), signup()]); const [alice, bob, carol] = await Promise.all([signup(), signup(), signup()]);
await api('following/create', { userId: bob.id }, alice); await api('following/create', { userId: bob.id }, alice);
await sleep(1000); await setTimeout(1000);
const bobNote = await post(bob, { text: 'hi', visibility: 'followers' }); const bobNote = await post(bob, { text: 'hi', visibility: 'followers' });
const carolNote = await post(carol, { text: 'hi' }); const carolNote = await post(carol, { text: 'hi' });
@ -77,7 +78,7 @@ describe('Timelines', () => {
const [alice, bob, carol] = await Promise.all([signup(), signup(), signup()]); const [alice, bob, carol] = await Promise.all([signup(), signup(), signup()]);
await api('following/create', { userId: bob.id }, alice); await api('following/create', { userId: bob.id }, alice);
await sleep(1000); await setTimeout(1000);
const carolNote = await post(carol, { text: 'hi' }); const carolNote = await post(carol, { text: 'hi' });
const bobNote = await post(bob, { text: 'hi', replyId: carolNote.id }); const bobNote = await post(bob, { text: 'hi', replyId: carolNote.id });
@ -94,7 +95,7 @@ describe('Timelines', () => {
await api('following/create', { userId: bob.id }, alice); await api('following/create', { userId: bob.id }, alice);
await api('following/update', { userId: bob.id, withReplies: true }, alice); await api('following/update', { userId: bob.id, withReplies: true }, alice);
await sleep(1000); await setTimeout(1000);
const carolNote = await post(carol, { text: 'hi' }); const carolNote = await post(carol, { text: 'hi' });
const bobNote = await post(bob, { text: 'hi', replyId: carolNote.id }); const bobNote = await post(bob, { text: 'hi', replyId: carolNote.id });
@ -111,7 +112,7 @@ describe('Timelines', () => {
await api('following/create', { userId: bob.id }, alice); await api('following/create', { userId: bob.id }, alice);
await api('following/update', { userId: bob.id, withReplies: true }, alice); await api('following/update', { userId: bob.id, withReplies: true }, alice);
await sleep(1000); await setTimeout(1000);
const carolNote = await post(carol, { text: 'hi' }); const carolNote = await post(carol, { text: 'hi' });
const bobNote = await post(bob, { text: 'hi', replyId: carolNote.id, visibility: 'specified', visibleUserIds: [carolNote.id] }); const bobNote = await post(bob, { text: 'hi', replyId: carolNote.id, visibility: 'specified', visibleUserIds: [carolNote.id] });
@ -128,7 +129,7 @@ describe('Timelines', () => {
await api('following/create', { userId: bob.id }, alice); await api('following/create', { userId: bob.id }, alice);
await api('following/update', { userId: bob.id, withReplies: true }, alice); await api('following/update', { userId: bob.id, withReplies: true }, alice);
await sleep(1000); await setTimeout(1000);
const carolNote = await post(carol, { text: 'hi', visibility: 'followers' }); const carolNote = await post(carol, { text: 'hi', visibility: 'followers' });
const bobNote = await post(bob, { text: 'hi', replyId: carolNote.id }); const bobNote = await post(bob, { text: 'hi', replyId: carolNote.id });
@ -147,7 +148,7 @@ describe('Timelines', () => {
await api('following/create', { userId: carol.id }, alice); await api('following/create', { userId: carol.id }, alice);
await api('following/create', { userId: carol.id }, bob); await api('following/create', { userId: carol.id }, bob);
await api('following/update', { userId: bob.id, withReplies: true }, alice); await api('following/update', { userId: bob.id, withReplies: true }, alice);
await sleep(1000); await setTimeout(1000);
const carolNote = await post(carol, { text: 'hi', visibility: 'followers' }); const carolNote = await post(carol, { text: 'hi', visibility: 'followers' });
const bobNote = await post(bob, { text: 'hi', replyId: carolNote.id }); const bobNote = await post(bob, { text: 'hi', replyId: carolNote.id });
@ -166,7 +167,7 @@ describe('Timelines', () => {
await api('following/create', { userId: bob.id }, alice); await api('following/create', { userId: bob.id }, alice);
await api('following/create', { userId: carol.id }, alice); await api('following/create', { userId: carol.id }, alice);
await api('following/update', { userId: bob.id, withReplies: true }, alice); await api('following/update', { userId: bob.id, withReplies: true }, alice);
await sleep(1000); await setTimeout(1000);
const carolNote = await post(carol, { text: 'hi' }); const carolNote = await post(carol, { text: 'hi' });
const bobNote = await post(bob, { text: 'hi', replyId: carolNote.id, visibility: 'specified', visibleUserIds: [carolNote.id] }); const bobNote = await post(bob, { text: 'hi', replyId: carolNote.id, visibility: 'specified', visibleUserIds: [carolNote.id] });
@ -182,7 +183,7 @@ describe('Timelines', () => {
const [alice, bob] = await Promise.all([signup(), signup()]); const [alice, bob] = await Promise.all([signup(), signup()]);
await api('following/create', { userId: bob.id }, alice); await api('following/create', { userId: bob.id }, alice);
await sleep(1000); await setTimeout(1000);
const bobNote1 = await post(bob, { text: 'hi' }); const bobNote1 = await post(bob, { text: 'hi' });
const bobNote2 = await post(bob, { text: 'hi', replyId: bobNote1.id }); const bobNote2 = await post(bob, { text: 'hi', replyId: bobNote1.id });
@ -198,7 +199,7 @@ describe('Timelines', () => {
const [alice, bob] = await Promise.all([signup(), signup()]); const [alice, bob] = await Promise.all([signup(), signup()]);
await api('following/create', { userId: bob.id }, alice); await api('following/create', { userId: bob.id }, alice);
await sleep(1000); await setTimeout(1000);
const aliceNote = await post(alice, { text: 'hi' }); const aliceNote = await post(alice, { text: 'hi' });
const bobNote = await post(bob, { text: 'hi', replyId: aliceNote.id }); const bobNote = await post(bob, { text: 'hi', replyId: aliceNote.id });
@ -228,7 +229,7 @@ describe('Timelines', () => {
const [alice, bob, carol] = await Promise.all([signup(), signup(), signup()]); const [alice, bob, carol] = await Promise.all([signup(), signup(), signup()]);
await api('following/create', { userId: bob.id }, alice); await api('following/create', { userId: bob.id }, alice);
await sleep(1000); await setTimeout(1000);
const carolNote = await post(carol, { text: 'hi' }); const carolNote = await post(carol, { text: 'hi' });
const bobNote = await post(bob, { renoteId: carolNote.id }); const bobNote = await post(bob, { renoteId: carolNote.id });
@ -244,7 +245,7 @@ describe('Timelines', () => {
const [alice, bob, carol] = await Promise.all([signup(), signup(), signup()]); const [alice, bob, carol] = await Promise.all([signup(), signup(), signup()]);
await api('following/create', { userId: bob.id }, alice); await api('following/create', { userId: bob.id }, alice);
await sleep(1000); await setTimeout(1000);
const carolNote = await post(carol, { text: 'hi' }); const carolNote = await post(carol, { text: 'hi' });
const bobNote = await post(bob, { renoteId: carolNote.id }); const bobNote = await post(bob, { renoteId: carolNote.id });
@ -262,7 +263,7 @@ describe('Timelines', () => {
const [alice, bob, carol] = await Promise.all([signup(), signup(), signup()]); const [alice, bob, carol] = await Promise.all([signup(), signup(), signup()]);
await api('following/create', { userId: bob.id }, alice); await api('following/create', { userId: bob.id }, alice);
await sleep(1000); await setTimeout(1000);
const carolNote = await post(carol, { text: 'hi' }); const carolNote = await post(carol, { text: 'hi' });
const bobNote = await post(bob, { text: 'hi', renoteId: carolNote.id }); const bobNote = await post(bob, { text: 'hi', renoteId: carolNote.id });
@ -280,7 +281,7 @@ describe('Timelines', () => {
const [alice, bob, carol] = await Promise.all([signup(), signup(), signup()]); const [alice, bob, carol] = await Promise.all([signup(), signup(), signup()]);
await api('following/create', { userId: bob.id }, alice); await api('following/create', { userId: bob.id }, alice);
await sleep(1000); await setTimeout(1000);
const bobNote = await post(bob, { text: 'hi', visibility: 'specified', visibleUserIds: [carol.id] }); const bobNote = await post(bob, { text: 'hi', visibility: 'specified', visibleUserIds: [carol.id] });
await waitForPushToTl(); await waitForPushToTl();
@ -295,7 +296,7 @@ describe('Timelines', () => {
await api('following/create', { userId: bob.id }, alice); await api('following/create', { userId: bob.id }, alice);
await api('mute/create', { userId: carol.id }, alice); await api('mute/create', { userId: carol.id }, alice);
await sleep(1000); await setTimeout(1000);
const carolNote = await post(carol, { text: 'hi' }); const carolNote = await post(carol, { text: 'hi' });
const bobNote = await post(bob, { text: 'hi', renoteId: carolNote.id }); const bobNote = await post(bob, { text: 'hi', renoteId: carolNote.id });
@ -313,7 +314,7 @@ describe('Timelines', () => {
await api('following/create', { userId: bob.id }, alice); await api('following/create', { userId: bob.id }, alice);
await api('following/update', { userId: bob.id, withReplies: true }, alice); await api('following/update', { userId: bob.id, withReplies: true }, alice);
await api('mute/create', { userId: carol.id }, alice); await api('mute/create', { userId: carol.id }, alice);
await sleep(1000); await setTimeout(1000);
const carolNote = await post(carol, { text: 'hi' }); const carolNote = await post(carol, { text: 'hi' });
const bobNote = await post(bob, { text: 'hi', replyId: carolNote.id }); const bobNote = await post(bob, { text: 'hi', replyId: carolNote.id });
@ -359,7 +360,7 @@ describe('Timelines', () => {
const [alice, bob, carol] = await Promise.all([signup(), signup(), signup()]); const [alice, bob, carol] = await Promise.all([signup(), signup(), signup()]);
await api('following/create', { userId: bob.id }, alice); await api('following/create', { userId: bob.id }, alice);
await sleep(1000); await setTimeout(1000);
const [bobFile, carolFile] = await Promise.all([ const [bobFile, carolFile] = await Promise.all([
uploadUrl(bob, 'https://raw.githubusercontent.com/misskey-dev/assets/main/public/icon.png'), uploadUrl(bob, 'https://raw.githubusercontent.com/misskey-dev/assets/main/public/icon.png'),
uploadUrl(carol, 'https://raw.githubusercontent.com/misskey-dev/assets/main/public/icon.png'), uploadUrl(carol, 'https://raw.githubusercontent.com/misskey-dev/assets/main/public/icon.png'),
@ -384,7 +385,7 @@ describe('Timelines', () => {
const channel = await api('channels/create', { name: 'channel' }, bob).then(x => x.body); const channel = await api('channels/create', { name: 'channel' }, bob).then(x => x.body);
await api('following/create', { userId: bob.id }, alice); await api('following/create', { userId: bob.id }, alice);
await sleep(1000); await setTimeout(1000);
const bobNote = await post(bob, { text: 'hi', channelId: channel.id }); const bobNote = await post(bob, { text: 'hi', channelId: channel.id });
await waitForPushToTl(); await waitForPushToTl();
@ -411,7 +412,7 @@ describe('Timelines', () => {
const [alice, bob] = await Promise.all([signup(), signup()]); const [alice, bob] = await Promise.all([signup(), signup()]);
await api('following/create', { userId: bob.id }, alice); await api('following/create', { userId: bob.id }, alice);
await sleep(1000); await setTimeout(1000);
const bobNote = await post(bob, { text: 'hi', visibility: 'specified', visibleUserIds: [alice.id] }); const bobNote = await post(bob, { text: 'hi', visibility: 'specified', visibleUserIds: [alice.id] });
await waitForPushToTl(); await waitForPushToTl();
@ -438,7 +439,7 @@ describe('Timelines', () => {
const [alice, bob, carol] = await Promise.all([signup(), signup(), signup()]); const [alice, bob, carol] = await Promise.all([signup(), signup(), signup()]);
await api('following/create', { userId: bob.id }, alice); await api('following/create', { userId: bob.id }, alice);
await sleep(1000); await setTimeout(1000);
const bobNote = await post(bob, { text: 'hi', visibility: 'specified', visibleUserIds: [carol.id] }); const bobNote = await post(bob, { text: 'hi', visibility: 'specified', visibleUserIds: [carol.id] });
await waitForPushToTl(); await waitForPushToTl();
@ -566,7 +567,7 @@ describe('Timelines', () => {
const [alice, bob, carol] = await Promise.all([signup(), signup(), signup()]); const [alice, bob, carol] = await Promise.all([signup(), signup(), signup()]);
await api('following/create', { userId: carol.id }, alice); await api('following/create', { userId: carol.id }, alice);
await sleep(1000); await setTimeout(1000);
const carolNote = await post(carol, { text: 'hi', visibility: 'home' }); const carolNote = await post(carol, { text: 'hi', visibility: 'home' });
const bobNote = await post(bob, { text: 'hi' }); const bobNote = await post(bob, { text: 'hi' });
@ -582,7 +583,7 @@ describe('Timelines', () => {
const [alice, bob, carol] = await Promise.all([signup(), signup(), signup()]); const [alice, bob, carol] = await Promise.all([signup(), signup(), signup()]);
await api('mute/create', { userId: carol.id }, alice); await api('mute/create', { userId: carol.id }, alice);
await sleep(1000); await setTimeout(1000);
const carolNote = await post(carol, { text: 'hi' }); const carolNote = await post(carol, { text: 'hi' });
const bobNote = await post(bob, { text: 'hi' }); const bobNote = await post(bob, { text: 'hi' });
@ -599,7 +600,7 @@ describe('Timelines', () => {
await api('following/create', { userId: bob.id }, alice); await api('following/create', { userId: bob.id }, alice);
await api('mute/create', { userId: carol.id }, alice); await api('mute/create', { userId: carol.id }, alice);
await sleep(1000); await setTimeout(1000);
const carolNote = await post(carol, { text: 'hi' }); const carolNote = await post(carol, { text: 'hi' });
const bobNote = await post(bob, { text: 'hi', renoteId: carolNote.id }); const bobNote = await post(bob, { text: 'hi', renoteId: carolNote.id });
@ -617,7 +618,7 @@ describe('Timelines', () => {
await api('following/create', { userId: bob.id }, alice); await api('following/create', { userId: bob.id }, alice);
await api('following/update', { userId: bob.id, withReplies: true }, alice); await api('following/update', { userId: bob.id, withReplies: true }, alice);
await api('mute/create', { userId: carol.id }, alice); await api('mute/create', { userId: carol.id }, alice);
await sleep(1000); await setTimeout(1000);
const carolNote = await post(carol, { text: 'hi' }); const carolNote = await post(carol, { text: 'hi' });
const bobNote = await post(bob, { text: 'hi', replyId: carolNote.id }); const bobNote = await post(bob, { text: 'hi', replyId: carolNote.id });
@ -633,7 +634,7 @@ describe('Timelines', () => {
const [alice, bob] = await Promise.all([signup(), signup()]); const [alice, bob] = await Promise.all([signup(), signup()]);
await api('following/create', { userId: bob.id }, alice); await api('following/create', { userId: bob.id }, alice);
await sleep(1000); await setTimeout(1000);
const aliceNote = await post(alice, { text: 'hi' }); const aliceNote = await post(alice, { text: 'hi' });
const bobNote = await post(bob, { text: 'hi', replyId: aliceNote.id }); const bobNote = await post(bob, { text: 'hi', replyId: aliceNote.id });
@ -703,7 +704,7 @@ describe('Timelines', () => {
const [alice, bob] = await Promise.all([signup(), signup()]); const [alice, bob] = await Promise.all([signup(), signup()]);
await api('following/create', { userId: bob.id }, alice); await api('following/create', { userId: bob.id }, alice);
await sleep(1000); await setTimeout(1000);
const bobNote = await post(bob, { text: 'hi', visibility: 'home' }); const bobNote = await post(bob, { text: 'hi', visibility: 'home' });
await waitForPushToTl(); await waitForPushToTl();
@ -717,7 +718,7 @@ describe('Timelines', () => {
const [alice, bob] = await Promise.all([signup(), signup()]); const [alice, bob] = await Promise.all([signup(), signup()]);
await api('following/create', { userId: bob.id }, alice); await api('following/create', { userId: bob.id }, alice);
await sleep(1000); await setTimeout(1000);
const aliceNote = await post(alice, { text: 'hi' }); const aliceNote = await post(alice, { text: 'hi' });
const bobNote = await post(bob, { text: 'hi', replyId: aliceNote.id }); const bobNote = await post(bob, { text: 'hi', replyId: aliceNote.id });
@ -820,7 +821,7 @@ describe('Timelines', () => {
const list = await api('users/lists/create', { name: 'list' }, alice).then(res => res.body); const list = await api('users/lists/create', { name: 'list' }, alice).then(res => res.body);
await api('users/lists/push', { listId: list.id, userId: bob.id }, alice); await api('users/lists/push', { listId: list.id, userId: bob.id }, alice);
await sleep(1000); await setTimeout(1000);
const bobNote = await post(bob, { text: 'hi' }); const bobNote = await post(bob, { text: 'hi' });
await waitForPushToTl(); await waitForPushToTl();
@ -835,7 +836,7 @@ describe('Timelines', () => {
const list = await api('users/lists/create', { name: 'list' }, alice).then(res => res.body); const list = await api('users/lists/create', { name: 'list' }, alice).then(res => res.body);
await api('users/lists/push', { listId: list.id, userId: bob.id }, alice); await api('users/lists/push', { listId: list.id, userId: bob.id }, alice);
await sleep(1000); await setTimeout(1000);
const bobNote = await post(bob, { text: 'hi', visibility: 'home' }); const bobNote = await post(bob, { text: 'hi', visibility: 'home' });
await waitForPushToTl(); await waitForPushToTl();
@ -850,7 +851,7 @@ describe('Timelines', () => {
const list = await api('users/lists/create', { name: 'list' }, alice).then(res => res.body); const list = await api('users/lists/create', { name: 'list' }, alice).then(res => res.body);
await api('users/lists/push', { listId: list.id, userId: bob.id }, alice); await api('users/lists/push', { listId: list.id, userId: bob.id }, alice);
await sleep(1000); await setTimeout(1000);
const bobNote = await post(bob, { text: 'hi', visibility: 'followers' }); const bobNote = await post(bob, { text: 'hi', visibility: 'followers' });
await waitForPushToTl(); await waitForPushToTl();
@ -865,7 +866,7 @@ describe('Timelines', () => {
const list = await api('users/lists/create', { name: 'list' }, alice).then(res => res.body); const list = await api('users/lists/create', { name: 'list' }, alice).then(res => res.body);
await api('users/lists/push', { listId: list.id, userId: bob.id }, alice); await api('users/lists/push', { listId: list.id, userId: bob.id }, alice);
await sleep(1000); await setTimeout(1000);
const carolNote = await post(carol, { text: 'hi' }); const carolNote = await post(carol, { text: 'hi' });
const bobNote = await post(bob, { text: 'hi', replyId: carolNote.id }); const bobNote = await post(bob, { text: 'hi', replyId: carolNote.id });
@ -881,7 +882,7 @@ describe('Timelines', () => {
const list = await api('users/lists/create', { name: 'list' }, alice).then(res => res.body); const list = await api('users/lists/create', { name: 'list' }, alice).then(res => res.body);
await api('users/lists/push', { listId: list.id, userId: bob.id }, alice); await api('users/lists/push', { listId: list.id, userId: bob.id }, alice);
await sleep(1000); await setTimeout(1000);
const bobNote1 = await post(bob, { text: 'hi' }); const bobNote1 = await post(bob, { text: 'hi' });
const bobNote2 = await post(bob, { text: 'hi', replyId: bobNote1.id }); const bobNote2 = await post(bob, { text: 'hi', replyId: bobNote1.id });
@ -899,7 +900,7 @@ describe('Timelines', () => {
const list = await api('users/lists/create', { name: 'list' }, alice).then(res => res.body); const list = await api('users/lists/create', { name: 'list' }, alice).then(res => res.body);
await api('users/lists/push', { listId: list.id, userId: bob.id }, alice); await api('users/lists/push', { listId: list.id, userId: bob.id }, alice);
await api('users/lists/update-membership', { listId: list.id, userId: bob.id, withReplies: false }, alice); await api('users/lists/update-membership', { listId: list.id, userId: bob.id, withReplies: false }, alice);
await sleep(1000); await setTimeout(1000);
const aliceNote = await post(alice, { text: 'hi' }); const aliceNote = await post(alice, { text: 'hi' });
const bobNote = await post(bob, { text: 'hi', replyId: aliceNote.id }); const bobNote = await post(bob, { text: 'hi', replyId: aliceNote.id });
@ -916,7 +917,7 @@ describe('Timelines', () => {
const list = await api('users/lists/create', { name: 'list' }, alice).then(res => res.body); const list = await api('users/lists/create', { name: 'list' }, alice).then(res => res.body);
await api('users/lists/push', { listId: list.id, userId: bob.id }, alice); await api('users/lists/push', { listId: list.id, userId: bob.id }, alice);
await api('users/lists/update-membership', { listId: list.id, userId: bob.id, withReplies: false }, alice); await api('users/lists/update-membership', { listId: list.id, userId: bob.id, withReplies: false }, alice);
await sleep(1000); await setTimeout(1000);
const carolNote = await post(carol, { text: 'hi' }); const carolNote = await post(carol, { text: 'hi' });
const bobNote = await post(bob, { text: 'hi', replyId: carolNote.id }); const bobNote = await post(bob, { text: 'hi', replyId: carolNote.id });
@ -933,7 +934,7 @@ describe('Timelines', () => {
const list = await api('users/lists/create', { name: 'list' }, alice).then(res => res.body); const list = await api('users/lists/create', { name: 'list' }, alice).then(res => res.body);
await api('users/lists/push', { listId: list.id, userId: bob.id }, alice); await api('users/lists/push', { listId: list.id, userId: bob.id }, alice);
await api('users/lists/update-membership', { listId: list.id, userId: bob.id, withReplies: true }, alice); await api('users/lists/update-membership', { listId: list.id, userId: bob.id, withReplies: true }, alice);
await sleep(1000); await setTimeout(1000);
const carolNote = await post(carol, { text: 'hi' }); const carolNote = await post(carol, { text: 'hi' });
const bobNote = await post(bob, { text: 'hi', replyId: carolNote.id }); const bobNote = await post(bob, { text: 'hi', replyId: carolNote.id });
@ -950,7 +951,7 @@ describe('Timelines', () => {
await api('following/create', { userId: bob.id }, alice); await api('following/create', { userId: bob.id }, alice);
const list = await api('users/lists/create', { name: 'list' }, alice).then(res => res.body); const list = await api('users/lists/create', { name: 'list' }, alice).then(res => res.body);
await api('users/lists/push', { listId: list.id, userId: bob.id }, alice); await api('users/lists/push', { listId: list.id, userId: bob.id }, alice);
await sleep(1000); await setTimeout(1000);
const bobNote = await post(bob, { text: 'hi', visibility: 'home' }); const bobNote = await post(bob, { text: 'hi', visibility: 'home' });
await waitForPushToTl(); await waitForPushToTl();
@ -966,7 +967,7 @@ describe('Timelines', () => {
await api('following/create', { userId: bob.id }, alice); await api('following/create', { userId: bob.id }, alice);
const list = await api('users/lists/create', { name: 'list' }, alice).then(res => res.body); const list = await api('users/lists/create', { name: 'list' }, alice).then(res => res.body);
await api('users/lists/push', { listId: list.id, userId: bob.id }, alice); await api('users/lists/push', { listId: list.id, userId: bob.id }, alice);
await sleep(1000); await setTimeout(1000);
const bobNote = await post(bob, { text: 'hi', visibility: 'followers' }); const bobNote = await post(bob, { text: 'hi', visibility: 'followers' });
await waitForPushToTl(); await waitForPushToTl();
@ -982,7 +983,7 @@ describe('Timelines', () => {
const list = await api('users/lists/create', { name: 'list' }, alice).then(res => res.body); const list = await api('users/lists/create', { name: 'list' }, alice).then(res => res.body);
await api('users/lists/push', { listId: list.id, userId: alice.id }, alice); await api('users/lists/push', { listId: list.id, userId: alice.id }, alice);
await sleep(1000); await setTimeout(1000);
const aliceNote = await post(alice, { text: 'hi', visibility: 'followers' }); const aliceNote = await post(alice, { text: 'hi', visibility: 'followers' });
await waitForPushToTl(); await waitForPushToTl();
@ -999,7 +1000,7 @@ describe('Timelines', () => {
const channel = await api('channels/create', { name: 'channel' }, bob).then(x => x.body); const channel = await api('channels/create', { name: 'channel' }, bob).then(x => x.body);
const list = await api('users/lists/create', { name: 'list' }, alice).then(res => res.body); const list = await api('users/lists/create', { name: 'list' }, alice).then(res => res.body);
await api('users/lists/push', { listId: list.id, userId: bob.id }, alice); await api('users/lists/push', { listId: list.id, userId: bob.id }, alice);
await sleep(1000); await setTimeout(1000);
const bobNote = await post(bob, { text: 'hi', channelId: channel.id }); const bobNote = await post(bob, { text: 'hi', channelId: channel.id });
await waitForPushToTl(); await waitForPushToTl();
@ -1031,7 +1032,7 @@ describe('Timelines', () => {
const list = await api('users/lists/create', { name: 'list' }, alice).then(res => res.body); const list = await api('users/lists/create', { name: 'list' }, alice).then(res => res.body);
await api('users/lists/push', { listId: list.id, userId: bob.id }, alice); await api('users/lists/push', { listId: list.id, userId: bob.id }, alice);
await sleep(1000); await setTimeout(1000);
const bobNote = await post(bob, { text: 'hi', visibility: 'specified', visibleUserIds: [alice.id] }); const bobNote = await post(bob, { text: 'hi', visibility: 'specified', visibleUserIds: [alice.id] });
await waitForPushToTl(); await waitForPushToTl();
@ -1048,7 +1049,7 @@ describe('Timelines', () => {
const list = await api('users/lists/create', { name: 'list' }, alice).then(res => res.body); const list = await api('users/lists/create', { name: 'list' }, alice).then(res => res.body);
await api('users/lists/push', { listId: list.id, userId: bob.id }, alice); await api('users/lists/push', { listId: list.id, userId: bob.id }, alice);
await api('users/lists/push', { listId: list.id, userId: carol.id }, alice); await api('users/lists/push', { listId: list.id, userId: carol.id }, alice);
await sleep(1000); await setTimeout(1000);
const bobNote = await post(bob, { text: 'hi', visibility: 'specified', visibleUserIds: [carol.id] }); const bobNote = await post(bob, { text: 'hi', visibility: 'specified', visibleUserIds: [carol.id] });
await waitForPushToTl(); await waitForPushToTl();
@ -1088,7 +1089,7 @@ describe('Timelines', () => {
const [alice, bob] = await Promise.all([signup(), signup()]); const [alice, bob] = await Promise.all([signup(), signup()]);
await api('following/create', { userId: bob.id }, alice); await api('following/create', { userId: bob.id }, alice);
await sleep(1000); await setTimeout(1000);
const bobNote = await post(bob, { text: 'hi', visibility: 'followers' }); const bobNote = await post(bob, { text: 'hi', visibility: 'followers' });
await waitForPushToTl(); await waitForPushToTl();
@ -1228,7 +1229,7 @@ describe('Timelines', () => {
const [alice, bob, carol] = await Promise.all([signup(), signup(), signup()]); const [alice, bob, carol] = await Promise.all([signup(), signup(), signup()]);
await api('mute/create', { userId: carol.id }, alice); await api('mute/create', { userId: carol.id }, alice);
await sleep(1000); await setTimeout(1000);
const carolNote = await post(carol, { text: 'hi' }); const carolNote = await post(carol, { text: 'hi' });
const bobNote = await post(bob, { text: 'hi', renoteId: carolNote.id }); const bobNote = await post(bob, { text: 'hi', renoteId: carolNote.id });
@ -1243,7 +1244,7 @@ describe('Timelines', () => {
const [alice, bob] = await Promise.all([signup(), signup()]); const [alice, bob] = await Promise.all([signup(), signup()]);
await api('mute/create', { userId: bob.id }, alice); await api('mute/create', { userId: bob.id }, alice);
await sleep(1000); await setTimeout(1000);
const bobNote1 = await post(bob, { text: 'hi' }); const bobNote1 = await post(bob, { text: 'hi' });
const bobNote2 = await post(bob, { text: 'hi', replyId: bobNote1.id }); const bobNote2 = await post(bob, { text: 'hi', replyId: bobNote1.id });
const bobNote3 = await post(bob, { text: 'hi', renoteId: bobNote1.id }); const bobNote3 = await post(bob, { text: 'hi', renoteId: bobNote1.id });

View File

@ -17,8 +17,8 @@ describe('users/notes', () => {
beforeAll(async () => { beforeAll(async () => {
alice = await signup({ username: 'alice' }); alice = await signup({ username: 'alice' });
const jpg = await uploadUrl(alice, 'https://raw.githubusercontent.com/misskey-dev/misskey/develop/packages/backend/test/resources/Lenna.jpg'); const jpg = await uploadUrl(alice, 'https://raw.githubusercontent.com/misskey-dev/misskey/develop/packages/backend/test/resources/192.jpg');
const png = await uploadUrl(alice, 'https://raw.githubusercontent.com/misskey-dev/misskey/develop/packages/backend/test/resources/Lenna.png'); const png = await uploadUrl(alice, 'https://raw.githubusercontent.com/misskey-dev/misskey/develop/packages/backend/test/resources/192.png');
jpgNote = await post(alice, { jpgNote = await post(alice, {
fileIds: [jpg.id], fileIds: [jpg.id],
}); });

View File

@ -0,0 +1,22 @@
import globals from 'globals';
import tsParser from '@typescript-eslint/parser';
import sharedConfig from '../../shared/eslint.config.js';
export default [
...sharedConfig,
{
files: ['**/*.ts', '**/*.tsx'],
languageOptions: {
globals: {
...globals.node,
...globals.jest,
},
parserOptions: {
parser: tsParser,
project: ['./tsconfig.json'],
sourceType: 'module',
tsconfigRootDir: import.meta.dirname,
},
},
},
];

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 463 KiB

View File

@ -83,21 +83,21 @@ describe('FileInfoService', () => {
describe('IMAGE', () => { describe('IMAGE', () => {
test('Generic JPEG', async () => { test('Generic JPEG', async () => {
const path = `${resources}/Lenna.jpg`; const path = `${resources}/192.jpg`;
const info = await fileInfoService.getFileInfo(path, { skipSensitiveDetection: true }) as any; const info = await fileInfoService.getFileInfo(path, { skipSensitiveDetection: true }) as any;
delete info.warnings; delete info.warnings;
delete info.blurhash; delete info.blurhash;
delete info.sensitive; delete info.sensitive;
delete info.porn; delete info.porn;
assert.deepStrictEqual(info, { assert.deepStrictEqual(info, {
size: 25360, size: 5131,
md5: '091b3f259662aa31e2ffef4519951168', md5: '8c9ed0677dd2b8f9f7472c3af247e5e3',
type: { type: {
mime: 'image/jpeg', mime: 'image/jpeg',
ext: 'jpg', ext: 'jpg',
}, },
width: 512, width: 192,
height: 512, height: 192,
orientation: undefined, orientation: undefined,
}); });
}); });

View File

@ -5,6 +5,7 @@
process.env.NODE_ENV = 'test'; process.env.NODE_ENV = 'test';
import { setTimeout } from 'node:timers/promises';
import { jest } from '@jest/globals'; import { jest } from '@jest/globals';
import { ModuleMocker } from 'jest-mock'; import { ModuleMocker } from 'jest-mock';
import { Test } from '@nestjs/testing'; import { Test } from '@nestjs/testing';
@ -29,7 +30,6 @@ import { secureRndstr } from '@/misc/secure-rndstr.js';
import { NotificationService } from '@/core/NotificationService.js'; import { NotificationService } from '@/core/NotificationService.js';
import { RoleCondFormulaValue } from '@/models/Role.js'; import { RoleCondFormulaValue } from '@/models/Role.js';
import { UserEntityService } from '@/core/entities/UserEntityService.js'; import { UserEntityService } from '@/core/entities/UserEntityService.js';
import { sleep } from '../utils.js';
import type { TestingModule } from '@nestjs/testing'; import type { TestingModule } from '@nestjs/testing';
import type { MockFunctionMetadata } from 'jest-mock'; import type { MockFunctionMetadata } from 'jest-mock';
@ -278,7 +278,7 @@ describe('RoleService', () => {
// ストリーミング経由で反映されるまでちょっと待つ // ストリーミング経由で反映されるまでちょっと待つ
clock.uninstall(); clock.uninstall();
await sleep(100); await setTimeout(100);
const resultAfter25hAgain = await roleService.getUserPolicies(user.id); const resultAfter25hAgain = await roleService.getUserPolicies(user.id);
expect(resultAfter25hAgain.canManageCustomEmojis).toBe(true); expect(resultAfter25hAgain.canManageCustomEmojis).toBe(true);
@ -807,7 +807,7 @@ describe('RoleService', () => {
await roleService.assign(user.id, role.id); await roleService.assign(user.id, role.id);
clock.uninstall(); clock.uninstall();
await sleep(100); await setTimeout(100);
const assignments = await roleAssignmentsRepository.find({ const assignments = await roleAssignmentsRepository.find({
where: { where: {
@ -835,7 +835,7 @@ describe('RoleService', () => {
await roleService.assign(user.id, role.id); await roleService.assign(user.id, role.id);
clock.uninstall(); clock.uninstall();
await sleep(100); await setTimeout(100);
const assignments = await roleAssignmentsRepository.find({ const assignments = await roleAssignmentsRepository.find({
where: { where: {

View File

@ -3,6 +3,7 @@
* SPDX-License-Identifier: AGPL-3.0-only * SPDX-License-Identifier: AGPL-3.0-only
*/ */
import { setTimeout } from 'node:timers/promises';
import { afterEach, beforeEach, describe, expect, jest } from '@jest/globals'; import { afterEach, beforeEach, describe, expect, jest } from '@jest/globals';
import { Test, TestingModule } from '@nestjs/testing'; import { Test, TestingModule } from '@nestjs/testing';
import { MiUser } from '@/models/User.js'; import { MiUser } from '@/models/User.js';
@ -16,7 +17,7 @@ import { DI } from '@/di-symbols.js';
import { QueueService } from '@/core/QueueService.js'; import { QueueService } from '@/core/QueueService.js';
import { LoggerService } from '@/core/LoggerService.js'; import { LoggerService } from '@/core/LoggerService.js';
import { SystemWebhookService } from '@/core/SystemWebhookService.js'; import { SystemWebhookService } from '@/core/SystemWebhookService.js';
import { randomString, sleep } from '../utils.js'; import { randomString } from '../utils.js';
describe('SystemWebhookService', () => { describe('SystemWebhookService', () => {
let app: TestingModule; let app: TestingModule;
@ -358,7 +359,7 @@ describe('SystemWebhookService', () => {
); );
// redisでの配信経由で更新されるのでちょっと待つ // redisでの配信経由で更新されるのでちょっと待つ
await sleep(500); await setTimeout(500);
const fetchedWebhooks = await service.fetchActiveSystemWebhooks(); const fetchedWebhooks = await service.fetchActiveSystemWebhooks();
expect(fetchedWebhooks).toEqual([webhook]); expect(fetchedWebhooks).toEqual([webhook]);
@ -377,7 +378,7 @@ describe('SystemWebhookService', () => {
); );
// redisでの配信経由で更新されるのでちょっと待つ // redisでの配信経由で更新されるのでちょっと待つ
await sleep(500); await setTimeout(500);
const fetchedWebhooks = await service.fetchActiveSystemWebhooks(); const fetchedWebhooks = await service.fetchActiveSystemWebhooks();
expect(fetchedWebhooks).toEqual([]); expect(fetchedWebhooks).toEqual([]);
@ -407,7 +408,7 @@ describe('SystemWebhookService', () => {
); );
// redisでの配信経由で更新されるのでちょっと待つ // redisでの配信経由で更新されるのでちょっと待つ
await sleep(500); await setTimeout(500);
const fetchedWebhooks = await service.fetchActiveSystemWebhooks(); const fetchedWebhooks = await service.fetchActiveSystemWebhooks();
expect(fetchedWebhooks).toEqual([webhook2]); expect(fetchedWebhooks).toEqual([webhook2]);
@ -434,7 +435,7 @@ describe('SystemWebhookService', () => {
); );
// redisでの配信経由で更新されるのでちょっと待つ // redisでの配信経由で更新されるのでちょっと待つ
await sleep(500); await setTimeout(500);
const fetchedWebhooks = await service.fetchActiveSystemWebhooks(); const fetchedWebhooks = await service.fetchActiveSystemWebhooks();
expect(fetchedWebhooks.length).toEqual(0); expect(fetchedWebhooks.length).toEqual(0);
@ -457,7 +458,7 @@ describe('SystemWebhookService', () => {
); );
// redisでの配信経由で更新されるのでちょっと待つ // redisでの配信経由で更新されるのでちょっと待つ
await sleep(500); await setTimeout(500);
const fetchedWebhooks = await service.fetchActiveSystemWebhooks(); const fetchedWebhooks = await service.fetchActiveSystemWebhooks();
expect(fetchedWebhooks).toEqual([webhook2]); expect(fetchedWebhooks).toEqual([webhook2]);
@ -481,7 +482,7 @@ describe('SystemWebhookService', () => {
); );
// redisでの配信経由で更新されるのでちょっと待つ // redisでの配信経由で更新されるのでちょっと待つ
await sleep(500); await setTimeout(500);
const fetchedWebhooks = await service.fetchActiveSystemWebhooks(); const fetchedWebhooks = await service.fetchActiveSystemWebhooks();
expect(fetchedWebhooks.length).toEqual(0); expect(fetchedWebhooks.length).toEqual(0);
@ -504,7 +505,7 @@ describe('SystemWebhookService', () => {
); );
// redisでの配信経由で更新されるのでちょっと待つ // redisでの配信経由で更新されるのでちょっと待つ
await sleep(500); await setTimeout(500);
const fetchedWebhooks = await service.fetchActiveSystemWebhooks(); const fetchedWebhooks = await service.fetchActiveSystemWebhooks();
expect(fetchedWebhooks.length).toEqual(0); expect(fetchedWebhooks.length).toEqual(0);

View File

@ -297,7 +297,7 @@ export const uploadFile = async (user?: UserToken, { path, name, blob }: UploadO
body: misskey.entities.DriveFile | null body: misskey.entities.DriveFile | null
}> => { }> => {
const absPath = path == null const absPath = path == null
? new URL('resources/Lenna.jpg', import.meta.url) ? new URL('resources/192.jpg', import.meta.url)
: isAbsolute(path.toString()) : isAbsolute(path.toString())
? new URL(path) ? new URL(path)
: new URL(path, new URL('resources/', import.meta.url)); : new URL(path, new URL('resources/', import.meta.url));
@ -605,14 +605,6 @@ export async function initTestDb(justBorrow = false, initEntities?: any[]) {
return db; return db;
} }
export function sleep(msec: number) {
return new Promise<void>(res => {
setTimeout(() => {
res();
}, msec);
});
}
export async function sendEnvUpdateRequest(params: { key: string, value?: string }) { export async function sendEnvUpdateRequest(params: { key: string, value?: string }) {
const res = await fetch( const res = await fetch(
`http://localhost:${port + 1000}/env`, `http://localhost:${port + 1000}/env`,

View File

@ -1,81 +0,0 @@
module.exports = {
root: true,
env: {
'node': false,
},
parser: 'vue-eslint-parser',
parserOptions: {
'parser': '@typescript-eslint/parser',
tsconfigRootDir: __dirname,
project: ['./tsconfig.json'],
extraFileExtensions: ['.vue'],
},
extends: [
'../shared/.eslintrc.js',
'plugin:vue/vue3-recommended',
],
rules: {
'@typescript-eslint/no-empty-interface': [
'error',
{
'allowSingleExtends': true,
},
],
// window の禁止理由: グローバルスコープと衝突し、予期せぬ結果を招くため
'id-denylist': ['error', 'window'],
'no-shadow': ['warn'],
'vue/attributes-order': ['error', {
'alphabetical': false,
}],
'vue/no-use-v-if-with-v-for': ['error', {
'allowUsingIterationVar': false,
}],
'vue/no-ref-as-operand': 'error',
'vue/no-multi-spaces': ['error', {
'ignoreProperties': false,
}],
'vue/no-v-html': 'warn',
'vue/order-in-components': 'error',
'vue/html-indent': ['warn', 'tab', {
'attribute': 1,
'baseIndent': 0,
'closeBracket': 0,
'alignAttributesVertically': true,
'ignores': [],
}],
'vue/html-closing-bracket-spacing': ['warn', {
'startTag': 'never',
'endTag': 'never',
'selfClosingTag': 'never',
}],
'vue/multi-word-component-names': 'warn',
'vue/require-v-for-key': 'warn',
'vue/no-unused-components': 'warn',
'vue/no-unused-vars': 'warn',
'vue/no-dupe-keys': 'warn',
'vue/valid-v-for': 'warn',
'vue/return-in-computed-property': 'warn',
'vue/no-setup-props-reactivity-loss': 'warn',
'vue/max-attributes-per-line': 'off',
'vue/html-self-closing': 'off',
'vue/singleline-html-element-content-newline': 'off',
'vue/v-on-event-hyphenation': ['error', 'never', { autofix: true }],
'vue/attribute-hyphenation': ['error', 'never'],
},
globals: {
// Node.js
'module': false,
'require': false,
'__dirname': false,
// Misskey
'_DEV_': false,
'_LANGS_': false,
'_VERSION_': false,
'_ENV_': false,
'_PERF_PREFIX_': false,
'_DATA_TRANSFER_DRIVE_FILE_': false,
'_DATA_TRANSFER_DRIVE_FOLDER_': false,
'_DATA_TRANSFER_DECK_COLUMN_': false,
},
};

View File

@ -47,7 +47,6 @@ await fs.readFile(
) )
) )
.map((path) => path.replace(/(?:(?<=\.stories)\.(?:impl|meta)|\.msw)(?=\.ts$)/g, '')) .map((path) => path.replace(/(?:(?<=\.stories)\.(?:impl|meta)|\.msw)(?=\.ts$)/g, ''))
.map((path) => (path.startsWith('.') ? path : `./${path}`))
); );
if ( if (
micromatch(Array.from(modules), [ micromatch(Array.from(modules), [

View File

@ -3,7 +3,7 @@
* SPDX-License-Identifier: AGPL-3.0-only * SPDX-License-Identifier: AGPL-3.0-only
*/ */
import { FORCE_REMOUNT } from '@storybook/core-events'; import { FORCE_RE_RENDER, FORCE_REMOUNT } from '@storybook/core-events';
import { addons } from '@storybook/preview-api'; import { addons } from '@storybook/preview-api';
import { type Preview, setup } from '@storybook/vue3'; import { type Preview, setup } from '@storybook/vue3';
import isChromatic from 'chromatic/isChromatic'; import isChromatic from 'chromatic/isChromatic';
@ -16,7 +16,7 @@ import '../src/style.scss';
const appInitialized = Symbol(); const appInitialized = Symbol();
let lastStory = null; let lastStory: string | null = null;
let moduleInitialized = false; let moduleInitialized = false;
let unobserve = () => {}; let unobserve = () => {};
let misskeyOS = null; let misskeyOS = null;
@ -110,7 +110,7 @@ const preview = {
}).catch(() => {}); }).catch(() => {});
Promise.all([resetIndexedDBPromise, resetDefaultStorePromise]).then(() => { Promise.all([resetIndexedDBPromise, resetDefaultStorePromise]).then(() => {
initLocalStorage(); initLocalStorage();
channel.emit(FORCE_REMOUNT, { storyId: context.id }); channel.emit(FORCE_RE_RENDER, { storyId: context.id });
}); });
} }
const story = Story(); const story = Story();

View File

@ -0,0 +1,95 @@
import globals from 'globals';
import tsParser from '@typescript-eslint/parser';
import parser from 'vue-eslint-parser';
import pluginVue from 'eslint-plugin-vue';
import pluginMisskey from '@misskey-dev/eslint-plugin';
import sharedConfig from '../shared/eslint.config.js';
export default [
...sharedConfig,
{
files: ['src/**/*.vue'],
...pluginMisskey.configs.typescript,
},
...pluginVue.configs['flat/recommended'],
{
files: ['src/**/*.{ts,vue}'],
languageOptions: {
globals: {
...Object.fromEntries(Object.entries(globals.node).map(([key]) => [key, 'off'])),
...globals.browser,
// Node.js
module: false,
require: false,
__dirname: false,
// Misskey
_DEV_: false,
_LANGS_: false,
_VERSION_: false,
_ENV_: false,
_PERF_PREFIX_: false,
_DATA_TRANSFER_DRIVE_FILE_: false,
_DATA_TRANSFER_DRIVE_FOLDER_: false,
_DATA_TRANSFER_DECK_COLUMN_: false,
},
parser,
parserOptions: {
extraFileExtensions: ['.vue'],
parser: tsParser,
project: ['./tsconfig.json'],
sourceType: 'module',
tsconfigRootDir: import.meta.dirname,
},
},
rules: {
'@typescript-eslint/no-empty-interface': ['error', {
allowSingleExtends: true,
}],
// window の禁止理由: グローバルスコープと衝突し、予期せぬ結果を招くため
// e の禁止理由: error や event など、複数のキーワードの頭文字であり分かりにくいため
'id-denylist': ['error', 'window', 'e'],
'no-shadow': ['warn'],
'vue/attributes-order': ['error', {
alphabetical: false,
}],
'vue/no-use-v-if-with-v-for': ['error', {
allowUsingIterationVar: false,
}],
'vue/no-ref-as-operand': 'error',
'vue/no-multi-spaces': ['error', {
ignoreProperties: false,
}],
'vue/no-v-html': 'warn',
'vue/order-in-components': 'error',
'vue/html-indent': ['warn', 'tab', {
attribute: 1,
baseIndent: 0,
closeBracket: 0,
alignAttributesVertically: true,
ignores: [],
}],
'vue/html-closing-bracket-spacing': ['warn', {
startTag: 'never',
endTag: 'never',
selfClosingTag: 'never',
}],
'vue/multi-word-component-names': 'warn',
'vue/require-v-for-key': 'warn',
'vue/no-unused-components': 'warn',
'vue/no-unused-vars': 'warn',
'vue/no-dupe-keys': 'warn',
'vue/valid-v-for': 'warn',
'vue/return-in-computed-property': 'warn',
'vue/no-setup-props-reactivity-loss': 'warn',
'vue/max-attributes-per-line': 'off',
'vue/html-self-closing': 'off',
'vue/singleline-html-element-content-newline': 'off',
'vue/v-on-event-hyphenation': ['error', 'never', {
autofix: true,
}],
'vue/attribute-hyphenation': ['error', 'never'],
},
},
];

View File

@ -22,24 +22,24 @@
"@mcaptcha/vanilla-glue": "0.1.0-alpha-3", "@mcaptcha/vanilla-glue": "0.1.0-alpha-3",
"@misskey-dev/browser-image-resizer": "2024.1.0", "@misskey-dev/browser-image-resizer": "2024.1.0",
"@rollup/plugin-json": "6.1.0", "@rollup/plugin-json": "6.1.0",
"@rollup/plugin-replace": "5.0.5", "@rollup/plugin-replace": "5.0.7",
"@rollup/pluginutils": "5.1.0", "@rollup/pluginutils": "5.1.0",
"@syuilo/aiscript": "0.18.0", "@syuilo/aiscript": "0.18.0",
"@tabler/icons-webfont": "3.3.0", "@tabler/icons-webfont": "3.3.0",
"@twemoji/parser": "15.1.1", "@twemoji/parser": "15.1.1",
"@vitejs/plugin-vue": "5.0.4", "@vitejs/plugin-vue": "5.0.5",
"@vue/compiler-sfc": "3.4.26", "@vue/compiler-sfc": "3.4.31",
"aiscript-vscode": "github:aiscript-dev/aiscript-vscode#v0.1.9", "aiscript-vscode": "github:aiscript-dev/aiscript-vscode#v0.1.9",
"astring": "1.8.6", "astring": "1.8.6",
"broadcast-channel": "7.0.0", "broadcast-channel": "7.0.0",
"buraha": "0.0.1", "buraha": "0.0.1",
"canvas-confetti": "1.9.3", "canvas-confetti": "1.9.3",
"chart.js": "4.4.2", "chart.js": "4.4.3",
"chartjs-adapter-date-fns": "3.0.0", "chartjs-adapter-date-fns": "3.0.0",
"chartjs-chart-matrix": "2.0.1", "chartjs-chart-matrix": "2.0.1",
"chartjs-plugin-gradient": "0.6.1", "chartjs-plugin-gradient": "0.6.1",
"chartjs-plugin-zoom": "2.0.1", "chartjs-plugin-zoom": "2.0.1",
"chromatic": "11.3.0", "chromatic": "11.5.4",
"compare-versions": "6.1.0", "compare-versions": "6.1.0",
"cropperjs": "2.0.0-beta.5", "cropperjs": "2.0.0-beta.5",
"date-fns": "2.30.0", "date-fns": "2.30.0",
@ -56,89 +56,87 @@
"misskey-mahjong": "workspace:*", "misskey-mahjong": "workspace:*",
"misskey-js": "workspace:*", "misskey-js": "workspace:*",
"misskey-reversi": "workspace:*", "misskey-reversi": "workspace:*",
"photoswipe": "5.4.3", "photoswipe": "5.4.4",
"punycode": "2.3.1", "punycode": "2.3.1",
"rollup": "4.17.2", "rollup": "4.18.0",
"sanitize-html": "2.13.0", "sanitize-html": "2.13.0",
"sass": "1.76.0", "sass": "1.77.6",
"shiki": "1.4.0", "shiki": "1.10.0",
"strict-event-emitter-types": "2.0.0", "strict-event-emitter-types": "2.0.0",
"textarea-caret": "3.1.0", "textarea-caret": "3.1.0",
"three": "0.164.1", "three": "0.165.0",
"throttle-debounce": "5.0.0", "throttle-debounce": "5.0.2",
"tinycolor2": "1.6.0", "tinycolor2": "1.6.0",
"tsc-alias": "1.8.8", "tsc-alias": "1.8.10",
"tsconfig-paths": "4.2.0", "tsconfig-paths": "4.2.0",
"typescript": "5.5.2", "typescript": "5.5.3",
"uuid": "9.0.1", "uuid": "10.0.0",
"v-code-diff": "1.11.0", "v-code-diff": "1.12.0",
"vite": "5.2.11", "vite": "5.3.2",
"vue": "3.4.26", "vue": "3.4.31",
"vuedraggable": "next" "vuedraggable": "next"
}, },
"devDependencies": { "devDependencies": {
"@misskey-dev/eslint-plugin": "1.0.0",
"@misskey-dev/summaly": "5.1.0", "@misskey-dev/summaly": "5.1.0",
"@storybook/addon-actions": "8.0.9", "@storybook/addon-actions": "8.1.11",
"@storybook/addon-essentials": "8.0.9", "@storybook/addon-essentials": "8.1.11",
"@storybook/addon-interactions": "8.0.9", "@storybook/addon-interactions": "8.1.11",
"@storybook/addon-links": "8.0.9", "@storybook/addon-links": "8.1.11",
"@storybook/addon-mdx-gfm": "8.0.9", "@storybook/addon-mdx-gfm": "8.1.11",
"@storybook/addon-storysource": "8.0.9", "@storybook/addon-storysource": "8.1.11",
"@storybook/blocks": "8.0.9", "@storybook/blocks": "8.1.11",
"@storybook/components": "8.0.9", "@storybook/components": "8.1.11",
"@storybook/core-events": "8.0.9", "@storybook/core-events": "8.1.11",
"@storybook/manager-api": "8.0.9", "@storybook/manager-api": "8.1.11",
"@storybook/preview-api": "8.0.9", "@storybook/preview-api": "8.1.11",
"@storybook/react": "8.0.9", "@storybook/react": "8.1.11",
"@storybook/react-vite": "8.0.9", "@storybook/react-vite": "8.1.11",
"@storybook/test": "8.0.9", "@storybook/test": "8.1.11",
"@storybook/theming": "8.0.9", "@storybook/theming": "8.1.11",
"@storybook/types": "8.0.9", "@storybook/types": "8.1.11",
"@storybook/vue3": "8.0.9", "@storybook/vue3": "8.1.11",
"@storybook/vue3-vite": "8.0.9", "@storybook/vue3-vite": "8.1.11",
"@testing-library/vue": "8.0.3", "@testing-library/vue": "8.1.0",
"@types/escape-regexp": "0.0.3", "@types/escape-regexp": "0.0.3",
"@types/estree": "1.0.5", "@types/estree": "1.0.5",
"@types/matter-js": "0.19.6", "@types/matter-js": "0.19.6",
"@types/micromatch": "4.0.7", "@types/micromatch": "4.0.9",
"@types/node": "20.12.7", "@types/node": "20.14.9",
"@types/punycode": "2.1.4", "@types/punycode": "2.1.4",
"@types/sanitize-html": "2.11.0", "@types/sanitize-html": "2.11.0",
"@types/seedrandom": "3.0.8", "@types/seedrandom": "3.0.8",
"@types/throttle-debounce": "5.0.2", "@types/throttle-debounce": "5.0.2",
"@types/tinycolor2": "1.4.6", "@types/tinycolor2": "1.4.6",
"@types/uuid": "9.0.8", "@types/uuid": "10.0.0",
"@types/ws": "8.5.10", "@types/ws": "8.5.10",
"@typescript-eslint/eslint-plugin": "7.7.1", "@typescript-eslint/eslint-plugin": "7.15.0",
"@typescript-eslint/parser": "7.7.1", "@typescript-eslint/parser": "7.15.0",
"@vitest/coverage-v8": "0.34.6", "@vitest/coverage-v8": "1.6.0",
"@vue/runtime-core": "3.4.26", "@vue/runtime-core": "3.4.31",
"acorn": "8.11.3", "acorn": "8.12.0",
"cross-env": "7.0.3", "cross-env": "7.0.3",
"cypress": "13.8.1", "cypress": "13.13.0",
"eslint": "8.57.0",
"eslint-plugin-import": "2.29.1", "eslint-plugin-import": "2.29.1",
"eslint-plugin-vue": "9.25.0", "eslint-plugin-vue": "9.26.0",
"fast-glob": "3.3.2", "fast-glob": "3.3.2",
"happy-dom": "10.0.3", "happy-dom": "10.0.3",
"intersection-observer": "0.12.2", "intersection-observer": "0.12.2",
"micromatch": "4.0.5", "micromatch": "4.0.7",
"msw": "2.2.14", "msw": "2.3.1",
"msw-storybook-addon": "2.0.1", "msw-storybook-addon": "2.0.2",
"nodemon": "3.1.0", "nodemon": "3.1.4",
"prettier": "3.2.5", "prettier": "3.3.2",
"react": "18.3.1", "react": "18.3.1",
"react-dom": "18.3.1", "react-dom": "18.3.1",
"seedrandom": "3.0.5", "seedrandom": "3.0.5",
"start-server-and-test": "2.0.3", "start-server-and-test": "2.0.4",
"storybook": "8.0.9", "storybook": "8.1.11",
"storybook-addon-misskey-theme": "github:misskey-dev/storybook-addon-misskey-theme", "storybook-addon-misskey-theme": "github:misskey-dev/storybook-addon-misskey-theme",
"vite-plugin-turbosnap": "1.0.3", "vite-plugin-turbosnap": "1.0.3",
"vitest": "0.34.6", "vitest": "1.6.0",
"vitest-fetch-mock": "0.2.2", "vitest-fetch-mock": "0.2.2",
"vue-component-type-helpers": "2.0.16", "vue-component-type-helpers": "2.0.24",
"vue-eslint-parser": "9.4.2", "vue-eslint-parser": "9.4.3",
"vue-tsc": "2.0.16" "vue-tsc": "2.0.24"
} }
} }

View File

@ -184,10 +184,12 @@ export async function refreshAccount() {
export async function login(token: Account['token'], redirect?: string) { export async function login(token: Account['token'], redirect?: string) {
const showing = ref(true); const showing = ref(true);
popup(defineAsyncComponent(() => import('@/components/MkWaitingDialog.vue')), { const { dispose } = popup(defineAsyncComponent(() => import('@/components/MkWaitingDialog.vue')), {
success: false, success: false,
showing: showing, showing: showing,
}, {}, 'closed'); }, {
closed: () => dispose(),
});
if (_DEV_) console.log('logging as token ', token); if (_DEV_) console.log('logging as token ', token);
const me = await fetchAccount(token, undefined, true) const me = await fetchAccount(token, undefined, true)
.catch(reason => { .catch(reason => {
@ -223,21 +225,23 @@ export async function openAccountMenu(opts: {
if (!$i) return; if (!$i) return;
function showSigninDialog() { function showSigninDialog() {
popup(defineAsyncComponent(() => import('@/components/MkSigninDialog.vue')), {}, { const { dispose } = popup(defineAsyncComponent(() => import('@/components/MkSigninDialog.vue')), {}, {
done: res => { done: res => {
addAccount(res.id, res.i); addAccount(res.id, res.i);
success(); success();
}, },
}, 'closed'); closed: () => dispose(),
});
} }
function createAccount() { function createAccount() {
popup(defineAsyncComponent(() => import('@/components/MkSignupDialog.vue')), {}, { const { dispose } = popup(defineAsyncComponent(() => import('@/components/MkSignupDialog.vue')), {}, {
done: res => { done: res => {
addAccount(res.id, res.i); addAccount(res.id, res.i);
switchAccountWithToken(res.i); switchAccountWithToken(res.i);
}, },
}, 'closed'); closed: () => dispose(),
});
} }
async function switchAccount(account: Misskey.entities.UserDetailed) { async function switchAccount(account: Misskey.entities.UserDetailed) {

View File

@ -35,7 +35,9 @@ export async function mainBoot() {
emojiPicker.init(); emojiPicker.init();
if (isClientUpdated && $i) { if (isClientUpdated && $i) {
popup(defineAsyncComponent(() => import('@/components/MkUpdated.vue')), {}, {}, 'closed'); const { dispose } = popup(defineAsyncComponent(() => import('@/components/MkUpdated.vue')), {}, {
closed: () => dispose(),
});
} }
const stream = useStream(); const stream = useStream();
@ -96,7 +98,7 @@ export async function mainBoot() {
}).render(); }).render();
} }
} }
} }
} catch (error) { } catch (error) {
// console.error(error); // console.error(error);
console.error('Failed to initialise the seasonal screen effect canvas context:', error); console.error('Failed to initialise the seasonal screen effect canvas context:', error);
@ -108,22 +110,28 @@ export async function mainBoot() {
defaultStore.loaded.then(() => { defaultStore.loaded.then(() => {
if (defaultStore.state.accountSetupWizard !== -1) { if (defaultStore.state.accountSetupWizard !== -1) {
popup(defineAsyncComponent(() => import('@/components/MkUserSetupDialog.vue')), {}, {}, 'closed'); const { dispose } = popup(defineAsyncComponent(() => import('@/components/MkUserSetupDialog.vue')), {}, {
closed: () => dispose(),
});
} }
}); });
for (const announcement of ($i.unreadAnnouncements ?? []).filter(x => x.display === 'dialog')) { for (const announcement of ($i.unreadAnnouncements ?? []).filter(x => x.display === 'dialog')) {
popup(defineAsyncComponent(() => import('@/components/MkAnnouncementDialog.vue')), { const { dispose } = popup(defineAsyncComponent(() => import('@/components/MkAnnouncementDialog.vue')), {
announcement, announcement,
}, {}, 'closed'); }, {
closed: () => dispose(),
});
} }
stream.on('announcementCreated', (ev) => { stream.on('announcementCreated', (ev) => {
const announcement = ev.announcement; const announcement = ev.announcement;
if (announcement.display === 'dialog') { if (announcement.display === 'dialog') {
popup(defineAsyncComponent(() => import('@/components/MkAnnouncementDialog.vue')), { const { dispose } = popup(defineAsyncComponent(() => import('@/components/MkAnnouncementDialog.vue')), {
announcement, announcement,
}, {}, 'closed'); }, {
closed: () => dispose(),
});
} }
}); });
@ -247,13 +255,17 @@ export async function mainBoot() {
const neverShowDonationInfo = miLocalStorage.getItem('neverShowDonationInfo'); const neverShowDonationInfo = miLocalStorage.getItem('neverShowDonationInfo');
if (neverShowDonationInfo !== 'true' && (createdAt.getTime() < (Date.now() - (1000 * 60 * 60 * 24 * 3))) && !location.pathname.startsWith('/miauth')) { if (neverShowDonationInfo !== 'true' && (createdAt.getTime() < (Date.now() - (1000 * 60 * 60 * 24 * 3))) && !location.pathname.startsWith('/miauth')) {
if (latestDonationInfoShownAt == null || (new Date(latestDonationInfoShownAt).getTime() < (Date.now() - (1000 * 60 * 60 * 24 * 30)))) { if (latestDonationInfoShownAt == null || (new Date(latestDonationInfoShownAt).getTime() < (Date.now() - (1000 * 60 * 60 * 24 * 30)))) {
popup(defineAsyncComponent(() => import('@/components/MkDonation.vue')), {}, {}, 'closed'); const { dispose } = popup(defineAsyncComponent(() => import('@/components/MkDonation.vue')), {}, {
closed: () => dispose(),
});
} }
} }
const modifiedVersionMustProminentlyOfferInAgplV3Section13Read = miLocalStorage.getItem('modifiedVersionMustProminentlyOfferInAgplV3Section13Read'); const modifiedVersionMustProminentlyOfferInAgplV3Section13Read = miLocalStorage.getItem('modifiedVersionMustProminentlyOfferInAgplV3Section13Read');
if (modifiedVersionMustProminentlyOfferInAgplV3Section13Read !== 'true' && instance.repositoryUrl !== 'https://github.com/misskey-dev/misskey') { if (modifiedVersionMustProminentlyOfferInAgplV3Section13Read !== 'true' && instance.repositoryUrl !== 'https://github.com/misskey-dev/misskey') {
popup(defineAsyncComponent(() => import('@/components/MkSourceCodeAvailablePopup.vue')), {}, {}, 'closed'); const { dispose } = popup(defineAsyncComponent(() => import('@/components/MkSourceCodeAvailablePopup.vue')), {}, {
closed: () => dispose(),
});
} }
if ('Notification' in window) { if ('Notification' in window) {

View File

@ -12,14 +12,12 @@ import { expect, userEvent, within } from '@storybook/test';
import { channel } from '../../.storybook/fakes.js'; import { channel } from '../../.storybook/fakes.js';
import { commonHandlers } from '../../.storybook/mocks.js'; import { commonHandlers } from '../../.storybook/mocks.js';
import MkChannelFollowButton from './MkChannelFollowButton.vue'; import MkChannelFollowButton from './MkChannelFollowButton.vue';
import { semaphore } from '@/scripts/test-utils.js';
import { i18n } from '@/i18n.js'; import { i18n } from '@/i18n.js';
function sleep(ms: number) { function sleep(ms: number) {
return new Promise(resolve => setTimeout(resolve, ms)); return new Promise(resolve => setTimeout(resolve, ms));
} }
const s = semaphore();
export const Default = { export const Default = {
render(args) { render(args) {
return { return {
@ -46,17 +44,13 @@ export const Default = {
full: true, full: true,
}, },
async play({ canvasElement }) { async play({ canvasElement }) {
await s.acquire();
await sleep(1000);
const canvas = within(canvasElement); const canvas = within(canvasElement);
const buttonElement = canvas.getByRole<HTMLButtonElement>('button'); const buttonElement = canvas.getByRole<HTMLButtonElement>('button');
await expect(buttonElement).toHaveTextContent(i18n.ts.follow); await expect(buttonElement).toHaveTextContent(i18n.ts.follow);
await userEvent.click(buttonElement); await userEvent.click(buttonElement);
await sleep(1000); await sleep(1000);
await expect(buttonElement).toHaveTextContent(i18n.ts.unfollow); await expect(buttonElement).toHaveTextContent(i18n.ts.unfollow);
await sleep(100);
await userEvent.click(buttonElement); await userEvent.click(buttonElement);
s.release();
}, },
parameters: { parameters: {
layout: 'centered', layout: 'centered',

View File

@ -8,7 +8,7 @@
import { StoryObj } from '@storybook/vue3'; import { StoryObj } from '@storybook/vue3';
import { HttpResponse, http } from 'msw'; import { HttpResponse, http } from 'msw';
import { action } from '@storybook/addon-actions'; import { action } from '@storybook/addon-actions';
import { expect, within } from '@storybook/test'; import { expect, userEvent, within } from '@storybook/test';
import { commonHandlers } from '../../.storybook/mocks.js'; import { commonHandlers } from '../../.storybook/mocks.js';
import MkClickerGame from './MkClickerGame.vue'; import MkClickerGame from './MkClickerGame.vue';
@ -41,12 +41,10 @@ export const Default = {
await sleep(1000); await sleep(1000);
const canvas = within(canvasElement); const canvas = within(canvasElement);
const count = canvas.getByTestId('count'); const count = canvas.getByTestId('count');
// NOTE: flaky なので N/A も通しておく await expect(count).toHaveTextContent('0');
await expect(count).toHaveTextContent(/^(0|N\/A)$/); const buttonElement = canvas.getByRole<HTMLButtonElement>('button');
// FIXME: flaky await userEvent.click(buttonElement);
// const buttonElement = canvas.getByRole<HTMLButtonElement>('button'); await expect(count).toHaveTextContent('1');
// await userEvent.click(buttonElement);
// await expect(count).toHaveTextContent('1');
}, },
parameters: { parameters: {
layout: 'centered', layout: 'centered',

View File

@ -35,7 +35,9 @@ const prevCookies = ref(0);
function onClick(ev: MouseEvent) { function onClick(ev: MouseEvent) {
const x = ev.clientX; const x = ev.clientX;
const y = ev.clientY; const y = ev.clientY;
os.popup(MkPlusOneEffect, { x, y }, {}, 'end'); const { dispose } = os.popup(MkPlusOneEffect, { x, y }, {
end: () => dispose(),
});
saveData.value!.cookies++; saveData.value!.cookies++;
saveData.value!.totalCookies++; saveData.value!.totalCookies++;

View File

@ -11,13 +11,6 @@ import { expect, userEvent, within } from '@storybook/test';
import { file } from '../../.storybook/fakes.js'; import { file } from '../../.storybook/fakes.js';
import MkCwButton from './MkCwButton.vue'; import MkCwButton from './MkCwButton.vue';
import { i18n } from '@/i18n.js'; import { i18n } from '@/i18n.js';
import { semaphore } from '@/scripts/test-utils.js';
function sleep(ms: number) {
return new Promise(resolve => setTimeout(resolve, ms));
}
const s = semaphore();
export const Default = { export const Default = {
render(args) { render(args) {
@ -54,8 +47,6 @@ export const Default = {
text: 'Some CW content', text: 'Some CW content',
}, },
async play({ canvasElement }) { async play({ canvasElement }) {
await s.acquire();
await sleep(1000);
const canvas = within(canvasElement); const canvas = within(canvasElement);
const buttonElement = canvas.getByRole<HTMLButtonElement>('button'); const buttonElement = canvas.getByRole<HTMLButtonElement>('button');
await expect(buttonElement).toHaveTextContent(i18n.ts._cw.show); await expect(buttonElement).toHaveTextContent(i18n.ts._cw.show);
@ -63,7 +54,6 @@ export const Default = {
await userEvent.click(buttonElement); await userEvent.click(buttonElement);
await expect(buttonElement).toHaveTextContent(i18n.ts._cw.hide); await expect(buttonElement).toHaveTextContent(i18n.ts._cw.hide);
await userEvent.click(buttonElement); await userEvent.click(buttonElement);
s.release();
}, },
parameters: { parameters: {
chromatic: { chromatic: {

View File

@ -257,10 +257,11 @@ function onContextmenu(ev: MouseEvent) {
text: i18n.ts.openInWindow, text: i18n.ts.openInWindow,
icon: 'ti ti-app-window', icon: 'ti ti-app-window',
action: () => { action: () => {
os.popup(defineAsyncComponent(() => import('@/components/MkDriveWindow.vue')), { const { dispose } = os.popup(defineAsyncComponent(() => import('@/components/MkDriveWindow.vue')), {
initialFolder: props.folder, initialFolder: props.folder,
}, { }, {
}, 'closed'); closed: () => dispose(),
});
}, },
}, { type: 'divider' }, { }, { type: 'divider' }, {
text: i18n.ts.rename, text: i18n.ts.rename,

View File

@ -402,7 +402,9 @@ function chosen(emoji: any, ev?: MouseEvent) {
const rect = el.getBoundingClientRect(); const rect = el.getBoundingClientRect();
const x = rect.left + (el.offsetWidth / 2); const x = rect.left + (el.offsetWidth / 2);
const y = rect.top + (el.offsetHeight / 2); const y = rect.top + (el.offsetHeight / 2);
os.popup(MkRippleEffect, { x, y }, {}, 'end'); const { dispose } = os.popup(MkRippleEffect, { x, y }, {
end: () => dispose(),
});
} }
const key = getKey(emoji); const key = getKey(emoji);

View File

@ -37,11 +37,13 @@ const el = ref<HTMLElement | { $el: HTMLElement }>();
if (isEnabledUrlPreview.value) { if (isEnabledUrlPreview.value) {
useTooltip(el, (showing) => { useTooltip(el, (showing) => {
os.popup(defineAsyncComponent(() => import('@/components/MkUrlPreviewPopup.vue')), { const { dispose } = os.popup(defineAsyncComponent(() => import('@/components/MkUrlPreviewPopup.vue')), {
showing, showing,
url: props.url, url: props.url,
source: el.value instanceof HTMLElement ? el.value : el.value?.$el, source: el.value instanceof HTMLElement ? el.value : el.value?.$el,
}, {}, 'closed'); }, {
closed: () => dispose(),
});
}); });
} }
</script> </script>

View File

@ -335,12 +335,14 @@ if (!props.mock) {
if (users.length < 1) return; if (users.length < 1) return;
os.popup(MkUsersTooltip, { const { dispose } = os.popup(MkUsersTooltip, {
showing, showing,
users, users,
count: appearNote.value.renoteCount, count: appearNote.value.renoteCount,
targetElement: renoteButton.value, targetElement: renoteButton.value,
}, {}, 'closed'); }, {
closed: () => dispose(),
});
}); });
if (appearNote.value.reactionAcceptance === 'likeOnly') { if (appearNote.value.reactionAcceptance === 'likeOnly') {
@ -355,13 +357,15 @@ if (!props.mock) {
if (users.length < 1) return; if (users.length < 1) return;
os.popup(MkReactionsViewerDetails, { const { dispose } = os.popup(MkReactionsViewerDetails, {
showing, showing,
reaction: '❤️', reaction: '❤️',
users, users,
count: appearNote.value.reactionCount, count: appearNote.value.reactionCount,
targetElement: reactButton.value!, targetElement: reactButton.value!,
}, {}, 'closed'); }, {
closed: () => dispose(),
});
}); });
} }
} }
@ -409,7 +413,9 @@ function react(viaKeyboard = false): void {
const rect = el.getBoundingClientRect(); const rect = el.getBoundingClientRect();
const x = rect.left + (el.offsetWidth / 2); const x = rect.left + (el.offsetWidth / 2);
const y = rect.top + (el.offsetHeight / 2); const y = rect.top + (el.offsetHeight / 2);
os.popup(MkRippleEffect, { x, y }, {}, 'end'); const { dispose } = os.popup(MkRippleEffect, { x, y }, {
end: () => dispose(),
});
} }
} else { } else {
blur(); blur();

View File

@ -346,12 +346,14 @@ useTooltip(renoteButton, async (showing) => {
if (users.length < 1) return; if (users.length < 1) return;
os.popup(MkUsersTooltip, { const { dispose } = os.popup(MkUsersTooltip, {
showing, showing,
users, users,
count: appearNote.value.renoteCount, count: appearNote.value.renoteCount,
targetElement: renoteButton.value, targetElement: renoteButton.value,
}, {}, 'closed'); }, {
closed: () => dispose(),
});
}); });
if (appearNote.value.reactionAcceptance === 'likeOnly') { if (appearNote.value.reactionAcceptance === 'likeOnly') {
@ -366,13 +368,15 @@ if (appearNote.value.reactionAcceptance === 'likeOnly') {
if (users.length < 1) return; if (users.length < 1) return;
os.popup(MkReactionsViewerDetails, { const { dispose } = os.popup(MkReactionsViewerDetails, {
showing, showing,
reaction: '❤️', reaction: '❤️',
users, users,
count: appearNote.value.reactionCount, count: appearNote.value.reactionCount,
targetElement: reactButton.value!, targetElement: reactButton.value!,
}, {}, 'closed'); }, {
closed: () => dispose(),
});
}); });
} }
@ -413,7 +417,9 @@ function react(viaKeyboard = false): void {
const rect = el.getBoundingClientRect(); const rect = el.getBoundingClientRect();
const x = rect.left + (el.offsetWidth / 2); const x = rect.left + (el.offsetWidth / 2);
const y = rect.top + (el.offsetHeight / 2); const y = rect.top + (el.offsetHeight / 2);
os.popup(MkRippleEffect, { x, y }, {}, 'end'); const { dispose } = os.popup(MkRippleEffect, { x, y }, {
end: () => dispose(),
});
} }
} else { } else {
blur(); blur();

View File

@ -463,7 +463,7 @@ function setVisibility() {
return; return;
} }
os.popup(defineAsyncComponent(() => import('@/components/MkVisibilityPicker.vue')), { const { dispose } = os.popup(defineAsyncComponent(() => import('@/components/MkVisibilityPicker.vue')), {
currentVisibility: visibility.value, currentVisibility: visibility.value,
isSilenced: $i.isSilenced, isSilenced: $i.isSilenced,
localOnly: localOnly.value, localOnly: localOnly.value,
@ -476,7 +476,8 @@ function setVisibility() {
defaultStore.set('visibility', visibility.value); defaultStore.set('visibility', visibility.value);
} }
}, },
}, 'closed'); closed: () => dispose(),
});
} }
async function toggleLocalOnly() { async function toggleLocalOnly() {
@ -624,8 +625,8 @@ async function onPaste(ev: ClipboardEvent) {
return; return;
} }
const fileName = formatTimeString(new Date(), defaultStore.state.pastedFileName).replace(/{{number}}/g, "0"); const fileName = formatTimeString(new Date(), defaultStore.state.pastedFileName).replace(/{{number}}/g, '0');
const file = new File([paste], `${fileName}.txt`, { type: "text/plain" }); const file = new File([paste], `${fileName}.txt`, { type: 'text/plain' });
upload(file, `${fileName}.txt`); upload(file, `${fileName}.txt`);
}); });
} }
@ -731,7 +732,9 @@ async function post(ev?: MouseEvent) {
const rect = el.getBoundingClientRect(); const rect = el.getBoundingClientRect();
const x = rect.left + (el.offsetWidth / 2); const x = rect.left + (el.offsetWidth / 2);
const y = rect.top + (el.offsetHeight / 2); const y = rect.top + (el.offsetHeight / 2);
os.popup(MkRippleEffect, { x, y }, {}, 'end'); const { dispose } = os.popup(MkRippleEffect, { x, y }, {
end: () => dispose(),
});
} }
} }

View File

@ -108,7 +108,7 @@ async function rename(file) {
async function describe(file) { async function describe(file) {
if (mock) return; if (mock) return;
os.popup(defineAsyncComponent(() => import('@/components/MkFileCaptionEditWindow.vue')), { const { dispose } = os.popup(defineAsyncComponent(() => import('@/components/MkFileCaptionEditWindow.vue')), {
default: file.comment !== null ? file.comment : '', default: file.comment !== null ? file.comment : '',
file: file, file: file,
}, { }, {
@ -121,7 +121,8 @@ async function describe(file) {
file.comment = comment; file.comment = comment;
}); });
}, },
}, 'closed'); closed: () => dispose(),
});
} }
async function crop(file: Misskey.entities.DriveFile): Promise<void> { async function crop(file: Misskey.entities.DriveFile): Promise<void> {

View File

@ -101,17 +101,19 @@ const steps = computed(() => {
} }
}); });
const onMousedown = (ev: MouseEvent | TouchEvent) => { function onMousedown(ev: MouseEvent | TouchEvent) {
ev.preventDefault(); ev.preventDefault();
const tooltipShowing = ref(true); const tooltipShowing = ref(true);
os.popup(defineAsyncComponent(() => import('@/components/MkTooltip.vue')), { const { dispose } = os.popup(defineAsyncComponent(() => import('@/components/MkTooltip.vue')), {
showing: tooltipShowing, showing: tooltipShowing,
text: computed(() => { text: computed(() => {
return props.textConverter(finalValue.value); return props.textConverter(finalValue.value);
}), }),
targetElement: thumbEl, targetElement: thumbEl,
}, {}, 'closed'); }, {
closed: () => dispose(),
});
const style = document.createElement('style'); const style = document.createElement('style');
style.appendChild(document.createTextNode('* { cursor: grabbing !important; } body * { pointer-events: none !important; }')); style.appendChild(document.createTextNode('* { cursor: grabbing !important; } body * { pointer-events: none !important; }'));
@ -152,7 +154,7 @@ const onMousedown = (ev: MouseEvent | TouchEvent) => {
window.addEventListener('touchmove', onDrag); window.addEventListener('touchmove', onDrag);
window.addEventListener('mouseup', onMouseup, { once: true }); window.addEventListener('mouseup', onMouseup, { once: true });
window.addEventListener('touchend', onMouseup, { once: true }); window.addEventListener('touchend', onMouseup, { once: true });
}; }
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>

View File

@ -24,11 +24,13 @@ const elRef = shallowRef();
if (props.withTooltip) { if (props.withTooltip) {
useTooltip(elRef, (showing) => { useTooltip(elRef, (showing) => {
os.popup(defineAsyncComponent(() => import('@/components/MkReactionTooltip.vue')), { const { dispose } = os.popup(defineAsyncComponent(() => import('@/components/MkReactionTooltip.vue')), {
showing, showing,
reaction: props.reaction.replace(/^:(\w+):$/, ':$1@.:'), reaction: props.reaction.replace(/^:(\w+):$/, ':$1@.:'),
targetElement: elRef.value.$el, targetElement: elRef.value.$el,
}, {}, 'closed'); }, {
closed: () => dispose(),
});
}); });
} }
</script> </script>

View File

@ -114,10 +114,12 @@ async function menu(ev) {
text: i18n.ts.info, text: i18n.ts.info,
icon: 'ti ti-info-circle', icon: 'ti ti-info-circle',
action: async () => { action: async () => {
os.popup(MkCustomEmojiDetailedDialog, { const { dispose } = os.popup(MkCustomEmojiDetailedDialog, {
emoji: await misskeyApiGet('emoji', { emoji: await misskeyApiGet('emoji', {
name: props.reaction.replace(/:/g, '').replace(/@\./, ''), name: props.reaction.replace(/:/g, '').replace(/@\./, ''),
}), }),
}, {
closed: () => dispose(),
}); });
}, },
}], ev.currentTarget ?? ev.target); }], ev.currentTarget ?? ev.target);
@ -129,7 +131,9 @@ function anime() {
const rect = buttonEl.value.getBoundingClientRect(); const rect = buttonEl.value.getBoundingClientRect();
const x = rect.left + 16; const x = rect.left + 16;
const y = rect.top + (buttonEl.value.offsetHeight / 2); const y = rect.top + (buttonEl.value.offsetHeight / 2);
os.popup(MkReactionEffect, { reaction: props.reaction, x, y }, {}, 'end'); const { dispose } = os.popup(MkReactionEffect, { reaction: props.reaction, x, y }, {
end: () => dispose(),
});
} }
watch(() => props.count, (newCount, oldCount) => { watch(() => props.count, (newCount, oldCount) => {
@ -151,13 +155,15 @@ if (!mock) {
const users = reactions.map(x => x.user); const users = reactions.map(x => x.user);
os.popup(XDetails, { const { dispose } = os.popup(XDetails, {
showing, showing,
reaction: props.reaction, reaction: props.reaction,
users, users,
count: props.count, count: props.count,
targetElement: buttonEl.value, targetElement: buttonEl.value,
}, {}, 'closed'); }, {
closed: () => dispose(),
});
}, 100); }, 100);
} }
</script> </script>

View File

@ -218,8 +218,9 @@ function loginFailed(err: any): void {
} }
function resetPassword(): void { function resetPassword(): void {
os.popup(defineAsyncComponent(() => import('@/components/MkForgotPassword.vue')), {}, { const { dispose } = os.popup(defineAsyncComponent(() => import('@/components/MkForgotPassword.vue')), {}, {
}, 'closed'); closed: () => dispose(),
});
} }
</script> </script>

View File

@ -25,15 +25,15 @@ export type MkSystemWebhookResult = {
export async function showSystemWebhookEditorDialog(props: MkSystemWebhookEditorProps): Promise<MkSystemWebhookResult | null> { export async function showSystemWebhookEditorDialog(props: MkSystemWebhookEditorProps): Promise<MkSystemWebhookResult | null> {
const { dispose, result } = await new Promise<{ dispose: () => void, result: MkSystemWebhookResult | null }>(async resolve => { const { dispose, result } = await new Promise<{ dispose: () => void, result: MkSystemWebhookResult | null }>(async resolve => {
const res = await os.popup( const { dispose: _dispose } = os.popup(
defineAsyncComponent(() => import('@/components/MkSystemWebhookEditor.vue')), defineAsyncComponent(() => import('@/components/MkSystemWebhookEditor.vue')),
props, props,
{ {
submitted: (ev: MkSystemWebhookResult) => { submitted: (ev: MkSystemWebhookResult) => {
resolve({ dispose: res.dispose, result: ev }); resolve({ dispose: _dispose, result: ev });
}, },
closed: () => { closed: () => {
resolve({ dispose: res.dispose, result: null }); resolve({ dispose: _dispose, result: null });
}, },
}, },
); );

View File

@ -188,11 +188,13 @@ function adjustTweetHeight(message: any) {
if (height) tweetHeight.value = height; if (height) tweetHeight.value = height;
} }
const openPlayer = (): void => { function openPlayer(): void {
os.popup(defineAsyncComponent(() => import('@/components/MkYouTubePlayer.vue')), { const { dispose } = os.popup(defineAsyncComponent(() => import('@/components/MkYouTubePlayer.vue')), {
url: requestUrl.href, url: requestUrl.href,
}, {
// TODO
}); });
}; }
(window as any).addEventListener('message', adjustTweetHeight); (window as any).addEventListener('message', adjustTweetHeight);

View File

@ -176,9 +176,11 @@ function setupComplete() {
function launchTutorial() { function launchTutorial() {
setupComplete(); setupComplete();
nextTick(() => { nextTick(() => {
os.popup(defineAsyncComponent(() => import('@/components/MkTutorialDialog.vue')), { const { dispose } = os.popup(defineAsyncComponent(() => import('@/components/MkTutorialDialog.vue')), {
initialPage: 1, initialPage: 1,
}, {}, 'closed'); }, {
closed: () => dispose(),
});
}); });
} }

View File

@ -74,15 +74,19 @@ misskeyApi('stats', {}).then((res) => {
}); });
function signin() { function signin() {
os.popup(XSigninDialog, { const { dispose } = os.popup(XSigninDialog, {
autoSet: true, autoSet: true,
}, {}, 'closed'); }, {
closed: () => dispose(),
});
} }
function signup() { function signup() {
os.popup(XSignupDialog, { const { dispose } = os.popup(XSignupDialog, {
autoSet: true, autoSet: true,
}, {}, 'closed'); }, {
closed: () => dispose(),
});
} }
function showMenu(ev) { function showMenu(ev) {

View File

@ -35,12 +35,10 @@ export const Default = {
// FIXME: 通るけどその後落ちるのでコメントアウト // FIXME: 通るけどその後落ちるのでコメントアウト
// await expect(a.href).toMatch(/^https?:\/\/.*#test$/); // await expect(a.href).toMatch(/^https?:\/\/.*#test$/);
await userEvent.pointer({ keys: '[MouseRight]', target: a }); await userEvent.pointer({ keys: '[MouseRight]', target: a });
await tick();
const menu = canvas.getByRole('menu'); const menu = canvas.getByRole('menu');
await expect(menu).toBeInTheDocument(); await expect(menu).toBeInTheDocument();
await userEvent.click(a); await userEvent.click(a);
a.blur(); a.blur();
await tick();
await expect(menu).not.toBeInTheDocument(); await expect(menu).not.toBeInTheDocument();
}, },
args: { args: {

View File

@ -9,12 +9,6 @@ import { StoryObj } from '@storybook/vue3';
import MkAd from './MkAd.vue'; import MkAd from './MkAd.vue';
import { i18n } from '@/i18n.js'; import { i18n } from '@/i18n.js';
let lock: Promise<undefined> | undefined;
function sleep(ms: number) {
return new Promise(resolve => setTimeout(resolve, ms));
}
const common = { const common = {
render(args) { render(args) {
return { return {
@ -37,56 +31,41 @@ const common = {
}; };
}, },
async play({ canvasElement, args }) { async play({ canvasElement, args }) {
if (lock) { const canvas = within(canvasElement);
console.warn('This test is unexpectedly running twice in parallel, fix it!'); const a = canvas.getByRole<HTMLAnchorElement>('link');
console.warn('See also: https://github.com/misskey-dev/misskey/issues/11267'); // FIXME: 通るけどその後落ちるのでコメントアウト
await lock; // await expect(a.href).toMatch(/^https?:\/\/.*#test$/);
const img = within(a).getByRole('img');
await expect(img).toBeInTheDocument();
let buttons = canvas.getAllByRole<HTMLButtonElement>('button');
await expect(buttons).toHaveLength(1);
const i = buttons[0];
await expect(i).toBeInTheDocument();
await userEvent.click(i);
await expect(canvasElement).toHaveTextContent(i18n.ts._ad.back);
await expect(a).not.toBeInTheDocument();
await expect(i).not.toBeInTheDocument();
buttons = canvas.getAllByRole<HTMLButtonElement>('button');
const hasReduceFrequency = args.specify?.ratio !== 0;
await expect(buttons).toHaveLength(hasReduceFrequency ? 2 : 1);
const reduce = hasReduceFrequency ? buttons[0] : null;
const back = buttons[hasReduceFrequency ? 1 : 0];
if (reduce) {
await expect(reduce).toBeInTheDocument();
await expect(reduce).toHaveTextContent(i18n.ts._ad.reduceFrequencyOfThisAd);
} }
await expect(back).toBeInTheDocument();
let resolve: (value?: any) => void; await expect(back).toHaveTextContent(i18n.ts._ad.back);
lock = new Promise(r => resolve = r); await userEvent.click(back);
await waitFor(() => expect(canvas.queryByRole('img')).toBeTruthy());
try { if (reduce) {
// NOTE: sleep しないと何故か落ちる await expect(reduce).not.toBeInTheDocument();
await sleep(100);
const canvas = within(canvasElement);
const a = canvas.getByRole<HTMLAnchorElement>('link');
// await expect(a.href).toMatch(/^https?:\/\/.*#test$/);
const img = within(a).getByRole('img');
await expect(img).toBeInTheDocument();
let buttons = canvas.getAllByRole<HTMLButtonElement>('button');
await expect(buttons).toHaveLength(1);
const i = buttons[0];
await expect(i).toBeInTheDocument();
await userEvent.click(i);
await expect(canvasElement).toHaveTextContent(i18n.ts._ad.back);
await expect(a).not.toBeInTheDocument();
await expect(i).not.toBeInTheDocument();
buttons = canvas.getAllByRole<HTMLButtonElement>('button');
const hasReduceFrequency = args.specify?.ratio !== 0;
await expect(buttons).toHaveLength(hasReduceFrequency ? 2 : 1);
const reduce = hasReduceFrequency ? buttons[0] : null;
const back = buttons[hasReduceFrequency ? 1 : 0];
if (reduce) {
await expect(reduce).toBeInTheDocument();
await expect(reduce).toHaveTextContent(i18n.ts._ad.reduceFrequencyOfThisAd);
}
await expect(back).toBeInTheDocument();
await expect(back).toHaveTextContent(i18n.ts._ad.back);
await userEvent.click(back);
await waitFor(() => expect(canvas.queryByRole('img')).toBeTruthy());
if (reduce) {
await expect(reduce).not.toBeInTheDocument();
}
await expect(back).not.toBeInTheDocument();
const aAgain = canvas.getByRole<HTMLAnchorElement>('link');
await expect(aAgain).toBeInTheDocument();
const imgAgain = within(aAgain).getByRole('img');
await expect(imgAgain).toBeInTheDocument();
} finally {
resolve!();
lock = undefined;
} }
await expect(back).not.toBeInTheDocument();
const aAgain = canvas.getByRole<HTMLAnchorElement>('link');
await expect(aAgain).toBeInTheDocument();
const imgAgain = within(aAgain).getByRole('img');
await expect(imgAgain).toBeInTheDocument();
}, },
args: { args: {
prefer: [], prefer: [],

View File

@ -106,12 +106,12 @@ function onClick(ev: MouseEvent) {
text: i18n.ts.info, text: i18n.ts.info,
icon: 'ti ti-info-circle', icon: 'ti ti-info-circle',
action: async () => { action: async () => {
os.popup(MkCustomEmojiDetailedDialog, { const { dispose } = os.popup(MkCustomEmojiDetailedDialog, {
emoji: await misskeyApiGet('emoji', { emoji: await misskeyApiGet('emoji', {
name: customEmojiName.value, name: customEmojiName.value,
}), }),
}, { }, {
anchor: ev.target, closed: () => dispose(),
}); });
}, },
}], ev.currentTarget ?? ev.target); }], ev.currentTarget ?? ev.target);

View File

@ -50,11 +50,13 @@ const el = ref();
if (props.showUrlPreview && isEnabledUrlPreview.value) { if (props.showUrlPreview && isEnabledUrlPreview.value) {
useTooltip(el, (showing) => { useTooltip(el, (showing) => {
os.popup(defineAsyncComponent(() => import('@/components/MkUrlPreviewPopup.vue')), { const { dispose } = os.popup(defineAsyncComponent(() => import('@/components/MkUrlPreviewPopup.vue')), {
showing, showing,
url: props.url, url: props.url,
source: el.value instanceof HTMLElement ? el.value : el.value?.$el, source: el.value instanceof HTMLElement ? el.value : el.value?.$el,
}, {}, 'closed'); }, {
closed: () => dispose(),
});
}); });
} }

View File

@ -17,7 +17,9 @@ export default {
const x = rect.left + (el.offsetWidth / 2); const x = rect.left + (el.offsetWidth / 2);
const y = rect.top + (el.offsetHeight / 2); const y = rect.top + (el.offsetHeight / 2);
popup(MkRippleEffect, { x, y }, {}, 'end'); const { dispose } = popup(MkRippleEffect, { x, y }, {
end: () => dispose(),
});
}); });
}, },
}; };

View File

@ -51,13 +51,15 @@ export default {
if (self.text == null) return; if (self.text == null) return;
const showing = ref(true); const showing = ref(true);
popup(defineAsyncComponent(() => import('@/components/MkTooltip.vue')), { const { dispose } = popup(defineAsyncComponent(() => import('@/components/MkTooltip.vue')), {
showing, showing,
text: self.text, text: self.text,
asMfm: binding.modifiers.mfm, asMfm: binding.modifiers.mfm,
direction: binding.modifiers.left ? 'left' : binding.modifiers.right ? 'right' : binding.modifiers.top ? 'top' : binding.modifiers.bottom ? 'bottom' : 'top', direction: binding.modifiers.left ? 'left' : binding.modifiers.right ? 'right' : binding.modifiers.top ? 'top' : binding.modifiers.bottom ? 'bottom' : 'top',
targetElement: el, targetElement: el,
}, {}, 'closed'); }, {
closed: () => dispose(),
});
self._close = () => { self._close = () => {
showing.value = false; showing.value = false;

View File

@ -35,7 +35,7 @@ export class UserPreview {
const showing = ref(true); const showing = ref(true);
popup(defineAsyncComponent(() => import('@/components/MkUserPopup.vue')), { const { dispose } = popup(defineAsyncComponent(() => import('@/components/MkUserPopup.vue')), {
showing, showing,
q: this.user, q: this.user,
source: this.el, source: this.el,
@ -47,7 +47,8 @@ export class UserPreview {
window.clearTimeout(this.showTimer); window.clearTimeout(this.showTimer);
this.hideTimer = window.setTimeout(this.close, 500); this.hideTimer = window.setTimeout(this.close, 500);
}, },
}, 'closed'); closed: () => dispose(),
});
this.promise = { this.promise = {
cancel: () => { cancel: () => {

View File

@ -116,11 +116,13 @@ export function promiseDialog<T extends Promise<any>>(
}); });
// NOTE: dynamic importすると挙動がおかしくなる(showingの変更が伝播しない) // NOTE: dynamic importすると挙動がおかしくなる(showingの変更が伝播しない)
popup(MkWaitingDialog, { const { dispose } = popup(MkWaitingDialog, {
success: success, success: success,
showing: showing, showing: showing,
text: text, text: text,
}, {}, 'closed'); }, {
closed: () => dispose(),
});
return promise; return promise;
} }
@ -166,28 +168,24 @@ type EmitsExtractor<T> = {
[K in keyof T as K extends `onVnode${string}` ? never : K extends `on${infer E}` ? Uncapitalize<E> : K extends string ? never : K]: T[K]; [K in keyof T as K extends `onVnode${string}` ? never : K extends `on${infer E}` ? Uncapitalize<E> : K extends string ? never : K]: T[K];
}; };
export async function popup<T extends Component>( export function popup<T extends Component>(
component: T, component: T,
props: ComponentProps<T>, props: ComponentProps<T>,
events: ComponentEmit<T> = {} as ComponentEmit<T>, events: ComponentEmit<T> = {} as ComponentEmit<T>,
disposeEvent?: keyof ComponentEmit<T>, ): { dispose: () => void } {
): Promise<{ dispose: () => void }> {
markRaw(component); markRaw(component);
const id = ++popupIdCount; const id = ++popupIdCount;
const dispose = () => { const dispose = () => {
// このsetTimeoutが無いと挙動がおかしくなる(autocompleteが閉じなくなる)。Vueのバグ // このsetTimeoutが無いと挙動がおかしくなる(autocompleteが閉じなくなる)。Vueのバグ
window.setTimeout(() => { window.setTimeout(() => {
popups.value = popups.value.filter(popup => popup.id !== id); popups.value = popups.value.filter(p => p.id !== id);
}, 0); }, 0);
}; };
const state = { const state = {
component, component,
props, props,
events: disposeEvent ? { events,
...events,
[disposeEvent]: dispose,
} : events,
id, id,
}; };
@ -199,15 +197,19 @@ export async function popup<T extends Component>(
} }
export function pageWindow(path: string) { export function pageWindow(path: string) {
popup(MkPageWindow, { const { dispose } = popup(MkPageWindow, {
initialPath: path, initialPath: path,
}, {}, 'closed'); }, {
closed: () => dispose(),
});
} }
export function toast(message: string) { export function toast(message: string) {
popup(MkToast, { const { dispose } = popup(MkToast, {
message, message,
}, {}, 'closed'); }, {
closed: () => dispose(),
});
} }
export function alert(props: { export function alert(props: {
@ -216,11 +218,12 @@ export function alert(props: {
text?: string; text?: string;
}): Promise<void> { }): Promise<void> {
return new Promise(resolve => { return new Promise(resolve => {
popup(MkDialog, props, { const { dispose } = popup(MkDialog, props, {
done: () => { done: () => {
resolve(); resolve();
}, },
}, 'closed'); closed: () => dispose(),
});
}); });
} }
@ -232,14 +235,15 @@ export function confirm(props: {
cancelText?: string; cancelText?: string;
}): Promise<{ canceled: boolean }> { }): Promise<{ canceled: boolean }> {
return new Promise(resolve => { return new Promise(resolve => {
popup(MkDialog, { const { dispose } = popup(MkDialog, {
...props, ...props,
showCancelButton: true, showCancelButton: true,
}, { }, {
done: result => { done: result => {
resolve(result ? result : { canceled: true }); resolve(result ? result : { canceled: true });
}, },
}, 'closed'); closed: () => dispose(),
});
}); });
} }
@ -261,7 +265,7 @@ export function actions<T extends {
canceled: false; result: T[number]['value']; canceled: false; result: T[number]['value'];
}> { }> {
return new Promise(resolve => { return new Promise(resolve => {
popup(MkDialog, { const { dispose } = popup(MkDialog, {
...props, ...props,
actions: props.actions.map(a => ({ actions: props.actions.map(a => ({
text: a.text, text: a.text,
@ -275,7 +279,8 @@ export function actions<T extends {
done: result => { done: result => {
resolve(result ? result : { canceled: true }); resolve(result ? result : { canceled: true });
}, },
}, 'closed'); closed: () => dispose(),
});
}); });
} }
@ -323,7 +328,7 @@ export function inputText(props: {
canceled: false; result: string | null; canceled: false; result: string | null;
}> { }> {
return new Promise(resolve => { return new Promise(resolve => {
popup(MkDialog, { const { dispose } = popup(MkDialog, {
title: props.title, title: props.title,
text: props.text, text: props.text,
input: { input: {
@ -338,7 +343,8 @@ export function inputText(props: {
done: result => { done: result => {
resolve(result ? result : { canceled: true }); resolve(result ? result : { canceled: true });
}, },
}, 'closed'); closed: () => dispose(),
});
}); });
} }
@ -377,7 +383,7 @@ export function inputNumber(props: {
canceled: false; result: number | null; canceled: false; result: number | null;
}> { }> {
return new Promise(resolve => { return new Promise(resolve => {
popup(MkDialog, { const { dispose } = popup(MkDialog, {
title: props.title, title: props.title,
text: props.text, text: props.text,
input: { input: {
@ -390,7 +396,8 @@ export function inputNumber(props: {
done: result => { done: result => {
resolve(result ? result : { canceled: true }); resolve(result ? result : { canceled: true });
}, },
}, 'closed'); closed: () => dispose(),
});
}); });
} }
@ -405,7 +412,7 @@ export function inputDate(props: {
canceled: false; result: Date; canceled: false; result: Date;
}> { }> {
return new Promise(resolve => { return new Promise(resolve => {
popup(MkDialog, { const { dispose } = popup(MkDialog, {
title: props.title, title: props.title,
text: props.text, text: props.text,
input: { input: {
@ -417,7 +424,8 @@ export function inputDate(props: {
done: result => { done: result => {
resolve(result ? { result: new Date(result.result), canceled: false } : { result: undefined, canceled: true }); resolve(result ? { result: new Date(result.result), canceled: false } : { result: undefined, canceled: true });
}, },
}, 'closed'); closed: () => dispose(),
});
}); });
} }
@ -427,11 +435,12 @@ export function authenticateDialog(): Promise<{
canceled: false; result: { password: string; token: string | null; }; canceled: false; result: { password: string; token: string | null; };
}> { }> {
return new Promise(resolve => { return new Promise(resolve => {
popup(MkPasswordDialog, {}, { const { dispose } = popup(MkPasswordDialog, {}, {
done: result => { done: result => {
resolve(result ? { canceled: false, result } : { canceled: true, result: undefined }); resolve(result ? { canceled: false, result } : { canceled: true, result: undefined });
}, },
}, 'closed'); closed: () => dispose(),
});
}); });
} }
@ -476,7 +485,7 @@ export function select<C = any>(props: {
canceled: false; result: C | null; canceled: false; result: C | null;
}> { }> {
return new Promise(resolve => { return new Promise(resolve => {
popup(MkDialog, { const { dispose } = popup(MkDialog, {
title: props.title, title: props.title,
text: props.text, text: props.text,
select: { select: {
@ -487,7 +496,8 @@ export function select<C = any>(props: {
done: result => { done: result => {
resolve(result ? result : { canceled: true }); resolve(result ? result : { canceled: true });
}, },
}, 'closed'); closed: () => dispose(),
});
}); });
} }
@ -497,53 +507,57 @@ export function success(): Promise<void> {
window.setTimeout(() => { window.setTimeout(() => {
showing.value = false; showing.value = false;
}, 1000); }, 1000);
popup(MkWaitingDialog, { const { dispose } = popup(MkWaitingDialog, {
success: true, success: true,
showing: showing, showing: showing,
}, { }, {
done: () => resolve(), done: () => resolve(),
}, 'closed'); closed: () => dispose(),
});
}); });
} }
export function waiting(): Promise<void> { export function waiting(): Promise<void> {
return new Promise(resolve => { return new Promise(resolve => {
const showing = ref(true); const showing = ref(true);
popup(MkWaitingDialog, { const { dispose } = popup(MkWaitingDialog, {
success: false, success: false,
showing: showing, showing: showing,
}, { }, {
done: () => resolve(), done: () => resolve(),
}, 'closed'); closed: () => dispose(),
});
}); });
} }
export function form<F extends Form>(title: string, f: F): Promise<{ canceled: true, result?: undefined } | { canceled?: false, result: GetFormResultType<F> }> { export function form<F extends Form>(title: string, f: F): Promise<{ canceled: true, result?: undefined } | { canceled?: false, result: GetFormResultType<F> }> {
return new Promise(resolve => { return new Promise(resolve => {
popup(defineAsyncComponent(() => import('@/components/MkFormDialog.vue')), { title, form: f }, { const { dispose } = popup(defineAsyncComponent(() => import('@/components/MkFormDialog.vue')), { title, form: f }, {
done: result => { done: result => {
resolve(result); resolve(result);
}, },
}, 'closed'); closed: () => dispose(),
});
}); });
} }
export async function selectUser(opts: { includeSelf?: boolean; localOnly?: boolean; } = {}): Promise<Misskey.entities.UserDetailed> { export async function selectUser(opts: { includeSelf?: boolean; localOnly?: boolean; } = {}): Promise<Misskey.entities.UserDetailed> {
return new Promise(resolve => { return new Promise(resolve => {
popup(defineAsyncComponent(() => import('@/components/MkUserSelectDialog.vue')), { const { dispose } = popup(defineAsyncComponent(() => import('@/components/MkUserSelectDialog.vue')), {
includeSelf: opts.includeSelf, includeSelf: opts.includeSelf,
localOnly: opts.localOnly, localOnly: opts.localOnly,
}, { }, {
ok: user => { ok: user => {
resolve(user); resolve(user);
}, },
}, 'closed'); closed: () => dispose(),
});
}); });
} }
export async function selectDriveFile(multiple: boolean): Promise<Misskey.entities.DriveFile[]> { export async function selectDriveFile(multiple: boolean): Promise<Misskey.entities.DriveFile[]> {
return new Promise(resolve => { return new Promise(resolve => {
popup(defineAsyncComponent(() => import('@/components/MkDriveSelectDialog.vue')), { const { dispose } = popup(defineAsyncComponent(() => import('@/components/MkDriveSelectDialog.vue')), {
type: 'file', type: 'file',
multiple, multiple,
}, { }, {
@ -552,13 +566,14 @@ export async function selectDriveFile(multiple: boolean): Promise<Misskey.entiti
resolve(files); resolve(files);
} }
}, },
}, 'closed'); closed: () => dispose(),
});
}); });
} }
export async function selectDriveFolder(multiple: boolean): Promise<Misskey.entities.DriveFolder[]> { export async function selectDriveFolder(multiple: boolean): Promise<Misskey.entities.DriveFolder[]> {
return new Promise(resolve => { return new Promise(resolve => {
popup(defineAsyncComponent(() => import('@/components/MkDriveSelectDialog.vue')), { const { dispose } = popup(defineAsyncComponent(() => import('@/components/MkDriveSelectDialog.vue')), {
type: 'folder', type: 'folder',
multiple, multiple,
}, { }, {
@ -567,20 +582,22 @@ export async function selectDriveFolder(multiple: boolean): Promise<Misskey.enti
resolve(folders); resolve(folders);
} }
}, },
}, 'closed'); closed: () => dispose(),
});
}); });
} }
export async function pickEmoji(src: HTMLElement, opts: ComponentProps<typeof MkEmojiPickerDialog>): Promise<string> { export async function pickEmoji(src: HTMLElement, opts: ComponentProps<typeof MkEmojiPickerDialog>): Promise<string> {
return new Promise(resolve => { return new Promise(resolve => {
popup(MkEmojiPickerDialog, { const { dispose } = popup(MkEmojiPickerDialog, {
src, src,
...opts, ...opts,
}, { }, {
done: emoji => { done: emoji => {
resolve(emoji); resolve(emoji);
}, },
}, 'closed'); closed: () => dispose(),
});
}); });
} }
@ -589,7 +606,7 @@ export async function cropImage(image: Misskey.entities.DriveFile, options: {
uploadFolder?: string | null; uploadFolder?: string | null;
}): Promise<Misskey.entities.DriveFile> { }): Promise<Misskey.entities.DriveFile> {
return new Promise(resolve => { return new Promise(resolve => {
popup(defineAsyncComponent(() => import('@/components/MkCropperDialog.vue')), { const { dispose } = popup(defineAsyncComponent(() => import('@/components/MkCropperDialog.vue')), {
file: image, file: image,
aspectRatio: options.aspectRatio, aspectRatio: options.aspectRatio,
uploadFolder: options.uploadFolder, uploadFolder: options.uploadFolder,
@ -597,7 +614,8 @@ export async function cropImage(image: Misskey.entities.DriveFile, options: {
ok: x => { ok: x => {
resolve(x); resolve(x);
}, },
}, 'closed'); closed: () => dispose(),
});
}); });
} }
@ -608,8 +626,7 @@ export function popupMenu(items: MenuItem[], src?: HTMLElement | EventTarget | n
onClosing?: () => void; onClosing?: () => void;
}): Promise<void> { }): Promise<void> {
return new Promise(resolve => { return new Promise(resolve => {
let dispose; const { dispose } = popup(MkPopupMenu, {
popup(MkPopupMenu, {
items, items,
src, src,
width: options?.width, width: options?.width,
@ -623,8 +640,6 @@ export function popupMenu(items: MenuItem[], src?: HTMLElement | EventTarget | n
closing: () => { closing: () => {
if (options?.onClosing) options.onClosing(); if (options?.onClosing) options.onClosing();
}, },
}).then(res => {
dispose = res.dispose;
}); });
}); });
} }
@ -632,8 +647,7 @@ export function popupMenu(items: MenuItem[], src?: HTMLElement | EventTarget | n
export function contextMenu(items: MenuItem[], ev: MouseEvent): Promise<void> { export function contextMenu(items: MenuItem[], ev: MouseEvent): Promise<void> {
ev.preventDefault(); ev.preventDefault();
return new Promise(resolve => { return new Promise(resolve => {
let dispose; const { dispose } = popup(MkContextMenu, {
popup(MkContextMenu, {
items, items,
ev, ev,
}, { }, {
@ -641,8 +655,6 @@ export function contextMenu(items: MenuItem[], ev: MouseEvent): Promise<void> {
resolve(); resolve();
dispose(); dispose();
}, },
}).then(res => {
dispose = res.dispose;
}); });
}); });
} }
@ -656,14 +668,11 @@ export function post(props: Record<string, any> = {}): Promise<void> {
// Vueが渡されたコンポーネントに内部的に__propsというプロパティを生やす影響で、 // Vueが渡されたコンポーネントに内部的に__propsというプロパティを生やす影響で、
// 複数のpost formを開いたときに場合によってはエラーになる // 複数のpost formを開いたときに場合によってはエラーになる
// もちろん複数のpost formを開けること自体Misskeyサイドのバグなのだが // もちろん複数のpost formを開けること自体Misskeyサイドのバグなのだが
let dispose; const { dispose } = popup(MkPostFormDialog, props, {
popup(MkPostFormDialog, props, {
closed: () => { closed: () => {
resolve(); resolve();
dispose(); dispose();
}, },
}).then(res => {
dispose = res.dispose;
}); });
}); });
} }

View File

@ -464,16 +464,20 @@ function toggleRoleItem(role) {
} }
function createAnnouncement() { function createAnnouncement() {
os.popup(defineAsyncComponent(() => import('@/components/MkUserAnnouncementEditDialog.vue')), { const { dispose } = os.popup(defineAsyncComponent(() => import('@/components/MkUserAnnouncementEditDialog.vue')), {
user: user.value, user: user.value,
}, {}, 'closed'); }, {
closed: () => dispose(),
});
} }
function editAnnouncement(announcement) { function editAnnouncement(announcement) {
os.popup(defineAsyncComponent(() => import('@/components/MkUserAnnouncementEditDialog.vue')), { const { dispose } = os.popup(defineAsyncComponent(() => import('@/components/MkUserAnnouncementEditDialog.vue')), {
user: user.value, user: user.value,
announcement, announcement,
}, {}, 'closed'); }, {
closed: () => dispose(),
});
} }
watch(() => props.userId, () => { watch(() => props.userId, () => {

View File

@ -109,7 +109,7 @@ async function onDeleteButtonClicked(id: string) {
async function showEditor(mode: 'create' | 'edit', id?: string) { async function showEditor(mode: 'create' | 'edit', id?: string) {
const { dispose, needLoad } = await new Promise<{ dispose: () => void, needLoad: boolean }>(async resolve => { const { dispose, needLoad } = await new Promise<{ dispose: () => void, needLoad: boolean }>(async resolve => {
const res = await os.popup( const { dispose: _dispose } = os.popup(
defineAsyncComponent(() => import('./notification-recipient.editor.vue')), defineAsyncComponent(() => import('./notification-recipient.editor.vue')),
{ {
mode, mode,
@ -117,10 +117,10 @@ async function showEditor(mode: 'create' | 'edit', id?: string) {
}, },
{ {
submitted: async () => { submitted: async () => {
resolve({ dispose: res.dispose, needLoad: true }); resolve({ dispose: _dispose, needLoad: true });
}, },
closed: () => { closed: () => {
resolve({ dispose: res.dispose, needLoad: false }); resolve({ dispose: _dispose, needLoad: false });
}, },
}, },
); );

View File

@ -129,18 +129,19 @@ const toggleSelect = (emoji) => {
}; };
const add = async (ev: MouseEvent) => { const add = async (ev: MouseEvent) => {
os.popup(defineAsyncComponent(() => import('./emoji-edit-dialog.vue')), { const { dispose } = os.popup(defineAsyncComponent(() => import('./emoji-edit-dialog.vue')), {
}, { }, {
done: result => { done: result => {
if (result.created) { if (result.created) {
emojisPaginationComponent.value.prepend(result.created); emojisPaginationComponent.value.prepend(result.created);
} }
}, },
}, 'closed'); closed: () => dispose(),
});
}; };
const edit = (emoji) => { const edit = (emoji) => {
os.popup(defineAsyncComponent(() => import('./emoji-edit-dialog.vue')), { const { dispose } = os.popup(defineAsyncComponent(() => import('./emoji-edit-dialog.vue')), {
emoji: emoji, emoji: emoji,
}, { }, {
done: result => { done: result => {
@ -153,7 +154,8 @@ const edit = (emoji) => {
emojisPaginationComponent.value.removeItem(emoji.id); emojisPaginationComponent.value.removeItem(emoji.id);
} }
}, },
}, 'closed'); closed: () => dispose(),
});
}; };
const importEmoji = (emoji) => { const importEmoji = (emoji) => {

View File

@ -160,7 +160,7 @@ function rename() {
function describe() { function describe() {
if (!file.value) return; if (!file.value) return;
os.popup(defineAsyncComponent(() => import('@/components/MkFileCaptionEditWindow.vue')), { const { dispose } = os.popup(defineAsyncComponent(() => import('@/components/MkFileCaptionEditWindow.vue')), {
default: file.value.comment ?? '', default: file.value.comment ?? '',
file: file.value, file: file.value,
}, { }, {
@ -172,7 +172,8 @@ function describe() {
await fetch(); await fetch();
}); });
}, },
}, 'closed'); closed: () => dispose(),
});
} }
async function deleteFile() { async function deleteFile() {

View File

@ -1008,8 +1008,18 @@ function attachGameEvents() {
const domX = rect.left + (x * viewScale); const domX = rect.left + (x * viewScale);
const domY = rect.top + (y * viewScale); const domY = rect.top + (y * viewScale);
const scoreUnit = getScoreUnit(props.gameMode); const scoreUnit = getScoreUnit(props.gameMode);
os.popup(MkRippleEffect, { x: domX, y: domY }, {}, 'end');
os.popup(MkPlusOneEffect, { x: domX, y: domY, value: scoreDelta + (scoreUnit === 'pt' ? '' : scoreUnit) }, {}, 'end'); {
const { dispose } = os.popup(MkRippleEffect, { x: domX, y: domY }, {
end: () => dispose(),
});
}
{
const { dispose } = os.popup(MkPlusOneEffect, { x: domX, y: domY, value: scoreDelta + (scoreUnit === 'pt' ? '' : scoreUnit) }, {
end: () => dispose(),
});
}
if (nextMono) { if (nextMono) {
const def = monoDefinitions.value.find(x => x.id === nextMono.id)!; const def = monoDefinitions.value.find(x => x.id === nextMono.id)!;

View File

@ -14,8 +14,8 @@ SPDX-License-Identifier: AGPL-3.0-only
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import * as os from '@/os.js';
import * as Misskey from 'misskey-js'; import * as Misskey from 'misskey-js';
import * as os from '@/os.js';
import { misskeyApiGet } from '@/scripts/misskey-api.js'; import { misskeyApiGet } from '@/scripts/misskey-api.js';
import copyToClipboard from '@/scripts/copy-to-clipboard.js'; import copyToClipboard from '@/scripts/copy-to-clipboard.js';
import { i18n } from '@/i18n.js'; import { i18n } from '@/i18n.js';
@ -40,12 +40,12 @@ function menu(ev) {
text: i18n.ts.info, text: i18n.ts.info,
icon: 'ti ti-info-circle', icon: 'ti ti-info-circle',
action: async () => { action: async () => {
os.popup(MkCustomEmojiDetailedDialog, { const { dispose } = os.popup(MkCustomEmojiDetailedDialog, {
emoji: await misskeyApiGet('emoji', { emoji: await misskeyApiGet('emoji', {
name: props.emoji.name, name: props.emoji.name,
}) }),
}, { }, {
anchor: ev.target, closed: () => dispose(),
}); });
}, },
}], ev.currentTarget ?? ev.target); }], ev.currentTarget ?? ev.target);

View File

@ -44,7 +44,9 @@ async function save() {
onMounted(() => { onMounted(() => {
if (props.token == null) { if (props.token == null) {
os.popup(defineAsyncComponent(() => import('@/components/MkForgotPassword.vue')), {}, {}, 'closed'); const { dispose } = os.popup(defineAsyncComponent(() => import('@/components/MkForgotPassword.vue')), {}, {
closed: () => dispose(),
});
mainRouter.push('/'); mainRouter.push('/');
} }
}); });

View File

@ -108,9 +108,11 @@ async function registerTOTP(): Promise<void> {
token: auth.result.token, token: auth.result.token,
}); });
os.popup(defineAsyncComponent(() => import('./2fa.qrdialog.vue')), { const { dispose } = os.popup(defineAsyncComponent(() => import('./2fa.qrdialog.vue')), {
twoFactorData, twoFactorData,
}, {}, 'closed'); }, {
closed: () => dispose(),
});
} }
async function unregisterTOTP(): Promise<void> { async function unregisterTOTP(): Promise<void> {

View File

@ -74,22 +74,24 @@ async function removeAccount(account) {
} }
function addExistingAccount() { function addExistingAccount() {
os.popup(defineAsyncComponent(() => import('@/components/MkSigninDialog.vue')), {}, { const { dispose } = os.popup(defineAsyncComponent(() => import('@/components/MkSigninDialog.vue')), {}, {
done: async res => { done: async res => {
await addAccounts(res.id, res.i); await addAccounts(res.id, res.i);
os.success(); os.success();
init(); init();
}, },
}, 'closed'); closed: () => dispose(),
});
} }
function createAccount() { function createAccount() {
os.popup(defineAsyncComponent(() => import('@/components/MkSignupDialog.vue')), {}, { const { dispose } = os.popup(defineAsyncComponent(() => import('@/components/MkSignupDialog.vue')), {}, {
done: async res => { done: async res => {
await addAccounts(res.id, res.i); await addAccounts(res.id, res.i);
switchAccountWithToken(res.i); switchAccountWithToken(res.i);
}, },
}, 'closed'); closed: () => dispose(),
});
} }
async function switchAccount(account: any) { async function switchAccount(account: any) {

View File

@ -23,7 +23,7 @@ import { definePageMetadata } from '@/scripts/page-metadata.js';
const isDesktop = ref(window.innerWidth >= 1100); const isDesktop = ref(window.innerWidth >= 1100);
function generateToken() { function generateToken() {
os.popup(defineAsyncComponent(() => import('@/components/MkTokenGenerateWindow.vue')), {}, { const { dispose } = os.popup(defineAsyncComponent(() => import('@/components/MkTokenGenerateWindow.vue')), {}, {
done: async result => { done: async result => {
const { name, permissions } = result; const { name, permissions } = result;
const { token } = await misskeyApi('miauth/gen-token', { const { token } = await misskeyApi('miauth/gen-token', {
@ -38,7 +38,8 @@ function generateToken() {
text: token, text: token,
}); });
}, },
}, 'closed'); closed: () => dispose(),
});
} }
const headerActions = computed(() => []); const headerActions = computed(() => []);

View File

@ -67,7 +67,7 @@ misskeyApi('get-avatar-decorations').then(_avatarDecorations => {
}); });
function openDecoration(avatarDecoration, index?: number) { function openDecoration(avatarDecoration, index?: number) {
os.popup(defineAsyncComponent(() => import('./avatar-decoration.dialog.vue')), { const { dispose } = os.popup(defineAsyncComponent(() => import('./avatar-decoration.dialog.vue')), {
decoration: avatarDecoration, decoration: avatarDecoration,
usingIndex: index, usingIndex: index,
}, { }, {
@ -108,7 +108,8 @@ function openDecoration(avatarDecoration, index?: number) {
}); });
$i.avatarDecorations = update; $i.avatarDecorations = update;
}, },
}, 'closed'); closed: () => dispose(),
});
} }
function detachAllDecorations() { function detachAllDecorations() {

View File

@ -27,7 +27,7 @@ function rename(file: Misskey.entities.DriveFile) {
} }
function describe(file: Misskey.entities.DriveFile) { function describe(file: Misskey.entities.DriveFile) {
os.popup(defineAsyncComponent(() => import('@/components/MkFileCaptionEditWindow.vue')), { const { dispose } = os.popup(defineAsyncComponent(() => import('@/components/MkFileCaptionEditWindow.vue')), {
default: file.comment ?? '', default: file.comment ?? '',
file: file, file: file,
}, { }, {
@ -37,7 +37,8 @@ function describe(file: Misskey.entities.DriveFile) {
comment: caption.length === 0 ? null : caption, comment: caption.length === 0 ? null : caption,
}); });
}, },
}, 'closed'); closed: () => dispose(),
});
} }
function toggleSensitive(file: Misskey.entities.DriveFile) { function toggleSensitive(file: Misskey.entities.DriveFile) {

View File

@ -136,10 +136,12 @@ export function getAbuseNoteMenu(note: Misskey.entities.Note, text: string): Men
let noteInfo = ''; let noteInfo = '';
if (note.url ?? note.uri != null) noteInfo = `Note: ${note.url ?? note.uri}\n`; if (note.url ?? note.uri != null) noteInfo = `Note: ${note.url ?? note.uri}\n`;
noteInfo += `Local Note: ${localUrl}\n`; noteInfo += `Local Note: ${localUrl}\n`;
os.popup(defineAsyncComponent(() => import('@/components/MkAbuseReportWindow.vue')), { const { dispose } = os.popup(defineAsyncComponent(() => import('@/components/MkAbuseReportWindow.vue')), {
user: note.user, user: note.user,
initialComment: `${noteInfo}-----\n`, initialComment: `${noteInfo}-----\n`,
}, {}, 'closed'); }, {
closed: () => dispose(),
});
}, },
}; };
} }
@ -530,7 +532,9 @@ export function getRenoteMenu(props: {
const rect = el.getBoundingClientRect(); const rect = el.getBoundingClientRect();
const x = rect.left + (el.offsetWidth / 2); const x = rect.left + (el.offsetWidth / 2);
const y = rect.top + (el.offsetHeight / 2); const y = rect.top + (el.offsetHeight / 2);
os.popup(MkRippleEffect, { x, y }, {}, 'end'); const { dispose } = os.popup(MkRippleEffect, { x, y }, {
end: () => dispose(),
});
} }
if (!props.mock) { if (!props.mock) {
@ -566,7 +570,9 @@ export function getRenoteMenu(props: {
const rect = el.getBoundingClientRect(); const rect = el.getBoundingClientRect();
const x = rect.left + (el.offsetWidth / 2); const x = rect.left + (el.offsetWidth / 2);
const y = rect.top + (el.offsetHeight / 2); const y = rect.top + (el.offsetHeight / 2);
os.popup(MkRippleEffect, { x, y }, {}, 'end'); const { dispose } = os.popup(MkRippleEffect, { x, y }, {
end: () => dispose(),
});
} }
const configuredVisibility = defaultStore.state.rememberNoteVisibility ? defaultStore.state.visibility : defaultStore.state.defaultNoteVisibility; const configuredVisibility = defaultStore.state.rememberNoteVisibility ? defaultStore.state.visibility : defaultStore.state.defaultNoteVisibility;
@ -615,7 +621,9 @@ export function getRenoteMenu(props: {
const rect = el.getBoundingClientRect(); const rect = el.getBoundingClientRect();
const x = rect.left + (el.offsetWidth / 2); const x = rect.left + (el.offsetWidth / 2);
const y = rect.top + (el.offsetHeight / 2); const y = rect.top + (el.offsetHeight / 2);
os.popup(MkRippleEffect, { x, y }, {}, 'end'); const { dispose } = os.popup(MkRippleEffect, { x, y }, {
end: () => dispose(),
});
} }
if (!props.mock) { if (!props.mock) {

View File

@ -100,9 +100,11 @@ export function getUserMenu(user: Misskey.entities.UserDetailed, router: IRouter
} }
function reportAbuse() { function reportAbuse() {
os.popup(defineAsyncComponent(() => import('@/components/MkAbuseReportWindow.vue')), { const { dispose } = os.popup(defineAsyncComponent(() => import('@/components/MkAbuseReportWindow.vue')), {
user: user, user: user,
}, {}, 'closed'); }, {
closed: () => dispose(),
});
} }
async function getConfirmed(text: string): Promise<boolean> { async function getConfirmed(text: string): Promise<boolean> {

View File

@ -103,7 +103,7 @@ export async function installPlugin(code: string, meta?: AiScriptPluginMeta) {
} }
const token = realMeta.permissions == null || realMeta.permissions.length === 0 ? null : await new Promise((res, rej) => { const token = realMeta.permissions == null || realMeta.permissions.length === 0 ? null : await new Promise((res, rej) => {
os.popup(defineAsyncComponent(() => import('@/components/MkTokenGenerateWindow.vue')), { const { dispose } = os.popup(defineAsyncComponent(() => import('@/components/MkTokenGenerateWindow.vue')), {
title: i18n.ts.tokenRequested, title: i18n.ts.tokenRequested,
information: i18n.ts.pluginTokenRequestedDescription, information: i18n.ts.pluginTokenRequestedDescription,
initialName: realMeta.name, initialName: realMeta.name,
@ -118,7 +118,8 @@ export async function installPlugin(code: string, meta?: AiScriptPluginMeta) {
}); });
res(token); res(token);
}, },
}, 'closed'); closed: () => dispose(),
});
}); });
savePlugin({ savePlugin({

View File

@ -11,7 +11,7 @@ import { popup } from '@/os.js';
export function pleaseLogin(path?: string) { export function pleaseLogin(path?: string) {
if ($i) return; if ($i) return;
popup(defineAsyncComponent(() => import('@/components/MkSigninDialog.vue')), { const { dispose } = popup(defineAsyncComponent(() => import('@/components/MkSigninDialog.vue')), {
autoSet: true, autoSet: true,
message: i18n.ts.signinRequired, message: i18n.ts.signinRequired,
}, { }, {
@ -20,7 +20,8 @@ export function pleaseLogin(path?: string) {
window.location.href = path; window.location.href = path;
} }
}, },
}, 'closed'); closed: () => dispose(),
});
throw new Error('signin required'); throw new Error('signin required');
} }

View File

@ -7,13 +7,3 @@ export async function tick(): Promise<void> {
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
await new Promise((globalThis.requestIdleCallback ?? setTimeout) as never); await new Promise((globalThis.requestIdleCallback ?? setTimeout) as never);
} }
/**
* @see https://github.com/misskey-dev/misskey/issues/11267
*/
export function semaphore(counter = 0, waiting: (() => void)[] = []) {
return {
acquire: () => ++counter > 1 && new Promise<void>(resolve => waiting.push(resolve)),
release: () => --counter && waiting.pop()?.(),
};
}

View File

@ -17,20 +17,16 @@ export function useChartTooltip(opts: { position: 'top' | 'middle' } = { positio
borderColor: string; borderColor: string;
text: string; text: string;
}[] | null>(null); }[] | null>(null);
let disposeTooltipComponent; const { dispose: disposeTooltipComponent } = os.popup(MkChartTooltip, {
os.popup(MkChartTooltip, {
showing: tooltipShowing, showing: tooltipShowing,
x: tooltipX, x: tooltipX,
y: tooltipY, y: tooltipY,
title: tooltipTitle, title: tooltipTitle,
series: tooltipSeries, series: tooltipSeries,
}, {}).then(({ dispose }) => { }, {});
disposeTooltipComponent = dispose;
});
onUnmounted(() => { onUnmounted(() => {
if (disposeTooltipComponent) disposeTooltipComponent(); disposeTooltipComponent();
}); });
onDeactivated(() => { onDeactivated(() => {

View File

@ -112,7 +112,9 @@ export function openInstanceMenu(ev: MouseEvent) {
text: i18n.ts._initialTutorial.launchTutorial, text: i18n.ts._initialTutorial.launchTutorial,
icon: 'ti ti-presentation', icon: 'ti ti-presentation',
action: () => { action: () => {
os.popup(defineAsyncComponent(() => import('@/components/MkTutorialDialog.vue')), {}, {}, 'closed'); const { dispose } = os.popup(defineAsyncComponent(() => import('@/components/MkTutorialDialog.vue')), {}, {
closed: () => dispose(),
});
}, },
} : undefined, { } : undefined, {
type: 'link', type: 'link',

View File

@ -74,8 +74,9 @@ function openAccountMenu(ev: MouseEvent) {
} }
function more() { function more() {
os.popup(defineAsyncComponent(() => import('@/components/MkLaunchPad.vue')), {}, { const { dispose } = os.popup(defineAsyncComponent(() => import('@/components/MkLaunchPad.vue')), {}, {
}, 'closed'); closed: () => dispose(),
});
} }
</script> </script>

View File

@ -99,10 +99,11 @@ function openAccountMenu(ev: MouseEvent) {
} }
function more(ev: MouseEvent) { function more(ev: MouseEvent) {
os.popup(defineAsyncComponent(() => import('@/components/MkLaunchPad.vue')), { const { dispose } = os.popup(defineAsyncComponent(() => import('@/components/MkLaunchPad.vue')), {
src: ev.currentTarget ?? ev.target, src: ev.currentTarget ?? ev.target,
}, { }, {
}, 'closed'); closed: () => dispose(),
});
} }
</script> </script>

View File

@ -71,11 +71,12 @@ const otherNavItemIndicated = computed<boolean>(() => {
}); });
function more(ev: MouseEvent) { function more(ev: MouseEvent) {
os.popup(defineAsyncComponent(() => import('@/components/MkLaunchPad.vue')), { const { dispose } = os.popup(defineAsyncComponent(() => import('@/components/MkLaunchPad.vue')), {
src: ev.currentTarget ?? ev.target, src: ev.currentTarget ?? ev.target,
anchor: { x: 'center', y: 'bottom' }, anchor: { x: 'center', y: 'bottom' },
}, { }, {
}, 'closed'); closed: () => dispose(),
});
} }
function openAccountMenu(ev: MouseEvent) { function openAccountMenu(ev: MouseEvent) {

View File

@ -86,9 +86,11 @@ function calcViewState() {
} }
function more(ev: MouseEvent) { function more(ev: MouseEvent) {
os.popup(defineAsyncComponent(() => import('@/components/MkLaunchPad.vue')), { const { dispose } = os.popup(defineAsyncComponent(() => import('@/components/MkLaunchPad.vue')), {
src: ev.currentTarget ?? ev.target, src: ev.currentTarget ?? ev.target,
}, {}, 'closed'); }, {
closed: () => dispose(),
});
} }
function openAccountMenu(ev: MouseEvent) { function openAccountMenu(ev: MouseEvent) {

View File

@ -27,7 +27,7 @@ const props = defineProps<{
const notificationsComponent = shallowRef<InstanceType<typeof XNotifications>>(); const notificationsComponent = shallowRef<InstanceType<typeof XNotifications>>();
function func() { function func() {
os.popup(defineAsyncComponent(() => import('@/components/MkNotificationSelectWindow.vue')), { const { dispose } = os.popup(defineAsyncComponent(() => import('@/components/MkNotificationSelectWindow.vue')), {
excludeTypes: props.column.excludeTypes, excludeTypes: props.column.excludeTypes,
}, { }, {
done: async (res) => { done: async (res) => {
@ -36,7 +36,8 @@ function func() {
excludeTypes: excludeTypes, excludeTypes: excludeTypes,
}); });
}, },
}, 'closed'); closed: () => dispose(),
});
} }
const menu = [{ const menu = [{

View File

@ -126,15 +126,19 @@ const keymap = computed(() => {
}); });
function signin() { function signin() {
os.popup(XSigninDialog, { const { dispose } = os.popup(XSigninDialog, {
autoSet: true, autoSet: true,
}, {}, 'closed'); }, {
closed: () => dispose(),
});
} }
function signup() { function signup() {
os.popup(XSignupDialog, { const { dispose } = os.popup(XSignupDialog, {
autoSet: true, autoSet: true,
}, {}, 'closed'); }, {
closed: () => dispose(),
});
} }
onMounted(() => { onMounted(() => {

View File

@ -54,7 +54,7 @@ const { widgetProps, configure, save } = useWidgetPropsManager(name,
); );
const configureNotification = () => { const configureNotification = () => {
os.popup(defineAsyncComponent(() => import('@/components/MkNotificationSelectWindow.vue')), { const { dispose } = os.popup(defineAsyncComponent(() => import('@/components/MkNotificationSelectWindow.vue')), {
excludeTypes: widgetProps.excludeTypes, excludeTypes: widgetProps.excludeTypes,
}, { }, {
done: async (res) => { done: async (res) => {
@ -62,7 +62,8 @@ const configureNotification = () => {
widgetProps.excludeTypes = excludeTypes; widgetProps.excludeTypes = excludeTypes;
save(); save();
}, },
}, 'closed'); closed: () => dispose(),
});
}; };
defineExpose<WidgetComponentExpose>({ defineExpose<WidgetComponentExpose>({

View File

@ -44,7 +44,6 @@
}, },
"compileOnSave": false, "compileOnSave": false,
"include": [ "include": [
".eslintrc.js",
"./**/*.ts", "./**/*.ts",
"./**/*.vue" "./**/*.vue"
], ],

View File

@ -1,6 +1,8 @@
import dns from 'dns'; import dns from 'dns';
import { readFile } from 'node:fs/promises'; import { readFile } from 'node:fs/promises';
import type { IncomingMessage } from 'node:http';
import { defineConfig } from 'vite'; import { defineConfig } from 'vite';
import type { UserConfig } from 'vite';
import * as yaml from 'js-yaml'; import * as yaml from 'js-yaml';
import locales from '../../locales/index.js'; import locales from '../../locales/index.js';
import { getConfig } from './vite.config.js'; import { getConfig } from './vite.config.js';
@ -14,7 +16,15 @@ const { port } = yaml.load(await readFile('../../.config/default.yml', 'utf-8'))
const httpUrl = `http://localhost:${port}/`; const httpUrl = `http://localhost:${port}/`;
const websocketUrl = `ws://localhost:${port}/`; const websocketUrl = `ws://localhost:${port}/`;
const devConfig = { // activitypubリクエストはProxyを通し、それ以外はViteの開発サーバーを返す
function varyHandler(req: IncomingMessage) {
if (req.headers.accept?.includes('application/activity+json')) {
return null;
}
return '/index.html';
}
const devConfig: UserConfig = {
// 基本の設定は vite.config.js から引き継ぐ // 基本の設定は vite.config.js から引き継ぐ
...defaultConfig, ...defaultConfig,
root: 'src', root: 'src',
@ -54,15 +64,11 @@ const devConfig = {
'/inbox': httpUrl, '/inbox': httpUrl,
'/notes': { '/notes': {
target: httpUrl, target: httpUrl,
headers: { bypass: varyHandler,
'Accept': 'application/activity+json',
},
}, },
'/users': { '/users': {
target: httpUrl, target: httpUrl,
headers: { bypass: varyHandler,
'Accept': 'application/activity+json',
},
}, },
'/.well-known': { '/.well-known': {
target: httpUrl, target: httpUrl,

View File

@ -1,8 +0,0 @@
node_modules
/built
/coverage
/.eslintrc.js
/jest.config.ts
/test
/test-d
build.js

View File

@ -1,9 +0,0 @@
module.exports = {
parserOptions: {
tsconfigRootDir: __dirname,
project: ['./tsconfig.json'],
},
extends: [
'../shared/.eslintrc.js',
],
};

View File

@ -0,0 +1,27 @@
import tsParser from '@typescript-eslint/parser';
import sharedConfig from '../shared/eslint.config.js';
export default [
...sharedConfig,
{
ignores: [
'**/node_modules',
'built',
'coverage',
'jest.config.ts',
'test',
'test-d',
],
},
{
files: ['**/*.ts', '**/*.tsx'],
languageOptions: {
parserOptions: {
parser: tsParser,
project: ['./tsconfig.json'],
sourceType: 'module',
tsconfigRootDir: import.meta.dirname,
},
},
},
];

View File

@ -17,18 +17,16 @@
"scripts": { "scripts": {
"build": "node ./build.js", "build": "node ./build.js",
"watch": "nodemon -w package.json -e json --exec \"node ./build.js --watch\"", "watch": "nodemon -w package.json -e json --exec \"node ./build.js --watch\"",
"eslint": "eslint . --ext .js,.jsx,.ts,.tsx", "eslint": "eslint './**/*.{js,jsx,ts,tsx}'",
"typecheck": "tsc --noEmit", "typecheck": "tsc --noEmit",
"lint": "pnpm typecheck && pnpm eslint" "lint": "pnpm typecheck && pnpm eslint"
}, },
"devDependencies": { "devDependencies": {
"@misskey-dev/eslint-plugin": "1.0.0",
"@types/matter-js": "0.19.6", "@types/matter-js": "0.19.6",
"@types/seedrandom": "3.0.8", "@types/seedrandom": "3.0.8",
"@types/node": "20.11.5", "@types/node": "20.11.5",
"@typescript-eslint/eslint-plugin": "7.1.0", "@typescript-eslint/eslint-plugin": "7.1.0",
"@typescript-eslint/parser": "7.1.0", "@typescript-eslint/parser": "7.1.0",
"eslint": "8.57.0",
"nodemon": "3.0.2", "nodemon": "3.0.2",
"execa": "8.0.1", "execa": "8.0.1",
"typescript": "5.3.3", "typescript": "5.3.3",

View File

@ -1,8 +0,0 @@
node_modules
/built
/coverage
/.eslintrc.js
/jest.config.ts
/test
/test-d
build.js

Some files were not shown because too many files have changed in this diff Show More