Added all modulo apps except publish to interface monorepo.
63
.gitignore
vendored
Normal 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
@ -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
@ -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
42
chat/package.json
Normal 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
@ -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
@ -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;
|
||||
}
|
||||
|
2
chat/src/css/tachyons.css
Normal file
4
chat/src/index.css
Normal file
@ -0,0 +1,4 @@
|
||||
@import 'css/tachyons.css';
|
||||
@import 'css/fonts.css';
|
||||
@import 'css/custom.css';
|
||||
|
16
chat/src/index.js
Normal 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
@ -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;
|
120
chat/src/js/components/chat.js
Normal 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>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
136
chat/src/js/components/lib/chat-input.js
Normal 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>
|
||||
);
|
||||
}
|
||||
}
|
56
chat/src/js/components/lib/chat-tabbar.js
Normal 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>
|
||||
);
|
||||
}
|
||||
}
|
9
chat/src/js/components/lib/icons/icon-send.js
Normal 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} />
|
||||
);
|
||||
}
|
||||
}
|
20
chat/src/js/components/lib/icons/sigil.js
Normal 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>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
46
chat/src/js/components/lib/member-element.js
Normal 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>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
58
chat/src/js/components/lib/message.js
Normal 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>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
106
chat/src/js/components/lib/seal-dict.js
Normal 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 }
|
91
chat/src/js/components/lib/sidebar-invite.js
Normal 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
|
||||
<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>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
29
chat/src/js/components/lib/sidebar-item.js
Normal 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>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
89
chat/src/js/components/member.js
Normal 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>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
120
chat/src/js/components/new.js
Normal 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>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
168
chat/src/js/components/root.js
Normal 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>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
69
chat/src/js/components/settings.js
Normal 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>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
98
chat/src/js/components/sidebar.js
Normal 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>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
19
chat/src/js/components/skeleton.js
Normal 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
@ -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;
|
15
chat/src/js/reducers/chat.js
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
42
chat/src/js/reducers/update.js
Normal 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
@ -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();
|
||||
|
53
chat/src/js/subscription.js
Normal 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
415
chat/urbit/app/chat.hoon
Normal 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]~
|
||||
==
|
||||
::
|
||||
--
|
2
chat/urbit/app/chat/css/index.css
Normal file
BIN
chat/urbit/app/chat/img/Send.png
Normal file
After Width: | Height: | Size: 1010 B |
16
chat/urbit/app/chat/index.html
Normal 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>
|
57686
chat/urbit/app/chat/js/index.js
Normal file
60
chat/urbit/lib/chat.hoon
Normal 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)]
|
||||
::
|
||||
--
|
||||
::
|
58
chat/urbit/mar/chat/action.hoon
Normal 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) ~)]
|
||||
==
|
||||
::
|
||||
--
|
||||
--
|
||||
--
|
47
chat/urbit/mar/chat/streams.hoon
Normal 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
|
||||
--
|
||||
--
|
63
chat/urbit/mar/chat/update.hoon
Normal 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
@ -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
@ -0,0 +1,5 @@
|
||||
module.exports = {
|
||||
URBIT_PIERS: [
|
||||
"/Users/bono/urbit/piers/zod/home"
|
||||
]
|
||||
};
|
132
launch/gulpfile.js
Normal 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
40
launch/package.json
Normal 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
@ -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
@ -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;
|
||||
}
|
||||
|
2
launch/src/css/tachyons.css
Normal file
4
launch/src/index.css
Normal file
@ -0,0 +1,4 @@
|
||||
@import 'css/tachyons.css';
|
||||
@import 'css/fonts.css';
|
||||
@import 'css/custom.css';
|
||||
|
13
launch/src/index.js
Normal 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
@ -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;
|
46
launch/src/js/components/dropdown.js
Normal 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>
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
56
launch/src/js/components/header.js
Normal 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>
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
|
60
launch/src/js/components/home.js
Normal 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>
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
|
27
launch/src/js/components/tile.js
Normal 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
@ -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
@ -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
@ -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;
|
44
launch/src/js/subscription.js
Normal 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;
|
116
launch/urbit/app/launch.hoon
Normal 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)]~
|
||||
==
|
||||
::
|
||||
--
|
2
launch/urbit/app/launch/css/index.css
Normal file
16
launch/urbit/app/launch/index.html
Normal 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>
|
49238
launch/urbit/app/launch/js/index.js
Normal file
116
modulo/gulpfile.js
Normal 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
41
modulo/package.json
Normal 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
30
modulo/src/index-js.js
Normal 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
@ -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();
|
42
modulo/src/js/subscription.js
Normal 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
@ -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>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
212
modulo/urbit-code/app/modulo.hoon
Normal 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)]
|
||||
::
|
||||
--
|
0
modulo/urbit-code/app/modulo/index-css.css
Normal file
1
modulo/urbit-code/app/modulo/index-js-min.js
vendored
Normal file
39281
modulo/urbit-code/app/modulo/index-js.js
Normal file
25
modulo/urbit-code/app/modulo/index.html
Normal 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>
|
6
modulo/urbit-code/mar/modulo/bind.hoon
Normal file
@ -0,0 +1,6 @@
|
||||
|_ ter=term
|
||||
++ grab
|
||||
|%
|
||||
++ noun term
|
||||
--
|
||||
--
|
14
modulo/urbit-code/mar/modulo/command.hoon
Normal 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)
|
||||
==
|
||||
--
|
||||
--
|
6
modulo/urbit-code/mar/modulo/unbind.hoon
Normal file
@ -0,0 +1,6 @@
|
||||
|_ ter=term
|
||||
++ grab
|
||||
|%
|
||||
++ noun term
|
||||
--
|
||||
--
|
7
modulo/urbit-code/sur/modulo.hoon
Normal file
@ -0,0 +1,7 @@
|
||||
|%
|
||||
+$ command
|
||||
$% [%forward ~]
|
||||
[%back ~]
|
||||
[%go app=term]
|
||||
==
|
||||
--
|
5
urbitrc
Normal 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
@ -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
38
weather/package.json
Normal 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
@ -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;
|
157
weather/urbit/app/weather.hoon
Normal 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]
|
||||
::
|
||||
--
|
BIN
weather/urbit/app/weather/img/chancerain.png
Normal file
After Width: | Height: | Size: 549 B |
BIN
weather/urbit/app/weather/img/clear-day.png
Normal file
After Width: | Height: | Size: 2.1 KiB |
BIN
weather/urbit/app/weather/img/clear-night.png
Normal file
After Width: | Height: | Size: 2.1 KiB |
BIN
weather/urbit/app/weather/img/cloudy.png
Normal file
After Width: | Height: | Size: 1.4 KiB |
BIN
weather/urbit/app/weather/img/fog.png
Normal file
After Width: | Height: | Size: 411 B |
BIN
weather/urbit/app/weather/img/high.png
Normal file
After Width: | Height: | Size: 960 B |
BIN
weather/urbit/app/weather/img/low.png
Normal file
After Width: | Height: | Size: 897 B |
BIN
weather/urbit/app/weather/img/partly-cloudy-day.png
Normal file
After Width: | Height: | Size: 2.3 KiB |
BIN
weather/urbit/app/weather/img/partly-cloudy-night.png
Normal file
After Width: | Height: | Size: 1.8 KiB |
BIN
weather/urbit/app/weather/img/rain.png
Normal file
After Width: | Height: | Size: 1.9 KiB |
BIN
weather/urbit/app/weather/img/sleet.png
Normal file
After Width: | Height: | Size: 593 B |
BIN
weather/urbit/app/weather/img/snow.png
Normal file
After Width: | Height: | Size: 1.5 KiB |