Merge pull request #3038 from urbit/lf/ts-global-store

interface: global (typed) store
This commit is contained in:
matildepark 2020-06-23 21:18:31 -04:00 committed by GitHub
commit 32b045cd5e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
102 changed files with 2635 additions and 1459 deletions

View File

@ -453,6 +453,7 @@
[%give %kick ~ ~]~
=; =json
[%give %fact ~ %json !>(json)]
%+ frond:enjs:format 'link-update'
%+ frond:enjs:format 'initial-submissions'
%- pairs:enjs:format
%+ turn
@ -514,6 +515,7 @@
:_ [%give %kick ~ ~]~
=; =json
[%give %fact ~ %json !>(json)]
%+ frond:enjs:format 'link-update'
%+ frond:enjs:format 'submission'
^- json
=; sub=(unit submission)
@ -538,6 +540,7 @@
[%give %kick ~ ~]~
=; =json
[%give %fact ~ %json !>(json)]
%+ frond:enjs:format 'link-update'
%+ frond:enjs:format 'initial-discussions'
%^ page-to-json p
%+ get-paginated `p
@ -552,6 +555,7 @@
?+ -.update ~|([dap.bowl %unexpected-update -.update] !!)
%submissions
%+ give-json
%+ frond:enjs:format 'link-update'
(update:enjs:store update)
:~ /json/0/submissions
(weld /json/0/submissions path.update)
@ -559,6 +563,7 @@
::
%discussions
%+ give-json
%+ frond:enjs:format 'link-update'
(update:enjs:store update)
:_ ~
%+ weld /json/0/discussions
@ -566,6 +571,7 @@
::
%observation
%+ give-json
%+ frond:enjs:format 'link-update'
(update:enjs:store update)
~[/json/seen]
==

View File

@ -46,11 +46,11 @@ module.exports = {
module: {
rules: [
{
test: /\.js?$/,
test: /\.(j|t)sx?$/,
use: {
loader: 'babel-loader',
options: {
presets: ['@babel/preset-env', '@babel/preset-react'],
presets: ['@babel/preset-env', '@babel/typescript', '@babel/preset-react'],
plugins: [
'@babel/plugin-proposal-object-rest-spread',
'@babel/plugin-proposal-optional-chaining',
@ -74,7 +74,7 @@ module.exports = {
]
},
resolve: {
extensions: ['.js']
extensions: ['.js', '.ts', '.tsx']
},
devtool: 'inline-source-map',
// devServer: {

View File

@ -15,7 +15,7 @@ module.exports = {
use: {
loader: 'babel-loader',
options: {
presets: ['@babel/preset-env', '@babel/preset-react'],
presets: ['@babel/preset-env', '@babel/typescript', '@babel/preset-react'],
plugins: [
'@babel/plugin-proposal-object-rest-spread',
'@babel/plugin-proposal-optional-chaining',

Binary file not shown.

View File

@ -34,6 +34,10 @@
"@babel/plugin-proposal-optional-chaining": "^7.9.0",
"@babel/preset-env": "^7.9.5",
"@babel/preset-react": "^7.9.4",
"@babel/preset-typescript": "^7.10.1",
"@types/lodash": "^4.14.155",
"@types/react": "^16.9.38",
"@types/react-router-dom": "^5.1.5",
"babel-eslint": "^10.1.0",
"babel-loader": "^8.1.0",
"clean-webpack-plugin": "^3.0.0",
@ -51,6 +55,8 @@
"scripts": {
"lint": "eslint ./**/*.js",
"lint-file": "eslint",
"tsc": "tsc",
"tsc:watch": "tsc --watch",
"build:dev": "cross-env NODE_ENV=development webpack --config config/webpack.dev.js",
"build:prod": "cross-env NODE_ENV=production webpack --config config/webpack.prod.js",
"start": "webpack-dev-server",

View File

@ -15,7 +15,7 @@ import PublishApp from './apps/publish/app';
import StatusBar from './components/StatusBar';
import NotFound from './components/404';
import GlobalStore from './store/global';
import GlobalStore from './store/store';
import GlobalSubscription from './subscription/global';
import GlobalApi from './api/global';
@ -55,11 +55,11 @@ export default class App extends React.Component {
this.appChannel = new window.channel();
this.api = new GlobalApi(this.ship, this.appChannel, this.store);
this.subscription =
new GlobalSubscription(this.store, this.api, this.appChannel);
}
componentDidMount() {
this.subscription =
new GlobalSubscription(this.store, this.api, this.appChannel);
this.subscription.start();
}
@ -68,6 +68,7 @@ export default class App extends React.Component {
const associations = this.state.associations ? this.state.associations : { contacts: {} };
const selectedGroups = this.state.selectedGroups ? this.state.selectedGroups : [];
const { state } = this;
return (
<ThemeProvider theme={light}>
@ -84,8 +85,8 @@ export default class App extends React.Component {
render={ p => (
<LaunchApp
ship={this.ship}
channel={channel}
selectedGroups={selectedGroups}
api={this.api}
{...state}
{...p}
/>
)}
@ -93,8 +94,9 @@ export default class App extends React.Component {
<Route path="/~chat" render={ p => (
<ChatApp
ship={this.ship}
channel={channel}
selectedGroups={selectedGroups}
api={this.api}
subscription={this.subscription}
{...state}
{...p}
/>
)}
@ -104,6 +106,7 @@ export default class App extends React.Component {
ship={this.ship}
channel={channel}
selectedGroups={selectedGroups}
subscription={this.subscription}
{...p}
/>
)}
@ -111,8 +114,9 @@ export default class App extends React.Component {
<Route path="/~groups" render={ p => (
<GroupsApp
ship={this.ship}
channel={channel}
selectedGroups={selectedGroups}
api={this.api}
subscription={this.subscription}
{...state}
{...p}
/>
)}
@ -120,8 +124,10 @@ export default class App extends React.Component {
<Route path="/~link" render={ p => (
<LinksApp
ship={this.ship}
channel={channel}
selectedGroups={selectedGroups}
ship={this.ship}
api={this.api}
subscription={this.subscription}
{...state}
{...p}
/>
)}
@ -129,8 +135,9 @@ export default class App extends React.Component {
<Route path="/~publish" render={ p => (
<PublishApp
ship={this.ship}
channel={channel}
selectedGroups={selectedGroups}
api={this.api}
subscription={this.subscription}
{...state}
{...p}
/>
)}

View File

@ -1,46 +0,0 @@
import _ from 'lodash';
import { uuid } from '../lib/util';
export default class BaseApi {
constructor(ship, channel, store) {
this.ship = ship;
this.channel = channel;
this.store = store;
this.bindPaths = [];
}
subscribe(path, method, ship = this.ship, app, success, fail, quit) {
this.bindPaths = _.uniq([...this.bindPaths, path]);
window.subscriptionId = this.channel.subscribe(ship, app, path,
(err) => {
fail(err);
},
(event) => {
success({
data: event,
from: {
ship,
path
}
});
},
(qui) => {
quit(qui);
});
}
action(appl, mark, data) {
return new Promise((resolve, reject) => {
this.channel.poke(window.ship, appl, mark, data,
(json) => {
resolve(json);
},
(err) => {
reject(err);
});
});
}
}

View File

@ -0,0 +1,56 @@
import _ from "lodash";
import { uuid } from "../lib/util";
import { Patp, Path } from "../types/noun";
import BaseStore from '../store/base';
export default class BaseApi<S extends object = {}> {
bindPaths: Path[] = [];
constructor(public ship: Patp, public channel: any, public store: BaseStore<S>) {}
unsubscribe(id: number) {
this.channel.unsubscribe(id);
}
subscribe(path: Path, method, ship = this.ship, app: string, success, fail, quit) {
this.bindPaths = _.uniq([...this.bindPaths, path]);
return this.channel.subscribe(
this.ship,
app,
path,
(err) => {
fail(err);
},
(event) => {
success({
data: event,
from: {
ship,
path,
},
});
},
(qui) => {
quit(qui);
}
);
}
action(appl: string, mark: string, data: any): Promise<any> {
return new Promise((resolve, reject) => {
this.channel.poke(
(window as any).ship,
appl,
mark,
data,
(json) => {
resolve(json);
},
(err) => {
reject(err);
}
);
});
}
}

View File

@ -1,222 +0,0 @@
import BaseApi from './base';
import { uuid } from '../lib/util';
export default class ChatApi {
constructor(ship, channel, store) {
const helper = new PrivateHelper(ship, channel, store);
this.ship = ship;
this.subscribe = helper.subscribe.bind(helper);
this.groups = {
add: helper.groupAdd.bind(helper),
remove: helper.groupRemove.bind(helper)
};
this.chat = {
message: helper.chatMessage.bind(helper),
read: helper.chatRead.bind(helper)
};
this.chatView = {
create: helper.chatViewCreate.bind(helper),
delete: helper.chatViewDelete.bind(helper),
join: helper.chatViewJoin.bind(helper),
groupify: helper.chatViewGroupify.bind(helper)
};
this.chatHook = {
addSynced: helper.chatHookAddSynced.bind(helper)
};
this.invite = {
accept: helper.inviteAccept.bind(helper),
decline: helper.inviteDecline.bind(helper)
};
this.metadata = {
add: helper.metadataAdd.bind(helper)
};
this.sidebarToggle = helper.sidebarToggle.bind(helper);
}
}
class PrivateHelper extends BaseApi {
groupsAction(data) {
return this.action('group-store', 'group-action', data);
}
groupAdd(members, path) {
return this.groupsAction({
add: {
members, path
}
});
}
groupRemove(members, path) {
this.groupsAction({
remove: {
members, path
}
});
}
chatAction(data) {
this.action('chat-store', 'json', data);
}
addPendingMessage(msg) {
if (this.store.state.pendingMessages.has(msg.path)) {
this.store.state.pendingMessages.get(msg.path).unshift(msg.envelope);
} else {
this.store.state.pendingMessages.set(msg.path, [msg.envelope]);
}
this.store.setState({
pendingMessages: this.store.state.pendingMessages
});
}
chatMessage(path, author, when, letter) {
const data = {
message: {
path,
envelope: {
uid: uuid(),
number: 0,
author,
when,
letter
}
}
};
this.action('chat-hook', 'json', data).then(() => {
this.chatRead(path);
});
data.message.envelope.author = data.message.envelope.author.substr(1);
this.addPendingMessage(data.message);
}
chatRead(path, read) {
this.chatAction({ read: { path } });
}
chatHookAddSynced(ship, path, askHistory) {
return this.action('chat-hook', 'chat-hook-action', {
'add-synced': {
ship,
path,
'ask-history': askHistory
}
});
}
chatViewAction(data) {
return this.action('chat-view', 'json', data);
}
chatViewCreate(
title, description, appPath, groupPath,
security, members, allowHistory
) {
return this.chatViewAction({
create: {
title,
description,
'app-path': appPath,
'group-path': groupPath,
security,
members,
'allow-history': allowHistory
}
});
}
chatViewDelete(path) {
this.chatViewAction({ delete: { 'app-path': path } });
}
chatViewJoin(ship, path, askHistory) {
this.chatViewAction({
join: {
ship,
'app-path': path,
'ask-history': askHistory
}
});
}
chatViewGroupify(path, group = null, inclusive = false) {
const action = { groupify: { 'app-path': path, existing: null } };
if (group) {
action.groupify.existing = {
'group-path': group,
inclusive: inclusive
};
}
return this.chatViewAction(action);
}
inviteAction(data) {
this.action('invite-store', 'json', data);
}
inviteAccept(uid) {
this.inviteAction({
accept: {
path: '/chat',
uid
}
});
}
inviteDecline(uid) {
this.inviteAction({
decline: {
path: '/chat',
uid
}
});
}
metadataAction(data) {
return this.action('metadata-hook', 'metadata-action', data);
}
metadataAdd(appPath, groupPath, title, description, dateCreated, color) {
const creator = `~${window.ship}`;
return this.metadataAction({
add: {
'group-path': groupPath,
resource: {
'app-path': appPath,
'app-name': 'chat'
},
metadata: {
title,
description,
color,
'date-created': dateCreated,
creator
}
}
});
}
sidebarToggle() {
let sidebarBoolean = true;
if (this.store.state.sidebarShown === true) {
sidebarBoolean = false;
}
this.store.handleEvent({
data: {
local: {
sidebarToggle: sidebarBoolean
}
}
});
}
}

View File

@ -0,0 +1,159 @@
import BaseApi from './base';
import { uuid } from '../lib/util';
import { Letter, ChatAction, Envelope } from '../types/chat-update';
import { Patp, Path, PatpNoSig } from '../types/noun';
import { StoreState } from '../store/type';
import BaseStore from '../store/base';
export default class ChatApi extends BaseApi<StoreState> {
/**
* Fetch backlog
*/
fetchMessages(start: number, end: number, path: Path) {
fetch(`/chat-view/paginate/${start}/${end}${path}`)
.then(response => response.json())
.then((json) => {
this.store.handleEvent({
data: json
});
});
}
/**
* Send a message to the chat at path
*/
message(path: Path, author: Patp, when: string, letter: Letter): Promise<void> {
const data: ChatAction = {
message: {
path,
envelope: {
uid: uuid(),
number: 0,
author,
when,
letter
}
}
};
const promise = this.proxyHookAction(data).then(() => {
this.read(path);
});
data.message.envelope.author = data.message.envelope.author.substr(1);
this.addPendingMessage(data.message.path, data.message.envelope);
return promise;
}
/**
* Mark chat as read
*/
read(path: Path): Promise<any> {
return this.storeAction({ read: { path } });
}
/**
* Create a chat and setup metadata
*/
create(
title: string, description: string, appPath: string, groupPath: string,
security: any, members: PatpNoSig[], allowHistory: boolean
): Promise<any> {
return this.viewAction({
create: {
title,
description,
'app-path': appPath,
'group-path': groupPath,
security,
members,
'allow-history': allowHistory
}
});
}
/**
* Deletes a chat
*
* If we don't host the chat, then it just leaves
*/
delete(path: Path) {
this.viewAction({ delete: { 'app-path': path } });
}
/**
* Join a chat
*/
join(ship: Patp, path: Path, askHistory: boolean): Promise<any> {
return this.viewAction({
join: {
ship,
'app-path': path,
'ask-history': askHistory
}
});
}
/**
* Groupify a chat that we host
*
* Will delete the old chat, recreate it based on a proper group,
* and invite the current whitelist to that group.
* existing messages get moved over.
*
* :existing is provided, associates chat with that group instead
* creating a new one. :inclusive indicates whether or not to add
* chat members to the group, if they aren't there already.
*/
groupify(path: Path, group: Path | null = null, inclusive = false) {
let action: any = { groupify: { 'app-path': path, existing: null } };
if (group) {
action.groupify.existing = {
'group-path': group,
inclusive: inclusive
};
}
return this.viewAction(action);
}
/**
* Begin syncing a chat from the host
*/
addSynced(ship: Patp, path: Path, askHistory: boolean): Promise<any> {
return this.action('chat-hook', 'chat-hook-action', {
'add-synced': {
ship,
path,
'ask-history': askHistory
}
});
}
private storeAction(action: ChatAction): Promise<any> {
return this.action('chat-store', 'json', action)
}
private proxyHookAction(action: ChatAction): Promise<any> {
return this.action('chat-hook', 'json', action);
}
private viewAction(action: unknown): Promise<any> {
return this.action('chat-view', 'json', action);
}
private addPendingMessage(path: Path, envelope: Envelope) {
const pending = this.store.state.pendingMessages.get(path);
if (pending) {
pending.unshift(envelope);
} else {
this.store.state.pendingMessages.set(path, [envelope]);
}
this.store.setState({
pendingMessages: this.store.state.pendingMessages
});
}
}

View File

@ -0,0 +1,62 @@
import BaseApi from './base';
import { StoreState } from '../store/type';
import { Patp, Path } from '../types/noun';
import { Contact, ContactEdit } from '../types/contact-update';
export default class ContactsApi extends BaseApi<StoreState> {
create(path: Path, ships: Patp[] = [], title: string, description: string) {
return this.viewAction({
create: {
path,
ships,
title,
description
}
});
}
share(recipient: Patp, path: Patp, ship: Patp, contact: Contact) {
return this.viewAction({
share: {
recipient, path, ship, contact
}
});
}
delete(path: Path) {
return this.viewAction({ delete: { path } });
}
remove(path: Path, ship: Patp) {
return this.viewAction({ remove: { path, ship } });
}
edit(path: Path, ship: Patp, editField: ContactEdit) {
/* editField can be...
{nickname: ''}
{email: ''}
{phone: ''}
{website: ''}
{notes: ''}
{color: 'fff'} // with no 0x prefix
{avatar: null}
{avatar: {url: ''}}
*/
return this.hookAction({
edit: {
path, ship, 'edit-field': editField
}
});
}
private hookAction(data) {
return this.action('contact-hook', 'contact-action', data);
}
private viewAction(data) {
return this.action('contact-view', 'json', data);
}
}

View File

@ -1,26 +0,0 @@
import BaseApi from './base';
class PrivateHelper extends BaseApi {
setSelected(selected) {
this.store.handleEvent({
data: {
local: {
selected: selected
}
}
});
}
}
export default class GlobalApi {
constructor(ship, channel, store) {
const helper = new PrivateHelper(ship, channel, store);
this.ship = ship;
this.subscribe = helper.subscribe.bind(helper);
this.setSelected = helper.setSelected.bind(helper);
}
}

View File

@ -0,0 +1,31 @@
import { Patp } from '../types/noun';
import BaseApi from './base';
import ChatApi from './chat';
import { StoreState } from '../store/type';
import GlobalStore from '../store/store';
import LocalApi from './local';
import InviteApi from './invite';
import MetadataApi from './metadata';
import ContactsApi from './contacts';
import GroupsApi from './groups';
import LaunchApi from './launch';
import LinksApi from './links';
import PublishApi from './publish';
export default class GlobalApi extends BaseApi<StoreState> {
chat = new ChatApi(this.ship, this.channel, this.store);
local = new LocalApi(this.ship, this.channel, this.store);
invite = new InviteApi(this.ship, this.channel, this.store);
metadata = new MetadataApi(this.ship, this.channel, this.store);
contacts = new ContactsApi(this.ship, this.channel, this.store);
groups = new GroupsApi(this.ship, this.channel, this.store);
launch = new LaunchApi(this.ship, this.channel, this.store);
links = new LinksApi(this.ship, this.channel, this.store);
publish = new PublishApi(this.ship, this.channel, this.store);
constructor(public ship: Patp, public channel: any, public store: GlobalStore) {
super(ship,channel,store);
}
}

View File

@ -1,37 +1,19 @@
import BaseApi from './base';
import { StoreState } from '../store/type';
import { Path, Patp } from '../types/noun';
export default class GroupsApi {
constructor(ship, channel, store) {
const helper = new PrivateHelper(ship, channel, store);
export default class GroupsApi extends BaseApi<StoreState> {
add(path: Path, ships: Patp[] = []) {
return this.action('group-store', 'group-action', {
add: { members: ships, path }
});
}
this.ship = ship;
this.subscribe = helper.subscribe.bind(helper);
this.contactHook = {
edit: helper.contactEdit.bind(helper)
};
this.contactView = {
create: helper.contactCreate.bind(helper),
delete: helper.contactDelete.bind(helper),
remove: helper.contactRemove.bind(helper),
share: helper.contactShare.bind(helper)
};
this.group = {
add: helper.groupAdd.bind(helper),
remove: helper.groupRemove.bind(helper)
};
this.metadata = {
add: helper.metadataAdd.bind(helper)
};
this.invite = {
accept: helper.inviteAccept.bind(helper),
decline: helper.inviteDecline.bind(helper)
};
remove(path: Path, ships: Patp[] = []) {
return this.action('group-store', 'group-action', {
remove: { members: ships, path }
});
}
}
@ -51,17 +33,9 @@ class PrivateHelper extends BaseApi {
});
}
groupAdd(path, ships = []) {
return this.action('group-store', 'group-action', {
add: { members: ships, path }
});
}
groupRemove(path, ships) {
return this.action('group-store', 'group-action', {
remove: { members: ships, path }
});
}
contactShare(recipient, path, ship, contact) {
return this.contactViewAction({

View File

@ -0,0 +1,27 @@
import BaseApi from "./base";
import { StoreState } from "../store/type";
import { Serial, Path } from "../types/noun";
export default class InviteApi extends BaseApi<StoreState> {
accept(app: Path, uid: Serial) {
return this.inviteAction({
accept: {
path: app,
uid
}
});
}
decline(app: Path, uid: Serial) {
return this.inviteAction({
decline: {
path: app,
uid
}
});
}
private inviteAction(action) {
return this.action('invite-store', 'json', action);
}
}

View File

@ -1,51 +0,0 @@
import BaseApi from './base';
class PrivateHelper extends BaseApi {
launchAction(data) {
this.action('launch', 'launch-action', data);
}
launchAdd(name, tile = { basic : { title: '', linkedUrl: '', iconUrl: '' }}) {
this.launchAction({ add: { name, tile } });
}
launchRemove(name) {
this.launchAction({ remove: name });
}
launchChangeOrder(orderedTiles = []) {
this.launchAction({ 'change-order': orderedTiles });
}
launchChangeFirstTime(firstTime = true) {
this.launchAction({ 'change-first-time': firstTime });
}
launchChangeIsShown(name, isShown = true) {
this.launchAction({ 'change-is-shown': { name, isShown }});
}
weatherAction(latlng) {
this.action('weather', 'json', latlng);
}
}
export default class LaunchApi {
constructor(ship, channel, store) {
const helper = new PrivateHelper(ship, channel, store);
this.ship = ship;
this.subscribe = helper.subscribe.bind(helper);
this.launch = {
add: helper.launchAdd.bind(helper),
remove: helper.launchRemove.bind(helper),
changeOrder: helper.launchChangeOrder.bind(helper),
changeFirstTime: helper.launchChangeFirstTime.bind(helper),
changeIsShown: helper.launchChangeIsShown.bind(helper)
};
this.weather = helper.weatherAction.bind(helper);
}
}

View File

@ -0,0 +1,36 @@
import BaseApi from './base';
import { StoreState } from '../store/type';
export default class LaunchApi extends BaseApi<StoreState> {
add(name: string, tile = { basic : { title: '', linkedUrl: '', iconUrl: '' }}) {
this.launchAction({ add: { name, tile } });
}
remove(name: string) {
this.launchAction({ remove: name });
}
changeOrder(orderedTiles = []) {
this.launchAction({ 'change-order': orderedTiles });
}
changeFirstTime(firstTime = true) {
this.launchAction({ 'change-first-time': firstTime });
}
changeIsShown(name: string, isShown = true) {
this.launchAction({ 'change-is-shown': { name, isShown }});
}
weather(latlng: any) {
this.action('weather', 'json', latlng);
}
private launchAction(data) {
this.action('launch', 'launch-action', data);
}
}

View File

@ -1,222 +0,0 @@
import { stringToTa } from '../lib/util';
import BaseApi from './base';
export default class LinksApi extends BaseApi {
constructor(ship, channel, store) {
super(ship, channel, store);
this.ship = ship;
this.invite = {
accept: this.inviteAccept.bind(this),
decline: this.inviteDecline.bind(this)
};
this.groups = {
remove: this.groupRemove.bind(this)
};
this.fetchLink = this.fetchLink.bind(this);
}
fetchLink(path, result, fail, quit) {
this.subscribe.bind(this)(
path,
'PUT',
this.ship,
'link-view',
result,
fail,
quit
);
}
groupsAction(data) {
this.action('group-store', 'group-action', data);
}
groupRemove(path, members) {
this.groupsAction({
remove: {
path,
members
}
});
}
inviteAction(data) {
this.action('invite-store', 'json', data);
}
inviteAccept(uid) {
this.inviteAction({
accept: {
path: '/link',
uid
}
});
}
inviteDecline(uid) {
this.inviteAction({
decline: {
path: '/link',
uid
}
});
}
getCommentsPage(path, url, page) {
const strictUrl = stringToTa(url);
const endpoint = '/json/' + page + '/discussions/' + strictUrl + path;
this.fetchLink(
endpoint,
(res) => {
if (res.data['initial-discussions']) {
// these aren't returned with the response,
// so this ensures the reducers know them.
res.data['initial-discussions'].path = path;
res.data['initial-discussions'].url = url;
}
this.store.handleEvent(res);
},
console.error,
() => {} // no-op on quit
);
}
getPage(path, page) {
const endpoint = '/json/' + page + '/submissions' + path;
this.fetchLink(
endpoint,
(dat) => {
this.store.handleEvent(dat);
},
console.error,
() => {} // no-op on quit
);
}
getSubmission(path, url, callback) {
const strictUrl = stringToTa(url);
const endpoint = '/json/0/submission/' + strictUrl + path;
this.fetchLink(
endpoint,
(res) => {
if (res.data.submission) {
callback(res.data.submission);
} else {
console.error('unexpected submission response', res);
}
},
console.error,
() => {} // no-op on quit
);
}
linkViewAction(data) {
return this.action('link-view', 'link-view-action', data);
}
createCollection(path, title, description, members, realGroup) {
// members is either {group:'/group-path'} or {'ships':[~zod]},
// with realGroup signifying if ships should become a managed group or not.
return this.linkViewAction({
create: { path, title, description, members, realGroup }
});
}
deleteCollection(path) {
return this.linkViewAction({
delete: { path }
});
}
inviteToCollection(path, ships) {
return this.linkViewAction({
invite: { path, ships }
});
}
linkListenAction(data) {
return this.action('link-listen-hook', 'link-listen-action', data);
}
joinCollection(path) {
return this.linkListenAction({ watch: path });
}
removeCollection(path) {
return this.linkListenAction({ leave: path });
}
linkAction(data) {
return this.action('link-store', 'link-action', data);
}
postLink(path, url, title) {
return this.linkAction({
save: { path, url, title }
});
}
postComment(path, url, comment) {
return this.linkAction({
note: { path, url, udon: comment }
});
}
// leave url as null to mark all under path as read
seenLink(path, url = null) {
return this.linkAction({
seen: { path, url }
});
}
metadataAction(data) {
return this.action('metadata-hook', 'metadata-action', data);
}
metadataAdd(appPath, groupPath, title, description, dateCreated, color) {
return this.metadataAction({
add: {
'group-path': groupPath,
resource: {
'app-path': appPath,
'app-name': 'link'
},
metadata: {
title,
description,
color,
'date-created': dateCreated,
creator: `~${window.ship}`
}
}
});
}
sidebarToggle() {
let sidebarBoolean = true;
if (this.store.state.sidebarShown === true) {
sidebarBoolean = false;
}
this.store.handleEvent({
data: {
local: {
sidebarToggle: sidebarBoolean
}
}
});
}
setSelected(selected) {
this.store.handleEvent({
data: {
local: {
selected: selected
}
}
});
}
}

View File

@ -0,0 +1,131 @@
import { stringToTa } from '../lib/util';
import BaseApi from './base';
import { StoreState } from '../store/type';
import { Path } from '../types/noun';
export default class LinksApi extends BaseApi<StoreState> {
getCommentsPage(path: Path, url: string, page: number) {
const strictUrl = stringToTa(url);
const endpoint = '/json/' + page + '/discussions/' + strictUrl + path;
this.fetchLink(
endpoint,
(res) => {
if (res.data['link-update']['initial-discussions']) {
// these aren't returned with the response,
// so this ensures the reducers know them.
res.data['link-update']['initial-discussions'].path = path;
res.data['link-update']['initial-discussions'].url = url;
}
this.store.handleEvent(res);
},
console.error,
() => {} // no-op on quit
);
}
getPage(path: Path, page: number) {
const endpoint = '/json/' + page + '/submissions' + path;
this.fetchLink(
endpoint,
(dat) => {
this.store.handleEvent(dat);
},
console.error,
() => {} // no-op on quit
);
}
getSubmission(path: Path, url: string, callback) {
const strictUrl = stringToTa(url);
const endpoint = '/json/0/submission/' + strictUrl + path;
this.fetchLink(
endpoint,
(res) => {
if (res.data.submission) {
callback(res.data.submission);
} else {
console.error('unexpected submission response', res);
}
},
console.error,
() => {} // no-op on quit
);
}
createCollection(path, title, description, members, realGroup) {
// members is either {group:'/group-path'} or {'ships':[~zod]},
// with realGroup signifying if ships should become a managed group or not.
return this.viewAction({
create: { path, title, description, members, realGroup }
});
}
deleteCollection(path) {
return this.viewAction({
delete: { path }
});
}
inviteToCollection(path, ships) {
return this.viewAction({
invite: { path, ships }
});
}
joinCollection(path) {
return this.linkListenAction({ watch: path });
}
removeCollection(path) {
return this.linkListenAction({ leave: path });
}
postLink(path: Path, url: string, title: string) {
return this.linkAction({
save: { path, url, title }
});
}
postComment(path: Path, url: string, comment: string) {
return this.linkAction({
note: { path, url, udon: comment }
});
}
// leave url as null to mark all under path as read
seenLink(path: Path, url?: string) {
return this.linkAction({
seen: { path, url: url || null }
});
}
private linkAction(data) {
return this.action('link-store', 'link-action', data);
}
private viewAction(data) {
return this.action('link-view', 'link-view-action', data);
}
private linkListenAction(data) {
return this.action('link-listen-hook', 'link-listen-action', data);
}
private fetchLink(path: Path, result, fail, quit) {
this.subscribe.bind(this)(
path,
'PUT',
this.ship,
'link-view',
result,
fail,
quit
);
}
}

View File

@ -0,0 +1,28 @@
import BaseApi from "./base";
import { StoreState } from "../store/type";
import { SelectedGroup } from "../types/local-update";
export default class LocalApi extends BaseApi<StoreState> {
setSelected(selected: SelectedGroup[]) {
this.store.handleEvent({
data: {
local: {
selected
}
}
})
}
sidebarToggle() {
this.store.handleEvent({
data: {
local: {
sidebarToggle: true
}
}
})
}
}

View File

@ -0,0 +1,31 @@
import BaseApi from './base';
import { StoreState } from '../store/type';
import { Path, Patp } from '../types/noun';
export default class MetadataApi extends BaseApi<StoreState> {
metadataAdd(appName: string, appPath: Path, groupPath: Path, title: string, description: string, dateCreated: string, color: string) {
const creator = `~${this.ship}`;
return this.metadataAction({
add: {
'group-path': groupPath,
resource: {
'app-path': appPath,
'app-name': appName
},
metadata: {
title,
description,
color,
'date-created': dateCreated,
creator
}
}
});
}
private metadataAction(data) {
return this.action('metadata-hook', 'metadata-action', data);
}
}

View File

@ -1,7 +1,10 @@
import BaseApi from './base';
import { PublishResponse } from '../types/publish-response';
import { PatpNoSig } from '../types/noun';
import { BookId, NoteId } from '../types/publish-update';
export default class PublishApi extends BaseApi {
handleEvent(data) {
handleEvent(data: PublishResponse) {
this.store.handleEvent({ data: { 'publish-response' : data } });
}
@ -16,7 +19,7 @@ export default class PublishApi extends BaseApi {
});
}
fetchNotebook(host, book) {
fetchNotebook(host: PatpNoSig, book: BookId) {
fetch(`/publish-view/${host}/${book}.json`)
.then(response => response.json())
.then((json) => {
@ -29,7 +32,7 @@ export default class PublishApi extends BaseApi {
});
}
fetchNote(host, book, note) {
fetchNote(host: PatpNoSig, book: BookId, note: NoteId) {
fetch(`/publish-view/${host}/${book}/${note}.json`)
.then(response => response.json())
.then((json) => {
@ -43,7 +46,7 @@ export default class PublishApi extends BaseApi {
});
}
fetchNotesPage(host, book, start, length) {
fetchNotesPage(host: PatpNoSig, book: BookId, start: number, length: number) {
fetch(`/publish-view/notes/${host}/${book}/${start}/${length}.json`)
.then(response => response.json())
.then((json) => {
@ -58,7 +61,7 @@ export default class PublishApi extends BaseApi {
});
}
fetchCommentsPage(host, book, note, start, length) {
fetchCommentsPage(host: PatpNoSig, book: BookId, note: NoteId, start: number, length: number) {
fetch(`/publish-view/comments/${host}/${book}/${note}/${start}/${length}.json`)
.then(response => response.json())
.then((json) => {
@ -74,30 +77,8 @@ export default class PublishApi extends BaseApi {
});
}
groupAction(act) {
return this.action('group-store', 'group-action', act);
}
inviteAction(act) {
return this.action('invite-store', 'invite-action', act);
}
publishAction(act) {
publishAction(act: any) {
return this.action('publish', 'publish-action', act);
}
sidebarToggle() {
let sidebarBoolean = true;
if (this.store.state.sidebarShown === true) {
sidebarBoolean = false;
}
this.store.handleEvent({
data: {
local: {
sidebarToggle: sidebarBoolean
}
}
});
}
}

View File

@ -1,11 +1,6 @@
import React from 'react';
import { Route, Switch } from 'react-router-dom';
import ChatApi from '../../api/chat';
import ChatStore from '../../store/chat';
import ChatSubscription from '../../subscription/chat';
import './css/custom.css';
import { Skeleton } from './components/skeleton';
@ -16,19 +11,22 @@ import { SettingsScreen } from './components/settings';
import { NewScreen } from './components/new';
import { JoinScreen } from './components/join';
import { NewDmScreen } from './components/new-dm';
import { PatpNoSig } from '../../types/noun';
import GlobalApi from '../../api/global';
import { StoreState } from '../../store/type';
import GlobalSubscription from '../../subscription/global';
type ChatAppProps = StoreState & {
ship: PatpNoSig;
api: GlobalApi;
subscription: GlobalSubscription;
};
export default class ChatApp extends React.Component<ChatAppProps, {}> {
totalUnreads = 0;
export default class ChatApp extends React.Component {
constructor(props) {
super(props);
this.store = new ChatStore();
this.state = this.store.state;
this.totalUnreads = 0;
this.resetControllers();
}
resetControllers() {
this.api = null;
this.subscription = null;
}
componentDidMount() {
@ -36,33 +34,31 @@ export default class ChatApp extends React.Component {
// preload spinner asset
new Image().src = '/~landscape/img/Spinner.png';
this.store.setStateHandler(this.setState.bind(this));
const channel = new this.props.channel();
this.api = new ChatApi(this.props.ship, channel, this.store);
this.props.subscription.startApp('chat');
this.subscription = new ChatSubscription(this.store, this.api, channel);
this.subscription.start();
if (!this.props.sidebarShown) {
this.props.api.local.sidebarToggle();
}
}
componentWillUnmount() {
this.subscription.delete();
this.store.clear();
this.store.setStateHandler(() => {});
this.resetControllers();
this.props.subscription.stopApp('chat');
}
render() {
const { state, props } = this;
const { props } = this;
const messagePreviews = {};
const unreads = {};
let totalUnreads = 0;
const selectedGroups = props.selectedGroups ? props.selectedGroups : [];
const associations = state.associations ? state.associations : { chat: {}, contacts: {} };
const associations = props.associations
? props.associations
: { chat: {}, contacts: {} };
Object.keys(state.inbox).forEach((stat) => {
const envelopes = state.inbox[stat].envelopes;
Object.keys(props.inbox).forEach((stat) => {
const envelopes = props.inbox[stat].envelopes;
if (envelopes.length === 0) {
messagePreviews[stat] = false;
@ -70,37 +66,54 @@ export default class ChatApp extends React.Component {
messagePreviews[stat] = envelopes[0];
}
const unread = Math.max(state.inbox[stat].config.length - state.inbox[stat].config.read, 0);
const unread = Math.max(
props.inbox[stat].config.length - props.inbox[stat].config.read,
0
);
unreads[stat] = Boolean(unread);
if (unread &&
(selectedGroups.length === 0 || selectedGroups.map(((e) => {
return e[0];
})).includes(associations.chat?.[stat]?.['group-path']) ||
associations.chat?.[stat]?.['group-path'].startsWith('/~/'))) {
totalUnreads += unread;
if (
unread &&
(selectedGroups.length === 0 ||
selectedGroups
.map((e) => {
return e[0];
})
.includes(associations.chat?.[stat]?.['group-path']) ||
associations.chat?.[stat]?.['group-path'].startsWith('/~/'))
) {
totalUnreads += unread;
}
});
if (totalUnreads !== this.totalUnreads) {
document.title = totalUnreads > 0 ? `OS1 - Chat (${totalUnreads})` : 'OS1 - Chat';
document.title =
totalUnreads > 0 ? `OS1 - Chat (${totalUnreads})` : 'OS1 - Chat';
this.totalUnreads = totalUnreads;
}
const invites = state.invites ? state.invites : { '/chat': {}, '/contacts': {} };
const {
invites,
s3,
sidebarShown,
inbox,
contacts,
permissions,
chatSynced,
api,
chatInitialized,
pendingMessages
} = props;
const contacts = state.contacts ? state.contacts : {};
const s3 = state.s3 ? state.s3 : {};
const renderChannelSidebar = (props, station) => (
const renderChannelSidebar = (props, station?) => (
<Sidebar
inbox={state.inbox}
inbox={inbox}
messagePreviews={messagePreviews}
associations={associations}
selectedGroups={selectedGroups}
contacts={contacts}
invites={invites['/chat'] || {}}
unreads={unreads}
api={this.api}
api={api}
station={station}
{...props}
/>
@ -117,14 +130,14 @@ export default class ChatApp extends React.Component {
associations={associations}
invites={invites}
chatHideonMobile={true}
sidebarShown={state.sidebarShown}
sidebarShown={sidebarShown}
sidebar={renderChannelSidebar(props)}
>
<div className="h-100 w-100 overflow-x-hidden flex flex-column bg-white bg-gray0-d">
<div className="pl3 pr3 pt2 dt pb3 w-100 h-100">
<p className="f8 pt3 gray2 w-100 h-100 dtc v-mid tc">
Select, create, or join a chat to begin.
</p>
</p>
</div>
</div>
</Skeleton>
@ -143,15 +156,15 @@ export default class ChatApp extends React.Component {
invites={invites}
sidebarHideOnMobile={true}
sidebar={renderChannelSidebar(props)}
sidebarShown={state.sidebarShown}
sidebarShown={sidebarShown}
>
<NewDmScreen
api={this.api}
inbox={state.inbox || {}}
permissions={state.permissions || {}}
contacts={state.contacts || {}}
api={api}
inbox={inbox}
permissions={permissions || {}}
contacts={contacts || {}}
associations={associations.contacts}
chatSynced={state.chatSynced || {}}
chatSynced={chatSynced || {}}
autoCreate={ship}
{...props}
/>
@ -169,15 +182,15 @@ export default class ChatApp extends React.Component {
invites={invites}
sidebarHideOnMobile={true}
sidebar={renderChannelSidebar(props)}
sidebarShown={state.sidebarShown}
sidebarShown={sidebarShown}
>
<NewScreen
api={this.api}
inbox={state.inbox || {}}
permissions={state.permissions || {}}
contacts={state.contacts || {}}
api={api}
inbox={inbox || {}}
permissions={permissions || {}}
contacts={contacts || {}}
associations={associations.contacts}
chatSynced={state.chatSynced || {}}
chatSynced={chatSynced || {}}
{...props}
/>
</Skeleton>
@ -188,8 +201,7 @@ export default class ChatApp extends React.Component {
exact
path="/~chat/join/(~)?/:ship?/:station?"
render={(props) => {
let station =
`/${props.match.params.ship}/${props.match.params.station}`;
let station = `/${props.match.params.ship}/${props.match.params.station}`;
const sig = props.match.url.includes('/~/');
if (sig) {
station = '/~' + station;
@ -201,13 +213,13 @@ export default class ChatApp extends React.Component {
invites={invites}
sidebarHideOnMobile={true}
sidebar={renderChannelSidebar(props)}
sidebarShown={state.sidebarShown}
sidebarShown={sidebarShown}
>
<JoinScreen
api={this.api}
inbox={state.inbox}
api={api}
inbox={inbox}
autoJoin={station}
chatSynced={state.chatSynced || {}}
chatSynced={chatSynced || {}}
{...props}
/>
</Skeleton>
@ -218,13 +230,12 @@ export default class ChatApp extends React.Component {
exact
path="/~chat/(popout)?/room/(~)?/:ship/:station+"
render={(props) => {
let station =
`/${props.match.params.ship}/${props.match.params.station}`;
let station = `/${props.match.params.ship}/${props.match.params.station}`;
const sig = props.match.url.includes('/~/');
if (sig) {
station = '/~' + station;
}
const mailbox = state.inbox[station] || {
const mailbox = inbox[station] || {
config: {
read: 0,
length: 0
@ -235,11 +246,11 @@ export default class ChatApp extends React.Component {
let roomContacts = {};
const associatedGroup =
station in associations['chat'] &&
'group-path' in associations.chat[station]
'group-path' in associations.chat[station]
? associations.chat[station]['group-path']
: '';
if ((associations.chat[station]) && (associatedGroup in contacts)) {
if (associations.chat[station] && associatedGroup in contacts) {
roomContacts = contacts[associatedGroup];
}
@ -247,10 +258,12 @@ export default class ChatApp extends React.Component {
station in associations['chat'] ? associations.chat[station] : {};
const permission =
station in state.permissions ? state.permissions[station] : {
who: new Set([]),
kind: 'white'
};
station in permissions
? permissions[station]
: {
who: new Set([]),
kind: 'white'
};
const popout = props.match.url.includes('/popout/');
return (
@ -259,26 +272,25 @@ export default class ChatApp extends React.Component {
invites={invites}
sidebarHideOnMobile={true}
popout={popout}
sidebarShown={state.sidebarShown}
sidebarShown={sidebarShown}
sidebar={renderChannelSidebar(props, station)}
>
<ChatScreen
chatSynced={state.chatSynced}
chatSynced={chatSynced || {}}
station={station}
association={association}
api={this.api}
subscription={this.subscription}
api={api}
read={mailbox.config.read}
length={mailbox.config.length}
envelopes={mailbox.envelopes}
inbox={state.inbox}
inbox={inbox}
contacts={roomContacts}
permission={permission}
pendingMessages={state.pendingMessages}
pendingMessages={pendingMessages}
s3={s3}
popout={popout}
sidebarShown={state.sidebarShown}
chatInitialized={state.chatInitialized}
sidebarShown={sidebarShown}
chatInitialized={chatInitialized}
{...props}
/>
</Skeleton>
@ -295,7 +307,7 @@ export default class ChatApp extends React.Component {
station = '/~' + station;
}
const permission = state.permissions[station] || {
const permission = permissions[station] || {
kind: '',
who: new Set([])
};
@ -309,20 +321,20 @@ export default class ChatApp extends React.Component {
associations={associations}
invites={invites}
sidebarHideOnMobile={true}
sidebarShown={state.sidebarShown}
sidebarShown={sidebarShown}
popout={popout}
sidebar={renderChannelSidebar(props, station)}
>
<MemberScreen
{...props}
api={this.api}
api={api}
station={station}
association={association}
permission={permission}
contacts={contacts}
permissions={state.permissions}
permissions={permissions}
popout={popout}
sidebarShown={state.sidebarShown}
sidebarShown={sidebarShown}
/>
</Skeleton>
);
@ -332,8 +344,7 @@ export default class ChatApp extends React.Component {
exact
path="/~chat/(popout)?/settings/(~)?/:ship/:station+"
render={(props) => {
let station =
`/${props.match.params.ship}/${props.match.params.station}`;
let station = `/${props.match.params.ship}/${props.match.params.station}`;
const sig = props.match.url.includes('/~/');
if (sig) {
station = '/~' + station;
@ -341,7 +352,7 @@ export default class ChatApp extends React.Component {
const popout = props.match.url.includes('/popout/');
const permission = state.permissions[station] || {
const permission = permissions[station] || {
kind: '',
who: new Set([])
};
@ -355,7 +366,7 @@ export default class ChatApp extends React.Component {
invites={invites}
sidebarHideOnMobile={true}
popout={popout}
sidebarShown={state.sidebarShown}
sidebarShown={sidebarShown}
sidebar={renderChannelSidebar(props, station)}
>
<SettingsScreen
@ -363,19 +374,19 @@ export default class ChatApp extends React.Component {
station={station}
association={association}
permission={permission}
permissions={state.permissions || {}}
contacts={state.contacts || {}}
permissions={permissions || {}}
contacts={contacts || {}}
associations={associations.contacts}
api={this.api}
inbox={state.inbox}
api={api}
inbox={inbox}
popout={popout}
sidebarShown={state.sidebarShown}
sidebarShown={sidebarShown}
/>
</Skeleton>
);
}}
/>
</Switch>
);
);
}
}

View File

@ -1,19 +1,26 @@
import React, { Component } from 'react';
import _ from 'lodash';
import moment from 'moment';
import React, { Component } from "react";
import _ from "lodash";
import moment from "moment";
import { Link } from 'react-router-dom';
import { Link, RouteComponentProps } from "react-router-dom";
import { ResubscribeElement } from './lib/resubscribe-element';
import { BacklogElement } from './lib/backlog-element';
import { Message } from './lib/message';
import { SidebarSwitcher } from '../../../components/SidebarSwitch';
import { ChatTabBar } from './lib/chat-tabbar';
import { ChatInput } from './lib/chat-input';
import { UnreadNotice } from './lib/unread-notice';
import { deSig } from '../../../lib/util';
import { ResubscribeElement } from "./lib/resubscribe-element";
import { BacklogElement } from "./lib/backlog-element";
import { Message } from "./lib/message";
import { SidebarSwitcher } from "../../../components/SidebarSwitch";
import { ChatTabBar } from "./lib/chat-tabbar";
import { ChatInput } from "./lib/chat-input";
import { UnreadNotice } from "./lib/unread-notice";
import { deSig } from "../../../lib/util";
import { ChatHookUpdate } from "../../../types/chat-hook-update";
import ChatApi from "../../../api/chat";
import { Inbox, Envelope } from "../../../types/chat-update";
import { Contacts } from "../../../types/contact-update";
import { Path, Patp } from "../../../types/noun";
import GlobalApi from "../../../api/global";
import { Association } from "../../../types/metadata-update";
function getNumPending(props) {
function getNumPending(props: any) {
const result = props.pendingMessages.has(props.station)
? props.pendingMessages.get(props.station).length
: 0;
@ -25,26 +32,32 @@ const DEFAULT_BACKLOG_SIZE = 300;
const MAX_BACKLOG_SIZE = 1000;
function scrollIsAtTop(container) {
if ((navigator.userAgent.includes("Safari") &&
if (
(navigator.userAgent.includes("Safari") &&
navigator.userAgent.includes("Chrome")) ||
navigator.userAgent.includes("Firefox")
navigator.userAgent.includes("Firefox")
) {
return container.scrollTop === 0;
} else if (navigator.userAgent.includes("Safari")) {
return container.scrollHeight + Math.round(container.scrollTop) <=
container.clientHeight + 10;
return (
container.scrollHeight + Math.round(container.scrollTop) <=
container.clientHeight + 10
);
} else {
return false;
}
}
function scrollIsAtBottom(container) {
if ((navigator.userAgent.includes("Safari") &&
if (
(navigator.userAgent.includes("Safari") &&
navigator.userAgent.includes("Chrome")) ||
navigator.userAgent.includes("Firefox")
navigator.userAgent.includes("Firefox")
) {
return container.scrollHeight - Math.round(container.scrollTop) <=
container.clientHeight + 10;
return (
container.scrollHeight - Math.round(container.scrollTop) <=
container.clientHeight + 10
);
} else if (navigator.userAgent.includes("Safari")) {
return container.scrollTop === 0;
} else {
@ -52,7 +65,50 @@ function scrollIsAtBottom(container) {
}
}
export class ChatScreen extends Component {
type IMessage = Envelope & { pending?: boolean };
type ChatScreenProps = RouteComponentProps<{
ship: Patp;
station: string;
}> & {
chatSynced: ChatHookUpdate;
station: any;
association: Association;
api: GlobalApi;
read: number;
length: number;
inbox: Inbox;
contacts: Contacts;
permission: any;
pendingMessages: Map<Path, Envelope[]>;
s3: any;
popout: boolean;
sidebarShown: boolean;
chatInitialized: boolean;
envelopes: Envelope[];
};
interface ChatScreenState {
numPages: number;
scrollLocked: boolean;
read: number;
active: boolean;
lastScrollHeight: number | null;
}
export class ChatScreen extends Component<ChatScreenProps, ChatScreenState> {
hasAskedForMessages = false;
lastNumPending = 0;
scrollContainer: HTMLElement | null = null;
unreadMarker = null;
scrolledToMarker = false;
activityTimeout: NodeJS.Timeout | null = null;
scrollElement: HTMLElement | null = null;
constructor(props) {
super(props);
@ -65,29 +121,22 @@ export class ChatScreen extends Component {
lastScrollHeight: null,
};
this.hasAskedForMessages = false;
this.lastNumPending = 0;
this.scrollContainer = null;
this.onScroll = this.onScroll.bind(this);
this.unreadMarker = null;
this.scrolledToMarker = false;
this.setUnreadMarker = this.setUnreadMarker.bind(this);
this.activityTimeout = true;
this.handleActivity = this.handleActivity.bind(this);
this.setInactive = this.setInactive.bind(this);
moment.updateLocale('en', {
moment.updateLocale("en", {
calendar: {
sameDay: '[Today]',
nextDay: '[Tomorrow]',
nextWeek: 'dddd',
lastDay: '[Yesterday]',
lastWeek: '[Last] dddd',
sameElse: 'DD/MM/YYYY'
}
sameDay: "[Today]",
nextDay: "[Tomorrow]",
nextWeek: "dddd",
lastDay: "[Yesterday]",
lastWeek: "[Last] dddd",
sameElse: "DD/MM/YYYY",
},
});
}
@ -104,17 +153,17 @@ export class ChatScreen extends Component {
document.removeEventListener("mousedown", this.handleActivity, false);
document.removeEventListener("keypress", this.handleActivity, false);
document.removeEventListener("touchmove", this.handleActivity, false);
if(this.activityTimeout) {
if (this.activityTimeout) {
clearTimeout(this.activityTimeout);
}
}
handleActivity() {
if(!this.state.active) {
if (!this.state.active) {
this.setState({ active: true });
}
if(this.activityTimeout) {
if (this.activityTimeout) {
clearTimeout(this.activityTimeout);
}
@ -139,13 +188,13 @@ export class ChatScreen extends Component {
const unreadUnloaded = unread - props.envelopes.length;
const excessUnread = unreadUnloaded > MAX_BACKLOG_SIZE;
if(!excessUnread && unreadUnloaded + 20 > DEFAULT_BACKLOG_SIZE) {
if (!excessUnread && unreadUnloaded + 20 > DEFAULT_BACKLOG_SIZE) {
this.askForMessages(unreadUnloaded + 20);
} else {
this.askForMessages(DEFAULT_BACKLOG_SIZE);
}
if(excessUnread || props.read === props.length){
if (excessUnread || props.read === props.length) {
this.scrolledToMarker = true;
this.setState(
{
@ -156,7 +205,7 @@ export class ChatScreen extends Component {
}
);
} else {
this.setState({ scrollLocked: true, numPages: Math.ceil(unread/100) });
this.setState({ scrollLocked: true, numPages: Math.ceil(unread / 100) });
}
}
@ -168,34 +217,36 @@ export class ChatScreen extends Component {
prevProps.match.params.ship !== props.match.params.ship
) {
this.receivedNewChat();
} else if (props.chatInitialized &&
!(props.station in props.inbox) &&
(Boolean(props.chatSynced) && !(props.station in props.chatSynced))) {
props.history.push('/~chat');
} else if (
props.envelopes.length >= prevProps.envelopes.length + 10
props.chatInitialized &&
!(props.station in props.inbox) &&
Boolean(props.chatSynced) &&
!(props.station in props.chatSynced)
) {
props.history.push("/~chat");
} else if (props.envelopes.length >= prevProps.envelopes.length + 10) {
this.hasAskedForMessages = false;
} else if(props.length !== prevProps.length &&
prevProps.length === prevState.read &&
state.active
} else if (
props.length !== prevProps.length &&
prevProps.length === prevState.read &&
state.active
) {
this.setState({ read: props.length });
this.props.api.chat.read(this.props.station);
}
if(!prevProps.chatInitialized && props.chatInitialized) {
if (!prevProps.chatInitialized && props.chatInitialized) {
this.receivedNewChat();
}
if (
(props.length !== prevProps.length ||
props.envelopes.length !== prevProps.envelopes.length ||
getNumPending(props) !== this.lastNumPending ||
state.numPages !== prevState.numPages)
props.length !== prevProps.length ||
props.envelopes.length !== prevProps.envelopes.length ||
getNumPending(props) !== this.lastNumPending ||
state.numPages !== prevState.numPages
) {
this.scrollToBottom();
if(navigator.userAgent.includes("Firefox")) {
if (navigator.userAgent.includes("Firefox")) {
this.recalculateScrollTop();
}
@ -219,7 +270,7 @@ export class ChatScreen extends Component {
if (start > 0) {
const end = start + size < props.length ? start + size : props.length;
this.hasAskedForMessages = true;
props.subscription.fetchMessages(start + 1, end, props.station);
props.api.chat.fetchMessages(start + 1, end, props.station);
}
}
@ -231,31 +282,31 @@ export class ChatScreen extends Component {
// Restore chat position on FF when new messages come in
recalculateScrollTop() {
if(!this.scrollContainer) {
const { lastScrollHeight } = this.state;
if (!this.scrollContainer || !lastScrollHeight) {
return;
}
const { lastScrollHeight } = this.state;
const target = this.scrollContainer;
const newScrollTop = this.scrollContainer.scrollHeight - lastScrollHeight;
if(target.scrollTop !== 0 || newScrollTop === target.scrollTop) {
if (target.scrollTop !== 0 || newScrollTop === target.scrollTop) {
return;
}
target.scrollTop = target.scrollHeight - lastScrollHeight;
}
onScroll(e) {
if(scrollIsAtTop(e.target)) {
if (scrollIsAtTop(e.target)) {
// Save scroll position for FF
if (navigator.userAgent.includes('Firefox')) {
if (navigator.userAgent.includes("Firefox")) {
this.setState({
lastScrollHeight: e.target.scrollHeight
lastScrollHeight: e.target.scrollHeight,
});
}
this.setState(
{
numPages: this.state.numPages + 1,
scrollLocked: true
scrollLocked: true,
},
() => {
this.askForMessages(DEFAULT_BACKLOG_SIZE);
@ -265,21 +316,20 @@ export class ChatScreen extends Component {
this.dismissUnread();
this.setState({
numPages: 1,
scrollLocked: false
scrollLocked: false,
});
}
}
setUnreadMarker(ref) {
if(ref && !this.scrolledToMarker) {
if (ref && !this.scrolledToMarker) {
this.setState({ scrollLocked: true }, () => {
ref.scrollIntoView({ block: 'center' });
if(ref.offsetParent &&
scrollIsAtBottom(ref.offsetParent)) {
ref.scrollIntoView({ block: "center" });
if (ref.offsetParent && scrollIsAtBottom(ref.offsetParent)) {
this.dismissUnread();
this.setState({
numPages: 1,
scrollLocked: false
scrollLocked: false,
});
}
});
@ -298,38 +348,32 @@ export class ChatScreen extends Component {
const { props, state } = this;
let messages = props.envelopes.slice(0);
let messages: IMessage[] = props.envelopes.slice(0);
const lastMsgNum = messages.length > 0 ? messages.length : 0;
if (messages.length > 100 * state.numPages) {
messages = messages.slice(0, 100 * state.numPages);
}
const pendingMessages = props.pendingMessages.has(props.station)
? props.pendingMessages.get(props.station)
: [];
pendingMessages.map((value) => {
return (value.pending = true);
});
const pendingMessages: IMessage[] = (
props.pendingMessages.get(props.station) || []
).map((value) => ({ ...value, pending: true }));
messages = pendingMessages.concat(messages);
const messageElements = messages.map((msg, i) => {
// Render sigil if previous message is not by the same sender
const aut = ['author'];
const aut = ["author"];
const renderSigil =
_.get(messages[i + 1], aut) !==
_.get(msg, aut, msg.author);
_.get(messages[i + 1], aut) !== _.get(msg, aut, msg.author);
const paddingTop = renderSigil;
const paddingBot =
_.get(messages[i - 1], aut) !==
_.get(msg, aut, msg.author);
_.get(messages[i - 1], aut) !== _.get(msg, aut, msg.author);
const when = ['when'];
const when = ["when"];
const dayBreak =
moment(_.get(messages[i+1], when)).format('YYYY.MM.DD') !==
moment(_.get(messages[i], when)).format('YYYY.MM.DD');
moment(_.get(messages[i + 1], when)).format("YYYY.MM.DD") !==
moment(_.get(messages[i], when)).format("YYYY.MM.DD");
const messageElem = (
<Message
@ -343,33 +387,39 @@ export class ChatScreen extends Component {
group={props.association}
/>
);
if(unread > 0 && i === unread - 1) {
if (unread > 0 && i === unread - 1) {
return (
<>
{messageElem}
<div key={'unreads'+ msg.uid} ref={this.setUnreadMarker} className="mv2 green2 flex items-center f9">
<div
key={"unreads" + msg.uid}
ref={this.setUnreadMarker}
className="mv2 green2 flex items-center f9"
>
<hr className="dn-s ma0 w2 b--green2 bt-0" />
<p className="mh4">
New messages below
</p>
<p className="mh4">New messages below</p>
<hr className="ma0 flex-grow-1 b--green2 bt-0" />
{ dayBreak && (
<p className="gray2 mh4">
{moment(_.get(messages[i], when)).calendar()}
</p>
{dayBreak && (
<p className="gray2 mh4">
{moment(_.get(messages[i], when)).calendar()}
</p>
)}
<hr style={{ width: 'calc(50% - 48px)' }} className="b--green2 ma0 bt-0" />
<hr
style={{ width: "calc(50% - 48px)" }}
className="b--green2 ma0 bt-0"
/>
</div>
</>
);
} else if(dayBreak) {
} else if (dayBreak) {
return (
<>
{messageElem}
<div key={'daybreak' + msg.uid} className="pv3 gray2 b--gray2 flex items-center justify-center f9 ">
<p>
{moment(_.get(messages[i], when)).calendar()}
</p>
<div
key={"daybreak" + msg.uid}
className="pv3 gray2 b--gray2 flex items-center justify-center f9 "
>
<p>{moment(_.get(messages[i], when)).calendar()}</p>
</div>
</>
);
@ -378,47 +428,47 @@ export class ChatScreen extends Component {
}
});
if (navigator.userAgent.includes('Firefox')) {
if (navigator.userAgent.includes("Firefox")) {
return (
<div className="relative overflow-y-scroll h-100" onScroll={this.onScroll}
ref={(e) => {
this.scrollContainer = e;
}}
<div
className="relative overflow-y-scroll h-100"
onScroll={this.onScroll}
ref={(e) => {
this.scrollContainer = e;
}}
>
<div
className="bg-white bg-gray0-d pt3 pb2 flex flex-column-reverse"
style={{ resize: 'vertical' }}
style={{ resize: "vertical" }}
>
<div
ref={(el) => {
this.scrollElement = el;
}}
></div>
{(props.chatInitialized &&
!(props.station in props.inbox)) && (
<BacklogElement />
{props.chatInitialized && !(props.station in props.inbox) && (
<BacklogElement />
)}
{props.chatSynced &&
!(props.station in props.chatSynced) &&
messages.length > 0 ? (
<ResubscribeElement
api={props.api}
host={props.match.params.ship}
station={props.station}
/>
) : (
<div />
)}
{(
props.chatSynced &&
!(props.station in props.chatSynced) &&
(messages.length > 0)
) ? (
<ResubscribeElement
api={props.api}
host={props.match.params.ship}
station={props.station}
/>
) : (<div />)
}
{messageElements}
</div>
</div>
);
} else {
} else {
return (
<div
className="overflow-y-scroll bg-white bg-gray0-d pt3 pb2 flex flex-column-reverse relative"
style={{ height: '100%', resize: 'vertical' }}
style={{ height: "100%", resize: "vertical" }}
onScroll={this.onScroll}
>
<div
@ -426,26 +476,24 @@ ref={(e) => {
this.scrollElement = el;
}}
></div>
{(props.chatInitialized &&
!(props.station in props.inbox)) && (
<BacklogElement />
{props.chatInitialized && !(props.station in props.inbox) && (
<BacklogElement />
)}
{props.chatSynced &&
!(props.station in props.chatSynced) &&
messages.length > 0 ? (
<ResubscribeElement
api={props.api}
host={props.match.params.ship}
station={props.station}
/>
) : (
<div />
)}
{(
props.chatSynced &&
!(props.station in props.chatSynced) &&
(messages.length > 0)
) ? (
<ResubscribeElement
api={props.api}
host={props.match.params.ship}
station={props.station}
/>
) : (<div />)
}
{messageElements}
</div>
);
}
}
}
render() {
@ -457,16 +505,16 @@ ref={(e) => {
const group = Array.from(props.permission.who.values());
const isinPopout = props.popout ? 'popout/' : '';
const isinPopout = props.popout ? "popout/" : "";
const ownerContact = (window.ship in props.contacts)
? props.contacts[window.ship] : false;
const ownerContact =
window.ship in props.contacts ? props.contacts[window.ship] : false;
let title = props.station.substr(1);
if (props.association && 'metadata' in props.association) {
if (props.association && "metadata" in props.association) {
title =
props.association.metadata.title !== ''
props.association.metadata.title !== ""
? props.association.metadata.title
: props.station.substr(1);
}
@ -475,8 +523,8 @@ ref={(e) => {
const unreadMsg = unread > 0 && messages[unread - 1];
const showUnreadNotice = props.length !== props.read && props.read === state.read;
const showUnreadNotice =
props.length !== props.read && props.read === state.read;
return (
<div
@ -485,14 +533,16 @@ ref={(e) => {
>
<div
className="w-100 dn-m dn-l dn-xl inter pt4 pb6 pl3 f8"
style={{ height: '1rem' }}
style={{ height: "1rem" }}
>
<Link to="/~chat/">{'⟵ All Chats'}</Link>
<Link to="/~chat/">{"⟵ All Chats"}</Link>
</div>
<div
className={'pl4 pt2 bb b--gray4 b--gray1-d bg-gray0-d flex relative ' +
'overflow-x-auto overflow-y-hidden flex-shrink-0 '}
className={
"pl4 pt2 bb b--gray4 b--gray1-d bg-gray0-d flex relative " +
"overflow-x-auto overflow-y-hidden flex-shrink-0 "
}
style={{ height: 48 }}
>
<SidebarSwitcher
@ -500,13 +550,16 @@ ref={(e) => {
popout={props.popout}
api={props.api}
/>
<Link to={'/~chat/' + isinPopout + 'room' + props.station}
className="pt2 white-d"
<Link
to={"/~chat/" + isinPopout + "room" + props.station}
className="pt2 white-d"
>
<h2
className={'dib f9 fw4 lh-solid v-top ' +
((title === props.station.substr(1)) ? 'mono' : '')}
style={{ width: 'max-content' }}
className={
"dib f9 fw4 lh-solid v-top " +
(title === props.station.substr(1) ? "mono" : "")
}
style={{ width: "max-content" }}
>
{title}
</h2>
@ -520,13 +573,13 @@ ref={(e) => {
api={props.api}
/>
</div>
{ !!unreadMsg && showUnreadNotice && (
{!!unreadMsg && showUnreadNotice && (
<UnreadNotice
unread={unread}
unreadMsg={unreadMsg}
onRead={() => this.dismissUnread()}
/>
) }
)}
{this.chatWindow(unread)}
<ChatInput
api={props.api}

View File

@ -45,7 +45,7 @@ export class JoinScreen extends Component {
this.setState({
station,
awaiting: true
}, () => props.api.chatView.join(ship, station, true));
}, () => props.api.chat.join(ship, station, true));
}
if (state.station in props.inbox ||
@ -78,7 +78,7 @@ export class JoinScreen extends Component {
station,
awaiting: true
}, () => {
props.api.chatView.join(ship, station, true);
props.api.chat.join(ship, station, true);
});
}

View File

@ -2,7 +2,7 @@ import React, { Component } from 'react';
export class ResubscribeElement extends Component {
onClickResubscribe() {
this.props.api.chatHook.addSynced(
this.props.api.chat.addSynced(
this.props.host,
this.props.station,
true);

View File

@ -2,11 +2,11 @@ import React, { Component } from 'react';
export class SidebarInvite extends Component {
onAccept() {
this.props.api.invite.accept(this.props.uid);
this.props.api.invite.accept('/chat', this.props.uid);
}
onDecline() {
this.props.api.invite.decline(this.props.uid);
this.props.api.invite.decline('/chat', this.props.uid);
}
render() {

View File

@ -63,7 +63,7 @@ export class NewDmScreen extends Component {
},
() => {
const groupPath = station;
props.api.chatView.create(
props.api.chat.create(
`~${window.ship} <-> ~${state.ship}`,
'',
station,

View File

@ -146,7 +146,7 @@ export class NewScreen extends Component {
if (state.groups.length > 0) {
groupPath = state.groups[0];
}
const submit = props.api.chatView.create(
const submit = props.api.chat.create(
state.title,
state.description,
appPath,

View File

@ -108,7 +108,8 @@ export class SettingsScreen extends Component {
if (chatOwner) {
this.setState({ awaiting: true, type: 'Editing chat...' }, (() => {
props.api.metadata.add(
props.api.metadata.metadataAdd(
'chat',
association['app-path'],
association['group-path'],
association.metadata.title,
@ -133,7 +134,7 @@ export class SettingsScreen extends Component {
? 'Deleting chat...'
: 'Leaving chat...'
}, (() => {
props.api.chatView.delete(props.station);
props.api.chat.delete(props.station);
}));
}
@ -145,7 +146,7 @@ export class SettingsScreen extends Component {
awaiting: true,
type: 'Converting chat...'
}, (() => {
props.api.chatView.groupify(
props.api.chat.groupify(
props.station, state.targetGroup, state.inclusive
).then(() => this.setState({ awaiting: false }));
}));
@ -278,7 +279,8 @@ export class SettingsScreen extends Component {
onBlur={() => {
if (chatOwner) {
this.setState({ awaiting: true, type: 'Editing chat...' }, (() => {
props.api.metadata.add(
props.api.metadata.metadataAdd(
'chat',
association['app-path'],
association['group-path'],
state.title,
@ -307,7 +309,8 @@ export class SettingsScreen extends Component {
onBlur={() => {
if (chatOwner) {
this.setState({ awaiting: true, type: 'Editing chat...' }, (() => {
props.api.metadata.add(
props.api.metadata.metadataAdd(
'chat',
association['app-path'],
association['group-path'],
association.metadata.title,

View File

@ -17,9 +17,6 @@ import GroupDetail from './components/lib/group-detail';
export default class GroupsApp extends Component {
constructor(props) {
super(props);
this.store = new GroupsStore();
this.state = this.store.state;
this.resetControllers();
}
componentDidMount() {
@ -27,41 +24,30 @@ export default class GroupsApp extends Component {
// preload spinner asset
new Image().src = '/~landscape/img/Spinner.png';
this.store.setStateHandler(this.setState.bind(this));
const channel = new this.props.channel();
this.api = new GroupsApi(this.props.ship, channel, this.store);
this.subscription = new GroupsSubscription(this.store, this.api, channel);
this.subscription.start();
this.props.subscription.startApp('groups')
}
componentWillUnmount() {
this.subscription.delete();
this.store.clear();
this.store.setStateHandler(() => {});
this.resetControllers();
this.props.subscription.stopApp('groups')
}
resetControllers() {
this.api = null;
this.subscription = null;
}
render() {
const { state, props } = this;
const { props } = this;
const contacts = state.contacts ? state.contacts : {};
const contacts = props.contacts || {};
const defaultContacts =
(Boolean(state.contacts) && '/~/default' in state.contacts) ?
state.contacts['/~/default'] : {};
const groups = state.groups ? state.groups : {};
(Boolean(props.contacts) && '/~/default' in props.contacts) ?
props.contacts['/~/default'] : {};
const groups = props.groups ? props.groups : {};
const invites =
(Boolean(state.invites) && '/contacts' in state.invites) ?
state.invites['/contacts'] : {};
const associations = state.associations ? state.associations : {};
(Boolean(props.invites) && '/contacts' in props.invites) ?
props.invites['/contacts'] : {};
const associations = props.associations ? props.associations : {};
const selectedGroups = props.selectedGroups ? props.selectedGroups : [];
const s3 = state.s3 ? state.s3 : {};
const s3 = props.s3 ? props.s3 : {};
const { api } = props;
return (
<Switch>
@ -72,7 +58,7 @@ export default class GroupsApp extends Component {
activeDrawer="groups"
selectedGroups={selectedGroups}
history={props.history}
api={this.api}
api={api}
contacts={contacts}
groups={groups}
invites={invites}
@ -95,7 +81,7 @@ export default class GroupsApp extends Component {
<Skeleton
history={props.history}
selectedGroups={selectedGroups}
api={this.api}
api={api}
contacts={contacts}
groups={groups}
invites={invites}
@ -106,7 +92,7 @@ export default class GroupsApp extends Component {
history={props.history}
groups={groups}
contacts={contacts}
api={this.api}
api={api}
/>
</Skeleton>
);
@ -129,7 +115,7 @@ export default class GroupsApp extends Component {
<Skeleton
history={props.history}
selectedGroups={selectedGroups}
api={this.api}
api={api}
contacts={contacts}
invites={invites}
groups={groups}
@ -142,7 +128,7 @@ export default class GroupsApp extends Component {
defaultContacts={defaultContacts}
group={group}
activeDrawer={(detail || settings) ? 'detail' : 'contacts'}
api={this.api}
api={api}
path={groupPath}
{...props}
/>
@ -153,7 +139,7 @@ export default class GroupsApp extends Component {
activeDrawer={(detail || settings) ? 'detail' : 'contacts'}
settings={settings}
associations={associations}
api={this.api}
api={api}
{...props}
/>
</Skeleton>
@ -171,7 +157,7 @@ export default class GroupsApp extends Component {
<Skeleton
history={props.history}
selectedGroups={selectedGroups}
api={this.api}
api={api}
contacts={contacts}
groups={groups}
invites={invites}
@ -185,11 +171,11 @@ export default class GroupsApp extends Component {
group={group}
activeDrawer="rightPanel"
path={groupPath}
api={this.api}
api={api}
{...props}
/>
<AddScreen
api={this.api}
api={api}
groups={groups}
path={groupPath}
history={props.history}
@ -215,7 +201,7 @@ export default class GroupsApp extends Component {
return (
<Skeleton
history={props.history}
api={this.api}
api={api}
selectedGroups={selectedGroups}
contacts={contacts}
groups={groups}
@ -230,12 +216,12 @@ export default class GroupsApp extends Component {
defaultContacts={defaultContacts}
group={group}
path={groupPath}
api={this.api}
api={api}
selectedContact={shipPath}
{...props}
/>
<ContactCard
api={this.api}
api={api}
history={props.history}
contact={contact}
path={groupPath}
@ -268,7 +254,7 @@ export default class GroupsApp extends Component {
return (
<Skeleton
history={props.history}
api={this.api}
api={api}
selectedGroups={selectedGroups}
contacts={contacts}
groups={groups}
@ -283,12 +269,12 @@ export default class GroupsApp extends Component {
defaultContacts={defaultContacts}
group={group}
path={groupPath}
api={this.api}
api={api}
selectedContact={shipPath}
{...props}
/>
<ContactCard
api={this.api}
api={api}
history={props.history}
contact={contact}
path={groupPath}
@ -307,7 +293,7 @@ export default class GroupsApp extends Component {
return (
<Skeleton
history={props.history}
api={this.api}
api={api}
selectedGroups={selectedGroups}
contacts={contacts}
groups={groups}
@ -317,7 +303,7 @@ export default class GroupsApp extends Component {
associations={associations}
>
<ContactCard
api={this.api}
api={api}
history={props.history}
path="/~/default"
contact={me}

View File

@ -42,7 +42,7 @@ export class AddScreen extends Component {
},
awaiting: true
}, () => {
const submit = props.api.group.add(props.path, aud);
const submit = props.api.groups.add(props.path, aud);
submit.then(() => {
this.setState({ awaiting: false });
props.history.push('/~groups' + props.path);

View File

@ -141,7 +141,7 @@ export class ContactCard extends Component {
type: 'Saving to group'
},
() => {
props.api.contactHook.edit(props.path, ship, {
props.api.contacts.edit(props.path, ship, {
avatar: {
url: state.avatarToSet
}})
@ -161,7 +161,7 @@ export class ContactCard extends Component {
if (hexTest && hexTest[1] !== currentColor && !props.share) {
this.setState({ awaiting: true, type: 'Saving to group' }, () => {
props.api.contactHook.edit(
props.api.contacts.edit(
props.path, `~${props.ship}`, { color: hexTest[1] })
.then(() => {
this.setState({ awaiting: false });
@ -180,7 +180,7 @@ export class ContactCard extends Component {
const emailTestResult = emailTest.exec(state.emailToSet);
if (emailTestResult) {
this.setState({ awaiting: true, type: 'Saving to group' }, () => {
props.api.contactHook.edit(
props.api.contacts.edit(
props.path, ship, { email: state.emailToSet })
.then(() => {
this.setState({ awaiting: false });
@ -197,7 +197,7 @@ export class ContactCard extends Component {
return false;
}
this.setState({ awaiting: true, type: 'Saving to group' }, () => {
props.api.contactHook.edit(
props.api.contacts.edit(
props.path, ship, { nickname: state.nickNameToSet })
.then(() => {
this.setState({ awaiting: false });
@ -214,7 +214,7 @@ export class ContactCard extends Component {
return false;
}
this.setState({ awaiting: true, type: 'Saving to group' }, () => {
props.api.contactHook.edit(
props.api.contacts.edit(
props.path, ship, { notes: state.notesToSet })
.then(() => {
this.setState({ awaiting: false });
@ -232,7 +232,7 @@ export class ContactCard extends Component {
const phoneTestResult = phoneTest.exec(state.phoneToSet);
if (phoneTestResult) {
this.setState({ awaiting: true, type: 'Saving to group' }, () => {
props.api.contactHook.edit(
props.api.contacts.edit(
props.path, ship, { phone: state.phoneToSet })
.then(() => {
this.setState({ awaiting: false });
@ -251,7 +251,7 @@ export class ContactCard extends Component {
const websiteTestResult = websiteTest.exec(state.websiteToSet);
if (websiteTestResult) {
this.setState({ awaiting: true, type: 'Saving to group' }, () => {
props.api.contactHook.edit(
props.api.contacts.edit(
props.path, ship, { website: state.websiteToSet })
.then(() => {
this.setState({ awaiting: false });
@ -264,7 +264,7 @@ export class ContactCard extends Component {
this.setState(
{ emailToSet: '', awaiting: true, type: 'Removing from group' },
() => {
props.api.contactHook.edit(props.path, ship, { email: '' })
props.api.contacts.edit(props.path, ship, { email: '' })
.then(() => {
this.setState({ awaiting: false });
});
@ -276,7 +276,7 @@ export class ContactCard extends Component {
this.setState(
{ nicknameToSet: '', awaiting: true, type: 'Removing from group' },
() => {
props.api.contactHook.edit(props.path, ship, { nickname: '' })
props.api.contacts.edit(props.path, ship, { nickname: '' })
.then(() => {
this.setState({ awaiting: false });
});
@ -288,7 +288,7 @@ export class ContactCard extends Component {
this.setState(
{ phoneToSet: '', awaiting: true, type: 'Removing from group' },
() => {
props.api.contactHook.edit(props.path, ship, { phone: '' }).then(() => {
props.api.contacts.edit(props.path, ship, { phone: '' }).then(() => {
this.setState({ awaiting: false });
});
}
@ -299,7 +299,7 @@ export class ContactCard extends Component {
this.setState(
{ websiteToSet: '', awaiting: true, type: 'Removing from group' },
() => {
props.api.contactHook.edit(props.path, ship, { website: '' }).then(() => {
props.api.contacts.edit(props.path, ship, { website: '' }).then(() => {
this.setState({ awaiting: false });
});
}
@ -314,7 +314,7 @@ export class ContactCard extends Component {
type: 'Removing from group'
},
() => {
props.api.contactHook.edit(props.path, ship, { avatar: null }).then(() => {
props.api.contacts.edit(props.path, ship, { avatar: null }).then(() => {
this.setState({ awaiting: false });
});
}
@ -325,7 +325,7 @@ export class ContactCard extends Component {
this.setState(
{ notesToSet: '', awaiting: true, type: 'Removing from group' },
() => {
props.api.contactHook.edit(props.path, ship, { notes: '' }).then(() => {
props.api.contacts.edit(props.path, ship, { notes: '' }).then(() => {
this.setState({ awaiting: false });
});
}
@ -380,9 +380,10 @@ export class ContactCard extends Component {
};
this.setState({ awaiting: true, type: 'Sharing with group' }, () => {
props.api.contactView
props.api.contacts
.share(`~${props.ship}`, props.path, `~${window.ship}`, contact)
.then(() => {
this.setState({ awaiting: false });
props.history.push(`/~groups/view${props.path}/${window.ship}`);
});
});
@ -402,7 +403,7 @@ export class ContactCard extends Component {
avatar: null
};
props.api.contactView.share(
props.api.contacts.share(
`~${props.ship}`,
props.path,
`~${window.ship}`,
@ -410,7 +411,7 @@ export class ContactCard extends Component {
);
this.setState({ awaiting: true, type: 'Removing from group' }, () => {
props.api.contactView.delete(props.path).then(() => {
props.api.contacts.delete(props.path).then(() => {
this.setState({ awaiting: false });
props.history.push('/~groups');
});
@ -421,7 +422,7 @@ export class ContactCard extends Component {
const { props } = this;
this.setState({ awaiting: true, type: 'Removing from group' }, () => {
props.api.contactView.remove(props.path, `~${props.ship}`).then(() => {
props.api.contacts.remove(props.path, `~${props.ship}`).then(() => {
this.setState({ awaiting: false });
props.history.push(`/~groups${props.path}`);
});

View File

@ -99,7 +99,7 @@ export class ContactSidebar extends Component {
style={{ paddingTop: 6 }}
onClick={() => {
this.setState({ awaiting: true }, (() => {
props.api.group.remove(props.path, [`~${member}`])
props.api.groups.remove(props.path, [`~${member}`])
.then(() => {
this.setState({ awaiting: false });
});

View File

@ -198,7 +198,8 @@ export class GroupDetail extends Component {
onBlur={() => {
if (groupOwner) {
this.setState({ awaiting: true }, (() => {
props.api.metadata.add(
props.api.metadata.metadataAdd(
'contacts',
association['app-path'],
association['group-path'],
this.state.title,
@ -227,7 +228,8 @@ export class GroupDetail extends Component {
onBlur={() => {
if (groupOwner) {
this.setState({ awaiting: true }, (() => {
props.api.metadata.add(
props.api.metadata.metadataAdd(
'contacts',
association['app-path'],
association['group-path'],
association.metadata.title,
@ -250,7 +252,7 @@ export class GroupDetail extends Component {
onClick={() => {
if (groupOwner) {
this.setState({ awaiting: true, type: 'Deleting' }, (() => {
props.api.contactView.delete(props.path).then(() => {
props.api.contacts.delete(props.path).then(() => {
props.history.push('/~groups');
});
}));

View File

@ -3,12 +3,12 @@ import React, { Component } from 'react';
export class SidebarInvite extends Component {
onAccept() {
const { props } = this;
props.api.invite.accept(props.uid);
props.api.invite.accept('/contacts', props.uid);
props.history.push(`/~groups${props.invite.path}`);
}
onDecline() {
this.props.api.invite.decline(this.props.uid);
this.props.api.invite.decline('/contacts', this.props.uid);
}
render() {

View File

@ -67,7 +67,7 @@ export class NewScreen extends Component {
invites: '',
awaiting: true
}, () => {
props.api.contactView.create(
props.api.contacts.create(
group,
aud,
this.state.title,

View File

@ -10,50 +10,28 @@ import Tiles from './components/tiles';
import Welcome from './components/welcome';
export default class LaunchApp extends React.Component {
constructor(props) {
super(props);
this.store = new LaunchStore();
this.state = this.store.state;
this.resetControllers();
}
resetControllers() {
this.api = null;
this.subscription = null;
}
componentDidMount() {
document.title = 'OS1 - Home';
// preload spinner asset
new Image().src = '/~landscape/img/Spinner.png';
this.store.setStateHandler(this.setState.bind(this));
const channel = new this.props.channel();
this.api = new LaunchApi(this.props.ship, channel, this.store);
this.subscription = new LaunchSubscription(this.store, this.api, channel);
this.subscription.start();
}
componentWillUnmount() {
this.subscription.delete();
this.store.clear();
this.store.setStateHandler(() => {});
this.resetControllers();
}
componentWillUnmount() {}
render() {
const { state } = this;
const { props } = this;
return (
<div className='v-mid ph2 dtc-m dtc-l dtc-xl flex justify-between flex-wrap' style={{ maxWidth: '40rem' }}>
<Welcome firstTime={state.launch.firstTime} api={this.api} />
<Welcome firstTime={props.launch.firstTime} api={props.api} />
<Tiles
tiles={state.launch.tiles}
tileOrdering={state.launch.tileOrdering}
api={this.api}
location={state.location}
weather={state.weather}
tiles={props.launch.tiles}
tileOrdering={props.launch.tileOrdering}
api={props.api}
location={props.userLocation}
weather={props.weather}
/>
</div>
);

View File

@ -37,7 +37,7 @@ export default class WeatherTile extends React.Component {
this.setState({ latlng }, (err) => {
console.log(err);
}, { maximumAge: Infinity, timeout: 10000 });
this.props.api.weather(latlng);
this.props.api.launch.weather(latlng);
this.setState({ manualEntry: !this.state.manualEntry });
} else {
this.setState({ error: true });

View File

@ -22,10 +22,7 @@ import { makeRoutePath, amOwnerOfGroup, base64urlDecode } from '../../lib/util';
export class LinksApp extends Component {
constructor(props) {
super(props);
this.store = new LinksStore();
this.state = this.store.state;
this.totalUnseen = 0;
this.resetControllers();
}
componentDidMount() {
@ -33,38 +30,29 @@ export class LinksApp extends Component {
// preload spinner asset
new Image().src = '/~landscape/img/Spinner.png';
this.store.setStateHandler(this.setState.bind(this));
const channel = new this.props.channel();
this.api = new LinksApi(this.props.ship, channel, this.store);
this.subscription = new LinksSubscription(this.store, this.api, channel);
this.subscription.start();
this.props.api.links.getPage('', 0);
this.props.subscription.startApp('link');
if (!this.props.sidebarShown) {
this.props.api.local.sidebarToggle();
}
}
componentWillUnmount() {
this.subscription.delete();
this.store.clear();
this.store.setStateHandler(() => {});
this.resetControllers();
this.props.subscription.stopApp('link');
}
resetControllers() {
this.api = null;
this.subscription = null;
}
render() {
const { state, props } = this;
const { props } = this;
const contacts = state.contacts ? state.contacts : {};
const groups = state.groups ? state.groups : {};
const contacts = props.contacts ? props.contacts : {};
const groups = props.groups ? props.groups : {};
const associations = state.associations ? state.associations : { link: {}, contacts: {} };
const links = state.links ? state.links : {};
const comments = state.comments ? state.comments : {};
const associations = props.associations ? props.associations : { link: {}, contacts: {} };
const links = props.links ? props.links : {};
const comments = props.linkComments ? props.linkComments : {};
const seen = state.seen ? state.seen : {};
const seen = props.linksSeen ? props.linksSeen : {};
const totalUnseen = _.reduce(
seen,
@ -77,11 +65,15 @@ export class LinksApp extends Component {
this.totalUnseen = totalUnseen;
}
const invites = state.invites ?
state.invites : {};
const invites = props.invites ?
props.invites : {};
const selectedGroups = props.selectedGroups ? props.selectedGroups : [];
const listening = props.linkListening;
const { api, sidebarShown } = this.props;
return (
<Switch>
<Route exact path="/~link"
@ -93,11 +85,11 @@ export class LinksApp extends Component {
invites={invites}
groups={groups}
rightPanelHide={true}
sidebarShown={state.sidebarShown}
sidebarShown={sidebarShown}
selectedGroups={selectedGroups}
links={links}
listening={state.listening}
api={this.api}
listening={listening}
api={api}
>
<MessageScreen text="Select or create a collection to begin." />
</Skeleton>
@ -111,17 +103,17 @@ export class LinksApp extends Component {
associations={associations}
invites={invites}
groups={groups}
sidebarShown={state.sidebarShown}
sidebarShown={sidebarShown}
selectedGroups={selectedGroups}
links={links}
listening={state.listening}
api={this.api}
listening={listening}
api={api}
>
<NewScreen
associations={associations}
groups={groups}
contacts={contacts}
api={this.api}
api={api}
{...props}
/>
</Skeleton>
@ -134,7 +126,7 @@ export class LinksApp extends Component {
const autoJoin = () => {
try {
this.api.joinCollection(resourcePath);
api.links.joinCollection(resourcePath);
props.history.push(makeRoutePath(resourcePath));
} catch(err) {
setTimeout(autoJoin, 2000);
@ -159,14 +151,14 @@ export class LinksApp extends Component {
invites={invites}
groups={groups}
selected={resourcePath}
sidebarShown={state.sidebarShown}
sidebarShown={sidebarShown}
selectedGroups={selectedGroups}
links={links}
listening={state.listening}
api={this.api}
listening={listening}
api={api}
>
<MemberScreen
sidebarShown={state.sidebarShown}
sidebarShown={sidebarShown}
resource={resource}
contacts={contacts}
contactDetails={contactDetails}
@ -175,7 +167,7 @@ export class LinksApp extends Component {
amOwner={amOwner}
resourcePath={resourcePath}
popout={popout}
api={this.api}
api={api}
{...props}
/>
</Skeleton>
@ -198,15 +190,15 @@ export class LinksApp extends Component {
invites={invites}
groups={groups}
selected={resourcePath}
sidebarShown={state.sidebarShown}
sidebarShown={sidebarShown}
selectedGroups={selectedGroups}
popout={popout}
links={links}
listening={state.listening}
api={this.api}
listening={listening}
api={api}
>
<SettingsScreen
sidebarShown={state.sidebarShown}
sidebarShown={sidebarShown}
resource={resource}
contacts={contacts}
contactDetails={contactDetails}
@ -215,7 +207,7 @@ export class LinksApp extends Component {
amOwner={amOwner}
resourcePath={resourcePath}
popout={popout}
api={this.api}
api={api}
{...props}
/>
</Skeleton>
@ -253,13 +245,13 @@ export class LinksApp extends Component {
invites={invites}
groups={groups}
selected={resourcePath}
sidebarShown={state.sidebarShown}
sidebarShown={sidebarShown}
selectedGroups={selectedGroups}
sidebarHideMobile={true}
popout={popout}
links={links}
listening={state.listening}
api={this.api}
listening={listening}
api={api}
>
<Links
{...props}
@ -272,8 +264,8 @@ export class LinksApp extends Component {
resource={resource}
amOwner={amOwner}
popout={popout}
sidebarShown={state.sidebarShown}
api={this.api}
sidebarShown={sidebarShown}
api={api}
/>
</Skeleton>
);
@ -311,13 +303,13 @@ export class LinksApp extends Component {
invites={invites}
groups={groups}
selected={resourcePath}
sidebarShown={state.sidebarShown}
sidebarShown={sidebarShown}
selectedGroups={selectedGroups}
sidebarHideMobile={true}
popout={popout}
links={links}
listening={state.listening}
api={this.api}
listening={listening}
api={api}
>
<LinkDetail
{...props}
@ -330,11 +322,11 @@ export class LinksApp extends Component {
groupPath={resource['group-path']}
amOwner={amOwner}
popout={popout}
sidebarShown={state.sidebarShown}
sidebarShown={sidebarShown}
data={data}
comments={coms}
commentPage={commentPage}
api={this.api}
api={api}
/>
</Skeleton>
);

View File

@ -16,7 +16,7 @@ export class Comments extends Component {
this.props.comments.local[page]
) {
this.setState({ requested: this.props.commentPage });
this.props.api.getCommentsPage(
this.props.api.links.getCommentsPage(
this.props.resourcePath,
this.props.url,
this.props.commentPage);

View File

@ -34,7 +34,7 @@ export class InviteElement extends Component {
success: true,
members: []
}, () => {
props.api.inviteToCollection(props.resourcePath, aud).then(() => {
props.api.links.inviteToCollection(props.resourcePath, aud).then(() => {
this.setState({ awaiting: false });
});
});

View File

@ -34,7 +34,7 @@ export class LinkItem extends Component {
}
markPostAsSeen() {
this.props.api.seenLink(this.props.resourcePath, this.props.url);
this.props.api.links.seenLink(this.props.resourcePath, this.props.url);
}
render() {

View File

@ -21,7 +21,7 @@ export class LinkSubmit extends Component {
? this.state.linkTitle
: this.state.linkValue;
this.setState({ disabled: true });
this.props.api.postLink(this.props.resourcePath, link, title).then((r) => {
this.props.api.links.postLink(this.props.resourcePath, link, title).then((r) => {
this.setState({
disabled: false,
linkValue: '',

View File

@ -1,11 +1,11 @@
import React, { Component } from 'react';
export class SidebarInvite extends Component {
onAccept() {
this.props.api.invite.accept(this.props.uid);
this.props.api.invite.accept('/link', this.props.uid);
}
onDecline() {
this.props.api.invite.decline(this.props.uid);
this.props.api.invite.decline('/link', this.props.uid);
}
render() {

View File

@ -36,7 +36,7 @@ export class LinkDetail extends Component {
componentDidUpdate(prevProps) {
// if we have no preloaded data, and we aren't expecting it, get it
if ((!this.state.data.title) && (this.props.api)) {
this.props.api?.getSubmission(
this.props.api?.links.getSubmission(
this.props.resourcePath, this.props.url, this.updateData.bind(this)
);
}
@ -69,7 +69,7 @@ export class LinkDetail extends Component {
pending.add(this.state.comment);
this.setState({ pending: pending, disabled: true });
this.props.api.postComment(
this.props.api.links.postComment(
this.props.resourcePath,
url,
this.state.comment

View File

@ -35,7 +35,7 @@ export class Links extends Component {
!this.props.links[linkPage] || // don't have info?
this.props.links.local[linkPage] // waiting on post confirmation?
) {
this.props.api?.getPage(this.props.resourcePath, this.props.page);
this.props.api?.links.getPage(this.props.resourcePath, this.props.page);
}
}

View File

@ -119,7 +119,7 @@ export class NewScreen extends Component {
ships: [],
disabled: true
}, () => {
const submit = props.api.createCollection(
const submit = props.api.links.createCollection(
appPath,
state.title,
state.description,

View File

@ -96,6 +96,7 @@ export class SettingsScreen extends Component {
if (props.amOwner) {
this.setState({ disabled: true });
props.api.metadataAdd(
'link',
props.resourcePath,
props.groupPath,
resource.metadata.title,
@ -117,7 +118,7 @@ export class SettingsScreen extends Component {
disabled: true,
type: 'Removing'
});
props.api.removeCollection(props.resourcePath)
props.api.links.removeCollection(props.resourcePath)
.then(() => {
this.setState({
isLoading: false
@ -133,7 +134,7 @@ export class SettingsScreen extends Component {
disabled: true,
type: 'Deleting'
});
props.api.deleteCollection(props.resourcePath)
props.api.links.deleteCollection(props.resourcePath)
.then(() => {
this.setState({
isLoading: false
@ -142,7 +143,7 @@ export class SettingsScreen extends Component {
}
markAllAsSeen() {
this.props.api.seenLink(this.props.resourcePath);
this.props.api.links.seenLink(this.props.resourcePath);
}
renderRemove() {
@ -208,7 +209,8 @@ export class SettingsScreen extends Component {
onBlur={() => {
if (props.amOwner) {
this.setState({ disabled: true });
props.api.metadataAdd(
props.api.metadata.metadataAdd(
'link',
props.resourcePath,
props.groupPath,
state.title,
@ -238,7 +240,8 @@ export class SettingsScreen extends Component {
onBlur={() => {
if (props.amOwner) {
this.setState({ disabled: true });
props.api.metadataAdd(
props.api.metadata.metadataAdd(
'link',
props.resourcePath,
props.groupPath,
resource.metadata.title,

View File

@ -19,15 +19,7 @@ import { EditPost } from './components/lib/edit-post';
export default class PublishApp extends React.Component {
constructor(props) {
super(props);
this.store = new PublishStore();
this.state = this.store.state;
this.unreadTotal = 0;
this.resetControllers();
}
resetControllers() {
this.api = null;
this.subscription = null;
}
componentDidMount() {
@ -35,31 +27,28 @@ export default class PublishApp extends React.Component {
// preload spinner asset
new Image().src = '/~landscape/img/Spinner.png';
this.store.setStateHandler(this.setState.bind(this));
this.props.subscription.startApp('publish');
const channel = new this.props.channel();
this.api = new PublishApi(this.props.ship, channel, this.store);
this.props.api.publish.fetchNotebooks();
if (!this.props.sidebarShown) {
this.props.api.local.sidebarToggle();
}
this.subscription = new PublishSubscription(this.store, this.api, channel);
this.subscription.start();
this.api.fetchNotebooks();
}
componentWillUnmount() {
this.subscription.delete();
this.store.clear();
this.store.setStateHandler(() => {});
this.resetControllers();
this.props.subscription.stopApp('publish');
}
render() {
const { state, props } = this;
const { props } = this;
const contacts = state.contacts ? state.contacts : {};
const associations = state.associations ? state.associations : { contacts: {} };
const contacts = props.contacts ? props.contacts : {};
const associations = props.associations ? props.associations : { contacts: {} };
const selectedGroups = props.selectedGroups ? props.selectedGroups : [];
const notebooks = state.notebooks ? state.notebooks : {};
const notebooks = props.notebooks ? props.notebooks : {};
const unreadTotal = _.chain(notebooks)
.values()
@ -80,6 +69,8 @@ export default class PublishApp extends React.Component {
this.unreadTotal = unreadTotal;
}
const { api, groups, permissions, sidebarShown } = props;
return (
<Switch>
<Route exact path="/~publish"
@ -90,12 +81,12 @@ export default class PublishApp extends React.Component {
active={'sidebar'}
rightPanelHide={true}
sidebarShown={true}
invites={state.invites}
invites={props.invites}
notebooks={notebooks}
associations={associations}
selectedGroups={selectedGroups}
contacts={contacts}
api={this.api}
api={api}
>
<div className={`h-100 w-100 overflow-x-hidden flex flex-column
bg-white bg-gray0-d dn db-ns`}
@ -117,20 +108,20 @@ export default class PublishApp extends React.Component {
popout={false}
active={'rightPanel'}
rightPanelHide={false}
sidebarShown={state.sidebarShown}
invites={state.invites}
sidebarShown={sidebarShown}
invites={props.invites}
notebooks={notebooks}
associations={associations}
selectedGroups={selectedGroups}
contacts={contacts}
api={this.api}
api={api}
>
<NewScreen
associations={associations.contacts}
notebooks={notebooks}
groups={state.groups}
groups={groups}
contacts={contacts}
api={this.api}
api={api}
{...props}
/>
</Skeleton>
@ -146,19 +137,19 @@ export default class PublishApp extends React.Component {
popout={false}
active={'rightPanel'}
rightPanelHide={false}
sidebarShown={state.sidebarShown}
invites={state.invites}
sidebarShown={sidebarShown}
invites={props.invites}
notebooks={notebooks}
associations={associations}
selectedGroups={selectedGroups}
contacts={contacts}
api={this.api}
api={api}
>
<JoinScreen
notebooks={notebooks}
ship={ship}
notebook={notebook}
api={this.api}
api={api}
{...props}
/>
</Skeleton>
@ -190,22 +181,22 @@ export default class PublishApp extends React.Component {
popout={popout}
active={'rightPanel'}
rightPanelHide={false}
sidebarShown={state.sidebarShown}
invites={state.invites}
sidebarShown={sidebarShown}
invites={props.invites}
notebooks={notebooks}
associations={associations}
selectedGroups={selectedGroups}
contacts={contacts}
path={path}
api={this.api}
api={api}
>
<NewPost
notebooks={notebooks}
ship={ship}
book={notebook}
sidebarShown={state.sidebarShown}
sidebarShown={sidebarShown}
popout={popout}
api={this.api}
api={api}
{...props}
/>
</Skeleton>
@ -216,28 +207,28 @@ export default class PublishApp extends React.Component {
popout={popout}
active={'rightPanel'}
rightPanelHide={false}
sidebarShown={state.sidebarShown}
invites={state.invites}
sidebarShown={sidebarShown}
invites={props.invites}
notebooks={notebooks}
associations={associations}
contacts={contacts}
selectedGroups={selectedGroups}
path={path}
api={this.api}
api={api}
>
<Notebook
notebooks={notebooks}
view={view}
ship={ship}
book={notebook}
groups={state.groups}
groups={groups}
contacts={contacts}
notebookContacts={notebookContacts}
associations={associations.contacts}
sidebarShown={state.sidebarShown}
sidebarShown={sidebarShown}
popout={popout}
permissions={state.permissions}
api={this.api}
permissions={permissions}
api={api}
{...props}
/>
</Skeleton>
@ -256,7 +247,7 @@ export default class PublishApp extends React.Component {
const bookGroupPath =
notebooks?.[ship]?.[notebook]?.['subscribers-group-path'];
const notebookContacts = (bookGroupPath in state.contacts)
const notebookContacts = (bookGroupPath in contacts)
? contacts[bookGroupPath] : {};
const edit = Boolean(props.match.params.edit) || false;
@ -267,23 +258,23 @@ export default class PublishApp extends React.Component {
popout={popout}
active={'rightPanel'}
rightPanelHide={false}
sidebarShown={state.sidebarShown}
invites={state.invites}
sidebarShown={sidebarShown}
invites={props.invites}
notebooks={notebooks}
selectedGroups={selectedGroups}
associations={associations}
contacts={contacts}
path={path}
api={this.api}
api={api}
>
<EditPost
notebooks={notebooks}
book={notebook}
note={note}
ship={ship}
sidebarShown={state.sidebarShown}
sidebarShown={sidebarShown}
popout={popout}
api={this.api}
api={api}
{...props}
/>
</Skeleton>
@ -294,25 +285,25 @@ export default class PublishApp extends React.Component {
popout={popout}
active={'rightPanel'}
rightPanelHide={false}
sidebarShown={state.sidebarShown}
invites={state.invites}
sidebarShown={sidebarShown}
invites={props.invites}
notebooks={notebooks}
associations={associations}
selectedGroups={selectedGroups}
contacts={contacts}
path={path}
api={this.api}
api={api}
>
<Note
notebooks={notebooks}
book={notebook}
groups={state.groups}
groups={groups}
contacts={notebookContacts}
ship={ship}
note={note}
sidebarShown={state.sidebarShown}
sidebarShown={sidebarShown}
popout={popout}
api={this.api}
api={api}
{...props}
/>
</Skeleton>

View File

@ -53,7 +53,7 @@ export class Comments extends Component {
this.textArea.value = '';
this.setState({ commentBody: '', awaiting: 'new' });
const submit = this.props.api.publishAction(comment);
const submit = this.props.api.publish.publishAction(comment);
submit.then(() => {
this.setState({ awaiting: null });
});
@ -87,11 +87,11 @@ export class Comments extends Component {
this.setState({ awaiting: 'edit' });
window.api
this.props.api.publish
.publishAction(comment)
.then(() => {
this.setState({ awaiting: null, editing: null });
});
this.setState({ awaiting: null, editing: null });
});
}
commentDelete(idx) {
@ -106,7 +106,7 @@ export class Comments extends Component {
};
this.setState({ awaiting: { kind: 'del', what: idx } });
window.api
this.props.api.publish
.publishAction(comment)
.then(() => {
this.setState({ awaiting: null });

View File

@ -55,7 +55,7 @@ export class EditPost extends Component {
}
};
this.setState({ awaiting: true });
this.props.api.publishAction(editNote).then(() => {
this.props.api.publish.publishAction(editNote).then(() => {
const editIndex = props.location.pathname.indexOf('/edit');
const noteHref = props.location.pathname.slice(0, editIndex);
this.setState({ awaiting: false });

View File

@ -95,7 +95,7 @@ export class JoinScreen extends Component {
// TODO: askHistory setting
this.setState({ disable: true });
this.props.api.publishAction(actionData).catch((err) => {
this.props.api.publish.publishAction(actionData).catch((err) => {
console.log(err);
}).then(() => {
this.setState({ awaiting: text });

View File

@ -37,14 +37,14 @@ export class NewPost extends Component {
};
this.setState({ disabled: true });
this.props.api.publishAction(newNote).then(() => {
this.props.api.publish.publishAction(newNote).then(() => {
this.setState({ awaiting: newNote['new-note'].note });
}).catch((err) => {
if (err.includes('note already exists')) {
const timestamp = Math.floor(Date.now() / 1000);
newNote['new-note'].note += '-' + timestamp;
this.setState({ awaiting: newNote['new-note'].note });
this.props.api.publishAction(newNote);
this.props.api.publish.publishAction(newNote);
} else {
this.setState({ disabled: false, awaiting: null });
}
@ -58,7 +58,7 @@ export class NewPost extends Component {
componentDidUpdate(prevProps) {
if (prevProps && prevProps.api !== this.props.api) {
this.props.api.fetchNotebook(this.props.ship, this.props.book);
this.props.api.publish.fetchNotebook(this.props.ship, this.props.book);
}
const notebook = this.props.notebooks[this.props.ship][this.props.book];

View File

@ -93,7 +93,7 @@ export class NewScreen extends Component {
}
};
this.setState({ awaiting: bookId, disabled: true }, () => {
props.api.publishAction(action).then(() => {
props.api.publish.publishAction(action).then(() => {
});
});
}

View File

@ -49,7 +49,7 @@ export class Note extends Component {
const { props } = this;
if ((prevProps && prevProps.api !== props.api) || props.api) {
if (!(props.notebooks[props.ship]?.[props.book]?.notes?.[props.note]?.file)) {
props.api.fetchNote(props.ship, props.book, props.note);
props.api.publish.fetchNote(props.ship, props.book, props.note);
}
if (prevProps) {
@ -63,7 +63,7 @@ export class Note extends Component {
note: props.note
}
};
props.api.publishAction(readAction);
props.api.publish.publishAction(readAction);
}
}
}
@ -92,7 +92,7 @@ export class Note extends Component {
const fullyLoaded = (loadedComments === allComments);
if (atBottom && !fullyLoaded) {
this.props.api.fetchCommentsPage(this.props.ship,
this.props.api.publish.fetchCommentsPage(this.props.ship,
this.props.book, this.props.note, loadedComments, 30);
}
}
@ -109,7 +109,7 @@ export class Note extends Component {
const popout = (props.popout) ? 'popout/' : '';
const baseUrl = `/~publish/${popout}notebook/${props.ship}/${props.book}`;
this.setState({ deleting: true });
this.props.api.publishAction(deleteAction)
this.props.api.publish.publishAction(deleteAction)
.then(() => {
props.history.push(baseUrl);
});

View File

@ -25,7 +25,7 @@ export class Notebook extends Component {
atBottom = true;
}
if (!notebook.notes && this.props.api) {
this.props.api.fetchNotebook(this.props.ship, this.props.book);
this.props.api.publish.fetchNotebook(this.props.ship, this.props.book);
return;
}
@ -35,7 +35,7 @@ export class Notebook extends Component {
const fullyLoaded = (loadedNotes === allNotes);
if (atBottom && !fullyLoaded) {
this.props.api.fetchNotesPage(this.props.ship, this.props.book, loadedNotes, 30);
this.props.api.publish.fetchNotesPage(this.props.ship, this.props.book, loadedNotes, 30);
}
}
@ -44,7 +44,7 @@ export class Notebook extends Component {
if ((prevProps && (prevProps.api !== props.api)) || props.api) {
const notebook = props.notebooks?.[props.ship]?.[props.book];
if (!notebook?.subscribers) {
props.api.fetchNotebook(props.ship, props.book);
props.api.publish.fetchNotebook(props.ship, props.book);
}
}
}
@ -64,7 +64,7 @@ export class Notebook extends Component {
book: this.props.book
}
};
this.props.api.publishAction(action);
this.props.api.publish.publishAction(action);
this.props.history.push('/~publish');
}

View File

@ -63,7 +63,7 @@ export class Settings extends Component {
changeComments() {
this.setState({ comments: !this.state.comments, disabled: true }, (() => {
this.props.api.publishAction({
this.props.api.publish.publishAction({
'edit-book': {
book: this.props.book,
title: this.props.notebook.title,
@ -84,7 +84,7 @@ export class Settings extends Component {
}
};
this.setState({ disabled: true, type: 'Deleting' });
this.props.api.publishAction(action).then(() => {
this.props.api.publish.publishAction(action).then(() => {
this.props.history.push('/~publish');
});
}
@ -108,7 +108,7 @@ export class Settings extends Component {
disabled: true,
type: 'Converting'
}, (() => {
this.props.api.publishAction({
this.props.api.publish.publishAction({
groupify: {
book: props.book,
target: state.targetGroup,
@ -253,7 +253,7 @@ export class Settings extends Component {
disabled={this.state.disabled}
onBlur={() => {
this.setState({ disabled: true });
this.props.api
this.props.api.publish
.publishAction({
'edit-book': {
book: this.props.book,
@ -280,7 +280,7 @@ export class Settings extends Component {
onChange={this.changeDescription}
onBlur={() => {
this.setState({ disabled: true });
this.props.api
this.props.api.publish
.publishAction({
'edit-book': {
book: this.props.book,

View File

@ -2,23 +2,11 @@ import React, { Component } from 'react';
export class SidebarInvite extends Component {
onAccept() {
const action = {
accept: {
path: '/publish',
uid: this.props.uid
}
};
this.props.api.inviteAction(action);
this.props.api.invite.accept('/publish', this.props.uid);
}
onDecline() {
const action = {
decline: {
path: '/publish',
uid: this.props.uid
}
};
this.props.api.inviteAction(action);
this.props.api.invite.decline('/publish', this.props.uid);
}
render() {

View File

@ -11,23 +11,11 @@ export class Subscribers extends Component {
}
addUser(who, path) {
const action = {
add: {
members: [who],
path: path
}
};
this.props.api.groupAction(action);
this.props.api.groups.add(path, [who]);
}
removeUser(who, path) {
const action = {
remove: {
members: [who],
path: path
}
};
this.props.api.groupAction(action);
this.props.api.groups.remove(path, [who]);
}
redirect(url) {

View File

@ -26,7 +26,7 @@ export default class GroupFilter extends Component {
const selected = localStorage.getItem('urbit-selectedGroups');
if (selected) {
this.setState({ selected: JSON.parse(selected) }, (() => {
this.props.api.setSelected(this.state.selected);
this.props.api.local.setSelected(this.state.selected);
}));
}
}

View File

@ -15,7 +15,7 @@ export class SidebarSwitcher extends Component {
<a
className='pointer flex-shrink-0'
onClick={() => {
this.props.api.sidebarToggle();
this.props.api.local.sidebarToggle();
}}
>
<img

View File

@ -1,8 +1,14 @@
import _ from 'lodash';
import { StoreState } from '../store/type';
import { Cage } from '../types/cage';
import { ChatUpdate } from '../types/chat-update';
import { ChatHookUpdate } from '../types/chat-hook-update';
export default class ChatReducer {
reduce(json, state) {
let data = _.get(json, 'chat-update', false);
type ChatState = Pick<StoreState, 'chatInitialized' | 'chatSynced' | 'inbox' | 'pendingMessages'>;
export default class ChatReducer<S extends ChatState> {
reduce(json: Cage, state: S) {
const data = json['chat-update'];
if (data) {
this.initial(data, state);
this.pending(data, state);
@ -13,13 +19,13 @@ export default class ChatReducer {
this.delete(data, state);
}
data = _.get(json, 'chat-hook-update', false);
if (data) {
this.hook(data, state);
const hookUpdate = json['chat-hook-update'];
if (hookUpdate) {
this.hook(hookUpdate, state);
}
}
initial(json, state) {
initial(json: ChatUpdate, state: S) {
const data = _.get(json, 'initial', false);
if (data) {
state.inbox = data;
@ -27,11 +33,11 @@ export default class ChatReducer {
}
}
hook(json, state) {
hook(json: ChatHookUpdate, state: S) {
state.chatSynced = json;
}
message(json, state) {
message(json: ChatUpdate, state: S) {
const data = _.get(json, 'message', false);
if (data) {
state.inbox[data.path].envelopes.unshift(data.envelope);
@ -40,7 +46,7 @@ export default class ChatReducer {
}
}
messages(json, state) {
messages(json: ChatUpdate, state: S) {
const data = _.get(json, 'messages', false);
if (data) {
state.inbox[data.path].envelopes =
@ -48,7 +54,7 @@ export default class ChatReducer {
}
}
read(json, state) {
read(json: ChatUpdate, state: S) {
const data = _.get(json, 'read', false);
if (data) {
state.inbox[data.path].config.read =
@ -56,7 +62,7 @@ export default class ChatReducer {
}
}
create(json, state) {
create(json: ChatUpdate, state: S) {
const data = _.get(json, 'create', false);
if (data) {
state.inbox[data.path] = {
@ -69,25 +75,25 @@ export default class ChatReducer {
}
}
delete(json, state) {
delete(json: ChatUpdate, state: S) {
const data = _.get(json, 'delete', false);
if (data) {
delete state.inbox[data.path];
}
}
pending(json, state) {
pending(json: ChatUpdate, state: S) {
const msg = _.get(json, 'message', false);
if (!msg || !state.pendingMessages.has(msg.path)) {
return;
}
const mailbox = state.pendingMessages.get(msg.path);
const mailbox = state.pendingMessages.get(msg.path) || [];
for (const pendingMsg of mailbox) {
if (msg.envelope.uid === pendingMsg.uid) {
const index = mailbox.indexOf(pendingMsg);
state.pendingMessages.get(msg.path).splice(index, 1);
mailbox.splice(index, 1);
}
}
}

View File

@ -1,7 +1,12 @@
import _ from 'lodash';
import { StoreState } from '../store/type';
import { Cage } from '../types/cage';
import { ContactUpdate } from '../types/contact-update';
export default class ContactReducer {
reduce(json, state) {
type ContactState = Pick<StoreState, 'contacts'>;
export default class ContactReducer<S extends ContactState> {
reduce(json: Cage, state: S) {
const data = _.get(json, 'contact-update', false);
if (data) {
this.initial(data, state);
@ -13,28 +18,28 @@ export default class ContactReducer {
}
}
initial(json, state) {
initial(json: ContactUpdate, state: S) {
const data = _.get(json, 'initial', false);
if (data) {
state.contacts = data;
}
}
create(json, state) {
create(json: ContactUpdate, state: S) {
const data = _.get(json, 'create', false);
if (data) {
state.contacts[data.path] = {};
}
}
delete(json, state) {
delete(json: ContactUpdate, state: S) {
const data = _.get(json, 'delete', false);
if (data) {
delete state.contacts[data.path];
}
}
add(json, state) {
add(json: ContactUpdate, state: S) {
const data = _.get(json, 'add', false);
if (
data &&
@ -44,7 +49,7 @@ export default class ContactReducer {
}
}
remove(json, state) {
remove(json: ContactUpdate, state: S) {
const data = _.get(json, 'remove', false);
if (
data &&
@ -55,7 +60,7 @@ export default class ContactReducer {
}
}
edit(json, state) {
edit(json: ContactUpdate, state: S) {
const data = _.get(json, 'edit', false);
if (
data &&

View File

@ -1,8 +1,13 @@
import _ from 'lodash';
import { StoreState } from '../store/type';
import { Cage } from '../types/cage';
import { GroupUpdate } from '../types/group-update';
export default class GroupReducer {
type GroupState = Pick<StoreState, 'groups' | 'groupKeys'>;
reduce(json, state) {
export default class GroupReducer<S extends GroupState> {
reduce(json: Cage, state: S) {
const data = _.get(json, "group-update", false);
if (data) {
this.initial(data, state);
@ -15,7 +20,7 @@ export default class GroupReducer {
}
}
initial(json, state) {
initial(json: GroupUpdate, state: S) {
const data = _.get(json, 'initial', false);
if (data) {
for (let group in data) {
@ -24,7 +29,7 @@ export default class GroupReducer {
}
}
add(json, state) {
add(json: GroupUpdate, state: S) {
const data = _.get(json, 'add', false);
if (data) {
for (const member of data.members) {
@ -33,7 +38,7 @@ export default class GroupReducer {
}
}
remove(json, state) {
remove(json: GroupUpdate, state: S) {
const data = _.get(json, 'remove', false);
if (data) {
for (const member of data.members) {
@ -42,21 +47,21 @@ export default class GroupReducer {
}
}
bundle(json, state) {
bundle(json: GroupUpdate, state: S) {
const data = _.get(json, 'bundle', false);
if (data) {
state.groups[data.path] = new Set();
}
}
unbundle(json, state) {
unbundle(json: GroupUpdate, state: S) {
const data = _.get(json, 'unbundle', false);
if (data) {
delete state.groups[data.path];
}
}
keys(json, state) {
keys(json: GroupUpdate, state: S) {
const data = _.get(json, 'keys', false);
if (data) {
state.groupKeys = new Set(data.keys);

View File

@ -1,10 +1,15 @@
import _ from 'lodash';
import { StoreState } from '../store/type';
import { Cage } from '../types/cage';
import { InviteUpdate } from '../types/invite-update';
export default class InviteReducer {
reduce(json, state) {
const data = _.get(json, 'invite-update', false);
type InviteState = Pick<StoreState, "invites">;
export default class InviteReducer<S extends InviteState> {
reduce(json: Cage, state: S) {
const data = json['invite-update'];
if (data) {
console.log(data);
this.initial(data, state);
this.create(data, state);
this.delete(data, state);
@ -14,35 +19,35 @@ export default class InviteReducer {
}
}
initial(json, state) {
initial(json: InviteUpdate, state: S) {
const data = _.get(json, 'initial', false);
if (data) {
state.invites = data;
}
}
create(json, state) {
create(json: InviteUpdate, state: S) {
const data = _.get(json, 'create', false);
if (data) {
state.invites[data.path] = {};
}
}
delete(json, state) {
delete(json: InviteUpdate, state: S) {
const data = _.get(json, 'delete', false);
if (data) {
delete state.invites[data.path];
}
}
invite(json, state) {
invite(json: InviteUpdate, state: S) {
const data = _.get(json, 'invite', false);
if (data) {
state.invites[data.path][data.uid] = data.invite;
}
}
accepted(json, state) {
accepted(json: InviteUpdate, state: S) {
const data = _.get(json, 'accepted', false);
if (data) {
console.log(data);
@ -50,7 +55,7 @@ export default class InviteReducer {
}
}
decline(json, state) {
decline(json: InviteUpdate, state: S) {
const data = _.get(json, 'decline', false);
if (data) {
delete state.invites[data.path][data.uid];

View File

@ -1,7 +1,12 @@
import _ from 'lodash';
import { LaunchUpdate } from '../types/launch-update';
import { Cage } from '../types/cage';
import { StoreState } from '../store/type';
export default class LaunchReducer {
reduce(json, state) {
type LaunchState = Pick<StoreState, 'launch' | 'weather' | 'userLocation'>;
export default class LaunchReducer<S extends LaunchState> {
reduce(json: Cage, state: S) {
const data = _.get(json, 'launch-update', false);
if (data) {
this.initial(data, state);
@ -18,32 +23,32 @@ export default class LaunchReducer {
const locationData = _.get(json, 'location', false);
if (locationData) {
state.location = locationData;
state.userLocation = locationData;
}
}
initial(json, state) {
initial(json: LaunchUpdate, state: S) {
const data = _.get(json, 'initial', false);
if (data) {
state.launch = data;
}
}
changeFirstTime(json, state) {
changeFirstTime(json: LaunchUpdate, state: S) {
const data = _.get(json, 'changeFirstTime', false);
if (data) {
state.launch.firstTime = data;
}
}
changeOrder(json, state) {
changeOrder(json: LaunchUpdate, state: S) {
const data = _.get(json, 'changeOrder', false);
if (data) {
state.launch.tileOrdering = data;
}
}
changeIsShown(json, state) {
changeIsShown(json: LaunchUpdate, state: S) {
const data = _.get(json, 'changeIsShown', false);
console.log(json, data);
if (data) {

View File

@ -1,20 +1,28 @@
import _ from 'lodash';
import { StoreState } from '../store/type';
import { LinkUpdate, Pagination } from '../types/link-update';
// page size as expected from link-view.
// must change in parallel with the +page-size in /app/link-view to
// ensure sane behavior.
const PAGE_SIZE = 25;
export default class LinkUpdateReducer {
reduce(json, state) {
this.submissionsPage(json, state);
this.submissionsUpdate(json, state);
this.discussionsPage(json, state);
this.discussionsUpdate(json, state);
this.observationUpdate(json, state);
type LinkState = Pick<StoreState, 'linksSeen' | 'links' | 'linkListening' | 'linkComments'>;
export default class LinkUpdateReducer<S extends LinkState> {
reduce(json: any, state: S) {
const data = _.get(json, 'link-update', false);
if(data) {
this.submissionsPage(data, state);
this.submissionsUpdate(data, state);
this.discussionsPage(data, state);
this.discussionsUpdate(data, state);
this.observationUpdate(data, state);
}
}
submissionsPage(json, state) {
submissionsPage(json: LinkUpdate, state: S) {
const data = _.get(json, 'initial-submissions', false);
if (data) {
// { "initial-submissions": {
@ -32,7 +40,12 @@ export default class LinkUpdateReducer {
// if we didn't have any state for this path yet, initialize.
if (!state.links[path]) {
state.links[path] = { local: {} };
state.links[path] = {
local: {},
totalItems: here.totalItems,
totalPages: here.totalPages,
unseenCount: here.unseenCount
};
}
// since data contains an up-to-date full version of the page,
@ -47,17 +60,17 @@ export default class LinkUpdateReducer {
// write seen status to a separate structure,
// for easier modification later.
if (!state.seen[path]) {
state.seen[path] = {};
if (!state.linksSeen[path]) {
state.linksSeen[path] = {};
}
(here.page || []).map((submission) => {
state.seen[path][submission.url] = submission.seen;
state.linksSeen[path][submission.url] = submission.seen;
});
}
}
}
submissionsUpdate(json, state) {
submissionsUpdate(json: LinkUpdate, state: S) {
const data = _.get(json, 'submissions', false);
if (data) {
// { "submissions": {
@ -70,7 +83,7 @@ export default class LinkUpdateReducer {
// stub in a comment count, which is more or less guaranteed to be 0
data.pages = data.pages.map((submission) => {
submission.commentCount = 0;
state.seen[path][submission.url] = false;
state.linksSeen[path][submission.url] = false;
return submission;
});
@ -83,7 +96,7 @@ export default class LinkUpdateReducer {
}
}
discussionsPage(json, state) {
discussionsPage(json: LinkUpdate, state: S) {
const data = _.get(json, 'initial-discussions', false);
if (data) {
// { "initial-discussions": {
@ -100,13 +113,17 @@ export default class LinkUpdateReducer {
const page = data.pageNumber;
// if we didn't have any state for this path yet, initialize.
if (!state.comments[path]) {
state.comments[path] = {};
if (!state.linkComments[path]) {
state.linkComments[path] = {};
}
if (!state.comments[path][url]) {
state.comments[path][url] = { local: {} };
}
const here = state.comments[path][url];
let comments = {...{
local: {},
totalPages: data.totalPages,
totalItems: data.totalItems
}, ...state.linkComments[path][url] };
state.linkComments[path][url] = comments;
const here = state.linkComments[path][url];
// since data contains an up-to-date full version of the page,
// we can safely overwrite the one in state.
@ -117,7 +134,7 @@ export default class LinkUpdateReducer {
}
}
discussionsUpdate(json, state) {
discussionsUpdate(json: LinkUpdate, state: S) {
const data = _.get(json, 'discussions', false);
if (data) {
// { "discussions": {
@ -130,13 +147,13 @@ export default class LinkUpdateReducer {
const url = data.url;
// add new comments to state, update totals
state.comments[path][url] = this._addNewItems(
data.comments, state.comments[path][url]
state.linkComments[path][url] = this._addNewItems(
data.comments || [], state.linkComments[path][url]
);
}
}
observationUpdate(json, state) {
observationUpdate(json: LinkUpdate, state: S) {
const data = _.get(json, 'observation', false);
if (data) {
// { "observation": {
@ -145,10 +162,10 @@ export default class LinkUpdateReducer {
// } }
const path = data.path;
if (!state.seen[path]) {
state.seen[path] = {};
if (!state.linksSeen[path]) {
state.linksSeen[path] = {};
}
const seen = state.seen[path];
const seen = state.linksSeen[path];
// mark urls as seen
data.urls.map((url) => {
@ -163,7 +180,7 @@ export default class LinkUpdateReducer {
//
_addNewItems(items, pages, page = 0) {
_addNewItems<S extends { time: number }>(items: S[], pages: Pagination<S>, page = 0) {
if (!pages) {
pages = {
local: {},

View File

@ -1,34 +0,0 @@
import _ from 'lodash';
export default class ListenUpdateReducer {
reduce(json, state) {
const data = _.get(json, 'link-listen-update', false);
if (data) {
this.listening(data, state);
this.watch(data, state);
this.leave(data, state);
}
}
listening(json, state) {
const data = _.get(json, 'listening', false);
if (data) {
state.listening = new Set(data);
}
}
watch(json, state) {
const data = _.get(json, 'watch', false);
if (data) {
state.listening.add(data);
}
}
leave(json, state) {
const data = _.get(json, 'leave', false);
if (data) {
state.listening.delete(data);
}
}
}

View File

@ -0,0 +1,39 @@
import _ from 'lodash';
import { StoreState } from '../store/type';
import { Cage } from '../types/cage';
import { LinkListenUpdate } from '../types/link-listen-update';
type LinkListenState = Pick<StoreState, 'linkListening'>;
export default class LinkListenReducer<S extends LinkListenState> {
reduce(json: Cage, state: S) {
const data = _.get(json, 'link-listen-update', false);
if (data) {
this.listening(data, state);
this.watch(data, state);
this.leave(data, state);
}
}
listening(json: LinkListenUpdate, state: S) {
const data = _.get(json, 'listening', false);
if (data) {
state.linkListening = new Set(data);
}
}
watch(json: LinkListenUpdate, state: S) {
const data = _.get(json, 'watch', false);
if (data) {
state.linkListening.add(data);
}
}
leave(json: LinkListenUpdate, state: S) {
const data = _.get(json, 'leave', false);
if (data) {
state.linkListening.delete(data);
}
}
}

View File

@ -1,25 +0,0 @@
import _ from 'lodash';
export default class LocalReducer {
reduce(json, state) {
const data = _.get(json, 'local', false);
if (data) {
this.sidebarToggle(data, state);
this.setSelected(data, state);
}
}
sidebarToggle(obj, state) {
const data = _.has(obj, 'sidebarToggle', false);
if (data) {
state.sidebarShown = obj.sidebarToggle;
}
}
setSelected(obj, state) {
const data = _.has(obj, 'selected', false);
if (data) {
state.selectedGroups = obj.selected;
}
}
}

View File

@ -0,0 +1,28 @@
import _ from 'lodash';
import { StoreState } from '../store/type';
import { Cage } from '../types/cage';
import { LocalUpdate } from '../types/local-update';
type LocalState = Pick<StoreState, 'sidebarShown' | 'selectedGroups'>;
export default class LocalReducer<S extends LocalState> {
reduce(json: Cage, state: S) {
const data = json['local'];
if (data) {
this.sidebarToggle(data, state);
this.setSelected(data, state);
}
}
sidebarToggle(obj: LocalUpdate, state: S) {
if ('sidebarToggle' in obj) {
state.sidebarShown = !state.sidebarShown;
}
}
setSelected(obj: LocalUpdate, state: S) {
if ('selected' in obj) {
state.selectedGroups = obj.selected;
}
}
}

View File

@ -1,8 +1,15 @@
import _ from 'lodash';
export default class MetadataReducer {
reduce(json, state) {
let data = _.get(json, 'metadata-update', false);
import { StoreState } from '../store/type';
import { MetadataUpdate } from '../types/metadata-update';
import { Cage } from '../types/cage';
type MetadataState = Pick<StoreState, 'associations'>;
export default class MetadataReducer<S extends MetadataState> {
reduce(json: Cage, state: S) {
let data = json['metadata-update']
if (data) {
console.log('data: ', data);
this.associations(data, state);
@ -13,7 +20,7 @@ export default class MetadataReducer {
}
}
associations(json, state) {
associations(json: MetadataUpdate, state: S) {
let data = _.get(json, 'associations', false);
if (data) {
let metadata = state.associations;
@ -34,7 +41,7 @@ export default class MetadataReducer {
}
}
add(json, state) {
add(json: MetadataUpdate, state: S) {
let data = _.get(json, 'add', false);
if (data) {
let metadata = state.associations;
@ -53,7 +60,7 @@ export default class MetadataReducer {
}
}
update(json, state) {
update(json: MetadataUpdate, state: S) {
let data = _.get(json, 'update-metadata', false);
if (data) {
let metadata = state.associations;
@ -72,7 +79,7 @@ export default class MetadataReducer {
}
}
remove(json, state) {
remove(json: MetadataUpdate, state: S) {
let data = _.get(json, 'remove', false);
if (data) {
let metadata = state.associations;

View File

@ -1,7 +1,12 @@
import _ from 'lodash';
import { StoreState } from '../store/type';
import { Cage } from '../types/cage';
import { PermissionUpdate } from '../types/permission-update';
export default class PermissionReducer {
reduce(json, state) {
type PermissionState = Pick<StoreState, "permissions">;
export default class PermissionReducer<S extends PermissionState> {
reduce(json: Cage, state: S) {
const data = _.get(json, 'permission-update', false);
if (data) {
this.initial(data, state);
@ -12,7 +17,7 @@ export default class PermissionReducer {
}
}
initial(json, state) {
initial(json: PermissionUpdate, state: S) {
const data = _.get(json, 'initial', false);
if (data) {
for (const perm in data) {
@ -24,7 +29,7 @@ export default class PermissionReducer {
}
}
create(json, state) {
create(json: PermissionUpdate, state: S) {
const data = _.get(json, 'create', false);
if (data) {
state.permissions[data.path] = {
@ -34,14 +39,14 @@ export default class PermissionReducer {
}
}
delete(json, state) {
delete(json: PermissionUpdate, state: S) {
const data = _.get(json, 'delete', false);
if (data) {
delete state.permissions[data.path];
}
}
add(json, state) {
add(json: PermissionUpdate, state: S) {
const data = _.get(json, 'add', false);
if (data) {
for (const member of data.who) {
@ -50,7 +55,7 @@ export default class PermissionReducer {
}
}
remove(json, state) {
remove(json: PermissionUpdate, state: S) {
const data = _.get(json, 'remove', false);
if (data) {
for (const member of data.who) {

View File

@ -1,7 +1,11 @@
import _ from 'lodash';
import { StoreState } from '../store/type';
import { Cage } from '../types/cage';
export default class PublishResponseReducer {
reduce(json, state) {
type PublishState = Pick<StoreState, 'notebooks'>;
export default class PublishResponseReducer<S extends PublishState> {
reduce(json: Cage, state: S) {
const data = _.get(json, 'publish-response', false);
if (!data) { return; }
switch(data.type) {
@ -194,12 +198,4 @@ export default class PublishResponseReducer {
throw Error("tried to fetch paginated comments, but we don't have the note");
}
}
sidebarToggle(json, state) {
let data = _.has(json.data, 'sidebarToggle', false);
if (data) {
state.sidebarShown = json.data.sidebarToggle;
}
}
}

View File

@ -1,9 +1,21 @@
import _ from 'lodash';
export default class PublishUpdateReducer {
reduce(preJson, state){
let json = _.get(preJson, "publish-update", false);
switch(Object.keys(json)[0]){
import { PublishUpdate } from '../types/publish-update';
import { Cage } from '../types/cage';
import { StoreState } from '../store/type';
import { getTagFromFrond } from '../types/noun';
type PublishState = Pick<StoreState, 'notebooks'>;
export default class PublishUpdateReducer<S extends PublishState> {
reduce(data: Cage, state: S){
let json = data["publish-update"];
if(!json) {
return;
}
const tag = getTagFromFrond(json);
switch(tag){
case "add-book":
this.addBook(json["add-book"], state);
break;
@ -39,7 +51,7 @@ export default class PublishUpdateReducer {
}
}
addBook(json, state) {
addBook(json, state: S) {
let host = Object.keys(json)[0];
let book = Object.keys(json[host])[0];
if (state.notebooks[host]) {
@ -49,7 +61,7 @@ export default class PublishUpdateReducer {
}
}
addNote(json, state) {
addNote(json, state: S) {
let host = Object.keys(json)[0];
let book = Object.keys(json[host])[0];
let noteId = json[host][book]["note-id"];
@ -77,13 +89,13 @@ export default class PublishUpdateReducer {
let prevNoteId = state.notebooks[host][book]["notes-by-date"][1] || null;
state.notebooks[host][book].notes[noteId]["prev-note"] = prevNoteId
state.notebooks[host][book].notes[noteId]["next-note"] = null;
if (state.notebooks[host][book].notes[prevNoteId]) {
if (prevNoteId && state.notebooks[host][book].notes[prevNoteId]) {
state.notebooks[host][book].notes[prevNoteId]["next-note"] = noteId;
}
}
}
addComment(json, state) {
addComment(json, state: S) {
let host = json.host
let book = json.book
let note = json.note
@ -97,7 +109,7 @@ export default class PublishUpdateReducer {
if (state.notebooks[host][book].notes[note].comments) {
let limboCommentIdx =
_.findIndex(state.notebooks[host][book].notes[note].comments, (o) => {
let oldVal = o[Object.keys(o)[0]];
let oldVal = o[getTagFromFrond(o)];
let newVal = comment[Object.keys(comment)[0]];
return (oldVal.pending &&
(oldVal.author === newVal.author) &&

View File

@ -1,7 +1,12 @@
import _ from 'lodash';
import { StoreState } from '../store/type';
import { Cage } from '../types/cage';
import { S3Update } from '../types/s3-update';
export default class S3Reducer{
reduce(json, state) {
type S3State = Pick<StoreState, 's3'>;
export default class S3Reducer<S extends S3State> {
reduce(json: Cage, state: S) {
const data = _.get(json, 's3-update', false);
if (data) {
this.credentials(data, state);
@ -15,14 +20,14 @@ export default class S3Reducer{
}
}
credentials(json, state) {
credentials(json: S3Update, state: S) {
const data = _.get(json, 'credentials', false);
if (data) {
state.s3.credentials = data;
}
}
configuration(json, state) {
configuration(json: S3Update, state: S) {
const data = _.get(json, 'configuration', false);
if (data) {
state.s3.configuration = {
@ -32,14 +37,14 @@ export default class S3Reducer{
}
}
currentBucket(json, state) {
currentBucket(json: S3Update, state: S) {
const data = _.get(json, 'setCurrentBucket', false);
if (data) {
state.s3.configuration.currentBucket = data;
if (data && state.s3) {
}
}
addBucket(json, state) {
addBucket(json: S3Update, state: S) {
const data = _.get(json, 'addBucket', false);
if (data) {
state.s3.configuration.buckets =
@ -47,31 +52,30 @@ export default class S3Reducer{
}
}
removeBucket(json, state) {
removeBucket(json: S3Update, state: S) {
const data = _.get(json, 'removeBucket', false);
if (data) {
state.s3.configuration.buckets =
state.s3.configuration.buckets.delete(data);
state.s3.configuration.buckets.delete(data);
}
}
endpoint(json, state) {
endpoint(json: S3Update, state: S) {
const data = _.get(json, 'setEndpoint', false);
if (data) {
if (data && state.s3.credentials) {
state.s3.credentials.endpoint = data;
}
}
accessKeyId(json, state) {
accessKeyId(json: S3Update , state: S) {
const data = _.get(json, 'setAccessKeyId', false);
if (data) {
if (data && state.s3.credentials) {
state.s3.credentials.accessKeyId = data;
}
}
secretAccessKey(json, state) {
secretAccessKey(json: S3Update, state: S) {
const data = _.get(json, 'setSecretAccessKey', false);
if (data) {
if (data && state.s3.credentials) {
state.s3.credentials.secretAccessKey = data;
}
}

View File

@ -1,20 +1,21 @@
export default class BaseStore {
export default class BaseStore<S extends object> {
state: S;
setState: (s: Partial<S>) => void = (s) => {};
constructor() {
this.state = this.initialState();
this.setState = () => {};
}
initialState() {
return {};
return {} as S;
}
setStateHandler(setState) {
setStateHandler(setState: (s: Partial<S>) => void) {
this.setState = setState;
}
clear() {
this.handleEvent({
data: { clear: true }
data: { clear: true },
});
}
@ -25,7 +26,7 @@ export default class BaseStore {
return;
}
if ('clear' in json && json.clear) {
if ("clear" in json && json.clear) {
this.setState(this.initialState());
return;
}
@ -38,4 +39,3 @@ export default class BaseStore {
// extend me!
}
}

View File

@ -1,51 +0,0 @@
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,92 @@
import BaseStore from './base';
import InviteReducer from '../reducers/invite-update';
import MetadataReducer from '../reducers/metadata-update';
import LocalReducer from '../reducers/local';
import ChatReducer from '../reducers/chat-update';
import { StoreState } from './type';
import { Cage } from '../types/cage';
import ContactReducer from '../reducers/contact-update';
import LinkUpdateReducer from '../reducers/link-update';
import S3Reducer from '../reducers/s3-update';
import GroupReducer from '../reducers/group-update';
import PermissionReducer from '../reducers/permission-update';
import PublishUpdateReducer from '../reducers/publish-update';
import PublishResponseReducer from '../reducers/publish-response';
import LaunchReducer from '../reducers/launch-update';
import LinkListenReducer from '../reducers/listen-update';
export default class GlobalStore extends BaseStore<StoreState> {
inviteReducer = new InviteReducer();
metadataReducer = new MetadataReducer();
localReducer = new LocalReducer();
chatReducer = new ChatReducer();
contactReducer = new ContactReducer();
linkReducer = new LinkUpdateReducer();
linkListenReducer = new LinkListenReducer();
s3Reducer = new S3Reducer();
groupReducer = new GroupReducer();
permissionReducer = new PermissionReducer();
publishUpdateReducer = new PublishUpdateReducer();
publishResponseReducer = new PublishResponseReducer();
launchReducer = new LaunchReducer();
initialState(): StoreState {
return {
pendingMessages: new Map(),
chatInitialized: false,
sidebarShown: true,
invites: {},
associations: {
chat: {},
contacts: {},
link: {},
publish: {}
},
groups: {},
groupKeys: new Set(),
launch: {
firstTime: false,
tileOrdering: [],
tiles: {},
},
weather: {},
userLocation: null,
permissions: {},
s3: {
configuration: {
buckets: new Set(),
currentBucket: ''
},
credentials: null
},
links: {},
linksSeen: {},
linkListening: new Set(),
linkComments: {},
notebooks: {},
contacts: {},
selectedGroups: [],
inbox: {},
chatSynced: null,
};
}
reduce(data: Cage, state: StoreState) {
this.inviteReducer.reduce(data, this.state);
this.metadataReducer.reduce(data, this.state);
this.localReducer.reduce(data, this.state);
this.chatReducer.reduce(data, this.state);
this.contactReducer.reduce(data, this.state);
this.linkReducer.reduce(data, this.state);
this.s3Reducer.reduce(data, this.state);
this.groupReducer.reduce(data, this.state);
this.permissionReducer.reduce(data, this.state);
this.publishUpdateReducer.reduce(data, this.state);
this.publishResponseReducer.reduce(data, this.state);
this.launchReducer.reduce(data, this.state);
this.linkListenReducer.reduce(data, this.state);
}
}

View File

@ -0,0 +1,52 @@
import { Inbox, Envelope } from '../types/chat-update';
import { ChatHookUpdate } from '../types/chat-hook-update';
import { Path } from '../types/noun';
import { Invites } from '../types/invite-update';
import { SelectedGroup } from '../types/local-update';
import { Associations } from '../types/metadata-update';
import { Rolodex } from '../types/contact-update';
import { Notebooks } from '../types/publish-update';
import { Groups } from '../types/group-update';
import { S3State } from '../types/s3-update';
import { Permissions } from '../types/permission-update';
import { LaunchState, WeatherState } from '../types/launch-update';
import { LinkComments, LinkCollections, LinkSeen } from '../types/link-update';
export interface StoreState {
// local state
sidebarShown: boolean;
selectedGroups: SelectedGroup[];
// invite state
invites: Invites;
// metadata state
associations: Associations;
// contact state
contacts: Rolodex;
// groups state
groups: Groups;
groupKeys: Set<Path>;
permissions: Permissions;
s3: S3State;
// App specific states
// launch state
launch: LaunchState;
weather: WeatherState | {} | null;
userLocation: string | null;
// links state
linksSeen: LinkSeen;
linkListening: Set<Path>;
links: LinkCollections;
linkComments: LinkComments;
// publish state
notebooks: Notebooks;
// Chat state
chatInitialized: boolean;
chatSynced: ChatHookUpdate | null;
inbox: Inbox;
pendingMessages: Map<Path, Envelope[]>;
}

View File

@ -1,8 +1,9 @@
export default class BaseSubscription {
constructor(store, api, channel) {
this.store = store;
this.api = api;
this.channel = channel;
import BaseStore from "../store/base";
import BaseApi from "../api/base";
import { Path } from "../types/noun";
export default class BaseSubscription<S extends object> {
constructor(public store: BaseStore<S>, public api: BaseApi<S>, public channel: any) {
this.channel.setOnChannelError(this.onChannelError.bind(this));
}
@ -18,8 +19,8 @@ export default class BaseSubscription {
}, 2000);
}
subscribe(path, app) {
this.api.subscribe(path, 'PUT', this.api.ship, app,
subscribe(path: Path, app: string) {
return this.api.subscribe(path, 'PUT', this.api.ship, app,
this.handleEvent.bind(this),
(err) => {
console.log(err);
@ -30,6 +31,10 @@ export default class BaseSubscription {
});
}
unsubscribe(id: number) {
this.api.unsubscribe(id);
}
start() {
// extend
}

View File

@ -1,9 +0,0 @@
import BaseSubscription from './base';
export default class GlobalSubscription extends BaseSubscription {
start() {
this.subscribe('/all', 'invite-store');
this.subscribe('/app-name/contacts', 'metadata-store');
}
}

View File

@ -0,0 +1,68 @@
import BaseSubscription from './base';
import { StoreState } from '../store/type';
import { Path } from '../types/noun';
/**
* Path to subscribe on and app to subscribe to
*/
type AppSubscription = [Path, string];
const chatSubscriptions: AppSubscription[] = [
['/primary', 'chat-view'],
['/synced', 'chat-hook']
];
const publishSubscriptions: AppSubscription[] = [
['/primary', 'publish'],
['/all', 'group-store']
];
const linkSubscriptions: AppSubscription[] = [
['/json/seen', 'link-view'],
['/listening', 'link-listen-hook']
]
const groupSubscriptions: AppSubscription[] = [
['/all', 'group-store'],
['/synced', 'contact-hook']
];
type AppName = 'publish' | 'chat' | 'link' | 'groups';
const appSubscriptions: Record<AppName, AppSubscription[]> = {
chat: chatSubscriptions,
publish: publishSubscriptions,
link: linkSubscriptions,
groups: groupSubscriptions
};
export default class GlobalSubscription extends BaseSubscription<StoreState> {
openSubscriptions: Record<AppName, number[]> = {
chat: [],
publish: [],
link: [],
groups: []
};
start() {
this.subscribe('/all', 'invite-store');
this.subscribe('/all', 'permission-store');
this.subscribe('/primary', 'contact-view');
this.subscribe('/all', 'metadata-store');
this.subscribe('/all', 's3-store');
this.subscribe('/all', 'launch');
this.subscribe('/all', 'weather');
}
startApp(app: AppName) {
if(this.openSubscriptions[app].length > 0) {
console.log(`${app} already started`);
return;
}
this.openSubscriptions[app] = appSubscriptions[app].map(([path, agent]) => this.subscribe(path, agent));
}
stopApp(app: AppName) {
this.openSubscriptions[app].map(id => this.unsubscribe(id))
this.openSubscriptions[app] = [];
}
}

View File

@ -0,0 +1,35 @@
import { ChatUpdate } from "./chat-update";
import { ChatHookUpdate } from "./chat-hook-update";
import { ContactUpdate } from "./contact-update";
import { InviteUpdate } from "./invite-update";
import { LocalUpdate } from "./local-update";
import { MetadataUpdate } from "./metadata-update";
import { PublishUpdate } from './publish-update';
import { PublishResponse } from "./publish-response";
import { GroupUpdate } from "./group-update";
import { PermissionUpdate } from "./permission-update";
import { LaunchUpdate, WeatherState } from "./launch-update";
import { LinkListenUpdate } from './link-listen-update';
interface MarksToTypes {
readonly json: any;
readonly "chat-update": ChatUpdate;
readonly "chat-hook-update": ChatHookUpdate;
readonly "contact-update": ContactUpdate;
readonly "invite-update": InviteUpdate;
readonly "metadata-update": MetadataUpdate;
readonly 'publish-update': PublishUpdate;
readonly "publish-response": PublishResponse;
readonly "group-update": GroupUpdate;
readonly "permission-update": PermissionUpdate;
readonly "launch-update": LaunchUpdate;
readonly "link-listen-update": LinkListenUpdate;
// not really marks but w/e
readonly 'local': LocalUpdate;
readonly 'weather': WeatherState | {};
readonly 'location': string;
}
export type Cage = Partial<MarksToTypes>;
export type Mark = keyof MarksToTypes;

View File

@ -0,0 +1,5 @@
import { Patp } from './noun';
export interface ChatHookUpdate {
[p: string]: Patp;
}

View File

@ -0,0 +1,95 @@
import { Path, Patp } from './noun';
export type ChatUpdate =
ChatUpdateInitial
| ChatUpdateCreate
| ChatUpdateDelete
| ChatUpdateMessage
| ChatUpdateMessages
| ChatUpdateRead;
export type ChatAction =
ChatUpdateCreate
| ChatUpdateDelete
| ChatUpdateMessage
| ChatUpdateRead;
interface ChatUpdateInitial {
initial: Inbox;
}
interface ChatUpdateCreate {
create: Path;
}
interface ChatUpdateDelete {
delete: Path;
}
interface ChatUpdateMessage {
message: {
path: Path;
envelope: Envelope;
}
}
interface ChatUpdateMessages {
messages: {
path: Path;
envelopes: Envelope[];
}
}
interface ChatUpdateRead {
read: {
path: Path;
};
}
// Data structures
// TODO: move to seperate file?
export interface Inbox {
[chatName: string]: Mailbox;
}
interface Mailbox {
config: MailboxConfig;
envelopes: Envelope[];
}
interface MailboxConfig {
length: number;
read: number;
}
export interface Envelope {
uid: string;
number: number;
author: Patp;
when: string;
letter: Letter;
}
interface LetterText {
text: string;
}
interface LetterUrl {
url: string;
}
interface LetterCode {
code: {
expression: string;
output: string;
}
}
interface LetterMe {
narrative: string;
}
export type Letter = LetterText | LetterUrl | LetterCode | LetterMe;

View File

@ -0,0 +1,85 @@
import { Path, Patp } from "./noun";
export type ContactUpdate =
| ContactUpdateCreate
| ContactUpdateDelete
| ContactUpdateAdd
| ContactUpdateRemove
| ContactUpdateEdit
| ContactUpdateInitial
| ContactUpdateContacts;
interface ContactUpdateCreate {
create: Path;
}
interface ContactUpdateDelete {
delete: Path;
}
interface ContactUpdateAdd {
add: {
path: Path;
ship: Patp;
contact: Contact;
};
}
interface ContactUpdateRemove {
remove: {
path: Path;
ship: Patp;
};
}
interface ContactUpdateEdit {
edit: {
path: Path;
ship: Patp;
"edit-field": ContactEdit;
};
}
interface ContactUpdateInitial {
initial: Rolodex;
}
interface ContactUpdateContacts {
contacts: {
path: Path;
contacts: Contacts;
};
}
//
type ContactAvatar = ContactAvatarUrl | ContactAvatarOcts;
export type Rolodex = {
[p in Path]: Contacts;
};
export type Contacts = {
[p in Patp]: Contact;
};
interface ContactAvatarUrl {
url: string;
}
interface ContactAvatarOcts {
octs: string;
}
export interface Contact {
nickname: string;
email: string;
phone: string;
website: string;
notes: string;
color: string;
avatar: ContactAvatar | null;
}
export type ContactEdit = {
[k in keyof Contact]: Contact[k];
};

View File

@ -0,0 +1,7 @@
import { PatpNoSig } from "./noun";
declare global {
interface Window {
ship: PatpNoSig;
}
}

View File

@ -0,0 +1,59 @@
import { PatpNoSig, Path } from './noun';
export type Group = Set<PatpNoSig>
export type Groups = {
[p in Path]: Group;
}
interface GroupUpdateInitial {
initial: Groups;
}
interface GroupUpdateAdd {
add: {
members: PatpNoSig[];
path: Path;
}
}
interface GroupUpdateRemove {
remove: {
members: PatpNoSig[];
path: Path;
}
}
interface GroupUpdateBundle {
bundle: {
path: Path;
}
}
interface GroupUpdateUnbundle {
unbundle: {
path: Path;
}
}
interface GroupUpdateKeys {
keys: {
keys: Path[];
}
}
interface GroupUpdatePath {
path: {
path: Path;
members: PatpNoSig[];
}
}
export type GroupUpdate =
GroupUpdateInitial
| GroupUpdateAdd
| GroupUpdateRemove
| GroupUpdateBundle
| GroupUpdateUnbundle
| GroupUpdateKeys
| GroupUpdatePath;

View File

@ -0,0 +1,67 @@
import { Serial, PatpNoSig, Path } from './noun';
export type InviteUpdate =
InviteUpdateInitial
| InviteUpdateCreate
| InviteUpdateDelete
| InviteUpdateInvite
| InviteUpdateAccepted
| InviteUpdateDecline;
interface InviteUpdateInitial {
initial: Invites;
}
interface InviteUpdateCreate {
create: {
path: Path;
};
}
interface InviteUpdateDelete {
delete: {
path: Path;
};
}
interface InviteUpdateInvite {
invite: {
path: Path;
uid: Serial;
invite: Invite;
};
}
interface InviteUpdateAccepted {
accepted: {
path: Path;
uid: Serial;
};
}
interface InviteUpdateDecline {
decline: {
path: Path;
uid: Serial;
};
}
// actual datastructures
export type Invites = {
[p in Path]: AppInvites;
};
export type AppInvites = {
[s in Serial]: Invite;
};
export interface Invite {
app: string;
path: Path;
recipeint: PatpNoSig;
ship: PatpNoSig;
text: string;
}

View File

@ -0,0 +1,83 @@
export type LaunchUpdate =
LaunchUpdateInitial
| LaunchUpdateFirstTime
| LaunchUpdateOrder
| LaunchUpdateIsShown;
interface LaunchUpdateInitial {
initial: LaunchState;
}
interface LaunchUpdateFirstTime {
changeFirstTime: boolean;
}
interface LaunchUpdateOrder {
changeOrder: string[];
}
interface LaunchUpdateIsShown {
changeIsShown: {
name: string;
isShown: boolean;
}
}
export interface LaunchState {
firstTime: boolean;
tileOrdering: string[];
tiles: {
[app: string]: Tile;
}
}
interface Tile {
isShown: boolean;
type: TileType;
}
type TileType = TileTypeBasic | TileTypeCustom;
interface TileTypeBasic {
basic: {
iconUrl: string;
linkedUrl: string;
title: string;
}
}
interface TileTypeCustom {
custom: null;
}
interface WeatherDay {
apparentTemperature: number;
cloudCover: number;
dewPoint: number;
humidity: number;
icon: string;
ozone: number;
precipIntensity: number;
precipProbability: number;
precipType: string;
pressure: number;
summary: string;
temperature: number;
time: number;
uvIndex: number;
visibility: number;
windBearing: number;
windGust: number;
windSpeed: number;
}
export interface WeatherState {
currently: WeatherDay;
daily: {
data: WeatherDay[];
icon: string;
summary: string;
}
}

View File

@ -0,0 +1,18 @@
import { Path } from './noun';
interface LinkListenUpdateListening {
listening: Path[];
}
interface LinkListenUpdateWatch {
watch: Path;
}
interface LinkListenUpdateLeave {
leave: Path;
}
export type LinkListenUpdate =
LinkListenUpdateListening
| LinkListenUpdateWatch
| LinkListenUpdateLeave;

View File

@ -0,0 +1,84 @@
import { PatpNoSig, Path } from "./noun";
export type LinkCollections = {
[p in Path]: Collection;
};
export type LinkSeen = {
[p in Path]: {
[url: string]: boolean;
};
};
export type Pagination<S> = {
local: LocalPages;
[p: number]: S[];
totalItems: number;
totalPages: number;
}
export type LinkComments = {
[p in Path]: {
[url: string]: Pagination<LinkComment> & {
totalItems: number;
totalPages: number;
}
}
}
interface LinkComment {
ship: PatpNoSig;
time: number;
udon: string;
}
interface CollectionStats {
unseenCount: number;
}
type LocalPages = {
[p: number]: boolean;
}
type Collection = CollectionStats & Pagination<Link>;
interface Link {
commentCount: number;
seen: boolean;
ship: PatpNoSig;
time: number;
title: string;
url: string;
}
interface LinkInitialSubmissions {
'initial-submissions': {
[p in Path]: CollectionStats & {
pageNumber?: number;
pages?: Link[];
}
};
};
interface LinkUpdateSubmission {
'submissions': {
path: Path;
pages: Link[];
}
}
interface LinkInitialDiscussion {
'intitial-discussion': {
path: Path;
url: string;
page: Comment[];
totalItems: number;
totalPages: number;
pageNumber: number;
}
}
export type LinkUpdate =
LinkInitialSubmissions
| LinkUpdateSubmission
| LinkInitialDiscussion;

View File

@ -0,0 +1,15 @@
import { Path } from './noun';
export type LocalUpdate =
LocalUpdateSidebarToggle
| LocalUpdateSelectedGroups;
interface LocalUpdateSidebarToggle {
sidebarToggle: boolean;
}
interface LocalUpdateSelectedGroups {
selected: SelectedGroup[];
}
export type SelectedGroup = [Path, string];

View File

@ -0,0 +1,54 @@
import { AppName, Path, Patp } from './noun';
export type MetadataUpdate =
MetadataUpdateInitial
| MetadataUpdateAdd
| MetadataUpdateUpdate
| MetadataUpdateRemove;
interface MetadataUpdateInitial {
associations: ResourceAssociations;
}
type ResourceAssociations = {
[p in Path]: Association;
}
type MetadataUpdateAdd = {
add: Association;
}
type MetadataUpdateUpdate = {
update: Association;
}
type MetadataUpdateRemove = {
remove: Resource & {
'group-path': Path;
}
}
export type Associations = Record<AppName, AppAssociations>;
type AppAssociations = {
[p in Path]: Association;
}
interface Resource {
'app-path': Path;
'app-name': AppName;
}
export type Association = Resource & {
'group-path': Path;
metadata: Metadata;
};
interface Metadata {
color: string;
creator: Patp;
'date-created': string;
description: string;
title: string;
}

View File

@ -0,0 +1,25 @@
// an urbit style path render as string
export type Path = string;
// patp including leading sig
export type Patp = string;
// patp excluding leading sig
export type PatpNoSig = string;
// @uvH encoded string
export type Serial = string;
// name of app
export type AppName = 'chat' | 'link' | 'contacts' | 'publish';
export function getTagFromFrond<O>(frond: O): keyof O {
const tags = Object.keys(frond) as Array<keyof O>;
const tag = tags[0];
if(!tag) {
throw new Error("bad frond");
}
return tag;
}

View File

@ -0,0 +1,55 @@
import { Path, PatpNoSig } from './noun';
export type PermissionUpdate =
PermissionUpdateInitial
| PermissionUpdateCreate
| PermissionUpdateDelete
| PermissionUpdateRemove
| PermissionUpdateAdd;
interface PermissionUpdateInitial {
initial: {
[p in Path]: {
who: PatpNoSig[];
kind: PermissionKind;
};
}
}
interface PermissionUpdateCreate {
create: {
path: Path;
kind: PermissionKind;
who: PatpNoSig[];
}
}
interface PermissionUpdateDelete {
delete: {
path: Path;
}
}
interface PermissionUpdateAdd {
add: {
path: Path;
who: PatpNoSig[];
}
}
interface PermissionUpdateRemove {
remove: {
path: Path;
who: PatpNoSig[];
}
}
export type Permissions = {
[p in Path]: Permission;
};
export interface Permission {
who: Set<PatpNoSig>;
kind: PermissionKind;
}
export type PermissionKind = 'white' | 'black';

View File

@ -0,0 +1,48 @@
import { Notebooks, Notebook, Note, BookId, NoteId } from './publish-update';
import { Patp } from './noun';
export type PublishResponse =
NotebooksResponse
| NotebookResponse
| NoteResponse
| NotesPageResponse
| CommentsPageResponse;
interface NotebooksResponse {
type: 'notebooks';
data: Notebooks;
}
interface NotebookResponse {
type: 'notebook';
data: Notebook;
host: Patp;
notebook: BookId;
}
interface NoteResponse {
type: 'note';
data: Note;
host: Patp;
notebook: BookId;
note: NoteId;
}
interface NotesPageResponse {
type: 'notes-page';
data: Note[];
host: Patp;
notebook: BookId;
startIndex: number;
length: number;
}
interface CommentsPageResponse {
type: 'comments-page';
data: Comment[];
host: Patp;
notebook: BookId;
note: NoteId;
startIndex: number;
length: number;
}

View File

@ -0,0 +1,158 @@
import { Patp, PatpNoSig, Path } from './noun';
export type NoteId = string;
export type BookId = string;
export type PublishUpdate =
PublishUpdateAddBook
| PublishUpdateAddNote
| PublishUpdateAddComment
| PublishUpdateEditBook
| PublishUpdateEditNote
| PublishUpdateEditComment
| PublishUpdateDelBook
| PublishUpdateDelNote
| PublishUpdateDelComment;
type PublishUpdateBook = {
[s in Patp]: {
[b in BookId]: {
title: string;
'date-created': number;
about: string;
'num-notes': number;
'num-unread': number;
comments: boolean;
'writers-group-path': Path;
'subscribers-group-path': Path;
};
};
}
type PublishUpdateNote = {
[s in Patp]: {
[b in BookId]: {
'note-id': NoteId;
author: Patp;
title: string;
'date-created': string;
snippet: string;
file: string;
'num-comments': number;
comments: Comment[];
read: boolean;
pending: boolean;
};
};
};
interface PublishUpdateAddBook {
'add-book': PublishUpdateBook;
}
interface PublishUpdateEditBook {
'edit-book': PublishUpdateBook;
}
interface PublishUpdateDelBook {
'del-book': {
host: Patp;
book: string;
}
}
interface PublishUpdateAddNote {
'add-note': PublishUpdateNote;
}
interface PublishUpdateEditNote {
'edit-note': PublishUpdateNote;
}
interface PublishUpdateDelNote {
'del-note': {
host: Patp;
book: BookId;
note: NoteId;
}
}
interface PublishUpdateAddComment {
'add-comment': {
who: Patp;
host: BookId;
note: NoteId;
body: string;
}
}
interface PublishUpdateEditComment {
'edit-comment': {
host: Patp;
book: BookId;
note: NoteId;
body: string;
comment: Comment;
}
}
interface PublishUpdateDelComment {
'del-comment': {
host: Patp;
book: BookId;
note: NoteId;
comment: string;
}
}
export type Notebooks = {
[host in Patp]: {
[book in BookId]: Notebook;
}
}
export interface Notebook {
about: string;
comments: boolean;
'date-created': number;
notes: Notes;
'notes-by-date': NoteId[];
'num-notes': number;
'num-unread': number;
subscribers: PatpNoSig[];
'subscribers-group-path': Path;
title: string;
'writers-group-path': Path;
}
type Notes = {
[id in NoteId]: Note;
};
export interface Note {
author: Patp;
comments: Comment[];
'date-created': number;
file: string;
'next-note': NoteId | null;
'note-id': NoteId;
'num-comments': number;
pending: boolean;
'prev-note': NoteId | null;
read: boolean;
snippet: string;
title: string;
}
interface Comment {
[date: string]: {
author: Patp;
content: string;
'date-created': number;
pending: boolean;
};
}

Some files were not shown because too many files have changed in this diff Show More