Merge branch 'refactor/lists' into 'develop'

refactor: Fix types of components

Co-authored-by: Lhcfl <Lhcfl@outlook.com>

See merge request firefish/firefish!10730
This commit is contained in:
naskya 2024-04-07 03:57:10 +00:00
commit cb7e9ab449
45 changed files with 608 additions and 420 deletions

View File

@ -1,12 +1,30 @@
{
"$schema": "https://biomejs.dev/schemas/1.0.0/schema.json",
"$schema": "https://biomejs.dev/schemas/1.6.4/schema.json",
"organizeImports": {
"enabled": true
},
"linter": {
"enabled": true,
"rules": {
"recommended": true
"recommended": true,
"style": {
"noUselessElse": "off"
}
}
}
},
"overrides": [
{
"include": ["*.vue"],
"linter": {
"rules": {
"style": {
"useImportType": "warn",
"useShorthandFunctionType": "warn",
"useTemplate": "warn",
"noNonNullAssertion": "off"
}
}
}
}
]
}

View File

@ -14,6 +14,7 @@
"caughtErrorsIgnorePattern": "^_",
"destructuredArrayIgnorePattern": "^_"
}
]
],
"vue/no-setup-props-destructure": "off"
}
}

View File

@ -8,6 +8,7 @@
"build:debug": "pnpm run build",
"lint": "pnpm biome check **/*.ts --apply ; pnpm run lint:vue",
"lint:vue": "pnpm eslint src --fix '**/*.vue' --cache ; pnpm run format",
"types:check": "pnpm vue-tsc --noEmit",
"format": "pnpm biome format * --write"
},
"devDependencies": {
@ -86,6 +87,7 @@
"vue": "3.4.21",
"vue-draggable-plus": "^0.3.5",
"vue-plyr": "^7.0.0",
"vue-prism-editor": "2.0.0-alpha.2"
"vue-prism-editor": "2.0.0-alpha.2",
"vue-tsc": "2.0.6"
}
}

View File

