Merge branch 'master' of github.com:urbit/interface

This commit is contained in:
Logan Allen 2019-05-28 13:51:39 -07:00
commit ab87d0d620
56 changed files with 69696 additions and 1 deletions

132
apps/publish/gulpfile.js Normal file
View File

@ -0,0 +1,132 @@
var gulp = require('gulp');
var cssimport = require('gulp-cssimport');
var cssnano = require('gulp-cssnano');
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('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(cssnano())
.pipe(gulp.dest('./urbit/app/write/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/write/js/'))
.on('end', cb);
});
gulp.task('js-minify', function () {
return gulp.src('./urbit/app/write/js/index.js')
.pipe(minify())
.pipe(gulp.dest('./urbit/app/write/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/write/js/index-min.js ./urbit/app/write/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', 'js-cachebust'))
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'));
}));

7133
apps/publish/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

42
apps/publish/package.json Normal file
View File

@ -0,0 +1,42 @@
{
"name": "urbit-apps",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"author": "",
"license": "ISC",
"devDependencies": {
"@sucrase/gulp-plugin": "^2.0.0",
"gulp": "^4.0.0",
"rollup": "^1.6.0",
"gulp-better-rollup": "^4.0.1",
"gulp-cssimport": "^6.0.0",
"gulp-cssnano": "^2.1.2",
"gulp-minify": "^3.1.0",
"rollup-plugin-commonjs": "^9.2.0",
"rollup-plugin-json": "^2.3.0",
"rollup-plugin-node-builtins": "^2.1.2",
"rollup-plugin-node-globals": "^1.4.0",
"rollup-plugin-node-resolve": "^3.4.0",
"rollup-plugin-replace": "^2.0.0",
"rollup-plugin-root-import": "^0.2.3",
"sucrase": "^3.8.0"
},
"dependencies": {
"classnames": "^2.2.6",
"lodash": "^4.17.11",
"moment": "^2.20.1",
"mousetrap": "^1.6.1",
"react": "^16.5.2",
"react-custom-scrollbars": "^4.2.1",
"react-dom": "^16.8.6",
"react-router-dom": "^5.0.0",
"urbit-ob": "^3.1.1"
},
"resolutions": {
"natives": "1.1.3"
}
}

View File

@ -0,0 +1,54 @@
p, h1, h2, h3, h4, h5, h6, a, input, textarea, button {
margin-block-end: unset;
margin-block-start: unset;
font-family: Inter, sans-serif;
}
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-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;
}

View File

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

File diff suppressed because one or more lines are too long

View File

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

33
apps/publish/src/index.js Normal file
View File

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

189
apps/publish/src/js/api.js Normal file
View File

@ -0,0 +1,189 @@
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) {
console.log('binding to ...', appl, ", path: ", path, ", as ship: ", ship, ", by method: ", method);
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);
});
}
hall(data) {
this.action("hall", "hall-action", data);
}
coll(data) {
this.action("collections", "collections-action", data);
}
setOnboardingBit(value) {
this.action("collections", "json", { onboard: value });
}
action(appl, mark, data) {
return new Promise((resolve, reject) => {
window.urb.poke(ship, appl, mark, data,
(json) => {
resolve(json);
},
(err) => {
reject(err);
});
});
}
/*
Special actions
*/
permit(cir, aud, message) {
this.hall({
permit: {
nom: cir,
sis: aud,
inv: true
}
});
if (message) {
this.invite(cir, aud);
}
}
permitCol(cir, aud, message, nom) {
this.hall({
permit: {
nom: cir,
sis: aud,
inv: true
}
});
if (message) {
this.inviteCol(cir, aud, nom);
}
}
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
});
}
inviteCol(cir, aud, nom) {
let audInboxes = aud.map((aud) => `~${aud}/i`);
let inviteMessage = {
aud: audInboxes,
ses: [{
app: {
app: nom,
sep: {
inv: {
inv: true,
cir: `~${window.ship}/${cir}`
}
}
}
}]
};
this.hall({
phrase: inviteMessage
});
}
message(aud, words) {
let msg = {
aud,
ses: [{
lin: {
msg: words,
pat: false
}
}]
};
this.hall({
phrase: msg
});
}
source(nom, sub) {
this.hall({
source: {
nom: "inbox",
sub: sub,
srs: [nom]
}
})
}
create(nom, priv) {
this.hall({
create: {
nom: nom,
des: "chatroom",
sec: priv ? "village" : "channel"
}
});
}
ire(aud, uid, msg) {
let message = {
aud: aud,
ses: [{
ire: {
top: uid,
sep: {
lin: {
msg: msg,
pat: false
}
}
}
}]
}
this.hall({
phrase: message
})
}
}
export let api = new UrbitApi();
window.api = api;

View File

@ -0,0 +1,164 @@
import React, { Component } from 'react';
import { Scrollbars } from 'react-custom-scrollbars';
import classnames from 'classnames';
import _ from 'lodash';
import { Message } from '/components/lib/message';
import { ChatHeader } from '/components/lib/chat-header';
import { ChatInput } from '/components/lib/chat-input';
import { prettyShip, getMessageContent, isDMStation } from '/lib/util';
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,
message: "",
pendingMessages: [],
numPeople: 0
};
this.onScrollStop = this.onScrollStop.bind(this);
this.buildMessage = this.buildMessage.bind(this);
this.setPendingMessage = this.setPendingMessage.bind(this);
this.scrollbarRef = React.createRef();
}
componentDidMount() {
if (isDMStation(this.state.station)) {
let cir = this.state.station.split("/")[1];
this.props.api.hall({
newdm: {
sis: cir.split(".")
}
})
}
this.scrollIfLocked();
this.updateNumPeople();
}
componentDidUpdate(prevProps, prevState) {
const { props } = this;
if ((props.match.params.ship != prevProps.match.params.ship) ||
(props.match.params.station != prevProps.match.params.station)) {
this.setState({
station: props.match.params.ship + "/" + props.match.params.station,
circle: props.match.params.station,
host: props.match.params.ship,
message: "",
pendingMessages: [],
numPeople: 0
});
}
this.updateNumPeople();
this.updateNumMessagesLoaded(prevProps, prevState);
}
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 });
}
}
updateNumMessagesLoaded(prevProps, prevState) {
let station = prevProps.store.messages.stations[this.state.station] || [];
let numMessages = station.length;
if (numMessages > prevState.numMessages && this.scrollbarRef.current) {
this.setState({
numMessages: numMessages
});
this.scrollIfLocked();
}
}
scrollIfLocked() {
if (this.state.scrollLocked && this.scrollbarRef.current) {
this.scrollbarRef.current.scrollToBottom();
}
}
onScrollStop() {
let scroll = this.scrollbarRef.current.getValues();
this.setState({
scrollLocked: (scroll.top === 1)
});
if (scroll.top === 0) {
this.requestChatBatch();
}
}
setPendingMessage(message) {
this.setState({
pendingMessages: this.state.pendingMessages.concat({...message, pending: true})
});
}
buildMessage(msg) {
let details = msg.printship ? null : getMessageContent(msg);
if (msg.printship) {
return (
<a
className="vanilla hoverline text-600 text-mono"
href={prettyShip(msg.aut)[1]}>
{prettyShip(`~${msg.aut}`)[0]}
</a>
);
}
return (
<Message msg={msg} details={details} />
);
}
render() {
let messages = this.props.store.messages.stations[this.state.station] || [];
messages = [...messages, ...this.state.pendingMessages];
let chatMessages = messages.map(this.buildMessage);
return (
<div className="h-100 w-100 overflow-x-hidden flex flex-column">
<div style={{ flexBasis:72 }}>
<ChatHeader title={this.state.circle} numPeople={this.state.numPeople} />
</div>
<div style={{ flexGrow: 1 }}>
<Scrollbars
ref={this.scrollbarRef}
renderTrackHorizontal={props => <div style={{display: "none"}}/>}
onScrollStop={this.onScrollStop}
renderView={props => <div {...props} />}
style={{ height: '100%' }}
autoHide>
{chatMessages}
</Scrollbars>
</div>
<div style={{ flexBasis:112 }}>
<ChatInput
api={this.props.api}
store={this.props.store}
station={this.state.station}
circle={this.state.circle}
scrollbarRef={this.scrollbarRef}
setPendingMessage={this.setPendingMessage}
placeholder='Message...' />
</div>
</div>
)
}
}

View File

@ -0,0 +1,27 @@
import React, { Component } from 'react';
import classnames from 'classnames';
export class CollectionItem extends Component {
onClick() {
const { props } = this;
console.log("collection-item clicked!");
// props.history.push(props.url);
}
render() {
const { props } = this;
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'>
<h3 className='w-60 dib sans-serif'>{props.title}</h3>
<p className='w-40 tr dib sans-serif gray'>{props.datetime}</p>
</div>
<p className='pt2 sans-serif gray'>{props.description}</p>
</div>
)
}
}

View File

@ -0,0 +1,27 @@
import React, { Component } from 'react';
import classnames from 'classnames';
export class CollectionList extends Component {
render() {
console.log("collection-list.props", this.props);
let listItems = this.props.list.map((coll) => {
return (
<p className="w-100">
{coll.data.info.title}
</p>
);
});
console.log(listItems);
return (
<div className="w-100">
{listItems}
</div>
);
}
}

View File

@ -0,0 +1,26 @@
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 { isUrl, uuid, isDMStation } from '/lib/util';
export class ChatHeader extends Component {
render() {
return (
<div className="w-100 pa2 mb3 cf">
<div className="fl">
<p className="f3 sans-serif">{this.props.title}</p>
<div>
<p className="dib mid-gray mr2 sans-serif">{this.props.numPeople} Participants</p>
</div>
</div>
<div className="fr">
</div>
</div>
);
}
}

View File

@ -0,0 +1,131 @@
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 { isUrl, uuid, isDMStation } from '/lib/util';
export class ChatInput extends Component {
/*
Props:
- station
- api
- store
- circle
- placeholder
- setPendingMessage
- scrollbarRef
*/
constructor(props) {
super(props);
this.state = {
message: ""
};
this.textareaRef = React.createRef();
this.messageSubmit = this.messageSubmit.bind(this);
this.messageChange = this.messageChange.bind(this);
}
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() {
let aud, sep;
let wen = Date.now();
let uid = uuid();
let aut = window.ship;
let config = this.props.store.configs[this.state.station];
if (isDMStation(this.props.station)) {
aud = this.props.station
.split("/")[1]
.split(".")
.map((mem) => `~${mem}/${this.props.circle}`);
} else {
aud = [this.props.station];
}
if (isUrl(this.state.message)) {
sep = {
url: this.state.message
}
} else {
sep = {
lin: {
msg: this.state.message,
pat: false
}
}
}
let message = {
uid,
aut,
wen,
aud,
sep,
};
this.props.api.hall({
convey: [message]
});
this.props.setPendingMessage(message);
console.log('ending message submit');
this.setState({
message: ""
});
// TODO: Push to end of event queue to let pendingMessages render before scrolling
// There's probably a better way to do this
setTimeout(() => {
if (this.props.scrollbarRef.current) this.props.scrollbarRef.current.scrollToBottom();
})
}
render() {
return (
<div className="w-100 pa2 mb3 cf flex">
<div className="fl mr2" style={{ flexBasis: 48 }}>
<Sigil ship={window.ship} size={44} />
</div>
<div className="fr" style={{ flexGrow: 1, border: "2px solid #e6e6e6" }}>
<textarea className="w-100 h-100 bn sans-serif pa2"
style={{
resize: "none"
}}
ref={this.textareaRef}
placeholder={this.props.placeholder}
value={this.state.message}
onChange={this.messageChange} />
</div>
</div>
);
}
}

View File

