mirror of
https://github.com/balzack/databag.git
synced 2025-02-12 03:29:16 +00:00
refactor of virtual list
This commit is contained in:
parent
641b553999
commit
5202a19b52
@ -29,12 +29,13 @@ export function useConversation() {
|
||||
|
||||
useEffect(() => {
|
||||
conversation.actions.setConversationId(cardId, channelId);
|
||||
updateState({ cardId, channelId });
|
||||
}, [cardId, channelId]);
|
||||
|
||||
useEffect(() => {
|
||||
updateState({
|
||||
init: conversation.state.init,
|
||||
cardId: conversation.state.cardId,
|
||||
channelId: conversation.state.channelId,
|
||||
topics: Array.from(conversation.state.topics.values()),
|
||||
});
|
||||
}, [conversation]);
|
||||
|
@ -4,10 +4,10 @@ import ReactResizeDetector from 'react-resize-detector';
|
||||
|
||||
export function VirtualList({ id, items, itemRenderer }) {
|
||||
|
||||
const REDZONE = 256; // recenter on canvas if in canvas edge redzone
|
||||
const HOLDZONE = 1024; // drop slots outside of holdzone of view
|
||||
const OVERSCAN = 256; // add slots in overscan of view
|
||||
const DEFAULT_ITEM_HEIGHT = 64;
|
||||
const REDZONE = 1024; // recenter on canvas if in canvas edge redzone
|
||||
const HOLDZONE = 2048; // drop slots outside of holdzone of view
|
||||
const OVERSCAN = 1024; // add slots in overscan of view
|
||||
const DEFAULT_ITEM_HEIGHT = 256;
|
||||
const DEFAULT_LIST_HEIGHT = 4096;
|
||||
const GUTTER = 6;
|
||||
|
||||
@ -16,10 +16,10 @@ export function VirtualList({ id, items, itemRenderer }) {
|
||||
const [ slots, setSlots ] = useState(new Map());
|
||||
const [ scroll, setScroll ] = useState('hidden');
|
||||
|
||||
let update = useRef(0);
|
||||
let latch = useRef(true);
|
||||
let scrollTop = useRef(0);
|
||||
let containers = useRef([]);
|
||||
let anchorBottom = useRef(true);
|
||||
let listRef = useRef();
|
||||
let key = useRef(null);
|
||||
|
||||
@ -32,7 +32,7 @@ export function VirtualList({ id, items, itemRenderer }) {
|
||||
}
|
||||
|
||||
const removeSlot = (id) => {
|
||||
setSlots((m) => { m.delete(id); return new Map(m); })
|
||||
setSlots((m) => { m.delete(id); return new Map(m); });
|
||||
}
|
||||
|
||||
const clearSlots = () => {
|
||||
@ -52,7 +52,7 @@ export function VirtualList({ id, items, itemRenderer }) {
|
||||
if (viewHeight * 3 > canvasHeight) {
|
||||
growCanvasHeight(viewHeight * 3);
|
||||
}
|
||||
setItems();
|
||||
updateCanvas();
|
||||
}, [viewHeight]);
|
||||
|
||||
useEffect(() => {
|
||||
@ -60,48 +60,63 @@ export function VirtualList({ id, items, itemRenderer }) {
|
||||
key.current = id;
|
||||
latch.current = true;
|
||||
containers.current = [];
|
||||
anchorBottom.current = true;
|
||||
scrollTop.current = 0;
|
||||
listRef.current.scrollTo({ top: scrollTop.current, left: 0 });
|
||||
clearSlots();
|
||||
}
|
||||
setItems();
|
||||
}, [items, id]);
|
||||
|
||||
useEffect(() => {
|
||||
if (latch.current) {
|
||||
alignSlots();
|
||||
}
|
||||
updateCanvas();
|
||||
}, [viewHeight, canvasHeight]);
|
||||
|
||||
const onScrollWheel = (e) => {
|
||||
if (e.deltaY < 0 && latch.current) {
|
||||
scrollTop.current -= 32;
|
||||
listRef.current.scrollTo({ top: scrollTop.current, left: 0, behavior: 'smooth' });
|
||||
setScroll('auto');
|
||||
latch.current = false;
|
||||
}
|
||||
}
|
||||
|
||||
const onScrollView = (e) => {
|
||||
|
||||
scrollTop.current = e.target.scrollTop;
|
||||
loadNextSlot();
|
||||
dropSlots();
|
||||
centerCanvas();
|
||||
limitScroll();
|
||||
}
|
||||
|
||||
if (!latch.current) {
|
||||
let view = getPlacement();
|
||||
const updateCanvas = () => {
|
||||
alignSlots();
|
||||
loadNextSlot();
|
||||
dropSlots();
|
||||
centerCanvas();
|
||||
limitScroll();
|
||||
latchScroll();
|
||||
}
|
||||
|
||||
const limitScroll = () => {
|
||||
let view = getPlacement();
|
||||
if (view && containers.current[containers.current.length - 1].index == items.length - 1) {
|
||||
if (view?.overscan?.bottom <= 0) {
|
||||
if (view.position.height < viewHeight) {
|
||||
if (scrollTop.current != view.position.top) {
|
||||
scrollTop.current = view.position.top;
|
||||
listRef.current.scrollTo({ top: scrollTop.current, left: 0 });
|
||||
}
|
||||
}
|
||||
else {
|
||||
if (scrollTop.current != view.position.bottom - viewHeight) {
|
||||
scrollTop.current = view.position.bottom - viewHeight;
|
||||
listRef.current.scrollTo({ top: scrollTop.current, left: 0 });
|
||||
}
|
||||
}
|
||||
latch.current = true;
|
||||
}
|
||||
}
|
||||
if (view && containers.current[0].index == 0) {
|
||||
if (view?.overscan?.top <= 0) {
|
||||
scrollTop.current = containers.current[0].top;
|
||||
listRef.current.scrollTo({ top: scrollTop.current, left: 0 });
|
||||
}
|
||||
if (view?.overscan?.bottom <= 0) {
|
||||
setScroll('hidden');
|
||||
latch.current = true;
|
||||
alignSlots();
|
||||
listRef.current.scrollTo({ top: scrollTop.current, left: 0 });
|
||||
}
|
||||
}
|
||||
|
||||
loadNextSlot();
|
||||
}
|
||||
|
||||
const loadNextSlot = () => {
|
||||
@ -119,14 +134,7 @@ export function VirtualList({ id, items, itemRenderer }) {
|
||||
}
|
||||
containers.current.unshift(container);
|
||||
addSlot(container.id, getSlot(container))
|
||||
anchorBottom.current = true;
|
||||
|
||||
if (containers.current[containers.current.length - 1].top > scrollTop.current + viewHeight + HOLDZONE) {
|
||||
removeSlot(containers.current[containers.current.length - 1].id);
|
||||
containers.current.pop();
|
||||
}
|
||||
|
||||
alignSlots();
|
||||
loadNextSlot();
|
||||
}
|
||||
}
|
||||
if (view.overscan.bottom < OVERSCAN) {
|
||||
@ -141,127 +149,113 @@ export function VirtualList({ id, items, itemRenderer }) {
|
||||
}
|
||||
containers.current.push(container);
|
||||
addSlot(container.id, getSlot(container))
|
||||
anchorBottom.current = false;
|
||||
|
||||
if (containers.current[0].top + containers.current[0].height + 2 * GUTTER + HOLDZONE < scrollTop.current) {
|
||||
removeSlot(containers.current[0].id);
|
||||
containers.current.shift();
|
||||
}
|
||||
|
||||
alignSlots();
|
||||
loadNextSlot();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const dropSlots = () => {
|
||||
while (containers.current.length > 0 &&
|
||||
containers.current[0].top + containers.current[0].height + 2 * GUTTER + HOLDZONE < scrollTop.current) {
|
||||
removeSlot(containers.current[0].id);
|
||||
containers.current.shift();
|
||||
}
|
||||
while (containers.current.length > 0 &&
|
||||
containers.current[containers.current.length - 1].top > scrollTop.current + viewHeight + HOLDZONE) {
|
||||
removeSlot(containers.current[containers.current.length - 1].id);
|
||||
containers.current.pop();
|
||||
}
|
||||
}
|
||||
|
||||
const alignSlots = () => {
|
||||
if (containers.current.length > 0) {
|
||||
if (containers.current.length > 1) {
|
||||
let mid = Math.floor(containers.current.length / 2);
|
||||
|
||||
if (anchorBottom.current) {
|
||||
let pos = containers.current[containers.current.length - 1].top;
|
||||
for (let i = containers.current.length - 2; i >= 0; i--) {
|
||||
pos -= (containers.current[i].height + 2 * GUTTER);
|
||||
if (containers.current[i].top != pos) {
|
||||
containers.current[i].top = pos;
|
||||
updateSlot(containers.current[i].id, getSlot(containers.current[i]));
|
||||
}
|
||||
}
|
||||
|
||||
if (pos < REDZONE) {
|
||||
let shift = canvasHeight / 2;
|
||||
for (let i = 0; i < containers.current.length; i++) {
|
||||
containers.current[i].top += shift;
|
||||
updateSlot(containers.current[i].id, getSlot(containers.current[i]));
|
||||
}
|
||||
scrollTop.current += shift;
|
||||
listRef.current.scrollTo({ top: scrollTop.current, left: 0 });
|
||||
|
||||
let view = getPlacement();
|
||||
if (view.position.bottom + REDZONE > canvasHeight) {
|
||||
growCanvasHeight(view.position.bottom + REDZONE);
|
||||
}
|
||||
}
|
||||
}
|
||||
else {
|
||||
let pos = containers.current[0].top + containers.current[0].height + 2 * GUTTER;
|
||||
for (let i = 1; i < containers.current.length; i++) {
|
||||
if (containers.current[i].top != pos) {
|
||||
containers.current[i].top = pos;
|
||||
updateSlot(containers.current[i].id, getSlot(containers.current[i]));
|
||||
}
|
||||
pos += containers.current[i].height + 2 * GUTTER;
|
||||
}
|
||||
|
||||
if (pos + REDZONE > canvasHeight) {
|
||||
let shift = canvasHeight / 2;
|
||||
let view = getPlacement();
|
||||
if (view.position.top < shift + REDZONE) {
|
||||
growCanvasHeight(view.position.bottom + REDZONE);
|
||||
}
|
||||
else {
|
||||
for (let i = 0; i < containers.current.length; i++) {
|
||||
containers.current[i].top -= shift;
|
||||
updateSlot(containers.current[i].id, getSlot(containers.current[i]));
|
||||
}
|
||||
scrollTop.current -= shift;
|
||||
listRef.current.scrollTo({ top: scrollTop.current, left: 0 });
|
||||
}
|
||||
let alignTop = containers.current[mid].top + containers.current[mid].height + 2 * GUTTER;
|
||||
for (let i = mid + 1; i < containers.current.length; i++) {
|
||||
if (containers.current[i].top != alignTop) {
|
||||
containers.current[i].top = alignTop;
|
||||
updateSlot(containers.current[i].id, getSlot(containers.current[i]));
|
||||
}
|
||||
alignTop += containers.current[i].height + 2 * GUTTER;
|
||||
}
|
||||
|
||||
let alignBottom = containers.current[mid].top;
|
||||
for (let i = mid - 1; i >= 0; i--) {
|
||||
alignBottom -= (containers.current[i].height + 2 * GUTTER);
|
||||
if (containers.current[i].top != alignBottom) {
|
||||
containers.current[i].top = alignBottom;
|
||||
updateSlot(containers.current[i].id, getSlot(containers.current[i]));
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const centerCanvas = () => {
|
||||
let view = getPlacement();
|
||||
if (view) {
|
||||
let height = canvasHeight;
|
||||
let resize = view.position.height * 3 + (2 * REDZONE) + (2 * HOLDZONE);
|
||||
if (resize > canvasHeight) {
|
||||
height = 2 * resize;
|
||||
growCanvasHeight(height);
|
||||
}
|
||||
if (view.position.top < REDZONE) {
|
||||
let shift = height / 2;
|
||||
for (let i = 0; i < containers.current.length; i++) {
|
||||
containers.current[i].top += shift;
|
||||
updateSlot(containers.current[i].id, getSlot(containers.current[i]));
|
||||
}
|
||||
scrollTop.current += shift;
|
||||
listRef.current.scrollTo({ top: scrollTop.current, left: 0 });
|
||||
}
|
||||
else if (view.position.bottom + REDZONE > height) {
|
||||
let shift = height / 2;
|
||||
for (let i = 0; i < containers.current.length; i++) {
|
||||
containers.current[i].top -= shift;
|
||||
updateSlot(containers.current[i].id, getSlot(containers.current[i]));
|
||||
}
|
||||
scrollTop.current -= shift;
|
||||
listRef.current.scrollTo({ top: scrollTop.current, left: 0 });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const latchScroll = () => {
|
||||
if (latch.current) {
|
||||
let view = getPlacement();
|
||||
if (latch.current) {
|
||||
if (view) {
|
||||
if (view.position.height < viewHeight) {
|
||||
if (scrollTop.current != view.position.top) {
|
||||
listRef.current.scrollTo({ top: view.position.top, left: 0, behavior: 'smooth' });
|
||||
scrollTop.current = view.position.top;
|
||||
listRef.current.scrollTo({ top: scrollTop.current, left: 0, behavior: 'smooth' });
|
||||
}
|
||||
}
|
||||
else {
|
||||
if (scrollTop.current != view.position.bottom - viewHeight) {
|
||||
listRef.current.scrollTo({ top: view.position.bottom - viewHeight, left: 0, behavior: 'smooth' });
|
||||
scrollTop.current = view.position.bottom - viewHeight;
|
||||
listRef.current.scrollTo({ top: scrollTop.current, left: 0, behavior: 'smooth' });
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
loadNextSlot();
|
||||
}
|
||||
|
||||
const setItems = () => {
|
||||
|
||||
// update or removed any affected slots
|
||||
if (anchorBottom.current) {
|
||||
for (let i = containers.current.length - 1; i >= 0; i--) {
|
||||
let container = containers.current[i];
|
||||
if (items.length <= container.index || items[container.index].id != container.id) {
|
||||
for (let j = 0; j <= i; j++) {
|
||||
let shifted = containers.current.shift();
|
||||
removeSlot(shifted.id);
|
||||
}
|
||||
break;
|
||||
}
|
||||
else if (items[container.index].revision != container.revision) {
|
||||
updateSlot(container.id, getSlot(containers.current[i]));
|
||||
containers.revision = items[container.index].revision;
|
||||
for (let i = 0; i < containers.current.length; i++) {
|
||||
let container = containers.current[i];
|
||||
if (items.length <= container.index || items[container.index].id != container.id) {
|
||||
for (let j = i; j < containers.current.length; j++) {
|
||||
let popped = containers.current.pop();
|
||||
removeSlot(popped.id);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
else {
|
||||
for (let i = 0; i < containers.current.length; i++) {
|
||||
let container = containers.current[i];
|
||||
if (items.length <= container.index || items[container.index].id != container.id) {
|
||||
for (let j = i; j < containers.current.length; j++) {
|
||||
let popped = containers.current.pop();
|
||||
removeSlot(popped.id);
|
||||
}
|
||||
break;
|
||||
}
|
||||
else if (items[container.index].revision != container.revision) {
|
||||
updateSlot(container.id, getSlot(containers.current[i]));
|
||||
containers.revision = items[container.index].revision;
|
||||
}
|
||||
else if (items[container.index].revision != container.revision) {
|
||||
updateSlot(container.id, getSlot(containers.current[i]));
|
||||
containers.revision = items[container.index].revision;
|
||||
}
|
||||
}
|
||||
|
||||
@ -281,21 +275,17 @@ export function VirtualList({ id, items, itemRenderer }) {
|
||||
revision: items[items.length - 1].revision,
|
||||
}
|
||||
|
||||
anchorBottom.current = true;
|
||||
containers.current.push(container);
|
||||
addSlot(container.id, getSlot(container));
|
||||
|
||||
listRef.current.scrollTo({ top: container.top, left: 0, behavior: 'smooth' });
|
||||
}
|
||||
else {
|
||||
loadNextSlot();
|
||||
}
|
||||
}
|
||||
|
||||
updateCanvas();
|
||||
}
|
||||
|
||||
const onItemHeight = (container, height) => {
|
||||
container.height = height;
|
||||
alignSlots();
|
||||
updateCanvas();
|
||||
}
|
||||
|
||||
const getSlot = (container) => {
|
||||
@ -333,7 +323,7 @@ export function VirtualList({ id, items, itemRenderer }) {
|
||||
setViewHeight(height);
|
||||
return (
|
||||
<VirtualListWrapper onScroll={onScrollView} onWheel={onScrollWheel}>
|
||||
<div class="rollview" style={{ overflowY: scroll }} ref={listRef} onScroll={onScrollView}>
|
||||
<div class="rollview" style={{ overflowY: 'auto' }} ref={listRef} onScroll={onScrollView}>
|
||||
<div class="roll" style={{ height: canvasHeight }}>
|
||||
{ slots.values() }
|
||||
</div>
|
||||
|
@ -6,6 +6,8 @@ export function useConversationContext() {
|
||||
|
||||
const [state, setState] = useState({
|
||||
init: false,
|
||||
cardId: null,
|
||||
channelId: null,
|
||||
topics: new Map(),
|
||||
});
|
||||
|
||||
@ -15,6 +17,7 @@ export function useConversationContext() {
|
||||
const revision = useRef(null);
|
||||
const count = useRef(0);
|
||||
const conversationId = useRef(null);
|
||||
const view = useRef(0);
|
||||
|
||||
const updateState = (value) => {
|
||||
setState((s) => ({ ...s, ...value }));
|
||||
@ -22,11 +25,13 @@ export function useConversationContext() {
|
||||
|
||||
const setTopics = async () => {
|
||||
const { cardId, channelId } = conversationId.current;
|
||||
const curRevision = revision.current;
|
||||
const curView = view.current;
|
||||
|
||||
if (cardId) {
|
||||
let rev = card.actions.getChannelRevision(cardId, channelId);
|
||||
if (revision.current != rev) {
|
||||
let delta = await card.actions.getChannelTopics(cardId, channelId, revision.current);
|
||||
let deltaRevision = card.actions.getChannelRevision(cardId, channelId);
|
||||
if (curRevision != deltaRevision) {
|
||||
let delta = await card.actions.getChannelTopics(cardId, channelId, curRevision);
|
||||
for (let topic of delta) {
|
||||
if (topic.data == null) {
|
||||
topics.current.delete(topic.id);
|
||||
@ -51,17 +56,22 @@ export function useConversationContext() {
|
||||
topics.current.set(topic.id, cur);
|
||||
}
|
||||
}
|
||||
updateState({
|
||||
init: true,
|
||||
topics: topics.current,
|
||||
});
|
||||
revision.current = rev;
|
||||
if (curView == view.current) {
|
||||
updateState({
|
||||
init: true,
|
||||
topics: topics.current,
|
||||
});
|
||||
revision.current = deltaRevision;
|
||||
}
|
||||
else {
|
||||
topics.current = new Map();
|
||||
}
|
||||
}
|
||||
}
|
||||
else {
|
||||
let rev = channel.actions.getChannelRevision(channelId);
|
||||
if (revision.current != rev) {
|
||||
let delta = await channel.actions.getChannelTopics(channelId, revision.current);
|
||||
let deltaRevision = channel.actions.getChannelRevision(channelId);
|
||||
if (curRevision != deltaRevision) {
|
||||
let delta = await channel.actions.getChannelTopics(channelId, curRevision);
|
||||
for (let topic of delta) {
|
||||
if (topic.data == null) {
|
||||
topics.current.delete(topic.id);
|
||||
@ -86,11 +96,16 @@ export function useConversationContext() {
|
||||
topics.current.set(topic.id, cur);
|
||||
}
|
||||
}
|
||||
updateState({
|
||||
init: true,
|
||||
topics: topics.current,
|
||||
});
|
||||
revision.current = rev;
|
||||
if (curView == view.current) {
|
||||
updateState({
|
||||
init: true,
|
||||
topics: topics.current,
|
||||
});
|
||||
revision.current = deltaRevision;
|
||||
}
|
||||
else {
|
||||
topics.current = new Map();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -133,10 +148,11 @@ export function useConversationContext() {
|
||||
|
||||
const actions = {
|
||||
setConversationId: (cardId, channelId) => {
|
||||
view.current += 1;
|
||||
conversationId.current = { cardId, channelId };
|
||||
revision.current = null;
|
||||
topics.current = new Map();
|
||||
updateState({ init: false, topics: topics.current });
|
||||
updateState({ init: false, cardId, channelId, topics: topics.current });
|
||||
updateConversation();
|
||||
},
|
||||
getAssetUrl: (topicId, assetId) => {
|
||||
|
Loading…
Reference in New Issue
Block a user