graph-post-js: starting work on recursive reducer

This commit is contained in:
Logan Allen 2020-06-15 13:46:08 -04:00
parent 7cb9ac4b62
commit 6609a25b37
16 changed files with 1679 additions and 8 deletions

View File

@ -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}

View 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>
);
}
}

View File

@ -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>
);
}
}

View File

@ -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>
);
}
}

View 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>
);
}
}
}

View File

@ -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>
);
}
}

View 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>
);
}
}

View 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>
);
}
}

View 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>
);
}
}

View 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>
);
}
}

View 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>
);
}
}

View 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>
);
}
}

View 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;
}
}

View File

@ -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;
}
}

View File

@ -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);
}
}

View File

@ -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() {