From f790aaebc24ba9bddab4ef2805cd30c1ede40e30 Mon Sep 17 00:00:00 2001 From: Lance Stout Date: Wed, 18 Sep 2013 16:24:40 -0700 Subject: [PATCH] Add avatar changer. --- clientapp/helpers/xmppEventHandlers.js | 20 ++-- clientapp/libraries/resampler.js | 114 +++++++++++++++++++++ clientapp/libraries/stanza.io.js | 4 +- clientapp/models/me.js | 41 +++++++- clientapp/pages/main.js | 42 +++++++- clientapp/templates.js | 11 ++- clientapp/templates/pages/main.jade | 8 ++ public/css/app/settings.css | 9 ++ public/css/app/settings.styl | 12 +++ public/css/otalk.css | 131 ++++++++++++++----------- public/css/otalk.styl | 5 +- server.js | 1 + 12 files changed, 324 insertions(+), 74 deletions(-) create mode 100644 clientapp/libraries/resampler.js create mode 100644 public/css/app/settings.css create mode 100644 public/css/app/settings.styl diff --git a/clientapp/helpers/xmppEventHandlers.js b/clientapp/helpers/xmppEventHandlers.js index 3faa5fe..8a298fa 100644 --- a/clientapp/helpers/xmppEventHandlers.js +++ b/clientapp/helpers/xmppEventHandlers.js @@ -186,15 +186,21 @@ module.exports = function (client, app) { client.on('avatar', function (info) { var contact = me.getContact(info.jid); - if (contact) { - var id = ''; - var type = 'image/png'; - if (info.avatars.length > 0) { - id = info.avatars[0].id; - type = info.avatars[0].type || 'image/png'; + if (!contact) { + if (me.isMe(info.jid)) { + contact = me; + } else { + return; } - contact.setAvatar(id, type); } + + var id = ''; + var type = 'image/png'; + if (info.avatars.length > 0) { + id = info.avatars[0].id; + type = info.avatars[0].type || 'image/png'; + } + contact.setAvatar(id, type); }); client.on('chatState', function (info) { diff --git a/clientapp/libraries/resampler.js b/clientapp/libraries/resampler.js new file mode 100644 index 0000000..3a01c76 --- /dev/null +++ b/clientapp/libraries/resampler.js @@ -0,0 +1,114 @@ +var Resample = (function (canvas) { + + // (C) WebReflection Mit Style License + + // Resample function, accepts an image + // as url, base64 string, or Image/HTMLImgElement + // optional width or height, and a callback + // to invoke on operation complete + function Resample(img, width, height, onresample) { + var + // check the image type + load = typeof img == "string", + // Image pointer + i = load || img + ; + // if string, a new Image is needed + if (load) { + i = new Image; + // with propers callbacks + i.onload = onload; + i.onerror = onerror; + } + // easy/cheap way to store info + i._onresample = onresample; + i._width = width; + i._height = height; + // if string, we trust the onload event + // otherwise we call onload directly + // with the image as callback context + load ? (i.src = img) : onload.call(img); + } + + // just in case something goes wrong + function onerror() { + throw ("not found: " + this.src); + } + + // called when the Image is ready + function onload() { + var + // minifier friendly + img = this, + // the desired width, if any + width = img._width, + // the desired height, if any + height = img._height, + // the callback + onresample = img._onresample + ; + // if width and height are both specified + // the resample uses these pixels + // if width is specified but not the height + // the resample respects proportions + // accordingly with orginal size + // same is if there is a height, but no width + width == null && (width = round(img.width * height / img.height)); + height == null && (height = round(img.height * width / img.width)); + // remove (hopefully) stored info + delete img._onresample; + delete img._width; + delete img._height; + // when we reassign a canvas size + // this clears automatically + // the size should be exactly the same + // of the final image + // so that toDataURL ctx method + // will return the whole canvas as png + // without empty spaces or lines + canvas.width = width; + canvas.height = height; + // drawImage has different overloads + // in this case we need the following one ... + context.drawImage( + // original image + img, + // starting x point + 0, + // starting y point + 0, + // image width + img.width, + // image height + img.height, + // destination x point + 0, + // destination y point + 0, + // destination width + width, + // destination height + height + ); + // retrieve the canvas content as + // base4 encoded PNG image + // and pass the result to the callback + onresample(canvas.toDataURL("image/png")); + } + + var + // point one, use every time ... + context = canvas.getContext("2d"), + // local scope shortcut + round = Math.round + ; + + return Resample; + +}( + // lucky us we don't even need to append + // and render anything on the screen + // let's keep this DOM node in RAM + // for all resizes we want + this.document.createElement("canvas")) +); diff --git a/clientapp/libraries/stanza.io.js b/clientapp/libraries/stanza.io.js index 5e1ac2b..1c1a842 100644 --- a/clientapp/libraries/stanza.io.js +++ b/clientapp/libraries/stanza.io.js @@ -708,14 +708,14 @@ module.exports = function (client) { }); client.publishAvatar = function (id, data, cb) { - client.publish(null, 'urn:xmpp:avatar:data', { + client.publish('', 'urn:xmpp:avatar:data', { id: id, avatarData: data }, cb); }; client.useAvatars = function (info, cb) { - client.publish(null, 'urn:xmpp:avatar:metadata', { + client.publish('', 'urn:xmpp:avatar:metadata', { id: 'current', avatars: info }, cb); diff --git a/clientapp/models/me.js b/clientapp/models/me.js index 326739d..ae54573 100644 --- a/clientapp/models/me.js +++ b/clientapp/models/me.js @@ -1,4 +1,4 @@ -/*global app, client*/ +/*global app, client, XMPP*/ "use strict"; var HumanModel = require('human-model'); @@ -20,6 +20,7 @@ module.exports = HumanModel.define({ jid: ['object', true], status: ['string', true, ''], avatar: ['string', true, ''], + avatarID: ['string', true, ''], connected: ['bool', true, false], shouldAskForAlertsPermission: ['bool', true, false], hasFocus: ['bool', true, false], @@ -41,6 +42,44 @@ module.exports = HumanModel.define({ } this._activeContact = jid; }, + setAvatar: function (id, type) { + var self = this; + + if (!id) { + var gID = XMPP.crypto.createHash('md5').update(this.jid).digest('hex'); + self.avatar = 'https://gravatar.com/avatar/' + gID + '?s=30&d=mm'; + return; + } + + app.storage.avatars.get(id, function (err, avatar) { + if (err) { + if (!type) { + // We can't find the ID, and we don't know the type, so fallback. + var gID = XMPP.crypto.createHash('md5').update(self.jid.bare).digest('hex'); + self.avatar = 'https://gravatar.com/avatar/' + gID + '?s=30&d=mm'; + return; + } + app.whenConnected(function () { + client.getAvatar(self.jid.bare, id, function (err, resp) { + if (err) return; + resp = resp.toJSON(); + var avatarData = resp.pubsub.retrieve.item.avatarData; + var dataURI = 'data:' + type + ';base64,' + avatarData; + app.storage.avatars.add({id: id, uri: dataURI}); + self.set({ + avatar: dataURI, + avatarID: id + }); + }); + }); + } else { + self.set({ + avatar: avatar.uri, + avatarID: avatar.id + }); + } + }); + }, getContact: function (jid, alt) { if (typeof jid === 'string') jid = new client.JID(jid); if (typeof alt === 'string') alt = new client.JID(alt); diff --git a/clientapp/pages/main.js b/clientapp/pages/main.js index 7c7e2d1..56ff8cc 100644 --- a/clientapp/pages/main.js +++ b/clientapp/pages/main.js @@ -1,4 +1,4 @@ -/*global app, me*/ +/*global app, me, XMPP, client, Resample*/ "use strict"; var BasePage = require('./base'); @@ -8,10 +8,15 @@ var templates = require('../templates'); module.exports = BasePage.extend({ template: templates.pages.main, classBindings: { - 'shouldAskForAlertsPermission': '.enableAlerts' + shouldAskForAlertsPermission: '.enableAlerts' + }, + srcBindings: { + avatar: '#avatarChanger img' }, events: { - 'click .enableAlerts': 'enableAlerts' + 'click .enableAlerts': 'enableAlerts', + 'dragover': 'handleAvatarChangeDragOver', + 'drop': 'handleAvatarChange' }, initialize: function (spec) { me.shouldAskForAlertsPermission = app.notifier.shouldAskPermission(); @@ -27,5 +32,36 @@ module.exports = BasePage.extend({ }); } }); + }, + handleAvatarChangeDragOver: function (e) { + e.preventDefault(); + return false; + }, + handleAvatarChange: function (e) { + e.preventDefault(); + var file = e.dataTransfer.files[0]; + if (file.type.match('image.*')) { + console.log('Got an image file!', file.type); + var fileTracker = new FileReader(); + fileTracker.onload = function () { + var resampler = new Resample(this.result, 80, 80, function (data) { + var b64Data = data.split(',')[1]; + var id = XMPP.crypto.createHash('sha1').update(atob(b64Data)).digest('hex'); + console.log(id); + app.storage.avatars.add({id: id, uri: data}); + client.publishAvatar(id, b64Data, function (err, res) { + if (err) return; + client.useAvatars([{ + id: id, + width: 80, + height: 80, + type: 'image/png', + bytes: b64Data.length + }]); + }); + }); + }; + fileTracker.readAsDataURL(file); + } } }); diff --git a/clientapp/templates.js b/clientapp/templates.js index 8b292be..3247277 100644 --- a/clientapp/templates.js +++ b/clientapp/templates.js @@ -73,6 +73,15 @@ exports.includes.mucListItem = function anonymous(locals) { return buf.join(""); }; +// mucMessage.jade compiled template +exports.includes.mucMessage = function anonymous(locals) { + var buf = []; + with (locals || {}) { + buf.push('
  • ' + jade.escape(null == (jade.interp = message.nick) ? "" : jade.interp) + '' + jade.escape(null == (jade.interp = message.created) ? "" : jade.interp) + '

    ' + jade.escape(null == (jade.interp = message.body) ? "" : jade.interp) + "

  • "); + } + return buf.join(""); +}; + // growlMessage.jade compiled template exports.misc.growlMessage = function anonymous(locals) { var buf = []; @@ -122,7 +131,7 @@ exports.pages.groupchat = function anonymous(locals) { exports.pages.main = function anonymous(locals) { var buf = []; with (locals || {}) { - buf.push('

    This space intentionally blank

    '); + buf.push('

    Change Avatar

    Drag and drop a new avatar here

    This space intentionally blank

    '); } return buf.join(""); }; diff --git a/clientapp/templates/pages/main.jade b/clientapp/templates/pages/main.jade index d244218..f2e299d 100644 --- a/clientapp/templates/pages/main.jade +++ b/clientapp/templates/pages/main.jade @@ -1,4 +1,12 @@ section.page.main button.enableAlerts Enable alerts + div#avatarChanger + h1 Change Avatar + div.uploadRegion + p Drag and drop a new avatar here + img(width="40", height="40") + form + input#uploader(type="file") + h1 This space intentionally blank diff --git a/public/css/app/settings.css b/public/css/app/settings.css new file mode 100644 index 0000000..09f8e89 --- /dev/null +++ b/public/css/app/settings.css @@ -0,0 +1,9 @@ +.uploadRegion { + -moz-border-radius: 10px; + -webkit-border-radius: 10px; + -khtml-border-radius: 10px; + -o-border-radius: 10px; + -border-radius: 10px; + border-radius: 10px; + border: 1px dashed #777; +} diff --git a/public/css/app/settings.styl b/public/css/app/settings.styl new file mode 100644 index 0000000..335c3a5 --- /dev/null +++ b/public/css/app/settings.styl @@ -0,0 +1,12 @@ +@import '../_variables' +@import '../_mixins' + +.uploadRegion + font-size: 12px + roundall: 10px + border: 1px dashed #777 + margin: 10px + text-align: center + + input + width: 100% diff --git a/public/css/otalk.css b/public/css/otalk.css index 482bb09..f1da549 100644 --- a/public/css/otalk.css +++ b/public/css/otalk.css @@ -245,6 +245,67 @@ td { .clearfix { zoom: 1; } +body { + background: #ecf0f2; + color: #222; + font-family: 'Lucdia Grande', 'Helvetica Neue Light', 'Helvetica Neue', Helvetica, Arial, sans-serif; + font-size: 17px; + font-weight: 500; + -webkit-font-smoothing: antialiased; +} +body #pages { + position: absolute; + top: 0px; + right: 0px; + left: 156px; + -moz-box-sizing: border-box; + -webkit-box-sizing: border-box; + box-sizing: border-box; +} +body #menu { + width: 155px; +} +#connectionOverlay { + position: fixed; + top: 0px; + left: 0px; + width: 100%; + height: 100%; + background: rgba(0,0,0,0.5); + z-index: 1000; + -webkit-transition: all 0.25s linear 0; + -o-transition: all 0.25s linear 0; + transition: all 0.25s linear 0; + -moz-transition: all 0.25s linear 0; +} +#connectionOverlay.connected { + height: 0px; +} +#connectionOverlay.connected #connectionStatus { + top: -51px; +} +#connectionStatus { + height: 50px; + line-height: 50px; + top: 0px; + position: fixed; + background-color: #333; + border-bottom: 1px solid #000; + width: 100%; + z-index: 9999; + text-align: center; +} +#connectionStatus span.message { + display: inline-block; + padding: 0px 10px; + font-weight: bold; + font-size: 24px; +} +#connectionStatus button { + padding: 5px 8px; + position: relative; + top: -3px; +} #menu { position: fixed; top: 0px; @@ -627,66 +688,20 @@ td { .chatBox textarea.editing { background-color: #ff0; } -#connectionOverlay { - position: fixed; - top: 0px; - left: 0px; - width: 100%; - height: 100%; - background: rgba(0,0,0,0.5); - z-index: 1000; - -webkit-transition: all 0.25s linear 0; - -o-transition: all 0.25s linear 0; - transition: all 0.25s linear 0; - -moz-transition: all 0.25s linear 0; -} -#connectionOverlay.connected { - height: 0px; -} -#connectionOverlay.connected #connectionStatus { - top: -51px; -} -#connectionStatus { - height: 50px; - line-height: 50px; - top: 0px; - position: fixed; - background-color: #333; - border-bottom: 1px solid #000; - width: 100%; - z-index: 9999; +.uploadRegion { + font-size: 12px; + -moz-border-radius: 10px; + -webkit-border-radius: 10px; + -khtml-border-radius: 10px; + -o-border-radius: 10px; + -border-radius: 10px; + border-radius: 10px; + border: 1px dashed #777; + margin: 10px; text-align: center; } -#connectionStatus span.message { - display: inline-block; - padding: 0px 10px; - font-weight: bold; - font-size: 24px; -} -#connectionStatus button { - padding: 5px 8px; - position: relative; - top: -3px; -} -body { - background: #ecf0f2; - color: #222; - font-family: 'Lucdia Grande', 'Helvetica Neue Light', 'Helvetica Neue', Helvetica, Arial, sans-serif; - font-size: 17px; - font-weight: 500; - -webkit-font-smoothing: antialiased; -} -body #pages { - position: absolute; - top: 0px; - right: 0px; - left: 156px; - -moz-box-sizing: border-box; - -webkit-box-sizing: border-box; - box-sizing: border-box; -} -body #menu { - width: 155px; +.uploadRegion input { + width: 100%; } .aux header { margin-top: 10%; diff --git a/public/css/otalk.styl b/public/css/otalk.styl index fa0affb..05affc5 100644 --- a/public/css/otalk.styl +++ b/public/css/otalk.styl @@ -1,8 +1,9 @@ @import '_reset' @import '_variables' @import '_mixins' +@import 'app/layout' +@import 'app/connectionStatus' @import 'app/roster' @import 'app/chat' -@import 'app/connectionStatus' -@import 'app/layout' +@import 'app/settings' @import 'app/aux' diff --git a/server.js b/server.js index f2879d6..909e14b 100644 --- a/server.js +++ b/server.js @@ -24,6 +24,7 @@ var clientApp = new Moonboots({ libraries: [ __dirname + '/clientapp/libraries/zepto.js', __dirname + '/clientapp/libraries/ui.js', + __dirname + '/clientapp/libraries/resampler.js', __dirname + '/clientapp/libraries/IndexedDBShim.min.js', __dirname + '/clientapp/libraries/stanza.io.js' ],