mirror of
https://github.com/urbit/shrub.git
synced 2025-01-02 01:25:55 +03:00
Merge pull request #3231 from urbit/mp/omnibox
interface: add new omnibox navigation and header bar
This commit is contained in:
commit
f75e24a9ee
BIN
pkg/arvo/app/landscape/img/groups.png
Normal file
BIN
pkg/arvo/app/landscape/img/groups.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 693 B |
Binary file not shown.
Before Width: | Height: | Size: 255 B After Width: | Height: | Size: 582 B |
5
pkg/interface/package-lock.json
generated
5
pkg/interface/package-lock.json
generated
@ -6500,6 +6500,11 @@
|
||||
"resolved": "https://registry.npmjs.org/mousetrap/-/mousetrap-1.6.5.tgz",
|
||||
"integrity": "sha512-QNo4kEepaIBwiT8CDhP98umTetp+JNfQYBWvC1pc6/OAibuXtRcxZ58Qz8skvEHYvURne/7R8T5VoOI7rDsEUA=="
|
||||
},
|
||||
"mousetrap-global-bind": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/mousetrap-global-bind/-/mousetrap-global-bind-1.1.0.tgz",
|
||||
"integrity": "sha1-zX3pIivQZG+i4BDVTISnTCaojt0="
|
||||
},
|
||||
"move-concurrently": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/move-concurrently/-/move-concurrently-1.0.1.tgz",
|
||||
|
@ -18,6 +18,7 @@
|
||||
"markdown-to-jsx": "^6.11.4",
|
||||
"moment": "^2.20.1",
|
||||
"mousetrap": "^1.6.5",
|
||||
"mousetrap-global-bind": "^1.1.0",
|
||||
"prop-types": "^15.7.2",
|
||||
"react": "^16.5.2",
|
||||
"react-codemirror2": "^6.0.1",
|
||||
|
@ -5,6 +5,9 @@ import { BrowserRouter as Router, Route, withRouter, Switch } from 'react-router
|
||||
import styled, { ThemeProvider, createGlobalStyle } from 'styled-components';
|
||||
import { sigil as sigiljs, stringRenderer } from 'urbit-sigil-js';
|
||||
|
||||
import Mousetrap from 'mousetrap';
|
||||
import 'mousetrap-global-bind';
|
||||
|
||||
import './css/indigo-static.css';
|
||||
import './css/fonts.css';
|
||||
import light from './themes/light';
|
||||
@ -18,6 +21,7 @@ import LinksApp from './apps/links/app';
|
||||
import PublishApp from './apps/publish/app';
|
||||
|
||||
import StatusBar from './components/StatusBar';
|
||||
import Omnibox from './components/Omnibox';
|
||||
import ErrorComponent from './components/Error';
|
||||
|
||||
import GlobalStore from './store/store';
|
||||
@ -74,6 +78,10 @@ class App extends React.Component {
|
||||
this.api.local.setDark(this.themeWatcher.matches);
|
||||
this.themeWatcher.addListener(this.updateTheme);
|
||||
this.api.local.getBaseHash();
|
||||
Mousetrap.bindGlobal(['command+/', 'ctrl+/'], (e) => {
|
||||
e.preventDefault();
|
||||
this.api.local.setOmnibox();
|
||||
});
|
||||
this.setFavicon();
|
||||
}
|
||||
|
||||
@ -113,7 +121,6 @@ class App extends React.Component {
|
||||
const channel = window.channel;
|
||||
|
||||
const associations = this.state.associations ? this.state.associations : { contacts: {} };
|
||||
const selectedGroups = this.state.selectedGroups ? this.state.selectedGroups : [];
|
||||
const { state } = this;
|
||||
const theme = state.dark ? dark : light;
|
||||
|
||||
@ -121,81 +128,99 @@ class App extends React.Component {
|
||||
<ThemeProvider theme={theme}>
|
||||
<Root>
|
||||
<Router>
|
||||
<StatusBarWithRouter props={this.props}
|
||||
associations={associations}
|
||||
invites={this.state.invites}
|
||||
api={this.api}
|
||||
connection={this.state.connection}
|
||||
subscription={this.subscription}
|
||||
<StatusBarWithRouter
|
||||
props={this.props}
|
||||
associations={associations}
|
||||
invites={this.state.invites}
|
||||
api={this.api}
|
||||
connection={this.state.connection}
|
||||
subscription={this.subscription}
|
||||
/>
|
||||
<Omnibox
|
||||
associations={state.associations}
|
||||
apps={state.launch}
|
||||
api={this.api}
|
||||
dark={state.dark}
|
||||
show={state.omniboxShown}
|
||||
/>
|
||||
<Content>
|
||||
<Switch>
|
||||
<Route exact path="/"
|
||||
render={ p => (
|
||||
<LaunchApp
|
||||
ship={this.ship}
|
||||
api={this.api}
|
||||
{...state}
|
||||
{...p}
|
||||
<Switch>
|
||||
<Route
|
||||
exact
|
||||
path='/'
|
||||
render={p => (
|
||||
<LaunchApp
|
||||
ship={this.ship}
|
||||
api={this.api}
|
||||
{...state}
|
||||
{...p}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
<Route path="/~chat" render={ p => (
|
||||
<ChatApp
|
||||
ship={this.ship}
|
||||
api={this.api}
|
||||
subscription={this.subscription}
|
||||
{...state}
|
||||
{...p}
|
||||
<Route
|
||||
path='/~chat'
|
||||
render={p => (
|
||||
<ChatApp
|
||||
ship={this.ship}
|
||||
api={this.api}
|
||||
subscription={this.subscription}
|
||||
{...state}
|
||||
{...p}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
<Route path="/~dojo" render={ p => (
|
||||
<DojoApp
|
||||
ship={this.ship}
|
||||
channel={channel}
|
||||
selectedGroups={selectedGroups}
|
||||
subscription={this.subscription}
|
||||
{...p}
|
||||
<Route
|
||||
path='/~dojo'
|
||||
render={p => (
|
||||
<DojoApp
|
||||
ship={this.ship}
|
||||
channel={channel}
|
||||
subscription={this.subscription}
|
||||
{...p}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
<Route path="/~groups" render={ p => (
|
||||
<GroupsApp
|
||||
ship={this.ship}
|
||||
api={this.api}
|
||||
subscription={this.subscription}
|
||||
{...state}
|
||||
{...p}
|
||||
<Route
|
||||
path='/~groups'
|
||||
render={p => (
|
||||
<GroupsApp
|
||||
ship={this.ship}
|
||||
api={this.api}
|
||||
subscription={this.subscription}
|
||||
{...state}
|
||||
{...p}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
<Route path="/~link" render={ p => (
|
||||
<LinksApp
|
||||
ship={this.ship}
|
||||
ship={this.ship}
|
||||
api={this.api}
|
||||
subscription={this.subscription}
|
||||
{...state}
|
||||
{...p}
|
||||
<Route
|
||||
path='/~link'
|
||||
render={p => (
|
||||
<LinksApp
|
||||
ship={this.ship}
|
||||
api={this.api}
|
||||
subscription={this.subscription}
|
||||
{...state}
|
||||
{...p}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
<Route path="/~publish" render={ p => (
|
||||
<PublishApp
|
||||
ship={this.ship}
|
||||
api={this.api}
|
||||
subscription={this.subscription}
|
||||
{...state}
|
||||
{...p}
|
||||
<Route
|
||||
path='/~publish'
|
||||
render={p => (
|
||||
<PublishApp
|
||||
ship={this.ship}
|
||||
api={this.api}
|
||||
subscription={this.subscription}
|
||||
{...state}
|
||||
{...p}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
<Route
|
||||
render={(props) => (
|
||||
render={props => (
|
||||
<ErrorComponent {...props} code={404} description="Not Found" />
|
||||
)}
|
||||
/>
|
||||
/>
|
||||
</Switch>
|
||||
</Content>
|
||||
</Router>
|
||||
|
@ -1,6 +1,5 @@
|
||||
import BaseApi from "./base";
|
||||
import { StoreState } from "../store/type";
|
||||
import { SelectedGroup } from "../types/local-update";
|
||||
|
||||
export default class LocalApi extends BaseApi<StoreState> {
|
||||
getBaseHash() {
|
||||
@ -9,16 +8,6 @@ export default class LocalApi extends BaseApi<StoreState> {
|
||||
});
|
||||
}
|
||||
|
||||
setSelected(selected: SelectedGroup[]) {
|
||||
this.store.handleEvent({
|
||||
data: {
|
||||
local: {
|
||||
selected
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
sidebarToggle() {
|
||||
this.store.handleEvent({
|
||||
data: {
|
||||
@ -39,4 +28,14 @@ export default class LocalApi extends BaseApi<StoreState> {
|
||||
});
|
||||
}
|
||||
|
||||
setOmnibox() {
|
||||
this.store.handleEvent({
|
||||
data: {
|
||||
local: {
|
||||
omniboxShown: true
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -53,7 +53,6 @@ export default class ChatApp extends React.Component<ChatAppProps, {}> {
|
||||
const unreads = {};
|
||||
let totalUnreads = 0;
|
||||
|
||||
const selectedGroups = props.selectedGroups ? props.selectedGroups : [];
|
||||
const associations = props.associations
|
||||
? props.associations
|
||||
: { chat: {}, contacts: {} };
|
||||
@ -74,14 +73,7 @@ export default class ChatApp extends React.Component<ChatAppProps, {}> {
|
||||
unreads[stat] = Boolean(unread);
|
||||
if (
|
||||
unread &&
|
||||
stat in associations.chat &&
|
||||
(selectedGroups.length === 0 ||
|
||||
selectedGroups
|
||||
.map((e) => {
|
||||
return e[0];
|
||||
})
|
||||
.includes(associations.chat?.[stat]?.['group-path']) ||
|
||||
props.groups[associations.chat?.[stat]?.['group-path']]?.hidden)
|
||||
stat in associations.chat
|
||||
) {
|
||||
totalUnreads += unread;
|
||||
}
|
||||
@ -111,7 +103,6 @@ export default class ChatApp extends React.Component<ChatAppProps, {}> {
|
||||
inbox={inbox}
|
||||
messagePreviews={messagePreviews}
|
||||
associations={associations}
|
||||
selectedGroups={selectedGroups}
|
||||
contacts={contacts}
|
||||
invites={invites['/chat'] || {}}
|
||||
unreads={unreads}
|
||||
|
@ -13,8 +13,6 @@ export class Sidebar extends Component {
|
||||
render() {
|
||||
const { props } = this;
|
||||
|
||||
const selectedGroups = props.selectedGroups ? props.selectedGroups : [];
|
||||
|
||||
const contactAssoc =
|
||||
(props.associations && 'contacts' in props.associations)
|
||||
? alphabetiseAssociations(props.associations.contacts) : {};
|
||||
@ -61,15 +59,6 @@ export class Sidebar extends Component {
|
||||
|
||||
const groupedItems = Object.keys(contactAssoc)
|
||||
.filter(each => (groupedChannels[each] || []).length !== 0)
|
||||
.filter((each) => {
|
||||
if (selectedGroups.length === 0) {
|
||||
return true;
|
||||
}
|
||||
const selectedPaths = selectedGroups.map((e) => {
|
||||
return e[0];
|
||||
});
|
||||
return selectedPaths.includes(each);
|
||||
})
|
||||
.map((each, i) => {
|
||||
const channels = groupedChannels[each] || [];
|
||||
return(
|
||||
|
@ -48,7 +48,6 @@ export default class GroupsApp extends Component<GroupsAppProps, {}> {
|
||||
const invites =
|
||||
(Boolean(props.invites) && '/contacts' in props.invites) ?
|
||||
props.invites['/contacts'] : {};
|
||||
const selectedGroups = props.selectedGroups ? props.selectedGroups : [];
|
||||
const s3 = props.s3 ? props.s3 : {};
|
||||
const groups = props.groups || {};
|
||||
const associations = props.associations || {};
|
||||
@ -62,7 +61,6 @@ export default class GroupsApp extends Component<GroupsAppProps, {}> {
|
||||
return (
|
||||
<Skeleton
|
||||
activeDrawer="groups"
|
||||
selectedGroups={selectedGroups}
|
||||
history={props.history}
|
||||
api={api}
|
||||
contacts={contacts}
|
||||
@ -86,7 +84,6 @@ export default class GroupsApp extends Component<GroupsAppProps, {}> {
|
||||
return (
|
||||
<Skeleton
|
||||
history={props.history}
|
||||
selectedGroups={selectedGroups}
|
||||
api={api}
|
||||
contacts={contacts}
|
||||
groups={groups}
|
||||
@ -111,7 +108,6 @@ export default class GroupsApp extends Component<GroupsAppProps, {}> {
|
||||
return (
|
||||
<Skeleton
|
||||
history={props.history}
|
||||
selectedGroups={selectedGroups}
|
||||
api={api}
|
||||
contacts={contacts}
|
||||
groups={groups}
|
||||
@ -150,7 +146,6 @@ export default class GroupsApp extends Component<GroupsAppProps, {}> {
|
||||
return (
|
||||
<Skeleton
|
||||
history={props.history}
|
||||
selectedGroups={selectedGroups}
|
||||
api={api}
|
||||
contacts={contacts}
|
||||
invites={invites}
|
||||
@ -198,7 +193,6 @@ export default class GroupsApp extends Component<GroupsAppProps, {}> {
|
||||
return (
|
||||
<Skeleton
|
||||
history={props.history}
|
||||
selectedGroups={selectedGroups}
|
||||
api={api}
|
||||
contacts={contacts}
|
||||
groups={groups}
|
||||
@ -248,7 +242,6 @@ export default class GroupsApp extends Component<GroupsAppProps, {}> {
|
||||
<Skeleton
|
||||
history={props.history}
|
||||
api={api}
|
||||
selectedGroups={selectedGroups}
|
||||
contacts={contacts}
|
||||
groups={groups}
|
||||
invites={invites}
|
||||
@ -305,7 +298,6 @@ export default class GroupsApp extends Component<GroupsAppProps, {}> {
|
||||
<Skeleton
|
||||
history={props.history}
|
||||
api={api}
|
||||
selectedGroups={selectedGroups}
|
||||
contacts={contacts}
|
||||
groups={groups}
|
||||
invites={invites}
|
||||
@ -345,7 +337,6 @@ export default class GroupsApp extends Component<GroupsAppProps, {}> {
|
||||
<Skeleton
|
||||
history={props.history}
|
||||
api={api}
|
||||
selectedGroups={selectedGroups}
|
||||
contacts={contacts}
|
||||
groups={groups}
|
||||
invites={invites}
|
||||
|
@ -103,6 +103,7 @@ export class JoinScreen extends Component {
|
||||
spellCheck="false"
|
||||
rows={1}
|
||||
cols={32}
|
||||
autoFocus={true}
|
||||
onKeyPress={(e) => {
|
||||
if (e.key === 'Enter') {
|
||||
e.preventDefault();
|
||||
@ -110,7 +111,7 @@ export class JoinScreen extends Component {
|
||||
}
|
||||
}}
|
||||
style={{
|
||||
resize: 'none',
|
||||
resize: 'none'
|
||||
}}
|
||||
onChange={this.groupChange}
|
||||
value={this.state.group}
|
||||
|
@ -72,16 +72,6 @@ export class GroupSidebar extends Component {
|
||||
(path in props.groups)
|
||||
);
|
||||
})
|
||||
.filter((path) => {
|
||||
const selectedGroups = props.selectedGroups ? props.selectedGroups : [];
|
||||
if (selectedGroups.length === 0) {
|
||||
return true;
|
||||
}
|
||||
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);
|
||||
|
@ -17,7 +17,6 @@ export class Skeleton extends Component {
|
||||
invites={props.invites}
|
||||
activeDrawer={props.activeDrawer}
|
||||
selected={props.selected}
|
||||
selectedGroups={props.selectedGroups}
|
||||
history={props.history}
|
||||
api={props.api}
|
||||
associations={props.associations}
|
||||
|
@ -1,6 +1,7 @@
|
||||
import React from 'react';
|
||||
import classnames from 'classnames';
|
||||
import { Link } from 'react-router-dom';
|
||||
import defaultApps from '../../../../lib/default-apps';
|
||||
|
||||
import Tile from './tile';
|
||||
|
||||
@ -29,7 +30,9 @@ export default class BasicTile extends React.PureComponent {
|
||||
</span>
|
||||
);
|
||||
|
||||
const routeList = ['/~chat', '/~publish', '/~link', '/~groups', '/~dojo'];
|
||||
const routeList = defaultApps.map((e) => {
|
||||
return `/~${e}`;
|
||||
});
|
||||
|
||||
const tile = ( routeList.indexOf(props.linkedUrl) !== -1 ) ? (
|
||||
<Link className="w-100 h-100 db pa2 no-underline" to={props.linkedUrl}>
|
||||
|
@ -41,7 +41,7 @@ export class LinksApp extends Component {
|
||||
render() {
|
||||
const { props } = this;
|
||||
|
||||
const contacts = props.contacts ? props.contacts : {};
|
||||
const contacts = props.contacts ? props.contacts : {};
|
||||
|
||||
const groups = props.groups ? props.groups : {};
|
||||
|
||||
@ -51,18 +51,9 @@ export class LinksApp extends Component {
|
||||
|
||||
const seen = props.linksSeen ? props.linksSeen : {};
|
||||
|
||||
const selectedGroups = props.selectedGroups ? props.selectedGroups : [];
|
||||
|
||||
const selGroupPaths = selectedGroups.map(g => g[0]);
|
||||
const totalUnseen = _.reduce(
|
||||
links,
|
||||
(acc, collection, path) => {
|
||||
if(selGroupPaths.length > 0
|
||||
&& !selGroupPaths.includes(associations.link?.[path]?.['group-path'])) {
|
||||
return acc;
|
||||
}
|
||||
return acc + collection.unseenCount;
|
||||
},
|
||||
(acc, collection) => acc + collection.unseenCount,
|
||||
0
|
||||
);
|
||||
|
||||
@ -91,7 +82,6 @@ export class LinksApp extends Component {
|
||||
groups={groups}
|
||||
rightPanelHide={true}
|
||||
sidebarShown={sidebarShown}
|
||||
selectedGroups={selectedGroups}
|
||||
links={links}
|
||||
listening={listening}
|
||||
api={api}
|
||||
@ -109,7 +99,6 @@ export class LinksApp extends Component {
|
||||
invites={invites}
|
||||
groups={groups}
|
||||
sidebarShown={sidebarShown}
|
||||
selectedGroups={selectedGroups}
|
||||
links={links}
|
||||
listening={listening}
|
||||
api={api}
|
||||
@ -157,7 +146,6 @@ export class LinksApp extends Component {
|
||||
groups={groups}
|
||||
selected={resourcePath}
|
||||
sidebarShown={sidebarShown}
|
||||
selectedGroups={selectedGroups}
|
||||
links={links}
|
||||
listening={listening}
|
||||
api={api}
|
||||
@ -198,7 +186,6 @@ export class LinksApp extends Component {
|
||||
groups={groups}
|
||||
selected={resourcePath}
|
||||
sidebarShown={sidebarShown}
|
||||
selectedGroups={selectedGroups}
|
||||
popout={popout}
|
||||
links={links}
|
||||
listening={listening}
|
||||
@ -253,7 +240,6 @@ export class LinksApp extends Component {
|
||||
groups={groups}
|
||||
selected={resourcePath}
|
||||
sidebarShown={sidebarShown}
|
||||
selectedGroups={selectedGroups}
|
||||
sidebarHideMobile={true}
|
||||
popout={popout}
|
||||
links={links}
|
||||
@ -311,7 +297,6 @@ export class LinksApp extends Component {
|
||||
groups={groups}
|
||||
selected={resourcePath}
|
||||
sidebarShown={sidebarShown}
|
||||
selectedGroups={selectedGroups}
|
||||
sidebarHideMobile={true}
|
||||
popout={popout}
|
||||
links={links}
|
||||
|
@ -51,24 +51,14 @@ export class ChannelsSidebar extends Component {
|
||||
}
|
||||
});
|
||||
|
||||
const selectedGroups = props.selectedGroups ? props.selectedGroups : [];
|
||||
let i = -1;
|
||||
const groupedItems = Object.keys(associations)
|
||||
.filter((each) => {
|
||||
if (selectedGroups.length === 0) {
|
||||
return true;
|
||||
};
|
||||
const selectedPaths = selectedGroups.map((e) => {
|
||||
return e[0];
|
||||
});
|
||||
return selectedPaths.includes(each);
|
||||
})
|
||||
.map((each) => {
|
||||
const channels = groupedChannels[each];
|
||||
if (!channels || channels.length === 0)
|
||||
return;
|
||||
i++;
|
||||
if ((selectedGroups.length === 0) && groupedChannels['/~/'] && groupedChannels['/~/'].length !== 0) {
|
||||
if (groupedChannels['/~/'] && groupedChannels['/~/'].length !== 0) {
|
||||
i++;
|
||||
}
|
||||
|
||||
@ -84,7 +74,7 @@ export class ChannelsSidebar extends Component {
|
||||
/>
|
||||
);
|
||||
});
|
||||
if ((selectedGroups.length === 0) && groupedChannels['/~/'] && groupedChannels['/~/'].length !== 0) {
|
||||
if (groupedChannels['/~/'] && groupedChannels['/~/'].length !== 0) {
|
||||
groupedItems.unshift(
|
||||
<GroupItem
|
||||
key={'/~/'}
|
||||
|
@ -31,7 +31,6 @@ export class Skeleton extends Component {
|
||||
invites={linkInvites}
|
||||
groups={props.groups}
|
||||
selected={props.selected}
|
||||
selectedGroups={props.selectedGroups}
|
||||
sidebarShown={props.sidebarShown}
|
||||
links={props.links}
|
||||
listening={props.listening}
|
||||
|
@ -42,7 +42,6 @@ export default class PublishApp extends React.Component {
|
||||
|
||||
const contacts = props.contacts ? props.contacts : {};
|
||||
const associations = props.associations ? props.associations : { contacts: {} };
|
||||
const selectedGroups = props.selectedGroups ? props.selectedGroups : [];
|
||||
|
||||
const notebooks = props.notebooks ? props.notebooks : {};
|
||||
|
||||
@ -50,12 +49,6 @@ export default class PublishApp extends React.Component {
|
||||
.values()
|
||||
.map(_.values)
|
||||
.flatten() // flatten into array of notebooks
|
||||
.filter((each) => {
|
||||
return ((selectedGroups.map((e) => {
|
||||
return e[0];
|
||||
}).includes(each?.['writers-group-path'])) ||
|
||||
(selectedGroups.length === 0));
|
||||
})
|
||||
.map('num-unread')
|
||||
.reduce((acc, count) => acc + count, 0)
|
||||
.value();
|
||||
@ -80,7 +73,6 @@ export default class PublishApp extends React.Component {
|
||||
invites={invites}
|
||||
notebooks={notebooks}
|
||||
associations={associations}
|
||||
selectedGroups={selectedGroups}
|
||||
contacts={contacts}
|
||||
api={api}
|
||||
>
|
||||
@ -111,7 +103,6 @@ export default class PublishApp extends React.Component {
|
||||
invites={invites}
|
||||
notebooks={notebooks}
|
||||
associations={associations}
|
||||
selectedGroups={selectedGroups}
|
||||
contacts={contacts}
|
||||
api={api}
|
||||
>
|
||||
@ -142,7 +133,6 @@ export default class PublishApp extends React.Component {
|
||||
invites={invites}
|
||||
notebooks={notebooks}
|
||||
associations={associations}
|
||||
selectedGroups={selectedGroups}
|
||||
contacts={contacts}
|
||||
api={api}
|
||||
>
|
||||
@ -188,7 +178,6 @@ export default class PublishApp extends React.Component {
|
||||
invites={invites}
|
||||
notebooks={notebooks}
|
||||
associations={associations}
|
||||
selectedGroups={selectedGroups}
|
||||
contacts={contacts}
|
||||
path={path}
|
||||
api={api}
|
||||
@ -215,7 +204,6 @@ export default class PublishApp extends React.Component {
|
||||
notebooks={notebooks}
|
||||
associations={associations}
|
||||
contacts={contacts}
|
||||
selectedGroups={selectedGroups}
|
||||
path={path}
|
||||
api={api}
|
||||
>
|
||||
@ -265,7 +253,6 @@ export default class PublishApp extends React.Component {
|
||||
sidebarShown={sidebarShown}
|
||||
invites={invites}
|
||||
notebooks={notebooks}
|
||||
selectedGroups={selectedGroups}
|
||||
associations={associations}
|
||||
contacts={contacts}
|
||||
path={path}
|
||||
@ -293,7 +280,6 @@ export default class PublishApp extends React.Component {
|
||||
invites={invites}
|
||||
notebooks={notebooks}
|
||||
associations={associations}
|
||||
selectedGroups={selectedGroups}
|
||||
contacts={contacts}
|
||||
path={path}
|
||||
api={api}
|
||||
|
@ -64,23 +64,12 @@ export class Sidebar extends Component {
|
||||
}
|
||||
});
|
||||
|
||||
const selectedGroups = props.selectedGroups ? props.selectedGroups: [];
|
||||
const groupedItems = Object.keys(associations)
|
||||
.filter((each) => {
|
||||
if (selectedGroups.length === 0) {
|
||||
return true;
|
||||
}
|
||||
const selectedPaths = selectedGroups.map((e) => {
|
||||
return e[0];
|
||||
});
|
||||
return (selectedPaths.includes(each));
|
||||
})
|
||||
.map((each, i) => {
|
||||
const books = groupedNotebooks[each] || [];
|
||||
if (books.length === 0)
|
||||
return;
|
||||
if ((selectedGroups.length === 0) &&
|
||||
groupedNotebooks['/~/'] &&
|
||||
if (groupedNotebooks['/~/'] &&
|
||||
groupedNotebooks['/~/'].length !== 0) {
|
||||
i = i + 1;
|
||||
}
|
||||
@ -95,8 +84,7 @@ export class Sidebar extends Component {
|
||||
/>
|
||||
);
|
||||
});
|
||||
if ((selectedGroups.length === 0) &&
|
||||
groupedNotebooks['/~/'] &&
|
||||
if (groupedNotebooks['/~/'] &&
|
||||
groupedNotebooks['/~/'].length !== 0) {
|
||||
groupedItems.unshift(
|
||||
<GroupItem
|
||||
|
@ -30,7 +30,6 @@ export class Skeleton extends Component {
|
||||
path={props.path}
|
||||
invites={props.invites}
|
||||
associations={props.associations}
|
||||
selectedGroups={props.selectedGroups}
|
||||
api={this.props.api}
|
||||
/>
|
||||
<div className={'h-100 w-100 relative white-d flex-auto ' + rightPanelHide} style={{
|
||||
|
@ -1,249 +0,0 @@
|
||||
import React, { Component } from 'react';
|
||||
import { Link } from 'react-router-dom';
|
||||
|
||||
|
||||
export default class GroupFilter extends Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
open: false,
|
||||
selected: [],
|
||||
groups: [],
|
||||
searchTerm: '',
|
||||
results: []
|
||||
};
|
||||
this.toggleOpen = this.toggleOpen.bind(this);
|
||||
this.handleClickOutside = this.handleClickOutside.bind(this);
|
||||
this.groupIndex = this.groupIndex.bind(this);
|
||||
this.search = this.search.bind(this);
|
||||
this.addGroup = this.addGroup.bind(this);
|
||||
this.deleteGroup = this.deleteGroup.bind(this);
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
document.addEventListener('mousedown', this.handleClickOutside);
|
||||
this.groupIndex();
|
||||
const selected = localStorage.getItem('urbit-selectedGroups');
|
||||
if (selected) {
|
||||
this.setState({ selected: JSON.parse(selected) }, (() => {
|
||||
this.props.api.local.setSelected(this.state.selected);
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
document.removeEventListener('mousedown', this.handleClickOutside);
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps) {
|
||||
if (prevProps !== this.props) {
|
||||
this.groupIndex();
|
||||
}
|
||||
}
|
||||
|
||||
handleClickOutside(evt) {
|
||||
if ((this.dropdown && !this.dropdown.contains(evt.target))
|
||||
&& (this.toggleButton && !this.toggleButton.contains(evt.target))) {
|
||||
this.setState({ open: false });
|
||||
}
|
||||
}
|
||||
|
||||
toggleOpen() {
|
||||
this.setState({ open: !this.state.open });
|
||||
}
|
||||
|
||||
groupIndex() {
|
||||
const { props } = this;
|
||||
let index = [];
|
||||
const associations =
|
||||
(props.associations && 'contacts' in props.associations) ?
|
||||
props.associations.contacts : {};
|
||||
index = Object.keys(associations).map((each) => {
|
||||
const eachGroup = [];
|
||||
eachGroup.push(each);
|
||||
let name = each;
|
||||
if (associations[each].metadata) {
|
||||
name = (associations[each].metadata.title !== '')
|
||||
? associations[each].metadata.title : name;
|
||||
}
|
||||
eachGroup.push(name);
|
||||
return eachGroup;
|
||||
});
|
||||
this.setState({ groups: index });
|
||||
}
|
||||
|
||||
search(evt) {
|
||||
this.setState({ searchTerm: evt.target.value });
|
||||
const term = evt.target.value.toLowerCase();
|
||||
|
||||
if (term.length < 3) {
|
||||
return this.setState({ results: [] });
|
||||
}
|
||||
|
||||
let groupMatches = [];
|
||||
groupMatches = this.state.groups.filter((e) => {
|
||||
return (e[0].includes(term) || e[1].includes(term));
|
||||
});
|
||||
this.setState({ results: groupMatches });
|
||||
}
|
||||
|
||||
addGroup(group) {
|
||||
const selected = this.state.selected;
|
||||
if (!(group in selected)) {
|
||||
selected.push(group);
|
||||
}
|
||||
this.setState({
|
||||
searchTerm: '',
|
||||
selected: selected,
|
||||
results: []
|
||||
}, (() => {
|
||||
this.props.api.local.setSelected(this.state.selected);
|
||||
localStorage.setItem('urbit-selectedGroups', JSON.stringify(this.state.selected));
|
||||
}));
|
||||
}
|
||||
|
||||
deleteGroup(group) {
|
||||
let selected = this.state.selected;
|
||||
selected = selected.filter((e) => {
|
||||
return e !== group;
|
||||
});
|
||||
this.setState({ selected: selected }, (() => {
|
||||
this.props.api.local.setSelected(this.state.selected);
|
||||
localStorage.setItem('urbit-selectedGroups', JSON.stringify(this.state.selected));
|
||||
}));
|
||||
}
|
||||
|
||||
render() {
|
||||
const { props, state } = this;
|
||||
|
||||
let currentGroup = 'All Groups';
|
||||
|
||||
if (state.selected.length > 0) {
|
||||
const titles = state.selected.map((each) => {
|
||||
return each[1];
|
||||
});
|
||||
currentGroup = titles.join(' + ');
|
||||
}
|
||||
|
||||
const buttonOpened = (state.open)
|
||||
? 'bg-gray5 bg-gray1-d white-d' : 'hover-bg-gray5 hover-bg-gray1-d white-d';
|
||||
|
||||
const dropdownClass = (state.open)
|
||||
? 'absolute db z-2 bg-white bg-gray0-d white-d ba b--gray3 b--gray1-d'
|
||||
: 'dn';
|
||||
|
||||
const inviteCount = (props.invites && Object.keys(props.invites).length > 0)
|
||||
? <template className="dib fr">
|
||||
<p className="dib bg-green2 bg-gray2-d white fw6 ph1 br1 v-mid" style={{ marginBottom: 2 }}>
|
||||
{Object.keys(props.invites).length}
|
||||
</p>
|
||||
<span className="dib v-mid ml1">
|
||||
<img
|
||||
className="v-mid"
|
||||
src="/~landscape/img/Chevron.png"
|
||||
style={{ height: 16, width: 16, paddingBottom: 1 }}
|
||||
/>
|
||||
</span>
|
||||
</template>
|
||||
: <template className="dib fr">
|
||||
<span className="dib v-top ml1">
|
||||
<img className="v-mid"
|
||||
src="/~landscape/img/Chevron.png"
|
||||
style={{ height: 16, width: 16, paddingBottom: 1 }}
|
||||
/>
|
||||
</span>
|
||||
</template>;
|
||||
|
||||
let selectedGroups = <div />;
|
||||
let searchResults = <div />;
|
||||
|
||||
if (state.results.length > 0) {
|
||||
const groupResults = state.results.map(((group) => {
|
||||
return(
|
||||
<li
|
||||
key={group[0]}
|
||||
className="tl list white-d f9 pv2 ph3 pointer hover-bg-gray4 hover-bg-gray1-d inter"
|
||||
onClick={() => this.addGroup(group)}
|
||||
>
|
||||
<span className="mix-blend-diff white">{(group[1]) ? group[1] : group[0]}</span>
|
||||
</li>
|
||||
);
|
||||
}));
|
||||
searchResults = (
|
||||
<div className={'tl absolute bg-white bg-gray0-d white-d pv3 z-1 w-100 ba b--gray4 b--white-d overflow-y-scroll'} style={{ maxWidth: '15.67rem', maxHeight: '8rem' }}>
|
||||
<p className="f9 tl gray2 ph3 pb2">Groups</p>
|
||||
{groupResults}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (state.selected.length > 0) {
|
||||
const allSelected = this.state.selected.map((each) => {
|
||||
const name = each[1];
|
||||
return(
|
||||
<span
|
||||
key={each[0]}
|
||||
className={'f9 inter black pa2 bg-gray5 bg-gray1-d ' +
|
||||
'ba b--gray4 b--gray2-d white-d dib mr2 mt2 c-default'}
|
||||
>
|
||||
{name}
|
||||
<span
|
||||
className="white-d ml3 mono pointer"
|
||||
onClick={e => this.deleteGroup(each)}
|
||||
>
|
||||
x
|
||||
</span>
|
||||
</span>
|
||||
);
|
||||
});
|
||||
selectedGroups = (
|
||||
<div className={
|
||||
'f9 gray2 bb bl br b--gray3 b--gray2-d bg-gray0-d ' +
|
||||
'white-d pa3 db w-100 inter bg-gray5 lh-solid tl'
|
||||
}
|
||||
style={{ width: 251 }}
|
||||
>
|
||||
{allSelected}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="ml1 dib">
|
||||
<div className={buttonOpened}
|
||||
onClick={() => this.toggleOpen()}
|
||||
ref={el => this.toggleButton = el}
|
||||
>
|
||||
<p className="inter dib f9 pointer pv1 ph2 mw5 truncate v-mid">{currentGroup}</p>
|
||||
</div>
|
||||
<div className={dropdownClass}
|
||||
style={{ maxHeight: '24rem', width: 285 }}
|
||||
ref={(el) => {
|
||||
this.dropdown = el;
|
||||
}}
|
||||
>
|
||||
<p className="tc bb b--gray3 b--gray1-d gray3 pv4 f9">Group Select and Filter</p>
|
||||
<Link to="/~groups"
|
||||
className="ma4 bg-gray5 bg-gray1-d f9 tl pa1 br1 db no-underline"
|
||||
style={{ paddingLeft: '6.5px', paddingRight: '6.5px' }}
|
||||
onClick={() => this.setState({ open: false })}
|
||||
>
|
||||
Manage all Groups
|
||||
{inviteCount}
|
||||
</Link>
|
||||
<p className="pt4 gray3 f9 tl mh4">Filter Groups</p>
|
||||
<div className="relative w-100 ph4 pt2 pb4">
|
||||
<input className="ba b--gray3 white-d bg-gray0-d inter w-100 f9 pa2"
|
||||
style={{ boxSizing: 'border-box' }}
|
||||
placeholder="Group name..."
|
||||
onChange={this.search}
|
||||
value={state.searchTerm}
|
||||
/>
|
||||
{searchResults}
|
||||
{selectedGroups}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
249
pkg/interface/src/components/Omnibox.js
Normal file
249
pkg/interface/src/components/Omnibox.js
Normal file
@ -0,0 +1,249 @@
|
||||
import React, { Component } from 'react';
|
||||
import { withRouter } from 'react-router-dom';
|
||||
import { Box, Row, Rule, Text } from '@tlon/indigo-react';
|
||||
import index from '../lib/omnibox';
|
||||
import Mousetrap from 'mousetrap';
|
||||
import OmniboxInput from './OmniboxInput';
|
||||
import OmniboxResult from './OmniboxResult';
|
||||
|
||||
import { cite } from '../lib/util';
|
||||
|
||||
export class Omnibox extends Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
index: new Map([]),
|
||||
query: '',
|
||||
results: this.initialResults(),
|
||||
selected: ''
|
||||
};
|
||||
this.handleClickOutside = this.handleClickOutside.bind(this);
|
||||
this.search = this.search.bind(this);
|
||||
this.navigate = this.navigate.bind(this);
|
||||
this.control - this.control.bind(this);
|
||||
this.setPreviousSelected = this.setPreviousSelected.bind(this);
|
||||
this.setNextSelected = this.setNextSelected.bind(this);
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps) {
|
||||
if (prevProps !== this.props) {
|
||||
this.setState({ index: index(this.props.associations, this.props.apps.tiles) });
|
||||
}
|
||||
|
||||
if (prevProps && this.props.show && prevProps.show !== this.props.show) {
|
||||
Mousetrap.bind('escape', () => this.props.api.local.setOmnibox());
|
||||
document.addEventListener('mousedown', this.handleClickOutside);
|
||||
const touchstart = new Event('touchstart');
|
||||
this.omniInput.input.dispatchEvent(touchstart);
|
||||
this.omniInput.input.focus();
|
||||
}
|
||||
}
|
||||
|
||||
componentWillUpdate(prevProps) {
|
||||
if (this.props.show && prevProps.show !== this.props.show) {
|
||||
Mousetrap.unbind('escape');
|
||||
document.removeEventListener('mousedown', this.handleClickOutside);
|
||||
}
|
||||
}
|
||||
|
||||
control(evt) {
|
||||
if (evt.key === 'Escape') {
|
||||
if (this.state.query.length > 0) {
|
||||
this.setState({ query: '', results: this.initialResults() });
|
||||
} else if (this.props.show) {
|
||||
this.props.api.local.setOmnibox();
|
||||
}
|
||||
};
|
||||
|
||||
if (
|
||||
evt.key === 'ArrowUp' ||
|
||||
(evt.shiftKey && evt.key === 'Tab')) {
|
||||
evt.preventDefault();
|
||||
return this.setPreviousSelected();
|
||||
}
|
||||
|
||||
if (evt.key === 'ArrowDown' || evt.key === 'Tab') {
|
||||
evt.preventDefault();
|
||||
this.setNextSelected();
|
||||
}
|
||||
|
||||
if (evt.key === 'Enter') {
|
||||
if (this.state.selected !== '') {
|
||||
this.navigate(this.state.selected);
|
||||
} else {
|
||||
this.navigate(Array.from(this.state.results.values()).flat()[0].link);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
handleClickOutside(evt) {
|
||||
if (this.props.show && !this.omniBox.contains(evt.target)) {
|
||||
this.setState({ results: this.initialResults(), query: '' }, () => {
|
||||
this.props.api.local.setOmnibox();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
initialResults() {
|
||||
return new Map([
|
||||
['commands', []],
|
||||
['subscriptions', []],
|
||||
['groups', []],
|
||||
['apps', []]
|
||||
]);
|
||||
}
|
||||
|
||||
navigate(link) {
|
||||
const { props } = this;
|
||||
this.setState({ results: this.initialResults(), query: '' }, () => {
|
||||
props.api.local.setOmnibox();
|
||||
props.history.push(link);
|
||||
});
|
||||
}
|
||||
|
||||
search(event) {
|
||||
const { state } = this;
|
||||
const query = event.target.value;
|
||||
const results = this.initialResults();
|
||||
|
||||
this.setState({ query: query });
|
||||
|
||||
// wipe results if backspacing
|
||||
if (query.length === 0) {
|
||||
this.setState({ results: results, selected: '' });
|
||||
return;
|
||||
}
|
||||
|
||||
// don't search for single characters
|
||||
if (query.length === 1) {
|
||||
return;
|
||||
}
|
||||
|
||||
['commands', 'subscriptions', 'groups', 'apps'].map((category) => {
|
||||
const categoryIndex = state.index.get(category);
|
||||
results.set(category,
|
||||
categoryIndex.filter((result) => {
|
||||
return (
|
||||
result.title.toLowerCase().includes(query) ||
|
||||
result.link.toLowerCase().includes(query) ||
|
||||
result.app.toLowerCase().includes(query) ||
|
||||
(result.host !== null ? result.host.includes(query) : false)
|
||||
);
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
this.setState({ results: results });
|
||||
}
|
||||
|
||||
setPreviousSelected() {
|
||||
const current = this.state.selected;
|
||||
const flattenedResults = Array.from(this.state.results.values()).flat();
|
||||
const totalLength = flattenedResults.length;
|
||||
if (current !== '') {
|
||||
const currentIndex = flattenedResults.indexOf(
|
||||
...flattenedResults.filter((e) => {
|
||||
return e.link === current;
|
||||
})
|
||||
);
|
||||
if (currentIndex > 0) {
|
||||
const nextLink = flattenedResults[currentIndex - 1].link;
|
||||
this.setState({ selected: nextLink });
|
||||
} else {
|
||||
const nextLink = flattenedResults[totalLength - 1].link;
|
||||
this.setState({ selected: nextLink });
|
||||
}
|
||||
} else {
|
||||
const nextLink = flattenedResults[totalLength - 1].link;
|
||||
this.setState({ selected: nextLink });
|
||||
}
|
||||
}
|
||||
|
||||
setNextSelected() {
|
||||
const current = this.state.selected;
|
||||
const flattenedResults = Array.from(this.state.results.values()).flat();
|
||||
if (current !== '') {
|
||||
const currentIndex = flattenedResults.indexOf(
|
||||
...flattenedResults.filter((e) => {
|
||||
return e.link === current;
|
||||
})
|
||||
);
|
||||
if (currentIndex < flattenedResults.length - 1) {
|
||||
const nextLink = flattenedResults[currentIndex + 1].link;
|
||||
this.setState({ selected: nextLink });
|
||||
} else {
|
||||
const nextLink = flattenedResults[0].link;
|
||||
this.setState({ selected: nextLink });
|
||||
}
|
||||
} else {
|
||||
const nextLink = flattenedResults[0].link;
|
||||
this.setState({ selected: nextLink });
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
const { props, state } = this;
|
||||
const categoryResult = [];
|
||||
|
||||
const renderResults = <Box maxHeight="400px" overflowY="scroll" overflowX="hidden">
|
||||
{categoryResult}
|
||||
</Box>;
|
||||
|
||||
['commands', 'subscriptions', 'groups', 'apps'].map((category, i) => {
|
||||
const categoryResults = state.results.get(category);
|
||||
if (categoryResults.length > 0) {
|
||||
const each = categoryResults.map((result, i) => {
|
||||
return <OmniboxResult
|
||||
key={i}
|
||||
icon={result.app}
|
||||
text={result.title}
|
||||
subtext={cite(result.host)}
|
||||
link={result.link}
|
||||
navigate={() => this.navigate(result.link)}
|
||||
selected={this.state.selected}
|
||||
dark={props.dark}
|
||||
/>;
|
||||
});
|
||||
categoryResult.push(<Box key={i} width='max(50vw, 300px)' maxWidth='600px'>
|
||||
<Rule borderTopWidth="0.5px" color="washedGray" />
|
||||
<Text gray ml={2}>{category.charAt(0).toUpperCase() + category.slice(1)}</Text>
|
||||
{each}
|
||||
</Box>);
|
||||
}
|
||||
});
|
||||
|
||||
return (
|
||||
<Box
|
||||
backgroundColor='lightGray'
|
||||
width='100vw'
|
||||
height='100vh'
|
||||
position='absolute'
|
||||
top='0'
|
||||
right='0'
|
||||
zIndex='9'
|
||||
display={props.show ? 'block' : 'none'}>
|
||||
<Row justifyContent='center'>
|
||||
<Box
|
||||
mt='20vh'
|
||||
width='max(50vw, 300px)'
|
||||
maxWidth='600px'
|
||||
borderRadius='2'
|
||||
backgroundColor='white'
|
||||
ref={(el) => {
|
||||
this.omniBox = el;
|
||||
}}>
|
||||
<OmniboxInput
|
||||
ref={(el) => { this.omniInput = el; }}
|
||||
control={e => this.control(e)}
|
||||
search={this.search}
|
||||
query={state.query}
|
||||
/>
|
||||
{renderResults}
|
||||
</Box>
|
||||
</Row>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default withRouter(Omnibox);
|
25
pkg/interface/src/components/OmniboxInput.js
Normal file
25
pkg/interface/src/components/OmniboxInput.js
Normal file
@ -0,0 +1,25 @@
|
||||
|
||||
import React, { Component } from 'react';
|
||||
|
||||
export class OmniboxInput extends Component {
|
||||
render() {
|
||||
const { props } = this;
|
||||
return (
|
||||
<input
|
||||
ref={(el) => {
|
||||
this.input = el;
|
||||
}
|
||||
}
|
||||
className='ba b--transparent w-100 br2 white-d bg-gray0-d inter f9 pa2'
|
||||
style={{ maxWidth: 'calc(600px - 1.15rem)', boxSizing: 'border-box' }}
|
||||
placeholder='Search...'
|
||||
onKeyDown={props.control}
|
||||
onChange={props.search}
|
||||
value={props.query}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default OmniboxInput;
|
||||
|
83
pkg/interface/src/components/OmniboxResult.js
Normal file
83
pkg/interface/src/components/OmniboxResult.js
Normal file
@ -0,0 +1,83 @@
|
||||
import React, { Component } from 'react';
|
||||
import { Row, Icon, Text } from '@tlon/indigo-react';
|
||||
import defaultApps from '../lib/default-apps';
|
||||
|
||||
export class OmniboxResult extends Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
isSelected: false,
|
||||
hovered: false
|
||||
};
|
||||
this.setHover = this.setHover.bind(this);
|
||||
}
|
||||
|
||||
setHover(boolean) {
|
||||
this.setState({ hovered: boolean });
|
||||
}
|
||||
render() {
|
||||
const { icon, text, subtext, link, navigate, selected, dark } = this.props;
|
||||
|
||||
let invertGraphic = {};
|
||||
|
||||
if (icon.toLowerCase() !== 'dojo') {
|
||||
invertGraphic = (!dark && this.state.hovered) ||
|
||||
selected === link ||
|
||||
(dark && !(this.state.hovered || selected === link))
|
||||
? { filter: 'invert(1)', paddingTop: 2 }
|
||||
: { filter: 'invert(0)', paddingTop: 2 };
|
||||
} else {
|
||||
invertGraphic =
|
||||
(!dark && this.state.hovered) ||
|
||||
selected === link ||
|
||||
(dark && !(this.state.hovered || selected === link))
|
||||
? { filter: 'invert(0)', paddingTop: 2 }
|
||||
: { filter: 'invert(1)', paddingTop: 2 };
|
||||
}
|
||||
|
||||
let graphic = <div />;
|
||||
if (defaultApps.includes(icon.toLowerCase()) || icon.toLowerCase() === 'links') {
|
||||
graphic = <img className="mr2 v-mid" height="12" width="12" src={`/~landscape/img/${icon.toLowerCase()}.png`} style={invertGraphic} />;
|
||||
} else {
|
||||
graphic = <Icon verticalAlign="middle" mr={2} size="12px" />;
|
||||
}
|
||||
return (
|
||||
<Row
|
||||
py='2'
|
||||
px='2'
|
||||
display='flex'
|
||||
flexDirection='row'
|
||||
style={{ cursor: 'pointer' }}
|
||||
onMouseEnter={() => this.setHover(true)}
|
||||
onMouseLeave={() => this.setHover(false)}
|
||||
backgroundColor={
|
||||
this.state.hovered || selected === link ? 'blue' : 'white'
|
||||
}
|
||||
onClick={navigate}
|
||||
width="100%"
|
||||
>
|
||||
{this.state.hovered || selected === link ? (
|
||||
<>
|
||||
{graphic}
|
||||
<Text color='white' mr='1' style={{ 'flex-shrink': 0 }}>
|
||||
{text}
|
||||
</Text>
|
||||
<Text pr='2' color='white' width='100%' textAlign='right'>
|
||||
{subtext}
|
||||
</Text>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
{graphic}
|
||||
<Text mr='1' style={{ 'flex-shrink': 0 }}>{text}</Text>
|
||||
<Text pr='2' gray width='100%' textAlign='right'>
|
||||
{subtext}
|
||||
</Text>
|
||||
</>
|
||||
)}
|
||||
</Row>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default OmniboxResult;
|
46
pkg/interface/src/components/ReconnectButton.js
Normal file
46
pkg/interface/src/components/ReconnectButton.js
Normal file
@ -0,0 +1,46 @@
|
||||
import React from 'react';
|
||||
import { Box, Text } from '@tlon/indigo-react';
|
||||
|
||||
const ReconnectButton = ({ connection, subscription }) => {
|
||||
const connectedStatus = connection || 'connected';
|
||||
const reconnect = subscription.restart.bind(subscription);
|
||||
if (connectedStatus === 'disconnected') {
|
||||
return (
|
||||
<>
|
||||
<Box
|
||||
ml={4}
|
||||
px={2}
|
||||
py={1}
|
||||
display='inline-block'
|
||||
color='red'
|
||||
border={1}
|
||||
lineHeight='min'
|
||||
borderRadius={2}
|
||||
style={{ cursor: 'pointer' }}
|
||||
onClick={reconnect}>
|
||||
<Text color='red'>Reconnect ↻</Text>
|
||||
</Box>
|
||||
</>
|
||||
);
|
||||
} else if (connectedStatus === 'reconnecting') {
|
||||
return (
|
||||
<>
|
||||
<Box
|
||||
ml={4}
|
||||
px={2}
|
||||
py={1}
|
||||
lineHeight="min"
|
||||
display='inline-block'
|
||||
color='yellow'
|
||||
border={1}
|
||||
borderRadius={2}>
|
||||
<Text color='yellow'>Reconnecting</Text>
|
||||
</Box>
|
||||
</>
|
||||
);
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
export default ReconnectButton;
|
@ -1,82 +1,103 @@
|
||||
import React from 'react';
|
||||
import { useLocation, Link } from 'react-router-dom';
|
||||
|
||||
import GroupFilter from './GroupFilter';
|
||||
import { Sigil } from '../lib/sigil';
|
||||
|
||||
const getLocationName = (basePath) => {
|
||||
if (basePath === '~chat')
|
||||
return 'Chat';
|
||||
else if (basePath === '~dojo')
|
||||
return 'Dojo';
|
||||
else if (basePath === '~groups')
|
||||
return 'Groups';
|
||||
else if (basePath === '~link')
|
||||
return 'Links';
|
||||
else if (basePath === '~publish')
|
||||
return 'Publish';
|
||||
else
|
||||
return 'Unknown';
|
||||
};
|
||||
import { useLocation } from 'react-router-dom';
|
||||
import { Box, Text, Icon } from '@tlon/indigo-react';
|
||||
import ReconnectButton from './ReconnectButton';
|
||||
|
||||
const StatusBar = (props) => {
|
||||
const location = useLocation();
|
||||
const basePath = location.pathname.split('/')[1];
|
||||
const locationName = location.pathname === '/'
|
||||
? 'Home'
|
||||
: getLocationName(basePath);
|
||||
const atHome = Boolean(location.pathname === '/');
|
||||
|
||||
const display = (!window.location.href.includes('popout/') &&
|
||||
(locationName !== 'Unknown'))
|
||||
const display = (!window.location.href.includes('popout/'))
|
||||
? 'db' : 'dn';
|
||||
|
||||
const invites = (props.invites && props.invites['/contacts'])
|
||||
? props.invites['/contacts']
|
||||
: {};
|
||||
const connection = props.connection || 'connected';
|
||||
|
||||
const reconnect = props.subscription.restart.bind(props.subscription);
|
||||
const Notification = (Object.keys(invites).length > 0)
|
||||
? <Icon size="22px" icon="Bullet"
|
||||
fill="blue" position="absolute"
|
||||
top={'-8px'} right={'7px'}
|
||||
/>
|
||||
: null;
|
||||
|
||||
const metaKey = (window.navigator.platform.includes('Mac')) ? '⌘' : 'Ctrl+';
|
||||
|
||||
const mobile = /Android|webOS|iPhone|iPad|iPod|BlackBerry/i.test(
|
||||
navigator.userAgent
|
||||
);
|
||||
|
||||
return (
|
||||
<div
|
||||
className={
|
||||
'bg-white bg-gray0-d w-100 justify-between relative tc pt3 ' + display
|
||||
}
|
||||
style={{ height: 45 }}
|
||||
>
|
||||
<div className="fl lh-copy absolute left-0 pl4" style={{ top: 8 }}>
|
||||
<Link to="/~groups/me"
|
||||
className="dib v-top" style={{ lineHeight: 0, paddingTop: 6 }}>
|
||||
<Sigil
|
||||
ship={'~' + window.ship}
|
||||
classes="v-mid mix-blend-diff"
|
||||
size={16}
|
||||
color={'#000000'}
|
||||
style={{ height: 45 }}>
|
||||
<div className='absolute left-0 pl4' style={{ top: 10 }}>
|
||||
{atHome ? null : (
|
||||
<Box
|
||||
style={{ cursor: 'pointer' }}
|
||||
display='inline-block'
|
||||
borderRadius={2}
|
||||
color='washedGray'
|
||||
border={1}
|
||||
py={1}
|
||||
px={2}
|
||||
mr={2}
|
||||
onClick={() => props.history.push('/')}>
|
||||
<img
|
||||
className='invert-d'
|
||||
src='/~landscape/img/icon-home.png'
|
||||
height='12'
|
||||
width='12'
|
||||
/>
|
||||
</Box>
|
||||
)}
|
||||
<Box
|
||||
border={1}
|
||||
borderRadius={2}
|
||||
color='washedGray'
|
||||
display='inline-block'
|
||||
style={{ cursor: 'pointer' }}
|
||||
lineHeight='min'
|
||||
py={1}
|
||||
px={2}
|
||||
onClick={() => props.api.local.setOmnibox()}>
|
||||
<Text display='inline-block' style={{ transform: 'rotate(180deg)' }}>
|
||||
↩
|
||||
</Text>
|
||||
<Text ml={2} color='black'>
|
||||
Leap
|
||||
</Text>
|
||||
<Text display={mobile ? 'none' : 'inline-block'} ml={4} color='gray'>
|
||||
{metaKey}/
|
||||
</Text>
|
||||
</Box>
|
||||
<ReconnectButton
|
||||
connection={props.connection}
|
||||
subscription={props.subscription}
|
||||
/>
|
||||
</div>
|
||||
<div className='fl absolute relative right-0 pr4' style={{ top: 10 }}>
|
||||
<Box
|
||||
style={{ cursor: 'pointer' }}
|
||||
display='inline-block'
|
||||
borderRadius={2}
|
||||
color='washedGray'
|
||||
lineHeight='min'
|
||||
border={1}
|
||||
px={2}
|
||||
py={1}
|
||||
onClick={() => props.history.push('/~groups')}>
|
||||
<img
|
||||
className='invert-d v-mid mr1'
|
||||
src='/~landscape/img/groups.png'
|
||||
height='16'
|
||||
width='16'
|
||||
/>
|
||||
</Link>
|
||||
<GroupFilter invites={invites} associations={props.associations} api={props.api} />
|
||||
<span className="dib f9 v-mid gray2 ml1 mr1 c-default inter">/</span>
|
||||
{
|
||||
location.pathname === '/'
|
||||
? null
|
||||
: <Link
|
||||
className="dib f9 v-mid inter ml2 no-underline white-d"
|
||||
to="/"
|
||||
style={{ top: 14 }}
|
||||
>
|
||||
⟵
|
||||
</Link>
|
||||
}
|
||||
<p className="dib f9 v-mid inter ml2 white-d">{locationName}</p>
|
||||
{ connection === 'disconnected' &&
|
||||
(<span
|
||||
onClick={reconnect}
|
||||
className="ml4 ph2 dib f9 v-mid red2 inter ba b-red2 br1 pointer"
|
||||
>Reconnect ↻</span> )
|
||||
}
|
||||
{ connection === 'reconnecting' &&
|
||||
(<span className="ml4 ph2 dib f9 v-mid yellow2 inter ba b-yellow2 br1">Reconnecting</span> )
|
||||
}
|
||||
{Notification}
|
||||
<Text ml={1}>Groups</Text>
|
||||
</Box>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
3
pkg/interface/src/lib/default-apps.js
Normal file
3
pkg/interface/src/lib/default-apps.js
Normal file
@ -0,0 +1,3 @@
|
||||
const defaultApps = ['chat', 'dojo', 'groups', 'link', 'publish'];
|
||||
|
||||
export default defaultApps;
|
106
pkg/interface/src/lib/omnibox.js
Normal file
106
pkg/interface/src/lib/omnibox.js
Normal file
@ -0,0 +1,106 @@
|
||||
import defaultApps from './default-apps';
|
||||
|
||||
export default function index(associations, apps) {
|
||||
const index = new Map([
|
||||
['commands', []],
|
||||
['subscriptions', []],
|
||||
['groups', []],
|
||||
['apps', []]
|
||||
]);
|
||||
|
||||
// result schematic
|
||||
const result = function(title, link, app, host) {
|
||||
return {
|
||||
'title': title,
|
||||
'link': link,
|
||||
'app': app,
|
||||
'host': host
|
||||
};
|
||||
};
|
||||
|
||||
// commands are special cased for default suite
|
||||
const commands = [];
|
||||
defaultApps.filter((e) => {
|
||||
return (e !== 'dojo');
|
||||
}).map((e) => {
|
||||
let title = e;
|
||||
if (e === 'link') {
|
||||
title = 'Links';
|
||||
}
|
||||
|
||||
title = title.charAt(0).toUpperCase() + title.slice(1);
|
||||
|
||||
let obj = result(`${title}: Create`, `/~${e}/new`, e, null);
|
||||
commands.push(obj);
|
||||
|
||||
if (title === 'Groups') {
|
||||
obj = result(`${title}: Join Group`, `/~${e}/join`, title, null);
|
||||
commands.push(obj);
|
||||
}
|
||||
});
|
||||
index.set('commands', commands);
|
||||
|
||||
// all metadata from all apps is indexed
|
||||
// into subscriptions and groups
|
||||
const subscriptions = [];
|
||||
const groups = [];
|
||||
Object.keys(associations).filter((e) => {
|
||||
// skip apps with no metadata
|
||||
return Object.keys(associations[e]).length > 0;
|
||||
}).map((e) => {
|
||||
// iterate through each app's metadata object
|
||||
Object.keys(associations[e]).map((association) => {
|
||||
const each = associations[e][association];
|
||||
let title = each['app-path'];
|
||||
if (each.metadata.title !== '') {
|
||||
title = each.metadata.title;
|
||||
}
|
||||
|
||||
let app = each['app-name'];
|
||||
if (each['app-name'] === 'contacts') {
|
||||
app = 'groups';
|
||||
};
|
||||
|
||||
const shipStart = each['app-path'].substr(each['app-path'].indexOf('~'));
|
||||
|
||||
if (app === 'groups') {
|
||||
const obj = result(
|
||||
title,
|
||||
`/~${app}${each['app-path']}`,
|
||||
app.charAt(0).toUpperCase() + app.slice(1),
|
||||
shipStart.slice(0, shipStart.indexOf('/'))
|
||||
);
|
||||
groups.push(obj);
|
||||
} else {
|
||||
let endpoint = '';
|
||||
if (app === 'chat') {
|
||||
endpoint = '/room';
|
||||
} else if (app === 'publish') {
|
||||
endpoint = '/notebook';
|
||||
}
|
||||
const obj = result(
|
||||
title,
|
||||
`/~${each['app-name']}${endpoint}${each['app-path']}`,
|
||||
app.charAt(0).toUpperCase() + app.slice(1),
|
||||
shipStart.slice(0, shipStart.indexOf('/'))
|
||||
);
|
||||
subscriptions.push(obj);
|
||||
}
|
||||
});
|
||||
});
|
||||
index.set('subscriptions', subscriptions);
|
||||
index.set('groups', groups);
|
||||
|
||||
// all apps are indexed from launch data
|
||||
// indexed into 'apps'
|
||||
const applications = [];
|
||||
Object.keys(apps).filter((e) => {
|
||||
return (apps[e]?.type?.basic);
|
||||
}).map((e) => {
|
||||
const obj = result(apps[e].type.basic.title, apps[e].type.basic.linkedUrl, apps[e].type.basic.title, null);
|
||||
applications.push(obj);
|
||||
});
|
||||
index.set('apps', applications);
|
||||
|
||||
return index;
|
||||
};
|
@ -119,6 +119,9 @@ export function writeText(str) {
|
||||
// trim patps to match dojo, chat-cli
|
||||
export function cite(ship) {
|
||||
let patp = ship, shortened = '';
|
||||
if (patp === null || patp === '') {
|
||||
return null;
|
||||
}
|
||||
if (patp.startsWith('~')) {
|
||||
patp = patp.substr(1);
|
||||
}
|
||||
|
@ -3,16 +3,16 @@ import { StoreState } from '../store/type';
|
||||
import { Cage } from '../types/cage';
|
||||
import { LocalUpdate } from '../types/local-update';
|
||||
|
||||
type LocalState = Pick<StoreState, 'sidebarShown' | 'selectedGroups' | 'dark' | 'baseHash'>;
|
||||
type LocalState = Pick<StoreState, 'sidebarShown' | 'omniboxShown' | 'dark' | 'baseHash'>;
|
||||
|
||||
export default class LocalReducer<S extends LocalState> {
|
||||
reduce(json: Cage, state: S) {
|
||||
const data = json['local'];
|
||||
if (data) {
|
||||
this.sidebarToggle(data, state);
|
||||
this.setSelected(data, state);
|
||||
this.setDark(data, state);
|
||||
this.baseHash(data, state);
|
||||
this.omniboxShown(data, state);
|
||||
}
|
||||
}
|
||||
baseHash(obj: LocalUpdate, state: S) {
|
||||
@ -21,18 +21,18 @@ export default class LocalReducer<S extends LocalState> {
|
||||
}
|
||||
}
|
||||
|
||||
omniboxShown(obj: LocalUpdate, state: S) {
|
||||
if ('omniboxShown' in obj) {
|
||||
state.omniboxShown = !state.omniboxShown;
|
||||
}
|
||||
}
|
||||
|
||||
sidebarToggle(obj: LocalUpdate, state: S) {
|
||||
if ('sidebarToggle' in obj) {
|
||||
state.sidebarShown = !state.sidebarShown;
|
||||
}
|
||||
}
|
||||
|
||||
setSelected(obj: LocalUpdate, state: S) {
|
||||
if ('selected' in obj) {
|
||||
state.selectedGroups = obj.selected;
|
||||
}
|
||||
}
|
||||
|
||||
setDark(obj: LocalUpdate, state: S) {
|
||||
if('setDark' in obj) {
|
||||
state.dark = obj.setDark;
|
||||
|
@ -41,6 +41,7 @@ export default class GlobalStore extends BaseStore<StoreState> {
|
||||
chatInitialized: false,
|
||||
connection: 'connected',
|
||||
sidebarShown: true,
|
||||
omniboxShown: false,
|
||||
baseHash: null,
|
||||
invites: {},
|
||||
associations: {
|
||||
@ -72,7 +73,6 @@ export default class GlobalStore extends BaseStore<StoreState> {
|
||||
linkComments: {},
|
||||
notebooks: {},
|
||||
contacts: {},
|
||||
selectedGroups: [],
|
||||
dark: false,
|
||||
inbox: {},
|
||||
chatSynced: null,
|
||||
|
@ -2,7 +2,6 @@ import { Inbox, Envelope } from '../types/chat-update';
|
||||
import { ChatHookUpdate } from '../types/chat-hook-update';
|
||||
import { Path } from '../types/noun';
|
||||
import { Invites } from '../types/invite-update';
|
||||
import { SelectedGroup } from '../types/local-update';
|
||||
import { Associations } from '../types/metadata-update';
|
||||
import { Rolodex } from '../types/contact-update';
|
||||
import { Notebooks } from '../types/publish-update';
|
||||
@ -16,7 +15,7 @@ import { ConnectionStatus } from '../types/connection';
|
||||
export interface StoreState {
|
||||
// local state
|
||||
sidebarShown: boolean;
|
||||
selectedGroups: SelectedGroup[];
|
||||
omniboxShown: boolean;
|
||||
dark: boolean;
|
||||
connection: ConnectionStatus;
|
||||
baseHash: string | null;
|
||||
|
@ -1,19 +1,13 @@
|
||||
import { Path } from './noun';
|
||||
|
||||
export type LocalUpdate =
|
||||
LocalUpdateSidebarToggle
|
||||
| LocalUpdateSelectedGroups
|
||||
| LocalUpdateSetDark
|
||||
| LocalUpdateSetOmniboxShown
|
||||
| LocalUpdateBaseHash;
|
||||
|
||||
interface LocalUpdateSidebarToggle {
|
||||
sidebarToggle: boolean;
|
||||
}
|
||||
|
||||
interface LocalUpdateSelectedGroups {
|
||||
selected: SelectedGroup[];
|
||||
}
|
||||
|
||||
interface LocalUpdateSetDark {
|
||||
setDark: boolean;
|
||||
}
|
||||
@ -22,4 +16,6 @@ interface LocalUpdateBaseHash {
|
||||
baseHash: string;
|
||||
}
|
||||
|
||||
export type SelectedGroup = [Path, string];
|
||||
interface LocalUpdateSetOmniboxShown {
|
||||
omniboxShown: boolean;
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user