refactor: frontendのcomponentsの型エラーを改善 (#12926)

* add: safeFloatParserを追加

* fix: 欠けていた型を追加

* refactor: pageBlockTypesをjson-schemaに移植

* refactor: components/global内の型エラーが出ている箇所を修正

* lint: fix null check style

* refactor: fix type error

* refactor: fix some type errors

* fix: 翻訳が抜けていた箇所を修正

* refactor: getJsonSchemaで正しいスキーマが返されるように修正

* fix: MkChartの型エラーとbytesオプションが機能していない問題を修正

* fix(misskey-js): `drive`->`folderUpdated`のpayloadの型が間違っていたのを修正

* refactor: fix some type errors

* change: Captcha読み込み中の文言をLoadingに変更

* refactor(backend/misskey-js): MainEventの型を改善

* refactor: chartjs-plugin-gradientが二重でpluginに登録されていたのを修正

* update: misskey-js.api.md

* refactor: fix some type errors

* fix: backendのtypecheckが落ちていたのを修正

* update: misskey-js.api.md

* add: json-schemaのnoteにpollの型定義を追加

* refactor: noteのjson-schemaの型を改善

* refactor: MkPoll

* refactor: fix some type errors

* change: UserLiteにisLockedを持たせるように

* fix: notificationスキーマにroleが含まれていないのを修正

* Revert "change: UserLiteにisLockedを持たせるように"

This reverts commit 1bb0c8e7a9b19a4e9f21bf7381712b98f27672a5.

* fix: フォロー通知から鍵垢へのフォローを行うと処理中のまま止まってしまう問題を修正

* refactor: noteスキーマのvisibilityにenumを追加

* change: deepCloneのCloneableTypeにundefinedを追加

* refactor: fix some type errors

* refactor: `allowEmpty: false`を使用していた箇所を`minLength: 1`に置き換え

* enhance: API 'retension' のresponseの型を追加

* fix: Chart関連のtooltipが正しい位置に表示されない問題を修正

* refactor: fix some type errors

* fix: 型情報が不足していたのを修正

* enhance: announcementスキーマにenumを追加

* enhance: ロールポリシーの型定義をRoleServiceからjson-schemaに移植

* refactor: policiesを`ref: RolePolicies`に統一

* fix: API `meta` のレスポンスの型にpoliciesが含まれていないのを修正

* refactor: fix some type errors

* fix: backendのlintが落ちているのを修正

* fix: MkFoldableSectionの開閉時のanimationが適用されていない問題を修正

* fix: backendのtypecheckが落ちているのを修正

* update: run build-misskey-js-with-types

* fix: MkDialogのmount時に文字数制限の判定が行われない問題を修正

* update: CHANGELOG.md

* refactor: MkUserSelectDialogの型を改善

* fix: deepCloneでundefinedはcloneしないように (#9207)

* change: frontendのcloneをbackend側にも反映

* update: CHANGELOG.md

* fix: RoleServiceからPackを通して型RolePoliciesに依存させないように

* Update packages/frontend/src/scripts/get-note-summary.ts

* revert RoleService.ts changes

* change:  optional chaining -> non-null assertion

* remove: unused import

* fix: propsで渡されたuserがUserLiteの場合に意図しない動作になってしまうのを修正

* change: fix null check style

* refactor: fix type error

* change: fix null check style

* Update packages/frontend/src/components/MkDrive.vue

Co-authored-by: syuilo <Syuilotan@yahoo.co.jp>

* refactor: css moduleでglobalを使わないように

* refactor: roleのiconUrlは必ず存在するものとして扱うように

* enhance: MenuButtonのactiveにcomputedを受け付けられるように

* Update packages/frontend/src/components/MkNotePreview.vue

* Update MkWindow.vue

* refactor: notification.noteは必ず存在するものとして扱うように

* Update packages/frontend/src/components/MkNotification.vue

Co-authored-by: syuilo <Syuilotan@yahoo.co.jp>

* fix: MkSignupDialogでdoneのemit時にresを含んでいなかったのを修正

* Update packages/frontend/src/scripts/clone.ts

Co-authored-by: syuilo <Syuilotan@yahoo.co.jp>

* refactor: 不要な返り値の型を削除

* refactor: 不要なnullチェックを削除

* update: misskey-js-autogen

* update: clone.ts

* refactor

* Update MkNotification.vue

* Update MkNotification.vue

* ✌️

* Update MkNotification.vue

* Update MkNotification.vue

* Update MkNotification.vue

* Update MkNotifications.vue

* Update MkUserSetupDialog.Profile.vue

* Update MkUserCardMini.vue

* ✌️

* Update MkMenu.vue

---------

Co-authored-by: syuilo <Syuilotan@yahoo.co.jp>
This commit is contained in:
yukineko 2024-01-30 19:53:53 +09:00 committed by GitHub
parent 9ac2c36d76
commit a6a91fec3a
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
139 changed files with 1944 additions and 1193 deletions

View file

@ -50,6 +50,12 @@
- 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で大きな画像を変換してアップロードできない問題を修正 - Fix: iOSで大きな画像を変換してアップロードできない問題を修正

View file

@ -54,9 +54,9 @@ export interface MainEventTypes {
reply: Packed<'Note'>; reply: Packed<'Note'>;
renote: Packed<'Note'>; renote: Packed<'Note'>;
follow: Packed<'UserDetailedNotMe'>; follow: Packed<'UserDetailedNotMe'>;
followed: Packed<'User'>; followed: Packed<'UserDetailed' | 'UserLite'>;
unfollow: Packed<'User'>; unfollow: Packed<'UserDetailed'>;
meUpdated: Packed<'User'>; meUpdated: Packed<'UserDetailed'>;
pageEvent: { pageEvent: {
pageId: MiPage['id']; pageId: MiPage['id'];
event: string; event: string;

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

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

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, packedEmojiSimpleSchema } from '@/models/jso
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,
@ -79,12 +80,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

@ -69,6 +69,7 @@ export const packedNoteSchema = {
visibility: { visibility: {
type: 'string', type: 'string',
optional: false, nullable: false, optional: false, nullable: false,
enum: ['public', 'home', 'followers', 'specified'],
}, },
mentions: { mentions: {
type: 'array', type: 'array',
@ -117,6 +118,48 @@ export const packedNoteSchema = {
poll: { poll: {
type: 'object', type: 'object',
optional: true, nullable: true, optional: true, nullable: true,
properties: {
expiresAt: {
type: 'string',
optional: true, nullable: true,
format: 'date-time',
},
multiple: {
type: 'boolean',
optional: false, nullable: false,
},
choices: {
type: 'array',
optional: false, nullable: false,
items: {
type: 'object',
optional: false, nullable: false,
properties: {
isVoted: {
type: 'boolean',
optional: false, nullable: false,
},
text: {
type: 'string',
optional: false, nullable: false,
},
votes: {
type: 'number',
optional: false, nullable: false,
},
},
},
},
},
},
emojis: {
type: 'object',
optional: true, nullable: false,
additionalProperties: {
anyOf: [{
type: 'string',
}],
},
}, },
channelId: { channelId: {
type: 'string', type: 'string',
@ -162,9 +205,23 @@ export const packedNoteSchema = {
type: 'string', type: 'string',
optional: false, nullable: true, optional: false, nullable: true,
}, },
reactionEmojis: {
type: 'object',
optional: false, nullable: false,
additionalProperties: {
anyOf: [{
type: 'string',
}],
},
},
reactions: { reactions: {
type: 'object', type: 'object',
optional: false, nullable: false, optional: false, nullable: false,
additionalProperties: {
anyOf: [{
type: 'number',
}],
},
}, },
renoteCount: { renoteCount: {
type: 'number', type: 'number',
@ -196,7 +253,7 @@ export const packedNoteSchema = {
}, },
myReaction: { myReaction: {
type: 'object', type: 'string',
optional: true, nullable: true, optional: true, nullable: true,
}, },
}, },

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

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

@ -590,104 +590,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: {

View file

@ -4,8 +4,7 @@
*/ */
import { permissions } from 'misskey-js'; import { permissions } from 'misskey-js';
import type { Schema } from '@/misc/json-schema.js'; import type { KeyOf, Schema } from '@/misc/json-schema.js';
import { RolePolicies } from '@/core/RoleService.js';
import * as ep___admin_meta from './endpoints/admin/meta.js'; import * as ep___admin_meta from './endpoints/admin/meta.js';
import * as ep___admin_abuseUserReports from './endpoints/admin/abuse-user-reports.js'; import * as ep___admin_abuseUserReports from './endpoints/admin/abuse-user-reports.js';
@ -776,7 +775,7 @@ interface IEndpointMetaBase {
*/ */
readonly requireAdmin?: boolean; readonly requireAdmin?: boolean;
readonly requireRolePolicy?: keyof RolePolicies; readonly requireRolePolicy?: KeyOf<'RolePolicies'>;
/** /**
* *

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

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

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

@ -39,7 +39,7 @@ import * as os from '@/os.js';
import { i18n } from '@/i18n.js'; import { i18n } from '@/i18n.js';
const props = defineProps<{ const props = defineProps<{
user: Misskey.entities.User; user: Misskey.entities.UserDetailed;
initialComment?: string; initialComment?: string;
}>(); }>();

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

@ -22,7 +22,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<MkA <MkA
v-else class="_button" v-else class="_button"
:class="[$style.root, { [$style.inline]: inline, [$style.primary]: primary, [$style.gradate]: gradate, [$style.danger]: danger, [$style.rounded]: rounded, [$style.full]: full, [$style.small]: small, [$style.large]: large, [$style.transparent]: transparent, [$style.asLike]: asLike }]" :class="[$style.root, { [$style.inline]: inline, [$style.primary]: primary, [$style.gradate]: gradate, [$style.danger]: danger, [$style.rounded]: rounded, [$style.full]: full, [$style.small]: small, [$style.large]: large, [$style.transparent]: transparent, [$style.asLike]: asLike }]"
:to="to" :to="to ?? '#'"
@mousedown="onMousedown" @mousedown="onMousedown"
> >
<div ref="ripples" :class="$style.ripples" :data-children-class="$style.ripple"></div> <div ref="ripples" :class="$style.ripples" :data-children-class="$style.ripple"></div>

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

@ -30,8 +30,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>
@ -125,7 +125,7 @@ const selectedValue = ref(props.select?.default ?? null);
const okButtonDisabledReason = computed<null | 'charactersExceeded' | 'charactersBelow'>(() => { const okButtonDisabledReason = computed<null | 'charactersExceeded' | 'charactersBelow'>(() => {
if (props.input) { if (props.input) {
if (props.input.minLength) { if (props.input.minLength) {
if ((inputValue.value || inputValue.value === '') && (inputValue.value as string).length < props.input.minLength) { if (inputValue.value == null || (inputValue.value as string).length < props.input.minLength) {
return 'charactersBelow'; return 'charactersBelow';
} }
} }

View file

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

View file

@ -98,6 +98,7 @@ SPDX-License-Identifier: AGPL-3.0-only
import { nextTick, onActivated, onBeforeUnmount, onMounted, ref, shallowRef, watch } from 'vue'; import { nextTick, onActivated, onBeforeUnmount, onMounted, ref, shallowRef, watch } from 'vue';
import * as Misskey from 'misskey-js'; import * as Misskey from 'misskey-js';
import MkButton from './MkButton.vue'; import MkButton from './MkButton.vue';
import type { MenuItem } from '@/types/menu.js';
import XNavFolder from '@/components/MkDrive.navFolder.vue'; import XNavFolder from '@/components/MkDrive.navFolder.vue';
import XFolder from '@/components/MkDrive.folder.vue'; import XFolder from '@/components/MkDrive.folder.vue';
import XFile from '@/components/MkDrive.file.vue'; import XFile from '@/components/MkDrive.file.vue';
@ -427,7 +428,7 @@ function chooseFolder(folderToChoose: Misskey.entities.DriveFolder) {
} }
} }
function move(target?: Misskey.entities.DriveFolder) { function move(target?: Misskey.entities.DriveFolder | Misskey.entities.DriveFolder['id' | 'parentId']) {
if (!target) { if (!target) {
goRoot(); goRoot();
return; return;
@ -613,7 +614,7 @@ function fetchMoreFiles() {
} }
function getMenu() { function getMenu() {
return [{ const menu: MenuItem[] = [{
type: 'switch', type: 'switch',
text: i18n.ts.keepOriginalUploading, text: i18n.ts.keepOriginalUploading,
ref: keepOriginal, ref: keepOriginal,
@ -634,7 +635,7 @@ function getMenu() {
}, folder.value ? { }, folder.value ? {
text: i18n.ts.renameFolder, text: i18n.ts.renameFolder,
icon: 'ti ti-forms', icon: 'ti ti-forms',
action: () => { renameFolder(folder.value); }, action: () => { if (folder.value) renameFolder(folder.value); },
} : undefined, folder.value ? { } : undefined, folder.value ? {
text: i18n.ts.deleteFolder, text: i18n.ts.deleteFolder,
icon: 'ti ti-trash', icon: 'ti ti-trash',
@ -644,6 +645,8 @@ function getMenu() {
icon: 'ti ti-folder-plus', icon: 'ti ti-folder-plus',
action: () => { createFolder(); }, action: () => { createFolder(); },
}]; }];
return menu;
} }
function showMenu(ev: MouseEvent) { function showMenu(ev: MouseEvent) {

View file

@ -27,7 +27,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<!-- フォルダの中にはカスタム絵文字やフォルダがある --> <!-- フォルダの中にはカスタム絵文字やフォルダがある -->
<section v-else v-panel style="border-radius: 6px; border-bottom: 0.5px solid var(--divider);"> <section v-else v-panel style="border-radius: 6px; border-bottom: 0.5px solid var(--divider);">
<header class="_acrylic" @click="shown = !shown"> <header class="_acrylic" @click="shown = !shown">
<i class="toggle ti-fw" :class="shown ? 'ti ti-chevron-down' : 'ti ti-chevron-up'"></i> <slot></slot> (<i class="ti ti-folder ti-fw"></i>:{{ customEmojiTree.length }} <i class="ti ti-icons ti-fw"></i>:{{ emojis.length }}) <i class="toggle ti-fw" :class="shown ? 'ti ti-chevron-down' : 'ti ti-chevron-up'"></i> <slot></slot> (<i class="ti ti-folder ti-fw"></i>:{{ customEmojiTree?.length }} <i class="ti ti-icons ti-fw"></i>:{{ emojis.length }})
</header> </header>
<div v-if="shown" style="padding-left: 9px;"> <div v-if="shown" style="padding-left: 9px;">
<MkEmojiPickerSection <MkEmojiPickerSection
@ -61,7 +61,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<script lang="ts" setup> <script lang="ts" setup>
import { ref, computed, Ref } from 'vue'; import { ref, computed, Ref } from 'vue';
import { CustomEmojiFolderTree, getEmojiName } from '@/scripts/emojilist.js'; import { CustomEmojiFolderTree, getEmojiName } from '@/scripts/emojilist.js';
import { i18n } from '../i18n.js'; import { i18n } from '@/i18n.js';
import { customEmojis } from '@/custom-emojis.js'; import { customEmojis } from '@/custom-emojis.js';
import MkEmojiPickerSection from '@/components/MkEmojiPicker.section.vue'; import MkEmojiPickerSection from '@/components/MkEmojiPicker.section.vue';
@ -87,7 +87,7 @@ function computeButtonTitle(ev: MouseEvent): void {
elm.title = getEmojiName(emoji) ?? emoji; elm.title = getEmojiName(emoji) ?? emoji;
} }
function nestedChosen(emoji: any, ev?: MouseEvent) { function nestedChosen(emoji: any, ev: MouseEvent) {
emit('chosen', emoji, ev); emit('chosen', emoji, ev);
} }
</script> </script>

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 && pinned.length > 0"> <section v-if="showPinned && (pinned && pinned.length > 0)">
<div class="body"> <div class="body">
<button <button
v-for="emoji in pinned" v-for="emoji in pinned"
@ -340,7 +340,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

@ -4,7 +4,7 @@ SPDX-License-Identifier: AGPL-3.0-only
--> -->
<template> <template>
<div ref="el" :class="$style.root"> <div ref="rootEl" :class="$style.root">
<header :class="$style.header" class="_button" :style="{ background: bg }" @click="showBody = !showBody"> <header :class="$style.header" class="_button" :style="{ background: bg }" @click="showBody = !showBody">
<div :class="$style.title"><div><slot name="header"></slot></div></div> <div :class="$style.title"><div><slot name="header"></slot></div></div>
<div :class="$style.divider"></div> <div :class="$style.divider"></div>
@ -14,7 +14,10 @@ SPDX-License-Identifier: AGPL-3.0-only
</button> </button>
</header> </header>
<Transition <Transition
:name="defaultStore.state.animation ? 'folder-toggle' : ''" :enterActiveClass="defaultStore.state.animation ? $style['folder-toggle-enter-active'] : ''"
:leaveActiveClass="defaultStore.state.animation ? $style['folder-toggle-leave-active'] : ''"
:enterFromClass="defaultStore.state.animation ? $style['folder-toggle-enter-from'] : ''"
:leaveToClass="defaultStore.state.animation ? $style['folder-toggle-leave-to'] : ''"
@enter="enter" @enter="enter"
@afterEnter="afterEnter" @afterEnter="afterEnter"
@leave="leave" @leave="leave"
@ -42,8 +45,8 @@ const props = withDefaults(defineProps<{
expanded: true, expanded: true,
}); });
const el = shallowRef<HTMLDivElement>(); const rootEl = shallowRef<HTMLDivElement>();
const bg = ref<string | null>(null); const bg = ref<string>();
const showBody = ref((props.persistKey && miLocalStorage.getItem(`${miLocalStoragePrefix}${props.persistKey}`)) ? (miLocalStorage.getItem(`${miLocalStoragePrefix}${props.persistKey}`) === 't') : props.expanded); const showBody = ref((props.persistKey && miLocalStorage.getItem(`${miLocalStoragePrefix}${props.persistKey}`)) ? (miLocalStorage.getItem(`${miLocalStoragePrefix}${props.persistKey}`) === 't') : props.expanded);
watch(showBody, () => { watch(showBody, () => {
@ -52,40 +55,44 @@ watch(showBody, () => {
} }
}); });
function enter(el: Element) { function enter(element: Element) {
const el = element as HTMLElement;
const elementHeight = el.getBoundingClientRect().height; const elementHeight = el.getBoundingClientRect().height;
el.style.height = 0; el.style.height = '0';
el.offsetHeight; // reflow el.offsetHeight; // reflow
el.style.height = elementHeight + 'px'; el.style.height = elementHeight + 'px';
} }
function afterEnter(el: Element) { function afterEnter(element: Element) {
el.style.height = null; const el = element as HTMLElement;
el.style.height = 'unset';
} }
function leave(el: Element) { function leave(element: Element) {
const el = element as HTMLElement;
const elementHeight = el.getBoundingClientRect().height; const elementHeight = el.getBoundingClientRect().height;
el.style.height = elementHeight + 'px'; el.style.height = elementHeight + 'px';
el.offsetHeight; // reflow el.offsetHeight; // reflow
el.style.height = 0; el.style.height = '0';
} }
function afterLeave(el: Element) { function afterLeave(element: Element) {
el.style.height = null; const el = element as HTMLElement;
el.style.height = 'unset';
} }
onMounted(() => { onMounted(() => {
function getParentBg(el: HTMLElement | null): string { function getParentBg(el?: HTMLElement | null): string {
if (el == null || el.tagName === 'BODY') return 'var(--bg)'; if (el == null || el.tagName === 'BODY') return 'var(--bg)';
const bg = el.style.background || el.style.backgroundColor; const background = el.style.background || el.style.backgroundColor;
if (bg) { if (background) {
return bg; return background;
} else { } else {
return getParentBg(el.parentElement); return getParentBg(el.parentElement);
} }
} }
const rawBg = getParentBg(el.value); const rawBg = getParentBg(rootEl.value);
const _bg = tinycolor(rawBg.startsWith('var(') ? getComputedStyle(document.documentElement).getPropertyValue(rawBg.slice(4, -1)) : rawBg); const _bg = tinycolor(rawBg.startsWith('var(') ? getComputedStyle(document.documentElement).getPropertyValue(rawBg.slice(4, -1)) : rawBg);
_bg.setAlpha(0.85); _bg.setAlpha(0.85);
bg.value = _bg.toRgbString(); bg.value = _bg.toRgbString();
@ -97,10 +104,8 @@ onMounted(() => {
overflow-y: clip; overflow-y: clip;
transition: opacity 0.5s, height 0.5s !important; transition: opacity 0.5s, height 0.5s !important;
} }
.folder-toggle-enter-from {
opacity: 0; .folder-toggle-enter-from, .folder-toggle-leave-to {
}
.folder-toggle-leave-to {
opacity: 0; opacity: 0;
} }

View file

@ -25,7 +25,7 @@ SPDX-License-Identifier: AGPL-3.0-only
</div> </div>
</template> </template>
<div v-if="openedAtLeastOnce" :class="[$style.body, { [$style.bgSame]: bgSame }]" :style="{ maxHeight: maxHeight ? `${maxHeight}px` : null, overflow: maxHeight ? `auto` : null }" :aria-hidden="!opened"> <div v-if="openedAtLeastOnce" :class="[$style.body, { [$style.bgSame]: bgSame }]" :style="{ maxHeight: maxHeight ? `${maxHeight}px` : undefined, overflow: maxHeight ? `auto` : undefined }" :aria-hidden="!opened">
<Transition <Transition
:enterActiveClass="defaultStore.state.animation ? $style.transition_toggle_enterActive : ''" :enterActiveClass="defaultStore.state.animation ? $style.transition_toggle_enterActive : ''"
:leaveActiveClass="defaultStore.state.animation ? $style.transition_toggle_leaveActive : ''" :leaveActiveClass="defaultStore.state.animation ? $style.transition_toggle_leaveActive : ''"
@ -109,7 +109,7 @@ function toggle() {
onMounted(() => { onMounted(() => {
const computedStyle = getComputedStyle(document.documentElement); const computedStyle = getComputedStyle(document.documentElement);
const parentBg = getBgColor(rootEl.value.parentElement); const parentBg = getBgColor(rootEl.value!.parentElement!);
const myBg = computedStyle.getPropertyValue('--panel'); const myBg = computedStyle.getPropertyValue('--panel');
bgSame.value = parentBg === myBg; bgSame.value = parentBg === myBg;
}); });

View file

@ -8,7 +8,7 @@ SPDX-License-Identifier: AGPL-3.0-only
ref="dialog" ref="dialog"
:width="370" :width="370"
:height="400" :height="400"
@close="dialog.close()" @close="dialog?.close()"
@closed="emit('closed')" @closed="emit('closed')"
> >
<template #header>{{ i18n.ts.forgotPassword }}</template> <template #header>{{ i18n.ts.forgotPassword }}</template>
@ -66,6 +66,6 @@ async function onSubmit() {
email: email.value, email: email.value,
}); });
emit('done'); emit('done');
dialog.value.close(); dialog.value?.close();
} }
</script> </script>

View file

@ -40,11 +40,11 @@ SPDX-License-Identifier: AGPL-3.0-only
</MkSwitch> </MkSwitch>
<MkSelect v-else-if="form[item].type === 'enum'" v-model="values[item]"> <MkSelect v-else-if="form[item].type === 'enum'" v-model="values[item]">
<template #label><span v-text="form[item].label || item"></span><span v-if="form[item].required === false"> ({{ i18n.ts.optional }})</span></template> <template #label><span v-text="form[item].label || item"></span><span v-if="form[item].required === false"> ({{ i18n.ts.optional }})</span></template>
<option v-for="item in form[item].enum" :key="item.value" :value="item.value">{{ item.label }}</option> <option v-for="option in form[item].enum" :key="option.value" :value="option.value">{{ option.label }}</option>
</MkSelect> </MkSelect>
<MkRadios v-else-if="form[item].type === 'radio'" v-model="values[item]"> <MkRadios v-else-if="form[item].type === 'radio'" v-model="values[item]">
<template #label><span v-text="form[item].label || item"></span><span v-if="form[item].required === false"> ({{ i18n.ts.optional }})</span></template> <template #label><span v-text="form[item].label || item"></span><span v-if="form[item].required === false"> ({{ i18n.ts.optional }})</span></template>
<option v-for="item in form[item].options" :key="item.value" :value="item.value">{{ item.label }}</option> <option v-for="option in form[item].options" :key="option.value" :value="option.value">{{ option.label }}</option>
</MkRadios> </MkRadios>
<MkRange v-else-if="form[item].type === 'range'" v-model="values[item]" :min="form[item].min" :max="form[item].max" :step="form[item].step" :textConverter="form[item].textConverter"> <MkRange v-else-if="form[item].type === 'range'" v-model="values[item]" :min="form[item].min" :max="form[item].max" :step="form[item].step" :textConverter="form[item].textConverter">
<template #label><span v-text="form[item].label || item"></span><span v-if="form[item].required === false"> ({{ i18n.ts.optional }})</span></template> <template #label><span v-text="form[item].label || item"></span><span v-if="form[item].required === false"> ({{ i18n.ts.optional }})</span></template>
@ -86,6 +86,7 @@ const emit = defineEmits<{
canceled?: boolean; canceled?: boolean;
result?: any; result?: any;
}): void; }): void;
(ev: 'closed'): void;
}>(); }>();
const dialog = shallowRef<InstanceType<typeof MkModalWindow>>(); const dialog = shallowRef<InstanceType<typeof MkModalWindow>>();
@ -99,13 +100,13 @@ function ok() {
emit('done', { emit('done', {
result: values, result: values,
}); });
dialog.value.close(); dialog.value?.close();
} }
function cancel() { function cancel() {
emit('done', { emit('done', {
canceled: true, canceled: true,
}); });
dialog.value.close(); dialog.value?.close();
} }
</script> </script>

View file

@ -14,8 +14,8 @@ SPDX-License-Identifier: AGPL-3.0-only
leaveActiveClass: $style.transition_toggle_leaveActive, leaveActiveClass: $style.transition_toggle_leaveActive,
leaveToClass: $style.transition_toggle_leaveTo, leaveToClass: $style.transition_toggle_leaveTo,
}" }"
:src="post.files[0].thumbnailUrl" :src="post.files?.[0]?.thumbnailUrl"
:hash="post.files[0].blurhash" :hash="post.files?.[0]?.blurhash"
:forceBlurhash="!show" :forceBlurhash="!show"
/> />
</Transition> </Transition>

View file

@ -35,10 +35,10 @@ const props = withDefaults(defineProps<{
label: '', label: '',
}); });
const rootEl = shallowRef<HTMLDivElement>(null); const rootEl = shallowRef<HTMLDivElement | null>(null);
const chartEl = shallowRef<HTMLCanvasElement>(null); const chartEl = shallowRef<HTMLCanvasElement | null>(null);
const now = new Date(); const now = new Date();
let chartInstance: Chart = null; let chartInstance: Chart | null = null;
const fetching = ref(true); const fetching = ref(true);
const { handler: externalTooltipHandler } = useChartTooltip({ const { handler: externalTooltipHandler } = useChartTooltip({
@ -46,6 +46,7 @@ const { handler: externalTooltipHandler } = useChartTooltip({
}); });
async function renderChart() { async function renderChart() {
if (rootEl.value == null) return;
if (chartInstance) { if (chartInstance) {
chartInstance.destroy(); chartInstance.destroy();
} }
@ -64,7 +65,7 @@ async function renderChart() {
return new Date(y, m, d - ago); return new Date(y, m, d - ago);
}; };
const format = (arr) => { const format = (arr: number[]) => {
return arr.map((v, i) => { return arr.map((v, i) => {
const dt = getDate(i); const dt = getDate(i);
const iso = `${dt.getFullYear()}-${(dt.getMonth() + 1).toString().padStart(2, '0')}-${dt.getDate().toString().padStart(2, '0')}`; const iso = `${dt.getFullYear()}-${(dt.getMonth() + 1).toString().padStart(2, '0')}-${dt.getDate().toString().padStart(2, '0')}`;
@ -77,7 +78,7 @@ async function renderChart() {
}); });
}; };
let values; let values: number[] = [];
if (props.src === 'active-users') { if (props.src === 'active-users') {
const raw = await misskeyApi('charts/active-users', { limit: chartLimit, span: 'day' }); const raw = await misskeyApi('charts/active-users', { limit: chartLimit, span: 'day' });
@ -114,25 +115,25 @@ async function renderChart() {
const marginEachCell = 4; const marginEachCell = 4;
if (chartEl.value == null) return;
chartInstance = new Chart(chartEl.value, { chartInstance = new Chart(chartEl.value, {
type: 'matrix', type: 'matrix',
data: { data: {
datasets: [{ datasets: [{
label: props.label, label: props.label,
data: format(values), data: format(values) as any,
pointRadius: 0,
borderWidth: 0, borderWidth: 0,
borderJoinStyle: 'round',
borderRadius: 3, borderRadius: 3,
backgroundColor(c) { backgroundColor(c) {
const value = c.dataset.data[c.dataIndex].v; // @ts-expect-error TS(2339)
const value = c.dataset.data[c.dataIndex].v as number;
let a = (value - min) / max; let a = (value - min) / max;
if (value !== 0) { // 0 if (value !== 0) { // 0
a = Math.max(a, 0.05); a = Math.max(a, 0.05);
} }
return alpha(color, a); return alpha(color, a);
}, },
fill: true,
width(c) { width(c) {
const a = c.chart.chartArea ?? {}; const a = c.chart.chartArea ?? {};
return (a.right - a.left) / weeks - marginEachCell; return (a.right - a.left) / weeks - marginEachCell;
@ -206,11 +207,13 @@ async function renderChart() {
enabled: false, enabled: false,
callbacks: { callbacks: {
title(context) { title(context) {
const v = context[0].dataset.data[context[0].dataIndex]; // @ts-expect-error TS(2339)
return v.d; return context[0].dataset.data[context[0].dataIndex].d;
}, },
label(context) { label(context) {
const v = context.dataset.data[context.dataIndex]; const v = context.dataset.data[context.dataIndex];
// @ts-expect-error TS(2339)
return [v.v]; return [v.v];
}, },
}, },

View file

@ -73,7 +73,7 @@ const props = withDefaults(defineProps<{
leaveFromClass?: string; leaveFromClass?: string;
} | null; } | null;
src?: string | null; src?: string | null;
hash?: string; hash?: string | null;
alt?: string | null; alt?: string | null;
title?: string | null; title?: string | null;
height?: number; height?: number;

View file

@ -88,17 +88,18 @@ const focused = ref(false);
const changed = ref(false); const changed = ref(false);
const invalid = ref(false); const invalid = ref(false);
const filled = computed(() => v.value !== '' && v.value != null); const filled = computed(() => v.value !== '' && v.value != null);
const inputEl = shallowRef<HTMLElement>(); const inputEl = shallowRef<HTMLInputElement>();
const prefixEl = shallowRef<HTMLElement>(); const prefixEl = shallowRef<HTMLElement>();
const suffixEl = shallowRef<HTMLElement>(); const suffixEl = shallowRef<HTMLElement>();
const height = const height =
props.small ? 33 : props.small ? 33 :
props.large ? 39 : props.large ? 39 :
36; 36;
let autocomplete: Autocomplete; let autocompleteWorker: Autocomplete | null = null;
const focus = () => inputEl.value.focus(); const focus = () => inputEl.value?.focus();
const onInput = (ev: KeyboardEvent) => { const onInput = (event: Event) => {
const ev = event as KeyboardEvent;
changed.value = true; changed.value = true;
emit('change', ev); emit('change', ev);
}; };
@ -115,9 +116,9 @@ const onKeydown = (ev: KeyboardEvent) => {
const updated = () => { const updated = () => {
changed.value = false; changed.value = false;
if (type.value === 'number') { if (type.value === 'number') {
emit('update:modelValue', parseFloat(v.value)); emit('update:modelValue', typeof v.value === 'number' ? v.value : parseFloat(v.value ?? '0'));
} else { } else {
emit('update:modelValue', v.value); emit('update:modelValue', v.value ?? '');
} }
}; };
@ -127,7 +128,7 @@ watch(modelValue, newValue => {
v.value = newValue; v.value = newValue;
}); });
watch(v, newValue => { watch(v, () => {
if (!props.manualSave) { if (!props.manualSave) {
if (props.debounce) { if (props.debounce) {
debouncedUpdated(); debouncedUpdated();
@ -136,12 +137,14 @@ watch(v, newValue => {
} }
} }
invalid.value = inputEl.value.validity.badInput; invalid.value = inputEl.value?.validity.badInput ?? true;
}); });
// //
// 0 // 0
useInterval(() => { useInterval(() => {
if (inputEl.value == null) return;
if (prefixEl.value) { if (prefixEl.value) {
if (prefixEl.value.offsetWidth) { if (prefixEl.value.offsetWidth) {
inputEl.value.style.paddingLeft = prefixEl.value.offsetWidth + 'px'; inputEl.value.style.paddingLeft = prefixEl.value.offsetWidth + 'px';
@ -163,15 +166,15 @@ onMounted(() => {
focus(); focus();
} }
}); });
if (props.mfmAutocomplete) { if (props.mfmAutocomplete && inputEl.value) {
autocomplete = new Autocomplete(inputEl.value, v, props.mfmAutocomplete === true ? null : props.mfmAutocomplete); autocompleteWorker = new Autocomplete(inputEl.value, v, props.mfmAutocomplete === true ? undefined : props.mfmAutocomplete);
} }
}); });
onUnmounted(() => { onUnmounted(() => {
if (autocomplete) { if (autocompleteWorker) {
autocomplete.detach(); autocompleteWorker.detach();
} }
}); });

View file

@ -138,7 +138,8 @@ function createDoughnut(chartEl, tooltip, data) {
}, },
}, },
onClick: (ev) => { onClick: (ev) => {
const hit = chartInstance.getElementsAtEventForMode(ev, 'nearest', { intersect: true }, false)[0]; if (ev.native == null) return;
const hit = chartInstance.getElementsAtEventForMode(ev.native, 'nearest', { intersect: true }, false)[0];
if (hit && data[hit.index].onClick) { if (hit && data[hit.index].onClick) {
data[hit.index].onClick(); data[hit.index].onClick();
} }
@ -164,23 +165,46 @@ function createDoughnut(chartEl, tooltip, data) {
onMounted(() => { onMounted(() => {
misskeyApiGet('federation/stats', { limit: 30 }).then(fedStats => { misskeyApiGet('federation/stats', { limit: 30 }).then(fedStats => {
createDoughnut(subDoughnutEl.value, externalTooltipHandler1, fedStats.topSubInstances.map(x => ({ type ChartData = {
name: string,
color: string | null,
value: number,
onClick?: () => void,
}[];
const subs: ChartData = fedStats.topSubInstances.map(x => ({
name: x.host, name: x.host,
color: x.themeColor, color: x.themeColor,
value: x.followersCount, value: x.followersCount,
onClick: () => { onClick: () => {
os.pageWindow(`/instance-info/${x.host}`); os.pageWindow(`/instance-info/${x.host}`);
}, },
})).concat([{ name: '(other)', color: '#80808080', value: fedStats.otherFollowersCount }])); }));
createDoughnut(pubDoughnutEl.value, externalTooltipHandler2, fedStats.topPubInstances.map(x => ({ subs.push({
name: '(other)',
color: '#80808080',
value: fedStats.otherFollowersCount,
});
createDoughnut(subDoughnutEl.value, externalTooltipHandler1, subs);
const pubs: ChartData = fedStats.topPubInstances.map(x => ({
name: x.host, name: x.host,
color: x.themeColor, color: x.themeColor,
value: x.followingCount, value: x.followingCount,
onClick: () => { onClick: () => {
os.pageWindow(`/instance-info/${x.host}`); os.pageWindow(`/instance-info/${x.host}`);
}, },
})).concat([{ name: '(other)', color: '#80808080', value: fedStats.otherFollowingCount }])); }));
pubs.push({
name: '(other)',
color: '#80808080',
value: fedStats.otherFollowingCount,
});
createDoughnut(pubDoughnutEl.value, externalTooltipHandler2, pubs);
}); });
}); });
</script> </script>

View file

@ -18,9 +18,9 @@ import { getProxiedImageUrlNullable } from '@/scripts/media-proxy.js';
const props = defineProps<{ const props = defineProps<{
instance?: { instance?: {
faviconUrl?: string faviconUrl?: string | null
name: string name?: string | null
themeColor?: string themeColor?: string | null
} }
}>(); }>();
@ -30,7 +30,7 @@ const instance = props.instance ?? {
themeColor: (document.querySelector('meta[name="theme-color-orig"]') as HTMLMetaElement).content, themeColor: (document.querySelector('meta[name="theme-color-orig"]') as HTMLMetaElement).content,
}; };
const faviconUrl = computed(() => props.instance ? getProxiedImageUrlNullable(props.instance.faviconUrl, 'preview') : getProxiedImageUrlNullable(Instance.iconUrl, 'preview') ?? getProxiedImageUrlNullable(Instance.faviconUrl, 'preview') ?? '/favicon.ico'); const faviconUrl = computed(() => props.instance ? getProxiedImageUrlNullable(props.instance.faviconUrl, 'preview') : getProxiedImageUrlNullable(Instance.iconUrl, 'preview') ?? '/favicon.ico');
const themeColor = instance.themeColor ?? '#777777'; const themeColor = instance.themeColor ?? '#777777';

View file

@ -4,7 +4,7 @@ SPDX-License-Identifier: AGPL-3.0-only
--> -->
<template> <template>
<MkModal ref="modal" v-slot="{ type, maxHeight }" :preferType="preferedModalType" :anchor="anchor" :transparentBg="true" :src="src" @click="modal.close()" @closed="emit('closed')"> <MkModal ref="modal" v-slot="{ type, maxHeight }" :preferType="preferedModalType" :anchor="anchor" :transparentBg="true" :src="src" @click="modal?.close()" @closed="emit('closed')">
<div class="szkkfdyq _popup _shadow" :class="{ asDrawer: type === 'drawer' }" :style="{ maxHeight: maxHeight ? maxHeight + 'px' : '' }"> <div class="szkkfdyq _popup _shadow" :class="{ asDrawer: type === 'drawer' }" :style="{ maxHeight: maxHeight ? maxHeight + 'px' : '' }">
<div class="main"> <div class="main">
<template v-for="item in items" :key="item.text"> <template v-for="item in items" :key="item.text">
@ -63,7 +63,7 @@ const items = Object.keys(navbarItemDef).filter(k => !menu.includes(k)).map(k =>
})); }));
function close() { function close() {
modal.value.close(); modal.value?.close();
} }
</script> </script>

View file

@ -30,6 +30,7 @@ export default {
const contentEl = ref<HTMLElement>(); const contentEl = ref<HTMLElement>();
function calc() { function calc() {
if (contentEl.value == null) return;
const eachLength = contentEl.value.offsetWidth / props.repeat; const eachLength = contentEl.value.offsetWidth / props.repeat;
const factor = 3000; const factor = 3000;
const duration = props.duration / ((1 / eachLength) * factor); const duration = props.duration / ((1 / eachLength) * factor);

View file

@ -52,7 +52,7 @@ const count = computed(() => props.mediaList.filter(media => previewable(media))
let lightbox: PhotoSwipeLightbox | null; let lightbox: PhotoSwipeLightbox | null;
const popstateHandler = (): void => { const popstateHandler = (): void => {
if (lightbox.pswp && lightbox.pswp.isOpen === true) { if (lightbox?.pswp && lightbox.pswp.isOpen === true) {
lightbox.pswp.close(); lightbox.pswp.close();
} }
}; };
@ -67,7 +67,10 @@ async function calcAspectRatio() {
return; return;
} }
const ratioMax = (ratio: number) => `${Math.max(ratio, img.properties.width / img.properties.height).toString()} / 1`; const ratioMax = (ratio: number) => {
if (img.properties.width == null || img.properties.height == null) return '';
return `${Math.max(ratio, img.properties.width / img.properties.height).toString()} / 1`;
};
switch (defaultStore.state.mediaListWithOneImageAppearance) { switch (defaultStore.state.mediaListWithOneImageAppearance) {
case '16_9': case '16_9':
@ -137,7 +140,7 @@ onMounted(() => {
// element is children // element is children
const { element } = itemData; const { element } = itemData;
const id = element.dataset.id; const id = element?.dataset.id;
const file = props.mediaList.find(media => media.id === id); const file = props.mediaList.find(media => media.id === id);
if (!file) return; if (!file) return;
@ -147,14 +150,14 @@ onMounted(() => {
if (file.properties.orientation != null && file.properties.orientation >= 5) { if (file.properties.orientation != null && file.properties.orientation >= 5) {
[itemData.w, itemData.h] = [itemData.h, itemData.w]; [itemData.w, itemData.h] = [itemData.h, itemData.w];
} }
itemData.msrc = file.thumbnailUrl; itemData.msrc = file.thumbnailUrl ?? undefined;
itemData.alt = file.comment ?? file.name; itemData.alt = file.comment ?? file.name;
itemData.comment = file.comment ?? file.name; itemData.comment = file.comment ?? file.name;
itemData.thumbCropped = true; itemData.thumbCropped = true;
}); });
lightbox.on('uiRegister', () => { lightbox.on('uiRegister', () => {
lightbox.pswp.ui.registerElement({ lightbox?.pswp?.ui?.registerElement({
name: 'altText', name: 'altText',
className: 'pwsp__alt-text-container', className: 'pwsp__alt-text-container',
appendTo: 'wrapper', appendTo: 'wrapper',
@ -163,8 +166,8 @@ onMounted(() => {
textBox.className = 'pwsp__alt-text _acrylic'; textBox.className = 'pwsp__alt-text _acrylic';
el.appendChild(textBox); el.appendChild(textBox);
pwsp.on('change', (a) => { pwsp.on('change', () => {
textBox.textContent = pwsp.currSlide.data.comment; textBox.textContent = pwsp.currSlide?.data.comment;
}); });
}, },
}); });

View file

@ -33,6 +33,7 @@ const align = 'left';
const SCROLLBAR_THICKNESS = 16; const SCROLLBAR_THICKNESS = 16;
function setPosition() { function setPosition() {
if (el.value == null) return;
const rootRect = props.rootElement.getBoundingClientRect(); const rootRect = props.rootElement.getBoundingClientRect();
const parentRect = props.targetElement.getBoundingClientRect(); const parentRect = props.targetElement.getBoundingClientRect();
const myRect = el.value.getBoundingClientRect(); const myRect = el.value.getBoundingClientRect();
@ -66,7 +67,7 @@ const ro = new ResizeObserver((entries, observer) => {
}); });
onMounted(() => { onMounted(() => {
ro.observe(el.value); if (el.value) ro.observe(el.value);
setPosition(); setPosition();
nextTick(() => { nextTick(() => {
setPosition(); setPosition();
@ -79,7 +80,7 @@ onUnmounted(() => {
defineExpose({ defineExpose({
checkHit: (ev: MouseEvent) => { checkHit: (ev: MouseEvent) => {
return (ev.target === el.value || el.value.contains(ev.target)); return (ev.target === el.value || el.value?.contains(ev.target as Node));
}, },
}); });
</script> </script>

View file

@ -12,7 +12,7 @@ SPDX-License-Identifier: AGPL-3.0-only
:style="{ width: (width && !asDrawer) ? width + 'px' : '', maxHeight: maxHeight ? maxHeight + 'px' : '' }" :style="{ width: (width && !asDrawer) ? width + 'px' : '', maxHeight: maxHeight ? maxHeight + 'px' : '' }"
@contextmenu.self="e => e.preventDefault()" @contextmenu.self="e => e.preventDefault()"
> >
<template v-for="(item, i) in items2"> <template v-for="(item, i) in (items2 ?? [])">
<div v-if="item.type === 'divider'" role="separator" :class="$style.divider"></div> <div v-if="item.type === 'divider'" role="separator" :class="$style.divider"></div>
<span v-else-if="item.type === 'label'" role="menuitem" :class="[$style.label, $style.item]"> <span v-else-if="item.type === 'label'" role="menuitem" :class="[$style.label, $style.item]">
<span style="opacity: 0.7;">{{ item.text }}</span> <span style="opacity: 0.7;">{{ item.text }}</span>
@ -54,7 +54,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<span :class="$style.caret" style="pointer-events: none;"><i class="ti ti-chevron-right ti-fw"></i></span> <span :class="$style.caret" style="pointer-events: none;"><i class="ti ti-chevron-right ti-fw"></i></span>
</div> </div>
</button> </button>
<button v-else :tabindex="i" class="_button" role="menuitem" :class="[$style.item, { [$style.danger]: item.danger, [$style.active]: item.active }]" :disabled="item.active" @click="clicked(item.action, $event)" @mouseenter.passive="onItemMouseEnter(item)" @mouseleave.passive="onItemMouseLeave(item)"> <button v-else :tabindex="i" class="_button" role="menuitem" :class="[$style.item, { [$style.danger]: item.danger, [$style.active]: getValue(item.active) }]" :disabled="getValue(item.active)" @click="clicked(item.action, $event)" @mouseenter.passive="onItemMouseEnter(item)" @mouseleave.passive="onItemMouseLeave(item)">
<i v-if="item.icon" class="ti-fw" :class="[$style.icon, item.icon]"></i> <i v-if="item.icon" class="ti-fw" :class="[$style.icon, item.icon]"></i>
<MkAvatar v-if="item.avatar" :user="item.avatar" :class="$style.avatar"/> <MkAvatar v-if="item.avatar" :user="item.avatar" :class="$style.avatar"/>
<div :class="$style.item_content"> <div :class="$style.item_content">
@ -63,18 +63,18 @@ SPDX-License-Identifier: AGPL-3.0-only
</div> </div>
</button> </button>
</template> </template>
<span v-if="items2.length === 0" :class="[$style.none, $style.item]"> <span v-if="items2 == null || items2.length === 0" :class="[$style.none, $style.item]">
<span>{{ i18n.ts.none }}</span> <span>{{ i18n.ts.none }}</span>
</span> </span>
</div> </div>
<div v-if="childMenu"> <div v-if="childMenu">
<XChild ref="child" :items="childMenu" :targetElement="childTarget" :rootElement="itemsEl" showing @actioned="childActioned" @close="close(false)"/> <XChild ref="child" :items="childMenu" :targetElement="childTarget!" :rootElement="itemsEl!" showing @actioned="childActioned" @close="close(false)"/>
</div> </div>
</div> </div>
</template> </template>
<script lang="ts"> <script lang="ts">
import { computed, defineAsyncComponent, nextTick, onBeforeUnmount, onMounted, ref, shallowRef, watch } from 'vue'; import { ComputedRef, computed, defineAsyncComponent, isRef, nextTick, onBeforeUnmount, onMounted, ref, shallowRef, watch } from 'vue';
import { focusPrev, focusNext } from '@/scripts/focus.js'; import { focusPrev, focusNext } from '@/scripts/focus.js';
import MkSwitchButton from '@/components/MkSwitch.button.vue'; import MkSwitchButton from '@/components/MkSwitch.button.vue';
import { MenuItem, InnerMenuItem, MenuPending, MenuAction, MenuSwitch, MenuParent } from '@/types/menu.js'; import { MenuItem, InnerMenuItem, MenuPending, MenuAction, MenuSwitch, MenuParent } from '@/types/menu.js';
@ -104,7 +104,7 @@ const emit = defineEmits<{
const itemsEl = shallowRef<HTMLDivElement>(); const itemsEl = shallowRef<HTMLDivElement>();
const items2 = ref<InnerMenuItem[]>([]); const items2 = ref<InnerMenuItem[]>();
const child = shallowRef<InstanceType<typeof XChild>>(); const child = shallowRef<InstanceType<typeof XChild>>();
@ -119,15 +119,15 @@ const childShowingItem = ref<MenuItem | null>();
let preferClick = isTouchUsing || props.asDrawer; let preferClick = isTouchUsing || props.asDrawer;
watch(() => props.items, () => { watch(() => props.items, () => {
const items: (MenuItem | MenuPending)[] = [...props.items].filter(item => item !== undefined); const items = [...props.items].filter(item => item !== undefined) as (NonNullable<MenuItem> | MenuPending)[];
for (let i = 0; i < items.length; i++) { for (let i = 0; i < items.length; i++) {
const item = items[i]; const item = items[i];
if (item && 'then' in item) { // if item is Promise if ('then' in item) { // if item is Promise
items[i] = { type: 'pending' }; items[i] = { type: 'pending' };
item.then(actualItem => { item.then(actualItem => {
items2.value[i] = actualItem; if (items2.value?.[i]) items2.value[i] = actualItem;
}); });
} }
} }
@ -151,7 +151,7 @@ function childActioned() {
} }
const onGlobalMousedown = (event: MouseEvent) => { const onGlobalMousedown = (event: MouseEvent) => {
if (childTarget.value && (event.target === childTarget.value || childTarget.value.contains(event.target))) return; if (childTarget.value && (event.target === childTarget.value || childTarget.value.contains(event.target as Node))) return;
if (child.value && child.value.checkHit(event)) return; if (child.value && child.value.checkHit(event)) return;
closeChild(); closeChild();
}; };
@ -169,7 +169,7 @@ function onItemMouseLeave(item) {
} }
async function showChildren(item: MenuParent, ev: MouseEvent) { async function showChildren(item: MenuParent, ev: MouseEvent) {
const children = await (async () => { const children: MenuItem[] = await (async () => {
if (childrenCache.has(item)) { if (childrenCache.has(item)) {
return childrenCache.get(item)!; return childrenCache.get(item)!;
} else { } else {
@ -189,7 +189,7 @@ async function showChildren(item: MenuParent, ev: MouseEvent) {
}); });
emit('hide'); emit('hide');
} else { } else {
childTarget.value = ev.currentTarget ?? ev.target; childTarget.value = (ev.currentTarget ?? ev.target) as HTMLElement;
// //
childMenu.value = children; childMenu.value = children;
childShowingItem.value = item; childShowingItem.value = item;
@ -218,6 +218,10 @@ function switchItem(item: MenuSwitch & { ref: any }) {
item.ref = !item.ref; item.ref = !item.ref;
} }
function getValue<T>(item?: ComputedRef<T> | T) {
return isRef(item) ? item.value : item;
}
onMounted(() => { onMounted(() => {
if (props.viaKeyboard) { if (props.viaKeyboard) {
nextTick(() => { nextTick(() => {

View file

@ -22,8 +22,8 @@ SPDX-License-Identifier: AGPL-3.0-only
stroke-width="2" stroke-width="2"
/> />
<circle <circle
:cx="headX" :cx="headX ?? undefined"
:cy="headY" :cy="headY ?? undefined"
r="3" r="3"
:fill="color" :fill="color"
/> />

View file

@ -51,7 +51,7 @@ const bodyWidth = ref(0);
const bodyHeight = ref(0); const bodyHeight = ref(0);
const close = () => { const close = () => {
modal.value.close(); modal.value?.close();
}; };
const onBgClick = () => { const onBgClick = () => {
@ -67,11 +67,13 @@ const onKeydown = (evt) => {
}; };
const ro = new ResizeObserver((entries, observer) => { const ro = new ResizeObserver((entries, observer) => {
if (rootEl.value == null || headerEl.value == null) return;
bodyWidth.value = rootEl.value.offsetWidth; bodyWidth.value = rootEl.value.offsetWidth;
bodyHeight.value = rootEl.value.offsetHeight - headerEl.value.offsetHeight; bodyHeight.value = rootEl.value.offsetHeight - headerEl.value.offsetHeight;
}); });
onMounted(() => { onMounted(() => {
if (rootEl.value == null || headerEl.value == null) return;
bodyWidth.value = rootEl.value.offsetWidth; bodyWidth.value = rootEl.value.offsetWidth;
bodyHeight.value = rootEl.value.offsetHeight - headerEl.value.offsetHeight; bodyHeight.value = rootEl.value.offsetHeight - headerEl.value.offsetHeight;
ro.observe(rootEl.value); ro.observe(rootEl.value);

View file

@ -7,7 +7,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<div <div
v-if="!hardMuted && muted === false" v-if="!hardMuted && muted === false"
v-show="!isDeleted" v-show="!isDeleted"
ref="el" ref="rootEl"
v-hotkey="keymap" v-hotkey="keymap"
:class="[$style.root, { [$style.showActionsOnlyHover]: defaultStore.state.showNoteActionsOnlyHover }]" :class="[$style.root, { [$style.showActionsOnlyHover]: defaultStore.state.showNoteActionsOnlyHover }]"
:tabindex="!isDeleted ? '-1' : undefined" :tabindex="!isDeleted ? '-1' : undefined"
@ -72,16 +72,16 @@ SPDX-License-Identifier: AGPL-3.0-only
/> />
<div v-if="translating || translation" :class="$style.translation"> <div v-if="translating || translation" :class="$style.translation">
<MkLoading v-if="translating" mini/> <MkLoading v-if="translating" mini/>
<div v-else> <div v-else-if="translation">
<b>{{ i18n.tsx.translatedFrom({ x: translation.sourceLang }) }}: </b> <b>{{ i18n.tsx.translatedFrom({ x: translation.sourceLang }) }}: </b>
<Mfm :text="translation.text" :author="appearNote.user" :nyaize="'respect'" :emojiUrls="appearNote.emojis"/> <Mfm :text="translation.text" :author="appearNote.user" :nyaize="'respect'" :emojiUrls="appearNote.emojis"/>
</div> </div>
</div> </div>
</div> </div>
<div v-if="appearNote.files.length > 0"> <div v-if="appearNote.files && appearNote.files.length > 0">
<MkMediaList :mediaList="appearNote.files"/> <MkMediaList :mediaList="appearNote.files"/>
</div> </div>
<MkPoll v-if="appearNote.poll" :note="appearNote" :class="$style.poll"/> <MkPoll v-if="appearNote.poll" :noteId="appearNote.id" :poll="appearNote.poll" :class="$style.poll"/>
<MkUrlPreview v-for="url in urls" :key="url" :url="url" :compact="true" :detail="false" :class="$style.urlPreview"/> <MkUrlPreview v-for="url in urls" :key="url" :url="url" :compact="true" :detail="false" :class="$style.urlPreview"/>
<div v-if="appearNote.renote" :class="$style.quote"><MkNoteSimple :note="appearNote.renote" :class="$style.quoteNote"/></div> <div v-if="appearNote.renote" :class="$style.quote"><MkNoteSimple :note="appearNote.renote" :class="$style.quoteNote"/></div>
<button v-if="isLong && collapsed" :class="$style.collapsed" class="_button" @click="collapsed = false"> <button v-if="isLong && collapsed" :class="$style.collapsed" class="_button" @click="collapsed = false">
@ -126,7 +126,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<button v-if="defaultStore.state.showClipButtonInNoteFooter" ref="clipButton" :class="$style.footerButton" class="_button" @mousedown="clip()"> <button v-if="defaultStore.state.showClipButtonInNoteFooter" ref="clipButton" :class="$style.footerButton" class="_button" @mousedown="clip()">
<i class="ti ti-paperclip"></i> <i class="ti ti-paperclip"></i>
</button> </button>
<button ref="menuButton" :class="$style.footerButton" class="_button" @mousedown="menu()"> <button ref="menuButton" :class="$style.footerButton" class="_button" @mousedown="showMenu()">
<i class="ti ti-dots"></i> <i class="ti ti-dots"></i>
</button> </button>
</footer> </footer>
@ -222,7 +222,7 @@ if (noteViewInterruptors.length > 0) {
let result: Misskey.entities.Note | null = deepClone(note.value); let result: Misskey.entities.Note | null = deepClone(note.value);
for (const interruptor of noteViewInterruptors) { for (const interruptor of noteViewInterruptors) {
try { try {
result = await interruptor.handler(result); result = await interruptor.handler(result!) as Misskey.entities.Note | null;
if (result === null) { if (result === null) {
isDeleted.value = true; isDeleted.value = true;
return; return;
@ -231,7 +231,7 @@ if (noteViewInterruptors.length > 0) {
console.error(err); console.error(err);
} }
} }
note.value = result; note.value = result as Misskey.entities.Note;
}); });
} }
@ -239,11 +239,11 @@ const isRenote = (
note.value.renote != null && note.value.renote != null &&
note.value.text == null && note.value.text == null &&
note.value.cw == null && note.value.cw == null &&
note.value.fileIds.length === 0 && note.value.fileIds && note.value.fileIds.length === 0 &&
note.value.poll == null note.value.poll == null
); );
const el = shallowRef<HTMLElement>(); const rootEl = shallowRef<HTMLElement>();
const menuButton = shallowRef<HTMLElement>(); const menuButton = shallowRef<HTMLElement>();
const renoteButton = shallowRef<HTMLElement>(); const renoteButton = shallowRef<HTMLElement>();
const renoteTime = shallowRef<HTMLElement>(); const renoteTime = shallowRef<HTMLElement>();
@ -262,8 +262,8 @@ const hardMuted = ref(props.withHardMute && checkMute(appearNote.value, $i?.hard
const translation = ref<Misskey.entities.NotesTranslateResponse | null>(null); const translation = ref<Misskey.entities.NotesTranslateResponse | null>(null);
const translating = ref(false); const translating = ref(false);
const showTicker = (defaultStore.state.instanceTicker === 'always') || (defaultStore.state.instanceTicker === 'remote' && appearNote.value.user.instance); const showTicker = (defaultStore.state.instanceTicker === 'always') || (defaultStore.state.instanceTicker === 'remote' && appearNote.value.user.instance);
const canRenote = computed(() => ['public', 'home'].includes(appearNote.value.visibility) || (appearNote.value.visibility === 'followers' && appearNote.value.userId === $i.id)); const canRenote = computed(() => ['public', 'home'].includes(appearNote.value.visibility) || (appearNote.value.visibility === 'followers' && appearNote.value.userId === $i?.id));
const renoteCollapsed = ref(defaultStore.state.collapseRenotes && isRenote && (($i && ($i.id === note.value.userId || $i.id === appearNote.value.userId)) || (appearNote.value.myReaction != null))); const renoteCollapsed = ref(defaultStore.state.collapseRenotes && isRenote && (($i && ($i.id === note.value.userId || $i.id === appearNote.value.userId)) ?? (appearNote.value.myReaction != null)));
/* Overload FunctionLint /* Overload FunctionLint
function checkMute(noteToCheck: Misskey.entities.Note, mutedWords: Array<string | string[]> | undefined | null, checkOnly: true): boolean; function checkMute(noteToCheck: Misskey.entities.Note, mutedWords: Array<string | string[]> | undefined | null, checkOnly: true): boolean;
@ -285,11 +285,11 @@ function checkMute(noteToCheck: Misskey.entities.Note, mutedWords: Array<string
const keymap = { const keymap = {
'r': () => reply(true), 'r': () => reply(true),
'e|a|plus': () => react(true), 'e|a|plus': () => react(true),
'q': () => renoteButton.value.renote(true), 'q': () => renote(true),
'up|k|shift+tab': focusBefore, 'up|k|shift+tab': focusBefore,
'down|j|tab': focusAfter, 'down|j|tab': focusAfter,
'esc': blur, 'esc': blur,
'm|o': () => menu(true), 'm|o': () => showMenu(true),
's': () => showContent.value !== showContent.value, 's': () => showContent.value !== showContent.value,
}; };
@ -306,7 +306,7 @@ if (props.mock) {
}, { deep: true }); }, { deep: true });
} else { } else {
useNoteCapture({ useNoteCapture({
rootEl: el, rootEl: rootEl,
note: appearNote, note: appearNote,
pureNote: note, pureNote: note,
isDeletedRef: isDeleted, isDeletedRef: isDeleted,
@ -352,7 +352,7 @@ function reply(viaKeyboard = false): void {
reply: appearNote.value, reply: appearNote.value,
channel: appearNote.value.channel, channel: appearNote.value.channel,
animation: !viaKeyboard, animation: !viaKeyboard,
}, () => { }).then(() => {
focus(); focus();
}); });
} }
@ -371,7 +371,7 @@ function react(viaKeyboard = false): void {
noteId: appearNote.value.id, noteId: appearNote.value.id,
reaction: '❤️', reaction: '❤️',
}); });
const el = reactButton.value as HTMLElement | null | undefined; const el = reactButton.value;
if (el) { if (el) {
const rect = el.getBoundingClientRect(); const rect = el.getBoundingClientRect();
const x = rect.left + (el.offsetWidth / 2); const x = rect.left + (el.offsetWidth / 2);
@ -380,7 +380,7 @@ function react(viaKeyboard = false): void {
} }
} else { } else {
blur(); blur();
reactionPicker.show(reactButton.value, reaction => { reactionPicker.show(reactButton.value ?? null, reaction => {
sound.playMisskeySfx('reaction'); sound.playMisskeySfx('reaction');
if (props.mock) { if (props.mock) {
@ -401,8 +401,8 @@ function react(viaKeyboard = false): void {
} }
} }
function undoReact(note): void { function undoReact(targetNote: Misskey.entities.Note): void {
const oldReaction = note.myReaction; const oldReaction = targetNote.myReaction;
if (!oldReaction) return; if (!oldReaction) return;
if (props.mock) { if (props.mock) {
@ -411,7 +411,7 @@ function undoReact(note): void {
} }
misskeyApi('notes/reactions/delete', { misskeyApi('notes/reactions/delete', {
noteId: note.id, noteId: targetNote.id,
}); });
} }
@ -420,32 +420,34 @@ function onContextmenu(ev: MouseEvent): void {
return; return;
} }
const isLink = (el: HTMLElement) => { const isLink = (el: HTMLElement): boolean => {
if (el.tagName === 'A') return true; if (el.tagName === 'A') return true;
// Audio // Audio
if (el.tagName === 'AUDIO') return true; if (el.tagName === 'AUDIO') return true;
if (el.parentElement) { if (el.parentElement) {
return isLink(el.parentElement); return isLink(el.parentElement);
} }
return false;
}; };
if (isLink(ev.target)) return;
if (window.getSelection().toString() !== '') return; if (ev.target && isLink(ev.target as HTMLElement)) return;
if (window.getSelection()?.toString() !== '') return;
if (defaultStore.state.useReactionPickerForContextMenu) { if (defaultStore.state.useReactionPickerForContextMenu) {
ev.preventDefault(); ev.preventDefault();
react(); react();
} else { } else {
const { menu, cleanup } = getNoteMenu({ note: note.value, translating, translation, menuButton, isDeleted, currentClip: currentClip?.value }); const { menu, cleanup } = getNoteMenu({ note: note.value, translating, translation, isDeleted, currentClip: currentClip?.value });
os.contextMenu(menu, ev).then(focus).finally(cleanup); os.contextMenu(menu, ev).then(focus).finally(cleanup);
} }
} }
function menu(viaKeyboard = false): void { function showMenu(viaKeyboard = false): void {
if (props.mock) { if (props.mock) {
return; return;
} }
const { menu, cleanup } = getNoteMenu({ note: note.value, translating, translation, menuButton, isDeleted, currentClip: currentClip?.value }); const { menu, cleanup } = getNoteMenu({ note: note.value, translating, translation, isDeleted, currentClip: currentClip?.value });
os.popupMenu(menu, menuButton.value, { os.popupMenu(menu, menuButton.value, {
viaKeyboard, viaKeyboard,
}).then(focus).finally(cleanup); }).then(focus).finally(cleanup);
@ -492,7 +494,7 @@ function showRenoteMenu(viaKeyboard = false): void {
getCopyNoteLinkMenu(note.value, i18n.ts.copyLinkRenote), getCopyNoteLinkMenu(note.value, i18n.ts.copyLinkRenote),
{ type: 'divider' }, { type: 'divider' },
getAbuseNoteMenu(note.value, i18n.ts.reportAbuseRenote), getAbuseNoteMenu(note.value, i18n.ts.reportAbuseRenote),
$i.isModerator || $i.isAdmin ? getUnrenote() : undefined, ($i?.isModerator || $i?.isAdmin) ? getUnrenote() : undefined,
], renoteTime.value, { ], renoteTime.value, {
viaKeyboard: viaKeyboard, viaKeyboard: viaKeyboard,
}); });
@ -500,19 +502,19 @@ function showRenoteMenu(viaKeyboard = false): void {
} }
function focus() { function focus() {
el.value.focus(); rootEl.value?.focus();
} }
function blur() { function blur() {
el.value.blur(); rootEl.value?.blur();
} }
function focusBefore() { function focusBefore() {
focusPrev(el.value); focusPrev(rootEl.value ?? null);
} }
function focusAfter() { function focusAfter() {
focusNext(el.value); focusNext(rootEl.value ?? null);
} }
function readPromo() { function readPromo() {

View file

@ -7,7 +7,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<div <div
v-if="!muted" v-if="!muted"
v-show="!isDeleted" v-show="!isDeleted"
ref="el" ref="rootEl"
v-hotkey="keymap" v-hotkey="keymap"
:class="$style.root" :class="$style.root"
> >
@ -86,15 +86,15 @@ SPDX-License-Identifier: AGPL-3.0-only
<a v-if="appearNote.renote != null" :class="$style.rn">RN:</a> <a v-if="appearNote.renote != null" :class="$style.rn">RN:</a>
<div v-if="translating || translation" :class="$style.translation"> <div v-if="translating || translation" :class="$style.translation">
<MkLoading v-if="translating" mini/> <MkLoading v-if="translating" mini/>
<div v-else> <div v-else-if="translation">
<b>{{ i18n.tsx.translatedFrom({ x: translation.sourceLang }) }}: </b> <b>{{ i18n.tsx.translatedFrom({ x: translation.sourceLang }) }}: </b>
<Mfm :text="translation.text" :author="appearNote.user" :nyaize="'respect'" :emojiUrls="appearNote.emojis"/> <Mfm :text="translation.text" :author="appearNote.user" :nyaize="'respect'" :emojiUrls="appearNote.emojis"/>
</div> </div>
</div> </div>
<div v-if="appearNote.files.length > 0"> <div v-if="appearNote.files && appearNote.files.length > 0">
<MkMediaList :mediaList="appearNote.files"/> <MkMediaList :mediaList="appearNote.files"/>
</div> </div>
<MkPoll v-if="appearNote.poll" ref="pollViewer" :note="appearNote" :class="$style.poll"/> <MkPoll v-if="appearNote.poll" ref="pollViewer" :noteId="appearNote.id" :poll="appearNote.poll" :class="$style.poll"/>
<MkUrlPreview v-for="url in urls" :key="url" :url="url" :compact="true" :detail="true" style="margin-top: 6px;"/> <MkUrlPreview v-for="url in urls" :key="url" :url="url" :compact="true" :detail="true" style="margin-top: 6px;"/>
<div v-if="appearNote.renote" :class="$style.quote"><MkNoteSimple :note="appearNote.renote" :class="$style.quoteNote"/></div> <div v-if="appearNote.renote" :class="$style.quote"><MkNoteSimple :note="appearNote.renote" :class="$style.quoteNote"/></div>
</div> </div>
@ -134,7 +134,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<button v-if="defaultStore.state.showClipButtonInNoteFooter" ref="clipButton" class="_button" :class="$style.noteFooterButton" @mousedown="clip()"> <button v-if="defaultStore.state.showClipButtonInNoteFooter" ref="clipButton" class="_button" :class="$style.noteFooterButton" @mousedown="clip()">
<i class="ti ti-paperclip"></i> <i class="ti ti-paperclip"></i>
</button> </button>
<button ref="menuButton" class="_button" :class="$style.noteFooterButton" @mousedown="menu()"> <button ref="menuButton" class="_button" :class="$style.noteFooterButton" @mousedown="showMenu()">
<i class="ti ti-dots"></i> <i class="ti ti-dots"></i>
</button> </button>
</footer> </footer>
@ -225,7 +225,7 @@ import { claimAchievement } from '@/scripts/achievements.js';
import MkRippleEffect from '@/components/MkRippleEffect.vue'; import MkRippleEffect from '@/components/MkRippleEffect.vue';
import { showMovedDialog } from '@/scripts/show-moved-dialog.js'; import { showMovedDialog } from '@/scripts/show-moved-dialog.js';
import MkUserCardMini from '@/components/MkUserCardMini.vue'; import MkUserCardMini from '@/components/MkUserCardMini.vue';
import MkPagination from '@/components/MkPagination.vue'; import MkPagination, { type Paging } from '@/components/MkPagination.vue';
import MkReactionIcon from '@/components/MkReactionIcon.vue'; import MkReactionIcon from '@/components/MkReactionIcon.vue';
import MkButton from '@/components/MkButton.vue'; import MkButton from '@/components/MkButton.vue';
@ -243,7 +243,7 @@ if (noteViewInterruptors.length > 0) {
let result: Misskey.entities.Note | null = deepClone(note.value); let result: Misskey.entities.Note | null = deepClone(note.value);
for (const interruptor of noteViewInterruptors) { for (const interruptor of noteViewInterruptors) {
try { try {
result = await interruptor.handler(result); result = await interruptor.handler(result!) as Misskey.entities.Note | null;
if (result === null) { if (result === null) {
isDeleted.value = true; isDeleted.value = true;
return; return;
@ -252,18 +252,18 @@ if (noteViewInterruptors.length > 0) {
console.error(err); console.error(err);
} }
} }
note.value = result; note.value = result as Misskey.entities.Note;
}); });
} }
const isRenote = ( const isRenote = (
note.value.renote != null && note.value.renote != null &&
note.value.text == null && note.value.text == null &&
note.value.fileIds.length === 0 && note.value.fileIds && note.value.fileIds.length === 0 &&
note.value.poll == null note.value.poll == null
); );
const el = shallowRef<HTMLElement>(); const rootEl = shallowRef<HTMLElement>();
const menuButton = shallowRef<HTMLElement>(); const menuButton = shallowRef<HTMLElement>();
const renoteButton = shallowRef<HTMLElement>(); const renoteButton = shallowRef<HTMLElement>();
const renoteTime = shallowRef<HTMLElement>(); const renoteTime = shallowRef<HTMLElement>();
@ -281,14 +281,14 @@ const urls = parsed ? extractUrlFromMfm(parsed) : null;
const showTicker = (defaultStore.state.instanceTicker === 'always') || (defaultStore.state.instanceTicker === 'remote' && appearNote.value.user.instance); const showTicker = (defaultStore.state.instanceTicker === 'always') || (defaultStore.state.instanceTicker === 'remote' && appearNote.value.user.instance);
const conversation = ref<Misskey.entities.Note[]>([]); const conversation = ref<Misskey.entities.Note[]>([]);
const replies = ref<Misskey.entities.Note[]>([]); const replies = ref<Misskey.entities.Note[]>([]);
const canRenote = computed(() => ['public', 'home'].includes(appearNote.value.visibility) || appearNote.value.userId === $i.id); const canRenote = computed(() => ['public', 'home'].includes(appearNote.value.visibility) || appearNote.value.userId === $i?.id);
const keymap = { const keymap = {
'r': () => reply(true), 'r': () => reply(true),
'e|a|plus': () => react(true), 'e|a|plus': () => react(true),
'q': () => renoteButton.value.renote(true), 'q': () => renote(true),
'esc': blur, 'esc': blur,
'm|o': () => menu(true), 'm|o': () => showMenu(true),
's': () => showContent.value !== showContent.value, 's': () => showContent.value !== showContent.value,
}; };
@ -302,7 +302,7 @@ provide('react', (reaction: string) => {
const tab = ref('replies'); const tab = ref('replies');
const reactionTabType = ref<string | null>(null); const reactionTabType = ref<string | null>(null);
const renotesPagination = computed(() => ({ const renotesPagination = computed<Paging>(() => ({
endpoint: 'notes/renotes', endpoint: 'notes/renotes',
limit: 10, limit: 10,
params: { params: {
@ -310,7 +310,7 @@ const renotesPagination = computed(() => ({
}, },
})); }));
const reactionsPagination = computed(() => ({ const reactionsPagination = computed<Paging>(() => ({
endpoint: 'notes/reactions', endpoint: 'notes/reactions',
limit: 10, limit: 10,
params: { params: {
@ -320,7 +320,7 @@ const reactionsPagination = computed(() => ({
})); }));
useNoteCapture({ useNoteCapture({
rootEl: el, rootEl: rootEl,
note: appearNote, note: appearNote,
pureNote: note, pureNote: note,
isDeletedRef: isDeleted, isDeletedRef: isDeleted,
@ -361,7 +361,7 @@ function reply(viaKeyboard = false): void {
reply: appearNote.value, reply: appearNote.value,
channel: appearNote.value.channel, channel: appearNote.value.channel,
animation: !viaKeyboard, animation: !viaKeyboard,
}, () => { }).then(() => {
focus(); focus();
}); });
} }
@ -385,7 +385,7 @@ function react(viaKeyboard = false): void {
} }
} else { } else {
blur(); blur();
reactionPicker.show(reactButton.value, reaction => { reactionPicker.show(reactButton.value ?? null, reaction => {
sound.playMisskeySfx('reaction'); sound.playMisskeySfx('reaction');
misskeyApi('notes/reactions/create', { misskeyApi('notes/reactions/create', {
@ -410,26 +410,28 @@ function undoReact(note): void {
} }
function onContextmenu(ev: MouseEvent): void { function onContextmenu(ev: MouseEvent): void {
const isLink = (el: HTMLElement) => { const isLink = (el: HTMLElement): boolean => {
if (el.tagName === 'A') return true; if (el.tagName === 'A') return true;
if (el.parentElement) { if (el.parentElement) {
return isLink(el.parentElement); return isLink(el.parentElement);
} }
return false;
}; };
if (isLink(ev.target)) return;
if (window.getSelection().toString() !== '') return; if (ev.target && isLink(ev.target as HTMLElement)) return;
if (window.getSelection()?.toString() !== '') return;
if (defaultStore.state.useReactionPickerForContextMenu) { if (defaultStore.state.useReactionPickerForContextMenu) {
ev.preventDefault(); ev.preventDefault();
react(); react();
} else { } else {
const { menu, cleanup } = getNoteMenu({ note: note.value, translating, translation, menuButton, isDeleted }); const { menu, cleanup } = getNoteMenu({ note: note.value, translating, translation, isDeleted });
os.contextMenu(menu, ev).then(focus).finally(cleanup); os.contextMenu(menu, ev).then(focus).finally(cleanup);
} }
} }
function menu(viaKeyboard = false): void { function showMenu(viaKeyboard = false): void {
const { menu, cleanup } = getNoteMenu({ note: note.value, translating, translation, menuButton, isDeleted }); const { menu, cleanup } = getNoteMenu({ note: note.value, translating, translation, isDeleted });
os.popupMenu(menu, menuButton.value, { os.popupMenu(menu, menuButton.value, {
viaKeyboard, viaKeyboard,
}).then(focus).finally(cleanup); }).then(focus).finally(cleanup);
@ -458,11 +460,11 @@ function showRenoteMenu(viaKeyboard = false): void {
} }
function focus() { function focus() {
el.value.focus(); rootEl.value?.focus();
} }
function blur() { function blur() {
el.value.blur(); rootEl.value?.blur();
} }
const repliesLoaded = ref(false); const repliesLoaded = ref(false);
@ -481,6 +483,7 @@ const conversationLoaded = ref(false);
function loadConversation() { function loadConversation() {
conversationLoaded.value = true; conversationLoaded.value = true;
if (appearNote.value.replyId == null) return;
misskeyApi('notes/conversation', { misskeyApi('notes/conversation', {
noteId: appearNote.value.replyId, noteId: appearNote.value.replyId,
}).then(res => { }).then(res => {

View file

@ -14,7 +14,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<div v-if="note.user.isBot" :class="$style.isBot">bot</div> <div v-if="note.user.isBot" :class="$style.isBot">bot</div>
<div :class="$style.username"><MkAcct :user="note.user"/></div> <div :class="$style.username"><MkAcct :user="note.user"/></div>
<div v-if="note.user.badgeRoles" :class="$style.badgeRoles"> <div v-if="note.user.badgeRoles" :class="$style.badgeRoles">
<img v-for="role in note.user.badgeRoles" :key="role.id" v-tooltip="role.name" :class="$style.badgeRole" :src="role.iconUrl"/> <img v-for="(role, i) in note.user.badgeRoles" :key="i" v-tooltip="role.name" :class="$style.badgeRole" :src="role.iconUrl!"/>
</div> </div>
<div :class="$style.info"> <div :class="$style.info">
<div v-if="mock"> <div v-if="mock">

View file

@ -12,7 +12,7 @@ SPDX-License-Identifier: AGPL-3.0-only
</div> </div>
<div> <div>
<p v-if="useCw" :class="$style.cw"> <p v-if="useCw" :class="$style.cw">
<Mfm v-if="cw != ''" :text="cw" :author="user" :nyaize="'respect'" :i="user" style="margin-right: 8px;"/> <Mfm v-if="cw != null && cw != ''" :text="cw" :author="user" :nyaize="'respect'" :i="user" style="margin-right: 8px;"/>
<MkCwButton v-model="showContent" :text="text.trim()" :files="files" :poll="poll" style="margin: 4px 0;"/> <MkCwButton v-model="showContent" :text="text.trim()" :files="files" :poll="poll" style="margin: 4px 0;"/>
</p> </p>
<div v-show="!useCw || showContent"> <div v-show="!useCw || showContent">
@ -26,6 +26,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<script lang="ts" setup> <script lang="ts" setup>
import { ref } from 'vue'; import { ref } from 'vue';
import * as Misskey from 'misskey-js'; import * as Misskey from 'misskey-js';
import type { PollEditorModelValue } from '@/components/MkPollEditor.vue';
import MkCwButton from '@/components/MkCwButton.vue'; import MkCwButton from '@/components/MkCwButton.vue';
const showContent = ref(false); const showContent = ref(false);
@ -33,12 +34,7 @@ const showContent = ref(false);
const props = defineProps<{ const props = defineProps<{
text: string; text: string;
files: Misskey.entities.DriveFile[]; files: Misskey.entities.DriveFile[];
poll?: { poll?: PollEditorModelValue;
choices: string[];
multiple: boolean;
expiresAt: string | null;
expiredAfter: string | null;
};
useCw: boolean; useCw: boolean;
cw: string | null; cw: string | null;
user: Misskey.entities.User; user: Misskey.entities.User;

View file

@ -6,10 +6,8 @@ SPDX-License-Identifier: AGPL-3.0-only
<template> <template>
<div :class="$style.root"> <div :class="$style.root">
<div :class="$style.head"> <div :class="$style.head">
<MkAvatar v-if="notification.type === 'pollEnded'" :class="$style.icon" :user="notification.note.user" link preview/> <MkAvatar v-if="['pollEnded', 'note'].includes(notification.type) && notification.note" :class="$style.icon" :user="notification.note.user" link preview/>
<MkAvatar v-else-if="notification.type === 'note'" :class="$style.icon" :user="notification.note.user" link preview/> <MkAvatar v-else-if="['roleAssigned', 'achievementEarned'].includes(notification.type)" :class="$style.icon" :user="$i" link preview/>
<MkAvatar v-else-if="notification.type === 'roleAssigned'" :class="$style.icon" :user="$i" link preview/>
<MkAvatar v-else-if="notification.type === 'achievementEarned'" :class="$style.icon" :user="$i" link preview/>
<div v-else-if="notification.type === 'reaction:grouped'" :class="[$style.icon, $style.icon_reactionGroup]"><i class="ti ti-plus" style="line-height: 1;"></i></div> <div v-else-if="notification.type === 'reaction:grouped'" :class="[$style.icon, $style.icon_reactionGroup]"><i class="ti ti-plus" style="line-height: 1;"></i></div>
<div v-else-if="notification.type === 'renote:grouped'" :class="[$style.icon, $style.icon_renoteGroup]"><i class="ti ti-repeat" style="line-height: 1;"></i></div> <div v-else-if="notification.type === 'renote:grouped'" :class="[$style.icon, $style.icon_renoteGroup]"><i class="ti ti-repeat" style="line-height: 1;"></i></div>
<img v-else-if="notification.type === 'test'" :class="$style.icon" :src="infoImageUrl"/> <img v-else-if="notification.type === 'test'" :class="$style.icon" :src="infoImageUrl"/>
@ -26,6 +24,7 @@ SPDX-License-Identifier: AGPL-3.0-only
[$style.t_quote]: notification.type === 'quote', [$style.t_quote]: notification.type === 'quote',
[$style.t_pollEnded]: notification.type === 'pollEnded', [$style.t_pollEnded]: notification.type === 'pollEnded',
[$style.t_achievementEarned]: notification.type === 'achievementEarned', [$style.t_achievementEarned]: notification.type === 'achievementEarned',
[$style.t_roleAssigned]: notification.type === 'roleAssigned' && notification.role.iconUrl == null,
}]" }]"
> >
<i v-if="notification.type === 'follow'" class="ti ti-plus"></i> <i v-if="notification.type === 'follow'" class="ti ti-plus"></i>
@ -37,12 +36,14 @@ SPDX-License-Identifier: AGPL-3.0-only
<i v-else-if="notification.type === 'quote'" class="ti ti-quote"></i> <i v-else-if="notification.type === 'quote'" class="ti ti-quote"></i>
<i v-else-if="notification.type === 'pollEnded'" class="ti ti-chart-arrows"></i> <i v-else-if="notification.type === 'pollEnded'" class="ti ti-chart-arrows"></i>
<i v-else-if="notification.type === 'achievementEarned'" class="ti ti-medal"></i> <i v-else-if="notification.type === 'achievementEarned'" class="ti ti-medal"></i>
<img v-else-if="notification.type === 'roleAssigned'" style="height: 1.3em; vertical-align: -22%;" :src="notification.role.iconUrl" alt=""/> <template v-else-if="notification.type === 'roleAssigned'">
<!-- notification.reaction null になることはまずないがここでoptional chaining使うと一部ブラウザで刺さるので念の為 --> <img v-if="notification.role.iconUrl" style="height: 1.3em; vertical-align: -22%;" :src="notification.role.iconUrl" alt=""/>
<i v-else class="ti ti-badges"></i>
</template>
<MkReactionIcon <MkReactionIcon
v-else-if="notification.type === 'reaction'" v-else-if="notification.type === 'reaction'"
:withTooltip="true" :withTooltip="true"
:reaction="notification.reaction ? notification.reaction.replace(/^:(\w+):$/, ':$1@.:') : notification.reaction" :reaction="notification.reaction.replace(/^:(\w+):$/, ':$1@.:')"
:noStyle="true" :noStyle="true"
style="width: 100%; height: 100%;" style="width: 100%; height: 100%;"
/> />
@ -55,10 +56,10 @@ SPDX-License-Identifier: AGPL-3.0-only
<span v-else-if="notification.type === 'roleAssigned'">{{ i18n.ts._notification.roleAssigned }}</span> <span v-else-if="notification.type === 'roleAssigned'">{{ i18n.ts._notification.roleAssigned }}</span>
<span v-else-if="notification.type === 'achievementEarned'">{{ i18n.ts._notification.achievementEarned }}</span> <span v-else-if="notification.type === 'achievementEarned'">{{ i18n.ts._notification.achievementEarned }}</span>
<span v-else-if="notification.type === 'test'">{{ i18n.ts._notification.testNotification }}</span> <span v-else-if="notification.type === 'test'">{{ i18n.ts._notification.testNotification }}</span>
<MkA v-else-if="notification.user" v-user-preview="notification.user.id" :class="$style.headerName" :to="userPage(notification.user)"><MkUserName :user="notification.user"/></MkA> <MkA v-else-if="notification.type === 'follow' || notification.type === 'mention' || notification.type === 'reply' || notification.type === 'renote' || notification.type === 'quote' || notification.type === 'reaction' || notification.type === 'receiveFollowRequest' || notification.type === 'followRequestAccepted'" v-user-preview="notification.user.id" :class="$style.headerName" :to="userPage(notification.user)"><MkUserName :user="notification.user"/></MkA>
<span v-else-if="notification.type === 'reaction:grouped'">{{ i18n.tsx._notification.reactedBySomeUsers({ n: notification.reactions.length }) }}</span> <span v-else-if="notification.type === 'reaction:grouped'">{{ i18n.tsx._notification.reactedBySomeUsers({ n: notification.reactions.length }) }}</span>
<span v-else-if="notification.type === 'renote:grouped'">{{ i18n.tsx._notification.renotedBySomeUsers({ n: notification.users.length }) }}</span> <span v-else-if="notification.type === 'renote:grouped'">{{ i18n.tsx._notification.renotedBySomeUsers({ n: notification.users.length }) }}</span>
<span v-else>{{ notification.header }}</span> <span v-else-if="notification.type === 'app'">{{ notification.header }}</span>
<MkTime v-if="withTime" :time="notification.createdAt" :class="$style.headerTime"/> <MkTime v-if="withTime" :time="notification.createdAt" :class="$style.headerTime"/>
</header> </header>
<div> <div>
@ -97,7 +98,6 @@ SPDX-License-Identifier: AGPL-3.0-only
</MkA> </MkA>
<template v-else-if="notification.type === 'follow'"> <template v-else-if="notification.type === 'follow'">
<span :class="$style.text" style="opacity: 0.6;">{{ i18n.ts.youGotNewFollower }}</span> <span :class="$style.text" style="opacity: 0.6;">{{ i18n.ts.youGotNewFollower }}</span>
<div v-if="full"><MkFollowButton :user="notification.user" :full="true"/></div>
</template> </template>
<span v-else-if="notification.type === 'followRequestAccepted'" :class="$style.text" style="opacity: 0.6;">{{ i18n.ts.followRequestAccepted }}</span> <span v-else-if="notification.type === 'followRequestAccepted'" :class="$style.text" style="opacity: 0.6;">{{ i18n.ts.followRequestAccepted }}</span>
<template v-else-if="notification.type === 'receiveFollowRequest'"> <template v-else-if="notification.type === 'receiveFollowRequest'">
@ -113,12 +113,12 @@ SPDX-License-Identifier: AGPL-3.0-only
</span> </span>
<div v-if="notification.type === 'reaction:grouped'"> <div v-if="notification.type === 'reaction:grouped'">
<div v-for="reaction of notification.reactions" :class="$style.reactionsItem"> <div v-for="reaction of notification.reactions" :key="reaction.user.id + reaction.reaction" :class="$style.reactionsItem">
<MkAvatar :class="$style.reactionsItemAvatar" :user="reaction.user" link preview/> <MkAvatar :class="$style.reactionsItemAvatar" :user="reaction.user" link preview/>
<div :class="$style.reactionsItemReaction"> <div :class="$style.reactionsItemReaction">
<MkReactionIcon <MkReactionIcon
:withTooltip="true" :withTooltip="true"
:reaction="reaction.reaction ? reaction.reaction.replace(/^:(\w+):$/, ':$1@.:') : reaction.reaction" :reaction="reaction.reaction.replace(/^:(\w+):$/, ':$1@.:')"
:noStyle="true" :noStyle="true"
style="width: 100%; height: 100%;" style="width: 100%; height: 100%;"
/> />
@ -126,7 +126,7 @@ SPDX-License-Identifier: AGPL-3.0-only
</div> </div>
</div> </div>
<div v-else-if="notification.type === 'renote:grouped'"> <div v-else-if="notification.type === 'renote:grouped'">
<div v-for="user of notification.users" :class="$style.reactionsItem"> <div v-for="user of notification.users" :key="user.id" :class="$style.reactionsItem">
<MkAvatar :class="$style.reactionsItemAvatar" :user="user" link preview/> <MkAvatar :class="$style.reactionsItemAvatar" :user="user" link preview/>
</div> </div>
</div> </div>
@ -139,16 +139,17 @@ SPDX-License-Identifier: AGPL-3.0-only
import { ref } from 'vue'; import { ref } from 'vue';
import * as Misskey from 'misskey-js'; import * as Misskey from 'misskey-js';
import MkReactionIcon from '@/components/MkReactionIcon.vue'; import MkReactionIcon from '@/components/MkReactionIcon.vue';
import MkFollowButton from '@/components/MkFollowButton.vue';
import MkButton from '@/components/MkButton.vue'; import MkButton from '@/components/MkButton.vue';
import { getNoteSummary } from '@/scripts/get-note-summary.js'; import { getNoteSummary } from '@/scripts/get-note-summary.js';
import { notePage } from '@/filters/note.js'; import { notePage } from '@/filters/note.js';
import { userPage } from '@/filters/user.js'; import { userPage } from '@/filters/user.js';
import { i18n } from '@/i18n.js'; import { i18n } from '@/i18n.js';
import { misskeyApi } from '@/scripts/misskey-api.js'; import { misskeyApi } from '@/scripts/misskey-api.js';
import { $i } from '@/account.js'; import { signinRequired } from '@/account.js';
import { infoImageUrl } from '@/instance.js'; import { infoImageUrl } from '@/instance.js';
const $i = signinRequired();
const props = withDefaults(defineProps<{ const props = withDefaults(defineProps<{
notification: Misskey.entities.Notification; notification: Misskey.entities.Notification;
withTime?: boolean; withTime?: boolean;
@ -161,11 +162,13 @@ const props = withDefaults(defineProps<{
const followRequestDone = ref(false); const followRequestDone = ref(false);
const acceptFollowRequest = () => { const acceptFollowRequest = () => {
if (props.notification.user == null) return;
followRequestDone.value = true; followRequestDone.value = true;
misskeyApi('following/requests/accept', { userId: props.notification.user.id }); misskeyApi('following/requests/accept', { userId: props.notification.user.id });
}; };
const rejectFollowRequest = () => { const rejectFollowRequest = () => {
if (props.notification.user == null) return;
followRequestDone.value = true; followRequestDone.value = true;
misskeyApi('following/requests/reject', { userId: props.notification.user.id }); misskeyApi('following/requests/reject', { userId: props.notification.user.id });
}; };
@ -283,6 +286,12 @@ const rejectFollowRequest = () => {
pointer-events: none; pointer-events: none;
} }
.t_roleAssigned {
padding: 3px;
background: #88a6b7;
pointer-events: none;
}
.tail { .tail {
flex: 1; flex: 1;
min-width: 0; min-width: 0;

View file

@ -15,7 +15,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<template #default="{ items: notifications }"> <template #default="{ items: notifications }">
<MkDateSeparatedList v-slot="{ item: notification }" :class="$style.list" :items="notifications" :noGap="true"> <MkDateSeparatedList v-slot="{ item: notification }" :class="$style.list" :items="notifications" :noGap="true">
<MkNote v-if="['reply', 'quote', 'mention'].includes(notification.type)" :key="notification.id" :note="notification.note" :withHardMute="true"/> <MkNote v-if="['reply', 'quote', 'mention'].includes(notification.type)" :key="notification.id + ':note'" :note="notification.note" :withHardMute="true"/>
<XNotification v-else :key="notification.id" :notification="notification" :withTime="true" :full="true" class="_panel"/> <XNotification v-else :key="notification.id" :notification="notification" :withTime="true" :full="true" class="_panel"/>
</MkDateSeparatedList> </MkDateSeparatedList>
</template> </template>
@ -63,7 +63,7 @@ function onNotification(notification) {
} }
if (!isMuted) { if (!isMuted) {
pagingComponent.value.prepend(notification); pagingComponent.value?.prepend(notification);
} }
} }

View file

@ -27,7 +27,7 @@ const omitted = ref(false);
const ignoreOmit = ref(false); const ignoreOmit = ref(false);
const calcOmit = () => { const calcOmit = () => {
if (omitted.value || ignoreOmit.value) return; if (omitted.value || ignoreOmit.value || content.value == null) return;
omitted.value = content.value.offsetHeight > props.maxHeight; omitted.value = content.value.offsetHeight > props.maxHeight;
}; };
@ -37,7 +37,7 @@ const omitObserver = new ResizeObserver((entries, observer) => {
onMounted(() => { onMounted(() => {
calcOmit(); calcOmit();
omitObserver.observe(content.value); omitObserver.observe(content.value as HTMLElement);
}); });
onUnmounted(() => { onUnmounted(() => {

View file

@ -20,7 +20,7 @@ SPDX-License-Identifier: AGPL-3.0-only
</header> </header>
<p v-if="page.summary" :title="page.summary">{{ page.summary.length > 85 ? page.summary.slice(0, 85) + '…' : page.summary }}</p> <p v-if="page.summary" :title="page.summary">{{ page.summary.length > 85 ? page.summary.slice(0, 85) + '…' : page.summary }}</p>
<footer> <footer>
<img class="icon" :src="page.user.avatarUrl"/> <img v-if="page.user.avatarUrl" class="icon" :src="page.user.avatarUrl"/>
<p>{{ userName(page.user) }}</p> <p>{{ userName(page.user) }}</p>
</footer> </footer>
</article> </article>

View file

@ -55,7 +55,7 @@ defineEmits<{
const routerFactory = useRouterFactory(); const routerFactory = useRouterFactory();
const windowRouter = routerFactory(props.initialPath); const windowRouter = routerFactory(props.initialPath);
const contents = shallowRef<HTMLElement>(); const contents = shallowRef<HTMLElement | null>(null);
const pageMetadata = ref<null | ComputedRef<PageMetadata>>(); const pageMetadata = ref<null | ComputedRef<PageMetadata>>();
const windowEl = shallowRef<InstanceType<typeof MkWindow>>(); const windowEl = shallowRef<InstanceType<typeof MkWindow>>();
const history = ref<{ path: string; key: any; }[]>([{ const history = ref<{ path: string; key: any; }[]>([{
@ -63,7 +63,7 @@ const history = ref<{ path: string; key: any; }[]>([{
key: windowRouter.getCurrentKey(), key: windowRouter.getCurrentKey(),
}]); }]);
const buttonsLeft = computed(() => { const buttonsLeft = computed(() => {
const buttons = []; const buttons: Record<string, unknown>[] = [];
if (history.value.length > 1) { if (history.value.length > 1) {
buttons.push({ buttons.push({
@ -121,7 +121,7 @@ const contextmenu = computed(() => ([{
text: i18n.ts.openInNewTab, text: i18n.ts.openInNewTab,
action: () => { action: () => {
window.open(url + windowRouter.getCurrentPath(), '_blank', 'noopener'); window.open(url + windowRouter.getCurrentPath(), '_blank', 'noopener');
windowEl.value.close(); windowEl.value?.close();
}, },
}, { }, {
icon: 'ti ti-link', icon: 'ti ti-link',
@ -141,17 +141,17 @@ function reload() {
} }
function close() { function close() {
windowEl.value.close(); windowEl.value?.close();
} }
function expand() { function expand() {
mainRouter.push(windowRouter.getCurrentPath(), 'forcePage'); mainRouter.push(windowRouter.getCurrentPath(), 'forcePage');
windowEl.value.close(); windowEl.value?.close();
} }
function popout() { function popout() {
_popout(windowRouter.getCurrentPath(), windowEl.value.$el); _popout(windowRouter.getCurrentPath(), windowEl.value?.$el);
windowEl.value.close(); windowEl.value?.close();
} }
useScrollPositionManager(() => getScrollContainer(contents.value), windowRouter); useScrollPositionManager(() => getScrollContainer(contents.value), windowRouter);

View file

@ -204,7 +204,7 @@ async function init(): Promise<void> {
queue.value = new Map(); queue.value = new Map();
fetching.value = true; fetching.value = true;
const params = props.pagination.params ? isRef(props.pagination.params) ? props.pagination.params.value : props.pagination.params : {}; const params = props.pagination.params ? isRef(props.pagination.params) ? props.pagination.params.value : props.pagination.params : {};
await misskeyApi(props.pagination.endpoint, { await misskeyApi<MisskeyEntity[]>(props.pagination.endpoint, {
...params, ...params,
limit: props.pagination.limit ?? 10, limit: props.pagination.limit ?? 10,
allowPartial: true, allowPartial: true,
@ -240,7 +240,7 @@ const fetchMore = async (): Promise<void> => {
if (!more.value || fetching.value || moreFetching.value || items.value.size === 0) return; if (!more.value || fetching.value || moreFetching.value || items.value.size === 0) return;
moreFetching.value = true; moreFetching.value = true;
const params = props.pagination.params ? isRef(props.pagination.params) ? props.pagination.params.value : props.pagination.params : {}; const params = props.pagination.params ? isRef(props.pagination.params) ? props.pagination.params.value : props.pagination.params : {};
await misskeyApi(props.pagination.endpoint, { await misskeyApi<MisskeyEntity[]>(props.pagination.endpoint, {
...params, ...params,
limit: SECOND_FETCH_LIMIT, limit: SECOND_FETCH_LIMIT,
...(props.pagination.offsetMode ? { ...(props.pagination.offsetMode ? {
@ -304,7 +304,7 @@ const fetchMoreAhead = async (): Promise<void> => {
if (!more.value || fetching.value || moreFetching.value || items.value.size === 0) return; if (!more.value || fetching.value || moreFetching.value || items.value.size === 0) return;
moreFetching.value = true; moreFetching.value = true;
const params = props.pagination.params ? isRef(props.pagination.params) ? props.pagination.params.value : props.pagination.params : {}; const params = props.pagination.params ? isRef(props.pagination.params) ? props.pagination.params.value : props.pagination.params : {};
await misskeyApi(props.pagination.endpoint, { await misskeyApi<MisskeyEntity[]>(props.pagination.endpoint, {
...params, ...params,
limit: SECOND_FETCH_LIMIT, limit: SECOND_FETCH_LIMIT,
...(props.pagination.offsetMode ? { ...(props.pagination.offsetMode ? {

View file

@ -6,7 +6,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<template> <template>
<div :class="{ [$style.done]: closed || isVoted }"> <div :class="{ [$style.done]: closed || isVoted }">
<ul :class="$style.choices"> <ul :class="$style.choices">
<li v-for="(choice, i) in note.poll.choices" :key="i" :class="$style.choice" @click="vote(i)"> <li v-for="(choice, i) in poll.choices" :key="i" :class="$style.choice" @click="vote(i)">
<div :class="$style.bg" :style="{ 'width': `${showResult ? (choice.votes / total * 100) : 0}%` }"></div> <div :class="$style.bg" :style="{ 'width': `${showResult ? (choice.votes / total * 100) : 0}%` }"></div>
<span :class="$style.fg"> <span :class="$style.fg">
<template v-if="choice.isVoted"><i class="ti ti-check" style="margin-right: 4px; color: var(--accent);"></i></template> <template v-if="choice.isVoted"><i class="ti ti-check" style="margin-right: 4px; color: var(--accent);"></i></template>
@ -35,35 +35,35 @@ import * as os from '@/os.js';
import { misskeyApi } from '@/scripts/misskey-api.js'; import { misskeyApi } from '@/scripts/misskey-api.js';
import { i18n } from '@/i18n.js'; import { i18n } from '@/i18n.js';
import { useInterval } from '@/scripts/use-interval.js'; import { useInterval } from '@/scripts/use-interval.js';
import { WithNonNullable } from '@/type.js';
const props = defineProps<{ const props = defineProps<{
note: WithNonNullable<Misskey.entities.Note, 'poll'>; noteId: string;
poll: NonNullable<Misskey.entities.Note['poll']>;
readOnly?: boolean; readOnly?: boolean;
}>(); }>();
const remaining = ref(-1); const remaining = ref(-1);
const total = computed(() => sum(props.note.poll.choices.map(x => x.votes))); const total = computed(() => sum(props.poll.choices.map(x => x.votes)));
const closed = computed(() => remaining.value === 0); const closed = computed(() => remaining.value === 0);
const isVoted = computed(() => !props.note.poll.multiple && props.note.poll.choices.some(c => c.isVoted)); const isVoted = computed(() => !props.poll.multiple && props.poll.choices.some(c => c.isVoted));
const timer = computed(() => i18n.tsx._poll[ const timer = computed(() => i18n.tsx._poll[
remaining.value >= 86400 ? 'remainingDays' : remaining.value >= 86400 ? 'remainingDays' :
remaining.value >= 3600 ? 'remainingHours' : remaining.value >= 3600 ? 'remainingHours' :
remaining.value >= 60 ? 'remainingMinutes' : 'remainingSeconds' remaining.value >= 60 ? 'remainingMinutes' : 'remainingSeconds'
]({ ]({
s: Math.floor(remaining.value % 60), s: Math.floor(remaining.value % 60),
m: Math.floor(remaining.value / 60) % 60, m: Math.floor(remaining.value / 60) % 60,
h: Math.floor(remaining.value / 3600) % 24, h: Math.floor(remaining.value / 3600) % 24,
d: Math.floor(remaining.value / 86400), d: Math.floor(remaining.value / 86400),
})); }));
const showResult = ref(props.readOnly || isVoted.value); const showResult = ref(props.readOnly || isVoted.value);
// //
if (props.note.poll.expiresAt) { if (props.poll.expiresAt) {
const tick = () => { const tick = () => {
remaining.value = Math.floor(Math.max(new Date(props.note.poll.expiresAt).getTime() - Date.now(), 0) / 1000); remaining.value = Math.floor(Math.max(new Date(props.poll.expiresAt!).getTime() - Date.now(), 0) / 1000);
if (remaining.value === 0) { if (remaining.value === 0) {
showResult.value = true; showResult.value = true;
} }
@ -82,15 +82,15 @@ const vote = async (id) => {
const { canceled } = await os.confirm({ const { canceled } = await os.confirm({
type: 'question', type: 'question',
text: i18n.tsx.voteConfirm({ choice: props.note.poll.choices[id].text }), text: i18n.tsx.voteConfirm({ choice: props.poll.choices[id].text }),
}); });
if (canceled) return; if (canceled) return;
await misskeyApi('notes/polls/vote', { await misskeyApi('notes/polls/vote', {
noteId: props.note.id, noteId: props.noteId,
choice: id, choice: id,
}); });
if (!showResult.value) showResult.value = !props.note.poll.multiple; if (!showResult.value) showResult.value = !props.poll.multiple;
}; };
</script> </script>

View file

@ -62,21 +62,18 @@ import { formatDateTimeString } from '@/scripts/format-time-string.js';
import { addTime } from '@/scripts/time.js'; import { addTime } from '@/scripts/time.js';
import { i18n } from '@/i18n.js'; import { i18n } from '@/i18n.js';
export type PollEditorModelValue = {
expiresAt: number | null;
expiredAfter: number | null;
choices: string[];
multiple: boolean;
};
const props = defineProps<{ const props = defineProps<{
modelValue: { modelValue: PollEditorModelValue;
expiresAt: string;
expiredAfter: number;
choices: string[];
multiple: boolean;
};
}>(); }>();
const emit = defineEmits<{ const emit = defineEmits<{
(ev: 'update:modelValue', v: { (ev: 'update:modelValue', v: PollEditorModelValue): void;
expiresAt: string;
expiredAfter: number;
choices: string[];
multiple: boolean;
}): void;
}>(); }>();
const choices = ref(props.modelValue.choices); const choices = ref(props.modelValue.choices);
@ -89,7 +86,9 @@ const unit = ref('second');
if (props.modelValue.expiresAt) { if (props.modelValue.expiresAt) {
expiration.value = 'at'; expiration.value = 'at';
atDate.value = atTime.value = props.modelValue.expiresAt; const expiresAt = new Date(props.modelValue.expiresAt);
atDate.value = formatDateTimeString(expiresAt, 'yyyy-MM-dd');
atTime.value = formatDateTimeString(expiresAt, 'HH:mm');
} else if (typeof props.modelValue.expiredAfter === 'number') { } else if (typeof props.modelValue.expiredAfter === 'number') {
expiration.value = 'after'; expiration.value = 'after';
after.value = props.modelValue.expiredAfter / 1000; after.value = props.modelValue.expiredAfter / 1000;
@ -113,20 +112,21 @@ function remove(i) {
choices.value = choices.value.filter((_, _i) => _i !== i); choices.value = choices.value.filter((_, _i) => _i !== i);
} }
function get() { function get(): PollEditorModelValue {
const calcAt = () => { const calcAt = () => {
return new Date(`${atDate.value} ${atTime.value}`).getTime(); return new Date(`${atDate.value} ${atTime.value}`).getTime();
}; };
const calcAfter = () => { const calcAfter = () => {
let base = parseInt(after.value); let base = parseInt(after.value.toString());
switch (unit.value) { switch (unit.value) {
// @ts-expect-error fallthrough
case 'day': base *= 24; case 'day': base *= 24;
// fallthrough // @ts-expect-error fallthrough
case 'hour': base *= 60; case 'hour': base *= 60;
// fallthrough // @ts-expect-error fallthrough
case 'minute': base *= 60; case 'minute': base *= 60;
// fallthrough // eslint-disable-next-line no-fallthrough
case 'second': return base *= 1000; case 'second': return base *= 1000;
default: return null; default: return null;
} }
@ -135,10 +135,8 @@ function get() {
return { return {
choices: choices.value, choices: choices.value,
multiple: multiple.value, multiple: multiple.value,
...( expiresAt: expiration.value === 'at' ? calcAt() : null,
expiration.value === 'at' ? { expiresAt: calcAt() } : expiredAfter: expiration.value === 'after' ? calcAfter() : null,
expiration.value === 'after' ? { expiredAfter: calcAfter() } : {}
),
}; };
} }

View file

@ -84,7 +84,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<button v-tooltip="i18n.ts.useCw" class="_button" :class="[$style.footerButton, { [$style.footerButtonActive]: useCw }]" @click="useCw = !useCw"><i class="ti ti-eye-off"></i></button> <button v-tooltip="i18n.ts.useCw" class="_button" :class="[$style.footerButton, { [$style.footerButtonActive]: useCw }]" @click="useCw = !useCw"><i class="ti ti-eye-off"></i></button>
<button v-tooltip="i18n.ts.mention" class="_button" :class="$style.footerButton" @click="insertMention"><i class="ti ti-at"></i></button> <button v-tooltip="i18n.ts.mention" class="_button" :class="$style.footerButton" @click="insertMention"><i class="ti ti-at"></i></button>
<button v-tooltip="i18n.ts.hashtags" class="_button" :class="[$style.footerButton, { [$style.footerButtonActive]: withHashtags }]" @click="withHashtags = !withHashtags"><i class="ti ti-hash"></i></button> <button v-tooltip="i18n.ts.hashtags" class="_button" :class="[$style.footerButton, { [$style.footerButtonActive]: withHashtags }]" @click="withHashtags = !withHashtags"><i class="ti ti-hash"></i></button>
<button v-if="postFormActions.length > 0" v-tooltip="i18n.ts.plugin" class="_button" :class="$style.footerButton" @click="showActions"><i class="ti ti-plug"></i></button> <button v-if="postFormActions.length > 0" v-tooltip="i18n.ts.plugins" class="_button" :class="$style.footerButton" @click="showActions"><i class="ti ti-plug"></i></button>
<button v-tooltip="i18n.ts.emoji" :class="['_button', $style.footerButton]" @click="insertEmoji"><i class="ti ti-mood-happy"></i></button> <button v-tooltip="i18n.ts.emoji" :class="['_button', $style.footerButton]" @click="insertEmoji"><i class="ti ti-mood-happy"></i></button>
<button v-if="showAddMfmFunction" v-tooltip="i18n.ts.addMfmFunction" :class="['_button', $style.footerButton]" @click="insertMfmFunction"><i class="ti ti-palette"></i></button> <button v-if="showAddMfmFunction" v-tooltip="i18n.ts.addMfmFunction" :class="['_button', $style.footerButton]" @click="insertMfmFunction"><i class="ti ti-palette"></i></button>
</div> </div>
@ -108,7 +108,7 @@ import { toASCII } from 'punycode/';
import MkNoteSimple from '@/components/MkNoteSimple.vue'; import MkNoteSimple from '@/components/MkNoteSimple.vue';
import MkNotePreview from '@/components/MkNotePreview.vue'; import MkNotePreview from '@/components/MkNotePreview.vue';
import XPostFormAttaches from '@/components/MkPostFormAttaches.vue'; import XPostFormAttaches from '@/components/MkPostFormAttaches.vue';
import MkPollEditor from '@/components/MkPollEditor.vue'; import MkPollEditor, { type PollEditorModelValue } from '@/components/MkPollEditor.vue';
import { host, url } from '@/config.js'; import { host, url } from '@/config.js';
import { erase, unique } from '@/scripts/array.js'; import { erase, unique } from '@/scripts/array.js';
import { extractMentions } from '@/scripts/extract-mentions.js'; import { extractMentions } from '@/scripts/extract-mentions.js';
@ -139,13 +139,13 @@ const props = withDefaults(defineProps<{
renote?: Misskey.entities.Note; renote?: Misskey.entities.Note;
channel?: Misskey.entities.Channel; // TODO channel?: Misskey.entities.Channel; // TODO
mention?: Misskey.entities.User; mention?: Misskey.entities.User;
specified?: Misskey.entities.User; specified?: Misskey.entities.UserDetailed;
initialText?: string; initialText?: string;
initialCw?: string; initialCw?: string;
initialVisibility?: (typeof Misskey.noteVisibilities)[number]; initialVisibility?: (typeof Misskey.noteVisibilities)[number];
initialFiles?: Misskey.entities.DriveFile[]; initialFiles?: Misskey.entities.DriveFile[];
initialLocalOnly?: boolean; initialLocalOnly?: boolean;
initialVisibleUsers?: Misskey.entities.User[]; initialVisibleUsers?: Misskey.entities.UserDetailed[];
initialNote?: Misskey.entities.Note; initialNote?: Misskey.entities.Note;
instant?: boolean; instant?: boolean;
fixed?: boolean; fixed?: boolean;
@ -178,12 +178,7 @@ const posting = ref(false);
const posted = ref(false); const posted = ref(false);
const text = ref(props.initialText ?? ''); const text = ref(props.initialText ?? '');
const files = ref(props.initialFiles ?? []); const files = ref(props.initialFiles ?? []);
const poll = ref<{ const poll = ref<PollEditorModelValue | null>(null);
choices: string[];
multiple: boolean;
expiresAt: string | null;
expiredAfter: string | null;
} | null>(null);
const useCw = ref<boolean>(!!props.initialCw); const useCw = ref<boolean>(!!props.initialCw);
const showPreview = ref(defaultStore.state.showPreview); const showPreview = ref(defaultStore.state.showPreview);
watch(showPreview, () => defaultStore.set('showPreview', showPreview.value)); watch(showPreview, () => defaultStore.set('showPreview', showPreview.value));
@ -332,7 +327,7 @@ if (props.reply && ['home', 'followers', 'specified'].includes(props.reply.visib
if (visibility.value === 'specified') { if (visibility.value === 'specified') {
if (props.reply.visibleUserIds) { if (props.reply.visibleUserIds) {
misskeyApi('users/show', { misskeyApi('users/show', {
userIds: props.reply.visibleUserIds.filter(uid => uid !== $i.id && uid !== props.reply.userId), userIds: props.reply.visibleUserIds.filter(uid => uid !== $i.id && uid !== props.reply?.userId),
}).then(users => { }).then(users => {
users.forEach(pushVisibleUser); users.forEach(pushVisibleUser);
}); });
@ -534,7 +529,7 @@ async function toggleReactionAcceptance() {
reactionAcceptance.value = select.result; reactionAcceptance.value = select.result;
} }
function pushVisibleUser(user) { function pushVisibleUser(user: Misskey.entities.UserDetailed) {
if (!visibleUsers.value.some(u => u.username === user.username && u.host === user.host)) { if (!visibleUsers.value.some(u => u.username === user.username && u.host === user.host)) {
visibleUsers.value.push(user); visibleUsers.value.push(user);
} }
@ -576,10 +571,12 @@ function onCompositionEnd(ev: CompositionEvent) {
async function onPaste(ev: ClipboardEvent) { async function onPaste(ev: ClipboardEvent) {
if (props.mock) return; if (props.mock) return;
if (!ev.clipboardData) return;
for (const { item, i } of Array.from(ev.clipboardData.items, (item, i) => ({ item, i }))) { for (const { item, i } of Array.from(ev.clipboardData.items, (data, x) => ({ item: data, i: x }))) {
if (item.kind === 'file') { if (item.kind === 'file') {
const file = item.getAsFile(); const file = item.getAsFile();
if (!file) continue;
const lio = file.name.lastIndexOf('.'); const lio = file.name.lastIndexOf('.');
const ext = lio >= 0 ? file.name.slice(lio) : ''; const ext = lio >= 0 ? file.name.slice(lio) : '';
const formatted = `${formatTimeString(new Date(file.lastModified), defaultStore.state.pastedFileName).replace(/{{number}}/g, `${i + 1}`)}${ext}`; const formatted = `${formatTimeString(new Date(file.lastModified), defaultStore.state.pastedFileName).replace(/{{number}}/g, `${i + 1}`)}${ext}`;
@ -601,7 +598,7 @@ async function onPaste(ev: ClipboardEvent) {
return; return;
} }
quoteId.value = paste.substring(url.length).match(/^\/notes\/(.+?)\/?$/)[1]; quoteId.value = paste.substring(url.length).match(/^\/notes\/(.+?)\/?$/)?.[1] ?? null;
}); });
} }
} }
@ -632,26 +629,26 @@ function onDragover(ev) {
} }
} }
function onDragenter(ev) { function onDragenter() {
draghover.value = true; draghover.value = true;
} }
function onDragleave(ev) { function onDragleave() {
draghover.value = false; draghover.value = false;
} }
function onDrop(ev): void { function onDrop(ev: DragEvent): void {
draghover.value = false; draghover.value = false;
// //
if (ev.dataTransfer.files.length > 0) { if (ev.dataTransfer && ev.dataTransfer.files.length > 0) {
ev.preventDefault(); ev.preventDefault();
for (const x of Array.from(ev.dataTransfer.files)) upload(x); for (const x of Array.from(ev.dataTransfer.files)) upload(x);
return; return;
} }
//#region //#region
const driveFile = ev.dataTransfer.getData(_DATA_TRANSFER_DRIVE_FILE_); const driveFile = ev.dataTransfer?.getData(_DATA_TRANSFER_DRIVE_FILE_);
if (driveFile != null && driveFile !== '') { if (driveFile != null && driveFile !== '') {
const file = JSON.parse(driveFile); const file = JSON.parse(driveFile);
files.value.push(file); files.value.push(file);
@ -699,11 +696,14 @@ async function post(ev?: MouseEvent) {
} }
if (ev) { if (ev) {
const el = ev.currentTarget ?? ev.target; const el = (ev.currentTarget ?? ev.target) as HTMLElement | null;
const rect = el.getBoundingClientRect();
const x = rect.left + (el.offsetWidth / 2); if (el) {
const y = rect.top + (el.offsetHeight / 2); const rect = el.getBoundingClientRect();
os.popup(MkRippleEffect, { x, y }, {}, 'end'); const x = rect.left + (el.offsetWidth / 2);
const y = rect.top + (el.offsetHeight / 2);
os.popup(MkRippleEffect, { x, y }, {}, 'end');
}
} }
if (props.mock) return; if (props.mock) return;
@ -772,18 +772,18 @@ async function post(ev?: MouseEvent) {
if (notePostInterruptors.length > 0) { if (notePostInterruptors.length > 0) {
for (const interruptor of notePostInterruptors) { for (const interruptor of notePostInterruptors) {
try { try {
postData = await interruptor.handler(deepClone(postData)); postData = await interruptor.handler(deepClone(postData)) as typeof postData;
} catch (err) { } catch (err) {
console.error(err); console.error(err);
} }
} }
} }
let token = undefined; let token: string | undefined = undefined;
if (postAccount.value) { if (postAccount.value) {
const storedAccounts = await getAccounts(); const storedAccounts = await getAccounts();
token = storedAccounts.find(x => x.id === postAccount.value.id)?.token; token = storedAccounts.find(x => x.id === postAccount.value?.id)?.token;
} }
posting.value = true; posting.value = true;
@ -797,7 +797,7 @@ async function post(ev?: MouseEvent) {
deleteDraft(); deleteDraft();
emit('posted'); emit('posted');
if (postData.text && postData.text !== '') { if (postData.text && postData.text !== '') {
const hashtags_ = mfm.parse(postData.text).filter(x => x.type === 'hashtag').map(x => x.props.hashtag); const hashtags_ = mfm.parse(postData.text).map(x => x.type === 'hashtag' && x.props.hashtag).filter(x => x) as string[];
const history = JSON.parse(miLocalStorage.getItem('hashtags') ?? '[]') as string[]; const history = JSON.parse(miLocalStorage.getItem('hashtags') ?? '[]') as string[];
miLocalStorage.setItem('hashtags', JSON.stringify(unique(hashtags_.concat(history)))); miLocalStorage.setItem('hashtags', JSON.stringify(unique(hashtags_.concat(history))));
} }
@ -867,9 +867,10 @@ function insertMention() {
async function insertEmoji(ev: MouseEvent) { async function insertEmoji(ev: MouseEvent) {
textAreaReadOnly.value = true; textAreaReadOnly.value = true;
const target = ev.currentTarget ?? ev.target;
if (target == null) return;
emojiPicker.show( emojiPicker.show(
ev.currentTarget ?? ev.target, target as HTMLElement,
emoji => { emoji => {
insertTextAtCursor(textareaEl.value, emoji); insertTextAtCursor(textareaEl.value, emoji);
}, },
@ -881,6 +882,7 @@ async function insertEmoji(ev: MouseEvent) {
} }
async function insertMfmFunction(ev: MouseEvent) { async function insertMfmFunction(ev: MouseEvent) {
if (textareaEl.value == null) return;
mfmFunctionPicker( mfmFunctionPicker(
ev.currentTarget ?? ev.target, ev.currentTarget ?? ev.target,
textareaEl.value, textareaEl.value,
@ -888,14 +890,15 @@ async function insertMfmFunction(ev: MouseEvent) {
); );
} }
function showActions(ev) { function showActions(ev: MouseEvent) {
os.popupMenu(postFormActions.map(action => ({ os.popupMenu(postFormActions.map(action => ({
text: action.title, text: action.title,
action: () => { action: () => {
action.handler({ action.handler({
text: text.value, text: text.value,
cw: cw.value, cw: cw.value,
}, (key, value) => { }, (key, value: any) => {
if (typeof key !== 'string') return;
if (key === 'text') { text.value = value; } if (key === 'text') { text.value = value; }
if (key === 'cw') { useCw.value = value !== null; cw.value = value; } if (key === 'cw') { useCw.value = value !== null; cw.value = value; }
}); });
@ -932,9 +935,9 @@ onMounted(() => {
} }
// TODO: detach when unmount // TODO: detach when unmount
new Autocomplete(textareaEl.value, text); if (textareaEl.value) new Autocomplete(textareaEl.value, text);
new Autocomplete(cwInputEl.value, cw); if (cwInputEl.value) new Autocomplete(cwInputEl.value, cw);
new Autocomplete(hashtagsInputEl.value, hashtags); if (hashtagsInputEl.value) new Autocomplete(hashtagsInputEl.value, hashtags);
nextTick(() => { nextTick(() => {
// 稿 // 稿
@ -957,19 +960,19 @@ onMounted(() => {
if (props.initialNote) { if (props.initialNote) {
const init = props.initialNote; const init = props.initialNote;
text.value = init.text ? init.text : ''; text.value = init.text ? init.text : '';
files.value = init.files; files.value = init.files ?? [];
cw.value = init.cw; cw.value = init.cw ?? null;
useCw.value = init.cw != null; useCw.value = init.cw != null;
if (init.poll) { if (init.poll) {
poll.value = { poll.value = {
choices: init.poll.choices.map(x => x.text), choices: init.poll.choices.map(x => x.text),
multiple: init.poll.multiple, multiple: init.poll.multiple,
expiresAt: init.poll.expiresAt, expiresAt: init.poll.expiresAt ? (new Date(init.poll.expiresAt)).getTime() : null,
expiredAfter: init.poll.expiredAfter, expiredAfter: null,
}; };
} }
visibility.value = init.visibility; visibility.value = init.visibility;
localOnly.value = init.localOnly; localOnly.value = init.localOnly ?? false;
quoteId.value = init.renote ? init.renote.id : null; quoteId.value = init.renote ? init.renote.id : null;
} }

View file

@ -93,7 +93,7 @@ async function rename(file) {
const { canceled, result } = await os.inputText({ const { canceled, result } = await os.inputText({
title: i18n.ts.enterFileName, title: i18n.ts.enterFileName,
default: file.name, default: file.name,
allowEmpty: false, minLength: 1,
}); });
if (canceled) return; if (canceled) return;
misskeyApi('drive/files/update', { misskeyApi('drive/files/update', {

View file

@ -4,8 +4,8 @@ SPDX-License-Identifier: AGPL-3.0-only
--> -->
<template> <template>
<MkModal ref="modal" :preferType="'dialog'" @click="modal.close()" @closed="onModalClosed()"> <MkModal ref="modal" :preferType="'dialog'" @click="modal?.close()" @closed="onModalClosed()">
<MkPostForm ref="form" :class="$style.form" v-bind="props" autofocus freezeAfterPosted @posted="onPosted" @cancel="modal.close()" @esc="modal.close()"/> <MkPostForm ref="form" :class="$style.form" v-bind="props" autofocus freezeAfterPosted @posted="onPosted" @cancel="modal?.close()" @esc="modal?.close()"/>
</MkModal> </MkModal>
</template> </template>
@ -20,13 +20,13 @@ const props = defineProps<{
renote?: Misskey.entities.Note; renote?: Misskey.entities.Note;
channel?: any; // TODO channel?: any; // TODO
mention?: Misskey.entities.User; mention?: Misskey.entities.User;
specified?: Misskey.entities.User; specified?: Misskey.entities.UserDetailed;
initialText?: string; initialText?: string;
initialCw?: string; initialCw?: string;
initialVisibility?: typeof Misskey.noteVisibilities; initialVisibility?: (typeof Misskey.noteVisibilities)[number];
initialFiles?: Misskey.entities.DriveFile[]; initialFiles?: Misskey.entities.DriveFile[];
initialLocalOnly?: boolean; initialLocalOnly?: boolean;
initialVisibleUsers?: Misskey.entities.User[]; initialVisibleUsers?: Misskey.entities.UserDetailed[];
initialNote?: Misskey.entities.Note; initialNote?: Misskey.entities.Note;
instant?: boolean; instant?: boolean;
fixed?: boolean; fixed?: boolean;
@ -41,7 +41,7 @@ const modal = shallowRef<InstanceType<typeof MkModal>>();
const form = shallowRef<InstanceType<typeof MkPostForm>>(); const form = shallowRef<InstanceType<typeof MkPostForm>>();
function onPosted() { function onPosted() {
modal.value.close({ modal.value?.close({
useSendAnimation: true, useSendAnimation: true,
}); });
} }

View file

@ -126,7 +126,7 @@ async function unsubscribe() {
} }
function encode(buffer: ArrayBuffer | null) { function encode(buffer: ArrayBuffer | null) {
return btoa(String.fromCharCode.apply(null, new Uint8Array(buffer))); return btoa(String.fromCharCode.apply(null, buffer ? new Uint8Array(buffer) as any : []));
} }
/** /**

View file

@ -38,7 +38,7 @@ export default defineComponent({
h('div', { h('div', {
class: 'body', class: 'body',
}, options.map(option => h(MkRadio, { }, options.map(option => h(MkRadio, {
key: option.key, key: option.key as string,
value: option.props?.value, value: option.props?.value,
modelValue: value.value, modelValue: value.value,
'onUpdate:modelValue': _v => value.value = _v, 'onUpdate:modelValue': _v => value.value = _v,

View file

@ -86,7 +86,7 @@ onMounted(() => {
ro = new ResizeObserver((entries, observer) => { ro = new ResizeObserver((entries, observer) => {
calcThumbPosition(); calcThumbPosition();
}); });
ro.observe(containerEl.value); if (containerEl.value) ro.observe(containerEl.value);
}); });
onUnmounted(() => { onUnmounted(() => {
@ -122,7 +122,7 @@ const onMousedown = (ev: MouseEvent | TouchEvent) => {
const onDrag = (ev: MouseEvent | TouchEvent) => { const onDrag = (ev: MouseEvent | TouchEvent) => {
ev.preventDefault(); ev.preventDefault();
const containerRect = containerEl.value!.getBoundingClientRect(); const containerRect = containerEl.value!.getBoundingClientRect();
const pointerX = ev.touches && ev.touches.length > 0 ? ev.touches[0].clientX : ev.clientX; const pointerX = 'touches' in ev && ev.touches.length > 0 ? ev.touches[0].clientX : 'clientX' in ev ? ev.clientX : 0;
const pointerPositionOnContainer = pointerX - (containerRect.left + (thumbWidth / 2)); const pointerPositionOnContainer = pointerX - (containerRect.left + (thumbWidth / 2));
rawValue.value = Math.min(1, Math.max(0, pointerPositionOnContainer / (containerEl.value!.offsetWidth - thumbWidth))); rawValue.value = Math.min(1, Math.max(0, pointerPositionOnContainer / (containerEl.value!.offsetWidth - thumbWidth)));

View file

@ -20,9 +20,9 @@ SPDX-License-Identifier: AGPL-3.0-only
<script lang="ts" setup> <script lang="ts" setup>
import { computed, inject, onMounted, shallowRef, watch } from 'vue'; import { computed, inject, onMounted, shallowRef, watch } from 'vue';
import * as Misskey from 'misskey-js'; import * as Misskey from 'misskey-js';
import MkCustomEmojiDetailedDialog from './MkCustomEmojiDetailedDialog.vue';
import XDetails from '@/components/MkReactionsViewer.details.vue'; import XDetails from '@/components/MkReactionsViewer.details.vue';
import MkReactionIcon from '@/components/MkReactionIcon.vue'; import MkReactionIcon from '@/components/MkReactionIcon.vue';
import MkCustomEmojiDetailedDialog from './MkCustomEmojiDetailedDialog.vue';
import * as os from '@/os.js'; import * as os from '@/os.js';
import { misskeyApi, misskeyApiGet } from '@/scripts/misskey-api.js'; import { misskeyApi, misskeyApiGet } from '@/scripts/misskey-api.js';
import { useTooltip } from '@/scripts/use-tooltip.js'; import { useTooltip } from '@/scripts/use-tooltip.js';
@ -102,7 +102,7 @@ async function toggleReaction() {
async function menu(ev) { async function menu(ev) {
if (!canToggle.value) return; if (!canToggle.value) return;
if (!props.reaction.includes(":")) return; if (!props.reaction.includes(':')) return;
os.popupMenu([{ os.popupMenu([{
text: i18n.ts.info, text: i18n.ts.info,
icon: 'ti ti-info-circle', icon: 'ti ti-info-circle',
@ -117,8 +117,7 @@ async function menu(ev) {
} }
function anime() { function anime() {
if (document.hidden) return; if (document.hidden || !defaultStore.state.animation || buttonEl.value == null) return;
if (!defaultStore.state.animation) return;
const rect = buttonEl.value.getBoundingClientRect(); const rect = buttonEl.value.getBoundingClientRect();
const x = rect.left + 16; const x = rect.left + 16;

View file

@ -23,10 +23,9 @@ import { initChart } from '@/scripts/init-chart.js';
initChart(); initChart();
const rootEl = shallowRef<HTMLDivElement>(null); const rootEl = shallowRef<HTMLDivElement | null>(null);
const chartEl = shallowRef<HTMLCanvasElement>(null); const chartEl = shallowRef<HTMLCanvasElement | null>(null);
const now = new Date(); let chartInstance: Chart | null = null;
let chartInstance: Chart = null;
const fetching = ref(true); const fetching = ref(true);
const { handler: externalTooltipHandler } = useChartTooltip({ const { handler: externalTooltipHandler } = useChartTooltip({
@ -34,6 +33,7 @@ const { handler: externalTooltipHandler } = useChartTooltip({
}); });
async function renderChart() { async function renderChart() {
if (rootEl.value == null) return;
if (chartInstance) { if (chartInstance) {
chartInstance.destroy(); chartInstance.destroy();
} }
@ -47,7 +47,12 @@ async function renderChart() {
raw = raw.slice(0, maxDays + 1); raw = raw.slice(0, maxDays + 1);
const data = []; const data: {
x: number;
y: string;
v: number;
}[] = [];
for (const record of raw) { for (const record of raw) {
data.push({ data.push({
x: 0, x: 0,
@ -83,19 +88,20 @@ async function renderChart() {
const marginEachCell = 12; const marginEachCell = 12;
if (chartEl.value == null) return;
chartInstance = new Chart(chartEl.value, { chartInstance = new Chart(chartEl.value, {
type: 'matrix', type: 'matrix',
data: { data: {
datasets: [{ datasets: [{
label: 'Active', label: 'Active',
data: data, data: data as any,
pointRadius: 0,
borderWidth: 0, borderWidth: 0,
borderJoinStyle: 'round',
borderRadius: 3, borderRadius: 3,
backgroundColor(c) { backgroundColor(c) {
const value = c.dataset.data[c.dataIndex].v; const v = c.dataset.data[c.dataIndex] as unknown as typeof data[0];
const m = max(c.dataset.data[c.dataIndex].y); const value = v.v;
const m = max(v.y);
if (m === 0) { if (m === 0) {
return alpha(color, 0); return alpha(color, 0);
} else { } else {
@ -103,7 +109,6 @@ async function renderChart() {
return alpha(color, a); return alpha(color, a);
} }
}, },
fill: true,
width(c) { width(c) {
const a = c.chart.chartArea ?? {}; const a = c.chart.chartArea ?? {};
return (a.right - a.left) / maxDays - marginEachCell; return (a.right - a.left) / maxDays - marginEachCell;
@ -146,7 +151,6 @@ async function renderChart() {
}, },
y: { y: {
type: 'time', type: 'time',
min: new Date(new Date().getFullYear(), new Date().getMonth(), new Date().getDate() - maxDays),
offset: true, offset: true,
reverse: true, reverse: true,
position: 'left', position: 'left',
@ -179,7 +183,7 @@ async function renderChart() {
return getYYYYMMDD(new Date(new Date(v.y).getTime() + (v.x * 86400000))); return getYYYYMMDD(new Date(new Date(v.y).getTime() + (v.x * 86400000)));
}, },
label(context) { label(context) {
const v = context.dataset.data[context.dataIndex]; const v = context.dataset.data[context.dataIndex] as unknown as typeof data[0];
const m = max(v.y); const m = max(v.y);
if (m === 0) { if (m === 0) {
return [`Active: ${v.v} (-%)`]; return [`Active: ${v.v} (-%)`];

View file

@ -20,11 +20,11 @@ import { misskeyApi } from '@/scripts/misskey-api.js';
initChart(); initChart();
const chartEl = shallowRef<HTMLCanvasElement>(null); const chartEl = shallowRef<HTMLCanvasElement | null>(null);
const { handler: externalTooltipHandler } = useChartTooltip(); const { handler: externalTooltipHandler } = useChartTooltip();
let chartInstance: Chart; let chartInstance: Chart | null = null;
const getYYYYMMDD = (date: Date) => { const getYYYYMMDD = (date: Date) => {
const y = date.getFullYear().toString().padStart(2, '0'); const y = date.getFullYear().toString().padStart(2, '0');
@ -47,6 +47,8 @@ onMounted(async () => {
const accent = tinycolor(getComputedStyle(document.documentElement).getPropertyValue('--accent')); const accent = tinycolor(getComputedStyle(document.documentElement).getPropertyValue('--accent'));
const color = accent.toHex(); const color = accent.toHex();
if (chartEl.value == null) return;
chartInstance = new Chart(chartEl.value, { chartInstance = new Chart(chartEl.value, {
type: 'line', type: 'line',
data: { data: {
@ -67,7 +69,7 @@ onMounted(async () => {
x: (i + 1).toString(), x: (i + 1).toString(),
y: (v / record.users) * 100, y: (v / record.users) * 100,
d: getYYYYMMDD(new Date(record.createdAt)), d: getYYYYMMDD(new Date(record.createdAt)),
}))], }))] as any,
})), })),
}, },
options: { options: {
@ -109,11 +111,11 @@ onMounted(async () => {
enabled: false, enabled: false,
callbacks: { callbacks: {
title(context) { title(context) {
const v = context[0].dataset.data[context[0].dataIndex]; const v = context[0].dataset.data[context[0].dataIndex] as unknown as { x: string, y: number, d: string };
return `${v.x} days later`; return `${v.x} days later`;
}, },
label(context) { label(context) {
const v = context.dataset.data[context.dataIndex]; const v = context.dataset.data[context.dataIndex] as unknown as { x: string, y: number, d: string };
const p = Math.round(v.y) + '%'; const p = Math.round(v.y) + '%';
return `${v.d} ${p}`; return `${v.d} ${p}`;
}, },

View file

@ -32,11 +32,12 @@ SPDX-License-Identifier: AGPL-3.0-only
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import { onMounted, nextTick, ref, watch, computed, toRefs, VNode, useSlots } from 'vue'; import { onMounted, nextTick, ref, watch, computed, toRefs, VNode, useSlots, VNodeChild } from 'vue';
import MkButton from '@/components/MkButton.vue'; import MkButton from '@/components/MkButton.vue';
import * as os from '@/os.js'; import * as os from '@/os.js';
import { useInterval } from '@/scripts/use-interval.js'; import { useInterval } from '@/scripts/use-interval.js';
import { i18n } from '@/i18n.js'; import { i18n } from '@/i18n.js';
import { MenuItem } from '@/types/menu.js';
const props = defineProps<{ const props = defineProps<{
modelValue: string | null; modelValue: string | null;
@ -52,7 +53,7 @@ const props = defineProps<{
}>(); }>();
const emit = defineEmits<{ const emit = defineEmits<{
(ev: 'changeByUser'): void; (ev: 'changeByUser', value: string | null): void;
(ev: 'update:modelValue', value: string | null): void; (ev: 'update:modelValue', value: string | null): void;
}>(); }>();
@ -74,7 +75,7 @@ const height =
props.large ? 39 : props.large ? 39 :
36; 36;
const focus = () => inputEl.value.focus(); const focus = () => inputEl.value?.focus();
const onInput = (ev) => { const onInput = (ev) => {
changed.value = true; changed.value = true;
}; };
@ -88,17 +89,19 @@ watch(modelValue, newValue => {
v.value = newValue; v.value = newValue;
}); });
watch(v, newValue => { watch(v, () => {
if (!props.manualSave) { if (!props.manualSave) {
updated(); updated();
} }
invalid.value = inputEl.value.validity.badInput; invalid.value = inputEl.value?.validity.badInput ?? true;
}); });
// //
// 0 // 0
useInterval(() => { useInterval(() => {
if (inputEl.value == null) return;
if (prefixEl.value) { if (prefixEl.value) {
if (prefixEl.value.offsetWidth) { if (prefixEl.value.offsetWidth) {
inputEl.value.style.paddingLeft = prefixEl.value.offsetWidth + 'px'; inputEl.value.style.paddingLeft = prefixEl.value.offsetWidth + 'px';
@ -122,36 +125,37 @@ onMounted(() => {
}); });
}); });
function show(ev: MouseEvent) { function show() {
focused.value = true; focused.value = true;
opening.value = true; opening.value = true;
const menu = []; const menu: MenuItem[] = [];
let options = slots.default!(); let options = slots.default!();
const pushOption = (option: VNode) => { const pushOption = (option: VNode) => {
menu.push({ menu.push({
text: option.children, text: option.children as string,
active: computed(() => v.value === option.props.value), active: computed(() => v.value === option.props?.value),
action: () => { action: () => {
v.value = option.props.value; v.value = option.props?.value;
emit('changeByUser', v.value); emit('changeByUser', v.value);
}, },
}); });
}; };
const scanOptions = (options: VNode[]) => { const scanOptions = (options: VNodeChild[]) => {
for (const vnode of options) { for (const vnode of options) {
if (typeof vnode !== 'object' || vnode === null || Array.isArray(vnode)) continue;
if (vnode.type === 'optgroup') { if (vnode.type === 'optgroup') {
const optgroup = vnode; const optgroup = vnode;
menu.push({ menu.push({
type: 'label', type: 'label',
text: optgroup.props.label, text: optgroup.props?.label,
}); });
scanOptions(optgroup.children); if (Array.isArray(optgroup.children)) scanOptions(optgroup.children);
} else if (Array.isArray(vnode.children)) { // } else if (Array.isArray(vnode.children)) { //
const fragment = vnode; const fragment = vnode;
scanOptions(fragment.children); if (Array.isArray(fragment.children)) scanOptions(fragment.children);
} else if (vnode.props == null) { // v-if false } else if (vnode.props == null) { // v-if false
// nop? // nop?
} else { } else {
@ -164,7 +168,7 @@ function show(ev: MouseEvent) {
scanOptions(options); scanOptions(options);
os.popupMenu(menu, container.value, { os.popupMenu(menu, container.value, {
width: container.value.offsetWidth, width: container.value?.offsetWidth,
onClosing: () => { onClosing: () => {
opening.value = false; opening.value = false;
}, },

View file

@ -112,6 +112,7 @@ function onLogin(res: any): Promise<void> | void {
} }
async function queryKey(): Promise<void> { async function queryKey(): Promise<void> {
if (credentialRequest.value == null) return;
queryingKey.value = true; queryingKey.value = true;
await webAuthnRequest(credentialRequest.value) await webAuthnRequest(credentialRequest.value)
.catch(() => { .catch(() => {

View file

@ -80,6 +80,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<script lang="ts" setup> <script lang="ts" setup>
import { ref, computed } from 'vue'; import { ref, computed } from 'vue';
import { toUnicode } from 'punycode/'; import { toUnicode } from 'punycode/';
import * as Misskey from 'misskey-js';
import MkButton from './MkButton.vue'; import MkButton from './MkButton.vue';
import MkInput from './MkInput.vue'; import MkInput from './MkInput.vue';
import MkCaptcha, { type Captcha } from '@/components/MkCaptcha.vue'; import MkCaptcha, { type Captcha } from '@/components/MkCaptcha.vue';
@ -97,7 +98,7 @@ const props = withDefaults(defineProps<{
}); });
const emit = defineEmits<{ const emit = defineEmits<{
(ev: 'signup', user: Record<string, any>): void; (ev: 'signup', user: Misskey.entities.SigninResponse): void;
(ev: 'signupEmailPending'): void; (ev: 'signupEmailPending'): void;
}>(); }>();

View file

@ -34,8 +34,8 @@ SPDX-License-Identifier: AGPL-3.0-only
<template #label>{{ tosPrivacyPolicyLabel }}</template> <template #label>{{ tosPrivacyPolicyLabel }}</template>
<template #suffix><i v-if="agreeTosAndPrivacyPolicy" class="ti ti-check" style="color: var(--success)"></i></template> <template #suffix><i v-if="agreeTosAndPrivacyPolicy" class="ti ti-check" style="color: var(--success)"></i></template>
<div class="_gaps_s"> <div class="_gaps_s">
<div v-if="availableTos"><a :href="instance.tosUrl" class="_link" target="_blank">{{ i18n.ts.termsOfService }} <i class="ti ti-external-link"></i></a></div> <div v-if="availableTos"><a :href="instance.tosUrl ?? undefined" class="_link" target="_blank">{{ i18n.ts.termsOfService }} <i class="ti ti-external-link"></i></a></div>
<div v-if="availablePrivacyPolicy"><a :href="instance.privacyPolicyUrl" class="_link" target="_blank">{{ i18n.ts.privacyPolicy }} <i class="ti ti-external-link"></i></a></div> <div v-if="availablePrivacyPolicy"><a :href="instance.privacyPolicyUrl ?? undefined" class="_link" target="_blank">{{ i18n.ts.privacyPolicy }} <i class="ti ti-external-link"></i></a></div>
</div> </div>
<MkSwitch :modelValue="agreeTosAndPrivacyPolicy" style="margin-top: 16px;" @update:modelValue="updateAgreeTosAndPrivacyPolicy">{{ i18n.ts.agree }}</MkSwitch> <MkSwitch :modelValue="agreeTosAndPrivacyPolicy" style="margin-top: 16px;" @update:modelValue="updateAgreeTosAndPrivacyPolicy">{{ i18n.ts.agree }}</MkSwitch>
@ -96,7 +96,7 @@ const tosPrivacyPolicyLabel = computed(() => {
} else if (availablePrivacyPolicy) { } else if (availablePrivacyPolicy) {
return i18n.ts.privacyPolicy; return i18n.ts.privacyPolicy;
} else { } else {
return ""; return '';
} }
}); });

View file

@ -8,7 +8,7 @@ SPDX-License-Identifier: AGPL-3.0-only
ref="dialog" ref="dialog"
:width="500" :width="500"
:height="600" :height="600"
@close="dialog.close()" @close="dialog?.close()"
@closed="$emit('closed')" @closed="$emit('closed')"
> >
<template #header>{{ i18n.ts.signup }}</template> <template #header>{{ i18n.ts.signup }}</template>
@ -22,7 +22,7 @@ SPDX-License-Identifier: AGPL-3.0-only
:leaveToClass="$style.transition_x_leaveTo" :leaveToClass="$style.transition_x_leaveTo"
> >
<template v-if="!isAcceptedServerRule"> <template v-if="!isAcceptedServerRule">
<XServerRules @done="isAcceptedServerRule = true" @cancel="dialog.close()"/> <XServerRules @done="isAcceptedServerRule = true" @cancel="dialog?.close()"/>
</template> </template>
<template v-else> <template v-else>
<XSignup :autoSet="autoSet" @signup="onSignup" @signupEmailPending="onSignupEmailPending"/> <XSignup :autoSet="autoSet" @signup="onSignup" @signupEmailPending="onSignupEmailPending"/>
@ -34,7 +34,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<script lang="ts" setup> <script lang="ts" setup>
import { shallowRef, ref } from 'vue'; import { shallowRef, ref } from 'vue';
import * as Misskey from 'misskey-js';
import XSignup from '@/components/MkSignupDialog.form.vue'; import XSignup from '@/components/MkSignupDialog.form.vue';
import XServerRules from '@/components/MkSignupDialog.rules.vue'; import XServerRules from '@/components/MkSignupDialog.rules.vue';
import MkModalWindow from '@/components/MkModalWindow.vue'; import MkModalWindow from '@/components/MkModalWindow.vue';
@ -47,7 +47,7 @@ const props = withDefaults(defineProps<{
}); });
const emit = defineEmits<{ const emit = defineEmits<{
(ev: 'done'): void; (ev: 'done', res: Misskey.entities.SigninResponse): void;
(ev: 'closed'): void; (ev: 'closed'): void;
}>(); }>();
@ -55,13 +55,13 @@ const dialog = shallowRef<InstanceType<typeof MkModalWindow>>();
const isAcceptedServerRule = ref(false); const isAcceptedServerRule = ref(false);
function onSignup(res) { function onSignup(res: Misskey.entities.SigninResponse) {
emit('done', res); emit('done', res);
dialog.value.close(); dialog.value?.close();
} }
function onSignupEmailPending() { function onSignupEmailPending() {
dialog.value.close(); dialog.value?.close();
} }
</script> </script>

View file

@ -89,10 +89,11 @@ let ro: ResizeObserver | undefined;
onMounted(() => { onMounted(() => {
ro = new ResizeObserver((entries, observer) => { ro = new ResizeObserver((entries, observer) => {
width.value = el.value?.offsetWidth + 64; if (el.value == null) return;
height.value = el.value?.offsetHeight + 64; width.value = el.value.offsetWidth + 64;
height.value = el.value.offsetHeight + 64;
}); });
ro.observe(el.value); if (el.value) ro.observe(el.value);
const add = () => { const add = () => {
if (stop) return; if (stop) return;
const x = (Math.random() * (width.value - 64)); const x = (Math.random() * (width.value - 64));

View file

@ -7,18 +7,18 @@ SPDX-License-Identifier: AGPL-3.0-only
<div :class="[$style.root, { [$style.collapsed]: collapsed }]"> <div :class="[$style.root, { [$style.collapsed]: collapsed }]">
<div> <div>
<span v-if="note.isHidden" style="opacity: 0.5">({{ i18n.ts.private }})</span> <span v-if="note.isHidden" style="opacity: 0.5">({{ i18n.ts.private }})</span>
<span v-if="note.deletedAt" style="opacity: 0.5">({{ i18n.ts.deleted }})</span> <span v-if="note.deletedAt" style="opacity: 0.5">({{ i18n.ts.deletedNote }})</span>
<MkA v-if="note.replyId" :class="$style.reply" :to="`/notes/${note.replyId}`"><i class="ti ti-arrow-back-up"></i></MkA> <MkA v-if="note.replyId" :class="$style.reply" :to="`/notes/${note.replyId}`"><i class="ti ti-arrow-back-up"></i></MkA>
<Mfm v-if="note.text" :text="note.text" :author="note.user" :nyaize="'respect'" :emojiUrls="note.emojis"/> <Mfm v-if="note.text" :text="note.text" :author="note.user" :nyaize="'respect'" :emojiUrls="note.emojis"/>
<MkA v-if="note.renoteId" :class="$style.rp" :to="`/notes/${note.renoteId}`">RN: ...</MkA> <MkA v-if="note.renoteId" :class="$style.rp" :to="`/notes/${note.renoteId}`">RN: ...</MkA>
</div> </div>
<details v-if="note.files.length > 0"> <details v-if="note.files && note.files.length > 0">
<summary>({{ i18n.tsx.withNFiles({ n: note.files.length }) }})</summary> <summary>({{ i18n.tsx.withNFiles({ n: note.files.length }) }})</summary>
<MkMediaList :mediaList="note.files"/> <MkMediaList :mediaList="note.files"/>
</details> </details>
<details v-if="note.poll"> <details v-if="note.poll">
<summary>{{ i18n.ts.poll }}</summary> <summary>{{ i18n.ts.poll }}</summary>
<MkPoll :note="note"/> <MkPoll :noteId="note.id" :poll="note.poll"/>
</details> </details>
<button v-if="isLong && collapsed" :class="$style.fade" class="_button" @click="collapsed = false"> <button v-if="isLong && collapsed" :class="$style.fade" class="_button" @click="collapsed = false">
<span :class="$style.fadeLabel">{{ i18n.ts.showMore }}</span> <span :class="$style.fadeLabel">{{ i18n.ts.showMore }}</span>

View file

@ -24,7 +24,7 @@ import { i18n } from '@/i18n.js';
const props = withDefaults(defineProps<{ const props = withDefaults(defineProps<{
checked: boolean | Ref<boolean>; checked: boolean | Ref<boolean>;
disabled?: boolean; disabled?: boolean | Ref<boolean>;
}>(), { }>(), {
disabled: false, disabled: false,
}); });

View file

@ -13,18 +13,18 @@ export default defineComponent({
}, },
}, },
setup(props, { emit, slots }) { setup(props, { emit, slots }) {
const options = slots.default(); const options = slots.default?.() ?? [];
return () => h('div', { return () => h('div', {
class: 'pxhvhrfw', class: 'pxhvhrfw',
}, options.map(option => withDirectives(h('button', { }, options.map(option => withDirectives(h('button', {
class: ['_button', { active: props.modelValue === option.props.value }], class: ['_button', { active: props.modelValue === option.props?.value }],
key: option.key, key: option.key as string,
disabled: props.modelValue === option.props.value, disabled: props.modelValue === option.props?.value,
onClick: () => { onClick: () => {
emit('update:modelValue', option.props.value); emit('update:modelValue', option.props?.value);
}, },
}, option.children), [ }, option.children ?? []), [
[resolveDirective('click-anime')], [resolveDirective('click-anime')],
]))); ])));
}, },

View file

@ -52,7 +52,7 @@ watch(available, () => {
}); });
onMounted(() => { onMounted(() => {
width.value = rootEl.value.offsetWidth; if (rootEl.value) width.value = rootEl.value.offsetWidth;
if (loaded) { if (loaded) {
available.value = true; available.value = true;

View file

@ -17,7 +17,7 @@ SPDX-License-Identifier: AGPL-3.0-only
:readonly="readonly" :readonly="readonly"
:placeholder="placeholder" :placeholder="placeholder"
:pattern="pattern" :pattern="pattern"
:autocomplete="props.autocomplete" :autocomplete="autocomplete"
:spellcheck="spellcheck" :spellcheck="spellcheck"
@focus="focused = true" @focus="focused = true"
@blur="focused = false" @blur="focused = false"
@ -76,9 +76,9 @@ const invalid = ref(false);
const filled = computed(() => v.value !== '' && v.value != null); const filled = computed(() => v.value !== '' && v.value != null);
const inputEl = shallowRef<HTMLTextAreaElement>(); const inputEl = shallowRef<HTMLTextAreaElement>();
const preview = ref(false); const preview = ref(false);
let autocomplete: Autocomplete; let autocompleteWorker: Autocomplete | null = null;
const focus = () => inputEl.value.focus(); const focus = () => inputEl.value?.focus();
const onInput = (ev) => { const onInput = (ev) => {
changed.value = true; changed.value = true;
emit('change', ev); emit('change', ev);
@ -111,10 +111,10 @@ const updated = () => {
const debouncedUpdated = debounce(1000, updated); const debouncedUpdated = debounce(1000, updated);
watch(modelValue, newValue => { watch(modelValue, newValue => {
v.value = newValue; v.value = newValue ?? '';
}); });
watch(v, newValue => { watch(v, () => {
if (!props.manualSave) { if (!props.manualSave) {
if (props.debounce) { if (props.debounce) {
debouncedUpdated(); debouncedUpdated();
@ -123,7 +123,7 @@ watch(v, newValue => {
} }
} }
invalid.value = inputEl.value.validity.badInput; invalid.value = inputEl.value?.validity.badInput ?? true;
}); });
onMounted(() => { onMounted(() => {
@ -133,14 +133,14 @@ onMounted(() => {
} }
}); });
if (props.mfmAutocomplete) { if (props.mfmAutocomplete && inputEl.value) {
autocomplete = new Autocomplete(inputEl.value, v, props.mfmAutocomplete === true ? null : props.mfmAutocomplete); autocompleteWorker = new Autocomplete(inputEl.value, v, props.mfmAutocomplete === true ? undefined : props.mfmAutocomplete);
} }
}); });
onUnmounted(() => { onUnmounted(() => {
if (autocomplete) { if (autocompleteWorker) {
autocomplete.detach(); autocompleteWorker.detach();
} }
}); });
</script> </script>

View file

@ -18,6 +18,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<script lang="ts" setup> <script lang="ts" setup>
import { computed, watch, onUnmounted, provide, ref, shallowRef } from 'vue'; import { computed, watch, onUnmounted, provide, ref, shallowRef } from 'vue';
import Misskey from 'misskey-js';
import { Connection } from 'misskey-js/built/streaming.js'; import { Connection } from 'misskey-js/built/streaming.js';
import MkNotes from '@/components/MkNotes.vue'; import MkNotes from '@/components/MkNotes.vue';
import MkPullToRefresh from '@/components/MkPullToRefresh.vue'; import MkPullToRefresh from '@/components/MkPullToRefresh.vue';
@ -29,7 +30,7 @@ import { defaultStore } from '@/store.js';
import { Paging } from '@/components/MkPagination.vue'; import { Paging } from '@/components/MkPagination.vue';
const props = withDefaults(defineProps<{ const props = withDefaults(defineProps<{
src: string; src: 'home' | 'local' | 'social' | 'global' | 'mentions' | 'directs' | 'list' | 'antenna' | 'channel' | 'role';
list?: string; list?: string;
antenna?: string; antenna?: string;
channel?: string; channel?: string;
@ -94,6 +95,7 @@ const stream = useStream();
function connectChannel() { function connectChannel() {
if (props.src === 'antenna') { if (props.src === 'antenna') {
if (props.antenna == null) return;
connection = stream.useChannel('antenna', { connection = stream.useChannel('antenna', {
antennaId: props.antenna, antennaId: props.antenna,
}); });
@ -132,21 +134,24 @@ function connectChannel() {
connection = stream.useChannel('main'); connection = stream.useChannel('main');
connection.on('mention', onNote); connection.on('mention', onNote);
} else if (props.src === 'list') { } else if (props.src === 'list') {
if (props.list == null) return;
connection = stream.useChannel('userList', { connection = stream.useChannel('userList', {
withRenotes: props.withRenotes, withRenotes: props.withRenotes,
withFiles: props.onlyFiles ? true : undefined, withFiles: props.onlyFiles ? true : undefined,
listId: props.list, listId: props.list,
}); });
} else if (props.src === 'channel') { } else if (props.src === 'channel') {
if (props.channel == null) return;
connection = stream.useChannel('channel', { connection = stream.useChannel('channel', {
channelId: props.channel, channelId: props.channel,
}); });
} else if (props.src === 'role') { } else if (props.src === 'role') {
if (props.role == null) return;
connection = stream.useChannel('roleTimeline', { connection = stream.useChannel('roleTimeline', {
roleId: props.role, roleId: props.role,
}); });
} }
if (props.src !== 'directs' || props.src !== 'mentions') connection.on('note', prepend); if (props.src !== 'directs' && props.src !== 'mentions') connection.on('note', prepend);
} }
function disconnectChannel() { function disconnectChannel() {
@ -155,7 +160,7 @@ function disconnectChannel() {
} }
function updatePaginationQuery() { function updatePaginationQuery() {
let endpoint: string | null; let endpoint: keyof Misskey.Endpoints | null;
let query: TimelineQueryType | null; let query: TimelineQueryType | null;
if (props.src === 'antenna') { if (props.src === 'antenna') {

View file

@ -55,7 +55,7 @@ const el = shallowRef<HTMLElement>();
const zIndex = os.claimZIndex('high'); const zIndex = os.claimZIndex('high');
function setPosition() { function setPosition() {
if (!el.value || !props.targetElement) return; if (el.value == null) return;
const data = calcPopupPosition(el.value, { const data = calcPopupPosition(el.value, {
anchorElement: props.targetElement, anchorElement: props.targetElement,
direction: props.direction, direction: props.direction,

View file

@ -17,7 +17,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<div v-else-if="phase === 'howToReact'" class="_gaps"> <div v-else-if="phase === 'howToReact'" class="_gaps">
<div style="text-align: center; padding: 0 16px;">{{ i18n.ts._initialTutorial._reaction.description }}</div> <div style="text-align: center; padding: 0 16px;">{{ i18n.ts._initialTutorial._reaction.description }}</div>
<div>{{ i18n.ts._initialTutorial._reaction.letsTryReacting }}</div> <div>{{ i18n.ts._initialTutorial._reaction.letsTryReacting }}</div>
<MkNote :class="$style.exampleNoteRoot" :note="exampleNote" :mock="true" @reaction="addReaction" @removeReaction="removeReaction" @updateReaction="updateReaction"/> <MkNote :class="$style.exampleNoteRoot" :note="exampleNote" :mock="true" @reaction="addReaction" @removeReaction="removeReaction"/>
<div v-if="onceReacted"><b style="color: var(--accent);"><i class="ti ti-check"></i> {{ i18n.ts._initialTutorial.wellDone }}</b> {{ i18n.ts._initialTutorial._reaction.reactNotification }}<br>{{ i18n.ts._initialTutorial._reaction.reactDone }}</div> <div v-if="onceReacted"><b style="color: var(--accent);"><i class="ti ti-check"></i> {{ i18n.ts._initialTutorial.wellDone }}</b> {{ i18n.ts._initialTutorial._reaction.reactNotification }}<br>{{ i18n.ts._initialTutorial._reaction.reactDone }}</div>
</div> </div>
</template> </template>
@ -53,7 +53,7 @@ const exampleNote = reactive<Misskey.entities.Note>({
isBot: false, isBot: false,
isCat: true, isCat: true,
emojis: {}, emojis: {},
onlineStatus: null, onlineStatus: 'unknown',
badgeRoles: [], badgeRoles: [],
}, },
text: 'just setting up my msky', text: 'just setting up my msky',
@ -86,7 +86,6 @@ function doNotification(emoji: string): void {
const notification: Misskey.entities.Notification = { const notification: Misskey.entities.Notification = {
id: Math.random().toString(), id: Math.random().toString(),
createdAt: new Date().toUTCString(), createdAt: new Date().toUTCString(),
isRead: false,
type: 'reaction', type: 'reaction',
reaction: emoji, reaction: emoji,
user: $i, user: $i,

View file

@ -58,7 +58,7 @@ const exampleCWNote = reactive<Misskey.entities.Note>({
isBot: false, isBot: false,
isCat: true, isCat: true,
emojis: {}, emojis: {},
onlineStatus: null, onlineStatus: 'unknown',
badgeRoles: [], badgeRoles: [],
}, },
text: i18n.ts._initialTutorial._postNote._cw._exampleNote.note, text: i18n.ts._initialTutorial._postNote._cw._exampleNote.note,

View file

@ -40,7 +40,7 @@ const emit = defineEmits<{
const onceSucceeded = ref<boolean>(false); const onceSucceeded = ref<boolean>(false);
function doSucceeded(fileId: string, to: boolean) { function doSucceeded(fileId: string, to: boolean) {
if (fileId === exampleNote.fileIds[0] && to) { if (fileId === exampleNote.fileIds?.[0] && to) {
onceSucceeded.value = true; onceSucceeded.value = true;
emit('succeeded'); emit('succeeded');
} }

View file

@ -7,7 +7,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<MkModalWindow <MkModalWindow
ref="dialog" ref="dialog"
:width="400" :width="400"
@close="dialog.close()" @close="dialog?.close()"
@closed="$emit('closed')" @closed="$emit('closed')"
> >
<template v-if="announcement" #header>:{{ announcement.title }}:</template> <template v-if="announcement" #header>:{{ announcement.title }}:</template>
@ -64,14 +64,14 @@ import MkRadios from '@/components/MkRadios.vue';
const props = defineProps<{ const props = defineProps<{
user: Misskey.entities.User, user: Misskey.entities.User,
announcement?: any, announcement?: Misskey.entities.Announcement,
}>(); }>();
const dialog = ref<InstanceType<typeof MkModalWindow> | null>(null); const dialog = ref<InstanceType<typeof MkModalWindow> | null>(null);
const title = ref<string>(props.announcement ? props.announcement.title : ''); const title = ref(props.announcement ? props.announcement.title : '');
const text = ref<string>(props.announcement ? props.announcement.text : ''); const text = ref(props.announcement ? props.announcement.text : '');
const icon = ref<string>(props.announcement ? props.announcement.icon : 'info'); const icon = ref(props.announcement ? props.announcement.icon : 'info');
const display = ref<string>(props.announcement ? props.announcement.display : 'dialog'); const display = ref(props.announcement ? props.announcement.display : 'dialog');
const needConfirmationToRead = ref(props.announcement ? props.announcement.needConfirmationToRead : false); const needConfirmationToRead = ref(props.announcement ? props.announcement.needConfirmationToRead : false);
const emit = defineEmits<{ const emit = defineEmits<{
@ -92,18 +92,18 @@ async function done() {
if (props.announcement) { if (props.announcement) {
await os.apiWithDialog('admin/announcements/update', { await os.apiWithDialog('admin/announcements/update', {
id: props.announcement.id,
...params, ...params,
id: props.announcement.id,
}); });
emit('done', { emit('done', {
updated: { updated: {
id: props.announcement.id,
...params, ...params,
id: props.announcement.id,
}, },
}); });
dialog.value.close(); dialog.value?.close();
} else { } else {
const created = await os.apiWithDialog('admin/announcements/create', params); const created = await os.apiWithDialog('admin/announcements/create', params);
@ -111,7 +111,7 @@ async function done() {
created: created, created: created,
}); });
dialog.value.close(); dialog.value?.close();
} }
} }
@ -122,14 +122,16 @@ async function del() {
}); });
if (canceled) return; if (canceled) return;
misskeyApi('admin/announcements/delete', { if (props.announcement) {
id: props.announcement.id, await misskeyApi('admin/announcements/delete', {
}).then(() => { id: props.announcement.id,
emit('done', {
deleted: true,
}); });
dialog.value.close(); }
emit('done', {
deleted: true,
}); });
dialog.value?.close();
} }
</script> </script>

View file

@ -4,13 +4,13 @@ SPDX-License-Identifier: AGPL-3.0-only
--> -->
<template> <template>
<div v-adaptive-bg :class="[$style.root, { yellow: user.isSilenced, red: user.isSuspended, gray: false }]"> <div v-adaptive-bg :class="[$style.root]">
<MkAvatar class="avatar" :user="user" indicator/> <MkAvatar :class="$style.avatar" :user="user" indicator/>
<div class="body"> <div :class="$style.body">
<span class="name"><MkUserName class="name" :user="user"/></span> <span :class="$style.name"><MkUserName :user="user"/></span>
<span class="sub"><span class="acct _monospace">@{{ acct(user) }}</span></span> <span :class="$style.sub"><span class="_monospace">@{{ acct(user) }}</span></span>
</div> </div>
<MkMiniChart v-if="chartValues" class="chart" :src="chartValues"/> <MkMiniChart v-if="chartValues" :class="$style.chart" :src="chartValues"/>
</div> </div>
</template> </template>
@ -42,71 +42,53 @@ onMounted(() => {
</script> </script>
<style lang="scss" module> <style lang="scss" module>
.root { $bodyTitleHieght: 18px;
$bodyTitleHieght: 18px; $bodyInfoHieght: 16px;
$bodyInfoHieght: 16px;
.root {
display: flex; display: flex;
align-items: center; align-items: center;
padding: 16px; padding: 16px;
background: var(--panel); background: var(--panel);
border-radius: 8px; border-radius: 8px;
}
> :global(.avatar) { .avatar {
display: block; display: block;
width: ($bodyTitleHieght + $bodyInfoHieght); width: ($bodyTitleHieght + $bodyInfoHieght);
height: ($bodyTitleHieght + $bodyInfoHieght); height: ($bodyTitleHieght + $bodyInfoHieght);
margin-right: 12px; margin-right: 12px;
} }
> :global(.body) { .body {
flex: 1; flex: 1;
overflow: hidden; overflow: hidden;
font-size: 0.9em; font-size: 0.9em;
color: var(--fg); color: var(--fg);
padding-right: 8px; padding-right: 8px;
}
> :global(.name) { .name {
display: block; display: block;
width: 100%; width: 100%;
white-space: nowrap; white-space: nowrap;
overflow: hidden; overflow: hidden;
text-overflow: ellipsis; text-overflow: ellipsis;
line-height: $bodyTitleHieght; line-height: $bodyTitleHieght;
} }
> :global(.sub) { .sub {
display: block; display: block;
width: 100%; width: 100%;
font-size: 95%; font-size: 95%;
opacity: 0.7; opacity: 0.7;
line-height: $bodyInfoHieght; line-height: $bodyInfoHieght;
white-space: nowrap; white-space: nowrap;
overflow: hidden; overflow: hidden;
text-overflow: ellipsis; text-overflow: ellipsis;
} }
}
> :global(.chart) { .chart {
height: 30px; height: 30px;
}
&:global(.yellow) {
--c: rgb(255 196 0 / 15%);
background-image: linear-gradient(45deg, var(--c) 16.67%, transparent 16.67%, transparent 50%, var(--c) 50%, var(--c) 66.67%, transparent 66.67%, transparent 100%);
background-size: 16px 16px;
}
&:global(.red) {
--c: rgb(255 0 0 / 15%);
background-image: linear-gradient(45deg, var(--c) 16.67%, transparent 16.67%, transparent 50%, var(--c) 50%, var(--c) 66.67%, transparent 66.67%, transparent 100%);
background-size: 16px 16px;
}
&:global(.gray) {
--c: var(--bg);
background-image: linear-gradient(45deg, var(--c) 16.67%, transparent 16.67%, transparent 50%, var(--c) 50%, var(--c) 66.67%, transparent 66.67%, transparent 100%);
background-size: 16px 16px;
}
} }
</style> </style>

View file

@ -86,6 +86,7 @@ const top = ref(0);
const left = ref(0); const left = ref(0);
function showMenu(ev: MouseEvent) { function showMenu(ev: MouseEvent) {
if (user.value == null) return;
const { menu, cleanup } = getUserMenu(user.value); const { menu, cleanup } = getUserMenu(user.value);
os.popupMenu(menu, ev.currentTarget ?? ev.target).finally(cleanup); os.popupMenu(menu, ev.currentTarget ?? ev.target).finally(cleanup);
} }

View file

@ -80,9 +80,9 @@ const props = defineProps<{
const username = ref(''); const username = ref('');
const host = ref(''); const host = ref('');
const users = ref<Misskey.entities.UserDetailed[]>([]); const users = ref<Misskey.entities.UserLite[]>([]);
const recentUsers = ref<Misskey.entities.UserDetailed[]>([]); const recentUsers = ref<Misskey.entities.UserDetailed[]>([]);
const selected = ref<Misskey.entities.UserDetailed | null>(null); const selected = ref<Misskey.entities.UserLite | null>(null);
const dialogEl = ref(); const dialogEl = ref();
function search() { function search() {
@ -100,14 +100,19 @@ function search() {
}); });
} }
function ok() { async function ok() {
if (selected.value == null) return; if (selected.value == null) return;
emit('ok', selected.value);
const user = await misskeyApi('users/show', {
userId: selected.value.id,
});
emit('ok', user);
dialogEl.value.close(); dialogEl.value.close();
// 使 // 使
let recents = defaultStore.state.recentlyUsedUsers; let recents = defaultStore.state.recentlyUsedUsers;
recents = recents.filter(x => x !== selected.value.id); recents = recents.filter(x => x !== selected.value?.id);
recents.unshift(selected.value.id); recents.unshift(selected.value.id);
defaultStore.set('recentlyUsedUsers', recents.splice(0, 16)); defaultStore.set('recentlyUsedUsers', recents.splice(0, 16));
} }
@ -122,7 +127,7 @@ onMounted(() => {
userIds: defaultStore.state.recentlyUsedUsers, userIds: defaultStore.state.recentlyUsedUsers,
}).then(users => { }).then(users => {
if (props.includeSelf && users.find(x => $i ? x.id === $i.id : true) == null) { if (props.includeSelf && users.find(x => $i ? x.id === $i.id : true) == null) {
recentUsers.value = [$i, ...users]; recentUsers.value = [$i!, ...users];
} else { } else {
recentUsers.value = users; recentUsers.value = users;
} }

View file

@ -13,7 +13,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<MkPagination :pagination="pinnedUsers"> <MkPagination :pagination="pinnedUsers">
<template #default="{ items }"> <template #default="{ items }">
<div :class="$style.users"> <div :class="$style.users">
<XUser v-for="item in items" :key="item.id" :user="item"/> <XUser v-for="item in (items as Misskey.entities.UserDetailed[])" :key="item.id" :user="item"/>
</div> </div>
</template> </template>
</MkPagination> </MkPagination>
@ -25,7 +25,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<MkPagination :pagination="popularUsers"> <MkPagination :pagination="popularUsers">
<template #default="{ items }"> <template #default="{ items }">
<div :class="$style.users"> <div :class="$style.users">
<XUser v-for="item in items" :key="item.id" :user="item"/> <XUser v-for="item in (items as Misskey.entities.UserDetailed[])" :key="item.id" :user="item"/>
</div> </div>
</template> </template>
</MkPagination> </MkPagination>
@ -34,18 +34,28 @@ SPDX-License-Identifier: AGPL-3.0-only
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import Misskey from 'misskey-js';
import { i18n } from '@/i18n.js'; import { i18n } from '@/i18n.js';
import MkFolder from '@/components/MkFolder.vue'; import MkFolder from '@/components/MkFolder.vue';
import XUser from '@/components/MkUserSetupDialog.User.vue'; import XUser from '@/components/MkUserSetupDialog.User.vue';
import MkPagination from '@/components/MkPagination.vue'; import MkPagination, { type Paging } from '@/components/MkPagination.vue';
const pinnedUsers = { endpoint: 'pinned-users', noPaging: true }; const pinnedUsers: Paging = {
endpoint: 'pinned-users',
noPaging: true,
limit: 10,
};
const popularUsers = { endpoint: 'users', limit: 10, noPaging: true, params: { const popularUsers: Paging = {
state: 'alive', endpoint: 'users',
origin: 'local', limit: 10,
sort: '+follower', noPaging: true,
} }; params: {
state: 'alive',
origin: 'local',
sort: '+follower',
},
};
</script> </script>
<style lang="scss" module> <style lang="scss" module>

View file

@ -39,7 +39,9 @@ import FormSlot from '@/components/form/slot.vue';
import MkInfo from '@/components/MkInfo.vue'; import MkInfo from '@/components/MkInfo.vue';
import { chooseFileFromPc } from '@/scripts/select-file.js'; import { chooseFileFromPc } from '@/scripts/select-file.js';
import * as os from '@/os.js'; import * as os from '@/os.js';
import { $i } from '@/account.js'; import { signinRequired } from '@/account.js';
const $i = signinRequired();
const name = ref($i.name ?? ''); const name = ref($i.name ?? '');
const description = ref($i.description ?? ''); const description = ref($i.description ?? '');

View file

@ -4,7 +4,7 @@ SPDX-License-Identifier: AGPL-3.0-only
--> -->
<template> <template>
<MkModal ref="modal" v-slot="{ type }" :zPriority="'high'" :src="src" @click="modal.close()" @closed="emit('closed')"> <MkModal ref="modal" v-slot="{ type }" :zPriority="'high'" :src="src" @click="modal?.close()" @closed="emit('closed')">
<div class="_popup" :class="{ [$style.root]: true, [$style.asDrawer]: type === 'drawer' }"> <div class="_popup" :class="{ [$style.root]: true, [$style.asDrawer]: type === 'drawer' }">
<div :class="[$style.label, $style.item]"> <div :class="[$style.label, $style.item]">
{{ i18n.ts.visibility }} {{ i18n.ts.visibility }}

View file

@ -13,7 +13,7 @@ SPDX-License-Identifier: AGPL-3.0-only
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import { onMounted, shallowRef, ref } from 'vue'; import { onMounted, shallowRef, ref, nextTick } from 'vue';
import { Chart } from 'chart.js'; import { Chart } from 'chart.js';
import gradient from 'chartjs-plugin-gradient'; import gradient from 'chartjs-plugin-gradient';
import tinycolor from 'tinycolor2'; import tinycolor from 'tinycolor2';
@ -25,9 +25,9 @@ import { initChart } from '@/scripts/init-chart.js';
initChart(); initChart();
const chartEl = shallowRef<HTMLCanvasElement>(null); const chartEl = shallowRef<HTMLCanvasElement | null>(null);
const now = new Date(); const now = new Date();
let chartInstance: Chart = null; let chartInstance: Chart | null = null;
const chartLimit = 30; const chartLimit = 30;
const fetching = ref(true); const fetching = ref(true);
@ -55,6 +55,10 @@ async function renderChart() {
const raw = await misskeyApi('charts/active-users', { limit: chartLimit, span: 'day' }); const raw = await misskeyApi('charts/active-users', { limit: chartLimit, span: 'day' });
fetching.value = false;
await nextTick();
const vLineColor = defaultStore.state.darkMode ? 'rgba(255, 255, 255, 0.2)' : 'rgba(0, 0, 0, 0.2)'; const vLineColor = defaultStore.state.darkMode ? 'rgba(255, 255, 255, 0.2)' : 'rgba(0, 0, 0, 0.2)';
const computedStyle = getComputedStyle(document.documentElement); const computedStyle = getComputedStyle(document.documentElement);
@ -65,6 +69,8 @@ async function renderChart() {
const max = Math.max(...raw.read); const max = Math.max(...raw.read);
if (chartEl.value == null) return;
chartInstance = new Chart(chartEl.value, { chartInstance = new Chart(chartEl.value, {
type: 'bar', type: 'bar',
data: { data: {
@ -97,7 +103,6 @@ async function renderChart() {
type: 'time', type: 'time',
offset: true, offset: true,
time: { time: {
stepSize: 1,
unit: 'day', unit: 'day',
displayFormats: { displayFormats: {
day: 'M/d', day: 'M/d',
@ -108,6 +113,7 @@ async function renderChart() {
display: false, display: false,
}, },
ticks: { ticks: {
stepSize: 1,
display: true, display: true,
maxRotation: 0, maxRotation: 0,
autoSkipPadding: 8, autoSkipPadding: 8,
@ -141,13 +147,10 @@ async function renderChart() {
}, },
external: externalTooltipHandler, external: externalTooltipHandler,
}, },
gradient,
}, },
}, },
plugins: [chartVLine(vLineColor)], plugins: [chartVLine(vLineColor)],
}); });
fetching.value = false;
} }
onMounted(async () => { onMounted(async () => {

View file

@ -6,7 +6,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<template> <template>
<div v-if="meta" :class="$style.root"> <div v-if="meta" :class="$style.root">
<div :class="[$style.main, $style.panel]"> <div :class="[$style.main, $style.panel]">
<img :src="instance.iconUrl || instance.faviconUrl || '/favicon.ico'" alt="" :class="$style.mainIcon"/> <img :src="instance.iconUrl || '/favicon.ico'" alt="" :class="$style.mainIcon"/>
<button class="_button _acrylic" :class="$style.mainMenu" @click="showMenu"><i class="ti ti-dots"></i></button> <button class="_button _acrylic" :class="$style.mainMenu" @click="showMenu"><i class="ti ti-dots"></i></button>
<div :class="$style.mainFg"> <div :class="$style.mainFg">
<h1 :class="$style.mainTitle"> <h1 :class="$style.mainTitle">
@ -106,19 +106,19 @@ function showMenu(ev) {
text: i18n.ts.impressum, text: i18n.ts.impressum,
icon: 'ti ti-file-invoice', icon: 'ti ti-file-invoice',
action: () => { action: () => {
window.open(instance.impressumUrl, '_blank', 'noopener'); window.open(instance.impressumUrl!, '_blank', 'noopener');
}, },
} : undefined, (instance.tosUrl) ? { } : undefined, (instance.tosUrl) ? {
text: i18n.ts.termsOfService, text: i18n.ts.termsOfService,
icon: 'ti ti-notebook', icon: 'ti ti-notebook',
action: () => { action: () => {
window.open(instance.tosUrl, '_blank', 'noopener'); window.open(instance.tosUrl!, '_blank', 'noopener');
}, },
} : undefined, (instance.privacyPolicyUrl) ? { } : undefined, (instance.privacyPolicyUrl) ? {
text: i18n.ts.privacyPolicy, text: i18n.ts.privacyPolicy,
icon: 'ti ti-shield-lock', icon: 'ti ti-shield-lock',
action: () => { action: () => {
window.open(instance.privacyPolicyUrl, '_blank', 'noopener'); window.open(instance.privacyPolicyUrl!, '_blank', 'noopener');
}, },
} : undefined, (!instance.impressumUrl && !instance.tosUrl && !instance.privacyPolicyUrl) ? undefined : { type: 'divider' }, { } : undefined, (!instance.impressumUrl && !instance.tosUrl && !instance.privacyPolicyUrl) ? undefined : { type: 'divider' }, {
text: i18n.ts.help, text: i18n.ts.help,

View file

@ -32,7 +32,7 @@ const emit = defineEmits<{
function done() { function done() {
emit('done'); emit('done');
modal.value.close(); modal.value?.close();
} }
watch(() => props.showing, () => { watch(() => props.showing, () => {

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