contacts: added store, view, marks, ui

This commit is contained in:
Logan Allen 2019-11-18 14:41:08 -08:00
parent 524bdbd8c1
commit 47c1745074
29 changed files with 8243 additions and 0 deletions

View File

@ -0,0 +1,179 @@
/+ *contact-json
|%
+$ move [bone card]
::
+$ card
$% [%diff [%contact-update contact-update]]
[%quit ~]
==
::
+$ state
$% state-zero
==
::
+$ state-zero
$: %0
=rolodex
==
--
::
|_ [bol=bowl:gall state-zero]
::
++ this .
::
++ prep
|= old=(unit state)
^- (quip move _this)
:- ~
?~ old this
?- -.old
%0 this(+<+ u.old)
==
::
++ peek-x-all
|= pax=path
^- (unit (unit [%noun (map path contacts)]))
[~ ~ %noun rolodex]
::
++ peek-x-contacts
|= pax=path
^- (unit (unit [%noun (unit contacts)]))
?~ pax
~
=/ contacts=(unit contacts) (~(get by rolodex) pax)
[~ ~ %noun contacts]
::
++ peek-x-contact
|= pax=path
^- (unit (unit [%noun (unit contact)]))
:: /:path/:ship
=/ pas (flop pax)
?~ pas
~
=/ =ship (slav %p i.pas)
=. pax (scag (dec (lent pax)) `(list @ta)`pax)
=/ contacts=(unit contacts) (~(get by rolodex) pax)
?~ contacts
~
=/ contact=(unit contact) (~(get by u.contacts) ship)
[~ ~ %noun contact]
::
++ peer-all
|= pax=path
^- (quip move _this)
?> (team:title our.bol src.bol)
:: send all updates from now on
:_ this
[ost.bol %diff %contact-update [%rolodex rolodex]]~
::
++ peer-updates
|= pax=path
^- (quip move _this)
?> (team:title our.bol src.bol)
:: send all updates from now on
[~ this]
::
++ peer-contacts
|= pax=path
^- (quip move _this)
?> (team:title our.bol src.bol)
:_ this
[ost.bol %diff %contact-update [%contacts pax (~(got by rolodex) pax)]]~
::
::++ poke-json
:: |= =json
:: ^- (quip move _this)
:: ?> (team:title our.bol src.bol)
:: (poke-contact-action (json-to-action json))
::
++ poke-contact-action
|= action=contact-action
^- (quip move _this)
?> (team:title our.bol src.bol)
?- -.action
%create (handle-create action)
%delete (handle-delete action)
%add (handle-add action)
%remove (handle-remove action)
%edit (handle-edit action)
==
::
++ handle-create
|= act=contact-action
^- (quip move _this)
?> ?=(%create -.act)
?: (~(has by rolodex) path.act)
[~ this]
:- (send-diff path.act act)
this(rolodex (~(put by rolodex) path.act *contacts))
::
++ handle-delete
|= act=contact-action
^- (quip move _this)
?> ?=(%delete -.act)
?. (~(has by rolodex) path.act)
[~ this]
:- (send-diff path.act act)
this(rolodex (~(del by rolodex) path.act))
::
++ handle-add
|= act=contact-action
^- (quip move _this)
?> ?=(%add -.act)
=/ contacts (~(got by rolodex) path.act)
?> (~(has by contacts) ship.act)
=. contacts (~(put by contacts) ship.act contact.act)
:- (send-diff path.act act)
this(rolodex (~(put by rolodex) path.act contacts))
::
++ handle-remove
|= act=contact-action
^- (quip move _this)
?> ?=(%remove -.act)
=/ contacts (~(got by rolodex) path.act)
?< (~(has by contacts) ship.act)
=. contacts (~(del by contacts) ship.act)
:- (send-diff path.act act)
this(rolodex (~(put by rolodex) path.act contacts))
::
++ handle-edit
|= act=contact-action
^- (quip move _this)
?> ?=(%edit -.act)
=/ contacts (~(got by rolodex) path.act)
=/ contact (~(got by contacts) ship.act)
=. contact (edit-contact contact edit-field.act)
=. contacts (~(put by contacts) ship.act contact)
:- (send-diff path.act act)
this(rolodex (~(put by rolodex) path.act contacts))
::
++ edit-contact
|= [con=contact edit=edit-field]
^- contact
?- -.edit
%nickname con(nickname nickname.edit)
%email con(email email.edit)
%phone con(phone phone.edit)
%website con(website website.edit)
%notes con(notes notes.edit)
%color con(color color.edit)
%avatar con(avatar avatar.edit)
==
::
++ update-subscribers
|= [pax=path upd=contact-update]
^- (list move)
%+ turn (prey:pubsub:userlib pax bol)
|= [=bone *]
[bone %diff %contact-update upd]
::
++ send-diff
|= [pax=path upd=contact-update]
^- (list move)
%- zing
:~ (update-subscribers /all upd)
(update-subscribers /updates upd)
(update-subscribers [%contacts pax] upd)
==
::
--

View File

@ -0,0 +1,122 @@
:: contact-view: sets up contact JS client and combines commands
:: into semantic actions for the UI
::
/+ *server, *contact-json
/= index
/^ octs
/; as-octs:mimes:html
/: /===/app/contacts/index
/| /html/
/~ ~
==
/= tile-js
/^ octs
/; as-octs:mimes:html
/: /===/app/contacts/js/tile
/| /js/
/~ ~
==
/= script
/^ octs
/; as-octs:mimes:html
/: /===/app/contacts/js/index
/| /js/
/~ ~
==
/= style
/^ octs
/; as-octs:mimes:html
/: /===/app/contacts/css/index
/| /css/
/~ ~
==
/= contact-png
/^ (map knot @)
/: /===/app/contacts/img /_ /png/
::
|%
::
+$ move [bone card]
::
+$ card
$% [%http-response =http-event:http]
[%connect wire binding:eyre term]
[%quit ~]
[%poke wire dock poke]
==
::
+$ poke
$% [%launch-action [@tas path @t]]
==
--
::
|_ [bol=bowl:gall ?]
::
++ this .
::
++ prep
|= old=(unit ?)
^- (quip move _this)
?~ old
:_ this
:~ [ost.bol %connect / [~ /'~contact'] %contact-view]
(launch-poke [/configs '/~contact/js/tile.js'])
==
[~ this(+<+ u.old)]
::
++ bound
|= [wir=wire success=? binding=binding:eyre]
^- (quip move _this)
[~ this]
::
++ poke-handle-http-request
%- (require-authorization:app ost.bol move this)
|= =inbound-request:eyre
^- (quip move _this)
::
=+ url=(parse-request-line url.request.inbound-request)
=/ name=@t
=+ back-path=(flop site.url)
?~ back-path
''
i.back-path
?: =(name 'tile')
[[ost.bol %http-response (js-response:app tile-js)]~ this]
?+ site.url
:_ this
[ost.bol %http-response not-found:app]~
::
:: styling
::
[%'~contacts' %css %index ~]
:_ this
[ost.bol %http-response (css-response:app style)]~
::
:: javascript
::
[%'~contacts' %js %index ~]
:_ this
[ost.bol %http-response (js-response:app script)]~
::
:: images
::
[%'~contacts' %img *]
=/ img (as-octs:mimes:html (~(got by contact-png) `@ta`name))
:_ this
[ost.bol %http-response (png-response:app img)]~
::
:: main page
::
[%'~contacts' *]
:_ this
[ost.bol %http-response (html-response:app index)]~
==
::
::
:: +utilities
::
++ launch-poke
|= [pax=path =cord]
^- move
[ost.bol %poke / [our.bol %launch] [%launch-action [%contact-view pax cord]]]
--

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,167 @@
/- *contact-store
|%
++ nu :: parse number as hex
|= jon/json
?> ?=({$n *} jon)
(rash p.jon hex)
::
++ rolodex-to-json
|= rolo=rolodex
=, enjs:format
^- json
%+ frond %contact-initial
%- pairs
%+ turn ~(tap by rolo)
|= [pax=^path =contacts]
^- [cord json]
:- (spat pax)
(pairs [%contacts (contacts-to-json contacts)]~)
::
++ contacts-to-json
|= con=contacts
^- json
=, enjs:format
%- pairs
%+ turn ~(tap by con)
|= [shp=^ship =contact]
^- [cord json]
:- (crip (slag 1 (scow %p shp)))
(contact-to-json contact)
::
++ contact-to-json
|= con=contact
^- json
=, enjs:format
%- pairs
:~ [%nickname s+nickname.con]
[%email s+email.con]
[%phone s+phone.con]
[%website s+website.con]
[%notes s+notes.con]
[%color s+(scot %ux color.con)]
[%avatar s+'TODO']
==
::
++ edit-to-json
|= edit=edit-field
^- json
?- -.edit
%nickname s+nickname.edit
%email s+email.edit
%phone s+phone.edit
%website s+website.edit
%notes s+notes.edit
%color s+(scot %ux color.edit)
%avatar s+'TODO'
==
::
++ update-to-json
|= upd=contact-update
=, enjs:format
^- json
%+ frond %contact-update
%- pairs
:~
?: ?=(%create -.upd)
[%create (pairs [%path (path path.upd)]~)]
?: ?=(%delete -.upd)
[%delete (pairs [%path (path path.upd)]~)]
?: ?=(%add -.upd)
:- %add
%- pairs
:~ [%path (path path.upd)]
[%ship (ship ship.upd)]
[%contact (contact-to-json contact.upd)]
==
?: ?=(%remove -.upd)
:- %remove
%- pairs
:~ [%path (path path.upd)]
[%ship (ship ship.upd)]
==
?: ?=(%edit -.upd)
:- %edit
%- pairs
:~ [%path (path path.upd)]
[%ship (ship ship.upd)]
[%edit-field (edit-to-json edit-field.upd)]
==
[*@t *^json]
==
::
++ json-to-action
|= jon=json
^- contact-action
=, dejs:format
=< (parse-json jon)
|%
++ parse-json
%- of
:~ [%create create]
[%delete delete]
[%add add]
[%remove remove]
[%edit edit]
==
::
++ create
(ot [%path pa]~)
::
++ delete
(ot [%path pa]~)
::
++ add
%- ot
:~ [%path pa]
[%ship (su ;~(pfix sig fed:ag))]
[%contact cont]
==
::
++ remove
%- ot
:~ [%path pa]
[%ship (su ;~(pfix sig fed:ag))]
==
::
++ edit
%- ot
:~ [%path pa]
[%ship (su ;~(pfix sig fed:ag))]
[%edit-field edit-fi]
==
--
::
++ octet
%- ot:dejs:format
:~ [%p ni:dejs:format]
[%q so:dejs:format]
==
::
++ avat
%- ot:dejs:format
:~ [%content-type so:dejs:format]
[%octs octet]
==
::
++ cont
%- ot:dejs:format
:~ [%nickname so:dejs:format]
[%email so:dejs:format]
[%phone so:dejs:format]
[%website so:dejs:format]
[%notes so:dejs:format]
[%color nu]
[%avatar (mu:dejs:format avat)]
==
::
++ edit-fi
%- of:dejs:format
:~ [%nickname so:dejs:format]
[%email so:dejs:format]
[%phone so:dejs:format]
[%website so:dejs:format]
[%notes so:dejs:format]
[%color nu]
[%avatar (mu:dejs:format avat)]
==
--

View File

@ -109,6 +109,8 @@
%chat-view
%chat-cli
%soto
%contact-store
%contact-view
==
::
++ deft-fish :: default connects

View File

@ -0,0 +1,42 @@
/- *identity
|%
+$ rolodex (map path contacts)
::
+$ contacts (map ship contact)
::
+$ avatar [content-type=@t octs=[p=@ud q=@t]]
::
+$ contact
$: nickname=@t
email=@t
phone=@t
website=@t
notes=@t
color=@ux
avatar=(unit avatar)
==
::
+$ edit-field
$% [%nickname nickname=@t]
[%email email=@t]
[%phone phone=@t]
[%website website=@t]
[%notes notes=@t]
[%color color=@ux]
[%avatar avatar=(unit avatar)]
==
::
+$ contact-action
$% [%create =path]
[%delete =path]
[%add =path =ship =contact]
[%remove =path =ship]
[%edit =path =ship =edit-field]
==
::
+$ contact-update
$% [%rolodex =rolodex]
[%contacts =path =contacts]
contact-action
==
--

View File

@ -0,0 +1,183 @@
var gulp = require('gulp');
var cssimport = require('gulp-cssimport');
var rollup = require('gulp-better-rollup');
var cssnano = require('cssnano');
var postcss = require('gulp-postcss');
var sucrase = require('@sucrase/gulp-plugin');
var minify = require('gulp-minify');
var rename = require('gulp-rename');
var del = require('del');
var resolve = require('rollup-plugin-node-resolve');
var commonjs = require('rollup-plugin-commonjs');
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 = [
cssnano()
];
return gulp
.src('src/index.css')
.pipe(cssimport())
.pipe(postcss(plugins))
.pipe(gulp.dest('../../arvo/app/contacts/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' ],
}
}),
rootImport({
root: `${__dirname}/dist/js`,
useEntry: 'prepend',
extensions: '.js'
}),
globals(),
resolve()
]
}, 'umd'))
.on('error', function(e){
console.log(e);
cb();
})
.pipe(gulp.dest('../../arvo/app/contacts/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'
}),
globals(),
resolve()
]
}, 'umd'))
.on('error', function(e){
console.log(e);
cb();
})
.pipe(gulp.dest('../../arvo/app/contacts/js/'))
.on('end', cb);
});
gulp.task('js-minify', function () {
return gulp.src('../../arvo/app/contacts/js/index.js')
.pipe(minify())
.pipe(gulp.dest('../../arvo/app/contacts/js/'));
});
gulp.task('tile-js-minify', function () {
return gulp.src('../../arvo/app/contacts/js/tile.js')
.pipe(minify())
.pipe(gulp.dest('../../arvo/app/contacts/js/'));
});
gulp.task('rename-index-min', function() {
return gulp.src('../../arvo/app/contacts/js/index-min.js')
.pipe(rename('index.js'))
.pipe(gulp.dest('../../arvo/app/contacts/js/'))
});
gulp.task('rename-tile-min', function() {
return gulp.src('../../arvo/app/contacts/js/tile-min.js')
.pipe(rename('tile.js'))
.pipe(gulp.dest('../../arvo/app/contacts/js/'))});
gulp.task('clean-min', function() {
return del(['../../arvo/app/contacts/js/index-min.js', '../../arvo/app/contacts/js/tile-min.js'], {force: true})
});
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',
'clean-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'));
}));

6529
pkg/interface/contacts/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,42 @@
{
"name": "urbit-apps",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"author": "",
"license": "ISC",
"devDependencies": {
"@sucrase/gulp-plugin": "^2.0.0",
"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-node-globals": "^1.4.0",
"rollup-plugin-node-resolve": "^4.0.0",
"rollup-plugin-root-import": "^0.2.3",
"sucrase": "^3.8.0"
},
"dependencies": {
"classnames": "^2.2.6",
"del": "^5.1.0",
"lodash": "^4.17.11",
"moment": "^2.20.1",
"mousetrap": "^1.6.3",
"react": "^16.5.2",
"react-dom": "^16.8.6",
"react-router-dom": "^5.0.0",
"urbit-ob": "^4.1.2",
"urbit-sigil-js": "^1.3.2"
},
"resolutions": {
"natives": "1.1.3"
}
}

View File

@ -0,0 +1,147 @@
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;
padding: 0;
}
textarea, 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;
}
.fs-italic {
font-style: italic;
}
.td-underline {
text-decoration: underline;
}
.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;
}
.clamp-message {
max-width: calc(100% - 36px - 1.5rem);
}
.clamp-attachment {
overflow: scroll;
max-height: 10em;
max-width: 100%;
}
.lh-16 {
line-height: 16px;
}
.mono {
font-family: "Source Code Pro", monospace;
}
.label-small-mono.list-ship {
line-height: 29px;
}

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,195 @@
import React from 'react';
import ReactDOM from 'react-dom';
import _ from 'lodash';
import { uuid } from '/lib/util';
import { store } from '/store';
class UrbitApi {
setAuthTokens(authTokens) {
this.authTokens = authTokens;
this.bindPaths = [];
this.groups = {
add: this.groupAdd.bind(this),
remove: this.groupRemove.bind(this)
};
this.contacts = {
create: this.contactCreate.bind(this),
delete: this.contactDelete.bind(this),
add: this.contactAdd.bind(this),
remove: this.contactRemove.bind(this),
edit: this.contactEdit.bind(this)
};
this.invite = {
accept: this.inviteAccept.bind(this),
decline: this.inviteDecline.bind(this),
invite: this.inviteInvite.bind(this)
};
}
bind(path, method, ship = this.authTokens.ship, app, success, fail, quit) {
this.bindPaths = _.uniq([...this.bindPaths, path]);
window.subscriptionId = window.urb.subscribe(ship, app, path,
(err) => {
fail(err);
},
(event) => {
success({
data: event,
from: {
ship,
path
}
});
},
(qui) => {
quit(qui);
});
}
action(appl, mark, data) {
return new Promise((resolve, reject) => {
window.urb.poke(ship, appl, mark, data,
(json) => {
resolve(json);
},
(err) => {
reject(err);
});
});
}
addPendingMessage(msg) {
if (store.state.pendingMessages.has(msg.path)) {
store.state.pendingMessages.get(msg.path).push(msg.envelope);
} else {
store.state.pendingMessages.set(msg.path, [msg.envelope]);
}
store.setState({
pendingMessages: store.state.pendingMessages
});
}
groupsAction(data) {
this.action("group-store", "group-action", data);
}
groupAdd(members, path) {
this.groupsAction({
add: {
members, path
}
});
}
groupRemove(members, path) {
this.groupsAction({
remove: {
members, path
}
});
}
contactAction(data) {
this.action("contact-store", "json", data);
}
contactCreate(path) {
this.contactAction({ create: { path }});
}
contactDelete(path) {
this.contactAction({ delete: { path }});
}
contactAdd(path, ship, contact = {
nickname: '',
email: '',
phone: '',
website: '',
notes: '',
color: '0x000000',
avatar: null
}) {
this.contactAction({
add: {
path, ship, contact
}
});
}
contactRemove(path, ship) {
this.contactAction({
remove: {
path, ship
}
});
}
contactEdit(path, ship, editField) {
/* editField can be...
{nickname: ''}
{email: ''}
{phone: ''}
{website: ''}
{notes: ''}
{color: '0xfff'}
{avatar: null}
{avatar: {p: length, q: bytestream}}
*/
this.contactAction({
edit: {
path, ship, 'edit-field': editField
}
});
}
inviteAction(data) {
this.action("invite-store", "json", data);
}
inviteInvite(path, ship) {
this.action("invite-hook", "json",
{
invite: {
path: '/chat',
invite: {
path,
ship: `~${window.ship}`,
recipient: ship,
app: 'chat-hook',
text: `You have been invited to /${window.ship}${path}`,
},
uid: uuid()
}
}
);
}
inviteAccept(uid) {
this.inviteAction({
accept: {
path: '/chat',
uid
}
});
}
inviteDecline(uid) {
this.inviteAction({
decline: {
path: '/chat',
uid
}
});
}
}
export let api = new UrbitApi();
window.api = api;

