Merge remote-tracking branch 'misskey-original/develop' into develop

# Conflicts:
#	package.json
#	packages/backend/src/server/api/endpoints.ts
#	packages/frontend/src/components/MkButton.vue
#	packages/frontend/src/components/MkDrive.vue
#	packages/frontend/src/components/MkEmojiPicker.section.vue
#	packages/frontend/src/components/MkEmojiPicker.vue
#	packages/frontend/src/components/MkFoldableSection.vue
#	packages/frontend/src/components/MkInput.vue
#	packages/frontend/src/components/MkMenu.vue
#	packages/frontend/src/components/MkNote.vue
#	packages/frontend/src/components/MkPostForm.vue
#	packages/frontend/src/components/MkSignupDialog.form.vue
#	packages/frontend/src/components/MkSwitch.button.vue
#	packages/frontend/src/components/MkTab.vue
#	packages/frontend/src/components/MkTimeline.vue
#	packages/frontend/src/components/MkUserSelectDialog.vue
#	packages/frontend/src/components/global/MkCustomEmoji.vue
#	packages/frontend/src/components/global/MkMisskeyFlavoredMarkdown.ts
#	packages/frontend/src/os.ts
#	packages/frontend/src/pages/admin/index.vue
#	packages/frontend/src/pages/user/home.vue
#	packages/frontend/src/ui/universal.vue
This commit is contained in:
mattyatea 2024-01-31 19:51:51 +09:00
commit 942b7f1b3c
250 changed files with 2788 additions and 1788 deletions

View File