@ -0,0 +1,40 @@
import React, { Component } from 'react';
import { getStationDetails } from '/services';
export class ChatList extends Component {
componentDidMount() {
let path = `/public`;
this.props.api.bind(path, "PUT", this.props.hostship.slice(1));
}
renderChats() {
if (this.props.store.public[this.props.hostship]) {
const chats = this.props.store.public[this.props.hostship].map((cir) => {
const deets = getStationDetails(cir)
if (deets.type == "stream-chat" || deets.type == "stream-dm") {
return (
<div className="mt-2 text-500">
<a href={`/~chat/{cir}`}>{cir}</a>
</div>
)
} else {
return null;
}
});
return chats;
} else {
return null;
}
}
render() {
const chats = this.renderChats();
return (
<div>
<div className="text-600 mt-8">Chats</div>
{chats}
</div>
);
}
}

View File

@ -0,0 +1,30 @@
import React, { Component } from 'react';
import { secToString, esoo } from '/lib/util';
// display elapsed time by converting galactic time to client time
/*
Goes from:
1531938314116 // "javascript unix time"
To:
4m // "elapsed timestring from current time"
*/
export class Elapsed extends Component {
constructor(props) {
super(props);
// console.log('elapsed props...', props);
}
renderTime() {
let parsed = esoo(this.props.timestring);
const serverTime = new Date(parsed ? parsed : this.props.timestring);
const clientTime = new Date(); // local
return secToString((clientTime - serverTime) / 1000).split(' ')[0];
}
render() {
return (
<span className={this.props.classes}>-{this.renderTime()}</span>
)
}
}

View File

@ -0,0 +1,74 @@
import React, { Component } from 'react';
import { IconInbox } from '/components/lib/icons/icon-inbox';
import { IconComment } from '/components/lib/icons/icon-comment';
import { IconSig } from '/components/lib/icons/icon-sig';
import { IconDecline } from '/components/lib/icons/icon-decline';
import { IconUser } from '/components/lib/icons/icon-user';
export class Icon extends Component {
render() {
let iconElem = null;
switch(this.props.type) {
case "icon-stream-chat":
iconElem = <span className="icon-stream-chat"></span>;
break;
case "icon-stream-dm":
iconElem = <span className="icon-stream-dm"></span>;
break;
case "icon-collection-index":
iconElem = <span className="icon-collection"></span>;
break;
case "icon-collection-post":
iconElem = <span className="icon-collection-post"></span>;
break;
case "icon-collection-comment":
iconElem = <span className="icon-collection icon-collection-comment"></span>;
break;
case "icon-panini":
// TODO: Should icons be display: block, inline, or inline-blocks?
// 1) Should naturally flow inline
// 2) But can't make icon-panini naturally inline without hacks like &nbsp;
iconElem = <div className="icon-panini"></div>
break;
case "icon-x":
iconElem = <span className="icon-x"></span>
break;
case "icon-decline":
iconElem = <IconDecline />
break;
case "icon-lus":
iconElem = <span className="icon-lus"></span>
break;
case "icon-inbox":
iconElem = <IconInbox />
break;
case "icon-comment":
iconElem = <IconComment />
break;
case "icon-sig":
iconElem = <IconSig />
break;
case "icon-user":
iconElem = <IconUser />
break;
case "icon-ellipsis":
iconElem = (
<div className="icon-ellipsis-wrapper icon-label">
<div className="icon-ellipsis-dot"></div>
<div className="icon-ellipsis-dot"></div>
<div className="icon-ellipsis-dot"></div>
</div>
)
break;
}
let className = this.props.label ? "icon-label" : "";
return (
<span className={className}>
{iconElem}
</span>
)
}
}

View File

@ -0,0 +1,11 @@
import React, { Component } from 'react';
export class IconCheck extends Component {
render() {
return (
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fillRule="evenodd" clipRule="evenodd" d="M14.9999 4.63293L13.2766 3L6.1698 9.7341L2.72327 6.46823L1 8.10117L6.16992 13L7.24512 11.9812L7.89319 11.3671L14.9999 4.63293Z" fill="white"/>
</svg>
)
}
}

View File

@ -0,0 +1,17 @@
import React, { Component } from 'react';
export class IconComment extends Component {
render() {
return (
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<mask id="mask0" mask-type="alpha" maskUnits="userSpaceOnUse" x="2" y="2" width="12" height="12">
<path fillRule="evenodd" clipRule="evenodd" d="M9.2 10.4L14 14V2H2V10.4H9.2ZM3.2 9.2H9.35486C9.48986 9.2 9.62096 9.24554 9.72686 9.32924L12.8 11.7578V3.2H3.2V9.2Z" fill="black"/>
<path d="M3.2 9.2H9.35486C9.48986 9.2 9.62096 9.24554 9.72686 9.32924L12.8 11.7578V3.2H3.2V9.2Z" fill="black"/>
</mask>
<g mask="url(#mask0)">
<rect width="16" height="16" fill="black"/>
</g>
</svg>
)
}
}

View File

@ -0,0 +1,11 @@
import React, { Component } from 'react';
export class IconCross extends Component {
render() {
return (
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fillRule="evenodd" clipRule="evenodd" d="M8 6.28568L3.71436 2L2.00012 3.71429L6.28577 7.99994L2 12.2857L3.71423 14L8 9.71423L12.2858 14L14 12.2857L9.71436 7.99997L14 3.71429L12.2856 2.00003L8 6.28568Z" fill="black"/>
</svg>
)
}
}

View File

@ -0,0 +1,13 @@
import React, { Component } from 'react';
export class IconDecline extends Component {
render() {
return (
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<g id="Icon/Decline">
<path id="Union" fillRule="evenodd" clipRule="evenodd" d="M6.28577 7.99992L2 12.2857L3.71423 14L8 9.71422L12.2858 14L14 12.2857L9.71423 7.99997L14 3.71428L12.2856 1.99998L8 6.28568L3.71436 2L2.00012 3.71428L6.28577 7.99992Z" fill="black"/>
</g>
</svg>
);
}
}

View File

@ -0,0 +1,11 @@
import React, { Component } from 'react';
export class IconInbox extends Component {
render() {
return (
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fillRule="evenodd" clipRule="evenodd" d="M8 6C9.65686 6 11 4.65686 11 3H13C13.5523 3 14 3.44772 14 4V12C14 12.5523 13.5523 13 13 13H3C2.44771 13 2 12.5523 2 12V4C2 3.44772 2.44771 3 3 3H5C5 4.65686 6.34314 6 8 6Z" fill="black"/>
</svg>
)
}
}

View File

@ -0,0 +1,11 @@
import React, { Component } from 'react';
export class IconSig extends Component {
render() {
return (
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M15 6H12.6564C12.4097 7.51007 11.8238 8.48322 10.7445 8.48322C8.86344 8.48322 7.78414 6 5.03965 6C2.54185 6 1.2467 7.71141 1 11H3.34361C3.59031 9.48993 4.17621 8.51678 5.25551 8.51678C7.19824 8.51678 8.18502 11 10.9912 11C13.3965 11 14.7533 9.28859 15 6Z" fill="black"/>
</svg>
)
}
}

View File

@ -0,0 +1,9 @@
import React, { Component } from 'react';
export class IconUser extends Component {
render() {
return (
<svg fill="none" height="16" width="16" xmlns="http://www.w3.org/2000/svg"><path clipRule="evenodd" d="m8 2a3 3 0 1 0 0 6 3 3 0 0 0 0-6zm4.667 12h1.333c0-2.761-2.686-5-6-5s-6 2.239-6 5z" fill="#000" fillRule="evenodd"/></svg>
)
}
}

View File

@ -0,0 +1,18 @@
import React, { Component } from 'react';
import { sealDict } from '/components/lib/seal-dict';
export class Sigil extends Component {
render() {
let prefix = this.props.prefix ? JSON.parse(this.props.prefix) : false;
return (
<div
className="bg-black"
style={{ flexBasis: 48, padding: 4, paddingBottom: 0 }}>
{
sealDict.getSeal(this.props.ship, this.props.size, prefix)
}
</div>
);
}
}

View File

@ -0,0 +1,69 @@
import React, { Component } from 'react';
import { isDMStation, getMessageContent } from '/lib/util';
import { Sigil } from '/components/lib/icons/sigil';
import classnames from 'classnames';
import moment from 'moment';
export class Message extends Component {
buildPostTitle(messageDetails) {
if (messageDetails.postUrl) {
return (
<a className="pr-12 text-600 underline"
href={messageDetails.postUrl}>
{messageDetails.postTitle}
</a>
)
} else {
return null;
}
}
renderContent(type) {
if (type === "text") {
return this.buildPostTitle(this.props.details);
} else if (type === "url") {
if (/(jpg|img|png|gif|tiff|jpeg|JPG|IMG|PNG|TIFF)$/.exec(this.props.details.content)) {
return (
<img src={this.props.details.content} style={{width:"30%"}}></img>
)
} else {
return (
<a href={this.props.details.content} target="_blank">{this.props.details.content}</a>
)
}
} else if (type === "exp") {
return (
<div className="text-body">
<div className="text-mono">{this.props.details.content}</div>
<pre className="text-mono mt-0">{this.props.details.res}</pre>
</div>
)
} else if (['new item', 'edited item'].includes(type)) {
return <span className="text-body" dangerouslySetInnerHTML={{__html: this.props.details.snip}}></span>
} else if (type === "lin") {
return (
<p className="sans-serif">{this.props.details.content}</p>
);
} else {
return <span className="text-mono">{'<unknown message type>'}</span>;
}
}
render() {
return (
<div className="w-100 pa2 mb3 cf flex">
<div className="fl mr2">
<Sigil ship={this.props.msg.aut} size={44} />
</div>
<div className="fr" style={{ flexGrow: 1 }}>
<div className="mb2">
<p className="sans-serif gray dib mr2">~{this.props.msg.aut}</p>
<p className="sans-serif gray dib">{moment.unix(this.props.msg.wen).format('hh:mm')}</p>
</div>
{this.renderContent(this.props.details.type)}
</div>
</div>
);
}
}

View File

@ -0,0 +1,90 @@
import React, { Component } from 'react';
import { pour } from '/vendor/sigils-1.2.5';
import _ from 'lodash';
const ReactSVGComponents = {
svg: p => {
return (
<svg {...p.attr} version={'1.1'} xmlns={'http://www.w3.org/2000/svg'}>
{ _.map(_.get(p, 'children', []), child => ReactSVGComponents[child.tag](child)) }
</svg>
)
},
circle: p => {
return (
<circle {...p.attr}>
{ _.map(_.get(p, 'children', []), child => ReactSVGComponents[child.tag](child)) }
</circle>
)
},
rect: p => {
return (
<rect {...p.attr}>
{ _.map(_.get(p, 'children', []), child => ReactSVGComponents[child.tag](child)) }
</rect>
)
},
path: p => {
return (
<path {...p.attr}>
{ _.map(_.get(p, 'children', []), child => ReactSVGComponents[child.tag](child)) }
</path>
)
},
g: p => {
return (
<g {...p.attr}>
{ _.map(_.get(p, 'children', []), child => ReactSVGComponents[child.tag](child)) }
</g>
)
},
polygon: p => {
return (
<polygon {...p.attr}>
{ _.map(_.get(p, 'children', []), child => ReactSVGComponents[child.tag](child)) }
</polygon>
)
},
line: p => {
return (
<line {...p.attr}>
{ _.map(_.get(p, 'children', []), child => ReactSVGComponents[child.tag](child)) }
</line>
)
},
polyline: p => {
return (
<polyline {...p.attr}>
{ _.map(_.get(p, 'children', []), child => ReactSVGComponents[child.tag](child)) }
</polyline>
)
}
}
export class SealDict {
constructor() {
this.dict = {};
}
getPrefix(patp) {
return patp.length === 3 ? patp : patp.substr(0, 3);
}
getSeal(patp, size, prefix) {
if (patp.length > 13) {
patp = "tiz";
}
let sigilShip = prefix ? this.getPrefix(patp) : patp;
let key = `${sigilShip}+${size}`;
if (!this.dict[key]) {
this.dict[key] = pour({size: size, patp: sigilShip, renderer: ReactSVGComponents, margin: 0, colorway: ["#fff", "#000"]})
}
return this.dict[key];
}
}
const sealDict = new SealDict;
export { sealDict }

