Merge branch 'feat/spa' into lf/groups-refactor

This commit is contained in:
Liam Fitzgerald 2020-06-10 14:41:17 +10:00
commit ef20a4d08a
63 changed files with 747 additions and 704 deletions

View File

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:58deed8e9b8cd2c84d092cb8f638b9881cb0d12b97f7c719339e3604c9e9d1d2
size 13826033
oid sha256:575484aaf6c8bc03ab3b962ca52d48a90113bcb38a29a1ac84f2d49d1363b4ba
size 7319532

View File

@ -1,74 +0,0 @@
:: invite-view: provide a json interface to invite-store
::
:: accepts subscriptions at the /primary path.
:: passes through all invites and their updates.
:: only accepts subcriptions from the host's team.
::
::TODO could maybe use /lib/proxy-hook, be renamed invite-proxy-hook
::
/+ *invite-json, default-agent, dbug
::
|%
+$ card card:agent:gall
--
::
=>
|%
++ watch-updates
|= our=ship
^- card
[%pass /store %agent [our %invite-store] %watch /updates]
--
::
%- agent:dbug
^- agent:gall
|_ =bowl:gall
+* this .
def ~(. (default-agent this %|) bowl)
::
++ on-init
^- (quip card _this)
[[(watch-updates our.bowl)]~ this]
::
++ on-save on-save:def
++ on-load
|= old=vase
^- (quip card _this)
[~ this]
::
++ on-watch
|= =path
^- (quip card _this)
?> (team:title our.bowl src.bowl)
?. =(/primary path)
(on-watch:def path)
:_ this
=/ =invites
.^(invites %gx /=invite-store/(scot %da now.bowl)/all/noun)
[%give %fact ~ %json !>((invites-to-json invites))]~
::
++ on-agent
|= [=wire =sign:agent:gall]
^- (quip card _this)
:_ this
?- -.sign
%poke-ack ~|([dap.bowl %unexpected-poke-ack] !!)
%watch-ack ~
%kick [(watch-updates our.bowl)]~
::
%fact
~| [dap.bowl %unexpected-fact-mark p.cage.sign]
?> ?=(%invite-update p.cage.sign)
:~ :*
%give %fact
~[/primary] %json
!>((update-to-json !<(invite-update q.cage.sign)))
== ==
==
::
++ on-poke on-poke:def
++ on-peek on-peek:def
++ on-leave on-leave:def
++ on-arvo on-arvo:def
++ on-fail on-fail:def
--

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.3 KiB

View File

@ -2161,7 +2161,6 @@
[[[~ %json] [%'publish-view' %notebooks ~]] ~]
%- json-response:gen
%- json-to-octs
%+ frond:enjs:format %publish-response
(notebooks-map:enjs our.bol books)
::
:: notes pagination
@ -2181,7 +2180,6 @@
not-found:gen
%- json-response:gen
%- json-to-octs
%+ frond:enjs:format %publish-response
:- %o
(notes-page:enjs notes.u.book u.start u.length)
::
@ -2206,7 +2204,6 @@
not-found:gen
%- json-response:gen
%- json-to-octs
%+ frond:enjs:format %publish-response
(comments-page:enjs comments.u.note u.start u.length)
::
:: single notebook with initial 50 notes in short form, as json
@ -2225,9 +2222,7 @@
(~(put by p.notebook-json) %subscribers (get-subscribers-json book-name))
=. p.notebook-json
(~(put by p.notebook-json) %writers (get-writers-json u.host book-name))
=/ jon=json
(frond:enjs:format %publish-response (pairs notebook+notebook-json ~))
(json-response:gen (json-to-octs jon))
(json-response:gen (json-to-octs (pairs notebook+notebook-json ~)))
::
:: single note, with initial 50 comments, as json
[[[~ %json] [%'publish-view' @ @ @ ~]] ~]
@ -2241,7 +2236,6 @@
=/ note=(unit note) (~(get by notes.u.book) note-name)
?~ note not-found:gen
=/ jon=json
%+ frond %publish-response
o+(note-presentation:enjs u.book note-name u.note)
(json-response:gen (json-to-octs jon))
==

View File

@ -56,12 +56,16 @@
[%read (numb read.config)]
==
::
++ inbox
|= box=^inbox
++ update
|= upd=^update
^- json
%+ frond %chat-initial
%+ frond %chat-update
%- pairs
%+ turn ~(tap by box)
:~
?: ?=(%initial -.upd)
:- %initial
%- pairs
%+ turn ~(tap by inbox.upd)
|= [pax=^path =mailbox]
^- [cord json]
:- (spat pax)
@ -69,13 +73,6 @@
:~ [%envelopes [%a (turn envelopes.mailbox envelope)]]
[%config (config config.mailbox)]
==
::
++ update
|= upd=^update
^- json
%+ frond %chat-update
%- pairs
:~
?: ?=(%message -.upd)
:- %message
%- pairs

View File

@ -104,7 +104,6 @@
%permission-group-hook
%invite-store
%invite-hook
%invite-view
%chat-store
%chat-hook
%chat-view

View File

@ -979,7 +979,7 @@
=/ actual-redirect ?:(=(u.redirect '') '/' u.redirect)
%- handle-response
:* %start
:- status-code=307
:- status-code=303
^= headers
:~ ['location' actual-redirect]
['set-cookie' cookie-line]

View File

@ -613,12 +613,12 @@
^- (hypo sign:http-server-gate) :- *type
:* %g %unto %fact
%http-response-header
!>([307 ['location' '/~/login?redirect=/~landscape/inner-path']~])
!>([303 ['location' '/~/login?redirect=/~landscape/inner-path']~])
==
==
^= expected-move
:~ :* duct=~[/http-blah] %give %response
[%start [307 ['location' '/~/login?redirect=/~landscape/inner-path']~] ~ %.n]
[%start [303 ['location' '/~/login?redirect=/~landscape/inner-path']~] ~ %.n]
== == ==
:: the browser then fetches the login page
::
@ -2152,7 +2152,7 @@
%give
%response
%start
:- 307
:- 303
:~ ['location' '/~landscape']
:- 'set-cookie'
'urbauth-~nul=0v3.q0p7t.mlkkq.cqtto.p0nvi.2ieea; Path=/; Max-Age=604800'

View File

@ -1,5 +1,6 @@
module.exports = {
URBIT_PIERS: [
"/Users/user/ships/zod/home",
]
],
herb: false
};

View File

@ -2,6 +2,41 @@ const path = require('path');
// const HtmlWebpackPlugin = require('html-webpack-plugin');
// const { CleanWebpackPlugin } = require('clean-webpack-plugin');
const urbitrc = require('./urbitrc');
const fs = require('fs');
const util = require('util');
const exec = util.promisify(require('child_process').exec);
function copyFile(src,dest) {
return new Promise((res,rej) =>
fs.copyFile(src,dest, err => err ? rej(err) : res()));
}
class UrbitShipPlugin {
constructor(urbitrc) {
this.piers = urbitrc.URBIT_PIERS;
this.herb = urbitrc.herb || false;
}
apply(compiler) {
compiler.hooks.afterCompile.tapPromise(
'UrbitShipPlugin',
async (compilation) => {
const src = path.resolve(compiler.options.output.path, 'index.js');
return Promise.all(this.piers.map(pier => {
const dst = path.resolve(pier, 'app/landscape/js/index.js');
copyFile(src, dst).then(() => {
if(!this.herb) {
return;
}
pier = pier.split('/');
const desk = pier.pop();
return exec(`herb -p hood -d '+hood/commit %${desk}' ${pier.join('/')}`);
});
}));
}
)
}
}
module.exports = {
mode: 'development',
@ -49,16 +84,18 @@ module.exports = {
// historyApiFallback: true
// },
plugins: [
new UrbitShipPlugin(urbitrc)
// new CleanWebpackPlugin(),
// new HtmlWebpackPlugin({
// title: 'Hot Module Replacement',
// template: './public/index.html',
// }),
],
watch: true,
output: {
filename: 'index.js',
chunkFilename: 'index.js',
path: path.resolve(urbitrc.URBIT_PIERS[0] + '/app/landscape/', 'js'),
path: path.resolve(__dirname, '../dist'),
publicPath: '/'
},
optimization: {

View File

@ -1,5 +1,5 @@
import * as React from 'react';
import { BrowserRouter as Router, Route, Link, withRouter } from 'react-router-dom';
import { BrowserRouter as Router, Route, withRouter, Switch } from 'react-router-dom';
import styled, { ThemeProvider, createGlobalStyle } from 'styled-components';
import './css/indigo-static.css';
import './css/fonts.css';
@ -8,11 +8,13 @@ import { light } from '@tlon/indigo-react';
import LaunchApp from './apps/launch/app';
import ChatApp from './apps/chat/app';
import DojoApp from './apps/dojo/app';
import StatusBar from './components/StatusBar';
import GroupsApp from './apps/groups/app';
import LinksApp from './apps/links/app';
import PublishApp from './apps/publish/app';
import StatusBar from './components/StatusBar';
import NotFound from './components/404';
import GlobalStore from './store/global';
import GlobalSubscription from './subscription/global';
import GlobalApi from './api/global';
@ -71,48 +73,64 @@ export default class App extends React.Component {
api={this.api}
/>
<div>
<Route exact path="/" render={ p => (
<Switch>
<Route exact path="/"
render={ p => (
<LaunchApp
ship={this.ship}
channel={channel}
selectedGroups={selectedGroups}
{...p} />
)} />
{...p}
/>
)}
/>
<Route path="/~chat" render={ p => (
<ChatApp
ship={this.ship}
channel={channel}
selectedGroups={selectedGroups}
{...p} />
)} />
{...p}
/>
)}
/>
<Route path="/~dojo" render={ p => (
<DojoApp
ship={this.ship}
channel={channel}
selectedGroups={selectedGroups}
{...p} />
)} />
{...p}
/>
)}
/>
<Route path="/~groups" render={ p => (
<GroupsApp
ship={this.ship}
channel={channel}
selectedGroups={selectedGroups}
{...p} />
)} />
{...p}
/>
)}
/>
<Route path="/~link" render={ p => (
<LinksApp
ship={this.ship}
channel={channel}
selectedGroups={selectedGroups}
{...p} />
)} />
{...p}
/>
)}
/>
<Route path="/~publish" render={ p => (
<PublishApp
ship={this.ship}
channel={channel}
selectedGroups={selectedGroups}
{...p} />
)} />
{...p}
/>
)}
/>
<Route component={NotFound} />
</Switch>
</div>
</Router>
</Root>

View File

@ -1,4 +1,5 @@
import BaseApi from './base';
import { uuid } from '../lib/util';
export default class ChatApi {
constructor(ship, channel, store) {
@ -36,6 +37,7 @@ export default class ChatApi {
this.metadata = {
add: helper.metadataAdd.bind(helper)
};
this.sidebarToggle = helper.sidebarToggle.bind(helper);
}
}
@ -66,7 +68,7 @@ class PrivateHelper extends BaseApi {
addPendingMessage(msg) {
if (this.store.state.pendingMessages.has(msg.path)) {
this.store.state.pendingMessages.get(msg.path).push(msg.envelope);
this.store.state.pendingMessages.get(msg.path).unshift(msg.envelope);
} else {
this.store.state.pendingMessages.set(msg.path, [msg.envelope]);
}
@ -203,5 +205,18 @@ class PrivateHelper extends BaseApi {
});
}
sidebarToggle() {
let sidebarBoolean = true;
if (this.store.state.sidebarShown === true) {
sidebarBoolean = false;
}
this.store.handleEvent({
data: {
local: {
sidebarToggle: sidebarBoolean
}
}
});
}
}

