removing custom implementation of virtuallist now that i discovered flex-column-reverse (DUH!)

This commit is contained in:
Roland Osborne 2022-10-27 15:21:46 -07:00
parent 5ce0c4ad75
commit 4fdba26371
7 changed files with 30 additions and 481 deletions

View File

@ -44,6 +44,7 @@ export function useConversationContext() {
const profile = useContext(ProfileContext);
const topics = useRef(new Map());
const view = useRef(0);
const more = useRef(true);
const serialize = useRef(0);
const updateState = (value) => {
@ -268,9 +269,15 @@ export function useConversationContext() {
updateConversation();
},
addHistory: () => {
if (more.current && !state.loadingMore) {
more.current = false;
updateState({ loadingMore: true });
events.current.push({ type: EVENT_MORE });
updateConversation();
setTimeout(() => {
more.current = true;
}, 2000);
}
},
setChannelSubject: async (subject) => {
return await channel.actions.setChannelSubject(channelView.current.channelId, subject);

View File

@ -1,20 +1,31 @@
import { useRef } from 'react';
import { ConversationWrapper, StatusError } from './Conversation.styled';
import { ExclamationCircleOutlined, SettingOutlined, CloseOutlined } from '@ant-design/icons';
import { useConversation } from './useConversation.hook';
import { Logo } from 'logo/Logo';
import { AddTopic } from './addTopic/AddTopic';
import { VirtualList } from './virtualList/VirtualList';
import { TopicItem } from './topicItem/TopicItem';
import { Spin, Tooltip } from 'antd';
import { List, Spin, Tooltip } from 'antd';
export function Conversation({ closeConversation, openDetails, cardId, channelId }) {
const { state, actions } = useConversation(cardId, channelId);
const thread = useRef(null);
const topicRenderer = (topic) => {
return (<TopicItem host={cardId == null} topic={topic} />)
}
const scrollThread = (e) => {
const content = thread.current?.scrollHeight;
const frame = thread.current?.clientHeight;
const position = e.target.scrollTop;
const above = content - (Math.abs(position) + frame);
if (above < 1024) {
actions.more();
}
};
return (
<ConversationWrapper>
<div class="header">
@ -47,9 +58,9 @@ export function Conversation({ closeConversation, openDetails, cardId, channelId
</div>
)}
</div>
<div class="thread">
<VirtualList id={`${cardId}:${channelId}`}
items={state.topics} itemRenderer={topicRenderer} loadMore={actions.more} />
<div class="thread" ref={thread} onScroll={scrollThread}>
<List local={{ emptyText: '' }} itemLayout="horizontal" dataSource={state.topics} gutter="0"
renderItem={topicRenderer} />
{ state.loadingInit && (
<div class="loading">
<Spin size="large" delay={250} />

View File

@ -54,6 +54,9 @@ export const ConversationWrapper = styled.div`
position: relative;
flex-grow: 1;
min-height: 0;
overflow: auto;
display: flex;
flex-direction: column-reverse;
.loading {
position: absolute;

View File

@ -105,6 +105,7 @@ export function useConversation(cardId, channelId) {
}, [cardId, channelId, card, channel]);
useEffect(() => {
updateState({ topics: [] });
conversation.actions.setConversationId(cardId, channelId);
// eslint-disable-next-line
}, [cardId, channelId]);

View File

@ -1,392 +0,0 @@
import React, { useRef, useState, useEffect } from 'react';
import { VirtualListWrapper, VirtualItem } from './VirtualList.styled';
import ReactResizeDetector from 'react-resize-detector';
import { useVirtualList } from './useVirtualList.hook';
export function VirtualList({ id, items, itemRenderer, loadMore }) {
const redZone = 1024;
const holdZone = 2048;
const fillZone = 1024;
const pushDelay = 250;
const moreDelay = 2000;
const latchDelay = 500;
const pad = 4;
const defaultHeight = 128;
const rollHeight = 16384;
const { state, actions } = useVirtualList();
const containers = useRef([]);
const itemView = useRef([]);
const debounce = useRef([]);
const nomore = useRef(false);
const nolatch = useRef(false);
const list = useRef(null);
const scrollTop = useRef(0);
const latched = useRef(true);
const [scrollPos, setScrollPos] = useState(0);
useEffect(() => {
latched.current = true;
actions.clearSlots();
scrollTop.current = 8192;
list.current.scrollTo({ top: 8192, left: 0 });
// eslint-disable-next-line
}, [id]);
useEffect(() => {
// reference copy
itemView.current = items;
// genearte set of active ids
let ids = new Map();
for (let i = 0; i < itemView.current.length; i++) {
ids.set(getItemKey(itemView.current[i]), i);
}
// remove any deleted items
let slots = [];
containers.current.forEach((container) => {
if (!ids.has(container.key)) {
actions.removeSlot(container.key);
}
else {
container.index = ids.get(container.key);
container.item = itemView.current[container.index];
slots.push(container);
}
});
containers.current = slots;
// sort by index
containers.current.sort((a, b) => {
if (a.index < b.index) {
return -1;
}
return 1;
});
// rerender list
layoutItems();
// eslint-disable-next-line
}, [items]);
useEffect(() => {
layoutItems();
// eslint-disable-next-line
}, [scrollPos, state.listHeight, state.view]);
const layoutItems = () => {
alignSlots();
loadSlots();
releaseSlots();
centerSlots();
latchSlots();
pushSlots();
};
const latchSlots = () => {
if (containers.current.length > 0 && latched.current && state.listHeight > 0) {
if (!nolatch.current) {
const last = containers.current[containers.current.length - 1];
const bottom = last.top + last.height;
if (last.heightSet && scrollTop.current < bottom - (state.listHeight - pad)) {
list.current.scrollTo({ top: bottom - (state.listHeight - pad), left: 0, behavior: 'smooth' });
nolatch.current = true;
setTimeout(() => {
nolatch.current = false;
latchSlots();
}, latchDelay);
}
}
}
}
const pushSlots = () => {
if (debounce.current != null) {
clearTimeout(debounce.current);
};
debounce.current = setTimeout(() => {
if (containers.current.length > 0) {
const range = getContainerRange();
if (range.bottom - range.top < (state.listHeight - pad)) {
if (scrollTop.current + (state.listHeight - pad) !== range.bottom) {
list.current.scrollTo({ top: range.bottom - (state.listHeight - pad), left: 0 });
latched.current = true;
}
}
else if (scrollTop.current + (state.listHeight - pad) > range.bottom) {
list.current.scrollTo({ top: range.bottom - (state.listHeight - pad), left: 0 });
latched.current = true;
}
else if (scrollTop.current < range.top) {
list.current.scrollTo({ top: range.top, left: 0 });
}
}
}, pushDelay);
};
const alignSlots = () => {
if (containers.current.length > 1) {
if (latched.current) {
const index = containers.current.length - 1;
const last = containers.current[index];
let bottom = last.top + last.height;
for (let i = index; i >= 0; i--) {
let container = containers.current[i];
if (container.top + container.height !== bottom) {
container.top = bottom - container.height;
actions.updateSlot(container.key, getSlot(container));
}
bottom -= container.height;
}
}
else {
const index = Math.floor(containers.current.length / 2);
const mid = containers.current[index];
let top = mid.top;
for (let i = index; i < containers.current.length; i++) {
let container = containers.current[i];
if (container.top !== top) {
container.top = top;
actions.updateSlot(container.key, getSlot(container));
}
top += container.height;
}
let bottom = mid.top + mid.height;
for (let i = index; i >= 0; i--) {
let container = containers.current[i];
if (container.top + container.height !== bottom) {
container.top = bottom - container.height;
actions.updateSlot(container.key, getSlot(container));
}
bottom -= container.height;
}
}
}
}
const loadSlots = () => {
if (state.listHeight === 0) {
return;
}
if (containers.current.length === 0) {
// add the first slot
if (itemView.current.length > 0) {
let item = itemView.current[itemView.current.length - 1];
let slot = {
top: rollHeight / 2 + (state.listHeight - pad),
height: defaultHeight,
heightSet: false,
key: getItemKey(item),
index: itemView.current.length - 1,
item: item,
}
containers.current.push(slot);
actions.addSlot(slot.key, getSlot(slot));
list.current.scrollTo({ top: rollHeight / 2, left: 0 });
}
}
else {
// fill in any missing slots
let index = containers.current[0].index;
for (let i = 1; i < containers.current.length; i++) {
let container = containers.current[i];
if (container.index !== index + i) {
const item = itemView.current[index + i];
let slot = {
top: container.top - defaultHeight,
height: defaultHeight,
heightSet: false,
index: index + i,
item: item,
key: getItemKey(item),
}
containers.current.splice(i, 0, slot);
actions.addSlot(slot.key, getSlot(slot));
}
}
}
loadSlotAbove();
loadSlotBelow();
};
const loadSlotAbove = () => {
if (containers.current.length > 0) {
const range = getContainerRange();
if (scrollTop.current - fillZone < range.top) {
const container = containers.current[0];
if (container.index > 0) {
const index = container.index - 1;
const item = itemView.current[index];
let slot = {
top: container.top - defaultHeight,
height: defaultHeight,
heightSet: false,
index: index,
item: item,
key: getItemKey(item),
}
containers.current.unshift(slot);
actions.addSlot(slot.key, getSlot(slot));
loadSlotAbove();
}
}
}
}
const loadSlotBelow = () => {
if (containers.current.length > 0) {
const container = containers.current[containers.current.length - 1];
if (container.index + 1 < itemView.current.length) {
const range = getContainerRange();
if (scrollTop.current + (state.listHeight - pad) + fillZone > range.bottom) {
const index = container.index + 1;
const item = itemView.current[index];
let slot = {
top: container.top + container.height,
height: defaultHeight,
heightSet: false,
index: index,
item: item,
key: getItemKey(item),
}
containers.current.push(slot);
actions.addSlot(slot.key, getSlot(slot));
loadSlotBelow();
}
}
}
}
const releaseSlots = () => {
releaseSlotAbove();
releaseSlotBelow();
};
const releaseSlotAbove = () => {
if (containers.current.length > 1) {
const container = containers.current[0];
if (container.top + container.height < scrollTop.current - holdZone) {
actions.removeSlot(container.key);
containers.current.shift();
releaseSlotAbove();
}
}
}
const releaseSlotBelow = () => {
if (containers.current.length > 1) {
const container = containers.current[containers.current.length - 1];
if (container.top > scrollTop.current + state.listHeight + holdZone) {
actions.removeSlot(container.key);
containers.current.pop();
releaseSlotBelow();
}
}
}
const centerSlots = () => {
if (containers.current.length > 0) {
const top = containers.current[0];
if (top.top < redZone) {
containers.current.forEach(container => {
container.top += rollHeight / 2;
actions.updateSlot(container.key, getSlot(container));
});
list.current.scrollTo({ top: scrollTop.current + rollHeight / 2, left: 0 });
}
const bottom = containers.current[containers.current.length - 1];
if (bottom.top + bottom.height > rollHeight - redZone) {
containers.current.forEach(container => {
container.top -= rollHeight / 2;
actions.updateSlot(container.key, getSlot(container));
});
list.current.scrollTo({ top: scrollTop.current - rollHeight / 2, left: 0 });
}
}
};
const getItemKey = (item) => {
return `${id}.${item.id}.${item.revision}`
}
const getSlot = (item) => {
const container = item;
return (
<VirtualItem style={{ top: container.top, key: container.key }}>
<ReactResizeDetector handleHeight={true}>
{({ height }) => {
if (typeof height !== 'undefined' && container.height !== height) {
container.height = height;
container.heightSet = true;
layoutItems();
}
return itemRenderer(container.item);
}}
</ReactResizeDetector>
</VirtualItem>
)
}
const getContainerRange = () => {
let top = rollHeight;
let bottom = 0;
containers.current.forEach((c) => {
if (c.top < top) {
top = c.top;
}
if (c.top + c.height > bottom) {
bottom = c.top + c.height;
}
});
return { top, bottom };
}
const scrollView = (e) => {
if (containers.current.length > 0 && containers.current[0].index === 0 && !nomore.current) {
loadMore();
nomore.current = true;
setTimeout(() => {
nomore.current = false;
}, moreDelay);
}
scrollTop.current = e.target.scrollTop;
setScrollPos(e.target.scrollTop);
}
const unlatch = () => {
latched.current = false;
}
return (
<div style={{ position: 'relative', height: '100%' }}>
<ReactResizeDetector handleHeight={true} handleWidth={false}>
{({ height }) => {
if (height && state.listHeight !== height) {
actions.setListHeight(height);
}
return (
<VirtualListWrapper onScroll={scrollView}
onWheel={unlatch} onTouchStart={unlatch} >
<div class="rollview" ref={list} onScroll={scrollView}>
<div class="roll" style={{ height: rollHeight }}>
{ state.slots }
</div>
</div>
</VirtualListWrapper>
)
}}
</ReactResizeDetector>
</div>
)
}

View File

@ -1,33 +0,0 @@
import styled from 'styled-components';
export const VirtualListWrapper = styled.div`
width: 100%;
height: 100%;
overflow: hidden;
.rollview {
width: 100%;
height: 100%;
overflow-y: auto;
/* hide scrollbar for IE, Edge and Firefox */
-ms-overflow-style: none;
scrollbar-width: none;
}
.rollview::-webkit-scrollbar {
display: none;
}
.roll {
width: 100%;
position: relative;
}
`;
export const VirtualItem = styled.div`
position: absolute;
width: 100%;
overflow: hidden;
max-height: 1024px;
`;

View File

@ -1,48 +0,0 @@
import { useState, useRef } from 'react';
export function useVirtualList(id) {
const [state, setState] = useState({
view: null,
listHeight: 0,
slots: [],
});
const slots = useRef(new Map());
const updateState = (value) => {
setState((s) => ({ ...s, ...value }));
}
const actions = {
setView: (view) => {
updateState({ view });
},
setListHeight: (listHeight) => {
updateState({ listHeight: listHeight });
},
addSlot: (id, slot) => {
slots.current.set(id, slot);
let items = Array.from(slots.current.values());
updateState({ slots: items });
},
updateSlot: (id, slot) => {
slots.current.set(id, slot);
let items = Array.from(slots.current.values());
updateState({ slots: items });
},
removeSlot: (id) => {
slots.current.set(id, (<></>));
let items = Array.from(slots.current.values());
updateState({ slots: items });
},
clearSlots: () => {
slots.current = new Map();
let items = Array.from(slots.current.values());
updateState({ slots: items });
},
};
return { state, actions };
}