Merge pull request #5361 from urbit/lf/post-assembly-grabbag

garden/landscape: fixes
This commit is contained in:
Liam Fitzgerald 2021-10-25 19:55:57 -05:00 committed by GitHub
commit a3664da492
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
14 changed files with 19 additions and 555 deletions

View File

@ -232,8 +232,8 @@
:: if the new chad is a site, we're instantly done
::
?: ?=(%site -.href.docket)
:- ~[add-fact:cha]
=. charges (new-chad:cha %site ~)
:- ~[add-fact:cha]
state
::
=. by-base (~(put by by-base) base.href.docket desk)

View File

@ -10,7 +10,7 @@
"serve": "vite preview",
"lint": "eslint --cache \"**/*.{js,jsx,ts,tsx}\"",
"lint:fix": "npm run lint -- --fix",
"test": "echo \"No test yet\"",
"test": "tsc --noEmit",
"tsc": "tsc --noEmit"
},
"dependencies": {

View File

@ -8,7 +8,7 @@ import { Dialog, DialogClose, DialogContent, DialogTrigger } from './Dialog';
import { DocketHeader } from './DocketHeader';
import { Spinner } from './Spinner';
import { VatMeta } from './VatMeta';
import useDocketState, { ChargeWithDesk } from '../state/docket';
import useDocketState, { ChargeWithDesk, useTreaty } from '../state/docket';
import { getAppHref, getAppName } from '../state/util';
import { addRecentApp } from '../nav/search/Home';
import { TreatyMeta } from './TreatyMeta';
@ -52,6 +52,7 @@ export const AppInfo: FC<AppInfoProps> = ({ docket, vat, className }) => {
const [ship, desk] = getRemoteDesk(docket, vat);
const publisher = vat?.arak?.rail?.publisher ?? ship;
const [copied, setCopied] = useState(false);
const treaty = useTreaty(ship, desk);
const installApp = async () => {
if (installStatus === 'installed') {
@ -135,18 +136,20 @@ export const AppInfo: FC<AppInfoProps> = ({ docket, vat, className }) => {
</PillButton>
</div>
</DocketHeader>
<div className="space-y-6">
{vat ? (
<>
<hr className="-mx-5 sm:-mx-8 border-gray-50" />
<VatMeta vat={vat} />
</>
) : null}
{'chad' in docket ? null : (
{!treaty ? null : (
<>
<hr className="-mx-5 sm:-mx-8 border-gray-50" />
<TreatyMeta treaty={docket} />
<TreatyMeta treaty={treaty} />
</>
)}
</div>
</div>
);
};

View File

@ -1,5 +1,4 @@
import React from 'react';
import moment from 'moment';
import { Vat } from '@urbit/api/hood';
import { Attribute } from './Attribute';
@ -12,12 +11,6 @@ export function VatMeta(props: { vat: Vat }) {
const pluralUpdates = next?.length !== 1;
return (
<div className="mt-5 sm:mt-8 space-y-5 sm:space-y-8">
<Attribute title="Developer Desk" attr="desk">
{ship}/{foreignDesk}
</Attribute>
<Attribute title="Last Software Update" attr="case">
{moment(cass.da).format('YYYY.MM.DD')}
</Attribute>
<Attribute title="Desk Hash" attr="hash">
{hash}
</Attribute>

View File

@ -1,18 +1,12 @@
import {
cite,
GraphNotifIndex,
GroupNotifIndex,
IndexedNotification,
NotificationGraphConfig,
Post,
Unreads
} from '@urbit/api';
import { patp } from 'urbit-ob';
import bigInt, { BigInteger } from 'big-integer';
import _ from 'lodash';
import f from 'lodash/fp';
import { pluralize } from './util';
import useMetadataState from '../state/metadata';
import { emptyHarkStats } from '../state/hark';
export function getLastSeen(
@ -60,94 +54,3 @@ export function isWatching(
);
}
export function getNotificationKey(
time: BigInteger,
notification: IndexedNotification
): string {
const base = time.toString();
if ('graph' in notification.index) {
const { graph, index, description } = notification.index.graph;
return `${base}-${graph}-${index}-${description}`;
} else if ('group' in notification.index) {
const { group } = notification.index.group;
return `${base}-${group}`;
}
return `${base}-unknown`;
}
export function notificationReferent(not: IndexedNotification) {
if ('graph' in not.index) {
return not.index.graph.graph;
} else {
return not.index.group.group;
}
}
export function describeNotification(notification: IndexedNotification) {
function group(idx: GroupNotifIndex) {
switch (idx.description) {
case 'add-members':
return 'joined';
case 'remove-members':
return 'left';
default:
return idx.description;
}
}
function graph(idx: GraphNotifIndex, plural: boolean, singleAuthor: boolean) {
const isDm = idx.graph === `/ship/~${window.ship}/dm-inbox`;
if (isDm) {
return 'New DM from ';
}
switch (idx.description) {
case 'post':
return 'Your post received replies in';
case 'link':
return `New link${plural ? 's' : ''} in`;
case 'comment':
return `New comment${plural ? 's' : ''} on`;
case 'note':
return `New Note${plural ? 's' : ''} in`;
// @ts-ignore need better types
case 'edit-note':
return `updated ${pluralize('note', plural)} in`;
case 'mention':
return 'You were mentioned in';
case 'message':
if (isDm) {
return 'messaged you';
}
return `New message${plural ? 's' : ''} in`;
default: return idx.description;
}
}
if ('group' in notification.index) {
return group(notification.index.group);
} else if ('graph' in notification.index) {
// @ts-ignore needs better type guard
const contents = notification.notification?.contents?.graph ?? ([] as Post[]);
return graph(
notification.index.graph,
contents.length > 1,
_.uniq(_.map(contents, 'author')).length === 1
);
}
}
export function getReferent(notification: IndexedNotification) {
const meta = useMetadataState.getState();
if ('graph' in notification.index) {
if (notification.index.graph.graph === `/ship/~${window.ship}/dm-inbox`) {
const [, ship] = notification.index.graph.index.split('/');
return cite(patp(ship));
}
return (
meta.associations.graph[notification.index.graph.graph]?.metadata
?.title ?? notification.index.graph
);
} else if ('group' in notification.index) {
return (
meta.associations.groups[notification.index.group.group]?.metadata?.title ??
notification.index.group.group
);
}
}

View File

@ -1,21 +0,0 @@
import { GraphNotificationContents, GraphNotifIndex } from '@urbit/api';
export function getParentIndex(
idx: GraphNotifIndex,
contents: GraphNotificationContents
) {
const origIndex = contents[0].index.slice(1).split('/');
const ret = (i: string[]) => `/${i.join('/')}`;
switch (idx.description) {
case 'link':
return '/';
case 'comment':
return ret(origIndex.slice(0, 1));
case 'note':
return '/';
case 'mention':
return undefined;
default:
return undefined;
}
}

View File

@ -42,6 +42,7 @@ export interface HarkState {
notificationsGroupConfig: string[];
unreads: Unreads;
archiveNote: (bin: HarkBin, lid: HarkLid) => Promise<void>;
readCount: (path: string) => Promise<void>;
}
const useHarkState = createState<HarkState>(
@ -171,5 +172,4 @@ export function useHarkGraphIndex(graph: string, index: string) {
);
}
window.hark = useHarkState.getState;
export default useHarkState;

View File

@ -1,302 +0,0 @@
import { Box, Col, Icon, Row, Text } from '@tlon/indigo-react';
import { Association, GraphNotificationContents, GraphNotifIndex, Post } from '@urbit/api';
import { BigInteger } from 'big-integer';
import { patp } from 'urbit-ob';
import _ from 'lodash';
import React, { useCallback } from 'react';
import { Link, useHistory } from 'react-router-dom';
import styled from 'styled-components';
import { pluralize } from '~/logic/lib/util';
import useGroupState from '~/logic/state/group';
import {
useAssocForGraph,
useAssocForGroup
} from '~/logic/state/metadata';
import Author from '~/views/components/Author';
import { GraphContent } from '~/views/landscape/components/Graph/GraphContent';
import { Header } from './header';
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 describeNotification(
description: string,
plural: boolean,
isDm: boolean,
singleAuthor: boolean
): string {
switch (description) {
case 'post':
return singleAuthor ? 'replied to you' : 'Your post received replies';
case 'link':
return `New link${plural ? 's' : ''} in`;
case 'comment':
return `New comment${plural ? 's' : ''} on`;
case 'note':
return `New Note${plural ? 's' : ''} in`;
case 'edit-note':
return `updated ${pluralize('note', plural)} in`;
case 'mention':
return singleAuthor ? 'mentioned you in' : 'You were mentioned in';
case 'message':
if (isDm) {
return 'messaged you';
}
return `New message${plural ? 's' : ''} in`;
default:
return description;
}
}
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 = ({ post, mod, index, hidden, association }) => {
const { contents } = post;
const idx = index.slice(1).split('/');
const url = getNodeUrl(mod, hidden, association?.group, association?.resource, index);
if (mod === 'graph-validator-link' && idx.length === 1) {
const [{ text: title }] = contents;
return (
<ContentSummary to={url} icon="Links" name={title} author={post.author} />
);
}
if (mod === 'graph-validator-publish' && idx[1] === '1') {
const [{ text: title }] = contents;
return (
<ContentSummary to={url} icon="Note" name={title} author={post.author} />
);
}
return (
<TruncBox truncate={8}>
<GraphContent contents={post.contents} showOurContact />
</TruncBox>
);
};
function getNodeUrl(
mod: string,
hidden: boolean,
groupPath: string,
graph: string,
index: string
) {
const graphValidator = 'graph-validator-';
const rmValidator = mod.slice(graphValidator.length);
if (hidden && mod === 'graph-validator-chat') {
groupPath = '/messages';
} else if (hidden) {
groupPath = '/home';
}
const graphUrl = `/~landscape${groupPath}/resource/${rmValidator}${graph}`;
const idx = index.slice(1).split('/');
if (mod === 'graph-validator-publish') {
const [noteId, kind, commId] = idx;
const selected = kind === '2' ? `?selected=${commId}` : '';
return `${graphUrl}/note/${noteId}${selected}`;
} else if (mod === 'graph-validator-link') {
const [linkId, commId] = idx;
return `${graphUrl}/index/${linkId}${commId ? `?selected=${commId}` : ''}`;
} else if (mod === 'graph-validator-chat') {
if (idx.length > 0) {
return `${graphUrl}?msg=${idx[0]}`;
}
return graphUrl;
} else if (mod === 'graph-validator-post') {
return `/~landscape${groupPath}/feed/thread${index}`;
} else if (mod === 'graph-validator-dm') {
return `/~landscape${groupPath}/dm/${patp(idx[0])}`;
}
return '';
}
interface PostsByAuthor {
author: string;
posts: Post[];
}
const GraphNodes = (props: {
posts: Post[];
hideAuthors?: boolean;
index: string;
mod: string;
association: Association;
hidden: boolean;
}) => {
const {
posts,
mod,
hidden,
index,
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] }];
},
[]
);
return (
<>
{_.map(postsByConsecAuthor, ({ posts, author }, idx) => {
const time = posts[0]?.['time-sent'];
return (
<Col key={idx} flexGrow={1} alignItems="flex-start">
{!hideAuthors && (
<Author
size={24}
sigilPadding={6}
showImage
ship={author}
date={time}
/>
)}
<Col gapY={2} py={hideAuthors ? 0 : 2} width="100%">
{_.map(posts, post => (
<GraphNodeContent
key={post.index}
post={post}
mod={mod}
index={index}
association={association}
hidden={hidden}
/>
))}
</Col>
</Col>
);
})}
</>
);
};
export function GraphNotification(props: {
index: GraphNotifIndex;
contents: GraphNotificationContents;
read: boolean;
time: number;
timebox: BigInteger;
}) {
const { contents, index, read, time, timebox } = props;
const history = useHistory();
const authors = _.uniq(_.map(contents, 'author'));
const singleAuthor = authors.length === 1;
const { graph, mark } = index;
const association = useAssocForGraph(graph);
const dm = mark === 'graph-validator-dm';
const desc = describeNotification(
index.description,
contents.length !== 1,
dm,
singleAuthor
);
const groupAssociation = useAssocForGroup(association?.group ?? '');
const groups = useGroupState(state => state.groups);
const onClick = useCallback(() => {
if(dm) {
history.push(`/~landscape/messages/dm/~${authors[0]}`);
return;
}
const first = contents[0];
history.push(
getNodeUrl(
index.mark,
groups[association?.group]?.hidden,
association?.group,
association?.resource,
first.index
)
);
}, [timebox, index, read, history.push, authors, dm]);
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 (
<>
<Header
time={time}
authors={authorsInHeader ? authors : []}
channelTitle={channelTitle}
description={desc}
groupTitle={groupTitle}
content
/>
<Col onClick={onClick} gapY={2} flexGrow={1} width="100%" gridArea="main">
<GraphNodes
hideAuthors={hideAuthors}
posts={contents.slice(0, 4)}
mod={index.mark}
index={contents?.[0].index}
association={association}
hidden={groups[association?.group]?.hidden}
/>
{contents.length > 4 && (
<Text mb={2} gray>
+ {contents.length - 4} more
</Text>
)}
</Col>
</>
);
}

View File

@ -1,61 +0,0 @@
import { Col } from '@tlon/indigo-react';
import {
GroupNotificationContents,
GroupNotifIndex,
GroupUpdate
} from '@urbit/api';
import _ from 'lodash';
import React, { ReactElement } from 'react';
import { useAssocForGroup } from '~/logic/state/metadata';
import { Header } from './header';
function describeNotification(description: string, plural: boolean) {
switch (description) {
case 'add-members':
return 'joined';
case 'remove-members':
return 'left';
default:
return description;
}
}
function getGroupUpdateParticipants(update: GroupUpdate): string[] {
if ('addMembers' in update) {
return update.addMembers.ships;
}
if ('removeMembers' in update) {
return update.removeMembers.ships;
}
return [];
}
interface GroupNotificationProps {
index: GroupNotifIndex;
contents: GroupNotificationContents;
time: number;
}
export function GroupNotification(props: GroupNotificationProps): ReactElement {
const { contents, index, time } = props;
const authors = _.flatten(_.map(contents, getGroupUpdateParticipants));
const { group } = index;
const desc = describeNotification(index.description, contents.length !== 1);
const association = useAssocForGroup(group);
const groupTitle = association?.metadata?.title ?? group;
return (
<Col>
<Header
time={time}
authors={authors}
description={desc}
groupTitle={groupTitle}
/>
</Col>
);
}

View File

@ -20,7 +20,7 @@ export interface AuthorProps {
size?: number;
lineHeight?: string | number;
isRelativeTime?: boolean;
dontShowTime: boolean;
dontShowTime?: boolean;
}
// eslint-disable-next-line max-lines-per-function

View File

@ -25,7 +25,7 @@ import { CommentItem } from './CommentItem';
import airlock from '~/logic/api';
import useGraphState from '~/logic/state/graph';
import { useHistory } from 'react-router';
import { toHarkPlace } from '~/logic/lib/util';
import { toHarkPath, toHarkPlace } from '~/logic/lib/util';
interface CommentsProps {
comments: GraphNode;
@ -134,7 +134,8 @@ export function Comments(props: CommentsProps & PropFunc<typeof Col>) {
}, [comments.post?.index, association.resource]);
const unreads = useHarkState(state => state.unreads);
const readCount = children.length - getUnreadCount(unreads, association.resource, parentIndex);
const harkPath = toHarkPath(association.resource, parentIndex);
const readCount = children.length - getUnreadCount(unreads, harkPath);
const canComment = isWriter(group, association.resource) || association.metadata.vip === 'reader-comments';

View File

@ -26,7 +26,6 @@ import useGroupState from '~/logic/state/group';
import useMetadataState, { useAssocForGraph } from '~/logic/state/metadata';
import { PropFunc } from '~/types';
import { Header } from '~/views/apps/notifications/header';
import { NotificationWrapper } from '~/views/apps/notifications/notification';
import { MetadataIcon } from '~/views/landscape/components/MetadataIcon';
import { StatelessAsyncButton } from '../StatelessAsyncButton';
import airlock from '~/logic/api';
@ -297,7 +296,7 @@ export function GroupInvite(props: GroupInviteProps): ReactElement {
};
return (
<NotificationWrapper>
<>
<Header content {...headerProps} />
<Row
onClick={onClick}
@ -321,6 +320,6 @@ export function GroupInvite(props: GroupInviteProps): ReactElement {
/>
</ResponsiveRow>
</Row>
</NotificationWrapper>
</>
);
}

View File

@ -1,51 +0,0 @@
import { deSig, Graphs, UnreadStats } from '@urbit/api';
import { useCallback } from 'react';
import { SidebarAppConfig } from './types';
export function useGraphModule(
graphKeys: Set<string>,
graphs: Graphs,
graphUnreads: Record<string, Record<string, UnreadStats>>
): SidebarAppConfig {
const getStatus = useCallback(
(s: string) => {
const [, , host, name] = s.split('/');
const graphKey = `${deSig(host)}/${name}`;
if (!graphKeys.has(graphKey)) {
return 'unsubscribed';
}
const notifications = graphUnreads?.[s]?.['/']?.notifications;
if (
notifications &&
((typeof notifications === 'number' && notifications > 0)
|| typeof notifications === 'object' && notifications.length)
) {
return 'notification';
}
const unreads = graphUnreads?.[s]?.['/']?.unreads;
if (typeof unreads === 'number' ? unreads > 0 : unreads?.size ?? 0 > 0) {
return 'unread';
}
return undefined;
},
[graphs, graphKeys, graphUnreads]
);
const lastUpdated = useCallback((s: string) => {
// cant get link timestamps without loading posts
const last = graphUnreads?.[s]?.['/']?.last;
if(last) {
return last;
}
const stat = getStatus(s);
if(stat === 'unsubscribed') {
return 0;
}
return 1;
}, [getStatus, graphUnreads]);
return { getStatus, lastUpdated };
}

View File

@ -1,5 +1,5 @@
import React, { ReactElement, useCallback } from 'react';
import { Associations, Graph } from '@urbit/api';
import { Associations, Graph, Unreads } from '@urbit/api';
import { patp, patp2dec } from 'urbit-ob';
import _ from 'lodash';
@ -13,9 +13,8 @@ import useMetadataState from '~/logic/state/metadata';
import { useHistory } from 'react-router';
import { useShortcut } from '~/logic/state/settings';
function sidebarSort(pending: Set<string>): Record<SidebarSort, (a: string, b: string) => number> {
function sidebarSort(unreads: Unreads, pending: Set<string>): Record<SidebarSort, (a: string, b: string) => number> {
const { associations } = useMetadataState.getState();
const { unreads } = useHarkState.getState();
const alphabetical = (a: string, b: string) => {
const aAssoc = associations[a];
const bAssoc = associations[b];
@ -102,9 +101,10 @@ export function SidebarList(props: {
const inbox = useInbox();
const graphKeys = useGraphState(s => s.graphKeys);
const pending = useGraphState(s => s.pendingDms);
const unreads = useHarkState(s => s.unreads);
const ordered = getItems(associations, workspace, inbox, pending)
.sort(sidebarSort(pending)[config.sortBy]);
.sort(sidebarSort(unreads, pending)[config.sortBy]);
const history = useHistory();