Merge branch 'fetch' into serve-stream
This commit is contained in:
commit
a58771c230
|
@ -122,10 +122,12 @@ id: 'aid'
|
||||||
# Proxy for HTTP/HTTPS
|
# Proxy for HTTP/HTTPS
|
||||||
#proxy: http://127.0.0.1:3128
|
#proxy: http://127.0.0.1:3128
|
||||||
|
|
||||||
#proxyBypassHosts: [
|
proxyBypassHosts:
|
||||||
# 'example.com',
|
- api.deepl.com
|
||||||
# '192.0.2.8'
|
- api-free.deepl.com
|
||||||
#]
|
- www.recaptcha.net
|
||||||
|
- hcaptcha.com
|
||||||
|
- challenges.cloudflare.com
|
||||||
|
|
||||||
# Proxy for SMTP/SMTPS
|
# Proxy for SMTP/SMTPS
|
||||||
#proxySmtp: http://127.0.0.1:3128 # use HTTP/1.1 CONNECT
|
#proxySmtp: http://127.0.0.1:3128 # use HTTP/1.1 CONNECT
|
||||||
|
|
|
@ -20,6 +20,9 @@ You should also include the user name that made the change.
|
||||||
- You may have to `yarn run clean-all`, `sudo corepack enable` and `yarn set version berry` before running `yarn install` if you're still on yarn classic
|
- You may have to `yarn run clean-all`, `sudo corepack enable` and `yarn set version berry` before running `yarn install` if you're still on yarn classic
|
||||||
- 新たに動的なPagesを作ることはできなくなりました
|
- 新たに動的なPagesを作ることはできなくなりました
|
||||||
- 代わりに今後AiScriptを用いてより柔軟に動的なコンテンツを作成できるMisskey Play機能の実装を予定しています。
|
- 代わりに今後AiScriptを用いてより柔軟に動的なコンテンツを作成できるMisskey Play機能の実装を予定しています。
|
||||||
|
- AiScriptが0.12.0にアップデートされました
|
||||||
|
- 0.12.0の変更点についてはこちら https://github.com/syuilo/aiscript/blob/master/CHANGELOG.md#0120
|
||||||
|
- 0.12.0未満のプラグインは読み込むことはできません
|
||||||
- iOS15以下のデバイスはサポートされなくなりました
|
- iOS15以下のデバイスはサポートされなくなりました
|
||||||
- API: カスタム絵文字エンティティに`url`プロパティが含まれなくなりました
|
- API: カスタム絵文字エンティティに`url`プロパティが含まれなくなりました
|
||||||
- 絵文字画像を表示するには、`<instance host>/emoji/<emoji name>.webp`にリクエストすると画像が返ります。
|
- 絵文字画像を表示するには、`<instance host>/emoji/<emoji name>.webp`にリクエストすると画像が返ります。
|
||||||
|
@ -27,6 +30,7 @@ You should also include the user name that made the change.
|
||||||
- remote: `https://p1.a9z.dev/emoji/syuilo_birth_present@mk.f72u.net.webp`
|
- remote: `https://p1.a9z.dev/emoji/syuilo_birth_present@mk.f72u.net.webp`
|
||||||
- API: `user`および`note`エンティティに`emojis`プロパティが含まれなくなりました
|
- API: `user`および`note`エンティティに`emojis`プロパティが含まれなくなりました
|
||||||
- API: `user`エンティティに`avatarColor`および`bannerColor`プロパティが含まれなくなりました
|
- API: `user`エンティティに`avatarColor`および`bannerColor`プロパティが含まれなくなりました
|
||||||
|
- API: `instance`エンティティに`latestStatus`、`lastCommunicatedAt`、`latestRequestSentAt`プロパティが含まれなくなりました
|
||||||
|
|
||||||
### Improvements
|
### Improvements
|
||||||
- Push notification of Antenna note @tamaina
|
- Push notification of Antenna note @tamaina
|
||||||
|
@ -41,6 +45,7 @@ You should also include the user name that made the change.
|
||||||
- Server: delete outdated notifications regularly to improve db performance @syuilo
|
- Server: delete outdated notifications regularly to improve db performance @syuilo
|
||||||
- Server: delete outdated hard-mutes regularly to improve db performance @syuilo
|
- Server: delete outdated hard-mutes regularly to improve db performance @syuilo
|
||||||
- Server: delete outdated notes of antenna regularly to improve db performance @syuilo
|
- Server: delete outdated notes of antenna regularly to improve db performance @syuilo
|
||||||
|
- Server: improve activitypub deliver performance @syuilo
|
||||||
- Client: use tabler-icons instead of fontawesome to better design @syuilo
|
- Client: use tabler-icons instead of fontawesome to better design @syuilo
|
||||||
- Client: Add new gabber kick sounds (thanks for noizenecio)
|
- Client: Add new gabber kick sounds (thanks for noizenecio)
|
||||||
- Client: Add link to user RSS feed in profile menu @ssmucny
|
- Client: Add link to user RSS feed in profile menu @ssmucny
|
||||||
|
@ -74,6 +79,7 @@ You should also include the user name that made the change.
|
||||||
- Client: Webhookの編集画面で、内容を保存することができない問題を修正 @m-hayabusa
|
- Client: Webhookの編集画面で、内容を保存することができない問題を修正 @m-hayabusa
|
||||||
- Client: update emoji picker immediately on all input @saschanaz
|
- Client: update emoji picker immediately on all input @saschanaz
|
||||||
- Client: チャートのツールチップが画面に残ることがあるのを修正 @syuilo
|
- Client: チャートのツールチップが画面に残ることがあるのを修正 @syuilo
|
||||||
|
- Client: fix wrong link in tutorial @syuilo
|
||||||
|
|
||||||
## 12.119.1 (2022/12/03)
|
## 12.119.1 (2022/12/03)
|
||||||
### Bugfixes
|
### Bugfixes
|
||||||
|
|
|
@ -1382,6 +1382,7 @@ _profile:
|
||||||
changeBanner: "Banner ändern"
|
changeBanner: "Banner ändern"
|
||||||
_exportOrImport:
|
_exportOrImport:
|
||||||
allNotes: "Alle Notizen"
|
allNotes: "Alle Notizen"
|
||||||
|
favoritedNotes: "Als Favorit markierte Notizen"
|
||||||
followingList: "Gefolgte Benutzer"
|
followingList: "Gefolgte Benutzer"
|
||||||
muteList: "Stummschaltungen"
|
muteList: "Stummschaltungen"
|
||||||
blockingList: "Blockierungen"
|
blockingList: "Blockierungen"
|
||||||
|
|
|
@ -1382,6 +1382,7 @@ _profile:
|
||||||
changeBanner: "Change banner"
|
changeBanner: "Change banner"
|
||||||
_exportOrImport:
|
_exportOrImport:
|
||||||
allNotes: "All notes"
|
allNotes: "All notes"
|
||||||
|
favoritedNotes: "Favorite notes"
|
||||||
followingList: "Followed users"
|
followingList: "Followed users"
|
||||||
muteList: "Muted users"
|
muteList: "Muted users"
|
||||||
blockingList: "Blocked users"
|
blockingList: "Blocked users"
|
||||||
|
|
|
@ -167,7 +167,6 @@ annotation: "注釈"
|
||||||
federation: "連合"
|
federation: "連合"
|
||||||
instances: "インスタンス"
|
instances: "インスタンス"
|
||||||
registeredAt: "初観測"
|
registeredAt: "初観測"
|
||||||
latestRequestSentAt: "直近のリクエスト送信"
|
|
||||||
latestRequestReceivedAt: "直近のリクエスト受信"
|
latestRequestReceivedAt: "直近のリクエスト受信"
|
||||||
latestStatus: "直近のステータス"
|
latestStatus: "直近のステータス"
|
||||||
storageUsage: "ストレージ使用量"
|
storageUsage: "ストレージ使用量"
|
||||||
|
@ -916,6 +915,7 @@ caption: "キャプション"
|
||||||
loggedInAsBot: "Botアカウントでログイン中"
|
loggedInAsBot: "Botアカウントでログイン中"
|
||||||
tools: "ツール"
|
tools: "ツール"
|
||||||
cannotLoad: "読み込めません"
|
cannotLoad: "読み込めません"
|
||||||
|
numberOfProfileView: "プロフィール表示回数"
|
||||||
|
|
||||||
_sensitiveMediaDetection:
|
_sensitiveMediaDetection:
|
||||||
description: "機械学習を使って自動でセンシティブなメディアを検出し、モデレーションに役立てることができます。サーバーの負荷が少し増えます。"
|
description: "機械学習を使って自動でセンシティブなメディアを検出し、モデレーションに役立てることができます。サーバーの負荷が少し増えます。"
|
||||||
|
|
|
@ -915,6 +915,7 @@ windowRestore: "복구"
|
||||||
caption: "캡션"
|
caption: "캡션"
|
||||||
loggedInAsBot: "봇 계정으로 로그인중"
|
loggedInAsBot: "봇 계정으로 로그인중"
|
||||||
tools: "도구"
|
tools: "도구"
|
||||||
|
cannotLoad: "불러오지 못했습니다"
|
||||||
_sensitiveMediaDetection:
|
_sensitiveMediaDetection:
|
||||||
description: "기계학습을 통해 자동으로 민감한 미디어를 탐지하여, 모더레이션에 참고할 수 있도록 합니다. 서버의 부하를 약간 증가시킵니다."
|
description: "기계학습을 통해 자동으로 민감한 미디어를 탐지하여, 모더레이션에 참고할 수 있도록 합니다. 서버의 부하를 약간 증가시킵니다."
|
||||||
sensitivity: "탐지 민감도"
|
sensitivity: "탐지 민감도"
|
||||||
|
|
|
@ -1382,6 +1382,7 @@ _profile:
|
||||||
changeBanner: "เปลี่ยนแบนเนอร์"
|
changeBanner: "เปลี่ยนแบนเนอร์"
|
||||||
_exportOrImport:
|
_exportOrImport:
|
||||||
allNotes: "โน้ตทั้งหมด"
|
allNotes: "โน้ตทั้งหมด"
|
||||||
|
favoritedNotes: "บันทึกที่ชื่นชอบ"
|
||||||
followingList: "กำลังติดตาม"
|
followingList: "กำลังติดตาม"
|
||||||
muteList: "ปิดเสียง"
|
muteList: "ปิดเสียง"
|
||||||
blockingList: "บล็อค"
|
blockingList: "บล็อค"
|
||||||
|
|
|
@ -915,6 +915,7 @@ windowRestore: "还原"
|
||||||
caption: "标题"
|
caption: "标题"
|
||||||
loggedInAsBot: "已登录的Bot"
|
loggedInAsBot: "已登录的Bot"
|
||||||
tools: "工具"
|
tools: "工具"
|
||||||
|
cannotLoad: "无法加载"
|
||||||
_sensitiveMediaDetection:
|
_sensitiveMediaDetection:
|
||||||
description: "可以使用机器学习技术自动检测敏感媒体,以便进行审核。服务器负载将略微增加。"
|
description: "可以使用机器学习技术自动检测敏感媒体,以便进行审核。服务器负载将略微增加。"
|
||||||
sensitivity: "检测敏感度"
|
sensitivity: "检测敏感度"
|
||||||
|
|
|
@ -915,6 +915,8 @@ windowRestore: "復原"
|
||||||
caption: "標題"
|
caption: "標題"
|
||||||
loggedInAsBot: "以機器人帳號登入中"
|
loggedInAsBot: "以機器人帳號登入中"
|
||||||
tools: "工具"
|
tools: "工具"
|
||||||
|
cannotLoad: "無法載入"
|
||||||
|
numberOfProfileView: "個人檔案檢視次數"
|
||||||
_sensitiveMediaDetection:
|
_sensitiveMediaDetection:
|
||||||
description: "您可以使用機器學習自動檢測敏感媒體並將其用於審核。 伺服器的負荷會稍微增加。"
|
description: "您可以使用機器學習自動檢測敏感媒體並將其用於審核。 伺服器的負荷會稍微增加。"
|
||||||
sensitivity: "檢測敏感度"
|
sensitivity: "檢測敏感度"
|
||||||
|
@ -1381,6 +1383,7 @@ _profile:
|
||||||
changeBanner: "變更橫幅圖像"
|
changeBanner: "變更橫幅圖像"
|
||||||
_exportOrImport:
|
_exportOrImport:
|
||||||
allNotes: "所有貼文"
|
allNotes: "所有貼文"
|
||||||
|
favoritedNotes: "「我的最愛」貼文"
|
||||||
followingList: "追隨中"
|
followingList: "追隨中"
|
||||||
muteList: "靜音"
|
muteList: "靜音"
|
||||||
blockingList: "封鎖"
|
blockingList: "封鎖"
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "misskey",
|
"name": "misskey",
|
||||||
"version": "13.0.0-beta.14",
|
"version": "13.0.0-beta.20",
|
||||||
"codename": "indigo",
|
"codename": "indigo",
|
||||||
"repository": {
|
"repository": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
|
@ -57,7 +57,7 @@
|
||||||
"@typescript-eslint/parser": "5.47.1",
|
"@typescript-eslint/parser": "5.47.1",
|
||||||
"cross-env": "7.0.3",
|
"cross-env": "7.0.3",
|
||||||
"cypress": "12.2.0",
|
"cypress": "12.2.0",
|
||||||
"eslint": "^8.30.0",
|
"eslint": "^8.31.0",
|
||||||
"start-server-and-test": "1.15.2",
|
"start-server-and-test": "1.15.2",
|
||||||
"typescript": "4.9.4"
|
"typescript": "4.9.4"
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,11 @@
|
||||||
|
export class removeLatestRequestSentAt1672703171386 {
|
||||||
|
name = 'removeLatestRequestSentAt1672703171386'
|
||||||
|
|
||||||
|
async up(queryRunner) {
|
||||||
|
await queryRunner.query(`ALTER TABLE "instance" DROP COLUMN "latestRequestSentAt"`);
|
||||||
|
}
|
||||||
|
|
||||||
|
async down(queryRunner) {
|
||||||
|
await queryRunner.query(`ALTER TABLE "instance" ADD "latestRequestSentAt" TIMESTAMP WITH TIME ZONE`);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,11 @@
|
||||||
|
export class removeLastCommunicatedAt1672704017999 {
|
||||||
|
name = 'removeLastCommunicatedAt1672704017999'
|
||||||
|
|
||||||
|
async up(queryRunner) {
|
||||||
|
await queryRunner.query(`ALTER TABLE "instance" DROP COLUMN "lastCommunicatedAt"`);
|
||||||
|
}
|
||||||
|
|
||||||
|
async down(queryRunner) {
|
||||||
|
await queryRunner.query(`ALTER TABLE "instance" ADD "lastCommunicatedAt" TIMESTAMP WITH TIME ZONE NOT NULL`);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,11 @@
|
||||||
|
export class removeLatestStatus1672704136584 {
|
||||||
|
name = 'removeLatestStatus1672704136584'
|
||||||
|
|
||||||
|
async up(queryRunner) {
|
||||||
|
await queryRunner.query(`ALTER TABLE "instance" DROP COLUMN "latestStatus"`);
|
||||||
|
}
|
||||||
|
|
||||||
|
async down(queryRunner) {
|
||||||
|
await queryRunner.query(`ALTER TABLE "instance" ADD "latestStatus" integer`);
|
||||||
|
}
|
||||||
|
}
|
|
@ -21,9 +21,9 @@
|
||||||
"@tensorflow/tfjs-node": "4.1.0"
|
"@tensorflow/tfjs-node": "4.1.0"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@bull-board/api": "^4.9.0",
|
"@bull-board/api": "^4.10.0",
|
||||||
"@bull-board/fastify": "^4.9.0",
|
"@bull-board/fastify": "^4.10.0",
|
||||||
"@bull-board/ui": "^4.9.0",
|
"@bull-board/ui": "^4.10.0",
|
||||||
"@discordapp/twemoji": "14.0.2",
|
"@discordapp/twemoji": "14.0.2",
|
||||||
"@fastify/accepts": "4.1.0",
|
"@fastify/accepts": "4.1.0",
|
||||||
"@fastify/cookie": "^8.3.0",
|
"@fastify/cookie": "^8.3.0",
|
||||||
|
@ -37,12 +37,11 @@
|
||||||
"@nestjs/testing": "9.2.1",
|
"@nestjs/testing": "9.2.1",
|
||||||
"@peertube/http-signature": "1.7.0",
|
"@peertube/http-signature": "1.7.0",
|
||||||
"@sinonjs/fake-timers": "10.0.2",
|
"@sinonjs/fake-timers": "10.0.2",
|
||||||
"@syuilo/aiscript": "0.11.1",
|
|
||||||
"accepts": "^1.3.8",
|
"accepts": "^1.3.8",
|
||||||
"ajv": "8.11.2",
|
"ajv": "8.11.2",
|
||||||
"archiver": "5.3.1",
|
"archiver": "5.3.1",
|
||||||
"autwh": "0.1.0",
|
"autwh": "0.1.0",
|
||||||
"aws-sdk": "2.1272.0",
|
"aws-sdk": "2.1286.0",
|
||||||
"bcryptjs": "2.4.3",
|
"bcryptjs": "2.4.3",
|
||||||
"blurhash": "2.0.4",
|
"blurhash": "2.0.4",
|
||||||
"bull": "4.10.2",
|
"bull": "4.10.2",
|
||||||
|
@ -57,7 +56,7 @@
|
||||||
"date-fns": "2.29.3",
|
"date-fns": "2.29.3",
|
||||||
"deep-email-validator": "0.1.21",
|
"deep-email-validator": "0.1.21",
|
||||||
"escape-regexp": "0.0.1",
|
"escape-regexp": "0.0.1",
|
||||||
"fastify": "4.10.2",
|
"fastify": "4.11.0",
|
||||||
"feed": "4.2.2",
|
"feed": "4.2.2",
|
||||||
"file-type": "18.0.0",
|
"file-type": "18.0.0",
|
||||||
"fluent-ffmpeg": "2.1.2",
|
"fluent-ffmpeg": "2.1.2",
|
||||||
|
@ -69,7 +68,7 @@
|
||||||
"is-svg": "4.3.2",
|
"is-svg": "4.3.2",
|
||||||
"js-yaml": "4.1.0",
|
"js-yaml": "4.1.0",
|
||||||
"jsdom": "20.0.3",
|
"jsdom": "20.0.3",
|
||||||
"json5": "2.2.2",
|
"json5": "2.2.3",
|
||||||
"json5-loader": "4.0.1",
|
"json5-loader": "4.0.1",
|
||||||
"jsonld": "8.1.0",
|
"jsonld": "8.1.0",
|
||||||
"jsrsasign": "10.6.1",
|
"jsrsasign": "10.6.1",
|
||||||
|
@ -78,7 +77,6 @@
|
||||||
"misskey-js": "0.0.14",
|
"misskey-js": "0.0.14",
|
||||||
"ms": "3.0.0-canary.1",
|
"ms": "3.0.0-canary.1",
|
||||||
"nested-property": "4.0.0",
|
"nested-property": "4.0.0",
|
||||||
"node-fetch": "3.3.0",
|
|
||||||
"nodemailer": "6.8.0",
|
"nodemailer": "6.8.0",
|
||||||
"nsfwjs": "2.4.2",
|
"nsfwjs": "2.4.2",
|
||||||
"oauth": "^0.10.0",
|
"oauth": "^0.10.0",
|
||||||
|
@ -115,10 +113,11 @@
|
||||||
"tinycolor2": "1.5.1",
|
"tinycolor2": "1.5.1",
|
||||||
"tmp": "0.2.1",
|
"tmp": "0.2.1",
|
||||||
"tsc-alias": "1.8.2",
|
"tsc-alias": "1.8.2",
|
||||||
"tsconfig-paths": "4.1.1",
|
"tsconfig-paths": "4.1.2",
|
||||||
"twemoji-parser": "14.0.0",
|
"twemoji-parser": "14.0.0",
|
||||||
"typeorm": "0.3.11",
|
"typeorm": "0.3.11",
|
||||||
"ulid": "2.3.0",
|
"ulid": "2.3.0",
|
||||||
|
"undici": "^5.14.0",
|
||||||
"unzipper": "0.10.11",
|
"unzipper": "0.10.11",
|
||||||
"uuid": "9.0.0",
|
"uuid": "9.0.0",
|
||||||
"vary": "1.1.2",
|
"vary": "1.1.2",
|
||||||
|
@ -141,7 +140,7 @@
|
||||||
"@types/escape-regexp": "0.0.1",
|
"@types/escape-regexp": "0.0.1",
|
||||||
"@types/fluent-ffmpeg": "2.1.20",
|
"@types/fluent-ffmpeg": "2.1.20",
|
||||||
"@types/ioredis": "4.28.10",
|
"@types/ioredis": "4.28.10",
|
||||||
"@types/jest": "29.2.4",
|
"@types/jest": "29.2.5",
|
||||||
"@types/js-yaml": "4.0.5",
|
"@types/js-yaml": "4.0.5",
|
||||||
"@types/jsdom": "20.0.1",
|
"@types/jsdom": "20.0.1",
|
||||||
"@types/jsonld": "1.5.8",
|
"@types/jsonld": "1.5.8",
|
||||||
|
@ -161,7 +160,7 @@
|
||||||
"@types/rename": "1.0.4",
|
"@types/rename": "1.0.4",
|
||||||
"@types/sanitize-html": "2.8.0",
|
"@types/sanitize-html": "2.8.0",
|
||||||
"@types/semver": "7.3.13",
|
"@types/semver": "7.3.13",
|
||||||
"@types/sharp": "0.31.0",
|
"@types/sharp": "0.31.1",
|
||||||
"@types/sinonjs__fake-timers": "8.1.2",
|
"@types/sinonjs__fake-timers": "8.1.2",
|
||||||
"@types/speakeasy": "2.0.7",
|
"@types/speakeasy": "2.0.7",
|
||||||
"@types/syslog-pro": "^1.0.0",
|
"@types/syslog-pro": "^1.0.0",
|
||||||
|
@ -172,15 +171,16 @@
|
||||||
"@types/vary": "1.1.0",
|
"@types/vary": "1.1.0",
|
||||||
"@types/web-push": "3.3.2",
|
"@types/web-push": "3.3.2",
|
||||||
"@types/websocket": "1.0.5",
|
"@types/websocket": "1.0.5",
|
||||||
"@types/ws": "8.5.3",
|
"@types/ws": "8.5.4",
|
||||||
"@typescript-eslint/eslint-plugin": "5.47.1",
|
"@typescript-eslint/eslint-plugin": "5.47.1",
|
||||||
"@typescript-eslint/parser": "5.47.1",
|
"@typescript-eslint/parser": "5.47.1",
|
||||||
"cross-env": "7.0.3",
|
"cross-env": "7.0.3",
|
||||||
"eslint": "8.30.0",
|
"eslint": "8.31.0",
|
||||||
"eslint-plugin-import": "2.26.0",
|
"eslint-plugin-import": "2.26.0",
|
||||||
"execa": "6.1.0",
|
"execa": "6.1.0",
|
||||||
"jest": "29.3.1",
|
"jest": "29.3.1",
|
||||||
"jest-mock": "^29.3.1",
|
"jest-mock": "^29.3.1",
|
||||||
|
"node-fetch": "3.3.0",
|
||||||
"typescript": "4.9.4"
|
"typescript": "4.9.4"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -27,16 +27,19 @@ export class CaptchaService {
|
||||||
response,
|
response,
|
||||||
});
|
});
|
||||||
|
|
||||||
const res = await fetch(url, {
|
const res = await this.httpRequestService.fetch(
|
||||||
method: 'POST',
|
url,
|
||||||
body: params,
|
{
|
||||||
headers: {
|
method: 'POST',
|
||||||
'User-Agent': this.config.userAgent,
|
body: params,
|
||||||
|
headers: {
|
||||||
|
'User-Agent': this.config.userAgent,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
// TODO
|
{
|
||||||
//timeout: 10 * 1000,
|
noOkError: true,
|
||||||
agent: (url, bypassProxy) => this.httpRequestService.getAgentByUrl(url, bypassProxy),
|
}
|
||||||
}).catch(err => {
|
).catch(err => {
|
||||||
throw `${err.message ?? err}`;
|
throw `${err.message ?? err}`;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -8,11 +8,12 @@ import got, * as Got from 'got';
|
||||||
import chalk from 'chalk';
|
import chalk from 'chalk';
|
||||||
import { DI } from '@/di-symbols.js';
|
import { DI } from '@/di-symbols.js';
|
||||||
import type { Config } from '@/config.js';
|
import type { Config } from '@/config.js';
|
||||||
import { HttpRequestService } from '@/core/HttpRequestService.js';
|
import { HttpRequestService, UndiciFetcher } from '@/core/HttpRequestService.js';
|
||||||
import { createTemp } from '@/misc/create-temp.js';
|
import { createTemp } from '@/misc/create-temp.js';
|
||||||
import { StatusError } from '@/misc/status-error.js';
|
import { StatusError } from '@/misc/status-error.js';
|
||||||
import { LoggerService } from '@/core/LoggerService.js';
|
import { LoggerService } from '@/core/LoggerService.js';
|
||||||
import type Logger from '@/logger.js';
|
import type Logger from '@/logger.js';
|
||||||
|
import { buildConnector } from 'undici';
|
||||||
|
|
||||||
const pipeline = util.promisify(stream.pipeline);
|
const pipeline = util.promisify(stream.pipeline);
|
||||||
import { bindThis } from '@/decorators.js';
|
import { bindThis } from '@/decorators.js';
|
||||||
|
@ -20,6 +21,7 @@ import { bindThis } from '@/decorators.js';
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class DownloadService {
|
export class DownloadService {
|
||||||
private logger: Logger;
|
private logger: Logger;
|
||||||
|
private undiciFetcher: UndiciFetcher;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
@Inject(DI.config)
|
@Inject(DI.config)
|
||||||
|
@ -29,69 +31,58 @@ export class DownloadService {
|
||||||
private loggerService: LoggerService,
|
private loggerService: LoggerService,
|
||||||
) {
|
) {
|
||||||
this.logger = this.loggerService.getLogger('download');
|
this.logger = this.loggerService.getLogger('download');
|
||||||
|
|
||||||
|
this.undiciFetcher = new UndiciFetcher(this.httpRequestService.getStandardUndiciFetcherOption(
|
||||||
|
{
|
||||||
|
connect: process.env.NODE_ENV === 'development' ?
|
||||||
|
this.httpRequestService.clientDefaults.connect
|
||||||
|
:
|
||||||
|
this.httpRequestService.getConnectorWithIpCheck(
|
||||||
|
buildConnector({
|
||||||
|
...this.httpRequestService.clientDefaults.connect,
|
||||||
|
}),
|
||||||
|
(ip) => !this.isPrivateIp(ip)
|
||||||
|
),
|
||||||
|
bodyTimeout: 30 * 1000,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
connect: this.httpRequestService.clientDefaults.connect,
|
||||||
|
}
|
||||||
|
), this.logger);
|
||||||
}
|
}
|
||||||
|
|
||||||
@bindThis
|
@bindThis
|
||||||
public gotUrl(url: string): Got.Request {
|
public fetchUrl(url: string): any {
|
||||||
this.logger.info(`Downloading ${chalk.cyan(url)} ...`);
|
this.logger.info(`Downloading ${chalk.cyan(url)} ...`);
|
||||||
|
|
||||||
const timeout = 30 * 1000;
|
const timeout = 30 * 1000;
|
||||||
const operationTimeout = 60 * 1000;
|
const operationTimeout = 60 * 1000;
|
||||||
const maxSize = this.config.maxFileSize ?? 262144000;
|
const maxSize = this.config.maxFileSize ?? 262144000;
|
||||||
|
|
||||||
const req = got.stream(url, {
|
const response = await this.undiciFetcher.fetch(
|
||||||
headers: {
|
url,
|
||||||
'User-Agent': this.config.userAgent,
|
{
|
||||||
},
|
method: 'GET',
|
||||||
timeout: {
|
headers: {
|
||||||
lookup: timeout,
|
'User-Agent': this.config.userAgent,
|
||||||
connect: timeout,
|
},
|
||||||
secureConnect: timeout,
|
|
||||||
socket: timeout, // read timeout
|
|
||||||
response: timeout,
|
|
||||||
send: timeout,
|
|
||||||
request: operationTimeout, // whole operation timeout
|
|
||||||
},
|
|
||||||
agent: {
|
|
||||||
http: this.httpRequestService.httpAgent,
|
|
||||||
https: this.httpRequestService.httpsAgent,
|
|
||||||
},
|
|
||||||
http2: false, // default
|
|
||||||
retry: {
|
|
||||||
limit: 0,
|
|
||||||
},
|
|
||||||
}).on('response', (res: Got.Response) => {
|
|
||||||
if ((process.env.NODE_ENV === 'production' || process.env.NODE_ENV === 'test') && !this.config.proxy && res.ip) {
|
|
||||||
if (this.isPrivateIp(res.ip)) {
|
|
||||||
this.logger.warn(`Blocked address: ${res.ip}`);
|
|
||||||
req.destroy();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
);
|
||||||
|
|
||||||
const contentLength = res.headers['content-length'];
|
if (response.body === null) {
|
||||||
if (contentLength != null) {
|
throw new StatusError('No body', 400, 'No body');
|
||||||
const size = Number(contentLength);
|
}
|
||||||
if (size > maxSize) {
|
|
||||||
this.logger.warn(`maxSize exceeded (${size} > ${maxSize}) on response`);
|
|
||||||
req.destroy();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}).on('downloadProgress', (progress: Got.Progress) => {
|
|
||||||
if (progress.transferred > maxSize) {
|
|
||||||
this.logger.warn(`maxSize exceeded (${progress.transferred} > ${maxSize}) on downloadProgress`);
|
|
||||||
req.destroy();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
return req;
|
this.logger.succ(`Download finished: ${chalk.cyan(url)}`);
|
||||||
|
|
||||||
|
return response;
|
||||||
}
|
}
|
||||||
|
|
||||||
@bindThis
|
@bindThis
|
||||||
public async pipeRequestToFile(req: Got.Request, path: string): Promise<void> {
|
public async pipeRequestToFile(response: any, path: string): Promise<void> {
|
||||||
const copied = req.pipe(new stream.PassThrough());
|
|
||||||
try {
|
try {
|
||||||
this.logger.info(`Saving File to ${chalk.cyanBright(path)} from downloading ...`);
|
this.logger.info(`Saving File to ${chalk.cyanBright(path)} from downloading ...`);
|
||||||
await pipeline(copied, fs.createWriteStream(path));
|
await pipeline(stream.Readable.fromWeb(response.body), fs.createWriteStream(path));
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
if (e instanceof Got.HTTPError) {
|
if (e instanceof Got.HTTPError) {
|
||||||
throw new StatusError(`${e.response.statusCode} ${e.response.statusMessage}`, e.response.statusCode, e.response.statusMessage);
|
throw new StatusError(`${e.response.statusCode} ${e.response.statusMessage}`, e.response.statusCode, e.response.statusMessage);
|
||||||
|
@ -135,6 +126,6 @@ export class DownloadService {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return PrivateIp(ip);
|
return PrivateIp(ip) ?? false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -22,7 +22,7 @@ export class FederatedInstanceService {
|
||||||
}
|
}
|
||||||
|
|
||||||
@bindThis
|
@bindThis
|
||||||
public async registerOrFetchInstanceDoc(host: string): Promise<Instance> {
|
public async fetch(host: string): Promise<Instance> {
|
||||||
host = this.utilityService.toPuny(host);
|
host = this.utilityService.toPuny(host);
|
||||||
|
|
||||||
const cached = this.cache.get(host);
|
const cached = this.cache.get(host);
|
||||||
|
@ -35,7 +35,6 @@ export class FederatedInstanceService {
|
||||||
id: this.idService.genId(),
|
id: this.idService.genId(),
|
||||||
host,
|
host,
|
||||||
caughtAt: new Date(),
|
caughtAt: new Date(),
|
||||||
lastCommunicatedAt: new Date(),
|
|
||||||
}).then(x => this.instancesRepository.findOneByOrFail(x.identifiers[0]));
|
}).then(x => this.instancesRepository.findOneByOrFail(x.identifiers[0]));
|
||||||
|
|
||||||
this.cache.set(host, i);
|
this.cache.set(host, i);
|
||||||
|
@ -45,4 +44,17 @@ export class FederatedInstanceService {
|
||||||
return index;
|
return index;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@bindThis
|
||||||
|
public async updateCachePartial(host: string, data: Partial<Instance>): Promise<void> {
|
||||||
|
host = this.utilityService.toPuny(host);
|
||||||
|
|
||||||
|
const cached = this.cache.get(host);
|
||||||
|
if (cached == null) return;
|
||||||
|
|
||||||
|
this.cache.set(host, {
|
||||||
|
...cached,
|
||||||
|
...data,
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,7 +1,6 @@
|
||||||
import { URL } from 'node:url';
|
import { URL } from 'node:url';
|
||||||
import { Inject, Injectable } from '@nestjs/common';
|
import { Inject, Injectable } from '@nestjs/common';
|
||||||
import { JSDOM } from 'jsdom';
|
import { JSDOM } from 'jsdom';
|
||||||
import fetch from 'node-fetch';
|
|
||||||
import tinycolor from 'tinycolor2';
|
import tinycolor from 'tinycolor2';
|
||||||
import type { Instance } from '@/models/entities/Instance.js';
|
import type { Instance } from '@/models/entities/Instance.js';
|
||||||
import type { InstancesRepository } from '@/models/index.js';
|
import type { InstancesRepository } from '@/models/index.js';
|
||||||
|
@ -191,11 +190,7 @@ export class FetchInstanceMetadataService {
|
||||||
|
|
||||||
const faviconUrl = url + '/favicon.ico';
|
const faviconUrl = url + '/favicon.ico';
|
||||||
|
|
||||||
const favicon = await fetch(faviconUrl, {
|
const favicon = await this.httpRequestService.fetch(faviconUrl, {}, { noOkError: true });
|
||||||
// TODO
|
|
||||||
//timeout: 10000,
|
|
||||||
agent: url => this.httpRequestService.getAgentByUrl(url),
|
|
||||||
});
|
|
||||||
|
|
||||||
if (favicon.ok) {
|
if (favicon.ok) {
|
||||||
return faviconUrl;
|
return faviconUrl;
|
||||||
|
|
|
@ -1,67 +1,255 @@
|
||||||
import * as http from 'node:http';
|
import * as http from 'node:http';
|
||||||
import * as https from 'node:https';
|
import * as https from 'node:https';
|
||||||
import CacheableLookup from 'cacheable-lookup';
|
import CacheableLookup from 'cacheable-lookup';
|
||||||
import fetch from 'node-fetch';
|
|
||||||
import { HttpProxyAgent, HttpsProxyAgent } from 'hpagent';
|
import { HttpProxyAgent, HttpsProxyAgent } from 'hpagent';
|
||||||
import { Inject, Injectable } from '@nestjs/common';
|
import { Inject, Injectable } from '@nestjs/common';
|
||||||
import { DI } from '@/di-symbols.js';
|
import { DI } from '@/di-symbols.js';
|
||||||
import type { Config } from '@/config.js';
|
import type { Config } from '@/config.js';
|
||||||
import { StatusError } from '@/misc/status-error.js';
|
import { StatusError } from '@/misc/status-error.js';
|
||||||
import { bindThis } from '@/decorators.js';
|
import { bindThis } from '@/decorators.js';
|
||||||
import type { Response } from 'node-fetch';
|
import * as undici from 'undici';
|
||||||
import type { URL } from 'node:url';
|
import { LookupFunction } from 'node:net';
|
||||||
|
import { LoggerService } from '@/core/LoggerService.js';
|
||||||
|
import type Logger from '@/logger.js';
|
||||||
|
|
||||||
|
// true to allow, false to deny
|
||||||
|
export type IpChecker = (ip: string) => boolean;
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Child class to create and save Agent for fetch.
|
||||||
|
* You should construct this when you want
|
||||||
|
* to change timeout, size limit, socket connect function, etc.
|
||||||
|
*/
|
||||||
|
export class UndiciFetcher {
|
||||||
|
/**
|
||||||
|
* Get http non-proxy agent (undici)
|
||||||
|
*/
|
||||||
|
public nonProxiedAgent: undici.Agent;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get http proxy or non-proxy agent (undici)
|
||||||
|
*/
|
||||||
|
public agent: undici.ProxyAgent | undici.Agent;
|
||||||
|
|
||||||
|
private proxyBypassHosts: string[];
|
||||||
|
private userAgent: string | undefined;
|
||||||
|
|
||||||
|
private logger: Logger | undefined;
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
args: {
|
||||||
|
agentOptions: undici.Agent.Options;
|
||||||
|
proxy?: {
|
||||||
|
uri: string;
|
||||||
|
options?: undici.Agent.Options; // Override of agentOptions
|
||||||
|
},
|
||||||
|
proxyBypassHosts?: string[];
|
||||||
|
userAgent?: string;
|
||||||
|
},
|
||||||
|
logger?: Logger,
|
||||||
|
) {
|
||||||
|
this.logger = logger;
|
||||||
|
this.logger?.debug('UndiciFetcher constructor', args);
|
||||||
|
|
||||||
|
this.proxyBypassHosts = args.proxyBypassHosts ?? [];
|
||||||
|
this.userAgent = args.userAgent;
|
||||||
|
|
||||||
|
this.nonProxiedAgent = new undici.Agent({
|
||||||
|
...args.agentOptions,
|
||||||
|
connect: (process.env.NODE_ENV !== 'production' && typeof args.agentOptions.connect !== 'function')
|
||||||
|
? (options, cb) => {
|
||||||
|
// Custom connector for debug
|
||||||
|
undici.buildConnector(args.agentOptions.connect as undici.buildConnector.BuildOptions)(options, (err, socket) => {
|
||||||
|
this.logger?.debug('Socket connector called', socket);
|
||||||
|
if (err) {
|
||||||
|
this.logger?.debug(`Socket error`, err);
|
||||||
|
cb(new Error(`Error while socket connecting\n${err}`), null);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.logger?.debug(`Socket connected: port ${socket.localPort} => remote ${socket.remoteAddress}`);
|
||||||
|
cb(null, socket);
|
||||||
|
});
|
||||||
|
} : args.agentOptions.connect,
|
||||||
|
});
|
||||||
|
|
||||||
|
this.agent = args.proxy
|
||||||
|
? new undici.ProxyAgent({
|
||||||
|
...args.agentOptions,
|
||||||
|
...args.proxy.options,
|
||||||
|
|
||||||
|
uri: args.proxy.uri,
|
||||||
|
|
||||||
|
connect: (process.env.NODE_ENV !== 'production' && typeof (args.proxy?.options?.connect ?? args.agentOptions.connect) !== 'function')
|
||||||
|
? (options, cb) => {
|
||||||
|
// Custom connector for debug
|
||||||
|
undici.buildConnector((args.proxy?.options?.connect ?? args.agentOptions.connect) as undici.buildConnector.BuildOptions)(options, (err, socket) => {
|
||||||
|
this.logger?.debug('Socket connector called (secure)', socket);
|
||||||
|
if (err) {
|
||||||
|
this.logger?.debug(`Socket error`, err);
|
||||||
|
cb(new Error(`Error while socket connecting\n${err}`), null);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.logger?.debug(`Socket connected (secure): port ${socket.localPort} => remote ${socket.remoteAddress}`);
|
||||||
|
cb(null, socket);
|
||||||
|
});
|
||||||
|
} : (args.proxy?.options?.connect ?? args.agentOptions.connect),
|
||||||
|
})
|
||||||
|
: this.nonProxiedAgent;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get agent by URL
|
||||||
|
* @param url URL
|
||||||
|
* @param bypassProxy Allways bypass proxy
|
||||||
|
*/
|
||||||
|
@bindThis
|
||||||
|
public getAgentByUrl(url: URL, bypassProxy = false): undici.Agent | undici.ProxyAgent {
|
||||||
|
if (bypassProxy || this.proxyBypassHosts.includes(url.hostname)) {
|
||||||
|
return this.nonProxiedAgent;
|
||||||
|
} else {
|
||||||
|
return this.agent;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@bindThis
|
||||||
|
public async fetch(
|
||||||
|
url: string | URL,
|
||||||
|
options: undici.RequestInit = {},
|
||||||
|
privateOptions: { noOkError?: boolean; bypassProxy?: boolean; } = { noOkError: false, bypassProxy: false }
|
||||||
|
): Promise<undici.Response> {
|
||||||
|
const res = await undici.fetch(url, {
|
||||||
|
dispatcher: this.getAgentByUrl(new URL(url), privateOptions.bypassProxy),
|
||||||
|
...options,
|
||||||
|
}).catch((err) => {
|
||||||
|
this.logger?.error('fetch error', err);
|
||||||
|
throw new StatusError('Resource Unreachable', 500, 'Resource Unreachable');
|
||||||
|
});
|
||||||
|
if (!res.ok && !privateOptions.noOkError) {
|
||||||
|
throw new StatusError(`${res.status} ${res.statusText}`, res.status, res.statusText);
|
||||||
|
}
|
||||||
|
return res;
|
||||||
|
}
|
||||||
|
|
||||||
|
@bindThis
|
||||||
|
public async getJson<T extends unknown>(url: string, accept = 'application/json, */*', headers?: Record<string, string>): Promise<T> {
|
||||||
|
const res = await this.fetch(
|
||||||
|
url,
|
||||||
|
{
|
||||||
|
headers: Object.assign({
|
||||||
|
'User-Agent': this.userAgent,
|
||||||
|
Accept: accept,
|
||||||
|
}, headers ?? {}),
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
return await res.json() as T;
|
||||||
|
}
|
||||||
|
|
||||||
|
@bindThis
|
||||||
|
public async getHtml(url: string, accept = 'text/html, */*', headers?: Record<string, string>): Promise<string> {
|
||||||
|
const res = await this.fetch(
|
||||||
|
url,
|
||||||
|
{
|
||||||
|
headers: Object.assign({
|
||||||
|
'User-Agent': this.userAgent,
|
||||||
|
Accept: accept,
|
||||||
|
}, headers ?? {}),
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
return await res.text();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class HttpRequestService {
|
export class HttpRequestService {
|
||||||
/**
|
public defaultFetcher: UndiciFetcher;
|
||||||
* Get http non-proxy agent
|
public fetch: UndiciFetcher['fetch'];
|
||||||
*/
|
public getHtml: UndiciFetcher['getHtml'];
|
||||||
|
public defaultJsonFetcher: UndiciFetcher;
|
||||||
|
public getJson: UndiciFetcher['getJson'];
|
||||||
|
|
||||||
|
//#region for old http/https, only used in S3Service
|
||||||
|
// http non-proxy agent
|
||||||
private http: http.Agent;
|
private http: http.Agent;
|
||||||
|
|
||||||
/**
|
// https non-proxy agent
|
||||||
* Get https non-proxy agent
|
|
||||||
*/
|
|
||||||
private https: https.Agent;
|
private https: https.Agent;
|
||||||
|
|
||||||
/**
|
// http proxy or non-proxy agent
|
||||||
* Get http proxy or non-proxy agent
|
|
||||||
*/
|
|
||||||
public httpAgent: http.Agent;
|
public httpAgent: http.Agent;
|
||||||
|
|
||||||
/**
|
// https proxy or non-proxy agent
|
||||||
* Get https proxy or non-proxy agent
|
|
||||||
*/
|
|
||||||
public httpsAgent: https.Agent;
|
public httpsAgent: https.Agent;
|
||||||
|
//#endregion
|
||||||
|
|
||||||
|
public readonly dnsCache: CacheableLookup;
|
||||||
|
public readonly clientDefaults: undici.Agent.Options;
|
||||||
|
private maxSockets: number;
|
||||||
|
|
||||||
|
private logger: Logger;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
@Inject(DI.config)
|
@Inject(DI.config)
|
||||||
private config: Config,
|
private config: Config,
|
||||||
|
private loggerService: LoggerService,
|
||||||
) {
|
) {
|
||||||
const cache = new CacheableLookup({
|
this.logger = this.loggerService.getLogger('http-request');
|
||||||
|
|
||||||
|
this.dnsCache = new CacheableLookup({
|
||||||
maxTtl: 3600, // 1hours
|
maxTtl: 3600, // 1hours
|
||||||
errorTtl: 30, // 30secs
|
errorTtl: 30, // 30secs
|
||||||
lookup: false, // nativeのdns.lookupにfallbackしない
|
lookup: false, // nativeのdns.lookupにfallbackしない
|
||||||
});
|
});
|
||||||
|
|
||||||
|
this.clientDefaults = {
|
||||||
|
keepAliveTimeout: 30 * 1000,
|
||||||
|
keepAliveMaxTimeout: 10 * 60 * 1000,
|
||||||
|
keepAliveTimeoutThreshold: 1 * 1000,
|
||||||
|
strictContentLength: true,
|
||||||
|
headersTimeout: 10 * 1000,
|
||||||
|
bodyTimeout: 10 * 1000,
|
||||||
|
maxHeaderSize: 16364, // default
|
||||||
|
maxResponseSize: 10 * 1024 * 1024,
|
||||||
|
maxRedirections: 3,
|
||||||
|
connect: {
|
||||||
|
timeout: 10 * 1000, // コネクションが確立するまでのタイムアウト
|
||||||
|
maxCachedSessions: 300, // TLSセッションのキャッシュ数 https://github.com/nodejs/undici/blob/v5.14.0/lib/core/connect.js#L80
|
||||||
|
lookup: this.dnsCache.lookup as LookupFunction, // https://github.com/nodejs/undici/blob/v5.14.0/lib/core/connect.js#L98
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
this.maxSockets = Math.max(256, this.config.deliverJobConcurrency ?? 128);
|
||||||
|
|
||||||
|
this.defaultFetcher = new UndiciFetcher(this.getStandardUndiciFetcherOption(), this.logger);
|
||||||
|
|
||||||
|
this.fetch = this.defaultFetcher.fetch;
|
||||||
|
this.getHtml = this.defaultFetcher.getHtml;
|
||||||
|
|
||||||
|
this.defaultJsonFetcher = new UndiciFetcher(this.getStandardUndiciFetcherOption({
|
||||||
|
maxResponseSize: 1024 * 256,
|
||||||
|
}), this.logger);
|
||||||
|
|
||||||
|
this.getJson = this.defaultJsonFetcher.getJson;
|
||||||
|
|
||||||
|
//#region for old http/https, only used in S3Service
|
||||||
this.http = new http.Agent({
|
this.http = new http.Agent({
|
||||||
keepAlive: true,
|
keepAlive: true,
|
||||||
keepAliveMsecs: 30 * 1000,
|
keepAliveMsecs: 30 * 1000,
|
||||||
lookup: cache.lookup,
|
lookup: this.dnsCache.lookup,
|
||||||
} as http.AgentOptions);
|
} as http.AgentOptions);
|
||||||
|
|
||||||
this.https = new https.Agent({
|
this.https = new https.Agent({
|
||||||
keepAlive: true,
|
keepAlive: true,
|
||||||
keepAliveMsecs: 30 * 1000,
|
keepAliveMsecs: 30 * 1000,
|
||||||
lookup: cache.lookup,
|
lookup: this.dnsCache.lookup,
|
||||||
} as https.AgentOptions);
|
} as https.AgentOptions);
|
||||||
|
|
||||||
const maxSockets = Math.max(256, config.deliverJobConcurrency ?? 128);
|
|
||||||
|
|
||||||
this.httpAgent = config.proxy
|
this.httpAgent = config.proxy
|
||||||
? new HttpProxyAgent({
|
? new HttpProxyAgent({
|
||||||
keepAlive: true,
|
keepAlive: true,
|
||||||
keepAliveMsecs: 30 * 1000,
|
keepAliveMsecs: 30 * 1000,
|
||||||
maxSockets,
|
maxSockets: this.maxSockets,
|
||||||
maxFreeSockets: 256,
|
maxFreeSockets: 256,
|
||||||
scheduling: 'lifo',
|
scheduling: 'lifo',
|
||||||
proxy: config.proxy,
|
proxy: config.proxy,
|
||||||
|
@ -72,21 +260,46 @@ export class HttpRequestService {
|
||||||
? new HttpsProxyAgent({
|
? new HttpsProxyAgent({
|
||||||
keepAlive: true,
|
keepAlive: true,
|
||||||
keepAliveMsecs: 30 * 1000,
|
keepAliveMsecs: 30 * 1000,
|
||||||
maxSockets,
|
maxSockets: this.maxSockets,
|
||||||
maxFreeSockets: 256,
|
maxFreeSockets: 256,
|
||||||
scheduling: 'lifo',
|
scheduling: 'lifo',
|
||||||
proxy: config.proxy,
|
proxy: config.proxy,
|
||||||
})
|
})
|
||||||
: this.https;
|
: this.https;
|
||||||
|
//#endregion
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get agent by URL
|
* Get http agent by URL
|
||||||
* @param url URL
|
* @param url URL
|
||||||
* @param bypassProxy Allways bypass proxy
|
* @param bypassProxy Allways bypass proxy
|
||||||
*/
|
*/
|
||||||
@bindThis
|
@bindThis
|
||||||
public getAgentByUrl(url: URL, bypassProxy = false): http.Agent | https.Agent {
|
public getStandardUndiciFetcherOption(opts: undici.Agent.Options = {}, proxyOpts: undici.Agent.Options = {}) {
|
||||||
|
return {
|
||||||
|
agentOptions: {
|
||||||
|
...this.clientDefaults,
|
||||||
|
...opts,
|
||||||
|
},
|
||||||
|
...(this.config.proxy ? {
|
||||||
|
proxy: {
|
||||||
|
uri: this.config.proxy,
|
||||||
|
options: {
|
||||||
|
connections: this.maxSockets,
|
||||||
|
...proxyOpts,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} : {}),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get http agent by URL
|
||||||
|
* @param url URL
|
||||||
|
* @param bypassProxy Allways bypass proxy
|
||||||
|
*/
|
||||||
|
@bindThis
|
||||||
|
public getHttpAgentByUrl(url: URL, bypassProxy = false): http.Agent | https.Agent {
|
||||||
if (bypassProxy || (this.config.proxyBypassHosts || []).includes(url.hostname)) {
|
if (bypassProxy || (this.config.proxyBypassHosts || []).includes(url.hostname)) {
|
||||||
return url.protocol === 'http:' ? this.http : this.https;
|
return url.protocol === 'http:' ? this.http : this.https;
|
||||||
} else {
|
} else {
|
||||||
|
@ -94,67 +307,37 @@ export class HttpRequestService {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* check ip
|
||||||
|
*/
|
||||||
@bindThis
|
@bindThis
|
||||||
public async getJson(url: string, accept = 'application/json, */*', timeout = 10000, headers?: Record<string, string>): Promise<unknown> {
|
public getConnectorWithIpCheck(connector: undici.buildConnector.connector, checkIp: IpChecker): undici.buildConnector.connectorAsync {
|
||||||
const res = await this.getResponse({
|
return (options, cb) => {
|
||||||
url,
|
connector(options, (err, socket) => {
|
||||||
method: 'GET',
|
this.logger.debug('Socket connector (with ip checker) called', socket);
|
||||||
headers: Object.assign({
|
if (err) {
|
||||||
'User-Agent': this.config.userAgent,
|
this.logger.error(`Socket error`, err)
|
||||||
Accept: accept,
|
cb(new Error(`Error while socket connecting\n${err}`), null);
|
||||||
}, headers ?? {}),
|
return;
|
||||||
timeout,
|
}
|
||||||
size: 1024 * 256,
|
|
||||||
});
|
|
||||||
|
|
||||||
return await res.json();
|
if (socket.remoteAddress == undefined) {
|
||||||
}
|
this.logger.error(`Socket error: remoteAddress is undefined`);
|
||||||
|
cb(new Error('remoteAddress is undefined (maybe socket destroyed)'), null);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
@bindThis
|
// allow
|
||||||
public async getHtml(url: string, accept = 'text/html, */*', timeout = 10000, headers?: Record<string, string>): Promise<string> {
|
if (checkIp(socket.remoteAddress)) {
|
||||||
const res = await this.getResponse({
|
this.logger.debug(`Socket connected (ip ok): ${socket.localPort} => ${socket.remoteAddress}`);
|
||||||
url,
|
cb(null, socket);
|
||||||
method: 'GET',
|
return;
|
||||||
headers: Object.assign({
|
}
|
||||||
'User-Agent': this.config.userAgent,
|
|
||||||
Accept: accept,
|
|
||||||
}, headers ?? {}),
|
|
||||||
timeout,
|
|
||||||
});
|
|
||||||
|
|
||||||
return await res.text();
|
this.logger.error('IP is not allowed', socket);
|
||||||
}
|
cb(new StatusError('IP is not allowed', 403, 'IP is not allowed'), null);
|
||||||
|
socket.destroy();
|
||||||
@bindThis
|
});
|
||||||
public async getResponse(args: {
|
};
|
||||||
url: string,
|
|
||||||
method: string,
|
|
||||||
body?: string,
|
|
||||||
headers: Record<string, string>,
|
|
||||||
timeout?: number,
|
|
||||||
size?: number,
|
|
||||||
}): Promise<Response> {
|
|
||||||
const timeout = args.timeout ?? 10 * 1000;
|
|
||||||
|
|
||||||
const controller = new AbortController();
|
|
||||||
setTimeout(() => {
|
|
||||||
controller.abort();
|
|
||||||
}, timeout * 6);
|
|
||||||
|
|
||||||
const res = await fetch(args.url, {
|
|
||||||
method: args.method,
|
|
||||||
headers: args.headers,
|
|
||||||
body: args.body,
|
|
||||||
timeout,
|
|
||||||
size: args.size ?? 10 * 1024 * 1024,
|
|
||||||
agent: (url) => this.getAgentByUrl(url),
|
|
||||||
signal: controller.signal,
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!res.ok) {
|
|
||||||
throw new StatusError(`${res.status} ${res.statusText}`, res.status, res.statusText);
|
|
||||||
}
|
|
||||||
|
|
||||||
return res;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -428,7 +428,7 @@ export class NoteCreateService {
|
||||||
|
|
||||||
// Register host
|
// Register host
|
||||||
if (this.userEntityService.isRemoteUser(user)) {
|
if (this.userEntityService.isRemoteUser(user)) {
|
||||||
this.federatedInstanceService.registerOrFetchInstanceDoc(user.host).then(i => {
|
this.federatedInstanceService.fetch(user.host).then(i => {
|
||||||
this.instancesRepository.increment({ id: i.id }, 'notesCount', 1);
|
this.instancesRepository.increment({ id: i.id }, 'notesCount', 1);
|
||||||
this.instanceChart.updateNote(i.host, note, true);
|
this.instanceChart.updateNote(i.host, note, true);
|
||||||
});
|
});
|
||||||
|
|
|
@ -100,7 +100,7 @@ export class NoteDeleteService {
|
||||||
this.perUserNotesChart.update(user, note, false);
|
this.perUserNotesChart.update(user, note, false);
|
||||||
|
|
||||||
if (this.userEntityService.isRemoteUser(user)) {
|
if (this.userEntityService.isRemoteUser(user)) {
|
||||||
this.federatedInstanceService.registerOrFetchInstanceDoc(user.host).then(i => {
|
this.federatedInstanceService.fetch(user.host).then(i => {
|
||||||
this.instancesRepository.decrement({ id: i.id }, 'notesCount', 1);
|
this.instancesRepository.decrement({ id: i.id }, 'notesCount', 1);
|
||||||
this.instanceChart.updateNote(i.host, note, false);
|
this.instanceChart.updateNote(i.host, note, false);
|
||||||
});
|
});
|
||||||
|
|
|
@ -33,7 +33,7 @@ export class S3Service {
|
||||||
? false
|
? false
|
||||||
: meta.objectStorageS3ForcePathStyle,
|
: meta.objectStorageS3ForcePathStyle,
|
||||||
httpOptions: {
|
httpOptions: {
|
||||||
agent: this.httpRequestService.getAgentByUrl(new URL(u), !meta.objectStorageUseProxy),
|
agent: this.httpRequestService.getHttpAgentByUrl(new URL(u), !meta.objectStorageUseProxy),
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
@ -205,12 +205,12 @@ export class UserFollowingService {
|
||||||
|
|
||||||
//#region Update instance stats
|
//#region Update instance stats
|
||||||
if (this.userEntityService.isRemoteUser(follower) && this.userEntityService.isLocalUser(followee)) {
|
if (this.userEntityService.isRemoteUser(follower) && this.userEntityService.isLocalUser(followee)) {
|
||||||
this.federatedInstanceService.registerOrFetchInstanceDoc(follower.host).then(i => {
|
this.federatedInstanceService.fetch(follower.host).then(i => {
|
||||||
this.instancesRepository.increment({ id: i.id }, 'followingCount', 1);
|
this.instancesRepository.increment({ id: i.id }, 'followingCount', 1);
|
||||||
this.instanceChart.updateFollowing(i.host, true);
|
this.instanceChart.updateFollowing(i.host, true);
|
||||||
});
|
});
|
||||||
} else if (this.userEntityService.isLocalUser(follower) && this.userEntityService.isRemoteUser(followee)) {
|
} else if (this.userEntityService.isLocalUser(follower) && this.userEntityService.isRemoteUser(followee)) {
|
||||||
this.federatedInstanceService.registerOrFetchInstanceDoc(followee.host).then(i => {
|
this.federatedInstanceService.fetch(followee.host).then(i => {
|
||||||
this.instancesRepository.increment({ id: i.id }, 'followersCount', 1);
|
this.instancesRepository.increment({ id: i.id }, 'followersCount', 1);
|
||||||
this.instanceChart.updateFollowers(i.host, true);
|
this.instanceChart.updateFollowers(i.host, true);
|
||||||
});
|
});
|
||||||
|
@ -323,12 +323,12 @@ export class UserFollowingService {
|
||||||
|
|
||||||
//#region Update instance stats
|
//#region Update instance stats
|
||||||
if (this.userEntityService.isRemoteUser(follower) && this.userEntityService.isLocalUser(followee)) {
|
if (this.userEntityService.isRemoteUser(follower) && this.userEntityService.isLocalUser(followee)) {
|
||||||
this.federatedInstanceService.registerOrFetchInstanceDoc(follower.host).then(i => {
|
this.federatedInstanceService.fetch(follower.host).then(i => {
|
||||||
this.instancesRepository.decrement({ id: i.id }, 'followingCount', 1);
|
this.instancesRepository.decrement({ id: i.id }, 'followingCount', 1);
|
||||||
this.instanceChart.updateFollowing(i.host, false);
|
this.instanceChart.updateFollowing(i.host, false);
|
||||||
});
|
});
|
||||||
} else if (this.userEntityService.isLocalUser(follower) && this.userEntityService.isRemoteUser(followee)) {
|
} else if (this.userEntityService.isLocalUser(follower) && this.userEntityService.isRemoteUser(followee)) {
|
||||||
this.federatedInstanceService.registerOrFetchInstanceDoc(followee.host).then(i => {
|
this.federatedInstanceService.fetch(followee.host).then(i => {
|
||||||
this.instancesRepository.decrement({ id: i.id }, 'followersCount', 1);
|
this.instancesRepository.decrement({ id: i.id }, 'followersCount', 1);
|
||||||
this.instanceChart.updateFollowers(i.host, false);
|
this.instanceChart.updateFollowers(i.host, false);
|
||||||
});
|
});
|
||||||
|
|
|
@ -30,7 +30,7 @@ export class WebfingerService {
|
||||||
public async webfinger(query: string): Promise<IWebFinger> {
|
public async webfinger(query: string): Promise<IWebFinger> {
|
||||||
const url = this.genUrl(query);
|
const url = this.genUrl(query);
|
||||||
|
|
||||||
return await this.httpRequestService.getJson(url, 'application/jrd+json, application/json') as IWebFinger;
|
return await this.httpRequestService.getJson<IWebFinger>(url, 'application/jrd+json, application/json');
|
||||||
}
|
}
|
||||||
|
|
||||||
@bindThis
|
@bindThis
|
||||||
|
|
|
@ -5,7 +5,7 @@ import { DI } from '@/di-symbols.js';
|
||||||
import type { Config } from '@/config.js';
|
import type { Config } from '@/config.js';
|
||||||
import type { User } from '@/models/entities/User.js';
|
import type { User } from '@/models/entities/User.js';
|
||||||
import { UserKeypairStoreService } from '@/core/UserKeypairStoreService.js';
|
import { UserKeypairStoreService } from '@/core/UserKeypairStoreService.js';
|
||||||
import { HttpRequestService } from '@/core/HttpRequestService.js';
|
import { HttpRequestService, UndiciFetcher } from '@/core/HttpRequestService.js';
|
||||||
import { bindThis } from '@/decorators.js';
|
import { bindThis } from '@/decorators.js';
|
||||||
|
|
||||||
type Request = {
|
type Request = {
|
||||||
|
@ -28,6 +28,8 @@ type PrivateKey = {
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class ApRequestService {
|
export class ApRequestService {
|
||||||
|
private undiciFetcher: UndiciFetcher;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
@Inject(DI.config)
|
@Inject(DI.config)
|
||||||
private config: Config,
|
private config: Config,
|
||||||
|
@ -35,6 +37,9 @@ export class ApRequestService {
|
||||||
private userKeypairStoreService: UserKeypairStoreService,
|
private userKeypairStoreService: UserKeypairStoreService,
|
||||||
private httpRequestService: HttpRequestService,
|
private httpRequestService: HttpRequestService,
|
||||||
) {
|
) {
|
||||||
|
this.undiciFetcher = new UndiciFetcher(this.httpRequestService.getStandardUndiciFetcherOption({
|
||||||
|
maxRedirections: 0,
|
||||||
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
@bindThis
|
@bindThis
|
||||||
|
@ -152,12 +157,14 @@ export class ApRequestService {
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
await this.httpRequestService.getResponse({
|
await this.undiciFetcher.fetch(
|
||||||
url,
|
url,
|
||||||
method: req.request.method,
|
{
|
||||||
headers: req.request.headers,
|
method: req.request.method,
|
||||||
body,
|
headers: req.request.headers,
|
||||||
});
|
body,
|
||||||
|
}
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -180,11 +187,13 @@ export class ApRequestService {
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const res = await this.httpRequestService.getResponse({
|
const res = await this.httpRequestService.fetch(
|
||||||
url,
|
url,
|
||||||
method: req.request.method,
|
{
|
||||||
headers: req.request.headers,
|
method: req.request.method,
|
||||||
});
|
headers: req.request.headers,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
return await res.json();
|
return await res.json();
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,7 +4,7 @@ import { InstanceActorService } from '@/core/InstanceActorService.js';
|
||||||
import type { NotesRepository, PollsRepository, NoteReactionsRepository, UsersRepository } from '@/models/index.js';
|
import type { NotesRepository, PollsRepository, NoteReactionsRepository, UsersRepository } from '@/models/index.js';
|
||||||
import type { Config } from '@/config.js';
|
import type { Config } from '@/config.js';
|
||||||
import { MetaService } from '@/core/MetaService.js';
|
import { MetaService } from '@/core/MetaService.js';
|
||||||
import { HttpRequestService } from '@/core/HttpRequestService.js';
|
import { HttpRequestService, UndiciFetcher } from '@/core/HttpRequestService.js';
|
||||||
import { DI } from '@/di-symbols.js';
|
import { DI } from '@/di-symbols.js';
|
||||||
import { UtilityService } from '@/core/UtilityService.js';
|
import { UtilityService } from '@/core/UtilityService.js';
|
||||||
import { bindThis } from '@/decorators.js';
|
import { bindThis } from '@/decorators.js';
|
||||||
|
@ -17,6 +17,7 @@ import type { IObject, ICollection, IOrderedCollection } from './type.js';
|
||||||
export class Resolver {
|
export class Resolver {
|
||||||
private history: Set<string>;
|
private history: Set<string>;
|
||||||
private user?: ILocalUser;
|
private user?: ILocalUser;
|
||||||
|
private undiciFetcher: UndiciFetcher;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private config: Config,
|
private config: Config,
|
||||||
|
@ -34,6 +35,9 @@ export class Resolver {
|
||||||
private recursionLimit = 100,
|
private recursionLimit = 100,
|
||||||
) {
|
) {
|
||||||
this.history = new Set();
|
this.history = new Set();
|
||||||
|
this.undiciFetcher = new UndiciFetcher(this.httpRequestService.getStandardUndiciFetcherOption({
|
||||||
|
maxRedirections: 0,
|
||||||
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
@bindThis
|
@bindThis
|
||||||
|
@ -96,8 +100,8 @@ export class Resolver {
|
||||||
}
|
}
|
||||||
|
|
||||||
const object = (this.user
|
const object = (this.user
|
||||||
? await this.apRequestService.signedGet(value, this.user)
|
? await this.apRequestService.signedGet(value, this.user) as IObject
|
||||||
: await this.httpRequestService.getJson(value, 'application/activity+json, application/ld+json')) as IObject;
|
: await this.undiciFetcher.getJson<IObject>(value, 'application/activity+json, application/ld+json'));
|
||||||
|
|
||||||
if (object == null || (
|
if (object == null || (
|
||||||
Array.isArray(object['@context']) ?
|
Array.isArray(object['@context']) ?
|
||||||
|
|
|
@ -1,6 +1,5 @@
|
||||||
import * as crypto from 'node:crypto';
|
import * as crypto from 'node:crypto';
|
||||||
import { Inject, Injectable } from '@nestjs/common';
|
import { Inject, Injectable } from '@nestjs/common';
|
||||||
import fetch from 'node-fetch';
|
|
||||||
import { HttpRequestService } from '@/core/HttpRequestService.js';
|
import { HttpRequestService } from '@/core/HttpRequestService.js';
|
||||||
import { bindThis } from '@/decorators.js';
|
import { bindThis } from '@/decorators.js';
|
||||||
import { CONTEXTS } from './misc/contexts.js';
|
import { CONTEXTS } from './misc/contexts.js';
|
||||||
|
@ -116,14 +115,19 @@ class LdSignature {
|
||||||
|
|
||||||
@bindThis
|
@bindThis
|
||||||
private async fetchDocument(url: string) {
|
private async fetchDocument(url: string) {
|
||||||
const json = await fetch(url, {
|
const json = await this.httpRequestService.fetch(
|
||||||
headers: {
|
url,
|
||||||
Accept: 'application/ld+json, application/json',
|
{
|
||||||
|
headers: {
|
||||||
|
Accept: 'application/ld+json, application/json',
|
||||||
|
},
|
||||||
|
// TODO
|
||||||
|
//timeout: this.loderTimeout,
|
||||||
},
|
},
|
||||||
// TODO
|
{
|
||||||
//timeout: this.loderTimeout,
|
noOkError: true,
|
||||||
agent: u => u.protocol === 'http:' ? this.httpRequestService.httpAgent : this.httpRequestService.httpsAgent,
|
}
|
||||||
}).then(res => {
|
).then(res => {
|
||||||
if (!res.ok) {
|
if (!res.ok) {
|
||||||
throw `${res.status} ${res.statusText}`;
|
throw `${res.status} ${res.statusText}`;
|
||||||
} else {
|
} else {
|
||||||
|
|
|
@ -348,7 +348,7 @@ export class ApPersonService implements OnModuleInit {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Register host
|
// Register host
|
||||||
this.federatedInstanceService.registerOrFetchInstanceDoc(host).then(i => {
|
this.federatedInstanceService.fetch(host).then(i => {
|
||||||
this.instancesRepository.increment({ id: i.id }, 'usersCount', 1);
|
this.instancesRepository.increment({ id: i.id }, 'usersCount', 1);
|
||||||
this.instanceChart.newUser(i.host);
|
this.instanceChart.newUser(i.host);
|
||||||
this.fetchInstanceMetadataService.fetchInstanceMetadata(i);
|
this.fetchInstanceMetadataService.fetchInstanceMetadata(i);
|
||||||
|
|
|
@ -86,7 +86,7 @@ export default class FederationChart extends Chart<typeof schema> {
|
||||||
.where(`instance.host IN (${ subInstancesQuery.getQuery() })`)
|
.where(`instance.host IN (${ subInstancesQuery.getQuery() })`)
|
||||||
.andWhere(meta.blockedHosts.length === 0 ? '1=1' : 'instance.host NOT IN (:...blocked)', { blocked: meta.blockedHosts })
|
.andWhere(meta.blockedHosts.length === 0 ? '1=1' : 'instance.host NOT IN (:...blocked)', { blocked: meta.blockedHosts })
|
||||||
.andWhere('instance.isSuspended = false')
|
.andWhere('instance.isSuspended = false')
|
||||||
.andWhere('instance.lastCommunicatedAt > :gt', { gt: new Date(Date.now() - (1000 * 60 * 60 * 24 * 30)) })
|
.andWhere('instance.isNotResponding = false')
|
||||||
.getRawOne()
|
.getRawOne()
|
||||||
.then(x => parseInt(x.count, 10)),
|
.then(x => parseInt(x.count, 10)),
|
||||||
this.instancesRepository.createQueryBuilder('instance')
|
this.instancesRepository.createQueryBuilder('instance')
|
||||||
|
@ -94,7 +94,7 @@ export default class FederationChart extends Chart<typeof schema> {
|
||||||
.where(`instance.host IN (${ pubInstancesQuery.getQuery() })`)
|
.where(`instance.host IN (${ pubInstancesQuery.getQuery() })`)
|
||||||
.andWhere(meta.blockedHosts.length === 0 ? '1=1' : 'instance.host NOT IN (:...blocked)', { blocked: meta.blockedHosts })
|
.andWhere(meta.blockedHosts.length === 0 ? '1=1' : 'instance.host NOT IN (:...blocked)', { blocked: meta.blockedHosts })
|
||||||
.andWhere('instance.isSuspended = false')
|
.andWhere('instance.isSuspended = false')
|
||||||
.andWhere('instance.lastCommunicatedAt > :gt', { gt: new Date(Date.now() - (1000 * 60 * 60 * 24 * 30)) })
|
.andWhere('instance.isNotResponding = false')
|
||||||
.getRawOne()
|
.getRawOne()
|
||||||
.then(x => parseInt(x.count, 10)),
|
.then(x => parseInt(x.count, 10)),
|
||||||
]);
|
]);
|
||||||
|
|
|
@ -7,8 +7,8 @@ import type { } from '@/models/entities/Blocking.js';
|
||||||
import type { User } from '@/models/entities/User.js';
|
import type { User } from '@/models/entities/User.js';
|
||||||
import type { Instance } from '@/models/entities/Instance.js';
|
import type { Instance } from '@/models/entities/Instance.js';
|
||||||
import { MetaService } from '@/core/MetaService.js';
|
import { MetaService } from '@/core/MetaService.js';
|
||||||
import { UserEntityService } from './UserEntityService.js';
|
|
||||||
import { bindThis } from '@/decorators.js';
|
import { bindThis } from '@/decorators.js';
|
||||||
|
import { UserEntityService } from './UserEntityService.js';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class InstanceEntityService {
|
export class InstanceEntityService {
|
||||||
|
@ -33,8 +33,6 @@ export class InstanceEntityService {
|
||||||
notesCount: instance.notesCount,
|
notesCount: instance.notesCount,
|
||||||
followingCount: instance.followingCount,
|
followingCount: instance.followingCount,
|
||||||
followersCount: instance.followersCount,
|
followersCount: instance.followersCount,
|
||||||
latestRequestSentAt: instance.latestRequestSentAt ? instance.latestRequestSentAt.toISOString() : null,
|
|
||||||
lastCommunicatedAt: instance.lastCommunicatedAt.toISOString(),
|
|
||||||
isNotResponding: instance.isNotResponding,
|
isNotResponding: instance.isNotResponding,
|
||||||
isSuspended: instance.isSuspended,
|
isSuspended: instance.isSuspended,
|
||||||
isBlocked: meta.blockedHosts.includes(instance.host),
|
isBlocked: meta.blockedHosts.includes(instance.host),
|
||||||
|
|
|
@ -59,22 +59,6 @@ export class Instance {
|
||||||
})
|
})
|
||||||
public followersCount: number;
|
public followersCount: number;
|
||||||
|
|
||||||
/**
|
|
||||||
* 直近のリクエスト送信日時
|
|
||||||
*/
|
|
||||||
@Column('timestamp with time zone', {
|
|
||||||
nullable: true,
|
|
||||||
})
|
|
||||||
public latestRequestSentAt: Date | null;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 直近のリクエスト送信時のHTTPステータスコード
|
|
||||||
*/
|
|
||||||
@Column('integer', {
|
|
||||||
nullable: true,
|
|
||||||
})
|
|
||||||
public latestStatus: number | null;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 直近のリクエスト受信日時
|
* 直近のリクエスト受信日時
|
||||||
*/
|
*/
|
||||||
|
@ -83,12 +67,6 @@ export class Instance {
|
||||||
})
|
})
|
||||||
public latestRequestReceivedAt: Date | null;
|
public latestRequestReceivedAt: Date | null;
|
||||||
|
|
||||||
/**
|
|
||||||
* このインスタンスと最後にやり取りした日時
|
|
||||||
*/
|
|
||||||
@Column('timestamp with time zone')
|
|
||||||
public lastCommunicatedAt: Date;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* このインスタンスと不通かどうか
|
* このインスタンスと不通かどうか
|
||||||
*/
|
*/
|
||||||
|
|
|
@ -32,16 +32,6 @@ export const packedFederationInstanceSchema = {
|
||||||
type: 'number',
|
type: 'number',
|
||||||
optional: false, nullable: false,
|
optional: false, nullable: false,
|
||||||
},
|
},
|
||||||
latestRequestSentAt: {
|
|
||||||
type: 'string',
|
|
||||||
optional: false, nullable: true,
|
|
||||||
format: 'date-time',
|
|
||||||
},
|
|
||||||
lastCommunicatedAt: {
|
|
||||||
type: 'string',
|
|
||||||
optional: false, nullable: false,
|
|
||||||
format: 'date-time',
|
|
||||||
},
|
|
||||||
isNotResponding: {
|
isNotResponding: {
|
||||||
type: 'boolean',
|
type: 'boolean',
|
||||||
optional: false, nullable: false,
|
optional: false, nullable: false,
|
||||||
|
|
|
@ -15,10 +15,10 @@ import ApRequestChart from '@/core/chart/charts/ap-request.js';
|
||||||
import FederationChart from '@/core/chart/charts/federation.js';
|
import FederationChart from '@/core/chart/charts/federation.js';
|
||||||
import { StatusError } from '@/misc/status-error.js';
|
import { StatusError } from '@/misc/status-error.js';
|
||||||
import { UtilityService } from '@/core/UtilityService.js';
|
import { UtilityService } from '@/core/UtilityService.js';
|
||||||
|
import { bindThis } from '@/decorators.js';
|
||||||
import { QueueLoggerService } from '../QueueLoggerService.js';
|
import { QueueLoggerService } from '../QueueLoggerService.js';
|
||||||
import type Bull from 'bull';
|
import type Bull from 'bull';
|
||||||
import type { DeliverJobData } from '../types.js';
|
import type { DeliverJobData } from '../types.js';
|
||||||
import { bindThis } from '@/decorators.js';
|
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class DeliverProcessorService {
|
export class DeliverProcessorService {
|
||||||
|
@ -48,7 +48,6 @@ export class DeliverProcessorService {
|
||||||
) {
|
) {
|
||||||
this.logger = this.queueLoggerService.logger.createSubLogger('deliver');
|
this.logger = this.queueLoggerService.logger.createSubLogger('deliver');
|
||||||
this.suspendedHostsCache = new Cache<Instance[]>(1000 * 60 * 60);
|
this.suspendedHostsCache = new Cache<Instance[]>(1000 * 60 * 60);
|
||||||
this.latest = null;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@bindThis
|
@bindThis
|
||||||
|
@ -76,20 +75,18 @@ export class DeliverProcessorService {
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
if (this.latest !== (this.latest = JSON.stringify(job.data.content, null, 2))) {
|
|
||||||
this.logger.debug(`delivering ${this.latest}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
await this.apRequestService.signedPost(job.data.user, job.data.to, job.data.content);
|
await this.apRequestService.signedPost(job.data.user, job.data.to, job.data.content);
|
||||||
|
|
||||||
// Update stats
|
// Update stats
|
||||||
this.federatedInstanceService.registerOrFetchInstanceDoc(host).then(i => {
|
this.federatedInstanceService.fetch(host).then(i => {
|
||||||
this.instancesRepository.update(i.id, {
|
if (i.isNotResponding) {
|
||||||
latestRequestSentAt: new Date(),
|
this.instancesRepository.update(i.id, {
|
||||||
latestStatus: 200,
|
isNotResponding: false,
|
||||||
lastCommunicatedAt: new Date(),
|
});
|
||||||
isNotResponding: false,
|
this.federatedInstanceService.updateCachePartial(host, {
|
||||||
});
|
isNotResponding: false,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
this.fetchInstanceMetadataService.fetchInstanceMetadata(i);
|
this.fetchInstanceMetadataService.fetchInstanceMetadata(i);
|
||||||
|
|
||||||
|
@ -100,13 +97,16 @@ export class DeliverProcessorService {
|
||||||
|
|
||||||
return 'Success';
|
return 'Success';
|
||||||
} catch (res) {
|
} catch (res) {
|
||||||
// Update stats
|
// Update stats
|
||||||
this.federatedInstanceService.registerOrFetchInstanceDoc(host).then(i => {
|
this.federatedInstanceService.fetch(host).then(i => {
|
||||||
this.instancesRepository.update(i.id, {
|
if (!i.isNotResponding) {
|
||||||
latestRequestSentAt: new Date(),
|
this.instancesRepository.update(i.id, {
|
||||||
latestStatus: res instanceof StatusError ? res.statusCode : null,
|
isNotResponding: true,
|
||||||
isNotResponding: true,
|
});
|
||||||
});
|
this.federatedInstanceService.updateCachePartial(host, {
|
||||||
|
isNotResponding: true,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
this.instanceChart.requestSent(i.host, false);
|
this.instanceChart.requestSent(i.host, false);
|
||||||
this.apRequestChart.deliverFail();
|
this.apRequestChart.deliverFail();
|
||||||
|
@ -114,17 +114,17 @@ export class DeliverProcessorService {
|
||||||
});
|
});
|
||||||
|
|
||||||
if (res instanceof StatusError) {
|
if (res instanceof StatusError) {
|
||||||
// 4xx
|
// 4xx
|
||||||
if (res.isClientError) {
|
if (res.isClientError) {
|
||||||
// HTTPステータスコード4xxはクライアントエラーであり、それはつまり
|
// HTTPステータスコード4xxはクライアントエラーであり、それはつまり
|
||||||
// 何回再送しても成功することはないということなのでエラーにはしないでおく
|
// 何回再送しても成功することはないということなのでエラーにはしないでおく
|
||||||
return `${res.statusCode} ${res.statusMessage}`;
|
return `${res.statusCode} ${res.statusMessage}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 5xx etc.
|
// 5xx etc.
|
||||||
throw `${res.statusCode} ${res.statusMessage}`;
|
throw `${res.statusCode} ${res.statusMessage}`;
|
||||||
} else {
|
} else {
|
||||||
// DNS error, socket error, timeout ...
|
// DNS error, socket error, timeout ...
|
||||||
throw res;
|
throw res;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -176,10 +176,12 @@ export class InboxProcessorService {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update stats
|
// Update stats
|
||||||
this.federatedInstanceService.registerOrFetchInstanceDoc(authUser.user.host).then(i => {
|
this.federatedInstanceService.fetch(authUser.user.host).then(i => {
|
||||||
this.instancesRepository.update(i.id, {
|
this.instancesRepository.update(i.id, {
|
||||||
latestRequestReceivedAt: new Date(),
|
latestRequestReceivedAt: new Date(),
|
||||||
lastCommunicatedAt: new Date(),
|
isNotResponding: false,
|
||||||
|
});
|
||||||
|
this.federatedInstanceService.updateCachePartial(host, {
|
||||||
isNotResponding: false,
|
isNotResponding: false,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -33,24 +33,26 @@ export class WebhookDeliverProcessorService {
|
||||||
try {
|
try {
|
||||||
this.logger.debug(`delivering ${job.data.webhookId}`);
|
this.logger.debug(`delivering ${job.data.webhookId}`);
|
||||||
|
|
||||||
const res = await this.httpRequestService.getResponse({
|
const res = await this.httpRequestService.fetch(
|
||||||
url: job.data.to,
|
job.data.to,
|
||||||
method: 'POST',
|
{
|
||||||
headers: {
|
method: 'POST',
|
||||||
'User-Agent': 'Misskey-Hooks',
|
headers: {
|
||||||
'X-Misskey-Host': this.config.host,
|
'User-Agent': 'Misskey-Hooks',
|
||||||
'X-Misskey-Hook-Id': job.data.webhookId,
|
'X-Misskey-Host': this.config.host,
|
||||||
'X-Misskey-Hook-Secret': job.data.secret,
|
'X-Misskey-Hook-Id': job.data.webhookId,
|
||||||
},
|
'X-Misskey-Hook-Secret': job.data.secret,
|
||||||
body: JSON.stringify({
|
},
|
||||||
hookId: job.data.webhookId,
|
body: JSON.stringify({
|
||||||
userId: job.data.userId,
|
hookId: job.data.webhookId,
|
||||||
eventId: job.data.eventId,
|
userId: job.data.userId,
|
||||||
createdAt: job.data.createdAt,
|
eventId: job.data.eventId,
|
||||||
type: job.data.type,
|
createdAt: job.data.createdAt,
|
||||||
body: job.data.content,
|
type: job.data.type,
|
||||||
}),
|
body: job.data.content,
|
||||||
});
|
}),
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
this.webhooksRepository.update({ id: job.data.webhookId }, {
|
this.webhooksRepository.update({ id: job.data.webhookId }, {
|
||||||
latestSentAt: new Date(),
|
latestSentAt: new Date(),
|
||||||
|
|
|
@ -64,8 +64,8 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
|
||||||
case '-followers': query.orderBy('instance.followersCount', 'ASC'); break;
|
case '-followers': query.orderBy('instance.followersCount', 'ASC'); break;
|
||||||
case '+caughtAt': query.orderBy('instance.caughtAt', 'DESC'); break;
|
case '+caughtAt': query.orderBy('instance.caughtAt', 'DESC'); break;
|
||||||
case '-caughtAt': query.orderBy('instance.caughtAt', 'ASC'); break;
|
case '-caughtAt': query.orderBy('instance.caughtAt', 'ASC'); break;
|
||||||
case '+lastCommunicatedAt': query.orderBy('instance.lastCommunicatedAt', 'DESC'); break;
|
case '+latestRequestReceivedAt': query.orderBy('instance.latestRequestReceivedAt', 'DESC'); break;
|
||||||
case '-lastCommunicatedAt': query.orderBy('instance.lastCommunicatedAt', 'ASC'); break;
|
case '-latestRequestReceivedAt': query.orderBy('instance.latestRequestReceivedAt', 'ASC'); break;
|
||||||
|
|
||||||
default: query.orderBy('instance.id', 'DESC'); break;
|
default: query.orderBy('instance.id', 'DESC'); break;
|
||||||
}
|
}
|
||||||
|
|
|
@ -33,15 +33,17 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
|
||||||
private httpRequestService: HttpRequestService,
|
private httpRequestService: HttpRequestService,
|
||||||
) {
|
) {
|
||||||
super(meta, paramDef, async (ps, me) => {
|
super(meta, paramDef, async (ps, me) => {
|
||||||
const res = await this.httpRequestService.getResponse({
|
const res = await this.httpRequestService.fetch(
|
||||||
url: ps.url,
|
ps.url,
|
||||||
method: 'GET',
|
{
|
||||||
headers: Object.assign({
|
method: 'GET',
|
||||||
'User-Agent': config.userAgent,
|
headers: Object.assign({
|
||||||
Accept: 'application/rss+xml, */*',
|
'User-Agent': config.userAgent,
|
||||||
}),
|
Accept: 'application/rss+xml, */*',
|
||||||
timeout: 5000,
|
}),
|
||||||
});
|
// timeout: 5000,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
const text = await res.text();
|
const text = await res.text();
|
||||||
|
|
||||||
|
|
|
@ -1,5 +1,4 @@
|
||||||
import { URLSearchParams } from 'node:url';
|
import { URLSearchParams } from 'node:url';
|
||||||
import fetch from 'node-fetch';
|
|
||||||
import { Inject, Injectable } from '@nestjs/common';
|
import { Inject, Injectable } from '@nestjs/common';
|
||||||
import type { NotesRepository } from '@/models/index.js';
|
import type { NotesRepository } from '@/models/index.js';
|
||||||
import { Endpoint } from '@/server/api/endpoint-base.js';
|
import { Endpoint } from '@/server/api/endpoint-base.js';
|
||||||
|
@ -84,25 +83,28 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
|
||||||
|
|
||||||
const endpoint = instance.deeplIsPro ? 'https://api.deepl.com/v2/translate' : 'https://api-free.deepl.com/v2/translate';
|
const endpoint = instance.deeplIsPro ? 'https://api.deepl.com/v2/translate' : 'https://api-free.deepl.com/v2/translate';
|
||||||
|
|
||||||
const res = await fetch(endpoint, {
|
const res = await this.httpRequestService.fetch(
|
||||||
method: 'POST',
|
endpoint,
|
||||||
headers: {
|
{
|
||||||
'Content-Type': 'application/x-www-form-urlencoded',
|
method: 'POST',
|
||||||
'User-Agent': config.userAgent,
|
headers: {
|
||||||
Accept: 'application/json, */*',
|
'Content-Type': 'application/x-www-form-urlencoded',
|
||||||
|
'User-Agent': config.userAgent,
|
||||||
|
Accept: 'application/json, */*',
|
||||||
|
},
|
||||||
|
body: params.toString(),
|
||||||
},
|
},
|
||||||
body: params,
|
{
|
||||||
// TODO
|
noOkError: false,
|
||||||
//timeout: 10000,
|
}
|
||||||
agent: (url) => this.httpRequestService.getAgentByUrl(url),
|
);
|
||||||
});
|
|
||||||
|
|
||||||
const json = (await res.json()) as {
|
const json = (await res.json()) as {
|
||||||
translations: {
|
translations: {
|
||||||
detected_source_language: string;
|
detected_source_language: string;
|
||||||
text: string;
|
text: string;
|
||||||
}[];
|
}[];
|
||||||
};
|
};
|
||||||
|
|
||||||
return {
|
return {
|
||||||
sourceLang: json.translations[0].detected_source_language,
|
sourceLang: json.translations[0].detected_source_language,
|
||||||
|
|
|
@ -139,10 +139,12 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
|
||||||
throw new ApiError(meta.errors.noSuchUser);
|
throw new ApiError(meta.errors.noSuchUser);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (me == null && ip != null) {
|
if (user.host == null) {
|
||||||
this.perUserPvChart.commitByVisitor(user, ip);
|
if (me == null && ip != null) {
|
||||||
} else if (me && me.id !== user.id) {
|
this.perUserPvChart.commitByVisitor(user, ip);
|
||||||
this.perUserPvChart.commitByUser(user, me.id);
|
} else if (me && me.id !== user.id) {
|
||||||
|
this.perUserPvChart.commitByUser(user, me.id);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return await this.userEntityService.pack(user, me, {
|
return await this.userEntityService.pack(user, me, {
|
||||||
|
|
|
@ -181,7 +181,7 @@ export class DiscordServerService {
|
||||||
}
|
}
|
||||||
}));
|
}));
|
||||||
|
|
||||||
const { id, username, discriminator } = (await this.httpRequestService.getJson('https://discord.com/api/users/@me', '*/*', 10 * 1000, {
|
const { id, username, discriminator } = (await this.httpRequestService.getJson('https://discord.com/api/users/@me', '*/*', {
|
||||||
'Authorization': `Bearer ${accessToken}`,
|
'Authorization': `Bearer ${accessToken}`,
|
||||||
})) as Record<string, unknown>;
|
})) as Record<string, unknown>;
|
||||||
|
|
||||||
|
@ -249,7 +249,7 @@ export class DiscordServerService {
|
||||||
}
|
}
|
||||||
}));
|
}));
|
||||||
|
|
||||||
const { id, username, discriminator } = (await this.httpRequestService.getJson('https://discord.com/api/users/@me', '*/*', 10 * 1000, {
|
const { id, username, discriminator } = (await this.httpRequestService.getJson('https://discord.com/api/users/@me', '*/*', {
|
||||||
'Authorization': `Bearer ${accessToken}`,
|
'Authorization': `Bearer ${accessToken}`,
|
||||||
})) as Record<string, unknown>;
|
})) as Record<string, unknown>;
|
||||||
if (typeof id !== 'string' || typeof username !== 'string' || typeof discriminator !== 'string') {
|
if (typeof id !== 'string' || typeof username !== 'string' || typeof discriminator !== 'string') {
|
||||||
|
|
|
@ -174,7 +174,7 @@ export class GithubServerService {
|
||||||
}
|
}
|
||||||
}));
|
}));
|
||||||
|
|
||||||
const { login, id } = (await this.httpRequestService.getJson('https://api.github.com/user', 'application/vnd.github.v3+json', 10 * 1000, {
|
const { login, id } = (await this.httpRequestService.getJson('https://api.github.com/user', 'application/vnd.github.v3+json', {
|
||||||
'Authorization': `bearer ${accessToken}`,
|
'Authorization': `bearer ${accessToken}`,
|
||||||
})) as Record<string, unknown>;
|
})) as Record<string, unknown>;
|
||||||
if (typeof login !== 'string' || typeof id !== 'string') {
|
if (typeof login !== 'string' || typeof id !== 'string') {
|
||||||
|
@ -223,7 +223,7 @@ export class GithubServerService {
|
||||||
}
|
}
|
||||||
}));
|
}));
|
||||||
|
|
||||||
const { login, id } = (await this.httpRequestService.getJson('https://api.github.com/user', 'application/vnd.github.v3+json', 10 * 1000, {
|
const { login, id } = (await this.httpRequestService.getJson('https://api.github.com/user', 'application/vnd.github.v3+json', {
|
||||||
'Authorization': `bearer ${accessToken}`,
|
'Authorization': `bearer ${accessToken}`,
|
||||||
})) as Record<string, unknown>;
|
})) as Record<string, unknown>;
|
||||||
|
|
||||||
|
|
|
@ -63,9 +63,8 @@ export class UrlPreviewService {
|
||||||
this.logger.info(meta.summalyProxy
|
this.logger.info(meta.summalyProxy
|
||||||
? `(Proxy) Getting preview of ${url}@${lang} ...`
|
? `(Proxy) Getting preview of ${url}@${lang} ...`
|
||||||
: `Getting preview of ${url}@${lang} ...`);
|
: `Getting preview of ${url}@${lang} ...`);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const summary = meta.summalyProxy ? await this.httpRequestService.getJson(`${meta.summalyProxy}?${query({
|
const summary = meta.summalyProxy ? await this.httpRequestService.getJson<ReturnType<typeof summaly.default>>(`${meta.summalyProxy}?${query({
|
||||||
url: url,
|
url: url,
|
||||||
lang: lang ?? 'ja-JP',
|
lang: lang ?? 'ja-JP',
|
||||||
})}`) : await summaly.default(url, {
|
})}`) : await summaly.default(url, {
|
||||||
|
|
|
@ -11,14 +11,14 @@
|
||||||
"@rollup/plugin-alias": "4.0.2",
|
"@rollup/plugin-alias": "4.0.2",
|
||||||
"@rollup/plugin-json": "6.0.0",
|
"@rollup/plugin-json": "6.0.0",
|
||||||
"@rollup/pluginutils": "5.0.2",
|
"@rollup/pluginutils": "5.0.2",
|
||||||
"@syuilo/aiscript": "0.11.1",
|
"@syuilo/aiscript": "0.12.0",
|
||||||
"@tabler/icons": "^1.118.0",
|
"@tabler/icons": "^1.118.0",
|
||||||
"@vitejs/plugin-vue": "4.0.0",
|
"@vitejs/plugin-vue": "4.0.0",
|
||||||
"@vue/compiler-sfc": "3.2.45",
|
"@vue/compiler-sfc": "3.2.45",
|
||||||
"autobind-decorator": "2.4.0",
|
"autobind-decorator": "2.4.0",
|
||||||
"autosize": "5.0.2",
|
"autosize": "5.0.2",
|
||||||
"blurhash": "2.0.4",
|
"blurhash": "2.0.4",
|
||||||
"broadcast-channel": "4.18.1",
|
"broadcast-channel": "4.19.1",
|
||||||
"browser-image-resizer": "git+https://github.com/misskey-dev/browser-image-resizer#v2.2.1-misskey.3",
|
"browser-image-resizer": "git+https://github.com/misskey-dev/browser-image-resizer#v2.2.1-misskey.3",
|
||||||
"chart.js": "4.1.1",
|
"chart.js": "4.1.1",
|
||||||
"chartjs-adapter-date-fns": "3.0.0",
|
"chartjs-adapter-date-fns": "3.0.0",
|
||||||
|
@ -34,7 +34,7 @@
|
||||||
"idb-keyval": "6.2.0",
|
"idb-keyval": "6.2.0",
|
||||||
"insert-text-at-cursor": "0.3.0",
|
"insert-text-at-cursor": "0.3.0",
|
||||||
"is-file-animated": "1.0.2",
|
"is-file-animated": "1.0.2",
|
||||||
"json5": "2.2.2",
|
"json5": "2.2.3",
|
||||||
"katex": "0.15.6",
|
"katex": "0.15.6",
|
||||||
"matter-js": "0.18.0",
|
"matter-js": "0.18.0",
|
||||||
"mfm-js": "0.23.0",
|
"mfm-js": "0.23.0",
|
||||||
|
@ -57,7 +57,7 @@
|
||||||
"throttle-debounce": "5.0.0",
|
"throttle-debounce": "5.0.0",
|
||||||
"tinycolor2": "1.5.1",
|
"tinycolor2": "1.5.1",
|
||||||
"tsc-alias": "1.8.2",
|
"tsc-alias": "1.8.2",
|
||||||
"tsconfig-paths": "4.1.1",
|
"tsconfig-paths": "4.1.2",
|
||||||
"twemoji-parser": "14.0.0",
|
"twemoji-parser": "14.0.0",
|
||||||
"typescript": "4.9.4",
|
"typescript": "4.9.4",
|
||||||
"uuid": "9.0.0",
|
"uuid": "9.0.0",
|
||||||
|
@ -76,22 +76,22 @@
|
||||||
"@types/matter-js": "0.18.2",
|
"@types/matter-js": "0.18.2",
|
||||||
"@types/punycode": "2.1.0",
|
"@types/punycode": "2.1.0",
|
||||||
"@types/sanitize-html": "^2.8.0",
|
"@types/sanitize-html": "^2.8.0",
|
||||||
"@types/seedrandom": "3.0.3",
|
"@types/seedrandom": "3.0.4",
|
||||||
"@types/throttle-debounce": "5.0.0",
|
"@types/throttle-debounce": "5.0.0",
|
||||||
"@types/tinycolor2": "1.4.3",
|
"@types/tinycolor2": "1.4.3",
|
||||||
"@types/uuid": "9.0.0",
|
"@types/uuid": "9.0.0",
|
||||||
"@types/websocket": "1.0.5",
|
"@types/websocket": "1.0.5",
|
||||||
"@types/ws": "8.5.3",
|
"@types/ws": "8.5.4",
|
||||||
"@typescript-eslint/eslint-plugin": "5.47.1",
|
"@typescript-eslint/eslint-plugin": "5.47.1",
|
||||||
"@typescript-eslint/parser": "5.47.1",
|
"@typescript-eslint/parser": "5.47.1",
|
||||||
"@vue/runtime-core": "3.2.45",
|
"@vue/runtime-core": "3.2.45",
|
||||||
"cross-env": "7.0.3",
|
"cross-env": "7.0.3",
|
||||||
"cypress": "12.2.0",
|
"cypress": "12.2.0",
|
||||||
"eslint": "8.30.0",
|
"eslint": "8.31.0",
|
||||||
"eslint-plugin-import": "2.26.0",
|
"eslint-plugin-import": "2.26.0",
|
||||||
"eslint-plugin-vue": "9.8.0",
|
"eslint-plugin-vue": "9.8.0",
|
||||||
"start-server-and-test": "1.15.2",
|
"start-server-and-test": "1.15.2",
|
||||||
"vue-eslint-parser": "^9.1.0",
|
"vue-eslint-parser": "^9.1.0",
|
||||||
"vue-tsc": "^1.0.18"
|
"vue-tsc": "^1.0.19"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -23,7 +23,7 @@
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref } from 'vue';
|
import { ref, shallowRef } from 'vue';
|
||||||
import * as Misskey from 'misskey-js';
|
import * as Misskey from 'misskey-js';
|
||||||
import XWindow from '@/components/MkWindow.vue';
|
import XWindow from '@/components/MkWindow.vue';
|
||||||
import MkTextarea from '@/components/form/textarea.vue';
|
import MkTextarea from '@/components/form/textarea.vue';
|
||||||
|
@ -40,7 +40,7 @@ const emit = defineEmits<{
|
||||||
(ev: 'closed'): void;
|
(ev: 'closed'): void;
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
const uiWindow = ref<InstanceType<typeof XWindow>>();
|
const uiWindow = shallowRef<InstanceType<typeof XWindow>>();
|
||||||
const comment = ref(props.initialComment || '');
|
const comment = ref(props.initialComment || '');
|
||||||
|
|
||||||
function send() {
|
function send() {
|
||||||
|
|
|
@ -16,9 +16,9 @@
|
||||||
</li>
|
</li>
|
||||||
</ol>
|
</ol>
|
||||||
<ol v-else-if="emojis.length > 0" ref="suggests" class="emojis">
|
<ol v-else-if="emojis.length > 0" ref="suggests" class="emojis">
|
||||||
<li v-for="emoji in emojis" tabindex="-1" :key="emoji.emoji" @click="complete(type, emoji.emoji)" @keydown="onKeydown">
|
<li v-for="emoji in emojis" :key="emoji.emoji" tabindex="-1" @click="complete(type, emoji.emoji)" @keydown="onKeydown">
|
||||||
<div class="emoji">
|
<div class="emoji">
|
||||||
<MkEmoji :emoji="emoji.emoji" />
|
<MkEmoji :emoji="emoji.emoji"/>
|
||||||
</div>
|
</div>
|
||||||
<!-- eslint-disable-next-line vue/no-v-html -->
|
<!-- eslint-disable-next-line vue/no-v-html -->
|
||||||
<span v-if="q" class="name" v-html="sanitizeHtml(emoji.name.replace(q, `<b>${q}</b>`))"></span>
|
<span v-if="q" class="name" v-html="sanitizeHtml(emoji.name.replace(q, `<b>${q}</b>`))"></span>
|
||||||
|
@ -35,7 +35,8 @@
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { markRaw, ref, onUpdated, onMounted, onBeforeUnmount, nextTick, watch } from 'vue';
|
import { markRaw, ref, shallowRef, onUpdated, onMounted, onBeforeUnmount, nextTick, watch } from 'vue';
|
||||||
|
import sanitizeHtml from 'sanitize-html';
|
||||||
import contains from '@/scripts/contains';
|
import contains from '@/scripts/contains';
|
||||||
import { char2twemojiFilePath, char2fluentEmojiFilePath } from '@/scripts/emoji-base';
|
import { char2twemojiFilePath, char2fluentEmojiFilePath } from '@/scripts/emoji-base';
|
||||||
import { acct } from '@/filters/user';
|
import { acct } from '@/filters/user';
|
||||||
|
@ -45,7 +46,6 @@ import { defaultStore } from '@/store';
|
||||||
import { emojilist } from '@/scripts/emojilist';
|
import { emojilist } from '@/scripts/emojilist';
|
||||||
import { instance } from '@/instance';
|
import { instance } from '@/instance';
|
||||||
import { i18n } from '@/i18n';
|
import { i18n } from '@/i18n';
|
||||||
import sanitizeHtml from 'sanitize-html';
|
|
||||||
|
|
||||||
type EmojiDef = {
|
type EmojiDef = {
|
||||||
emoji: string;
|
emoji: string;
|
||||||
|
@ -136,7 +136,7 @@ const emit = defineEmits<{
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
const suggests = ref<Element>();
|
const suggests = ref<Element>();
|
||||||
const rootEl = ref<HTMLDivElement>();
|
const rootEl = shallowRef<HTMLDivElement>();
|
||||||
|
|
||||||
const fetching = ref(true);
|
const fetching = ref(true);
|
||||||
const users = ref<any[]>([]);
|
const users = ref<any[]>([]);
|
||||||
|
@ -384,7 +384,7 @@ onBeforeUnmount(() => {
|
||||||
position: fixed;
|
position: fixed;
|
||||||
max-width: 100%;
|
max-width: 100%;
|
||||||
margin-top: calc(1em + 8px);
|
margin-top: calc(1em + 8px);
|
||||||
overflow: hidden;
|
overflow: clip;
|
||||||
transition: top 0.1s ease, left 0.1s ease;
|
transition: top 0.1s ease, left 0.1s ease;
|
||||||
|
|
||||||
> ol {
|
> ol {
|
||||||
|
@ -401,7 +401,7 @@ onBeforeUnmount(() => {
|
||||||
align-items: center;
|
align-items: center;
|
||||||
padding: 4px 12px;
|
padding: 4px 12px;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
overflow: hidden;
|
overflow: clip;
|
||||||
font-size: 0.9em;
|
font-size: 0.9em;
|
||||||
cursor: default;
|
cursor: default;
|
||||||
|
|
||||||
|
|
|
@ -47,8 +47,8 @@ const emit = defineEmits<{
|
||||||
(ev: 'click', payload: MouseEvent): void;
|
(ev: 'click', payload: MouseEvent): void;
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
let el = $ref<HTMLElement | null>(null);
|
let el = $shallowRef<HTMLElement | null>(null);
|
||||||
let ripples = $ref<HTMLElement | null>(null);
|
let ripples = $shallowRef<HTMLElement | null>(null);
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
if (props.autofocus) {
|
if (props.autofocus) {
|
||||||
|
@ -207,7 +207,7 @@ function onMousedown(evt: MouseEvent): void {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
border-radius: 6px;
|
border-radius: 6px;
|
||||||
overflow: hidden;
|
overflow: clip;
|
||||||
|
|
||||||
::v-deep(div) {
|
::v-deep(div) {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
|
|
|
@ -6,7 +6,7 @@
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { ref, computed, onMounted, onBeforeUnmount, watch } from 'vue';
|
import { ref, shallowRef, computed, onMounted, onBeforeUnmount, watch } from 'vue';
|
||||||
import { defaultStore } from '@/store';
|
import { defaultStore } from '@/store';
|
||||||
import { i18n } from '@/i18n';
|
import { i18n } from '@/i18n';
|
||||||
|
|
||||||
|
@ -42,7 +42,7 @@ const emit = defineEmits<{
|
||||||
|
|
||||||
const available = ref(false);
|
const available = ref(false);
|
||||||
|
|
||||||
const captchaEl = ref<HTMLDivElement | undefined>();
|
const captchaEl = shallowRef<HTMLDivElement | undefined>();
|
||||||
|
|
||||||
const variable = computed(() => {
|
const variable = computed(() => {
|
||||||
switch (props.provider) {
|
switch (props.provider) {
|
||||||
|
@ -62,7 +62,7 @@ const src = computed(() => {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
const scriptId = computed(() => `script-${props.provider}`)
|
const scriptId = computed(() => `script-${props.provider}`);
|
||||||
|
|
||||||
const captcha = computed<Captcha>(() => window[variable.value] || {} as unknown as Captcha);
|
const captcha = computed<Captcha>(() => window[variable.value] || {} as unknown as Captcha);
|
||||||
|
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
<template>
|
<template>
|
||||||
<div class="cbbedffa">
|
<div class="cbbedffa">
|
||||||
<canvas ref="chartEl"></canvas>
|
<canvas ref="chartEl"></canvas>
|
||||||
|
<MkChartLegend ref="legendEl" style="margin-top: 8px;"/>
|
||||||
<div v-if="fetching" class="fetching">
|
<div v-if="fetching" class="fetching">
|
||||||
<MkLoading/>
|
<MkLoading/>
|
||||||
</div>
|
</div>
|
||||||
|
@ -13,27 +14,9 @@
|
||||||
id-denylist violation when setting it. This is causing about 60+ lint issues.
|
id-denylist violation when setting it. This is causing about 60+ lint issues.
|
||||||
As this is part of Chart.js's API it makes sense to disable the check here.
|
As this is part of Chart.js's API it makes sense to disable the check here.
|
||||||
*/
|
*/
|
||||||
import { onMounted, ref, watch, PropType, onUnmounted } from 'vue';
|
import { onMounted, ref, shallowRef, watch, PropType, onUnmounted } from 'vue';
|
||||||
import {
|
import { Chart } from 'chart.js';
|
||||||
Chart,
|
|
||||||
ArcElement,
|
|
||||||
LineElement,
|
|
||||||
BarElement,
|
|
||||||
PointElement,
|
|
||||||
BarController,
|
|
||||||
LineController,
|
|
||||||
CategoryScale,
|
|
||||||
LinearScale,
|
|
||||||
TimeScale,
|
|
||||||
Legend,
|
|
||||||
Title,
|
|
||||||
Tooltip,
|
|
||||||
SubTitle,
|
|
||||||
Filler,
|
|
||||||
} from 'chart.js';
|
|
||||||
import 'chartjs-adapter-date-fns';
|
|
||||||
import { enUS } from 'date-fns/locale';
|
import { enUS } from 'date-fns/locale';
|
||||||
import zoomPlugin from 'chartjs-plugin-zoom';
|
|
||||||
import gradient from 'chartjs-plugin-gradient';
|
import gradient from 'chartjs-plugin-gradient';
|
||||||
import * as os from '@/os';
|
import * as os from '@/os';
|
||||||
import { defaultStore } from '@/store';
|
import { defaultStore } from '@/store';
|
||||||
|
@ -41,6 +24,11 @@ import { useChartTooltip } from '@/scripts/use-chart-tooltip';
|
||||||
import { chartVLine } from '@/scripts/chart-vline';
|
import { chartVLine } from '@/scripts/chart-vline';
|
||||||
import { alpha } from '@/scripts/color';
|
import { alpha } from '@/scripts/color';
|
||||||
import date from '@/filters/date';
|
import date from '@/filters/date';
|
||||||
|
import { initChart } from '@/scripts/init-chart';
|
||||||
|
import { chartLegend } from '@/scripts/chart-legend';
|
||||||
|
import MkChartLegend from '@/components/MkChartLegend.vue';
|
||||||
|
|
||||||
|
initChart();
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
src: {
|
src: {
|
||||||
|
@ -82,24 +70,7 @@ const props = defineProps({
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
Chart.register(
|
let legendEl = $shallowRef<InstanceType<typeof MkChartLegend>>();
|
||||||
ArcElement,
|
|
||||||
LineElement,
|
|
||||||
BarElement,
|
|
||||||
PointElement,
|
|
||||||
BarController,
|
|
||||||
LineController,
|
|
||||||
CategoryScale,
|
|
||||||
LinearScale,
|
|
||||||
TimeScale,
|
|
||||||
Legend,
|
|
||||||
Title,
|
|
||||||
Tooltip,
|
|
||||||
SubTitle,
|
|
||||||
Filler,
|
|
||||||
zoomPlugin,
|
|
||||||
gradient,
|
|
||||||
);
|
|
||||||
|
|
||||||
const sum = (...arr) => arr.reduce((r, a) => r.map((b, i) => a[i] + b));
|
const sum = (...arr) => arr.reduce((r, a) => r.map((b, i) => a[i] + b));
|
||||||
const negate = arr => arr.map(x => -x);
|
const negate = arr => arr.map(x => -x);
|
||||||
|
@ -135,7 +106,7 @@ let chartData: {
|
||||||
}[];
|
}[];
|
||||||
} = null;
|
} = null;
|
||||||
|
|
||||||
const chartEl = ref<HTMLCanvasElement>(null);
|
const chartEl = shallowRef<HTMLCanvasElement>(null);
|
||||||
const fetching = ref(true);
|
const fetching = ref(true);
|
||||||
|
|
||||||
const getDate = (ago: number) => {
|
const getDate = (ago: number) => {
|
||||||
|
@ -161,12 +132,8 @@ const render = () => {
|
||||||
chartInstance.destroy();
|
chartInstance.destroy();
|
||||||
}
|
}
|
||||||
|
|
||||||
const gridColor = defaultStore.state.darkMode ? 'rgba(255, 255, 255, 0.1)' : 'rgba(0, 0, 0, 0.1)';
|
|
||||||
const vLineColor = defaultStore.state.darkMode ? 'rgba(255, 255, 255, 0.2)' : 'rgba(0, 0, 0, 0.2)';
|
const vLineColor = defaultStore.state.darkMode ? 'rgba(255, 255, 255, 0.2)' : 'rgba(0, 0, 0, 0.2)';
|
||||||
|
|
||||||
// フォントカラー
|
|
||||||
Chart.defaults.color = getComputedStyle(document.documentElement).getPropertyValue('--fg');
|
|
||||||
|
|
||||||
const maxes = chartData.series.map((x, i) => Math.max(...x.data.map(d => d.y)));
|
const maxes = chartData.series.map((x, i) => Math.max(...x.data.map(d => d.y)));
|
||||||
|
|
||||||
chartInstance = new Chart(chartEl.value, {
|
chartInstance = new Chart(chartEl.value, {
|
||||||
|
@ -221,8 +188,6 @@ const render = () => {
|
||||||
unit: props.span === 'day' ? 'month' : 'day',
|
unit: props.span === 'day' ? 'month' : 'day',
|
||||||
},
|
},
|
||||||
grid: {
|
grid: {
|
||||||
color: gridColor,
|
|
||||||
borderColor: 'rgb(0, 0, 0, 0)',
|
|
||||||
},
|
},
|
||||||
ticks: {
|
ticks: {
|
||||||
display: props.detailed,
|
display: props.detailed,
|
||||||
|
@ -241,8 +206,6 @@ const render = () => {
|
||||||
stacked: props.stacked,
|
stacked: props.stacked,
|
||||||
suggestedMax: 50,
|
suggestedMax: 50,
|
||||||
grid: {
|
grid: {
|
||||||
color: gridColor,
|
|
||||||
borderColor: 'rgb(0, 0, 0, 0)',
|
|
||||||
},
|
},
|
||||||
ticks: {
|
ticks: {
|
||||||
display: props.detailed,
|
display: props.detailed,
|
||||||
|
@ -260,14 +223,9 @@ const render = () => {
|
||||||
hoverBorderWidth: 2,
|
hoverBorderWidth: 2,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
animation: false,
|
|
||||||
plugins: {
|
plugins: {
|
||||||
legend: {
|
legend: {
|
||||||
display: props.detailed,
|
display: false,
|
||||||
position: 'bottom',
|
|
||||||
labels: {
|
|
||||||
boxWidth: 16,
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
tooltip: {
|
tooltip: {
|
||||||
enabled: false,
|
enabled: false,
|
||||||
|
@ -307,7 +265,7 @@ const render = () => {
|
||||||
gradient,
|
gradient,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
plugins: [chartVLine(vLineColor)],
|
plugins: [chartVLine(vLineColor), ...(props.detailed ? [chartLegend(legendEl)] : [])],
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -742,6 +700,33 @@ const fetchPerUserNotesChart = async (): Promise<typeof chartData> => {
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const fetchPerUserPvChart = async (): Promise<typeof chartData> => {
|
||||||
|
const raw = await os.apiGet('charts/user/pv', { userId: props.args.user.id, limit: props.limit, span: props.span });
|
||||||
|
return {
|
||||||
|
series: [{
|
||||||
|
name: 'Unique PV (user)',
|
||||||
|
type: 'area',
|
||||||
|
data: format(raw.upv.user),
|
||||||
|
color: colors.purple,
|
||||||
|
}, {
|
||||||
|
name: 'PV (user)',
|
||||||
|
type: 'area',
|
||||||
|
data: format(raw.pv.user),
|
||||||
|
color: colors.green,
|
||||||
|
}, {
|
||||||
|
name: 'Unique PV (visitor)',
|
||||||
|
type: 'area',
|
||||||
|
data: format(raw.upv.visitor),
|
||||||
|
color: colors.yellow,
|
||||||
|
}, {
|
||||||
|
name: 'PV (visitor)',
|
||||||
|
type: 'area',
|
||||||
|
data: format(raw.pv.visitor),
|
||||||
|
color: colors.blue,
|
||||||
|
}],
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
const fetchPerUserFollowingChart = async (): Promise<typeof chartData> => {
|
const fetchPerUserFollowingChart = async (): Promise<typeof chartData> => {
|
||||||
const raw = await os.apiGet('charts/user/following', { userId: props.args.user.id, limit: props.limit, span: props.span });
|
const raw = await os.apiGet('charts/user/following', { userId: props.args.user.id, limit: props.limit, span: props.span });
|
||||||
return {
|
return {
|
||||||
|
@ -814,6 +799,7 @@ const fetchAndRender = async () => {
|
||||||
case 'instance-drive-files-total': return fetchInstanceDriveFilesChart(true);
|
case 'instance-drive-files-total': return fetchInstanceDriveFilesChart(true);
|
||||||
|
|
||||||
case 'per-user-notes': return fetchPerUserNotesChart();
|
case 'per-user-notes': return fetchPerUserNotesChart();
|
||||||
|
case 'per-user-pv': return fetchPerUserPvChart();
|
||||||
case 'per-user-following': return fetchPerUserFollowingChart();
|
case 'per-user-following': return fetchPerUserFollowingChart();
|
||||||
case 'per-user-followers': return fetchPerUserFollowersChart();
|
case 'per-user-followers': return fetchPerUserFollowersChart();
|
||||||
case 'per-user-drive': return fetchPerUserDriveChart();
|
case 'per-user-drive': return fetchPerUserDriveChart();
|
||||||
|
|
|
@ -0,0 +1,75 @@
|
||||||
|
<template>
|
||||||
|
<div :class="$style.root">
|
||||||
|
<button v-for="item in items" class="_button item" :class="{ disabled: item.hidden }" @click="onClick(item)">
|
||||||
|
<span class="box" :style="{ background: chart.config.type === 'line' ? item.strokeStyle?.toString() : item.fillStyle?.toString() }"></span>
|
||||||
|
{{ item.text }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import { onMounted, ref, shallowRef, watch, PropType, onUnmounted } from 'vue';
|
||||||
|
import { Chart, LegendItem } from 'chart.js';
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
});
|
||||||
|
|
||||||
|
let chart = $shallowRef<Chart>();
|
||||||
|
let items = $shallowRef<LegendItem[]>([]);
|
||||||
|
|
||||||
|
function update(_chart: Chart, _items: LegendItem[]) {
|
||||||
|
chart = _chart,
|
||||||
|
items = _items;
|
||||||
|
}
|
||||||
|
|
||||||
|
function onClick(item: LegendItem) {
|
||||||
|
if (chart == null) return;
|
||||||
|
const { type } = chart.config;
|
||||||
|
if (type === 'pie' || type === 'doughnut') {
|
||||||
|
// Pie and doughnut charts only have a single dataset and visibility is per item
|
||||||
|
chart.toggleDataVisibility(item.index);
|
||||||
|
} else {
|
||||||
|
chart.setDatasetVisibility(item.datasetIndex, !chart.isDatasetVisible(item.datasetIndex));
|
||||||
|
}
|
||||||
|
chart.update();
|
||||||
|
}
|
||||||
|
|
||||||
|
defineExpose({
|
||||||
|
update,
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" module>
|
||||||
|
.root {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 8px;
|
||||||
|
|
||||||
|
&:global {
|
||||||
|
> .item {
|
||||||
|
font-size: 85%;
|
||||||
|
padding: 4px 12px 4px 8px;
|
||||||
|
border: solid 1px var(--divider);
|
||||||
|
border-radius: 999px;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
border-color: var(--inputBorderHover);
|
||||||
|
}
|
||||||
|
|
||||||
|
&.disabled {
|
||||||
|
text-decoration: line-through;
|
||||||
|
opacity: 0.6;
|
||||||
|
}
|
||||||
|
|
||||||
|
> .box {
|
||||||
|
display: inline-block;
|
||||||
|
width: 12px;
|
||||||
|
height: 12px;
|
||||||
|
border-radius: 100%;
|
||||||
|
vertical-align: -10%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -22,7 +22,7 @@ const emit = defineEmits<{
|
||||||
(ev: 'closed'): void;
|
(ev: 'closed'): void;
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
let rootEl = $ref<HTMLDivElement>();
|
let rootEl = $shallowRef<HTMLDivElement>();
|
||||||
|
|
||||||
let zIndex = $ref<number>(os.claimZIndex('high'));
|
let zIndex = $ref<number>(os.claimZIndex('high'));
|
||||||
|
|
||||||
|
|
|
@ -50,8 +50,8 @@ const props = defineProps<{
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
const imgUrl = getProxiedImageUrl(props.file.url);
|
const imgUrl = getProxiedImageUrl(props.file.url);
|
||||||
let dialogEl = $ref<InstanceType<typeof XModalWindow>>();
|
let dialogEl = $shallowRef<InstanceType<typeof XModalWindow>>();
|
||||||
let imgEl = $ref<HTMLImageElement>();
|
let imgEl = $shallowRef<HTMLImageElement>();
|
||||||
let cropper: Cropper | null = null;
|
let cropper: Cropper | null = null;
|
||||||
let loading = $ref(true);
|
let loading = $ref(true);
|
||||||
|
|
||||||
|
|
|
@ -39,7 +39,7 @@
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { onBeforeUnmount, onMounted, ref } from 'vue';
|
import { onBeforeUnmount, onMounted, ref, shallowRef } from 'vue';
|
||||||
import MkModal from '@/components/MkModal.vue';
|
import MkModal from '@/components/MkModal.vue';
|
||||||
import MkButton from '@/components/MkButton.vue';
|
import MkButton from '@/components/MkButton.vue';
|
||||||
import MkInput from '@/components/form/input.vue';
|
import MkInput from '@/components/form/input.vue';
|
||||||
|
@ -94,7 +94,7 @@ const emit = defineEmits<{
|
||||||
(ev: 'closed'): void;
|
(ev: 'closed'): void;
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
const modal = ref<InstanceType<typeof MkModal>>();
|
const modal = shallowRef<InstanceType<typeof MkModal>>();
|
||||||
|
|
||||||
const inputValue = ref(props.input?.default || null);
|
const inputValue = ref(props.input?.default || null);
|
||||||
const selectedValue = ref(props.select?.default || null);
|
const selectedValue = ref(props.select?.default || null);
|
||||||
|
|
|
@ -88,7 +88,7 @@
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { markRaw, nextTick, onActivated, onBeforeUnmount, onMounted, ref, watch } from 'vue';
|
import { markRaw, nextTick, onActivated, onBeforeUnmount, onMounted, ref, shallowRef, watch } from 'vue';
|
||||||
import * as Misskey from 'misskey-js';
|
import * as Misskey from 'misskey-js';
|
||||||
import MkButton from './MkButton.vue';
|
import MkButton from './MkButton.vue';
|
||||||
import XNavFolder from '@/components/MkDrive.navFolder.vue';
|
import XNavFolder from '@/components/MkDrive.navFolder.vue';
|
||||||
|
@ -118,8 +118,8 @@ const emit = defineEmits<{
|
||||||
(ev: 'open-folder', v: Misskey.entities.DriveFolder): void;
|
(ev: 'open-folder', v: Misskey.entities.DriveFolder): void;
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
const loadMoreFiles = ref<InstanceType<typeof MkButton>>();
|
const loadMoreFiles = shallowRef<InstanceType<typeof MkButton>>();
|
||||||
const fileInput = ref<HTMLInputElement>();
|
const fileInput = shallowRef<HTMLInputElement>();
|
||||||
|
|
||||||
const folder = ref<Misskey.entities.DriveFolder | null>(null);
|
const folder = ref<Misskey.entities.DriveFolder | null>(null);
|
||||||
const files = ref<Misskey.entities.DriveFile[]>([]);
|
const files = ref<Misskey.entities.DriveFile[]>([]);
|
||||||
|
|
|
@ -19,7 +19,7 @@
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { ref } from 'vue';
|
import { ref, shallowRef } from 'vue';
|
||||||
import * as Misskey from 'misskey-js';
|
import * as Misskey from 'misskey-js';
|
||||||
import XDrive from '@/components/MkDrive.vue';
|
import XDrive from '@/components/MkDrive.vue';
|
||||||
import XModalWindow from '@/components/MkModalWindow.vue';
|
import XModalWindow from '@/components/MkModalWindow.vue';
|
||||||
|
@ -38,7 +38,7 @@ const emit = defineEmits<{
|
||||||
(ev: 'closed'): void;
|
(ev: 'closed'): void;
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
const dialog = ref<InstanceType<typeof XModalWindow>>();
|
const dialog = shallowRef<InstanceType<typeof XModalWindow>>();
|
||||||
|
|
||||||
const selected = ref<Misskey.entities.DriveFile[]>([]);
|
const selected = ref<Misskey.entities.DriveFile[]>([]);
|
||||||
|
|
||||||
|
|
|
@ -77,7 +77,7 @@
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { ref, computed, watch, onMounted } from 'vue';
|
import { ref, shallowRef, computed, watch, onMounted } from 'vue';
|
||||||
import * as Misskey from 'misskey-js';
|
import * as Misskey from 'misskey-js';
|
||||||
import XSection from '@/components/MkEmojiPicker.section.vue';
|
import XSection from '@/components/MkEmojiPicker.section.vue';
|
||||||
import { emojilist, UnicodeEmojiDef, unicodeEmojiCategories as categories } from '@/scripts/emojilist';
|
import { emojilist, UnicodeEmojiDef, unicodeEmojiCategories as categories } from '@/scripts/emojilist';
|
||||||
|
@ -102,8 +102,8 @@ const emit = defineEmits<{
|
||||||
(ev: 'chosen', v: string): void;
|
(ev: 'chosen', v: string): void;
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
const search = ref<HTMLInputElement>();
|
const search = shallowRef<HTMLInputElement>();
|
||||||
const emojis = ref<HTMLDivElement>();
|
const emojis = shallowRef<HTMLDivElement>();
|
||||||
|
|
||||||
const {
|
const {
|
||||||
reactions: pinned,
|
reactions: pinned,
|
||||||
|
|
|
@ -26,7 +26,7 @@
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { ref } from 'vue';
|
import { shallowRef } from 'vue';
|
||||||
import MkModal from '@/components/MkModal.vue';
|
import MkModal from '@/components/MkModal.vue';
|
||||||
import MkEmojiPicker from '@/components/MkEmojiPicker.vue';
|
import MkEmojiPicker from '@/components/MkEmojiPicker.vue';
|
||||||
import { defaultStore } from '@/store';
|
import { defaultStore } from '@/store';
|
||||||
|
@ -48,8 +48,8 @@ const emit = defineEmits<{
|
||||||
(ev: 'closed'): void;
|
(ev: 'closed'): void;
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
const modal = ref<InstanceType<typeof MkModal>>();
|
const modal = shallowRef<InstanceType<typeof MkModal>>();
|
||||||
const picker = ref<InstanceType<typeof MkEmojiPicker>>();
|
const picker = shallowRef<InstanceType<typeof MkEmojiPicker>>();
|
||||||
|
|
||||||
function chosen(emoji: any) {
|
function chosen(emoji: any) {
|
||||||
emit('done', emoji);
|
emit('done', emoji);
|
||||||
|
|
|
@ -37,7 +37,7 @@ const emit = defineEmits<{
|
||||||
(ev: 'closed'): void;
|
(ev: 'closed'): void;
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
const dialog = $ref<InstanceType<typeof XModalWindow>>();
|
const dialog = $shallowRef<InstanceType<typeof XModalWindow>>();
|
||||||
|
|
||||||
let caption = $ref(props.default);
|
let caption = $ref(props.default);
|
||||||
|
|
||||||
|
|
|
@ -9,57 +9,25 @@
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { markRaw, version as vueVersion, onMounted, onBeforeUnmount, nextTick, watch } from 'vue';
|
import { markRaw, version as vueVersion, onMounted, onBeforeUnmount, nextTick, watch } from 'vue';
|
||||||
import {
|
import { Chart } from 'chart.js';
|
||||||
Chart,
|
|
||||||
ArcElement,
|
|
||||||
LineElement,
|
|
||||||
BarElement,
|
|
||||||
PointElement,
|
|
||||||
BarController,
|
|
||||||
LineController,
|
|
||||||
CategoryScale,
|
|
||||||
LinearScale,
|
|
||||||
TimeScale,
|
|
||||||
Legend,
|
|
||||||
Title,
|
|
||||||
Tooltip,
|
|
||||||
SubTitle,
|
|
||||||
Filler,
|
|
||||||
} from 'chart.js';
|
|
||||||
import { enUS } from 'date-fns/locale';
|
import { enUS } from 'date-fns/locale';
|
||||||
import tinycolor from 'tinycolor2';
|
import tinycolor from 'tinycolor2';
|
||||||
|
import { MatrixController, MatrixElement } from 'chartjs-chart-matrix';
|
||||||
import * as os from '@/os';
|
import * as os from '@/os';
|
||||||
import 'chartjs-adapter-date-fns';
|
|
||||||
import { defaultStore } from '@/store';
|
import { defaultStore } from '@/store';
|
||||||
import { useChartTooltip } from '@/scripts/use-chart-tooltip';
|
import { useChartTooltip } from '@/scripts/use-chart-tooltip';
|
||||||
import { MatrixController, MatrixElement } from 'chartjs-chart-matrix';
|
|
||||||
import { chartVLine } from '@/scripts/chart-vline';
|
import { chartVLine } from '@/scripts/chart-vline';
|
||||||
import { alpha } from '@/scripts/color';
|
import { alpha } from '@/scripts/color';
|
||||||
|
import { initChart } from '@/scripts/init-chart';
|
||||||
|
|
||||||
Chart.register(
|
initChart();
|
||||||
ArcElement,
|
|
||||||
LineElement,
|
|
||||||
BarElement,
|
|
||||||
PointElement,
|
|
||||||
BarController,
|
|
||||||
LineController,
|
|
||||||
CategoryScale,
|
|
||||||
LinearScale,
|
|
||||||
TimeScale,
|
|
||||||
Legend,
|
|
||||||
Title,
|
|
||||||
Tooltip,
|
|
||||||
SubTitle,
|
|
||||||
Filler,
|
|
||||||
MatrixController, MatrixElement,
|
|
||||||
);
|
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
src: string;
|
src: string;
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
const rootEl = $ref<HTMLDivElement>(null);
|
const rootEl = $shallowRef<HTMLDivElement>(null);
|
||||||
const chartEl = $ref<HTMLCanvasElement>(null);
|
const chartEl = $shallowRef<HTMLCanvasElement>(null);
|
||||||
const now = new Date();
|
const now = new Date();
|
||||||
let chartInstance: Chart = null;
|
let chartInstance: Chart = null;
|
||||||
let fetching = $ref(true);
|
let fetching = $ref(true);
|
||||||
|
@ -123,11 +91,6 @@ async function renderChart() {
|
||||||
|
|
||||||
await nextTick();
|
await nextTick();
|
||||||
|
|
||||||
const gridColor = defaultStore.state.darkMode ? 'rgba(255, 255, 255, 0.1)' : 'rgba(0, 0, 0, 0.1)';
|
|
||||||
|
|
||||||
// フォントカラー
|
|
||||||
Chart.defaults.color = getComputedStyle(document.documentElement).getPropertyValue('--fg');
|
|
||||||
|
|
||||||
const color = defaultStore.state.darkMode ? '#b4e900' : '#86b300';
|
const color = defaultStore.state.darkMode ? '#b4e900' : '#86b300';
|
||||||
|
|
||||||
// 視覚上の分かりやすさのため上から最も大きい3つの値の平均を最大値とする
|
// 視覚上の分かりやすさのため上から最も大きい3つの値の平均を最大値とする
|
||||||
|
@ -191,8 +154,6 @@ async function renderChart() {
|
||||||
},
|
},
|
||||||
grid: {
|
grid: {
|
||||||
display: false,
|
display: false,
|
||||||
color: gridColor,
|
|
||||||
borderColor: 'rgb(0, 0, 0, 0)',
|
|
||||||
},
|
},
|
||||||
ticks: {
|
ticks: {
|
||||||
display: true,
|
display: true,
|
||||||
|
@ -206,8 +167,6 @@ async function renderChart() {
|
||||||
position: 'right',
|
position: 'right',
|
||||||
grid: {
|
grid: {
|
||||||
display: false,
|
display: false,
|
||||||
color: gridColor,
|
|
||||||
borderColor: 'rgb(0, 0, 0, 0)',
|
|
||||||
},
|
},
|
||||||
ticks: {
|
ticks: {
|
||||||
maxRotation: 0,
|
maxRotation: 0,
|
||||||
|
@ -220,7 +179,6 @@ async function renderChart() {
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
animation: false,
|
|
||||||
plugins: {
|
plugins: {
|
||||||
legend: {
|
legend: {
|
||||||
display: false,
|
display: false,
|
||||||
|
|
|
@ -28,7 +28,7 @@ const emit = defineEmits<{
|
||||||
(ev: 'closed'): void;
|
(ev: 'closed'): void;
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
const modal = $ref<InstanceType<typeof MkModal>>();
|
const modal = $shallowRef<InstanceType<typeof MkModal>>();
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
|
|
|
@ -24,7 +24,7 @@ const props = withDefaults(defineProps<{
|
||||||
cover: true,
|
cover: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
const canvas = $ref<HTMLCanvasElement>();
|
const canvas = $shallowRef<HTMLCanvasElement>();
|
||||||
let loaded = $ref(false);
|
let loaded = $ref(false);
|
||||||
|
|
||||||
function draw() {
|
function draw() {
|
||||||
|
|
|
@ -77,24 +77,7 @@
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { onMounted } from 'vue';
|
import { onMounted } from 'vue';
|
||||||
import {
|
import { Chart } from 'chart.js';
|
||||||
Chart,
|
|
||||||
ArcElement,
|
|
||||||
LineElement,
|
|
||||||
BarElement,
|
|
||||||
PointElement,
|
|
||||||
BarController,
|
|
||||||
LineController,
|
|
||||||
CategoryScale,
|
|
||||||
LinearScale,
|
|
||||||
TimeScale,
|
|
||||||
Legend,
|
|
||||||
Title,
|
|
||||||
Tooltip,
|
|
||||||
SubTitle,
|
|
||||||
Filler,
|
|
||||||
DoughnutController,
|
|
||||||
} from 'chart.js';
|
|
||||||
import MkSelect from '@/components/form/select.vue';
|
import MkSelect from '@/components/form/select.vue';
|
||||||
import MkChart from '@/components/MkChart.vue';
|
import MkChart from '@/components/MkChart.vue';
|
||||||
import { useChartTooltip } from '@/scripts/use-chart-tooltip';
|
import { useChartTooltip } from '@/scripts/use-chart-tooltip';
|
||||||
|
@ -103,31 +86,16 @@ import { i18n } from '@/i18n';
|
||||||
import MkHeatmap from '@/components/MkHeatmap.vue';
|
import MkHeatmap from '@/components/MkHeatmap.vue';
|
||||||
import MkFolder from '@/components/MkFolder.vue';
|
import MkFolder from '@/components/MkFolder.vue';
|
||||||
import MkRetentionHeatmap from '@/components/MkRetentionHeatmap.vue';
|
import MkRetentionHeatmap from '@/components/MkRetentionHeatmap.vue';
|
||||||
|
import { initChart } from '@/scripts/init-chart';
|
||||||
|
|
||||||
Chart.register(
|
initChart();
|
||||||
ArcElement,
|
|
||||||
LineElement,
|
|
||||||
BarElement,
|
|
||||||
PointElement,
|
|
||||||
BarController,
|
|
||||||
LineController,
|
|
||||||
DoughnutController,
|
|
||||||
CategoryScale,
|
|
||||||
LinearScale,
|
|
||||||
TimeScale,
|
|
||||||
Legend,
|
|
||||||
Title,
|
|
||||||
Tooltip,
|
|
||||||
SubTitle,
|
|
||||||
Filler,
|
|
||||||
);
|
|
||||||
|
|
||||||
const chartLimit = 500;
|
const chartLimit = 500;
|
||||||
let chartSpan = $ref<'hour' | 'day'>('hour');
|
let chartSpan = $ref<'hour' | 'day'>('hour');
|
||||||
let chartSrc = $ref('active-users');
|
let chartSrc = $ref('active-users');
|
||||||
let heatmapSrc = $ref('active-users');
|
let heatmapSrc = $ref('active-users');
|
||||||
let subDoughnutEl = $ref<HTMLCanvasElement>();
|
let subDoughnutEl = $shallowRef<HTMLCanvasElement>();
|
||||||
let pubDoughnutEl = $ref<HTMLCanvasElement>();
|
let pubDoughnutEl = $shallowRef<HTMLCanvasElement>();
|
||||||
|
|
||||||
const { handler: externalTooltipHandler1 } = useChartTooltip({
|
const { handler: externalTooltipHandler1 } = useChartTooltip({
|
||||||
position: 'middle',
|
position: 'middle',
|
||||||
|
|
|
@ -44,7 +44,7 @@ const preferedModalType = (deviceKind === 'desktop' && props.src != null) ? 'pop
|
||||||
deviceKind === 'smartphone' ? 'drawer' :
|
deviceKind === 'smartphone' ? 'drawer' :
|
||||||
'dialog';
|
'dialog';
|
||||||
|
|
||||||
const modal = $ref<InstanceType<typeof MkModal>>();
|
const modal = $shallowRef<InstanceType<typeof MkModal>>();
|
||||||
|
|
||||||
const menu = defaultStore.state.menu;
|
const menu = defaultStore.state.menu;
|
||||||
|
|
||||||
|
|
|
@ -38,7 +38,7 @@ const props = withDefaults(defineProps<{
|
||||||
}>(), {
|
}>(), {
|
||||||
});
|
});
|
||||||
|
|
||||||
const audioEl = $ref<HTMLAudioElement | null>();
|
const audioEl = $shallowRef<HTMLAudioElement | null>();
|
||||||
let hide = $ref(true);
|
let hide = $ref(true);
|
||||||
|
|
||||||
function volumechange() {
|
function volumechange() {
|
||||||
|
|
|
@ -6,7 +6,7 @@
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { on } from 'events';
|
import { on } from 'events';
|
||||||
import { nextTick, onBeforeUnmount, onMounted, onUnmounted, ref, watch } from 'vue';
|
import { nextTick, onBeforeUnmount, onMounted, onUnmounted, ref, shallowRef, watch } from 'vue';
|
||||||
import MkMenu from './MkMenu.vue';
|
import MkMenu from './MkMenu.vue';
|
||||||
import { MenuItem } from '@/types/menu';
|
import { MenuItem } from '@/types/menu';
|
||||||
import * as os from '@/os';
|
import * as os from '@/os';
|
||||||
|
@ -24,7 +24,7 @@ const emit = defineEmits<{
|
||||||
(ev: 'actioned'): void;
|
(ev: 'actioned'): void;
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
const el = ref<HTMLElement>();
|
const el = shallowRef<HTMLElement>();
|
||||||
const align = 'left';
|
const align = 'left';
|
||||||
|
|
||||||
function setPosition() {
|
function setPosition() {
|
||||||
|
|
|
@ -78,11 +78,11 @@ const emit = defineEmits<{
|
||||||
(ev: 'close', actioned?: boolean): void;
|
(ev: 'close', actioned?: boolean): void;
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
let itemsEl = $ref<HTMLDivElement>();
|
let itemsEl = $shallowRef<HTMLDivElement>();
|
||||||
|
|
||||||
let items2: InnerMenuItem[] = $ref([]);
|
let items2: InnerMenuItem[] = $ref([]);
|
||||||
|
|
||||||
let child = $ref<InstanceType<typeof XChild>>();
|
let child = $shallowRef<InstanceType<typeof XChild>>();
|
||||||
|
|
||||||
let keymap = $computed(() => ({
|
let keymap = $computed(() => ({
|
||||||
'up|k|shift+tab': focusUp,
|
'up|k|shift+tab': focusUp,
|
||||||
|
@ -112,7 +112,7 @@ watch(() => props.items, () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
let childMenu = $ref<MenuItem[] | null>();
|
let childMenu = $ref<MenuItem[] | null>();
|
||||||
let childTarget = $ref<HTMLElement | null>();
|
let childTarget = $shallowRef<HTMLElement | null>();
|
||||||
|
|
||||||
function closeChild() {
|
function closeChild() {
|
||||||
childMenu = null;
|
childMenu = null;
|
||||||
|
@ -203,7 +203,7 @@ onBeforeUnmount(() => {
|
||||||
> .item {
|
> .item {
|
||||||
display: block;
|
display: block;
|
||||||
position: relative;
|
position: relative;
|
||||||
padding: 6px 16px;
|
padding: 5px 16px;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
|
@ -226,10 +226,6 @@ onBeforeUnmount(() => {
|
||||||
border-radius: 6px;
|
border-radius: 6px;
|
||||||
}
|
}
|
||||||
|
|
||||||
> * {
|
|
||||||
position: relative;
|
|
||||||
}
|
|
||||||
|
|
||||||
&:not(:disabled):hover {
|
&:not(:disabled):hover {
|
||||||
color: var(--accent);
|
color: var(--accent);
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
|
|
|
@ -61,9 +61,9 @@ let maxHeight = $ref<number>();
|
||||||
let fixed = $ref(false);
|
let fixed = $ref(false);
|
||||||
let transformOrigin = $ref('center');
|
let transformOrigin = $ref('center');
|
||||||
let showing = $ref(true);
|
let showing = $ref(true);
|
||||||
let content = $ref<HTMLElement>();
|
let content = $shallowRef<HTMLElement>();
|
||||||
const zIndex = os.claimZIndex(props.zPriority);
|
const zIndex = os.claimZIndex(props.zPriority);
|
||||||
const type = $computed(() => {
|
const type = $computed<ModalTypes>(() => {
|
||||||
if (props.preferType === 'auto') {
|
if (props.preferType === 'auto') {
|
||||||
if (!defaultStore.state.disableDrawer && isTouchUsing && deviceKind === 'smartphone') {
|
if (!defaultStore.state.disableDrawer && isTouchUsing && deviceKind === 'smartphone') {
|
||||||
return 'drawer';
|
return 'drawer';
|
||||||
|
@ -383,6 +383,7 @@ defineExpose({
|
||||||
mask-image: linear-gradient(0deg, rgba(0,0,0,0) 0%, rgba(0,0,0,1) 32px, rgba(0,0,0,1) calc(100% - 32px), rgba(0,0,0,0) 100%);
|
mask-image: linear-gradient(0deg, rgba(0,0,0,0) 0%, rgba(0,0,0,1) 32px, rgba(0,0,0,1) calc(100% - 32px), rgba(0,0,0,0) 100%);
|
||||||
overflow: auto;
|
overflow: auto;
|
||||||
display: flex;
|
display: flex;
|
||||||
|
container-type: inline-size;
|
||||||
|
|
||||||
@media (max-width: 500px) {
|
@media (max-width: 500px) {
|
||||||
padding: 16px;
|
padding: 16px;
|
||||||
|
|
|
@ -49,7 +49,7 @@ router.addListener('push', ctx => {
|
||||||
|
|
||||||
let pageMetadata = $ref<null | ComputedRef<PageMetadata>>();
|
let pageMetadata = $ref<null | ComputedRef<PageMetadata>>();
|
||||||
let rootEl = $ref();
|
let rootEl = $ref();
|
||||||
let modal = $ref<InstanceType<typeof MkModal>>();
|
let modal = $shallowRef<InstanceType<typeof MkModal>>();
|
||||||
let path = $ref(props.initialPath);
|
let path = $ref(props.initialPath);
|
||||||
let width = $ref(860);
|
let width = $ref(860);
|
||||||
let height = $ref(660);
|
let height = $ref(660);
|
||||||
|
|
|
@ -41,9 +41,9 @@ const emit = defineEmits<{
|
||||||
(event: 'ok'): void;
|
(event: 'ok'): void;
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
let modal = $ref<InstanceType<typeof MkModal>>();
|
let modal = $shallowRef<InstanceType<typeof MkModal>>();
|
||||||
let rootEl = $ref<HTMLElement>();
|
let rootEl = $shallowRef<HTMLElement>();
|
||||||
let headerEl = $ref<HTMLElement>();
|
let headerEl = $shallowRef<HTMLElement>();
|
||||||
let bodyWidth = $ref(0);
|
let bodyWidth = $ref(0);
|
||||||
let bodyHeight = $ref(0);
|
let bodyHeight = $ref(0);
|
||||||
|
|
||||||
|
|
|
@ -59,7 +59,7 @@
|
||||||
</div>
|
</div>
|
||||||
<MkPoll v-if="appearNote.poll" ref="pollViewer" :note="appearNote" class="poll"/>
|
<MkPoll v-if="appearNote.poll" ref="pollViewer" :note="appearNote" class="poll"/>
|
||||||
<MkUrlPreview v-for="url in urls" :key="url" :url="url" :compact="true" :detail="false" class="url-preview"/>
|
<MkUrlPreview v-for="url in urls" :key="url" :url="url" :compact="true" :detail="false" class="url-preview"/>
|
||||||
<div v-if="appearNote.renote" class="renote"><MkNoteSimple :note="appearNote.renote"/></div>
|
<div v-if="appearNote.renote" class="renote"><MkNoteSimple :note="appearNote.renote" class="note"/></div>
|
||||||
<button v-if="isLong && collapsed" class="fade _button" @click="collapsed = false">
|
<button v-if="isLong && collapsed" class="fade _button" @click="collapsed = false">
|
||||||
<span>{{ i18n.ts.showMore }}</span>
|
<span>{{ i18n.ts.showMore }}</span>
|
||||||
</button>
|
</button>
|
||||||
|
@ -101,7 +101,7 @@
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { computed, inject, onMounted, onUnmounted, reactive, ref, Ref } from 'vue';
|
import { computed, inject, onMounted, onUnmounted, reactive, ref, shallowRef, Ref } from 'vue';
|
||||||
import * as mfm from 'mfm-js';
|
import * as mfm from 'mfm-js';
|
||||||
import * as misskey from 'misskey-js';
|
import * as misskey from 'misskey-js';
|
||||||
import MkNoteSub from '@/components/MkNoteSub.vue';
|
import MkNoteSub from '@/components/MkNoteSub.vue';
|
||||||
|
@ -156,11 +156,11 @@ const isRenote = (
|
||||||
note.poll == null
|
note.poll == null
|
||||||
);
|
);
|
||||||
|
|
||||||
const el = ref<HTMLElement>();
|
const el = shallowRef<HTMLElement>();
|
||||||
const menuButton = ref<HTMLElement>();
|
const menuButton = shallowRef<HTMLElement>();
|
||||||
const renoteButton = ref<InstanceType<typeof MkRenoteButton>>();
|
const renoteButton = shallowRef<InstanceType<typeof MkRenoteButton>>();
|
||||||
const renoteTime = ref<HTMLElement>();
|
const renoteTime = shallowRef<HTMLElement>();
|
||||||
const reactButton = ref<HTMLElement>();
|
const reactButton = shallowRef<HTMLElement>();
|
||||||
let appearNote = $computed(() => isRenote ? note.renote as misskey.entities.Note : note);
|
let appearNote = $computed(() => isRenote ? note.renote as misskey.entities.Note : note);
|
||||||
const isMyRenote = $i && ($i.id === note.userId);
|
const isMyRenote = $i && ($i.id === note.userId);
|
||||||
const showContent = ref(false);
|
const showContent = ref(false);
|
||||||
|
@ -468,7 +468,7 @@ function readPromo() {
|
||||||
&.collapsed {
|
&.collapsed {
|
||||||
position: relative;
|
position: relative;
|
||||||
max-height: 9em;
|
max-height: 9em;
|
||||||
overflow: hidden;
|
overflow: clip;
|
||||||
|
|
||||||
> .fade {
|
> .fade {
|
||||||
display: block;
|
display: block;
|
||||||
|
@ -529,7 +529,7 @@ function readPromo() {
|
||||||
> .renote {
|
> .renote {
|
||||||
padding: 8px 0;
|
padding: 8px 0;
|
||||||
|
|
||||||
> * {
|
> .note {
|
||||||
padding: 16px;
|
padding: 16px;
|
||||||
border: dashed 1px var(--renote);
|
border: dashed 1px var(--renote);
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
|
|
|
@ -70,7 +70,7 @@
|
||||||
</div>
|
</div>
|
||||||
<MkPoll v-if="appearNote.poll" ref="pollViewer" :note="appearNote" class="poll"/>
|
<MkPoll v-if="appearNote.poll" ref="pollViewer" :note="appearNote" class="poll"/>
|
||||||
<MkUrlPreview v-for="url in urls" :key="url" :url="url" :compact="true" :detail="true" class="url-preview"/>
|
<MkUrlPreview v-for="url in urls" :key="url" :url="url" :compact="true" :detail="true" class="url-preview"/>
|
||||||
<div v-if="appearNote.renote" class="renote"><MkNoteSimple :note="appearNote.renote"/></div>
|
<div v-if="appearNote.renote" class="renote"><MkNoteSimple :note="appearNote.renote" class="note"/></div>
|
||||||
</div>
|
</div>
|
||||||
<MkA v-if="appearNote.channel && !inChannel" class="channel" :to="`/channels/${appearNote.channel.id}`"><i class="ti ti-device-tv"></i> {{ appearNote.channel.name }}</MkA>
|
<MkA v-if="appearNote.channel && !inChannel" class="channel" :to="`/channels/${appearNote.channel.id}`"><i class="ti ti-device-tv"></i> {{ appearNote.channel.name }}</MkA>
|
||||||
</div>
|
</div>
|
||||||
|
@ -112,7 +112,7 @@
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { computed, inject, onMounted, onUnmounted, reactive, ref } from 'vue';
|
import { computed, inject, onMounted, onUnmounted, reactive, ref, shallowRef } from 'vue';
|
||||||
import * as mfm from 'mfm-js';
|
import * as mfm from 'mfm-js';
|
||||||
import * as misskey from 'misskey-js';
|
import * as misskey from 'misskey-js';
|
||||||
import MkNoteSub from '@/components/MkNoteSub.vue';
|
import MkNoteSub from '@/components/MkNoteSub.vue';
|
||||||
|
@ -166,11 +166,11 @@ const isRenote = (
|
||||||
note.poll == null
|
note.poll == null
|
||||||
);
|
);
|
||||||
|
|
||||||
const el = ref<HTMLElement>();
|
const el = shallowRef<HTMLElement>();
|
||||||
const menuButton = ref<HTMLElement>();
|
const menuButton = shallowRef<HTMLElement>();
|
||||||
const renoteButton = ref<InstanceType<typeof MkRenoteButton>>();
|
const renoteButton = shallowRef<InstanceType<typeof MkRenoteButton>>();
|
||||||
const renoteTime = ref<HTMLElement>();
|
const renoteTime = shallowRef<HTMLElement>();
|
||||||
const reactButton = ref<HTMLElement>();
|
const reactButton = shallowRef<HTMLElement>();
|
||||||
let appearNote = $computed(() => isRenote ? note.renote as misskey.entities.Note : note);
|
let appearNote = $computed(() => isRenote ? note.renote as misskey.entities.Note : note);
|
||||||
const isMyRenote = $i && ($i.id === note.userId);
|
const isMyRenote = $i && ($i.id === note.userId);
|
||||||
const showContent = ref(false);
|
const showContent = ref(false);
|
||||||
|
@ -298,7 +298,7 @@ if (appearNote.replyId) {
|
||||||
.lxwezrsl {
|
.lxwezrsl {
|
||||||
position: relative;
|
position: relative;
|
||||||
transition: box-shadow 0.1s ease;
|
transition: box-shadow 0.1s ease;
|
||||||
overflow: hidden;
|
overflow: clip;
|
||||||
contain: content;
|
contain: content;
|
||||||
|
|
||||||
&:focus-visible {
|
&:focus-visible {
|
||||||
|
@ -491,7 +491,7 @@ if (appearNote.replyId) {
|
||||||
> .renote {
|
> .renote {
|
||||||
padding: 8px 0;
|
padding: 8px 0;
|
||||||
|
|
||||||
> * {
|
> .note {
|
||||||
padding: 16px;
|
padding: 16px;
|
||||||
border: dashed 1px var(--renote);
|
border: dashed 1px var(--renote);
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
|
|
|
@ -18,7 +18,7 @@
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { ref } from 'vue';
|
import { shallowRef } from 'vue';
|
||||||
import XNote from '@/components/MkNote.vue';
|
import XNote from '@/components/MkNote.vue';
|
||||||
import XList from '@/components/MkDateSeparatedList.vue';
|
import XList from '@/components/MkDateSeparatedList.vue';
|
||||||
import MkPagination, { Paging } from '@/components/MkPagination.vue';
|
import MkPagination, { Paging } from '@/components/MkPagination.vue';
|
||||||
|
@ -29,7 +29,7 @@ const props = defineProps<{
|
||||||
noGap?: boolean;
|
noGap?: boolean;
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
const pagingComponent = ref<InstanceType<typeof MkPagination>>();
|
const pagingComponent = shallowRef<InstanceType<typeof MkPagination>>();
|
||||||
|
|
||||||
defineExpose({
|
defineExpose({
|
||||||
pagingComponent,
|
pagingComponent,
|
||||||
|
|
|
@ -73,7 +73,7 @@
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { ref, onMounted, onUnmounted, watch } from 'vue';
|
import { ref, shallowRef, onMounted, onUnmounted, watch } from 'vue';
|
||||||
import * as misskey from 'misskey-js';
|
import * as misskey from 'misskey-js';
|
||||||
import XReactionIcon from '@/components/MkReactionIcon.vue';
|
import XReactionIcon from '@/components/MkReactionIcon.vue';
|
||||||
import MkFollowButton from '@/components/MkFollowButton.vue';
|
import MkFollowButton from '@/components/MkFollowButton.vue';
|
||||||
|
@ -95,7 +95,7 @@ const props = withDefaults(defineProps<{
|
||||||
full: false,
|
full: false,
|
||||||
});
|
});
|
||||||
|
|
||||||
const elRef = ref<HTMLElement>(null);
|
const elRef = shallowRef<HTMLElement>(null);
|
||||||
const reactionRef = ref(null);
|
const reactionRef = ref(null);
|
||||||
|
|
||||||
let readObserver: IntersectionObserver | undefined;
|
let readObserver: IntersectionObserver | undefined;
|
||||||
|
|
|
@ -56,7 +56,7 @@ const props = withDefaults(defineProps<{
|
||||||
|
|
||||||
let includingTypes = $computed(() => props.includingTypes || []);
|
let includingTypes = $computed(() => props.includingTypes || []);
|
||||||
|
|
||||||
const dialog = $ref<InstanceType<typeof XModalWindow>>();
|
const dialog = $shallowRef<InstanceType<typeof XModalWindow>>();
|
||||||
|
|
||||||
let typesMap = $ref<Record<typeof notificationTypes[number], boolean>>({});
|
let typesMap = $ref<Record<typeof notificationTypes[number], boolean>>({});
|
||||||
let useGlobalSetting = $ref((includingTypes === null || includingTypes.length === 0) && props.showGlobalToggle);
|
let useGlobalSetting = $ref((includingTypes === null || includingTypes.length === 0) && props.showGlobalToggle);
|
||||||
|
|
|
@ -17,7 +17,7 @@
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { defineComponent, markRaw, onUnmounted, onMounted, computed, ref } from 'vue';
|
import { defineComponent, markRaw, onUnmounted, onMounted, computed, shallowRef } from 'vue';
|
||||||
import { notificationTypes } from 'misskey-js';
|
import { notificationTypes } from 'misskey-js';
|
||||||
import MkPagination, { Paging } from '@/components/MkPagination.vue';
|
import MkPagination, { Paging } from '@/components/MkPagination.vue';
|
||||||
import XNotification from '@/components/MkNotification.vue';
|
import XNotification from '@/components/MkNotification.vue';
|
||||||
|
@ -33,7 +33,7 @@ const props = defineProps<{
|
||||||
unreadOnly?: boolean;
|
unreadOnly?: boolean;
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
const pagingComponent = ref<InstanceType<typeof MkPagination>>();
|
const pagingComponent = shallowRef<InstanceType<typeof MkPagination>>();
|
||||||
|
|
||||||
const pagination: Paging = {
|
const pagination: Paging = {
|
||||||
endpoint: 'i/notifications' as const,
|
endpoint: 'i/notifications' as const,
|
||||||
|
|
|
@ -47,7 +47,7 @@ defineEmits<{
|
||||||
const router = new Router(routes, props.initialPath);
|
const router = new Router(routes, props.initialPath);
|
||||||
|
|
||||||
let pageMetadata = $ref<null | ComputedRef<PageMetadata>>();
|
let pageMetadata = $ref<null | ComputedRef<PageMetadata>>();
|
||||||
let windowEl = $ref<InstanceType<typeof XWindow>>();
|
let windowEl = $shallowRef<InstanceType<typeof XWindow>>();
|
||||||
const history = $ref<{ path: string; key: any; }[]>([{
|
const history = $ref<{ path: string; key: any; }[]>([{
|
||||||
path: router.getCurrentPath(),
|
path: router.getCurrentPath(),
|
||||||
key: router.getCurrentKey(),
|
key: router.getCurrentKey(),
|
||||||
|
|
|
@ -32,7 +32,7 @@
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { computed, ComputedRef, isRef, markRaw, onActivated, onDeactivated, Ref, ref, watch } from 'vue';
|
import { computed, ComputedRef, isRef, markRaw, onActivated, onDeactivated, Ref, ref, shallowRef, watch } from 'vue';
|
||||||
import * as misskey from 'misskey-js';
|
import * as misskey from 'misskey-js';
|
||||||
import * as os from '@/os';
|
import * as os from '@/os';
|
||||||
import { onScrollTop, isTopVisible, getScrollPosition, getScrollContainer } from '@/scripts/scroll';
|
import { onScrollTop, isTopVisible, getScrollPosition, getScrollContainer } from '@/scripts/scroll';
|
||||||
|
@ -65,7 +65,7 @@ const props = withDefaults(defineProps<{
|
||||||
disableAutoLoad?: boolean;
|
disableAutoLoad?: boolean;
|
||||||
displayLimit?: number;
|
displayLimit?: number;
|
||||||
}>(), {
|
}>(), {
|
||||||
displayLimit: 30,
|
displayLimit: 20,
|
||||||
});
|
});
|
||||||
|
|
||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
|
@ -74,7 +74,7 @@ const emit = defineEmits<{
|
||||||
|
|
||||||
type Item = { id: string; [another: string]: unknown; };
|
type Item = { id: string; [another: string]: unknown; };
|
||||||
|
|
||||||
const rootEl = ref<HTMLElement>();
|
const rootEl = shallowRef<HTMLElement>();
|
||||||
const items = ref<Item[]>([]);
|
const items = ref<Item[]>([]);
|
||||||
const queue = ref<Item[]>([]);
|
const queue = ref<Item[]>([]);
|
||||||
const offset = ref(0);
|
const offset = ref(0);
|
||||||
|
|
|
@ -19,7 +19,7 @@ const emit = defineEmits<{
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
let up = $ref(false);
|
let up = $ref(false);
|
||||||
const zIndex = os.claimZIndex('high');
|
const zIndex = os.claimZIndex('veryLow');
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
window.setTimeout(() => {
|
window.setTimeout(() => {
|
||||||
|
|
|
@ -102,7 +102,7 @@ const vote = async (id) => {
|
||||||
//border: solid 0.5px var(--divider);
|
//border: solid 0.5px var(--divider);
|
||||||
background: var(--accentedBg);
|
background: var(--accentedBg);
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
overflow: hidden;
|
overflow: clip;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
|
|
||||||
> .backdrop {
|
> .backdrop {
|
||||||
|
|
|
@ -22,7 +22,7 @@ const emit = defineEmits<{
|
||||||
(ev: 'closed'): void;
|
(ev: 'closed'): void;
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
let modal = $ref<InstanceType<typeof MkModal>>();
|
let modal = $shallowRef<InstanceType<typeof MkModal>>();
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
|
|
|
@ -128,10 +128,10 @@ const emit = defineEmits<{
|
||||||
(ev: 'esc'): void;
|
(ev: 'esc'): void;
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
const textareaEl = $ref<HTMLTextAreaElement | null>(null);
|
const textareaEl = $shallowRef<HTMLTextAreaElement | null>(null);
|
||||||
const cwInputEl = $ref<HTMLInputElement | null>(null);
|
const cwInputEl = $shallowRef<HTMLInputElement | null>(null);
|
||||||
const hashtagsInputEl = $ref<HTMLInputElement | null>(null);
|
const hashtagsInputEl = $shallowRef<HTMLInputElement | null>(null);
|
||||||
const visibilityButton = $ref<HTMLElement | null>(null);
|
const visibilityButton = $shallowRef<HTMLElement | null>(null);
|
||||||
|
|
||||||
let posting = $ref(false);
|
let posting = $ref(false);
|
||||||
let posted = $ref(false);
|
let posted = $ref(false);
|
||||||
|
|
|
@ -31,8 +31,8 @@ const emit = defineEmits<{
|
||||||
(ev: 'closed'): void;
|
(ev: 'closed'): void;
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
let modal = $ref<InstanceType<typeof MkModal>>();
|
let modal = $shallowRef<InstanceType<typeof MkModal>>();
|
||||||
let form = $ref<InstanceType<typeof MkPostForm>>();
|
let form = $shallowRef<InstanceType<typeof MkPostForm>>();
|
||||||
|
|
||||||
function onPosted() {
|
function onPosted() {
|
||||||
modal.close({
|
modal.close({
|
||||||
|
|
|
@ -12,7 +12,7 @@
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { computed, onMounted, ref, watch } from 'vue';
|
import { computed, onMounted, ref, shallowRef, watch } from 'vue';
|
||||||
import * as misskey from 'misskey-js';
|
import * as misskey from 'misskey-js';
|
||||||
import XDetails from '@/components/MkReactionsViewer.details.vue';
|
import XDetails from '@/components/MkReactionsViewer.details.vue';
|
||||||
import XReactionIcon from '@/components/MkReactionIcon.vue';
|
import XReactionIcon from '@/components/MkReactionIcon.vue';
|
||||||
|
@ -28,7 +28,7 @@ const props = defineProps<{
|
||||||
note: misskey.entities.Note;
|
note: misskey.entities.Note;
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
const buttonRef = ref<HTMLElement>();
|
const buttonRef = shallowRef<HTMLElement>();
|
||||||
|
|
||||||
const canToggle = computed(() => !props.reaction.match(/@\w/) && $i);
|
const canToggle = computed(() => !props.reaction.match(/@\w/) && $i);
|
||||||
|
|
||||||
|
|
|
@ -14,7 +14,7 @@
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { computed, ref } from 'vue';
|
import { computed, ref, shallowRef } from 'vue';
|
||||||
import * as misskey from 'misskey-js';
|
import * as misskey from 'misskey-js';
|
||||||
import XDetails from '@/components/MkUsersTooltip.vue';
|
import XDetails from '@/components/MkUsersTooltip.vue';
|
||||||
import { pleaseLogin } from '@/scripts/please-login';
|
import { pleaseLogin } from '@/scripts/please-login';
|
||||||
|
@ -28,7 +28,7 @@ const props = defineProps<{
|
||||||
count: number;
|
count: number;
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
const buttonRef = ref<HTMLElement>();
|
const buttonRef = shallowRef<HTMLElement>();
|
||||||
|
|
||||||
const canRenote = computed(() => ['public', 'home'].includes(props.note.visibility) || props.note.userId === $i.id);
|
const canRenote = computed(() => ['public', 'home'].includes(props.note.visibility) || props.note.userId === $i.id);
|
||||||
|
|
||||||
|
|
|
@ -9,53 +9,21 @@
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { markRaw, version as vueVersion, onMounted, onBeforeUnmount, nextTick } from 'vue';
|
import { markRaw, version as vueVersion, onMounted, onBeforeUnmount, nextTick } from 'vue';
|
||||||
import {
|
import { Chart } from 'chart.js';
|
||||||
Chart,
|
|
||||||
ArcElement,
|
|
||||||
LineElement,
|
|
||||||
BarElement,
|
|
||||||
PointElement,
|
|
||||||
BarController,
|
|
||||||
LineController,
|
|
||||||
CategoryScale,
|
|
||||||
LinearScale,
|
|
||||||
TimeScale,
|
|
||||||
Legend,
|
|
||||||
Title,
|
|
||||||
Tooltip,
|
|
||||||
SubTitle,
|
|
||||||
Filler,
|
|
||||||
} from 'chart.js';
|
|
||||||
import { enUS } from 'date-fns/locale';
|
import { enUS } from 'date-fns/locale';
|
||||||
import tinycolor from 'tinycolor2';
|
import tinycolor from 'tinycolor2';
|
||||||
|
import { MatrixController, MatrixElement } from 'chartjs-chart-matrix';
|
||||||
import * as os from '@/os';
|
import * as os from '@/os';
|
||||||
import 'chartjs-adapter-date-fns';
|
|
||||||
import { defaultStore } from '@/store';
|
import { defaultStore } from '@/store';
|
||||||
import { useChartTooltip } from '@/scripts/use-chart-tooltip';
|
import { useChartTooltip } from '@/scripts/use-chart-tooltip';
|
||||||
import { MatrixController, MatrixElement } from 'chartjs-chart-matrix';
|
|
||||||
import { chartVLine } from '@/scripts/chart-vline';
|
import { chartVLine } from '@/scripts/chart-vline';
|
||||||
import { alpha } from '@/scripts/color';
|
import { alpha } from '@/scripts/color';
|
||||||
|
import { initChart } from '@/scripts/init-chart';
|
||||||
|
|
||||||
Chart.register(
|
initChart();
|
||||||
ArcElement,
|
|
||||||
LineElement,
|
|
||||||
BarElement,
|
|
||||||
PointElement,
|
|
||||||
BarController,
|
|
||||||
LineController,
|
|
||||||
CategoryScale,
|
|
||||||
LinearScale,
|
|
||||||
TimeScale,
|
|
||||||
Legend,
|
|
||||||
Title,
|
|
||||||
Tooltip,
|
|
||||||
SubTitle,
|
|
||||||
Filler,
|
|
||||||
MatrixController, MatrixElement,
|
|
||||||
);
|
|
||||||
|
|
||||||
const rootEl = $ref<HTMLDivElement>(null);
|
const rootEl = $shallowRef<HTMLDivElement>(null);
|
||||||
const chartEl = $ref<HTMLCanvasElement>(null);
|
const chartEl = $shallowRef<HTMLCanvasElement>(null);
|
||||||
const now = new Date();
|
const now = new Date();
|
||||||
let chartInstance: Chart = null;
|
let chartInstance: Chart = null;
|
||||||
let fetching = $ref(true);
|
let fetching = $ref(true);
|
||||||
|
@ -95,11 +63,6 @@ async function renderChart() {
|
||||||
|
|
||||||
await nextTick();
|
await nextTick();
|
||||||
|
|
||||||
const gridColor = defaultStore.state.darkMode ? 'rgba(255, 255, 255, 0.1)' : 'rgba(0, 0, 0, 0.1)';
|
|
||||||
|
|
||||||
// フォントカラー
|
|
||||||
Chart.defaults.color = getComputedStyle(document.documentElement).getPropertyValue('--fg');
|
|
||||||
|
|
||||||
const color = defaultStore.state.darkMode ? '#b4e900' : '#86b300';
|
const color = defaultStore.state.darkMode ? '#b4e900' : '#86b300';
|
||||||
|
|
||||||
// 視覚上の分かりやすさのため上から最も大きい3つの値の平均を最大値とする
|
// 視覚上の分かりやすさのため上から最も大きい3つの値の平均を最大値とする
|
||||||
|
@ -150,8 +113,6 @@ async function renderChart() {
|
||||||
suggestedMax: maxDays,
|
suggestedMax: maxDays,
|
||||||
grid: {
|
grid: {
|
||||||
display: false,
|
display: false,
|
||||||
color: gridColor,
|
|
||||||
borderColor: 'rgb(0, 0, 0, 0)',
|
|
||||||
},
|
},
|
||||||
ticks: {
|
ticks: {
|
||||||
display: true,
|
display: true,
|
||||||
|
@ -174,8 +135,6 @@ async function renderChart() {
|
||||||
},
|
},
|
||||||
grid: {
|
grid: {
|
||||||
display: false,
|
display: false,
|
||||||
color: gridColor,
|
|
||||||
borderColor: 'rgb(0, 0, 0, 0)',
|
|
||||||
},
|
},
|
||||||
ticks: {
|
ticks: {
|
||||||
maxRotation: 0,
|
maxRotation: 0,
|
||||||
|
@ -187,7 +146,6 @@ async function renderChart() {
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
animation: false,
|
|
||||||
plugins: {
|
plugins: {
|
||||||
legend: {
|
legend: {
|
||||||
display: false,
|
display: false,
|
||||||
|
|
|
@ -32,7 +32,7 @@ const emit = defineEmits<{
|
||||||
(ev: 'cancelled'): void;
|
(ev: 'cancelled'): void;
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
const dialog = $ref<InstanceType<typeof XModalWindow>>();
|
const dialog = $shallowRef<InstanceType<typeof XModalWindow>>();
|
||||||
|
|
||||||
function onClose() {
|
function onClose() {
|
||||||
emit('cancelled');
|
emit('cancelled');
|
||||||
|
|
|
@ -33,7 +33,7 @@ const emit = defineEmits<{
|
||||||
(ev: 'closed'): void;
|
(ev: 'closed'): void;
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
const dialog = $ref<InstanceType<typeof XModalWindow>>();
|
const dialog = $shallowRef<InstanceType<typeof XModalWindow>>();
|
||||||
|
|
||||||
function onSignup(res) {
|
function onSignup(res) {
|
||||||
emit('done', res);
|
emit('done', res);
|
||||||
|
|
|
@ -64,10 +64,10 @@
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { onMounted, onUnmounted, ref } from 'vue';
|
import { onMounted, onUnmounted, ref, shallowRef } from 'vue';
|
||||||
|
|
||||||
const particles = ref([]);
|
const particles = ref([]);
|
||||||
const el = ref<HTMLElement>();
|
const el = shallowRef<HTMLElement>();
|
||||||
const width = ref(0);
|
const width = ref(0);
|
||||||
const height = ref(0);
|
const height = ref(0);
|
||||||
const colors = ['#FF1493', '#00FFFF', '#FFE202', '#FFE202', '#FFE202'];
|
const colors = ['#FF1493', '#00FFFF', '#FFE202', '#FFE202', '#FFE202'];
|
||||||
|
|
|
@ -59,7 +59,7 @@ const collapsed = $ref(
|
||||||
&.collapsed {
|
&.collapsed {
|
||||||
position: relative;
|
position: relative;
|
||||||
max-height: 9em;
|
max-height: 9em;
|
||||||
overflow: hidden;
|
overflow: clip;
|
||||||
|
|
||||||
> .fade {
|
> .fade {
|
||||||
display: block;
|
display: block;
|
||||||
|
|
|
@ -19,9 +19,9 @@ const computedStyle = getComputedStyle(document.documentElement);
|
||||||
const idForCanvas = Array.from(Array(16)).map(() => SAFE_FOR_HTML_ID[Math.floor(Math.random() * SAFE_FOR_HTML_ID.length)]).join('');
|
const idForCanvas = Array.from(Array(16)).map(() => SAFE_FOR_HTML_ID[Math.floor(Math.random() * SAFE_FOR_HTML_ID.length)]).join('');
|
||||||
const idForTags = Array.from(Array(16)).map(() => SAFE_FOR_HTML_ID[Math.floor(Math.random() * SAFE_FOR_HTML_ID.length)]).join('');
|
const idForTags = Array.from(Array(16)).map(() => SAFE_FOR_HTML_ID[Math.floor(Math.random() * SAFE_FOR_HTML_ID.length)]).join('');
|
||||||
let available = $ref(false);
|
let available = $ref(false);
|
||||||
let rootEl = $ref<HTMLElement | null>(null);
|
let rootEl = $shallowRef<HTMLElement | null>(null);
|
||||||
let canvasEl = $ref<HTMLCanvasElement | null>(null);
|
let canvasEl = $shallowRef<HTMLCanvasElement | null>(null);
|
||||||
let tagsEl = $ref<HTMLElement | null>(null);
|
let tagsEl = $shallowRef<HTMLElement | null>(null);
|
||||||
let width = $ref(300);
|
let width = $ref(300);
|
||||||
|
|
||||||
watch($$(available), () => {
|
watch($$(available), () => {
|
||||||
|
|
|
@ -54,7 +54,7 @@ const emit = defineEmits<{
|
||||||
(ev: 'done', result: { name: string | null, permissions: string[] }): void;
|
(ev: 'done', result: { name: string | null, permissions: string[] }): void;
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
const dialog = $ref<InstanceType<typeof XModalWindow>>();
|
const dialog = $shallowRef<InstanceType<typeof XModalWindow>>();
|
||||||
let name = $ref(props.initialName);
|
let name = $ref(props.initialName);
|
||||||
let permissions = $ref({});
|
let permissions = $ref({});
|
||||||
|
|
||||||
|
|
|
@ -10,7 +10,7 @@
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { nextTick, onMounted, onUnmounted, ref } from 'vue';
|
import { nextTick, onMounted, onUnmounted, ref, shallowRef } from 'vue';
|
||||||
import * as os from '@/os';
|
import * as os from '@/os';
|
||||||
import { calcPopupPosition } from '@/scripts/popup-position';
|
import { calcPopupPosition } from '@/scripts/popup-position';
|
||||||
|
|
||||||
|
@ -34,7 +34,7 @@ const emit = defineEmits<{
|
||||||
(ev: 'closed'): void;
|
(ev: 'closed'): void;
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
const el = ref<HTMLElement>();
|
const el = shallowRef<HTMLElement>();
|
||||||
const zIndex = os.claimZIndex('high');
|
const zIndex = os.claimZIndex('high');
|
||||||
|
|
||||||
function setPosition() {
|
function setPosition() {
|
||||||
|
|
|
@ -10,14 +10,14 @@
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { ref } from 'vue';
|
import { shallowRef } from 'vue';
|
||||||
import MkModal from '@/components/MkModal.vue';
|
import MkModal from '@/components/MkModal.vue';
|
||||||
import MkButton from '@/components/MkButton.vue';
|
import MkButton from '@/components/MkButton.vue';
|
||||||
import MkSparkle from '@/components/MkSparkle.vue';
|
import MkSparkle from '@/components/MkSparkle.vue';
|
||||||
import { version } from '@/config';
|
import { version } from '@/config';
|
||||||
import { i18n } from '@/i18n';
|
import { i18n } from '@/i18n';
|
||||||
|
|
||||||
const modal = ref<InstanceType<typeof MkModal>>();
|
const modal = shallowRef<InstanceType<typeof MkModal>>();
|
||||||
|
|
||||||
const whatIsNew = () => {
|
const whatIsNew = () => {
|
||||||
modal.value.close();
|
modal.value.close();
|
||||||
|
|
|
@ -175,7 +175,7 @@ onUnmounted(() => {
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
box-shadow: 0 0 0 1px var(--divider);
|
box-shadow: 0 0 0 1px var(--divider);
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
overflow: hidden;
|
overflow: clip;
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
<template>
|
<template>
|
||||||
<MkPagination ref="pagingComponent" :pagination="pagination">
|
<MkPagination :pagination="pagination">
|
||||||
<template #empty>
|
<template #empty>
|
||||||
<div class="_fullinfo">
|
<div class="_fullinfo">
|
||||||
<img src="https://xn--931a.moe/assets/info.jpg" class="_ghost"/>
|
<img src="https://xn--931a.moe/assets/info.jpg" class="_ghost"/>
|
||||||
|
@ -16,7 +16,7 @@
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { ref } from 'vue';
|
import { shallowRef } from 'vue';
|
||||||
import MkUserInfo from '@/components/MkUserInfo.vue';
|
import MkUserInfo from '@/components/MkUserInfo.vue';
|
||||||
import MkPagination, { Paging } from '@/components/MkPagination.vue';
|
import MkPagination, { Paging } from '@/components/MkPagination.vue';
|
||||||
import { userPage } from '@/filters/user';
|
import { userPage } from '@/filters/user';
|
||||||
|
@ -26,8 +26,6 @@ const props = defineProps<{
|
||||||
pagination: Paging;
|
pagination: Paging;
|
||||||
noGap?: boolean;
|
noGap?: boolean;
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
const pagingComponent = ref<InstanceType<typeof MkPagination>>();
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
|
|
|
@ -22,7 +22,7 @@ const props = defineProps<{
|
||||||
},
|
},
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
const specified = $ref<HTMLElement>();
|
const specified = $shallowRef<HTMLElement>();
|
||||||
|
|
||||||
if (props.note.visibility === 'specified') {
|
if (props.note.visibility === 'specified') {
|
||||||
useTooltip($$(specified), async (showing) => {
|
useTooltip($$(specified), async (showing) => {
|
||||||
|
|
|
@ -1,42 +1,42 @@
|
||||||
<template>
|
<template>
|
||||||
<MkModal ref="modal" :z-priority="'high'" :src="src" @click="modal.close()" @closed="emit('closed')">
|
<MkModal ref="modal" :z-priority="'high'" :src="src" @click="modal.close()" @closed="emit('closed')">
|
||||||
<div class="gqyayizv _popup">
|
<div class="gqyayizv _popup">
|
||||||
<button key="public" class="_button" :class="{ active: v === 'public' }" data-index="1" @click="choose('public')">
|
<button key="public" class="_button item" :class="{ active: v === 'public' }" data-index="1" @click="choose('public')">
|
||||||
<div><i class="ti ti-world"></i></div>
|
<div class="icon"><i class="ti ti-world"></i></div>
|
||||||
<div>
|
<div class="body">
|
||||||
<span>{{ i18n.ts._visibility.public }}</span>
|
<span>{{ i18n.ts._visibility.public }}</span>
|
||||||
<span>{{ i18n.ts._visibility.publicDescription }}</span>
|
<span>{{ i18n.ts._visibility.publicDescription }}</span>
|
||||||
</div>
|
</div>
|
||||||
</button>
|
</button>
|
||||||
<button key="home" class="_button" :class="{ active: v === 'home' }" data-index="2" @click="choose('home')">
|
<button key="home" class="_button item" :class="{ active: v === 'home' }" data-index="2" @click="choose('home')">
|
||||||
<div><i class="ti ti-home"></i></div>
|
<div class="icon"><i class="ti ti-home"></i></div>
|
||||||
<div>
|
<div class="body">
|
||||||
<span>{{ i18n.ts._visibility.home }}</span>
|
<span>{{ i18n.ts._visibility.home }}</span>
|
||||||
<span>{{ i18n.ts._visibility.homeDescription }}</span>
|
<span>{{ i18n.ts._visibility.homeDescription }}</span>
|
||||||
</div>
|
</div>
|
||||||
</button>
|
</button>
|
||||||
<button key="followers" class="_button" :class="{ active: v === 'followers' }" data-index="3" @click="choose('followers')">
|
<button key="followers" class="_button item" :class="{ active: v === 'followers' }" data-index="3" @click="choose('followers')">
|
||||||
<div><i class="ti ti-lock-open"></i></div>
|
<div class="icon"><i class="ti ti-lock-open"></i></div>
|
||||||
<div>
|
<div class="body">
|
||||||
<span>{{ i18n.ts._visibility.followers }}</span>
|
<span>{{ i18n.ts._visibility.followers }}</span>
|
||||||
<span>{{ i18n.ts._visibility.followersDescription }}</span>
|
<span>{{ i18n.ts._visibility.followersDescription }}</span>
|
||||||
</div>
|
</div>
|
||||||
</button>
|
</button>
|
||||||
<button key="specified" :disabled="localOnly" class="_button" :class="{ active: v === 'specified' }" data-index="4" @click="choose('specified')">
|
<button key="specified" :disabled="localOnly" class="_button item" :class="{ active: v === 'specified' }" data-index="4" @click="choose('specified')">
|
||||||
<div><i class="ti ti-mail"></i></div>
|
<div class="icon"><i class="ti ti-mail"></i></div>
|
||||||
<div>
|
<div class="body">
|
||||||
<span>{{ i18n.ts._visibility.specified }}</span>
|
<span>{{ i18n.ts._visibility.specified }}</span>
|
||||||
<span>{{ i18n.ts._visibility.specifiedDescription }}</span>
|
<span>{{ i18n.ts._visibility.specifiedDescription }}</span>
|
||||||
</div>
|
</div>
|
||||||
</button>
|
</button>
|
||||||
<div class="divider"></div>
|
<div class="divider"></div>
|
||||||
<button key="localOnly" class="_button localOnly" :class="{ active: localOnly }" data-index="5" @click="localOnly = !localOnly">
|
<button key="localOnly" class="_button item localOnly" :class="{ active: localOnly }" data-index="5" @click="localOnly = !localOnly">
|
||||||
<div><i class="ti ti-world-off"></i></div>
|
<div class="icon"><i class="ti ti-world-off"></i></div>
|
||||||
<div>
|
<div class="body">
|
||||||
<span>{{ i18n.ts._visibility.localOnly }}</span>
|
<span>{{ i18n.ts._visibility.localOnly }}</span>
|
||||||
<span>{{ i18n.ts._visibility.localOnlyDescription }}</span>
|
<span>{{ i18n.ts._visibility.localOnlyDescription }}</span>
|
||||||
</div>
|
</div>
|
||||||
<div><i :class="localOnly ? 'ti ti-toggle-right' : 'ti ti-toggle-left'"></i></div>
|
<div class="toggle"><i :class="localOnly ? 'ti ti-toggle-right' : 'ti ti-toggle-left'"></i></div>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</MkModal>
|
</MkModal>
|
||||||
|
@ -48,7 +48,7 @@ import * as misskey from 'misskey-js';
|
||||||
import MkModal from '@/components/MkModal.vue';
|
import MkModal from '@/components/MkModal.vue';
|
||||||
import { i18n } from '@/i18n';
|
import { i18n } from '@/i18n';
|
||||||
|
|
||||||
const modal = $ref<InstanceType<typeof MkModal>>();
|
const modal = $shallowRef<InstanceType<typeof MkModal>>();
|
||||||
|
|
||||||
const props = withDefaults(defineProps<{
|
const props = withDefaults(defineProps<{
|
||||||
currentVisibility: typeof misskey.noteVisibilities[number];
|
currentVisibility: typeof misskey.noteVisibilities[number];
|
||||||
|
@ -89,7 +89,7 @@ function choose(visibility: typeof misskey.noteVisibilities[number]): void {
|
||||||
border-top: solid 0.5px var(--divider);
|
border-top: solid 0.5px var(--divider);
|
||||||
}
|
}
|
||||||
|
|
||||||
> button {
|
> .item {
|
||||||
display: flex;
|
display: flex;
|
||||||
padding: 8px 14px;
|
padding: 8px 14px;
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
|
@ -115,7 +115,7 @@ function choose(visibility: typeof misskey.noteVisibilities[number]): void {
|
||||||
background: inherit;
|
background: inherit;
|
||||||
}
|
}
|
||||||
|
|
||||||
> *:nth-child(1) {
|
> .icon {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
@ -127,7 +127,7 @@ function choose(visibility: typeof misskey.noteVisibilities[number]): void {
|
||||||
margin-bottom: auto;
|
margin-bottom: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
> *:nth-child(2) {
|
> .body {
|
||||||
flex: 1 1 auto;
|
flex: 1 1 auto;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
|
@ -143,7 +143,7 @@ function choose(visibility: typeof misskey.noteVisibilities[number]): void {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
> *:nth-child(3) {
|
> .toggle {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
|
|
@ -9,10 +9,10 @@
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { watch, ref } from 'vue';
|
import { watch, shallowRef } from 'vue';
|
||||||
import MkModal from '@/components/MkModal.vue';
|
import MkModal from '@/components/MkModal.vue';
|
||||||
|
|
||||||
const modal = ref<InstanceType<typeof MkModal>>();
|
const modal = shallowRef<InstanceType<typeof MkModal>>();
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
success: boolean;
|
success: boolean;
|
||||||
|
|
|
@ -88,7 +88,7 @@ const emit = defineEmits<{
|
||||||
|
|
||||||
provide('inWindow', true);
|
provide('inWindow', true);
|
||||||
|
|
||||||
let rootEl = $ref<HTMLElement | null>();
|
let rootEl = $shallowRef<HTMLElement | null>();
|
||||||
let showing = $ref(true);
|
let showing = $ref(true);
|
||||||
let beforeClickedAt = 0;
|
let beforeClickedAt = 0;
|
||||||
let maximized = $ref(false);
|
let maximized = $ref(false);
|
||||||
|
|
|
@ -35,7 +35,7 @@ const emit = defineEmits<{
|
||||||
(ev: 'update:modelValue', v: boolean): void;
|
(ev: 'update:modelValue', v: boolean): void;
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
let button = $ref<HTMLElement>();
|
let button = $shallowRef<HTMLElement>();
|
||||||
const checked = toRefs(props).modelValue;
|
const checked = toRefs(props).modelValue;
|
||||||
const toggle = () => {
|
const toggle = () => {
|
||||||
if (props.disabled) return;
|
if (props.disabled) return;
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
<template>
|
<template>
|
||||||
<div class="dwzlatin" :class="{ opened }" ref="root">
|
<div class="dwzlatin" :class="{ opened }">
|
||||||
<div class="header _button" @click="toggle">
|
<div class="header _button" @click="toggle">
|
||||||
<span class="icon"><slot name="icon"></slot></span>
|
<span class="icon"><slot name="icon"></slot></span>
|
||||||
<span class="text"><slot name="label"></slot></span>
|
<span class="text"><slot name="label"></slot></span>
|
||||||
|
@ -19,7 +19,7 @@
|
||||||
>
|
>
|
||||||
<KeepAlive>
|
<KeepAlive>
|
||||||
<div v-show="opened">
|
<div v-show="opened">
|
||||||
<MkSpacer :margin-min="14" :margin-max="22" :container="root">
|
<MkSpacer :margin-min="14" :margin-max="22">
|
||||||
<slot></slot>
|
<slot></slot>
|
||||||
</MkSpacer>
|
</MkSpacer>
|
||||||
</div>
|
</div>
|
||||||
|
@ -40,7 +40,6 @@ const props = withDefaults(defineProps<{
|
||||||
|
|
||||||
let opened = $ref(props.defaultOpen);
|
let opened = $ref(props.defaultOpen);
|
||||||
let openedAtLeastOnce = $ref(props.defaultOpen);
|
let openedAtLeastOnce = $ref(props.defaultOpen);
|
||||||
let root = $ref<HTMLElement>();
|
|
||||||
|
|
||||||
function enter(el) {
|
function enter(el) {
|
||||||
const elementHeight = el.getBoundingClientRect().height;
|
const elementHeight = el.getBoundingClientRect().height;
|
||||||
|
@ -142,6 +141,7 @@ function toggle() {
|
||||||
> .body {
|
> .body {
|
||||||
background: var(--panel);
|
background: var(--panel);
|
||||||
border-radius: 0 0 6px 6px;
|
border-radius: 0 0 6px 6px;
|
||||||
|
container-type: inline-size;
|
||||||
}
|
}
|
||||||
|
|
||||||
&.opened {
|
&.opened {
|
||||||
|
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue