diff --git a/src/assets/stylesheets/info-dialog.scss b/src/assets/stylesheets/info-dialog.scss
index 11c79e6de8bb8a08692be706a96b4216c77ca894..7b55cdab85c45eec62cd81d61876615abe616830 100644
--- a/src/assets/stylesheets/info-dialog.scss
+++ b/src/assets/stylesheets/info-dialog.scss
@@ -82,6 +82,32 @@
   text-align: center;
   margin: 0;
 
+  &__input_fields {
+      position: relative;
+  }
+
+  &__file {
+      width: 0.1px;
+      height: 0.1px;
+      opacity: 0;
+      overflow: hidden;
+      position: absolute;
+      z-index: -1;
+  }
+
+  &__file_label {
+      font-size: 1.25em;
+      font-weight: 700;
+      color: white;
+      background-color: black;
+      display: inline-block;
+      top: 19px;
+      right: 24px;
+      bottom: 19px;
+      position: absolute;
+      line-height: 30px;
+  }
+
   &__buttons {
     display: flex;
     flex-direction: row;
@@ -101,7 +127,7 @@
     background-color: transparent;
     line-height: 2.0em;
     padding-left: 1.25em;
-    padding-right: 1.25em;
+    padding-right: 2.25em;
     margin: 0.5em 0;
     width: 100%;
   }
