From db682786bd72b8f5ebbddfd647409f2d34ea1551 Mon Sep 17 00:00:00 2001 From: Pierre-Yves Chibon Date: May 02 2016 11:10:11 +0000 Subject: [PATCH 1/12] Make the favicon be the user's avatar --- diff --git a/hubs/templates/master.html b/hubs/templates/master.html index b2fc833..e4ae32e 100644 --- a/hubs/templates/master.html +++ b/hubs/templates/master.html @@ -10,6 +10,9 @@ type="text/css" rel="stylesheet" /> +
From 2847f9e9e2ebf32249bd4fb6595e190cf2c10241 Mon Sep 17 00:00:00 2001 From: Pierre-Yves Chibon Date: May 02 2016 15:30:03 +0000 Subject: [PATCH 2/12] Add a new endpoint serving and caching locally the user's avatar --- diff --git a/hubs/app.py b/hubs/app.py index a35dee8..2037a04 100755 --- a/hubs/app.py +++ b/hubs/app.py @@ -6,6 +6,7 @@ import os import flask import flask.json import munch +import requests import six from flask.ext.openid import OpenID @@ -78,6 +79,21 @@ def index(): return flask.redirect(flask.url_for('hub', name=flask.g.auth.nickname)) +@app.route('/_avatar/') +def avatar(username): + favicon = os.path.join( + app.static_folder, 'cache', '%s.png' % username) + if not os.path.exists(favicon): + req = requests.get(hubs.utils.username2avatar(username, s=32)) + if req: + with open(favicon, 'w') as stream: + stream.write(req.content) + + return flask.redirect( + flask.url_for('static', filename='cache/%s.png' % username) + ) + + @app.route('/groups') def groups(): if not flask.g.auth.logged_in: From 0ae15d4759901348cc0188d20b9feb9442a56e7f Mon Sep 17 00:00:00 2001 From: Pierre-Yves Chibon Date: May 02 2016 20:24:49 +0000 Subject: [PATCH 3/12] Let hubs.widgets.base return the url to the avatar endpoint --- diff --git a/hubs/widgets/base.py b/hubs/widgets/base.py index fcce04a..e8efbdc 100755 --- a/hubs/widgets/base.py +++ b/hubs/widgets/base.py @@ -99,8 +99,4 @@ def wraps(original): def avatar(username, size=32): - openid = 'http://%s.id.fedoraproject.org/' % username - query = six.moves.urllib_parse.urlencode({'s': size, 'd': 'retro'}) - hash = hashlib.sha256(openid.encode('utf-8')).hexdigest() - template = "https://seccdn.libravatar.org/avatar/%s?%s" - return template % (hash, query) + return flask.url_for('avatar', username=username) From de8142f22bf29abc8c6a0043b31944e105d64a2f Mon Sep 17 00:00:00 2001 From: Pierre-Yves Chibon Date: May 02 2016 20:27:12 +0000 Subject: [PATCH 4/12] Use the new endpoint for the avatar instead of username2avatar --- diff --git a/hubs/models.py b/hubs/models.py index 059ad28..d228464 100755 --- a/hubs/models.py +++ b/hubs/models.py @@ -41,7 +41,6 @@ import fedmsg.utils import hubs.defaults import hubs.widgets -from hubs.utils import username2avatar class HubsBase(object): @@ -228,7 +227,7 @@ class Hub(BASE): @classmethod def create_user_hub(cls, session, username, fullname): hub = cls(name=username, summary=fullname, - avatar=username2avatar(username), + avatar=hubs.widget.base.avatar(username), user_hub=True) session.add(hub) @@ -242,7 +241,7 @@ class Hub(BASE): def create_group_hub(cls, session, name, summary, **extra): hub = cls(name=name, summary=summary, # TODO -- do something else, smarter for group avatars - avatar=username2avatar(name), + avatar=hubs.widget.base.avatar(username), user_hub=False) session.add(hub) @@ -349,7 +348,7 @@ class User(BASE): return { 'username': self.username, 'openid': self.openid, - 'avatar': username2avatar(self.username), + 'avatar': hubs.widget.base.avatar(self.username), 'fullname': self.fullname, 'created_on': self.created_on, # We'll need hubs subscribed to, owned, etc.. From dc5c703eca247eb6fe7ac53dbf08f9047b933bb8 Mon Sep 17 00:00:00 2001 From: Pierre-Yves Chibon Date: May 02 2016 20:33:37 +0000 Subject: [PATCH 5/12] Import our customize notificon JS library and fix html --- diff --git a/hubs/static/notificon.js b/hubs/static/notificon.js new file mode 100644 index 0000000..b7742a8 --- /dev/null +++ b/hubs/static/notificon.js @@ -0,0 +1,185 @@ +/* +Notificon :: Client-side Favicon Notifications - Usage: Notificon(label='',favicon_url={default_favicon}) +========================================================================================================= + +Copyright (c) 2011 Matt Williams . All rights reserved. + +Redistribution and use in source and binary forms, with or without modification, are +permitted provided that the following conditions are met: + + 1. Redistributions of source code must retain the above copyright notice, this list of + conditions and the following disclaimer. + + 2. Redistributions in binary form must reproduce the above copyright notice, this list + of conditions and the following disclaimer in the documentation and/or other materials + provided with the distribution. + +THIS SOFTWARE IS PROVIDED BY MATT WILLIAMS ''AS IS'' AND ANY EXPRESS OR IMPLIED +WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND +FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL MATT WILLIAMS OR +CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON +ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF +ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +The views and conclusions contained in the software and documentation are those of the +authors and should not be interpreted as representing official policies, +either expressed or implied, of Matt Williams. + +Modified by Pierre-Yves Chibon using code from the +favicon-notification project (https://github.com/igorprado/favicon-notification) +licensed under the MIT license. + +*/ + +(function(){ + + var unsupported = false; + var _options = {}; + + var checkSupport = function checkSupport() { + if (unsupported) { + return false; + } + if (!document.createElement('canvas').getContext) { + unsupported = true; + if (console) { + console.log('Notificon: requires canvas support'); + } + return false; + } + return true; + } + + + var _defaults = { + color: '#eb361e', + lineColor: '#ffffff' + }; + + var _setOptions = function(options) { + if (!options) { + _options = _defaults; + return; + } + + _options = {}; + + for(var key in _defaults){ + _options[key] = options.hasOwnProperty(key) ? options[key] : _defaults[key]; + } + }; + + var findFaviconTag = function findFaviconTag(notificon) { + var link_tags = document.getElementsByTagName('link'); + for (var i=0; i < link_tags.length; i++) { + if (notificon && (/\bnotificon\b/i).test(link_tags[i].getAttribute('rel'))) { + return link_tags[i]; + } else if (!notificon && (/\bicon\b/i).test(link_tags[i].getAttribute('rel'))) { + return link_tags[i]; + } + } + return false; + }; + + var getExistingFavicon = function getExistingFavicon() { + var favicon = findFaviconTag(); + return favicon ? favicon.getAttribute('href') : '/favicon.ico'; + }; + + var removeNotificon = function removeNotificon() { + var notificon = findFaviconTag(true); + if (notificon) { + notificon.parentNode.removeChild(notificon); + removeNotificon(); + } + }; + + var changeFavicon = function changeFavicon(canvas) { + var link = document.createElement('link'); + link.type = 'image/x-icon'; + link.rel = 'icon notificon'; + link.href = canvas.toDataURL("image/png"); + removeNotificon(); + document.getElementsByTagName('head')[0].appendChild(link); + }; + + var drawLabel = function drawLabel(canvas, label, favicon) { + var img = document.createElement('img'); + img.src = favicon; + + img.onload = function() { + var lineWidth = 2; + + canvas.width = img.width; + canvas.height = img.height; + + var context = canvas.getContext('2d'); + context.clearRect(0, 0, img.width, img.height); + context.drawImage(img, 0, 0); + + var centerX = img.width - (img.width / 4.5) - lineWidth; + var centerY = img.height - (img.height / 4.5) - lineWidth; + var radius = img.width / 4.5; + + context.fillStyle = _options.color; + context.strokeStyle = _options.lineColor; + context.lineWidth = lineWidth; + + context.beginPath(); + context.arc(centerX, centerY, radius, 0, Math.PI * 2, false); + context.closePath(); + context.fill(); + context.stroke(); + + changeFavicon(canvas) + } + }; + + var imgToCanvas = function imgToCanvas(img) { + var canvas = document.createElement("canvas"); + canvas.width = img.width; + canvas.height = img.height; + var context = canvas.getContext("2d"); + context.drawImage(img, 0, 0); + return canvas; + }; + + var createNotificon = function createNotificon(label, favicon) { + if (!checkSupport()) { + return false; + } + if (!favicon) { + favicon = getExistingFavicon(); + } + var img = document.createElement("img"); + img.src = favicon; + img.onload = function() { + var canvas = imgToCanvas(img); + if (label) { + drawLabel(canvas, label, favicon); + } + try { + changeFavicon(canvas); + } catch(e) { + if (console) { + console.log(e) + console.log('Notificon: cannot use icons located on a different domain (' + favicon + ')'); + } + } + + }; + img.onerror = function() { + if (console) { + console.log('Notificon: image not found (' + favicon + ')'); + } + }; + }; + + this.Notificon = function(label, favicon, options) { + _setOptions(options); + createNotificon(label, favicon); + }; +})(); diff --git a/hubs/templates/master.html b/hubs/templates/master.html index e4ae32e..cb35ed6 100644 --- a/hubs/templates/master.html +++ b/hubs/templates/master.html @@ -6,6 +6,7 @@ + - +
From 23bc2f764605a0adcaec76b4992de41769642c71 Mon Sep 17 00:00:00 2001 From: Pierre-Yves Chibon Date: May 03 2016 05:46:43 +0000 Subject: [PATCH 6/12] Let's provide the color of the notification as argument to Notificon --- diff --git a/hubs/static/notificon.js b/hubs/static/notificon.js index b7742a8..82322c8 100644 --- a/hubs/static/notificon.js +++ b/hubs/static/notificon.js @@ -106,7 +106,7 @@ licensed under the MIT license. document.getElementsByTagName('head')[0].appendChild(link); }; - var drawLabel = function drawLabel(canvas, label, favicon) { + var drawLabel = function drawLabel(canvas, color, favicon) { var img = document.createElement('img'); img.src = favicon; @@ -124,7 +124,7 @@ licensed under the MIT license. var centerY = img.height - (img.height / 4.5) - lineWidth; var radius = img.width / 4.5; - context.fillStyle = _options.color; + context.fillStyle = color; context.strokeStyle = _options.lineColor; context.lineWidth = lineWidth; @@ -147,7 +147,7 @@ licensed under the MIT license. return canvas; }; - var createNotificon = function createNotificon(label, favicon) { + var createNotificon = function createNotificon(color, favicon) { if (!checkSupport()) { return false; } @@ -158,8 +158,8 @@ licensed under the MIT license. img.src = favicon; img.onload = function() { var canvas = imgToCanvas(img); - if (label) { - drawLabel(canvas, label, favicon); + if (color) { + drawLabel(canvas, color, favicon); } try { changeFavicon(canvas); @@ -178,8 +178,8 @@ licensed under the MIT license. }; }; - this.Notificon = function(label, favicon, options) { + this.Notificon = function(color, favicon, options) { _setOptions(options); - createNotificon(label, favicon); + createNotificon(color, favicon); }; })(); From d1f4cd2db37e509a018c863023f9a059ee033ffa Mon Sep 17 00:00:00 2001 From: Pierre-Yves Chibon Date: May 03 2016 05:47:01 +0000 Subject: [PATCH 7/12] Let's show then hide the notification, just as testing for now --- diff --git a/hubs/templates/hubs.html b/hubs/templates/hubs.html index 77c0135..865c123 100644 --- a/hubs/templates/hubs.html +++ b/hubs/templates/hubs.html @@ -307,5 +307,11 @@ function setup_widgets(widgets) { setup_widgets(); +/* This is how to activate and remove (here after 13 sec) the favicon +notification */ +Notificon('#33ff00'); +setTimeout(Notificon, 13000) + + {% endblock %} From 5a98296e97f8ccacb58df2a50f8e202347c3cff6 Mon Sep 17 00:00:00 2001 From: Pierre-Yves Chibon Date: May 04 2016 13:27:31 +0000 Subject: [PATCH 8/12] Drop the change for the avatar Now all the avatar work is done in one location instead of two. --- diff --git a/hubs/app.py b/hubs/app.py index 2037a04..1a77545 100755 --- a/hubs/app.py +++ b/hubs/app.py @@ -6,7 +6,6 @@ import os import flask import flask.json import munch -import requests import six from flask.ext.openid import OpenID @@ -24,7 +23,7 @@ app = flask.Flask(__name__) def days_since(then): return (datetime.datetime.utcnow() - then).days app.template_filter('days_since')(days_since) -app.template_filter('avatar')(hubs.widgets.base.avatar) +app.template_filter('avatar')(username2avatar) logging.basicConfig() @@ -79,21 +78,6 @@ def index(): return flask.redirect(flask.url_for('hub', name=flask.g.auth.nickname)) -@app.route('/_avatar/') -def avatar(username): - favicon = os.path.join( - app.static_folder, 'cache', '%s.png' % username) - if not os.path.exists(favicon): - req = requests.get(hubs.utils.username2avatar(username, s=32)) - if req: - with open(favicon, 'w') as stream: - stream.write(req.content) - - return flask.redirect( - flask.url_for('static', filename='cache/%s.png' % username) - ) - - @app.route('/groups') def groups(): if not flask.g.auth.logged_in: diff --git a/hubs/models.py b/hubs/models.py index d228464..49a9503 100755 --- a/hubs/models.py +++ b/hubs/models.py @@ -41,6 +41,7 @@ import fedmsg.utils import hubs.defaults import hubs.widgets +from hubs.utils import username2avatar class HubsBase(object): @@ -227,7 +228,7 @@ class Hub(BASE): @classmethod def create_user_hub(cls, session, username, fullname): hub = cls(name=username, summary=fullname, - avatar=hubs.widget.base.avatar(username), + avatar=username2avatar(username), user_hub=True) session.add(hub) @@ -241,7 +242,7 @@ class Hub(BASE): def create_group_hub(cls, session, name, summary, **extra): hub = cls(name=name, summary=summary, # TODO -- do something else, smarter for group avatars - avatar=hubs.widget.base.avatar(username), + avatar=username2avatar(username), user_hub=False) session.add(hub) @@ -348,7 +349,7 @@ class User(BASE): return { 'username': self.username, 'openid': self.openid, - 'avatar': hubs.widget.base.avatar(self.username), + 'avatar': username2avatar(self.username), 'fullname': self.fullname, 'created_on': self.created_on, # We'll need hubs subscribed to, owned, etc.. diff --git a/hubs/widgets/base.py b/hubs/widgets/base.py index e8efbdc..91d3931 100755 --- a/hubs/widgets/base.py +++ b/hubs/widgets/base.py @@ -96,7 +96,3 @@ def wraps(original): subsequent.widget_arguments = getattr(original, 'widget_arguments', []) return subsequent return decorator - - -def avatar(username, size=32): - return flask.url_for('avatar', username=username) diff --git a/hubs/widgets/rules.py b/hubs/widgets/rules.py index d0bfad7..edf141a 100755 --- a/hubs/widgets/rules.py +++ b/hubs/widgets/rules.py @@ -2,8 +2,9 @@ from collections import OrderedDict as ordereddict from hubs.hinting import hint, prefixed as _ from hubs.widgets.chrome import panel -from hubs.widgets.base import argument, avatar +from hubs.widgets.base import argument from hubs.widgets import templating +from hubs.utils import username2avatar from hubs import validators chrome = panel() @@ -25,7 +26,9 @@ position = 'both' help="Link to meeting minutes from past meetings..") def data(session, widget, link, schedule_text, schedule_link, minutes_link): owners = widget.hub.owners - owners = ordereddict([(o.username, avatar(o.username)) for o in owners]) + owners = ordereddict([ + (o.username, username2avatar(o.username)) for o in owners + ]) return dict(owners=owners, link=link, schedule_text=schedule_text, schedule_link=schedule_link, From 86bc97d1e5dda2f2f3e6c2d8320108d3819eb299 Mon Sep 17 00:00:00 2001 From: Pierre-Yves Chibon Date: May 04 2016 13:28:04 +0000 Subject: [PATCH 9/12] Add a hubs favicon and make it present on all hubs --- diff --git a/hubs/static/img/favicon.png b/hubs/static/img/favicon.png new file mode 100644 index 0000000..bc35b8d Binary files /dev/null and b/hubs/static/img/favicon.png differ diff --git a/hubs/templates/master.html b/hubs/templates/master.html index cb35ed6..899847c 100644 --- a/hubs/templates/master.html +++ b/hubs/templates/master.html @@ -11,9 +11,8 @@ type="text/css" rel="stylesheet" /> - +
From e71bfaa5cacea3aff5e5f08f195c4222787772f5 Mon Sep 17 00:00:00 2001 From: Pierre-Yves Chibon Date: May 04 2016 13:29:16 +0000 Subject: [PATCH 10/12] Show logic to toggle notification on and off --- diff --git a/hubs/templates/hubs.html b/hubs/templates/hubs.html index 865c123..c18533b 100644 --- a/hubs/templates/hubs.html +++ b/hubs/templates/hubs.html @@ -309,8 +309,12 @@ setup_widgets(); /* This is how to activate and remove (here after 13 sec) the favicon notification */ +console.log('Show notification'); Notificon('#33ff00'); -setTimeout(Notificon, 13000) +setTimeout(function(){console.log('Hide notification'); Notificon()}, 13000) +setTimeout(function(){ + console.log('Show notification'); Notificon('#eb361e') + }, 15000) From b09c41c3718f52ec42da377e7c802683e82a9377 Mon Sep 17 00:00:00 2001 From: Pierre-Yves Chibon Date: May 04 2016 13:39:46 +0000 Subject: [PATCH 11/12] Hide the notification at the end --- diff --git a/hubs/templates/hubs.html b/hubs/templates/hubs.html index c18533b..618c45a 100644 --- a/hubs/templates/hubs.html +++ b/hubs/templates/hubs.html @@ -315,7 +315,7 @@ setTimeout(function(){console.log('Hide notification'); Notificon()}, 13000) setTimeout(function(){ console.log('Show notification'); Notificon('#eb361e') }, 15000) - +setTimeout(function(){console.log('Hide notification'); Notificon()}, 18000) {% endblock %} From 9cd74442c72eebce47980f89417d5edb71aa77b9 Mon Sep 17 00:00:00 2001 From: Pierre-Yves Chibon Date: May 06 2016 09:16:21 +0000 Subject: [PATCH 12/12] Fix typo found by @sayanchowdhury --- diff --git a/hubs/models.py b/hubs/models.py index 49a9503..059ad28 100755 --- a/hubs/models.py +++ b/hubs/models.py @@ -242,7 +242,7 @@ class Hub(BASE): def create_group_hub(cls, session, name, summary, **extra): hub = cls(name=name, summary=summary, # TODO -- do something else, smarter for group avatars - avatar=username2avatar(username), + avatar=username2avatar(name), user_hub=False) session.add(hub)