mirror of
https://github.com/urbit/shrub.git
synced 2024-12-01 14:42:02 +03:00
links, chat: add drag and drop
This commit is contained in:
parent
0ddb4a4887
commit
c9aa908838
@ -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}
|
||||
|
@ -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>
|
||||
);
|
||||
|
@ -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',
|
||||
}}
|
@ -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;
|
@ -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;
|
@ -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 };
|
@ -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>
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
142
pkg/interface/src/views/components/s3-upload.tsx
Normal file
142
pkg/interface/src/views/components/s3-upload.tsx
Normal 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>
|
||||
}
|
||||
</>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user