mirror of
https://github.com/ilyakooo0/urbit.git
synced 2024-12-12 15:01:38 +03:00
Merge pull request #4032 from tylershuster/upload-fixes
s3: upload behavior
This commit is contained in:
commit
5ffcd57b83
@ -1,15 +1,39 @@
|
||||
import { useState, useCallback, useMemo, useEffect } from "react";
|
||||
|
||||
function validateDragEvent(e: DragEvent): FileList | null {
|
||||
const files = e.dataTransfer?.files;
|
||||
console.log(files);
|
||||
if(!files?.length) {
|
||||
return null;
|
||||
function validateDragEvent(e: DragEvent): FileList | File[] | true | null {
|
||||
const files: File[] = [];
|
||||
let valid = false;
|
||||
if (e.dataTransfer?.files) {
|
||||
Array.from(e.dataTransfer.files).forEach(f => files.push(f));
|
||||
}
|
||||
return files || null;
|
||||
if (e.dataTransfer?.items) {
|
||||
Array.from(e.dataTransfer.items || [])
|
||||
.filter((i) => i.kind === 'file')
|
||||
.forEach(f => {
|
||||
valid = true; // Valid if file exists, but on DragOver, won't reveal its contents for security
|
||||
const data = f.getAsFile();
|
||||
if (data) {
|
||||
files.push(data);
|
||||
}
|
||||
});
|
||||
}
|
||||
if (files.length) {
|
||||
return [...new Set(files)];
|
||||
}
|
||||
if (navigator.userAgent.includes('Safari')) {
|
||||
if (e.dataTransfer?.effectAllowed === 'all') {
|
||||
valid = true;
|
||||
} else if (e.dataTransfer?.files.length) {
|
||||
return e.dataTransfer.files;
|
||||
}
|
||||
}
|
||||
if (valid) {
|
||||
return true;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
export function useFileDrag(dragged: (f: FileList) => void) {
|
||||
export function useFileDrag(dragged: (f: FileList | File[], e: DragEvent) => void) {
|
||||
const [dragging, setDragging] = useState(false);
|
||||
|
||||
const onDragEnter = useCallback(
|
||||
@ -25,18 +49,21 @@ export function useFileDrag(dragged: (f: FileList) => void) {
|
||||
const onDrop = useCallback(
|
||||
(e: DragEvent) => {
|
||||
setDragging(false);
|
||||
e.preventDefault();
|
||||
const files = validateDragEvent(e);
|
||||
if (!files) {
|
||||
if (!files || files === true) {
|
||||
return;
|
||||
}
|
||||
dragged(files);
|
||||
e.preventDefault();
|
||||
dragged(files, e);
|
||||
},
|
||||
[setDragging, dragged]
|
||||
);
|
||||
|
||||
const onDragOver = useCallback(
|
||||
(e: DragEvent) => {
|
||||
if (!validateDragEvent(e)) {
|
||||
return;
|
||||
}
|
||||
e.preventDefault();
|
||||
setDragging(true);
|
||||
},
|
||||
@ -53,6 +80,18 @@ export function useFileDrag(dragged: (f: FileList) => void) {
|
||||
[setDragging]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
const mouseleave = (e) => {
|
||||
if (!e.relatedTarget && !e.toElement) {
|
||||
setDragging(false);
|
||||
}
|
||||
};
|
||||
document.body.addEventListener('mouseout', mouseleave);
|
||||
return () => {
|
||||
document.body.removeEventListener('mouseout', mouseleave);
|
||||
}
|
||||
}, []);
|
||||
|
||||
const bind = {
|
||||
onDragLeave,
|
||||
onDragOver,
|
||||
|
@ -1,51 +1,96 @@
|
||||
import { useCallback, useMemo, useEffect, useRef } from "react";
|
||||
import { useCallback, useMemo, useEffect, useRef, useState } from "react";
|
||||
import { S3State } from "../../types/s3-update";
|
||||
import S3 from "aws-sdk/clients/s3";
|
||||
import { dateToDa, deSig } from "./util";
|
||||
|
||||
export function useS3(s3: S3State) {
|
||||
const { configuration, credentials } = s3;
|
||||
export interface IuseS3 {
|
||||
canUpload: boolean;
|
||||
upload: (file: File, bucket: string) => Promise<string>;
|
||||
uploadDefault: (file: File) => Promise<string>;
|
||||
uploading: boolean;
|
||||
promptUpload: () => Promise<string | undefined>;
|
||||
}
|
||||
|
||||
const useS3 = (s3: S3State, { accept = '*' } = { accept: '*' }): IuseS3 => {
|
||||
const [uploading, setUploading] = useState(false);
|
||||
|
||||
const client = useRef<S3 | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (!credentials) {
|
||||
if (!s3.credentials) {
|
||||
return;
|
||||
}
|
||||
client.current = new S3({ credentials, endpoint: credentials.endpoint });
|
||||
}, [credentials]);
|
||||
client.current = new S3({
|
||||
credentials: s3.credentials,
|
||||
endpoint: s3.credentials.endpoint
|
||||
});
|
||||
}, [s3.credentials]);
|
||||
|
||||
const canUpload = useMemo(
|
||||
() =>
|
||||
(client && credentials && configuration.currentBucket !== "") || false,
|
||||
[credentials, configuration.currentBucket, client]
|
||||
(client && s3.credentials && s3.configuration.currentBucket !== "") || false,
|
||||
[s3.credentials, s3.configuration.currentBucket, client]
|
||||
);
|
||||
|
||||
const uploadDefault = useCallback(async (file: File) => {
|
||||
if (configuration.currentBucket === "") {
|
||||
throw new Error("current bucket not set");
|
||||
}
|
||||
return upload(file, configuration.currentBucket);
|
||||
}, []);
|
||||
|
||||
const upload = useCallback(
|
||||
async (file: File, bucket: string) => {
|
||||
if (!client.current) {
|
||||
throw new Error("S3 not ready");
|
||||
}
|
||||
|
||||
const fileParts = file.name.split('.');
|
||||
const fileName = fileParts.slice(0, -1);
|
||||
const fileExtension = fileParts.pop();
|
||||
const timestamp = deSig(dateToDa(new Date()));
|
||||
|
||||
const params = {
|
||||
Bucket: bucket,
|
||||
Key: file.name,
|
||||
Key: `${window.ship}/${timestamp}-${fileName}.${fileExtension}`,
|
||||
Body: file,
|
||||
ACL: "public-read",
|
||||
ContentType: file.type,
|
||||
};
|
||||
|
||||
setUploading(true);
|
||||
|
||||
const { Location } = await client.current.upload(params).promise();
|
||||
|
||||
setUploading(false);
|
||||
|
||||
return Location;
|
||||
},
|
||||
[client]
|
||||
[client, setUploading]
|
||||
);
|
||||
|
||||
return { canUpload, upload, uploadDefault };
|
||||
}
|
||||
const uploadDefault = useCallback(async (file: File) => {
|
||||
if (s3.configuration.currentBucket === "") {
|
||||
throw new Error("current bucket not set");
|
||||
}
|
||||
return upload(file, s3.configuration.currentBucket);
|
||||
}, [s3]);
|
||||
|
||||
const promptUpload = useCallback(
|
||||
() => {
|
||||
return new Promise((resolve, reject) => {
|
||||
const fileSelector = document.createElement('input');
|
||||
fileSelector.setAttribute('type', 'file');
|
||||
fileSelector.setAttribute('accept', accept);
|
||||
fileSelector.addEventListener('change', () => {
|
||||
const files = fileSelector.files;
|
||||
if (!files || files.length <= 0) {
|
||||
reject();
|
||||
return;
|
||||
}
|
||||
uploadDefault(files[0]).then(resolve);
|
||||
})
|
||||
fileSelector.click();
|
||||
})
|
||||
|
||||
},
|
||||
[uploadDefault]
|
||||
);
|
||||
|
||||
return { canUpload, upload, uploadDefault, uploading, promptUpload };
|
||||
};
|
||||
|
||||
export default useS3;
|
@ -353,4 +353,4 @@ export function usePreventWindowUnload(shouldPreventDefault: boolean, message =
|
||||
|
||||
export function pluralize(text: string, isPlural = false, vowel = false) {
|
||||
return isPlural ? `${text}s`: `${vowel ? 'an' : 'a'} ${text}`;
|
||||
}
|
||||
}
|
@ -9,8 +9,9 @@ import { useFileDrag } from '~/logic/lib/useDrag';
|
||||
import ChatWindow from './components/ChatWindow';
|
||||
import ChatInput from './components/ChatInput';
|
||||
import GlobalApi from '~/logic/api/global';
|
||||
import { SubmitDragger } from '~/views/components/s3-upload';
|
||||
import SubmitDragger from '~/views/components/SubmitDragger';
|
||||
import { useLocalStorageState } from '~/logic/lib/useLocalStorageState';
|
||||
import useS3 from '~/logic/lib/useS3';
|
||||
|
||||
type ChatResourceProps = StoreState & {
|
||||
association: Association;
|
||||
@ -65,20 +66,6 @@ export function ChatResource(props: ChatResourceProps) {
|
||||
const ourContact = contacts?.[window.ship];
|
||||
const lastMsgNum = envelopes.length || 0;
|
||||
|
||||
const chatInput = useRef<ChatInput>();
|
||||
|
||||
const onFileDrag = useCallback(
|
||||
(files: FileList) => {
|
||||
if (!chatInput.current) {
|
||||
return;
|
||||
}
|
||||
chatInput.current?.uploadFiles(files);
|
||||
},
|
||||
[chatInput?.current]
|
||||
);
|
||||
|
||||
const { bind, dragging } = useFileDrag(onFileDrag);
|
||||
|
||||
const [unsent, setUnsent] = useLocalStorageState<Record<string, string>>(
|
||||
'chat-unsent',
|
||||
{}
|
||||
@ -89,13 +76,29 @@ export function ChatResource(props: ChatResourceProps) {
|
||||
[station]
|
||||
);
|
||||
|
||||
const clearUnsent = useCallback(() => setUnsent(s => _.omit(s, station)), [
|
||||
station
|
||||
]);
|
||||
const clearUnsent = useCallback(
|
||||
() => setUnsent(s => _.omit(s, station)),
|
||||
[station]
|
||||
);
|
||||
|
||||
const chatInput = useRef<ChatInput>();
|
||||
|
||||
const onFileDrag = useCallback(
|
||||
(files: FileList | File[]) => {
|
||||
if (!chatInput.current) {
|
||||
return;
|
||||
}
|
||||
chatInput.current.uploadFiles(files);
|
||||
},
|
||||
[chatInput.current]
|
||||
);
|
||||
|
||||
const { bind, dragging } = useFileDrag(onFileDrag);
|
||||
|
||||
const scrollTo = new URLSearchParams(location.search).get('msg');
|
||||
|
||||
useEffect(() => {
|
||||
const clear = () => {
|
||||
const clear = () => {
|
||||
props.history.replace(location.pathname);
|
||||
};
|
||||
setTimeout(clear, 10000);
|
||||
|
@ -1,15 +1,16 @@
|
||||
import React, { Component } from 'react';
|
||||
import ChatEditor from './chat-editor';
|
||||
import { S3Upload } from '~/views/components/s3-upload' ;
|
||||
import { IuseS3 } from '~/logic/lib/useS3';
|
||||
import { uxToHex } from '~/logic/lib/util';
|
||||
import { Sigil } from '~/logic/lib/sigil';
|
||||
import tokenizeMessage, { isUrl } from '~/logic/lib/tokenizeMessage';
|
||||
import GlobalApi from '~/logic/api/global';
|
||||
import { Envelope } from '~/types/chat-update';
|
||||
import { Contacts } from '~/types';
|
||||
import { Row, BaseImage, Box, Icon } from '@tlon/indigo-react';
|
||||
import { Row, BaseImage, Box, Icon, LoadingSpinner } from '@tlon/indigo-react';
|
||||
import withS3 from '~/views/components/withS3';
|
||||
|
||||
interface ChatInputProps {
|
||||
type ChatInputProps = IuseS3 & {
|
||||
api: GlobalApi;
|
||||
numMsgs: number;
|
||||
station: any;
|
||||
@ -22,7 +23,6 @@ interface ChatInputProps {
|
||||
message: string;
|
||||
deleteMessage(): void;
|
||||
hideAvatars: boolean;
|
||||
onPaste?(): void;
|
||||
}
|
||||
|
||||
interface ChatInputState {
|
||||
@ -31,8 +31,7 @@ interface ChatInputState {
|
||||
uploadingPaste: boolean;
|
||||
}
|
||||
|
||||
export default class ChatInput extends Component<ChatInputProps, ChatInputState> {
|
||||
public s3Uploader: React.RefObject<S3Upload>;
|
||||
class ChatInput extends Component<ChatInputProps, ChatInputState> {
|
||||
private chatEditor: React.RefObject<ChatEditor>;
|
||||
|
||||
constructor(props) {
|
||||
@ -44,11 +43,12 @@ export default class ChatInput extends Component<ChatInputProps, ChatInputState>
|
||||
uploadingPaste: false
|
||||
};
|
||||
|
||||
this.s3Uploader = React.createRef();
|
||||
this.chatEditor = React.createRef();
|
||||
|
||||
this.submit = this.submit.bind(this);
|
||||
this.toggleCode = this.toggleCode.bind(this);
|
||||
this.uploadSuccess = this.uploadSuccess.bind(this);
|
||||
this.uploadError = this.uploadError.bind(this);
|
||||
}
|
||||
|
||||
toggleCode() {
|
||||
@ -136,10 +136,6 @@ export default class ChatInput extends Component<ChatInputProps, ChatInputState>
|
||||
// no-op for now
|
||||
}
|
||||
|
||||
readyToUpload(): boolean {
|
||||
return Boolean(this.s3Uploader.current?.inputRef.current);
|
||||
}
|
||||
|
||||
onPaste(codemirrorInstance, event: ClipboardEvent) {
|
||||
if (!event.clipboardData || !event.clipboardData.files.length) {
|
||||
return;
|
||||
@ -150,16 +146,15 @@ export default class ChatInput extends Component<ChatInputProps, ChatInputState>
|
||||
this.uploadFiles(event.clipboardData.files);
|
||||
}
|
||||
|
||||
uploadFiles(files: FileList) {
|
||||
if (!this.readyToUpload()) {
|
||||
uploadFiles(files: FileList | File[]) {
|
||||
if (!this.props.canUpload) {
|
||||
return;
|
||||
}
|
||||
if (!this.s3Uploader.current || !this.s3Uploader.current.inputRef.current)
|
||||
return;
|
||||
this.s3Uploader.current.inputRef.current.files = files;
|
||||
const fire = document.createEvent('HTMLEvents');
|
||||
fire.initEvent('change', true, true);
|
||||
this.s3Uploader.current?.inputRef.current?.dispatchEvent(fire);
|
||||
Array.from(files).forEach(file => {
|
||||
this.props.uploadDefault(file)
|
||||
.then(this.uploadSuccess)
|
||||
.catch(this.uploadError);
|
||||
});
|
||||
}
|
||||
|
||||
render() {
|
||||
@ -189,13 +184,13 @@ return;
|
||||
<Row
|
||||
alignItems='center'
|
||||
position='relative'
|
||||
flexGrow='1'
|
||||
flexShrink='0'
|
||||
borderTop='1'
|
||||
flexGrow={1}
|
||||
flexShrink={0}
|
||||
borderTop={1}
|
||||
borderTopColor='washedGray'
|
||||
backgroundColor='white'
|
||||
className='cf'
|
||||
zIndex='0'
|
||||
zIndex={0}
|
||||
>
|
||||
<Row p='2' alignItems='center'>
|
||||
{avatar}
|
||||
@ -210,29 +205,26 @@ return;
|
||||
placeholder='Message...'
|
||||
/>
|
||||
<Box
|
||||
mx='2'
|
||||
flexShrink='0'
|
||||
mx={2}
|
||||
flexShrink={0}
|
||||
height='16px'
|
||||
width='16px'
|
||||
flexBasis='16px'
|
||||
>
|
||||
<S3Upload
|
||||
ref={this.s3Uploader}
|
||||
configuration={props.s3.configuration}
|
||||
credentials={props.s3.credentials}
|
||||
uploadSuccess={this.uploadSuccess.bind(this)}
|
||||
uploadError={this.uploadError.bind(this)}
|
||||
accept="*"
|
||||
>
|
||||
<Icon icon='Links'
|
||||
width="16"
|
||||
height="16"
|
||||
/>
|
||||
</S3Upload>
|
||||
{this.props.canUpload
|
||||
? this.props.uploading
|
||||
? <LoadingSpinner />
|
||||
: <Icon icon='Links'
|
||||
width="16"
|
||||
height="16"
|
||||
onClick={() => this.props.promptUpload().then(this.uploadSuccess)}
|
||||
/>
|
||||
: null
|
||||
}
|
||||
</Box>
|
||||
<Box
|
||||
mr='2'
|
||||
flexShrink='0'
|
||||
mr={2}
|
||||
flexShrink={0}
|
||||
height='16px'
|
||||
width='16px'
|
||||
flexBasis='16px'
|
||||
@ -247,3 +239,5 @@ return;
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default withS3(ChatInput, {accept: 'image/*'});
|
@ -9,7 +9,7 @@ import { uxToHex } from '~/logic/lib/util';
|
||||
import { RouteComponentProps } from "react-router-dom";
|
||||
|
||||
import { LinkItem } from "./components/LinkItem";
|
||||
import { LinkSubmit } from "./components/link-submit";
|
||||
import LinkSubmit from "./components/LinkSubmit";
|
||||
import { LinkPreview } from "./components/link-preview";
|
||||
import { Comments } from "~/views/components/comments";
|
||||
|
||||
|
228
pkg/interface/src/views/apps/links/components/LinkSubmit.tsx
Normal file
228
pkg/interface/src/views/apps/links/components/LinkSubmit.tsx
Normal file
@ -0,0 +1,228 @@
|
||||
import { BaseInput, Box, Button, LoadingSpinner, Text } from "@tlon/indigo-react";
|
||||
import React, { useCallback, useState } from "react";
|
||||
import GlobalApi from "~/logic/api/global";
|
||||
import { useFileDrag } from "~/logic/lib/useDrag";
|
||||
import useS3 from "~/logic/lib/useS3";
|
||||
import { S3State } from "~/types";
|
||||
import SubmitDragger from "~/views/components/SubmitDragger";
|
||||
import { createPost } from "~/logic/api/graph";
|
||||
import { hasProvider } from "oembed-parser";
|
||||
|
||||
interface LinkSubmitProps {
|
||||
api: GlobalApi;
|
||||
s3: S3State;
|
||||
name: string;
|
||||
ship: string;
|
||||
};
|
||||
|
||||
const LinkSubmit = (props: LinkSubmitProps) => {
|
||||
let { canUpload, uploadDefault, uploading, promptUpload } = useS3(props.s3);
|
||||
|
||||
const [submitFocused, setSubmitFocused] = useState(false);
|
||||
const [urlFocused, setUrlFocused] = useState(false);
|
||||
const [linkValue, setLinkValueHook] = useState('');
|
||||
const [linkTitle, setLinkTitle] = useState('');
|
||||
const [disabled, setDisabled] = useState(false);
|
||||
const [linkValid, setLinkValid] = useState(false);
|
||||
|
||||
const doPost = () => {
|
||||
const url = linkValue;
|
||||
const text = linkTitle ? linkTitle : linkValue;
|
||||
setDisabled(true);
|
||||
const parentIndex = props.parentIndex || '';
|
||||
const post = createPost([
|
||||
{ text },
|
||||
{ url }
|
||||
], parentIndex);
|
||||
|
||||
props.api.graph.addPost(
|
||||
`~${props.ship}`,
|
||||
props.name,
|
||||
post
|
||||
).then(() => {
|
||||
setDisabled(false);
|
||||
setLinkValue('');
|
||||
setLinkTitle('');
|
||||
setLinkValid(false);
|
||||
});
|
||||
};
|
||||
|
||||
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}/
|
||||
);
|
||||
|
||||
let linkValid = URLparser.test(link);
|
||||
|
||||
if (!linkValid) {
|
||||
linkValid = URLparser.test(`http://${link}`);
|
||||
if (linkValid) {
|
||||
link = `http://${link}`;
|
||||
setLinkValue(link);
|
||||
}
|
||||
}
|
||||
|
||||
if (linkValid) {
|
||||
if (hasProvider(linkValue)) {
|
||||
fetch(`https://noembed.com/embed?url=${linkValue}`)
|
||||
.then(response => response.json())
|
||||
.then((result) => {
|
||||
if (result.title && !linkTitle) {
|
||||
setLinkTitle(result.title);
|
||||
}
|
||||
}).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}-/, '')
|
||||
));
|
||||
}
|
||||
}
|
||||
return link;
|
||||
};
|
||||
|
||||
const onFileDrag = useCallback(
|
||||
(files: FileList | File[], e: DragEvent): void => {
|
||||
if (!canUpload) {
|
||||
return;
|
||||
}
|
||||
uploadDefault(files[0]).then(setLinkValue);
|
||||
},
|
||||
[uploadDefault, canUpload]
|
||||
);
|
||||
|
||||
const { bind, dragging } = useFileDrag(onFileDrag);
|
||||
|
||||
const onLinkChange = (linkValue: string) => {
|
||||
setLinkValueHook(linkValue);
|
||||
const link = validateLink(linkValue)
|
||||
setLinkValid(link);
|
||||
};
|
||||
|
||||
const setLinkValue = (linkValue: string) => {
|
||||
onLinkChange(linkValue);
|
||||
setLinkValueHook(linkValue);
|
||||
};
|
||||
|
||||
const onPaste = useCallback(
|
||||
(event: ClipboardEvent) => {
|
||||
if (!event.clipboardData || !event.clipboardData.files.length) {
|
||||
return;
|
||||
}
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
uploadDefault(event.clipboardData.files[0]).then(setLinkValue);
|
||||
}, [setLinkValue, uploadDefault]
|
||||
);
|
||||
|
||||
const onKeyPress = (e) => {
|
||||
if (e.key === 'Enter') {
|
||||
e.preventDefault();
|
||||
doPost();
|
||||
}
|
||||
};
|
||||
|
||||
const placeholder = <Text
|
||||
gray
|
||||
position="absolute"
|
||||
px={2}
|
||||
pt={2}
|
||||
fontSize={0}
|
||||
style={{ pointerEvents: 'none' }}
|
||||
>{canUpload
|
||||
? <>
|
||||
Drop or{' '}
|
||||
<Text
|
||||
cursor='pointer'
|
||||
color='blue'
|
||||
style={{ pointerEvents: 'all' }}
|
||||
onClick={() => promptUpload().then(setLinkValue)}
|
||||
>upload</Text>
|
||||
{' '}a file, or paste a link here
|
||||
</>
|
||||
: 'Paste a link here'
|
||||
}</Text>;
|
||||
|
||||
return (
|
||||
<>
|
||||
<Box
|
||||
flexShrink={0}
|
||||
position='relative'
|
||||
border='1px solid'
|
||||
borderColor={submitFocused ? 'black' : 'washedGray'}
|
||||
width='100%'
|
||||
borderRadius={2}
|
||||
{...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>}
|
||||
{dragging && <SubmitDragger />}
|
||||
<Box position='relative'>
|
||||
{!(linkValue || urlFocused || disabled) && placeholder}
|
||||
<BaseInput
|
||||
type="url"
|
||||
pl={2}
|
||||
width="100%"
|
||||
fontSize={0}
|
||||
py={2}
|
||||
color="black"
|
||||
backgroundColor="transparent"
|
||||
onChange={e => onLinkChange(e.target.value)}
|
||||
onBlur={() => [setUrlFocused(false), setSubmitFocused(false)]}
|
||||
onFocus={() => [setUrlFocused(true), setSubmitFocused(true)]}
|
||||
spellCheck="false"
|
||||
onPaste={onPaste}
|
||||
onKeyPress={onKeyPress}
|
||||
value={linkValue}
|
||||
/>
|
||||
</Box>
|
||||
<BaseInput
|
||||
type="text"
|
||||
pl={2}
|
||||
backgroundColor="transparent"
|
||||
width="100%"
|
||||
fontSize={0}
|
||||
color="black"
|
||||
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}
|
||||
/>
|
||||
</Box>
|
||||
<Box mt={2} mb={4}>
|
||||
<Button
|
||||
primary
|
||||
flexShrink={0}
|
||||
disabled={!linkValid || disabled}
|
||||
onClick={doPost}
|
||||
>Post link</Button>
|
||||
</Box>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default LinkSubmit;
|
@ -1,310 +0,0 @@
|
||||
import React, { Component } from 'react';
|
||||
import { hasProvider } from 'oembed-parser';
|
||||
|
||||
import { S3Upload, SubmitDragger } from '~/views/components/s3-upload';
|
||||
import { Box, Text, BaseInput, Button } from '@tlon/indigo-react';
|
||||
import GlobalApi from '~/logic/api/global';
|
||||
import { S3State } from '~/types';
|
||||
|
||||
import { createPost } from '~/logic/api/graph';
|
||||
|
||||
interface LinkSubmitProps {
|
||||
api: GlobalApi;
|
||||
s3: S3State;
|
||||
name: string;
|
||||
ship: string;
|
||||
}
|
||||
|
||||
interface LinkSubmitState {
|
||||
linkValue: string;
|
||||
linkTitle: string;
|
||||
linkValid: boolean;
|
||||
submitFocus: boolean;
|
||||
urlFocus: boolean;
|
||||
disabled: boolean;
|
||||
dragover: boolean;
|
||||
}
|
||||
|
||||
export class LinkSubmit extends Component<LinkSubmitProps, LinkSubmitState> {
|
||||
private s3Uploader: React.RefObject<S3Upload>;
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
linkValue: '',
|
||||
linkTitle: '',
|
||||
linkValid: false,
|
||||
submitFocus: false,
|
||||
urlFocus: false,
|
||||
disabled: false,
|
||||
dragover: false
|
||||
};
|
||||
this.setLinkValue = this.setLinkValue.bind(this);
|
||||
this.setLinkTitle = this.setLinkTitle.bind(this);
|
||||
this.onDragEnter = this.onDragEnter.bind(this);
|
||||
this.onDrop = this.onDrop.bind(this);
|
||||
this.onPaste = this.onPaste.bind(this);
|
||||
this.uploadFiles = this.uploadFiles.bind(this);
|
||||
this.s3Uploader = React.createRef();
|
||||
}
|
||||
|
||||
onClickPost() {
|
||||
const link = this.state.linkValue;
|
||||
const title = this.state.linkTitle
|
||||
? this.state.linkTitle
|
||||
: this.state.linkValue;
|
||||
this.setState({ disabled: true });
|
||||
|
||||
const parentIndex = this.props.parentIndex || '';
|
||||
const post = createPost([
|
||||
{ text: title },
|
||||
{ url: link }
|
||||
], parentIndex);
|
||||
|
||||
this.props.api.graph.addPost(
|
||||
`~${this.props.ship}`,
|
||||
this.props.name,
|
||||
post
|
||||
).then((r) => {
|
||||
this.setState({
|
||||
disabled: false,
|
||||
linkValue: '',
|
||||
linkTitle: '',
|
||||
linkValid: false
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
setLinkValid(linkValue) {
|
||||
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}/
|
||||
);
|
||||
|
||||
let linkValid = URLparser.test(linkValue);
|
||||
|
||||
if (!linkValid) {
|
||||
linkValid = URLparser.test(`http://${linkValue}`);
|
||||
if (linkValid) {
|
||||
linkValue = `http://${linkValue}`;
|
||||
}
|
||||
}
|
||||
|
||||
this.setState({ linkValid, linkValue });
|
||||
|
||||
if (linkValid) {
|
||||
if (hasProvider(linkValue)) {
|
||||
fetch(`https://noembed.com/embed?url=${linkValue}`)
|
||||
.then(response => response.json())
|
||||
.then((result) => {
|
||||
if (result.title && !this.state.linkTitle) {
|
||||
this.setState({ linkTitle: result.title });
|
||||
}
|
||||
}).catch((error) => { /* noop*/ });
|
||||
} else if (!this.state.linkTitle) {
|
||||
this.setState({
|
||||
linkTitle: decodeURIComponent(linkValue
|
||||
.split('/')
|
||||
.pop()
|
||||
.split('.')
|
||||
.slice(0, -1)
|
||||
.join('.')
|
||||
.replace('_', ' ')
|
||||
.replace(/\d{4}\.\d{1,2}\.\d{2}\.\.\d{2}\.\d{2}\.\d{2}-/, '')
|
||||
)
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
setLinkValue(event) {
|
||||
this.setState({ linkValue: event.target.value });
|
||||
this.setLinkValid(event.target.value);
|
||||
}
|
||||
|
||||
setLinkTitle(event) {
|
||||
this.setState({ linkTitle: event.target.value });
|
||||
}
|
||||
|
||||
uploadSuccess(url) {
|
||||
this.setState({ linkValue: url });
|
||||
this.setLinkValid(url);
|
||||
}
|
||||
|
||||
uploadError(error) {
|
||||
// no-op for now
|
||||
}
|
||||
|
||||
readyToUpload(): boolean {
|
||||
return Boolean(this.s3Uploader.current && this.s3Uploader.current.inputRef.current);
|
||||
}
|
||||
|
||||
onDragEnter() {
|
||||
if (!this.readyToUpload()) {
|
||||
return;
|
||||
}
|
||||
this.setState({ dragover: true });
|
||||
}
|
||||
|
||||
onDrop(event: DragEvent) {
|
||||
this.setState({ dragover: false });
|
||||
if (!event.dataTransfer || !event.dataTransfer.files.length) {
|
||||
return;
|
||||
}
|
||||
event.preventDefault();
|
||||
this.uploadFiles(event.dataTransfer.files);
|
||||
}
|
||||
|
||||
onPaste(event: ClipboardEvent) {
|
||||
if (!event.clipboardData || !event.clipboardData.files.length) {
|
||||
return;
|
||||
}
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
this.uploadFiles(event.clipboardData.files);
|
||||
}
|
||||
|
||||
uploadFiles(files: FileList) {
|
||||
if (!this.readyToUpload()) {
|
||||
return;
|
||||
}
|
||||
this.s3Uploader.current.inputRef.current.files = files;
|
||||
const fire = document.createEvent('HTMLEvents');
|
||||
fire.initEvent('change', true, true);
|
||||
this.s3Uploader.current?.inputRef.current?.dispatchEvent(fire);
|
||||
}
|
||||
|
||||
render() {
|
||||
const isS3Ready =
|
||||
( this.props.s3.credentials.secretAccessKey &&
|
||||
this.props.s3.credentials.endpoint &&
|
||||
this.props.s3.credentials.accessKeyId
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Box
|
||||
flexShrink='0'
|
||||
position='relative'
|
||||
border='1px solid'
|
||||
borderColor={this.state.submitFocus ? 'black' : 'washedGray'}
|
||||
width='100%'
|
||||
borderRadius='2'
|
||||
onDragEnter={this.onDragEnter.bind(this)}
|
||||
onDragOver={(e) => {
|
||||
e.preventDefault();
|
||||
if (isS3Ready) {
|
||||
this.setState({ dragover: true });
|
||||
}
|
||||
}}
|
||||
onDragLeave={() => this.setState({ dragover: false })}
|
||||
onDrop={this.onDrop}
|
||||
>
|
||||
{this.state.dragover ? <SubmitDragger /> : null}
|
||||
<Box position='relative'>
|
||||
{
|
||||
( this.state.linkValue ||
|
||||
this.state.urlFocus ||
|
||||
this.state.disabled
|
||||
) ? null : (
|
||||
isS3Ready ? (
|
||||
<Text gray position='absolute'
|
||||
pl='2' pt='2'
|
||||
pb='2' fontSize='0'
|
||||
style={{ pointerEvents: 'none' }}
|
||||
>
|
||||
Drop or
|
||||
<Text cursor='pointer' color='blue'
|
||||
style={{ pointerEvents: 'all' }}
|
||||
onClick={(event) => {
|
||||
if (!this.readyToUpload()) {
|
||||
return;
|
||||
}
|
||||
this.s3Uploader.current.inputRef.current.click();
|
||||
}}
|
||||
> upload </Text>
|
||||
a file, or paste a link here
|
||||
</Text>
|
||||
) : (
|
||||
<Text gray position='absolute'
|
||||
pl='2' pt='2'
|
||||
pb='2' fontSize='0'
|
||||
style={{ pointerEvents: 'none' }}
|
||||
>
|
||||
Paste a link here
|
||||
</Text>
|
||||
)
|
||||
)
|
||||
}
|
||||
{!this.state.disabled && isS3Ready ? <S3Upload
|
||||
ref={this.s3Uploader}
|
||||
configuration={this.props.s3.configuration}
|
||||
credentials={this.props.s3.credentials}
|
||||
uploadSuccess={this.uploadSuccess.bind(this)}
|
||||
uploadError={this.uploadError.bind(this)}
|
||||
className="dn absolute pt3 pb2 pl2 w-100"
|
||||
></S3Upload> : null}
|
||||
<BaseInput
|
||||
type="url"
|
||||
pl='2'
|
||||
width='100%'
|
||||
fontSize='0'
|
||||
pt='2'
|
||||
pb='2'
|
||||
color='black'
|
||||
backgroundColor='transparent'
|
||||
onChange={this.setLinkValue}
|
||||
onBlur={() => this.setState({ submitFocus: false, urlFocus: false })}
|
||||
onFocus={() => this.setState({ submitFocus: true, urlFocus: true })}
|
||||
spellCheck="false"
|
||||
onPaste={this.onPaste}
|
||||
onKeyPress={(e) => {
|
||||
if (e.key === 'Enter') {
|
||||
e.preventDefault();
|
||||
this.onClickPost();
|
||||
}
|
||||
}}
|
||||
value={this.state.linkValue}
|
||||
/>
|
||||
</Box>
|
||||
<BaseInput
|
||||
pl='2'
|
||||
backgroundColor='transparent'
|
||||
width='100%'
|
||||
fontSize='0'
|
||||
color='black'
|
||||
type="text"
|
||||
className="linkTitle"
|
||||
style={{
|
||||
resize: 'none',
|
||||
height: 40
|
||||
}}
|
||||
placeholder="Provide a title"
|
||||
onChange={this.setLinkTitle}
|
||||
onBlur={() => this.setState({ submitFocus: false })}
|
||||
onFocus={() => this.setState({ submitFocus: true })}
|
||||
spellCheck="false"
|
||||
onKeyPress={(e) => {
|
||||
if (e.key === 'Enter') {
|
||||
e.preventDefault();
|
||||
this.onClickPost();
|
||||
}
|
||||
}}
|
||||
value={this.state.linkTitle}
|
||||
/>
|
||||
</Box>
|
||||
<Box mt='2' mb='4'>
|
||||
<Button
|
||||
flexShrink='0'
|
||||
primary
|
||||
disabled={!this.state.linkValid || this.state.disabled}
|
||||
onClick={this.onClickPost.bind(this)}
|
||||
>
|
||||
Post link
|
||||
</Button>
|
||||
</Box>
|
||||
</>
|
||||
) ;
|
||||
}
|
||||
}
|
||||
|
||||
export default LinkSubmit;
|
@ -39,6 +39,7 @@ export function PublishResource(props: PublishResourceProps) {
|
||||
hideNicknames={props.hideNicknames}
|
||||
remoteContentPolicy={props.remoteContentPolicy}
|
||||
graphs={props.graphs}
|
||||
s3={props.s3}
|
||||
/>
|
||||
</Box>
|
||||
);
|
||||
|
@ -4,7 +4,7 @@ import { PostFormSchema, PostForm } from "./NoteForm";
|
||||
import { FormikHelpers } from "formik";
|
||||
import GlobalApi from "~/logic/api/global";
|
||||
import { RouteComponentProps, useLocation } from "react-router-dom";
|
||||
import { GraphNode, TextContent, Association } from "~/types";
|
||||
import { GraphNode, TextContent, Association, S3State } from "~/types";
|
||||
import { getLatestRevision, editPost } from "~/logic/lib/publish";
|
||||
import {useWaitForProps} from "~/logic/lib/useWaitForProps";
|
||||
interface EditPostProps {
|
||||
@ -13,10 +13,11 @@ interface EditPostProps {
|
||||
note: GraphNode;
|
||||
api: GlobalApi;
|
||||
book: string;
|
||||
s3: S3State;
|
||||
}
|
||||
|
||||
export function EditPost(props: EditPostProps & RouteComponentProps) {
|
||||
const { note, book, noteId, api, ship, history } = props;
|
||||
const { note, book, noteId, api, ship, history, s3 } = props;
|
||||
const [revNum, title, body] = getLatestRevision(note);
|
||||
const location = useLocation();
|
||||
|
||||
@ -53,6 +54,7 @@ export function EditPost(props: EditPostProps & RouteComponentProps) {
|
||||
cancel
|
||||
history={history}
|
||||
onSubmit={onSubmit}
|
||||
s3={s3}
|
||||
submitLabel="Update"
|
||||
loadingText="Updating..."
|
||||
/>
|
||||
|
@ -1,7 +1,8 @@
|
||||
import React, { useCallback } from "react";
|
||||
import { UnControlled as CodeEditor } from "react-codemirror2";
|
||||
import React, { createRef, useCallback, useRef } from "react";
|
||||
import { IUnControlledCodeMirror, UnControlled as CodeEditor } from "react-codemirror2";
|
||||
import { useFormikContext } from 'formik';
|
||||
import { Prompt } from 'react-router-dom';
|
||||
import { Editor } from 'codemirror';
|
||||
|
||||
import { MOBILE_BROWSER_REGEX, usePreventWindowUnload } from "~/logic/lib/util";
|
||||
import { PropFunc } from "~/types/util";
|
||||
@ -12,6 +13,10 @@ import "codemirror/addon/display/placeholder";
|
||||
|
||||
import "codemirror/lib/codemirror.css";
|
||||
import { Box } from "@tlon/indigo-react";
|
||||
import { useFileDrag } from "~/logic/lib/useDrag";
|
||||
import SubmitDragger from "~/views/components/SubmitDragger";
|
||||
import useS3 from "~/logic/lib/useS3";
|
||||
import { S3State } from "~/types";
|
||||
|
||||
const MARKDOWN_CONFIG = {
|
||||
name: "markdown",
|
||||
@ -22,6 +27,7 @@ interface MarkdownEditorProps {
|
||||
value: string;
|
||||
onChange: (s: string) => void;
|
||||
onBlur?: (e: any) => void;
|
||||
s3: S3State;
|
||||
}
|
||||
|
||||
const PromptIfDirty = () => {
|
||||
@ -50,6 +56,8 @@ export function MarkdownEditor(
|
||||
placeholder: placeholder || "",
|
||||
};
|
||||
|
||||
const editor: React.RefObject<any> = useRef();
|
||||
|
||||
const handleChange = useCallback(
|
||||
(_e, _d, v: string) => {
|
||||
onChange(v);
|
||||
@ -64,10 +72,33 @@ export function MarkdownEditor(
|
||||
[onBlur]
|
||||
);
|
||||
|
||||
const { uploadDefault, canUpload } = useS3(props.s3);
|
||||
|
||||
const onFileDrag = useCallback(
|
||||
async (files: FileList | File[], e: DragEvent) => {
|
||||
if (!canUpload || !editor.current) {
|
||||
return;
|
||||
}
|
||||
const codeMirror: Editor = editor.current.editor;
|
||||
const doc = codeMirror.getDoc();
|
||||
|
||||
Array.from(files).forEach(async file => {
|
||||
const placeholder = `![Uploading ${file.name}](...)`;
|
||||
doc.setValue(doc.getValue() + placeholder);
|
||||
const url = await uploadDefault(file);
|
||||
const markdown = `![${file.name}](${url})`;
|
||||
doc.setValue(doc.getValue().replace(placeholder, markdown));
|
||||
});
|
||||
},
|
||||
[uploadDefault, canUpload, value, onChange]
|
||||
);
|
||||
|
||||
const { bind, dragging } = useFileDrag(onFileDrag);
|
||||
|
||||
return (
|
||||
<Box
|
||||
height="100%"
|
||||
position="static"
|
||||
position="relative"
|
||||
className="publish"
|
||||
p={1}
|
||||
border={1}
|
||||
@ -78,12 +109,18 @@ export function MarkdownEditor(
|
||||
>
|
||||
<PromptIfDirty />
|
||||
<CodeEditor
|
||||
ref={editor}
|
||||
autoCursor={false}
|
||||
onBlur={onBlur}
|
||||
value={value}
|
||||
options={options}
|
||||
onChange={handleChange}
|
||||
onDragLeave={(editor, e) => bind.onDragLeave(e)}
|
||||
onDragOver={(editor, e) => bind.onDragOver(e)}
|
||||
onDrop={(editor, e) => bind.onDrop(e)}
|
||||
onDragEnter={(editor, e) => bind.onDragEnter(e)}
|
||||
/>
|
||||
{dragging && <SubmitDragger />}
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
@ -6,6 +6,7 @@ import { MarkdownEditor } from "./MarkdownEditor";
|
||||
|
||||
export const MarkdownField = ({
|
||||
id,
|
||||
s3,
|
||||
...rest
|
||||
}: { id: string } & Parameters<typeof Box>[0]) => {
|
||||
const [{ value, onBlur }, { error, touched }, { setValue }] = useField(id);
|
||||
@ -13,7 +14,6 @@ export const MarkdownField = ({
|
||||
const handleBlur = useCallback(
|
||||
(e: any) => {
|
||||
_.set(e, "target.id", id);
|
||||
console.log(e);
|
||||
onBlur && onBlur(e);
|
||||
},
|
||||
[onBlur, id]
|
||||
@ -35,6 +35,7 @@ export const MarkdownField = ({
|
||||
onBlur={handleBlur}
|
||||
value={value}
|
||||
onChange={setValue}
|
||||
s3={s3}
|
||||
/>
|
||||
<ErrorLabel mt="2" hasError={!!(error && touched)}>
|
||||
{error}
|
||||
|
@ -5,10 +5,11 @@ import {
|
||||
Row,
|
||||
Col,
|
||||
Button
|
||||
} from '@tlon/indigo-react';
|
||||
import { AsyncButton } from '../../../components/AsyncButton';
|
||||
import { Formik, Form, FormikHelpers } from 'formik';
|
||||
import { MarkdownField } from './MarkdownField';
|
||||
} from "@tlon/indigo-react";
|
||||
import { AsyncButton } from "../../../components/AsyncButton";
|
||||
import { Formik, Form, FormikHelpers } from "formik";
|
||||
import { MarkdownField } from "./MarkdownField";
|
||||
import { S3State } from "~/types";
|
||||
|
||||
interface PostFormProps {
|
||||
initial: PostFormSchema;
|
||||
@ -20,6 +21,7 @@ interface PostFormProps {
|
||||
) => Promise<any>;
|
||||
submitLabel: string;
|
||||
loadingText: string;
|
||||
s3: S3State;
|
||||
}
|
||||
|
||||
const formSchema = Yup.object({
|
||||
@ -33,7 +35,7 @@ export interface PostFormSchema {
|
||||
}
|
||||
|
||||
export function PostForm(props: PostFormProps) {
|
||||
const { initial, onSubmit, cancel, submitLabel, loadingText, history } = props;
|
||||
const { initial, onSubmit, submitLabel, loadingText, s3, cancel, history } = props;
|
||||
|
||||
return (
|
||||
<Col width="100%" height="100%" p={[2, 4]}>
|
||||
@ -64,7 +66,7 @@ export function PostForm(props: PostFormProps) {
|
||||
type="button">Cancel</Button>}
|
||||
</Row>
|
||||
</Row>
|
||||
<MarkdownField flexGrow={1} id="body" />
|
||||
<MarkdownField flexGrow={1} id="body" s3={s3} />
|
||||
</Form>
|
||||
</Formik>
|
||||
</Col>
|
||||
|
@ -6,7 +6,7 @@ import { RouteComponentProps } from "react-router-dom";
|
||||
import Note from "./Note";
|
||||
import { EditPost } from "./EditPost";
|
||||
|
||||
import { GraphNode, Graph, Contacts, LocalUpdateRemoteContentPolicy, Group } from "~/types";
|
||||
import { GraphNode, Graph, Contacts, LocalUpdateRemoteContentPolicy, Group, S3State } from "~/types";
|
||||
|
||||
interface NoteRoutesProps {
|
||||
ship: string;
|
||||
@ -21,6 +21,7 @@ interface NoteRoutesProps {
|
||||
hideAvatars: boolean;
|
||||
baseUrl?: string;
|
||||
rootUrl?: string;
|
||||
s3: S3State;
|
||||
}
|
||||
|
||||
export function NoteRoutes(props: NoteRoutesProps & RouteComponentProps) {
|
||||
|
@ -8,7 +8,8 @@ import {
|
||||
Groups,
|
||||
Contacts,
|
||||
Rolodex,
|
||||
LocalUpdateRemoteContentPolicy
|
||||
LocalUpdateRemoteContentPolicy,
|
||||
S3State
|
||||
} from "~/types";
|
||||
import { Center, LoadingSpinner } from "@tlon/indigo-react";
|
||||
import { Notebook as INotebook } from "~/types/publish-update";
|
||||
@ -35,6 +36,7 @@ interface NotebookRoutesProps {
|
||||
association: Association;
|
||||
remoteContentPolicy: LocalUpdateRemoteContentPolicy;
|
||||
associations: Associations;
|
||||
s3: S3State;
|
||||
}
|
||||
|
||||
export function NotebookRoutes(
|
||||
@ -79,6 +81,7 @@ export function NotebookRoutes(
|
||||
association={props.association}
|
||||
graph={graph}
|
||||
baseUrl={baseUrl}
|
||||
s3={props.s3}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
@ -110,6 +113,7 @@ export function NotebookRoutes(
|
||||
hideAvatars={props.hideAvatars}
|
||||
hideNicknames={props.hideNicknames}
|
||||
remoteContentPolicy={props.remoteContentPolicy}
|
||||
s3={props.s3}
|
||||
{...routeProps}
|
||||
/>
|
||||
);
|
||||
|
@ -6,7 +6,7 @@ import { RouteComponentProps } from "react-router-dom";
|
||||
import { PostForm, PostFormSchema } from "./NoteForm";
|
||||
import {createPost} from "~/logic/api/graph";
|
||||
import {Graph} from "~/types/graph-update";
|
||||
import {Association} from "~/types";
|
||||
import {Association, S3State} from "~/types";
|
||||
import {newPost} from "~/logic/lib/publish";
|
||||
|
||||
interface NewPostProps {
|
||||
@ -16,6 +16,7 @@ interface NewPostProps {
|
||||
graph: Graph;
|
||||
association: Association;
|
||||
baseUrl: string;
|
||||
s3: S3State;
|
||||
}
|
||||
|
||||
export default function NewPost(props: NewPostProps & RouteComponentProps) {
|
||||
@ -50,6 +51,7 @@ export default function NewPost(props: NewPostProps & RouteComponentProps) {
|
||||
onSubmit={onSubmit}
|
||||
submitLabel="Publish"
|
||||
loadingText="Posting..."
|
||||
s3={props.s3}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
@ -11,7 +11,7 @@ import {
|
||||
} from "@tlon/indigo-react";
|
||||
import { useField } from "formik";
|
||||
import { S3State } from "~/types/s3-update";
|
||||
import { useS3 } from "~/logic/lib/useS3";
|
||||
import useS3 from "~/logic/lib/useS3";
|
||||
|
||||
type ImageInputProps = Parameters<typeof Box>[0] & {
|
||||
id: string;
|
||||
@ -23,9 +23,7 @@ type ImageInputProps = Parameters<typeof Box>[0] & {
|
||||
export function ImageInput(props: ImageInputProps) {
|
||||
const { id, label, s3, caption, placeholder, ...rest } = props;
|
||||
|
||||
const { uploadDefault, canUpload } = useS3(s3);
|
||||
|
||||
const [uploading, setUploading] = useState(false);
|
||||
const { uploadDefault, canUpload, uploading } = useS3(s3);
|
||||
|
||||
const [field, meta, { setValue, setError }] = useField(id);
|
||||
|
||||
@ -38,14 +36,12 @@ export function ImageInput(props: ImageInputProps) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
setUploading(true);
|
||||
const url = await uploadDefault(file);
|
||||
setUploading(false);
|
||||
setValue(url);
|
||||
} catch (e) {
|
||||
setError(e.message);
|
||||
}
|
||||
}, [ref.current, uploadDefault, canUpload, setUploading, setValue]);
|
||||
}, [ref.current, uploadDefault, canUpload, setValue]);
|
||||
|
||||
const onClick = useCallback(() => {
|
||||
ref.current?.click();
|
||||
|
27
pkg/interface/src/views/components/SubmitDragger.tsx
Normal file
27
pkg/interface/src/views/components/SubmitDragger.tsx
Normal file
@ -0,0 +1,27 @@
|
||||
import { BaseInput, Box, Icon, LoadingSpinner, Text } from "@tlon/indigo-react";
|
||||
import React, { useCallback } from "react";
|
||||
import useS3 from "~/logic/lib/useS3";
|
||||
import { S3State } from "~/types";
|
||||
|
||||
const SubmitDragger = () => (
|
||||
<Box
|
||||
top='0'
|
||||
bottom='0'
|
||||
left='0'
|
||||
right='0'
|
||||
position='absolute'
|
||||
backgroundColor='white'
|
||||
height='100%'
|
||||
width='100%'
|
||||
display='flex'
|
||||
alignItems='center'
|
||||
justifyContent='center'
|
||||
style={{ pointerEvents: 'none', zIndex: 999 }}
|
||||
>
|
||||
<Text fontSize='1' color='black'>
|
||||
Drop a file to upload
|
||||
</Text>
|
||||
</Box>
|
||||
);
|
||||
|
||||
export default SubmitDragger;
|
@ -1,157 +0,0 @@
|
||||
import React, { Component } from 'react'
|
||||
import { BaseInput, Box, Text, Icon, LoadingSpinner } from "@tlon/indigo-react";
|
||||
|
||||
import S3Client from '~/logic/lib/s3';
|
||||
import { S3Credentials, S3Configuration } from '~/types';
|
||||
import { dateToDa, deSig } from '~/logic/lib/util';
|
||||
|
||||
export const SubmitDragger = () => (
|
||||
<Box
|
||||
top='0'
|
||||
bottom='0'
|
||||
left='0'
|
||||
right='0'
|
||||
position='absolute'
|
||||
backgroundColor='white'
|
||||
height='100%'
|
||||
width='100%'
|
||||
display='flex'
|
||||
alignItems='center'
|
||||
justifyContent='center'
|
||||
style={{ pointerEvents: 'none', zIndex: 999 }}
|
||||
>
|
||||
<Text fontSize='1' color='black'>
|
||||
Drop a file to upload
|
||||
</Text>
|
||||
</Box>
|
||||
);
|
||||
|
||||
interface S3UploadProps {
|
||||
credentials: S3Credentials;
|
||||
configuration: S3Configuration;
|
||||
uploadSuccess: Function;
|
||||
uploadError: Function;
|
||||
className?: string;
|
||||
accept: string;
|
||||
}
|
||||
|
||||
interface S3UploadState {
|
||||
isUploading: boolean;
|
||||
}
|
||||
|
||||
export class S3Upload extends Component<S3UploadProps, S3UploadState> {
|
||||
private s3: S3Client;
|
||||
public inputRef: React.RefObject<HTMLInputElement>;
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
isUploading: false
|
||||
};
|
||||
this.s3 = new S3Client();
|
||||
this.setCredentials(props.credentials, props.configuration);
|
||||
this.inputRef = React.createRef();
|
||||
}
|
||||
|
||||
isReady(creds, config): boolean {
|
||||
return (
|
||||
!!creds &&
|
||||
'endpoint' in creds &&
|
||||
'accessKeyId' in creds &&
|
||||
'secretAccessKey' in creds &&
|
||||
creds.endpoint !== '' &&
|
||||
creds.accessKeyId !== '' &&
|
||||
creds.secretAccessKey !== '' &&
|
||||
!!config &&
|
||||
'currentBucket' in config &&
|
||||
config.currentBucket !== ''
|
||||
);
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps): void {
|
||||
const { props } = this;
|
||||
if (!props.credentials !== prevProps.credentials || props.configuration !== prevProps.configuration) {
|
||||
this.setCredentials(props.credentials, props.configuration);
|
||||
}
|
||||
}
|
||||
|
||||
setCredentials(credentials, configuration): void {
|
||||
if (!this.isReady(credentials, configuration)) { return; }
|
||||
this.s3.setCredentials(
|
||||
credentials.endpoint,
|
||||
credentials.accessKeyId,
|
||||
credentials.secretAccessKey
|
||||
);
|
||||
}
|
||||
|
||||
onChange(): void {
|
||||
const { props } = this;
|
||||
if (!this.inputRef.current) { return; }
|
||||
let files = this.inputRef.current.files;
|
||||
if (!files || files.length <= 0) { return; }
|
||||
|
||||
let file = files.item(0);
|
||||
if (!file) { return; }
|
||||
const fileParts = file.name.split('.');
|
||||
const fileName = fileParts.slice(0, -1);
|
||||
const fileExtension = fileParts.pop();
|
||||
const timestamp = deSig(dateToDa(new Date()));
|
||||
let bucket = props.configuration.currentBucket;
|
||||
|
||||
setTimeout(() => {
|
||||
if (this.state.isUploading) return;
|
||||
this.setState({ isUploading: true });
|
||||
this.s3.upload(bucket, `${window.ship}/${timestamp}-${fileName}.${fileExtension}`, file)
|
||||
.then((data) => {
|
||||
if (!data || !('Location' in data)) {
|
||||
return;
|
||||
}
|
||||
this.props.uploadSuccess(data.Location);
|
||||
})
|
||||
.catch((err) => {
|
||||
console.error(err);
|
||||
this.props.uploadError(err);
|
||||
})
|
||||
.finally(() => {
|
||||
this.setState({ isUploading: false });
|
||||
});
|
||||
}, 200);
|
||||
}
|
||||
|
||||
onClick() {
|
||||
if (!this.inputRef.current) { return; }
|
||||
this.inputRef.current.click();
|
||||
}
|
||||
|
||||
render() {
|
||||
const {
|
||||
credentials,
|
||||
configuration,
|
||||
className = '',
|
||||
accept = '*',
|
||||
children = false
|
||||
} = this.props;
|
||||
|
||||
if (!this.isReady(credentials, configuration)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const display = children || <Icon icon='ArrowNorth' />;
|
||||
return (
|
||||
<>
|
||||
<BaseInput
|
||||
display='none'
|
||||
type="file"
|
||||
id="fileElement"
|
||||
ref={this.inputRef}
|
||||
accept={accept}
|
||||
onChange={this.onChange.bind(this)} />
|
||||
{this.state.isUploading
|
||||
? <LoadingSpinner background="gray" foreground="black" />
|
||||
: <Text cursor='pointer' className={className} onClick={this.onClick.bind(this)}>{display}</Text>
|
||||
}
|
||||
</>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
12
pkg/interface/src/views/components/withS3.tsx
Normal file
12
pkg/interface/src/views/components/withS3.tsx
Normal file
@ -0,0 +1,12 @@
|
||||
import React from "react";
|
||||
import useS3 from "~/logic/lib/useS3";
|
||||
|
||||
const withS3 = (Component, params = {}) => {
|
||||
return React.forwardRef((props: any, ref) => {
|
||||
const s3 = useS3(props.s3, params);
|
||||
|
||||
return <Component ref={ref} {...s3} {...props} />;
|
||||
});
|
||||
};
|
||||
|
||||
export default withS3;
|
Loading…
Reference in New Issue
Block a user