groups: handle s3 upload failures in chat

This commit is contained in:
Patrick O'Sullivan 2022-03-30 15:04:01 -05:00
parent 836cdb2478
commit 9db2024676

View File

@ -1,6 +1,14 @@
import { Box, Icon, LoadingSpinner, Row } from '@tlon/indigo-react'; import { Box, Col, Icon, LoadingSpinner, Row, Text } from '@tlon/indigo-react';
import { Contact, Content, evalCord } from '@urbit/api'; import { Contact, Content, evalCord } from '@urbit/api';
import React, { FC, PropsWithChildren, useRef, useState } from 'react'; import VisibilitySensor from 'react-visibility-sensor';
import React, {
FC,
PropsWithChildren,
useCallback,
useEffect,
useRef,
useState
} from 'react';
import tokenizeMessage from '~/logic/lib/tokenizeMessage'; import tokenizeMessage from '~/logic/lib/tokenizeMessage';
import { IuseStorage } from '~/logic/lib/useStorage'; import { IuseStorage } from '~/logic/lib/useStorage';
import { MOBILE_BROWSER_REGEX } from '~/logic/lib/util'; import { MOBILE_BROWSER_REGEX } from '~/logic/lib/util';
@ -11,41 +19,54 @@ import { ChatAvatar } from './ChatAvatar';
import { useChatStore } from './ChatPane'; import { useChatStore } from './ChatPane';
import { useImperativeHandle } from 'react'; import { useImperativeHandle } from 'react';
import { FileUploadSource, useFileUpload } from '~/logic/lib/useFileUpload'; import { FileUploadSource, useFileUpload } from '~/logic/lib/useFileUpload';
import { Portal } from '~/views/components/Portal';
import styled from 'styled-components';
import { useOutsideClick } from '~/logic/lib/useOutsideClick';
type ChatInputProps = PropsWithChildren<IuseStorage & { const FixedOverlay = styled(Col)`
hideAvatars: boolean; position: fixed;
ourContact?: Contact; -webkit-transition: all 0.1s ease-out;
placeholder: string; -moz-transition: all 0.1s ease-out;
onSubmit: (contents: Content[]) => void; -o-transition: all 0.1s ease-out;
}>; transition: all 0.1s ease-out;
`;
type ChatInputProps = PropsWithChildren<
IuseStorage & {
hideAvatars: boolean;
ourContact?: Contact;
placeholder: string;
onSubmit: (contents: Content[]) => void;
}
>;
const InputBox: FC = ({ children }) => ( const InputBox: FC = ({ children }) => (
<Row <Row
alignItems='center' alignItems="center"
position='relative' position="relative"
flexGrow={1} flexGrow={1}
flexShrink={0} flexShrink={0}
borderTop={1} borderTop={1}
borderTopColor='lightGray' borderTopColor="lightGray"
backgroundColor='white' backgroundColor="white"
className='cf' className="cf"
zIndex={0} zIndex={0}
> >
{ children } {children}
</Row> </Row>
); );
const IconBox = ({ children, ...props }) => ( const IconBox = ({ children, ...props }) => (
<Box <Box
ml='12px' ml="12px"
mr={3} mr={3}
flexShrink={0} flexShrink={0}
height='16px' height="16px"
width='16px' width="16px"
flexBasis='16px' flexBasis="16px"
{...props} {...props}
> >
{ children } {children}
</Box> </Box>
); );
@ -68,99 +89,153 @@ const MobileSubmitButton = ({ enabled, onSubmit }) => (
</Box> </Box>
); );
export const ChatInput = React.forwardRef(({ ourContact, hideAvatars, placeholder, onSubmit }: ChatInputProps, ref) => { export const ChatInput = React.forwardRef(
const chatEditor = useRef<CodeMirrorShim>(null); ({ ourContact, hideAvatars, placeholder, onSubmit }: ChatInputProps, ref) => {
useImperativeHandle(ref, () => chatEditor.current); const chatEditor = useRef<CodeMirrorShim>(null);
const [inCodeMode, setInCodeMode] = useState(false); useImperativeHandle(ref, () => chatEditor.current);
const [inCodeMode, setInCodeMode] = useState(false);
const [uploadError, setUploadError] = useState<string>('');
const [showPortal, setShowPortal] = useState(false);
const [visible, setVisible] = useState(false);
const innerRef = useRef<HTMLDivElement>(null);
const outerRef = useRef<HTMLDivElement>(null);
const { useEffect(() => {
message, if (!visible) {
setMessage setShowPortal(false);
} = useChatStore(); }
const { canUpload, uploading, promptUpload, onPaste } = useFileUpload({ }, [visible]);
onSuccess: uploadSuccess
});
function uploadSuccess(url: string, source: FileUploadSource) { useOutsideClick(innerRef, () => setShowPortal(false));
if (source === 'paste') {
setMessage(url);
} else {
onSubmit([{ url }]);
}
}
function toggleCode() { const handleError = useCallback((err: Error) => {
setInCodeMode(!inCodeMode); setUploadError(err.message);
} }, []);
async function submit() { const { message, setMessage } = useChatStore();
const text = chatEditor.current?.getValue() || ''; const { canUpload, uploading, promptUpload, onPaste } = useFileUpload({
onSuccess: uploadSuccess,
onError: handleError
});
if (text === '') { function uploadSuccess(url: string, source: FileUploadSource) {
return; if (source === 'paste') {
setMessage(url);
} else {
onSubmit([{ url }]);
}
} }
if (inCodeMode) { function toggleCode() {
const output = await airlock.thread<string[]>(evalCord(text)); setInCodeMode(!inCodeMode);
onSubmit([{ code: { output, expression: text } }]);
} else {
onSubmit(tokenizeMessage(text));
} }
setInCodeMode(false); async function submit() {
setMessage(''); const text = chatEditor.current?.getValue() || '';
chatEditor.current.focus();
}
return ( if (text === '') {
<InputBox> return;
<Row p='12px 4px 12px 12px' flexShrink={0} alignItems='center'> }
<ChatAvatar contact={ourContact} hideAvatars={hideAvatars} />
</Row> if (inCodeMode) {
<ChatEditor const output = await airlock.thread<string[]>(evalCord(text));
ref={chatEditor} onSubmit([{ code: { output, expression: text } }]);
inCodeMode={inCodeMode} } else {
submit={submit} onSubmit(tokenizeMessage(text));
onPaste={(cm, e) => onPaste(e)} }
placeholder={placeholder}
/> setInCodeMode(false);
<IconBox mr={canUpload ? '12px' : 3}> setMessage('');
<Icon chatEditor.current.focus();
icon='Dojo' }
cursor='pointer'
onClick={toggleCode} return (
color={inCodeMode ? 'blue' : 'black'} <Box ref={outerRef}>
/> <VisibilitySensor active={showPortal} onChange={setVisible}>
</IconBox> <InputBox>
{canUpload && ( {showPortal && (
<IconBox> <Portal>
{uploading ? ( <FixedOverlay
<LoadingSpinner /> ref={innerRef}
) : ( backgroundColor="white"
<Icon color="washedGray"
icon='Attachment' border={1}
cursor='pointer' right={25}
width='16' bottom={75}
height='16' borderRadius={2}
onClick={() => borderColor="lightGray"
promptUpload().then(url => uploadSuccess(url, 'direct')) boxShadow="0px 0px 0px 3px"
} zIndex={3}
fontSize={0}
height="75px"
width="250px"
padding={3}
justifyContent="center"
alignItems="center"
>
<Text>{uploadError}</Text>
<Text>Please check S3 settings.</Text>
</FixedOverlay>
</Portal>
)}
<Row p="12px 4px 12px 12px" flexShrink={0} alignItems="center">
<ChatAvatar contact={ourContact} hideAvatars={hideAvatars} />
</Row>
<ChatEditor
ref={chatEditor}
inCodeMode={inCodeMode}
submit={submit}
onPaste={(cm, e) => onPaste(e)}
placeholder={placeholder}
/> />
)} <IconBox mr={canUpload ? '12px' : 3}>
</IconBox> <Icon
)} icon="Dojo"
{MOBILE_BROWSER_REGEX.test(navigator.userAgent) && ( cursor="pointer"
<MobileSubmitButton onClick={toggleCode}
enabled={message !== ''} color={inCodeMode ? 'blue' : 'black'}
onSubmit={submit} />
/> </IconBox>
)} {canUpload && (
</InputBox> <IconBox>
); {uploading ? (
}); uploadError != '' ? (
<Icon
icon="ExclaimationMark"
cursor="pointer"
onClick={() => setShowPortal(true)}
/>
) : (
<LoadingSpinner />
)
) : (
<Icon
icon="Attachment"
cursor="pointer"
width="16"
height="16"
onClick={() =>
promptUpload(handleError).then(url =>
uploadSuccess(url, 'direct')
)
}
/>
)}
</IconBox>
)}
{MOBILE_BROWSER_REGEX.test(navigator.userAgent) && (
<MobileSubmitButton enabled={message !== ''} onSubmit={submit} />
)}
</InputBox>
</VisibilitySensor>
</Box>
);
}
);
// @ts-ignore withLocalState prop passing weirdness // @ts-ignore withLocalState prop passing weirdness
export default withLocalState<Omit<ChatInputProps, keyof IuseStorage>, 'hideAvatars', ChatInput>( export default withLocalState<
ChatInput, Omit<ChatInputProps, keyof IuseStorage>,
['hideAvatars'] 'hideAvatars',
); typeof ChatInput
>(ChatInput, ['hideAvatars']);