mirror of
https://github.com/ilyakooo0/urbit.git
synced 2024-12-14 08:34:25 +03:00
hark-fe: add inbox view
This commit is contained in:
parent
ec5804bb3c
commit
0a69c6fd5b
@ -124,6 +124,9 @@ class App extends React.Component {
|
||||
const theme = state.dark ? dark : light;
|
||||
const { background } = state;
|
||||
|
||||
const notificationsCount = state.notificationsCount || 0;
|
||||
const doNotDisturb = state.doNotDisturb || false;
|
||||
|
||||
return (
|
||||
<ThemeProvider theme={theme}>
|
||||
<Helmet>
|
||||
@ -142,6 +145,8 @@ class App extends React.Component {
|
||||
connection={this.state.connection}
|
||||
subscription={this.subscription}
|
||||
ship={this.ship}
|
||||
doNotDisturb={doNotDisturb}
|
||||
notificationsCount={notificationsCount}
|
||||
/>
|
||||
</ErrorBoundary>
|
||||
<ErrorBoundary>
|
||||
|
162
pkg/interface/src/views/apps/notifications/graph.tsx
Normal file
162
pkg/interface/src/views/apps/notifications/graph.tsx
Normal file
@ -0,0 +1,162 @@
|
||||
import React, { ReactNode, useCallback } from "react";
|
||||
import moment from "moment";
|
||||
import { Row, Box, Col, Text, Anchor, Icon, Action } from "@tlon/indigo-react";
|
||||
import _ from "lodash";
|
||||
import {
|
||||
Post,
|
||||
GraphNotifIndex,
|
||||
GraphNotificationContents,
|
||||
Associations,
|
||||
Content,
|
||||
Rolodex,
|
||||
} from "~/types";
|
||||
import { Header } from "./header";
|
||||
import { cite, deSig } from "~/logic/lib/util";
|
||||
import { Sigil } from "~/logic/lib/sigil";
|
||||
import RichText from "~/views/components/RichText";
|
||||
import GlobalApi from "~/logic/api/global";
|
||||
|
||||
function getGraphModuleIcon(module: string) {
|
||||
if (module === "link") {
|
||||
return "Links";
|
||||
}
|
||||
return _.capitalize(module);
|
||||
}
|
||||
|
||||
function describeNotification(description: string, plural: boolean) {
|
||||
switch (description) {
|
||||
case "link":
|
||||
return `added ${plural ? "new links" : "a new link"} to`;
|
||||
case "comment":
|
||||
return `left ${plural ? "comments" : "a comment"} on`;
|
||||
case "mention":
|
||||
return "mentioned you on";
|
||||
default:
|
||||
return description;
|
||||
}
|
||||
}
|
||||
|
||||
const GraphUrl = ({ url, title }) => (
|
||||
<Box borderRadius="1" p="2" bg="washedGray">
|
||||
<Anchor target="_blank" color="gray" href={url}>
|
||||
<Icon verticalAlign="bottom" mr="2" icon="ArrowExternal" />
|
||||
{title}
|
||||
</Anchor>
|
||||
</Box>
|
||||
);
|
||||
|
||||
const GraphNodeContent = ({ contents, module, description, index }) => {
|
||||
if (module === "link") {
|
||||
const lent = index.slice(1).split("/").length;
|
||||
if (lent === 1) {
|
||||
const [{ text }, { url }] = contents;
|
||||
return <GraphUrl title={text} url={url} />;
|
||||
} else if (lent === 2) {
|
||||
const [{ text }] = contents;
|
||||
return <RichText>{text}</RichText>;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
return null;
|
||||
};
|
||||
const GraphNode = ({ contents, author, module, description, time, index }) => {
|
||||
author = deSig(author);
|
||||
|
||||
const img = (
|
||||
<Sigil
|
||||
ship={`~${author}`}
|
||||
size={16}
|
||||
icon
|
||||
color={`#000000`}
|
||||
classes="mix-blend-diff"
|
||||
/>
|
||||
);
|
||||
|
||||
return (
|
||||
<Row gapX="2" py="2">
|
||||
<Col>{img}</Col>
|
||||
<Col alignItems="flex-start">
|
||||
<Row
|
||||
mb="2"
|
||||
height="16px"
|
||||
alignItems="center"
|
||||
p="1"
|
||||
backgroundColor="white"
|
||||
>
|
||||
<Text mono title={author}>
|
||||
{cite(author)}
|
||||
</Text>
|
||||
<Text ml="2" gray>
|
||||
{moment(time).format("HH:mm")}
|
||||
</Text>
|
||||
</Row>
|
||||
<Row p="1">
|
||||
<GraphNodeContent
|
||||
contents={contents}
|
||||
module={module}
|
||||
description={description}
|
||||
index={index}
|
||||
/>
|
||||
</Row>
|
||||
</Col>
|
||||
</Row>
|
||||
);
|
||||
};
|
||||
|
||||
export function GraphNotification(props: {
|
||||
index: GraphNotifIndex;
|
||||
contents: GraphNotificationContents;
|
||||
archived: boolean;
|
||||
read: boolean;
|
||||
time: number;
|
||||
timebox: BigInteger;
|
||||
associations: Associations;
|
||||
contacts: Rolodex;
|
||||
api: GlobalApi;
|
||||
}) {
|
||||
const { contents, index, read, time, api, timebox } = props;
|
||||
|
||||
const authors = _.map(contents, "author");
|
||||
const { graph, group } = index;
|
||||
const icon = getGraphModuleIcon(index.module);
|
||||
const desc = describeNotification(index.description, contents.length !== 1);
|
||||
|
||||
const onClick = useCallback(() => {
|
||||
if (props.archived) {
|
||||
return;
|
||||
}
|
||||
|
||||
const func = read ? "unread" : "read";
|
||||
return api.hark[func](timebox, { graph: index });
|
||||
}, [api, timebox, index, read]);
|
||||
|
||||
return (
|
||||
<Col onClick={onClick} p="2">
|
||||
<Header
|
||||
archived={props.archived}
|
||||
time={time}
|
||||
read={read}
|
||||
authors={authors}
|
||||
moduleIcon={icon}
|
||||
channel={graph}
|
||||
contacts={props.contacts}
|
||||
group={group}
|
||||
description={desc}
|
||||
associations={props.associations}
|
||||
/>
|
||||
<Col pl="5">
|
||||
{_.map(contents, (content, idx) => (
|
||||
<GraphNode
|
||||
author={content.author}
|
||||
contents={content.contents}
|
||||
module={index.module}
|
||||
time={content["time-sent"]}
|
||||
description={index.description}
|
||||
index={content.index}
|
||||
/>
|
||||
))}
|
||||
</Col>
|
||||
</Col>
|
||||
);
|
||||
}
|
||||
|
90
pkg/interface/src/views/apps/notifications/group.tsx
Normal file
90
pkg/interface/src/views/apps/notifications/group.tsx
Normal file
@ -0,0 +1,90 @@
|
||||
import React, { ReactNode, useCallback } from "react";
|
||||
import moment from "moment";
|
||||
import { Row, Box, Col, Text, Anchor, Icon, Action } from "@tlon/indigo-react";
|
||||
import _ from "lodash";
|
||||
import { NotificationProps } from "./types";
|
||||
import {
|
||||
Post,
|
||||
GraphNotifIndex,
|
||||
GraphNotificationContents,
|
||||
Associations,
|
||||
Content,
|
||||
IndexedNotification,
|
||||
GroupNotificationContents,
|
||||
GroupNotifIndex,
|
||||
GroupUpdate,
|
||||
Rolodex,
|
||||
} from "~/types";
|
||||
import { Header } from "./header";
|
||||
import { cite, deSig } from "~/logic/lib/util";
|
||||
import { Sigil } from "~/logic/lib/sigil";
|
||||
import RichText from "~/views/components/RichText";
|
||||
import GlobalApi from "~/logic/api/global";
|
||||
import { StatelessAsyncAction } from "~/views/components/StatelessAsyncAction";
|
||||
|
||||
|
||||
function describeNotification(description: string, plural: boolean) {
|
||||
switch (description) {
|
||||
case "add-members":
|
||||
return "joined";
|
||||
case "remove-members":
|
||||
return "left";
|
||||
default:
|
||||
return description;
|
||||
}
|
||||
}
|
||||
|
||||
function getGroupUpdateParticipants(update: GroupUpdate) {
|
||||
if ("addMembers" in update) {
|
||||
return update.addMembers.ships;
|
||||
}
|
||||
if ("removeMembers" in update) {
|
||||
return update.removeMembers.ships;
|
||||
}
|
||||
return [];
|
||||
}
|
||||
|
||||
interface GroupNotificationProps {
|
||||
index: GroupNotifIndex;
|
||||
contents: GroupNotificationContents;
|
||||
archived: boolean;
|
||||
read: boolean;
|
||||
time: number;
|
||||
timebox: BigInteger;
|
||||
associations: Associations;
|
||||
contacts: Rolodex;
|
||||
api: GlobalApi;
|
||||
}
|
||||
|
||||
export function GroupNotification(props: GroupNotificationProps) {
|
||||
const { contents, index, read, time, api, timebox, associations } = props;
|
||||
|
||||
const authors = _.flatten(_.map(contents, getGroupUpdateParticipants));
|
||||
|
||||
const { group } = index;
|
||||
const desc = describeNotification(index.description, contents.length !== 1);
|
||||
|
||||
const onClick = useCallback(() => {
|
||||
if (props.archived) {
|
||||
return;
|
||||
}
|
||||
const func = read ? "unread" : "read";
|
||||
return api.hark[func](timebox, { group: index });
|
||||
}, [api, timebox, index, read]);
|
||||
|
||||
return (
|
||||
<Col onClick={onClick} p="2">
|
||||
<Header
|
||||
archived={props.archived}
|
||||
time={time}
|
||||
read={read}
|
||||
group={group}
|
||||
contacts={props.contacts}
|
||||
authors={authors}
|
||||
description={desc}
|
||||
associations={associations}
|
||||
/>
|
||||
</Col>
|
||||
);
|
||||
}
|
||||
|
104
pkg/interface/src/views/apps/notifications/header.tsx
Normal file
104
pkg/interface/src/views/apps/notifications/header.tsx
Normal file
@ -0,0 +1,104 @@
|
||||
import React from "react";
|
||||
import { Text as NormalText, Row, Icon } from "@tlon/indigo-react";
|
||||
import f from "lodash/fp";
|
||||
import moment from "moment";
|
||||
import { PropFunc } from "~/types/util";
|
||||
import { getContactDetails } from "~/logic/lib/util";
|
||||
import { Associations, Contact, Contacts, Rolodex } from "~/types";
|
||||
|
||||
const Text = (props: PropFunc<typeof Text>) => (
|
||||
<NormalText fontWeight="500" {...props} />
|
||||
);
|
||||
|
||||
const Divider = (props: PropFunc<typeof Text>) => (
|
||||
<Text lineHeight="tall" mx="1" fontWeight="bold" color="lightGray">
|
||||
|
|
||||
</Text>
|
||||
);
|
||||
|
||||
function Author(props: { patp: string; contacts: Contacts; last?: boolean }) {
|
||||
const contact: Contact | undefined = props.contacts?.[props.patp];
|
||||
|
||||
const showNickname = !!contact?.nickname;
|
||||
const name = contact?.nickname || `~${props.patp}`;
|
||||
|
||||
return (
|
||||
<Text mono={!showNickname}>
|
||||
{name}
|
||||
{!props.last && ", "}
|
||||
</Text>
|
||||
);
|
||||
}
|
||||
|
||||
export function Header(props: {
|
||||
authors: string[];
|
||||
archived?: boolean;
|
||||
channel?: string;
|
||||
group: string;
|
||||
contacts: Rolodex;
|
||||
description: string;
|
||||
moduleIcon?: string;
|
||||
time: number;
|
||||
read: boolean;
|
||||
associations: Associations;
|
||||
}) {
|
||||
const { description, channel, group, moduleIcon, read } = props;
|
||||
const contacts = props.contacts[group] || {};
|
||||
|
||||
const authors = _.uniq(props.authors);
|
||||
|
||||
const authorDesc = f.flow(
|
||||
f.take(3),
|
||||
f.entries,
|
||||
f.map(([idx, p]: [string, string]) => {
|
||||
const lent = Math.min(3, authors.length);
|
||||
const last = lent - 1 === parseInt(idx, 10);
|
||||
return <Author key={idx} contacts={contacts} patp={p} last={last} />;
|
||||
}),
|
||||
(auths) => (
|
||||
<React.Fragment>
|
||||
{auths}
|
||||
|
||||
{authors.length > 3 &&
|
||||
` and ${authors.length - 3} other${authors.length === 4 ? "" : "s"}`}
|
||||
</React.Fragment>
|
||||
)
|
||||
)(authors);
|
||||
|
||||
const time = moment(props.time).format("HH:mm");
|
||||
const groupTitle =
|
||||
props.associations.contacts?.[props.group]?.metadata?.title || props.group;
|
||||
|
||||
const channelTitle =
|
||||
(channel && props.associations.graph?.[channel]?.metadata?.title) ||
|
||||
channel;
|
||||
|
||||
return (
|
||||
<Row p="2" flexWrap="wrap" gapX="1" alignItems="center">
|
||||
{!props.archived && (
|
||||
<Icon
|
||||
display="block"
|
||||
mr="1"
|
||||
icon={read ? "Circle" : "Bullet"}
|
||||
color="blue"
|
||||
/>
|
||||
)}
|
||||
<Text mr="1" mono>
|
||||
{authorDesc}
|
||||
</Text>
|
||||
<Text mr="1">{description}</Text>
|
||||
{!!moduleIcon && <Icon icon={moduleIcon as any} />}
|
||||
{!!channel && <Text fontWeight="500">{channelTitle}</Text>}
|
||||
<Text mx="1" fontWeight="bold" color="lightGray">
|
||||
|
|
||||
</Text>
|
||||
<Text fontWeight="500">{groupTitle}</Text>
|
||||
<Text lineHeight="tall" mx="1" fontWeight="bold" color="lightGray">
|
||||
|
|
||||
</Text>
|
||||
<Text fontWeight="regular" color="lightGray">
|
||||
{time}
|
||||
</Text>
|
||||
</Row>
|
||||
);
|
||||
}
|
143
pkg/interface/src/views/apps/notifications/inbox.tsx
Normal file
143
pkg/interface/src/views/apps/notifications/inbox.tsx
Normal file
@ -0,0 +1,143 @@
|
||||
import React, { useEffect } from "react";
|
||||
import f from "lodash/fp";
|
||||
import _ from "lodash";
|
||||
import { Icon, Col, Row, Box, Text, Anchor } from "@tlon/indigo-react";
|
||||
import moment from "moment";
|
||||
import { Notifications, Rolodex, Timebox, IndexedNotification } from "~/types";
|
||||
import { MOMENT_CALENDAR_DATE, daToUnix } from "~/logic/lib/util";
|
||||
import { BigInteger } from "big-integer";
|
||||
import GlobalApi from "~/logic/api/global";
|
||||
import { Notification } from "./notification";
|
||||
import { Associations } from "~/types";
|
||||
|
||||
export default function Inbox(props: {
|
||||
notifications: Notifications;
|
||||
archive: Notifications;
|
||||
showArchive?: boolean;
|
||||
api: GlobalApi;
|
||||
associations: Associations;
|
||||
contacts: Rolodex;
|
||||
gr;
|
||||
}) {
|
||||
const { api, associations } = props;
|
||||
useEffect(() => {
|
||||
let seen = false;
|
||||
setTimeout(() => {
|
||||
seen = true;
|
||||
}, 3000);
|
||||
return () => {
|
||||
if (seen) {
|
||||
//api.hark.seen();
|
||||
}
|
||||
};
|
||||
}, []);
|
||||
|
||||
const [newNotifications, ...notifications] =
|
||||
Array.from(props.showArchive ? props.archive : props.notifications) || [];
|
||||
|
||||
const inspect = f.tap((x: any) => {
|
||||
console.log(x);
|
||||
});
|
||||
|
||||
const notificationsByDay = f.flow(
|
||||
f.groupBy<[BigInteger, Timebox]>(([date]) =>
|
||||
moment(daToUnix(date)).format("DDMMYYYY")
|
||||
),
|
||||
f.values,
|
||||
inspect
|
||||
)(notifications);
|
||||
|
||||
return (
|
||||
<Col overflowY="auto" flexGrow="1">
|
||||
{newNotifications && (
|
||||
<DaySection
|
||||
latest
|
||||
timeboxes={[newNotifications]}
|
||||
contacts={props.contacts}
|
||||
archive={!!props.showArchive}
|
||||
associations={props.associations}
|
||||
graphConfig={props.notificationsGraphConfig}
|
||||
groupConfig={props.notificationsGroupConfig}
|
||||
api={api}
|
||||
/>
|
||||
)}
|
||||
|
||||
{_.map(
|
||||
notificationsByDay,
|
||||
(timeboxes, idx) =>
|
||||
timeboxes.length > 0 && (
|
||||
<DaySection
|
||||
key={idx}
|
||||
timeboxes={timeboxes}
|
||||
contacts={props.contacts}
|
||||
archive={!!props.showArchive}
|
||||
associations={props.associations}
|
||||
api={api}
|
||||
graphConfig={props.notificationsGraphConfig}
|
||||
groupConfig={props.notificationsGroupConfig}
|
||||
/>
|
||||
)
|
||||
)}
|
||||
</Col>
|
||||
);
|
||||
}
|
||||
|
||||
type DatedTimebox = [BigInteger, Timebox];
|
||||
|
||||
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({
|
||||
contacts,
|
||||
archive,
|
||||
timeboxes,
|
||||
latest = false,
|
||||
associations,
|
||||
api,
|
||||
groupConfig,
|
||||
graphConfig,
|
||||
}) {
|
||||
const calendar = latest
|
||||
? MOMENT_CALENDAR_DATE
|
||||
: { ...MOMENT_CALENDAR_DATE, sameDay: "[Earlier Today]" };
|
||||
if (timeboxes.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<Box position="sticky" zIndex="2" top="0px" bg="white">
|
||||
<Box p="2" bg="scales.black05">
|
||||
{moment(daToUnix(timeboxes[0][0])).calendar(null, calendar)}
|
||||
</Box>
|
||||
</Box>
|
||||
{_.map(timeboxes.sort(sortTimeboxes), ([date, nots], i) =>
|
||||
_.map(nots.sort(sortIndexedNotification), (not, j: number) => (
|
||||
<React.Fragment key={j}>
|
||||
{(i !== 0 || j !== 0) && (
|
||||
<Box flexShrink="0" height="4px" bg="scales.black05" />
|
||||
)}
|
||||
<Notification
|
||||
graphConfig={graphConfig}
|
||||
groupConfig={groupConfig}
|
||||
api={api}
|
||||
associations={associations}
|
||||
notification={not}
|
||||
archived={archive}
|
||||
contacts={contacts}
|
||||
time={date}
|
||||
/>
|
||||
</React.Fragment>
|
||||
))
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
43
pkg/interface/src/views/apps/notifications/metadata.tsx
Normal file
43
pkg/interface/src/views/apps/notifications/metadata.tsx
Normal file
@ -0,0 +1,43 @@
|
||||
import React from "react";
|
||||
import { Box } from "@tlon/indigo-react";
|
||||
|
||||
import { MetadataBody, NotificationProps } from "./types";
|
||||
import { Header } from "./header";
|
||||
|
||||
function getInvolvedUsers(body: MetadataBody) {
|
||||
return [];
|
||||
}
|
||||
|
||||
function getDescription(body: MetadataBody) {
|
||||
const b = body.metadata;
|
||||
if ("new" in b) {
|
||||
return "created";
|
||||
} else if ("changedTitle" in b) {
|
||||
return "changed the title to";
|
||||
} else if ("changedDescription" in b) {
|
||||
return "changed the description to";
|
||||
} else if ("changedColor" in b) {
|
||||
return "changed the color to";
|
||||
} else if ("deleted" in b) {
|
||||
return "deleted";
|
||||
} else {
|
||||
throw new Error("bad metadata frond");
|
||||
}
|
||||
}
|
||||
|
||||
export function MetadataNotification(props: NotificationProps<"metadata">) {
|
||||
const { unread } = props;
|
||||
const description = getDescription(unread.unreads[0].body);
|
||||
|
||||
return (
|
||||
<Box p="2">
|
||||
<Header
|
||||
authors={[]}
|
||||
description={description}
|
||||
group={unread.group}
|
||||
channel={unread.channel}
|
||||
date={unread.date}
|
||||
/>
|
||||
</Box>
|
||||
);
|
||||
}
|
142
pkg/interface/src/views/apps/notifications/notification.tsx
Normal file
142
pkg/interface/src/views/apps/notifications/notification.tsx
Normal file
@ -0,0 +1,142 @@
|
||||
import React, { ReactNode, useCallback, useMemo } from "react";
|
||||
import { Row, Box, Col, Text, Anchor, Icon, Action } from "@tlon/indigo-react";
|
||||
import _ from "lodash";
|
||||
import {
|
||||
GraphNotificationContents,
|
||||
IndexedNotification,
|
||||
GroupNotificationContents,
|
||||
NotificationGraphConfig,
|
||||
GroupNotificationsConfig,
|
||||
NotifIndex,
|
||||
Associations
|
||||
} from "~/types";
|
||||
import GlobalApi from "~/logic/api/global";
|
||||
import { StatelessAsyncAction } from "~/views/components/StatelessAsyncAction";
|
||||
import { GroupNotification } from "./group";
|
||||
import { GraphNotification } from "./graph";
|
||||
|
||||
interface NotificationProps {
|
||||
notification: IndexedNotification;
|
||||
time: BigInteger;
|
||||
associations: Associations;
|
||||
api: GlobalApi;
|
||||
archived: boolean;
|
||||
graphConfig: NotificationGraphConfig;
|
||||
groupConfig: GroupNotificationsConfig;
|
||||
}
|
||||
|
||||
function getMuted(
|
||||
idx: NotifIndex,
|
||||
groups: GroupNotificationsConfig,
|
||||
graphs: NotificationGraphConfig
|
||||
) {
|
||||
if ("graph" in idx) {
|
||||
const { graph } = idx.graph;
|
||||
return _.findIndex(graphs?.watching || [], (g) => g === graph) === -1;
|
||||
}
|
||||
if ("group" in idx) {
|
||||
return _.findIndex(groups || [], (g) => g === idx.group.group) === -1;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
function NotificationWrapper(props: {
|
||||
api: GlobalApi;
|
||||
time: BigInteger;
|
||||
notif: IndexedNotification;
|
||||
children: ReactNode;
|
||||
archived: boolean;
|
||||
graphConfig: NotificationGraphConfig;
|
||||
groupConfig: GroupNotificationsConfig;
|
||||
}) {
|
||||
const { api, time, notif, children } = props;
|
||||
|
||||
const onArchive = useCallback(async () => {
|
||||
return api.hark.archive(time, notif.index);
|
||||
}, [time, notif]);
|
||||
|
||||
const isMuted = getMuted(notif.index, props.groupConfig, props.graphConfig);
|
||||
|
||||
const onChangeMute = useCallback(async () => {
|
||||
const func = isMuted ? "unmute" : "mute";
|
||||
return api.hark[func](notif.index);
|
||||
}, [notif, api, isMuted]);
|
||||
|
||||
const changeMuteDesc = isMuted ? "Unmute" : "Mute";
|
||||
return (
|
||||
<Row alignItems="center" justifyContent="space-between">
|
||||
{children}
|
||||
<Row gapX="2" p="2" alignItems="center">
|
||||
<StatelessAsyncAction name={changeMuteDesc} onClick={onChangeMute}>
|
||||
{changeMuteDesc}
|
||||
</StatelessAsyncAction>
|
||||
{!props.archived && (
|
||||
<StatelessAsyncAction name={time.toString()} onClick={onArchive}>
|
||||
Archive
|
||||
</StatelessAsyncAction>
|
||||
)}
|
||||
</Row>
|
||||
</Row>
|
||||
);
|
||||
}
|
||||
|
||||
export function Notification(props: NotificationProps) {
|
||||
const { notification, associations, archived } = props;
|
||||
const { read, contents, time } = notification.notification;
|
||||
|
||||
if ("graph" in notification.index) {
|
||||
const index = notification.index.graph;
|
||||
const c: GraphNotificationContents = (contents as any).graph;
|
||||
|
||||
return (
|
||||
<NotificationWrapper
|
||||
archived={archived}
|
||||
notif={notification}
|
||||
time={props.time}
|
||||
api={props.api}
|
||||
graphConfig={props.graphConfig}
|
||||
groupConfig={props.groupConfig}
|
||||
>
|
||||
<GraphNotification
|
||||
api={props.api}
|
||||
index={index}
|
||||
contents={c}
|
||||
contacts={props.contacts}
|
||||
read={read}
|
||||
archived={archived}
|
||||
timebox={props.time}
|
||||
time={time}
|
||||
associations={associations}
|
||||
/>
|
||||
</NotificationWrapper>
|
||||
);
|
||||
}
|
||||
if ("group" in notification.index) {
|
||||
const index = notification.index.group;
|
||||
const c: GroupNotificationContents = (contents as any).group;
|
||||
return (
|
||||
<NotificationWrapper
|
||||
archived={archived}
|
||||
notif={notification}
|
||||
time={props.time}
|
||||
api={props.api}
|
||||
graphConfig={props.graphConfig}
|
||||
groupConfig={props.groupConfig}
|
||||
>
|
||||
<GroupNotification
|
||||
api={props.api}
|
||||
index={index}
|
||||
contents={c}
|
||||
contacts={props.contacts}
|
||||
read={read}
|
||||
timebox={props.time}
|
||||
archived={archived}
|
||||
time={time}
|
||||
associations={associations}
|
||||
/>
|
||||
</NotificationWrapper>
|
||||
);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
93
pkg/interface/src/views/apps/notifications/notifications.tsx
Normal file
93
pkg/interface/src/views/apps/notifications/notifications.tsx
Normal file
@ -0,0 +1,93 @@
|
||||
import React, { useCallback } from "react";
|
||||
import { Box, Col, Text, Row } from "@tlon/indigo-react";
|
||||
import { Link, Switch, Route } from "react-router-dom";
|
||||
|
||||
import { Body } from "~/views/components/Body";
|
||||
import { PropFunc } from "~/types/util";
|
||||
import Inbox from "./inbox";
|
||||
import NotificationPreferences from "./preferences";
|
||||
|
||||
const baseUrl = "/~notifications";
|
||||
|
||||
const HeaderLink = (
|
||||
props: PropFunc<typeof Text> & { view?: string; current: string }
|
||||
) => {
|
||||
const { current, view, ...textProps } = props;
|
||||
const to = view ? `${baseUrl}/${view}` : baseUrl;
|
||||
const active = view ? current === view : !current;
|
||||
|
||||
return (
|
||||
<Link to={to}>
|
||||
<Text px="2" {...textProps} gray={!active} />
|
||||
</Link>
|
||||
);
|
||||
};
|
||||
|
||||
export default function NotificationsScreen(props: any) {
|
||||
const relativePath = (p: string) => baseUrl + p;
|
||||
return (
|
||||
<Switch>
|
||||
<Route
|
||||
path={[relativePath("/:view"), relativePath("")]}
|
||||
render={(routeProps) => {
|
||||
const { view } = routeProps.match.params;
|
||||
return (
|
||||
<Body>
|
||||
<Col height="100%">
|
||||
<Row
|
||||
p="3"
|
||||
alignItems="center"
|
||||
height="48px"
|
||||
justifyContent="space-between"
|
||||
width="100%"
|
||||
borderBottom="1"
|
||||
borderBottomColor="washedGray"
|
||||
>
|
||||
<Box>Updates </Box>
|
||||
<Row>
|
||||
<Box>
|
||||
<HeaderLink current={view} view="">
|
||||
Inbox
|
||||
</HeaderLink>
|
||||
</Box>
|
||||
<Box>
|
||||
<HeaderLink current={view} view="archive">
|
||||
Archive
|
||||
</HeaderLink>
|
||||
</Box>
|
||||
<Box>
|
||||
<HeaderLink current={view} view="preferences">
|
||||
Preferences
|
||||
</HeaderLink>
|
||||
</Box>
|
||||
</Row>
|
||||
<Box>
|
||||
<Text mr="1" gray>
|
||||
Filter:
|
||||
</Text>
|
||||
All
|
||||
</Box>
|
||||
</Row>
|
||||
{view === "archive" && (
|
||||
<Inbox
|
||||
{...props}
|
||||
archive={props.archivedNotifications}
|
||||
showArchive
|
||||
/>
|
||||
)}
|
||||
{view === "preferences" && (
|
||||
<NotificationPreferences
|
||||
graphConfig={props.notificationsGraphConfig}
|
||||
api={props.api}
|
||||
dnd={props.doNotDisturb}
|
||||
/>
|
||||
)}
|
||||
{!view && <Inbox {...props} />}
|
||||
</Col>
|
||||
</Body>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
</Switch>
|
||||
);
|
||||
}
|
90
pkg/interface/src/views/apps/notifications/preferences.tsx
Normal file
90
pkg/interface/src/views/apps/notifications/preferences.tsx
Normal file
@ -0,0 +1,90 @@
|
||||
import React, { useCallback } from "react";
|
||||
|
||||
import { Box, Col, ManagedCheckboxField as Checkbox } from "@tlon/indigo-react";
|
||||
import { Formik, Form, FormikHelpers } from "formik";
|
||||
import * as Yup from "yup";
|
||||
import _ from "lodash";
|
||||
import { AsyncButton } from "~/views/components/AsyncButton";
|
||||
import { FormikOnBlur } from "~/views/components/FormikOnBlur";
|
||||
import { NotificationGraphConfig } from "~/types";
|
||||
import GlobalApi from "~/logic/api/global";
|
||||
|
||||
interface FormSchema {
|
||||
mentions: boolean;
|
||||
dnd: boolean;
|
||||
watchOnSelf: boolean;
|
||||
watching: string[];
|
||||
}
|
||||
|
||||
interface NotificationPreferencesProps {
|
||||
graphConfig: NotificationGraphConfig;
|
||||
dnd: boolean;
|
||||
api: GlobalApi;
|
||||
}
|
||||
|
||||
export default function NotificationPreferences(
|
||||
props: NotificationPreferencesProps
|
||||
) {
|
||||
const { graphConfig, api, dnd } = props;
|
||||
|
||||
const initialValues: FormSchema = {
|
||||
mentions: graphConfig.mentions,
|
||||
watchOnSelf: graphConfig.watchOnSelf,
|
||||
dnd,
|
||||
watching: graphConfig.watching,
|
||||
};
|
||||
|
||||
const onSubmit = useCallback(
|
||||
async (values: FormSchema, actions: FormikHelpers<FormSchema>) => {
|
||||
console.log(values);
|
||||
try {
|
||||
let promises: Promise<any>[] = [];
|
||||
if (values.mentions !== graphConfig.mentions) {
|
||||
promises.push(api.hark.setMentions(values.mentions));
|
||||
}
|
||||
if (values.watchOnSelf !== graphConfig.watchOnSelf) {
|
||||
promises.push(api.hark.setWatchOnSelf(values.watchOnSelf));
|
||||
}
|
||||
if (values.dnd !== dnd && !_.isUndefined(values.dnd)) {
|
||||
promises.push(api.hark.setDoNotDisturb(values.dnd))
|
||||
}
|
||||
|
||||
await Promise.all(promises);
|
||||
actions.setStatus({ success: null });
|
||||
actions.resetForm({ values: initialValues });
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
actions.setStatus({ error: e.message });
|
||||
}
|
||||
},
|
||||
[api, graphConfig]
|
||||
);
|
||||
|
||||
return (
|
||||
<FormikOnBlur
|
||||
initialValues={initialValues}
|
||||
onSubmit={onSubmit}
|
||||
>
|
||||
<Form>
|
||||
<Col maxWidth="384px" p="3" gapY="4">
|
||||
<Checkbox
|
||||
label="Do not disturb"
|
||||
id="dnd"
|
||||
caption="You won't seee the notification badge, but notifications will still appear in your inbox."
|
||||
/>
|
||||
|
||||
<Checkbox
|
||||
label="Watch for replies"
|
||||
id="watchOnSelf"
|
||||
caption="Automatically follow a post for notifications when it's yours"
|
||||
/>
|
||||
<Checkbox
|
||||
label="Watch for mentions"
|
||||
id="mentions"
|
||||
caption="Notify me if someone mentions my @p in a channel I've joined"
|
||||
/>
|
||||
</Col>
|
||||
</Form>
|
||||
</FormikOnBlur>
|
||||
);
|
||||
}
|
@ -25,6 +25,11 @@ const StatusBar = (props) => {
|
||||
<Icon icon='Home' color='black'/>
|
||||
</StatusBarItem>
|
||||
<StatusBarItem mr={2} onClick={() => props.api.local.setOmnibox()}>
|
||||
{ !props.doNotDisturb && props.notificationsCount > 0 &&
|
||||
(<Box display="block" right="-8px" top="-8px" position="absolute" >
|
||||
<Icon color="blue" icon="Bullet" />
|
||||
</Box>
|
||||
)}
|
||||
<Icon icon='LeapArrow'/>
|
||||
<Text ml={2} color='black'>
|
||||
Leap
|
||||
|
Loading…
Reference in New Issue
Block a user