mirror of
https://github.com/urbit/shrub.git
synced 2024-12-24 03:14:30 +03:00
Merge pull request #4856 from urbit/lf/keybinds
interface: add basic keybinds
This commit is contained in:
commit
f7aab5893c
71
pkg/interface/src/logic/lib/shortcutContext.tsx
Normal file
71
pkg/interface/src/logic/lib/shortcutContext.tsx
Normal file
@ -0,0 +1,71 @@
|
|||||||
|
import React, {
|
||||||
|
createContext,
|
||||||
|
useCallback,
|
||||||
|
useContext,
|
||||||
|
useEffect,
|
||||||
|
useMemo,
|
||||||
|
useRef,
|
||||||
|
useState,
|
||||||
|
} from 'react';
|
||||||
|
import _ from 'lodash';
|
||||||
|
import { getChord } from '~/logic/lib/util';
|
||||||
|
|
||||||
|
type Handler = (e: KeyboardEvent) => void;
|
||||||
|
const fallback: ShortcutContextProps = {
|
||||||
|
add: () => {},
|
||||||
|
remove: () => {},
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
export const ShortcutContext = createContext(fallback);
|
||||||
|
export interface ShortcutContextProps {
|
||||||
|
add: (cb: (e: KeyboardEvent) => void, key: string) => void;
|
||||||
|
remove: (cb: (e: KeyboardEvent) => void, key: string) => void;
|
||||||
|
}
|
||||||
|
export function ShortcutContextProvider({ children }) {
|
||||||
|
const [shortcuts, setShortcuts] = useState({} as Record<string, Handler>);
|
||||||
|
const handlerRef = useRef<Handler>(() => {});
|
||||||
|
|
||||||
|
const add = useCallback((cb: Handler, key: string) => {
|
||||||
|
setShortcuts((s) => ({ ...s, [key]: cb }));
|
||||||
|
}, []);
|
||||||
|
const remove = useCallback((cb: Handler, key: string) => {
|
||||||
|
setShortcuts((s) => (key in s ? _.omit(s, key) : s));
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
function onKeypress(e: KeyboardEvent) {
|
||||||
|
handlerRef.current(e);
|
||||||
|
}
|
||||||
|
document.addEventListener('keypress', onKeypress);
|
||||||
|
return () => {
|
||||||
|
document.removeEventListener('keypress', onKeypress);
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
handlerRef.current = function (e: KeyboardEvent) {
|
||||||
|
const chord = getChord(e);
|
||||||
|
console.log(chord);
|
||||||
|
shortcuts?.[chord]?.(e);
|
||||||
|
};
|
||||||
|
}, [shortcuts]);
|
||||||
|
|
||||||
|
const value = useMemo(() => ({ add, remove }), [add, remove])
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ShortcutContext.Provider value={value}>
|
||||||
|
{children}
|
||||||
|
</ShortcutContext.Provider>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useShortcut(key: string, cb: Handler) {
|
||||||
|
const { add, remove } = useContext(ShortcutContext);
|
||||||
|
useEffect(() => {
|
||||||
|
add(cb, key);
|
||||||
|
return () => {
|
||||||
|
remove(cb, key);
|
||||||
|
};
|
||||||
|
}, [key, cb]);
|
||||||
|
}
|
@ -8,7 +8,7 @@ import _ from 'lodash';
|
|||||||
import f from 'lodash/fp';
|
import f from 'lodash/fp';
|
||||||
import React, { useCallback, useEffect, useMemo, useState } from 'react';
|
import React, { useCallback, useEffect, useMemo, useState } from 'react';
|
||||||
import { foregroundFromBackground } from '~/logic/lib/sigil';
|
import { foregroundFromBackground } from '~/logic/lib/sigil';
|
||||||
import { IconRef } from '~/types';
|
import { IconRef, Workspace } from '~/types';
|
||||||
import useContactState from '../state/contact';
|
import useContactState from '../state/contact';
|
||||||
import useSettingsState from '../state/settings';
|
import useSettingsState from '../state/settings';
|
||||||
|
|
||||||
@ -49,6 +49,26 @@ export function parentPath(path: string) {
|
|||||||
return _.dropRight(path.split('/'), 1).join('/');
|
return _.dropRight(path.split('/'), 1).join('/');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const getChord = (e: KeyboardEvent) => {
|
||||||
|
let chord = [e.key];
|
||||||
|
if(e.metaKey) {
|
||||||
|
chord.unshift('meta');
|
||||||
|
}
|
||||||
|
if(e.ctrlKey) {
|
||||||
|
chord.unshift('ctrl');
|
||||||
|
}
|
||||||
|
return chord.join('+');
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getResourcePath(workspace: Workspace, path: string, joined: boolean, mod: string) {
|
||||||
|
const base = workspace.type === 'group'
|
||||||
|
? `/~landscape${workspace.group}`
|
||||||
|
: workspace.type === 'home'
|
||||||
|
? `/~landscape/home`
|
||||||
|
: `/~landscape/messages`;
|
||||||
|
return `${base}/${joined ? 'resource' : 'join'}/${mod}${path}`
|
||||||
|
}
|
||||||
|
|
||||||
const DA_UNIX_EPOCH = bigInt('170141184475152167957503069145530368000'); // `@ud` ~1970.1.1
|
const DA_UNIX_EPOCH = bigInt('170141184475152167957503069145530368000'); // `@ud` ~1970.1.1
|
||||||
const DA_SECOND = bigInt('18446744073709551616'); // `@ud` ~s1
|
const DA_SECOND = bigInt('18446744073709551616'); // `@ud` ~s1
|
||||||
export function daToUnix(da: BigInteger) {
|
export function daToUnix(da: BigInteger) {
|
||||||
@ -105,6 +125,13 @@ export function clamp(x: number, min: number, max: number) {
|
|||||||
return Math.max(min, Math.min(max, x));
|
return Math.max(min, Math.min(max, x));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Euclidean modulo
|
||||||
|
*/
|
||||||
|
export function modulo(x: number, mod: number) {
|
||||||
|
return x < 0 ? (x % mod + mod) % mod : x % mod;
|
||||||
|
}
|
||||||
|
|
||||||
// color is a #000000 color
|
// color is a #000000 color
|
||||||
export function adjustHex(color: string, amount: number): string {
|
export function adjustHex(color: string, amount: number): string {
|
||||||
return f.flow(
|
return f.flow(
|
||||||
@ -259,6 +286,8 @@ export function lengthOrder(a: string, b: string) {
|
|||||||
return b.length - a.length;
|
return b.length - a.length;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const keys = <T extends {}>(o: T) => Object.keys(o) as (keyof T)[];
|
||||||
|
|
||||||
// TODO: deprecated
|
// TODO: deprecated
|
||||||
export function alphabetiseAssociations(associations: any) {
|
export function alphabetiseAssociations(associations: any) {
|
||||||
const result = {};
|
const result = {};
|
||||||
|
@ -1,6 +1,17 @@
|
|||||||
import f from 'lodash/fp';
|
import f from 'lodash/fp';
|
||||||
|
import { RemoteContentPolicy, LeapCategories, leapCategories } from "~/types/local-update";
|
||||||
|
import { useShortcut as usePlainShortcut } from '~/logic/lib/shortcutContext';
|
||||||
import { BaseState, createState } from '~/logic/state/base';
|
import { BaseState, createState } from '~/logic/state/base';
|
||||||
import { LeapCategories, leapCategories, RemoteContentPolicy } from '~/types/local-update';
|
import {useCallback} from 'react';
|
||||||
|
|
||||||
|
export interface ShortcutMapping {
|
||||||
|
cycleForward: string;
|
||||||
|
cycleBack: string;
|
||||||
|
navForward: string;
|
||||||
|
navBack: string;
|
||||||
|
hideSidebar: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
export interface SettingsState extends BaseState<SettingsState> {
|
export interface SettingsState extends BaseState<SettingsState> {
|
||||||
display: {
|
display: {
|
||||||
@ -16,6 +27,7 @@ export interface SettingsState extends BaseState<SettingsState> {
|
|||||||
hideGroups: boolean;
|
hideGroups: boolean;
|
||||||
hideUtilities: boolean;
|
hideUtilities: boolean;
|
||||||
};
|
};
|
||||||
|
keyboard: ShortcutMapping;
|
||||||
remoteContentPolicy: RemoteContentPolicy;
|
remoteContentPolicy: RemoteContentPolicy;
|
||||||
leap: {
|
leap: {
|
||||||
categories: LeapCategories[];
|
categories: LeapCategories[];
|
||||||
@ -59,7 +71,19 @@ const useSettingsState = createState<SettingsState>('Settings', {
|
|||||||
tutorial: {
|
tutorial: {
|
||||||
seen: true,
|
seen: true,
|
||||||
joined: undefined
|
joined: undefined
|
||||||
|
},
|
||||||
|
keyboard: {
|
||||||
|
cycleForward: 'ctrl+\'',
|
||||||
|
cycleBack: 'ctrl+;',
|
||||||
|
navForward: 'ctrl+[',
|
||||||
|
navBack: 'ctrl+[',
|
||||||
|
hideSidebar: 'ctrl+\\'
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
export function useShortcut<T extends keyof ShortcutMapping>(name: T, cb: (e: KeyboardEvent) => void) {
|
||||||
|
const key = useSettingsState(useCallback(s => s.keyboard[name], [name]));
|
||||||
|
return usePlainShortcut(key, cb);
|
||||||
|
}
|
||||||
|
|
||||||
export default useSettingsState;
|
export default useSettingsState;
|
||||||
|
@ -16,6 +16,8 @@ import useContactState from '~/logic/state/contact';
|
|||||||
import useGroupState from '~/logic/state/group';
|
import useGroupState from '~/logic/state/group';
|
||||||
import useLocalState from '~/logic/state/local';
|
import useLocalState from '~/logic/state/local';
|
||||||
import useSettingsState from '~/logic/state/settings';
|
import useSettingsState from '~/logic/state/settings';
|
||||||
|
import { ShortcutContextProvider } from '~/logic/lib/shortcutContext';
|
||||||
|
|
||||||
import GlobalStore from '~/logic/store/store';
|
import GlobalStore from '~/logic/store/store';
|
||||||
import GlobalSubscription from '~/logic/subscription/global';
|
import GlobalSubscription from '~/logic/subscription/global';
|
||||||
import ErrorBoundary from '~/views/components/ErrorBoundary';
|
import ErrorBoundary from '~/views/components/ErrorBoundary';
|
||||||
@ -141,6 +143,7 @@ class App extends React.Component {
|
|||||||
const ourContact = this.props.contacts[`~${this.ship}`] || null;
|
const ourContact = this.props.contacts[`~${this.ship}`] || null;
|
||||||
return (
|
return (
|
||||||
<ThemeProvider theme={theme}>
|
<ThemeProvider theme={theme}>
|
||||||
|
<ShortcutContextProvider>
|
||||||
<Helmet>
|
<Helmet>
|
||||||
{window.ship.length < 14
|
{window.ship.length < 14
|
||||||
? <link rel="icon" type="image/svg+xml" href={svgDataURL(favicon())} />
|
? <link rel="icon" type="image/svg+xml" href={svgDataURL(favicon())} />
|
||||||
@ -178,6 +181,7 @@ class App extends React.Component {
|
|||||||
</Router>
|
</Router>
|
||||||
</Root>
|
</Root>
|
||||||
<div id="portal-root" />
|
<div id="portal-root" />
|
||||||
|
</ShortcutContextProvider>
|
||||||
</ThemeProvider>
|
</ThemeProvider>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -128,10 +128,27 @@ export default class ChatEditor extends Component<ChatEditorProps, ChatEditorSta
|
|||||||
};
|
};
|
||||||
|
|
||||||
this.editor = null;
|
this.editor = null;
|
||||||
|
this.onKeyPress = this.onKeyPress.bind(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
componentDidMount() {
|
||||||
|
document.addEventListener('keydown', this.onKeyPress);
|
||||||
}
|
}
|
||||||
|
|
||||||
componentWillUnmount(): void {
|
componentWillUnmount(): void {
|
||||||
this.props.onUnmount(this.state.message);
|
this.props.onUnmount(this.state.message);
|
||||||
|
document.removeEventListener('keydown', this.onKeyPress);
|
||||||
|
}
|
||||||
|
|
||||||
|
onKeyPress(e) {
|
||||||
|
const focusedTag = document.activeElement?.nodeName?.toLowerCase();
|
||||||
|
const shouldCapture = !(focusedTag === 'textarea' || focusedTag === 'input' || e.metaKey || e.ctrlKey);
|
||||||
|
if(/^[a-z]|[A-Z]$/.test(e.key) && shouldCapture) {
|
||||||
|
this.editor.focus();
|
||||||
|
}
|
||||||
|
if(e.key === 'Escape') {
|
||||||
|
this.editor.getInputField().blur();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
componentDidUpdate(prevProps: ChatEditorProps): void {
|
componentDidUpdate(prevProps: ChatEditorProps): void {
|
||||||
@ -140,9 +157,9 @@ export default class ChatEditor extends Component<ChatEditorProps, ChatEditorSta
|
|||||||
if (prevProps.message !== props.message && this.editor) {
|
if (prevProps.message !== props.message && this.editor) {
|
||||||
this.editor.setValue(props.message);
|
this.editor.setValue(props.message);
|
||||||
this.editor.setOption('mode', MARKDOWN_CONFIG);
|
this.editor.setOption('mode', MARKDOWN_CONFIG);
|
||||||
this.editor?.focus();
|
//this.editor?.focus();
|
||||||
this.editor.execCommand('goDocEnd');
|
//this.editor.execCommand('goDocEnd');
|
||||||
this.editor?.focus();
|
//this.editor?.focus();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -281,7 +298,6 @@ return;
|
|||||||
onChange={(e, d, v) => this.messageChange(e, d, v)}
|
onChange={(e, d, v) => this.messageChange(e, d, v)}
|
||||||
editorDidMount={(editor) => {
|
editorDidMount={(editor) => {
|
||||||
this.editor = editor;
|
this.editor = editor;
|
||||||
editor.focus();
|
|
||||||
}}
|
}}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
|
@ -0,0 +1,117 @@
|
|||||||
|
import React, { useCallback, useEffect, useState } from 'react';
|
||||||
|
import _ from 'lodash';
|
||||||
|
|
||||||
|
import { Box, Col, Text } from '@tlon/indigo-react';
|
||||||
|
import { Formik, Form, useField } from 'formik';
|
||||||
|
|
||||||
|
import GlobalApi from '~/logic/api/global';
|
||||||
|
import { getChord } from '~/logic/lib/util';
|
||||||
|
import useSettingsState, {
|
||||||
|
selectSettingsState,
|
||||||
|
ShortcutMapping,
|
||||||
|
} from '~/logic/state/settings';
|
||||||
|
import { AsyncButton } from '~/views/components/AsyncButton';
|
||||||
|
import { BackButton } from './BackButton';
|
||||||
|
|
||||||
|
interface ShortcutSettingsProps {
|
||||||
|
api: GlobalApi;
|
||||||
|
}
|
||||||
|
|
||||||
|
const settingsSel = selectSettingsState(['keyboard']);
|
||||||
|
|
||||||
|
export function ChordInput(props: { id: string; label: string }) {
|
||||||
|
const { id, label } = props;
|
||||||
|
const [capturing, setCapturing] = useState(false);
|
||||||
|
const [{ value }, , { setValue }] = useField(id);
|
||||||
|
const onCapture = useCallback(() => {
|
||||||
|
setCapturing(true);
|
||||||
|
}, []);
|
||||||
|
useEffect(() => {
|
||||||
|
if (!capturing) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
function onKeydown(e: KeyboardEvent) {
|
||||||
|
if (['Control', 'Shift', 'Meta'].includes(e.key)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const chord = getChord(e);
|
||||||
|
setValue(chord);
|
||||||
|
e.stopImmediatePropagation();
|
||||||
|
e.preventDefault();
|
||||||
|
setCapturing(false);
|
||||||
|
}
|
||||||
|
document.addEventListener('keydown', onKeydown);
|
||||||
|
return () => {
|
||||||
|
document.removeEventListener('keydown', onKeydown);
|
||||||
|
};
|
||||||
|
}, [capturing]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Box p="1">
|
||||||
|
<Text>{label}</Text>
|
||||||
|
</Box>
|
||||||
|
<Box
|
||||||
|
border="1"
|
||||||
|
borderColor="lightGray"
|
||||||
|
borderRadius="2"
|
||||||
|
onClick={onCapture}
|
||||||
|
p="1"
|
||||||
|
>
|
||||||
|
<Text>{capturing ? 'Press' : value}</Text>
|
||||||
|
</Box>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function ShortcutSettings(props: ShortcutSettingsProps) {
|
||||||
|
const { api } = props;
|
||||||
|
|
||||||
|
const { keyboard } = useSettingsState(settingsSel);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Formik
|
||||||
|
initialValues={keyboard}
|
||||||
|
onSubmit={async (values: ShortcutMapping, actions) => {
|
||||||
|
const promises = _.map(values, (value, key) => {
|
||||||
|
return keyboard[key] !== value
|
||||||
|
? api.settings.putEntry('keyboard', key, value)
|
||||||
|
: Promise.resolve();
|
||||||
|
});
|
||||||
|
await Promise.all(promises);
|
||||||
|
actions.setStatus({ success: null });
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Form>
|
||||||
|
<BackButton />
|
||||||
|
<Col p="5" pt="4" gapY="5">
|
||||||
|
<Col gapY="1" mt="0">
|
||||||
|
<Text color="black" fontSize={2} fontWeight="medium">
|
||||||
|
Shortcuts
|
||||||
|
</Text>
|
||||||
|
<Text gray>Customize keyboard shortcuts for landscape</Text>
|
||||||
|
</Col>
|
||||||
|
<Box
|
||||||
|
display="grid"
|
||||||
|
gridTemplateColumns="1fr 100px"
|
||||||
|
gridGap={3}
|
||||||
|
maxWidth="500px"
|
||||||
|
>
|
||||||
|
<ChordInput id="navForward" label="Go forward in history" />
|
||||||
|
<ChordInput id="navBack" label="Go backward in history" />
|
||||||
|
<ChordInput
|
||||||
|
id="cycleForward"
|
||||||
|
label="Cycle forward through channel list"
|
||||||
|
/>
|
||||||
|
<ChordInput
|
||||||
|
id="cycleBack"
|
||||||
|
label="Cycle backward through channel list"
|
||||||
|
/>
|
||||||
|
<ChordInput id="hideSidebar" label="Show/hide group sidebar" />
|
||||||
|
</Box>
|
||||||
|
<AsyncButton primary width="fit-content">Save Changes</AsyncButton>
|
||||||
|
</Col>
|
||||||
|
</Form>
|
||||||
|
</Formik>
|
||||||
|
);
|
||||||
|
}
|
@ -12,6 +12,7 @@ import { LeapSettings } from './components/lib/LeapSettings';
|
|||||||
import { NotificationPreferences } from './components/lib/NotificationPref';
|
import { NotificationPreferences } from './components/lib/NotificationPref';
|
||||||
import S3Form from './components/lib/S3Form';
|
import S3Form from './components/lib/S3Form';
|
||||||
import SecuritySettings from './components/lib/Security';
|
import SecuritySettings from './components/lib/Security';
|
||||||
|
import ShortcutSettings from './components/lib/ShortcutSettings';
|
||||||
|
|
||||||
export const Skeleton = (props: { children: ReactNode }) => (
|
export const Skeleton = (props: { children: ReactNode }) => (
|
||||||
<Box height='100%' width='100%' px={[0, 3]} pb={[0, 3]} borderRadius={1}>
|
<Box height='100%' width='100%' px={[0, 3]} pb={[0, 3]} borderRadius={1}>
|
||||||
@ -113,6 +114,7 @@ return;
|
|||||||
<SidebarItem icon='Upload' text='Remote Storage' hash='s3' />
|
<SidebarItem icon='Upload' text='Remote Storage' hash='s3' />
|
||||||
<SidebarItem icon='LeapArrow' text='Leap' hash='leap' />
|
<SidebarItem icon='LeapArrow' text='Leap' hash='leap' />
|
||||||
<SidebarItem icon='Node' text='CalmEngine' hash='calm' />
|
<SidebarItem icon='Node' text='CalmEngine' hash='calm' />
|
||||||
|
<SidebarItem icon='Keyboard' text='Shortcuts' hash='shortcuts' />
|
||||||
<SidebarItem
|
<SidebarItem
|
||||||
icon='Locked'
|
icon='Locked'
|
||||||
text='Devices + Security'
|
text='Devices + Security'
|
||||||
@ -129,6 +131,7 @@ return;
|
|||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{hash === 'display' && <DisplayForm api={props.api} />}
|
{hash === 'display' && <DisplayForm api={props.api} />}
|
||||||
|
{hash === 'shortcuts' && <ShortcutSettings api={props.api} />}
|
||||||
{hash === 's3' && <S3Form api={props.api} />}
|
{hash === 's3' && <S3Form api={props.api} />}
|
||||||
{hash === 'leap' && <LeapSettings api={props.api} />}
|
{hash === 'leap' && <LeapSettings api={props.api} />}
|
||||||
{hash === 'calm' && <CalmPrefs api={props.api} />}
|
{hash === 'calm' && <CalmPrefs api={props.api} />}
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import { Box } from '@tlon/indigo-react';
|
import { Box } from '@tlon/indigo-react';
|
||||||
import React, { useEffect } from 'react';
|
import React, { useCallback, useEffect } from 'react';
|
||||||
import { Route, Switch } from 'react-router-dom';
|
import { Route, Switch, useHistory } from 'react-router-dom';
|
||||||
import styled from 'styled-components';
|
import styled from 'styled-components';
|
||||||
import { useLocalStorageState } from '~/logic/lib/useLocalStorageState';
|
import { useLocalStorageState } from '~/logic/lib/useLocalStorageState';
|
||||||
import LaunchApp from '~/views/apps/launch/App';
|
import LaunchApp from '~/views/apps/launch/App';
|
||||||
@ -10,6 +10,8 @@ import Profile from '~/views/apps/profile/profile';
|
|||||||
import Settings from '~/views/apps/settings/settings';
|
import Settings from '~/views/apps/settings/settings';
|
||||||
import TermApp from '~/views/apps/term/app';
|
import TermApp from '~/views/apps/term/app';
|
||||||
import ErrorComponent from '~/views/components/Error';
|
import ErrorComponent from '~/views/components/Error';
|
||||||
|
import { useShortcut } from '~/logic/state/settings';
|
||||||
|
|
||||||
import Landscape from '~/views/landscape/index';
|
import Landscape from '~/views/landscape/index';
|
||||||
import GraphApp from '../../apps/graph/App';
|
import GraphApp from '../../apps/graph/App';
|
||||||
|
|
||||||
@ -21,6 +23,21 @@ export const Container = styled(Box)`
|
|||||||
`;
|
`;
|
||||||
|
|
||||||
export const Content = (props) => {
|
export const Content = (props) => {
|
||||||
|
const history = useHistory();
|
||||||
|
|
||||||
|
useShortcut('navForward', useCallback((e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopImmediatePropagation();
|
||||||
|
history.goForward();
|
||||||
|
}, [history.goForward]));
|
||||||
|
|
||||||
|
useShortcut('navBack', useCallback((e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopImmediatePropagation();
|
||||||
|
history.goBack();
|
||||||
|
}, [history.goBack]));
|
||||||
|
|
||||||
|
|
||||||
const [hasProtocol, setHasProtocol] = useLocalStorageState(
|
const [hasProtocol, setHasProtocol] = useLocalStorageState(
|
||||||
'registeredProtocol', false
|
'registeredProtocol', false
|
||||||
);
|
);
|
||||||
|
@ -1,10 +1,14 @@
|
|||||||
import { AppAssociations } from '@urbit/api';
|
import React, { ReactElement, useCallback, useEffect } from 'react';
|
||||||
import React, { ReactElement } from 'react';
|
import { AppAssociations, Groups, Rolodex } from '@urbit/api';
|
||||||
import { alphabeticalOrder } from '~/logic/lib/util';
|
|
||||||
import useMetadataState from '~/logic/state/metadata';
|
import { alphabeticalOrder, getResourcePath, modulo } from '~/logic/lib/util';
|
||||||
import { Workspace } from '~/types/workspace';
|
|
||||||
import { SidebarItem } from './SidebarItem';
|
|
||||||
import { SidebarAppConfigs, SidebarListConfig, SidebarSort } from './types';
|
import { SidebarAppConfigs, SidebarListConfig, SidebarSort } from './types';
|
||||||
|
import { SidebarItem } from './SidebarItem';
|
||||||
|
import { Workspace } from '~/types/workspace';
|
||||||
|
import useMetadataState from '~/logic/state/metadata';
|
||||||
|
import useGraphState from '~/logic/state/graph';
|
||||||
|
import {useHistory} from 'react-router';
|
||||||
|
import { useShortcut } from '~/logic/state/settings';
|
||||||
|
|
||||||
function sidebarSort(
|
function sidebarSort(
|
||||||
associations: AppAssociations,
|
associations: AppAssociations,
|
||||||
@ -47,6 +51,7 @@ export function SidebarList(props: {
|
|||||||
}): ReactElement {
|
}): ReactElement {
|
||||||
const { selected, group, config, workspace } = props;
|
const { selected, group, config, workspace } = props;
|
||||||
const associationState = useMetadataState(state => state.associations);
|
const associationState = useMetadataState(state => state.associations);
|
||||||
|
const graphKeys = useGraphState(s => s.graphKeys);
|
||||||
const associations = { ...associationState.graph };
|
const associations = { ...associationState.graph };
|
||||||
|
|
||||||
const ordered = Object.keys(associations)
|
const ordered = Object.keys(associations)
|
||||||
@ -72,6 +77,30 @@ export function SidebarList(props: {
|
|||||||
})
|
})
|
||||||
.sort(sidebarSort(associations, props.apps)[config.sortBy]);
|
.sort(sidebarSort(associations, props.apps)[config.sortBy]);
|
||||||
|
|
||||||
|
const history = useHistory();
|
||||||
|
|
||||||
|
const cycleChannels = useCallback((backward: boolean) => {
|
||||||
|
const idx = ordered.findIndex(s => s === selected);
|
||||||
|
const offset = backward ? -1 : 1
|
||||||
|
|
||||||
|
const newIdx = modulo(idx+offset, ordered.length - 1);
|
||||||
|
const { metadata, resource } = associations[ordered[newIdx]];
|
||||||
|
const joined = graphKeys.has(resource.slice(7));
|
||||||
|
const path = getResourcePath(workspace, resource, joined, metadata.config.graph)
|
||||||
|
history.push(path)
|
||||||
|
}, [selected, history.push]);
|
||||||
|
|
||||||
|
useShortcut('cycleForward', useCallback((e: KeyboardEvent) => {
|
||||||
|
cycleChannels(false);
|
||||||
|
e.preventDefault();
|
||||||
|
}, [cycleChannels]));
|
||||||
|
|
||||||
|
useShortcut('cycleBack', useCallback((e: KeyboardEvent) => {
|
||||||
|
cycleChannels(true);
|
||||||
|
e.preventDefault();
|
||||||
|
}, [cycleChannels]))
|
||||||
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{ordered.map((path) => {
|
{ordered.map((path) => {
|
||||||
|
@ -1,13 +1,14 @@
|
|||||||
|
import React, { Children, ReactElement, ReactNode, useCallback, useMemo, useState } from 'react';
|
||||||
|
import { Sidebar } from './Sidebar/Sidebar';
|
||||||
import { AppName } from '@urbit/api';
|
import { AppName } from '@urbit/api';
|
||||||
import React, { ReactElement, ReactNode, useMemo } from 'react';
|
|
||||||
import GlobalApi from '~/logic/api/global';
|
import GlobalApi from '~/logic/api/global';
|
||||||
import useGraphState from '~/logic/state/graph';
|
import useGraphState from '~/logic/state/graph';
|
||||||
import useHarkState from '~/logic/state/hark';
|
import useHarkState from '~/logic/state/hark';
|
||||||
import { Workspace } from '~/types/workspace';
|
import { Workspace } from '~/types/workspace';
|
||||||
import { Body } from '~/views/components/Body';
|
import { Body } from '~/views/components/Body';
|
||||||
import ErrorBoundary from '~/views/components/ErrorBoundary';
|
import ErrorBoundary from '~/views/components/ErrorBoundary';
|
||||||
|
import { useShortcut } from '~/logic/state/settings';
|
||||||
import { useGraphModule } from './Sidebar/Apps';
|
import { useGraphModule } from './Sidebar/Apps';
|
||||||
import { Sidebar } from './Sidebar/Sidebar';
|
|
||||||
|
|
||||||
interface SkeletonProps {
|
interface SkeletonProps {
|
||||||
children: ReactNode;
|
children: ReactNode;
|
||||||
@ -21,6 +22,10 @@ interface SkeletonProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function Skeleton(props: SkeletonProps): ReactElement {
|
export function Skeleton(props: SkeletonProps): ReactElement {
|
||||||
|
const [sidebar, setSidebar] = useState(true)
|
||||||
|
useShortcut('hideSidebar', useCallback(() => {
|
||||||
|
setSidebar(s => !s);
|
||||||
|
}, []));
|
||||||
const graphs = useGraphState(state => state.graphs);
|
const graphs = useGraphState(state => state.graphs);
|
||||||
const graphKeys = useGraphState(state => state.graphKeys);
|
const graphKeys = useGraphState(state => state.graphKeys);
|
||||||
const unreads = useHarkState(state => state.unreads);
|
const unreads = useHarkState(state => state.unreads);
|
||||||
@ -32,7 +37,7 @@ export function Skeleton(props: SkeletonProps): ReactElement {
|
|||||||
[graphConfig]
|
[graphConfig]
|
||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return !sidebar ? (<Body> {props.children} </Body>) : (
|
||||||
<Body
|
<Body
|
||||||
display="grid"
|
display="grid"
|
||||||
gridTemplateColumns={
|
gridTemplateColumns={
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
|
||||||
if (process.env.NODE_ENV === 'development') {
|
if (false && process.env.NODE_ENV === 'development') {
|
||||||
const whyDidYouRender = require('@welldone-software/why-did-you-render');
|
const whyDidYouRender = require('@welldone-software/why-did-you-render');
|
||||||
whyDidYouRender(React, {
|
whyDidYouRender(React, {
|
||||||
trackAllPureComponents: true
|
trackAllPureComponents: true
|
||||||
|
@ -39,7 +39,7 @@ export default class BigIntOrderedMap<V> implements Iterable<[BigInteger, V]> {
|
|||||||
items.forEach(([key, value]) => {
|
items.forEach(([key, value]) => {
|
||||||
draft.root[key.toString()] = castDraft(value);
|
draft.root[key.toString()] = castDraft(value);
|
||||||
});
|
});
|
||||||
draft.generateCachedIter();
|
draft.cachedIter = null;
|
||||||
},
|
},
|
||||||
(patches) => {
|
(patches) => {
|
||||||
//console.log(`gassed with ${JSON.stringify(patches, null, 2)}`);
|
//console.log(`gassed with ${JSON.stringify(patches, null, 2)}`);
|
||||||
|
Loading…
Reference in New Issue
Block a user