@ -24,13 +24,14 @@
<script lang="ts" setup>
import MkChannelPreview from "@/components/MkChannelPreview.vue";
import type { Paging } from "@/components/MkPagination.vue";
import type { PagingOf } from "@/components/MkPagination.vue";
import MkPagination from "@/components/MkPagination.vue";
import { i18n } from "@/i18n";
import type { entities } from "firefish-js";
const props = withDefaults(
defineProps<{
pagination: Paging;
pagination: PagingOf<entities.Channel>;
noGap?: boolean;
extractor?: (item: any) => any;
}>(),

View File

@ -1,130 +1,83 @@
<script lang="ts">
import type { PropType } from "vue";
import { TransitionGroup, defineComponent, h } from "vue";
<template>
<component
:is="defaultStore.state.animation? TransitionGroup : 'div'"
tag="div"
class="sqadhkmv"
name="list"
:class="{ noGap }"
:data-direction = "props.direction"
:data-reversed = "props.reversed ? 'true' : 'false'"
>
<template v-for="(item, index) in items" :key="item.id">
<slot :item="item"> </slot>
<div
v-if="index !== items.length - 1 &&
new Date(item.createdAt).getDate() !==
new Date(items[index + 1].createdAt).getDate()"
class="separator"
>
<p class="date">
<span>
<i class="icon" :class="icon('ph-caret-up')"></i>
{{ getDateText(item.createdAt) }}
</span>
<span>
{{ getDateText(items[index + 1].createdAt) }}
<i class="icon" :class="icon('ph-caret-down')"></i>
</span>
</p>
</div>
<!-- class="a" means advertise -->
<MkAd
v-else-if="ad && item._shouldInsertAd_"
class="a"
:prefer="['inline', 'inline-big']"
/>
</template>
</component>
</template>
<script lang="ts" setup generic="T extends Item">
import { TransitionGroup } from "vue";
import MkAd from "@/components/global/MkAd.vue";
import { i18n } from "@/i18n";
import { defaultStore } from "@/store";
import icon from "@/scripts/icon";
export default defineComponent({
props: {
items: {
type: Array as PropType<
{ id: string; createdAt: string; _shouldInsertAd_?: boolean }[]
>,
required: true,
},
direction: {
type: String,
required: false,
default: "down",
},
reversed: {
type: Boolean,
required: false,
default: false,
},
noGap: {
type: Boolean,
required: false,
default: false,
},
ad: {
type: Boolean,
required: false,
default: false,
},
export interface Item {
id: string;
createdAt: string;
_shouldInsertAd_?: boolean;
}
const props = withDefaults(
defineProps<{
items: T[];
direction?: string;
reversed?: boolean;
noGap?: boolean;
ad?: boolean;
}>(),
{
direction: "down",
reversed: false,
noGap: false,
ad: false,
},
);
setup(props, { slots, expose }) {
function getDateText(time: string) {
const date = new Date(time).getDate();
const month = new Date(time).getMonth() + 1;
return i18n.t("monthAndDay", {
month: month.toString(),
day: date.toString(),
});
}
const slots = defineSlots<{
default(props: { item: T }): unknown;
}>();
if (props.items.length === 0) return;
const renderChildren = () =>
props.items.map((item, i) => {
if (!slots || !slots.default) return;
const el = slots.default({
item,
})[0];
if (el.key == null && item.id) el.key = item.id;
if (
i !== props.items.length - 1 &&
new Date(item.createdAt).getDate() !==
new Date(props.items[i + 1].createdAt).getDate()
) {
const separator = h(
"div",
{
class: "separator",
key: item.id + ":separator",
},
h(
"p",
{
class: "date",
},
[
h("span", [
h("i", {
class: `${icon("ph-caret-up")} icon`,
}),
getDateText(item.createdAt),
]),
h("span", [
getDateText(props.items[i + 1].createdAt),
h("i", {
class: `${icon("ph-caret-down")} icon`,
}),
]),
],
),
);
return [el, separator];
} else {
if (props.ad && item._shouldInsertAd_) {
return [
h(MkAd, {
class: "a", // advertise()
key: item.id + ":ad",
prefer: ["inline", "inline-big"],
}),
el,
];
} else {
return el;
}
}
});
return () =>
h(
defaultStore.state.animation ? TransitionGroup : "div",
defaultStore.state.animation
? {
class: "sqadhkmv" + (props.noGap ? " noGap" : ""),
name: "list",
tag: "div",
"data-direction": props.direction,
"data-reversed": props.reversed ? "true" : "false",
}
: {
class: "sqadhkmv" + (props.noGap ? " noGap" : ""),
},
{ default: renderChildren },
);
},
});
function getDateText(time: string) {
const date = new Date(time).getDate();
const month = new Date(time).getMonth() + 1;
return i18n.t("monthAndDay", {
month: month.toString(),
day: date.toString(),
});
}
</script>
<style lang="scss">

View File

@ -1,7 +1,7 @@
<template>
<div>
<MkPagination
v-slot="{ items }"
v-slot="{ items }: { items: entities.DriveFile[]}"
:pagination="pagination"
class="urempief"
:class="{ grid: viewMode === 'grid' }"
@ -53,13 +53,15 @@
<script lang="ts" setup>
import { acct } from "firefish-js";
import type { entities } from "firefish-js";
import MkPagination from "@/components/MkPagination.vue";
import type { PagingOf } from "@/components/MkPagination.vue";
import MkDriveFileThumbnail from "@/components/MkDriveFileThumbnail.vue";
import bytes from "@/filters/bytes";
import { i18n } from "@/i18n";
defineProps<{
pagination: any;
pagination: PagingOf<entities.DriveFile>;
viewMode: "grid" | "list";
}>();
</script>

View File

@ -40,17 +40,18 @@
<script lang="ts" setup>
import { ref } from "vue";
import type { Paging } from "@/components/MkPagination.vue";
import type { PagingOf } from "@/components/MkPagination.vue";
import XNote from "@/components/MkNote.vue";
import XList from "@/components/MkDateSeparatedList.vue";
import MkPagination from "@/components/MkPagination.vue";
import { i18n } from "@/i18n";
import { scroll } from "@/scripts/scroll";
import type { entities } from "firefish-js";
const tlEl = ref<HTMLElement>();
defineProps<{
pagination: Paging;
pagination: PagingOf<entities.Note>;
noGap?: boolean;
disableAutoLoad?: boolean;
}>();

View File

@ -19,12 +19,8 @@
:no-gap="true"
>
<XNote
v-if="
['reply', 'quote', 'mention'].includes(
notification.type,
)
"
:key="notification.id"
v-if="isNoteNotification(notification)"
:key="'nn-' + notification.id"
:note="notification.note"
:collapsed-reply="
notification.type === 'reply' ||
@ -34,7 +30,7 @@
/>
<XNotification
v-else
:key="notification.id"
:key="'n-' + notification.id"
:notification="notification"
:with-time="true"
:full="true"
@ -47,8 +43,7 @@
<script lang="ts" setup>
import { computed, onMounted, onUnmounted, ref } from "vue";
import type { notificationTypes } from "firefish-js";
import type { Paging } from "@/components/MkPagination.vue";
import type { StreamTypes, entities, notificationTypes } from "firefish-js";
import MkPagination from "@/components/MkPagination.vue";
import XNotification from "@/components/MkNotification.vue";
import XList from "@/components/MkDateSeparatedList.vue";
@ -66,20 +61,29 @@ const stream = useStream();
const pagingComponent = ref<InstanceType<typeof MkPagination>>();
const pagination: Paging = {
const pagination = {
endpoint: "i/notifications" as const,
limit: 10,
params: computed(() => ({
includeTypes: props.includeTypes ?? undefined,
excludeTypes: props.includeTypes ? undefined : me.mutingNotificationTypes,
excludeTypes: props.includeTypes ? undefined : me?.mutingNotificationTypes,
unreadOnly: props.unreadOnly,
})),
};
const onNotification = (notification) => {
function isNoteNotification(
n: entities.Notification,
): n is
| entities.ReplyNotification
| entities.QuoteNotification
| entities.MentionNotification {
return n.type === "reply" || n.type === "quote" || n.type === "mention";
}
const onNotification = (notification: entities.Notification) => {
const isMuted = props.includeTypes
? !props.includeTypes.includes(notification.type)
: me.mutingNotificationTypes.includes(notification.type);
: me?.mutingNotificationTypes.includes(notification.type);
if (isMuted || document.visibilityState === "visible") {
stream.send("readNotification", {
id: notification.id,
@ -94,7 +98,7 @@ const onNotification = (notification) => {
}
};
let connection;
let connection: StreamTypes.ChannelOf<"main"> | undefined;
onMounted(() => {
connection = stream.useChannel("main");

View File

@ -66,10 +66,10 @@
</transition>
</template>
<script lang="ts" setup>
<script lang="ts" setup generic="E extends PagingKey">
import type { ComputedRef } from "vue";
import { computed, isRef, onActivated, onDeactivated, ref, watch } from "vue";
import type { Endpoints } from "firefish-js";
import type { Endpoints, TypeUtils } from "firefish-js";
import * as os from "@/os";
import {
getScrollContainer,
@ -81,7 +81,10 @@ import MkButton from "@/components/MkButton.vue";
import { i18n } from "@/i18n";
import { defaultStore } from "@/store";
export interface Paging<E extends keyof Endpoints = keyof Endpoints> {
// biome-ignore lint/suspicious/noExplicitAny: Used Intentionally
export type PagingKey = TypeUtils.EndpointsOf<any[]>;
export interface Paging<E extends PagingKey = PagingKey> {
endpoint: E;
limit: number;
params?: Endpoints[E]["req"] | ComputedRef<Endpoints[E]["req"]>;
@ -100,11 +103,13 @@ export interface Paging<E extends keyof Endpoints = keyof Endpoints> {
offsetMode?: boolean;
}
export type PagingOf<T> = Paging<TypeUtils.EndpointsOf<T[]>>;
const SECOND_FETCH_LIMIT = 30;
const props = withDefaults(
defineProps<{
pagination: Paging;
pagination: Paging<E>;
disableAutoLoad?: boolean;
displayLimit?: number;
}>(),
@ -113,14 +118,18 @@ const props = withDefaults(
},
);
const emit = defineEmits<{
(ev: "queue", count: number): void;
(ev: "status", error: boolean): void;
const slots = defineSlots<{
default(props: { items: Item[] }): unknown;
empty(props: Record<string, never>): never;
}>();
type Item = Endpoints[typeof props.pagination.endpoint]["res"] & {
id: string;
};
const emit = defineEmits<{
(ev: "queue", count: number): void;
(ev: "status", hasError: boolean): void;
}>();
type Param = Endpoints[E]["req"] | Record<string, never>;
type Item = Endpoints[E]["res"][number];
const rootEl = ref<HTMLElement>();
const items = ref<Item[]>([]);
@ -137,8 +146,9 @@ const error = ref(false);
const init = async (): Promise<void> => {
queue.value = [];
fetching.value = true;
const params = props.pagination.params
? isRef(props.pagination.params)
? isRef<Param>(props.pagination.params)
? props.pagination.params.value
: props.pagination.params
: {};
@ -150,7 +160,7 @@ const init = async (): Promise<void> => {
: (props.pagination.limit || 10) + 1,
})
.then(
(res) => {
(res: Item[]) => {
for (let i = 0; i < res.length; i++) {
const item = res[i];
if (props.pagination.reversed) {
@ -174,7 +184,7 @@ const init = async (): Promise<void> => {
error.value = false;
fetching.value = false;
},
(err) => {
(_err) => {
error.value = true;
fetching.value = false;
},
@ -188,7 +198,7 @@ const reload = (): Promise<void> => {
const refresh = async (): Promise<void> => {
const params = props.pagination.params
? isRef(props.pagination.params)
? isRef<Param>(props.pagination.params)
? props.pagination.params.value
: props.pagination.params
: {};
@ -199,7 +209,7 @@ const refresh = async (): Promise<void> => {
offset: 0,
})
.then(
(res) => {
(res: Item[]) => {
const ids = items.value.reduce(
(a, b) => {
a[b.id] = true;
@ -210,7 +220,7 @@ const refresh = async (): Promise<void> => {
for (let i = 0; i < res.length; i++) {
const item = res[i];
if (!updateItem(item.id, (old) => item)) {
if (!updateItem(item.id, (_old) => item)) {
append(item);
}
delete ids[item.id];
@ -220,7 +230,7 @@ const refresh = async (): Promise<void> => {
removeItem((i) => i.id === id);
}
},
(err) => {
(_err) => {
error.value = true;
fetching.value = false;
},
@ -238,7 +248,7 @@ const fetchMore = async (): Promise<void> => {
moreFetching.value = true;
backed.value = true;
const params = props.pagination.params
? isRef(props.pagination.params)
? isRef<Param>(props.pagination.params)
? props.pagination.params.value
: props.pagination.params
: {};
@ -259,7 +269,7 @@ const fetchMore = async (): Promise<void> => {
}),
})
.then(
(res) => {
(res: Item[]) => {
for (let i = 0; i < res.length; i++) {
const item = res[i];
if (props.pagination.reversed) {
@ -283,7 +293,7 @@ const fetchMore = async (): Promise<void> => {
offset.value += res.length;
moreFetching.value = false;
},
(err) => {
(_err) => {
moreFetching.value = false;
},
);
@ -299,7 +309,7 @@ const fetchMoreAhead = async (): Promise<void> => {
return;
moreFetching.value = true;
const params = props.pagination.params
? isRef(props.pagination.params)
? isRef<Param>(props.pagination.params)
? props.pagination.params.value
: props.pagination.params
: {};
@ -320,7 +330,7 @@ const fetchMoreAhead = async (): Promise<void> => {
}),
})
.then(
(res) => {
(res: Item[]) => {
if (res.length > SECOND_FETCH_LIMIT) {
res.pop();
items.value = props.pagination.reversed
@ -336,7 +346,7 @@ const fetchMoreAhead = async (): Promise<void> => {
offset.value += res.length;
moreFetching.value = false;
},
(err) => {
(_err) => {
moreFetching.value = false;
},
);
@ -428,7 +438,7 @@ const updateItem = (id: Item["id"], replacer: (old: Item) => Item): boolean => {
return true;
};
if (props.pagination.params && isRef(props.pagination.params)) {
if (props.pagination.params && isRef<Param>(props.pagination.params)) {
watch(props.pagination.params, init, { deep: true });
}

View File

@ -44,7 +44,7 @@
<script lang="ts" setup>
import { computed, onUnmounted, provide, ref } from "vue";
import type { Endpoints } from "firefish-js";
import type { entities, StreamTypes } from "firefish-js";
import MkPullToRefresh from "@/components/MkPullToRefresh.vue";
import XNotes from "@/components/MkNotes.vue";
import MkInfo from "@/components/MkInfo.vue";
@ -54,10 +54,23 @@ import { isSignedIn, me } from "@/me";
import { i18n } from "@/i18n";
import { defaultStore } from "@/store";
import icon from "@/scripts/icon";
import type { Paging } from "@/components/MkPagination.vue";
import type { EndpointsOf } from "@/components/MkPagination.vue";
export type TimelineSource =
| "antenna"
| "home"
| "local"
| "recommended"
| "social"
| "global"
| "mentions"
| "directs"
| "list"
| "channel"
| "file";
const props = defineProps<{
src: string;
src: TimelineSource;
list?: string;
antenna?: string;
channel?: string;
@ -73,7 +86,7 @@ const emit = defineEmits<{
const tlComponent = ref<InstanceType<typeof XNotes>>();
const pullToRefreshComponent = ref<InstanceType<typeof MkPullToRefresh>>();
let endpoint = ""; // keyof Endpoints
let endpoint: EndpointsOf<entities.Note[]>; // keyof Endpoints
let query: {
antennaId?: string | undefined;
withReplies?: boolean;
@ -81,14 +94,19 @@ let query: {
listId?: string | undefined;
channelId?: string | undefined;
fileId?: string | undefined;
};
let connection: {
on: (
arg0: string,
arg1: { (note: any): void; (note: any): void; (note: any): void },
) => void;
dispose: () => void;
};
} = {};
// FIXME: The type defination is wrong here, need fix
let connection:
| StreamTypes.ChannelOf<"antenna">
| StreamTypes.ChannelOf<"homeTimeline">
| StreamTypes.ChannelOf<"recommendedTimeline">
| StreamTypes.ChannelOf<"hybridTimeline">
| StreamTypes.ChannelOf<"globalTimeline">
| StreamTypes.ChannelOf<"main">
| StreamTypes.ChannelOf<"userList">
| StreamTypes.ChannelOf<"channel">;
let connection2: { dispose: () => void } | null;
let tlHint: string;
@ -96,14 +114,14 @@ let tlHintClosed: boolean;
let tlNotesCount = 0;
const queue = ref(0);
const prepend = (note) => {
const prepend = (note: entities.Note) => {
tlNotesCount++;
tlComponent.value?.pagingComponent?.prepend(note);
emit("note");
if (props.sound) {
sound.play(isSignedIn && note.userId === me.id ? "noteMy" : "note");
sound.play(isSignedIn && note.userId === me?.id ? "noteMy" : "note");
}
};
@ -169,6 +187,8 @@ if (props.src === "antenna") {
query = {
fileId: props.fileId,
};
} else {
throw "NoEndpointError";
}
const stream = useStream();
@ -194,8 +214,9 @@ function connectChannel() {
}
if (props.src === "antenna") {
if (!props.antenna) throw "NoAntennaProvided";
connection = stream.useChannel("antenna", {
antennaId: props.antenna!,
antennaId: props.antenna,
});
} else if (props.src === "home") {
connection = stream.useChannel("homeTimeline", {
@ -272,8 +293,8 @@ function reloadTimeline() {
});
}
const pagination: Paging = {
endpoint: endpoint as keyof Endpoints,
const pagination = {
endpoint,
limit: 10,
params: query,
};

View File

@ -36,7 +36,7 @@ const props = defineProps<{
}>();
const emit = defineEmits<{
(ev: "update:modelValue", v: boolean): void;
"update:modelValue": [v: boolean];
}>();
const button = ref<HTMLElement>();
@ -46,9 +46,9 @@ const toggle = () => {
emit("update:modelValue", !checked.value);
if (!checked.value) {
const rect = button.value.getBoundingClientRect();
const x = rect.left + button.value.offsetWidth / 2;
const y = rect.top + button.value.offsetHeight / 2;
const rect = button.value!.getBoundingClientRect();
const x = rect.left + button.value!.offsetWidth / 2;
const y = rect.top + button.value!.offsetHeight / 2;
os.popup(Ripple, { x, y, particle: false }, {}, "end");
}
};

View File

@ -26,7 +26,7 @@
@input="onInput"
/>
<datalist v-if="datalist" :id="id">
<option v-for="data in datalist" :value="data" />
<option v-for="data in datalist" :key="data" :value="data" />
</datalist>
<div ref="suffixEl" class="suffix">
<slot name="suffix"></slot>
@ -47,7 +47,7 @@
<script lang="ts" setup>
import { nextTick, onMounted, ref, toRefs, watch } from "vue";
import { debounce } from "throttle-debounce";
import { debounce as Debounce } from "throttle-debounce";
import MkButton from "@/components/MkButton.vue";
import { useInterval } from "@/scripts/use-interval";
import { i18n } from "@/i18n";
@ -72,7 +72,7 @@ const props = defineProps<{
autofocus?: boolean;
autocomplete?: string;
spellcheck?: boolean;
step?: any;
step?: number | string;
datalist?: string[];
inline?: boolean;
debounce?: boolean;
@ -82,10 +82,10 @@ const props = defineProps<{
}>();
const emit = defineEmits<{
(ev: "change", _ev: KeyboardEvent): void;
(ev: "change", _ev: Event): void;
(ev: "keydown", _ev: KeyboardEvent): void;
(ev: "enter"): void;
(ev: "update:modelValue", value: string | number): void;
(ev: "update:modelValue", value: string | number | null): void;
}>();
const { modelValue, type, autofocus } = toRefs(props);
@ -94,14 +94,15 @@ const id = Math.random().toString(); // TODO: uuid?
const focused = ref(false);
const changed = ref(false);
const invalid = ref(false);
const inputEl = ref<HTMLElement>();
const inputEl = ref<HTMLInputElement>();
const prefixEl = ref<HTMLElement>();
const suffixEl = ref<HTMLElement>();
const height = props.small ? 36 : props.large ? 40 : 38;
const focus = () => inputEl.value.focus();
const selectRange = (start, end) => inputEl.value.setSelectionRange(start, end);
const onInput = (ev: KeyboardEvent) => {
const focus = () => inputEl.value!.focus();
const selectRange = (start, end) =>
inputEl.value!.setSelectionRange(start, end);
const onInput = (ev: Event) => {
changed.value = true;
emit("change", ev);
};
@ -116,13 +117,13 @@ const onKeydown = (ev: KeyboardEvent) => {
const updated = () => {
changed.value = false;
if (type.value === "number") {
emit("update:modelValue", parseFloat(v.value));
emit("update:modelValue", Number.parseFloat(v.value as string));
} else {
emit("update:modelValue", v.value);
}
};
const debouncedUpdated = debounce(1000, updated);
const debouncedUpdated = Debounce(1000, updated);
watch(modelValue, (newValue) => {
v.value = newValue;
@ -137,7 +138,7 @@ watch(v, (_) => {
}
}
invalid.value = inputEl.value.validity.badInput;
invalid.value = inputEl.value!.validity.badInput;
});
//
@ -146,12 +147,12 @@ useInterval(
() => {
if (prefixEl.value) {
if (prefixEl.value.offsetWidth) {
inputEl.value.style.paddingLeft = prefixEl.value.offsetWidth + "px";
inputEl.value!.style.paddingLeft = `${prefixEl.value.offsetWidth}px`;
}
}
if (suffixEl.value) {
if (suffixEl.value.offsetWidth) {
inputEl.value.style.paddingRight = suffixEl.value.offsetWidth + "px";
inputEl.value!.style.paddingRight = `${suffixEl.value.offsetWidth}px`;
}
}
},

View File

@ -16,19 +16,22 @@
<script lang="ts" setup>
import { computed } from "vue";
// biome-ignore lint/suspicious/noExplicitAny: FIXME
type ValueType = any;
const props = defineProps<{
modelValue: any;
value: any;
modelValue: ValueType;
value: ValueType;
disabled: boolean;
}>();
const emit = defineEmits<{
(ev: "update:modelValue", value: any): void;
"update:modelValue": [value: ValueType];
}>();
const checked = computed(() => props.modelValue === props.value);
function toggle(x) {
function toggle(_ev: Event) {
if (props.disabled) return;
emit("update:modelValue", props.value);
}

View File

@ -1,5 +1,5 @@
<script lang="ts">
import { defineComponent, h } from "vue";
import { type VNode, defineComponent, h } from "vue";
import MkRadio from "./radio.vue";
export default defineComponent({
@ -22,13 +22,17 @@ export default defineComponent({
},
},
render() {
let options = this.$slots.default();
const label = this.$slots.label && this.$slots.label();
const caption = this.$slots.caption && this.$slots.caption();
let options = this.$slots.default!();
const label = this.$slots.label && this.$slots.label!();
const caption = this.$slots.caption && this.$slots.caption!();
// Fragment
if (options.length === 1 && options[0].props == null)
options = options[0].children;
if (
options.length === 1 &&
options[0].props == null &&
Array.isArray(options[0].children)
)
options = options[0].children as VNode[];
return h(
"fieldset",
@ -56,11 +60,15 @@ export default defineComponent({
h(
MkRadio,
{
// FIXME: It seems that there is a type error
key: option.key,
value: option.props?.value,
disabled: option.props?.disabled,
modelValue: this.value,
"onUpdate:modelValue": (value) => (this.value = value),
"onUpdate:modelValue": (value) => {
this.value = value;
return value;
},
},
option.children,
),
@ -83,7 +91,7 @@ export default defineComponent({
});
</script>
<style lang="scss">
<style lang="scss" scoped>
.novjtcto {
border: 0;
padding: 0;

View File

@ -21,7 +21,7 @@
@mouseleave="tooltipHide"
@input="
(x) => {
inputVal = x.target.value;
inputVal = Number((x.target as HTMLInputElement).value);
if (instant) onChange(x);
}
"
@ -29,6 +29,7 @@
<datalist v-if="showTicks && steps" :id="id">
<option
v-for="i in steps"
:key="`step-${i}`"
:value="i + min"
:label="(i + min).toString()"
></option>
@ -69,11 +70,11 @@ const props = withDefaults(
},
);
const inputEl = ref<HTMLElement>();
const inputEl = ref<HTMLInputElement>();
const inputVal = ref(props.modelValue);
const emit = defineEmits<{
(ev: "update:modelValue", value: number): void;
"update:modelValue": [value: number];
}>();
const steps = computed(() => {
@ -84,7 +85,7 @@ const steps = computed(() => {
}
});
function onChange(x) {
function onChange(_x) {
emit("update:modelValue", inputVal.value);
}

View File

@ -58,6 +58,7 @@ import * as os from "@/os";
import { useInterval } from "@/scripts/use-interval";
import { i18n } from "@/i18n";
import icon from "@/scripts/icon";
import type { MenuItem } from "@/types/menu";
const props = defineProps<{
modelValue: string | null;
@ -85,13 +86,13 @@ const focused = ref(false);
const opening = ref(false);
const changed = ref(false);
const invalid = ref(false);
const inputEl = ref(null);
const prefixEl = ref(null);
const suffixEl = ref(null);
const container = ref(null);
const inputEl = ref<HTMLInputElement | null>(null);
const prefixEl = ref<HTMLElement | null>(null);
const suffixEl = ref<HTMLElement | null>(null);
const container = ref<HTMLElement | null>(null);
const height = props.small ? 33 : props.large ? 39 : 36;
const focus = () => inputEl.value.focus();
const focus = () => inputEl.value!.focus();
const onInput = (ev) => {
changed.value = true;
emit("change", ev);
@ -106,26 +107,27 @@ watch(modelValue, (newValue) => {
v.value = newValue;
});
watch(v, (newValue) => {
watch(v, (_newValue) => {
if (!props.manualSave) {
updated();
}
invalid.value = inputEl.value.validity.badInput;
invalid.value = inputEl.value!.validity.badInput;
});
//
// 0
useInterval(
() => {
if (inputEl.value == null) return;
if (prefixEl.value) {
if (prefixEl.value.offsetWidth) {
inputEl.value.style.paddingLeft = prefixEl.value.offsetWidth + "px";
inputEl.value.style.paddingLeft = `${prefixEl.value.offsetWidth}px`;
}
}
if (suffixEl.value) {
if (suffixEl.value.offsetWidth) {
inputEl.value.style.paddingRight = suffixEl.value.offsetWidth + "px";
inputEl.value.style.paddingRight = `${suffixEl.value.offsetWidth}px`;
}
}
},
@ -144,19 +146,19 @@ onMounted(() => {
});
});
function show(ev: MouseEvent) {
function show(_ev: MouseEvent) {
focused.value = true;
opening.value = true;
const menu = [];
const menu: MenuItem[] = [];
const options = slots.default!();
const pushOption = (option: VNode) => {
menu.push({
text: option.children,
active: computed(() => v.value === option.props.value),
text: option.children as string,
active: computed(() => v.value === option.props?.value).value,
action: () => {
v.value = option.props.value;
v.value = option.props?.value;
},
});
};
@ -167,13 +169,13 @@ function show(ev: MouseEvent) {
const optgroup = vnode;
menu.push({
type: "label",
text: optgroup.props.label,
text: optgroup.props?.label,
});
scanOptions(optgroup.children);
scanOptions(optgroup.children as VNode[]);
} else if (Array.isArray(vnode.children)) {
//
const fragment = vnode;
scanOptions(fragment.children);
scanOptions(fragment.children as VNode[]);
} else if (vnode.props == null) {
// v-if false
// nop?
@ -186,12 +188,13 @@ function show(ev: MouseEvent) {
scanOptions(options);
os.popupMenu(menu, container.value, {
width: container.value.offsetWidth,
onClosing: () => {
opening.value = false;
},
os.popupMenu(menu, container.value!, {
width: container.value!.offsetWidth,
// onClosing: () => {
// opening.value = false;
// },
}).then(() => {
opening.value = false;
focused.value = false;
});
}

View File

@ -14,7 +14,7 @@ const props = withDefaults(
},
);
const minWidth = props.minWidth + "px";
const minWidth = `${props.minWidth}px`;
</script>
<style lang="scss" scoped>

View File

@ -39,12 +39,13 @@ export default defineComponent({
props: {
p: {
// biome-ignore lint/suspicious/noExplicitAny: FIXME
type: Function as PropType<() => Promise<any>>,
required: true,
},
},
setup(props, context) {
setup(props, _context) {
const pending = ref(true);
const resolved = ref(false);
const rejected = ref(false);

View File

@ -2,7 +2,7 @@
<label class="ziffeomt">
<input
type="checkbox"
:checked="modelValue"
:checked="toValue(modelValue)"
:disabled="disabled"
@change="(x) => toggle(x)"
/>
@ -18,7 +18,7 @@
</template>
<script lang="ts" setup>
import type { Ref } from "vue";
import { type Ref, toValue } from "vue";
const props = defineProps<{
modelValue: boolean | Ref<boolean>;
@ -26,12 +26,12 @@ const props = defineProps<{
}>();
const emit = defineEmits<{
(ev: "update:modelValue", v: boolean): void;
"update:modelValue": [v: boolean];
}>();
function toggle(x) {
function toggle(x: Event) {
if (props.disabled) return;
emit("update:modelValue", x.target.checked);
emit("update:modelValue", (x.target as HTMLInputElement).checked);
}
</script>

View File

@ -60,6 +60,7 @@ export default defineComponent({
props: {
modelValue: {
type: String,
required: true,
},
type: {
@ -92,9 +93,11 @@ export default defineComponent({
default: false,
},
autocomplete: {
type: String,
required: false,
},
spellcheck: {
type: Boolean,
required: false,
},
code: {
@ -132,9 +135,9 @@ export default defineComponent({
const changed = ref(false);
const invalid = ref(false);
const filled = computed(() => v.value !== "" && v.value != null);
const inputEl = ref(null);
const inputEl = ref<HTMLTextAreaElement | null>(null);
const focus = () => inputEl.value.focus();
const focus = () => inputEl.value!.focus();
const onInput = (ev) => {
changed.value = true;
context.emit("change", ev);
@ -167,7 +170,7 @@ export default defineComponent({
}
}
invalid.value = inputEl.value.validity.badInput;
invalid.value = inputEl.value!.validity.badInput;
});
onMounted(() => {

View File

@ -1,7 +1,7 @@
<template>
<div
v-for="chosenItem in chosen"
v-if="chosen && chosen.length > 0 && defaultStore.state.showAds"
v-if="chosen && Array.isArray(chosen) && chosen.length > 0 && defaultStore.state.showAds"
class="qiivuoyo"
>
<div v-if="!showMenu" class="main" :class="chosenItem.place">
@ -10,7 +10,7 @@
</a>
</div>
</div>
<div v-else-if="chosen && defaultStore.state.showAds" class="qiivuoyo">
<div v-else-if="chosen && !Array.isArray(chosen) && defaultStore.state.showAds" class="qiivuoyo">
<div v-if="!showMenu" class="main" :class="chosen.place">
<a :href="chosen.url" target="_blank">
<img :src="chosen.imageUrl" />
@ -60,7 +60,7 @@ const toggleMenu = (): void => {
showMenu.value = !showMenu.value;
};
const choseAd = (): Ad | null => {
const choseAd = (): Ad | Ad[] | null => {
if (props.specify) {
return props.specify;
}
@ -113,6 +113,7 @@ const chosen = ref(choseAd());
function reduceFrequency(): void {
if (chosen.value == null) return;
if (Array.isArray(chosen.value)) return;
if (defaultStore.state.mutedAds.includes(chosen.value.id)) return;
defaultStore.push("mutedAds", chosen.value.id);
os.success();

View File

@ -98,7 +98,7 @@ const props = withDefaults(
);
const emit = defineEmits<{
(ev: "click", v: MouseEvent): void;
click: [v: MouseEvent];
}>();
const url = computed(() =>
@ -123,18 +123,18 @@ watch(
},
);
const gallery = ref(null);
const gallery = ref<HTMLElement | null>(null);
onMounted(() => {
const lightbox = new PhotoSwipeLightbox({
dataSource: [
{
src: url,
src: url.value,
w: 300,
h: 300,
},
],
gallery: gallery.value,
gallery: gallery.value || undefined,
children: ".avatar",
thumbSelector: ".avatar",
loop: false,
@ -174,7 +174,7 @@ onMounted(() => {
history.pushState(null, "", location.href);
addEventListener("popstate", close);
// This is a workaround. Not sure why, but when clicking to open, it doesn't move focus to the photoswipe. Preventing using esc to close. However when using keyboard to open it already focuses the lightbox fine.
lightbox.pswp.element.focus();
lightbox.pswp?.element?.focus();
});
lightbox.on("close", () => {
removeEventListener("popstate", close);
@ -186,7 +186,7 @@ onMounted(() => {
function close() {
removeEventListener("popstate", close);
history.forward();
lightbox.pswp.close();
lightbox.pswp?.close();
}
});
</script>

View File

@ -4,16 +4,16 @@
class="mk-emoji custom"
:class="{ normal, noStyle }"
:src="url"
:alt="alt"
:title="alt"
:alt="alt || undefined"
:title="alt || undefined"
decoding="async"
/>
<img
v-else-if="char && !useOsNativeEmojis"
class="mk-emoji"
:src="url"
:alt="alt"
:title="alt"
:alt="alt || undefined"
:title="alt || undefined"
decoding="async"
/>
<span v-else-if="char && useOsNativeEmojis">{{ char }}</span>
@ -32,7 +32,7 @@ const props = defineProps<{
emoji: string;
normal?: boolean;
noStyle?: boolean;
customEmojis?: entities.CustomEmoji[];
customEmojis?: entities.EmojiLite[];
isReaction?: boolean;
}>();
@ -50,6 +50,7 @@ const customEmoji = computed(() =>
: null,
);
const url = computed(() => {
if (!customEmoji.value) return undefined;
if (char.value) {
return char2filePath(char.value);
} else {

View File

@ -31,7 +31,7 @@
</template>
<script lang="ts" setup>
const props = withDefaults(
withDefaults(
defineProps<{
inline?: boolean;
colored?: boolean;

View File

@ -19,22 +19,22 @@
import {} from "vue";
import MfmCore from "@/components/mfm";
import { defaultStore } from "@/store";
import type { entities } from "firefish-js";
const props = withDefaults(
withDefaults(
defineProps<{
text: string;
plain?: boolean;
nowrap?: boolean;
author?: any;
customEmojis?: any;
author?: entities.User;
customEmojis?: entities.EmojiLite[];
isNote?: boolean;
advancedMfm: boolean;
advancedMfm?: boolean;
lang?: string;
}>(),
{
plain: false,
nowrap: false,
author: null,
isNote: true,
},
);

View File

@ -3,8 +3,8 @@
v-if="show"
ref="el"
class="fdidabkb"
:class="{ thin: thin_, tabs: tabs?.length > 0 }"
:style="{ background: bg }"
:class="{ thin: thin_, tabs: isTabs(tabs)}"
:style="{ background: bg || undefined }"
@click="onClick"
>
<div class="left">
@ -70,14 +70,14 @@
</div>
<template v-if="metadata">
<nav
v-if="hasTabs"
v-if="isTabs(tabs)"
ref="tabsEl"
class="tabs"
:class="{ collapse: hasTabs && tabs.length > 3 }"
:class="{ collapse: tabs.length > 3 }"
>
<button
v-for="tab in tabs"
:ref="(el) => (tabRefs[tab.key] = el)"
:ref="(el) => (tab.key && (tabRefs[tab.key] = el))"
v-tooltip.noDelay="tab.title"
v-vibrate="5"
class="tab _button"
@ -157,6 +157,7 @@ const props = defineProps<{
actions?: {
text: string;
icon: string;
highlighted?: boolean;
handler: (ev: MouseEvent) => void;
}[];
thin?: boolean;
@ -171,7 +172,7 @@ const displayBackButton =
inject("shouldBackButton", true);
const emit = defineEmits<{
(ev: "update:tab", key: string);
"update:tab": [key: string];
}>();
const metadata = injectPageMetadata();
@ -183,9 +184,14 @@ const el = ref<HTMLElement | null>(null);
const tabRefs = {};
const tabHighlightEl = ref<HTMLElement | null>(null);
const tabsEl = ref<HTMLElement | null>(null);
const bg = ref(null);
const bg = ref<string | null | number>(null);
const narrow = ref(false);
const hasTabs = computed(() => props.tabs && props.tabs.length > 0);
function isTabs(t: Tab[] | undefined): t is Tab[] {
return t != null && t.length > 0;
}
const hasActions = computed(() => props.actions && props.actions.length > 0);
const show = computed(() => {
return !hideTitle || hasTabs.value || hasActions.value;
@ -201,7 +207,7 @@ const openAccountMenu = (ev: MouseEvent) => {
};
const showTabsPopup = (ev: MouseEvent) => {
if (!hasTabs.value) return;
if (!isTabs(props.tabs)) return;
if (!narrow.value) return;
ev.preventDefault();
ev.stopPropagation();
@ -213,7 +219,7 @@ const showTabsPopup = (ev: MouseEvent) => {
onTabClick(tab, ev);
},
}));
popupMenu(menu, ev.currentTarget ?? ev.target);
popupMenu(menu, (ev.currentTarget ?? ev.target) as HTMLElement);
};
const preventDrag = (ev: TouchEvent) => {
@ -224,7 +230,9 @@ const onClick = () => {
if (props.to) {
location.href = props.to;
} else {
scrollToTop(el.value, { behavior: "smooth" });
if (el.value) {
scrollToTop(el.value, { behavior: "smooth" });
}
}
};
@ -257,6 +265,8 @@ onMounted(() => {
() => [props.tab, props.tabs],
() => {
nextTick(() => {
if (props.tab == null) return;
if (!isTabs(props.tabs)) return;
const tabEl = tabRefs[props.tab];
if (tabEl && tabHighlightEl.value) {
// offsetWidth offsetLeft getBoundingClientRect 使
@ -266,7 +276,8 @@ onMounted(() => {
tabEl.style = `--width: ${tabSizeX}px`;
}
setTimeout(() => {
tabHighlightEl.value.style.width = tabSizeX + "px";
if (tabHighlightEl.value == null) return;
tabHighlightEl.value.style.width = `${tabSizeX}px`;
tabHighlightEl.value.style.transform = `translateX(${tabEl.offsetLeft}px)`;
window.requestAnimationFrame(() => {
tabsEl.value?.scrollTo({
@ -283,10 +294,10 @@ onMounted(() => {
},
);
if (el.value && el.value.parentElement) {
if (el.value?.parentElement) {
narrow.value = el.value.parentElement.offsetWidth < 500;
ro = new ResizeObserver((entries, observer) => {
if (el.value.parentElement && document.body.contains(el.value)) {
ro = new ResizeObserver((_entries, _observer) => {
if (el.value?.parentElement && document.body.contains(el.value)) {
narrow.value = el.value.parentElement.offsetWidth < 500;
}
});

View File

@ -28,8 +28,8 @@ const parentStickyTop = inject<Ref<number>>(CURRENT_STICKY_TOP, ref(0));
provide(CURRENT_STICKY_TOP, childStickyTop);
const calc = () => {
childStickyTop.value = parentStickyTop.value + headerEl.value.offsetHeight;
headerHeight.value = headerEl.value.offsetHeight.toString();
childStickyTop.value = parentStickyTop.value + headerEl.value!.offsetHeight;
headerHeight.value = headerEl.value!.offsetHeight.toString();
};
const observer = new ResizeObserver(() => {
@ -46,7 +46,7 @@ onMounted(() => {
watch(
childStickyTop,
() => {
bodyEl.value.style.setProperty(
bodyEl.value!.style.setProperty(
"--stickyTop",
`${childStickyTop.value}px`,
);
@ -56,7 +56,7 @@ onMounted(() => {
},
);
observer.observe(headerEl.value);
observer.observe(headerEl.value!);
});
onUnmounted(() => {

View File

@ -27,7 +27,7 @@ const props = withDefaults(
const _time =
props.time == null
? NaN
? Number.NaN
: typeof props.time === "number"
? props.time
: (props.time instanceof Date

View File

@ -38,13 +38,14 @@ export default defineComponent({
return h(
this.tag,
parsed.map((x) =>
typeof x === "string"
? this.textTag
? h(this.textTag, x)
: x
: this.$slots[x.arg](),
),
parsed.map((x) => {
if (typeof x === "string") {
return this.textTag ? h(this.textTag, x) : x;
} else {
const t = this.$slots[x.arg];
return t ? t() : `I18n[${x.arg}]`;
}
}),
);
},
});

View File

@ -1,6 +1,6 @@
import { defineComponent, h } from "vue";
import * as mfm from "mfm-js";
import type { VNode } from "vue";
import type { VNode, PropType } from "vue";
import MkUrl from "@/components/global/MkUrl.vue";
import MkLink from "@/components/MkLink.vue";
import MkMention from "@/components/MkMention.vue";
@ -14,6 +14,7 @@ import MkA from "@/components/global/MkA.vue";
import { host } from "@/config";
import { reducedMotion } from "@/scripts/reduced-motion";
import { defaultStore } from "@/store";
import type { entities } from "firefish-js";
export default defineComponent({
props: {
@ -38,6 +39,7 @@ export default defineComponent({
default: null,
},
customEmojis: {
type: Array as PropType<entities.EmojiLite[]>,
required: false,
},
isNote: {

View File

@ -855,8 +855,8 @@ export function popupMenu(
noReturnFocus?: boolean;
},
) {
return new Promise((resolve, reject) => {
let dispose;
return new Promise<void>((resolve, _reject) => {
let dispose: () => void;
popup(
defineAsyncComponent({
loader: () => import("@/components/MkPopupMenu.vue"),

View File

@ -113,10 +113,11 @@ import MkInstanceCardMini from "@/components/MkInstanceCardMini.vue";
import FormSplit from "@/components/form/split.vue";
import { i18n } from "@/i18n";
import icon from "@/scripts/icon";
import type { instanceSortParam } from "firefish-js";
const host = ref("");
const state = ref("federating");
const sort = ref("+pubSub");
const sort = ref<(typeof instanceSortParam)[number]>("+pubSub");
const pagination = {
endpoint: "federation/instances" as const,
limit: 10,

View File

@ -94,7 +94,7 @@ const origin = ref("local");
const type = ref(null);
const searchHost = ref("");
const userId = ref("");
const viewMode = ref("grid");
const viewMode = ref<"list" | "grid">("grid");
const pagination = {
endpoint: "admin/drive/files" as const,
limit: 10,

View File

@ -313,7 +313,7 @@ const isSilenced = ref(false);
const faviconUrl = ref<string | null>(null);
const usersPagination = {
endpoint: isAdmin ? "admin/show-users" : ("users" as const),
endpoint: isAdmin ? ("admin/show-users" as const) : ("users" as const),
limit: 10,
params: {
sort: "+updatedAt",

View File

@ -8,12 +8,12 @@
<MkPagination
v-else
ref="pagingComponent"
v-slot="{ items }: { items: entities.NoteEdit[] }"
v-slot="{ items }"
:pagination="pagination"
>
<div ref="tlEl" class="giivymft noGap">
<XList
v-slot="{ item }: { item: entities.Note }"
v-slot="{ item }"
:items="convertNoteEditsToNotes(items)"
class="notes"
:no-gap="true"
@ -35,7 +35,6 @@
<script lang="ts" setup>
import { computed, onMounted, ref } from "vue";
import MkPagination from "@/components/MkPagination.vue";
import type { Paging } from "@/components/MkPagination.vue";
import { api } from "@/os";
import XList from "@/components/MkDateSeparatedList.vue";
import XNote from "@/components/MkNote.vue";
@ -50,7 +49,7 @@ const props = defineProps<{
noteId: string;
}>();
const pagination: Paging = {
const pagination = {
endpoint: "notes/history" as const,
limit: 10,
offsetMode: true,

View File

@ -9,7 +9,7 @@ export interface PageMetadata {
title: string;
subtitle?: string;
icon?: string | null;
avatar?: entities.User | null;
avatar?: entities.UserDetailed | null;
userName?: entities.User | null;
bg?: string;
}

View File

@ -26,10 +26,9 @@
"@/*": ["./src/*"]
},
"typeRoots": ["node_modules/@types", "@types"],
"types": ["vite/client"],
"lib": ["esnext", "dom"],
"jsx": "preserve"
},
"compileOnSave": false,
"include": ["./**/*.ts", "./**/*.vue"]
"include": ["./src/**/*.ts", "./src/**/*.vue"]
}

View File

@ -38,6 +38,8 @@ import type {
UserSorting,
} from "./entities";
import type * as consts from "./consts";
type TODO = Record<string, any> | null;
type NoParams = Record<string, never>;
@ -84,7 +86,7 @@ export type Endpoints = {
"admin/server-info": { req: TODO; res: TODO };
"admin/show-moderation-logs": { req: TODO; res: TODO };
"admin/show-user": { req: TODO; res: TODO };
"admin/show-users": { req: TODO; res: TODO };
"admin/show-users": { req: TODO; res: User[] };
"admin/silence-user": { req: TODO; res: TODO };
"admin/suspend-user": { req: TODO; res: TODO };
"admin/unsilence-user": { req: TODO; res: TODO };
@ -101,7 +103,18 @@ export type Endpoints = {
"admin/announcements/update": { req: TODO; res: TODO };
"admin/drive/clean-remote-files": { req: TODO; res: TODO };
"admin/drive/cleanup": { req: TODO; res: TODO };
"admin/drive/files": { req: TODO; res: TODO };
"admin/drive/files": {
req: {
limit?: number;
sinceId?: DriveFile["id"];
untilId?: DriveFile["id"];
userId?: User["id"];
type?: string;
origin?: "combined" | "local" | "remote";
hostname?: string;
};
res: DriveFile[];
};
"admin/drive/show-file": { req: TODO; res: TODO };
"admin/emoji/add": { req: TODO; res: TODO };
"admin/emoji/copy": { req: TODO; res: TODO };
@ -200,7 +213,7 @@ export type Endpoints = {
"channels/owned": { req: TODO; res: TODO };
"channels/pin-note": { req: TODO; res: TODO };
"channels/show": { req: TODO; res: TODO };
"channels/timeline": { req: TODO; res: TODO };
"channels/timeline": { req: TODO; res: Note[] };
"channels/unfollow": { req: TODO; res: TODO };
"channels/update": { req: TODO; res: TODO };
@ -238,7 +251,7 @@ export type Endpoints = {
};
res: DriveFile[];
};
"drive/files/attached-notes": { req: TODO; res: TODO };
"drive/files/attached-notes": { req: TODO; res: Note[] };
"drive/files/check-existence": { req: TODO; res: TODO };
"drive/files/create": { req: TODO; res: TODO };
"drive/files/delete": { req: { fileId: DriveFile["id"] }; res: null };
@ -360,25 +373,7 @@ export type Endpoints = {
publishing?: boolean | null;
limit?: number;
offset?: number;
sort?:
| "+pubSub"
| "-pubSub"
| "+notes"
| "-notes"
| "+users"
| "-users"
| "+following"
| "-following"
| "+followers"
| "-followers"
| "+caughtAt"
| "-caughtAt"
| "+lastCommunicatedAt"
| "-lastCommunicatedAt"
| "+driveUsage"
| "-driveUsage"
| "+driveFiles"
| "-driveFiles";
sort?: (typeof consts.instanceSortParam)[number];
};
res: Instance[];
};

View File

@ -151,3 +151,24 @@ export const languages = [
"yi",
"zh",
] as const;
export const instanceSortParam = [
"+pubSub",
"-pubSub",
"+notes",
"-notes",
"+users",
"-users",
"+following",
"-following",
"+followers",
"-followers",
"+caughtAt",
"-caughtAt",
"+lastCommunicatedAt",
"-lastCommunicatedAt",
"+driveUsage",
"-driveUsage",
"+driveFiles",
"-driveFiles",
] as const;

View File

@ -1,10 +1,12 @@
import type * as consts from "./consts";
export type ID = string;
export type DateString = string;
type TODO = Record<string, any>;
// NOTE: 極力この型を使うのは避け、UserLite か UserDetailed か明示するように
export type User = UserLite | UserDetailed;
export type User = UserLite & Partial<UserDetailed>;
export type UserLite = {
id: ID;
@ -108,7 +110,7 @@ export type MeDetailed = UserDetailed & {
isExplorable: boolean;
mutedWords: string[][];
mutedPatterns: string[];
mutingNotificationTypes: string[];
mutingNotificationTypes: (typeof consts.notificationTypes)[number][];
noCrawle: boolean;
preventAiLearning: boolean;
receiveAnnouncementEmail: boolean;
@ -129,6 +131,8 @@ export type DriveFile = {
blurhash: string;
comment: string | null;
properties: Record<string, any>;
userId?: User["id"];
user?: User;
};
export type DriveFolder = TODO;
@ -152,7 +156,8 @@ export type Note = {
visibleUserIds?: User["id"][];
lang?: string;
localOnly?: boolean;
channel?: Channel["id"];
channelId?: Channel["id"];
channel?: Channel;
myReaction?: string;
reactions: Record<string, number>;
renoteCount: number;
@ -199,82 +204,98 @@ export type NoteReaction = {
type: string;
};
export type Notification = {
interface BaseNotification {
id: ID;
createdAt: DateString;
isRead: boolean;
} & (
| {
type: "reaction";
reaction: string;
user: User;
userId: User["id"];
note: Note;
}
| {
type: "reply";
user: User;
userId: User["id"];
note: Note;
}
| {
type: "renote";
user: User;
userId: User["id"];
note: Note;
}
| {
type: "quote";
user: User;
userId: User["id"];
note: Note;
}
| {
type: "mention";
user: User;
userId: User["id"];
note: Note;
}
| {
type: "pollVote";
user: User;
userId: User["id"];
note: Note;
}
| {
type: "pollEnded";
user: User;
userId: User["id"];
note: Note;
}
| {
type: "follow";
user: User;
userId: User["id"];
}
| {
type: "followRequestAccepted";
user: User;
userId: User["id"];
}
| {
type: "receiveFollowRequest";
user: User;
userId: User["id"];
}
| {
type: "groupInvited";
invitation: UserGroup;
user: User;
userId: User["id"];
}
| {
type: "app";
header?: string | null;
body: string;
icon?: string | null;
}
);
type: (typeof consts.notificationTypes)[number];
}
export interface ReactionNotification extends BaseNotification {
type: "reaction";
reaction: string;
user: User;
userId: User["id"];
note: Note;
}
export interface ReplyNotification extends BaseNotification {
type: "reply";
user: User;
userId: User["id"];
note: Note;
}
export interface RenoteNotification extends BaseNotification {
type: "renote";
user: User;
userId: User["id"];
note: Note;
}
export interface QuoteNotification extends BaseNotification {
type: "quote";
user: User;
userId: User["id"];
note: Note;
}
export interface MentionNotification extends BaseNotification {
type: "mention";
user: User;
userId: User["id"];
note: Note;
}
export interface PollVoteNotification extends BaseNotification {
type: "pollVote";
user: User;
userId: User["id"];
note: Note;
}
export interface PollEndedNotification extends BaseNotification {
type: "pollEnded";
user: User;
userId: User["id"];
note: Note;
}
export interface FollowNotification extends BaseNotification {
type: "follow";
user: User;
userId: User["id"];
}
export interface FollowRequestAcceptedNotification extends BaseNotification {
type: "followRequestAccepted";
user: User;
userId: User["id"];
}
export interface ReceiveFollowRequestNotification extends BaseNotification {
type: "receiveFollowRequest";
user: User;
userId: User["id"];
}
export interface GroupInvitedNotification extends BaseNotification {
type: "groupInvited";
invitation: UserGroup;
user: User;
userId: User["id"];
}
export interface AppNotification extends BaseNotification {
type: "app";
header?: string | null;
body: string;
icon?: string | null;
}
export type Notification =
| ReactionNotification
| ReplyNotification
| RenoteNotification
| QuoteNotification
| MentionNotification
| PollVoteNotification
| PollEndedNotification
| FollowNotification
| FollowRequestAcceptedNotification
| ReceiveFollowRequestNotification
| GroupInvitedNotification
| AppNotification;
export type MessagingMessage = {
id: ID;
@ -300,6 +321,11 @@ export type CustomEmoji = {
aliases: string[];
};
export type EmojiLite = {
name: string;
url: string;
};
export type LiteInstanceMetadata = {
maintainerName: string | null;
maintainerEmail: string | null;
@ -449,6 +475,7 @@ export type FollowRequest = {
export type Channel = {
id: ID;
name: string;
// TODO
};

View File

@ -4,6 +4,7 @@ import { Endpoints } from "./api.types";
import * as consts from "./consts";
import Stream, { Connection } from "./streaming";
import * as StreamTypes from "./streaming.types";
import type * as TypeUtils from "./type-utils";
export {
Endpoints,
@ -12,6 +13,7 @@ export {
StreamTypes,
acct,
type Acct,
type TypeUtils,
};
export const permissions = consts.permissions;
@ -20,6 +22,7 @@ export const noteVisibilities = consts.noteVisibilities;
export const mutedNoteReasons = consts.mutedNoteReasons;
export const languages = consts.languages;
export const ffVisibility = consts.ffVisibility;
export const instanceSortParam = consts.instanceSortParam;
// api extractor not supported yet
//export * as api from './api';

View File

@ -236,14 +236,14 @@ export default class Stream extends EventEmitter<StreamEvents> {
// TODO: これらのクラスを Stream クラスの内部クラスにすれば余計なメンバをpublicにしないで済むかも
// もしくは @internal を使う? https://www.typescriptlang.org/tsconfig#stripInternal
class Pool {
public channel: string;
public channel: keyof Channels;
public id: string;
protected stream: Stream;
public users = 0;
private disposeTimerId: any;
private isConnected = false;
constructor(stream: Stream, channel: string, id: string) {
constructor(stream: Stream, channel: keyof Channels, id: string) {
this.channel = channel;
this.stream = stream;
this.id = id;
@ -301,7 +301,7 @@ class Pool {
export abstract class Connection<
Channel extends AnyOf<Channels> = any,
> extends EventEmitter<Channel["events"]> {
public channel: string;
public channel: keyof Channels;
protected stream: Stream;
public abstract id: string;
@ -309,7 +309,7 @@ export abstract class Connection<
public inCount = 0; // for debug
public outCount = 0; // for debug
constructor(stream: Stream, channel: string, name?: string) {
constructor(stream: Stream, channel: keyof Channels, name?: string) {
super();
this.stream = stream;
@ -342,7 +342,12 @@ class SharedConnection<
return this.pool.id;
}
constructor(stream: Stream, channel: string, pool: Pool, name?: string) {
constructor(
stream: Stream,
channel: keyof Channels,
pool: Pool,
name?: string,
) {
super(stream, channel, name);
this.pool = pool;
@ -364,7 +369,7 @@ class NonSharedConnection<
constructor(
stream: Stream,
channel: string,
channel: keyof Channels,
id: string,
params: Channel["params"],
) {

View File

@ -10,9 +10,14 @@ import type {
User,
UserGroup,
} from "./entities";
import type { Connection } from "./streaming";
type FIXME = any;
type TimelineParams = {
withReplies?: boolean;
};
export type Channels = {
main: {
params: null;
@ -56,35 +61,35 @@ export type Channels = {
receives: null;
};
homeTimeline: {
params: null;
params?: TimelineParams;
events: {
note: (payload: Note) => void;
};
receives: null;
};
localTimeline: {
params: null;
params: TimelineParams;
events: {
note: (payload: Note) => void;
};
receives: null;
};
hybridTimeline: {
params: null;
params: TimelineParams;
events: {
note: (payload: Note) => void;
};
receives: null;
};
recommendedTimeline: {
params: null;
params: TimelineParams;
events: {
note: (payload: Note) => void;
};
receives: null;
};
globalTimeline: {
params: null;
params: TimelineParams;
events: {
note: (payload: Note) => void;
};
@ -195,3 +200,5 @@ export type BroadcastEvents = {
emoji: CustomEmoji;
}) => void;
};
export type ChannelOf<C extends keyof Channels> = Connection<Channels[C]>;

View File

@ -0,0 +1,7 @@
import type { Endpoints } from "./api.types";
type PropertyOfType<Type, U> = {
[K in keyof Type]: Type[K] extends U ? K : never;
}[keyof Type];
export type EndpointsOf<T> = PropertyOfType<Endpoints, { res: T }>;

View File

@ -785,6 +785,9 @@ importers:
vue-prism-editor:
specifier: 2.0.0-alpha.2
version: 2.0.0-alpha.2(vue@3.4.21)
vue-tsc:
specifier: 2.0.6
version: 2.0.6(typescript@5.4.3)
packages/firefish-js:
dependencies:
@ -4805,6 +4808,25 @@ packages:
vue: 3.4.21(typescript@5.4.3)
dev: true
/@volar/language-core@2.1.6:
resolution: {integrity: sha512-pAlMCGX/HatBSiDFMdMyqUshkbwWbLxpN/RL7HCQDOo2gYBE+uS+nanosLc1qR6pTQ/U8q00xt8bdrrAFPSC0A==}
dependencies:
'@volar/source-map': 2.1.6
dev: true
/@volar/source-map@2.1.6:
resolution: {integrity: sha512-TeyH8pHHonRCHYI91J7fWUoxi0zWV8whZTVRlsWHSYfjm58Blalkf9LrZ+pj6OiverPTmrHRkBsG17ScQyWECw==}
dependencies:
muggle-string: 0.4.1
dev: true
/@volar/typescript@2.1.6:
resolution: {integrity: sha512-JgPGhORHqXuyC3r6skPmPHIZj4LoMmGlYErFTuPNBq9Nhc9VTv7ctHY7A3jMN3ngKEfRrfnUcwXHztvdSQqNfw==}
dependencies:
'@volar/language-core': 2.1.6
path-browserify: 1.0.1
dev: true
/@vue/compiler-core@3.4.21:
resolution: {integrity: sha512-MjXawxZf2SbZszLPYxaFCjxfibYrzr3eYbKxwpLR9EQN+oaziSu3qKVbwBERj1IFIB8OLUewxB5m/BFzi613og==}
dependencies:
@ -4851,6 +4873,24 @@ packages:
'@vue/shared': 3.4.21
dev: true
/@vue/language-core@2.0.6(typescript@5.4.3):
resolution: {integrity: sha512-UzqU12tzf9XLqRO3TiWPwRNpP4fyUzE6MAfOQWQNZ4jy6a30ARRUpmODDKq6O8C4goMc2AlPqTmjOHPjHkilSg==}
peerDependencies:
typescript: '*'
peerDependenciesMeta:
typescript:
optional: true
dependencies:
'@volar/language-core': 2.1.6
'@vue/compiler-dom': 3.4.21
'@vue/shared': 3.4.21
computeds: 0.0.1
minimatch: 9.0.3
path-browserify: 1.0.1
typescript: 5.4.3
vue-template-compiler: 2.7.16
dev: true
/@vue/reactivity@3.4.21:
resolution: {integrity: sha512-UhenImdc0L0/4ahGCyEzc/pZNwVgcglGy9HVzJ1Bq2Mm9qXOpP8RyNTjookw/gOCUlXSEtuZ2fUg5nrHcoqJcw==}
dependencies:
@ -6795,6 +6835,10 @@ packages:
readable-stream: 4.5.2
dev: false
/computeds@0.0.1:
resolution: {integrity: sha512-7CEBgcMjVmitjYo5q8JTJVra6X5mQ20uTThdK+0kR7UEaDrAWEQcRiBtWJzga4eRpP6afNwwLsX2SET2JhVB1Q==}
dev: true
/concat-map@0.0.1:
resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==}
@ -7317,6 +7361,10 @@ packages:
resolution: {integrity: sha512-vjAczensTgRcqDERK0SR2XMwsF/tSvnvlv6VcF2GIhg6Sx4yOIt/irsr1RDJsKiIyBzJDpCoXiWWq28MqH2cnQ==}
dev: false
/de-indent@1.0.2:
resolution: {integrity: sha512-e/1zu3xH5MQryN2zdVaF0OrdNLUbvWxzMbi+iNA6Bky7l1RoP8a2fIbRocyHclXt/arDrrR6lL3TqFD9pMQTsg==}
dev: true
/debug@2.6.9:
resolution: {integrity: sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==}
peerDependencies:
@ -12896,6 +12944,10 @@ packages:
msgpackr-extract: 3.0.2
dev: false
/muggle-string@0.4.1:
resolution: {integrity: sha512-VNTrAak/KhO2i8dqqnqnAHOa3cYBwXEZe9h+D5h/1ZqFSTEFHdM65lR7RoIqq3tBBYavsOXV84NoHXZ0AkPyqQ==}
dev: true
/multer@1.4.5-lts.1:
resolution: {integrity: sha512-ywPWvcDMeH+z9gQq5qYHCCy+ethsk4goepZ45GLD63fOu0YcNecQxi64nDs3qluZB+murG3/D4dJ7+dGctcCQQ==}
engines: {node: '>= 6.0.0'}
@ -13676,6 +13728,10 @@ packages:
resolution: {integrity: sha512-Wy8PXTLqPAN0oEgBrlnsXPMww3SYJ44tQ8aVrGAI4h4JZYCS0oYqsPqtPR8OhJpv6qFbpbB7XAn0liKV7EXubA==}
dev: false
/path-browserify@1.0.1:
resolution: {integrity: sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g==}
dev: true
/path-dirname@1.0.2:
resolution: {integrity: sha512-ALzNPpyNq9AqXMBjeymIjFDAkAFH06mHJH/cSBHAgU0s4vfpBn6b2nf8tiRLvagKD8RbTpq2FKTBg7cl9l3c7Q==}
dev: false
@ -17233,6 +17289,25 @@ packages:
vue: 3.4.21(typescript@5.4.3)
dev: true
/vue-template-compiler@2.7.16:
resolution: {integrity: sha512-AYbUWAJHLGGQM7+cNTELw+KsOG9nl2CnSv467WobS5Cv9uk3wFcnr1Etsz2sEIHEZvw1U+o9mRlEO6QbZvUPGQ==}
dependencies:
de-indent: 1.0.2
he: 1.2.0
dev: true
/vue-tsc@2.0.6(typescript@5.4.3):
resolution: {integrity: sha512-kK50W4XqQL34vHRkxlRWLicrT6+F9xfgCgJ4KSmCHcytKzc1u3c94XXgI+CjmhOSxyw0krpExF7Obo7y4+0dVQ==}
hasBin: true
peerDependencies:
typescript: '*'
dependencies:
'@volar/typescript': 2.1.6
'@vue/language-core': 2.0.6(typescript@5.4.3)
semver: 7.6.0
typescript: 5.4.3
dev: true
/vue@2.7.14:
resolution: {integrity: sha512-b2qkFyOM0kwqWFuQmgd4o+uHGU7T+2z3T+WQp8UBjADfEv2n4FEMffzBmCKNP0IGzOEEfYjvtcC62xaSKeQDrQ==}
deprecated: Vue 2 has reached EOL and is no longer actively maintained. See https://v2.vuejs.org/eol/ for more details.