groups: handle s3 upload failures in collections

This commit is contained in:
Patrick O'Sullivan 2022-03-30 14:12:13 -05:00
parent f0192cb66c
commit 836cdb2478
4 changed files with 176 additions and 103 deletions

View File

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

View File

@ -1,5 +1,5 @@
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 SubmitDragger from '~/views/components/SubmitDragger';
@ -21,6 +21,7 @@ export function LinkBlockInput(props: LinkBlockInputProps) {
const [url, setUrl] = useState(props.url || '');
const [valid, setValid] = useState(false);
const [focussed, setFocussed] = useState(false);
const [error, setError] = useState<string>('');
const addPost = useGraphState(selGraph);
@ -41,8 +42,13 @@ export function LinkBlockInput(props: LinkBlockInputProps) {
setValid(URLparser.test(val) || Boolean(parsePermalink(val)));
}, []);
const handleError = useCallback((err: Error) => {
setError(err.message);
}, []);
const { uploading, canUpload, promptUpload, drag } = useFileUpload({
onSuccess: handleChange
onSuccess: handleChange,
onError: handleError
});
const doPost = () => {
@ -64,7 +70,7 @@ export function LinkBlockInput(props: LinkBlockInputProps) {
};
const onKeyPress = useCallback(
(e) => {
(e: any) => {
if (e.key === 'Enter') {
e.preventDefault();
doPost();
@ -89,22 +95,42 @@ export function LinkBlockInput(props: LinkBlockInputProps) {
backgroundColor="washedGray"
{...drag.bind}
>
{drag.dragging && <SubmitDragger />}
{drag.dragging && canUpload && <SubmitDragger />}
{uploading ? (
<Box
display="flex"
width="100%"
height="100%"
position="absolute"
left={0}
right={0}
bg="white"
zIndex={9}
alignItems="center"
justifyContent="center"
>
<LoadingSpinner />
</Box>
error != '' ? (
<Box
display="flex"
flexDirection="column"
width="100%"
height="100%"
position="absolute"
left={0}
right={0}
bg="white"
zIndex={9}
alignItems="center"
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
value={url}
@ -114,6 +140,7 @@ export function LinkBlockInput(props: LinkBlockInputProps) {
focussed={focussed}
onBlur={onBlur}
promptUpload={promptUpload}
handleError={handleError}
onKeyPress={onKeyPress}
center
/>
@ -125,7 +152,11 @@ export function LinkBlockInput(props: LinkBlockInputProps) {
p="2"
justifyContent="row-end"
>
<Action onClick={doPost} disabled={!valid} backgroundColor="transparent">
<Action
onClick={doPost}
disabled={!valid}
backgroundColor="transparent"
>
Post
</Action>
</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 React, { useState, useEffect } from 'react';
import React, { useState, useEffect, useCallback } from 'react';
import { parsePermalink, permalinkToReference } from '~/logic/lib/permalinks';
import { StatelessUrlInput } from '~/views/components/StatelessUrlInput';
import SubmitDragger from '~/views/components/SubmitDragger';
@ -22,15 +29,15 @@ const LinkSubmit = (props: LinkSubmitProps) => {
const [linkTitle, setLinkTitle] = useState('');
const [disabled, setDisabled] = useState(false);
const [linkValid, setLinkValid] = useState(false);
const [error, setError] = useState<string>('');
const {
canUpload,
uploading,
promptUpload,
drag,
onPaste
} = useFileUpload({
const handleError = useCallback((err: Error) => {
setError(err.message);
}, []);
const { canUpload, uploading, promptUpload, drag, onPaste } = useFileUpload({
onSuccess: setLinkValue,
onError: handleError,
multiple: false
});
@ -38,25 +45,21 @@ const LinkSubmit = (props: LinkSubmitProps) => {
const url = linkValue;
const text = linkTitle ? linkTitle : linkValue;
const contents = url.startsWith('web+urbitgraph:/')
? [{ text }, permalinkToReference(parsePermalink(url)!)]
: [{ text }, { url }];
? [{ text }, permalinkToReference(parsePermalink(url)!)]
: [{ text }, { url }];
setDisabled(true);
const parentIndex = props.parentIndex || '';
const post = createPost(window.ship, contents, parentIndex);
addPost(
`~${props.ship}`,
props.name,
post
);
addPost(`~${props.ship}`, props.name, post);
setDisabled(false);
setLinkValue('');
setLinkTitle('');
setLinkValid(false);
};
const validateLink = (link) => {
const validateLink = link => {
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}/
);
@ -70,9 +73,9 @@ const LinkSubmit = (props: LinkSubmitProps) => {
setLinkValue(link);
}
}
if(link.startsWith('web+urbitgraph://')) {
if (link.startsWith('web+urbitgraph://')) {
const permalink = parsePermalink(link);
if(!permalink) {
if (!permalink) {
setLinkValid(false);
return;
}
@ -82,21 +85,27 @@ const LinkSubmit = (props: LinkSubmitProps) => {
if (hasProvider(linkValue)) {
fetch(`https://noembed.com/embed?url=${linkValue}`)
.then(response => response.json())
.then((result) => {
.then(result => {
if (result.title && !linkTitle) {
setLinkTitle(result.title);
}
}).catch((error) => { /* noop*/ });
})
.catch(error => {
/* noop*/
});
} else if (!linkTitle) {
setLinkTitle(decodeURIComponent(link
.split('/')
.pop()
.split('.')
.slice(0, -1)
.join('.')
.replace('_', ' ')
.replace(/\d{4}\.\d{1,2}\.\d{2}\.\.\d{2}\.\d{2}\.\d{2}-/, '')
));
setLinkTitle(
decodeURIComponent(
link
.split('/')
.pop()
.split('.')
.slice(0, -1)
.join('.')
.replace('_', ' ')
.replace(/\d{4}\.\d{1,2}\.\d{2}\.\.\d{2}\.\d{2}\.\d{2}-/, '')
)
);
}
}
return link;
@ -113,7 +122,7 @@ const LinkSubmit = (props: LinkSubmitProps) => {
useEffect(onLinkChange, [linkValue]);
const onKeyPress = (e) => {
const onKeyPress = e => {
if (e.key === 'Enter') {
e.preventDefault();
doPost();
@ -122,60 +131,86 @@ const LinkSubmit = (props: LinkSubmitProps) => {
return (
<>
{/* @ts-ignore archaic event type mismatch */}
{/* @ts-ignore archaic event type mismatch */}
<Box
flexShrink={0}
position='relative'
border='1px solid'
position="relative"
border="1px solid"
borderColor={submitFocused ? 'black' : 'lightGray'}
width='100%'
width="100%"
borderRadius={2}
{...drag.bind}
>
{uploading && <Box
display="flex"
width="100%"
height="100%"
position="absolute"
left={0}
right={0}
bg="white"
zIndex={9}
alignItems="center"
justifyContent="center"
>
<LoadingSpinner />
</Box>}
{drag.dragging && <SubmitDragger />}
<StatelessUrlInput
value={linkValue}
promptUpload={promptUpload}
canUpload={canUpload}
onSubmit={doPost}
onChange={setLinkValue}
error={linkValid ? 'Invalid URL' : undefined}
onKeyPress={onKeyPress}
onPaste={onPaste}
/>
<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}
/>
{uploading ? (
error !== '' ? (
<Box
display="flex"
flexDirection="column"
width="100%"
height="100%"
left={0}
right={0}
bg="white"
zIndex={9}
alignItems="center"
justifyContent="center"
py={2}
>
<Icon icon="ExclaimationMarkBold" size={32} />
<Text bold>{error}</Text>
<Text>Please check your S3 settings.</Text>
</Box>
) : (
<Box
display="flex"
width="100%"
height="100%"
left={0}
right={0}
bg="white"
zIndex={9}
alignItems="center"
justifyContent="center"
py={2}
>
<LoadingSpinner />
</Box>
)
) : (
<>
<StatelessUrlInput
value={linkValue}
promptUpload={promptUpload}
canUpload={canUpload}
onSubmit={doPost}
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 mt={2} mb={4}>
<Button
@ -183,7 +218,9 @@ const LinkSubmit = (props: LinkSubmitProps) => {
flexShrink={0}
disabled={!linkValid || disabled}
onClick={doPost}
>Post link</Button>
>
Post link
</Button>
</Box>
</>
);

View File

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