From 5202a19b528b7e7f9389458a19134882a5069705 Mon Sep 17 00:00:00 2001 From: Roland Osborne Date: Wed, 4 May 2022 00:50:31 -0700 Subject: [PATCH] refactor of virtual list --- .../User/Conversation/useConversation.hook.js | 3 +- net/web/src/VirtualList/VirtualList.jsx | 260 +++++++++--------- .../context/useConversationContext.hook.js | 50 ++-- 3 files changed, 160 insertions(+), 153 deletions(-) diff --git a/net/web/src/User/Conversation/useConversation.hook.js b/net/web/src/User/Conversation/useConversation.hook.js index af6dc6d4..36e00935 100644 --- a/net/web/src/User/Conversation/useConversation.hook.js +++ b/net/web/src/User/Conversation/useConversation.hook.js @@ -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]); diff --git a/net/web/src/VirtualList/VirtualList.jsx b/net/web/src/VirtualList/VirtualList.jsx index 1ed46240..6a25903c 100644 --- a/net/web/src/VirtualList/VirtualList.jsx +++ b/net/web/src/VirtualList/VirtualList.jsx @@ -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 ( -
+
{ slots.values() }
diff --git a/net/web/src/context/useConversationContext.hook.js b/net/web/src/context/useConversationContext.hook.js index 1d724f26..2fa3b237 100644 --- a/net/web/src/context/useConversationContext.hook.js +++ b/net/web/src/context/useConversationContext.hook.js @@ -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) => {