@ -21,14 +21,14 @@
< div v -else ref = "rootEl" >
< div v-show ="pagination.reversed && more" key="_more_" class="_margin" >
< MkButton v-if ="!moreFetching" v-appear="(enableInfiniteScroll && !props.disableAutoLoad) ? f etchMoreAhead : null" :class="$style.more" :disabled="moreFetching" :style="{ cursor: moreFetching ? 'wait' : 'pointer' }" primary rounded @click="fetchMoreAhead">
< MkButton v-if ="!moreFetching" v-appear="(enableInfiniteScroll && !props.disableAutoLoad) ? appearF etchMoreAhead : null" :class="$style.more" :disabled="moreFetching" :style="{ cursor: moreFetching ? 'wait' : 'pointer' }" primary rounded @click="fetchMoreAhead">
{ { i18n . ts . loadMore } }
< / MkButton >
< MkLoading v -else class = "loading" / >
< / div >
< slot :items =" items" : fetching = "fetching || moreFetching" > < / slot >
< slot :items =" Array.from( items.values()) " : fetching = "fetching || moreFetching" > < / slot >
< div v-show ="!pagination.reversed && more" key="_more_" class="_margin" >
< MkButton v-if ="!moreFetching" v-appear="(enableInfiniteScroll && !props.disableAutoLoad) ? f etchMore : null" :class="$style.more" :disabled="moreFetching" :style="{ cursor: moreFetching ? 'wait' : 'pointer' }" primary rounded @click="fetchMore">
< MkButton v-if ="!moreFetching" v-appear="(enableInfiniteScroll && !props.disableAutoLoad) ? appearF etchMore : null" :class="$style.more" :disabled="moreFetching" :style="{ cursor: moreFetching ? 'wait' : 'pointer' }" primary rounded @click="fetchMore">
{ { i18n . ts . loadMore } }
< / MkButton >
< MkLoading v -else class = "loading" / >
@ -50,6 +50,7 @@ import { i18n } from '@/i18n';
const SECOND _FETCH _LIMIT = 30 ;
const TOLERANCE = 16 ;
const APPEAR _MINIMUM _INTERVAL = 600 ;
export type Paging < E extends keyof misskey.Endpoints = keyof misskey.Endpoints > = {
endpoint : E ;
@ -71,6 +72,16 @@ export type Paging<E extends keyof misskey.Endpoints = keyof misskey.Endpoints>
pageEl ? : HTMLElement ;
} ;
type MisskeyEntityMap = Map < string , MisskeyEntity > ;
function arrayToEntries ( entities : MisskeyEntity [ ] ) : [ string , MisskeyEntity ] [ ] {
return entities . map ( en => [ en . id , en ] ) ;
}
function concatMapWithArray ( map : MisskeyEntityMap , entities : MisskeyEntity [ ] ) : MisskeyEntityMap {
return new Map ( [ ... map , ... arrayToEntries ( entities ) ] ) ;
}
< / script >
< script lang = "ts" setup >
import { infoImageUrl } from '@/instance' ;
@ -94,21 +105,38 @@ let backed = $ref(false);
let scrollRemove = $ref < ( ( ) => void ) | null > ( null ) ;
const items = ref < MisskeyEntity [ ] > ( [ ] ) ;
const queue = ref < MisskeyEntity [ ] > ( [ ] ) ;
/ * *
* 表示するアイテムのソース
* 最新が0番目
* /
const items = ref < MisskeyEntityMap > ( new Map ( ) ) ;
/ * *
* タブが非アクティブなどの場合に更新を貯めておく
* 最新が0番目
* /
const queue = ref < MisskeyEntityMap > ( new Map ( ) ) ;
const offset = ref ( 0 ) ;
/ * *
* 初期化中かどうか ( trueならMkLoadingで全て隠す )
* /
const fetching = ref ( true ) ;
const moreFetching = ref ( false ) ;
const more = ref ( false ) ;
const preventAppearFetchMore = ref ( false ) ;
const preventAppearFetchMoreTimer = ref < number | null > ( null ) ;
const isBackTop = ref ( false ) ;
const empty = computed ( ( ) => items . value . length === 0 ) ;
const empty = computed ( ( ) => items . value . size === 0 ) ;
const error = ref ( false ) ;
const {
enableInfiniteScroll ,
} = defaultStore . reactiveState ;
const contentEl = $computed ( ( ) => props . pagination . pageEl ? ? rootEl ) ;
const scrollableElement = $computed ( ( ) => getScrollContainer( contentEl ) ) ;
const scrollableElement = $computed ( ( ) => contentEl ? getScrollContainer( contentEl ) : document . body ) ;
const visibility = useDocumentVisibility ( ) ;
@ -133,9 +161,9 @@ watch([() => props.pagination.reversed, $$(scrollableElement)], () => {
} , { immediate : true } ) ;
watch ( $$ ( rootEl ) , ( ) => {
scrollObserver . disconnect ( ) ;
scrollObserver ? . disconnect ( ) ;
nextTick ( ( ) => {
if ( rootEl ) scrollObserver . observe ( rootEl ) ;
if ( rootEl ) scrollObserver ? . observe ( rootEl ) ;
} ) ;
} ) ;
@ -155,12 +183,12 @@ if (props.pagination.params && isRef(props.pagination.params)) {
}
watch ( queue , ( a , b ) => {
if ( a . length === 0 && b . length === 0 ) return ;
emit ( 'queue' , queue . value . length ) ;
if ( a . size === 0 && b . size === 0 ) return ;
emit ( 'queue' , queue . value . size ) ;
} , { deep : true } ) ;
async function init ( ) : Promise < void > {
queue . value = [ ] ;
queue . value = new Map ( ) ;
fetching . value = true ;
const params = props . pagination . params ? isRef ( props . pagination . params ) ? props . pagination . params . value : props . pagination . params : { } ;
await os . api ( props . pagination . endpoint , {
@ -173,11 +201,11 @@ async function init(): Promise<void> {
}
if ( res . length === 0 || props . pagination . noPaging ) {
items. value = res ;
concatItems( res ) ;
more . value = false ;
} else {
if ( props . pagination . reversed ) moreFetching . value = true ;
items. value = res ;
concatItems( res ) ;
more . value = true ;
}
@ -191,12 +219,13 @@ async function init(): Promise<void> {
}
const reload = ( ) : Promise < void > => {
items . value = [ ] ;
items . value = new Map ( ) ;
queue . value = new Map ( ) ;
return init ( ) ;
} ;
const fetchMore = async ( ) : Promise < void > => {
if ( ! more . value || fetching . value || moreFetching . value || items . value . length === 0 ) return ;
if ( ! more . value || fetching . value || moreFetching . value || items . value . size === 0 ) return ;
moreFetching . value = true ;
const params = props . pagination . params ? isRef ( props . pagination . params ) ? props . pagination . params . value : props . pagination . params : { } ;
await os . api ( props . pagination . endpoint , {
@ -205,7 +234,7 @@ const fetchMore = async (): Promise<void> => {
... ( props . pagination . offsetMode ? {
offset : offset . value ,
} : {
untilId : items . value [items . value . length - 1 ] . id ,
untilId : Array . from ( items . value .keys ( ) ) [items . value . size - 1 ] ,
} ) ,
} ) . then ( res => {
for ( let i = 0 ; i < res . length ; i ++ ) {
@ -217,7 +246,7 @@ const fetchMore = async (): Promise<void> => {
const oldHeight = scrollableElement ? scrollableElement . scrollHeight : getBodyScrollHeight ( ) ;
const oldScroll = scrollableElement ? scrollableElement . scrollTop : window . scrollY ;
items . value = items. value . concat ( _res ) ;
items . value = concatMapWithArray( items . value , _res ) ;
return nextTick ( ( ) => {
if ( scrollableElement ) {
@ -237,7 +266,7 @@ const fetchMore = async (): Promise<void> => {
moreFetching . value = false ;
} ) ;
} else {
items . value = items. value . concat ( res ) ;
items . value = concatMapWithArray( items . value , res ) ;
more . value = false ;
moreFetching . value = false ;
}
@ -248,7 +277,7 @@ const fetchMore = async (): Promise<void> => {
moreFetching . value = false ;
} ) ;
} else {
items . value = items. value . concat ( res ) ;
items . value = concatMapWithArray( items . value , res ) ;
more . value = true ;
moreFetching . value = false ;
}
@ -260,7 +289,7 @@ const fetchMore = async (): Promise<void> => {
} ;
const fetchMoreAhead = async ( ) : Promise < void > => {
if ( ! more . value || fetching . value || moreFetching . value || items . value . length === 0 ) return ;
if ( ! more . value || fetching . value || moreFetching . value || items . value . size === 0 ) return ;
moreFetching . value = true ;
const params = props . pagination . params ? isRef ( props . pagination . params ) ? props . pagination . params . value : props . pagination . params : { } ;
await os . api ( props . pagination . endpoint , {
@ -269,14 +298,14 @@ const fetchMoreAhead = async (): Promise<void> => {
... ( props . pagination . offsetMode ? {
offset : offset . value ,
} : {
sinceId : items . value [items . value . length - 1 ] . id ,
sinceId : Array . from ( items . value .keys ( ) ) [items . value . size - 1 ] ,
} ) ,
} ) . then ( res => {
if ( res . length === 0 ) {
items . value = items. value . concat ( res ) ;
items . value = concatMapWithArray( items . value , res ) ;
more . value = false ;
} else {
items . value = items. value . concat ( res ) ;
items . value = concatMapWithArray( items . value , res ) ;
more . value = true ;
}
offset . value += res . length ;
@ -286,7 +315,32 @@ const fetchMoreAhead = async (): Promise<void> => {
} ) ;
} ;
const isTop = ( ) : boolean => isBackTop . value || ( props . pagination . reversed ? isBottomVisible : isTopVisible ) ( contentEl , TOLERANCE ) ;
/ * *
* Appear ( IntersectionObserver ) によってfetchMoreが呼ばれる場合 、
* APPEAR _MINIMUM _INTERVALミリ秒以内に2回fetchMoreが呼ばれるのを防ぐ
* /
const fetchMoreApperTimeoutFn = ( ) : void => {
preventAppearFetchMore . value = false ;
preventAppearFetchMoreTimer . value = null ;
} ;
const fetchMoreAppearTimeout = ( ) : void => {
preventAppearFetchMore . value = true ;
preventAppearFetchMoreTimer . value = window . setTimeout ( fetchMoreApperTimeoutFn , APPEAR _MINIMUM _INTERVAL ) ;
} ;
const appearFetchMore = async ( ) : Promise < void > => {
if ( preventAppearFetchMore . value ) return ;
await fetchMore ( ) ;
fetchMoreAppearTimeout ( ) ;
} ;
const appearFetchMoreAhead = async ( ) : Promise < void > => {
if ( preventAppearFetchMore . value ) return ;
await fetchMoreAhead ( ) ;
fetchMoreAppearTimeout ( ) ;
} ;
const isTop = ( ) : boolean => isBackTop . value || ( props . pagination . reversed ? isBottomVisible : isTopVisible ) ( contentEl ! , TOLERANCE ) ;
watch ( visibility , ( ) => {
if ( visibility . value === 'hidden' ) {
@ -308,10 +362,15 @@ watch(visibility, () => {
}
} ) ;
/ * *
* 最新のものとして1つだけアイテムを追加する
* ストリーミングから降ってきたアイテムはこれで追加する
* @ param item アイテム
* /
const prepend = ( item : MisskeyEntity ) : void => {
/ / 初 回 表 示 時 は u n s h i f t だ け で O K
if ( ! rootEl ) {
items . value . unshift ( item ) ;
if ( items . value . size === 0 ) {
items . value . set ( item . id , item ) ;
fetching. value = false ;
return ;
}
@ -319,38 +378,55 @@ const prepend = (item: MisskeyEntity): void => {
else prependQueue ( item ) ;
} ;
/ * *
* 新着アイテムをitemsの先頭に追加し 、 displayLimitを適用する
* @ param newItems 新しいアイテムの配列
* /
function unshiftItems ( newItems : MisskeyEntity [ ] ) {
const length = newItems . length + items . value . length ;
items . value = [ ... newItems , ... items . value ] . slice ( 0 , props . displayLimit ) ;
const length = newItems . length + items . value . size ;
items . value = new Map ( [ ... arrayToEntries ( newItems ) , ... items . value ] . slice ( 0 , props . displayLimit ) ) ;
if ( length >= props . displayLimit ) more . value = true ;
}
/ * *
* 古いアイテムをitemsの末尾に追加し 、 displayLimitを適用する
* @ param oldItems 古いアイテムの配列
* /
function concatItems ( oldItems : MisskeyEntity [ ] ) {
const length = oldItems . length + items . value . size ;
items . value = new Map ( [ ... items . value , ... arrayToEntries ( oldItems ) ] . slice ( 0 , props . displayLimit ) ) ;
if ( length >= props . displayLimit ) more . value = true ;
}
function executeQueue ( ) {
if ( queue . value . length === 0 ) return ;
unshiftItems ( queue . value ) ;
queue . value = [ ] ;
unshiftItems ( Array . from ( queue . value . values ( ) ) ) ;
queue . value = new Map ( ) ;
}
function prependQueue ( newItem : MisskeyEntity ) {
queue . value . unshift ( newItem ) ;
if ( queue . value . length >= props . displayLimit ) {
queue . value . pop ( ) ;
}
queue . value = new Map ( [ [ newItem . id , newItem ] , ... queue . value ] . slice ( 0 , props . displayLimit ) as [ string , MisskeyEntity ] [ ] ) ;
}
/ *
* アイテムを末尾に追加する ( 使うの ? )
* /
const appendItem = ( item : MisskeyEntity ) : void => {
items . value . push ( item ) ;
items . value . set( item . id , item ) ;
} ;
const removeItem = ( f in der : ( item: Mi sskeyEn tity) => boolea n) => {
const i = items . value . findIndex ( finder ) ;
items. value . splice ( i , 1 ) ;
const removeItem = ( id: str ing ) => {
items . value . delete ( id ) ;
queue. value . delete ( id ) ;
} ;
const updateItem = ( id : MisskeyEntity [ 'id' ] , replacer : ( old : MisskeyEntity ) => MisskeyEntity ) : void => {
const i = items . value . findIndex ( item => item . id === id ) ;
items . value [ i ] = replacer ( items . value [ i ] ) ;
const item = items . value . get ( id ) ;
if ( item ) items . value . set ( id , replacer ( item ) ) ;
const queueItem = queue . value . get ( id ) ;
if ( queueItem ) queue . value . set ( id , replacer ( queueItem ) ) ;
} ;
const inited = init ( ) ;
@ -364,7 +440,7 @@ onDeactivated(() => {
} ) ;
function toBottom ( ) {
scrollToBottom ( contentEl ) ;
scrollToBottom ( contentEl ! ) ;
}
onMounted ( ( ) => {
@ -388,7 +464,11 @@ onBeforeUnmount(() => {
clearTimeout ( timerForSetPause ) ;
timerForSetPause = null ;
}
scrollObserver . disconnect ( ) ;
if ( preventAppearFetchMoreTimer . value ) {
clearTimeout ( preventAppearFetchMoreTimer . value ) ;
preventAppearFetchMoreTimer . value = null ;
}
scrollObserver ? . disconnect ( ) ;
} ) ;
defineExpose ( {