View File

@ -21,7 +21,11 @@ export default class GroupsApi {
this.group = {
add: helper.groupAdd.bind(helper),
delete: helper.groupRemove.bind(helper)
remove: helper.groupRemove.bind(helper)
};
this.metadata = {
add: helper.metadataAdd.bind(helper)
};
this.invite = {

View File

@ -24,6 +24,14 @@ class PrivateHelper extends BaseApi {
launchChangeIsShown(name, isShown = true) {
this.launchAction({ 'change-is-shown': { name, isShown }});
}
clockAction(latlng) {
return this.action('clock', 'json', latlng);
}
weatherAction(latlng) {
return this.action('weather', 'json', latlng);
}
}
export default class LaunchApi {
@ -40,6 +48,10 @@ export default class LaunchApi {
changeFirstTime: helper.launchChangeFirstTime.bind(helper),
changeIsShown: helper.launchChangeIsShown.bind(helper)
};
this.clock = { action: helper.clockAction.bind(helper) };
this.weather ={ action: helper.weatherAction.bind(helper) };
}
}

View File

@ -1,9 +1,7 @@
import _ from 'lodash';
import { stringToTa } from '../lib/util';
import BaseApi from './base';
export default class LinksApi extends BaseApi {
constructor(ship, channel, store) {
super(ship, channel, store);

View File

@ -1,12 +1,15 @@
import BaseApi from './base';
export default class PublishApi extends BaseApi {
handleEvent(data) {
this.store.handleEvent({ data: { 'publish-response' : data } });
}
fetchNotebooks() {
fetch('/publish-view/notebooks.json')
.then(response => response.json())
.then((json) => {
this.store.handleEvent({
this.handleEvent({
type: 'notebooks',
data: json
});
@ -17,7 +20,7 @@ export default class PublishApi extends BaseApi {
fetch(`/publish-view/${host}/${book}.json`)
.then(response => response.json())
.then((json) => {
this.store.handleEvent({
this.handleEvent({
type: 'notebook',
data: json,
host: host,
@ -30,7 +33,7 @@ export default class PublishApi extends BaseApi {
fetch(`/publish-view/${host}/${book}/${note}.json`)
.then(response => response.json())
.then((json) => {
this.store.handleEvent({
this.handleEvent({
type: 'note',
data: json,
host: host,
@ -44,7 +47,7 @@ export default class PublishApi extends BaseApi {
fetch(`/publish-view/notes/${host}/${book}/${start}/${length}.json`)
.then(response => response.json())
.then((json) => {
this.store.handleEvent({
this.handleEvent({
type: 'notes-page',
data: json,
host: host,
@ -59,7 +62,7 @@ export default class PublishApi extends BaseApi {
fetch(`/publish-view/comments/${host}/${book}/${note}/${start}/${length}.json`)
.then(response => response.json())
.then((json) => {
this.store.handleEvent({
this.handleEvent({
type: 'comments-page',
data: json,
host: host,
@ -71,15 +74,28 @@ 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) {
return this.action('publish', 'publish-action', act);
}
sidebarToggle() {
let sidebarBoolean = true;
if (this.store.state.sidebarShown === true) {
sidebarBoolean = false;
}
this.store.handleEvent({
type: 'local',
data: {
'sidebarToggle': sidebarBoolean
local: {
sidebarToggle: sidebarBoolean
}
}
});
}

View File

@ -32,7 +32,7 @@ export default class ChatApp extends React.Component {
}
componentDidMount() {
window.title = 'OS1 - Chat';
document.title = 'OS1 - Chat';
// preload spinner asset
new Image().src = '/~landscape/img/Spinner.png';
@ -74,7 +74,7 @@ export default class ChatApp extends React.Component {
});
if (totalUnreads !== this.totalUnreads) {
document.title = totalUnreads > 0 ? `Chat - (${totalUnreads})` : 'Chat';
document.title = totalUnreads > 0 ? `OS1 - Chat (${totalUnreads})` : 'OS1 - Chat';
this.totalUnreads = totalUnreads;
}
@ -310,7 +310,7 @@ export default class ChatApp extends React.Component {
>
<MemberScreen
{...props}
api={api}
api={this.api}
station={station}
association={association}
permission={permission}
@ -361,7 +361,7 @@ export default class ChatApp extends React.Component {
permissions={state.permissions || {}}
contacts={state.contacts || {}}
associations={associations.contacts}
api={api}
api={this.api}
inbox={state.inbox}
popout={popout}
sidebarShown={state.sidebarShown}

View File

@ -493,9 +493,9 @@ ref={(e) => {
style={{ height: 48 }}
>
<SidebarSwitcher
sidebarShown={this.props.sidebarShown}
popout={this.props.popout}
api={this.props.api}
sidebarShown={props.sidebarShown}
popout={props.popout}
api={props.api}
/>
<Link to={'/~chat/' + isinPopout + 'room' + props.station}
className="pt2 white-d"
@ -513,7 +513,7 @@ ref={(e) => {
station={props.station}
numPeers={group.length}
isOwner={deSig(props.match.params.ship) === window.ship}
popout={this.props.popout}
popout={props.popout}
api={props.api}
/>
</div>

View File

@ -108,7 +108,7 @@ export class SettingsScreen extends Component {
if (chatOwner) {
this.setState({ awaiting: true, type: 'Editing chat...' }, (() => {
props.api.metadataAdd(
props.api.metadata.add(
association['app-path'],
association['group-path'],
association.metadata.title,
@ -272,16 +272,16 @@ export class SettingsScreen extends Component {
<input
className={'f8 ba b--gray3 b--gray2-d bg-gray0-d white-d ' +
'focus-b--black focus-b--white-d pa3 db w-100 flex-auto mr3'}
value={this.state.title}
value={state.title}
disabled={!chatOwner}
onChange={this.changeTitle}
onBlur={() => {
if (chatOwner) {
this.setState({ awaiting: true, type: 'Editing chat...' }, (() => {
props.api.metadataAdd(
props.api.metadata.add(
association['app-path'],
association['group-path'],
this.state.title,
state.title,
association.metadata.description,
association.metadata['date-created'],
uxToHex(association.metadata.color)
@ -301,17 +301,17 @@ export class SettingsScreen extends Component {
<input
className={'f8 ba b--gray3 b--gray2-d bg-gray0-d white-d ' +
'focus-b--black focus-b--white-d pa3 db w-100 flex-auto mr3'}
value={this.state.description}
value={state.description}
disabled={!chatOwner}
onChange={this.changeDescription}
onBlur={() => {
if (chatOwner) {
this.setState({ awaiting: true, type: 'Editing chat...' }, (() => {
props.api.metadataAdd(
props.api.metadata.add(
association['app-path'],
association['group-path'],
association.metadata.title,
this.state.description,
state.description,
association.metadata['date-created'],
uxToHex(association.metadata.color)
).then(() => {
@ -339,7 +339,7 @@ export class SettingsScreen extends Component {
<input
className={'pl7 f8 ba b--gray3 b--gray2-d bg-gray0-d white-d ' +
'focus-b--black focus-b--white-d pa3 db w-100 flex-auto mr3'}
value={this.state.color}
value={state.color}
disabled={!chatOwner}
onChange={this.changeColor}
onBlur={this.submitColor}
@ -400,7 +400,9 @@ export class SettingsScreen extends Component {
/>
</div>
<div className="w-100 pl3 mt4 cf">
<Spinner awaiting={this.state.awaiting} classes="absolute right-2 bottom-2 ba pa2 b--gray1-d" text={this.state.type} />
<Spinner awaiting={state.awaiting}
classes="absolute right-2 bottom-2 ba pa2 b--gray1-d"
text={state.type} />
</div>
</div>
);
@ -477,7 +479,9 @@ export class SettingsScreen extends Component {
{this.renderGroupify()}
{this.renderDelete()}
{this.renderMetadataSettings()}
<Spinner awaiting={this.state.awaiting} classes="absolute right-2 bottom-2 ba pa2 b--gray1-d" text={this.state.type} />
<Spinner awaiting={state.awaiting}
classes="absolute right-2 bottom-2 ba pa2 b--gray1-d"
text={state.type} />
</div>
</div>
);

View File

@ -14,6 +14,7 @@ export class Sidebar extends Component {
dmOverlay: false
};
}
onClickNew() {
this.props.history.push('/~chat/new');
}
@ -37,10 +38,14 @@ export class Sidebar extends Component {
const selectedGroups = props.selectedGroups ? props.selectedGroups : [];
const associations =
const contactAssoc =
(props.associations && 'contacts' in props.associations)
? alphabetiseAssociations(props.associations.contacts) : {};
const chatAssoc =
(props.associations && 'chat' in props.associations)
? alphabetiseAssociations(props.associations.chat) : {};
const groupedChannels = {};
Object.keys(props.inbox).map((box) => {
if (box.startsWith('/~/')) {
@ -51,10 +56,11 @@ export class Sidebar extends Component {
} else {
groupedChannels['/~/'] = [box];
}
}
const path = props.associations.chat[box]
? props.associations.chat[box]['group-path'] : box;
if (path in associations) {
} else {
const path = chatAssoc[box]
? chatAssoc[box]['group-path'] : box;
if (path in contactAssoc) {
if (groupedChannels[path]) {
const array = groupedChannels[path];
array.push(box);
@ -63,6 +69,7 @@ export class Sidebar extends Component {
groupedChannels[path] = [box];
}
}
}
});
const sidebarInvites = Object.keys(props.invites)
@ -77,7 +84,7 @@ export class Sidebar extends Component {
);
});
const groupedItems = Object.keys(associations)
const groupedItems = Object.keys(contactAssoc)
.filter(each => (groupedChannels[each] || []).length !== 0)
.filter((each) => {
if (selectedGroups.length === 0) {
@ -94,8 +101,8 @@ export class Sidebar extends Component {
<GroupItem
key={i}
index={i}
association={associations[each]}
chatMetadata={props.associations['chat']}
association={contactAssoc[each]}
chatMetadata={chatAssoc}
channels={channels}
inbox={props.inbox}
station={props.station}
@ -108,7 +115,7 @@ export class Sidebar extends Component {
groupedItems.push(
<GroupItem
association={'/~/'}
chatMetadata={props.associations['chat']}
chatMetadata={chatAssoc}
channels={groupedChannels['/~/']}
inbox={props.inbox}
station={props.station}

View File

@ -51,7 +51,7 @@ h2 {
}
.clamp-attachment {
overflow: scroll;
overflow: auto;
max-height: 10em;
max-width: 100%;
}

View File

@ -28,7 +28,7 @@ export default class DojoApp extends Component {
}
componentDidMount() {
window.title = 'OS1 - Dojo';
document.title = 'OS1 - Dojo';
const channel = new this.props.channel();
this.api = new Api(this.props.ship, channel);
@ -47,7 +47,7 @@ export default class DojoApp extends Component {
render() {
return (
<div
className="bg-white bg-gray1-d"
className="bg-white bg-gray0-d"
style={{ height: 'calc(100vh - 45px)' }}
>
<Route
@ -72,7 +72,7 @@ export default class DojoApp extends Component {
<div
className={
'pa3 bg-white bg-gray0-d black white-d mono w-100 f8 relative' +
' h-100-m40-s b--gray2 br1 flex-auto ' +
' h-100-m40-s b--gray2 br1 flex-auto flex flex-column ' +
popoutClasses
}
style={{

View File

@ -8,8 +8,8 @@ export class History extends Component {
render() {
return (
<div
className="relative flex flex-column-reverse overflow-container flex-auto"
style={{ height: 'calc(100% - 1rem)', resize: 'none' }}
className="h-100 relative flex flex-column-reverse overflow-container flex-auto"
style={{ resize: 'none' }}
>
<div style={{ marginTop: 'auto' }}>
{this.props.commandLog.map((text, index) => {

View File

@ -7,7 +7,7 @@ export class Popout extends Component {
: 'dib-m dib-l dib-xl';
return (
<div
className="dib absolute z-2"
className="db tr z-2"
style={{
right: 16,
top: 16

View File

@ -23,7 +23,7 @@ export default class GroupsApp extends Component {
}
componentDidMount() {
window.title = 'OS1 - Groups';
document.title = 'OS1 - Groups';
// preload spinner asset
new Image().src = '/~landscape/img/Spinner.png';
@ -121,8 +121,8 @@ export default class GroupsApp extends Component {
const detail = Boolean(props.match.url.includes('/detail'));
const settings = Boolean(props.match.url.includes('/settings'));
const association = (associations[groupPath])
? associations[groupPath]
const association = (associations.contacts?.[groupPath])
? associations.contacts[groupPath]
: {};
return (
@ -152,6 +152,7 @@ export default class GroupsApp extends Component {
group={group}
activeDrawer={(detail || settings) ? 'detail' : 'contacts'}
settings={settings}
associations={associations}
api={this.api}
{...props}
/>

View File

@ -141,12 +141,10 @@ export class ContactCard extends Component {
type: 'Saving to group'
},
() => {
props.api
.contactEdit(props.path, ship, {
props.api.contactHook.edit(props.path, ship, {
avatar: {
url: state.avatarToSet
}
})
}})
.then(() => {
this.setState({ awaiting: false });
});
@ -163,8 +161,8 @@ export class ContactCard extends Component {
if (hexTest && hexTest[1] !== currentColor && !props.share) {
this.setState({ awaiting: true, type: 'Saving to group' }, () => {
props.api
.contactEdit(props.path, `~${props.ship}`, { color: hexTest[1] })
props.api.contactHook.edit(
props.path, `~${props.ship}`, { color: hexTest[1] })
.then(() => {
this.setState({ awaiting: false });
});
@ -182,8 +180,8 @@ export class ContactCard extends Component {
const emailTestResult = emailTest.exec(state.emailToSet);
if (emailTestResult) {
this.setState({ awaiting: true, type: 'Saving to group' }, () => {
props.api
.contactEdit(props.path, ship, { email: state.emailToSet })
props.api.contactHook.edit(
props.path, ship, { email: state.emailToSet })
.then(() => {
this.setState({ awaiting: false });
});
@ -199,8 +197,8 @@ export class ContactCard extends Component {
return false;
}
this.setState({ awaiting: true, type: 'Saving to group' }, () => {
props.api
.contactEdit(props.path, ship, { nickname: state.nickNameToSet })
props.api.contactHook.edit(
props.path, ship, { nickname: state.nickNameToSet })
.then(() => {
this.setState({ awaiting: false });
});
@ -216,8 +214,8 @@ export class ContactCard extends Component {
return false;
}
this.setState({ awaiting: true, type: 'Saving to group' }, () => {
props.api
.contactEdit(props.path, ship, { notes: state.notesToSet })
props.api.contactHook.edit(
props.path, ship, { notes: state.notesToSet })
.then(() => {
this.setState({ awaiting: false });
});
@ -234,8 +232,8 @@ export class ContactCard extends Component {
const phoneTestResult = phoneTest.exec(state.phoneToSet);
if (phoneTestResult) {
this.setState({ awaiting: true, type: 'Saving to group' }, () => {
props.api
.contactEdit(props.path, ship, { phone: state.phoneToSet })
props.api.contactHook.edit(
props.path, ship, { phone: state.phoneToSet })
.then(() => {
this.setState({ awaiting: false });
});
@ -253,8 +251,8 @@ export class ContactCard extends Component {
const websiteTestResult = websiteTest.exec(state.websiteToSet);
if (websiteTestResult) {
this.setState({ awaiting: true, type: 'Saving to group' }, () => {
props.api
.contactEdit(props.path, ship, { website: state.websiteToSet })
props.api.contactHook.edit(
props.path, ship, { website: state.websiteToSet })
.then(() => {
this.setState({ awaiting: false });
});
@ -266,7 +264,8 @@ export class ContactCard extends Component {
this.setState(
{ emailToSet: '', awaiting: true, type: 'Removing from group' },
() => {
props.api.contactEdit(props.path, ship, { email: '' }).then(() => {
props.api.contactHook.edit(props.path, ship, { email: '' })
.then(() => {
this.setState({ awaiting: false });
});
}
@ -277,7 +276,8 @@ export class ContactCard extends Component {
this.setState(
{ nicknameToSet: '', awaiting: true, type: 'Removing from group' },
() => {
props.api.contactEdit(props.path, ship, { nickname: '' }).then(() => {
props.api.contactHook.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.contactEdit(props.path, ship, { phone: '' }).then(() => {
props.api.contactHook.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.contactEdit(props.path, ship, { website: '' }).then(() => {
props.api.contactHook.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.contactEdit(props.path, ship, { avatar: null }).then(() => {
props.api.contactHook.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.contactEdit(props.path, ship, { notes: '' }).then(() => {
props.api.contactHook.edit(props.path, ship, { notes: '' }).then(() => {
this.setState({ awaiting: false });
});
}
@ -519,9 +519,10 @@ export class ContactCard extends Component {
key={'avatar' + currentColor}
/>
<div
className="tl mt4 mb4 w-auto ml-auto mr-auto"
className="tc mt4 mb4 w-auto ml-auto mr-auto"
style={{ width: 'fit-content' }}
>
<div className="tl dib">
<p className="f9 gray2 lh-copy">Sigil Color</p>
<textarea
className={
@ -544,6 +545,7 @@ export class ContactCard extends Component {
}}
></textarea>
</div>
</div>
<div className="w-100 pt8 pb8 lh-copy tl">
<p className="f9 gray2">Ship Name</p>
<p className="f8 mono">~{props.ship}</p>

View File

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

View File

@ -18,11 +18,10 @@ export class GroupDetail extends Component {
componentDidMount() {
const { props } = this;
const channelPath = `${props.path}/contacts${props.path}`;
if ((props.association) && (props.association[channelPath])) {
if (props.association.metadata) {
this.setState({
title: props.association[channelPath].metadata.title,
description: props.association[channelPath].metadata.description
title: props.association.metadata.title,
description: props.association.metadata.description
});
}
}
@ -30,11 +29,10 @@ export class GroupDetail extends Component {
componentDidUpdate(prevProps) {
const { props } = this;
if (prevProps !== this.props) {
const channelPath = `${props.path}/contacts${props.path}`;
if ((props.association) && (props.association[channelPath])) {
if (props.association.metadata) {
this.setState({
title: props.association[channelPath].metadata.title,
description: props.association[channelPath].metadata.description
title: props.association.metadata.title,
description: props.association.metadata.description
});
}
}
@ -54,70 +52,65 @@ export class GroupDetail extends Component {
const responsiveClass =
props.activeDrawer === 'detail' ? 'db ' : 'dn db-ns ';
const isEmpty = (Object.keys(props.association).length === 0) ||
((Object.keys(props.association).length === 1) &&
(Object.keys(props.association)[0].includes('contacts')));
let channelList = [];
let channelList = (<div />);
Object.keys(props.associations).filter((app) => {
return app !== 'contacts';
}).map((app) => {
Object.keys(props.associations[app]).filter((channel) => {
return props.associations[app][channel]['group-path'] === props.association['group-path'];
})
.map((channel) => {
const channelObj = props.associations[app][channel];
const title =
channelObj.metadata?.title || channelObj['app-path'] || '';
channelList = Object.keys(props.association).sort((a, b) => {
const aChannel = props.association[a];
const bChannel = props.association[b];
const aTitle = aChannel.metadata.title || a;
const bTitle = bChannel.metadata.title || b;
return aTitle.toLowerCase().localeCompare(bTitle.toLowerCase());
}).map((key) => {
const channel = props.association[key];
if (!('metadata' in channel)) {
return <div key={channel} />;
}
if (channel['app-name'] === 'contacts') {
return <div key={channel} />;
}
const title = channel.metadata.title || channel['app-path'] || '';
const color = uxToHex(channel.metadata.color) || '000000';
let app = channel['app-name'] || 'Unknown';
const channelPath = channel['app-path'];
const color = uxToHex(channelObj.metadata?.color) || '000000';
const channelPath = channelObj['app-path'];
const link = `/~${app}/join${channelPath}`;
app = app.charAt(0).toUpperCase() + app.slice(1);
return(
channelList.push({
title: title,
color: color,
app: app.charAt(0).toUpperCase() + app.slice(1),
link: link
})
);
});
});
const isEmpty = (Boolean(channelList.length === 0));
if (channelList.length === 0) {
channelList = <div />;
} else {
channelList = channelList.sort((a, b) => {
return a.title.toLowerCase().localeCompare(b.title.toLowerCase());
}).map((each) => {
const overlay = {
r: parseInt(color.slice(0, 2), 16),
g: parseInt(color.slice(2, 4), 16),
b: parseInt(color.slice(4, 6), 16)
r: parseInt(each.color.slice(0, 2), 16),
g: parseInt(each.color.slice(2, 4), 16),
b: parseInt(each.color.slice(4, 6), 16)
};
const tile = (app === 'Unknown')
? <div className="dib ba pa1" style={{
backgroundColor: `#${color}`,
borderColor: `#${color}`,
height: 24,
width: 24 }}
/>
: <div className="ba" style={{
borderColor: `#${color}`,
return (
<li key={each.link} className="f9 list flex pv1 w-100">
<div className="ba" style={{
borderColor: `#${each.color}`,
backgroundColor: `rgba(${overlay.r}, ${overlay.g}, ${overlay.b}, 0.25)`
}}
>
<img
src={`/~groups/img/${app}.png`}
src={`/~landscape/img/${each.app}.png`}
className="dib invert-d pa1 v-mid"
style={{ height: 26, width: 26 }}
/>
</div>;
return (
<li key={channelPath} className="f9 list flex pv1 w-100">
{tile}
</div>
<div className="flex flex-column flex-auto">
<p className="f9 inter ml2 w-100">{title}</p>
<p className="f9 inter ml2 w-100">{each.title}</p>
<p className="f9 inter ml2 w-100">
<span className="f9 di mr2 inter">{app}</span>
<Link className="f9 di green2" to={link}>
<span className="f9 di mr2 inter">{each.app}</span>
<Link className="f9 di green2" to={each.link}>
Open
</Link>
</p>
@ -125,6 +118,7 @@ export class GroupDetail extends Component {
</li>
);
});
}
let backLink = props.location.pathname;
backLink = backLink.slice(0, props.location.pathname.indexOf('/detail'));
@ -139,13 +133,12 @@ export class GroupDetail extends Component {
let title = props.path.substr(1);
let description = '';
const channel = `${props.path}/contacts${props.path}`;
if ((props.association) && (props.association[channel])) {
title = (props.association[channel].metadata.title !== '')
? props.association[channel].metadata.title
if (props.association?.metadata) {
title = (props.association.metadata.title !== '')
? props.association.metadata.title
: props.path.substr(1);
description = (props.association[channel].metadata.description !== '')
? props.association[channel].metadata.description
description = (props.association.metadata.description !== '')
? props.association.metadata.description
: '';
}
@ -181,10 +174,7 @@ export class GroupDetail extends Component {
const groupOwner = (deSig(props.match.params.ship) === window.ship);
const channelPath = `${props.path}/contacts${props.path}`;
const association = ((props.association) && (props.association[channelPath]))
? props.association[channelPath] : {};
const association = props.association;
const deleteButtonClasses = (groupOwner) ? 'b--red2 red2 pointer bg-gray0-d' : 'b--gray3 gray3 bg-gray0-d c-default';
@ -208,7 +198,7 @@ export class GroupDetail extends Component {
onBlur={() => {
if (groupOwner) {
this.setState({ awaiting: true }, (() => {
props.api.metadataAdd(
props.api.metadata.add(
association['app-path'],
association['group-path'],
this.state.title,
@ -237,7 +227,7 @@ export class GroupDetail extends Component {
onBlur={() => {
if (groupOwner) {
this.setState({ awaiting: true }, (() => {
props.api.metadataAdd(
props.api.metadata.add(
association['app-path'],
association['group-path'],
association.metadata.title,

View File

@ -70,32 +70,22 @@ export class GroupSidebar extends Component {
}
const selectedPaths = selectedGroups.map(((e) => {
return e[0];
}));
}));
return (selectedPaths.includes(path));
})
.sort((a, b) => {
let aName = a.substr(1);
let bName = b.substr(1);
const aChannel = `${a}/contacts${a}`;
const bChannel = `${b}/contacts${b}`;
if (
props.associations[a] &&
props.associations[a][aChannel] &&
props.associations[a][aChannel].metadata
) {
if (props.associations.contacts?.[a]?.metadata) {
aName =
props.associations[a][aChannel].metadata.title !== ''
? props.associations[a][aChannel].metadata.title
props.associations.contacts[a].metadata.title !== ''
? props.associations.contacts[a].metadata.title
: a.substr(1);
}
if (
props.associations[b] &&
props.associations[b][bChannel] &&
props.associations[b][bChannel].metadata
) {
if (props.associations.contacts?.[b]?.metadata) {
bName =
props.associations[b][bChannel].metadata.title !== ''
? props.associations[b][bChannel].metadata.title
props.associations.contacts[b].metadata.title !== ''
? props.associations.contacts[b].metadata.title
: b.substr(1);
}
@ -104,15 +94,10 @@ export class GroupSidebar extends Component {
.map((path) => {
let name = path.substr(1);
const selected = props.selected === path;
const groupChannel = `${path}/contacts${path}`;
if (
props.associations[path] &&
props.associations[path][groupChannel] &&
props.associations[path][groupChannel].metadata
) {
if (props.associations.contacts?.[path]?.metadata) {
name =
props.associations[path][groupChannel].metadata.title !== ''
? props.associations[path][groupChannel].metadata.title
props.associations.contacts[path].metadata.title !== ''
? props.associations.contacts[path].metadata.title
: path.substr(1);
}
return (

View File

@ -23,7 +23,7 @@ export default class LaunchApp extends React.Component {
}
componentDidMount() {
window.title = 'OS1 - Home';
document.title = 'OS1 - Home';
// preload spinner asset
new Image().src = '/~landscape/img/Spinner.png';

View File

@ -1,14 +1,12 @@
import React from 'react';
import classnames from 'classnames';
import moment from 'moment';
import SunCalc from 'suncalc';
import Tile from './tile';
const outerSize = 124; //tile size
const innerSize = 124; //clock size
const innerSize = 124; // clock size
//polar to cartesian
// polar to cartesian
// var ptc = function(r, theta) {
// return {
// x: r * Math.cos(theta),
@ -16,77 +14,72 @@ const innerSize = 124; //clock size
// }
// }
let text = "#000000", background = "#ffffff";
let text = '#000000', background = '#ffffff';
let dark = window.matchMedia('(prefers-color-scheme: dark)');
const dark = window.matchMedia('(prefers-color-scheme: dark)');
if (dark.matches) {
text = "#7f7f7f";
background = "#333";
text = '#7f7f7f';
background = '#333';
}
function darkColors(dark) {
if (dark.matches) {
text = "#7f7f7f";
background = "#333";
text = '#7f7f7f';
background = '#333';
} else {
text = "#000000";
background = "#ffffff"
text = '#000000';
background = '#ffffff';
}
}
dark.addListener(darkColors);
const toRelativeTime = (date, referenceTime, unit) => moment(date)
.diff(referenceTime, unit)
.diff(referenceTime, unit);
const minsToDegs = (mins) => {
// 1440 = total minutes in an earth day
return (mins / 1440) * 360
}
return (mins / 1440) * 360;
};
const clockwise = (deg, delta) => deg + delta
const anticlockwise = (deg, delta) => deg - delta
const splitArc = (start, end) => end + ((start - end) * 0.5)
const splitArc = (start, end) => end + ((start - end) * 0.5);
const isOdd = n => Math.abs(n % 2) == 1;
const radToDeg = (rad) => rad * (180 / Math.PI);
const radToDeg = rad => rad * (180 / Math.PI);
const degToRad = (deg) => deg * (Math.PI / 180);
const degToRad = deg => deg * (Math.PI / 180);
const convert = (date, referenceTime) => {
return minsToDegs(toRelativeTime(date, referenceTime, 'minutes'))
}
return minsToDegs(toRelativeTime(date, referenceTime, 'minutes'));
};
const circle = (ctx, x, y, r, from, to, fill) => {
ctx.beginPath();
ctx.arc( x, y, r, from, to, );
ctx.arc( x, y, r, from, to );
ctx.strokeStyle = 'rgba(0,0,0,0)';
ctx.fillStyle = fill || 'rgba(0,0,0,0)';
ctx.fill();
}
};
const circleOutline = (ctx, x, y, r, from, to, stroke, lineWidth) => {
ctx.beginPath();
ctx.arc( x, y, r, from, to, );
ctx.arc( x, y, r, from, to );
ctx.fillStyle = 'rgba(0,0,0,0)';
ctx.lineWidth = lineWidth;
ctx.strokeStyle = stroke || 'rgba(0,0,0,0)';
ctx.stroke();
}
};
const arc = (ctx, x, y, r, from, to, fill) => {
ctx.beginPath();
ctx.arc( x, y, r, from, to, );
ctx.arc( x, y, r, from, to );
ctx.fillStyle = 'rgba(0,0,0,0)';
ctx.lineWidth = r * 2;
ctx.strokeStyle = fill || 'rgba(0,0,0,0)';
ctx.stroke();
}
};
const degArc = (ctx, x, y, r, from, to, fill) => {
ctx.beginPath();
@ -95,17 +88,16 @@ const degArc = (ctx, x, y, r, from, to, fill) => {
ctx.lineWidth = r * 2;
ctx.strokeStyle = fill || 'rgba(0,0,0,0)';
ctx.stroke();
}
};
class Clock extends React.Component {
constructor(props) {
super(props);
this.animate = this.animate.bind(this);
this.canvasRef = React.createRef();
this.canvas = null;
this.angle = 0;
this.referenceTime = moment().startOf('day').subtract(6, 'hours')
this.referenceTime = moment().startOf('day').subtract(6, 'hours');
this.state = {
lat: 0,
lon: 0,
@ -119,23 +111,22 @@ class Clock extends React.Component {
night: 0,
nightEnd: 0,
nauticalDawn: 0,
nauticalDusk: 0,
nauticalDusk: 0
// sunriseStartTime = 1509967519,
// sunriseEndTime = 2500 + 1509967519,
// sunsetStartTime = 1510003982,
// sunsetEndTime
// moonPhase = 0.59,
}
};
}
initGeolocation() {
if (typeof this.props.data === 'string') {
const latlon = this.props.data.split(',')
const lat = latlon[0]
const lon = latlon[1]
const latlon = this.props.data.split(',');
const lat = latlon[0];
const lon = latlon[1];
const suncalc = SunCalc.getTimes(new Date(), lat, lon)
const suncalc = SunCalc.getTimes(new Date(), lat, lon);
const convertedSunCalc = {
sunset: convert(suncalc.sunset, this.referenceTime),
@ -147,25 +138,24 @@ class Clock extends React.Component {
night: convert(suncalc.night, this.referenceTime),
nightEnd: convert(suncalc.nightEnd, this.referenceTime),
nauticalDawn: convert(suncalc.nauticalDawn, this.referenceTime),
nauticalDusk: convert(suncalc.nauticalDusk, this.referenceTime),
}
nauticalDusk: convert(suncalc.nauticalDusk, this.referenceTime)
};
this.setState({
lat,
lon,
...convertedSunCalc,
geolocationSuccess: true,
})
geolocationSuccess: true
});
}
}
componentDidUpdate(prevProps) {
if (prevProps !== this.props) {
this.initGeolocation()
this.initGeolocation();
}
}
componentDidMount() {
this.canvas = initCanvas(
this.canvasRef,
@ -173,29 +163,27 @@ class Clock extends React.Component {
4
);
this.initGeolocation()
this.animate()
this.initGeolocation();
this.animate();
}
animate() {
window.setTimeout(() => window.requestAnimationFrame(this.animate), 1000)
window.setTimeout(() => window.requestAnimationFrame(this.animate), 1000);
const { state } = this
const { state } = this;
const time = new Date();
const ctx = this.canvas.getContext("2d");
const ctx = this.canvas.getContext('2d');
ctx.clearRect(0, 0, ctx.width, ctx.height);
ctx.save();
const ctr = innerSize / 2
const ctr = innerSize / 2;
// Sun+moon calculations
const dd = 4
var cx = ctr
var cy = ctr
this.angle = degToRad(convert(time, this.referenceTime))
var newX = cx + (ctr - 15) * Math.cos(this.angle);
var newY = cy + (ctr - 15) * Math.sin(this.angle);
const cx = ctr;
const cy = ctr;
this.angle = degToRad(convert(time, this.referenceTime));
const newX = cx + (ctr - 15) * Math.cos(this.angle);
const newY = cy + (ctr - 15) * Math.sin(this.angle);
// Initial background
circle(
@ -206,7 +194,7 @@ class Clock extends React.Component {
-1,
2 * Math.PI,
background
)
);
// Day
degArc(
@ -276,7 +264,7 @@ class Clock extends React.Component {
0,
2 * Math.PI,
'#FCC440'
)
);
// Sun circle border
circleOutline(
@ -299,7 +287,7 @@ class Clock extends React.Component {
0,
2 * Math.PI,
'#FFFFFF'
)
);
// Moon circle outline
circleOutline(
ctx,
@ -357,7 +345,7 @@ class Clock extends React.Component {
-1,
2 * Math.PI,
background
)
);
// Center white circle border
circleOutline(
@ -374,26 +362,27 @@ class Clock extends React.Component {
// Text for time and date
const timeText = isOdd(time.getSeconds())
? moment().format('h mm A')
: moment().format('h:mm A')
const dateText = moment().format('MMM Do')
ctx.textAlign = 'center'
ctx.fillStyle = text
ctx.font = '12px Inter'
ctx.fillText(timeText, ctr, ctr + 6 - 7)
ctx.fillStyle = text
ctx.font = '12px Inter'
ctx.fillText(dateText, ctr, ctr + 6 + 7)
: moment().format('h:mm A');
const dateText = moment().format('MMM Do');
ctx.textAlign = 'center';
ctx.fillStyle = text;
ctx.font = '12px Inter';
ctx.fillText(timeText, ctr, ctr + 6 - 7);
ctx.fillStyle = text;
ctx.font = '12px Inter';
ctx.fillText(dateText, ctr, ctr + 6 + 7);
ctx.restore();
}
render() {
return <div style={{position:'relative'}}>
return <div style={{ position:'relative' }}>
<canvas
style={{position:'absolute'}}
style={{ position:'absolute' }}
ref={ canvasRef => this.canvasRef = canvasRef }
id="clock-canvas"/>
</div>
id="clock-canvas"
/>
</div>;
}
}
@ -411,27 +400,15 @@ export default class ClockTile extends React.Component {
}
render() {
let data = !!this.props.data ? this.props.data : {};
const data = this.props.data ? this.props.data : {};
return this.renderWrapper((
<Clock data={data}/>
<Clock data={data} />
));
}
}
const loadImg = (base64, cb) => new Promise(resolve => {
const img = new Image();
img.onload = () => resolve(cb(img));
img.onerror = () => reject('Error loading image');
img.src = base64;
});
const initCanvas = (canvas, size, ratio) => {
const { x, y } = size;
let ctx = canvas.getContext('2d');
// let ratio = ctx.webkitBackingStorePixelRatio < 2
// ? window.devicePixelRatio
// : 1;
@ -439,7 +416,6 @@ const initCanvas = (canvas, size, ratio) => {
// default for high print resolution.
// ratio = ratio * resMult;
canvas.width = x * ratio;
canvas.height = y * ratio;
canvas.style.width = x + 'px';
@ -448,5 +424,5 @@ const initCanvas = (canvas, size, ratio) => {
canvas.getContext('2d').scale(ratio, ratio);
return canvas;
}
};

View File

@ -1,5 +1,4 @@
import React from 'react';
import classnames from 'classnames';
import moment from 'moment';
import Tile from './tile';
@ -13,121 +12,123 @@ export default class WeatherTile extends React.Component {
error: false
};
let api = props.api;
this.api = props.api;
}
// geolocation and manual input functions
locationSubmit() {
navigator.geolocation.getCurrentPosition((res) => {
let latlng = `${res.coords.latitude},${res.coords.longitude}`;
const latlng = `${res.coords.latitude},${res.coords.longitude}`;
this.setState({
latlng
}, (err) => {
console.log(err);
}, { maximumAge: Infinity, timeout: 10000 });
api.action("clock", "json", latlng);
api.action('weather', 'json', latlng);
// this.api.clock.action(latlng);
this.api.weather.action(latlng);
this.setState({ manualEntry: !this.state.manualEntry });
});
}
manualLocationSubmit() {
event.preventDefault();
let gpsInput = document.getElementById('gps');
let latlngNoSpace = gpsInput.value.replace(/\s+/g, '');
let latlngParse = /-?[0-9]+(?:\.[0-9]*)?,-?[0-9]+(?:\.[0-9]*)?/g;
const gpsInput = document.getElementById('gps');
const latlngNoSpace = gpsInput.value.replace(/\s+/g, '');
const latlngParse = /-?[0-9]+(?:\.[0-9]*)?,-?[0-9]+(?:\.[0-9]*)?/g;
if (latlngParse.test(latlngNoSpace)) {
let latlng = latlngNoSpace;
this.setState({latlng}, (err) => {console.log(err)}, {maximumAge: Infinity, timeout: 10000});
api.action("clock", "json", latlng);
api.action('weather', 'json', latlng);
this.setState({manualEntry: !this.state.manualEntry});
}
else {
this.setState({error: true});
const latlng = latlngNoSpace;
this.setState({ latlng }, (err) => {
console.log(err);
}, { maximumAge: Infinity, timeout: 10000 });
// this.api.clock.action(latlng);
this.api.weather.action(latlng);
this.setState({ manualEntry: !this.state.manualEntry });
} else {
this.setState({ error: true });
return false;
}
}
// set appearance based on weather
setColors(data) {
let weatherStyle = {
gradient1: "",
gradient2: "",
text: ""
gradient1: '',
gradient2: '',
text: ''
};
switch (data.daily.icon) {
case "clear-day":
case 'clear-day':
weatherStyle = {
gradient1: "#A5CEF0", gradient2: "#FEF4E0", text: "black"
}
break;
case "clear-night":
weatherStyle = {
gradient1: "#56668e", gradient2: "#000080", text: "white"
}
break;
case "rain":
weatherStyle = {
gradient1: "#b1b2b3", gradient2: "#b0c7ff", text: "black"
gradient1: '#A5CEF0', gradient2: '#FEF4E0', text: 'black'
};
break;
case "snow":
case 'clear-night':
weatherStyle = {
gradient1: "#eee", gradient2: "#f9f9f9", text: "black"
gradient1: '#56668e', gradient2: '#000080', text: 'white'
};
break;
case "sleet":
case 'rain':
weatherStyle = {
gradient1: "#eee", gradient2: "#f9f9f9", text: "black"
gradient1: '#b1b2b3', gradient2: '#b0c7ff', text: 'black'
};
break;
case "wind":
case 'snow':
weatherStyle = {
gradient1: "#eee", gradient2: "#fff", text: "black"
gradient1: '#eee', gradient2: '#f9f9f9', text: 'black'
};
break;
case "fog":
case 'sleet':
weatherStyle = {
gradient1: "#eee", gradient2: "#fff", text: "black"
gradient1: '#eee', gradient2: '#f9f9f9', text: 'black'
};
break;
case "cloudy":
case 'wind':
weatherStyle = {
gradient1: "#eee", gradient2: "#b1b2b3", text: "black"
gradient1: '#eee', gradient2: '#fff', text: 'black'
};
break;
case "partly-cloudy-day":
case 'fog':
weatherStyle = {
gradient1: "#fcc440", gradient2: "#b1b2b3", text: "black"
gradient1: '#eee', gradient2: '#fff', text: 'black'
};
break;
case "partly-cloudy-night":
case 'cloudy':
weatherStyle = {
gradient1: "#7f7f7f", gradient2: "#56668e", text: "white"
gradient1: '#eee', gradient2: '#b1b2b3', text: 'black'
};
break;
case 'partly-cloudy-day':
weatherStyle = {
gradient1: '#fcc440', gradient2: '#b1b2b3', text: 'black'
};
break;
case 'partly-cloudy-night':
weatherStyle = {
gradient1: '#7f7f7f', gradient2: '#56668e', text: 'white'
};
break;
default:
weatherStyle = {
gradient1: "white", gradient2: "white", text: "black"
gradient1: 'white', gradient2: 'white', text: 'black'
};
}
return weatherStyle;
}
// all tile views
renderWrapper(child,
weatherStyle = { gradient1: "white", gradient2: "white", text: "black" }
weatherStyle = { gradient1: 'white', gradient2: 'white', text: 'black' }
) {
return (
<Tile>
<div
className={"relative " + weatherStyle.text}
className={'relative ' + weatherStyle.text}
style={{
width: 126,
height: 126,
background: `linear-gradient(135deg, ${weatherStyle.gradient1} 0%,` +
`${weatherStyle.gradient2} 45%, ${weatherStyle.gradient2} 65%,` +
`${weatherStyle.gradient1} 100%)`
}}>
}}
>
{child}
</div>
</Tile>
@ -139,56 +140,65 @@ export default class WeatherTile extends React.Component {
let error;
if (this.state.error === true) {
error = <p
className="f9 red2 pt1">Please try again.
</p>
className="f9 red2 pt1"
>Please try again.
</p>;
}
if (location.protocol === "https:") {
if (location.protocol === 'https:') {
secureCheck = <a
className="black white-d f9 absolute pointer"
style={{right: 8, top: 8}}
onClick={() => this.locationSubmit()}>Detect -></a>
style={{ right: 8, top: 8 }}
onClick={() => this.locationSubmit()}
>Detect -></a>;
}
return this.renderWrapper(
<div className={"pa2 w-100 h-100 bg-white bg-gray0-d black white-d " +
"b--black b--gray1-d ba"}>
<div className={'pa2 w-100 h-100 bg-white bg-gray0-d black white-d ' +
'b--black b--gray1-d ba'}
>
<a
className="f9 black white-d pointer"
className="f9 black white-d pointer absolute"
style={{ top: 8 }}
onClick={() =>
this.setState({ manualEntry: !this.state.manualEntry })
}>
}
>
&lt;&#45;
</a>
{secureCheck}
<p className="f9 pt2">
Please enter your{" "}
<p className="f9 pt5">
Please enter your{' '}
<a
className="black white-d"
className="black bb white-d"
href="https://latitudeandlongitude.org/"
target="_blank">
target="_blank"
>
latitude and longitude
</a>
.
</p>
{error}
<div className="absolute" style={{left: 8, bottom: 8}}>
<form className="flex" style={{marginBlockEnd: 0 }}>
<div className="absolute" style={{ left: 8, bottom: 8 }}>
<form className="flex" style={{ marginBlockEnd: 0 }}>
<input
id="gps"
className="w-100 black white-d bg-transparent bn f9"
type="text"
placeholder="29.558107, -95.089023"
onKeyDown={(e) => {
if (e.key === "Enter") {
if (e.key === 'Enter') {
e.preventDefault();
this.manualLocationSubmit(e.target.value);
}}
}/>
}
}
}
/>
<input
className={"bg-transparent black white-d bn pointer " +
"f9 flex-shrink-0"}
className={'bg-transparent black white-d bn pointer ' +
'f9 flex-shrink-0 pr1'}
type="submit"
onClick={() => this.manualLocationSubmit()}
value="->"/>
value="->"
/>
</form>
</div>
</div>
@ -198,15 +208,18 @@ export default class WeatherTile extends React.Component {
renderNoData() {
return this.renderWrapper((
<div
className={"pa2 w-100 h-100 b--black b--gray1-d ba " +
"bg-white bg-gray0-d black white-d"}
onClick={() => this.setState({manualEntry: !this.state.manualEntry})}>
className={'pa2 w-100 h-100 b--black b--gray1-d ba ' +
'bg-white bg-gray0-d black white-d'}
onClick={() => this.setState({ manualEntry: !this.state.manualEntry })}
>
<p className="f9 absolute"
style={{left: 8, top: 8}}>
style={{ left: 8, top: 8 }}
>
Weather
</p>
<p className="absolute w-100 flex-col f9"
style={{bottom: 8, left: 8, cursor: "pointer"}}>
style={{ bottom: 8, left: 8, cursor: 'pointer' }}
>
-> Set location
</p>
</div>
@ -214,14 +227,15 @@ export default class WeatherTile extends React.Component {
}
renderWithData(data, weatherStyle) {
let c = data.currently;
let d = data.daily.data[0];
const c = data.currently;
const d = data.daily.data[0];
let da = moment.unix(d.sunsetTime).format('h:mm a') || '';
const da = moment.unix(d.sunsetTime).format('h:mm a') || '';
return this.renderWrapper(
<div className="w-100 h-100 b--black b--gray1-d ba"
style={{backdropFilter: "blur(80px)"}}>
style={{ backdropFilter: 'blur(80px)' }}
>
<p className="f9 absolute" style={{ left: 8, top: 8 }}>
Weather
</p>
@ -230,7 +244,8 @@ export default class WeatherTile extends React.Component {
style={{ right: 8, top: 8 }}
onClick={() =>
this.setState({ manualEntry: !this.state.manualEntry })
}>
}
>
->
</a>
<div className="w-100 absolute" style={{ left: 8, bottom: 8 }}>
@ -243,20 +258,19 @@ export default class WeatherTile extends React.Component {
}
render() {
let data = !!this.props.data ? this.props.data : {};
const data = this.props.data ? this.props.data : {};
if (this.state.manualEntry === true) {
return this.renderManualEntry();
}
if ('currently' in data && 'daily' in data) {
let weatherStyle = this.setColors(data);
const weatherStyle = this.setColors(data);
return this.renderWithData(data, weatherStyle);
}
return this.renderNoData();
}
}
window.weatherTile = WeatherTile;

View File

@ -13,14 +13,6 @@ p, h1, h2, h3, h4, h5, h6, a, input, textarea, button {
font-family: Inter, sans-serif;
}
a:any-link {
color: unset;
}
a:-webkit-any-link {
color: unset;
}
textarea, select, input, button { outline: none; }
.c-default {

View File

@ -29,7 +29,7 @@ export class LinksApp extends Component {
}
componentDidMount() {
window.title = 'OS1 - Groups';
document.title = 'OS1 - Links';
// preload spinner asset
new Image().src = '/~landscape/img/Spinner.png';
@ -73,7 +73,7 @@ export class LinksApp extends Component {
);
if(totalUnseen !== this.totalUnseen) {
document.title = totalUnseen !== 0 ? `Links - (${totalUnseen})` : 'Links';
document.title = totalUnseen !== 0 ? `OS1 - Links (${totalUnseen})` : 'OS1 - Links';
this.totalUnseen = totalUnseen;
}

View File

@ -67,7 +67,7 @@ export class LinkItem extends Component {
classes={(member ? 'mix-blend-diff' : '')}
/>;
return (
<div className="w-100 pv3 flex bg-white bg-gray0-d">
<div className="w-100 pv3 flex bg-white bg-gray0-d lh-solid">
{img}
<div className="flex flex-column ml2 flex-auto">
<a href={props.url}
@ -80,7 +80,7 @@ export class LinkItem extends Component {
</p>
<span className="gray2 dib v-btm ml2 f8 flex-shrink-0">{hostname} </span>
</a>
<div className="w-100 pt1">
<div className="w-100">
<span className={'f9 pr2 dib ' + mono}
title={props.ship}
>

View File

@ -30,15 +30,17 @@ export class LinkDetail extends Component {
}
componentDidMount() {
// if we have no preloaded data, and we aren't expecting it, get it
if (!this.state.data.title) {
this.props.api.getSubmission(
this.props.resourcePath, this.props.url, this.updateData.bind(this)
);
}
this.componentDidUpdate();
}
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.resourcePath, this.props.url, this.updateData.bind(this)
);
}
if (prevProps) {
if (this.props.url !== prevProps.url) {
this.updateData(this.props.data);
}
@ -58,6 +60,7 @@ export class LinkDetail extends Component {
}
}
}
}
onClickPost() {
const url = this.props.url || '';

View File

@ -26,14 +26,16 @@ export class Links extends Component {
// and don't have links for it yet,
// or the links we have might not be complete,
// request the links for that page.
if ( (!prevProps ||
linkPage !== prevProps.page ||
this.props.resourcePath !== prevProps.resourcePath
) &&
!this.props.links[linkPage] ||
this.props.links.local[linkPage]
if ( ((!prevProps || // first load?
linkPage !== prevProps.page || // already waiting on response?
this.props.resourcePath !== prevProps.resourcePath // new page?
) ||
(prevProps.api !== this.props.api)) // api prop instantiated?
&&
!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?.getPage(this.props.resourcePath, this.props.page);
}
}

View File

@ -31,7 +31,7 @@ export default class PublishApp extends React.Component {
}
componentDidMount() {
window.title = 'OS1 - Groups';
document.title = 'OS1 - Publish';
// preload spinner asset
new Image().src = '/~landscape/img/Spinner.png';
@ -59,7 +59,9 @@ export default class PublishApp extends React.Component {
const associations = state.associations ? state.associations : { contacts: {} };
const selectedGroups = props.selectedGroups ? props.selectedGroups : [];
const unreadTotal = _.chain(state.notebooks)
const notebooks = state.notebooks ? state.notebooks : {};
const unreadTotal = _.chain(notebooks)
.values()
.map(_.values)
.flatten() // flatten into array of notebooks
@ -68,7 +70,7 @@ export default class PublishApp extends React.Component {
.value();
if (this.unreadTotal !== unreadTotal) {
window.title = unreadTotal > 0 ? `Publish - (${unreadTotal})` : 'Publish';
document.title = unreadTotal > 0 ? `OS1 - Publish (${unreadTotal})` : 'OS1 - Publish';
this.unreadTotal = unreadTotal;
}
@ -83,7 +85,7 @@ export default class PublishApp extends React.Component {
rightPanelHide={true}
sidebarShown={true}
invites={state.invites}
notebooks={state.notebooks}
notebooks={notebooks}
associations={associations}
selectedGroups={selectedGroups}
contacts={contacts}
@ -111,7 +113,7 @@ export default class PublishApp extends React.Component {
rightPanelHide={false}
sidebarShown={state.sidebarShown}
invites={state.invites}
notebooks={state.notebooks}
notebooks={notebooks}
associations={associations}
selectedGroups={selectedGroups}
contacts={contacts}
@ -119,7 +121,7 @@ export default class PublishApp extends React.Component {
>
<NewScreen
associations={associations.contacts}
notebooks={state.notebooks}
notebooks={notebooks}
groups={state.groups}
contacts={contacts}
api={this.api}
@ -140,14 +142,14 @@ export default class PublishApp extends React.Component {
rightPanelHide={false}
sidebarShown={state.sidebarShown}
invites={state.invites}
notebooks={state.notebooks}
notebooks={notebooks}
associations={associations}
selectedGroups={selectedGroups}
contacts={contacts}
api={this.api}
>
<JoinScreen
notebooks={state.notebooks}
notebooks={notebooks}
ship={ship}
notebook={notebook}
api={this.api}
@ -171,7 +173,7 @@ export default class PublishApp extends React.Component {
const path = `${ship}/${notebook}`;
const bookGroupPath =
state.notebooks[ship][notebook]['subscribers-group-path'];
notebooks?.[ship]?.[notebook]?.['subscribers-group-path'];
const notebookContacts = (bookGroupPath in contacts)
? contacts[bookGroupPath] : {};
@ -184,7 +186,7 @@ export default class PublishApp extends React.Component {
rightPanelHide={false}
sidebarShown={state.sidebarShown}
invites={state.invites}
notebooks={state.notebooks}
notebooks={notebooks}
associations={associations}
selectedGroups={selectedGroups}
contacts={contacts}
@ -192,7 +194,7 @@ export default class PublishApp extends React.Component {
api={this.api}
>
<NewPost
notebooks={state.notebooks}
notebooks={notebooks}
ship={ship}
book={notebook}
sidebarShown={state.sidebarShown}
@ -210,7 +212,7 @@ export default class PublishApp extends React.Component {
rightPanelHide={false}
sidebarShown={state.sidebarShown}
invites={state.invites}
notebooks={state.notebooks}
notebooks={notebooks}
associations={associations}
contacts={contacts}
selectedGroups={selectedGroups}
@ -218,7 +220,7 @@ export default class PublishApp extends React.Component {
api={this.api}
>
<Notebook
notebooks={state.notebooks}
notebooks={notebooks}
view={view}
ship={ship}
book={notebook}
@ -247,7 +249,7 @@ export default class PublishApp extends React.Component {
const popout = Boolean(props.match.params.popout) || false;
const bookGroupPath =
state.notebooks[ship][notebook]['subscribers-group-path'];
notebooks?.[ship]?.[notebook]?.['subscribers-group-path'];
const notebookContacts = (bookGroupPath in state.contacts)
? contacts[bookGroupPath] : {};
@ -261,7 +263,7 @@ export default class PublishApp extends React.Component {
rightPanelHide={false}
sidebarShown={state.sidebarShown}
invites={state.invites}
notebooks={state.notebooks}
notebooks={notebooks}
selectedGroups={selectedGroups}
associations={associations}
contacts={contacts}
@ -269,7 +271,7 @@ export default class PublishApp extends React.Component {
api={this.api}
>
<EditPost
notebooks={state.notebooks}
notebooks={notebooks}
book={notebook}
note={note}
ship={ship}
@ -288,7 +290,7 @@ export default class PublishApp extends React.Component {
rightPanelHide={false}
sidebarShown={state.sidebarShown}
invites={state.invites}
notebooks={state.notebooks}
notebooks={notebooks}
associations={associations}
selectedGroups={selectedGroups}
contacts={contacts}
@ -296,7 +298,7 @@ export default class PublishApp extends React.Component {
api={this.api}
>
<Note
notebooks={state.notebooks}
notebooks={notebooks}
book={notebook}
groups={state.groups}
contacts={notebookContacts}

View File

@ -8,7 +8,7 @@ const CommentInput = React.forwardRef((props, ref) => (
name="comment"
placeholder="Leave a comment here"
className={
'f9 db border-box w-100 ba b--gray3 pt3 ph3 br1 ' +
'f9 db border-box w-100 ba b--gray3 pt2 ph2 br1 ' +
'b--gray2-d mb2 focus-b--black focus-b--white-d white-d bg-gray0-d'
}
aria-describedby="comment-desc"

View File

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

View File

@ -20,13 +20,15 @@ export class EditPost extends Component {
}
componentDidMount() {
const { props } = this;
if (!(props.notebooks[props.ship]) ||
!(props.notebooks[props.ship][props.book]) ||
!(props.notebooks[props.ship][props.book].notes[props.note]) ||
!(props.notebooks[props.ship][props.book].notes[props.note].file)) {
this.props.api.fetchNote(props.ship, props.book, props.note);
} else {
this.componentDidUpdate();
}
componentDidUpdate(prevProps) {
const { props, state } = this;
if (prevProps && prevProps.api !== props.api) {
if (!(props.notebooks[props.ship]?.[props.book]?.notes?.[props.note]?.file)) {
props.api?.fetchNote(props.ship, props.book, props.note);
} else if (state.body === '') {
const notebook = props.notebooks[props.ship][props.book];
const note = notebook.notes[props.note];
const file = note.file;
@ -34,6 +36,7 @@ export class EditPost extends Component {
this.setState({ body: body });
}
}
}
postSubmit() {
const { props, state } = this;
@ -50,7 +53,7 @@ export class EditPost extends Component {
}
};
this.setState({ awaiting: true });
this.props.api.action('publish', 'publish-action', editNote).then(() => {
this.props.api.publishAction(editNote).then(() => {
const editIndex = props.location.pathname.indexOf('/edit');
const noteHref = props.location.pathname.slice(0, editIndex);
this.setState({ awaiting: false });
@ -112,7 +115,7 @@ export class EditPost extends Component {
to={popoutHref}
target="_blank"
>
<img src="/~publish/popout.png"
<img src="/~landscape/img/popout.png"
height={16}
width={16}
/>

View File

@ -93,7 +93,7 @@ export class JoinScreen extends Component {
// TODO: askHistory setting
this.setState({ disable: true });
this.props.api.action('publish','publish-action', actionData).catch((err) => {
this.props.api.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.action('publish', 'publish-action', newNote).then(() => {
this.props.api.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.action('publish', 'publish-action', newNote);
this.props.api.publishAction(newNote);
} else {
this.setState({ disabled: false, awaiting: null });
}
@ -52,11 +52,15 @@ export class NewPost extends Component {
}
}
componentWillMount() {
componentDidMount() {
this.componentDidUpdate();
}
componentDidUpdate(prevProps) {
if (prevProps && prevProps.api !== this.props.api) {
this.props.api.fetchNotebook(this.props.ship, this.props.book);
}
componentDidUpdate() {
const notebook = this.props.notebooks[this.props.ship][this.props.book];
if (notebook.notes[this.state.awaiting]) {
this.setState({ disabled: false, awaiting: null });
@ -131,7 +135,7 @@ export class NewPost extends Component {
to={popoutHref}
target='_blank'
>
<img src='/~publish/popout.png' height={16} width={16} />
<img src='/~landscape/img/popout.png' height={16} width={16} />
</Link>
</div>
<div className='mw6 center'>

View File

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

View File

@ -40,54 +40,40 @@ export class Note extends Component {
this.deletePost = this.deletePost.bind(this);
}
componentWillMount() {
const readAction = {
read: {
who: this.props.ship.slice(1),
book: this.props.book,
note: this.props.note
}
};
this.props.api.action('publish', 'publish-action', readAction);
this.props.api.fetchNote(this.props.ship, this.props.book, this.props.note);
}
componentDidMount() {
if (!(this.props.notebooks[this.props.ship]) ||
!(this.props.notebooks[this.props.ship][this.props.book]) ||
!(this.props.notebooks[this.props.ship][this.props.book].notes[this.props.note]) ||
!(this.props.notebooks[this.props.ship][this.props.book].notes[this.props.note].file)) {
this.props.api.fetchNote(this.props.ship, this.props.book, this.props.note);
}
this.componentDidUpdate();
this.onScroll();
}
componentDidUpdate(prevProps) {
if (!(this.props.notebooks[this.props.ship]) ||
!(this.props.notebooks[this.props.ship][this.props.book]) ||
!(this.props.notebooks[this.props.ship][this.props.book].notes[this.props.note]) ||
!(this.props.notebooks[this.props.ship][this.props.book].notes[this.props.note].file)) {
this.props.api.fetchNote(this.props.ship, this.props.book, this.props.note);
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);
}
if ((prevProps.book !== this.props.book) ||
(prevProps.note !== this.props.note) ||
(prevProps.ship !== this.props.ship)) {
if (prevProps) {
if ((prevProps.book !== props.book) ||
(prevProps.note !== props.note) ||
(prevProps.ship !== props.ship)) {
const readAction = {
read: {
who: this.props.ship.slice(1),
book: this.props.book,
note: this.props.note
who: props.ship.slice(1),
book: props.book,
note: props.note
}
};
this.props.api.action('publish', 'publish-action', readAction);
props.api.publishAction(readAction);
}
}
}
}
onScroll() {
const notebook = this.props.notebooks[this.props.ship][this.props.book];
const note = notebook.notes[this.props.note];
const notebook = this.props.notebooks?.[this.props.ship]?.[this.props.book];
const note = notebook?.notes?.[this.props.note];
if (!note.comments) {
if (!note?.comments) {
return;
}
@ -123,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.action('publish', 'publish-action', deleteAction)
this.props.api.publishAction(deleteAction)
.then(() => {
props.history.push(baseUrl);
});
@ -131,12 +117,12 @@ export class Note extends Component {
render() {
const { props } = this;
const notebook = props.notebooks[props.ship][props.book] || {};
const comments = notebook.notes[props.note].comments || false;
const title = notebook.notes[props.note].title || '';
const author = notebook.notes[props.note].author || '';
const file = notebook.notes[props.note].file || '';
const date = moment(notebook.notes[props.note]['date-created']).fromNow() || 0;
const notebook = props.notebooks?.[props.ship]?.[props.book] || {};
const comments = notebook?.notes?.[props.note]?.comments || false;
const title = notebook?.notes?.[props.note]?.title || '';
const author = notebook?.notes?.[props.note]?.author || '';
const file = notebook?.notes?.[props.note]?.file || '';
const date = moment(notebook.notes?.[props.note]?.['date-created']).fromNow() || 0;
const contact = author.substr(1) in props.contacts
? props.contacts[author.substr(1)] : false;
@ -156,22 +142,24 @@ export class Note extends Component {
}
const newfile = file.slice(file.indexOf(';>')+2);
const prevId = notebook.notes[props.note]['prev-note'] || null;
const nextId = notebook.notes[props.note]['next-note'] || null;
const prevId = notebook?.notes?.[props.note]?.['prev-note'] || null;
const nextId = notebook?.notes?.[props.note]?.['next-note'] || null;
const prevDate = moment(notebook?.notes?.[prevId]?.['date-created']).fromNow() || 0;
const nextDate = moment(notebook?.notes?.[nextId]?.['date-created']).fromNow() || 0;
const prev = (prevId === null)
? null
: {
id: prevId,
title: notebook.notes[prevId].title,
date: moment(notebook.notes[prevId]['date-created']).fromNow()
title: notebook?.notes?.[prevId]?.title,
date: prevDate
};
const next = (nextId === null)
? null
: {
id: nextId,
title: notebook.notes[nextId].title,
date: moment(notebook.notes[nextId]['date-created']).fromNow()
title: notebook?.notes?.[nextId]?.title,
date: nextDate
};
let editPost = null;
@ -218,7 +206,7 @@ export class Note extends Component {
className={'dn absolute right-1 top-1 ' + hiddenOnPopout}
target='_blank'
>
<img src='/~publish/popout.png' height={16} width={16} />
<img src='/~landscape/img/popout.png' height={16} width={16} />
</Link>
</div>
<div className='w-100 mw6'>

View File

@ -24,13 +24,13 @@ export class Notebook extends Component {
if (scrollHeight - scrollTop - clientHeight < 40) {
atBottom = true;
}
if (!notebook.notes) {
if (!notebook.notes && this.props.api) {
this.props.api.fetchNotebook(this.props.ship, this.props.book);
return;
}
const loadedNotes = Object.keys(notebook.notes).length;
const allNotes = notebook['notes-by-date'].length;
const loadedNotes = Object.keys(notebook?.notes).length || 0;
const allNotes = notebook?.['notes-by-date'].length || 0;
const fullyLoaded = (loadedNotes === allNotes);
@ -39,20 +39,20 @@ export class Notebook extends Component {
}
}
componentWillMount() {
this.props.api.fetchNotebook(this.props.ship, this.props.book);
}
componentDidUpdate(prevProps) {
const notebook = this.props.notebooks[this.props.ship][this.props.book];
if (!notebook.subscribers) {
this.props.api.fetchNotebook(this.props.ship, this.props.book);
const { props } = this;
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);
}
}
}
componentDidMount() {
const notebook = this.props.notebooks[this.props.ship][this.props.book];
if (notebook.notes) {
this.componentDidUpdate();
const notebook = this.props.notebooks?.[this.props.ship]?.[this.props.book];
if (notebook?.notes) {
this.onScroll();
}
}
@ -64,7 +64,7 @@ export class Notebook extends Component {
book: this.props.book
}
};
this.props.api.action('publish', 'publish-action', action);
this.props.api.publishAction(action);
this.props.history.push('/~publish');
}
@ -78,7 +78,7 @@ export class Notebook extends Component {
const hiddenOnPopout = props.popout ? '' : 'dib-m dib-l dib-xl';
const notebook = props.notebooks[props.ship][props.book];
const notebook = props.notebooks?.[props.ship]?.[props.book];
const tabStyles = {
posts: 'bb b--gray4 b--gray2-d gray2 pv4 ph2',
@ -91,8 +91,8 @@ export class Notebook extends Component {
let inner = null;
switch (props.view) {
case 'posts': {
const notesList = notebook['notes-by-date'] || [];
const notes = notebook.notes || null;
const notesList = notebook?.['notes-by-date'] || [];
const notes = notebook?.notes || null;
inner = <NotebookPosts notes={notes}
popout={props.popout}
list={notesList}
@ -103,7 +103,7 @@ export class Notebook extends Component {
break;
}
case 'about':
inner = <p className="f8 lh-solid">{notebook.about}</p>;
inner = <p className="f8 lh-solid">{notebook?.about}</p>;
break;
case 'subscribers':
inner = <Subscribers
@ -112,6 +112,7 @@ export class Notebook extends Component {
notebook={notebook}
permissions={this.props.permissions}
groups={this.props.groups}
api={this.props.api}
/>;
break;
case 'settings':
@ -123,6 +124,7 @@ export class Notebook extends Component {
contacts={this.props.contacts}
associations={this.props.associations}
history={this.props.history}
api={this.props.api}
/>;
break;
default:
@ -150,9 +152,9 @@ export class Notebook extends Component {
const newUrl = base + '/new';
let newPost = null;
if (notebook['writers-group-path'] in props.groups) {
const writers = notebook['writers-group-path'];
if (props.groups[writers].has(window.ship)) {
if (notebook?.['writers-group-path'] in props.groups) {
const writers = notebook?.['writers-group-path'];
if (props.groups?.[writers].has(window.ship)) {
newPost = (
<Link
to={newUrl}
@ -186,7 +188,7 @@ export class Notebook extends Component {
return (
<div
className='overflow-y-scroll'
className='overflow-y-scroll h-100'
style={{ paddingLeft: 16, paddingRight: 16 }}
onScroll={this.onScroll}
ref={(el) => {
@ -213,12 +215,12 @@ export class Notebook extends Component {
to={popoutHref}
target='_blank'
>
<img src='/~publish/popout.png' height={16} width={16} />
<img src='/~landscape/img/popout.png' height={16} width={16} />
</Link>
<div className='h-100 pt0 pt8-m pt8-l pt8-xl no-scrollbar'>
<div className='flex justify-between' style={{ marginBottom: 32 }}>
<div className='flex-col'>
<div className='mb1'>{notebook.title}</div>
<div className='mb1'>{notebook?.title}</div>
<span>
<span className='gray3 mr1'>by</span>
<span

View File

@ -63,7 +63,7 @@ export class Settings extends Component {
changeComments() {
this.setState({ comments: !this.state.comments, disabled: true }, (() => {
this.props.api.action('publish', 'publish-action', {
this.props.api.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.action('publish', 'publish-action', action).then(() => {
this.props.api.publishAction(action).then(() => {
this.props.history.push('/~publish');
});
}
@ -108,7 +108,7 @@ export class Settings extends Component {
disabled: true,
type: 'Converting'
}, (() => {
this.props.api.action('publish', 'publish-action', {
this.props.api.publishAction({
groupify: {
book: props.book,
target: state.targetGroup,
@ -125,7 +125,7 @@ export class Settings extends Component {
const ownedUnmanaged =
owner &&
props.notebook['writers-group-path'].slice(0, 3) === '/~/';
props.notebook?.['writers-group-path'].slice(0, 3) === '/~/';
if (!ownedUnmanaged) {
return null;
@ -162,11 +162,9 @@ export class Settings extends Component {
return (
<div>
<div className={'w-100 fl mt3 mb3'} style={{ maxWidth: '29rem' }}>
<p className="f8 mt3 lh-copy db">Convert Notebook</p>
<p className="f9 gray2 db mb4">
Convert this notebook into a group with associated chat, or select a
group to add this notebook to.
</p>
{this.renderHeader(
'Convert Notebook',
'Convert this notebook into a group with associated chat, or select a group to add this notebook to.')}
<InviteSearch
groups={props.groups}
contacts={props.contacts}
@ -185,7 +183,7 @@ export class Settings extends Component {
className={'dib f9 black gray4-d bg-gray0-d ba pa2 mt4 b--black b--gray1-d pointer'}
disabled={this.state.disabled}
>
Convert to group
{state.targetGroup ? 'Add to group' : 'Convert to group'}
</button>
</div>
</div>
@ -193,14 +191,22 @@ export class Settings extends Component {
}
}
renderHeader(title, subtitle) {
return (
<>
<p className="f9 mt6 lh-copy">{title}</p>
<p className="f9 gray2 db mb4">{subtitle}</p>
</>
);
}
render() {
const commentsSwitchClasses = (this.state.comments)
? 'relative checked bg-green2 br3 h1 toggle v-mid z-0'
: 'relative bg-gray4 bg-gray1-d br3 h1 toggle v-mid z-0';
const copyShortcode = <div>
<p className="f9 mt3 lh-copy">Share</p>
<p className="f9 gray2 mb4">Share a shortcode to join this notebook</p>
{this.renderHeader('Share', 'Share a shortcode to join this notebook')}
<div className="relative w-100 flex" style={{ maxWidth: '29rem' }}>
<input
className={'f8 mono ba b--gray3 b--gray2-d bg-gray0-d white-d ' +
@ -226,19 +232,16 @@ export class Settings extends Component {
<div className="flex-column">
{copyShortcode}
{this.renderGroupify()}
<p className="f9 mt6 lh-copy db">Delete Notebook</p>
<p className="f9 gray2 db mb4">
Permanently delete this notebook. (All current members will no
longer see this notebook)
</p>
{this.renderHeader(
'Delete Notebook',
'Permanently delete this notebook. (All current members will no longer see this notebook)')}
<button
className="bg-transparent b--red2 red2 pointer dib f9 ba pa2"
onClick={this.deleteNotebook}
>
Delete this notebook
</button>
<p className="f9 mt6 lh-copy">Rename</p>
<p className="f9 gray2 db mb4">Change the name of this notebook</p>
{this.renderHeader('Rename', 'Change the name of this notebook')}
<div className="relative w-100 flex" style={{ maxWidth: '29rem' }}>
<input
className={
@ -251,7 +254,7 @@ export class Settings extends Component {
onBlur={() => {
this.setState({ disabled: true });
this.props.api
.action('publish', 'publish-action', {
.publishAction({
'edit-book': {
book: this.props.book,
title: this.state.title,
@ -266,8 +269,7 @@ export class Settings extends Component {
}}
/>
</div>
<p className="f9 mt6 lh-copy">Change description</p>
<p className="f9 gray2 db mb4">Change the description of this notebook</p>
{this.renderHeader("Change description", "Change the description of this notebook")}
<div className="relative w-100 flex" style={{ maxWidth: '29rem' }}>
<input
className={
@ -279,7 +281,7 @@ export class Settings extends Component {
onBlur={() => {
this.setState({ disabled: true });
this.props.api
.action('publish', 'publish-action', {
.publishAction({
'edit-book': {
book: this.props.book,
title: this.props.notebook.title,

View File

@ -8,7 +8,7 @@ export class SidebarInvite extends Component {
uid: this.props.uid
}
};
window.api.action('invite-store', 'invite-action', action);
this.props.api.inviteAction(action);
}
onDecline() {
@ -18,7 +18,7 @@ export class SidebarInvite extends Component {
uid: this.props.uid
}
};
this.props.api.action('invite-store', 'invite-action', action);
this.props.api.inviteAction(action);
}
render() {

View File

@ -17,7 +17,7 @@ export class Subscribers extends Component {
path: path
}
};
this.props.api.action('group-store', 'group-action', action);
this.props.api.groupAction(action);
}
removeUser(who, path) {
@ -27,7 +27,7 @@ export class Subscribers extends Component {
path: path
}
};
this.props.api.action('group-store', 'group-action', action);
this.props.api.groupAction(action);
}
redirect(url) {

View File

@ -0,0 +1,15 @@
import React from 'react';
const NotFound = () => <div
className="fixed inter tc"
style={{
top: '50vh',
left: '50%',
transform: 'translate(-50%, -25vh)',
fontFeatureSettings: '\'zero\' 1' }}>
<h1 className="fw2 f2">404</h1>
<p className="tc">Not found.<br /><br />
If this is unexpected, email <code>support@tlon.io</code> or <a className="bb" href="https://github.com/urbit/urbit/issues/new/choose">submit an issue</a>.</p>
</div>;
export default NotFound;

View File

@ -10,7 +10,7 @@ export class Spinner extends Component {
return (
<div className={classes + ' z-2 bg-white bg-gray0-d white-d'}>
<img className="invert-d spin-active v-mid"
src="/~chat/img/Spinner.png"
src="/~landscape/img/Spinner.png"
width={16}
height={16}
/>

View File

@ -7,14 +7,16 @@ import { Sigil } from '../lib/sigil';
const getLocationName = (basePath) => {
if (basePath === '~chat')
return 'Chat';
if (basePath === '~dojo')
else if (basePath === '~dojo')
return 'Dojo';
if (basePath === '~groups')
else if (basePath === '~groups')
return 'Groups';
if (basePath === '~link')
else if (basePath === '~link')
return 'Links';
if (basePath === '~publish')
else if (basePath === '~publish')
return 'Publish';
else
return 'Unknown';
};
const StatusBar = (props) => {
@ -24,8 +26,9 @@ const StatusBar = (props) => {
? 'Home'
: getLocationName(basePath);
const popout = window.location.href.includes('popout/')
? 'dn' : 'db';
const display = (!window.location.href.includes('popout/') &&
(locationName !== 'Unknown'))
? 'db' : 'dn';
const invites = (props.invites && props.invites['/contacts'])
? props.invites['/contacts']
@ -34,7 +37,7 @@ const StatusBar = (props) => {
return (
<div
className={
'bg-white bg-gray0-d w-100 justify-between relative tc pt3 ' + popout
'bg-white bg-gray0-d w-100 justify-between relative tc pt3 ' + display
}
style={{ height: 45 }}
>
@ -54,7 +57,7 @@ const StatusBar = (props) => {
location.pathname === '/'
? null
: <Link
className="dib f9 v-mid inter ml2 no-underline"
className="dib f9 v-mid inter ml2 no-underline white-d"
to="/"
style={{ top: 14 }}
>

View File

@ -4,6 +4,7 @@ export default class InviteReducer {
reduce(json, state) {
const data = _.get(json, 'invite-update', false);
if (data) {
console.log(data);
this.initial(data, state);
this.create(data, state);
this.delete(data, state);

View File

@ -4,25 +4,32 @@ export default class MetadataReducer {
reduce(json, state) {
let data = _.get(json, 'metadata-update', false);
if (data) {
console.log('data: ', data);
this.associations(data, state);
this.add(data, state);
this.update(data, state);
this.remove(data, state);
console.log('state: ', state);
}
}
associations(json, state) {
let data = _.get(json, 'associations', false);
if (data) {
let metadata = {};
let metadata = state.associations;
Object.keys(data).forEach((key) => {
let val = data[key];
let groupPath = val['group-path'];
if (!(groupPath in metadata)) {
metadata[groupPath] = {};
let appName = val['app-name'];
let appPath = val['app-path'];
if (!(appName in metadata)) {
metadata[appName] = {};
}
metadata[groupPath][key] = val;
if (!(appPath in metadata[appName])) {
metadata[appName][appPath] = {};
}
metadata[appName][appPath] = val;
});
state.associations = metadata;
}
}
@ -31,11 +38,16 @@ export default class MetadataReducer {
let data = _.get(json, 'add', false);
if (data) {
let metadata = state.associations;
if (!(data['group-path'] in metadata)) {
metadata[data['group-path']] = {};
let appName = data['app-name'];
let appPath = data['app-path'];
if (!(appName in metadata)) {
metadata[appName] = {};
}
metadata[data['group-path']]
[`${data["group-path"]}/${data["app-name"]}${data["app-path"]}`] = data;
if (!(appPath in metadata[appName])) {
metadata[appName][appPath] = {};
}
metadata[appName][appPath] = data;
state.associations = metadata;
}
@ -45,12 +57,16 @@ export default class MetadataReducer {
let data = _.get(json, 'update-metadata', false);
if (data) {
let metadata = state.associations;
if (!(data["group-path"] in metadata)) {
metadata[data["group-path"]] = {};
let appName = data['app-name'];
let appPath = data['app-path'];
if (!(appName in metadata)) {
metadata[appName] = {};
}
metadata[data["group-path"]][
`${data["group-path"]}/${data["app-name"]}${data["app-path"]}`
] = data;
if (!(appPath in metadata[appName])) {
metadata[appName][appPath] = {};
}
metadata[appName][appPath] = data;
state.associations = metadata;
}
@ -60,12 +76,13 @@ export default class MetadataReducer {
let data = _.get(json, 'remove', false);
if (data) {
let metadata = state.associations;
if (data['group-path'] in metadata) {
let path =
`${data['group-path']}/${data['app-name']}${data['app-path']}`
delete metadata[data["group-path"]][path];
let appName = data['app-name'];
let appPath = data['app-path'];
if (appName in metadata && appPath in metadata[appName]) {
delete metadata[appName][appPath];
}
state.associations = metadata;
}
}
}
}

View File

@ -19,7 +19,11 @@ export default class BaseStore {
}
handleEvent(data) {
let json = data.data;
const json = data.data;
if (json === null) {
return;
}
if ('clear' in json && json.clear) {
this.setState(this.initialState());

View File

@ -12,7 +12,9 @@ export default class LaunchStore extends BaseStore {
launch: {
firstTime: false,
tileOrdering: [],
tiles: {}
tiles: {},
weather: {},
clock: {}
}
};
}

View File

@ -1,6 +1,8 @@
import BaseStore from './base';
import ContactReducer from '../reducers/contact-update';
import GroupReducer from '../reducers/group-update';
import LocalReducer from '../reducers/local';
import PublishReducer from '../reducers/publish-update';
import InviteReducer from '../reducers/invite-update';
import PublishResponseReducer from '../reducers/publish-response';
@ -11,7 +13,9 @@ export default class PublishStore extends BaseStore {
constructor() {
super();
this.contactReducer = new ContactReducer();
this.groupReducer = new GroupReducer();
this.localReducer = new LocalReducer();
this.publishReducer = new PublishReducer();
this.inviteReducer = new InviteReducer();
this.responseReducer = new PublishResponseReducer();
@ -34,7 +38,9 @@ export default class PublishStore extends BaseStore {
}
reduce(data, state) {
this.contactReducer.reduce(data, this.state);
this.groupReducer.reduce(data, this.state);
this.localReducer.reduce(data, this.state);
this.publishReducer.reduce(data, this.state);
this.permissionReducer.reduce(data, this.state);
this.metadataReducer.reduce(data, this.state);

View File

@ -5,7 +5,7 @@ export default class ChatSubscription extends BaseSubscription {
this.subscribe('/primary', 'chat-view');
setTimeout(() => {
this.subscribe('/synced', 'chat-hook');
this.subscribe('/primary', 'invite-view');
this.subscribe('/all', 'invite-store');
this.subscribe('/all', 'permission-store');
this.subscribe('/primary', 'contact-view');
this.subscribe('/app-name/chat', 'metadata-store');

View File

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

View File

@ -6,7 +6,7 @@ export default class GroupsSubscription extends BaseSubscription {
this.subscribe('/all', 'group-store');
this.subscribe('/all', 'metadata-store');
this.subscribe('/synced', 'contact-hook');
this.subscribe('/primary', 'invite-view');
this.subscribe('/all', 'invite-store');
this.subscribe('/all', 's3-store');
}
}

View File

@ -3,6 +3,7 @@ import BaseSubscription from './base';
export default class LaunchSubscription extends BaseSubscription {
start() {
this.subscribe('/all', 'launch');
this.subscribe('/weathertile', 'weather');
}
}

View File

@ -4,7 +4,7 @@ export default class LinksSubscription extends BaseSubscription {
start() {
this.subscribe('/all', 'group-store');
this.subscribe('/primary', 'contact-view');
this.subscribe('/primary', 'invite-view');
this.subscribe('/all', 'invite-store');
this.subscribe('/app-name/link', 'metadata-store');
this.subscribe('/app-name/contacts', 'metadata-store');
this.subscribe('/listening', 'link-listen-hook');

View File

@ -5,7 +5,7 @@ export default class PublishSubscription extends BaseSubscription {
this.subscribe('/primary', 'publish');
this.subscribe('/all', 'group-store');
this.subscribe('/primary', 'contact-view');
this.subscribe('/primary', 'invite-view');
this.subscribe('/all', 'invite-store');
this.subscribe('/all', 'permission-store');
this.subscribe('/app-name/contacts', 'metadata-store');
}