mirror of
https://github.com/urbit/shrub.git
synced 2024-12-01 06:35:32 +03:00
grid: update for new hark timeboxing
This commit is contained in:
parent
1d94d08d60
commit
993b529b9d
@ -1,6 +1,6 @@
|
|||||||
import React, { useCallback, useEffect } from 'react';
|
import React, { useCallback, useEffect } from 'react';
|
||||||
import Mousetrap from 'mousetrap';
|
import Mousetrap from 'mousetrap';
|
||||||
import { BrowserRouter, Switch, Route, useHistory } from 'react-router-dom';
|
import { BrowserRouter, Switch, Route, useHistory, useLocation } from 'react-router-dom';
|
||||||
import { Grid } from './pages/Grid';
|
import { Grid } from './pages/Grid';
|
||||||
import useDocketState from './state/docket';
|
import useDocketState from './state/docket';
|
||||||
import { PermalinkRoutes } from './pages/PermalinkRoutes';
|
import { PermalinkRoutes } from './pages/PermalinkRoutes';
|
||||||
@ -9,9 +9,26 @@ import { usePreferencesStore } from './nav/preferences/usePreferencesStore';
|
|||||||
import useContactState from './state/contact';
|
import useContactState from './state/contact';
|
||||||
import api from './state/api';
|
import api from './state/api';
|
||||||
|
|
||||||
|
const getNoteRedirect = (path: string) => {
|
||||||
|
if (path.startsWith('/desk/')) {
|
||||||
|
const [, , desk] = path.split('/');
|
||||||
|
return `/app/${desk}`;
|
||||||
|
}
|
||||||
|
return '';
|
||||||
|
};
|
||||||
|
|
||||||
const AppRoutes = () => {
|
const AppRoutes = () => {
|
||||||
const { push } = useHistory();
|
const { push } = useHistory();
|
||||||
const theme = usePreferencesStore((s) => s.theme);
|
const theme = usePreferencesStore((s) => s.theme);
|
||||||
|
const { search } = useLocation();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const query = new URLSearchParams(location.search);
|
||||||
|
if (query.has('grid-note')) {
|
||||||
|
const redir = getNoteRedirect(query.get('grid-note')!);
|
||||||
|
push(redir);
|
||||||
|
}
|
||||||
|
}, [location.search]);
|
||||||
|
|
||||||
const updateThemeClass = useCallback(
|
const updateThemeClass = useCallback(
|
||||||
(e: MediaQueryListEvent) => {
|
(e: MediaQueryListEvent) => {
|
||||||
@ -36,6 +53,8 @@ const AppRoutes = () => {
|
|||||||
};
|
};
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
useEffect(() => {}, []);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
window.name = 'grid';
|
window.name = 'grid';
|
||||||
|
|
||||||
|
32
pkg/grid/src/components/DeskLink.tsx
Normal file
32
pkg/grid/src/components/DeskLink.tsx
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
import React, { ReactNode } from 'react';
|
||||||
|
import { Link } from 'react-router-dom';
|
||||||
|
import { useCharge } from '../state/docket';
|
||||||
|
import { getAppHref } from '../state/util';
|
||||||
|
|
||||||
|
interface DeskLinkProps extends React.AnchorHTMLAttributes<any> {
|
||||||
|
desk: string;
|
||||||
|
to?: string;
|
||||||
|
children?: ReactNode;
|
||||||
|
className?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function DeskLink({ children, className, desk, to = '', ...rest }: DeskLinkProps) {
|
||||||
|
const charge = useCharge(desk);
|
||||||
|
|
||||||
|
if (!charge) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
if (desk === window.desk) {
|
||||||
|
return (
|
||||||
|
<Link to={to} className={className} {...rest}>
|
||||||
|
{children}
|
||||||
|
</Link>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
const href = `${getAppHref(charge.href)}${to}`;
|
||||||
|
return (
|
||||||
|
<a href={href} target={desk} className={className} {...rest}>
|
||||||
|
{children}
|
||||||
|
</a>
|
||||||
|
);
|
||||||
|
}
|
@ -1,5 +1,5 @@
|
|||||||
import React, { useEffect } from 'react';
|
import React, { useEffect } from 'react';
|
||||||
import { Link } from 'react-router-dom';
|
import { Link, NavLink, Route, Switch } from 'react-router-dom';
|
||||||
import { Notification } from '@urbit/api';
|
import { Notification } from '@urbit/api';
|
||||||
import { useLeapStore } from './Nav';
|
import { useLeapStore } from './Nav';
|
||||||
import { Button } from '../components/Button';
|
import { Button } from '../components/Button';
|
||||||
@ -11,6 +11,7 @@ import {
|
|||||||
import { useNotifications } from '../state/notifications';
|
import { useNotifications } from '../state/notifications';
|
||||||
import { useHarkStore } from '../state/hark';
|
import { useHarkStore } from '../state/hark';
|
||||||
import { OnboardingNotification } from './notifications/OnboardingNotification';
|
import { OnboardingNotification } from './notifications/OnboardingNotification';
|
||||||
|
import { Inbox } from './notifications/Inbox';
|
||||||
|
|
||||||
function renderNotification(notification: Notification, key: string, unread = false) {
|
function renderNotification(notification: Notification, key: string, unread = false) {
|
||||||
// Special casing
|
// Special casing
|
||||||
@ -36,7 +37,7 @@ const Empty = () => (
|
|||||||
|
|
||||||
export const Notifications = () => {
|
export const Notifications = () => {
|
||||||
const select = useLeapStore((s) => s.select);
|
const select = useLeapStore((s) => s.select);
|
||||||
const { unreads, reads, hasAnyNotifications } = useNotifications();
|
const { unseen, seen, hasAnyNotifications } = useNotifications();
|
||||||
const markAllAsRead = () => {
|
const markAllAsRead = () => {
|
||||||
const { readAll } = useHarkStore.getState();
|
const { readAll } = useHarkStore.getState();
|
||||||
readAll();
|
readAll();
|
||||||
@ -46,12 +47,37 @@ export const Notifications = () => {
|
|||||||
select('Notifications');
|
select('Notifications');
|
||||||
const { getMore } = useHarkStore.getState();
|
const { getMore } = useHarkStore.getState();
|
||||||
getMore();
|
getMore();
|
||||||
|
|
||||||
|
function visibilitychange() {
|
||||||
|
useHarkStore.getState().opened();
|
||||||
|
}
|
||||||
|
document.addEventListener('visibilitychange', visibilitychange);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
document.removeEventListener('visibilitychange', visibilitychange);
|
||||||
|
useHarkStore.getState().opened();
|
||||||
|
};
|
||||||
}, []);
|
}, []);
|
||||||
// const select = useLeapStore((s) => s.select);
|
// const select = useLeapStore((s) => s.select);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="grid grid-rows-[auto,1fr] h-full p-4 md:p-8 overflow-hidden">
|
<div className="grid grid-rows-[auto,1fr] h-full p-4 md:p-8 overflow-hidden">
|
||||||
<header className="space-x-2 mb-8">
|
<header className="space-x-2 mb-8">
|
||||||
|
<NavLink
|
||||||
|
exact
|
||||||
|
activeClassName="text-black"
|
||||||
|
className="text-base font-semibold px-4"
|
||||||
|
to="/leap/notifications"
|
||||||
|
>
|
||||||
|
New
|
||||||
|
</NavLink>
|
||||||
|
<NavLink
|
||||||
|
activeClassName="text-black"
|
||||||
|
className="text-base font-semibold px-4"
|
||||||
|
to="/leap/notifications/archive"
|
||||||
|
>
|
||||||
|
Archive
|
||||||
|
</NavLink>
|
||||||
<Button onClick={markAllAsRead} variant="secondary" className="py-1.5 px-6 rounded-full">
|
<Button onClick={markAllAsRead} variant="secondary" className="py-1.5 px-6 rounded-full">
|
||||||
Mark All as Read
|
Mark All as Read
|
||||||
</Button>
|
</Button>
|
||||||
@ -64,17 +90,14 @@ export const Notifications = () => {
|
|||||||
Notification Settings
|
Notification Settings
|
||||||
</Button>
|
</Button>
|
||||||
</header>
|
</header>
|
||||||
|
<Switch>
|
||||||
{!hasAnyNotifications && <Empty />}
|
<Route path="/leap/notifications" exact>
|
||||||
{hasAnyNotifications && (
|
<Inbox />
|
||||||
<section className="text-gray-400 space-y-2 overflow-y-auto">
|
</Route>
|
||||||
{unreads.map((n, index) => renderNotification(n, index.toString(), true))}
|
<Route path="/leap/notifications/archive" exact>
|
||||||
{Array.from(reads)
|
<Inbox archived />
|
||||||
.map(([, nots]) => nots)
|
</Route>
|
||||||
.flat()
|
</Switch>
|
||||||
.map((n, index) => renderNotification(n, `reads-${index}`))}
|
|
||||||
</section>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { Notification } from '@urbit/api';
|
import { Notification, Timebox } from '@urbit/api';
|
||||||
import { Link, LinkProps } from 'react-router-dom';
|
import { Link, LinkProps } from 'react-router-dom';
|
||||||
import { Bullet } from '../components/icons/Bullet';
|
import { Bullet } from '../components/icons/Bullet';
|
||||||
import { useNotifications } from '../state/notifications';
|
import { useNotifications } from '../state/notifications';
|
||||||
@ -8,7 +8,8 @@ import { MenuState } from './Nav';
|
|||||||
|
|
||||||
type NotificationsState = 'empty' | 'unread' | 'attention-needed';
|
type NotificationsState = 'empty' | 'unread' | 'attention-needed';
|
||||||
|
|
||||||
function getNotificationsState(notifications: Notification[]): NotificationsState {
|
function getNotificationsState(box: Timebox): NotificationsState {
|
||||||
|
const notifications = Object.values(box);
|
||||||
if (notifications.filter(({ bin }) => bin.place.desk === window.desk).length > 0) {
|
if (notifications.filter(({ bin }) => bin.place.desk === window.desk).length > 0) {
|
||||||
return 'attention-needed';
|
return 'attention-needed';
|
||||||
}
|
}
|
||||||
@ -27,8 +28,8 @@ type NotificationsLinkProps = Omit<LinkProps<HTMLAnchorElement>, 'to'> & {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const NotificationsLink = ({ navOpen, menu }: NotificationsLinkProps) => {
|
export const NotificationsLink = ({ navOpen, menu }: NotificationsLinkProps) => {
|
||||||
const { unreads } = useNotifications();
|
const { unseen } = useNotifications();
|
||||||
const state = getNotificationsState(unreads);
|
const state = getNotificationsState(unseen);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Link
|
<Link
|
||||||
@ -44,7 +45,7 @@ export const NotificationsLink = ({ navOpen, menu }: NotificationsLinkProps) =>
|
|||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{state === 'empty' && <Bullet className="w-6 h-6" />}
|
{state === 'empty' && <Bullet className="w-6 h-6" />}
|
||||||
{state === 'unread' && unreads.length}
|
{state === 'unread' && Object.keys(unseen).length}
|
||||||
{state === 'attention-needed' && (
|
{state === 'attention-needed' && (
|
||||||
<span className="h2">
|
<span className="h2">
|
||||||
! <span className="sr-only">Attention needed</span>
|
! <span className="sr-only">Attention needed</span>
|
||||||
|
@ -1,25 +1,28 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import cn from 'classnames';
|
import cn from 'classnames';
|
||||||
import { Notification, harkBinToId, HarkContent } from '@urbit/api';
|
import { Notification, harkBinToId, HarkContent, HarkLid } from '@urbit/api';
|
||||||
import { map, take } from 'lodash';
|
import { map, take } from 'lodash';
|
||||||
import { useCharge } from '../../state/docket';
|
import { useCharge } from '../../state/docket';
|
||||||
import { Elbow } from '../../components/icons/Elbow';
|
import { Elbow } from '../../components/icons/Elbow';
|
||||||
import { ShipName } from '../../components/ShipName';
|
import { ShipName } from '../../components/ShipName';
|
||||||
import { getAppHref } from '../../state/util';
|
import { getAppHref } from '../../state/util';
|
||||||
|
import { Link } from 'react-router-dom';
|
||||||
|
import { DeskLink } from '../../components/DeskLink';
|
||||||
|
import {useHarkStore} from '../../state/hark';
|
||||||
|
|
||||||
interface BasicNotificationProps {
|
interface BasicNotificationProps {
|
||||||
notification: Notification;
|
notification: Notification;
|
||||||
unread?: boolean;
|
lid: HarkLid;
|
||||||
}
|
}
|
||||||
|
|
||||||
const MAX_CONTENTS = 20;
|
const MAX_CONTENTS = 5;
|
||||||
|
|
||||||
const NotificationText = ({ contents }: { contents: HarkContent[] }) => {
|
const NotificationText = ({ contents }: { contents: HarkContent[] }) => {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{contents.map((content, idx) => {
|
{contents.map((content, idx) => {
|
||||||
if ('ship' in content) {
|
if ('ship' in content) {
|
||||||
return <ShipName key={idx} name={content.ship} />;
|
return <ShipName className="color-blue" key={idx} name={content.ship} />;
|
||||||
}
|
}
|
||||||
return content.text;
|
return content.text;
|
||||||
})}
|
})}
|
||||||
@ -27,7 +30,7 @@ const NotificationText = ({ contents }: { contents: HarkContent[] }) => {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const BasicNotification = ({ notification, unread = false }: BasicNotificationProps) => {
|
export const BasicNotification = ({ notification, lid }: BasicNotificationProps) => {
|
||||||
const { desk } = notification.bin.place;
|
const { desk } = notification.bin.place;
|
||||||
const binId = harkBinToId(notification.bin);
|
const binId = harkBinToId(notification.bin);
|
||||||
const id = `notif-${notification.time}-${binId}`;
|
const id = `notif-${notification.time}-${binId}`;
|
||||||
@ -39,15 +42,19 @@ export const BasicNotification = ({ notification, unread = false }: BasicNotific
|
|||||||
}
|
}
|
||||||
const contents = map(notification.body, 'content').filter((c) => c.length > 0);
|
const contents = map(notification.body, 'content').filter((c) => c.length > 0);
|
||||||
const large = contents.length === 0;
|
const large = contents.length === 0;
|
||||||
const link = `${getAppHref(charge.href)}?grid-note=${encodeURIComponent(first.link)}`;
|
const archive = () => {
|
||||||
|
const { bin } = notification;
|
||||||
|
useHarkStore.getState().archiveNote(notification.bin, lid);
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<a
|
<DeskLink
|
||||||
href={link}
|
onClick={archive}
|
||||||
target={desk}
|
to={`?grid-note=${encodeURIComponent(first.link)}`}
|
||||||
|
desk={desk}
|
||||||
className={cn(
|
className={cn(
|
||||||
'text-black rounded',
|
'text-black rounded',
|
||||||
unread ? 'bg-blue-100' : 'bg-gray-100',
|
'unseen' in lid ? 'bg-blue-100' : 'bg-gray-100',
|
||||||
large ? 'note-grid-no-content' : 'note-grid-content'
|
large ? 'note-grid-no-content' : 'note-grid-content'
|
||||||
)}
|
)}
|
||||||
aria-labelledby={id}
|
aria-labelledby={id}
|
||||||
@ -75,6 +82,6 @@ export const BasicNotification = ({ notification, unread = false }: BasicNotific
|
|||||||
) : null}
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
) : null}
|
) : null}
|
||||||
</a>
|
</DeskLink>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
92
pkg/grid/src/nav/notifications/Inbox.tsx
Normal file
92
pkg/grid/src/nav/notifications/Inbox.tsx
Normal file
@ -0,0 +1,92 @@
|
|||||||
|
import React, { useEffect } from 'react';
|
||||||
|
import { Link } from 'react-router-dom';
|
||||||
|
import { HarkLid, Notification } from '@urbit/api';
|
||||||
|
import { useLeapStore } from '../Nav';
|
||||||
|
import { Button } from '../../components/Button';
|
||||||
|
import { BasicNotification } from './BasicNotification';
|
||||||
|
import { BaseBlockedNotification, RuntimeLagNotification } from './SystemNotification';
|
||||||
|
import { useNotifications } from '../../state/notifications';
|
||||||
|
import { useHarkStore } from '../../state/hark';
|
||||||
|
import { OnboardingNotification } from './OnboardingNotification';
|
||||||
|
|
||||||
|
function renderNotification(notification: Notification, key: string, lid: HarkLid) {
|
||||||
|
// Special casing
|
||||||
|
if (notification.bin.place.desk === window.desk) {
|
||||||
|
if (notification.bin.place.path === '/lag') {
|
||||||
|
return <RuntimeLagNotification key={key} />;
|
||||||
|
}
|
||||||
|
if (notification.bin.place.path === '/blocked') {
|
||||||
|
return <BaseBlockedNotification key={key} />;
|
||||||
|
}
|
||||||
|
if (notification.bin.place.path === '/onboard') {
|
||||||
|
return <OnboardingNotification key={key} unread={false} />;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return <BasicNotification key={key} notification={notification} lid={lid} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
const Empty = () => (
|
||||||
|
<section className="flex justify-center items-center min-h-[480px] text-gray-400 space-y-2">
|
||||||
|
<span className="h4">All clear!</span>
|
||||||
|
</section>
|
||||||
|
);
|
||||||
|
|
||||||
|
export const Inbox = ({ archived = false }) => {
|
||||||
|
const select = useLeapStore((s) => s.select);
|
||||||
|
const { unseen, seen, hasAnyNotifications } = useNotifications();
|
||||||
|
const archive = useHarkStore((s) => s.archive);
|
||||||
|
const markAllAsRead = () => {};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
useHarkStore.getState().getMore();
|
||||||
|
|
||||||
|
}, [archived]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
select('Notifications');
|
||||||
|
const { getMore } = useHarkStore.getState();
|
||||||
|
getMore();
|
||||||
|
|
||||||
|
function visibilitychange() {
|
||||||
|
setTimeout(() => useHarkStore.getState().opened(), 100);
|
||||||
|
}
|
||||||
|
document.addEventListener('visibilitychange', visibilitychange);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
document.removeEventListener('visibilitychange', visibilitychange);
|
||||||
|
visibilitychange();
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
// const select = useLeapStore((s) => s.select);
|
||||||
|
|
||||||
|
if (false) {
|
||||||
|
return <Empty />;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="text-gray-400 space-y-2 overflow-y-auto">
|
||||||
|
{archived ? (
|
||||||
|
Array.from(archive).map(([key, box]) => {
|
||||||
|
return Object.entries(box)
|
||||||
|
.sort(([, a], [, b]) => b.time - a.time)
|
||||||
|
.map(([binId, n], index) => renderNotification(n, `${key.toString}-${binId}`, { time: key.toString() }));
|
||||||
|
})
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<header>Unseen</header>
|
||||||
|
<section className="space-y-2">
|
||||||
|
{Object.entries(unseen)
|
||||||
|
.sort(([, a], [, b]) => b.time - a.time)
|
||||||
|
.map(([binId, n], index) => renderNotification(n, `unseen-${binId}`, { unseen: null }))}
|
||||||
|
</section>
|
||||||
|
<header>Seen</header>
|
||||||
|
<section className="space-y-2">
|
||||||
|
{Object.entries(seen)
|
||||||
|
.sort(([, a], [, b]) => b.time - a.time)
|
||||||
|
.map(([binId, n], index) => renderNotification(n, `seen-${binId}`, { seen: null }))}
|
||||||
|
</section>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
@ -47,7 +47,7 @@ const OnboardingCard = ({ title, button, body, color }: OnboardingCardProps) =>
|
|||||||
style={color ? { backgroundColor: color } : {}}
|
style={color ? { backgroundColor: color } : {}}
|
||||||
>
|
>
|
||||||
<div className="space-y-1">
|
<div className="space-y-1">
|
||||||
<h4 className="font-semibold">{title}</h4>
|
<h4 className="font-semibold text-black">{title}</h4>
|
||||||
<p>{body}</p>
|
<p>{body}</p>
|
||||||
</div>
|
</div>
|
||||||
<Button variant="primary" className="bg-gray-500">
|
<Button variant="primary" className="bg-gray-500">
|
||||||
|
@ -6,7 +6,11 @@ import { usePreferencesStore } from './usePreferencesStore';
|
|||||||
const selDnd = (s: SettingsState) => s.display.doNotDisturb;
|
const selDnd = (s: SettingsState) => s.display.doNotDisturb;
|
||||||
async function toggleDnd() {
|
async function toggleDnd() {
|
||||||
const state = useSettingsState.getState();
|
const state = useSettingsState.getState();
|
||||||
await state.putEntry('display', 'doNotDisturb', !selDnd(state));
|
const curr = selDnd(state);
|
||||||
|
if(curr) {
|
||||||
|
Notification.requestPermission();
|
||||||
|
}
|
||||||
|
await state.putEntry('display', 'doNotDisturb', !curr);
|
||||||
}
|
}
|
||||||
|
|
||||||
export const NotificationPrefs = () => {
|
export const NotificationPrefs = () => {
|
||||||
|
@ -1,6 +1,20 @@
|
|||||||
/* eslint-disable no-param-reassign */
|
/* eslint-disable no-param-reassign */
|
||||||
import create from 'zustand';
|
import create from 'zustand';
|
||||||
import { Notification, harkBinEq, makePatDa, readAll, decToUd, unixToDa } from '@urbit/api';
|
import {
|
||||||
|
Notification as HarkNotification,
|
||||||
|
harkBinEq,
|
||||||
|
makePatDa,
|
||||||
|
readAll,
|
||||||
|
decToUd,
|
||||||
|
unixToDa,
|
||||||
|
Timebox,
|
||||||
|
harkBinToId,
|
||||||
|
opened,
|
||||||
|
HarkBin,
|
||||||
|
HarkLid,
|
||||||
|
archive,
|
||||||
|
HarkContent
|
||||||
|
} from '@urbit/api';
|
||||||
import BigIntOrderedMap from '@urbit/api/lib/BigIntOrderedMap';
|
import BigIntOrderedMap from '@urbit/api/lib/BigIntOrderedMap';
|
||||||
/* eslint-disable-next-line camelcase */
|
/* eslint-disable-next-line camelcase */
|
||||||
import { unstable_batchedUpdates } from 'react-dom';
|
import { unstable_batchedUpdates } from 'react-dom';
|
||||||
@ -9,37 +23,42 @@ import { map } from 'lodash';
|
|||||||
import api from './api';
|
import api from './api';
|
||||||
import { useMockData } from './util';
|
import { useMockData } from './util';
|
||||||
import { mockNotifications } from './mock-data';
|
import { mockNotifications } from './mock-data';
|
||||||
|
import useDocketState from './docket';
|
||||||
|
import { useSettingsState } from './settings';
|
||||||
|
|
||||||
interface HarkStore {
|
interface HarkStore {
|
||||||
unreads: Notification[];
|
seen: Timebox;
|
||||||
reads: BigIntOrderedMap<Notification[]>;
|
unseen: Timebox;
|
||||||
|
archive: BigIntOrderedMap<Timebox>;
|
||||||
set: (f: (s: HarkStore) => void) => void;
|
set: (f: (s: HarkStore) => void) => void;
|
||||||
readAll: () => Promise<void>;
|
opened: () => Promise<void>;
|
||||||
|
archiveAll: () => Promise<void>;
|
||||||
|
archiveNote: (bin: HarkBin, lid: HarkLid) => Promise<void>;
|
||||||
getMore: () => Promise<void>;
|
getMore: () => Promise<void>;
|
||||||
|
webNotes: {
|
||||||
|
[binId: string]: Notification[];
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export const useHarkStore = create<HarkStore>((set, get) => ({
|
export const useHarkStore = create<HarkStore>((set, get) => ({
|
||||||
unreads: useMockData ? mockNotifications : [],
|
seen: {},
|
||||||
reads: new BigIntOrderedMap<Notification[]>(),
|
unseen: {},
|
||||||
|
archive: new BigIntOrderedMap<Timebox>(),
|
||||||
|
webNotes: {},
|
||||||
set: (f) => {
|
set: (f) => {
|
||||||
const newState = produce(get(), f);
|
const newState = produce(get(), f);
|
||||||
set(newState);
|
set(newState);
|
||||||
},
|
},
|
||||||
readAll: async () => {
|
archiveAll: async () => {},
|
||||||
const { set: innerSet } = get();
|
archiveNote: async (bin, lid) => {
|
||||||
innerSet((state) => {
|
await api.poke(archive(bin, lid));
|
||||||
state.unreads.forEach((note) => {
|
},
|
||||||
const time = makePatDa(note.time as any);
|
opened: async () => {
|
||||||
const box = state.reads.get(time) || [];
|
await api.poke(opened);
|
||||||
state.reads.set(time, [...box, note]);
|
|
||||||
});
|
|
||||||
state.unreads = [];
|
|
||||||
});
|
|
||||||
await api.poke(readAll);
|
|
||||||
},
|
},
|
||||||
getMore: async () => {
|
getMore: async () => {
|
||||||
const { reads } = get();
|
const { archive } = get();
|
||||||
const idx = decToUd((reads.peekSmallest()?.[0] || unixToDa(Date.now() * 1000)).toString());
|
const idx = decToUd((archive.peekSmallest()?.[0] || unixToDa(Date.now() * 1000)).toString());
|
||||||
const update = await api.scry({
|
const update = await api.scry({
|
||||||
app: 'hark-store',
|
app: 'hark-store',
|
||||||
path: `/recent/inbox/${idx}/5`
|
path: `/recent/inbox/${idx}/5`
|
||||||
@ -60,35 +79,59 @@ function reduceHark(u: any) {
|
|||||||
} else if ('all-stats' in u) {
|
} else if ('all-stats' in u) {
|
||||||
// TODO: probably ignore?
|
// TODO: probably ignore?
|
||||||
} else if ('added' in u) {
|
} else if ('added' in u) {
|
||||||
set((state) => {
|
set((draft) => {
|
||||||
state.unreads = state.unreads.filter((unread) => !harkBinEq(unread.bin, u.added.bin));
|
const { bin } = u.added;
|
||||||
|
const binId = harkBinToId(bin);
|
||||||
state.unreads.unshift(u.added);
|
draft.unseen[binId] = u.added;
|
||||||
});
|
});
|
||||||
} else if ('timebox' in u) {
|
} else if ('timebox' in u) {
|
||||||
const { timebox } = u;
|
const { timebox } = u;
|
||||||
const notifications = map(timebox.notifications, 'notification');
|
console.log(timebox);
|
||||||
if (timebox.time) {
|
const { lid, notifications } = timebox;
|
||||||
|
if ('archive' in lid) {
|
||||||
|
set((draft) => {
|
||||||
|
const time = makePatDa(lid.archive);
|
||||||
|
const timebox = draft.archive.get(time) || {};
|
||||||
|
console.log(timebox);
|
||||||
|
notifications.forEach((note: any) => {
|
||||||
|
console.log(note);
|
||||||
|
const binId = harkBinToId(note.bin);
|
||||||
|
timebox[binId] = note;
|
||||||
|
});
|
||||||
console.log(notifications);
|
console.log(notifications);
|
||||||
set((state) => {
|
draft.archive = draft.archive.set(time, timebox);
|
||||||
state.reads = state.reads.set(makePatDa(timebox.time), notifications);
|
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
set((state) => {
|
set((draft) => {
|
||||||
state.unreads = [...state.unreads, ...notifications];
|
const seen = 'seen' in lid ? 'seen' : 'unseen';
|
||||||
|
notifications.forEach((note: any) => {
|
||||||
|
const binId = harkBinToId(note.bin);
|
||||||
|
draft[seen][binId] = note;
|
||||||
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
} else if ('archive' in u) {
|
} else if ('archived' in u) {
|
||||||
console.log(u.archive);
|
const { lid, notification } = u.archived;
|
||||||
set((state) => {
|
console.log(u.archived);
|
||||||
const time = u.archive?.time;
|
set((draft) => {
|
||||||
if (time) {
|
const seen = 'seen' in lid ? 'seen' : 'unseen';
|
||||||
let box = state.reads.get(makePatDa(time)) || [];
|
const binId = harkBinToId(notification.bin);
|
||||||
box = box.filter((n) => !harkBinEq(u.archive.bin, n.bin));
|
delete draft[seen][binId];
|
||||||
state.reads = state.reads.set(makePatDa(time), box);
|
const time = makePatDa(u.archived.time);
|
||||||
} else {
|
const timebox = draft.archive.get(time) || {};
|
||||||
state.unreads = state.unreads.filter((n) => !harkBinEq(u.archive.bin, n.bin));
|
timebox[binId] = notification;
|
||||||
}
|
draft.archive = draft.archive.set(time, timebox);
|
||||||
|
});
|
||||||
|
} else if ('opened' in u) {
|
||||||
|
set((draft) => {
|
||||||
|
const bins = Object.keys(draft.unseen);
|
||||||
|
bins.forEach((bin) => {
|
||||||
|
const old = draft.seen[bin];
|
||||||
|
const curr = draft.unseen[bin];
|
||||||
|
curr.body = [...curr.body, ...(old?.body || [])];
|
||||||
|
draft.seen[bin] = curr;
|
||||||
|
delete draft.unseen[bin];
|
||||||
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -103,3 +146,37 @@ api.subscribe({
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
function harkContentsToPlainText(contents: HarkContent[]) {
|
||||||
|
return contents
|
||||||
|
.map((c) => {
|
||||||
|
if ('ship' in c) {
|
||||||
|
return c.ship;
|
||||||
|
}
|
||||||
|
return c.text;
|
||||||
|
})
|
||||||
|
.join('');
|
||||||
|
}
|
||||||
|
|
||||||
|
api.subscribe({
|
||||||
|
app: 'hark-store',
|
||||||
|
path: '/notes',
|
||||||
|
event: (u: any) => {
|
||||||
|
if ('add-note' in u) {
|
||||||
|
if (useSettingsState.getState().display.doNotDisturb) {
|
||||||
|
//return;
|
||||||
|
}
|
||||||
|
const { bin, body } = u['add-note'];
|
||||||
|
const binId = harkBinToId(bin);
|
||||||
|
const { title, content } = body;
|
||||||
|
const image = useDocketState.getState().charges[bin.desk]?.image;
|
||||||
|
|
||||||
|
const notification = new Notification(harkContentsToPlainText(title), {
|
||||||
|
body: harkContentsToPlainText(content),
|
||||||
|
tag: binId,
|
||||||
|
renotify: true
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
window.hark = useHarkStore.getState;
|
||||||
|
@ -2,12 +2,12 @@ import shallow from 'zustand/shallow';
|
|||||||
import { useHarkStore } from './hark';
|
import { useHarkStore } from './hark';
|
||||||
|
|
||||||
export const useNotifications = () => {
|
export const useNotifications = () => {
|
||||||
const [unreads, reads] = useHarkStore((s) => [s.unreads, s.reads], shallow);
|
const [unseen, seen] = useHarkStore((s) => [s.unseen, s.seen], shallow);
|
||||||
const hasAnyNotifications = unreads.length > 0 || reads.size > 0;
|
const hasAnyNotifications = Object.keys(seen).length > 0 || Object.keys(unseen).length > 0;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
unreads,
|
unseen,
|
||||||
reads,
|
seen,
|
||||||
hasAnyNotifications
|
hasAnyNotifications
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
@ -20,6 +20,7 @@ export default ({ mode }) => {
|
|||||||
mode === 'mock'
|
mode === 'mock'
|
||||||
? undefined
|
? undefined
|
||||||
: {
|
: {
|
||||||
|
https: true,
|
||||||
proxy: {
|
proxy: {
|
||||||
'^/apps/grid/desk.js': {
|
'^/apps/grid/desk.js': {
|
||||||
target: SHIP_URL
|
target: SHIP_URL
|
||||||
|
Loading…
Reference in New Issue
Block a user