mirror of
https://github.com/ilyakooo0/urbit.git
synced 2024-12-15 01:52:42 +03:00
Merge branch 'release/next-js' into lf/dm-redir
This commit is contained in:
commit
c0ee07a240
@ -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) => {
|
||||
|
@ -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);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -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>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
@ -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">
|
||||
|
@ -112,6 +112,7 @@ export function ProfileStatus(props: any): ReactElement {
|
||||
display='inline-block'
|
||||
verticalAlign='middle'
|
||||
color='gray'
|
||||
title={contact?.status ?? ''}
|
||||
>
|
||||
{contact?.status ?? ''}
|
||||
</RichText>
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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"
|
||||
/>
|
||||
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
@ -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>
|
||||
|
@ -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";
|
||||
|
@ -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}
|
||||
|
@ -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;
|
||||
|
@ -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;
|
||||
|
@ -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>
|
||||
);
|
||||
|
@ -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}
|
||||
|
Loading…
Reference in New Issue
Block a user