View File

@ -0,0 +1,50 @@
import React, { Component } from 'react';
import { BrowserRouter, Route, Link } 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';
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;
return (
<BrowserRouter>
<div>
<Route exact path="/~contacts"
render={ (props) => {
return (
<Skeleton>
<div className="h-100 w-100 overflow-x-hidden flex flex-column">
<div className="pl3 pr3 pt2 pb3">
<h2>Home</h2>
</div>
</div>
</Skeleton>
);
}} />
</div>
</BrowserRouter>
)
}
}

View File

@ -0,0 +1,22 @@
import React, { Component } from 'react';
import classnames from 'classnames';
export class Skeleton extends Component {
render() {
return (
<div className="h-100 w-100 absolute">
<div className="cf w-100 absolute flex"
style={{
height: 'calc(100% - 48px)'
}}>
<div className="h-100 w-100" style={{
flexGrow: 1,
}}>
{this.props.children}
</div>
</div>
</div>
);
}
}

View File

@ -0,0 +1,63 @@
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,29 @@
import _ from 'lodash';
export class ContactUpdateReducer {
reduce(json, state) {
let data = _.get(json, 'contact-update', false);
if (data) {
this.create(data, state);
this.delete(data, state);
this.delete(data, state);
}
}
create(json, state) {
let data = _.get(json, 'create', false);
if (data) {
state.contacts[data.path] = {};
}
}
delete(json, state) {
let data = _.get(json, 'delete', false);
if (data) {
delete state.contacts[data.path];
}
}
}

