Skip to content

Commit a8cd6f6

Browse files
feat: custom virtual slots nav list
1 parent abee1db commit a8cd6f6

11 files changed

Lines changed: 1074 additions & 325 deletions

File tree

src/consts.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
export const slotsPerLeader = 4;
2-
export const slotsListPinnedSlotOffset = 5;
2+
export const slotsListTopPaddingIndex = 5;
33
export const scheduleUpcomingSlotsCount = 3;
44

55
export const solDecimals = 4;
Lines changed: 72 additions & 240 deletions
Original file line numberDiff line numberDiff line change
@@ -1,36 +1,22 @@
1-
import { useAtomValue, useSetAtom } from "jotai";
1+
import { useAtomValue } from "jotai";
22
import {
3-
autoScrollAtom,
4-
currentLeaderSlotAtom,
3+
currentSlotAtom,
54
epochAtom,
5+
isCurrentlyLeaderAtom,
66
leaderSlotsAtom,
7+
nextLeaderSlotAtom,
8+
nextLeaderSlotIndexAtom,
79
SlotNavFilter,
810
slotNavFilterAtom,
9-
slotOverrideAtom,
1011
} from "../../atoms";
11-
import { Box, Flex, Text } from "@radix-ui/themes";
12-
import type { RefObject } from "react";
13-
import { memo, useCallback, useEffect, useMemo, useRef, useState } from "react";
12+
import { Flex, Text } from "@radix-ui/themes";
13+
import { memo, useMemo, type CSSProperties } from "react";
1414
import styles from "./slotsList.module.css";
15-
import { slotsListPinnedSlotOffset } from "../../consts";
16-
import { throttle } from "lodash";
17-
import SlotsRenderer, { MSlotsPlaceholder } from "./SlotsRenderer";
18-
import type { ScrollSeekConfiguration, VirtuosoHandle } from "react-virtuoso";
19-
import { Virtuoso } from "react-virtuoso";
2015
import ResetLive from "./ResetLive";
21-
import type { DebouncedState } from "use-debounce";
22-
import { useDebouncedCallback } from "use-debounce";
23-
import clsx from "clsx";
24-
import {
25-
getAllSlotsListProps,
26-
getMySlotsListProps,
27-
type SlotsIndexProps,
28-
} from "./utils";
29-
30-
const computeItemKey = (slot: number) => slot;
31-
32-
// Add one future slot to prevent current leader transition from flickering
33-
const increaseViewportBy = { top: 24, bottom: 0 };
16+
import VirtualSlotsList from "./VirtualSlotsList";
17+
import { slotGroupCssVars, type SlotsIndexProps } from "./const";
18+
import { getAllSlotsListProps } from "./allSlotsUtils";
19+
import { getMySlotsListProps } from "./mySlotsUtils";
3420

