fix types

This commit is contained in:
Lhcfl 2024-04-14 03:15:31 +08:00
parent 1a6ba246f2
commit cee3a13f51
16 changed files with 194 additions and 191 deletions

6
packages/client/@types/window.d.ts vendored Normal file
View file

@ -0,0 +1,6 @@
declare global {
interface Window {
__misskey_input_ref__?: HTMLInputElement | null;
}
}
export type {};

View file

@ -216,25 +216,32 @@ interface Input {
| "paragraph";
placeholder?: string | null;
autocomplete?: string;
default: string | number | null;
default?: string | number | null;
minLength?: number;
maxLength?: number;
}
interface Select {
items: {
value: string;
text: string;
}[];
groupedItems: {
label: string;
items: {
value: string;
text: string;
}[];
}[];
default: string | null;
}
type Select = {
default?: string | null;
} & (
| {
items: {
value: string;
text: string;
}[];
groupedItems?: undefined;
}
| {
items?: undefined;
groupedItems: {
label: string;
items: {
value: string;
text: string;
}[];
}[];
}
);
const props = withDefaults(
defineProps<{

View file

@ -254,8 +254,7 @@ const isActive = ref();
watch(
() => props.items,
() => {
// FIXME: what's this?
const items: (MenuItem | MenuPending)[] = [...props.items].filter(
const items: (MenuItem | MenuPending)[] = props.items.filter(
(item) => item !== undefined,
);

View file

@ -181,10 +181,10 @@ function adjustTweetHeight(message: any) {
if (height) tweetHeight.value = height;
}
(window as any).addEventListener("message", adjustTweetHeight);
window.addEventListener("message", adjustTweetHeight);
onUnmounted(() => {
(window as any).removeEventListener("message", adjustTweetHeight);
window.removeEventListener("message", adjustTweetHeight);
});
</script>

View file

@ -13,7 +13,7 @@
]"
>
<i
v-if="success"
v-if="unref(success)"
:class="[$style.icon, $style.success, iconify('ph-check')]"
></i>
<MkLoading
@ -29,15 +29,15 @@
</template>
<script lang="ts" setup>
import { shallowRef, watch } from "vue";
import { MaybeRef, shallowRef, watch, unref } from "vue";
import MkModal from "@/components/MkModal.vue";
import iconify from "@/scripts/icon";
const modal = shallowRef<InstanceType<typeof MkModal>>();
const props = defineProps<{
success: boolean;
showing: boolean;
success: MaybeRef<boolean>;
showing: MaybeRef<boolean>;
text?: string;
}>();

View file

@ -1,7 +1,7 @@
// TODO: なんでもかんでもos.tsに突っ込むのやめたいのでよしなに分割する
import { EventEmitter } from "eventemitter3";
import { type entities, api as firefishApi } from "firefish-js";
import { type entities, api as firefishApi, type Endpoints } from "firefish-js";
import insertTextAtCursor from "insert-text-at-cursor";
import type { Component, Ref } from "vue";
import { defineAsyncComponent, markRaw, ref } from "vue";
@ -13,7 +13,7 @@ import MkWaitingDialog from "@/components/MkWaitingDialog.vue";
import { apiUrl, url } from "@/config";
import { me } from "@/me";
import type { MenuItem } from "@/types/menu";
import type { FormItemType, GetFormResultType } from "@/types/form";
import type { Form, GetFormResultType } from "@/types/form";
export const pendingApiRequestsCount = ref(0);
@ -52,7 +52,7 @@ export const api = ((
if (res.status === 200) {
resolve(body);
} else if (res.status === 204) {
resolve();
resolve(undefined);
} else {
reject(body.error);
}
@ -67,7 +67,7 @@ export const api = ((
export const apiGet = ((
endpoint: string,
data: Record<string, any> = {},
data: URLSearchParams | string | string[][] | Record<string, string> = {},
token?: string | null | undefined,
) => {
pendingApiRequestsCount.value++;
@ -97,7 +97,7 @@ export const apiGet = ((
if (res.status === 200) {
resolve(body);
} else if (res.status === 204) {
resolve();
resolve(undefined);
} else {
reject(body.error);
}
@ -111,15 +111,15 @@ export const apiGet = ((
}) as typeof apiClient.request;
export const apiWithDialog = ((
endpoint: string,
data: Record<string, any> = {},
endpoint: keyof Endpoints,
data: Record<string, unknown> = {},
token?: string | null | undefined,
) => {
const promise = api(endpoint, data, token);
promiseDialog(promise, null, (err) => {
alert({
type: "error",
text: err.message + "\n" + (err as any).id,
text: `${err.message}\n${err.id}`,
});
});
@ -129,7 +129,7 @@ export const apiWithDialog = ((
export function promiseDialog<T>(
promise: Promise<T>,
onSuccess?: ((res: T) => void) | null,
onFailure?: ((err: Error) => void) | null,
onFailure?: ((err: firefishApi.APIError) => void) | null,
text?: string,
): Promise<T> {
const showing = ref(true);
@ -294,7 +294,7 @@ export function alert(props: {
text?: string | null;
isPlaintext?: boolean;
}): Promise<void> {
return new Promise((resolve, reject) => {
return new Promise((resolve, _reject) => {
if (props.text == null && props.type === "error") {
props.text = i18n.ts.somethingHappened;
}
@ -319,7 +319,7 @@ export function confirm(props: {
cancelText?: string;
isPlaintext?: boolean;
}): Promise<{ canceled: boolean }> {
return new Promise((resolve, reject) => {
return new Promise((resolve, _reject) => {
popup(
MkDialog,
{
@ -342,7 +342,7 @@ export function yesno(props: {
text?: string | null;
isPlaintext?: boolean;
}): Promise<{ canceled: boolean }> {
return new Promise((resolve, reject) => {
return new Promise((resolve, _reject) => {
popup(
defineAsyncComponent({
loader: () => import("@/components/MkDialog.vue"),
@ -374,13 +374,13 @@ export function inputText(props: {
minLength?: number;
maxLength?: number;
}): Promise<
| { canceled: true; result: undefined }
| { canceled: true; result?: undefined }
| {
canceled: false;
result: string;
}
> {
return new Promise((resolve, reject) => {
return new Promise((resolve, _reject) => {
popup(
MkDialog,
{
@ -397,7 +397,14 @@ export function inputText(props: {
},
{
done: (result) => {
resolve(result || { canceled: true });
if (result.canceled) {
resolve({ canceled: true });
} else {
resolve({
canceled: false,
result: String(result.result),
});
}
},
},
"closed",
@ -411,13 +418,13 @@ export function inputParagraph(props: {
placeholder?: string | null;
default?: string | null;
}): Promise<
| { canceled: true; result: undefined }
| { canceled: true; result?: undefined }
| {
canceled: false;
result: string;
}
> {
return new Promise((resolve, reject) => {
return new Promise((resolve, _reject) => {
popup(
defineAsyncComponent({
loader: () => import("@/components/MkDialog.vue"),
@ -435,7 +442,14 @@ export function inputParagraph(props: {
},
{
done: (result) => {
resolve(result || { canceled: true });
if (result.canceled) {
resolve({ canceled: true });
} else {
resolve({
canceled: false,
result: String(result.result),
});
}
},
},
"closed",
@ -450,13 +464,13 @@ export function inputNumber(props: {
default?: number | null;
autocomplete?: string;
}): Promise<
| { canceled: true; result: undefined }
| { canceled: true; result?: undefined }
| {
canceled: false;
result: number;
}
> {
return new Promise((resolve, reject) => {
return new Promise((resolve, _reject) => {
popup(
defineAsyncComponent({
loader: () => import("@/components/MkDialog.vue"),
@ -475,7 +489,14 @@ export function inputNumber(props: {
},
{
done: (result) => {
resolve(result || { canceled: true });
if (result.canceled) {
resolve({ canceled: true });
} else {
resolve({
canceled: false,
result: Number(result.result),
});
}
},
},
"closed",
@ -487,15 +508,16 @@ export function inputDate(props: {
title?: string | null;
text?: string | null;
placeholder?: string | null;
default?: Date | null;
default?: Date | string | null;
}): Promise<
| { canceled: true; result: undefined }
| { canceled: true; result?: undefined }
| {
canceled: false;
result: Date;
}
> {
return new Promise((resolve, reject) => {
props.default ??= new Date();
return new Promise((resolve, _reject) => {
popup(
MkDialog,
{
@ -504,7 +526,10 @@ export function inputDate(props: {
input: {
type: "date",
placeholder: props.placeholder,
default: props.default,
default:
props.default instanceof Date
? props.default.toISOString().slice(0, 10)
: props.default,
},
},
{
@ -512,7 +537,7 @@ export function inputDate(props: {
resolve(
result
? {
result: new Date(result.result),
result: new Date(result.result as string | number),
canceled: false,
}
: { canceled: true },
@ -524,7 +549,7 @@ export function inputDate(props: {
});
}
export function select<C = any>(
export function select<C extends string>(
props: {
title?: string | null;
text?: string | null;
@ -535,8 +560,10 @@ export function select<C = any>(
value: C;
text: string;
}[];
groupedItems?: undefined;
}
| {
items?: undefined;
groupedItems: {
label: string;
items: {
@ -547,27 +574,35 @@ export function select<C = any>(
}
),
): Promise<
| { canceled: true; result: undefined }
| { canceled: true; result?: undefined }
| {
canceled: false;
result: C;
}
> {
return new Promise((resolve, reject) => {
return new Promise((resolve, _reject) => {
popup(
MkDialog,
{
title: props.title,
text: props.text,
select: {
items: props.items,
groupedItems: props.groupedItems,
default: props.default,
},
select: props.items
? {
items: props.items,
default: props.default,
}
: {
groupedItems: props.groupedItems,
default: props.default,
},
},
{
done: (result) => {
resolve(result || { canceled: true });
if (result.canceled) {
resolve({ canceled: true });
} else {
resolve(result as never);
}
},
},
"closed",
@ -576,7 +611,7 @@ export function select<C = any>(
}
export function success(): Promise<void> {
return new Promise((resolve, reject) => {
return new Promise((resolve, _reject) => {
const showing = ref(true);
window.setTimeout(() => {
showing.value = false;
@ -596,7 +631,7 @@ export function success(): Promise<void> {
}
export function waiting(): Promise<void> {
return new Promise((resolve, reject) => {
return new Promise((resolve, _reject) => {
const showing = ref(true);
popup(
MkWaitingDialog,
@ -612,14 +647,9 @@ export function waiting(): Promise<void> {
});
}
export function form<T extends Record<string, FormItemType>>(
title: string,
form: T,
) {
export function form<T extends Form>(title: string, form: T) {
return new Promise<{
result?: {
[K in keyof T]: GetFormResultType<T[K]["type"]>;
};
result?: GetFormResultType<T>;
canceled?: true;
}>((resolve, _reject) => {
popup(
@ -639,8 +669,8 @@ export function form<T extends Record<string, FormItemType>>(
});
}
export async function selectUser() {
return new Promise<entities.UserDetailed>((resolve, reject) => {
export function selectUser() {
return new Promise<entities.UserDetailed>((resolve, _reject) => {
popup(
defineAsyncComponent({
loader: () => import("@/components/MkUserSelectDialog.vue"),
@ -658,8 +688,8 @@ export async function selectUser() {
});
}
export async function selectLocalUser() {
return new Promise((resolve, reject) => {
export function selectLocalUser() {
return new Promise<entities.UserDetailed>((resolve, _reject) => {
popup(
defineAsyncComponent({
loader: () => import("@/components/MkUserSelectLocalDialog.vue"),
@ -677,8 +707,8 @@ export async function selectLocalUser() {
});
}
export async function selectInstance(): Promise<entities.Instance> {
return new Promise((resolve, reject) => {
export function selectInstance(): Promise<entities.Instance> {
return new Promise((resolve, _reject) => {
popup(
defineAsyncComponent({
loader: () => import("@/components/MkInstanceSelectDialog.vue"),
@ -696,8 +726,10 @@ export async function selectInstance(): Promise<entities.Instance> {
});
}
export async function selectDriveFile(multiple: boolean) {
return new Promise((resolve, reject) => {
export function selectDriveFile<Multiple extends boolean>(multiple: Multiple) {
return new Promise<
Multiple extends true ? entities.DriveFile[] : entities.DriveFile
>((resolve, _reject) => {
popup(
defineAsyncComponent({
loader: () => import("@/components/MkDriveSelectDialog.vue"),
@ -711,7 +743,7 @@ export async function selectDriveFile(multiple: boolean) {
{
done: (files) => {
if (files) {
resolve(multiple ? files : files[0]);
resolve((multiple ? files : files[0]) as never);
}
},
},
@ -721,7 +753,7 @@ export async function selectDriveFile(multiple: boolean) {
}
export async function selectDriveFolder(multiple: boolean) {
return new Promise((resolve, reject) => {
return new Promise((resolve, _reject) => {
popup(
defineAsyncComponent({
loader: () => import("@/components/MkDriveSelectDialog.vue"),
@ -745,7 +777,7 @@ export async function selectDriveFolder(multiple: boolean) {
}
export async function pickEmoji(src: HTMLElement | null, opts) {
return new Promise((resolve, reject) => {
return new Promise((resolve, _reject) => {
popup(
defineAsyncComponent({
loader: () => import("@/components/MkEmojiPickerDialog.vue"),
@ -772,7 +804,7 @@ export async function cropImage(
aspectRatio: number;
},
): Promise<entities.DriveFile> {
return new Promise((resolve, reject) => {
return new Promise((resolve, _reject) => {
popup(
defineAsyncComponent({
loader: () => import("@/components/MkCropperDialog.vue"),
@ -795,13 +827,13 @@ export async function cropImage(
type AwaitType<T> = T extends Promise<infer U>
? U
: T extends (...args: any[]) => Promise<infer V>
: T extends (...args: unknown[]) => Promise<infer V>
? V
: T;
let openingEmojiPicker: AwaitType<ReturnType<typeof popup>> | null = null;
let activeTextarea: HTMLTextAreaElement | HTMLInputElement | null = null;
let activeTextarea: HTMLTextAreaElement | HTMLInputElement;
export async function openEmojiPicker(
src?: HTMLElement,
src: HTMLElement | undefined,
opts,
initialTextarea: typeof activeTextarea,
) {
@ -809,7 +841,7 @@ export async function openEmojiPicker(
activeTextarea = initialTextarea;
const textareas = document.querySelectorAll("textarea, input");
const textareas = document.querySelectorAll<HTMLTextAreaElement | HTMLInputElement>("textarea, input");
for (const textarea of Array.from(textareas)) {
textarea.addEventListener("focus", () => {
activeTextarea = textarea;
@ -821,7 +853,7 @@ export async function openEmojiPicker(
for (const node of Array.from(record.addedNodes).filter(
(node) => node instanceof HTMLElement,
) as HTMLElement[]) {
const textareas = node.querySelectorAll("textarea, input");
const textareas = node.querySelectorAll<HTMLTextAreaElement | HTMLInputElement>("textarea, input");
for (const textarea of Array.from(textareas).filter(
(textarea) => textarea.dataset.preventEmojiInsert == null,
)) {
@ -859,7 +891,7 @@ export async function openEmojiPicker(
insertTextAtCursor(activeTextarea, emoji);
},
closed: () => {
openingEmojiPicker!.dispose();
openingEmojiPicker?.dispose();
openingEmojiPicker = null;
observer.disconnect();
},
@ -910,8 +942,8 @@ export function contextMenu(
ev: MouseEvent,
) {
ev.preventDefault();
return new Promise((resolve, reject) => {
let dispose;
return new Promise<void>((resolve, _reject) => {
let dispose: () => void;
popup(
defineAsyncComponent({
loader: () => import("@/components/MkContextMenu.vue"),
@ -938,7 +970,7 @@ export function post(
props: InstanceType<typeof MkPostFormDialog>["$props"] = {},
onClosed?: () => void,
) {
return new Promise<void>((resolve, reject) => {
return new Promise<void>((resolve, _reject) => {
// NOTE: MkPostFormDialogをdynamic importするとiOSでテキストエリアに自動フォーカスできない
// NOTE: ただ、dynamic importしない場合、MkPostFormDialogインスタンスが使いまわされ、
// Vueが渡されたコンポーネントに内部的に__propsというプロパティを生やす影響で、
@ -966,7 +998,7 @@ export const deckGlobalEvents = new EventEmitter();
/*
export function checkExistence(fileData: ArrayBuffer): Promise<any> {
return new Promise((resolve, reject) => {
return new Promise((resolve, _reject) => {
const data = new FormData();
data.append('md5', getMD5(fileData));

View file

@ -44,9 +44,9 @@ const props = withDefaults(
const file = ref<any>(null);
async function choose() {
os.selectDriveFile(false).then((fileResponse: any) => {
os.selectDriveFile(false).then((fileResponse) => {
file.value = fileResponse;
props.value.fileId = fileResponse.id;
props.value.fileId = fileResponse?.id;
});
}

View file

@ -299,12 +299,12 @@ function loadFile(): void {
});
//
(window as any).__misskey_input_ref__ = null;
window.__misskey_input_ref__ = null;
};
// https://qiita.com/fukasawah/items/b9dc732d95d99551013d
// iOS Safari
(window as any).__misskey_input_ref__ = input;
window.__misskey_input_ref__ = input;
input.click();
}

View file

@ -56,8 +56,8 @@ async function timetravel() {
title: i18n.ts.date,
});
if (canceled) return;
tlEl.value.timetravel(date);
// FIXME:
tlEl.value!.timetravel(date);
}
const headerActions = computed(() =>

View file

@ -1,72 +1,11 @@
export type FormItem =
| {
label?: string;
type: "string";
default: string | null;
hidden?: boolean;
multiline?: boolean;
}
| {
label?: string;
type: "number";
default: number | null;
hidden?: boolean;
step?: number;
}
| {
label?: string;
type: "boolean";
default: boolean | null;
hidden?: boolean;
}
| {
label?: string;
type: "enum";
default: string | null;
hidden?: boolean;
enum: string[];
}
| {
label?: string;
type: "radio";
default: unknown | null;
hidden?: boolean;
options: {
label: string;
value: unknown;
}[];
}
| {
label?: string;
type: "object";
default: Record<string, unknown> | null;
hidden: true;
}
| {
label?: string;
type: "array";
default: unknown[] | null;
hidden: true;
};
// TODO: replace this file with @/types/form.ts
import type { FormItemType, GetFormItemResultType } from "@/types/form";
export type FormItem = FormItemType;
export type Form = Record<string, FormItem>;
type GetItemType<Item extends FormItem> = Item["type"] extends "string"
? string
: Item["type"] extends "number"
? number
: Item["type"] extends "boolean"
? boolean
: Item["type"] extends "radio"
? unknown
: Item["type"] extends "enum"
? string
: Item["type"] extends "array"
? unknown[]
: Item["type"] extends "object"
? Record<string, unknown>
: never;
export type GetFormResultType<F extends Form> = {
[P in keyof F]: GetItemType<F[P]>;
[P in keyof F]: NonNullable<GetFormItemResultType<F[P]["type"]>>;
};

View file

@ -14,6 +14,7 @@ import icon from "@/scripts/icon";
import { useRouter } from "@/router";
import { notePage } from "@/filters/note";
import type { NoteTranslation } from "@/types/note";
import type { MenuItem } from "@/types/menu";
const router = useRouter();
@ -291,7 +292,7 @@ export function getNoteMenu(props: {
props.translating.value = false;
}
let menu;
let menu: MenuItem[];
if (isSignedIn) {
const statePromise = os.api("notes/state", {
noteId: appearNote.id,
@ -396,7 +397,7 @@ export function getNoteMenu(props: {
}
: undefined,
{
type: "parent",
type: "parent" as const,
icon: `${icon("ph-share-network")}`,
text: i18n.ts.share,
children: [
@ -499,7 +500,7 @@ export function getNoteMenu(props: {
!isAppearAuthor ? null : undefined,
!isAppearAuthor
? {
type: "parent",
type: "parent" as const,
icon: `${icon("ph-user")}`,
text: i18n.ts.user,
children: getUserMenu(appearNote.user),

View file

@ -9,12 +9,14 @@ import icon from "@/scripts/icon";
const stream = useStream();
function select(
src: any,
function select<Multiple extends boolean>(
src: HTMLElement | null | undefined,
label: string | null,
multiple: boolean,
): Promise<entities.DriveFile | entities.DriveFile[]> {
return new Promise((res, rej) => {
multiple: Multiple,
) {
return new Promise<
Multiple extends true ? entities.DriveFile[] : entities.DriveFile
>((res, rej) => {
const keepOriginal = ref(defaultStore.state.keepOriginalUploading);
const chooseFileFromPc = () => {
@ -22,6 +24,9 @@ function select(
input.type = "file";
input.multiple = multiple;
input.onchange = () => {
if (input.files === null) {
return;
}
const promises = Array.from(input.files).map((file) =>
uploadFile(
file,
@ -33,19 +38,19 @@ function select(
Promise.all(promises)
.then((driveFiles) => {
res(multiple ? driveFiles : driveFiles[0]);
res((multiple ? driveFiles : driveFiles[0]) as never);
})
.catch((err) => {
// アップロードのエラーは uploadFile 内でハンドリングされているためアラートダイアログを出したりはしてはいけない
});
// 一応廃棄
(window as any).__misskey_input_ref__ = null;
window.__misskey_input_ref__ = null;
};
// https://qiita.com/fukasawah/items/b9dc732d95d99551013d
// iOS Safari で正常に動かす為のおまじない
(window as any).__misskey_input_ref__ = input;
window.__misskey_input_ref__ = input;
input.click();
};
@ -69,7 +74,7 @@ function select(
const connection = stream.useChannel("main");
connection.on("urlUploadFinished", (urlResponse) => {
if (urlResponse.marker === marker) {
res(multiple ? [urlResponse.file] : urlResponse.file);
res((multiple ? [urlResponse.file] : urlResponse.file) as never);
connection.dispose();
}
});
@ -122,15 +127,15 @@ function select(
}
export function selectFile(
src: any,
src: HTMLElement | null | undefined,
label: string | null = null,
): Promise<entities.DriveFile> {
return select(src, label, false) as Promise<entities.DriveFile>;
) {
return select(src, label, false);
}
export function selectFiles(
src: any,
src: HTMLElement | null | undefined,
label: string | null = null,
): Promise<entities.DriveFile[]> {
return select(src, label, true) as Promise<entities.DriveFile[]>;
) {
return select(src, label, true);
}

View file

@ -54,18 +54,18 @@ export type FormItemSwitch = BaseFormItem & {
};
export type FormItemSelect = BaseFormItem & {
type: "enum";
default?: string | null;
default?: string | number | symbol | null;
enum: {
value: string | number | symbol | undefined;
value: string | number | symbol;
label: string;
}[];
};
export type FormItemRadios = BaseFormItem & {
type: "radio";
default?: string | number | symbol | undefined | null;
default?: string | number | symbol | null;
options: {
label: string;
value: string | number | symbol | undefined;
value: string | number | symbol;
}[];
};
export type FormItemRange = BaseFormItem & {
@ -114,10 +114,25 @@ export type FormItemInput = FormItemInputArray[number];
export type FormItemType = FormItemTypeArray[number];
export type GetFormItemByType<T extends FormItemType["type"], F extends FormItemType = FormItemType> = F extends {type: T} ? F : never;
export type Form = Record<string, FormItemType>;
export type GetFormItemByType<
T extends FormItemType["type"],
F extends FormItemType = FormItemType,
> = F extends { type: T } ? F : never;
type NonUndefindAble<T> = T extends undefined ? never : T;
type NonNullAble<T> = T extends null ? never : T;
export type GetFormResultType<T extends FormItemType["type"], I extends FormItemType = GetFormItemByType<T>> = NonUndefindAble<
export type GetFormItemResultType<
T extends FormItemType["type"],
I extends FormItemType = GetFormItemByType<T>,
> = NonUndefindAble<
"__result_typedef" extends keyof I ? I["__result_typedef"] : I["default"]
>
>;
export type GetFormResultType<F extends Form> = {
[K in keyof F]: F[K]["required"] extends false
? GetFormItemResultType<F[K]["type"]>
: NonNullAble<GetFormItemResultType<F[K]["type"]>>;
};

View file

@ -23,9 +23,7 @@ export interface WidgetComponentExpose {
configure: () => void;
}
export const useWidgetPropsManager = <
F extends Form & Record<string, { default: any }>,
>(
export const useWidgetPropsManager = <F extends Form>(
name: string,
propsDef: F,
props: Readonly<WidgetComponentProps<GetFormResultType<F>>>,

View file

@ -122,6 +122,6 @@ export class APIClient {
.catch(reject);
});
return promise as any;
return promise;
}
}

View file

@ -36,6 +36,7 @@ import type {
UserDetailed,
UserGroup,
UserList,
UserLite,
UserSorting,
} from "./entities";