]> git.mxchange.org Git - friendica.git/commitdiff
first cut at ajax photo upload
authorMike Macgirvin <mike@macgirvin.com>
Fri, 23 Jul 2010 03:22:03 +0000 (20:22 -0700)
committerMike Macgirvin <mike@macgirvin.com>
Fri, 23 Jul 2010 03:22:03 +0000 (20:22 -0700)
.gitignore
images/camera-icon.gif [new file with mode: 0644]
images/link-icon.gif [new file with mode: 0644]
include/ajaxupload.js [new file with mode: 0644]
mod/wall_upload.php [new file with mode: 0644]
view/jot-header.tpl
view/jot.tpl
view/style.css
wip/todo [deleted file]

index a69789254f70bb08d960d2733616e6476470d2fe..6bdf5927dd038235267dc9db424c4f5bdffb2232 100644 (file)
@@ -1,4 +1,6 @@
 .htconfig.php
 \#*
-wip
-include/jquery-1.4.2.min.js
\ No newline at end of file
+wip/*
+include/jquery-1.4.2.min.js
+*.log
+*.out
diff --git a/images/camera-icon.gif b/images/camera-icon.gif
new file mode 100644 (file)
index 0000000..83c7124
Binary files /dev/null and b/images/camera-icon.gif differ
diff --git a/images/link-icon.gif b/images/link-icon.gif
new file mode 100644 (file)
index 0000000..c012d71
Binary files /dev/null and b/images/link-icon.gif differ
diff --git a/include/ajaxupload.js b/include/ajaxupload.js
new file mode 100644 (file)
index 0000000..f0fbfe6
--- /dev/null
@@ -0,0 +1,691 @@
+/**
+ * AJAX Upload ( http://valums.com/ajax-upload/ ) 
+ * Copyright (c) Andris Valums
+ * Licensed under the MIT license ( http://valums.com/mit-license/ )
+ * Thanks to Gary Haran, David Mark, Corey Burns and others for contributions. 
+ */
+(function () {
+    /* global window */
+    /* jslint browser: true, devel: true, undef: true, nomen: true, bitwise: true, regexp: true, newcap: true, immed: true */
+    
+    /**
+     * Wrapper for FireBug's console.log
+     */
+    function log(){
+        if (typeof(console) != 'undefined' && typeof(console.log) == 'function'){            
+            Array.prototype.unshift.call(arguments, '[Ajax Upload]');
+            console.log( Array.prototype.join.call(arguments, ' '));
+        }
+    } 
+
+    /**
+     * Attaches event to a dom element.
+     * @param {Element} el
+     * @param type event name
+     * @param fn callback This refers to the passed element
+     */
+    function addEvent(el, type, fn){
+        if (el.addEventListener) {
+            el.addEventListener(type, fn, false);
+        } else if (el.attachEvent) {
+            el.attachEvent('on' + type, function(){
+                fn.call(el);
+               });
+           } else {
+            throw new Error('not supported or DOM not loaded');
+        }
+    }   
+    
+    /**
+     * Attaches resize event to a window, limiting
+     * number of event fired. Fires only when encounteres
+     * delay of 100 after series of events.
+     * 
+     * Some browsers fire event multiple times when resizing
+     * http://www.quirksmode.org/dom/events/resize.html
+     * 
+     * @param fn callback This refers to the passed element
+     */
+    function addResizeEvent(fn){
+        var timeout;
+               
+           addEvent(window, 'resize', function(){
+            if (timeout){
+                clearTimeout(timeout);
+            }
+            timeout = setTimeout(fn, 100);                        
+        });
+    }    
+    
+    // Needs more testing, will be rewriten for next version        
+    // getOffset function copied from jQuery lib (http://jquery.com/)
+    if (document.documentElement.getBoundingClientRect){
+        // Get Offset using getBoundingClientRect
+        // http://ejohn.org/blog/getboundingclientrect-is-awesome/
+        var getOffset = function(el){
+            var box = el.getBoundingClientRect();
+            var doc = el.ownerDocument;
+            var body = doc.body;
+            var docElem = doc.documentElement; // for ie 
+            var clientTop = docElem.clientTop || body.clientTop || 0;
+            var clientLeft = docElem.clientLeft || body.clientLeft || 0;
+             
+            // In Internet Explorer 7 getBoundingClientRect property is treated as physical,
+            // while others are logical. Make all logical, like in IE8.        
+            var zoom = 1;            
+            if (body.getBoundingClientRect) {
+                var bound = body.getBoundingClientRect();
+                zoom = (bound.right - bound.left) / body.clientWidth;
+            }
+            
+            if (zoom > 1) {
+                clientTop = 0;
+                clientLeft = 0;
+            }
+            
+            var top = box.top / zoom + (window.pageYOffset || docElem && docElem.scrollTop / zoom || body.scrollTop / zoom) - clientTop, left = box.left / zoom + (window.pageXOffset || docElem && docElem.scrollLeft / zoom || body.scrollLeft / zoom) - clientLeft;
+            
+            return {
+                top: top,
+                left: left
+            };
+        };        
+    } else {
+        // Get offset adding all offsets 
+        var getOffset = function(el){
+            var top = 0, left = 0;
+            do {
+                top += el.offsetTop || 0;
+                left += el.offsetLeft || 0;
+                el = el.offsetParent;
+            } while (el);
+            
+            return {
+                left: left,
+                top: top
+            };
+        };
+    }
+    
+    /**
+     * Returns left, top, right and bottom properties describing the border-box,
+     * in pixels, with the top-left relative to the body
+     * @param {Element} el
+     * @return {Object} Contains left, top, right,bottom
+     */
+    function getBox(el){
+        var left, right, top, bottom;
+        var offset = getOffset(el);
+        left = offset.left;
+        top = offset.top;
+        
+        right = left + el.offsetWidth;
+        bottom = top + el.offsetHeight;
+        
+        return {
+            left: left,
+            right: right,
+            top: top,
+            bottom: bottom
+        };
+    }
+    
+    /**
+     * Helper that takes object literal
+     * and add all properties to element.style
+     * @param {Element} el
+     * @param {Object} styles
+     */
+    function addStyles(el, styles){
+        for (var name in styles) {
+            if (styles.hasOwnProperty(name)) {
+                el.style[name] = styles[name];
+            }
+        }
+    }
+        
+    /**
+     * Function places an absolutely positioned
+     * element on top of the specified element
+     * copying position and dimentions.
+     * @param {Element} from
+     * @param {Element} to
+     */    
+    function copyLayout(from, to){
+           var box = getBox(from);
+        
+        addStyles(to, {
+               position: 'absolute',                    
+               left : box.left + 'px',
+               top : box.top + 'px',
+               width : from.offsetWidth + 'px',
+               height : from.offsetHeight + 'px'
+           });        
+       to.title = from.title;
+    }
+
+    /**
+    * Creates and returns element from html chunk
+    * Uses innerHTML to create an element
+    */
+    var toElement = (function(){
+        var div = document.createElement('div');
+        return function(html){
+            div.innerHTML = html;
+            var el = div.firstChild;
+            return div.removeChild(el);
+        };
+    })();
+            
+    /**
+     * Function generates unique id
+     * @return unique id 
+     */
+    var getUID = (function(){
+        var id = 0;
+        return function(){
+            return 'ValumsAjaxUpload' + id++;
+        };
+    })();        
+    /**
+     * Get file name from path
+     * @param {String} file path to file
+     * @return filename
+     */  
+    function fileFromPath(file){
+        return file.replace(/.*(\/|\\)/, "");
+    }
+    
+    /**
+     * Get file extension lowercase
+     * @param {String} file name
+     * @return file extenstion
+     */    
+    function getExt(file){
+        return (-1 !== file.indexOf('.')) ? file.replace(/.*[.]/, '') : '';
+    }
+
+    function hasClass(el, name){        
+        var re = new RegExp('\\b' + name + '\\b');        
+        return re.test(el.className);
+    }    
+    function addClass(el, name){
+        if ( ! hasClass(el, name)){   
+            el.className += ' ' + name;
+        }
+    }    
+    function removeClass(el, name){
+        var re = new RegExp('\\b' + name + '\\b');                
+        el.className = el.className.replace(re, '');        
+    }
+    
+    function removeNode(el){
+        el.parentNode.removeChild(el);
+    }
+
+    /**
+     * Easy styling and uploading
+     * @constructor
+     * @param button An element you want convert to 
+     * upload button. Tested dimentions up to 500x500px
+     * @param {Object} options See defaults below.
+     */
+    window.AjaxUpload = function(button, options){
+        this._settings = {
+            // Location of the server-side upload script
+            action: 'upload.php',
+            // File upload name
+            name: 'userfile',
+            // Additional data to send
+            data: {},
+            // Submit file as soon as it's selected
+            autoSubmit: true,
+            // The type of data that you're expecting back from the server.
+            // html and xml are detected automatically.
+            // Only useful when you are using json data as a response.
+            // Set to "json" in that case. 
+            responseType: false,
+            // Class applied to button when mouse is hovered
+            hoverClass: 'hover',
+            // Class applied to button when button is focused
+            focusClass: 'focus',
+            // Class applied to button when AU is disabled
+            disabledClass: 'disabled',            
+            // When user selects a file, useful with autoSubmit disabled
+            // You can return false to cancel upload                   
+            onChange: function(file, extension){
+            },
+            // Callback to fire before file is uploaded
+            // You can return false to cancel upload
+            onSubmit: function(file, extension){
+            },
+            // Fired when file upload is completed
+            // WARNING! DO NOT USE "FALSE" STRING AS A RESPONSE!
+            onComplete: function(file, response){
+            }
+        };
+                        
+        // Merge the users options with our defaults
+        for (var i in options) {
+            if (options.hasOwnProperty(i)){
+                this._settings[i] = options[i];
+            }
+        }
+                
+        // button isn't necessary a dom element
+        if (button.jquery){
+            // jQuery object was passed
+            button = button[0];
+        } else if (typeof button == "string") {
+            if (/^#.*/.test(button)){
+                // If jQuery user passes #elementId don't break it                                     
+                button = button.slice(1);                
+            }
+            
+            button = document.getElementById(button);
+        }
+        
+        if ( ! button || button.nodeType !== 1){
+            throw new Error("Please make sure that you're passing a valid element"); 
+        }
+                
+        if ( button.nodeName.toUpperCase() == 'A'){
+            // disable link                       
+            addEvent(button, 'click', function(e){
+                if (e && e.preventDefault){
+                    e.preventDefault();
+                } else if (window.event){
+                    window.event.returnValue = false;
+                }
+            });
+        }
+                    
+        // DOM element
+        this._button = button;        
+        // DOM element                 
+        this._input = null;
+        // If disabled clicking on button won't do anything
+        this._disabled = false;
+        
+        // if the button was disabled before refresh if will remain
+        // disabled in FireFox, let's fix it
+        this.enable();        
+        
+        this._rerouteClicks();
+    };
+    
+    // assigning methods to our class
+    AjaxUpload.prototype = {
+        setData: function(data){
+            this._settings.data = data;
+        },
+        disable: function(){            
+            addClass(this._button, this._settings.disabledClass);
+            this._disabled = true;
+            
+            var nodeName = this._button.nodeName.toUpperCase();            
+            if (nodeName == 'INPUT' || nodeName == 'BUTTON'){
+                this._button.setAttribute('disabled', 'disabled');
+            }            
+            
+            // hide input
+            if (this._input){
+                // We use visibility instead of display to fix problem with Safari 4
+                // The problem is that the value of input doesn't change if it 
+                // has display none when user selects a file           
+                this._input.parentNode.style.visibility = 'hidden';
+            }
+        },
+        enable: function(){
+            removeClass(this._button, this._settings.disabledClass);
+            this._button.removeAttribute('disabled');
+            this._disabled = false;
+            
+        },
+        /**
+         * Creates invisible file input 
+         * that will hover above the button
+         * <div><input type='file' /></div>
+         */
+        _createInput: function(){ 
+            var self = this;
+                        
+            var input = document.createElement("input");
+            input.setAttribute('type', 'file');
+            input.setAttribute('name', this._settings.name);
+            
+            addStyles(input, {
+                'position' : 'absolute',
+                // in Opera only 'browse' button
+                // is clickable and it is located at
+                // the right side of the input
+                'right' : 0,
+                'margin' : 0,
+                'padding' : 0,
+                'fontSize' : '480px',
+                // in Firefox if font-family is set to
+                // 'inherit' the input doesn't work
+                'fontFamily' : 'sans-serif',
+                'cursor' : 'pointer'
+            });            
+
+            var div = document.createElement("div");                        
+            addStyles(div, {
+                'display' : 'block',
+                'position' : 'absolute',
+                'overflow' : 'hidden',
+                'margin' : 0,
+                'padding' : 0,                
+                'opacity' : 0,
+                // Make sure browse button is in the right side
+                // in Internet Explorer
+                'direction' : 'ltr',
+                //Max zIndex supported by Opera 9.0-9.2
+                'zIndex': 2147483583
+            });
+            
+            // Make sure that element opacity exists.
+            // Otherwise use IE filter            
+            if ( div.style.opacity !== "0") {
+                if (typeof(div.filters) == 'undefined'){
+                    throw new Error('Opacity not supported by the browser');
+                }
+                div.style.filter = "alpha(opacity=0)";
+            }            
+            
+            addEvent(input, 'change', function(){
+                 
+                if ( ! input || input.value === ''){                
+                    return;                
+                }
+                            
+                // Get filename from input, required                
+                // as some browsers have path instead of it          
+                var file = fileFromPath(input.value);
+                                
+                if (false === self._settings.onChange.call(self, file, getExt(file))){
+                    self._clearInput();                
+                    return;
+                }
+                
+                // Submit form when value is changed
+                if (self._settings.autoSubmit) {
+                    self.submit();
+                }
+            });            
+
+            addEvent(input, 'mouseover', function(){
+                addClass(self._button, self._settings.hoverClass);
+            });
+            
+            addEvent(input, 'mouseout', function(){
+                removeClass(self._button, self._settings.hoverClass);
+                removeClass(self._button, self._settings.focusClass);
+                
+                // We use visibility instead of display to fix problem with Safari 4
+                // The problem is that the value of input doesn't change if it 
+                // has display none when user selects a file           
+                input.parentNode.style.visibility = 'hidden';
+
+            });   
+                        
+            addEvent(input, 'focus', function(){
+                addClass(self._button, self._settings.focusClass);
+            });
+            
+            addEvent(input, 'blur', function(){
+                removeClass(self._button, self._settings.focusClass);
+            });
+            
+               div.appendChild(input);
+            document.body.appendChild(div);
+              
+            this._input = input;
+        },
+        _clearInput : function(){
+            if (!this._input){
+                return;
+            }            
+                             
+            // this._input.value = ''; Doesn't work in IE6                               
+            removeNode(this._input.parentNode);
+            this._input = null;                                                                   
+            this._createInput();
+            
+            removeClass(this._button, this._settings.hoverClass);
+            removeClass(this._button, this._settings.focusClass);
+        },
+        /**
+         * Function makes sure that when user clicks upload button,
+         * the this._input is clicked instead
+         */
+        _rerouteClicks: function(){
+            var self = this;
+            
+            // IE will later display 'access denied' error
+            // if you use using self._input.click()
+            // other browsers just ignore click()
+
+            addEvent(self._button, 'mouseover', function(){
+                if (self._disabled){
+                    return;
+                }
+                                
+                if ( ! self._input){
+                       self._createInput();
+                }
+                
+                var div = self._input.parentNode;                            
+                copyLayout(self._button, div);
+                div.style.visibility = 'visible';
+                                
+            });
+            
+            
+            // commented because we now hide input on mouseleave
+            /**
+             * When the window is resized the elements 
+             * can be misaligned if button position depends
+             * on window size
+             */
+            //addResizeEvent(function(){
+            //    if (self._input){
+            //        copyLayout(self._button, self._input.parentNode);
+            //    }
+            //});            
+                                         
+        },
+        /**
+         * Creates iframe with unique name
+         * @return {Element} iframe
+         */
+        _createIframe: function(){
+            // We can't use getTime, because it sometimes return
+            // same value in safari :(
+            var id = getUID();            
+             
+            // We can't use following code as the name attribute
+            // won't be properly registered in IE6, and new window
+            // on form submit will open
+            // var iframe = document.createElement('iframe');
+            // iframe.setAttribute('name', id);                        
+            var iframe = toElement('<iframe src="javascript:false;" name="' + id + '" />');
+            // src="javascript:false; was added
+            // because it possibly removes ie6 prompt 
+            // "This page contains both secure and nonsecure items"
+            // Anyway, it doesn't do any harm.            
+            iframe.setAttribute('id', id);
+            
+            iframe.style.display = 'none';
+            document.body.appendChild(iframe);
+            
+            return iframe;
+        },
+        /**
+         * Creates form, that will be submitted to iframe
+         * @param {Element} iframe Where to submit
+         * @return {Element} form
+         */
+        _createForm: function(iframe){
+            var settings = this._settings;
+                        
+            // We can't use the following code in IE6
+            // var form = document.createElement('form');
+            // form.setAttribute('method', 'post');
+            // form.setAttribute('enctype', 'multipart/form-data');
+            // Because in this case file won't be attached to request                    
+            var form = toElement('<form method="post" enctype="multipart/form-data"></form>');
+                        
+            form.setAttribute('action', settings.action);
+            form.setAttribute('target', iframe.name);                                   
+            form.style.display = 'none';
+            document.body.appendChild(form);
+            
+            // Create hidden input element for each data key
+            for (var prop in settings.data) {
+                if (settings.data.hasOwnProperty(prop)){
+                    var el = document.createElement("input");
+                    el.setAttribute('type', 'hidden');
+                    el.setAttribute('name', prop);
+                    el.setAttribute('value', settings.data[prop]);
+                    form.appendChild(el);
+                }
+            }
+            return form;
+        },
+        /**
+         * Gets response from iframe and fires onComplete event when ready
+         * @param iframe
+         * @param file Filename to use in onComplete callback 
+         */
+        _getResponse : function(iframe, file){            
+            // getting response
+            var toDeleteFlag = false, self = this, settings = this._settings;   
+               
+            addEvent(iframe, 'load', function(){                
+                
+                if (// For Safari 
+                    iframe.src == "javascript:'%3Chtml%3E%3C/html%3E';" ||
+                    // For FF, IE
+                    iframe.src == "javascript:'<html></html>';"){                                                                        
+                        // First time around, do not delete.
+                        // We reload to blank page, so that reloading main page
+                        // does not re-submit the post.
+                        
+                        if (toDeleteFlag) {
+                            // Fix busy state in FF3
+                            setTimeout(function(){
+                                removeNode(iframe);
+                            }, 0);
+                        }
+                                                
+                        return;
+                }
+                
+                var doc = iframe.contentDocument ? iframe.contentDocument : window.frames[iframe.id].document;
+                
+                // fixing Opera 9.26,10.00
+                if (doc.readyState && doc.readyState != 'complete') {
+                   // Opera fires load event multiple times
+                   // Even when the DOM is not ready yet
+                   // this fix should not affect other browsers
+                   return;
+                }
+                
+                // fixing Opera 9.64
+                if (doc.body && doc.body.innerHTML == "false") {
+                    // In Opera 9.64 event was fired second time
+                    // when body.innerHTML changed from false 
+                    // to server response approx. after 1 sec
+                    return;
+                }
+                
+                var response;
+                
+                if (doc.XMLDocument) {
+                    // response is a xml document Internet Explorer property
+                    response = doc.XMLDocument;
+                } else if (doc.body){
+                    // response is html document or plain text
+                    response = doc.body.innerHTML;
+                    
+                    if (settings.responseType && settings.responseType.toLowerCase() == 'json') {
+                        // If the document was sent as 'application/javascript' or
+                        // 'text/javascript', then the browser wraps the text in a <pre>
+                        // tag and performs html encoding on the contents.  In this case,
+                        // we need to pull the original text content from the text node's
+                        // nodeValue property to retrieve the unmangled content.
+                        // Note that IE6 only understands text/html
+                        if (doc.body.firstChild && doc.body.firstChild.nodeName.toUpperCase() == 'PRE') {
+                            doc.normalize();
+                            response = doc.body.firstChild.firstChild.nodeValue;
+                        }
+                        
+                        if (response) {
+                            response = eval("(" + response + ")");
+                        } else {
+                            response = {};
+                        }
+                    }
+                } else {
+                    // response is a xml document
+                    response = doc;
+                }
+                
+                settings.onComplete.call(self, file, response);
+                
+                // Reload blank page, so that reloading main page
+                // does not re-submit the post. Also, remember to
+                // delete the frame
+                toDeleteFlag = true;
+                
+                // Fix IE mixed content issue
+                iframe.src = "javascript:'<html></html>';";
+            });            
+        },        
+        /**
+         * Upload file contained in this._input
+         */
+        submit: function(){                        
+            var self = this, settings = this._settings;
+            
+            if ( ! this._input || this._input.value === ''){                
+                return;                
+            }
+                                    
+            var file = fileFromPath(this._input.value);
+            
+            // user returned false to cancel upload
+            if (false === settings.onSubmit.call(this, file, getExt(file))){
+                this._clearInput();                
+                return;
+            }
+            
+            // sending request    
+            var iframe = this._createIframe();
+            var form = this._createForm(iframe);
+            
+            // assuming following structure
+            // div -> input type='file'
+            removeNode(this._input.parentNode);            
+            removeClass(self._button, self._settings.hoverClass);
+            removeClass(self._button, self._settings.focusClass);
+                        
+            form.appendChild(this._input);
+                        
+            form.submit();
+
+            // request set, clean up                
+            removeNode(form); form = null;                          
+            removeNode(this._input); this._input = null;            
+            
+            // Get response from iframe and fire onComplete event when ready
+            this._getResponse(iframe, file);            
+
+            // get ready for next request            
+            this._createInput();
+        }
+    };
+})(); 
diff --git a/mod/wall_upload.php b/mod/wall_upload.php
new file mode 100644 (file)
index 0000000..769e5dc
--- /dev/null
@@ -0,0 +1,16 @@
+<?php
+
+
+function wall_upload_post(&$a) {
+
+
+ $src      = $_FILES['userfile']['tmp_name'];
+
+
+unlink($src);
+
+
+       echo "<img src=\"".$a->get_baseurl(). "/images/default-profile.jpg\" alt=\"default\" />";
+       killme();
+
+}
\ No newline at end of file
index 7c17196ce0a14bcbce028d7891c672c4f9c1d475..97e30cdae79f2d3fc5e18e4bc4252fc4728c8342 100644 (file)
@@ -2,7 +2,6 @@
           src="$baseurl/tinymce/jscripts/tiny_mce/tiny_mce_src.js"></script>
           <script language="javascript" type="text/javascript">
 