View File

@ -0,0 +1,65 @@
import _ from 'lodash';
export class GroupUpdateReducer {
reduce(json, state) {
let data = _.get(json, 'group-update', false);
if (data) {
this.add(data, state);
this.remove(data, state);
this.bundle(data, state);
this.unbundle(data, state);
this.keys(data, state);
this.path(data, state);
}
}
add(json, state) {
let data = _.get(json, 'add', false);
if (data) {
for (let member of data.members) {
state.groups[data.path].add(member);
}
}
}
remove(json, state) {
let data = _.get(json, 'remove', false);
if (data) {
for (let member of data.members) {
state.groups[data.path].delete(member);
}
}
}
bundle(json, state) {
let data = _.get(json, 'bundle', false);
if (data) {
state.groups[data.path] = new Set();
}
}
unbundle(json, state) {
let data = _.get(json, 'unbundle', false);
if (data) {
delete state.groups[data.path];
}
}
keys(json, state) {
let data = _.get(json, 'keys', false);
if (data) {
state.groupKeys = new Set(data.keys);
}
}
path(json, state) {
let data = _.get(json, 'path', false);
if (data) {
state.groups[data.path] = new Set([data.members]);
}
}
}

View File

@ -0,0 +1,34 @@
import _ from 'lodash';
export class InitialReducer {
reduce(json, state) {
let data = _.get(json, 'chat-initial', false);
if (data) {
state.inbox = data;
}
data = _.get(json, 'group-initial', false);
if (data) {
for (let group in data) {
state.groups[group] = new Set(data[group]);
}
}
data = _.get(json, 'permission-initial', false);
if (data) {
for (let perm in data) {
state.permissions[perm] = {
who: new Set(data[perm].who),
kind: data[perm].kind
}
}
}
data = _.get(json, 'invite-initial', false);
if (data) {
state.invites = data;
}
}
}

View File

@ -0,0 +1,53 @@
import _ from 'lodash';
export class InviteUpdateReducer {
reduce(json, state) {
let data = _.get(json, 'invite-update', false);
if (data) {
this.create(data, state);
this.delete(data, state);
this.invite(data, state);
this.accepted(data, state);
this.decline(data, state);
}
}
create(json, state) {
let data = _.get(json, 'create', false);
if (data) {
state.invites[data.path] = {};
}
}
delete(json, state) {
let data = _.get(json, 'delete', false);
if (data) {
delete state.invites[data.path];
}
}
invite(json, state) {
let data = _.get(json, 'invite', false);
if (data) {
state.invites[data.path][data.uid] = data.invite;
}
}
accepted(json, state) {
let data = _.get(json, 'accepted', false);
if (data) {
console.log(data);
delete state.invites[data.path][data.uid];
}
}
decline(json, state) {
let data = _.get(json, 'decline', false);
if (data) {
delete state.invites[data.path][data.uid];
}
}
}

