mirror of
https://github.com/ilyakooo0/urbit.git
synced 2024-12-15 10:02:47 +03:00
Merge remote-tracking branch 'origin/release/next-js' into lf/notif-v2
This commit is contained in:
commit
d5bbc58c2b
@ -5,6 +5,20 @@ let
|
||||
|
||||
set -eu
|
||||
|
||||
# set defaults
|
||||
amesPort=${toString amesPort}
|
||||
|
||||
# check args
|
||||
for i in "$@"
|
||||
do
|
||||
case $i in
|
||||
-p=*|--port=*)
|
||||
amesPort="''${i#*=}"
|
||||
shift
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
# If the container is not started with the `-i` flag
|
||||
# then STDIN will be closed and we need to start
|
||||
# Urbit/vere with the `-t` flag.
|
||||
@ -23,7 +37,7 @@ let
|
||||
mv $keyname /tmp
|
||||
|
||||
# Boot urbit with the key, exit when done booting
|
||||
urbit $ttyflag -w $(basename $keyname .key) -k /tmp/$keyname -c $(basename $keyname .key) -p ${toString amesPort} -x
|
||||
urbit $ttyflag -w $(basename $keyname .key) -k /tmp/$keyname -c $(basename $keyname .key) -p $amesPort -x
|
||||
|
||||
# Remove the keyfile for security
|
||||
rm /tmp/$keyname
|
||||
@ -34,7 +48,7 @@ let
|
||||
cometname=''${comets[0]}
|
||||
rm *.comet
|
||||
|
||||
urbit $ttyflag -c $(basename $cometname .comet) -p ${toString amesPort} -x
|
||||
urbit $ttyflag -c $(basename $cometname .comet) -p $amesPort -x
|
||||
fi
|
||||
|
||||
# Find the first directory and start urbit with the ship therein
|
||||
@ -42,7 +56,7 @@ let
|
||||
dirs=( $dirnames )
|
||||
dirname=''${dirnames[0]}
|
||||
|
||||
urbit $ttyflag -p ${toString amesPort} $dirname
|
||||
exec urbit $ttyflag -p $amesPort $dirname
|
||||
'';
|
||||
|
||||
|
||||
|
@ -64,20 +64,20 @@ export function ProfileHeaderImageEdit(props: any): ReactElement {
|
||||
{contact?.cover ? (
|
||||
<div>
|
||||
{editCover ? (
|
||||
<ImageInput id='cover' marginTop='-8px' />
|
||||
<ImageInput id='cover' marginTop='-8px' width='288px' />
|
||||
) : (
|
||||
<Row>
|
||||
<Button mr='2' onClick={() => setEditCover(true)}>
|
||||
Replace Header
|
||||
</Button>
|
||||
<Button onClick={(e) => handleClear(e)}>
|
||||
<Button onClick={e => handleClear(e)}>
|
||||
{removedCoverLabel}
|
||||
</Button>
|
||||
</Row>
|
||||
)}
|
||||
</div>
|
||||
) : (
|
||||
<ImageInput id='cover' marginTop='-8px' />
|
||||
<ImageInput id='cover' marginTop='-8px' width='288px' />
|
||||
)}
|
||||
</>
|
||||
);
|
||||
|
@ -1,32 +1,36 @@
|
||||
import React, { ReactElement } from 'react';
|
||||
|
||||
import {
|
||||
Box,
|
||||
Text,
|
||||
Row,
|
||||
Label,
|
||||
Col,
|
||||
ManagedRadioButtonField as Radio,
|
||||
ManagedRadioButtonField as Radio
|
||||
} from '@tlon/indigo-react';
|
||||
|
||||
import GlobalApi from '~/logic/api/global';
|
||||
import { ImageInput } from '~/views/components/ImageInput';
|
||||
import { ColorInput } from '~/views/components/ColorInput';
|
||||
import { StorageState } from '~/types';
|
||||
|
||||
export type BgType = 'none' | 'url' | 'color';
|
||||
|
||||
export function BackgroundPicker({
|
||||
bgType,
|
||||
bgUrl,
|
||||
api,
|
||||
api
|
||||
}: {
|
||||
bgType: BgType;
|
||||
bgUrl?: string;
|
||||
api: GlobalApi;
|
||||
}): ReactElement {
|
||||
const rowSpace = { my: 0, alignItems: 'center' };
|
||||
const colProps = { my: 3, mr: 4, gapY: 1 };
|
||||
const colProps = {
|
||||
my: 3,
|
||||
mr: 4,
|
||||
gapY: 1,
|
||||
minWidth: '266px',
|
||||
width: ['100%', '288px']
|
||||
};
|
||||
return (
|
||||
<Col>
|
||||
<Label>Landscape Background</Label>
|
||||
@ -40,7 +44,7 @@ export function BackgroundPicker({
|
||||
id="bgUrl"
|
||||
placeholder="Drop or upload a file, or paste a link here"
|
||||
name="bgUrl"
|
||||
url={bgUrl || ""}
|
||||
url={bgUrl || ''}
|
||||
/>
|
||||
</Col>
|
||||
</Row>
|
||||
@ -48,13 +52,13 @@ export function BackgroundPicker({
|
||||
<Col {...colProps}>
|
||||
<Radio mb="1" label="Color" id="color" name="bgType" />
|
||||
<Text ml="5" gray>Set a hex-based background</Text>
|
||||
<ColorInput placeholder="FFFFFF" ml="5" id="bgColor" />
|
||||
<ColorInput placeholder="FFFFFF" ml="5" id="bgColor" />
|
||||
</Col>
|
||||
</Row>
|
||||
<Radio
|
||||
my="3"
|
||||
caption="Your home screen will simply render as its respective day/night mode color"
|
||||
name="bgType"
|
||||
name="bgType"
|
||||
label="None"
|
||||
id="none" />
|
||||
</Col>
|
||||
|
@ -7,11 +7,11 @@ import {
|
||||
Row,
|
||||
Button,
|
||||
Label,
|
||||
ErrorLabel,
|
||||
BaseInput
|
||||
BaseInput,
|
||||
Text,
|
||||
Icon
|
||||
} from '@tlon/indigo-react';
|
||||
|
||||
import { StorageState } from '~/types';
|
||||
import useStorage from '~/logic/lib/useStorage';
|
||||
|
||||
type ImageInputProps = Parameters<typeof Box>[0] & {
|
||||
@ -20,13 +20,100 @@ type ImageInputProps = Parameters<typeof Box>[0] & {
|
||||
placeholder?: string;
|
||||
};
|
||||
|
||||
const prompt = (field, uploading, meta, clickUploadButton) => {
|
||||
if (!field.value && !uploading && meta.error === undefined) {
|
||||
return (
|
||||
<Text
|
||||
black
|
||||
fontWeight='500'
|
||||
position='absolute'
|
||||
left={2}
|
||||
top={2}
|
||||
style={{ pointerEvents: 'none' }}
|
||||
>
|
||||
Paste a link here, or{' '}
|
||||
<Text
|
||||
fontWeight='500'
|
||||
cursor='pointer'
|
||||
color='blue'
|
||||
style={{ pointerEvents: 'all' }}
|
||||
onClick={clickUploadButton}
|
||||
>
|
||||
upload
|
||||
</Text>{' '}
|
||||
a file
|
||||
</Text>
|
||||
);
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
const uploadingStatus = (uploading, meta) => {
|
||||
if (uploading && meta.error === undefined) {
|
||||
return (
|
||||
<Text position='absolute' left={2} top={2} gray>
|
||||
Uploading...
|
||||
</Text>
|
||||
);
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
const errorRetry = (meta, uploading, clickUploadButton) => {
|
||||
if (meta.error !== undefined) {
|
||||
return (
|
||||
<Text
|
||||
position='absolute'
|
||||
left={2}
|
||||
top={2}
|
||||
color='red'
|
||||
style={{ pointerEvents: 'none' }}
|
||||
>
|
||||
{meta.error}{', '}please{' '}
|
||||
<Text
|
||||
fontWeight='500'
|
||||
cursor='pointer'
|
||||
color='blue'
|
||||
style={{ pointerEvents: 'all' }}
|
||||
onClick={clickUploadButton}
|
||||
>
|
||||
retry
|
||||
</Text>
|
||||
</Text>
|
||||
);
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
const clearButton = (field, uploading, clearEvt) => {
|
||||
if (field.value && !uploading) {
|
||||
return (
|
||||
<Box
|
||||
position='absolute'
|
||||
right={0}
|
||||
top={0}
|
||||
px={1}
|
||||
height='100%'
|
||||
cursor='pointer'
|
||||
onClick={clearEvt}
|
||||
backgroundColor='white'
|
||||
display='flex'
|
||||
alignItems='center'
|
||||
borderRadius='0 4px 4px 0'
|
||||
border='1px solid'
|
||||
borderColor='lightGray'
|
||||
>
|
||||
<Icon icon='X' />
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
export function ImageInput(props: ImageInputProps): ReactElement {
|
||||
const { id, label, caption, placeholder } = props;
|
||||
|
||||
const { id, label, caption } = props;
|
||||
const { uploadDefault, canUpload, uploading } = useStorage();
|
||||
|
||||
const [field, meta, { setValue, setError }] = useField(id);
|
||||
|
||||
const ref = useRef<HTMLInputElement | null>(null);
|
||||
|
||||
const onImageUpload = useCallback(async () => {
|
||||
@ -43,10 +130,14 @@ export function ImageInput(props: ImageInputProps): ReactElement {
|
||||
}
|
||||
}, [ref.current, uploadDefault, canUpload, setValue]);
|
||||
|
||||
const onClick = useCallback(() => {
|
||||
const clickUploadButton = useCallback(() => {
|
||||
ref.current?.click();
|
||||
}, [ref]);
|
||||
|
||||
const clearEvt = useCallback(() => {
|
||||
setValue('');
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<Box display="flex" flexDirection="column" {...props}>
|
||||
<Label htmlFor={id}>{label}</Label>
|
||||
@ -55,25 +146,25 @@ export function ImageInput(props: ImageInputProps): ReactElement {
|
||||
{caption}
|
||||
</Label>
|
||||
) : null}
|
||||
<Row mt="2" alignItems="flex-end">
|
||||
<Input
|
||||
type={'text'}
|
||||
hasError={meta.touched && meta.error !== undefined}
|
||||
placeholder={placeholder}
|
||||
{...field}
|
||||
/>
|
||||
<Row mt="2" alignItems="flex-end" position='relative' width='100%'>
|
||||
{prompt(field, uploading, meta, clickUploadButton)}
|
||||
{clearButton(field, uploading, clearEvt)}
|
||||
{uploadingStatus(uploading, meta)}
|
||||
{errorRetry(meta, uploading, clickUploadButton)}
|
||||
<Box background='white' borderRadius={2} width='100%'>
|
||||
<Input
|
||||
width='100%'
|
||||
type={'text'}
|
||||
hasError={meta.touched && meta.error !== undefined}
|
||||
{...field}
|
||||
/>
|
||||
</Box>
|
||||
{canUpload && (
|
||||
<>
|
||||
<Button
|
||||
type="button"
|
||||
ml={1}
|
||||
border={1}
|
||||
borderColor="lightGray"
|
||||
onClick={onClick}
|
||||
flexShrink={0}
|
||||
>
|
||||
{uploading ? 'Uploading' : 'Upload'}
|
||||
</Button>
|
||||
display='none'
|
||||
onClick={clickUploadButton}
|
||||
/>
|
||||
<BaseInput
|
||||
style={{ display: 'none' }}
|
||||
type="file"
|
||||
@ -85,9 +176,6 @@ export function ImageInput(props: ImageInputProps): ReactElement {
|
||||
</>
|
||||
)}
|
||||
</Row>
|
||||
<ErrorLabel mt="2" hasError={Boolean(meta.touched && meta.error)}>
|
||||
{meta.error}
|
||||
</ErrorLabel>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
@ -1,11 +1,16 @@
|
||||
import React, { useMemo, useRef, useCallback, useEffect, useState } from 'react';
|
||||
import React, {
|
||||
useMemo,
|
||||
useRef,
|
||||
useCallback,
|
||||
useEffect,
|
||||
useState
|
||||
} from 'react';
|
||||
import { useLocation, useHistory } from 'react-router-dom';
|
||||
import * as ob from 'urbit-ob';
|
||||
import Mousetrap from 'mousetrap';
|
||||
import { omit } from 'lodash';
|
||||
|
||||
import { Box, Row, Text } from '@tlon/indigo-react';
|
||||
import { Associations, Contacts, Groups, Invites } from '@urbit/api';
|
||||
|
||||
import makeIndex from '~/logic/lib/omnibox';
|
||||
import OmniboxInput from './OmniboxInput';
|
||||
import OmniboxResult from './OmniboxResult';
|
||||
@ -13,10 +18,9 @@ import { deSig } from '~/logic/lib/util';
|
||||
import { withLocalState } from '~/logic/state/local';
|
||||
|
||||
import defaultApps from '~/logic/lib/default-apps';
|
||||
import {useOutsideClick} from '~/logic/lib/useOutsideClick';
|
||||
import {Portal} from '../Portal';
|
||||
import useSettingsState, {SettingsState} from '~/logic/state/settings';
|
||||
import { Tile } from '~/types';
|
||||
import { useOutsideClick } from '~/logic/lib/useOutsideClick';
|
||||
import { Portal } from '../Portal';
|
||||
import useSettingsState, { SettingsState } from '~/logic/state/settings';
|
||||
import useContactState from '~/logic/state/contact';
|
||||
import useGroupState from '~/logic/state/group';
|
||||
import useHarkState from '~/logic/state/hark';
|
||||
@ -30,14 +34,21 @@ interface OmniboxProps {
|
||||
notifications: number;
|
||||
}
|
||||
|
||||
const SEARCHED_CATEGORIES = ['commands', 'ships', 'other', 'groups', 'subscriptions', 'apps'];
|
||||
const SEARCHED_CATEGORIES = [
|
||||
'commands',
|
||||
'ships',
|
||||
'other',
|
||||
'groups',
|
||||
'subscriptions',
|
||||
'apps'
|
||||
];
|
||||
const settingsSel = (s: SettingsState) => s.leap;
|
||||
|
||||
export function Omnibox(props: OmniboxProps) {
|
||||
export function Omnibox(props: OmniboxProps): ReactElement {
|
||||
const location = useLocation();
|
||||
const history = useHistory();
|
||||
const leapConfig = useSettingsState(settingsSel);
|
||||
const omniboxRef = useRef<HTMLDivElement | null>(null)
|
||||
const omniboxRef = useRef<HTMLDivElement | null>(null);
|
||||
const inputRef = useRef<HTMLInputElement | null>(null);
|
||||
|
||||
const [query, setQuery] = useState('');
|
||||
@ -46,21 +57,24 @@ export function Omnibox(props: OmniboxProps) {
|
||||
const notifications = useHarkState(state => state.notifications);
|
||||
const invites = useInviteState(state => state.invites);
|
||||
const tiles = useLaunchState(state => state.tiles);
|
||||
const [leapCursor, setLeapCursor] = useState('pointer');
|
||||
|
||||
const contacts = useMemo(() => {
|
||||
const maybeShip = `~${deSig(query)}`;
|
||||
return ob.isValidPatp(maybeShip)
|
||||
? { ...contactState, [maybeShip]: {} }
|
||||
: contactState;
|
||||
const selflessContactState = omit(contactState, `~${window.ship}`);
|
||||
return ob.isValidPatp(maybeShip) && maybeShip !== `~${window.ship}`
|
||||
? { ...selflessContactState, [maybeShip]: {} }
|
||||
: selflessContactState;
|
||||
}, [contactState, query]);
|
||||
|
||||
const groups = useGroupState(state => state.groups);
|
||||
const associations = useMetadataState(state => state.associations);
|
||||
|
||||
const selectedGroup = useMemo(
|
||||
() => location.pathname.startsWith('/~landscape/ship/')
|
||||
? '/' + location.pathname.split('/').slice(2,5).join('/')
|
||||
: null,
|
||||
const selectedGroup = useMemo(
|
||||
() =>
|
||||
location.pathname.startsWith('/~landscape/ship/')
|
||||
? '/' + location.pathname.split('/').slice(2, 5).join('/')
|
||||
: null,
|
||||
[location.pathname]
|
||||
);
|
||||
|
||||
@ -71,16 +85,9 @@ export function Omnibox(props: OmniboxProps) {
|
||||
tiles,
|
||||
selectedGroup,
|
||||
groups,
|
||||
leapConfig,
|
||||
leapConfig
|
||||
);
|
||||
}, [
|
||||
selectedGroup,
|
||||
leapConfig,
|
||||
contacts,
|
||||
associations,
|
||||
groups,
|
||||
tiles
|
||||
]);
|
||||
}, [selectedGroup, leapConfig, contacts, associations, groups, tiles]);
|
||||
|
||||
const onOutsideClick = useCallback(() => {
|
||||
props.show && props.toggle();
|
||||
@ -90,7 +97,7 @@ export function Omnibox(props: OmniboxProps) {
|
||||
|
||||
// handle omnibox show
|
||||
useEffect(() => {
|
||||
if(!props.show) {
|
||||
if (!props.show) {
|
||||
return;
|
||||
}
|
||||
Mousetrap.bind('escape', props.toggle);
|
||||
@ -104,29 +111,37 @@ export function Omnibox(props: OmniboxProps) {
|
||||
}, [props.show]);
|
||||
|
||||
const initialResults = useMemo(() => {
|
||||
return new Map(SEARCHED_CATEGORIES.map((category) => {
|
||||
if (category === 'other') {
|
||||
return ['other', index.get('other').filter(({ app }) => app !== 'tutorial')];
|
||||
}
|
||||
return [category, []];
|
||||
}));
|
||||
return new Map(
|
||||
SEARCHED_CATEGORIES.map((category) => {
|
||||
if (category === 'other') {
|
||||
return [
|
||||
'other',
|
||||
index.get('other').filter(({ app }) => app !== 'tutorial')
|
||||
];
|
||||
}
|
||||
return [category, []];
|
||||
})
|
||||
);
|
||||
}, [index]);
|
||||
|
||||
const results = useMemo(() => {
|
||||
if(query.length <= 1) {
|
||||
if (query.length <= 1) {
|
||||
return initialResults;
|
||||
}
|
||||
const q = query.toLowerCase();
|
||||
const resultsMap = new Map();
|
||||
SEARCHED_CATEGORIES.map((category) => {
|
||||
const categoryIndex = index.get(category);
|
||||
resultsMap.set(category,
|
||||
resultsMap.set(
|
||||
category,
|
||||
categoryIndex.filter((result) => {
|
||||
return (
|
||||
result.title.toLowerCase().includes(q) ||
|
||||
result.link.toLowerCase().includes(q) ||
|
||||
result.app.toLowerCase().includes(q) ||
|
||||
(result.host !== null ? result.host.toLowerCase().includes(q) : false)
|
||||
(result.host !== null
|
||||
? result.host.toLowerCase().includes(q)
|
||||
: false)
|
||||
);
|
||||
})
|
||||
);
|
||||
@ -134,21 +149,26 @@ export function Omnibox(props: OmniboxProps) {
|
||||
return resultsMap;
|
||||
}, [query, index]);
|
||||
|
||||
const navigate = useCallback((app: string, link: string) => {
|
||||
props.toggle();
|
||||
if (defaultApps.includes(app.toLowerCase())
|
||||
|| app === 'profile'
|
||||
|| app === 'messages'
|
||||
|| app === 'tutorial'
|
||||
|| app === 'Links'
|
||||
|| app === 'Terminal'
|
||||
|| app === 'home'
|
||||
|| app === 'inbox') {
|
||||
history.push(link);
|
||||
} else {
|
||||
window.location.href = link;
|
||||
}
|
||||
}, [history, props.toggle]);
|
||||
const navigate = useCallback(
|
||||
(app: string, link: string) => {
|
||||
props.toggle();
|
||||
if (
|
||||
defaultApps.includes(app.toLowerCase()) ||
|
||||
app === 'profile' ||
|
||||
app === 'messages' ||
|
||||
app === 'tutorial' ||
|
||||
app === 'Links' ||
|
||||
app === 'Terminal' ||
|
||||
app === 'home' ||
|
||||
app === 'inbox'
|
||||
) {
|
||||
history.push(link);
|
||||
} else {
|
||||
window.location.href = link;
|
||||
}
|
||||
},
|
||||
[history, props.toggle]
|
||||
);
|
||||
|
||||
const setPreviousSelected = useCallback(() => {
|
||||
const flattenedResults = Array.from(results.values()).flat();
|
||||
@ -193,50 +213,59 @@ export function Omnibox(props: OmniboxProps) {
|
||||
}
|
||||
}, [selected, results]);
|
||||
|
||||
const control = useCallback((evt) => {
|
||||
if (evt.key === 'Escape') {
|
||||
if (query.length > 0) {
|
||||
setQuery('');
|
||||
return;
|
||||
} else if (props.show) {
|
||||
props.toggle();
|
||||
return;
|
||||
const setSelection = (app, link) => {
|
||||
setLeapCursor('pointer');
|
||||
setSelected([app, link]);
|
||||
};
|
||||
|
||||
const control = useCallback(
|
||||
(evt) => {
|
||||
if (evt.key === 'Escape') {
|
||||
if (query.length > 0) {
|
||||
setQuery('');
|
||||
return;
|
||||
} else if (props.show) {
|
||||
props.toggle();
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (
|
||||
evt.key === 'ArrowUp' ||
|
||||
(evt.shiftKey && evt.key === 'Tab')) {
|
||||
if (evt.key === 'ArrowUp' || (evt.shiftKey && evt.key === 'Tab')) {
|
||||
evt.preventDefault();
|
||||
setPreviousSelected();
|
||||
return;
|
||||
}
|
||||
if (evt.key === 'ArrowDown' || evt.key === 'Tab') {
|
||||
evt.preventDefault();
|
||||
setNextSelected();
|
||||
return;
|
||||
}
|
||||
if (evt.key === 'Enter') {
|
||||
evt.preventDefault();
|
||||
if (selected.length) {
|
||||
navigate(selected[0], selected[1]);
|
||||
} else if (Array.from(results.values()).flat().length === 0) {
|
||||
setPreviousSelected();
|
||||
setLeapCursor('none');
|
||||
return;
|
||||
} else {
|
||||
navigate(
|
||||
Array.from(results.values()).flat()[0].app,
|
||||
Array.from(results.values()).flat()[0].link);
|
||||
}
|
||||
}
|
||||
}, [
|
||||
props.toggle,
|
||||
selected,
|
||||
navigate,
|
||||
query,
|
||||
props.show,
|
||||
results,
|
||||
setPreviousSelected,
|
||||
setNextSelected
|
||||
]);
|
||||
if (evt.key === 'ArrowDown' || evt.key === 'Tab') {
|
||||
evt.preventDefault();
|
||||
setNextSelected();
|
||||
setLeapCursor('none');
|
||||
return;
|
||||
}
|
||||
if (evt.key === 'Enter') {
|
||||
evt.preventDefault();
|
||||
if (selected.length) {
|
||||
navigate(selected[0], selected[1]);
|
||||
} else if (Array.from(results.values()).flat().length === 0) {
|
||||
return;
|
||||
} else {
|
||||
navigate(
|
||||
Array.from(results.values()).flat()[0].app,
|
||||
Array.from(results.values()).flat()[0].link
|
||||
);
|
||||
}
|
||||
}
|
||||
},
|
||||
[
|
||||
props.toggle,
|
||||
selected,
|
||||
navigate,
|
||||
query,
|
||||
props.show,
|
||||
results,
|
||||
setPreviousSelected,
|
||||
setNextSelected
|
||||
]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
const flattenedResultLinks = Array.from(results.values())
|
||||
@ -252,88 +281,106 @@ export function Omnibox(props: OmniboxProps) {
|
||||
}, []);
|
||||
|
||||
// Sort Omnibox results alphabetically
|
||||
const sortResults = (a: Record<'title', string>, b: Record<'title', string>) => {
|
||||
const sortResults = (
|
||||
a: Record<'title', string>,
|
||||
b: Record<'title', string>
|
||||
) => {
|
||||
// Do not sort unless searching (preserves order of menu actions)
|
||||
if (query === '') { return 0 };
|
||||
if (a.title < b.title) { return -1 };
|
||||
if (a.title > b.title) { return 1 };
|
||||
if (query === '') {
|
||||
return 0;
|
||||
}
|
||||
if (a.title < b.title) {
|
||||
return -1;
|
||||
}
|
||||
if (a.title > b.title) {
|
||||
return 1;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
};
|
||||
|
||||
const renderResults = useCallback(() => {
|
||||
return <Box
|
||||
maxHeight={['200px', '400px']}
|
||||
overflowY="auto"
|
||||
overflowX="hidden"
|
||||
borderBottomLeftRadius='2'
|
||||
borderBottomRightRadius='2'
|
||||
>
|
||||
{SEARCHED_CATEGORIES
|
||||
.map(category => Object({ category, categoryResults: results.get(category) }))
|
||||
.filter(category => category.categoryResults.length > 0)
|
||||
.map(({ category, categoryResults }, i) => {
|
||||
const categoryTitle = (category === 'other')
|
||||
? null : <Row pl='2' height='5' alignItems='center' bg='washedGray'><Text gray bold>{category.charAt(0).toUpperCase() + category.slice(1)}</Text></Row>;
|
||||
const sel = selected?.length ? selected[1] : '';
|
||||
return (<Box key={i} width='max(50vw, 300px)' maxWidth='600px'>
|
||||
{categoryTitle}
|
||||
{categoryResults
|
||||
.sort(sortResults)
|
||||
.map((result, i2) => (
|
||||
<OmniboxResult
|
||||
key={i2}
|
||||
icon={result.app}
|
||||
text={result.title}
|
||||
subtext={result.host}
|
||||
link={result.link}
|
||||
navigate={() => navigate(result.app, result.link)}
|
||||
selected={sel}
|
||||
/>
|
||||
))}
|
||||
</Box>
|
||||
);
|
||||
})
|
||||
}
|
||||
</Box>;
|
||||
return (
|
||||
<Box
|
||||
maxHeight={['200px', '400px']}
|
||||
overflowY='auto'
|
||||
overflowX='hidden'
|
||||
borderBottomLeftRadius='2'
|
||||
borderBottomRightRadius='2'
|
||||
>
|
||||
{SEARCHED_CATEGORIES.map(category =>
|
||||
Object({ category, categoryResults: results.get(category) })
|
||||
)
|
||||
.filter(category => category.categoryResults.length > 0)
|
||||
.map(({ category, categoryResults }, i) => {
|
||||
const categoryTitle =
|
||||
category === 'other' ? null : (
|
||||
<Row pl='2' height='5' alignItems='center' bg='washedGray'>
|
||||
<Text gray bold>
|
||||
{category.charAt(0).toUpperCase() + category.slice(1)}
|
||||
</Text>
|
||||
</Row>
|
||||
);
|
||||
const sel = selected?.length ? selected[1] : '';
|
||||
return (
|
||||
<Box key={i} width='max(50vw, 300px)' maxWidth='600px'>
|
||||
{categoryTitle}
|
||||
{categoryResults.sort(sortResults).map((result, i2) => (
|
||||
<OmniboxResult
|
||||
key={i2}
|
||||
icon={result.app}
|
||||
text={result.title}
|
||||
subtext={result.host}
|
||||
link={result.link}
|
||||
cursor={leapCursor}
|
||||
navigate={() => navigate(result.app, result.link)}
|
||||
setSelection={() => setSelection(result.app, result.link)}
|
||||
selected={sel}
|
||||
/>
|
||||
))}
|
||||
</Box>
|
||||
);
|
||||
})}
|
||||
</Box>
|
||||
);
|
||||
}, [results, navigate, selected, contactState, notifications, invites]);
|
||||
|
||||
return (
|
||||
<Portal>
|
||||
<Box
|
||||
backgroundColor='scales.black30'
|
||||
width='100%'
|
||||
height='100%'
|
||||
position='absolute'
|
||||
top='0'
|
||||
right='0'
|
||||
zIndex={11}
|
||||
display={props.show ? 'block' : 'none'}
|
||||
>
|
||||
<Row justifyContent='center'>
|
||||
<Box
|
||||
mt={['10vh', '20vh']}
|
||||
width='max(50vw, 300px)'
|
||||
maxWidth='600px'
|
||||
borderRadius='2'
|
||||
backgroundColor='white'
|
||||
<Box
|
||||
backgroundColor='scales.black30'
|
||||
width='100%'
|
||||
height='100%'
|
||||
position='absolute'
|
||||
top='0'
|
||||
right='0'
|
||||
zIndex={11}
|
||||
display={props.show ? 'block' : 'none'}
|
||||
>
|
||||
<Row justifyContent='center'>
|
||||
<Box
|
||||
mt={['10vh', '20vh']}
|
||||
width='max(50vw, 300px)'
|
||||
maxWidth='600px'
|
||||
borderRadius='2'
|
||||
backgroundColor='white'
|
||||
ref={(el) => {
|
||||
omniboxRef.current = el;
|
||||
}}
|
||||
>
|
||||
<OmniboxInput
|
||||
ref={(el) => {
|
||||
omniboxRef.current = el;
|
||||
}}
|
||||
>
|
||||
<OmniboxInput
|
||||
ref={(el) => {
|
||||
inputRef.current = el;
|
||||
}}
|
||||
control={e => control(e)}
|
||||
search={search}
|
||||
query={query}
|
||||
/>
|
||||
{renderResults()}
|
||||
</Box>
|
||||
</Row>
|
||||
</Box>
|
||||
</Portal>
|
||||
);
|
||||
}
|
||||
inputRef.current = el;
|
||||
}}
|
||||
control={e => control(e)}
|
||||
search={search}
|
||||
query={query}
|
||||
/>
|
||||
{renderResults()}
|
||||
</Box>
|
||||
</Row>
|
||||
</Box>
|
||||
</Portal>
|
||||
);
|
||||
}
|
||||
|
||||
export default withLocalState(Omnibox, ['toggleOmnibox', 'omniboxShown']);
|
||||
|
@ -1,4 +1,3 @@
|
||||
|
||||
import React, { Component } from 'react';
|
||||
import { BaseInput } from '@tlon/indigo-react';
|
||||
|
||||
@ -6,33 +5,31 @@ export class OmniboxInput extends Component {
|
||||
render() {
|
||||
const { props } = this;
|
||||
return (
|
||||
<BaseInput
|
||||
ref={(el) => {
|
||||
this.input = el;
|
||||
<BaseInput
|
||||
ref={(el) => {
|
||||
this.input = el;
|
||||
if (el && document.activeElement.isSameNode(el)) {
|
||||
el.blur();
|
||||
el.focus();
|
||||
}
|
||||
}
|
||||
}
|
||||
width='100%'
|
||||
p='2'
|
||||
backgroundColor='white'
|
||||
color='black'
|
||||
border='1px solid transparent'
|
||||
borderRadius='2'
|
||||
maxWidth='calc(600px - 1.15rem)'
|
||||
fontSize='1'
|
||||
style={{ boxSizing: 'border-box' }}
|
||||
placeholder='Search...'
|
||||
onKeyDown={props.control}
|
||||
onChange={props.search}
|
||||
spellCheck={false}
|
||||
value={props.query}
|
||||
/>
|
||||
}}
|
||||
width='100%'
|
||||
p='2'
|
||||
backgroundColor='white'
|
||||
color='black'
|
||||
border='1px solid transparent'
|
||||
borderRadius='2'
|
||||
maxWidth='calc(600px - 1.15rem)'
|
||||
fontSize='1'
|
||||
style={{ boxSizing: 'border-box' }}
|
||||
placeholder='Search...'
|
||||
onKeyDown={props.control}
|
||||
onChange={props.search}
|
||||
spellCheck={false}
|
||||
value={props.query}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default OmniboxInput;
|
||||
|
||||
|
@ -21,52 +21,143 @@ export class OmniboxResult extends Component {
|
||||
|
||||
componentDidUpdate(prevProps) {
|
||||
const { props, state } = this;
|
||||
if (prevProps &&
|
||||
if (
|
||||
prevProps &&
|
||||
!state.hovered &&
|
||||
prevProps.selected !== props.selected &&
|
||||
props.selected === props.link
|
||||
) {
|
||||
this.result.current.scrollIntoView({ block: 'nearest' });
|
||||
}
|
||||
) {
|
||||
this.result.current.scrollIntoView({ block: 'nearest' });
|
||||
}
|
||||
}
|
||||
|
||||
getIcon(icon, selected, link, invites, notifications, text, color) {
|
||||
const iconFill = (this.state.hovered || (selected === link)) ? 'white' : 'black';
|
||||
const bulletFill = (this.state.hovered || (selected === link)) ? 'white' : 'blue';
|
||||
const iconFill =
|
||||
this.state.hovered || selected === link ? 'white' : 'black';
|
||||
const bulletFill =
|
||||
this.state.hovered || selected === link ? 'white' : 'blue';
|
||||
|
||||
const inviteCount = [].concat(...Object.values(invites).map(obj => Object.values(obj)));
|
||||
const inviteCount = [].concat(
|
||||
...Object.values(invites).map((obj) => Object.values(obj))
|
||||
);
|
||||
|
||||
let graphic = <div />;
|
||||
if (defaultApps.includes(icon.toLowerCase())
|
||||
|| icon.toLowerCase() === 'links'
|
||||
|| icon.toLowerCase() === 'terminal')
|
||||
{
|
||||
icon = (icon === 'Link') ? 'Collection' :
|
||||
(icon === 'Terminal') ? 'Dojo' : icon;
|
||||
graphic = <Icon display="inline-block" verticalAlign="middle" icon={icon} mr='2' size='18px' color={iconFill} />;
|
||||
if (
|
||||
defaultApps.includes(icon.toLowerCase()) ||
|
||||
icon.toLowerCase() === 'links' ||
|
||||
icon.toLowerCase() === 'terminal'
|
||||
) {
|
||||
icon =
|
||||
icon === 'Link' ? 'Collection' : icon === 'Terminal' ? 'Dojo' : icon;
|
||||
graphic = (
|
||||
<Icon
|
||||
display='inline-block'
|
||||
verticalAlign='middle'
|
||||
icon={icon}
|
||||
mr='2'
|
||||
size='18px'
|
||||
color={iconFill}
|
||||
/>
|
||||
);
|
||||
} else if (icon === 'inbox') {
|
||||
graphic = <Box display='flex' verticalAlign='middle' position="relative">
|
||||
<Icon display='inline-block' verticalAlign='middle' icon='Inbox' mr='2' size='18px' color={iconFill} />
|
||||
{(notifications > 0 || inviteCount.length > 0) && (
|
||||
<Icon display='inline-block' icon='Bullet' style={{ position: 'absolute', top: -5, left: 5 }} color={bulletFill} />
|
||||
)}
|
||||
</Box>;
|
||||
graphic = (
|
||||
<Box display='flex' verticalAlign='middle' position='relative'>
|
||||
<Icon
|
||||
display='inline-block'
|
||||
verticalAlign='middle'
|
||||
icon='Inbox'
|
||||
mr='2'
|
||||
size='18px'
|
||||
color={iconFill}
|
||||
/>
|
||||
{(notifications > 0 || inviteCount.length > 0) && (
|
||||
<Icon
|
||||
display='inline-block'
|
||||
icon='Bullet'
|
||||
style={{ position: 'absolute', top: -5, left: 5 }}
|
||||
color={bulletFill}
|
||||
/>
|
||||
)}
|
||||
</Box>
|
||||
);
|
||||
} else if (icon === 'logout') {
|
||||
graphic = <Icon display="inline-block" verticalAlign="middle" icon='SignOut' mr='2' size='18px' color={iconFill} />;
|
||||
graphic = (
|
||||
<Icon
|
||||
display='inline-block'
|
||||
verticalAlign='middle'
|
||||
icon='SignOut'
|
||||
mr='2'
|
||||
size='18px'
|
||||
color={iconFill}
|
||||
/>
|
||||
);
|
||||
} else if (icon === 'profile') {
|
||||
text = text.startsWith('Profile') ? window.ship : text;
|
||||
graphic = <Sigil color={color} classes='dib flex-shrink-0 v-mid mr2' ship={text} size={18} icon padding={2} />;
|
||||
graphic = (
|
||||
<Sigil
|
||||
color={color}
|
||||
classes='dib flex-shrink-0 v-mid mr2'
|
||||
ship={text}
|
||||
size={18}
|
||||
icon
|
||||
padding={2}
|
||||
/>
|
||||
);
|
||||
} else if (icon === 'home') {
|
||||
graphic = <Icon display='inline-block' verticalAlign='middle' icon='Home' mr='2' size='18px' color={iconFill} />;
|
||||
graphic = (
|
||||
<Icon
|
||||
display='inline-block'
|
||||
verticalAlign='middle'
|
||||
icon='Home'
|
||||
mr='2'
|
||||
size='18px'
|
||||
color={iconFill}
|
||||
/>
|
||||
);
|
||||
} else if (icon === 'notifications') {
|
||||
graphic = <Icon display='inline-block' verticalAlign='middle' icon='Inbox' mr='2' size='18px' color={iconFill} />;
|
||||
graphic = (
|
||||
<Icon
|
||||
display='inline-block'
|
||||
verticalAlign='middle'
|
||||
icon='Inbox'
|
||||
mr='2'
|
||||
size='18px'
|
||||
color={iconFill}
|
||||
/>
|
||||
);
|
||||
} else if (icon === 'messages') {
|
||||
graphic = <Icon display='inline-block' verticalAlign='middle' icon='Users' mr='2' size='18px' color={iconFill} />;
|
||||
graphic = (
|
||||
<Icon
|
||||
display='inline-block'
|
||||
verticalAlign='middle'
|
||||
icon='Users'
|
||||
mr='2'
|
||||
size='18px'
|
||||
color={iconFill}
|
||||
/>
|
||||
);
|
||||
} else if (icon === 'tutorial') {
|
||||
graphic = <Icon display='inline-block' verticalAlign='middle' icon='Tutorial' mr='2' size='18px' color={iconFill} />;
|
||||
}
|
||||
else {
|
||||
graphic = <Icon display='inline-block' icon='NullIcon' verticalAlign="middle" mr='2' size="16px" color={iconFill} />;
|
||||
graphic = (
|
||||
<Icon
|
||||
display='inline-block'
|
||||
verticalAlign='middle'
|
||||
icon='Tutorial'
|
||||
mr='2'
|
||||
size='18px'
|
||||
color={iconFill}
|
||||
/>
|
||||
);
|
||||
} else {
|
||||
graphic = (
|
||||
<Icon
|
||||
display='inline-block'
|
||||
icon='NullIcon'
|
||||
verticalAlign='middle'
|
||||
mr='2'
|
||||
size='16px'
|
||||
color={iconFill}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return graphic;
|
||||
@ -77,53 +168,81 @@ export class OmniboxResult extends Component {
|
||||
}
|
||||
|
||||
render() {
|
||||
const { icon, text, subtext, link, navigate, selected, invites, notificationsCount, contacts } = this.props;
|
||||
const {
|
||||
icon,
|
||||
text,
|
||||
subtext,
|
||||
link,
|
||||
cursor,
|
||||
navigate,
|
||||
selected,
|
||||
invites,
|
||||
notificationsCount,
|
||||
contacts,
|
||||
setSelection
|
||||
} = this.props;
|
||||
|
||||
const color = contacts?.[text] ? `#${uxToHex(contacts[text].color)}` : "#000000";
|
||||
const graphic = this.getIcon(icon, selected, link, invites, notificationsCount, text, color);
|
||||
const color = contacts?.[text]
|
||||
? `#${uxToHex(contacts[text].color)}`
|
||||
: '#000000';
|
||||
const graphic = this.getIcon(
|
||||
icon,
|
||||
selected,
|
||||
link,
|
||||
invites,
|
||||
notificationsCount,
|
||||
text,
|
||||
color
|
||||
);
|
||||
|
||||
return (
|
||||
<Row
|
||||
py='2'
|
||||
px='2'
|
||||
cursor='pointer'
|
||||
onMouseEnter={() => this.setHover(true)}
|
||||
onMouseLeave={() => this.setHover(false)}
|
||||
backgroundColor={
|
||||
this.state.hovered || selected === link ? 'blue' : 'white'
|
||||
}
|
||||
onClick={navigate}
|
||||
width="100%"
|
||||
justifyContent="space-between"
|
||||
ref={this.result}
|
||||
py='2'
|
||||
px='2'
|
||||
cursor={cursor}
|
||||
onMouseMove={() => setSelection()}
|
||||
onMouseLeave={() => this.setHover(false)}
|
||||
backgroundColor={
|
||||
this.state.hovered || selected === link ? 'blue' : 'white'
|
||||
}
|
||||
onClick={navigate}
|
||||
width='100%'
|
||||
justifyContent='space-between'
|
||||
ref={this.result}
|
||||
>
|
||||
<Box display="flex" verticalAlign="middle" maxWidth="60%" flexShrink={0}>
|
||||
{graphic}
|
||||
<Text
|
||||
mono={(icon == 'profile' && text.startsWith('~'))}
|
||||
color={this.state.hovered || selected === link ? 'white' : 'black'}
|
||||
display='inline-block'
|
||||
verticalAlign='middle'
|
||||
width='100%'
|
||||
overflow='hidden'
|
||||
textOverflow='ellipsis'
|
||||
whiteSpace='pre'
|
||||
mr='1'
|
||||
<Box
|
||||
display='flex'
|
||||
verticalAlign='middle'
|
||||
maxWidth='60%'
|
||||
flexShrink={0}
|
||||
>
|
||||
{text.startsWith("~") ? cite(text) : text}
|
||||
</Text>
|
||||
{graphic}
|
||||
<Text
|
||||
mono={icon == 'profile' && text.startsWith('~')}
|
||||
color={this.state.hovered || selected === link ? 'white' : 'black'}
|
||||
display='inline-block'
|
||||
verticalAlign='middle'
|
||||
width='100%'
|
||||
overflow='hidden'
|
||||
textOverflow='ellipsis'
|
||||
whiteSpace='pre'
|
||||
mr='1'
|
||||
>
|
||||
{text.startsWith('~') ? cite(text) : text}
|
||||
</Text>
|
||||
</Box>
|
||||
<Text pr='2'
|
||||
display="inline-block"
|
||||
verticalAlign="middle"
|
||||
color={this.state.hovered || selected === link ? 'white' : 'black'}
|
||||
width='100%'
|
||||
minWidth={0}
|
||||
textOverflow="ellipsis"
|
||||
whiteSpace="pre"
|
||||
overflow="hidden"
|
||||
maxWidth="40%"
|
||||
textAlign='right'
|
||||
<Text
|
||||
pr='2'
|
||||
display='inline-block'
|
||||
verticalAlign='middle'
|
||||
color={this.state.hovered || selected === link ? 'white' : 'black'}
|
||||
width='100%'
|
||||
minWidth={0}
|
||||
textOverflow='ellipsis'
|
||||
whiteSpace='pre'
|
||||
overflow='hidden'
|
||||
maxWidth='40%'
|
||||
textAlign='right'
|
||||
>
|
||||
{subtext}
|
||||
</Text>
|
||||
@ -136,4 +255,4 @@ export default withState(OmniboxResult, [
|
||||
[useInviteState],
|
||||
[useHarkState, ['notificationsCount']],
|
||||
[useContactState]
|
||||
]);
|
||||
]);
|
||||
|
@ -18,7 +18,11 @@ export function useGraphModule(
|
||||
}
|
||||
|
||||
const notifications = graphUnreads?.[s]?.['/']?.notifications;
|
||||
if ( notifications > 0 ) {
|
||||
if (
|
||||
notifications &&
|
||||
((typeof notifications === 'number' && notifications > 0)
|
||||
|| notifications.length)
|
||||
) {
|
||||
return 'notification';
|
||||
}
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user