-
 tinyMCE.init({
        theme : "advanced",
        mode : "specific_textareas",
@@ -19,10 +18,24 @@ tinyMCE.init({
        add_unload_trigger : false,
        remove_linebreaks : false,
        content_css: "$baseurl/view/custom_tinymce.css"
-
-
 });
 
+</script>
+<script type="text/javascript" src="include/ajaxupload.js" ></script>
+<script>
+       $(document).ready(function() {
+               var uploader = new window.AjaxUpload(
+                       'wall-image-upload',
+                       { action: 'wall_upload',
+                       name: 'userfile',
+                       onComplete: function(file,response) {
+                               tinyMCE.execCommand('mceInsertRawHTML',false,response);
+                       }                                
+                       }
+               );
+
+       });
+
 
 </script>
 
index dca307c391d2172a58b3f7eea46e9807e1fef421..695ac19e0ea8dc60e793a3ca4b6d40db278d1888 100644 (file)
@@ -13,6 +13,13 @@ What's on your mind?
 </div>
 <div id="profile-jot-submit-wrapper" >
 <input type="submit" id="profile-jot-submit" name="submit" value="Submit" />
+       <div id="profile-upload-wrapper" style="display: $visitor;" >
+               <div id="wall-image-upload-div" ><img id="wall-image-upload" src="images/camera-icon.gif" alt="Upload Photo" title="Upload Photo" /></div>
+       </div> 
+       <div id="profile-link-wrapper" style="display: $visitor;" >
+               <img id="profile-link" src="images/link-icon.gif" alt="Insert web link" title="Insert web link" />
+       </div> 
+
        <div id="profile-jot-perms" class="profile-jot-perms" style="display: $visitor;" ><img src="images/$lockstate_icon.gif" alt="Permission Settings" title="Permission Settings" onClick="openClose('profile-jot-acl-wrapper');" /></div>
        <div id="profile-jot-perms-end"></div>
        <div id="profile-jot-acl-wrapper" style="display: none;" >$acl</div>
index bde06ec5cd6eb42d5560eb50b010d5021b9f538b..faf1a091cbe87ad96e76b7d8194b60f50c1dc7a2 100644 (file)
@@ -512,10 +512,19 @@ input#dfrn-url {
 #profile-jot-submit {
        float: left;
 }
+#profile-upload-wrapper {
+       float: left;
+       margin-left: 50px;
+}
+
+#profile-link-wrapper {
+       float: left;
+       margin-left: 20px;
+}
 
 #profile-jot-perms {
        float: left;
-       margin-left: 350px;
+       margin-left: 280px;
 }
 
 #profile-jot-perms-end {
diff --git a/wip/todo b/wip/todo
deleted file mode 100644 (file)
index 9bee344..0000000
--- a/wip/todo
+++ /dev/null
@@ -1,56 +0,0 @@
-
-finish one world photo resolution (update timestamps in atom feeds)
->>>>>>>>contact editor
->>>>>>>>       block photo
-
->>>>>>>>profile "you name it" field
-
-
-group - delete (keep id in acl lists to prvent them from breaking security)
-
-pager - photos
-
-photos/albums/ java uploader
-
-item delete
-
->>>>>>>>item edit
-
-dfrn_poll - import items
-
->>>>>>>>local/remote indicator
-
->>>>>>>>side/text
-
-
-notififier abstraction
-
-emails and offline notifications
-
-registration workflow
-
-admin approved registration (requires admin)
-
-atom elements
-
-       tombstone
-       activity
-
-email
-
-chat
-
->>>>>>>>plugin api
-
->>>>>>>>theme api
-
-ajax
-
-image/link inline ajax
-
-publish to external directory
-
-local data performance improvements
-
-product registration/protection
-