View File

@ -0,0 +1,70 @@
import React, { Component } from 'react';
import { BrowserRouter, Route } from "react-router-dom";
import Mousetrap from 'mousetrap';
import classnames from 'classnames';
import _ from 'lodash';
import { api } from '/api';
import { store } from '/store';
import { Skeleton } from '/components/skeleton';
import { Sidebar } from '/components/sidebar';
import { CollectionList } from '/components/collection-list';
export class Root extends Component {
constructor(props) {
super(props);
this.state = store.collections;
console.log("root.state", this.state);
store.setStateHandler(this.setState.bind(this));
}
render() {
return (
<BrowserRouter>
<div>
<Route exact path="/~publish"
render={ (props) => {
return (
<div className="cf h-100 w-100 absolute">
<div className="fl w-100 h3">
<h1>Publish</h1>
</div>
<div className="fl flex w-100 h-100">
<div className="fl h-100 overflow-x-hidden" style={{ flexBasis: 400 }}>
<p className="fl w-100 h2 bb">
Latest
</p>
</div>
<div className="fl h-100 overflow-x-hidden" style={{ flexBasis: 400 }}>
<p className="fl w-100 h2 bb">
Subs
</p>
<CollectionList
list={this.state.subs}
/>
</div>
<div className="fl h-100 overflow-x-hidden" style={{ flexBasis: 400 }}>
<p className="fl w-100 h2 bb">
Pubs
</p>
<CollectionList
list={this.state.pubs}
/>
</div>
<div className="fl h-100 overflow-x-hidden" style={{ flexBasis: 400 }}>
<p className="fl w-100 h2 bb">
Create Button? idk
</p>
</div>
</div>
</div>
);
}} />
</div>
</BrowserRouter>
)
}
}

View File

@ -0,0 +1,26 @@
import React, { Component } from 'react';
import classnames from 'classnames';
export class SidebarItem extends Component {
onClick() {
const { props } = this;
props.history.push('/~chat/' + props.title);
}
render() {
const { props } = this;
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'>
<h3 className='w-60 dib sans-serif'>{props.title}</h3>
<p className='w-40 tr dib sans-serif gray'>{props.datetime}</p>
</div>
<p className='pt2 sans-serif gray'>{props.description}</p>
</div>
)
}
}

View File

@ -0,0 +1,58 @@
import React, { Component } from 'react';
import classnames from 'classnames';
import { Scrollbars } from 'react-custom-scrollbars';
import moment from 'moment';
import { getMessageContent } from '/lib/util';
import { SidebarItem } from '/components/sidebar-item';
export class Sidebar extends Component {
render() {
const { props } = this;
let station = props.match.params.ship + '/' + props.match.params.station;
let sidebarItems = props.circles.map((cir) => {
let msg = props.messagePreviews[cir];
let parsed = getMessageContent(msg);
let wen = moment.unix(msg.wen / 1000).from(moment.utc());
return (
<CollectionItem
title={cir}
description={parsed.content}
datetime={wen}
selected={station === cir}
history={props.history}
/>
);
});
return (
<div className="h-100 w-100 overflow-x-hidden flex flex-column">
<div className="pl3 pr3 pt2 pb2 cf">
<h2 className="dib lh-title sans-serif w-50 f2">Publish</h2>
<a className="dib tr lh-title sans-serif w-50 f4 underline">+ New</a>
</div>
<div className='mt2 pl3 pr3 mb2 w-100'>
<div>My Collections</div>
</div>
<div style={{ flexGrow: 1 }}>
<Scrollbars
ref={this.scrollbarRef}
renderTrackHorizontal={props => <div style={{display: "none"}}/>}
onScrollStop={this.onScrollStop}
renderView={props => <div {...props} />}
style={{ height: '100%' }}
autoHide>
{sidebarItems}
</Scrollbars>
</div>
<div className='mt2 pl3 pr3 mb2 w-100'>
<div>My Subscriptions</div>
</div>
</div>
)
}
}

View File

@ -0,0 +1,19 @@
import React, { Component } from 'react';
import classnames from 'classnames';
export class Skeleton extends Component {
render() {
return (
<div className="cf h-100 w-100 absolute flex">
<div className="fl h-100 br overflow-x-hidden" style={{ flexBasis: 320 }}>
{this.props.sidebar}
</div>
<div className="h-100 fr" style={{ flexGrow: 1 }}>
{this.props.children}
</div>
</div>
);
}
}

View File

@ -0,0 +1,9 @@
Object.arrayify = (obj) => {
let ret = [];
Object.keys(obj).forEach((key) => {
ret.push({key, value: obj[key]});
})
return ret;
}

View File

@ -0,0 +1,35 @@
export const REPORT_KEYS = [
'landscape.prize',
// /circle/<cir_name>/grams
// call automatically on inbox
// call automatically on /urbit-meta
// call automatically on any DM circles created
'circle.gram',
'circle.nes',
// /circle/<cir_name>/config-l
// used for loading inbox config
'circle.cos.loc',
// /circle/<cir_name>/config-r
// used for loading inbox's sources' configs
'circle.cos.rem',
// /circle/<cir_name>/config-l
// used for fora topic creation....maybe? let me check
'circle.config',
'circle.config.dif.full',
// /circle/<cir_name>/config-l
// used for subscription / unsubscription
'circle.config.dif.source',
// /circles, required for initialization
'circles',
// frontend specific, no server calls
'menu.toggle',
'config.ext',
'inbox.sources-loaded',
'circle.read',
'dm.new',
'dm.clear',
];

View File

