Added all modulo apps except publish to interface monorepo.

This commit is contained in:
Logan Allen 2019-05-28 13:32:52 -07:00
parent 513ca1614a
commit e2c0d8bc25
105 changed files with 191859 additions and 0 deletions

63
.gitignore vendored Normal file
View File

@ -0,0 +1,63 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
*.swp
.DS_Store
# Runtime data
pids
*.pid
*.seed
*.pid.lock
# Directory for instrumented libs generated by jscoverage/JSCover
lib-cov
# Coverage directory used by tools like istanbul
coverage
# nyc test coverage
.nyc_output
# Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files)
.grunt
# Bower dependency directory (https://bower.io/)
bower_components
# node-waf configuration
.lock-wscript
# Compiled binary addons (https://nodejs.org/api/addons.html)
build/Release
# Dependency directories
node_modules/
jspm_packages/
# TypeScript v1 declaration files
typings/
# Optional npm cache directory
.npm
# Optional eslint cache
.eslintcache
# Optional REPL history
.node_repl_history
# Output of 'npm pack'
*.tgz
# Yarn Integrity file
.yarn-integrity
# dotenv environment variables file
.env
# next.js build output
.next

25
README.md Normal file
View File

@ -0,0 +1,25 @@
# Home
Create a `.urbitrc` file in this directory like so:
```
module.exports = {
URBIT_PIERS: [
"/path/to/fakezod/home"
]
};
```
You'll need `npm` installed (we recommend using [NVM](https://github.com/creationix/nvm) with node version 10.13.0)
Then:
```
npm install
npm install -g gulp-cli
gulp watch
```
Whenever you change some Home source code, this will recompile the code and
copy the updated version into your fakezod pier. Visit localhost:80 to launch Home (or whichever port is printed out to your terminal upon booting your ship).

132
chat/gulpfile.js Normal file
View File

@ -0,0 +1,132 @@
var gulp = require('gulp');
var cssimport = require('gulp-cssimport');
var cssnano = require('gulp-cssnano');
var rollup = require('gulp-better-rollup');
var sucrase = require('@sucrase/gulp-plugin');
var minify = require('gulp-minify');
var exec = require('child_process').exec;
var resolve = require('rollup-plugin-node-resolve');
var commonjs = require('rollup-plugin-commonjs');
var replace = require('rollup-plugin-replace');
var json = require('rollup-plugin-json');
var builtins = require('rollup-plugin-node-builtins');
var rootImport = require('rollup-plugin-root-import');
var globals = require('rollup-plugin-node-globals');
/***
Main config options
***/
var urbitrc = require('./.urbitrc');
/***
End main config options
***/
gulp.task('css-bundle', function() {
return gulp
.src('src/index.css')
.pipe(cssimport())
.pipe(cssnano())
.pipe(gulp.dest('./urbit/app/chat/css'));
});
gulp.task('jsx-transform', function(cb) {
return gulp.src('src/**/*.js')
.pipe(sucrase({
transforms: ['jsx']
}))
.pipe(gulp.dest('dist'));
});
gulp.task('js-imports', function(cb) {
return gulp.src('dist/index.js')
.pipe(rollup({
plugins: [
commonjs({
namedExports: {
'node_modules/react/index.js': [ 'Component' ],
'node_modules/react-is/index.js': [ 'isValidElementType' ],
}
}),
replace({
'process.env.NODE_ENV': JSON.stringify('development')
}),
rootImport({
root: `${__dirname}/dist/js`,
useEntry: 'prepend',
extensions: '.js'
}),
json(),
globals(),
builtins(),
resolve()
]
}, 'umd'))
.on('error', function(e){
console.log(e);
cb();
})
.pipe(gulp.dest('./urbit/app/chat/js/'))
.on('end', cb);
});
gulp.task('js-minify', function () {
return gulp.src('./urbit/app/chat/js/index.js')
.pipe(minify())
.pipe(gulp.dest('./urbit/app/chat/js/'));
});
gulp.task('js-cachebust', function(cb) {
return Promise.resolve(
exec('git log', function (err, stdout, stderr) {
let firstLine = stdout.split("\n")[0];
let commitHash = firstLine.split(' ')[1].substr(0, 10);
let newFilename = "index-" + commitHash + "-min.js";
exec('mv ./urbit/app/chat/js/index-min.js ./urbit/app/chat/js/' + newFilename);
})
);
})
gulp.task('urbit-copy', function () {
let ret = gulp.src('urbit/**/*');
urbitrc.URBIT_PIERS.forEach(function(pier) {
ret = ret.pipe(gulp.dest(pier));
});
return ret;
});
gulp.task('js-bundle-dev', gulp.series('jsx-transform', 'js-imports'));
gulp.task('js-bundle-prod', gulp.series('jsx-transform', 'js-imports', 'js-minify', 'js-cachebust'))
gulp.task('bundle-dev',
gulp.series(
gulp.parallel(
'css-bundle',
'js-bundle-dev'
),
'urbit-copy'
)
);
gulp.task('bundle-prod',
gulp.series(
gulp.parallel(
'css-bundle',
'js-bundle-prod'
),
'urbit-copy'
)
);
gulp.task('default', gulp.series('bundle-dev'));
gulp.task('watch', gulp.series('default', function() {
gulp.watch('src/**/*.js', gulp.parallel('js-bundle-dev'));
gulp.watch('src/**/*.css', gulp.parallel('css-bundle'));
gulp.watch('urbit/**/*', gulp.parallel('urbit-copy'));
}));

7133
chat/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

42
chat/package.json Normal file
View File

@ -0,0 +1,42 @@
{
"name": "urbit-apps",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"author": "",
"license": "ISC",
"devDependencies": {
"@sucrase/gulp-plugin": "^2.0.0",
"gulp": "^4.0.0",
"rollup": "^1.6.0",
"gulp-better-rollup": "^4.0.1",
"gulp-cssimport": "^6.0.0",
"gulp-cssnano": "^2.1.2",
"gulp-minify": "^3.1.0",
"rollup-plugin-commonjs": "^9.2.0",
"rollup-plugin-json": "^2.3.0",
"rollup-plugin-node-builtins": "^2.1.2",
"rollup-plugin-node-globals": "^1.4.0",
"rollup-plugin-node-resolve": "^3.4.0",
"rollup-plugin-replace": "^2.0.0",
"rollup-plugin-root-import": "^0.2.3",
"sucrase": "^3.8.0"
},
"dependencies": {
"classnames": "^2.2.6",
"lodash": "^4.17.11",
"moment": "^2.20.1",
"mousetrap": "^1.6.1",
"react": "^16.5.2",
"react-custom-scrollbars": "^4.2.1",
"react-dom": "^16.8.6",
"react-router-dom": "^5.0.0",
"urbit-ob": "^3.1.1"
},
"resolutions": {
"natives": "1.1.3"
}
}

90
chat/src/css/custom.css Normal file
View File

@ -0,0 +1,90 @@
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;
}
textarea, select, input, button {
outline: none;
-webkit-appearance: none;
border: none;
background-color:none;
}
h2 {
font-size: 32px;
line-height: 48px;
font-weight: bold;
}
.body-regular {
font-size: 16px;
line-height: 24px;
font-weight: 600;
}
.body-large {
font-size: 20px;
line-height: 24px;
}
.label-regular {
font-size: 14px;
line-height: 24px;
}
.label-small-mono {
font-size: 12px;
line-height: 24px;
font-family: "Source Code Pro", monospace;
}
.body-regular-400 {
font-size: 16px;
line-height: 24px;
font-weight: 400;
}
.plus-font {
font-size: 48px;
line-height: 24px;
}
.btn-font {
font-size: 14px;
line-height: 16px;
font-weight: 600;
}
.fw-normal {
font-weight: 400;
}
.fw-bold {
font-weight: bold;
}
.bg-v-light-gray {
background-color: #f9f9f9;
}
.nice-green {
color: #2AA779;
}
.bg-nice-green {
background: #2ED196;
}
.nice-red {
color: #EE5432;
}
.inter {
font-family: Inter, sans-serif;
}

63
chat/src/css/fonts.css Normal file
View File

@ -0,0 +1,63 @@
@font-face {
font-family: 'Inter';
font-style: normal;
font-weight: 400;
src: url("https://media.urbit.org/fonts/Inter-Regular.woff2") format("woff2");
}
@font-face {
font-family: 'Inter';
font-style: italic;
font-weight: 400;
src: url("https://media.urbit.org/fonts/Inter-Italic.woff2") format("woff2");
}
@font-face {
font-family: 'Inter';
font-style: normal;
font-weight: 700;
src: url("https://media.urbit.org/fonts/Inter-Bold.woff2") format("woff2");
}
@font-face {
font-family: 'Inter';
font-style: italic;
font-weight: 700;
src: url("https://media.urbit.org/fonts/Inter-BoldItalic.woff2") format("woff2");
}
@font-face {
font-family: "Source Code Pro";
src: url("https://storage.googleapis.com/media.urbit.org/fonts/scp-extralight.woff");
font-weight: 200;
}
@font-face {
font-family: "Source Code Pro";
src: url("https://storage.googleapis.com/media.urbit.org/fonts/scp-light.woff");
font-weight: 300;
}
@font-face {
font-family: "Source Code Pro";
src: url("https://storage.googleapis.com/media.urbit.org/fonts/scp-regular.woff");
font-weight: 400;
}
@font-face {
font-family: "Source Code Pro";
src: url("https://storage.googleapis.com/media.urbit.org/fonts/scp-medium.woff");
font-weight: 500;
}
@font-face {
font-family: "Source Code Pro";
src: url("https://storage.googleapis.com/media.urbit.org/fonts/scp-semibold.woff");
font-weight: 600;
}
@font-face {
font-family: "Source Code Pro";
src: url("https://storage.googleapis.com/media.urbit.org/fonts/scp-bold.woff");
font-weight: 700;
}

File diff suppressed because one or more lines are too long

4
chat/src/index.css Normal file
View File

@ -0,0 +1,4 @@
@import 'css/tachyons.css';
@import 'css/fonts.css';
@import 'css/custom.css';

16
chat/src/index.js Normal file
View File

@ -0,0 +1,16 @@
import React from 'react';
import ReactDOM from 'react-dom';
import { Root } from '/components/root';
import { api } from '/api';
import { store } from '/store';
import { subscription } from "/subscription";
api.setAuthTokens({
ship: window.ship
});
subscription.start();
ReactDOM.render((
<Root />
), document.querySelectorAll("#root")[0]);

183
chat/src/js/api.js Normal file
View File

@ -0,0 +1,183 @@
import React from 'react';
import ReactDOM from 'react-dom';
import _ from 'lodash';
import { uuid } from '/lib/util';
class UrbitApi {
setAuthTokens(authTokens) {
this.authTokens = authTokens;
this.bindPaths = [];
}
// keep default bind to hall, since its bind procedure more complex for now AA
bind(path, method, ship = this.authTokens.ship, appl = "hall", success, fail) {
this.bindPaths = _.uniq([...this.bindPaths, path]);
window.urb.subscribe(ship, appl, path,
(err) => {
fail(err);
},
(event) => {
success({
data: event,
from: {
ship,
path
}
});
},
(err) => {
fail(err);
});
}
hall(data) {
this.action("hall", "hall-action", data);
}
chat(data) {
this.action("chat", "chat-action", data);
}
action(appl, mark, data) {
return new Promise((resolve, reject) => {
window.urb.poke(ship, appl, mark, data,
(json) => {
resolve(json);
},
(err) => {
reject(err);
});
});
}
/*
Special actions
*/
permit(cir, aud, message) {
this.hall({
permit: {
nom: cir,
sis: aud,
inv: true
}
});
if (message) {
this.invite(cir, aud);
}
}
unpermit(cir, ship) {
/*
* lol, never send an unpermit to yourself.
* it puts your ship into an infinite loop.
* */
if (ship === window.ship) {
return;
}
this.hall({
permit: {
nom: cir,
sis: [ship],
inv: false
}
});
}
invite(cir, aud) {
let audInboxes = aud.map((aud) => `~${aud}/i`);
let inviteMessage = {
aud: audInboxes,
ses: [{
inv: {
inv: true,
cir: `~${window.ship}/${cir}`
}
}]
};
this.hall({
phrase: inviteMessage
});
}
message(aud, words) {
let msg = {
aud,
ses: [{
lin: {
msg: words,
pat: false
}
}]
};
this.hall({
phrase: msg
});
}
source(nom, sub) {
this.hall({
source: {
nom: "inbox",
sub: sub,
srs: [nom]
}
})
}
delete(nom) {
this.hall({
delete: {
nom,
why: ''
}
})
}
read(nom, red) {
this.hall({
read: {
nom,
red
}
})
}
create(nom, priv) {
this.hall({
create: {
nom: nom,
des: "chatroom",
sec: priv ? "village" : "channel"
}
});
}
ire(aud, uid, msg) {
let message = {
aud: aud,
ses: [{
ire: {
top: uid,
sep: {
lin: {
msg: msg,
pat: false
}
}
}
}]
}
this.hall({
phrase: message
})
}
}
export let api = new UrbitApi();
window.api = api;

View File

