Merge pull request #3432 from tylershuster/link-drag

links, chat: add drag and drop
This commit is contained in:
matildepark 2020-09-08 15:10:22 -04:00 committed by GitHub
commit e07509fa11
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 544 additions and 325 deletions

View File

@ -15,6 +15,8 @@ import GlobalApi from "~/logic/api/global";
import { Association } from "~/types/metadata-update";
import {Group} from "~/types/group-update";
import { LocalUpdateRemoteContentPolicy } from "~/types";
import { S3Upload, SubmitDragger } from '~/views/components/s3-upload';
import { IUnControlledCodeMirror } from "react-codemirror2";
type ChatScreenProps = RouteComponentProps<{
@ -43,9 +45,11 @@ type ChatScreenProps = RouteComponentProps<{
interface ChatScreenState {
messages: Map<string, string>;
dragover: boolean;
}
export class ChatScreen extends Component<ChatScreenProps, ChatScreenState> {
private chatInput: React.RefObject<ChatInput>;
lastNumPending = 0;
activityTimeout: NodeJS.Timeout | null = null;
@ -54,8 +58,11 @@ export class ChatScreen extends Component<ChatScreenProps, ChatScreenState> {
this.state = {
messages: new Map(),
dragover: false,
};
this.chatInput = React.createRef();
moment.updateLocale("en", {
calendar: {
sameDay: "[Today]",
@ -68,6 +75,26 @@ export class ChatScreen extends Component<ChatScreenProps, ChatScreenState> {
});
}
readyToUpload(): boolean {
return Boolean(this.chatInput.current?.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.chatInput.current?.uploadFiles(event.dataTransfer.files);
}
render() {
const { props, state } = this;
@ -104,7 +131,18 @@ export class ChatScreen extends Component<ChatScreenProps, ChatScreenState> {
return (
<div
key={props.station}
className="h-100 w-100 overflow-hidden flex flex-column relative">
className="h-100 w-100 overflow-hidden flex flex-column relative"
onDragEnter={this.onDragEnter.bind(this)}
onDragOver={event => {
event.preventDefault();
if (!this.state.dragover) {
this.setState({ dragover: true });
}
}}
onDragLeave={() => this.setState({ dragover: false })}
onDrop={this.onDrop.bind(this)}
>
{this.state.dragover ? <SubmitDragger /> : null}
<ChatHeader {...props} />
<ChatWindow
isChatMissing={isChatMissing}
@ -116,6 +154,7 @@ export class ChatScreen extends Component<ChatScreenProps, ChatScreenState> {
ship={props.match.params.ship}
{...props} />
<ChatInput
ref={this.chatInput}
api={props.api}
numMsgs={lastMsgNum}
station={props.station}

View File

@ -101,9 +101,14 @@ export default class ChatEditor extends Component {
}
render() {
const { props } = this;
const {
inCodeMode,
placeholder,
message,
...props
} = this.props;
const codeTheme = props.inCodeMode ? ' code' : '';
const codeTheme = inCodeMode ? ' code' : '';
const options = {
mode: MARKDOWN_CONFIG,
@ -112,7 +117,7 @@ export default class ChatEditor extends Component {
lineWrapping: true,
scrollbarStyle: 'native',
cursorHeight: 0.85,
placeholder: props.inCodeMode ? 'Code...' : props.placeholder,
placeholder: inCodeMode ? 'Code...' : placeholder,
extraKeys: {
'Enter': () => {
this.submit();
@ -127,11 +132,11 @@ export default class ChatEditor extends Component {
<div
className={
'chat fr h-100 flex bg-gray0-d lh-copy pl2 w-100 items-center' +
(props.inCodeMode ? ' code' : '')
(inCodeMode ? ' code' : '')
}
style={{ flexGrow: 1, maxHeight: '224px', width: 'calc(100% - 72px)' }}>
<CodeEditor
value={props.message}
value={message}
options={options}
onChange={(e, d, v) => this.messageChange(e, d, v)}
editorDidMount={(editor) => {
@ -140,6 +145,7 @@ export default class ChatEditor extends Component {
editor.focus();
}
}}
{...props}
/>
</div>
);

View File

@ -1,38 +1,57 @@
import React, { Component } from 'react';
import ChatEditor from './chat-editor';
import { S3Upload } from '~/views/components/s3-upload'
import { S3Upload, SubmitDragger } from '~/views/components/s3-upload'
;
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, S3Configuration } from '~/types';
interface ChatInputProps {
api: GlobalApi;
numMsgs: number;
station: any;
owner: string;
ownerContact: any;
envelopes: Envelope[];
contacts: Contacts;
onUnmount(msg: string): void;
s3: any;
placeholder: string;
message: string;
deleteMessage(): void;
hideAvatars: boolean;
onPaste?(): void;
}
interface ChatInputState {
inCodeMode: boolean;
submitFocus: boolean;
uploadingPaste: boolean;
}
export class ChatInput extends Component<ChatInputProps, ChatInputState> {
public s3Uploader: React.RefObject<S3Upload>;
private chatEditor: React.RefObject<ChatEditor>;
export class ChatInput extends Component {
constructor(props) {
super(props);
this.state = {
inCodeMode: false,
submitFocus: false,
uploadingPaste: false,
};
this.s3Uploader = React.createRef();
this.chatEditor = React.createRef();
this.submit = this.submit.bind(this);
this.toggleCode = this.toggleCode.bind(this);
}
uploadSuccess(url) {
const { props } = this;
props.api.chat.message(
props.station,
`~${window.ship}`,
Date.now(),
{ url }
);
}
uploadError(error) {
// no-op for now
}
toggleCode() {
@ -105,18 +124,48 @@ export class ChatInput extends Component {
uploadSuccess(url) {
const { props } = this;
props.api.chat.message(
props.station,
`~${window.ship}`,
Date.now(),
{ url }
);
if (this.state.uploadingPaste) {
this.chatEditor.current.editor.setValue(url);
this.setState({ uploadingPaste: false });
} else {
props.api.chat.message(
props.station,
`~${window.ship}`,
Date.now(),
{ url }
);
}
}
uploadError(error) {
// 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;
}
this.setState({ uploadingPaste: true });
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 { props, state } = this;
@ -143,7 +192,8 @@ export class ChatInput extends Component {
"pa3 cf flex black white-d bt b--gray4 b--gray1-d bg-white " +
"bg-gray0-d relative"
}
style={{ flexGrow: 1 }}>
style={{ flexGrow: 1 }}
>
<div className="fl"
style={{
marginTop: 6,
@ -153,11 +203,14 @@ export class ChatInput extends Component {
{avatar}
</div>
<ChatEditor
ref={this.chatEditor}
inCodeMode={state.inCodeMode}
submit={this.submit}
onUnmount={props.onUnmount}
message={props.message}
placeholder='Message...' />
onPaste={this.onPaste.bind(this)}
placeholder='Message...'
/>
<div className="ml2 mr2"
style={{
height: '16px',
@ -166,11 +219,12 @@ export class ChatInput extends Component {
marginTop: 10
}}>
<S3Upload
ref={this.s3Uploader}
configuration={props.s3.configuration}
credentials={props.s3.credentials}
uploadSuccess={this.uploadSuccess.bind(this)}
uploadError={this.uploadError.bind(this)}
accept="image/*"
accept="*"
>
<img
className="invert-d"
@ -187,7 +241,7 @@ export class ChatInput extends Component {
marginTop: 10
}}>
<img style={{
filter: state.inCodeMode && 'invert(100%)',
filter: state.inCodeMode ? 'invert(100%)' : '',
height: '14px',
width: '14px',
}}

View File

@ -1,153 +0,0 @@
import React, { Component } from 'react';
import { S3Upload } from '~/views/components/s3-upload';
import { Spinner } from '~/views/components/Spinner';
import { Icon } from "@tlon/indigo-react";
export class LinkSubmit extends Component {
constructor() {
super();
this.state = {
linkValue: '',
linkTitle: '',
linkValid: false,
submitFocus: false,
disabled: false
};
this.setLinkValue = this.setLinkValue.bind(this);
this.setLinkTitle = this.setLinkTitle.bind(this);
}
onClickPost() {
const link = this.state.linkValue;
const title = this.state.linkTitle
? this.state.linkTitle
: this.state.linkValue;
this.setState({ disabled: true });
this.props.api.links.postLink(this.props.resourcePath, link, title).then((r) => {
this.setState({
disabled: false,
linkValue: '',
linkTitle: '',
linkValid: false
});
});
}
setLinkValid(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}/
);
const validURL = URLparser.exec(link);
if (!validURL) {
const checkProtocol = URLparser.exec('http://' + link);
if (checkProtocol) {
this.setState({ linkValid: true });
this.setState({ linkValue: 'http://' + link });
} else {
this.setState({ linkValid: false });
}
} else if (validURL) {
this.setState({ linkValid: true });
}
}
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
}
render() {
console.log('s3', this.props.s3);
const activeClasses = (this.state.linkValid && !this.state.disabled)
? 'green2 pointer' : 'gray2';
const focus = (this.state.submitFocus)
? 'b--black b--white-d'
: 'b--gray4 b--gray2-d';
return (
<div className={'relative ba br1 w-100 mb6 ' + focus}>
<input
type="url"
className="pl2 bg-gray0-d white-d w-100 f8 pt2"
placeholder="Paste link here"
onChange={this.setLinkValue}
onBlur={() => this.setState({ submitFocus: false })}
onFocus={() => this.setState({ submitFocus: true })}
spellCheck="false"
rows={1}
onKeyPress={(e) => {
if (e.key === 'Enter') {
e.preventDefault();
this.onClickPost();
}
}}
value={this.state.linkValue}
/>
<div className="flex flex-auto mt2 pt2">
<input
type="text"
className="pl2 bg-gray0-d white-d w-100 f8"
style={{
resize: 'none',
height: 40
}}
placeholder="Enter title"
onChange={this.setLinkTitle}
onBlur={() => this.setState({ submitFocus: false })}
onFocus={() => this.setState({ submitFocus: true })}
spellCheck="false"
rows={1}
onKeyPress={(e) => {
if (e.key === 'Enter') {
e.preventDefault();
this.onClickPost();
}
}}
value={this.state.linkTitle}
/>
{!this.state.disabled ? <S3Upload
configuration={this.props.s3.configuration}
credentials={this.props.s3.credentials}
uploadSuccess={this.uploadSuccess.bind(this)}
uploadError={this.uploadError.bind(this)}
className="nowrap flex items-center pr2 h-100"
><span className="green2 f8">Upload File</span></S3Upload> : null}
{!this.state.disabled ? <button
className={
'bg-gray0-d f8 flex-shrink-0 pr2 ' + activeClasses
}
disabled={!this.state.linkValid || this.state.disabled}
onClick={this.onClickPost.bind(this)}
style={{
bottom: 12,
right: 8
}}
>
Post
</button> : null}
<Spinner awaiting={this.state.disabled} classes="nowrap flex items-center pr2" style={{flex: '1 1 14rem'}} text="Posting to collection..." />
</div>
</div>
) ;
}
}
export default LinkSubmit;

View File

@ -0,0 +1,255 @@
import React, { Component } from 'react';
import { hasProvider } from 'oembed-parser';
import { S3Upload, SubmitDragger } from '~/views/components/s3-upload';
import { Spinner } from '~/views/components/Spinner';
import { Icon } from "@tlon/indigo-react";
import GlobalApi from '~/logic/api/global';
import { S3State } from '~/types';
interface LinkSubmitProps {
api: GlobalApi;
resourcePath: string;
s3: S3State;
}
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 });
this.props.api.links.postLink(this.props.resourcePath, link, title).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.setState({ linkTitle: result.title });
}
}).catch((error) => {/*noop*/});
} else {
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 activeClasses = (this.state.linkValid && !this.state.disabled)
? 'green2 pointer' : 'gray2';
const focus = (this.state.submitFocus)
? 'b--black b--white-d'
: 'b--gray4 b--gray2-d';
return (
<div
className={`relative ba br1 w-100 mb6 ${focus}`}
onDragEnter={this.onDragEnter.bind(this)}
onDragOver={e => {e.preventDefault();this.setState({ dragover: true})}}
onDragLeave={() => this.setState({ dragover: false })}
onDrop={this.onDrop}
>
{this.state.dragover ? <SubmitDragger /> : null}
<div className="relative">
{(this.state.linkValue || this.state.urlFocus || this.state.disabled) ? null : <span className="gray2 absolute pl2 pt3 pb2 f8" style={{pointerEvents: 'none'}}>
Drop or <span className="pointer green2" style={{pointerEvents: 'all'}} onClick={(event) => {
if (!this.readyToUpload()) {
return;
}
this.s3Uploader.current.inputRef.current.click();
}}>upload</span> a file, or paste a link here
</span>}
{!this.state.disabled ? <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}
<input
type="url"
className="pl2 w-100 f8 pt3 pb2 white-d bg-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}
/>
</div>
<input
type="text"
className="pl2 bg-transparent w-100 f8 white-d"
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}
/>
{!this.state.disabled ? <button
className={
'bg-transparent f8 flex-shrink-0 pr2 pl2 pt2 pb3 ' + activeClasses
}
disabled={!this.state.linkValid || this.state.disabled}
onClick={this.onClickPost.bind(this)}
style={{
bottom: 12,
right: 8
}}
>
Post link
</button> : null}
<Spinner awaiting={this.state.disabled} classes="nowrap flex items-center pr2 pl2 pt2 pb4" style={{flex: '1 1 14rem'}} text="Posting to collection..." />
</div>
) ;
}
}
export default LinkSubmit;

View File

@ -1,24 +1,18 @@
import React, { Component } from 'react';
export class Spinner extends Component {
render() {
const classes = this.props.classes ? this.props.classes : '';
const text = this.props.text ? this.props.text : '';
const awaiting = this.props.awaiting ? this.props.awaiting : false;
const Spinner = ({
classes = '',
text = '',
awaiting = false
}) => awaiting ? (
<div className={classes + ' z-2 bg-white bg-gray0-d white-d flex'}>
<img className="invert-d spin-active v-mid"
src="/~landscape/img/Spinner.png"
width={16}
height={16}
/>
<p className="dib f9 ml2 v-mid inter">{text}</p>
</div>
) : null;
if (awaiting) {
return (
<div className={classes + ' z-2 bg-white bg-gray0-d white-d flex'}>
<img className="invert-d spin-active v-mid"
src="/~landscape/img/Spinner.png"
width={16}
height={16}
/>
<p className="dib f9 ml2 v-mid inter">{text}</p>
</div>
);
} else {
return null;
}
}
}
export { Spinner as default, Spinner };

View File

@ -1,118 +0,0 @@
import React, { Component } from 'react'
import { Icon } from "@tlon/indigo-react";
import S3Client from '~/logic/lib/s3';
import { Spinner } from './Spinner';
export class S3Upload extends Component {
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) {
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) {
const { props } = this;
this.setCredentials(props.credentials, props.configuration);
}
setCredentials(credentials, configuration) {
if (!this.isReady(credentials, configuration)) { return; }
this.s3.setCredentials(
credentials.endpoint,
credentials.accessKeyId,
credentials.secretAccessKey
);
}
getFileUrl(endpoint, filename) {
return endpoint + '/' + filename;
}
onChange() {
const { props } = this;
if (!this.inputRef.current) { return; }
let files = this.inputRef.current.files;
if (files.length <= 0) { return; }
let file = files.item(0);
let bucket = props.configuration.currentBucket;
this.setState({ isUploading: true });
this.s3.upload(bucket, file.name, 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 });
});
}
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 <div></div>;
} else {
let classes = !!className
? "pointer " + className
: "pointer";
const display = children || <Icon icon='ArrowNorth' />;
return (
<div>
<input
className="dn"
type="file"
id="fileElement"
ref={this.inputRef}
accept={accept}
onChange={this.onChange.bind(this)} />
{this.state.isUploading
? <Spinner awaiting={true} classes={className} />
: <span className={`pointer ${className}`} onClick={this.onClick.bind(this)}>{display}</span>
}
</div>
);
}
}
}

View File

@ -0,0 +1,142 @@
import React, { Component } from 'react'
import { Icon } from "@tlon/indigo-react";
import S3Client from '~/logic/lib/s3';
import { Spinner } from './Spinner';
import { S3Credentials, S3Configuration } from '~/types';
import { dateToDa, deSig } from '~/logic/lib/util';
export const SubmitDragger = () => (
<div
className="top-0 bottom-0 left-0 right-0 absolute bg-gray5 h-100 w-100 flex items-center justify-center z-999"
style={{pointerEvents: 'none'}}
>Drop a file to upload</div>
);
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;
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 });
});
}
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 (
<>
<input
className="dn"
type="file"
id="fileElement"
ref={this.inputRef}
accept={accept}
onChange={this.onChange.bind(this)} />
{this.state.isUploading
? <Spinner awaiting={true} classes={className} />
: <span className={`pointer ${className}`} onClick={this.onClick.bind(this)}>{display}</span>
}
</>
);
}
}