@ -0,0 +1,354 @@
import _ from 'lodash';
import urbitOb from 'urbit-ob';
import classnames from 'classnames';
export const AGGREGATOR_COLL = "c";
export const AGGREGATOR_INBOX = "aggregator-inbox";
export const AGGREGATOR_NAMES = [AGGREGATOR_INBOX, AGGREGATOR_COLL];
export function capitalize(str) {
return `${str[0].toUpperCase()}${str.substr(1)}`;
}
// takes a galactic (urbit) time and converts to 8601
export function esoo(str) {
var dubb = function(num) {
return num < 10 ? '0' + parseInt(num) : parseInt(num);
}
const p = /\~(\d\d\d\d).(\d\d?).(\d\d?)..(\d\d?).(\d\d?).(\d\d?)/.exec(str);
if (p) {
return `${p[1]}-${dubb(p[2])}-${dubb(p[3])}T${dubb(p[4])}:${dubb(p[5])}:${dubb(p[6])}Z`
}
return false;
}
// check if hostname follows ship.*.urbit.org scheme
export function isProxyHosted(hostName) {
const r = /([a-z,-]+)\.(.+\.)?urbit\.org/.exec(hostName);
if (r && urbitOb.isValidPatp(r[1])) {
return true;
}
return false;
}
export function getQueryParams() {
if (window.location.search !== "") {
return JSON.parse('{"' + decodeURI(window.location.search.substr(1).replace(/&/g, "\",\"").replace(/=/g,"\":\"")) + '"}');
} else {
return {};
}
}
export function isAggregator(station) {
let cir = station.split("/")[1]
return AGGREGATOR_NAMES.includes(cir);
}
/*
Goes from:
1531943107869 // "javascript unix time"
To:
"48711y 2w 5d 11m 9s" // "stringified time increments"
*/
export function secToString(secs) {
if (secs <= 0) {
return 'Completed';
}
secs = Math.floor(secs)
var min = 60;
var hour = 60 * min;
var day = 24 * hour;
var week = 7 * day;
var year = 52 * week;
var fy = function(s) {
if (s < year) {
return ['', s];
} else {
return [Math.floor(s / year) + 'y', s % year];
}
}
var fw = function(tup) {
var str = tup[0];
var sec = tup[1];
if (sec < week) {
return [str, sec];
} else {
return [str + ' ' + Math.floor(sec / week) + 'w', sec % week];
}
}
var fd = function(tup) {
var str = tup[0];
var sec = tup[1];
if (sec < day) {
return [str, sec];
} else {
return [str + ' ' + Math.floor(sec / day) + 'd', sec % day];
}
}
var fh = function(tup) {
var str = tup[0];
var sec = tup[1];
if (sec < hour) {
return [str, sec];
} else {
return [str + ' ' + Math.floor(sec / hour) + 'h', sec % hour];
}
}
var fm = function(tup) {
var str = tup[0];
var sec = tup[1];
if (sec < min) {
return [str, sec];
} else {
return [str + ' ' + Math.floor(sec / min) + 'm', sec % min];
}
}
var fs = function(tup) {
var str = tup[0];
var sec = tup[1];
return str + ' ' + sec + 's';
}
return fs(fm(fh(fd(fw(fy(secs)))))).trim();
}
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;
}
export function isValidStation(st) {
let tokens = st.split("/")
if (tokens.length !== 2) return false;
return urbitOb.isValidPatp(tokens[0]) && isPatTa(tokens[1]);
}
/*
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" : ""}`
  );
}
// ascending for clarity
// export function sortSrc(circleArray, topic=false){
// let sc = circleArray.map((c) => util.parseCollCircle(c)).filter((pc) => typeof pc != 'undefined' && typeof pc.top == 'undefined');
// return sc.map((src) => src.coll).sort((a, b) => util.daToDate(a) - util.daToDate(b));
// }
export function arrayEqual(a, b) {
if (a === b) return true;
if (a == null || b == null) return false;
if (a.length != b.length) return false;
// If you don't care about the order of the elements inside
// the array, you should sort both arrays here.
for (var i = 0; i < a.length; ++i) {
if (a[i] !== b[i]) return false;
}
return true;
}
function deSig(ship) {
return ship.replace('~', '');
}
// use urbit.org proxy if it's not on our ship
export function foreignUrl(shipName, own, urlFrag) {
if (deSig(shipName) != deSig(own)) {
return `http://${deSig(shipName)}.urbit.org${urlFrag}`
} else {
return urlFrag
}
}
// shorten comet names
export function prettyShip(ship) {
const sp = ship.split('-');
return [sp.length == 9 ? `${sp[0]}_${sp[8]}`: ship, ship[0] === '~' ? `/~profile/${ship}` : `/~profile/~${ship}`];
}
export function profileUrl(ship) {
return `/~landscape/profile/~${ship}`;
}
export function isDMStation(station) {
let host = station.split('/')[0].substr(1);
let circle = station.split('/')[1];
return (
station.indexOf('.') !== -1 &&
circle.indexOf(host) !== -1
);
}
export function isRootCollection(station) {
return station.split("/")[1] === "c";
}
// maybe do fancier stuff later
export function isUrl(string) {
const r = /^http|^www|\.com$/.exec(string)
if (r) {
return true
}
else {
return false
}
}
export function getMessageContent(msg) {
let ret;
const MESSAGE_TYPES = {
'sep.app.sep.fat.sep.lin.msg': 'app',
'sep.app.sep.lin.msg': 'app',
'sep.app.sep.inv': (msg) => {
let sta = msg.sep.app.sep.inv.cir;
let [hos, cir] = sta.split('/');
return {
type: 'inv',
msg: msg,
content: {
nom: msg.sep.app.app,
sta: sta,
hos: hos,
inv: msg.sep.app.sep.inv.inv
}
}
},
'sep.inv': (msg) => {
let sta = msg.sep.inv.cir;
let [hos, cir] = sta.split('/');
return {
type: 'inv',
msg: msg,
content: {
nom: cir,
inv: msg.sep.inv.inv,
hos,
sta,
cir
}
}
},
'sep.fat': (msg) => {
let type = msg.sep.fat.tac.text;
let station = msg.aud[0];
let jason = JSON.parse(msg.sep.fat.sep.lin.msg);
let content = (type.includes('collection')) ? null : jason.content;
let par = jason.path.slice(0, -1);
return {
type: msg.sep.fat.tac.text,
msg: msg,
contentType: jason.type,
content: content,
snip: jason.snip,
author: jason.author,
host: jason.host,
date: jason.date,
path: jason.path,
postTitle: jason.name,
postUrl: `/~landscape/collections/${jason.host}/${jason.path.slice(2).join('/')}`,
}
},
'sep.lin.msg': 'lin',
'sep.ire.sep.lin': (msg) => {
return {
type: "lin",
msg: msg,
content: msg.sep.ire.sep.lin.msg,
replyUid: msg.sep.ire.top
}
},
'sep.ire': 'ire',
'sep.url': 'url',
'sep.exp': (msg) => {
return {
type: "exp",
msg: msg,
content: msg.sep.exp.exp,
res: msg.sep.exp.res.join('\n')
}
},
}
Object.arrayify(MESSAGE_TYPES).some(({key, value}) => {
if (_.has(msg, key)) {
if (typeof value === "string") {
ret = {
type: value,
msg: msg,
content: _.get(msg, key)
}
} else if (typeof value === "function") {
ret = value(msg);
}
return true;
}
});
if (typeof ret === "undefined") {
ret = {type: "unknown"};
console.log("ASSERT: unknown message type on ", msg)
}
return ret;
}
window.getMessageContent = getMessageContent;

View File

@ -0,0 +1,19 @@
export class CirclesReducer {
reduce(reports, store) {
reports.forEach((rep) => {
switch (rep.type) {
case "circles":
if (rep.data.add) {
store.circles = [...store.circles, rep.data.cir]
} else {
store.circles = rep.data
}
break;
case "landscape.prize":
store.circles = [...store.circles, rep.data["circles-our"]];
break;
}
});
}
}

View File

@ -0,0 +1,123 @@
import { isAggregator } from '/lib/util';
export class ConfigsReducer {
reduce(reports, store) {
reports.forEach(rep => {
let stationName;
let stations = {};
switch (rep.type) {
case "circle.gram":
this.processGramConfigs([rep.data], store.configs);
break;
case "circle.nes":
this.processGramConfigs(rep.data, store.configs);
break;
case "circle.cos.loc":
stationName = `~${rep.from.ship}/${rep.from.path.split("/")[2]}`;
stations[stationName] = rep.data;
this.addConfigs(stations, store.configs);
break;
case "circle.cos.rem":
this.addConfigs(rep.data, store.configs);
break;
case "circle.pes.loc":
stationName = `~${rep.from.ship}/${rep.from.path.split("/")[2]}`;
this.updateConfig({pes: rep.data}, store.configs[stationName]);
break;
case "circle.config.dif.source":
stationName = `~${rep.from.ship}/${rep.from.path.split("/")[2]}`;
this.updateConfig(rep.data, store.configs[stationName]);
break;
case "circle.config.dif.full":
stationName = rep.data.src[0]; // TODO: API weirdness; we have to get name of new station from new station config's src property. Should maybe return a dict.
stations[stationName] = rep.data;
this.addConfigs(stations, store.configs);
break;
case "circle.config.dif.permit": // TODO: This is very wonky, should be fixed with API discussion
stationName = rep.data.cir;
this.updateConfig(rep.data.dif.permit, store.configs[stationName]);
break;
case "circle.config.dif.remove":
delete store.configs[rep.data.cir];
break;
case "config.ext":
store.configs[rep.data.station] = store.configs[rep.data.station] || {};
store.configs[rep.data.station].extConf = rep.data.extConf;
break;
case "circle.read":
store.configs[rep.data.station] = store.configs[rep.data.station] || {};
store.configs[rep.data.station].lastReadNum = rep.data.lastReadNum;
break;
case "circle.config":
let readChange = _.get(rep.data, 'dif.read', null);
if (readChange) {
store.configs[rep.data.cir] = {...store.configs[rep.data.cir], red: readChange};
}
break;
case "landscape.prize":
rep.data.circles.forEach(c => {
store.configs[c.circle] = c.config || {};
});
break;
}
});
}
processGramConfigs(grams, storeConfigs) {
grams.forEach(gram => {
let tac = _.get(gram, 'gam.sep.fat.tac.text', null);
if (tac && ['new item', 'edited item'].includes(tac)) {
let conf = _.get(gram, 'gam.sep.fat.sep.lin.msg', null);
if (conf) {
let parsedConf = JSON.parse(conf);
if (parsedConf['parent-config']) {
storeConfigs[gram.gam.aud[0]] = {
...storeConfigs[gram.gam.aud[0]],
...{ extConf: parsedConf['parent-config'] }
};
}
}
}
})
}
addConfigs(configs, storeConfigs) {
Object.keys(configs)
.forEach((cos) => {
storeConfigs[cos] = storeConfigs[cos] || {};
Object.assign(storeConfigs[cos], configs[cos]);
});
}
updateConfig(data, station) {
if (!station) return;
if (data.src) {
if (data.add) {
station.src.push(data.src);
} else {
station.src = station.src.filter((val) => val !== data.src);
}
}
if (data.sis) {
if (data.add) {
station.con.sis = station.con.sis.concat(data.sis);
} else {
station.con.sis = station.con.sis.filter((val) => !data.sis.includes(val));
}
}
if (data.pes) {
station.pes = station.pes || {};
Object.assign(station.pes, data.pes);
}
}
}

View File

@ -0,0 +1,54 @@
// let newSep = {
// sep: {
// inv: {
// inv: true,
// cir: "~zod/null"
// }
// },
// wen: (new Date()).getTime()
// };
// import { isDMStation, getMessageContent } from '/lib/util';
// import _ from 'lodash';
//
// export class DmsReducer {
// reduce(reports, store) {
// reports.forEach((rep) => {
// switch (rep.type) {
// case "circles":
// if (_.isArray(rep.data)) {
// let newStations = rep.data.filter(station => isDMStation(`${rep.from.ship}/${station}`));
// store.dms.stations = _.uniq([...store.dms.stations, ...newStations]);
// store.dms.stored = true;
// } else if (rep.data.cir) {
// if (rep.data.add) {
// store.dms.stations = _.uniq([...store.dms.stations, rep.data.cir]);
// } else {
// store.dms.stations = _.filter(store.dms.stations, s => s !== rep.data.cir);
// }
// }
// break;
//
// case "circle.gram":
// this.addStationsFromInvites([rep.data], store);
// break;
//
// case "circle.nes":
// this.addStationsFromInvites(rep.data, store);
// break;
// }
// });
// }
//
// addStationFromInvite(msgs, store) {
// let inviteStations = [];
// msgs.forEach(msg => {
// let msgContent = getMessageContent(msg);
// if (msgContent.type === "inv" && isDMStation(msgContent.content.sta)) {
// inviteStations.push(msgContent.content.sta);
// }
// });
//
// store.dms.stations = [...store.dms.stations, ...inviteStations];
// }
// }

View File

@ -0,0 +1,191 @@
import _ from 'lodash';
import { isDMStation, isRootCollection, getMessageContent } from '/lib/util';
const INBOX_MESSAGE_COUNT = 30;
export class MessagesReducer {
reduce(reports, store) {
reports.forEach((rep) => {
let fromCircle = rep.from && rep.from.path.split("/")[2];
let fromInbox = fromCircle === "inbox";
switch (rep.type) {
case "circle.nes":
this.processMessages(rep.data, store);
break;
case "circle.gram":
this.processMessages([rep.data], store);
break;
case "circle.config.dif.remove":
delete store.messages.stations[rep.data.cir];
break;
case "circle.cos.loc":
if (fromInbox) {
store.messages.inbox.config = rep.data;
store.messages.inbox.src = rep.data.src;
this.storeInboxMessages(store);
}
break;
case "circle.config.dif.source":
if (fromInbox) {
if (rep.data.add) {
store.messages.inbox.src = [...store.messages.inbox.src, rep.data.src];
} else {
store.messages.inbox.src = store.messages.inbox.src.filter(src => src !== rep.data.src);
}
this.storeInboxMessages(store);
}
break;
case "circle.config":
fromInbox = rep.data.cir.includes("inbox");
if (fromInbox && _.get(rep.data, 'dif.source', null)) {
if (rep.data.dif.source.add) {
store.messages.inbox.src = [...store.messages.inbox.src, rep.data.dif.source.src];
} else {
store.messages.inbox.src = store.messages.inbox.src.filter(src => src !== rep.data.dif.source.src);
}
this.storeInboxMessages(store);
}
break;
case "landscape.prize":
if (rep.data.inbox) {
store.messages.inbox.src = [...store.messages.inbox.src, ...rep.data.inbox.config.src];
store.messages.inbox.config = rep.data.inbox.config;
this.processMessages(rep.data.inbox.messages, store);
this.processMessages(rep.data.invites, store);
this.storeInboxMessages(store);
} else {
console.log("WEIRD: no inbox property in landscape.prize?")
}
// if (fromInbox) {
// if (rep.data.add) {
// store.messages.inbox.src = [...store.messages.inbox.src, rep.data.src];
// } else {
// store.messages.inbox.src = store.messages.inbox.src.filter(src => src !== rep.data.src);
// }
// this.storeInboxMessages(store);
// }
break;
case "dm.new": {
store.messages.notifications = [...store.messages.notifications, ...rep.data];
break;
}
case "dm.clear": {
store.messages.notifications = store.messages.notifications.filter(n => !rep.data.includes(n.uid));
break;
}
}
});
}
processMessages(messages, store) {
let msgs = messages.filter(m => {
return !m.gam.aud.some(st => isRootCollection(st));
});
this.storeStationMessages(msgs, store);
this.storeInboxMessages(store);
}
// TODO: Make this more like storeInboxMessages
storeStationMessages(messages, store) {
messages.forEach((message) => {
let msg = message.gam;
msg.num = message.num;
msg.aud.forEach((aud) => {
let msgClone = { ...msg, aud: [aud] };
let station = store.messages.stations[aud]
if (!station) {
store.messages.stations[aud] = [msgClone];
} else if (station.findIndex(o => o.uid === msgClone.uid) === -1) {
let newest = true;
for (let i = 0; i < station.length; i++) {
if (msgClone.wen < station[i].wen) {
station.splice(i, 0, msgClone);
newest = false;
break;
}
}
if (newest) station.push(msgClone);
// Print messages by date, for debugging:
// for (let msgClone of station.messages) {
// console.log(`msgClone ${msg.uid}: ${msg.wen}`);
// }
}
})
});
}
storeInboxMessages(store) {
let messages = store.messages.inbox.src.reduce((msgs, src) => {
let msgGroup = store.messages.stations[src];
if (!msgGroup) return msgs;
return msgs.concat(msgGroup.filter(this.filterInboxMessages)); // filter out app & accepted invite msgs
}, []);
let ret = _(messages)
.sort((a, b) => b.wen - a.wen) // sort by date
// sort must come before uniqBy! if uniqBy detects a dupe, it takes
// earlier element in the array. since we want later timestamps to
// override, sort first
.uniqBy('uid') // dedupe
.slice(0, INBOX_MESSAGE_COUNT) // grab the first 30 or so
.value(); // unwrap lodash chain
// for (let msg of ret) {
// console.log(`msg ${msg.uid}: ${msg.wen}`);
// }
// store.messages.inbox.messages = [
// {
// aud: ["~zod/marzod.zod"],
// aut: "zod",
// sep: { lin: {
// msg: "Hey marzod!"
// }},
// uid: "0v4.85q7h.25nnt.5mhop.92c1u.3rhsa",
// wen: 1538084786999,
// }, {
// aud: ["~zod/marzod.zod"],
// aut: "marzod",
// sep: { lin: {
// msg: "oh hey zod"
// }},
// uid: "0v4.85q7h.25nnt.5mhop.92c1u.3rhfa",
// wen: 1538084787000,
// },
// ...ret
// ];
store.messages.inbox.messages = ret;
}
// Filter out of inbox:
// - app messages
// - accepted invites
// - all DM invites (should automatically accept)
filterInboxMessages(msg) {
let msgDetails = getMessageContent(msg);
let typeApp = msgDetails.type === "app";
let typeInv = msgDetails.type === "inv";
// let isDmInvite = typeInv && isDMStation(msgDetails.content);
let isInboxMsg = msg.aud[0].split("/")[1] === "inbox";
let isEditUpdate = msgDetails.type === "edited item";
// let hasResponded = typeInv && msgDetails.content === "~zod/null";
if (typeApp) return false;
if (typeInv) return false;
// if (hasResponded) return false;
if (isEditUpdate) return false;
if (isInboxMsg) return false;
return true;
}
}

View File

@ -0,0 +1,92 @@
import { getStationDetails } from '/services';
import _ from 'lodash';
export class NamesReducer {
reduce(reports, store) {
reports.forEach((rep) => {
let ships = {};
let details;
switch (rep.type) {
case "circle.cos.loc":
ships[rep.from.ship] = [rep.from.path.split("/")[2]];
rep.data.con.sis.forEach((mem) => ships[mem] = []);
this.storeNames(ships, store.names);
break;
case "circle.cos.rem":
Object.arrayify(rep.data).forEach(({key: station, value: config}) => {
let details = getStationDetails(station);
if (ships[details.host]) {
ships[details.host] = _.uniq(ships[details.host].concat(details.cir));
} else {
ships[details.host] = [details.cir];
}
config.con.sis.forEach((mem) => {
if (!ships[mem]) ships[mem] = [];
});
});
this.storeNames(ships, store.names);
break;
case "circle.nes":
this.storeMessagesNames(rep.data, store.names);
break;
case "circle.gram":
this.storeMessagesNames([rep.data], store.names);
break;
// case "circle.pes.loc":
// stationName = `~${rep.from.ship}/${rep.from.path.split("/")[2]}`;
// this.updateConfig({pes: rep.data}, store.configs[stationName]);
// break;
case "circle.config.dif.source":
details = getStationDetails(rep.data.src);
ships[details.host] = [details.cir];
this.storeNames(ships, store.names);
break;
case "circle.config.dif.full":
details = getStationDetails(rep.data.src[0]); // TODO: API weirdness; we have to get name of new station from new station config's src property. Should maybe return a dict.
ships[details.host] = [details.cir];
this.storeNames(ships, store.names);
break;
case "circle.config.dif.permit": // TODO: This is very wonky, should be fixed with API discussion
details = getStationDetails(rep.data.cir); // TODO: API weirdness; we have to get name of new station from new station config's src property. Should maybe return a dict.
ships[details.host] = [details.cir];
this.storeNames(ships, store.names);
// case "circle.config.dif.remove":
// delete store.names[rep.data.cir];
// break;
}
});
}
storeMessagesNames(messages, storeNames) {
let ships = {};
messages.forEach((message) => {
let msg = message.gam;
msg.aud.forEach((aud) => {
let details = getStationDetails(aud); // TODO: API weirdness; we have to get name of new station from new station config's src property. Should maybe return a dict.
ships[details.host] = [details.cir];
});
ships[msg.aut] = ships[msg.aut] || [];
});
this.storeNames(ships, storeNames);
}
storeNames(ships, storeNames) {
Object.arrayify(ships).forEach(({key: ship, value: stations}) => {
let sttns = stations.filter(s => s !== "c");
if (!storeNames[ship]) {
storeNames[ship] = sttns;
} else {
storeNames[ship] = _.uniq(sttns.concat(storeNames[ship]))
}
});
}
}

View File

@ -0,0 +1,33 @@
import _ from 'lodash';
export class PublicReducer {
reduce(reports, store) {
reports.forEach((rep) => {
if (rep.type == "public") {
if (_.isArray(rep.data)) {
rep.data.forEach((c) => {
this.storeCircle(`~${rep.from.ship}`, c, store.public);
})
} else {
if (rep.data.add) {
this.storeCircle(`~${rep.from.ship}`, rep.data.cir, store.public);
} else {
this.removeCircle(`~${rep.from.ship}`, rep.data.cir, store.public);
}
}
}
});
}
storeCircle(ship, circle, storePublic) {
if (!storePublic[ship]) {
storePublic[ship] = [circle];
} else {
if (storePublic[ship].indexOf(circle) === -1) {
storePublic[ship] = [...storePublic[ship], circle];
}
}
}
removeCircle(ship, circle, storePublic) {
storePublic[ship] = storePublic[ship].filter((e) => e !== circle);
}
}

View File

@ -0,0 +1,43 @@
import { isDMStation } from '/lib/util';
import { warehouse } from '/warehouse';
import { api } from '/api';
export function getStationDetails(station) {
let host = station.split("/")[0].substr(1);
let config = warehouse.store.configs[station];
let ret = {
type: "none",
station: station,
host: host,
cir: station.split("/")[1],
};
let circleParts = ret.cir.split("-");
if (ret.cir === "c") {
ret.type = "aggregator";
} else if (isDMStation(station)) {
ret.type = "stream-dm";
} else {
ret.type = "stream-chat";
}
switch (ret.type) {
case "stream-chat":
ret.stationUrl = `/~chat/${station}`;
ret.stationTitle = ret.cir;
break;
case "stream-dm":
ret.stationTitle = ret.cir
.split(".")
.filter((mem) => mem !== api.authTokens.ship)
.map((mem) => `~${mem}`)
.join(", ");;
ret.stationUrl = `/~landscape/stream?station=${station}`;
break;
}
return ret;
}

View File

@ -0,0 +1,18 @@
class Store {
constructor() {
this.collections = window.injectedState;
this.setState = () => {};
}
setStateHandler(setState) {
this.setState = setState;
}
handleEvent(data) {
console.log("store.handleEvent", data);
}
}
export let store = new Store();
window.store = store;

View File

@ -0,0 +1,56 @@
import { api } from '/api';
import _ from 'lodash';
import { store } from '/store';
export class Subscription {
start() {
if (api.authTokens) {
console.log("subscription.start", window.injectedState);
this.initializeLandscape();
this.setCleanupTasks();
} else {
console.error("~~~ ERROR: Must set api.authTokens before operation ~~~");
}
}
setCleanupTasks() {
window.addEventListener("beforeunload", e => {
api.bindPaths.forEach(p => {
this.wipeSubscription(p);
});
});
}
wipeSubscription(path) {
api.hall({
wipe: {
sub: [{
hos: api.authTokens.ship,
pax: path
}]
}
});
}
initializeLandscape() {
api.bind(`/primary`, "PUT", api.authTokens.ship, 'write',
this.handleEvent.bind(this),
this.handleError.bind(this));
}
handleEvent(diff) {
console.log("subscription.handleEvent", diff);
store.handleEvent(diff);
}
handleError(err) {
console.error(err);
api.bind(`/primary`, "PUT", api.authTokens.ship, 'write',
this.handleEvent.bind(this),
this.handleError.bind(this));
}
}
export let subscription = new Subscription();

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,895 @@
::
:: /app/write.hoon
::
/- hall, *write
/+ *server, *write
::
/= index
/^ $-(json manx)
/: /===/app/write/index /!noun/
::
/= js
/^ octs
/; as-octs:mimes:html
/: /===/app/write/js/index /js/
::
/= css
/^ octs
/; as-octs:mimes:html
/: /===/app/write/css/index /css/
::
|%
::
+$ move [bone card]
::
+$ card
$% [%info wire toro:clay]
[%poke wire dock poke]
[%perm wire desk path rite:clay]
[%peer wire dock path]
[%pull wire dock ~]
[%quit ~]
[%diff diff]
[%build wire ? schematic:ford]
[%kill wire ~]
[%connect wire binding:http-server term]
[%http-response http-event:http]
[%disconnect binding:http-server]
==
::
+$ poke
$% [%hall-action action:hall]
[%write-action action]
==
::
+$ diff
$% [%hall-rumor rumor:hall]
[%json json]
[%write-collection collection]
[%write-rumor rumor]
==
::
--
::
|_ [bol=bowl:gall sat=state]
::
++ this .
:: +our-beak: beak for this app, with case set to current invocation date
::
++ our-beak /(scot %p our.bol)/[q.byk.bol]/(scot %da now.bol)
:: +allowed-by: checks if ship :who is allowed by the permission rules in :dic
::
++ allowed-by
|= [who=@p dic=dict:clay]
^- ?
?: =(who our.bol) &
=/ in-list=?
?| (~(has in p.who.rul.dic) who)
::
%- ~(rep by q.who.rul.dic)
|= [[@ta cru=crew:clay] out=_|]
?: out &
(~(has in cru) who)
==
?: =(%black mod.rul.dic)
!in-list
in-list
:: +write-file: write file at path
::
++ write-file
=, space:userlib
|= [pax=path cay=cage]
^- move
=. pax (weld our-beak pax)
[ost.bol %info (weld /write-file pax) (foal pax cay)]
::
++ update-udon-front
|= [fro=(map knot cord) udon=@t]
^- @t
%- of-wain:format
=/ tum (trip udon)
=/ id (find ";>" tum)
?~ id
%+ weld (front-to-wain fro)
(to-wain:format (crip (weld ";>\0a" tum)))
%+ weld (front-to-wain fro)
(to-wain:format (crip (slag u.id tum)))
::
++ front-to-wain
|= a=(map knot cord)
^- wain
=/ entries=wain
%+ turn ~(tap by a)
|= b=[knot cord]
=/ c=[term cord] (,[term cord] b)
(crip " [{<-.c>} {<+.c>}]")
::
?~ entries ~
;: weld
[':- :~' ~]
entries
[' ==' ~]
==
::
++ prep
|= old=(unit *)
^- (quip move _this)
~& write-prep+act.bol
?~ old
:_ this
[ost.bol %connect / [~ /'~publish'] %write]~
[~ this(sat (state u.old))]
::
++ poke-noun
|= a=*
^- (quip move _this)
?. =(src.bol our.bol)
[~ this]
?+ a
[~ this]
::
%test-build
=/ schema=schematic:ford
:-
:*
%bake
%write-info
*coin
[[our.bol q.byk.bol] /fora/write/web]
==
:*
%bake
%write-post
*coin
[[our.bol q.byk.bol] /post-1/fora/write/web]
==
:_ this
[ost.bol %build /test/build %.n schema]~
::
%print-subs
~& sup.bol
[~ this]
::
%kill-all-builds
:_ this
:~ [ost.bol %kill /collection/fora ~]
[ost.bol %kill /post/fora/post-1 ~]
[ost.bol %kill /comments/fora/post-1 ~]
[ost.bol %kill /post/fora/post-2 ~]
[ost.bol %kill /comments/fora/post-2 ~]
[ost.bol %kill /post/fora/post-3 ~]
[ost.bol %kill /comments/fora/post-3 ~]
==
::
::
%send-diff
=/ rum=json (frond:enjs:format %poke-noun ~)
=/ mov=(list move)
%+ turn (prey:pubsub:userlib /primary bol)
|= [=bone *]
[bone %diff %json rum]
~& mov+mov
[mov this]
::
%peer
~& %peer
:_ this
[ost.bol %peer /collection/fora [~zod %write] /collection/fora]~
::
%pull
~& %pull
=/ wir=wire /collection/fora
:_ this
[ost.bol %pull wir [~zod %write] ~]~
::
%flush-state
[~ this(sat *state)]
::
%print-state
~& sat
[~ this]
::
==
::
++ da
|_ moves=(list move)
::
++ da-this .
::
++ da-done
^- (quip move _this)
[(flop moves) this]
::
++ da-emit
|= mov=move
%_ da-this
moves [mov moves]
==
::
++ da-emil
|= mov=(list move)
%_ da-this
moves (welp (flop mov) moves)
==
::
++ da-change
|= del=delta
^+ da-this
?- -.del
::
%collection
=/ old=(unit collection)
?: =(our.bol who.del)
(~(get by pubs.sat) col.del)
(~(get by subs.sat) who.del col.del)
=/ new=collection
?~ old
[dat.del ~ ~]
[dat.del pos.u.old com.u.old]
=? pubs.sat =(our.bol who.del)
(~(put by pubs.sat) col.del new)
=? subs.sat !=(our.bol who.del)
(~(put by subs.sat) [who.del col.del] new)
(da-emil (affection del))
::
%post
=/ old=(unit collection)
?: =(our.bol who.del)
(~(get by pubs.sat) col.del)
(~(get by subs.sat) who.del col.del)
=/ new=collection
?~ old
[[%.n ~] (my [pos.del dat.del] ~) ~]
[col.u.old (~(put by pos.u.old) pos.del dat.del) com.u.old]
=? pubs.sat =(our.bol who.del)
(~(put by pubs.sat) col.del new)
=? subs.sat !=(our.bol who.del)
(~(put by subs.sat) [who.del col.del] new)
=. da-this (da-insert who.del col.del pos.del)
(da-emil (affection del))
::
%comments
=/ old=(unit collection)
?: =(our.bol who.del)
(~(get by pubs.sat) col.del)
(~(get by subs.sat) who.del col.del)
=/ new=collection
?~ old
[[%.n ~] ~ (my [pos.del dat.del] ~)]
[col.u.old pos.u.old (~(put by com.u.old) pos.del dat.del)]
=? pubs.sat =(our.bol who.del)
(~(put by pubs.sat) col.del new)
=? subs.sat !=(our.bol who.del)
(~(put by subs.sat) [who.del col.del] new)
(da-emil (affection del))
::
%total
=? pubs.sat =(our.bol who.del)
(~(put by pubs.sat) col.del dat.del)
=? subs.sat !=(our.bol who.del)
(~(put by subs.sat) [who.del col.del] dat.del)
::
=/ posts=(list @tas) ~(tap in ~(key by pos.dat.del))
=. da-this
|-
?~ posts
da-this
%= $
da-this (da-insert who.del col.del i.posts)
posts t.posts
==
(da-emil (affection del))
::
==
::
++ da-insert
|= [who=@p coll=@tas post=@tas]
^+ da-this
:: assume we've read our own posts
::
=? unread.sat !=(who our.bol)
(~(put in unread.sat) who coll post)
:: insertion sort into latest
::
=/ new-date=@da (need (get-date-for-index who coll post))
=/ pre=(list [@p @tas @tas]) ~
=/ suf=(list [@p @tas @tas]) latest.sat
=. latest.sat
|-
?~ suf
(weld pre [who coll post]~)
=/ i-date=@da (need (get-date-for-index i.suf))
?: (gte new-date i-date)
(weld pre [[who coll post] suf])
%= $
suf t.suf
pre (snoc pre i.suf)
==
da-this
--
:: +bake: apply delta
::
++ bake
|= del=delta
^- (quip move _this)
da-done:(da-change:da del)
:: +affection: rumors to interested
::
++ affection
|= del=delta
^- (list move)
%- zing
%+ turn ~(tap by sup.bol)
|= [b=bone s=ship p=path]
^- (list move)
=/ rum=(unit rumor) (feel p del)
?~ rum
~
[b %diff %write-rumor u.rum]~
:: +feel: delta to rumor
::
++ feel
|= [query=wire del=delta]
^- (unit rumor)
?+ query
~
[%primary ~]
[~ del]
::
[%collection @t ~]
=/ coll=@tas i.t.query
?: =(coll col.del)
[~ del]
~
::
==
::
++ get-date-for-index
|= [who=@p coll=@tas post=@tas]
^- (unit @da)
=/ col=(unit collection)
?: =(our.bol who)
(~(get by pubs.sat) coll)
(~(get by subs.sat) who coll)
?~ col ~
=/ pos=(unit (each [post-info manx] tang))
(~(get by pos.u.col) post)
?~ pos ~
?: ?=(%.n -.u.pos) ~
[~ date-created.-.p.u.pos]
::
++ made
|= [wir=wire wen=@da mad=made-result:ford]
^- (quip move _this)
?+ wir
[~ this]
::
[%collection @t ~]
=/ col=@tas i.t.wir
=/ awa (~(get by awaiting.sat) col)
::
=/ dat=(each collection-info tang)
?: ?=([%incomplete *] mad)
[%.n tang.mad]
?: ?=([%error *] build-result.mad)
[%.n message.build-result.mad]
?> ?=(%bake +<.build-result.mad)
?> ?=(%write-info p.cage.build-result.mad)
[%.y (collection-info q.q.cage.build-result.mad)]
::
?~ awa
(bake [%collection our.bol col dat])
=. builds.u.awa (~(del in builds.u.awa) wir)
?~ partial.u.awa
?~ builds.u.awa
:: one-off build, make delta and process it
::
=. awaiting.sat (~(del by awaiting.sat) col)
(bake [%collection our.bol col dat])
:: 1st part of multi-part, store partial delta and don't process it
::
=/ del=delta [%total our.bol col dat ~ ~]
=. awaiting.sat (~(put by awaiting.sat) col builds.u.awa `del)
[~ this]
::
?~ builds.u.awa
:: last part of multipart, update partial delta and process it
::
?> ?=(%total -.u.partial.u.awa)
=/ del=delta
:* %total
our.bol
col
dat
pos.dat.u.partial.u.awa
com.dat.u.partial.u.awa
==
=. awaiting.sat (~(del by awaiting.sat) col)
(bake del)
:: nth part of multi-part, update partial delta and don't process it
::
?> ?=(%total -.u.partial.u.awa)
=/ del=delta
:* %total
our.bol
col
dat
pos.dat.u.partial.u.awa
com.dat.u.partial.u.awa
==
=. awaiting.sat (~(put by awaiting.sat) col builds.u.awa `del)
[~ this]
::
[%post @t @t ~]
=/ col=@tas i.t.wir
=/ pos=@tas i.t.t.wir
=/ awa (~(get by awaiting.sat) col)
::
=/ dat=(each [post-info manx] tang)
?: ?=([%incomplete *] mad)
[%.n tang.mad]
?: ?=([%error *] build-result.mad)
[%.n message.build-result.mad]
?> ?=(%bake +<.build-result.mad)
?> ?=(%write-post p.cage.build-result.mad)
[%.y (,[post-info manx] q.q.cage.build-result.mad)]
::
?~ awa
(bake [%post our.bol col pos dat])
=. builds.u.awa (~(del in builds.u.awa) wir)
?~ partial.u.awa
?~ builds.u.awa
:: one-off build, make delta and process it
::
=. awaiting.sat (~(del by awaiting.sat) col)
(bake [%post our.bol col pos dat])
:: 1st part of multi-part, store partial delta and don't process it
::
=/ del=delta [%total our.bol col [%.n ~] (my [pos dat] ~) ~]
=. awaiting.sat (~(put by awaiting.sat) col builds.u.awa `del)
[~ this]
::
?~ builds.u.awa
:: last part of multipart, update partial delta and process it
::
?> ?=(%total -.u.partial.u.awa)
=/ del=delta
:* %total
our.bol
col
col.dat.u.partial.u.awa
(~(put by pos.dat.u.partial.u.awa) pos dat)
com.dat.u.partial.u.awa
==
=. awaiting.sat (~(del by awaiting.sat) col)
(bake del)
:: nth part of multi-part, update partial delta and don't process it
::
?> ?=(%total -.u.partial.u.awa)
=/ del=delta
:* %total
our.bol
col
col.dat.u.partial.u.awa
(~(put by pos.dat.u.partial.u.awa) pos dat)
com.dat.u.partial.u.awa
==
=. awaiting.sat (~(put by awaiting.sat) col builds.u.awa `del)
[~ this]
::
[%comments @t @t ~]
=/ col=@tas i.t.wir
=/ pos=@tas i.t.t.wir
=/ awa (~(get by awaiting.sat) col)
::
=/ dat=(each (list [comment-info manx]) tang)
?: ?=([%incomplete *] mad)
[%.n tang.mad]
?: ?=([%error *] build-result.mad)
[%.n message.build-result.mad]
?> ?=(%bake +<.build-result.mad)
?> ?=(%write-comments p.cage.build-result.mad)
[%.y (,(list [comment-info manx]) q.q.cage.build-result.mad)]
::
?~ awa
(bake [%comments our.bol col pos dat])
=. builds.u.awa (~(del in builds.u.awa) wir)
?~ partial.u.awa
?~ builds.u.awa
:: one-off build, make delta and process it
::
=. awaiting.sat (~(del by awaiting.sat) col)
(bake [%comments our.bol col pos dat])
:: 1st part of multi-part, store partial delta and don't process it
::
=/ del=delta [%total our.bol col [%.n ~] ~ (my [pos dat] ~)]
=. awaiting.sat (~(put by awaiting.sat) col builds.u.awa `del)
[~ this]
::
?~ builds.u.awa
:: last part of multipart, update partial delta and process it
::
?> ?=(%total -.u.partial.u.awa)
=/ del=delta
:* %total
our.bol
col
col.dat.u.partial.u.awa
pos.dat.u.partial.u.awa
(~(put by com.dat.u.partial.u.awa) pos dat)
==
=. awaiting.sat (~(del by awaiting.sat) col)
(bake del)
:: nth part of multi-part, update partial delta and don't process it
::
?> ?=(%total -.u.partial.u.awa)
=/ del=delta
:* %total
our.bol
col
col.dat.u.partial.u.awa
pos.dat.u.partial.u.awa
(~(put by com.dat.u.partial.u.awa) pos dat)
==
=. awaiting.sat (~(put by awaiting.sat) col builds.u.awa `del)
[~ this]
==
::
++ poke-write-action
|= act=action
^- (quip move _this)
?- -.act
::
%new-collection
:: XX check permissions of src.bol
:: XX check if file already exists
=/ conf=collection-info
:* our.bol
title.act
name.act
com.act
edit.act
now.bol
now.bol
==
:: XX set permissions
:: XX automatically serve collection
:: (add to set of builds)
=/ pax=path /web/write/[name.act]/write-info
::
=/ wir=wire /collection/[name.act]
=/ schema=schematic:ford
:* %bake
%write-info
*coin
[[our.bol q.byk.bol] /[name.act]/write/web]
==
:_ this
:~ (write-file pax %write-info !>(conf))
[ost.bol %build wir %.y schema]
==
::
%new-post
:: XX check permissions of src.bol
:: XX check if file already exists
:: XX check if coll doesn't exist
=. content.act (cat 3 content.act '\0a') :: XX fix udon parser
=/ front=(map knot cord)
%- my
:~ [%creator (scot %p src.bol)]
[%title title.act]
[%collection coll.act]
[%filename name.act]
[%comments com.act]
[%date-created (scot %da now.bol)]
[%last-modified (scot %da now.bol)]
[%pinned %false]
==
:: XX set permissions
:: XX add to set of builds
=/ pax=path /web/write/[coll.act]/[name.act]/udon
=/ out=@t (update-udon-front front content.act)
::
=/ post-wir=wire /post/[coll.act]/[name.act]
=/ post-schema=schematic:ford
:* %bake
%write-post
*coin
[[our.bol q.byk.bol] /[name.act]/[coll.act]/write/web]
==
::
=/ comments-wir=wire /comments/[coll.act]/[name.act]
=/ comments-schema=schematic:ford
:* %bake
%write-comments
*coin
[[our.bol q.byk.bol] /[name.act]/[coll.act]/write/web]
==
:_ this
:~ (write-file pax %udon !>(out))
[ost.bol %build comments-wir %.y comments-schema]
[ost.bol %build post-wir %.y post-schema]
==
::
%new-comment
:: XX check permissions of src.bol
:: XX check if file already exists
=. content.act (cat 3 content.act '\0a') :: XX fix udon parser
=/ front=(map knot cord)
%- my
:~ [%creator (scot %p src.bol)]
[%collection coll.act]
[%post post.act]
[%date-created (scot %da now.bol)]
[%last-modified (scot %da now.bol)]
==
:: XX set permissions
:: XX add to set of builds
=/ pax=path /web/write/[coll.act]/[post.act]/(scot %da now.bol)/udon
=/ out=@t (update-udon-front front content.act)
:_ this
[(write-file pax %udon !>(out))]~
::
%delete
[~ this]
::
%edit-collection
[~ this]
::
%edit-post
[~ this]
::
%edit-comment
[~ this]
::
%invite
[~ this]
::
:: %serve:
::
%serve
:: XX specialize this check for subfiles
?: (~(has by pubs.sat) coll.act)
[~ this]
=/ files=(list path)
.^((list path) %ct (weld our-beak /web/write/[coll.act]))
=/ all=[moves=(list move) builds=(set wire)]
%+ roll files
|= [pax=path out=[moves=(list move) builds=(set wire)]]
?+ pax
out
::
[%web %write @tas %write-info ~]
?> =(coll.act i.t.t.pax)
=/ wir=wire /collection/[coll.act]
=/ schema=schematic:ford
:* %bake
%write-info
*coin
[[our.bol q.byk.bol] /[coll.act]/write/web]
==
%= out
moves [[ost.bol %build wir %.y schema] moves.out]
builds (~(put in builds.out) wir)
==
::
[%web %write @tas @tas %udon ~]
?> =(coll.act i.t.t.pax)
=/ post i.t.t.t.pax
=/ post-wir=wire /post/[coll.act]/[post]
=/ post-schema=schematic:ford
:* %bake
%write-post
*coin
[[our.bol q.byk.bol] /[post]/[coll.act]/write/web]
==
::
=/ comments-wir=wire /comments/[coll.act]/[post]
=/ comments-schema=schematic:ford
:* %bake
%write-comments
*coin
[[our.bol q.byk.bol] /[post]/[coll.act]/write/web]
==
%= out
moves
:* [ost.bol %build post-wir %.y post-schema]
[ost.bol %build comments-wir %.y comments-schema]
moves.out
==
::
builds
(~(uni in builds.out) (sy post-wir comments-wir ~))
==
::
==
:- moves.all
%= this
awaiting.sat (~(put by awaiting.sat) coll.act builds.all ~)
==
::
:: %unserve:
::
%unserve
:: XX pull subscriptions for unserved collections
::
=/ col=(unit collection) (~(get by pubs.sat) coll.act)
?~ col
~| [%non-existent-collection coll.act] !!
=/ kills=(list move)
%+ roll ~(tap by pos.u.col)
|= [[post=@tas *] out=(list move)]
:* [ost.bol %kill /post/[coll.act]/[post] ~]
[ost.bol %kill /comments/[coll.act]/[post] ~]
out
==
::
=/ new-latest=(list [@p @tas @tas])
%+ skip latest.sat
|= [who=@p coll=@tas post=@tas]
?& =(who our.bol)
=(coll coll.act)
==
::
=/ new-unread=(set [@p @tas @tas])
%- sy
%+ skip ~(tap in unread.sat)
|= [who=@p coll=@tas post=@tas]
?& =(who our.bol)
=(coll coll.act)
==
::
:- [[ost.bol %kill /collection/[coll.act] ~] kills]
%= this
pubs.sat (~(del by pubs.sat) coll.act)
awaiting.sat (~(del by awaiting.sat) coll.act)
latest.sat new-latest
unread.sat new-unread
==
::
:: %subscribe:
::
%subscribe
=/ wir=wire /collection/[coll.act]
:_ this
[ost.bol %peer wir [who.act %write] wir]~
::
:: %unsubscribe:
::
%unsubscribe
=/ new-latest=(list [@p @tas @tas])
%+ skip latest.sat
|= [who=@p coll=@tas post=@tas]
?& =(who our.bol)
=(coll coll.act)
==
::
=/ new-unread=(set [@p @tas @tas])
%- sy
%+ skip ~(tap in unread.sat)
|= [who=@p coll=@tas post=@tas]
?& =(who our.bol)
=(coll coll.act)
==
=/ wir=wire /collection/[coll.act]
:- [ost.bol %pull wir [who.act %write] ~]~
%= this
subs.sat (~(del by subs.sat) who.act coll.act)
latest.sat new-latest
unread.sat new-unread
==
::
==
::
++ bound
|= [wir=wire success=? binding=binding:http-server]
^- (quip move _this)
[~ this]
::
:: +poke-handle-http-request: received on a new connection established
::
++ poke-handle-http-request
%- (require-authorization:app ost.bol move this)
|= =inbound-request:http-server
^- (quip move _this)
::
=/ request-line (parse-request-line url.request.inbound-request)
?+ request-line
:_ this
[ost.bol %http-response not-found:app]~
:: styling
::
[[[~ %css] [%'~publish' %index ~]] ~]
:_ this
[ost.bol %http-response (css-response:app css)]~
:: scripting
::
[[[~ %js] [%'~publish' %index ~]] ~]
:_ this
[ost.bol %http-response (js-response:app js)]~
::
::
[[~ [%'~publish' ~]] ~]
=/ hym=manx (index (state-to-json sat))
:_ this
[ost.bol %http-response (manx-response:app hym)]~
::
::
[[~ [%'~publish' @t ~]] ~]
=/ who=(unit ship) (rush i.t.site.request-line ;~(pfix sig fed:ag))
?~ who
:_ this
[ost.bol %http-response not-found:app]~
=/ hym=manx
;div: {<u.who>} root page
:_ this
[ost.bol %http-response (manx-response:app hym)]~
::
:: forum view
::
[[~ [%'~publish' @t @t ~]] ~]
=/ who=(unit ship) (rush i.t.site.request-line ;~(pfix sig fed:ag))
=/ coll=@tas i.t.t.site.request-line
:: ?~ who
:_ this
[ost.bol %http-response not-found:app]~
::
:: post view
::
[[~ [%'~publish' @t @t @t ~]] ~]
=/ who=(unit ship) (rush i.t.site.request-line ;~(pfix sig fed:ag))
=/ coll=@tas i.t.t.site.request-line
=/ post=@tas i.t.t.t.site.request-line
:: ?~ who
:_ this
[ost.bol %http-response not-found:app]~
:: local request
::
::
==
::
++ peer-primary
|= wir=wire
^- (quip move _this)
?. =(our.bol src.bol)
:: only we are allowed to subscribe on primary
::
:_ this
[ost.bol %quit ~]~
[~ this]
::
++ pull
|= wir=wire
^- (quip move _this)
[~ this]
::
++ peer-collection
|= wir=wire
^- (quip move _this)
?. ?=([@tas ~] wir)
[~ this]
:: XX handle permissions for foreign subscriptions
::
=/ coll=@tas i.wir
=/ col=(unit collection) (~(get by pubs.sat) coll)
?~ col
[~ this]
=/ rum=rumor
[%total our.bol coll u.col]
:_ this
[ost.bol %diff %write-rumor rum]~
::
++ diff-write-rumor
|= [wir=wire rum=rumor]
^- (quip move _this)
(bake rum)
::
:: +poke-handle-http-cancel: received when a connection was killed
::
++ poke-handle-http-cancel
|= =inbound-request:http-server
^- (quip move _this)
[~ this]
::
--

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,21 @@
|= inject=json
^- manx
;html
::
;head
;title: Write
;meta(charset "utf-8");
;meta
=name "viewport"
=content "width=device-width, initial-scale=1, shrink-to-fit=no";
;link(rel "stylesheet", href "/~publish/index.css");
;script@"/~/channel/channel.js";
;script@"/session.js";
;script: window.injectedState = {(en-json:html inject)}
==
::
;body
;div#root;
;script@"/~publish/index.js";
==
==

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,188 @@
/- *write
/+ elem-to-react-json
|%
::
++ front-to-post-info
|= fro=(map knot cord)
^- post-info
=/ got ~(got by fro)
~| %invalid-frontmatter
:* (slav %p (got %creator))
(got %title)
(got %collection)
(got %filename)
(comment-config (got %comments))
(slav %da (got %date-created))
(slav %da (got %last-modified))
(rash (got %pinned) (fuss %true %false))
==
::
++ front-to-comment-info
|= fro=(map knot cord)
^- comment-info
=/ got ~(got by fro)
~| %invalid-frontmatter
:* (slav %p (got %creator))
(got %collection)
(got %post)
(slav %da (got %date-created))
(slav %da (got %last-modified))
==
::
++ collection-info-to-json
|= con=collection-info
^- json
%- pairs:enjs:format
:~ :- %owner [%s (scot %p owner.con)]
:- %title [%s title.con]
:- %comments [%s comments.con]
:- %allow-edit [%s allow-edit.con]
:- %date-created (time:enjs:format date-created.con)
:- %last-modified (time:enjs:format last-modified.con)
:- %filename [%s filename.con]
==
::
++ post-info-to-json
|= info=post-info
^- json
%- pairs:enjs:format
:~ :- %creator [%s (scot %p creator.info)]
:- %title [%s title.info]
:- %comments [%s comments.info]
:- %date-created (time:enjs:format date-created.info)
:- %last-modified (time:enjs:format last-modified.info)
:- %pinned [%b pinned.info]
:- %filename [%s filename.info]
:- %collection [%s collection.info]
==
::
++ comment-info-to-json
|= info=comment-info
^- json
%- pairs:enjs:format
:~ :- %creator [%s (scot %p creator.info)]
:- %date-created (time:enjs:format date-created.info)
:- %last-modified (time:enjs:format last-modified.info)
:- %post [%s post.info]
:- %collection [%s collection.info]
==
::
++ tang-to-json
|= tan=tang
%- wall:enjs:format
%- zing
%+ turn tan
|= a=tank
(wash [0 80] a)
::
++ string-to-symbol
|= tap=tape
^- @tas
%- crip
%+ turn tap
|= a=@
?: ?| &((gte a 'a') (lte a 'z'))
&((gte a '0') (lte a '9'))
==
a
?: &((gte a 'A') (lte a 'Z'))
(add 32 a)
'-'
::
++ collection-build-to-json
|= bud=(each collection-info tang)
^- json
?: ?=(%.y -.bud)
(collection-info-to-json +.bud)
(tang-to-json +.bud)
::
++ post-build-to-json
|= bud=(each [post-info manx] tang)
^- json
?: ?=(%.y -.bud)
%- pairs:enjs:format
:~ info+(post-info-to-json +<.bud)
body+(elem-to-react-json +>.bud)
==
(tang-to-json +.bud)
::
++ comment-build-to-json
|= bud=(each (list [comment-info manx]) tang)
^- json
?: ?=(%.y -.bud)
:- %a
%+ turn p.bud
|= [com=comment-info man=manx]
^- json
%- pairs:enjs:format
:~ info+(comment-info-to-json com)
body+(elem-to-react-json man)
==
(tang-to-json +.bud)
::
++ total-build-to-json
|= col=collection
^- json
%- pairs:enjs:format
:~ info+(collection-build-to-json col.col)
:+ %posts
%a
%+ turn ~(tap in ~(key by pos.col))
|= post=@tas
^- json
=/ post-build (~(got by pos.col) post)
=/ comm-build (~(got by com.col) post)
%- pairs:enjs:format
:~ name+s+post
post+(post-build-to-json post-build)
comments+(comment-build-to-json comm-build)
==
==
::
++ state-to-json
|= sat=state
^- json
%- pairs:enjs:format
:~ :+ %pubs
%a
%+ turn ~(tap by pubs.sat)
|= [nom=@tas col=collection]
^- json
%- pairs:enjs:format
:~ [%coll s+nom]
[%data (total-build-to-json col)]
==
::
:+ %subs
%a
%+ turn ~(tap by subs.sat)
|= [[who=@p nom=@tas] col=collection]
^- json
%- pairs:enjs:format
:~ [%coll s+nom]
[%who (ship:enjs:format who)]
[%data (total-build-to-json col)]
==
::
:+ %latest
%a
%+ turn latest.sat
|= [who=@p coll=@tas post=@tas]
%- pairs:enjs:format
:~ who+(ship:enjs:format who)
coll+s+coll
post+s+post
==
::
:+ %unread
%a
%+ turn ~(tap in unread.sat)
|= [who=@p coll=@tas post=@tas]
%- pairs:enjs:format
:~ who+(ship:enjs:format who)
coll+s+coll
post+s+post
==
==
::
--

View File

@ -0,0 +1,187 @@
::
:::: /hoon/action/write/mar
::
/? 309
/- write
=, format
::
|_ act=action:write
::
++ grow
|%
++ tank >act<
--
::
++ grab
|%
++ noun action:write
++ json
|= jon=^json
%- action:write
=< (action jon)
|%
++ action
%- of:dejs
:~ new-collection+new-collection
new-post+new-post
new-comment+new-comment
::
delete+item-id
::
edit-collection+edit-collection
edit-post+edit-post
edit-comment+edit-comment
::
invite+invite
::
serve+serve
unserve+unserve
::
subscribe+subscribe
unsubscribe+unsubscribe
::
==
::
++ new-collection
%- ot:dejs
:~ name+(su:dejs sym)
title+so:dejs
comments+comment-config
allow-edit+edit-config
perm+perm-config
==
::
++ new-post
%- ot:dejs
:~ coll+(su:dejs sym)
name+(su:dejs sym)
title+so:dejs
comments+comment-config
perm+perm-config
content+so:dejs
==
::
++ new-comment
%- ot:dejs
:~ coll+(su:dejs sym)
name+(su:dejs sym)
content+so:dejs
==
::
++ edit-collection
%- ot:dejs
:~ name+(su:dejs sym)
title+so:dejs
comments+comment-config
allow-edit+edit-config
perm+perm-config
==
::
++ edit-post
%- ot:dejs
:~ coll+(su:dejs sym)
name+(su:dejs sym)
title+so:dejs
comments+comment-config
perm+perm-config
content+so:dejs
==
::
++ edit-comment
%- ot:dejs
:~ coll+(su:dejs sym)
name+(su:dejs sym)
id+(su:dejs sym)
content+so:dejs
==
::
++ comment-config
%- su:dejs
;~(pose (jest %open) (jest %closed) (jest %none))
::
++ edit-config
%- su:dejs
;~(pose (jest %post) (jest %comment) (jest %all) (jest %none))
::
++ perm-config
%- ot:dejs
:~ :- %read
%- ot:dejs
:~ mod+(su:dejs ;~(pose (jest %black) (jest %white)))
who+whoms
==
:- %write
%- ot:dejs
:~ mod+(su:dejs ;~(pose (jest %black) (jest %white)))
who+whoms
== ==
::
++ whoms
|= jon=^json
^- (set whom:clay)
=/ x ((ar:dejs (su:dejs fed:ag)) jon)
%- (set whom:clay)
%- ~(run in (sy x))
|=(w=@ [& w])
::
++ item-id
|= jon=^json
^- item-id:write
?> ?=(%a -.jon) :: must be array
?< ?=(~ +.jon) :: must have at least one item
?> ?=([%s @t] -.+.jon) :: first item must be string
=/ coll=@tas (slav %tas +.-.+.jon) :: get first item as @tas
?~ +.+.jon :: if only one item, return it
coll
?> ?=([%s @t] -.+.+.jon) :: second item must be string
=/ post=@tas (slav %tas +.-.+.+.jon) :: get second item as @tas
?~ +.+.+.jon :: if two items, return them
[coll post]
?> ?=([%s @t] -.+.+.+.jon) :: third item must be string
=/ comm=@tas (slav %tas +.-.+.+.+.jon) :: get third item as @tas
?> ?=(~ +.+.+.+.jon) :: no fourth item
[coll post comm]
::
++ invite
%- ot:dejs
:~ coll+(su:dejs sym)
who+(ar:dejs (su:dejs fed:ag))
==
::
++ serve
%- ot:dejs
:~ coll+(su:dejs sym)
==
::
++ unserve
%- ot:dejs
:~ coll+(su:dejs sym)
==
::
++ subscribe
%- ot:dejs
:~ who+(su:dejs fed:ag)
coll+(su:dejs sym)
==
::
++ unsubscribe
%- ot:dejs
:~ who+(su:dejs fed:ag)
coll+(su:dejs sym)
==
::
--
--
--

View File

@ -0,0 +1,84 @@
::
:::: /hoon/info/write/mar
::
/- write
!:
|_ con=collection-info:write
::
::
++ grow
|%
++ mime
:- /text/x-write-info
(as-octs:mimes:html (of-wain:format txt))
++ txt
^- wain
:~ (cat 3 'owner: ' (scot %p owner.con))
(cat 3 'title: ' title.con)
(cat 3 'filename: ' filename.con)
(cat 3 'comments: ' comments.con)
(cat 3 'allow-edit: ' allow-edit.con)
(cat 3 'date-created: ' (scot %da date-created.con))
(cat 3 'last-modified: ' (scot %da last-modified.con))
==
--
++ grab
|%
++ mime
|= [mite:eyre p=octs:eyre]
(txt (to-wain:format q.p))
++ txt
|= txs=(pole @t)
^- collection-info:write
:: TODO: putting ~ instead of * breaks this but shouldn't
::
?> ?= $: owner=@t
title=@t
filename=@t
comments=@t
allow-edit=@t
date-created=@t
last-modified=@t
*
==
txs
::
:* %+ rash owner.txs
;~(pfix (jest 'owner: ~') fed:ag)
::
%+ rash title.txs
;~(pfix (jest 'title: ') (cook crip (star next)))
::
%+ rash filename.txs
;~(pfix (jest 'filename: ') (cook crip (star next)))
::
%+ rash comments.txs
;~ pfix
(jest 'comments: ')
%+ cook comment-config:write
;~(pose (jest %open) (jest %closed) (jest %none))
==
::
%+ rash allow-edit.txs
;~ pfix
(jest 'allow-edit: ')
%+ cook edit-config:write
;~(pose (jest %post) (jest %comment) (jest %all) (jest %none))
==
::
%+ rash date-created.txs
;~ pfix
(jest 'date-created: ~')
(cook year when:so)
==
::
%+ rash last-modified.txs
;~ pfix
(jest 'last-modified: ~')
(cook year when:so)
==
==
++ noun collection-info:write
--
++ grad %mime
--

View File

@ -0,0 +1,47 @@
/- *write
/+ *write, elem-to-react-json
|_ rum=rumor
++ grab
|%
++ noun rumor
--
++ grow
|%
++ noun rum
++ json
=, enjs:format
%+ frond -.rum
?- -.rum
%collection
%- pairs
:~ [%coll s+col.rum]
[%who (ship who.rum)]
[%data (collection-build-to-json dat.rum)]
==
::
%post
%- pairs
:~ [%coll s+col.rum]
[%post s+pos.rum]
[%who (ship who.rum)]
[%data (post-build-to-json dat.rum)]
==
::
%comments
%- pairs
:~ [%coll s+col.rum]
[%post s+pos.rum]
[%who (ship who.rum)]
[%data (comment-build-to-json dat.rum)]
==
::
%total
%- pairs
:~ [%coll s+col.rum]
[%who (ship who.rum)]
[%data (total-build-to-json dat.rum)]
==
==
::
--
--

View File

@ -0,0 +1,24 @@
/- write
/+ write, cram, elem-to-react-json
/= args /$ ,[beam *]
/= result
/^ (list [comment-info:write manx])
/;
|= $= comments
%+ map knot
$: comment-front=(map knot cord)
comment-content=manx
~
==
^- (list [comment-info:write manx])
:: XX sort this list
%+ turn ~(tap by comments)
|= [fil=knot front=(map knot cord) content=manx ~]
^- [comment-info:write manx]
[(front-to-comment-info:write front) content]
::
/_
/. /&front&/udon/
/&elem&/udon/
==
result

View File

@ -0,0 +1,17 @@
/- write
/+ write, cram, elem-to-react-json
/= args /$ ,[beam *]
/= result
/^ [post-info:write manx]
/;
|= $: post-front=(map knot cord)
post-content=manx
~
==
:- (front-to-post-info:write post-front)
post-content
::
/. /&front&/udon/
/&elem&/udon/
==
result

View File

@ -0,0 +1,123 @@
|%
+$ item-id
$? coll=@tas
[coll=@tas post=@tas]
[coll=@tas post=@tas comment=@tas]
==
::
+$ action
$% $: %new-collection
name=@tas
title=@t
com=comment-config
edit=edit-config
perm=perm-config
==
::
$: %new-post
coll=@tas
name=@tas
title=@t
com=comment-config
perm=perm-config
content=@t
==
::
[%new-comment coll=@tas post=@tas content=@t]
::
[%delete item-id]
::
$: %edit-collection
name=@tas
title=@t
com=comment-config
edit=edit-config
perm=perm-config
==
::
$: %edit-post
coll=@tas
name=@tas
title=@t
com=comment-config
perm=perm-config
content=@t
==
::
[%edit-comment coll=@tas post=@tas id=@tas content=@t]
::
[%invite coll=@tas who=(list ship)]
::
[%serve coll=@tas]
[%unserve coll=@tas]
::
[%subscribe who=@p coll=@tas]
[%unsubscribe who=@p coll=@tas]
::
==
::
+$ collection-info
$: owner=@p
title=@t
filename=@tas
comments=comment-config
allow-edit=edit-config
date-created=@da
last-modified=@da
==
::
+$ post-info
$: creator=@p
title=@t
collection=@tas
filename=@tas
comments=comment-config
date-created=@da
last-modified=@da
pinned=?
==
::
+$ comment-info
$: creator=@p
collection=@tas
post=@tas
date-created=@da
last-modified=@da
==
::
+$ perm-config [read=rule:clay write=rule:clay]
::
::
+$ comment-config $?(%open %closed %none)
::
::
+$ edit-config $?(%post %comment %all %none)
::
+$ rumor delta
:: $% [%collection who=@p col=@tas dat=(each collection-info tang)]
:: [%post who=@p col=@tas pos=@tas dat=(each [post-info manx] tang)]
:: [%comments who=@p col=@tas pos=@tas dat=(each (list [comment-info manx]) tang)]
:: [%serve who=@p nom=@tas col=collection]
:: ==
::
+$ collection
$: col=(each collection-info tang)
pos=(map @tas (each [post-info manx] tang))
com=(map @tas (each (list [comment-info manx]) tang))
==
::
+$ state
$: pubs=(map @tas collection)
subs=(map [ship @tas] collection)
awaiting=(map @tas [builds=(set wire) partial=(unit delta)])
latest=(list [who=ship coll=@tas post=@tas])
unread=(set [who=ship coll=@tas post=@tas])
==
::
+$ delta
$% [%collection who=@p col=@tas dat=(each collection-info tang)]
[%post who=@p col=@tas pos=@tas dat=(each [post-info manx] tang)]
[%comments who=@p col=@tas pos=@tas dat=(each (list [comment-info manx]) tang)]
[%total who=@p col=@tas dat=collection]
==
--

View File

@ -1,2 +1,6 @@
#!/bin/bash
cp urbitrc ./apps/*/.urbitrc
cp urbitrc ./apps/chat/.urbitrc
cp urbitrc ./apps/launch/.urbitrc
cp urbitrc ./apps/modulo/.urbitrc
cp urbitrc ./apps/publish/.urbitrc
cp urbitrc ./apps/weather/.urbitrc