diff --git a/pkg/interface/src/views/apps/notifications/Archive.tsx b/pkg/interface/src/views/apps/notifications/Archive.tsx new file mode 100644 index 0000000000..e4df3c2bbb --- /dev/null +++ b/pkg/interface/src/views/apps/notifications/Archive.tsx @@ -0,0 +1,31 @@ +import { Box } from '@tlon/indigo-react'; +import React, { useEffect } from 'react'; +import useHarkState, { HarkState } from '~/logic/state/hark'; +import { Notification } from './notification'; + +const selArchive = (s: HarkState) => s.archive; + +export function Archive() { + const archive = useHarkState(selArchive); + const keys = archive.keys(); + + useEffect(() => { + useHarkState.getState().getMore(); + }, []); + + return ( + + {keys.map(key => + Object.entries(archive.get(key)!) + .sort(([, a], [, b]) => b.time - a.time) + .map(([binId, n]) => ( + + )) + )} + + ); +} diff --git a/pkg/interface/src/views/apps/notifications/NewBox.tsx b/pkg/interface/src/views/apps/notifications/NewBox.tsx new file mode 100644 index 0000000000..56fb99ea4f --- /dev/null +++ b/pkg/interface/src/views/apps/notifications/NewBox.tsx @@ -0,0 +1,58 @@ +import { Box, Center, Col, Text } from '@tlon/indigo-react'; +import React from 'react'; +import useHarkState, { HarkState } from '~/logic/state/hark'; +import { harkBinToId, HarkLid, Timebox } from '../../../../../npm/api/dist'; +import { Notification } from './notification'; + +const unseenLid = { unseen: null }; +const seenLid = { seen: null }; +const selUnseen = (s: HarkState) => s.unseen; +const selSeen = (s: HarkState) => s.seen; +export function NewBox() { + const seen = useHarkState(selSeen); + const unseen = useHarkState(selUnseen); + const empty = Object.keys(seen).length + Object.keys(unseen).length === 0; + + return ( + + {empty ? ( +
+ All clear! +
+ ) : ( + <> + + + + )} +
+ ); +} + +function Lid({ + lid, + timebox, + title +}: { + lid: HarkLid; + timebox: Timebox; + title: string; +}) { + if(Object.keys(timebox).length === 0) { + return null; + } + return ( + <> + + {title} + + + {Object.entries(timebox) + .sort(([, a], [, b]) => b.time - a.time) + .map(([binId, n]) => ( + + ))} + + + ); +} diff --git a/pkg/interface/src/views/apps/notifications/inbox.tsx b/pkg/interface/src/views/apps/notifications/inbox.tsx deleted file mode 100644 index c0490d8654..0000000000 --- a/pkg/interface/src/views/apps/notifications/inbox.tsx +++ /dev/null @@ -1,135 +0,0 @@ -import { Col } from '@tlon/indigo-react'; -import { - IndexedNotification, - JoinRequests, - Notifications, - seen, - Timebox, - unixToDa -} from '@urbit/api'; -import { BigInteger } from 'big-integer'; -import _ from 'lodash'; -import f from 'lodash/fp'; -import moment from 'moment'; -import React, { useEffect } from 'react'; -import { getNotificationKey } from '~/logic/lib/hark'; -import { daToUnix } from '~/logic/lib/util'; -import useHarkState from '~/logic/state/hark'; -import { Invites } from './invites'; -import { Notification } from './notification'; -import airlock from '~/logic/api'; - -type DatedTimebox = [BigInteger, Timebox]; - -function filterNotification(groups: string[]) { - if (groups.length === 0) { - return () => true; - } - return (n: IndexedNotification) => { - if ('graph' in n.index) { - const { group } = n.index.graph; - return groups.findIndex(g => group === g) !== -1; - } else if ('group' in n.index) { - const { group } = n.index.group; - return groups.findIndex(g => group === g) !== -1; - } - return true; - }; -} - -export default function Inbox(props: { - archive: Notifications; - showArchive?: boolean; - filter: string[]; - pendingJoin: JoinRequests; -}) { - useEffect(() => { - let hasSeen = false; - setTimeout(() => { - hasSeen = true; - }, 3000); - return () => { - if (hasSeen) { - airlock.poke(seen()); - } - }; - }, []); - - const ready = useHarkState( - s => Object.keys(s.unreads.graph).length > 0 - ); - - const getMore = useHarkState(s => s.getMore); - - const notificationState = useHarkState(state => state.notifications); - const unreadNotes = useHarkState(s => s.unreadNotes); - const archivedNotifications = useHarkState(state => state.archivedNotifications); - - const notifications = - Array.from(props.showArchive ? archivedNotifications : notificationState) || []; - - const notificationsByDay = f.flow( - f.map(([date, nots]) => [ - date, - nots.filter(filterNotification(props.filter)) - ]), - f.groupBy(([d]) => { - const date = moment(daToUnix(d)); - if (moment().subtract(6, 'hours').isBefore(date)) { - return 'latest'; - } else { - return date.format('YYYYMMDD'); - } - }) - )(notifications); - - const notificationsByDayMap = new Map( - Object.keys(notificationsByDay).map((timebox) => { - return [timebox, notificationsByDay[timebox]]; - }) - ); - - const date = unixToDa(Date.now()); - - return ( - - - - ); -} - -function sortTimeboxes([a]: DatedTimebox, [b]: DatedTimebox) { - return b.subtract(a); -} - -function sortIndexedNotification( - { notification: a }: IndexedNotification, - { notification: b }: IndexedNotification -) { - return b.time - a.time; -} - -function DaySection({ - timeboxes, - unread = false -}) { - const lent = timeboxes.map(([,nots]) => nots.length).reduce(f.add, 0); - if (lent === 0 || timeboxes.length === 0) { - return null; - } - - return ( - <> - {_.map(timeboxes.sort(sortTimeboxes), ([date, nots], i: number) => - _.map(nots.sort(sortIndexedNotification), (not, j: number) => ( - - )) - )} - - ); -} diff --git a/pkg/interface/src/views/apps/notifications/notification.tsx b/pkg/interface/src/views/apps/notifications/notification.tsx index b9fbbf5d64..43f9b03d0a 100644 --- a/pkg/interface/src/views/apps/notifications/notification.tsx +++ b/pkg/interface/src/views/apps/notifications/notification.tsx @@ -1,64 +1,111 @@ -import { Box, Button, Icon, Row } from '@tlon/indigo-react'; +import { Box, Col, Text, Button, Icon, Row } from '@tlon/indigo-react'; import { - GraphNotificationContents, - GroupNotificationContents, - IndexedNotification + HarkLid, + harkLidToId, + harkBinToId, + Notification as INotification, + HarkContent } from '@urbit/api'; import { BigInteger } from 'big-integer'; -import React, { ReactNode, useCallback } from 'react'; -import { getNotificationKey } from '~/logic/lib/hark'; +import React, { useCallback } from 'react'; import { useHovering } from '~/logic/lib/util'; import useLocalState from '~/logic/state/local'; import { StatelessAsyncAction } from '~/views/components/StatelessAsyncAction'; import { SwipeMenu } from '~/views/components/SwipeMenu'; -import { GraphNotification } from './graph'; -import { GroupNotification } from './group'; import useHarkState from '~/logic/state/hark'; -import shallow from 'zustand/shallow'; +import { map, take } from 'lodash'; +import { Mention } from '~/views/components/MentionText'; +import { PropFunc } from '~/types'; +import { useHistory } from 'react-router-dom'; +import { getNotificationRedirect } from '~/logic/lib/notificationRedirects'; export interface NotificationProps { - notification: IndexedNotification; + notification: INotification; time: BigInteger; unread: boolean; } -export function NotificationWrapper(props: { - time?: BigInteger; - read?: boolean; - notification?: IndexedNotification; - children: ReactNode; +const MAX_CONTENTS = 5; + +interface NotificationTextProps extends PropFunc { + contents: HarkContent[]; +} +const NotificationText = ({ contents, ...rest }: NotificationTextProps) => { + return ( + <> + {contents.map((content, idx) => { + if ('ship' in content) { + return ( + + ); + } + return {content.text}; + })} + + ); +}; + +export function Notification(props: { + lid: HarkLid; + notification: INotification; }) { - const { time, notification, children, read = false } = props; + const { notification, lid } = props; + const read = !('unseen' in lid); + const key = `${harkLidToId(lid)}-${harkBinToId(notification.bin)}`; + const history = useHistory(); const isMobile = useLocalState(s => s.mobile); - const [archive, readNote] = useHarkState(s => [s.archive, s.readNote], shallow); - - const onArchive = useCallback(async (e) => { - e.stopPropagation(); - if (!notification) { - return; - } - await archive(notification.index, time); - }, [time, notification]); - - const onClick = (e: any) => { - if (!notification || read) { - return; - } - return readNote(notification.index); - }; + const onArchive = useCallback( + async (e) => { + e.stopPropagation(); + if (!notification) { + return; + } + useHarkState.getState().archiveNote(notification.bin, lid); + }, + [notification, lid] + ); const { hovering, bind } = useHovering(); + const contents = map(notification.body, 'content').filter( + c => c.length > 0 + ); + const first = notification.body[0]; + if (!first) { + // should be unreachable + return null; + } + + const onClick = (e: any) => { + const redirect = getNotificationRedirect(first.link); + if(redirect) { + history.push(redirect); + } else { + console.log('no redirect'); + } + }; return ( + } @@ -74,7 +121,24 @@ export function NotificationWrapper(props: { p={2} {...bind} > - {children} + + + + + + {take(contents, MAX_CONTENTS).map((content, i) => ( + + + + ))} + + {contents.length > MAX_CONTENTS ? ( + + and {contents.length - MAX_CONTENTS} more + + ) : null} + + - {notification && ( + {!('time' in lid) && ( ); } - -export function Notification(props: NotificationProps) { - const { notification, unread } = props; - const { contents, time } = notification.notification; - - const wrapperProps = { - notification, - read: !unread, - time: props.time - }; - - if ('graph' in notification.index) { - const index = notification.index.graph; - const c: GraphNotificationContents = (contents as any).graph; - - return ( - - - - ); - } - if ('group' in notification.index) { - const index = notification.index.group; - const c: GroupNotificationContents = (contents as any).group; - return ( - - - - ); - } - - return null; -} diff --git a/pkg/interface/src/views/apps/notifications/notifications.tsx b/pkg/interface/src/views/apps/notifications/notifications.tsx index 26d9fe1e41..9a48d903aa 100644 --- a/pkg/interface/src/views/apps/notifications/notifications.tsx +++ b/pkg/interface/src/views/apps/notifications/notifications.tsx @@ -1,29 +1,50 @@ -import { Box, Col, Icon, Row, Text } from '@tlon/indigo-react'; -import React, { ReactElement, useCallback, useRef } from 'react'; +import { Action, Box, Col, Icon, Row, Text } from '@tlon/indigo-react'; +import React, { ReactElement, ReactNode, useRef } from 'react'; import Helmet from 'react-helmet'; -import { Link, Route, Switch } from 'react-router-dom'; -import useGroupState from '~/logic/state/group'; +import { Link, Route, Switch, useHistory, useLocation } from 'react-router-dom'; import useHarkState from '~/logic/state/hark'; import { Body } from '~/views/components/Body'; import { StatelessAsyncAction } from '~/views/components/StatelessAsyncAction'; import { useTutorialModal } from '~/views/components/useTutorialModal'; -import Inbox from './inbox'; -import airlock from '~/logic/api'; -import { readAll } from '@urbit/api'; +import { Archive } from './Archive'; +import { NewBox } from './NewBox'; const baseUrl = '/~notifications'; +export function NavLink({ + href, + children +}: { + href: string; + children: ReactNode; +}) { + const location = useLocation(); + const { push } = useHistory(); + + const isActive = href === location.pathname; + + const onClick = () => { + push(href); + }; + + return ( + + {children} + + ); +} + export default function NotificationsScreen(props: any): ReactElement { const relativePath = (p: string) => baseUrl + p; - const pendingJoin = useGroupState(s => s.pendingJoin); - const onReadAll = useCallback(async () => { - await airlock.poke(readAll()); - }, []); - const anchorRef = useRef(null); useTutorialModal('notifications', true, anchorRef); const notificationsCount = useHarkState(state => state.notificationsCount); + const onReadAll = async () => {}; return ( - { notificationsCount ? `(${String(notificationsCount) }) `: '' }Landscape - Notifications + + {notificationsCount ? `(${String(notificationsCount)}) ` : ''} + Groups - Notifications + @@ -46,22 +70,29 @@ export default function NotificationsScreen(props: any): ReactElement { borderBottom={1} borderBottomColor="lightGray" > - - - Notifications - - - - Mark All Read - + Notifications + + + New + Archive + + + { (false as boolean) ? ( + + Mark All Read + + ) : null} @@ -69,11 +100,9 @@ export default function NotificationsScreen(props: any): ReactElement { - {!view && } + { view === 'archive' ? ( + + ) : }