mirror of
https://github.com/urbit/shrub.git
synced 2024-12-23 19:05:48 +03:00
js: removed demo post UI
This commit is contained in:
parent
d237fb9ad6
commit
4cd093df1a
@ -8,7 +8,6 @@ import { light } from '@tlon/indigo-react';
|
||||
|
||||
import LaunchApp from './apps/launch/app';
|
||||
import ChatApp from './apps/chat/app';
|
||||
import PostApp from './apps/post/app';
|
||||
import DojoApp from './apps/dojo/app';
|
||||
import GroupsApp from './apps/groups/app';
|
||||
import LinksApp from './apps/links/app';
|
||||
@ -103,15 +102,6 @@ export default class App extends React.Component {
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
<Route path="/~post" render={ p => (
|
||||
<PostApp
|
||||
ship={this.ship}
|
||||
channel={channel}
|
||||
selectedGroups={selectedGroups}
|
||||
{...p}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
<Route path="/~dojo" render={ p => (
|
||||
<DojoApp
|
||||
ship={this.ship}
|
||||
|
@ -1,139 +0,0 @@
|
||||
import BaseApi from './base';
|
||||
|
||||
|
||||
class PrivateHelper extends BaseApi {
|
||||
graphAction(data) {
|
||||
this.action('graph-view', 'json', data);
|
||||
}
|
||||
|
||||
addGraph(ship = 'zod', name = 'asdf', graph = {}) {
|
||||
this.graphAction({
|
||||
'add-graph': {
|
||||
resource: {
|
||||
ship: `~${ship}`,
|
||||
name
|
||||
},
|
||||
graph
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
removeGraph(ship = 'zod', name = 'asdf') {
|
||||
this.graphAction({
|
||||
'remove-graph': {
|
||||
resource: {
|
||||
ship: `~${ship}`,
|
||||
name
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
createPost(contents, parentIndex = '') {
|
||||
return {
|
||||
author: `~${window.ship}`,
|
||||
index: parentIndex + '/' + Date.now(),
|
||||
'time-sent': Date.now(),
|
||||
contents,
|
||||
hash: null,
|
||||
signatures: []
|
||||
};
|
||||
}
|
||||
|
||||
addPost(ship = 'zod', name = 'asdf', post) {
|
||||
let nodes = {};
|
||||
nodes[post.index] = {
|
||||
post,
|
||||
children: { empty: null }
|
||||
};
|
||||
|
||||
this.graphAction({
|
||||
'add-nodes': {
|
||||
resource: {
|
||||
ship: `~${ship}`,
|
||||
name
|
||||
},
|
||||
nodes
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
addNodes(ship = 'zod', name ='asdf', nodes = {}) {
|
||||
this.graphAction({
|
||||
'add-nodes': {
|
||||
resource: {
|
||||
ship: `~${ship}`,
|
||||
name
|
||||
},
|
||||
nodes
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
removeNodes(ship = 'zod', name = 'asdf', indices = []) {
|
||||
this.graphAction({
|
||||
'remove-nodes': {
|
||||
resource: {
|
||||
ship: `~${ship}`,
|
||||
name
|
||||
},
|
||||
indices
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
addSignatures() {
|
||||
this.graphAction();
|
||||
}
|
||||
|
||||
removeSignatures() {
|
||||
this.graphAction();
|
||||
}
|
||||
|
||||
addTag() {
|
||||
this.graphAction();
|
||||
}
|
||||
|
||||
removeTag() {
|
||||
this.graphAction();
|
||||
}
|
||||
|
||||
fetch(connection = 0) {
|
||||
this.action('graph-view', 'graph-view-action', {
|
||||
fetch: {
|
||||
connection,
|
||||
type: { all: null }
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export default class GraphApi {
|
||||
constructor(ship, channel, store) {
|
||||
const helper = new PrivateHelper(ship, channel, store);
|
||||
|
||||
this.ship = ship;
|
||||
this.subscribe = helper.subscribe.bind(helper);
|
||||
|
||||
// store
|
||||
this.addGraph = helper.addGraph.bind(helper);
|
||||
this.removeGraph = helper.removeGraph.bind(helper);
|
||||
|
||||
this.addNodes = helper.addNodes.bind(helper);
|
||||
this.removeNodes = helper.removeNodes.bind(helper);
|
||||
|
||||
this.addSignatures = helper.addSignatures.bind(helper);
|
||||
this.removeSignatures = helper.removeSignatures.bind(helper);
|
||||
|
||||
this.addTag = helper.addTag.bind(helper);
|
||||
this.removeTag = helper.removeTag.bind(helper);
|
||||
|
||||
// view
|
||||
this.fetch = helper.fetch.bind(helper);
|
||||
|
||||
// helpers
|
||||
this.createPost = helper.createPost.bind(helper);
|
||||
this.addPost = helper.addPost.bind(helper);
|
||||
}
|
||||
}
|
||||
|
@ -1,197 +0,0 @@
|
||||
import React from 'react';
|
||||
import { Route } from 'react-router-dom';
|
||||
|
||||
import GraphApi from '../../api/graph';
|
||||
import GraphStore from '../../store/graph';
|
||||
import GraphSubscription from '../../subscription/graph';
|
||||
|
||||
|
||||
import './css/custom.css';
|
||||
|
||||
import { Skeleton } from './components/skeleton';
|
||||
import { Sidebar } from './components/sidebar';
|
||||
import { PostScreen } from './components/post';
|
||||
import { NodeTreeScreen } from './components/node-tree';
|
||||
import { NewScreen } from './components/new';
|
||||
|
||||
|
||||
export default class PostApp extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.store = new GraphStore();
|
||||
this.state = this.store.state;
|
||||
this.resetControllers();
|
||||
}
|
||||
|
||||
resetControllers() {
|
||||
this.api = null;
|
||||
this.subscription = null;
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
document.title = 'OS1 - Chat';
|
||||
// preload spinner asset
|
||||
new Image().src = '/~landscape/img/Spinner.png';
|
||||
|
||||
this.store.setStateHandler(this.setState.bind(this));
|
||||
const channel = new this.props.channel();
|
||||
this.api = new GraphApi(this.props.ship, channel, this.store);
|
||||
|
||||
this.subscription = new GraphSubscription(this.store, this.api, channel);
|
||||
this.subscription.start();
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
this.subscription.delete();
|
||||
this.store.clear();
|
||||
this.store.setStateHandler(() => {});
|
||||
this.resetControllers();
|
||||
}
|
||||
|
||||
render() {
|
||||
const { state, props } = this;
|
||||
|
||||
if (!this.api || !this.subscription) {
|
||||
return <div></div>;
|
||||
}
|
||||
|
||||
const renderChannelSidebar = (props, resource) => (
|
||||
<Sidebar
|
||||
keys={state.keys}
|
||||
api={this.api}
|
||||
resource={resource}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Route
|
||||
exact
|
||||
path="/~post"
|
||||
render={(props) => {
|
||||
return (
|
||||
<Skeleton
|
||||
chatHideonMobile={true}
|
||||
sidebarShown={state.sidebarShown}
|
||||
sidebar={renderChannelSidebar(props)}
|
||||
>
|
||||
<div className="h-100 w-100 overflow-x-hidden flex flex-column bg-white bg-gray0-d">
|
||||
<div className="pl3 pr3 pt2 dt pb3 w-100 h-100">
|
||||
<p className="f8 pt3 gray2 w-100 h-100 dtc v-mid tc">
|
||||
Select, create, or join a chat to begin.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</Skeleton>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
<Route
|
||||
exact
|
||||
path="/~post/new"
|
||||
render={(props) => {
|
||||
return (
|
||||
<Skeleton
|
||||
sidebarHideOnMobile={true}
|
||||
sidebar={renderChannelSidebar(props)}
|
||||
sidebarShown={state.sidebarShown}
|
||||
>
|
||||
<NewScreen
|
||||
api={this.api}
|
||||
graphs={state.graphs || {}}
|
||||
{...props}
|
||||
/>
|
||||
</Skeleton>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
<Route
|
||||
exact
|
||||
path="/~post/room/:ship/:name"
|
||||
render={(props) => {
|
||||
let resource =
|
||||
`${props.match.params.ship}/${props.match.params.name}`;
|
||||
const graph = state.graphs[resource] || new Map();
|
||||
|
||||
return (
|
||||
<Skeleton
|
||||
sidebarHideOnMobile={true}
|
||||
sidebarShown={state.sidebarShown}
|
||||
sidebar={renderChannelSidebar(props, resource)}
|
||||
>
|
||||
<PostScreen
|
||||
resource={{
|
||||
ship: props.match.params.ship,
|
||||
name: props.match.params.name
|
||||
}}
|
||||
api={this.api}
|
||||
subscription={this.subscription}
|
||||
graph={graph}
|
||||
sidebarShown={state.sidebarShown}
|
||||
{...props}
|
||||
/>
|
||||
</Skeleton>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
<Route
|
||||
exact
|
||||
path="/~post/room/:ship/:name/:nodeId+"
|
||||
render={(props) => {
|
||||
let resource =
|
||||
`${props.match.params.ship}/${props.match.params.name}`;
|
||||
let index = props.match.params.nodeId
|
||||
.split('/').map((ind) => {
|
||||
return parseInt(ind, 10);
|
||||
});
|
||||
let graph = state.graphs[resource] || new Map();
|
||||
let node = {
|
||||
post: this.api.createPost([]),
|
||||
children: new Map()
|
||||
};
|
||||
|
||||
while (index.length > 0) {
|
||||
if (!node) {
|
||||
return <div></div>;
|
||||
}
|
||||
|
||||
node = graph.get(index[0]);
|
||||
graph = (!!node && 'children' in node) ?
|
||||
node.children : new Map();
|
||||
index = index.slice(1);
|
||||
}
|
||||
|
||||
if (!node) {
|
||||
node = {
|
||||
post: this.api.createPost([]),
|
||||
children: new Map()
|
||||
};
|
||||
}
|
||||
|
||||
return (
|
||||
<Skeleton
|
||||
sidebarHideOnMobile={true}
|
||||
sidebarShown={state.sidebarShown}
|
||||
sidebar={renderChannelSidebar(props, resource)}
|
||||
>
|
||||
<NodeTreeScreen
|
||||
resource={{
|
||||
ship: props.match.params.ship,
|
||||
name: props.match.params.name
|
||||
}}
|
||||
api={this.api}
|
||||
subscription={this.subscription}
|
||||
node={node}
|
||||
sidebarShown={state.sidebarShown}
|
||||
parentIndex={'/' + props.match.params.nodeId}
|
||||
{...props}
|
||||
/>
|
||||
</Skeleton>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
@ -1,30 +0,0 @@
|
||||
import React, { Component } from 'react';
|
||||
|
||||
|
||||
export default class CodeContent extends Component {
|
||||
|
||||
render() {
|
||||
const { props } = this;
|
||||
const font = !!props.isParent ? "f6" : "f7";
|
||||
|
||||
const content = props.content;
|
||||
|
||||
const outputElement =
|
||||
(Boolean(content.code.output) &&
|
||||
content.code.output.length && content.code.output.length > 0) ?
|
||||
(
|
||||
<pre className={`${font} clamp-attachment pa1 mt0 mb0 b--gray4 b--gray1-d bl br bb`}>
|
||||
{content.code.output[0].join('\n')}
|
||||
</pre>
|
||||
) : null;
|
||||
|
||||
return (
|
||||
<div className="mv2">
|
||||
<pre className={`${font} clamp-attachment pa1 mt0 mb0 bg-light-gray b--gray4 b--gray1-d ba`}>
|
||||
{content.code.expression}
|
||||
</pre>
|
||||
{outputElement}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
@ -1,47 +0,0 @@
|
||||
import React, { Component } from 'react';
|
||||
import ReactMarkdown from 'react-markdown';
|
||||
import RemarkDisableTokenizers from 'remark-disable-tokenizers';
|
||||
|
||||
const DISABLED_BLOCK_TOKENS = [
|
||||
'indentedCode',
|
||||
'blockquote',
|
||||
'atxHeading',
|
||||
'thematicBreak',
|
||||
'list',
|
||||
'setextHeading',
|
||||
'html',
|
||||
'definition',
|
||||
'table'
|
||||
];
|
||||
|
||||
const DISABLED_INLINE_TOKENS = [
|
||||
'autoLink',
|
||||
'url',
|
||||
'email',
|
||||
'link',
|
||||
'reference'
|
||||
];
|
||||
|
||||
const MessageMarkdown = React.memo(
|
||||
props => (<ReactMarkdown
|
||||
{...props}
|
||||
plugins={[[RemarkDisableTokenizers, { block: DISABLED_BLOCK_TOKENS, inline: DISABLED_INLINE_TOKENS }]]}
|
||||
/>));
|
||||
|
||||
|
||||
export default class TextContent extends Component {
|
||||
|
||||
render() {
|
||||
const { props } = this;
|
||||
const font = !!props.isParent ? "f6" : "f7";
|
||||
|
||||
const content = props.content;
|
||||
return (
|
||||
<section>
|
||||
<MessageMarkdown
|
||||
source={content.text}
|
||||
/>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
}
|
@ -1,102 +0,0 @@
|
||||
import React, { Component } from 'react';
|
||||
|
||||
|
||||
const IMAGE_REGEX =
|
||||
/(jpg|img|png|gif|tiff|jpeg|JPG|IMG|PNG|TIFF|GIF|webp|WEBP|webm|WEBM|svg|SVG)$/;
|
||||
|
||||
const YOUTUBE_REGEX =
|
||||
new RegExp(
|
||||
String(/(?:https?:\/\/(?:[a-z]+.)?)/.source) // protocol
|
||||
+ /(?:youtu\.?be(?:\.com)?\/)(?:embed\/)?/.source // short and long-links
|
||||
+ /(?:(?:(?:(?:watch\?)?(?:time_continue=(?:[0-9]+))?.+v=)?([a-zA-Z0-9_-]+))(?:\?t\=(?:[0-9a-zA-Z]+))?)/.source // id
|
||||
);
|
||||
|
||||
export default class UrlContent extends Component {
|
||||
constructor() {
|
||||
super();
|
||||
this.state = {
|
||||
unfold: false,
|
||||
copied: false
|
||||
};
|
||||
this.unfoldEmbed = this.unfoldEmbed.bind(this);
|
||||
}
|
||||
|
||||
unfoldEmbed(id) {
|
||||
let unfoldState = this.state.unfold;
|
||||
unfoldState = !unfoldState;
|
||||
this.setState({ unfold: unfoldState });
|
||||
const iframe = this.refs.iframe;
|
||||
iframe.setAttribute('src', iframe.getAttribute('data-src'));
|
||||
}
|
||||
|
||||
render() {
|
||||
const { props } = this;
|
||||
const content = props.content;
|
||||
const font = !!props.isParent ? "f6" : "f7";
|
||||
const imgMatch = IMAGE_REGEX.exec(props.content.url);
|
||||
const ytMatch = YOUTUBE_REGEX.exec(props.content.url);
|
||||
|
||||
let contents = content.url;
|
||||
if (imgMatch) {
|
||||
contents = (
|
||||
<img
|
||||
className="o-80-d"
|
||||
src={content.url}
|
||||
style={{
|
||||
width: '50%',
|
||||
maxWidth: '250px'
|
||||
}}
|
||||
></img>
|
||||
);
|
||||
return (
|
||||
<a className={`${font} lh-copy v-top word-break-all`}
|
||||
href={content.url}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
{contents}
|
||||
</a>
|
||||
);
|
||||
} else if (ytMatch) {
|
||||
contents = (
|
||||
<div className={'embed-container mb2 w-100 w-75-l w-50-xl ' +
|
||||
((this.state.unfold === true)
|
||||
? 'db' : 'dn')}
|
||||
>
|
||||
<iframe
|
||||
ref="iframe"
|
||||
width="560"
|
||||
height="315"
|
||||
data-src={`https://www.youtube.com/embed/${ytMatch[1]}`}
|
||||
frameBorder="0" allow="picture-in-picture, fullscreen"
|
||||
>
|
||||
</iframe>
|
||||
</div>
|
||||
);
|
||||
return (
|
||||
<div>
|
||||
<a href={content.url}
|
||||
className={`${font} lh-copy v-top bb b--white-d word-break-all`}
|
||||
href={content.url}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer">{content.url}</a>
|
||||
<a className="ml2 f7 pointer lh-copy v-top"
|
||||
onClick={e => this.unfoldEmbed()}>
|
||||
[embed]
|
||||
</a>
|
||||
{contents}
|
||||
</div>
|
||||
);
|
||||
} else {
|
||||
return (
|
||||
<a className={`${font} lh-copy v-top bb b--white-d b--black word-break-all`}
|
||||
href={content.url}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
{contents}
|
||||
</a>
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
@ -1,27 +0,0 @@
|
||||
import React, { Component } from 'react';
|
||||
import { Sigil } from '../../../../lib/sigil';
|
||||
|
||||
export class OverlaySigil extends Component {
|
||||
constructor() {
|
||||
super();
|
||||
}
|
||||
|
||||
render() {
|
||||
const { props, state } = this;
|
||||
|
||||
return (
|
||||
<div
|
||||
className={props.className + ' pointer relative'}
|
||||
style={{ height: '24px' }}
|
||||
>
|
||||
<Sigil
|
||||
ship={props.ship}
|
||||
size={24}
|
||||
color={'000'}
|
||||
classes={props.sigilClass}
|
||||
/>
|
||||
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
@ -1,28 +0,0 @@
|
||||
import React, { Component } from 'react';
|
||||
|
||||
import TextContent from './content/text';
|
||||
import CodeContent from './content/code';
|
||||
import UrlContent from './content/url';
|
||||
|
||||
|
||||
export default class PostContent extends Component {
|
||||
|
||||
render() {
|
||||
const { props } = this;
|
||||
if (props.contents.length <= 0) {
|
||||
return null;
|
||||
}
|
||||
const content = props.contents[0];
|
||||
|
||||
if ('code' in content) {
|
||||
return <CodeContent content={content} isParent={props.isParent} />;
|
||||
} else if ('url' in content) {
|
||||
return <UrlContent content={content} isParent={props.isParent} />;
|
||||
} else if ('text' in content) {
|
||||
return <TextContent content={content} isParent={props.isParent} />;
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -1,109 +0,0 @@
|
||||
import React, { Component } from 'react';
|
||||
import { UnControlled as CodeEditor } from 'react-codemirror2';
|
||||
import CodeMirror from 'codemirror';
|
||||
|
||||
import 'codemirror/mode/markdown/markdown';
|
||||
import 'codemirror/addon/display/placeholder';
|
||||
|
||||
import 'codemirror/lib/codemirror.css';
|
||||
|
||||
const BROWSER_REGEX =
|
||||
new RegExp(String(!/Android|webOS|iPhone|iPad|iPod|BlackBerry/i));
|
||||
|
||||
|
||||
const MARKDOWN_CONFIG = {
|
||||
name: 'markdown',
|
||||
tokenTypeOverrides: {
|
||||
header: 'presentation',
|
||||
quote: 'presentation',
|
||||
list1: 'presentation',
|
||||
list2: 'presentation',
|
||||
list3: 'presentation',
|
||||
hr: 'presentation',
|
||||
image: 'presentation',
|
||||
imageAltText: 'presentation',
|
||||
imageMarker: 'presentation',
|
||||
formatting: 'presentation',
|
||||
linkInline: 'presentation',
|
||||
linkEmail: 'presentation',
|
||||
linkText: 'presentation',
|
||||
linkHref: 'presentation'
|
||||
}
|
||||
};
|
||||
|
||||
export default class PostEditor extends Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.editor = null;
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps) {
|
||||
const { props } = this;
|
||||
if (!props.inCodeMode) {
|
||||
this.editor.setOption('mode', MARKDOWN_CONFIG);
|
||||
this.editor.setOption('placeholder', this.props.placeholder);
|
||||
} else {
|
||||
this.editor.setOption('mode', null);
|
||||
this.editor.setOption('placeholder', 'Code...');
|
||||
}
|
||||
const value = this.editor.getValue();
|
||||
|
||||
// Force redraw of placeholder
|
||||
if(value.length === 0) {
|
||||
this.editor.setValue(' ');
|
||||
this.editor.setValue('');
|
||||
}
|
||||
}
|
||||
|
||||
submit() {
|
||||
if(!this.editor) {
|
||||
return;
|
||||
}
|
||||
|
||||
let editorMessage = this.editor.getValue();
|
||||
if (editorMessage === '') {
|
||||
return;
|
||||
}
|
||||
|
||||
this.props.submit(editorMessage);
|
||||
this.editor.setValue('');
|
||||
}
|
||||
|
||||
render() {
|
||||
const { props } = this;
|
||||
|
||||
const codeTheme = props.inCodeMode ? ' code' : '';
|
||||
|
||||
const options = {
|
||||
mode: MARKDOWN_CONFIG,
|
||||
theme: 'tlon' + codeTheme,
|
||||
lineNumbers: false,
|
||||
lineWrapping: true,
|
||||
scrollbarStyle: 'native',
|
||||
cursorHeight: 0.85,
|
||||
placeholder: props.inCodeMode ? 'Code...' : props.placeholder,
|
||||
extraKeys: {
|
||||
'Enter': () => {
|
||||
this.submit();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
className="chat fr h-100 flex bg-gray0-d lh-copy pl2 w-100 items-center"
|
||||
style={{ flexGrow: 1, maxHeight: '224px', width: 'calc(100% - 72px)' }}
|
||||
>
|
||||
<CodeEditor
|
||||
options={options}
|
||||
editorDidMount={(editor) => {
|
||||
this.editor = editor;
|
||||
if (BROWSER_REGEX.test(navigator.userAgent)) {
|
||||
editor.focus();
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
@ -1,126 +0,0 @@
|
||||
import React, { Component } from 'react';
|
||||
import { Sigil } from '../../../../lib/sigil';
|
||||
import PostEditor from './post-editor';
|
||||
|
||||
|
||||
const URL_REGEX = new RegExp(String(/^((\w+:\/\/)[-a-zA-Z0-9:@;?&=\/%\+\.\*!'\(\),\$_\{\}\^~\[\]`#|]+)/.source));
|
||||
|
||||
export class PostInput extends Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
inCodeMode: false
|
||||
};
|
||||
|
||||
this.submit = this.submit.bind(this);
|
||||
this.toggleCode = this.toggleCode.bind(this);
|
||||
}
|
||||
|
||||
toggleCode() {
|
||||
this.setState({
|
||||
inCodeMode: !this.state.inCodeMode
|
||||
});
|
||||
}
|
||||
|
||||
getLetterType(letter) {
|
||||
if (this.isUrl(letter)) {
|
||||
return {
|
||||
url: letter
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
text: letter
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
isUrl(string) {
|
||||
try {
|
||||
return URL_REGEX.test(string);
|
||||
} catch (e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
submit(text) {
|
||||
const { props, state } = this;
|
||||
if (state.inCodeMode) {
|
||||
let post = props.api.createPost([
|
||||
{
|
||||
code: {
|
||||
expression: text,
|
||||
output: null
|
||||
}
|
||||
}
|
||||
], props.parentIndex);
|
||||
|
||||
this.setState({
|
||||
inCodeMode: false
|
||||
}, () => {
|
||||
props.api.addPost(props.resource.ship, props.resource.name, post);
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
let message = this.getLetterType(text);
|
||||
let post = props.api.createPost([message], props.parentIndex);
|
||||
props.api.addPost(props.resource.ship, props.resource.name, post);
|
||||
|
||||
// perf testing:
|
||||
/*let closure = () => {
|
||||
let x = 0;
|
||||
for (var i = 0; i < 30; i++) {
|
||||
x++;
|
||||
props.api.addPost(
|
||||
props.resource.ship,
|
||||
props.resource.name,
|
||||
props.api.createPost([{
|
||||
text: `${i} - ${Date.now()}`
|
||||
}])
|
||||
);
|
||||
}
|
||||
setTimeout(closure, 1000);
|
||||
};
|
||||
setTimeout(closure, 2000);*/
|
||||
}
|
||||
|
||||
render() {
|
||||
const { props, state } = this;
|
||||
return (
|
||||
<div className="pa3 cf flex black white-d bt b--gray4 b--gray1-d bg-white bg-gray0-d relative"
|
||||
style={{ flexGrow: 1 }}
|
||||
>
|
||||
<div
|
||||
className="fl"
|
||||
style={{
|
||||
marginTop: 6,
|
||||
flexBasis: 24,
|
||||
height: 24
|
||||
}}
|
||||
>
|
||||
<Sigil
|
||||
ship={window.ship}
|
||||
size={24}
|
||||
color={`#000`}
|
||||
/>
|
||||
</div>
|
||||
<PostEditor
|
||||
inCodeMode={state.inCodeMode}
|
||||
submit={this.submit}
|
||||
placeholder='Post...' />
|
||||
<div className="ml2 mr2"
|
||||
style={{ height: '16px', width: '16px', flexBasis: 16, marginTop: 10 }}>
|
||||
</div>
|
||||
<div style={{ height: '16px', width: '16px', flexBasis: 16, marginTop: 10 }}>
|
||||
<img
|
||||
style={{ filter: state.inCodeMode && 'invert(100%)', height: '100%', width: '100%' }}
|
||||
onClick={this.toggleCode}
|
||||
src="/~chat/img/CodeEval.png"
|
||||
className="contrast-10-d bg-white bg-none-d ba b--gray1-d br1"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
@ -1,93 +0,0 @@
|
||||
import React, { Component } from 'react';
|
||||
import { Post } from './post';
|
||||
|
||||
|
||||
const PAGE_SIZE = 50;
|
||||
|
||||
export class PostList extends Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.scrollRef = React.createRef();
|
||||
|
||||
this.state = {
|
||||
page: 0
|
||||
};
|
||||
}
|
||||
|
||||
previousPage() {
|
||||
const { state } = this;
|
||||
this.setState({
|
||||
page: state.page - 1
|
||||
}, () => {
|
||||
this.scrollRef.current.scrollTo(0, this.scrollRef.current.scrollHeight);
|
||||
});
|
||||
}
|
||||
|
||||
nextPage() {
|
||||
const { state } = this;
|
||||
this.setState({
|
||||
page: state.page + 1
|
||||
}, () => {
|
||||
this.scrollRef.current.scrollTo(0, 0);
|
||||
});
|
||||
}
|
||||
|
||||
paginatedNodes() {
|
||||
const { props, state } = this;
|
||||
let nodes = Array.from(props.graph).sort((a,b) => {
|
||||
return b[0] - a[0];
|
||||
});
|
||||
|
||||
return {
|
||||
nodes: nodes.slice(
|
||||
PAGE_SIZE * state.page,
|
||||
PAGE_SIZE * (state.page + 1)
|
||||
),
|
||||
length: nodes.length
|
||||
};
|
||||
}
|
||||
|
||||
render() {
|
||||
const { props, state } = this;
|
||||
let { nodes, length } = this.paginatedNodes();
|
||||
|
||||
return (
|
||||
<div className={
|
||||
'overflow-y-scroll bg-white bg-gray0-d pt3 pb2' +
|
||||
'flex flex-column relative'
|
||||
}
|
||||
style={{ height: '100%', resize: 'vertical' }}
|
||||
ref={this.scrollRef}>
|
||||
{ (state.page !== 0) ?
|
||||
(
|
||||
<button onClick={this.previousPage.bind(this)}>
|
||||
Load New
|
||||
</button>
|
||||
) : null
|
||||
}
|
||||
{ nodes.map((node, i) => {
|
||||
let post = node[1].post;
|
||||
|
||||
return (
|
||||
<Post
|
||||
key={post.index}
|
||||
msg={post}
|
||||
resource={props.resource}
|
||||
index={post.index}
|
||||
history={props.history}
|
||||
/>
|
||||
);
|
||||
})
|
||||
}
|
||||
{ (PAGE_SIZE * (state.page + 1) < length) ?
|
||||
(
|
||||
<button onClick={this.nextPage.bind(this)}>
|
||||
Load Old
|
||||
</button>
|
||||
) : null
|
||||
}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
}
|
@ -1,65 +0,0 @@
|
||||
import React, { Component } from 'react';
|
||||
import { OverlaySigil } from './overlay-sigil';
|
||||
import PostContent from './post-content';
|
||||
import { uxToHex, cite, writeText } from '../../../../lib/util';
|
||||
import moment from 'moment';
|
||||
|
||||
|
||||
export class Post extends Component {
|
||||
render() {
|
||||
const { props, state } = this;
|
||||
const datestamp = '~' + moment.unix(props.msg['time-sent'] / 1000).format('YYYY.M.D');
|
||||
|
||||
const paddingTop = { 'paddingTop': '6px' };
|
||||
|
||||
const timestamp = moment.unix(props.msg['time-sent'] / 1000).format('hh:mm a');
|
||||
|
||||
let name = `~${props.msg.author}`;
|
||||
let color = '#000000';
|
||||
let sigilClass = 'mix-blend-diff';
|
||||
|
||||
const bodyFont = !!props.isParent ? "f6" : "f8";
|
||||
const smallFont = !!props.isParent ? "f7" : "f9";
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={this.containerRef}
|
||||
className={
|
||||
`w-100 ${bodyFont} pl3 pt4 pr3 cf flex lh-copy bt b--white pointer`
|
||||
}
|
||||
style={{
|
||||
minHeight: 'min-content'
|
||||
}}
|
||||
onClick={() => {
|
||||
props.history.push(`/~post/room/` +
|
||||
`${props.resource.ship}/${props.resource.name}${props.index}`);
|
||||
}}
|
||||
>
|
||||
<OverlaySigil
|
||||
ship={props.msg.author}
|
||||
color={'#000'}
|
||||
sigilClass={sigilClass}
|
||||
group={props.group}
|
||||
className="fl pr3 v-top bg-white bg-gray0-d"
|
||||
/>
|
||||
<div
|
||||
className="fr clamp-message white-d"
|
||||
style={{ flexGrow: 1, marginTop: -8 }}
|
||||
>
|
||||
<div className="hide-child" style={paddingTop}>
|
||||
<p className={`v-mid ${smallFont} gray2 dib mr3 c-default`}>
|
||||
<span>{`~${props.msg.author}`}</span>
|
||||
</p>
|
||||
<p className={`v-mid mono ${smallFont} gray2 dib`}>{timestamp}</p>
|
||||
<p className={`v-mid mono ${smallFont} ml2 gray2 dib child dn-s`}>
|
||||
{datestamp}
|
||||
</p>
|
||||
</div>
|
||||
<PostContent
|
||||
contents={props.msg.contents}
|
||||
isParent={props.isParent} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
@ -1,93 +0,0 @@
|
||||
import React, { Component } from 'react';
|
||||
import { Spinner } from '../../../components/Spinner';
|
||||
import { Link } from 'react-router-dom';
|
||||
import { deSig } from '../../../lib/util';
|
||||
import urbitOb from 'urbit-ob';
|
||||
|
||||
|
||||
export class NewScreen extends Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
title: '',
|
||||
idName: '',
|
||||
awaiting: false
|
||||
};
|
||||
|
||||
this.titleChange = this.titleChange.bind(this);
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps, prevState) {
|
||||
const { props, state } = this;
|
||||
|
||||
if (prevProps !== props) {
|
||||
const resource = `${window.ship}/${state.idName}`;
|
||||
if (!!props.keys && props.keys.has(resource)) {
|
||||
props.history.push('/~post/room/' + resource);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
titleChange(event) {
|
||||
const asciiSafe = event.target.value.toLowerCase()
|
||||
.replace(/[^a-z0-9~_.-]/g, '-');
|
||||
this.setState({
|
||||
idName: asciiSafe,
|
||||
title: event.target.value
|
||||
});
|
||||
}
|
||||
|
||||
onClickCreate() {
|
||||
const { props, state } = this;
|
||||
|
||||
this.setState({
|
||||
awaiting: true
|
||||
}, () => {
|
||||
props.api.addGraph(window.ship, state.idName, {});
|
||||
});
|
||||
}
|
||||
|
||||
render() {
|
||||
const { props, state } = this;
|
||||
const createClasses = state.idName
|
||||
? 'pointer db f9 green2 bg-gray0-d ba pv3 ph4 b--green2'
|
||||
: 'pointer db f9 gray2 ba bg-gray0-d pa2 pv3 ph4 b--gray3';
|
||||
|
||||
const idClasses =
|
||||
'f7 ba b--gray3 b--gray2-d bg-gray0-d white-d pa3 db w-100 ' +
|
||||
'focus-b--black focus-b--white-d ';
|
||||
|
||||
return (
|
||||
<div
|
||||
className={
|
||||
'h-100 w-100 mw6 pa3 pt4 overflow-x-hidden ' +
|
||||
'bg-gray0-d white-d flex flex-column'
|
||||
}
|
||||
>
|
||||
<div className="w-100 dn-m dn-l dn-xl inter pt1 pb6 f8">
|
||||
<Link to="/~chat/">{'⟵ All Chats'}</Link>
|
||||
</div>
|
||||
<h2 className="mb3 f8">New Chat</h2>
|
||||
<div className="w-100">
|
||||
<p className="f8 mt3 lh-copy db">Name</p>
|
||||
<textarea
|
||||
className={idClasses}
|
||||
placeholder="Secret Chat"
|
||||
rows={1}
|
||||
style={{
|
||||
resize: 'none'
|
||||
}}
|
||||
onChange={this.titleChange}
|
||||
/>
|
||||
<button
|
||||
onClick={this.onClickCreate.bind(this)}
|
||||
className={createClasses}
|
||||
>
|
||||
Start Chat
|
||||
</button>
|
||||
<Spinner awaiting={this.state.awaiting} classes="mt4" text="Creating chat..." />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
@ -1,111 +0,0 @@
|
||||
import React, { Component } from 'react';
|
||||
import _ from 'lodash';
|
||||
import moment from 'moment';
|
||||
|
||||
import { Link } from 'react-router-dom';
|
||||
|
||||
import { PostList } from './lib/post-list';
|
||||
import { Post } from './lib/post';
|
||||
import { PostInput } from './lib/post-input';
|
||||
import { deSig } from '../../../lib/util';
|
||||
|
||||
export class NodeTreeScreen extends Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.state = {};
|
||||
|
||||
moment.updateLocale('en', {
|
||||
calendar: {
|
||||
sameDay: '[Today]',
|
||||
nextDay: '[Tomorrow]',
|
||||
nextWeek: 'dddd',
|
||||
lastDay: '[Yesterday]',
|
||||
lastWeek: '[Last] dddd',
|
||||
sameElse: 'DD/MM/YYYY'
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
parentPost() {
|
||||
const { props } = this;
|
||||
const node = props.node;
|
||||
|
||||
let prevIndex = node.post.index.split('/');
|
||||
prevIndex.pop();
|
||||
prevIndex = prevIndex.join('/');
|
||||
|
||||
return (
|
||||
<div>
|
||||
<span className="dib f9 v-mid gray2 ml1 mr1 c-default inter">
|
||||
<Link className="dib f9 v-mid inter ml2 no-underline white-d"
|
||||
to={
|
||||
"/~post/room/" +
|
||||
`${props.resource.ship}/${props.resource.name}` +
|
||||
`${prevIndex}`
|
||||
}>
|
||||
⟵
|
||||
</Link>
|
||||
</span>
|
||||
<Post
|
||||
isParent={true}
|
||||
key={node.post.index}
|
||||
msg={node.post}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
replyModal() {
|
||||
const { props, state } = this;
|
||||
|
||||
return (
|
||||
<div>
|
||||
<PostInput
|
||||
api={props.api}
|
||||
resource={props.resource}
|
||||
owner={deSig(props.match.params.ship)}
|
||||
placeholder="Message..."
|
||||
parentIndex={props.parentIndex}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
render() {
|
||||
const { props } = this;
|
||||
|
||||
return (
|
||||
<div key={props.resource.name}
|
||||
className="h-100 w-100 overflow-hidden flex flex-column relative">
|
||||
<div className="w-100 dn-m dn-l dn-xl inter pt4 pb6 pl3 f8"
|
||||
style={{ height: '1rem' }}>
|
||||
<Link to="/~post/">{'⟵ All Graphs'}</Link>
|
||||
</div>
|
||||
<div
|
||||
className={
|
||||
'pl4 pt2 bb b--gray4 b--gray1-d bg-gray0-d' +
|
||||
'flex relative flex-shrink-0' +
|
||||
'overflow-x-scroll overflow-x-auto-l overflow-x-auto-xl'
|
||||
}
|
||||
style={{ height: 48 }}>
|
||||
<Link to={`/~post/room/${props.resource.ship}/${props.resource.name}`}
|
||||
className="pt2 white-d">
|
||||
<h2 className={'dib f9 fw4 lh-solid v-top '}
|
||||
style={{ width: 'max-content' }}>
|
||||
{props.resource.name}
|
||||
</h2>
|
||||
</Link>
|
||||
</div>
|
||||
{this.parentPost()}
|
||||
{this.replyModal()}
|
||||
<PostList
|
||||
api={props.api}
|
||||
graph={props.node.children}
|
||||
history={props.history}
|
||||
resource={props.resource}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
@ -1,52 +0,0 @@
|
||||
import React, { Component } from 'react';
|
||||
|
||||
import { Link } from 'react-router-dom';
|
||||
|
||||
import { Post } from './lib/post';
|
||||
import { PostInput } from './lib/post-input';
|
||||
import { PostList } from './lib/post-list';
|
||||
import { deSig } from '../../../lib/util';
|
||||
|
||||
|
||||
export class PostScreen extends Component {
|
||||
render() {
|
||||
const { props } = this;
|
||||
|
||||
return (
|
||||
<div key={props.resource.name}
|
||||
className="h-100 w-100 overflow-hidden flex flex-column relative">
|
||||
<div className="w-100 dn-m dn-l dn-xl inter pt4 pb6 pl3 f8"
|
||||
style={{ height: '1rem' }}>
|
||||
<Link to="/~post/">{'⟵ All Graphs'}</Link>
|
||||
</div>
|
||||
<div
|
||||
className={
|
||||
'pl4 pt2 bb b--gray4 b--gray1-d bg-gray0-d' +
|
||||
'flex relative flex-shrink-0' +
|
||||
'overflow-x-scroll overflow-x-auto-l overflow-x-auto-xl'
|
||||
}
|
||||
style={{ height: 48 }}>
|
||||
<Link to={`/~post/room/${props.resource.ship}/${props.resource.name}`}
|
||||
className="pt2 white-d">
|
||||
<h2 className={'dib f9 fw4 lh-solid v-top '}
|
||||
style={{ width: 'max-content' }}>
|
||||
{props.resource.name}
|
||||
</h2>
|
||||
</Link>
|
||||
</div>
|
||||
<PostInput
|
||||
api={props.api}
|
||||
resource={props.resource}
|
||||
owner={deSig(props.match.params.ship)}
|
||||
placeholder="Post..."
|
||||
/>
|
||||
<PostList
|
||||
api={props.api}
|
||||
graph={props.graph}
|
||||
history={props.history}
|
||||
resource={props.resource}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
@ -1,55 +0,0 @@
|
||||
import React, { Component } from 'react';
|
||||
import { Link } from 'react-router-dom';
|
||||
import _ from 'lodash';
|
||||
|
||||
|
||||
export class Sidebar extends Component {
|
||||
constructor() {
|
||||
super();
|
||||
}
|
||||
|
||||
onClickNew() {
|
||||
this.props.history.push('/~post/new');
|
||||
}
|
||||
|
||||
renderKeys(keys) {
|
||||
const rooms = Array.from(keys).map((key) => {
|
||||
return (
|
||||
<div key={key}>
|
||||
<Link
|
||||
className={'no-underline '}
|
||||
to={'/~post/room/' + key}>
|
||||
{key}
|
||||
</Link>
|
||||
</div>
|
||||
);
|
||||
});
|
||||
return (
|
||||
<div>
|
||||
{rooms}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
render() {
|
||||
const { props, state } = this;
|
||||
return (
|
||||
<div
|
||||
className={`h-100-minus-96-s h-100 w-100 overflow-x-hidden flex
|
||||
bg-gray0-d flex-column relative z1`}
|
||||
>
|
||||
<div className="w-100 bg-transparent pa4">
|
||||
<a
|
||||
className="dib f9 pointer green2 gray4-d mr4"
|
||||
onClick={this.onClickNew.bind(this)}
|
||||
>
|
||||
New Graph
|
||||
</a>
|
||||
</div>
|
||||
<div className="overflow-y-auto h-100">
|
||||
{this.renderKeys(props.keys)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
@ -1,73 +0,0 @@
|
||||
import React, { Component } from 'react';
|
||||
import classnames from 'classnames';
|
||||
import { Link } from 'react-router-dom';
|
||||
|
||||
export class Skeleton extends Component {
|
||||
render() {
|
||||
// sidebar and chat panel conditional classes
|
||||
const sidebarHide = (!this.props.sidebarShown || this.props.popout)
|
||||
? 'dn' : '';
|
||||
|
||||
const sidebarHideOnMobile = this.props.sidebarHideOnMobile
|
||||
? 'dn-s' : '';
|
||||
|
||||
const chatHideOnMobile = this.props.chatHideonMobile
|
||||
? 'dn-s' : '';
|
||||
|
||||
// mobile-specific navigation classes
|
||||
const mobileNavClasses = classnames({
|
||||
'dn': this.props.chatHideOnMobile,
|
||||
'db dn-m dn-l dn-xl': !this.props.chatHideOnMobile,
|
||||
'w-100 inter pt4 f8': !this.props.chatHideOnMobile
|
||||
});
|
||||
|
||||
// popout switches out window chrome and borders
|
||||
const popoutWindow = this.props.popout
|
||||
? '' : 'ph4-m ph4-l ph4-xl pb4-m pb4-l pb4-xl';
|
||||
|
||||
const popoutBorder = this.props.popout
|
||||
? '' : 'ba-m ba-l ba-xl b--gray4 b--gray1-d br1 ';
|
||||
|
||||
return (
|
||||
// app outer skeleton
|
||||
<div style={{
|
||||
height: 'calc(100vh - 45px)'
|
||||
}} className={'h-100 w-100 bg-gray0-d ' + popoutWindow}
|
||||
>
|
||||
{/* app window borders */}
|
||||
<div className={ 'cf w-100 flex h-100 ' + popoutBorder }>
|
||||
{/* sidebar skeleton, hidden on mobile when in chat panel */}
|
||||
<div
|
||||
className={
|
||||
`fl h-100 br b--gray4 b--gray1-d overflow-x-hidden
|
||||
flex-basis-full-s flex-basis-250-m flex-basis-250-l
|
||||
flex-basis-250-xl ` +
|
||||
sidebarHide +
|
||||
' ' +
|
||||
sidebarHideOnMobile
|
||||
}
|
||||
>
|
||||
{/* mobile-specific navigation */}
|
||||
<div className={mobileNavClasses}>
|
||||
<div className="bb b--gray4 b--gray1-d white-d inter f8 pl3 pt6 pb3">
|
||||
All Chats
|
||||
</div>
|
||||
</div>
|
||||
{/* sidebar component inside the sidebar skeleton */}
|
||||
{this.props.sidebar}
|
||||
</div>
|
||||
{/* right-hand panel for chat, members, settings */}
|
||||
<div
|
||||
className={'h-100 fr ' + chatHideOnMobile}
|
||||
style={{
|
||||
flexGrow: 1,
|
||||
width: 'calc(100% - 300px)'
|
||||
}}
|
||||
>
|
||||
{this.props.children}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
@ -1,442 +0,0 @@
|
||||
* {
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-webkit-touch-callout: none;
|
||||
}
|
||||
|
||||
html, body {
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
p, h1, h2, h3, h4, h5, h6, a, input, textarea, button {
|
||||
margin-block-end: unset;
|
||||
margin-block-start: unset;
|
||||
-webkit-margin-before: unset;
|
||||
-webkit-margin-after: unset;
|
||||
font-family: Inter, sans-serif;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
textarea, input, button {
|
||||
outline: none;
|
||||
-webkit-appearance: none;
|
||||
border: none;
|
||||
background-color: #fff;
|
||||
}
|
||||
|
||||
a {
|
||||
color: #000;
|
||||
font-weight: 400;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
h2 {
|
||||
font-weight: 400;
|
||||
}
|
||||
|
||||
.inter {
|
||||
font-family: Inter, sans-serif;
|
||||
}
|
||||
|
||||
.clamp-3 {
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
-webkit-line-clamp: 2;
|
||||
-webkit-box-orient: vertical;
|
||||
}
|
||||
|
||||
.clamp-message {
|
||||
max-width: calc(100% - 36px - 1.5rem);
|
||||
}
|
||||
|
||||
.clamp-attachment {
|
||||
overflow: auto;
|
||||
max-height: 10em;
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
.lh-16 {
|
||||
line-height: 16px;
|
||||
}
|
||||
|
||||
.mono {
|
||||
font-family: "Source Code Pro", monospace;
|
||||
}
|
||||
|
||||
.bg-welcome-green {
|
||||
background-color: #ECF6F2;
|
||||
}
|
||||
|
||||
.list-ship {
|
||||
line-height: 2.2;
|
||||
}
|
||||
|
||||
.c-default {
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
.word-break-all {
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
.focus-b--black:focus {
|
||||
border-color: #000;
|
||||
}
|
||||
|
||||
.mix-blend-diff {
|
||||
mix-blend-mode: difference;
|
||||
}
|
||||
|
||||
.placeholder-inter::placeholder {
|
||||
font-family: "Inter", sans-serif;
|
||||
}
|
||||
|
||||
/* spinner */
|
||||
|
||||
.spin-active {
|
||||
animation: spin 2s infinite;
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
0% {transform: rotate(0deg);}
|
||||
25% {transform: rotate(90deg);}
|
||||
50% {transform: rotate(180deg);}
|
||||
75% {transform: rotate(270deg);}
|
||||
100% {transform: rotate(360deg);}
|
||||
}
|
||||
|
||||
/* embeds */
|
||||
.embed-container {
|
||||
position: relative;
|
||||
height: 0;
|
||||
overflow: hidden;
|
||||
padding-bottom: 28.125%;
|
||||
}
|
||||
|
||||
.embed-container iframe, .embed-container object, .embed-container embed {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.mh-16 {
|
||||
max-height: 16rem;
|
||||
}
|
||||
|
||||
/* toggler checkbox */
|
||||
.toggle::after {
|
||||
content: "";
|
||||
height: 12px;
|
||||
width: 12px;
|
||||
background: white;
|
||||
position: absolute;
|
||||
top: 2px;
|
||||
left: 2px;
|
||||
border-radius: 100%;
|
||||
}
|
||||
|
||||
.toggle.checked::after {
|
||||
content: "";
|
||||
height: 12px;
|
||||
width: 12px;
|
||||
background: white;
|
||||
position: absolute;
|
||||
top: 2px;
|
||||
left: 14px;
|
||||
border-radius: 100%;
|
||||
}
|
||||
|
||||
.shadow-6 {
|
||||
box-shadow: 2px 4px 20px rgba(0, 0, 0, 0.25);
|
||||
}
|
||||
|
||||
.brt2 {
|
||||
border-radius: 0.25rem 0.25rem 0 0;
|
||||
}
|
||||
|
||||
|
||||
.green3 {
|
||||
color: #7ea899;
|
||||
}
|
||||
|
||||
.unread-notice {
|
||||
top: 48px;
|
||||
}
|
||||
|
||||
/* responsive */
|
||||
|
||||
@media all and (max-width: 34.375em) {
|
||||
.dn-s {
|
||||
display: none;
|
||||
}
|
||||
.flex-basis-full-s {
|
||||
flex-basis: 100%;
|
||||
}
|
||||
.h-100-minus-96-s {
|
||||
height: calc(100% - 96px);
|
||||
}
|
||||
.embed-container {
|
||||
padding-bottom: 56.25%;
|
||||
}
|
||||
.unread-notice {
|
||||
top: 96px;
|
||||
}
|
||||
}
|
||||
|
||||
@media all and (min-width: 34.375em) and (max-width: 46.875em) {
|
||||
.flex-basis-250-m {
|
||||
flex-basis: 250px;
|
||||
}
|
||||
.embed-container {
|
||||
padding-bottom: 56.25%;
|
||||
}
|
||||
}
|
||||
|
||||
@media all and (min-width: 46.875em) and (max-width: 60em) {
|
||||
.flex-basis-250-l {
|
||||
flex-basis: 250px;
|
||||
}
|
||||
.embed-container {
|
||||
padding-bottom: 37.5%;
|
||||
}
|
||||
}
|
||||
|
||||
@media all and (min-width: 60em) {
|
||||
.flex-basis-250-xl {
|
||||
flex-basis: 250px;
|
||||
}
|
||||
}
|
||||
|
||||
blockquote {
|
||||
padding-left: 24px;
|
||||
padding-top: 8px;
|
||||
padding-bottom: 8px;
|
||||
margin-left: 0;
|
||||
margin-right: 0;
|
||||
margin-top: 8px;
|
||||
margin-bottom: 8px;
|
||||
border-left: 1px solid black;
|
||||
}
|
||||
|
||||
|
||||
:root {
|
||||
--dark-gray: #555555;
|
||||
--gray: #7F7F7F;
|
||||
--medium-gray: #CCCCCC;
|
||||
--light-gray: rgba(0,0,0,0.08);
|
||||
}
|
||||
.chat .react-codemirror2 {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.chat .CodeMirror {
|
||||
height: 100% !important;
|
||||
width: 100% !important;
|
||||
cursor: text;
|
||||
}
|
||||
|
||||
.chat .CodeMirror * {
|
||||
font-family: 'Inter';
|
||||
}
|
||||
|
||||
.chat .CodeMirror.cm-s-code.chat .cm-s-tlon * {
|
||||
font-family: 'Source Code Pro';
|
||||
|
||||
}
|
||||
|
||||
.chat .CodeMirror-selected { background:#BAE3FE !important; color: black; }
|
||||
pre.CodeMirror-placeholder.CodeMirror-line-like { color: var(--gray); }
|
||||
|
||||
.chat .cm-s-tlon span { font-family: "Inter"}
|
||||
.chat .cm-s-tlon span.cm-meta { color: var(--gray); }
|
||||
.chat .cm-s-tlon span.cm-number { color: var(--gray); }
|
||||
.chat .cm-s-tlon span.cm-keyword { line-height: 1em; font-weight: bold; color: var(--gray); }
|
||||
.chat .cm-s-tlon span.cm-atom { font-weight: bold; color: var(--gray); }
|
||||
.chat .cm-s-tlon span.cm-def { color: black; }
|
||||
.chat .cm-s-tlon span.cm-variable { color: black; }
|
||||
.chat .cm-s-tlon span.cm-variable-2 { color: black; }
|
||||
.chat .cm-s-tlon span.cm-variable-3, .chat .cm-s-tlon span.cm-type { color: black; }
|
||||
.chat .cm-s-tlon span.cm-property { color: black; }
|
||||
.chat .cm-s-tlon span.cm-operator { color: black; }
|
||||
.chat .cm-s-tlon span.cm-comment { font-family: 'Source Code Pro'; color: black; background-color: var(--light-gray); display: inline-block; border-radius: 2px;}
|
||||
.chat .cm-s-tlon span.cm-string { color: var(--dark-gray); }
|
||||
.chat .cm-s-tlon span.cm-string-2 { color: var(--gray); }
|
||||
.chat .cm-s-tlon span.cm-qualifier { color: #555; }
|
||||
.chat .cm-s-tlon span.cm-error { color: #FF0000; }
|
||||
.chat .cm-s-tlon span.cm-attribute { color: var(--gray); }
|
||||
.chat .cm-s-tlon span.cm-tag { color: var(--gray); }
|
||||
.chat .cm-s-tlon span.cm-link { color: var(--dark-gray); text-decoration: none;}
|
||||
.chat .cm-s-tlon .CodeMirror-activeline-background { background: var(--gray); }
|
||||
.chat .cm-s-tlon .CodeMirror-cursor {
|
||||
border-left: 2px solid #3687FF;
|
||||
}
|
||||
|
||||
.chat .cm-s-tlon span.cm-builtin { color: var(--gray); }
|
||||
.chat .cm-s-tlon span.cm-bracket { color: var(--gray); }
|
||||
/* .chat .cm-s-tlon { font-family: Consolas, Menlo, Monaco, Lucida Console, Liberation Mono, DejaVu Sans Mono, Bitstream Vera Sans Mono, Courier New, monospace, serif;} */
|
||||
|
||||
|
||||
.chat .cm-s-tlon .CodeMirror-matchingbracket { outline:1px solid grey; color:black !important; }
|
||||
|
||||
.chat .CodeMirror-hints.tlon {
|
||||
/* font-family: Menlo, Monaco, Consolas, 'Courier New', monospace; */
|
||||
color: #616569;
|
||||
background-color: #ebf3fd !important;
|
||||
}
|
||||
|
||||
.chat .CodeMirror-hints.tlon .CodeMirror-hint-active {
|
||||
background-color: #a2b8c9 !important;
|
||||
color: #5c6065 !important;
|
||||
}
|
||||
|
||||
.title-input[placeholder]:empty:before {
|
||||
content: attr(placeholder);
|
||||
color: #7F7F7F;
|
||||
}
|
||||
|
||||
/* dark */
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
body {
|
||||
background-color: #333;
|
||||
}
|
||||
.bg-black-d {
|
||||
background-color: black;
|
||||
}
|
||||
.white-d {
|
||||
color: white;
|
||||
}
|
||||
.gray1-d {
|
||||
color: #4d4d4d;
|
||||
}
|
||||
.gray2-d {
|
||||
color: #7f7f7f;
|
||||
}
|
||||
.gray3-d {
|
||||
color: #b1b2b3;
|
||||
}
|
||||
.gray4-d {
|
||||
color: #e6e6e6;
|
||||
}
|
||||
.bg-gray0-d {
|
||||
background-color: #333;
|
||||
}
|
||||
.bg-gray1-d {
|
||||
background-color: #4d4d4d;
|
||||
}
|
||||
.b--gray0-d {
|
||||
border-color: #333;
|
||||
}
|
||||
.b--gray1-d {
|
||||
border-color: #4d4d4d;
|
||||
}
|
||||
.b--gray2-d {
|
||||
border-color: #7f7f7f;
|
||||
}
|
||||
.b--white-d {
|
||||
border-color: #fff;
|
||||
}
|
||||
.b--green2-d {
|
||||
border-color: #2aa779;
|
||||
}
|
||||
.bb-d {
|
||||
border-bottom-width: 1px;
|
||||
border-bottom-style: solid;
|
||||
}
|
||||
.invert-d {
|
||||
filter: invert(1);
|
||||
}
|
||||
.o-80-d {
|
||||
opacity: .8;
|
||||
}
|
||||
.focus-b--white-d:focus {
|
||||
border-color: #fff;
|
||||
}
|
||||
a {
|
||||
color: #fff;
|
||||
}
|
||||
.hover-bg-gray1-d:hover {
|
||||
background-color: #4d4d4d;
|
||||
}
|
||||
blockquote {
|
||||
border-left: 1px solid white;
|
||||
}
|
||||
|
||||
.contrast-10-d {
|
||||
filter: contrast(0.1);
|
||||
}
|
||||
|
||||
.bg-none-d {
|
||||
background: none;
|
||||
}
|
||||
|
||||
|
||||
/* codemirror */
|
||||
.chat .cm-s-tlon.CodeMirror {
|
||||
background: #333;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.chat .cm-s-tlon span.cm-def {
|
||||
color: white;
|
||||
}
|
||||
|
||||
.chat .cm-s-tlon span.cm-variable {
|
||||
color: white;
|
||||
}
|
||||
|
||||
.chat .cm-s-tlon span.cm-variable-2 {
|
||||
color: white;
|
||||
}
|
||||
|
||||
.chat .cm-s-tlon span.cm-variable-3,
|
||||
.chat .cm-s-tlon span.cm-type {
|
||||
color: white;
|
||||
}
|
||||
|
||||
.chat .cm-s-tlon span.cm-property {
|
||||
color: white;
|
||||
}
|
||||
|
||||
.chat .cm-s-tlon span.cm-operator {
|
||||
color: white;
|
||||
}
|
||||
|
||||
|
||||
.chat .cm-s-tlon span.cm-string {
|
||||
color: var(--gray);
|
||||
}
|
||||
|
||||
.chat .cm-s-tlon span.cm-string-2 {
|
||||
color: var(--gray);
|
||||
}
|
||||
|
||||
.chat .cm-s-tlon span.cm-attribute {
|
||||
color: var(--gray);
|
||||
}
|
||||
|
||||
.chat .cm-s-tlon span.cm-tag {
|
||||
color: var(--gray);
|
||||
}
|
||||
|
||||
.chat .cm-s-tlon span.cm-link {
|
||||
color: var(--gray);
|
||||
}
|
||||
|
||||
/* set rules w/ both color & bg-color last to preserve legibility */
|
||||
.chat .CodeMirror-selected {
|
||||
background: var(--medium-gray) !important;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.chat .cm-s-tlon span.cm-comment {
|
||||
color: black;
|
||||
display: inline-block;
|
||||
padding: 0;
|
||||
background-color: rgba(255,255,255, 0.3);
|
||||
border-radius: 2px;
|
||||
}
|
||||
}
|
@ -1,142 +0,0 @@
|
||||
import _ from 'lodash';
|
||||
|
||||
|
||||
const OrderedMap = (arr = []) => {
|
||||
let map = new Map(arr);
|
||||
// map[Symbol.iterator] = function* () {
|
||||
// yield* [...this.entries()].sort((a, b) => a[1] - b[1]);
|
||||
// };
|
||||
return map;
|
||||
};
|
||||
|
||||
export default class GraphReducer {
|
||||
reduce(json, state) {
|
||||
const data = _.get(json, 'graph-update', false);
|
||||
if (data) {
|
||||
this.keys(data, state);
|
||||
this.addGraph(data, state);
|
||||
this.addNodes(data, state);
|
||||
}
|
||||
}
|
||||
|
||||
keys(json, state) {
|
||||
const data = _.get(json, 'keys', false);
|
||||
if (data) {
|
||||
state.keys = new Set(data.map((res) => {
|
||||
return res.ship + '/' + res.name;
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
addGraph(json, state) {
|
||||
const data = _.get(json, 'add-graph', false);
|
||||
if (data) {
|
||||
if (!('graphs' in state)) {
|
||||
state.graphs = {};
|
||||
}
|
||||
|
||||
let resource = data.resource.ship + '/' + data.resource.name;
|
||||
if (!(resource in state.graphs)) {
|
||||
state.graphs[resource] = new OrderedMap();
|
||||
}
|
||||
|
||||
for (let i in data.graph) {
|
||||
let item = data.graph[i];
|
||||
let index = item[0].split('/').slice(1).map((ind) => {
|
||||
return parseInt(ind, 10);
|
||||
});
|
||||
|
||||
if (index.length === 0) { break; }
|
||||
|
||||
let node = this._processNode(item[1]);
|
||||
state.graphs[resource].set(index[index.length - 1], node);
|
||||
}
|
||||
state.keys.add(resource);
|
||||
}
|
||||
}
|
||||
|
||||
_processNode(node) {
|
||||
// is empty
|
||||
if (!node.children) {
|
||||
node.children = new OrderedMap();
|
||||
return node;
|
||||
}
|
||||
|
||||
// is graph
|
||||
let converted = new OrderedMap();
|
||||
for (let i in node.children) {
|
||||
let item = node.children[i];
|
||||
let index = item[0].split('/').slice(1).map((ind) => {
|
||||
return parseInt(ind, 10);
|
||||
});
|
||||
|
||||
if (index.length === 0) { break; }
|
||||
|
||||
converted.set(
|
||||
index[index.length - 1],
|
||||
this._processNode(item[1])
|
||||
);
|
||||
}
|
||||
node.children = converted;
|
||||
return node;
|
||||
}
|
||||
|
||||
removeGraph(json, state) {
|
||||
const data = _.get(json, 'remove-graph', false);
|
||||
if (data) {
|
||||
if (!('graphs' in state)) {
|
||||
state.graphs = {};
|
||||
}
|
||||
let resource = data.resource.ship + '/' + data.resource.name;
|
||||
delete state.graphs[resource];
|
||||
}
|
||||
}
|
||||
|
||||
addNodes(json, state) {
|
||||
const data = _.get(json, 'add-nodes', false);
|
||||
if (data) {
|
||||
if (!('graphs' in state)) { return; }
|
||||
|
||||
let resource = data.resource.ship + '/' + data.resource.name;
|
||||
if (!(resource in state.graphs)) { return; }
|
||||
|
||||
for (let i in data.nodes) {
|
||||
let item = data.nodes[i];
|
||||
if (item[0].split('/').length === 0) { return; }
|
||||
|
||||
let index = item[0].split('/').slice(1).map((ind) => {
|
||||
return parseInt(ind, 10);
|
||||
});
|
||||
|
||||
if (index.length === 0) { return; }
|
||||
|
||||
// TODO: support adding nodes with children
|
||||
item[1].children = new Map();
|
||||
|
||||
state.graphs[resource] = this._addNode(
|
||||
state.graphs[resource],
|
||||
index,
|
||||
item[1]
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
_addNode(graph, index, node) {
|
||||
// set child of graph
|
||||
if (index.length === 1) {
|
||||
graph.set(index[0], node);
|
||||
return graph;
|
||||
}
|
||||
|
||||
// set parent of graph
|
||||
let parNode = graph.get(index[0]);
|
||||
if (!parNode) {
|
||||
console.error('parent node does not exist, cannot add child');
|
||||
return;
|
||||
}
|
||||
parNode.children = this._addNode(parNode.children, index.slice(1), node);
|
||||
graph.set(index[0], parNode);
|
||||
return graph;
|
||||
}
|
||||
}
|
@ -1,29 +0,0 @@
|
||||
import GraphReducer from '../reducers/graph-update';
|
||||
|
||||
import BaseStore from './base';
|
||||
|
||||
|
||||
export default class GraphStore extends BaseStore {
|
||||
constructor() {
|
||||
super();
|
||||
this.graphReducer = new GraphReducer();
|
||||
//this.perf = performance.now();
|
||||
}
|
||||
|
||||
initialState() {
|
||||
return {
|
||||
keys: new Set([]),
|
||||
graphs: {},
|
||||
sidebarShown: true,
|
||||
};
|
||||
}
|
||||
|
||||
reduce(data, state) {
|
||||
this.graphReducer.reduce(data, this.state);
|
||||
//console.log(data, state);
|
||||
/*let perf = performance.now();
|
||||
console.log(perf - this.perf);
|
||||
this.perf = perf;*/
|
||||
}
|
||||
}
|
||||
|
@ -1,26 +0,0 @@
|
||||
import BaseSubscription from './base';
|
||||
|
||||
let getRandomInt = (max) => {
|
||||
return Math.floor(Math.random() * Math.floor(max));
|
||||
}
|
||||
|
||||
export default class GraphSubscription extends BaseSubscription {
|
||||
constructor(store, api, channel) {
|
||||
super(store, api, channel);
|
||||
this.connectionNumber = getRandomInt(999);
|
||||
}
|
||||
|
||||
start() {
|
||||
this.subscribe('/updates/' + this.connectionNumber, 'graph-view');
|
||||
}
|
||||
|
||||
handleEvent(diff) {
|
||||
if ('graph-view' in diff.data) {
|
||||
this.api.fetch(this.connectionNumber);
|
||||
} else {
|
||||
// extend
|
||||
this.store.handleEvent(diff);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user