1- import { Flex , Heading } from '@invoke-ai/ui-library' ;
2- import { useAppDispatch , useAppSelector } from 'app/store/storeHooks' ;
1+ import { Flex , Heading , ListItem } from '@invoke-ai/ui-library' ;
32import { IAINoContentFallbackWithSpinner } from 'common/components/IAIImageFallback' ;
4- import { overlayScrollbarsParams } from 'common/components/OverlayScrollbars/constants' ;
5- import {
6- listCursorChanged ,
7- listPriorityChanged ,
8- selectQueueListCursor ,
9- selectQueueListPriority ,
10- } from 'features/queue/store/queueSlice' ;
11- import { useOverlayScrollbars } from 'overlayscrollbars-react' ;
12- import { memo , useCallback , useEffect , useMemo , useRef , useState } from 'react' ;
3+ import { useRangeBasedQueueItemFetching } from 'features/queue/hooks/useRangeBasedQueueItemFetching' ;
4+ import { memo , useCallback , useMemo , useRef , useState } from 'react' ;
135import { useTranslation } from 'react-i18next' ;
14- import type { Components , ItemContent } from 'react-virtuoso' ;
6+ import type {
7+ Components ,
8+ Components ,
9+ Components ,
10+ ComputeItemKey ,
11+ ItemContent ,
12+ ListRange ,
13+ ScrollSeekConfiguration ,
14+ VirtuosoHandle ,
15+ } from 'react-virtuoso' ;
1516import { Virtuoso } from 'react-virtuoso' ;
16- import { queueItemsAdapterSelectors , useListQueueItemsQuery } from 'services/api/endpoints/queue' ;
17- import type { S } from 'services/api/types' ;
17+ import { queueApi } from 'services/api/endpoints/queue' ;
1818
19- import QueueItemComponent from './QueueItemComponent' ;
19+ import QueueItemComponent , { QueueItemPlaceholder } from './QueueItemComponent' ;
2020import QueueListComponent from './QueueListComponent' ;
2121import QueueListHeader from './QueueListHeader' ;
2222import type { ListContext } from './types' ;
23+ import { useQueueItemIds } from './useQueueItemIds' ;
24+ import { useScrollableQueueList } from './useScrollableQueueList' ;
25+
26+ const QueueItemAtPosition = memo (
27+ ( { index, itemId, context } : { index : number ; itemId : number ; context : ListContext } ) => {
28+ /*
29+ * We rely on the useRangeBasedQueueItemFetching to fetch all queue items, caching them with RTK Query.
30+ *
31+ * In this component, we just want to consume that cache. Unforutnately, RTK Query does not provide a way to
32+ * subscribe to a query without triggering a new fetch.
33+ *
34+ * There is a hack, though:
35+ * - https://github.com/reduxjs/redux-toolkit/discussions/4213
36+ *
37+ * This essentially means "subscribe to the query once it has some data".
38+ */
39+
40+ // Use `currentData` instead of `data` to prevent a flash of previous queue item rendered at this index
41+ const { currentData : queueItem , isUninitialized } = queueApi . endpoints . getQueueItem . useQueryState ( itemId ) ;
42+ queueApi . endpoints . getQueueItem . useQuerySubscription ( itemId , { skip : isUninitialized } ) ;
43+
44+ if ( ! queueItem ) {
45+ return < QueueItemPlaceholder item-id = { itemId } /> ;
46+ }
47+
48+ return < QueueItemComponent index = { index } item = { queueItem } context = { context } /> ;
49+ }
50+ ) ;
51+ QueueItemAtPosition . displayName = 'QueueItemAtPosition' ;
52+
53+ const computeItemKey : ComputeItemKey < number , ListContext > = ( index , itemId , { queryArgs } ) => {
54+ return `${ JSON . stringify ( queryArgs ) } -${ itemId ?? index } ` ;
55+ } ;
2356
24- // eslint-disable-next-line @typescript-eslint/no-explicit-any
25- type TableVirtuosoScrollerRef = ( ref : HTMLElement | Window | null ) => any ;
57+ const itemContent : ItemContent < number , ListContext > = ( index , itemId , context ) => (
58+ < QueueItemAtPosition index = { index } itemId = { itemId } context = { context } />
59+ ) ;
60+
61+ const ScrollSeekPlaceholderComponent : Components < ListContext > [ 'ScrollSeekPlaceholder' ] = ( props ) => (
62+ < ListItem aspectRatio = "1/1" { ...props } >
63+ < QueueItemPlaceholder />
64+ </ ListItem >
65+ ) ;
2666
27- const computeItemKey = ( index : number , item : S [ 'SessionQueueItem' ] ) : number => item . item_id ;
67+ ScrollSeekPlaceholderComponent . displayName = 'ScrollSeekPlaceholderComponent' ;
2868
29- const components : Components < S [ 'SessionQueueItem' ] , ListContext > = {
69+ const components : Components < number , ListContext > = {
3070 List : QueueListComponent ,
71+ // ScrollSeekPlaceholder: ScrollSeekPlaceholderComponent,
3172} ;
3273
33- const itemContent : ItemContent < S [ 'SessionQueueItem' ] , ListContext > = ( index , item , context ) => (
34- < QueueItemComponent index = { index } item = { item } context = { context } />
35- ) ;
74+ const scrollSeekConfiguration : ScrollSeekConfiguration = {
75+ enter : ( velocity ) => {
76+ return Math . abs ( velocity ) > 2048 ;
77+ } ,
78+ exit : ( velocity ) => {
79+ return velocity === 0 ;
80+ } ,
81+ } ;
3682
37- const QueueList = ( ) => {
38- const listCursor = useAppSelector ( selectQueueListCursor ) ;
39- const listPriority = useAppSelector ( selectQueueListPriority ) ;
40- const dispatch = useAppDispatch ( ) ;
83+ export const QueueList = ( ) => {
84+ const virtuosoRef = useRef < VirtuosoHandle > ( null ) ;
85+ const rangeRef = useRef < ListRange > ( { startIndex : 0 , endIndex : 0 } ) ;
4186 const rootRef = useRef < HTMLDivElement > ( null ) ;
42- const [ scroller , setScroller ] = useState < HTMLElement | null > ( null ) ;
43- const [ initialize , osInstance ] = useOverlayScrollbars ( overlayScrollbarsParams ) ;
4487 const { t } = useTranslation ( ) ;
4588
46- useEffect ( ( ) => {
47- const { current : root } = rootRef ;
48- if ( scroller && root ) {
49- initialize ( {
50- target : root ,
51- elements : {
52- viewport : scroller ,
53- } ,
54- } ) ;
55- }
56- return ( ) => osInstance ( ) ?. destroy ( ) ;
57- } , [ scroller , initialize , osInstance ] ) ;
58-
59- const { data : listQueueItemsData , isLoading } = useListQueueItemsQuery (
60- {
61- cursor : listCursor ,
62- priority : listPriority ,
89+ // Get the ordered list of queue item ids - this is our primary data source for virtualization
90+ const { queryArgs, itemIds, isLoading } = useQueueItemIds ( ) ;
91+
92+ // Use range-based fetching for bulk loading queue items into cache based on the visible range
93+ const { onRangeChanged } = useRangeBasedQueueItemFetching ( {
94+ itemIds,
95+ enabled : ! isLoading ,
96+ } ) ;
97+
98+ const scrollerRef = useScrollableQueueList ( rootRef ) as ( ref : HTMLElement | Window | null ) => void ;
99+
100+ /*
101+ * We have to keep track of the visible range for keep-selected-image-in-view functionality and push the range to
102+ * the range-based queue item fetching hook.
103+ */
104+ const handleRangeChanged = useCallback (
105+ ( range : ListRange ) => {
106+ rangeRef . current = range ;
107+ onRangeChanged ( range ) ;
63108 } ,
64- {
65- refetchOnMountOrArgChange : true ,
66- }
109+ [ onRangeChanged ]
67110 ) ;
68111
69- const queueItems = useMemo ( ( ) => {
70- if ( ! listQueueItemsData ) {
71- return [ ] ;
72- }
73- return queueItemsAdapterSelectors . selectAll ( listQueueItemsData ) ;
74- } , [ listQueueItemsData ] ) ;
75-
76- const handleLoadMore = useCallback ( ( ) => {
77- if ( ! listQueueItemsData ?. has_more ) {
78- return ;
79- }
80- const lastItem = queueItems [ queueItems . length - 1 ] ;
81- if ( ! lastItem ) {
82- return ;
83- }
84- dispatch ( listCursorChanged ( lastItem . item_id ) ) ;
85- dispatch ( listPriorityChanged ( lastItem . priority ) ) ;
86- } , [ dispatch , listQueueItemsData ?. has_more , queueItems ] ) ;
87-
88112 const [ openQueueItems , setOpenQueueItems ] = useState < number [ ] > ( [ ] ) ;
89113
90114 const toggleQueueItem = useCallback ( ( item_id : number ) => {
@@ -96,13 +120,16 @@ const QueueList = () => {
96120 } ) ;
97121 } , [ ] ) ;
98122
99- const context = useMemo < ListContext > ( ( ) => ( { openQueueItems, toggleQueueItem } ) , [ openQueueItems , toggleQueueItem ] ) ;
123+ const context = useMemo < ListContext > (
124+ ( ) => ( { queryArgs, openQueueItems, toggleQueueItem } ) ,
125+ [ queryArgs , openQueueItems , toggleQueueItem ]
126+ ) ;
100127
101128 if ( isLoading ) {
102129 return < IAINoContentFallbackWithSpinner /> ;
103130 }
104131
105- if ( ! queueItems . length ) {
132+ if ( ! itemIds . length ) {
106133 return (
107134 < Flex w = "full" h = "full" alignItems = "center" justifyContent = "center" >
108135 < Heading color = "base.500" > { t ( 'queue.queueEmpty' ) } </ Heading >
@@ -114,18 +141,18 @@ const QueueList = () => {
114141 < Flex w = "full" h = "full" flexDir = "column" >
115142 < QueueListHeader />
116143 < Flex ref = { rootRef } w = "full" h = "full" alignItems = "center" justifyContent = "center" >
117- < Virtuoso < S [ 'SessionQueueItem' ] , ListContext >
118- data = { queueItems }
119- endReached = { handleLoadMore }
120- scrollerRef = { setScroller as TableVirtuosoScrollerRef }
144+ < Virtuoso < number , ListContext >
145+ ref = { virtuosoRef }
146+ context = { context }
147+ data = { itemIds }
121148 itemContent = { itemContent }
122149 computeItemKey = { computeItemKey }
123150 components = { components }
124- context = { context }
151+ scrollerRef = { scrollerRef }
152+ scrollSeekConfiguration = { scrollSeekConfiguration }
153+ rangeChanged = { handleRangeChanged }
125154 />
126155 </ Flex >
127156 </ Flex >
128157 ) ;
129158} ;
130-
131- export default memo ( QueueList ) ;
0 commit comments