mirror of
https://github.com/ilyakooo0/urbit.git
synced 2024-12-16 02:22:12 +03:00
notifications: refactor graph notification to match spec
This commit is contained in:
parent
08028efcd7
commit
992b607e3c
@ -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>
|
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user