@ -0,0 +1,66 @@
name: Deploy test environment
on:
push:
workflow_dispatch:
inputs:
repository:
description: 'Repository to deploy (optional)'
required: false
branch:
description: 'Branch to deploy (optional)'
required: false
jobs:
deploy-test-environment:
runs-on: ubuntu-latest
steps:
- name: Set environment variable (for tput command & pnpm)
run: |
echo "TERM=xterm" >> $GITHUB_ENV
REPOSITORY=${{ github.event.inputs.repository || github.repository }}
echo "REPOSITORY=$REPOSITORY" >> $GITHUB_ENV
BRANCH=${{ github.event.inputs.branch || github.ref_name }}
echo "BRANCH=$BRANCH" >> $GITHUB_ENV
- name: Checkout
uses: actions/checkout@v4
with:
repository: ${{ env.REPOSITORY }}
ref: ${{ env.BRANCH }}
- name: Get the latest commit SHA
run: |
SHA=$(git log -1 --format="%H")
echo "SHA=$SHA" >> $GITHUB_ENV
- name: Start cloudflare tunnel (quick)
run: |
wget https://github.com/cloudflare/cloudflared/releases/latest/download/cloudflared-linux-amd64.deb
sudo dpkg -i cloudflared-linux-amd64.deb
sudo cloudflared tunnel --metrics localhost:55555 --url localhost:3000 > /dev/null 2>&1 &
sleep 15
TUNNEL_RESPONSE=$(curl http://localhost:55555/quicktunnel)
TUNNEL_DOMAIN=$(echo $TUNNEL_RESPONSE | grep -o '"hostname":"[^"]*' | grep -o '[^"]*$')
echo "::add-mask::$TUNNEL_DOMAIN"
echo "TUNNEL_DOMAIN=$TUNNEL_DOMAIN" >> $GITHUB_ENV
- name: Install misskey
run: |
wget https://raw.githubusercontent.com/joinmisskey/bash-install/v4/misskey-install.sh
wget https://raw.githubusercontent.com/joinmisskey/bash-install/v4/testenv_githubactions.txt
sed -i "s/host=127.0.0.1/host=$TUNNEL_DOMAIN/g" testenv_githubactions.txt
sed -i "s|git_repository=https://github.com/misskey-dev/misskey|git_repository=https://github.com/$REPOSITORY|g" testenv_githubactions.txt
sed -i "s|git_branch=master|git_branch=$BRANCH|g" testenv_githubactions.txt
sudo chmod 555 ./misskey-install.sh
sudo bash -x ./misskey-install.sh -c ./testenv_githubactions.txt
- name: Post tunnel info to Discord
run: |
CURRENT_TIME=$(TZ=Asia/Tokyo date +'%Y-%m-%d %H:%M:%S JST')
COMMIT_URL="https://github.com/$REPOSITORY/commit/$SHA"
curl -X POST -H "Content-Type: application/json" -d "{\"content\": \"==============================\nURL: https://$TUNNEL_DOMAIN\nRepository: $REPOSITORY\nBranch: $BRANCH\nCommit: $COMMIT_URL\nTime: $CURRENT_TIME\n==============================\"}" ${{ secrets.DISCORD_WEBHOOK_URL }}
- name: Wait
run: |
timeout 3600 tail -f /var/log/syslog || true

View File

@ -50,7 +50,14 @@
- 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: 連合先のレートリミットに引っかかった際にリトライするようになりました

2
locales/index.d.ts vendored
View File

@ -5098,7 +5098,7 @@ export interface Locale extends ILocale {
*/ */
"readConfirmText": ParameterizedString<"title">; "readConfirmText": ParameterizedString<"title">;
/** /**
* UXを損ねる可能性が高いため使 * UXを損ねる可能性が高いため使
*/ */
"shouldNotBeUsedToPresentPermanentInfo": string; "shouldNotBeUsedToPresentPermanentInfo": string;
/** /**

View File

@ -1274,7 +1274,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: "オンにすると、このお知らせは通知されず、既読にする必要もなくなります。"

View File

@ -1,6 +1,6 @@
{ {
"name": "misskey", "name": "misskey",
"version": "2024.2.0-beta.7-PrisMisskey.1", "version": "2024.2.0-beta.8-PrisMisskey.1",
"codename": "nasubi", "codename": "nasubi",
"repository": { "repository": {
"type": "git", "type": "git",

View File

@ -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.10.2", "@bull-board/api": "5.14.0",
"@bull-board/fastify": "5.10.2", "@bull-board/fastify": "5.14.0",
"@bull-board/ui": "5.10.2", "@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.2.10", "@nestjs/core": "10.2.10",
"@nestjs/testing": "10.2.10", "@nestjs/testing": "10.2.10",
"@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.1.10", "@smithy/node-http-handler": "2.1.10",
"@swc/cli": "0.1.63", "@swc/cli": "0.1.63",
"@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",
@ -98,7 +98,7 @@
"bcryptjs": "2.4.3", "bcryptjs": "2.4.3",
"blurhash": "2.0.5", "blurhash": "2.0.5",
"body-parser": "1.20.2", "body-parser": "1.20.2",
"bullmq": "5.1.4", "bullmq": "5.1.5",
"cacheable-lookup": "7.0.0", "cacheable-lookup": "7.0.0",
"cbor": "9.0.1", "cbor": "9.0.1",
"chalk": "5.3.0", "chalk": "5.3.0",
@ -115,7 +115,7 @@
"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",
@ -147,7 +147,7 @@
"otpauth": "9.2.2", "otpauth": "9.2.2",
"parse5": "7.1.2", "parse5": "7.1.2",
"pg": "8.11.3", "pg": "8.11.3",
"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",
@ -168,12 +168,12 @@
"slacc": "0.0.10", "slacc": "0.0.10",
"strict-event-emitter-types": "2.0.0", "strict-event-emitter-types": "2.0.0",
"stringz": "2.1.0", "stringz": "2.1.0",
"systeminformation": "5.21.23", "systeminformation": "5.21.24",
"tinycolor2": "1.6.0", "tinycolor2": "1.6.0",
"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",
@ -184,7 +184,7 @@
"devDependencies": { "devDependencies": {
"@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.0", "@nestjs/platform-express": "10.3.1",
"@simplewebauthn/typescript-types": "8.3.4", "@simplewebauthn/typescript-types": "8.3.4",
"@swc/jest": "0.2.31", "@swc/jest": "0.2.31",
"@types/accepts": "1.3.7", "@types/accepts": "1.3.7",
@ -203,13 +203,13 @@
"@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.5", "@types/node": "20.11.10",
"@types/node-fetch": "3.0.3", "@types/node-fetch": "3.0.3",
"@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",

View File

@ -96,7 +96,7 @@ export class AccountMoveService {
await this.apDeliverManagerService.deliverToFollowers(src, moveAct); await this.apDeliverManagerService.deliverToFollowers(src, moveAct);
// Publish meUpdated event // Publish meUpdated event
const iObj = await this.userEntityService.pack<true, true>(src.id, src, { detail: true, includeSecrets: true }); const iObj = await this.userEntityService.pack(src.id, src, { schema: 'MeDetailed', includeSecrets: true });
this.globalEventService.publishMainStream(src.id, 'meUpdated', iObj); this.globalEventService.publishMainStream(src.id, 'meUpdated', iObj);
// Unfollow after 24 hours // Unfollow after 24 hours

View File

@ -54,15 +54,15 @@ 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<'UserLite'>;
unfollow: Packed<'User'>; unfollow: Packed<'UserDetailedNotMe'>;
meUpdated: Packed<'User'>; meUpdated: Packed<'MeDetailed'>;
pageEvent: { pageEvent: {
pageId: MiPage['id']; pageId: MiPage['id'];
event: string; event: string;
var: any; var: any;
userId: MiUser['id']; userId: MiUser['id'];
user: Packed<'User'>; user: Packed<'UserDetailed'>;
}; };
urlUploadFinished: { urlUploadFinished: {
marker?: string | null; marker?: string | null;
@ -92,7 +92,7 @@ export interface MainEventTypes {
}; };
driveFileCreated: Packed<'DriveFile'>; driveFileCreated: Packed<'DriveFile'>;
readAntenna: MiAntenna; readAntenna: MiAntenna;
receiveFollowRequest: Packed<'User'>; receiveFollowRequest: Packed<'UserLite'>;
announcementCreated: { announcementCreated: {
announcement: Packed<'Announcement'>; announcement: Packed<'Announcement'>;
}; };
@ -140,8 +140,8 @@ type NoteStreamEventTypes = {
}; };
export interface UserListEventTypes { export interface UserListEventTypes {
userAdded: Packed<'User'>; userAdded: Packed<'UserLite'>;
userRemoved: Packed<'User'>; userRemoved: Packed<'UserLite'>;
} }
export interface AntennaEventTypes { export interface AntennaEventTypes {

View File

@ -109,13 +109,13 @@ export class UserBlockingService implements OnModuleInit {
if (this.userEntityService.isLocalUser(followee)) { if (this.userEntityService.isLocalUser(followee)) {
this.userEntityService.pack(followee, followee, { this.userEntityService.pack(followee, followee, {
detail: true, schema: 'MeDetailed',
}).then(packed => this.globalEventService.publishMainStream(followee.id, 'meUpdated', packed)); }).then(packed => this.globalEventService.publishMainStream(followee.id, 'meUpdated', packed));
} }
if (this.userEntityService.isLocalUser(follower) && !silent) { if (this.userEntityService.isLocalUser(follower) && !silent) {
this.userEntityService.pack(followee, follower, { this.userEntityService.pack(followee, follower, {
detail: true, schema: 'UserDetailedNotMe',
}).then(async packed => { }).then(async packed => {
this.globalEventService.publishMainStream(follower.id, 'unfollow', packed); this.globalEventService.publishMainStream(follower.id, 'unfollow', packed);

View File

@ -293,9 +293,9 @@ export class UserFollowingService implements OnModuleInit {
if (this.userEntityService.isLocalUser(follower) && !silent) { if (this.userEntityService.isLocalUser(follower) && !silent) {
// Publish follow event // Publish follow event
this.userEntityService.pack(followee.id, follower, { this.userEntityService.pack(followee.id, follower, {
detail: true, schema: 'UserDetailedNotMe',
}).then(async packed => { }).then(async packed => {
this.globalEventService.publishMainStream(follower.id, 'follow', packed as Packed<'UserDetailedNotMe'>); this.globalEventService.publishMainStream(follower.id, 'follow', packed);
const webhooks = (await this.webhookService.getActiveWebhooks()).filter(x => x.userId === follower.id && x.on.includes('follow')); const webhooks = (await this.webhookService.getActiveWebhooks()).filter(x => x.userId === follower.id && x.on.includes('follow'));
for (const webhook of webhooks) { for (const webhook of webhooks) {
@ -360,7 +360,7 @@ export class UserFollowingService implements OnModuleInit {
if (!silent && this.userEntityService.isLocalUser(follower)) { if (!silent && this.userEntityService.isLocalUser(follower)) {
// Publish unfollow event // Publish unfollow event
this.userEntityService.pack(followee.id, follower, { this.userEntityService.pack(followee.id, follower, {
detail: true, schema: 'UserDetailedNotMe',
}).then(async packed => { }).then(async packed => {
this.globalEventService.publishMainStream(follower.id, 'unfollow', packed); this.globalEventService.publishMainStream(follower.id, 'unfollow', packed);
@ -500,7 +500,7 @@ export class UserFollowingService implements OnModuleInit {
this.userEntityService.pack(follower.id, followee).then(packed => this.globalEventService.publishMainStream(followee.id, 'receiveFollowRequest', packed)); this.userEntityService.pack(follower.id, followee).then(packed => this.globalEventService.publishMainStream(followee.id, 'receiveFollowRequest', packed));
this.userEntityService.pack(followee.id, followee, { this.userEntityService.pack(followee.id, followee, {
detail: true, schema: 'MeDetailed',
}).then(packed => this.globalEventService.publishMainStream(followee.id, 'meUpdated', packed)); }).then(packed => this.globalEventService.publishMainStream(followee.id, 'meUpdated', packed));
// 通知を作成 // 通知を作成
@ -548,7 +548,7 @@ export class UserFollowingService implements OnModuleInit {
}); });
this.userEntityService.pack(followee.id, followee, { this.userEntityService.pack(followee.id, followee, {
detail: true, schema: 'MeDetailed',
}).then(packed => this.globalEventService.publishMainStream(followee.id, 'meUpdated', packed)); }).then(packed => this.globalEventService.publishMainStream(followee.id, 'meUpdated', packed));
} }
@ -576,7 +576,7 @@ export class UserFollowingService implements OnModuleInit {
} }
this.userEntityService.pack(followee.id, followee, { this.userEntityService.pack(followee.id, followee, {
detail: true, schema: 'MeDetailed',
}).then(packed => this.globalEventService.publishMainStream(followee.id, 'meUpdated', packed)); }).then(packed => this.globalEventService.publishMainStream(followee.id, 'meUpdated', packed));
} }
@ -696,7 +696,7 @@ export class UserFollowingService implements OnModuleInit {
@bindThis @bindThis
private async publishUnfollow(followee: Both, follower: Local): Promise<void> { private async publishUnfollow(followee: Both, follower: Local): Promise<void> {
const packedFollowee = await this.userEntityService.pack(followee.id, follower, { const packedFollowee = await this.userEntityService.pack(followee.id, follower, {
detail: true, schema: 'UserDetailedNotMe',
}); });
this.globalEventService.publishMainStream(follower.id, 'unfollow', packedFollowee); this.globalEventService.publishMainStream(follower.id, 'unfollow', packedFollowee);

View File

@ -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>>>;

View File

@ -60,13 +60,13 @@ export class AbuseUserReportEntityService {
targetUserId: report.targetUserId, targetUserId: report.targetUserId,
assigneeId: report.assigneeId, assigneeId: report.assigneeId,
reporter: this.userEntityService.pack(report.reporter ?? report.reporterId, null, { reporter: this.userEntityService.pack(report.reporter ?? report.reporterId, null, {
detail: true, schema: 'UserDetailedNotMe',
}), }),
targetUser: this.userEntityService.pack(report.targetUser ?? report.targetUserId, null, { targetUser: this.userEntityService.pack(report.targetUser ?? report.targetUserId, null, {
detail: true, schema: 'UserDetailedNotMe',
}), }),
assignee: report.assigneeId ? this.userEntityService.pack(report.assignee ?? report.assigneeId, null, { assignee: report.assigneeId ? this.userEntityService.pack(report.assignee ?? report.assigneeId, null, {
detail: true, schema: 'UserDetailedNotMe',
}) : null, }) : null,
forwarded: report.forwarded, forwarded: report.forwarded,
}); });

View File

@ -37,7 +37,7 @@ export class BlockingEntityService {
createdAt: this.idService.parse(blocking.id).date.toISOString(), createdAt: this.idService.parse(blocking.id).date.toISOString(),
blockeeId: blocking.blockeeId, blockeeId: blocking.blockeeId,
blockee: this.userEntityService.pack(blocking.blockeeId, me, { blockee: this.userEntityService.pack(blocking.blockeeId, me, {
detail: true, schema: 'UserDetailedNotMe',
}), }),
}); });
} }

View File

@ -42,7 +42,7 @@ export class FlashEntityService {
createdAt: this.idService.parse(flash.id).date.toISOString(), createdAt: this.idService.parse(flash.id).date.toISOString(),
updatedAt: flash.updatedAt.toISOString(), updatedAt: flash.updatedAt.toISOString(),
userId: flash.userId, userId: flash.userId,
user: this.userEntityService.pack(flash.user ?? flash.userId, me), // { detail: true } すると無限ループするので注意 user: this.userEntityService.pack(flash.user ?? flash.userId, me), // { schema: 'UserDetailed' } すると無限ループするので注意
title: flash.title, title: flash.title,
summary: flash.summary, summary: flash.summary,
script: flash.script, script: flash.script,

View File

@ -89,10 +89,10 @@ export class FollowingEntityService {
followeeId: following.followeeId, followeeId: following.followeeId,
followerId: following.followerId, followerId: following.followerId,
followee: opts.populateFollowee ? this.userEntityService.pack(following.followee ?? following.followeeId, me, { followee: opts.populateFollowee ? this.userEntityService.pack(following.followee ?? following.followeeId, me, {
detail: true, schema: 'UserDetailedNotMe',
}) : undefined, }) : undefined,
follower: opts.populateFollower ? this.userEntityService.pack(following.follower ?? following.followerId, me, { follower: opts.populateFollower ? this.userEntityService.pack(following.follower ?? following.followerId, me, {
detail: true, schema: 'UserDetailedNotMe',
}) : undefined, }) : undefined,
}); });
} }

View File

@ -37,7 +37,7 @@ export class ModerationLogEntityService {
info: log.info, info: log.info,
userId: log.userId, userId: log.userId,
user: this.userEntityService.pack(log.user ?? log.userId, null, { user: this.userEntityService.pack(log.user ?? log.userId, null, {
detail: true, schema: 'UserDetailedNotMe',
}), }),
}); });
} }

View File

@ -39,7 +39,7 @@ export class MutingEntityService {
expiresAt: muting.expiresAt ? muting.expiresAt.toISOString() : null, expiresAt: muting.expiresAt ? muting.expiresAt.toISOString() : null,
muteeId: muting.muteeId, muteeId: muting.muteeId,
mutee: this.userEntityService.pack(muting.muteeId, me, { mutee: this.userEntityService.pack(muting.muteeId, me, {
detail: true, schema: 'UserDetailedNotMe',
}), }),
}); });
} }

View File

@ -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,
}; };
} }
@ -325,9 +325,7 @@ export class NoteEntityService implements OnModuleInit {
createdAt: this.idService.parse(note.id).date.toISOString(), createdAt: this.idService.parse(note.id).date.toISOString(),
updatedAt: note.updatedAt ? note.updatedAt.toISOString() : undefined, updatedAt: note.updatedAt ? note.updatedAt.toISOString() : undefined,
userId: note.userId, userId: note.userId,
user: this.userEntityService.pack(note.user ?? note.userId, me, { user: this.userEntityService.pack(note.user ?? note.userId, me),
detail: false,
}),
text: text, text: text,
cw: note.cw, cw: note.cw,
visibility: note.visibility, visibility: note.visibility,

View File

@ -62,7 +62,7 @@ export class NotificationEntityService implements OnModuleInit {
}, },
hint?: { hint?: {
packedNotes: Map<MiNote['id'], Packed<'Note'>>; packedNotes: Map<MiNote['id'], Packed<'Note'>>;
packedUsers: Map<MiUser['id'], Packed<'User'>>; packedUsers: Map<MiUser['id'], Packed<'UserLite'>>;
}, },
): Promise<Packed<'Notification'>> { ): Promise<Packed<'Notification'>> {
const notification = src; const notification = src;
@ -76,9 +76,7 @@ export class NotificationEntityService implements OnModuleInit {
const userIfNeed = 'notifierId' in notification ? ( const userIfNeed = 'notifierId' in notification ? (
hint?.packedUsers != null hint?.packedUsers != null
? hint.packedUsers.get(notification.notifierId) ? hint.packedUsers.get(notification.notifierId)
: this.userEntityService.pack(notification.notifierId, { id: meId }, { : this.userEntityService.pack(notification.notifierId, { id: meId })
detail: false,
})
) : undefined; ) : undefined;
const role = notification.type === 'roleAssigned' ? await this.roleEntityService.pack(notification.roleId) : undefined; const role = notification.type === 'roleAssigned' ? await this.roleEntityService.pack(notification.roleId) : undefined;
@ -131,9 +129,7 @@ export class NotificationEntityService implements OnModuleInit {
const users = userIds.length > 0 ? await this.usersRepository.find({ const users = userIds.length > 0 ? await this.usersRepository.find({
where: { id: In(userIds) }, where: { id: In(userIds) },
}) : []; }) : [];
const packedUsersArray = await this.userEntityService.packMany(users, { id: meId }, { const packedUsersArray = await this.userEntityService.packMany(users, { id: meId });
detail: false,
});
const packedUsers = new Map(packedUsersArray.map(p => [p.id, p])); const packedUsers = new Map(packedUsersArray.map(p => [p.id, p]));
// 既に解決されたフォローリクエストの通知を除外 // 既に解決されたフォローリクエストの通知を除外
@ -161,7 +157,7 @@ export class NotificationEntityService implements OnModuleInit {
}, },
hint?: { hint?: {
packedNotes: Map<MiNote['id'], Packed<'Note'>>; packedNotes: Map<MiNote['id'], Packed<'Note'>>;
packedUsers: Map<MiUser['id'], Packed<'User'>>; packedUsers: Map<MiUser['id'], Packed<'UserLite'>>;
}, },
): Promise<Packed<'Notification'>> { ): Promise<Packed<'Notification'>> {
const notification = src; const notification = src;
@ -175,18 +171,14 @@ export class NotificationEntityService implements OnModuleInit {
const userIfNeed = 'notifierId' in notification ? ( const userIfNeed = 'notifierId' in notification ? (
hint?.packedUsers != null hint?.packedUsers != null
? hint.packedUsers.get(notification.notifierId) ? hint.packedUsers.get(notification.notifierId)
: this.userEntityService.pack(notification.notifierId, { id: meId }, { : this.userEntityService.pack(notification.notifierId, { id: meId })
detail: false,
})
) : undefined; ) : undefined;
if (notification.type === 'reaction:grouped') { if (notification.type === 'reaction:grouped') {
const reactions = await Promise.all(notification.reactions.map(async reaction => { const reactions = await Promise.all(notification.reactions.map(async reaction => {
const user = hint?.packedUsers != null const user = hint?.packedUsers != null
? hint.packedUsers.get(reaction.userId)! ? hint.packedUsers.get(reaction.userId)!
: await this.userEntityService.pack(reaction.userId, { id: meId }, { : await this.userEntityService.pack(reaction.userId, { id: meId });
detail: false,
});
return { return {
user, user,
reaction: reaction.reaction, reaction: reaction.reaction,
@ -206,9 +198,7 @@ export class NotificationEntityService implements OnModuleInit {
return packedUser; return packedUser;
} }
return this.userEntityService.pack(userId, { id: meId }, { return this.userEntityService.pack(userId, { id: meId });
detail: false,
});
})); }));
return await awaitAll({ return await awaitAll({
id: notification.id, id: notification.id,
@ -275,9 +265,7 @@ export class NotificationEntityService implements OnModuleInit {
const users = userIds.length > 0 ? await this.usersRepository.find({ const users = userIds.length > 0 ? await this.usersRepository.find({
where: { id: In(userIds) }, where: { id: In(userIds) },
}) : []; }) : [];
const packedUsersArray = await this.userEntityService.packMany(users, { id: meId }, { const packedUsersArray = await this.userEntityService.packMany(users, { id: meId });
detail: false,
});
const packedUsers = new Map(packedUsersArray.map(p => [p.id, p])); const packedUsers = new Map(packedUsersArray.map(p => [p.id, p]));
// 既に解決されたフォローリクエストの通知を除外 // 既に解決されたフォローリクエストの通知を除外

View File

@ -90,7 +90,7 @@ export class PageEntityService {
createdAt: this.idService.parse(page.id).date.toISOString(), createdAt: this.idService.parse(page.id).date.toISOString(),
updatedAt: page.updatedAt.toISOString(), updatedAt: page.updatedAt.toISOString(),
userId: page.userId, userId: page.userId,
user: this.userEntityService.pack(page.user ?? page.userId, me), // { detail: true } すると無限ループするので注意 user: this.userEntityService.pack(page.user ?? page.userId, me), // { schema: 'UserDetailed' } すると無限ループするので注意
content: page.content, content: page.content,
variables: page.variables, variables: page.variables,
title: page.title, title: page.title,

View File

@ -38,7 +38,7 @@ export class RenoteMutingEntityService {
createdAt: this.idService.parse(muting.id).date.toISOString(), createdAt: this.idService.parse(muting.id).date.toISOString(),
muteeId: muting.muteeId, muteeId: muting.muteeId,
mutee: this.userEntityService.pack(muting.muteeId, me, { mutee: this.userEntityService.pack(muting.muteeId, me, {
detail: true, schema: 'UserDetailedNotMe',
}), }),
}); });
} }

View File

@ -30,14 +30,6 @@ import type { NoteEntityService } from './NoteEntityService.js';
import type { DriveFileEntityService } from './DriveFileEntityService.js'; import type { DriveFileEntityService } from './DriveFileEntityService.js';
import type { PageEntityService } from './PageEntityService.js'; import type { PageEntityService } from './PageEntityService.js';
type IsUserDetailed<Detailed extends boolean> = Detailed extends true ? Packed<'UserDetailed'> : Packed<'UserLite'>;
type IsMeAndIsUserDetailed<ExpectsMe extends boolean | null, Detailed extends boolean> =
Detailed extends true ?
ExpectsMe extends true ? Packed<'MeDetailed'> :
ExpectsMe extends false ? Packed<'UserDetailedNotMe'> :
Packed<'UserDetailed'> :
Packed<'UserLite'>;
const Ajv = _Ajv.default; const Ajv = _Ajv.default;
const ajv = new Ajv(); const ajv = new Ajv();
@ -303,33 +295,34 @@ export class UserEntityService implements OnModuleInit {
return `${this.config.url}/users/${userId}`; return `${this.config.url}/users/${userId}`;
} }
public async pack<ExpectsMe extends boolean | null = null, D extends boolean = false>( public async pack<S extends 'MeDetailed' | 'UserDetailedNotMe' | 'UserDetailed' | 'UserLite' = 'UserLite'>(
src: MiUser['id'] | MiUser, src: MiUser['id'] | MiUser,
me?: { id: MiUser['id']; } | null | undefined, me?: { id: MiUser['id']; } | null | undefined,
options?: { options?: {
detail?: D, schema?: S,
includeSecrets?: boolean, includeSecrets?: boolean,
userProfile?: MiUserProfile, userProfile?: MiUserProfile,
}, },
): Promise<IsMeAndIsUserDetailed<ExpectsMe, D>> { ): Promise<Packed<S>> {
const opts = Object.assign({ const opts = Object.assign({
detail: false, schema: 'UserLite',
includeSecrets: false, includeSecrets: false,
}, options); }, options);
const user = typeof src === 'object' ? src : await this.usersRepository.findOneByOrFail({ id: src }); const user = typeof src === 'object' ? src : await this.usersRepository.findOneByOrFail({ id: src });
const isDetailed = opts.schema !== 'UserLite';
const meId = me ? me.id : null; const meId = me ? me.id : null;
const isMe = meId === user.id; const isMe = meId === user.id;
const iAmModerator = me ? await this.roleService.isModerator(me as MiUser) : false; const iAmModerator = me ? await this.roleService.isModerator(me as MiUser) : false;
const relation = meId && !isMe && opts.detail ? await this.getRelation(meId, user.id) : null; const relation = meId && !isMe && isDetailed ? await this.getRelation(meId, user.id) : null;
const pins = opts.detail ? await this.userNotePiningsRepository.createQueryBuilder('pin') const pins = isDetailed ? await this.userNotePiningsRepository.createQueryBuilder('pin')
.where('pin.userId = :userId', { userId: user.id }) .where('pin.userId = :userId', { userId: user.id })
.innerJoinAndSelect('pin.note', 'note') .innerJoinAndSelect('pin.note', 'note')
.orderBy('pin.id', 'DESC') .orderBy('pin.id', 'DESC')
.getMany() : []; .getMany() : [];
const profile = opts.detail ? (opts.userProfile ?? await this.userProfilesRepository.findOneByOrFail({ userId: user.id })) : null; const profile = isDetailed ? (opts.userProfile ?? await this.userProfilesRepository.findOneByOrFail({ userId: user.id })) : null;
const followingCount = profile == null ? null : const followingCount = profile == null ? null :
(profile.followingVisibility === 'public') || isMe ? user.followingCount : (profile.followingVisibility === 'public') || isMe ? user.followingCount :
@ -341,15 +334,15 @@ export class UserEntityService implements OnModuleInit {
(profile.followersVisibility === 'followers') && (relation && relation.isFollowing) ? user.followersCount : (profile.followersVisibility === 'followers') && (relation && relation.isFollowing) ? user.followersCount :
null; null;
const isModerator = isMe && opts.detail ? this.roleService.isModerator(user) : null; const isModerator = isMe && isDetailed ? this.roleService.isModerator(user) : null;
const isAdmin = isMe && opts.detail ? this.roleService.isAdministrator(user) : null; const isAdmin = isMe && isDetailed ? this.roleService.isAdministrator(user) : null;
const unreadAnnouncements = isMe && opts.detail ? const unreadAnnouncements = isMe && isDetailed ?
(await this.announcementService.getUnreadAnnouncements(user)).map((announcement) => ({ (await this.announcementService.getUnreadAnnouncements(user)).map((announcement) => ({
createdAt: this.idService.parse(announcement.id).date.toISOString(), createdAt: this.idService.parse(announcement.id).date.toISOString(),
...announcement, ...announcement,
})) : null; })) : null;
const notificationsInfo = isMe && opts.detail ? await this.getNotificationsInfo(user.id) : null; const notificationsInfo = isMe && isDetailed ? await this.getNotificationsInfo(user.id) : null;
const packed = { const packed = {
id: user.id, id: user.id,
@ -386,7 +379,7 @@ export class UserEntityService implements OnModuleInit {
displayOrder: r.displayOrder, displayOrder: r.displayOrder,
}))) : undefined, }))) : undefined,
...(opts.detail ? { ...(isDetailed ? {
url: profile!.url, url: profile!.url,
uri: user.uri, uri: user.uri,
movedTo: user.movedToUri ? this.apPersonService.resolvePerson(user.movedToUri).then(user => user.id).catch(() => null) : null, movedTo: user.movedToUri ? this.apPersonService.resolvePerson(user.movedToUri).then(user => user.id).catch(() => null) : null,
@ -444,7 +437,7 @@ export class UserEntityService implements OnModuleInit {
moderationNote: iAmModerator ? (profile!.moderationNote ?? '') : undefined, moderationNote: iAmModerator ? (profile!.moderationNote ?? '') : undefined,
} : {}), } : {}),
...(opts.detail && isMe ? { ...(isDetailed && isMe ? {
avatarId: user.avatarId, avatarId: user.avatarId,
bannerId: user.bannerId, bannerId: user.bannerId,
isModerator: isModerator, isModerator: isModerator,
@ -516,19 +509,19 @@ export class UserEntityService implements OnModuleInit {
notify: relation.following?.notify ?? 'none', notify: relation.following?.notify ?? 'none',
withReplies: relation.following?.withReplies ?? false, withReplies: relation.following?.withReplies ?? false,
} : {}), } : {}),
} as Promiseable<Packed<'User'>> as Promiseable<IsMeAndIsUserDetailed<ExpectsMe, D>>; } as Promiseable<Packed<S>>;
return await awaitAll(packed); return await awaitAll(packed);
} }
public packMany<D extends boolean = false>( public packMany<S extends 'MeDetailed' | 'UserDetailedNotMe' | 'UserDetailed' | 'UserLite' = 'UserLite'>(
users: (MiUser['id'] | MiUser)[], users: (MiUser['id'] | MiUser)[],
me?: { id: MiUser['id'] } | null | undefined, me?: { id: MiUser['id'] } | null | undefined,
options?: { options?: {
detail?: D, schema?: S,
includeSecrets?: boolean, includeSecrets?: boolean,
}, },
): Promise<IsUserDetailed<D>[]> { ): Promise<Packed<S>[]> {
return Promise.all(users.map(u => this.pack(u, me, options))); return Promise.all(users.map(u => this.pack(u, me, options)));
} }
} }

View File

@ -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 {

View File

@ -25,7 +25,7 @@ import { packedBlockingSchema } from '@/models/json-schema/blocking.js';
import { packedNoteReactionSchema } from '@/models/json-schema/note-reaction.js'; import { packedNoteReactionSchema } from '@/models/json-schema/note-reaction.js';
import { packedHashtagSchema } from '@/models/json-schema/hashtag.js'; import { packedHashtagSchema } from '@/models/json-schema/hashtag.js';
import { packedInviteCodeSchema } from '@/models/json-schema/invite-code.js'; import { packedInviteCodeSchema } from '@/models/json-schema/invite-code.js';
import { packedPageSchema } from '@/models/json-schema/page.js'; import { packedPageSchema, packedPageBlockSchema } from '@/models/json-schema/page.js';
import { packedNoteFavoriteSchema } from '@/models/json-schema/note-favorite.js'; import { packedNoteFavoriteSchema } from '@/models/json-schema/note-favorite.js';
import { packedChannelSchema } from '@/models/json-schema/channel.js'; import { packedChannelSchema } from '@/models/json-schema/channel.js';
import { packedAntennaSchema } from '@/models/json-schema/antenna.js'; import { packedAntennaSchema } from '@/models/json-schema/antenna.js';
@ -37,7 +37,7 @@ import { packedEmojiDetailedSchema, packedEmojiRequestSimpleSchema, packedEmojiS
import { packedFlashSchema } from '@/models/json-schema/flash.js'; import { packedFlashSchema } from '@/models/json-schema/flash.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';
@ -67,6 +67,7 @@ export const refs = {
Hashtag: packedHashtagSchema, Hashtag: packedHashtagSchema,
InviteCode: packedInviteCodeSchema, InviteCode: packedInviteCodeSchema,
Page: packedPageSchema, Page: packedPageSchema,
PageBlock: packedPageBlockSchema,
Channel: packedChannelSchema, Channel: packedChannelSchema,
QueueCount: packedQueueCountSchema, QueueCount: packedQueueCountSchema,
Antenna: packedAntennaSchema, Antenna: packedAntennaSchema,
@ -81,12 +82,16 @@ 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,
}; };
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 :

View File

@ -38,7 +38,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 ... お知らせページ掲載 + バナー表示
@ -47,7 +47,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,

View File

@ -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',

View File

@ -25,7 +25,7 @@ export const packedBlockingSchema = {
blockee: { blockee: {
type: 'object', type: 'object',
optional: false, nullable: false, optional: false, nullable: false,
ref: 'UserDetailed', ref: 'UserDetailedNotMe',
}, },
}, },
} as const; } as const;

View File

@ -30,12 +30,12 @@ export const packedFollowingSchema = {
followee: { followee: {
type: 'object', type: 'object',
optional: true, nullable: false, optional: true, nullable: false,
ref: 'UserDetailed', ref: 'UserDetailedNotMe',
}, },
follower: { follower: {
type: 'object', type: 'object',
optional: true, nullable: false, optional: true, nullable: false,
ref: 'UserDetailed', ref: 'UserDetailedNotMe',
}, },
}, },
} as const; } as const;

View File

@ -30,7 +30,7 @@ export const packedMutingSchema = {
mutee: { mutee: {
type: 'object', type: 'object',
optional: false, nullable: false, optional: false, nullable: false,
ref: 'UserDetailed', ref: 'UserDetailedNotMe',
}, },
}, },
} as const; } as const;

View File

@ -74,6 +74,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',
@ -122,6 +123,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',
@ -167,9 +210,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',
@ -201,7 +258,7 @@ export const packedNoteSchema = {
}, },
myReaction: { myReaction: {
type: 'object', type: 'string',
optional: true, nullable: true, optional: true, nullable: true,
}, },
}, },

View File

@ -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;

View File

@ -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: {

View File

@ -25,7 +25,7 @@ export const packedRenoteMutingSchema = {
mutee: { mutee: {
type: 'object', type: 'object',
optional: false, nullable: false, optional: false, nullable: false,
ref: 'UserDetailed', ref: 'UserDetailedNotMe',
}, },
}, },
} as const; } as const;

View File

@ -1,26 +1,103 @@
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,
},
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,
},
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,31 +198,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,
canUseTranslator: rolePolicyValue,
avatarDecorationLimit: rolePolicyValue,
}, },
}, },
usersCount: { usersCount: {

View File

@ -599,104 +599,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,
},
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: {
@ -791,13 +694,5 @@ export const packedUserSchema = {
type: 'object', type: 'object',
ref: 'UserDetailed', ref: 'UserDetailed',
}, },
{
type: 'object',
ref: 'UserDetailedNotMe',
},
{
type: 'object',
ref: 'MeDetailed',
},
], ],
} as const; } as const;

View File

@ -204,7 +204,7 @@ export class ServerService implements OnApplicationShutdown {
}); });
this.globalEventService.publishMainStream(profile.userId, 'meUpdated', await this.userEntityService.pack(profile.userId, { id: profile.userId }, { this.globalEventService.publishMainStream(profile.userId, 'meUpdated', await this.userEntityService.pack(profile.userId, { id: profile.userId }, {
detail: true, schema: 'MeDetailed',
includeSecrets: true, includeSecrets: true,
})); }));

View File

@ -157,7 +157,7 @@ export class ApiServerService {
return { return {
ok: true, ok: true,
token: token.token, token: token.token,
user: await this.userEntityService.pack(token.userId, null, { detail: true }), user: await this.userEntityService.pack(token.userId, null, { schema: 'UserDetailedNotMe' }),
}; };
} else { } else {
return { return {

View File

@ -213,7 +213,7 @@ export class SignupApiService {
}); });
const res = await this.userEntityService.pack(account, account, { const res = await this.userEntityService.pack(account, account, {
detail: true, schema: 'MeDetailed',
includeSecrets: true, includeSecrets: true,
}); });

View File

@ -4,7 +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 { RolePolicies } from '@/core/RoleService.js';
import * as ep___admin_emoji_setlocalOnlyBulk from './endpoints/admin/emoji/set-localonly-bulk.js'; import * as ep___admin_emoji_setlocalOnlyBulk from './endpoints/admin/emoji/set-localonly-bulk.js';
import * as ep___admin_emoji_setisSensitiveBulk from './endpoints/admin/emoji/set-issensitive-bulk.js'; import * as ep___admin_emoji_setisSensitiveBulk from './endpoints/admin/emoji/set-issensitive-bulk.js';
@ -799,7 +799,7 @@ interface IEndpointMetaBase {
*/ */
readonly requireAdmin?: boolean; readonly requireAdmin?: boolean;
readonly requireRolePolicy?: keyof RolePolicies; readonly requireRolePolicy?: KeyOf<'RolePolicies'>;
/** /**
* *

View File

@ -62,17 +62,17 @@ export const meta = {
reporter: { reporter: {
type: 'object', type: 'object',
nullable: false, optional: false, nullable: false, optional: false,
ref: 'User', ref: 'UserDetailedNotMe',
}, },
targetUser: { targetUser: {
type: 'object', type: 'object',
nullable: false, optional: false, nullable: false, optional: false,
ref: 'User', ref: 'UserDetailedNotMe',
}, },
assignee: { assignee: {
type: 'object', type: 'object',
nullable: true, optional: true, nullable: true, optional: true,
ref: 'User', ref: 'UserDetailedNotMe',
}, },
}, },
}, },

View File

@ -11,6 +11,7 @@ import { SignupService } from '@/core/SignupService.js';
import { UserEntityService } from '@/core/entities/UserEntityService.js'; import { UserEntityService } from '@/core/entities/UserEntityService.js';
import { localUsernameSchema, passwordSchema } from '@/models/User.js'; import { localUsernameSchema, passwordSchema } from '@/models/User.js';
import { DI } from '@/di-symbols.js'; import { DI } from '@/di-symbols.js';
import { Packed } from '@/misc/json-schema.js';
export const meta = { export const meta = {
tags: ['admin'], tags: ['admin'],
@ -18,7 +19,7 @@ export const meta = {
res: { res: {
type: 'object', type: 'object',
optional: false, nullable: false, optional: false, nullable: false,
ref: 'User', ref: 'MeDetailed',
properties: { properties: {
token: { token: {
type: 'string', type: 'string',
@ -60,11 +61,11 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
}); });
const res = await this.userEntityService.pack(account, account, { const res = await this.userEntityService.pack(account, account, {
detail: true, schema: 'MeDetailed',
includeSecrets: true, includeSecrets: true,
}); }) as Packed<'MeDetailed'> & { token: string };
(res as any).token = secret; res.token = secret;
return res; return res;
}); });

View File

@ -27,7 +27,7 @@ export const meta = {
res: { res: {
type: 'object', type: 'object',
optional: false, nullable: false, optional: false, nullable: false,
ref: 'User', ref: 'UserDetailedNotMe',
}, },
} as const; } as const;
@ -58,7 +58,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
} }
const res = await this.userEntityService.pack(profile.user!, null, { const res = await this.userEntityService.pack(profile.user!, null, {
detail: true, schema: 'UserDetailedNotMe',
}); });
return res; return res;

View File

@ -40,7 +40,7 @@ export const meta = {
}, },
required: ['id', 'createdAt', 'user'], required: ['id', 'createdAt', 'user'],
}, },
} },
} as const; } as const;
export const paramDef = { export const paramDef = {
@ -92,7 +92,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
return await Promise.all(assigns.map(async assign => ({ return await Promise.all(assigns.map(async assign => ({
id: assign.id, id: assign.id,
createdAt: this.idService.parse(assign.id).date.toISOString(), createdAt: this.idService.parse(assign.id).date.toISOString(),
user: await this.userEntityService.pack(assign.user!, me, { detail: true }), user: await this.userEntityService.pack(assign.user!, me, { schema: 'UserDetailed' }),
expiresAt: assign.expiresAt?.toISOString() ?? null, expiresAt: assign.expiresAt?.toISOString() ?? null,
}))); })));
}); });

View File

@ -50,7 +50,7 @@ export const meta = {
user: { user: {
type: 'object', type: 'object',
optional: false, nullable: false, optional: false, nullable: false,
ref: 'UserDetailed', ref: 'UserDetailedNotMe',
}, },
}, },
}, },

View File

@ -114,7 +114,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
const users = await query.getMany(); const users = await query.getMany();
return await this.userEntityService.packMany(users, me, { detail: true }); return await this.userEntityService.packMany(users, me, { schema: 'UserDetailed' });
}); });
} }
} }

View File

@ -148,7 +148,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
if (user != null) { if (user != null) {
return { return {
type: 'User', type: 'User',
object: await this.userEntityService.pack(user, me, { detail: true }), object: await this.userEntityService.pack(user, me, { schema: 'UserDetailedNotMe' }),
}; };
} else if (note != null) { } else if (note != null) {
try { try {

View File

@ -112,7 +112,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
return { return {
accessToken: accessToken.token, accessToken: accessToken.token,
user: await this.userEntityService.pack(session.userId, null, { user: await this.userEntityService.pack(session.userId, null, {
detail: true, schema: 'UserDetailedNotMe',
}), }),
}; };
}); });

View File

@ -102,7 +102,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
await this.userBlockingService.block(blocker, blockee); await this.userBlockingService.block(blocker, blockee);
return await this.userEntityService.pack(blockee.id, blocker, { return await this.userEntityService.pack(blockee.id, blocker, {
detail: true, schema: 'UserDetailedNotMe',
}); });
}); });
} }

View File

@ -103,7 +103,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
await this.userBlockingService.unblock(blocker, blockee); await this.userBlockingService.unblock(blocker, blockee);
return await this.userEntityService.pack(blockee.id, blocker, { return await this.userEntityService.pack(blockee.id, blocker, {
detail: true, schema: 'UserDetailedNotMe',
}); });
}); });
} }

View File

@ -61,7 +61,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
relations: ['user'], relations: ['user'],
}); });
const users = await this.userEntityService.packMany(records.map(r => r.user!), null, { detail: false }); const users = await this.userEntityService.packMany(records.map(r => r.user!), null);
return records.map(r => ({ return records.map(r => ({
id: r.id, id: r.id,

View File

@ -54,7 +54,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
.limit(ps.limit) .limit(ps.limit)
.getMany(); .getMany();
return await this.userEntityService.packMany(users, me, { detail: true }); return await this.userEntityService.packMany(users, me, { schema: 'UserDetailedNotMe' });
}); });
} }
} }

View File

@ -76,7 +76,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
const users = await query.limit(ps.limit).getMany(); const users = await query.limit(ps.limit).getMany();
return await this.userEntityService.packMany(users, me, { detail: true }); return await this.userEntityService.packMany(users, me, { schema: 'UserDetailed' });
}); });
} }
} }

View File

@ -71,8 +71,8 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
userProfile.loggedInDates = [...userProfile.loggedInDates, today]; userProfile.loggedInDates = [...userProfile.loggedInDates, today];
} }
return await this.userEntityService.pack<true, true>(userProfile.user!, userProfile.user!, { return await this.userEntityService.pack(userProfile.user!, userProfile.user!, {
detail: true, schema: 'MeDetailed',
includeSecrets: isSecure, includeSecrets: isSecure,
userProfile, userProfile,
}); });

View File

@ -64,7 +64,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
// Publish meUpdated event // Publish meUpdated event
this.globalEventService.publishMainStream(me.id, 'meUpdated', await this.userEntityService.pack(me.id, me, { this.globalEventService.publishMainStream(me.id, 'meUpdated', await this.userEntityService.pack(me.id, me, {
detail: true, schema: 'MeDetailed',
includeSecrets: true, includeSecrets: true,
})); }));

View File

@ -111,7 +111,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
// Publish meUpdated event // Publish meUpdated event
this.globalEventService.publishMainStream(me.id, 'meUpdated', await this.userEntityService.pack(me.id, me, { this.globalEventService.publishMainStream(me.id, 'meUpdated', await this.userEntityService.pack(me.id, me, {
detail: true, schema: 'MeDetailed',
includeSecrets: true, includeSecrets: true,
})); }));

View File

@ -74,7 +74,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
// Publish meUpdated event // Publish meUpdated event
this.globalEventService.publishMainStream(me.id, 'meUpdated', await this.userEntityService.pack(me.id, me, { this.globalEventService.publishMainStream(me.id, 'meUpdated', await this.userEntityService.pack(me.id, me, {
detail: true, schema: 'MeDetailed',
includeSecrets: true, includeSecrets: true,
})); }));
}); });

View File

@ -97,7 +97,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
// Publish meUpdated event // Publish meUpdated event
this.globalEventService.publishMainStream(me.id, 'meUpdated', await this.userEntityService.pack(me.id, me, { this.globalEventService.publishMainStream(me.id, 'meUpdated', await this.userEntityService.pack(me.id, me, {
detail: true, schema: 'MeDetailed',
includeSecrets: true, includeSecrets: true,
})); }));

View File

@ -76,7 +76,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
// Publish meUpdated event // Publish meUpdated event
this.globalEventService.publishMainStream(me.id, 'meUpdated', await this.userEntityService.pack(me.id, me, { this.globalEventService.publishMainStream(me.id, 'meUpdated', await this.userEntityService.pack(me.id, me, {
detail: true, schema: 'MeDetailed',
includeSecrets: true, includeSecrets: true,
})); }));
}); });

View File

@ -69,7 +69,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
// Publish meUpdated event // Publish meUpdated event
this.globalEventService.publishMainStream(me.id, 'meUpdated', await this.userEntityService.pack(me.id, me, { this.globalEventService.publishMainStream(me.id, 'meUpdated', await this.userEntityService.pack(me.id, me, {
detail: true, schema: 'MeDetailed',
includeSecrets: true, includeSecrets: true,
})); }));

View File

@ -66,8 +66,8 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
throw err; throw err;
}); });
return await this.userEntityService.pack<true, true>(me.id, me, { return await this.userEntityService.pack(me.id, me, {
detail: true, schema: 'MeDetailed',
}); });
}); });
} }

View File

@ -51,8 +51,8 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
throw err; throw err;
}); });
return await this.userEntityService.pack<true, true>(me.id, me, { return await this.userEntityService.pack(me.id, me, {
detail: true, schema: 'MeDetailed',
}); });
}); });
} }

View File

@ -43,7 +43,7 @@ export const meta = {
res: { res: {
type: 'object', type: 'object',
ref: 'UserDetailed', ref: 'MeDetailed',
}, },
} as const; } as const;
@ -106,7 +106,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
}); });
const iObj = await this.userEntityService.pack(me.id, me, { const iObj = await this.userEntityService.pack(me.id, me, {
detail: true, schema: 'MeDetailed',
includeSecrets: true, includeSecrets: true,
}); });

View File

@ -451,8 +451,8 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
verifiedLinks: [], verifiedLinks: [],
}); });
const iObj = await this.userEntityService.pack<true, true>(user.id, user, { const iObj = await this.userEntityService.pack(user.id, user, {
detail: true, schema: 'MeDetailed',
includeSecrets: isSecure, includeSecrets: isSecure,
}); });

View File

@ -303,6 +303,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;

View File

@ -55,7 +55,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
var: ps.var, var: ps.var,
userId: me.id, userId: me.id,
user: await this.userEntityService.pack(me.id, { id: page.userId }, { user: await this.userEntityService.pack(me.id, { id: page.userId }, {
detail: true, schema: 'UserDetailed',
}), }),
}); });
}); });

View File

@ -52,7 +52,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
host: acct.host ?? IsNull(), host: acct.host ?? IsNull(),
}))); })));
return await this.userEntityService.packMany(users.filter(x => x !== null) as MiUser[], me, { detail: true }); return await this.userEntityService.packMany(users.filter(x => x !== null) as MiUser[], me, { schema: 'UserDetailed' });
}); });
} }
} }

View File

@ -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,

View File

@ -33,11 +33,11 @@ export const meta = {
properties: { properties: {
id: { id: {
type: 'string', type: 'string',
format: 'misskey:id' format: 'misskey:id',
}, },
user: { user: {
type: 'object', type: 'object',
ref: 'User' ref: 'UserDetailed',
}, },
}, },
required: ['id', 'user'], required: ['id', 'user'],
@ -94,7 +94,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
return await Promise.all(assigns.map(async assign => ({ return await Promise.all(assigns.map(async assign => ({
id: assign.id, id: assign.id,
user: await this.userEntityService.pack(assign.user!, me, { detail: true }), user: await this.userEntityService.pack(assign.user!, me, { schema: 'UserDetailed' }),
}))); })));
}); });
} }

View File

@ -89,7 +89,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
const users = await query.getMany(); const users = await query.getMany();
return await this.userEntityService.packMany(users, me, { detail: true }); return await this.userEntityService.packMany(users, me, { schema: 'UserDetailed' });
}); });
} }
} }

View File

@ -122,7 +122,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
// Make replies object (includes weights) // Make replies object (includes weights)
const repliesObj = await Promise.all(topRepliedUsers.map(async (user) => ({ const repliesObj = await Promise.all(topRepliedUsers.map(async (user) => ({
user: await this.userEntityService.pack(user, me, { detail: true }), user: await this.userEntityService.pack(user, me, { schema: 'UserDetailed' }),
weight: repliedUsers[user] / peak, weight: repliedUsers[user] / peak,
}))); })));

View File

@ -46,7 +46,7 @@ export const meta = {
}, },
user: { user: {
type: 'object', type: 'object',
ref: 'User', ref: 'UserLite',
}, },
withReplies: { withReplies: {
type: 'boolean', type: 'boolean',

View File

@ -76,7 +76,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
const users = await query.limit(ps.limit).offset(ps.offset).getMany(); const users = await query.limit(ps.limit).offset(ps.offset).getMany();
return await this.userEntityService.packMany(users, me, { detail: true }); return await this.userEntityService.packMany(users, me, { schema: 'UserDetailed' });
}); });
} }
} }

View File

@ -131,7 +131,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
.getMany(); .getMany();
} }
return await this.userEntityService.packMany(users, me, { detail: !!ps.detail }); return await this.userEntityService.packMany(users, me, { schema: ps.detail ? 'UserDetailed' : 'UserLite' });
}); });
} }
} }

View File

@ -141,7 +141,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
} }
} }
return await this.userEntityService.packMany(users, me, { detail: ps.detail }); return await this.userEntityService.packMany(users, me, { schema: ps.detail ? 'UserDetailed' : 'UserLite' });
}); });
} }
} }

View File

@ -116,7 +116,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
} }
return await Promise.all(_users.map(u => this.userEntityService.pack(u, me, { return await Promise.all(_users.map(u => this.userEntityService.pack(u, me, {
detail: true, schema: 'UserDetailed',
}))); })));
} else { } else {
// Lookup user // Lookup user
@ -146,7 +146,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
} }
return await this.userEntityService.pack(user, me, { return await this.userEntityService.pack(user, me, {
detail: true, schema: 'UserDetailed',
}); });
} }
}); });

View File

@ -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;
}

View File

@ -20,15 +20,15 @@
"@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/pluginutils": "5.1.0", "@rollup/pluginutils": "5.1.0",
"@syuilo/aiscript": "0.17.0", "@syuilo/aiscript": "0.17.0",
"@tabler/icons-webfont": "2.44.0", "@tabler/icons-webfont": "2.44.0",
"@twemoji/parser": "15.0.0", "@twemoji/parser": "15.0.0",
"@vitejs/plugin-vue": "5.0.2", "@vitejs/plugin-vue": "5.0.3",
"@vue/compiler-sfc": "3.4.3", "@vue/compiler-sfc": "3.4.15",
"aiscript-vscode": "github:aiscript-dev/aiscript-vscode#v0.0.6", "aiscript-vscode": "github:aiscript-dev/aiscript-vscode#v0.0.6",
"astring": "1.8.6", "astring": "1.8.6",
"broadcast-channel": "7.0.0", "broadcast-channel": "7.0.0",
@ -39,7 +39,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.3.1", "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",
@ -63,7 +63,7 @@
"shiki": "0.14.7", "shiki": "0.14.7",
"strict-event-emitter-types": "2.0.0", "strict-event-emitter-types": "2.0.0",
"textarea-caret": "3.1.0", "textarea-caret": "3.1.0",
"three": "0.160.0", "three": "0.160.1",
"throttle-debounce": "5.0.0", "throttle-debounce": "5.0.0",
"tinycolor2": "1.6.0", "tinycolor2": "1.6.0",
"tsc-alias": "1.8.8", "tsc-alias": "1.8.8",
@ -76,8 +76,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",
@ -101,12 +101,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.5", "@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",
@ -134,7 +134,7 @@
"vite-plugin-turbosnap": "1.0.3", "vite-plugin-turbosnap": "1.0.3",
"vitest": "0.34.6", "vitest": "0.34.6",
"vitest-fetch-mock": "0.2.2", "vitest-fetch-mock": "0.2.2",
"vue-eslint-parser": "9.4.0", "vue-eslint-parser": "9.4.2",
"vue-tsc": "1.8.27" "vue-tsc": "1.8.27"
} }
} }

View File

@ -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}`);

View File

@ -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(

View File

@ -68,7 +68,7 @@ import MkNoteSimple from '@/components/MkNoteSimple.vue';
import MkSwitch from '@/components/MkSwitch.vue'; import MkSwitch from '@/components/MkSwitch.vue';
const props = defineProps<{ const props = defineProps<{
user: Misskey.entities.User; user: Misskey.entities.UserDetailed;
initialComment?: string; initialComment?: string;
initialNoteId?: Misskey.entities.Note['id']; initialNoteId?: Misskey.entities.Note['id'];
}>(); }>();

View File

@ -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, {

View File

@ -49,7 +49,7 @@ async function ok() {
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),
@ -57,7 +57,7 @@ async function ok() {
} }
function onBgClick() { function onBgClick() {
rootEl.value.animate([{ rootEl.value?.animate([{
offset: 0, offset: 0,
transform: 'scale(1)', transform: 'scale(1)',
}, { }, {

View File

@ -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,
}); });
} }

View File

@ -54,7 +54,7 @@ SPDX-License-Identifier: AGPL-3.0-only
[$style.gamingLight]: gaming === 'light', [$style.gamingLight]: gaming === 'light',
} }
]" ]"
: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>

View File

@ -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 = {

View File

@ -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;

View File

@ -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();
} }

View File

@ -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>

View File

@ -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);
}); });

View File

@ -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(' / ');
}); });

View File

@ -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>

View File

@ -31,8 +31,8 @@ SPDX-License-Identifier: AGPL-3.0-only
<MkInput v-if="input" v-model="inputValue" autofocus :type="input.type || 'text'" :placeholder="input.placeholder || undefined" :autocomplete="input.autocomplete" @keydown="onInputKeydown"> <MkInput v-if="input" 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>
<MkSelect v-if="select" v-model="selectedValue" autofocus> <MkSelect v-if="select" v-model="selectedValue" autofocus>
@ -128,7 +128,7 @@ const mkresult= ref(false)
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';
} }
} }

View File

@ -48,7 +48,7 @@ import { $i } from '@/account.js';
import { getDriveFileMenu, getDriveMultiFileMenu } from '@/scripts/get-drive-file-menu.js'; import { getDriveFileMenu, getDriveMultiFileMenu } from '@/scripts/get-drive-file-menu.js';
import { isTouchUsing } from '@/scripts/touch.js'; import { isTouchUsing } from '@/scripts/touch.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();

View File

@ -215,7 +215,7 @@ function onDragend() {
} }
function go() { function go() {
emit('move', props.folder.id); emit('move', props.folder);
} }
function rename() { function rename() {

View File

@ -123,6 +123,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';
@ -455,7 +456,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;
@ -642,7 +643,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,
@ -668,7 +669,7 @@ function getMenu() {
text: i18n.ts.renameFolder, text: i18n.ts.renameFolder,
icon: 'ti ti-forms', icon: 'ti ti-forms',
action: () => { action: () => {
renameFolder(folder.value); if (folder.value)renameFolder(folder.value);
}, },
} : undefined, folder.value ? { } : undefined, folder.value ? {
text: i18n.ts.deleteFolder, text: i18n.ts.deleteFolder,
@ -683,6 +684,8 @@ function getMenu() {
createFolder(); createFolder();
}, },
}]; }];
return menu;
} }
async function isSensitive(Files, isSensitive: boolean) { async function isSensitive(Files, isSensitive: boolean) {

View File

@ -4,64 +4,64 @@ SPDX-License-Identifier: AGPL-3.0-only
--> -->
<template> <template>
<!-- このコンポーネントの要素のclassは親から利用されるのでむやみに弄らないこと --> <!-- このコンポーネントの要素のclassは親から利用されるのでむやみに弄らないこと -->
<!-- フォルダの中にはカスタム絵文字だけUnicode絵文字もこっち --> <!-- フォルダの中にはカスタム絵文字だけUnicode絵文字もこっち -->
<section v-if="!hasChildSection" style="border-radius: 6px;"> <section v-if="!hasChildSection" 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> ({{ emojis.length }}) <i class="toggle ti-fw" :class="shown ? 'ti ti-chevron-down' : 'ti ti-chevron-up'"></i> <slot></slot> (<i class="ti ti-icons"></i>:{{ emojis.length }})
</header> </header>
<div v-if="shown" class="body"> <div v-if="shown" class="body">
<button <button
v-for="emoji in emojis" v-for="emoji in emojis"
:key="emoji" :key="emoji"
:data-emoji="emoji" :data-emoji="emoji"
class="_button item" class="_button item"
@pointerenter="computeButtonTitle" @pointerenter="computeButtonTitle"
@click="emit('chosen', emoji, $event)" @click="emit('chosen', emoji, $event)"
> >
<MkCustomEmoji v-if="emoji[0] === ':'" class="emoji" :name="emoji" :normal="true"/> <MkCustomEmoji v-if="emoji[0] === ':'" class="emoji" :name="emoji" :normal="true"/>
<MkEmoji v-else class="emoji" :emoji="emoji" :normal="true"/> <MkEmoji v-else class="emoji" :emoji="emoji" :normal="true"/>
</button> </button>
</div> </div>
</section> </section>
<!-- フォルダの中にはカスタム絵文字やフォルダがある --> <!-- フォルダの中にはカスタム絵文字やフォルダがある -->
<section v-else style="border-radius: 6px; padding-top: 9px;"> <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="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
v-for="child in customEmojiTree" v-for="child in customEmojiTree"
:key="`custom:${child.value}`" :key="`custom:${child.value}`"
:initialShown="initialShown" :initialShown="initialShown"
:emojis="computed(() => customEmojis.filter(e => e.category === child.category || e.category === child.category+'/'+child.category).map(e => `:${e.name}:`))" :emojis="computed(() => customEmojis.filter(e => e.category === child.category).map(e => `:${e.name}:`))"
:hasChildSection="child.children.length !== 0" :hasChildSection="child.children.length !== 0"
:customEmojiTree="child.children" :customEmojiTree="child.children"
@chosen="nestedChosen" @chosen="nestedChosen"
> >
{{ child.value || i18n.ts.other }} {{ child.value || i18n.ts.other }}
</MkEmojiPickerSection> </MkEmojiPickerSection>
</div> </div>
<div v-if="shown" class="body"> <div v-if="shown" class="body">
<button <button
v-for="emoji in emojis" v-for="emoji in emojis"
:key="emoji" :key="emoji"
:data-emoji="emoji" :data-emoji="emoji"
class="_button item" class="_button item"
@pointerenter="computeButtonTitle" @pointerenter="computeButtonTitle"
@click="emit('chosen', emoji, $event)" @click="emit('chosen', emoji, $event)"
> >
<MkCustomEmoji v-if="emoji[0] === ':'" class="emoji" :name="emoji" :normal="true"/> <MkCustomEmoji v-if="emoji[0] === ':'" class="emoji" :name="emoji" :normal="true"/>
<MkEmoji v-else class="emoji" :emoji="emoji" :normal="true"/> <MkEmoji v-else class="emoji" :emoji="emoji" :normal="true"/>
</button> </button>
</div> </div>
</section> </section>
</template> </template>
<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>

View File

@ -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"> <section v-if="showPinned && (pinned && pinned.length > 0)">
<div style="display: flex; "> <div style="display: flex; ">
<div v-for="a in profileMax" :key="a" :title="defaultStore.state[`pickerProfileName${a > 1 ? a - 1 : ''}`]" class="sllfktkhgl" :class="{ active: activeIndex === a || isDefaultProfile === a }" @click="pinnedProfileSelect(a)"> <div v-for="a in profileMax" :key="a" :title="defaultStore.state[`pickerProfileName${a > 1 ? a - 1 : ''}`]" class="sllfktkhgl" :class="{ active: activeIndex === a || isDefaultProfile === a }" @click="pinnedProfileSelect(a)">
{{ defaultStore.state[`pickerProfileName${a > 1 ? a - 1 : ''}`] }} {{ defaultStore.state[`pickerProfileName${a > 1 ? a - 1 : ''}`] }}
@ -349,7 +349,7 @@ watch(q, () => {
}); });
function filterAvailable(emoji: Misskey.entities.EmojiSimple): boolean { function filterAvailable(emoji: Misskey.entities.EmojiSimple): boolean {
return (emoji.roleIdsThatCanBeUsedThisEmojiAsReaction == null || emoji.roleIdsThatCanBeUsedThisEmojiAsReaction.length === 0) || ($i && $i.roles.some(r => emoji.roleIdsThatCanBeUsedThisEmojiAsReaction.includes(r.id))); return ((emoji.roleIdsThatCanBeUsedThisEmojiAsReaction == null || emoji.roleIdsThatCanBeUsedThisEmojiAsReaction.length === 0) || ($i && $i.roles.some(r => emoji.roleIdsThatCanBeUsedThisEmojiAsReaction?.includes(r.id)))) ?? false;
} }
function focus() { function focus() {

View File

@ -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>

View File

@ -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}`"

View File

@ -33,8 +33,9 @@ SPDX-License-Identifier: AGPL-3.0-only
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import { nextTick, onMounted, shallowRef, ref } from 'vue'; import { nextTick, onMounted, shallowRef, ref, watch } from 'vue';
import { defaultStore } from '@/store.js'; import { defaultStore } from '@/store.js';
const miLocalStoragePrefix = 'ui:folder:' as const;
const props = withDefaults(defineProps<{ const props = withDefaults(defineProps<{
defaultOpen?: boolean; defaultOpen?: boolean;
@ -43,41 +44,53 @@ const props = withDefaults(defineProps<{
defaultOpen: true, defaultOpen: true,
maxHeight: null, maxHeight: null,
}); });
const getBgColor = (el: HTMLElement) => { const getBgColor = (el: HTMLElement) => {
const style = window.getComputedStyle(el); const style = window.getComputedStyle(el);
if (style.backgroundColor && !['rgba(0, 0, 0, 0)', 'rgba(0,0,0,0)', 'transparent'].includes(style.backgroundColor)) { if (style.backgroundColor && !['rgba(0, 0, 0, 0)', 'rgba(0,0,0,0)', 'transparent'].includes(style.backgroundColor)) {
return style.backgroundColor; return style.backgroundColor;
} else { } else {
return el.parentElement ? getBgColor(el.parentElement) : 'transparent'; return el.parentElement ? getBgColor(el.parentElement) : 'transparent';
} }
}; };
const rootEl = shallowRef<HTMLDivElement>();
const bg = ref<string>();
const showBody = ref((props.persistKey && miLocalStorage.getItem(`${miLocalStoragePrefix}${props.persistKey}`)) ? (miLocalStorage.getItem(`${miLocalStoragePrefix}${props.persistKey}`) === 't') : props.expanded);
watch(showBody, () => {
if (props.persistKey) {
miLocalStorage.setItem(`${miLocalStoragePrefix}${props.persistKey}`, showBody.value ? 't' : 'f');
}
});
const rootEl = shallowRef<HTMLElement>(); const rootEl = shallowRef<HTMLElement>();
const bgSame = ref(false); const bgSame = ref(false);
const opened = ref(props.defaultOpen); const opened = ref(props.defaultOpen);
const openedAtLeastOnce = ref(props.defaultOpen); const openedAtLeastOnce = ref(props.defaultOpen);
function enter(el) { 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 = Math.min(elementHeight, props.maxHeight ?? Infinity) + 'px'; el.style.height = Math.min(elementHeight, props.maxHeight ?? Infinity) + 'px';
} }
function afterEnter(el) { function afterEnter(element: Element) {
el.style.height = null; const el = element as HTMLElement;
el.style.height = 'unset';
} }
function leave(el) { 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) { function afterLeave(element: Element) {
el.style.height = null; const el = element as HTMLElement;
el.style.height = 'unset';
} }
function toggle() { function toggle() {
@ -91,10 +104,23 @@ function toggle() {
} }
onMounted(() => { onMounted(() => {
const computedStyle = getComputedStyle(document.documentElement); function getParentBg(el?: HTMLElement | null): string {
const parentBg = getBgColor(rootEl.value.parentElement); if (el == null || el.tagName === 'BODY') return 'var(--bg)';
const myBg = computedStyle.getPropertyValue('--panel'); const background = el.style.background || el.style.backgroundColor;
bgSame.value = parentBg === myBg; if (background) {
return background;
} else {
return getParentBg(el.parentElement);
}
}
const computedStyle = getComputedStyle(document.documentElement);
const parentBg = getBgColor(rootEl.value.parentElement);
const myBg = computedStyle.getPropertyValue('--panel');
bgSame.value = parentBg === myBg;
const rawBg = getParentBg(rootEl.value);
const _bg = tinycolor(rawBg.startsWith('var(') ? getComputedStyle(document.documentElement).getPropertyValue(rawBg.slice(4, -1)) : rawBg);
_bg.setAlpha(0.85);
bg.value = _bg.toRgbString();
}); });
</script> </script>

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