View File

@ -0,0 +1,51 @@
import _ from 'lodash';
export class PermissionUpdateReducer {
reduce(json, state) {
let data = _.get(json, 'permission-update', false);
if (data) {
this.create(data, state);
this.delete(data, state);
this.add(data, state);
this.remove(data, state);
}
}
create(json, state) {
let data = _.get(json, 'create', false);
if (data) {
state.permissions[data.path] = {
kind: data.kind,
who: new Set(data.who)
};
}
}
delete(json, state) {
let data = _.get(json, 'delete', false);
if (data) {
delete state.permissions[data.path];
}
}
add(json, state) {
let data = _.get(json, 'add', false);
if (data) {
for (let member of data.who) {
state.permissions[data.path].who.add(member);
}
}
}
remove(json, state) {
let data = _.get(json, 'remove', false);
if (data) {
for (let member of data.who) {
state.permissions[data.path].who.delete(member);
}
}
}
}

View File

@ -0,0 +1,45 @@
import { InitialReducer } from '/reducers/initial';
import { ContactUpdateReducer } from '/reducers/contact-update';
import { GroupUpdateReducer } from '/reducers/group-update';
import { InviteUpdateReducer } from '/reducers/invite-update';
import { PermissionUpdateReducer } from '/reducers/permission-update';
class Store {
constructor() {
this.state = {
contacts: {},
groups: {},
permissions: {},
invites: {},
spinner: false
};
this.initialReducer = new InitialReducer();
this.groupUpdateReducer = new GroupUpdateReducer();
this.permissionUpdateReducer = new PermissionUpdateReducer();
this.contactUpdateReducer = new ContactUpdateReducer();
this.inviteUpdateReducer = new InviteUpdateReducer();
this.setState = () => {};
}
setStateHandler(setState) {
this.setState = setState;
}
handleEvent(data) {
let json = data.data;
console.log(json);
this.initialReducer.reduce(json, this.state);
this.groupUpdateReducer.reduce(json, this.state);
this.permissionUpdateReducer.reduce(json, this.state);
this.contactUpdateReducer.reduce(json, this.state);
this.inviteUpdateReducer.reduce(json, this.state);
this.setState(this.state);
}
}
export let store = new Store();
window.store = store;

View File

@ -0,0 +1,64 @@
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-view',
this.handleEvent.bind(this),
this.handleError.bind(this),
this.handleQuitAndResubscribe.bind(this));
api.bind('/primary', 'PUT', api.authTokens.ship, 'invite-view',
this.handleEvent.bind(this),
this.handleError.bind(this),
this.handleQuitAndResubscribe.bind(this));
api.bind('/all', 'PUT', api.authTokens.ship, 'group-store',
this.handleEvent.bind(this),
this.handleError.bind(this),
this.handleQuitAndResubscribe.bind(this));
api.bind('/all', 'PUT', api.authTokens.ship, 'permission-store',
this.handleEvent.bind(this),
this.handleError.bind(this),
this.handleQuitAndResubscribe.bind(this));
}
handleEvent(diff) {
store.handleEvent(diff);
}
handleError(err) {
console.error(err);
}
handleQuitSilently(quit) {
// no-op
}
handleQuitAndResubscribe(quit) {
// TODO: resubscribe
}
fetchMessages(start, end, path) {
console.log(start, end, path);
fetch(`/~chat/paginate/${start}/${end}${path}`)
.then((response) => response.json())
.then((json) => {
store.handleEvent({
data: json
});
});
}
}
export let subscription = new Subscription();

View File

@ -0,0 +1,31 @@
import React, { Component } from 'react';
import classnames from 'classnames';
import _ from 'lodash';
export default class ContactTile extends Component {
render() {
const { props } = this;
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}}>
Contacts
</p>
<img
className="absolute"
style={{ left: 68, top: 65 }}
src="/~chat/img/Tile.png"
width={106}
height={98} />
</a>
</div>
);
}
}
window['contact-viewTile'] = ContactTile;