3521
interface SlotsListProps {
3622
width: number;
@@ -44,244 +30,90 @@ export default function SlotsList({ width, height }: SlotsListProps) {
4430
if (!epoch) return null;
4531

4632
return navFilter === SlotNavFilter.MySlots ? (
47-
<MySlotsList key={epoch.epoch} width={width} height={height} />
33+
<MMySlotsList key={epoch.epoch} width={width} height={height} />
4834
) : (
49-
<AllSlotsList key={epoch.epoch} width={width} height={height} />
35+
<MAllSlotsList key={epoch.epoch} width={width} height={height} />
5036
);
5137
}
5238

53-
function InnerSlotsList({
39+
const MInnerSlotsList = memo(function InnerSlotsList({
5440
width,
5541
height,
5642
itemsCount,
5743
getSlotAtIndex,
5844
getIndexForSlot,
45+
offsetHelpers,
5946
}: SlotsIndexProps & SlotsListProps) {
60-
const listContainerRef = useRef<HTMLDivElement>(null);
61-
const listRef = useRef<VirtuosoHandle>(null);
62-
const visibleStartIndexRef = useRef<number | null>(null);
63-
64-
const [hideList, setHideList] = useState(true);
65-
const [showPlaceholder, setShowPlaceholder] = useState(true);
66-
67-
useEffect(() => {
68-
// initially hide list to
69-
const timeout = setTimeout(() => {
70-
setHideList(false);
71-
}, 100);
72-
73-
return () => clearTimeout(timeout);
74-
}, []);
75-
76-
const setSlotOverride = useSetAtom(slotOverrideAtom);
77-
78-
const debouncedScroll = useDebouncedCallback(() => {}, 100);
79-
80-
const { rangeChanged, scrollSeekConfiguration } = useMemo(() => {
81-
const rangeChangedFn = ({ startIndex }: { startIndex: number }) => {
82-
// account for increaseViewportBy
83-
visibleStartIndexRef.current = startIndex + 1;
84-
};
85-
86-
const config: ScrollSeekConfiguration = {
87-
enter: (velocity) => Math.abs(velocity) > 1500,
88-
exit: (velocity) => Math.abs(velocity) < 500,
89-
change: (_, range) => rangeChangedFn(range),
47+
const style: CSSProperties = useMemo(() => {
48+
return {
49+
...slotGroupCssVars,
50+
position: "relative",
51+
width,
52+
height,
9053
};
91-
return { rangeChanged: rangeChangedFn, scrollSeekConfiguration: config };
92-
}, [visibleStartIndexRef]);
93-
94-
// Setup user scroll handling
95-
useEffect(() => {
96-
if (!listContainerRef.current) return;
97-
const container = listContainerRef.current;
98-
99-
const handleSlotOverride = throttle(
100-
() => {
101-
if (visibleStartIndexRef.current === null) return;
102-
103-
debouncedScroll();
104-
105-
const slotIndex = Math.min(
106-
visibleStartIndexRef.current + slotsListPinnedSlotOffset,
107-
itemsCount - 1,
108-
);
109-
110-
const slot = getSlotAtIndex(slotIndex);
111-
setSlotOverride(slot);
112-
},
113-
50,
114-
{ leading: true, trailing: true },
115-
);
116-
117-
const handleScroll = () => {
118-
handleSlotOverride();
119-
};
120-
121-
container.addEventListener("wheel", handleScroll);
122-
container.addEventListener("touchmove", handleScroll);
123-
124-
return () => {
125-
container.removeEventListener("wheel", handleScroll);
126-
container.removeEventListener("touchmove", handleScroll);
127-
};
128-
}, [
129-
getSlotAtIndex,
130-
debouncedScroll,
131-
setSlotOverride,
132-
itemsCount,
133-
visibleStartIndexRef,
134-
]);
135-
136-
const getItemContent = useCallback(
137-
(index: number) => {
138-
const leader = getSlotAtIndex(index);
139-
if (leader == null) return null;
140-
return <SlotsRenderer leaderSlotForGroup={leader} />;
141-
},
142-
[getSlotAtIndex],
143-
);
144-
145-
const totalListHeightChanged = useCallback(
146-
(totalListHeight: number) => setShowPlaceholder(totalListHeight >= height),
147-
[height],
148-
);
54+
}, [height, width]);
14955

15056
return (
151-
<Box
152-
ref={listContainerRef}
153-
position="relative"
154-
width={`${width}px`}
155-
height={`${height}px`}
156-
>
157-
<MRtAutoScroll listRef={listRef} getIndexForSlot={getIndexForSlot} />
158-
<MSlotOverrideScroll
159-
listRef={listRef}
160-
getIndexForSlot={getIndexForSlot}
161-
debouncedScroll={debouncedScroll}
162-
/>
163-
{showPlaceholder && <MSlotsPlaceholder width={width} height={height} />}
57+
<div style={style}>
16458
<ResetLive />
165-
<Virtuoso
166-
ref={listRef}
167-
className={clsx(styles.slotsList, { [styles.hidden]: hideList })}
168-
width={width}
169-
height={height}
170-
totalCount={itemsCount}
171-
increaseViewportBy={increaseViewportBy}
172-
// height of past slots that the user is most likely to scroll through
173-
defaultItemHeight={42}
174-
skipAnimationFrameInResizeObserver
175-
computeItemKey={computeItemKey}
176-
itemContent={getItemContent}
177-
rangeChanged={rangeChanged}
178-
components={{ ScrollSeekPlaceholder: MScrollSeekPlaceHolder }}
179-
scrollSeekConfiguration={scrollSeekConfiguration}
180-
totalListHeightChanged={totalListHeightChanged}
59+
<VirtualSlotsList
60+
visibleWidth={width}
61+
visibleHeight={height}
62+
itemsCount={itemsCount}
63+
getSlotAtIndex={getSlotAtIndex}
64+
getIndexForSlot={getIndexForSlot}
65+
offsetHelpers={offsetHelpers}
18166
/>
182-
</Box>
67+
</div>
18368
);
184-
}
185-
186-
// Render nothing when scrolling quickly to improve performance
187-
const MScrollSeekPlaceHolder = memo(function ScrollSeekPlaceholder() {
188-
return null;
18969
});
19070

191-
interface RTAutoScrollProps {
192-
listRef: RefObject<VirtuosoHandle>;
193-
getIndexForSlot: (slot: number) => number | undefined;
194-
}
195-
const MRtAutoScroll = memo(function RTAutoScroll({
196-
listRef,
197-
getIndexForSlot,
198-
}: RTAutoScrollProps) {
199-
const currentLeaderSlot = useAtomValue(currentLeaderSlotAtom);
200-
const autoScroll = useAtomValue(autoScrollAtom);
201-
202-
useEffect(() => {
203-
if (!autoScroll || currentLeaderSlot === undefined || !listRef.current)
204-
return;
205-
206-
// scroll to new current leader slot
207-
const slotIndex = getIndexForSlot(currentLeaderSlot);
208-
const visibleStartIndex = slotIndex
209-
? Math.max(0, slotIndex - slotsListPinnedSlotOffset)
210-
: 0;
211-
212-
listRef.current.scrollToIndex({
213-
index: visibleStartIndex,
214-
align: "start",
215-
});
216-
}, [autoScroll, currentLeaderSlot, getIndexForSlot, listRef]);
217-
218-
return null;
219-
});
220-
221-
interface SlotOverrideScrollProps {
222-
listRef: RefObject<VirtuosoHandle>;
223-
getIndexForSlot: (slot: number) => number | undefined;
224-
debouncedScroll: DebouncedState<() => void>;
225-
}
226-
const MSlotOverrideScroll = memo(function SlotOverrideScroll({
227-
listRef,
228-
getIndexForSlot,
229-
debouncedScroll,
230-
}: SlotOverrideScrollProps) {
231-
const rafIdRef = useRef<number | null>(null);
232-
const slotOverride = useAtomValue(slotOverrideAtom);
233-
234-
useEffect(() => {
235-
if (
236-
slotOverride === undefined ||
237-
!listRef.current ||
238-
debouncedScroll.isPending()
239-
) {
240-
return;
241-
}
242-
243-
const slotIndex = getIndexForSlot(slotOverride);
244-
const targetIndex = slotIndex
245-
? Math.max(0, slotIndex - slotsListPinnedSlotOffset)
246-
: 0;
247-
248-
const prevRafId = rafIdRef.current;
249-
rafIdRef.current = requestAnimationFrame(() => {
250-
if (prevRafId !== null) {
251-
cancelAnimationFrame(prevRafId);
252-
}
253-
254-
listRef.current?.scrollToIndex({
255-
index: targetIndex,
256-
align: "start",
257-
});
258-
});
259-
260-
return () => {
261-
if (rafIdRef.current !== null) {
262-
cancelAnimationFrame(rafIdRef.current);
263-
rafIdRef.current = null;
264-
}
265-
};
266-
}, [getIndexForSlot, slotOverride, listRef, debouncedScroll]);
267-
268-
return null;
269-
});
270-
271-
function AllSlotsList({ width, height }: SlotsListProps) {
71+
const MAllSlotsList = memo(function AllSlotsList({
72+
width,
73+
height,
74+
}: SlotsListProps) {
27275
const epoch = useAtomValue(epochAtom);
76+
const currentSlot = useAtomValue(currentSlotAtom);
77+
const leaderSlots = useAtomValue(leaderSlotsAtom);
78+
const nextLeaderSlot = useAtomValue(nextLeaderSlotAtom);
27379

274-
const slotsListProps = useMemo(() => getAllSlotsListProps(epoch), [epoch]);
80+
const slotsListProps = useMemo(
81+
() => getAllSlotsListProps(epoch, currentSlot, leaderSlots, nextLeaderSlot),
82+
[epoch, currentSlot, leaderSlots, nextLeaderSlot],
83+
);
27584

27685
if (!slotsListProps) return null;
27786

278-
return <InnerSlotsList width={width} height={height} {...slotsListProps} />;
279-
}
87+
return <MInnerSlotsList width={width} height={height} {...slotsListProps} />;
88+
});
28089

281-
function MySlotsList({ width, height }: SlotsListProps) {
90+
const MMySlotsList = memo(function MySlotsList({
91+
width,
92+
height,
93+
}: SlotsListProps) {
28294
const mySlots = useAtomValue(leaderSlotsAtom);
283-
284-
const slotsListProps = useMemo(() => getMySlotsListProps(mySlots), [mySlots]);
95+
const currentSlot = useAtomValue(currentSlotAtom);
96+
const nextLeaderSlot = useAtomValue(nextLeaderSlotAtom);
97+
const isCurrentlyLeader = useAtomValue(isCurrentlyLeaderAtom);
98+
const nextLeaderSlotIndex = useAtomValue(nextLeaderSlotIndexAtom);
99+
100+
const slotsListProps = useMemo(
101+
() =>
102+
getMySlotsListProps(
103+
mySlots,
104+
currentSlot,
105+
nextLeaderSlot,
106+
isCurrentlyLeader,
107+
nextLeaderSlotIndex,
108+
),
109+
[
110+
mySlots,
111+
currentSlot,
112+
nextLeaderSlot,
113+
isCurrentlyLeader,
114+
nextLeaderSlotIndex,
115+
],
116+
);
285117

286118
if (!slotsListProps) return null;
287119

@@ -302,5 +134,5 @@ function MySlotsList({ width, height }: SlotsListProps) {
302134
);
303135
}
304136

305-
return <InnerSlotsList width={width} height={height} {...slotsListProps} />;
306-
}
137+
return <MInnerSlotsList width={width} height={height} {...slotsListProps} />;
138+
});

src/features/Navigation/SlotsRenderer.tsx

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -101,7 +101,12 @@ function YourNextLeaderSlotGroup({ firstSlot }: { firstSlot: number }) {
101101
return (
102102
<Flex
103103
direction="column"
104-
className={clsx(styles.slotGroup, styles.future, styles.you)}
104+
className={clsx(
105+
styles.slotGroup,
106+
styles.future,
107+
styles.you,
108+
styles.nextYou,
109+
)}
105110
>
106111
<Flex justify="between" gap="2px">
107112
<MSlotIconName slot={firstSlot} />

0 commit comments

Comments
 (0)