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";
|
import { useState, useCallback, useMemo, useEffect } from "react";
|
||||||
|
|
||||||
function validateDragEvent(e: DragEvent): FileList | null {
|
function validateDragEvent(e: DragEvent): FileList | File[] | true | null {
|
||||||
const files = e.dataTransfer?.files;
|
const files: File[] = [];
|
||||||
console.log(files);
|
let valid = false;
|
||||||
if(!files?.length) {
|
if (e.dataTransfer?.files) {
|
||||||
return null;
|
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 [dragging, setDragging] = useState(false);
|
||||||
|
|
||||||
const onDragEnter = useCallback(
|
const onDragEnter = useCallback(
|
||||||
@ -25,18 +49,21 @@ export function useFileDrag(dragged: (f: FileList) => void) {
|
|||||||
const onDrop = useCallback(
|
const onDrop = useCallback(
|
||||||
(e: DragEvent) => {
|
(e: DragEvent) => {
|
||||||
setDragging(false);
|
setDragging(false);
|
||||||
e.preventDefault();
|
|
||||||
const files = validateDragEvent(e);
|
const files = validateDragEvent(e);
|
||||||
if (!files) {
|
if (!files || files === true) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
dragged(files);
|
e.preventDefault();
|
||||||
|
dragged(files, e);
|
||||||
},
|
},
|
||||||
[setDragging, dragged]
|
[setDragging, dragged]
|
||||||
);
|
);
|
||||||
|
|
||||||
const onDragOver = useCallback(
|
const onDragOver = useCallback(
|
||||||
(e: DragEvent) => {
|
(e: DragEvent) => {
|
||||||
|
if (!validateDragEvent(e)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
setDragging(true);
|
setDragging(true);
|
||||||
},
|
},
|
||||||
@ -53,6 +80,18 @@ export function useFileDrag(dragged: (f: FileList) => void) {
|
|||||||
[setDragging]
|
[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 = {
|
const bind = {
|
||||||
onDragLeave,
|
onDragLeave,
|
||||||
onDragOver,
|
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 { S3State } from "../../types/s3-update";
|
||||||
import S3 from "aws-sdk/clients/s3";
|
import S3 from "aws-sdk/clients/s3";
|
||||||
|
import { dateToDa, deSig } from "./util";
|
||||||
|
|
||||||
export function useS3(s3: S3State) {
|
export interface IuseS3 {
|
||||||
const { configuration, credentials } = s3;
|
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);
|
const client = useRef<S3 | null>(null);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!credentials) {
|
if (!s3.credentials) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
client.current = new S3({ credentials, endpoint: credentials.endpoint });
|
client.current = new S3({
|
||||||
}, [credentials]);
|
credentials: s3.credentials,
|
||||||
|
endpoint: s3.credentials.endpoint
|
||||||
|
});
|
||||||
|
}, [s3.credentials]);
|
||||||
|
|
||||||
const canUpload = useMemo(
|
const canUpload = useMemo(
|
||||||
() =>
|
() =>
|
||||||
(client && credentials && configuration.currentBucket !== "") || false,
|
(client && s3.credentials && s3.configuration.currentBucket !== "") || false,
|
||||||
[credentials, configuration.currentBucket, client]
|
[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(
|
const upload = useCallback(
|
||||||
async (file: File, bucket: string) => {
|
async (file: File, bucket: string) => {
|
||||||
if (!client.current) {
|
if (!client.current) {
|
||||||
throw new Error("S3 not ready");
|
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 = {
|
const params = {
|
||||||
Bucket: bucket,
|
Bucket: bucket,
|
||||||
Key: file.name,
|
Key: `${window.ship}/${timestamp}-${fileName}.${fileExtension}`,
|
||||||
Body: file,
|
Body: file,
|
||||||
ACL: "public-read",
|
ACL: "public-read",
|
||||||
ContentType: file.type,
|
ContentType: file.type,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
setUploading(true);
|
||||||
|
|
||||||
const { Location } = await client.current.upload(params).promise();
|
const { Location } = await client.current.upload(params).promise();
|
||||||
|
|
||||||
|
setUploading(false);
|
||||||
|
|
||||||
return Location;
|
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) {
|
export function pluralize(text: string, isPlural = false, vowel = false) {
|
||||||
return isPlural ? `${text}s`: `${vowel ? 'an' : 'a'} ${text}`;
|
return isPlural ? `${text}s`: `${vowel ? 'an' : 'a'} ${text}`;
|
||||||
}
|
}
|
@ -9,8 +9,9 @@ import { useFileDrag } from '~/logic/lib/useDrag';
|
|||||||
import ChatWindow from './components/ChatWindow';
|
import ChatWindow from './components/ChatWindow';
|
||||||
import ChatInput from './components/ChatInput';
|
import ChatInput from './components/ChatInput';
|
||||||
import GlobalApi from '~/logic/api/global';
|
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 { useLocalStorageState } from '~/logic/lib/useLocalStorageState';
|
||||||
|
import useS3 from '~/logic/lib/useS3';
|
||||||
|
|
||||||
type ChatResourceProps = StoreState & {
|
type ChatResourceProps = StoreState & {
|
||||||
association: Association;
|
association: Association;
|
||||||
@ -65,20 +66,6 @@ export function ChatResource(props: ChatResourceProps) {
|
|||||||
const ourContact = contacts?.[window.ship];
|
const ourContact = contacts?.[window.ship];
|
||||||
const lastMsgNum = envelopes.length || 0;
|
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>>(
|
const [unsent, setUnsent] = useLocalStorageState<Record<string, string>>(
|
||||||
'chat-unsent',
|
'chat-unsent',
|
||||||
{}
|
{}
|
||||||
@ -89,13 +76,29 @@ export function ChatResource(props: ChatResourceProps) {
|
|||||||
[station]
|
[station]
|
||||||
);
|
);
|
||||||
|
|
||||||
const clearUnsent = useCallback(() => setUnsent(s => _.omit(s, station)), [
|
const clearUnsent = useCallback(
|
||||||
station
|
() => 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');
|
const scrollTo = new URLSearchParams(location.search).get('msg');
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const clear = () => {
|
const clear = () => {
|
||||||
props.history.replace(location.pathname);
|
props.history.replace(location.pathname);
|
||||||
};
|
};
|
||||||
setTimeout(clear, 10000);
|
setTimeout(clear, 10000);
|
||||||
|
@ -1,15 +1,16 @@
|
|||||||
import React, { Component } from 'react';
|
import React, { Component } from 'react';
|
||||||
import ChatEditor from './chat-editor';
|
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 { uxToHex } from '~/logic/lib/util';
|
||||||
import { Sigil } from '~/logic/lib/sigil';
|
import { Sigil } from '~/logic/lib/sigil';
|
||||||
import tokenizeMessage, { isUrl } from '~/logic/lib/tokenizeMessage';
|
import tokenizeMessage, { isUrl } from '~/logic/lib/tokenizeMessage';
|
||||||
import GlobalApi from '~/logic/api/global';
|
import GlobalApi from '~/logic/api/global';
|
||||||
import { Envelope } from '~/types/chat-update';
|
import { Envelope } from '~/types/chat-update';
|
||||||
import { Contacts } from '~/types';
|
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;
|
api: GlobalApi;
|
||||||
numMsgs: number;
|
numMsgs: number;
|
||||||
station: any;
|
station: any;
|
||||||
@ -22,7 +23,6 @@ interface ChatInputProps {
|
|||||||
message: string;
|
message: string;
|
||||||
deleteMessage(): void;
|
deleteMessage(): void;
|
||||||
hideAvatars: boolean;
|
hideAvatars: boolean;
|
||||||
onPaste?(): void;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
interface ChatInputState {
|
interface ChatInputState {
|
||||||
@ -31,8 +31,7 @@ interface ChatInputState {
|
|||||||
uploadingPaste: boolean;
|
uploadingPaste: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default class ChatInput extends Component<ChatInputProps, ChatInputState> {
|
class ChatInput extends Component<ChatInputProps, ChatInputState> {
|
||||||
public s3Uploader: React.RefObject<S3Upload>;
|
|
||||||
private chatEditor: React.RefObject<ChatEditor>;
|
private chatEditor: React.RefObject<ChatEditor>;
|
||||||
|
|
||||||
constructor(props) {
|
constructor(props) {
|
||||||
@ -44,11 +43,12 @@ export default class ChatInput extends Component<ChatInputProps, ChatInputState>
|
|||||||
uploadingPaste: false
|
uploadingPaste: false
|
||||||
};
|
};
|
||||||
|
|
||||||
this.s3Uploader = React.createRef();
|
|
||||||
this.chatEditor = React.createRef();
|
this.chatEditor = React.createRef();
|
||||||
|
|
||||||
this.submit = this.submit.bind(this);
|
this.submit = this.submit.bind(this);
|
||||||
this.toggleCode = this.toggleCode.bind(this);
|
this.toggleCode = this.toggleCode.bind(this);
|
||||||
|
this.uploadSuccess = this.uploadSuccess.bind(this);
|
||||||
|
this.uploadError = this.uploadError.bind(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
toggleCode() {
|
toggleCode() {
|
||||||
@ -136,10 +136,6 @@ export default class ChatInput extends Component<ChatInputProps, ChatInputState>
|
|||||||
// no-op for now
|
// no-op for now
|
||||||
}
|
}
|
||||||
|
|
||||||
readyToUpload(): boolean {
|
|
||||||
return Boolean(this.s3Uploader.current?.inputRef.current);
|
|
||||||
}
|
|
||||||
|
|
||||||
onPaste(codemirrorInstance, event: ClipboardEvent) {
|
onPaste(codemirrorInstance, event: ClipboardEvent) {
|
||||||
if (!event.clipboardData || !event.clipboardData.files.length) {
|
if (!event.clipboardData || !event.clipboardData.files.length) {
|
||||||
return;
|
return;
|
||||||
@ -150,16 +146,15 @@ export default class ChatInput extends Component<ChatInputProps, ChatInputState>
|
|||||||
this.uploadFiles(event.clipboardData.files);
|
this.uploadFiles(event.clipboardData.files);
|
||||||
}
|
}
|
||||||
|
|
||||||
uploadFiles(files: FileList) {
|
uploadFiles(files: FileList | File[]) {
|
||||||
if (!this.readyToUpload()) {
|
if (!this.props.canUpload) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (!this.s3Uploader.current || !this.s3Uploader.current.inputRef.current)
|
Array.from(files).forEach(file => {
|
||||||
return;
|
this.props.uploadDefault(file)
|
||||||
this.s3Uploader.current.inputRef.current.files = files;
|
.then(this.uploadSuccess)
|
||||||
const fire = document.createEvent('HTMLEvents');
|
.catch(this.uploadError);
|
||||||
fire.initEvent('change', true, true);
|
});
|
||||||
this.s3Uploader.current?.inputRef.current?.dispatchEvent(fire);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
@ -189,13 +184,13 @@ return;
|
|||||||
<Row
|
<Row
|
||||||
alignItems='center'
|
alignItems='center'
|
||||||
position='relative'
|
position='relative'
|
||||||
flexGrow='1'
|
flexGrow={1}
|
||||||
flexShrink='0'
|
flexShrink={0}
|
||||||
borderTop='1'
|
borderTop={1}
|
||||||
borderTopColor='washedGray'
|
borderTopColor='washedGray'
|
||||||
backgroundColor='white'
|
backgroundColor='white'
|
||||||
className='cf'
|
className='cf'
|
||||||
zIndex='0'
|
zIndex={0}
|
||||||
>
|
>
|
||||||
<Row p='2' alignItems='center'>
|
<Row p='2' alignItems='center'>
|
||||||
{avatar}
|
{avatar}
|
||||||
@ -210,29 +205,26 @@ return;
|
|||||||
placeholder='Message...'
|
placeholder='Message...'
|
||||||
/>
|
/>
|
||||||
<Box
|
<Box
|
||||||
mx='2'
|
mx={2}
|
||||||
flexShrink='0'
|
flexShrink={0}
|
||||||
height='16px'
|
height='16px'
|
||||||
width='16px'
|
width='16px'
|
||||||
flexBasis='16px'
|
flexBasis='16px'
|
||||||
>
|
>
|
||||||
<S3Upload
|
{this.props.canUpload
|
||||||
ref={this.s3Uploader}
|
? this.props.uploading
|
||||||
configuration={props.s3.configuration}
|
? <LoadingSpinner />
|
||||||
credentials={props.s3.credentials}
|
: <Icon icon='Links'
|
||||||
uploadSuccess={this.uploadSuccess.bind(this)}
|
width="16"
|
||||||
uploadError={this.uploadError.bind(this)}
|
height="16"
|
||||||
accept="*"
|
onClick={() => this.props.promptUpload().then(this.uploadSuccess)}
|
||||||
>
|
/>
|
||||||
<Icon icon='Links'
|
: null
|
||||||
width="16"
|
}
|
||||||
height="16"
|
|
||||||
/>
|
|
||||||
</S3Upload>
|
|
||||||
</Box>
|
</Box>
|
||||||
<Box
|
<Box
|
||||||
mr='2'
|
mr={2}
|
||||||
flexShrink='0'
|
flexShrink={0}
|
||||||
height='16px'
|
height='16px'
|
||||||
width='16px'
|
width='16px'
|
||||||
flexBasis='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 { RouteComponentProps } from "react-router-dom";
|
||||||
|
|
||||||
import { LinkItem } from "./components/LinkItem";
|
import { LinkItem } from "./components/LinkItem";
|
||||||
import { LinkSubmit } from "./components/link-submit";
|
import LinkSubmit from "./components/LinkSubmit";
|
||||||
import { LinkPreview } from "./components/link-preview";
|
import { LinkPreview } from "./components/link-preview";
|
||||||
import { Comments } from "~/views/components/comments";
|
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}
|
hideNicknames={props.hideNicknames}
|
||||||
remoteContentPolicy={props.remoteContentPolicy}
|
remoteContentPolicy={props.remoteContentPolicy}
|
||||||
graphs={props.graphs}
|
graphs={props.graphs}
|
||||||
|
s3={props.s3}
|
||||||
/>
|
/>
|
||||||
</Box>
|
</Box>
|
||||||
);
|
);
|
||||||
|
@ -4,7 +4,7 @@ import { PostFormSchema, PostForm } from "./NoteForm";
|
|||||||
import { FormikHelpers } from "formik";
|
import { FormikHelpers } from "formik";
|
||||||
import GlobalApi from "~/logic/api/global";
|
import GlobalApi from "~/logic/api/global";
|
||||||
import { RouteComponentProps, useLocation } from "react-router-dom";
|
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 { getLatestRevision, editPost } from "~/logic/lib/publish";
|
||||||
import {useWaitForProps} from "~/logic/lib/useWaitForProps";
|
import {useWaitForProps} from "~/logic/lib/useWaitForProps";
|
||||||
interface EditPostProps {
|
interface EditPostProps {
|
||||||
@ -13,10 +13,11 @@ interface EditPostProps {
|
|||||||
note: GraphNode;
|
note: GraphNode;
|
||||||
api: GlobalApi;
|
api: GlobalApi;
|
||||||
book: string;
|
book: string;
|
||||||
|
s3: S3State;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function EditPost(props: EditPostProps & RouteComponentProps) {
|
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 [revNum, title, body] = getLatestRevision(note);
|
||||||
const location = useLocation();
|
const location = useLocation();
|
||||||
|
|
||||||
@ -53,6 +54,7 @@ export function EditPost(props: EditPostProps & RouteComponentProps) {
|
|||||||
cancel
|
cancel
|
||||||
history={history}
|
history={history}
|
||||||
onSubmit={onSubmit}
|
onSubmit={onSubmit}
|
||||||
|
s3={s3}
|
||||||
submitLabel="Update"
|
submitLabel="Update"
|
||||||
loadingText="Updating..."
|
loadingText="Updating..."
|
||||||
/>
|
/>
|
||||||
|
@ -1,7 +1,8 @@
|
|||||||
import React, { useCallback } from "react";
|
import React, { createRef, useCallback, useRef } from "react";
|
||||||
import { UnControlled as CodeEditor } from "react-codemirror2";
|
import { IUnControlledCodeMirror, UnControlled as CodeEditor } from "react-codemirror2";
|
||||||
import { useFormikContext } from 'formik';
|
import { useFormikContext } from 'formik';
|
||||||
import { Prompt } from 'react-router-dom';
|
import { Prompt } from 'react-router-dom';
|
||||||
|
import { Editor } from 'codemirror';
|
||||||
|
|
||||||
import { MOBILE_BROWSER_REGEX, usePreventWindowUnload } from "~/logic/lib/util";
|
import { MOBILE_BROWSER_REGEX, usePreventWindowUnload } from "~/logic/lib/util";
|
||||||
import { PropFunc } from "~/types/util";
|
import { PropFunc } from "~/types/util";
|
||||||
@ -12,6 +13,10 @@ import "codemirror/addon/display/placeholder";
|
|||||||
|
|
||||||
import "codemirror/lib/codemirror.css";
|
import "codemirror/lib/codemirror.css";
|
||||||
import { Box } from "@tlon/indigo-react";
|
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 = {
|
const MARKDOWN_CONFIG = {
|
||||||
name: "markdown",
|
name: "markdown",
|
||||||
@ -22,6 +27,7 @@ interface MarkdownEditorProps {
|
|||||||
value: string;
|
value: string;
|
||||||
onChange: (s: string) => void;
|
onChange: (s: string) => void;
|
||||||
onBlur?: (e: any) => void;
|
onBlur?: (e: any) => void;
|
||||||
|
s3: S3State;
|
||||||
}
|
}
|
||||||
|
|
||||||
const PromptIfDirty = () => {
|
const PromptIfDirty = () => {
|
||||||
@ -50,6 +56,8 @@ export function MarkdownEditor(
|
|||||||
placeholder: placeholder || "",
|
placeholder: placeholder || "",
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const editor: React.RefObject<any> = useRef();
|
||||||
|
|
||||||
const handleChange = useCallback(
|
const handleChange = useCallback(
|
||||||
(_e, _d, v: string) => {
|
(_e, _d, v: string) => {
|
||||||
onChange(v);
|
onChange(v);
|
||||||
@ -64,10 +72,33 @@ export function MarkdownEditor(
|
|||||||
[onBlur]
|
[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 (
|
return (
|
||||||
<Box
|
<Box
|
||||||
height="100%"
|
height="100%"
|
||||||
position="static"
|
position="relative"
|
||||||
className="publish"
|
className="publish"
|
||||||
p={1}
|
p={1}
|
||||||
border={1}
|
border={1}
|
||||||
@ -78,12 +109,18 @@ export function MarkdownEditor(
|
|||||||
>
|
>
|
||||||
<PromptIfDirty />
|
<PromptIfDirty />
|
||||||
<CodeEditor
|
<CodeEditor
|
||||||
|
ref={editor}
|
||||||
autoCursor={false}
|
autoCursor={false}
|
||||||
onBlur={onBlur}
|
onBlur={onBlur}
|
||||||
value={value}
|
value={value}
|
||||||
options={options}
|
options={options}
|
||||||
onChange={handleChange}
|
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>
|
</Box>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -6,6 +6,7 @@ import { MarkdownEditor } from "./MarkdownEditor";
|
|||||||
|
|
||||||
export const MarkdownField = ({
|
export const MarkdownField = ({
|
||||||
id,
|
id,
|
||||||
|
s3,
|
||||||
...rest
|
...rest
|
||||||
}: { id: string } & Parameters<typeof Box>[0]) => {
|
}: { id: string } & Parameters<typeof Box>[0]) => {
|
||||||
const [{ value, onBlur }, { error, touched }, { setValue }] = useField(id);
|
const [{ value, onBlur }, { error, touched }, { setValue }] = useField(id);
|
||||||
@ -13,7 +14,6 @@ export const MarkdownField = ({
|
|||||||
const handleBlur = useCallback(
|
const handleBlur = useCallback(
|
||||||
(e: any) => {
|
(e: any) => {
|
||||||
_.set(e, "target.id", id);
|
_.set(e, "target.id", id);
|
||||||
console.log(e);
|
|
||||||
onBlur && onBlur(e);
|
onBlur && onBlur(e);
|
||||||
},
|
},
|
||||||
[onBlur, id]
|
[onBlur, id]
|
||||||
@ -35,6 +35,7 @@ export const MarkdownField = ({
|
|||||||
onBlur={handleBlur}
|
onBlur={handleBlur}
|
||||||
value={value}
|
value={value}
|
||||||
onChange={setValue}
|
onChange={setValue}
|
||||||
|
s3={s3}
|
||||||
/>
|
/>
|
||||||
<ErrorLabel mt="2" hasError={!!(error && touched)}>
|
<ErrorLabel mt="2" hasError={!!(error && touched)}>
|
||||||
{error}
|
{error}
|
||||||
|
@ -5,10 +5,11 @@ import {
|
|||||||
Row,
|
Row,
|
||||||
Col,
|
Col,
|
||||||
Button
|
Button
|
||||||
} from '@tlon/indigo-react';
|
} from "@tlon/indigo-react";
|
||||||
import { AsyncButton } from '../../../components/AsyncButton';
|
import { AsyncButton } from "../../../components/AsyncButton";
|
||||||
import { Formik, Form, FormikHelpers } from 'formik';
|
import { Formik, Form, FormikHelpers } from "formik";
|
||||||
import { MarkdownField } from './MarkdownField';
|
import { MarkdownField } from "./MarkdownField";
|
||||||
|
import { S3State } from "~/types";
|
||||||
|
|
||||||
interface PostFormProps {
|
interface PostFormProps {
|
||||||
initial: PostFormSchema;
|
initial: PostFormSchema;
|
||||||
@ -20,6 +21,7 @@ interface PostFormProps {
|
|||||||
) => Promise<any>;
|
) => Promise<any>;
|
||||||
submitLabel: string;
|
submitLabel: string;
|
||||||
loadingText: string;
|
loadingText: string;
|
||||||
|
s3: S3State;
|
||||||
}
|
}
|
||||||
|
|
||||||
const formSchema = Yup.object({
|
const formSchema = Yup.object({
|
||||||
@ -33,7 +35,7 @@ export interface PostFormSchema {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function PostForm(props: PostFormProps) {
|
export function PostForm(props: PostFormProps) {
|
||||||
const { initial, onSubmit, cancel, submitLabel, loadingText, history } = props;
|
const { initial, onSubmit, submitLabel, loadingText, s3, cancel, history } = props;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Col width="100%" height="100%" p={[2, 4]}>
|
<Col width="100%" height="100%" p={[2, 4]}>
|
||||||
@ -64,7 +66,7 @@ export function PostForm(props: PostFormProps) {
|
|||||||
type="button">Cancel</Button>}
|
type="button">Cancel</Button>}
|
||||||
</Row>
|
</Row>
|
||||||
</Row>
|
</Row>
|
||||||
<MarkdownField flexGrow={1} id="body" />
|
<MarkdownField flexGrow={1} id="body" s3={s3} />
|
||||||
</Form>
|
</Form>
|
||||||
</Formik>
|
</Formik>
|
||||||
</Col>
|
</Col>
|
||||||
|
@ -6,7 +6,7 @@ import { RouteComponentProps } from "react-router-dom";
|
|||||||
import Note from "./Note";
|
import Note from "./Note";
|
||||||
import { EditPost } from "./EditPost";
|
import { EditPost } from "./EditPost";
|
||||||
|
|
||||||
import { GraphNode, Graph, Contacts, LocalUpdateRemoteContentPolicy, Group } from "~/types";
|
import { GraphNode, Graph, Contacts, LocalUpdateRemoteContentPolicy, Group, S3State } from "~/types";
|
||||||
|
|
||||||
interface NoteRoutesProps {
|
interface NoteRoutesProps {
|
||||||
ship: string;
|
ship: string;
|
||||||
@ -21,6 +21,7 @@ interface NoteRoutesProps {
|
|||||||
hideAvatars: boolean;
|
hideAvatars: boolean;
|
||||||
baseUrl?: string;
|
baseUrl?: string;
|
||||||
rootUrl?: string;
|
rootUrl?: string;
|
||||||
|
s3: S3State;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function NoteRoutes(props: NoteRoutesProps & RouteComponentProps) {
|
export function NoteRoutes(props: NoteRoutesProps & RouteComponentProps) {
|
||||||
|
@ -8,7 +8,8 @@ import {
|
|||||||
Groups,
|
Groups,
|
||||||
Contacts,
|
Contacts,
|
||||||
Rolodex,
|
Rolodex,
|
||||||
LocalUpdateRemoteContentPolicy
|
LocalUpdateRemoteContentPolicy,
|
||||||
|
S3State
|
||||||
} from "~/types";
|
} from "~/types";
|
||||||
import { Center, LoadingSpinner } from "@tlon/indigo-react";
|
import { Center, LoadingSpinner } from "@tlon/indigo-react";
|
||||||
import { Notebook as INotebook } from "~/types/publish-update";
|
import { Notebook as INotebook } from "~/types/publish-update";
|
||||||
@ -35,6 +36,7 @@ interface NotebookRoutesProps {
|
|||||||
association: Association;
|
association: Association;
|
||||||
remoteContentPolicy: LocalUpdateRemoteContentPolicy;
|
remoteContentPolicy: LocalUpdateRemoteContentPolicy;
|
||||||
associations: Associations;
|
associations: Associations;
|
||||||
|
s3: S3State;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function NotebookRoutes(
|
export function NotebookRoutes(
|
||||||
@ -79,6 +81,7 @@ export function NotebookRoutes(
|
|||||||
association={props.association}
|
association={props.association}
|
||||||
graph={graph}
|
graph={graph}
|
||||||
baseUrl={baseUrl}
|
baseUrl={baseUrl}
|
||||||
|
s3={props.s3}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
@ -110,6 +113,7 @@ export function NotebookRoutes(
|
|||||||
hideAvatars={props.hideAvatars}
|
hideAvatars={props.hideAvatars}
|
||||||
hideNicknames={props.hideNicknames}
|
hideNicknames={props.hideNicknames}
|
||||||
remoteContentPolicy={props.remoteContentPolicy}
|
remoteContentPolicy={props.remoteContentPolicy}
|
||||||
|
s3={props.s3}
|
||||||
{...routeProps}
|
{...routeProps}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
@ -6,7 +6,7 @@ import { RouteComponentProps } from "react-router-dom";
|
|||||||
import { PostForm, PostFormSchema } from "./NoteForm";
|
import { PostForm, PostFormSchema } from "./NoteForm";
|
||||||
import {createPost} from "~/logic/api/graph";
|
import {createPost} from "~/logic/api/graph";
|
||||||
import {Graph} from "~/types/graph-update";
|
import {Graph} from "~/types/graph-update";
|
||||||
import {Association} from "~/types";
|
import {Association, S3State} from "~/types";
|
||||||
import {newPost} from "~/logic/lib/publish";
|
import {newPost} from "~/logic/lib/publish";
|
||||||
|
|
||||||
interface NewPostProps {
|
interface NewPostProps {
|
||||||
@ -16,6 +16,7 @@ interface NewPostProps {
|
|||||||
graph: Graph;
|
graph: Graph;
|
||||||
association: Association;
|
association: Association;
|
||||||
baseUrl: string;
|
baseUrl: string;
|
||||||
|
s3: S3State;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function NewPost(props: NewPostProps & RouteComponentProps) {
|
export default function NewPost(props: NewPostProps & RouteComponentProps) {
|
||||||
@ -50,6 +51,7 @@ export default function NewPost(props: NewPostProps & RouteComponentProps) {
|
|||||||
onSubmit={onSubmit}
|
onSubmit={onSubmit}
|
||||||
submitLabel="Publish"
|
submitLabel="Publish"
|
||||||
loadingText="Posting..."
|
loadingText="Posting..."
|
||||||
|
s3={props.s3}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -11,7 +11,7 @@ import {
|
|||||||
} from "@tlon/indigo-react";
|
} from "@tlon/indigo-react";
|
||||||
import { useField } from "formik";
|
import { useField } from "formik";
|
||||||
import { S3State } from "~/types/s3-update";
|
import { S3State } from "~/types/s3-update";
|
||||||
import { useS3 } from "~/logic/lib/useS3";
|
import useS3 from "~/logic/lib/useS3";
|
||||||
|
|
||||||
type ImageInputProps = Parameters<typeof Box>[0] & {
|
type ImageInputProps = Parameters<typeof Box>[0] & {
|
||||||
id: string;
|
id: string;
|
||||||
@ -23,9 +23,7 @@ type ImageInputProps = Parameters<typeof Box>[0] & {
|
|||||||
export function ImageInput(props: ImageInputProps) {
|
export function ImageInput(props: ImageInputProps) {
|
||||||
const { id, label, s3, caption, placeholder, ...rest } = props;
|
const { id, label, s3, caption, placeholder, ...rest } = props;
|
||||||
|
|
||||||
const { uploadDefault, canUpload } = useS3(s3);
|
const { uploadDefault, canUpload, uploading } = useS3(s3);
|
||||||
|
|
||||||
const [uploading, setUploading] = useState(false);
|
|
||||||
|
|
||||||
const [field, meta, { setValue, setError }] = useField(id);
|
const [field, meta, { setValue, setError }] = useField(id);
|
||||||
|
|
||||||
@ -38,14 +36,12 @@ export function ImageInput(props: ImageInputProps) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
setUploading(true);
|
|
||||||
const url = await uploadDefault(file);
|
const url = await uploadDefault(file);
|
||||||
setUploading(false);
|
|
||||||
setValue(url);
|
setValue(url);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
setError(e.message);
|
setError(e.message);
|
||||||
}
|
}
|
||||||
}, [ref.current, uploadDefault, canUpload, setUploading, setValue]);
|
}, [ref.current, uploadDefault, canUpload, setValue]);
|
||||||
|
|
||||||
const onClick = useCallback(() => {
|
const onClick = useCallback(() => {
|
||||||
ref.current?.click();
|
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