mirror of
https://github.com/misskey-dev/misskey.git
synced 2026-06-04 20:34:10 +02:00
Merge branch 'develop' into copilot/add-user-mute-settings
This commit is contained in:
@@ -18,6 +18,9 @@
|
||||
- Enhance: プッシュ通知を行うための権限確認をより確実に行うように
|
||||
- Enhance: 投稿フォームのチュートリアルを追加
|
||||
- Enhance: 「自動でもっと見る」をほとんどの箇所で利用可能に
|
||||
- Enhance: アンテナ・リスト設定画面とタイムラインの動線を改善
|
||||
- アンテナ・リスト一覧画面の項目を選択すると、設定画面ではなくタイムラインに移動するようになりました
|
||||
- アンテナ・リストの設定画面の右上にタイムラインに移動するボタンを追加しました
|
||||
- Enhance: ミュートの付与期間を自由に設定できるように
|
||||
- Enhance: ロールの付与期間を自由に設定できるように
|
||||
- Fix: 紙吹雪エフェクトがアニメーション設定を考慮せず常に表示される問題を修正
|
||||
|
||||
4
locales/index.d.ts
vendored
4
locales/index.d.ts
vendored
@@ -6207,6 +6207,10 @@ export interface Locale extends ILocale {
|
||||
* 絵文字ピッカーに固定表示するプリセットをパレットとして登録したり、ピッカーの表示方法をカスタマイズしたりできます。
|
||||
*/
|
||||
"emojiPaletteBanner": string;
|
||||
/**
|
||||
* アニメーション画像を有効にする
|
||||
*/
|
||||
"enableAnimatedImages": string;
|
||||
"_chat": {
|
||||
/**
|
||||
* 送信者の名前を表示
|
||||
|
||||
@@ -1559,6 +1559,7 @@ _settings:
|
||||
showAvailableReactionsFirstInNote: "利用できるリアクションを先頭に表示"
|
||||
showPageTabBarBottom: "ページのタブバーを下部に表示"
|
||||
emojiPaletteBanner: "絵文字ピッカーに固定表示するプリセットをパレットとして登録したり、ピッカーの表示方法をカスタマイズしたりできます。"
|
||||
enableAnimatedImages: "アニメーション画像を有効にする"
|
||||
|
||||
_chat:
|
||||
showSenderName: "送信者の名前を表示"
|
||||
|
||||
@@ -133,6 +133,7 @@ export class UtilityService {
|
||||
|
||||
@bindThis
|
||||
public isFederationAllowedHost(host: string): boolean {
|
||||
if (this.isSelfHost(host)) return true;
|
||||
if (this.meta.federation === 'none') return false;
|
||||
if (this.meta.federation === 'specified' && !this.meta.federationHosts.some(x => `.${host.toLowerCase()}`.endsWith(`.${x}`))) return false;
|
||||
if (this.isBlockedHost(this.meta.blockedHosts, host)) return false;
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
process.env.NODE_ENV = 'test';
|
||||
|
||||
import { setTimeout } from 'node:timers/promises';
|
||||
import { jest } from '@jest/globals';
|
||||
import { describe, jest } from '@jest/globals';
|
||||
import { ModuleMocker } from 'jest-mock';
|
||||
import { Test } from '@nestjs/testing';
|
||||
import * as lolex from '@sinonjs/fake-timers';
|
||||
@@ -168,6 +168,61 @@ describe('RoleService', () => {
|
||||
await app.close();
|
||||
});
|
||||
|
||||
describe('getUserAssigns', () => {
|
||||
test('アサインされたロールを取得できる', async () => {
|
||||
const user = await createUser();
|
||||
const role1 = await createRole({ name: 'a' });
|
||||
const role2 = await createRole({ name: 'b' });
|
||||
|
||||
await roleService.assign(user.id, role1.id);
|
||||
await roleService.assign(user.id, role2.id);
|
||||
|
||||
const assigns = await roleService.getUserAssigns(user.id);
|
||||
expect(assigns).toHaveLength(2);
|
||||
expect(assigns.some(a => a.roleId === role1.id)).toBe(true);
|
||||
expect(assigns.some(a => a.roleId === role2.id)).toBe(true);
|
||||
});
|
||||
|
||||
test('アサインされたロールの有効/期限切れパターンを取得できる', async () => {
|
||||
const user = await createUser();
|
||||
const roleNoExpiry = await createRole({ name: 'no-expires' });
|
||||
const roleNotExpired = await createRole({ name: 'not-expired' });
|
||||
const roleExpired = await createRole({ name: 'expired' });
|
||||
|
||||
// expiresAtなし
|
||||
await roleService.assign(user.id, roleNoExpiry.id);
|
||||
|
||||
// expiresAtあり(期限切れでない)
|
||||
const future = new Date(Date.now() + 1000 * 60 * 60); // +1 hour
|
||||
await roleService.assign(user.id, roleNotExpired.id, future);
|
||||
|
||||
// expiresAtあり(期限切れ)
|
||||
await assignRole({ userId: user.id, roleId: roleExpired.id, expiresAt: new Date(Date.now() - 1000) });
|
||||
|
||||
const assigns = await roleService.getUserAssigns(user.id);
|
||||
expect(assigns.some(a => a.roleId === roleNoExpiry.id)).toBe(true);
|
||||
expect(assigns.some(a => a.roleId === roleNotExpired.id)).toBe(true);
|
||||
expect(assigns.some(a => a.roleId === roleExpired.id)).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getUserRoles', () => {
|
||||
test('アサインされたロールとコンディショナルロールの両方が取得できる', async () => {
|
||||
const user = await createUser();
|
||||
const manualRole = await createRole({ name: 'manual role' });
|
||||
const conditionalRole = await createConditionalRole({
|
||||
id: aidx(),
|
||||
type: 'isBot',
|
||||
});
|
||||
await roleService.assign(user.id, manualRole.id);
|
||||
await roleService.assign(user.id, conditionalRole.id);
|
||||
|
||||
const roles = await roleService.getUserRoles(user.id);
|
||||
expect(roles.some(r => r.id === manualRole.id)).toBe(true);
|
||||
expect(roles.some(r => r.id === conditionalRole.id)).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getUserPolicies', () => {
|
||||
test('instance default policies', async () => {
|
||||
const user = await createUser();
|
||||
@@ -280,6 +335,112 @@ describe('RoleService', () => {
|
||||
const resultAfter25hAgain = await roleService.getUserPolicies(user.id);
|
||||
expect(resultAfter25hAgain.canManageCustomEmojis).toBe(true);
|
||||
});
|
||||
|
||||
test('role with no policy set', async () => {
|
||||
const user = await createUser();
|
||||
const roleWithPolicy = await createRole({
|
||||
name: 'roleWithPolicy',
|
||||
policies: {
|
||||
pinLimit: {
|
||||
useDefault: false,
|
||||
priority: 0,
|
||||
value: 10,
|
||||
},
|
||||
},
|
||||
});
|
||||
const roleWithoutPolicy = await createRole({
|
||||
name: 'roleWithoutPolicy',
|
||||
policies: {}, // ポリシーが空
|
||||
});
|
||||
await roleService.assign(user.id, roleWithPolicy.id);
|
||||
await roleService.assign(user.id, roleWithoutPolicy.id);
|
||||
meta.policies = {
|
||||
pinLimit: 5,
|
||||
};
|
||||
|
||||
const result = await roleService.getUserPolicies(user.id);
|
||||
|
||||
// roleWithoutPolicy は default 値 (5) を使い、roleWithPolicy の 10 と比較して大きい方が採用される
|
||||
expect(result.pinLimit).toBe(10);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getUserBadgeRoles', () => {
|
||||
test('手動アサイン済みのバッジロールのみが返る', async () => {
|
||||
const user = await createUser();
|
||||
const badgeRole = await createRole({ name: 'badge', asBadge: true });
|
||||
const normalRole = await createRole({ name: 'normal', asBadge: false });
|
||||
|
||||
await roleService.assign(user.id, badgeRole.id);
|
||||
await roleService.assign(user.id, normalRole.id);
|
||||
|
||||
const roles = await roleService.getUserBadgeRoles(user.id);
|
||||
expect(roles.some(r => r.id === badgeRole.id)).toBe(true);
|
||||
expect(roles.some(r => r.id === normalRole.id)).toBe(false);
|
||||
});
|
||||
|
||||
test('コンディショナルなバッジロールが条件一致で返る', async () => {
|
||||
const user = await createUser({ isBot: true });
|
||||
const condBadgeRole = await createConditionalRole({
|
||||
id: aidx(),
|
||||
type: 'isBot',
|
||||
}, { asBadge: true, name: 'cond-badge' });
|
||||
const condNonBadgeRole = await createConditionalRole({
|
||||
id: aidx(),
|
||||
type: 'isBot',
|
||||
}, { asBadge: false, name: 'cond-non-badge' });
|
||||
|
||||
const roles = await roleService.getUserBadgeRoles(user.id);
|
||||
expect(roles.some(r => r.id === condBadgeRole.id)).toBe(true);
|
||||
expect(roles.some(r => r.id === condNonBadgeRole.id)).toBe(false);
|
||||
});
|
||||
|
||||
test('roleAssignedTo 条件のバッジロール: アサイン有無で変化する', async () => {
|
||||
const [user1, user2] = await Promise.all([createUser(), createUser()]);
|
||||
const manualRole = await createRole({ name: 'manual' });
|
||||
const condBadgeRole = await createConditionalRole({
|
||||
id: aidx(),
|
||||
type: 'roleAssignedTo',
|
||||
roleId: manualRole.id,
|
||||
}, { asBadge: true, name: 'assigned-badge' });
|
||||
|
||||
await roleService.assign(user2.id, manualRole.id);
|
||||
|
||||
const [roles1, roles2] = await Promise.all([
|
||||
roleService.getUserBadgeRoles(user1.id),
|
||||
roleService.getUserBadgeRoles(user2.id),
|
||||
]);
|
||||
expect(roles1.some(r => r.id === condBadgeRole.id)).toBe(false);
|
||||
expect(roles2.some(r => r.id === condBadgeRole.id)).toBe(true);
|
||||
});
|
||||
|
||||
test('期限切れのバッジロールは除外される', async () => {
|
||||
const user = await createUser();
|
||||
const roleNoExpiry = await createRole({ name: 'no-exp', asBadge: true });
|
||||
const roleNotExpired = await createRole({ name: 'not-expired', asBadge: true });
|
||||
const roleExpired = await createRole({ name: 'expired', asBadge: true });
|
||||
|
||||
// expiresAt なし
|
||||
await roleService.assign(user.id, roleNoExpiry.id);
|
||||
|
||||
// expiresAt あり(期限切れでない)
|
||||
const future = new Date(Date.now() + 1000 * 60 * 60); // +1 hour
|
||||
await roleService.assign(user.id, roleNotExpired.id, future);
|
||||
|
||||
// expiresAt あり(期限切れ)
|
||||
await assignRole({ userId: user.id, roleId: roleExpired.id, expiresAt: new Date(Date.now() - 1000) });
|
||||
|
||||
const rolesBefore = await roleService.getUserBadgeRoles(user.id);
|
||||
expect(rolesBefore.some(r => r.id === roleNoExpiry.id)).toBe(true);
|
||||
expect(rolesBefore.some(r => r.id === roleNotExpired.id)).toBe(true);
|
||||
expect(rolesBefore.some(r => r.id === roleExpired.id)).toBe(false);
|
||||
|
||||
// 時間経過で roleNotExpired を失効させる
|
||||
clock.tick('02:00:00');
|
||||
const rolesAfter = await roleService.getUserBadgeRoles(user.id);
|
||||
expect(rolesAfter.some(r => r.id === roleNoExpiry.id)).toBe(true);
|
||||
expect(rolesAfter.some(r => r.id === roleNotExpired.id)).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getModeratorIds', () => {
|
||||
@@ -413,9 +574,9 @@ describe('RoleService', () => {
|
||||
expect(result).toEqual([modeUser1.id, modeUser2.id, rootUser.id]);
|
||||
});
|
||||
|
||||
test('root has moderator role', async () => {
|
||||
const [adminUser1, modeUser1, normalUser1, rootUser] = await Promise.all([
|
||||
createUser(), createUser(), createUser(), createRoot(),
|
||||
test('includeAdmins = false, includeRoot = true, excludeExpire = true', async () => {
|
||||
const [adminUser1, adminUser2, modeUser1, modeUser2, normalUser1, normalUser2, rootUser] = await Promise.all([
|
||||
createUser(), createUser(), createUser(), createUser(), createUser(), createUser(), createRoot(),
|
||||
]);
|
||||
|
||||
const role1 = await createRole({ name: 'admin', isAdministrator: true });
|
||||
@@ -424,9 +585,11 @@ describe('RoleService', () => {
|
||||
|
||||
await Promise.all([
|
||||
assignRole({ userId: adminUser1.id, roleId: role1.id }),
|
||||
assignRole({ userId: adminUser2.id, roleId: role1.id, expiresAt: new Date(Date.now() - 1000) }),
|
||||
assignRole({ userId: modeUser1.id, roleId: role2.id }),
|
||||
assignRole({ userId: rootUser.id, roleId: role2.id }),
|
||||
assignRole({ userId: modeUser2.id, roleId: role2.id, expiresAt: new Date(Date.now() - 1000) }),
|
||||
assignRole({ userId: normalUser1.id, roleId: role3.id }),
|
||||
assignRole({ userId: normalUser2.id, roleId: role3.id, expiresAt: new Date(Date.now() - 1000) }),
|
||||
]);
|
||||
|
||||
const result = await roleService.getModeratorIds({
|
||||
@@ -434,12 +597,12 @@ describe('RoleService', () => {
|
||||
includeRoot: true,
|
||||
excludeExpire: false,
|
||||
});
|
||||
expect(result).toEqual([modeUser1.id, rootUser.id]);
|
||||
expect(result).toEqual([modeUser1.id, modeUser2.id, rootUser.id]);
|
||||
});
|
||||
|
||||
test('root has administrator role', async () => {
|
||||
const [adminUser1, modeUser1, normalUser1, rootUser] = await Promise.all([
|
||||
createUser(), createUser(), createUser(), createRoot(),
|
||||
test('includeAdmins = true, includeRoot = true, excludeExpire = false', async () => {
|
||||
const [adminUser1, adminUser2, modeUser1, modeUser2, normalUser1, normalUser2, rootUser] = await Promise.all([
|
||||
createUser(), createUser(), createUser(), createUser(), createUser(), createUser(), createRoot(),
|
||||
]);
|
||||
|
||||
const role1 = await createRole({ name: 'admin', isAdministrator: true });
|
||||
@@ -448,9 +611,11 @@ describe('RoleService', () => {
|
||||
|
||||
await Promise.all([
|
||||
assignRole({ userId: adminUser1.id, roleId: role1.id }),
|
||||
assignRole({ userId: rootUser.id, roleId: role1.id }),
|
||||
assignRole({ userId: adminUser2.id, roleId: role1.id, expiresAt: new Date(Date.now() - 1000) }),
|
||||
assignRole({ userId: modeUser1.id, roleId: role2.id }),
|
||||
assignRole({ userId: modeUser2.id, roleId: role2.id, expiresAt: new Date(Date.now() - 1000) }),
|
||||
assignRole({ userId: normalUser1.id, roleId: role3.id }),
|
||||
assignRole({ userId: normalUser2.id, roleId: role3.id, expiresAt: new Date(Date.now() - 1000) }),
|
||||
]);
|
||||
|
||||
const result = await roleService.getModeratorIds({
|
||||
@@ -458,12 +623,12 @@ describe('RoleService', () => {
|
||||
includeRoot: true,
|
||||
excludeExpire: false,
|
||||
});
|
||||
expect(result).toEqual([adminUser1.id, modeUser1.id, rootUser.id]);
|
||||
expect(result).toEqual([adminUser1.id, adminUser2.id, modeUser1.id, modeUser2.id, rootUser.id]);
|
||||
});
|
||||
|
||||
test('root has moderator role(expire)', async () => {
|
||||
const [adminUser1, modeUser1, normalUser1, rootUser] = await Promise.all([
|
||||
createUser(), createUser(), createUser(), createRoot(),
|
||||
test('includeAdmins = true, includeRoot = true, excludeExpire = true', async () => {
|
||||
const [adminUser1, adminUser2, modeUser1, modeUser2, normalUser1, normalUser2, rootUser] = await Promise.all([
|
||||
createUser(), createUser(), createUser(), createUser(), createUser(), createUser(), createRoot(),
|
||||
]);
|
||||
|
||||
const role1 = await createRole({ name: 'admin', isAdministrator: true });
|
||||
@@ -472,17 +637,71 @@ describe('RoleService', () => {
|
||||
|
||||
await Promise.all([
|
||||
assignRole({ userId: adminUser1.id, roleId: role1.id }),
|
||||
assignRole({ userId: modeUser1.id, roleId: role2.id, expiresAt: new Date(Date.now() - 1000) }),
|
||||
assignRole({ userId: rootUser.id, roleId: role2.id, expiresAt: new Date(Date.now() - 1000) }),
|
||||
assignRole({ userId: adminUser2.id, roleId: role1.id, expiresAt: new Date(Date.now() - 1000) }),
|
||||
assignRole({ userId: modeUser1.id, roleId: role2.id }),
|
||||
assignRole({ userId: modeUser2.id, roleId: role2.id, expiresAt: new Date(Date.now() - 1000) }),
|
||||
assignRole({ userId: normalUser1.id, roleId: role3.id }),
|
||||
assignRole({ userId: normalUser2.id, roleId: role3.id, expiresAt: new Date(Date.now() - 1000) }),
|
||||
]);
|
||||
|
||||
const result = await roleService.getModeratorIds({
|
||||
includeAdmins: false,
|
||||
includeAdmins: true,
|
||||
includeRoot: true,
|
||||
excludeExpire: true,
|
||||
});
|
||||
expect(result).toEqual([rootUser.id]);
|
||||
expect(result).toEqual([adminUser1.id, modeUser1.id, rootUser.id]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getAdministratorIds', () => {
|
||||
test('should return only user IDs with administrator roles', async () => {
|
||||
const adminUser1 = await createUser();
|
||||
const adminUser2 = await createUser();
|
||||
const normalUser = await createUser();
|
||||
const moderatorUser = await createUser();
|
||||
|
||||
const adminRole = await createRole({ name: 'admin', isAdministrator: true, isModerator: false });
|
||||
const moderatorRole = await createRole({ name: 'moderator', isModerator: true, isAdministrator: false });
|
||||
const normalRole = await createRole({ name: 'normal', isAdministrator: false, isModerator: false });
|
||||
|
||||
await roleService.assign(adminUser1.id, adminRole.id);
|
||||
await roleService.assign(adminUser2.id, adminRole.id);
|
||||
await roleService.assign(moderatorUser.id, moderatorRole.id);
|
||||
await roleService.assign(normalUser.id, normalRole.id);
|
||||
|
||||
const adminIds = await roleService.getAdministratorIds();
|
||||
|
||||
// sort for deterministic order
|
||||
adminIds.sort();
|
||||
const expectedIds = [adminUser1.id, adminUser2.id].sort();
|
||||
|
||||
expect(adminIds).toEqual(expectedIds);
|
||||
});
|
||||
|
||||
test('should return an empty array if no users have administrator roles', async () => {
|
||||
const normalUser = await createUser();
|
||||
const normalRole = await createRole({ name: 'normal', isAdministrator: false });
|
||||
await roleService.assign(normalUser.id, normalRole.id);
|
||||
|
||||
const adminIds = await roleService.getAdministratorIds();
|
||||
|
||||
expect(adminIds).toHaveLength(0);
|
||||
});
|
||||
|
||||
test('should return an empty array if there are no administrator roles defined', async () => {
|
||||
await createUser(); // create user to ensure not empty db
|
||||
const adminIds = await roleService.getAdministratorIds();
|
||||
expect(adminIds).toHaveLength(0);
|
||||
});
|
||||
|
||||
// TODO: rootユーザーは現在実装に含まれていないため、テストもそれに倣う
|
||||
test('should not include the root user', async () => {
|
||||
const rootUser = await createUser();
|
||||
meta.rootUserId = rootUser.id;
|
||||
|
||||
const adminIds = await roleService.getAdministratorIds();
|
||||
|
||||
expect(adminIds).not.toContain(rootUser.id);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -13,17 +13,16 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||
</MkPagination>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
<script lang="ts" setup generic="P extends IPaginator">
|
||||
import * as Misskey from 'misskey-js';
|
||||
import type { IPaginator } from '@/utility/paginator.js';
|
||||
import type { IPaginator, ExtractorFunction } from '@/utility/paginator.js';
|
||||
import MkChannelPreview from '@/components/MkChannelPreview.vue';
|
||||
import MkPagination from '@/components/MkPagination.vue';
|
||||
import { i18n } from '@/i18n.js';
|
||||
|
||||
const props = withDefaults(defineProps<{
|
||||
paginator: IPaginator;
|
||||
paginator: P;
|
||||
noGap?: boolean;
|
||||
extractor?: (item: any) => Misskey.entities.Channel;
|
||||
extractor?: ExtractorFunction<P, Misskey.entities.Channel>;
|
||||
}>(), {
|
||||
extractor: (item) => item,
|
||||
});
|
||||
|
||||
@@ -38,7 +38,7 @@ export const Default = {
|
||||
};
|
||||
},
|
||||
args: {
|
||||
imageFile: file(),
|
||||
imageFile: new File([], 'image.webp', { type: 'image/webp' }),
|
||||
aspectRatio: NaN,
|
||||
},
|
||||
parameters: {
|
||||
|
||||
@@ -60,7 +60,9 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||
<button class="_buttonPrimary" style="padding: 4px; border-radius: 8px;" @click="addVisibleUser"><i class="ti ti-plus ti-fw"></i></button>
|
||||
</div>
|
||||
</div>
|
||||
<MkInfo v-if="!store.r.tips.value.postForm" :class="$style.showHowToUse"><button class="_textButton" @click="showTour">{{ i18n.ts._postForm.showHowToUse }}</button></MkInfo>
|
||||
<MkInfo v-if="!store.r.tips.value.postForm" :class="$style.showHowToUse" closable @close="closeTip('postForm')">
|
||||
<button class="_textButton" @click="showTour">{{ i18n.ts._postForm.showHowToUse }}</button>
|
||||
</MkInfo>
|
||||
<MkInfo v-if="scheduledAt != null" :class="$style.scheduledAt">
|
||||
<I18n :src="i18n.ts.scheduleToPostOnX" tag="span">
|
||||
<template #x>
|
||||
|
||||
@@ -120,6 +120,7 @@ function onPasskeyLogin(): void {
|
||||
.then((res) => {
|
||||
passkeyContext.value = res.context ?? '';
|
||||
credentialRequest.value = parseRequestOptionsFromJSON({
|
||||
// @ts-expect-error TODO: misskey-js由来の型(@simplewebauthn/types)とフロントエンド由来の型(@github/webauthn-json)が合わない
|
||||
publicKey: res.option,
|
||||
});
|
||||
|
||||
@@ -134,7 +135,7 @@ function onPasskeyDone(credential: AuthenticationPublicKeyCredential): void {
|
||||
waiting.value = true;
|
||||
|
||||
if (doingPasskeyFromInputPage.value) {
|
||||
misskeyApi('signin-with-passkey', {
|
||||
misskeyApi<Misskey.entities.SigninWithPasskeyResponse>('signin-with-passkey', {
|
||||
credential: credential.toJSON(),
|
||||
context: passkeyContext.value,
|
||||
}).then((res) => {
|
||||
@@ -149,6 +150,7 @@ function onPasskeyDone(credential: AuthenticationPublicKeyCredential): void {
|
||||
tryLogin({
|
||||
username: userInfo.value.username,
|
||||
password: password.value,
|
||||
// @ts-expect-error TODO: misskey-js由来の型(@simplewebauthn/types)とフロントエンド由来の型(@github/webauthn-json)が合わない
|
||||
credential: credential.toJSON(),
|
||||
});
|
||||
}
|
||||
@@ -253,6 +255,7 @@ async function tryLogin(req: Partial<Misskey.entities.SigninFlowRequest>): Promi
|
||||
case 'passkey': {
|
||||
if (webAuthnSupported()) {
|
||||
credentialRequest.value = parseRequestOptionsFromJSON({
|
||||
// @ts-expect-error TODO: misskey-js由来の型(@simplewebauthn/types)とフロントエンド由来の型(@github/webauthn-json)が合わない
|
||||
publicKey: res.authRequest,
|
||||
});
|
||||
page.value = 'passkey';
|
||||
|
||||
@@ -15,17 +15,17 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||
</MkPagination>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
<script lang="ts" setup generic="P extends IPaginator">
|
||||
import * as Misskey from 'misskey-js';
|
||||
import type { IPaginator } from '@/utility/paginator.js';
|
||||
import type { IPaginator, ExtractorFunction } from '@/utility/paginator.js';
|
||||
import MkUserInfo from '@/components/MkUserInfo.vue';
|
||||
import MkPagination from '@/components/MkPagination.vue';
|
||||
import { i18n } from '@/i18n.js';
|
||||
|
||||
const props = withDefaults(defineProps<{
|
||||
paginator: IPaginator;
|
||||
paginator: P;
|
||||
noGap?: boolean;
|
||||
extractor?: (item: any) => Misskey.entities.UserDetailed;
|
||||
extractor?: ExtractorFunction<P, Misskey.entities.UserDetailed>;
|
||||
}>(), {
|
||||
extractor: (item) => item,
|
||||
});
|
||||
|
||||
@@ -12,7 +12,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||
<h1 :class="$style.mainTitle">
|
||||
<!-- 背景色によってはロゴが見えなくなるのでとりあえず無効に -->
|
||||
<!-- <img class="logo" v-if="instance.logoImageUrl" :src="instance.logoImageUrl"><span v-else class="text">{{ instanceName }}</span> -->
|
||||
<span>{{ instanceName }}</span>
|
||||
<MkA to="/">{{ instanceName }}</MkA>
|
||||
</h1>
|
||||
<div :class="$style.mainAbout">
|
||||
<!-- eslint-disable-next-line vue/no-v-html -->
|
||||
|
||||
@@ -19,10 +19,13 @@ export function useForm<T extends Record<string, any>>(initialState: T, save: (n
|
||||
const currentState = reactive<T>(copy(initialState));
|
||||
const previousState = reactive<T>(copy(initialState));
|
||||
|
||||
const modifiedStates = reactive<Record<keyof T, boolean>>({} as any);
|
||||
for (const key in currentState) {
|
||||
modifiedStates[key] = false;
|
||||
}
|
||||
const modifiedStates = reactive<Record<keyof T, boolean>>((() => {
|
||||
const obj: Record<keyof T, boolean> = {} as Record<keyof T, boolean>;
|
||||
for (const key in initialState) {
|
||||
obj[key] = false;
|
||||
}
|
||||
return obj;
|
||||
})());
|
||||
const modified = computed(() => Object.values(modifiedStates).some(v => v));
|
||||
const modifiedCount = computed(() => Object.values(modifiedStates).filter(v => v).length);
|
||||
|
||||
|
||||
@@ -4,13 +4,14 @@
|
||||
*/
|
||||
|
||||
import type { InjectionKey, Ref } from 'vue';
|
||||
import type { PageMetadata } from '@/page.js';
|
||||
import type { Router } from '@/router.js';
|
||||
|
||||
export const DI = {
|
||||
routerCurrentDepth: Symbol() as InjectionKey<number>,
|
||||
router: Symbol() as InjectionKey<Router>,
|
||||
mock: Symbol() as InjectionKey<boolean>,
|
||||
pageMetadata: Symbol() as InjectionKey<Ref<Record<string, any> | null>>,
|
||||
pageMetadata: Symbol() as InjectionKey<Ref<PageMetadata | null>>,
|
||||
viewId: Symbol() as InjectionKey<string>,
|
||||
currentStickyTop: Symbol() as InjectionKey<Ref<number>>,
|
||||
currentStickyBottom: Symbol() as InjectionKey<Ref<number>>,
|
||||
|
||||
@@ -7,7 +7,7 @@ import type { Directive } from 'vue';
|
||||
import { getBgColor } from '@/utility/get-bg-color.js';
|
||||
import { globalEvents } from '@/events.js';
|
||||
|
||||
const handlerMap = new WeakMap<any, any>();
|
||||
const handlerMap = new WeakMap<HTMLElement, () => void>();
|
||||
|
||||
export const adaptiveBorderDirective = {
|
||||
mounted(src) {
|
||||
|
||||
@@ -119,9 +119,9 @@ export const userPreviewDirective = {
|
||||
|
||||
// TODO: 新たにプロパティを作るのをやめMapを使う
|
||||
// ただメモリ的には↓の方が省メモリかもしれないので検討中
|
||||
const self = (el as any)._userPreviewDirective_ = {} as any;
|
||||
|
||||
self.preview = new UserPreview(el, binding.value);
|
||||
el._userPreviewDirective_ = {
|
||||
preview: new UserPreview(el, binding.value),
|
||||
};
|
||||
},
|
||||
|
||||
unmounted(el, binding) {
|
||||
|
||||
@@ -59,7 +59,7 @@ export class Pizzax<T extends StateDef> {
|
||||
private pizzaxChannel: BroadcastChannel<PizzaxChannelMessage<T>>;
|
||||
|
||||
// 簡易的にキューイングして占有ロックとする
|
||||
private currentIdbJob: Promise<any> = Promise.resolve();
|
||||
private currentIdbJob: Promise<unknown> = Promise.resolve();
|
||||
private addIdbSetJob<T>(job: () => Promise<T>) {
|
||||
const promise = this.currentIdbJob.then(job, err => {
|
||||
console.error('Pizzax failed to save data to idb!', err);
|
||||
|
||||
@@ -257,7 +257,7 @@ const {
|
||||
const user = ref(result.user);
|
||||
const info = ref(result.info);
|
||||
const ips = ref(result.ips);
|
||||
const ap = ref<any>(null);
|
||||
const ap = ref<Misskey.entities.ApGetResponse | null>(null);
|
||||
const moderator = ref(info.value.isModerator);
|
||||
const silenced = ref(info.value.isSilenced);
|
||||
const suspended = ref(info.value.isSuspended);
|
||||
|
||||
@@ -44,7 +44,7 @@ async function addRelay() {
|
||||
if (canceled || inbox == null) return;
|
||||
misskeyApi('admin/relays/add', {
|
||||
inbox,
|
||||
}).then((relay: any) => {
|
||||
}).then(() => {
|
||||
refresh();
|
||||
}).catch((err: any) => {
|
||||
os.alert({
|
||||
|
||||
@@ -161,7 +161,11 @@ async function _fetch_() {
|
||||
},
|
||||
raw: res.data,
|
||||
};
|
||||
} catch (err: any) {
|
||||
} catch (err) {
|
||||
if (!(err instanceof Error)) {
|
||||
throw err;
|
||||
}
|
||||
|
||||
switch (err.message.toLowerCase()) {
|
||||
case 'this theme is already installed':
|
||||
errorKV.value = {
|
||||
|
||||
@@ -36,7 +36,17 @@ misskeyApi('antennas/show', { antennaId: props.antennaId }).then((antennaRespons
|
||||
antenna.value = antennaResponse;
|
||||
});
|
||||
|
||||
const headerActions = computed(() => []);
|
||||
const headerActions = computed(() => antenna.value ? [{
|
||||
icon: 'ti ti-timeline',
|
||||
text: i18n.ts.timeline,
|
||||
handler: () => {
|
||||
router.push('/timeline/antenna/:antennaId', {
|
||||
params: {
|
||||
antennaId: antenna.value!.id,
|
||||
},
|
||||
});
|
||||
},
|
||||
}] : []);
|
||||
const headerTabs = computed(() => []);
|
||||
|
||||
definePage(() => ({
|
||||
|
||||
@@ -12,7 +12,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||
<MkButton :link="true" to="/my/antennas/create" primary :class="$style.add"><i class="ti ti-plus"></i> {{ i18n.ts.add }}</MkButton>
|
||||
|
||||
<div v-if="antennas.length > 0" class="_gaps">
|
||||
<MkA v-for="antenna in antennas" :key="antenna.id" :class="$style.antenna" :to="`/my/antennas/${antenna.id}`">
|
||||
<MkA v-for="antenna in antennas" :key="antenna.id" :class="$style.antenna" :to="`/timeline/antenna/${antenna.id}`">
|
||||
<div class="name">{{ antenna.name }}</div>
|
||||
</MkA>
|
||||
</div>
|
||||
|
||||
@@ -16,7 +16,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||
<MkButton primary rounded style="margin: 0 auto;" @click="create"><i class="ti ti-plus"></i> {{ i18n.ts.createList }}</MkButton>
|
||||
|
||||
<div v-if="items.length > 0" class="_gaps">
|
||||
<MkA v-for="list in items" :key="list.id" class="_panel" :class="$style.list" :to="`/my/lists/${ list.id }`">
|
||||
<MkA v-for="list in items" :key="list.id" class="_panel" :class="$style.list" :to="`/timeline/list/${list.id}`">
|
||||
<div style="margin-bottom: 4px;">{{ list.name }} <span :class="$style.nUsers">({{ i18n.tsx.nUsers({ n: `${list.userIds!.length}/${$i.policies['userEachUserListsLimit']}` }) }})</span></div>
|
||||
<MkAvatars :userIds="list.userIds!" :limit="10"/>
|
||||
</MkA>
|
||||
|
||||
@@ -26,10 +26,10 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||
<template #label>{{ i18n.ts.members }}</template>
|
||||
<template #caption>{{ i18n.tsx.nUsers({ n: `${list.userIds!.length}/${$i.policies['userEachUserListsLimit']}` }) }}</template>
|
||||
|
||||
<div class="_gaps_s">
|
||||
<MkButton rounded primary style="margin: 0 auto;" @click="addUser()">{{ i18n.ts.addUser }}</MkButton>
|
||||
<div class="_gaps">
|
||||
<MkButton rounded primary style="margin: 0 auto;" @click="addUser()"><i class="ti ti-plus"></i> {{ i18n.ts.addUser }}</MkButton>
|
||||
|
||||
<MkPagination :paginator="membershipsPaginator" withControl>
|
||||
<MkPagination :paginator="membershipsPaginator">
|
||||
<template #default="{ items }">
|
||||
<div class="_gaps_s">
|
||||
<div v-for="item in items" :key="item.id">
|
||||
@@ -67,12 +67,13 @@ import MkInput from '@/components/MkInput.vue';
|
||||
import { userListsCache } from '@/cache.js';
|
||||
import { ensureSignin } from '@/i.js';
|
||||
import MkPagination from '@/components/MkPagination.vue';
|
||||
import { mainRouter } from '@/router.js';
|
||||
import { prefer } from '@/preferences.js';
|
||||
import { useRouter } from '@/router.js';
|
||||
import { Paginator } from '@/utility/paginator.js';
|
||||
|
||||
const $i = ensureSignin();
|
||||
|
||||
const router = useRouter();
|
||||
|
||||
const props = defineProps<{
|
||||
listId: string;
|
||||
}>();
|
||||
@@ -162,7 +163,7 @@ async function deleteList() {
|
||||
listId: list.value.id,
|
||||
});
|
||||
userListsCache.delete();
|
||||
mainRouter.push('/my/lists');
|
||||
router.push('/my/lists');
|
||||
}
|
||||
|
||||
async function updateSettings() {
|
||||
@@ -181,7 +182,17 @@ async function updateSettings() {
|
||||
|
||||
watch(() => props.listId, fetchList, { immediate: true });
|
||||
|
||||
const headerActions = computed(() => []);
|
||||
const headerActions = computed(() => list.value ? [{
|
||||
icon: 'ti ti-timeline',
|
||||
text: i18n.ts.timeline,
|
||||
handler: () => {
|
||||
router.push('/timeline/list/:listId', {
|
||||
params: {
|
||||
listId: list.value!.id,
|
||||
},
|
||||
});
|
||||
},
|
||||
}] : []);
|
||||
|
||||
const headerTabs = computed(() => []);
|
||||
|
||||
|
||||
@@ -60,6 +60,7 @@ import { Interpreter, Parser, utils } from '@syuilo/aiscript';
|
||||
import type { Ref } from 'vue';
|
||||
import type { AsUiComponent } from '@/aiscript/ui.js';
|
||||
import type { AsUiRoot } from '@/aiscript/ui.js';
|
||||
import type { Value } from '@syuilo/aiscript/interpreter/value.js';
|
||||
import MkContainer from '@/components/MkContainer.vue';
|
||||
import MkButton from '@/components/MkButton.vue';
|
||||
import MkTextarea from '@/components/MkTextarea.vue';
|
||||
@@ -141,7 +142,7 @@ async function run() {
|
||||
switch (type) {
|
||||
case 'end': logs.value.push({
|
||||
id: Math.random(),
|
||||
text: utils.valToString(params.val, true),
|
||||
text: utils.valToString(params.val as Value, true),
|
||||
print: false,
|
||||
}); break;
|
||||
default: break;
|
||||
|
||||
@@ -603,6 +603,18 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||
</MkPreferenceContainer>
|
||||
</SearchMarker>
|
||||
|
||||
<SearchMarker :keywords="['animation', 'image', 'photo', 'picture', 'media', 'thumbnail', 'gif']">
|
||||
<MkPreferenceContainer k="disableShowingAnimatedImages">
|
||||
<MkSwitch :modelValue="!disableShowingAnimatedImages" @update:modelValue="v => disableShowingAnimatedImages = !v">
|
||||
<template #label><SearchLabel>{{ i18n.ts._settings.enableAnimatedImages }}</SearchLabel></template>
|
||||
<template #caption>
|
||||
<SearchText>{{ i18n.ts.turnOffToImprovePerformance }}</SearchText>
|
||||
<div>{{ i18n.ts.disableShowingAnimatedImages_caption }}</div>
|
||||
</template>
|
||||
</MkSwitch>
|
||||
</MkPreferenceContainer>
|
||||
</SearchMarker>
|
||||
|
||||
<SearchMarker :keywords="['blur']">
|
||||
<MkPreferenceContainer k="useBlurEffect">
|
||||
<MkSwitch v-model="useBlurEffect">
|
||||
|
||||
@@ -7,8 +7,6 @@ import * as Misskey from 'misskey-js';
|
||||
import { markRaw } from 'vue';
|
||||
import { $i } from '@/i.js';
|
||||
import { wsOrigin } from '@@/js/config.js';
|
||||
// TODO: No WebsocketモードでStreamMockが使えそう
|
||||
//import { StreamMock } from '@/utility/stream-mock.js';
|
||||
|
||||
// heart beat interval in ms
|
||||
const HEART_BEAT_INTERVAL = 1000 * 60;
|
||||
|
||||
@@ -8,16 +8,18 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||
<a v-if="isRoot" href="https://github.com/misskey-dev/misskey" target="_blank" class="github-corner" aria-label="View source on GitHub"><svg width="80" height="80" viewBox="0 0 250 250" style="fill:var(--MI_THEME-panel); color:var(--MI_THEME-fg); position: fixed; z-index: 10; top: 0; border: 0; right: 0;" aria-hidden="true"><path d="M0,0 L115,115 L130,115 L142,142 L250,250 L250,0 Z"></path><path d="M128.3,109.0 C113.8,99.7 119.0,89.6 119.0,89.6 C122.0,82.7 120.5,78.6 120.5,78.6 C119.2,72.0 123.4,76.3 123.4,76.3 C127.3,80.9 125.5,87.3 125.5,87.3 C122.9,97.6 130.6,101.9 134.4,103.2" fill="currentColor" style="transform-origin: 130px 106px;" class="octo-arm"></path><path d="M115.0,115.0 C114.9,115.1 118.7,116.5 119.8,115.4 L133.7,101.6 C136.9,99.2 139.9,98.4 142.2,98.6 C133.8,88.0 127.5,74.4 143.8,58.0 C148.5,53.4 154.0,51.2 159.7,51.0 C160.3,49.4 163.2,43.6 171.4,40.1 C171.4,40.1 176.1,42.5 178.8,56.2 C183.1,58.6 187.2,61.8 190.9,65.4 C194.5,69.0 197.7,73.2 200.1,77.6 C213.8,80.2 216.3,84.9 216.3,84.9 C212.7,93.1 206.9,96.0 205.4,96.6 C205.1,102.4 203.0,107.8 198.3,112.5 C181.9,128.9 168.3,122.5 157.7,114.1 C157.9,116.9 156.7,120.9 152.7,124.9 L141.0,136.5 C139.8,137.7 141.6,141.9 141.8,141.8 Z" fill="currentColor" class="octo-body"></path></svg></a>
|
||||
|
||||
<div v-if="!narrow && !isRoot" :class="$style.side">
|
||||
<div :class="$style.banner" :style="{ backgroundImage: instance.backgroundImageUrl ? `url(${ instance.backgroundImageUrl })` : 'none' }"></div>
|
||||
<div :class="$style.dashboard">
|
||||
<div :class="$style.sideBanner" :style="{ backgroundImage: instance.backgroundImageUrl ? `url(${ instance.backgroundImageUrl })` : 'none' }"></div>
|
||||
<div :class="$style.sideDashboard">
|
||||
<MkVisitorDashboard/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div :class="$style.main">
|
||||
<button v-if="!isRoot" :class="$style.homeButton" class="_button" @click="goHome">
|
||||
<i class="ti ti-home"></i>
|
||||
</button>
|
||||
<div v-if="narrow && !isRoot" :class="$style.header">
|
||||
<img :src="instance.iconUrl || '/favicon.ico'" alt="" :class="$style.headerIcon"/>
|
||||
<MkA to="/" :class="$style.headerTitle">{{ instanceName }}</MkA>
|
||||
<MkButton primary rounded :class="$style.headerButton" @click="goHome">{{ i18n.ts.signup }}</MkButton>
|
||||
</div>
|
||||
<div :class="$style.content">
|
||||
<RouterView/>
|
||||
</div>
|
||||
@@ -38,6 +40,7 @@ import { i18n } from '@/i18n.js';
|
||||
import MkVisitorDashboard from '@/components/MkVisitorDashboard.vue';
|
||||
import { mainRouter } from '@/router.js';
|
||||
import { DI } from '@/di.js';
|
||||
import MkButton from '@/components/MkButton.vue';
|
||||
|
||||
const isRoot = computed(() => mainRouter.currentRoute.value.name === 'index');
|
||||
|
||||
@@ -93,16 +96,26 @@ onMounted(() => {
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.homeButton {
|
||||
position: fixed;
|
||||
z-index: 1000;
|
||||
bottom: 16px;
|
||||
right: 16px;
|
||||
width: 60px;
|
||||
height: 60px;
|
||||
.header {
|
||||
padding: 16px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
background: var(--MI_THEME-panel);
|
||||
border-radius: 999px;
|
||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.3);
|
||||
}
|
||||
|
||||
.headerIcon {
|
||||
width: 48px;
|
||||
vertical-align: bottom;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.headerTitle {
|
||||
margin: 0 16px;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.headerButton {
|
||||
margin-left: auto;
|
||||
}
|
||||
|
||||
.side {
|
||||
@@ -112,7 +125,7 @@ onMounted(() => {
|
||||
background: var(--MI_THEME-accent);
|
||||
}
|
||||
|
||||
.banner {
|
||||
.sideBanner {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
@@ -124,7 +137,7 @@ onMounted(() => {
|
||||
mask-image: linear-gradient(rgba(0, 0, 0, 1.0), transparent);
|
||||
}
|
||||
|
||||
.dashboard {
|
||||
.sideDashboard {
|
||||
padding: 32px;
|
||||
}
|
||||
|
||||
|
||||
@@ -124,7 +124,7 @@ export function uploadFile(file: File | Blob, options: {
|
||||
const driveFile = JSON.parse(ev.target.response);
|
||||
globalEvents.emit('driveFileCreated', driveFile);
|
||||
resolve(driveFile);
|
||||
}) as (ev: ProgressEvent<EventTarget>) => any;
|
||||
}) as (ev: ProgressEvent<EventTarget>) => void;
|
||||
|
||||
if (options.onProgress) {
|
||||
xhr.upload.onprogress = ev => {
|
||||
|
||||
@@ -13,7 +13,7 @@ interface CommonParamDef {
|
||||
type: string;
|
||||
label?: string;
|
||||
caption?: string;
|
||||
default: any;
|
||||
default: unknown;
|
||||
}
|
||||
|
||||
interface NumberParamDef extends CommonParamDef {
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
|
||||
import { ref, shallowRef, triggerRef } from 'vue';
|
||||
import * as Misskey from 'misskey-js';
|
||||
import type { ComputedRef, Ref, ShallowRef } from 'vue';
|
||||
import type { ComputedRef, Ref, ShallowRef, UnwrapRef } from 'vue';
|
||||
import { misskeyApi } from '@/utility/misskey-api.js';
|
||||
|
||||
const MAX_ITEMS = 30;
|
||||
@@ -19,7 +19,12 @@ export type MisskeyEntity = {
|
||||
_shouldInsertAd_?: boolean;
|
||||
};
|
||||
|
||||
type FilterByEpRes<E extends Record<string, any>> = {
|
||||
type AbsEndpointType = {
|
||||
req: unknown;
|
||||
res: unknown;
|
||||
};
|
||||
|
||||
type FilterByEpRes<E extends Record<string, AbsEndpointType>> = {
|
||||
[K in keyof E]: E[K]['res'] extends Array<{ id: string }> ? K : never
|
||||
}[keyof E];
|
||||
export type PaginatorCompatibleEndpointPaths = FilterByEpRes<Misskey.Endpoints>;
|
||||
@@ -27,6 +32,8 @@ export type PaginatorCompatibleEndpoints = {
|
||||
[K in PaginatorCompatibleEndpointPaths]: Misskey.Endpoints[K];
|
||||
};
|
||||
|
||||
export type ExtractorFunction<P extends IPaginator, T> = (item: UnwrapRef<P['items']>[number]) => T;
|
||||
|
||||
export interface IPaginator<T = unknown, _T = T & MisskeyEntity> {
|
||||
/**
|
||||
* 外部から直接操作しないでください
|
||||
|
||||
@@ -1,81 +0,0 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: syuilo and misskey-project
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import { EventEmitter } from 'eventemitter3';
|
||||
import * as Misskey from 'misskey-js';
|
||||
import type { Channels, StreamEvents, IStream, IChannelConnection } from 'misskey-js';
|
||||
|
||||
type AnyOf<T extends Record<any, any>> = T[keyof T];
|
||||
type OmitFirst<T extends any[]> = T extends [any, ...infer R] ? R : never;
|
||||
|
||||
/**
|
||||
* Websocket無効化時に使うStreamのモック(なにもしない)
|
||||
*/
|
||||
export class StreamMock extends EventEmitter<StreamEvents> implements IStream {
|
||||
public readonly state = 'initializing';
|
||||
|
||||
constructor(...args: ConstructorParameters<typeof Misskey.Stream>) {
|
||||
super();
|
||||
// do nothing
|
||||
}
|
||||
|
||||
public useChannel<C extends keyof Channels>(channel: C, params?: Channels[C]['params'], name?: string): ChannelConnectionMock<Channels[C]> {
|
||||
return new ChannelConnectionMock(this, channel, name);
|
||||
}
|
||||
|
||||
public removeSharedConnection(connection: any): void {
|
||||
// do nothing
|
||||
}
|
||||
|
||||
public removeSharedConnectionPool(pool: any): void {
|
||||
// do nothing
|
||||
}
|
||||
|
||||
public disconnectToChannel(): void {
|
||||
// do nothing
|
||||
}
|
||||
|
||||
public send(typeOrPayload: string): void;
|
||||
public send(typeOrPayload: string, payload: any): void;
|
||||
public send(typeOrPayload: Record<string, any> | any[]): void;
|
||||
public send(typeOrPayload: string | Record<string, any> | any[], payload?: any): void {
|
||||
// do nothing
|
||||
}
|
||||
|
||||
public ping(): void {
|
||||
// do nothing
|
||||
}
|
||||
|
||||
public heartbeat(): void {
|
||||
// do nothing
|
||||
}
|
||||
|
||||
public close(): void {
|
||||
// do nothing
|
||||
}
|
||||
}
|
||||
|
||||
class ChannelConnectionMock<Channel extends AnyOf<Channels> = any> extends EventEmitter<Channel['events']> implements IChannelConnection<Channel> {
|
||||
public id = '';
|
||||
public name?: string; // for debug
|
||||
public inCount = 0; // for debug
|
||||
public outCount = 0; // for debug
|
||||
public channel: string;
|
||||
|
||||
constructor(stream: IStream, ...args: OmitFirst<ConstructorParameters<typeof Misskey.ChannelConnection<Channel>>>) {
|
||||
super();
|
||||
|
||||
this.channel = args[0];
|
||||
this.name = args[1];
|
||||
}
|
||||
|
||||
public send<T extends keyof Channel['receives']>(type: T, body: Channel['receives'][T]): void {
|
||||
// do nothing
|
||||
}
|
||||
|
||||
public dispose(): void {
|
||||
// do nothing
|
||||
}
|
||||
}
|
||||
@@ -222,7 +222,8 @@ export class WatermarkRenderer {
|
||||
},
|
||||
});
|
||||
} else {
|
||||
throw new Error(`Unrecognized layer type: ${(layer as any).type}`);
|
||||
// @ts-expect-error Should be unreachable
|
||||
throw new Error(`Unrecognized layer type: ${layer.type}`);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -24,6 +24,7 @@ import { Interpreter, Parser, utils } from '@syuilo/aiscript';
|
||||
import { useWidgetPropsManager } from './widget.js';
|
||||
import type { WidgetComponentEmits, WidgetComponentExpose, WidgetComponentProps } from './widget.js';
|
||||
import type { FormWithDefault, GetFormResultType } from '@/utility/form.js';
|
||||
import type { Value } from '@syuilo/aiscript/interpreter/value.js';
|
||||
import * as os from '@/os.js';
|
||||
import MkContainer from '@/components/MkContainer.vue';
|
||||
import { aiScriptReadline, createAiScriptEnv } from '@/aiscript/api.js';
|
||||
@@ -83,7 +84,7 @@ const run = async () => {
|
||||
switch (type) {
|
||||
case 'end': logs.value.push({
|
||||
id: genId(),
|
||||
text: utils.valToString(params.val, true),
|
||||
text: utils.valToString(params.val as Value, true),
|
||||
print: false,
|
||||
}); break;
|
||||
default: break;
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
"declaration": false,
|
||||
"sourceMap": false,
|
||||
"target": "ES2022",
|
||||
"module": "ES2022",
|
||||
"module": "esnext",
|
||||
"moduleResolution": "Bundler",
|
||||
"removeComments": false,
|
||||
"noLib": false,
|
||||
|
||||
@@ -3512,6 +3512,7 @@ type SigninFlowRequest = {
|
||||
'g-recaptcha-response'?: string | null;
|
||||
'turnstile-response'?: string | null;
|
||||
'm-captcha-response'?: string | null;
|
||||
'testcaptcha-response'?: string | null;
|
||||
};
|
||||
|
||||
// @public (undocumented)
|
||||
|
||||
@@ -294,6 +294,7 @@ export type SigninFlowRequest = {
|
||||
'g-recaptcha-response'?: string | null;
|
||||
'turnstile-response'?: string | null;
|
||||
'm-captcha-response'?: string | null;
|
||||
'testcaptcha-response'?: string | null;
|
||||
};
|
||||
|
||||
export type SigninFlowResponse = {
|
||||
|
||||
Reference in New Issue
Block a user