Merge remote-tracking branch 'origin/master' into HEAD

This commit is contained in:
Philip Monk 2019-07-26 18:44:29 -07:00
commit 89cde4a272
No known key found for this signature in database
GPG Key ID: B66E1F02604E44EC
164 changed files with 50052 additions and 352764 deletions

7
.gitignore vendored
View File

@ -9,3 +9,10 @@ tags
TAGS
cross/
release/
**/.DS_Store
**/dist
**/node_modules
**/urbitrc
**/*.swp
**/*.swo
**/*-min.js

View File

@ -23,6 +23,9 @@ pills:
sh/update-brass-pill
sh/update-ivory-pill
interface:
sh/build-interface
clean:
rm -rf ./out ./work
rm -f result result-*

View File

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:42d7fae92d11b3dbc3ee9aaa1e2edc5328f308c047dcf126b6cd661862904782
size 4352384
oid sha256:0e520b9ab0232d1765e1dacde96a1210845768e7334a334b5705d1c40348c82b
size 4464201

View File

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:b4804169319b8f69948f980f5212c9ff7caa39b2803b875e394ce1cf181653e7
size 9523484
oid sha256:a4a8e1daf0bfe86d5fc4ef7060b0c6a6c2678a344787926f14bb4b8cfabe8752
size 9549390

View File

@ -1,6 +1,7 @@
{
pkgs,
debug,
ivory ? ../../../bin/ivory.pill,
argon2, ed25519, ent, ge-additions, h2o, murmur3, scrypt, secp256k1, sni, softfloat3, uv
}:
@ -30,6 +31,7 @@ pkgs.stdenv.mkDerivation {
hardeningDisable = if debug then [ "all" ] else [];
CFLAGS = if debug then "-O3 -g -Werror" else "-O3 -Werror";
IVORY = ivory;
MEMORY_DEBUG = debug;
CPU_DEBUG = debug;
EVENT_TIME_DEBUG = false;

View File

@ -1,6 +1,12 @@
{ env_name, env, deps }:
{ ent, ge-additions, cacert, xxd, name ? "urbit", debug ? false }:
{
ent,
name ? "urbit",
debug ? false,
ivory ? ../../../bin/ivory.pill,
ge-additions, cacert, xxd
}:
let
@ -22,6 +28,7 @@ env.make_derivation {
EVENT_TIME_DEBUG = false;
NCURSES = env.ncurses;
SSL_CERT_FILE = "${cacert}/etc/ssl/certs/ca-bundle.crt";
IVORY = ivory;
name = "${name}-${env_name}";
exename = name;

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -632,12 +632,7 @@
(json-octs u.r.httr parse-record:(provider authority))
?~(dat ~. id.u.dat)
::
=/ =address:dns
?>(?=(%direct -.target) +.target)
=/ =turf
(weld dom.authority /(crip +:(scow %p ship)))
;< ~ bind:m (poke-app:stdio collector-app [%dns-complete ship address turf])
;< now=@da bind:m get-time:stdio
;< now=@da bind:m get-time:stdio
=/ =bound
[now id target ?~(existing ~ [[wen cur] hit]:u.existing)]
(pure:m (some bound))
@ -764,6 +759,10 @@
?~ new
~& [%bind-failed in-poke-data]
(pure:m state)
=/ =turf
(weld dom.aut.nam /(crip +:(scow %p who)))
;< ~ bind:m
(poke-app:stdio collector-app [%dns-complete who +.tar turf])
=. bon.nam (~(put by bon.nam) who u.new)
=. nem.state (some nam)
::

View File

@ -124,9 +124,10 @@
=/ req=(unit address:dns) (~(get by requested.state) who)
:: ignore established bindings that don't match requested
::
?: ?& ?=(^ req)
?: ?| ?=(~ req)
!=(adr u.req)
==
~& %unknown-complete
this
=: requested.state (~(del by requested.state) who)
completed.state (~(put by completed.state) who [adr tuf])

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,14 @@
:: :dns-collector: manually "complete" (fulfill) requests
::
:::: /hoon/complete/dns-collector/gen
::
/- *dns, *sole
/+ *generators
:- %say
|= $: [now=@da eny=@uvJ bec=beak]
[who=@p addr=@if =turf ~]
~
==
:- %dns-complete
^- [ship binding]
[who [%if addr] turf]

View File

@ -0,0 +1,26 @@
:: Produce a raw event to reload a vane
::
:: Try: .event/ovo +reload-event %c, then restart urbit with
:: -I pier/.urb/put/event.ovo
::
:- %say
|= $: [now=@da eny=@uvJ bek=beak]
[[tam=term ~] ~]
==
:- %ovo
=/ top `path`/(scot %p p.bek)/[q.bek]/(scot r.bek)
=/ nam
=/ van=(list [term ~])
:- zus=[%zuse ~]
~(tap by dir:.^(arch %cy (welp top /sys/vane)))
?. =(1 (met 3 tam))
tam
=+ ^- zaz=(list [p=knot ~])
(skim van |=([a=term ~] =(tam (end 3 1 a))))
?> ?=([[@ ~] ~] zaz)
`term`p.i.zaz
=/ tip (end 3 1 nam)
=/ bip ?:(=('z' tip) %$ tip)
=/ way ?:(=('z' tip) (welp top /sys/[nam]) (welp top /sys/vane/[nam]))
=/ fil .^(@ %cx (welp way /hoon))
[//arvo %veer bip way fil]

View File

@ -1,4 +1,3 @@
!:
:::::: ::::::::::::::::::::::::::::::::::::::::::::::::::::::
:::::: :::::: Postface ::::::
:::::: ::::::::::::::::::::::::::::::::::::::::::::::::::::::

View File

@ -10400,6 +10400,11 @@
{$tsbn *}
=+ lem=$(gen p.gen, gol %noun)
$(gen q.gen, sut p.lem, dox q.lem)
::
{$tscm *}
=/ boc (busk p.gen)
=/ nuf (busk(sut dox) p.gen)
$(gen q.gen, sut boc, dox nuf)
::
{$wtcl *}
=+ nor=$(gen p.gen, gol bool)

View File

@ -580,8 +580,8 @@
[~ ~ %wait ~]
?: ?=(%writ +<.sign)
=/ uni-rang=rang
:- (~(uni by hut.ran) hut.new-rang)
(~(uni by lat.ran) lat.new-rang)
:- (~(uni by hut.new-rang) hut.ran)
(~(uni by lat.new-rang) lat.ran)
[~ ~ %done p.sign uni-rang]
~| [%expected-writ got=+<.sign]
!!
@ -1611,7 +1611,10 @@
(page-to-lobe:sutil [p q.q]:(~(got by both-patched) pax))
[(lobe-to-mark:sutil u.-) u.-]
[p q.q]:cay
[(~(put by hat) pax p.bol) (~(put by lat) p.bol bol)]
:- (~(put by hat) pax p.bol)
?: (~(has by lat) p.bol)
lat
(~(put by lat) p.bol bol)
:: ~& old=(~(run by old) mug)
:: ~& newdal=(~(run by new.dal) mug)
:: ~& newdob=(~(run by new.dob) mug)
@ -2047,14 +2050,14 @@
(turn ~(tap in lar.nako) |=(=yaki [r.yaki yaki]))
:: hut: updated commits by hash
::
=/ hut (~(gas by hut.ran) nut)
=/ hut (~(uni by (malt nut)) hut.ran)
:: nat: new blob-hash/blob pairs
::
=/ nat
(turn ~(tap in bar.nako) |=(=blob [p.blob blob]))
:: lat: updated blobs by hash
::
=/ lat (~(gas by lat.ran) nat)
=/ lat (~(uni by (malt nat)) lat.ran)
:: traverse updated state and sanity check
::
=+ ~| :* %bad-foreign-update
@ -2396,15 +2399,20 @@
::
:: Update the object store with new blobs.
::
:: Must uni the old-lat into the new-lat so that if we recreate
:: the same blob hash, we use the old blob not the new one. Else
:: you get mutually recurring %delta blobs.
::
++ add-blobs
|= [new-blobs=(map path blob) old-lat=(map lobe blob)]
^- (map lobe blob)
%- ~(uni by old-lat)
%- malt
%+ turn
~(tap by new-blobs)
|= [=path =blob]
[p.blob blob]
=/ new-lat=(map lobe blob)
%- malt
%+ turn
~(tap by new-blobs)
|= [=path =blob]
[p.blob blob]
(~(uni by new-lat) old-lat)
::
:: Apply a change list, creating the commit and applying it to
:: the current state.
@ -3029,8 +3037,8 @@
|= [=dome =rang]
^+ +>
=: dom dome
hut.ran (~(uni by hut.ran) hut.rang)
lat.ran (~(uni by lat.ran) lat.rang)
hut.ran (~(uni by hut.rang) hut.ran)
lat.ran (~(uni by lat.rang) lat.ran)
==
=. +>.$ wake
finish-write
@ -3070,8 +3078,8 @@
^+ +>
=. +>.$ (emit [hen %give %mere %& conflicts])
=: dom dome
hut.ran (~(uni by hut.ran) hut.rang)
lat.ran (~(uni by lat.ran) lat.rang)
hut.ran (~(uni by hut.rang) hut.ran)
lat.ran (~(uni by lat.rang) lat.ran)
==
=. +>.$ wake
finish-write
@ -4143,10 +4151,17 @@
[[[hen %slip %d %flog req] ~] ..^$]
::
%drop
=^ mos ruf
=/ den ((de our now ski hen ruf) our des.req)
abet:drop-me:den
[mos ..^$]
?: =(~ act.ruf)
~& %clay-idle
[~ ..^$]
~& :- %clay-cancelling
?> ?=(^ act.ruf)
[hen -.req -.eval-data]:u.act.ruf
=. act.ruf ~
?~ cue.ruf
[~ ..^$]
=/ =duct duct:(need ~(top to cue.ruf))
[[duct %pass /queued-request %b %wait now]~ ..^$]
::
%info
?: =(%$ des.req)

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/*

View File

@ -0,0 +1,196 @@
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 rename = require('gulp-rename');
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('../../arvo/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('../../arvo/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('../../arvo/app/chat/js/'))
.on('end', cb);
});
gulp.task('js-minify', function () {
return gulp.src('../../arvo/app/chat/js/index.js')
.pipe(minify())
.pipe(gulp.dest('../../arvo/app/chat/js/'));
});
gulp.task('tile-js-minify', function () {
return gulp.src('../../arvo/app/chat/js/tile.js')
.pipe(minify())
.pipe(gulp.dest('../../arvo/app/chat/js/'));
});
gulp.task('rename-index-min', function() {
return gulp.src('../../arvo/app/chat/js/index-min.js')
.pipe(rename('index.js'))
.pipe(gulp.dest('../../arvo/app/chat/js/'));
});
gulp.task('rename-tile-min', function() {
return gulp.src('../../arvo/app/chat/js/tile-min.js')
.pipe(rename('tile.js'))
.pipe(gulp.dest('../../arvo/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 ../../arvo/app/chat/js/index-min.js ../../arvo/app/chat/js/' + newFilename);
})
);
})
gulp.task('urbit-copy', function () {
let ret = gulp.src('../../arvo/**/*');
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',
),
'rename-index-min',
'rename-tile-min',
'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('../../arvo/**/*', gulp.parallel('urbit-copy'));
}));

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

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,46 @@
{
"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",
"gulp-rename": "^1.4.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;

63
pkg/interface/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,144 @@
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 rename = require('gulp-rename');
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('../../arvo/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('../../arvo/app/clock/js/'))
.on('end', cb);
});
gulp.task('js-minify', function () {
return gulp.src('../../arvo/app/clock/js/index.js')
.pipe(minify())
.pipe(gulp.dest('../../arvo/app/clock/js/'));
});
gulp.task('tile-js-minify', function () {
return gulp.src('../../arvo/app/clock/js/tile.js')
.pipe(minify())
.pipe(gulp.dest('../../arvo/app/clock/js/'));
});
gulp.task('rename-tile-min', function() {
return gulp.src('../../arvo/app/clock/js/tile-min.js')
.pipe(rename('tile.js'))
.pipe(gulp.dest('../../arvo/app/clock/js/'));
});
gulp.task('urbit-copy', function () {
let ret = gulp.src('../../arvo/**/*');
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', 'rename-tile-min', '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('../../arvo/**/*', gulp.parallel('urbit-copy'));
}));

