Frontend: Magnetar endpoint pagination
ci/woodpecker/push/ociImagePush Pipeline was successful
Details
ci/woodpecker/push/ociImagePush Pipeline was successful
Details
This commit is contained in:
parent
80a29f771f
commit
4834daceec
|
@ -0,0 +1,399 @@
|
|||
<template>
|
||||
<transition :name="$store.state.animation ? 'fade' : ''" mode="out-in">
|
||||
<MkLoading v-if="initialFetching" />
|
||||
|
||||
<MkError v-else-if="error" @retry="init()" />
|
||||
|
||||
<div v-else-if="empty" key="_empty_" class="empty">
|
||||
<slot name="empty">
|
||||
<div class="_fullinfo">
|
||||
<img
|
||||
src="/static-assets/badges/info.png"
|
||||
class="_ghost"
|
||||
alt="Error"
|
||||
/>
|
||||
<div>{{ i18n.ts.nothing }}</div>
|
||||
</div>
|
||||
</slot>
|
||||
</div>
|
||||
|
||||
<div v-else ref="rootEl" class="list">
|
||||
<div
|
||||
v-show="pagination.reversed && next"
|
||||
key="_more_"
|
||||
class="cxiknjgy _gap"
|
||||
>
|
||||
<MkButton
|
||||
v-if="!moreFetching"
|
||||
class="button"
|
||||
:disabled="moreFetching"
|
||||
:style="{ cursor: moreFetching ? 'wait' : 'pointer' }"
|
||||
primary
|
||||
@click="fetchMore"
|
||||
>
|
||||
{{ i18n.ts.loadMore }}
|
||||
</MkButton>
|
||||
<MkLoading v-else class="loading" />
|
||||
</div>
|
||||
<slot name="items" :items="items"></slot>
|
||||
<div
|
||||
v-show="!pagination.reversed && next"
|
||||
key="_more_"
|
||||
class="cxiknjgy _gap"
|
||||
>
|
||||
<MkButton
|
||||
v-if="!moreFetching"
|
||||
v-appear="
|
||||
$store.state.enableInfiniteScroll && !disableAutoLoad
|
||||
? fetchMore
|
||||
: null
|
||||
"
|
||||
class="button"
|
||||
:disabled="moreFetching"
|
||||
:style="{ cursor: moreFetching ? 'wait' : 'pointer' }"
|
||||
primary
|
||||
@click="fetchMore"
|
||||
>
|
||||
{{ i18n.ts.loadMore }}
|
||||
</MkButton>
|
||||
<MkLoading v-else class="loading" />
|
||||
</div>
|
||||
</div>
|
||||
</transition>
|
||||
</template>
|
||||
|
||||
<script
|
||||
lang="ts"
|
||||
setup
|
||||
generic="T extends BackendApiEndpoint<
|
||||
T['method'] & Method,
|
||||
T['pathParams'] & string[],
|
||||
T['request'],
|
||||
T['response'],
|
||||
T['paginated'] & true
|
||||
>"
|
||||
>
|
||||
import {
|
||||
computed,
|
||||
ComputedRef,
|
||||
isRef,
|
||||
onActivated,
|
||||
onDeactivated,
|
||||
ref,
|
||||
watch,
|
||||
} from "vue";
|
||||
import * as os from "@/os";
|
||||
import {
|
||||
getScrollContainer,
|
||||
getScrollPosition,
|
||||
isTopVisible,
|
||||
onScrollTop,
|
||||
} from "@/scripts/scroll";
|
||||
import MkButton from "@/components/MkButton.vue";
|
||||
import {
|
||||
BackendApiEndpoint,
|
||||
Method,
|
||||
PaginatedResult,
|
||||
types,
|
||||
} from "magnetar-common";
|
||||
import { SpanFilter } from "magnetar-common/built/types/SpanFilter";
|
||||
import { i18n } from "@/i18n";
|
||||
|
||||
type PathParams = {
|
||||
[K in keyof T["pathParams"] as T["pathParams"][K] & string]:
|
||||
| string
|
||||
| number;
|
||||
};
|
||||
|
||||
export type Paging = {
|
||||
endpoint: T;
|
||||
pathParams: PathParams | ComputedRef<PathParams>;
|
||||
params?: T["request"] | ComputedRef<T["request"]>;
|
||||
|
||||
limit?: number;
|
||||
|
||||
reversed?: boolean;
|
||||
};
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
pagination: Paging;
|
||||
disableAutoLoad?: boolean;
|
||||
displayLimit?: number;
|
||||
}>(),
|
||||
{
|
||||
displayLimit: 30,
|
||||
}
|
||||
);
|
||||
|
||||
const emit = defineEmits<{
|
||||
(ev: "queue", count: number): void;
|
||||
}>();
|
||||
|
||||
type PageItem = PaginatedResult<T["response"]>["data"][number] & { id: string };
|
||||
|
||||
const rootEl = ref<HTMLElement>();
|
||||
const items = ref<PageItem[]>([]);
|
||||
const queue = ref<PageItem[]>([]);
|
||||
const initialFetching = ref(true);
|
||||
const moreFetching = ref(false);
|
||||
const next = ref<URL | null>(null);
|
||||
const backed = ref(false);
|
||||
const isBackTop = ref(false);
|
||||
const empty = computed(() => items.value.length === 0);
|
||||
const error = ref(false);
|
||||
|
||||
const fetch = async (
|
||||
pagination: SpanFilter
|
||||
): Promise<PaginatedResult<T["response"]> & { data: { id: string } }[]> => {
|
||||
const pathParams = isRef(props.pagination.pathParams)
|
||||
? props.pagination.pathParams.value
|
||||
: props.pagination.pathParams;
|
||||
const params = isRef(props.pagination.params)
|
||||
? props.pagination.params.value
|
||||
: props.pagination.params;
|
||||
|
||||
return os
|
||||
.magApi(
|
||||
props.pagination.endpoint,
|
||||
{
|
||||
...params,
|
||||
pagination,
|
||||
limit: props.pagination.limit,
|
||||
},
|
||||
pathParams,
|
||||
undefined
|
||||
)
|
||||
.then(
|
||||
(res) =>
|
||||
res as PaginatedResult<T["response"]> &
|
||||
{ data: { id: string } }[]
|
||||
);
|
||||
};
|
||||
|
||||
const init = async (): Promise<void> => {
|
||||
queue.value = [];
|
||||
initialFetching.value = true;
|
||||
|
||||
fetch({}).then(
|
||||
(res) => {
|
||||
items.value = props.pagination.reversed
|
||||
? [...res.data].reverse()
|
||||
: res.data;
|
||||
next.value = res.next ?? null;
|
||||
error.value = false;
|
||||
initialFetching.value = false;
|
||||
},
|
||||
(err) => {
|
||||
error.value = true;
|
||||
initialFetching.value = false;
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
const reload = (): void => {
|
||||
items.value = [];
|
||||
init();
|
||||
};
|
||||
|
||||
const refresh = async (): Promise<void> => {
|
||||
fetch({}).then(
|
||||
(res) => {
|
||||
let ids = new Set(items.value.map((i) => i.id));
|
||||
|
||||
for (let i = 0; i < res.data.length; i++) {
|
||||
const item = res.data[i];
|
||||
if (!updateItem(item.id, (old) => item)) {
|
||||
append(item);
|
||||
}
|
||||
ids.delete(item.id);
|
||||
}
|
||||
|
||||
for (const id in ids) {
|
||||
removeItem((i) => i.id === id);
|
||||
}
|
||||
},
|
||||
(err) => {
|
||||
error.value = true;
|
||||
initialFetching.value = false;
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
const fetchMore = async (): Promise<void> => {
|
||||
if (
|
||||
!next.value ||
|
||||
initialFetching.value ||
|
||||
moreFetching.value ||
|
||||
items.value.length === 0
|
||||
)
|
||||
return;
|
||||
|
||||
if (next.value.searchParams.has("pagination")) return;
|
||||
|
||||
const nextCursorRaw = next.value.searchParams.get("pagination") as string;
|
||||
const nextCursorDecoded = {
|
||||
...Object.fromEntries(new URLSearchParams(nextCursorRaw).entries()),
|
||||
} as types.PaginationShape["pagination"];
|
||||
|
||||
moreFetching.value = true;
|
||||
backed.value = true;
|
||||
|
||||
await fetch(nextCursorDecoded).then(
|
||||
(res) => {
|
||||
items.value = props.pagination.reversed
|
||||
? [...res.data].reverse().concat(items.value)
|
||||
: items.value.concat(res.data);
|
||||
next.value = res.next ?? null;
|
||||
moreFetching.value = false;
|
||||
},
|
||||
(err) => {
|
||||
moreFetching.value = false;
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
const prepend = (item: PageItem): void => {
|
||||
if (props.pagination.reversed) {
|
||||
if (rootEl.value) {
|
||||
const container = getScrollContainer(rootEl.value);
|
||||
if (container == null) {
|
||||
// TODO?
|
||||
} else {
|
||||
const pos = getScrollPosition(rootEl.value);
|
||||
const viewHeight = container.clientHeight;
|
||||
const height = container.scrollHeight;
|
||||
const isBottom = pos + viewHeight > height - 32;
|
||||
if (isBottom) {
|
||||
items.value = items.value.slice(-props.displayLimit);
|
||||
|
||||
// TODO
|
||||
// next.value = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
items.value.push(item);
|
||||
// TODO
|
||||
} else {
|
||||
// 初回表示時はunshiftだけでOK
|
||||
if (!rootEl.value) {
|
||||
items.value.unshift(item);
|
||||
return;
|
||||
}
|
||||
|
||||
const isTop =
|
||||
isBackTop.value ||
|
||||
(document.body.contains(rootEl.value) &&
|
||||
isTopVisible(rootEl.value));
|
||||
|
||||
if (isTop) {
|
||||
// Prepend the item
|
||||
items.value = [item, ...items.value].slice(0, props.displayLimit);
|
||||
} else {
|
||||
if (!queue.value.length) {
|
||||
onScrollTop(rootEl.value, () => {
|
||||
items.value = [
|
||||
...queue.value.reverse(),
|
||||
...items.value,
|
||||
].slice(0, props.displayLimit);
|
||||
queue.value = [];
|
||||
});
|
||||
}
|
||||
|
||||
queue.value = [...queue.value, item].slice(-props.displayLimit);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const append = (item: PageItem): void => {
|
||||
items.value.push(item);
|
||||
};
|
||||
|
||||
const removeItem = (finder: (item: PageItem) => boolean): boolean => {
|
||||
const i = items.value.findIndex(finder);
|
||||
if (i === -1) {
|
||||
return false;
|
||||
}
|
||||
|
||||
items.value.splice(i, 1);
|
||||
return true;
|
||||
};
|
||||
|
||||
const updateItem = (
|
||||
id: PageItem["id"],
|
||||
replacer: (old: PageItem) => PageItem
|
||||
): boolean => {
|
||||
const i = items.value.findIndex((item) => item.id === id);
|
||||
if (i === -1) {
|
||||
return false;
|
||||
}
|
||||
|
||||
items.value[i] = replacer(items.value[i]);
|
||||
return true;
|
||||
};
|
||||
|
||||
if (props.pagination.params && isRef(props.pagination.params)) {
|
||||
watch(props.pagination.params, init, { deep: true });
|
||||
}
|
||||
|
||||
watch(
|
||||
queue,
|
||||
(a, b) => {
|
||||
if (a.length === 0 && b.length === 0) return;
|
||||
emit("queue", queue.value.length);
|
||||
},
|
||||
{ deep: true }
|
||||
);
|
||||
|
||||
init();
|
||||
|
||||
onActivated(() => {
|
||||
isBackTop.value = false;
|
||||
});
|
||||
|
||||
onDeactivated(() => {
|
||||
isBackTop.value = window.scrollY === 0;
|
||||
});
|
||||
|
||||
defineSlots<{
|
||||
empty(props: {}): any;
|
||||
items(props: { items: PageItem[] }): any;
|
||||
}>();
|
||||
|
||||
defineExpose({
|
||||
items,
|
||||
queue,
|
||||
backed,
|
||||
reload,
|
||||
refresh,
|
||||
prepend,
|
||||
append,
|
||||
removeItem,
|
||||
updateItem,
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.fade-enter-active,
|
||||
.fade-leave-active {
|
||||
transition: opacity 0.125s ease;
|
||||
}
|
||||
.fade-enter-from,
|
||||
.fade-leave-to {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.cxiknjgy {
|
||||
> .button {
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
}
|
||||
}
|
||||
.list > :deep(._button) {
|
||||
margin-inline: auto;
|
||||
margin-bottom: 16px;
|
||||
&:last-of-type:not(:first-child) {
|
||||
margin-top: 16px;
|
||||
}
|
||||
}
|
||||
</style>
|
|
@ -3,7 +3,13 @@
|
|||
<div
|
||||
class="banner"
|
||||
:style="
|
||||
user.bannerUrl ? `background-image: url(${user.bannerUrl})` : ''
|
||||
magTransProperty(user, 'banner_url', 'bannerUrl')
|
||||
? `background-image: url(${magTransProperty(
|
||||
user,
|
||||
'banner_url',
|
||||
'bannerUrl'
|
||||
)})`
|
||||
: ''
|
||||
"
|
||||
></div>
|
||||
<MagAvatarResolvingProxy
|
||||
|
@ -21,6 +27,7 @@
|
|||
<div class="description">
|
||||
<div v-if="user.description" class="mfm">
|
||||
<Mfm
|
||||
:mm="magMaybeProperty(user, 'description_mm')"
|
||||
:text="user.description"
|
||||
:author="user"
|
||||
:i="$i"
|
||||
|
@ -34,15 +41,33 @@
|
|||
<div class="status">
|
||||
<div>
|
||||
<p>{{ i18n.ts.notes }}</p>
|
||||
<MkNumber :value="user.notesCount" />
|
||||
<MkNumber
|
||||
:value="magTransProperty(user, 'note_count', 'notesCount')"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<p>{{ i18n.ts.following }}</p>
|
||||
<MkNumber :value="user.followingCount" />
|
||||
<MkNumber
|
||||
:value="
|
||||
magTransProperty(
|
||||
user,
|
||||
'following_count',
|
||||
'followingCount'
|
||||
)
|
||||
"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<p>{{ i18n.ts.followers }}</p>
|
||||
<MkNumber :value="user.followersCount" />
|
||||
<MkNumber
|
||||
:value="
|
||||
magTransProperty(
|
||||
user,
|
||||
'follower_count',
|
||||
'followersCount'
|
||||
)
|
||||
"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="koudoku-button">
|
||||
|
@ -58,9 +83,11 @@ import MkNumber from "@/components/MkNumber.vue";
|
|||
import { userPage } from "@/filters/user";
|
||||
import { i18n } from "@/i18n";
|
||||
import { $i } from "@/account";
|
||||
import { packed } from "magnetar-common";
|
||||
import { magMaybeProperty, magTransProperty } from "@/scripts-mag/mag-util";
|
||||
|
||||
defineProps<{
|
||||
user: misskey.entities.UserDetailed;
|
||||
user: packed.PackUserMaybeAll | misskey.entities.UserDetailed;
|
||||
}>();
|
||||
</script>
|
||||
|
||||
|
|
|
@ -10,6 +10,10 @@ export const userName = (user: misskey.entities.User) => {
|
|||
return user.name || user.username;
|
||||
};
|
||||
|
||||
export const userPage = (user: misskey.Acct, path?, absolute = false) => {
|
||||
export const userPage = (
|
||||
user: misskey.Acct,
|
||||
path?: string,
|
||||
absolute = false
|
||||
) => {
|
||||
return `${absolute ? url : ""}/@${acct(user)}${path ? `/${path}` : ""}`;
|
||||
};
|
||||
|
|
|
@ -18,6 +18,7 @@ import {
|
|||
FrontendApiEndpoints,
|
||||
MagApiClient,
|
||||
Method,
|
||||
PaginatedResult,
|
||||
types,
|
||||
} from "magnetar-common";
|
||||
import { magReactionToLegacy } from "@/scripts-mag/mag-util";
|
||||
|
@ -48,7 +49,9 @@ export async function magApi<
|
|||
| number;
|
||||
},
|
||||
token?: string | null | undefined
|
||||
): Promise<T["response"]> {
|
||||
): Promise<
|
||||
T["paginated"] extends true ? PaginatedResult<T["response"]> : T["response"]
|
||||
> {
|
||||
pendingApiRequestsCount.value++;
|
||||
|
||||
try {
|
||||
|
|
|
@ -2,7 +2,10 @@
|
|||
<MkStickyContainer>
|
||||
<template #header><MkPageHeader /></template>
|
||||
<MkSpacer :content-max="800">
|
||||
<MkPagination ref="paginationComponent" :pagination="pagination">
|
||||
<MagPagination
|
||||
ref="paginationComponent"
|
||||
:pagination="followRequestPagination"
|
||||
>
|
||||
<template #empty>
|
||||
<div class="_fullinfo">
|
||||
<img
|
||||
|
@ -13,88 +16,165 @@
|
|||
<div>{{ i18n.ts.noFollowRequests }}</div>
|
||||
</div>
|
||||
</template>
|
||||
<template #default="{ items }">
|
||||
<template #items="{ items: users }">
|
||||
<MkInfo v-if="$i?.isLocked === false" warn class="info"
|
||||
>{{ i18n.ts.silencedWarning }}
|
||||
</MkInfo>
|
||||
<div class="mk-follow-requests">
|
||||
<div
|
||||
v-for="req in items"
|
||||
:key="req.id"
|
||||
v-for="user in users"
|
||||
:key="user.id"
|
||||
class="user _panel"
|
||||
>
|
||||
<MagAvatarResolvingProxy
|
||||
class="avatar"
|
||||
:user="req.follower"
|
||||
:show-indicator="true"
|
||||
disableLink
|
||||
/>
|
||||
<div class="body">
|
||||
<div class="header">
|
||||
<MagAvatar class="avatar" :user="user" />
|
||||
<div class="name">
|
||||
<MkA
|
||||
v-user-preview="req.follower.id"
|
||||
v-user-preview="user.id"
|
||||
class="name"
|
||||
:to="userPage(req.follower)"
|
||||
><MkUserName :user="req.follower"
|
||||
:to="userPage(user)"
|
||||
><MkUserName :user="user"
|
||||
/></MkA>
|
||||
<p class="acct">
|
||||
@{{ acct(req.follower) }}
|
||||
</p>
|
||||
</div>
|
||||
<div
|
||||
v-if="req.follower.description"
|
||||
class="description"
|
||||
:title="req.follower.description"
|
||||
>
|
||||
<Mfm
|
||||
:text="req.follower.description"
|
||||
:is-note="false"
|
||||
:author="req.follower"
|
||||
:i="$i"
|
||||
:custom-emojis="req.follower.emojis"
|
||||
:plain="true"
|
||||
:nowrap="true"
|
||||
/>
|
||||
<p class="acct">@{{ acct(user) }}</p>
|
||||
</div>
|
||||
<div class="actions">
|
||||
<button
|
||||
class="_button"
|
||||
@click="accept(req.follower)"
|
||||
:aria-label="i18n.ts.accept"
|
||||
@click="accept(user)"
|
||||
>
|
||||
<i class="ph-check ph-bold ph-lg"></i>
|
||||
</button>
|
||||
<button
|
||||
class="_button"
|
||||
@click="reject(req.follower)"
|
||||
:aria-label="i18n.ts.reject"
|
||||
@click="reject(user)"
|
||||
>
|
||||
<i class="ph-x ph-bold ph-lg"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="moved" v-if="user.moved_to">
|
||||
{{ i18n.ts.accountMoved }}
|
||||
<MagMention
|
||||
class="link"
|
||||
:username="user.moved_to.username"
|
||||
:host="user.moved_to.host"
|
||||
/>
|
||||
</div>
|
||||
<div class="remote" v-if="user.host != null">
|
||||
{{ i18n.ts.remoteUserCaution }}
|
||||
<a
|
||||
class="link"
|
||||
:href="user.url!"
|
||||
rel="nofollow noopener"
|
||||
target="_blank"
|
||||
>{{ i18n.ts.showOnRemote }}</a
|
||||
>
|
||||
</div>
|
||||
<div class="description">
|
||||
<div v-if="user.description" class="mfm">
|
||||
<Mfm
|
||||
:mm="user.description_mm"
|
||||
:text="user.description"
|
||||
:author="user"
|
||||
:i="$i"
|
||||
:custom-emojis="user.emojis"
|
||||
/>
|
||||
</div>
|
||||
<span v-else style="opacity: 0.7">{{
|
||||
i18n.ts.noAccountDescription
|
||||
}}</span>
|
||||
</div>
|
||||
<div class="fields">
|
||||
<dl class="field">
|
||||
<dt class="name">
|
||||
<i
|
||||
class="ph-calendar-blank ph-bold ph-lg ph-fw ph-lg"
|
||||
></i>
|
||||
{{ i18n.ts.registeredDate }}
|
||||
</dt>
|
||||
<dd class="value">
|
||||
{{
|
||||
new Date(
|
||||
user.created_at
|
||||
).toLocaleString()
|
||||
}}
|
||||
(<MkTime :time="user.created_at" />)
|
||||
</dd>
|
||||
</dl>
|
||||
</div>
|
||||
<div v-if="user.fields?.length > 0" class="fields">
|
||||
<dl
|
||||
v-for="(field, i) in user.fields"
|
||||
:key="i"
|
||||
class="field"
|
||||
>
|
||||
<dt class="name">
|
||||
<Mfm
|
||||
:text="field.name"
|
||||
:plain="true"
|
||||
:custom-emojis="user.emojis"
|
||||
:colored="false"
|
||||
/>
|
||||
</dt>
|
||||
<dd class="value">
|
||||
<Mfm
|
||||
:mm="field.value_mm ?? undefined"
|
||||
:text="field.value"
|
||||
:author="user"
|
||||
:i="$i"
|
||||
:custom-emojis="user.emojis"
|
||||
:colored="false"
|
||||
/>
|
||||
</dd>
|
||||
</dl>
|
||||
</div>
|
||||
<div class="status">
|
||||
<div>
|
||||
<p>{{ i18n.ts.notes }}</p>
|
||||
<MkNumber :value="user.note_count" />
|
||||
</div>
|
||||
<div>
|
||||
<p>{{ i18n.ts.following }}</p>
|
||||
<MkNumber :value="user.following_count" />
|
||||
</div>
|
||||
<div>
|
||||
<p>{{ i18n.ts.followers }}</p>
|
||||
<MkNumber :value="user.follower_count" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</MkPagination>
|
||||
</MagPagination>
|
||||
</MkSpacer>
|
||||
</MkStickyContainer>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { computed, onBeforeUnmount, onMounted, ref } from "vue";
|
||||
import MkPagination from "@/components/MkPagination.vue";
|
||||
import { acct, userPage } from "@/filters/user";
|
||||
import * as os from "@/os";
|
||||
import { i18n } from "@/i18n";
|
||||
import { definePageMetadata } from "@/scripts/page-metadata";
|
||||
import { $i } from "@/account";
|
||||
import { globalEvents } from "@/events";
|
||||
import MagPagination, { Paging } from "@/components/MagPagination.vue";
|
||||
import { endpoints, types } from "magnetar-common";
|
||||
import MkInfo from "@/components/MkInfo.vue";
|
||||
import MkNumber from "@/components/MkNumber.vue";
|
||||
import MkTime from "@/components/global/MkTime.vue";
|
||||
import Mfm from "@/components/mfm.vue";
|
||||
import MagMention from "@/components/MagMention.vue";
|
||||
|
||||
const paginationComponent = ref<InstanceType<typeof MkPagination>>();
|
||||
const paginationComponent = ref<InstanceType<typeof MagPagination>>();
|
||||
|
||||
const pagination = {
|
||||
endpoint: "following/requests/list" as const,
|
||||
limit: 10,
|
||||
noPaging: true,
|
||||
const followRequestPagination: Paging = {
|
||||
endpoint: endpoints.GetFollowRequestsSelf,
|
||||
limit: 20,
|
||||
pathParams: {},
|
||||
params: {},
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
|
@ -113,13 +193,13 @@ onBeforeUnmount(() => {
|
|||
);
|
||||
});
|
||||
|
||||
function accept(user) {
|
||||
function accept(user: types.Id) {
|
||||
os.api("following/requests/accept", { userId: user.id }).then(() => {
|
||||
globalEvents.emit("followeeProcessed", user);
|
||||
});
|
||||
}
|
||||
|
||||
function reject(user) {
|
||||
function reject(user: types.Id) {
|
||||
os.api("following/requests/reject", { userId: user.id }).then(() => {
|
||||
globalEvents.emit("followeeProcessed", user);
|
||||
});
|
||||
|
@ -141,29 +221,25 @@ definePageMetadata(
|
|||
.mk-follow-requests {
|
||||
> .user {
|
||||
display: flex;
|
||||
padding: 16px;
|
||||
flex-direction: column;
|
||||
margin: 10px 0 auto;
|
||||
|
||||
> .avatar {
|
||||
display: block;
|
||||
flex-shrink: 0;
|
||||
margin: 0 12px 0 0;
|
||||
width: 42px;
|
||||
height: 42px;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
> .body {
|
||||
> .header {
|
||||
display: flex;
|
||||
width: calc(100% - 54px);
|
||||
position: relative;
|
||||
padding: 16px;
|
||||
flex-direction: row;
|
||||
|
||||
> .avatar {
|
||||
display: block;
|
||||
flex-shrink: 0;
|
||||
margin: 0 12px 0 0;
|
||||
width: 42px;
|
||||
height: 42px;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
> .name {
|
||||
width: 45%;
|
||||
|
||||
@media (max-width: 500px) {
|
||||
width: 100%;
|
||||
}
|
||||
flex-grow: 1;
|
||||
|
||||
> .name,
|
||||
> .acct {
|
||||
|
@ -186,35 +262,103 @@ definePageMetadata(
|
|||
}
|
||||
}
|
||||
|
||||
> .description {
|
||||
width: 55%;
|
||||
line-height: 42px;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
opacity: 0.7;
|
||||
font-size: 14px;
|
||||
padding-right: 40px;
|
||||
padding-left: 8px;
|
||||
box-sizing: border-box;
|
||||
|
||||
@media (max-width: 500px) {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
> .actions {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
right: 0;
|
||||
margin: auto 0;
|
||||
flex-shrink: 0;
|
||||
|
||||
> button {
|
||||
padding: 12px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
> .moved,
|
||||
> .remote {
|
||||
padding: 16px;
|
||||
font-size: 0.8em;
|
||||
border-top: solid 0.5px var(--divider);
|
||||
color: var(--infoWarnFg);
|
||||
|
||||
> .link {
|
||||
margin-left: 4px;
|
||||
text-decoration: underline;
|
||||
color: var(--accent);
|
||||
|
||||
&:hover {
|
||||
text-decoration: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
> .description {
|
||||
padding: 16px;
|
||||
font-size: 0.8em;
|
||||
border-top: solid 0.5px var(--divider);
|
||||
|
||||
> .mfm {
|
||||
display: -webkit-box;
|
||||
-webkit-line-clamp: 10;
|
||||
-webkit-box-orient: vertical;
|
||||
overflow: hidden;
|
||||
}
|
||||
}
|
||||
|
||||
> .fields {
|
||||
border-top: solid 0.5px var(--divider);
|
||||
padding: 10px 16px;
|
||||
font-size: 0.9em;
|
||||
|
||||
> .field {
|
||||
display: flex;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
align-items: center;
|
||||
|
||||
&:not(:last-child) {
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
> .name {
|
||||
flex-grow: 1;
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
font-weight: bold;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
> .value {
|
||||
flex-grow: 2;
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
margin: 0;
|
||||
text-align: center;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
> .status {
|
||||
padding: 10px 16px 16px;
|
||||
border-top: solid 0.5px var(--divider);
|
||||
|
||||
> div {
|
||||
display: inline-flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
width: 33%;
|
||||
|
||||
> p {
|
||||
margin: 0;
|
||||
font-size: 0.7em;
|
||||
color: var(--fg);
|
||||
}
|
||||
|
||||
> span {
|
||||
font-size: 1em;
|
||||
color: var(--accent);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -1,52 +1,53 @@
|
|||
<template>
|
||||
<div>
|
||||
<MkPagination
|
||||
v-slot="{ items }"
|
||||
<MagPagination
|
||||
ref="list"
|
||||
:pagination="
|
||||
type === 'following' ? followingPagination : followersPagination
|
||||
"
|
||||
class="mk-following-or-followers"
|
||||
>
|
||||
<div class="users">
|
||||
<MkUserInfo
|
||||
v-for="user in items.map((x) =>
|
||||
type === 'following' ? x.followee : x.follower
|
||||
)"
|
||||
:key="user.id"
|
||||
class="user"
|
||||
:user="user"
|
||||
/>
|
||||
</div>
|
||||
</MkPagination>
|
||||
<template #items="{ items: users }">
|
||||
<div class="users">
|
||||
<MkUserInfo
|
||||
v-for="user in users as typeof endpoints.GetFollowingById.response"
|
||||
:key="user.id"
|
||||
class="user"
|
||||
:user="user"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
</MagPagination>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { computed } from "vue";
|
||||
import MkUserInfo from "@/components/MkUserInfo.vue";
|
||||
import MkPagination from "@/components/MkPagination.vue";
|
||||
import { packed } from "magnetar-common";
|
||||
import { endpoints, packed } from "magnetar-common";
|
||||
import MagPagination, { Paging } from "@/components/MagPagination.vue";
|
||||
|
||||
const props = defineProps<{
|
||||
user: packed.PackUserBase;
|
||||
type: "following" | "followers";
|
||||
}>();
|
||||
|
||||
const followingPagination = {
|
||||
endpoint: "users/following" as const,
|
||||
const followingPagination: Paging = {
|
||||
endpoint: endpoints.GetFollowingById,
|
||||
limit: 20,
|
||||
params: computed(() => ({
|
||||
userId: props.user.id,
|
||||
pathParams: computed(() => ({
|
||||
id: props.user.id,
|
||||
})),
|
||||
params: {},
|
||||
};
|
||||
|
||||
const followersPagination = {
|
||||
endpoint: "users/followers" as const,
|
||||
const followersPagination: Paging = {
|
||||
endpoint: endpoints.GetFollowersById,
|
||||
limit: 20,
|
||||
params: computed(() => ({
|
||||
userId: props.user.id,
|
||||
pathParams: computed(() => ({
|
||||
id: props.user.id,
|
||||
})),
|
||||
params: {},
|
||||
};
|
||||
</script>
|
||||
|
||||
|
|
|
@ -13,7 +13,7 @@ export interface BackendApiEndpoint<
|
|||
endpoint: string;
|
||||
pathParams: PP;
|
||||
request?: T;
|
||||
response?: R;
|
||||
response?: R & (PG extends true ? any[] : any);
|
||||
paginated: PG;
|
||||
}
|
||||
|
||||
|
@ -58,7 +58,7 @@ export interface MagApiError {
|
|||
|
||||
export interface PaginatedResult<T> {
|
||||
prev?: URL;
|
||||
data: T;
|
||||
data: T & any[];
|
||||
next?: URL;
|
||||
}
|
||||
|
||||
|
@ -81,7 +81,12 @@ function extractHeaderRel(
|
|||
const relMatch = relPar.match(/rel="(.+?)"/)?.[1];
|
||||
|
||||
if (relMatch == rel && urlMatch) {
|
||||
return new URL(urlMatch);
|
||||
try {
|
||||
return new URL(urlMatch);
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -4,6 +4,7 @@ import {
|
|||
MagApiError,
|
||||
MagApiErrorCode,
|
||||
Method,
|
||||
PaginatedResult,
|
||||
} from "./be-api";
|
||||
|
||||
import {
|
||||
|
@ -19,6 +20,7 @@ export * as endpoints from "./endpoints";
|
|||
export {
|
||||
Method,
|
||||
BackendApiEndpoint,
|
||||
PaginatedResult,
|
||||
MagApiError,
|
||||
MagApiClient,
|
||||
MagApiErrorCode,
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
import type { SpanFilter } from "./SpanFilter";
|
||||
|
||||
export interface PaginationShape { pagination: SpanFilter, limit: bigint, }
|
||||
export interface PaginationShape { pagination: SpanFilter, limit: number, }
|
|
@ -426,6 +426,14 @@ packages:
|
|||
'@babel/types': 7.22.5
|
||||
dev: true
|
||||
|
||||
/@babel/parser@7.23.6:
|
||||
resolution: {integrity: sha512-Z2uID7YJ7oNvAI20O9X0bblw7Qqs8Q2hFy0R9tAfnfLkp5MW0UH9eUvnDSnFwKZ0AvgS1ucqR4KzvVHgnke1VQ==}
|
||||
engines: {node: '>=6.0.0'}
|
||||
hasBin: true
|
||||
dependencies:
|
||||
'@babel/types': 7.22.5
|
||||
dev: true
|
||||
|
||||
/@babel/runtime@7.20.7:
|
||||
resolution: {integrity: sha512-UF0tvkUtxwAgZ5W/KrkHf0Rn0fdnLDU9ScxBrEVNUprE/MzirjK4MJUX1/BVDv00Sv8cljtukVK1aky++X1SjQ==}
|
||||
engines: {node: '>=6.9.0'}
|
||||
|
@ -1445,7 +1453,7 @@ packages:
|
|||
/@vue/compiler-core@3.3.4:
|
||||
resolution: {integrity: sha512-cquyDNvZ6jTbf/+x+AgM2Arrp6G4Dzbb0R64jiG804HRMfRiFXWI6kqUVqZ6ZR0bQhIoQjB4+2bhNtVwndW15g==}
|
||||
dependencies:
|
||||
'@babel/parser': 7.22.7
|
||||
'@babel/parser': 7.23.6
|
||||
'@vue/shared': 3.3.4
|
||||
estree-walker: 2.0.2
|
||||
source-map-js: 1.0.2
|
||||
|
@ -1461,7 +1469,7 @@ packages:
|
|||
/@vue/compiler-sfc@2.7.14:
|
||||
resolution: {integrity: sha512-aNmNHyLPsw+sVvlQFQ2/8sjNuLtK54TC6cuKnVzAY93ks4ZBrvwQSnkkIh7bsbNhum5hJBS00wSDipQ937f5DA==}
|
||||
dependencies:
|
||||
'@babel/parser': 7.22.7
|
||||
'@babel/parser': 7.23.6
|
||||
postcss: 8.4.25
|
||||
source-map: 0.6.1
|
||||
dev: true
|
||||
|
@ -1516,7 +1524,7 @@ packages:
|
|||
dependencies:
|
||||
'@vue/runtime-core': 3.3.4
|
||||
'@vue/shared': 3.3.4
|
||||
csstype: 3.1.2
|
||||
csstype: 3.1.3
|
||||
dev: true
|
||||
|
||||
/@vue/server-renderer@3.3.4(vue@3.3.4):
|
||||
|
@ -2793,8 +2801,8 @@ packages:
|
|||
source-map: 0.5.7
|
||||
dev: true
|
||||
|
||||
/csstype@3.1.2:
|
||||
resolution: {integrity: sha512-I7K1Uu0MBPzaFKg4nI5Q7Vs2t+3gWWW648spaF+Rg7pI9ds18Ugn+lvg4SHczUdKlHI5LWBXyqfS8+DufyBsgQ==}
|
||||
/csstype@3.1.3:
|
||||
resolution: {integrity: sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==}
|
||||
dev: true
|
||||
|
||||
/custom-event-polyfill@1.0.7:
|
||||
|
@ -7444,9 +7452,10 @@ packages:
|
|||
|
||||
/vue@2.7.14:
|
||||
resolution: {integrity: sha512-b2qkFyOM0kwqWFuQmgd4o+uHGU7T+2z3T+WQp8UBjADfEv2n4FEMffzBmCKNP0IGzOEEfYjvtcC62xaSKeQDrQ==}
|
||||
deprecated: Vue 2 has reached EOL and is no longer actively maintained. See https://v2.vuejs.org/eol/ for more details.
|
||||
dependencies:
|
||||
'@vue/compiler-sfc': 2.7.14
|
||||
csstype: 3.1.2
|
||||
csstype: 3.1.3
|
||||
dev: true
|
||||
|
||||
/vue@3.3.4:
|
||||
|
|
|
@ -11,11 +11,11 @@ impl<const MIN: u64, const MAX: u64> TS for U64Range<MIN, MAX> {
|
|||
const EXPORT_TO: Option<&'static str> = Some("bindings/util/u64_range.ts");
|
||||
|
||||
fn decl() -> String {
|
||||
<u64 as TS>::decl()
|
||||
<usize as TS>::decl()
|
||||
}
|
||||
|
||||
fn name() -> String {
|
||||
<u64 as TS>::name()
|
||||
<usize as TS>::name()
|
||||
}
|
||||
|
||||
fn dependencies() -> Vec<ts_rs::Dependency> {
|
||||
|
|
Loading…
Reference in New Issue