media object: adds PDF support, validation check to restrict certain uploads

This commit is contained in:
jimmylee 2020-08-01 23:00:04 -07:00
parent e85bd9b16a
commit 5d4f58eddc
11 changed files with 149 additions and 173 deletions

View File

@ -26,3 +26,15 @@ export const password = (text) => {
return true;
};
export const isFileTypeAllowed = (type = "") => {
if (type.startsWith("application/pdf")) {
return true;
}
if (type.startsWith("image/")) {
return true;
}
return false;
};

View File

@ -3,6 +3,7 @@ import * as NavigationData from "~/common/navigation-data";
import * as Actions from "~/common/actions";
import * as State from "~/common/state";
import * as Credentials from "~/common/credentials";
import * as Validations from "~/common/validations";
// NOTE(jim):
// Scenes each have an ID and can be navigated to with _handleAction
@ -11,7 +12,6 @@ import SceneEditAccount from "~/scenes/SceneEditAccount";
import SceneFile from "~/scenes/SceneFile";
import SceneFilesFolder from "~/scenes/SceneFilesFolder";
import SceneHome from "~/scenes/SceneHome";
import SceneMiners from "~/scenes/SceneMiners";
import SceneSettings from "~/scenes/SceneSettings";
import SceneWallet from "~/scenes/SceneWallet";
import SceneSlates from "~/scenes/SceneSlates";
@ -26,7 +26,6 @@ import SidebarCreateSlate from "~/components/sidebars/SidebarCreateSlate";
import SidebarCreateWalletAddress from "~/components/sidebars/SidebarCreateWalletAddress";
import SidebarWalletSendFunds from "~/components/sidebars/SidebarWalletSendFunds";
import SidebarFileStorageDeal from "~/components/sidebars/SidebarFileStorageDeal";
import SidebarCreatePaymentChannel from "~/components/sidebars/SidebarCreatePaymentChannel";
import SidebarAddFileToBucket from "~/components/sidebars/SidebarAddFileToBucket";
// NOTE(jim):
@ -68,7 +67,7 @@ export default class ApplicationPage extends React.Component {
this.setState({ fileLoading: true });
let data = new FormData();
data.append("image", file);
data.append("data", file);
const options = {
method: "POST",
@ -156,7 +155,9 @@ export default class ApplicationPage extends React.Component {
if (e.dataTransfer.items[i].kind === "file") {
var file = e.dataTransfer.items[i].getAsFile();
await this._handleSetFile({ file, slate });
if (Validations.isFileTypeAllowed(file.type)) {
await this._handleSetFile({ file, slate });
}
break;
}
}

View File

@ -0,0 +1,69 @@
import * as React from "react";
import * as Constants from "~/common/constants";
import { css } from "@emotion/react";
const STYLES_FAILURE = css`
background-color: ${Constants.system.pitchBlack};
color: ${Constants.system.white};
display: flex;
align-items: center;
justify-content: center;
font-size: 88px;
margin: 0;
padding: 0;
width: 100%;
min-height: 10%;
height: 100%;
`;
const STYLES_OBJECT = css`
display: block;
margin: 0;
padding: 0;
width: 100%;
min-height: 10%;
height: 100%;
`;
const STYLES_ASSET = css`
background-color: ${Constants.system.pitchBlack};
width: 100%;
margin: 0;
padding: 0;
min-height: 10%;
height: 100%;
display: flex;
align-items: center;
justify-content: center;
position: relative;
`;
const STYLES_IMAGE = css`
display: block;
max-width: 100%;
max-height: 100%;
`;
export default class MediaObject extends React.Component {
render() {
const name = `${this.props.data.name}`;
const url = this.props.data.url ? this.props.data.url : `https://hub.textile.io${this.props.data.ipfs}`;
let mediaElement = <div css={STYLES_FAILURE}>No Preview</div>;
if (this.props.data.type.startsWith("application/pdf")) {
mediaElement = <object css={STYLES_OBJECT} data={url} type={this.props.data.type} />;
}
if (this.props.data.type.startsWith("image/")) {
mediaElement = (
<div css={STYLES_ASSET}>
<img css={STYLES_IMAGE} src={url} />
</div>
);
}
return mediaElement;
}
}

View File

@ -1,8 +1,7 @@
import * as React from "react";
import * as Strings from "~/common/strings";
import * as Constants from "~/common/constants";
import * as SVG from "~/components/system/svg";
import * as System from "~/components/system";
import * as Validations from "~/common/validations";
import { css } from "@emotion/react";
@ -49,36 +48,31 @@ export default class SidebarAddFileToBucket extends React.Component {
let file = e.target.files[0];
if (!file) {
alert("Something went wrong");
alert("TODO: Something went wrong");
return;
}
const isAllowed = Validations.isFileTypeAllowed(file.type);
if (!isAllowed) {
alert("TODO: File type is not allowed, yet.");
return;
}
await this.props.onSetFile({
file,
slate:
this.props.data && this.props.data.slateId
? { id: this.props.data.slateId }
: null,
slate: this.props.data && this.props.data.slateId ? { id: this.props.data.slateId } : null,
});
};
render() {
return (
<React.Fragment>
<System.P style={{ fontFamily: Constants.font.semiBold }}>
Upload a file to Slate
</System.P>
<input
css={STYLES_FILE_HIDDEN}
type="file"
id="file"
onChange={this._handleUpload}
/>
<System.P style={{ fontFamily: Constants.font.semiBold }}>Upload a file to Slate</System.P>
<input css={STYLES_FILE_HIDDEN} type="file" id="file" onChange={this._handleUpload} />
{this.props.data && this.props.data.decorator === "SLATE" ? (
<System.P style={{ marginTop: 24 }}>
This will add an image to your Slate named{" "}
<strong>{this.props.data.slatename}</strong>.
This will add an image to your Slate named <strong>{this.props.data.slatename}</strong>.
</System.P>
) : null}
@ -86,16 +80,12 @@ export default class SidebarAddFileToBucket extends React.Component {
type="label"
htmlFor="file"
style={{ marginTop: 24 }}
loading={this.props.fileLoading}
>
loading={this.props.fileLoading}>
Add file
</System.ButtonPrimaryFull>
{!this.props.fileLoading ? (
<System.ButtonSecondaryFull
style={{ marginTop: 16 }}
onClick={this.props.onCancel}
>
<System.ButtonSecondaryFull style={{ marginTop: 16 }} onClick={this.props.onCancel}>
Cancel
</System.ButtonSecondaryFull>
) : null}

View File

@ -19,32 +19,30 @@ export const formMultipart = (req, res, { user }) =>
});
}
if (!files.image) {
console.log(files);
if (!files.data) {
return reject({
decorator: "SERVER_UPLOAD_NOT_IMAGE_TYPE",
decorator: "SERVER_UPLOAD_ERROR",
error: true,
message: files,
});
}
const path = files.image._writeStream.path;
const path = files.data._writeStream.path;
const localPath = `./${path}`;
const data = LibraryManager.createLocalDataIncomplete(files.image);
const data = LibraryManager.createLocalDataIncomplete(files.data);
const {
buckets,
bucketKey,
bucketName,
} = await Utilities.getBucketAPIFromUserToken(user.data.tokens.api);
const { buckets, bucketKey, bucketName } = await Utilities.getBucketAPIFromUserToken(user.data.tokens.api);
let readFile;
let push;
// TODO(jim): Send this file to buckets.
try {
// NOTE(jim): Push pathPath to your bucket.
readFile = await FS.readFileSync(path).buffer;
push = await buckets.pushPath(bucketKey, data.name, readFile);
} catch (e) {
await FS.unlinkSync(localPath);
return reject({
decorator: "SERVER_BUCKETS_PUSH_ISSUE",
error: true,

View File

@ -1,8 +1,6 @@
import * as MW from "~/node_common/middleware";
import * as Utilities from "~/node_common/utilities";
import * as Data from "~/node_common/data";
import * as Strings from "~/common/strings";
import * as Powergate from "~/node_common/powergate";
const initCORS = MW.init(MW.CORS);
const initAuth = MW.init(MW.RequireCookieAuthentication);
@ -13,9 +11,7 @@ export default async (req, res) => {
const id = Utilities.getIdFromCookie(req);
if (!id) {
return res
.status(403)
.json({ decorator: "SERVER_ADD_TO_SLATE_USER_NOT_FOUND", error: true });
return res.status(403).json({ decorator: "SERVER_ADD_TO_SLATE_USER_NOT_FOUND", error: true });
}
const user = await Data.getUserById({
@ -62,6 +58,7 @@ export default async (req, res) => {
id: req.body.data.id,
ownerId: user.id,
name: req.body.data.name,
type: req.body.data.type,
url: `https://hub.textile.io${req.body.data.ipfs}`,
},
...slate.data.objects,
@ -83,7 +80,5 @@ export default async (req, res) => {
});
}
return res
.status(200)
.json({ decorator: "SERVER_SLATE_ADD_TO_SLATE", slate });
return res.status(200).json({ decorator: "SERVER_SLATE_ADD_TO_SLATE", slate });
};

View File

@ -42,7 +42,13 @@ export default class SceneEditAccount extends React.Component {
let file = e.target.files[0];
if (!file) {
alert("Something went wrong");
alert("TODO: Something went wrong");
return;
}
if (!file.type.startsWith("image/")) {
alert("TODO: Error message for not an image.");
return;
}
let data = new FormData();
@ -144,25 +150,15 @@ export default class SceneEditAccount extends React.Component {
description="This image will appear in various lists."
/>
<Avatar
style={{ marginTop: 24 }}
size={256}
url={this.props.viewer.data.photo}
/>
<Avatar style={{ marginTop: 24 }} size={256} url={this.props.viewer.data.photo} />
<div style={{ marginTop: 24 }}>
<input
css={STYLES_FILE_HIDDEN}
type="file"
id="file"
onChange={this._handleUpload}
/>
<input css={STYLES_FILE_HIDDEN} type="file" id="file" onChange={this._handleUpload} />
<System.ButtonPrimary
style={{ margin: "0 16px 16px 0" }}
type="label"
htmlFor="file"
loading={this.state.changingAvatar}
>
loading={this.state.changingAvatar}>
Pick avatar
</System.ButtonPrimary>
</div>
@ -172,8 +168,7 @@ export default class SceneEditAccount extends React.Component {
label="Username"
description={
<React.Fragment>
This is your username on Slate. Your username is used for your
profile URL{" "}
This is your username on Slate. Your username is used for your profile URL{" "}
<a href={profileURL} target="_blank">
{profileURL}
</a>
@ -186,10 +181,7 @@ export default class SceneEditAccount extends React.Component {
/>
<div style={{ marginTop: 24 }}>
<System.ButtonPrimary
onClick={this._handleSave}
loading={this.state.changingUsername}
>
<System.ButtonPrimary onClick={this._handleSave} loading={this.state.changingUsername}>
Change username
</System.ButtonPrimary>
</div>
@ -221,10 +213,7 @@ export default class SceneEditAccount extends React.Component {
/>
<div style={{ marginTop: 24 }}>
<System.ButtonPrimary
onClick={this._handleChangePassword}
loading={this.state.changingPassword}
>
<System.ButtonPrimary onClick={this._handleChangePassword} loading={this.state.changingPassword}>
Change password
</System.ButtonPrimary>
</div>
@ -236,10 +225,7 @@ export default class SceneEditAccount extends React.Component {
/>
<div style={{ marginTop: 24 }}>
<System.ButtonPrimary
onClick={this._handleDelete}
loading={this.state.deleting}
>
<System.ButtonPrimary onClick={this._handleDelete} loading={this.state.deleting}>
Delete my account
</System.ButtonPrimary>
</div>

View File

@ -1,14 +1,10 @@
import * as React from "react";
import * as Strings from "~/common/strings";
import * as Constants from "~/common/constants";
import * as Fixtures from "~/common/fixtures";
import * as System from "~/components/system";
import * as SVG from "~/components/system/svg";
import { css } from "@emotion/react";
import Section from "~/components/core/Section";
import ScenePage from "~/components/core/ScenePage";
import MediaObject from "~/components/core/MediaObject";
const STYLES_FLEX = css`
display: flex;
@ -47,19 +43,6 @@ const STYLES_RIGHT = css`
}
`;
const STYLES_ASSET = css`
display: block;
width: 100%;
margin: 0;
padding: 0;
min-height: 10%;
height: 100%;
background-color: ${Constants.system.pitchBlack};
background-size: contain;
background-repeat: no-repeat;
background-position: 50% 50%;
`;
const STYLES_BOTTOM = css`
background: ${Constants.system.pitchBlack};
color: ${Constants.system.white};
@ -79,62 +62,21 @@ const STYLES_PATH = css`
overflow-wrap: break-word;
`;
const STYLES_ITEM = css`
border-radius: 4px;
outline: 0;
border: 0;
min-height: 32px;
padding: 6px 16px 6px 16px;
display: inline-flex;
align-items: center;
justify-content: center;
font-size: 12px;
letter-spacing: 0.2px;
font-family: ${Constants.font.semiBold};
transition: 200ms ease all;
cursor: pointer;
background-color: ${Constants.system.brand};
color: ${Constants.system.white};
margin-left: 16px;
:hover {
background-color: ${Constants.system.green};
}
:focus {
box-shadow: inset 0 0 5px 2px rgba(0, 0, 0, 0.3);
outline: 0;
border: 0;
}
`;
export default class SceneFile extends React.Component {
state = {};
_handleChange = (e) => {
this.setState({ [e.target.name]: e.target.value });
};
render() {
const file = this.props.data;
const fileName = `${file.name}`;
const fileURL = file.url ? file.url : `https://hub.textile.io${file.ipfs}`;
const fileURL = this.props.data.url ? this.props.data.url : `https://hub.textile.io${this.props.data.ipfs}`;
return (
<div css={STYLES_FLEX}>
<div css={STYLES_TOP}>
<div css={STYLES_LEFT}>
<span css={STYLES_PATH}>{fileName}</span>
<span css={STYLES_PATH}>{fileURL}</span>
</div>
<div css={STYLES_RIGHT} onClick={() => this.props.onBack()}>
<SVG.Dismiss height="24px" />
</div>
</div>
<div
css={STYLES_ASSET}
style={{ backgroundImage: `url('${fileURL}')` }}
/>
<MediaObject data={this.props.data} />
</div>
);
}

View File

@ -1,6 +1,4 @@
import * as React from "react";
import * as Strings from "~/common/strings";
import * as Constants from "~/common/constants";
import * as Actions from "~/common/actions";
import * as System from "~/components/system";
@ -30,13 +28,14 @@ export default class SceneFilesFolder extends React.Component {
});
console.log({ jobs });
if (jobs.length) {
const response = await Actions.checkCIDStatus(jobs);
const response = await Actions.checkCIDStatus(jobs);
console.log(response);
console.log(response);
if (response && response.update) {
await this.props.onRehydrate();
if (response && response.update) {
await this.props.onRehydrate();
}
}
if (this._interval) {
@ -63,7 +62,8 @@ export default class SceneFilesFolder extends React.Component {
const data = {
columns: [
{ key: "name", name: "File", type: "FILE_LINK" },
{ key: "name", name: "File", type: "FILE_LINK", width: "100%" },
{ key: "type", name: "Type" },
{
key: "size",
name: "Size",
@ -105,7 +105,7 @@ export default class SceneFilesFolder extends React.Component {
title={this.props.current.name}
buttons={[
{
name: "Upload to IPFS",
name: "Upload data",
type: "SIDEBAR",
value: "SIDEBAR_ADD_FILE_TO_BUCKET",
},

View File

@ -78,14 +78,13 @@ export default class SceneHome extends React.Component {
};
// TODO(jim): Refactor later.
const slateButtons = [
{ name: "Create slate", type: "SIDEBAR", value: "SIDEBAR_CREATE_SLATE" },
];
const slateButtons = [{ name: "Create slate", type: "SIDEBAR", value: "SIDEBAR_CREATE_SLATE" }];
// TODO(jim): Refactor later.
const data = {
columns: [
{ key: "name", name: "Data", type: "FILE_LINK" },
{ key: "name", name: "Data", type: "FILE_LINK", width: "100%" },
{ key: "type", name: "Type" },
{
key: "size",
name: "Size",
@ -97,14 +96,13 @@ export default class SceneHome extends React.Component {
name: "Date uploaded",
width: "160px",
type: "FILE_DATE",
tooltip:
"This date represents when the file was first uploaded to IPFS.",
},
{
key: "networks",
name: "Network",
type: "NETWORK_TYPE",
width: "188px",
tooltip: "This data is publicly available to share on the internet!",
},
],
rows: this.props.viewer.library[0].children.map((each) => {
@ -123,7 +121,7 @@ export default class SceneHome extends React.Component {
value: this.props.viewer.library[0].id,
},
{
name: "Upload to IPFS",
name: "Upload data",
type: "SIDEBAR",
value: "SIDEBAR_ADD_FILE_TO_BUCKET",
},
@ -152,11 +150,7 @@ export default class SceneHome extends React.Component {
<ScenePage>
<System.H1>Home</System.H1>
{this.props.viewer.addresses[0] ? (
<Section
title="Wallet addresses"
buttons={walletButtons}
onAction={this.props.onAction}
>
<Section title="Wallet addresses" buttons={walletButtons} onAction={this.props.onAction}>
<System.Table
data={wallet}
name="transaction"
@ -166,11 +160,7 @@ export default class SceneHome extends React.Component {
</Section>
) : null}
<Section
title="Slates"
buttons={slateButtons}
onAction={this.props.onAction}
>
<Section title="Slates" buttons={slateButtons} onAction={this.props.onAction}>
<System.Table
data={slates}
name="slate"
@ -180,11 +170,7 @@ export default class SceneHome extends React.Component {
</Section>
{this.props.viewer.library[0] ? (
<Section
title="Recent data"
buttons={dataButtons}
onAction={this.props.onAction}
>
<Section title="Recent data" buttons={dataButtons} onAction={this.props.onAction}>
<System.Table
data={data}
name="data"

View File

@ -15,6 +15,7 @@ export default class SceneSlate extends React.Component {
const slates = {
columns: [
{ key: "name", name: "Data", type: "FILE_LINK", width: "288px" },
{ key: "type", name: "Data type" },
{ key: "url", name: "Asset URL", width: "100%" },
],
rows: images,
@ -45,11 +46,7 @@ export default class SceneSlate extends React.Component {
<System.H1>
https://slate.host/@{this.props.viewer.username}/{slatename}
</System.H1>
<Section
title="Images"
buttons={slateButtons}
onAction={this.props.onAction}
>
<Section title="Images" buttons={slateButtons} onAction={this.props.onAction}>
<System.Table
data={slates}
name={`/@${this.props.viewer.username}/${slatename}`}