interface: added oembeds and drag and drop back

This commit is contained in:
Logan Allen 2020-09-24 16:48:23 -05:00
parent 19b5d8cbfc
commit 528e4d345f
6 changed files with 332 additions and 134 deletions

View File

@ -160,6 +160,7 @@ export default class LinksApp extends Component {
<LinkList
{...props}
api={api}
s3={s3}
graph={graph}
graphResource={graphKeys.has(resourcePath)}
resourcePath={resourcePath}

View File

@ -1,21 +1,39 @@
import React from 'react';
import { cite } from '~/logic/lib/util';
import RemoteContent from "~/views/components/RemoteContent";
import moment from 'moment';
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}/
);
export const LinkPreview = (props) => {
const showNickname = props.nickname && !props.hideNicknames;
const nameClass = showNickname ? 'inter' : 'mono';
const author = props.post.author;
const title = props.post.contents[0].text;
const url = props.post.contents[1].url;
const hostname = URLparser.exec(url) ? URLparser.exec(url)[4] : null;
const timeSent =
moment.unix(props.post['time-sent'] / 1000).format('hh:mm a');
const title = props.post.contents[0].text;
const url = props.post.contents[1].url;
const embed = (
<RemoteContent
unfold={true}
renderUrl={false}
url={url}
remoteContentPolicy={props.remoteContentPolicy}
className="mw-100"
/>
);
return (
<div className="pb6 w-100">
<div className='w-100 tc'>{embed}</div>
<div className="flex flex-column ml2 pt6 flex-auto">
<a href={url}
className="w-100 flex"
@ -23,7 +41,7 @@ export const LinkPreview = (props) => {
rel="noopener noreferrer">
<p className="f8 truncate">{title}</p>
<span className="gray2 ml2 f8 dib v-btm flex-shrink-0">
{url}
{hostname}
</span>
</a>
<div className="w-100 pt1">

View File

@ -1,126 +0,0 @@
import React, { Component } from 'react';
import { Spinner } from '~/views/components/Spinner';
import { createPost } from '~/logic/api/graph';
export class LinkSubmit extends Component {
constructor() {
super();
this.state = {
linkValue: '',
linkTitle: '',
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;
const parentIndex = this.props.parentIndex || '';
let post = createPost([
{ text: title },
{ url: link }
], parentIndex);
this.setState({ disabled: true }, () => {
this.props.api.graph.addPost(
`~${this.props.ship}`,
this.props.name,
post
).then((r) => {
this.setState({
disabled: false,
linkValue: '',
linkTitle: '',
});
});
});
}
setLinkValue(event) {
this.setState({ linkValue: event.target.value });
}
setLinkTitle(event) {
this.setState({ linkTitle: event.target.value });
}
render() {
const activeClasses = (!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}>
<textarea
className="pl2 bg-gray0-d white-d w-100 f8"
style={{
resize: 'none',
height: 40,
paddingTop: 10
}}
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}
/>
<textarea
className="pl2 bg-gray0-d white-d w-100 f8"
style={{
resize: 'none',
height: 40,
paddingTop: 16
}}
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}
/>
<button
className={
'absolute bg-gray0-d f8 ml2 flex-shrink-0 ' + activeClasses
}
disabled={this.state.disabled}
onClick={this.onClickPost.bind(this)}
style={{
bottom: 12,
right: 8
}}
>
Post
</button>
<Spinner
awaiting={this.state.disabled}
classes="mt3 absolute right-0"
text="Posting to collection..." />
</div>
);
}
}

View File

@ -0,0 +1,302 @@
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';
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 || '';
let 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.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';
console.log(this.props.s3);
const isS3Ready =
( this.props.s3.secretAccessKey &&
this.props.s3.endpoint &&
this.props.s3.accessKeyId
);
return (
<div
className={`relative ba br1 w-100 mb6 ${focus}`}
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}
<div className="relative">
{
( this.state.linkValue ||
this.state.urlFocus ||
this.state.disabled
) ? null : (
isS3Ready ? (
<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>
) : (
<span className="gray2 absolute pl2 pt3 pb2 f8"
style={{pointerEvents: 'none'}}>
Paste a link here
</span>
)
)
}
{!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}
<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

@ -70,7 +70,9 @@ export const LinkDetail = (props) => {
post={props.node.post}
nickname={nickname}
hideNicknames={props.hideNicknames}
commentNumber={props.node.children.size} />
commentNumber={props.node.children.size}
remoteContentPolicy={props.remoteContentPolicy}
/>
<div className="flex">
<CommentSubmit
name={props.name}

View File

@ -4,7 +4,7 @@ import { TabBar } from '~/views/components/chat-link-tabbar';
import { SidebarSwitcher } from '~/views/components/SidebarSwitch';
import { Link } from 'react-router-dom';
import { LinkItem } from './lib/link-item';
import { LinkSubmit } from './lib/link-submit';
import LinkSubmit from './lib/link-submit';
import { getContactDetails } from '~/logic/lib/util';
@ -66,7 +66,8 @@ export const LinkList = (props) => {
<LinkSubmit
name={props.name}
ship={props.ship}
api={props.api} />
api={props.api}
s3={props.s3} />
</div>
{ Array.from(props.graph.values()).map((node) => {
const { nickname, color, avatar } =