refactor: Add more type support to MkPagination

This commit is contained in:
Lhcfl 2024-04-03 23:10:49 +08:00
parent 914fff5658
commit 3d39daff8c
16 changed files with 234 additions and 145 deletions

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,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 { 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,

View File

@ -66,10 +66,10 @@
</transition>
</template>
<script lang="ts" setup>
<script lang="ts" setup generic="E extends keyof Endpoints">
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,
@ -100,11 +100,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 +115,17 @@ const props = withDefaults(
},
);
const slots = defineSlots<{
default(props: { items: Item[] }): unknown;
empty(props: null): never;
}>();
const emit = defineEmits<{
(ev: "queue", count: number): void;
(ev: "status", error: boolean): void;
}>();
type Item = Endpoints[typeof props.pagination.endpoint]["res"] & {
id: string;
};
type Item = Endpoints[E]["res"][number];
const rootEl = ref<HTMLElement>();
const items = ref<Item[]>([]);

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 } 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,7 +94,9 @@ let query: {
listId?: string | undefined;
channelId?: string | undefined;
fileId?: string | undefined;
};
} = {};
// FIXME: The type defination is wrong here, need fix
let connection: {
on: (
arg0: string,
@ -96,14 +111,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,14 +184,17 @@ if (props.src === "antenna") {
query = {
fileId: props.fileId,
};
} else {
throw "NoEndpointError";
}
const stream = useStream();
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", {
@ -265,8 +283,8 @@ function reloadTimeline() {
});
}
const pagination: Paging = {
endpoint: endpoint as keyof Endpoints,
const pagination = {
endpoint,
limit: 10,
params: query,
};

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

@ -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,3 +1,5 @@
import type * as consts from "./consts";
export type ID = string;
export type DateString = string;
@ -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;
@ -451,6 +472,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

@ -13,6 +13,10 @@ import type {
type FIXME = any;
type TimelineParams = {
withReplies?: boolean;
};
export type Channels = {
main: {
params: null;
@ -56,35 +60,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;
};

View File

@ -0,0 +1,13 @@
import type { Endpoints } from "./api.types";
export type Equal<X, Y> = (<T>() => T extends X ? 1 : 2) extends <
T,
>() => T extends Y ? 1 : 2
? true
: false;
export type PropertyOfType<Type, U> = {
[K in keyof Type]: Type[K] extends U ? K : never;
}[keyof Type];
export type EndpointsOf<T> = PropertyOfType<Endpoints, { res: T }>;