Add 'pkg/interface/' from commit 'ae564f567fccff9413b63158ee821b36652d6b53'

git-subtree-dir: pkg/interface
git-subtree-mainline: 7ce50ad75e
git-subtree-split: ae564f567f
This commit is contained in:
Isaac Visintainer 2019-07-23 12:49:05 -07:00
commit f293a76db5
192 changed files with 138469 additions and 0 deletions

84
pkg/interface/.gitignore vendored Normal file
View File

@ -0,0 +1,84 @@
#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
apps/*/dist/
*/dist/
*/node_modules/
*/.DS_Store
.urbitrc
# vim swap files
*.swo
*.swp
*.swn
# built js and css files
apps/publish/urbit/app/publish/js/*
apps/publish/urbit/app/publish/css/*
apps/chat/urbit/app/chat/js/*
apps/chat/urbit/app/chat/css/*
apps/clock/urbit/app/clock/js/*
apps/launch/urbit/app/launch/js/*
apps/timer/urbit/app/timer/js/*
apps/weather/urbit/app/weather/js/*

21
pkg/interface/LICENSE Normal file
View File

@ -0,0 +1,21 @@
MIT License
Copyright (c) 2019 urbit
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

25
pkg/interface/README.md Normal file
View File

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

View File

@ -0,0 +1,181 @@
var gulp = require('gulp');
var cssimport = require('gulp-cssimport');
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('@joseph184/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(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('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' ],
}
}),
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('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/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('tile-js-minify', function () {
return gulp.src('./urbit/app/chat/js/tile.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('tile-js-bundle-dev', gulp.series('tile-jsx-transform', 'tile-js-imports'));
gulp.task('js-bundle-prod', gulp.series('jsx-transform', 'js-imports', 'js-minify'))
gulp.task('tile-js-bundle-prod',
gulp.series('tile-jsx-transform', 'tile-js-imports', 'tile-js-minify'));
gulp.task('bundle-dev',
gulp.series(
gulp.parallel(
'css-bundle',
'js-bundle-dev',
'tile-js-bundle-dev'
),
'urbit-copy'
)
);
gulp.task('bundle-prod',
gulp.series(
gulp.parallel(
'css-bundle',
'js-bundle-prod',
'tile-js-bundle-prod',
),
'urbit-copy'
)
);
gulp.task('default', gulp.series('bundle-dev'));
gulp.task('watch', gulp.series('default', function() {
gulp.watch('tile/**/*.js', gulp.parallel('tile-js-bundle-dev'));
gulp.watch('src/**/*.js', gulp.parallel('js-bundle-dev'));
gulp.watch('src/**/*.css', gulp.parallel('css-bundle'));
gulp.watch('urbit/**/*', gulp.parallel('urbit-copy'));
}));

6796
pkg/interface/apps/chat/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,45 @@
{
"name": "urbit-apps",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"author": "",
"license": "ISC",
"devDependencies": {
"@joseph184/rollup-plugin-node-builtins": "^2.1.4",
"@sucrase/gulp-plugin": "^2.0.0",
"autoprefixer": "^9.6.1",
"cssnano": "^4.1.10",
"gulp": "^4.0.0",
"gulp-better-rollup": "^4.0.1",
"gulp-cssimport": "^7.0.0",
"gulp-minify": "^3.1.0",
"gulp-postcss": "^8.0.0",
"rollup": "^1.6.0",
"rollup-plugin-commonjs": "^9.2.0",
"rollup-plugin-json": "^4.0.0",
"rollup-plugin-node-globals": "^1.4.0",
"rollup-plugin-node-resolve": "^4.0.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-dom": "^16.8.6",
"react-router-dom": "^5.0.0",
"rollup-plugin-postcss": "^2.0.3",
"urbit-ob": "^3.1.1",
"urbit-sigil-js": "^1.3.2"
},
"resolutions": {
"natives": "1.1.3"
}
}

View File

@ -0,0 +1,120 @@
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: #fff;
}
a {
color: #000 !important;
font-weight: 400 !important;
}
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;
}
.body-medium {
font-size: 16px;
line-height: 19px;
}
.body-small {
font-size: 12px;
line-height: 16px;
color: #7f7f7f;
}
.label-regular {
font-size: 14px;
line-height: 24px;
}
.label-small {
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: 32px;
line-height: 24px;
}
.btn-font {
font-size: 14px;
line-height: 16px;
font-weight: 600 !important;
}
.fw-normal {
font-weight: 400;
}
.fw-bold {
font-weight: bold;
}
.bg-v-light-gray {
background-color: #f9f9f9;
}
.nice-green {
color: #2AA779 !important;
}
.bg-nice-green {
background: #2ED196;
}
.nice-red {
color: #EE5432 !important;
}
.inter {
font-family: Inter, sans-serif;
}
.clamp-3 {
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
}
.lh-16 {
line-height: 16px;
}

View File

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

View File

@ -0,0 +1,38 @@
.spinner-pending {
position: relative;
content: "";
border-radius: 100%;
height: 16px;
width: 16px;
background-color: rgba(255,255,255,1);
}
.spinner-pending::after {
content: "";
background-color: rgba(128,128,128,1);
width: 16px;
height: 16px;
position: absolute;
border-radius: 100%;
clip: rect(0, 16px, 16px, 8px);
animation: spin 1s cubic-bezier(0.745, 0.045, 0.355, 1.000) infinite;
}
@keyframes spin {
0% {transform:rotate(0deg)}
25% {transform:rotate(90deg)}
50% {transform:rotate(180deg)}
75% {transform:rotate(270deg)}
100% {transform:rotate(360deg)}
}
.spinner-nostart {
width: 8px;
height: 8px;
border-radius: 100%;
content:'';
background-color: black;
}

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,5 @@
@import "css/tachyons.css";
@import "css/fonts.css";
@import "css/spinner.css";
@import "css/custom.css";

View File

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

View File

@ -0,0 +1,145 @@
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.subscriptionId = 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(lis) {
this.action("chat", "chat-action", {
actions: {
lis
}
});
}
action(appl, mark, data) {
return new Promise((resolve, reject) => {
window.urb.poke(ship, appl, mark, data,
(json) => {
resolve(json);
},
(err) => {
reject(err);
});
});
}
notify(aud, bool) {
this.hall({
notify: {
aud,
pes: !!bool ? 'hear' : 'gone'
}
});
}
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
});
}
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
}
})
}
}
export let api = new UrbitApi();
window.api = api;

View File

@ -0,0 +1,229 @@
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';
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,
numPages: 1,
scrollLocked: false,
};
this.hasAskedForMessages = false;
this.onScroll = this.onScroll.bind(this);
this.updateReadInterval = setInterval(
this.updateReadNumber.bind(this),
1000
);
}
componentDidMount() {
this.updateNumPeople();
this.updateReadNumber();
}
componentWillUnmount() {
if (this.updateReadInterval) {
clearInterval(this.updateReadInterval);
this.updateReadInterval = null;
}
}
componentDidUpdate(prevProps, prevState) {
const { props, state } = this;
if (prevProps.match.params.ship !== props.match.params.ship ||
prevProps.match.params.station !== props.match.params.station
) {
console.log('switched circle');
this.hasAskedForMessages = false;
clearInterval(this.updateReadInterval);
this.setState({
station: props.match.params.ship + "/" + props.match.params.station,
circle: props.match.params.station,
host: props.match.params.ship,
numPeople: 0,
scrollLocked: false
}, () => {
this.updateNumPeople();
this.scrollToBottom();
this.updateReadInterval = setInterval(
this.updateReadNumber.bind(this),
1000
);
this.updateReadNumber();
});
} else if (!(state.station in props.configs)) {
props.history.push('/~chat');
}
}
updateReadNumber() {
const { props, state } = this;
let internalCircle = 'hall-internal-' + state.circle;
let internalStation = `~${window.ship}/${internalCircle}`;
let internalConfig = props.configs[internalStation] || false;
let regularConfig = props.configs[state.station] || false;
let config = internalConfig || regularConfig;
let messages = props.messages;
let lastMsgNum = (messages.length > 0) ?
( messages[messages.length - 1].num + 1 ) : 0;
if (config && config.red < lastMsgNum) {
if (internalConfig) {
props.api.read(internalCircle, lastMsgNum);
} else {
props.api.read(state.circle, lastMsgNum);
}
}
}
askForMessages() {
const { props, state } = this;
let messages = props.messages;
if (state.numPages * 50 < props.messages.length - 200 ||
this.hasAskedForMessages) {
return;
}
if (messages.length > 0) {
let end = messages[0].num;
if (end > 0) {
let start = ((end - 400) > 0) ? end - 400 : 0;
this.hasAskedForMessages = true;
console.log('fetching new messages');
props.subscription.fetchMessages(state.station, start, end - 1);
}
}
}
scrollToBottom() {
if (!this.state.scrollLocked && this.scrollElement) {
this.scrollElement.scrollIntoView({ behavior: 'smooth' });
}
}
onScroll(e) {
if (navigator.userAgent.includes('Safari') &&
navigator.userAgent.includes('Chrome')) {
// Google Chrome
if (e.target.scrollTop === 0) {
this.setState({
numPages: this.state.numPages + 1,
scrollLocked: true
}, () => {
this.askForMessages();
});
} else if (
(e.target.scrollHeight - Math.round(e.target.scrollTop)) ===
e.target.clientHeight
) {
this.setState({
numPages: 1,
scrollLocked: false
});
}
} else if (navigator.userAgent.includes('Safari')) {
// Safari
if (e.target.scrollTop === 0) {
this.setState({
numPages: 1,
scrollLocked: false
});
} else if (
(e.target.scrollHeight + Math.round(e.target.scrollTop)) ===
e.target.clientHeight
) {
this.setState({
numPages: this.state.numPages + 1,
scrollLocked: true
}, () => {
this.askForMessages();
});
}
} else {
console.log('Your browser is not supported.');
}
}
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 });
}
}
render() {
const { props, state } = this;
let config = props.configs[state.station] || {};
let messages = props.messages.slice(0);
let lastMsgNum = (messages.length > 0) ?
messages[messages.length - 1].num : 0;
if (messages.length > 50 * state.numPages) {
messages = messages
.slice(messages.length - (50 * state.numPages), messages.length);
}
let chatMessages = messages.reverse().map((msg) => {
return (
<Message
key={msg.gam.uid}
msg={msg.gam} />
);
});
let peers = props.peers[state.station] || [window.ship];
return (
<div key={state.station}
className="h-100 w-100 overflow-hidden flex flex-column">
<div className='pl3 pt2 bb'>
<h2>{state.circle}</h2>
<ChatTabBar {...props}
station={state.station}
numPeers={peers.length} />
</div>
<div
className="overflow-y-scroll pt3 flex flex-column-reverse"
style={{ height: 'calc(100% - 157px)' }}
onScroll={this.onScroll}>
<div ref={ el => { this.scrollElement = el; }}></div>
{chatMessages}
</div>
<ChatInput
api={props.api}
numMsgs={lastMsgNum}
station={state.station}
circle={state.circle}
security={config.con}
placeholder='Message...' />
</div>
)
}
}

View File

@ -0,0 +1,60 @@
import React, { Component } from 'react';
import classnames from 'classnames';
export class LandingScreen extends Component {
constructor(props) {
super(props);
}
componentDidMount() {
const { props } = this;
let station = props.match.params.ship + '/' + props.match.params.station;
if (station in props.configs) {
props.history.push(`/~chat/${station}`);
}
}
componentDidUpdate(prevProps, prevState) {
const { props } = this;
let station = props.match.params.ship + '/' + props.match.params.station;
if (station in props.configs) {
props.history.push(`/~chat/${station}`);
}
}
onClickSubscribe() {
let station = props.match.params.ship + '/' + props.match.params.station;
this.props.api.source(station, true);
this.props.history.push('/~chat');
}
render() {
const { props } = this;
let station = props.match.params.ship + '/' + props.match.params.station;
return (
<div className="h-100 w-100 pt2 overflow-x-hidden flex flex-column">
<div className='pl2 pt2 bb'>
<h2>{station}</h2>
</div>
<div className="pa3 pl2">
<h2 className="body-large">Not Yet Subscribed</h2>
<p className="body-regular-400">
You aren't subscribed to this chat yet.
Subscribe to see its messages and members.
</p>
<br />
<button
onClick={this.onClickSubscribe.bind(this)}
className="label-r"
>Subscribe</button>
</div>
</div>
);
}
}

View File

@ -0,0 +1,178 @@
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 { uuid } from '/lib/util';
export class ChatInput extends Component {
constructor(props) {
super(props);
/*let closure = () => {
let aud, sep;
let wen = Date.now();
let aut = window.ship;
let config = props.configs[props.station];
aud = [props.station];
sep = {
lin: {
msg: Date.now().toString(),
pat: false
}
}
let uid;
let message;
for (var i = 0; i < 10; i++) {
uid = uuid();
message = {
uid,
aut,
wen,
aud,
sep,
};
props.api.hall({
convey: [message]
});
}
setTimeout(closure, 1000);
};
setTimeout(closure, 2000);*/
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() {
const { props, state } = this;
let message = {
uid: uuid(),
aut: window.ship,
wen: Date.now(),
aud: [props.station],
sep: {
lin: {
msg: state.message,
pat: false
}
}
};
props.api.hall(
{
convey: [message]
}
);
this.setState({
message: ""
});
}
readOnlyRender() {
return (
<div className="mt2 pa3 cf flex black bt o-50">
<div className="fl" style={{ flexBasis: 35, height: 40 }}>
<Sigil ship={window.ship} size={32} />
</div>
<div className="fr h-100 flex pa2" style={{ flexGrow: 1, height: 40 }}>
<p>This chat is read only and you cannot post.</p>
</div>
</div>
);
}
render() {
const { props, state } = this;
if (props.security && props.security.sec !== 'channel' &&
!props.security.sis.includes(window.ship)) {
return this.readOnlyRender();
}
return (
<div className="mt2 pa3 cf flex black bt b--black-30">
<div className="fl" style={{
marginTop: 4,
flexBasis: 32,
height: 36
}}>
<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={props.placeholder}
value={state.message}
onChange={this.messageChange}
autoFocus={true}
/>
<div className="pointer" onClick={this.messageSubmit}>
<IconSend />
</div>
</div>
</div>
);
}
}

View File

@ -0,0 +1,59 @@
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';
}
let membersText = this.props.numPeers === 1
? '1 Member' : `${this.props.numPeers} Members`;
return (
<div className="w-100" style={{ height:28 }}>
<div className={"dib h-100" + bbStream} style={{width:'160px'}}>
<Link
className={'no-underline label-regular v-mid ' + strColor}
to={toBaseLink}>Stream</Link>
</div>
<div className={"dib h-100" + bbMembers} style={{width:'160px'}}>
<Link
className={'no-underline label-regular v-mid ' + memColor}
to={toBaseLink + '/members'}>{membersText}</Link>
</div>
<div className={"dib h-100" + bbSettings} style={{width:'160px'}}>
<Link
className={'no-underline label-regular v-mid ' + setColor}
to={toBaseLink + '/settings'}>Settings</Link>
</div>
</div>
);
}
}

