Merge pull request #5682 from urbit/po/fix-silent-s3-failures

groups: fix silent s3 failures
This commit is contained in:
Hunter Miller 2022-03-31 11:26:40 -05:00 committed by GitHub
commit 1949174f63
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 380 additions and 207 deletions

View File

@ -10,7 +10,7 @@ export interface IuseStorage {
upload: (file: File, bucket: string) => Promise<string>; upload: (file: File, bucket: string) => Promise<string>;
uploadDefault: (file: File) => Promise<string>; uploadDefault: (file: File) => Promise<string>;
uploading: boolean; uploading: boolean;
promptUpload: () => Promise<string>; promptUpload: (onError?: (err: Error) => void) => Promise<string>;
} }
const useStorage = ({ accept = '*' } = { accept: '*' }): IuseStorage => { const useStorage = ({ accept = '*' } = { accept: '*' }): IuseStorage => {
@ -85,7 +85,7 @@ const useStorage = ({ accept = '*' } = { accept: '*' }): IuseStorage => {
}, [s3, upload]); }, [s3, upload]);
const promptUpload = useCallback( const promptUpload = useCallback(
(): Promise<string> => { (onError?: (err: Error) => void): Promise<string> => {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
const fileSelector = document.createElement('input'); const fileSelector = document.createElement('input');
fileSelector.setAttribute('type', 'file'); fileSelector.setAttribute('type', 'file');
@ -95,6 +95,9 @@ const useStorage = ({ accept = '*' } = { accept: '*' }): IuseStorage => {
const files = fileSelector.files; const files = fileSelector.files;
if (!files || files.length <= 0) { if (!files || files.length <= 0) {
reject(); reject();
} else if (onError) {
uploadDefault(files[0]).then(resolve).catch(err => onError(err));
document.body.removeChild(fileSelector);
} else { } else {
uploadDefault(files[0]).then(resolve); uploadDefault(files[0]).then(resolve);
document.body.removeChild(fileSelector); document.body.removeChild(fileSelector);

View File

@ -1,6 +1,13 @@
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,
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 +18,57 @@ 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;
uploadError: string;
setUploadError: (val: string) => void;
handleUploadError: (err: Error) => 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 +91,158 @@ 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); (
useImperativeHandle(ref, () => chatEditor.current); {
const [inCodeMode, setInCodeMode] = useState(false); ourContact,
hideAvatars,
placeholder,
onSubmit,
uploadError,
setUploadError,
handleUploadError
}: ChatInputProps,
ref
) => {
const chatEditor = useRef<CodeMirrorShim>(null);
useImperativeHandle(ref, () => chatEditor.current);
const [inCodeMode, setInCodeMode] = useState(false);
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 { message, setMessage } = useChatStore();
setInCodeMode(!inCodeMode); const { canUpload, uploading, promptUpload, onPaste } = useFileUpload({
} onSuccess: uploadSuccess,
onError: handleUploadError
});
async function submit() { function uploadSuccess(url: string, source: FileUploadSource) {
const text = chatEditor.current?.getValue() || ''; if (source === 'paste') {
setMessage(url);
if (text === '') { } else {
return; onSubmit([{ url }]);
}
setUploadError('');
} }
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}
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>
)} {console.log({ uploadError })}
</InputBox> {canUpload && (
); <IconBox>
}); {uploadError == '' && uploading && <LoadingSpinner />}
{uploadError !== '' && (
<Icon
icon="ExclaimationMark"
cursor="pointer"
onClick={() => setShowPortal(true)}
/>
)}
{uploadError == '' && !uploading && (
<Icon
icon="Attachment"
cursor="pointer"
width="16"
height="16"
onClick={() =>
promptUpload(handleUploadError).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']);

View File

@ -114,8 +114,18 @@ export function ChatPane(props: ChatPaneProps): ReactElement {
const graphTimesentMap = useGraphTimesent(id); const graphTimesentMap = useGraphTimesent(id);
const ourContact = useOurContact(); const ourContact = useOurContact();
const { restore, setMessage } = useChatStore(s => ({ setMessage: s.setMessage, restore: s.restore })); const { restore, setMessage } = useChatStore(s => ({ setMessage: s.setMessage, restore: s.restore }));
const [uploadError, setUploadError] = useState<string>('');
const handleUploadError = useCallback((err: Error) => {
setUploadError(err.message);
}, []);
const { canUpload, drag } = useFileUpload({ const { canUpload, drag } = useFileUpload({
onSuccess: url => onSubmit([{ url }]) onSuccess: (url) => {
onSubmit([{ url }]);
setUploadError('');
},
onError: handleUploadError
}); });
useEffect(() => { useEffect(() => {
@ -171,6 +181,9 @@ export function ChatPane(props: ChatPaneProps): ReactElement {
onSubmit={onSubmit} onSubmit={onSubmit}
ourContact={(promptShare.length === 0 && ourContact) || undefined} ourContact={(promptShare.length === 0 && ourContact) || undefined}
placeholder="Message..." placeholder="Message..."
uploadError={uploadError}
setUploadError={setUploadError}
handleUploadError={handleUploadError}
/> />
)} )}
</Col> </Col>

View File

@ -1,5 +1,5 @@
import React, { useCallback, useState } from 'react'; import React, { useCallback, useState } from 'react';
import { Box, LoadingSpinner, Action, Row } from '@tlon/indigo-react'; import { Box, LoadingSpinner, Action, Row, Icon, Text } from '@tlon/indigo-react';
import { StatelessUrlInput } from '~/views/components/StatelessUrlInput'; import { StatelessUrlInput } from '~/views/components/StatelessUrlInput';
import SubmitDragger from '~/views/components/SubmitDragger'; import SubmitDragger from '~/views/components/SubmitDragger';
@ -21,6 +21,7 @@ export function LinkBlockInput(props: LinkBlockInputProps) {
const [url, setUrl] = useState(props.url || ''); const [url, setUrl] = useState(props.url || '');
const [valid, setValid] = useState(false); const [valid, setValid] = useState(false);
const [focussed, setFocussed] = useState(false); const [focussed, setFocussed] = useState(false);
const [error, setError] = useState<string>('');
const addPost = useGraphState(selGraph); const addPost = useGraphState(selGraph);
@ -39,10 +40,16 @@ export function LinkBlockInput(props: LinkBlockInputProps) {
const handleChange = useCallback((val: string) => { const handleChange = useCallback((val: string) => {
setUrl(val); setUrl(val);
setValid(URLparser.test(val) || Boolean(parsePermalink(val))); setValid(URLparser.test(val) || Boolean(parsePermalink(val)));
setError('');
}, []);
const handleError = useCallback((err: Error) => {
setError(err.message);
}, []); }, []);
const { uploading, canUpload, promptUpload, drag } = useFileUpload({ const { uploading, canUpload, promptUpload, drag } = useFileUpload({
onSuccess: handleChange onSuccess: handleChange,
onError: handleError
}); });
const doPost = () => { const doPost = () => {
@ -64,7 +71,7 @@ export function LinkBlockInput(props: LinkBlockInputProps) {
}; };
const onKeyPress = useCallback( const onKeyPress = useCallback(
(e) => { (e: any) => {
if (e.key === 'Enter') { if (e.key === 'Enter') {
e.preventDefault(); e.preventDefault();
doPost(); doPost();
@ -89,22 +96,43 @@ export function LinkBlockInput(props: LinkBlockInputProps) {
backgroundColor="washedGray" backgroundColor="washedGray"
{...drag.bind} {...drag.bind}
> >
{drag.dragging && <SubmitDragger />} {drag.dragging && canUpload && <SubmitDragger />}
{uploading ? ( {uploading ? (
<Box error != '' ? (
display="flex" <Box
width="100%" display="flex"
height="100%" flexDirection="column"
position="absolute" width="100%"
left={0} height="100%"
right={0} padding={3}
bg="white" position="absolute"
zIndex={9} left={0}
alignItems="center" right={0}
justifyContent="center" bg="white"
> zIndex={9}
<LoadingSpinner /> alignItems="center"
</Box> justifyContent="center"
>
<Icon icon="ExclaimationMarkBold" size={32} />
<Text bold>{error}</Text>
<Text>Please check your S3 settings.</Text>
</Box>
) : (
<Box
display="flex"
width="100%"
height="100%"
position="absolute"
left={0}
right={0}
bg="white"
zIndex={9}
alignItems="center"
justifyContent="center"
>
<LoadingSpinner />
</Box>
)
) : ( ) : (
<StatelessUrlInput <StatelessUrlInput
value={url} value={url}
@ -114,6 +142,7 @@ export function LinkBlockInput(props: LinkBlockInputProps) {
focussed={focussed} focussed={focussed}
onBlur={onBlur} onBlur={onBlur}
promptUpload={promptUpload} promptUpload={promptUpload}
handleError={handleError}
onKeyPress={onKeyPress} onKeyPress={onKeyPress}
center center
/> />
@ -125,7 +154,11 @@ export function LinkBlockInput(props: LinkBlockInputProps) {
p="2" p="2"
justifyContent="row-end" justifyContent="row-end"
> >
<Action onClick={doPost} disabled={!valid} backgroundColor="transparent"> <Action
onClick={doPost}
disabled={!valid}
backgroundColor="transparent"
>
Post Post
</Action> </Action>
</Row> </Row>

View File

@ -1,6 +1,13 @@
import { BaseInput, Box, Button, LoadingSpinner } from '@tlon/indigo-react'; import {
BaseInput,
Box,
Button,
Icon,
LoadingSpinner,
Text
} from '@tlon/indigo-react';
import { hasProvider } from 'oembed-parser'; import { hasProvider } from 'oembed-parser';
import React, { useState, useEffect } from 'react'; import React, { useState, useEffect, useCallback } from 'react';
import { parsePermalink, permalinkToReference } from '~/logic/lib/permalinks'; import { parsePermalink, permalinkToReference } from '~/logic/lib/permalinks';
import { StatelessUrlInput } from '~/views/components/StatelessUrlInput'; import { StatelessUrlInput } from '~/views/components/StatelessUrlInput';
import SubmitDragger from '~/views/components/SubmitDragger'; import SubmitDragger from '~/views/components/SubmitDragger';
@ -22,15 +29,18 @@ const LinkSubmit = (props: LinkSubmitProps) => {
const [linkTitle, setLinkTitle] = useState(''); const [linkTitle, setLinkTitle] = useState('');
const [disabled, setDisabled] = useState(false); const [disabled, setDisabled] = useState(false);
const [linkValid, setLinkValid] = useState(false); const [linkValid, setLinkValid] = useState(false);
const [error, setError] = useState<string>('');
const { const handleError = useCallback((err: Error) => {
canUpload, setError(err.message);
uploading, }, []);
promptUpload,
drag, const { canUpload, uploading, promptUpload, drag, onPaste } = useFileUpload({
onPaste onSuccess: (url) => {
} = useFileUpload({ setLinkValue(url);
onSuccess: setLinkValue, setError('');
},
onError: handleError,
multiple: false multiple: false
}); });
@ -38,25 +48,21 @@ const LinkSubmit = (props: LinkSubmitProps) => {
const url = linkValue; const url = linkValue;
const text = linkTitle ? linkTitle : linkValue; const text = linkTitle ? linkTitle : linkValue;
const contents = url.startsWith('web+urbitgraph:/') const contents = url.startsWith('web+urbitgraph:/')
? [{ text }, permalinkToReference(parsePermalink(url)!)] ? [{ text }, permalinkToReference(parsePermalink(url)!)]
: [{ text }, { url }]; : [{ text }, { url }];
setDisabled(true); setDisabled(true);
const parentIndex = props.parentIndex || ''; const parentIndex = props.parentIndex || '';
const post = createPost(window.ship, contents, parentIndex); const post = createPost(window.ship, contents, parentIndex);
addPost( addPost(`~${props.ship}`, props.name, post);
`~${props.ship}`,
props.name,
post
);
setDisabled(false); setDisabled(false);
setLinkValue(''); setLinkValue('');
setLinkTitle(''); setLinkTitle('');
setLinkValid(false); setLinkValid(false);
}; };
const validateLink = (link) => { const validateLink = (link: any) => {
const URLparser = new RegExp( const URLparser = new RegExp(
/((?:([\w\d\.-]+)\:\/\/?){1}(?:(www)\.?){0,1}(((?:[\w\d-]+\.)*)([\w\d-]+\.[\w\d]+))){1}(?:\:(\d+)){0,1}((\/(?:(?:[^\/\s\?]+\/)*))(?:([^\?\/\s#]+?(?:.[^\?\s]+){0,1}){0,1}(?:\?([^\s#]+)){0,1})){0,1}(?:#([^#\s]+)){0,1}/ /((?:([\w\d\.-]+)\:\/\/?){1}(?:(www)\.?){0,1}(((?:[\w\d-]+\.)*)([\w\d-]+\.[\w\d]+))){1}(?:\:(\d+)){0,1}((\/(?:(?:[^\/\s\?]+\/)*))(?:([^\?\/\s#]+?(?:.[^\?\s]+){0,1}){0,1}(?:\?([^\s#]+)){0,1})){0,1}(?:#([^#\s]+)){0,1}/
); );
@ -70,9 +76,9 @@ const LinkSubmit = (props: LinkSubmitProps) => {
setLinkValue(link); setLinkValue(link);
} }
} }
if(link.startsWith('web+urbitgraph://')) { if (link.startsWith('web+urbitgraph://')) {
const permalink = parsePermalink(link); const permalink = parsePermalink(link);
if(!permalink) { if (!permalink) {
setLinkValid(false); setLinkValid(false);
return; return;
} }
@ -86,17 +92,23 @@ const LinkSubmit = (props: LinkSubmitProps) => {
if (result.title && !linkTitle) { if (result.title && !linkTitle) {
setLinkTitle(result.title); setLinkTitle(result.title);
} }
}).catch((error) => { /* noop*/ }); })
.catch((error) => {
/* noop*/
});
} else if (!linkTitle) { } else if (!linkTitle) {
setLinkTitle(decodeURIComponent(link setLinkTitle(
.split('/') decodeURIComponent(
.pop() link
.split('.') .split('/')
.slice(0, -1) .pop()
.join('.') .split('.')
.replace('_', ' ') .slice(0, -1)
.replace(/\d{4}\.\d{1,2}\.\d{2}\.\.\d{2}\.\d{2}\.\d{2}-/, '') .join('.')
)); .replace('_', ' ')
.replace(/\d{4}\.\d{1,2}\.\d{2}\.\.\d{2}\.\d{2}\.\d{2}-/, '')
)
);
} }
} }
return link; return link;
@ -113,7 +125,7 @@ const LinkSubmit = (props: LinkSubmitProps) => {
useEffect(onLinkChange, [linkValue]); useEffect(onLinkChange, [linkValue]);
const onKeyPress = (e) => { const onKeyPress = (e: any) => {
if (e.key === 'Enter') { if (e.key === 'Enter') {
e.preventDefault(); e.preventDefault();
doPost(); doPost();
@ -122,60 +134,86 @@ const LinkSubmit = (props: LinkSubmitProps) => {
return ( return (
<> <>
{/* @ts-ignore archaic event type mismatch */} {/* @ts-ignore archaic event type mismatch */}
<Box <Box
flexShrink={0} flexShrink={0}
position='relative' position="relative"
border='1px solid' border="1px solid"
borderColor={submitFocused ? 'black' : 'lightGray'} borderColor={submitFocused ? 'black' : 'lightGray'}
width='100%' width="100%"
borderRadius={2} borderRadius={2}
{...drag.bind} {...drag.bind}
> >
{uploading && <Box {uploading ? (
display="flex" error !== '' ? (
width="100%" <Box
height="100%" display="flex"
position="absolute" flexDirection="column"
left={0} width="100%"
right={0} height="100%"
bg="white" left={0}
zIndex={9} right={0}
alignItems="center" bg="white"
justifyContent="center" zIndex={9}
> alignItems="center"
<LoadingSpinner /> justifyContent="center"
</Box>} py={2}
{drag.dragging && <SubmitDragger />} >
<StatelessUrlInput <Icon icon="ExclaimationMarkBold" size={32} />
value={linkValue} <Text bold>{error}</Text>
promptUpload={promptUpload} <Text>Please check your S3 settings.</Text>
canUpload={canUpload} </Box>
onSubmit={doPost} ) : (
onChange={setLinkValue} <Box
error={linkValid ? 'Invalid URL' : undefined} display="flex"
onKeyPress={onKeyPress} width="100%"
onPaste={onPaste} height="100%"
/> left={0}
<BaseInput right={0}
type="text" bg="white"
pl={2} zIndex={9}
backgroundColor="transparent" alignItems="center"
width="100%" justifyContent="center"
color="black" py={2}
fontSize={1} >
style={{ <LoadingSpinner />
resize: 'none', </Box>
height: 40 )
}} ) : (
placeholder="Provide a title" <>
onChange={e => setLinkTitle(e.target.value)} <StatelessUrlInput
onBlur={() => setSubmitFocused(false)} value={linkValue}
onFocus={() => setSubmitFocused(true)} promptUpload={promptUpload}
spellCheck="false" canUpload={canUpload}
onKeyPress={onKeyPress} onSubmit={doPost}
value={linkTitle} onChange={setLinkValue}
/> error={linkValid ? 'Invalid URL' : undefined}
onKeyPress={onKeyPress}
onPaste={onPaste}
handleError={handleError}
/>
<BaseInput
type="text"
pl={2}
backgroundColor="transparent"
width="100%"
color="black"
fontSize={1}
style={{
resize: 'none',
height: 40
}}
placeholder="Provide a title"
onChange={e => setLinkTitle(e.target.value)}
onBlur={() => setSubmitFocused(false)}
onFocus={() => setSubmitFocused(true)}
spellCheck="false"
onKeyPress={onKeyPress}
value={linkTitle}
/>
</>
)}
{drag.dragging && <SubmitDragger />}
</Box> </Box>
<Box mt={2} mb={4}> <Box mt={2} mb={4}>
<Button <Button
@ -183,7 +221,9 @@ const LinkSubmit = (props: LinkSubmitProps) => {
flexShrink={0} flexShrink={0}
disabled={!linkValid || disabled} disabled={!linkValid || disabled}
onClick={doPost} onClick={doPost}
>Post link</Button> >
Post link
</Button>
</Box> </Box>
</> </>
); );

View File

@ -9,8 +9,9 @@ type StatelessUrlInputProps = PropFunc<typeof BaseInput> & {
focussed?: boolean; focussed?: boolean;
disabled?: boolean; disabled?: boolean;
onChange?: (value: string) => void; onChange?: (value: string) => void;
promptUpload: () => Promise<string>; promptUpload: (onError: (err: Error) => void) => Promise<string>;
canUpload: boolean; canUpload: boolean;
handleError: (err: Error) => void;
center?: boolean; center?: boolean;
}; };
@ -22,6 +23,7 @@ export function StatelessUrlInput(props: StatelessUrlInputProps) {
onChange = () => {}, onChange = () => {},
promptUpload, promptUpload,
canUpload, canUpload,
handleError,
center = false, center = false,
...rest ...rest
} = props; } = props;
@ -53,7 +55,7 @@ export function StatelessUrlInput(props: StatelessUrlInputProps) {
cursor="pointer" cursor="pointer"
color="blue" color="blue"
style={{ pointerEvents: 'all' }} style={{ pointerEvents: 'all' }}
onClick={() => promptUpload().then(onChange)} onClick={() => promptUpload(handleError).then(onChange)}
> >
upload upload
</Text>{' '} </Text>{' '}