mirror of
https://github.com/ilyakooo0/urbit.git
synced 2024-12-01 03:23:09 +03:00
Merge remote-tracking branch 'origin/master' into HEAD
This commit is contained in:
commit
89cde4a272
7
.gitignore
vendored
7
.gitignore
vendored
@ -9,3 +9,10 @@ tags
|
||||
TAGS
|
||||
cross/
|
||||
release/
|
||||
**/.DS_Store
|
||||
**/dist
|
||||
**/node_modules
|
||||
**/urbitrc
|
||||
**/*.swp
|
||||
**/*.swo
|
||||
**/*-min.js
|
||||
|
3
Makefile
3
Makefile
@ -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-*
|
||||
|
@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:42d7fae92d11b3dbc3ee9aaa1e2edc5328f308c047dcf126b6cd661862904782
|
||||
size 4352384
|
||||
oid sha256:0e520b9ab0232d1765e1dacde96a1210845768e7334a334b5705d1c40348c82b
|
||||
size 4464201
|
||||
|
@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:b4804169319b8f69948f980f5212c9ff7caa39b2803b875e394ce1cf181653e7
|
||||
size 9523484
|
||||
oid sha256:a4a8e1daf0bfe86d5fc4ef7060b0c6a6c2678a344787926f14bb4b8cfabe8752
|
||||
size 9549390
|
||||
|
@ -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;
|
||||
|
@ -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
@ -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)
|
||||
::
|
||||
|
@ -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
14
pkg/arvo/gen/dns-collector/complete.hoon
Normal file
14
pkg/arvo/gen/dns-collector/complete.hoon
Normal 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]
|
26
pkg/arvo/gen/reload-event.hoon
Normal file
26
pkg/arvo/gen/reload-event.hoon
Normal 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]
|
@ -1,4 +1,3 @@
|
||||
!:
|
||||
:::::: ::::::::::::::::::::::::::::::::::::::::::::::::::::::
|
||||
:::::: :::::: Postface ::::::
|
||||
:::::: ::::::::::::::::::::::::::::::::::::::::::::::::::::::
|
||||
|
@ -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)
|
||||
|
@ -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
84
pkg/interface/.gitignore
vendored
Normal 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/*
|
196
pkg/interface/chat/gulpfile.js
Normal file
196
pkg/interface/chat/gulpfile.js
Normal 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
6821
pkg/interface/chat/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
46
pkg/interface/chat/package.json
Normal file
46
pkg/interface/chat/package.json
Normal 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"
|
||||
}
|
||||
}
|
120
pkg/interface/chat/src/css/custom.css
Normal file
120
pkg/interface/chat/src/css/custom.css
Normal 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;
|
||||
}
|
63
pkg/interface/chat/src/css/fonts.css
Normal file
63
pkg/interface/chat/src/css/fonts.css
Normal file
@ -0,0 +1,63 @@
|
||||
@font-face {
|
||||
font-family: 'Inter';
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
src: url("https://media.urbit.org/fonts/Inter-Regular.woff2") format("woff2");
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: 'Inter';
|
||||
font-style: italic;
|
||||
font-weight: 400;
|
||||
src: url("https://media.urbit.org/fonts/Inter-Italic.woff2") format("woff2");
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: 'Inter';
|
||||
font-style: normal;
|
||||
font-weight: 700;
|
||||
src: url("https://media.urbit.org/fonts/Inter-Bold.woff2") format("woff2");
|
||||
}
|
||||
@font-face {
|
||||
font-family: 'Inter';
|
||||
font-style: italic;
|
||||
font-weight: 700;
|
||||
src: url("https://media.urbit.org/fonts/Inter-BoldItalic.woff2") format("woff2");
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: "Source Code Pro";
|
||||
src: url("https://storage.googleapis.com/media.urbit.org/fonts/scp-extralight.woff");
|
||||
font-weight: 200;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: "Source Code Pro";
|
||||
src: url("https://storage.googleapis.com/media.urbit.org/fonts/scp-light.woff");
|
||||
font-weight: 300;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: "Source Code Pro";
|
||||
src: url("https://storage.googleapis.com/media.urbit.org/fonts/scp-regular.woff");
|
||||
font-weight: 400;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: "Source Code Pro";
|
||||
src: url("https://storage.googleapis.com/media.urbit.org/fonts/scp-medium.woff");
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: "Source Code Pro";
|
||||
src: url("https://storage.googleapis.com/media.urbit.org/fonts/scp-semibold.woff");
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: "Source Code Pro";
|
||||
src: url("https://storage.googleapis.com/media.urbit.org/fonts/scp-bold.woff");
|
||||
font-weight: 700;
|
||||
}
|
||||
|
38
pkg/interface/chat/src/css/spinner.css
Normal file
38
pkg/interface/chat/src/css/spinner.css
Normal 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;
|
||||
}
|
||||
|
2
pkg/interface/chat/src/css/tachyons.css
Normal file
2
pkg/interface/chat/src/css/tachyons.css
Normal file
File diff suppressed because one or more lines are too long
5
pkg/interface/chat/src/index.css
Normal file
5
pkg/interface/chat/src/index.css
Normal file
@ -0,0 +1,5 @@
|
||||
@import "css/tachyons.css";
|
||||
@import "css/fonts.css";
|
||||
@import "css/spinner.css";
|
||||
@import "css/custom.css";
|
||||
|
16
pkg/interface/chat/src/index.js
Normal file
16
pkg/interface/chat/src/index.js
Normal file
@ -0,0 +1,16 @@
|
||||
import React from 'react';
|
||||
import ReactDOM from 'react-dom';
|
||||
import { Root } from '/components/root';
|
||||
import { api } from '/api';
|
||||
import { store } from '/store';
|
||||
import { subscription } from "/subscription";
|
||||
|
||||
api.setAuthTokens({
|
||||
ship: window.ship
|
||||
});
|
||||
|
||||
subscription.start();
|
||||
|
||||
ReactDOM.render((
|
||||
<Root />
|
||||
), document.querySelectorAll("#root")[0]);
|
145
pkg/interface/chat/src/js/api.js
Normal file
145
pkg/interface/chat/src/js/api.js
Normal 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;
|
229
pkg/interface/chat/src/js/components/chat.js
Normal file
229
pkg/interface/chat/src/js/components/chat.js
Normal 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>
|
||||
)
|
||||
}
|
||||
}
|
60
pkg/interface/chat/src/js/components/landing.js
Normal file
60
pkg/interface/chat/src/js/components/landing.js
Normal 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>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
178
pkg/interface/chat/src/js/components/lib/chat-input.js
Normal file
178
pkg/interface/chat/src/js/components/lib/chat-input.js
Normal 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>
|
||||
);
|
||||
}
|
||||
}
|
59
pkg/interface/chat/src/js/components/lib/chat-tabbar.js
Normal file
59
pkg/interface/chat/src/js/components/lib/chat-tabbar.js
Normal 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>
|
||||
);
|
||||
}
|
||||
}
|
32
pkg/interface/chat/src/js/components/lib/header-bar.js
Normal file
32
pkg/interface/chat/src/js/components/lib/header-bar.js
Normal 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>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -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} />
|
||||
);
|
||||
}
|
||||
}
|
@ -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} />
|
||||
);
|
||||
}
|
||||
}
|
@ -0,0 +1,9 @@
|
||||
import React, { Component } from 'react';
|
||||
|
||||
export class IconSpinner extends Component {
|
||||
render() {
|
||||
return (
|
||||
<div className="spinner-pending"></div>
|
||||
);
|
||||
}
|
||||
}
|
34
pkg/interface/chat/src/js/components/lib/icons/sigil.js
Normal file
34
pkg/interface/chat/src/js/components/lib/icons/sigil.js
Normal 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>
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
50
pkg/interface/chat/src/js/components/lib/member-element.js
Normal file
50
pkg/interface/chat/src/js/components/lib/member-element.js
Normal 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>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
97
pkg/interface/chat/src/js/components/lib/message.js
Normal file
97
pkg/interface/chat/src/js/components/lib/message.js
Normal 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>
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
|
120
pkg/interface/chat/src/js/components/lib/sidebar-invite.js
Normal file
120
pkg/interface/chat/src/js/components/lib/sidebar-invite.js
Normal 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
|
||||
<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>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
38
pkg/interface/chat/src/js/components/lib/sidebar-item.js
Normal file
38
pkg/interface/chat/src/js/components/lib/sidebar-item.js
Normal 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>
|
||||
)
|
||||
}
|
||||
}
|
135
pkg/interface/chat/src/js/components/member.js
Normal file
135
pkg/interface/chat/src/js/components/member.js
Normal 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>
|
||||
)
|
||||
}
|
||||
}
|
206
pkg/interface/chat/src/js/components/new.js
Normal file
206
pkg/interface/chat/src/js/components/new.js
Normal 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>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
250
pkg/interface/chat/src/js/components/root.js
Normal file
250
pkg/interface/chat/src/js/components/root.js
Normal 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>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
127
pkg/interface/chat/src/js/components/settings.js
Normal file
127
pkg/interface/chat/src/js/components/settings.js
Normal 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>
|
||||
)
|
||||
}
|
||||
}
|
163
pkg/interface/chat/src/js/components/sidebar.js
Normal file
163
pkg/interface/chat/src/js/components/sidebar.js
Normal 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>
|
||||
)
|
||||
}
|
||||
}
|
28
pkg/interface/chat/src/js/components/skeleton.js
Normal file
28
pkg/interface/chat/src/js/components/skeleton.js
Normal 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>
|
||||
);
|
||||
}
|
||||
}
|
64
pkg/interface/chat/src/js/lib/util.js
Normal file
64
pkg/interface/chat/src/js/lib/util.js
Normal 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('~', '');
|
||||
}
|
||||
|
16
pkg/interface/chat/src/js/reducers/config.js
Normal file
16
pkg/interface/chat/src/js/reducers/config.js
Normal 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 || {};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
16
pkg/interface/chat/src/js/reducers/initial.js
Normal file
16
pkg/interface/chat/src/js/reducers/initial.js
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
83
pkg/interface/chat/src/js/reducers/update.js
Normal file
83
pkg/interface/chat/src/js/reducers/update.js
Normal 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];
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
40
pkg/interface/chat/src/js/store.js
Normal file
40
pkg/interface/chat/src/js/store.js
Normal 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;
|
44
pkg/interface/chat/src/js/subscription.js
Normal file
44
pkg/interface/chat/src/js/subscription.js
Normal 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();
|
109
pkg/interface/chat/tile/tile.js
Normal file
109
pkg/interface/chat/tile/tile.js
Normal 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
63
pkg/interface/clock/.gitignore
vendored
Normal file
@ -0,0 +1,63 @@
|
||||
# Logs
|
||||
logs
|
||||
*.log
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
*.swp
|
||||
.DS_Store
|
||||
|
||||
# Runtime data
|
||||
pids
|
||||
*.pid
|
||||
*.seed
|
||||
*.pid.lock
|
||||
|
||||
# Directory for instrumented libs generated by jscoverage/JSCover
|
||||
lib-cov
|
||||
|
||||
# Coverage directory used by tools like istanbul
|
||||
coverage
|
||||
|
||||
# nyc test coverage
|
||||
.nyc_output
|
||||
|
||||
# Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files)
|
||||
.grunt
|
||||
|
||||
# Bower dependency directory (https://bower.io/)
|
||||
bower_components
|
||||
|
||||
# node-waf configuration
|
||||
.lock-wscript
|
||||
|
||||
# Compiled binary addons (https://nodejs.org/api/addons.html)
|
||||
build/Release
|
||||
|
||||
# Dependency directories
|
||||
node_modules/
|
||||
jspm_packages/
|
||||
|
||||
# TypeScript v1 declaration files
|
||||
typings/
|
||||
|
||||
# Optional npm cache directory
|
||||
.npm
|
||||
|
||||
# Optional eslint cache
|
||||
.eslintcache
|
||||
|
||||
# Optional REPL history
|
||||
.node_repl_history
|
||||
|
||||
# Output of 'npm pack'
|
||||
*.tgz
|
||||
|
||||
# Yarn Integrity file
|
||||
.yarn-integrity
|
||||
|
||||
# dotenv environment variables file
|
||||
.env
|
||||
|
||||
# next.js build output
|
||||
.next
|
21
pkg/interface/clock/LICENSE
Normal file
21
pkg/interface/clock/LICENSE
Normal 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.
|
10
pkg/interface/clock/README.md
Normal file
10
pkg/interface/clock/README.md
Normal 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.
|
144
pkg/interface/clock/gulpfile.js
Normal file
144
pkg/interface/clock/gulpfile.js
Normal 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
6599
pkg/interface/clock/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
45
pkg/interface/clock/package.json
Normal file
45
pkg/interface/clock/package.json
Normal 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"
|
||||
}
|
||||
}
|
204
pkg/interface/clock/tile/tile.js
Normal file
204
pkg/interface/clock/tile/tile.js
Normal 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;
|
5
pkg/interface/clock/urbitrc-sample
Normal file
5
pkg/interface/clock/urbitrc-sample
Normal 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
9
pkg/interface/launch/.gitignore
vendored
Normal file
@ -0,0 +1,9 @@
|
||||
.DS_Store
|
||||
|
||||
.urbitrc
|
||||
|
||||
node_modules/
|
||||
dist/
|
||||
urbit-code/web/landscape/js/*
|
||||
urbit-code/web/landscape/css/*
|
||||
*.swp
|
5
pkg/interface/launch/.urbitrc-sample
Normal file
5
pkg/interface/launch/.urbitrc-sample
Normal file
@ -0,0 +1,5 @@
|
||||
module.exports = {
|
||||
URBIT_PIERS: [
|
||||
"/Users/bono/urbit/piers/zod/home"
|
||||
]
|
||||
};
|
146
pkg/interface/launch/gulpfile.js
Normal file
146
pkg/interface/launch/gulpfile.js
Normal 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
6486
pkg/interface/launch/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
45
pkg/interface/launch/package.json
Normal file
45
pkg/interface/launch/package.json
Normal 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"
|
||||
}
|
||||
}
|
74
pkg/interface/launch/src/css/custom.css
Normal file
74
pkg/interface/launch/src/css/custom.css
Normal 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;
|
||||
}
|
63
pkg/interface/launch/src/css/fonts.css
Normal file
63
pkg/interface/launch/src/css/fonts.css
Normal file
@ -0,0 +1,63 @@
|
||||
@font-face {
|
||||
font-family: 'Inter';
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
src: url("https://media.urbit.org/fonts/Inter-Regular.woff2") format("woff2");
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: 'Inter';
|
||||
font-style: italic;
|
||||
font-weight: 400;
|
||||
src: url("https://media.urbit.org/fonts/Inter-Italic.woff2") format("woff2");
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: 'Inter';
|
||||
font-style: normal;
|
||||
font-weight: 700;
|
||||
src: url("https://media.urbit.org/fonts/Inter-Bold.woff2") format("woff2");
|
||||
}
|
||||
@font-face {
|
||||
font-family: 'Inter';
|
||||
font-style: italic;
|
||||
font-weight: 700;
|
||||
src: url("https://media.urbit.org/fonts/Inter-BoldItalic.woff2") format("woff2");
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: "Source Code Pro";
|
||||
src: url("https://storage.googleapis.com/media.urbit.org/fonts/scp-extralight.woff");
|
||||
font-weight: 200;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: "Source Code Pro";
|
||||
src: url("https://storage.googleapis.com/media.urbit.org/fonts/scp-light.woff");
|
||||
font-weight: 300;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: "Source Code Pro";
|
||||
src: url("https://storage.googleapis.com/media.urbit.org/fonts/scp-regular.woff");
|
||||
font-weight: 400;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: "Source Code Pro";
|
||||
src: url("https://storage.googleapis.com/media.urbit.org/fonts/scp-medium.woff");
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: "Source Code Pro";
|
||||
src: url("https://storage.googleapis.com/media.urbit.org/fonts/scp-semibold.woff");
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: "Source Code Pro";
|
||||
src: url("https://storage.googleapis.com/media.urbit.org/fonts/scp-bold.woff");
|
||||
font-weight: 700;
|
||||
}
|
||||
|
2
pkg/interface/launch/src/css/tachyons.css
Normal file
2
pkg/interface/launch/src/css/tachyons.css
Normal file
File diff suppressed because one or more lines are too long
4
pkg/interface/launch/src/index.css
Normal file
4
pkg/interface/launch/src/index.css
Normal file
@ -0,0 +1,4 @@
|
||||
@import 'css/tachyons.css';
|
||||
@import 'css/fonts.css';
|
||||
@import 'css/custom.css';
|
||||
|
13
pkg/interface/launch/src/index.js
Normal file
13
pkg/interface/launch/src/index.js
Normal file
@ -0,0 +1,13 @@
|
||||
import React from 'react';
|
||||
import ReactDOM from 'react-dom';
|
||||
|
||||
import { subscription } from "/subscription";
|
||||
import App from '/app';
|
||||
|
||||
|
||||
subscription.setAuthTokens({
|
||||
ship: window.ship
|
||||
});
|
||||
|
||||
ReactDOM.render(<App />, document.querySelectorAll("#root")[0]);
|
||||
|
38
pkg/interface/launch/src/js/app.js
Normal file
38
pkg/interface/launch/src/js/app.js
Normal file
@ -0,0 +1,38 @@
|
||||
import React, { Component } from 'react';
|
||||
import { BrowserRouter, Route } from "react-router-dom";
|
||||
|
||||
import { store } from '/store';
|
||||
import Home from '/components/home';
|
||||
|
||||
export default class App extends Component {
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
this.state = {
|
||||
};
|
||||
|
||||
store.setStateHandler(this.setState.bind(this));
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<BrowserRouter>
|
||||
<div>
|
||||
<Route exact path="/"
|
||||
render={ (props) => {
|
||||
return (
|
||||
<Home
|
||||
{...props}
|
||||
data={this.state}
|
||||
keys={new Set(Object.keys(this.state))}
|
||||
/>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</BrowserRouter>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
window.app = App;
|
46
pkg/interface/launch/src/js/components/dropdown.js
Normal file
46
pkg/interface/launch/src/js/components/dropdown.js
Normal file
@ -0,0 +1,46 @@
|
||||
import React, { Component } from 'react';
|
||||
import { subscription } from '/subscription';
|
||||
import { api } from '/lib/api';
|
||||
import classnames from 'classnames';
|
||||
|
||||
let style = {
|
||||
circle: {
|
||||
width: '2em',
|
||||
height: '2em',
|
||||
background: '#000000',
|
||||
border: '4px solid #333333',
|
||||
'borderRadius': '2em'
|
||||
},
|
||||
triangle: {
|
||||
width: '0px',
|
||||
height: '0px',
|
||||
'borderTop': '8px solid #FFFFFF',
|
||||
'borderLeft': '8px solid transparent',
|
||||
'borderRight': '8px solid transparent',
|
||||
'fontSize': 0,
|
||||
'lineHeight': 0,
|
||||
'marginLeft': 'auto',
|
||||
'marginRight': 'auto',
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
export default class Dropdown extends Component {
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<div>
|
||||
<div className="ml2" style={style.circle}>
|
||||
<div className="mt2" style={style.triangle}></div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
55
pkg/interface/launch/src/js/components/header.js
Normal file
55
pkg/interface/launch/src/js/components/header.js
Normal 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>
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
|
36
pkg/interface/launch/src/js/components/home.js
Normal file
36
pkg/interface/launch/src/js/components/home.js
Normal 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>
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
|
27
pkg/interface/launch/src/js/components/tile.js
Normal file
27
pkg/interface/launch/src/js/components/tile.js
Normal file
@ -0,0 +1,27 @@
|
||||
import React, { Component } from 'react';
|
||||
import { subscription } from '/subscription';
|
||||
import { api } from '/lib/api';
|
||||
import classnames from 'classnames';
|
||||
|
||||
export default class Tile extends Component {
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
}
|
||||
|
||||
render() {
|
||||
let SpecificTile = window[this.props.type + 'Tile'];
|
||||
|
||||
return (
|
||||
<div className="fl ma2 bg-white overflow-hidden"
|
||||
style={{ height: '234px', width: '234px' }}>
|
||||
{ !!SpecificTile ?
|
||||
<SpecificTile data={this.props.data} />
|
||||
: <div></div>
|
||||
}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
|
37
pkg/interface/launch/src/js/lib/api.js
Normal file
37
pkg/interface/launch/src/js/lib/api.js
Normal file
@ -0,0 +1,37 @@
|
||||
|
||||
class Api {
|
||||
bind(app, path, success, fail, ship) {
|
||||
window.urb.subscribe(ship, app, path,
|
||||
(err) => {
|
||||
fail(err, app, path, ship);
|
||||
},
|
||||
(event) => {
|
||||
success({
|
||||
data: event,
|
||||
from: {
|
||||
app,
|
||||
ship,
|
||||
path
|
||||
}
|
||||
});
|
||||
},
|
||||
(err) => {
|
||||
fail(err, app, path, ship);
|
||||
});
|
||||
}
|
||||
|
||||
action(appl, mark, data) {
|
||||
return new Promise((resolve, reject) => {
|
||||
window.urb.poke(ship, appl, mark, data,
|
||||
(json) => {
|
||||
resolve(json);
|
||||
},
|
||||
(err) => {
|
||||
reject(err);
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export let api = new Api();
|
||||
window.api = api;
|
12
pkg/interface/launch/src/js/lib/util.js
Normal file
12
pkg/interface/launch/src/js/lib/util.js
Normal file
@ -0,0 +1,12 @@
|
||||
export function daToDate(st) {
|
||||
var dub = function(n) {
|
||||
return parseInt(n) < 10 ? "0" + parseInt(n) : n.toString();
|
||||
};
|
||||
var da = st.split('..');
|
||||
var bigEnd = da[0].split('.');
|
||||
var lilEnd = da[1].split('.');
|
||||
var ds = `${bigEnd[0].slice(1)}-${dub(bigEnd[1])}-${dub(bigEnd[2])}T${dub(lilEnd[0])}:${dub(lilEnd[1])}:${dub(lilEnd[2])}Z`;
|
||||
return new Date(ds);
|
||||
}
|
||||
|
||||
|
20
pkg/interface/launch/src/js/store.js
Normal file
20
pkg/interface/launch/src/js/store.js
Normal file
@ -0,0 +1,20 @@
|
||||
|
||||
class Store {
|
||||
constructor() {
|
||||
this.state = {};
|
||||
this.setState = () => {};
|
||||
}
|
||||
|
||||
setStateHandler(setState) {
|
||||
this.setState = setState;
|
||||
}
|
||||
|
||||
handleEvent(data) {
|
||||
let json = data.data;
|
||||
|
||||
this.setState(json);
|
||||
}
|
||||
}
|
||||
|
||||
export let store = new Store();
|
||||
window.store = store;
|
44
pkg/interface/launch/src/js/subscription.js
Normal file
44
pkg/interface/launch/src/js/subscription.js
Normal file
@ -0,0 +1,44 @@
|
||||
import _ from 'lodash';
|
||||
import { api } from '/lib/api';
|
||||
import { store } from '/store';
|
||||
|
||||
|
||||
export class Subscription {
|
||||
|
||||
constructor() {
|
||||
this.bindPaths = [];
|
||||
this.authTokens = null;
|
||||
}
|
||||
|
||||
setAuthTokens(authTokens) {
|
||||
this.authTokens = authTokens;
|
||||
}
|
||||
|
||||
subscribe(path, ship = this.authTokens.ship) {
|
||||
let bindPaths = _.uniq([...this.bindPaths, path]);
|
||||
if (bindPaths.length == this.bindPaths.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.bindPaths = bindPaths;
|
||||
|
||||
api.bind("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;
|
204
pkg/interface/publish/gulpfile.js
Normal file
204
pkg/interface/publish/gulpfile.js
Normal 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
6603
pkg/interface/publish/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
46
pkg/interface/publish/package.json
Normal file
46
pkg/interface/publish/package.json
Normal 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"
|
||||
}
|
||||
}
|
275
pkg/interface/publish/src/css/custom.css
Normal file
275
pkg/interface/publish/src/css/custom.css
Normal 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;
|
||||
}
|
63
pkg/interface/publish/src/css/fonts.css
Normal file
63
pkg/interface/publish/src/css/fonts.css
Normal file
@ -0,0 +1,63 @@
|
||||
@font-face {
|
||||
font-family: 'Inter';
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
src: url("https://media.urbit.org/fonts/Inter-Regular.woff2") format("woff2");
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: 'Inter';
|
||||
font-style: italic;
|
||||
font-weight: 400;
|
||||
src: url("https://media.urbit.org/fonts/Inter-Italic.woff2") format("woff2");
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: 'Inter';
|
||||
font-style: normal;
|
||||
font-weight: 700;
|
||||
src: url("https://media.urbit.org/fonts/Inter-Bold.woff2") format("woff2");
|
||||
}
|
||||
@font-face {
|
||||
font-family: 'Inter';
|
||||
font-style: italic;
|
||||
font-weight: 700;
|
||||
src: url("https://media.urbit.org/fonts/Inter-BoldItalic.woff2") format("woff2");
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: "Source Code Pro";
|
||||
src: url("https://storage.googleapis.com/media.urbit.org/fonts/scp-extralight.woff");
|
||||
font-weight: 200;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: "Source Code Pro";
|
||||
src: url("https://storage.googleapis.com/media.urbit.org/fonts/scp-light.woff");
|
||||
font-weight: 300;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: "Source Code Pro";
|
||||
src: url("https://storage.googleapis.com/media.urbit.org/fonts/scp-regular.woff");
|
||||
font-weight: 400;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: "Source Code Pro";
|
||||
src: url("https://storage.googleapis.com/media.urbit.org/fonts/scp-medium.woff");
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: "Source Code Pro";
|
||||
src: url("https://storage.googleapis.com/media.urbit.org/fonts/scp-semibold.woff");
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: "Source Code Pro";
|
||||
src: url("https://storage.googleapis.com/media.urbit.org/fonts/scp-bold.woff");
|
||||
font-weight: 700;
|
||||
}
|
||||
|
40
pkg/interface/publish/src/css/spinner.css
Normal file
40
pkg/interface/publish/src/css/spinner.css
Normal 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;
|
||||
}
|
2
pkg/interface/publish/src/css/tachyons.css
Normal file
2
pkg/interface/publish/src/css/tachyons.css
Normal file
File diff suppressed because one or more lines are too long
5
pkg/interface/publish/src/index.css
Normal file
5
pkg/interface/publish/src/index.css
Normal file
@ -0,0 +1,5 @@
|
||||
@import 'css/tachyons.css';
|
||||
@import 'css/fonts.css';
|
||||
@import 'css/custom.css';
|
||||
@import 'css/spinner.css';
|
||||
|
33
pkg/interface/publish/src/index.js
Normal file
33
pkg/interface/publish/src/index.js
Normal 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]);
|
48
pkg/interface/publish/src/js/api.js
Normal file
48
pkg/interface/publish/src/js/api.js
Normal 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;
|
369
pkg/interface/publish/src/js/components/blog.js
Normal file
369
pkg/interface/publish/src/js/components/blog.js
Normal 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>
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
90
pkg/interface/publish/src/js/components/lib/blog-data.js
Normal file
90
pkg/interface/publish/src/js/components/lib/blog-data.js
Normal 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>
|
||||
);
|
||||
}
|
||||
}
|
50
pkg/interface/publish/src/js/components/lib/blog-notes.js
Normal file
50
pkg/interface/publish/src/js/components/lib/blog-notes.js
Normal 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>
|
||||
);
|
||||
}
|
||||
}
|
122
pkg/interface/publish/src/js/components/lib/blog-settings.js
Normal file
122
pkg/interface/publish/src/js/components/lib/blog-settings.js
Normal 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>
|
||||
);
|
||||
}
|
||||
}
|
140
pkg/interface/publish/src/js/components/lib/blog-subs.js
Normal file
140
pkg/interface/publish/src/js/components/lib/blog-subs.js
Normal 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
Loading…
Reference in New Issue
Block a user