mirror of
https://github.com/urbit/shrub.git
synced 2024-11-28 05:22:27 +03:00
Merge remote-tracking branch 'origin/master' into release/next-sys
This commit is contained in:
commit
d15d832ef4
@ -26,6 +26,6 @@
|
|||||||
<div id="portal-root"></div>
|
<div id="portal-root"></div>
|
||||||
<script src="/~landscape/js/channel.js"></script>
|
<script src="/~landscape/js/channel.js"></script>
|
||||||
<script src="/~landscape/js/session.js"></script>
|
<script src="/~landscape/js/session.js"></script>
|
||||||
<script src="/~btc/js/bundle/index.3e8bcc150ebd820dd3b2.js"></script>
|
<script src="/~btc/js/bundle/index.02730169f3d73fd22950.js"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
@ -5,8 +5,8 @@
|
|||||||
/- glob, *resource
|
/- glob, *resource
|
||||||
/+ default-agent, verb, dbug
|
/+ default-agent, verb, dbug
|
||||||
|%
|
|%
|
||||||
++ landscape-hash 0vrbiqe.v6al2.0b4jc.u9vp7.k1e0i
|
++ landscape-hash 0v4.56ejl.qq4js.h1i0m.t3e94.bg6qr
|
||||||
++ btc-wallet-hash 0v7.v4dng.o33qi.kc497.5jc02.ke5es
|
++ btc-wallet-hash 0v2.ifoe4.fbv35.aigir.66su4.fbspu
|
||||||
+$ state-0 [%0 hash=@uv glob=(unit (each glob:glob tid=@ta))]
|
+$ state-0 [%0 hash=@uv glob=(unit (each glob:glob tid=@ta))]
|
||||||
+$ state-1 [%1 =globs:glob]
|
+$ state-1 [%1 =globs:glob]
|
||||||
+$ all-states
|
+$ all-states
|
||||||
|
@ -24,6 +24,6 @@
|
|||||||
<div id="portal-root"></div>
|
<div id="portal-root"></div>
|
||||||
<script src="/~landscape/js/channel.js"></script>
|
<script src="/~landscape/js/channel.js"></script>
|
||||||
<script src="/~landscape/js/session.js"></script>
|
<script src="/~landscape/js/session.js"></script>
|
||||||
<script src="/~landscape/js/bundle/index.8074ae0006fba19803f5.js"></script>
|
<script src="/~landscape/js/bundle/index.306e3d6fb700897c1bc1.js"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
@ -25,8 +25,6 @@
|
|||||||
^- (list @tas)
|
^- (list @tas)
|
||||||
:~ %group-store
|
:~ %group-store
|
||||||
%metadata-store
|
%metadata-store
|
||||||
%contact-store
|
|
||||||
%contact-hook
|
|
||||||
%invite-store
|
%invite-store
|
||||||
%graph-store
|
%graph-store
|
||||||
==
|
==
|
||||||
|
@ -5,4 +5,4 @@ dojo:
|
|||||||
|
|
||||||
it should return with the following hash:
|
it should return with the following hash:
|
||||||
|
|
||||||
`0v7.v4dng.o33qi.kc497.5jc02.ke5es`
|
`0v2.ifoe4.fbv35.aigir.66su4.fbspu`
|
||||||
|
@ -103,6 +103,9 @@ const addMembers = (json: GroupUpdate, state: GroupState): GroupState => {
|
|||||||
if ('addMembers' in json) {
|
if ('addMembers' in json) {
|
||||||
const { resource, ships } = json.addMembers;
|
const { resource, ships } = json.addMembers;
|
||||||
const resourcePath = resourceAsPath(resource);
|
const resourcePath = resourceAsPath(resource);
|
||||||
|
if(!(resourcePath in state.groups)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
for (const member of ships) {
|
for (const member of ships) {
|
||||||
state.groups[resourcePath].members.add(member);
|
state.groups[resourcePath].members.add(member);
|
||||||
if (
|
if (
|
||||||
|
@ -177,9 +177,6 @@ const ChatEditor = React.forwardRef<CodeMirrorShim, ChatEditorProps>(({ inCodeMo
|
|||||||
}, [inCodeMode, placeholder]);
|
}, [inCodeMode, placeholder]);
|
||||||
|
|
||||||
function messageChange(editor, data, value) {
|
function messageChange(editor, data, value) {
|
||||||
if(value.endsWith('/')) {
|
|
||||||
editor.showHint(['test', 'foo']);
|
|
||||||
}
|
|
||||||
if (message !== '' && value == '') {
|
if (message !== '' && value == '') {
|
||||||
setMessage(value);
|
setMessage(value);
|
||||||
}
|
}
|
||||||
|
@ -138,7 +138,8 @@ class ChatWindow extends Component<
|
|||||||
}
|
}
|
||||||
if(this.unreadSet &&
|
if(this.unreadSet &&
|
||||||
this.dismissedInitialUnread() &&
|
this.dismissedInitialUnread() &&
|
||||||
this.virtualList!.startOffset() < 5) {
|
this.virtualList!.startOffset() < 5 &&
|
||||||
|
document.hasFocus()) {
|
||||||
this.props.dismissUnread();
|
this.props.dismissUnread();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -77,7 +77,7 @@ function GroupRoutes(props: { group: string; url: string }) {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
if(!graphKeys.has(`${ship.slice(1)}/${name}`)) {
|
if(!graphKeys.has(`${ship.slice(1)}/${name}`)) {
|
||||||
if(graphKeys.size > 0) {
|
if(graphKeys.size > 1) { // TODO: Better loading logic see https://github.com/urbit/landscape/issues/1063
|
||||||
return <Redirect
|
return <Redirect
|
||||||
to={toQuery(
|
to={toQuery(
|
||||||
{ auto: 'y', redir: location.pathname },
|
{ auto: 'y', redir: location.pathname },
|
||||||
|
@ -2,7 +2,7 @@ import { BaseAnchor, Box, Center, Col, Icon, Row, Text } from '@tlon/indigo-reac
|
|||||||
import { Association, GraphNode, resourceFromPath, GraphConfig } from '@urbit/api';
|
import { Association, GraphNode, resourceFromPath, GraphConfig } from '@urbit/api';
|
||||||
import React, { useCallback, useEffect, useState } from 'react';
|
import React, { useCallback, useEffect, useState } from 'react';
|
||||||
import _ from 'lodash';
|
import _ from 'lodash';
|
||||||
import { useHistory, useLocation } from 'react-router-dom';
|
import { Link, useLocation } from 'react-router-dom';
|
||||||
import {
|
import {
|
||||||
getPermalinkForGraph, GraphPermalink as IGraphPermalink, parsePermalink
|
getPermalinkForGraph, GraphPermalink as IGraphPermalink, parsePermalink
|
||||||
} from '~/logic/lib/permalinks';
|
} from '~/logic/lib/permalinks';
|
||||||
@ -76,7 +76,6 @@ function GraphPermalink(
|
|||||||
}
|
}
|
||||||
) {
|
) {
|
||||||
const { full = false, showOurContact, pending, graph, group, index, transcluded } = props;
|
const { full = false, showOurContact, pending, graph, group, index, transcluded } = props;
|
||||||
const history = useHistory();
|
|
||||||
const location = useLocation();
|
const location = useLocation();
|
||||||
const { ship, name } = resourceFromPath(graph);
|
const { ship, name } = resourceFromPath(graph);
|
||||||
const node = useGraphState(
|
const node = useGraphState(
|
||||||
@ -114,11 +113,6 @@ function GraphPermalink(
|
|||||||
const showTransclusion = Boolean(association && node && transcluded < 1);
|
const showTransclusion = Boolean(association && node && transcluded < 1);
|
||||||
const permalink = getPermalinkForGraph(group, graph, index);
|
const permalink = getPermalinkForGraph(group, graph, index);
|
||||||
|
|
||||||
const navigate = (e) => {
|
|
||||||
e.stopPropagation();
|
|
||||||
history.push(`/perma${permalink.slice(16)}`);
|
|
||||||
};
|
|
||||||
|
|
||||||
const [nodeGroupHost, nodeGroupName] = association?.group.split('/').slice(-2) ?? ['Unknown', 'Unknown'];
|
const [nodeGroupHost, nodeGroupName] = association?.group.split('/').slice(-2) ?? ['Unknown', 'Unknown'];
|
||||||
const [nodeChannelHost, nodeChannelName] = association?.resource
|
const [nodeChannelHost, nodeChannelName] = association?.resource
|
||||||
.split('/')
|
.split('/')
|
||||||
@ -141,15 +135,16 @@ function GraphPermalink(
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Col
|
<Col
|
||||||
|
as={Link}
|
||||||
|
to={`/perma${permalink.slice(16)}`}
|
||||||
width="100%"
|
width="100%"
|
||||||
bg="white"
|
bg="white"
|
||||||
maxWidth={full ? null : '500px'}
|
maxWidth={full ? null : '500px'}
|
||||||
border={full ? null : '1'}
|
border={full ? null : '1'}
|
||||||
borderColor="lightGray"
|
borderColor="lightGray"
|
||||||
borderRadius={2}
|
borderRadius={2}
|
||||||
cursor="pointer"
|
|
||||||
onClick={(e) => {
|
onClick={(e) => {
|
||||||
navigate(e);
|
e.stopPropagation();
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{loading && association && !errored && Placeholder((association.metadata.config as GraphConfig).graph)}
|
{loading && association && !errored && Placeholder((association.metadata.config as GraphConfig).graph)}
|
||||||
@ -167,7 +162,6 @@ function GraphPermalink(
|
|||||||
showTransclusion={showTransclusion}
|
showTransclusion={showTransclusion}
|
||||||
icon={getModuleIcon((association.metadata.config as GraphConfig).graph as GraphModule)}
|
icon={getModuleIcon((association.metadata.config as GraphConfig).graph as GraphModule)}
|
||||||
title={association.metadata.title}
|
title={association.metadata.title}
|
||||||
permalink={permalink}
|
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{association && isInSameResource && transcluded === 2 && !loading && (
|
{association && isInSameResource && transcluded === 2 && !loading && (
|
||||||
@ -176,7 +170,6 @@ function GraphPermalink(
|
|||||||
showTransclusion={showTransclusion}
|
showTransclusion={showTransclusion}
|
||||||
icon={getModuleIcon((association.metadata.config as GraphConfig).graph as GraphModule)}
|
icon={getModuleIcon((association.metadata.config as GraphConfig).graph as GraphModule)}
|
||||||
title={association.metadata.title}
|
title={association.metadata.title}
|
||||||
permalink={permalink}
|
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{isInSameResource && transcluded !== 2 && !loading && <Row height='2' />}
|
{isInSameResource && transcluded !== 2 && !loading && <Row height='2' />}
|
||||||
@ -185,7 +178,6 @@ function GraphPermalink(
|
|||||||
icon="Groups"
|
icon="Groups"
|
||||||
showDetails={false}
|
showDetails={false}
|
||||||
title={graph.slice(5)}
|
title={graph.slice(5)}
|
||||||
permalink={permalink}
|
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</Col>
|
</Col>
|
||||||
@ -195,7 +187,6 @@ function GraphPermalink(
|
|||||||
function PermalinkDetails(props: {
|
function PermalinkDetails(props: {
|
||||||
title: string;
|
title: string;
|
||||||
icon: any;
|
icon: any;
|
||||||
permalink: string;
|
|
||||||
showTransclusion?: boolean;
|
showTransclusion?: boolean;
|
||||||
showDetails?: boolean;
|
showDetails?: boolean;
|
||||||
known?: boolean;
|
known?: boolean;
|
||||||
|
@ -110,50 +110,52 @@ return false;
|
|||||||
group={group}
|
group={group}
|
||||||
isRelativeTime
|
isRelativeTime
|
||||||
></Author>
|
></Author>
|
||||||
<Box opacity={hovering ? '100%' : '0%'}>
|
{!post.pending &&
|
||||||
<Dropdown
|
<Box opacity={hovering ? '100%' : '0%'}>
|
||||||
alignX="right"
|
<Dropdown
|
||||||
alignY="top"
|
alignX="right"
|
||||||
options={
|
alignY="top"
|
||||||
<Col
|
options={
|
||||||
p="2"
|
<Col
|
||||||
border="1"
|
p="2"
|
||||||
borderRadius="1"
|
border="1"
|
||||||
borderColor="lightGray"
|
borderRadius="1"
|
||||||
backgroundColor="white"
|
borderColor="lightGray"
|
||||||
gapY="2"
|
backgroundColor="white"
|
||||||
>
|
gapY="2"
|
||||||
<Action bg="white" onClick={doCopy}>
|
>
|
||||||
{copyDisplay}
|
<Action bg="white" onClick={doCopy}>
|
||||||
</Action>
|
{copyDisplay}
|
||||||
{(window.ship == post?.author && !disabled) ? (
|
|
||||||
<ActionLink
|
|
||||||
color="blue"
|
|
||||||
to={{
|
|
||||||
pathname: props.baseUrl,
|
|
||||||
search: `?edit=${commentIndex}`
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
Update
|
|
||||||
</ActionLink>
|
|
||||||
) : null}
|
|
||||||
{(window.ship == post?.author || ourRole == 'admin') &&
|
|
||||||
!disabled ? (
|
|
||||||
<Action
|
|
||||||
height="unset"
|
|
||||||
bg="white"
|
|
||||||
onClick={onDelete}
|
|
||||||
destructive
|
|
||||||
>
|
|
||||||
Delete
|
|
||||||
</Action>
|
</Action>
|
||||||
) : null}
|
{(window.ship == post?.author && !disabled) ? (
|
||||||
</Col>
|
<ActionLink
|
||||||
}
|
color="blue"
|
||||||
>
|
to={{
|
||||||
<Icon icon="Ellipsis" />
|
pathname: props.baseUrl,
|
||||||
</Dropdown>
|
search: `?edit=${commentIndex}`
|
||||||
</Box>
|
}}
|
||||||
|
>
|
||||||
|
Update
|
||||||
|
</ActionLink>
|
||||||
|
) : null}
|
||||||
|
{(window.ship == post?.author || ourRole == 'admin') &&
|
||||||
|
!disabled ? (
|
||||||
|
<Action
|
||||||
|
height="unset"
|
||||||
|
bg="white"
|
||||||
|
onClick={onDelete}
|
||||||
|
destructive
|
||||||
|
>
|
||||||
|
Delete
|
||||||
|
</Action>
|
||||||
|
) : null}
|
||||||
|
</Col>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<Icon icon="Ellipsis" />
|
||||||
|
</Dropdown>
|
||||||
|
</Box>
|
||||||
|
}
|
||||||
</Row>
|
</Row>
|
||||||
<GraphContent
|
<GraphContent
|
||||||
borderRadius={1}
|
borderRadius={1}
|
||||||
|
@ -1,56 +1,63 @@
|
|||||||
import { Box, Col, Icon, Row, Text } from '@tlon/indigo-react';
|
import { Box, Col, Icon, Row, Text } from '@tlon/indigo-react';
|
||||||
import React, { ReactElement, useCallback } from 'react';
|
import React, { ReactElement, useCallback } from 'react';
|
||||||
import { useHistory } from 'react-router-dom';
|
import { Link } from 'react-router-dom';
|
||||||
import { useModal } from '~/logic/lib/useModal';
|
import { useModal } from '~/logic/lib/useModal';
|
||||||
import useMetadataState, { usePreview } from '~/logic/state/metadata';
|
import useMetadataState, { usePreview } from '~/logic/state/metadata';
|
||||||
import { PropFunc } from '~/types';
|
import { PropFunc } from '~/types';
|
||||||
import { JoinGroup } from '../landscape/components/JoinGroup';
|
import { JoinGroup } from '../landscape/components/JoinGroup';
|
||||||
import { MetadataIcon } from '../landscape/components/MetadataIcon';
|
import { MetadataIcon } from '../landscape/components/MetadataIcon';
|
||||||
|
|
||||||
export function GroupLink(
|
type GroupLinkProps = {
|
||||||
props: {
|
resource: string;
|
||||||
resource: string;
|
detailed?: boolean;
|
||||||
detailed?: boolean;
|
} & PropFunc<typeof Row>
|
||||||
} & PropFunc<typeof Row>
|
|
||||||
): ReactElement {
|
export function GroupLink({
|
||||||
const { resource, ...rest } = props;
|
resource,
|
||||||
|
borderColor,
|
||||||
|
...rest
|
||||||
|
}: GroupLinkProps): ReactElement {
|
||||||
const name = resource.slice(6);
|
const name = resource.slice(6);
|
||||||
const joined = useMetadataState(
|
const joined = useMetadataState(
|
||||||
useCallback(s => resource in s.associations.groups, [resource])
|
useCallback(s => resource in s.associations.groups, [resource])
|
||||||
);
|
);
|
||||||
const history = useHistory();
|
|
||||||
|
|
||||||
const { modal, showModal } = useModal({
|
const { modal, showModal } = useModal({
|
||||||
modal: <JoinGroup autojoin={name} />
|
modal: <JoinGroup autojoin={name} />
|
||||||
});
|
});
|
||||||
|
|
||||||
const { preview } = usePreview(resource);
|
const { preview } = usePreview(resource);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box
|
<>
|
||||||
maxWidth="500px"
|
|
||||||
cursor='pointer'
|
|
||||||
{...rest}
|
|
||||||
onClick={(e) => {
|
|
||||||
e.stopPropagation();
|
|
||||||
}}
|
|
||||||
backgroundColor='white'
|
|
||||||
borderColor={props.borderColor}
|
|
||||||
>
|
|
||||||
{modal}
|
{modal}
|
||||||
<Row
|
<Row
|
||||||
width="100%"
|
{...rest}
|
||||||
|
as={Link}
|
||||||
|
to={joined ? `/~landscape/ship/${name}` : `/perma/group/${name}`}
|
||||||
|
onClick={(e: React.MouseEvent<HTMLAnchorElement>) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
|
||||||
|
if (e.metaKey || e.ctrlKey) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
e.preventDefault();
|
||||||
|
showModal();
|
||||||
|
}}
|
||||||
flexShrink={1}
|
flexShrink={1}
|
||||||
alignItems="center"
|
alignItems="center"
|
||||||
|
width="100%"
|
||||||
|
maxWidth="500px"
|
||||||
py={2}
|
py={2}
|
||||||
pr={2}
|
pr={2}
|
||||||
onClick={
|
cursor='pointer'
|
||||||
joined ? () => history.push(`/~landscape/ship/${name}`) : showModal
|
backgroundColor='white'
|
||||||
}
|
borderColor={borderColor}
|
||||||
opacity={preview ? '1' : '0.6'}
|
opacity={preview ? '1' : '0.6'}
|
||||||
>
|
>
|
||||||
<MetadataIcon height={6} width={6} metadata={preview ? preview.metadata : { color: '0x0' , picture: '' }} />
|
<MetadataIcon height={6} width={6} metadata={preview ? preview.metadata : { color: '0x0' , picture: '' }} />
|
||||||
<Col>
|
<Col>
|
||||||
<Text ml={2} fontWeight="medium" mono={!preview}>
|
<Text ml={2} fontWeight="medium" mono={!preview}>
|
||||||
{preview ? preview.metadata.title : name}
|
{preview ? preview.metadata.title : name}
|
||||||
</Text>
|
</Text>
|
||||||
@ -70,6 +77,6 @@ export function GroupLink(
|
|||||||
</Box>
|
</Box>
|
||||||
</Col>
|
</Col>
|
||||||
</Row>
|
</Row>
|
||||||
</Box>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -8,7 +8,7 @@ import {
|
|||||||
Text
|
Text
|
||||||
} from '@tlon/indigo-react';
|
} from '@tlon/indigo-react';
|
||||||
import React, { useRef } from 'react';
|
import React, { useRef } from 'react';
|
||||||
import { useHistory } from 'react-router-dom';
|
import { Link, useHistory } from 'react-router-dom';
|
||||||
import { Sigil } from '~/logic/lib/sigil';
|
import { Sigil } from '~/logic/lib/sigil';
|
||||||
import { uxToHex } from '~/logic/lib/util';
|
import { uxToHex } from '~/logic/lib/util';
|
||||||
import useContactState from '~/logic/state/contact';
|
import useContactState from '~/logic/state/contact';
|
||||||
@ -75,11 +75,12 @@ const StatusBar = (props) => {
|
|||||||
>
|
>
|
||||||
<Row>
|
<Row>
|
||||||
<Button
|
<Button
|
||||||
|
as={Link}
|
||||||
|
to="/"
|
||||||
width='32px'
|
width='32px'
|
||||||
borderColor='lightGray'
|
borderColor='lightGray'
|
||||||
mr={2}
|
mr={2}
|
||||||
px={2}
|
px={2}
|
||||||
onClick={() => history.push('/')}
|
|
||||||
{...props}
|
{...props}
|
||||||
>
|
>
|
||||||
<Icon icon='Dashboard' color='black' />
|
<Icon icon='Dashboard' color='black' />
|
||||||
@ -126,9 +127,10 @@ const StatusBar = (props) => {
|
|||||||
<Icon icon="Bug" color="#000000" />
|
<Icon icon="Bug" color="#000000" />
|
||||||
</StatusBarItem>
|
</StatusBarItem>
|
||||||
<StatusBarItem
|
<StatusBarItem
|
||||||
|
as={Link}
|
||||||
|
to="/~landscape/messages"
|
||||||
width='32px'
|
width='32px'
|
||||||
mr={2}
|
mr={2}
|
||||||
onClick={() => props.history.push('/~landscape/messages')}
|
|
||||||
>
|
>
|
||||||
<Icon icon='Messages' />
|
<Icon icon='Messages' />
|
||||||
</StatusBarItem>
|
</StatusBarItem>
|
||||||
@ -150,24 +152,26 @@ const StatusBar = (props) => {
|
|||||||
boxShadow='0px 0px 0px 3px'
|
boxShadow='0px 0px 0px 3px'
|
||||||
>
|
>
|
||||||
<Row
|
<Row
|
||||||
|
as={Link}
|
||||||
|
to={`/~profile/~${ship}`}
|
||||||
color='black'
|
color='black'
|
||||||
cursor='pointer'
|
cursor='pointer'
|
||||||
fontSize={1}
|
fontSize={1}
|
||||||
fontWeight='500'
|
fontWeight='500'
|
||||||
px={3}
|
px={3}
|
||||||
py={2}
|
py={2}
|
||||||
onClick={() => history.push(`/~profile/~${ship}`)}
|
|
||||||
>
|
>
|
||||||
View Profile
|
View Profile
|
||||||
</Row>
|
</Row>
|
||||||
<Row
|
<Row
|
||||||
|
as={Link}
|
||||||
|
to="/~settings"
|
||||||
color='black'
|
color='black'
|
||||||
cursor='pointer'
|
cursor='pointer'
|
||||||
fontSize={1}
|
fontSize={1}
|
||||||
fontWeight='500'
|
fontWeight='500'
|
||||||
px={3}
|
px={3}
|
||||||
py={2}
|
py={2}
|
||||||
onClick={() => history.push('/~settings')}
|
|
||||||
>
|
>
|
||||||
System Preferences
|
System Preferences
|
||||||
</Row>
|
</Row>
|
||||||
|
@ -1,17 +1,20 @@
|
|||||||
import { Col, ColProps } from '@tlon/indigo-react';
|
import { Col, ColProps } from '@tlon/indigo-react';
|
||||||
import { Post } from '@urbit/api';
|
import { Post } from '@urbit/api';
|
||||||
import React, { ReactElement } from 'react';
|
import React, { ReactElement, useCallback, useState } from 'react';
|
||||||
import styled from 'styled-components';
|
import styled, { css } from 'styled-components';
|
||||||
import { GraphContent } from '~/views/landscape/components/Graph/GraphContent';
|
import { GraphContent } from '~/views/landscape/components/Graph/GraphContent';
|
||||||
|
|
||||||
type TruncateProps = ColProps & {
|
type TruncateProps = ColProps & {
|
||||||
truncate?: number;
|
truncate: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
const TruncatedBox = styled(Col)<TruncateProps>`
|
const TruncatedBox = styled(Col)<TruncateProps>`
|
||||||
display: -webkit-box;
|
display: -webkit-box;
|
||||||
-webkit-line-clamp: ${p => p.truncate ?? 'unset'};
|
|
||||||
-webkit-box-orient: vertical;
|
-webkit-box-orient: vertical;
|
||||||
|
${p => p.truncate && css`
|
||||||
|
max-height: 300px;
|
||||||
|
mask-image: linear-gradient(to bottom, rgba(0,0,0,1) 0%, rgba(0,0,0,1) 60%, transparent 100%);
|
||||||
|
`}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
interface PostContentProps {
|
interface PostContentProps {
|
||||||
@ -20,16 +23,24 @@ interface PostContentProps {
|
|||||||
isReply: boolean;
|
isReply: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
const PostContent = (props: PostContentProps): ReactElement => {
|
const PostContent = ({ post, isParent }: PostContentProps): ReactElement => {
|
||||||
const { post, isParent } = props;
|
const [height, setHeight] = useState(0);
|
||||||
|
const showFade = !isParent && height >= 300;
|
||||||
|
|
||||||
|
const measuredRef = useCallback((node) => {
|
||||||
|
if (node !== null) {
|
||||||
|
setHeight(node.getBoundingClientRect().height);
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<TruncatedBox
|
<TruncatedBox
|
||||||
|
ref={measuredRef}
|
||||||
|
truncate={showFade}
|
||||||
display="-webkit-box"
|
display="-webkit-box"
|
||||||
width="90%"
|
width="90%"
|
||||||
px={2}
|
px={2}
|
||||||
pb={2}
|
pb={2}
|
||||||
truncate={isParent ? null : 8}
|
|
||||||
textOverflow="ellipsis"
|
textOverflow="ellipsis"
|
||||||
overflow="hidden"
|
overflow="hidden"
|
||||||
>
|
>
|
||||||
|
2
pkg/npm/api/package-lock.json
generated
2
pkg/npm/api/package-lock.json
generated
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@urbit/api",
|
"name": "@urbit/api",
|
||||||
"version": "1.2.0",
|
"version": "1.3.0",
|
||||||
"lockfileVersion": 1,
|
"lockfileVersion": 1,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@urbit/api",
|
"name": "@urbit/api",
|
||||||
"version": "1.2.0",
|
"version": "1.3.0",
|
||||||
"description": "",
|
"description": "",
|
||||||
"repository": {
|
"repository": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
@ -21,7 +21,7 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/runtime": "^7.12.5",
|
"@babel/runtime": "^7.12.5",
|
||||||
"@types/lodash": "^4.14.168",
|
"@types/lodash": "^4.14.168",
|
||||||
"@urbit/eslint-config": "^1.0.1",
|
"@urbit/eslint-config": "^1.0.2",
|
||||||
"big-integer": "^1.6.48",
|
"big-integer": "^1.6.48",
|
||||||
"immer": "^9.0.1",
|
"immer": "^9.0.1",
|
||||||
"lodash": "^4.17.20",
|
"lodash": "^4.17.20",
|
||||||
|
2
pkg/npm/eslint-config/package-lock.json
generated
2
pkg/npm/eslint-config/package-lock.json
generated
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@urbit/eslint-config",
|
"name": "@urbit/eslint-config",
|
||||||
"version": "1.0.1",
|
"version": "1.0.2",
|
||||||
"lockfileVersion": 1,
|
"lockfileVersion": 1,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@urbit/eslint-config",
|
"name": "@urbit/eslint-config",
|
||||||
"version": "1.0.1",
|
"version": "1.0.2",
|
||||||
"description": "",
|
"description": "",
|
||||||
"repository": {
|
"repository": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
|
2
pkg/npm/http-api/package-lock.json
generated
2
pkg/npm/http-api/package-lock.json
generated
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@urbit/http-api",
|
"name": "@urbit/http-api",
|
||||||
"version": "1.2.3",
|
"version": "1.3.0",
|
||||||
"lockfileVersion": 1,
|
"lockfileVersion": 1,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@urbit/http-api",
|
"name": "@urbit/http-api",
|
||||||
"version": "1.2.3",
|
"version": "1.3.0",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"description": "Library to interact with an Urbit ship over HTTP",
|
"description": "Library to interact with an Urbit ship over HTTP",
|
||||||
"repository": {
|
"repository": {
|
||||||
|
@ -164,14 +164,18 @@ export class Urbit {
|
|||||||
/**
|
/**
|
||||||
* Initializes the SSE pipe for the appropriate channel.
|
* Initializes the SSE pipe for the appropriate channel.
|
||||||
*/
|
*/
|
||||||
eventSource(): Promise<void> {
|
async eventSource(): Promise<void> {
|
||||||
if(this.lastEventId === 0) {
|
if(this.sseClientInitialized) {
|
||||||
// Can't receive events until the channel is open
|
return Promise.resolve();
|
||||||
this.skipDebounce = true;
|
|
||||||
return this.poke({ app: 'hood', mark: 'helm-hi', json: 'Opening API channel' }).then(() => {});
|
|
||||||
}
|
}
|
||||||
|
if(this.lastEventId === 0) {
|
||||||
|
// Can't receive events until the channel is open,
|
||||||
|
// so poke and open then
|
||||||
|
await this.poke({ app: 'hood', mark: 'helm-hi', json: 'Opening API channel' });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.sseClientInitialized = true;
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
if (!this.sseClientInitialized) {
|
|
||||||
const sseOptions: SSEOptions = {
|
const sseOptions: SSEOptions = {
|
||||||
headers: {}
|
headers: {}
|
||||||
};
|
};
|
||||||
@ -193,6 +197,7 @@ export class Urbit {
|
|||||||
resolve();
|
resolve();
|
||||||
return; // everything's good
|
return; // everything's good
|
||||||
} else {
|
} else {
|
||||||
|
this.onError && this.onError(new Error('bad response'));
|
||||||
reject();
|
reject();
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@ -201,14 +206,13 @@ export class Urbit {
|
|||||||
console.log('Received SSE: ', event);
|
console.log('Received SSE: ', event);
|
||||||
}
|
}
|
||||||
if (!event.id) return;
|
if (!event.id) return;
|
||||||
this.ack(Number(event.id));
|
this.lastEventId = parseInt(event.id, 10);
|
||||||
if (event.data && JSON.parse(event.data)) {
|
if((this.lastEventId - this.lastAcknowledgedEventId) > 20) {
|
||||||
|
this.ack(this.lastEventId);
|
||||||
const data: any = JSON.parse(event.data);
|
}
|
||||||
|
|
||||||
if (data.response === 'diff') {
|
if (event.data && JSON.parse(event.data)) {
|
||||||
this.clearQueue();
|
const data: any = JSON.parse(event.data);
|
||||||
}
|
|
||||||
|
|
||||||
if (data.response === 'poke' && this.outstandingPokes.has(data.id)) {
|
if (data.response === 'poke' && this.outstandingPokes.has(data.id)) {
|
||||||
const funcs = this.outstandingPokes.get(data.id);
|
const funcs = this.outstandingPokes.get(data.id);
|
||||||
@ -221,8 +225,8 @@ export class Urbit {
|
|||||||
console.error('Invalid poke response', data);
|
console.error('Invalid poke response', data);
|
||||||
}
|
}
|
||||||
this.outstandingPokes.delete(data.id);
|
this.outstandingPokes.delete(data.id);
|
||||||
} else if (data.response === 'subscribe' ||
|
} else if (data.response === 'subscribe'
|
||||||
(data.response === 'poke' && this.outstandingSubscriptions.has(data.id))) {
|
&& this.outstandingSubscriptions.has(data.id)) {
|
||||||
const funcs = this.outstandingSubscriptions.get(data.id);
|
const funcs = this.outstandingSubscriptions.get(data.id);
|
||||||
if (data.hasOwnProperty('err')) {
|
if (data.hasOwnProperty('err')) {
|
||||||
console.error(data.err);
|
console.error(data.err);
|
||||||
@ -231,7 +235,11 @@ export class Urbit {
|
|||||||
}
|
}
|
||||||
} else if (data.response === 'diff' && this.outstandingSubscriptions.has(data.id)) {
|
} else if (data.response === 'diff' && this.outstandingSubscriptions.has(data.id)) {
|
||||||
const funcs = this.outstandingSubscriptions.get(data.id);
|
const funcs = this.outstandingSubscriptions.get(data.id);
|
||||||
funcs.event(data.json);
|
try {
|
||||||
|
funcs.event(data.json);
|
||||||
|
} catch (e) {
|
||||||
|
console.error('Failed to call subscription event callback', e);
|
||||||
|
}
|
||||||
} else if (data.response === 'quit' && this.outstandingSubscriptions.has(data.id)) {
|
} else if (data.response === 'quit' && this.outstandingSubscriptions.has(data.id)) {
|
||||||
const funcs = this.outstandingSubscriptions.get(data.id);
|
const funcs = this.outstandingSubscriptions.get(data.id);
|
||||||
funcs.quit(data);
|
funcs.quit(data);
|
||||||
@ -257,10 +265,7 @@ export class Urbit {
|
|||||||
|
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
this.sseClientInitialized = true;
|
})
|
||||||
}
|
|
||||||
resolve();
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -272,12 +277,6 @@ export class Urbit {
|
|||||||
this.abort.abort();
|
this.abort.abort();
|
||||||
this.abort = new AbortController();
|
this.abort = new AbortController();
|
||||||
this.uid = `${Math.floor(Date.now() / 1000)}-${hexString(6)}`;
|
this.uid = `${Math.floor(Date.now() / 1000)}-${hexString(6)}`;
|
||||||
if(this.debounceTimer) {
|
|
||||||
clearTimeout(this.debounceTimer);
|
|
||||||
this.debounceTimer = null;
|
|
||||||
}
|
|
||||||
this.calm = true;
|
|
||||||
this.outstandingJSON = [];
|
|
||||||
this.lastEventId = 0;
|
this.lastEventId = 0;
|
||||||
this.lastAcknowledgedEventId = 0;
|
this.lastAcknowledgedEventId = 0;
|
||||||
this.outstandingSubscriptions = new Map();
|
this.outstandingSubscriptions = new Map();
|
||||||
@ -299,6 +298,7 @@ export class Urbit {
|
|||||||
* @param eventId The event to acknowledge.
|
* @param eventId The event to acknowledge.
|
||||||
*/
|
*/
|
||||||
private async ack(eventId: number): Promise<number | void> {
|
private async ack(eventId: number): Promise<number | void> {
|
||||||
|
this.lastAcknowledgedEventId = eventId;
|
||||||
const message: Message = {
|
const message: Message = {
|
||||||
action: 'ack',
|
action: 'ack',
|
||||||
'event-id': eventId
|
'event-id': eventId
|
||||||
@ -307,90 +307,18 @@ export class Urbit {
|
|||||||
return eventId;
|
return eventId;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
private async sendJSONtoChannel(...json: Message[]): Promise<void> {
|
||||||
* This is a wrapper method that can be used to send any action with data.
|
const response = await fetch(this.channelUrl, {
|
||||||
*
|
...this.fetchOptions,
|
||||||
* Every message sent has some common parameters, like method, headers, and data
|
method: 'PUT',
|
||||||
* structure, so this method exists to prevent duplication.
|
body: JSON.stringify(json)
|
||||||
*
|
|
||||||
* @param action The action to send
|
|
||||||
* @param data The data to send with the action
|
|
||||||
*
|
|
||||||
* @returns void | number If successful, returns the number of the message that was sent
|
|
||||||
*/
|
|
||||||
// async sendMessage(action: Action, data?: object): Promise<number | void> {
|
|
||||||
// const id = this.getEventId();
|
|
||||||
// if (this.verbose) {
|
|
||||||
// console.log(`Sending message ${id}:`, action, data,);
|
|
||||||
// }
|
|
||||||
// const message: Message = { id, action, ...data };
|
|
||||||
// await this.sendJSONtoChannel(message);
|
|
||||||
// return id;
|
|
||||||
// }
|
|
||||||
|
|
||||||
private outstandingJSON: Message[] = [];
|
|
||||||
|
|
||||||
private debounceTimer: NodeJS.Timeout = null;
|
|
||||||
private debounceInterval = 10;
|
|
||||||
private skipDebounce = false;
|
|
||||||
private calm = true;
|
|
||||||
|
|
||||||
private sendJSONtoChannel(json: Message): Promise<boolean | void> {
|
|
||||||
this.outstandingJSON.push(json);
|
|
||||||
return this.processQueue();
|
|
||||||
}
|
|
||||||
|
|
||||||
private processQueue(): Promise<boolean | void> {
|
|
||||||
return new Promise(async (resolve, reject) => {
|
|
||||||
const process = async () => {
|
|
||||||
if (this.calm) {
|
|
||||||
if (this.outstandingJSON.length === 0) resolve(true);
|
|
||||||
this.calm = false; // We are now occupied
|
|
||||||
const json = this.outstandingJSON;
|
|
||||||
const body = JSON.stringify(json);
|
|
||||||
this.outstandingJSON = [];
|
|
||||||
if (body === '[]') {
|
|
||||||
this.calm = true;
|
|
||||||
return resolve(false);
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
const response = await fetch(this.channelUrl, {
|
|
||||||
...this.fetchOptions,
|
|
||||||
method: 'PUT',
|
|
||||||
body
|
|
||||||
});
|
|
||||||
if(!response.ok) {
|
|
||||||
throw new Error('failed to PUT');
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.log(error);
|
|
||||||
json.forEach(failed => this.outstandingJSON.push(failed));
|
|
||||||
if (this.onError) {
|
|
||||||
this.onError(error);
|
|
||||||
} else {
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
this.calm = true;
|
|
||||||
if (!this.sseClientInitialized) {
|
|
||||||
this.eventSource().then(resolve); // We can open the channel for subscriptions once we've sent data over it
|
|
||||||
}
|
|
||||||
resolve(true);
|
|
||||||
} else {
|
|
||||||
clearTimeout(this.debounceTimer);
|
|
||||||
this.debounceTimer = setTimeout(process, this.debounceInterval);
|
|
||||||
resolve(false);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if(this.skipDebounce) {
|
|
||||||
process();
|
|
||||||
this.skipDebounce = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.debounceTimer = setTimeout(process, this.debounceInterval);
|
|
||||||
|
|
||||||
|
|
||||||
});
|
});
|
||||||
|
if(!response.ok) {
|
||||||
|
throw new Error('Failed to PUT channel');
|
||||||
|
}
|
||||||
|
if(!this.sseClientInitialized) {
|
||||||
|
await this.eventSource();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -433,24 +361,6 @@ export class Urbit {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
// resetDebounceTimer() {
|
|
||||||
// if (this.debounceTimer) {
|
|
||||||
// clearTimeout(this.debounceTimer);
|
|
||||||
// this.debounceTimer = null;
|
|
||||||
// }
|
|
||||||
// this.calm = false;
|
|
||||||
// this.debounceTimer = setTimeout(() => {
|
|
||||||
// this.calm = true;
|
|
||||||
// }, this.debounceInterval);
|
|
||||||
// }
|
|
||||||
|
|
||||||
clearQueue() {
|
|
||||||
clearTimeout(this.debounceTimer);
|
|
||||||
this.debounceTimer = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Pokes a ship with data.
|
* Pokes a ship with data.
|
||||||
*
|
*
|
||||||
@ -458,7 +368,7 @@ export class Urbit {
|
|||||||
* @param mark The mark of the data being sent
|
* @param mark The mark of the data being sent
|
||||||
* @param json The data to send
|
* @param json The data to send
|
||||||
*/
|
*/
|
||||||
poke<T>(params: PokeInterface<T>): Promise<number> {
|
async poke<T>(params: PokeInterface<T>): Promise<number> {
|
||||||
const {
|
const {
|
||||||
app,
|
app,
|
||||||
mark,
|
mark,
|
||||||
@ -472,29 +382,30 @@ export class Urbit {
|
|||||||
ship: this.ship,
|
ship: this.ship,
|
||||||
...params
|
...params
|
||||||
};
|
};
|
||||||
return new Promise((resolve, reject) => {
|
const message: Message = {
|
||||||
const message: Message = {
|
id: this.getEventId(),
|
||||||
id: this.getEventId(),
|
action: 'poke',
|
||||||
action: 'poke',
|
ship,
|
||||||
ship,
|
app,
|
||||||
app,
|
mark,
|
||||||
mark,
|
json
|
||||||
json
|
};
|
||||||
};
|
const [send, result] = await Promise.all([
|
||||||
this.outstandingPokes.set(message.id, {
|
this.sendJSONtoChannel(message),
|
||||||
onSuccess: () => {
|
new Promise<number>((resolve, reject) => {
|
||||||
onSuccess();
|
this.outstandingPokes.set(message.id, {
|
||||||
resolve(message.id);
|
onSuccess: () => {
|
||||||
},
|
onSuccess();
|
||||||
onError: (event) => {
|
resolve(message.id);
|
||||||
onError(event);
|
},
|
||||||
reject(event.err);
|
onError: (event) => {
|
||||||
}
|
onError(event);
|
||||||
});
|
reject(event.err);
|
||||||
this.sendJSONtoChannel(message).then(() => {
|
}
|
||||||
resolve(message.id);
|
});
|
||||||
});
|
})
|
||||||
});
|
]);
|
||||||
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -1,5 +1,7 @@
|
|||||||
|
|
||||||
import Urbit from '../src';
|
import Urbit from '../src';
|
||||||
import { Readable } from 'streams';
|
import { Readable } from 'streams';
|
||||||
|
import 'jest';
|
||||||
|
|
||||||
function fakeSSE(messages = [], timeout = 0) {
|
function fakeSSE(messages = [], timeout = 0) {
|
||||||
const ourMessages = [...messages];
|
const ourMessages = [...messages];
|
||||||
@ -60,7 +62,6 @@ describe('Initialisation', () => {
|
|||||||
let fetchSpy;
|
let fetchSpy;
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
airlock = new Urbit('', '+code');
|
airlock = new Urbit('', '+code');
|
||||||
airlock.debounceInterval = 10;
|
|
||||||
});
|
});
|
||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
fetchSpy.mockReset();
|
fetchSpy.mockReset();
|
||||||
@ -73,7 +74,7 @@ describe('Initialisation', () => {
|
|||||||
Promise.resolve({ ok: true, body: fakeSSE() })
|
Promise.resolve({ ok: true, body: fakeSSE() })
|
||||||
)
|
)
|
||||||
.mockImplementationOnce(() =>
|
.mockImplementationOnce(() =>
|
||||||
Promise.resolve({ ok: true, body: fakeSSE() })
|
Promise.resolve({ ok: true, body: fakeSSE([ack(1)]) })
|
||||||
);
|
);
|
||||||
await airlock.eventSource();
|
await airlock.eventSource();
|
||||||
|
|
||||||
@ -82,13 +83,16 @@ describe('Initialisation', () => {
|
|||||||
it('should handle failures', async () => {
|
it('should handle failures', async () => {
|
||||||
fetchSpy = jest.spyOn(window, 'fetch');
|
fetchSpy = jest.spyOn(window, 'fetch');
|
||||||
fetchSpy
|
fetchSpy
|
||||||
.mockImplementation(() =>
|
.mockImplementationOnce(() =>
|
||||||
|
Promise.resolve({ ok: true, body: fakeSSE() })
|
||||||
|
)
|
||||||
|
.mockImplementationOnce(() =>
|
||||||
Promise.resolve({ ok: false, body: fakeSSE() })
|
Promise.resolve({ ok: false, body: fakeSSE() })
|
||||||
)
|
)
|
||||||
|
|
||||||
airlock.onError = jest.fn();
|
airlock.onError = jest.fn();
|
||||||
try {
|
try {
|
||||||
await airlock.eventSource();
|
await airlock.eventSource();
|
||||||
wait(100);
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
expect(airlock.onError).toHaveBeenCalled();
|
expect(airlock.onError).toHaveBeenCalled();
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user