fix: introduce virtua virtual scroller library (#3567)

This commit is contained in:
TAKAHASHI Shuuji 2026-03-16 14:36:10 +09:00 committed by GitHub
parent df5ed65560
commit e413a8cd96
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
7 changed files with 69 additions and 72 deletions

View file

@ -1,8 +1,6 @@
<script setup lang="ts" generic="T, O, U = T">
import type { mastodon } from 'masto'
// @ts-expect-error missing types
import { DynamicScroller } from 'vue-virtual-scroller'
import 'vue-virtual-scroller/dist/vue-virtual-scroller.css'
import { WindowVirtualizer } from 'virtua/vue'
const {
paginator,
@ -27,7 +25,6 @@ defineSlots<{
items: U[]
item: U
index: number
active?: boolean
older: U
newer: U // newer is undefined when index === 0
}) => void
@ -83,23 +80,16 @@ defineExpose({ createEntry, removeEntry, updateEntry })
<slot v-if="prevItems.length" name="updater" v-bind="{ number: prevItems.length, update }" />
<slot name="items" :items="items as U[]">
<template v-if="virtualScroller">
<DynamicScroller
v-slot="{ item, active, index }"
:items="items"
:min-item-size="200"
:key-field="keyProp"
page-mode
>
<WindowVirtualizer v-slot="{ item, index }" :data="items">
<slot
v-bind="{ key: item[keyProp] }"
v-bind="{ key: (item as any)[keyProp] }"
:item="item"
:active="active"
:older="items[index + 1] as U"
:newer="items[index - 1] as U"
:index="index"
:items="items as U[]"
/>
</DynamicScroller>
</WindowVirtualizer>
</template>
<template v-else>
<slot

View file

@ -1,8 +1,6 @@
<script setup lang="ts">
import type { GroupedAccountLike, NotificationSlot } from '#shared/types'
import type { mastodon } from 'masto'
// @ts-expect-error missing types
import { DynamicScrollerItem } from 'vue-virtual-scroller'
defineProps<{
paginator: mastodon.Paginator<mastodon.v1.Notification[], mastodon.rest.v1.ListNotificationsParams>
@ -186,26 +184,24 @@ const { formatNumber } = useHumanReadableNumber()
{{ $t('timeline.show_new_items', number, { named: { v: formatNumber(number) } }) }}
</button>
</template>
<template #default="{ item, active }">
<template #default="{ item }">
<template v-if="virtualScroller">
<DynamicScrollerItem :item="item" :active="active" tag="div">
<NotificationGroupedFollow
v-if="item.type === 'grouped-follow'"
:items="item"
border="b base"
/>
<NotificationGroupedLikes
v-else-if="item.type === 'grouped-reblogs-and-favourites'"
:group="item"
border="b base"
/>
<NotificationCard
v-else
:notification="item"
hover:bg-active
border="b base"
/>
</DynamicScrollerItem>
<NotificationGroupedFollow
v-if="item.type === 'grouped-follow'"
:items="item"
border="b base"
/>
<NotificationGroupedLikes
v-else-if="item.type === 'grouped-reblogs-and-favourites'"
:group="item"
border="b base"
/>
<NotificationCard
v-else
:notification="item"
hover:bg-active
border="b base"
/>
</template>
<template v-else>
<NotificationGroupedFollow

View file

@ -1,8 +1,5 @@
<script setup lang="ts">
import type { mastodon } from 'masto'
// @ts-expect-error missing types
import { DynamicScrollerItem } from 'vue-virtual-scroller'
import 'vue-virtual-scroller/dist/vue-virtual-scroller.css'
const { account, buffer = 10, endMessage = true, followedTags = [] } = defineProps<{
paginator: mastodon.Paginator<mastodon.v1.Status[], mastodon.rest.v1.ListAccountStatusesParams>
@ -36,17 +33,15 @@ function getFollowedTag(status: mastodon.v1.Status): string | null {
{{ $t('timeline.show_new_items', number, { named: { v: formatNumber(number) } }) }}
</button>
</template>
<template #default="{ item, older, newer, active }">
<template #default="{ item, older, newer }">
<template v-if="virtualScroller">
<DynamicScrollerItem :item="item" :active="active" tag="article">
<StatusCard :followed-tag="getFollowedTag(item)" :status="item" :context="context" :older="older" :newer="newer" :account="account" />
</DynamicScrollerItem>
<StatusCard :followed-tag="getFollowedTag(item)" :status="item" :context="context" :older="older" :newer="newer" :account="account" />
</template>
<template v-else>
<StatusCard :followed-tag="getFollowedTag(item)" :status="item" :context="context" :older="older" :newer="newer" :account="account" />
</template>
</template>
<template v-if="context === 'account' " #done="{ items }">
<template v-if="context === 'account'" #done="{ items }">
<div
v-if="showOriginSite || items.length === 0"
p5 text-secondary text-center flex flex-col items-center gap1

View file

@ -2,10 +2,6 @@
import type { CommonPaginator } from '#components'
import type { mastodon } from 'masto'
import type { ComponentExposed } from 'vue-component-type-helpers'
// @ts-expect-error missing types
import { DynamicScrollerItem } from 'vue-virtual-scroller'
import 'vue-virtual-scroller/dist/vue-virtual-scroller.css'
const { account, buffer = 10, endMessage = true } = defineProps<{
paginator: mastodon.Paginator<mastodon.v1.ScheduledStatus[], mastodon.DefaultPaginationParams>
@ -38,11 +34,10 @@ const showOriginSite = computed(() =>
{{ $t('timeline.show_new_items', number, { named: { v: formatNumber(number) } }) }}
</button>
</template>
<template #default="{ item, active }">
<template #default="{ item }">
<component
:is="virtualScroller ? DynamicScrollerItem : 'article'"
:item="item"
:active="active"
:is="virtualScroller ? 'div' : 'article'"
:key="item.id"
>
<StatusScheduledCard
:item="item"

View file

@ -1,7 +1,6 @@
<script setup lang="ts">
import type { ComponentPublicInstance } from 'vue'
// @ts-expect-error missing types
import { DynamicScroller, DynamicScrollerItem } from 'vue-virtual-scroller'
import { WindowVirtualizer } from 'virtua/vue'
definePageMeta({
name: 'status',
@ -95,26 +94,20 @@ onReactivated(() => {
/>
<template v-if="!pendingContext">
<DynamicScroller
v-slot="{ item, index, active }"
:items="context?.descendants || []"
:min-item-size="200"
:buffer="800"
key-field="id"
page-mode
<WindowVirtualizer
v-slot="{ item, index }"
:data="context?.descendants || []"
>
<DynamicScrollerItem :item="item" :active="active">
<StatusCard
:key="item.id"
:status="item"
context="account"
:older="context?.descendants[index + 1]"
:newer="index > 0 ? context?.descendants[index - 1] : status"
:has-newer="index === 0"
:main="status"
/>
</DynamicScrollerItem>
</DynamicScroller>
<StatusCard
:key="item.id"
:status="item"
context="account"
:older="context?.descendants[index + 1]"
:newer="index > 0 ? context?.descendants[index - 1] : status"
:has-newer="index === 0"
:main="status"
/>
</WindowVirtualizer>
</template>
</div>
</template>

View file

@ -117,6 +117,7 @@
"ultrahtml": "^1.5.3",
"unimport": "^5.6.0",
"unstorage": "^1.17.1",
"virtua": "^0.48.6",
"vite": "^7.1.7",
"vite-plugin-pwa": "^1.2.0",
"vue": "^3.5.4",

View file

@ -266,6 +266,9 @@ importers:
unstorage:
specifier: ^1.17.4
version: 1.17.4(@upstash/redis@1.35.4)(@vercel/kv@3.0.0)(db0@0.3.2(better-sqlite3@12.4.1))(idb-keyval@6.2.2)(ioredis@5.8.0)
virtua:
specifier: ^0.48.6
version: 0.48.8(vue@3.5.22(typescript@5.9.2))
vite:
specifier: ^7.1.7
version: 7.1.11(@types/node@25.4.0)(jiti@2.6.0)(lightningcss@1.30.2)(terser@5.46.0)(tsx@4.20.6)(yaml@2.8.2)
@ -10190,6 +10193,26 @@ packages:
vfile@6.0.3:
resolution: {integrity: sha512-KzIbH/9tXat2u30jf+smMwFCsno4wHVdNmzFyL+T/L3UGqqk6JKfVqOFOZEpZSHADH1k40ab6NUIXZq422ov3Q==}
virtua@0.48.8:
resolution: {integrity: sha512-jpsxOw5V4B6hg44JePRLo9DL0TV7N1lBEVtPjKpAJebXyhI2s9lfiXJESaLapNtr3vtiSk/pWHiLf7B2a6UcgQ==}
peerDependencies:
react: '>=16.14.0'
react-dom: '>=16.14.0'
solid-js: '>=1.0'
svelte: '>=5.0'
vue: ^3.5.4
peerDependenciesMeta:
react:
optional: true
react-dom:
optional: true
solid-js:
optional: true
svelte:
optional: true
vue:
optional: true
vite-dev-rpc@1.1.0:
resolution: {integrity: sha512-pKXZlgoXGoE8sEKiKJSng4hI1sQ4wi5YT24FCrwrLt6opmkjlqPPVmiPWWJn8M8byMxRGzp1CrFuqQs4M/Z39A==}
peerDependencies:
@ -22917,6 +22940,10 @@ snapshots:
'@types/unist': 3.0.3
vfile-message: 4.0.3
virtua@0.48.8(vue@3.5.22(typescript@5.9.2)):
optionalDependencies:
vue: 3.5.22(typescript@5.9.2)
vite-dev-rpc@1.1.0(vite@7.1.11(@types/node@25.4.0)(jiti@2.6.0)(lightningcss@1.30.2)(terser@5.46.0)(tsx@4.20.6)(yaml@2.8.2)):
dependencies:
birpc: 2.6.1