mirror of
https://github.com/ilyakooo0/urbit.git
synced 2024-11-28 03:00:15 +03:00
Add 'pkg/interface/' from commit 'ae564f567fccff9413b63158ee821b36652d6b53'
git-subtree-dir: pkg/interface git-subtree-mainline:7ce50ad75e
git-subtree-split:ae564f567f
This commit is contained in:
commit
f293a76db5
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/*
|
21
pkg/interface/LICENSE
Normal file
21
pkg/interface/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.
|
25
pkg/interface/README.md
Normal file
25
pkg/interface/README.md
Normal file
@ -0,0 +1,25 @@
|
||||
# Home
|
||||
|
||||
Create a `.urbitrc` file in this directory like so:
|
||||
|
||||
```
|
||||
module.exports = {
|
||||
URBIT_PIERS: [
|
||||
"/path/to/fakezod/home"
|
||||
]
|
||||
};
|
||||
```
|
||||
|
||||
You'll need `npm` installed (we recommend using [NVM](https://github.com/creationix/nvm) with node version 10.13.0)
|
||||
|
||||
Then:
|
||||
|
||||
```
|
||||
npm install
|
||||
npm install -g gulp-cli
|
||||
gulp watch
|
||||
```
|
||||
|
||||
Whenever you change some Home source code, this will recompile the code and
|
||||
copy the updated version into your fakezod pier. Visit localhost:80 to launch Home (or whichever port is printed out to your terminal upon booting your ship).
|
||||
|
181
pkg/interface/apps/chat/gulpfile.js
Normal file
181
pkg/interface/apps/chat/gulpfile.js
Normal file
@ -0,0 +1,181 @@
|
||||
var gulp = require('gulp');
|
||||
var cssimport = require('gulp-cssimport');
|
||||
var rollup = require('gulp-better-rollup');
|
||||
var sucrase = require('@sucrase/gulp-plugin');
|
||||
var minify = require('gulp-minify');
|
||||
var exec = require('child_process').exec;
|
||||
|
||||
var resolve = require('rollup-plugin-node-resolve');
|
||||
var commonjs = require('rollup-plugin-commonjs');
|
||||
var replace = require('rollup-plugin-replace');
|
||||
var json = require('rollup-plugin-json');
|
||||
var builtins = require('@joseph184/rollup-plugin-node-builtins');
|
||||
var rootImport = require('rollup-plugin-root-import');
|
||||
var globals = require('rollup-plugin-node-globals');
|
||||
|
||||
/***
|
||||
Main config options
|
||||
***/
|
||||
|
||||
var urbitrc = require('./.urbitrc');
|
||||
|
||||
/***
|
||||
End main config options
|
||||
***/
|
||||
|
||||
gulp.task('css-bundle', function() {
|
||||
return gulp
|
||||
.src('src/index.css')
|
||||
.pipe(cssimport())
|
||||
.pipe(gulp.dest('./urbit/app/chat/css'));
|
||||
});
|
||||
|
||||
gulp.task('jsx-transform', function(cb) {
|
||||
return gulp.src('src/**/*.js')
|
||||
.pipe(sucrase({
|
||||
transforms: ['jsx']
|
||||
}))
|
||||
.pipe(gulp.dest('dist'));
|
||||
});
|
||||
|
||||
gulp.task('tile-jsx-transform', function(cb) {
|
||||
return gulp.src('tile/**/*.js')
|
||||
.pipe(sucrase({
|
||||
transforms: ['jsx']
|
||||
}))
|
||||
.pipe(gulp.dest('dist'));
|
||||
});
|
||||
|
||||
gulp.task('js-imports', function(cb) {
|
||||
return gulp.src('dist/index.js')
|
||||
.pipe(rollup({
|
||||
plugins: [
|
||||
commonjs({
|
||||
namedExports: {
|
||||
'node_modules/react/index.js': [ 'Component' ],
|
||||
'node_modules/react-is/index.js': [ 'isValidElementType' ],
|
||||
}
|
||||
}),
|
||||
replace({
|
||||
'process.env.NODE_ENV': JSON.stringify('development')
|
||||
}),
|
||||
rootImport({
|
||||
root: `${__dirname}/dist/js`,
|
||||
useEntry: 'prepend',
|
||||
extensions: '.js'
|
||||
}),
|
||||
json(),
|
||||
globals(),
|
||||
builtins(),
|
||||
resolve()
|
||||
]
|
||||
}, 'umd'))
|
||||
.on('error', function(e){
|
||||
console.log(e);
|
||||
cb();
|
||||
})
|
||||
.pipe(gulp.dest('./urbit/app/chat/js/'))
|
||||
.on('end', cb);
|
||||
});
|
||||
|
||||
gulp.task('tile-js-imports', function(cb) {
|
||||
return gulp.src('dist/tile.js')
|
||||
.pipe(rollup({
|
||||
plugins: [
|
||||
commonjs({
|
||||
namedExports: {
|
||||
'node_modules/react/index.js': [ 'Component' ],
|
||||
}
|
||||
}),
|
||||
rootImport({
|
||||
root: `${__dirname}/dist/js`,
|
||||
useEntry: 'prepend',
|
||||
extensions: '.js'
|
||||
}),
|
||||
json(),
|
||||
globals(),
|
||||
builtins(),
|
||||
resolve()
|
||||
]
|
||||
}, 'umd'))
|
||||
.on('error', function(e){
|
||||
console.log(e);
|
||||
cb();
|
||||
})
|
||||
.pipe(gulp.dest('./urbit/app/chat/js/'))
|
||||
.on('end', cb);
|
||||
});
|
||||
|
||||
|
||||
gulp.task('js-minify', function () {
|
||||
return gulp.src('./urbit/app/chat/js/index.js')
|
||||
.pipe(minify())
|
||||
.pipe(gulp.dest('./urbit/app/chat/js/'));
|
||||
});
|
||||
|
||||
gulp.task('tile-js-minify', function () {
|
||||
return gulp.src('./urbit/app/chat/js/tile.js')
|
||||
.pipe(minify())
|
||||
.pipe(gulp.dest('./urbit/app/chat/js/'));
|
||||
});
|
||||
|
||||
gulp.task('js-cachebust', function(cb) {
|
||||
return Promise.resolve(
|
||||
exec('git log', function (err, stdout, stderr) {
|
||||
let firstLine = stdout.split("\n")[0];
|
||||
let commitHash = firstLine.split(' ')[1].substr(0, 10);
|
||||
let newFilename = "index-" + commitHash + "-min.js";
|
||||
|
||||
exec('mv ./urbit/app/chat/js/index-min.js ./urbit/app/chat/js/' + newFilename);
|
||||
})
|
||||
);
|
||||
})
|
||||
|
||||
gulp.task('urbit-copy', function () {
|
||||
let ret = gulp.src('urbit/**/*');
|
||||
|
||||
urbitrc.URBIT_PIERS.forEach(function(pier) {
|
||||
ret = ret.pipe(gulp.dest(pier));
|
||||
});
|
||||
|
||||
return ret;
|
||||
});
|
||||
|
||||
gulp.task('js-bundle-dev', gulp.series('jsx-transform', 'js-imports'));
|
||||
gulp.task('tile-js-bundle-dev', gulp.series('tile-jsx-transform', 'tile-js-imports'));
|
||||
gulp.task('js-bundle-prod', gulp.series('jsx-transform', 'js-imports', 'js-minify'))
|
||||
gulp.task('tile-js-bundle-prod',
|
||||
gulp.series('tile-jsx-transform', 'tile-js-imports', 'tile-js-minify'));
|
||||
|
||||
gulp.task('bundle-dev',
|
||||
gulp.series(
|
||||
gulp.parallel(
|
||||
'css-bundle',
|
||||
'js-bundle-dev',
|
||||
'tile-js-bundle-dev'
|
||||
),
|
||||
'urbit-copy'
|
||||
)
|
||||
);
|
||||
|
||||
gulp.task('bundle-prod',
|
||||
gulp.series(
|
||||
gulp.parallel(
|
||||
'css-bundle',
|
||||
'js-bundle-prod',
|
||||
'tile-js-bundle-prod',
|
||||
),
|
||||
'urbit-copy'
|
||||
)
|
||||
);
|
||||
|
||||
gulp.task('default', gulp.series('bundle-dev'));
|
||||
|
||||
gulp.task('watch', gulp.series('default', function() {
|
||||
gulp.watch('tile/**/*.js', gulp.parallel('tile-js-bundle-dev'));
|
||||
|
||||
gulp.watch('src/**/*.js', gulp.parallel('js-bundle-dev'));
|
||||
gulp.watch('src/**/*.css', gulp.parallel('css-bundle'));
|
||||
|
||||
gulp.watch('urbit/**/*', gulp.parallel('urbit-copy'));
|
||||
}));
|
6796
pkg/interface/apps/chat/package-lock.json
generated
Normal file
6796
pkg/interface/apps/chat/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
45
pkg/interface/apps/chat/package.json
Normal file
45
pkg/interface/apps/chat/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",
|
||||
"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/apps/chat/src/css/custom.css
Normal file
120
pkg/interface/apps/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/apps/chat/src/css/fonts.css
Normal file
63
pkg/interface/apps/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/apps/chat/src/css/spinner.css
Normal file
38
pkg/interface/apps/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/apps/chat/src/css/tachyons.css
Normal file
2
pkg/interface/apps/chat/src/css/tachyons.css
Normal file
File diff suppressed because one or more lines are too long
5
pkg/interface/apps/chat/src/index.css
Normal file
5
pkg/interface/apps/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/apps/chat/src/index.js
Normal file
16
pkg/interface/apps/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/apps/chat/src/js/api.js
Normal file
145
pkg/interface/apps/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/apps/chat/src/js/components/chat.js
Normal file
229
pkg/interface/apps/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/apps/chat/src/js/components/landing.js
Normal file
60
pkg/interface/apps/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/apps/chat/src/js/components/lib/chat-input.js
Normal file
178
pkg/interface/apps/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/apps/chat/src/js/components/lib/chat-tabbar.js
Normal file
59
pkg/interface/apps/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/apps/chat/src/js/components/lib/header-bar.js
Normal file
32
pkg/interface/apps/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/apps/chat/src/js/components/lib/icons/sigil.js
Normal file
34
pkg/interface/apps/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>
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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/apps/chat/src/js/components/lib/message.js
Normal file
97
pkg/interface/apps/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/apps/chat/src/js/components/lib/sidebar-invite.js
Normal file
120
pkg/interface/apps/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>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
@ -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/apps/chat/src/js/components/member.js
Normal file
135
pkg/interface/apps/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/apps/chat/src/js/components/new.js
Normal file
206
pkg/interface/apps/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/apps/chat/src/js/components/root.js
Normal file
250
pkg/interface/apps/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/apps/chat/src/js/components/settings.js
Normal file
127
pkg/interface/apps/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/apps/chat/src/js/components/sidebar.js
Normal file
163
pkg/interface/apps/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/apps/chat/src/js/components/skeleton.js
Normal file
28
pkg/interface/apps/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/apps/chat/src/js/lib/util.js
Normal file
64
pkg/interface/apps/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/apps/chat/src/js/reducers/config.js
Normal file
16
pkg/interface/apps/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/apps/chat/src/js/reducers/initial.js
Normal file
16
pkg/interface/apps/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/apps/chat/src/js/reducers/update.js
Normal file
83
pkg/interface/apps/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/apps/chat/src/js/store.js
Normal file
40
pkg/interface/apps/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/apps/chat/src/js/subscription.js
Normal file
44
pkg/interface/apps/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/apps/chat/tile/tile.js
Normal file
109
pkg/interface/apps/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;
|
658
pkg/interface/apps/chat/urbit/app/chat.hoon
Normal file
658
pkg/interface/apps/chat/urbit/app/chat.hoon
Normal file
@ -0,0 +1,658 @@
|
||||
/- hall
|
||||
/+ *server, chat, hall-json
|
||||
/= index
|
||||
/^ octs
|
||||
/; as-octs:mimes:html
|
||||
/: /===/app/chat/index
|
||||
/| /html/
|
||||
/~ ~
|
||||
==
|
||||
/= tile-js
|
||||
/^ octs
|
||||
/; as-octs:mimes:html
|
||||
/: /===/app/chat/js/tile
|
||||
/| /js/
|
||||
/~ ~
|
||||
==
|
||||
/= script
|
||||
/^ octs
|
||||
/; as-octs:mimes:html
|
||||
/: /===/app/chat/js/index
|
||||
/| /js/
|
||||
/~ ~
|
||||
==
|
||||
/= style
|
||||
/^ octs
|
||||
/; as-octs:mimes:html
|
||||
/: /===/app/chat/css/index
|
||||
/| /css/
|
||||
/~ ~
|
||||
==
|
||||
/= style
|
||||
/^ octs
|
||||
/; as-octs:mimes:html
|
||||
/: /===/app/chat/css/index
|
||||
/| /css/
|
||||
/~ ~
|
||||
==
|
||||
/= chat-png
|
||||
/^ (map knot @)
|
||||
/: /===/app/chat/img /_ /png/
|
||||
::
|
||||
=, chat
|
||||
::
|
||||
|_ [bol=bowl:gall sta=state]
|
||||
::
|
||||
++ this .
|
||||
::
|
||||
:: +prep: set up the app, migrate the state once started
|
||||
::
|
||||
++ prep
|
||||
|= old=(unit state)
|
||||
^- (quip move _this)
|
||||
=/ launcha/poke
|
||||
[%launch-action [%chat /chattile '/~chat/js/tile.js']]
|
||||
?~ old
|
||||
=/ inboxpat /circle/inbox/config/group
|
||||
=/ circlespat /circles/[(scot %p our.bol)]
|
||||
=/ inboxwir /circle/[(scot %p our.bol)]/inbox/config/group
|
||||
=/ inboxi/poke
|
||||
:- %hall-action
|
||||
[%source %inbox %.y (silt [[our.bol %i] ~]~)]
|
||||
=/ fakeannounce=poke
|
||||
:- %hall-action
|
||||
[%create %hall-internal-announcements '' %village]
|
||||
=/ announce=poke
|
||||
:- %hall-action
|
||||
[%create %announcements 'Announcements from Tlon' %journal]
|
||||
=/ help=poke
|
||||
:- %hall-action
|
||||
[%create %urbit-help 'Get help about Urbit' %channel]
|
||||
=/ dev=poke
|
||||
:- %hall-action
|
||||
[%create %urbit-dev 'Chat about developing on Urbit' %channel]
|
||||
=/ sourcefakeannounce/poke
|
||||
:- %hall-action
|
||||
[%source %inbox %.y (silt [[our.bol %hall-internal-announcements] ~]~)]
|
||||
=/ sourceannounce/poke
|
||||
:- %hall-action
|
||||
[%source %inbox %.y (silt [[~marzod %announcements] ~]~)]
|
||||
=/ hallactions=(list move)
|
||||
?: =((clan:title our.bol) %czar)
|
||||
~
|
||||
?: =(our.bol ~marzod)
|
||||
~& %marzod-chat
|
||||
:- [ost.bol %poke /announce [our.bol %hall] announce]
|
||||
[ost.bol %poke /announce [our.bol %hall] sourceannounce]~
|
||||
?: =(our.bol ~dopzod)
|
||||
~& %dopzod-chat
|
||||
:- [ost.bol %poke /announce [our.bol %hall] dev]
|
||||
[ost.bol %poke /announce [our.bol %hall] help]~
|
||||
:- [ost.bol %poke /announce [our.bol %hall] fakeannounce]
|
||||
:- [ost.bol %poke /announce [our.bol %hall] sourcefakeannounce]
|
||||
[ost.bol %poke /announce [our.bol %hall] sourceannounce]~
|
||||
=/ moves=(list move)
|
||||
:~ [ost.bol %peer inboxwir [our.bol %hall] inboxpat]
|
||||
[ost.bol %peer circlespat [our.bol %hall] circlespat]
|
||||
[ost.bol %connect / [~ /'~chat'] %chat]
|
||||
[ost.bol %poke /chat [our.bol %hall] inboxi]
|
||||
[ost.bol %poke /chat [our.bol %launch] launcha]
|
||||
==
|
||||
:_ this
|
||||
%+ weld moves hallactions
|
||||
:- [ost.bol %poke /chat [our.bol %launch] launcha]~
|
||||
this(sta u.old)
|
||||
::
|
||||
++ construct-tile-json
|
||||
|= str=streams
|
||||
^- json
|
||||
=/ numbers/(list [circle:hall @ud])
|
||||
%+ turn ~(tap by messages.str)
|
||||
|= [cir=circle:hall lis=(list envelope:hall)]
|
||||
^- [circle:hall @ud]
|
||||
?~ lis
|
||||
[cir 0]
|
||||
=/ last (snag (dec (lent lis)) `(list envelope:hall)`lis)
|
||||
[cir (add num.last 1)]
|
||||
=/ maptjson=(map @t json)
|
||||
%- my
|
||||
:~ ['config' (config-to-json str)]
|
||||
['numbers' (numbers-to-json numbers)]
|
||||
==
|
||||
[%o maptjson]
|
||||
::
|
||||
++ peer-chattile
|
||||
|= wir=wire
|
||||
^- (quip move _this)
|
||||
:_ this
|
||||
[ost.bol %diff %json (construct-tile-json str.sta)]~
|
||||
::
|
||||
:: +peer-messages: subscribe to subset of messages and updates
|
||||
::
|
||||
::
|
||||
++ peer-primary
|
||||
|= wir=wire
|
||||
^- (quip move _this)
|
||||
=* messages messages.str.sta
|
||||
=/ lismov/(list move)
|
||||
%+ murn ~(tap by messages)
|
||||
|= [cir=circle:hall lis=(list envelope:hall)]
|
||||
^- (unit move)
|
||||
=/ envs/(unit (list envelope:hall)) (~(get by messages) cir)
|
||||
?~ envs
|
||||
~
|
||||
=/ length/@ (lent u.envs)
|
||||
=/ start/@
|
||||
?: (gte length 100)
|
||||
(sub length 100)
|
||||
0
|
||||
=/ end/@ length
|
||||
=/ offset/@ (sub end start)
|
||||
:- ~
|
||||
:* ost.bol
|
||||
%diff
|
||||
%chat-update
|
||||
[%messages cir start end (swag [start offset] u.envs)]
|
||||
==
|
||||
:_ this
|
||||
[[ost.bol %diff %chat-config str.sta] lismov]
|
||||
::
|
||||
:: +poke-chat: send us an action
|
||||
::
|
||||
++ poke-chat-action
|
||||
|= act=action:chat
|
||||
^- (quip move _this)
|
||||
:_ this
|
||||
%+ turn lis.act
|
||||
|= hac=action:hall
|
||||
^- move
|
||||
:* ost.bol
|
||||
%poke
|
||||
/p/[(scot %da now.bol)]
|
||||
[our.bol %hall]
|
||||
[%hall-action hac]
|
||||
==
|
||||
::
|
||||
:: +send-chat-update: utility func for sending updates to all our subscribers
|
||||
::
|
||||
++ send-chat-update
|
||||
|= [upd=update str=streams]
|
||||
^- (list move)
|
||||
=/ updates/(list move)
|
||||
%+ turn (prey:pubsub:userlib /primary bol)
|
||||
|= [=bone *]
|
||||
[bone %diff %chat-update upd]
|
||||
::
|
||||
=/ jon/json (construct-tile-json str)
|
||||
=/ tile-updates/(list move)
|
||||
%+ turn (prey:pubsub:userlib /chattile bol)
|
||||
|= [=bone *]
|
||||
[bone %diff %json jon]
|
||||
::
|
||||
%+ weld
|
||||
updates
|
||||
tile-updates
|
||||
::
|
||||
::
|
||||
:: +hall arms
|
||||
::
|
||||
::
|
||||
:: +diff-hall-prize: handle full state initially handed to us by hall
|
||||
::
|
||||
++ diff-hall-prize
|
||||
|= [wir=wire piz=prize:hall]
|
||||
^- (quip move _this)
|
||||
?~ wir
|
||||
(mean [leaf+"invalid wire for diff: {(spud wir)}"]~)
|
||||
?+ i.wir
|
||||
(mean [leaf+"invalid wire for diff: {(spud wir)}"]~)
|
||||
::
|
||||
:: %circles wire
|
||||
::
|
||||
%circles
|
||||
?> ?=(%circles -.piz)
|
||||
=/ str %= str.sta
|
||||
circles cis.piz
|
||||
==
|
||||
:- (send-chat-update [[%circles cis.piz] str])
|
||||
this(str.sta str)
|
||||
::
|
||||
:: %circle wire
|
||||
::
|
||||
%circle
|
||||
:: ::
|
||||
:: :: %circle prize
|
||||
:: ::
|
||||
:: %circle
|
||||
?> ?=(%circle -.piz)
|
||||
=/ circle/circle:hall [our.bol &3:wir]
|
||||
?: =(circle [our.bol %inbox])
|
||||
::
|
||||
:: fill inbox config and remote configs with prize data
|
||||
::
|
||||
=/ configs
|
||||
%- ~(uni in configs.str.sta)
|
||||
^- (map circle:hall (unit config:hall))
|
||||
(~(run by rem.cos.piz) |=(a=config:hall `a))
|
||||
::
|
||||
=/ circles/(list circle:hall)
|
||||
%+ turn ~(tap in src.loc.cos.piz)
|
||||
|= src=source:hall
|
||||
^- circle:hall
|
||||
cir.src
|
||||
::
|
||||
=/ meslis/(list [circle:hall (list envelope:hall)])
|
||||
%+ turn circles
|
||||
|= cir=circle:hall
|
||||
^- [circle:hall (list envelope:hall)]
|
||||
[cir ~]
|
||||
::
|
||||
=/ localpeers/(set @p)
|
||||
%- silt %+ turn ~(tap by loc.pes.piz)
|
||||
|= [shp=@p stat=status:hall]
|
||||
shp
|
||||
::
|
||||
=/ peers/(map circle:hall (set @p))
|
||||
%- ~(rep by rem.pes.piz)
|
||||
|= [[cir=circle:hall grp=group:hall] acc=(map circle:hall (set @p))]
|
||||
^- (map circle:hall (set @p))
|
||||
=/ newset
|
||||
%- silt %+ turn ~(tap by grp)
|
||||
|= [shp=@p stat=status:hall]
|
||||
shp
|
||||
(~(put by acc) cir newset)
|
||||
::
|
||||
:-
|
||||
%+ turn ~(tap in (~(del in (silt circles)) [our.bol %inbox]))
|
||||
|= cir=circle:hall
|
||||
^- move
|
||||
=/ wir/wire /circle/[(scot %p our.bol)]/[nom.cir]/config/group
|
||||
=/ pat/path /circle/[nom.cir]/config/group
|
||||
[ost.bol %peer wir [our.bol %hall] pat]
|
||||
::
|
||||
%= this
|
||||
inbox.str.sta loc.cos.piz
|
||||
configs.str.sta configs
|
||||
messages.str.sta (molt meslis)
|
||||
peers.str.sta (~(put by peers) [our.bol %inbox] localpeers)
|
||||
==
|
||||
::
|
||||
:: fill remote configs with message data
|
||||
::
|
||||
=* messages messages.str.sta
|
||||
=/ circle/circle:hall [`@p`(slav %p &2:wir) &3:wir]
|
||||
=/ localpeers/(set @p)
|
||||
%- silt %+ turn ~(tap by loc.pes.piz)
|
||||
|= [shp=@p stat=status:hall]
|
||||
shp
|
||||
::
|
||||
=/ peers/(map circle:hall (set @p))
|
||||
%- ~(rep by rem.pes.piz)
|
||||
|= [[cir=circle:hall grp=group:hall] acc=(map circle:hall (set @p))]
|
||||
^- (map circle:hall (set @p))
|
||||
=/ newset
|
||||
%- silt %+ turn ~(tap by grp)
|
||||
|= [shp=@p stat=status:hall]
|
||||
shp
|
||||
(~(put by acc) cir newset)
|
||||
=/ str
|
||||
%= str.sta
|
||||
messages (~(put by messages) circle nes.piz)
|
||||
peers (~(uni by peers.str.sta) (~(put by peers) circle localpeers))
|
||||
==
|
||||
=/ messageupdate/update
|
||||
:* %messages
|
||||
circle
|
||||
0
|
||||
(lent messages)
|
||||
nes.piz
|
||||
==
|
||||
:- (send-chat-update [messageupdate str])
|
||||
this(str.sta str)
|
||||
==
|
||||
::
|
||||
:: +diff-hall-rumor: handle updates to hall state
|
||||
::
|
||||
++ diff-hall-rumor
|
||||
|= [wir=wire rum=rumor:hall]
|
||||
^- (quip move _this)
|
||||
?~ wir
|
||||
(mean [leaf+"invalid wire for diff: {(spud wir)}"]~)
|
||||
?+ i.wir
|
||||
(mean [leaf+"invalid wire for diff: {(spud wir)}"]~)
|
||||
::
|
||||
:: %circles
|
||||
%circles
|
||||
?> ?=(%circles -.rum)
|
||||
=/ cis
|
||||
?: add.rum
|
||||
(~(put in circles.str.sta) cir.rum)
|
||||
(~(del in circles.str.sta) cir.rum)
|
||||
=/ str
|
||||
%= str.sta
|
||||
circles cis
|
||||
peers
|
||||
?: add.rum
|
||||
(~(put by peers.str.sta) [our.bol cir.rum] ~)
|
||||
(~(del by peers.str.sta) [our.bol cir.rum])
|
||||
==
|
||||
:- (send-chat-update [[%circles cis] str])
|
||||
this(str.sta str)
|
||||
::
|
||||
::
|
||||
:: %circle: fill remote configs with message data
|
||||
::
|
||||
%circle
|
||||
?> ?=(%circle -.rum)
|
||||
=* sto rum.rum
|
||||
?+ -.sto
|
||||
[~ this]
|
||||
::
|
||||
:: %gram:
|
||||
::
|
||||
%gram
|
||||
?> ?=(%gram -.sto)
|
||||
=* messages messages.str.sta
|
||||
=/ circle/circle:hall [`@p`(slav %p &2:wir) &3:wir]
|
||||
=/ unes/(unit (list envelope:hall)) (~(get by messages) circle)
|
||||
?~ unes
|
||||
[~ this]
|
||||
=/ nes u.unes
|
||||
=/ str
|
||||
%= str.sta
|
||||
messages (~(put by messages) circle (snoc nes nev.sto))
|
||||
==
|
||||
:- (send-chat-update [[%message circle nev.sto] str])
|
||||
this(str.sta str)
|
||||
::
|
||||
:: %status:
|
||||
::
|
||||
%status
|
||||
?> ?=(%status -.sto)
|
||||
=/ upeers/(unit (set @p)) (~(get by peers.str.sta) cir.sto)
|
||||
?~ upeers
|
||||
[~ this]
|
||||
=/ peers/(set @p)
|
||||
?: =(%remove -.dif.sto)
|
||||
(~(del in u.upeers) who.sto)
|
||||
(~(put in u.upeers) who.sto)
|
||||
=/ str
|
||||
%= str.sta
|
||||
peers (~(put by peers.str.sta) cir.sto peers)
|
||||
==
|
||||
:- (send-chat-update [[%peers cir.sto peers] str])
|
||||
this(str.sta str)
|
||||
::
|
||||
:: %config: config has changed
|
||||
::
|
||||
%config
|
||||
=* circ cir.sto
|
||||
::
|
||||
?+ -.dif.sto
|
||||
[~ this]
|
||||
::
|
||||
:: %full: set all of config without side effects
|
||||
::
|
||||
%full
|
||||
=* conf cof.dif.sto
|
||||
=/ str
|
||||
%= str.sta
|
||||
configs (~(put by configs.str.sta) circ `conf)
|
||||
==
|
||||
:- (send-chat-update [[%config circ conf] str])
|
||||
this(str.sta str)
|
||||
::
|
||||
:: %read: the read count of one of our configs has changed
|
||||
::
|
||||
%read
|
||||
?: =(circ [our.bol %inbox])
|
||||
:: ignore when circ is inbox
|
||||
[~ this]
|
||||
=/ uconf/(unit config:hall) (~(got by configs.str.sta) circ)
|
||||
?~ uconf
|
||||
:: should we crash?
|
||||
[~ this]
|
||||
=/ conf/config:hall
|
||||
%= u.uconf
|
||||
red red.dif.sto
|
||||
==
|
||||
=/ str
|
||||
%= str.sta
|
||||
configs (~(put by configs.str.sta) circ `conf)
|
||||
==
|
||||
:- (send-chat-update [[%config circ conf] str])
|
||||
this(str.sta str)
|
||||
::
|
||||
:: %source: the sources of our inbox have changed
|
||||
::
|
||||
%source
|
||||
?. =(circ [our.bol %inbox])
|
||||
:: ignore when circ is not inbox
|
||||
[~ this]
|
||||
=* affectedcir cir.src.dif.sto
|
||||
=/ newwir/wire
|
||||
/circle/[(scot %p hos.affectedcir)]/[nom.affectedcir]/grams/0/config/group
|
||||
=/ pat/path /circle/[nom.affectedcir]/grams/0/config/group
|
||||
:: we've added a source to our inbox
|
||||
::
|
||||
?: add.dif.sto
|
||||
=/ newinbox %= inbox.str.sta
|
||||
src (~(put in src.inbox.str.sta) src.dif.sto)
|
||||
==
|
||||
=/ str
|
||||
%= str.sta
|
||||
inbox newinbox
|
||||
::
|
||||
configs
|
||||
?: (~(has by configs.str.sta) affectedcir)
|
||||
configs.str.sta
|
||||
(~(put by configs.str.sta) affectedcir ~)
|
||||
==
|
||||
::
|
||||
:_ this(str.sta str)
|
||||
%+ weld
|
||||
[ost.bol %peer newwir [hos.affectedcir %hall] pat]~
|
||||
(send-chat-update [[%inbox newinbox] str])
|
||||
::
|
||||
=/ newinbox %= inbox.str.sta
|
||||
src (~(del in src.inbox.str.sta) src.dif.sto)
|
||||
==
|
||||
:: we've removed a source from our inbox
|
||||
::
|
||||
=/ str
|
||||
%= str.sta
|
||||
inbox newinbox
|
||||
::
|
||||
configs (~(del by configs.str.sta) affectedcir)
|
||||
messages (~(del by messages.str.sta) affectedcir)
|
||||
peers (~(del by peers.str.sta) affectedcir)
|
||||
==
|
||||
=/ fakecir/circle:hall
|
||||
:- our.bol
|
||||
%- crip
|
||||
%+ weld (trip 'hall-internal-') (trip nom.affectedcir)
|
||||
::
|
||||
?~ (~(get by configs.str) fakecir)
|
||||
:: just forward the delete to our clients
|
||||
::
|
||||
:_ this(str.sta str)
|
||||
%+ weld
|
||||
[ost.bol %pull newwir [hos.affectedcir %hall] ~]~
|
||||
%+ weld
|
||||
(send-chat-update [[%inbox newinbox] str])
|
||||
(send-chat-update [[%delete affectedcir] str])
|
||||
:: if we get a delete from another ship, delete our fake circle copy
|
||||
::
|
||||
=/ deletefake/poke
|
||||
:- %hall-action
|
||||
[%delete nom.fakecir ~]
|
||||
:_ this(str.sta str)
|
||||
%+ weld
|
||||
[ost.bol %pull newwir [hos.affectedcir %hall] ~]~
|
||||
%+ weld
|
||||
[ost.bol %poke /fake [our.bol %hall] deletefake]~
|
||||
%+ weld
|
||||
(send-chat-update [[%inbox newinbox] str])
|
||||
(send-chat-update [[%delete affectedcir] str])
|
||||
::
|
||||
:: %remove: remove a circle
|
||||
::
|
||||
%remove
|
||||
=/ str
|
||||
%= str.sta
|
||||
configs (~(del by configs.str.sta) circ)
|
||||
messages (~(del by messages.str.sta) circ)
|
||||
peers (~(del by peers.str.sta) circ)
|
||||
==
|
||||
:- (send-chat-update [[%delete circ] str])
|
||||
this(str.sta str)
|
||||
::
|
||||
==
|
||||
:: end of branching on dif.sto type
|
||||
==
|
||||
:: end of branching on sto type
|
||||
==
|
||||
:: end of i.wir branching
|
||||
::
|
||||
:: +lient arms
|
||||
::
|
||||
::
|
||||
:: +bound: lient tells us we successfully bound our server to the ~chat url
|
||||
::
|
||||
++ bound
|
||||
|= [wir=wire success=? binding=binding:eyre]
|
||||
^- (quip move _this)
|
||||
[~ this]
|
||||
::
|
||||
:: +poke-handle-http-request: serve pages from file system based on URl path
|
||||
::
|
||||
++ poke-handle-http-request
|
||||
%- (require-authorization:app ost.bol move this)
|
||||
|= =inbound-request:eyre
|
||||
^- (quip move _this)
|
||||
::
|
||||
=+ request-line=(parse-request-line url.request.inbound-request)
|
||||
=/ name=@t
|
||||
=+ back-path=(flop site.request-line)
|
||||
?~ back-path
|
||||
''
|
||||
i.back-path
|
||||
?: =(name 'tile')
|
||||
[[ost.bol %http-response (js-response:app tile-js)]~ this]
|
||||
?+ site.request-line
|
||||
:_ this
|
||||
[ost.bol %http-response not-found:app]~
|
||||
::
|
||||
:: styling
|
||||
::
|
||||
[%'~chat' %css %index ~]
|
||||
:_ this
|
||||
[ost.bol %http-response (css-response:app style)]~
|
||||
::
|
||||
:: javascript
|
||||
::
|
||||
[%'~chat' %js %index ~]
|
||||
:_ this
|
||||
[ost.bol %http-response (js-response:app script)]~
|
||||
::
|
||||
:: images
|
||||
::
|
||||
[%'~chat' %img *]
|
||||
=/ img (as-octs:mimes:html (~(got by chat-png) `@ta`name))
|
||||
:_ this
|
||||
[ost.bol %http-response (png-response:app img)]~
|
||||
::
|
||||
:: paginated message data
|
||||
::
|
||||
[%'~chat' %scroll @t @t @t @t ~]
|
||||
=/ cir/circle:hall [(slav %p &3:site.request-line) &4:site.request-line]
|
||||
=/ start/@ud (need (rush &5:site.request-line dem))
|
||||
=/ parsedend/@ud (need (rush &6:site.request-line dem))
|
||||
=* messages messages.str.sta
|
||||
=/ envs/(unit (list envelope:hall)) (~(get by messages) cir)
|
||||
?~ envs
|
||||
[~ this]
|
||||
?: (gte start (lent u.envs))
|
||||
[~ this]
|
||||
=/ end/@
|
||||
?: (gte parsedend (lent u.envs))
|
||||
(dec (lent u.envs))
|
||||
parsedend
|
||||
=/ offset (sub end start)
|
||||
=/ jon/json %- msg-to-json
|
||||
:* %messages
|
||||
cir
|
||||
start
|
||||
end
|
||||
(swag [start offset] u.envs)
|
||||
==
|
||||
:_ this
|
||||
[ost.bol %http-response (json-response:app (json-to-octs jon))]~
|
||||
::
|
||||
::
|
||||
:: inbox page
|
||||
::
|
||||
[%'~chat' *]
|
||||
:_ this
|
||||
[ost.bol %http-response (html-response:app index)]~
|
||||
==
|
||||
::
|
||||
::
|
||||
:: +subscription-retry arms
|
||||
::
|
||||
::
|
||||
:: +reap: recieve acknowledgement for peer, retry on failure
|
||||
::
|
||||
++ reap
|
||||
|= [wir=wire err=(unit tang)]
|
||||
^- (quip move _this)
|
||||
?~ err
|
||||
[~ this]
|
||||
?~ wir
|
||||
(mean [leaf+"invalid wire for diff: {(spud wir)}"]~)
|
||||
?+ i.wir
|
||||
(mean [leaf+"invalid wire for diff: {(spud wir)}"]~)
|
||||
::
|
||||
%circle
|
||||
=/ shp/@p (slav %p &2:wir)
|
||||
=/ pat /circle/[&3:wir]/config/group
|
||||
?: =(&3:wir 'inbox')
|
||||
:_ this
|
||||
[ost.bol %peer wir [shp %hall] pat]~
|
||||
?: (~(has in src.inbox.str.sta) [[shp &3:wir] ~])
|
||||
:_ this
|
||||
[ost.bol %peer wir [shp %hall] pat]~
|
||||
[~ this]
|
||||
::
|
||||
%circles
|
||||
:_ this
|
||||
[ost.bol %peer wir [our.bol %hall] wir]~
|
||||
==
|
||||
::
|
||||
:: +quit: subscription failed/quit at some point, retry
|
||||
::
|
||||
++ quit
|
||||
|= wir=wire
|
||||
^- (quip move _this)
|
||||
?~ wir
|
||||
(mean [leaf+"invalid wire for diff: {(spud wir)}"]~)
|
||||
?+ i.wir
|
||||
(mean [leaf+"invalid wire for diff: {(spud wir)}"]~)
|
||||
::
|
||||
%circle
|
||||
=/ shp/@p (slav %p &2:wir)
|
||||
=/ pat /circle/[&3:wir]/config/group
|
||||
?: =(&3:wir 'inbox')
|
||||
:_ this
|
||||
[ost.bol %peer wir [shp %hall] pat]~
|
||||
?: (~(has in src.inbox.str.sta) [[shp &3:wir] ~])
|
||||
:_ this
|
||||
[ost.bol %peer wir [shp %hall] pat]~
|
||||
[~ this]
|
||||
::
|
||||
%circles
|
||||
:_ this
|
||||
[ost.bol %peer wir [our.bol %hall] wir]~
|
||||
==
|
||||
::
|
||||
--
|
BIN
pkg/interface/apps/chat/urbit/app/chat/img/Send.png
Normal file
BIN
pkg/interface/apps/chat/urbit/app/chat/img/Send.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 1010 B |
BIN
pkg/interface/apps/chat/urbit/app/chat/img/Tile.png
Normal file
BIN
pkg/interface/apps/chat/urbit/app/chat/img/Tile.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 1.7 KiB |
17
pkg/interface/apps/chat/urbit/app/chat/index.html
Normal file
17
pkg/interface/apps/chat/urbit/app/chat/index.html
Normal file
@ -0,0 +1,17 @@
|
||||
<!doctype html>
|
||||
<html>
|
||||
<head>
|
||||
<title>Chat</title>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport"
|
||||
content="width=device-width, initial-scale=1, shrink-to-fit=no"/>
|
||||
<link rel="stylesheet" href="/~chat/css/index.css" />
|
||||
<link rel="icon" type="image/png" href="/~launch/img/Favicon.png">
|
||||
</head>
|
||||
<body>
|
||||
<div id="root" />
|
||||
<script src="/~/channel/channel.js"></script>
|
||||
<script src="/~modulo/session.js"></script>
|
||||
<script src="/~chat/js/index.js"></script>
|
||||
</body>
|
||||
</html>
|
62277
pkg/interface/apps/chat/urbit/app/chat/js/index.js
Normal file
62277
pkg/interface/apps/chat/urbit/app/chat/js/index.js
Normal file
File diff suppressed because it is too large
Load Diff
19330
pkg/interface/apps/chat/urbit/app/chat/js/tile.js
Normal file
19330
pkg/interface/apps/chat/urbit/app/chat/js/tile.js
Normal file
File diff suppressed because it is too large
Load Diff
140
pkg/interface/apps/chat/urbit/lib/chat.hoon
Normal file
140
pkg/interface/apps/chat/urbit/lib/chat.hoon
Normal file
@ -0,0 +1,140 @@
|
||||
/- hall
|
||||
/+ hall-json
|
||||
|%
|
||||
::
|
||||
+$ move [bone card]
|
||||
::
|
||||
+$ card
|
||||
$% [%http-response =http-event:http]
|
||||
[%connect wire binding:eyre term]
|
||||
[%peer wire dock path]
|
||||
[%quit ~]
|
||||
[%poke wire dock poke]
|
||||
[%peer wire dock path]
|
||||
[%pull wire dock ~]
|
||||
[%diff diff]
|
||||
==
|
||||
::
|
||||
+$ diff
|
||||
$% [%hall-rumor rumor:hall]
|
||||
[%chat-update update]
|
||||
[%chat-config streams]
|
||||
[%json json]
|
||||
==
|
||||
::
|
||||
+$ poke
|
||||
$% [%hall-action action:hall]
|
||||
[%launch-action [@tas path @t]]
|
||||
==
|
||||
::
|
||||
+$ state
|
||||
$% [%0 str=streams]
|
||||
==
|
||||
::
|
||||
+$ streams
|
||||
$: :: inbox config
|
||||
::
|
||||
inbox=config:hall
|
||||
:: names and configs of all circles we know about
|
||||
::
|
||||
configs=(map circle:hall (unit config:hall))
|
||||
:: messages for all circles we know about
|
||||
::
|
||||
messages=(map circle:hall (list envelope:hall))
|
||||
::
|
||||
::
|
||||
circles=(set name:hall)
|
||||
::
|
||||
::
|
||||
peers=(map circle:hall (set @p))
|
||||
==
|
||||
::
|
||||
+$ update
|
||||
$% [%inbox con=config:hall]
|
||||
[%message cir=circle:hall env=envelope:hall]
|
||||
[%messages cir=circle:hall start=@ud end=@ud env=(list envelope:hall)]
|
||||
[%config cir=circle:hall con=config:hall]
|
||||
[%circles cir=(set name:hall)]
|
||||
[%peers cir=circle:hall per=(set @p)]
|
||||
[%delete cir=circle:hall]
|
||||
==
|
||||
::
|
||||
+$ action [%actions lis=(list action:hall)]
|
||||
::
|
||||
::
|
||||
:: +utilities
|
||||
::
|
||||
++ msg-to-json
|
||||
=, enjs:format
|
||||
|= upd=update
|
||||
^- json
|
||||
?> ?=(%messages -.upd)
|
||||
%+ frond %update
|
||||
%- pairs
|
||||
:~
|
||||
:- %messages
|
||||
%- pairs
|
||||
:~
|
||||
[%circle (circ:enjs:hall-json cir.upd)]
|
||||
[%start (numb start.upd)]
|
||||
[%end (numb end.upd)]
|
||||
[%envelopes [%a (turn env.upd enve:enjs:hall-json)]]
|
||||
==
|
||||
==
|
||||
::
|
||||
++ config-to-json
|
||||
|= str=streams
|
||||
=, enjs:format
|
||||
^- json
|
||||
%+ frond %chat
|
||||
%- pairs
|
||||
:~
|
||||
::
|
||||
[%inbox (conf:enjs:hall-json inbox.str)]
|
||||
::
|
||||
:- %configs
|
||||
%- pairs
|
||||
%+ turn ~(tap by configs.str)
|
||||
|= [cir=circle:hall con=(unit config:hall)]
|
||||
^- [@t json]
|
||||
:- (crip (circ:en-tape:hall-json cir))
|
||||
?~(con ~ (conf:enjs:hall-json u.con))
|
||||
::
|
||||
:- %circles :- %a
|
||||
%+ turn ~(tap in circles.str)
|
||||
|= nom=name:hall
|
||||
[%s nom]
|
||||
::
|
||||
:- %peers
|
||||
%- pairs
|
||||
%+ turn ~(tap by peers.str)
|
||||
|= [cir=circle:hall per=(set @p)]
|
||||
^- [@t json]
|
||||
:- (crip (circ:en-tape:hall-json cir))
|
||||
[%a (turn ~(tap in per) ship)]
|
||||
::
|
||||
==
|
||||
::
|
||||
++ numbers-to-json
|
||||
|= num=(list [circle:hall @ud])
|
||||
^- json
|
||||
=, enjs:format
|
||||
%+ frond %chat
|
||||
%- pairs
|
||||
:~
|
||||
::
|
||||
:: %config
|
||||
:- %numbers
|
||||
:- %a
|
||||
%+ turn num
|
||||
|= [cir=circle:hall len=@ud]
|
||||
^- json
|
||||
%- pairs
|
||||
:~
|
||||
[%circle (circ:enjs:hall-json cir)]
|
||||
[%length (numb len)]
|
||||
==
|
||||
==
|
||||
::
|
||||
--
|
||||
::
|
58
pkg/interface/apps/chat/urbit/mar/chat/action.hoon
Normal file
58
pkg/interface/apps/chat/urbit/mar/chat/action.hoon
Normal file
@ -0,0 +1,58 @@
|
||||
::
|
||||
::
|
||||
/- hall
|
||||
/+ chat, hall-json
|
||||
::
|
||||
|_ act=action:chat
|
||||
++ grow
|
||||
|%
|
||||
++ tank !!
|
||||
--
|
||||
::
|
||||
++ grab
|
||||
|%
|
||||
++ noun action:chat
|
||||
++ json
|
||||
|= jon=^json
|
||||
=< (parse-chat-action jon)
|
||||
|%
|
||||
::
|
||||
++ hall-action
|
||||
=, dejs:hall-json
|
||||
=, dejs-soft:format
|
||||
|= a=^json
|
||||
^- action:hall
|
||||
=- (need ((of -) a))
|
||||
:~ create+(ot nom+so des+so sec+secu ~)
|
||||
design+(ot nom+so cof+conf ~)
|
||||
delete+(ot nom+so why+(mu so) ~)
|
||||
depict+(ot nom+so des+so ~)
|
||||
filter+(ot nom+so fit+filt ~)
|
||||
permit+(ot nom+so inv+bo sis+(as (su fed:ag)) ~)
|
||||
source+(ot nom+so sub+bo srs+(as sorc) ~)
|
||||
read+(ot nom+so red+ni ~)
|
||||
usage+(ot nom+so add+bo tas+(as so) ~)
|
||||
newdm+(ot sis+(as (su fed:ag)) ~)
|
||||
::
|
||||
convey+(ar thot)
|
||||
phrase+(ot aud+audi ses+(ar spec:dejs:hall-json) ~)
|
||||
::
|
||||
notify+(ot aud+audi pes+(mu pres) ~)
|
||||
naming+(ot aud+audi man+huma ~)
|
||||
::
|
||||
glyph+(ot gyf+so aud+audi bin+bo ~)
|
||||
nick+(ot who+(su fed:ag) nic+so ~)
|
||||
::
|
||||
public+(ot add+bo cir+circ ~)
|
||||
==
|
||||
::
|
||||
++ parse-chat-action
|
||||
=, dejs:format
|
||||
%- of
|
||||
:~
|
||||
[%actions (ot lis+(ar hall-action) ~)]
|
||||
==
|
||||
::
|
||||
--
|
||||
--
|
||||
--
|
48
pkg/interface/apps/chat/urbit/mar/chat/config.hoon
Normal file
48
pkg/interface/apps/chat/urbit/mar/chat/config.hoon
Normal file
@ -0,0 +1,48 @@
|
||||
::
|
||||
::
|
||||
/? 309
|
||||
::
|
||||
/- hall
|
||||
/+ chat, hall-json
|
||||
::
|
||||
|_ str=streams:chat
|
||||
++ grow
|
||||
|%
|
||||
++ json
|
||||
=, enjs:format
|
||||
^- ^json
|
||||
%+ frond %chat
|
||||
%- pairs
|
||||
:~
|
||||
::
|
||||
[%inbox (conf:enjs:hall-json inbox.str)]
|
||||
::
|
||||
:- %configs
|
||||
%- pairs
|
||||
%+ turn ~(tap by configs.str)
|
||||
|= [cir=circle:hall con=(unit config:hall)]
|
||||
^- [@t ^json]
|
||||
:- (crip (circ:en-tape:hall-json cir))
|
||||
?~(con ~ (conf:enjs:hall-json u.con))
|
||||
::
|
||||
:- %circles :- %a
|
||||
%+ turn ~(tap in circles.str)
|
||||
|= nom=name:hall
|
||||
[%s nom]
|
||||
::
|
||||
:- %peers
|
||||
%- pairs
|
||||
%+ turn ~(tap by peers.str)
|
||||
|= [cir=circle:hall per=(set @p)]
|
||||
^- [@t ^json]
|
||||
:- (crip (circ:en-tape:hall-json cir))
|
||||
[%a (turn ~(tap in per) ship)]
|
||||
::
|
||||
==
|
||||
--
|
||||
::
|
||||
++ grab
|
||||
|%
|
||||
++ noun streams:chat
|
||||
--
|
||||
--
|
96
pkg/interface/apps/chat/urbit/mar/chat/update.hoon
Normal file
96
pkg/interface/apps/chat/urbit/mar/chat/update.hoon
Normal file
@ -0,0 +1,96 @@
|
||||
::
|
||||
::
|
||||
/? 309
|
||||
::
|
||||
/- hall
|
||||
/+ chat, hall-json
|
||||
::
|
||||
|_ upd=update:chat
|
||||
++ grow
|
||||
|%
|
||||
++ json
|
||||
=, enjs:format
|
||||
^- ^json
|
||||
%+ frond %update
|
||||
%- pairs
|
||||
:~
|
||||
::
|
||||
:: %inbox
|
||||
?: =(%inbox -.upd)
|
||||
?> ?=(%inbox -.upd)
|
||||
[%inbox (conf:enjs:hall-json con.upd)]
|
||||
::
|
||||
:: %message
|
||||
?: =(%message -.upd)
|
||||
?> ?=(%message -.upd)
|
||||
:- %message
|
||||
%- pairs
|
||||
:~
|
||||
[%circle (circ:enjs:hall-json cir.upd)]
|
||||
[%envelope (enve:enjs:hall-json env.upd)]
|
||||
==
|
||||
::
|
||||
:: %messages
|
||||
?: =(%messages -.upd)
|
||||
?> ?=(%messages -.upd)
|
||||
:- %messages
|
||||
%- pairs
|
||||
:~
|
||||
[%circle (circ:enjs:hall-json cir.upd)]
|
||||
[%start (numb start.upd)]
|
||||
[%end (numb end.upd)]
|
||||
[%envelopes [%a (turn env.upd enve:enjs:hall-json)]]
|
||||
==
|
||||
::
|
||||
:: %config
|
||||
?: =(%config -.upd)
|
||||
?> ?=(%config -.upd)
|
||||
:- %config
|
||||
%- pairs
|
||||
:~
|
||||
[%circle (circ:enjs:hall-json cir.upd)]
|
||||
[%config (conf:enjs:hall-json con.upd)]
|
||||
==
|
||||
::
|
||||
:: %circles
|
||||
?: =(%circles -.upd)
|
||||
?> ?=(%circles -.upd)
|
||||
:- %circles
|
||||
%- pairs
|
||||
:~
|
||||
:- %circles
|
||||
:- %a
|
||||
%+ turn ~(tap in cir.upd)
|
||||
|= nom=name:hall
|
||||
[%s nom]
|
||||
==
|
||||
::
|
||||
:: %peers
|
||||
?: =(%peers -.upd)
|
||||
?> ?=(%peers -.upd)
|
||||
:- %peers
|
||||
%- pairs
|
||||
:~
|
||||
[%circle (circ:enjs:hall-json cir.upd)]
|
||||
[%peers [%a (turn ~(tap in per.upd) ship:enjs:format)]]
|
||||
==
|
||||
::
|
||||
:: %delete
|
||||
?: =(%delete -.upd)
|
||||
?> ?=(%delete -.upd)
|
||||
:- %delete
|
||||
%- pairs
|
||||
:~
|
||||
[%circle (circ:enjs:hall-json cir.upd)]
|
||||
==
|
||||
::
|
||||
:: %noop
|
||||
[*@t *^json]
|
||||
==
|
||||
--
|
||||
::
|
||||
++ grab
|
||||
|%
|
||||
++ noun update:chat
|
||||
--
|
||||
--
|
63
pkg/interface/apps/clock/.gitignore
vendored
Normal file
63
pkg/interface/apps/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/apps/clock/LICENSE
Normal file
21
pkg/interface/apps/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/apps/clock/README.md
Normal file
10
pkg/interface/apps/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.
|
136
pkg/interface/apps/clock/gulpfile.js
Normal file
136
pkg/interface/apps/clock/gulpfile.js
Normal file
@ -0,0 +1,136 @@
|
||||
var gulp = require('gulp');
|
||||
var cssimport = require('gulp-cssimport');
|
||||
var rollup = require('gulp-better-rollup');
|
||||
var cssnano = require('cssnano');
|
||||
var autoprefixer = require('autoprefixer');
|
||||
var postcss = require('gulp-postcss')
|
||||
var sucrase = require('@sucrase/gulp-plugin');
|
||||
var minify = require('gulp-minify');
|
||||
var exec = require('child_process').exec;
|
||||
|
||||
var resolve = require('rollup-plugin-node-resolve');
|
||||
var commonjs = require('rollup-plugin-commonjs');
|
||||
var replace = require('rollup-plugin-replace');
|
||||
var json = require('rollup-plugin-json');
|
||||
var builtins = require('@joseph184/rollup-plugin-node-builtins');
|
||||
var rootImport = require('rollup-plugin-root-import');
|
||||
var globals = require('rollup-plugin-node-globals');
|
||||
|
||||
/***
|
||||
Main config options
|
||||
***/
|
||||
|
||||
var urbitrc = require('./.urbitrc');
|
||||
|
||||
/***
|
||||
End main config options
|
||||
***/
|
||||
|
||||
gulp.task('jsx-transform', function (cb) {
|
||||
return gulp.src('src/**/*.js')
|
||||
.pipe(sucrase({
|
||||
transforms: ['jsx']
|
||||
}))
|
||||
.pipe(gulp.dest('dist'));
|
||||
});
|
||||
|
||||
gulp.task('tile-jsx-transform', function (cb) {
|
||||
return gulp.src('tile/**/*.js')
|
||||
.pipe(sucrase({
|
||||
transforms: ['jsx']
|
||||
}))
|
||||
.pipe(gulp.dest('dist'));
|
||||
});
|
||||
|
||||
|
||||
gulp.task('js-imports', function (cb) {
|
||||
return gulp.src('dist/index.js')
|
||||
.pipe(rollup({
|
||||
plugins: [
|
||||
commonjs({
|
||||
namedExports: {
|
||||
'node_modules/react/index.js': ['Component'],
|
||||
'node_modules/react-is/index.js': ['isValidElementType'],
|
||||
}
|
||||
}),
|
||||
rootImport({
|
||||
root: `${__dirname}/dist/js`,
|
||||
useEntry: 'prepend',
|
||||
extensions: '.js'
|
||||
}),
|
||||
json(),
|
||||
globals(),
|
||||
builtins(),
|
||||
resolve()
|
||||
]
|
||||
}, 'umd'))
|
||||
.on('error', function (e) {
|
||||
console.log(e);
|
||||
cb();
|
||||
})
|
||||
.pipe(gulp.dest('./urbit/app/clock/js/'))
|
||||
.on('end', cb);
|
||||
});
|
||||
|
||||
gulp.task('tile-js-imports', function (cb) {
|
||||
return gulp.src('dist/tile.js')
|
||||
.pipe(rollup({
|
||||
plugins: [
|
||||
commonjs({
|
||||
namedExports: {
|
||||
'node_modules/react/index.js': ['Component'],
|
||||
}
|
||||
}),
|
||||
rootImport({
|
||||
root: `${__dirname}/dist/js`,
|
||||
useEntry: 'prepend',
|
||||
extensions: '.js'
|
||||
}),
|
||||
json(),
|
||||
globals(),
|
||||
builtins(),
|
||||
resolve()
|
||||
]
|
||||
}, 'umd'))
|
||||
.on('error', function (e) {
|
||||
console.log(e);
|
||||
cb();
|
||||
})
|
||||
.pipe(gulp.dest('./urbit/app/clock/js/'))
|
||||
.on('end', cb);
|
||||
});
|
||||
|
||||
|
||||
gulp.task('js-minify', function () {
|
||||
return gulp.src('./urbit/app/clock/js/index.js')
|
||||
.pipe(minify())
|
||||
.pipe(gulp.dest('./urbit/app/clock/js/'));
|
||||
});
|
||||
|
||||
gulp.task('tile-js-minify', function () {
|
||||
return gulp.src('./urbit/app/clock/js/tile.js')
|
||||
.pipe(minify())
|
||||
.pipe(gulp.dest('./urbit/app/clock/js/'));
|
||||
});
|
||||
|
||||
gulp.task('urbit-copy', function () {
|
||||
let ret = gulp.src('urbit/**/*');
|
||||
|
||||
urbitrc.URBIT_PIERS.forEach(function (pier) {
|
||||
ret = ret.pipe(gulp.dest(pier));
|
||||
});
|
||||
|
||||
return ret;
|
||||
});
|
||||
|
||||
gulp.task('tile-js-bundle-dev', gulp.series('tile-jsx-transform', 'tile-js-imports'));
|
||||
gulp.task('tile-js-bundle-prod',
|
||||
gulp.series('tile-jsx-transform', 'tile-js-imports', 'tile-js-minify'));
|
||||
|
||||
gulp.task('bundle-prod', gulp.series('tile-js-bundle-prod', 'urbit-copy'));
|
||||
|
||||
gulp.task('default', gulp.series('tile-js-bundle-dev', 'urbit-copy'));
|
||||
gulp.task('watch', gulp.series('default', function () {
|
||||
gulp.watch('tile/**/*.js', gulp.parallel('tile-js-bundle-dev'));
|
||||
gulp.watch('urbit/**/*', gulp.parallel('urbit-copy'));
|
||||
}));
|
44
pkg/interface/apps/clock/package.json
Normal file
44
pkg/interface/apps/clock/package.json
Normal file
@ -0,0 +1,44 @@
|
||||
{
|
||||
"name": "urbit-apps",
|
||||
"version": "1.0.0",
|
||||
"description": "",
|
||||
"main": "index.js",
|
||||
"scripts": {
|
||||
"test": "echo \"Error: no test specified\" && exit 1"
|
||||
},
|
||||
"author": "",
|
||||
"license": "ISC",
|
||||
"devDependencies": {
|
||||
"@joseph184/rollup-plugin-node-builtins": "^2.1.4",
|
||||
"@sucrase/gulp-plugin": "^2.0.0",
|
||||
"autoprefixer": "^9.6.1",
|
||||
"cssnano": "^4.1.10",
|
||||
"gulp": "^4.0.0",
|
||||
"gulp-better-rollup": "^4.0.1",
|
||||
"gulp-cssimport": "^7.0.0",
|
||||
"gulp-minify": "^3.1.0",
|
||||
"gulp-postcss": "^8.0.0",
|
||||
"rollup": "^1.6.0",
|
||||
"rollup-plugin-commonjs": "^9.2.0",
|
||||
"rollup-plugin-json": "^2.3.0",
|
||||
"rollup-plugin-node-globals": "^1.4.0",
|
||||
"rollup-plugin-node-resolve": "^3.4.0",
|
||||
"rollup-plugin-replace": "^2.0.0",
|
||||
"rollup-plugin-root-import": "^0.2.3",
|
||||
"sucrase": "^3.8.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"classnames": "^2.2.6",
|
||||
"lodash": "^4.17.11",
|
||||
"moment": "^2.20.1",
|
||||
"mousetrap": "^1.6.1",
|
||||
"react": "^16.5.2",
|
||||
"react-custom-scrollbars": "^4.2.1",
|
||||
"react-dom": "^16.8.6",
|
||||
"react-router-dom": "^5.0.0",
|
||||
"urbit-ob": "^3.1.1"
|
||||
},
|
||||
"resolutions": {
|
||||
"natives": "1.1.3"
|
||||
}
|
||||
}
|
204
pkg/interface/apps/clock/tile/tile.js
Normal file
204
pkg/interface/apps/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;
|
80
pkg/interface/apps/clock/urbit/app/clock.hoon
Normal file
80
pkg/interface/apps/clock/urbit/app/clock.hoon
Normal file
@ -0,0 +1,80 @@
|
||||
/+ *server
|
||||
/= tile-js
|
||||
/^ octs
|
||||
/; as-octs:mimes:html
|
||||
/: /===/app/clock/js/tile
|
||||
/| /js/
|
||||
/~ ~
|
||||
==
|
||||
=, format
|
||||
::
|
||||
|%
|
||||
:: +move: output effect
|
||||
::
|
||||
+$ move [bone card]
|
||||
:: +card: output effect payload
|
||||
::
|
||||
+$ poke
|
||||
$% [%launch-action [@tas path @t]]
|
||||
==
|
||||
::
|
||||
+$ card
|
||||
$% [%poke wire dock poke]
|
||||
[%http-response =http-event:http]
|
||||
[%connect wire binding:eyre term]
|
||||
[%diff %json json]
|
||||
==
|
||||
::
|
||||
--
|
||||
::
|
||||
|_ [bol=bowl:gall ~]
|
||||
::
|
||||
++ this .
|
||||
::
|
||||
++ bound
|
||||
|= [wir=wire success=? binding=binding:eyre]
|
||||
^- (quip move _this)
|
||||
[~ this]
|
||||
::
|
||||
++ prep
|
||||
|= old=(unit ~)
|
||||
^- (quip move _this)
|
||||
=/ launcha
|
||||
[%launch-action [%clock /tile '/~clock/js/tile.js']]
|
||||
:_ this
|
||||
:~
|
||||
[ost.bol %connect / [~ /'~clock'] %clock]
|
||||
[ost.bol %poke /clock [our.bol %launch] launcha]
|
||||
==
|
||||
::
|
||||
++ peer-tile
|
||||
|= pax=path
|
||||
^- (quip move _this)
|
||||
[[ost.bol %diff %json *json]~ this]
|
||||
::
|
||||
++ send-tile-diff
|
||||
|= jon=json
|
||||
^- (list move)
|
||||
%+ turn (prey:pubsub:userlib /tile bol)
|
||||
|= [=bone ^]
|
||||
[bone %diff %json jon]
|
||||
::
|
||||
++ poke-handle-http-request
|
||||
%- (require-authorization:app ost.bol move this)
|
||||
|= =inbound-request:eyre
|
||||
^- (quip move _this)
|
||||
=/ request-line (parse-request-line url.request.inbound-request)
|
||||
=/ back-path (flop site.request-line)
|
||||
=/ name=@t
|
||||
=/ back-path (flop site.request-line)
|
||||
?~ back-path
|
||||
''
|
||||
i.back-path
|
||||
::
|
||||
?~ back-path
|
||||
[[ost.bol %http-response not-found:app]~ this]
|
||||
?: =(name 'tile')
|
||||
[[ost.bol %http-response (js-response:app tile-js)]~ this]
|
||||
[[ost.bol %http-response not-found:app]~ this]
|
||||
::
|
||||
--
|
5
pkg/interface/apps/clock/urbitrc-sample
Normal file
5
pkg/interface/apps/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/apps/launch/.gitignore
vendored
Normal file
9
pkg/interface/apps/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/apps/launch/.urbitrc-sample
Normal file
5
pkg/interface/apps/launch/.urbitrc-sample
Normal file
@ -0,0 +1,5 @@
|
||||
module.exports = {
|
||||
URBIT_PIERS: [
|
||||
"/Users/bono/urbit/piers/zod/home"
|
||||
]
|
||||
};
|
138
pkg/interface/apps/launch/gulpfile.js
Normal file
138
pkg/interface/apps/launch/gulpfile.js
Normal file
@ -0,0 +1,138 @@
|
||||
var gulp = require('gulp');
|
||||
var cssimport = require('gulp-cssimport');
|
||||
var rollup = require('gulp-better-rollup');
|
||||
var cssnano = require('cssnano');
|
||||
var autoprefixer = require('autoprefixer');
|
||||
var postcss = require('gulp-postcss')
|
||||
var sucrase = require('@sucrase/gulp-plugin');
|
||||
var minify = require('gulp-minify');
|
||||
var exec = require('child_process').exec;
|
||||
|
||||
var resolve = require('rollup-plugin-node-resolve');
|
||||
var commonjs = require('rollup-plugin-commonjs');
|
||||
var replace = require('rollup-plugin-replace');
|
||||
var json = require('rollup-plugin-json');
|
||||
var builtins = require('@joseph184/rollup-plugin-node-builtins');
|
||||
var rootImport = require('rollup-plugin-root-import');
|
||||
var globals = require('rollup-plugin-node-globals');
|
||||
|
||||
/***
|
||||
Main config options
|
||||
***/
|
||||
|
||||
var urbitrc = require('./.urbitrc');
|
||||
|
||||
/***
|
||||
End main config options
|
||||
***/
|
||||
|
||||
gulp.task('css-bundle', function() {
|
||||
let plugins = [
|
||||
autoprefixer({ browsers: ['last 1 version'] }),
|
||||
cssnano()
|
||||
];
|
||||
return gulp
|
||||
.src('src/index.css')
|
||||
.pipe(cssimport())
|
||||
.pipe(postcss(plugins))
|
||||
.pipe(gulp.dest('./urbit/app/launch/css'));
|
||||
});
|
||||
|
||||
gulp.task('jsx-transform', function(cb) {
|
||||
return gulp.src('src/**/*.js')
|
||||
.pipe(sucrase({
|
||||
transforms: ['jsx']
|
||||
}))
|
||||
.pipe(gulp.dest('dist'));
|
||||
});
|
||||
|
||||
gulp.task('js-imports', function(cb) {
|
||||
return gulp.src('dist/index.js')
|
||||
.pipe(rollup({
|
||||
plugins: [
|
||||
commonjs({
|
||||
namedExports: {
|
||||
'node_modules/react/index.js': [ 'Component' ],
|
||||
'node_modules/react-is/index.js': [ 'isValidElementType' ],
|
||||
}
|
||||
}),
|
||||
replace({
|
||||
'process.env.NODE_ENV': JSON.stringify('development')
|
||||
}),
|
||||
rootImport({
|
||||
root: `${__dirname}/dist/js`,
|
||||
useEntry: 'prepend',
|
||||
extensions: '.js'
|
||||
}),
|
||||
json(),
|
||||
globals(),
|
||||
builtins(),
|
||||
resolve()
|
||||
]
|
||||
}, 'umd'))
|
||||
.on('error', function(e){
|
||||
console.log(e);
|
||||
cb();
|
||||
})
|
||||
.pipe(gulp.dest('./urbit/app/launch/js/'))
|
||||
.on('end', cb);
|
||||
});
|
||||
|
||||
gulp.task('js-minify', function () {
|
||||
return gulp.src('./urbit/app/launch/js/index.js')
|
||||
.pipe(minify())
|
||||
.pipe(gulp.dest('./urbit/app/launch/js/'));
|
||||
});
|
||||
|
||||
gulp.task('js-cachebust', function(cb) {
|
||||
return Promise.resolve(
|
||||
exec('git log', function (err, stdout, stderr) {
|
||||
let firstLine = stdout.split("\n")[0];
|
||||
let commitHash = firstLine.split(' ')[1].substr(0, 10);
|
||||
let newFilename = "index-" + commitHash + "-min.js";
|
||||
|
||||
exec('mv ./urbit/app/launch/js/index-min.js ./urbit/app/launch/js/' + newFilename);
|
||||
})
|
||||
);
|
||||
})
|
||||
|
||||
gulp.task('urbit-copy', function () {
|
||||
let ret = gulp.src('urbit/**/*');
|
||||
|
||||
urbitrc.URBIT_PIERS.forEach(function(pier) {
|
||||
ret = ret.pipe(gulp.dest(pier));
|
||||
});
|
||||
|
||||
return ret;
|
||||
});
|
||||
|
||||
gulp.task('js-bundle-dev', gulp.series('jsx-transform', 'js-imports'));
|
||||
gulp.task('js-bundle-prod', gulp.series('jsx-transform', 'js-imports', 'js-minify'))
|
||||
|
||||
gulp.task('bundle-dev',
|
||||
gulp.series(
|
||||
gulp.parallel(
|
||||
'css-bundle',
|
||||
'js-bundle-dev'
|
||||
),
|
||||
'urbit-copy'
|
||||
)
|
||||
);
|
||||
|
||||
gulp.task('bundle-prod',
|
||||
gulp.series(
|
||||
gulp.parallel(
|
||||
'css-bundle',
|
||||
'js-bundle-prod'
|
||||
),
|
||||
'urbit-copy'
|
||||
)
|
||||
);
|
||||
|
||||
gulp.task('default', gulp.series('bundle-dev'));
|
||||
gulp.task('watch', gulp.series('default', function() {
|
||||
gulp.watch('src/**/*.js', gulp.parallel('js-bundle-dev'));
|
||||
gulp.watch('src/**/*.css', gulp.parallel('css-bundle'));
|
||||
|
||||
gulp.watch('urbit/**/*', gulp.parallel('urbit-copy'));
|
||||
}));
|
6763
pkg/interface/apps/launch/package-lock.json
generated
Normal file
6763
pkg/interface/apps/launch/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
44
pkg/interface/apps/launch/package.json
Normal file
44
pkg/interface/apps/launch/package.json
Normal file
@ -0,0 +1,44 @@
|
||||
{
|
||||
"name": "urbit-apps",
|
||||
"version": "1.0.0",
|
||||
"description": "",
|
||||
"main": "index.js",
|
||||
"scripts": {
|
||||
"test": "echo \"Error: no test specified\" && exit 1"
|
||||
},
|
||||
"author": "",
|
||||
"license": "ISC",
|
||||
"devDependencies": {
|
||||
"@joseph184/rollup-plugin-node-builtins": "^2.1.4",
|
||||
"@sucrase/gulp-plugin": "^2.0.0",
|
||||
"autoprefixer": "^9.6.1",
|
||||
"cssnano": "^4.1.10",
|
||||
"gulp": "^4.0.0",
|
||||
"gulp-better-rollup": "^4.0.1",
|
||||
"gulp-cssimport": "^7.0.0",
|
||||
"gulp-minify": "^3.1.0",
|
||||
"gulp-postcss": "^8.0.0",
|
||||
"rollup": "^1.6.0",
|
||||
"rollup-plugin-commonjs": "^9.2.0",
|
||||
"rollup-plugin-json": "^2.3.0",
|
||||
"rollup-plugin-node-globals": "^1.4.0",
|
||||
"rollup-plugin-node-resolve": "^3.4.0",
|
||||
"rollup-plugin-replace": "^2.0.0",
|
||||
"rollup-plugin-root-import": "^0.2.3",
|
||||
"sucrase": "^3.8.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"classnames": "^2.2.6",
|
||||
"lodash": "^4.17.11",
|
||||
"moment": "^2.20.1",
|
||||
"mousetrap": "^1.6.1",
|
||||
"react": "^16.5.2",
|
||||
"react-custom-scrollbars": "^4.2.1",
|
||||
"react-dom": "^16.8.6",
|
||||
"react-router-dom": "^5.0.0",
|
||||
"urbit-ob": "^3.1.1"
|
||||
},
|
||||
"resolutions": {
|
||||
"natives": "1.1.3"
|
||||
}
|
||||
}
|
74
pkg/interface/apps/launch/src/css/custom.css
Normal file
74
pkg/interface/apps/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/apps/launch/src/css/fonts.css
Normal file
63
pkg/interface/apps/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/apps/launch/src/css/tachyons.css
Normal file
2
pkg/interface/apps/launch/src/css/tachyons.css
Normal file
File diff suppressed because one or more lines are too long
4
pkg/interface/apps/launch/src/index.css
Normal file
4
pkg/interface/apps/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/apps/launch/src/index.js
Normal file
13
pkg/interface/apps/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/apps/launch/src/js/app.js
Normal file
38
pkg/interface/apps/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/apps/launch/src/js/components/dropdown.js
Normal file
46
pkg/interface/apps/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/apps/launch/src/js/components/header.js
Normal file
55
pkg/interface/apps/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/apps/launch/src/js/components/home.js
Normal file
36
pkg/interface/apps/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/apps/launch/src/js/components/tile.js
Normal file
27
pkg/interface/apps/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/apps/launch/src/js/lib/api.js
Normal file
37
pkg/interface/apps/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/apps/launch/src/js/lib/util.js
Normal file
12
pkg/interface/apps/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/apps/launch/src/js/store.js
Normal file
20
pkg/interface/apps/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/apps/launch/src/js/subscription.js
Normal file
44
pkg/interface/apps/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;
|
133
pkg/interface/apps/launch/urbit/app/launch.hoon
Normal file
133
pkg/interface/apps/launch/urbit/app/launch.hoon
Normal file
@ -0,0 +1,133 @@
|
||||
/+ *server, launch
|
||||
/= index
|
||||
/^ $-(marl manx)
|
||||
/: /===/app/launch/index /!noun/
|
||||
/= script
|
||||
/^ octs
|
||||
/; as-octs:mimes:html
|
||||
/: /===/app/launch/js/index
|
||||
/| /js/
|
||||
/~ ~
|
||||
==
|
||||
/= style
|
||||
/^ octs
|
||||
/; as-octs:mimes:html
|
||||
/: /===/app/launch/css/index
|
||||
/| /css/
|
||||
/~ ~
|
||||
==
|
||||
/= launch-png
|
||||
/^ (map knot @)
|
||||
/: /===/app/launch/img /_ /png/
|
||||
::
|
||||
=, launch
|
||||
::
|
||||
|_ [bol=bowl:gall sta=state]
|
||||
::
|
||||
++ this .
|
||||
::
|
||||
++ prep
|
||||
|= old=(unit state)
|
||||
^- (quip move _this)
|
||||
?~ old
|
||||
:_ this
|
||||
[ost.bol %connect / [~ /] %launch]~
|
||||
[~ this(sta u.old)]
|
||||
::
|
||||
++ poke-launch-action
|
||||
|= act=action:launch
|
||||
^- (quip move _this)
|
||||
=/ beforedata (~(get by data.sta) name.act)
|
||||
=/ newdata
|
||||
?~ beforedata
|
||||
(~(put by data.sta) name.act [*json url.act])
|
||||
(~(put by data.sta) name.act [jon.u.beforedata url.act])
|
||||
:- [ost.bol %peer subscribe.act [our.bol name.act] subscribe.act]~
|
||||
%= this
|
||||
tiles.sta (~(put in tiles.sta) [name.act subscribe.act])
|
||||
data.sta newdata
|
||||
path-to-tile.sta (~(put by path-to-tile.sta) subscribe.act name.act)
|
||||
==
|
||||
::
|
||||
++ peer-main
|
||||
|= [pax=path]
|
||||
^- (quip move _this)
|
||||
=/ data/json
|
||||
%- pairs:enjs:format
|
||||
%+ turn ~(tap by data.sta)
|
||||
|= [key=@tas [jon=json url=@t]]
|
||||
[key jon]
|
||||
:_ this
|
||||
[ost.bol %diff %json data]~
|
||||
::
|
||||
++ diff-json
|
||||
|= [pax=path jon=json]
|
||||
^- (quip move _this)
|
||||
=/ name/@tas (~(got by path-to-tile.sta) pax)
|
||||
=/ data/(unit [json url=@t]) (~(get by data.sta) name)
|
||||
?~ data
|
||||
[~ this]
|
||||
::
|
||||
:-
|
||||
%+ turn (prey:pubsub:userlib /main bol)
|
||||
|= [=bone *]
|
||||
[bone %diff %json (frond:enjs:format name jon)]
|
||||
::
|
||||
%= this
|
||||
data.sta (~(put by data.sta) name [jon url.u.data])
|
||||
==
|
||||
::
|
||||
++ generate-script-marl
|
||||
|= data=tile-data
|
||||
^- marl
|
||||
%+ turn ~(tap by data)
|
||||
|= [key=@tas [jon=json url=@t]]
|
||||
^- manx
|
||||
;script@"{(trip url)}";
|
||||
::
|
||||
++ poke-handle-http-request
|
||||
%- (require-authorization:app ost.bol move this)
|
||||
|= =inbound-request:eyre
|
||||
^- (quip move _this)
|
||||
::
|
||||
=/ request-line (parse-request-line url.request.inbound-request)
|
||||
=/ name=@t
|
||||
=/ back-path (flop site.request-line)
|
||||
?~ back-path
|
||||
''
|
||||
i.back-path
|
||||
=/ site (flop site.request-line)
|
||||
?~ site
|
||||
=/ hym=manx (index (generate-script-marl data.sta))
|
||||
:_ this
|
||||
[ost.bol %http-response (manx-response:app hym)]~
|
||||
?+ site.request-line
|
||||
:_ this
|
||||
[ost.bol %http-response not-found:app]~
|
||||
::
|
||||
:: styling
|
||||
::
|
||||
[%'~launch' %css %index ~]
|
||||
:_ this
|
||||
[ost.bol %http-response (css-response:app style)]~
|
||||
::
|
||||
:: javascript
|
||||
::
|
||||
[%'~launch' %js %index ~]
|
||||
:_ this
|
||||
[ost.bol %http-response (js-response:app script)]~
|
||||
::
|
||||
:: images
|
||||
::
|
||||
[%'~launch' %img *]
|
||||
=/ img (as-octs:mimes:html (~(got by launch-png) `@ta`name))
|
||||
:_ this
|
||||
[ost.bol %http-response (png-response:app img)]~
|
||||
==
|
||||
::
|
||||
++ bound
|
||||
|= [wir=wire success=? binding=binding:eyre]
|
||||
^- (quip move _this)
|
||||
[~ this]
|
||||
::
|
||||
--
|
BIN
pkg/interface/apps/launch/urbit/app/launch/img/Favicon.png
Normal file
BIN
pkg/interface/apps/launch/urbit/app/launch/img/Favicon.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 2.8 KiB |
BIN
pkg/interface/apps/launch/urbit/app/launch/img/Home.png
Normal file
BIN
pkg/interface/apps/launch/urbit/app/launch/img/Home.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 255 B |
18
pkg/interface/apps/launch/urbit/app/launch/index.hoon
Normal file
18
pkg/interface/apps/launch/urbit/app/launch/index.hoon
Normal file
@ -0,0 +1,18 @@
|
||||
|= scripts=marl
|
||||
;html
|
||||
;head
|
||||
;title: Home
|
||||
;meta(charset "utf-8");
|
||||
;meta
|
||||
=name "viewport"
|
||||
=content "width=device-width, initial-scale=1, shrink-to-fit=no";
|
||||
;link(rel "stylesheet", href "/~launch/css/index.css");
|
||||
==
|
||||
;body
|
||||
;div#root;
|
||||
;script@"/~/channel/channel.js";
|
||||
;script@"/~modulo/session.js";
|
||||
;* scripts
|
||||
;script@"/~launch/js/index.js";
|
||||
==
|
||||
==
|
24
pkg/interface/apps/launch/urbit/lib/launch.hoon
Normal file
24
pkg/interface/apps/launch/urbit/lib/launch.hoon
Normal file
@ -0,0 +1,24 @@
|
||||
::
|
||||
|%
|
||||
::
|
||||
+$ move [bone card]
|
||||
::
|
||||
+$ card
|
||||
$% [%http-response =http-event:http]
|
||||
[%connect wire binding:eyre term]
|
||||
[%peer wire dock path]
|
||||
[%diff %json json]
|
||||
==
|
||||
::
|
||||
+$ tile [name=@tas subscribe=path]
|
||||
::
|
||||
+$ tile-data (map @tas [jon=json url=@t])
|
||||
::
|
||||
+$ action [name=@tas subscribe=path url=@t]
|
||||
::
|
||||
+$ state
|
||||
$% [%0 tiles=(set tile) data=tile-data path-to-tile=(map path @tas)]
|
||||
==
|
||||
::
|
||||
--
|
||||
::
|
11
pkg/interface/apps/launch/urbit/mar/launch/action.hoon
Normal file
11
pkg/interface/apps/launch/urbit/mar/launch/action.hoon
Normal file
@ -0,0 +1,11 @@
|
||||
::
|
||||
::
|
||||
/+ launch
|
||||
::
|
||||
|_ act=action:launch
|
||||
::
|
||||
++ grab
|
||||
|%
|
||||
++ noun action:launch
|
||||
--
|
||||
--
|
189
pkg/interface/apps/publish/gulpfile.js
Normal file
189
pkg/interface/apps/publish/gulpfile.js
Normal file
@ -0,0 +1,189 @@
|
||||
var gulp = require('gulp');
|
||||
var cssimport = require('gulp-cssimport');
|
||||
var rollup = require('gulp-better-rollup');
|
||||
var cssnano = require('cssnano');
|
||||
var autoprefixer = require('autoprefixer');
|
||||
var postcss = require('gulp-postcss')
|
||||
var sucrase = require('@sucrase/gulp-plugin');
|
||||
var minify = require('gulp-minify');
|
||||
var exec = require('child_process').exec;
|
||||
|
||||
var resolve = require('rollup-plugin-node-resolve');
|
||||
var commonjs = require('rollup-plugin-commonjs');
|
||||
var replace = require('rollup-plugin-replace');
|
||||
var json = require('rollup-plugin-json');
|
||||
var builtins = require('@joseph184/rollup-plugin-node-builtins');
|
||||
var rootImport = require('rollup-plugin-root-import');
|
||||
var globals = require('rollup-plugin-node-globals');
|
||||
|
||||
/***
|
||||
Main config options
|
||||
***/
|
||||
|
||||
var urbitrc = require('./.urbitrc');
|
||||
|
||||
/***
|
||||
End main config options
|
||||
***/
|
||||
|
||||
gulp.task('css-bundle', function() {
|
||||
let plugins = [
|
||||
autoprefixer({ browsers: ['last 1 version'] }),
|
||||
cssnano()
|
||||
];
|
||||
return gulp
|
||||
.src('src/index.css')
|
||||
.pipe(cssimport())
|
||||
.pipe(postcss(plugins))
|
||||
.pipe(gulp.dest('./urbit/app/publish/css'));
|
||||
});
|
||||
|
||||
gulp.task('jsx-transform', function(cb) {
|
||||
return gulp.src('src/**/*.js')
|
||||
.pipe(sucrase({
|
||||
transforms: ['jsx']
|
||||
}))
|
||||
.pipe(gulp.dest('dist'));
|
||||
});
|
||||
|
||||
gulp.task('tile-jsx-transform', function(cb) {
|
||||
return gulp.src('tile/**/*.js')
|
||||
.pipe(sucrase({
|
||||
transforms: ['jsx']
|
||||
}))
|
||||
.pipe(gulp.dest('dist'));
|
||||
});
|
||||
|
||||
gulp.task('js-imports', function(cb) {
|
||||
return gulp.src('dist/index.js')
|
||||
.pipe(rollup({
|
||||
plugins: [
|
||||
commonjs({
|
||||
namedExports: {
|
||||
'node_modules/react/index.js': [ 'Component' ],
|
||||
'node_modules/react-is/index.js': [ 'isValidElementType' ],
|
||||
}
|
||||
}),
|
||||
replace({
|
||||
'process.env.NODE_ENV': JSON.stringify('development')
|
||||
}),
|
||||
rootImport({
|
||||
root: `${__dirname}/dist/js`,
|
||||
useEntry: 'prepend',
|
||||
extensions: '.js'
|
||||
}),
|
||||
json(),
|
||||
globals(),
|
||||
builtins(),
|
||||
resolve()
|
||||
]
|
||||
}, 'umd'))
|
||||
.on('error', function(e){
|
||||
console.log(e);
|
||||
cb();
|
||||
})
|
||||
.pipe(gulp.dest('./urbit/app/publish/js/'))
|
||||
.on('end', cb);
|
||||
});
|
||||
|
||||
gulp.task('tile-js-imports', function(cb) {
|
||||
return gulp.src('dist/tile.js')
|
||||
.pipe(rollup({
|
||||
plugins: [
|
||||
commonjs({
|
||||
namedExports: {
|
||||
'node_modules/react/index.js': [ 'Component' ],
|
||||
}
|
||||
}),
|
||||
rootImport({
|
||||
root: `${__dirname}/dist/js`,
|
||||
useEntry: 'prepend',
|
||||
extensions: '.js'
|
||||
}),
|
||||
json(),
|
||||
globals(),
|
||||
builtins(),
|
||||
resolve()
|
||||
]
|
||||
}, 'umd'))
|
||||
.on('error', function(e){
|
||||
console.log(e);
|
||||
cb();
|
||||
})
|
||||
.pipe(gulp.dest('./urbit/app/publish/js/'))
|
||||
.on('end', cb);
|
||||
});
|
||||
|
||||
|
||||
gulp.task('js-minify', function () {
|
||||
return gulp.src('./urbit/app/publish/js/index.js')
|
||||
.pipe(minify())
|
||||
.pipe(gulp.dest('./urbit/app/publish/js/'));
|
||||
});
|
||||
|
||||
gulp.task('tile-js-minify', function () {
|
||||
return gulp.src('./urbit/app/publish/js/tile.js')
|
||||
.pipe(minify())
|
||||
.pipe(gulp.dest('./urbit/app/publish/js/'));
|
||||
});
|
||||
|
||||
gulp.task('js-cachebust', function(cb) {
|
||||
return Promise.resolve(
|
||||
exec('git log', function (err, stdout, stderr) {
|
||||
let firstLine = stdout.split("\n")[0];
|
||||
let commitHash = firstLine.split(' ')[1].substr(0, 10);
|
||||
let newFilename = "index-" + commitHash + "-min.js";
|
||||
|
||||
exec('mv ./urbit/app/publish/js/index-min.js ./urbit/app/publish/js/' + newFilename);
|
||||
})
|
||||
);
|
||||
})
|
||||
|
||||
gulp.task('urbit-copy', function () {
|
||||
let ret = gulp.src('urbit/**/*');
|
||||
|
||||
urbitrc.URBIT_PIERS.forEach(function(pier) {
|
||||
ret = ret.pipe(gulp.dest(pier));
|
||||
});
|
||||
|
||||
return ret;
|
||||
});
|
||||
|
||||
gulp.task('js-bundle-dev', gulp.series('jsx-transform', 'js-imports'));
|
||||
gulp.task('tile-js-bundle-dev', gulp.series('tile-jsx-transform', 'tile-js-imports'));
|
||||
gulp.task('js-bundle-prod', gulp.series('jsx-transform', 'js-imports', 'js-minify'))
|
||||
gulp.task('tile-js-bundle-prod',
|
||||
gulp.series('tile-jsx-transform', 'tile-js-imports', 'tile-js-minify'));
|
||||
|
||||
gulp.task('bundle-dev',
|
||||
gulp.series(
|
||||
gulp.parallel(
|
||||
'css-bundle',
|
||||
'js-bundle-dev',
|
||||
'tile-js-bundle-dev'
|
||||
),
|
||||
'urbit-copy'
|
||||
)
|
||||
);
|
||||
|
||||
gulp.task('bundle-prod',
|
||||
gulp.series(
|
||||
gulp.parallel(
|
||||
'css-bundle',
|
||||
'js-bundle-prod',
|
||||
'tile-js-bundle-prod',
|
||||
),
|
||||
'urbit-copy'
|
||||
)
|
||||
);
|
||||
|
||||
gulp.task('default', gulp.series('bundle-dev'));
|
||||
|
||||
gulp.task('watch', gulp.series('default', function() {
|
||||
gulp.watch('tile/**/*.js', gulp.parallel('tile-js-bundle-dev'));
|
||||
|
||||
gulp.watch('src/**/*.js', gulp.parallel('js-bundle-dev'));
|
||||
gulp.watch('src/**/*.css', gulp.parallel('css-bundle'));
|
||||
|
||||
gulp.watch('urbit/**/*', gulp.parallel('urbit-copy'));
|
||||
}));
|
6597
pkg/interface/apps/publish/package-lock.json
generated
Normal file
6597
pkg/interface/apps/publish/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
45
pkg/interface/apps/publish/package.json
Normal file
45
pkg/interface/apps/publish/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",
|
||||
"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/apps/publish/src/css/custom.css
Normal file
275
pkg/interface/apps/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/apps/publish/src/css/fonts.css
Normal file
63
pkg/interface/apps/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/apps/publish/src/css/spinner.css
Normal file
40
pkg/interface/apps/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/apps/publish/src/css/tachyons.css
Normal file
2
pkg/interface/apps/publish/src/css/tachyons.css
Normal file
File diff suppressed because one or more lines are too long
5
pkg/interface/apps/publish/src/index.css
Normal file
5
pkg/interface/apps/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/apps/publish/src/index.js
Normal file
33
pkg/interface/apps/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/apps/publish/src/js/api.js
Normal file
48
pkg/interface/apps/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/apps/publish/src/js/components/blog.js
Normal file
369
pkg/interface/apps/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>
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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>
|
||||
);
|
||||
}
|
||||
}
|
@ -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>
|
||||
);
|
||||
}
|
||||
}
|
@ -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/apps/publish/src/js/components/lib/blog-subs.js
Normal file
140
pkg/interface/apps/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>
|
||||
);
|
||||
}
|
||||
}
|
@ -0,0 +1,78 @@
|
||||
import React, { Component } from 'react';
|
||||
import classnames from 'classnames';
|
||||
import { Sigil } from '/components/lib/icons/sigil';
|
||||
|
||||
class PostButton extends Component {
|
||||
render() {
|
||||
if (this.props.enabled) {
|
||||
return (
|
||||
<p className="body-regular pointer" onClick={this.props.post}>
|
||||
-> Post
|
||||
</p>
|
||||
);
|
||||
} else {
|
||||
return (
|
||||
<p className="body-regular gray-30">
|
||||
-> Post
|
||||
</p>
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export class CommentBox extends Component {
|
||||
constructor(props){
|
||||
super(props);
|
||||
|
||||
this.commentChange = this.commentChange.bind(this);
|
||||
this.commentHeight = 54;
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps, prevState) {
|
||||
if (!prevProps.enabled && this.props.enabled) {
|
||||
if (this.commentInput) {
|
||||
this.commentInput.value = '';
|
||||
this.commentInput.style.height = 54;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
commentChange(evt) {
|
||||
this.commentInput.style.height = 'auto';
|
||||
let newHeight = (this.commentInput.scrollHeight < 54)
|
||||
? 54 : this.commentInput.scrollHeight+2;
|
||||
this.commentInput.style.height = newHeight+'px';
|
||||
this.commentHeight = this.commentInput.style.height;
|
||||
|
||||
this.props.action(evt);
|
||||
}
|
||||
|
||||
render() {
|
||||
let textClass = (this.props.enabled)
|
||||
? "body-regular-400 w-100"
|
||||
: "body-regular-400 w-100 gray-30";
|
||||
return (
|
||||
<div className="cb w-100 flex"
|
||||
style={{paddingBottom: 8, marginTop: 32}}>
|
||||
<div className="fl" style={{marginRight: 10}}>
|
||||
<Sigil ship={this.props.our} size={36}/>
|
||||
</div>
|
||||
<div className="flex-col w-100">
|
||||
<textarea className={textClass}
|
||||
ref={(el) => {this.commentInput = el}}
|
||||
style={{resize: "none", height: this.commentHeight}}
|
||||
type="text"
|
||||
name="commentBody"
|
||||
defaultValue=''
|
||||
onChange={this.commentChange}
|
||||
disabled={(!this.props.enabled)}>
|
||||
</textarea>
|
||||
<PostButton
|
||||
post={this.props.post}
|
||||
enabled={(Boolean(this.props.content) && this.props.enabled)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
59
pkg/interface/apps/publish/src/js/components/lib/comment.js
Normal file
59
pkg/interface/apps/publish/src/js/components/lib/comment.js
Normal file
@ -0,0 +1,59 @@
|
||||
import React, { Component } from 'react';
|
||||
import classnames from 'classnames';
|
||||
import { Sigil } from '/components/lib/icons/sigil';
|
||||
import moment from 'moment';
|
||||
|
||||
export class Comment extends Component {
|
||||
constructor(props){
|
||||
super(props);
|
||||
|
||||
moment.updateLocale('en', {
|
||||
relativeTime: {
|
||||
past: function(input) {
|
||||
return input === 'just now'
|
||||
? input
|
||||
: input + ' ago'
|
||||
},
|
||||
s : 'just now',
|
||||
future : 'in %s',
|
||||
m : '1m',
|
||||
mm : '%dm',
|
||||
h : '1h',
|
||||
hh : '%dh',
|
||||
d : '1d',
|
||||
dd : '%dd',
|
||||
M : '1 month',
|
||||
MM : '%d months',
|
||||
y : '1 year',
|
||||
yy : '%d years',
|
||||
}
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
render(){
|
||||
let body = this.props.body.split("\n").map((line, i) =>{
|
||||
return (<p key={i}>{line}</p>);
|
||||
});
|
||||
|
||||
let date = moment(this.props.date).fromNow();
|
||||
|
||||
return (
|
||||
<div className="cb w-100 flex" style={{paddingBottom: 16}}>
|
||||
<div className="fl" style={{marginRight: 10}}>
|
||||
<Sigil ship={this.props.ship} size={36} />
|
||||
</div>
|
||||
<div className="flex-col fl">
|
||||
<div className="label-small-mono gray-50">
|
||||
<p className="fl label-small-mono"
|
||||
style={{width: 107}}>{this.props.ship}</p>
|
||||
<p className="fl label-small-mono">{date}</p>
|
||||
</div>
|
||||
<div className="cb body-regular-400">
|
||||
{body}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
112
pkg/interface/apps/publish/src/js/components/lib/comments.js
Normal file
112
pkg/interface/apps/publish/src/js/components/lib/comments.js
Normal file
@ -0,0 +1,112 @@
|
||||
import React, { Component } from 'react';
|
||||
import classnames from 'classnames';
|
||||
import { Comment } from '/components/lib/comment';
|
||||
import { CommentBox } from '/components/lib/comment-box';
|
||||
|
||||
export class Comments extends Component {
|
||||
constructor(props){
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
show: false,
|
||||
commentBody: '',
|
||||
awaiting: false,
|
||||
}
|
||||
|
||||
this.toggleDisplay = this.toggleDisplay.bind(this);
|
||||
this.commentChange = this.commentChange.bind(this);
|
||||
this.postComment = this.postComment.bind(this);
|
||||
}
|
||||
|
||||
commentChange(evt) {
|
||||
this.setState({commentBody: evt.target.value});
|
||||
}
|
||||
|
||||
toggleDisplay() {
|
||||
this.setState({show: !this.state.show});
|
||||
}
|
||||
|
||||
postComment() {
|
||||
this.props.setSpinner(true);
|
||||
let comment = {
|
||||
"new-comment": {
|
||||
who: this.props.ship,
|
||||
coll: this.props.blogId,
|
||||
name: this.props.postId,
|
||||
content: this.state.commentBody,
|
||||
}
|
||||
};
|
||||
|
||||
this.setState({
|
||||
awaiting: {
|
||||
ship: this.props.ship,
|
||||
blogId: this.props.blogId,
|
||||
postId: this.props.postId,
|
||||
}
|
||||
}, () => {
|
||||
this.props.api.action("publish", "publish-action", comment)
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps, prevState) {
|
||||
if (this.state.awaiting) {
|
||||
if (prevProps.comments != this.props.comments) {
|
||||
this.props.setSpinner(false);
|
||||
this.setState({awaiting: false, commentBody: ''});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
render(){
|
||||
if (this.state.show) {
|
||||
let our = `~${window.ship}`;
|
||||
let comments = this.props.comments.map((comment, i) => {
|
||||
let commentProps = {
|
||||
ship: comment.info.creator,
|
||||
date: comment.info["date-created"],
|
||||
body: comment.body,
|
||||
};
|
||||
return (<Comment {...commentProps} key={i} />);
|
||||
});
|
||||
return (
|
||||
<div className="cb mt3 mb4">
|
||||
<p className="gray-50 body-large b">
|
||||
<span>{this.props.comments.length} </span>
|
||||
<span className="black">
|
||||
Comments
|
||||
</span>
|
||||
</p>
|
||||
<p className="cl body-regular pointer" onClick={this.toggleDisplay}>
|
||||
- Hide Comments
|
||||
</p>
|
||||
|
||||
<CommentBox our={our}
|
||||
action={this.commentChange}
|
||||
enabled={!(Boolean(this.state.awaiting))}
|
||||
content={this.state.commentBody}
|
||||
post={this.postComment}/>
|
||||
|
||||
|
||||
<div className="flex-col" style={{marginTop: 32}}>
|
||||
{comments}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
} else {
|
||||
return (
|
||||
<div className="cb mt3 mb4">
|
||||
<p className="gray-50 body-large b">
|
||||
<span>{this.props.comments.length} </span>
|
||||
<span className="black">
|
||||
Comments
|
||||
</span>
|
||||
</p>
|
||||
<p className="cl body-regular pointer" onClick={this.toggleDisplay}>
|
||||
+ Show Comments
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user