diff --git a/components/common/CommonPaginator.vue b/components/common/CommonPaginator.vue index 4b976d65..0fd1b558 100644 --- a/components/common/CommonPaginator.vue +++ b/components/common/CommonPaginator.vue @@ -2,12 +2,13 @@ // @ts-expect-error missing types import { DynamicScroller } from 'vue-virtual-scroller' import 'vue-virtual-scroller/dist/vue-virtual-scroller.css' -import type { Paginator } from 'masto' +import type { Paginator, WsEvents } from 'masto' -const { paginator, keyProp = 'id', virtualScroller = false } = defineProps<{ +const { paginator, stream, keyProp = 'id', virtualScroller = false } = defineProps<{ paginator: Paginator<any, any[]> keyProp?: string virtualScroller: boolean + stream?: WsEvents }>() defineSlots<{ @@ -15,14 +16,19 @@ defineSlots<{ item: any active?: boolean } + updater: { + number: number + update: () => void + } loading: {} }>() -const { items, state, endAnchor, error } = usePaginator(paginator) +const { items, prevItems, update, state, endAnchor, error } = usePaginator(paginator, stream) </script> <template> <div> + <slot v-if="prevItems.length" name="updater" v-bind="{ number: prevItems.length, update }" /> <template v-if="virtualScroller"> <DynamicScroller v-slot="{ item, active }" diff --git a/components/timeline/TimelinePaginator.vue b/components/timeline/TimelinePaginator.vue index 6f56e8d7..3b91389d 100644 --- a/components/timeline/TimelinePaginator.vue +++ b/components/timeline/TimelinePaginator.vue @@ -1,15 +1,21 @@ <script setup lang="ts"> // @ts-expect-error missing types import { DynamicScrollerItem } from 'vue-virtual-scroller' -import type { Paginator, Status } from 'masto' +import type { Paginator, Status, WsEvents } from 'masto' -const { paginator } = defineProps<{ +const { paginator, stream } = defineProps<{ paginator: Paginator<any, Status[]> + stream?: WsEvents }>() </script> <template> - <CommonPaginator :paginator="paginator" virtual-scroller> + <CommonPaginator v-bind="{ paginator, stream }" virtual-scroller> + <template #updater="{ number, update }"> + <button py-4 border="b base" flex="~ col" p-3 w-full text-primary font-bold @click="update"> + Show {{ number }} new items + </button> + </template> <template #default="{ item, active }"> <DynamicScrollerItem :item="item" :active="active" tag="article"> <StatusCard diff --git a/composables/paginator.ts b/composables/paginator.ts index 10dc1196..a051976b 100644 --- a/composables/paginator.ts +++ b/composables/paginator.ts @@ -1,11 +1,12 @@ -import type { Paginator } from 'masto' +import type { Paginator, WsEvents } from 'masto' import { useDeactivated } from './lifecycle' import type { PaginatorState } from '~/types' -export function usePaginator<T>(paginator: Paginator<any, T[]>) { +export function usePaginator<T>(paginator: Paginator<any, T[]>, stream?: WsEvents) { const state = ref<PaginatorState>('idle') const items = ref<T[]>([]) - const newItems = ref<T[]>([]) + const nextItems = ref<T[]>([]) + const prevItems = ref<T[]>([]) const endAnchor = ref<HTMLDivElement>() const bound = reactive(useElementBounding(endAnchor)) @@ -13,6 +14,22 @@ export function usePaginator<T>(paginator: Paginator<any, T[]>) { const error = ref<unknown | undefined>() const deactivated = useDeactivated() + async function update() { + items.value.unshift(...prevItems.value) + prevItems.value = [] + } + + stream?.on('update', (status) => { + prevItems.value.unshift(status as any) + }) + + // TODO: update statuses + stream?.on('status.update', (status) => { + const index = items.value.findIndex((s: any) => s.id === status.id) + if (index >= 0) + items.value[index] = status as any + }) + async function loadNext() { if (state.value !== 'idle') return @@ -22,8 +39,8 @@ export function usePaginator<T>(paginator: Paginator<any, T[]>) { const result = await paginator.next() if (result.value?.length) { - newItems.value = result.value - items.value.push(...newItems.value) + nextItems.value = result.value + items.value.push(...nextItems.value) state.value = 'idle' } else { @@ -59,7 +76,9 @@ export function usePaginator<T>(paginator: Paginator<any, T[]>) { return { items, - newItems, + prevItems, + nextItems, + update, state, error, endAnchor, diff --git a/pages/home.vue b/pages/home.vue index 95ad5569..23913eed 100644 --- a/pages/home.vue +++ b/pages/home.vue @@ -12,6 +12,8 @@ if (useRoute().path === '/signin/callback') { } const paginator = useMasto().timelines.getHomeIterable() +const stream = await useMasto().stream.streamUser() +onBeforeUnmount(() => stream.disconnect()) const { t } = useI18n() useHead({ @@ -26,7 +28,7 @@ useHead({ </template> <slot> <PublishWidget draft-key="home" border="b base" /> - <TimelinePaginator :paginator="paginator" /> + <TimelinePaginator v-bind="{ paginator, stream }" /> </slot> </MainContent> </template> diff --git a/pages/public/index.vue b/pages/public/index.vue index bbfdec7d..23c984d3 100644 --- a/pages/public/index.vue +++ b/pages/public/index.vue @@ -1,5 +1,7 @@ <script setup lang="ts"> const paginator = useMasto().timelines.getPublicIterable() +const stream = await useMasto().stream.streamPublicTimeline() +onBeforeUnmount(() => stream.disconnect()) useHead({ title: 'Federated', @@ -13,7 +15,7 @@ useHead({ </template> <slot> - <TimelinePaginator :paginator="paginator" /> + <TimelinePaginator v-bind="{ paginator, stream }" /> </slot> </MainContent> </template> diff --git a/pages/public/local.vue b/pages/public/local.vue index d563ffe5..88cc9950 100644 --- a/pages/public/local.vue +++ b/pages/public/local.vue @@ -1,5 +1,7 @@ <script setup lang="ts"> const paginator = useMasto().timelines.getPublicIterable({ local: true }) +const stream = await useMasto().stream.streamCommunityTimeline() +onBeforeUnmount(() => stream.disconnect()) useHead({ title: 'Local', @@ -13,7 +15,7 @@ useHead({ </template> <slot> - <TimelinePaginator :paginator="paginator" /> + <TimelinePaginator v-bind="{ paginator, stream }" /> </slot> </MainContent> </template> diff --git a/pages/tags/[tag].vue b/pages/tags/[tag].vue index d8d8c2bc..e228f83e 100644 --- a/pages/tags/[tag].vue +++ b/pages/tags/[tag].vue @@ -3,6 +3,8 @@ const params = useRoute().params const tag = $(computedEager(() => params.tag as string)) const paginator = useMasto().timelines.getHashtagIterable(tag) +const stream = await useMasto().stream.streamTagTimeline(tag) +onBeforeUnmount(() => stream.disconnect()) useHead({ title: `#${tag}`, @@ -16,7 +18,7 @@ useHead({ </template> <slot> - <TimelinePaginator :paginator="paginator" /> + <TimelinePaginator v-bind="{ paginator, stream }" /> </slot> </MainContent> </template>