notifications: refactor graph notification to match spec

This commit is contained in:
Liam Fitzgerald 2021-04-16 16:01:05 +10:00
parent 08028efcd7
commit 992b607e3c
No known key found for this signature in database
GPG Key ID: D390E12C61D1CFFB

View File

@ -1,65 +1,80 @@
import React, { ReactNode, useCallback } from 'react'; import React, { ReactNode, useCallback } from "react";
import moment from 'moment'; import moment from "moment";
import { Row, Box, Col, Text, Anchor, Icon, Action } from '@tlon/indigo-react'; import { Row, Box, Col, Text, Anchor, Icon, Action } from "@tlon/indigo-react";
import { Link, useHistory } from 'react-router-dom'; import { Link, useHistory } from "react-router-dom";
import _ from 'lodash'; import _ from "lodash";
import { import {
GraphNotifIndex, GraphNotifIndex,
GraphNotificationContents, GraphNotificationContents,
Associations, Associations,
Rolodex, Rolodex,
Groups Groups,
} from '~/types'; } from "~/types";
import { Header } from './header'; import { Header } from "./header";
import { cite, deSig, pluralize, useShowNickname } from '~/logic/lib/util'; import {
import Author from '~/views/components/Author'; cite,
import GlobalApi from '~/logic/api/global'; deSig,
import { getSnippet } from '~/logic/lib/publish'; pluralize,
import styled from 'styled-components'; useShowNickname,
import { MentionText } from '~/views/components/MentionText'; isDm,
import ChatMessage from '../chat/components/ChatMessage'; } from "~/logic/lib/util";
import useContactState from '~/logic/state/contact'; import { GraphContentWide } from "~/views/landscape/components/Graph/GraphContentWide";
import useGroupState from '~/logic/state/group'; import Author from "~/views/components/Author";
import useMetadataState from '~/logic/state/metadata'; import GlobalApi from "~/logic/api/global";
import {PermalinkEmbed} from '../permalinks/embed'; import styled from "styled-components";
import {parsePermalink, referenceToPermalink} from '~/logic/lib/permalinks'; import useContactState from "~/logic/state/contact";
import useGroupState from "~/logic/state/group";
import useMetadataState, {
useAssocForGraph,
useAssocForGroup,
} from "~/logic/state/metadata";
import { PermalinkEmbed } from "../permalinks/embed";
import { parsePermalink, referenceToPermalink } from "~/logic/lib/permalinks";
import { Post, Group, Association } from "@urbit/api";
import { BigInteger } from "big-integer";
const TruncBox = styled(Box)<{ truncate?: number }>`
-webkit-line-clamp: ${(p) => p.truncate ?? "unset"};
display: -webkit-box;
overflow: hidden;
-webkit-box-orient: vertical;
color: ${(p) => p.theme.colors.black};
`;
function getGraphModuleIcon(module: string) { function getGraphModuleIcon(module: string) {
if (module === 'link') { if (module === "link") {
return 'Collection'; return "Collection";
} }
if(module === 'post') { if (module === "post") {
return 'Groups'; return "Groups";
} }
return _.capitalize(module); return _.capitalize(module);
} }
const FilterBox = styled(Box)` function describeNotification(
background: linear-gradient( description: string,
to bottom, plural: boolean,
transparent, isDm: boolean,
${(p) => p.theme.colors.white} singleAuthor: boolean
); ): string {
`;
function describeNotification(description: string, plural: boolean): string {
switch (description) { switch (description) {
case 'post': case "post":
return 'replied to you'; return singleAuthor ? "replied to you" : "Your post received replies";
case 'link': case "link":
return `added ${pluralize('new link', plural)} to`; return `New link${plural ? "s" : ""} in`;
case 'comment': case "comment":
return `left ${pluralize('comment', plural)} on`; return `New comment${plural ? "s" : ""} on`;
case 'edit-comment': case "note":
return `updated ${pluralize('comment', plural)} on`; return `New Note${plural ? "s" : ""} in`;
case 'note': case "edit-note":
return `posted ${pluralize('note', plural)} to`; return `updated ${pluralize("note", plural)} in`;
case 'edit-note': case "mention":
return `updated ${pluralize('note', plural)} in`; return singleAuthor ? "mentioned you in" : "You were mentioned in";
case 'mention': case "message":
return 'mentioned you on'; if (isDm) {
case 'message': return "messaged you";
return `sent ${pluralize('message', plural)} to`; }
return `New message${plural ? "s" : ""} in`;
default: default:
return description; return description;
} }
@ -67,105 +82,88 @@ function describeNotification(description: string, plural: boolean): string {
const GraphUrl = ({ contents, api }) => { const GraphUrl = ({ contents, api }) => {
const [{ text }, link] = contents; const [{ text }, link] = contents;
if('reference' in link) { if ("reference" in link) {
return ( return (
<PermalinkEmbed <PermalinkEmbed
transcluded={1} transcluded={1}
link={referenceToPermalink(link).link} link={referenceToPermalink(link).link}
api={api} api={api}
showOurContact showOurContact
/>); />
);
} }
return ( return (
<Box borderRadius='2' p='2' bg='scales.black05'> <Box borderRadius="2" p="2" bg="scales.black05">
<Anchor underline={false} target='_blank' color='black' href={link.url}> <Anchor underline={false} target="_blank" color="black" href={link.url}>
<Icon verticalAlign='bottom' mr='2' icon='ArrowExternal' /> <Icon verticalAlign="bottom" mr="2" icon="ArrowExternal" />
{text} {text}
</Anchor> </Anchor>
</Box> </Box>
); );
};
function ContentSummary({ icon, name, author, to }) {
return (
<Link to={to}>
<Col
gapY="1"
flexDirection={["column", "row"]}
alignItems={["flex-start", "center"]}
>
<Row
alignItems="center"
gapX="2"
p="1"
width="fit-content"
borderRadius="2"
border="1"
borderColor="lightGray"
>
<Icon display="block" icon={icon} />
<Text verticalAlign="baseline" fontWeight="medium">
{name}
</Text>
</Row>
<Row ml={[0, 1]} alignItems="center">
<Text lineHeight="1" fontWeight="medium" mr="1">
by
</Text>
<Author
sigilPadding={6}
size={24}
dontShowTime
ship={author}
showImage
/>
</Row>
</Col>
</Link>
);
} }
export const GraphNodeContent = ({ export const GraphNodeContent = ({ post, mod, index, hidden, association }) => {
group,
association,
post,
mod,
index,
}) => {
const { contents } = post; const { contents } = post;
const idx = index.slice(1).split('/'); const idx = index.slice(1).split("/");
if (mod === 'link') { const { group, resource } = association;
if (idx.length === 1) { const url = getNodeUrl(mod, hidden, group, resource, index);
return <GraphUrl contents={contents} />; if (mod === "link" && idx.length === 1) {
} else if (idx.length === 3) { const [{ text: title }] = contents;
return <MentionText content={contents} group={group} />;
}
return null;
}
if (mod === 'publish') {
if (idx[1] === '2') {
return (
<MentionText
content={contents}
group={group}
fontSize='14px'
lineHeight='tall'
/>
);
} else if (idx[1] === '1') {
const [{ text: header }, { text: body }] = contents;
const snippet = getSnippet(body);
return (
<Col>
<Box mb='2' fontWeight='500'>
<Text>{header}</Text>
</Box>
<Box overflow='hidden' maxHeight='400px' position='relative'>
<Text lineHeight='tall'>{snippet}</Text>
<FilterBox
width='100%'
zIndex='1'
height='calc(100% - 2em)'
bottom='-4px'
position='absolute'
/>
</Box>
</Col>
);
}
}
if(mod === 'post') {
return <MentionText content={contents} group={group} />;
}
if (mod === 'chat') {
return ( return (
<Row <ContentSummary to={url} icon="Links" name={title} author={post.author} />
width='100%'
flexShrink={0}
flexGrow={1}
flexWrap='wrap'
marginLeft='-32px'
>
<ChatMessage
renderSigil={false}
containerClass='items-top cf hide-child'
group={group}
groups={{}}
association={association}
associations={{ graph: {}, groups: {} }}
msg={post}
fontSize='0'
pt='2'
hideHover={true}
/>
</Row>
); );
} }
return null; if (mod === "publish" && idx[1] === "1") {
const [{ text: title }] = contents;
return (
<ContentSummary to={url} icon="Note" name={title} author={post.author} />
);
}
return (
<TruncBox truncate={8}>
<GraphContentWide api={{} as any} post={post} showOurContact />
</TruncBox>
);
}; };
function getNodeUrl( function getNodeUrl(
@ -175,78 +173,103 @@ function getNodeUrl(
graph: string, graph: string,
index: string index: string
) { ) {
if (hidden && mod === 'chat') { if (hidden && mod === "chat") {
groupPath = '/messages'; groupPath = "/messages";
} else if (hidden) { } else if (hidden) {
groupPath = '/home'; groupPath = "/home";
} }
const graphUrl = `/~landscape${groupPath}/resource/${mod}${graph}`; const graphUrl = `/~landscape${groupPath}/resource/${mod}${graph}`;
const idx = index.slice(1).split('/'); const idx = index.slice(1).split("/");
if (mod === 'publish') { if (mod === "publish") {
const [noteId] = idx; console.log(idx);
return `${graphUrl}/note/${noteId}`; const [noteId, kind, commId] = idx;
} else if (mod === 'link') { const selected = kind === "2" ? `?selected=${commId}` : "";
const [linkId] = idx; return `${graphUrl}/note/${noteId}${selected}`;
return `${graphUrl}/index/${linkId}`; } else if (mod === "link") {
} else if (mod === 'chat') { const [linkId, commId] = idx;
if(idx.length > 0) { return `${graphUrl}/index/${linkId}${commId ? `?selected=${commId}` : ""}`;
} else if (mod === "chat") {
if (idx.length > 0) {
return `${graphUrl}?msg=${idx[0]}`; return `${graphUrl}?msg=${idx[0]}`;
} }
return graphUrl; return graphUrl;
} else if( mod === 'post') { } else if (mod === "post") {
return `/~landscape${groupPath}/feed${index}`; return `/~landscape${groupPath}/feed${index}`;
} }
return ''; return "";
} }
const GraphNode = ({
post,
author,
mod,
description,
time,
index,
graph,
groupPath,
group,
read,
onRead,
showContact = false
}) => {
author = deSig(author);
const history = useHistory();
const contacts = useContactState((state) => state.contacts);
const nodeUrl = getNodeUrl(mod, group?.hidden, groupPath, graph, index); interface PostsByAuthor {
const association = useMetadataState( author: string;
useCallback(s => s.associations.graph[graph], [graph]) posts: Post[];
}
const GraphNodes = (props: {
posts: Post[];
graph: string;
hideAuthors?: boolean;
group?: Group;
groupPath: string;
description: string;
index: string;
mod: string;
association: Association;
hidden: boolean;
}) => {
const {
posts,
mod,
hidden,
index,
description,
hideAuthors = false,
association,
} = props;
const postsByConsecAuthor = _.reduce(
posts,
(acc: PostsByAuthor[], val: Post, key: number) => {
const lent = acc.length;
if (lent > 0 && acc?.[lent - 1]?.author === val.author) {
const last = acc[lent - 1];
const rest = acc.slice(0, -1);
return [...rest, { ...last, posts: [...last.posts, val] }];
}
return [...acc, { author: val.author, posts: [val] }];
},
[]
); );
const onClick = useCallback(() => {
if (!read) {
onRead();
}
history.push(nodeUrl);
}, [read, onRead]);
return ( return (
<Row onClick={onClick} gapX='2' pt={showContact ? 2 : 0}> <>
<Col flexGrow={1} alignItems='flex-start'> {_.map(postsByConsecAuthor, ({ posts, author }, idx) => {
{showContact && ( const time = posts[0]?.["time-sent"];
<Author showImage ship={author} date={time} group={group} /> return (
)} <Col key={idx} flexGrow={1} alignItems="flex-start">
<Row width='100%' p='1' flexDirection='column'> {!hideAuthors && (
<GraphNodeContent <Author
post={post} size={24}
mod={mod} sigilPadding={6}
description={description} showImage
association={association} ship={author}
index={index} date={time}
group={group} />
remoteContentPolicy={{}} )}
/> <Col gapY="2" py={hideAuthors ? 0 : 2} width="100%">
</Row> {_.map(posts, (post) => (
</Col> <GraphNodeContent
</Row> key={post.index}
post={post}
mod={mod}
index={index}
association={association}
hidden={hidden}
/>
))}
</Col>
</Col>
);
})}
</>
); );
}; };
@ -260,53 +283,80 @@ export function GraphNotification(props: {
api: GlobalApi; api: GlobalApi;
}) { }) {
const { contents, index, read, time, api, timebox } = props; const { contents, index, read, time, api, timebox } = props;
const history = useHistory();
const authors = _.map(contents, 'author'); const authors = _.uniq(_.map(contents, "author"));
const singleAuthor = authors.length === 1;
const { graph, group } = index; const { graph, group } = index;
const icon = getGraphModuleIcon(index.module); const association = useAssocForGraph(graph)!;
const desc = describeNotification(index.description, contents.length !== 1); const dm = isDm(graph);
const desc = describeNotification(
index.description,
contents.length !== 1,
dm,
singleAuthor
);
const groupAssociation = useAssocForGroup(association.group);
const groups = useGroupState((state) => state.groups);
const onClick = useCallback(() => { const onClick = useCallback(() => {
if (props.archived || read) { if (
return; !(
(index.description === "note" || index.description === "link") &&
index.index === "/"
)
) {
const first = contents[0];
const { group, resource } = association;
history.push(
getNodeUrl(
index.module,
groups[association.group]?.hidden,
group,
resource,
first.index
)
);
} }
return api.hark['read'](timebox, { graph: index });
}, [api, timebox, index, read]); }, [api, timebox, index, read]);
const groups = useGroupState((state) => state.groups); const authorsInHeader =
dm ||
((index.description === "mention" || index.description === "post") &&
singleAuthor);
const hideAuthors =
authorsInHeader ||
index.description === "note" ||
index.description === "link";
const channelTitle = dm ? undefined : association.metadata.title ?? graph;
const groupTitle = groupAssociation?.metadata?.title;
return ( return (
<> <>
<Header <Header
onClick={onClick}
archived={props.archived}
time={time} time={time}
read={read} authors={authorsInHeader ? authors : []}
authors={authors} channelTitle={channelTitle}
moduleIcon={icon}
channel={graph}
group={group}
description={desc} description={desc}
groupTitle={groupTitle}
content
/> />
<Box flexGrow={1} width='100%' pl={5} gridArea='main'> <Col onClick={onClick} gapY="2" flexGrow={1} width="100%" gridArea="main">
{_.map(contents, (content, idx) => ( <GraphNodes
<GraphNode hideAuthors={hideAuthors}
post={content} posts={contents.slice(0, 4)}
author={content.author} mod={index.module}
mod={index.module} description={index.description}
time={content?.['time-sent']} index={contents?.[0].index}
description={index.description} association={association}
index={content.index} hidden={groups[association.group]?.hidden}
graph={graph} />
group={groups[group]} {contents.length > 4 && (
groupPath={group} <Text mb="2" gray>
read={read} + {contents.length - 4} more
onRead={onClick} </Text>
showContact={idx === 0} )}
/> </Col>
))}
</Box>
</> </>
); );
} }