Merge pull request #4673 from urbit/la/feed-perf

group-feed: improve performance and make mobile load
This commit is contained in:
matildepark 2021-03-29 15:08:53 -04:00 committed by GitHub
commit dd563ac26a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
15 changed files with 213 additions and 215 deletions

View File

@ -3,29 +3,9 @@ import { StoreState } from '../store/type';
import { Path } from '@urbit/api';
import _ from 'lodash';
/**
* Path to subscribe on and app to subscribe to
*/
type AppSubscription = [Path, string];
const groupSubscriptions: AppSubscription[] = [
];
const graphSubscriptions: AppSubscription[] = [
['/updates', 'graph-store']
];
type AppName = 'groups' | 'graph';
const appSubscriptions: Record<AppName, AppSubscription[]> = {
groups: groupSubscriptions,
graph: graphSubscriptions
};
export default class GlobalSubscription extends BaseSubscription<StoreState> {
openSubscriptions: Record<AppName, number[]> = {
groups: [],
graph: []
};
openSubscriptions: any = {};
start() {
this.subscribe('/all', 'metadata-store');
@ -35,7 +15,6 @@ export default class GlobalSubscription extends BaseSubscription<StoreState> {
this.subscribe('/groups', 'group-store');
this.clearQueue();
// TODO: update to get /updates
this.subscribe('/all', 'contact-store');
this.subscribe('/all', 's3-store');
this.subscribe('/keys', 'graph-store');
@ -45,29 +24,37 @@ export default class GlobalSubscription extends BaseSubscription<StoreState> {
this.subscribe('/all', 'settings-store');
this.subscribe('/all', 'group-view');
this.subscribe('/nacks', 'contact-pull-hook');
this.clearQueue();
this.subscribe('/updates', 'graph-store');
}
subscribe(path: Path, app: string) {
if (`${app}${path}` in this.openSubscriptions) {
return;
}
const id = super.subscribe(path, app);
this.openSubscriptions[`${app}${path}`] = { app, path, id };
}
unsubscribe(id) {
for (let key in Object.keys(this.openSubscriptions)) {
let val = this.openSubscriptions[key];
if (id === val.id) {
delete this.openSubscriptions[`${val.app}${val.path}`];
super.unsubscribe(id);
}
}
}
restart() {
super.restart();
_.mapValues(this.openSubscriptions, (subs, app: AppName) => {
if(subs.length > 0) {
this.stopApp(app);
this.startApp(app);
}
});
}
for (let key in Object.keys(this.openSubscriptions)) {
let val = this.openSubscriptions[key];
startApp(app: AppName) {
if(this.openSubscriptions[app].length > 0) {
console.log(`${app} already started`);
return;
unsubscribe(val.id);
}
this.openSubscriptions[app] =
appSubscriptions[app].map(([path, agent]) => this.subscribe(path, agent));
}
stopApp(app: AppName) {
this.openSubscriptions[app].map(id => this.unsubscribe(id));
this.openSubscriptions[app] = [];
this.start();
}
}

View File

@ -44,7 +44,7 @@ registerRoute(
// Check to see if the request's destination is style for stylesheets, script for JavaScript, or worker for web worker
({ request }) =>
request.destination === 'style' ||
request.destination === 'script' ||
// request.destination === 'script' ||
request.destination === 'worker',
// Use a Stale While Revalidate caching strategy
new StaleWhileRevalidate({

View File

@ -196,7 +196,10 @@ export function GroupsPane(props: GroupsPaneProps) {
return (
<>
<Helmet defer={false}>
<title>{notificationsCount ? `(${String(notificationsCount)}) ` : ''}{ title }</title>
<title>
{notificationsCount ? `(${String(notificationsCount)}) ` : ''}
{ title }
</title>
</Helmet>
<Skeleton
mobileHide={shouldHideSidebar}
@ -204,14 +207,9 @@ export function GroupsPane(props: GroupsPaneProps) {
baseUrl={baseUrl}
{...props}>
<GroupHome
{...routeProps}
api={api}
baseUrl={baseUrl}
associations={associations}
groups={groups}
groupPath={groupPath}
contacts={contacts}
workspace={workspace}
/>
{popovers(routeProps, baseUrl)}
</Skeleton>

View File

@ -10,6 +10,8 @@ import { AsyncButton } from "~/views/components/AsyncButton";
import GlobalApi from "~/logic/api/global";
import { resourceFromPath, Tag, resourceAsPath } from "@urbit/api";
import useGroupState, { useGroup } from "~/logic/state/group";
import { useHistory } from 'react-router-dom';
interface FormSchema {
permissions: GroupFeedPermissions;
@ -20,7 +22,13 @@ export function EnableGroupFeed(props: {
dismiss: () => void;
api: GlobalApi;
}) {
const { api, groupPath, dismiss } = props;
const { api, groupPath, baseUrl } = props;
const history = useHistory();
const dismiss = () => {
history.push(baseUrl);
};
const initialValues: FormSchema = {
permissions: "everyone",
};
@ -44,7 +52,7 @@ export function EnableGroupFeed(props: {
actions.setStatus({ success: null });
dismiss();
},
[groupPath, dismiss]
[groupPath, baseUrl]
);
return (

View File

@ -6,21 +6,26 @@ 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 { useHistory } from 'react-router-dom';
import PostTimeline from './Post/PostTimeline';
import PostReplies from './Post/PostReplies';
import useMetadataState from '~/logic/state/metadata';
export function GroupFeed(props) {
function GroupFeed(props) {
const {
baseUrl,
api,
history,
associations,
graphPath
} = props;
const associations = useMetadataState(state => state.associations);
const graphs = useGraphState(state => state.graphs);
const graphResource = resourceFromPath(graphPath);
const graphTimesentMap = useGraphState(state => state.graphTimesentMap);
const pendingSize = Object.keys(
graphTimesentMap[`${graphResource.ship.slice(1)}/${graphResource.name}`] ||
{}
@ -29,6 +34,12 @@ export function GroupFeed(props) {
const relativePath = (path) => baseUrl + path;
const association = associations.graph[graphPath];
const history = useHistory();
const locationUrl = history.location.pathname;
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 +64,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 +78,14 @@ export function GroupFeed(props) {
render={(routeProps) => {
return (
<PostReplies
{...props}
locationUrl={locationUrl}
baseUrl={baseUrl}
api={api}
history={history}
graphPath={graphPath}
association={association}
graphs={graphs}
pendingSize={pendingSize}
graphResource={graphResource} />
graph={graph}
pendingSize={pendingSize} />
);
}} />
</Switch>
@ -77,3 +93,6 @@ export function GroupFeed(props) {
);
}
GroupFeed.whyDidYouRender = true;
export { GroupFeed };

View File

@ -5,20 +5,22 @@ import { EnableGroupFeed } from './EnableGroupFeed';
import { EmptyGroupHome } from './EmptyGroupHome';
import { GroupFeed } from './GroupFeed';
import { AddFeedBanner } from './AddFeedBanner';
import {Route, useHistory} from 'react-router-dom';
import { Route } from 'react-router-dom';
import useGroupState from '~/logic/state/group';
import useMetadataState from '~/logic/state/metadata';
export function GroupHome(props) {
function GroupHome(props) {
const {
associations,
api,
groupPath,
groups,
graphs,
baseUrl,
contacts,
baseUrl
} = props;
const associations = useMetadataState(state => state.associations);
const groups = useGroupState(state => state.groups);
const metadata = associations?.groups[groupPath]?.metadata;
const askFeedBanner =
metadata &&
@ -33,7 +35,6 @@ export function GroupHome(props) {
'resource' in metadata.config.group;
const graphPath = metadata?.config?.group?.resource;
const history = useHistory();
return (
<Box width="100%" height="100%" overflow="hidden">
@ -42,7 +43,7 @@ export function GroupHome(props) {
return (
<EnableGroupFeed
groupPath={groupPath}
dismiss={() => history.push(baseUrl)}
baseUrl={baseUrl}
api={api}
/>
);
@ -57,13 +58,8 @@ export function GroupHome(props) {
) : null }
{ isFeedEnabled ? (
<GroupFeed
associations={associations}
groups={groups}
contacts={contacts}
graphPath={graphPath}
graphs={graphs}
api={api}
history={history}
baseUrl={baseUrl} />
) : (
<EmptyGroupHome {...props} />
@ -71,3 +67,5 @@ export function GroupHome(props) {
</Box>
);
}
export { GroupHome };

View File

@ -1,15 +1,15 @@
import React from 'react';
import bigInt from 'big-integer';
import VirtualScroller from "~/views/components/VirtualScroller";
import PostItem from './PostItem';
import PostItem from './PostItem/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

@ -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

@ -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

@ -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

@ -3,10 +3,11 @@ import { Box, Col, Row, Text } from '@tlon/indigo-react';
import { PostHeader } from './PostHeader';
import { PostContent } from './PostContent';
import { PostFooter } from './PostFooter';
import { PostInput } from './PostInput';
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 = '';
@ -85,7 +86,6 @@ class PostItem extends React.Component {
{...bind}>
<PostHeader
post={node.post}
contacts={contacts}
api={api}
association={association}
isReply={isReply} />
@ -98,8 +98,7 @@ class PostItem extends React.Component {
<PostContent
post={node.post}
isParent={isParent}
api={api}
contacts={contacts} />
api={api} />
<PostFooter
replyCount={node.children.size}
toggleReplyMode={this.toggleReplyMode} />
@ -113,7 +112,7 @@ class PostItem extends React.Component {
borderLeftColor="lightGray"></Box>
<PostInput
api={api}
graphResource={graphResource}
graphPath={graphPath}
index={indexString}
submitCallback={this.submitCallback} />
</Col>

View File

@ -2,26 +2,28 @@ import React from 'react';
import bigInt from 'big-integer';
import { Text, Col, Box } from '@tlon/indigo-react'
import { PostInput } from './PostInput';
import PostItem from './PostItem';
import PostItem from './PostItem/PostItem';
import { PostFeed } from './PostFeed';
import { Loading } from '~/views/components/Loading';
import { resourceFromPath } from '~/logic/lib/group';
export function PostReplies(props) {
export default function PostReplies(props) {
const {
baseUrl,
api,
history,
association,
groups,
contacts,
graphPath,
graphs,
pendingSize,
graphResource
pendingSize
} = props;
const graphResource = resourceFromPath(graphPath);
const graphId = `${graphResource.ship.slice(1)}/${graphResource.name}`;
const shouldRenderFeed = graphId in graphs;
let graph = props.graph;
const shouldRenderFeed = !!graph;
if (!shouldRenderFeed) {
return (
@ -32,13 +34,12 @@ export function PostReplies(props) {
}
const locationUrl =
history.location.pathname.replace(`${baseUrl}/feed`, '');
props.locationUrl.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;
@ -58,7 +59,7 @@ export function PostReplies(props) {
if (!first) {
return (
<Col
key={0}
key={locationUrl}
width="100%"
height="100%"
alignItems="center" overflowY="scroll">
@ -66,8 +67,7 @@ export function PostReplies(props) {
<PostItem
key={node.post.index}
node={node}
contacts={contacts}
graphResource={graphResource}
graphPath={graphPath}
association={association}
api={api}
index={nodeIndex}
@ -96,13 +96,12 @@ export function PostReplies(props) {
<Box height="calc(100% - 48px)" width="100%" alignItems="center" pl="1" pt="3">
<PostFeed
key={locationUrl}
graphResource={graphResource}
graphPath={graphPath}
graph={graph}
parentNode={node}
pendingSize={pendingSize}
association={association}
groups={groups}
contacts={contacts}
api={api}
history={history}
baseUrl={baseUrl}

View File

@ -3,23 +3,21 @@ 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) {
export default function PostTimeline(props) {
const {
baseUrl,
api,
history,
association,
groups,
contacts,
graphPath,
graphs,
graph,
pendingSize,
graphResource
} = props;
const graphId = `${graphResource.ship.slice(1)}/${graphResource.name}`;
const shouldRenderFeed = graphId in graphs;
const graphResource = resourceFromPath(graphPath);
const shouldRenderFeed = !!graph;
if (!shouldRenderFeed) {
return (
@ -29,7 +27,6 @@ export function PostTimeline(props) {
);
}
const graph = graphs[graphId];
const first = graph.peekLargest()?.[0];
if (!first) {
return (
@ -48,7 +45,7 @@ export function PostTimeline(props) {
alignItems="center">
<PostInput
api={api}
graphResource={graphResource} />
graphPath={graphPath} />
</Col>
<Box
pl="2"
@ -77,17 +74,15 @@ export function PostTimeline(props) {
mb="3"
flexDirection="column"
alignItems="center">
<PostInput api={api} graphResource={graphResource} />
<PostInput api={api} graphPath={graphPath} />
</Box>
<Box height="calc(100% - 176px)" width="100%" alignItems="center" pl="1">
<PostFeed
key={graphPath}
graphResource={graphResource}
graph={graphs[graphId]}
graphPath={graphPath}
graph={graph}
pendingSize={pendingSize}
association={association}
groups={groups}
contacts={contacts}
api={api}
history={history}
baseUrl={baseUrl}

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']]
]);