@ -0,0 +1,120 @@
import React, { Component } from 'react';
import classnames from 'classnames';
import _ from 'lodash';
import { Message } from '/components/lib/message';
import { ChatTabBar } from '/components/lib/chat-tabbar';
import { ChatInput } from '/components/lib/chat-input';
import { prettyShip, getMessageContent, isDMStation } from '/lib/util';
export class ChatScreen extends Component {
constructor(props) {
super(props);
this.state = {
station: props.match.params.ship + "/" + props.match.params.station,
circle: props.match.params.station,
host: props.match.params.ship,
numPeople: 0
};
this.buildMessage = this.buildMessage.bind(this);
}
componentDidMount() {
this.updateNumPeople();
}
componentDidUpdate(prevProps, prevState) {
const { props } = this;
if (prevProps !== props) {
this.setState({
station: props.match.params.ship + "/" + props.match.params.station,
circle: props.match.params.station,
host: props.match.params.ship,
numPeople: 0
});
}
this.updateReadNumber();
this.updateNumPeople();
this.updateNumMessagesLoaded(prevProps, prevState);
}
updateReadNumber() {
const { props, state } = this;
let messages = props.messages[state.station] || [];
let config = props.configs[state.station] || false;
let lastMsgNum = (messages.length > 0) ?
messages[messages.length - 1].num : 0;
if (config && config.red > lastMsgNum) {
props.api.read(circle, lastMsgNum);
}
}
updateNumPeople() {
let conf = this.props.configs[this.state.station] || {};
let sis = _.get(conf, 'con.sis');
let numPeople = !!sis ? sis.length : 0;
if (numPeople !== this.state.numPeople) {
this.setState({ numPeople });
}
}
updateNumMessagesLoaded(prevProps, prevState) {
let station = prevProps.messages[this.state.station] || [];
let numMessages = station.length;
if (numMessages > prevState.numMessages) {
this.setState({
numMessages: numMessages
});
}
}
buildMessage(msg) {
let details = msg.printship ? null : getMessageContent(msg.gam);
if (msg.printship) {
return (
<a
className="vanilla hoverline text-600 text-mono"
href={prettyShip(msg.gam.aut)[1]}>
{prettyShip(`~${msg.gam.aut}`)[0]}
</a>
);
}
return (
<Message key={msg.gam.uid} msg={msg.gam} details={details} />
);
}
render() {
let messages = this.props.messages[this.state.station] || [];
let chatMessages = messages.map(this.buildMessage);
return (
<div className="h-100 w-100 overflow-hidden flex flex-column">
<div className='pl2 pt2 bb mb3'>
<h2>{this.state.circle}</h2>
<ChatTabBar {...this.props} station={this.state.station} />
</div>
<div className="overflow-y-scroll" style={{ flexGrow: 1 }}>
{chatMessages}
</div>
<ChatInput
api={this.props.api}
configs={this.props.configs}
station={this.state.station}
circle={this.state.circle}
placeholder='Message...' />
</div>
)
}
}

View File

@ -0,0 +1,136 @@
import React, { Component } from 'react';
import _ from 'lodash';
import moment from 'moment';
import Mousetrap from 'mousetrap';
import classnames from 'classnames';
import { Sigil } from '/components/lib/icons/sigil';
import { IconSend } from '/components/lib/icons/icon-send';
import { isUrl, uuid, isDMStation } from '/lib/util';
export class ChatInput extends Component {
constructor(props) {
super(props);
this.state = {
message: ""
};
this.textareaRef = React.createRef();
this.messageSubmit = this.messageSubmit.bind(this);
this.messageChange = this.messageChange.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"
}
});
}
componentDidMount() {
this.bindShortcuts();
}
bindShortcuts() {
Mousetrap(this.textareaRef.current).bind('enter', e => {
e.preventDefault();
e.stopPropagation();
this.messageSubmit(e);
});
}
messageChange(event) {
this.setState({message: event.target.value});
}
messageSubmit() {
let aud, sep;
let wen = Date.now();
let uid = uuid();
let aut = window.ship;
let config = this.props.configs[this.state.station];
if (isDMStation(this.props.station)) {
aud = this.props.station
.split("/")[1]
.split(".")
.map((mem) => `~${mem}/${this.props.circle}`);
} else {
aud = [this.props.station];
}
if (isUrl(this.state.message)) {
sep = {
url: this.state.message
}
} else {
sep = {
lin: {
msg: this.state.message,
pat: false
}
}
}
let message = {
uid,
aut,
wen,
aud,
sep,
};
this.props.api.hall({
convey: [message]
});
this.setState({
message: ""
});
}
render() {
return (
<div className="mt2 pa3 cf flex black bt">
<div className="fl" style={{ flexBasis: 35, height: 40 }}>
<Sigil ship={window.ship} size={32} />
</div>
<div className="fr h-100 flex" style={{ flexGrow: 1, height: 40 }}>
<input className="ml2 bn"
style={{ flexGrow: 1 }}
ref={this.textareaRef}
placeholder={this.props.placeholder}
value={this.state.message}
onChange={this.messageChange} />
<div className="pointer" onClick={this.messageSubmit}>
<IconSend />
</div>
</div>
</div>
);
}
}

View File

@ -0,0 +1,56 @@
import React, { Component } from 'react';
import { Route, Link } from "react-router-dom";
import classnames from 'classnames';
export class ChatTabBar extends Component {
render() {
let toBaseLink = '/~chat/' + this.props.station;
let bbStream = '',
bbMembers = '',
bbSettings = '';
let strColor = '',
memColor = '',
setColor = '';
if (this.props.location.pathname.includes('/settings')) {
bbSettings = ' bb';
strColor = 'gray';
memColor = 'gray';
setColor = 'black';
} else if (this.props.location.pathname.includes('/members')) {
bbMembers = ' bb';
strColor = 'gray';
memColor = 'black';
setColor = 'gray';
} else {
bbStream = ' bb';
strColor = 'black';
memColor = 'gray';
setColor = 'gray';
}
return (
<div className="w-100" style={{ height:28 }}>
<div className={"dib w-20 h-100" + bbStream}>
<Link
className={'no-underline label-regular v-mid ' + strColor}
to={toBaseLink}>Stream</Link>
</div>
<div className={"dib w-20 h-100" + bbMembers}>
<Link
className={'no-underline label-regular v-mid ' + memColor}
to={toBaseLink + '/members'}>32 Members</Link>
</div>
<div className={"dib w-20 h-100" + bbSettings}>
<Link
className={'no-underline label-regular v-mid ' + setColor}
to={toBaseLink + '/settings'}>Settings</Link>
</div>
</div>
);
}
}

View File

@ -0,0 +1,9 @@
import React, { Component } from 'react';
export class IconSend extends Component {
render() {
return (
<img src="/~chat/img/Send.png" width={40} height={40} />
);
}
}

View File

@ -0,0 +1,20 @@
import React, { Component } from 'react';
import { sealDict } from '/components/lib/seal-dict';
export class Sigil extends Component {
render() {
let prefix = this.props.prefix ? JSON.parse(this.props.prefix) : false;
return (
<div
className="bg-black"
style={{ flexBasis: 35, padding: 4, paddingBottom: 0 }}>
{
sealDict.getSeal(this.props.ship, this.props.size, prefix)
}
</div>
);
}
}

View File

@ -0,0 +1,46 @@
import React, { Component } from 'react';
import classnames from 'classnames';
export class MemberElement extends Component {
onRemove() {
const { props } = this;
props.api.unpermit(props.circle, props.ship);
}
render() {
const { props } = this;
let actionElem;
if (props.isHost) {
actionElem = (
<p className="dib w-40 underline black label-small-mono label-regular">
Host
</p>
);
} else {
actionElem = (
<a onClick={this.onRemove.bind(this)}
className="w-40 dib underline black btn-font">
Remove
</a>
);
}
return (
<div>
<a
className={
"w-60 dib underline black pr3 mb2 label-small-mono label-regular"
}
href={`/~profile/${props.ship}`}>
{props.ship}
</a>
{actionElem}
</div>
);
}
}

View File

@ -0,0 +1,58 @@
import React, { Component } from 'react';
import { isDMStation, getMessageContent } from '/lib/util';
import { Sigil } from '/components/lib/icons/sigil';
import classnames from 'classnames';
import moment from 'moment';
export class Message extends Component {
renderContent(type) {
if (type === "url") {
if (/(jpg|img|png|gif|tiff|jpeg|JPG|IMG|PNG|TIFF)$/.exec(this.props.details.content)) {
return (
<img src={this.props.details.content} style={{width:"30%"}}></img>
)
} else {
return (
<a className="body-regular"
href={this.props.details.content}
target="_blank">{this.props.details.content}</a>
)
}
} else if (type === "exp") {
return (
<div>
<div className="label-small-mono">{this.props.details.content}</div>
<pre className="label-small-mono mt-0">{this.props.details.res}</pre>
</div>
)
} else if (['new item', 'edited item'].includes(type)) {
return <span className="text-body" dangerouslySetInnerHTML={{__html: this.props.details.snip}}></span>
} else if (type === "lin") {
return (
<p className="body-regular-400 v-top">{this.props.details.content}</p>
);
} else {
return <span className="label-small-mono">{'<unknown message type>'}</span>;
}
}
render() {
let pending = !!this.props.msg.pending ? ' o-80' : '';
return (
<div className={"w-100 pl3 pr3 pt2 pb2 mb2 cf flex" + pending}>
<div className="fl mr2">
<Sigil ship={this.props.msg.aut} size={32} />
</div>
<div className="fr" style={{ flexGrow: 1, marginTop: -4 }}>
<div>
<p className="v-top label-small-mono gray dib mr3">~{this.props.msg.aut}</p>
<p className="v-top label-small-mono gray dib">{moment.unix(this.props.msg.wen).format('hh:mm')}</p>
</div>
{this.renderContent(this.props.details.type)}
</div>
</div>
);
}
}

View File

@ -0,0 +1,106 @@
import React, { Component } from 'react';
import { pour } from '/vendor/sigils-1.2.5';
import _ from 'lodash';
const ReactSVGComponents = {
svg: p => {
return (
<svg key={Math.random()}
version={'1.1'}
xmlns={'http://www.w3.org/2000/svg'}
{...p.attr}>
{ _.map(_.get(p, 'children', []), child => ReactSVGComponents[child.tag](child)) }
</svg>
)
},
circle: p => {
return (
<circle
key={Math.random()} {...p.attr}>
{ _.map(_.get(p, 'children', []), child => ReactSVGComponents[child.tag](child)) }
</circle>
)
},
rect: p => {
return (
<rect
key={Math.random()}
{...p.attr}>
{ _.map(_.get(p, 'children', []), child => ReactSVGComponents[child.tag](child)) }
</rect>
)
},
path: p => {
return (
<path
key={Math.random()}
{...p.attr}>
{ _.map(_.get(p, 'children', []), child => ReactSVGComponents[child.tag](child)) }
</path>
)
},
g: p => {
return (
<g
key={Math.random()}
{...p.attr}>
{ _.map(_.get(p, 'children', []), child => ReactSVGComponents[child.tag](child)) }
</g>
)
},
polygon: p => {
return (
<polygon
key={Math.random()}
{...p.attr}>
{ _.map(_.get(p, 'children', []), child => ReactSVGComponents[child.tag](child)) }
</polygon>
)
},
line: p => {
return (
<line
key={Math.random()}
{...p.attr}>
{ _.map(_.get(p, 'children', []), child => ReactSVGComponents[child.tag](child)) }
</line>
)
},
polyline: p => {
return (
<polyline
key={Math.random()}
{...p.attr}>
{ _.map(_.get(p, 'children', []), child => ReactSVGComponents[child.tag](child)) }
</polyline>
)
}
}
export class SealDict {
constructor() {
this.dict = {};
}
getPrefix(patp) {
return patp.length === 3 ? patp : patp.substr(0, 3);
}
getSeal(patp, size, prefix) {
if (patp.length > 13) {
patp = "tiz";
}
let sigilShip = prefix ? this.getPrefix(patp) : patp;
let key = `${sigilShip}+${size}`;
if (!this.dict[key]) {
this.dict[key] = pour({size: size, patp: sigilShip, renderer: ReactSVGComponents, margin: 0, colorway: ["#fff", "#000"]})
}
return this.dict[key];
}
}
const sealDict = new SealDict;
export { sealDict }

View File

@ -0,0 +1,91 @@
import React, { Component } from 'react';
import classnames from 'classnames';
import _ from 'lodash';
export class SidebarInvite extends Component {
onAccept() {
const { props } = this;
let msg = props.msg;
let cir = _.get(props, 'msg.sep.inv.cir', false);
if (!cir) {
return;
}
this.updateInvite(msg.uid, cir, 'Accept');
}
onReject() {
const { props } = this;
let msg = props.msg;
let cir = _.get(props, 'msg.sep.inv.cir', false);
if (!cir) {
return;
}
this.updateInvite(msg.uid, cir, 'Reject');
}
updateInvite(uid, cir, resp) {
let tagstring = resp ? "Accept" : "Reject";
let msg = {
aud: [`~${window.ship}/i`],
ses: [{
ire: {
top: uid,
sep: {
lin: {
msg: `${tagstring} ${cir}`,
pat: false
}
}
}
}]
};
this.props.api.hall({
phrase: msg
});
this.props.api.source(cir, true);
}
render() {
const { props } = this;
let cir = _.get(props, 'msg.sep.inv.cir', false);
let aut = _.get(props, 'msg.aut', false);
if (!aut || !cir) {
return (
<div></div>
);
}
cir = cir.split('/')[1];
return (
<div className='pa3'>
<div className='w-100 v-mid'>
<div className="dib mr2 bg-nice-green" style={{
borderRadius: 12,
width: 12,
height: 12
}}></div>
<p className="dib body-regular fw-normal">Invite to&nbsp;
<span className='fw-bold'>
{cir}
</span>
</p>
</div>
<div className="w-100">
<p className='dib gray label-small-mono'>Hosted by {aut}</p>
</div>
<a className="dib w-50 pointer btn-font nice-green underline" onClick={this.onAccept.bind(this)}>Accept</a>
<a className="dib w-50 tr pointer btn-font nice-red underline" onClick={this.onReject.bind(this)}>Reject</a>
</div>
)
}
}

View File

@ -0,0 +1,29 @@
import React, { Component } from 'react';
import classnames from 'classnames';
export class SidebarItem extends Component {
onClick() {
const { props } = this;
props.history.push('/~chat/' + props.cir);
}
render() {
const { props } = this;
let selectedCss = !!props.selected ? 'bg-light-gray' : 'bg-white pointer';
return (
<div className={'pa3 ' + selectedCss} onClick={this.onClick.bind(this)}>
<div className='w-100 v-mid'>
<p className="body-regular">{props.title}</p>
</div>
<div className="w-100">
<p className='dib gray label-small-mono mr3'>{props.ship}</p>
<p className='dib gray label-small-mono'>{props.datetime}</p>
</div>
<p className='body-regular-400 gray'>{props.description}</p>
</div>
)
}
}

View File

