mirror of
https://github.com/urbit/shrub.git
synced 2024-12-24 11:24:21 +03:00
graph-post-js: starting work on recursive reducer
This commit is contained in:
parent
7cb9ac4b62
commit
6609a25b37
@ -7,6 +7,7 @@ import { light } from '@tlon/indigo-react';
|
||||
|
||||
import LaunchApp from './apps/launch/app';
|
||||
import GraphChatApp from './apps/graph-chat/app';
|
||||
import GraphPostApp from './apps/graph-post/app';
|
||||
import DojoApp from './apps/dojo/app';
|
||||
import GroupsApp from './apps/groups/app';
|
||||
import LinksApp from './apps/links/app';
|
||||
@ -99,6 +100,15 @@ export default class App extends React.Component {
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
<Route path="/~post" render={ p => (
|
||||
<GraphPostApp
|
||||
ship={this.ship}
|
||||
channel={channel}
|
||||
selectedGroups={selectedGroups}
|
||||
{...p}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
<Route path="/~dojo" render={ p => (
|
||||
<DojoApp
|
||||
ship={this.ship}
|
||||
|
169
pkg/interface/src/apps/graph-post/app.js
Normal file
169
pkg/interface/src/apps/graph-post/app.js
Normal file
@ -0,0 +1,169 @@
|
||||
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 GraphPostApp 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;
|
||||
|
||||
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}`;
|
||||
const graph = state.graphs[resource] || new Map();
|
||||
const node = graph.get(parseInt(props.match.params.nodeId, 10));
|
||||
console.log(graph);
|
||||
console.log(node);
|
||||
|
||||
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}
|
||||
{...props}
|
||||
/>
|
||||
</Skeleton>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
@ -0,0 +1,37 @@
|
||||
import React, { Component } from 'react';
|
||||
|
||||
export class ChannelItem extends Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
}
|
||||
|
||||
onClick() {
|
||||
const { props } = this;
|
||||
props.history.push('/~chat/room' + props.box);
|
||||
}
|
||||
|
||||
render() {
|
||||
const { props } = this;
|
||||
|
||||
const unreadElem = props.unread ? 'fw6' : '';
|
||||
|
||||
const title = props.title;
|
||||
|
||||
const selectedCss = props.selected
|
||||
? 'bg-gray4 bg-gray1-d gray3-d c-default'
|
||||
: 'bg-white bg-gray0-d gray3-d hover-bg-gray5 hover-bg-gray1-d pointer';
|
||||
|
||||
return (
|
||||
<div
|
||||
className={'z1 ph4 pb1 ' + selectedCss}
|
||||
onClick={this.onClick.bind(this)}
|
||||
>
|
||||
<div className="w-100 v-mid">
|
||||
<p className={'dib f9 ' + unreadElem}>
|
||||
{title}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
@ -0,0 +1,12 @@
|
||||
import React, { Component } from 'react';
|
||||
import { Link } from 'react-router-dom';
|
||||
|
||||
export class ChatTabBar extends Component {
|
||||
render() {
|
||||
const props = this.props;
|
||||
return (
|
||||
<div className="dib flex-shrink-0 flex-grow-1">
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
235
pkg/interface/src/apps/graph-post/components/lib/message.js
Normal file
235
pkg/interface/src/apps/graph-post/components/lib/message.js
Normal file
@ -0,0 +1,235 @@
|
||||
import React, { Component } from 'react';
|
||||
import { OverlaySigil } from './overlay-sigil';
|
||||
import { uxToHex, cite, writeText } from '../../../../lib/util';
|
||||
import moment from 'moment';
|
||||
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 class Message 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'));
|
||||
}
|
||||
|
||||
renderContent() {
|
||||
const { props } = this;
|
||||
const content = props.msg.contents[0];
|
||||
|
||||
if ('code' in content) {
|
||||
const outputElement =
|
||||
(Boolean(content.code.output) &&
|
||||
content.code.output.length && content.code.output.length > 0) ?
|
||||
(
|
||||
<pre className="f7 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="f7 clamp-attachment pa1 mt0 mb0 bg-light-gray b--gray4 b--gray1-d ba">
|
||||
{content.code.expression}
|
||||
</pre>
|
||||
{outputElement}
|
||||
</div>
|
||||
);
|
||||
} else if ('url' in content) {
|
||||
let imgMatch =
|
||||
/(jpg|img|png|gif|tiff|jpeg|JPG|IMG|PNG|TIFF|GIF|webp|WEBP|webm|WEBM|svg|SVG)$/
|
||||
.exec(content.url);
|
||||
const youTubeRegex = 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
|
||||
);
|
||||
const ytMatch =
|
||||
youTubeRegex.exec(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="f7 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="f7 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="f7 lh-copy v-top bb b--white-d b--black word-break-all"
|
||||
href={content.url}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
{contents}
|
||||
</a>
|
||||
);
|
||||
}
|
||||
} else if ('me' in content) {
|
||||
return (
|
||||
<p className='f7 i lh-copy v-top'>
|
||||
{content.me}
|
||||
</p>
|
||||
);
|
||||
} else {
|
||||
return (
|
||||
<section>
|
||||
<MessageMarkdown
|
||||
source={content.text}
|
||||
/>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
const { props, state } = this;
|
||||
const datestamp = '~' + moment.unix(props.msg['time-sent'] / 1000).format('YYYY.M.D');
|
||||
|
||||
const paddingTop = { 'paddingTop': '6px' };
|
||||
|
||||
if (true) {
|
||||
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';
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={this.containerRef}
|
||||
className={
|
||||
'w-100 f7 pl3 pt4 pr3 cf flex lh-copy bt b--white pointer'
|
||||
}
|
||||
style={{
|
||||
minHeight: 'min-content'
|
||||
}}
|
||||
onClick={() => {
|
||||
props.history.push(`/~post/room/${props.resource}/${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 f9 gray2 dib mr3 c-default">
|
||||
<span
|
||||
title={`~${props.msg.author}`}
|
||||
>
|
||||
</span>
|
||||
</p>
|
||||
<p className="v-mid mono f9 gray2 dib">{timestamp}</p>
|
||||
<p className="v-mid mono f9 ml2 gray2 dib child dn-s">{datestamp}</p>
|
||||
</div>
|
||||
{this.renderContent()}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
} else {
|
||||
const timestamp = moment.unix(props.msg['time-sent'] / 1000).format('hh:mm');
|
||||
|
||||
return (
|
||||
<div
|
||||
className={'w-100 pr3 cf hide-child flex'}
|
||||
style={{
|
||||
minHeight: 'min-content'
|
||||
}}
|
||||
>
|
||||
<p className="child pt2 pl2 pr1 mono f9 gray2 dib">{timestamp}</p>
|
||||
<div className="fr f7 clamp-message white-d pr3 lh-copy" style={{ flexGrow: 1 }}>
|
||||
{this.renderContent()}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,27 @@
|
||||
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>
|
||||
);
|
||||
}
|
||||
}
|
262
pkg/interface/src/apps/graph-post/components/lib/post-input.js
Normal file
262
pkg/interface/src/apps/graph-post/components/lib/post-input.js
Normal file
@ -0,0 +1,262 @@
|
||||
import React, { Component } from 'react';
|
||||
import _ from 'lodash';
|
||||
import moment from 'moment';
|
||||
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';
|
||||
|
||||
import { Sigil } from '../../../../lib/sigil';
|
||||
|
||||
import { uxToHex } from '../../../../lib/util';
|
||||
|
||||
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 class PostInput extends Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
message: '',
|
||||
};
|
||||
|
||||
this.textareaRef = React.createRef();
|
||||
|
||||
this.messageSubmit = this.messageSubmit.bind(this);
|
||||
|
||||
this.toggleCode = this.toggleCode.bind(this);
|
||||
|
||||
this.editor = null;
|
||||
|
||||
// perf testing:
|
||||
/* let closure = () => {
|
||||
let x = 0;
|
||||
for (var i = 0; i < 30; i++) {
|
||||
x++;
|
||||
props.api.chat.message(
|
||||
props.resource,
|
||||
`~${window.ship}`,
|
||||
Date.now(),
|
||||
{
|
||||
text: `${x}`
|
||||
}
|
||||
);
|
||||
}
|
||||
setTimeout(closure, 1000);
|
||||
};
|
||||
this.closure = closure.bind(this);*/
|
||||
|
||||
moment.updateLocale('en', {
|
||||
relativeTime : {
|
||||
past: function(input) {
|
||||
return input === 'just now'
|
||||
? input
|
||||
: input + ' ago';
|
||||
},
|
||||
s : 'just now',
|
||||
future: 'in %s',
|
||||
ss : '%d sec',
|
||||
m: 'a minute',
|
||||
mm: '%d min',
|
||||
h: 'an hr',
|
||||
hh: '%d hrs',
|
||||
d: 'a day',
|
||||
dd: '%d days',
|
||||
M: 'a month',
|
||||
MM: '%d months',
|
||||
y: 'a year',
|
||||
yy: '%d years'
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
getLetterType(letter) {
|
||||
if (letter.startsWith('/me ')) {
|
||||
letter = letter.slice(4);
|
||||
// remove insignificant leading whitespace.
|
||||
// aces might be relevant to style.
|
||||
while (letter[0] === '\n') {
|
||||
letter = letter.slice(1);
|
||||
}
|
||||
|
||||
return {
|
||||
me: letter
|
||||
};
|
||||
} else if (this.isUrl(letter)) {
|
||||
return {
|
||||
url: letter
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
text: letter
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
isUrl(string) {
|
||||
try {
|
||||
const websiteTest = new RegExp(String(/^((\w+:\/\/)[-a-zA-Z0-9:@;?&=\/%\+\.\*!'\(\),\$_\{\}\^~\[\]`#|]+)/.source)
|
||||
);
|
||||
return websiteTest.test(string);
|
||||
} catch (e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
messageSubmit() {
|
||||
if(!this.editor) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
const { props, state } = this;
|
||||
const editorMessage = this.editor.getValue();
|
||||
|
||||
console.log(editorMessage);
|
||||
|
||||
if (editorMessage === '') {
|
||||
return;
|
||||
}
|
||||
|
||||
if(state.code) {
|
||||
let post = props.api.createPost([
|
||||
{
|
||||
code: {
|
||||
expression: editorMessage,
|
||||
output: null
|
||||
}
|
||||
}
|
||||
]);
|
||||
|
||||
props.api.addPost(props.resource.ship, props.resource.name, post);
|
||||
this.editor.setValue('');
|
||||
return;
|
||||
}
|
||||
|
||||
let message = this.getLetterType(editorMessage);
|
||||
let post = props.api.createPost([message]);
|
||||
props.api.addPost(props.resource.ship, props.resource.name, post);
|
||||
|
||||
// perf:
|
||||
// setTimeout(this.closure, 2000);
|
||||
|
||||
this.editor.setValue('');
|
||||
}
|
||||
|
||||
toggleCode() {
|
||||
if(this.state.code) {
|
||||
this.setState({ code: false });
|
||||
this.editor.setOption('mode', MARKDOWN_CONFIG);
|
||||
this.editor.setOption('placeholder', this.props.placeholder);
|
||||
} else {
|
||||
this.setState({ code: true });
|
||||
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('');
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
const { props, state } = this;
|
||||
|
||||
|
||||
const codeTheme = state.code ? ' code' : '';
|
||||
|
||||
const options = {
|
||||
mode: MARKDOWN_CONFIG,
|
||||
theme: 'tlon' + codeTheme,
|
||||
lineNumbers: false,
|
||||
lineWrapping: true,
|
||||
scrollbarStyle: 'native',
|
||||
cursorHeight: 0.85,
|
||||
placeholder: state.code ? 'Code...' : props.placeholder,
|
||||
extraKeys: {
|
||||
'Enter': () => {
|
||||
this.messageSubmit();
|
||||
if (this.state.code) {
|
||||
this.toggleCode();
|
||||
}
|
||||
},
|
||||
'Shift-3': cm =>
|
||||
cm.getValue().length === 0
|
||||
? this.toggleCode()
|
||||
: CodeMirror.Pass
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="chat 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>
|
||||
<div
|
||||
className="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 (!/Android|webOS|iPhone|iPad|iPod|BlackBerry/i.test(
|
||||
navigator.userAgent
|
||||
)) {
|
||||
editor.focus();
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<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.code && '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>
|
||||
);
|
||||
}
|
||||
}
|
93
pkg/interface/src/apps/graph-post/components/new.js
Normal file
93
pkg/interface/src/apps/graph-post/components/new.js
Normal file
@ -0,0 +1,93 @@
|
||||
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('/~chat/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>
|
||||
);
|
||||
}
|
||||
}
|
116
pkg/interface/src/apps/graph-post/components/node-tree.js
Normal file
116
pkg/interface/src/apps/graph-post/components/node-tree.js
Normal file
@ -0,0 +1,116 @@
|
||||
import React, { Component } from 'react';
|
||||
import _ from 'lodash';
|
||||
import moment from 'moment';
|
||||
|
||||
import { Link } from 'react-router-dom';
|
||||
|
||||
import { Message } from './lib/message';
|
||||
import { ChatTabBar } from './lib/chat-tabbar';
|
||||
import { PostInput } from './lib/post-input';
|
||||
import { deSig } from '../../../lib/util';
|
||||
|
||||
export class NodeTreeScreen extends Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
console.log(props);
|
||||
|
||||
moment.updateLocale('en', {
|
||||
calendar: {
|
||||
sameDay: '[Today]',
|
||||
nextDay: '[Tomorrow]',
|
||||
nextWeek: 'dddd',
|
||||
lastDay: '[Yesterday]',
|
||||
lastWeek: '[Last] dddd',
|
||||
sameElse: 'DD/MM/YYYY'
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
parentPost() {
|
||||
console.log(props);
|
||||
return (
|
||||
<div></div>
|
||||
);
|
||||
}
|
||||
|
||||
postWindow() {
|
||||
const { props } = this;
|
||||
|
||||
//let graph = props.graph;
|
||||
let graph = new Map();
|
||||
let messages = Array.from(graph).reverse();
|
||||
|
||||
const messageElements = messages.map((msg, i) => {
|
||||
let index = msg[0];
|
||||
let node = msg[1];
|
||||
let post = node.post;
|
||||
|
||||
return (
|
||||
<Message
|
||||
key={index}
|
||||
msg={post}
|
||||
/>
|
||||
);
|
||||
});
|
||||
|
||||
|
||||
return (
|
||||
<div
|
||||
className="overflow-y-scroll bg-white bg-gray0-d pt3 pb2 flex flex-column relative"
|
||||
style={{ height: '100%', resize: 'vertical' }}
|
||||
>
|
||||
{messageElements}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
render() {
|
||||
const { props } = this;
|
||||
|
||||
let title = props.resource.name;
|
||||
console.log(props.node);
|
||||
|
||||
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' +
|
||||
'overflow-x-scroll overflow-x-auto-l overflow-x-auto-xl flex-shrink-0'}
|
||||
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' }}
|
||||
>
|
||||
{title}
|
||||
</h2>
|
||||
</Link>
|
||||
<ChatTabBar
|
||||
resource={props.resource}
|
||||
api={props.api}
|
||||
/>
|
||||
</div>
|
||||
<PostInput
|
||||
api={props.api}
|
||||
resource={props.resource}
|
||||
owner={deSig(props.match.params.ship)}
|
||||
placeholder="Message..."
|
||||
/>
|
||||
{this.parentPost()}
|
||||
{this.postWindow()}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
107
pkg/interface/src/apps/graph-post/components/post.js
Normal file
107
pkg/interface/src/apps/graph-post/components/post.js
Normal file
@ -0,0 +1,107 @@
|
||||
import React, { Component } from 'react';
|
||||
import _ from 'lodash';
|
||||
import moment from 'moment';
|
||||
|
||||
import { Link } from 'react-router-dom';
|
||||
|
||||
import { Message } from './lib/message';
|
||||
import { ChatTabBar } from './lib/chat-tabbar';
|
||||
import { PostInput } from './lib/post-input';
|
||||
import { deSig } from '../../../lib/util';
|
||||
|
||||
export class PostScreen extends Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
moment.updateLocale('en', {
|
||||
calendar: {
|
||||
sameDay: '[Today]',
|
||||
nextDay: '[Tomorrow]',
|
||||
nextWeek: 'dddd',
|
||||
lastDay: '[Yesterday]',
|
||||
lastWeek: '[Last] dddd',
|
||||
sameElse: 'DD/MM/YYYY'
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
postWindow() {
|
||||
const { props } = this;
|
||||
|
||||
let graph = props.graph;
|
||||
let messages = Array.from(graph).reverse();
|
||||
|
||||
const messageElements = messages.map((msg, i) => {
|
||||
let index = msg[0];
|
||||
let node = msg[1];
|
||||
let post = node.post;
|
||||
|
||||
return (
|
||||
<Message
|
||||
key={index}
|
||||
msg={post}
|
||||
resource={`${props.resource.ship}/${props.resource.name}`}
|
||||
index={index}
|
||||
history={props.history}
|
||||
/>
|
||||
);
|
||||
});
|
||||
|
||||
|
||||
return (
|
||||
<div
|
||||
className="overflow-y-scroll bg-white bg-gray0-d pt3 pb2 flex flex-column relative"
|
||||
style={{ height: '100%', resize: 'vertical' }}
|
||||
>
|
||||
{messageElements}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
render() {
|
||||
const { props } = this;
|
||||
|
||||
let title = props.resource.name;
|
||||
|
||||
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' +
|
||||
'overflow-x-scroll overflow-x-auto-l overflow-x-auto-xl flex-shrink-0'}
|
||||
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' }}
|
||||
>
|
||||
{title}
|
||||
</h2>
|
||||
</Link>
|
||||
<ChatTabBar
|
||||
resource={props.resource}
|
||||
api={props.api}
|
||||
/>
|
||||
</div>
|
||||
<PostInput
|
||||
api={props.api}
|
||||
resource={props.resource}
|
||||
owner={deSig(props.match.params.ship)}
|
||||
placeholder="Message..."
|
||||
/>
|
||||
{this.postWindow()}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
55
pkg/interface/src/apps/graph-post/components/sidebar.js
Normal file
55
pkg/interface/src/apps/graph-post/components/sidebar.js
Normal file
@ -0,0 +1,55 @@
|
||||
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>
|
||||
);
|
||||
}
|
||||
}
|
73
pkg/interface/src/apps/graph-post/components/skeleton.js
Normal file
73
pkg/interface/src/apps/graph-post/components/skeleton.js
Normal file
@ -0,0 +1,73 @@
|
||||
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>
|
||||
);
|
||||
}
|
||||
}
|
442
pkg/interface/src/apps/graph-post/css/custom.css
Normal file
442
pkg/interface/src/apps/graph-post/css/custom.css
Normal file
@ -0,0 +1,442 @@
|
||||
* {
|
||||
-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,5 +1,13 @@
|
||||
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);
|
||||
@ -28,7 +36,7 @@ export default class GraphReducer {
|
||||
|
||||
let resource = data.resource.ship + '/' + data.resource.name;
|
||||
if (!(resource in state.graphs)) {
|
||||
state.graphs[resource] = new Map();
|
||||
state.graphs[resource] = new OrderedMap();
|
||||
}
|
||||
|
||||
for (let i in data.graph) {
|
||||
@ -56,15 +64,43 @@ export default class GraphReducer {
|
||||
if (!('graphs' in state)) { return; }
|
||||
|
||||
let resource = data.resource.ship + '/' + data.resource.name;
|
||||
if (!(resource in state.graphs)) {
|
||||
return;
|
||||
}
|
||||
if (!(resource in state.graphs)) { return; }
|
||||
|
||||
for (let i in data.nodes) {
|
||||
let node = data.nodes[i];
|
||||
state.graphs[resource].set(node.index, node.node);
|
||||
if (node.index.split('/').length === 0) { return; }
|
||||
|
||||
let index = node.index.split('/').slice(1).map((ind) => {
|
||||
return parseInt(ind, 10);
|
||||
});
|
||||
|
||||
if (index.length === 0) { return; }
|
||||
|
||||
state.graphs[resource] = this._addNode(
|
||||
state.graphs[resource],
|
||||
index,
|
||||
node.node
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: recursive add node
|
||||
_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;
|
||||
}
|
||||
}
|
||||
|
@ -18,9 +18,7 @@ export default class GraphStore extends BaseStore {
|
||||
}
|
||||
|
||||
reduce(data, state) {
|
||||
console.log(data);
|
||||
this.graphReducer.reduce(data, this.state);
|
||||
console.log(state);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -8,7 +8,6 @@ export default class GraphSubscription extends BaseSubscription {
|
||||
constructor(store, api, channel) {
|
||||
super(store, api, channel);
|
||||
this.connectionNumber = getRandomInt(999);
|
||||
console.log(this.connectionNumber);
|
||||
}
|
||||
|
||||
start() {
|
||||
|
Loading…
Reference in New Issue
Block a user