mirror of
https://github.com/urbit/shrub.git
synced 2024-12-19 08:32:39 +03:00
virtualscroller: ios viable
This commit is contained in:
parent
a960234e2c
commit
89f63ac443
6
pkg/interface/src/logic/lib/platform.ts
Normal file
6
pkg/interface/src/logic/lib/platform.ts
Normal file
@ -0,0 +1,6 @@
|
||||
|
||||
const ua = window.navigator.userAgent;
|
||||
|
||||
export const IS_IOS = ua.includes('iPhone');
|
||||
|
||||
console.log(IS_IOS);
|
@ -38,7 +38,7 @@ export function ChatResource(props: ChatResourceProps) {
|
||||
const canWrite = isWriter(group, station);
|
||||
|
||||
useEffect(() => {
|
||||
const count = 100;
|
||||
const count = 100 + unreadCount;
|
||||
props.api.graph.getNewest(owner, name, count);
|
||||
}, [station]);
|
||||
|
||||
@ -149,6 +149,7 @@ export function ChatResource(props: ChatResourceProps) {
|
||||
/>
|
||||
{dragging && <SubmitDragger />}
|
||||
<ChatWindow
|
||||
key={station}
|
||||
history={props.history}
|
||||
graph={graph}
|
||||
unreadCount={unreadCount}
|
||||
|
@ -182,6 +182,7 @@ class ChatMessage extends Component<ChatMessageProps> {
|
||||
pt={renderSigil ? 2 : 0}
|
||||
pb={isLastMessage ? 4 : 2}
|
||||
className={containerClass}
|
||||
backgroundColor={highlighted ? 'blue' : 'white'}
|
||||
style={style}
|
||||
>
|
||||
{dayBreak && !isLastRead ? (
|
||||
@ -409,6 +410,7 @@ export const Message = ({
|
||||
color='black'
|
||||
>
|
||||
<RemoteContent
|
||||
key={content.url}
|
||||
url={content.url}
|
||||
imageProps={{
|
||||
style: {
|
||||
|
@ -77,7 +77,6 @@ export default class ChatWindow extends Component<
|
||||
this.handleWindowBlur = this.handleWindowBlur.bind(this);
|
||||
this.handleWindowFocus = this.handleWindowFocus.bind(this);
|
||||
this.stayLockedIfActive = this.stayLockedIfActive.bind(this);
|
||||
this.dismissIfLineVisible = this.dismissIfLineVisible.bind(this);
|
||||
|
||||
this.virtualList = null;
|
||||
this.unreadMarkerRef = React.createRef();
|
||||
@ -111,6 +110,7 @@ export default class ChatWindow extends Component<
|
||||
});
|
||||
return;
|
||||
}
|
||||
console.log(`found unread: ${unreadIndex}`);
|
||||
this.setState({
|
||||
unreadIndex
|
||||
});
|
||||
@ -157,15 +157,15 @@ export default class ChatWindow extends Component<
|
||||
return;
|
||||
}
|
||||
|
||||
//this.virtualList?.scrollToData(unreadIndex);
|
||||
this.virtualList?.scrollToIndex(this.state.unreadIndex);
|
||||
}
|
||||
|
||||
dismissUnread() {
|
||||
const { association } = this.props;
|
||||
if (this.state.fetchPending) return;
|
||||
if (this.props.unreadCount === 0) return;
|
||||
console.log('dismissing unreads');
|
||||
this.props.api.hark.markCountAsRead(association, '/', 'message');
|
||||
this.props.api.hark.markCountAsRead(association, '/', 'mention');
|
||||
}
|
||||
|
||||
async fetchMessages(newer: boolean): Promise<boolean> {
|
||||
@ -184,12 +184,12 @@ export default class ChatWindow extends Component<
|
||||
await api.graph.getYoungerSiblings(
|
||||
ship,
|
||||
name,
|
||||
20,
|
||||
100,
|
||||
`/${index.toString()}`
|
||||
);
|
||||
} else {
|
||||
const [index] = graph.peekSmallest()!;
|
||||
await api.graph.getOlderSiblings(ship, name, 20, `/${index.toString()}`);
|
||||
await api.graph.getOlderSiblings(ship, name, 100, `/${index.toString()}`);
|
||||
this.calculateUnreadIndex();
|
||||
}
|
||||
this.setState({ fetchPending: false });
|
||||
@ -202,22 +202,8 @@ export default class ChatWindow extends Component<
|
||||
this.setState({ idle: true });
|
||||
}
|
||||
|
||||
this.dismissIfLineVisible();
|
||||
}
|
||||
|
||||
dismissIfLineVisible() {
|
||||
if (this.props.unreadCount === 0) return;
|
||||
if (!this.unreadMarkerRef.current || !this.virtualList?.window) return;
|
||||
const parent = this.unreadMarkerRef.current.parentElement?.parentElement;
|
||||
if (!parent) return;
|
||||
const { scrollTop, scrollHeight, offsetHeight } = this.virtualList.window;
|
||||
if (
|
||||
scrollHeight - parent.offsetTop > scrollTop &&
|
||||
scrollHeight - parent.offsetTop < scrollTop + offsetHeight
|
||||
) {
|
||||
this.dismissUnread();
|
||||
}
|
||||
}
|
||||
|
||||
renderer = React.forwardRef(({ index, scrollWindow }, ref) => {
|
||||
const {
|
||||
@ -256,8 +242,7 @@ export default class ChatWindow extends Component<
|
||||
const isLastMessage = index.eq(
|
||||
graph.peekLargest()?.[0] ?? bigInt.zero
|
||||
);
|
||||
const highlighted = bigInt(this.props.scrollTo || -1).eq(index);
|
||||
|
||||
const highlighted = false; // this.state.unreadIndex.eq(index);
|
||||
const keys = graph.keys().reverse();
|
||||
const graphIdx = keys.findIndex((idx) => idx.eq(index));
|
||||
const prevIdx = keys[graphIdx + 1];
|
||||
@ -330,16 +315,18 @@ export default class ChatWindow extends Component<
|
||||
ref={(list) => {
|
||||
this.virtualList = list;
|
||||
}}
|
||||
offset={unreadCount}
|
||||
origin='bottom'
|
||||
style={{ height: '100%' }}
|
||||
onStartReached={() => {
|
||||
this.setState({ idle: false });
|
||||
this.dismissUnread();
|
||||
//this.dismissUnread();
|
||||
}}
|
||||
onScroll={this.onScroll.bind(this)}
|
||||
data={graph}
|
||||
size={graph.size}
|
||||
id={association.resource}
|
||||
averageHeight={22}
|
||||
renderer={this.renderer}
|
||||
loadRows={this.fetchMessages.bind(this)}
|
||||
/>
|
||||
|
@ -1,11 +1,18 @@
|
||||
import React from 'react';
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import moment from 'moment';
|
||||
import { Box, Text } from '@tlon/indigo-react';
|
||||
import VisibilitySensor from 'react-visibility-sensor';
|
||||
|
||||
import Timestamp from '~/views/components/Timestamp';
|
||||
|
||||
export const UnreadNotice = (props) => {
|
||||
const { unreadCount, unreadMsg, dismissUnread, onClick } = props;
|
||||
const [visible, setVisible] = useState(false);
|
||||
useEffect(() => {
|
||||
if(visible && unreadCount) {
|
||||
dismissUnread();
|
||||
}
|
||||
}, [visible])
|
||||
|
||||
if (!unreadMsg || (unreadCount === 0)) {
|
||||
return null;
|
||||
@ -21,6 +28,7 @@ export const UnreadNotice = (props) => {
|
||||
}
|
||||
|
||||
return (
|
||||
<VisibilitySensor onChange={setVisible}>
|
||||
<Box style={{ left: '0px', top: '0px' }}
|
||||
p='4'
|
||||
width='100%'
|
||||
@ -53,5 +61,6 @@ export const UnreadNotice = (props) => {
|
||||
</Text>
|
||||
</Box>
|
||||
</Box>
|
||||
</VisibilitySensor>
|
||||
);
|
||||
}
|
||||
|
@ -3,9 +3,10 @@ import _ from 'lodash';
|
||||
import normalizeWheel from 'normalize-wheel';
|
||||
import bigInt, { BigInteger } from 'big-integer';
|
||||
|
||||
import { Box } from '@tlon/indigo-react';
|
||||
import { Box, LoadingSpinner, Row, Center } from '@tlon/indigo-react';
|
||||
import BigIntOrderedMap from '@urbit/api/lib/BigIntOrderedMap';
|
||||
import {VirtualContext} from '~/logic/lib/virtualContext';
|
||||
import { IS_IOS } from '~/logic/lib/platform';
|
||||
|
||||
interface RendererProps {
|
||||
index: BigInteger;
|
||||
@ -23,20 +24,28 @@ interface VirtualScrollerProps<T> {
|
||||
onEndReached?(): void;
|
||||
size: number;
|
||||
totalSize: number;
|
||||
|
||||
averageHeight: number;
|
||||
offset: number;
|
||||
onCalculateVisibleItems?(visibleItems: BigIntOrderedMap<T>): void;
|
||||
onScroll?({ scrollTop, scrollHeight, windowHeight }): void;
|
||||
style?: any;
|
||||
}
|
||||
|
||||
interface VirtualScrollerState<T> {
|
||||
startgap: number | undefined;
|
||||
visibleItems: BigIntOrderedMap<T>;
|
||||
endgap: number | undefined;
|
||||
totalHeight: number;
|
||||
averageHeight: number;
|
||||
}
|
||||
|
||||
type LogLevel = 'scroll' | 'network' | 'bail' | 'reflow';
|
||||
let logLevel = ['bail', 'scroll', 'reflow'] as LogLevel[];
|
||||
|
||||
const log = (level: LogLevel, message: string) => {
|
||||
if(logLevel.includes(level)) {
|
||||
console.log(`[${level}]: ${message}`);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
// nb: in this file, an index refers to a BigInteger and an offset refers to a
|
||||
// number used to index a listified BigIntOrderedMap
|
||||
|
||||
@ -85,37 +94,43 @@ export default class VirtualScroller<T> extends Component<VirtualScrollerProps<T
|
||||
constructor(props: VirtualScrollerProps<T>) {
|
||||
super(props);
|
||||
this.state = {
|
||||
startgap: props.origin === 'top' ? 0 : undefined,
|
||||
visibleItems: new BigIntOrderedMap(),
|
||||
endgap: props.origin === 'bottom' ? 0 : undefined,
|
||||
totalHeight: 0,
|
||||
averageHeight: 130,
|
||||
};
|
||||
|
||||
this.updateVisible = this.updateVisible.bind(this);
|
||||
this.updateVisible = IS_IOS
|
||||
? _.debounce(this.updateVisible.bind(this), 100)
|
||||
: this.updateVisible.bind(this);
|
||||
|
||||
this.invertedKeyHandler = this.invertedKeyHandler.bind(this);
|
||||
this.onScroll = this.onScroll.bind(this)
|
||||
this.onScroll = IS_IOS ? _.debounce(this.onScroll.bind(this), 100) : this.onScroll.bind(this);
|
||||
this.scrollKeyMap = this.scrollKeyMap.bind(this);
|
||||
this.setWindow = this.setWindow.bind(this);
|
||||
window.restore = () => this.restore();
|
||||
window.save = () => this.save();
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
this.updateVisible(0);
|
||||
if(true) {
|
||||
this.updateVisible(0);
|
||||
this.resetScroll();
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
componentDidUpdate(prevProps: VirtualScrollerProps<T>, _prevState: VirtualScrollerState<T>) {
|
||||
const { id, size, data } = this.props;
|
||||
const { id, size, data, offset } = this.props;
|
||||
const { visibleItems } = this.state;
|
||||
if(id !== prevProps.id) {
|
||||
console.log('changed id');
|
||||
this.resetScroll();
|
||||
this.updateVisible(0);
|
||||
//this.resetScroll();
|
||||
//this.updateVisible(offset ?? 0);
|
||||
} else if(size !== prevProps.size) {
|
||||
if(this.scrollLocked) {
|
||||
console.log('locked');
|
||||
this.updateVisible(0)
|
||||
this.updateVisible(0);
|
||||
if(IS_IOS) {
|
||||
(this.updateVisible as any).flush();
|
||||
|
||||
}
|
||||
this.resetScroll();
|
||||
}
|
||||
}
|
||||
@ -126,7 +141,10 @@ export default class VirtualScroller<T> extends Component<VirtualScrollerProps<T
|
||||
}
|
||||
|
||||
startOffset() {
|
||||
const startIndex = this.state.visibleItems.peekLargest()?.[0]!;
|
||||
const startIndex = this.state.visibleItems.peekLargest()?.[0];
|
||||
if(!startIndex) {
|
||||
return 0;
|
||||
}
|
||||
const offset = [...this.props.data].findIndex(([i]) => i.eq(startIndex))
|
||||
if(offset === -1) {
|
||||
throw new Error("a");
|
||||
@ -167,8 +185,8 @@ export default class VirtualScroller<T> extends Component<VirtualScrollerProps<T
|
||||
|
||||
scrollKeyMap(): Map<string, number> {
|
||||
return new Map([
|
||||
['ArrowUp', this.state.averageHeight],
|
||||
['ArrowDown', this.state.averageHeight * -1],
|
||||
['ArrowUp', this.props.averageHeight],
|
||||
['ArrowDown', this.props.averageHeight * -1],
|
||||
['PageUp', this.window!.offsetHeight],
|
||||
['PageDown', this.window!.offsetHeight * -1],
|
||||
['Home', this.window!.scrollHeight],
|
||||
@ -204,9 +222,10 @@ export default class VirtualScroller<T> extends Component<VirtualScrollerProps<T
|
||||
window.removeEventListener('keydown', this.invertedKeyHandler);
|
||||
}
|
||||
}
|
||||
const { averageHeight } = this.props;
|
||||
|
||||
this.window = element;
|
||||
this.pageSize = Math.floor(element.offsetHeight / 22);
|
||||
this.pageSize = Math.floor(element.offsetHeight / Math.floor(averageHeight / 5));
|
||||
this.pageDelta = Math.floor(this.pageSize / 3);
|
||||
if (this.props.origin === 'bottom') {
|
||||
element.addEventListener('wheel', (event) => {
|
||||
@ -248,6 +267,7 @@ export default class VirtualScroller<T> extends Component<VirtualScrollerProps<T
|
||||
return;
|
||||
}
|
||||
if(this.saveDepth > 0) {
|
||||
log('bail', 'deep scroll queue');
|
||||
return;
|
||||
}
|
||||
const { onStartReached, onEndReached } = this.props;
|
||||
@ -256,9 +276,8 @@ export default class VirtualScroller<T> extends Component<VirtualScrollerProps<T
|
||||
|
||||
const startOffset = this.startOffset();
|
||||
if (scrollTop < 30) {
|
||||
console.log(scrollTop);
|
||||
console.log('start');
|
||||
if (onStartReached) {
|
||||
log('scroll', `Entered start zone ${scrollTop}`);
|
||||
if (startOffset === 0 && onStartReached) {
|
||||
onStartReached();
|
||||
}
|
||||
const newOffset = Math.max(0, startOffset - this.pageDelta);
|
||||
@ -274,12 +293,15 @@ export default class VirtualScroller<T> extends Component<VirtualScrollerProps<T
|
||||
}
|
||||
}
|
||||
else if (scrollTop + windowHeight >= scrollHeight - 20) {
|
||||
if (onEndReached) {
|
||||
this.scrollLocked = false;
|
||||
log('scroll', `Entered end zone ${scrollTop}`);
|
||||
|
||||
const newOffset = Math.min(startOffset + this.pageDelta, this.props.data.size - this.pageSize);
|
||||
if (onEndReached && startOffset === 0) {
|
||||
onEndReached();
|
||||
}
|
||||
|
||||
const newOffset = Math.min(startOffset + this.pageDelta, this.props.data.size - this.pageSize);
|
||||
if((newOffset + 2 * this.pageSize > this.props.data.size)) {
|
||||
if((newOffset + (3 * this.pageSize) > this.props.data.size)) {
|
||||
setTimeout(() => this.loadRows(false));
|
||||
}
|
||||
|
||||
@ -296,6 +318,7 @@ export default class VirtualScroller<T> extends Component<VirtualScrollerProps<T
|
||||
return;
|
||||
}
|
||||
if(this.saveDepth !== 1) {
|
||||
console.log('bail', 'Deep restore');
|
||||
return;
|
||||
}
|
||||
console.log(this.childRefs.size);
|
||||
@ -304,9 +327,8 @@ export default class VirtualScroller<T> extends Component<VirtualScrollerProps<T
|
||||
//ref.scrollIntoView();
|
||||
const newScrollTop = this.window.scrollHeight - ref.offsetTop - this.savedDistance;
|
||||
|
||||
this.window.style['-webkit-overflow-scrolling'] = 'auto';
|
||||
this.window.scrollTop = newScrollTop;
|
||||
this.window.style['-webkit-overflow-scrolling'] = 'touch';
|
||||
//this.window.scrollTop = newScrollTop;
|
||||
this.window.scrollTo(0, newScrollTop);
|
||||
requestAnimationFrame(() => {
|
||||
this.savedIndex = null;
|
||||
this.savedDistance = 0;
|
||||
@ -314,12 +336,41 @@ export default class VirtualScroller<T> extends Component<VirtualScrollerProps<T
|
||||
});
|
||||
}
|
||||
|
||||
scrollToIndex = (index: BigInteger) => {
|
||||
let ref = this.childRefs.get(index);
|
||||
if(!ref) {
|
||||
const offset = [...this.props.data].findIndex(([idx]) => idx.eq(index));
|
||||
if(offset === -1) {
|
||||
return;
|
||||
}
|
||||
this.updateVisible(offset - this.pageDelta);
|
||||
if(IS_IOS) {
|
||||
(this.updateVisible as any).flush();
|
||||
}
|
||||
requestAnimationFrame(() => {
|
||||
ref = this.childRefs.get(index);
|
||||
this.savedIndex = null;
|
||||
this.savedDistance = 0;
|
||||
this.saveDepth = 0;
|
||||
|
||||
ref?.scrollIntoView({ block: 'center' });
|
||||
});
|
||||
} else {
|
||||
this.savedIndex = null;
|
||||
this.savedDistance = 0;
|
||||
this.saveDepth = 0;
|
||||
|
||||
ref?.scrollIntoView({ block: 'center' });
|
||||
}
|
||||
};
|
||||
|
||||
save() {
|
||||
if(!this.window || this.savedIndex) {
|
||||
return;
|
||||
}
|
||||
this.saveDepth++;
|
||||
if(this.saveDepth !== 1) {
|
||||
console.log('bail', 'deep save');
|
||||
return;
|
||||
}
|
||||
|
||||
@ -355,7 +406,9 @@ export default class VirtualScroller<T> extends Component<VirtualScrollerProps<T
|
||||
if(element) {
|
||||
this.childRefs.set(index, element);
|
||||
} else {
|
||||
this.childRefs.delete(index);
|
||||
setTimeout(() => {
|
||||
this.childRefs.delete(index);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@ -372,15 +425,21 @@ export default class VirtualScroller<T> extends Component<VirtualScrollerProps<T
|
||||
style,
|
||||
} = this.props;
|
||||
|
||||
const indexesToRender = origin === 'top' ? visibleItems.keys() : visibleItems.keys().reverse();
|
||||
const isTop = origin === 'top';
|
||||
|
||||
const transform = origin === 'top' ? 'scale3d(1, 1, 1)' : 'scale3d(1, -1, 1)';
|
||||
const indexesToRender = isTop ? visibleItems.keys() : visibleItems.keys().reverse();
|
||||
|
||||
const transform = isTop ? 'scale3d(1, 1, 1)' : 'scale3d(1, -1, 1)';
|
||||
|
||||
const atStart = this.props.data.peekLargest()![0].eq(visibleItems.peekLargest()?.[0] || bigInt.zero)
|
||||
const atEnd = false;
|
||||
|
||||
return (
|
||||
<Box overflowY='scroll' ref={this.setWindow} onScroll={this.onScroll} style={{ ...style, ...{ transform } }}>
|
||||
<Box overflowY='scroll' ref={this.setWindow} onScroll={this.onScroll} style={{ ...style, ...{ transform }, "-webkit-overflow-scrolling": "auto" }}>
|
||||
<Box style={{ transform, width: '100%' }}>
|
||||
<Box style={{ height: `${origin === 'top' ? startgap : endgap}px` }}></Box>
|
||||
{(isTop ? !atStart : !atEnd) && (<Center height="5">
|
||||
<LoadingSpinner />
|
||||
</Center>)}
|
||||
<VirtualContext.Provider value={this.shiftLayout}>
|
||||
{indexesToRender.map(index => (
|
||||
<VirtualChild
|
||||
@ -392,7 +451,10 @@ export default class VirtualScroller<T> extends Component<VirtualScrollerProps<T
|
||||
/>
|
||||
))}
|
||||
</VirtualContext.Provider>
|
||||
<Box style={{ height: `${origin === 'top' ? endgap : startgap}px` }}></Box>
|
||||
{(!isTop ? !atStart : !atEnd) &&
|
||||
(<Center height="5">
|
||||
<LoadingSpinner />
|
||||
</Center>)}
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
|
Loading…
Reference in New Issue
Block a user