diff --git a/pkg/interface/src/App.js b/pkg/interface/src/App.js index 5b481df2f..2ed981f25 100644 --- a/pkg/interface/src/App.js +++ b/pkg/interface/src/App.js @@ -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 { /> )} /> + ( + + )} + /> ( {}); + this.resetControllers(); + } + + render() { + const { state, props } = this; + + const renderChannelSidebar = (props, resource) => ( + + ); + + return ( +
+ { + return ( + +
+
+

+ Select, create, or join a chat to begin. +

+
+
+
+ ); + }} + /> + { + return ( + + + + ); + }} + /> + { + let resource = + `${props.match.params.ship}/${props.match.params.name}`; + const graph = state.graphs[resource] || new Map(); + + return ( + + + + ); + }} + /> + { + 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 ( + + + + ); + }} + /> +
+ ); + } +} diff --git a/pkg/interface/src/apps/graph-post/components/lib/channel-item.js b/pkg/interface/src/apps/graph-post/components/lib/channel-item.js new file mode 100644 index 000000000..8995fca1c --- /dev/null +++ b/pkg/interface/src/apps/graph-post/components/lib/channel-item.js @@ -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 ( +
+
+

+ {title} +

+
+
+ ); + } +} diff --git a/pkg/interface/src/apps/graph-post/components/lib/chat-tabbar.js b/pkg/interface/src/apps/graph-post/components/lib/chat-tabbar.js new file mode 100644 index 000000000..120d60e85 --- /dev/null +++ b/pkg/interface/src/apps/graph-post/components/lib/chat-tabbar.js @@ -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 ( +
+
+ ); + } +} diff --git a/pkg/interface/src/apps/graph-post/components/lib/message.js b/pkg/interface/src/apps/graph-post/components/lib/message.js new file mode 100644 index 000000000..d7241c299 --- /dev/null +++ b/pkg/interface/src/apps/graph-post/components/lib/message.js @@ -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 => ()); + +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) ? + ( +
+            {content.code.output[0].join('\n')}
+          
+ ) : null; + return ( +
+
+            {content.code.expression}
+          
+ {outputElement} +
+ ); + } 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 = ( + + ); + return ( + + {contents} + + ); + } else if (ytMatch) { + contents = ( +
+ +
+ ); + return ( + + ); + } else { + return ( + + {contents} + + ); + } + } else if ('me' in content) { + return ( +

+ {content.me} +

+ ); + } else { + return ( +
+ +
+ ); + } + } + + 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 ( +
{ + props.history.push(`/~post/room/${props.resource}/${props.index}`); + }} + > + +
+
+

+ + +

+

{timestamp}

+

{datestamp}

+
+ {this.renderContent()} +
+
+ ); + } else { + const timestamp = moment.unix(props.msg['time-sent'] / 1000).format('hh:mm'); + + return ( +
+

{timestamp}

+
+ {this.renderContent()} +
+
+ ); + } + } +} diff --git a/pkg/interface/src/apps/graph-post/components/lib/overlay-sigil.js b/pkg/interface/src/apps/graph-post/components/lib/overlay-sigil.js new file mode 100644 index 000000000..551643936 --- /dev/null +++ b/pkg/interface/src/apps/graph-post/components/lib/overlay-sigil.js @@ -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 ( +
+ + +
+ ); + } +} diff --git a/pkg/interface/src/apps/graph-post/components/lib/post-input.js b/pkg/interface/src/apps/graph-post/components/lib/post-input.js new file mode 100644 index 000000000..62f640c72 --- /dev/null +++ b/pkg/interface/src/apps/graph-post/components/lib/post-input.js @@ -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 ( +
+
+ +
+
+ { + this.editor = editor; + if (!/Android|webOS|iPhone|iPad|iPod|BlackBerry/i.test( + navigator.userAgent + )) { + editor.focus(); + } + }} + /> +
+
+
+
+ +
+
+ ); + } +} diff --git a/pkg/interface/src/apps/graph-post/components/new.js b/pkg/interface/src/apps/graph-post/components/new.js new file mode 100644 index 000000000..0219b3763 --- /dev/null +++ b/pkg/interface/src/apps/graph-post/components/new.js @@ -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 ( +
+
+ {'⟵ All Chats'} +
+

New Chat

+
+

Name

+