mirror of
https://github.com/ilyakooo0/urbit.git
synced 2024-09-20 23:18:00 +03:00
Merge pull request #4331 from urbit/lf/hark-lazy-scroll
notifications: lazy load correctly
This commit is contained in:
commit
8f3afbd0ef
BIN
pkg/interface/package-lock.json
generated
BIN
pkg/interface/package-lock.json
generated
Binary file not shown.
@ -196,10 +196,11 @@ export class HarkApi extends BaseApi<StoreState> {
|
||||
});
|
||||
}
|
||||
|
||||
getMore() {
|
||||
async getMore(): Promise<boolean> {
|
||||
const offset = this.store.state['notifications']?.size || 0;
|
||||
const count = 3;
|
||||
return this.getSubset(offset, count, false);
|
||||
await this.getSubset(offset, count, false);
|
||||
return offset === (this.store.state.notifications?.size || 0);
|
||||
}
|
||||
|
||||
async getSubset(offset:number, count:number, isArchive: boolean) {
|
||||
|
51
pkg/interface/src/logic/lib/useLazyScroll.ts
Normal file
51
pkg/interface/src/logic/lib/useLazyScroll.ts
Normal file
@ -0,0 +1,51 @@
|
||||
import { useEffect, RefObject, useRef, useState } from "react";
|
||||
import _ from "lodash";
|
||||
|
||||
export function distanceToBottom(el: HTMLElement) {
|
||||
const { scrollTop, scrollHeight, clientHeight } = el;
|
||||
const scrolledPercent =
|
||||
(scrollHeight - scrollTop - clientHeight) / scrollHeight;
|
||||
return _.isNaN(scrolledPercent) ? 0 : scrolledPercent;
|
||||
}
|
||||
|
||||
export function useLazyScroll(
|
||||
ref: RefObject<HTMLElement>,
|
||||
margin: number,
|
||||
loadMore: () => Promise<boolean>
|
||||
) {
|
||||
const [isDone, setIsDone] = useState(false);
|
||||
useEffect(() => {
|
||||
if (!ref.current) {
|
||||
return;
|
||||
}
|
||||
setIsDone(false);
|
||||
const scroll = ref.current;
|
||||
const loadUntil = (el: HTMLElement) => {
|
||||
if (!isDone && distanceToBottom(el) < margin) {
|
||||
return loadMore().then((done) => {
|
||||
if (done) {
|
||||
setIsDone(true);
|
||||
return Promise.resolve();
|
||||
}
|
||||
return loadUntil(el);
|
||||
});
|
||||
}
|
||||
return Promise.resolve();
|
||||
};
|
||||
|
||||
loadUntil(scroll);
|
||||
|
||||
const onScroll = (e: Event) => {
|
||||
const el = e.currentTarget! as HTMLElement;
|
||||
loadUntil(el);
|
||||
};
|
||||
|
||||
ref.current.addEventListener("scroll", onScroll);
|
||||
|
||||
return () => {
|
||||
ref.current?.removeEventListener("scroll", onScroll);
|
||||
};
|
||||
}, [ref?.current]);
|
||||
|
||||
return isDone;
|
||||
}
|
@ -1,4 +1,5 @@
|
||||
import { Serial, PatpNoSig, Path } from './noun';
|
||||
import {Resource} from './group-update';
|
||||
|
||||
export type InviteUpdate =
|
||||
InviteUpdateInitial
|
||||
@ -60,8 +61,8 @@ export type AppInvites = {
|
||||
|
||||
export interface Invite {
|
||||
app: string;
|
||||
path: Path;
|
||||
recipeint: PatpNoSig;
|
||||
recipient: PatpNoSig;
|
||||
resource: Resource;
|
||||
ship: PatpNoSig;
|
||||
text: string;
|
||||
}
|
||||
|
@ -1,18 +1,16 @@
|
||||
import React, { useEffect, useCallback } from "react";
|
||||
import React, { useEffect, useCallback, useRef, useState } from "react";
|
||||
import f from "lodash/fp";
|
||||
import _ from "lodash";
|
||||
import { Icon, Col, Row, Box, Text, Anchor, Rule } from "@tlon/indigo-react";
|
||||
import { Icon, Col, Row, Box, Text, Anchor, Rule, Center } from "@tlon/indigo-react";
|
||||
import moment from "moment";
|
||||
import { Notifications, Rolodex, Timebox, IndexedNotification, Groups } from "~/types";
|
||||
import { Notifications, Rolodex, Timebox, IndexedNotification, Groups, GroupNotificationsConfig, NotificationGraphConfig } from "~/types";
|
||||
import { MOMENT_CALENDAR_DATE, daToUnix, resourceAsPath } from "~/logic/lib/util";
|
||||
import { BigInteger } from "big-integer";
|
||||
import GlobalApi from "~/logic/api/global";
|
||||
import { Notification } from "./notification";
|
||||
import { Associations } from "~/types";
|
||||
import { cite } from '~/logic/lib/util';
|
||||
import { InviteItem } from '~/views/components/Invite';
|
||||
import { useWaitForProps } from "~/logic/lib/useWaitForProps";
|
||||
import { useHistory } from "react-router-dom";
|
||||
import {Invites} from "./invites";
|
||||
import {useLazyScroll} from "~/logic/lib/useLazyScroll";
|
||||
|
||||
type DatedTimebox = [BigInteger, Timebox];
|
||||
|
||||
@ -45,10 +43,10 @@ export default function Inbox(props: {
|
||||
contacts: Rolodex;
|
||||
filter: string[];
|
||||
invites: any;
|
||||
notificationsGroupConfig: GroupNotificationsConfig;
|
||||
notificationsGraphConfig: NotificationGraphConfig;
|
||||
}) {
|
||||
const { api, associations, invites } = props;
|
||||
const waiter = useWaitForProps(props)
|
||||
const history = useHistory();
|
||||
useEffect(() => {
|
||||
let seen = false;
|
||||
setTimeout(() => {
|
||||
@ -75,12 +73,12 @@ export default function Inbox(props: {
|
||||
};
|
||||
|
||||
let notificationsByDay = f.flow(
|
||||
f.map<DatedTimebox>(([date, nots]) => [
|
||||
f.map<DatedTimebox, DatedTimebox>(([date, nots]) => [
|
||||
date,
|
||||
nots.filter(filterNotification(associations, props.filter)),
|
||||
]),
|
||||
f.groupBy<DatedTimebox>(([date]) => {
|
||||
date = moment(daToUnix(date));
|
||||
f.groupBy<DatedTimebox>(([d]) => {
|
||||
const date = moment(daToUnix(d));
|
||||
if (moment().subtract(6, 'hours').isBefore(date)) {
|
||||
return 'latest';
|
||||
} else {
|
||||
@ -88,69 +86,27 @@ export default function Inbox(props: {
|
||||
}
|
||||
}),
|
||||
)(notifications);
|
||||
notificationsByDay = new Map(Object.keys(notificationsByDay).sort().reverse().map(timebox => {
|
||||
return [timebox, notificationsByDay[timebox]];
|
||||
}));
|
||||
|
||||
useEffect(() => {
|
||||
api.hark.getMore(props.showArchive);
|
||||
}, [props.showArchive]);
|
||||
const notificationsByDayMap = new Map<string, DatedTimebox[]>(
|
||||
Object.keys(notificationsByDay).map(timebox => {
|
||||
return [timebox, notificationsByDay[timebox]];
|
||||
})
|
||||
);
|
||||
|
||||
const onScroll = useCallback((e) => {
|
||||
let container = e.target;
|
||||
const { scrollHeight, scrollTop, clientHeight } = container;
|
||||
if((scrollHeight - scrollTop) < 1.5 * clientHeight) {
|
||||
api.hark.getMore(props.showArchive);
|
||||
}
|
||||
}, [props.showArchive]);
|
||||
const scrollRef = useRef(null);
|
||||
|
||||
const acceptInvite = (app: string, uid: string) => async (invite) => {
|
||||
const resource = {
|
||||
ship: `~${invite.resource.ship}`,
|
||||
name: invite.resource.name
|
||||
};
|
||||
const loadMore = useCallback(async () => {
|
||||
return api.hark.getMore();
|
||||
}, [api]);
|
||||
|
||||
const resourcePath = resourceAsPath(invite.resource);
|
||||
if(app === 'contacts') {
|
||||
await api.contacts.join(resource);
|
||||
await waiter(p => resourcePath in p.associations?.contacts);
|
||||
await api.invite.accept(app, uid);
|
||||
history.push(`/~landscape${resourcePath}`);
|
||||
} else if ( app === 'chat') {
|
||||
await api.invite.accept(app, uid);
|
||||
history.push(`/~landscape/home/resource/chat${resourcePath.slice(5)}`);
|
||||
} else if ( app === 'graph') {
|
||||
await api.invite.accept(app, uid);
|
||||
history.push(`/~graph/join${resourcePath}`);
|
||||
}
|
||||
};
|
||||
const loadedAll = useLazyScroll(scrollRef, 0.2, loadMore);
|
||||
|
||||
const inviteItems = (invites, api) => {
|
||||
const returned = [];
|
||||
Object.keys(invites).map((appKey) => {
|
||||
const app = invites[appKey];
|
||||
Object.keys(app).map((uid) => {
|
||||
const invite = app[uid];
|
||||
const inviteItem =
|
||||
<InviteItem
|
||||
key={uid}
|
||||
invite={invite}
|
||||
onAccept={acceptInvite(appKey, uid)}
|
||||
onDecline={() => api.invite.decline(appKey, uid)}
|
||||
/>;
|
||||
returned.push(inviteItem);
|
||||
});
|
||||
});
|
||||
return returned;
|
||||
};
|
||||
|
||||
return (
|
||||
<Col position="relative" height="100%" overflowY="auto" onScroll={onScroll} >
|
||||
<Col zIndex={4} gapY={2} bg="white" top="0px" position="sticky" flexShrink={0}>
|
||||
{inviteItems(invites, api)}
|
||||
</Col>
|
||||
{[...notificationsByDay.keys()].map((day, index) => {
|
||||
const timeboxes = notificationsByDay.get(day);
|
||||
<Col ref={scrollRef} position="relative" height="100%" overflowY="auto">
|
||||
<Invites invites={invites} api={api} associations={associations} />
|
||||
{[...notificationsByDayMap.keys()].sort().reverse().map((day, index) => {
|
||||
const timeboxes = notificationsByDayMap.get(day)!;
|
||||
return timeboxes.length > 0 && (
|
||||
<DaySection
|
||||
key={day}
|
||||
@ -163,10 +119,14 @@ export default function Inbox(props: {
|
||||
groups={props.groups}
|
||||
graphConfig={props.notificationsGraphConfig}
|
||||
groupConfig={props.notificationsGroupConfig}
|
||||
chatConfig={props.notificationsChatConfig}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
{loadedAll && (
|
||||
<Center mt="2" borderTop={notifications.length !== 0 ? 1 : 0} borderTopColor="washedGray" width="100%" height="96px">
|
||||
<Text gray fontSize="1">No more notifications</Text>
|
||||
</Center>
|
||||
)}
|
||||
</Col>
|
||||
);
|
||||
}
|
||||
@ -192,7 +152,6 @@ function DaySection({
|
||||
api,
|
||||
groupConfig,
|
||||
graphConfig,
|
||||
chatConfig,
|
||||
}) {
|
||||
|
||||
const lent = timeboxes.map(([,nots]) => nots.length).reduce(f.add, 0);
|
||||
@ -202,23 +161,22 @@ function DaySection({
|
||||
|
||||
return (
|
||||
<>
|
||||
<Box position="sticky" zIndex="3" top="-1px" bg="white">
|
||||
<Box position="sticky" zIndex={3} top="-1px" bg="white">
|
||||
<Box p="2" bg="scales.black05">
|
||||
<Text>
|
||||
{label}
|
||||
</Text>
|
||||
</Box>
|
||||
</Box>
|
||||
{_.map(timeboxes.sort(sortTimeboxes), ([date, nots], i) =>
|
||||
{_.map(timeboxes.sort(sortTimeboxes), ([date, nots], i: number) =>
|
||||
_.map(nots.sort(sortIndexedNotification), (not, j: number) => (
|
||||
<React.Fragment key={j}>
|
||||
{(i !== 0 || j !== 0) && (
|
||||
<Box flexShrink="0" height="4px" bg="scales.black05" />
|
||||
<Box flexShrink={0} height="4px" bg="scales.black05" />
|
||||
)}
|
||||
<Notification
|
||||
graphConfig={graphConfig}
|
||||
groupConfig={groupConfig}
|
||||
chatConfig={chatConfig}
|
||||
api={api}
|
||||
associations={associations}
|
||||
notification={not}
|
||||
|
74
pkg/interface/src/views/apps/notifications/invites.tsx
Normal file
74
pkg/interface/src/views/apps/notifications/invites.tsx
Normal file
@ -0,0 +1,74 @@
|
||||
import React, { useCallback } from "react";
|
||||
import { Box, Row, Col } from "@tlon/indigo-react";
|
||||
import GlobalApi from "~/logic/api/global";
|
||||
import { Invites as IInvites, Associations, Invite } from "~/types";
|
||||
import { resourceAsPath } from "~/logic/lib/util";
|
||||
import { useHistory } from "react-router-dom";
|
||||
import { useWaitForProps } from "~/logic/lib/useWaitForProps";
|
||||
import InviteItem from "~/views/components/Invite";
|
||||
|
||||
interface InvitesProps {
|
||||
api: GlobalApi;
|
||||
invites: IInvites;
|
||||
associations: Associations;
|
||||
}
|
||||
|
||||
export function Invites(props: InvitesProps) {
|
||||
const { api, invites } = props;
|
||||
const history = useHistory();
|
||||
const waiter = useWaitForProps(props);
|
||||
|
||||
const acceptInvite = (
|
||||
app: string,
|
||||
uid: string,
|
||||
invite: Invite
|
||||
) => async () => {
|
||||
const resource = {
|
||||
ship: `~${invite.resource.ship}`,
|
||||
name: invite.resource.name,
|
||||
};
|
||||
|
||||
const resourcePath = resourceAsPath(invite.resource);
|
||||
if (app === "contacts") {
|
||||
await api.contacts.join(resource);
|
||||
await waiter((p) => resourcePath in p.associations?.contacts);
|
||||
await api.invite.accept(app, uid);
|
||||
history.push(`/~landscape${resourcePath}`);
|
||||
} else if (app === "graph") {
|
||||
await api.invite.accept(app, uid);
|
||||
history.push(`/~graph/join${resourcePath}`);
|
||||
}
|
||||
};
|
||||
|
||||
const declineInvite = useCallback(
|
||||
(app: string, uid: string) => () => api.invite.decline(app, uid),
|
||||
[api]
|
||||
);
|
||||
|
||||
return (
|
||||
<Col
|
||||
zIndex={4}
|
||||
gapY={2}
|
||||
bg="white"
|
||||
top="0px"
|
||||
position="sticky"
|
||||
flexShrink={0}
|
||||
>
|
||||
{Object.keys(invites).reduce((items, appKey) => {
|
||||
const app = invites[appKey];
|
||||
let appItems = Object.keys(app).map((uid) => {
|
||||
const invite = app[uid];
|
||||
return (
|
||||
<InviteItem
|
||||
key={uid}
|
||||
invite={invite}
|
||||
onAccept={acceptInvite(appKey, uid, invite)}
|
||||
onDecline={declineInvite(appKey, uid)}
|
||||
/>
|
||||
);
|
||||
});
|
||||
return [...items, ...appItems];
|
||||
}, [] as JSX.Element[])}
|
||||
</Col>
|
||||
);
|
||||
}
|
@ -29,7 +29,6 @@ interface NotificationProps {
|
||||
contacts: Contacts;
|
||||
graphConfig: NotificationGraphConfig;
|
||||
groupConfig: GroupNotificationsConfig;
|
||||
chatConfig: string[];
|
||||
}
|
||||
|
||||
function getMuted(
|
||||
|
Loading…
Reference in New Issue
Block a user