@ -0,0 +1,89 @@
import React, { Component } from 'react';
import classnames from 'classnames';
import { ChatTabBar } from '/components/lib/chat-tabbar';
import { MemberElement } from '/components/lib/member-element';
export class MemberScreen extends Component {
constructor(props) {
super(props);
this.state = {
station: props.match.params.ship + "/" + props.match.params.station,
circle: props.match.params.station,
host: props.match.params.ship,
invMembers: ''
};
}
inviteMembers() {
const { props, state } = this;
let sis = state.invMembers.trim().split(',');
console.log(sis);
props.api.permit(state.circle, sis, true);
}
inviteMembersChange(e) {
this.setState({
invMembers: e.target.value
});
}
render() {
const { props, state } = this;
let listMembers = [
'zod',
'bus'
].map((mem) => {
return (
<MemberElement
key={mem}
isHost={ props.ship === state.host }
ship={mem}
circle={state.circle}
api={props.api} />
);
});
return (
<div className="h-100 w-100 overflow-x-hidden flex flex-column">
<div className='pl2 pt2 bb mb3'>
<h2>{state.circle}</h2>
<ChatTabBar {...props} station={state.station} />
</div>
<div className="w-100 cf">
<div className="w-50 fl pa2">
<p className="body-regular">Members</p>
<p className="label-regular gray mb3">
Everyone subscribed to this chat.
</p>
{listMembers}
</div>
{ `~${window.ship}` === state.host ? (
<div className="w-50 fr pa2">
<p className="body-regular">Invite</p>
<p className="label-regular gray mb3">
Invite new participants to this chat.
</p>
<textarea
className="w-80 db ba overflow-y-hidden gray mb2"
style={{
resize: 'none',
height: 150
}}
onChange={this.inviteMembersChange.bind(this)}></textarea>
<a
onClick={this.inviteMembers.bind(this)}
className="label-regular underline gray btn-font">
Invite
</a>
</div>
) : null }
</div>
</div>
)
}
}

View File

@ -0,0 +1,120 @@
import React, { Component } from 'react';
import classnames from 'classnames';
export class NewScreen extends Component {
constructor(props) {
super(props);
this.state = {
displayName: '',
idName: '',
invites: ''
};
this.dnChange = this.dnChange.bind(this);
this.idChange = this.idChange.bind(this);
this.invChange = this.invChange.bind(this);
}
dnChange(event) {
this.setState({displayName: event.target.value});
}
idChange(event) {
this.setState({idName: event.target.value});
}
invChange(event) {
this.setState({invites: event.target.value});
}
onClickCreate() {
if (!this.state.displayName) { return; }
if (!this.state.idName) { return; }
let station = `~${this.props.api.authTokens.ship}/${this.state.idName}`;
let actions = [
{
create: {
nom: this.state.idName,
des: "chatroom",
sec: "channel"
}
},
{
source: {
nom: 'inbox',
sub: true,
srs: [station]
}
}
];
if (this.state.invites.length > 0) {
let aud = this.state.invites
.trim()
.split(",")
.map(t => t.trim().substr(1));
actions.push({
permit: {
nom: this.state.idName,
sis: aud,
inv: true
}
});
actions.push({
phrase: {
aud: aud.map((aud) => `~${aud}/i`),
ses: [{
inv: {
inv: true,
cir: station
}
}]
}
});
}
this.props.api.chat({
actions: {
lis: actions
}
});
this.props.history.push('/~chat/' + station);
}
render() {
return (
<div className="h-100 w-100 pa3 pt2 overflow-x-hidden flex flex-column">
<h2 className="mb3">Create a New Chat</h2>
<div>
<p className="label-regular fw-bold">Display Name</p>
<input
className="body-large bn pa2 pl0 mb2 w-50"
placeholder="friend-club"
onChange={this.dnChange} />
<p className="label-regular fw-bold">Id Name</p>
<input
className="body-large bn pa2 pl0 mb2 w-50"
placeholder="secret-chat"
onChange={this.idChange} />
<p className="label-regular fw-bold">Invites</p>
<input
className="body-large bn pa2 pl0 mb2 w-50"
placeholder="~zod, ~bus"
onChange={this.invChange} />
<br />
<button
onClick={this.onClickCreate.bind(this)}
className="body-large pointer underline bn">-> Create</button>
</div>
</div>
);
}
}

View File

@ -0,0 +1,168 @@
import React, { Component } from 'react';
import { BrowserRouter, Route } from "react-router-dom";
import Mousetrap from 'mousetrap';
import classnames from 'classnames';
import _ from 'lodash';
import { api } from '/api';
import { store } from '/store';
import { Skeleton } from '/components/skeleton';
import { Sidebar } from '/components/sidebar';
import { ChatScreen } from '/components/chat';
import { MemberScreen } from '/components/member';
import { SettingsScreen } from '/components/settings';
import { NewScreen } from '/components/new';
export class Root extends Component {
constructor(props) {
super(props);
this.state = store.state;
store.setStateHandler(this.setState.bind(this));
Mousetrap.bind(["mod+n"], () => {
props.history.push('/~chat/new');
return false;
});
}
render() {
let configs = !!this.state.configs ? this.state.configs : {};
let circles = Object.keys(configs).filter((conf) => {
let cap = configs[conf].cap;
return cap === 'dm' || cap === 'chatroom';
});
let messages = _.get(this.state, 'messages', {});
let messagePreviews = {};
Object.keys(messages).forEach((stat) => {
let arr = messages[stat];
if (arr.length === 0) {
messagePreviews[stat] = null;
} else {
messagePreviews[stat] = arr[arr.length - 1];
}
});
let invites = _.get(this.state, 'messages', {});
if (`~${window.ship}/i` in invites) {
invites = invites[`~${window.ship}/i`];
} else {
invites = [];
}
return (
<BrowserRouter>
<div>
<Route exact path="/~chat"
render={ (props) => {
return (
<Skeleton
sidebar={
<Sidebar
circles={circles}
messagePreviews={messagePreviews}
invites={invites}
api={api}
{...props}
/>
}>
<div className="w-100 h-100 fr" style={{ flexGrow: 1 }}>
<div className="dt w-100 h-100">
<div className="dtc center v-mid w-100 h-100 bg-white">
<p className="tc">Cmd + N to start a new chat</p>
</div>
</div>
</div>
</Skeleton>
);
}} />
<Route exact path="/~chat/new"
render={ (props) => {
return (
<Skeleton
sidebar={
<Sidebar
circles={circles}
messagePreviews={messagePreviews}
invites={invites}
api={api}
{...props}
/>
}>
<NewScreen
api={api}
{...props}
/>
</Skeleton>
);
}} />
<Route exact path="/~chat/:ship/:station"
render={ (props) => {
return (
<Skeleton
sidebar={
<Sidebar
circles={circles}
messagePreviews={messagePreviews}
invites={invites}
api={api}
{...props}
/>
}>
<ChatScreen
api={api}
configs={configs}
messages={this.state.messages}
{...props}
/>
</Skeleton>
);
}} />
<Route exact path="/~chat/:ship/:station/members"
render={ (props) => {
return (
<Skeleton
sidebar={
<Sidebar
circles={circles}
messagePreviews={messagePreviews}
invites={invites}
api={api}
{...props}
/>
}>
<MemberScreen
{...props}
api={api}
/>
</Skeleton>
);
}} />
<Route exact path="/~chat/:ship/:station/settings"
render={ (props) => {
return (
<Skeleton
sidebar={
<Sidebar
circles={circles}
messagePreviews={messagePreviews}
invites={invites}
api={api}
{...props}
/>
}>
<SettingsScreen
{...props}
api={api}
store={store} />
</Skeleton>
);
}} />
</div>
</BrowserRouter>
)
}
}

View File

@ -0,0 +1,69 @@
import React, { Component } from 'react';
import classnames from 'classnames';
import { ChatTabBar } from '/components/lib/chat-tabbar';
export class SettingsScreen extends Component {
constructor(props) {
super(props);
this.state = {
station: props.match.params.ship + "/" + props.match.params.station,
circle: props.match.params.station,
host: props.match.params.ship,
};
}
deleteChat() {
const { props, state } = this;
props.api.delete(state.circle);
if (state.station in props.store.state.messages) {
delete props.store.state.messages[state.station];
}
if (state.station in props.store.state.configs) {
delete props.store.state.configs[state.station];
}
props.store.state.circles = props.store.state.circles
.filter((elem) => {
return elem !== state.circle;
});
props.history.push('/~chat');
props.store.setState(props.store.state);
}
render() {
return (
<div className="h-100 w-100 overflow-x-hidden flex flex-column">
<div className='pl2 pt2 bb mb3'>
<h2>{this.state.circle}</h2>
<ChatTabBar {...this.props} station={this.state.station} />
</div>
<div className="w-100 cf pa3">
<h2>Settings</h2>
<div className="w-50 fl pr2 mt3">
<p className="body-regular">Rename</p>
<p className="label-regular gray mb3">
Change the name of this chat.
</p>
<p className="label-small-mono inter">Chat Name</p>
<input type="text" className="ba gray pa2 w-80" />
</div>
<div className="w-50 fr pl2 mt3">
<p className="body-regular">Delete Chat</p>
<p className="label-regular gray mb3">
Permanently delete this chat.
</p>
<a onClick={this.deleteChat.bind(this)}
className="pointer btn-font underline nice-red">-> Delete</a>
</div>
</div>
</div>
)
}
}

View File

@ -0,0 +1,98 @@
import React, { Component } from 'react';
import classnames from 'classnames';
import moment from 'moment';
import _ from 'lodash';
import { getMessageContent } from '/lib/util';
import { SidebarItem } from '/components/lib/sidebar-item';
import { SidebarInvite } from '/components/lib/sidebar-invite';
export class Sidebar extends Component {
onClickNew() {
this.props.history.push('/~chat/new');
}
render() {
const { props } = this;
let station = props.match.params.ship + '/' + props.match.params.station;
let sidebarItems = props.circles.map((cir) => {
let msg = props.messagePreviews[cir];
let parsed = !!msg ? getMessageContent(msg.gam) : {
content: 'No messages yet'
};
let aut = !!msg ? msg.gam.aut : '';
let wen = !!msg ? msg.gam.wen : 0;
let datetime =
!!msg ?
moment.unix(wen / 1000).from(moment.utc())
: '';
return {
msg,
datetime,
wen,
aut,
parsed,
cir,
title: cir.split('/')[1],
selected: station === cir
};
})
.sort((a, b) => {
return b.wen - a.wen;
})
.map((obj) => {
return (
<SidebarItem
key={obj.cir}
title={obj.title}
description={obj.parsed.content}
cir={obj.cir}
datetime={obj.datetime}
ship={obj.aut}
selected={obj.selected}
history={props.history}
/>
);
});
let invites = [];
let filterInvites = {};
props.invites.forEach((msg) => {
let uid = _.get(msg, 'gam.sep.ire.top', false);
if (!uid) {
invites.push(msg.gam);
} else {
filterInvites[uid] = true;
}
});
let inviteItems = invites.filter((msg) => {
return !(msg.uid in filterInvites);
}).map((inv) => {
return (
<SidebarInvite key={inv.uid} msg={inv} api={props.api} />
);
});
return (
<div className="h-100 w-100 overflow-x-hidden flex flex-column">
<div className="pl3 pr3 pt3 pb3 cf">
<p className="dib w-50 fw-bold body-large">Chat</p>
<a
className="dib tr w-50 pointer plus-font"
onClick={this.onClickNew.bind(this)}>+</a>
</div>
<div style={{ flexGrow: 1 }}>
{inviteItems}
{sidebarItems}
</div>
</div>
)
}
}

View File

@ -0,0 +1,19 @@
import React, { Component } from 'react';
import classnames from 'classnames';
export class Skeleton extends Component {
render() {
return (
<div className="cf h-100 w-100 absolute flex">
<div className="fl h-100 br overflow-x-hidden" style={{ flexBasis: 320 }}>
{this.props.sidebar}
</div>
<div className="h-100 fr" style={{ flexGrow: 1 }}>
{this.props.children}
</div>
</div>
);
}
}

363
chat/src/js/lib/util.js Normal file
View File