View File

@ -0,0 +1,32 @@
import React, { Component } from 'react';
import classnames from 'classnames';
import { IconHome } from '/components/lib/icons/icon-home';
import { IconSpinner } from '/components/lib/icons/icon-spinner';
export class HeaderBar extends Component {
render() {
let spin = (this.props.spinner)
? <div className="absolute"
style={{width: 16, height: 16, top: 16, right: 16}}>
<IconSpinner/>
</div>
: null;
return (
<div className="bg-black w-100 justify-between"
style={{ height: 48, padding: 8}}>
<a className="db"
style={{ background: '#1A1A1A',
borderRadius: 16,
width: 32,
height: 32,
top: 8 }}
href='/'>
<IconHome />
</a>
{spin}
</div>
);
}
}

View File

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

View File

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

View File

@ -0,0 +1,9 @@
import React, { Component } from 'react';
export class IconSpinner extends Component {
render() {
return (
<div className="spinner-pending"></div>
);
}
}

View File

@ -0,0 +1,34 @@
import React, { Component } from 'react';
import { sigil, reactRenderer } from 'urbit-sigil-js';
export class Sigil extends Component {
render() {
const { props } = this;
console.log("sigil ship", props.ship);
if (props.ship.length > 14) {
return (
<div className="bg-black" style={{width: 32, height: 32}}>
</div>
);
} else {
return (
<div
className="bg-black"
style={{ flexBasis: 32 }}>
{
sigil({
patp: props.ship,
renderer: reactRenderer,
size: props.size,
colors: ['black', 'white'],
})
}
</div>
);
}
}
}

View File

@ -0,0 +1,50 @@
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.ship}` === props.host) {
actionElem = (
<p className="dib w-40 underline black label-small-mono label-regular">
Host
</p>
);
} else if (window.ship !== props.ship &&
`~${window.ship}` === props.host) {
actionElem = (
<a onClick={this.onRemove.bind(this)}
className="w-40 dib underline black btn-font">
Remove
</a>
);
} else {
actionElem = (
<span></span>
);
}
return (
<div>
<p
className={
"w-60 dib black pr3 mb2 label-small-mono label-regular"
}>
{props.ship}
</p>
{actionElem}
</div>
);
}
}

View File

@ -0,0 +1,97 @@
import React, { Component } from 'react';
import { Sigil } from '/components/lib/icons/sigil';
import classnames from 'classnames';
import moment from 'moment';
import _ from 'lodash';
export class Message extends Component {
renderMessage(content) {
return (
<p className="body-regular-400 v-top">
{content}
</p>
);
}
renderContent() {
const { props } = this;
let content = _.get(
props.msg,
'sep.lin.msg',
'<unknown message type>'
);
try {
let url = new URL(content);
let imgMatch =
/(jpg|img|png|gif|tiff|jpeg|JPG|IMG|PNG|TIFF|GIF|webp|WEBP|webm|WEBM)$/
.exec(
url.pathname
);
if (imgMatch) {
return (
<img
src={content}
style={{
width:"50%",
maxWidth: '250px'
}}
></img>
)
} else {
let url = this.urlTransmogrifier(content);
return (
<a className="body-regular"
href={url}
target="_blank">{url}</a>
)
}
} catch(e) {
return this.renderMessage(content);
}
}
urlTransmogrifier(url) {
if (typeof url !== 'string') { throw 'Only transmogrify strings!'; }
const ship = window.ship;
if (url.indexOf('arvo://') === 0) {
return `http://${ship}.arvo.network` + url.split('arvo://')[1];
}
return url;
}
render() {
const { props } = this;
let pending = !!props.msg.pending ? ' o-80' : '';
let timestamp = moment.unix(props.msg.wen / 1000).format('hh:mm');
let datestamp = moment.unix(props.msg.wen / 1000).format('LL');
return (
<div className={"w-100 pl3 pr3 pt2 pb2 cf flex" + pending}
style={{
minHeight: 'min-content'
}}>
<div className="fl mr2">
<Sigil ship={props.msg.aut} size={36} />
</div>
<div className="fr" style={{ flexGrow: 1, marginTop: -8 }}>
<div className="hide-child">
<p className="v-top label-small-mono gray dib mr3">
~{props.msg.aut}
</p>
<p className="v-top label-small-mono gray dib">{timestamp}</p>
<p className="v-top label-small-mono ml2 gray dib child">
{datestamp}
</p>
</div>
{this.renderContent()}
</div>
</div>
);
}
}

View File

@ -0,0 +1,120 @@
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, true);
}
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, false);
}
updateInvite(uid, cir, resp) {
let tagstring = resp ? "Accept" : "Reject";
let hostName = cir.split('/')[0];
let circleName = cir.split('/')[1];
let actions = [
{
phrase: {
aud: [`~${window.ship}/i`],
ses: [{
ire: {
top: uid,
sep: {
lin: {
msg: `${tagstring} ${cir}`,
pat: false
}
}
}
}]
}
}
];
if (resp && hostName !== `~${window.ship}`) {
actions = actions.concat([
{
create: {
nom: 'hall-internal-' + circleName,
des: "chatroom",
sec: "channel"
}
},
{
source: {
nom: "inbox",
sub: true,
srs: [cir]
}
},
{
source: {
nom: "inbox",
sub: true,
srs: [`~${window.ship}/hall-internal-${circleName}`]
}
}
]);
}
this.props.api.chat(actions);
}
render() {
const { props } = this;
let cir = _.get(props, 'msg.sep.inv.cir', false);
let aut = _.get(props, 'msg.aut', false);
if (!aut || !cir || !props.config) {
return (
<div></div>
);
}
cir = cir.split('/')[1];
return (
<div className='pa3'>
<div className='w-100 v-mid'>
<div className="dib mr2 bg-nice-green" style={{
borderRadius: 12,
width: 12,
height: 12
}}></div>
<p className="dib body-regular fw-normal">Invite to&nbsp;
<span className='fw-bold'>
{cir}
</span>
</p>
</div>
<div className="w-100">
<p className='dib gray label-small-mono'>Hosted by {aut}</p>
</div>
<a className="dib w-50 pointer btn-font nice-green underline" onClick={this.onAccept.bind(this)}>Accept</a>
<a className="dib w-50 tr pointer btn-font nice-red underline" onClick={this.onReject.bind(this)}>Reject</a>
</div>
)
}
}

View File

@ -0,0 +1,38 @@
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 unreadElem = !!props.unread ? (
<div
className="bg-nice-green dib mr2"
style={{ borderRadius: 6, width: 12, height: 12 }}>
</div>
) : (
<div className="dib"></div>
);
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'>
{unreadElem}
<p className="dib body-regular lh-16">{props.title}</p>
</div>
<div className="w-100">
<p className='dib gray label-small-mono mr3 lh-16'>{props.ship}</p>
<p className='dib gray label-small-mono lh-16'>{props.datetime}</p>
</div>
<p className='label-small gray clamp-3 lh-16 pt1'>{props.description}</p>
</div>
)
}
}

View File

@ -0,0 +1,135 @@
import React, { Component } from 'react';
import classnames from 'classnames';
import urbitOb from 'urbit-ob';
import { deSig } from '/lib/util';
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: '',
error: false,
success: false
};
}
inviteMembers() {
const { props, state } = this;
let sis = state.invMembers.split(',')
.map((mem) => mem.trim())
.map(deSig);
let isValid = true;
sis.forEach((mem) => {
if (!urbitOb.isValidPatp(`~${mem}`)) {
isValid = false;
}
});
if (isValid) {
props.api.permit(state.circle, sis, true);
if (this.textarea) {
this.textarea.value = '';
}
this.setState({
error: false,
success: true,
invMembers: ''
});
} else {
this.setState({ error: true, success: false });
}
}
inviteMembersChange(e) {
this.setState({
invMembers: e.target.value
});
}
render() {
const { props, state } = this;
let peers = props.peers[state.station] || [window.ship];
let listMembers = peers.map((mem) => {
return (
<MemberElement
key={mem}
host={state.host}
ship={mem}
circle={state.circle}
api={props.api} />
);
});
let errorElem = !!this.state.error ? (
<p className="pa2 nice-red label-regular">Invalid ship name.</p>
) : (
<div></div>
);
let successElem = !!this.state.success ? (
<p className="pa2 nice-green label-regular">Sent invites!</p>
) : (
<div></div>
);
let inviteButtonClasses = "label-regular underline black btn-font pointer";
if (!this.state.error) {
inviteButtonClasses = inviteButtonClasses + ' black';
}
return (
<div className="h-100 w-100 overflow-x-hidden flex flex-column">
<div className='pl3 pt2 bb mb3'>
<h2>{state.circle}</h2>
<ChatTabBar
{...props}
station={state.station}
numPeers={peers.length} />
</div>
<div className="w-100 cf">
<div className="w-50 fl pa2">
<p className="body-regular">Permitted Members</p>
<p className="label-regular gray mb3">
Everyone with permission to see 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
ref={ e => { this.textarea = e; } }
className="w-80 db ba overflow-y-hidden gray mb2"
style={{
resize: 'none',
height: 150
}}
onChange={this.inviteMembersChange.bind(this)}></textarea>
<button
onClick={this.inviteMembers.bind(this)}
className={inviteButtonClasses}>
Invite
</button>
{errorElem}
{successElem}
</div>
) : null }
</div>
</div>
)
}
}

View File

@ -0,0 +1,206 @@
import React, { Component } from 'react';
import classnames from 'classnames';
import { uuid, isPatTa, deSig } from '/lib/util';
import urbitOb from 'urbit-ob';
export class NewScreen extends Component {
constructor(props) {
super(props);
this.state = {
idName: '',
invites: '',
idError: false,
inviteError: false
};
this.idChange = this.idChange.bind(this);
this.invChange = this.invChange.bind(this);
}
componentDidUpdate(prevProps, prevState) {
const { props, state } = this;
if (prevProps.circles !== props.circles) {
let station = `~${window.ship}/${state.idName}`;
if (props.circles.includes(station)) {
props.history.push('/~chat/' + station);
}
}
}
idChange(event) {
this.setState({
idName: event.target.value
});
}
invChange(event) {
this.setState({invites: event.target.value});
}
onClickCreate() {
const { props, state } = this;
if (!state.idName) {
this.setState({
idError: true,
inviteError: false
});
return;
}
let station = `~${window.ship}/${state.idName}`;
let actions = [
{
create: {
nom: state.idName,
des: "chatroom",
sec: "village"
}
},
{
source: {
nom: 'inbox',
sub: true,
srs: [station]
}
}
];
if (state.invites.length > 0) {
let aud = state.invites.split(',')
.map((mem) => mem.trim())
.map(deSig);
let isValid = true;
aud.forEach((mem) => {
if (!urbitOb.isValidPatp(`~${mem}`)) {
isValid = false;
}
});
if (isValid) {
actions.push({
permit: {
nom: state.idName,
sis: aud,
inv: true
}
});
actions.push({
phrase: {
aud: aud.map((aud) => `~${aud}/i`),
ses: [{
inv: {
inv: true,
cir: station
}
}]
}
});
if (this.textarea) {
this.textarea.value = '';
}
this.setState({
inviteError: false,
idError: false,
success: true,
invites: ''
}, () => {
props.setSpinner(true);
props.api.chat(actions);
});
} else {
this.setState({
inviteError: true,
idError: false,
success: false
});
}
} else {
this.setState({
error: false,
success: true,
invites: ''
}, () => {
props.setSpinner(true);
props.api.chat(actions);
});
}
}
render() {
let createClasses = "db label-regular mt4 btn-font pointer underline bn";
if (!this.state.idName) {
createClasses = createClasses + ' gray';
}
let idErrElem = (<span />);
if (this.state.idError) {
idErrElem = (
<span className="body-small inter nice-red db">
Chat must have a valid name.
</span>
);
}
let invErrElem = (<span />);
if (this.state.inviteError) {
invErrElem = (
<span className="body-small inter nice-red db">
Invites must be validly formatted ship names.
</span>
);
}
return (
<div className="h-100 w-100 pa3 pt2 overflow-x-hidden flex flex-column">
<h2 className="mb3">Create</h2>
<div className="w-50">
<p className="body-medium db">Chat Name</p>
<p className="body-small db mt2 mb3">
Name this chat. Names must be lowercase and only contain letters, numbers, and dashes.
</p>
<textarea
className="body-regular fw-normal ba pa2 db w-100"
placeholder="secret-chat"
rows={1}
style={{
resize: 'none',
}}
onChange={this.idChange} />
{idErrElem}
<p className="body-medium mt3 db">Invites</p>
<p className="body-small db mt2 mb3">
Invite new participants to this chat.
</p>
<textarea
ref={ e => { this.textarea = e; } }
className="body-regular fw-normal ba pa2 mb2 db w-100"
placeholder="~zod, ~bus"
style={{
resize: 'none',
height: 150
}}
onChange={this.invChange} />
{invErrElem}
<button
onClick={this.onClickCreate.bind(this)}
className={createClasses}
style={{ fontSize: '18px' }}
>-> Create</button>
</div>
</div>
);
}
}

