Merge branch 'release/next-js' into lf/dm-redir

This commit is contained in:
Matilde Park 2021-03-23 21:47:41 -04:00
commit c0ee07a240
16 changed files with 298 additions and 314 deletions

View File

@ -76,7 +76,8 @@ class GcpManager {
if (this.isConfigured()) {
this.refreshLoop();
} else {
this.refreshAfter(10_000);
console.log('GcpManager: GCP storage not configured; stopping.');
this.stop();
}
})
.catch((reason) => {

View File

@ -20,6 +20,7 @@ import GcpReducer from '../reducers/gcp-reducer';
import { OrderedMap } from '../lib/OrderedMap';
import { BigIntOrderedMap } from '../lib/BigIntOrderedMap';
import { GroupViewReducer } from '../reducers/group-view';
import { unstable_batchedUpdates } from 'react-dom';
export default class GlobalStore extends BaseStore<StoreState> {
inviteReducer = new InviteReducer();
@ -50,21 +51,23 @@ export default class GlobalStore extends BaseStore<StoreState> {
}
reduce(data: Cage, state: StoreState) {
// debug shim
const tag = Object.keys(data)[0];
const oldActions = this.pastActions[tag] || [];
this.pastActions[tag] = [data[tag], ...oldActions.slice(0,14)];
this.inviteReducer.reduce(data);
this.metadataReducer.reduce(data);
this.s3Reducer.reduce(data);
this.groupReducer.reduce(data);
GroupViewReducer(data);
this.launchReducer.reduce(data);
this.connReducer.reduce(data, this.state);
GraphReducer(data);
HarkReducer(data);
ContactReducer(data);
this.settingsReducer.reduce(data);
this.gcpReducer.reduce(data);
unstable_batchedUpdates(() => {
// debug shim
const tag = Object.keys(data)[0];
const oldActions = this.pastActions[tag] || [];
this.pastActions[tag] = [data[tag], ...oldActions.slice(0, 14)];
this.inviteReducer.reduce(data);
this.metadataReducer.reduce(data);
this.s3Reducer.reduce(data);
this.groupReducer.reduce(data);
GroupViewReducer(data);
this.launchReducer.reduce(data);
this.connReducer.reduce(data, this.state);
GraphReducer(data);
HarkReducer(data);
ContactReducer(data);
this.settingsReducer.reduce(data);
this.gcpReducer.reduce(data);
});
}
}

View File

@ -199,6 +199,7 @@ export default class ChatEditor extends Component {
width='calc(100% - 88px)'
className={inCodeMode ? 'chat code' : 'chat'}
color="black"
overflow='scroll'
>
{MOBILE_BROWSER_REGEX.test(navigator.userAgent)
? <MobileBox

View File

@ -1,6 +1,6 @@
import React, { useEffect, useState } from 'react';
import moment from 'moment';
import { Box, Text } from '@tlon/indigo-react';
import { Box, Text, Center, Icon } from '@tlon/indigo-react';
import VisibilitySensor from 'react-visibility-sensor';
import Timestamp from '~/views/components/Timestamp';
@ -8,51 +8,67 @@ import Timestamp from '~/views/components/Timestamp';
export const UnreadNotice = (props) => {
const { unreadCount, unreadMsg, dismissUnread, onClick } = props;
if (!unreadMsg || (unreadCount === 0)) {
if (!unreadMsg || unreadCount === 0) {
return null;
}
const stamp = moment.unix(unreadMsg.post['time-sent'] / 1000);
let datestamp = moment.unix(unreadMsg.post['time-sent'] / 1000).format('YYYY.M.D');
const timestamp = moment.unix(unreadMsg.post['time-sent'] / 1000).format('HH:mm');
let datestamp = moment
.unix(unreadMsg.post['time-sent'] / 1000)
.format('YYYY.M.D');
const timestamp = moment
.unix(unreadMsg.post['time-sent'] / 1000)
.format('HH:mm');
if (datestamp === moment().format('YYYY.M.D')) {
datestamp = null;
}
return (
<Box style={{ left: '0px', top: '0px' }}
p='4'
<Box
style={{ left: '0px', top: '0px' }}
p='12px'
width='100%'
position='absolute'
zIndex='1'
className='unread-notice'
>
<Box
backgroundColor='white'
display='flex'
alignItems='center'
p='2'
fontSize='0'
justifyContent='space-between'
borderRadius='1'
border='1'
borderColor='blue'>
<Text flexShrink='1' textOverflow='ellipsis' whiteSpace='pre' overflow='hidden' display='flex' cursor='pointer' onClick={onClick}>
{unreadCount} new message{unreadCount > 1 ? 's' : ''} since{' '}
<Timestamp stamp={stamp} color='blue' date={true} fontSize={1} />
</Text>
<Text
ml='4'
color='blue'
cursor='pointer'
textAlign='right'
flexShrink='0'
onClick={dismissUnread}>
Mark as Read
</Text>
</Box>
<Center>
<Box backgroundColor='white' borderRadius='2'>
<Box
backgroundColor='washedBlue'
display='flex'
alignItems='center'
p='2'
fontSize='0'
justifyContent='space-between'
borderRadius='3'
border='1'
borderColor='lightBlue'
>
<Text
textOverflow='ellipsis'
whiteSpace='pre'
overflow='hidden'
display='flex'
cursor='pointer'
onClick={onClick}
>
{unreadCount} new message{unreadCount > 1 ? 's' : ''} since{' '}
<Timestamp stamp={stamp} color='black' date={true} fontSize={1} />
</Text>
<Icon
icon='X'
ml='4'
color='black'
cursor='pointer'
textAlign='right'
onClick={dismissUnread}
/>
</Box>
</Box>
</Center>
</Box>
);
}
};

View File

@ -93,7 +93,10 @@ function Group(props: GroupProps) {
);
const { hideUnreads } = useSettingsState(selectCalmState);
const joined = useSettingsState(selectJoined);
const days = Math.floor(moment.duration(moment(joined).add(14, 'days').diff(moment())).as('days'));
const days = Math.max(0, Math.floor(moment.duration(moment(joined)
.add(14, 'days')
.diff(moment()))
.as('days'))) || 0;
return (
<Tile ref={anchorRef} position="relative" bg={isTutorialGroup ? 'lightBlue' : undefined} to={`/~landscape${path}`} gridColumnStart={first ? '1' : null}>
<Col height="100%" justifyContent="space-between">

View File

@ -112,6 +112,7 @@ export function ProfileStatus(props: any): ReactElement {
display='inline-block'
verticalAlign='middle'
color='gray'
title={contact?.status ?? ''}
>
{contact?.status ?? ''}
</RichText>

View File

@ -142,6 +142,10 @@
margin-bottom: 16px;
}
.md ul ul {
margin-bottom: 0px;
}
.md h2, .md h3, .md h4, .md h5, .md p, .md a, .md ul {
font-weight: 400;
}

View File

@ -54,10 +54,10 @@ export function CalmPrefs(props: {
hideUnreads,
hideGroups,
hideUtilities,
imageShown,
videoShown,
oembedShown,
audioShown,
imageShown: !imageShown,
videoShown: !videoShown,
oembedShown: !oembedShown,
audioShown: !audioShown
};
const onSubmit = useCallback(async (v: FormSchema, actions: FormikHelpers<FormSchema>) => {
@ -67,10 +67,10 @@ export function CalmPrefs(props: {
api.settings.putEntry('calm', 'hideUnreads', v.hideUnreads),
api.settings.putEntry('calm', 'hideGroups', v.hideGroups),
api.settings.putEntry('calm', 'hideUtilities', v.hideUtilities),
api.settings.putEntry('remoteContentPolicy', 'imageShown', v.imageShown),
api.settings.putEntry('remoteContentPolicy', 'videoShown', v.videoShown),
api.settings.putEntry('remoteContentPolicy', 'audioShown', v.audioShown),
api.settings.putEntry('remoteContentPolicy', 'oembedShown', v.oembedShown),
api.settings.putEntry('remoteContentPolicy', 'imageShown', !v.imageShown),
api.settings.putEntry('remoteContentPolicy', 'videoShown', !v.videoShown),
api.settings.putEntry('remoteContentPolicy', 'audioShown', !v.audioShown),
api.settings.putEntry('remoteContentPolicy', 'oembedShown', !v.oembedShown),
]);
actions.setStatus({ success: null });
}, [api]);
@ -115,24 +115,24 @@ export function CalmPrefs(props: {
id="hideNicknames"
caption="Do not show user-set nicknames"
/>
<Text fontWeight="medium">Remote Content</Text>
<Text fontWeight="medium">Remote content</Text>
<Toggle
label="Load images"
label="Disable images"
id="imageShown"
caption="Images will be replaced with an inline placeholder that must be clicked to be viewed"
/>
<Toggle
label="Load audio files"
label="Disable audio files"
id="audioShown"
caption="Audio content will be replaced with an inline placeholder that must be clicked to be viewed"
/>
<Toggle
label="Load video files"
label="Disable video files"
id="videoShown"
caption="Video content will be replaced with an inline placeholder that must be clicked to be viewed"
/>
<Toggle
label="Load embedded content"
label="Disable embedded content"
id="oembedShown"
caption="Embedded content may contain scripts that can track you"
/>

View File

@ -1,85 +0,0 @@
import React from 'react';
import {
Box,
Button,
ManagedCheckboxField as Checkbox
} from '@tlon/indigo-react';
import { Formik, Form } from 'formik';
import * as Yup from 'yup';
import GlobalApi from '~/logic/api/global';
import useSettingsState, {selectSettingsState} from '~/logic/state/settings';
const formSchema = Yup.object().shape({
imageShown: Yup.boolean(),
audioShown: Yup.boolean(),
videoShown: Yup.boolean(),
oembedShown: Yup.boolean()
});
interface FormSchema {
imageShown: boolean;
audioShown: boolean;
videoShown: boolean;
oembedShown: boolean;
}
interface RemoteContentFormProps {
api: GlobalApi;
}
const selState = selectSettingsState(['remoteContentPolicy', 'set']);
export default function RemoteContentForm(props: RemoteContentFormProps) {
const { api } = props;
const { remoteContentPolicy, set: setRemoteContentPolicy} = useSettingsState(selState);
const imageShown = remoteContentPolicy.imageShown;
const audioShown = remoteContentPolicy.audioShown;
const videoShown = remoteContentPolicy.videoShown;
const oembedShown = remoteContentPolicy.oembedShown;
return (
<Formik
validationSchema={formSchema}
initialValues={
{
imageShown,
audioShown,
videoShown,
oembedShown
} as FormSchema
}
onSubmit={(values, actions) => {
setRemoteContentPolicy((state) => {
Object.assign(state.remoteContentPolicy, values);
});
actions.setSubmitting(false);
}}
>
{props => (
<Form>
<Box
display="grid"
gridTemplateColumns="1fr"
gridTemplateRows="audio"
gridRowGap={5}
>
<Box color="black" fontSize={1} fontWeight={900}>
Remote Content
</Box>
<Checkbox label="Load images" id="imageShown" />
<Checkbox label="Load audio files" id="audioShown" />
<Checkbox label="Load video files" id="videoShown" />
<Checkbox
label="Load embedded content"
id="oembedShown"
caption="Embedded content may contain scripts"
/>
<Button style={{ cursor: 'pointer' }} border={1} borderColor="washedGray" type="submit">
Save
</Button>
</Box>
</Form>
)}
</Formik>
);
}

View File

@ -1,5 +1,5 @@
import React, { ReactElement, useCallback } from 'react';
import { Formik } from 'formik';
import { Formik, FormikHelpers } from 'formik';
import {
ManagedTextInputField as Input,
@ -10,6 +10,7 @@ import {
Col,
Anchor
} from '@tlon/indigo-react';
import { AsyncButton } from "~/views/components/AsyncButton";
import GlobalApi from '~/logic/api/global';
import { BucketList } from './BucketList';
@ -35,19 +36,19 @@ export default function S3Form(props: S3FormProps): ReactElement {
const { api } = props;
const s3 = useStorageState((state) => state.s3);
const onSubmit = useCallback(
(values: FormSchema) => {
const onSubmit = useCallback(async (values: FormSchema, actions: FormikHelpers<FormSchema>) => {
if (values.s3secretAccessKey !== s3.credentials?.secretAccessKey) {
api.s3.setSecretAccessKey(values.s3secretAccessKey);
await api.s3.setSecretAccessKey(values.s3secretAccessKey);
}
if (values.s3endpoint !== s3.credentials?.endpoint) {
api.s3.setEndpoint(values.s3endpoint);
await api.s3.setEndpoint(values.s3endpoint);
}
if (values.s3accessKeyId !== s3.credentials?.accessKeyId) {
api.s3.setAccessKeyId(values.s3accessKeyId);
await api.s3.setAccessKeyId(values.s3accessKeyId);
}
actions.setStatus({ success: null });
},
[api, s3]
);
@ -95,9 +96,9 @@ export default function S3Form(props: S3FormProps): ReactElement {
label='Secret Access Key'
id='s3secretAccessKey'
/>
<Button style={{ cursor: 'pointer' }} type='submit'>
<AsyncButton primary style={{ cursor: 'pointer' }} type='submit'>
Submit
</Button>
</AsyncButton>
</Col>
</Form>
</Formik>

View File

@ -7,7 +7,6 @@ import { StoreState } from "~/logic/store/type";
import DisplayForm from "./lib/DisplayForm";
import S3Form from "./lib/S3Form";
import SecuritySettings from "./lib/Security";
import RemoteContentForm from "./lib/RemoteContent";
import { NotificationPreferences } from "./lib/NotificationPref";
import { CalmPrefs } from "./lib/CalmPref";
import { Link } from "react-router-dom";

View File

@ -11,7 +11,7 @@ import {
import { Invite } from '@urbit/api/invite';
import { Text, Icon, Row } from '@tlon/indigo-react';
import { cite } from '~/logic/lib/util';
import { cite, useShowNickname } from '~/logic/lib/util';
import GlobalApi from '~/logic/api/global';
import { resourceFromPath } from '~/logic/lib/group';
import { GroupInvite } from './Group';
@ -19,6 +19,7 @@ import { InviteSkeleton } from './InviteSkeleton';
import { JoinSkeleton } from './JoinSkeleton';
import { useWaitForProps } from '~/logic/lib/useWaitForProps';
import useGroupState from '~/logic/state/group';
import useContactState from '~/logic/state/contact';
import useMetadataState from '~/logic/state/metadata';
import useGraphState from '~/logic/state/graph';
@ -38,6 +39,9 @@ export function InviteItem(props: InviteItemProps) {
const groups = useGroupState(state => state.groups);
const graphKeys = useGraphState(s => s.graphKeys);
const associations = useMetadataState(state => state.associations);
const contacts = useContactState(state => state.contacts);
const contact = contacts?.[`~${invite?.ship}`] ?? {};
const showNickname = useShowNickname(contact);
const waiter = useWaitForProps(
{ associations, groups, pendingJoin, graphKeys: Array.from(graphKeys) },
50000
@ -119,8 +123,10 @@ export function InviteItem(props: InviteItemProps) {
>
<Row py="1" alignItems="center">
<Icon display="block" color="blue" icon="Bullet" mr="2" />
<Text mr="1" mono>
{cite(`~${invite!.ship}`)}
<Text mr="1"
mono={!showNickname}
fontWeight={showNickname ? '500' : '400'}>
{showNickname ? contact?.nickname : cite(`~${invite!.ship}`)}
</Text>
<Text mr="1">invited you to a DM</Text>
</Row>
@ -145,8 +151,10 @@ export function InviteItem(props: InviteItemProps) {
>
<Row py="1" alignItems="center">
<Icon display="block" color="blue" icon="Bullet" mr="2" />
<Text mr="1" mono>
{cite(`~${invite!.ship}`)}
<Text mr="1"
mono={!showNickname}
fontWeight={showNickname ? '500' : '400'}>
{showNickname ? contact?.nickname : cite(`~${invite!.ship}`)}
</Text>
<Text mr="1">
invited you to ~{invite.resource.ship}/{invite.resource.name}

View File

@ -11,7 +11,8 @@ import {
Text,
BaseImage,
ColProps,
Icon
Icon,
Center
} from '@tlon/indigo-react';
import RichText from './RichText';
import { ProfileStatus } from './ProfileStatus';
@ -44,16 +45,19 @@ const ProfileOverlay = (props: ProfileOverlayProps) => {
onDismiss,
...rest
} = props;
const hideAvatars = useSettingsState(state => state.calm.hideAvatars);
const hideNicknames = useSettingsState(state => state.calm.hideNicknames);
const hideAvatars = useSettingsState((state) => state.calm.hideAvatars);
const hideNicknames = useSettingsState((state) => state.calm.hideNicknames);
const popoverRef = useRef<typeof Col>(null);
const onDocumentClick = useCallback((event) => {
if (!popoverRef.current || popoverRef?.current?.contains(event.target)) {
return;
}
onDismiss();
}, [onDismiss, popoverRef]);
const onDocumentClick = useCallback(
(event) => {
if (!popoverRef.current || popoverRef?.current?.contains(event.target)) {
return;
}
onDismiss();
},
[onDismiss, popoverRef]
);
useEffect(() => {
document.addEventListener('mousedown', onDocumentClick);
@ -62,123 +66,124 @@ const ProfileOverlay = (props: ProfileOverlayProps) => {
return () => {
document.removeEventListener('mousedown', onDocumentClick);
document.removeEventListener('touchstart', onDocumentClick);
}
};
}, [onDocumentClick]);
let top, bottom;
if (topSpace < OVERLAY_HEIGHT / 2) {
top = '0px';
}
if (bottomSpace < OVERLAY_HEIGHT / 2) {
bottom = '0px';
}
if (!(top || bottom)) {
bottom = `-${Math.round(OVERLAY_HEIGHT / 2)}px`;
}
const containerStyle = { top, bottom, left: '100%' };
if (topSpace < OVERLAY_HEIGHT / 2) {
top = '0px';
}
if (bottomSpace < OVERLAY_HEIGHT / 2) {
bottom = '0px';
}
if (!(top || bottom)) {
bottom = `-${Math.round(OVERLAY_HEIGHT / 2)}px`;
}
const containerStyle = { top, bottom, left: '100%' };
const isOwn = window.ship === ship;
const isOwn = window.ship === ship;
const img =
contact?.avatar && !hideAvatars ? (
<BaseImage
referrerPolicy="no-referrer"
display='inline-block'
style={{ objectFit: 'cover' }}
src={contact.avatar}
height={72}
width={72}
borderRadius={2}
/>
) : (
<Sigil ship={ship} size={72} color={color} />
);
const showNickname = useShowNickname(contact, hideNicknames);
return (
<Col
ref={popoverRef}
backgroundColor='white'
color='washedGray'
border={1}
const img =
contact?.avatar && !hideAvatars ? (
<BaseImage
referrerPolicy='no-referrer'
display='inline-block'
style={{ objectFit: 'cover' }}
src={contact.avatar}
height={60}
width={60}
borderRadius={2}
borderColor='lightGray'
boxShadow='0px 0px 0px 3px'
position='absolute'
zIndex='3'
fontSize='0'
height='250px'
width='250px'
padding={3}
justifyContent='center'
style={containerStyle}
{...rest}
>
<Row color='black' padding={3} position='absolute' top={0} left={0}>
{!isOwn && (
<Icon
icon='Chat'
size={16}
cursor='pointer'
onClick={() => history.push(`/~landscape/dm/${ship}`)}
/>
)}
</Row>
<Box
alignSelf='center'
height='72px'
cursor='pointer'
onClick={() => history.push(`/~profile/~${ship}`)}
overflow='hidden'
borderRadius={2}
>
{img}
</Box>
<Col
position='absolute'
overflow='hidden'
minWidth='0'
width='100%'
padding={3}
bottom={0}
left={0}
>
<Row width='100%'>
<Text
fontWeight='600'
mono={!showNickname}
textOverflow='ellipsis'
overflow='hidden'
whiteSpace='pre'
marginBottom='0'
>
{showNickname ? contact?.nickname : cite(ship)}
</Text>
</Row>
{isOwn ? (
<ProfileStatus
api={props.api}
ship={`~${ship}`}
contact={contact}
/>
) : (
<RichText
display='inline-block'
width='100%'
minWidth='0'
textOverflow='ellipsis'
overflow='hidden'
whiteSpace='pre'
marginBottom='0'
disableRemoteContent
gray
>
{contact?.status ? contact.status : ''}
</RichText>
)}
</Col>
</Col>
/>
) : (
<Box size={60} backgroundColor={color}>
<Center height={60}>
<Sigil ship={ship} size={32} color={color} />
</Center>
</Box>
);
const showNickname = useShowNickname(contact, hideNicknames);
return (
<Col
ref={popoverRef}
backgroundColor='white'
color='washedGray'
border={1}
borderRadius={2}
borderColor='lightGray'
boxShadow='0px 0px 0px 3px'
position='absolute'
zIndex='3'
fontSize='0'
height='250px'
width='250px'
padding={3}
justifyContent='center'
style={containerStyle}
{...rest}
>
<Row color='black' padding={3} position='absolute' top={0} left={0}>
{!isOwn && (
<Icon
icon='Chat'
size={16}
cursor='pointer'
onClick={() => history.push(`/~landscape/dm/${ship}`)}
/>
)}
</Row>
<Box
alignSelf='center'
height='60px'
cursor='pointer'
onClick={() => history.push(`/~profile/~${ship}`)}
overflow='hidden'
borderRadius={2}
>
{img}
</Box>
<Col
position='absolute'
overflow='hidden'
minWidth='0'
width='100%'
padding={3}
bottom={0}
left={0}
>
<Row width='100%'>
<Text
fontWeight='600'
mono={!showNickname}
textOverflow='ellipsis'
overflow='hidden'
whiteSpace='pre'
marginBottom='0'
>
{showNickname ? contact?.nickname : cite(ship)}
</Text>
</Row>
{isOwn ? (
<ProfileStatus api={props.api} ship={`~${ship}`} contact={contact} />
) : (
<RichText
display='inline-block'
width='100%'
minWidth='0'
textOverflow='ellipsis'
overflow='hidden'
whiteSpace='pre'
marginBottom='0'
disableRemoteContent
gray
title={contact?.status ?? ''}
>
{contact?.status ?? ''}
</RichText>
)}
</Col>
</Col>
);
};
export default ProfileOverlay;
export default ProfileOverlay;

View File

@ -11,20 +11,24 @@ export type TimestampProps = BoxProps & {
stamp: MomentType;
date?: boolean;
time?: boolean;
}
};
const Timestamp = (props: TimestampProps): ReactElement | null=> {
const Timestamp = (props: TimestampProps): ReactElement | null => {
const { stamp, date, time, color, fontSize, ...rest } = {
time: true, color: 'gray', fontSize: 0, ...props
time: true,
color: 'gray',
fontSize: 0,
...props
};
if (!stamp) return null;
const { hovering, bind } = date === true
? { hovering: true, bind: {} }
: useHovering();
const { hovering, bind } =
date === true ? { hovering: true, bind: {} } : useHovering();
let datestamp = stamp.format(DateFormat);
if (stamp.format(DateFormat) === moment().format(DateFormat)) {
datestamp = 'Today';
} else if (stamp.format(DateFormat) === moment().subtract(1, 'day').format(DateFormat)) {
} else if (
stamp.format(DateFormat) === moment().subtract(1, 'day').format(DateFormat)
) {
datestamp = 'Yesterday';
}
const timestamp = stamp.format(TimeFormat);
@ -33,22 +37,28 @@ const Timestamp = (props: TimestampProps): ReactElement | null=> {
{...bind}
display='flex'
flex='row'
flexWrap="nowrap"
flexWrap='nowrap'
{...rest}
title={stamp.format(DateFormat + ' ' + TimeFormat)}
>
{time && <Text flexShrink={0} color={color} fontSize={fontSize}>{timestamp}</Text>}
{date !== false && <Text
flexShrink={0}
color={color}
fontSize={fontSize}
ml={time ? 2 : 0}
display={time ? ['none', hovering ? 'block' : 'none'] : 'block'}
>
{datestamp}
</Text>}
{time && (
<Text flexShrink={0} color={color} fontSize={fontSize}>
{timestamp}
</Text>
)}
{date !== false && (
<Text
flexShrink={0}
color={color}
fontSize={fontSize}
display={time ? ['none', hovering ? 'block' : 'none'] : 'block'}
>
{time ? '\u00A0' : ''}
{datestamp}
</Text>
)}
</Box>
)
}
);
};
export default Timestamp;
export default Timestamp;

View File

@ -30,7 +30,7 @@ interface OmniboxProps {
notifications: number;
}
const SEARCHED_CATEGORIES = ['ships', 'other', 'commands', 'groups', 'subscriptions', 'apps'];
const SEARCHED_CATEGORIES = ['commands', 'ships', 'other', 'groups', 'subscriptions', 'apps'];
const settingsSel = (s: SettingsState) => s.leap;
export function Omnibox(props: OmniboxProps) {
@ -251,6 +251,15 @@ export function Omnibox(props: OmniboxProps) {
setQuery(event.target.value);
}, []);
// Sort Omnibox results alphabetically
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 };
return 0;
}
const renderResults = useCallback(() => {
return <Box
maxHeight={['200px', '400px']}
@ -268,16 +277,18 @@ export function Omnibox(props: OmniboxProps) {
const sel = selected?.length ? selected[1] : '';
return (<Box key={i} width='max(50vw, 300px)' maxWidth='600px'>
{categoryTitle}
{categoryResults.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}
/>
{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>
);

View File

@ -64,7 +64,7 @@ export class OmniboxResult extends Component {
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} />;
}
@ -102,6 +102,12 @@ export class OmniboxResult extends Component {
<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}