group-feed: improve performance and make mobile load

This commit is contained in:
Logan Allen 2021-03-26 16:52:50 -05:00
parent b7e52edd58
commit 2dddf1b837
12 changed files with 322 additions and 294 deletions

View File

@ -0,0 +1,20 @@
import { memo } from 'react';
const withMemo = (Component, checkedProps) => {
function areEqual(prevProps, nextProps) {
let isEqual = true;
for (let i = 0; i < checkedProps.length; i++) {
const checkedProp = checkedProps[i];
console.log(checkedProp);
if (JSON.stringify(prevProps[checkedProp]) !== JSON.stringify(nextProps[checkedProp])) {
isEqual = false;
break;
}
}
return isEqual;
}
return memo(Component, areEqual);
};
export default withMemo;

View File

@ -210,7 +210,6 @@ export function GroupsPane(props: GroupsPaneProps) {
associations={associations}
groups={groups}
groupPath={groupPath}
contacts={contacts}
workspace={workspace}
/>
{popovers(routeProps, baseUrl)}

View File

@ -6,8 +6,8 @@ import { Col } from '@tlon/indigo-react'
import { resourceFromPath } from '~/logic/lib/group';
import useGraphState from '~/logic/state/graph';
import { GroupFeedHeader } from './GroupFeedHeader';
import { PostTimeline } from './Post/PostTimeline';
import { PostReplies } from './Post/PostReplies';
import PostTimeline from './Post/PostTimeline';
import PostReplies from './Post/PostReplies';
export function GroupFeed(props) {
@ -16,7 +16,7 @@ export function GroupFeed(props) {
api,
history,
associations,
graphPath
graphPath,
} = props;
const graphs = useGraphState(state => state.graphs);
const graphResource = resourceFromPath(graphPath);
@ -29,6 +29,9 @@ export function GroupFeed(props) {
const relativePath = (path) => baseUrl + path;
const association = associations.graph[graphPath];
const graphId = `${graphResource.ship.slice(1)}/${graphResource.name}`;
const graph = graphs[graphId];
useEffect(() => {
// TODO: VirtualScroller should support lower starting values than 100
api.graph.getNewest(graphResource.ship, graphResource.name, 100);
@ -53,11 +56,13 @@ export function GroupFeed(props) {
render={(routeProps) => {
return (
<PostTimeline
{...props}
baseUrl={baseUrl}
api={api}
history={history}
graphPath={graphPath}
association={association}
graphs={graphs}
pendingSize={pendingSize}
graphResource={graphResource} />
graph={graph}
pendingSize={pendingSize} />
);
}} />
<Route
@ -65,11 +70,13 @@ export function GroupFeed(props) {
render={(routeProps) => {
return (
<PostReplies
{...props}
baseUrl={baseUrl}
api={api}
history={history}
graphPath={graphPath}
association={association}
graphs={graphs}
pendingSize={pendingSize}
graphResource={graphResource} />
graph={graph}
pendingSize={pendingSize} />
);
}} />
</Switch>

View File

@ -15,8 +15,7 @@ export function GroupHome(props) {
groupPath,
groups,
graphs,
baseUrl,
contacts,
baseUrl
} = props;
const metadata = associations?.groups[groupPath]?.metadata;
@ -59,7 +58,6 @@ export function GroupHome(props) {
<GroupFeed
associations={associations}
groups={groups}
contacts={contacts}
graphPath={graphPath}
graphs={graphs}
api={api}

View File

@ -1,10 +1,13 @@
import React from 'react';
import { Col } from '@tlon/indigo-react';
import { MentionText } from '~/views/components/MentionText';
import useContactState from '~/logic/state/contact';
export function PostContent(props) {
const { post, contacts, isParent, api } = props;
const { post, isParent, api } = props;
const contacts = useContactState(state => state.contacts);
return (
<Col
width="100%"

View File

@ -3,13 +3,13 @@ import bigInt from 'big-integer';
import VirtualScroller from "~/views/components/VirtualScroller";
import PostItem from './PostItem';
import { Col } from '@tlon/indigo-react';
import { resourceFromPath } from '~/logic/lib/group';
const virtualScrollerStyle = {
height: "100%"
};
export class PostFeed extends React.Component {
constructor(props) {
super(props);
@ -18,14 +18,14 @@ export class PostFeed extends React.Component {
this.renderItem = React.forwardRef(({ index, scrollWindow }, ref) => {
const {
graph,
graphResource,
contacts,
graphPath,
api,
history,
baseUrl,
parentNode,
association
} = this.props;
const graphResource = resourceFromPath(graphPath);
const node = graph.get(index);
if (!node) { return null; }
@ -39,7 +39,6 @@ export class PostFeed extends React.Component {
}) : [];
if (parentNode && index.eq(first ?? bigInt.zero)) {
return (
<React.Fragment key={index.toString()}>
<Col
@ -52,8 +51,7 @@ export class PostFeed extends React.Component {
key={parentNode.post.index}
ref={ref}
node={parentNode}
contacts={contacts}
graphResource={graphResource}
graphPath={graphPath}
association={association}
api={api}
index={nodeIndex}
@ -65,8 +63,7 @@ export class PostFeed extends React.Component {
<PostItem
ref={ref}
node={node}
contacts={contacts}
graphResource={graphResource}
graphPath={graphPath}
association={association}
api={api}
index={[...nodeIndex, index]}
@ -84,8 +81,7 @@ export class PostFeed extends React.Component {
key={index.toString()}
ref={ref}
node={node}
contacts={contacts}
graphResource={graphResource}
graphPath={graphPath}
association={association}
api={api}
index={[...nodeIndex, index]}
@ -102,7 +98,8 @@ export class PostFeed extends React.Component {
}
async fetchPosts(newer) {
const { graph, graphResource, api } = this.props;
const { graph, graphPath, api } = this.props;
const graphResource = resourceFromPath(graphPath);
if (this.isFetching) {
return false;
@ -134,11 +131,12 @@ export class PostFeed extends React.Component {
}
render() {
const { graph, pendingSize, parentNode } = this.props;
const { graph, pendingSize, parentNode, history } = this.props;
return (
<Col width="100%" height="100%" position="relative">
<VirtualScroller
key={history.location.pathname}
origin="top"
offset={0}
data={graph}

View File

@ -4,11 +4,12 @@ import Author from '~/views/components/Author';
import { useCopy } from '~/logic/lib/useCopy';
import { getPermalinkForGraph } from '~/logic/lib/permalinks';
import { Dropdown } from '~/views/components/Dropdown';
import useContactState from '~/logic/state/contact';
export function PostHeader(props) {
const { post, contacts, api, association, isReply } = props;
const { post, api, association, isReply } = props;
const contacts = useContactState(state => state.contacts);
const mb = isReply ? "2" : "3";
const permalink = !!association ? getPermalinkForGraph(

View File

@ -7,10 +7,12 @@ import tokenizeMessage from '~/logic/lib/tokenizeMessage';
import { useToggleState } from '~/logic/lib/useToggleState';
import { createPost } from '~/logic/api/graph';
import useStorage from '~/logic/lib/useStorage';
import { resourceFromPath } from '~/logic/lib/group';
export function PostInput(props) {
const { api, graphResource, index, submitCallback } = props;
const { api, graphPath, index, submitCallback } = props;
const graphResource = resourceFromPath(graphPath);
const [disabled, setDisabled] = useState(false);
const [code, toggleCode] = useToggleState(false);

View File

@ -7,6 +7,7 @@ import { PostInput } from './PostInput';
import { Mention } from "~/views/components/MentionText";
import withState from '~/logic/lib/withState';
import { useHovering } from '~/logic/lib/util';
import { resourceFromPath } from '~/logic/lib/group';
class PostItem extends React.Component {
@ -43,9 +44,8 @@ class PostItem extends React.Component {
render() {
const {
node,
contacts,
api,
graphResource,
graphPath,
association,
index,
innerRef,
@ -55,6 +55,7 @@ class PostItem extends React.Component {
hovering,
bind
} = this.props;
const graphResource = resourceFromPath(graphPath);
let indexString = '';
@ -113,7 +114,7 @@ class PostItem extends React.Component {
borderLeftColor="lightGray"></Box>
<PostInput
api={api}
graphResource={graphResource}
graphPath={graphPath}
index={indexString}
submitCallback={this.submitCallback} />
</Col>

View File

@ -5,109 +5,115 @@ import { PostInput } from './PostInput';
import PostItem from './PostItem';
import { PostFeed } from './PostFeed';
import { Loading } from '~/views/components/Loading';
import { resourceFromPath } from '~/logic/lib/group';
export function PostReplies(props) {
const {
baseUrl,
api,
history,
association,
groups,
contacts,
graphPath,
graphs,
pendingSize,
graphResource
} = props;
const graphId = `${graphResource.ship.slice(1)}/${graphResource.name}`;
const shouldRenderFeed = graphId in graphs;
export default class PostReplies extends React.PureComponent {
constructor(props) {
super(props);
this.whyDidYouRender = true;
}
render() {
let graph = this.props.graph;
const {
baseUrl,
api,
history,
association,
groups,
graphPath,
pendingSize
} = this.props;
const graphResource = resourceFromPath(graphPath);
const graphId = `${graphResource.ship.slice(1)}/${graphResource.name}`;
const shouldRenderFeed = !!graph;
if (!shouldRenderFeed) {
return (
<Box height="100%" width="100%" alignItems="center" pl="1" pt="3">
<Loading />
</Box>
);
}
const locationUrl =
history.location.pathname.replace(`${baseUrl}/feed`, '');
let nodeIndex = locationUrl.split('/').slice(1).map((ind) => {
return bigInt(ind);
});
let node;
nodeIndex.forEach((i) => {
if (!graph) {
return null;
}
node = graph.get(i);
if (!node) {
return null;
}
graph = node.children;
});
if (!node || !graph) {
return null;
}
const first = graph.peekLargest()?.[0];
if (!first) {
return (
<Col
key={0}
width="100%"
height="100%"
alignItems="center" overflowY="scroll">
<Box mt="3" width="100%" alignItems="center">
<PostItem
key={node.post.index}
node={node}
graphPath={graphPath}
association={association}
api={api}
index={nodeIndex}
baseUrl={baseUrl}
history={history}
isParent={true}
/>
</Box>
<Box
pl="2"
pr="2"
width="100%"
maxWidth="616px"
alignItems="center">
<Col bg="washedGray" width="100%" alignItems="center" p="3">
<Text textAlign="center" width="100%">
No one has posted any replies yet.
</Text>
</Col>
</Box>
</Col>
);
}
if (!shouldRenderFeed) {
return (
<Box height="100%" width="100%" alignItems="center" pl="1" pt="3">
<Loading />
<Box height="calc(100% - 48px)" width="100%" alignItems="center" pl="1" pt="3">
<PostFeed
key={locationUrl}
graphPath={graphPath}
graph={graph}
parentNode={node}
pendingSize={pendingSize}
association={association}
groups={groups}
contacts={contacts}
api={api}
history={history}
baseUrl={baseUrl}
/>
</Box>
);
}
const locationUrl =
history.location.pathname.replace(`${baseUrl}/feed`, '');
let nodeIndex = locationUrl.split('/').slice(1).map((ind) => {
return bigInt(ind);
});
let node;
let graph = graphs[graphId];
nodeIndex.forEach((i) => {
if (!graph) {
return null;
}
node = graph.get(i);
if (!node) {
return null;
}
graph = node.children;
});
if (!node || !graph) {
return null;
}
const first = graph.peekLargest()?.[0];
if (!first) {
return (
<Col
key={0}
width="100%"
height="100%"
alignItems="center" overflowY="scroll">
<Box mt="3" width="100%" alignItems="center">
<PostItem
key={node.post.index}
node={node}
contacts={contacts}
graphResource={graphResource}
association={association}
api={api}
index={nodeIndex}
baseUrl={baseUrl}
history={history}
isParent={true}
/>
</Box>
<Box
pl="2"
pr="2"
width="100%"
maxWidth="616px"
alignItems="center">
<Col bg="washedGray" width="100%" alignItems="center" p="3">
<Text textAlign="center" width="100%">
No one has posted any replies yet.
</Text>
</Col>
</Box>
</Col>
);
}
return (
<Box height="calc(100% - 48px)" width="100%" alignItems="center" pl="1" pt="3">
<PostFeed
key={locationUrl}
graphResource={graphResource}
graph={graph}
parentNode={node}
pendingSize={pendingSize}
association={association}
groups={groups}
contacts={contacts}
api={api}
history={history}
baseUrl={baseUrl}
/>
</Box>
);
}

View File

@ -3,97 +3,99 @@ import { Text, Col, Box } from '@tlon/indigo-react'
import { PostInput } from './PostInput';
import { PostFeed } from './PostFeed';
import { Loading } from '~/views/components/Loading';
import { resourceFromPath } from '~/logic/lib/group';
export function PostTimeline(props) {
const {
baseUrl,
api,
history,
association,
groups,
contacts,
graphPath,
graphs,
pendingSize,
graphResource
} = props;
const graphId = `${graphResource.ship.slice(1)}/${graphResource.name}`;
const shouldRenderFeed = graphId in graphs;
if (!shouldRenderFeed) {
return (
<Box height="100%" pt="3" pb="3" width="100%" alignItems="center" pl="1">
<Loading />
</Box>
);
export default class PostTimeline extends React.PureComponent {
constructor(props) {
super(props);
this.whyDidYouRender = true;
}
const graph = graphs[graphId];
const first = graph.peekLargest()?.[0];
if (!first) {
return (
<Col
key={0}
width="100%"
height="100%"
alignItems="center">
render() {
const {
baseUrl,
api,
history,
association,
graphPath,
graph,
pendingSize,
} = this.props;
const graphResource = resourceFromPath(graphPath);
const shouldRenderFeed = !!graph;
if (!shouldRenderFeed) {
return (
<Box height="100%" pt="3" pb="3" width="100%" alignItems="center" pl="1">
<Loading />
</Box>
);
}
const first = graph.peekLargest()?.[0];
if (!first) {
return (
<Col
key={0}
width="100%"
height="100%"
alignItems="center">
<Col
width="100%"
maxWidth="616px"
pt="3"
pl="2"
pr="2"
mb="3"
alignItems="center">
<PostInput
api={api}
graphPath={graphPath} />
</Col>
<Box
pl="2"
pr="2"
width="100%"
maxWidth="616px"
alignItems="center">
<Col bg="washedGray" width="100%" alignItems="center" p="3">
<Text textAlign="center" width="100%">
No one has posted anything here yet.
</Text>
</Col>
</Box>
</Col>
);
}
return (
<>
<Box
width="100%"
maxWidth="616px"
pt="3"
pl="2"
pr="2"
mb="3"
flexDirection="column"
alignItems="center">
<PostInput
<PostInput api={api} graphPath={graphPath} />
</Box>
<Box height="calc(100% - 176px)" width="100%" alignItems="center" pl="1">
<PostFeed
key={graphPath}
graphPath={graphPath}
graph={graph}
pendingSize={pendingSize}
association={association}
api={api}
graphResource={graphResource} />
</Col>
<Box
pl="2"
pr="2"
width="100%"
maxWidth="616px"
alignItems="center">
<Col bg="washedGray" width="100%" alignItems="center" p="3">
<Text textAlign="center" width="100%">
No one has posted anything here yet.
</Text>
</Col>
history={history}
baseUrl={baseUrl}
/>
</Box>
</Col>
</>
);
}
return (
<>
<Box
width="100%"
maxWidth="616px"
pt="3"
pl="2"
pr="2"
mb="3"
flexDirection="column"
alignItems="center">
<PostInput api={api} graphResource={graphResource} />
</Box>
<Box height="calc(100% - 176px)" width="100%" alignItems="center" pl="1">
<PostFeed
key={graphPath}
graphResource={graphResource}
graph={graphs[graphId]}
pendingSize={pendingSize}
association={association}
groups={groups}
contacts={contacts}
api={api}
history={history}
baseUrl={baseUrl}
/>
</Box>
</>
);
}

View File

@ -21,6 +21,7 @@ import useGraphState from '~/logic/state/graph';
import useHarkState, { withHarkState } from '~/logic/state/hark';
import withState from '~/logic/lib/withState';
type LandscapeProps = StoreState & {
ship: PatpNoSig;
api: GlobalApi;
@ -67,95 +68,85 @@ export function DMRedirect(props: LandscapeProps & RouteComponentProps & { ship:
);
}
class Landscape extends Component<LandscapeProps, Record<string, never>> {
componentDidMount(): void {
this.props.subscription.startApp('groups');
this.props.subscription.startApp('graph');
}
export default function Landscape(props) {
const notificationsCount = useHarkState(s => s.notificationsCount);
render(): ReactElement {
const { props } = this;
return (
<>
<Helmet defer={false}>
<title>{ props.notificationsCount ? `(${String(props.notificationsCount) }) `: '' }Landscape</title>
</Helmet>
<Switch>
<Route path="/~landscape/ship/:host/:name"
render={(routeProps) => {
const {
host,
name
} = routeProps.match.params as Record<string, string>;
const groupPath = `/ship/${host}/${name}`;
const baseUrl = `/~landscape${groupPath}`;
const ws: Workspace = { type: 'group', group: groupPath };
return (
<GroupsPane workspace={ws} baseUrl={baseUrl} {...props} />
);
}}
/>
<Route path="/~landscape/home"
render={() => {
const ws: Workspace = { type: 'home' };
return (
<GroupsPane workspace={ws} baseUrl="/~landscape/home" {...props} />
);
}}
/>
<Route path="/~landscape/messages"
render={() => {
const ws: Workspace = { type: 'messages' };
return (
<GroupsPane workspace={ws} baseUrl="/~landscape/messages" {...props} />
);
}}
/>
<Route path="/~landscape/new"
render={(routeProps) => {
return (
<Body>
<Box maxWidth="300px">
<NewGroup
api={props.api}
{...routeProps}
/>
</Box>
</Body>
);
}}
/>
<Route path='/~landscape/dm/:ship?'
return (
<>
<Helmet defer={false}>
<title>{ props.notificationsCount ? `(${String(props.notificationsCount) }) `: '' }Landscape</title>
</Helmet>
<Switch>
<Route path="/~landscape/ship/:host/:name"
render={(routeProps) => {
const { ship } = routeProps.match.params;
return <DMRedirect {...routeProps} {...props} ship={ship} />;
const {
host,
name
} = routeProps.match.params as Record<string, string>;
const groupPath = `/ship/${host}/${name}`;
const baseUrl = `/~landscape${groupPath}`;
const ws: Workspace = { type: 'group', group: groupPath };
return (
<GroupsPane workspace={ws} baseUrl={baseUrl} {...props} />
);
}}
/>
<Route path="/~landscape/join/:ship?/:name?"
render={(routeProps) => {
const { ship, name } = routeProps.match.params;
const autojoin = ship && name ? `${ship}/${name}` : undefined;
return (
<Body>
<Box maxWidth="300px">
<JoinGroup
api={props.api}
autojoin={autojoin}
{...routeProps}
/>
</Box>
</Body>
);
}}
/>
</Switch>
</>
);
}
/>
<Route path="/~landscape/home"
render={() => {
const ws: Workspace = { type: 'home' };
return (
<GroupsPane workspace={ws} baseUrl="/~landscape/home" {...props} />
);
}}
/>
<Route path="/~landscape/messages"
render={() => {
const ws: Workspace = { type: 'messages' };
return (
<GroupsPane workspace={ws} baseUrl="/~landscape/messages" {...props} />
);
}}
/>
<Route path="/~landscape/new"
render={(routeProps) => {
return (
<Body>
<Box maxWidth="300px">
<NewGroup
api={props.api}
{...routeProps}
/>
</Box>
</Body>
);
}}
/>
<Route path='/~landscape/dm/:ship?'
render={(routeProps) => {
const { ship } = routeProps.match.params;
return <DMRedirect {...routeProps} {...props} ship={ship} />;
}}
/>
<Route path="/~landscape/join/:ship?/:name?"
render={(routeProps) => {
const { ship, name } = routeProps.match.params;
const autojoin = ship && name ? `${ship}/${name}` : undefined;
return (
<Body>
<Box maxWidth="300px">
<JoinGroup
api={props.api}
autojoin={autojoin}
{...routeProps}
/>
</Box>
</Body>
);
}}
/>
</Switch>
</>
);
}
export default withState(Landscape, [
[useHarkState, ['notificationsCount']]
]);