mirror of
https://github.com/urbit/shrub.git
synced 2024-12-19 16:51:42 +03:00
Merge pull request #4823 from urbit/lf/more-virt-perf
VirtualScroller: performance enhancements
This commit is contained in:
commit
6bf0bf82cd
@ -5,7 +5,7 @@
|
|||||||
/- glob
|
/- glob
|
||||||
/+ default-agent, verb, dbug
|
/+ default-agent, verb, dbug
|
||||||
|%
|
|%
|
||||||
++ hash 0v3.g6u13.haedt.jt4hd.61ek5.6t30q
|
++ hash 0v3.hls3k.gsbae.rm6pr.p6qve.46dh8
|
||||||
+$ state-0 [%0 hash=@uv glob=(unit (each glob:glob tid=@ta))]
|
+$ state-0 [%0 hash=@uv glob=(unit (each glob:glob tid=@ta))]
|
||||||
+$ all-states
|
+$ all-states
|
||||||
$% state-0
|
$% state-0
|
||||||
|
@ -24,6 +24,6 @@
|
|||||||
<div id="portal-root"></div>
|
<div id="portal-root"></div>
|
||||||
<script src="/~landscape/js/channel.js"></script>
|
<script src="/~landscape/js/channel.js"></script>
|
||||||
<script src="/~landscape/js/session.js"></script>
|
<script src="/~landscape/js/session.js"></script>
|
||||||
<script src="/~landscape/js/bundle/index.59e682153138f604d358.js"></script>
|
<script src="/~landscape/js/bundle/index.b253f1f3f824fdeb29d3.js"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
@ -43,8 +43,8 @@ export function useVirtualResizeState(s: boolean) {
|
|||||||
[_setState, save]
|
[_setState, save]
|
||||||
);
|
);
|
||||||
|
|
||||||
useLayoutEffect(() => {
|
useEffect(() => {
|
||||||
restore();
|
requestAnimationFrame(restore);
|
||||||
}, [state]);
|
}, [state]);
|
||||||
|
|
||||||
return [state, setState] as const;
|
return [state, setState] as const;
|
||||||
@ -58,7 +58,7 @@ export function useVirtualResizeProp(prop: Primitive) {
|
|||||||
save();
|
save();
|
||||||
}
|
}
|
||||||
|
|
||||||
useLayoutEffect(() => {
|
useEffect(() => {
|
||||||
requestAnimationFrame(restore);
|
requestAnimationFrame(restore);
|
||||||
}, [prop]);
|
}, [prop]);
|
||||||
|
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
import _ from 'lodash';
|
import _ from 'lodash';
|
||||||
import BigIntOrderedMap from "@urbit/api/lib/BigIntOrderedMap";
|
import BigIntOrderedMap from "@urbit/api/lib/BigIntOrderedMap";
|
||||||
|
import produce from 'immer';
|
||||||
import bigInt, { BigInteger } from "big-integer";
|
import bigInt, { BigInteger } from "big-integer";
|
||||||
import useGraphState, { GraphState } from '../state/graph';
|
import useGraphState, { GraphState } from '../state/graph';
|
||||||
import { reduceState } from '../state/base';
|
import { reduceState } from '../state/base';
|
||||||
@ -51,23 +52,18 @@ const keys = (json, state: GraphState): GraphState => {
|
|||||||
const processNode = (node) => {
|
const processNode = (node) => {
|
||||||
// is empty
|
// is empty
|
||||||
if (!node.children) {
|
if (!node.children) {
|
||||||
node.children = new BigIntOrderedMap();
|
return produce(node, draft => {
|
||||||
return node;
|
draft.children = new BigIntOrderedMap();
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// is graph
|
// is graph
|
||||||
let converted = new BigIntOrderedMap();
|
return produce(node, draft => {
|
||||||
for (let idx in node.children) {
|
draft.children = new BigIntOrderedMap()
|
||||||
let item = node.children[idx];
|
.gas(_.map(draft.children, (item, idx) =>
|
||||||
let index = bigInt(idx);
|
[bigInt(idx), processNode(item)] as [BigInteger, any]
|
||||||
|
));
|
||||||
converted.set(
|
});
|
||||||
index,
|
|
||||||
processNode(item)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
node.children = converted;
|
|
||||||
return node;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
@ -85,17 +81,10 @@ const addGraph = (json, state: GraphState): GraphState => {
|
|||||||
state.graphTimesentMap[resource] = {};
|
state.graphTimesentMap[resource] = {};
|
||||||
|
|
||||||
|
|
||||||
for (let idx in data.graph) {
|
state.graphs[resource] = state.graphs[resource].gas(Object.keys(data.graph).map(idx => {
|
||||||
let item = data.graph[idx];
|
return [bigInt(idx), processNode(data.graph[idx])];
|
||||||
let index = bigInt(idx);
|
}));
|
||||||
|
|
||||||
let node = processNode(item);
|
|
||||||
|
|
||||||
state.graphs[resource].set(
|
|
||||||
index,
|
|
||||||
node
|
|
||||||
);
|
|
||||||
}
|
|
||||||
state.graphKeys.add(resource);
|
state.graphKeys.add(resource);
|
||||||
}
|
}
|
||||||
return state;
|
return state;
|
||||||
@ -116,7 +105,7 @@ const removeGraph = (json, state: GraphState): GraphState => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const mapifyChildren = (children) => {
|
const mapifyChildren = (children) => {
|
||||||
return new BigIntOrderedMap(
|
return new BigIntOrderedMap().gas(
|
||||||
_.map(children, (node, idx) => {
|
_.map(children, (node, idx) => {
|
||||||
idx = idx && idx.startsWith('/') ? idx.slice(1) : idx;
|
idx = idx && idx.startsWith('/') ? idx.slice(1) : idx;
|
||||||
const nd = {...node, children: mapifyChildren(node.children || {}) };
|
const nd = {...node, children: mapifyChildren(node.children || {}) };
|
||||||
@ -128,8 +117,7 @@ const addNodes = (json, state) => {
|
|||||||
const _addNode = (graph, index, node) => {
|
const _addNode = (graph, index, node) => {
|
||||||
// set child of graph
|
// set child of graph
|
||||||
if (index.length === 1) {
|
if (index.length === 1) {
|
||||||
graph.set(index[0], node);
|
return graph.set(index[0], node);
|
||||||
return graph;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// set parent of graph
|
// set parent of graph
|
||||||
@ -138,19 +126,20 @@ const addNodes = (json, state) => {
|
|||||||
console.error('parent node does not exist, cannot add child');
|
console.error('parent node does not exist, cannot add child');
|
||||||
return graph;
|
return graph;
|
||||||
}
|
}
|
||||||
parNode.children = _addNode(parNode.children, index.slice(1), node);
|
return graph.set(index[0], produce(parNode, draft => {
|
||||||
graph.set(index[0], parNode);
|
draft.children = _addNode(draft.children, index.slice(1), node);
|
||||||
return graph;
|
}));
|
||||||
};
|
};
|
||||||
|
|
||||||
const _remove = (graph, index) => {
|
const _remove = (graph, index) => {
|
||||||
if (index.length === 1) {
|
if (index.length === 1) {
|
||||||
graph.delete(index[0]);
|
return graph.delete(index[0]);
|
||||||
} else {
|
} else {
|
||||||
const child = graph.get(index[0]);
|
const child = graph.get(index[0]);
|
||||||
if (child) {
|
if (child) {
|
||||||
child.children = _remove(child.children, index.slice(1));
|
return graph.set(index[0], produce(child, draft => {
|
||||||
graph.set(index[0], child);
|
draft.children = _remove(draft.children, index.slice(1));
|
||||||
|
}));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -166,10 +155,9 @@ const addNodes = (json, state) => {
|
|||||||
return bigInt(ind);
|
return bigInt(ind);
|
||||||
});
|
});
|
||||||
|
|
||||||
graph = _remove(graph, indexArr);
|
|
||||||
delete state.graphTimesentMap[resource][timestamp];
|
delete state.graphTimesentMap[resource][timestamp];
|
||||||
|
return _remove(graph, indexArr);
|
||||||
}
|
}
|
||||||
|
|
||||||
return graph;
|
return graph;
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -208,11 +196,12 @@ const addNodes = (json, state) => {
|
|||||||
return aArr.length - bArr.length;
|
return aArr.length - bArr.length;
|
||||||
});
|
});
|
||||||
|
|
||||||
let graph = state.graphs[resource];
|
|
||||||
|
|
||||||
indices.forEach((index) => {
|
indices.forEach((index) => {
|
||||||
let node = data.nodes[index];
|
let node = data.nodes[index];
|
||||||
graph = _removePending(graph, node.post, resource);
|
const old = state.graphs[resource].size;
|
||||||
|
state.graphs[resource] = _removePending(state.graphs[resource], node.post, resource);
|
||||||
|
const newSize = state.graphs[resource].size;
|
||||||
|
|
||||||
|
|
||||||
if (index.split('/').length === 0) { return; }
|
if (index.split('/').length === 0) { return; }
|
||||||
let indexArr = index.split('/').slice(1).map((ind) => {
|
let indexArr = index.split('/').slice(1).map((ind) => {
|
||||||
@ -225,17 +214,21 @@ const addNodes = (json, state) => {
|
|||||||
state.graphTimesentMap[resource][node.post['time-sent']] = index;
|
state.graphTimesentMap[resource][node.post['time-sent']] = index;
|
||||||
}
|
}
|
||||||
|
|
||||||
node.children = mapifyChildren(node?.children || {});
|
|
||||||
|
|
||||||
graph = _addNode(
|
state.graphs[resource] = _addNode(
|
||||||
graph,
|
state.graphs[resource],
|
||||||
indexArr,
|
indexArr,
|
||||||
node
|
produce(node, draft => {
|
||||||
|
draft.children = mapifyChildren(draft?.children || {});
|
||||||
|
})
|
||||||
);
|
);
|
||||||
|
if(newSize !== old) {
|
||||||
|
console.log(`${resource}, (${old}, ${newSize}, ${state.graphs[resource].size})`);
|
||||||
|
}
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|
||||||
state.graphs[resource] = graph;
|
|
||||||
}
|
}
|
||||||
return state;
|
return state;
|
||||||
};
|
};
|
||||||
@ -243,13 +236,15 @@ const addNodes = (json, state) => {
|
|||||||
const removeNodes = (json, state: GraphState): GraphState => {
|
const removeNodes = (json, state: GraphState): GraphState => {
|
||||||
const _remove = (graph, index) => {
|
const _remove = (graph, index) => {
|
||||||
if (index.length === 1) {
|
if (index.length === 1) {
|
||||||
graph.delete(index[0]);
|
return graph.delete(index[0]);
|
||||||
} else {
|
} else {
|
||||||
const child = graph.get(index[0]);
|
const child = graph.get(index[0]);
|
||||||
if (child) {
|
if (child) {
|
||||||
_remove(child.children, index.slice(1));
|
return graph.set(index[0], produce(draft => {
|
||||||
graph.set(index[0], child);
|
draft.children = _remove(draft.children, index.slice(1))
|
||||||
|
}));
|
||||||
}
|
}
|
||||||
|
return graph;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -264,7 +259,7 @@ const removeNodes = (json, state: GraphState): GraphState => {
|
|||||||
let indexArr = index.split('/').slice(1).map((ind) => {
|
let indexArr = index.split('/').slice(1).map((ind) => {
|
||||||
return bigInt(ind);
|
return bigInt(ind);
|
||||||
});
|
});
|
||||||
_remove(state.graphs[res], indexArr);
|
state.graphs[res] = _remove(state.graphs[res], indexArr);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
return state;
|
return state;
|
||||||
|
@ -329,9 +329,9 @@ function added(json: any, state: HarkState): HarkState {
|
|||||||
);
|
);
|
||||||
if (arrIdx !== -1) {
|
if (arrIdx !== -1) {
|
||||||
timebox[arrIdx] = { index, notification };
|
timebox[arrIdx] = { index, notification };
|
||||||
state.notifications.set(time, timebox);
|
state.notifications = state.notifications.set(time, timebox);
|
||||||
} else {
|
} else {
|
||||||
state.notifications.set(time, [...timebox, { index, notification }]);
|
state.notifications = state.notifications.set(time, [...timebox, { index, notification }]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return state;
|
return state;
|
||||||
@ -350,7 +350,7 @@ const timebox = (json: any, state: HarkState): HarkState => {
|
|||||||
if (data) {
|
if (data) {
|
||||||
const time = makePatDa(data.time);
|
const time = makePatDa(data.time);
|
||||||
if (!data.archive) {
|
if (!data.archive) {
|
||||||
state.notifications.set(time, data.notifications);
|
state.notifications = state.notifications.set(time, data.notifications);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return state;
|
return state;
|
||||||
@ -403,7 +403,7 @@ function setRead(
|
|||||||
return state;
|
return state;
|
||||||
}
|
}
|
||||||
timebox[arrIdx].notification.read = read;
|
timebox[arrIdx].notification.read = read;
|
||||||
state.notifications.set(patDa, timebox);
|
state.notifications = state.notifications.set(patDa, timebox);
|
||||||
return state;
|
return state;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -128,6 +128,8 @@ const useGraphState = createState<GraphState>('Graph', {
|
|||||||
// });
|
// });
|
||||||
// graphReducer(node);
|
// graphReducer(node);
|
||||||
// },
|
// },
|
||||||
}, ['graphs', 'graphKeys', 'looseNodes']);
|
}, ['graphs', 'graphKeys', 'looseNodes', 'graphTimesentMap']);
|
||||||
|
|
||||||
|
window.useGraphState = useGraphState;
|
||||||
|
|
||||||
export default useGraphState;
|
export default useGraphState;
|
||||||
|
@ -28,9 +28,9 @@ type ChatResourceProps = StoreState & {
|
|||||||
association: Association;
|
association: Association;
|
||||||
api: GlobalApi;
|
api: GlobalApi;
|
||||||
baseUrl: string;
|
baseUrl: string;
|
||||||
} & RouteComponentProps;
|
};
|
||||||
|
|
||||||
export function ChatResource(props: ChatResourceProps) {
|
function ChatResource(props: ChatResourceProps) {
|
||||||
const station = props.association.resource;
|
const station = props.association.resource;
|
||||||
const groupPath = props.association.group;
|
const groupPath = props.association.group;
|
||||||
const groups = useGroupState(state => state.groups);
|
const groups = useGroupState(state => state.groups);
|
||||||
@ -40,7 +40,7 @@ export function ChatResource(props: ChatResourceProps) {
|
|||||||
const graphPath = station.slice(7);
|
const graphPath = station.slice(7);
|
||||||
const graph = graphs[graphPath];
|
const graph = graphs[graphPath];
|
||||||
const unreads = useHarkState(state => state.unreads);
|
const unreads = useHarkState(state => state.unreads);
|
||||||
const unreadCount = unreads.graph?.[station]?.['/']?.unreads || 0;
|
const unreadCount = unreads.graph?.[station]?.['/']?.unreads as number || 0;
|
||||||
const graphTimesentMap = useGraphState(state => state.graphTimesentMap);
|
const graphTimesentMap = useGraphState(state => state.graphTimesentMap);
|
||||||
const [,, owner, name] = station.split('/');
|
const [,, owner, name] = station.split('/');
|
||||||
const ourContact = contacts?.[`~${window.ship}`];
|
const ourContact = contacts?.[`~${window.ship}`];
|
||||||
@ -48,7 +48,7 @@ export function ChatResource(props: ChatResourceProps) {
|
|||||||
const canWrite = isWriter(group, station);
|
const canWrite = isWriter(group, station);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const count = 100 + unreadCount;
|
const count = Math.min(400, 100 + unreadCount);
|
||||||
props.api.graph.getNewest(owner, name, count);
|
props.api.graph.getNewest(owner, name, count);
|
||||||
}, [station]);
|
}, [station]);
|
||||||
|
|
||||||
@ -165,10 +165,9 @@ export function ChatResource(props: ChatResourceProps) {
|
|||||||
{dragging && <SubmitDragger />}
|
{dragging && <SubmitDragger />}
|
||||||
<ChatWindow
|
<ChatWindow
|
||||||
key={station}
|
key={station}
|
||||||
history={props.history}
|
|
||||||
graph={graph}
|
graph={graph}
|
||||||
graphSize={graph.size}
|
graphSize={graph.size}
|
||||||
unreadCount={unreadCount}
|
unreadCount={unreadCount as number}
|
||||||
showOurContact={ !showBanner && hasLoadedAllowed }
|
showOurContact={ !showBanner && hasLoadedAllowed }
|
||||||
association={props.association}
|
association={props.association}
|
||||||
pendingSize={Object.keys(graphTimesentMap[graphPath] || {}).length}
|
pendingSize={Object.keys(graphTimesentMap[graphPath] || {}).length}
|
||||||
@ -196,3 +195,8 @@ export function ChatResource(props: ChatResourceProps) {
|
|||||||
</Col>
|
</Col>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ChatResource.whyDidYouRender = true;
|
||||||
|
|
||||||
|
export { ChatResource };
|
||||||
|
|
||||||
|
@ -3,6 +3,7 @@ import bigInt from 'big-integer';
|
|||||||
import React, {
|
import React, {
|
||||||
useState,
|
useState,
|
||||||
useEffect,
|
useEffect,
|
||||||
|
useMemo,
|
||||||
useRef,
|
useRef,
|
||||||
Component,
|
Component,
|
||||||
PureComponent,
|
PureComponent,
|
||||||
@ -40,11 +41,12 @@ import styled from 'styled-components';
|
|||||||
import useLocalState from '~/logic/state/local';
|
import useLocalState from '~/logic/state/local';
|
||||||
import useSettingsState, { selectCalmState } from '~/logic/state/settings';
|
import useSettingsState, { selectCalmState } from '~/logic/state/settings';
|
||||||
import Timestamp from '~/views/components/Timestamp';
|
import Timestamp from '~/views/components/Timestamp';
|
||||||
import useContactState from '~/logic/state/contact';
|
import useContactState, {useContact} from '~/logic/state/contact';
|
||||||
import { useIdlingState } from '~/logic/lib/idling';
|
import { useIdlingState } from '~/logic/lib/idling';
|
||||||
import ProfileOverlay from '~/views/components/ProfileOverlay';
|
import ProfileOverlay from '~/views/components/ProfileOverlay';
|
||||||
import {useCopy} from '~/logic/lib/useCopy';
|
import {useCopy} from '~/logic/lib/useCopy';
|
||||||
import {GraphContentWide} from '~/views/landscape/components/Graph/GraphContentWide';
|
import {GraphContentWide} from '~/views/landscape/components/Graph/GraphContentWide';
|
||||||
|
import {Contact} from '@urbit/api';
|
||||||
|
|
||||||
|
|
||||||
export const DATESTAMP_FORMAT = '[~]YYYY.M.D';
|
export const DATESTAMP_FORMAT = '[~]YYYY.M.D';
|
||||||
@ -80,7 +82,7 @@ export const DayBreak = ({ when, shimTop = false }: DayBreakProps) => (
|
|||||||
);
|
);
|
||||||
|
|
||||||
export const UnreadMarker = React.forwardRef(
|
export const UnreadMarker = React.forwardRef(
|
||||||
({ dayBreak, when, api, association }, ref) => {
|
({ dayBreak, when, api, association }: any, ref) => {
|
||||||
const [visible, setVisible] = useState(false);
|
const [visible, setVisible] = useState(false);
|
||||||
const idling = useIdlingState();
|
const idling = useIdlingState();
|
||||||
const dismiss = useCallback(() => {
|
const dismiss = useCallback(() => {
|
||||||
@ -141,7 +143,7 @@ const MessageActionItem = (props) => {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const MessageActions = ({ api, onReply, association, history, msg, group }) => {
|
const MessageActions = ({ api, onReply, association, msg, group }) => {
|
||||||
const isAdmin = () => group.tags.role.admin.has(window.ship);
|
const isAdmin = () => group.tags.role.admin.has(window.ship);
|
||||||
const isOwn = () => msg.author === window.ship;
|
const isOwn = () => msg.author === window.ship;
|
||||||
const { doCopy, copyDisplay } = useCopy(`web+urbitgraph://group${association.group.slice(5)}/graph${association.resource.slice(5)}${msg.index}`, 'Copy Message Link');
|
const { doCopy, copyDisplay } = useCopy(`web+urbitgraph://group${association.group.slice(5)}/graph${association.resource.slice(5)}${msg.index}`, 'Copy Message Link');
|
||||||
@ -241,49 +243,43 @@ interface ChatMessageProps {
|
|||||||
className?: string;
|
className?: string;
|
||||||
isPending: boolean;
|
isPending: boolean;
|
||||||
style?: unknown;
|
style?: unknown;
|
||||||
scrollWindow: HTMLDivElement;
|
|
||||||
isLastMessage?: boolean;
|
isLastMessage?: boolean;
|
||||||
unreadMarkerRef: React.RefObject<HTMLDivElement>;
|
unreadMarkerRef: React.RefObject<HTMLDivElement>;
|
||||||
history: unknown;
|
|
||||||
api: GlobalApi;
|
api: GlobalApi;
|
||||||
highlighted?: boolean;
|
highlighted?: boolean;
|
||||||
renderSigil?: boolean;
|
renderSigil?: boolean;
|
||||||
hideHover?: boolean;
|
hideHover?: boolean;
|
||||||
innerRef: (el: HTMLDivElement | null) => void;
|
innerRef: (el: HTMLDivElement | null) => void;
|
||||||
onReply?: (msg: Post) => void;
|
onReply?: (msg: Post) => void;
|
||||||
|
showOurContact: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
class ChatMessage extends Component<ChatMessageProps> {
|
function ChatMessage(props: ChatMessageProps) {
|
||||||
private divRef: React.RefObject<HTMLDivElement>;
|
let { highlighted } = this.props;
|
||||||
|
const {
|
||||||
|
msg,
|
||||||
|
previousMsg,
|
||||||
|
nextMsg,
|
||||||
|
isLastRead,
|
||||||
|
group,
|
||||||
|
association,
|
||||||
|
className = '',
|
||||||
|
isPending,
|
||||||
|
style,
|
||||||
|
isLastMessage,
|
||||||
|
unreadMarkerRef,
|
||||||
|
api,
|
||||||
|
showOurContact,
|
||||||
|
fontSize,
|
||||||
|
hideHover
|
||||||
|
} = props;
|
||||||
|
|
||||||
constructor(props) {
|
let onReply = props?.onReply ?? (() => {});
|
||||||
super(props);
|
const transcluded = props?.transcluded ?? 0;
|
||||||
this.divRef = React.createRef();
|
const renderSigil = props.renderSigil ?? (Boolean(nextMsg && msg.author !== nextMsg.author) ||
|
||||||
}
|
!nextMsg ||
|
||||||
|
msg.number === 1
|
||||||
componentDidMount() {}
|
);
|
||||||
|
|
||||||
render() {
|
|
||||||
let { highlighted } = this.props;
|
|
||||||
const {
|
|
||||||
msg,
|
|
||||||
previousMsg,
|
|
||||||
nextMsg,
|
|
||||||
isLastRead,
|
|
||||||
group,
|
|
||||||
association,
|
|
||||||
className = '',
|
|
||||||
isPending,
|
|
||||||
style,
|
|
||||||
scrollWindow,
|
|
||||||
isLastMessage,
|
|
||||||
unreadMarkerRef,
|
|
||||||
history,
|
|
||||||
api,
|
|
||||||
showOurContact,
|
|
||||||
fontSize,
|
|
||||||
hideHover
|
|
||||||
} = this.props;
|
|
||||||
|
|
||||||
const ourMention = msg?.contents?.some((e) => {
|
const ourMention = msg?.contents?.some((e) => {
|
||||||
return e?.mention && e?.mention === window.ship;
|
return e?.mention && e?.mention === window.ship;
|
||||||
@ -295,126 +291,120 @@ class ChatMessage extends Component<ChatMessageProps> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let onReply = this.props?.onReply ?? (() => {});
|
const date = useMemo(() => daToUnix(bigInt(msg.index.split('/')[1])), [msg.index]);
|
||||||
const transcluded = this.props?.transcluded ?? 0;
|
const nextDate = useMemo(() => nextMsg ? (
|
||||||
let { renderSigil } = this.props;
|
daToUnix(bigInt(nextMsg.index.split('/')[1]))
|
||||||
|
) : null,
|
||||||
|
[nextMsg]
|
||||||
|
);
|
||||||
|
|
||||||
if (renderSigil === undefined) {
|
const dayBreak = useMemo(() =>
|
||||||
renderSigil = Boolean(
|
nextDate &&
|
||||||
(nextMsg && msg.author !== nextMsg.author) ||
|
new Date(date).getDate() !==
|
||||||
!nextMsg ||
|
new Date(nextDate).getDate()
|
||||||
msg.number === 1
|
, [nextDate, date])
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const date = daToUnix(bigInt(msg.index.split('/')[1]));
|
const containerClass = `${isPending ? 'o-40' : ''} ${className}`;
|
||||||
const nextDate = nextMsg ? (
|
|
||||||
daToUnix(bigInt(nextMsg.index.split('/')[1]))
|
|
||||||
) : null;
|
|
||||||
|
|
||||||
const dayBreak =
|
const timestamp = useMemo(() => moment
|
||||||
nextMsg &&
|
.unix(date / 1000)
|
||||||
new Date(date).getDate() !==
|
.format(renderSigil ? 'h:mm A' : 'h:mm'),
|
||||||
new Date(nextDate).getDate();
|
[date, renderSigil]
|
||||||
|
);
|
||||||
|
|
||||||
const containerClass = `${isPending ? 'o-40' : ''} ${className}`;
|
const messageProps = {
|
||||||
|
msg,
|
||||||
|
timestamp,
|
||||||
|
association,
|
||||||
|
group,
|
||||||
|
isPending,
|
||||||
|
showOurContact,
|
||||||
|
api,
|
||||||
|
highlighted,
|
||||||
|
fontSize,
|
||||||
|
hideHover,
|
||||||
|
transcluded,
|
||||||
|
onReply
|
||||||
|
};
|
||||||
|
|
||||||
const timestamp = moment
|
const message = useMemo(() => (
|
||||||
.unix(date / 1000)
|
<Message
|
||||||
.format(renderSigil ? 'h:mm A' : 'h:mm');
|
msg={msg}
|
||||||
|
timestamp={timestamp}
|
||||||
|
timestampHover={!renderSigil}
|
||||||
|
api={api}
|
||||||
|
transcluded={transcluded}
|
||||||
|
showOurContact={showOurContact}
|
||||||
|
/>
|
||||||
|
), [renderSigil, msg, timestamp, api, transcluded, showOurContact]);
|
||||||
|
|
||||||
const messageProps = {
|
const unreadContainerStyle = {
|
||||||
msg,
|
height: isLastRead ? '2rem' : '0'
|
||||||
timestamp,
|
};
|
||||||
association,
|
|
||||||
group,
|
|
||||||
style,
|
|
||||||
containerClass,
|
|
||||||
isPending,
|
|
||||||
showOurContact,
|
|
||||||
history,
|
|
||||||
api,
|
|
||||||
scrollWindow,
|
|
||||||
highlighted,
|
|
||||||
fontSize,
|
|
||||||
hideHover,
|
|
||||||
transcluded,
|
|
||||||
onReply
|
|
||||||
};
|
|
||||||
|
|
||||||
const unreadContainerStyle = {
|
return (
|
||||||
height: isLastRead ? '2rem' : '0'
|
<Box
|
||||||
};
|
ref={props.innerRef}
|
||||||
|
pt={renderSigil ? 2 : 0}
|
||||||
return (
|
width="100%"
|
||||||
<Box
|
pb={isLastMessage ? '20px' : 0}
|
||||||
ref={this.props.innerRef}
|
className={containerClass}
|
||||||
pt={renderSigil ? 2 : 0}
|
style={style}
|
||||||
width="100%"
|
>
|
||||||
pb={isLastMessage ? '20px' : 0}
|
{dayBreak && !isLastRead ? (
|
||||||
className={containerClass}
|
<DayBreak when={date} shimTop={renderSigil} />
|
||||||
style={style}
|
) : null}
|
||||||
>
|
{renderSigil ? (
|
||||||
{dayBreak && !isLastRead ? (
|
<MessageWrapper {...messageProps}>
|
||||||
<DayBreak when={date} shimTop={renderSigil} />
|
<MessageAuthor pb={1} {...messageProps} />
|
||||||
|
{message}
|
||||||
|
</MessageWrapper>
|
||||||
|
) : (
|
||||||
|
<MessageWrapper {...messageProps}>
|
||||||
|
{message}
|
||||||
|
</MessageWrapper>
|
||||||
|
)}
|
||||||
|
<Box style={unreadContainerStyle}>
|
||||||
|
{isLastRead ? (
|
||||||
|
<UnreadMarker
|
||||||
|
association={association}
|
||||||
|
api={api}
|
||||||
|
dayBreak={dayBreak}
|
||||||
|
when={date}
|
||||||
|
ref={unreadMarkerRef}
|
||||||
|
/>
|
||||||
) : null}
|
) : null}
|
||||||
{renderSigil ? (
|
|
||||||
<MessageWrapper {...messageProps}>
|
|
||||||
<MessageAuthor pb={1} {...messageProps} />
|
|
||||||
<Message pl={'44px'} pr={4} {...messageProps} />
|
|
||||||
</MessageWrapper>
|
|
||||||
) : (
|
|
||||||
<MessageWrapper {...messageProps}>
|
|
||||||
<Message pl={'44px'} pr={4} timestampHover {...messageProps} />
|
|
||||||
</MessageWrapper>
|
|
||||||
)}
|
|
||||||
<Box style={unreadContainerStyle}>
|
|
||||||
{isLastRead ? (
|
|
||||||
<UnreadMarker
|
|
||||||
association={association}
|
|
||||||
api={api}
|
|
||||||
dayBreak={dayBreak}
|
|
||||||
when={date}
|
|
||||||
ref={unreadMarkerRef}
|
|
||||||
/>
|
|
||||||
) : null}
|
|
||||||
</Box>
|
|
||||||
</Box>
|
</Box>
|
||||||
);
|
</Box>
|
||||||
}
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default React.forwardRef((props, ref) => (
|
export default React.forwardRef((props: Omit<ChatMessageProps, 'innerRef'>, ref: any) => (
|
||||||
<ChatMessage {...props} innerRef={ref} />
|
<ChatMessage {...props} innerRef={ref} />
|
||||||
));
|
));
|
||||||
|
|
||||||
export const MessageAuthor = ({
|
export const MessageAuthor = ({
|
||||||
timestamp,
|
timestamp,
|
||||||
msg,
|
msg,
|
||||||
group,
|
|
||||||
api,
|
api,
|
||||||
history,
|
|
||||||
scrollWindow,
|
|
||||||
showOurContact,
|
showOurContact,
|
||||||
...rest
|
|
||||||
}) => {
|
}) => {
|
||||||
const osDark = useLocalState((state) => state.dark);
|
const osDark = useLocalState((state) => state.dark);
|
||||||
|
|
||||||
const theme = useSettingsState((s) => s.display.theme);
|
const theme = useSettingsState((s) => s.display.theme);
|
||||||
const dark = theme === 'dark' || (theme === 'auto' && osDark);
|
const dark = theme === 'dark' || (theme === 'auto' && osDark);
|
||||||
const contacts = useContactState((state) => state.contacts);
|
let contact: Contact | null = useContact(`~${msg.author}`);
|
||||||
|
|
||||||
const date = daToUnix(bigInt(msg.index.split('/')[1]));
|
const date = daToUnix(bigInt(msg.index.split('/')[1]));
|
||||||
|
|
||||||
const datestamp = moment
|
const datestamp = moment
|
||||||
.unix(date / 1000)
|
.unix(date / 1000)
|
||||||
.format(DATESTAMP_FORMAT);
|
.format(DATESTAMP_FORMAT);
|
||||||
const contact =
|
contact =
|
||||||
((msg.author === window.ship && showOurContact) ||
|
((msg.author === window.ship && showOurContact) ||
|
||||||
msg.author !== window.ship) &&
|
msg.author !== window.ship)
|
||||||
`~${msg.author}` in contacts
|
? contact
|
||||||
? contacts[`~${msg.author}`]
|
: null;
|
||||||
: undefined;
|
|
||||||
|
|
||||||
const showNickname = useShowNickname(contact);
|
const showNickname = useShowNickname(contact);
|
||||||
const { hideAvatars } = useSettingsState(selectCalmState);
|
const { hideAvatars } = useSettingsState(selectCalmState);
|
||||||
@ -467,7 +457,7 @@ export const MessageAuthor = ({
|
|||||||
</Box>
|
</Box>
|
||||||
);
|
);
|
||||||
return (
|
return (
|
||||||
<Box display='flex' alignItems='flex-start' {...rest}>
|
<Box display='flex' alignItems='flex-start'>
|
||||||
<Box
|
<Box
|
||||||
height={24}
|
height={24}
|
||||||
pr={2}
|
pr={2}
|
||||||
@ -519,20 +509,20 @@ export const MessageAuthor = ({
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const Message = ({
|
type MessageProps = { timestamp: string; timestampHover: boolean; }
|
||||||
|
& Pick<ChatMessageProps, "msg" | "api" | "transcluded" | "showOurContact">
|
||||||
|
|
||||||
|
export const Message = React.memo(({
|
||||||
timestamp,
|
timestamp,
|
||||||
msg,
|
msg,
|
||||||
group,
|
|
||||||
api,
|
api,
|
||||||
scrollWindow,
|
|
||||||
timestampHover,
|
timestampHover,
|
||||||
transcluded,
|
transcluded,
|
||||||
showOurContact,
|
showOurContact
|
||||||
...rest
|
}: MessageProps) => {
|
||||||
}) => {
|
|
||||||
const { hovering, bind } = useHovering();
|
const { hovering, bind } = useHovering();
|
||||||
return (
|
return (
|
||||||
<Box width="100%" position='relative' {...rest}>
|
<Box pl="44px" width="100%" position='relative'>
|
||||||
{timestampHover ? (
|
{timestampHover ? (
|
||||||
<Text
|
<Text
|
||||||
display={hovering ? 'block' : 'none'}
|
display={hovering ? 'block' : 'none'}
|
||||||
@ -559,7 +549,9 @@ export const Message = ({
|
|||||||
/>
|
/>
|
||||||
</Box>
|
</Box>
|
||||||
);
|
);
|
||||||
};
|
});
|
||||||
|
|
||||||
|
Message.displayName = 'Message';
|
||||||
|
|
||||||
export const MessagePlaceholder = ({
|
export const MessagePlaceholder = ({
|
||||||
height,
|
height,
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import React, { Component } from 'react';
|
import React, { useEffect, Component, useRef, useState, useCallback } from 'react';
|
||||||
import { RouteComponentProps } from 'react-router-dom';
|
import { RouteComponentProps } from 'react-router-dom';
|
||||||
import _ from 'lodash';
|
import _ from 'lodash';
|
||||||
import bigInt, { BigInteger } from 'big-integer';
|
import bigInt, { BigInteger } from 'big-integer';
|
||||||
@ -30,10 +30,13 @@ const DEFAULT_BACKLOG_SIZE = 100;
|
|||||||
const IDLE_THRESHOLD = 64;
|
const IDLE_THRESHOLD = 64;
|
||||||
const MAX_BACKLOG_SIZE = 1000;
|
const MAX_BACKLOG_SIZE = 1000;
|
||||||
|
|
||||||
type ChatWindowProps = RouteComponentProps<{
|
const getCurrGraphSize = (ship: string, name: string) => {
|
||||||
ship: Patp;
|
const { graphs } = useGraphState.getState();
|
||||||
station: string;
|
const graph = graphs[`${ship}/${name}`];
|
||||||
}> & {
|
return graph.size;
|
||||||
|
};
|
||||||
|
|
||||||
|
type ChatWindowProps = {
|
||||||
unreadCount: number;
|
unreadCount: number;
|
||||||
graph: Graph;
|
graph: Graph;
|
||||||
graphSize: number;
|
graphSize: number;
|
||||||
@ -44,6 +47,8 @@ type ChatWindowProps = RouteComponentProps<{
|
|||||||
api: GlobalApi;
|
api: GlobalApi;
|
||||||
scrollTo?: BigInteger;
|
scrollTo?: BigInteger;
|
||||||
onReply: (msg: Post) => void;
|
onReply: (msg: Post) => void;
|
||||||
|
pendingSize?: number;
|
||||||
|
showOurContact: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
interface ChatWindowState {
|
interface ChatWindowState {
|
||||||
@ -55,6 +60,7 @@ interface ChatWindowState {
|
|||||||
|
|
||||||
const virtScrollerStyle = { height: '100%' };
|
const virtScrollerStyle = { height: '100%' };
|
||||||
|
|
||||||
|
|
||||||
class ChatWindow extends Component<
|
class ChatWindow extends Component<
|
||||||
ChatWindowProps,
|
ChatWindowProps,
|
||||||
ChatWindowState
|
ChatWindowState
|
||||||
@ -81,6 +87,7 @@ class ChatWindow extends Component<
|
|||||||
this.handleWindowBlur = this.handleWindowBlur.bind(this);
|
this.handleWindowBlur = this.handleWindowBlur.bind(this);
|
||||||
this.handleWindowFocus = this.handleWindowFocus.bind(this);
|
this.handleWindowFocus = this.handleWindowFocus.bind(this);
|
||||||
this.stayLockedIfActive = this.stayLockedIfActive.bind(this);
|
this.stayLockedIfActive = this.stayLockedIfActive.bind(this);
|
||||||
|
this.fetchMessages = this.fetchMessages.bind(this);
|
||||||
|
|
||||||
this.virtualList = null;
|
this.virtualList = null;
|
||||||
this.unreadMarkerRef = React.createRef();
|
this.unreadMarkerRef = React.createRef();
|
||||||
@ -109,9 +116,11 @@ class ChatWindow extends Component<
|
|||||||
}
|
}
|
||||||
const unreadIndex = graph.keys()[unreadCount];
|
const unreadIndex = graph.keys()[unreadCount];
|
||||||
if (!unreadIndex || unreadCount === 0) {
|
if (!unreadIndex || unreadCount === 0) {
|
||||||
this.setState({
|
if(state.unreadIndex.neq(bigInt.zero)) {
|
||||||
unreadIndex: bigInt.zero
|
this.setState({
|
||||||
});
|
unreadIndex: bigInt.zero
|
||||||
|
});
|
||||||
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
this.setState({
|
this.setState({
|
||||||
@ -122,8 +131,8 @@ class ChatWindow extends Component<
|
|||||||
dismissedInitialUnread() {
|
dismissedInitialUnread() {
|
||||||
const { unreadCount, graph } = this.props;
|
const { unreadCount, graph } = this.props;
|
||||||
|
|
||||||
return this.state.unreadIndex.neq(bigInt.zero) &&
|
return this.state.unreadIndex.eq(bigInt.zero) ? unreadCount > graph.size :
|
||||||
this.state.unreadIndex.neq(graph.keys()?.[unreadCount]?.[0] ?? bigInt.zero);
|
this.state.unreadIndex.neq(graph.keys()?.[unreadCount]?.[0] ?? bigInt.zero);
|
||||||
}
|
}
|
||||||
|
|
||||||
handleWindowBlur() {
|
handleWindowBlur() {
|
||||||
@ -138,7 +147,7 @@ class ChatWindow extends Component<
|
|||||||
}
|
}
|
||||||
|
|
||||||
componentDidUpdate(prevProps: ChatWindowProps, prevState) {
|
componentDidUpdate(prevProps: ChatWindowProps, prevState) {
|
||||||
const { history, graph, unreadCount, graphSize, station } = this.props;
|
const { graph, unreadCount, graphSize, station } = this.props;
|
||||||
if(unreadCount === 0 && prevProps.unreadCount !== unreadCount) {
|
if(unreadCount === 0 && prevProps.unreadCount !== unreadCount) {
|
||||||
this.unreadSet = true;
|
this.unreadSet = true;
|
||||||
}
|
}
|
||||||
@ -195,31 +204,35 @@ class ChatWindow extends Component<
|
|||||||
this.props.api.hark.markCountAsRead(association, '/', 'message');
|
this.props.api.hark.markCountAsRead(association, '/', 'message');
|
||||||
}
|
}
|
||||||
|
|
||||||
setActive = () => {
|
|
||||||
if(this.state.idle) {
|
|
||||||
this.setState({ idle: false });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fetchMessages = async (newer: boolean): Promise<boolean> => {
|
async fetchMessages(newer: boolean): Promise<boolean> {
|
||||||
const { api, station, graph } = this.props;
|
const { api, station, graph } = this.props;
|
||||||
const pageSize = 100;
|
const pageSize = 100;
|
||||||
|
|
||||||
const [, , ship, name] = station.split('/');
|
const [, , ship, name] = station.split('/');
|
||||||
const expectedSize = graph.size + pageSize;
|
const expectedSize = graph.size + pageSize;
|
||||||
if (newer) {
|
if (newer) {
|
||||||
const [index] = graph.peekLargest()!;
|
const index = graph.peekLargest()?.[0];
|
||||||
|
if(!index) {
|
||||||
|
console.log(`no index for: ${graph}`);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
await api.graph.getYoungerSiblings(
|
await api.graph.getYoungerSiblings(
|
||||||
ship,
|
ship,
|
||||||
name,
|
name,
|
||||||
pageSize,
|
pageSize,
|
||||||
`/${index.toString()}`
|
`/${index.toString()}`
|
||||||
);
|
);
|
||||||
return expectedSize !== graph.size;
|
return expectedSize !== getCurrGraphSize(ship.slice(1), name);
|
||||||
} else {
|
} else {
|
||||||
const [index] = graph.peekSmallest()!;
|
console.log('x');
|
||||||
|
const index = graph.peekSmallest()?.[0];
|
||||||
|
if(!index) {
|
||||||
|
console.log(`no index for: ${graph}`);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
await api.graph.getOlderSiblings(ship, name, pageSize, `/${index.toString()}`);
|
await api.graph.getOlderSiblings(ship, name, pageSize, `/${index.toString()}`);
|
||||||
const done = expectedSize !== graph.size;
|
const done = expectedSize !== getCurrGraphSize(ship.slice(1), name);
|
||||||
if(done) {
|
if(done) {
|
||||||
this.calculateUnreadIndex();
|
this.calculateUnreadIndex();
|
||||||
}
|
}
|
||||||
@ -238,12 +251,9 @@ class ChatWindow extends Component<
|
|||||||
const {
|
const {
|
||||||
api,
|
api,
|
||||||
association,
|
association,
|
||||||
group,
|
|
||||||
showOurContact,
|
showOurContact,
|
||||||
graph,
|
graph,
|
||||||
history,
|
group,
|
||||||
groups,
|
|
||||||
associations,
|
|
||||||
onReply
|
onReply
|
||||||
} = this.props;
|
} = this.props;
|
||||||
const { unreadMarkerRef } = this;
|
const { unreadMarkerRef } = this;
|
||||||
@ -252,10 +262,8 @@ class ChatWindow extends Component<
|
|||||||
group,
|
group,
|
||||||
showOurContact,
|
showOurContact,
|
||||||
unreadMarkerRef,
|
unreadMarkerRef,
|
||||||
history,
|
|
||||||
api,
|
api,
|
||||||
groups,
|
group,
|
||||||
associations,
|
|
||||||
onReply
|
onReply
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -275,10 +283,10 @@ class ChatWindow extends Component<
|
|||||||
graph.peekLargest()?.[0] ?? bigInt.zero
|
graph.peekLargest()?.[0] ?? bigInt.zero
|
||||||
);
|
);
|
||||||
const highlighted = index.eq(this.props.scrollTo ?? bigInt.zero);
|
const highlighted = index.eq(this.props.scrollTo ?? bigInt.zero);
|
||||||
const keys = graph.keys().reverse();
|
const keys = graph.keys();
|
||||||
const graphIdx = keys.findIndex((idx) => idx.eq(index));
|
const graphIdx = keys.findIndex((idx) => idx.eq(index));
|
||||||
const prevIdx = keys[graphIdx + 1];
|
const prevIdx = keys[graphIdx - 1];
|
||||||
const nextIdx = keys[graphIdx - 1];
|
const nextIdx = keys[graphIdx + 1];
|
||||||
const isLastRead: boolean = this.state.unreadIndex.eq(index);
|
const isLastRead: boolean = this.state.unreadIndex.eq(index);
|
||||||
const props = {
|
const props = {
|
||||||
highlighted,
|
highlighted,
|
||||||
@ -308,12 +316,8 @@ class ChatWindow extends Component<
|
|||||||
association,
|
association,
|
||||||
group,
|
group,
|
||||||
graph,
|
graph,
|
||||||
history,
|
|
||||||
groups,
|
|
||||||
associations,
|
|
||||||
showOurContact,
|
showOurContact,
|
||||||
pendingSize,
|
pendingSize = 0,
|
||||||
onReply,
|
|
||||||
} = this.props;
|
} = this.props;
|
||||||
|
|
||||||
const unreadMarkerRef = this.unreadMarkerRef;
|
const unreadMarkerRef = this.unreadMarkerRef;
|
||||||
@ -321,16 +325,10 @@ class ChatWindow extends Component<
|
|||||||
association,
|
association,
|
||||||
group,
|
group,
|
||||||
unreadMarkerRef,
|
unreadMarkerRef,
|
||||||
history,
|
|
||||||
api,
|
api,
|
||||||
associations
|
|
||||||
};
|
};
|
||||||
const unreadMsg = graph.get(this.state.unreadIndex);
|
const unreadMsg = graph.get(this.state.unreadIndex);
|
||||||
|
|
||||||
// hack to force a re-render when we toggle showing contact
|
|
||||||
const contactsModified =
|
|
||||||
showOurContact ? 0 : 100;
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Col height='100%' overflow='hidden' position='relative'>
|
<Col height='100%' overflow='hidden' position='relative'>
|
||||||
{ this.dismissedInitialUnread() &&
|
{ this.dismissedInitialUnread() &&
|
||||||
@ -353,12 +351,11 @@ class ChatWindow extends Component<
|
|||||||
offset={unreadCount}
|
offset={unreadCount}
|
||||||
origin='bottom'
|
origin='bottom'
|
||||||
style={virtScrollerStyle}
|
style={virtScrollerStyle}
|
||||||
onStartReached={this.setActive}
|
|
||||||
onBottomLoaded={this.onBottomLoaded}
|
onBottomLoaded={this.onBottomLoaded}
|
||||||
onScroll={this.onScroll}
|
onScroll={this.onScroll}
|
||||||
data={graph}
|
data={graph}
|
||||||
size={graph.size}
|
size={graph.size}
|
||||||
pendingSize={pendingSize + contactsModified}
|
pendingSize={pendingSize}
|
||||||
id={association.resource}
|
id={association.resource}
|
||||||
averageHeight={22}
|
averageHeight={22}
|
||||||
renderer={this.renderer}
|
renderer={this.renderer}
|
||||||
@ -369,8 +366,5 @@ class ChatWindow extends Component<
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default withState(ChatWindow, [
|
|
||||||
[useGroupState, ['groups']],
|
export default ChatWindow
|
||||||
[useMetadataState, ['associations']],
|
|
||||||
[useGraphState, ['pendingSize']]
|
|
||||||
]);
|
|
||||||
|
@ -8,22 +8,11 @@ import Timestamp from '~/views/components/Timestamp';
|
|||||||
export const UnreadNotice = (props) => {
|
export const UnreadNotice = (props) => {
|
||||||
const { unreadCount, unreadMsg, dismissUnread, onClick } = props;
|
const { unreadCount, unreadMsg, dismissUnread, onClick } = props;
|
||||||
|
|
||||||
if (!unreadMsg || unreadCount === 0) {
|
if (unreadCount === 0) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const stamp = moment.unix(unreadMsg.post['time-sent'] / 1000);
|
const stamp = unreadMsg && moment.unix(unreadMsg.post['time-sent'] / 1000);
|
||||||
|
|
||||||
let datestamp = moment
|
|
||||||
.unix(unreadMsg.post['time-sent'] / 1000)
|
|
||||||
.format('YYYY.M.D');
|
|
||||||
const timestamp = moment
|
|
||||||
.unix(unreadMsg.post['time-sent'] / 1000)
|
|
||||||
.format('HH:mm');
|
|
||||||
|
|
||||||
if (datestamp === moment().format('YYYY.M.D')) {
|
|
||||||
datestamp = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box
|
<Box
|
||||||
@ -52,15 +41,20 @@ export const UnreadNotice = (props) => {
|
|||||||
whiteSpace='pre'
|
whiteSpace='pre'
|
||||||
overflow='hidden'
|
overflow='hidden'
|
||||||
display='flex'
|
display='flex'
|
||||||
cursor='pointer'
|
cursor={unreadMsg ? 'pointer' : null}
|
||||||
onClick={onClick}
|
onClick={onClick}
|
||||||
>
|
>
|
||||||
{unreadCount} new message{unreadCount > 1 ? 's' : ''} since{' '}
|
{unreadCount} new message{unreadCount > 1 ? 's' : ''}
|
||||||
<Timestamp stamp={stamp} color='black' date={true} fontSize={1} />
|
{unreadMsg && (
|
||||||
|
<>
|
||||||
|
{' '}since{' '}
|
||||||
|
<Timestamp stamp={stamp} color='black' date={true} fontSize={1} />
|
||||||
|
</>
|
||||||
|
)}
|
||||||
</Text>
|
</Text>
|
||||||
<Icon
|
<Icon
|
||||||
icon='X'
|
icon='X'
|
||||||
ml='4'
|
ml={unreadMsg ? 4 : 1}
|
||||||
color='black'
|
color='black'
|
||||||
cursor='pointer'
|
cursor='pointer'
|
||||||
textAlign='right'
|
textAlign='right'
|
||||||
|
@ -23,7 +23,7 @@ type LinkResourceProps = StoreState & {
|
|||||||
association: Association;
|
association: Association;
|
||||||
api: GlobalApi;
|
api: GlobalApi;
|
||||||
baseUrl: string;
|
baseUrl: string;
|
||||||
} & RouteComponentProps;
|
};
|
||||||
|
|
||||||
export function LinkResource(props: LinkResourceProps) {
|
export function LinkResource(props: LinkResourceProps) {
|
||||||
const {
|
const {
|
||||||
|
@ -6,7 +6,7 @@ import React, {
|
|||||||
Component,
|
Component,
|
||||||
} from "react";
|
} from "react";
|
||||||
|
|
||||||
import { Col, Text } from "@tlon/indigo-react";
|
import { Box, Col, Text } from "@tlon/indigo-react";
|
||||||
import bigInt from "big-integer";
|
import bigInt from "big-integer";
|
||||||
import { Association, Graph, Unreads, Group, Rolodex } from "@urbit/api";
|
import { Association, Graph, Unreads, Group, Rolodex } from "@urbit/api";
|
||||||
|
|
||||||
@ -48,7 +48,7 @@ class LinkWindow extends Component<LinkWindowProps, {}> {
|
|||||||
return isWriter(group, association.resource);
|
return isWriter(group, association.resource);
|
||||||
}
|
}
|
||||||
|
|
||||||
renderItem = ({ index, scrollWindow }) => {
|
renderItem = React.forwardRef(({ index, scrollWindow }, ref) => {
|
||||||
const { props } = this;
|
const { props } = this;
|
||||||
const { association, graph, api } = props;
|
const { association, graph, api } = props;
|
||||||
const [, , ship, name] = association.resource.split("/");
|
const [, , ship, name] = association.resource.split("/");
|
||||||
@ -80,12 +80,14 @@ class LinkWindow extends Component<LinkWindowProps, {}> {
|
|||||||
api={api}
|
api={api}
|
||||||
/>
|
/>
|
||||||
</Col>
|
</Col>
|
||||||
<LinkItem {...linkProps} />
|
<LinkItem ref={ref} {...linkProps} />
|
||||||
</React.Fragment>
|
</React.Fragment>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
return <LinkItem key={index.toString()} {...linkProps} />;
|
return <Box ref={ref}>
|
||||||
};
|
<LinkItem ref={ref} key={index.toString()} {...linkProps} />;
|
||||||
|
</Box>
|
||||||
|
});
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const { graph, api, association } = this.props;
|
const { graph, api, association } = this.props;
|
||||||
@ -136,4 +138,4 @@ class LinkWindow extends Component<LinkWindowProps, {}> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default LinkWindow;
|
export default LinkWindow;
|
||||||
|
@ -19,7 +19,7 @@ interface LinkItemProps {
|
|||||||
node: GraphNode;
|
node: GraphNode;
|
||||||
association: Association;
|
association: Association;
|
||||||
resource: string; api: GlobalApi; group: Group; path: string; }
|
resource: string; api: GlobalApi; group: Group; path: string; }
|
||||||
export const LinkItem = (props: LinkItemProps): ReactElement => {
|
export const LinkItem = React.forwardRef((props: LinkItemProps, ref): ReactElement => {
|
||||||
const {
|
const {
|
||||||
association,
|
association,
|
||||||
node,
|
node,
|
||||||
@ -30,7 +30,6 @@ export const LinkItem = (props: LinkItemProps): ReactElement => {
|
|||||||
...rest
|
...rest
|
||||||
} = props;
|
} = props;
|
||||||
|
|
||||||
const ref = useRef<HTMLDivElement | null>(null);
|
|
||||||
const remoteRef = useRef<typeof RemoteContent | null>(null);
|
const remoteRef = useRef<typeof RemoteContent | null>(null);
|
||||||
const index = node.post.index.split('/')[1];
|
const index = node.post.index.split('/')[1];
|
||||||
|
|
||||||
@ -210,5 +209,5 @@ export const LinkItem = (props: LinkItemProps): ReactElement => {
|
|||||||
|
|
||||||
</Row>
|
</Row>
|
||||||
</Box>);
|
</Box>);
|
||||||
};
|
});
|
||||||
|
|
||||||
|
@ -11,7 +11,7 @@ type PublishResourceProps = StoreState & {
|
|||||||
association: Association;
|
association: Association;
|
||||||
api: GlobalApi;
|
api: GlobalApi;
|
||||||
baseUrl: string;
|
baseUrl: string;
|
||||||
} & RouteComponentProps;
|
};
|
||||||
|
|
||||||
export function PublishResource(props: PublishResourceProps) {
|
export function PublishResource(props: PublishResourceProps) {
|
||||||
const { association, api, baseUrl, notebooks } = props;
|
const { association, api, baseUrl, notebooks } = props;
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import React, { Component, useCallback } from 'react';
|
import React, { Component, useCallback, SyntheticEvent } from 'react';
|
||||||
import _ from 'lodash';
|
import _ from 'lodash';
|
||||||
import normalizeWheel from 'normalize-wheel';
|
import normalizeWheel from 'normalize-wheel';
|
||||||
import bigInt, { BigInteger } from 'big-integer';
|
import bigInt, { BigInteger } from 'big-integer';
|
||||||
@ -76,7 +76,7 @@ interface VirtualScrollerProps<T> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
interface VirtualScrollerState<T> {
|
interface VirtualScrollerState<T> {
|
||||||
visibleItems: BigIntOrderedMap<T>;
|
visibleItems: BigInteger[];
|
||||||
scrollbar: number;
|
scrollbar: number;
|
||||||
loaded: {
|
loaded: {
|
||||||
top: boolean;
|
top: boolean;
|
||||||
@ -91,10 +91,9 @@ const log = (level: LogLevel, message: string) => {
|
|||||||
if(logLevel.includes(level)) {
|
if(logLevel.includes(level)) {
|
||||||
console.log(`[${level}]: ${message}`);
|
console.log(`[${level}]: ${message}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const ZONE_SIZE = IS_IOS ? 10 : 40;
|
const ZONE_SIZE = IS_IOS ? 10 : 80;
|
||||||
|
|
||||||
|
|
||||||
// nb: in this file, an index refers to a BigInteger and an offset refers to a
|
// nb: in this file, an index refers to a BigInteger and an offset refers to a
|
||||||
@ -114,7 +113,7 @@ export default class VirtualScroller<T> extends Component<VirtualScrollerProps<T
|
|||||||
/**
|
/**
|
||||||
* A map of child refs, used to calculate scroll position
|
* A map of child refs, used to calculate scroll position
|
||||||
*/
|
*/
|
||||||
private childRefs = new BigIntOrderedMap<HTMLElement>();
|
private childRefs = new Map<string, HTMLElement>();
|
||||||
/**
|
/**
|
||||||
* A set of child refs which have been unmounted
|
* A set of child refs which have been unmounted
|
||||||
*/
|
*/
|
||||||
@ -149,7 +148,7 @@ export default class VirtualScroller<T> extends Component<VirtualScrollerProps<T
|
|||||||
constructor(props: VirtualScrollerProps<T>) {
|
constructor(props: VirtualScrollerProps<T>) {
|
||||||
super(props);
|
super(props);
|
||||||
this.state = {
|
this.state = {
|
||||||
visibleItems: new BigIntOrderedMap(),
|
visibleItems: [],
|
||||||
scrollbar: 0,
|
scrollbar: 0,
|
||||||
loaded: {
|
loaded: {
|
||||||
top: false,
|
top: false,
|
||||||
@ -160,10 +159,11 @@ export default class VirtualScroller<T> extends Component<VirtualScrollerProps<T
|
|||||||
this.updateVisible = this.updateVisible.bind(this);
|
this.updateVisible = this.updateVisible.bind(this);
|
||||||
|
|
||||||
this.invertedKeyHandler = this.invertedKeyHandler.bind(this);
|
this.invertedKeyHandler = this.invertedKeyHandler.bind(this);
|
||||||
this.onScroll = IS_IOS ? _.debounce(this.onScroll.bind(this), 400) : this.onScroll.bind(this);
|
this.onScroll = IS_IOS ? _.debounce(this.onScroll.bind(this), 200) : this.onScroll.bind(this);
|
||||||
this.scrollKeyMap = this.scrollKeyMap.bind(this);
|
this.scrollKeyMap = this.scrollKeyMap.bind(this);
|
||||||
this.setWindow = this.setWindow.bind(this);
|
this.setWindow = this.setWindow.bind(this);
|
||||||
this.restore = this.restore.bind(this);
|
this.restore = this.restore.bind(this);
|
||||||
|
this.startOffset = this.startOffset.bind(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
componentDidMount() {
|
componentDidMount() {
|
||||||
@ -181,7 +181,7 @@ export default class VirtualScroller<T> extends Component<VirtualScrollerProps<T
|
|||||||
}
|
}
|
||||||
[...this.orphans].forEach(o => {
|
[...this.orphans].forEach(o => {
|
||||||
const index = bigInt(o);
|
const index = bigInt(o);
|
||||||
this.childRefs.delete(index);
|
this.childRefs.delete(index.toString());
|
||||||
});
|
});
|
||||||
this.orphans.clear();
|
this.orphans.clear();
|
||||||
};
|
};
|
||||||
@ -206,13 +206,10 @@ export default class VirtualScroller<T> extends Component<VirtualScrollerProps<T
|
|||||||
|
|
||||||
componentDidUpdate(prevProps: VirtualScrollerProps<T>, _prevState: VirtualScrollerState<T>) {
|
componentDidUpdate(prevProps: VirtualScrollerProps<T>, _prevState: VirtualScrollerState<T>) {
|
||||||
const { id, size, data, offset, pendingSize } = this.props;
|
const { id, size, data, offset, pendingSize } = this.props;
|
||||||
const { visibleItems } = this.state;
|
|
||||||
|
|
||||||
if(size !== prevProps.size || pendingSize !== prevProps.pendingSize) {
|
if(size !== prevProps.size || pendingSize !== prevProps.pendingSize) {
|
||||||
if(this.scrollLocked && visibleItems?.peekLargest() && data?.peekLargest()) {
|
if(this.scrollLocked) {
|
||||||
if(!visibleItems.peekLargest()[0].eq(data.peekLargest()[0])) {
|
this.updateVisible(0);
|
||||||
this.updateVisible(0);
|
|
||||||
}
|
|
||||||
this.resetScroll();
|
this.resetScroll();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -228,11 +225,13 @@ export default class VirtualScroller<T> extends Component<VirtualScrollerProps<T
|
|||||||
}
|
}
|
||||||
|
|
||||||
startOffset() {
|
startOffset() {
|
||||||
const startIndex = this.state?.visibleItems?.peekLargest()?.[0];
|
const { data } = this.props;
|
||||||
|
const startIndex = this.state.visibleItems?.[0];
|
||||||
if(!startIndex) {
|
if(!startIndex) {
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
const offset = [...this.props.data].findIndex(([i]) => i.eq(startIndex))
|
const dataList = Array.from(data);
|
||||||
|
const offset = dataList.findIndex(([i]) => i.eq(startIndex))
|
||||||
if(offset === -1) {
|
if(offset === -1) {
|
||||||
// TODO: revisit when we remove nodes for any other reason than
|
// TODO: revisit when we remove nodes for any other reason than
|
||||||
// pending indices being removed
|
// pending indices being removed
|
||||||
@ -252,19 +251,17 @@ export default class VirtualScroller<T> extends Component<VirtualScrollerProps<T
|
|||||||
log('reflow', `from: ${this.startOffset()} to: ${newOffset}`);
|
log('reflow', `from: ${this.startOffset()} to: ${newOffset}`);
|
||||||
|
|
||||||
const { data, onCalculateVisibleItems } = this.props;
|
const { data, onCalculateVisibleItems } = this.props;
|
||||||
const visibleItems = new BigIntOrderedMap<any>(
|
const visibleItems = data.keys().slice(newOffset, newOffset + this.pageSize);
|
||||||
[...data].slice(newOffset, newOffset + this.pageSize)
|
|
||||||
);
|
|
||||||
|
|
||||||
this.save();
|
this.save();
|
||||||
|
|
||||||
this.setState({
|
this.setState({
|
||||||
visibleItems,
|
visibleItems,
|
||||||
}, () => {
|
|
||||||
requestAnimationFrame(() => {
|
|
||||||
this.restore();
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
requestAnimationFrame(() => {
|
||||||
|
this.restore();
|
||||||
|
});
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
scrollKeyMap(): Map<string, number> {
|
scrollKeyMap(): Map<string, number> {
|
||||||
@ -296,7 +293,6 @@ export default class VirtualScroller<T> extends Component<VirtualScrollerProps<T
|
|||||||
setWindow(element) {
|
setWindow(element) {
|
||||||
if (!element)
|
if (!element)
|
||||||
return;
|
return;
|
||||||
console.log('resetting window');
|
|
||||||
this.save();
|
this.save();
|
||||||
|
|
||||||
if (this.window) {
|
if (this.window) {
|
||||||
@ -309,8 +305,8 @@ export default class VirtualScroller<T> extends Component<VirtualScrollerProps<T
|
|||||||
const { averageHeight } = this.props;
|
const { averageHeight } = this.props;
|
||||||
|
|
||||||
this.window = element;
|
this.window = element;
|
||||||
this.pageSize = Math.floor(element.offsetHeight / Math.floor(averageHeight / 5.5));
|
this.pageSize = Math.floor(element.offsetHeight / Math.floor(averageHeight / 2));
|
||||||
this.pageDelta = Math.floor(this.pageSize / 3);
|
this.pageDelta = Math.floor(this.pageSize / 4);
|
||||||
if (this.props.origin === 'bottom') {
|
if (this.props.origin === 'bottom') {
|
||||||
element.addEventListener('wheel', (event) => {
|
element.addEventListener('wheel', (event) => {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
@ -356,7 +352,7 @@ export default class VirtualScroller<T> extends Component<VirtualScrollerProps<T
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
onScroll(event: UIEvent) {
|
onScroll(event: SyntheticEvent<HTMLElement, ScrollEvent>) {
|
||||||
this.updateScroll();
|
this.updateScroll();
|
||||||
if(!this.window) {
|
if(!this.window) {
|
||||||
// bail if we're going to adjust scroll anyway
|
// bail if we're going to adjust scroll anyway
|
||||||
@ -371,6 +367,8 @@ export default class VirtualScroller<T> extends Component<VirtualScrollerProps<T
|
|||||||
const { scrollTop, scrollHeight } = this.window;
|
const { scrollTop, scrollHeight } = this.window;
|
||||||
|
|
||||||
const startOffset = this.startOffset();
|
const startOffset = this.startOffset();
|
||||||
|
|
||||||
|
const scrollEnd = scrollTop + windowHeight;
|
||||||
if (scrollTop < ZONE_SIZE) {
|
if (scrollTop < ZONE_SIZE) {
|
||||||
log('scroll', `Entered start zone ${scrollTop}`);
|
log('scroll', `Entered start zone ${scrollTop}`);
|
||||||
if (startOffset === 0) {
|
if (startOffset === 0) {
|
||||||
@ -415,30 +413,37 @@ export default class VirtualScroller<T> extends Component<VirtualScrollerProps<T
|
|||||||
log('bail', 'Deep restore');
|
log('bail', 'Deep restore');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if(this.scrollLocked) {
|
if(this.scrollLocked) {
|
||||||
this.resetScroll();
|
this.resetScroll();
|
||||||
this.savedIndex = null;
|
requestAnimationFrame(() => {
|
||||||
this.savedDistance = 0;
|
this.savedIndex = null;
|
||||||
this.saveDepth--;
|
this.savedDistance = 0;
|
||||||
|
this.saveDepth--;
|
||||||
|
});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
let ref = this.childRefs.get(this.savedIndex)
|
let ref = this.childRefs.get(this.savedIndex.toString())
|
||||||
if(!ref) {
|
if(!ref) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const newScrollTop = this.window.scrollHeight - ref.offsetTop - this.savedDistance;
|
|
||||||
|
const newScrollTop = this.props.origin === 'top'
|
||||||
|
? this.savedDistance + ref.offsetTop
|
||||||
|
: this.window.scrollHeight - ref.offsetTop - this.savedDistance;
|
||||||
|
|
||||||
|
|
||||||
this.window.scrollTo(0, newScrollTop);
|
this.window.scrollTo(0, newScrollTop);
|
||||||
requestAnimationFrame(() => {
|
requestAnimationFrame(() => {
|
||||||
this.savedIndex = null;
|
this.savedIndex = null;
|
||||||
this.savedDistance = 0;
|
this.savedDistance = 0;
|
||||||
this.saveDepth--;
|
this.saveDepth--;
|
||||||
});
|
});
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
scrollToIndex = (index: BigInteger) => {
|
scrollToIndex = (index: BigInteger) => {
|
||||||
let ref = this.childRefs.get(index);
|
let ref = this.childRefs.get(index.toString());
|
||||||
if(!ref) {
|
if(!ref) {
|
||||||
const offset = [...this.props.data].findIndex(([idx]) => idx.eq(index));
|
const offset = [...this.props.data].findIndex(([idx]) => idx.eq(index));
|
||||||
if(offset === -1) {
|
if(offset === -1) {
|
||||||
@ -446,7 +451,7 @@ export default class VirtualScroller<T> extends Component<VirtualScrollerProps<T
|
|||||||
}
|
}
|
||||||
this.updateVisible(Math.max(offset - this.pageDelta, 0));
|
this.updateVisible(Math.max(offset - this.pageDelta, 0));
|
||||||
requestAnimationFrame(() => {
|
requestAnimationFrame(() => {
|
||||||
ref = this.childRefs.get(index);
|
ref = this.childRefs.get(index.toString());
|
||||||
this.savedIndex = null;
|
this.savedIndex = null;
|
||||||
this.savedDistance = 0;
|
this.savedDistance = 0;
|
||||||
this.saveDepth = 0;
|
this.saveDepth = 0;
|
||||||
@ -467,17 +472,20 @@ export default class VirtualScroller<T> extends Component<VirtualScrollerProps<T
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if(this.saveDepth !== 0) {
|
if(this.saveDepth !== 0) {
|
||||||
console.log('bail', 'deep save');
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.saveDepth++;
|
log('scroll', 'saving...');
|
||||||
|
|
||||||
let bottomIndex: BigInteger | null = null;
|
this.saveDepth++;
|
||||||
|
const { visibleItems } = this.state;
|
||||||
|
|
||||||
|
let bottomIndex = visibleItems[visibleItems.length - 1];
|
||||||
const { scrollTop, scrollHeight } = this.window;
|
const { scrollTop, scrollHeight } = this.window;
|
||||||
const topSpacing = scrollHeight - scrollTop;
|
const topSpacing = this.props.origin === 'top' ? scrollTop : scrollHeight - scrollTop;
|
||||||
[...Array.from(this.state.visibleItems)].reverse().forEach(([index, datum]) => {
|
const items = this.props.origin === 'top' ? visibleItems : [...visibleItems].reverse();
|
||||||
const el = this.childRefs.get(index);
|
items.forEach((index) => {
|
||||||
|
const el = this.childRefs.get(index.toString());
|
||||||
if(!el) {
|
if(!el) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -490,20 +498,22 @@ export default class VirtualScroller<T> extends Component<VirtualScrollerProps<T
|
|||||||
if(!bottomIndex) {
|
if(!bottomIndex) {
|
||||||
// weird, shouldn't really happen
|
// weird, shouldn't really happen
|
||||||
this.saveDepth--;
|
this.saveDepth--;
|
||||||
|
log('bail', 'no index found');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.savedIndex = bottomIndex;
|
this.savedIndex = bottomIndex;
|
||||||
const ref = this.childRefs.get(bottomIndex)!;
|
const ref = this.childRefs.get(bottomIndex.toString())!;
|
||||||
const { offsetTop } = ref;
|
const { offsetTop } = ref;
|
||||||
this.savedDistance = topSpacing - offsetTop
|
this.savedDistance = topSpacing - offsetTop
|
||||||
}
|
}
|
||||||
|
|
||||||
shiftLayout = { save: this.save.bind(this), restore: this.restore.bind(this) };
|
// disabled until we work out race conditions with loading new nodes
|
||||||
|
shiftLayout = { save: () => {}, restore: () => {} };
|
||||||
|
|
||||||
setRef = (element: HTMLElement | null, index: BigInteger) => {
|
setRef = (element: HTMLElement | null, index: BigInteger) => {
|
||||||
if(element) {
|
if(element) {
|
||||||
this.childRefs.set(index, element);
|
this.childRefs.set(index.toString(), element);
|
||||||
this.orphans.delete(index.toString());
|
this.orphans.delete(index.toString());
|
||||||
} else {
|
} else {
|
||||||
this.orphans.add(index.toString());
|
this.orphans.add(index.toString());
|
||||||
@ -525,12 +535,11 @@ export default class VirtualScroller<T> extends Component<VirtualScrollerProps<T
|
|||||||
|
|
||||||
const isTop = origin === 'top';
|
const isTop = origin === 'top';
|
||||||
|
|
||||||
const indexesToRender = isTop ? visibleItems.keys() : visibleItems.keys().reverse();
|
|
||||||
|
|
||||||
const transform = isTop ? 'scale3d(1, 1, 1)' : 'scale3d(1, -1, 1)';
|
const transform = isTop ? 'scale3d(1, 1, 1)' : 'scale3d(1, -1, 1)';
|
||||||
|
const children = isTop ? visibleItems : [...visibleItems].reverse();
|
||||||
|
|
||||||
const atStart = (this.props.data.peekLargest()?.[0] ?? bigInt.zero).eq(visibleItems.peekLargest()?.[0] || bigInt.zero);
|
const atStart = (this.props.data.peekLargest()?.[0] ?? bigInt.zero).eq(visibleItems?.[0] || bigInt.zero);
|
||||||
const atEnd = this.state.loaded.top;
|
const atEnd = (this.props.data.peekSmallest()?.[0] ?? bigInt.zero).eq(visibleItems?.[visibleItems.length -1 ] || bigInt.zero);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
@ -542,7 +551,7 @@ export default class VirtualScroller<T> extends Component<VirtualScrollerProps<T
|
|||||||
<LoadingSpinner />
|
<LoadingSpinner />
|
||||||
</Center>)}
|
</Center>)}
|
||||||
<VirtualContext.Provider value={this.shiftLayout}>
|
<VirtualContext.Provider value={this.shiftLayout}>
|
||||||
{indexesToRender.map(index => (
|
{children.map(index => (
|
||||||
<VirtualChild
|
<VirtualChild
|
||||||
key={index.toString()}
|
key={index.toString()}
|
||||||
setRef={this.setRef}
|
setRef={this.setRef}
|
||||||
@ -575,8 +584,10 @@ function VirtualChild(props: VirtualChildProps) {
|
|||||||
|
|
||||||
const ref = useCallback((el: HTMLElement | null) => {
|
const ref = useCallback((el: HTMLElement | null) => {
|
||||||
setRef(el, props.index);
|
setRef(el, props.index);
|
||||||
}, [setRef, props.index])
|
// VirtualChild should always be keyed on the index, so the index should be
|
||||||
|
// valid for the entire lifecycle of the component, hence no dependencies
|
||||||
|
}, []);
|
||||||
|
|
||||||
return (<Renderer ref={ref} {...rest} />);
|
return <Renderer ref={ref} {...rest} />
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -124,7 +124,6 @@ export function GroupsPane(props: GroupsPaneProps) {
|
|||||||
>
|
>
|
||||||
<Resource
|
<Resource
|
||||||
{...props}
|
{...props}
|
||||||
{...routeProps}
|
|
||||||
association={association}
|
association={association}
|
||||||
baseUrl={baseUrl}
|
baseUrl={baseUrl}
|
||||||
/>
|
/>
|
||||||
|
@ -2,7 +2,7 @@ import React from 'react';
|
|||||||
import bigInt from 'big-integer';
|
import bigInt from 'big-integer';
|
||||||
import VirtualScroller from "~/views/components/VirtualScroller";
|
import VirtualScroller from "~/views/components/VirtualScroller";
|
||||||
import PostItem from './PostItem/PostItem';
|
import PostItem from './PostItem/PostItem';
|
||||||
import { Col } from '@tlon/indigo-react';
|
import { Col, Box } from '@tlon/indigo-react';
|
||||||
import { resourceFromPath } from '~/logic/lib/group';
|
import { resourceFromPath } from '~/logic/lib/group';
|
||||||
|
|
||||||
|
|
||||||
@ -15,102 +15,103 @@ export class PostFeed extends React.Component {
|
|||||||
super(props);
|
super(props);
|
||||||
|
|
||||||
this.isFetching = false;
|
this.isFetching = false;
|
||||||
this.renderItem = React.forwardRef(({ index, scrollWindow }, ref) => {
|
|
||||||
const {
|
|
||||||
graph,
|
|
||||||
graphPath,
|
|
||||||
api,
|
|
||||||
history,
|
|
||||||
baseUrl,
|
|
||||||
parentNode,
|
|
||||||
grandparentNode,
|
|
||||||
association,
|
|
||||||
group,
|
|
||||||
vip
|
|
||||||
} = this.props;
|
|
||||||
const graphResource = resourceFromPath(graphPath);
|
|
||||||
const node = graph.get(index);
|
|
||||||
if (!node) { return null; }
|
|
||||||
|
|
||||||
const first = graph.peekLargest()?.[0];
|
|
||||||
const post = node?.post;
|
|
||||||
if (!node || !post) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
let nodeIndex = parentNode ? parentNode.post.index.split('/').slice(1).map((ind) => {
|
|
||||||
return bigInt(ind);
|
|
||||||
}) : [];
|
|
||||||
|
|
||||||
if (parentNode && index.eq(first ?? bigInt.zero)) {
|
|
||||||
return (
|
|
||||||
<React.Fragment key={index.toString()}>
|
|
||||||
<Col
|
|
||||||
key={index.toString()}
|
|
||||||
mb="3"
|
|
||||||
width="100%"
|
|
||||||
flexShrink={0}
|
|
||||||
>
|
|
||||||
<PostItem
|
|
||||||
key={parentNode.post.index}
|
|
||||||
ref={ref}
|
|
||||||
parentPost={grandparentNode?.post}
|
|
||||||
node={parentNode}
|
|
||||||
parentNode={grandparentNode}
|
|
||||||
graphPath={graphPath}
|
|
||||||
association={association}
|
|
||||||
api={api}
|
|
||||||
index={nodeIndex}
|
|
||||||
baseUrl={baseUrl}
|
|
||||||
history={history}
|
|
||||||
isParent={true}
|
|
||||||
isRelativeTime={false}
|
|
||||||
vip={vip}
|
|
||||||
group={group}
|
|
||||||
/>
|
|
||||||
</Col>
|
|
||||||
<PostItem
|
|
||||||
ref={ref}
|
|
||||||
node={node}
|
|
||||||
graphPath={graphPath}
|
|
||||||
association={association}
|
|
||||||
api={api}
|
|
||||||
index={[...nodeIndex, index]}
|
|
||||||
baseUrl={baseUrl}
|
|
||||||
history={history}
|
|
||||||
isReply={true}
|
|
||||||
parentPost={parentNode.post}
|
|
||||||
isRelativeTime={true}
|
|
||||||
vip={vip}
|
|
||||||
group={group}
|
|
||||||
/>
|
|
||||||
</React.Fragment>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<PostItem
|
|
||||||
key={index.toString()}
|
|
||||||
ref={ref}
|
|
||||||
node={node}
|
|
||||||
graphPath={graphPath}
|
|
||||||
association={association}
|
|
||||||
api={api}
|
|
||||||
index={[...nodeIndex, index]}
|
|
||||||
baseUrl={baseUrl}
|
|
||||||
history={history}
|
|
||||||
parentPost={parentNode?.post}
|
|
||||||
isReply={!!parentNode}
|
|
||||||
isRelativeTime={true}
|
|
||||||
vip={vip}
|
|
||||||
group={group}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
this.fetchPosts = this.fetchPosts.bind(this);
|
this.fetchPosts = this.fetchPosts.bind(this);
|
||||||
this.doNotFetch = this.doNotFetch.bind(this);
|
this.doNotFetch = this.doNotFetch.bind(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
renderItem = React.forwardRef(({ index, scrollWindow }, ref) => {
|
||||||
|
const {
|
||||||
|
graph,
|
||||||
|
graphPath,
|
||||||
|
api,
|
||||||
|
history,
|
||||||
|
baseUrl,
|
||||||
|
parentNode,
|
||||||
|
grandparentNode,
|
||||||
|
association,
|
||||||
|
group,
|
||||||
|
vip
|
||||||
|
} = this.props;
|
||||||
|
const graphResource = resourceFromPath(graphPath);
|
||||||
|
const node = graph.get(index);
|
||||||
|
if (!node) { return null; }
|
||||||
|
|
||||||
|
const first = graph.peekLargest()?.[0];
|
||||||
|
const post = node?.post;
|
||||||
|
if (!node || !post) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
let nodeIndex = parentNode ? parentNode.post.index.split('/').slice(1).map((ind) => {
|
||||||
|
return bigInt(ind);
|
||||||
|
}) : [];
|
||||||
|
|
||||||
|
if (parentNode && index.eq(first ?? bigInt.zero)) {
|
||||||
|
return (
|
||||||
|
<React.Fragment key={index.toString()}>
|
||||||
|
<Col
|
||||||
|
key={index.toString()}
|
||||||
|
ref={ref}
|
||||||
|
mb="3"
|
||||||
|
width="100%"
|
||||||
|
flexShrink={0}
|
||||||
|
>
|
||||||
|
<PostItem
|
||||||
|
key={parentNode.post.index}
|
||||||
|
parentPost={grandparentNode?.post}
|
||||||
|
node={parentNode}
|
||||||
|
parentNode={grandparentNode}
|
||||||
|
graphPath={graphPath}
|
||||||
|
association={association}
|
||||||
|
api={api}
|
||||||
|
index={nodeIndex}
|
||||||
|
baseUrl={baseUrl}
|
||||||
|
history={history}
|
||||||
|
isParent={true}
|
||||||
|
isRelativeTime={false}
|
||||||
|
vip={vip}
|
||||||
|
group={group}
|
||||||
|
/>
|
||||||
|
</Col>
|
||||||
|
<PostItem
|
||||||
|
node={node}
|
||||||
|
graphPath={graphPath}
|
||||||
|
association={association}
|
||||||
|
api={api}
|
||||||
|
index={[...nodeIndex, index]}
|
||||||
|
baseUrl={baseUrl}
|
||||||
|
history={history}
|
||||||
|
isReply={true}
|
||||||
|
parentPost={parentNode.post}
|
||||||
|
isRelativeTime={true}
|
||||||
|
vip={vip}
|
||||||
|
group={group}
|
||||||
|
/>
|
||||||
|
</React.Fragment>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Box key={index.toString()} ref={ref}>
|
||||||
|
<PostItem
|
||||||
|
node={node}
|
||||||
|
graphPath={graphPath}
|
||||||
|
association={association}
|
||||||
|
api={api}
|
||||||
|
index={[...nodeIndex, index]}
|
||||||
|
baseUrl={baseUrl}
|
||||||
|
history={history}
|
||||||
|
parentPost={parentNode?.post}
|
||||||
|
isReply={!!parentNode}
|
||||||
|
isRelativeTime={true}
|
||||||
|
vip={vip}
|
||||||
|
group={group}
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
async fetchPosts(newer) {
|
async fetchPosts(newer) {
|
||||||
const { graph, graphPath, api } = this.props;
|
const { graph, graphPath, api } = this.props;
|
||||||
const graphResource = resourceFromPath(graphPath);
|
const graphResource = resourceFromPath(graphPath);
|
||||||
|
@ -15,12 +15,14 @@ import useGroupState from '~/logic/state/group';
|
|||||||
import useContactState from '~/logic/state/contact';
|
import useContactState from '~/logic/state/contact';
|
||||||
import useHarkState from '~/logic/state/hark';
|
import useHarkState from '~/logic/state/hark';
|
||||||
import useMetadataState from '~/logic/state/metadata';
|
import useMetadataState from '~/logic/state/metadata';
|
||||||
|
import {Workspace} from '~/types';
|
||||||
|
|
||||||
type ResourceProps = StoreState & {
|
type ResourceProps = StoreState & {
|
||||||
association: Association;
|
association: Association;
|
||||||
api: GlobalApi;
|
api: GlobalApi;
|
||||||
baseUrl: string;
|
baseUrl: string;
|
||||||
} & RouteComponentProps;
|
workspace: Workspace;
|
||||||
|
};
|
||||||
|
|
||||||
export function Resource(props: ResourceProps): ReactElement {
|
export function Resource(props: ResourceProps): ReactElement {
|
||||||
const { association, api, notificationsGraphConfig } = props;
|
const { association, api, notificationsGraphConfig } = props;
|
||||||
|
@ -1,6 +1,10 @@
|
|||||||
import { immerable } from 'immer';
|
import produce, { immerable, castImmutable, castDraft, setAutoFreeze, enablePatches } from 'immer';
|
||||||
import bigInt, { BigInteger } from "big-integer";
|
import bigInt, { BigInteger } from "big-integer";
|
||||||
|
|
||||||
|
setAutoFreeze(false);
|
||||||
|
|
||||||
|
enablePatches();
|
||||||
|
|
||||||
function sortBigInt(a: BigInteger, b: BigInteger) {
|
function sortBigInt(a: BigInteger, b: BigInteger) {
|
||||||
if (a.lt(b)) {
|
if (a.lt(b)) {
|
||||||
return 1;
|
return 1;
|
||||||
@ -11,19 +15,18 @@ function sortBigInt(a: BigInteger, b: BigInteger) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
export default class BigIntOrderedMap<V> implements Iterable<[BigInteger, V]> {
|
export default class BigIntOrderedMap<V> implements Iterable<[BigInteger, V]> {
|
||||||
private root: Record<string, V> = {}
|
root: Record<string, V> = {}
|
||||||
private cachedIter: [BigInteger, V][] | null = null;
|
cachedIter: [BigInteger, V][] = [];
|
||||||
[immerable] = true;
|
[immerable] = true;
|
||||||
|
|
||||||
constructor(items: [BigInteger, V][] = []) {
|
constructor(items: [BigInteger, V][] = []) {
|
||||||
items.forEach(([key, val]) => {
|
items.forEach(([key, val]) => {
|
||||||
this.set(key, val);
|
this.set(key, val);
|
||||||
});
|
});
|
||||||
this.generateCachedIter();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
get size() {
|
get size() {
|
||||||
return this.cachedIter?.length ?? Object.keys(this.root).length;
|
return Object.keys(this.root).length;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@ -31,14 +34,30 @@ export default class BigIntOrderedMap<V> implements Iterable<[BigInteger, V]> {
|
|||||||
return this.root[key.toString()] ?? null;
|
return this.root[key.toString()] ?? null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
gas(items: [BigInteger, V][]) {
|
||||||
|
return produce(this, draft => {
|
||||||
|
items.forEach(([key, value]) => {
|
||||||
|
draft.root[key.toString()] = castDraft(value);
|
||||||
|
});
|
||||||
|
draft.generateCachedIter();
|
||||||
|
},
|
||||||
|
(patches) => {
|
||||||
|
//console.log(`gassed with ${JSON.stringify(patches, null, 2)}`);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
set(key: BigInteger, value: V) {
|
set(key: BigInteger, value: V) {
|
||||||
this.root[key.toString()] = value;
|
return produce(this, draft => {
|
||||||
this.cachedIter = null;
|
draft.root[key.toString()] = castDraft(value);
|
||||||
|
draft.generateCachedIter();
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
clear() {
|
clear() {
|
||||||
this.cachedIter = null;
|
return produce(this, draft => {
|
||||||
this.root = {}
|
draft.cachedIter = [];
|
||||||
|
draft.root = {}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
has(key: BigInteger) {
|
has(key: BigInteger) {
|
||||||
@ -46,17 +65,15 @@ export default class BigIntOrderedMap<V> implements Iterable<[BigInteger, V]> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
delete(key: BigInteger) {
|
delete(key: BigInteger) {
|
||||||
const had = this.has(key);
|
return produce(this, draft => {
|
||||||
if(had) {
|
delete draft.root[key.toString()];
|
||||||
delete this.root[key.toString()];
|
draft.cachedIter = draft.cachedIter.filter(([x]) => x.eq(key));
|
||||||
this.cachedIter = null;
|
});
|
||||||
}
|
|
||||||
return had;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
[Symbol.iterator](): IterableIterator<[BigInteger, V]> {
|
[Symbol.iterator](): IterableIterator<[BigInteger, V]> {
|
||||||
let idx = 0;
|
let idx = 0;
|
||||||
const result = this.generateCachedIter();
|
let result = [...this.cachedIter];
|
||||||
return {
|
return {
|
||||||
[Symbol.iterator]: this[Symbol.iterator],
|
[Symbol.iterator]: this[Symbol.iterator],
|
||||||
next: (): IteratorResult<[BigInteger, V]> => {
|
next: (): IteratorResult<[BigInteger, V]> => {
|
||||||
@ -79,19 +96,15 @@ export default class BigIntOrderedMap<V> implements Iterable<[BigInteger, V]> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
keys() {
|
keys() {
|
||||||
return Object.keys(this.root).map(k => bigInt(k)).sort(sortBigInt)
|
return Array.from(this).map(([k,v]) => k);
|
||||||
}
|
}
|
||||||
|
|
||||||
private generateCachedIter() {
|
generateCachedIter() {
|
||||||
if(this.cachedIter) {
|
|
||||||
return this.cachedIter;
|
|
||||||
}
|
|
||||||
const result = Object.keys(this.root).map(key => {
|
const result = Object.keys(this.root).map(key => {
|
||||||
const num = bigInt(key);
|
const num = bigInt(key);
|
||||||
return [num, this.root[key]] as [BigInteger, V];
|
return [num, this.root[key]] as [BigInteger, V];
|
||||||
}).sort(([a], [b]) => sortBigInt(a,b));
|
}).sort(([a], [b]) => sortBigInt(a,b));
|
||||||
this.cachedIter = result;
|
this.cachedIter = result;
|
||||||
return result;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user