diff --git a/dev/App/Abstract.js b/dev/App/Abstract.js index 6fe033604..f38dbe7b1 100644 --- a/dev/App/Abstract.js +++ b/dev/App/Abstract.js @@ -172,7 +172,7 @@ /** * @param {string} sTitle */ - AbstractApp.prototype.setTitle = function (sTitle) + AbstractApp.prototype.setWindowTitle = function (sTitle) { sTitle = ((Utils.isNormal(sTitle) && 0 < sTitle.length) ? sTitle + ' - ' : '') + Settings.settingsGet('Title') || ''; diff --git a/dev/App/User.js b/dev/App/User.js index ccb02367c..0c3093beb 100644 --- a/dev/App/User.js +++ b/dev/App/User.js @@ -9,6 +9,7 @@ $ = require('$'), moment = require('moment'), SimplePace = require('SimplePace'), + Tinycon = require('Tinycon'), Enums = require('Common/Enums'), Globals = require('Common/Globals'), @@ -1342,7 +1343,7 @@ { Globals.$html.addClass('rl-user-auth'); - this.setTitle(Translator.i18n('TITLES/LOADING')); + this.setWindowTitle(Translator.i18n('TITLES/LOADING')); //require.ensure([], function() { // require code splitting @@ -1479,6 +1480,17 @@ _.defer(function () { self.initVerticalLayoutResizer(Enums.ClientSideKeyName.FolderListSize); }); + + if (Tinycon) + { + Tinycon.setOptions({ + fallback: false + }); + + Events.sub('mailbox.inbox-unread-count', function (iCount) { + Tinycon.setBubble(0 < iCount ? (99 < iCount ? 99 : iCount) : 0); + }); + } } } else diff --git a/dev/Screen/Admin/Login.js b/dev/Screen/Admin/Login.js index 9dd723aef..5624218a8 100644 --- a/dev/Screen/Admin/Login.js +++ b/dev/Screen/Admin/Login.js @@ -24,7 +24,7 @@ LoginAdminScreen.prototype.onShow = function () { - require('App/Admin').setTitle(''); + require('App/Admin').setWindowTitle(''); }; module.exports = LoginAdminScreen; diff --git a/dev/Screen/Admin/Settings.js b/dev/Screen/Admin/Settings.js index 74ccb0851..edd717088 100644 --- a/dev/Screen/Admin/Settings.js +++ b/dev/Screen/Admin/Settings.js @@ -75,7 +75,7 @@ SettingsAdminScreen.prototype.onShow = function () { - require('App/Admin').setTitle(''); + require('App/Admin').setWindowTitle(''); }; module.exports = SettingsAdminScreen; diff --git a/dev/Screen/User/About.js b/dev/Screen/User/About.js index d63c8a2b2..424f0728c 100644 --- a/dev/Screen/User/About.js +++ b/dev/Screen/User/About.js @@ -24,7 +24,7 @@ AboutUserScreen.prototype.onShow = function () { - require('App/User').setTitle('RainLoop'); + require('App/User').setWindowTitle('RainLoop'); }; module.exports = AboutUserScreen; diff --git a/dev/Screen/User/Login.js b/dev/Screen/User/Login.js index bc999a788..744aa2686 100644 --- a/dev/Screen/User/Login.js +++ b/dev/Screen/User/Login.js @@ -24,7 +24,7 @@ LoginUserScreen.prototype.onShow = function () { - require('App/User').setTitle(''); + require('App/User').setWindowTitle(''); }; module.exports = LoginUserScreen; diff --git a/dev/Screen/User/MailBox.js b/dev/Screen/User/MailBox.js index 50f20bcaf..e98426af3 100644 --- a/dev/Screen/User/MailBox.js +++ b/dev/Screen/User/MailBox.js @@ -45,21 +45,21 @@ */ MailBoxUserScreen.prototype.oLastRoute = {}; - MailBoxUserScreen.prototype.setNewTitle = function () + MailBoxUserScreen.prototype.updateWindowTitle = function () { var sEmail = AccountStore.email(), nFoldersInboxUnreadCount = FolderStore.foldersInboxUnreadCount() ; - require('App/User').setTitle(('' === sEmail ? '' : + require('App/User').setWindowTitle(('' === sEmail ? '' : '' + (0 < nFoldersInboxUnreadCount ? '(' + nFoldersInboxUnreadCount + ') ' : ' ') + sEmail + ' - ') + Translator.i18n('TITLES/MAILBOX')); }; MailBoxUserScreen.prototype.onShow = function () { - this.setNewTitle(); + this.updateWindowTitle(); Globals.keyScope(Enums.KeyState.MessageList); }; @@ -112,7 +112,7 @@ SettingsStore.layout.valueHasMutated(); }, 50); - Events.sub('mailbox.inbox-unread-count', function (iCount) { + Events.sub('mailbox.inbox-unread-count', _.bind(function (iCount) { FolderStore.foldersInboxUnreadCount(iCount); @@ -124,12 +124,10 @@ oItem.count(iCount); } }); - }); - FolderStore.foldersInboxUnreadCount.subscribe(function () { - this.setNewTitle(); - }, this); + this.updateWindowTitle(); + }, this)); }; MailBoxUserScreen.prototype.onBuild = function () diff --git a/dev/Screen/User/Settings.js b/dev/Screen/User/Settings.js index ecd5359fc..b9e05ff5f 100644 --- a/dev/Screen/User/Settings.js +++ b/dev/Screen/User/Settings.js @@ -122,7 +122,7 @@ SettingsUserScreen.prototype.setSettingsTitle = function () { - require('App/User').setTitle(this.sSettingsTitle); + require('App/User').setWindowTitle(this.sSettingsTitle); }; module.exports = SettingsUserScreen; diff --git a/dev/bootstrap.js b/dev/bootstrap.js index 2572f95fa..d88505169 100644 --- a/dev/bootstrap.js +++ b/dev/bootstrap.js @@ -54,7 +54,7 @@ window['__APP_BOOT'] = function (fCall) { - $(function () { + $(_.delay(function () { if (!$('#rl-content').is(':visible')) { @@ -82,7 +82,8 @@ } window['__APP_BOOT'] = null; - }); + + }, 10)); }; }; diff --git a/gulpfile.js b/gulpfile.js index ff5587109..4976b8976 100644 --- a/gulpfile.js +++ b/gulpfile.js @@ -157,6 +157,7 @@ cfg.paths.js = { 'vendors/queue/queue.min.js', 'vendors/inputosaurus/inputosaurus.min.js', 'vendors/moment/min/moment.min.js ', + 'vendors/tinycon/tinycon.min.js ', 'vendors/routes/signals.min.js', 'vendors/routes/hasher.min.js', 'vendors/routes/crossroads.min.js', diff --git a/vendors/tinycon/LICENSE b/vendors/tinycon/LICENSE new file mode 100644 index 000000000..fdb4f08c8 --- /dev/null +++ b/vendors/tinycon/LICENSE @@ -0,0 +1,19 @@ +Copyright (c) 2013 Tom Moor + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/vendors/tinycon/README.md b/vendors/tinycon/README.md new file mode 100644 index 000000000..80dac2627 --- /dev/null +++ b/vendors/tinycon/README.md @@ -0,0 +1,90 @@ +# Tinycon + +A small library for manipulating the favicon, in particular adding alert bubbles and changing images. Tinycon gracefully falls back to a number in title approach for browers that don't support canvas or dynamic favicons. + + + +See the Live Demo here. + +## Documentation + +Tinycon adds a single object to the global namespace and does not require initialisation. + +### Basic Usage + +```javascript +Tinycon.setBubble(6); +``` + +### Options + +Tinycon can take a range of options to customise the look + +* width: the width of the alert bubble +* height: the height of the alert bubble +* font: a css string to use for the fontface (recommended to leave this) +* colour: the foreground font colour +* background: the alert bubble background colour +* fallback: should we fallback to a number in brackets for browsers that don't support canvas/dynamic favicons? Boolean, or use the string 'force' to ensure a title update even in supported browsers. +* abbreviate: should tinycon shrink large numbers such as 1000 to an abbreviated version (1k). Boolean, defaults to true + +```javascript +Tinycon.setOptions({ + width: 7, + height: 9, + font: '10px arial', + colour: '#ffffff', + background: '#549A2F', + fallback: true +}); +``` + +### AMD support + +Tinycon can also be used as an asynchronous module. + +```javascript +require([ + 'tinycon.js' +], function (T) { + + T.setOptions({ + width: 7, + height: 9, + font: '10px arial', + colour: '#ffffff', + background: '#549A2F', + fallback: true + }); + + T.setBubble(7); + +}); +``` + +## Browser Support + +Tinycon has been tested to work completely in the following browsers. Older versions may be supported, but haven't been tested: + +* Chrome 15+ +* Firefox 9+ +* Opera 11+ + +Currently the library degrades to title update: + +* Internet Explorer 9 +* Safari 5 + + +## License / Credits + +Tinycon is released under the MIT license. It is simple and easy to understand and places almost no restrictions on what you can do with Tinycon. +[More Information](http://en.wikipedia.org/wiki/MIT_License) + +Tinycon was inspired by [Notificon](https://github.com/makeable/Notificon) + + +## Download + +Releases are available for download from +[GitHub](http://github.com/tommoor/tinycon/downloads). diff --git a/vendors/tinycon/tinycon.js b/vendors/tinycon/tinycon.js new file mode 100644 index 000000000..3955fb3c7 --- /dev/null +++ b/vendors/tinycon/tinycon.js @@ -0,0 +1,279 @@ +/*! + * Tinycon - A small library for manipulating the Favicon + * Tom Moor, http://tommoor.com + * Copyright (c) 2012 Tom Moor + * @license MIT Licensed + * @version 0.6.3 + */ + +(function(){ + + var Tinycon = {}; + var currentFavicon = null; + var originalFavicon = null; + var faviconImage = null; + var canvas = null; + var options = {}; + var r = window.devicePixelRatio || 1; + var size = 16 * r; + var defaults = { + width: 7, + height: 9, + font: 10 * r + 'px arial', + colour: '#ffffff', + background: '#F03D25', + fallback: true, + crossOrigin: true, + abbreviate: true + }; + + var ua = (function () { + var agent = navigator.userAgent.toLowerCase(); + // New function has access to 'agent' via closure + return function (browser) { + return agent.indexOf(browser) !== -1; + }; + }()); + + var browser = { + ie: ua('msie'), + chrome: ua('chrome'), + webkit: ua('chrome') || ua('safari'), + safari: ua('safari') && !ua('chrome'), + mozilla: ua('mozilla') && !ua('chrome') && !ua('safari') + }; + + // private methods + var getFaviconTag = function(){ + + var links = document.getElementsByTagName('link'); + + for(var i=0, len=links.length; i < len; i++) { + if ((links[i].getAttribute('rel') || '').match(/\bicon\b/)) { + return links[i]; + } + } + + return false; + }; + + var removeFaviconTag = function(){ + + var links = document.getElementsByTagName('link'); + var head = document.getElementsByTagName('head')[0]; + + for(var i=0, len=links.length; i < len; i++) { + var exists = (typeof(links[i]) !== 'undefined'); + if (exists && (links[i].getAttribute('rel') || '').match(/\bicon\b/)) { + head.removeChild(links[i]); + } + } + }; + + var getCurrentFavicon = function(){ + + if (!originalFavicon || !currentFavicon) { + var tag = getFaviconTag(); + originalFavicon = currentFavicon = tag ? tag.getAttribute('href') : '/favicon.ico'; + } + + return currentFavicon; + }; + + var getCanvas = function (){ + + if (!canvas) { + canvas = document.createElement("canvas"); + canvas.width = size; + canvas.height = size; + } + + return canvas; + }; + + var setFaviconTag = function(url){ + removeFaviconTag(); + + var link = document.createElement('link'); + link.type = 'image/x-icon'; + link.rel = 'icon'; + link.href = url; + document.getElementsByTagName('head')[0].appendChild(link); + }; + + var log = function(message){ + if (window.console) window.console.log(message); + }; + + var drawFavicon = function(label, colour) { + + // fallback to updating the browser title if unsupported + if (!getCanvas().getContext || browser.ie || browser.safari || options.fallback === 'force') { + return updateTitle(label); + } + + var context = getCanvas().getContext("2d"); + var colour = colour || '#000000'; + var src = getCurrentFavicon(); + + faviconImage = document.createElement('img'); + faviconImage.onload = function() { + + // clear canvas + context.clearRect(0, 0, size, size); + + // draw the favicon + context.drawImage(faviconImage, 0, 0, faviconImage.width, faviconImage.height, 0, 0, size, size); + + // draw bubble over the top + if ((label + '').length > 0) drawBubble(context, label, colour); + + // refresh tag in page + refreshFavicon(); + }; + + // allow cross origin resource requests if the image is not a data:uri + // as detailed here: https://github.com/mrdoob/three.js/issues/1305 + if (!src.match(/^data/) && options.crossOrigin) { + faviconImage.crossOrigin = 'anonymous'; + } + + faviconImage.src = src; + }; + + var updateTitle = function(label) { + + if (options.fallback) { + // Grab the current title that we can prefix with the label + var originalTitle = document.title; + + // Strip out the old label if there is one + if (originalTitle[0] === '(') { + originalTitle = originalTitle.slice(originalTitle.indexOf(' ')); + } + + if ((label + '').length > 0) { + document.title = '(' + label + ') ' + originalTitle; + } else { + document.title = originalTitle; + } + } + }; + + var drawBubble = function(context, label, colour) { + + // automatic abbreviation for long (>2 digits) numbers + if (typeof label == 'number' && label > 99 && options.abbreviate) { + label = abbreviateNumber(label); + } + + // bubble needs to be larger for double digits + var len = (label + '').length-1; + + var width = options.width * r + (6 * r * len), + height = options.height * r; + + var top = size - height, + left = size - width - r, + bottom = 16 * r, + right = 16 * r, + radius = 2 * r; + + // webkit seems to render fonts lighter than firefox + context.font = (browser.webkit ? 'bold ' : '') + options.font; + context.fillStyle = options.background; + context.strokeStyle = options.background; + context.lineWidth = r; + + // bubble + context.beginPath(); + context.moveTo(left + radius, top); + context.quadraticCurveTo(left, top, left, top + radius); + context.lineTo(left, bottom - radius); + context.quadraticCurveTo(left, bottom, left + radius, bottom); + context.lineTo(right - radius, bottom); + context.quadraticCurveTo(right, bottom, right, bottom - radius); + context.lineTo(right, top + radius); + context.quadraticCurveTo(right, top, right - radius, top); + context.closePath(); + context.fill(); + + // bottom shadow + context.beginPath(); + context.strokeStyle = "rgba(0,0,0,0.3)"; + context.moveTo(left + radius / 2.0, bottom); + context.lineTo(right - radius / 2.0, bottom); + context.stroke(); + + // label + context.fillStyle = options.colour; + context.textAlign = "right"; + context.textBaseline = "top"; + + // unfortunately webkit/mozilla are a pixel different in text positioning + context.fillText(label, r === 2 ? 29 : 15, browser.mozilla ? 7*r : 6*r); + }; + + var refreshFavicon = function(){ + // check support + if (!getCanvas().getContext) return; + + setFaviconTag(getCanvas().toDataURL()); + }; + + var abbreviateNumber = function(label) { + var metricPrefixes = [ + ['G', 1000000000], + ['M', 1000000], + ['k', 1000] + ]; + + for(var i = 0; i < metricPrefixes.length; ++i) { + if (label >= metricPrefixes[i][1]) { + label = round(label / metricPrefixes[i][1]) + metricPrefixes[i][0]; + break; + } + } + + return label; + }; + + var round = function (value, precision) { + var number = new Number(value); + return number.toFixed(precision); + }; + + // public methods + Tinycon.setOptions = function(custom){ + options = {}; + + for(var key in defaults){ + options[key] = custom.hasOwnProperty(key) ? custom[key] : defaults[key]; + } + return this; + }; + + Tinycon.setImage = function(url){ + currentFavicon = url; + refreshFavicon(); + return this; + }; + + Tinycon.setBubble = function(label, colour) { + label = label || ''; + drawFavicon(label, colour); + return this; + }; + + Tinycon.reset = function(){ + setFaviconTag(originalFavicon); + }; + + Tinycon.setOptions(defaults); + window.Tinycon = Tinycon; + + if(typeof define === 'function' && define.amd) { + define(Tinycon); + } + +})(); diff --git a/vendors/tinycon/tinycon.min.js b/vendors/tinycon/tinycon.min.js new file mode 100644 index 000000000..283f511ca --- /dev/null +++ b/vendors/tinycon/tinycon.min.js @@ -0,0 +1,8 @@ +/*! + Tinycon - A small library for manipulating the Favicon + Tom Moor, http://tommoor.com + Copyright (c) 2012 Tom Moor + @license MIT Licensed + @version 0.6.3 +*/ +(function(){var Tinycon={};var currentFavicon=null;var originalFavicon=null;var faviconImage=null;var canvas=null;var options={};var r=window.devicePixelRatio||1;var size=16*r;var defaults={width:7,height:9,font:10*r+'px arial',colour:'#ffffff',background:'#F03D25',fallback:true,crossOrigin:true,abbreviate:true};var ua=(function(){var agent=navigator.userAgent.toLowerCase();return function(browser){return agent.indexOf(browser)!==-1}}());var browser={ie:ua('msie'),chrome:ua('chrome'),webkit:ua('chrome')||ua('safari'),safari:ua('safari')&&!ua('chrome'),mozilla:ua('mozilla')&&!ua('chrome')&&!ua('safari')};var getFaviconTag=function(){var links=document.getElementsByTagName('link');for(var i=0,len=links.length;i0)drawBubble(context,label,colour);refreshFavicon()};if(!src.match(/^data/)&&options.crossOrigin){faviconImage.crossOrigin='anonymous'}faviconImage.src=src};var updateTitle=function(label){if(options.fallback){var originalTitle=document.title;if(originalTitle[0]==='('){originalTitle=originalTitle.slice(originalTitle.indexOf(' '))}if((label+'').length>0){document.title='('+label+') '+originalTitle}else{document.title=originalTitle}}};var drawBubble=function(context,label,colour){if(typeof label=='number'&&label>99&&options.abbreviate){label=abbreviateNumber(label)}var len=(label+'').length-1;var width=options.width*r+(6*r*len),height=options.height*r;var top=size-height,left=size-width-r,bottom=16*r,right=16*r,radius=2*r;context.font=(browser.webkit?'bold ':'')+options.font;context.fillStyle=options.background;context.strokeStyle=options.background;context.lineWidth=r;context.beginPath();context.moveTo(left+radius,top);context.quadraticCurveTo(left,top,left,top+radius);context.lineTo(left,bottom-radius);context.quadraticCurveTo(left,bottom,left+radius,bottom);context.lineTo(right-radius,bottom);context.quadraticCurveTo(right,bottom,right,bottom-radius);context.lineTo(right,top+radius);context.quadraticCurveTo(right,top,right-radius,top);context.closePath();context.fill();context.beginPath();context.strokeStyle="rgba(0,0,0,0.3)";context.moveTo(left+radius/2.0,bottom);context.lineTo(right-radius/2.0,bottom);context.stroke();context.fillStyle=options.colour;context.textAlign="right";context.textBaseline="top";context.fillText(label,r===2?29:15,browser.mozilla?7*r:6*r)};var refreshFavicon=function(){if(!getCanvas().getContext)return;setFaviconTag(getCanvas().toDataURL())};var abbreviateNumber=function(label){var metricPrefixes=[['G',1000000000],['M',1000000],['k',1000]];for(var i=0;i=metricPrefixes[i][1]){label=round(label/metricPrefixes[i][1])+metricPrefixes[i][0];break}}return label};var round=function(value,precision){var number=new Number(value);return number.toFixed(precision)};Tinycon.setOptions=function(custom){options={};for(var key in defaults){options[key]=custom.hasOwnProperty(key)?custom[key]:defaults[key]}return this};Tinycon.setImage=function(url){currentFavicon=url;refreshFavicon();return this};Tinycon.setBubble=function(label,colour){label=label||'';drawFavicon(label,colour);return this};Tinycon.reset=function(){setFaviconTag(originalFavicon)};Tinycon.setOptions(defaults);window.Tinycon=Tinycon;if(typeof define==='function'&&define.amd){define(Tinycon)}})(); \ No newline at end of file diff --git a/webpack.config.js b/webpack.config.js index e6300ec27..d1ce5de8e 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -38,6 +38,7 @@ module.exports = { 'hasher': 'window.hasher', 'Jua': 'window.Jua', 'Autolinker': 'window.Autolinker', + 'Tinycon': 'window.Tinycon', 'buzz': 'window.buzz', 'ssm': 'window.ssm', 'key': 'window.key',