@ -0,0 +1,363 @@
import _ from 'lodash';
import urbitOb from 'urbit-ob';
import classnames from 'classnames';
export const AGGREGATOR_COLL = "c";
export const AGGREGATOR_INBOX = "aggregator-inbox";
export const AGGREGATOR_NAMES = [AGGREGATOR_INBOX, AGGREGATOR_COLL];
export function capitalize(str) {
return `${str[0].toUpperCase()}${str.substr(1)}`;
}
// takes a galactic (urbit) time and converts to 8601
export function esoo(str) {
var dubb = function(num) {
return num < 10 ? '0' + parseInt(num) : parseInt(num);
}
const p = /\~(\d\d\d\d).(\d\d?).(\d\d?)..(\d\d?).(\d\d?).(\d\d?)/.exec(str);
if (p) {
return `${p[1]}-${dubb(p[2])}-${dubb(p[3])}T${dubb(p[4])}:${dubb(p[5])}:${dubb(p[6])}Z`
}
return false;
}
// check if hostname follows ship.*.urbit.org scheme
export function isProxyHosted(hostName) {
const r = /([a-z,-]+)\.(.+\.)?urbit\.org/.exec(hostName);
if (r && urbitOb.isValidPatp(r[1])) {
return true;
}
return false;
}
export function getQueryParams() {
if (window.location.search !== "") {
return JSON.parse('{"' + decodeURI(window.location.search.substr(1).replace(/&/g, "\",\"").replace(/=/g,"\":\"")) + '"}');
} else {
return {};
}
}
export function isAggregator(station) {
let cir = station.split("/")[1]
return AGGREGATOR_NAMES.includes(cir);
}
/*
Goes from:
1531943107869 // "javascript unix time"
To:
"48711y 2w 5d 11m 9s" // "stringified time increments"
*/
export function secToString(secs) {
if (secs <= 0) {
return 'Completed';
}
secs = Math.floor(secs)
var min = 60;
var hour = 60 * min;
var day = 24 * hour;
var week = 7 * day;
var year = 52 * week;
var fy = function(s) {
if (s < year) {
return ['', s];
} else {
return [Math.floor(s / year) + 'y', s % year];
}
}
var fw = function(tup) {
var str = tup[0];
var sec = tup[1];
if (sec < week) {
return [str, sec];
} else {
return [str + ' ' + Math.floor(sec / week) + 'w', sec % week];
}
}
var fd = function(tup) {
var str = tup[0];
var sec = tup[1];
if (sec < day) {
return [str, sec];
} else {
return [str + ' ' + Math.floor(sec / day) + 'd', sec % day];
}
}
var fh = function(tup) {
var str = tup[0];
var sec = tup[1];
if (sec < hour) {
return [str, sec];
} else {
return [str + ' ' + Math.floor(sec / hour) + 'h', sec % hour];
}
}
var fm = function(tup) {
var str = tup[0];
var sec = tup[1];
if (sec < min) {
return [str, sec];
} else {
return [str + ' ' + Math.floor(sec / min) + 'm', sec % min];
}
}
var fs = function(tup) {
var str = tup[0];
var sec = tup[1];
return str + ' ' + sec + 's';
}
return fs(fm(fh(fd(fw(fy(secs)))))).trim();
}
export function uuid() {
let str = "0v"
str += Math.ceil(Math.random()*8)+"."
for (var i = 0; i < 5; i++) {
let _str = Math.ceil(Math.random()*10000000).toString(32);
_str = ("00000"+_str).substr(-5,5);
str += _str+".";
}
return str.slice(0,-1);
}
export function isPatTa(str) {
const r = /^[a-z,0-9,\-,\.,_,~]+$/.exec(str)
return !!r;
}
export function isValidStation(st) {
let tokens = st.split("/")
if (tokens.length !== 2) return false;
return urbitOb.isValidPatp(tokens[0]) && isPatTa(tokens[1]);
}
/*
Goes from:
~2018.7.17..23.15.09..5be5 // urbit @da
To:
(javascript Date object)
*/
export function daToDate(st) {
var dub = function(n) {
return parseInt(n) < 10 ? "0" + parseInt(n) : n.toString();
};
var da = st.split('..');
var bigEnd = da[0].split('.');
var lilEnd = da[1].split('.');
var ds = `${bigEnd[0].slice(1)}-${dub(bigEnd[1])}-${dub(bigEnd[2])}T${dub(lilEnd[0])}:${dub(lilEnd[1])}:${dub(lilEnd[2])}Z`;
return new Date(ds);
}
/*
Goes from:
(javascript Date object)
To:
~2018.7.17..23.15.09..5be5 // urbit @da
*/
export function dateToDa(d, mil) {
  var fil = function(n) {
    return n >= 10 ? n : "0" + n;
  };
  return (
    `~${d.getUTCFullYear()}.` +
    `${(d.getUTCMonth() + 1)}.` +
    `${fil(d.getUTCDate())}..` +
    `${fil(d.getUTCHours())}.` +
    `${fil(d.getUTCMinutes())}.` +
    `${fil(d.getUTCSeconds())}` +
`${mil ? "..0000" : ""}`
  );
}
// ascending for clarity
// export function sortSrc(circleArray, topic=false){
// let sc = circleArray.map((c) => util.parseCollCircle(c)).filter((pc) => typeof pc != 'undefined' && typeof pc.top == 'undefined');
// return sc.map((src) => src.coll).sort((a, b) => util.daToDate(a) - util.daToDate(b));
// }
export function arrayEqual(a, b) {
if (a === b) return true;
if (a == null || b == null) return false;
if (a.length != b.length) return false;
// If you don't care about the order of the elements inside
// the array, you should sort both arrays here.
for (var i = 0; i < a.length; ++i) {
if (a[i] !== b[i]) return false;
}
return true;
}
function deSig(ship) {
return ship.replace('~', '');
}
// use urbit.org proxy if it's not on our ship
export function foreignUrl(shipName, own, urlFrag) {
if (deSig(shipName) != deSig(own)) {
return `http://${deSig(shipName)}.urbit.org${urlFrag}`
} else {
return urlFrag
}
}
// shorten comet names
export function prettyShip(ship) {
const sp = ship.split('-');
return [sp.length == 9 ? `${sp[0]}_${sp[8]}`: ship, ship[0] === '~' ? `/~profile/${ship}` : `/~profile/~${ship}`];
}
export function profileUrl(ship) {
return `/~landscape/profile/~${ship}`;
}
export function isDMStation(station) {
let host = station.split('/')[0].substr(1);
let circle = station.split('/')[1];
return (
station.indexOf('.') !== -1 &&
circle.indexOf(host) !== -1
);
}
export function isRootCollection(station) {
return station.split("/")[1] === "c";
}
// maybe do fancier stuff later
export function isUrl(string) {
const r = /^http|^www|\.com$/.exec(string)
if (r) {
return true
}
else {
return false
}
}
export function arrayify(obj) {
let ret = [];
Object.keys(obj).forEach((key) => {
ret.push({key, value: obj[key]});
})
return ret;
}
export function getMessageContent(msg) {
let ret;
const MESSAGE_TYPES = {
'sep.app.sep.fat.sep.lin.msg': 'app',
'sep.app.sep.lin.msg': 'app',
'sep.app.sep.inv': (msg) => {
let sta = msg.sep.app.sep.inv.cir;
let [hos, cir] = sta.split('/');
return {
type: 'inv',
msg: msg,
content: {
nom: msg.sep.app.app,
sta: sta,
hos: hos,
inv: msg.sep.app.sep.inv.inv
}
}
},
'sep.inv': (msg) => {
let sta = msg.sep.inv.cir;
let [hos, cir] = sta.split('/');
return {
type: 'inv',
msg: msg,
content: {
nom: cir,
inv: msg.sep.inv.inv,
hos,
sta,
cir
}
}
},
'sep.fat': (msg) => {
let type = msg.sep.fat.tac.text;
let station = msg.aud[0];
let jason = JSON.parse(msg.sep.fat.sep.lin.msg);
let content = (type.includes('collection')) ? null : jason.content;
let par = jason.path.slice(0, -1);
return {
type: msg.sep.fat.tac.text,
msg: msg,
contentType: jason.type,
content: content,
snip: jason.snip,
author: jason.author,
host: jason.host,
date: jason.date,
path: jason.path,
postTitle: jason.name,
postUrl: `/~landscape/collections/${jason.host}/${jason.path.slice(2).join('/')}`,
}
},
'sep.lin.msg': 'lin',
'sep.ire.sep.lin': (msg) => {
return {
type: "lin",
msg: msg,
content: msg.sep.ire.sep.lin.msg,
replyUid: msg.sep.ire.top
}
},
'sep.ire': 'ire',
'sep.url': 'url',
'sep.exp': (msg) => {
return {
type: "exp",
msg: msg,
content: msg.sep.exp.exp,
res: msg.sep.exp.res.join('\n')
}
},
}
arrayify(MESSAGE_TYPES).some(({key, value}) => {
if (_.has(msg, key)) {
if (typeof value === "string") {
ret = {
type: value,
msg: msg,
content: _.get(msg, key)
}
} else if (typeof value === "function") {
ret = value(msg);
}
return true;
}
});
if (typeof ret === "undefined") {
ret = {type: "unknown"};
console.log("ASSERT: unknown message type on ", msg)
}
return ret;
}
window.getMessageContent = getMessageContent;

View File

@ -0,0 +1,15 @@
import _ from 'lodash';
export class ChatReducer {
reduce(json, state) {
let data = _.get(json, 'chat', false);
if (data) {
state.messages = data.messages;
state.inbox = data.inbox;
state.configs = data.configs;
state.circles = data.circles;
}
}
}

View File

@ -0,0 +1,42 @@
import _ from 'lodash';
export class UpdateReducer {
reduce(json, state) {
let data = _.get(json, 'update', false);
if (data) {
this.reduceInbox(_.get(data, 'inbox', false), state);
this.reduceMessage(_.get(data, 'message', false), state);
this.reduceConfig(_.get(data, 'config', false), state);
this.reduceCircles(_.get(data, 'circles', false), state);
}
}
reduceInbox(inbox, state) {
if (inbox) {
state.inbox = inbox;
}
}
reduceMessage(message, state) {
if (message.circle in state.messages) {
state.messages[message.circle].push(message.envelope);
} else {
state.messages[message.circle] = [message.envelope];
}
}
reduceConfig(config, state) {
if (config) {
state.configs[config.circle] = config.config;
}
}
reduceCircles(circles, state) {
if (circles) {
state.circles = circles;
}
}
}

35
chat/src/js/store.js Normal file
View File

@ -0,0 +1,35 @@
import _ from 'lodash';
import { ChatReducer } from '/reducers/chat';
import { UpdateReducer } from '/reducers/update';
class Store {
constructor() {
this.state = {
inbox: {},
messages: [],
configs: {},
circles: []
};
this.chatReducer = new ChatReducer();
this.updateReducer = new UpdateReducer();
this.setState = () => {};
}
setStateHandler(setState) {
this.setState = setState;
}
handleEvent(data) {
let json = data.data;
this.chatReducer.reduce(json, this.state);
this.updateReducer.reduce(json, this.state);
this.setState(this.state);
}
}
export let store = new Store();

View File

@ -0,0 +1,53 @@
import { api } from '/api';
import _ from 'lodash';
import { store } from '/store';
export class Subscription {
start() {
if (api.authTokens) {
this.initializeChat();
//this.setCleanupTasks();
} else {
console.error("~~~ ERROR: Must set api.authTokens before operation ~~~");
}
}
/*setCleanupTasks() {
window.addEventListener("beforeunload", e => {
api.bindPaths.forEach(p => {
this.wipeSubscription(p);
});
});
}
wipeSubscription(path) {
api.hall({
wipe: {
sub: [{
hos: api.authTokens.ship,
pax: path
}]
}
});
}*/
initializeChat() {
api.bind('/primary', 'PUT', api.authTokens.ship, 'chat',
this.handleEvent.bind(this),
this.handleError.bind(this));
}
handleEvent(diff) {
store.handleEvent(diff);
}
handleError(err) {
console.error(err);
api.bind('/', "PUT", api.authTokens.ship, 'chat',
this.handleEvent.bind(this),
this.handleError.bind(this));
}
}
export let subscription = new Subscription();

1
chat/src/js/vendor/sigils-1.2.5.js vendored Normal file

File diff suppressed because one or more lines are too long

415
chat/urbit/app/chat.hoon Normal file
View File

