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": { "organizeImports": {
"enabled": true "enabled": true
}, },
"linter": { "linter": {
"enabled": true, "enabled": true,
"rules": { "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": "^_", "caughtErrorsIgnorePattern": "^_",
"destructuredArrayIgnorePattern": "^_" "destructuredArrayIgnorePattern": "^_"
} }
] ],
"vue/no-setup-props-destructure": "off"
} }
} }

View file

@ -8,6 +8,7 @@
"build:debug": "pnpm run build", "build:debug": "pnpm run build",
"lint": "pnpm biome check **/*.ts --apply ; pnpm run lint:vue", "lint": "pnpm biome check **/*.ts --apply ; pnpm run lint:vue",
"lint:vue": "pnpm eslint src --fix '**/*.vue' --cache ; pnpm run format", "lint:vue": "pnpm eslint src --fix '**/*.vue' --cache ; pnpm run format",
"types:check": "pnpm vue-tsc --noEmit",
"format": "pnpm biome format * --write" "format": "pnpm biome format * --write"
}, },
"devDependencies": { "devDependencies": {
@ -86,6 +87,7 @@
"vue": "3.4.21", "vue": "3.4.21",
"vue-draggable-plus": "^0.3.5", "vue-draggable-plus": "^0.3.5",
"vue-plyr": "^7.0.0", "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> <script lang="ts" setup>
import MkChannelPreview from "@/components/MkChannelPreview.vue"; 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 MkPagination from "@/components/MkPagination.vue";
import { i18n } from "@/i18n"; import { i18n } from "@/i18n";
import type { entities } from "firefish-js";
const props = withDefaults( const props = withDefaults(
defineProps<{ defineProps<{
pagination: Paging; pagination: PagingOf<entities.Channel>;
noGap?: boolean; noGap?: boolean;
extractor?: (item: any) => any; extractor?: (item: any) => any;
}>(), }>(),

View file

@ -1,130 +1,83 @@
<script lang="ts"> <template>
import type { PropType } from "vue"; <component
import { TransitionGroup, defineComponent, h } from "vue"; :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 MkAd from "@/components/global/MkAd.vue";
import { i18n } from "@/i18n"; import { i18n } from "@/i18n";
import { defaultStore } from "@/store"; import { defaultStore } from "@/store";
import icon from "@/scripts/icon"; import icon from "@/scripts/icon";
export default defineComponent({ export interface Item {
props: { id: string;
items: { createdAt: string;
type: Array as PropType< _shouldInsertAd_?: boolean;
{ 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,
},
},
setup(props, { slots, expose }) { const props = withDefaults(
function getDateText(time: string) { defineProps<{
items: T[];
direction?: string;
reversed?: boolean;
noGap?: boolean;
ad?: boolean;
}>(),
{
direction: "down",
reversed: false,
noGap: false,
ad: false,
},
);
const slots = defineSlots<{
default(props: { item: T }): unknown;
}>();
function getDateText(time: string) {
const date = new Date(time).getDate(); const date = new Date(time).getDate();
const month = new Date(time).getMonth() + 1; const month = new Date(time).getMonth() + 1;
return i18n.t("monthAndDay", { return i18n.t("monthAndDay", {
month: month.toString(), month: month.toString(),
day: date.toString(), day: date.toString(),
}); });
} }
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 },
);
},
});
</script> </script>
<style lang="scss"> <style lang="scss">

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -26,7 +26,7 @@
@input="onInput" @input="onInput"
/> />
<datalist v-if="datalist" :id="id"> <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> </datalist>
<div ref="suffixEl" class="suffix"> <div ref="suffixEl" class="suffix">
<slot name="suffix"></slot> <slot name="suffix"></slot>
@ -47,7 +47,7 @@
<script lang="ts" setup> <script lang="ts" setup>
import { nextTick, onMounted, ref, toRefs, watch } from "vue"; 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 MkButton from "@/components/MkButton.vue";
import { useInterval } from "@/scripts/use-interval"; import { useInterval } from "@/scripts/use-interval";
import { i18n } from "@/i18n"; import { i18n } from "@/i18n";
@ -72,7 +72,7 @@ const props = defineProps<{
autofocus?: boolean; autofocus?: boolean;
autocomplete?: string; autocomplete?: string;
spellcheck?: boolean; spellcheck?: boolean;
step?: any; step?: number | string;
datalist?: string[]; datalist?: string[];
inline?: boolean; inline?: boolean;
debounce?: boolean; debounce?: boolean;
@ -82,10 +82,10 @@ const props = defineProps<{
}>(); }>();
const emit = defineEmits<{ const emit = defineEmits<{
(ev: "change", _ev: KeyboardEvent): void; (ev: "change", _ev: Event): void;
(ev: "keydown", _ev: KeyboardEvent): void; (ev: "keydown", _ev: KeyboardEvent): void;
(ev: "enter"): 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); const { modelValue, type, autofocus } = toRefs(props);
@ -94,14 +94,15 @@ const id = Math.random().toString(); // TODO: uuid?
const focused = ref(false); const focused = ref(false);
const changed = ref(false); const changed = ref(false);
const invalid = ref(false); const invalid = ref(false);
const inputEl = ref<HTMLElement>(); const inputEl = ref<HTMLInputElement>();
const prefixEl = ref<HTMLElement>(); const prefixEl = ref<HTMLElement>();
const suffixEl = ref<HTMLElement>(); const suffixEl = ref<HTMLElement>();
const height = props.small ? 36 : props.large ? 40 : 38; const height = props.small ? 36 : props.large ? 40 : 38;
const focus = () => inputEl.value.focus(); const focus = () => inputEl.value!.focus();
const selectRange = (start, end) => inputEl.value.setSelectionRange(start, end); const selectRange = (start, end) =>
const onInput = (ev: KeyboardEvent) => { inputEl.value!.setSelectionRange(start, end);
const onInput = (ev: Event) => {
changed.value = true; changed.value = true;
emit("change", ev); emit("change", ev);
}; };
@ -116,13 +117,13 @@ const onKeydown = (ev: KeyboardEvent) => {
const updated = () => { const updated = () => {
changed.value = false; changed.value = false;
if (type.value === "number") { if (type.value === "number") {
emit("update:modelValue", parseFloat(v.value)); emit("update:modelValue", Number.parseFloat(v.value as string));
} else { } else {
emit("update:modelValue", v.value); emit("update:modelValue", v.value);
} }
}; };
const debouncedUpdated = debounce(1000, updated); const debouncedUpdated = Debounce(1000, updated);
watch(modelValue, (newValue) => { watch(modelValue, (newValue) => {
v.value = 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) {
if (prefixEl.value.offsetWidth) { 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) {
if (suffixEl.value.offsetWidth) { 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> <script lang="ts" setup>
import { computed } from "vue"; import { computed } from "vue";
// biome-ignore lint/suspicious/noExplicitAny: FIXME
type ValueType = any;
const props = defineProps<{ const props = defineProps<{
modelValue: any; modelValue: ValueType;
value: any; value: ValueType;
disabled: boolean; disabled: boolean;
}>(); }>();
const emit = defineEmits<{ const emit = defineEmits<{
(ev: "update:modelValue", value: any): void; "update:modelValue": [value: ValueType];
}>(); }>();
const checked = computed(() => props.modelValue === props.value); const checked = computed(() => props.modelValue === props.value);
function toggle(x) { function toggle(_ev: Event) {
if (props.disabled) return; if (props.disabled) return;
emit("update:modelValue", props.value); emit("update:modelValue", props.value);
} }

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -98,7 +98,7 @@ const props = withDefaults(
); );
const emit = defineEmits<{ const emit = defineEmits<{
(ev: "click", v: MouseEvent): void; click: [v: MouseEvent];
}>(); }>();
const url = computed(() => const url = computed(() =>
@ -123,18 +123,18 @@ watch(
}, },
); );
const gallery = ref(null); const gallery = ref<HTMLElement | null>(null);
onMounted(() => { onMounted(() => {
const lightbox = new PhotoSwipeLightbox({ const lightbox = new PhotoSwipeLightbox({
dataSource: [ dataSource: [
{ {
src: url, src: url.value,
w: 300, w: 300,
h: 300, h: 300,
}, },
], ],
gallery: gallery.value, gallery: gallery.value || undefined,
children: ".avatar", children: ".avatar",
thumbSelector: ".avatar", thumbSelector: ".avatar",
loop: false, loop: false,
@ -174,7 +174,7 @@ onMounted(() => {
history.pushState(null, "", location.href); history.pushState(null, "", location.href);
addEventListener("popstate", close); 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. // 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", () => { lightbox.on("close", () => {
removeEventListener("popstate", close); removeEventListener("popstate", close);
@ -186,7 +186,7 @@ onMounted(() => {
function close() { function close() {
removeEventListener("popstate", close); removeEventListener("popstate", close);
history.forward(); history.forward();
lightbox.pswp.close(); lightbox.pswp?.close();
} }
}); });
</script> </script>

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -855,8 +855,8 @@ export function popupMenu(
noReturnFocus?: boolean; noReturnFocus?: boolean;
}, },
) { ) {
return new Promise((resolve, reject) => { return new Promise<void>((resolve, _reject) => {
let dispose; let dispose: () => void;
popup( popup(
defineAsyncComponent({ defineAsyncComponent({
loader: () => import("@/components/MkPopupMenu.vue"), 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 FormSplit from "@/components/form/split.vue";
import { i18n } from "@/i18n"; import { i18n } from "@/i18n";
import icon from "@/scripts/icon"; import icon from "@/scripts/icon";
import type { instanceSortParam } from "firefish-js";
const host = ref(""); const host = ref("");
const state = ref("federating"); const state = ref("federating");
const sort = ref("+pubSub"); const sort = ref<(typeof instanceSortParam)[number]>("+pubSub");
const pagination = { const pagination = {
endpoint: "federation/instances" as const, endpoint: "federation/instances" as const,
limit: 10, limit: 10,

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -10,9 +10,14 @@ import type {
User, User,
UserGroup, UserGroup,
} from "./entities"; } from "./entities";
import type { Connection } from "./streaming";
type FIXME = any; type FIXME = any;
type TimelineParams = {
withReplies?: boolean;
};
export type Channels = { export type Channels = {
main: { main: {
params: null; params: null;
@ -56,35 +61,35 @@ export type Channels = {
receives: null; receives: null;
}; };
homeTimeline: { homeTimeline: {
params: null; params?: TimelineParams;
events: { events: {
note: (payload: Note) => void; note: (payload: Note) => void;
}; };
receives: null; receives: null;
}; };
localTimeline: { localTimeline: {
params: null; params: TimelineParams;
events: { events: {
note: (payload: Note) => void; note: (payload: Note) => void;
}; };
receives: null; receives: null;
}; };
hybridTimeline: { hybridTimeline: {
params: null; params: TimelineParams;
events: { events: {
note: (payload: Note) => void; note: (payload: Note) => void;
}; };
receives: null; receives: null;
}; };
recommendedTimeline: { recommendedTimeline: {
params: null; params: TimelineParams;
events: { events: {
note: (payload: Note) => void; note: (payload: Note) => void;
}; };
receives: null; receives: null;
}; };
globalTimeline: { globalTimeline: {
params: null; params: TimelineParams;
events: { events: {
note: (payload: Note) => void; note: (payload: Note) => void;
}; };
@ -195,3 +200,5 @@ export type BroadcastEvents = {
emoji: CustomEmoji; emoji: CustomEmoji;
}) => void; }) => 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: vue-prism-editor:
specifier: 2.0.0-alpha.2 specifier: 2.0.0-alpha.2
version: 2.0.0-alpha.2(vue@3.4.21) 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: packages/firefish-js:
dependencies: dependencies:
@ -4805,6 +4808,25 @@ packages:
vue: 3.4.21(typescript@5.4.3) vue: 3.4.21(typescript@5.4.3)
dev: true 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: /@vue/compiler-core@3.4.21:
resolution: {integrity: sha512-MjXawxZf2SbZszLPYxaFCjxfibYrzr3eYbKxwpLR9EQN+oaziSu3qKVbwBERj1IFIB8OLUewxB5m/BFzi613og==} resolution: {integrity: sha512-MjXawxZf2SbZszLPYxaFCjxfibYrzr3eYbKxwpLR9EQN+oaziSu3qKVbwBERj1IFIB8OLUewxB5m/BFzi613og==}
dependencies: dependencies:
@ -4851,6 +4873,24 @@ packages:
'@vue/shared': 3.4.21 '@vue/shared': 3.4.21
dev: true 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: /@vue/reactivity@3.4.21:
resolution: {integrity: sha512-UhenImdc0L0/4ahGCyEzc/pZNwVgcglGy9HVzJ1Bq2Mm9qXOpP8RyNTjookw/gOCUlXSEtuZ2fUg5nrHcoqJcw==} resolution: {integrity: sha512-UhenImdc0L0/4ahGCyEzc/pZNwVgcglGy9HVzJ1Bq2Mm9qXOpP8RyNTjookw/gOCUlXSEtuZ2fUg5nrHcoqJcw==}
dependencies: dependencies:
@ -6795,6 +6835,10 @@ packages:
readable-stream: 4.5.2 readable-stream: 4.5.2
dev: false dev: false
/computeds@0.0.1:
resolution: {integrity: sha512-7CEBgcMjVmitjYo5q8JTJVra6X5mQ20uTThdK+0kR7UEaDrAWEQcRiBtWJzga4eRpP6afNwwLsX2SET2JhVB1Q==}
dev: true
/concat-map@0.0.1: /concat-map@0.0.1:
resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==} resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==}
@ -7317,6 +7361,10 @@ packages:
resolution: {integrity: sha512-vjAczensTgRcqDERK0SR2XMwsF/tSvnvlv6VcF2GIhg6Sx4yOIt/irsr1RDJsKiIyBzJDpCoXiWWq28MqH2cnQ==} resolution: {integrity: sha512-vjAczensTgRcqDERK0SR2XMwsF/tSvnvlv6VcF2GIhg6Sx4yOIt/irsr1RDJsKiIyBzJDpCoXiWWq28MqH2cnQ==}
dev: false dev: false
/de-indent@1.0.2:
resolution: {integrity: sha512-e/1zu3xH5MQryN2zdVaF0OrdNLUbvWxzMbi+iNA6Bky7l1RoP8a2fIbRocyHclXt/arDrrR6lL3TqFD9pMQTsg==}
dev: true
/debug@2.6.9: /debug@2.6.9:
resolution: {integrity: sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==} resolution: {integrity: sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==}
peerDependencies: peerDependencies:
@ -12896,6 +12944,10 @@ packages:
msgpackr-extract: 3.0.2 msgpackr-extract: 3.0.2
dev: false dev: false
/muggle-string@0.4.1:
resolution: {integrity: sha512-VNTrAak/KhO2i8dqqnqnAHOa3cYBwXEZe9h+D5h/1ZqFSTEFHdM65lR7RoIqq3tBBYavsOXV84NoHXZ0AkPyqQ==}
dev: true
/multer@1.4.5-lts.1: /multer@1.4.5-lts.1:
resolution: {integrity: sha512-ywPWvcDMeH+z9gQq5qYHCCy+ethsk4goepZ45GLD63fOu0YcNecQxi64nDs3qluZB+murG3/D4dJ7+dGctcCQQ==} resolution: {integrity: sha512-ywPWvcDMeH+z9gQq5qYHCCy+ethsk4goepZ45GLD63fOu0YcNecQxi64nDs3qluZB+murG3/D4dJ7+dGctcCQQ==}
engines: {node: '>= 6.0.0'} engines: {node: '>= 6.0.0'}
@ -13676,6 +13728,10 @@ packages:
resolution: {integrity: sha512-Wy8PXTLqPAN0oEgBrlnsXPMww3SYJ44tQ8aVrGAI4h4JZYCS0oYqsPqtPR8OhJpv6qFbpbB7XAn0liKV7EXubA==} resolution: {integrity: sha512-Wy8PXTLqPAN0oEgBrlnsXPMww3SYJ44tQ8aVrGAI4h4JZYCS0oYqsPqtPR8OhJpv6qFbpbB7XAn0liKV7EXubA==}
dev: false dev: false
/path-browserify@1.0.1:
resolution: {integrity: sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g==}
dev: true
/path-dirname@1.0.2: /path-dirname@1.0.2:
resolution: {integrity: sha512-ALzNPpyNq9AqXMBjeymIjFDAkAFH06mHJH/cSBHAgU0s4vfpBn6b2nf8tiRLvagKD8RbTpq2FKTBg7cl9l3c7Q==} resolution: {integrity: sha512-ALzNPpyNq9AqXMBjeymIjFDAkAFH06mHJH/cSBHAgU0s4vfpBn6b2nf8tiRLvagKD8RbTpq2FKTBg7cl9l3c7Q==}
dev: false dev: false
@ -17233,6 +17289,25 @@ packages:
vue: 3.4.21(typescript@5.4.3) vue: 3.4.21(typescript@5.4.3)
dev: true 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: /vue@2.7.14:
resolution: {integrity: sha512-b2qkFyOM0kwqWFuQmgd4o+uHGU7T+2z3T+WQp8UBjADfEv2n4FEMffzBmCKNP0IGzOEEfYjvtcC62xaSKeQDrQ==} 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. deprecated: Vue 2 has reached EOL and is no longer actively maintained. See https://v2.vuejs.org/eol/ for more details.