diff --git a/src/components/media-loader.js b/src/components/media-loader.js
index 544d4443a4304e0ed1d3ad75523c89cafa46c979..1ae37f321a5374919e9dc5fedd51ab722b245ffa 100644
--- a/src/components/media-loader.js
+++ b/src/components/media-loader.js
@@ -6,6 +6,8 @@ const fetchContentType = async url => fetch(url, { method: "HEAD" }).then(r => r
 AFRAME.registerComponent("media-loader", {
   schema: {
     src: { type: "string" },
+    token: { type: "string" },
+    contentType: { type: "string" },
     resize: { default: false }
   },
 
@@ -46,17 +48,23 @@ AFRAME.registerComponent("media-loader", {
   async update() {
     try {
       const url = this.data.src;
+      const token = this.data.token;
 
-      this.showLoaderTimeout = setTimeout(() => {
-        const loadingObj = new THREE.Mesh(new THREE.BoxGeometry(), new THREE.MeshBasicMaterial());
-        this.el.setObject3D("mesh", loadingObj);
-        this.setShapeAndScale(true);
-      }, 100);
+      this.showLoaderTimeout =
+        this.showLoaderTimeout ||
+        setTimeout(() => {
+          const loadingObj = new THREE.Mesh(new THREE.BoxGeometry(), new THREE.MeshBasicMaterial());
+          this.el.setObject3D("mesh", loadingObj);
+          this.setShapeAndScale(true);
+        }, 100);
+
+      if (!url) return;
 
       const { raw, origin, meta } = await resolveMedia(url);
       console.log("resolved", url, raw, origin, meta);
 
-      const contentType = (meta && meta.expected_content_type) || (await fetchContentType(raw));
+      const contentType =
+        this.data.contentType || (meta && meta.expected_content_type) || (await fetchContentType(raw));
       if (contentType.startsWith("image/") || contentType.startsWith("video/") || contentType.startsWith("audio/")) {
         this.el.addEventListener(
           "image-loaded",
@@ -65,7 +73,16 @@ AFRAME.registerComponent("media-loader", {
           },
           { once: true }
         );
-        this.el.setAttribute("image-plus", { src: raw, contentType });
+        let blobUrl;
+        if (token) {
+          const imageResponse = await fetch(raw, {
+            method: "GET",
+            headers: { Authorization: `Token ${token}` }
+          });
+          const blob = await imageResponse.blob();
+          blobUrl = window.URL.createObjectURL(blob);
+        }
+        this.el.setAttribute("image-plus", { src: blobUrl || raw, contentType });
         this.el.setAttribute("position-at-box-shape-border", { target: ".delete-button", dirs: ["forward", "back"] });
       } else if (contentType.startsWith("model/gltf") || url.endsWith(".gltf") || url.endsWith(".glb")) {
         this.el.addEventListener(
diff --git a/src/hub.js b/src/hub.js
index 3b4744abf0bd38a7361d6640aa3b5a6d878ccd5d..ac0acb54b32ee72454010a78e36ff989d79f2eb8 100644
--- a/src/hub.js
+++ b/src/hub.js
@@ -83,7 +83,7 @@ import HubChannel from "./utils/hub-channel";
 import LinkChannel from "./utils/link-channel";
 import { connectToReticulum } from "./utils/phoenix-utils";
 import { disableiOSZoom } from "./utils/disable-ios-zoom";
-import { addMedia } from "./utils/media-utils";
+import { addMedia} from "./utils/media-utils";
 
 import "./systems/personal-space-bubble";
 import "./systems/app-mode";
@@ -303,8 +303,8 @@ const onReady = async () => {
     });
 
     const offset = { x: 0, y: 0, z: -1.5 };
-    const spawnMediaInfrontOfPlayer = url => {
-      const entity = addMedia(url, true);
+    const spawnMediaInfrontOfPlayer = async (src) => {
+      const entity = addMedia(src, true);
       entity.setAttribute("offset-relative-to", {
         target: "#player-camera",
         offset
@@ -330,11 +330,9 @@ const onReady = async () => {
 
       document.addEventListener("drop", e => {
         e.preventDefault();
-        const imgUrl = e.dataTransfer.getData("url");
-        if (imgUrl) {
-          console.log("Dropped: ", imgUrl);
-          spawnMediaInfrontOfPlayer(imgUrl);
-        }
+        const url = e.dataTransfer.getData("url");
+        const file = e.dataTransfer.files && e.dataTransfer.files[0];
+        spawnMediaInfrontOfPlayer(url || file);
       });
     }
 
diff --git a/src/react-components/create-object-dialog.js b/src/react-components/create-object-dialog.js
index f450f6f478e0907e8ebebcbde204be52260c5b1b..a7223d468668825d7c8771168e9703c427711cdd 100644
--- a/src/react-components/create-object-dialog.js
+++ b/src/react-components/create-object-dialog.js
@@ -13,7 +13,9 @@ let lastUrl = "";
 
 export default class CreateObjectDialog extends Component {
   state = {
-    url: ""
+    url: "",
+    file: null,
+    text: ""
   };
 
   static propTypes = {
@@ -35,13 +37,25 @@ export default class CreateObjectDialog extends Component {
     if (e && e.target.value && e.target.value !== "") {
       this.setState({
         url: e.target.value,
+        text: e.target.value,
         attributionImage: e.target.validity.valid && attributionHostnames[new URL(e.target.value).hostname]
       });
+    } else {
+      this.setState({
+        text: ""
+      });
     }
   };
 
+  onFileChange = (e) =>{
+    this.setState({
+      file: e.target.files[0],
+      text: e.target.files[0].name
+    });
+  };
+
   onCreateClicked = () => {
-    this.props.onCreateObject(this.state.url || DEFAULT_OBJECT_URL);
+    this.props.onCreateObject(this.state.file || this.state.url || DEFAULT_OBJECT_URL);
     this.props.onCloseDialog();
   };
 
@@ -60,14 +74,22 @@ export default class CreateObjectDialog extends Component {
 
         <form onSubmit={this.onCreateClicked}>
           <div className="add-media-form">
-            <input
-              ref={el => (this.input = el)}
-              type="url"
-              placeholder="Image, Video, or GLTF URL"
-              className="add-media-form__link_field"
-              value={this.state.url}
-              onChange={this.onUrlChange}
-            />
+            <div className="add-media-form__input_fields">
+              <input
+                ref={el => (this.input = el)}
+                type={this.state.file ? "text" : "url"}
+                placeholder="Image, Video, or GLTF URL"
+                className="add-media-form__link_field"
+                value={this.state.text}
+                onChange={this.onUrlChange}
+              />
+              <input className="add-media-form__file"
+                id="file"
+                type="file"
+                onChange={this.onFileChange}
+              />
+              <label className="add-media-form__file_label" htmlFor="file">Choose a file</label>
+            </div>
             <div className="add-media-form__buttons">
               <button className="add-media-form__action-button">
                 <span>create</span>
diff --git a/src/utils/media-utils.js b/src/utils/media-utils.js
index e864bfc9fd979ef42b4fd4083cd67325e3465b3a..b21095e772d09954a4e880d31def490c2efad233 100644
--- a/src/utils/media-utils.js
+++ b/src/utils/media-utils.js
@@ -29,7 +29,31 @@ export const addMedia = (src, resize = false) => {
   const entity = document.createElement("a-entity");
   entity.id = "interactable-media-" + interactableId++;
   entity.setAttribute("networked", { template: "#interactable-media" });
-  entity.setAttribute("media-loader", { src, resize });
+  entity.setAttribute("media-loader", { resize, src: typeof src === "string" ? src : "" });
   scene.appendChild(entity);
+
+  if (typeof src === "object") {
+    const uploadResponse = upload(src).then(response => {
+      const src = response.raw;
+      const contentType = response.meta.expected_content_type;
+      const token = response.meta.access_token;
+      entity.setAttribute("media-loader", { src, contentType, token });
+    });
+  }
   return entity;
 };
+
+export const upload = file => {
+  const formData = new FormData();
+  formData.append("media", file);
+  return fetch(mediaAPIEndpoint, {
+    method: "POST",
+    body: formData
+
+    // We do NOT specify a Content-Type header like so
+    //     headers: { "Content-Type" : "multipart/form-data" },
+    // because we want the browser to automatically add
+    //     "Content-Type" : "multipart/form-data; boundary=...--------------<boundary_size>",
+    // See https://stanko.github.io/uploading-files-using-fetch-multipart-form-data/ for details.
+  }).then(r => r.json());
+};