View File

@ -0,0 +1,250 @@
import React, { Component } from 'react';
import { BrowserRouter, Route } from "react-router-dom";
import classnames from 'classnames';
import _ from 'lodash';
import { api } from '/api';
import { subscription } from '/subscription';
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';
import { LandingScreen } from '/components/landing';
export class Root extends Component {
constructor(props) {
super(props);
this.state = store.state;
store.setStateHandler(this.setState.bind(this));
this.setSpinner = this.setSpinner.bind(this);
}
setSpinner(spinner) {
this.setState({
spinner
});
}
render() {
const { props, state } = this;
let configs = !!state.configs ? state.configs : {};
let circles = Object.keys(configs).filter((conf) => {
return !!configs[conf] && conf.split('/')[1] !== 'i';
});
let messages = _.get(state, 'messages', {});
let messagePreviews = {};
Object.keys(messages).forEach((stat) => {
let arr = messages[stat];
if (arr.length === 0) {
messagePreviews[stat] = false;
} else {
messagePreviews[stat] = arr[arr.length - 1];
}
});
let unreads = {};
circles.forEach((cir) => {
if (cir in messages) {
if (messages[cir].length === 0) {
unreads[cir] = false;
} else {
let host = `~${window.ship}`;
let circle = cir.split('/')[1];
let internalStation = host + '/hall-internal-' + circle;
if (internalStation in state.configs) {
unreads[cir] =
state.configs[internalStation].red <=
messages[cir][messages[cir].length - 1].num;
} else {
unreads[cir] =
state.configs[cir].red <=
messages[cir][messages[cir].length - 1].num;
}
}
} else {
unreads[cir] = false;
}
});
let invites = _.get(state, 'messages', {});
if (`~${window.ship}/i` in invites) {
invites = invites[`~${window.ship}/i`];
} else {
invites = [];
}
let inviteConfig = false;
if (`~${window.ship}/i` in configs) {
inviteConfig = configs[`~${window.ship}/i`];
}
return (
<BrowserRouter>
<div>
<Route exact path="/~chat"
render={ (props) => {
return (
<Skeleton
sidebar={
<Sidebar
circles={circles}
messagePreviews={messagePreviews}
invites={invites}
unreads={unreads}
api={api}
inviteConfig={inviteConfig}
{...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">
</div>
</div>
</div>
</Skeleton>
);
}} />
<Route exact path="/~chat/new"
render={ (props) => {
return (
<Skeleton
spinner={this.state.spinner}
sidebar={
<Sidebar
circles={circles}
messagePreviews={messagePreviews}
invites={invites}
unreads={unreads}
api={api}
inviteConfig={inviteConfig}
{...props}
/>
}>
<NewScreen
setSpinner={this.setSpinner}
api={api}
circles={circles}
{...props}
/>
</Skeleton>
);
}} />
<Route exact path="/~chat/join/:ship/:station"
render={ (props) => {
return (
<Skeleton
sidebar={
<Sidebar
circles={circles}
messagePreviews={messagePreviews}
invites={invites}
unreads={unreads}
api={api}
inviteConfig={inviteConfig}
{...props}
/>
}>
<LandingScreen
api={api}
configs={configs}
{...props}
/>
</Skeleton>
);
}} />
<Route exact path="/~chat/:ship/:station"
render={ (props) => {
let station =
props.match.params.ship
+ "/" +
props.match.params.station;
let messages = state.messages[station] || [];
return (
<Skeleton
sidebar={
<Sidebar
circles={circles}
messagePreviews={messagePreviews}
invites={invites}
unreads={unreads}
api={api}
inviteConfig={inviteConfig}
{...props}
/>
}>
<ChatScreen
api={api}
configs={configs}
messages={messages}
peers={state.peers}
subscription={subscription}
{...props}
/>
</Skeleton>
);
}} />
<Route exact path="/~chat/:ship/:station/members"
render={ (props) => {
return (
<Skeleton
sidebar={
<Sidebar
circles={circles}
messagePreviews={messagePreviews}
invites={invites}
unreads={unreads}
api={api}
inviteConfig={inviteConfig}
{...props}
/>
}>
<MemberScreen
{...props}
api={api}
peers={state.peers}
/>
</Skeleton>
);
}} />
<Route exact path="/~chat/:ship/:station/settings"
render={ (props) => {
return (
<Skeleton
spinner={this.state.spinner}
sidebar={
<Sidebar
circles={circles}
messagePreviews={messagePreviews}
invites={invites}
unreads={unreads}
api={api}
inviteConfig={inviteConfig}
{...props}
/>
}>
<SettingsScreen
{...props}
setSpinner={this.setSpinner}
api={api}
peers={state.peers}
circles={state.circles}
/>
</Skeleton>
);
}} />
</div>
</BrowserRouter>
)
}
}

View File

@ -0,0 +1,127 @@
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,
isLoading: false
};
this.renderDelete = this.renderDelete.bind(this);
}
componentDidUpdate(prevProps, prevState) {
const { props, state } = this;
if (!!state.isLoading && !props.circles.includes(state.station)) {
this.setState({
isLoading: false
}, () => {
props.history.push('/~chat');
});
}
}
deleteChat() {
const { props, state } = this;
if (state.host === `~${window.ship}`) {
props.api.delete(state.circle);
} else {
let internalCircle = 'hall-internal-' + state.circle;
props.api.chat([
{
source: {
nom: 'inbox',
sub: false,
srs: [state.station]
}
},
{
delete: {
nom: internalCircle,
why: ''
}
}
]);
}
props.setSpinner(true);
this.setState({
isLoading: true
});
}
renderDelete() {
const { props, state } = this;
let titleText = "Delete Chat";
let descriptionText = "Permanently delete this chat.";
let buttonText = "-> Delete";
if (state.host !== `~${window.ship}`) {
titleText = "Leave Chat"
descriptionText = "Leave this chat."
buttonText = "-> Leave";
}
return (
<div className="w-50 fl pl2 mt3">
<p className="body-regular">{titleText}</p>
<p className="label-regular gray mb3">{descriptionText}</p>
<a onClick={this.deleteChat.bind(this)}
className="pointer btn-font underline nice-red">{buttonText}</a>
</div>
);
}
render() {
const { props, state } = this;
let peers = props.peers[state.station] || [window.ship];
if (!!state.isLoading) {
let text = "Deleting...";
if (state.host === `~${window.ship}`) {
text = "Leaving...";
}
return (
<div className="h-100 w-100 overflow-x-hidden flex flex-column">
<div className='pl3 pt2 bb mb3'>
<h2>{state.circle}</h2>
<ChatTabBar
{...props}
station={state.station}
numPeers={peers.length} />
</div>
<div className="w-100 cf pa3">
<h2>{text}</h2>
</div>
</div>
);
}
return (
<div className="h-100 w-100 overflow-x-hidden flex flex-column">
<div className='pl3 pt2 bb mb3'>
<h2>{state.circle}</h2>
<ChatTabBar
{...props}
station={state.station}
numPeers={peers.length} />
</div>
<div className="w-100 cf pa3">
<h2>Settings</h2>
{this.renderDelete()}
</div>
</div>
)
}
}

View File

@ -0,0 +1,163 @@
import React, { Component } from 'react';
import classnames from 'classnames';
import moment from 'moment';
import _ from 'lodash';
import { SidebarItem } from '/components/lib/sidebar-item';
import { SidebarInvite } from '/components/lib/sidebar-invite';
export class Sidebar extends Component {
constructor(props) {
super(props);
this.state = {
invites: []
};
this.setInvitesToReadInterval = setInterval(
this.setInvitesToRead.bind(this),
1000
);
}
componentDidMount() {
this.filterInvites();
}
componentDidUpdate(prevProps, prevState) {
if (prevProps !== this.props) {
this.filterInvites();
}
}
filterInvites() {
const { props } = this;
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;
}
});
invites = invites.filter((msg) => {
return !(msg.uid in filterInvites);
})
this.setState({ invites });
}
componentWillUnmount() {
if (this.setInvitesToReadInterval) {
clearInterval(this.setInvitesToReadInterval);
this.setInvitesToReadInterval = null;
}
}
setInvitesToRead() {
const { props, state } = this;
if (
props.inviteConfig &&
'red' in props.inviteConfig &&
props.invites.length > 0
) {
let invNum = (props.invites[props.invites.length - 1].num + 1);
if (
props.inviteConfig.red < invNum &&
(invNum - props.inviteConfig.red) > state.invites.length
) {
props.api.read('i', invNum - state.invites.length);
}
}
}
onClickNew() {
this.props.history.push('/~chat/new');
}
render() {
const { props, state } = this;
let station = props.match.params.ship + '/' + props.match.params.station;
let sidebarItems = props.circles
.filter((cir) => {
return !cir.includes('hall-internal-');
})
.map((cir) => {
let msg = props.messagePreviews[cir];
let content = _.get(msg, 'gam.sep.lin.msg', '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,
content,
cir,
title: cir.split('/')[1],
selected: station === cir
};
})
.sort((a, b) => {
return b.wen - a.wen;
})
.map((obj) => {
let unread = props.unreads[obj.cir];
return (
<SidebarItem
key={obj.cir}
title={obj.title}
description={obj.content}
cir={obj.cir}
datetime={obj.datetime}
ship={obj.aut}
selected={obj.selected}
unread={unread}
history={props.history}
/>
);
});
let inviteItems = state.invites.map((inv) => {
return (
<SidebarInvite
key={inv.uid}
msg={inv}
api={props.api}
config={props.inviteConfig}
/>
);
});
return (
<div className="h-100 w-100 overflow-x-hidden flex flex-column">
<div className="pl3 pr3 pt2 pb3 cf bb b--black-30" style={{height: '88px'}}>
<h2 className="dib w-50 gray">Chat</h2>
<a
className="dib tr w-50 pointer plus-font"
onClick={this.onClickNew.bind(this)}>+</a>
</div>
<div className="overflow-y-auto" style={{
height: 'calc(100vh - 60px - 48px)'
}}>
{inviteItems}
{sidebarItems}
</div>
</div>
)
}
}

View File

@ -0,0 +1,28 @@
import React, { Component } from 'react';
import classnames from 'classnames';
import { HeaderBar } from '/components/lib/header-bar.js';
export class Skeleton extends Component {
render() {
return (
<div className="h-100 w-100 absolute">
<HeaderBar spinner={this.props.spinner}/>
<div className="cf w-100 absolute flex"
style={{
height: 'calc(100% - 48px)'
}}>
<div className="fl h-100 br b--black-30 overflow-x-hidden" style={{ flexBasis: 320 }}>
{this.props.sidebar}
</div>
<div className="h-100 fr" style={{
flexGrow: 1,
width: 'calc(100% - 320px)'
}}>
{this.props.children}
</div>
</div>
</div>
);
}
}

View File

@ -0,0 +1,64 @@
import _ from 'lodash';
import classnames from 'classnames';
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;
}
/*
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" : ""}`
  );
}
export function deSig(ship) {
return ship.replace('~', '');
}

View File

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

View File

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

View File

@ -0,0 +1,83 @@
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.reduceMessages(_.get(data, 'messages', false), state);
this.reduceConfig(_.get(data, 'config', false), state);
this.reduceCircles(_.get(data, 'circles', false), state);
this.reducePeers(_.get(data, 'peers', false), state);
this.reduceDelete(_.get(data, 'delete', false), state);
}
}
reduceInbox(inbox, state) {
if (inbox) {
state.inbox = inbox;
}
}
reduceMessage(message, state) {
if (message && message.circle in state.messages) {
state.messages[message.circle].push(message.envelope);
} else {
state.messages[message.circle] = [message.envelope];
}
}
reduceMessages(msgs, state) {
if (msgs) {
let staMsgs = state.messages[msgs.circle];
if (msgs.circle in state.messages) {
console.log('new messages object: ', msgs);
console.log('lowest num in store: ', staMsgs[0].num);
console.log('highest num in store: ', staMsgs[staMsgs.length - 1].num);
if (staMsgs.length > 0 && staMsgs[0].num - 1 === msgs.end) {
state.messages[msgs.circle] = msgs.envelopes.concat(staMsgs);
} else if (staMsgs.length === 0) {
state.messages[msgs.circle] = msgs.envelopes;
} else {
console.error('%messages has inconsistent indices');
}
} else {
state.messages[msgs.circle] = msgs.envelopes;
}
}
}
reduceConfig(config, state) {
if (config) {
state.configs[config.circle] = config.config;
}
}
reduceCircles(circles, state) {
if (circles) {
state.circles = circles.circles;
}
}
reducePeers(peers, state) {
if (peers) {
state.peers[peers.circle] = peers.peers;
}
}
reduceDelete(del, state) {
if (del) {
delete state.configs[del.circle];
delete state.messages[del.circle];
delete state.peers[del.circle];
}
}
}

View File

@ -0,0 +1,40 @@
import { InitialReducer } from '/reducers/initial';
import { ConfigReducer } from '/reducers/config';
import { UpdateReducer } from '/reducers/update';
class Store {
constructor() {
this.state = {
inbox: {},
messages: {},
configs: {},
circles: [],
peers: {},
spinner: false
};
this.initialReducer = new InitialReducer();
this.configReducer = new ConfigReducer();
this.updateReducer = new UpdateReducer();
this.setState = () => {};
}
setStateHandler(setState) {
this.setState = setState;
}
handleEvent(data) {
let json = data.data;
console.log(json);
this.initialReducer.reduce(json, this.state);
this.configReducer.reduce(json, this.state);
this.updateReducer.reduce(json, this.state);
this.setState(this.state);
}
}
export let store = new Store();
window.store = store;

View File

@ -0,0 +1,44 @@
import { api } from '/api';
import { store } from '/store';
import urbitOb from 'urbit-ob';
export class Subscription {
start() {
if (api.authTokens) {
this.initializeChat();
} else {
console.error("~~~ ERROR: Must set api.authTokens before operation ~~~");
}
}
initializeChat() {
api.bind('/primary', 'PUT', api.authTokens.ship, 'chat',
this.handleEvent.bind(this),
this.handleError.bind(this));
}
fetchMessages(circle, start, end) {
fetch(`/~chat/scroll/${circle}/${start}/${end}`)
.then((response) => response.json())
.then((json) => {
store.handleEvent({
data: json
});
});
}
handleEvent(diff) {
store.handleEvent(diff);
}
handleError(err) {
console.error(err);
api.bind('/primary', 'PUT', api.authTokens.ship, 'chat',
this.handleEvent.bind(this),
this.handleError.bind(this));
}
}
export let subscription = new Subscription();

View File

@ -0,0 +1,109 @@
import React, { Component } from 'react';
import classnames from 'classnames';
import _ from 'lodash';
export default class ChatTile extends Component {
render() {
const { props } = this;
let inviteNum = 0;
let msgNum = 0;
let inviteCircle = `~${window.ship}/i`;
let propNumbers = _.get(props, 'data.numbers.chat.numbers', false);
let propConfigs = _.get(props, 'data.config.chat.configs', false);
if (propNumbers && propConfigs) {
let numbers = {};
for (let i = 0; i < propNumbers.length; i++) {
let num = propNumbers[i];
numbers[num.circle] = num.length;
}
let configs = Object.keys(propConfigs);
for (let i = 0; i < configs.length; i++) {
let key = configs[i];
let host = key.split('/')[0];
if (!propConfigs[key]) { break; }
if (!(key in numbers)) { break; }
let red = propConfigs[key].red;
if (key === inviteCircle) {
inviteNum = inviteNum - red + numbers[key];
} else if (host === `~${window.ship}`) {
msgNum = msgNum - red + numbers[key];
} else {
msgNum = msgNum + numbers[key];
}
}
}
let invSuffix = (inviteNum === 1) ? (
<span>Invite</span>
) : (
<span>Invites</span>
);
let numInvElem = (inviteNum > 0) ? (
<p className="absolute white"
style={{
top: 180,
fontWeight: 600,
fontSize: 16,
lineHeight: '20px'
}}>
<span style={{
color: '#2AA779'
}}>{inviteNum} </span>
{invSuffix}
</p>
) : (
<div />
);
let msgSuffix = (msgNum === 1) ? (
<span>New Message</span>
) : (
<span>New Messages</span>
);
let numMsgElem = (msgNum > 0) ? (
<p className="absolute white"
style={{
top: 207,
fontWeight: 600,
fontSize: 16,
lineHeight: '20px'
}}>
<span style={{
color: '#2AA779'
}}>{msgNum} </span>
{msgSuffix}
</p>
) : (
<div />
);
return (
<div className="w-100 h-100 relative" style={{ background: '#1a1a1a' }}>
<a className="w-100 h-100 db pa2 no-underline" href="/~chat">
<p className="gray label-regular b absolute" style={{left: 8, top: 4}}>Chat</p>
<img
className="absolute"
style={{ left: 68, top: 65 }}
src="/~chat/img/Tile.png"
width={106}
height={98} />
{numInvElem}
{numMsgElem}
</a>
</div>
);
}
}
window.chatTile = ChatTile;

View File

@ -0,0 +1,658 @@
/- hall
/+ *server, chat, hall-json
/= index
/^ octs
/; as-octs:mimes:html
/: /===/app/chat/index
/| /html/
/~ ~
==
/= tile-js
/^ octs
/; as-octs:mimes:html
/: /===/app/chat/js/tile
/| /js/
/~ ~
==
/= 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)
=/ launcha/poke
[%launch-action [%chat /chattile '/~chat/js/tile.js']]
?~ old
=/ inboxpat /circle/inbox/config/group
=/ circlespat /circles/[(scot %p our.bol)]
=/ inboxwir /circle/[(scot %p our.bol)]/inbox/config/group
=/ inboxi/poke
:- %hall-action
[%source %inbox %.y (silt [[our.bol %i] ~]~)]
=/ fakeannounce=poke
:- %hall-action
[%create %hall-internal-announcements '' %village]
=/ announce=poke
:- %hall-action
[%create %announcements 'Announcements from Tlon' %journal]
=/ help=poke
:- %hall-action
[%create %urbit-help 'Get help about Urbit' %channel]
=/ dev=poke
:- %hall-action
[%create %urbit-dev 'Chat about developing on Urbit' %channel]
=/ sourcefakeannounce/poke
:- %hall-action
[%source %inbox %.y (silt [[our.bol %hall-internal-announcements] ~]~)]
=/ sourceannounce/poke
:- %hall-action
[%source %inbox %.y (silt [[~marzod %announcements] ~]~)]
=/ hallactions=(list move)
?: =((clan:title our.bol) %czar)
~
?: =(our.bol ~marzod)
~& %marzod-chat
:- [ost.bol %poke /announce [our.bol %hall] announce]
[ost.bol %poke /announce [our.bol %hall] sourceannounce]~
?: =(our.bol ~dopzod)
~& %dopzod-chat
:- [ost.bol %poke /announce [our.bol %hall] dev]
[ost.bol %poke /announce [our.bol %hall] help]~
:- [ost.bol %poke /announce [our.bol %hall] fakeannounce]
:- [ost.bol %poke /announce [our.bol %hall] sourcefakeannounce]
[ost.bol %poke /announce [our.bol %hall] sourceannounce]~
=/ moves=(list move)
:~ [ost.bol %peer inboxwir [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]
[ost.bol %poke /chat [our.bol %launch] launcha]
==
:_ this
%+ weld moves hallactions
:- [ost.bol %poke /chat [our.bol %launch] launcha]~
this(sta u.old)
::
++ construct-tile-json
|= str=streams
^- json
=/ numbers/(list [circle:hall @ud])
%+ turn ~(tap by messages.str)
|= [cir=circle:hall lis=(list envelope:hall)]
^- [circle:hall @ud]
?~ lis
[cir 0]
=/ last (snag (dec (lent lis)) `(list envelope:hall)`lis)
[cir (add num.last 1)]
=/ maptjson=(map @t json)
%- my
:~ ['config' (config-to-json str)]
['numbers' (numbers-to-json numbers)]
==
[%o maptjson]
::
++ peer-chattile
|= wir=wire
^- (quip move _this)
:_ this
[ost.bol %diff %json (construct-tile-json str.sta)]~
::
:: +peer-messages: subscribe to subset of messages and updates
::
::
++ peer-primary
|= wir=wire
^- (quip move _this)
=* messages messages.str.sta
=/ lismov/(list move)
%+ murn ~(tap by messages)
|= [cir=circle:hall lis=(list envelope:hall)]
^- (unit move)
=/ envs/(unit (list envelope:hall)) (~(get by messages) cir)
?~ envs
~
=/ length/@ (lent u.envs)
=/ start/@
?: (gte length 100)
(sub length 100)
0
=/ end/@ length
=/ offset/@ (sub end start)
:- ~
:* ost.bol
%diff
%chat-update
[%messages cir start end (swag [start offset] u.envs)]
==
:_ this
[[ost.bol %diff %chat-config str.sta] lismov]
::
:: +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 str=streams]
^- (list move)
=/ updates/(list move)
%+ turn (prey:pubsub:userlib /primary bol)
|= [=bone *]
[bone %diff %chat-update upd]
::
=/ jon/json (construct-tile-json str)
=/ tile-updates/(list move)
%+ turn (prey:pubsub:userlib /chattile bol)
|= [=bone *]
[bone %diff %json jon]
::
%+ weld
updates
tile-updates
::
::
:: +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)
=/ str %= str.sta
circles cis.piz
==
:- (send-chat-update [[%circles cis.piz] str])
this(str.sta str)
::
:: %circle wire
::
%circle
:: ::
:: :: %circle prize
:: ::
:: %circle
?> ?=(%circle -.piz)
=/ circle/circle:hall [our.bol &3: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))
::
=/ 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 ~]
::
=/ localpeers/(set @p)
%- silt %+ turn ~(tap by loc.pes.piz)
|= [shp=@p stat=status:hall]
shp
::
=/ peers/(map circle:hall (set @p))
%- ~(rep by rem.pes.piz)
|= [[cir=circle:hall grp=group:hall] acc=(map circle:hall (set @p))]
^- (map circle:hall (set @p))
=/ newset
%- silt %+ turn ~(tap by grp)
|= [shp=@p stat=status:hall]
shp
(~(put by acc) cir newset)
::
:-
%+ turn ~(tap in (~(del in (silt circles)) [our.bol %inbox]))
|= cir=circle:hall
^- move
=/ wir/wire /circle/[(scot %p our.bol)]/[nom.cir]/config/group
=/ pat/path /circle/[nom.cir]/config/group
[ost.bol %peer wir [our.bol %hall] pat]
::
%= this
inbox.str.sta loc.cos.piz
configs.str.sta configs
messages.str.sta (molt meslis)
peers.str.sta (~(put by peers) [our.bol %inbox] localpeers)
==
::
:: fill remote configs with message data
::
=* messages messages.str.sta
=/ circle/circle:hall [`@p`(slav %p &2:wir) &3:wir]
=/ localpeers/(set @p)
%- silt %+ turn ~(tap by loc.pes.piz)
|= [shp=@p stat=status:hall]
shp
::
=/ peers/(map circle:hall (set @p))
%- ~(rep by rem.pes.piz)
|= [[cir=circle:hall grp=group:hall] acc=(map circle:hall (set @p))]
^- (map circle:hall (set @p))
=/ newset
%- silt %+ turn ~(tap by grp)
|= [shp=@p stat=status:hall]
shp
(~(put by acc) cir newset)
=/ str
%= str.sta
messages (~(put by messages) circle nes.piz)
peers (~(uni by peers.str.sta) (~(put by peers) circle localpeers))
==
=/ messageupdate/update
:* %messages
circle
0
(lent messages)
nes.piz
==
:- (send-chat-update [messageupdate str])
this(str.sta str)
==
::
:: +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)
=/ cis
?: add.rum
(~(put in circles.str.sta) cir.rum)
(~(del in circles.str.sta) cir.rum)
=/ str
%= str.sta
circles cis
peers
?: add.rum
(~(put by peers.str.sta) [our.bol cir.rum] ~)
(~(del by peers.str.sta) [our.bol cir.rum])
==
:- (send-chat-update [[%circles cis] str])
this(str.sta str)
::
::
:: %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 [`@p`(slav %p &2:wir) &3:wir]
=/ unes/(unit (list envelope:hall)) (~(get by messages) circle)
?~ unes
[~ this]
=/ nes u.unes
=/ str
%= str.sta
messages (~(put by messages) circle (snoc nes nev.sto))
==
:- (send-chat-update [[%message circle nev.sto] str])
this(str.sta str)
::
:: %status:
::
%status
?> ?=(%status -.sto)
=/ upeers/(unit (set @p)) (~(get by peers.str.sta) cir.sto)
?~ upeers
[~ this]
=/ peers/(set @p)
?: =(%remove -.dif.sto)
(~(del in u.upeers) who.sto)
(~(put in u.upeers) who.sto)
=/ str
%= str.sta
peers (~(put by peers.str.sta) cir.sto peers)
==
:- (send-chat-update [[%peers cir.sto peers] str])
this(str.sta str)
::
:: %config: config has changed
::
%config
=* circ cir.sto
::
?+ -.dif.sto
[~ this]
::
:: %full: set all of config without side effects
::
%full
=* conf cof.dif.sto
=/ str
%= str.sta
configs (~(put by configs.str.sta) circ `conf)
==
:- (send-chat-update [[%config circ conf] str])
this(str.sta str)
::
:: %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
==
=/ str
%= str.sta
configs (~(put by configs.str.sta) circ `conf)
==
:- (send-chat-update [[%config circ conf] str])
this(str.sta str)
::
:: %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
=/ newwir/wire
/circle/[(scot %p hos.affectedcir)]/[nom.affectedcir]/grams/0/config/group
=/ pat/path /circle/[nom.affectedcir]/grams/0/config/group
:: 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)
==
=/ str
%= str.sta
inbox newinbox
::
configs
?: (~(has by configs.str.sta) affectedcir)
configs.str.sta
(~(put by configs.str.sta) affectedcir ~)
==
::
:_ this(str.sta str)
%+ weld
[ost.bol %peer newwir [hos.affectedcir %hall] pat]~
(send-chat-update [[%inbox newinbox] str])
::
=/ newinbox %= inbox.str.sta
src (~(del in src.inbox.str.sta) src.dif.sto)
==
:: we've removed a source from our inbox
::
=/ str
%= str.sta
inbox newinbox
::
configs (~(del by configs.str.sta) affectedcir)
messages (~(del by messages.str.sta) affectedcir)
peers (~(del by peers.str.sta) affectedcir)
==
=/ fakecir/circle:hall
:- our.bol
%- crip
%+ weld (trip 'hall-internal-') (trip nom.affectedcir)
::
?~ (~(get by configs.str) fakecir)
:: just forward the delete to our clients
::
:_ this(str.sta str)
%+ weld
[ost.bol %pull newwir [hos.affectedcir %hall] ~]~
%+ weld
(send-chat-update [[%inbox newinbox] str])
(send-chat-update [[%delete affectedcir] str])
:: if we get a delete from another ship, delete our fake circle copy
::
=/ deletefake/poke
:- %hall-action
[%delete nom.fakecir ~]
:_ this(str.sta str)
%+ weld
[ost.bol %pull newwir [hos.affectedcir %hall] ~]~
%+ weld
[ost.bol %poke /fake [our.bol %hall] deletefake]~
%+ weld
(send-chat-update [[%inbox newinbox] str])
(send-chat-update [[%delete affectedcir] str])
::
:: %remove: remove a circle
::
%remove
=/ str
%= str.sta
configs (~(del by configs.str.sta) circ)
messages (~(del by messages.str.sta) circ)
peers (~(del by peers.str.sta) circ)
==
:- (send-chat-update [[%delete circ] str])
this(str.sta str)
::
==
:: 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:eyre]
^- (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:eyre
^- (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
?: =(name 'tile')
[[ost.bol %http-response (js-response:app tile-js)]~ this]
?+ 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)]~
::
:: paginated message data
::
[%'~chat' %scroll @t @t @t @t ~]
=/ cir/circle:hall [(slav %p &3:site.request-line) &4:site.request-line]
=/ start/@ud (need (rush &5:site.request-line dem))
=/ parsedend/@ud (need (rush &6:site.request-line dem))
=* messages messages.str.sta
=/ envs/(unit (list envelope:hall)) (~(get by messages) cir)
?~ envs
[~ this]
?: (gte start (lent u.envs))
[~ this]
=/ end/@
?: (gte parsedend (lent u.envs))
(dec (lent u.envs))
parsedend
=/ offset (sub end start)
=/ jon/json %- msg-to-json
:* %messages
cir
start
end
(swag [start offset] u.envs)
==
:_ this
[ost.bol %http-response (json-response:app (json-to-octs jon))]~
::
::
:: 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
=/ shp/@p (slav %p &2:wir)
=/ pat /circle/[&3:wir]/config/group
?: =(&3:wir 'inbox')
:_ this
[ost.bol %peer wir [shp %hall] pat]~
?: (~(has in src.inbox.str.sta) [[shp &3:wir] ~])
:_ this
[ost.bol %peer wir [shp %hall] pat]~
[~ this]
::
%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
=/ shp/@p (slav %p &2:wir)
=/ pat /circle/[&3:wir]/config/group
?: =(&3:wir 'inbox')
:_ this
[ost.bol %peer wir [shp %hall] pat]~
?: (~(has in src.inbox.str.sta) [[shp &3:wir] ~])
:_ this
[ost.bol %peer wir [shp %hall] pat]~
[~ this]
::
%circles
:_ this
[ost.bol %peer wir [our.bol %hall] wir]~
==
::
--

Binary file not shown.

After

Width:  |  Height:  |  Size: 1010 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

View File

@ -0,0 +1,17 @@
<!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" />
<link rel="icon" type="image/png" href="/~launch/img/Favicon.png">
</head>
<body>
<div id="root" />
<script src="/~/channel/channel.js"></script>
<script src="/~modulo/session.js"></script>
<script src="/~chat/js/index.js"></script>
</body>
</html>

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,140 @@
/- hall
/+ hall-json
|%
::
+$ move [bone card]
::
+$ card
$% [%http-response =http-event:http]
[%connect wire binding:eyre 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-update update]
[%chat-config streams]
[%json json]
==
::
+$ poke
$% [%hall-action action:hall]
[%launch-action [@tas path @t]]
==
::
+$ 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]
[%messages cir=circle:hall start=@ud end=@ud env=(list envelope:hall)]
[%config cir=circle:hall con=config:hall]
[%circles cir=(set name:hall)]
[%peers cir=circle:hall per=(set @p)]
[%delete cir=circle:hall]
==
::
+$ action [%actions lis=(list action:hall)]
::
::
:: +utilities
::
++ msg-to-json
=, enjs:format
|= upd=update
^- json
?> ?=(%messages -.upd)
%+ frond %update
%- pairs
:~
:- %messages
%- pairs
:~
[%circle (circ:enjs:hall-json cir.upd)]
[%start (numb start.upd)]
[%end (numb end.upd)]
[%envelopes [%a (turn env.upd enve:enjs:hall-json)]]
==
==
::
++ config-to-json
|= str=streams
=, 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))
::
:- %circles :- %a
%+ turn ~(tap in circles.str)
|= nom=name:hall
[%s nom]
::
:- %peers
%- pairs
%+ turn ~(tap by peers.str)
|= [cir=circle:hall per=(set @p)]
^- [@t json]
:- (crip (circ:en-tape:hall-json cir))
[%a (turn ~(tap in per) ship)]
::
==
::
++ numbers-to-json
|= num=(list [circle:hall @ud])
^- json
=, enjs:format
%+ frond %chat
%- pairs
:~
::
:: %config
:- %numbers
:- %a
%+ turn num
|= [cir=circle:hall len=@ud]
^- json
%- pairs
:~
[%circle (circ:enjs:hall-json cir)]
[%length (numb len)]
==
==
::
--
::

View File

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

View File

@ -0,0 +1,48 @@
::
::
/? 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))
::
:- %circles :- %a
%+ turn ~(tap in circles.str)
|= nom=name:hall
[%s nom]
::
:- %peers
%- pairs
%+ turn ~(tap by peers.str)
|= [cir=circle:hall per=(set @p)]
^- [@t ^json]
:- (crip (circ:en-tape:hall-json cir))
[%a (turn ~(tap in per) ship)]
::
==
--
::
++ grab
|%
++ noun streams:chat
--
--

View File

@ -0,0 +1,96 @@
::
::
/? 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)]
==
::
:: %messages
?: =(%messages -.upd)
?> ?=(%messages -.upd)
:- %messages
%- pairs
:~
[%circle (circ:enjs:hall-json cir.upd)]
[%start (numb start.upd)]
[%end (numb end.upd)]
[%envelopes [%a (turn env.upd enve:enjs:hall-json)]]
==
::
:: %config
?: =(%config -.upd)
?> ?=(%config -.upd)
:- %config
%- pairs
:~
[%circle (circ:enjs:hall-json cir.upd)]
[%config (conf:enjs:hall-json con.upd)]
==
::
:: %circles
?: =(%circles -.upd)
?> ?=(%circles -.upd)
:- %circles
%- pairs
:~
:- %circles
:- %a
%+ turn ~(tap in cir.upd)
|= nom=name:hall
[%s nom]
==
::
:: %peers
?: =(%peers -.upd)
?> ?=(%peers -.upd)
:- %peers
%- pairs
:~
[%circle (circ:enjs:hall-json cir.upd)]
[%peers [%a (turn ~(tap in per.upd) ship:enjs:format)]]
==
::
:: %delete
?: =(%delete -.upd)
?> ?=(%delete -.upd)
:- %delete
%- pairs
:~
[%circle (circ:enjs:hall-json cir.upd)]
==
::
:: %noop
[*@t *^json]
==
--
::
++ grab
|%
++ noun update:chat
--
--

63
pkg/interface/apps/clock/.gitignore vendored Normal file
View File

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

View File

@ -0,0 +1,21 @@
MIT License
Copyright (c) 2019 urbit
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View File

@ -0,0 +1,10 @@
# Weather tile for Urbit
To install this on your Urbit planet:
1. In your Urbit's Dojo, run |mount %
2. Write in the filepath to your Urbit's pier in the urbitrc-sample file in this repository, then copy it to .urbitrc in this directory.
3. Run `npm install` in terminal in this directory.
4. Run `gulp default` in terminal in this directory.
5. Run |start %weather in your Urbit's Dojo.
To see it, navigate to your Urbit's url and add /~home to the URL path.

View File

@ -0,0 +1,136 @@
var gulp = require('gulp');
var cssimport = require('gulp-cssimport');
var rollup = require('gulp-better-rollup');
var cssnano = require('cssnano');
var autoprefixer = require('autoprefixer');
var postcss = require('gulp-postcss')
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('@joseph184/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/clock/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/clock/js/'))
.on('end', cb);
});
gulp.task('js-minify', function () {
return gulp.src('./urbit/app/clock/js/index.js')
.pipe(minify())
.pipe(gulp.dest('./urbit/app/clock/js/'));
});
gulp.task('tile-js-minify', function () {
return gulp.src('./urbit/app/clock/js/tile.js')
.pipe(minify())
.pipe(gulp.dest('./urbit/app/clock/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('tile-js-bundle-prod',
gulp.series('tile-jsx-transform', 'tile-js-imports', 'tile-js-minify'));
gulp.task('bundle-prod', gulp.series('tile-js-bundle-prod', 'urbit-copy'));
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'));
}));

View File

@ -0,0 +1,44 @@
{
"name": "urbit-apps",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"author": "",
"license": "ISC",
"devDependencies": {
"@joseph184/rollup-plugin-node-builtins": "^2.1.4",
"@sucrase/gulp-plugin": "^2.0.0",
"autoprefixer": "^9.6.1",
"cssnano": "^4.1.10",
"gulp": "^4.0.0",
"gulp-better-rollup": "^4.0.1",
"gulp-cssimport": "^7.0.0",
"gulp-minify": "^3.1.0",
"gulp-postcss": "^8.0.0",
"rollup": "^1.6.0",
"rollup-plugin-commonjs": "^9.2.0",
"rollup-plugin-json": "^2.3.0",
"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"
}
}

View File

@ -0,0 +1,204 @@
import React, { Component } from 'react';
import classnames from 'classnames';
const outerSize = 234; //tile size
const innerSize = 218; //clock size
//polar to cartesian
var ptc = function(r, theta) {
return {
x: r * Math.cos(theta),
y: r * Math.sin(theta)
}
}
class Clock extends Component {
constructor(props) {
super(props);
this.animate = this.animate.bind(this);
this.hourHand = this.hourHand.bind(this);
this.minuteHand = this.minuteHand.bind(this);
this.secondHand = this.secondHand.bind(this);
this.canvasRef = React.createRef();
this.dodecagonImg = null;
this.canvas = null;
}
componentDidMount() {
this.canvas = initCanvas(
this.canvasRef,
{ x: innerSize, y: innerSize },
4
);
this.animate()
}
hourHand(ctx, time) {
//get number of minutes in the day so far, this is so hour hand moves gradually
// rather than in large ticks
var mins = time.getMinutes() + (60 * time.getHours());
//draw the circle thing in the middle
ctx.fillStyle = "black";
ctx.beginPath();
ctx.arc(innerSize/2, innerSize/2, 5, 0, 2 * Math.PI);
ctx.fill();
//draw the actual hour hand
ctx.strokeStyle = "black";
ctx.beginPath();
ctx.moveTo(innerSize/2, innerSize/2);
var angle = (Math.PI/-2.0) + ((2*Math.PI/(12*60)) * mins);
var p = ptc(innerSize*.22, angle);
p.x += innerSize/2;
p.y += innerSize/2;
ctx.lineTo(p.x,p.y);
ctx.stroke();
}
minuteHand(ctx, time) {
//number of seconds in the hour so far
var secs = time.getSeconds() + (60 * time.getMinutes());
ctx.strokeStyle = "black";
ctx.beginPath();
ctx.moveTo(innerSize/2, innerSize/2);
var angle = (Math.PI/-2.0) + ((2*Math.PI/(60*60)) * secs);
var p = ptc(innerSize*.35, angle);
p.x += innerSize/2;
p.y += innerSize/2;
ctx.lineTo(p.x,p.y);
ctx.stroke();
}
secondHand(ctx, time) {
//get number of ms in minute so far
var secs = time.getSeconds();
let middle = {x: innerSize/2, y: innerSize*.75};
//draw the circle thing in the middle
ctx.fillStyle = "red";
ctx.beginPath();
ctx.arc(middle.x, middle.y, 5, 0, 2 * Math.PI);
ctx.fill();
//draw the actual second hand
ctx.strokeStyle = "red";
ctx.beginPath();
var angle = (Math.PI/-2.0) + ((2*Math.PI/(60)) * secs);
var p = ptc(30, angle);
var p2 = ptc(-10, angle ); //starting point is a little bit off center in the opposite direction
p.x += middle.x;
p.y += middle.y;
p2.x += middle.x;
p2.y += middle.y
ctx.moveTo(p2.x, p2.y);
ctx.lineTo(p.x,p.y);
ctx.stroke();
}
animate() {
var time = new Date();
//continuously animate
var c = this.canvas
var ctx = c.getContext("2d");
ctx.clearRect(0, 0, c.width, c.height);
ctx.save();
ctx.translate(0.5, 0.5); //forces antialias thus smoothing out jagged lines
//draw the clock itself
// this.dodecagon(ctx);
//draw the hands
this.secondHand(ctx, time);
this.hourHand(ctx, time);
this.minuteHand(ctx, time);
ctx.restore();
window.requestAnimationFrame(this.animate)
}
render() {
console.log('hi')
return <div style={{position:'relative'}}>
<svg style={{position:'absolute'}} width="218" height="218" viewBox="0 0 234 234" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M112.859 1.10961C115.572 0.38269 118.428 0.38269 121.141 1.10961L171.359 14.5654C174.072 15.2923 176.546 16.7206 178.531 18.7065L215.293 55.4685C217.279 57.4545 218.708 59.9282 219.435 62.6411L232.89 112.859C233.617 115.572 233.617 118.428 232.89 121.141L219.435 171.359C218.708 174.072 217.279 176.546 215.293 178.531L178.531 215.293C176.546 217.279 174.072 218.708 171.359 219.435L121.141 232.89C118.428 233.617 115.572 233.617 112.859 232.89L62.6411 219.435C59.9282 218.708 57.4545 217.279 55.4685 215.293L18.7065 178.531C16.7206 176.546 15.2923 174.072 14.5654 171.359L1.10961 121.141C0.38269 118.428 0.38269 115.572 1.10961 112.859L14.5654 62.6411C15.2923 59.9282 16.7206 57.4545 18.7065 55.4685L55.4685 18.7065C57.4545 16.7206 59.9282 15.2923 62.6411 14.5654L112.859 1.10961Z" fill="white"/>
</svg>
<canvas
style={{position:'absolute'}}
ref={ canvasRef => this.canvasRef = canvasRef }
id="clock-canvas"/>
</div>
}
}
export default class ClockTile extends Component {
constructor(props) {
super(props);
}
renderWrapper(child) {
return (
<div className="pa2" style={{
width: outerSize,
height: outerSize,
background: 'rgba(0,0,0,1)'
}}>
{child}
</div>
);
}
render() {
let data = !!this.props.data ? this.props.data : {};
return this.renderWrapper((
<Clock/>
));
}
}
const loadImg = (base64, cb) => new Promise(resolve => {
const img = new Image();
img.onload = () => resolve(cb(img));
img.onerror = () => reject('Error loading image');
img.src = base64;
});
const initCanvas = (canvas, size, ratio) => {
const { x, y } = size;
let ctx = canvas.getContext('2d');
// let ratio = ctx.webkitBackingStorePixelRatio < 2
// ? window.devicePixelRatio
// : 1;
// default for high print resolution.
// ratio = ratio * resMult;
canvas.width = x * ratio;
canvas.height = y * ratio;
canvas.style.width = x + 'px';
canvas.style.height = y + 'px';
canvas.getContext('2d').scale(ratio, ratio);
return canvas;
}
window.clockTile = ClockTile;

View File

@ -0,0 +1,80 @@
/+ *server
/= tile-js
/^ octs
/; as-octs:mimes:html
/: /===/app/clock/js/tile
/| /js/
/~ ~
==
=, format
::
|%
:: +move: output effect
::
+$ move [bone card]
:: +card: output effect payload
::
+$ poke
$% [%launch-action [@tas path @t]]
==
::
+$ card
$% [%poke wire dock poke]
[%http-response =http-event:http]
[%connect wire binding:eyre term]
[%diff %json json]
==
::
--
::
|_ [bol=bowl:gall ~]
::
++ this .
::
++ bound
|= [wir=wire success=? binding=binding:eyre]
^- (quip move _this)
[~ this]
::
++ prep
|= old=(unit ~)
^- (quip move _this)
=/ launcha
[%launch-action [%clock /tile '/~clock/js/tile.js']]
:_ this
:~
[ost.bol %connect / [~ /'~clock'] %clock]
[ost.bol %poke /clock [our.bol %launch] launcha]
==
::
++ peer-tile
|= pax=path
^- (quip move _this)
[[ost.bol %diff %json *json]~ this]
::
++ send-tile-diff
|= jon=json
^- (list move)
%+ turn (prey:pubsub:userlib /tile bol)
|= [=bone ^]
[bone %diff %json jon]
::
++ poke-handle-http-request
%- (require-authorization:app ost.bol move this)
|= =inbound-request:eyre
^- (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
[[ost.bol %http-response not-found:app]~ this]
?: =(name 'tile')
[[ost.bol %http-response (js-response:app tile-js)]~ this]
[[ost.bol %http-response not-found:app]~ this]
::
--

View File

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

9
pkg/interface/apps/launch/.gitignore vendored Normal file
View File

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

View File

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

View File

@ -0,0 +1,138 @@
var gulp = require('gulp');
var cssimport = require('gulp-cssimport');
var rollup = require('gulp-better-rollup');
var cssnano = require('cssnano');
var autoprefixer = require('autoprefixer');
var postcss = require('gulp-postcss')
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('@joseph184/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() {
let plugins = [
autoprefixer({ browsers: ['last 1 version'] }),
cssnano()
];
return gulp
.src('src/index.css')
.pipe(cssimport())
.pipe(postcss(plugins))
.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'))
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'));
}));

6763
pkg/interface/apps/launch/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,44 @@
{
"name": "urbit-apps",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"author": "",
"license": "ISC",
"devDependencies": {
"@joseph184/rollup-plugin-node-builtins": "^2.1.4",
"@sucrase/gulp-plugin": "^2.0.0",
"autoprefixer": "^9.6.1",
"cssnano": "^4.1.10",
"gulp": "^4.0.0",
"gulp-better-rollup": "^4.0.1",
"gulp-cssimport": "^7.0.0",
"gulp-minify": "^3.1.0",
"gulp-postcss": "^8.0.0",
"rollup": "^1.6.0",
"rollup-plugin-commonjs": "^9.2.0",
"rollup-plugin-json": "^2.3.0",
"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"
}
}

View File

@ -0,0 +1,74 @@
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;
}
a:-webkit-any-link {
color: unset;
}
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;
}
.gray-30 {
color: #B1B2B3;
}
.green-medium {
color: #2ED196;
}

View File

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

File diff suppressed because one or more lines are too long

View File

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

View File

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

View File

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

View File

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

View File

@ -0,0 +1,55 @@
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">
</div>
<div className="fr h2 bg-black">
<p className="white v-mid h2 sans-serif dtc pr2">{this.state.moment.format("MMM DD")}</p>
<p className="white v-mid h2 sans-serif dtc pr2">{this.state.moment.format("hh:mm a")}</p>
</div>
</header>
);
}
}

View File

@ -0,0 +1,36 @@
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';
export default class Home extends Component {
constructor(props) {
super(props);
subscription.subscribe("/main");
}
render() {
let keys = [...this.props.keys];
let tileElems = keys.map((tile) => {
return (
<Tile key={tile} type={tile} data={this.props.data[tile]} />
);
});
return (
<div className="fl w-100 vh-100 bg-black center">
<Header />
<div className="v-mid pa2 dtc">
{tileElems}
</div>
</div>
);
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -0,0 +1,133 @@
/+ *server, launch
/= index
/^ $-(marl manx)
/: /===/app/launch/index /!noun/
/= script
/^ octs
/; as-octs:mimes:html
/: /===/app/launch/js/index
/| /js/
/~ ~
==
/= style
/^ octs
/; as-octs:mimes:html
/: /===/app/launch/css/index
/| /css/
/~ ~
==
/= launch-png
/^ (map knot @)
/: /===/app/launch/img /_ /png/
::
=, launch
::
|_ [bol=bowl:gall sta=state]
::
++ this .
::
++ prep
|= old=(unit state)
^- (quip move _this)
?~ old
:_ this
[ost.bol %connect / [~ /] %launch]~
[~ this(sta u.old)]
::
++ poke-launch-action
|= act=action:launch
^- (quip move _this)
=/ beforedata (~(get by data.sta) name.act)
=/ newdata
?~ beforedata
(~(put by data.sta) name.act [*json url.act])
(~(put by data.sta) name.act [jon.u.beforedata url.act])
:- [ost.bol %peer subscribe.act [our.bol name.act] subscribe.act]~
%= this
tiles.sta (~(put in tiles.sta) [name.act subscribe.act])
data.sta newdata
path-to-tile.sta (~(put by path-to-tile.sta) subscribe.act name.act)
==
::
++ peer-main
|= [pax=path]
^- (quip move _this)
=/ data/json
%- pairs:enjs:format
%+ turn ~(tap by data.sta)
|= [key=@tas [jon=json url=@t]]
[key jon]
:_ this
[ost.bol %diff %json data]~
::
++ diff-json
|= [pax=path jon=json]
^- (quip move _this)
=/ name/@tas (~(got by path-to-tile.sta) pax)
=/ data/(unit [json url=@t]) (~(get by data.sta) name)
?~ data
[~ this]
::
:-
%+ turn (prey:pubsub:userlib /main bol)
|= [=bone *]
[bone %diff %json (frond:enjs:format name jon)]
::
%= this
data.sta (~(put by data.sta) name [jon url.u.data])
==
::
++ generate-script-marl
|= data=tile-data
^- marl
%+ turn ~(tap by data)
|= [key=@tas [jon=json url=@t]]
^- manx
;script@"{(trip url)}";
::
++ poke-handle-http-request
%- (require-authorization:app ost.bol move this)
|= =inbound-request:eyre
^- (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 (flop site.request-line)
?~ site
=/ hym=manx (index (generate-script-marl data.sta))
:_ this
[ost.bol %http-response (manx-response:app hym)]~
?+ 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)]~
::
:: images
::
[%'~launch' %img *]
=/ img (as-octs:mimes:html (~(got by launch-png) `@ta`name))
:_ this
[ost.bol %http-response (png-response:app img)]~
==
::
++ bound
|= [wir=wire success=? binding=binding:eyre]
^- (quip move _this)
[~ this]
::
--

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 255 B

View File

@ -0,0 +1,18 @@
|= scripts=marl
;html
;head
;title: Home
;meta(charset "utf-8");
;meta
=name "viewport"
=content "width=device-width, initial-scale=1, shrink-to-fit=no";
;link(rel "stylesheet", href "/~launch/css/index.css");
==
;body
;div#root;
;script@"/~/channel/channel.js";
;script@"/~modulo/session.js";
;* scripts
;script@"/~launch/js/index.js";
==
==

View File

@ -0,0 +1,24 @@
::
|%
::
+$ move [bone card]
::
+$ card
$% [%http-response =http-event:http]
[%connect wire binding:eyre term]
[%peer wire dock path]
[%diff %json json]
==
::
+$ tile [name=@tas subscribe=path]
::
+$ tile-data (map @tas [jon=json url=@t])
::
+$ action [name=@tas subscribe=path url=@t]
::
+$ state
$% [%0 tiles=(set tile) data=tile-data path-to-tile=(map path @tas)]
==
::
--
::

View File

@ -0,0 +1,11 @@
::
::
/+ launch
::
|_ act=action:launch
::
++ grab
|%
++ noun action:launch
--
--

View File

@ -0,0 +1,189 @@
var gulp = require('gulp');
var cssimport = require('gulp-cssimport');
var rollup = require('gulp-better-rollup');
var cssnano = require('cssnano');
var autoprefixer = require('autoprefixer');
var postcss = require('gulp-postcss')
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('@joseph184/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() {
let plugins = [
autoprefixer({ browsers: ['last 1 version'] }),
cssnano()
];
return gulp
.src('src/index.css')
.pipe(cssimport())
.pipe(postcss(plugins))
.pipe(gulp.dest('./urbit/app/publish/css'));
});
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' ],
}
}),
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/publish/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/publish/js/'))
.on('end', cb);
});
gulp.task('js-minify', function () {
return gulp.src('./urbit/app/publish/js/index.js')
.pipe(minify())
.pipe(gulp.dest('./urbit/app/publish/js/'));
});
gulp.task('tile-js-minify', function () {
return gulp.src('./urbit/app/publish/js/tile.js')
.pipe(minify())
.pipe(gulp.dest('./urbit/app/publish/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/publish/js/index-min.js ./urbit/app/publish/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('tile-js-bundle-dev', gulp.series('tile-jsx-transform', 'tile-js-imports'));
gulp.task('js-bundle-prod', gulp.series('jsx-transform', 'js-imports', 'js-minify'))
gulp.task('tile-js-bundle-prod',
gulp.series('tile-jsx-transform', 'tile-js-imports', 'tile-js-minify'));
gulp.task('bundle-dev',
gulp.series(
gulp.parallel(
'css-bundle',
'js-bundle-dev',
'tile-js-bundle-dev'
),
'urbit-copy'
)
);
gulp.task('bundle-prod',
gulp.series(
gulp.parallel(
'css-bundle',
'js-bundle-prod',
'tile-js-bundle-prod',
),
'urbit-copy'
)
);
gulp.task('default', gulp.series('bundle-dev'));
gulp.task('watch', gulp.series('default', function() {
gulp.watch('tile/**/*.js', gulp.parallel('tile-js-bundle-dev'));
gulp.watch('src/**/*.js', gulp.parallel('js-bundle-dev'));
gulp.watch('src/**/*.css', gulp.parallel('css-bundle'));
gulp.watch('urbit/**/*', gulp.parallel('urbit-copy'));
}));

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,45 @@
{
"name": "urbit-apps",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"author": "",
"license": "ISC",
"devDependencies": {
"@joseph184/rollup-plugin-node-builtins": "^2.1.4",
"@sucrase/gulp-plugin": "^2.0.0",
"autoprefixer": "^9.6.1",
"cssnano": "^4.1.10",
"gulp": "^4.0.0",
"gulp-better-rollup": "^4.0.1",
"gulp-cssimport": "^7.0.0",
"gulp-minify": "^3.1.0",
"gulp-postcss": "^8.0.0",
"rollup": "^1.6.0",
"rollup-plugin-commonjs": "^9.2.0",
"rollup-plugin-json": "^2.3.0",
"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",
"urbit-sigil-js": "^1.3.2"
},
"resolutions": {
"natives": "1.1.3"
}
}

View File

@ -0,0 +1,275 @@
p, h1, h2, h3, h4, h5, h6, a, input, textarea, button {
margin-block-end: unset;
margin-block-start: unset;
font-family: Inter, sans-serif;
}
button {
background: none;
color: inherit;
border: none;
cursor: pointer;
outline: inherit;
padding: 0;
}
p {
font-size: 16px;
line-height: 24px;
}
pre {
padding: 8px;
background-color: #f9f9f9;
}
a {
color: inherit;
text-decoration: inherit;
}
textarea, select, input, button { outline: none; }
h1 {
font-size: 48px;
line-height: 64px;
font-weight: bold;
}
h2 {
font-size: 32px;
line-height: 48px;
font-weight: bold;
}
h3 {
font-size: 24px;
line-height: 32px;
font-weight: bold;
}
h4 {
font-size: 20px;
line-height: 32px;
font-weight: bold;
}
.header-2 {
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-regular-mono {
font-size: 14px;
line-height: 24px;
font-family: "Source Code Pro", monospace;
}
.label-small-mono {
font-size: 12px;
line-height: 24px;
font-family: "Source Code Pro", monospace;
}
.label-small {
font-size: 12px;
line-height: 24px;
}
.label-small-2 {
font-size: 12px;
line-height: 16px;
}
.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;
}
.gray-50 {
color: #7F7F7F;
}
.gray-30 {
color: #B1B2B3;
}
.gray-10 {
color: #E6E6E6;
}
.green {
color: #2AA779;
}
.green-medium {
color: #2ED196;
}
.red {
color: #EE5432;
}
.w-336 {
width: 336px;
}
.w-688 {
width: 688px;
}
.mw-336 {
max-width: 336px;
}
.mw-688 {
max-width: 688px;
}
.w-680 {
width: 680px;
}
.w-16 {
width: 16px;
}
.mb-33 {
width: 33px;
}
.h-80 {
height: 80px;
}
.b-gray-30 {
border-color: #B1B2B3;
}
.header-menu-item {
float: left;
border-bottom-style: solid;
border-bottom-width: 1px;
border-color: #B1B2B3;
color: #B1B2B3;
flex-basis: 148px;
padding-bottom: 3px;
vertical-align: middle;
font-size: 14px;
line-height: 24px;
}
.publish {
float: left;
vertical-align: middle;
font-size: 20px;
line-height: 24px;
font-weight: bold;
color: #7F7F7F;
margin-left: 16px;
margin-top: 16px;
margin-bottom: 8px;
}
.create {
float: right;
font-size: 14px;
line-height: 16px;
font-weight: 600;
text-align: right;
margin-right: 16px;
margin-top: 22px;
}
.path-control {
width: 100%;
border-bottom-style: solid;
border-bottom-width: 1px;
border-color: #B1B2B3;
height: 28px;
clear: both;
}
.h-modulo-header {
height: 48px;
}
.h-publish-header {
height: 76px;
top: 48px;
}
.h-inner {
height: calc(100% - 124px);
top: 48px;
}
.h-footer {
height: 76px;
}
::placeholder {
color: #B1B2B3;
}
.bg-red {
background-color: #EE5432;
}
.bg-gray-30 {
background-color: #B1B2B3;
}
.two-lines {
display: -webkit-box;
-webkit-box-orient: vertical;
word-wrap: break-word;
-webkit-line-clamp: 2;
overflow: hidden;
}
.five-lines {
display: -webkit-box;
-webkit-box-orient: vertical;
word-wrap: break-word;
-webkit-line-clamp: 5;
overflow: hidden;
}
.one-line {
word-wrap: break-word;
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
}

View File

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

View File

@ -0,0 +1,40 @@
.spinner-pending {
position: relative;
content: "";
border-radius: 100%;
height: 16px;
width: 16px;
background-color: rgba(255,255,255,1);
}
.spinner-pending::after {
content: "";
background-color: rgba(128,128,128,1);
width: 16px;
height: 16px;
position: absolute;
border-radius: 100%;
clip: rect(0, 16px, 16px, 8px);
animation: spin 1s cubic-bezier(0.745, 0.045, 0.355, 1.000) infinite;
}
@keyframes spin {
0% {transform:rotate(0deg)}
25% {transform:rotate(90deg)}
50% {transform:rotate(180deg)}
75% {transform:rotate(270deg)}
100% {transform:rotate(360deg)}
}
.spinner-nostart {
width: 8px;
height: 8px;
border-radius: 100%;
content:'';
background-color: black;
}

File diff suppressed because one or more lines are too long

View File

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

View File

@ -0,0 +1,33 @@
import "/lib/object-extensions";
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";
import * as util from '/lib/util';
import _ from 'lodash';
console.log('app running');
/*
Common variables:
station : ~zod/club
circle : club
host : zod
*/
api.setAuthTokens({
ship: window.ship
});
subscription.start();
window.util = util;
window._ = _;
ReactDOM.render((
<Root />
), document.querySelectorAll("#root")[0]);

View File

@ -0,0 +1,48 @@
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 = [];
}
bind(path, method, ship = this.authTokens.ship, appl = "publish", 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);
}
);
}
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 UrbitApi();
window.api = api;

View File

@ -0,0 +1,369 @@
import React, { Component } from 'react';
import classnames from 'classnames';
import { PostPreview } from '/components/lib/post-preview';
import _ from 'lodash';
import { PathControl } from '/components/lib/path-control';
import { BlogData } from '/components/lib/blog-data';
import { BlogNotes } from '/components/lib/blog-notes';
import { BlogSubs } from '/components/lib/blog-subs';
import { BlogSettings } from '/components/lib/blog-settings';
import { withRouter } from 'react-router';
import { NotFound } from '/components/not-found';
import { Link } from 'react-router-dom';
const PC = withRouter(PathControl);
const NF = withRouter(NotFound);
const BN = withRouter(BlogNotes);
const BS = withRouter(BlogSettings)
export class Blog extends Component {
constructor(props){
super(props);
this.state = {
view: 'notes',
awaiting: false,
postProps: [],
blogTitle: '',
blogHost: '',
pathData: [],
temporary: false,
awaitingSubscribe: false,
awaitingUnsubscribe: false,
notFound: false,
};
this.subscribe = this.subscribe.bind(this);
this.unsubscribe = this.unsubscribe.bind(this);
this.viewSubs = this.viewSubs.bind(this);
this.viewSettings = this.viewSettings.bind(this);
this.viewNotes = this.viewNotes.bind(this);
this.blog = null;
}
handleEvent(diff) {
if (diff.data.total) {
let blog = diff.data.total.data;
this.blog = blog;
this.setState({
postProps: this.buildPosts(blog),
blog: blog,
blogTitle: blog.info.title,
blogHost: blog.info.owner,
awaiting: false,
pathData: [
{ text: "Home", url: "/~publish/recent" },
{ text: blog.info.title,
url: `/~publish/${blog.info.owner}/${blog.info.filename}` }
],
});
this.props.setSpinner(false);
} else if (diff.data.remove) {
if (diff.data.remove.post) {
// XX TODO
} else {
this.props.history.push("/~publish/recent");
}
}
}
handleError(err) {
this.props.setSpinner(false);
this.setState({notFound: true});
}
componentDidUpdate(prevProps, prevState) {
if (this.state.notFound) return;
let ship = this.props.ship;
let blogId = this.props.blogId;
let blog = (ship === window.ship)
? _.get(this.props, `pubs[${blogId}]`, false)
: _.get(this.props, `subs[${ship}][${blogId}]`, false);
if (!(blog) && (ship === window.ship)) {
this.setState({notFound: true});
return;
} else if (this.blog && !blog) {
this.props.history.push("/~publish/recent");
return;
}
this.blog = blog;
if (this.state.awaitingSubscribe && blog) {
this.setState({
temporary: false,
awaitingSubscribe: false,
});
this.props.setSpinner(false);
}
}
componentWillMount() {
let ship = this.props.ship;
let blogId = this.props.blogId;
let blog = (ship == window.ship)
? _.get(this.props, `pubs[${blogId}]`, false)
: _.get(this.props, `subs[${ship}][${blogId}]`, false);
if (!(blog) && (ship === window.ship)) {
this.setState({notFound: true});
return;
};
let temporary = (!(blog) && (ship != window.ship));
if (temporary) {
this.setState({
awaiting: {
ship: ship,
blogId: blogId,
},
temporary: true,
});
this.props.setSpinner(true);
this.props.api.bind(`/collection/${blogId}`, "PUT", ship, "publish",
this.handleEvent.bind(this),
this.handleError.bind(this));
} else {
this.blog = blog;
}
}
buildPosts(blog){
if (!blog) {
return [];
}
let pinProps = blog.order.pin.map((postId) => {
let post = blog.posts[postId];
return this.buildPostPreviewProps(post, blog, true);
});
let unpinProps = blog.order.unpin.map((postId) => {
let post = blog.posts[postId];
return this.buildPostPreviewProps(post, blog, false);
});
return pinProps.concat(unpinProps);
}
buildPostPreviewProps(post, blog, pinned){
return {
postTitle: post.post.info.title,
postName: post.post.info.filename,
postBody: post.post.body,
numComments: post.comments.length,
collectionTitle: blog.info.title,
collectionName: blog.info.filename,
author: post.post.info.creator,
blogOwner: blog.info.owner,
date: post.post.info["date-created"],
pinned: pinned,
}
}
buildData(){
let blog = (this.props.ship == window.ship)
? _.get(this.props, `pubs[${this.props.blogId}]`, false)
: _.get(this.props, `subs[${this.props.ship}][${this.props.blogId}]`, false);
if (this.state.temporary) {
return {
blog: this.state.blog,
postProps: this.state.postProps,
blogTitle: this.state.blogTitle,
blogHost: this.state.blogHost,
pathData: this.state.pathData,
};
} else {
if (!blog) {
return false;
}
return {
blog: blog,
postProps: this.buildPosts(blog),
blogTitle: blog.info.title,
blogHost: blog.info.owner,
pathData: [
{ text: "Home", url: "/~publish/recent" },
{ text: blog.info.title,
url: `/~publish/${blog.info.owner}/${blog.info.filename}` }
],
};
}
}
subscribe() {
let sub = {
subscribe: {
who: this.props.ship,
coll: this.props.blogId,
}
}
this.props.setSpinner(true);
this.setState({awaitingSubscribe: true}, () => {
this.props.api.action("publish", "publish-action", sub);
});
}
unsubscribe() {
let unsub = {
unsubscribe: {
who: this.props.ship,
coll: this.props.blogId,
}
}
this.props.api.action("publish", "publish-action", unsub);
this.props.history.push("/~publish/recent");
}
viewSubs() {
console.log("view subs");
this.setState({view: 'subs'});
}
viewSettings() {
console.log("view settings");
this.setState({view: 'settings'});
}
viewNotes() {
console.log("view notes");
this.setState({view: 'notes'});
}
render() {
if (this.state.notFound) {
return (
<NF/>
);
} else if (this.state.awaiting) {
return null;
} else {
let data = this.buildData();
let contributors = `~${this.props.ship}`;
let create = (this.props.ship === window.ship);
let subNum = _.get(data.blog, 'subscribers.length', 0);
let foreign = _.get(this.props,
`subs[${this.props.ship}][${this.props.blogId}]`, false);
let actionType = false;
if (this.state.temporary) {
actionType = 'subscribe';
} else if ((this.props.ship !== window.ship) && foreign) {
actionType = 'unsubscribe';
}
let viewSubs = (this.props.ship === window.ship)
? this.viewSubs
: null;
let viewSettings = (this.props.ship === window.ship)
? this.viewSettings
: null;
if (this.state.view === 'notes') {
return (
<div>
<PC pathData={data.pathData} create={create}/>
<div className="absolute w-100"
style={{top:124, paddingLeft: 16, paddingRight: 16, paddingTop: 32}}>
<div className="flex-col">
<h2 style={{wordBreak: "break-word"}}>
{data.blogTitle}
</h2>
<div className="flex" style={{marginTop: 22}}>
<BlogData
host={this.props.ship}
viewSubs={viewSubs}
subNum={subNum}
viewSettings={viewSettings}
subscribeAction={actionType}
subscribe={this.subscribe}
unsubscribe={this.unsubscribe}
/>
</div>
<BN ship={this.props.ship} posts={data.postProps} />
</div>
</div>
</div>
);
} else if (this.state.view === 'subs') {
let subscribers = _.get(data, 'blog.subscribers', []);
return (
<div>
<PC pathData={data.pathData} create={create}/>
<div className="absolute w-100"
style={{top:124, paddingLeft: 16, paddingRight: 16, paddingTop: 32}}>
<div className="flex-col">
<h2 style={{wordBreak: "break-word"}}>
{data.blogTitle}
</h2>
<div className="flex" style={{marginTop: 22}}>
<BlogData
host={this.props.ship}
viewSubs={viewSubs}
subNum={subNum}
viewSettings={viewSettings}
subscribeAction={actionType}
subscribe={this.subscribe}
unsubscribe={this.unsubscribe}
/>
</div>
<BlogSubs back={this.viewNotes}
subs={subscribers}
blogId={this.props.blogId}
title={data.blogTitle}
api={this.props.api}/>
</div>
</div>
</div>
);
} else if (this.state.view === 'settings') {
return (
<div>
<PC pathData={data.pathData} create={create}/>
<div className="absolute w-100"
style={{top:124, paddingLeft: 16, paddingRight: 16, paddingTop: 32}}>
<div className="flex-col">
<h2 style={{wordBreak: "break-word"}}>
{data.blogTitle}
</h2>
<div className="flex" style={{marginTop: 22}}>
<BlogData
host={this.props.ship}
viewSubs={viewSubs}
subNum={subNum}
viewSettings={viewSettings}
subscribeAction={actionType}
subscribe={this.subscribe}
unsubscribe={this.unsubscribe}
/>
</div>
<BS back={this.viewNotes}
blogId={this.props.blogId}
title={data.blogTitle}
api={this.props.api}/>
</div>
</div>
</div>
);
}
}
}
}

View File

@ -0,0 +1,90 @@
import React, { Component } from 'react';
import classnames from 'classnames';
class Subscribe extends Component {
constructor(props) {
super(props);
}
render() {
if (this.props.actionType === 'subscribe') {
return (
<p className="label-small b pointer"
onClick={this.props.subscribe}>
Subscribe
</p>
);
} else if (this.props.actionType === 'unsubscribe') {
return (
<p className="label-small b pointer"
onClick={this.props.unsubscribe}>
Unsubscribe
</p>
);
} else {
return null;
}
}
}
class Subscribers extends Component {
constructor(props) {
super(props);
}
render() {
let subscribers = (this.props.subNum === 1)
? `${this.props.subNum} Subscriber`
: `${this.props.subNum} Subscribers`;
if (this.props.action !== null) {
return (
<p className="label-small b pointer" onClick={this.props.action}>
{subscribers}
</p>
);
} else {
return (
<p className="label-small b">{subscribers}</p>
);
}
}
}
class Settings extends Component {
constructor(props) {
super(props);
}
render() {
if (this.props.action !== null) {
return (
<p className="label-small b pointer" onClick={this.props.action}>
Settings
</p>
);
} else {
return null;
}
}
}
export class BlogData extends Component {
constructor(props) {
super(props);
}
render() {
return (
<div className="flex-col">
<p className="label-small">By ~{this.props.host}</p>
<Subscribers action={this.props.viewSubs} subNum={this.props.subNum}/>
<Settings action={this.props.viewSettings}/>
<Subscribe actionType={this.props.subscribeAction}
subscribe={this.props.subscribe}
unsubscribe={this.props.unsubscribe}
/>
</div>
);
}
}

View File

@ -0,0 +1,50 @@
import React, { Component } from 'react';
import classnames from 'classnames';
import { PostPreview } from '/components/lib/post-preview';
import { Link } from 'react-router-dom';
export class BlogNotes extends Component {
constructor(props) {
super(props);
}
render() {
if (!this.props.posts ||
((this.props.posts.length === 0) &&
(this.props.ship === window.ship))) {
let link = {
pathname: "/~publish/new-post",
state: {
lastPath: this.props.location.pathname,
lastMatch: this.props.match.path,
lastParams: this.props.match.params,
}
}
return (
<div className="flex flex-wrap">
<div className="w-336 relative">
<hr className="gray-10" style={{marginTop: 48, marginBottom:25}}/>
<Link to={link}>
<p className="body-large b">
-> Create First Post
</p>
</Link>
</div>
</div>
);
}
let posts = this.props.posts.map((post, key) => {
return (
<PostPreview post={post} key={key}/>
);
});
return (
<div className="flex flex-wrap" style={{marginTop: 48}}>
{posts}
</div>
);
}
}

View File

@ -0,0 +1,122 @@
import React, { Component } from 'react';
import classnames from 'classnames';
class SaveLink extends Component {
constructor(props) {
super(props);
}
render() {
if (this.props.enabled) {
return (
<button className="label-regular b"
onClick={this.props.action}>
-> Save
</button>
);
} else {
return (
<p className="label-regular b gray-50">
-> Save
</p>
);
}
}
}
export class BlogSettings extends Component {
constructor(props) {
super(props);
this.state = {
title: '',
awaitingTitleChange: false,
}
this.rename = this.rename.bind(this);
this.titleChange = this.titleChange.bind(this);
this.deleteBlog = this.deleteBlog.bind(this);
}
rename() {
let edit = {
"edit-collection": {
name: this.props.blogId,
title: this.state.title,
}
}
this.setState({
awaitingTitleChange: true,
}, () => {
this.props.api.action("publish", "publish-action", edit);
});
}
titleChange(evt) {
this.setState({title: evt.target.value});
}
deleteBlog() {
let del = {
"delete-collection": {
coll: this.props.blogId,
}
}
this.props.api.action("publish", "publish-action", del);
this.props.history.push("/~publish/recent");
}
componentDidUpdate(prevProps) {
if (this.state.awaitingTitleChange) {
if (prevProps.title !== this.props.title){
this.titleInput.value = '';
this.setState({
awaitingTitleChange: false,
});
}
}
}
render() {
let back = '<- Back to notes'
let enableSave = ((this.state.title !== '') &&
(this.state.title !== this.props.title) &&
!this.state.awaitingTitleChange);
return (
<div className="flex-col mw-688" style={{marginTop:48}}>
<hr className="gray-30" style={{marginBottom:25}}/>
<p className="label-regular pointer b" onClick={this.props.back}>
{back}
</p>
<p className="body-large b" style={{marginTop:16, marginBottom: 20}}>
Settings
</p>
<div className="flex">
<div className="flex-col w-100">
<p className="body-regular-400">Delete Notebook</p>
<p className="gray-50 label-small-2" style={{marginTop:12, marginBottom:8}}>
Permanently delete this notebook
</p>
<button className="red label-regular b" onClick={this.deleteBlog}>
-> Delete
</button>
</div>
<div className="flex-col w-100">
<p className="body-regular-400">Rename</p>
<p className="gray-50 label-small-2" style={{marginTop:12, marginBottom:23}}>
Change the name of this notebook
</p>
<p className="label-small-2">Notebook Name</p>
<input className="body-regular-400 w-100"
ref={(el) => {this.titleInput = el}}
style={{marginBottom:8}}
placeholder={this.props.title}
onChange={this.titleChange}
disabled={this.state.awaitingTitleChange}/>
<SaveLink action={this.rename} enabled={enableSave}/>
</div>
</div>
</div>
);
}
}

View File

@ -0,0 +1,140 @@
import React, { Component } from 'react';
import classnames from 'classnames';
import urbitOb from 'urbit-ob';
class InviteLink extends Component {
constructor(props) {
super(props);
}
render() {
if (this.props.enabled) {
return (
<button className="label-regular b underline"
onClick={this.props.action}>
Invite
</button>
);
} else {
return (
<p className="label-regular b underline gray-50">
Invite
</p>
);
}
}
}
export class BlogSubs extends Component {
constructor(props) {
super(props);
this.state = {
validInvites: false,
invites: [],
}
this.inviteHeight = 133;
this.invite = this.invite.bind(this);
this.inviteChange = this.inviteChange.bind(this);
}
inviteChange(evt) {
this.inviteInput.style.height = 'auto';
let newHeight = (this.inviteInput.scrollHeight < 133)
? 133 : this.inviteInput.scrollHeight + 2;
this.inviteInput.style.height = newHeight+'px';
this.inviteHeight = this.inviteInput.style.height;;
let tokens = evt.target.value
.trim()
.split(/[\s,]+/)
.map(t => t.trim());
let valid = tokens.reduce((valid, s) =>
valid && ((s !== '~') && urbitOb.isValidPatp(s) && s.includes('~')), true);
if (valid) {
this.setState({
validInvites: true,
invites: tokens.map(t => t.slice(1)),
});
} else {
this.setState({validInvites: false});
}
}
invite() {
if (this.inviteInput) this.inviteInput.value = '';
let invite = {
invite: {
coll: this.props.blogId,
title: this.props.title,
who: this.state.invites,
}
}
this.inviteHeight = 133;
this.setState({
validInvites: false,
invites: [],
}, () => {
this.props.api.action("publish", "publish-action", invite);
});
}
render() {
let back = '<- Back to notes'
let subscribers = this.props.subs.map((sub, i) => {
return (
<div className="flex w-100" key={i+1}>
<p className="label-regular-mono w-100">~{sub}</p>
</div>
);
});
subscribers.unshift(
<div className="flex w-100" key={0}>
<p className="label-regular-mono w-100">~{window.ship}</p>
<p className="label-regular-mono w-100">Host (You)</p>
</div>
);
return (
<div className="flex-col mw-688" style={{marginTop:48}}>
<hr className="gray-30" style={{marginBottom:25}}/>
<p className="label-regular pointer b" onClick={this.props.back}>
{back}
</p>
<p className="body-large b" style={{marginTop:16, marginBottom: 20}}>
Manage Notebook
</p>
<div className="flex">
<div className="flex-col w-100">
<p className="body-regular-400">Members</p>
<p className="gray-50 label-small-2"
style={{marginTop:12, marginBottom: 23}}>
Everyone subscribed to this notebook
</p>
{subscribers}
</div>
<div className="flex-col w-100">
<p className="body-regular-400">Invite</p>
<p className="gray-50 label-small-2"
style={{marginTop:12, marginBottom: 23}}>
Invite people to subscribe to this notebook
</p>
<textarea className="w-100 label-regular-mono overflow-y-hidden"
ref={(el) => {this.inviteInput = el}}
style={{resize:"none", marginBottom:8, height: this.inviteHeight}}
onChange={this.inviteChange}>
</textarea>
<InviteLink enabled={this.state.validInvites} action={this.invite}/>
</div>
</div>
</div>
);
}
}

View File

@ -0,0 +1,78 @@
import React, { Component } from 'react';
import classnames from 'classnames';
import { Sigil } from '/components/lib/icons/sigil';
class PostButton extends Component {
render() {
if (this.props.enabled) {
return (
<p className="body-regular pointer" onClick={this.props.post}>
-> Post
</p>
);
} else {
return (
<p className="body-regular gray-30">
-> Post
</p>
);
}
}
}
export class CommentBox extends Component {
constructor(props){
super(props);
this.commentChange = this.commentChange.bind(this);
this.commentHeight = 54;
}
componentDidUpdate(prevProps, prevState) {
if (!prevProps.enabled && this.props.enabled) {
if (this.commentInput) {
this.commentInput.value = '';
this.commentInput.style.height = 54;
}
}
}
commentChange(evt) {
this.commentInput.style.height = 'auto';
let newHeight = (this.commentInput.scrollHeight < 54)
? 54 : this.commentInput.scrollHeight+2;
this.commentInput.style.height = newHeight+'px';
this.commentHeight = this.commentInput.style.height;
this.props.action(evt);
}
render() {
let textClass = (this.props.enabled)
? "body-regular-400 w-100"
: "body-regular-400 w-100 gray-30";
return (
<div className="cb w-100 flex"
style={{paddingBottom: 8, marginTop: 32}}>
<div className="fl" style={{marginRight: 10}}>
<Sigil ship={this.props.our} size={36}/>
</div>
<div className="flex-col w-100">
<textarea className={textClass}
ref={(el) => {this.commentInput = el}}
style={{resize: "none", height: this.commentHeight}}
type="text"
name="commentBody"
defaultValue=''
onChange={this.commentChange}
disabled={(!this.props.enabled)}>
</textarea>
<PostButton
post={this.props.post}
enabled={(Boolean(this.props.content) && this.props.enabled)}
/>
</div>
</div>
);
}
}

View File

@ -0,0 +1,59 @@
import React, { Component } from 'react';
import classnames from 'classnames';
import { Sigil } from '/components/lib/icons/sigil';
import moment from 'moment';
export class Comment extends Component {
constructor(props){
super(props);
moment.updateLocale('en', {
relativeTime: {
past: function(input) {
return input === 'just now'
? input
: input + ' ago'
},
s : 'just now',
future : 'in %s',
m : '1m',
mm : '%dm',
h : '1h',
hh : '%dh',
d : '1d',
dd : '%dd',
M : '1 month',
MM : '%d months',
y : '1 year',
yy : '%d years',
}
});
}
render(){
let body = this.props.body.split("\n").map((line, i) =>{
return (<p key={i}>{line}</p>);
});
let date = moment(this.props.date).fromNow();
return (
<div className="cb w-100 flex" style={{paddingBottom: 16}}>
<div className="fl" style={{marginRight: 10}}>
<Sigil ship={this.props.ship} size={36} />
</div>
<div className="flex-col fl">
<div className="label-small-mono gray-50">
<p className="fl label-small-mono"
style={{width: 107}}>{this.props.ship}</p>
<p className="fl label-small-mono">{date}</p>
</div>
<div className="cb body-regular-400">
{body}
</div>
</div>
</div>
);
}
}

View File

@ -0,0 +1,112 @@
import React, { Component } from 'react';
import classnames from 'classnames';
import { Comment } from '/components/lib/comment';
import { CommentBox } from '/components/lib/comment-box';
export class Comments extends Component {
constructor(props){
super(props);
this.state = {
show: false,
commentBody: '',
awaiting: false,
}
this.toggleDisplay = this.toggleDisplay.bind(this);
this.commentChange = this.commentChange.bind(this);
this.postComment = this.postComment.bind(this);
}
commentChange(evt) {
this.setState({commentBody: evt.target.value});
}
toggleDisplay() {
this.setState({show: !this.state.show});
}
postComment() {
this.props.setSpinner(true);
let comment = {
"new-comment": {
who: this.props.ship,
coll: this.props.blogId,
name: this.props.postId,
content: this.state.commentBody,
}
};
this.setState({
awaiting: {
ship: this.props.ship,
blogId: this.props.blogId,
postId: this.props.postId,
}
}, () => {
this.props.api.action("publish", "publish-action", comment)
});
}
componentDidUpdate(prevProps, prevState) {
if (this.state.awaiting) {
if (prevProps.comments != this.props.comments) {
this.props.setSpinner(false);
this.setState({awaiting: false, commentBody: ''});
}
}
}
render(){
if (this.state.show) {
let our = `~${window.ship}`;
let comments = this.props.comments.map((comment, i) => {
let commentProps = {
ship: comment.info.creator,
date: comment.info["date-created"],
body: comment.body,
};
return (<Comment {...commentProps} key={i} />);
});
return (
<div className="cb mt3 mb4">
<p className="gray-50 body-large b">
<span>{this.props.comments.length} </span>
<span className="black">
Comments
</span>
</p>
<p className="cl body-regular pointer" onClick={this.toggleDisplay}>
- Hide Comments
</p>
<CommentBox our={our}
action={this.commentChange}
enabled={!(Boolean(this.state.awaiting))}
content={this.state.commentBody}
post={this.postComment}/>
<div className="flex-col" style={{marginTop: 32}}>
{comments}
</div>
</div>
);
} else {
return (
<div className="cb mt3 mb4">
<p className="gray-50 body-large b">
<span>{this.props.comments.length} </span>
<span className="black">
Comments
</span>
</p>
<p className="cl body-regular pointer" onClick={this.toggleDisplay}>
+ Show Comments
</p>
</div>
);
}
}
}

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