6599
pkg/interface/clock/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",
"gulp-rename": "^1.4.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,5 @@
module.exports = {
URBIT_PIERS: [
"/Users/logan/Dev/light-urbit/build/zod-chess-9/home",
]
};

9
pkg/interface/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,146 @@
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 rename = require('gulp-rename');
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('../../arvo/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('../../arvo/app/launch/js/'))
.on('end', cb);
});
gulp.task('js-minify', function () {
return gulp.src('../../arvo/app/launch/js/index.js')
.pipe(minify())
.pipe(gulp.dest('../../arvo/app/launch/js/'));
});
gulp.task('rename-index-min', function() {
return gulp.src('../../arvo/app/launch/js/index-min.js')
.pipe(rename('index.js'))
.pipe(gulp.dest('../../arvo/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 ../../arvo/app/launch/js/index-min.js ../../arvo/app/launch/js/' + newFilename);
})
);
})
gulp.task('urbit-copy', function () {
let ret = gulp.src('../../arvo/**/*');
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'
),
'rename-index-min',
'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('../../arvo/**/*', gulp.parallel('urbit-copy'));
}));

6486
pkg/interface/launch/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",
"gulp-rename": "^1.4.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,204 @@
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 rename = require('gulp-rename');
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('../../arvo/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('../../arvo/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('../../arvo/app/publish/js/'))
.on('end', cb);
});
gulp.task('js-minify', function () {
return gulp.src('../../arvo/app/publish/js/index.js')
.pipe(minify())
.pipe(gulp.dest('../../arvo/app/publish/js/'));
});
gulp.task('tile-js-minify', function () {
return gulp.src('../../arvo/app/publish/js/tile.js')
.pipe(minify())
.pipe(gulp.dest('../../arvo/app/publish/js/'));
});
gulp.task('rename-index-min', function() {
return gulp.src('../../arvo/app/publish/js/index-min.js')
.pipe(rename('index.js'))
.pipe(gulp.dest('../../arvo/app/publish/js/'));
});
gulp.task('rename-tile-min', function() {
return gulp.src('../../arvo/app/publish/js/tile-min.js')
.pipe(rename('tile.js'))
.pipe(gulp.dest('../../arvo/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 ../../arvo/app/publish/js/index-min.js ../../arvo/app/publish/js/' + newFilename);
})
);
})
gulp.task('urbit-copy', function () {
let ret = gulp.src('../../arvo/**/*');
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',
),
'rename-index-min',
'rename-tile-min',
'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('../../arvo/**/*', gulp.parallel('urbit-copy'));
}));

6603
pkg/interface/publish/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,46 @@
{
"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",
"gulp-rename": "^1.4.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>
);
}
}

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