mirror of
https://github.com/ilyakooo0/urbit.git
synced 2024-12-15 18:12:47 +03:00
Merge pull request #4774 from urbit/lf/notif-v2
notifications: FE refactor/redesign
This commit is contained in:
commit
6e77821a09
@ -41,6 +41,12 @@ export function useLazyScroll(
|
|||||||
}
|
}
|
||||||
}, [count]);
|
}, [count]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if(!ready) {
|
||||||
|
setIsDone(false);
|
||||||
|
}
|
||||||
|
}, [ready]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!ref.current || isDone || !ready) {
|
if (!ref.current || isDone || !ready) {
|
||||||
return;
|
return;
|
||||||
@ -58,7 +64,7 @@ export function useLazyScroll(
|
|||||||
return () => {
|
return () => {
|
||||||
ref.current?.removeEventListener('scroll', onScroll);
|
ref.current?.removeEventListener('scroll', onScroll);
|
||||||
};
|
};
|
||||||
}, [ref?.current, count, ready]);
|
}, [ref?.current, ready, isDone]);
|
||||||
|
|
||||||
return { isDone, isLoading };
|
return { isDone, isLoading };
|
||||||
}
|
}
|
||||||
|
@ -10,7 +10,7 @@ export function useRunIO<I, O>(
|
|||||||
io: (i: I) => Promise<O>,
|
io: (i: I) => Promise<O>,
|
||||||
after: (o: O) => void,
|
after: (o: O) => void,
|
||||||
key: string
|
key: string
|
||||||
) {
|
): () => Promise<void> {
|
||||||
const [resolve, setResolve] = useState<() => void>(() => () => {});
|
const [resolve, setResolve] = useState<() => void>(() => () => {});
|
||||||
const [reject, setReject] = useState<(e: any) => void>(() => () => {});
|
const [reject, setReject] = useState<(e: any) => void>(() => () => {});
|
||||||
const [output, setOutput] = useState<O | null>(null);
|
const [output, setOutput] = useState<O | null>(null);
|
||||||
|
@ -63,6 +63,16 @@ export function unixToDa(unix: number) {
|
|||||||
return DA_UNIX_EPOCH.add(timeSinceEpoch);
|
return DA_UNIX_EPOCH.add(timeSinceEpoch);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function dmCounterparty(resource: string) {
|
||||||
|
const [,,ship,name] = resource.split('/');
|
||||||
|
return ship === `~${window.ship}` ? `~${name.slice(4)}` : ship;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isDm(resource: string) {
|
||||||
|
const [,,,name] = resource.split('/');
|
||||||
|
return name.startsWith('dm--');
|
||||||
|
}
|
||||||
|
|
||||||
export function makePatDa(patda: string) {
|
export function makePatDa(patda: string) {
|
||||||
return bigInt(udToDec(patda));
|
return bigInt(udToDec(patda));
|
||||||
}
|
}
|
||||||
|
@ -16,7 +16,7 @@ const useGroupState = createState<GroupState>('Group', {
|
|||||||
}, ['groups']);
|
}, ['groups']);
|
||||||
|
|
||||||
export function useGroup(group: string) {
|
export function useGroup(group: string) {
|
||||||
return useGroupState(useCallback(s => s.groups[group], [group]));
|
return useGroupState(useCallback(s => s.groups[group] as Group | undefined, [group]));
|
||||||
}
|
}
|
||||||
|
|
||||||
export function useGroupForAssoc(association: Association) {
|
export function useGroupForAssoc(association: Association) {
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
import { MetadataUpdatePreview, Associations } from "@urbit/api";
|
import { useCallback } from 'react';
|
||||||
|
import { MetadataUpdatePreview, Association, Associations } from "@urbit/api";
|
||||||
|
|
||||||
import { BaseState, createState } from "./base";
|
import { BaseState, createState } from "./base";
|
||||||
|
|
||||||
@ -9,6 +10,14 @@ export interface MetadataState extends BaseState<MetadataState> {
|
|||||||
// preview: (group: string) => Promise<MetadataUpdatePreview>;
|
// preview: (group: string) => Promise<MetadataUpdatePreview>;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export function useAssocForGraph(graph: string) {
|
||||||
|
return useMetadataState(useCallback(s => s.associations.graph[graph] as Association | undefined, [graph]));
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useAssocForGroup(group: string) {
|
||||||
|
return useMetadataState(useCallback(s => s.associations.groups[group] as Association | undefined, [group]));
|
||||||
|
}
|
||||||
|
|
||||||
const useMetadataState = createState<MetadataState>('Metadata', {
|
const useMetadataState = createState<MetadataState>('Metadata', {
|
||||||
associations: { groups: {}, graph: {}, contacts: {}, chat: {}, link: {}, publish: {} },
|
associations: { groups: {}, graph: {}, contacts: {}, chat: {}, link: {}, publish: {} },
|
||||||
// preview: async (group): Promise<MetadataUpdatePreview> => {
|
// preview: async (group): Promise<MetadataUpdatePreview> => {
|
||||||
|
@ -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;
|
||||||
}
|
}
|
||||||
@ -68,104 +83,87 @@ 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>
|
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -12,6 +12,7 @@ import {
|
|||||||
|
|
||||||
import { Header } from './header';
|
import { Header } from './header';
|
||||||
import GlobalApi from '~/logic/api/global';
|
import GlobalApi from '~/logic/api/global';
|
||||||
|
import {useAssocForGroup} from '~/logic/state/metadata';
|
||||||
|
|
||||||
function describeNotification(description: string, plural: boolean) {
|
function describeNotification(description: string, plural: boolean) {
|
||||||
switch (description) {
|
switch (description) {
|
||||||
@ -52,23 +53,16 @@ export function GroupNotification(props: GroupNotificationProps): ReactElement {
|
|||||||
const { group } = index;
|
const { group } = index;
|
||||||
const desc = describeNotification(index.description, contents.length !== 1);
|
const desc = describeNotification(index.description, contents.length !== 1);
|
||||||
|
|
||||||
const onClick = useCallback(() => {
|
const association = useAssocForGroup(group)
|
||||||
if (props.archived) {
|
const groupTitle = association?.metadata?.title ?? group;
|
||||||
return;
|
|
||||||
}
|
|
||||||
const func = read ? 'unread' : 'read';
|
|
||||||
return api.hark[func](timebox, { group: index });
|
|
||||||
}, [api, timebox, index, read]);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Col onClick={onClick} p="2">
|
<Col>
|
||||||
<Header
|
<Header
|
||||||
archived={props.archived}
|
|
||||||
time={time}
|
time={time}
|
||||||
read={read}
|
|
||||||
group={group}
|
|
||||||
authors={authors}
|
authors={authors}
|
||||||
description={desc}
|
description={desc}
|
||||||
|
groupTitle={groupTitle}
|
||||||
/>
|
/>
|
||||||
</Col>
|
</Col>
|
||||||
);
|
);
|
||||||
|
@ -1,103 +1,90 @@
|
|||||||
import React, { ReactElement } from 'react';
|
import React, { ReactElement } from "react";
|
||||||
import f from 'lodash/fp';
|
import _ from "lodash";
|
||||||
import _ from 'lodash';
|
import moment from "moment";
|
||||||
import moment from 'moment';
|
import { Text as NormalText, Row, Rule, Box, Col } from "@tlon/indigo-react";
|
||||||
|
|
||||||
import { Text as NormalText, Row, Icon, Rule } from '@tlon/indigo-react';
|
import { PropFunc } from "~/types/util";
|
||||||
import { Associations, Contact, Contacts, Rolodex } from '@urbit/api';
|
import Timestamp from "~/views/components/Timestamp";
|
||||||
|
import Author from "~/views/components/Author";
|
||||||
import { PropFunc } from '~/types/util';
|
import Dot from "~/views/components/Dot";
|
||||||
import { useShowNickname } from '~/logic/lib/util';
|
|
||||||
import Timestamp from '~/views/components/Timestamp';
|
|
||||||
import useContactState from '~/logic/state/contact';
|
|
||||||
import useMetadataState from '~/logic/state/metadata';
|
|
||||||
|
|
||||||
const Text = (props: PropFunc<typeof Text>) => (
|
const Text = (props: PropFunc<typeof Text>) => (
|
||||||
<NormalText fontWeight="500" {...props} />
|
<NormalText fontWeight="500" {...props} />
|
||||||
);
|
);
|
||||||
|
|
||||||
function Author(props: { patp: string; last?: boolean }): ReactElement {
|
export function Header(
|
||||||
const contacts = useContactState(state => state.contacts);
|
props: {
|
||||||
const contact: Contact | undefined = contacts?.[`~${props.patp}`];
|
channelTitle?: string;
|
||||||
|
groupTitle?: string;
|
||||||
const showNickname = useShowNickname(contact);
|
description: string;
|
||||||
const name = showNickname ? contact.nickname : `~${props.patp}`;
|
time?: number;
|
||||||
|
authors?: string[];
|
||||||
|
content?: boolean;
|
||||||
|
} & PropFunc<typeof Row>
|
||||||
|
): ReactElement {
|
||||||
|
const {
|
||||||
|
description,
|
||||||
|
channelTitle = "",
|
||||||
|
groupTitle,
|
||||||
|
authors = [],
|
||||||
|
content = false,
|
||||||
|
time,
|
||||||
|
} = props;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Text mono={!showNickname}>
|
<Row
|
||||||
{name}
|
flexDirection={["column-reverse", "row"]}
|
||||||
{!props.last && ', '}
|
minHeight="4"
|
||||||
</Text>
|
mb={content ? 2 : 0}
|
||||||
);
|
onClick={props.onClick}
|
||||||
}
|
flexWrap="wrap"
|
||||||
|
alignItems={["flex-start", "center"]}
|
||||||
export function Header(props: {
|
gridArea="header"
|
||||||
authors: string[];
|
overflow="hidden"
|
||||||
archived?: boolean;
|
>
|
||||||
channel?: string;
|
<Row gapX="1" overflow="hidden" alignItems="center">
|
||||||
group: string;
|
{authors.length > 0 && (
|
||||||
description: string;
|
<>
|
||||||
moduleIcon?: string;
|
<Author
|
||||||
time: number;
|
flexShrink={0}
|
||||||
read: boolean;
|
sigilPadding={6}
|
||||||
} & PropFunc<typeof Row> ): ReactElement {
|
size={24}
|
||||||
const { description, channel, moduleIcon, read } = props;
|
dontShowTime
|
||||||
const associations = useMetadataState(state => state.associations);
|
date={time}
|
||||||
|
ship={authors[0]}
|
||||||
const authors = _.uniq(props.authors);
|
showImage
|
||||||
|
/>
|
||||||
const authorDesc = f.flow(
|
{authors.length > 1 && (
|
||||||
f.take(3),
|
<Text lineHeight="tall">+ {authors.length - 1} more</Text>
|
||||||
f.entries,
|
)}
|
||||||
f.map(([idx, p]: [string, string]) => {
|
</>
|
||||||
const lent = Math.min(3, authors.length);
|
)}
|
||||||
const last = lent - 1 === parseInt(idx, 10);
|
<Box whiteSpace="nowrap" overflow="hidden" textOverflow="ellipsis">
|
||||||
return <Author key={idx} patp={p} last={last} />;
|
<Text lineHeight="tall" mr="1">
|
||||||
}),
|
{description} {channelTitle}
|
||||||
auths => (
|
</Text>
|
||||||
<React.Fragment>
|
</Box>
|
||||||
{auths}
|
</Row>
|
||||||
|
<Row ml={[0, 1]} mb={[1, 0]} gapX="1" alignItems="center">
|
||||||
{authors.length > 3 &&
|
{groupTitle && (
|
||||||
` and ${authors.length - 3} other${authors.length === 4 ? '' : 's'}`}
|
<>
|
||||||
</React.Fragment>
|
<Text lineHeight="tall" fontSize="1" gray>
|
||||||
)
|
{groupTitle}
|
||||||
)(authors);
|
</Text>
|
||||||
|
<Dot color="gray" />
|
||||||
const time = moment(props.time).format('HH:mm');
|
</>
|
||||||
const groupTitle =
|
)}
|
||||||
associations.groups?.[props.group]?.metadata?.title;
|
{time && (
|
||||||
|
<Timestamp
|
||||||
const app = 'graph';
|
lineHeight="tall"
|
||||||
const channelTitle =
|
fontSize="1"
|
||||||
(channel && associations?.[app]?.[channel]?.metadata?.title) ||
|
relative
|
||||||
channel;
|
stamp={moment(time)}
|
||||||
|
color="gray"
|
||||||
return (
|
date={false}
|
||||||
<Row onClick={props.onClick} p="2" flexWrap="wrap" alignItems="center" gridArea="header">
|
/>
|
||||||
{!props.archived && (
|
)}
|
||||||
<Icon
|
</Row>
|
||||||
display="block"
|
|
||||||
opacity={read ? 0 : 1}
|
|
||||||
mr={2}
|
|
||||||
icon="Bullet"
|
|
||||||
color="blue"
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
<Text mr="1" mono>
|
|
||||||
{authorDesc}
|
|
||||||
</Text>
|
|
||||||
<Text mr="1">{description}</Text>
|
|
||||||
{Boolean(moduleIcon) && <Icon icon={moduleIcon as any} mr={1} />}
|
|
||||||
{Boolean(channel) && <Text fontWeight="500" mr={1}>{channelTitle}</Text>}
|
|
||||||
<Rule vertical height="12px" mr={1} />
|
|
||||||
{groupTitle &&
|
|
||||||
<>
|
|
||||||
<Text fontWeight="500" mr={1}>{groupTitle}</Text>
|
|
||||||
<Rule vertical height="12px" mr={1} />
|
|
||||||
</>
|
|
||||||
}
|
|
||||||
<Timestamp stamp={moment(props.time)} color="lightGray" date={false} />
|
|
||||||
</Row>
|
</Row>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -121,7 +121,7 @@ export default function Inbox(props: {
|
|||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Col ref={scrollRef} position="relative" height="100%" overflowY="auto">
|
<Col p="1" ref={scrollRef} position="relative" height="100%" overflowY="auto">
|
||||||
<Invites pendingJoin={props.pendingJoin} api={api} />
|
<Invites pendingJoin={props.pendingJoin} api={api} />
|
||||||
{[...notificationsByDayMap.keys()].sort().reverse().map((day, index) => {
|
{[...notificationsByDayMap.keys()].sort().reverse().map((day, index) => {
|
||||||
const timeboxes = notificationsByDayMap.get(day)!;
|
const timeboxes = notificationsByDayMap.get(day)!;
|
||||||
@ -175,26 +175,15 @@ function DaySection({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Box position="sticky" zIndex={3} top="-1px" bg="white">
|
|
||||||
<Box p="2" bg="scales.black05">
|
|
||||||
<Text>
|
|
||||||
{label}
|
|
||||||
</Text>
|
|
||||||
</Box>
|
|
||||||
</Box>
|
|
||||||
{_.map(timeboxes.sort(sortTimeboxes), ([date, nots], i: number) =>
|
{_.map(timeboxes.sort(sortTimeboxes), ([date, nots], i: number) =>
|
||||||
_.map(nots.sort(sortIndexedNotification), (not, j: number) => (
|
_.map(nots.sort(sortIndexedNotification), (not, j: number) => (
|
||||||
<React.Fragment key={j}>
|
<Notification
|
||||||
{(i !== 0 || j !== 0) && (
|
key={j}
|
||||||
<Box flexShrink={0} height="4px" bg="scales.black05" />
|
api={api}
|
||||||
)}
|
notification={not}
|
||||||
<Notification
|
archived={archive}
|
||||||
api={api}
|
time={date}
|
||||||
notification={not}
|
/>
|
||||||
archived={archive}
|
|
||||||
time={date}
|
|
||||||
/>
|
|
||||||
</React.Fragment>
|
|
||||||
))
|
))
|
||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
|
@ -59,13 +59,6 @@ export function Invites(props: InvitesProps): ReactElement {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{Object.keys(invitesAndStatus).length > 0 && (
|
|
||||||
<Box position="sticky" zIndex={3} top="-1px" bg="white" flexShrink="0">
|
|
||||||
<Box p="2" bg="scales.black05">
|
|
||||||
<Text>Invites</Text>
|
|
||||||
</Box>
|
|
||||||
</Box>
|
|
||||||
)}
|
|
||||||
{Object.keys(invitesAndStatus)
|
{Object.keys(invitesAndStatus)
|
||||||
.sort(alphabeticalOrder)
|
.sort(alphabeticalOrder)
|
||||||
.map((resource) => {
|
.map((resource) => {
|
||||||
@ -89,10 +82,9 @@ export function Invites(props: InvitesProps): ReactElement {
|
|||||||
invite={invite}
|
invite={invite}
|
||||||
app={app}
|
app={app}
|
||||||
uid={uid}
|
uid={uid}
|
||||||
join={join}
|
|
||||||
resource={resource}
|
resource={resource}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import React, { ReactNode, useCallback, useMemo, useState } from 'react';
|
import React, { ReactNode, useCallback, useMemo, useState } from "react";
|
||||||
import { Row, Box } from '@tlon/indigo-react';
|
import { Row, Box } from "@tlon/indigo-react";
|
||||||
import _ from 'lodash';
|
import _ from "lodash";
|
||||||
import {
|
import {
|
||||||
GraphNotificationContents,
|
GraphNotificationContents,
|
||||||
IndexedNotification,
|
IndexedNotification,
|
||||||
@ -9,16 +9,16 @@ import {
|
|||||||
GroupNotificationsConfig,
|
GroupNotificationsConfig,
|
||||||
Groups,
|
Groups,
|
||||||
Associations,
|
Associations,
|
||||||
Contacts
|
Contacts,
|
||||||
} from '@urbit/api';
|
} from "@urbit/api";
|
||||||
import GlobalApi from '~/logic/api/global';
|
import GlobalApi from "~/logic/api/global";
|
||||||
import { getParentIndex } from '~/logic/lib/notification';
|
import { getParentIndex } from "~/logic/lib/notification";
|
||||||
import { StatelessAsyncAction } from '~/views/components/StatelessAsyncAction';
|
import { StatelessAsyncAction } from "~/views/components/StatelessAsyncAction";
|
||||||
import { GroupNotification } from './group';
|
import { GroupNotification } from "./group";
|
||||||
import { GraphNotification } from './graph';
|
import { GraphNotification } from "./graph";
|
||||||
import { BigInteger } from 'big-integer';
|
import { BigInteger } from "big-integer";
|
||||||
import { useHovering } from '~/logic/lib/util';
|
import { useHovering } from "~/logic/lib/util";
|
||||||
import useHarkState from '~/logic/state/hark';
|
import useHarkState from "~/logic/state/hark";
|
||||||
|
|
||||||
interface NotificationProps {
|
interface NotificationProps {
|
||||||
notification: IndexedNotification;
|
notification: IndexedNotification;
|
||||||
@ -33,73 +33,108 @@ function getMuted(
|
|||||||
graphs: NotificationGraphConfig
|
graphs: NotificationGraphConfig
|
||||||
) {
|
) {
|
||||||
const { index, notification } = idxNotif;
|
const { index, notification } = idxNotif;
|
||||||
if ('graph' in idxNotif.index) {
|
if ("graph" in idxNotif.index) {
|
||||||
const { graph } = idxNotif.index.graph;
|
const { graph } = idxNotif.index.graph;
|
||||||
if(!('graph' in notification.contents)) {
|
if (!("graph" in notification.contents)) {
|
||||||
throw new Error();
|
throw new Error();
|
||||||
}
|
}
|
||||||
const parent = getParentIndex(index.graph, notification.contents.graph);
|
const parent = getParentIndex(index.graph, notification.contents.graph);
|
||||||
|
|
||||||
return _.findIndex(
|
return (
|
||||||
graphs?.watching || [],
|
_.findIndex(
|
||||||
g => g.graph === graph && g.index === parent
|
graphs?.watching || [],
|
||||||
) === -1;
|
(g) => g.graph === graph && g.index === parent
|
||||||
|
) === -1
|
||||||
|
);
|
||||||
}
|
}
|
||||||
if ('group' in index) {
|
if ("group" in index) {
|
||||||
return _.findIndex(groups || [], g => g === index.group.group) === -1;
|
return _.findIndex(groups || [], (g) => g === index.group.group) === -1;
|
||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
function NotificationWrapper(props: {
|
export function NotificationWrapper(props: {
|
||||||
api: GlobalApi;
|
api: GlobalApi;
|
||||||
time: BigInteger;
|
time?: BigInteger;
|
||||||
notif: IndexedNotification;
|
notification?: IndexedNotification;
|
||||||
children: ReactNode;
|
children: ReactNode;
|
||||||
archived: boolean;
|
|
||||||
}) {
|
}) {
|
||||||
const { api, time, notif, children } = props;
|
const { api, time, notification, children } = props;
|
||||||
|
|
||||||
const onArchive = useCallback(async () => {
|
const onArchive = useCallback(async () => {
|
||||||
return api.hark.archive(time, notif.index);
|
if (!(time && notification)) {
|
||||||
}, [time, notif]);
|
return;
|
||||||
|
}
|
||||||
|
return api.hark.archive(time, notification.index);
|
||||||
|
}, [time, notification]);
|
||||||
|
|
||||||
const groupConfig = useHarkState(state => state.notificationsGroupConfig);
|
const groupConfig = useHarkState((state) => state.notificationsGroupConfig);
|
||||||
const graphConfig = useHarkState(state => state.notificationsGraphConfig);
|
const graphConfig = useHarkState((state) => state.notificationsGraphConfig);
|
||||||
|
|
||||||
const isMuted = getMuted(
|
const isMuted =
|
||||||
notif,
|
time && notification && getMuted(notification, groupConfig, graphConfig);
|
||||||
groupConfig,
|
|
||||||
graphConfig
|
|
||||||
);
|
|
||||||
|
|
||||||
const onChangeMute = useCallback(async () => {
|
const onChangeMute = useCallback(async () => {
|
||||||
const func = isMuted ? 'unmute' : 'mute';
|
if (!notification) {
|
||||||
return api.hark[func](notif);
|
return;
|
||||||
}, [notif, api, isMuted]);
|
}
|
||||||
|
const func = isMuted ? "unmute" : "mute";
|
||||||
|
return api.hark[func](notification);
|
||||||
|
}, [notification, api, isMuted]);
|
||||||
|
|
||||||
|
const onClick = () => {
|
||||||
|
if (!(time && notification) || notification.notification.read) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
return api.hark.read(time, notification.index);
|
||||||
|
};
|
||||||
|
|
||||||
const { hovering, bind } = useHovering();
|
const { hovering, bind } = useHovering();
|
||||||
|
|
||||||
const changeMuteDesc = isMuted ? 'Unmute' : 'Mute';
|
const changeMuteDesc = isMuted ? "Unmute" : "Mute";
|
||||||
return (
|
return (
|
||||||
<Box
|
<Box
|
||||||
width="100%"
|
onClick={onClick}
|
||||||
|
bg={
|
||||||
|
(notification ? notification?.notification?.read : false)
|
||||||
|
? "washedGray"
|
||||||
|
: "washedBlue"
|
||||||
|
}
|
||||||
|
borderRadius={2}
|
||||||
display="grid"
|
display="grid"
|
||||||
gridTemplateColumns="1fr 200px"
|
gridTemplateColumns={["1fr", "1fr 200px"]}
|
||||||
gridTemplateRows="auto"
|
gridTemplateRows="auto"
|
||||||
gridTemplateAreas="'header actions' 'main main'"
|
gridTemplateAreas={["'header' 'main'", "'header actions' 'main main'"]}
|
||||||
pb={2}
|
p={2}
|
||||||
|
m={2}
|
||||||
{...bind}
|
{...bind}
|
||||||
>
|
>
|
||||||
{children}
|
{children}
|
||||||
<Row gapX="2" p="2" pt='3' gridArea="actions" justifyContent="flex-end" opacity={[1, hovering ? 1 : 0]}>
|
<Row
|
||||||
<StatelessAsyncAction name={changeMuteDesc} onClick={onChangeMute} backgroundColor="transparent">
|
display={["none", "flex"]}
|
||||||
{changeMuteDesc}
|
alignItems="center"
|
||||||
</StatelessAsyncAction>
|
gapX="2"
|
||||||
{!props.archived && (
|
gridArea="actions"
|
||||||
<StatelessAsyncAction name={time.toString()} onClick={onArchive} backgroundColor="transparent">
|
justifyContent="flex-end"
|
||||||
Dismiss
|
opacity={[1, hovering ? 1 : 0]}
|
||||||
</StatelessAsyncAction>
|
>
|
||||||
|
{time && notification && (
|
||||||
|
<>
|
||||||
|
<StatelessAsyncAction
|
||||||
|
name={changeMuteDesc}
|
||||||
|
onClick={onChangeMute}
|
||||||
|
backgroundColor="transparent"
|
||||||
|
>
|
||||||
|
{changeMuteDesc}
|
||||||
|
</StatelessAsyncAction>
|
||||||
|
<StatelessAsyncAction
|
||||||
|
name={time.toString()}
|
||||||
|
onClick={onArchive}
|
||||||
|
backgroundColor="transparent"
|
||||||
|
>
|
||||||
|
Dismiss
|
||||||
|
</StatelessAsyncAction>
|
||||||
|
</>
|
||||||
)}
|
)}
|
||||||
</Row>
|
</Row>
|
||||||
</Box>
|
</Box>
|
||||||
@ -110,23 +145,18 @@ export function Notification(props: NotificationProps) {
|
|||||||
const { notification, associations, archived } = props;
|
const { notification, associations, archived } = props;
|
||||||
const { read, contents, time } = notification.notification;
|
const { read, contents, time } = notification.notification;
|
||||||
|
|
||||||
const Wrapper = ({ children }) => (
|
const wrapperProps = {
|
||||||
<NotificationWrapper
|
notification,
|
||||||
archived={archived}
|
time: props.time,
|
||||||
notif={notification}
|
api: props.api,
|
||||||
time={props.time}
|
};
|
||||||
api={props.api}
|
|
||||||
>
|
|
||||||
{children}
|
|
||||||
</NotificationWrapper>
|
|
||||||
);
|
|
||||||
|
|
||||||
if ('graph' in notification.index) {
|
if ("graph" in notification.index) {
|
||||||
const index = notification.index.graph;
|
const index = notification.index.graph;
|
||||||
const c: GraphNotificationContents = (contents as any).graph;
|
const c: GraphNotificationContents = (contents as any).graph;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Wrapper>
|
<NotificationWrapper {...wrapperProps}>
|
||||||
<GraphNotification
|
<GraphNotification
|
||||||
api={props.api}
|
api={props.api}
|
||||||
index={index}
|
index={index}
|
||||||
@ -136,14 +166,14 @@ export function Notification(props: NotificationProps) {
|
|||||||
timebox={props.time}
|
timebox={props.time}
|
||||||
time={time}
|
time={time}
|
||||||
/>
|
/>
|
||||||
</Wrapper>
|
</NotificationWrapper>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
if ('group' in notification.index) {
|
if ("group" in notification.index) {
|
||||||
const index = notification.index.group;
|
const index = notification.index.group;
|
||||||
const c: GroupNotificationContents = (contents as any).group;
|
const c: GroupNotificationContents = (contents as any).group;
|
||||||
return (
|
return (
|
||||||
<Wrapper>
|
<NotificationWrapper {...wrapperProps}>
|
||||||
<GroupNotification
|
<GroupNotification
|
||||||
api={props.api}
|
api={props.api}
|
||||||
index={index}
|
index={index}
|
||||||
@ -153,7 +183,7 @@ export function Notification(props: NotificationProps) {
|
|||||||
archived={archived}
|
archived={archived}
|
||||||
time={time}
|
time={time}
|
||||||
/>
|
/>
|
||||||
</Wrapper>
|
</NotificationWrapper>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -18,7 +18,7 @@ import { PropFunc } from '~/types';
|
|||||||
|
|
||||||
interface AuthorProps {
|
interface AuthorProps {
|
||||||
ship: string;
|
ship: string;
|
||||||
date: number;
|
date?: number;
|
||||||
showImage?: boolean;
|
showImage?: boolean;
|
||||||
children?: ReactNode;
|
children?: ReactNode;
|
||||||
unread?: boolean;
|
unread?: boolean;
|
||||||
@ -113,11 +113,13 @@ export default function Author(props: AuthorProps & PropFunc<typeof Box>): React
|
|||||||
lineHeight='tall'
|
lineHeight='tall'
|
||||||
fontFamily={showNickname ? 'sans' : 'mono'}
|
fontFamily={showNickname ? 'sans' : 'mono'}
|
||||||
fontWeight={showNickname ? '500' : '400'}
|
fontWeight={showNickname ? '500' : '400'}
|
||||||
|
mr={showNickname ? 0 : "2px"}
|
||||||
|
mt={showNickname ? 0 : "0px"}
|
||||||
onClick={doCopy}
|
onClick={doCopy}
|
||||||
>
|
>
|
||||||
{copyDisplay}
|
{copyDisplay}
|
||||||
</Box>
|
</Box>
|
||||||
{ !dontShowTime && (
|
{ !dontShowTime && time && (
|
||||||
<Timestamp
|
<Timestamp
|
||||||
relative={isRelativeTime}
|
relative={isRelativeTime}
|
||||||
stamp={stamp}
|
stamp={stamp}
|
||||||
|
@ -1,79 +1,329 @@
|
|||||||
import React, { ReactElement, ReactNode } from 'react';
|
import React, { ReactElement, ReactNode, useCallback } from "react";
|
||||||
import { Text, Box, Icon, Row } from '@tlon/indigo-react';
|
import {
|
||||||
|
Text,
|
||||||
|
Box,
|
||||||
|
Icon,
|
||||||
|
Row,
|
||||||
|
LoadingSpinner,
|
||||||
|
Button,
|
||||||
|
} from "@tlon/indigo-react";
|
||||||
|
import { css } from "@styled-system/css";
|
||||||
|
import _ from "lodash";
|
||||||
|
import { useHistory } from "react-router-dom";
|
||||||
|
|
||||||
import { cite } from '~/logic/lib/util';
|
import { cite, isDm } from "~/logic/lib/util";
|
||||||
import { MetadataUpdatePreview, JoinProgress, Invite, JoinRequest } from '@urbit/api';
|
import {
|
||||||
import { GroupSummary } from '~/views/landscape/components/GroupSummary';
|
MetadataUpdatePreview,
|
||||||
import { InviteSkeleton } from './InviteSkeleton';
|
joinProgress,
|
||||||
import { JoinSkeleton } from './JoinSkeleton';
|
JoinProgress,
|
||||||
import GlobalApi from '~/logic/api/global';
|
Invite,
|
||||||
|
JoinRequest,
|
||||||
|
resourceFromPath,
|
||||||
|
Metadata,
|
||||||
|
} from "@urbit/api";
|
||||||
|
import { GroupSummary } from "~/views/landscape/components/GroupSummary";
|
||||||
|
import { NotificationWrapper } from "~/views/apps/notifications/notification";
|
||||||
|
import { Header } from "~/views/apps/notifications/header";
|
||||||
|
import { InviteSkeleton } from "./InviteSkeleton";
|
||||||
|
import { JoinSkeleton } from "./JoinSkeleton";
|
||||||
|
import GlobalApi from "~/logic/api/global";
|
||||||
|
import { PropFunc } from "~/types";
|
||||||
|
import { MetadataIcon } from "~/views/landscape/components/MetadataIcon";
|
||||||
|
import { useContact } from "~/logic/state/contact";
|
||||||
|
import { useWaitForProps } from "~/logic/lib/useWaitForProps";
|
||||||
|
import useGroupState, {useGroup} from "~/logic/state/group";
|
||||||
|
import useContactState from "~/logic/state/contact";
|
||||||
|
import useMetadataState, {useAssocForGraph} from "~/logic/state/metadata";
|
||||||
|
import useGraphState from "~/logic/state/graph";
|
||||||
|
import { useRunIO } from "~/logic/lib/useRunIO";
|
||||||
|
import { StatelessAsyncButton } from "../StatelessAsyncButton";
|
||||||
|
import styled from "styled-components";
|
||||||
|
|
||||||
interface GroupInviteProps {
|
interface GroupInviteProps {
|
||||||
preview: MetadataUpdatePreview;
|
preview?: MetadataUpdatePreview;
|
||||||
status?: JoinRequest;
|
status?: JoinRequest;
|
||||||
|
app?: string;
|
||||||
|
uid?: string;
|
||||||
invite?: Invite;
|
invite?: Invite;
|
||||||
resource: string;
|
resource: string;
|
||||||
api: GlobalApi;
|
api: GlobalApi;
|
||||||
onAccept: () => Promise<any>;
|
|
||||||
onDecline: () => Promise<any>;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function GroupInvite(props: GroupInviteProps): ReactElement {
|
function Elbow(
|
||||||
const { resource, api, preview, invite, status, onAccept, onDecline } = props;
|
props: { size?: number; color?: string } & PropFunc<typeof Box>
|
||||||
const { metadata, members } = props.preview;
|
) {
|
||||||
|
const { size = 12, color = "lightGray", ...rest } = props;
|
||||||
|
|
||||||
let inner: ReactNode = null;
|
return (
|
||||||
let Outer: (p: { children: ReactNode }) => JSX.Element = p => (
|
<Box
|
||||||
<>{p.children}</>
|
{...rest}
|
||||||
|
overflow="hidden"
|
||||||
|
width={size}
|
||||||
|
height={size}
|
||||||
|
position="relative"
|
||||||
|
>
|
||||||
|
<Box
|
||||||
|
border="2px solid"
|
||||||
|
borderRadius={2}
|
||||||
|
borderColor={color}
|
||||||
|
position="absolute"
|
||||||
|
left="0px"
|
||||||
|
bottom="0px"
|
||||||
|
width={size * 2}
|
||||||
|
height={size * 2}
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const description: string[] = [
|
||||||
|
"Contacting host...",
|
||||||
|
"Retrieving data...",
|
||||||
|
"Finished join",
|
||||||
|
"Unable to join, you do not have the correct permissions",
|
||||||
|
"Internal error, please file an issue",
|
||||||
|
];
|
||||||
|
|
||||||
|
function inviteUrl(hidden: boolean, resource: string, metadata?: Metadata) {
|
||||||
|
if (!hidden) {
|
||||||
|
return `/~landscape${resource}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (metadata?.config.graph === "chat") {
|
||||||
|
return `/~landscape/messages/resource/${metadata?.config?.graph}${resource}`;
|
||||||
|
} else {
|
||||||
|
return `/~landscape/home/resource/${metadata?.config?.graph}${resource}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
function InviteMetadata(props: {
|
||||||
|
preview?: MetadataUpdatePreview;
|
||||||
|
resource: string;
|
||||||
|
}) {
|
||||||
|
const { resource, preview } = props;
|
||||||
|
const { ship, name } = resourceFromPath(resource);
|
||||||
|
const dm = isDm(resource);
|
||||||
|
if (dm) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const container = (children: ReactNode) => (
|
||||||
|
<Row overflow="hidden" height="4" gapX="2" alignItems="center">
|
||||||
|
{children}
|
||||||
|
</Row>
|
||||||
);
|
);
|
||||||
|
|
||||||
if (status) {
|
if (preview) {
|
||||||
inner = (
|
const { title } = preview.metadata;
|
||||||
<Text mr="1">
|
const { members } = preview;
|
||||||
You are joining <Text fontWeight="medium">{metadata.title}</Text>
|
return container(
|
||||||
</Text>
|
|
||||||
);
|
|
||||||
Outer = ({ children }) => (
|
|
||||||
<JoinSkeleton resource={resource} api={api} gapY="3" status={status}>
|
|
||||||
{children}
|
|
||||||
</JoinSkeleton>
|
|
||||||
);
|
|
||||||
} else if (invite) {
|
|
||||||
Outer = ({ children }) => (
|
|
||||||
<InviteSkeleton
|
|
||||||
onDecline={onDecline}
|
|
||||||
onAccept={onAccept}
|
|
||||||
acceptDesc="Join Group"
|
|
||||||
declineDesc="Decline Invitation"
|
|
||||||
gapY="3"
|
|
||||||
>
|
|
||||||
{children}
|
|
||||||
</InviteSkeleton>
|
|
||||||
);
|
|
||||||
inner = (
|
|
||||||
<>
|
<>
|
||||||
<Text mr="1" mono>
|
<MetadataIcon height={4} width={4} metadata={preview.metadata} />
|
||||||
{cite(`~${invite!.ship}`)}
|
<Text fontWeight="medium">{title}</Text>
|
||||||
|
<Text gray fontWeight="medium">
|
||||||
|
{members} Member{members > 1 ? "s" : ""}
|
||||||
</Text>
|
</Text>
|
||||||
<Text mr="1">invited you to </Text>
|
|
||||||
<Text fontWeight="medium">{metadata.title}</Text>
|
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
return (
|
|
||||||
<Outer>
|
return container(
|
||||||
<Row py="1" alignItems="center">
|
<>
|
||||||
<Icon display="block" mr={2} icon="Bullet" color="blue" />
|
<Text whiteSpace="nowrap" textOverflow="ellipsis" ml="1px" mb="2px" mono>
|
||||||
{inner}
|
{cite(ship)}/{name}
|
||||||
</Row>
|
</Text>
|
||||||
<Box px="4">
|
</>
|
||||||
<GroupSummary
|
);
|
||||||
gray
|
}
|
||||||
metadata={metadata}
|
|
||||||
memberCount={members}
|
function InviteStatus(props: { status?: JoinRequest }) {
|
||||||
channelCount={preview?.['channel-count']}
|
const { status } = props;
|
||||||
/>
|
|
||||||
</Box>
|
if (!status) {
|
||||||
</Outer>
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const current = status && joinProgress.indexOf(status.progress);
|
||||||
|
const desc = _.isNumber(current) && description[current];
|
||||||
|
return (
|
||||||
|
<Row gapX="1" alignItems="center" height={4}>
|
||||||
|
{ status.progress === 'done' ? <Icon icon="Checkmark" /> : <LoadingSpinner dark /> }
|
||||||
|
<Text gray>{desc}</Text>
|
||||||
|
</Row>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useInviteAccept(
|
||||||
|
resource: string,
|
||||||
|
api: GlobalApi,
|
||||||
|
app?: string,
|
||||||
|
uid?: string
|
||||||
|
) {
|
||||||
|
const { ship, name } = resourceFromPath(resource);
|
||||||
|
const history = useHistory();
|
||||||
|
const associations = useMetadataState((s) => s.associations);
|
||||||
|
const groups = useGroupState((s) => s.groups);
|
||||||
|
const graphKeys = useGraphState((s) => s.graphKeys);
|
||||||
|
|
||||||
|
const waiter = useWaitForProps({ associations, graphKeys, groups });
|
||||||
|
return useRunIO<void, boolean>(
|
||||||
|
async () => {
|
||||||
|
if (!(app && uid)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (resource in groups) {
|
||||||
|
await api.invite.decline(app, uid);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
await api.groups.join(ship, name);
|
||||||
|
await api.invite.accept(app, uid);
|
||||||
|
await waiter((p) => {
|
||||||
|
return (
|
||||||
|
(resource in p.groups &&
|
||||||
|
resource in (p.associations?.graph ?? {}) &&
|
||||||
|
p.graphKeys.has(resource.slice(7))) ||
|
||||||
|
resource in (p.associations?.groups ?? {})
|
||||||
|
);
|
||||||
|
});
|
||||||
|
return true;
|
||||||
|
},
|
||||||
|
(success: boolean) => {
|
||||||
|
if (!success) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const redir = inviteUrl(
|
||||||
|
groups?.[resource]?.hidden,
|
||||||
|
resource,
|
||||||
|
associations?.graph?.[resource]?.metadata
|
||||||
|
);
|
||||||
|
if (redir) {
|
||||||
|
// weird race condition
|
||||||
|
setTimeout(() => {
|
||||||
|
history.push(redir);
|
||||||
|
}, 200);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
resource
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function InviteActions(props: {
|
||||||
|
status?: JoinRequest;
|
||||||
|
resource: string;
|
||||||
|
api: GlobalApi;
|
||||||
|
app?: string;
|
||||||
|
uid?: string;
|
||||||
|
}) {
|
||||||
|
const { resource, api, app, uid } = props;
|
||||||
|
const inviteAccept = useInviteAccept(resource, api, app, uid);
|
||||||
|
|
||||||
|
const inviteDecline = useCallback(async () => {
|
||||||
|
if (!(app && uid)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
await api.invite.decline(app, uid);
|
||||||
|
}, [app, uid]);
|
||||||
|
|
||||||
|
const hideJoin = useCallback(async () => {
|
||||||
|
await api.groups.hide(resource);
|
||||||
|
}, [api, resource]);
|
||||||
|
|
||||||
|
const { status } = props;
|
||||||
|
if (status) {
|
||||||
|
if(status.progress === 'done') {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<Row gapX="2" alignItems="center" height={4}>
|
||||||
|
<StatelessAsyncButton
|
||||||
|
height={4}
|
||||||
|
backgroundColor="white"
|
||||||
|
onClick={hideJoin}
|
||||||
|
>
|
||||||
|
Cancel
|
||||||
|
</StatelessAsyncButton>
|
||||||
|
</Row>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Row gapX="2" alignItems="center" height={4}>
|
||||||
|
<StatelessAsyncButton
|
||||||
|
color="blue"
|
||||||
|
height={4}
|
||||||
|
backgroundColor="white"
|
||||||
|
onClick={inviteAccept}
|
||||||
|
>
|
||||||
|
Accept
|
||||||
|
</StatelessAsyncButton>
|
||||||
|
<StatelessAsyncButton
|
||||||
|
height={4}
|
||||||
|
backgroundColor="white"
|
||||||
|
onClick={inviteDecline}
|
||||||
|
>
|
||||||
|
Decline
|
||||||
|
</StatelessAsyncButton>
|
||||||
|
</Row>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const responsiveStyle = ({ gapXY = 0 as number | number[] }) => {
|
||||||
|
return css({
|
||||||
|
flexDirection: ["column", "row"],
|
||||||
|
"& > *": {
|
||||||
|
marginTop: _.isArray(gapXY) ? [gapXY[0], 0] : [gapXY, 0],
|
||||||
|
marginLeft: _.isArray(gapXY) ? [0, ...gapXY.slice(1)] : [0, gapXY],
|
||||||
|
},
|
||||||
|
"& > :first-child": {
|
||||||
|
marginTop: 0,
|
||||||
|
marginLeft: 0,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
const ResponsiveRow = styled(Row)(responsiveStyle);
|
||||||
|
export function GroupInvite(props: GroupInviteProps): ReactElement {
|
||||||
|
const { resource, api, preview, invite, status, app, uid } = props;
|
||||||
|
const dm = isDm(resource);
|
||||||
|
const history = useHistory();
|
||||||
|
|
||||||
|
const invitedTo = dm ? "DM" : "group";
|
||||||
|
const graphAssoc = useAssocForGraph(resource);
|
||||||
|
|
||||||
|
|
||||||
|
const headerProps = status
|
||||||
|
? { description: `You are joining a ${invitedTo}` }
|
||||||
|
: { description: `invited you to a ${invitedTo}`, authors: [invite!.ship] };
|
||||||
|
|
||||||
|
const onClick = () => {
|
||||||
|
if(status?.progress === 'done') {
|
||||||
|
const redir = inviteUrl(app !== 'groups', resource, graphAssoc?.metadata);
|
||||||
|
if(redir) {
|
||||||
|
history.push(redir);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<NotificationWrapper api={api}>
|
||||||
|
<Header content {...headerProps} />
|
||||||
|
<Row onClick={onClick} height={[null, 4]} alignItems="flex-start" gridArea="main">
|
||||||
|
<Elbow mx="2" />
|
||||||
|
<ResponsiveRow
|
||||||
|
gapXY={[1, 2]}
|
||||||
|
height={[null, 4]}
|
||||||
|
alignItems={["flex-start", "center"]}
|
||||||
|
>
|
||||||
|
<InviteMetadata preview={preview} resource={resource} />
|
||||||
|
<InviteStatus status={status} />
|
||||||
|
<InviteActions
|
||||||
|
api={api}
|
||||||
|
resource={resource}
|
||||||
|
status={status}
|
||||||
|
app={app}
|
||||||
|
uid={uid}
|
||||||
|
/>
|
||||||
|
</ResponsiveRow>
|
||||||
|
</Row>
|
||||||
|
</NotificationWrapper>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -7,113 +7,26 @@ import {
|
|||||||
JoinRequests,
|
JoinRequests,
|
||||||
Groups,
|
Groups,
|
||||||
Associations,
|
Associations,
|
||||||
|
JoinRequest,
|
||||||
} from "@urbit/api";
|
} from "@urbit/api";
|
||||||
import { Invite } from "@urbit/api/invite";
|
import { Invite } from "@urbit/api/invite";
|
||||||
import { Text, Icon, Row } from "@tlon/indigo-react";
|
|
||||||
|
|
||||||
import { cite, useShowNickname } from "~/logic/lib/util";
|
|
||||||
import GlobalApi from "~/logic/api/global";
|
import GlobalApi from "~/logic/api/global";
|
||||||
import { resourceFromPath } from "~/logic/lib/group";
|
|
||||||
import { GroupInvite } from "./Group";
|
import { GroupInvite } from "./Group";
|
||||||
import { InviteSkeleton } from "./InviteSkeleton";
|
|
||||||
import { JoinSkeleton } from "./JoinSkeleton";
|
|
||||||
import { useWaitForProps } from "~/logic/lib/useWaitForProps";
|
|
||||||
import useGroupState from "~/logic/state/group";
|
|
||||||
import useContactState from "~/logic/state/contact";
|
|
||||||
import useMetadataState from "~/logic/state/metadata";
|
|
||||||
import useGraphState from "~/logic/state/graph";
|
|
||||||
import { useRunIO } from "~/logic/lib/useRunIO";
|
|
||||||
|
|
||||||
interface InviteItemProps {
|
interface InviteItemProps {
|
||||||
invite?: Invite;
|
invite?: Invite;
|
||||||
resource: string;
|
resource: string;
|
||||||
pendingJoin?: string;
|
pendingJoin?: JoinRequest;
|
||||||
app?: string;
|
app?: string;
|
||||||
uid?: string;
|
uid?: string;
|
||||||
api: GlobalApi;
|
api: GlobalApi;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function useInviteAccept(
|
|
||||||
resource: string,
|
|
||||||
api: GlobalApi,
|
|
||||||
app?: string,
|
|
||||||
uid?: string,
|
|
||||||
invite?: Invite,
|
|
||||||
) {
|
|
||||||
const { ship, name } = resourceFromPath(resource);
|
|
||||||
const history = useHistory();
|
|
||||||
const associations = useMetadataState((s) => s.associations);
|
|
||||||
const groups = useGroupState((s) => s.groups);
|
|
||||||
const graphKeys = useGraphState((s) => s.graphKeys);
|
|
||||||
|
|
||||||
const waiter = useWaitForProps({ associations, graphKeys, groups });
|
|
||||||
return useRunIO<void, boolean>(
|
|
||||||
async () => {
|
|
||||||
if (!(app && invite && uid)) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
if (resource in groups) {
|
|
||||||
await api.invite.decline(app, uid);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
await api.groups.join(ship, name);
|
|
||||||
await api.invite.accept(app, uid);
|
|
||||||
await waiter((p) => {
|
|
||||||
return (
|
|
||||||
(resource in p.groups &&
|
|
||||||
resource in (p.associations?.graph ?? {}) &&
|
|
||||||
p.graphKeys.has(resource.slice(7))) ||
|
|
||||||
resource in (p.associations?.groups ?? {})
|
|
||||||
);
|
|
||||||
});
|
|
||||||
return true;
|
|
||||||
},
|
|
||||||
(success: boolean) => {
|
|
||||||
if (!success) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (groups?.[resource]?.hidden) {
|
|
||||||
const { metadata } = associations.graph[resource];
|
|
||||||
if (metadata && "graph" in metadata.config) {
|
|
||||||
if (metadata.config.graph === "chat") {
|
|
||||||
history.push(
|
|
||||||
`/~landscape/messages/resource/${metadata.config.graph}${resource}`
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
history.push(
|
|
||||||
`/~landscape/home/resource/${metadata.config.graph}${resource}`
|
|
||||||
);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
console.error("unknown metadata: ", metadata);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
history.push(`/~landscape${resource}`);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
resource
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function InviteItem(props: InviteItemProps) {
|
export function InviteItem(props: InviteItemProps) {
|
||||||
const [preview, setPreview] = useState<MetadataUpdatePreview | null>(null);
|
const [preview, setPreview] = useState<MetadataUpdatePreview | null>(null);
|
||||||
const { pendingJoin, invite, resource, uid, app, api } = props;
|
const { pendingJoin, invite, resource, uid, app, api } = props;
|
||||||
const { name } = resourceFromPath(resource);
|
|
||||||
const contacts = useContactState((state) => state.contacts);
|
|
||||||
const contact = contacts?.[`~${invite?.ship}`] ?? {};
|
|
||||||
const showNickname = useShowNickname(contact);
|
|
||||||
|
|
||||||
const inviteAccept = useInviteAccept(resource, api, app, uid, invite);
|
|
||||||
|
|
||||||
const inviteDecline = useCallback(async () => {
|
|
||||||
if (!(app && uid)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
await api.invite.decline(app, uid);
|
|
||||||
}, [app, uid]);
|
|
||||||
|
|
||||||
const handlers = { onAccept: inviteAccept, onDecline: inviteDecline };
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!app || app === "groups") {
|
if (!app || app === "groups") {
|
||||||
@ -132,86 +45,17 @@ export function InviteItem(props: InviteItemProps) {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (preview) {
|
return (
|
||||||
return (
|
<GroupInvite
|
||||||
<GroupInvite
|
resource={resource}
|
||||||
resource={resource}
|
api={api}
|
||||||
api={api}
|
preview={preview}
|
||||||
preview={preview}
|
invite={invite}
|
||||||
invite={invite}
|
status={pendingJoin}
|
||||||
status={pendingJoin}
|
uid={uid}
|
||||||
{...handlers}
|
app={app}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
} else if (invite && name.startsWith("dm--")) {
|
|
||||||
return (
|
|
||||||
<InviteSkeleton
|
|
||||||
gapY="3"
|
|
||||||
{...handlers}
|
|
||||||
acceptDesc="Join DM"
|
|
||||||
declineDesc="Decline DM"
|
|
||||||
>
|
|
||||||
<Row py="1" alignItems="center">
|
|
||||||
<Icon display="block" color="blue" icon="Bullet" mr="2" />
|
|
||||||
<Text
|
|
||||||
mr="1"
|
|
||||||
mono={!showNickname}
|
|
||||||
fontWeight={showNickname ? "500" : "400"}
|
|
||||||
>
|
|
||||||
{showNickname ? contact?.nickname : cite(`~${invite!.ship}`)}
|
|
||||||
</Text>
|
|
||||||
<Text mr="1">invited you to a DM</Text>
|
|
||||||
</Row>
|
|
||||||
</InviteSkeleton>
|
|
||||||
);
|
|
||||||
} else if (status && name.startsWith("dm--")) {
|
|
||||||
return (
|
|
||||||
<JoinSkeleton api={api} resource={resource} status={status} gapY="3">
|
|
||||||
<Row py="1" alignItems="center">
|
|
||||||
<Icon display="block" color="blue" icon="Bullet" mr="2" />
|
|
||||||
<Text mr="1">Joining direct message...</Text>
|
|
||||||
</Row>
|
|
||||||
</JoinSkeleton>
|
|
||||||
);
|
|
||||||
} else if (invite) {
|
|
||||||
return (
|
|
||||||
<InviteSkeleton
|
|
||||||
acceptDesc="Accept Invite"
|
|
||||||
declineDesc="Decline Invite"
|
|
||||||
resource={resource}
|
|
||||||
{...handlers}
|
|
||||||
gapY="3"
|
|
||||||
>
|
|
||||||
<Row py="1" alignItems="center">
|
|
||||||
<Icon display="block" color="blue" icon="Bullet" mr="2" />
|
|
||||||
<Text
|
|
||||||
mr="1"
|
|
||||||
mono={!showNickname}
|
|
||||||
fontWeight={showNickname ? "500" : "400"}
|
|
||||||
>
|
|
||||||
{showNickname ? contact?.nickname : cite(`~${invite!.ship}`)}
|
|
||||||
</Text>
|
|
||||||
<Text mr="1">
|
|
||||||
invited you to ~{invite.resource.ship}/{invite.resource.name}
|
|
||||||
</Text>
|
|
||||||
</Row>
|
|
||||||
</InviteSkeleton>
|
|
||||||
);
|
|
||||||
} else if (pendingJoin) {
|
|
||||||
const [, , ship, name] = resource.split("/");
|
|
||||||
return (
|
|
||||||
<JoinSkeleton api={api} resource={resource} status={pendingJoin}>
|
|
||||||
<Row py="1" alignItems="center">
|
|
||||||
<Icon display="block" color="blue" icon="Bullet" mr="2" />
|
|
||||||
<Text mr="1">You are joining</Text>
|
|
||||||
<Text mono>
|
|
||||||
{cite(ship)}/{name}
|
|
||||||
</Text>
|
|
||||||
</Row>
|
|
||||||
</JoinSkeleton>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
export default InviteItem;
|
export default InviteItem;
|
||||||
|
@ -23,6 +23,7 @@ const Timestamp = (props: TimestampProps): ReactElement | null => {
|
|||||||
relative,
|
relative,
|
||||||
dateNotRelative,
|
dateNotRelative,
|
||||||
fontSize,
|
fontSize,
|
||||||
|
lineHeight,
|
||||||
...rest
|
...rest
|
||||||
} = {
|
} = {
|
||||||
time: true,
|
time: true,
|
||||||
@ -62,7 +63,7 @@ const Timestamp = (props: TimestampProps): ReactElement | null => {
|
|||||||
title={stamp.format(DateFormat + ' ' + TimeFormat)}
|
title={stamp.format(DateFormat + ' ' + TimeFormat)}
|
||||||
>
|
>
|
||||||
{time && (
|
{time && (
|
||||||
<Text flexShrink={0} color={color} fontSize={fontSize}>
|
<Text lineHeight={lineHeight} flexShrink={0} color={color} fontSize={fontSize}>
|
||||||
{timestamp}
|
{timestamp}
|
||||||
</Text>
|
</Text>
|
||||||
)}
|
)}
|
||||||
@ -70,6 +71,7 @@ const Timestamp = (props: TimestampProps): ReactElement | null => {
|
|||||||
<Text
|
<Text
|
||||||
flexShrink={0}
|
flexShrink={0}
|
||||||
color={color}
|
color={color}
|
||||||
|
lineHeight={lineHeight}
|
||||||
fontSize={fontSize}
|
fontSize={fontSize}
|
||||||
display={time ? ['none', hovering ? 'block' : 'none'] : 'block'}
|
display={time ? ['none', hovering ? 'block' : 'none'] : 'block'}
|
||||||
>
|
>
|
||||||
|
Loading…
Reference in New Issue
Block a user