Merge remote-tracking branch 'misskey-dev/develop' into io
This commit is contained in:
commit
9ffa56aa1b
|
@ -160,14 +160,14 @@ id: 'aidx'
|
||||||
# Job concurrency per worker
|
# Job concurrency per worker
|
||||||
#deliverJobConcurrency: 128
|
#deliverJobConcurrency: 128
|
||||||
#inboxJobConcurrency: 16
|
#inboxJobConcurrency: 16
|
||||||
#relashionshipJobConcurrency: 16
|
#relationshipJobConcurrency: 16
|
||||||
# What's relashionshipJob?:
|
# What's relationshipJob?:
|
||||||
# Follow, unfollow, block and unblock(ings) while following-imports, etc. or account migrations.
|
# Follow, unfollow, block and unblock(ings) while following-imports, etc. or account migrations.
|
||||||
|
|
||||||
# Job rate limiter
|
# Job rate limiter
|
||||||
#deliverJobPerSec: 128
|
#deliverJobPerSec: 128
|
||||||
#inboxJobPerSec: 32
|
#inboxJobPerSec: 32
|
||||||
#relashionshipJobPerSec: 64
|
#relationshipJobPerSec: 64
|
||||||
|
|
||||||
# Job attempts
|
# Job attempts
|
||||||
#deliverJobMaxAttempts: 12
|
#deliverJobMaxAttempts: 12
|
||||||
|
|
|
@ -45,11 +45,19 @@
|
||||||
- Enhance: ノート作成画面のファイル添付メニューから直接ファイルを削除できるように
|
- Enhance: ノート作成画面のファイル添付メニューから直接ファイルを削除できるように
|
||||||
- Enhance: MFMの属性でオートコンプリートが使用できるように #12735
|
- Enhance: MFMの属性でオートコンプリートが使用できるように #12735
|
||||||
- Enhance: 絵文字編集ダイアログをモーダルではなくウィンドウで表示するように
|
- Enhance: 絵文字編集ダイアログをモーダルではなくウィンドウで表示するように
|
||||||
|
- Enhance: リモートのユーザーはメニューから直接リモートで表示できるように
|
||||||
- Fix: ネイティブモードの絵文字がモノクロにならないように
|
- Fix: ネイティブモードの絵文字がモノクロにならないように
|
||||||
- Fix: v2023.12.0で追加された「モデレーターがユーザーのアイコンもしくはバナー画像を未設定状態にできる機能」が管理画面上で正しく表示されていない問題を修正
|
- Fix: v2023.12.0で追加された「モデレーターがユーザーのアイコンもしくはバナー画像を未設定状態にできる機能」が管理画面上で正しく表示されていない問題を修正
|
||||||
- Fix: AiScriptの`readline`関数が不正な値を返すことがある問題のv2023.12.0時点での修正がPlay以外に適用されていないのを修正
|
- Fix: AiScriptの`readline`関数が不正な値を返すことがある問題のv2023.12.0時点での修正がPlay以外に適用されていないのを修正
|
||||||
- Fix: v2023.12.1で追加された`$[clickable ...]`および`onClickEv`が正しく機能していないのを修正
|
- Fix: v2023.12.1で追加された`$[clickable ...]`および`onClickEv`が正しく機能していないのを修正
|
||||||
|
- Fix: Renoteのキーボードショートカットが機能していなかった問題を修正
|
||||||
|
- Fix: 投稿フォームでアンケートの日時指定をした状態で再読み込みをすると期日が復元されない問題を修正
|
||||||
|
- Fix: アンケートを設定したノートを「削除して編集」をするとアンケートの期日が引き継がれず、リセットされてしまう問題を修正
|
||||||
|
- Fix: デッキのプロファイル作成時に名前を空にできる問題を修正
|
||||||
|
- Fix: テーマ作成時に名称が空欄でも作成できてしまう問題を修正
|
||||||
|
- Fix: プラグインで`Plugin:register_note_post_interruptor`を使用すると、ノートが投稿できなくなる問題を修正
|
||||||
- Enhance: ページ遷移時にPlayerを閉じるように
|
- Enhance: ページ遷移時にPlayerを閉じるように
|
||||||
|
- Fix: iOSで大きな画像を変換してアップロードできない問題を修正
|
||||||
|
|
||||||
### Server
|
### Server
|
||||||
- Enhance: 連合先のレートリミットに引っかかった際にリトライするようになりました
|
- Enhance: 連合先のレートリミットに引っかかった際にリトライするようになりました
|
||||||
|
@ -62,6 +70,7 @@
|
||||||
- Fix: `notes/create`で、`text`が空白文字のみで構成されていてかつリノート、ファイルまたは投票を含んでいるリクエストに対するレスポンスの`text`が`""`から`null`になるように変更
|
- Fix: `notes/create`で、`text`が空白文字のみで構成されていてかつリノート、ファイルまたは投票を含んでいるリクエストに対するレスポンスの`text`が`""`から`null`になるように変更
|
||||||
- Fix: ipv4とipv6の両方が利用可能な環境でallowedPrivateNetworksが設定されていた場合プライベートipの検証ができていなかった問題を修正
|
- Fix: ipv4とipv6の両方が利用可能な環境でallowedPrivateNetworksが設定されていた場合プライベートipの検証ができていなかった問題を修正
|
||||||
- Fix: properly handle cc followers
|
- Fix: properly handle cc followers
|
||||||
|
- Fix: ジョブに関する設定の名前を修正 relashionshipJobPerSec -> relationshipJobPerSec
|
||||||
|
|
||||||
### Service Worker
|
### Service Worker
|
||||||
- Enhance: オフライン表示のデザインを改善・多言語対応
|
- Enhance: オフライン表示のデザインを改善・多言語対応
|
||||||
|
|
|
@ -0,0 +1,30 @@
|
||||||
|
describe('Router transition', () => {
|
||||||
|
describe('Redirect', () => {
|
||||||
|
// サーバの初期化。ルートのテストに関しては各describeごとに1度だけ実行で十分だと思う(使いまわした方が早い)
|
||||||
|
before(() => {
|
||||||
|
cy.resetState();
|
||||||
|
|
||||||
|
// インスタンス初期セットアップ
|
||||||
|
cy.registerUser('admin', 'pass', true);
|
||||||
|
|
||||||
|
// ユーザー作成
|
||||||
|
cy.registerUser('alice', 'alice1234');
|
||||||
|
|
||||||
|
cy.login('alice', 'alice1234');
|
||||||
|
|
||||||
|
// アカウント初期設定ウィザード
|
||||||
|
// 表示に時間がかかるのでデフォルト秒数だとタイムアウトする
|
||||||
|
cy.get('[data-cy-user-setup] [data-cy-modal-window-close]', { timeout: 12000 }).click();
|
||||||
|
cy.wait(500);
|
||||||
|
cy.get('[data-cy-modal-dialog-ok]').click();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('redirect to user profile', () => {
|
||||||
|
// テストのためだけに用意されたリダイレクト用ルートに飛ぶ
|
||||||
|
cy.visit('/redirect-test');
|
||||||
|
|
||||||
|
// プロフィールページのURLであることを確認する
|
||||||
|
cy.url().should('include', '/@alice')
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
|
@ -5010,7 +5010,7 @@ export interface Locale extends ILocale {
|
||||||
*/
|
*/
|
||||||
"readConfirmText": ParameterizedString<"title">;
|
"readConfirmText": ParameterizedString<"title">;
|
||||||
/**
|
/**
|
||||||
* 特に新規ユーザーのUXを損ねる可能性が高いため、ストック情報ではなくフロー情報の掲示にお知らせを使用することを推奨します。
|
* 特に新規ユーザーのUXを損ねる可能性が高いため、常時掲示するための情報ではなく、即時性が求められる情報の掲示のためにお知らせを使用することを推奨します。
|
||||||
*/
|
*/
|
||||||
"shouldNotBeUsedToPresentPermanentInfo": string;
|
"shouldNotBeUsedToPresentPermanentInfo": string;
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -1255,7 +1255,7 @@ _announcement:
|
||||||
tooManyActiveAnnouncementDescription: "アクティブなお知らせが多いため、UXが低下する可能性があります。終了したお知らせはアーカイブすることを検討してください。"
|
tooManyActiveAnnouncementDescription: "アクティブなお知らせが多いため、UXが低下する可能性があります。終了したお知らせはアーカイブすることを検討してください。"
|
||||||
readConfirmTitle: "既読にしますか?"
|
readConfirmTitle: "既読にしますか?"
|
||||||
readConfirmText: "「{title}」の内容を読み、既読にします。"
|
readConfirmText: "「{title}」の内容を読み、既読にします。"
|
||||||
shouldNotBeUsedToPresentPermanentInfo: "特に新規ユーザーのUXを損ねる可能性が高いため、ストック情報ではなくフロー情報の掲示にお知らせを使用することを推奨します。"
|
shouldNotBeUsedToPresentPermanentInfo: "特に新規ユーザーのUXを損ねる可能性が高いため、常時掲示するための情報ではなく、即時性が求められる情報の掲示のためにお知らせを使用することを推奨します。"
|
||||||
dialogAnnouncementUxWarn: "ダイアログ形式のお知らせが同時に2つ以上ある場合、UXに悪影響を及ぼす可能性が非常に高いため、使用は慎重に行うことを推奨します。"
|
dialogAnnouncementUxWarn: "ダイアログ形式のお知らせが同時に2つ以上ある場合、UXに悪影響を及ぼす可能性が非常に高いため、使用は慎重に行うことを推奨します。"
|
||||||
silence: "非通知"
|
silence: "非通知"
|
||||||
silenceDescription: "オンにすると、このお知らせは通知されず、既読にする必要もなくなります。"
|
silenceDescription: "オンにすると、このお知らせは通知されず、既読にする必要もなくなります。"
|
||||||
|
|
|
@ -35,17 +35,17 @@
|
||||||
},
|
},
|
||||||
"optionalDependencies": {
|
"optionalDependencies": {
|
||||||
"@swc/core-android-arm64": "1.3.11",
|
"@swc/core-android-arm64": "1.3.11",
|
||||||
"@swc/core-darwin-arm64": "1.3.105",
|
"@swc/core-darwin-arm64": "1.3.107",
|
||||||
"@swc/core-darwin-x64": "1.3.105",
|
"@swc/core-darwin-x64": "1.3.107",
|
||||||
"@swc/core-freebsd-x64": "1.3.11",
|
"@swc/core-freebsd-x64": "1.3.11",
|
||||||
"@swc/core-linux-arm-gnueabihf": "1.3.105",
|
"@swc/core-linux-arm-gnueabihf": "1.3.107",
|
||||||
"@swc/core-linux-arm64-gnu": "1.3.105",
|
"@swc/core-linux-arm64-gnu": "1.3.107",
|
||||||
"@swc/core-linux-arm64-musl": "1.3.105",
|
"@swc/core-linux-arm64-musl": "1.3.107",
|
||||||
"@swc/core-linux-x64-gnu": "1.3.105",
|
"@swc/core-linux-x64-gnu": "1.3.107",
|
||||||
"@swc/core-linux-x64-musl": "1.3.105",
|
"@swc/core-linux-x64-musl": "1.3.107",
|
||||||
"@swc/core-win32-arm64-msvc": "1.3.105",
|
"@swc/core-win32-arm64-msvc": "1.3.107",
|
||||||
"@swc/core-win32-ia32-msvc": "1.3.105",
|
"@swc/core-win32-ia32-msvc": "1.3.107",
|
||||||
"@swc/core-win32-x64-msvc": "1.3.105",
|
"@swc/core-win32-x64-msvc": "1.3.107",
|
||||||
"@tensorflow/tfjs": "4.4.0",
|
"@tensorflow/tfjs": "4.4.0",
|
||||||
"@tensorflow/tfjs-node": "4.4.0",
|
"@tensorflow/tfjs-node": "4.4.0",
|
||||||
"bufferutil": "4.0.8",
|
"bufferutil": "4.0.8",
|
||||||
|
@ -67,9 +67,9 @@
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@aws-sdk/client-s3": "3.412.0",
|
"@aws-sdk/client-s3": "3.412.0",
|
||||||
"@aws-sdk/lib-storage": "3.412.0",
|
"@aws-sdk/lib-storage": "3.412.0",
|
||||||
"@bull-board/api": "5.13.0",
|
"@bull-board/api": "5.14.0",
|
||||||
"@bull-board/fastify": "5.13.0",
|
"@bull-board/fastify": "5.14.0",
|
||||||
"@bull-board/ui": "5.13.0",
|
"@bull-board/ui": "5.14.0",
|
||||||
"@discordapp/twemoji": "15.0.2",
|
"@discordapp/twemoji": "15.0.2",
|
||||||
"@fastify/accepts": "4.3.0",
|
"@fastify/accepts": "4.3.0",
|
||||||
"@fastify/cookie": "9.3.1",
|
"@fastify/cookie": "9.3.1",
|
||||||
|
@ -85,11 +85,11 @@
|
||||||
"@nestjs/core": "10.3.1",
|
"@nestjs/core": "10.3.1",
|
||||||
"@nestjs/testing": "10.3.1",
|
"@nestjs/testing": "10.3.1",
|
||||||
"@peertube/http-signature": "1.7.0",
|
"@peertube/http-signature": "1.7.0",
|
||||||
"@simplewebauthn/server": "9.0.0",
|
"@simplewebauthn/server": "9.0.1",
|
||||||
"@sinonjs/fake-timers": "11.2.2",
|
"@sinonjs/fake-timers": "11.2.2",
|
||||||
"@smithy/node-http-handler": "2.3.1",
|
"@smithy/node-http-handler": "2.3.1",
|
||||||
"@swc/cli": "0.1.65",
|
"@swc/cli": "0.1.65",
|
||||||
"@swc/core": "1.3.105",
|
"@swc/core": "1.3.107",
|
||||||
"@twemoji/parser": "15.0.0",
|
"@twemoji/parser": "15.0.0",
|
||||||
"accepts": "1.3.8",
|
"accepts": "1.3.8",
|
||||||
"ajv": "8.12.0",
|
"ajv": "8.12.0",
|
||||||
|
@ -109,13 +109,13 @@
|
||||||
"content-disposition": "0.5.4",
|
"content-disposition": "0.5.4",
|
||||||
"date-fns": "2.30.0",
|
"date-fns": "2.30.0",
|
||||||
"deep-email-validator": "0.1.21",
|
"deep-email-validator": "0.1.21",
|
||||||
"fastify": "4.25.2",
|
"fastify": "4.26.0",
|
||||||
"fastify-raw-body": "4.3.0",
|
"fastify-raw-body": "4.3.0",
|
||||||
"feed": "4.2.2",
|
"feed": "4.2.2",
|
||||||
"file-type": "19.0.0",
|
"file-type": "19.0.0",
|
||||||
"fluent-ffmpeg": "2.1.2",
|
"fluent-ffmpeg": "2.1.2",
|
||||||
"form-data": "4.0.0",
|
"form-data": "4.0.0",
|
||||||
"got": "14.0.0",
|
"got": "14.1.0",
|
||||||
"happy-dom": "10.0.3",
|
"happy-dom": "10.0.3",
|
||||||
"hpagent": "1.2.0",
|
"hpagent": "1.2.0",
|
||||||
"http-link-header": "1.1.1",
|
"http-link-header": "1.1.1",
|
||||||
|
@ -149,7 +149,7 @@
|
||||||
"pg": "8.11.3",
|
"pg": "8.11.3",
|
||||||
"pino": "8.17.2",
|
"pino": "8.17.2",
|
||||||
"pino-pretty": "10.3.1",
|
"pino-pretty": "10.3.1",
|
||||||
"pkce-challenge": "4.0.1",
|
"pkce-challenge": "4.1.0",
|
||||||
"probe-image-size": "7.2.3",
|
"probe-image-size": "7.2.3",
|
||||||
"promise-limit": "2.7.0",
|
"promise-limit": "2.7.0",
|
||||||
"pug": "3.0.2",
|
"pug": "3.0.2",
|
||||||
|
@ -175,7 +175,7 @@
|
||||||
"tmp": "0.2.1",
|
"tmp": "0.2.1",
|
||||||
"tsc-alias": "1.8.8",
|
"tsc-alias": "1.8.8",
|
||||||
"tsconfig-paths": "4.2.0",
|
"tsconfig-paths": "4.2.0",
|
||||||
"typeorm": "0.3.19",
|
"typeorm": "0.3.20",
|
||||||
"typescript": "5.3.3",
|
"typescript": "5.3.3",
|
||||||
"ulid": "2.3.0",
|
"ulid": "2.3.0",
|
||||||
"vary": "1.1.2",
|
"vary": "1.1.2",
|
||||||
|
@ -187,7 +187,7 @@
|
||||||
"@jest/globals": "29.7.0",
|
"@jest/globals": "29.7.0",
|
||||||
"@misskey-dev/eslint-plugin": "1.0.0",
|
"@misskey-dev/eslint-plugin": "1.0.0",
|
||||||
"@nestjs/platform-express": "10.3.1",
|
"@nestjs/platform-express": "10.3.1",
|
||||||
"@simplewebauthn/types": "9.0.0",
|
"@simplewebauthn/types": "9.0.1",
|
||||||
"@swc/jest": "0.2.31",
|
"@swc/jest": "0.2.31",
|
||||||
"@types/accepts": "1.3.7",
|
"@types/accepts": "1.3.7",
|
||||||
"@types/archiver": "6.0.2",
|
"@types/archiver": "6.0.2",
|
||||||
|
@ -204,12 +204,12 @@
|
||||||
"@types/jsrsasign": "10.5.12",
|
"@types/jsrsasign": "10.5.12",
|
||||||
"@types/mime-types": "2.1.4",
|
"@types/mime-types": "2.1.4",
|
||||||
"@types/ms": "0.7.34",
|
"@types/ms": "0.7.34",
|
||||||
"@types/node": "20.11.6",
|
"@types/node": "20.11.10",
|
||||||
"@types/nodemailer": "6.4.14",
|
"@types/nodemailer": "6.4.14",
|
||||||
"@types/oauth": "0.9.4",
|
"@types/oauth": "0.9.4",
|
||||||
"@types/oauth2orize": "1.11.3",
|
"@types/oauth2orize": "1.11.3",
|
||||||
"@types/oauth2orize-pkce": "0.1.2",
|
"@types/oauth2orize-pkce": "0.1.2",
|
||||||
"@types/pg": "8.10.9",
|
"@types/pg": "8.11.0",
|
||||||
"@types/pug": "2.0.10",
|
"@types/pug": "2.0.10",
|
||||||
"@types/punycode": "2.1.3",
|
"@types/punycode": "2.1.3",
|
||||||
"@types/qrcode": "1.5.5",
|
"@types/qrcode": "1.5.5",
|
||||||
|
|
|
@ -88,10 +88,10 @@ type Source = {
|
||||||
bullmqWorkerOptions?: Partial<Bull.WorkerOptions>;
|
bullmqWorkerOptions?: Partial<Bull.WorkerOptions>;
|
||||||
deliverJobConcurrency?: number;
|
deliverJobConcurrency?: number;
|
||||||
inboxJobConcurrency?: number;
|
inboxJobConcurrency?: number;
|
||||||
relashionshipJobConcurrency?: number;
|
relationshipJobConcurrency?: number;
|
||||||
deliverJobPerSec?: number;
|
deliverJobPerSec?: number;
|
||||||
inboxJobPerSec?: number;
|
inboxJobPerSec?: number;
|
||||||
relashionshipJobPerSec?: number;
|
relationshipJobPerSec?: number;
|
||||||
deliverJobMaxAttempts?: number;
|
deliverJobMaxAttempts?: number;
|
||||||
inboxJobMaxAttempts?: number;
|
inboxJobMaxAttempts?: number;
|
||||||
|
|
||||||
|
@ -152,10 +152,10 @@ export type Config = {
|
||||||
bullmqWorkerOptions: Partial<Bull.WorkerOptions>;
|
bullmqWorkerOptions: Partial<Bull.WorkerOptions>;
|
||||||
deliverJobConcurrency: number | undefined;
|
deliverJobConcurrency: number | undefined;
|
||||||
inboxJobConcurrency: number | undefined;
|
inboxJobConcurrency: number | undefined;
|
||||||
relashionshipJobConcurrency: number | undefined;
|
relationshipJobConcurrency: number | undefined;
|
||||||
deliverJobPerSec: number | undefined;
|
deliverJobPerSec: number | undefined;
|
||||||
inboxJobPerSec: number | undefined;
|
inboxJobPerSec: number | undefined;
|
||||||
relashionshipJobPerSec: number | undefined;
|
relationshipJobPerSec: number | undefined;
|
||||||
deliverJobMaxAttempts: number | undefined;
|
deliverJobMaxAttempts: number | undefined;
|
||||||
inboxJobMaxAttempts: number | undefined;
|
inboxJobMaxAttempts: number | undefined;
|
||||||
proxyRemoteFiles: boolean | undefined;
|
proxyRemoteFiles: boolean | undefined;
|
||||||
|
@ -278,10 +278,10 @@ export function loadConfig(): Config {
|
||||||
bullmqWorkerOptions: config.bullmqWorkerOptions ?? {},
|
bullmqWorkerOptions: config.bullmqWorkerOptions ?? {},
|
||||||
deliverJobConcurrency: config.deliverJobConcurrency,
|
deliverJobConcurrency: config.deliverJobConcurrency,
|
||||||
inboxJobConcurrency: config.inboxJobConcurrency,
|
inboxJobConcurrency: config.inboxJobConcurrency,
|
||||||
relashionshipJobConcurrency: config.relashionshipJobConcurrency,
|
relationshipJobConcurrency: config.relationshipJobConcurrency,
|
||||||
deliverJobPerSec: config.deliverJobPerSec,
|
deliverJobPerSec: config.deliverJobPerSec,
|
||||||
inboxJobPerSec: config.inboxJobPerSec,
|
inboxJobPerSec: config.inboxJobPerSec,
|
||||||
relashionshipJobPerSec: config.relashionshipJobPerSec,
|
relationshipJobPerSec: config.relationshipJobPerSec,
|
||||||
deliverJobMaxAttempts: config.deliverJobMaxAttempts,
|
deliverJobMaxAttempts: config.deliverJobMaxAttempts,
|
||||||
inboxJobMaxAttempts: config.inboxJobMaxAttempts,
|
inboxJobMaxAttempts: config.inboxJobMaxAttempts,
|
||||||
proxyRemoteFiles: config.proxyRemoteFiles,
|
proxyRemoteFiles: config.proxyRemoteFiles,
|
||||||
|
|
|
@ -54,9 +54,9 @@ export interface MainEventTypes {
|
||||||
reply: Packed<'Note'>;
|
reply: Packed<'Note'>;
|
||||||
renote: Packed<'Note'>;
|
renote: Packed<'Note'>;
|
||||||
follow: Packed<'UserDetailedNotMe'>;
|
follow: Packed<'UserDetailedNotMe'>;
|
||||||
followed: Packed<'User'>;
|
followed: Packed<'UserDetailed' | 'UserLite'>;
|
||||||
unfollow: Packed<'User'>;
|
unfollow: Packed<'UserDetailed'>;
|
||||||
meUpdated: Packed<'User'>;
|
meUpdated: Packed<'UserDetailed'>;
|
||||||
pageEvent: {
|
pageEvent: {
|
||||||
pageId: MiPage['id'];
|
pageId: MiPage['id'];
|
||||||
event: string;
|
event: string;
|
||||||
|
|
|
@ -94,6 +94,29 @@ type ToJsonSchema<S> = {
|
||||||
};
|
};
|
||||||
|
|
||||||
export function getJsonSchema<S extends Schema>(schema: S): ToJsonSchema<Unflatten<ChartResult<S>>> {
|
export function getJsonSchema<S extends Schema>(schema: S): ToJsonSchema<Unflatten<ChartResult<S>>> {
|
||||||
|
const unflatten = (str: string, parent: Record<string, any>) => {
|
||||||
|
const keys = str.split('.');
|
||||||
|
const key = keys.shift();
|
||||||
|
const nextKey = keys[0];
|
||||||
|
|
||||||
|
if (key == null) return;
|
||||||
|
|
||||||
|
if (parent.properties[key] == null) {
|
||||||
|
parent.properties[key] = nextKey ? {
|
||||||
|
type: 'object',
|
||||||
|
properties: {},
|
||||||
|
required: [],
|
||||||
|
} : {
|
||||||
|
type: 'array',
|
||||||
|
items: {
|
||||||
|
type: 'number',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (nextKey) unflatten(keys.join('.'), parent.properties[key] as Record<string, any>);
|
||||||
|
};
|
||||||
|
|
||||||
const jsonSchema = {
|
const jsonSchema = {
|
||||||
type: 'object',
|
type: 'object',
|
||||||
properties: {} as Record<string, unknown>,
|
properties: {} as Record<string, unknown>,
|
||||||
|
@ -101,10 +124,7 @@ export function getJsonSchema<S extends Schema>(schema: S): ToJsonSchema<Unflatt
|
||||||
};
|
};
|
||||||
|
|
||||||
for (const k in schema) {
|
for (const k in schema) {
|
||||||
jsonSchema.properties[k] = {
|
unflatten(k, jsonSchema);
|
||||||
type: 'array',
|
|
||||||
items: { type: 'number' },
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return jsonSchema as ToJsonSchema<Unflatten<ChartResult<S>>>;
|
return jsonSchema as ToJsonSchema<Unflatten<ChartResult<S>>>;
|
||||||
|
|
|
@ -164,7 +164,7 @@ export class NoteEntityService implements OnModuleInit {
|
||||||
|
|
||||||
return {
|
return {
|
||||||
multiple: poll.multiple,
|
multiple: poll.multiple,
|
||||||
expiresAt: poll.expiresAt,
|
expiresAt: poll.expiresAt?.toISOString() ?? null,
|
||||||
choices,
|
choices,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
@ -6,7 +6,7 @@
|
||||||
// structredCloneが遅いため
|
// structredCloneが遅いため
|
||||||
// SEE: http://var.blog.jp/archives/86038606.html
|
// SEE: http://var.blog.jp/archives/86038606.html
|
||||||
|
|
||||||
type Cloneable = string | number | boolean | null | { [key: string]: Cloneable } | Cloneable[];
|
type Cloneable = string | number | boolean | null | undefined | { [key: string]: Cloneable } | Cloneable[];
|
||||||
|
|
||||||
export function deepClone<T extends Cloneable>(x: T): T {
|
export function deepClone<T extends Cloneable>(x: T): T {
|
||||||
if (typeof x === 'object') {
|
if (typeof x === 'object') {
|
||||||
|
@ -14,7 +14,7 @@ export function deepClone<T extends Cloneable>(x: T): T {
|
||||||
if (Array.isArray(x)) return x.map(deepClone) as T;
|
if (Array.isArray(x)) return x.map(deepClone) as T;
|
||||||
const obj = {} as Record<string, Cloneable>;
|
const obj = {} as Record<string, Cloneable>;
|
||||||
for (const [k, v] of Object.entries(x)) {
|
for (const [k, v] of Object.entries(x)) {
|
||||||
obj[k] = deepClone(v);
|
obj[k] = v === undefined ? undefined : deepClone(v);
|
||||||
}
|
}
|
||||||
return obj as T;
|
return obj as T;
|
||||||
} else {
|
} else {
|
||||||
|
|
|
@ -32,14 +32,14 @@ import { packedNoteFavoriteSchema } from '@/models/json-schema/note-favorite.js'
|
||||||
import { packedNoteReactionSchema } from '@/models/json-schema/note-reaction.js';
|
import { packedNoteReactionSchema } from '@/models/json-schema/note-reaction.js';
|
||||||
import { packedNoteSchema } from '@/models/json-schema/note.js';
|
import { packedNoteSchema } from '@/models/json-schema/note.js';
|
||||||
import { packedNotificationSchema } from '@/models/json-schema/notification.js';
|
import { packedNotificationSchema } from '@/models/json-schema/notification.js';
|
||||||
import { packedPageLikeSchema, packedPageSchema } from '@/models/json-schema/page.js';
|
import { packedPageLikeSchema, packedPageBlockSchema, packedPageSchema } from '@/models/json-schema/page.js';
|
||||||
import { packedQueueCountSchema } from '@/models/json-schema/queue.js';
|
import { packedQueueCountSchema } from '@/models/json-schema/queue.js';
|
||||||
import { packedEmojiDetailedSchema, packedEmojiSimpleSchema } from '@/models/json-schema/emoji.js';
|
import { packedEmojiDetailedSchema, packedEmojiSimpleSchema } from '@/models/json-schema/emoji.js';
|
||||||
import { packedRenoteMutingSchema } from '@/models/json-schema/renote-muting.js';
|
import { packedRenoteMutingSchema } from '@/models/json-schema/renote-muting.js';
|
||||||
import { packedUserListMembershipSchema, packedUserListSchema } from '@/models/json-schema/user-list.js';
|
import { packedUserListMembershipSchema, packedUserListSchema } from '@/models/json-schema/user-list.js';
|
||||||
import { packedAnnouncementSchema } from '@/models/json-schema/announcement.js';
|
import { packedAnnouncementSchema } from '@/models/json-schema/announcement.js';
|
||||||
import { packedSigninSchema } from '@/models/json-schema/signin.js';
|
import { packedSigninSchema } from '@/models/json-schema/signin.js';
|
||||||
import { packedRoleLiteSchema, packedRoleSchema } from '@/models/json-schema/role.js';
|
import { packedRoleLiteSchema, packedRoleSchema, packedRolePoliciesSchema } from '@/models/json-schema/role.js';
|
||||||
import { packedAdSchema } from '@/models/json-schema/ad.js';
|
import { packedAdSchema } from '@/models/json-schema/ad.js';
|
||||||
import { packedReversiGameLiteSchema, packedReversiGameDetailedSchema } from '@/models/json-schema/reversi-game.js';
|
import { packedReversiGameLiteSchema, packedReversiGameDetailedSchema } from '@/models/json-schema/reversi-game.js';
|
||||||
|
|
||||||
|
@ -71,6 +71,7 @@ export const refs = {
|
||||||
Hashtag: packedHashtagSchema,
|
Hashtag: packedHashtagSchema,
|
||||||
InviteCode: packedInviteCodeSchema,
|
InviteCode: packedInviteCodeSchema,
|
||||||
Page: packedPageSchema,
|
Page: packedPageSchema,
|
||||||
|
PageBlock: packedPageBlockSchema,
|
||||||
PageLike: packedPageLikeSchema,
|
PageLike: packedPageLikeSchema,
|
||||||
Channel: packedChannelSchema,
|
Channel: packedChannelSchema,
|
||||||
QueueCount: packedQueueCountSchema,
|
QueueCount: packedQueueCountSchema,
|
||||||
|
@ -87,6 +88,7 @@ export const refs = {
|
||||||
Signin: packedSigninSchema,
|
Signin: packedSigninSchema,
|
||||||
RoleLite: packedRoleLiteSchema,
|
RoleLite: packedRoleLiteSchema,
|
||||||
Role: packedRoleSchema,
|
Role: packedRoleSchema,
|
||||||
|
RolePolicies: packedRolePoliciesSchema,
|
||||||
ReversiGameLite: packedReversiGameLiteSchema,
|
ReversiGameLite: packedReversiGameLiteSchema,
|
||||||
ReversiGameDetailed: packedReversiGameDetailedSchema,
|
ReversiGameDetailed: packedReversiGameDetailedSchema,
|
||||||
AbuseUserReport: packedAbuseUserReportSchema,
|
AbuseUserReport: packedAbuseUserReportSchema,
|
||||||
|
@ -95,6 +97,9 @@ export const refs = {
|
||||||
|
|
||||||
export type Packed<x extends keyof typeof refs> = SchemaType<typeof refs[x]>;
|
export type Packed<x extends keyof typeof refs> = SchemaType<typeof refs[x]>;
|
||||||
|
|
||||||
|
export type KeyOf<x extends keyof typeof refs> = PropertiesToUnion<typeof refs[x]>;
|
||||||
|
type PropertiesToUnion<p extends Schema> = p['properties'] extends NonNullable<Obj> ? keyof p['properties'] : never;
|
||||||
|
|
||||||
type TypeStringef = 'null' | 'boolean' | 'integer' | 'number' | 'string' | 'array' | 'object' | 'any';
|
type TypeStringef = 'null' | 'boolean' | 'integer' | 'number' | 'string' | 'array' | 'object' | 'any';
|
||||||
type StringDefToType<T extends TypeStringef> =
|
type StringDefToType<T extends TypeStringef> =
|
||||||
T extends 'null' ? null :
|
T extends 'null' ? null :
|
||||||
|
|
|
@ -45,7 +45,7 @@ export class MiAnnouncement {
|
||||||
length: 256, nullable: false,
|
length: 256, nullable: false,
|
||||||
default: 'info',
|
default: 'info',
|
||||||
})
|
})
|
||||||
public icon: string;
|
public icon: 'info' | 'warning' | 'error' | 'success';
|
||||||
|
|
||||||
// normal ... お知らせページ掲載
|
// normal ... お知らせページ掲載
|
||||||
// banner ... お知らせページ掲載 + バナー表示
|
// banner ... お知らせページ掲載 + バナー表示
|
||||||
|
@ -54,7 +54,7 @@ export class MiAnnouncement {
|
||||||
length: 256, nullable: false,
|
length: 256, nullable: false,
|
||||||
default: 'normal',
|
default: 'normal',
|
||||||
})
|
})
|
||||||
public display: string;
|
public display: 'normal' | 'banner' | 'dialog';
|
||||||
|
|
||||||
@Column('boolean', {
|
@Column('boolean', {
|
||||||
default: false,
|
default: false,
|
||||||
|
|
|
@ -37,10 +37,12 @@ export const packedAnnouncementSchema = {
|
||||||
icon: {
|
icon: {
|
||||||
type: 'string',
|
type: 'string',
|
||||||
optional: false, nullable: false,
|
optional: false, nullable: false,
|
||||||
|
enum: ['info', 'warning', 'error', 'success'],
|
||||||
},
|
},
|
||||||
display: {
|
display: {
|
||||||
type: 'string',
|
type: 'string',
|
||||||
optional: false, nullable: false,
|
optional: false, nullable: false,
|
||||||
|
enum: ['dialog', 'normal', 'banner'],
|
||||||
},
|
},
|
||||||
needConfirmationToRead: {
|
needConfirmationToRead: {
|
||||||
type: 'boolean',
|
type: 'boolean',
|
||||||
|
|
|
@ -69,6 +69,7 @@ export const packedNoteSchema = {
|
||||||
visibility: {
|
visibility: {
|
||||||
type: 'string',
|
type: 'string',
|
||||||
optional: false, nullable: false,
|
optional: false, nullable: false,
|
||||||
|
enum: ['public', 'home', 'followers', 'specified'],
|
||||||
},
|
},
|
||||||
mentions: {
|
mentions: {
|
||||||
type: 'array',
|
type: 'array',
|
||||||
|
@ -117,6 +118,48 @@ export const packedNoteSchema = {
|
||||||
poll: {
|
poll: {
|
||||||
type: 'object',
|
type: 'object',
|
||||||
optional: true, nullable: true,
|
optional: true, nullable: true,
|
||||||
|
properties: {
|
||||||
|
expiresAt: {
|
||||||
|
type: 'string',
|
||||||
|
optional: true, nullable: true,
|
||||||
|
format: 'date-time',
|
||||||
|
},
|
||||||
|
multiple: {
|
||||||
|
type: 'boolean',
|
||||||
|
optional: false, nullable: false,
|
||||||
|
},
|
||||||
|
choices: {
|
||||||
|
type: 'array',
|
||||||
|
optional: false, nullable: false,
|
||||||
|
items: {
|
||||||
|
type: 'object',
|
||||||
|
optional: false, nullable: false,
|
||||||
|
properties: {
|
||||||
|
isVoted: {
|
||||||
|
type: 'boolean',
|
||||||
|
optional: false, nullable: false,
|
||||||
|
},
|
||||||
|
text: {
|
||||||
|
type: 'string',
|
||||||
|
optional: false, nullable: false,
|
||||||
|
},
|
||||||
|
votes: {
|
||||||
|
type: 'number',
|
||||||
|
optional: false, nullable: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
emojis: {
|
||||||
|
type: 'object',
|
||||||
|
optional: true, nullable: false,
|
||||||
|
additionalProperties: {
|
||||||
|
anyOf: [{
|
||||||
|
type: 'string',
|
||||||
|
}],
|
||||||
|
},
|
||||||
},
|
},
|
||||||
channelId: {
|
channelId: {
|
||||||
type: 'string',
|
type: 'string',
|
||||||
|
@ -162,9 +205,23 @@ export const packedNoteSchema = {
|
||||||
type: 'string',
|
type: 'string',
|
||||||
optional: false, nullable: true,
|
optional: false, nullable: true,
|
||||||
},
|
},
|
||||||
|
reactionEmojis: {
|
||||||
|
type: 'object',
|
||||||
|
optional: false, nullable: false,
|
||||||
|
additionalProperties: {
|
||||||
|
anyOf: [{
|
||||||
|
type: 'string',
|
||||||
|
}],
|
||||||
|
},
|
||||||
|
},
|
||||||
reactions: {
|
reactions: {
|
||||||
type: 'object',
|
type: 'object',
|
||||||
optional: false, nullable: false,
|
optional: false, nullable: false,
|
||||||
|
additionalProperties: {
|
||||||
|
anyOf: [{
|
||||||
|
type: 'number',
|
||||||
|
}],
|
||||||
|
},
|
||||||
},
|
},
|
||||||
renoteCount: {
|
renoteCount: {
|
||||||
type: 'number',
|
type: 'number',
|
||||||
|
@ -196,7 +253,7 @@ export const packedNoteSchema = {
|
||||||
},
|
},
|
||||||
|
|
||||||
myReaction: {
|
myReaction: {
|
||||||
type: 'object',
|
type: 'string',
|
||||||
optional: true, nullable: true,
|
optional: true, nullable: true,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
|
@ -5,7 +5,7 @@
|
||||||
|
|
||||||
import { notificationTypes } from '@/types.js';
|
import { notificationTypes } from '@/types.js';
|
||||||
|
|
||||||
export const packedNotificationSchema = {
|
const baseSchema = {
|
||||||
type: 'object',
|
type: 'object',
|
||||||
properties: {
|
properties: {
|
||||||
id: {
|
id: {
|
||||||
|
@ -23,68 +23,368 @@ export const packedNotificationSchema = {
|
||||||
optional: false, nullable: false,
|
optional: false, nullable: false,
|
||||||
enum: [...notificationTypes, 'reaction:grouped', 'renote:grouped'],
|
enum: [...notificationTypes, 'reaction:grouped', 'renote:grouped'],
|
||||||
},
|
},
|
||||||
user: {
|
},
|
||||||
type: 'object',
|
} as const;
|
||||||
ref: 'UserLite',
|
|
||||||
optional: true, nullable: true,
|
export const packedNotificationSchema = {
|
||||||
},
|
type: 'object',
|
||||||
userId: {
|
oneOf: [{
|
||||||
type: 'string',
|
type: 'object',
|
||||||
optional: true, nullable: true,
|
properties: {
|
||||||
format: 'id',
|
...baseSchema.properties,
|
||||||
},
|
type: {
|
||||||
note: {
|
type: 'string',
|
||||||
type: 'object',
|
optional: false, nullable: false,
|
||||||
ref: 'Note',
|
enum: ['note'],
|
||||||
optional: true, nullable: true,
|
|
||||||
},
|
|
||||||
reaction: {
|
|
||||||
type: 'string',
|
|
||||||
optional: true, nullable: true,
|
|
||||||
},
|
|
||||||
achievement: {
|
|
||||||
type: 'string',
|
|
||||||
optional: true, nullable: false,
|
|
||||||
},
|
|
||||||
body: {
|
|
||||||
type: 'string',
|
|
||||||
optional: true, nullable: true,
|
|
||||||
},
|
|
||||||
header: {
|
|
||||||
type: 'string',
|
|
||||||
optional: true, nullable: true,
|
|
||||||
},
|
|
||||||
icon: {
|
|
||||||
type: 'string',
|
|
||||||
optional: true, nullable: true,
|
|
||||||
},
|
|
||||||
reactions: {
|
|
||||||
type: 'array',
|
|
||||||
optional: true, nullable: true,
|
|
||||||
items: {
|
|
||||||
type: 'object',
|
|
||||||
properties: {
|
|
||||||
user: {
|
|
||||||
type: 'object',
|
|
||||||
ref: 'UserLite',
|
|
||||||
optional: false, nullable: false,
|
|
||||||
},
|
|
||||||
reaction: {
|
|
||||||
type: 'string',
|
|
||||||
optional: false, nullable: false,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
required: ['user', 'reaction'],
|
|
||||||
},
|
},
|
||||||
},
|
user: {
|
||||||
users: {
|
|
||||||
type: 'array',
|
|
||||||
optional: true, nullable: true,
|
|
||||||
items: {
|
|
||||||
type: 'object',
|
type: 'object',
|
||||||
ref: 'UserLite',
|
ref: 'UserLite',
|
||||||
optional: false, nullable: false,
|
optional: false, nullable: false,
|
||||||
},
|
},
|
||||||
|
userId: {
|
||||||
|
type: 'string',
|
||||||
|
optional: false, nullable: false,
|
||||||
|
format: 'id',
|
||||||
|
},
|
||||||
|
note: {
|
||||||
|
type: 'object',
|
||||||
|
ref: 'Note',
|
||||||
|
optional: false, nullable: false,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
}, {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
...baseSchema.properties,
|
||||||
|
type: {
|
||||||
|
type: 'string',
|
||||||
|
optional: false, nullable: false,
|
||||||
|
enum: ['mention'],
|
||||||
|
},
|
||||||
|
user: {
|
||||||
|
type: 'object',
|
||||||
|
ref: 'UserLite',
|
||||||
|
optional: false, nullable: false,
|
||||||
|
},
|
||||||
|
userId: {
|
||||||
|
type: 'string',
|
||||||
|
optional: false, nullable: false,
|
||||||
|
format: 'id',
|
||||||
|
},
|
||||||
|
note: {
|
||||||
|
type: 'object',
|
||||||
|
ref: 'Note',
|
||||||
|
optional: false, nullable: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}, {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
...baseSchema.properties,
|
||||||
|
type: {
|
||||||
|
type: 'string',
|
||||||
|
optional: false, nullable: false,
|
||||||
|
enum: ['reply'],
|
||||||
|
},
|
||||||
|
user: {
|
||||||
|
type: 'object',
|
||||||
|
ref: 'UserLite',
|
||||||
|
optional: false, nullable: false,
|
||||||
|
},
|
||||||
|
userId: {
|
||||||
|
type: 'string',
|
||||||
|
optional: false, nullable: false,
|
||||||
|
format: 'id',
|
||||||
|
},
|
||||||
|
note: {
|
||||||
|
type: 'object',
|
||||||
|
ref: 'Note',
|
||||||
|
optional: false, nullable: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}, {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
...baseSchema.properties,
|
||||||
|
type: {
|
||||||
|
type: 'string',
|
||||||
|
optional: false, nullable: false,
|
||||||
|
enum: ['renote'],
|
||||||
|
},
|
||||||
|
user: {
|
||||||
|
type: 'object',
|
||||||
|
ref: 'UserLite',
|
||||||
|
optional: false, nullable: false,
|
||||||
|
},
|
||||||
|
userId: {
|
||||||
|
type: 'string',
|
||||||
|
optional: false, nullable: false,
|
||||||
|
format: 'id',
|
||||||
|
},
|
||||||
|
note: {
|
||||||
|
type: 'object',
|
||||||
|
ref: 'Note',
|
||||||
|
optional: false, nullable: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}, {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
...baseSchema.properties,
|
||||||
|
type: {
|
||||||
|
type: 'string',
|
||||||
|
optional: false, nullable: false,
|
||||||
|
enum: ['quote'],
|
||||||
|
},
|
||||||
|
user: {
|
||||||
|
type: 'object',
|
||||||
|
ref: 'UserLite',
|
||||||
|
optional: false, nullable: false,
|
||||||
|
},
|
||||||
|
userId: {
|
||||||
|
type: 'string',
|
||||||
|
optional: false, nullable: false,
|
||||||
|
format: 'id',
|
||||||
|
},
|
||||||
|
note: {
|
||||||
|
type: 'object',
|
||||||
|
ref: 'Note',
|
||||||
|
optional: false, nullable: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}, {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
...baseSchema.properties,
|
||||||
|
type: {
|
||||||
|
type: 'string',
|
||||||
|
optional: false, nullable: false,
|
||||||
|
enum: ['reaction'],
|
||||||
|
},
|
||||||
|
user: {
|
||||||
|
type: 'object',
|
||||||
|
ref: 'UserLite',
|
||||||
|
optional: false, nullable: false,
|
||||||
|
},
|
||||||
|
userId: {
|
||||||
|
type: 'string',
|
||||||
|
optional: false, nullable: false,
|
||||||
|
format: 'id',
|
||||||
|
},
|
||||||
|
note: {
|
||||||
|
type: 'object',
|
||||||
|
ref: 'Note',
|
||||||
|
optional: false, nullable: false,
|
||||||
|
},
|
||||||
|
reaction: {
|
||||||
|
type: 'string',
|
||||||
|
optional: false, nullable: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}, {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
...baseSchema.properties,
|
||||||
|
type: {
|
||||||
|
type: 'string',
|
||||||
|
optional: false, nullable: false,
|
||||||
|
enum: ['pollEnded'],
|
||||||
|
},
|
||||||
|
user: {
|
||||||
|
type: 'object',
|
||||||
|
ref: 'UserLite',
|
||||||
|
optional: false, nullable: false,
|
||||||
|
},
|
||||||
|
userId: {
|
||||||
|
type: 'string',
|
||||||
|
optional: false, nullable: false,
|
||||||
|
format: 'id',
|
||||||
|
},
|
||||||
|
note: {
|
||||||
|
type: 'object',
|
||||||
|
ref: 'Note',
|
||||||
|
optional: false, nullable: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}, {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
...baseSchema.properties,
|
||||||
|
type: {
|
||||||
|
type: 'string',
|
||||||
|
optional: false, nullable: false,
|
||||||
|
enum: ['follow'],
|
||||||
|
},
|
||||||
|
user: {
|
||||||
|
type: 'object',
|
||||||
|
ref: 'UserLite',
|
||||||
|
optional: false, nullable: false,
|
||||||
|
},
|
||||||
|
userId: {
|
||||||
|
type: 'string',
|
||||||
|
optional: false, nullable: false,
|
||||||
|
format: 'id',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}, {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
...baseSchema.properties,
|
||||||
|
type: {
|
||||||
|
type: 'string',
|
||||||
|
optional: false, nullable: false,
|
||||||
|
enum: ['receiveFollowRequest'],
|
||||||
|
},
|
||||||
|
user: {
|
||||||
|
type: 'object',
|
||||||
|
ref: 'UserLite',
|
||||||
|
optional: false, nullable: false,
|
||||||
|
},
|
||||||
|
userId: {
|
||||||
|
type: 'string',
|
||||||
|
optional: false, nullable: false,
|
||||||
|
format: 'id',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}, {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
...baseSchema.properties,
|
||||||
|
type: {
|
||||||
|
type: 'string',
|
||||||
|
optional: false, nullable: false,
|
||||||
|
enum: ['followRequestAccepted'],
|
||||||
|
},
|
||||||
|
user: {
|
||||||
|
type: 'object',
|
||||||
|
ref: 'UserLite',
|
||||||
|
optional: false, nullable: false,
|
||||||
|
},
|
||||||
|
userId: {
|
||||||
|
type: 'string',
|
||||||
|
optional: false, nullable: false,
|
||||||
|
format: 'id',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}, {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
...baseSchema.properties,
|
||||||
|
type: {
|
||||||
|
type: 'string',
|
||||||
|
optional: false, nullable: false,
|
||||||
|
enum: ['roleAssigned'],
|
||||||
|
},
|
||||||
|
role: {
|
||||||
|
type: 'object',
|
||||||
|
ref: 'Role',
|
||||||
|
optional: false, nullable: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}, {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
...baseSchema.properties,
|
||||||
|
type: {
|
||||||
|
type: 'string',
|
||||||
|
optional: false, nullable: false,
|
||||||
|
enum: ['achievementEarned'],
|
||||||
|
},
|
||||||
|
achievement: {
|
||||||
|
type: 'string',
|
||||||
|
optional: false, nullable: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}, {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
...baseSchema.properties,
|
||||||
|
type: {
|
||||||
|
type: 'string',
|
||||||
|
optional: false, nullable: false,
|
||||||
|
enum: ['app'],
|
||||||
|
},
|
||||||
|
body: {
|
||||||
|
type: 'string',
|
||||||
|
optional: false, nullable: false,
|
||||||
|
},
|
||||||
|
header: {
|
||||||
|
type: 'string',
|
||||||
|
optional: false, nullable: false,
|
||||||
|
},
|
||||||
|
icon: {
|
||||||
|
type: 'string',
|
||||||
|
optional: false, nullable: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}, {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
...baseSchema.properties,
|
||||||
|
type: {
|
||||||
|
type: 'string',
|
||||||
|
optional: false, nullable: false,
|
||||||
|
enum: ['reaction:grouped'],
|
||||||
|
},
|
||||||
|
note: {
|
||||||
|
type: 'object',
|
||||||
|
ref: 'Note',
|
||||||
|
optional: false, nullable: false,
|
||||||
|
},
|
||||||
|
reactions: {
|
||||||
|
type: 'array',
|
||||||
|
optional: false, nullable: false,
|
||||||
|
items: {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
user: {
|
||||||
|
type: 'object',
|
||||||
|
ref: 'UserLite',
|
||||||
|
optional: false, nullable: false,
|
||||||
|
},
|
||||||
|
reaction: {
|
||||||
|
type: 'string',
|
||||||
|
optional: false, nullable: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
required: ['user', 'reaction'],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}, {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
...baseSchema.properties,
|
||||||
|
type: {
|
||||||
|
type: 'string',
|
||||||
|
optional: false, nullable: false,
|
||||||
|
enum: ['renote:grouped'],
|
||||||
|
},
|
||||||
|
note: {
|
||||||
|
type: 'object',
|
||||||
|
ref: 'Note',
|
||||||
|
optional: false, nullable: false,
|
||||||
|
},
|
||||||
|
users: {
|
||||||
|
type: 'array',
|
||||||
|
optional: false, nullable: false,
|
||||||
|
items: {
|
||||||
|
type: 'object',
|
||||||
|
ref: 'UserLite',
|
||||||
|
optional: false, nullable: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}, {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
...baseSchema.properties,
|
||||||
|
type: {
|
||||||
|
type: 'string',
|
||||||
|
optional: false, nullable: false,
|
||||||
|
enum: ['test'],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}],
|
||||||
} as const;
|
} as const;
|
||||||
|
|
|
@ -3,6 +3,107 @@
|
||||||
* SPDX-License-Identifier: AGPL-3.0-only
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
const blockBaseSchema = {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
id: {
|
||||||
|
type: 'string',
|
||||||
|
optional: false, nullable: false,
|
||||||
|
},
|
||||||
|
type: {
|
||||||
|
type: 'string',
|
||||||
|
optional: false, nullable: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
const textBlockSchema = {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
...blockBaseSchema.properties,
|
||||||
|
type: {
|
||||||
|
type: 'string',
|
||||||
|
optional: false, nullable: false,
|
||||||
|
enum: ['text'],
|
||||||
|
},
|
||||||
|
text: {
|
||||||
|
type: 'string',
|
||||||
|
optional: false, nullable: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
const sectionBlockSchema = {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
...blockBaseSchema.properties,
|
||||||
|
type: {
|
||||||
|
type: 'string',
|
||||||
|
optional: false, nullable: false,
|
||||||
|
enum: ['section'],
|
||||||
|
},
|
||||||
|
title: {
|
||||||
|
type: 'string',
|
||||||
|
optional: false, nullable: false,
|
||||||
|
},
|
||||||
|
children: {
|
||||||
|
type: 'array',
|
||||||
|
optional: false, nullable: false,
|
||||||
|
items: {
|
||||||
|
type: 'object',
|
||||||
|
optional: false, nullable: false,
|
||||||
|
ref: 'PageBlock',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
const imageBlockSchema = {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
...blockBaseSchema.properties,
|
||||||
|
type: {
|
||||||
|
type: 'string',
|
||||||
|
optional: false, nullable: false,
|
||||||
|
enum: ['image'],
|
||||||
|
},
|
||||||
|
fileId: {
|
||||||
|
type: 'string',
|
||||||
|
optional: false, nullable: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
const noteBlockSchema = {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
...blockBaseSchema.properties,
|
||||||
|
type: {
|
||||||
|
type: 'string',
|
||||||
|
optional: false, nullable: false,
|
||||||
|
enum: ['note'],
|
||||||
|
},
|
||||||
|
detailed: {
|
||||||
|
type: 'boolean',
|
||||||
|
optional: false, nullable: false,
|
||||||
|
},
|
||||||
|
note: {
|
||||||
|
type: 'string',
|
||||||
|
optional: false, nullable: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
export const packedPageBlockSchema = {
|
||||||
|
type: 'object',
|
||||||
|
oneOf: [
|
||||||
|
textBlockSchema,
|
||||||
|
sectionBlockSchema,
|
||||||
|
imageBlockSchema,
|
||||||
|
noteBlockSchema,
|
||||||
|
],
|
||||||
|
} as const;
|
||||||
|
|
||||||
export const packedPageSchema = {
|
export const packedPageSchema = {
|
||||||
type: 'object',
|
type: 'object',
|
||||||
properties: {
|
properties: {
|
||||||
|
@ -38,6 +139,7 @@ export const packedPageSchema = {
|
||||||
items: {
|
items: {
|
||||||
type: 'object',
|
type: 'object',
|
||||||
optional: false, nullable: false,
|
optional: false, nullable: false,
|
||||||
|
ref: 'PageBlock',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
variables: {
|
variables: {
|
||||||
|
|
|
@ -1,26 +1,127 @@
|
||||||
const rolePolicyValue = {
|
export const packedRolePoliciesSchema = {
|
||||||
type: 'object',
|
type: 'object',
|
||||||
|
optional: false, nullable: false,
|
||||||
properties: {
|
properties: {
|
||||||
value: {
|
gtlAvailable: {
|
||||||
oneOf: [
|
type: 'boolean',
|
||||||
{
|
optional: false, nullable: false,
|
||||||
type: 'integer',
|
|
||||||
optional: false, nullable: false,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
type: 'boolean',
|
|
||||||
optional: false, nullable: false,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
},
|
||||||
priority: {
|
ltlAvailable: {
|
||||||
|
type: 'boolean',
|
||||||
|
optional: false, nullable: false,
|
||||||
|
},
|
||||||
|
canPublicNote: {
|
||||||
|
type: 'boolean',
|
||||||
|
optional: false, nullable: false,
|
||||||
|
},
|
||||||
|
canCreateContent: {
|
||||||
|
type: 'boolean',
|
||||||
|
optional: false, nullable: false,
|
||||||
|
},
|
||||||
|
canUpdateContent: {
|
||||||
|
type: 'boolean',
|
||||||
|
optional: false, nullable: false,
|
||||||
|
},
|
||||||
|
canDeleteContent: {
|
||||||
|
type: 'boolean',
|
||||||
|
optional: false, nullable: false,
|
||||||
|
},
|
||||||
|
canUpdateAvatar: {
|
||||||
|
type: 'boolean',
|
||||||
|
optional: false, nullable: false,
|
||||||
|
},
|
||||||
|
canUpdateBanner: {
|
||||||
|
type: 'boolean',
|
||||||
|
optional: false, nullable: false,
|
||||||
|
},
|
||||||
|
canInvite: {
|
||||||
|
type: 'boolean',
|
||||||
|
optional: false, nullable: false,
|
||||||
|
},
|
||||||
|
inviteLimit: {
|
||||||
type: 'integer',
|
type: 'integer',
|
||||||
optional: false, nullable: false,
|
optional: false, nullable: false,
|
||||||
},
|
},
|
||||||
useDefault: {
|
inviteLimitCycle: {
|
||||||
|
type: 'integer',
|
||||||
|
optional: false, nullable: false,
|
||||||
|
},
|
||||||
|
inviteExpirationTime: {
|
||||||
|
type: 'integer',
|
||||||
|
optional: false, nullable: false,
|
||||||
|
},
|
||||||
|
canManageCustomEmojis: {
|
||||||
type: 'boolean',
|
type: 'boolean',
|
||||||
optional: false, nullable: false,
|
optional: false, nullable: false,
|
||||||
},
|
},
|
||||||
|
canManageAvatarDecorations: {
|
||||||
|
type: 'boolean',
|
||||||
|
optional: false, nullable: false,
|
||||||
|
},
|
||||||
|
canSearchNotes: {
|
||||||
|
type: 'boolean',
|
||||||
|
optional: false, nullable: false,
|
||||||
|
},
|
||||||
|
canUseTranslator: {
|
||||||
|
type: 'boolean',
|
||||||
|
optional: false, nullable: false,
|
||||||
|
},
|
||||||
|
canUseDriveFileInSoundSettings: {
|
||||||
|
type: 'boolean',
|
||||||
|
optional: false, nullable: false,
|
||||||
|
},
|
||||||
|
canHideAds: {
|
||||||
|
type: 'boolean',
|
||||||
|
optional: false, nullable: false,
|
||||||
|
},
|
||||||
|
driveCapacityMb: {
|
||||||
|
type: 'integer',
|
||||||
|
optional: false, nullable: false,
|
||||||
|
},
|
||||||
|
alwaysMarkNsfw: {
|
||||||
|
type: 'boolean',
|
||||||
|
optional: false, nullable: false,
|
||||||
|
},
|
||||||
|
pinLimit: {
|
||||||
|
type: 'integer',
|
||||||
|
optional: false, nullable: false,
|
||||||
|
},
|
||||||
|
antennaLimit: {
|
||||||
|
type: 'integer',
|
||||||
|
optional: false, nullable: false,
|
||||||
|
},
|
||||||
|
wordMuteLimit: {
|
||||||
|
type: 'integer',
|
||||||
|
optional: false, nullable: false,
|
||||||
|
},
|
||||||
|
webhookLimit: {
|
||||||
|
type: 'integer',
|
||||||
|
optional: false, nullable: false,
|
||||||
|
},
|
||||||
|
clipLimit: {
|
||||||
|
type: 'integer',
|
||||||
|
optional: false, nullable: false,
|
||||||
|
},
|
||||||
|
noteEachClipsLimit: {
|
||||||
|
type: 'integer',
|
||||||
|
optional: false, nullable: false,
|
||||||
|
},
|
||||||
|
userListLimit: {
|
||||||
|
type: 'integer',
|
||||||
|
optional: false, nullable: false,
|
||||||
|
},
|
||||||
|
userEachUserListsLimit: {
|
||||||
|
type: 'integer',
|
||||||
|
optional: false, nullable: false,
|
||||||
|
},
|
||||||
|
rateLimitFactor: {
|
||||||
|
type: 'integer',
|
||||||
|
optional: false, nullable: false,
|
||||||
|
},
|
||||||
|
avatarDecorationLimit: {
|
||||||
|
type: 'integer',
|
||||||
|
optional: false, nullable: false,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
|
@ -121,32 +222,28 @@ export const packedRoleSchema = {
|
||||||
policies: {
|
policies: {
|
||||||
type: 'object',
|
type: 'object',
|
||||||
optional: false, nullable: false,
|
optional: false, nullable: false,
|
||||||
properties: {
|
additionalProperties: {
|
||||||
pinLimit: rolePolicyValue,
|
anyOf: [{
|
||||||
canInvite: rolePolicyValue,
|
type: 'object',
|
||||||
clipLimit: rolePolicyValue,
|
properties: {
|
||||||
canHideAds: rolePolicyValue,
|
value: {
|
||||||
inviteLimit: rolePolicyValue,
|
oneOf: [
|
||||||
antennaLimit: rolePolicyValue,
|
{
|
||||||
gtlAvailable: rolePolicyValue,
|
type: 'integer',
|
||||||
ltlAvailable: rolePolicyValue,
|
},
|
||||||
webhookLimit: rolePolicyValue,
|
{
|
||||||
canPublicNote: rolePolicyValue,
|
type: 'boolean',
|
||||||
userListLimit: rolePolicyValue,
|
},
|
||||||
wordMuteLimit: rolePolicyValue,
|
],
|
||||||
alwaysMarkNsfw: rolePolicyValue,
|
},
|
||||||
canSearchNotes: rolePolicyValue,
|
priority: {
|
||||||
driveCapacityMb: rolePolicyValue,
|
type: 'integer',
|
||||||
rateLimitFactor: rolePolicyValue,
|
},
|
||||||
inviteLimitCycle: rolePolicyValue,
|
useDefault: {
|
||||||
noteEachClipsLimit: rolePolicyValue,
|
type: 'boolean',
|
||||||
inviteExpirationTime: rolePolicyValue,
|
},
|
||||||
canManageCustomEmojis: rolePolicyValue,
|
},
|
||||||
userEachUserListsLimit: rolePolicyValue,
|
}],
|
||||||
canManageAvatarDecorations: rolePolicyValue,
|
|
||||||
canUseDriveFileInSoundSettings: rolePolicyValue,
|
|
||||||
canUseTranslator: rolePolicyValue,
|
|
||||||
avatarDecorationLimit: rolePolicyValue,
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
usersCount: {
|
usersCount: {
|
||||||
|
|
|
@ -582,108 +582,7 @@ export const packedMeDetailedOnlySchema = {
|
||||||
policies: {
|
policies: {
|
||||||
type: 'object',
|
type: 'object',
|
||||||
nullable: false, optional: false,
|
nullable: false, optional: false,
|
||||||
properties: {
|
ref: 'RolePolicies',
|
||||||
gtlAvailable: {
|
|
||||||
type: 'boolean',
|
|
||||||
nullable: false, optional: false,
|
|
||||||
},
|
|
||||||
ltlAvailable: {
|
|
||||||
type: 'boolean',
|
|
||||||
nullable: false, optional: false,
|
|
||||||
},
|
|
||||||
canPublicNote: {
|
|
||||||
type: 'boolean',
|
|
||||||
nullable: false, optional: false,
|
|
||||||
},
|
|
||||||
canInvite: {
|
|
||||||
type: 'boolean',
|
|
||||||
nullable: false, optional: false,
|
|
||||||
},
|
|
||||||
inviteLimit: {
|
|
||||||
type: 'number',
|
|
||||||
nullable: false, optional: false,
|
|
||||||
},
|
|
||||||
inviteLimitCycle: {
|
|
||||||
type: 'number',
|
|
||||||
nullable: false, optional: false,
|
|
||||||
},
|
|
||||||
inviteExpirationTime: {
|
|
||||||
type: 'number',
|
|
||||||
nullable: false, optional: false,
|
|
||||||
},
|
|
||||||
canManageCustomEmojis: {
|
|
||||||
type: 'boolean',
|
|
||||||
nullable: false, optional: false,
|
|
||||||
},
|
|
||||||
canManageAvatarDecorations: {
|
|
||||||
type: 'boolean',
|
|
||||||
nullable: false, optional: false,
|
|
||||||
},
|
|
||||||
canSearchNotes: {
|
|
||||||
type: 'boolean',
|
|
||||||
nullable: false, optional: false,
|
|
||||||
},
|
|
||||||
canUseTranslator: {
|
|
||||||
type: 'boolean',
|
|
||||||
nullable: false, optional: false,
|
|
||||||
},
|
|
||||||
canUseDriveFileInSoundSettings: {
|
|
||||||
type: 'boolean',
|
|
||||||
nullable: false, optional: false,
|
|
||||||
},
|
|
||||||
canHideAds: {
|
|
||||||
type: 'boolean',
|
|
||||||
nullable: false, optional: false,
|
|
||||||
},
|
|
||||||
driveCapacityMb: {
|
|
||||||
type: 'number',
|
|
||||||
nullable: false, optional: false,
|
|
||||||
},
|
|
||||||
alwaysMarkNsfw: {
|
|
||||||
type: 'boolean',
|
|
||||||
nullable: false, optional: false,
|
|
||||||
},
|
|
||||||
pinLimit: {
|
|
||||||
type: 'number',
|
|
||||||
nullable: false, optional: false,
|
|
||||||
},
|
|
||||||
antennaLimit: {
|
|
||||||
type: 'number',
|
|
||||||
nullable: false, optional: false,
|
|
||||||
},
|
|
||||||
wordMuteLimit: {
|
|
||||||
type: 'number',
|
|
||||||
nullable: false, optional: false,
|
|
||||||
},
|
|
||||||
webhookLimit: {
|
|
||||||
type: 'number',
|
|
||||||
nullable: false, optional: false,
|
|
||||||
},
|
|
||||||
clipLimit: {
|
|
||||||
type: 'number',
|
|
||||||
nullable: false, optional: false,
|
|
||||||
},
|
|
||||||
noteEachClipsLimit: {
|
|
||||||
type: 'number',
|
|
||||||
nullable: false, optional: false,
|
|
||||||
},
|
|
||||||
userListLimit: {
|
|
||||||
type: 'number',
|
|
||||||
nullable: false, optional: false,
|
|
||||||
},
|
|
||||||
userEachUserListsLimit: {
|
|
||||||
type: 'number',
|
|
||||||
nullable: false, optional: false,
|
|
||||||
},
|
|
||||||
rateLimitFactor: {
|
|
||||||
type: 'number',
|
|
||||||
nullable: false, optional: false,
|
|
||||||
},
|
|
||||||
avatarDecorationLimit: {
|
|
||||||
type: 'number',
|
|
||||||
nullable: false, optional: false,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
//#region secrets
|
//#region secrets
|
||||||
email: {
|
email: {
|
||||||
|
|
|
@ -286,9 +286,9 @@ export class QueueProcessorService implements OnApplicationShutdown {
|
||||||
}, {
|
}, {
|
||||||
...baseWorkerOptions(this.config.redisForRelationshipQueue, this.config.bullmqWorkerOptions, QUEUE.RELATIONSHIP),
|
...baseWorkerOptions(this.config.redisForRelationshipQueue, this.config.bullmqWorkerOptions, QUEUE.RELATIONSHIP),
|
||||||
autorun: false,
|
autorun: false,
|
||||||
concurrency: this.config.relashionshipJobConcurrency ?? 16,
|
concurrency: this.config.relationshipJobConcurrency ?? 16,
|
||||||
limiter: {
|
limiter: {
|
||||||
max: this.config.relashionshipJobPerSec ?? 64,
|
max: this.config.relationshipJobPerSec ?? 64,
|
||||||
duration: 1000,
|
duration: 1000,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
|
@ -4,8 +4,7 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { permissions } from 'misskey-js';
|
import { permissions } from 'misskey-js';
|
||||||
import type { Schema } from '@/misc/json-schema.js';
|
import type { KeyOf, Schema } from '@/misc/json-schema.js';
|
||||||
import { RolePolicies } from '@/core/RoleService.js';
|
|
||||||
|
|
||||||
import * as ep___admin_meta from './endpoints/admin/meta.js';
|
import * as ep___admin_meta from './endpoints/admin/meta.js';
|
||||||
import * as ep___admin_abuseUserReports from './endpoints/admin/abuse-user-reports.js';
|
import * as ep___admin_abuseUserReports from './endpoints/admin/abuse-user-reports.js';
|
||||||
|
@ -788,7 +787,7 @@ interface IEndpointMetaBase {
|
||||||
*/
|
*/
|
||||||
readonly requireAdmin?: boolean;
|
readonly requireAdmin?: boolean;
|
||||||
|
|
||||||
readonly requireRolePolicy?: keyof RolePolicies;
|
readonly requireRolePolicy?: KeyOf<'RolePolicies'>;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 引っ越し済みのユーザーによるリクエストを禁止するか
|
* 引っ越し済みのユーザーによるリクエストを禁止するか
|
||||||
|
|
|
@ -304,6 +304,11 @@ export const meta = {
|
||||||
type: 'string',
|
type: 'string',
|
||||||
optional: false, nullable: true,
|
optional: false, nullable: true,
|
||||||
},
|
},
|
||||||
|
policies: {
|
||||||
|
type: 'object',
|
||||||
|
optional: false, nullable: false,
|
||||||
|
ref: 'RolePolicies',
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
} as const;
|
} as const;
|
||||||
|
|
|
@ -14,6 +14,32 @@ export const meta = {
|
||||||
requireCredential: false,
|
requireCredential: false,
|
||||||
|
|
||||||
res: {
|
res: {
|
||||||
|
type: 'array',
|
||||||
|
items: {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
createdAt: {
|
||||||
|
type: 'string',
|
||||||
|
format: 'date-time',
|
||||||
|
},
|
||||||
|
users: {
|
||||||
|
type: 'number',
|
||||||
|
},
|
||||||
|
data: {
|
||||||
|
type: 'object',
|
||||||
|
additionalProperties: {
|
||||||
|
anyOf: [{
|
||||||
|
type: 'number',
|
||||||
|
}],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
required: [
|
||||||
|
'createdAt',
|
||||||
|
'users',
|
||||||
|
'data',
|
||||||
|
],
|
||||||
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
allowGet: true,
|
allowGet: true,
|
||||||
|
|
|
@ -16,3 +16,8 @@ declare const _DATA_TRANSFER_DECK_COLUMN_: string;
|
||||||
|
|
||||||
// for dev-mode
|
// for dev-mode
|
||||||
declare const _LANGS_FULL_: string[][];
|
declare const _LANGS_FULL_: string[][];
|
||||||
|
|
||||||
|
// TagCanvas
|
||||||
|
interface Window {
|
||||||
|
TagCanvas: any;
|
||||||
|
}
|
||||||
|
|
|
@ -20,7 +20,7 @@
|
||||||
"@discordapp/twemoji": "15.0.2",
|
"@discordapp/twemoji": "15.0.2",
|
||||||
"@github/webauthn-json": "2.1.1",
|
"@github/webauthn-json": "2.1.1",
|
||||||
"@mcaptcha/vanilla-glue": "0.1.0-alpha-3",
|
"@mcaptcha/vanilla-glue": "0.1.0-alpha-3",
|
||||||
"@misskey-dev/browser-image-resizer": "2.2.1-misskey.10",
|
"@misskey-dev/browser-image-resizer": "2024.1.0",
|
||||||
"@rollup/plugin-json": "6.1.0",
|
"@rollup/plugin-json": "6.1.0",
|
||||||
"@rollup/plugin-replace": "5.0.5",
|
"@rollup/plugin-replace": "5.0.5",
|
||||||
"@rollup/plugin-typescript": "11.1.6",
|
"@rollup/plugin-typescript": "11.1.6",
|
||||||
|
@ -40,7 +40,7 @@
|
||||||
"chartjs-chart-matrix": "2.0.1",
|
"chartjs-chart-matrix": "2.0.1",
|
||||||
"chartjs-plugin-gradient": "0.6.1",
|
"chartjs-plugin-gradient": "0.6.1",
|
||||||
"chartjs-plugin-zoom": "2.0.1",
|
"chartjs-plugin-zoom": "2.0.1",
|
||||||
"chromatic": "10.5.0",
|
"chromatic": "10.6.1",
|
||||||
"compare-versions": "6.1.0",
|
"compare-versions": "6.1.0",
|
||||||
"cropperjs": "2.0.0-beta.4",
|
"cropperjs": "2.0.0-beta.4",
|
||||||
"date-fns": "2.30.0",
|
"date-fns": "2.30.0",
|
||||||
|
@ -77,8 +77,8 @@
|
||||||
"vuedraggable": "next"
|
"vuedraggable": "next"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@misskey-dev/eslint-plugin": "^1.0.0",
|
"@misskey-dev/eslint-plugin": "1.0.0",
|
||||||
"@misskey-dev/summaly": "^5.0.3",
|
"@misskey-dev/summaly": "5.0.3",
|
||||||
"@storybook/addon-actions": "7.6.10",
|
"@storybook/addon-actions": "7.6.10",
|
||||||
"@storybook/addon-essentials": "7.6.10",
|
"@storybook/addon-essentials": "7.6.10",
|
||||||
"@storybook/addon-interactions": "7.6.10",
|
"@storybook/addon-interactions": "7.6.10",
|
||||||
|
@ -102,12 +102,12 @@
|
||||||
"@types/estree": "1.0.5",
|
"@types/estree": "1.0.5",
|
||||||
"@types/matter-js": "0.19.6",
|
"@types/matter-js": "0.19.6",
|
||||||
"@types/micromatch": "4.0.6",
|
"@types/micromatch": "4.0.6",
|
||||||
"@types/node": "20.11.6",
|
"@types/node": "20.11.10",
|
||||||
"@types/punycode": "2.1.3",
|
"@types/punycode": "2.1.3",
|
||||||
"@types/sanitize-html": "2.9.5",
|
"@types/sanitize-html": "2.9.5",
|
||||||
"@types/throttle-debounce": "5.0.2",
|
"@types/throttle-debounce": "5.0.2",
|
||||||
"@types/tinycolor2": "1.4.6",
|
"@types/tinycolor2": "1.4.6",
|
||||||
"@types/uuid": "9.0.7",
|
"@types/uuid": "9.0.8",
|
||||||
"@types/ws": "8.5.10",
|
"@types/ws": "8.5.10",
|
||||||
"@typescript-eslint/eslint-plugin": "6.18.1",
|
"@typescript-eslint/eslint-plugin": "6.18.1",
|
||||||
"@typescript-eslint/parser": "6.18.1",
|
"@typescript-eslint/parser": "6.18.1",
|
||||||
|
@ -118,12 +118,12 @@
|
||||||
"cypress": "13.6.3",
|
"cypress": "13.6.3",
|
||||||
"eslint": "8.56.0",
|
"eslint": "8.56.0",
|
||||||
"eslint-plugin-import": "2.29.1",
|
"eslint-plugin-import": "2.29.1",
|
||||||
"eslint-plugin-vue": "9.20.1",
|
"eslint-plugin-vue": "9.21.0",
|
||||||
"fast-glob": "3.3.2",
|
"fast-glob": "3.3.2",
|
||||||
"happy-dom": "10.0.3",
|
"happy-dom": "10.0.3",
|
||||||
"intersection-observer": "0.12.2",
|
"intersection-observer": "0.12.2",
|
||||||
"micromatch": "4.0.5",
|
"micromatch": "4.0.5",
|
||||||
"msw": "2.1.4",
|
"msw": "2.1.5",
|
||||||
"msw-storybook-addon": "1.10.0",
|
"msw-storybook-addon": "1.10.0",
|
||||||
"nodemon": "3.0.3",
|
"nodemon": "3.0.3",
|
||||||
"prettier": "3.2.4",
|
"prettier": "3.2.4",
|
||||||
|
|
|
@ -22,7 +22,7 @@ import { getAccountFromId } from '@/scripts/get-account-from-id.js';
|
||||||
import { deckStore } from '@/ui/deck/deck-store.js';
|
import { deckStore } from '@/ui/deck/deck-store.js';
|
||||||
import { miLocalStorage } from '@/local-storage.js';
|
import { miLocalStorage } from '@/local-storage.js';
|
||||||
import { fetchCustomEmojis } from '@/custom-emojis.js';
|
import { fetchCustomEmojis } from '@/custom-emojis.js';
|
||||||
import { setupRouter } from '@/global/router/definition.js';
|
import { setupRouter } from '@/router/definition.js';
|
||||||
|
|
||||||
export async function common(createVue: () => App<Element>) {
|
export async function common(createVue: () => App<Element>) {
|
||||||
console.info(`Misskey v${version}`);
|
console.info(`Misskey v${version}`);
|
||||||
|
|
|
@ -19,7 +19,7 @@ import { claimAchievement, claimedAchievements } from '@/scripts/achievements.js
|
||||||
import { initializeSw } from '@/scripts/initialize-sw.js';
|
import { initializeSw } from '@/scripts/initialize-sw.js';
|
||||||
import { deckStore } from '@/ui/deck/deck-store.js';
|
import { deckStore } from '@/ui/deck/deck-store.js';
|
||||||
import { emojiPicker } from '@/scripts/emoji-picker.js';
|
import { emojiPicker } from '@/scripts/emoji-picker.js';
|
||||||
import { mainRouter } from '@/global/router/main.js';
|
import { mainRouter } from '@/router/main.js';
|
||||||
|
|
||||||
export async function mainBoot() {
|
export async function mainBoot() {
|
||||||
const { isClientUpdated } = await common(() => createApp(
|
const { isClientUpdated } = await common(() => createApp(
|
||||||
|
|
|
@ -76,7 +76,7 @@ import { i18n } from '@/i18n.js';
|
||||||
import { misskeyApi } from '@/scripts/misskey-api.js';
|
import { misskeyApi } from '@/scripts/misskey-api.js';
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
user: Misskey.entities.User;
|
user: Misskey.entities.UserDetailed;
|
||||||
initialComment?: string;
|
initialComment?: string;
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
|
|
|
@ -6,7 +6,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
<template>
|
<template>
|
||||||
<div>
|
<div>
|
||||||
<div v-if="achievements" :class="$style.root">
|
<div v-if="achievements" :class="$style.root">
|
||||||
<div v-for="achievement in achievements" :key="achievement" :class="$style.achievement" class="_panel">
|
<div v-for="achievement in achievements" :key="achievement.name" :class="$style.achievement" class="_panel">
|
||||||
<div :class="$style.icon">
|
<div :class="$style.icon">
|
||||||
<div
|
<div
|
||||||
:class="[$style.iconFrame, {
|
:class="[$style.iconFrame, {
|
||||||
|
|
|
@ -55,7 +55,7 @@ async function gotIt(): Promise<void> {
|
||||||
if (confirm.canceled) return;
|
if (confirm.canceled) return;
|
||||||
}
|
}
|
||||||
|
|
||||||
modal.value.close();
|
modal.value?.close();
|
||||||
misskeyApi('i/read-announcement', { announcementId: props.announcement.id });
|
misskeyApi('i/read-announcement', { announcementId: props.announcement.id });
|
||||||
updateAccount({
|
updateAccount({
|
||||||
unreadAnnouncements: $i!.unreadAnnouncements.filter(a => a.id !== props.announcement.id),
|
unreadAnnouncements: $i!.unreadAnnouncements.filter(a => a.id !== props.announcement.id),
|
||||||
|
@ -63,7 +63,7 @@ async function gotIt(): Promise<void> {
|
||||||
}
|
}
|
||||||
|
|
||||||
function onBgClick() {
|
function onBgClick() {
|
||||||
rootEl.value.animate([{
|
rootEl.value?.animate([{
|
||||||
offset: 0,
|
offset: 0,
|
||||||
transform: 'scale(1)',
|
transform: 'scale(1)',
|
||||||
}, {
|
}, {
|
||||||
|
|
|
@ -10,8 +10,8 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
<MkAsUi v-if="!g(child).hidden" :component="g(child)" :components="props.components" :size="size"/>
|
<MkAsUi v-if="!g(child).hidden" :component="g(child)" :components="props.components" :size="size"/>
|
||||||
</template>
|
</template>
|
||||||
</div>
|
</div>
|
||||||
<span v-else-if="c.type === 'text'" :class="{ [$style.fontSerif]: c.font === 'serif', [$style.fontMonospace]: c.font === 'monospace' }" :style="{ fontSize: c.size ? `${c.size * 100}%` : null, fontWeight: c.bold ? 'bold' : null, color: c.color ?? null }">{{ c.text }}</span>
|
<span v-else-if="c.type === 'text'" :class="{ [$style.fontSerif]: c.font === 'serif', [$style.fontMonospace]: c.font === 'monospace' }" :style="{ fontSize: c.size ? `${c.size * 100}%` : undefined, fontWeight: c.bold ? 'bold' : undefined, color: c.color }">{{ c.text }}</span>
|
||||||
<Mfm v-else-if="c.type === 'mfm'" :class="{ [$style.fontSerif]: c.font === 'serif', [$style.fontMonospace]: c.font === 'monospace' }" :style="{ fontSize: c.size ? `${c.size * 100}%` : null, fontWeight: c.bold ? 'bold' : null, color: c.color ?? null }" :text="c.text" @clickEv="c.onClickEv"/>
|
<Mfm v-else-if="c.type === 'mfm'" :class="{ [$style.fontSerif]: c.font === 'serif', [$style.fontMonospace]: c.font === 'monospace' }" :style="{ fontSize: c.size ? `${c.size * 100}%` : null, fontWeight: c.bold ? 'bold' : null, color: c.color ?? null }" :text="c.text ?? ''" @clickEv="c.onClickEv"/>
|
||||||
<MkButton v-else-if="c.type === 'button'" :primary="c.primary" :rounded="c.rounded" :disabled="c.disabled" :small="size === 'small'" inline @click="c.onClick">{{ c.text }}</MkButton>
|
<MkButton v-else-if="c.type === 'button'" :primary="c.primary" :rounded="c.rounded" :disabled="c.disabled" :small="size === 'small'" inline @click="c.onClick">{{ c.text }}</MkButton>
|
||||||
<div v-else-if="c.type === 'buttons'" class="_buttons" :style="{ justifyContent: align }">
|
<div v-else-if="c.type === 'buttons'" class="_buttons" :style="{ justifyContent: align }">
|
||||||
<MkButton v-for="button in c.buttons" :primary="button.primary" :rounded="button.rounded" :disabled="button.disabled" inline :small="size === 'small'" @click="button.onClick">{{ button.text }}</MkButton>
|
<MkButton v-for="button in c.buttons" :primary="button.primary" :rounded="button.rounded" :disabled="button.disabled" inline :small="size === 'small'" @click="button.onClick">{{ button.text }}</MkButton>
|
||||||
|
@ -20,19 +20,19 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
<template v-if="c.label" #label>{{ c.label }}</template>
|
<template v-if="c.label" #label>{{ c.label }}</template>
|
||||||
<template v-if="c.caption" #caption>{{ c.caption }}</template>
|
<template v-if="c.caption" #caption>{{ c.caption }}</template>
|
||||||
</MkSwitch>
|
</MkSwitch>
|
||||||
<MkTextarea v-else-if="c.type === 'textarea'" :modelValue="c.default" @update:modelValue="c.onInput">
|
<MkTextarea v-else-if="c.type === 'textarea'" :modelValue="c.default ?? null" @update:modelValue="c.onInput">
|
||||||
<template v-if="c.label" #label>{{ c.label }}</template>
|
<template v-if="c.label" #label>{{ c.label }}</template>
|
||||||
<template v-if="c.caption" #caption>{{ c.caption }}</template>
|
<template v-if="c.caption" #caption>{{ c.caption }}</template>
|
||||||
</MkTextarea>
|
</MkTextarea>
|
||||||
<MkInput v-else-if="c.type === 'textInput'" :small="size === 'small'" :modelValue="c.default" @update:modelValue="c.onInput">
|
<MkInput v-else-if="c.type === 'textInput'" :small="size === 'small'" :modelValue="c.default ?? null" @update:modelValue="c.onInput">
|
||||||
<template v-if="c.label" #label>{{ c.label }}</template>
|
<template v-if="c.label" #label>{{ c.label }}</template>
|
||||||
<template v-if="c.caption" #caption>{{ c.caption }}</template>
|
<template v-if="c.caption" #caption>{{ c.caption }}</template>
|
||||||
</MkInput>
|
</MkInput>
|
||||||
<MkInput v-else-if="c.type === 'numberInput'" :small="size === 'small'" :modelValue="c.default" type="number" @update:modelValue="c.onInput">
|
<MkInput v-else-if="c.type === 'numberInput'" :small="size === 'small'" :modelValue="c.default ?? null" type="number" @update:modelValue="c.onInput">
|
||||||
<template v-if="c.label" #label>{{ c.label }}</template>
|
<template v-if="c.label" #label>{{ c.label }}</template>
|
||||||
<template v-if="c.caption" #caption>{{ c.caption }}</template>
|
<template v-if="c.caption" #caption>{{ c.caption }}</template>
|
||||||
</MkInput>
|
</MkInput>
|
||||||
<MkSelect v-else-if="c.type === 'select'" :small="size === 'small'" :modelValue="c.default" @update:modelValue="c.onChange">
|
<MkSelect v-else-if="c.type === 'select'" :small="size === 'small'" :modelValue="c.default ?? null" @update:modelValue="c.onChange">
|
||||||
<template v-if="c.label" #label>{{ c.label }}</template>
|
<template v-if="c.label" #label>{{ c.label }}</template>
|
||||||
<template v-if="c.caption" #caption>{{ c.caption }}</template>
|
<template v-if="c.caption" #caption>{{ c.caption }}</template>
|
||||||
<option v-for="item in c.items" :key="item.value" :value="item.value">{{ item.text }}</option>
|
<option v-for="item in c.items" :key="item.value" :value="item.value">{{ item.text }}</option>
|
||||||
|
@ -42,8 +42,8 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
<MkPostForm
|
<MkPostForm
|
||||||
fixed
|
fixed
|
||||||
:instant="true"
|
:instant="true"
|
||||||
:initialText="c.form.text"
|
:initialText="c.form?.text"
|
||||||
:initialCw="c.form.cw"
|
:initialCw="c.form?.cw"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<MkFolder v-else-if="c.type === 'folder'" :defaultOpen="c.opened">
|
<MkFolder v-else-if="c.type === 'folder'" :defaultOpen="c.opened">
|
||||||
|
@ -52,7 +52,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
<MkAsUi v-if="!g(child).hidden" :component="g(child)" :components="props.components" :size="size"/>
|
<MkAsUi v-if="!g(child).hidden" :component="g(child)" :components="props.components" :size="size"/>
|
||||||
</template>
|
</template>
|
||||||
</MkFolder>
|
</MkFolder>
|
||||||
<div v-else-if="c.type === 'container'" :class="[$style.container, { [$style.fontSerif]: c.font === 'serif', [$style.fontMonospace]: c.font === 'monospace' }]" :style="{ textAlign: c.align ?? null, backgroundColor: c.bgColor ?? null, color: c.fgColor ?? null, borderWidth: c.borderWidth ? `${c.borderWidth}px` : 0, borderColor: c.borderColor ?? 'var(--divider)', padding: c.padding ? `${c.padding}px` : 0, borderRadius: c.rounded ? '8px' : 0 }">
|
<div v-else-if="c.type === 'container'" :class="[$style.container, { [$style.fontSerif]: c.font === 'serif', [$style.fontMonospace]: c.font === 'monospace' }]" :style="{ textAlign: c.align, backgroundColor: c.bgColor, color: c.fgColor, borderWidth: c.borderWidth ? `${c.borderWidth}px` : 0, borderColor: c.borderColor ?? 'var(--divider)', padding: c.padding ? `${c.padding}px` : 0, borderRadius: c.rounded ? '8px' : 0 }">
|
||||||
<template v-for="child in c.children" :key="child">
|
<template v-for="child in c.children" :key="child">
|
||||||
<MkAsUi v-if="!g(child).hidden" :component="g(child)" :components="props.components" :size="size" :align="c.align"/>
|
<MkAsUi v-if="!g(child).hidden" :component="g(child)" :components="props.components" :size="size" :align="c.align"/>
|
||||||
</template>
|
</template>
|
||||||
|
@ -68,7 +68,7 @@ import MkInput from '@/components/MkInput.vue';
|
||||||
import MkSwitch from '@/components/MkSwitch.vue';
|
import MkSwitch from '@/components/MkSwitch.vue';
|
||||||
import MkTextarea from '@/components/MkTextarea.vue';
|
import MkTextarea from '@/components/MkTextarea.vue';
|
||||||
import MkSelect from '@/components/MkSelect.vue';
|
import MkSelect from '@/components/MkSelect.vue';
|
||||||
import { AsUiComponent } from '@/scripts/aiscript/ui.js';
|
import { AsUiComponent, AsUiRoot, AsUiPostFormButton } from '@/scripts/aiscript/ui.js';
|
||||||
import MkFolder from '@/components/MkFolder.vue';
|
import MkFolder from '@/components/MkFolder.vue';
|
||||||
import MkPostForm from '@/components/MkPostForm.vue';
|
import MkPostForm from '@/components/MkPostForm.vue';
|
||||||
|
|
||||||
|
@ -85,20 +85,32 @@ const props = withDefaults(defineProps<{
|
||||||
const c = props.component;
|
const c = props.component;
|
||||||
|
|
||||||
function g(id) {
|
function g(id) {
|
||||||
return props.components.find(x => x.value.id === id).value;
|
const v = props.components.find(x => x.value.id === id)?.value;
|
||||||
|
if (v) return v;
|
||||||
|
|
||||||
|
return {
|
||||||
|
id: 'dummy',
|
||||||
|
type: 'root',
|
||||||
|
children: [],
|
||||||
|
} as AsUiRoot;
|
||||||
}
|
}
|
||||||
|
|
||||||
const valueForSwitch = ref(c.default ?? false);
|
const valueForSwitch = ref('default' in c && typeof c.default === 'boolean' ? c.default : false);
|
||||||
|
|
||||||
function onSwitchUpdate(v) {
|
function onSwitchUpdate(v) {
|
||||||
valueForSwitch.value = v;
|
valueForSwitch.value = v;
|
||||||
if (c.onChange) c.onChange(v);
|
if ('onChange' in c && c.onChange) {
|
||||||
|
c.onChange(v as never);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function openPostForm() {
|
function openPostForm() {
|
||||||
|
const form = (c as AsUiPostFormButton).form;
|
||||||
|
if (!form) return;
|
||||||
|
|
||||||
os.post({
|
os.post({
|
||||||
initialText: c.form.text,
|
initialText: form.text,
|
||||||
initialCw: c.form.cw,
|
initialCw: form.cw,
|
||||||
instant: true,
|
instant: true,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
@ -22,7 +22,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
<MkA
|
<MkA
|
||||||
v-else class="_button"
|
v-else class="_button"
|
||||||
:class="[$style.root, { [$style.inline]: inline, [$style.primary]: primary, [$style.gradate]: gradate, [$style.danger]: danger, [$style.rounded]: rounded, [$style.full]: full, [$style.small]: small, [$style.large]: large, [$style.transparent]: transparent, [$style.asLike]: asLike }]"
|
:class="[$style.root, { [$style.inline]: inline, [$style.primary]: primary, [$style.gradate]: gradate, [$style.danger]: danger, [$style.rounded]: rounded, [$style.full]: full, [$style.small]: small, [$style.large]: large, [$style.transparent]: transparent, [$style.asLike]: asLike }]"
|
||||||
:to="to"
|
:to="to ?? '#'"
|
||||||
@mousedown="onMousedown"
|
@mousedown="onMousedown"
|
||||||
>
|
>
|
||||||
<div ref="ripples" :class="$style.ripples" :data-children-class="$style.ripple"></div>
|
<div ref="ripples" :class="$style.ripples" :data-children-class="$style.ripple"></div>
|
||||||
|
|
|
@ -5,7 +5,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div>
|
<div>
|
||||||
<span v-if="!available">{{ i18n.ts.waiting }}<MkEllipsis/></span>
|
<span v-if="!available">Loading<MkEllipsis/></span>
|
||||||
<div v-if="props.provider == 'mcaptcha'">
|
<div v-if="props.provider == 'mcaptcha'">
|
||||||
<div id="mcaptcha__widget-container" class="m-captcha-style"></div>
|
<div id="mcaptcha__widget-container" class="m-captcha-style"></div>
|
||||||
<div ref="captchaEl"></div>
|
<div ref="captchaEl"></div>
|
||||||
|
@ -17,7 +17,6 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { ref, shallowRef, computed, onMounted, onBeforeUnmount, watch, onUnmounted } from 'vue';
|
import { ref, shallowRef, computed, onMounted, onBeforeUnmount, watch, onUnmounted } from 'vue';
|
||||||
import { defaultStore } from '@/store.js';
|
import { defaultStore } from '@/store.js';
|
||||||
import { i18n } from '@/i18n.js';
|
|
||||||
|
|
||||||
// APIs provided by Captcha services
|
// APIs provided by Captcha services
|
||||||
export type Captcha = {
|
export type Captcha = {
|
||||||
|
|
|
@ -21,13 +21,13 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
*/
|
*/
|
||||||
import { onMounted, ref, shallowRef, watch, PropType } from 'vue';
|
import { onMounted, ref, shallowRef, watch, PropType } from 'vue';
|
||||||
import { Chart } from 'chart.js';
|
import { Chart } from 'chart.js';
|
||||||
import gradient from 'chartjs-plugin-gradient';
|
|
||||||
import { misskeyApiGet } from '@/scripts/misskey-api.js';
|
import { misskeyApiGet } from '@/scripts/misskey-api.js';
|
||||||
import { defaultStore } from '@/store.js';
|
import { defaultStore } from '@/store.js';
|
||||||
import { useChartTooltip } from '@/scripts/use-chart-tooltip.js';
|
import { useChartTooltip } from '@/scripts/use-chart-tooltip.js';
|
||||||
import { chartVLine } from '@/scripts/chart-vline.js';
|
import { chartVLine } from '@/scripts/chart-vline.js';
|
||||||
import { alpha } from '@/scripts/color.js';
|
import { alpha } from '@/scripts/color.js';
|
||||||
import date from '@/filters/date.js';
|
import date from '@/filters/date.js';
|
||||||
|
import bytes from '@/filters/bytes.js';
|
||||||
import { initChart } from '@/scripts/init-chart.js';
|
import { initChart } from '@/scripts/init-chart.js';
|
||||||
import { chartLegend } from '@/scripts/chart-legend.js';
|
import { chartLegend } from '@/scripts/chart-legend.js';
|
||||||
import MkChartLegend from '@/components/MkChartLegend.vue';
|
import MkChartLegend from '@/components/MkChartLegend.vue';
|
||||||
|
@ -95,7 +95,7 @@ const getColor = (i) => {
|
||||||
};
|
};
|
||||||
|
|
||||||
const now = new Date();
|
const now = new Date();
|
||||||
let chartInstance: Chart = null;
|
let chartInstance: Chart | null = null;
|
||||||
let chartData: {
|
let chartData: {
|
||||||
series: {
|
series: {
|
||||||
name: string;
|
name: string;
|
||||||
|
@ -108,9 +108,10 @@ let chartData: {
|
||||||
y: number;
|
y: number;
|
||||||
}[];
|
}[];
|
||||||
}[];
|
}[];
|
||||||
} = null;
|
bytes?: boolean;
|
||||||
|
} | null = null;
|
||||||
|
|
||||||
const chartEl = shallowRef<HTMLCanvasElement>(null);
|
const chartEl = shallowRef<HTMLCanvasElement | null>(null);
|
||||||
const fetching = ref(true);
|
const fetching = ref(true);
|
||||||
|
|
||||||
const getDate = (ago: number) => {
|
const getDate = (ago: number) => {
|
||||||
|
@ -132,6 +133,7 @@ const format = (arr) => {
|
||||||
const { handler: externalTooltipHandler } = useChartTooltip();
|
const { handler: externalTooltipHandler } = useChartTooltip();
|
||||||
|
|
||||||
const render = () => {
|
const render = () => {
|
||||||
|
if (chartData == null || chartEl.value == null) return;
|
||||||
if (chartInstance) {
|
if (chartInstance) {
|
||||||
chartInstance.destroy();
|
chartInstance.destroy();
|
||||||
}
|
}
|
||||||
|
@ -188,7 +190,6 @@ const render = () => {
|
||||||
stacked: props.stacked,
|
stacked: props.stacked,
|
||||||
offset: false,
|
offset: false,
|
||||||
time: {
|
time: {
|
||||||
stepSize: 1,
|
|
||||||
unit: props.span === 'day' ? 'month' : 'day',
|
unit: props.span === 'day' ? 'month' : 'day',
|
||||||
displayFormats: {
|
displayFormats: {
|
||||||
day: 'M/d',
|
day: 'M/d',
|
||||||
|
@ -198,6 +199,7 @@ const render = () => {
|
||||||
grid: {
|
grid: {
|
||||||
},
|
},
|
||||||
ticks: {
|
ticks: {
|
||||||
|
stepSize: 1,
|
||||||
display: props.detailed,
|
display: props.detailed,
|
||||||
maxRotation: 0,
|
maxRotation: 0,
|
||||||
autoSkipPadding: 16,
|
autoSkipPadding: 16,
|
||||||
|
@ -237,6 +239,9 @@ const render = () => {
|
||||||
duration: 0,
|
duration: 0,
|
||||||
},
|
},
|
||||||
external: externalTooltipHandler,
|
external: externalTooltipHandler,
|
||||||
|
callbacks: {
|
||||||
|
label: (item) => chartData?.bytes ? bytes(item.parsed.y * 1000, 1) : item.parsed.y.toString(),
|
||||||
|
},
|
||||||
},
|
},
|
||||||
zoom: props.detailed ? {
|
zoom: props.detailed ? {
|
||||||
pan: {
|
pan: {
|
||||||
|
@ -265,10 +270,9 @@ const render = () => {
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
} : undefined,
|
} : undefined,
|
||||||
gradient,
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
plugins: [chartVLine(vLineColor), ...(props.detailed ? [chartLegend(legendEl.value)] : [])],
|
plugins: [chartVLine(vLineColor), ...(props.detailed && legendEl.value ? [chartLegend(legendEl.value)] : [])],
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -566,7 +570,7 @@ const fetchDriveFilesChart = async (): Promise<typeof chartData> => {
|
||||||
};
|
};
|
||||||
|
|
||||||
const fetchInstanceRequestsChart = async (): Promise<typeof chartData> => {
|
const fetchInstanceRequestsChart = async (): Promise<typeof chartData> => {
|
||||||
const raw = await misskeyApiGet('charts/instance', { host: props.args.host, limit: props.limit, span: props.span });
|
const raw = await misskeyApiGet('charts/instance', { host: props.args?.host, limit: props.limit, span: props.span });
|
||||||
return {
|
return {
|
||||||
series: [{
|
series: [{
|
||||||
name: 'In',
|
name: 'In',
|
||||||
|
@ -588,7 +592,7 @@ const fetchInstanceRequestsChart = async (): Promise<typeof chartData> => {
|
||||||
};
|
};
|
||||||
|
|
||||||
const fetchInstanceUsersChart = async (total: boolean): Promise<typeof chartData> => {
|
const fetchInstanceUsersChart = async (total: boolean): Promise<typeof chartData> => {
|
||||||
const raw = await misskeyApiGet('charts/instance', { host: props.args.host, limit: props.limit, span: props.span });
|
const raw = await misskeyApiGet('charts/instance', { host: props.args?.host, limit: props.limit, span: props.span });
|
||||||
return {
|
return {
|
||||||
series: [{
|
series: [{
|
||||||
name: 'Users',
|
name: 'Users',
|
||||||
|
@ -603,7 +607,7 @@ const fetchInstanceUsersChart = async (total: boolean): Promise<typeof chartData
|
||||||
};
|
};
|
||||||
|
|
||||||
const fetchInstanceNotesChart = async (total: boolean): Promise<typeof chartData> => {
|
const fetchInstanceNotesChart = async (total: boolean): Promise<typeof chartData> => {
|
||||||
const raw = await misskeyApiGet('charts/instance', { host: props.args.host, limit: props.limit, span: props.span });
|
const raw = await misskeyApiGet('charts/instance', { host: props.args?.host, limit: props.limit, span: props.span });
|
||||||
return {
|
return {
|
||||||
series: [{
|
series: [{
|
||||||
name: 'Notes',
|
name: 'Notes',
|
||||||
|
@ -618,7 +622,7 @@ const fetchInstanceNotesChart = async (total: boolean): Promise<typeof chartData
|
||||||
};
|
};
|
||||||
|
|
||||||
const fetchInstanceFfChart = async (total: boolean): Promise<typeof chartData> => {
|
const fetchInstanceFfChart = async (total: boolean): Promise<typeof chartData> => {
|
||||||
const raw = await misskeyApiGet('charts/instance', { host: props.args.host, limit: props.limit, span: props.span });
|
const raw = await misskeyApiGet('charts/instance', { host: props.args?.host, limit: props.limit, span: props.span });
|
||||||
return {
|
return {
|
||||||
series: [{
|
series: [{
|
||||||
name: 'Following',
|
name: 'Following',
|
||||||
|
@ -641,7 +645,7 @@ const fetchInstanceFfChart = async (total: boolean): Promise<typeof chartData> =
|
||||||
};
|
};
|
||||||
|
|
||||||
const fetchInstanceDriveUsageChart = async (total: boolean): Promise<typeof chartData> => {
|
const fetchInstanceDriveUsageChart = async (total: boolean): Promise<typeof chartData> => {
|
||||||
const raw = await misskeyApiGet('charts/instance', { host: props.args.host, limit: props.limit, span: props.span });
|
const raw = await misskeyApiGet('charts/instance', { host: props.args?.host, limit: props.limit, span: props.span });
|
||||||
return {
|
return {
|
||||||
bytes: true,
|
bytes: true,
|
||||||
series: [{
|
series: [{
|
||||||
|
@ -649,7 +653,7 @@ const fetchInstanceDriveUsageChart = async (total: boolean): Promise<typeof char
|
||||||
type: 'area',
|
type: 'area',
|
||||||
color: '#008FFB',
|
color: '#008FFB',
|
||||||
data: format(total
|
data: format(total
|
||||||
? raw.drive.totalUsage
|
? sum(raw.drive.incUsage)
|
||||||
: sum(raw.drive.incUsage, negate(raw.drive.decUsage)),
|
: sum(raw.drive.incUsage, negate(raw.drive.decUsage)),
|
||||||
),
|
),
|
||||||
}],
|
}],
|
||||||
|
@ -657,7 +661,7 @@ const fetchInstanceDriveUsageChart = async (total: boolean): Promise<typeof char
|
||||||
};
|
};
|
||||||
|
|
||||||
const fetchInstanceDriveFilesChart = async (total: boolean): Promise<typeof chartData> => {
|
const fetchInstanceDriveFilesChart = async (total: boolean): Promise<typeof chartData> => {
|
||||||
const raw = await misskeyApiGet('charts/instance', { host: props.args.host, limit: props.limit, span: props.span });
|
const raw = await misskeyApiGet('charts/instance', { host: props.args?.host, limit: props.limit, span: props.span });
|
||||||
return {
|
return {
|
||||||
series: [{
|
series: [{
|
||||||
name: 'Drive files',
|
name: 'Drive files',
|
||||||
|
@ -672,11 +676,11 @@ const fetchInstanceDriveFilesChart = async (total: boolean): Promise<typeof char
|
||||||
};
|
};
|
||||||
|
|
||||||
const fetchPerUserNotesChart = async (): Promise<typeof chartData> => {
|
const fetchPerUserNotesChart = async (): Promise<typeof chartData> => {
|
||||||
const raw = await misskeyApiGet('charts/user/notes', { userId: props.args.user.id, limit: props.limit, span: props.span });
|
const raw = await misskeyApiGet('charts/user/notes', { userId: props.args?.user?.id, limit: props.limit, span: props.span });
|
||||||
return {
|
return {
|
||||||
series: [...(props.args.withoutAll ? [] : [{
|
series: [...(props.args?.withoutAll ? [] : [{
|
||||||
name: 'All',
|
name: 'All',
|
||||||
type: 'line',
|
type: 'line' as const,
|
||||||
data: format(sum(raw.inc, negate(raw.dec))),
|
data: format(sum(raw.inc, negate(raw.dec))),
|
||||||
color: '#888888',
|
color: '#888888',
|
||||||
}]), {
|
}]), {
|
||||||
|
@ -704,7 +708,7 @@ const fetchPerUserNotesChart = async (): Promise<typeof chartData> => {
|
||||||
};
|
};
|
||||||
|
|
||||||
const fetchPerUserPvChart = async (): Promise<typeof chartData> => {
|
const fetchPerUserPvChart = async (): Promise<typeof chartData> => {
|
||||||
const raw = await misskeyApiGet('charts/user/pv', { userId: props.args.user.id, limit: props.limit, span: props.span });
|
const raw = await misskeyApiGet('charts/user/pv', { userId: props.args?.user?.id, limit: props.limit, span: props.span });
|
||||||
return {
|
return {
|
||||||
series: [{
|
series: [{
|
||||||
name: 'Unique PV (user)',
|
name: 'Unique PV (user)',
|
||||||
|
@ -731,7 +735,7 @@ const fetchPerUserPvChart = async (): Promise<typeof chartData> => {
|
||||||
};
|
};
|
||||||
|
|
||||||
const fetchPerUserFollowingChart = async (): Promise<typeof chartData> => {
|
const fetchPerUserFollowingChart = async (): Promise<typeof chartData> => {
|
||||||
const raw = await misskeyApiGet('charts/user/following', { userId: props.args.user.id, limit: props.limit, span: props.span });
|
const raw = await misskeyApiGet('charts/user/following', { userId: props.args?.user?.id, limit: props.limit, span: props.span });
|
||||||
return {
|
return {
|
||||||
series: [{
|
series: [{
|
||||||
name: 'Local',
|
name: 'Local',
|
||||||
|
@ -746,7 +750,7 @@ const fetchPerUserFollowingChart = async (): Promise<typeof chartData> => {
|
||||||
};
|
};
|
||||||
|
|
||||||
const fetchPerUserFollowersChart = async (): Promise<typeof chartData> => {
|
const fetchPerUserFollowersChart = async (): Promise<typeof chartData> => {
|
||||||
const raw = await misskeyApiGet('charts/user/following', { userId: props.args.user.id, limit: props.limit, span: props.span });
|
const raw = await misskeyApiGet('charts/user/following', { userId: props.args?.user?.id, limit: props.limit, span: props.span });
|
||||||
return {
|
return {
|
||||||
series: [{
|
series: [{
|
||||||
name: 'Local',
|
name: 'Local',
|
||||||
|
@ -761,8 +765,9 @@ const fetchPerUserFollowersChart = async (): Promise<typeof chartData> => {
|
||||||
};
|
};
|
||||||
|
|
||||||
const fetchPerUserDriveChart = async (): Promise<typeof chartData> => {
|
const fetchPerUserDriveChart = async (): Promise<typeof chartData> => {
|
||||||
const raw = await misskeyApiGet('charts/user/drive', { userId: props.args.user.id, limit: props.limit, span: props.span });
|
const raw = await misskeyApiGet('charts/user/drive', { userId: props.args?.user?.id, limit: props.limit, span: props.span });
|
||||||
return {
|
return {
|
||||||
|
bytes: true,
|
||||||
series: [{
|
series: [{
|
||||||
name: 'Inc',
|
name: 'Inc',
|
||||||
type: 'area',
|
type: 'area',
|
||||||
|
@ -806,6 +811,8 @@ const fetchAndRender = async () => {
|
||||||
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();
|
||||||
|
|
||||||
|
default: return null;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
fetching.value = true;
|
fetching.value = true;
|
||||||
|
|
|
@ -6,7 +6,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
<template>
|
<template>
|
||||||
<div :class="$style.root">
|
<div :class="$style.root">
|
||||||
<button v-for="item in items" class="_button item" :class="{ disabled: item.hidden }" @click="onClick(item)">
|
<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>
|
<span class="box" :style="{ background: type === 'line' ? item.strokeStyle?.toString() : item.fillStyle?.toString() }"></span>
|
||||||
{{ item.text }}
|
{{ item.text }}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
@ -16,25 +16,23 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
import { shallowRef } from 'vue';
|
import { shallowRef } from 'vue';
|
||||||
import { Chart, LegendItem } from 'chart.js';
|
import { Chart, LegendItem } from 'chart.js';
|
||||||
|
|
||||||
const props = defineProps({
|
|
||||||
});
|
|
||||||
|
|
||||||
const chart = shallowRef<Chart>();
|
const chart = shallowRef<Chart>();
|
||||||
|
const type = shallowRef<string>();
|
||||||
const items = shallowRef<LegendItem[]>([]);
|
const items = shallowRef<LegendItem[]>([]);
|
||||||
|
|
||||||
function update(_chart: Chart, _items: LegendItem[]) {
|
function update(_chart: Chart, _items: LegendItem[]) {
|
||||||
chart.value = _chart,
|
chart.value = _chart,
|
||||||
items.value = _items;
|
items.value = _items;
|
||||||
|
if ('type' in _chart.config) type.value = _chart.config.type;
|
||||||
}
|
}
|
||||||
|
|
||||||
function onClick(item: LegendItem) {
|
function onClick(item: LegendItem) {
|
||||||
if (chart.value == null) return;
|
if (chart.value == null) return;
|
||||||
const { type } = chart.value.config;
|
if (type.value === 'pie' || type.value === 'doughnut') {
|
||||||
if (type === 'pie' || type === 'doughnut') {
|
|
||||||
// Pie and doughnut charts only have a single dataset and visibility is per item
|
// Pie and doughnut charts only have a single dataset and visibility is per item
|
||||||
chart.value.toggleDataVisibility(item.index);
|
if (item.index) chart.value.toggleDataVisibility(item.index);
|
||||||
} else {
|
} else {
|
||||||
chart.value.setDatasetVisibility(item.datasetIndex, !chart.value.isDatasetVisible(item.datasetIndex));
|
if (item.datasetIndex) chart.value.setDatasetVisibility(item.datasetIndex, !chart.value.isDatasetVisible(item.datasetIndex));
|
||||||
}
|
}
|
||||||
chart.value.update();
|
chart.value.update();
|
||||||
}
|
}
|
||||||
|
|
|
@ -41,8 +41,8 @@ const { modelValue } = toRefs(props);
|
||||||
const v = ref(modelValue.value);
|
const v = ref(modelValue.value);
|
||||||
const inputEl = shallowRef<HTMLElement>();
|
const inputEl = shallowRef<HTMLElement>();
|
||||||
|
|
||||||
const onInput = (ev: KeyboardEvent) => {
|
const onInput = () => {
|
||||||
emit('update:modelValue', v.value);
|
emit('update:modelValue', v.value ?? '');
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|
|
@ -44,8 +44,8 @@ onMounted(() => {
|
||||||
let left = props.ev.pageX + 1; // 間違って右ダブルクリックした場合に意図せずアイテムがクリックされるのを防ぐため + 1
|
let left = props.ev.pageX + 1; // 間違って右ダブルクリックした場合に意図せずアイテムがクリックされるのを防ぐため + 1
|
||||||
let top = props.ev.pageY + 1; // 間違って右ダブルクリックした場合に意図せずアイテムがクリックされるのを防ぐため + 1
|
let top = props.ev.pageY + 1; // 間違って右ダブルクリックした場合に意図せずアイテムがクリックされるのを防ぐため + 1
|
||||||
|
|
||||||
const width = rootEl.value.offsetWidth;
|
const width = rootEl.value!.offsetWidth;
|
||||||
const height = rootEl.value.offsetHeight;
|
const height = rootEl.value!.offsetHeight;
|
||||||
|
|
||||||
if (left + width - window.pageXOffset >= (window.innerWidth - SCROLLBAR_THICKNESS)) {
|
if (left + width - window.pageXOffset >= (window.innerWidth - SCROLLBAR_THICKNESS)) {
|
||||||
left = (window.innerWidth - SCROLLBAR_THICKNESS) - width + window.pageXOffset;
|
left = (window.innerWidth - SCROLLBAR_THICKNESS) - width + window.pageXOffset;
|
||||||
|
@ -63,8 +63,10 @@ onMounted(() => {
|
||||||
left = 0;
|
left = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
rootEl.value.style.top = `${top}px`;
|
if (rootEl.value) {
|
||||||
rootEl.value.style.left = `${left}px`;
|
rootEl.value.style.top = `${top}px`;
|
||||||
|
rootEl.value.style.left = `${left}px`;
|
||||||
|
}
|
||||||
|
|
||||||
document.body.addEventListener('mousedown', onMousedown);
|
document.body.addEventListener('mousedown', onMousedown);
|
||||||
});
|
});
|
||||||
|
|
|
@ -10,6 +10,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { computed } from 'vue';
|
import { computed } from 'vue';
|
||||||
import * as Misskey from 'misskey-js';
|
import * as Misskey from 'misskey-js';
|
||||||
|
import type { PollEditorModelValue } from '@/components/MkPollEditor.vue';
|
||||||
import { concat } from '@/scripts/array.js';
|
import { concat } from '@/scripts/array.js';
|
||||||
import { i18n } from '@/i18n.js';
|
import { i18n } from '@/i18n.js';
|
||||||
import MkButton from '@/components/MkButton.vue';
|
import MkButton from '@/components/MkButton.vue';
|
||||||
|
@ -17,22 +18,9 @@ import MkButton from '@/components/MkButton.vue';
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
modelValue: boolean;
|
modelValue: boolean;
|
||||||
text: string | null;
|
text: string | null;
|
||||||
renote: Misskey.entities.Note | null;
|
renote?: Misskey.entities.Note | null;
|
||||||
files: Misskey.entities.DriveFile[];
|
files?: Misskey.entities.DriveFile[];
|
||||||
poll?: {
|
poll?: Misskey.entities.Note['poll'] | PollEditorModelValue | null;
|
||||||
expiresAt: string | null;
|
|
||||||
multiple: boolean;
|
|
||||||
choices: {
|
|
||||||
isVoted: boolean;
|
|
||||||
text: string;
|
|
||||||
votes: number;
|
|
||||||
}[];
|
|
||||||
} | {
|
|
||||||
choices: string[];
|
|
||||||
multiple: boolean;
|
|
||||||
expiresAt: string | null;
|
|
||||||
expiredAfter: string | null;
|
|
||||||
};
|
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
|
@ -43,7 +31,7 @@ const label = computed(() => {
|
||||||
return concat([
|
return concat([
|
||||||
props.text ? [i18n.tsx._cw.chars({ count: props.text.length })] : [],
|
props.text ? [i18n.tsx._cw.chars({ count: props.text.length })] : [],
|
||||||
props.renote ? [i18n.ts.quote] : [],
|
props.renote ? [i18n.ts.quote] : [],
|
||||||
props.files.length !== 0 ? [i18n.tsx._cw.files({ count: props.files.length })] : [],
|
props.files && props.files.length !== 0 ? [i18n.tsx._cw.files({ count: props.files.length })] : [],
|
||||||
props.poll != null ? [i18n.ts.poll] : [],
|
props.poll != null ? [i18n.ts.poll] : [],
|
||||||
] as string[][]).join(' / ');
|
] as string[][]).join(' / ');
|
||||||
});
|
});
|
||||||
|
|
|
@ -118,34 +118,36 @@ export default defineComponent({
|
||||||
return children;
|
return children;
|
||||||
};
|
};
|
||||||
|
|
||||||
function onBeforeLeave(el: HTMLElement) {
|
function onBeforeLeave(element: Element) {
|
||||||
|
const el = element as HTMLElement;
|
||||||
el.style.top = `${el.offsetTop}px`;
|
el.style.top = `${el.offsetTop}px`;
|
||||||
el.style.left = `${el.offsetLeft}px`;
|
el.style.left = `${el.offsetLeft}px`;
|
||||||
}
|
}
|
||||||
|
|
||||||
function onLeaveCanceled(el: HTMLElement) {
|
function onLeaveCancelled(element: Element) {
|
||||||
|
const el = element as HTMLElement;
|
||||||
el.style.top = '';
|
el.style.top = '';
|
||||||
el.style.left = '';
|
el.style.left = '';
|
||||||
}
|
}
|
||||||
|
|
||||||
return () => h(
|
// eslint-disable-next-line vue/no-setup-props-destructure
|
||||||
defaultStore.state.animation ? TransitionGroup : 'div',
|
const classes = {
|
||||||
{
|
[$style['date-separated-list']]: true,
|
||||||
class: {
|
[$style['date-separated-list-nogap']]: props.noGap,
|
||||||
[$style['date-separated-list']]: true,
|
[$style['reversed']]: props.reversed,
|
||||||
[$style['date-separated-list-nogap']]: props.noGap,
|
[$style['direction-down']]: props.direction === 'down',
|
||||||
[$style['reversed']]: props.reversed,
|
[$style['direction-up']]: props.direction === 'up',
|
||||||
[$style['direction-down']]: props.direction === 'down',
|
};
|
||||||
[$style['direction-up']]: props.direction === 'up',
|
|
||||||
},
|
return () => defaultStore.state.animation ? h(TransitionGroup, {
|
||||||
...(defaultStore.state.animation ? {
|
class: classes,
|
||||||
name: 'list',
|
name: 'list',
|
||||||
tag: 'div',
|
tag: 'div',
|
||||||
onBeforeLeave,
|
onBeforeLeave,
|
||||||
onLeaveCanceled,
|
onLeaveCancelled,
|
||||||
} : {}),
|
}, { default: renderChildren }) : h('div', {
|
||||||
},
|
class: classes,
|
||||||
{ default: renderChildren });
|
}, { default: renderChildren });
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
|
@ -31,15 +31,15 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
<MkInput v-if="input.type !== 'textarea'" v-model="inputValue" autofocus :type="input.type || 'text'" :placeholder="input.placeholder || undefined" :autocomplete="input.autocomplete" @keydown="onInputKeydown">
|
<MkInput v-if="input.type !== 'textarea'" v-model="inputValue" autofocus :type="input.type || 'text'" :placeholder="input.placeholder || undefined" :autocomplete="input.autocomplete" @keydown="onInputKeydown">
|
||||||
<template v-if="input.type === 'password'" #prefix><i class="ti ti-lock"></i></template>
|
<template v-if="input.type === 'password'" #prefix><i class="ti ti-lock"></i></template>
|
||||||
<template #caption>
|
<template #caption>
|
||||||
<span v-if="okButtonDisabledReason === 'charactersExceeded'" v-text="i18n.tsx._dialog.charactersExceeded({ current: (inputValue as string).length, max: input.maxLength ?? 'NaN' })"/>
|
<span v-if="okButtonDisabledReason === 'charactersExceeded'" v-text="i18n.tsx._dialog.charactersExceeded({ current: (inputValue as string)?.length ?? 0, max: input.maxLength ?? 'NaN' })"/>
|
||||||
<span v-else-if="okButtonDisabledReason === 'charactersBelow'" v-text="i18n.tsx._dialog.charactersBelow({ current: (inputValue as string).length, min: input.minLength ?? 'NaN' })"/>
|
<span v-else-if="okButtonDisabledReason === 'charactersBelow'" v-text="i18n.tsx._dialog.charactersBelow({ current: (inputValue as string)?.length ?? 0, min: input.minLength ?? 'NaN' })"/>
|
||||||
</template>
|
</template>
|
||||||
</MkInput>
|
</MkInput>
|
||||||
<MkTextarea v-if="input.type === 'textarea'" v-model="inputValue" :placeholder="input.placeholder || undefined" :autocomplete="input.autocomplete">
|
<MkTextarea v-if="input.type === 'textarea'" v-model="inputValue" :placeholder="input.placeholder || undefined" :autocomplete="input.autocomplete">
|
||||||
<template #label>{{ input.placeholder }}</template>
|
<template #label>{{ input.placeholder }}</template>
|
||||||
<template #caption>
|
<template #caption>
|
||||||
<span v-if="okButtonDisabledReason === 'charactersExceeded'" v-text="i18n.tsx._dialog.charactersExceeded({ current: (inputValue as string).length, max: input.maxLength ?? 'NaN' })"/>
|
<span v-if="okButtonDisabledReason === 'charactersExceeded'" v-text="i18n.tsx._dialog.charactersExceeded({ current: (inputValue as string)?.length ?? 0, max: input.maxLength ?? 'NaN' })"/>
|
||||||
<span v-else-if="okButtonDisabledReason === 'charactersBelow'" v-text="i18n.tsx._dialog.charactersBelow({ current: (inputValue as string).length, min: input.minLength ?? 'NaN' })"/>
|
<span v-else-if="okButtonDisabledReason === 'charactersBelow'" v-text="i18n.tsx._dialog.charactersBelow({ current: (inputValue as string)?.length ?? 0, min: input.minLength ?? 'NaN' })"/>
|
||||||
</template>
|
</template>
|
||||||
</MkTextarea>
|
</MkTextarea>
|
||||||
</template>
|
</template>
|
||||||
|
@ -135,7 +135,7 @@ const selectedValue = ref(props.select?.default ?? null);
|
||||||
const okButtonDisabledReason = computed<null | 'charactersExceeded' | 'charactersBelow'>(() => {
|
const okButtonDisabledReason = computed<null | 'charactersExceeded' | 'charactersBelow'>(() => {
|
||||||
if (props.input) {
|
if (props.input) {
|
||||||
if (props.input.minLength) {
|
if (props.input.minLength) {
|
||||||
if ((inputValue.value || inputValue.value === '') && (inputValue.value as string).length < props.input.minLength) {
|
if (inputValue.value == null || (inputValue.value as string).length < props.input.minLength) {
|
||||||
return 'charactersBelow';
|
return 'charactersBelow';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -47,7 +47,7 @@ import { i18n } from '@/i18n.js';
|
||||||
import { $i } from '@/account.js';
|
import { $i } from '@/account.js';
|
||||||
import { getDriveFileMenu } from '@/scripts/get-drive-file-menu.js';
|
import { getDriveFileMenu } from '@/scripts/get-drive-file-menu.js';
|
||||||
import { deviceKind } from '@/scripts/device-kind.js';
|
import { deviceKind } from '@/scripts/device-kind.js';
|
||||||
import { useRouter } from '@/global/router/supplier.js';
|
import { useRouter } from '@/router/supplier.js';
|
||||||
|
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
|
||||||
|
|
|
@ -205,7 +205,7 @@ function onDragend() {
|
||||||
}
|
}
|
||||||
|
|
||||||
function go() {
|
function go() {
|
||||||
emit('move', props.folder.id);
|
emit('move', props.folder);
|
||||||
}
|
}
|
||||||
|
|
||||||
function rename() {
|
function rename() {
|
||||||
|
|
|
@ -98,6 +98,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
import { nextTick, onActivated, onBeforeUnmount, onMounted, ref, shallowRef, watch } from 'vue';
|
import { 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 type { MenuItem } from '@/types/menu.js';
|
||||||
import XNavFolder from '@/components/MkDrive.navFolder.vue';
|
import XNavFolder from '@/components/MkDrive.navFolder.vue';
|
||||||
import XFolder from '@/components/MkDrive.folder.vue';
|
import XFolder from '@/components/MkDrive.folder.vue';
|
||||||
import XFile from '@/components/MkDrive.file.vue';
|
import XFile from '@/components/MkDrive.file.vue';
|
||||||
|
@ -427,7 +428,7 @@ function chooseFolder(folderToChoose: Misskey.entities.DriveFolder) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function move(target?: Misskey.entities.DriveFolder) {
|
function move(target?: Misskey.entities.DriveFolder | Misskey.entities.DriveFolder['id' | 'parentId']) {
|
||||||
if (!target) {
|
if (!target) {
|
||||||
goRoot();
|
goRoot();
|
||||||
return;
|
return;
|
||||||
|
@ -613,7 +614,7 @@ function fetchMoreFiles() {
|
||||||
}
|
}
|
||||||
|
|
||||||
function getMenu() {
|
function getMenu() {
|
||||||
return [{
|
const menu: MenuItem[] = [{
|
||||||
type: 'switch',
|
type: 'switch',
|
||||||
text: i18n.ts.keepOriginalUploading,
|
text: i18n.ts.keepOriginalUploading,
|
||||||
ref: keepOriginal,
|
ref: keepOriginal,
|
||||||
|
@ -634,7 +635,7 @@ function getMenu() {
|
||||||
}, folder.value ? {
|
}, folder.value ? {
|
||||||
text: i18n.ts.renameFolder,
|
text: i18n.ts.renameFolder,
|
||||||
icon: 'ti ti-forms',
|
icon: 'ti ti-forms',
|
||||||
action: () => { renameFolder(folder.value); },
|
action: () => { if (folder.value) renameFolder(folder.value); },
|
||||||
} : undefined, folder.value ? {
|
} : undefined, folder.value ? {
|
||||||
text: i18n.ts.deleteFolder,
|
text: i18n.ts.deleteFolder,
|
||||||
icon: 'ti ti-trash',
|
icon: 'ti ti-trash',
|
||||||
|
@ -644,6 +645,8 @@ function getMenu() {
|
||||||
icon: 'ti ti-folder-plus',
|
icon: 'ti ti-folder-plus',
|
||||||
action: () => { createFolder(); },
|
action: () => { createFolder(); },
|
||||||
}];
|
}];
|
||||||
|
|
||||||
|
return menu;
|
||||||
}
|
}
|
||||||
|
|
||||||
function showMenu(ev: MouseEvent) {
|
function showMenu(ev: MouseEvent) {
|
||||||
|
|
|
@ -27,7 +27,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
<!-- フォルダの中にはカスタム絵文字やフォルダがある -->
|
<!-- フォルダの中にはカスタム絵文字やフォルダがある -->
|
||||||
<section v-else v-panel style="border-radius: 6px; border-bottom: 0.5px solid var(--divider);">
|
<section v-else v-panel style="border-radius: 6px; border-bottom: 0.5px solid var(--divider);">
|
||||||
<header class="_acrylic" @click="shown = !shown">
|
<header class="_acrylic" @click="shown = !shown">
|
||||||
<i class="toggle ti-fw" :class="shown ? 'ti ti-chevron-down' : 'ti ti-chevron-up'"></i> <slot></slot> (<i class="ti ti-folder ti-fw"></i>:{{ customEmojiTree.length }} <i class="ti ti-icons ti-fw"></i>:{{ emojis.length }})
|
<i class="toggle ti-fw" :class="shown ? 'ti ti-chevron-down' : 'ti ti-chevron-up'"></i> <slot></slot> (<i class="ti ti-folder ti-fw"></i>:{{ customEmojiTree?.length }} <i class="ti ti-icons ti-fw"></i>:{{ emojis.length }})
|
||||||
</header>
|
</header>
|
||||||
<div v-if="shown" style="padding-left: 9px;">
|
<div v-if="shown" style="padding-left: 9px;">
|
||||||
<MkEmojiPickerSection
|
<MkEmojiPickerSection
|
||||||
|
@ -61,7 +61,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { ref, computed, Ref } from 'vue';
|
import { ref, computed, Ref } from 'vue';
|
||||||
import { CustomEmojiFolderTree, getEmojiName } from '@/scripts/emojilist.js';
|
import { CustomEmojiFolderTree, getEmojiName } from '@/scripts/emojilist.js';
|
||||||
import { i18n } from '../i18n.js';
|
import { i18n } from '@/i18n.js';
|
||||||
import { customEmojis } from '@/custom-emojis.js';
|
import { customEmojis } from '@/custom-emojis.js';
|
||||||
import MkEmojiPickerSection from '@/components/MkEmojiPicker.section.vue';
|
import MkEmojiPickerSection from '@/components/MkEmojiPicker.section.vue';
|
||||||
|
|
||||||
|
@ -87,7 +87,7 @@ function computeButtonTitle(ev: MouseEvent): void {
|
||||||
elm.title = getEmojiName(emoji) ?? emoji;
|
elm.title = getEmojiName(emoji) ?? emoji;
|
||||||
}
|
}
|
||||||
|
|
||||||
function nestedChosen(emoji: any, ev?: MouseEvent) {
|
function nestedChosen(emoji: any, ev: MouseEvent) {
|
||||||
emit('chosen', emoji, ev);
|
emit('chosen', emoji, ev);
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
|
@ -36,7 +36,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<div v-if="tab === 'index'" class="group index">
|
<div v-if="tab === 'index'" class="group index">
|
||||||
<section v-if="showPinned && pinned.length > 0">
|
<section v-if="showPinned && (pinned && pinned.length > 0)">
|
||||||
<div class="body">
|
<div class="body">
|
||||||
<button
|
<button
|
||||||
v-for="emoji in pinned"
|
v-for="emoji in pinned"
|
||||||
|
|
|
@ -11,7 +11,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
:withOkButton="true"
|
:withOkButton="true"
|
||||||
:okButtonDisabled="false"
|
:okButtonDisabled="false"
|
||||||
@ok="ok()"
|
@ok="ok()"
|
||||||
@close="dialog.close()"
|
@close="dialog?.close()"
|
||||||
@closed="emit('closed')"
|
@closed="emit('closed')"
|
||||||
>
|
>
|
||||||
<template #header>{{ i18n.ts.describeFile }}</template>
|
<template #header>{{ i18n.ts.describeFile }}</template>
|
||||||
|
@ -48,6 +48,6 @@ const caption = ref(props.default);
|
||||||
|
|
||||||
async function ok() {
|
async function ok() {
|
||||||
emit('done', caption.value);
|
emit('done', caption.value);
|
||||||
dialog.value.close();
|
dialog.value?.close();
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
|
@ -7,7 +7,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
<div>
|
<div>
|
||||||
<MkPagination v-slot="{items}" :pagination="pagination" class="urempief" :class="{ grid: viewMode === 'grid' }">
|
<MkPagination v-slot="{items}" :pagination="pagination" class="urempief" :class="{ grid: viewMode === 'grid' }">
|
||||||
<MkA
|
<MkA
|
||||||
v-for="file in items"
|
v-for="file in (items as Misskey.entities.DriveFile[])"
|
||||||
:key="file.id"
|
:key="file.id"
|
||||||
v-tooltip.mfm="`${file.type}\n${bytes(file.size)}\n${dateString(file.createdAt)}\nby ${file.user ? '@' + Misskey.acct.toString(file.user) : 'system'}`"
|
v-tooltip.mfm="`${file.type}\n${bytes(file.size)}\n${dateString(file.createdAt)}\nby ${file.user ? '@' + Misskey.acct.toString(file.user) : 'system'}`"
|
||||||
:to="`/admin/file/${file.id}`"
|
:to="`/admin/file/${file.id}`"
|
||||||
|
|
|
@ -4,7 +4,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
-->
|
-->
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div ref="el" :class="$style.root">
|
<div ref="rootEl" :class="$style.root">
|
||||||
<header :class="$style.header" class="_button" :style="{ background: bg }" @click="showBody = !showBody">
|
<header :class="$style.header" class="_button" :style="{ background: bg }" @click="showBody = !showBody">
|
||||||
<div :class="$style.title"><div><slot name="header"></slot></div></div>
|
<div :class="$style.title"><div><slot name="header"></slot></div></div>
|
||||||
<div :class="$style.divider"></div>
|
<div :class="$style.divider"></div>
|
||||||
|
@ -14,7 +14,10 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
</button>
|
</button>
|
||||||
</header>
|
</header>
|
||||||
<Transition
|
<Transition
|
||||||
:name="defaultStore.state.animation ? 'folder-toggle' : ''"
|
:enterActiveClass="defaultStore.state.animation ? $style['folder-toggle-enter-active'] : ''"
|
||||||
|
:leaveActiveClass="defaultStore.state.animation ? $style['folder-toggle-leave-active'] : ''"
|
||||||
|
:enterFromClass="defaultStore.state.animation ? $style['folder-toggle-enter-from'] : ''"
|
||||||
|
:leaveToClass="defaultStore.state.animation ? $style['folder-toggle-leave-to'] : ''"
|
||||||
@enter="enter"
|
@enter="enter"
|
||||||
@afterEnter="afterEnter"
|
@afterEnter="afterEnter"
|
||||||
@leave="leave"
|
@leave="leave"
|
||||||
|
@ -42,8 +45,8 @@ const props = withDefaults(defineProps<{
|
||||||
expanded: true,
|
expanded: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
const el = shallowRef<HTMLDivElement>();
|
const rootEl = shallowRef<HTMLDivElement>();
|
||||||
const bg = ref<string | null>(null);
|
const bg = ref<string>();
|
||||||
const showBody = ref((props.persistKey && miLocalStorage.getItem(`${miLocalStoragePrefix}${props.persistKey}`)) ? (miLocalStorage.getItem(`${miLocalStoragePrefix}${props.persistKey}`) === 't') : props.expanded);
|
const showBody = ref((props.persistKey && miLocalStorage.getItem(`${miLocalStoragePrefix}${props.persistKey}`)) ? (miLocalStorage.getItem(`${miLocalStoragePrefix}${props.persistKey}`) === 't') : props.expanded);
|
||||||
|
|
||||||
watch(showBody, () => {
|
watch(showBody, () => {
|
||||||
|
@ -52,40 +55,44 @@ watch(showBody, () => {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
function enter(el: Element) {
|
function enter(element: Element) {
|
||||||
|
const el = element as HTMLElement;
|
||||||
const elementHeight = el.getBoundingClientRect().height;
|
const elementHeight = el.getBoundingClientRect().height;
|
||||||
el.style.height = 0;
|
el.style.height = '0';
|
||||||
el.offsetHeight; // reflow
|
el.offsetHeight; // reflow
|
||||||
el.style.height = elementHeight + 'px';
|
el.style.height = elementHeight + 'px';
|
||||||
}
|
}
|
||||||
|
|
||||||
function afterEnter(el: Element) {
|
function afterEnter(element: Element) {
|
||||||
el.style.height = null;
|
const el = element as HTMLElement;
|
||||||
|
el.style.height = 'unset';
|
||||||
}
|
}
|
||||||
|
|
||||||
function leave(el: Element) {
|
function leave(element: Element) {
|
||||||
|
const el = element as HTMLElement;
|
||||||
const elementHeight = el.getBoundingClientRect().height;
|
const elementHeight = el.getBoundingClientRect().height;
|
||||||
el.style.height = elementHeight + 'px';
|
el.style.height = elementHeight + 'px';
|
||||||
el.offsetHeight; // reflow
|
el.offsetHeight; // reflow
|
||||||
el.style.height = 0;
|
el.style.height = '0';
|
||||||
}
|
}
|
||||||
|
|
||||||
function afterLeave(el: Element) {
|
function afterLeave(element: Element) {
|
||||||
el.style.height = null;
|
const el = element as HTMLElement;
|
||||||
|
el.style.height = 'unset';
|
||||||
}
|
}
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
function getParentBg(el: HTMLElement | null): string {
|
function getParentBg(el?: HTMLElement | null): string {
|
||||||
if (el == null || el.tagName === 'BODY') return 'var(--bg)';
|
if (el == null || el.tagName === 'BODY') return 'var(--bg)';
|
||||||
const bg = el.style.background || el.style.backgroundColor;
|
const background = el.style.background || el.style.backgroundColor;
|
||||||
if (bg) {
|
if (background) {
|
||||||
return bg;
|
return background;
|
||||||
} else {
|
} else {
|
||||||
return getParentBg(el.parentElement);
|
return getParentBg(el.parentElement);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const rawBg = getParentBg(el.value);
|
const rawBg = getParentBg(rootEl.value);
|
||||||
const _bg = tinycolor(rawBg.startsWith('var(') ? getComputedStyle(document.documentElement).getPropertyValue(rawBg.slice(4, -1)) : rawBg);
|
const _bg = tinycolor(rawBg.startsWith('var(') ? getComputedStyle(document.documentElement).getPropertyValue(rawBg.slice(4, -1)) : rawBg);
|
||||||
_bg.setAlpha(0.85);
|
_bg.setAlpha(0.85);
|
||||||
bg.value = _bg.toRgbString();
|
bg.value = _bg.toRgbString();
|
||||||
|
@ -97,10 +104,8 @@ onMounted(() => {
|
||||||
overflow-y: clip;
|
overflow-y: clip;
|
||||||
transition: opacity 0.5s, height 0.5s !important;
|
transition: opacity 0.5s, height 0.5s !important;
|
||||||
}
|
}
|
||||||
.folder-toggle-enter-from {
|
|
||||||
opacity: 0;
|
.folder-toggle-enter-from, .folder-toggle-leave-to {
|
||||||
}
|
|
||||||
.folder-toggle-leave-to {
|
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -25,7 +25,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<div v-if="openedAtLeastOnce" :class="[$style.body, { [$style.bgSame]: bgSame }]" :style="{ maxHeight: maxHeight ? `${maxHeight}px` : null, overflow: maxHeight ? `auto` : null }" :aria-hidden="!opened">
|
<div v-if="openedAtLeastOnce" :class="[$style.body, { [$style.bgSame]: bgSame }]" :style="{ maxHeight: maxHeight ? `${maxHeight}px` : undefined, overflow: maxHeight ? `auto` : undefined }" :aria-hidden="!opened">
|
||||||
<Transition
|
<Transition
|
||||||
:enterActiveClass="defaultStore.state.animation ? $style.transition_toggle_enterActive : ''"
|
:enterActiveClass="defaultStore.state.animation ? $style.transition_toggle_enterActive : ''"
|
||||||
:leaveActiveClass="defaultStore.state.animation ? $style.transition_toggle_leaveActive : ''"
|
:leaveActiveClass="defaultStore.state.animation ? $style.transition_toggle_leaveActive : ''"
|
||||||
|
@ -109,7 +109,7 @@ function toggle() {
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
const computedStyle = getComputedStyle(document.documentElement);
|
const computedStyle = getComputedStyle(document.documentElement);
|
||||||
const parentBg = getBgColor(rootEl.value.parentElement);
|
const parentBg = getBgColor(rootEl.value!.parentElement!);
|
||||||
const myBg = computedStyle.getPropertyValue('--panel');
|
const myBg = computedStyle.getPropertyValue('--panel');
|
||||||
bgSame.value = parentBg === myBg;
|
bgSame.value = parentBg === myBg;
|
||||||
});
|
});
|
||||||
|
|
|
@ -8,7 +8,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
ref="dialog"
|
ref="dialog"
|
||||||
:width="370"
|
:width="370"
|
||||||
:height="400"
|
:height="400"
|
||||||
@close="dialog.close()"
|
@close="dialog?.close()"
|
||||||
@closed="emit('closed')"
|
@closed="emit('closed')"
|
||||||
>
|
>
|
||||||
<template #header>{{ i18n.ts.forgotPassword }}</template>
|
<template #header>{{ i18n.ts.forgotPassword }}</template>
|
||||||
|
@ -66,6 +66,6 @@ async function onSubmit() {
|
||||||
email: email.value,
|
email: email.value,
|
||||||
});
|
});
|
||||||
emit('done');
|
emit('done');
|
||||||
dialog.value.close();
|
dialog.value?.close();
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
|
@ -40,11 +40,11 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
</MkSwitch>
|
</MkSwitch>
|
||||||
<MkSelect v-else-if="form[item].type === 'enum'" v-model="values[item]">
|
<MkSelect v-else-if="form[item].type === 'enum'" v-model="values[item]">
|
||||||
<template #label><span v-text="form[item].label || item"></span><span v-if="form[item].required === false"> ({{ i18n.ts.optional }})</span></template>
|
<template #label><span v-text="form[item].label || item"></span><span v-if="form[item].required === false"> ({{ i18n.ts.optional }})</span></template>
|
||||||
<option v-for="item in form[item].enum" :key="item.value" :value="item.value">{{ item.label }}</option>
|
<option v-for="option in form[item].enum" :key="option.value" :value="option.value">{{ option.label }}</option>
|
||||||
</MkSelect>
|
</MkSelect>
|
||||||
<MkRadios v-else-if="form[item].type === 'radio'" v-model="values[item]">
|
<MkRadios v-else-if="form[item].type === 'radio'" v-model="values[item]">
|
||||||
<template #label><span v-text="form[item].label || item"></span><span v-if="form[item].required === false"> ({{ i18n.ts.optional }})</span></template>
|
<template #label><span v-text="form[item].label || item"></span><span v-if="form[item].required === false"> ({{ i18n.ts.optional }})</span></template>
|
||||||
<option v-for="item in form[item].options" :key="item.value" :value="item.value">{{ item.label }}</option>
|
<option v-for="option in form[item].options" :key="option.value" :value="option.value">{{ option.label }}</option>
|
||||||
</MkRadios>
|
</MkRadios>
|
||||||
<MkRange v-else-if="form[item].type === 'range'" v-model="values[item]" :min="form[item].min" :max="form[item].max" :step="form[item].step" :textConverter="form[item].textConverter">
|
<MkRange v-else-if="form[item].type === 'range'" v-model="values[item]" :min="form[item].min" :max="form[item].max" :step="form[item].step" :textConverter="form[item].textConverter">
|
||||||
<template #label><span v-text="form[item].label || item"></span><span v-if="form[item].required === false"> ({{ i18n.ts.optional }})</span></template>
|
<template #label><span v-text="form[item].label || item"></span><span v-if="form[item].required === false"> ({{ i18n.ts.optional }})</span></template>
|
||||||
|
@ -86,6 +86,7 @@ const emit = defineEmits<{
|
||||||
canceled?: boolean;
|
canceled?: boolean;
|
||||||
result?: any;
|
result?: any;
|
||||||
}): void;
|
}): void;
|
||||||
|
(ev: 'closed'): void;
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
const dialog = shallowRef<InstanceType<typeof MkModalWindow>>();
|
const dialog = shallowRef<InstanceType<typeof MkModalWindow>>();
|
||||||
|
@ -99,13 +100,13 @@ function ok() {
|
||||||
emit('done', {
|
emit('done', {
|
||||||
result: values,
|
result: values,
|
||||||
});
|
});
|
||||||
dialog.value.close();
|
dialog.value?.close();
|
||||||
}
|
}
|
||||||
|
|
||||||
function cancel() {
|
function cancel() {
|
||||||
emit('done', {
|
emit('done', {
|
||||||
canceled: true,
|
canceled: true,
|
||||||
});
|
});
|
||||||
dialog.value.close();
|
dialog.value?.close();
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
|
@ -14,8 +14,8 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
leaveActiveClass: $style.transition_toggle_leaveActive,
|
leaveActiveClass: $style.transition_toggle_leaveActive,
|
||||||
leaveToClass: $style.transition_toggle_leaveTo,
|
leaveToClass: $style.transition_toggle_leaveTo,
|
||||||
}"
|
}"
|
||||||
:src="post.files[0].thumbnailUrl"
|
:src="post.files?.[0]?.thumbnailUrl"
|
||||||
:hash="post.files[0].blurhash"
|
:hash="post.files?.[0]?.blurhash"
|
||||||
:forceBlurhash="!show"
|
:forceBlurhash="!show"
|
||||||
/>
|
/>
|
||||||
</Transition>
|
</Transition>
|
||||||
|
|
|
@ -35,10 +35,10 @@ const props = withDefaults(defineProps<{
|
||||||
label: '',
|
label: '',
|
||||||
});
|
});
|
||||||
|
|
||||||
const rootEl = shallowRef<HTMLDivElement>(null);
|
const rootEl = shallowRef<HTMLDivElement | null>(null);
|
||||||
const chartEl = shallowRef<HTMLCanvasElement>(null);
|
const chartEl = shallowRef<HTMLCanvasElement | null>(null);
|
||||||
const now = new Date();
|
const now = new Date();
|
||||||
let chartInstance: Chart = null;
|
let chartInstance: Chart | null = null;
|
||||||
const fetching = ref(true);
|
const fetching = ref(true);
|
||||||
|
|
||||||
const { handler: externalTooltipHandler } = useChartTooltip({
|
const { handler: externalTooltipHandler } = useChartTooltip({
|
||||||
|
@ -46,6 +46,7 @@ const { handler: externalTooltipHandler } = useChartTooltip({
|
||||||
});
|
});
|
||||||
|
|
||||||
async function renderChart() {
|
async function renderChart() {
|
||||||
|
if (rootEl.value == null) return;
|
||||||
if (chartInstance) {
|
if (chartInstance) {
|
||||||
chartInstance.destroy();
|
chartInstance.destroy();
|
||||||
}
|
}
|
||||||
|
@ -64,7 +65,7 @@ async function renderChart() {
|
||||||
return new Date(y, m, d - ago);
|
return new Date(y, m, d - ago);
|
||||||
};
|
};
|
||||||
|
|
||||||
const format = (arr) => {
|
const format = (arr: number[]) => {
|
||||||
return arr.map((v, i) => {
|
return arr.map((v, i) => {
|
||||||
const dt = getDate(i);
|
const dt = getDate(i);
|
||||||
const iso = `${dt.getFullYear()}-${(dt.getMonth() + 1).toString().padStart(2, '0')}-${dt.getDate().toString().padStart(2, '0')}`;
|
const iso = `${dt.getFullYear()}-${(dt.getMonth() + 1).toString().padStart(2, '0')}-${dt.getDate().toString().padStart(2, '0')}`;
|
||||||
|
@ -77,7 +78,7 @@ async function renderChart() {
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
let values;
|
let values: number[] = [];
|
||||||
|
|
||||||
if (props.src === 'active-users') {
|
if (props.src === 'active-users') {
|
||||||
const raw = await misskeyApi('charts/active-users', { limit: chartLimit, span: 'day' });
|
const raw = await misskeyApi('charts/active-users', { limit: chartLimit, span: 'day' });
|
||||||
|
@ -114,25 +115,25 @@ async function renderChart() {
|
||||||
|
|
||||||
const marginEachCell = 4;
|
const marginEachCell = 4;
|
||||||
|
|
||||||
|
if (chartEl.value == null) return;
|
||||||
|
|
||||||
chartInstance = new Chart(chartEl.value, {
|
chartInstance = new Chart(chartEl.value, {
|
||||||
type: 'matrix',
|
type: 'matrix',
|
||||||
data: {
|
data: {
|
||||||
datasets: [{
|
datasets: [{
|
||||||
label: props.label,
|
label: props.label,
|
||||||
data: format(values),
|
data: format(values) as any,
|
||||||
pointRadius: 0,
|
|
||||||
borderWidth: 0,
|
borderWidth: 0,
|
||||||
borderJoinStyle: 'round',
|
|
||||||
borderRadius: 3,
|
borderRadius: 3,
|
||||||
backgroundColor(c) {
|
backgroundColor(c) {
|
||||||
const value = c.dataset.data[c.dataIndex].v;
|
// @ts-expect-error TS(2339)
|
||||||
|
const value = c.dataset.data[c.dataIndex].v as number;
|
||||||
let a = (value - min) / max;
|
let a = (value - min) / max;
|
||||||
if (value !== 0) { // 0でない限りは完全に不可視にはしない
|
if (value !== 0) { // 0でない限りは完全に不可視にはしない
|
||||||
a = Math.max(a, 0.05);
|
a = Math.max(a, 0.05);
|
||||||
}
|
}
|
||||||
return alpha(color, a);
|
return alpha(color, a);
|
||||||
},
|
},
|
||||||
fill: true,
|
|
||||||
width(c) {
|
width(c) {
|
||||||
const a = c.chart.chartArea ?? {};
|
const a = c.chart.chartArea ?? {};
|
||||||
return (a.right - a.left) / weeks - marginEachCell;
|
return (a.right - a.left) / weeks - marginEachCell;
|
||||||
|
@ -206,11 +207,13 @@ async function renderChart() {
|
||||||
enabled: false,
|
enabled: false,
|
||||||
callbacks: {
|
callbacks: {
|
||||||
title(context) {
|
title(context) {
|
||||||
const v = context[0].dataset.data[context[0].dataIndex];
|
// @ts-expect-error TS(2339)
|
||||||
return v.d;
|
return context[0].dataset.data[context[0].dataIndex].d;
|
||||||
},
|
},
|
||||||
label(context) {
|
label(context) {
|
||||||
const v = context.dataset.data[context.dataIndex];
|
const v = context.dataset.data[context.dataIndex];
|
||||||
|
|
||||||
|
// @ts-expect-error TS(2339)
|
||||||
return [v.v];
|
return [v.v];
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
|
@ -73,7 +73,7 @@ const props = withDefaults(defineProps<{
|
||||||
leaveFromClass?: string;
|
leaveFromClass?: string;
|
||||||
} | null;
|
} | null;
|
||||||
src?: string | null;
|
src?: string | null;
|
||||||
hash?: string;
|
hash?: string | null;
|
||||||
alt?: string | null;
|
alt?: string | null;
|
||||||
title?: string | null;
|
title?: string | null;
|
||||||
height?: number;
|
height?: number;
|
||||||
|
|
|
@ -89,17 +89,18 @@ const focused = ref(false);
|
||||||
const changed = ref(false);
|
const changed = ref(false);
|
||||||
const invalid = ref(false);
|
const invalid = ref(false);
|
||||||
const filled = computed(() => v.value !== '' && v.value != null);
|
const filled = computed(() => v.value !== '' && v.value != null);
|
||||||
const inputEl = shallowRef<HTMLElement>();
|
const inputEl = shallowRef<HTMLInputElement>();
|
||||||
const prefixEl = shallowRef<HTMLElement>();
|
const prefixEl = shallowRef<HTMLElement>();
|
||||||
const suffixEl = shallowRef<HTMLElement>();
|
const suffixEl = shallowRef<HTMLElement>();
|
||||||
const height =
|
const height =
|
||||||
props.small ? 33 :
|
props.small ? 33 :
|
||||||
props.large ? 39 :
|
props.large ? 39 :
|
||||||
36;
|
36;
|
||||||
let autocomplete: Autocomplete;
|
let autocompleteWorker: Autocomplete | null = null;
|
||||||
|
|
||||||
const focus = () => inputEl.value.focus();
|
const focus = () => inputEl.value?.focus();
|
||||||
const onInput = (ev: KeyboardEvent) => {
|
const onInput = (event: Event) => {
|
||||||
|
const ev = event as KeyboardEvent;
|
||||||
changed.value = true;
|
changed.value = true;
|
||||||
emit('change', ev);
|
emit('change', ev);
|
||||||
};
|
};
|
||||||
|
@ -122,9 +123,9 @@ const onFocus = () => {
|
||||||
const updated = () => {
|
const updated = () => {
|
||||||
changed.value = false;
|
changed.value = false;
|
||||||
if (type.value === 'number') {
|
if (type.value === 'number') {
|
||||||
emit('update:modelValue', parseFloat(v.value));
|
emit('update:modelValue', typeof v.value === 'number' ? v.value : parseFloat(v.value ?? '0'));
|
||||||
} else {
|
} else {
|
||||||
emit('update:modelValue', v.value);
|
emit('update:modelValue', v.value ?? '');
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -134,7 +135,7 @@ watch(modelValue, newValue => {
|
||||||
v.value = newValue;
|
v.value = newValue;
|
||||||
});
|
});
|
||||||
|
|
||||||
watch(v, newValue => {
|
watch(v, () => {
|
||||||
if (!props.manualSave) {
|
if (!props.manualSave) {
|
||||||
if (props.debounce) {
|
if (props.debounce) {
|
||||||
debouncedUpdated();
|
debouncedUpdated();
|
||||||
|
@ -143,12 +144,14 @@ watch(v, newValue => {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
invalid.value = inputEl.value.validity.badInput;
|
invalid.value = inputEl.value?.validity.badInput ?? true;
|
||||||
});
|
});
|
||||||
|
|
||||||
// このコンポーネントが作成された時、非表示状態である場合がある
|
// このコンポーネントが作成された時、非表示状態である場合がある
|
||||||
// 非表示状態だと要素の幅などは0になってしまうので、定期的に計算する
|
// 非表示状態だと要素の幅などは0になってしまうので、定期的に計算する
|
||||||
useInterval(() => {
|
useInterval(() => {
|
||||||
|
if (inputEl.value == null) return;
|
||||||
|
|
||||||
if (prefixEl.value) {
|
if (prefixEl.value) {
|
||||||
if (prefixEl.value.offsetWidth) {
|
if (prefixEl.value.offsetWidth) {
|
||||||
inputEl.value.style.paddingLeft = prefixEl.value.offsetWidth + 'px';
|
inputEl.value.style.paddingLeft = prefixEl.value.offsetWidth + 'px';
|
||||||
|
@ -171,14 +174,14 @@ onMounted(() => {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
if (props.mfmAutocomplete) {
|
if (props.mfmAutocomplete && inputEl.value) {
|
||||||
autocomplete = new Autocomplete(inputEl.value, v, props.mfmAutocomplete === true ? null : props.mfmAutocomplete);
|
autocompleteWorker = new Autocomplete(inputEl.value, v, props.mfmAutocomplete === true ? undefined : props.mfmAutocomplete);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
onUnmounted(() => {
|
onUnmounted(() => {
|
||||||
if (autocomplete) {
|
if (autocompleteWorker) {
|
||||||
autocomplete.detach();
|
autocompleteWorker.detach();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -138,7 +138,8 @@ function createDoughnut(chartEl, tooltip, data) {
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
onClick: (ev) => {
|
onClick: (ev) => {
|
||||||
const hit = chartInstance.getElementsAtEventForMode(ev, 'nearest', { intersect: true }, false)[0];
|
if (ev.native == null) return;
|
||||||
|
const hit = chartInstance.getElementsAtEventForMode(ev.native, 'nearest', { intersect: true }, false)[0];
|
||||||
if (hit && data[hit.index].onClick) {
|
if (hit && data[hit.index].onClick) {
|
||||||
data[hit.index].onClick();
|
data[hit.index].onClick();
|
||||||
}
|
}
|
||||||
|
@ -164,23 +165,46 @@ function createDoughnut(chartEl, tooltip, data) {
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
misskeyApiGet('federation/stats', { limit: 30 }).then(fedStats => {
|
misskeyApiGet('federation/stats', { limit: 30 }).then(fedStats => {
|
||||||
createDoughnut(subDoughnutEl.value, externalTooltipHandler1, fedStats.topSubInstances.map(x => ({
|
type ChartData = {
|
||||||
|
name: string,
|
||||||
|
color: string | null,
|
||||||
|
value: number,
|
||||||
|
onClick?: () => void,
|
||||||
|
}[];
|
||||||
|
|
||||||
|
const subs: ChartData = fedStats.topSubInstances.map(x => ({
|
||||||
name: x.host,
|
name: x.host,
|
||||||
color: x.themeColor,
|
color: x.themeColor,
|
||||||
value: x.followersCount,
|
value: x.followersCount,
|
||||||
onClick: () => {
|
onClick: () => {
|
||||||
os.pageWindow(`/instance-info/${x.host}`);
|
os.pageWindow(`/instance-info/${x.host}`);
|
||||||
},
|
},
|
||||||
})).concat([{ name: '(other)', color: '#80808080', value: fedStats.otherFollowersCount }]));
|
}));
|
||||||
|
|
||||||
createDoughnut(pubDoughnutEl.value, externalTooltipHandler2, fedStats.topPubInstances.map(x => ({
|
subs.push({
|
||||||
|
name: '(other)',
|
||||||
|
color: '#80808080',
|
||||||
|
value: fedStats.otherFollowersCount,
|
||||||
|
});
|
||||||
|
|
||||||
|
createDoughnut(subDoughnutEl.value, externalTooltipHandler1, subs);
|
||||||
|
|
||||||
|
const pubs: ChartData = fedStats.topPubInstances.map(x => ({
|
||||||
name: x.host,
|
name: x.host,
|
||||||
color: x.themeColor,
|
color: x.themeColor,
|
||||||
value: x.followingCount,
|
value: x.followingCount,
|
||||||
onClick: () => {
|
onClick: () => {
|
||||||
os.pageWindow(`/instance-info/${x.host}`);
|
os.pageWindow(`/instance-info/${x.host}`);
|
||||||
},
|
},
|
||||||
})).concat([{ name: '(other)', color: '#80808080', value: fedStats.otherFollowingCount }]));
|
}));
|
||||||
|
|
||||||
|
pubs.push({
|
||||||
|
name: '(other)',
|
||||||
|
color: '#80808080',
|
||||||
|
value: fedStats.otherFollowingCount,
|
||||||
|
});
|
||||||
|
|
||||||
|
createDoughnut(pubDoughnutEl.value, externalTooltipHandler2, pubs);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
|
@ -18,9 +18,9 @@ import { getProxiedImageUrlNullable } from '@/scripts/media-proxy.js';
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
instance?: {
|
instance?: {
|
||||||
faviconUrl?: string
|
faviconUrl?: string | null
|
||||||
name: string
|
name?: string | null
|
||||||
themeColor?: string
|
themeColor?: string | null
|
||||||
}
|
}
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
|
@ -30,7 +30,7 @@ const instance = props.instance ?? {
|
||||||
themeColor: (document.querySelector('meta[name="theme-color-orig"]') as HTMLMetaElement).content,
|
themeColor: (document.querySelector('meta[name="theme-color-orig"]') as HTMLMetaElement).content,
|
||||||
};
|
};
|
||||||
|
|
||||||
const faviconUrl = computed(() => props.instance ? getProxiedImageUrlNullable(props.instance.faviconUrl, 'preview') : getProxiedImageUrlNullable(Instance.iconUrl, 'preview') ?? getProxiedImageUrlNullable(Instance.faviconUrl, 'preview') ?? '/favicon.ico');
|
const faviconUrl = computed(() => props.instance ? getProxiedImageUrlNullable(props.instance.faviconUrl, 'preview') : getProxiedImageUrlNullable(Instance.iconUrl, 'preview') ?? '/favicon.ico');
|
||||||
|
|
||||||
const themeColor = instance.themeColor ?? '#777777';
|
const themeColor = instance.themeColor ?? '#777777';
|
||||||
|
|
||||||
|
|
|
@ -4,7 +4,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
-->
|
-->
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<MkModal ref="modal" v-slot="{ type, maxHeight }" :preferType="preferedModalType" :anchor="anchor" :transparentBg="true" :src="src" @click="modal.close()" @closed="emit('closed')">
|
<MkModal ref="modal" v-slot="{ type, maxHeight }" :preferType="preferedModalType" :anchor="anchor" :transparentBg="true" :src="src" @click="modal?.close()" @closed="emit('closed')">
|
||||||
<div class="szkkfdyq _popup _shadow" :class="{ asDrawer: type === 'drawer' }" :style="{ maxHeight: maxHeight ? maxHeight + 'px' : '' }">
|
<div class="szkkfdyq _popup _shadow" :class="{ asDrawer: type === 'drawer' }" :style="{ maxHeight: maxHeight ? maxHeight + 'px' : '' }">
|
||||||
<div class="main">
|
<div class="main">
|
||||||
<template v-for="item in items" :key="item.text">
|
<template v-for="item in items" :key="item.text">
|
||||||
|
@ -63,7 +63,7 @@ const items = Object.keys(navbarItemDef).filter(k => !menu.includes(k)).map(k =>
|
||||||
}));
|
}));
|
||||||
|
|
||||||
function close() {
|
function close() {
|
||||||
modal.value.close();
|
modal.value?.close();
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|
|
@ -30,6 +30,7 @@ export default {
|
||||||
const contentEl = ref<HTMLElement>();
|
const contentEl = ref<HTMLElement>();
|
||||||
|
|
||||||
function calc() {
|
function calc() {
|
||||||
|
if (contentEl.value == null) return;
|
||||||
const eachLength = contentEl.value.offsetWidth / props.repeat;
|
const eachLength = contentEl.value.offsetWidth / props.repeat;
|
||||||
const factor = 3000;
|
const factor = 3000;
|
||||||
const duration = props.duration / ((1 / eachLength) * factor);
|
const duration = props.duration / ((1 / eachLength) * factor);
|
||||||
|
|
|
@ -52,7 +52,7 @@ const count = computed(() => props.mediaList.filter(media => previewable(media))
|
||||||
let lightbox: PhotoSwipeLightbox | null;
|
let lightbox: PhotoSwipeLightbox | null;
|
||||||
|
|
||||||
const popstateHandler = (): void => {
|
const popstateHandler = (): void => {
|
||||||
if (lightbox.pswp && lightbox.pswp.isOpen === true) {
|
if (lightbox?.pswp && lightbox.pswp.isOpen === true) {
|
||||||
lightbox.pswp.close();
|
lightbox.pswp.close();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
@ -67,7 +67,10 @@ async function calcAspectRatio() {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const ratioMax = (ratio: number) => `${Math.max(ratio, img.properties.width / img.properties.height).toString()} / 1`;
|
const ratioMax = (ratio: number) => {
|
||||||
|
if (img.properties.width == null || img.properties.height == null) return '';
|
||||||
|
return `${Math.max(ratio, img.properties.width / img.properties.height).toString()} / 1`;
|
||||||
|
};
|
||||||
|
|
||||||
switch (defaultStore.state.mediaListWithOneImageAppearance) {
|
switch (defaultStore.state.mediaListWithOneImageAppearance) {
|
||||||
case '16_9':
|
case '16_9':
|
||||||
|
@ -137,7 +140,7 @@ onMounted(() => {
|
||||||
// element is children
|
// element is children
|
||||||
const { element } = itemData;
|
const { element } = itemData;
|
||||||
|
|
||||||
const id = element.dataset.id;
|
const id = element?.dataset.id;
|
||||||
const file = props.mediaList.find(media => media.id === id);
|
const file = props.mediaList.find(media => media.id === id);
|
||||||
if (!file) return;
|
if (!file) return;
|
||||||
|
|
||||||
|
@ -147,14 +150,14 @@ onMounted(() => {
|
||||||
if (file.properties.orientation != null && file.properties.orientation >= 5) {
|
if (file.properties.orientation != null && file.properties.orientation >= 5) {
|
||||||
[itemData.w, itemData.h] = [itemData.h, itemData.w];
|
[itemData.w, itemData.h] = [itemData.h, itemData.w];
|
||||||
}
|
}
|
||||||
itemData.msrc = file.thumbnailUrl;
|
itemData.msrc = file.thumbnailUrl ?? undefined;
|
||||||
itemData.alt = file.comment ?? file.name;
|
itemData.alt = file.comment ?? file.name;
|
||||||
itemData.comment = file.comment ?? file.name;
|
itemData.comment = file.comment ?? file.name;
|
||||||
itemData.thumbCropped = true;
|
itemData.thumbCropped = true;
|
||||||
});
|
});
|
||||||
|
|
||||||
lightbox.on('uiRegister', () => {
|
lightbox.on('uiRegister', () => {
|
||||||
lightbox.pswp.ui.registerElement({
|
lightbox?.pswp?.ui?.registerElement({
|
||||||
name: 'altText',
|
name: 'altText',
|
||||||
className: 'pwsp__alt-text-container',
|
className: 'pwsp__alt-text-container',
|
||||||
appendTo: 'wrapper',
|
appendTo: 'wrapper',
|
||||||
|
@ -163,8 +166,8 @@ onMounted(() => {
|
||||||
textBox.className = 'pwsp__alt-text _acrylic';
|
textBox.className = 'pwsp__alt-text _acrylic';
|
||||||
el.appendChild(textBox);
|
el.appendChild(textBox);
|
||||||
|
|
||||||
pwsp.on('change', (a) => {
|
pwsp.on('change', () => {
|
||||||
textBox.textContent = pwsp.currSlide.data.comment;
|
textBox.textContent = pwsp.currSlide?.data.comment;
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
|
@ -33,6 +33,7 @@ const align = 'left';
|
||||||
const SCROLLBAR_THICKNESS = 16;
|
const SCROLLBAR_THICKNESS = 16;
|
||||||
|
|
||||||
function setPosition() {
|
function setPosition() {
|
||||||
|
if (el.value == null) return;
|
||||||
const rootRect = props.rootElement.getBoundingClientRect();
|
const rootRect = props.rootElement.getBoundingClientRect();
|
||||||
const parentRect = props.targetElement.getBoundingClientRect();
|
const parentRect = props.targetElement.getBoundingClientRect();
|
||||||
const myRect = el.value.getBoundingClientRect();
|
const myRect = el.value.getBoundingClientRect();
|
||||||
|
@ -66,7 +67,7 @@ const ro = new ResizeObserver((entries, observer) => {
|
||||||
});
|
});
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
ro.observe(el.value);
|
if (el.value) ro.observe(el.value);
|
||||||
setPosition();
|
setPosition();
|
||||||
nextTick(() => {
|
nextTick(() => {
|
||||||
setPosition();
|
setPosition();
|
||||||
|
@ -79,7 +80,7 @@ onUnmounted(() => {
|
||||||
|
|
||||||
defineExpose({
|
defineExpose({
|
||||||
checkHit: (ev: MouseEvent) => {
|
checkHit: (ev: MouseEvent) => {
|
||||||
return (ev.target === el.value || el.value.contains(ev.target));
|
return (ev.target === el.value || el.value?.contains(ev.target as Node));
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
|
@ -12,7 +12,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
:style="{ width: (width && !asDrawer) ? width + 'px' : '', maxHeight: maxHeight ? maxHeight + 'px' : '' }"
|
:style="{ width: (width && !asDrawer) ? width + 'px' : '', maxHeight: maxHeight ? maxHeight + 'px' : '' }"
|
||||||
@contextmenu.self="e => e.preventDefault()"
|
@contextmenu.self="e => e.preventDefault()"
|
||||||
>
|
>
|
||||||
<template v-for="(item, i) in items2">
|
<template v-for="(item, i) in (items2 ?? [])">
|
||||||
<div v-if="item.type === 'divider'" role="separator" :class="$style.divider"></div>
|
<div v-if="item.type === 'divider'" role="separator" :class="$style.divider"></div>
|
||||||
<span v-else-if="item.type === 'label'" role="menuitem" :class="[$style.label, $style.item]">
|
<span v-else-if="item.type === 'label'" role="menuitem" :class="[$style.label, $style.item]">
|
||||||
<span style="opacity: 0.7;">{{ item.text }}</span>
|
<span style="opacity: 0.7;">{{ item.text }}</span>
|
||||||
|
@ -54,7 +54,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
<span :class="$style.caret" style="pointer-events: none;"><i class="ti ti-chevron-right ti-fw"></i></span>
|
<span :class="$style.caret" style="pointer-events: none;"><i class="ti ti-chevron-right ti-fw"></i></span>
|
||||||
</div>
|
</div>
|
||||||
</button>
|
</button>
|
||||||
<button v-else :tabindex="i" class="_button" role="menuitem" :class="[$style.item, { [$style.danger]: item.danger, [$style.active]: item.active }]" :disabled="item.active" @click="clicked(item.action, $event)" @mouseenter.passive="onItemMouseEnter(item)" @mouseleave.passive="onItemMouseLeave(item)">
|
<button v-else :tabindex="i" class="_button" role="menuitem" :class="[$style.item, { [$style.danger]: item.danger, [$style.active]: getValue(item.active) }]" :disabled="getValue(item.active)" @click="clicked(item.action, $event)" @mouseenter.passive="onItemMouseEnter(item)" @mouseleave.passive="onItemMouseLeave(item)">
|
||||||
<i v-if="item.icon" class="ti-fw" :class="[$style.icon, item.icon]"></i>
|
<i v-if="item.icon" class="ti-fw" :class="[$style.icon, item.icon]"></i>
|
||||||
<MkAvatar v-if="item.avatar" :user="item.avatar" :class="$style.avatar"/>
|
<MkAvatar v-if="item.avatar" :user="item.avatar" :class="$style.avatar"/>
|
||||||
<div :class="$style.item_content">
|
<div :class="$style.item_content">
|
||||||
|
@ -63,18 +63,18 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
</div>
|
</div>
|
||||||
</button>
|
</button>
|
||||||
</template>
|
</template>
|
||||||
<span v-if="items2.length === 0" :class="[$style.none, $style.item]">
|
<span v-if="items2 == null || items2.length === 0" :class="[$style.none, $style.item]">
|
||||||
<span>{{ i18n.ts.none }}</span>
|
<span>{{ i18n.ts.none }}</span>
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="childMenu">
|
<div v-if="childMenu">
|
||||||
<XChild ref="child" :items="childMenu" :targetElement="childTarget" :rootElement="itemsEl" showing @actioned="childActioned" @close="close(false)"/>
|
<XChild ref="child" :items="childMenu" :targetElement="childTarget!" :rootElement="itemsEl!" showing @actioned="childActioned" @close="close(false)"/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { computed, defineAsyncComponent, nextTick, onBeforeUnmount, onMounted, ref, shallowRef, watch } from 'vue';
|
import { ComputedRef, computed, defineAsyncComponent, isRef, nextTick, onBeforeUnmount, onMounted, ref, shallowRef, watch } from 'vue';
|
||||||
import { focusPrev, focusNext } from '@/scripts/focus.js';
|
import { focusPrev, focusNext } from '@/scripts/focus.js';
|
||||||
import MkSwitchButton from '@/components/MkSwitch.button.vue';
|
import MkSwitchButton from '@/components/MkSwitch.button.vue';
|
||||||
import { MenuItem, InnerMenuItem, MenuPending, MenuAction, MenuSwitch, MenuParent } from '@/types/menu.js';
|
import { MenuItem, InnerMenuItem, MenuPending, MenuAction, MenuSwitch, MenuParent } from '@/types/menu.js';
|
||||||
|
@ -104,7 +104,7 @@ const emit = defineEmits<{
|
||||||
|
|
||||||
const itemsEl = shallowRef<HTMLDivElement>();
|
const itemsEl = shallowRef<HTMLDivElement>();
|
||||||
|
|
||||||
const items2 = ref<InnerMenuItem[]>([]);
|
const items2 = ref<InnerMenuItem[]>();
|
||||||
|
|
||||||
const child = shallowRef<InstanceType<typeof XChild>>();
|
const child = shallowRef<InstanceType<typeof XChild>>();
|
||||||
|
|
||||||
|
@ -119,15 +119,15 @@ const childShowingItem = ref<MenuItem | null>();
|
||||||
let preferClick = isTouchUsing || props.asDrawer;
|
let preferClick = isTouchUsing || props.asDrawer;
|
||||||
|
|
||||||
watch(() => props.items, () => {
|
watch(() => props.items, () => {
|
||||||
const items: (MenuItem | MenuPending)[] = [...props.items].filter(item => item !== undefined);
|
const items = [...props.items].filter(item => item !== undefined) as (NonNullable<MenuItem> | MenuPending)[];
|
||||||
|
|
||||||
for (let i = 0; i < items.length; i++) {
|
for (let i = 0; i < items.length; i++) {
|
||||||
const item = items[i];
|
const item = items[i];
|
||||||
|
|
||||||
if (item && 'then' in item) { // if item is Promise
|
if ('then' in item) { // if item is Promise
|
||||||
items[i] = { type: 'pending' };
|
items[i] = { type: 'pending' };
|
||||||
item.then(actualItem => {
|
item.then(actualItem => {
|
||||||
items2.value[i] = actualItem;
|
if (items2.value?.[i]) items2.value[i] = actualItem;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -151,7 +151,7 @@ function childActioned() {
|
||||||
}
|
}
|
||||||
|
|
||||||
const onGlobalMousedown = (event: MouseEvent) => {
|
const onGlobalMousedown = (event: MouseEvent) => {
|
||||||
if (childTarget.value && (event.target === childTarget.value || childTarget.value.contains(event.target))) return;
|
if (childTarget.value && (event.target === childTarget.value || childTarget.value.contains(event.target as Node))) return;
|
||||||
if (child.value && child.value.checkHit(event)) return;
|
if (child.value && child.value.checkHit(event)) return;
|
||||||
closeChild();
|
closeChild();
|
||||||
};
|
};
|
||||||
|
@ -169,7 +169,7 @@ function onItemMouseLeave(item) {
|
||||||
}
|
}
|
||||||
|
|
||||||
async function showChildren(item: MenuParent, ev: MouseEvent) {
|
async function showChildren(item: MenuParent, ev: MouseEvent) {
|
||||||
const children = await (async () => {
|
const children: MenuItem[] = await (async () => {
|
||||||
if (childrenCache.has(item)) {
|
if (childrenCache.has(item)) {
|
||||||
return childrenCache.get(item)!;
|
return childrenCache.get(item)!;
|
||||||
} else {
|
} else {
|
||||||
|
@ -189,7 +189,7 @@ async function showChildren(item: MenuParent, ev: MouseEvent) {
|
||||||
});
|
});
|
||||||
emit('hide');
|
emit('hide');
|
||||||
} else {
|
} else {
|
||||||
childTarget.value = ev.currentTarget ?? ev.target;
|
childTarget.value = (ev.currentTarget ?? ev.target) as HTMLElement;
|
||||||
// これでもリアクティビティは保たれる
|
// これでもリアクティビティは保たれる
|
||||||
childMenu.value = children;
|
childMenu.value = children;
|
||||||
childShowingItem.value = item;
|
childShowingItem.value = item;
|
||||||
|
@ -218,6 +218,10 @@ function switchItem(item: MenuSwitch & { ref: any }) {
|
||||||
item.ref = !item.ref;
|
item.ref = !item.ref;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function getValue<T>(item?: ComputedRef<T> | T) {
|
||||||
|
return isRef(item) ? item.value : item;
|
||||||
|
}
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
if (props.viaKeyboard) {
|
if (props.viaKeyboard) {
|
||||||
nextTick(() => {
|
nextTick(() => {
|
||||||
|
|
|
@ -22,8 +22,8 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
stroke-width="2"
|
stroke-width="2"
|
||||||
/>
|
/>
|
||||||
<circle
|
<circle
|
||||||
:cx="headX"
|
:cx="headX ?? undefined"
|
||||||
:cy="headY"
|
:cy="headY ?? undefined"
|
||||||
r="3"
|
r="3"
|
||||||
:fill="color"
|
:fill="color"
|
||||||
/>
|
/>
|
||||||
|
|
|
@ -51,7 +51,7 @@ const bodyWidth = ref(0);
|
||||||
const bodyHeight = ref(0);
|
const bodyHeight = ref(0);
|
||||||
|
|
||||||
const close = () => {
|
const close = () => {
|
||||||
modal.value.close();
|
modal.value?.close();
|
||||||
};
|
};
|
||||||
|
|
||||||
const onBgClick = () => {
|
const onBgClick = () => {
|
||||||
|
@ -67,11 +67,13 @@ const onKeydown = (evt) => {
|
||||||
};
|
};
|
||||||
|
|
||||||
const ro = new ResizeObserver((entries, observer) => {
|
const ro = new ResizeObserver((entries, observer) => {
|
||||||
|
if (rootEl.value == null || headerEl.value == null) return;
|
||||||
bodyWidth.value = rootEl.value.offsetWidth;
|
bodyWidth.value = rootEl.value.offsetWidth;
|
||||||
bodyHeight.value = rootEl.value.offsetHeight - headerEl.value.offsetHeight;
|
bodyHeight.value = rootEl.value.offsetHeight - headerEl.value.offsetHeight;
|
||||||
});
|
});
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
|
if (rootEl.value == null || headerEl.value == null) return;
|
||||||
bodyWidth.value = rootEl.value.offsetWidth;
|
bodyWidth.value = rootEl.value.offsetWidth;
|
||||||
bodyHeight.value = rootEl.value.offsetHeight - headerEl.value.offsetHeight;
|
bodyHeight.value = rootEl.value.offsetHeight - headerEl.value.offsetHeight;
|
||||||
ro.observe(rootEl.value);
|
ro.observe(rootEl.value);
|
||||||
|
|
|
@ -7,7 +7,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
<div
|
<div
|
||||||
v-if="muted === false"
|
v-if="muted === false"
|
||||||
v-show="!isDeleted"
|
v-show="!isDeleted"
|
||||||
ref="el"
|
ref="rootEl"
|
||||||
v-hotkey="keymap"
|
v-hotkey="keymap"
|
||||||
:class="[$style.root, { [$style.showActionsOnlyHover]: defaultStore.state.showNoteActionsOnlyHover }]"
|
:class="[$style.root, { [$style.showActionsOnlyHover]: defaultStore.state.showNoteActionsOnlyHover }]"
|
||||||
:tabindex="!isDeleted ? '-1' : undefined"
|
:tabindex="!isDeleted ? '-1' : undefined"
|
||||||
|
@ -72,16 +72,16 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
/>
|
/>
|
||||||
<div v-if="translating || translation" :class="$style.translation">
|
<div v-if="translating || translation" :class="$style.translation">
|
||||||
<MkLoading v-if="translating" mini/>
|
<MkLoading v-if="translating" mini/>
|
||||||
<div v-else>
|
<div v-else-if="translation">
|
||||||
<b>{{ i18n.tsx.translatedFrom({ x: translation.sourceLang }) }}: </b>
|
<b>{{ i18n.tsx.translatedFrom({ x: translation.sourceLang }) }}: </b>
|
||||||
<Mfm :text="translation.text" :author="appearNote.user" :nyaize="'respect'" :emojiUrls="appearNote.emojis"/>
|
<Mfm :text="translation.text" :author="appearNote.user" :nyaize="'respect'" :emojiUrls="appearNote.emojis"/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="appearNote.files.length > 0">
|
<div v-if="appearNote.files && appearNote.files.length > 0">
|
||||||
<MkMediaList :mediaList="appearNote.files"/>
|
<MkMediaList :mediaList="appearNote.files"/>
|
||||||
</div>
|
</div>
|
||||||
<MkPoll v-if="appearNote.poll" :note="appearNote" :class="$style.poll"/>
|
<MkPoll v-if="appearNote.poll" :noteId="appearNote.id" :poll="appearNote.poll" :class="$style.poll"/>
|
||||||
<MkUrlPreview v-for="url in urls" :key="url" :url="url" :compact="true" :detail="false" :class="$style.urlPreview"/>
|
<MkUrlPreview v-for="url in urls" :key="url" :url="url" :compact="true" :detail="false" :class="$style.urlPreview"/>
|
||||||
<div v-if="appearNote.renote" :class="$style.quote"><MkNoteSimple :note="appearNote.renote" :class="$style.quoteNote"/></div>
|
<div v-if="appearNote.renote" :class="$style.quote"><MkNoteSimple :note="appearNote.renote" :class="$style.quoteNote"/></div>
|
||||||
<button v-if="isLong && collapsed" :class="$style.collapsed" class="_button" @click="collapsed = false">
|
<button v-if="isLong && collapsed" :class="$style.collapsed" class="_button" @click="collapsed = false">
|
||||||
|
@ -126,7 +126,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
<button v-if="defaultStore.state.showClipButtonInNoteFooter" ref="clipButton" :class="$style.footerButton" class="_button" @mousedown="clip()">
|
<button v-if="defaultStore.state.showClipButtonInNoteFooter" ref="clipButton" :class="$style.footerButton" class="_button" @mousedown="clip()">
|
||||||
<i class="ti ti-paperclip"></i>
|
<i class="ti ti-paperclip"></i>
|
||||||
</button>
|
</button>
|
||||||
<button ref="menuButton" :class="$style.footerButton" class="_button" @mousedown="menu()">
|
<button ref="menuButton" :class="$style.footerButton" class="_button" @mousedown="showMenu()">
|
||||||
<i class="ti ti-dots"></i>
|
<i class="ti ti-dots"></i>
|
||||||
</button>
|
</button>
|
||||||
</footer>
|
</footer>
|
||||||
|
@ -215,7 +215,7 @@ if (noteViewInterruptors.length > 0) {
|
||||||
let result: Misskey.entities.Note | null = deepClone(note.value);
|
let result: Misskey.entities.Note | null = deepClone(note.value);
|
||||||
for (const interruptor of noteViewInterruptors) {
|
for (const interruptor of noteViewInterruptors) {
|
||||||
try {
|
try {
|
||||||
result = await interruptor.handler(result);
|
result = await interruptor.handler(result!) as Misskey.entities.Note | null;
|
||||||
if (result === null) {
|
if (result === null) {
|
||||||
isDeleted.value = true;
|
isDeleted.value = true;
|
||||||
return;
|
return;
|
||||||
|
@ -224,7 +224,7 @@ if (noteViewInterruptors.length > 0) {
|
||||||
console.error(err);
|
console.error(err);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
note.value = result;
|
note.value = result as Misskey.entities.Note;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -232,11 +232,11 @@ const isRenote = (
|
||||||
note.value.renote != null &&
|
note.value.renote != null &&
|
||||||
note.value.text == null &&
|
note.value.text == null &&
|
||||||
note.value.cw == null &&
|
note.value.cw == null &&
|
||||||
note.value.fileIds.length === 0 &&
|
note.value.fileIds && note.value.fileIds.length === 0 &&
|
||||||
note.value.poll == null
|
note.value.poll == null
|
||||||
);
|
);
|
||||||
|
|
||||||
const el = shallowRef<HTMLElement>();
|
const rootEl = shallowRef<HTMLElement>();
|
||||||
const menuButton = shallowRef<HTMLElement>();
|
const menuButton = shallowRef<HTMLElement>();
|
||||||
const renoteButton = shallowRef<HTMLElement>();
|
const renoteButton = shallowRef<HTMLElement>();
|
||||||
const renoteTime = shallowRef<HTMLElement>();
|
const renoteTime = shallowRef<HTMLElement>();
|
||||||
|
@ -254,8 +254,8 @@ const muted = ref(checkMute(appearNote.value, $i?.mutedWords));
|
||||||
const translation = ref<Misskey.entities.NotesTranslateResponse | null>(null);
|
const translation = ref<Misskey.entities.NotesTranslateResponse | null>(null);
|
||||||
const translating = ref(false);
|
const translating = ref(false);
|
||||||
const showTicker = (defaultStore.state.instanceTicker === 'always') || (defaultStore.state.instanceTicker === 'remote' && appearNote.value.user.instance);
|
const showTicker = (defaultStore.state.instanceTicker === 'always') || (defaultStore.state.instanceTicker === 'remote' && appearNote.value.user.instance);
|
||||||
const canRenote = computed(() => ['public', 'home'].includes(appearNote.value.visibility) || (appearNote.value.visibility === 'followers' && appearNote.value.userId === $i.id));
|
const canRenote = computed(() => ['public', 'home'].includes(appearNote.value.visibility) || (appearNote.value.visibility === 'followers' && appearNote.value.userId === $i?.id));
|
||||||
const renoteCollapsed = ref(defaultStore.state.collapseRenotes && isRenote && (($i && ($i.id === note.value.userId || $i.id === appearNote.value.userId)) || (appearNote.value.myReaction != null)));
|
const renoteCollapsed = ref(defaultStore.state.collapseRenotes && isRenote && (($i && ($i.id === note.value.userId || $i.id === appearNote.value.userId)) ?? (appearNote.value.myReaction != null)));
|
||||||
const hideMutedNotes = defaultStore.state.hideMutedNotes;
|
const hideMutedNotes = defaultStore.state.hideMutedNotes;
|
||||||
|
|
||||||
/* Overload FunctionにLintが対応していないのでコメントアウト
|
/* Overload FunctionにLintが対応していないのでコメントアウト
|
||||||
|
@ -278,11 +278,11 @@ function checkMute(noteToCheck: Misskey.entities.Note, mutedWords: Array<string
|
||||||
const keymap = {
|
const keymap = {
|
||||||
'r': () => reply(true),
|
'r': () => reply(true),
|
||||||
'e|a|plus': () => react(true),
|
'e|a|plus': () => react(true),
|
||||||
'q': () => renoteButton.value.renote(true),
|
'q': () => renote(true),
|
||||||
'up|k|shift+tab': focusBefore,
|
'up|k|shift+tab': focusBefore,
|
||||||
'down|j|tab': focusAfter,
|
'down|j|tab': focusAfter,
|
||||||
'esc': blur,
|
'esc': blur,
|
||||||
'm|o': () => menu(true),
|
'm|o': () => showMenu(true),
|
||||||
's': () => showContent.value !== showContent.value,
|
's': () => showContent.value !== showContent.value,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -299,7 +299,7 @@ if (props.mock) {
|
||||||
}, { deep: true });
|
}, { deep: true });
|
||||||
} else {
|
} else {
|
||||||
useNoteCapture({
|
useNoteCapture({
|
||||||
rootEl: el,
|
rootEl: rootEl,
|
||||||
note: appearNote,
|
note: appearNote,
|
||||||
pureNote: note,
|
pureNote: note,
|
||||||
isDeletedRef: isDeleted,
|
isDeletedRef: isDeleted,
|
||||||
|
@ -345,7 +345,7 @@ function reply(viaKeyboard = false): void {
|
||||||
reply: appearNote.value,
|
reply: appearNote.value,
|
||||||
channel: appearNote.value.channel,
|
channel: appearNote.value.channel,
|
||||||
animation: !viaKeyboard,
|
animation: !viaKeyboard,
|
||||||
}, () => {
|
}).then(() => {
|
||||||
focus();
|
focus();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -364,7 +364,7 @@ function react(viaKeyboard = false): void {
|
||||||
noteId: appearNote.value.id,
|
noteId: appearNote.value.id,
|
||||||
reaction: '❤️',
|
reaction: '❤️',
|
||||||
});
|
});
|
||||||
const el = reactButton.value as HTMLElement | null | undefined;
|
const el = reactButton.value;
|
||||||
if (el) {
|
if (el) {
|
||||||
const rect = el.getBoundingClientRect();
|
const rect = el.getBoundingClientRect();
|
||||||
const x = rect.left + (el.offsetWidth / 2);
|
const x = rect.left + (el.offsetWidth / 2);
|
||||||
|
@ -373,7 +373,7 @@ function react(viaKeyboard = false): void {
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
blur();
|
blur();
|
||||||
reactionPicker.show(reactButton.value, reaction => {
|
reactionPicker.show(reactButton.value ?? null, reaction => {
|
||||||
sound.playMisskeySfx('reaction');
|
sound.playMisskeySfx('reaction');
|
||||||
|
|
||||||
if (props.mock) {
|
if (props.mock) {
|
||||||
|
@ -394,8 +394,8 @@ function react(viaKeyboard = false): void {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function undoReact(note): void {
|
function undoReact(targetNote: Misskey.entities.Note): void {
|
||||||
const oldReaction = note.myReaction;
|
const oldReaction = targetNote.myReaction;
|
||||||
if (!oldReaction) return;
|
if (!oldReaction) return;
|
||||||
|
|
||||||
if (props.mock) {
|
if (props.mock) {
|
||||||
|
@ -404,7 +404,7 @@ function undoReact(note): void {
|
||||||
}
|
}
|
||||||
|
|
||||||
misskeyApi('notes/reactions/delete', {
|
misskeyApi('notes/reactions/delete', {
|
||||||
noteId: note.id,
|
noteId: targetNote.id,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -413,32 +413,34 @@ function onContextmenu(ev: MouseEvent): void {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const isLink = (el: HTMLElement) => {
|
const isLink = (el: HTMLElement): boolean => {
|
||||||
if (el.tagName === 'A') return true;
|
if (el.tagName === 'A') return true;
|
||||||
// 再生速度の選択などのために、Audio要素のコンテキストメニューはブラウザデフォルトとする。
|
// 再生速度の選択などのために、Audio要素のコンテキストメニューはブラウザデフォルトとする。
|
||||||
if (el.tagName === 'AUDIO') return true;
|
if (el.tagName === 'AUDIO') return true;
|
||||||
if (el.parentElement) {
|
if (el.parentElement) {
|
||||||
return isLink(el.parentElement);
|
return isLink(el.parentElement);
|
||||||
}
|
}
|
||||||
|
return false;
|
||||||
};
|
};
|
||||||
if (isLink(ev.target)) return;
|
|
||||||
if (window.getSelection().toString() !== '') return;
|
if (ev.target && isLink(ev.target as HTMLElement)) return;
|
||||||
|
if (window.getSelection()?.toString() !== '') return;
|
||||||
|
|
||||||
if (defaultStore.state.useReactionPickerForContextMenu) {
|
if (defaultStore.state.useReactionPickerForContextMenu) {
|
||||||
ev.preventDefault();
|
ev.preventDefault();
|
||||||
react();
|
react();
|
||||||
} else {
|
} else {
|
||||||
const { menu, cleanup } = getNoteMenu({ note: note.value, translating, translation, menuButton, isDeleted, currentClip: currentClip?.value });
|
const { menu, cleanup } = getNoteMenu({ note: note.value, translating, translation, isDeleted, currentClip: currentClip?.value });
|
||||||
os.contextMenu(menu, ev).then(focus).finally(cleanup);
|
os.contextMenu(menu, ev).then(focus).finally(cleanup);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function menu(viaKeyboard = false): void {
|
function showMenu(viaKeyboard = false): void {
|
||||||
if (props.mock) {
|
if (props.mock) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const { menu, cleanup } = getNoteMenu({ note: note.value, translating, translation, menuButton, isDeleted, currentClip: currentClip?.value });
|
const { menu, cleanup } = getNoteMenu({ note: note.value, translating, translation, isDeleted, currentClip: currentClip?.value });
|
||||||
os.popupMenu(menu, menuButton.value, {
|
os.popupMenu(menu, menuButton.value, {
|
||||||
viaKeyboard,
|
viaKeyboard,
|
||||||
}).then(focus).finally(cleanup);
|
}).then(focus).finally(cleanup);
|
||||||
|
@ -485,7 +487,7 @@ function showRenoteMenu(viaKeyboard = false): void {
|
||||||
getCopyNoteLinkMenu(note.value, i18n.ts.copyLinkRenote),
|
getCopyNoteLinkMenu(note.value, i18n.ts.copyLinkRenote),
|
||||||
{ type: 'divider' },
|
{ type: 'divider' },
|
||||||
getAbuseNoteMenu(note.value, i18n.ts.reportAbuseRenote),
|
getAbuseNoteMenu(note.value, i18n.ts.reportAbuseRenote),
|
||||||
$i.isModerator || $i.isAdmin ? getUnrenote() : undefined,
|
($i?.isModerator || $i?.isAdmin) ? getUnrenote() : undefined,
|
||||||
], renoteTime.value, {
|
], renoteTime.value, {
|
||||||
viaKeyboard: viaKeyboard,
|
viaKeyboard: viaKeyboard,
|
||||||
});
|
});
|
||||||
|
@ -493,19 +495,19 @@ function showRenoteMenu(viaKeyboard = false): void {
|
||||||
}
|
}
|
||||||
|
|
||||||
function focus() {
|
function focus() {
|
||||||
el.value.focus();
|
rootEl.value?.focus();
|
||||||
}
|
}
|
||||||
|
|
||||||
function blur() {
|
function blur() {
|
||||||
el.value.blur();
|
rootEl.value?.blur();
|
||||||
}
|
}
|
||||||
|
|
||||||
function focusBefore() {
|
function focusBefore() {
|
||||||
focusPrev(el.value);
|
focusPrev(rootEl.value ?? null);
|
||||||
}
|
}
|
||||||
|
|
||||||
function focusAfter() {
|
function focusAfter() {
|
||||||
focusNext(el.value);
|
focusNext(rootEl.value ?? null);
|
||||||
}
|
}
|
||||||
|
|
||||||
function readPromo() {
|
function readPromo() {
|
||||||
|
|
|
@ -7,7 +7,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
<div
|
<div
|
||||||
v-if="!muted"
|
v-if="!muted"
|
||||||
v-show="!isDeleted"
|
v-show="!isDeleted"
|
||||||
ref="el"
|
ref="rootEl"
|
||||||
v-hotkey="keymap"
|
v-hotkey="keymap"
|
||||||
:class="$style.root"
|
:class="$style.root"
|
||||||
>
|
>
|
||||||
|
@ -86,15 +86,15 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
<a v-if="appearNote.renote != null" :class="$style.rn">RN:</a>
|
<a v-if="appearNote.renote != null" :class="$style.rn">RN:</a>
|
||||||
<div v-if="translating || translation" :class="$style.translation">
|
<div v-if="translating || translation" :class="$style.translation">
|
||||||
<MkLoading v-if="translating" mini/>
|
<MkLoading v-if="translating" mini/>
|
||||||
<div v-else>
|
<div v-else-if="translation">
|
||||||
<b>{{ i18n.tsx.translatedFrom({ x: translation.sourceLang }) }}: </b>
|
<b>{{ i18n.tsx.translatedFrom({ x: translation.sourceLang }) }}: </b>
|
||||||
<Mfm :text="translation.text" :author="appearNote.user" :nyaize="'respect'" :emojiUrls="appearNote.emojis"/>
|
<Mfm :text="translation.text" :author="appearNote.user" :nyaize="'respect'" :emojiUrls="appearNote.emojis"/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="appearNote.files.length > 0">
|
<div v-if="appearNote.files && appearNote.files.length > 0">
|
||||||
<MkMediaList :mediaList="appearNote.files"/>
|
<MkMediaList :mediaList="appearNote.files"/>
|
||||||
</div>
|
</div>
|
||||||
<MkPoll v-if="appearNote.poll" ref="pollViewer" :note="appearNote" :class="$style.poll"/>
|
<MkPoll v-if="appearNote.poll" ref="pollViewer" :noteId="appearNote.id" :poll="appearNote.poll" :class="$style.poll"/>
|
||||||
<MkUrlPreview v-for="url in urls" :key="url" :url="url" :compact="true" :detail="true" style="margin-top: 6px;"/>
|
<MkUrlPreview v-for="url in urls" :key="url" :url="url" :compact="true" :detail="true" style="margin-top: 6px;"/>
|
||||||
<div v-if="appearNote.renote" :class="$style.quote"><MkNoteSimple :note="appearNote.renote" :class="$style.quoteNote"/></div>
|
<div v-if="appearNote.renote" :class="$style.quote"><MkNoteSimple :note="appearNote.renote" :class="$style.quoteNote"/></div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -134,7 +134,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
<button v-if="defaultStore.state.showClipButtonInNoteFooter" ref="clipButton" class="_button" :class="$style.noteFooterButton" @mousedown="clip()">
|
<button v-if="defaultStore.state.showClipButtonInNoteFooter" ref="clipButton" class="_button" :class="$style.noteFooterButton" @mousedown="clip()">
|
||||||
<i class="ti ti-paperclip"></i>
|
<i class="ti ti-paperclip"></i>
|
||||||
</button>
|
</button>
|
||||||
<button ref="menuButton" class="_button" :class="$style.noteFooterButton" @mousedown="menu()">
|
<button ref="menuButton" class="_button" :class="$style.noteFooterButton" @mousedown="showMenu()">
|
||||||
<i class="ti ti-dots"></i>
|
<i class="ti ti-dots"></i>
|
||||||
</button>
|
</button>
|
||||||
</footer>
|
</footer>
|
||||||
|
@ -225,7 +225,7 @@ import { claimAchievement } from '@/scripts/achievements.js';
|
||||||
import MkRippleEffect from '@/components/MkRippleEffect.vue';
|
import MkRippleEffect from '@/components/MkRippleEffect.vue';
|
||||||
import { showMovedDialog } from '@/scripts/show-moved-dialog.js';
|
import { showMovedDialog } from '@/scripts/show-moved-dialog.js';
|
||||||
import MkUserCardMini from '@/components/MkUserCardMini.vue';
|
import MkUserCardMini from '@/components/MkUserCardMini.vue';
|
||||||
import MkPagination from '@/components/MkPagination.vue';
|
import MkPagination, { type Paging } from '@/components/MkPagination.vue';
|
||||||
import MkReactionIcon from '@/components/MkReactionIcon.vue';
|
import MkReactionIcon from '@/components/MkReactionIcon.vue';
|
||||||
import MkButton from '@/components/MkButton.vue';
|
import MkButton from '@/components/MkButton.vue';
|
||||||
|
|
||||||
|
@ -243,7 +243,7 @@ if (noteViewInterruptors.length > 0) {
|
||||||
let result: Misskey.entities.Note | null = deepClone(note.value);
|
let result: Misskey.entities.Note | null = deepClone(note.value);
|
||||||
for (const interruptor of noteViewInterruptors) {
|
for (const interruptor of noteViewInterruptors) {
|
||||||
try {
|
try {
|
||||||
result = await interruptor.handler(result);
|
result = await interruptor.handler(result!) as Misskey.entities.Note | null;
|
||||||
if (result === null) {
|
if (result === null) {
|
||||||
isDeleted.value = true;
|
isDeleted.value = true;
|
||||||
return;
|
return;
|
||||||
|
@ -252,18 +252,18 @@ if (noteViewInterruptors.length > 0) {
|
||||||
console.error(err);
|
console.error(err);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
note.value = result;
|
note.value = result as Misskey.entities.Note;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const isRenote = (
|
const isRenote = (
|
||||||
note.value.renote != null &&
|
note.value.renote != null &&
|
||||||
note.value.text == null &&
|
note.value.text == null &&
|
||||||
note.value.fileIds.length === 0 &&
|
note.value.fileIds && note.value.fileIds.length === 0 &&
|
||||||
note.value.poll == null
|
note.value.poll == null
|
||||||
);
|
);
|
||||||
|
|
||||||
const el = shallowRef<HTMLElement>();
|
const rootEl = shallowRef<HTMLElement>();
|
||||||
const menuButton = shallowRef<HTMLElement>();
|
const menuButton = shallowRef<HTMLElement>();
|
||||||
const renoteButton = shallowRef<HTMLElement>();
|
const renoteButton = shallowRef<HTMLElement>();
|
||||||
const renoteTime = shallowRef<HTMLElement>();
|
const renoteTime = shallowRef<HTMLElement>();
|
||||||
|
@ -281,14 +281,14 @@ const urls = parsed ? extractUrlFromMfm(parsed) : null;
|
||||||
const showTicker = (defaultStore.state.instanceTicker === 'always') || (defaultStore.state.instanceTicker === 'remote' && appearNote.value.user.instance);
|
const showTicker = (defaultStore.state.instanceTicker === 'always') || (defaultStore.state.instanceTicker === 'remote' && appearNote.value.user.instance);
|
||||||
const conversation = ref<Misskey.entities.Note[]>([]);
|
const conversation = ref<Misskey.entities.Note[]>([]);
|
||||||
const replies = ref<Misskey.entities.Note[]>([]);
|
const replies = ref<Misskey.entities.Note[]>([]);
|
||||||
const canRenote = computed(() => ['public', 'home'].includes(appearNote.value.visibility) || appearNote.value.userId === $i.id);
|
const canRenote = computed(() => ['public', 'home'].includes(appearNote.value.visibility) || appearNote.value.userId === $i?.id);
|
||||||
|
|
||||||
const keymap = {
|
const keymap = {
|
||||||
'r': () => reply(true),
|
'r': () => reply(true),
|
||||||
'e|a|plus': () => react(true),
|
'e|a|plus': () => react(true),
|
||||||
'q': () => renoteButton.value.renote(true),
|
'q': () => renote(true),
|
||||||
'esc': blur,
|
'esc': blur,
|
||||||
'm|o': () => menu(true),
|
'm|o': () => showMenu(true),
|
||||||
's': () => showContent.value !== showContent.value,
|
's': () => showContent.value !== showContent.value,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -302,7 +302,7 @@ provide('react', (reaction: string) => {
|
||||||
const tab = ref('replies');
|
const tab = ref('replies');
|
||||||
const reactionTabType = ref<string | null>(null);
|
const reactionTabType = ref<string | null>(null);
|
||||||
|
|
||||||
const renotesPagination = computed(() => ({
|
const renotesPagination = computed<Paging>(() => ({
|
||||||
endpoint: 'notes/renotes',
|
endpoint: 'notes/renotes',
|
||||||
limit: 10,
|
limit: 10,
|
||||||
params: {
|
params: {
|
||||||
|
@ -310,7 +310,7 @@ const renotesPagination = computed(() => ({
|
||||||
},
|
},
|
||||||
}));
|
}));
|
||||||
|
|
||||||
const reactionsPagination = computed(() => ({
|
const reactionsPagination = computed<Paging>(() => ({
|
||||||
endpoint: 'notes/reactions',
|
endpoint: 'notes/reactions',
|
||||||
limit: 10,
|
limit: 10,
|
||||||
params: {
|
params: {
|
||||||
|
@ -320,7 +320,7 @@ const reactionsPagination = computed(() => ({
|
||||||
}));
|
}));
|
||||||
|
|
||||||
useNoteCapture({
|
useNoteCapture({
|
||||||
rootEl: el,
|
rootEl: rootEl,
|
||||||
note: appearNote,
|
note: appearNote,
|
||||||
pureNote: note,
|
pureNote: note,
|
||||||
isDeletedRef: isDeleted,
|
isDeletedRef: isDeleted,
|
||||||
|
@ -361,7 +361,7 @@ function reply(viaKeyboard = false): void {
|
||||||
reply: appearNote.value,
|
reply: appearNote.value,
|
||||||
channel: appearNote.value.channel,
|
channel: appearNote.value.channel,
|
||||||
animation: !viaKeyboard,
|
animation: !viaKeyboard,
|
||||||
}, () => {
|
}).then(() => {
|
||||||
focus();
|
focus();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -385,7 +385,7 @@ function react(viaKeyboard = false): void {
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
blur();
|
blur();
|
||||||
reactionPicker.show(reactButton.value, reaction => {
|
reactionPicker.show(reactButton.value ?? null, reaction => {
|
||||||
sound.playMisskeySfx('reaction');
|
sound.playMisskeySfx('reaction');
|
||||||
|
|
||||||
misskeyApi('notes/reactions/create', {
|
misskeyApi('notes/reactions/create', {
|
||||||
|
@ -410,26 +410,28 @@ function undoReact(note): void {
|
||||||
}
|
}
|
||||||
|
|
||||||
function onContextmenu(ev: MouseEvent): void {
|
function onContextmenu(ev: MouseEvent): void {
|
||||||
const isLink = (el: HTMLElement) => {
|
const isLink = (el: HTMLElement): boolean => {
|
||||||
if (el.tagName === 'A') return true;
|
if (el.tagName === 'A') return true;
|
||||||
if (el.parentElement) {
|
if (el.parentElement) {
|
||||||
return isLink(el.parentElement);
|
return isLink(el.parentElement);
|
||||||
}
|
}
|
||||||
|
return false;
|
||||||
};
|
};
|
||||||
if (isLink(ev.target)) return;
|
|
||||||
if (window.getSelection().toString() !== '') return;
|
if (ev.target && isLink(ev.target as HTMLElement)) return;
|
||||||
|
if (window.getSelection()?.toString() !== '') return;
|
||||||
|
|
||||||
if (defaultStore.state.useReactionPickerForContextMenu) {
|
if (defaultStore.state.useReactionPickerForContextMenu) {
|
||||||
ev.preventDefault();
|
ev.preventDefault();
|
||||||
react();
|
react();
|
||||||
} else {
|
} else {
|
||||||
const { menu, cleanup } = getNoteMenu({ note: note.value, translating, translation, menuButton, isDeleted });
|
const { menu, cleanup } = getNoteMenu({ note: note.value, translating, translation, isDeleted });
|
||||||
os.contextMenu(menu, ev).then(focus).finally(cleanup);
|
os.contextMenu(menu, ev).then(focus).finally(cleanup);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function menu(viaKeyboard = false): void {
|
function showMenu(viaKeyboard = false): void {
|
||||||
const { menu, cleanup } = getNoteMenu({ note: note.value, translating, translation, menuButton, isDeleted });
|
const { menu, cleanup } = getNoteMenu({ note: note.value, translating, translation, isDeleted });
|
||||||
os.popupMenu(menu, menuButton.value, {
|
os.popupMenu(menu, menuButton.value, {
|
||||||
viaKeyboard,
|
viaKeyboard,
|
||||||
}).then(focus).finally(cleanup);
|
}).then(focus).finally(cleanup);
|
||||||
|
@ -458,11 +460,11 @@ function showRenoteMenu(viaKeyboard = false): void {
|
||||||
}
|
}
|
||||||
|
|
||||||
function focus() {
|
function focus() {
|
||||||
el.value.focus();
|
rootEl.value?.focus();
|
||||||
}
|
}
|
||||||
|
|
||||||
function blur() {
|
function blur() {
|
||||||
el.value.blur();
|
rootEl.value?.blur();
|
||||||
}
|
}
|
||||||
|
|
||||||
const repliesLoaded = ref(false);
|
const repliesLoaded = ref(false);
|
||||||
|
@ -481,6 +483,7 @@ const conversationLoaded = ref(false);
|
||||||
|
|
||||||
function loadConversation() {
|
function loadConversation() {
|
||||||
conversationLoaded.value = true;
|
conversationLoaded.value = true;
|
||||||
|
if (appearNote.value.replyId == null) return;
|
||||||
misskeyApi('notes/conversation', {
|
misskeyApi('notes/conversation', {
|
||||||
noteId: appearNote.value.replyId,
|
noteId: appearNote.value.replyId,
|
||||||
}).then(res => {
|
}).then(res => {
|
||||||
|
|
|
@ -14,7 +14,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
<div v-if="note.user.isBot" :class="$style.isBot">bot</div>
|
<div v-if="note.user.isBot" :class="$style.isBot">bot</div>
|
||||||
<div :class="$style.username"><MkAcct :user="note.user"/></div>
|
<div :class="$style.username"><MkAcct :user="note.user"/></div>
|
||||||
<div v-if="note.user.badgeRoles" :class="$style.badgeRoles">
|
<div v-if="note.user.badgeRoles" :class="$style.badgeRoles">
|
||||||
<img v-for="role in note.user.badgeRoles" :key="role.id" v-tooltip="role.name" :class="$style.badgeRole" :src="role.iconUrl"/>
|
<img v-for="(role, i) in note.user.badgeRoles" :key="i" v-tooltip="role.name" :class="$style.badgeRole" :src="role.iconUrl!"/>
|
||||||
</div>
|
</div>
|
||||||
<div :class="$style.info">
|
<div :class="$style.info">
|
||||||
<div v-if="mock">
|
<div v-if="mock">
|
||||||
|
|
|
@ -12,7 +12,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<p v-if="useCw" :class="$style.cw">
|
<p v-if="useCw" :class="$style.cw">
|
||||||
<Mfm v-if="cw != ''" :text="cw" :author="user" :nyaize="'respect'" :i="user" style="margin-right: 8px;"/>
|
<Mfm v-if="cw != null && cw != ''" :text="cw" :author="user" :nyaize="'respect'" :i="user" style="margin-right: 8px;"/>
|
||||||
<MkCwButton v-model="showContent" :text="text.trim()" :files="files" :poll="poll" style="margin: 4px 0;"/>
|
<MkCwButton v-model="showContent" :text="text.trim()" :files="files" :poll="poll" style="margin: 4px 0;"/>
|
||||||
</p>
|
</p>
|
||||||
<div v-show="!useCw || showContent">
|
<div v-show="!useCw || showContent">
|
||||||
|
@ -26,6 +26,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { ref } from 'vue';
|
import { ref } from 'vue';
|
||||||
import * as Misskey from 'misskey-js';
|
import * as Misskey from 'misskey-js';
|
||||||
|
import type { PollEditorModelValue } from '@/components/MkPollEditor.vue';
|
||||||
import MkCwButton from '@/components/MkCwButton.vue';
|
import MkCwButton from '@/components/MkCwButton.vue';
|
||||||
|
|
||||||
const showContent = ref(false);
|
const showContent = ref(false);
|
||||||
|
@ -33,12 +34,7 @@ const showContent = ref(false);
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
text: string;
|
text: string;
|
||||||
files: Misskey.entities.DriveFile[];
|
files: Misskey.entities.DriveFile[];
|
||||||
poll?: {
|
poll?: PollEditorModelValue;
|
||||||
choices: string[];
|
|
||||||
multiple: boolean;
|
|
||||||
expiresAt: string | null;
|
|
||||||
expiredAfter: string | null;
|
|
||||||
};
|
|
||||||
useCw: boolean;
|
useCw: boolean;
|
||||||
cw: string | null;
|
cw: string | null;
|
||||||
user: Misskey.entities.User;
|
user: Misskey.entities.User;
|
||||||
|
|
|
@ -6,10 +6,8 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
<template>
|
<template>
|
||||||
<div :class="$style.root">
|
<div :class="$style.root">
|
||||||
<div :class="$style.head">
|
<div :class="$style.head">
|
||||||
<MkAvatar v-if="notification.type === 'pollEnded'" :class="$style.icon" :user="notification.note.user" link preview/>
|
<MkAvatar v-if="['pollEnded', 'note'].includes(notification.type) && notification.note" :class="$style.icon" :user="notification.note.user" link preview/>
|
||||||
<MkAvatar v-else-if="notification.type === 'note'" :class="$style.icon" :user="notification.note.user" link preview/>
|
<MkAvatar v-else-if="['roleAssigned', 'achievementEarned'].includes(notification.type)" :class="$style.icon" :user="$i" link preview/>
|
||||||
<MkAvatar v-else-if="notification.type === 'roleAssigned'" :class="$style.icon" :user="$i" link preview/>
|
|
||||||
<MkAvatar v-else-if="notification.type === 'achievementEarned'" :class="$style.icon" :user="$i" link preview/>
|
|
||||||
<div v-else-if="notification.type === 'reaction:grouped'" :class="[$style.icon, $style.icon_reactionGroup]"><i class="ti ti-plus" style="line-height: 1;"></i></div>
|
<div v-else-if="notification.type === 'reaction:grouped'" :class="[$style.icon, $style.icon_reactionGroup]"><i class="ti ti-plus" style="line-height: 1;"></i></div>
|
||||||
<div v-else-if="notification.type === 'renote:grouped'" :class="[$style.icon, $style.icon_renoteGroup]"><i class="ti ti-repeat" style="line-height: 1;"></i></div>
|
<div v-else-if="notification.type === 'renote:grouped'" :class="[$style.icon, $style.icon_renoteGroup]"><i class="ti ti-repeat" style="line-height: 1;"></i></div>
|
||||||
<img v-else-if="notification.type === 'test'" :class="$style.icon" :src="infoImageUrl"/>
|
<img v-else-if="notification.type === 'test'" :class="$style.icon" :src="infoImageUrl"/>
|
||||||
|
@ -26,6 +24,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
[$style.t_quote]: notification.type === 'quote',
|
[$style.t_quote]: notification.type === 'quote',
|
||||||
[$style.t_pollEnded]: notification.type === 'pollEnded',
|
[$style.t_pollEnded]: notification.type === 'pollEnded',
|
||||||
[$style.t_achievementEarned]: notification.type === 'achievementEarned',
|
[$style.t_achievementEarned]: notification.type === 'achievementEarned',
|
||||||
|
[$style.t_roleAssigned]: notification.type === 'roleAssigned' && notification.role.iconUrl == null,
|
||||||
}]"
|
}]"
|
||||||
>
|
>
|
||||||
<i v-if="notification.type === 'follow'" class="ti ti-plus"></i>
|
<i v-if="notification.type === 'follow'" class="ti ti-plus"></i>
|
||||||
|
@ -37,12 +36,14 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
<i v-else-if="notification.type === 'quote'" class="ti ti-quote"></i>
|
<i v-else-if="notification.type === 'quote'" class="ti ti-quote"></i>
|
||||||
<i v-else-if="notification.type === 'pollEnded'" class="ti ti-chart-arrows"></i>
|
<i v-else-if="notification.type === 'pollEnded'" class="ti ti-chart-arrows"></i>
|
||||||
<i v-else-if="notification.type === 'achievementEarned'" class="ti ti-medal"></i>
|
<i v-else-if="notification.type === 'achievementEarned'" class="ti ti-medal"></i>
|
||||||
<img v-else-if="notification.type === 'roleAssigned'" style="height: 1.3em; vertical-align: -22%;" :src="notification.role.iconUrl" alt=""/>
|
<template v-else-if="notification.type === 'roleAssigned'">
|
||||||
<!-- notification.reaction が null になることはまずないが、ここでoptional chaining使うと一部ブラウザで刺さるので念の為 -->
|
<img v-if="notification.role.iconUrl" style="height: 1.3em; vertical-align: -22%;" :src="notification.role.iconUrl" alt=""/>
|
||||||
|
<i v-else class="ti ti-badges"></i>
|
||||||
|
</template>
|
||||||
<MkReactionIcon
|
<MkReactionIcon
|
||||||
v-else-if="notification.type === 'reaction'"
|
v-else-if="notification.type === 'reaction'"
|
||||||
:withTooltip="true"
|
:withTooltip="true"
|
||||||
:reaction="notification.reaction ? notification.reaction.replace(/^:(\w+):$/, ':$1@.:') : notification.reaction"
|
:reaction="notification.reaction.replace(/^:(\w+):$/, ':$1@.:')"
|
||||||
:noStyle="true"
|
:noStyle="true"
|
||||||
style="width: 100%; height: 100%;"
|
style="width: 100%; height: 100%;"
|
||||||
/>
|
/>
|
||||||
|
@ -55,10 +56,10 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
<span v-else-if="notification.type === 'roleAssigned'">{{ i18n.ts._notification.roleAssigned }}</span>
|
<span v-else-if="notification.type === 'roleAssigned'">{{ i18n.ts._notification.roleAssigned }}</span>
|
||||||
<span v-else-if="notification.type === 'achievementEarned'">{{ i18n.ts._notification.achievementEarned }}</span>
|
<span v-else-if="notification.type === 'achievementEarned'">{{ i18n.ts._notification.achievementEarned }}</span>
|
||||||
<span v-else-if="notification.type === 'test'">{{ i18n.ts._notification.testNotification }}</span>
|
<span v-else-if="notification.type === 'test'">{{ i18n.ts._notification.testNotification }}</span>
|
||||||
<MkA v-else-if="notification.user" v-user-preview="notification.user.id" :class="$style.headerName" :to="userPage(notification.user)"><MkUserName :user="notification.user"/></MkA>
|
<MkA v-else-if="notification.type === 'follow' || notification.type === 'mention' || notification.type === 'reply' || notification.type === 'renote' || notification.type === 'quote' || notification.type === 'reaction' || notification.type === 'receiveFollowRequest' || notification.type === 'followRequestAccepted'" v-user-preview="notification.user.id" :class="$style.headerName" :to="userPage(notification.user)"><MkUserName :user="notification.user"/></MkA>
|
||||||
<span v-else-if="notification.type === 'reaction:grouped'">{{ i18n.tsx._notification.reactedBySomeUsers({ n: notification.reactions.length }) }}</span>
|
<span v-else-if="notification.type === 'reaction:grouped'">{{ i18n.tsx._notification.reactedBySomeUsers({ n: notification.reactions.length }) }}</span>
|
||||||
<span v-else-if="notification.type === 'renote:grouped'">{{ i18n.tsx._notification.renotedBySomeUsers({ n: notification.users.length }) }}</span>
|
<span v-else-if="notification.type === 'renote:grouped'">{{ i18n.tsx._notification.renotedBySomeUsers({ n: notification.users.length }) }}</span>
|
||||||
<span v-else>{{ notification.header }}</span>
|
<span v-else-if="notification.type === 'app'">{{ notification.header }}</span>
|
||||||
<MkTime v-if="withTime" :time="notification.createdAt" :class="$style.headerTime"/>
|
<MkTime v-if="withTime" :time="notification.createdAt" :class="$style.headerTime"/>
|
||||||
</header>
|
</header>
|
||||||
<div>
|
<div>
|
||||||
|
@ -97,7 +98,6 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
</MkA>
|
</MkA>
|
||||||
<template v-else-if="notification.type === 'follow'">
|
<template v-else-if="notification.type === 'follow'">
|
||||||
<span :class="$style.text" style="opacity: 0.6;">{{ i18n.ts.youGotNewFollower }}</span>
|
<span :class="$style.text" style="opacity: 0.6;">{{ i18n.ts.youGotNewFollower }}</span>
|
||||||
<div v-if="full"><MkFollowButton :user="notification.user" :full="true"/></div>
|
|
||||||
</template>
|
</template>
|
||||||
<span v-else-if="notification.type === 'followRequestAccepted'" :class="$style.text" style="opacity: 0.6;">{{ i18n.ts.followRequestAccepted }}</span>
|
<span v-else-if="notification.type === 'followRequestAccepted'" :class="$style.text" style="opacity: 0.6;">{{ i18n.ts.followRequestAccepted }}</span>
|
||||||
<template v-else-if="notification.type === 'receiveFollowRequest'">
|
<template v-else-if="notification.type === 'receiveFollowRequest'">
|
||||||
|
@ -113,12 +113,12 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
</span>
|
</span>
|
||||||
|
|
||||||
<div v-if="notification.type === 'reaction:grouped'">
|
<div v-if="notification.type === 'reaction:grouped'">
|
||||||
<div v-for="reaction of notification.reactions" :class="$style.reactionsItem">
|
<div v-for="reaction of notification.reactions" :key="reaction.user.id + reaction.reaction" :class="$style.reactionsItem">
|
||||||
<MkAvatar :class="$style.reactionsItemAvatar" :user="reaction.user" link preview/>
|
<MkAvatar :class="$style.reactionsItemAvatar" :user="reaction.user" link preview/>
|
||||||
<div :class="$style.reactionsItemReaction">
|
<div :class="$style.reactionsItemReaction">
|
||||||
<MkReactionIcon
|
<MkReactionIcon
|
||||||
:withTooltip="true"
|
:withTooltip="true"
|
||||||
:reaction="reaction.reaction ? reaction.reaction.replace(/^:(\w+):$/, ':$1@.:') : reaction.reaction"
|
:reaction="reaction.reaction.replace(/^:(\w+):$/, ':$1@.:')"
|
||||||
:noStyle="true"
|
:noStyle="true"
|
||||||
style="width: 100%; height: 100%;"
|
style="width: 100%; height: 100%;"
|
||||||
/>
|
/>
|
||||||
|
@ -126,7 +126,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div v-else-if="notification.type === 'renote:grouped'">
|
<div v-else-if="notification.type === 'renote:grouped'">
|
||||||
<div v-for="user of notification.users" :class="$style.reactionsItem">
|
<div v-for="user of notification.users" :key="user.id" :class="$style.reactionsItem">
|
||||||
<MkAvatar :class="$style.reactionsItemAvatar" :user="user" link preview/>
|
<MkAvatar :class="$style.reactionsItemAvatar" :user="user" link preview/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -139,16 +139,17 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
import { ref } from 'vue';
|
import { ref } from 'vue';
|
||||||
import * as Misskey from 'misskey-js';
|
import * as Misskey from 'misskey-js';
|
||||||
import MkReactionIcon from '@/components/MkReactionIcon.vue';
|
import MkReactionIcon from '@/components/MkReactionIcon.vue';
|
||||||
import MkFollowButton from '@/components/MkFollowButton.vue';
|
|
||||||
import MkButton from '@/components/MkButton.vue';
|
import MkButton from '@/components/MkButton.vue';
|
||||||
import { getNoteSummary } from '@/scripts/get-note-summary.js';
|
import { getNoteSummary } from '@/scripts/get-note-summary.js';
|
||||||
import { notePage } from '@/filters/note.js';
|
import { notePage } from '@/filters/note.js';
|
||||||
import { userPage } from '@/filters/user.js';
|
import { userPage } from '@/filters/user.js';
|
||||||
import { i18n } from '@/i18n.js';
|
import { i18n } from '@/i18n.js';
|
||||||
import { misskeyApi } from '@/scripts/misskey-api.js';
|
import { misskeyApi } from '@/scripts/misskey-api.js';
|
||||||
import { $i } from '@/account.js';
|
import { signinRequired } from '@/account.js';
|
||||||
import { infoImageUrl } from '@/instance.js';
|
import { infoImageUrl } from '@/instance.js';
|
||||||
|
|
||||||
|
const $i = signinRequired();
|
||||||
|
|
||||||
const props = withDefaults(defineProps<{
|
const props = withDefaults(defineProps<{
|
||||||
notification: Misskey.entities.Notification;
|
notification: Misskey.entities.Notification;
|
||||||
withTime?: boolean;
|
withTime?: boolean;
|
||||||
|
@ -161,11 +162,13 @@ const props = withDefaults(defineProps<{
|
||||||
const followRequestDone = ref(false);
|
const followRequestDone = ref(false);
|
||||||
|
|
||||||
const acceptFollowRequest = () => {
|
const acceptFollowRequest = () => {
|
||||||
|
if (props.notification.user == null) return;
|
||||||
followRequestDone.value = true;
|
followRequestDone.value = true;
|
||||||
misskeyApi('following/requests/accept', { userId: props.notification.user.id });
|
misskeyApi('following/requests/accept', { userId: props.notification.user.id });
|
||||||
};
|
};
|
||||||
|
|
||||||
const rejectFollowRequest = () => {
|
const rejectFollowRequest = () => {
|
||||||
|
if (props.notification.user == null) return;
|
||||||
followRequestDone.value = true;
|
followRequestDone.value = true;
|
||||||
misskeyApi('following/requests/reject', { userId: props.notification.user.id });
|
misskeyApi('following/requests/reject', { userId: props.notification.user.id });
|
||||||
};
|
};
|
||||||
|
@ -283,6 +286,12 @@ const rejectFollowRequest = () => {
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.t_roleAssigned {
|
||||||
|
padding: 3px;
|
||||||
|
background: #88a6b7;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
.tail {
|
.tail {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
|
|
|
@ -15,7 +15,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
<template #default="{ items: notifications }">
|
<template #default="{ items: notifications }">
|
||||||
<MkDateSeparatedList v-slot="{ item: notification }" :class="$style.list" :items="notifications" :noGap="true">
|
<MkDateSeparatedList v-slot="{ item: notification }" :class="$style.list" :items="notifications" :noGap="true">
|
||||||
<MkNote v-if="['reply', 'quote', 'mention'].includes(notification.type)" :key="notification.id" :note="notification.note"/>
|
<MkNote v-if="['reply', 'quote', 'mention'].includes(notification.type)" :key="notification.id + ':note'" :note="notification.note"/>
|
||||||
<XNotification v-else :key="notification.id" :notification="notification" :withTime="true" :full="true" class="_panel"/>
|
<XNotification v-else :key="notification.id" :notification="notification" :withTime="true" :full="true" class="_panel"/>
|
||||||
</MkDateSeparatedList>
|
</MkDateSeparatedList>
|
||||||
</template>
|
</template>
|
||||||
|
|
|
@ -27,7 +27,7 @@ const omitted = ref(false);
|
||||||
const ignoreOmit = ref(false);
|
const ignoreOmit = ref(false);
|
||||||
|
|
||||||
const calcOmit = () => {
|
const calcOmit = () => {
|
||||||
if (omitted.value || ignoreOmit.value) return;
|
if (omitted.value || ignoreOmit.value || content.value == null) return;
|
||||||
omitted.value = content.value.offsetHeight > props.maxHeight;
|
omitted.value = content.value.offsetHeight > props.maxHeight;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -37,7 +37,7 @@ const omitObserver = new ResizeObserver((entries, observer) => {
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
calcOmit();
|
calcOmit();
|
||||||
omitObserver.observe(content.value);
|
omitObserver.observe(content.value as HTMLElement);
|
||||||
});
|
});
|
||||||
|
|
||||||
onUnmounted(() => {
|
onUnmounted(() => {
|
||||||
|
|
|
@ -20,7 +20,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
</header>
|
</header>
|
||||||
<p v-if="page.summary" :title="page.summary">{{ page.summary.length > 85 ? page.summary.slice(0, 85) + '…' : page.summary }}</p>
|
<p v-if="page.summary" :title="page.summary">{{ page.summary.length > 85 ? page.summary.slice(0, 85) + '…' : page.summary }}</p>
|
||||||
<footer>
|
<footer>
|
||||||
<img class="icon" :src="page.user.avatarUrl"/>
|
<img v-if="page.user.avatarUrl" class="icon" :src="page.user.avatarUrl"/>
|
||||||
<p>{{ userName(page.user) }}</p>
|
<p>{{ userName(page.user) }}</p>
|
||||||
</footer>
|
</footer>
|
||||||
</article>
|
</article>
|
||||||
|
|
|
@ -41,8 +41,8 @@ import { PageMetadata, provideMetadataReceiver } from '@/scripts/page-metadata.j
|
||||||
import { openingWindowsCount } from '@/os.js';
|
import { openingWindowsCount } from '@/os.js';
|
||||||
import { claimAchievement } from '@/scripts/achievements.js';
|
import { claimAchievement } from '@/scripts/achievements.js';
|
||||||
import { getScrollContainer } from '@/scripts/scroll.js';
|
import { getScrollContainer } from '@/scripts/scroll.js';
|
||||||
import { useRouterFactory } from '@/global/router/supplier.js';
|
import { useRouterFactory } from '@/router/supplier.js';
|
||||||
import { mainRouter } from '@/global/router/main.js';
|
import { mainRouter } from '@/router/main.js';
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
initialPath: string;
|
initialPath: string;
|
||||||
|
@ -55,7 +55,7 @@ defineEmits<{
|
||||||
const routerFactory = useRouterFactory();
|
const routerFactory = useRouterFactory();
|
||||||
const windowRouter = routerFactory(props.initialPath);
|
const windowRouter = routerFactory(props.initialPath);
|
||||||
|
|
||||||
const contents = shallowRef<HTMLElement>();
|
const contents = shallowRef<HTMLElement | null>(null);
|
||||||
const pageMetadata = ref<null | ComputedRef<PageMetadata>>();
|
const pageMetadata = ref<null | ComputedRef<PageMetadata>>();
|
||||||
const windowEl = shallowRef<InstanceType<typeof MkWindow>>();
|
const windowEl = shallowRef<InstanceType<typeof MkWindow>>();
|
||||||
const history = ref<{ path: string; key: any; }[]>([{
|
const history = ref<{ path: string; key: any; }[]>([{
|
||||||
|
@ -63,7 +63,7 @@ const history = ref<{ path: string; key: any; }[]>([{
|
||||||
key: windowRouter.getCurrentKey(),
|
key: windowRouter.getCurrentKey(),
|
||||||
}]);
|
}]);
|
||||||
const buttonsLeft = computed(() => {
|
const buttonsLeft = computed(() => {
|
||||||
const buttons = [];
|
const buttons: Record<string, unknown>[] = [];
|
||||||
|
|
||||||
if (history.value.length > 1) {
|
if (history.value.length > 1) {
|
||||||
buttons.push({
|
buttons.push({
|
||||||
|
@ -93,6 +93,13 @@ windowRouter.addListener('push', ctx => {
|
||||||
history.value.push({ path: ctx.path, key: ctx.key });
|
history.value.push({ path: ctx.path, key: ctx.key });
|
||||||
});
|
});
|
||||||
|
|
||||||
|
windowRouter.addListener('replace', ctx => {
|
||||||
|
history.value.pop();
|
||||||
|
history.value.push({ path: ctx.path, key: ctx.key });
|
||||||
|
});
|
||||||
|
|
||||||
|
windowRouter.init();
|
||||||
|
|
||||||
provide('router', windowRouter);
|
provide('router', windowRouter);
|
||||||
provideMetadataReceiver((info) => {
|
provideMetadataReceiver((info) => {
|
||||||
pageMetadata.value = info;
|
pageMetadata.value = info;
|
||||||
|
@ -114,7 +121,7 @@ const contextmenu = computed(() => ([{
|
||||||
text: i18n.ts.openInNewTab,
|
text: i18n.ts.openInNewTab,
|
||||||
action: () => {
|
action: () => {
|
||||||
window.open(url + windowRouter.getCurrentPath(), '_blank', 'noopener');
|
window.open(url + windowRouter.getCurrentPath(), '_blank', 'noopener');
|
||||||
windowEl.value.close();
|
windowEl.value?.close();
|
||||||
},
|
},
|
||||||
}, {
|
}, {
|
||||||
icon: 'ti ti-link',
|
icon: 'ti ti-link',
|
||||||
|
@ -134,17 +141,17 @@ function reload() {
|
||||||
}
|
}
|
||||||
|
|
||||||
function close() {
|
function close() {
|
||||||
windowEl.value.close();
|
windowEl.value?.close();
|
||||||
}
|
}
|
||||||
|
|
||||||
function expand() {
|
function expand() {
|
||||||
mainRouter.push(windowRouter.getCurrentPath(), 'forcePage');
|
mainRouter.push(windowRouter.getCurrentPath(), 'forcePage');
|
||||||
windowEl.value.close();
|
windowEl.value?.close();
|
||||||
}
|
}
|
||||||
|
|
||||||
function popout() {
|
function popout() {
|
||||||
_popout(windowRouter.getCurrentPath(), windowEl.value.$el);
|
_popout(windowRouter.getCurrentPath(), windowEl.value?.$el);
|
||||||
windowEl.value.close();
|
windowEl.value?.close();
|
||||||
}
|
}
|
||||||
|
|
||||||
useScrollPositionManager(() => getScrollContainer(contents.value), windowRouter);
|
useScrollPositionManager(() => getScrollContainer(contents.value), windowRouter);
|
||||||
|
|
|
@ -173,7 +173,7 @@ async function init(): Promise<void> {
|
||||||
queue.value = [];
|
queue.value = [];
|
||||||
fetching.value = true;
|
fetching.value = true;
|
||||||
const params = props.pagination.params ? isRef(props.pagination.params) ? props.pagination.params.value : props.pagination.params : {};
|
const params = props.pagination.params ? isRef(props.pagination.params) ? props.pagination.params.value : props.pagination.params : {};
|
||||||
await misskeyApi(props.pagination.endpoint, {
|
await misskeyApi<MisskeyEntity[]>(props.pagination.endpoint, {
|
||||||
...params,
|
...params,
|
||||||
limit: props.pagination.limit ?? 10,
|
limit: props.pagination.limit ?? 10,
|
||||||
allowPartial: true,
|
allowPartial: true,
|
||||||
|
@ -210,7 +210,7 @@ const fetchMore = async (): Promise<void> => {
|
||||||
if (!more.value || fetching.value || moreFetching.value || items.value.length === 0) return;
|
if (!more.value || fetching.value || moreFetching.value || items.value.length === 0) return;
|
||||||
moreFetching.value = true;
|
moreFetching.value = true;
|
||||||
const params = props.pagination.params ? isRef(props.pagination.params) ? props.pagination.params.value : props.pagination.params : {};
|
const params = props.pagination.params ? isRef(props.pagination.params) ? props.pagination.params.value : props.pagination.params : {};
|
||||||
await misskeyApi(props.pagination.endpoint, {
|
await misskeyApi<MisskeyEntity[]>(props.pagination.endpoint, {
|
||||||
...params,
|
...params,
|
||||||
limit: SECOND_FETCH_LIMIT,
|
limit: SECOND_FETCH_LIMIT,
|
||||||
...(props.pagination.offsetMode ? {
|
...(props.pagination.offsetMode ? {
|
||||||
|
@ -274,7 +274,7 @@ const fetchMoreAhead = async (): Promise<void> => {
|
||||||
if (!more.value || fetching.value || moreFetching.value || items.value.length === 0) return;
|
if (!more.value || fetching.value || moreFetching.value || items.value.length === 0) return;
|
||||||
moreFetching.value = true;
|
moreFetching.value = true;
|
||||||
const params = props.pagination.params ? isRef(props.pagination.params) ? props.pagination.params.value : props.pagination.params : {};
|
const params = props.pagination.params ? isRef(props.pagination.params) ? props.pagination.params.value : props.pagination.params : {};
|
||||||
await misskeyApi(props.pagination.endpoint, {
|
await misskeyApi<MisskeyEntity[]>(props.pagination.endpoint, {
|
||||||
...params,
|
...params,
|
||||||
limit: SECOND_FETCH_LIMIT,
|
limit: SECOND_FETCH_LIMIT,
|
||||||
...(props.pagination.offsetMode ? {
|
...(props.pagination.offsetMode ? {
|
||||||
|
|
|
@ -6,7 +6,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
<template>
|
<template>
|
||||||
<div :class="{ [$style.done]: closed || isVoted }">
|
<div :class="{ [$style.done]: closed || isVoted }">
|
||||||
<ul :class="$style.choices">
|
<ul :class="$style.choices">
|
||||||
<li v-for="(choice, i) in note.poll.choices" :key="i" :class="$style.choice" @click="vote(i)">
|
<li v-for="(choice, i) in poll.choices" :key="i" :class="$style.choice" @click="vote(i)">
|
||||||
<div :class="$style.bg" :style="{ 'width': `${showResult ? (choice.votes / total * 100) : 0}%` }"></div>
|
<div :class="$style.bg" :style="{ 'width': `${showResult ? (choice.votes / total * 100) : 0}%` }"></div>
|
||||||
<span :class="$style.fg">
|
<span :class="$style.fg">
|
||||||
<template v-if="choice.isVoted"><i class="ti ti-check" style="margin-right: 4px; color: var(--accent);"></i></template>
|
<template v-if="choice.isVoted"><i class="ti ti-check" style="margin-right: 4px; color: var(--accent);"></i></template>
|
||||||
|
@ -35,35 +35,35 @@ import * as os from '@/os.js';
|
||||||
import { misskeyApi } from '@/scripts/misskey-api.js';
|
import { misskeyApi } from '@/scripts/misskey-api.js';
|
||||||
import { i18n } from '@/i18n.js';
|
import { i18n } from '@/i18n.js';
|
||||||
import { useInterval } from '@/scripts/use-interval.js';
|
import { useInterval } from '@/scripts/use-interval.js';
|
||||||
import { WithNonNullable } from '@/type.js';
|
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
note: WithNonNullable<Misskey.entities.Note, 'poll'>;
|
noteId: string;
|
||||||
|
poll: NonNullable<Misskey.entities.Note['poll']>;
|
||||||
readOnly?: boolean;
|
readOnly?: boolean;
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
const remaining = ref(-1);
|
const remaining = ref(-1);
|
||||||
|
|
||||||
const total = computed(() => sum(props.note.poll.choices.map(x => x.votes)));
|
const total = computed(() => sum(props.poll.choices.map(x => x.votes)));
|
||||||
const closed = computed(() => remaining.value === 0);
|
const closed = computed(() => remaining.value === 0);
|
||||||
const isVoted = computed(() => !props.note.poll.multiple && props.note.poll.choices.some(c => c.isVoted));
|
const isVoted = computed(() => !props.poll.multiple && props.poll.choices.some(c => c.isVoted));
|
||||||
const timer = computed(() => i18n.tsx._poll[
|
const timer = computed(() => i18n.tsx._poll[
|
||||||
remaining.value >= 86400 ? 'remainingDays' :
|
remaining.value >= 86400 ? 'remainingDays' :
|
||||||
remaining.value >= 3600 ? 'remainingHours' :
|
remaining.value >= 3600 ? 'remainingHours' :
|
||||||
remaining.value >= 60 ? 'remainingMinutes' : 'remainingSeconds'
|
remaining.value >= 60 ? 'remainingMinutes' : 'remainingSeconds'
|
||||||
]({
|
]({
|
||||||
s: Math.floor(remaining.value % 60),
|
s: Math.floor(remaining.value % 60),
|
||||||
m: Math.floor(remaining.value / 60) % 60,
|
m: Math.floor(remaining.value / 60) % 60,
|
||||||
h: Math.floor(remaining.value / 3600) % 24,
|
h: Math.floor(remaining.value / 3600) % 24,
|
||||||
d: Math.floor(remaining.value / 86400),
|
d: Math.floor(remaining.value / 86400),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
const showResult = ref(props.readOnly || isVoted.value);
|
const showResult = ref(props.readOnly || isVoted.value);
|
||||||
|
|
||||||
// 期限付きアンケート
|
// 期限付きアンケート
|
||||||
if (props.note.poll.expiresAt) {
|
if (props.poll.expiresAt) {
|
||||||
const tick = () => {
|
const tick = () => {
|
||||||
remaining.value = Math.floor(Math.max(new Date(props.note.poll.expiresAt).getTime() - Date.now(), 0) / 1000);
|
remaining.value = Math.floor(Math.max(new Date(props.poll.expiresAt!).getTime() - Date.now(), 0) / 1000);
|
||||||
if (remaining.value === 0) {
|
if (remaining.value === 0) {
|
||||||
showResult.value = true;
|
showResult.value = true;
|
||||||
}
|
}
|
||||||
|
@ -82,15 +82,15 @@ const vote = async (id) => {
|
||||||
|
|
||||||
const { canceled } = await os.confirm({
|
const { canceled } = await os.confirm({
|
||||||
type: 'question',
|
type: 'question',
|
||||||
text: i18n.tsx.voteConfirm({ choice: props.note.poll.choices[id].text }),
|
text: i18n.tsx.voteConfirm({ choice: props.poll.choices[id].text }),
|
||||||
});
|
});
|
||||||
if (canceled) return;
|
if (canceled) return;
|
||||||
|
|
||||||
await misskeyApi('notes/polls/vote', {
|
await misskeyApi('notes/polls/vote', {
|
||||||
noteId: props.note.id,
|
noteId: props.noteId,
|
||||||
choice: id,
|
choice: id,
|
||||||
});
|
});
|
||||||
if (!showResult.value) showResult.value = !props.note.poll.multiple;
|
if (!showResult.value) showResult.value = !props.poll.multiple;
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|
|
@ -62,21 +62,18 @@ import { formatDateTimeString } from '@/scripts/format-time-string.js';
|
||||||
import { addTime } from '@/scripts/time.js';
|
import { addTime } from '@/scripts/time.js';
|
||||||
import { i18n } from '@/i18n.js';
|
import { i18n } from '@/i18n.js';
|
||||||
|
|
||||||
|
export type PollEditorModelValue = {
|
||||||
|
expiresAt: number | null;
|
||||||
|
expiredAfter: number | null;
|
||||||
|
choices: string[];
|
||||||
|
multiple: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
modelValue: {
|
modelValue: PollEditorModelValue;
|
||||||
expiresAt: string;
|
|
||||||
expiredAfter: number;
|
|
||||||
choices: string[];
|
|
||||||
multiple: boolean;
|
|
||||||
};
|
|
||||||
}>();
|
}>();
|
||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
(ev: 'update:modelValue', v: {
|
(ev: 'update:modelValue', v: PollEditorModelValue): void;
|
||||||
expiresAt: string;
|
|
||||||
expiredAfter: number;
|
|
||||||
choices: string[];
|
|
||||||
multiple: boolean;
|
|
||||||
}): void;
|
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
const choices = ref(props.modelValue.choices);
|
const choices = ref(props.modelValue.choices);
|
||||||
|
@ -89,7 +86,9 @@ const unit = ref('second');
|
||||||
|
|
||||||
if (props.modelValue.expiresAt) {
|
if (props.modelValue.expiresAt) {
|
||||||
expiration.value = 'at';
|
expiration.value = 'at';
|
||||||
atDate.value = atTime.value = props.modelValue.expiresAt;
|
const expiresAt = new Date(props.modelValue.expiresAt);
|
||||||
|
atDate.value = formatDateTimeString(expiresAt, 'yyyy-MM-dd');
|
||||||
|
atTime.value = formatDateTimeString(expiresAt, 'HH:mm');
|
||||||
} else if (typeof props.modelValue.expiredAfter === 'number') {
|
} else if (typeof props.modelValue.expiredAfter === 'number') {
|
||||||
expiration.value = 'after';
|
expiration.value = 'after';
|
||||||
after.value = props.modelValue.expiredAfter / 1000;
|
after.value = props.modelValue.expiredAfter / 1000;
|
||||||
|
@ -113,20 +112,21 @@ function remove(i) {
|
||||||
choices.value = choices.value.filter((_, _i) => _i !== i);
|
choices.value = choices.value.filter((_, _i) => _i !== i);
|
||||||
}
|
}
|
||||||
|
|
||||||
function get() {
|
function get(): PollEditorModelValue {
|
||||||
const calcAt = () => {
|
const calcAt = () => {
|
||||||
return new Date(`${atDate.value} ${atTime.value}`).getTime();
|
return new Date(`${atDate.value} ${atTime.value}`).getTime();
|
||||||
};
|
};
|
||||||
|
|
||||||
const calcAfter = () => {
|
const calcAfter = () => {
|
||||||
let base = parseInt(after.value);
|
let base = parseInt(after.value.toString());
|
||||||
switch (unit.value) {
|
switch (unit.value) {
|
||||||
|
// @ts-expect-error fallthrough
|
||||||
case 'day': base *= 24;
|
case 'day': base *= 24;
|
||||||
// fallthrough
|
// @ts-expect-error fallthrough
|
||||||
case 'hour': base *= 60;
|
case 'hour': base *= 60;
|
||||||
// fallthrough
|
// @ts-expect-error fallthrough
|
||||||
case 'minute': base *= 60;
|
case 'minute': base *= 60;
|
||||||
// fallthrough
|
// eslint-disable-next-line no-fallthrough
|
||||||
case 'second': return base *= 1000;
|
case 'second': return base *= 1000;
|
||||||
default: return null;
|
default: return null;
|
||||||
}
|
}
|
||||||
|
@ -135,10 +135,8 @@ function get() {
|
||||||
return {
|
return {
|
||||||
choices: choices.value,
|
choices: choices.value,
|
||||||
multiple: multiple.value,
|
multiple: multiple.value,
|
||||||
...(
|
expiresAt: expiration.value === 'at' ? calcAt() : null,
|
||||||
expiration.value === 'at' ? { expiresAt: calcAt() } :
|
expiredAfter: expiration.value === 'after' ? calcAfter() : null,
|
||||||
expiration.value === 'after' ? { expiredAfter: calcAfter() } : {}
|
|
||||||
),
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -85,7 +85,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
<button v-tooltip="i18n.ts.useCw" class="_button" :class="[$style.footerButton, { [$style.footerButtonActive]: useCw }]" @click="useCw = !useCw"><i class="ti ti-eye-off"></i></button>
|
<button v-tooltip="i18n.ts.useCw" class="_button" :class="[$style.footerButton, { [$style.footerButtonActive]: useCw }]" @click="useCw = !useCw"><i class="ti ti-eye-off"></i></button>
|
||||||
<button v-tooltip="i18n.ts.mention" class="_button" :class="$style.footerButton" @click="insertMention"><i class="ti ti-at"></i></button>
|
<button v-tooltip="i18n.ts.mention" class="_button" :class="$style.footerButton" @click="insertMention"><i class="ti ti-at"></i></button>
|
||||||
<button v-tooltip="i18n.ts.hashtags" class="_button" :class="[$style.footerButton, { [$style.footerButtonActive]: withHashtags }]" @click="withHashtags = !withHashtags"><i class="ti ti-hash"></i></button>
|
<button v-tooltip="i18n.ts.hashtags" class="_button" :class="[$style.footerButton, { [$style.footerButtonActive]: withHashtags }]" @click="withHashtags = !withHashtags"><i class="ti ti-hash"></i></button>
|
||||||
<button v-if="postFormActions.length > 0" v-tooltip="i18n.ts.plugin" class="_button" :class="$style.footerButton" @click="showActions"><i class="ti ti-plug"></i></button>
|
<button v-if="postFormActions.length > 0" v-tooltip="i18n.ts.plugins" class="_button" :class="$style.footerButton" @click="showActions"><i class="ti ti-plug"></i></button>
|
||||||
<button v-tooltip="i18n.ts.emoji" :class="['_button', $style.footerButton]" @click="insertEmoji"><i class="ti ti-mood-happy"></i></button>
|
<button v-tooltip="i18n.ts.emoji" :class="['_button', $style.footerButton]" @click="insertEmoji"><i class="ti ti-mood-happy"></i></button>
|
||||||
<button v-if="showAddMfmFunction" v-tooltip="i18n.ts.addMfmFunction" :class="['_button', $style.footerButton]" @click="insertMfmFunction"><i class="ti ti-palette"></i></button>
|
<button v-if="showAddMfmFunction" v-tooltip="i18n.ts.addMfmFunction" :class="['_button', $style.footerButton]" @click="insertMfmFunction"><i class="ti ti-palette"></i></button>
|
||||||
</div>
|
</div>
|
||||||
|
@ -109,7 +109,7 @@ import { toASCII } from 'punycode/';
|
||||||
import MkNoteSimple from '@/components/MkNoteSimple.vue';
|
import MkNoteSimple from '@/components/MkNoteSimple.vue';
|
||||||
import MkNotePreview from '@/components/MkNotePreview.vue';
|
import MkNotePreview from '@/components/MkNotePreview.vue';
|
||||||
import XPostFormAttaches from '@/components/MkPostFormAttaches.vue';
|
import XPostFormAttaches from '@/components/MkPostFormAttaches.vue';
|
||||||
import MkPollEditor from '@/components/MkPollEditor.vue';
|
import MkPollEditor, { type PollEditorModelValue } from '@/components/MkPollEditor.vue';
|
||||||
import { host, url } from '@/config.js';
|
import { host, url } from '@/config.js';
|
||||||
import { erase, unique } from '@/scripts/array.js';
|
import { erase, unique } from '@/scripts/array.js';
|
||||||
import { extractMentions } from '@/scripts/extract-mentions.js';
|
import { extractMentions } from '@/scripts/extract-mentions.js';
|
||||||
|
@ -140,13 +140,13 @@ const props = withDefaults(defineProps<{
|
||||||
renote?: Misskey.entities.Note;
|
renote?: Misskey.entities.Note;
|
||||||
channel?: Misskey.entities.Channel; // TODO
|
channel?: Misskey.entities.Channel; // TODO
|
||||||
mention?: Misskey.entities.User;
|
mention?: Misskey.entities.User;
|
||||||
specified?: Misskey.entities.User;
|
specified?: Misskey.entities.UserDetailed;
|
||||||
initialText?: string;
|
initialText?: string;
|
||||||
initialCw?: string;
|
initialCw?: string;
|
||||||
initialVisibility?: (typeof Misskey.noteVisibilities)[number];
|
initialVisibility?: (typeof Misskey.noteVisibilities)[number];
|
||||||
initialFiles?: Misskey.entities.DriveFile[];
|
initialFiles?: Misskey.entities.DriveFile[];
|
||||||
initialLocalOnly?: boolean;
|
initialLocalOnly?: boolean;
|
||||||
initialVisibleUsers?: Misskey.entities.User[];
|
initialVisibleUsers?: Misskey.entities.UserDetailed[];
|
||||||
initialNote?: Misskey.entities.Note;
|
initialNote?: Misskey.entities.Note;
|
||||||
instant?: boolean;
|
instant?: boolean;
|
||||||
fixed?: boolean;
|
fixed?: boolean;
|
||||||
|
@ -179,12 +179,7 @@ const posting = ref(false);
|
||||||
const posted = ref(false);
|
const posted = ref(false);
|
||||||
const text = ref(props.initialText ?? '');
|
const text = ref(props.initialText ?? '');
|
||||||
const files = ref(props.initialFiles ?? []);
|
const files = ref(props.initialFiles ?? []);
|
||||||
const poll = ref<{
|
const poll = ref<PollEditorModelValue | null>(null);
|
||||||
choices: string[];
|
|
||||||
multiple: boolean;
|
|
||||||
expiresAt: string | null;
|
|
||||||
expiredAfter: string | null;
|
|
||||||
} | null>(null);
|
|
||||||
const useCw = ref<boolean>(!!props.initialCw);
|
const useCw = ref<boolean>(!!props.initialCw);
|
||||||
const showPreview = ref(defaultStore.state.showPreview);
|
const showPreview = ref(defaultStore.state.showPreview);
|
||||||
watch(showPreview, () => defaultStore.set('showPreview', showPreview.value));
|
watch(showPreview, () => defaultStore.set('showPreview', showPreview.value));
|
||||||
|
@ -337,7 +332,7 @@ if (props.reply && ['home', 'followers', 'specified'].includes(props.reply.visib
|
||||||
if (visibility.value === 'specified') {
|
if (visibility.value === 'specified') {
|
||||||
if (props.reply.visibleUserIds) {
|
if (props.reply.visibleUserIds) {
|
||||||
misskeyApi('users/show', {
|
misskeyApi('users/show', {
|
||||||
userIds: props.reply.visibleUserIds.filter(uid => uid !== $i.id && uid !== props.reply.userId),
|
userIds: props.reply.visibleUserIds.filter(uid => uid !== $i.id && uid !== props.reply?.userId),
|
||||||
}).then(users => {
|
}).then(users => {
|
||||||
users.forEach(pushVisibleUser);
|
users.forEach(pushVisibleUser);
|
||||||
});
|
});
|
||||||
|
@ -539,7 +534,7 @@ async function toggleReactionAcceptance() {
|
||||||
reactionAcceptance.value = select.result;
|
reactionAcceptance.value = select.result;
|
||||||
}
|
}
|
||||||
|
|
||||||
function pushVisibleUser(user) {
|
function pushVisibleUser(user: Misskey.entities.UserDetailed) {
|
||||||
if (!visibleUsers.value.some(u => u.username === user.username && u.host === user.host)) {
|
if (!visibleUsers.value.some(u => u.username === user.username && u.host === user.host)) {
|
||||||
visibleUsers.value.push(user);
|
visibleUsers.value.push(user);
|
||||||
}
|
}
|
||||||
|
@ -581,10 +576,12 @@ function onCompositionEnd(ev: CompositionEvent) {
|
||||||
|
|
||||||
async function onPaste(ev: ClipboardEvent) {
|
async function onPaste(ev: ClipboardEvent) {
|
||||||
if (props.mock) return;
|
if (props.mock) return;
|
||||||
|
if (!ev.clipboardData) return;
|
||||||
|
|
||||||
for (const { item, i } of Array.from(ev.clipboardData.items, (item, i) => ({ item, i }))) {
|
for (const { item, i } of Array.from(ev.clipboardData.items, (data, x) => ({ item: data, i: x }))) {
|
||||||
if (item.kind === 'file') {
|
if (item.kind === 'file') {
|
||||||
const file = item.getAsFile();
|
const file = item.getAsFile();
|
||||||
|
if (!file) continue;
|
||||||
const lio = file.name.lastIndexOf('.');
|
const lio = file.name.lastIndexOf('.');
|
||||||
const ext = lio >= 0 ? file.name.slice(lio) : '';
|
const ext = lio >= 0 ? file.name.slice(lio) : '';
|
||||||
const formatted = `${formatTimeString(new Date(file.lastModified), defaultStore.state.pastedFileName).replace(/{{number}}/g, `${i + 1}`)}${ext}`;
|
const formatted = `${formatTimeString(new Date(file.lastModified), defaultStore.state.pastedFileName).replace(/{{number}}/g, `${i + 1}`)}${ext}`;
|
||||||
|
@ -606,7 +603,7 @@ async function onPaste(ev: ClipboardEvent) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
quoteId.value = paste.substring(url.length).match(/^\/notes\/(.+?)\/?$/)[1];
|
quoteId.value = paste.substring(url.length).match(/^\/notes\/(.+?)\/?$/)?.[1] ?? null;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -637,26 +634,26 @@ function onDragover(ev) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function onDragenter(ev) {
|
function onDragenter() {
|
||||||
draghover.value = true;
|
draghover.value = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
function onDragleave(ev) {
|
function onDragleave() {
|
||||||
draghover.value = false;
|
draghover.value = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
function onDrop(ev): void {
|
function onDrop(ev: DragEvent): void {
|
||||||
draghover.value = false;
|
draghover.value = false;
|
||||||
|
|
||||||
// ファイルだったら
|
// ファイルだったら
|
||||||
if (ev.dataTransfer.files.length > 0) {
|
if (ev.dataTransfer && ev.dataTransfer.files.length > 0) {
|
||||||
ev.preventDefault();
|
ev.preventDefault();
|
||||||
for (const x of Array.from(ev.dataTransfer.files)) upload(x);
|
for (const x of Array.from(ev.dataTransfer.files)) upload(x);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
//#region ドライブのファイル
|
//#region ドライブのファイル
|
||||||
const driveFile = ev.dataTransfer.getData(_DATA_TRANSFER_DRIVE_FILE_);
|
const driveFile = ev.dataTransfer?.getData(_DATA_TRANSFER_DRIVE_FILE_);
|
||||||
if (driveFile != null && driveFile !== '') {
|
if (driveFile != null && driveFile !== '') {
|
||||||
const file = JSON.parse(driveFile);
|
const file = JSON.parse(driveFile);
|
||||||
files.value.push(file);
|
files.value.push(file);
|
||||||
|
@ -704,11 +701,14 @@ async function post(ev?: MouseEvent) {
|
||||||
}
|
}
|
||||||
|
|
||||||
if (ev) {
|
if (ev) {
|
||||||
const el = ev.currentTarget ?? ev.target;
|
const el = (ev.currentTarget ?? ev.target) as HTMLElement | null;
|
||||||
const rect = el.getBoundingClientRect();
|
|
||||||
const x = rect.left + (el.offsetWidth / 2);
|
if (el) {
|
||||||
const y = rect.top + (el.offsetHeight / 2);
|
const rect = el.getBoundingClientRect();
|
||||||
os.popup(MkRippleEffect, { x, y }, {}, 'end');
|
const x = rect.left + (el.offsetWidth / 2);
|
||||||
|
const y = rect.top + (el.offsetHeight / 2);
|
||||||
|
os.popup(MkRippleEffect, { x, y }, {}, 'end');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (props.mock) return;
|
if (props.mock) return;
|
||||||
|
@ -777,18 +777,18 @@ async function post(ev?: MouseEvent) {
|
||||||
if (notePostInterruptors.length > 0) {
|
if (notePostInterruptors.length > 0) {
|
||||||
for (const interruptor of notePostInterruptors) {
|
for (const interruptor of notePostInterruptors) {
|
||||||
try {
|
try {
|
||||||
postData = await interruptor.handler(deepClone(postData));
|
postData = await interruptor.handler(deepClone(postData)) as typeof postData;
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error(err);
|
console.error(err);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let token = undefined;
|
let token: string | undefined = undefined;
|
||||||
|
|
||||||
if (postAccount.value) {
|
if (postAccount.value) {
|
||||||
const storedAccounts = await getAccounts();
|
const storedAccounts = await getAccounts();
|
||||||
token = storedAccounts.find(x => x.id === postAccount.value.id)?.token;
|
token = storedAccounts.find(x => x.id === postAccount.value?.id)?.token;
|
||||||
}
|
}
|
||||||
|
|
||||||
posting.value = true;
|
posting.value = true;
|
||||||
|
@ -802,7 +802,7 @@ async function post(ev?: MouseEvent) {
|
||||||
deleteDraft();
|
deleteDraft();
|
||||||
emit('posted');
|
emit('posted');
|
||||||
if (postData.text && postData.text !== '') {
|
if (postData.text && postData.text !== '') {
|
||||||
const hashtags_ = mfm.parse(postData.text).filter(x => x.type === 'hashtag').map(x => x.props.hashtag);
|
const hashtags_ = mfm.parse(postData.text).map(x => x.type === 'hashtag' && x.props.hashtag).filter(x => x) as string[];
|
||||||
const history = JSON.parse(miLocalStorage.getItem('hashtags') ?? '[]') as string[];
|
const history = JSON.parse(miLocalStorage.getItem('hashtags') ?? '[]') as string[];
|
||||||
miLocalStorage.setItem('hashtags', JSON.stringify(unique(hashtags_.concat(history))));
|
miLocalStorage.setItem('hashtags', JSON.stringify(unique(hashtags_.concat(history))));
|
||||||
}
|
}
|
||||||
|
@ -870,7 +870,7 @@ function insertMention() {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function insertEmoji(ev: MouseEvent): void {
|
async function insertEmoji(ev: MouseEvent) {
|
||||||
os.openEmojiPicker(
|
os.openEmojiPicker(
|
||||||
(ev.currentTarget ?? ev.target) as HTMLElement,
|
(ev.currentTarget ?? ev.target) as HTMLElement,
|
||||||
{ asReactionPicker: false },
|
{ asReactionPicker: false },
|
||||||
|
@ -879,6 +879,7 @@ function insertEmoji(ev: MouseEvent): void {
|
||||||
}
|
}
|
||||||
|
|
||||||
async function insertMfmFunction(ev: MouseEvent) {
|
async function insertMfmFunction(ev: MouseEvent) {
|
||||||
|
if (textareaEl.value == null) return;
|
||||||
mfmFunctionPicker(
|
mfmFunctionPicker(
|
||||||
ev.currentTarget ?? ev.target,
|
ev.currentTarget ?? ev.target,
|
||||||
textareaEl.value,
|
textareaEl.value,
|
||||||
|
@ -886,14 +887,15 @@ async function insertMfmFunction(ev: MouseEvent) {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function showActions(ev) {
|
function showActions(ev: MouseEvent) {
|
||||||
os.popupMenu(postFormActions.map(action => ({
|
os.popupMenu(postFormActions.map(action => ({
|
||||||
text: action.title,
|
text: action.title,
|
||||||
action: () => {
|
action: () => {
|
||||||
action.handler({
|
action.handler({
|
||||||
text: text.value,
|
text: text.value,
|
||||||
cw: cw.value,
|
cw: cw.value,
|
||||||
}, (key, value) => {
|
}, (key, value: any) => {
|
||||||
|
if (typeof key !== 'string') return;
|
||||||
if (key === 'text') { text.value = value; }
|
if (key === 'text') { text.value = value; }
|
||||||
if (key === 'cw') { useCw.value = value !== null; cw.value = value; }
|
if (key === 'cw') { useCw.value = value !== null; cw.value = value; }
|
||||||
});
|
});
|
||||||
|
@ -954,19 +956,19 @@ onMounted(() => {
|
||||||
if (props.initialNote) {
|
if (props.initialNote) {
|
||||||
const init = props.initialNote;
|
const init = props.initialNote;
|
||||||
text.value = init.text ? init.text : '';
|
text.value = init.text ? init.text : '';
|
||||||
files.value = init.files;
|
files.value = init.files ?? [];
|
||||||
cw.value = init.cw;
|
cw.value = init.cw ?? null;
|
||||||
useCw.value = init.cw != null;
|
useCw.value = init.cw != null;
|
||||||
if (init.poll) {
|
if (init.poll) {
|
||||||
poll.value = {
|
poll.value = {
|
||||||
choices: init.poll.choices.map(x => x.text),
|
choices: init.poll.choices.map(x => x.text),
|
||||||
multiple: init.poll.multiple,
|
multiple: init.poll.multiple,
|
||||||
expiresAt: init.poll.expiresAt,
|
expiresAt: init.poll.expiresAt ? (new Date(init.poll.expiresAt)).getTime() : null,
|
||||||
expiredAfter: init.poll.expiredAfter,
|
expiredAfter: null,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
visibility.value = init.visibility;
|
visibility.value = init.visibility;
|
||||||
localOnly.value = init.localOnly;
|
localOnly.value = init.localOnly ?? false;
|
||||||
quoteId.value = init.renote ? init.renote.id : null;
|
quoteId.value = init.renote ? init.renote.id : null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -93,7 +93,7 @@ async function rename(file) {
|
||||||
const { canceled, result } = await os.inputText({
|
const { canceled, result } = await os.inputText({
|
||||||
title: i18n.ts.enterFileName,
|
title: i18n.ts.enterFileName,
|
||||||
default: file.name,
|
default: file.name,
|
||||||
allowEmpty: false,
|
minLength: 1,
|
||||||
});
|
});
|
||||||
if (canceled) return;
|
if (canceled) return;
|
||||||
misskeyApi('drive/files/update', {
|
misskeyApi('drive/files/update', {
|
||||||
|
|
|
@ -4,8 +4,8 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
-->
|
-->
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<MkModal ref="modal" :preferType="'dialog'" @click="modal.close()" @closed="onModalClosed()">
|
<MkModal ref="modal" :preferType="'dialog'" @click="modal?.close()" @closed="onModalClosed()">
|
||||||
<MkPostForm ref="form" :class="$style.form" v-bind="props" autofocus freezeAfterPosted @posted="onPosted" @cancel="modal.close()" @esc="modal.close()"/>
|
<MkPostForm ref="form" :class="$style.form" v-bind="props" autofocus freezeAfterPosted @posted="onPosted" @cancel="modal?.close()" @esc="modal?.close()"/>
|
||||||
</MkModal>
|
</MkModal>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
@ -20,13 +20,13 @@ const props = defineProps<{
|
||||||
renote?: Misskey.entities.Note;
|
renote?: Misskey.entities.Note;
|
||||||
channel?: any; // TODO
|
channel?: any; // TODO
|
||||||
mention?: Misskey.entities.User;
|
mention?: Misskey.entities.User;
|
||||||
specified?: Misskey.entities.User;
|
specified?: Misskey.entities.UserDetailed;
|
||||||
initialText?: string;
|
initialText?: string;
|
||||||
initialCw?: string;
|
initialCw?: string;
|
||||||
initialVisibility?: typeof Misskey.noteVisibilities;
|
initialVisibility?: (typeof Misskey.noteVisibilities)[number];
|
||||||
initialFiles?: Misskey.entities.DriveFile[];
|
initialFiles?: Misskey.entities.DriveFile[];
|
||||||
initialLocalOnly?: boolean;
|
initialLocalOnly?: boolean;
|
||||||
initialVisibleUsers?: Misskey.entities.User[];
|
initialVisibleUsers?: Misskey.entities.UserDetailed[];
|
||||||
initialNote?: Misskey.entities.Note;
|
initialNote?: Misskey.entities.Note;
|
||||||
instant?: boolean;
|
instant?: boolean;
|
||||||
fixed?: boolean;
|
fixed?: boolean;
|
||||||
|
@ -41,7 +41,7 @@ const modal = shallowRef<InstanceType<typeof MkModal>>();
|
||||||
const form = shallowRef<InstanceType<typeof MkPostForm>>();
|
const form = shallowRef<InstanceType<typeof MkPostForm>>();
|
||||||
|
|
||||||
function onPosted() {
|
function onPosted() {
|
||||||
modal.value.close({
|
modal.value?.close({
|
||||||
useSendAnimation: true,
|
useSendAnimation: true,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
@ -126,7 +126,7 @@ async function unsubscribe() {
|
||||||
}
|
}
|
||||||
|
|
||||||
function encode(buffer: ArrayBuffer | null) {
|
function encode(buffer: ArrayBuffer | null) {
|
||||||
return btoa(String.fromCharCode.apply(null, new Uint8Array(buffer)));
|
return btoa(String.fromCharCode.apply(null, buffer ? new Uint8Array(buffer) as any : []));
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -38,7 +38,7 @@ export default defineComponent({
|
||||||
h('div', {
|
h('div', {
|
||||||
class: 'body',
|
class: 'body',
|
||||||
}, options.map(option => h(MkRadio, {
|
}, options.map(option => h(MkRadio, {
|
||||||
key: option.key,
|
key: option.key as string,
|
||||||
value: option.props?.value,
|
value: option.props?.value,
|
||||||
modelValue: value.value,
|
modelValue: value.value,
|
||||||
'onUpdate:modelValue': _v => value.value = _v,
|
'onUpdate:modelValue': _v => value.value = _v,
|
||||||
|
|
|
@ -86,7 +86,7 @@ onMounted(() => {
|
||||||
ro = new ResizeObserver((entries, observer) => {
|
ro = new ResizeObserver((entries, observer) => {
|
||||||
calcThumbPosition();
|
calcThumbPosition();
|
||||||
});
|
});
|
||||||
ro.observe(containerEl.value);
|
if (containerEl.value) ro.observe(containerEl.value);
|
||||||
});
|
});
|
||||||
|
|
||||||
onUnmounted(() => {
|
onUnmounted(() => {
|
||||||
|
@ -122,7 +122,7 @@ const onMousedown = (ev: MouseEvent | TouchEvent) => {
|
||||||
const onDrag = (ev: MouseEvent | TouchEvent) => {
|
const onDrag = (ev: MouseEvent | TouchEvent) => {
|
||||||
ev.preventDefault();
|
ev.preventDefault();
|
||||||
const containerRect = containerEl.value!.getBoundingClientRect();
|
const containerRect = containerEl.value!.getBoundingClientRect();
|
||||||
const pointerX = ev.touches && ev.touches.length > 0 ? ev.touches[0].clientX : ev.clientX;
|
const pointerX = 'touches' in ev && ev.touches.length > 0 ? ev.touches[0].clientX : 'clientX' in ev ? ev.clientX : 0;
|
||||||
const pointerPositionOnContainer = pointerX - (containerRect.left + (thumbWidth / 2));
|
const pointerPositionOnContainer = pointerX - (containerRect.left + (thumbWidth / 2));
|
||||||
rawValue.value = Math.min(1, Math.max(0, pointerPositionOnContainer / (containerEl.value!.offsetWidth - thumbWidth)));
|
rawValue.value = Math.min(1, Math.max(0, pointerPositionOnContainer / (containerEl.value!.offsetWidth - thumbWidth)));
|
||||||
|
|
||||||
|
|
|
@ -20,9 +20,9 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { computed, inject, onMounted, shallowRef, watch } from 'vue';
|
import { computed, inject, onMounted, shallowRef, watch } from 'vue';
|
||||||
import * as Misskey from 'misskey-js';
|
import * as Misskey from 'misskey-js';
|
||||||
|
import MkCustomEmojiDetailedDialog from './MkCustomEmojiDetailedDialog.vue';
|
||||||
import XDetails from '@/components/MkReactionsViewer.details.vue';
|
import XDetails from '@/components/MkReactionsViewer.details.vue';
|
||||||
import MkReactionIcon from '@/components/MkReactionIcon.vue';
|
import MkReactionIcon from '@/components/MkReactionIcon.vue';
|
||||||
import MkCustomEmojiDetailedDialog from './MkCustomEmojiDetailedDialog.vue';
|
|
||||||
import * as os from '@/os.js';
|
import * as os from '@/os.js';
|
||||||
import { misskeyApi, misskeyApiGet } from '@/scripts/misskey-api.js';
|
import { misskeyApi, misskeyApiGet } from '@/scripts/misskey-api.js';
|
||||||
import { useTooltip } from '@/scripts/use-tooltip.js';
|
import { useTooltip } from '@/scripts/use-tooltip.js';
|
||||||
|
@ -102,7 +102,7 @@ async function toggleReaction() {
|
||||||
|
|
||||||
async function menu(ev) {
|
async function menu(ev) {
|
||||||
if (!canToggle.value) return;
|
if (!canToggle.value) return;
|
||||||
if (!props.reaction.includes(":")) return;
|
if (!props.reaction.includes(':')) return;
|
||||||
os.popupMenu([{
|
os.popupMenu([{
|
||||||
text: i18n.ts.info,
|
text: i18n.ts.info,
|
||||||
icon: 'ti ti-info-circle',
|
icon: 'ti ti-info-circle',
|
||||||
|
@ -117,8 +117,7 @@ async function menu(ev) {
|
||||||
}
|
}
|
||||||
|
|
||||||
function anime() {
|
function anime() {
|
||||||
if (document.hidden) return;
|
if (document.hidden || !defaultStore.state.animation || buttonEl.value == null) return;
|
||||||
if (!defaultStore.state.animation) return;
|
|
||||||
|
|
||||||
const rect = buttonEl.value.getBoundingClientRect();
|
const rect = buttonEl.value.getBoundingClientRect();
|
||||||
const x = rect.left + 16;
|
const x = rect.left + 16;
|
||||||
|
|
|
@ -23,10 +23,9 @@ import { initChart } from '@/scripts/init-chart.js';
|
||||||
|
|
||||||
initChart();
|
initChart();
|
||||||
|
|
||||||
const rootEl = shallowRef<HTMLDivElement>(null);
|
const rootEl = shallowRef<HTMLDivElement | null>(null);
|
||||||
const chartEl = shallowRef<HTMLCanvasElement>(null);
|
const chartEl = shallowRef<HTMLCanvasElement | null>(null);
|
||||||
const now = new Date();
|
let chartInstance: Chart | null = null;
|
||||||
let chartInstance: Chart = null;
|
|
||||||
const fetching = ref(true);
|
const fetching = ref(true);
|
||||||
|
|
||||||
const { handler: externalTooltipHandler } = useChartTooltip({
|
const { handler: externalTooltipHandler } = useChartTooltip({
|
||||||
|
@ -34,6 +33,7 @@ const { handler: externalTooltipHandler } = useChartTooltip({
|
||||||
});
|
});
|
||||||
|
|
||||||
async function renderChart() {
|
async function renderChart() {
|
||||||
|
if (rootEl.value == null) return;
|
||||||
if (chartInstance) {
|
if (chartInstance) {
|
||||||
chartInstance.destroy();
|
chartInstance.destroy();
|
||||||
}
|
}
|
||||||
|
@ -47,7 +47,12 @@ async function renderChart() {
|
||||||
|
|
||||||
raw = raw.slice(0, maxDays + 1);
|
raw = raw.slice(0, maxDays + 1);
|
||||||
|
|
||||||
const data = [];
|
const data: {
|
||||||
|
x: number;
|
||||||
|
y: string;
|
||||||
|
v: number;
|
||||||
|
}[] = [];
|
||||||
|
|
||||||
for (const record of raw) {
|
for (const record of raw) {
|
||||||
data.push({
|
data.push({
|
||||||
x: 0,
|
x: 0,
|
||||||
|
@ -83,19 +88,20 @@ async function renderChart() {
|
||||||
|
|
||||||
const marginEachCell = 12;
|
const marginEachCell = 12;
|
||||||
|
|
||||||
|
if (chartEl.value == null) return;
|
||||||
|
|
||||||
chartInstance = new Chart(chartEl.value, {
|
chartInstance = new Chart(chartEl.value, {
|
||||||
type: 'matrix',
|
type: 'matrix',
|
||||||
data: {
|
data: {
|
||||||
datasets: [{
|
datasets: [{
|
||||||
label: 'Active',
|
label: 'Active',
|
||||||
data: data,
|
data: data as any,
|
||||||
pointRadius: 0,
|
|
||||||
borderWidth: 0,
|
borderWidth: 0,
|
||||||
borderJoinStyle: 'round',
|
|
||||||
borderRadius: 3,
|
borderRadius: 3,
|
||||||
backgroundColor(c) {
|
backgroundColor(c) {
|
||||||
const value = c.dataset.data[c.dataIndex].v;
|
const v = c.dataset.data[c.dataIndex] as unknown as typeof data[0];
|
||||||
const m = max(c.dataset.data[c.dataIndex].y);
|
const value = v.v;
|
||||||
|
const m = max(v.y);
|
||||||
if (m === 0) {
|
if (m === 0) {
|
||||||
return alpha(color, 0);
|
return alpha(color, 0);
|
||||||
} else {
|
} else {
|
||||||
|
@ -103,7 +109,6 @@ async function renderChart() {
|
||||||
return alpha(color, a);
|
return alpha(color, a);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
fill: true,
|
|
||||||
width(c) {
|
width(c) {
|
||||||
const a = c.chart.chartArea ?? {};
|
const a = c.chart.chartArea ?? {};
|
||||||
return (a.right - a.left) / maxDays - marginEachCell;
|
return (a.right - a.left) / maxDays - marginEachCell;
|
||||||
|
@ -146,7 +151,6 @@ async function renderChart() {
|
||||||
},
|
},
|
||||||
y: {
|
y: {
|
||||||
type: 'time',
|
type: 'time',
|
||||||
min: new Date(new Date().getFullYear(), new Date().getMonth(), new Date().getDate() - maxDays),
|
|
||||||
offset: true,
|
offset: true,
|
||||||
reverse: true,
|
reverse: true,
|
||||||
position: 'left',
|
position: 'left',
|
||||||
|
@ -179,7 +183,7 @@ async function renderChart() {
|
||||||
return getYYYYMMDD(new Date(new Date(v.y).getTime() + (v.x * 86400000)));
|
return getYYYYMMDD(new Date(new Date(v.y).getTime() + (v.x * 86400000)));
|
||||||
},
|
},
|
||||||
label(context) {
|
label(context) {
|
||||||
const v = context.dataset.data[context.dataIndex];
|
const v = context.dataset.data[context.dataIndex] as unknown as typeof data[0];
|
||||||
const m = max(v.y);
|
const m = max(v.y);
|
||||||
if (m === 0) {
|
if (m === 0) {
|
||||||
return [`Active: ${v.v} (-%)`];
|
return [`Active: ${v.v} (-%)`];
|
||||||
|
|
|
@ -20,11 +20,11 @@ import { misskeyApi } from '@/scripts/misskey-api.js';
|
||||||
|
|
||||||
initChart();
|
initChart();
|
||||||
|
|
||||||
const chartEl = shallowRef<HTMLCanvasElement>(null);
|
const chartEl = shallowRef<HTMLCanvasElement | null>(null);
|
||||||
|
|
||||||
const { handler: externalTooltipHandler } = useChartTooltip();
|
const { handler: externalTooltipHandler } = useChartTooltip();
|
||||||
|
|
||||||
let chartInstance: Chart;
|
let chartInstance: Chart | null = null;
|
||||||
|
|
||||||
const getYYYYMMDD = (date: Date) => {
|
const getYYYYMMDD = (date: Date) => {
|
||||||
const y = date.getFullYear().toString().padStart(2, '0');
|
const y = date.getFullYear().toString().padStart(2, '0');
|
||||||
|
@ -47,6 +47,8 @@ onMounted(async () => {
|
||||||
const accent = tinycolor(getComputedStyle(document.documentElement).getPropertyValue('--accent'));
|
const accent = tinycolor(getComputedStyle(document.documentElement).getPropertyValue('--accent'));
|
||||||
const color = accent.toHex();
|
const color = accent.toHex();
|
||||||
|
|
||||||
|
if (chartEl.value == null) return;
|
||||||
|
|
||||||
chartInstance = new Chart(chartEl.value, {
|
chartInstance = new Chart(chartEl.value, {
|
||||||
type: 'line',
|
type: 'line',
|
||||||
data: {
|
data: {
|
||||||
|
@ -67,7 +69,7 @@ onMounted(async () => {
|
||||||
x: (i + 1).toString(),
|
x: (i + 1).toString(),
|
||||||
y: (v / record.users) * 100,
|
y: (v / record.users) * 100,
|
||||||
d: getYYYYMMDD(new Date(record.createdAt)),
|
d: getYYYYMMDD(new Date(record.createdAt)),
|
||||||
}))],
|
}))] as any,
|
||||||
})),
|
})),
|
||||||
},
|
},
|
||||||
options: {
|
options: {
|
||||||
|
@ -109,11 +111,11 @@ onMounted(async () => {
|
||||||
enabled: false,
|
enabled: false,
|
||||||
callbacks: {
|
callbacks: {
|
||||||
title(context) {
|
title(context) {
|
||||||
const v = context[0].dataset.data[context[0].dataIndex];
|
const v = context[0].dataset.data[context[0].dataIndex] as unknown as { x: string, y: number, d: string };
|
||||||
return `${v.x} days later`;
|
return `${v.x} days later`;
|
||||||
},
|
},
|
||||||
label(context) {
|
label(context) {
|
||||||
const v = context.dataset.data[context.dataIndex];
|
const v = context.dataset.data[context.dataIndex] as unknown as { x: string, y: number, d: string };
|
||||||
const p = Math.round(v.y) + '%';
|
const p = Math.round(v.y) + '%';
|
||||||
return `${v.d} ${p}`;
|
return `${v.d} ${p}`;
|
||||||
},
|
},
|
||||||
|
|
|
@ -31,11 +31,12 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { onMounted, nextTick, ref, watch, computed, toRefs, VNode, useSlots } from 'vue';
|
import { onMounted, nextTick, ref, watch, computed, toRefs, VNode, useSlots, VNodeChild } from 'vue';
|
||||||
import MkButton from '@/components/MkButton.vue';
|
import MkButton from '@/components/MkButton.vue';
|
||||||
import * as os from '@/os.js';
|
import * as os from '@/os.js';
|
||||||
import { useInterval } from '@/scripts/use-interval.js';
|
import { useInterval } from '@/scripts/use-interval.js';
|
||||||
import { i18n } from '@/i18n.js';
|
import { i18n } from '@/i18n.js';
|
||||||
|
import { MenuItem } from '@/types/menu.js';
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
modelValue: string | null;
|
modelValue: string | null;
|
||||||
|
@ -51,7 +52,7 @@ const props = defineProps<{
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
(ev: 'changeByUser'): void;
|
(ev: 'changeByUser', value: string | null): void;
|
||||||
(ev: 'update:modelValue', value: string | null): void;
|
(ev: 'update:modelValue', value: string | null): void;
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
|
@ -73,7 +74,7 @@ const height =
|
||||||
props.large ? 39 :
|
props.large ? 39 :
|
||||||
36;
|
36;
|
||||||
|
|
||||||
const focus = () => inputEl.value.focus();
|
const focus = () => inputEl.value?.focus();
|
||||||
const onInput = (ev) => {
|
const onInput = (ev) => {
|
||||||
changed.value = true;
|
changed.value = true;
|
||||||
};
|
};
|
||||||
|
@ -87,17 +88,19 @@ watch(modelValue, newValue => {
|
||||||
v.value = newValue;
|
v.value = newValue;
|
||||||
});
|
});
|
||||||
|
|
||||||
watch(v, newValue => {
|
watch(v, () => {
|
||||||
if (!props.manualSave) {
|
if (!props.manualSave) {
|
||||||
updated();
|
updated();
|
||||||
}
|
}
|
||||||
|
|
||||||
invalid.value = inputEl.value.validity.badInput;
|
invalid.value = inputEl.value?.validity.badInput ?? true;
|
||||||
});
|
});
|
||||||
|
|
||||||
// このコンポーネントが作成された時、非表示状態である場合がある
|
// このコンポーネントが作成された時、非表示状態である場合がある
|
||||||
// 非表示状態だと要素の幅などは0になってしまうので、定期的に計算する
|
// 非表示状態だと要素の幅などは0になってしまうので、定期的に計算する
|
||||||
useInterval(() => {
|
useInterval(() => {
|
||||||
|
if (inputEl.value == null) return;
|
||||||
|
|
||||||
if (prefixEl.value) {
|
if (prefixEl.value) {
|
||||||
if (prefixEl.value.offsetWidth) {
|
if (prefixEl.value.offsetWidth) {
|
||||||
inputEl.value.style.paddingLeft = prefixEl.value.offsetWidth + 'px';
|
inputEl.value.style.paddingLeft = prefixEl.value.offsetWidth + 'px';
|
||||||
|
@ -121,39 +124,41 @@ onMounted(() => {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
function show(ev: MouseEvent) {
|
function show() {
|
||||||
if (inputEl.value && inputEl.value.hasAttribute('disabled')) {
|
if (inputEl.value && inputEl.value.hasAttribute('disabled')) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
focused.value = true;
|
focused.value = true;
|
||||||
opening.value = true;
|
opening.value = true;
|
||||||
|
|
||||||
const menu = [];
|
const menu: MenuItem[] = [];
|
||||||
let options = slots.default!();
|
let options = slots.default!();
|
||||||
|
|
||||||
const pushOption = (option: VNode) => {
|
const pushOption = (option: VNode) => {
|
||||||
menu.push({
|
menu.push({
|
||||||
text: option.children,
|
text: option.children as string,
|
||||||
active: computed(() => v.value === option.props.value),
|
active: computed(() => v.value === option.props?.value),
|
||||||
action: () => {
|
action: () => {
|
||||||
v.value = option.props.value;
|
v.value = option.props?.value;
|
||||||
emit('changeByUser', v.value);
|
emit('changeByUser', v.value);
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const scanOptions = (options: VNode[]) => {
|
const scanOptions = (options: VNodeChild[]) => {
|
||||||
for (const vnode of options) {
|
for (const vnode of options) {
|
||||||
|
if (typeof vnode !== 'object' || vnode === null || Array.isArray(vnode)) continue;
|
||||||
if (vnode.type === 'optgroup') {
|
if (vnode.type === 'optgroup') {
|
||||||
const optgroup = vnode;
|
const optgroup = vnode;
|
||||||
menu.push({
|
menu.push({
|
||||||
type: 'label',
|
type: 'label',
|
||||||
text: optgroup.props.label,
|
text: optgroup.props?.label,
|
||||||
});
|
});
|
||||||
scanOptions(optgroup.children);
|
if (Array.isArray(optgroup.children)) scanOptions(optgroup.children);
|
||||||
} else if (Array.isArray(vnode.children)) { // 何故かフラグメントになってくることがある
|
} else if (Array.isArray(vnode.children)) { // 何故かフラグメントになってくることがある
|
||||||
const fragment = vnode;
|
const fragment = vnode;
|
||||||
scanOptions(fragment.children);
|
if (Array.isArray(fragment.children)) scanOptions(fragment.children);
|
||||||
} else if (vnode.props == null) { // v-if で条件が false のときにこうなる
|
} else if (vnode.props == null) { // v-if で条件が false のときにこうなる
|
||||||
// nop?
|
// nop?
|
||||||
} else {
|
} else {
|
||||||
|
@ -166,7 +171,7 @@ function show(ev: MouseEvent) {
|
||||||
scanOptions(options);
|
scanOptions(options);
|
||||||
|
|
||||||
os.popupMenu(menu, container.value, {
|
os.popupMenu(menu, container.value, {
|
||||||
width: container.value.offsetWidth,
|
width: container.value?.offsetWidth,
|
||||||
onClosing: () => {
|
onClosing: () => {
|
||||||
opening.value = false;
|
opening.value = false;
|
||||||
},
|
},
|
||||||
|
|
|
@ -112,6 +112,7 @@ function onLogin(res: any): Promise<void> | void {
|
||||||
}
|
}
|
||||||
|
|
||||||
async function queryKey(): Promise<void> {
|
async function queryKey(): Promise<void> {
|
||||||
|
if (credentialRequest.value == null) return;
|
||||||
queryingKey.value = true;
|
queryingKey.value = true;
|
||||||
await webAuthnRequest(credentialRequest.value)
|
await webAuthnRequest(credentialRequest.value)
|
||||||
.catch(() => {
|
.catch(() => {
|
||||||
|
|
|
@ -80,6 +80,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { ref, computed } from 'vue';
|
import { ref, computed } from 'vue';
|
||||||
import { toUnicode } from 'punycode/';
|
import { toUnicode } from 'punycode/';
|
||||||
|
import * as Misskey from 'misskey-js';
|
||||||
import MkButton from './MkButton.vue';
|
import MkButton from './MkButton.vue';
|
||||||
import MkInput from './MkInput.vue';
|
import MkInput from './MkInput.vue';
|
||||||
import MkCaptcha, { type Captcha } from '@/components/MkCaptcha.vue';
|
import MkCaptcha, { type Captcha } from '@/components/MkCaptcha.vue';
|
||||||
|
@ -97,7 +98,7 @@ const props = withDefaults(defineProps<{
|
||||||
});
|
});
|
||||||
|
|
||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
(ev: 'signup', user: Record<string, any>): void;
|
(ev: 'signup', user: Misskey.entities.SigninResponse): void;
|
||||||
(ev: 'signupEmailPending'): void;
|
(ev: 'signupEmailPending'): void;
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
|
|
|
@ -34,8 +34,8 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
<template #label>{{ tosPrivacyPolicyLabel }}</template>
|
<template #label>{{ tosPrivacyPolicyLabel }}</template>
|
||||||
<template #suffix><i v-if="agreeTosAndPrivacyPolicy" class="ti ti-check" style="color: var(--success)"></i></template>
|
<template #suffix><i v-if="agreeTosAndPrivacyPolicy" class="ti ti-check" style="color: var(--success)"></i></template>
|
||||||
<div class="_gaps_s">
|
<div class="_gaps_s">
|
||||||
<div v-if="availableTos"><a :href="instance.tosUrl" class="_link" rel="nofollow noopener" target="_blank">{{ i18n.ts.termsOfService }} <i class="ti ti-external-link"></i></a></div>
|
<div v-if="availableTos"><a :href="instance.tosUrl ?? undefined" class="_link" rel="nofollow noopener" target="_blank">{{ i18n.ts.termsOfService }} <i class="ti ti-external-link"></i></a></div>
|
||||||
<div v-if="availablePrivacyPolicy"><a :href="instance.privacyPolicyUrl" class="_link" rel="nofollow noopener" target="_blank">{{ i18n.ts.privacyPolicy }} <i class="ti ti-external-link"></i></a></div>
|
<div v-if="availablePrivacyPolicy"><a :href="instance.privacyPolicyUrl ?? undefined" class="_link" rel="nofollow noopener" target="_blank">{{ i18n.ts.privacyPolicy }} <i class="ti ti-external-link"></i></a></div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<MkSwitch :modelValue="agreeTosAndPrivacyPolicy" style="margin-top: 16px;" @update:modelValue="updateAgreeTosAndPrivacyPolicy">{{ i18n.ts.agree }}</MkSwitch>
|
<MkSwitch :modelValue="agreeTosAndPrivacyPolicy" style="margin-top: 16px;" @update:modelValue="updateAgreeTosAndPrivacyPolicy">{{ i18n.ts.agree }}</MkSwitch>
|
||||||
|
@ -96,7 +96,7 @@ const tosPrivacyPolicyLabel = computed(() => {
|
||||||
} else if (availablePrivacyPolicy) {
|
} else if (availablePrivacyPolicy) {
|
||||||
return i18n.ts.privacyPolicy;
|
return i18n.ts.privacyPolicy;
|
||||||
} else {
|
} else {
|
||||||
return "";
|
return '';
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -8,7 +8,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
ref="dialog"
|
ref="dialog"
|
||||||
:width="500"
|
:width="500"
|
||||||
:height="600"
|
:height="600"
|
||||||
@close="dialog.close()"
|
@close="dialog?.close()"
|
||||||
@closed="$emit('closed')"
|
@closed="$emit('closed')"
|
||||||
>
|
>
|
||||||
<template #header>{{ i18n.ts.signup }}</template>
|
<template #header>{{ i18n.ts.signup }}</template>
|
||||||
|
@ -22,7 +22,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
:leaveToClass="$style.transition_x_leaveTo"
|
:leaveToClass="$style.transition_x_leaveTo"
|
||||||
>
|
>
|
||||||
<template v-if="!isAcceptedServerRule">
|
<template v-if="!isAcceptedServerRule">
|
||||||
<XServerRules @done="isAcceptedServerRule = true" @cancel="dialog.close()"/>
|
<XServerRules @done="isAcceptedServerRule = true" @cancel="dialog?.close()"/>
|
||||||
</template>
|
</template>
|
||||||
<template v-else>
|
<template v-else>
|
||||||
<XSignup :autoSet="autoSet" @signup="onSignup" @signupEmailPending="onSignupEmailPending"/>
|
<XSignup :autoSet="autoSet" @signup="onSignup" @signupEmailPending="onSignupEmailPending"/>
|
||||||
|
@ -34,7 +34,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { shallowRef, ref } from 'vue';
|
import { shallowRef, ref } from 'vue';
|
||||||
|
import * as Misskey from 'misskey-js';
|
||||||
import XSignup from '@/components/MkSignupDialog.form.vue';
|
import XSignup from '@/components/MkSignupDialog.form.vue';
|
||||||
import XServerRules from '@/components/MkSignupDialog.rules.vue';
|
import XServerRules from '@/components/MkSignupDialog.rules.vue';
|
||||||
import MkModalWindow from '@/components/MkModalWindow.vue';
|
import MkModalWindow from '@/components/MkModalWindow.vue';
|
||||||
|
@ -47,7 +47,7 @@ const props = withDefaults(defineProps<{
|
||||||
});
|
});
|
||||||
|
|
||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
(ev: 'done'): void;
|
(ev: 'done', res: Misskey.entities.SigninResponse): void;
|
||||||
(ev: 'closed'): void;
|
(ev: 'closed'): void;
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
|
@ -55,13 +55,13 @@ const dialog = shallowRef<InstanceType<typeof MkModalWindow>>();
|
||||||
|
|
||||||
const isAcceptedServerRule = ref(false);
|
const isAcceptedServerRule = ref(false);
|
||||||
|
|
||||||
function onSignup(res) {
|
function onSignup(res: Misskey.entities.SigninResponse) {
|
||||||
emit('done', res);
|
emit('done', res);
|
||||||
dialog.value.close();
|
dialog.value?.close();
|
||||||
}
|
}
|
||||||
|
|
||||||
function onSignupEmailPending() {
|
function onSignupEmailPending() {
|
||||||
dialog.value.close();
|
dialog.value?.close();
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|
|
@ -89,10 +89,11 @@ let ro: ResizeObserver | undefined;
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
ro = new ResizeObserver((entries, observer) => {
|
ro = new ResizeObserver((entries, observer) => {
|
||||||
width.value = el.value?.offsetWidth + 64;
|
if (el.value == null) return;
|
||||||
height.value = el.value?.offsetHeight + 64;
|
width.value = el.value.offsetWidth + 64;
|
||||||
|
height.value = el.value.offsetHeight + 64;
|
||||||
});
|
});
|
||||||
ro.observe(el.value);
|
if (el.value) ro.observe(el.value);
|
||||||
const add = () => {
|
const add = () => {
|
||||||
if (stop) return;
|
if (stop) return;
|
||||||
const x = (Math.random() * (width.value - 64));
|
const x = (Math.random() * (width.value - 64));
|
||||||
|
|
|
@ -7,18 +7,18 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
<div :class="[$style.root, { [$style.collapsed]: collapsed }]">
|
<div :class="[$style.root, { [$style.collapsed]: collapsed }]">
|
||||||
<div>
|
<div>
|
||||||
<span v-if="note.isHidden" style="opacity: 0.5">({{ i18n.ts.private }})</span>
|
<span v-if="note.isHidden" style="opacity: 0.5">({{ i18n.ts.private }})</span>
|
||||||
<span v-if="note.deletedAt" style="opacity: 0.5">({{ i18n.ts.deleted }})</span>
|
<span v-if="note.deletedAt" style="opacity: 0.5">({{ i18n.ts.deletedNote }})</span>
|
||||||
<MkA v-if="note.replyId" :class="$style.reply" :to="`/notes/${note.replyId}`"><i class="ti ti-arrow-back-up"></i></MkA>
|
<MkA v-if="note.replyId" :class="$style.reply" :to="`/notes/${note.replyId}`"><i class="ti ti-arrow-back-up"></i></MkA>
|
||||||
<Mfm v-if="note.text" :text="note.text" :author="note.user" :nyaize="'respect'" :emojiUrls="note.emojis"/>
|
<Mfm v-if="note.text" :text="note.text" :author="note.user" :nyaize="'respect'" :emojiUrls="note.emojis"/>
|
||||||
<MkA v-if="note.renoteId" :class="$style.rp" :to="`/notes/${note.renoteId}`">RN: ...</MkA>
|
<MkA v-if="note.renoteId" :class="$style.rp" :to="`/notes/${note.renoteId}`">RN: ...</MkA>
|
||||||
</div>
|
</div>
|
||||||
<details v-if="note.files.length > 0">
|
<details v-if="note.files && note.files.length > 0">
|
||||||
<summary>({{ i18n.tsx.withNFiles({ n: note.files.length }) }})</summary>
|
<summary>({{ i18n.tsx.withNFiles({ n: note.files.length }) }})</summary>
|
||||||
<MkMediaList :mediaList="note.files"/>
|
<MkMediaList :mediaList="note.files"/>
|
||||||
</details>
|
</details>
|
||||||
<details v-if="note.poll">
|
<details v-if="note.poll">
|
||||||
<summary>{{ i18n.ts.poll }}</summary>
|
<summary>{{ i18n.ts.poll }}</summary>
|
||||||
<MkPoll :note="note"/>
|
<MkPoll :noteId="note.id" :poll="note.poll"/>
|
||||||
</details>
|
</details>
|
||||||
<button v-if="isLong && collapsed" :class="$style.fade" class="_button" @click="collapsed = false">
|
<button v-if="isLong && collapsed" :class="$style.fade" class="_button" @click="collapsed = false">
|
||||||
<span :class="$style.fadeLabel">{{ i18n.ts.showMore }}</span>
|
<span :class="$style.fadeLabel">{{ i18n.ts.showMore }}</span>
|
||||||
|
|
|
@ -24,7 +24,7 @@ import { i18n } from '@/i18n.js';
|
||||||
|
|
||||||
const props = withDefaults(defineProps<{
|
const props = withDefaults(defineProps<{
|
||||||
checked: boolean | Ref<boolean>;
|
checked: boolean | Ref<boolean>;
|
||||||
disabled?: boolean;
|
disabled?: boolean | Ref<boolean>;
|
||||||
}>(), {
|
}>(), {
|
||||||
disabled: false,
|
disabled: false,
|
||||||
});
|
});
|
||||||
|
|
|
@ -13,18 +13,18 @@ export default defineComponent({
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
setup(props, { emit, slots }) {
|
setup(props, { emit, slots }) {
|
||||||
const options = slots.default();
|
const options = slots.default?.() ?? [];
|
||||||
|
|
||||||
return () => h('div', {
|
return () => h('div', {
|
||||||
class: 'pxhvhrfw',
|
class: 'pxhvhrfw',
|
||||||
}, options.map(option => withDirectives(h('button', {
|
}, options.map(option => withDirectives(h('button', {
|
||||||
class: ['_button', { active: props.modelValue === option.props.value }],
|
class: ['_button', { active: props.modelValue === option.props?.value }],
|
||||||
key: option.key,
|
key: option.key as string,
|
||||||
disabled: props.modelValue === option.props.value,
|
disabled: props.modelValue === option.props?.value,
|
||||||
onClick: () => {
|
onClick: () => {
|
||||||
emit('update:modelValue', option.props.value);
|
emit('update:modelValue', option.props?.value);
|
||||||
},
|
},
|
||||||
}, option.children), [
|
}, option.children ?? []), [
|
||||||
[resolveDirective('click-anime')],
|
[resolveDirective('click-anime')],
|
||||||
])));
|
])));
|
||||||
},
|
},
|
||||||
|
|
|
@ -52,7 +52,7 @@ watch(available, () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
width.value = rootEl.value.offsetWidth;
|
if (rootEl.value) width.value = rootEl.value.offsetWidth;
|
||||||
|
|
||||||
if (loaded) {
|
if (loaded) {
|
||||||
available.value = true;
|
available.value = true;
|
||||||
|
|
|
@ -17,7 +17,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
:readonly="readonly"
|
:readonly="readonly"
|
||||||
:placeholder="placeholder"
|
:placeholder="placeholder"
|
||||||
:pattern="pattern"
|
:pattern="pattern"
|
||||||
:autocomplete="props.autocomplete"
|
:autocomplete="autocomplete"
|
||||||
:spellcheck="spellcheck"
|
:spellcheck="spellcheck"
|
||||||
@focus="focused = true"
|
@focus="focused = true"
|
||||||
@blur="focused = false"
|
@blur="focused = false"
|
||||||
|
@ -76,9 +76,9 @@ const invalid = ref(false);
|
||||||
const filled = computed(() => v.value !== '' && v.value != null);
|
const filled = computed(() => v.value !== '' && v.value != null);
|
||||||
const inputEl = shallowRef<HTMLTextAreaElement>();
|
const inputEl = shallowRef<HTMLTextAreaElement>();
|
||||||
const preview = ref(false);
|
const preview = ref(false);
|
||||||
let autocomplete: Autocomplete;
|
let autocompleteWorker: Autocomplete | null = null;
|
||||||
|
|
||||||
const focus = () => inputEl.value.focus();
|
const focus = () => inputEl.value?.focus();
|
||||||
const onInput = (ev) => {
|
const onInput = (ev) => {
|
||||||
changed.value = true;
|
changed.value = true;
|
||||||
emit('change', ev);
|
emit('change', ev);
|
||||||
|
@ -111,10 +111,10 @@ const updated = () => {
|
||||||
const debouncedUpdated = debounce(1000, updated);
|
const debouncedUpdated = debounce(1000, updated);
|
||||||
|
|
||||||
watch(modelValue, newValue => {
|
watch(modelValue, newValue => {
|
||||||
v.value = newValue;
|
v.value = newValue ?? '';
|
||||||
});
|
});
|
||||||
|
|
||||||
watch(v, newValue => {
|
watch(v, () => {
|
||||||
if (!props.manualSave) {
|
if (!props.manualSave) {
|
||||||
if (props.debounce) {
|
if (props.debounce) {
|
||||||
debouncedUpdated();
|
debouncedUpdated();
|
||||||
|
@ -123,7 +123,7 @@ watch(v, newValue => {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
invalid.value = inputEl.value.validity.badInput;
|
invalid.value = inputEl.value?.validity.badInput ?? true;
|
||||||
});
|
});
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
|
@ -133,14 +133,14 @@ onMounted(() => {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
if (props.mfmAutocomplete) {
|
if (props.mfmAutocomplete && inputEl.value) {
|
||||||
autocomplete = new Autocomplete(inputEl.value, v, props.mfmAutocomplete === true ? null : props.mfmAutocomplete);
|
autocompleteWorker = new Autocomplete(inputEl.value, v, props.mfmAutocomplete === true ? undefined : props.mfmAutocomplete);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
onUnmounted(() => {
|
onUnmounted(() => {
|
||||||
if (autocomplete) {
|
if (autocompleteWorker) {
|
||||||
autocomplete.detach();
|
autocompleteWorker.detach();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
|
@ -17,6 +17,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
|
import Misskey from 'misskey-js';
|
||||||
import { computed, watch, onUnmounted, provide, shallowRef } from 'vue';
|
import { computed, watch, onUnmounted, provide, shallowRef } from 'vue';
|
||||||
import { Connection } from 'misskey-js/streaming.js';
|
import { Connection } from 'misskey-js/streaming.js';
|
||||||
import MkNotes from '@/components/MkNotes.vue';
|
import MkNotes from '@/components/MkNotes.vue';
|
||||||
|
@ -29,7 +30,7 @@ import { defaultStore } from '@/store.js';
|
||||||
import { Paging } from '@/components/MkPagination.vue';
|
import { Paging } from '@/components/MkPagination.vue';
|
||||||
|
|
||||||
const props = withDefaults(defineProps<{
|
const props = withDefaults(defineProps<{
|
||||||
src: string;
|
src: 'home' | 'local' | 'media' | 'social' | 'global' | 'mentions' | 'directs' | 'list' | 'antenna' | 'channel' | 'role';
|
||||||
list?: string;
|
list?: string;
|
||||||
antenna?: string;
|
antenna?: string;
|
||||||
channel?: string;
|
channel?: string;
|
||||||
|
@ -94,6 +95,7 @@ const stream = useStream();
|
||||||
|
|
||||||
function connectChannel() {
|
function connectChannel() {
|
||||||
if (props.src === 'antenna') {
|
if (props.src === 'antenna') {
|
||||||
|
if (props.antenna == null) return;
|
||||||
connection = stream.useChannel('antenna', {
|
connection = stream.useChannel('antenna', {
|
||||||
antennaId: props.antenna,
|
antennaId: props.antenna,
|
||||||
});
|
});
|
||||||
|
@ -138,21 +140,24 @@ function connectChannel() {
|
||||||
connection = stream.useChannel('main');
|
connection = stream.useChannel('main');
|
||||||
connection.on('mention', onNote);
|
connection.on('mention', onNote);
|
||||||
} else if (props.src === 'list') {
|
} else if (props.src === 'list') {
|
||||||
|
if (props.list == null) return;
|
||||||
connection = stream.useChannel('userList', {
|
connection = stream.useChannel('userList', {
|
||||||
withRenotes: props.withRenotes,
|
withRenotes: props.withRenotes,
|
||||||
withFiles: props.onlyFiles ? true : undefined,
|
withFiles: props.onlyFiles ? true : undefined,
|
||||||
listId: props.list,
|
listId: props.list,
|
||||||
});
|
});
|
||||||
} else if (props.src === 'channel') {
|
} else if (props.src === 'channel') {
|
||||||
|
if (props.channel == null) return;
|
||||||
connection = stream.useChannel('channel', {
|
connection = stream.useChannel('channel', {
|
||||||
channelId: props.channel,
|
channelId: props.channel,
|
||||||
});
|
});
|
||||||
} else if (props.src === 'role') {
|
} else if (props.src === 'role') {
|
||||||
|
if (props.role == null) return;
|
||||||
connection = stream.useChannel('roleTimeline', {
|
connection = stream.useChannel('roleTimeline', {
|
||||||
roleId: props.role,
|
roleId: props.role,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
if (props.src !== 'directs' || props.src !== 'mentions') connection.on('note', prepend);
|
if (props.src !== 'directs' && props.src !== 'mentions') connection.on('note', prepend);
|
||||||
}
|
}
|
||||||
|
|
||||||
function disconnectChannel() {
|
function disconnectChannel() {
|
||||||
|
@ -161,7 +166,7 @@ function disconnectChannel() {
|
||||||
}
|
}
|
||||||
|
|
||||||
function updatePaginationQuery() {
|
function updatePaginationQuery() {
|
||||||
let endpoint: string | null;
|
let endpoint: keyof Misskey.Endpoints | null;
|
||||||
let query: TimelineQueryType | null;
|
let query: TimelineQueryType | null;
|
||||||
|
|
||||||
if (props.src === 'antenna') {
|
if (props.src === 'antenna') {
|
||||||
|
|
|
@ -55,7 +55,7 @@ const el = shallowRef<HTMLElement>();
|
||||||
const zIndex = os.claimZIndex('high');
|
const zIndex = os.claimZIndex('high');
|
||||||
|
|
||||||
function setPosition() {
|
function setPosition() {
|
||||||
if (!el.value || !props.targetElement) return;
|
if (el.value == null) return;
|
||||||
const data = calcPopupPosition(el.value, {
|
const data = calcPopupPosition(el.value, {
|
||||||
anchorElement: props.targetElement,
|
anchorElement: props.targetElement,
|
||||||
direction: props.direction,
|
direction: props.direction,
|
||||||
|
|
|
@ -17,7 +17,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
<div v-else-if="phase === 'howToReact'" class="_gaps">
|
<div v-else-if="phase === 'howToReact'" class="_gaps">
|
||||||
<div style="text-align: center; padding: 0 16px;">{{ i18n.ts._initialTutorial._reaction.description }}</div>
|
<div style="text-align: center; padding: 0 16px;">{{ i18n.ts._initialTutorial._reaction.description }}</div>
|
||||||
<div>{{ i18n.ts._initialTutorial._reaction.letsTryReacting }}</div>
|
<div>{{ i18n.ts._initialTutorial._reaction.letsTryReacting }}</div>
|
||||||
<MkNote :class="$style.exampleNoteRoot" :note="exampleNote" :mock="true" @reaction="addReaction" @removeReaction="removeReaction" @updateReaction="updateReaction"/>
|
<MkNote :class="$style.exampleNoteRoot" :note="exampleNote" :mock="true" @reaction="addReaction" @removeReaction="removeReaction"/>
|
||||||
<div v-if="onceReacted"><b style="color: var(--accent);"><i class="ti ti-check"></i> {{ i18n.ts._initialTutorial.wellDone }}</b> {{ i18n.ts._initialTutorial._reaction.reactNotification }}<br>{{ i18n.ts._initialTutorial._reaction.reactDone }}</div>
|
<div v-if="onceReacted"><b style="color: var(--accent);"><i class="ti ti-check"></i> {{ i18n.ts._initialTutorial.wellDone }}</b> {{ i18n.ts._initialTutorial._reaction.reactNotification }}<br>{{ i18n.ts._initialTutorial._reaction.reactDone }}</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
@ -53,7 +53,7 @@ const exampleNote = reactive<Misskey.entities.Note>({
|
||||||
isBot: false,
|
isBot: false,
|
||||||
isCat: true,
|
isCat: true,
|
||||||
emojis: {},
|
emojis: {},
|
||||||
onlineStatus: null,
|
onlineStatus: 'unknown',
|
||||||
badgeRoles: [],
|
badgeRoles: [],
|
||||||
},
|
},
|
||||||
text: 'just setting up my msky',
|
text: 'just setting up my msky',
|
||||||
|
@ -86,7 +86,6 @@ function doNotification(emoji: string): void {
|
||||||
const notification: Misskey.entities.Notification = {
|
const notification: Misskey.entities.Notification = {
|
||||||
id: Math.random().toString(),
|
id: Math.random().toString(),
|
||||||
createdAt: new Date().toUTCString(),
|
createdAt: new Date().toUTCString(),
|
||||||
isRead: false,
|
|
||||||
type: 'reaction',
|
type: 'reaction',
|
||||||
reaction: emoji,
|
reaction: emoji,
|
||||||
user: $i,
|
user: $i,
|
||||||
|
|
|
@ -58,7 +58,7 @@ const exampleCWNote = reactive<Misskey.entities.Note>({
|
||||||
isBot: false,
|
isBot: false,
|
||||||
isCat: true,
|
isCat: true,
|
||||||
emojis: {},
|
emojis: {},
|
||||||
onlineStatus: null,
|
onlineStatus: 'unknown',
|
||||||
badgeRoles: [],
|
badgeRoles: [],
|
||||||
},
|
},
|
||||||
text: i18n.ts._initialTutorial._postNote._cw._exampleNote.note,
|
text: i18n.ts._initialTutorial._postNote._cw._exampleNote.note,
|
||||||
|
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue