Merge pull request #2648 from urbit/mp/integrated-spinners

os1: reimplement spinner and behaviours as part of app layouts
This commit is contained in:
ixv 2020-04-02 15:24:08 -07:00 committed by GitHub
commit 506df1cc99
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
66 changed files with 582 additions and 522 deletions

View File

Before

Width:  |  Height:  |  Size: 679 B

After

Width:  |  Height:  |  Size: 679 B

View File

Before

Width:  |  Height:  |  Size: 679 B

After

Width:  |  Height:  |  Size: 679 B

View File

Before

Width:  |  Height:  |  Size: 679 B

After

Width:  |  Height:  |  Size: 679 B

View File

Before

Width:  |  Height:  |  Size: 679 B

After

Width:  |  Height:  |  Size: 679 B

View File

Before

Width:  |  Height:  |  Size: 679 B

After

Width:  |  Height:  |  Size: 679 B

View File

@ -246,16 +246,6 @@ class UrbitApi {
});
}
setSpinner(boolean) {
store.handleEvent({
data: {
local: {
spinner: boolean
}
}
})
}
setSelected(selected) {
store.handleEvent({
data: {

View File

@ -7,15 +7,6 @@ export class HeaderBar extends Component {
let popout = window.location.href.includes("popout/")
? "dn" : "dn db-m db-l db-xl";
// let spinner = !!this.props.spinner
// ? this.props.spinner : false;
// let spinnerClasses = "";
// if (spinner === true) {
// spinnerClasses = "spin-active";
// }
let invites = (this.props.invites && this.props.invites.contacts)
? this.props.invites.contacts
: {};

View File

@ -1,15 +0,0 @@
import React, { Component } from "react";
export class IconHome extends Component {
render() {
let classes = !!this.props.classes ? this.props.classes : "";
return (
<img
className={"invert-d " + classes}
src="/~chat/img/Home.png"
width={16}
height={16}
/>
);
}
}

View File

@ -0,0 +1,25 @@
import React, { Component } from 'react';
export class Spinner extends Component {
render() {
let classes = !!this.props.classes ? this.props.classes : "";
let text = !!this.props.text ? this.props.text : "";
let awaiting = !!this.props.awaiting ? this.props.awaiting : false;
if (awaiting) {
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"
width={16}
height={16} />
<p className="dib f9 ml2 v-mid inter">{text}</p>
</div>
);
}
else {
return null;
}
}
}

View File

@ -1,5 +1,6 @@
import React, { Component } from 'react';
import { InviteSearch } from './invite-search';
import { Spinner } from './icons/icon-spinner';
export class InviteElement extends Component {
@ -9,7 +10,8 @@ export class InviteElement extends Component {
this.state = {
members: [],
error: false,
success: false
success: false,
awaiting: false
};
this.setInvite = this.setInvite.bind(this);
}
@ -27,15 +29,15 @@ export class InviteElement extends Component {
return;
}
props.api.setSpinner(true);
this.setState({
error: false,
success: true,
members: []
members: [],
awaiting: true
}, () => {
props.api.groups.add(aud, props.path).then(() => {
props.api.setSpinner(false);
this.setState({awaiting: false});
});
});
}
@ -77,6 +79,7 @@ export class InviteElement extends Component {
className={modifyButtonClasses}>
{buttonText}
</button>
<Spinner awaiting={this.state.awaiting} classes="mt4" text="Inviting to chat..." />
</div>
);
}

View File

@ -1,6 +1,7 @@
import React, { Component } from 'react';
import classnames from 'classnames';
import { InviteSearch } from './lib/invite-search';
import { Spinner } from './lib/icons/icon-spinner';
import { Route, Link } from 'react-router-dom';
import { uuid, isPatTa, deSig } from '/lib/util';
import urbitOb from 'urbit-ob';
@ -19,7 +20,8 @@ export class NewScreen extends Component {
idError: false,
inviteError: false,
allowHistory: true,
createGroup: false
createGroup: false,
awaiting: false
};
this.titleChange = this.titleChange.bind(this);
@ -138,9 +140,9 @@ export class NewScreen extends Component {
error: false,
success: true,
group: [],
ships: []
ships: [],
awaiting: true
}, () => {
props.api.setSpinner(true);
// if we want a "proper group" that can be managed from the contacts UI,
// we make a path of the form /~zod/cool-group
// if not, we make a path of the form /~/~zod/free-chat
@ -162,7 +164,7 @@ export class NewScreen extends Component {
state.allowHistory
);
submit.then(() => {
props.api.setSpinner(false);
this.setState({awaiting: false});
props.history.push(`/~chat/room${appPath}`);
})
});
@ -293,6 +295,7 @@ export class NewScreen extends Component {
className={createClasses}>
Start Chat
</button>
<Spinner awaiting={this.state.awaiting} classes="mt4" text="Creating chat..." />
</div>
</div>
);

View File

@ -23,6 +23,11 @@ export class Root extends Component {
store.setStateHandler(this.setState.bind(this));
}
componentDidMount() {
//preload spinner asset
new Image().src = "/~chat/img/Spinner.png";
}
render() {
const { props, state } = this;
@ -97,7 +102,6 @@ export class Root extends Component {
associations={associations}
invites={invites}
sidebarHideOnMobile={true}
spinner={state.spinner}
sidebar={renderChannelSidebar(props)}
sidebarShown={state.sidebarShown}
>
@ -128,7 +132,6 @@ export class Root extends Component {
<Skeleton
associations={associations}
invites={invites}
spinner={state.spinner}
sidebarHideOnMobile={true}
sidebar={renderChannelSidebar(props)}
sidebarShown={state.sidebarShown}
@ -186,7 +189,6 @@ export class Root extends Component {
associations={associations}
invites={invites}
sidebarHideOnMobile={true}
spinner={state.spinner}
popout={popout}
sidebarShown={state.sidebarShown}
sidebar={renderChannelSidebar(props, station)}
@ -237,7 +239,6 @@ export class Root extends Component {
associations={associations}
invites={invites}
sidebarHideOnMobile={true}
spinner={state.spinner}
sidebarShown={state.sidebarShown}
popout={popout}
sidebar={renderChannelSidebar(props, station)}
@ -283,7 +284,6 @@ export class Root extends Component {
associations={associations}
invites={invites}
sidebarHideOnMobile={true}
spinner={state.spinner}
popout={popout}
sidebarShown={state.sidebarShown}
sidebar={renderChannelSidebar(props, station)}

View File

@ -3,7 +3,7 @@ import classnames from 'classnames';
import { deSig, uxToHex, writeText } from '/lib/util';
import { Route, Link } from "react-router-dom";
import { Spinner } from './lib/icons/icon-spinner';
import { ChatTabBar } from '/components/lib/chat-tabbar';
import { InviteSearch } from '/components/lib/invite-search';
import SidebarSwitcher from './lib/icons/icon-sidebar-switch';
@ -20,7 +20,9 @@ export class SettingsScreen extends Component {
color: "",
// groupify settings
targetGroup: null,
inclusive: false
inclusive: false,
awaiting: false,
type: "Editing chat..."
};
this.renderDelete = this.renderDelete.bind(this);
@ -49,7 +51,6 @@ export class SettingsScreen extends Component {
this.setState({
isLoading: false
}, () => {
props.api.setSpinner(false);
props.history.push('/~chat');
});
}
@ -108,17 +109,19 @@ export class SettingsScreen extends Component {
? props.association : {};
if (chatOwner) {
props.api.setSpinner(true);
props.api.metadataAdd(
association['app-path'],
association['group-path'],
association.metadata.title,
association.metadata.description,
association.metadata['date-created'],
color
).then(() => {
props.api.setSpinner(false);
})
this.setState({awaiting: true, type: "Editing chat..."}, (() => {
props.api.metadataAdd(
association['app-path'],
association['group-path'],
association.metadata.title,
association.metadata.description,
association.metadata['date-created'],
color
).then(() => {
this.setState({awaiting: false});
})
}))
}
}
}
@ -126,29 +129,29 @@ export class SettingsScreen extends Component {
deleteChat() {
const { props, state } = this;
props.api.chatView.delete(props.station);
props.api.setSpinner(true);
this.setState({
isLoading: true,
loadingText: (deSig(props.match.params.ship) === window.ship)
? 'Deleting...'
: 'Leaving...'
});
awaiting: true,
type: (deSig(props.match.params.ship) === window.ship)
? 'Deleting chat...'
: 'Leaving chat...'
}, (() => {
props.api.chatView.delete(props.station);
}));
}
groupifyChat() {
const { props, state } = this;
props.api.chatView.groupify(
props.station, state.targetGroup, state.inclusive
);
props.api.setSpinner(true);
this.setState({
isLoading: true,
loadingText: 'Converting...'
});
awaiting: true,
type: 'Converting chat...'
}, (() => {
props.api.chatView.groupify(
props.station, state.targetGroup, state.inclusive
).then(() => this.setState({awaiting: false}));
}));
}
renderDelete() {
@ -273,17 +276,18 @@ export class SettingsScreen extends Component {
onChange={this.changeTitle}
onBlur={() => {
if (chatOwner) {
props.api.setSpinner(true);
props.api.metadataAdd(
association['app-path'],
association['group-path'],
this.state.title,
association.metadata.description,
association.metadata['date-created'],
uxToHex(association.metadata.color)
).then(() => {
props.api.setSpinner(false);
})
this.setState({awaiting: true, type: "Editing chat..."}, (() => {
props.api.metadataAdd(
association['app-path'],
association['group-path'],
this.state.title,
association.metadata.description,
association.metadata['date-created'],
uxToHex(association.metadata.color)
).then(() => {
this.setState({awaiting: false});
})
}))
}
}}
/>
@ -300,17 +304,18 @@ export class SettingsScreen extends Component {
onChange={this.changeDescription}
onBlur={() => {
if (chatOwner) {
props.api.setSpinner(true);
props.api.metadataAdd(
association['app-path'],
association['group-path'],
association.metadata.title,
this.state.description,
association.metadata['date-created'],
uxToHex(association.metadata.color)
).then(() => {
props.api.setSpinner(false);
})
this.setState({awaiting: true, type: "Editing chat..."}, (() => {
props.api.metadataAdd(
association['app-path'],
association['group-path'],
association.metadata.title,
this.state.description,
association.metadata['date-created'],
uxToHex(association.metadata.color)
).then(() => {
this.setState({awaiting: false});
})
}))
}
}}
/>
@ -348,7 +353,6 @@ export class SettingsScreen extends Component {
let permission = Array.from(props.permission.who.values());
if (!!state.isLoading) {
let text = state.loadingText || 'Working...';
let title = props.station.substr(1);
@ -389,7 +393,7 @@ export class SettingsScreen extends Component {
/>
</div>
<div className="w-100 pl3 mt4 cf">
<h2 className="f8 pb2">{text}</h2>
<Spinner awaiting={this.state.awaiting} classes="absolute right-2 bottom-2 ba pa2 b--gray1-d" text={this.state.type} />
</div>
</div>
);
@ -459,6 +463,7 @@ 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}/>
</div>
</div>
);

View File

@ -32,7 +32,7 @@ export class Skeleton extends Component {
return (
// app outer skeleton
<div className={"absolute h-100 w-100 bg-gray0-d " + popoutWindow}>
<HeaderBar spinner={this.props.spinner} associations={this.props.associations} invites={this.props.invites} />
<HeaderBar associations={this.props.associations} invites={this.props.invites} />
{/* app window borders */}
<div className={
`cf w-100 flex ` +

View File

@ -5,7 +5,6 @@ export class LocalReducer {
let data = _.get(json, 'local', false);
if (data) {
this.sidebarToggle(data, state);
this.setSpinner(data, state);
this.setSelected(data, state);
}
}
@ -17,13 +16,6 @@ export class LocalReducer {
}
}
setSpinner(obj, state) {
let data = _.has(obj, 'spinner', false);
if (data) {
state.spinner = obj.spinner;
}
}
setSelected(obj, state) {
let data = _.has(obj, 'selected', false);
if (data) {

View File

@ -19,7 +19,6 @@ class Store {
chat: {},
contacts: {}
},
spinner: false,
selectedGroups: [],
sidebarShown: true,
pendingMessages: new Map([]),

View File

@ -181,16 +181,6 @@ class UrbitApi {
})
}
setSpinner(boolean) {
store.handleEvent({
data: {
local: {
spinner: boolean
}
}
})
}
setSelected(selected) {
store.handleEvent({
data: {

View File

@ -1,6 +1,7 @@
import React, { Component } from 'react';
import { Route, Link } from 'react-router-dom';
import { InviteSearch } from './invite-search';
import { Spinner } from './icons/icon-spinner';
export class AddScreen extends Component {
@ -11,7 +12,8 @@ export class AddScreen extends Component {
invites: {
groups: [],
ships: []
}
},
awaiting: false
};
this.invChange = this.invChange.bind(this);
@ -38,12 +40,12 @@ export class AddScreen extends Component {
invites: {
groups: [],
ships: []
}
},
awaiting: true
}, () => {
props.api.setSpinner(true);
let submit = props.api.group.add(props.path, aud);
submit.then(() => {
props.api.setSpinner(false);
this.setState({awaiting: false});
props.history.push("/~groups" + props.path);
})
});
@ -51,14 +53,6 @@ export class AddScreen extends Component {
render() {
const { props } = this;
let invErrElem = (<span />);
if (this.state.inviteError) {
invErrElem = (
<span className="f9 inter red2 ml3 mb5 db">
Invites must be validly formatted ship names.
</span>
);
}
return (
<div className="h-100 w-100 flex flex-column overflow-y-scroll white-d">
@ -86,6 +80,7 @@ export class AddScreen extends Component {
<Link to="/~groups">
<button className="f8 ml4 ba pa2 b--black pointer bg-transparent b--white-d white-d">Cancel</button>
</Link>
<Spinner awaiting={this.state.awaiting} classes="mt4 pl4" text="Inviting to group..." />
</div>
</div>
)

View File

@ -4,6 +4,7 @@ import { Sigil } from './icons/sigil';
import { api } from '/api';
import { Route, Link } from 'react-router-dom';
import { EditElement } from '/components/lib/edit-element';
import { Spinner } from './icons/icon-spinner';
import { uxToHex } from '/lib/util';
export class ContactCard extends Component {
@ -16,7 +17,9 @@ export class ContactCard extends Component {
emailToSet: null,
phoneToSet: null,
websiteToSet: null,
notesToSet: null
notesToSet: null,
awaiting: false,
type: "Saving to group"
};
this.editToggle = this.editToggle.bind(this);
this.sigilColorSet = this.sigilColorSet.bind(this);
@ -44,16 +47,6 @@ export class ContactCard extends Component {
});
return;
}
// sigil color updates are done by keystroke parsing on update
// other field edits are exclusively handled by setField()
let currentColor = (props.contact.color) ? props.contact.color : "000000";
currentColor = uxToHex(currentColor);
let hexExp = /([0-9A-Fa-f]{6})/
let hexTest = hexExp.exec(this.state.colorToSet);
if (hexTest && (hexTest[1] !== currentColor) && !props.share) {
api.contactEdit(props.path, `~${props.ship}`, {color: hexTest[1]});
}
}
editToggle() {
@ -116,6 +109,21 @@ export class ContactCard extends Component {
);
switch (field) {
case "color": {
let currentColor = (props.contact.color) ? props.contact.color : "000000";
currentColor = uxToHex(currentColor);
let hexExp = /([0-9A-Fa-f]{6})/
let hexTest = hexExp.exec(this.state.colorToSet);
if (hexTest && (hexTest[1] !== currentColor) && !props.share) {
this.setState({ awaiting: true, type: "Saving to group" }, (() => {
api.contactEdit(props.path, `~${props.ship}`, { color: hexTest[1] }).then(() => {
this.setState({ awaiting: false });
});
}))
}
break;
}
case "email": {
if (
(state.emailToSet === "") ||
@ -125,7 +133,11 @@ export class ContactCard extends Component {
}
let emailTestResult = emailTest.exec(state.emailToSet);
if (emailTestResult) {
api.contactEdit(props.path, ship, { email: state.emailToSet });
this.setState({ awaiting: true, type: "Saving to group" }, (() => {
api.contactEdit(props.path, ship, { email: state.emailToSet }).then(() => {
this.setState({awaiting: false});
});
}))
}
break;
}
@ -136,7 +148,12 @@ export class ContactCard extends Component {
) {
return false;
}
api.contactEdit(props.path, ship, { nickname: state.nickNameToSet });
this.setState({ awaiting: true, type: "Saving to group" }, (() => {
api.contactEdit(props.path, ship, { nickname: state.nickNameToSet }).then(() => {
this.setState({ awaiting: false });
});
}))
break;
}
case "notes": {
@ -146,7 +163,11 @@ export class ContactCard extends Component {
) {
return false;
}
api.contactEdit(props.path, ship, { notes: state.notesToSet });
this.setState({ awaiting: true, type: "Saving to group" }, (() => {
api.contactEdit(props.path, ship, { notes: state.notesToSet }).then(() => {
this.setState({ awaiting: false });
});
}))
break;
}
case "phone": {
@ -158,7 +179,11 @@ export class ContactCard extends Component {
}
let phoneTestResult = phoneTest.exec(state.phoneToSet);
if (phoneTestResult) {
api.contactEdit(props.path, ship, { phone: state.phoneToSet });
this.setState({ awaiting: true, type: "Saving to group" }, (() => {
api.contactEdit(props.path, ship, { phone: state.phoneToSet }).then(() => {
this.setState({ awaiting: false });
});
}))
}
break;
}
@ -171,37 +196,60 @@ export class ContactCard extends Component {
}
let websiteTestResult = websiteTest.exec(state.websiteToSet);
if (websiteTestResult) {
api.contactEdit(props.path, ship, { website: state.websiteToSet });
this.setState({ awaiting: true, type: "Saving to group" }, (() => {
api.contactEdit(props.path, ship, { website: state.websiteToSet }).then(() => {
this.setState({ awaiting: false });
});
}))
}
break;
}
case "removeAvatar": {
api.contactEdit(props.path, ship, { avatar: null });
this.setState({ awaiting: true, type: "Removing from group" }, (() => {
api.contactEdit(props.path, ship, { avatar: null }).then(() => {
this.setState({ awaiting: false });
});
}))
break;
}
case "removeEmail": {
this.setState({ emailToSet: "" });
api.contactEdit(props.path, ship, { email: "" });
this.setState({ emailToSet: "", awaiting: true, type: "Removing from group" }, (() => {
api.contactEdit(props.path, ship, { email: "" }).then(() => {
this.setState({awaiting: false});
});
}));
break;
}
case "removeNickname": {
this.setState({ nicknameToSet: "" });
api.contactEdit(props.path, ship, { nickname: "" });
this.setState({ nicknameToSet: "", awaiting: true, type: "Removing from group" }, (() => {
api.contactEdit(props.path, ship, { nickname: "" }).then(() => {
this.setState({awaiting: false});
});
}));
break;
}
case "removePhone": {
this.setState({ phoneToSet: "" });
api.contactEdit(props.path, ship, { phone: "" });
this.setState({ phoneToSet: "", awaiting: true, type: "Removing from group" }, (() => {
api.contactEdit(props.path, ship, { phone: "" }).then(() => {
this.setState({awaiting: false});
});
}));
break;
}
case "removeWebsite": {
this.setState({ websiteToSet: "" });
api.contactEdit(props.path, ship, { website: "" });
this.setState({ websiteToSet: "", awaiting: true, type: "Removing from group" }, (() => {
api.contactEdit(props.path, ship, { website: "" }).then(() => {
this.setState({awaiting: false});
});
}));
break;
}
case "removeNotes": {
this.setState({ notesToSet: "" });
api.contactEdit(props.path, ship, { notes: "" });
this.setState({ notesToSet: "", awaiting: true, type: "Removing from group" }, (() => {
api.contactEdit(props.path, ship, { notes: "" }).then(() => {
this.setState({awaiting: false});
});
}));
break;
}
}
@ -241,13 +289,13 @@ export class ContactCard extends Component {
color: this.pickFunction(state.colorToSet, defaultVal.color),
avatar: null
};
api.setSpinner(true);
api.contactView.share(
`~${props.ship}`, props.path, `~${window.ship}`, contact
).then(() => {
api.setSpinner(false);
props.history.push(`/~groups/view${props.path}/${window.ship}`)
});
this.setState({awaiting: true, type: "Sharing with group"}, (() => {
api.contactView.share(
`~${props.ship}`, props.path, `~${window.ship}`, contact
).then(() => {
props.history.push(`/~groups/view${props.path}/${window.ship}`)
});
}))
}
removeFromGroup() {
@ -268,13 +316,14 @@ export class ContactCard extends Component {
`~${props.ship}`, props.path, `~${window.ship}`, contact
);
api.setSpinner(true);
api.contactHook.remove(props.path, `~${props.ship}`).then(() => {
api.setSpinner(false);
let destination = (props.ship === window.ship)
? "" : props.path;
props.history.push(`/~groups${destination}`);
});
this.setState({awaiting: true, type: "Removing from group"}, (() => {
api.contactHook.remove(props.path, `~${props.ship}`).then(() => {
let destination = (props.ship === window.ship)
? "" : props.path;
this.setState({awaiting: false});
props.history.push(`/~groups${destination}`);
});
}))
}
renderEditCard() {
@ -318,6 +367,7 @@ export class ContactCard extends Component {
onChange={this.sigilColorSet}
defaultValue={defaultColor}
key={"default" + defaultColor}
onBlur={(() => this.setField("color"))}
style={{
resize: "none",
height: 40,
@ -535,6 +585,7 @@ export class ContactCard extends Component {
</button>
</div>
<div className="h-100 w-100 overflow-x-hidden pb8 white-d">{card}</div>
<Spinner awaiting={this.state.awaiting} text={`${this.state.type}...`} classes="absolute right-1 bottom-1 ba pa2 b--gray1-d" />
</div>
);
}

View File

@ -3,9 +3,16 @@ import { Route, Link } from 'react-router-dom';
import { ContactItem } from '/components/lib/contact-item';
import { ShareSheet } from '/components/lib/share-sheet';
import { Sigil } from '../lib/icons/sigil';
import { Spinner } from '../lib/icons/icon-spinner';
import { cite } from '../../lib/util';
export class ContactSidebar extends Component {
constructor(props) {
super(props);
this.state = {
awaiting: false
}
}
render() {
const { props } = this;
@ -71,11 +78,12 @@ export class ContactSidebar extends Component {
<p className={"v-mid f9 mh3 red2 pointer " + adminOpt}
style={{paddingTop: 6}}
onClick={() => {
props.api.setSpinner(true);
props.api.groupRemove(props.path, [`~${member}`])
.then(() => {
props.api.setSpinner(false);
})
this.setState({awaiting: true}, (() => {
props.api.groupRemove(props.path, [`~${member}`])
.then(() => {
this.setState({awaiting: false})
})
}))
}}>
Remove
</p>
@ -107,6 +115,7 @@ export class ContactSidebar extends Component {
{contactItems}
{groupItems}
</div>
<Spinner awaiting={this.state.awaiting} text="Removing from group..." classes="pa2 ba absolute right-1 bottom-1 b--gray1-d" />
</div>
);
}

View File

@ -1,5 +1,6 @@
import React, { Component } from 'react';
import { Route, Link } from 'react-router-dom';
import { Spinner } from './icons/icon-spinner';
import { deSig, uxToHex } from '/lib/util.js';
export class GroupDetail extends Component {
@ -8,6 +9,8 @@ export class GroupDetail extends Component {
this.state = {
title: "",
description: "",
awaiting: false,
type: "Editing"
}
this.changeTitle = this.changeTitle.bind(this);
this.changeDescription = this.changeDescription.bind(this);
@ -199,17 +202,18 @@ export class GroupDetail extends Component {
onChange={this.changeTitle}
onBlur={() => {
if (groupOwner) {
props.api.setSpinner(true);
props.api.metadataAdd(
association['app-path'],
association['group-path'],
this.state.title,
association.metadata.description,
association.metadata['date-created'],
uxToHex(association.metadata.color)
).then(() => {
props.api.setSpinner(false);
})
this.setState({awaiting: true}, (() => {
props.api.metadataAdd(
association['app-path'],
association['group-path'],
this.state.title,
association.metadata.description,
association.metadata['date-created'],
uxToHex(association.metadata.color)
).then(() => {
this.setState({awaiting: false})
})
}))
}
}}
/>
@ -226,17 +230,18 @@ export class GroupDetail extends Component {
onChange={this.changeDescription}
onBlur={() => {
if (groupOwner) {
props.api.setSpinner(true);
props.api.metadataAdd(
association['app-path'],
association['group-path'],
association.metadata.title,
this.state.description,
association.metadata['date-created'],
uxToHex(association.metadata.color)
).then(() => {
props.api.setSpinner(false);
})
this.setState({awaiting: true}, (() => {
props.api.metadataAdd(
association['app-path'],
association['group-path'],
association.metadata.title,
this.state.description,
association.metadata['date-created'],
uxToHex(association.metadata.color)
).then(() => {
this.setState({awaiting: false})
})
}))
}
}}
/>
@ -248,14 +253,15 @@ export class GroupDetail extends Component {
<a className={"dib f9 ba pa2 " + deleteButtonClasses}
onClick={() => {
if (groupOwner) {
props.api.setSpinner(true);
props.api.contactView.delete(props.path).then(() => {
props.api.setSpinner(false);
props.history.push("/~groups");
})
this.setState({awaiting: true, type: "Deleting"}, (() => {
props.api.contactView.delete(props.path).then(() => {
props.history.push("/~groups");
})
}))
}
}}>Delete this group</a>
</div>
<Spinner awaiting={this.state.awaiting} text={`${this.state.type} group...`} classes="pa2 ba absolute right-1 bottom-1 b--gray1-d"/>
</div>
)
}

View File

@ -7,15 +7,6 @@ export class HeaderBar extends Component {
let popout = window.location.href.includes("popout/")
? "dn" : "dn db-m db-l db-xl";
// let spinner = !!this.props.spinner
// ? this.props.spinner : false;
// let spinnerClasses = "";
// if (spinner === true) {
// spinnerClasses = "spin-active";
// }
let invites = (this.props.invites && this.props.invites.contacts)
? this.props.invites.contacts
: {};

View File

@ -1,16 +0,0 @@
import React, { Component } from "react";
export class IconHome extends Component {
render() {
let classes = !!this.props.classes ? this.props.classes : "";
return (
<img
className={"invert-d " + classes}
src="/~groups/img/Home.png"
width={16}
height={16}
/>
);
}
}

View File

@ -1,9 +1,25 @@
import React, { Component } from 'react';
export class IconSpinner extends Component {
export class Spinner extends Component {
render() {
return (
<div className="spinner-pending"></div>
);
let classes = !!this.props.classes ? this.props.classes : "";
let text = !!this.props.text ? this.props.text : "";
let awaiting = !!this.props.awaiting ? this.props.awaiting : false;
if (awaiting) {
return (
<div className={classes + " z-2 bg-white bg-gray0-d white-d"}>
<img className="invert-d spin-active v-mid"
src="/~groups/img/Spinner.png"
width={16}
height={16} />
<p className="dib f9 ml2 v-mid inter">{text}</p>
</div>
);
}
else {
return null;
}
}
}

View File

@ -2,6 +2,7 @@ import React, { Component } from 'react'
import { Route, Link } from 'react-router-dom';
import { InviteSearch } from './lib/invite-search';
import { Spinner } from './lib/icons/icon-spinner';
import { deSig } from '/lib/util';
import urbitOb from 'urbit-ob';
@ -19,6 +20,7 @@ export class NewScreen extends Component {
},
// color: '',
groupNameError: false,
awaiting: false
};
this.groupNameChange = this.groupNameChange.bind(this);
@ -64,16 +66,16 @@ export class NewScreen extends Component {
this.setState({
error: false,
success: true,
invites: ''
invites: '',
awaiting: true
}, () => {
props.api.setSpinner(true);
props.api.contactView.create(
group,
aud,
this.state.title,
this.state.description
).then(() => {
props.api.setSpinner(false);
this.setState({awaiting: false});
props.history.push(`/~groups${group}`);
})
});
@ -147,6 +149,7 @@ export class NewScreen extends Component {
<Link to="/~groups">
<button className="f9 ml3 ba pa2 b--black pointer bg-transparent b--white-d white-d">Cancel</button>
</Link>
<Spinner awaiting={this.state.awaiting} classes="mt4" text="Creating group..." />
</div>
</div>
);

View File

@ -22,6 +22,10 @@ export class Root extends Component {
store.setStateHandler(this.setState.bind(this));
}
componentDidMount() {
new Image().src = "/~groups/img/Spinner.png";
}
render() {
const { props, state } = this;
@ -45,7 +49,6 @@ export class Root extends Component {
return (
<Skeleton
activeDrawer="groups"
spinner={state.spinner}
selectedGroups={selectedGroups}
history={props.history}
api={api}
@ -67,7 +70,6 @@ export class Root extends Component {
render={ (props) => {
return (
<Skeleton
spinner={state.spinner}
history={props.history}
selectedGroups={selectedGroups}
api={api}
@ -100,7 +102,6 @@ export class Root extends Component {
return (
<Skeleton
spinner={state.spinner}
history={props.history}
selectedGroups={selectedGroups}
api={api}
@ -141,7 +142,6 @@ export class Root extends Component {
return (
<Skeleton
spinner={state.spinner}
history={props.history}
selectedGroups={selectedGroups}
api={api}
@ -185,7 +185,6 @@ export class Root extends Component {
return (
<Skeleton
spinner={state.spinner}
history={props.history}
api={api}
selectedGroups={selectedGroups}
@ -235,7 +234,6 @@ export class Root extends Component {
return (
<Skeleton
spinner={state.spinner}
history={props.history}
api={api}
selectedGroups={selectedGroups}
@ -271,7 +269,6 @@ export class Root extends Component {
return (
<Skeleton
spinner={state.spinner}
history={props.history}
api={api}
selectedGroups={selectedGroups}

View File

@ -12,7 +12,7 @@ export class Skeleton extends Component {
return (
<div className="h-100 w-100 ph4-m ph4-l ph4-xl pb4-m pb4-l pb4-xl">
<HeaderBar spinner={props.spinner} invites={props.invites} associations={props.associations} />
<HeaderBar invites={props.invites} associations={props.associations} />
<div className="cf w-100 h-100 h-100-m-40-ns flex ba-m ba-l ba-xl b--gray4 b--gray1-d br1">
<GroupSidebar
contacts={props.contacts}
@ -26,7 +26,7 @@ export class Skeleton extends Component {
associations={props.associations}
/>
<div
className={"h-100 w-100 " + rightPanelClasses}
className={"h-100 w-100 relative " + rightPanelClasses}
style={{ flexGrow: 1 }}>
{props.children}
</div>

View File

@ -4,17 +4,10 @@ export class LocalReducer {
reduce(json, state) {
let data = _.get(json, 'local', false);
if (data) {
this.setSpinner(data, state);
this.setSelected(data, state);
}
}
setSpinner(json, state) {
let data = _.has(json, 'spinner', false);
if (data) {
state.spinner = json.spinner;
}
}
setSelected(json, state) {
let data = _.has(json, 'selected', false);
if (data) {

View File

@ -15,8 +15,7 @@ class Store {
associations: {},
permissions: {},
invites: {},
selectedGroups: [],
spinner: false
selectedGroups: []
};
this.initialReducer = new InitialReducer();

View File

@ -237,16 +237,6 @@ class UrbitApi {
});
}
setSpinner(boolean) {
store.handleEvent({
data: {
local: {
spinner: boolean
}
}
})
}
setSelected(selected) {
store.handleEvent({
data: {

View File

@ -35,8 +35,10 @@ export class CommentItem extends Component {
let member = this.props.member || false;
let pending = !!this.props.pending ? "o-60" : "";
return (
<div className="w-100 pv3">
<div className={"w-100 pv3 " + pending}>
<div className="flex bg-white bg-gray0-d">
<Sigil
ship={"~" + props.ship}

View File

@ -7,15 +7,6 @@ export class HeaderBar extends Component {
let popout = window.location.href.includes("popout/")
? "dn" : "dn db-m db-l db-xl";
// let spinner = !!this.props.spinner
// ? this.props.spinner : false;
// let spinnerClasses = "";
// if (spinner === true) {
// spinnerClasses = "spin-active";
// }
let invites = (this.props.invites && this.props.invites.contacts)
? this.props.invites.contacts
: {};

View File

@ -1,16 +0,0 @@
import React, { Component } from "react";
export class IconHome extends Component {
render() {
let classes = !!this.props.classes ? this.props.classes : "";
return (
<img
className={"invert-d " + classes}
src="/~link/img/Home.png"
width={16}
height={16}
/>
);
}
}

View File

@ -0,0 +1,25 @@
import React, { Component } from 'react';
export class Spinner extends Component {
render() {
let classes = !!this.props.classes ? this.props.classes : "";
let text = !!this.props.text ? this.props.text : "";
let awaiting = !!this.props.awaiting ? this.props.awaiting : false;
if (awaiting) {
return (
<div className={classes + " z-2 bg-white bg-gray0-d white-d"}>
<img className="invert-d spin-active v-mid"
src="/~link/img/Spinner.png"
width={16}
height={16} />
<p className="dib f9 ml2 v-mid">{text}</p>
</div>
);
}
else {
return null;
}
}
}

View File

@ -1,5 +1,6 @@
import React, { Component } from 'react';
import { InviteSearch } from './invite-search';
import { Spinner } from './icons/icon-spinner';
export class InviteElement extends Component {
@ -8,7 +9,8 @@ export class InviteElement extends Component {
this.state = {
members: [],
error: false,
success: false
success: false,
awaiting: false
};
this.setInvite = this.setInvite.bind(this);
}
@ -26,7 +28,7 @@ export class InviteElement extends Component {
return;
}
api.setSpinner(true);
this.setState({awaiting: true});
this.setState({
error: false,
@ -34,7 +36,7 @@ export class InviteElement extends Component {
members: []
}, () => {
api.inviteToCollection(props.resourcePath, aud).then(() => {
api.setSpinner(false);
this.setState({awaiting: false});
});
});
}
@ -69,6 +71,7 @@ export class InviteElement extends Component {
className={modifyButtonClasses}>
Invite
</button>
<Spinner awaiting={this.state.awaiting} text="Inviting to collection..." classes="mt3"/>
</div>
);
}

View File

@ -1,5 +1,6 @@
import React, { Component } from "react";
import { api } from "../../api";
import { Spinner } from './icons/icon-spinner';
export class LinkSubmit extends Component {
constructor() {
@ -20,10 +21,8 @@ export class LinkSubmit extends Component {
let title = this.state.linkTitle
? this.state.linkTitle
: this.state.linkValue;
api.setSpinner(true);
this.setState({disabled: true})
api.postLink(this.props.resourcePath, link, title).then(r => {
api.setSpinner(false);
this.setState({
disabled: false,
linkValue: "",
@ -126,6 +125,7 @@ export class LinkSubmit extends Component {
}}>
Post
</button>
<Spinner awaiting={this.state.disabled} classes="mt3 absolute right-0" text="Posting to collection..." />
</div>
);
}

View File

@ -5,8 +5,10 @@ import { SidebarSwitcher } from '/components/lib/icons/icon-sidebar-switch.js';
import { api } from '../api';
import { Route, Link } from 'react-router-dom';
import { Comments } from './lib/comments';
import { Spinner } from './lib/icons/icon-spinner';
import { LoadingScreen } from './loading';
import { makeRoutePath, getContactDetails } from '../lib/util';
import CommentItem from './lib/comment-item';
export class LinkDetail extends Component {
constructor(props) {
@ -14,7 +16,9 @@ export class LinkDetail extends Component {
this.state = {
comment: "",
data: props.data,
commentFocus: false
commentFocus: false,
pending: new Set(),
disabled: false
};
this.setComment = this.setComment.bind(this);
@ -39,20 +43,36 @@ export class LinkDetail extends Component {
if (this.props.url !== prevProps.url) {
this.updateData(this.props.data);
}
if (prevProps.comments && prevProps.comments["0"] &&
this.props.comments && this.props.comments["0"]) {
let prevFirstComment = prevProps.comments["0"][0];
let thisFirstComment = this.props.comments["0"][0];
if ((prevFirstComment && prevFirstComment.udon) &&
(thisFirstComment && thisFirstComment.udon)) {
if (this.state.pending.has(thisFirstComment.udon)) {
let pending = this.state.pending;
pending.delete(thisFirstComment.udon);
this.setState({
pending: pending
});
}
}
}
}
onClickPost() {
let url = this.props.url || "";
api.setSpinner(true);
let pending = this.state.pending;
pending.add(this.state.comment);
this.setState({ pending: pending, disabled: true });
api.postComment(
this.props.resourcePath,
url,
this.state.comment
).then(() => {
api.setSpinner(false);
this.setState({ comment: "" });
this.setState({ comment: "", disabled: false });
});
}
@ -88,7 +108,24 @@ export class LinkDetail extends Component {
let focus = (this.state.commentFocus)
? "b--black b--white-d"
: "b--gray4 b--gray2-d"
: "b--gray4 b--gray2-d";
let our = getContactDetails(props.contacts[window.ship]);
let pendingArray = Array.from(this.state.pending).map((com, i) => {
return(
<CommentItem
key={i}
color={our.color}
nickname={our.nickname}
ship={window.ship}
pending={true}
content={com}
member={our.member}
time={new Date().getTime()}
/>
)
})
return (
<div className="h-100 w-100 overflow-hidden flex flex-column">
@ -121,37 +158,43 @@ export class LinkDetail extends Component {
linkIndex={props.linkIndex}
time={this.state.data.time}
/>
<div className={"relative ba br1 mt6 mb6 " + focus}>
<textarea
className="w-100 bg-gray0-d white-d f8 pa2 pr8"
style={{
resize: "none",
height: 75
}}
placeholder="Leave a comment on this link"
onChange={this.setComment}
onKeyPress={(e) => {
if ((e.getModifierState("Control") || event.getModifierState("Meta"))
&& e.key === "Enter") {
this.onClickPost();
<div className="relative">
<div className={"relative ba br1 mt6 mb6 " + focus}>
<textarea
className="w-100 bg-gray0-d white-d f8 pa2 pr8"
style={{
resize: "none",
height: 75
}}
placeholder="Leave a comment on this link"
onChange={this.setComment}
onKeyDown={e => {
if (
(e.getModifierState("Control") || e.metaKey) &&
e.key === "Enter"
) {
this.onClickPost();
}
}}
onFocus={() => this.setState({ commentFocus: true })}
onBlur={() => this.setState({ commentFocus: false })}
value={this.state.comment}
/>
<button
className={
"f8 bg-gray0-d ml2 absolute " + activeClasses
}
}}
onFocus={() => this.setState({commentFocus: true})}
onBlur={() => this.setState({commentFocus: false})}
value={this.state.comment}
/>
<button
className={
"f8 bg-gray0-d ml2 absolute " + activeClasses
}
disabled={!this.state.comment}
onClick={this.onClickPost.bind(this)}
style={{
bottom: 12,
right: 8
}}>
Post
</button>
disabled={!this.state.comment || this.state.disabled}
onClick={this.onClickPost.bind(this)}
style={{
bottom: 12,
right: 8
}}>
Post
</button>
</div>
<Spinner awaiting={this.state.disabled} classes="absolute pt5 right-0" text="Posting comment..." />
{pendingArray}
</div>
<Comments
resourcePath={props.resourcePath}

View File

@ -13,7 +13,6 @@ import { makeRoutePath, getContactDetails } from '../lib/util';
export class Links extends Component {
constructor(props) {
super(props);
this.markAllAsSeen = this.markAllAsSeen.bind(this);
}
componentDidMount() {
@ -30,10 +29,6 @@ export class Links extends Component {
}
}
markAllAsSeen() {
api.seenLink(this.props.resourcePath);
}
render() {
let props = this.props;
@ -121,11 +116,6 @@ export class Links extends Component {
<LinkSubmit resourcePath={props.resourcePath}/>
</div>
<div className="pb4">
<span
className="f9 inter gray2 ba b--gray2 br2 dib pa1 pointer"
onClick={this.markAllAsSeen}>
mark all as seen
</span>
{LinkList}
<Pagination
{...props}

View File

@ -1,5 +1,6 @@
import React, { Component } from 'react';
import { InviteSearch } from './lib/invite-search';
import { Spinner } from './lib/icons/icon-spinner';
import { Route, Link } from 'react-router-dom';
import { makeRoutePath, isPatTa, deSig } from '/lib/util';
import urbitOb from 'urbit-ob';
@ -16,7 +17,8 @@ export class NewScreen extends Component {
ships: [],
idError: false,
inviteError: false,
createGroup: false
createGroup: false,
disabled: false
};
this.titleChange = this.titleChange.bind(this);
@ -115,9 +117,9 @@ export class NewScreen extends Component {
error: false,
success: true,
group: [],
ships: []
ships: [],
disabled: true
}, () => {
api.setSpinner(true);
let submit = api.createCollection(
appPath,
state.title,
@ -126,7 +128,7 @@ export class NewScreen extends Component {
state.createGroup
);
submit.then(() => {
api.setSpinner(false);
this.setState({disabled: false})
props.history.push(makeRoutePath(appPath));
})
});
@ -231,9 +233,11 @@ export class NewScreen extends Component {
{createGroupToggle}
<button
onClick={this.onClickCreate.bind(this)}
className={createClasses}>
className={createClasses}
disabled={this.state.disabled}>
Create Collection
</button>
<Spinner awaiting={this.state.disabled} classes="mt3" text="Creating collection..." />
</div>
</div>
);

View File

@ -27,6 +27,11 @@ export class Root extends Component {
store.setStateHandler(this.setState.bind(this));
}
componentDidMount() {
//preload spinner asset
new Image().src = "/~link/img/Spinner.png";
}
render() {
const { state } = this;
@ -50,7 +55,6 @@ export class Root extends Component {
return (
<Skeleton
active="collections"
spinner={state.spinner}
associations={associations}
invites={invites}
groups={groups}
@ -73,7 +77,6 @@ export class Root extends Component {
render={(props) => {
return (
<Skeleton
spinner={state.spinner}
associations={associations}
invites={invites}
groups={groups}
@ -111,7 +114,6 @@ export class Root extends Component {
return (
<Skeleton
spinner={state.spinner}
associations={associations}
invites={invites}
groups={groups}
@ -149,7 +151,6 @@ export class Root extends Component {
return (
<Skeleton
spinner={state.spinner}
associations={associations}
invites={invites}
groups={groups}
@ -203,7 +204,6 @@ export class Root extends Component {
return (
<Skeleton
spinner={state.spinner}
associations={associations}
invites={invites}
groups={groups}
@ -259,7 +259,6 @@ export class Root extends Component {
return (
<Skeleton
spinner={state.spinner}
associations={associations}
invites={invites}
groups={groups}

View File

@ -1,9 +1,9 @@
import React, { Component } from 'react';
import classnames from 'classnames';
import { deSig, uxToHex } from '/lib/util';
import { Route, Link } from "react-router-dom";
import { LoadingScreen } from './loading';
import { Spinner } from './lib/icons/icon-spinner';
import { LinksTabBar } from '/components/lib/links-tabbar';
import SidebarSwitcher from './lib/icons/icon-sidebar-switch';
import { makeRoutePath } from '../lib/util';
@ -16,7 +16,9 @@ export class SettingsScreen extends Component {
isLoading: false,
title: "",
description: "",
color: ""
color: "",
disabled: false,
type: "Editing"
};
this.changeTitle = this.changeTitle.bind(this);
@ -44,7 +46,6 @@ export class SettingsScreen extends Component {
this.setState({
isLoading: false
}, () => {
api.setSpinner(false);
props.history.push('/~link');
});
}
@ -92,7 +93,7 @@ export class SettingsScreen extends Component {
}
if (hexTest && (hexTest[1] !== currentColor)) {
if (props.amOwner) {
api.setSpinner(true);
this.setState({disabled: true});
api.metadataAdd(
props.resourcePath,
props.groupPath,
@ -101,7 +102,7 @@ export class SettingsScreen extends Component {
resource.metadata['date-created'],
color
).then(() => {
api.setSpinner(false);
this.setState({disabled: false});
});
}
}
@ -110,35 +111,39 @@ export class SettingsScreen extends Component {
removeCollection() {
const { props, state } = this;
api.setSpinner(true);
this.setState({
isLoading: true
isLoading: true,
disabled: true,
type: "Removing"
});
api.removeCollection(props.resourcePath)
.then(() => {
this.setState({
isLoading: false
});
api.setSpinner(false);
});
}
deleteCollection() {
const { props, state } = this;
api.setSpinner(true);
this.setState({
isLoading: true
isLoading: true,
disabled: true,
type: "Deleting"
});
api.deleteCollection(props.resourcePath)
.then(() => {
this.setState({
isLoading: false
});
api.setSpinner(false);
});
}
markAllAsSeen() {
api.seenLink(this.props.resourcePath);
}
renderRemove() {
const { props, state } = this;
@ -196,11 +201,11 @@ export class SettingsScreen extends Component {
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}
disabled={!props.amOwner}
disabled={!props.amOwner || this.state.disabled}
onChange={this.changeTitle}
onBlur={() => {
if (props.amOwner) {
api.setSpinner(true);
this.setState({ disabled: true });
api.metadataAdd(
props.resourcePath,
props.groupPath,
@ -209,7 +214,7 @@ export class SettingsScreen extends Component {
resource.metadata['date-created'],
uxToHex(resource.metadata.color)
).then(() => {
api.setSpinner(false);
this.setState({ disabled: false });
});
}
}}
@ -225,20 +230,20 @@ export class SettingsScreen extends Component {
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}
disabled={!props.amOwner}
disabled={!props.amOwner || this.state.disabled}
onChange={this.changeDescription}
onBlur={() => {
if (props.amOwner) {
api.setSpinner(true);
this.setState({ disabled: true });
api.metadataAdd(
props.resourcePath,
props.groupPath,
resource.metadata.title,
state.description,
resource['date-created'],
uxToHex(resource.color)
resource.metadata['date-created'],
uxToHex(resource.metadata.color)
).then(() => {
api.setSpinner(false);
this.setState({ disabled: false });
});
}
}}
@ -260,7 +265,7 @@ export class SettingsScreen extends Component {
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}
disabled={!props.amOwner}
disabled={!props.amOwner || this.state.disabled}
onChange={this.changeColor}
onBlur={this.submitColor}
/>
@ -338,9 +343,20 @@ export class SettingsScreen extends Component {
</div>
<div className="w-100 pl3 mt4 cf">
<h2 className="f8 pb2">Collection Settings</h2>
<p className="f8 mt3 lh-copy db">Mark all links as read</p>
<p className="f9 gray2 db mb4">Mark all links in this collection as read.</p>
<a className="dib f9 black gray4-d bg-gray0-d ba pa2 b--black b--gray1-d pointer"
onClick={() => this.markAllAsSeen}>
Mark all as read
</a>
{this.renderRemove()}
{this.renderDelete()}
{this.renderMetadataSettings()}
<Spinner
awaiting={this.state.disabled}
classes="absolute right-1 bottom-1 pa2 ba b--black b--gray0-d white-d"
text={`${this.state.type} collection...`}
/>
</div>
</div>
);

View File

@ -25,7 +25,6 @@ export class Skeleton extends Component {
return (
<div className={"absolute h-100 w-100 " + popoutWindow}>
<HeaderBar
spinner={this.props.spinner}
invites={this.props.invites}
associations={this.props.associations}
/>
@ -41,7 +40,7 @@ export class Skeleton extends Component {
sidebarShown={this.props.sidebarShown}
links={this.props.links}
listening={this.props.listening}/>
<div className={"h-100 w-100 flex-auto" + rightPanelHide} style={{
<div className={"h-100 w-100 flex-auto relative " + rightPanelHide} style={{
flexGrow: 1,
}}>
{this.props.children}

View File

@ -5,7 +5,6 @@ export class LocalReducer {
let data = _.get(json, 'local', false);
if (data) {
this.sidebarToggle(data, state);
this.setSpinner(data, state);
this.setSelected(data, state);
}
}
@ -17,12 +16,6 @@ export class LocalReducer {
}
}
setSpinner(obj, state) {
let data = _.has(obj, 'spinner', false);
if (data) {
state.spinner = obj.spinner;
}
}
setSelected(obj, state) {
let data = _.has(obj, 'selected', false);
if (data) {

View File

@ -26,8 +26,7 @@ class Store {
comments: {},
seen: {},
permissions: {},
sidebarShown: true,
spinner: false
sidebarShown: true
};
this.initialReducer = new InitialReducer();

View File

@ -130,15 +130,6 @@ class UrbitApi {
});
}
setSpinner(boolean) {
store.handleEvent({
type: "local",
data: {
'spinner': boolean
}
});
}
setSelected(selected) {
store.handleEvent({
type: "local",

View File

@ -1,6 +1,7 @@
import React, { Component } from 'react'
import { CommentItem } from './comment-item';
import { dateToDa } from '/lib/util';
import { Spinner } from './icons/icon-spinner';
export class Comments extends Component {
constructor(props){
@ -49,11 +50,9 @@ export class Comments extends Component {
this.setState({pending: pendingState});
this.textArea.value = '';
window.api.setSpinner(true);
this.setState({commentBody: ""});
this.setState({commentBody: "", disabled: true});
let submit = window.api.action("publish", "publish-action", comment);
submit.then(() => {
window.api.setSpinner(false);
this.setState({ disabled: false });
})
}
@ -105,7 +104,7 @@ export class Comments extends Component {
return (
<div>
<div className="mv8">
<div className="mv8 relative">
<div>
<textarea style={{resize:'vertical'}}
ref={(el) => {this.textArea = el}}
@ -117,8 +116,8 @@ export class Comments extends Component {
aria-describedby="comment-desc"
style={{height: "4rem"}}
onChange={this.commentChange}
onKeyPress={(e) => {
if ((e.getModifierState("Control") || event.getModifierState("Meta"))
onKeyDown={(e) => {
if ((e.getModifierState("Control") || event.metaKey)
&& e.key === "Enter") {
this.commentSubmit();
}
@ -130,6 +129,7 @@ export class Comments extends Component {
className={commentClass}>
Add comment
</button>
<Spinner text="Posting comment..." awaiting={this.state.disabled} classes="absolute bottom-0 right-0 pb2"/>
</div>
{pendingArray}
{commentArray}

View File

@ -1,5 +1,6 @@
import React, { Component } from 'react';
import { SidebarSwitcher } from './icons/icon-sidebar-switch';
import { Spinner } from './icons/icon-spinner';
import { Route, Link } from 'react-router-dom';
import { Controlled as CodeMirror } from 'react-codemirror2';
import { dateToDa } from '/lib/util';
@ -11,7 +12,8 @@ export class EditPost extends Component {
super(props);
this.state = {
body: '',
submit: false
submit: false,
awaiting: false
}
this.postSubmit = this.postSubmit.bind(this);
this.bodyChange = this.bodyChange.bind(this);
@ -48,11 +50,11 @@ export class EditPost extends Component {
body: state.body
}
}
window.api.setSpinner(true);
this.setState({awaiting: true});
window.api.action("publish", "publish-action", editNote).then(() => {
let editIndex = props.location.pathname.indexOf("/edit");
let noteHref = props.location.pathname.slice(0, editIndex);
window.api.setSpinner(false);
this.setState({awaiting: false});
props.history.push(noteHref);
});
}
@ -125,6 +127,7 @@ export class EditPost extends Component {
onBeforeChange={(e, d, v) => this.bodyChange(e, d, v)}
onChange={(editor, data, value) => {}}
/>
<Spinner text="Editing post..." awaiting={this.state.awaiting} classes="absolute bottom-1 right-1 ba b--gray1-d pa2"/>
</div>
</div>
</div>

View File

@ -7,15 +7,6 @@ export class HeaderBar extends Component {
let popout = window.location.href.includes("popout/")
? "dn" : "dn db-m db-l db-xl";
// let spinner = !!this.props.spinner
// ? this.props.spinner : false;
// let spinnerClasses = "";
// if (spinner === true) {
// spinnerClasses = "spin-active";
// }
let invites = (this.props.invites && this.props.invites.contacts)
? this.props.invites.contacts
: {};

View File

@ -1,15 +0,0 @@
import React, { Component } from 'react';
export class IconHome extends Component {
render() {
let classes = !!this.props.classes ? this.props.classes : "";
return (
<img className={"invert-d " + classes}
src="/~publish/Home.png"
width={16}
height={16} />
);
}
}

View File

@ -0,0 +1,25 @@
import React, { Component } from 'react';
export class Spinner extends Component {
render() {
let classes = !!this.props.classes ? this.props.classes : "";
let text = !!this.props.text ? this.props.text : "";
let awaiting = !!this.props.awaiting ? this.props.awaiting : false;
if (awaiting) {
return (
<div className={classes + " z-2 bg-white bg-gray0-d white-d"}>
<img className="invert-d spin-active v-mid"
src="/~publish/Spinner.png"
width={16}
height={16} />
<p className="dib f9 ml2 v-mid">{text}</p>
</div>
);
}
else {
return null;
}
}
}

View File

@ -1,6 +1,7 @@
import React, { Component } from 'react'
import classnames from 'classnames';
import { Route, Link } from 'react-router-dom';
import { Spinner } from './icons/icon-spinner';
import urbitOb from 'urbit-ob';
export class JoinScreen extends Component {
@ -8,7 +9,7 @@ export class JoinScreen extends Component {
super(props);
this.state = {
book: '/',
book: '',
error: false,
awaiting: null,
disable: false
@ -36,6 +37,7 @@ export class JoinScreen extends Component {
let notebook = book[1];
if ((ship in this.props.notebooks) &&
(notebook in this.props.notebooks[ship])) {
this.setState({disable: false, book: "/"});
this.props.history.push(`/~publish/notebook/${ship}/${notebook}`)
}
}
@ -91,13 +93,11 @@ export class JoinScreen extends Component {
}
// TODO: askHistory setting
window.api.setSpinner(true);
this.setState({disable: true});
window.api.action("publish","publish-action", actionData).catch((err) => {
console.log(err)
}).then(() => {
this.setState({awaiting: text, disable: false, book: ""})
window.api.setSpinner(false);
this.setState({awaiting: text})
});
}
@ -152,7 +152,9 @@ export class JoinScreen extends Component {
style={{
resize: 'none',
}}
onChange={this.bookChange} />
onChange={this.bookChange}
value={this.state.book}
/>
{errElem}
<br />
<button
@ -160,10 +162,11 @@ export class JoinScreen extends Component {
onClick={this.onClickJoin.bind(this)}
className={joinClasses}
>Join Notebook</button>
<Spinner awaiting={this.state.disable} classes="mt4" text="Joining notebook..." />
</div>
</div>
);
}
}
export default JoinScreen
export default JoinScreen;

View File

@ -1,5 +1,6 @@
import React, { Component } from 'react'
import { SidebarSwitcher } from './icons/icon-sidebar-switch';
import { Spinner } from './icons/icon-spinner';
import { Route, Link } from 'react-router-dom';
import { Controlled as CodeMirror } from 'react-codemirror2'
import { dateToDa, stringToSymbol } from '/lib/util';
@ -35,7 +36,6 @@ export class NewPost extends Component {
}
}
window.api.setSpinner(true);
this.setState({ disabled: true });
window.api.action("publish", "publish-action", newNote).then(() => {
this.setState({ awaiting: newNote["new-note"].note, disabled: false });
@ -57,7 +57,6 @@ export class NewPost extends Component {
componentDidUpdate(prevProps, prevState) {
let notebook = this.props.notebooks[this.props.ship][this.props.book];
if (notebook.notes[this.state.awaiting]) {
window.api.setSpinner(false);
let popout = (this.props.popout) ? "popout/" : "";
let redirect =
`/~publish/${popout}note/${this.props.ship}/${this.props.book}/${this.state.awaiting}`;
@ -151,6 +150,7 @@ export class NewPost extends Component {
onBeforeChange={(e, d, v) => this.bodyChange(e, d, v)}
onChange={(editor, data, value) => {}}
/>
<Spinner text="Creating post..." awaiting={this.state.disabled} classes="absolute bottom-1 right-1 ba b--gray1-d pa2" />
</div>
</div>
</div>

View File

@ -1,5 +1,6 @@
import React, { Component } from 'react';
import { InviteSearch } from './invite-search';
import { Spinner } from './icons/icon-spinner';
import { Route, Link } from 'react-router-dom';
import { uuid, isPatTa, deSig, stringToSymbol } from "/lib/util";
import urbitOb from 'urbit-ob';
@ -30,7 +31,6 @@ export class NewScreen extends Component {
const { props, state } = this;
if (props.notebooks && (("~" + window.ship) in props.notebooks)) {
if (state.awaiting in props.notebooks["~" + window.ship]) {
props.api.setSpinner(false);
let notebook = `/~${window.ship}/${state.awaiting}`;
props.history.push("/~publish/notebook" + notebook);
}
@ -93,10 +93,8 @@ export class NewScreen extends Component {
group: groupInfo
}
}
props.api.setSpinner(true);
this.setState({awaiting: bookId, disabled: true}, () => {
props.api.action("publish", "publish-action", action).then(() => {
props.api.setSpinner(false);
});
});
}
@ -135,7 +133,7 @@ export class NewScreen extends Component {
Notebook must have a valid name.
</span>
);
}
}
return (
<div
@ -202,6 +200,7 @@ export class NewScreen extends Component {
className={createClasses}>
Create Notebook
</button>
<Spinner awaiting={this.state.awaiting} classes="mt3" text="Creating notebook..."/>
</div>
</div>
);

View File

@ -1,6 +1,7 @@
import React, { Component } from 'react';
import { Route, Link } from 'react-router-dom';
import { SidebarSwitcher } from './icons/icon-sidebar-switch';
import { Spinner } from './icons/icon-spinner';
import { Comments } from './comments';
import { NoteNavigation } from './note-navigation';
import moment from 'moment';
@ -8,8 +9,11 @@ import ReactMarkdown from 'react-markdown';
import { cite } from '../../lib/util';
export class Note extends Component {
constructor(props){
constructor(props) {
super(props);
this.state = {
deleting: false
}
moment.updateLocale('en', {
relativeTime: {
past: function(input) {
@ -119,10 +123,9 @@ export class Note extends Component {
}
let popout = (props.popout) ? "popout/" : "";
let baseUrl = `/~publish/${popout}notebook/${props.ship}/${props.book}`;
window.api.setSpinner(true);
this.setState({deleting: true});
window.api.action("publish", "publish-action", deleteAction)
.then(() => {
window.api.setSpinner(false);
props.history.push(baseUrl);
});
}
@ -251,6 +254,7 @@ export class Note extends Component {
comments={comments}
contacts={props.contacts}
/>
<Spinner text="Deleting post..." awaiting={this.state.deleting} classes="absolute bottom-1 right-1 ba b--gray1-d pa2" />
</div>
</div>
</div>

View File

@ -1,5 +1,6 @@
import React, { Component } from 'react';
import { writeText } from '../../lib/util';
import { Spinner } from './icons/icon-spinner';
export class Settings extends Component {
constructor(props){
@ -8,7 +9,8 @@ export class Settings extends Component {
title: "",
description: "",
comments: false,
disabled: false
disabled: false,
type: "Editing"
}
this.deleteNotebook = this.deleteNotebook.bind(this);
this.changeTitle = this.changeTitle.bind(this);
@ -29,13 +31,19 @@ export class Settings extends Component {
componentDidUpdate(prevProps) {
const { props } = this;
if (prevProps !== this.props) {
if (prevProps !== props) {
if (props.notebook) {
this.setState({
title: props.notebook.title,
description: props.notebook.about,
comments: props.notebook.comments
})
if (prevProps.notebook && prevProps.notebook !== props.notebook) {
if (prevProps.notebook.title !== props.notebook.title) {
this.setState({title: props.notebook.title});
}
if (prevProps.notebook.about !== props.notebook.about) {
this.setState({description: props.notebook.about});
}
if (prevProps.notebook.comments !== props.notebook.comments) {
this.setState({comments: props.notebook.comments})
}
}
}
}
}
@ -49,8 +57,7 @@ export class Settings extends Component {
}
changeComments() {
this.setState({comments: !this.state.comments}, (() => {
window.api.setSpinner(true);
this.setState({comments: !this.state.comments, disabled: true}, (() => {
window.api.action("publish", "publish-action", {
"edit-book": {
book: this.props.book,
@ -60,7 +67,7 @@ export class Settings extends Component {
group: null
}
}).then(() => {
window.api.setSpinner(false);
this.setState({disabled: false});
})
}));
}
@ -71,9 +78,8 @@ export class Settings extends Component {
book: this.props.book
}
}
window.api.setSpinner(true);
this.setState({ disabled: true, type: "Deleting" });
window.api.action("publish", "publish-action", action).then(() => {
window.api.setSpinner(false);
this.props.history.push("/~publish");
});
}
@ -132,7 +138,6 @@ export class Settings extends Component {
disabled={this.state.disabled}
onBlur={() => {
this.setState({ disabled: true });
window.api.setSpinner(true);
window.api
.action("publish", "publish-action", {
"edit-book": {
@ -145,7 +150,6 @@ export class Settings extends Component {
})
.then(() => {
this.setState({ disabled: false })
window.api.setSpinner(false);
});
}}
/>
@ -162,7 +166,6 @@ export class Settings extends Component {
onChange={this.changeDescription}
onBlur={() => {
this.setState({ disabled: true });
window.api.setSpinner(true);
window.api
.action("publish", "publish-action", {
"edit-book": {
@ -175,7 +178,6 @@ export class Settings extends Component {
})
.then(() => {
this.setState({ disabled: false });
window.api.setSpinner(false);
});
}}
/>
@ -192,6 +194,11 @@ export class Settings extends Component {
Subscribers may comment when enabled
</p>
</div>
<Spinner
awaiting={this.state.disabled}
classes="absolute right-1 bottom-1 pa2 ba b--black b--gray0-d white-d"
text={`${this.state.type} notebook...`}
/>
</div>
);
} else {

View File

@ -19,6 +19,11 @@ export class Root extends Component {
store.setStateHandler(this.setState.bind(this));
}
componentDidMount() {
//preload spinner asset
new Image().src = "/~publish/Spinner.png";
}
render() {
const { props, state } = this;
@ -36,7 +41,6 @@ export class Root extends Component {
active={"sidebar"}
rightPanelHide={true}
sidebarShown={true}
spinner={state.spinner}
invites={state.invites}
notebooks={state.notebooks}
associations={associations}
@ -62,7 +66,6 @@ export class Root extends Component {
active={"rightPanel"}
rightPanelHide={false}
sidebarShown={state.sidebarShown}
spinner={state.spinner}
invites={state.invites}
notebooks={state.notebooks}
associations={associations}
@ -89,7 +92,6 @@ export class Root extends Component {
active={"rightPanel"}
rightPanelHide={false}
sidebarShown={state.sidebarShown}
spinner={state.spinner}
invites={state.invites}
notebooks={state.notebooks}
associations={associations}
@ -128,7 +130,6 @@ export class Root extends Component {
active={"rightPanel"}
rightPanelHide={false}
sidebarShown={state.sidebarShown}
spinner={state.spinner}
invites={state.invites}
notebooks={state.notebooks}
associations={associations}
@ -153,7 +154,6 @@ export class Root extends Component {
active={"rightPanel"}
rightPanelHide={false}
sidebarShown={state.sidebarShown}
spinner={state.spinner}
invites={state.invites}
notebooks={state.notebooks}
associations={associations}
@ -199,7 +199,6 @@ export class Root extends Component {
active={"rightPanel"}
rightPanelHide={false}
sidebarShown={state.sidebarShown}
spinner={state.spinner}
invites={state.invites}
notebooks={state.notebooks}
selectedGroups={selectedGroups}
@ -224,7 +223,6 @@ export class Root extends Component {
active={"rightPanel"}
rightPanelHide={false}
sidebarShown={state.sidebarShown}
spinner={state.spinner}
invites={state.invites}
notebooks={state.notebooks}
associations={associations}

View File

@ -21,7 +21,6 @@ export class Skeleton extends Component {
return (
<div className={"absolute h-100 w-100 " + popoutWindow}>
<HeaderBar
spinner={props.spinner}
invites={props.invites}
associations={props.associations} />
<div className={`cf w-100 h-100 flex ` + popoutBorder}>

View File

@ -20,7 +20,6 @@ export class ResponseReducer {
break;
case "local":
this.sidebarToggle(json, state);
this.setSpinner(json, state);
this.setSelected(json, state);
break;
default:
@ -205,12 +204,6 @@ export class ResponseReducer {
}
}
setSpinner(json, state) {
let data = _.has(json.data, 'spinner', false);
if (data) {
state.spinner = json.data.spinner;
}
}
setSelected(json, state) {
let data = _.has(json.data, 'selected', false);
if (data) {

View File

@ -18,7 +18,6 @@ class Store {
permissions: {},
invites: {},
selectedGroups: [],
spinner: false,
sidebarShown: true
}

View File

@ -28,7 +28,7 @@ class UrbitApi {
}
soto(data) {
this.action("dojo", "sole-action",
return this.action("dojo", "sole-action",
{id: this.authTokens.dojoId, dat: data}
);
}

View File

@ -2,10 +2,15 @@ import React, { Component } from 'react';
import { store } from '../store';
import { api } from '../api';
import { cite } from '../lib/util';
import { Spinner } from './lib/icons/icon-spinner';
export class Input extends Component {
constructor(props) {
super(props);
this.state = {
awaiting: false,
type: "Sending to Dojo"
}
this.keyPress = this.keyPress.bind(this);
this.inputRef = React.createRef();
}
@ -32,8 +37,10 @@ export class Input extends Component {
// submit on enter
if (e.key === "Enter") {
store.setSpinner(true);
api.soto("ret");
this.setState({ awaiting: true, type: "Sending to Dojo"});
api.soto("ret").then(() => {
this.setState({awaiting: false});
});
}
else if ((e.key === "Backspace") && (this.props.cursor > 0)) {
@ -61,8 +68,10 @@ export class Input extends Component {
// tab completion
else if (e.key === "Tab") {
store.setSpinner(true);
api.soto({tab: this.props.cursor});
this.setState({awaiting: true, type: "Getting suggestions"})
api.soto({tab: this.props.cursor}).then(() => {
this.setState({awaiting: false})
});
}
// capture and transmit most characters
@ -74,7 +83,7 @@ export class Input extends Component {
render() {
return (
<div className="flex flex-row flex-grow-1">
<div className="flex flex-row flex-grow-1 relative">
<div className="flex-shrink-0">{cite(this.props.ship)}:dojo
</div>
<span id="prompt">
@ -100,6 +109,7 @@ render() {
ref={this.inputRef}
defaultValue={this.props.input}
/>
<Spinner awaiting={this.state.awaiting} text={`${this.state.type}...`} classes="absolute right-0 bottom-0 inter pa ba pa2 b--gray1-d"/>
</div>
)
}

View File

@ -6,15 +6,6 @@ export class HeaderBar extends Component {
let popout = window.location.href.includes("popout/")
? "dn" : "dn db-m db-l db-xl";
// let spinner = !!this.props.spinner
// ? this.props.spinner : false;
// let spinnerClasses = "";
// if (spinner === true) {
// spinnerClasses = "spin-active";
// }
let invites = (this.props.invites && this.props.invites.contacts)
? this.props.invites.contacts
: {};
@ -35,7 +26,7 @@ export class HeaderBar extends Component {
/>
</a>
<a
className="dib f9 v-mid inter ml2"
className="dib f9 v-mid inter ml2 black white-d"
href="/"
style={{ top: 14 }}>
</a> <p className="dib f9 v-mid inter ml2 white-d">Dojo</p>

View File

@ -1,16 +0,0 @@
import React, { Component } from "react";
export class IconHome extends Component {
render() {
let classes = !!this.props.classes ? this.props.classes : "";
return (
<img
className={"invert-d " + classes}
src="/~link/img/Home.png"
width={16}
height={16}
/>
);
}
}

View File

@ -0,0 +1,25 @@
import React, { Component } from 'react';
export class Spinner extends Component {
render() {
let classes = !!this.props.classes ? this.props.classes : "";
let text = !!this.props.text ? this.props.text : "";
let awaiting = !!this.props.awaiting ? this.props.awaiting : false;
if (awaiting) {
return (
<div className={classes + " z-2 bg-white bg-gray0-d white-d"}>
<img className="invert-d spin-active v-mid"
src="/~dojo/img/Spinner.png"
width={16}
height={16} />
<p className="dib f9 ml2 v-mid inter">{text}</p>
</div>
);
}
else {
return null;
}
}
}

View File

@ -16,11 +16,16 @@ export class Root extends Component {
store.setStateHandler(this.setState.bind(this));
}
componentDidMount() {
//preload spinner asset
new Image().src = "/~dojo/img/Spinner.png";
}
render() {
return (
<BrowserRouter>
<div className="w-100 h-100 bg-white bg-gray1-d">
<HeaderBar spinner={this.state.spinner}/>
<HeaderBar/>
<Route
exact path="/~dojo/:popout?"
render={(props) => {

View File

@ -9,8 +9,7 @@ export class Store {
txt: [],
prompt: '',
cursor: 0,
input: "",
spinner: false
input: ""
}
this.sync = this.sync.bind(this);
this.print = this.print.bind(this);
@ -21,18 +20,10 @@ export class Store {
if (data.data) {
var dojoReply = data.data;
}
else if (data.local) {
if (data.local.spinner) {
return this.setState({spinner: data.local.spinner})
}
}
else {
var dojoReply = data;
}
// on response, disable spinner
this.setState({spinner: false});
// %mor sole-effects are nested, so throw back to handler
if (dojoReply.map) {
return dojoReply.map(reply => this.handleEvent(reply));
@ -86,14 +77,6 @@ export class Store {
setStateHandler(setState) {
this.setState = setState;
}
setSpinner(boolean) {
store.handleEvent({
local: {
spinner: boolean
}
})
}
}
export let store = new Store();