js: reworked store/reducer patterns

This commit is contained in:
Logan Allen 2020-05-22 14:22:11 -04:00
parent 4550bc6dc1
commit fec5646229
48 changed files with 327 additions and 1253 deletions

View File

@ -6,12 +6,12 @@ import './css/fonts.css';
import { light } from '@tlon/indigo-react'; import { light } from '@tlon/indigo-react';
import LaunchApp from './apps/launch/app'; import LaunchApp from './apps/launch/app';
import ChatApp from './apps/chat/ChatApp'; import ChatApp from './apps/chat/app';
import DojoApp from './apps/dojo/DojoApp'; import DojoApp from './apps/dojo/app';
import StatusBar from './components/StatusBar'; import StatusBar from './components/StatusBar';
import GroupsApp from './apps/groups/GroupsApp'; import GroupsApp from './apps/groups/app';
import LinksApp from './apps/links/LinksApp'; import LinksApp from './apps/links/app';
import PublishApp from './apps/publish/PublishApp'; import PublishApp from './apps/publish/app';
import Store from './store'; import Store from './store';
import Subscription from './subscription'; import Subscription from './subscription';

View File

@ -1,57 +1,9 @@
import _ from 'lodash'; import BaseApi from './base';
export default class Api {
constructor(ship, channel, store) {
this.ship = ship;
this.channel = channel;
this.store = store;
this.bindPaths = [];
}
bind(path, method, ship = this.ship, appl = 'publish', success, fail) {
this.bindPaths = _.uniq([...this.bindPaths, path]);
this.channel.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) => {
this.channel.poke(this.ship, appl, mark, data,
(json) => {
resolve(json);
},
(err) => {
reject(err);
});
});
}
// TODO add error handling
handleErrors(response) {
if (!response.ok)
throw Error(response.status);
return response;
}
export default class PublishApi extends BaseApi {
fetchNotebooks() { fetchNotebooks() {
fetch('/~publish/notebooks.json') fetch('/publish-view/notebooks.json')
.then(response => response.json()) .then(response => response.json())
.then((json) => { .then((json) => {
this.store.handleEvent({ this.store.handleEvent({
@ -62,7 +14,7 @@ throw Error(response.status);
} }
fetchNotebook(host, book) { fetchNotebook(host, book) {
fetch(`/~publish/${host}/${book}.json`) fetch(`/publish-view/${host}/${book}.json`)
.then(response => response.json()) .then(response => response.json())
.then((json) => { .then((json) => {
this.store.handleEvent({ this.store.handleEvent({
@ -75,7 +27,7 @@ throw Error(response.status);
} }
fetchNote(host, book, note) { fetchNote(host, book, note) {
fetch(`/~publish/${host}/${book}/${note}.json`) fetch(`/publish-view/${host}/${book}/${note}.json`)
.then(response => response.json()) .then(response => response.json())
.then((json) => { .then((json) => {
this.store.handleEvent({ this.store.handleEvent({
@ -89,7 +41,7 @@ throw Error(response.status);
} }
fetchNotesPage(host, book, start, length) { fetchNotesPage(host, book, start, length) {
fetch(`/~publish/notes/${host}/${book}/${start}/${length}.json`) fetch(`/publish-view/notes/${host}/${book}/${start}/${length}.json`)
.then(response => response.json()) .then(response => response.json())
.then((json) => { .then((json) => {
this.store.handleEvent({ this.store.handleEvent({
@ -104,7 +56,7 @@ throw Error(response.status);
} }
fetchCommentsPage(host, book, note, start, length) { fetchCommentsPage(host, book, note, start, length) {
fetch(`/~publish/comments/${host}/${book}/${note}/${start}/${length}.json`) fetch(`/publish-view/comments/${host}/${book}/${note}/${start}/${length}.json`)
.then(response => response.json()) .then(response => response.json())
.then((json) => { .then((json) => {
this.store.handleEvent({ this.store.handleEvent({
@ -131,13 +83,5 @@ throw Error(response.status);
} }
}); });
} }
setSelected(selected) {
this.store.handleEvent({
type: 'local',
data: {
selected: selected
}
});
}
} }

View File

@ -2,7 +2,7 @@ import React from 'react';
import { Route } from 'react-router-dom'; import { Route } from 'react-router-dom';
import Api from './api'; import Api from './api';
import Store from './store'; import ChatStore from '../../store/chat';
import Subscription from './subscription'; import Subscription from './subscription';
import './css/custom.css'; import './css/custom.css';

View File

@ -1,73 +0,0 @@
import InitialReducer from '../../reducers/initial';
import ContactUpdateReducer from '../../reducers/contact-update';
import ChatUpdateReducer from '../../reducers/chat-update';
import InviteUpdateReducer from '../../reducers/invite-update';
import PermissionUpdateReducer from '../../reducers/permission-update';
import MetadataReducer from '../../reducers/metadata-update';
import LocalReducer from '../../reducers/local';
import S3Reducer from '../../reducers/s3';
export default class Store {
constructor() {
this.state = this.initialState();
this.initialReducer = new InitialReducer();
this.permissionUpdateReducer = new PermissionUpdateReducer();
this.contactUpdateReducer = new ContactUpdateReducer();
this.chatUpdateReducer = new ChatUpdateReducer();
this.inviteUpdateReducer = new InviteUpdateReducer();
this.metadataReducer = new MetadataReducer();
this.localReducer = new LocalReducer();
this.s3Reducer = new S3Reducer();
this.setState = () => {};
}
initialState() {
return {
inbox: {},
chatSynced: null,
contacts: {},
permissions: {},
invites: {},
associations: {
chat: {},
contacts: {}
},
sidebarShown: true,
pendingMessages: new Map([]),
chatInitialized: false,
s3: {}
};
}
setStateHandler(setState) {
this.setState = setState;
}
clear() {
this.handleEvent({
data: { clear: true }
});
}
handleEvent(data) {
let json = data.data;
if ('clear' in json && json.clear) {
this.setState(this.initialState());
return;
}
this.initialReducer.reduce(json, this.state);
this.permissionUpdateReducer.reduce(json, this.state);
this.contactUpdateReducer.reduce(json, this.state);
this.chatUpdateReducer.reduce(json, this.state);
this.inviteUpdateReducer.reduce(json, this.state);
this.metadataReducer.reduce(json, this.state);
this.localReducer.reduce(json, this.state);
this.s3Reducer.reduce(json, this.state);
this.setState(this.state);
}
}

View File

@ -3,7 +3,7 @@ import { Route } from 'react-router-dom';
import Api from './api'; import Api from './api';
import Subscription from './subscription'; import Subscription from './subscription';
import Store from './store'; import GroupsStore from '../../store/groups';
import './css/custom.css'; import './css/custom.css';
@ -17,7 +17,7 @@ import GroupDetail from './components/lib/group-detail';
export default class GroupsApp extends Component { export default class GroupsApp extends Component {
constructor(props) { constructor(props) {
super(props); super(props);
this.store = new Store(); this.store = new GroupsStore();
this.store.setStateHandler(this.setState.bind(this)); this.store.setStateHandler(this.setState.bind(this));
this.state = this.store.state; this.state = this.store.state;

View File

@ -1,71 +0,0 @@
import _ from 'lodash';
export class MetadataReducer {
reduce(json, state) {
let data = _.get(json, 'metadata-update', false);
if (data) {
this.associations(data, state);
this.add(data, state);
this.update(data, state);
this.remove(data, state);
}
}
associations(json, state) {
let data = _.get(json, 'associations', false);
if (data) {
let metadata = {};
Object.keys(data).forEach((key) => {
let val = data[key];
let groupPath = val['group-path'];
if (!(groupPath in metadata)) {
metadata[groupPath] = {};
}
metadata[groupPath][key] = val;
});
state.associations = metadata;
}
}
add(json, state) {
let data = _.get(json, 'add', false);
if (data) {
let metadata = state.associations;
if (!(data['group-path'] in metadata)) {
metadata[data['group-path']] = {};
}
metadata[data['group-path']]
[`${data["group-path"]}/${data["app-name"]}${data["app-path"]}`] = data;
state.associations = metadata;
}
}
update(json, state) {
let data = _.get(json, 'update-metadata', false);
if (data) {
let metadata = state.associations;
if (!(data["group-path"] in metadata)) {
metadata[data["group-path"]] = {};
}
metadata[data["group-path"]][
`${data["group-path"]}/${data["app-name"]}${data["app-path"]}`
] = data;
state.associations = metadata;
}
}
remove(json, state) {
let data = _.get(json, 'remove', false);
if (data) {
let metadata = state.associations;
if (data['group-path'] in metadata) {
let path =
`${data['group-path']}/${data['app-name']}${data['app-path']}`
delete metadata[data["group-path"]][path];
state.associations = metadata;
}
}
}
}

View File

@ -1,66 +0,0 @@
import InitialReducer from '../../reducers/initial';
import ContactUpdateReducer from '../../reducers/contact-update';
import GroupUpdateReducer from '../../reducers/group-update';
import InviteUpdateReducer from '../../reducers/invite-update';
import PermissionUpdateReducer from '../../reducers/permission-update';
import { MetadataReducer } from './reducers/metadata-update';
import LocalReducer from '../../reducers/local';
import S3Reducer from '../../reducers/s3.js';
export default class Store {
constructor() {
this.state = this.initialState();
this.initialReducer = new InitialReducer();
this.groupUpdateReducer = new GroupUpdateReducer();
this.permissionUpdateReducer = new PermissionUpdateReducer();
this.contactUpdateReducer = new ContactUpdateReducer();
this.inviteUpdateReducer = new InviteUpdateReducer();
this.metadataReducer = new MetadataReducer();
this.s3Reducer = new S3Reducer();
this.localReducer = new LocalReducer();
this.setState = () => {};
}
initialState() {
return {
contacts: {},
groups: {},
associations: {},
permissions: {},
invites: {},
s3: {}
};
}
clear() {
this.handleEvent({
data: { clear: true }
});
}
setStateHandler(setState) {
this.setState = setState;
}
handleEvent(data) {
const json = data.data;
if ('clear' in json && json.clear) {
this.setState(this.initialState());
return;
}
console.log(json);
this.initialReducer.reduce(json, this.state);
this.groupUpdateReducer.reduce(json, this.state);
this.permissionUpdateReducer.reduce(json, this.state);
this.contactUpdateReducer.reduce(json, this.state);
this.inviteUpdateReducer.reduce(json, this.state);
this.metadataReducer.reduce(json, this.state);
this.s3Reducer.reduce(json, this.state);
this.localReducer.reduce(json, this.state);
this.setState(this.state);
}
}

View File

@ -4,7 +4,7 @@ import { Switch, Route } from 'react-router-dom';
import _ from 'lodash'; import _ from 'lodash';
import Api from './api'; import Api from './api';
import Store from './store'; import LinksStore from '../../store/links';
import Subscription from './subscription'; import Subscription from './subscription';
import './css/custom.css'; import './css/custom.css';
@ -21,7 +21,7 @@ import { makeRoutePath, amOwnerOfGroup, base64urlDecode } from '../../lib/util';
export class LinksApp extends Component { export class LinksApp extends Component {
constructor(props) { constructor(props) {
super(props); super(props);
this.store = new Store(); this.store = new LinksStore();
this.store.setStateHandler(this.setState.bind(this)); this.store.setStateHandler(this.setState.bind(this));
this.state = this.store.state; this.state = this.store.state;

View File

@ -1,81 +0,0 @@
import InitialReducer from '../../reducers/initial';
import GroupUpdateReducer from '../../reducers/group-update';
import ContactUpdateReducer from '../../reducers/contact-update';
import PermissionUpdateReducer from '../../reducers/permission-update';
import MetadataReducer from '../../reducers/metadata-update';
import InviteUpdateReducer from '../../reducers/invite-update';
import LinkUpdateReducer from './reducers/link-update';
import ListenUpdateReducer from './reducers/listen-update';
import LocalReducer from '../../reducers/local';
export default class Store {
constructor() {
this.state = this.initialState();
this.initialReducer = new InitialReducer();
this.groupUpdateReducer = new GroupUpdateReducer();
this.contactUpdateReducer = new ContactUpdateReducer();
this.permissionUpdateReducer = new PermissionUpdateReducer();
this.metadataReducer = new MetadataReducer();
this.inviteUpdateReducer = new InviteUpdateReducer();
this.localReducer = new LocalReducer();
this.linkUpdateReducer = new LinkUpdateReducer();
this.listenUpdateReducer = new ListenUpdateReducer();
this.setState = () => {};
}
setStateHandler(setState) {
this.setState = setState;
}
initialState() {
return {
contacts: {},
groups: {},
associations: {
link: {},
contacts: {}
},
invites: {},
links: {},
listening: new Set(),
comments: {},
seen: {},
permissions: {},
sidebarShown: true
};
}
clear() {
this.handleEvent({
data: { clear: true }
});
}
handleEvent(data) {
let json;
if (data.data) {
json = data.data;
} else {
json = data;
}
if ('clear' in json && json.clear) {
this.setState(this.initialState());
return;
}
console.log('event', json);
this.initialReducer.reduce(json, this.state);
this.groupUpdateReducer.reduce(json, this.state);
this.contactUpdateReducer.reduce(json, this.state);
this.permissionUpdateReducer.reduce(json, this.state);
this.metadataReducer.reduce(json, this.state);
this.inviteUpdateReducer.reduce(json, this.state);
this.localReducer.reduce(json, this.state);
this.linkUpdateReducer.reduce(json, this.state);
this.listenUpdateReducer.reduce(json, this.state);
this.setState(this.state);
}
}

View File

@ -4,8 +4,8 @@ import _ from 'lodash';
import './css/custom.css'; import './css/custom.css';
import Api from './api'; import PublishApi from '../../api/publish';
import Store from './store'; import PublishStore from '../../store/publish';
import Subscription from './subscription'; import Subscription from './subscription';
import { Skeleton } from './components/skeleton'; import { Skeleton } from './components/skeleton';
@ -37,7 +37,7 @@ export default class PublishApp extends React.Component {
this.store.clear(); this.store.clear();
const channel = new this.props.channel(); const channel = new this.props.channel();
this.api = new Api(this.props.ship, channel, this.store); this.api = new PublishApi(this.props.ship, channel, this.store);
this.subscription = new Subscription(this.store, this.api, channel); this.subscription = new Subscription(this.store, this.api, channel);
this.subscription.start(); this.subscription.start();

View File

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

View File

@ -1,7 +0,0 @@
import _ from 'lodash';
export class InitialReducer {
reduce(json, state) {
state.notebooks = json.notebooks || null;
}
}

View File

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

View File

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

View File

@ -1,67 +0,0 @@
import { InitialReducer } from './reducers/initial';
import { PrimaryReducer } from './reducers/primary';
import { ResponseReducer } from './reducers/response';
import { GroupReducer } from './reducers/group';
import { InviteReducer } from './reducers/invite';
import { PermissionReducer } from './reducers/permission';
import MetadataReducer from '../../reducers/metadata-update';
export default class Store {
constructor() {
this.state = this.initialState();
this.initialReducer = new InitialReducer();
this.primaryReducer = new PrimaryReducer();
this.responseReducer = new ResponseReducer();
this.groupReducer = new GroupReducer();
this.inviteReducer = new InviteReducer();
this.permissionReducer = new PermissionReducer();
this.metadataReducer = new MetadataReducer();
this.setState = () => {};
}
initialState() {
return {
notebooks: {},
groups: {},
contacts: {},
associations: {
contacts: {}
},
permissions: {},
invites: {},
sidebarShown: true
};
}
clear() {
this.handleEvent({
data: { clear: true }
});
}
setStateHandler(setState) {
this.setState = setState;
}
handleEvent(evt) {
if (evt.data && 'clear' in evt.data && evt.data.clear) {
this.setState(this.initialState());
return;
}
if (evt.from && evt.from.path === '/all') {
this.groupReducer.reduce(evt.data, this.state);
this.permissionReducer.reduce(evt.data, this.state);
} else if (evt.from && evt.from.path === '/app-name/contacts') {
this.metadataReducer.reduce(evt.data, this.state);
} else if (evt.from && evt.from.path === '/primary') {
this.primaryReducer.reduce(evt.data, this.state);
this.inviteReducer.reduce(evt.data, this.state);
} else if (evt.type) {
this.responseReducer.reduce(evt, this.state);
}
this.setState(this.state);
}
}

View File

@ -1,60 +0,0 @@
import React, { Component } from 'react';
import classnames from 'classnames';
import _ from 'lodash';
export default class ChatTile extends Component {
render() {
const { props } = this;
let data = _.get(props.data, 'chat-configs', false);
let inviteNum = 0;
let msgNum = 0;
if (data) {
Object.keys(data).forEach((conf) => {
console.log(conf);
msgNum = msgNum + data[conf].length - data[conf].read;
});
}
let notificationsNum = inviteNum + msgNum;
let numNotificationsElem =
notificationsNum > 0 ? (
<p
className="absolute green2 white-d"
style={{
bottom: 6,
fontWeight: 400,
fontSize: 12,
lineHeight: "20px"
}}>
{notificationsNum > 99 ? "99+" : notificationsNum}
</p>
) : (
<div />
);
return (
<div className={"w-100 h-100 relative bg-white bg-gray0-d ba " +
"b--black b--gray1-d"}>
<a className="w-100 h-100 db pa2 no-underline" href="/~chat">
<p className="black white-d absolute f9" style={{left: 8, top: 8}}>Messaging</p>
<img
className="absolute invert-d"
style={{ left: 38, top: 38 }}
src="/~chat/img/Tile.png"
width={48}
height={48} />
{numNotificationsElem}
</a>
</div>
);
}
}
window['chat-viewTile'] = ChatTile;

View File

@ -1,58 +0,0 @@
import React, { Component } from 'react';
import classnames from 'classnames';
import _ from 'lodash';
export default class ContactTile extends Component {
render() {
const { props } = this;
let data = _.get(props.data, "invites", false);
let inviteNum = 0;
if (data && "/contacts" in data) {
inviteNum = Object.keys(data["/contacts"]).length;
}
let numNotificationsElem =
inviteNum > 0 ? (
<p
className="absolute green2 white-d"
style={{
bottom: 6,
fontWeight: 400,
fontSize: 12,
lineHeight: "20px"
}}>
{inviteNum > 99 ? "99+" : inviteNum}
</p>
) : (
<div />
);
return (
<div
className={
"w-100 h-100 relative bg-white bg-gray0-d " + "b--black b--gray1-d ba"
}>
<a className="w-100 h-100 db pa2 bn" href="/~groups">
<p className="black white-d absolute f9" style={{ left: 8, top: 8 }}>
Groups
</p>
<img
className="absolute invert-d"
style={{ left: 38, top: 38 }}
src="/~groups/img/Tile.png"
width={48}
height={48}
/>
{numNotificationsElem}
</a>
</div>
);
}
}
window['contact-viewTile'] = ContactTile;

View File

@ -1,92 +0,0 @@
const attemptPost = (endpoint, path, data) => {
console.log('sending', data, JSON.stringify(data));
return new Promise((resolve, reject) => {
fetch(`http://${endpoint}/~link${path}`, {
method: 'POST',
credentials: 'include',
body: JSON.stringify(data)
})
.then(response => {
console.log('resp', response.status);
resolve(response.status === 200);
})
.catch(error => {
console.error('post failed', error);
resolve(false);
});
});
}
const attemptGet = (endpoint, path, data) => {
return new Promise((resolve, reject) => {
fetch(`http://${endpoint}/~link{path}`, {
method: 'GET',
credentials: 'include',
body: JSON.stringify(data)
})
.then(response => {
console.log('get response');
console.log('response', response);
resolve(true);
})
.catch(error => {
console.log('fetch error', error);
resolve(false);
});
});
}
const saveUrl = (endpoint, title, url) => {
return attemptPost(endpoint, '/add/private', {title, url});
}
const openOptions = () => {
browser.tabs.create({
url: browser.runtime.getURL('options/index.html')
});
}
const openLogin = (endpoint) => {
browser.tabs.create({
url: `http://${endpoint}/~/login`
});
}
const doSave = async () => {
console.log('gonna do save!');
// if no endpoint, refer to options page
const endpoint = await getEndpoint();
console.log('endpoint', endpoint);
if (endpoint === null) {
return openOptions();
}
const tab = (await browser.tabs.query({currentWindow: true, active: true}))[0];
//TODO figure out if we're viewing urbit page, turn into arvo:// url?
const success = await saveUrl(endpoint, tab.title, tab.url);
console.log('success', success);
if (!success) {
console.log('failed, opening login');
openLogin(endpoint);
} else {
console.log('success!');
}
}
// perform save action when extension button is clicked
//TODO want to do a pop-up instead of on-click action here latern
//
browser.browserAction.onClicked.addListener(doSave);
// open settings page on-install, user will need to set endpoint
//
browser.runtime.onInstalled.addListener(async ({ reason, temporary }) => {
// if (temporary) return; // skip during development
switch (reason) {
case "install":
browser.runtime.openOptionsPage();
break;
}
});

View File

@ -1,11 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<link href="style.css" rel="stylesheet" />
</head>
<body>
<h1 id="myHeading">My browser action</h1>
<script src="script.js"></script>
</body>
</html>

View File

@ -1 +0,0 @@
console.log('script.js firing');

View File

@ -1,3 +0,0 @@
h1 {
font-style: italic;
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 979 B

View File

@ -1,39 +0,0 @@
{
"manifest_version": 2,
"name": "link",
"description": "Urbit Link",
"version": "0.0.0",
"icons": {
"64": "icons/icon.png"
},
"browser_action": {
"default_icon": {
"64": "icons/icon.png"
},
"todo__default_popup": "browserAction/index.html",
"default_title": "link"
},
"background": {
"scripts": [
"background.js",
"storage.js"
]
},
"options_ui": {
"page": "options/index.html"
},
"web_accessible_resources": [
"src/options/options.html"
],
"permissions": [
"storage", // storing config
"activeTab" // viewing current page url & title
],
"applications": {
"gecko": {
"id": "link-webext@tlon.io"
}
}
}

View File

@ -1,22 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<link href="style.css" rel="stylesheet" />
</head>
<body>
<form>
<label>
Ship HTTP endpoint:
<input id="endpoint" type="text" placeholder="your-ship.arvo.network" />
</label>
<button type="submit">Save</button>
</form>
<script src="../storage.js"></script>
<script src="script.js"></script>
</body>
</html>

View File

@ -1,21 +0,0 @@
function storeOptions(e) {
e.preventDefault();
// clean up endpoint address and store it
let endpoint = document.querySelector("#endpoint").value
.replace(/^.*:\/\//, '') // strip protocol
.replace(/\/+$/, ''); // strip trailing slashes
setEndpoint(endpoint);
}
async function restoreOptions() {
const endpoint = await getEndpoint();
console.log('prefilling with', endpoint);
document.querySelector("#endpoint").value = endpoint;
}
document.addEventListener("DOMContentLoaded", restoreOptions);
document.querySelector("form").addEventListener("submit", storeOptions);

View File

@ -1,3 +0,0 @@
h1 {
font-style: italic;
}

View File

@ -1,20 +0,0 @@
// use synced storage if supported, fall back to local
const storage = browser.storage.sync || browser.storage.local;
const setEndpoint = (endpoint) => {
return storage.set({endpoint});
}
const getEndpoint = () => {
return new Promise((resolve, reject) => {
storage.get("endpoint").then((res) => {
if (res && res.endpoint) {
resolve(res.endpoint);
} else {
resolve(null);
}
}, (err) => {
resolve(null);
});
});
}

View File

@ -1,48 +0,0 @@
import React, { Component } from 'react';
import classnames from 'classnames';
import _ from 'lodash';
export default class LinkTile extends Component {
render() {
const unseenCount = this.props.data.unseen || 0;
let displayUnseen = unseenCount <= 0
? null
: <p
className="absolute green2 white-d"
style={{
bottom: 6,
fontWeight: 400,
fontSize: 12,
lineHeight: "20px"
}}>
{unseenCount > 99 ? "99+" : unseenCount}
</p>;
return (
<div className={"w-100 h-100 relative ba b--black b--gray1-d " +
"bg-white bg-gray0-d"}>
<a className="w-100 h-100 db pa2 bn" href="/~link">
<p
className="f9 black white-d absolute"
style={{ left: 8, top: 8 }}>
Links
</p>
<img
className="absolute invert-d"
style={{ left: 38, top: 38 }}
src="/~link/img/Tile.png"
width={48}
height={48}
/>
{displayUnseen}
</a>
</div>
);
}
}
window['link-viewTile'] = LinkTile;

View File

@ -1,51 +0,0 @@
import React, { Component } from 'react'
import classnames from 'classnames';
export default class PublishTile extends Component {
constructor(props){
super(props);
console.log("publish-tile", this.props);
}
render(){
let notificationsNum = this.props.data.notifications;
if (notificationsNum === 0) {
notificationsNum = "";
}
else if (notificationsNum > 99) {
notificationsNum = "99+";
}
else if (isNaN(notificationsNum)) {
notificationsNum = "";
}
return (
<div className={"w-100 h-100 relative bg-white bg-gray0-d " +
"ba b--black b--gray1-d"}>
<a className="w-100 h-100 db no-underline" href="/~publish">
<p className="black white-d f9 absolute" style={{ left: 8, top: 8 }}>
Publishing
</p>
<img
className="absolute invert-d"
style={{ left: 38, top: 38 }}
src="/~publish/tile.png"
width={48}
height={48}
/>
<div
className="absolute w-100 flex-col f9"
style={{ verticalAlign: "bottom", bottom: 8, left: 8 }}>
<span className="green2 white-d">{notificationsNum}</span>
</div>
</a>
</div>
);
}
}
window.publishTile = PublishTile;

View File

@ -1,33 +0,0 @@
import React, { Component } from 'react';
import classnames from 'classnames';
import _ from 'lodash';
export default class sotoTile extends Component {
render() {
return (
<div className={"w-100 h-100 relative bg-black bg-gray0-d " +
"ba b--black b--gray1-d"}>
<a className="w-100 h-100 db bn" href="/~dojo">
<p className="white f9 absolute"
style={{ left: 8, top: 8 }}>
Dojo
</p>
<img src="~dojo/img/Tile.png"
className="absolute"
style={{
left: 38,
top: 38,
height: 48,
width: 48
}}
/>
</a>
</div>
);
}
}
window.sotoTile = sotoTile;

View File

@ -1,9 +1,10 @@
import _ from 'lodash'; import _ from 'lodash';
export default class ChatUpdateReducer { export default class ChatReducer {
reduce(json, state) { reduce(json, state) {
const data = _.get(json, 'chat-update', false); let data = _.get(json, 'chat-update', false);
if (data) { if (data) {
this.initial(data, state);
this.pending(data, state); this.pending(data, state);
this.message(data, state); this.message(data, state);
this.messages(data, state); this.messages(data, state);
@ -11,6 +12,23 @@ export default class ChatUpdateReducer {
this.create(data, state); this.create(data, state);
this.delete(data, state); this.delete(data, state);
} }
data = _.get(json, 'chat-hook-update', false);
if (data) {
this.hook(data, state);
}
}
initial(json, state) {
const data = _.get(json, 'initial', false);
if (data) {
state.inbox = data;
state.chatInitialized = true;
}
}
hook(json, state) {
state.chatSynced = data;
} }
message(json, state) { message(json, state) {

View File

@ -1,9 +1,10 @@
import _ from 'lodash'; import _ from 'lodash';
export default class ContactUpdateReducer { export default class ContactReducer {
reduce(json, state) { reduce(json, state) {
const data = _.get(json, 'contact-update', false); const data = _.get(json, 'contact-update', false);
if (data) { if (data) {
this.initial(data, state);
this.create(data, state); this.create(data, state);
this.delete(data, state); this.delete(data, state);
this.add(data, state); this.add(data, state);
@ -12,6 +13,13 @@ export default class ContactUpdateReducer {
} }
} }
initial(json, state) {
const data = _.get(json, 'initial', false);
if (data) {
state.contacts = data;
}
}
create(json, state) { create(json, state) {
const data = _.get(json, 'create', false); const data = _.get(json, 'create', false);
if (data) { if (data) {

View File

@ -1,9 +1,11 @@
import _ from 'lodash'; import _ from 'lodash';
export default class GroupUpdateReducer { export default class GroupReducer {
reduce(json, state) { reduce(json, state) {
const data = _.get(json, 'group-update', false); const data = _.get(json, "group-update", false);
if (data) { if (data) {
this.initial(data, state);
this.add(data, state); this.add(data, state);
this.remove(data, state); this.remove(data, state);
this.bundle(data, state); this.bundle(data, state);
@ -13,6 +15,15 @@ export default class GroupUpdateReducer {
} }
} }
initial(json, state) {
const data = _.get(json, 'initial', false);
if (data) {
for (let group in data) {
state.groups[group] = new Set(data[group]);
}
}
}
add(json, state) { add(json, state) {
const data = _.get(json, 'add', false); const data = _.get(json, 'add', false);
if (data) { if (data) {

View File

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

View File

@ -1,9 +1,10 @@
import _ from 'lodash'; import _ from 'lodash';
export default class InviteUpdateReducer { export default class InviteReducer {
reduce(json, state) { reduce(json, state) {
const data = _.get(json, 'invite-update', false); const data = _.get(json, 'invite-update', false);
if (data) { if (data) {
this.initial(data, state);
this.create(data, state); this.create(data, state);
this.delete(data, state); this.delete(data, state);
this.invite(data, state); this.invite(data, state);
@ -12,6 +13,13 @@ export default class InviteUpdateReducer {
} }
} }
initial(json, state) {
const data = _.get(json, 'initial', false);
if (data) {
state.invites = data;
}
}
create(json, state) { create(json, state) {
const data = _.get(json, 'create', false); const data = _.get(json, 'create', false);
if (data) { if (data) {

View File

@ -2,7 +2,7 @@ import _ from 'lodash';
export default class MetadataReducer { export default class MetadataReducer {
reduce(json, state) { reduce(json, state) {
const data = _.get(json, 'metadata-update', false); let data = _.get(json, 'metadata-update', false);
if (data) { if (data) {
this.associations(data, state); this.associations(data, state);
this.add(data, state); this.add(data, state);
@ -12,54 +12,60 @@ export default class MetadataReducer {
} }
associations(json, state) { associations(json, state) {
const data = _.get(json, 'associations', false); let data = _.get(json, 'associations', false);
if (data) { if (data) {
const metadata = state.associations; let metadata = {};
Object.keys(data).map((channel) => { Object.keys(data).forEach((key) => {
const channelObj = data[channel]; let val = data[key];
const app = data[channel]['app-name']; let groupPath = val['group-path'];
if (!(app in metadata)) { if (!(groupPath in metadata)) {
metadata[app] = {}; metadata[groupPath] = {};
} }
metadata[app][channelObj['app-path']] = channelObj; metadata[groupPath][key] = val;
}); });
state.associations = metadata; state.associations = metadata;
} }
} }
add(json, state) { add(json, state) {
const data = _.get(json, 'add', false); let data = _.get(json, 'add', false);
if (data) { if (data) {
const metadata = state.associations; let metadata = state.associations;
const app = data['app-name']; if (!(data['group-path'] in metadata)) {
if (!(app in metadata)) { metadata[data['group-path']] = {};
metadata[app] = {};
} }
metadata[app][data['app-path']] = data; metadata[data['group-path']]
[`${data["group-path"]}/${data["app-name"]}${data["app-path"]}`] = data;
state.associations = metadata; state.associations = metadata;
} }
} }
update(json, state) { update(json, state) {
const data = _.get(json, 'update-metadata', false); let data = _.get(json, 'update-metadata', false);
if (data) { if (data) {
const metadata = state.associations; let metadata = state.associations;
const app = data['app-name']; if (!(data["group-path"] in metadata)) {
metadata[app][data['app-path']] = data; metadata[data["group-path"]] = {};
}
metadata[data["group-path"]][
`${data["group-path"]}/${data["app-name"]}${data["app-path"]}`
] = data;
state.associations = metadata; state.associations = metadata;
} }
} }
remove(json, state) { remove(json, state) {
const data = _.get(json, 'remove', false); let data = _.get(json, 'remove', false);
if (data) { if (data) {
const metadata = state.associations; let metadata = state.associations;
const app = data['app-name']; if (data['group-path'] in metadata) {
if (!(app in metadata)) { let path =
return false; `${data['group-path']}/${data['app-name']}${data['app-path']}`
delete metadata[data["group-path"]][path];
state.associations = metadata;
} }
delete metadata[app][data['app-path']];
state.associations = metadata;
} }
} }
} }

View File

@ -1,9 +1,10 @@
import _ from 'lodash'; import _ from 'lodash';
export default class PermissionUpdateReducer { export default class PermissionReducer {
reduce(json, state) { reduce(json, state) {
const data = _.get(json, 'permission-update', false); const data = _.get(json, 'permission-update', false);
if (data) { if (data) {
this.initial(data, state);
this.create(data, state); this.create(data, state);
this.delete(data, state); this.delete(data, state);
this.add(data, state); this.add(data, state);
@ -11,6 +12,18 @@ export default class PermissionUpdateReducer {
} }
} }
initial(json, state) {
const data = _.get(json, 'initial', false);
if (data) {
for (const perm in data) {
state.permissions[perm] = {
who: new Set(data[perm].who),
kind: data[perm].kind
};
}
}
}
create(json, state) { create(json, state) {
const data = _.get(json, 'create', false); const data = _.get(json, 'create', false);
if (data) { if (data) {

View File

@ -1,26 +1,24 @@
import _ from 'lodash'; import _ from 'lodash';
export class ResponseReducer { export default class PublishResponseReducer {
reduce(json, state) { reduce(json, state) {
switch(json.type) { const data = _.get(json, 'publish-response', false);
if (!data) { return; }
switch(data.type) {
case "notebooks": case "notebooks":
this.handleNotebooks(json, state); this.handleNotebooks(data, state);
break; break;
case "notebook": case "notebook":
this.handleNotebook(json, state); this.handleNotebook(data, state);
break; break;
case "note": case "note":
this.handleNote(json, state); this.handleNote(data, state);
break; break;
case "notes-page": case "notes-page":
this.handleNotesPage(json, state); this.handleNotesPage(data, state);
break; break;
case "comments-page": case "comments-page":
this.handleCommentsPage(json, state); this.handleCommentsPage(data, state);
break;
case "local":
this.sidebarToggle(json, state);
this.setSelected(json, state);
break; break;
default: default:
break; break;
@ -204,11 +202,4 @@ export class ResponseReducer {
} }
} }
setSelected(json, state) {
let data = _.has(json.data, 'selected', false);
if (data) {
state.selectedGroups = json.data.selected;
}
}
} }

View File

@ -1,9 +1,9 @@
import _ from 'lodash'; import _ from 'lodash';
export class PrimaryReducer { export default class PublishUpdateReducer {
reduce(json, state){ reduce(preJson, state){
let json = _.get(preJson, "publish-update", false);
switch(Object.keys(json)[0]){ switch(Object.keys(json)[0]){
//publish actions
case "add-book": case "add-book":
this.addBook(json["add-book"], state); this.addBook(json["add-book"], state);
break; break;
@ -34,12 +34,6 @@ export class PrimaryReducer {
case "read": case "read":
this.read(json["read"], state); this.read(json["read"], state);
break; break;
// contacts actions
case "contact-initial":
this.contactInitial(json["contact-initial"], state);
break;
case "contact-update":
this.contactUpdate(json["contact-update"], state);
default: default:
break; break;
} }
@ -260,63 +254,4 @@ export class PrimaryReducer {
} }
} }
contactInitial(json, state) {
state.contacts = json;
}
contactUpdate(json, state) {
this.createContact(json, state);
this.deleteContact(json, state);
this.addContact(json, state);
this.removeContact(json, state);
this.editContact(json, state);
}
createContact(json, state) {
let data = _.get(json, "create", false);
if (data) {
state.contacts[data.path] = {};
}
}
deleteContact(json, state) {
let data = _.get(json, "delete", false);
if (data) {
delete state.contacts[data.path];
}
}
addContact(json, state) {
let data = _.get(json, "add", false);
if (data && data.path in state.contacts) {
state.contacts[data.path][data.ship] = data.contact;
}
}
removeContact(json, state) {
let data = _.get(json, "remove", false);
if (
data &&
data.path in state.contacts &&
data.ship in state.contacts[data.path]
) {
delete state.contacts[data.path][data.ship];
}
}
editContact(json, state) {
let data = _.get(json, "edit", false);
if (
data &&
data.path in state.contacts &&
data.ship in state.contacts[data.path]
) {
let edit = Object.keys(data["edit-field"]);
if (edit.length !== 1) {
return;
}
state.contacts[data.path][data.ship][edit[0]] =
data["edit-field"][edit[0]];
}
}
} }

View File

@ -1,6 +1,6 @@
import _ from 'lodash'; import _ from 'lodash';
export default class S3Reducer { export default class S3Reducer{
reduce(json, state) { reduce(json, state) {
const data = _.get(json, 's3-update', false); const data = _.get(json, 's3-update', false);
if (data) { if (data) {

View File

@ -1,4 +1,3 @@
import InitialReducer from './reducers/initial';
import InviteUpdateReducer from './reducers/invite-update'; import InviteUpdateReducer from './reducers/invite-update';
import MetadataReducer from './reducers/metadata-update'; import MetadataReducer from './reducers/metadata-update';
import LocalReducer from './reducers/local'; import LocalReducer from './reducers/local';
@ -27,7 +26,6 @@ export default class Store {
handleEvent(data) { handleEvent(data) {
let json = data.data; let json = data.data;
this.initialReducer.reduce(json, this.state);
this.inviteUpdateReducer.reduce(json, this.state); this.inviteUpdateReducer.reduce(json, this.state);
this.metadataReducer.reduce(json, this.state); this.metadataReducer.reduce(json, this.state);
this.localReducer.reduce(json, this.state); this.localReducer.reduce(json, this.state);

View File

@ -0,0 +1,51 @@
import ContactReducer from '../reducers/contact-update';
import ChatReducer from '../reducers/chat-update';
import InviteReducer from '../reducers/invite-update';
import PermissionReducer from '../reducers/permission-update';
import MetadataReducer from '../reducers/metadata-update';
import S3Reducer from '../reducers/s3-update';
import LocalReducer from '../reducers/local';
import BaseStore from './base';
export default class ChatStore extends BaseStore {
constructor() {
super();
this.permissionReducer = new PermissionReducer();
this.contactReducer = new ContactReducer();
this.chatReducer = new ChatReducer();
this.inviteReducer = new InviteReducer();
this.s3Reducer = new S3Reducer();
this.metadataReducer = new MetadataReducer();
this.localReducer = new LocalReducer();
}
initialState() {
return {
inbox: {},
chatSynced: null,
contacts: {},
permissions: {},
invites: {},
associations: {
chat: {},
contacts: {}
},
sidebarShown: true,
pendingMessages: new Map([]),
chatInitialized: false,
s3: {}
};
}
reduce(data, state) {
this.permissionReducer.reduce(data, this.state);
this.contactReducer.reduce(data, this.state);
this.chatReducer.reduce(data, this.state);
this.inviteReducer.reduce(data, this.state);
this.metadataReducer.reduce(data, this.state);
this.localReducer.reduce(data, this.state);
this.s3Reducer.reduce(data, this.state);
}
}

View File

@ -0,0 +1,45 @@
import ContactReducer from '../reducers/contact-update';
import GroupReducer from '../reducers/group-update';
import InviteReducer from '../reducers/invite-update';
import PermissionReducer from '../reducers/permission-update';
import MetadataReducer from '../reducers/metadata-update';
import LocalReducer from '../reducers/local';
import S3Reducer from '../reducers/s3-update';
import BaseStore from './base';
export default class GroupsStore extends BaseStore {
constructor() {
super();
this.groupReducer = new GroupReducer();
this.permissionReducer = new PermissionReducer();
this.contactReducer = new ContactReducer();
this.inviteReducer = new InviteReducer();
this.metadataReducer = new MetadataReducer();
this.s3Reducer = new S3Reducer();
this.localReducer = new LocalReducer();
}
initialState() {
return {
contacts: {},
groups: {},
associations: {},
permissions: {},
invites: {},
s3: {}
};
}
reduce(data, state) {
this.groupReducer.reduce(json, this.state);
this.permissionReducer.reduce(json, this.state);
this.contactReducer.reduce(json, this.state);
this.inviteReducer.reduce(json, this.state);
this.metadataReducer.reduce(json, this.state);
this.s3Reducer.reduce(json, this.state);
this.localReducer.reduce(json, this.state);
}
}

View File

@ -0,0 +1,55 @@
import GroupReducer from '../reducers/group-update';
import ContactReducer from '../reducers/contact-update';
import PermissionReducer from '../reducers/permission-update';
import MetadataReducer from '../reducers/metadata-update';
import InviteReducer from '../reducers/invite-update';
import LinkReducer from '../reducers/link-update';
import ListenReducer from '../reducers/listen-update';
import LocalReducer from '../reducers/local';
import BaseStore from './base';
export default class LinksStore extends BaseStore {
constructor() {
super();
this.groupReducer = new GroupReducer();
this.contactReducer = new ContactReducer();
this.permissionReducer = new PermissionReducer();
this.metadataReducer = new MetadataReducer();
this.inviteReducer = new InviteReducer();
this.localReducer = new LocalReducer();
this.linkReducer = new LinkReducer();
this.listenReducer = new ListenReducer();
}
initialState() {
return {
contacts: {},
groups: {},
associations: {
link: {},
contacts: {}
},
invites: {},
links: {},
listening: new Set(),
comments: {},
seen: {},
permissions: {},
sidebarShown: true
};
}
reduce(data, state) {
this.groupReducer.reduce(json, this.state);
this.contactReducer.reduce(json, this.state);
this.permissionReducer.reduce(json, this.state);
this.metadataReducer.reduce(json, this.state);
this.inviteReducer.reduce(json, this.state);
this.localReducer.reduce(json, this.state);
this.linkReducer.reduce(json, this.state);
this.listenReducer.reduce(json, this.state);
}
}

View File

@ -0,0 +1,45 @@
import BaseStore from './base';
import GroupReducer from '../reducers/group-update';
import PublishReducer from '../reducers/publish-update';
import InviteReducer from '../reducers/invite-update';
import PublishResponseReducer from '../reducers/publish-response';
import PermissionReducer from '../reducers/permission-update';
import MetadataReducer from '../reducers/metadata-update';
export default class PublishStore extends BaseStore {
constructor() {
super();
this.groupReducer = new GroupReducer();
this.publishReducer = new PublishReducer();
this.inviteReducer = new InviteReducer();
this.responseReducer = new PublishResponseReducer();
this.permissionReducer = new PermissionReducer();
this.metadataReducer = new MetadataReducer();
}
initialState() {
return {
notebooks: {},
groups: {},
contacts: {},
associations: {
contacts: {}
},
permissions: {},
invites: {},
sidebarShown: true
};
}
reduce(data, state) {
this.groupReducer.reduce(data, this.state);
this.publishReducer.reduce(data, this.state);
this.permissionReducer.reduce(data, this.state);
this.metadataReducer.reduce(data, this.state);
this.inviteReducer.reduce(data, this.state);
this.responseReducer.reduce(data, this.state);
}
}