mirror of
https://github.com/ilyakooo0/urbit.git
synced 2024-09-20 06:58:16 +03:00
Merge pull request #5682 from urbit/po/fix-silent-s3-failures
groups: fix silent s3 failures
This commit is contained in:
commit
1949174f63
@ -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);
|
||||||
|
@ -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']);
|
||||||
|
@ -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>
|
||||||
|
@ -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>
|
||||||
|
@ -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>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
@ -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>{' '}
|
||||||
|
Loading…
Reference in New Issue
Block a user