@ -0,0 +1,415 @@
/- hall
/+ *server, chat, hall-json
/= index
/^ octs
/; as-octs:mimes:html
/: /===/app/chat/index
/| /html/
/~ ~
==
/= script
/^ octs
/; as-octs:mimes:html
/: /===/app/chat/js/index
/| /js/
/~ ~
==
/= style
/^ octs
/; as-octs:mimes:html
/: /===/app/chat/css/index
/| /css/
/~ ~
==
/= style
/^ octs
/; as-octs:mimes:html
/: /===/app/chat/css/index
/| /css/
/~ ~
==
/= chat-png
/^ (map knot @)
/: /===/app/chat/img /_ /png/
::
=, chat
::
|_ [bol=bowl:gall sta=state]
::
++ this .
::
:: +prep: set up the app, migrate the state once started
::
++ prep
|= old=(unit state)
^- (quip move _this)
?~ old
=/ inboxpat /circle/inbox/config/group
=/ circlespat /circles/[(scot %p our.bol)]
=/ inboxi/poke
:- %hall-action
[%source %inbox %.y (silt [[our.bol %i] ~]~)]
:_ this
:~ [ost.bol %peer inboxpat [our.bol %hall] inboxpat]
[ost.bol %peer circlespat [our.bol %hall] circlespat]
[ost.bol %connect / [~ /'~chat'] %chat]
[ost.bol %poke /chat [our.bol %hall] inboxi]
==
[~ this(sta u.old)]
::
:: +peer-primary: subscribe to our data and updates
::
++ peer-primary
|= wir=wire
^- (quip move _this)
:_ this
[ost.bol %diff %chat-streams str.sta]~
::
++ poke-noun
|= a=*
^- (quip move _this)
~& sta
[~ this]
::
:: +poke-chat: send us an action
::
++ poke-chat-action
|= act=action:chat
^- (quip move _this)
:_ this
%+ turn lis.act
|= hac=action:hall
^- move
:* ost.bol
%poke
/p/[(scot %da now.bol)]
[our.bol %hall]
[%hall-action hac]
==
::
:: +send-chat-update: utility func for sending updates to all our subscribers
::
++ send-chat-update
|= upd=update
^- (list move)
%+ turn (prey:pubsub:userlib /primary bol)
|= [=bone *]
[bone %diff %chat-update upd]
::
::
:: +hall arms
::
::
:: +diff-hall-prize: handle full state initially handed to us by hall
::
++ diff-hall-prize
|= [wir=wire piz=prize:hall]
^- (quip move _this)
?~ wir
(mean [leaf+"invalid wire for diff: {(spud wir)}"]~)
?+ i.wir
(mean [leaf+"invalid wire for diff: {(spud wir)}"]~)
::
:: %circles wire
::
%circles
?> ?=(%circles -.piz)
:- (send-chat-update [%circles cis.piz])
%= this
circles.str.sta cis.piz
==
::
:: %circle wire
::
%circle
:: ?+ -.piz
:: ::
:: :: %peers prize
:: ::
:::: %peers
:::: ?> ?=(%peers -.piz)
:::: [~ this]
:: ::
:: :: %circle prize
:: ::
:: %circle
?> ?=(%circle -.piz)
=/ circle/circle:hall [our.bol &2:wir]
?: =(circle [our.bol %inbox])
::
:: fill inbox config and remote configs with prize data
::
=/ configs
%- ~(uni in configs.str.sta)
^- (map circle:hall (unit config:hall))
(~(run by rem.cos.piz) |=(a=config:hall `a))
~& pes.piz
=/ circles/(list circle:hall)
%+ turn ~(tap in src.loc.cos.piz)
|= src=source:hall
^- circle:hall
cir.src
=/ meslis/(list [circle:hall (list envelope:hall)])
%+ turn circles
|= cir=circle:hall
^- [circle:hall (list envelope:hall)]
[cir ~]
:-
%+ turn ~(tap in (~(del in (silt circles)) [our.bol %inbox]))
|= cir=circle:hall
^- move
=/ pat/path /circle/[nom.cir]/config/grams
[ost.bol %peer pat [our.bol %hall] pat]
%= this
inbox.str.sta loc.cos.piz
configs.str.sta configs
messages.str.sta (molt meslis)
==
::
:: fill remote configs with message data
::
=* messages messages.str.sta
=/ circle/circle:hall [our.bol &2:wir]
:- ~
%= this
messages.str.sta (~(put by messages) circle nes.piz)
==
==
::
:: +diff-hall-rumor: handle updates to hall state
::
++ diff-hall-rumor
|= [wir=wire rum=rumor:hall]
^- (quip move _this)
?~ wir
(mean [leaf+"invalid wire for diff: {(spud wir)}"]~)
?+ i.wir
(mean [leaf+"invalid wire for diff: {(spud wir)}"]~)
::
:: %circles
%circles
?> ?=(%circles -.rum)
?: add.rum
=/ cis (~(put in circles.str.sta) cir.rum)
:- (send-chat-update [%circles cis])
%= this
circles.str.sta cis
==
=/ cis (~(del in circles.str.sta) cir.rum)
:- (send-chat-update [%circles cis])
%= this
circles.str.sta cis
==
::
::
:: %circle: fill remote configs with message data
::
%circle
?> ?=(%circle -.rum)
=* sto rum.rum
?+ -.sto
[~ this]
::
:: %gram:
::
%gram
?> ?=(%gram -.sto)
=* messages messages.str.sta
=/ circle/circle:hall [our.bol &2:wir]
=/ nes/(list envelope:hall) (~(got by messages) circle)
:- (send-chat-update [%message circle nev.sto])
%= this
messages.str.sta (~(put by messages) circle (snoc nes nev.sto))
==
::
:: %peer:
::
%peer
?> ?=(%peer -.sto)
~& add.sto
~& who.sto
~& qer.sto
[~ this]
::
:: %config: config has changed
::
%config
=* circ cir.sto
::
?+ -.dif.sto
[~ this]
::
:: %full: set all of config without side effects
::
%full
=* conf cof.dif.sto
:- (send-chat-update [%config circ conf])
%= this
configs.str.sta (~(put by configs.str.sta) circ `conf)
==
::
:: %read: the read count of one of our configs has changed
::
%read
?: =(circ [our.bol %inbox])
:: ignore when circ is inbox
[~ this]
=/ uconf/(unit config:hall) (~(got by configs.str.sta) circ)
?~ uconf
:: should we crash?
[~ this]
=/ conf/config:hall
%= u.uconf
red red.dif.sto
==
:- (send-chat-update [%config circ conf])
%= this
configs.str.sta (~(put by configs.str.sta) circ `conf)
==
::
:: %source: the sources of our inbox have changed
::
%source
?. =(circ [our.bol %inbox])
:: ignore when circ is not inbox
[~ this]
=* affectedcir cir.src.dif.sto
=/ pat/path /circle/[nom.affectedcir]/grams/config
:: we've added a source to our inbox
::
?: add.dif.sto
=/ newinbox %= inbox.str.sta
src (~(put in src.inbox.str.sta) src.dif.sto)
==
:-
%+ weld
[ost.bol %peer pat [our.bol %hall] pat]~
(send-chat-update [%inbox newinbox])
%= this
inbox.str.sta newinbox
::src.inbox.str.sta (~(put in src.inbox.str.sta) src.dif.sto)
::
configs.str.sta
?: (~(has by configs.str.sta) affectedcir)
configs.str.sta
(~(put by configs.str.sta) affectedcir ~)
==
=/ newinbox %= inbox.str.sta
src (~(del in src.inbox.str.sta) src.dif.sto)
==
:: we've removed a source from our inbox
::
:-
%+ weld
[ost.bol %pull pat [our.bol %hall] ~]~
(send-chat-update [%inbox newinbox])
%= this
src.inbox.str.sta (~(del in src.inbox.str.sta) src.dif.sto)
::
configs.str.sta (~(del by configs.str.sta) affectedcir)
==
==
:: end of branching on dif.sto type
==
:: end of branching on sto type
==
:: end of i.wir branching
::
:: +lient arms
::
::
:: +bound: lient tells us we successfully bound our server to the ~chat url
::
++ bound
|= [wir=wire success=? binding=binding:http-server]
^- (quip move _this)
[~ this]
::
:: +poke-handle-http-request: serve pages from file system based on URl path
::
++ poke-handle-http-request
%- (require-authorization:app ost.bol move this)
|= =inbound-request:http-server
^- (quip move _this)
::
=+ request-line=(parse-request-line url.request.inbound-request)
=/ name=@t
=+ back-path=(flop site.request-line)
?~ back-path
''
i.back-path
?+ site.request-line
:_ this
[ost.bol %http-response not-found:app]~
::
:: styling
::
[%'~chat' %css %index ~]
:_ this
[ost.bol %http-response (css-response:app style)]~
::
:: javascript
::
[%'~chat' %js %index ~]
:_ this
[ost.bol %http-response (js-response:app script)]~
::
:: images
::
[%'~chat' %img *]
=/ img (as-octs:mimes:html (~(got by chat-png) `@ta`name))
:_ this
[ost.bol %http-response (png-response:app img)]~
::
:: inbox page
::
[%'~chat' *]
:_ this
[ost.bol %http-response (html-response:app index)]~
==
::
::
:: +subscription-retry arms
::
::
:: +reap: recieve acknowledgement for peer, retry on failure
::
++ reap
|= [wir=wire err=(unit tang)]
^- (quip move _this)
?~ err
[~ this]
?~ wir
(mean [leaf+"invalid wire for diff: {(spud wir)}"]~)
?+ i.wir
(mean [leaf+"invalid wire for diff: {(spud wir)}"]~)
::
%circle
:_ this
[ost.bol %peer wir [our.bol %hall] wir]~
::
%circles
:_ this
[ost.bol %peer wir [our.bol %hall] wir]~
==
::
:: +quit: subscription failed/quit at some point, retry
::
++ quit
|= wir=wire
^- (quip move _this)
?~ wir
(mean [leaf+"invalid wire for diff: {(spud wir)}"]~)
?+ i.wir
(mean [leaf+"invalid wire for diff: {(spud wir)}"]~)
::
%circle
:_ this
[ost.bol %peer wir [our.bol %hall] wir]~
::
%circles
:_ this
[ost.bol %peer wir [our.bol %hall] wir]~
==
::
--

File diff suppressed because one or more lines are too long

Binary file not shown.

After

Width:  |  Height:  |  Size: 1010 B

View File

@ -0,0 +1,16 @@
<!doctype html>
<html>
<head>
<title>Chat</title>
<meta charset="utf-8" />
<meta name="viewport"
content="width=device-width, initial-scale=1, shrink-to-fit=no"/>
<link rel="stylesheet" href="/~chat/css/index.css" />
</head>
<body>
<div id="root" />
<script src="/~/channel/channel.js"></script>
<script src="/~modulo/session.js"></script>
<script src="/~chat/js/index.js"></script>
</body>
</html>

File diff suppressed because one or more lines are too long

60
chat/urbit/lib/chat.hoon Normal file
View File

@ -0,0 +1,60 @@
/- hall
|%
::
+$ move [bone card]
::
+$ card
$% [%http-response =http-event:http]
[%connect wire binding:http-server term]
[%peer wire dock path]
[%quit ~]
[%poke wire dock poke]
[%peer wire dock path]
[%pull wire dock ~]
[%diff diff]
==
::
+$ diff
$% [%hall-rumor rumor:hall]
[%chat-streams streams]
[%chat-update update]
==
::
+$ poke
$% [%hall-action action:hall]
==
::
+$ state
$% [%0 str=streams]
==
::
+$ streams
$: :: inbox config
::
inbox=config:hall
:: names and configs of all circles we know about
::
configs=(map circle:hall (unit config:hall))
:: messages for all circles we know about
::
messages=(map circle:hall (list envelope:hall))
::
::
circles=(set name:hall)
::
::
peers=(map circle:hall (set @p))
==
::
+$ update
$% [%inbox con=config:hall]
[%message cir=circle:hall env=envelope:hall]
[%config cir=circle:hall con=config:hall]
[%circles cir=(set name:hall)]
[%peers cir=circle:hall per=(set @p)]
==
::
+$ action [%actions lis=(list action:hall)]
::
--
::

View File

@ -0,0 +1,58 @@
::
::
/- hall
/+ chat, hall-json
::
|_ act=action:chat
++ grow
|%
++ tank !!
--
::
++ grab
|%
++ noun streams:chat
++ json
|= jon=^json
=< (parse-chat-action jon)
|%
::
++ hall-action
=, dejs:hall-json
=, dejs-soft:format
|= a=^json
^- action:hall
=- (need ((of -) a))
:~ create+(ot nom+so des+so sec+secu ~)
design+(ot nom+so cof+conf ~)
delete+(ot nom+so why+(mu so) ~)
depict+(ot nom+so des+so ~)
filter+(ot nom+so fit+filt ~)
permit+(ot nom+so inv+bo sis+(as (su fed:ag)) ~)
source+(ot nom+so sub+bo srs+(as sorc) ~)
read+(ot nom+so red+ni ~)
usage+(ot nom+so add+bo tas+(as so) ~)
newdm+(ot sis+(as (su fed:ag)) ~)
::
convey+(ar thot)
phrase+(ot aud+audi ses+(ar spec:dejs:hall-json) ~)
::
notify+(ot aud+audi pes+(mu pres) ~)
naming+(ot aud+audi man+huma ~)
::
glyph+(ot gyf+so aud+audi bin+bo ~)
nick+(ot who+(su fed:ag) nic+so ~)
::
public+(ot add+bo cir+circ ~)
==
::
++ parse-chat-action
=, dejs:format
%- of
:~
[%actions (ot lis+(ar hall-action) ~)]
==
::
--
--
--

View File

@ -0,0 +1,47 @@
::
::
/? 309
::
/- hall
/+ chat, hall-json
::
|_ str=streams:chat
++ grow
|%
++ json
=, enjs:format
^- ^json
%+ frond %chat
%- pairs
:~
::
[%inbox (conf:enjs:hall-json inbox.str)]
::
:- %configs
%- pairs
%+ turn ~(tap by configs.str)
|= [cir=circle:hall con=(unit config:hall)]
^- [@t ^json]
:- (crip (circ:en-tape:hall-json cir))
?~(con ~ (conf:enjs:hall-json u.con))
::
:- %messages
%- pairs
%+ turn ~(tap by messages.str)
|= [cir=circle:hall lis=(list envelope:hall)]
^- [@t ^json]
:- (crip (circ:en-tape:hall-json cir))
[%a (turn lis enve:enjs:hall-json)]
::
:- %circles :- %a
%+ turn ~(tap in circles.str)
|= nom=name:hall
[%s nom]
==
--
::
++ grab
|%
++ noun streams:chat
--
--

View File

@ -0,0 +1,63 @@
::
::
/? 309
::
/- hall
/+ chat, hall-json
::
|_ upd=update:chat
++ grow
|%
++ json
=, enjs:format
^- ^json
%+ frond %update
%- pairs
:~
::
:: %inbox
?: =(%inbox -.upd)
?> ?=(%inbox -.upd)
[%inbox (conf:enjs:hall-json con.upd)]
::
:: %message
?: =(%message -.upd)
?> ?=(%message -.upd)
:- %message
%- pairs
:~
[%circle (circ:enjs:hall-json cir.upd)]
[%envelope (enve:enjs:hall-json env.upd)]
==
::
:: %config
?: =(%config -.upd)
?> ?=(%config -.upd)
:- %config
%- pairs
:~
[%circle (circ:enjs:hall-json cir.upd)]
[%config (conf:enjs:hall-json con.upd)]
==
?: =(%circles -.upd)
?> ?=(%circles -.upd)
:- %circles
%- pairs
:~
:- %circles
:- %a
%+ turn ~(tap in cir.upd)
|= nom=name:hall
[%s nom]
==
::
:: %noop
[*@t *^json]
==
--
::
++ grab
|%
++ noun update:chat
--
--

9
launch/.gitignore vendored Normal file
View File

@ -0,0 +1,9 @@
.DS_Store
.urbitrc
node_modules/
dist/
urbit-code/web/landscape/js/*
urbit-code/web/landscape/css/*
*.swp

5
launch/.urbitrc-sample Normal file
View File

@ -0,0 +1,5 @@
module.exports = {
URBIT_PIERS: [
"/Users/bono/urbit/piers/zod/home"
]
};

132
launch/gulpfile.js Normal file
View File

@ -0,0 +1,132 @@
var gulp = require('gulp');
var cssimport = require('gulp-cssimport');
var cssnano = require('gulp-cssnano');
var rollup = require('gulp-better-rollup');
var sucrase = require('@sucrase/gulp-plugin');
var minify = require('gulp-minify');
var exec = require('child_process').exec;
var resolve = require('rollup-plugin-node-resolve');
var commonjs = require('rollup-plugin-commonjs');
var replace = require('rollup-plugin-replace');
var json = require('rollup-plugin-json');
var builtins = require('rollup-plugin-node-builtins');
var rootImport = require('rollup-plugin-root-import');
var globals = require('rollup-plugin-node-globals');
/***
Main config options
***/
var urbitrc = require('./.urbitrc');
/***
End main config options
***/
gulp.task('css-bundle', function() {
return gulp
.src('src/index.css')
.pipe(cssimport())
.pipe(cssnano())
.pipe(gulp.dest('./urbit/app/launch/css'));
});
gulp.task('jsx-transform', function(cb) {
return gulp.src('src/**/*.js')
.pipe(sucrase({
transforms: ['jsx']
}))
.pipe(gulp.dest('dist'));
});
gulp.task('js-imports', function(cb) {
return gulp.src('dist/index.js')
.pipe(rollup({
plugins: [
commonjs({
namedExports: {
'node_modules/react/index.js': [ 'Component' ],
'node_modules/react-is/index.js': [ 'isValidElementType' ],
}
}),
replace({
'process.env.NODE_ENV': JSON.stringify('development')
}),
rootImport({
root: `${__dirname}/dist/js`,
useEntry: 'prepend',
extensions: '.js'
}),
json(),
globals(),
builtins(),
resolve()
]
}, 'umd'))
.on('error', function(e){
console.log(e);
cb();
})
.pipe(gulp.dest('./urbit/app/launch/js/'))
.on('end', cb);
});
gulp.task('js-minify', function () {
return gulp.src('./urbit/app/launch/js/index.js')
.pipe(minify())
.pipe(gulp.dest('./urbit/app/launch/js/'));
});
gulp.task('js-cachebust', function(cb) {
return Promise.resolve(
exec('git log', function (err, stdout, stderr) {
let firstLine = stdout.split("\n")[0];
let commitHash = firstLine.split(' ')[1].substr(0, 10);
let newFilename = "index-" + commitHash + "-min.js";
exec('mv ./urbit/app/launch/js/index-min.js ./urbit/app/launch/js/' + newFilename);
})
);
})
gulp.task('urbit-copy', function () {
let ret = gulp.src('urbit/**/*');
urbitrc.URBIT_PIERS.forEach(function(pier) {
ret = ret.pipe(gulp.dest(pier));
});
return ret;
});
gulp.task('js-bundle-dev', gulp.series('jsx-transform', 'js-imports'));
gulp.task('js-bundle-prod', gulp.series('jsx-transform', 'js-imports', 'js-minify', 'js-cachebust'))
gulp.task('bundle-dev',
gulp.series(
gulp.parallel(
'css-bundle',
'js-bundle-dev'
),
'urbit-copy'
)
);
gulp.task('bundle-prod',
gulp.series(
gulp.parallel(
'css-bundle',
'js-bundle-prod'
),
'urbit-copy'
)
);
gulp.task('default', gulp.series('bundle-dev'));
gulp.task('watch', gulp.series('default', function() {
gulp.watch('src/**/*.js', gulp.parallel('js-bundle-dev'));
gulp.watch('src/**/*.css', gulp.parallel('css-bundle'));
gulp.watch('urbit/**/*', gulp.parallel('urbit-copy'));
}));

7069
launch/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

40
launch/package.json Normal file
View File

@ -0,0 +1,40 @@
{
"name": "urbit-apps",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"author": "",
"license": "ISC",
"devDependencies": {
"@sucrase/gulp-plugin": "^2.0.0",
"gulp": "^4.0.0",
"rollup": "^1.6.0",
"gulp-better-rollup": "^4.0.1",
"gulp-cssimport": "^6.0.0",
"gulp-cssnano": "^2.1.2",
"gulp-minify": "^3.1.0",
"rollup-plugin-commonjs": "^9.2.0",
"rollup-plugin-json": "^2.3.0",
"rollup-plugin-node-builtins": "^2.1.2",
"rollup-plugin-node-globals": "^1.4.0",
"rollup-plugin-node-resolve": "^3.4.0",
"rollup-plugin-replace": "^2.0.0",
"rollup-plugin-root-import": "^0.2.3",
"sucrase": "^3.8.0"
},
"dependencies": {
"classnames": "^2.2.6",
"lodash": "^4.17.11",
"moment": "^2.20.1",
"react": "^16.5.2",
"react-dom": "^16.8.6",
"react-router-dom": "^5.0.0",
"urbit-ob": "^3.1.1"
},
"resolutions": {
"natives": "1.1.3"
}
}

60
launch/src/css/custom.css Normal file
View File

@ -0,0 +1,60 @@
p, h1, h2, h3, h4, h5, h6, a, input, textarea, button {
margin-block-end: unset;
margin-block-start: unset;
font-family: Inter, sans-serif;
}
textarea, select, input, button { outline: none; }
h2 {
font-size: 32px;
line-height: 48px;
font-weight: bold;
}
.body-regular {
font-size: 16px;
line-height: 24px;
font-weight: 600;
}
.body-large {
font-size: 20px;
line-height: 24px;
}
.label-regular {
font-size: 14px;
line-height: 24px;
}
.label-small {
font-size: 12px;
line-height: 24px;
font-weight: bold;
}
.label-small-mono {
font-size: 12px;
line-height: 24px;
font-family: "Source Code Pro", monospace;
}
.body-regular-400 {
font-size: 16px;
line-height: 24px;
font-weight: 400;
}
.plus-font {
font-size: 48px;
line-height: 24px;
}
.fw-bold {
font-weight: bold;
}
.bg-v-light-gray {
background-color: #f9f9f9;
}

63
launch/src/css/fonts.css Normal file
View File

@ -0,0 +1,63 @@
@font-face {
font-family: 'Inter';
font-style: normal;
font-weight: 400;
src: url("https://media.urbit.org/fonts/Inter-Regular.woff2") format("woff2");
}
@font-face {
font-family: 'Inter';
font-style: italic;
font-weight: 400;
src: url("https://media.urbit.org/fonts/Inter-Italic.woff2") format("woff2");
}
@font-face {
font-family: 'Inter';
font-style: normal;
font-weight: 700;
src: url("https://media.urbit.org/fonts/Inter-Bold.woff2") format("woff2");
}
@font-face {
font-family: 'Inter';
font-style: italic;
font-weight: 700;
src: url("https://media.urbit.org/fonts/Inter-BoldItalic.woff2") format("woff2");
}
@font-face {
font-family: "Source Code Pro";
src: url("https://storage.googleapis.com/media.urbit.org/fonts/scp-extralight.woff");
font-weight: 200;
}
@font-face {
font-family: "Source Code Pro";
src: url("https://storage.googleapis.com/media.urbit.org/fonts/scp-light.woff");
font-weight: 300;
}
@font-face {
font-family: "Source Code Pro";
src: url("https://storage.googleapis.com/media.urbit.org/fonts/scp-regular.woff");
font-weight: 400;
}
@font-face {
font-family: "Source Code Pro";
src: url("https://storage.googleapis.com/media.urbit.org/fonts/scp-medium.woff");
font-weight: 500;
}
@font-face {
font-family: "Source Code Pro";
src: url("https://storage.googleapis.com/media.urbit.org/fonts/scp-semibold.woff");
font-weight: 600;
}
@font-face {
font-family: "Source Code Pro";
src: url("https://storage.googleapis.com/media.urbit.org/fonts/scp-bold.woff");
font-weight: 700;
}

File diff suppressed because one or more lines are too long

4
launch/src/index.css Normal file
View File

@ -0,0 +1,4 @@
@import 'css/tachyons.css';
@import 'css/fonts.css';
@import 'css/custom.css';

13
launch/src/index.js Normal file
View File

@ -0,0 +1,13 @@
import React from 'react';
import ReactDOM from 'react-dom';
import { subscription } from "/subscription";
import App from '/app';
subscription.setAuthTokens({
ship: window.ship
});
ReactDOM.render(<App />, document.querySelectorAll("#root")[0]);

38
launch/src/js/app.js Normal file
View File

@ -0,0 +1,38 @@
import React, { Component } from 'react';
import { BrowserRouter, Route } from "react-router-dom";
import { store } from '/store';
import Home from '/components/home';
export default class App extends Component {
constructor() {
super();
this.state = {
};
store.setStateHandler(this.setState.bind(this));
}
render() {
return (
<BrowserRouter>
<div>
<Route exact path="/~home"
render={ (props) => {
return (
<Home
{...props}
data={this.state}
keys={new Set(Object.keys(this.state))}
/>
);
}}
/>
</div>
</BrowserRouter>
);
}
}
window.app = App;

View File

@ -0,0 +1,46 @@
import React, { Component } from 'react';
import { subscription } from '/subscription';
import { api } from '/lib/api';
import classnames from 'classnames';
let style = {
circle: {
width: '2em',
height: '2em',
background: '#000000',
border: '4px solid #333333',
'borderRadius': '2em'
},
triangle: {
width: '0px',
height: '0px',
'borderTop': '8px solid #FFFFFF',
'borderLeft': '8px solid transparent',
'borderRight': '8px solid transparent',
'fontSize': 0,
'lineHeight': 0,
'marginLeft': 'auto',
'marginRight': 'auto',
}
};
export default class Dropdown extends Component {
constructor(props) {
super(props);
}
render() {
return (
<div>
<div className="ml2" style={style.circle}>
<div className="mt2" style={style.triangle}></div>
</div>
</div>
);
}
}

View File

@ -0,0 +1,56 @@
import React, { Component } from 'react';
import { subscription } from '/subscription';
import { api } from '/lib/api';
import classnames from 'classnames';
import moment from 'moment';
import Dropdown from '/components/dropdown';
export default class Header extends Component {
constructor(props) {
super(props);
this.interval = null;
this.timeout = null;
this.state = {
moment: moment()
};
}
componentDidMount() {
let sec = parseInt(moment().format("s"), 10);
this.timeout = setTimeout(() => {
this.setState({
moment: moment()
});
this.interval = setInterval(() => {
this.setState({
moment: moment()
});
}, 60000);
}, (60 - sec) * 1000);
}
componentWillUnmount() {
clearTimeout(this.timeout);
clearInterval(this.interval);
}
render() {
return (
<header className="w-100 h2 cf">
<div className="fl h2 bg-black">
<Dropdown />
</div>
<div className="fr h2 bg-black">
<p className="white v-mid h2 sans-serif dtc pr2">{this.state.moment.format("MMM DD")}</p>
<p className="white v-mid h2 sans-serif dtc pr2">{this.state.moment.format("hh:mm a")}</p>
</div>
</header>
);
}
}

View File

@ -0,0 +1,60 @@
import React, { Component } from 'react';
import { subscription } from '/subscription';
import { api } from '/lib/api';
import classnames from 'classnames';
import Header from '/components/header';
import Tile from '/components/tile';
const loadExternalScript = (ext, callback) => {
const script = document.createElement('script');
script.src = '/~' + ext + '/tile.js';
script.id = ext;
document.body.appendChild(script);
script.onload = () => {
console.log('callback');
if (callback) callback();
};
};
export default class Home extends Component {
constructor(props) {
super(props);
this.loadedScripts = new Set();
subscription.subscribe("/main");
}
componentDidUpdate(prevProps, prevState) {
let difference = new Set(
[...this.props.keys]
.filter(x => !prevProps.keys.has(x))
);
difference.forEach((external) => {
loadExternalScript(external, this.forceUpdate.bind(this));
});
}
render() {
let keys = [...this.props.keys];
let tileElems = keys.map((tile) => {
return (
<Tile key={tile} type={tile} data={this.props.data[tile]} />
);
});
return (
<div className="fl w-100 vh-100 bg-black center">
<Header />
<div className="v-mid pa2 dtc">
{tileElems}
</div>
</div>
);
}
}

View File

@ -0,0 +1,27 @@
import React, { Component } from 'react';
import { subscription } from '/subscription';
import { api } from '/lib/api';
import classnames from 'classnames';
export default class Tile extends Component {
constructor(props) {
super(props);
}
render() {
let SpecificTile = window[this.props.type + 'Tile'];
console.log(this.props.type, this.props.data);
return (
<div className="fl ma2 bg-white overflow-hidden"
style={{ height: '234px', width: '234px' }}>
{ !!SpecificTile ?
<SpecificTile data={this.props.data} />
: <div></div>
}
</div>
);
}
}

37
launch/src/js/lib/api.js Normal file
View File

@ -0,0 +1,37 @@
class Api {
bind(app, path, success, fail, ship) {
window.urb.subscribe(ship, app, path,
(err) => {
fail(err, app, path, ship);
},
(event) => {
success({
data: event,
from: {
app,
ship,
path
}
});
},
(err) => {
fail(err, app, path, ship);
});
}
action(appl, mark, data) {
return new Promise((resolve, reject) => {
window.urb.poke(ship, appl, mark, data,
(json) => {
resolve(json);
},
(err) => {
reject(err);
});
});
}
}
export let api = new Api();
window.api = api;

12
launch/src/js/lib/util.js Normal file
View File

@ -0,0 +1,12 @@
export function daToDate(st) {
var dub = function(n) {
return parseInt(n) < 10 ? "0" + parseInt(n) : n.toString();
};
var da = st.split('..');
var bigEnd = da[0].split('.');
var lilEnd = da[1].split('.');
var ds = `${bigEnd[0].slice(1)}-${dub(bigEnd[1])}-${dub(bigEnd[2])}T${dub(lilEnd[0])}:${dub(lilEnd[1])}:${dub(lilEnd[2])}Z`;
return new Date(ds);
}

20
launch/src/js/store.js Normal file
View File

@ -0,0 +1,20 @@
class Store {
constructor() {
this.state = {};
this.setState = () => {};
}
setStateHandler(setState) {
this.setState = setState;
}
handleEvent(data) {
let json = data.data;
this.setState(json);
}
}
export let store = new Store();
window.store = store;

View File

@ -0,0 +1,44 @@
import _ from 'lodash';
import { api } from '/lib/api';
import { store } from '/store';
export class Subscription {
constructor() {
this.bindPaths = [];
this.authTokens = null;
}
setAuthTokens(authTokens) {
this.authTokens = authTokens;
}
subscribe(path, ship = this.authTokens.ship) {
let bindPaths = _.uniq([...this.bindPaths, path]);
if (bindPaths.length == this.bindPaths.length) {
return;
}
this.bindPaths = bindPaths;
api.bind("home", path,
this.handleEvent.bind(this),
this.handleError.bind(this),
ship);
}
handleEvent(diff) {
store.handleEvent(diff);
}
handleError(err, app, path, ship) {
api.bind(app, path,
this.handleEvent.bind(this),
this.handleError.bind(this),
ship);
}
}
export let subscription = new Subscription();
window.subscription = subscription;

View File

@ -0,0 +1,116 @@
/+ *server, collections
/= index
/^ octs
/; as-octs:mimes:html
/: /===/app/launch/index
/| /html/
/~ ~
==
/= script
/^ octs
/; as-octs:mimes:html
/: /===/app/launch/js/index
/| /js/
/~ ~
==
/= style
/^ octs
/; as-octs:mimes:html
/: /===/app/launch/css/index
/| /css/
/~ ~
==
::
|%
::
+$ move [bone card]
::
+$ card
$% [%http-response =http-event:http]
[%connect wire binding:http-server term]
[%peer wire dock path]
[%diff %json json]
==
+$ tile [name=@tas subscribe=path]
+$ tile-data (map @tas json)
+$ state
$% [%0 tiles=(set tile) data=tile-data path-to-tile=(map path @tas)]
==
::
--
::
|_ [bol=bowl:gall sta=state]
::
++ this .
::
++ prep
|= old=(unit state)
^- (quip move _this)
?~ old
:_ this
[ost.bol %connect / [~ /'~launch'] %launch]~
[~ this(sta u.old)]
::
++ bound
|= [wir=wire success=? binding=binding:http-server]
^- (quip move _this)
[~ this]
::
++ poke-noun
|= [name=@tas subscribe=path]
^- (quip move _this)
:- [ost.bol %peer subscribe [our.bol name] subscribe]~
%= this
tiles.sta (~(put in tiles.sta) [name subscribe])
data.sta (~(put by data.sta) name *json)
path-to-tile.sta (~(put by path-to-tile.sta) subscribe name)
==
::
++ diff-json
|= [pax=path jon=json]
=/ name/@tas (~(got by path-to-tile.sta) pax)
:-
%+ turn (prey:pubsub:userlib /main bol)
|= [=bone *]
[bone %diff %json (frond:enjs:format name jon)]
%= this
data.sta (~(put by data.sta) name jon)
==
::
++ peer-main
|= [pax=path]
^- (quip move _this)
=/ data/json %- pairs:enjs:format ~(tap by data.sta)
:_ this
[ost.bol %diff %json data]~
::
++ poke-handle-http-request
%- (require-authorization:app ost.bol move this)
|= =inbound-request:http-server
^- (quip move _this)
::
=+ request-line=(parse-request-line url.request.inbound-request)
?+ site.request-line
:_ this
[ost.bol %http-response not-found:app]~
::
:: styling
::
[%'~launch' %css %index ~]
:_ this
[ost.bol %http-response (css-response:app style)]~
::
:: javascript
::
[%'~launch' %js %index ~]
:_ this
[ost.bol %http-response (js-response:app script)]~
::
:: inbox page
::
[%'~launch' *]
:_ this
[ost.bol %http-response (html-response:app index)]~
==
::
--

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,16 @@
<!doctype html>
<html>
<head>
<title>Home</title>
<meta charset="utf-8" />
<meta name="viewport"
content="width=device-width, initial-scale=1, shrink-to-fit=no"/>
<link rel="stylesheet" href="/~home/css/index.css" />
</head>
<body>
<div id="root" />
<script src="/~/channel/channel.js"></script>
<script src="/~modulo/session.js"></script>
<script src="/~home/js/index.js"></script>
</body>
</html>

File diff suppressed because it is too large Load Diff

116
modulo/gulpfile.js Normal file
View File

@ -0,0 +1,116 @@
var gulp = require('gulp');
var cssimport = require('gulp-cssimport');
var cssnano = require('gulp-cssnano');
var rollup = require('gulp-better-rollup');
var sucrase = require('@sucrase/gulp-plugin');
var minify = require('gulp-minify');
var resolve = require('rollup-plugin-node-resolve');
var commonjs = require('rollup-plugin-commonjs');
var replace = require('rollup-plugin-replace');
var json = require('rollup-plugin-json');
var builtins = require('rollup-plugin-node-builtins');
var rootImport = require('rollup-plugin-root-import');
var globals = require('rollup-plugin-node-globals');
/***
Main config options
***/
var urbitrc = require('./.urbitrc');
/***
End main config options
***/
gulp.task('css-bundle', function() {
return gulp
.src('src/index-css.css')
.pipe(cssimport())
.pipe(cssnano())
.pipe(gulp.dest('./urbit-code/app/modulo'));
});
gulp.task('jsx-transform', function(cb) {
return gulp.src('src/**/*.js')
.pipe(sucrase({
transforms: ['jsx']
}))
.pipe(gulp.dest('dist'));
});
gulp.task('js-imports', function(cb) {
return gulp.src('dist/index-js.js')
.pipe(rollup({
plugins: [
commonjs({
namedExports: {
'node_modules/react/index.js': [ 'Component' ],
'node_modules/react-is/index.js': [ 'isValidElementType' ],
}
}),
rootImport({
root: `${__dirname}/dist/js`,
useEntry: 'prepend',
extensions: '.js'
}),
json(),
globals(),
builtins(),
resolve()
]
}, 'umd'))
.on('error', function(e){
console.log(e);
cb();
})
.pipe(gulp.dest('./urbit-code/app/modulo/'))
.on('end', cb);
});
gulp.task('js-minify', function () {
return gulp.src('./urbit-code/app/modulo/index-js.js')
.pipe(minify())
.pipe(gulp.dest('./urbit-code/app/modulo/'));
});
gulp.task('urbit-copy', function () {
let ret = gulp.src('urbit-code/**/*');
urbitrc.URBIT_PIERS.forEach(function(pier) {
ret = ret.pipe(gulp.dest(pier));
});
return ret;
});
gulp.task('js-bundle-dev', gulp.series('jsx-transform', 'js-imports'));
gulp.task('js-bundle-prod', gulp.series('jsx-transform', 'js-imports', 'js-minify'))
gulp.task('bundle-dev',
gulp.series(
gulp.parallel(
'css-bundle',
'js-bundle-dev'
),
'urbit-copy'
)
);
gulp.task('bundle-prod',
gulp.series(
gulp.parallel(
'css-bundle',
'js-bundle-prod'
),
'urbit-copy'
)
);
gulp.task('default', gulp.series('bundle-dev'));
gulp.task('watch', gulp.series('default', function() {
gulp.watch('src/**/*.js', gulp.parallel('js-bundle-dev'));
gulp.watch('src/**/*.css', gulp.parallel('css-bundle'));
gulp.watch('urbit-code/**/*', gulp.parallel('urbit-copy'));
}));

12571
modulo/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

41
modulo/package.json Normal file
View File

@ -0,0 +1,41 @@
{
"name": "urbit-apps",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"author": "",
"license": "ISC",
"devDependencies": {
"@sucrase/gulp-plugin": "^2.0.0",
"gulp": "^4.0.0",
"rollup": "^1.6.0",
"gulp-better-rollup": "^4.0.1",
"gulp-cssimport": "^6.0.0",
"gulp-cssnano": "^2.1.2",
"gulp-minify": "^3.1.0",
"rollup-plugin-commonjs": "^9.2.0",
"rollup-plugin-json": "^2.3.0",
"rollup-plugin-node-builtins": "^2.1.2",
"rollup-plugin-node-globals": "^1.4.0",
"rollup-plugin-node-resolve": "^3.4.0",
"rollup-plugin-replace": "^2.0.0",
"rollup-plugin-root-import": "^0.2.3",
"sucrase": "^3.8.0"
},
"dependencies": {
"chess.js": "^0.10.2",
"classnames": "^2.2.6",
"jquery": "^3.4.0",
"lodash": "^4.17.11",
"modulo-wrapper": "file:../modulo-wrapper",
"react": "^16.5.2",
"react-dom": "^16.5.2",
"react-router-dom": "^5.0.0"
},
"resolutions": {
"natives": "1.1.3"
}
}

0
modulo/src/index-css.css Normal file
View File

30
modulo/src/index-js.js Normal file
View File

@ -0,0 +1,30 @@
import React from 'react';
import ReactDOM from 'react-dom';
import Wrapper from '/wrapper';
import { subscription } from "/subscription";
subscription.setAuthTokens({
ship: window.ship
});
var css = document.createElement('link');
css.setAttribute('rel', 'stylesheet');
css.setAttribute('href', '/~chess/chessboard-css.css');
document.head.appendChild(css);
var script = document.createElement('script');
script.type = 'text/javascript';
script.setAttribute('src','/~chess/chessboard-js.js');
script.async = true;
script.onload = () => {
let App = window.app;
ReactDOM.render((
<Wrapper>
<App />
</Wrapper>
), document.querySelectorAll("#root")[0]);
};
document.head.appendChild(script);

36
modulo/src/js/lib/api.js Normal file
View File

@ -0,0 +1,36 @@
class Api {
bind(app, path, success, fail, ship) {
window.urb.subscribe(ship, app, path,
(err) => {
fail(err, app, path, ship);
},
(event) => {
success({
data: event,
from: {
app,
ship,
path
}
});
},
(err) => {
fail(err, app, path, ship);
});
}
action(appl, mark, data) {
return new Promise((resolve, reject) => {
window.urb.poke(ship, appl, mark, data,
(json) => {
resolve(json);
},
(err) => {
reject(err);
});
});
}
}
export let api = new Api();

View File

@ -0,0 +1,42 @@
import _ from 'lodash';
import { api } from '/lib/api';
export class Subscription {
constructor() {
this.bindPaths = [];
this.authTokens = null;
}
setAuthTokens(authTokens) {
this.authTokens = authTokens;
}
subscribe(path, ship = this.authTokens.ship) {
let bindPaths = _.uniq([...this.bindPaths, path]);
if (bindPaths.length == this.bindPaths.length) {
return;
}
this.bindPaths = bindPaths;
api.bind("chess", path,
this.handleEvent.bind(this),
this.handleError.bind(this),
ship);
}
handleEvent(diff) {
console.log(diff);
}
handleError(err, app, path, ship) {
api.bind(app, path,
this.handleEvent.bind(this),
this.handleError.bind(this),
ship);
}
}
export let subscription = new Subscription();

23
modulo/src/js/wrapper.js Normal file
View File

@ -0,0 +1,23 @@
import React, { Component } from 'react';
export default class Wrapper extends Component {
constructor() {
super();
}
render() {
return (
<div>
<div style={{ width:'30%', float: 'left' }}>
<p>Chat</p>
<p>Chess</p>
</div>
<div style={{ width:'70%', float: 'right' }}>
{this.props.children}
</div>
</div>
);
}
}

View File

@ -0,0 +1,212 @@
/- *modulo
/+ *server
/= index
/^ octs
/; as-octs:mimes:html
/: /===/app/modulo/index
/| /html/
/~ ~
==
/= modulo-js
/^ octs
/; as-octs:mimes:html
/: /===/app/modulo/index-js
/| /js/
/~ ~
==
=, format
|%
:: +move: output effect
::
+$ move [bone card]
:: +card: output effect payload
::
+$ card
$% [%connect wire binding:http-server term]
[%serve wire binding:http-server generator:http-server]
[%disconnect wire binding:http-server]
[%http-response =http-event:http]
[%poke wire dock poke]
[%diff %json json]
==
+$ poke
$% [%modulo-bind app=term]
[%modulo-unbind app=term]
==
::
+$ state
$% $: %0
session=(map term @t)
order=(list term)
cur=(unit [term @])
==
==
::
++ session-as-json
|= [cur=(unit [term @]) session=(map term @t) order=(list term)]
^- json
?~ cur
*json
%- pairs:enjs
:~ [%app %s -.u.cur]
[%url %s (~(got by session) -.u.cur)]
:- %list
:- %a
%+ turn order
|= [a=term]
[%s a]
==
::
--
::
|_ [bow=bowl:gall sta=state]
::
++ this .
::
++ prep
|= old=(unit *)
^- (quip move _this)
?~ old
:_ this
[ost.bow %connect / [~ /] %modulo]~
[~ this]
::
:: alerts us that we were bound. we need this because the vane calls back.
::
++ bound
|= [wir=wire success=? binding=binding:http-server]
^- (quip move _this)
[~ this]
::
++ peer-applist
|= [pax=path]
^- (quip move _this)
:_ this
[ost.bow %diff %json (session-as-json cur.sta session.sta order.sta)]~
::
++ session-js
^- octs
:: ?~ cur.sta
:: *octs
%- as-octt:mimes:html
;: weld
:: (trip 'window.onload = function() {')
"window.ship = '{+:(scow %p our.bow)}';"
"window.urb = new Channel();"
==
:: (trip '};')
:: ==
:: " window.state = "
:: (en-json:html (session-as-json cur.sta session.sta order.sta))
:: (trip '}();')
:: %- trip
:: '''
:: document.onkeydown = (event) => {
:: if (!event.metaKey || event.keyCode !== 75) { return; }
:: window.parent.postMessage("commandPalette", "*");
:: };
:: '''
::
:: +poke-handle-http-request: received on a new connection established
::
++ poke-handle-http-request
%- (require-authorization:app ost.bow move this)
|= =inbound-request:http-server
^- (quip move _this)
::
=/ request-line (parse-request-line url.request.inbound-request)
=/ site (flop site.request-line)
?~ site
[[ost.bow %http-response (redirect:app '~landscape')]~ this]
?+ site
[[ost.bow %http-response (html-response:app index)]~ this]
[%session *]
[[ost.bow %http-response (js-response:app session-js)]~ this]
[%script *]
[[ost.bow %http-response (js-response:app modulo-js)]~ this]
==
:: +poke-handle-http-cancel: received when a connection was killed
::
++ poke-handle-http-cancel
|= =inbound-request:http-server
^- (quip move _this)
:: the only long lived connections we keep state about are the stream ones.
::
[~ this]
::
++ poke-modulo-bind
|= bin=term
^- (quip move _this)
=/ url (crip "~{(scow %tas bin)}")
?: (~(has by session.sta) bin)
[~ this]
:- [`move`[ost.bow %connect / [~ /[url]] bin] ~]
%= this
session.sta
(~(put by session.sta) bin url)
::
order.sta
(weld order.sta ~[bin])
::
cur.sta
?~ cur.sta `[bin 0]
cur.sta
==
::
++ poke-modulo-unbind
|= bin=term
^- (quip move _this)
=/ url (crip "~{(scow %tas bin)}")
?. (~(has by session.sta) bin)
[~ this]
=/ ind (need (find ~[bin] order.sta))
=/ neworder (oust [ind 1] order.sta)
:- [`move`[ost.bow %disconnect / [~ /(crip "~{(scow %tas bin)}")]] ~]
%= this
session.sta (~(del by session.sta) bin)
order.sta neworder
cur.sta
::
?: =(1 (lent order.sta))
~
?: (lth ind +:(need cur.sta))
`[-:(need cur.sta) (dec +:(need cur.sta))]
?: =(ind +:(need cur.sta))
`[(snag 0 neworder) 0]
cur.sta
==
::
++ poke-modulo-command
|= com=command
^- (quip move _this)
=/ length (lent order.sta)
?~ cur.sta
[~ this]
?: =(length 1)
[~ this]
=/ new-cur=(unit [term @])
?- -.com
%forward
?: =((dec length) +.u.cur.sta)
`[(snag 0 order.sta) 0]
=/ ind +(+.u.cur.sta)
`[(snag ind order.sta) ind]
%back
?: =(0 +.u.cur.sta)
=/ ind (dec length)
`[(snag ind order.sta) ind]
=/ ind (dec +.u.cur.sta)
`[(snag ind order.sta) ind]
%go
=/ ind (find [app.com]~ order.sta)
?~ ind
cur.sta
`[app.com u.ind]
==
:_ this(cur.sta new-cur)
%+ turn (prey:pubsub:userlib /applist bow)
|= [=bone ^]
[bone %diff %json (session-as-json new-cur session.sta order.sta)]
::
--

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,25 @@
<!doctype html>
<html>
<head>
<script type="application/javascript" src="/~/channel/channel.js"></script>
<script type="application/javascript" src="/session.js"></script>
<style type="text/css">
.command-palette {
position: relative;
background-color: #f6f6f6;
top:10%;
left:10%;
width: 80%;
border: none;
outline: none;
height: 48px;
font-size: 20px;
line-height: 48px;
}
</style>
</head>
<body>
<div id="root"></div>
<script type="application/javascript" src="/script.js"></script>
</body>
</html>

View File

@ -0,0 +1,6 @@
|_ ter=term
++ grab
|%
++ noun term
--
--

View File

@ -0,0 +1,14 @@
/- *modulo
=, format
|_ com=command
++ grab
|%
++ noun command
++ json
%- of:dejs
:~ forward+ul:dejs
back+ul:dejs
go+(su:dejs sym)
==
--
--

View File

@ -0,0 +1,6 @@
|_ ter=term
++ grab
|%
++ noun term
--
--

View File

@ -0,0 +1,7 @@
|%
+$ command
$% [%forward ~]
[%back ~]
[%go app=term]
==
--

5
urbitrc Normal file
View File

@ -0,0 +1,5 @@
module.exports = {
URBIT_PIERS: [
"/Users/logan/Dev/light-urbit/build/zod-chess-9/home",
]
};

123
weather/gulpfile.js Normal file
View File

@ -0,0 +1,123 @@
var gulp = require('gulp');
var cssimport = require('gulp-cssimport');
var cssnano = require('gulp-cssnano');
var rollup = require('gulp-better-rollup');
var sucrase = require('@sucrase/gulp-plugin');
var minify = require('gulp-minify');
var resolve = require('rollup-plugin-node-resolve');
var commonjs = require('rollup-plugin-commonjs');
var replace = require('rollup-plugin-replace');
var json = require('rollup-plugin-json');
var builtins = require('rollup-plugin-node-builtins');
var rootImport = require('rollup-plugin-root-import');
var globals = require('rollup-plugin-node-globals');
/***
Main config options
***/
var urbitrc = require('./.urbitrc');
/***
End main config options
***/
gulp.task('jsx-transform', function(cb) {
return gulp.src('src/**/*.js')
.pipe(sucrase({
transforms: ['jsx']
}))
.pipe(gulp.dest('dist'));
});
gulp.task('tile-jsx-transform', function(cb) {
return gulp.src('tile/**/*.js')
.pipe(sucrase({
transforms: ['jsx']
}))
.pipe(gulp.dest('dist'));
});
gulp.task('js-imports', function(cb) {
return gulp.src('dist/index.js')
.pipe(rollup({
plugins: [
commonjs({
namedExports: {
'node_modules/react/index.js': [ 'Component' ],
'node_modules/react-is/index.js': [ 'isValidElementType' ],
}
}),
rootImport({
root: `${__dirname}/dist/js`,
useEntry: 'prepend',
extensions: '.js'
}),
json(),
globals(),
builtins(),
resolve()
]
}, 'umd'))
.on('error', function(e){
console.log(e);
cb();
})
.pipe(gulp.dest('./urbit/app/weather/js/'))
.on('end', cb);
});
gulp.task('tile-js-imports', function(cb) {
return gulp.src('dist/tile.js')
.pipe(rollup({
plugins: [
commonjs({
namedExports: {
'node_modules/react/index.js': [ 'Component' ],
}
}),
rootImport({
root: `${__dirname}/dist/js`,
useEntry: 'prepend',
extensions: '.js'
}),
json(),
globals(),
builtins(),
resolve()
]
}, 'umd'))
.on('error', function(e){
console.log(e);
cb();
})
.pipe(gulp.dest('./urbit/app/weather/js/'))
.on('end', cb);
});
gulp.task('js-minify', function () {
return gulp.src('./urbit/app/weather/js/index.js')
.pipe(minify())
.pipe(gulp.dest('./urbit/app/weather/js/'));
});
gulp.task('urbit-copy', function () {
let ret = gulp.src('urbit/**/*');
urbitrc.URBIT_PIERS.forEach(function(pier) {
ret = ret.pipe(gulp.dest(pier));
});
return ret;
});
gulp.task('tile-js-bundle-dev', gulp.series('tile-jsx-transform', 'tile-js-imports'));
gulp.task('default', gulp.series('tile-js-bundle-dev', 'urbit-copy'));
gulp.task('watch', gulp.series('default', function() {
gulp.watch('tile/**/*.js', gulp.parallel('tile-js-bundle-dev'));
gulp.watch('urbit/**/*', gulp.parallel('urbit-copy'));
}));

7040
weather/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

38
weather/package.json Normal file
View File

@ -0,0 +1,38 @@
{
"name": "urbit-apps",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"author": "",
"license": "ISC",
"devDependencies": {
"@sucrase/gulp-plugin": "^2.0.0",
"gulp": "^4.0.0",
"rollup": "^1.6.0",
"gulp-better-rollup": "^4.0.1",
"gulp-cssimport": "^6.0.0",
"gulp-cssnano": "^2.1.2",
"gulp-minify": "^3.1.0",
"rollup-plugin-commonjs": "^9.2.0",
"rollup-plugin-json": "^2.3.0",
"rollup-plugin-node-builtins": "^2.1.2",
"rollup-plugin-node-globals": "^1.4.0",
"rollup-plugin-node-resolve": "^3.4.0",
"rollup-plugin-replace": "^2.0.0",
"rollup-plugin-root-import": "^0.2.3",
"sucrase": "^3.8.0"
},
"dependencies": {
"classnames": "^2.2.6",
"moment": "^2.24.0",
"react": "^16.5.2",
"react-dom": "^16.5.2",
"react-router-dom": "^5.0.0"
},
"resolutions": {
"natives": "1.1.3"
}
}

161
weather/tile/tile.js Normal file
View File

@ -0,0 +1,161 @@
import React, { Component } from 'react';
import classnames from 'classnames';
import moment from 'moment';
class IconWithData extends Component {
render() {
const { props } = this;
return (
<div className='mt2'>
<img
src={'/~weather/img/' + props.icon + '.png'}
width={20}
height={20}
className="dib mr2" />
<p className="label-small dib white">{props.text}</p>
</div>
);
}
}
export default class WeatherTile extends Component {
constructor(props) {
super(props);
let ship = window.ship;
let api = window.api;
this.state = {
location: '',
latlng: ''
};
}
locationChange(e) {
this.setState({
location: e.target.value
});
}
firstSubmit() {
if (!this.state.location) {
return;
}
this.askForLatLong(this.state.location)
}
locationSubmit() {
if (!this.state.location) {
return;
}
api.action('weather', 'json', this.state.latlng);
}
askForLatLong(place) {
let url = 'https://maps.googleapis.com/maps/api/geocode/json?address=';
let key = '&key=AIzaSyDawAoOCGSB6nzge6J9yPnnZH2VUFuG24E';
fetch(url + encodeURI(place) + key)
.then((obj) => {
return obj.json();
}).then((json) => {
console.log(json);
if (json && json.results && json.results.length > 0) {
this.setState({
latlng:
json.results[0].geometry.location.lat
+ ','
+ json.results[0].geometry.location.lng
});
}
});
}
renderWrapper(child) {
return (
<div className="pa2 bg-dark-gray" style={{ width: 234, height: 234 }}>
{child}
</div>
);
}
renderNoData() {
return this.renderWrapper((
<div>
<p className="white sans-serif">Weather</p>
<input type="text" onChange={this.locationChange.bind(this)} />
{this.state.latlng}
<button onClick={this.firstSubmit.bind(this)}>Submit</button>
<button onClick={this.locationSubmit.bind(this)}>Go</button>
</div>
));
}
renderWithData(data) {
let c = data.currently;
let d = data.daily.data[0];
let da = moment.unix(d.sunsetTime).format('h:mm a') || '';
return this.renderWrapper((
<div>
<p className="white">Weather</p>
<div className="w-100 mb2 mt2">
<img
src={'/~weather/img/' + c.icon + '.png'}
width={64}
height={64}
className="dib" />
<h2
className="dib ml2 white"
style={{
fontSize: 72,
lineHeight: '64px',
fontWeight: 400
}}>
{Math.round(c.temperature)}°</h2>
</div>
<div className="w-100 cf">
<div className="fl w-50">
<IconWithData
icon='winddirection'
text={c.windBearing + '°'} />
<IconWithData
icon='chancerain'
text={c.precipProbability + '%'} />
<IconWithData
icon='windspeed'
text={Math.round(c.windSpeed) + ' mph'} />
</div>
<div className="fr w-50">
<IconWithData
icon='sunset'
text={da} />
<IconWithData
icon='low'
text={Math.round(d.temperatureLow) + '°'} />
<IconWithData
icon='high'
text={Math.round(d.temperatureHigh) + '°'} />
</div>
</div>
</div>
));
}
render() {
let data = !!this.props.data ? this.props.data : {};
if ('currently' in data && 'daily' in data) {
return this.renderWithData(data);
}
return this.renderNoData();
}
}
window.weatherTile = WeatherTile;

View File

@ -0,0 +1,157 @@
/+ *server
/= tile-js
/^ octs
/; as-octs:mimes:html
/: /===/app/weather/js/tile /js/
/= weather-png
/^ (map knot @)
/: /===/app/weather/img /_ /png/
=, format
::
|%
:: +move: output effect
::
+$ move [bone card]
:: +card: output effect payload
::
+$ card
$% [%poke wire dock poke]
[%http-response =http-event:http]
[%diff %json json]
[%connect wire binding:http-server term]
[%request wire request:http outbound-config:http-client]
[%wait wire @da]
==
+$ poke
$% [%noun [@tas path]]
==
+$ state
$% [%0 data=json time=@da location=@t timer=(unit @da)]
==
--
::
|_ [bol=bowl:gall state]
::
++ this .
::
++ bound
|= [wir=wire success=? binding=binding:http-server]
^- (quip move _this)
[~ this]
::
++ prep
|= old=(unit state)
^- (quip move _this)
=/ lismov/(list move) %+ weld
`(list move)`[ost.bol %connect / [~ /'~weather'] %weather]~
`(list move)`[ost.bol %poke /weather [our.bol %home] [%noun [%weather /tile]]]~
:- lismov
?~ old
this
%= this
data data.u.old
time time.u.old
==
::
++ peer-tile
|= pax=path
^- (quip move _this)
[[ost.bol %diff %json data]~ this]
::
++ poke-json
|= jon=json
^- (quip move _this)
?. ?=(%s -.jon)
[~ this]
=/ str/@t +.jon
=/ req/request:http (request-darksky str)
=/ out *outbound-config:http-client
?~ timer
:- %+ weld
`(list move)`[ost.bol %wait /timer (add now.bol ~d1)]~
`(list move)`[ost.bol %request /[(scot %da now.bol)] req out]~
%= this
location str
timer `(add now.bol ~d1)
==
:- [ost.bol %request /[(scot %da now.bol)] req out]~
%= this
location str
==
::
++ request-darksky
|= location=@t
^- request:http
=/ url/@t
%- crip %+ weld
(trip 'https://api.darksky.net/forecast/634639c10670c7376dc66b6692fe57ca/')
(trip location)
=/ hed [['Accept' 'application/json']]~
[%'GET' url hed *(unit octs)]
::
++ send-tile-diff
|= jon=json
^- (list move)
%+ turn (prey:pubsub:userlib /tile bol)
|= [=bone ^]
[bone %diff %json jon]
::
++ http-response
|= [=wire response=client-response:http-client]
^- (quip move _this)
:: ignore all but %finished
?. ?=(%finished -.response)
[~ this]
=/ data/(unit mime-data:http-client) full-file.response
?~ data
:: data is null
[~ this]
=/ jon/(unit json) (de-json:html q.data.u.data)
?~ jon
[~ this]
?> ?=(%o -.u.jon)
=/ ayyy/json %- pairs:enjs:format :~
currently+(~(got by p.u.jon) 'currently')
daily+(~(got by p.u.jon) 'daily')
==
:- (send-tile-diff ayyy)
%= this
data ayyy
time now.bol
==
::
++ poke-handle-http-request
%- (require-authorization:app ost.bol move this)
|= =inbound-request:http-server
^- (quip move _this)
=+ request-line=(parse-request-line url.request.inbound-request)
=+ back-path=(flop site.request-line)
=/ name=@t
=+ back-path=(flop site.request-line)
?~ back-path
''
i.back-path
::
?~ back-path
:_ this ~
?: =(name 'tile')
[[ost.bol %http-response (js-response:app tile-js)]~ this]
?: (lte (lent back-path) 1)
[[ost.bol %http-response not-found:app]~ this]
?: =(&2:site.request-line 'img')
=/ img (as-octs:mimes:html (~(got by weather-png) `@ta`name))
[[ost.bol %http-response (png-response:app img)]~ this]
[~ this]
::
++ wake
|= [wir=wire ~]
^- (quip move _this)
=/ req/request:http (request-darksky location)
=/ lismov/(list move)
`(list move)`[ost.bol %request /[(scot %da now.bol)] req *outbound-config:http-client]~
?~ timer
:- (weld lismov `(list move)`[ost.bol %wait /timer (add now.bol ~d1)]~)
this(timer `(add now.bol ~d1))
[lismov this]
::
--

Binary file not shown.

After

Width:  |  Height:  |  Size: 549 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 411 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 960 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 897 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 593 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

Some files were not shown because too many files have changed in this diff Show More