From 82fc20ed0dc004bb0b45a1fbe423195738b2ef41 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lorenz=20H=C3=BCbschle-Schneider?= Date: Sat, 20 Dec 2014 20:10:33 +0100 Subject: [PATCH 01/31] Allow % and ~ in front of channel names as well (irclinky) Those are IRC modifiers, we want /whois output to be clickable --- js/filters.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/js/filters.js b/js/filters.js index 3ca8297..68f01be 100644 --- a/js/filters.js +++ b/js/filters.js @@ -35,7 +35,7 @@ weechat.filter('irclinky', ['$filter', function($filter) { // However, it matches all *common* IRC channels while trying to minimise false positives. // "#1" is much more likely to be "number 1" than "IRC channel #1". // Thus, we only match channels beginning with a # and having at least one letter in them. - var channelRegex = /(^|[\s,.:;?!"'()+@-])(#+[^\x00\x07\r\n\s,:]*[a-z][^\x00\x07\r\n\s,:]*)/gmi; + var channelRegex = /(^|[\s,.:;?!"'()+@-\~%])(#+[^\x00\x07\r\n\s,:]*[a-z][^\x00\x07\r\n\s,:]*)/gmi; // This is SUPER nasty, but ng-click does not work inside a filter, as the markup has to be $compiled first, which is not possible in filter afaik. // Therefore, get the scope, fire the method, and $apply. Yuck. I sincerely hope someone finds a better way of doing this. var substitute = '$1$2'; From 1478b611da46c3b008856cf8bf44b5eb42f5e5e8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lorenz=20H=C3=BCbschle-Schneider?= Date: Tue, 30 Dec 2014 18:22:20 +0100 Subject: [PATCH 02/31] Properly escape HTML entities in irclinky filter Fixes #525 --- js/filters.js | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/js/filters.js b/js/filters.js index 68f01be..886dc6b 100644 --- a/js/filters.js +++ b/js/filters.js @@ -30,6 +30,13 @@ weechat.filter('irclinky', ['$filter', function($filter) { return text; } + // First, escape entities to prevent escaping issues because it's a bad idea + // to parse/modify HTML with regexes, which we do a couple of lines down... + var entities = {"<": "<", ">": ">", '"': '"', "'": ''', "&": "&", "/": '/'}; + text = text.replace(/[<>"'&\/]/g, function (char) { + return entities[char]; + }); + // This regex in no way matches all IRC channel names (they could also begin with &, + or an // exclamation mark followed by 5 alphanumeric characters, and are bounded in length by 50). // However, it matches all *common* IRC channels while trying to minimise false positives. From 8a740b765aef6a4f8cc5aa261669331d9617bd30 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lorenz=20H=C3=BCbschle-Schneider?= Date: Tue, 30 Dec 2014 20:23:32 +0100 Subject: [PATCH 03/31] Fix DOMfilter when replacing multiple occasions Previously, it would sometimes forget things at the end --- js/filters.js | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/js/filters.js b/js/filters.js index 886dc6b..4f0d6d9 100644 --- a/js/filters.js +++ b/js/filters.js @@ -94,13 +94,15 @@ weechat.filter('DOMfilter', ['$filter', '$sce', function($filter, $sce) { } else { parent.appendChild(newNode); } + return newNode; } } // recurse + if (node === undefined || node === null) return; node = node.firstChild; while (node) { - process(node); - node = node.nextSibling; + var nextNode = process(node); + node = (nextNode ? nextNode : node).nextSibling; } }; From 0c20484b5a7b0b04b390f7a640dff1d8c9fe6d75 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lorenz=20H=C3=BCbschle-Schneider?= Date: Tue, 30 Dec 2014 21:06:17 +0100 Subject: [PATCH 04/31] Add inline colour support for rgb(12,34,56) / rgba(1,2,3,0.4) colours Also improve the regexes --- js/filters.js | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/js/filters.js b/js/filters.js index 68f01be..e77933a 100644 --- a/js/filters.js +++ b/js/filters.js @@ -50,10 +50,12 @@ weechat.filter('inlinecolour', function() { } // only match 6-digit colour codes, 3-digit ones have too many false positives (issue numbers, etc) - var hexColourRegex = /(^|[^&])\#([0-9a-f]{6})($|[^\w'"])/gmi; - var substitute = '$1#$2
$3'; - - return text.replace(hexColourRegex, substitute); + var hexColourRegex = /(^|[^&])(\#[0-9a-f]{6};?)(?!\w)/gmi; + var rgbColourRegex = /(.?)(rgba?\((?:\s*\d+\s*,){2}\s*\d+\s*(?:,\s*[\d.]+\s*)?\);?)/gmi; + var substitute = '$1$2
'; + text = text.replace(hexColourRegex, substitute); + text = text.replace(rgbColourRegex, substitute); + return text; }; }); From de730a4505257ab7783400e332bf9c425789214b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lorenz=20H=C3=BCbschle-Schneider?= Date: Tue, 30 Dec 2014 21:05:40 +0100 Subject: [PATCH 05/31] Add some basic tests for filters --- bower.json | 4 ++- js/glowingbear.js | 5 +++- test/karma.conf.js | 2 ++ test/unit/filters.js | 65 ++++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 74 insertions(+), 2 deletions(-) create mode 100644 test/unit/filters.js diff --git a/bower.json b/bower.json index 8557a60..cb3a694 100644 --- a/bower.json +++ b/bower.json @@ -8,8 +8,10 @@ "dependencies": { "angular": "1.3.x", "angular-route": "1.3.x", + "angular-sanitize": "1.3.x", + "angular-touch": "1.3.x", "angular-loader": "1.3.x", - "angular-mocks": "~1.3.x", + "angular-mocks": "1.3.x", "html5-boilerplate": "~4.3.0" } } diff --git a/js/glowingbear.js b/js/glowingbear.js index 66b9730..90679ad 100644 --- a/js/glowingbear.js +++ b/js/glowingbear.js @@ -3,7 +3,10 @@ var weechat = angular.module('weechat', ['ngRoute', 'localStorage', 'weechatModels', 'plugins', 'IrcUtils', 'ngSanitize', 'ngWebsockets', 'ngTouch']); weechat.config(['$compileProvider', function ($compileProvider) { - $compileProvider.debugInfoEnabled(false); + // hack to determine whether we're executing the tests + if (typeof(it) === "undefined" && typeof(describe) === "undefined") { + $compileProvider.debugInfoEnabled(false); + } }]); weechat.controller('WeechatCtrl', ['$rootScope', '$scope', '$store', '$timeout', '$log', 'models', 'connection', 'notifications', 'utils', function ($rootScope, $scope, $store, $timeout, $log, models, connection, notifications, utils) { diff --git a/test/karma.conf.js b/test/karma.conf.js index 94d1739..66e3f04 100644 --- a/test/karma.conf.js +++ b/test/karma.conf.js @@ -7,6 +7,8 @@ module.exports = function(config){ 'bower_components/angular/angular.js', 'bower_components/angular-route/angular-route.js', 'bower_components/angular-mocks/angular-mocks.js', + 'bower_components/angular-sanitize/angular-sanitize.js', + 'bower_components/angular-touch/angular-touch.js', 'js/localstorage.js', 'js/weechat.js', 'js/irc-utils.js', diff --git a/test/unit/filters.js b/test/unit/filters.js new file mode 100644 index 0000000..934996e --- /dev/null +++ b/test/unit/filters.js @@ -0,0 +1,65 @@ +var weechat = angular.module('weechat'); + +describe('Filters', function() { + beforeEach(module('weechat')); + /*beforeEach(module(function($provide) { + $provide.value('version', 'TEST_VER'); + }));*/ + + it('has an irclinky filter', inject(function($filter) { + expect($filter('irclinky')).not.toBeNull(); + })); + + describe('irclinky', function() { + it('should not mess up text', inject(function(irclinkyFilter) { + expect(irclinkyFilter('foo')).toEqual('foo'); + })); + + it('should linkify IRC channels', inject(function(irclinkyFilter) { + expect(irclinkyFilter('#foo')).toEqual('#foo'); + })); + + it('should not mess up IRC channels surrounded by HTML entities', inject(function(irclinkyFilter) { + expect(irclinkyFilter('<"#foo">')).toEqual('<"#foo">'); + })); + }); + + describe('inlinecolour', function() { + it('should not mess up normal text', inject(function(inlinecolourFilter) { + expect(inlinecolourFilter('foo')).toEqual('foo'); + expect(inlinecolourFilter('test #foobar baz')).toEqual('test #foobar baz'); + })); + + it('should detect inline colours in #rrggbb format', inject(function(inlinecolourFilter) { + expect(inlinecolourFilter('#123456')).toEqual('#123456
'); + expect(inlinecolourFilter('#aabbcc')).toEqual('#aabbcc
'); + })); + + it('should not detect inline colours in #rgb format', inject(function(inlinecolourFilter) { + expect(inlinecolourFilter('#123')).toEqual('#123'); + expect(inlinecolourFilter('#abc')).toEqual('#abc'); + })); + + it('should detect inline colours in rgb(12,34,56) and rgba(12,34,56,0.78) format', inject(function(inlinecolourFilter) { + expect(inlinecolourFilter('rgb(1,2,3)')).toEqual('rgb(1,2,3)
'); + expect(inlinecolourFilter('rgb(1,2,3);')).toEqual('rgb(1,2,3);
'); + expect(inlinecolourFilter('rgba(1,2,3,0.4)')).toEqual('rgba(1,2,3,0.4)
'); + expect(inlinecolourFilter('rgba(255,123,0,0.5);')).toEqual('rgba(255,123,0,0.5);
'); + })); + + it('should tolerate whitespace in between numbers in rgb/rgba colours', inject(function(inlinecolourFilter) { + expect(inlinecolourFilter('rgb( 1\t, 2 , 3 )')).toEqual('rgb( 1\t, 2 , 3 )
'); + })); + + it('should handle multiple and mixed occurrences of colour values', inject(function(inlinecolourFilter) { + expect(inlinecolourFilter('rgb(1,2,3) #123456')).toEqual('rgb(1,2,3)
#123456
'); + expect(inlinecolourFilter('#f00baa #123456 #234567')).toEqual('#f00baa
#123456
#234567
'); + expect(inlinecolourFilter('rgba(1,2,3,0.4) foorgb(50,100,150)')).toEqual('rgba(1,2,3,0.4)
foorgb(50,100,150)
'); + })); + + it('should not replace HTML escaped 𞉀', inject(function(inlinecolourFilter) { + expect(inlinecolourFilter('𞉀')).toEqual('𞉀'); + })); + }); + +}); From 1d4caa91cdf128700c807eb55a3089d1d54fa146 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lorenz=20H=C3=BCbschle-Schneider?= Date: Wed, 19 Nov 2014 14:25:31 +0100 Subject: [PATCH 06/31] Parse colours in buffer title Requires a WeeChat version including https://github.com/weechat/weechat/commit/013165209af818daba6b6c5869e822ec9cb86fc3 i.e., WeeChat 1.1 or a November 2014 nightly (or later) (for previous versions, see https://github.com/weechat/weechat/issues/237) Fixes #308 --- index.html | 4 +- js/handlers.js | 7 +++- js/models.js | 96 ++++++++++++++++++++++++--------------------- js/notifications.js | 2 +- 4 files changed, 61 insertions(+), 48 deletions(-) diff --git a/index.html b/index.html index 112d994..8fb2157 100644 --- a/index.html +++ b/index.html @@ -215,7 +215,9 @@ $ openssl req -nodes -newkey rsa:4096 -keyout relay.pem -x509 -days 365 -out rel -
+
+ +
diff --git a/js/handlers.js b/js/handlers.js index e1dfe9d..065a4b8 100644 --- a/js/handlers.js +++ b/js/handlers.js @@ -64,8 +64,13 @@ weechat.factory('handlers', ['$rootScope', '$log', 'models', 'plugins', 'notific var buffer = obj.pointers[0]; var old = models.getBuffer(buffer); old.fullName = obj.full_name; - old.title = obj.title; + old.title = models.parseRichText(obj.title); old.number = obj.number; + + old.rtitle = ""; + for (var i = 0; i < old.title.length; ++i) { + old.rtitle += old.title[i].text; + } }; var handleBufferRenamed = function(message) { diff --git a/js/models.js b/js/models.js index 8f576f8..6ee7295 100644 --- a/js/models.js +++ b/js/models.js @@ -8,6 +8,48 @@ var models = angular.module('weechatModels', []); models.service('models', ['$rootScope', '$filter', function($rootScope, $filter) { + var parseRichText = function(text) { + var textElements = weeChat.Protocol.rawText2Rich(text), + typeToClassPrefixFg = { + 'option': 'cof-', + 'weechat': 'cwf-', + 'ext': 'cef-' + }, + typeToClassPrefixBg = { + 'option': 'cob-', + 'weechat': 'cwb-', + 'ext': 'ceb-' + }; + + textElements.forEach(function(textEl) { + textEl.classes = []; + + // foreground color + var prefix = typeToClassPrefixFg[textEl.fgColor.type]; + textEl.classes.push(prefix + textEl.fgColor.name); + + // background color + prefix = typeToClassPrefixBg[textEl.bgColor.type]; + textEl.classes.push(prefix + textEl.bgColor.name); + + // attributes + if (textEl.attrs.name !== null) { + textEl.classes.push('coa-' + textEl.attrs.name); + } + var attr, val; + for (attr in textEl.attrs.override) { + val = textEl.attrs.override[attr]; + if (val) { + textEl.classes.push('a-' + attr); + } else { + textEl.classes.push('a-no-' + attr); + } + } + }); + return textElements; + }; + this.parseRichText = parseRichText; + /* * Buffer class */ @@ -21,7 +63,7 @@ models.service('models', ['$rootScope', '$filter', function($rootScope, $filter) var trimmedName = shortName.replace(/^[#&+]/, '') || (shortName ? ' ' : null); // get channel identifier var prefix = ['#', '&', '+'].indexOf(shortName.charAt(0)) >= 0 ? shortName.charAt(0) : ''; - var title = message.title; + var title = parseRichText(message.title); var number = message.number; var pointer = message.pointers[0]; var notify = 3; // Default 3 == message @@ -44,6 +86,11 @@ models.service('models', ['$rootScope', '$filter', function($rootScope, $filter) notify = message.notify; } + var rtitle = ""; + for (var i = 0; i < title.length; ++i) { + rtitle += title[i].text; + } + /* * Adds a line to this buffer * @@ -234,6 +281,7 @@ models.service('models', ['$rootScope', '$filter', function($rootScope, $filter) prefix: prefix, number: number, title: title, + rtitle: rtitle, lines: lines, clear: clear, requestedLines: requestedLines, @@ -268,53 +316,11 @@ models.service('models', ['$rootScope', '$filter', function($rootScope, $filter) var date = message.date; var shortTime = $filter('date')(date, 'HH:mm'); - function addClasses(textElements) { - var typeToClassPrefixFg = { - 'option': 'cof-', - 'weechat': 'cwf-', - 'ext': 'cef-' - }; - var typeToClassPrefixBg = { - 'option': 'cob-', - 'weechat': 'cwb-', - 'ext': 'ceb-' - }; - textElements.forEach(function(textEl) { - textEl.classes = []; - - // foreground color - var prefix = typeToClassPrefixFg[textEl.fgColor.type]; - textEl.classes.push(prefix + textEl.fgColor.name); - - // background color - prefix = typeToClassPrefixBg[textEl.bgColor.type]; - textEl.classes.push(prefix + textEl.bgColor.name); - - // attributes - if (textEl.attrs.name !== null) { - textEl.classes.push('coa-' + textEl.attrs.name); - } - var val; - for (var attr in textEl.attrs.override) { - val = textEl.attrs.override[attr]; - if (val) { - textEl.classes.push('a-' + attr); - } else { - textEl.classes.push('a-no-' + attr); - } - } - }); - } - - - var prefix = weeChat.Protocol.rawText2Rich(message.prefix); - addClasses(prefix); - + var prefix = parseRichText(message.prefix); var tags_array = message.tags_array; var displayed = message.displayed; var highlight = message.highlight; - var content = weeChat.Protocol.rawText2Rich(message.message); - addClasses(content); + var content = parseRichText(message.message); if (highlight) { prefix.forEach(function(textEl) { diff --git a/js/notifications.js b/js/notifications.js index fbeba09..6bd71c9 100644 --- a/js/notifications.js +++ b/js/notifications.js @@ -52,7 +52,7 @@ weechat.factory('notifications', ['$rootScope', '$log', 'models', function($root var activeBuffer = models.getActiveBuffer(); if (activeBuffer) { - $rootScope.pageTitle = activeBuffer.shortName + ' | ' + activeBuffer.title; + $rootScope.pageTitle = activeBuffer.shortName + ' | ' + activeBuffer.rtitle; } }; From f125c43ccb45105c485af0715f24ac2d6b5c61cf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lorenz=20H=C3=BCbschle-Schneider?= Date: Sat, 3 Jan 2015 18:29:59 +0100 Subject: [PATCH 07/31] Adds emoji support using Twitter's twemoji --- README.md | 2 +- css/glowingbear.css | 8 ++++++++ index.html | 13 ++++++++++++- js/filters.js | 16 +++++++++++++++- js/glowingbear.js | 2 ++ 5 files changed, 38 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 7e81b37..7416a74 100644 --- a/README.md +++ b/README.md @@ -83,4 +83,4 @@ If you wish to submit code, we try to make the contribution process as simple as We'd also like to ask you to join our IRC channel, #glowing-bear on freenode, so we can discuss your ideas and changes. -If you're curious about the projects we're using, here's a list: [AngularJS](https://angularjs.org/), [Bootstrap](http://getbootstrap.com/), [Underscore](http://underscorejs.org/), [favico.js](http://lab.ejci.net/favico.js/), and [zlib.js](https://github.com/imaya/zlib.js). Technology-wise, [WebSockets](http://en.wikipedia.org/wiki/WebSocket) are the most important part, but we also use [local storage](https://developer.mozilla.org/en-US/docs/Web/Guide/API/DOM/Storage#localStorage), the [Notification Web API](https://developer.mozilla.org/en/docs/Web/API/notification), and last (but not least) [Apache Cordova](https://cordova.apache.org/) for our mobile app. +If you're curious about the projects we're using, here's a list: [AngularJS](https://angularjs.org/), [Bootstrap](http://getbootstrap.com/), [Underscore](http://underscorejs.org/), [favico.js](http://lab.ejci.net/favico.js/), [twemoji](https://github.com/twitter/twemoji), and [zlib.js](https://github.com/imaya/zlib.js). Technology-wise, [WebSockets](http://en.wikipedia.org/wiki/WebSocket) are the most important part, but we also use [local storage](https://developer.mozilla.org/en-US/docs/Web/Guide/API/DOM/Storage#localStorage), the [Notification Web API](https://developer.mozilla.org/en/docs/Web/API/notification), and last (but not least) [Apache Cordova](https://cordova.apache.org/) for our mobile app. diff --git a/css/glowingbear.css b/css/glowingbear.css index 9c7d540..d171615 100644 --- a/css/glowingbear.css +++ b/css/glowingbear.css @@ -522,6 +522,14 @@ li.buffer.indent.private a { user-select: none; } +/* Scales emoji to font size */ +img.emoji { + height: 1em; + width: 1em; + margin: 0 .05em 0 .1em; + vertical-align: -0.1em; +} + /* */ /* Mobile layout */ /* */ diff --git a/index.html b/index.html index 112d994..9875d17 100644 --- a/index.html +++ b/index.html @@ -18,6 +18,7 @@ + @@ -270,7 +271,7 @@ $ openssl req -nodes -newkey rsa:4096 -keyout relay.pem -x509 -days 365 -out rel
+ --> @@ -425,6 +426,16 @@ $ openssl req -nodes -newkey rsa:4096 -keyout relay.pem -x509 -days 365 -out rel
+
  • +
    +
    + +
    +
    +
  • - +
    @@ -306,12 +307,12 @@ $ openssl req -nodes -newkey rsa:4096 -keyout relay.pem -x509 -days 365 -out rel
    - +
    - +
    @@ -321,7 +322,7 @@ $ openssl req -nodes -newkey rsa:4096 -keyout relay.pem -x509 -days 365 -out rel
    - +
    @@ -331,7 +332,7 @@ $ openssl req -nodes -newkey rsa:4096 -keyout relay.pem -x509 -days 365 -out rel
    @@ -341,17 +342,17 @@ $ openssl req -nodes -newkey rsa:4096 -keyout relay.pem -x509 -days 365 -out rel
    -
      +
      • @@ -363,7 +364,7 @@ $ openssl req -nodes -newkey rsa:4096 -keyout relay.pem -x509 -days 365 -out rel
        @@ -373,7 +374,7 @@ $ openssl req -nodes -newkey rsa:4096 -keyout relay.pem -x509 -days 365 -out rel
        @@ -383,7 +384,7 @@ $ openssl req -nodes -newkey rsa:4096 -keyout relay.pem -x509 -days 365 -out rel
        @@ -393,7 +394,7 @@ $ openssl req -nodes -newkey rsa:4096 -keyout relay.pem -x509 -days 365 -out rel
        @@ -403,7 +404,7 @@ $ openssl req -nodes -newkey rsa:4096 -keyout relay.pem -x509 -days 365 -out rel
        @@ -413,7 +414,7 @@ $ openssl req -nodes -newkey rsa:4096 -keyout relay.pem -x509 -days 365 -out rel
        @@ -423,7 +424,7 @@ $ openssl req -nodes -newkey rsa:4096 -keyout relay.pem -x509 -days 365 -out rel
        diff --git a/js/glowingbear.js b/js/glowingbear.js index 1e7fc3a..ae4a92a 100644 --- a/js/glowingbear.js +++ b/js/glowingbear.js @@ -12,10 +12,12 @@ weechat.config(['$compileProvider', function ($compileProvider) { } }]); -weechat.controller('WeechatCtrl', ['$rootScope', '$scope', '$store', '$timeout', '$log', 'models', 'connection', 'notifications', 'utils', function ($rootScope, $scope, $store, $timeout, $log, models, connection, notifications, utils) { +weechat.controller('WeechatCtrl', ['$rootScope', '$scope', '$store', '$timeout', '$log', 'models', 'connection', 'notifications', 'utils', 'settings', + function ($rootScope, $scope, $store, $timeout, $log, models, connection, notifications, utils, settings) { $scope.command = ''; $scope.themes = ['dark', 'light']; + $scope.settings = settings; // From: http://stackoverflow.com/a/18539624 by StackOverflow user "plantian" $rootScope.countWatchers = function () { @@ -193,7 +195,7 @@ weechat.controller('WeechatCtrl', ['$rootScope', '$scope', '$store', '$timeout', // we will send a /buffer bufferName command every time // the user switches a buffer. This will ensure that notifications // are cleared in the buffer the user switches to - if ($scope.hotlistsync && ab.fullName) { + if (settings.hotlistsync && ab.fullName) { connection.sendCoreCommand('/buffer ' + ab.fullName); } @@ -215,7 +217,7 @@ weechat.controller('WeechatCtrl', ['$rootScope', '$scope', '$store', '$timeout', $rootScope.$on('notificationChanged', function() { notifications.updateTitle(); - if ($scope.useFavico && $rootScope.favico) { + if (settings.useFavico && $rootScope.favico) { notifications.updateFavico(); } }); @@ -246,72 +248,31 @@ weechat.controller('WeechatCtrl', ['$rootScope', '$scope', '$store', '$timeout', $rootScope.iterCandidate = null; - $store.bind($scope, "host", "localhost"); - $store.bind($scope, "port", "9001"); - $store.bind($scope, "proto", "weechat"); - $store.bind($scope, "ssl", (window.location.protocol === "https:")); - $store.bind($scope, "savepassword", false); - if ($scope.savepassword) { - $store.bind($scope, "password", ""); + if (settings.savepassword) { + $scope.$watch('password', function() { + settings.password = $scope.password; + }); + settings.addCallback('password', function(password) { + $scope.password = password; + }); + $scope.password = settings.password; + } else { + settings.password = ''; } - $store.bind($scope, "autoconnect", false); - - // If we are on mobile change some defaults - // We use 968 px as the cutoff, which should match the value in glowingbear.css - var nonicklist = false; - var noembed = false; - var showtimestamp = true; $rootScope.wasMobileUi = false; - if (utils.isMobileUi()) { - nonicklist = true; - noembed = true; $rootScope.wasMobileUi = true; } - - // Save setting for displaying only buffers with unread messages - $store.bind($scope, "onlyUnread", false); - - // Save setting for syncing hotlist - $store.bind($scope, "hotlistsync", true); - // Save setting for displaying nicklist - $store.bind($scope, "nonicklist", nonicklist); - // Save setting for displaying embeds - $store.bind($scope, "noembed", noembed); - // Save setting for channel ordering - $store.bind($scope, "orderbyserver", true); - // Save setting for updating favicon - $store.bind($scope, "useFavico", true); - // Save setting for showtimestamp - $store.bind($scope, "showtimestamp", showtimestamp); - // Save setting for showing seconds on timestamps - $store.bind($scope, "showtimestampSeconds", false); - // Save setting for playing sound on notification - $store.bind($scope, "soundnotification", false); - // Save setting for font family - $store.bind($scope, "fontfamily"); - // Save setting for theme - $store.bind($scope, "theme", 'dark'); - // Save setting for font size - $store.bind($scope, "fontsize", "14px"); - // Save setting for readline keybindings - $store.bind($scope, "readlineBindings", false); - // Save settings for non-native Emoji support - $store.bind($scope, "enableJSEmoji", false); - - if (!$scope.fontfamily) { + if (!settings.fontfamily) { if (utils.isMobileUi()) { - $scope.fontfamily = 'sans-serif'; + settings.fontfamily = 'sans-serif'; } else { - $scope.fontfamily = "Inconsolata, Consolas, Monaco, Ubuntu Mono, monospace"; + settings.fontfamily = "Inconsolata, Consolas, Monaco, Ubuntu Mono, monospace"; } } - // Save setting for displaying embeds in rootScope so it can be used from service - $rootScope.auto_display_embedded_content = $scope.noembed === false; - $scope.isSidebarVisible = function() { return document.getElementById('content').getAttribute('sidebar-state') === 'visible'; }; @@ -333,9 +294,8 @@ weechat.controller('WeechatCtrl', ['$rootScope', '$scope', '$store', '$timeout', document.getElementById('content').setAttribute('sidebar-state', 'hidden'); } }; - // This also fires on page load - $scope.$watch('autoconnect', function() { - if ($scope.autoconnect && !$rootScope.connected && !$rootScope.sslError && !$rootScope.securityError && !$rootScope.errorMessage) { + settings.addCallback('autoconnect', function(autoconnect) { + if (autoconnect && !$rootScope.connected && !$rootScope.sslError && !$rootScope.securityError && !$rootScope.errorMessage) { $scope.connect(); } }); @@ -354,35 +314,31 @@ weechat.controller('WeechatCtrl', ['$rootScope', '$scope', '$store', '$timeout', // Open and close panels while on mobile devices through swiping $scope.openNick = function() { if (utils.isMobileUi()) { - if ($scope.nonicklist) { - $scope.nonicklist = false; + if (settings.nonicklist) { + settings.nonicklist = false; } } }; $scope.closeNick = function() { if (utils.isMobileUi()) { - if (!$scope.nonicklist) { - $scope.nonicklist = true; + if (!settings.nonicklist) { + settings.nonicklist = true; } } }; - // Watch model and update show setting when it changes - $scope.$watch('noembed', function() { - $rootScope.auto_display_embedded_content = $scope.noembed === false; - }); // Watch model and update channel sorting when it changes - $scope.$watch('orderbyserver', function() { - $rootScope.predicate = $scope.orderbyserver ? 'serverSortKey' : 'number'; + settings.addCallback('orderbyserver', function(orderbyserver) { + $rootScope.predicate = orderbyserver ? 'serverSortKey' : 'number'; }); - $scope.$watch('useFavico', function() { + settings.addCallback('useFavico', function(useFavico) { // this check is necessary as this is called on page load, too if (!$rootScope.connected) { return; } - if ($scope.useFavico) { + if (useFavico) { notifications.updateFavico(); } else { $rootScope.favico.reset(); @@ -390,17 +346,12 @@ weechat.controller('WeechatCtrl', ['$rootScope', '$scope', '$store', '$timeout', }); // Update font family when changed - $scope.$watch('fontfamily', function() { - utils.changeClassStyle('favorite-font', 'fontFamily', $scope.fontfamily); + settings.addCallback('fontfamily', function(fontfamily) { + utils.changeClassStyle('favorite-font', 'fontFamily', fontfamily); }); // Update font size when changed - $scope.$watch('fontsize', function() { - utils.changeClassStyle('favorite-font', 'fontSize', $scope.fontsize); - }); - // Crude scoping hack. The keypress listener does not live in the same scope as - // the checkbox, so we need to transfer this between scopes here. - $scope.$watch('readlineBindings', function() { - $rootScope.readlineBindings = $scope.readlineBindings; + settings.addCallback('fontsize', function(fontsize) { + utils.changeClassStyle('favorite-font', 'fontSize', fontsize); }); $scope.setActiveBuffer = function(bufferId, key) { @@ -526,7 +477,7 @@ weechat.controller('WeechatCtrl', ['$rootScope', '$scope', '$store', '$timeout', $rootScope.errorMessage = false; $rootScope.bufferBottom = true; $scope.connectbutton = 'Connecting ...'; - connection.connect($scope.host, $scope.port, $scope.password, $scope.ssl); + connection.connect(settings.host, settings.port, $scope.password, settings.ssl); }; $scope.disconnect = function() { $scope.connectbutton = 'Connect'; @@ -594,7 +545,7 @@ weechat.controller('WeechatCtrl', ['$rootScope', '$scope', '$store', '$timeout', if ($scope.search && $scope.search !== "") { return true; } - if ($scope.onlyUnread) { + if (settings.onlyUnread) { // Always show current buffer in list if (models.getActiveBuffer() === buffer) { return true; @@ -609,7 +560,7 @@ weechat.controller('WeechatCtrl', ['$rootScope', '$scope', '$store', '$timeout', }; // Watch model and update show setting when it changes - $scope.$watch('nonicklist', function() { + settings.addCallback('nonicklist', function() { $scope.showNicklist = $scope.updateShowNicklist(); // restore bottom view if ($rootScope.connected && $rootScope.bufferBottom) { @@ -628,7 +579,7 @@ weechat.controller('WeechatCtrl', ['$rootScope', '$scope', '$store', '$timeout', return false; } // Check if option no nicklist is set - if ($scope.nonicklist) { + if (settings.nonicklist) { return false; } // Check if nicklist is empty @@ -662,7 +613,7 @@ weechat.controller('WeechatCtrl', ['$rootScope', '$scope', '$store', '$timeout', }; // Helper function since the keypress handler is in a different scope $rootScope.toggleNicklist = function() { - $scope.nonicklist = !$scope.nonicklist; + settings.nonicklist = !settings.nonicklist; }; diff --git a/js/inputbar.js b/js/inputbar.js index e65a327..ab4394a 100644 --- a/js/inputbar.js +++ b/js/inputbar.js @@ -14,13 +14,14 @@ weechat.directive('inputBar', function() { command: '=command' }, - controller: ['$rootScope', '$scope', '$element', '$log', 'connection', 'models', 'IrcUtils', function($rootScope, + controller: ['$rootScope', '$scope', '$element', '$log', 'connection', 'models', 'IrcUtils', 'settings', function($rootScope, $scope, $element, //XXX do we need this? don't seem to be using it $log, connection, //XXX we should eliminate this dependency and use signals instead models, - IrcUtils) { + IrcUtils, + settings) { /* * Returns the input element @@ -340,7 +341,7 @@ weechat.directive('inputBar', function() { } // Some readline keybindings - if ($rootScope.readlineBindings && $event.ctrlKey && !$event.altKey && !$event.shiftKey && document.activeElement === inputNode) { + if (settings.readlineBindings && $event.ctrlKey && !$event.altKey && !$event.shiftKey && document.activeElement === inputNode) { // get current caret position caretPos = inputNode.selectionStart; // Ctrl-a diff --git a/js/localstorage.js b/js/localstorage.js index 4f2a9da..0c1218e 100644 --- a/js/localstorage.js +++ b/js/localstorage.js @@ -81,6 +81,16 @@ ls.factory("$store", ["$parse", function($parse){ storage.removeItem(key); return true; }, + /** + * Enumerate all keys + */ + enumerateKeys: function() { + var keys = []; + for (var i = 0, len = storage.length; i < len; ++i) { + keys.push(storage.key(i)); + } + return keys; + }, /** * Bind - lets you directly bind a localStorage value to a $scope variable * @param $scope - the current scope you want the variable available in diff --git a/js/notifications.js b/js/notifications.js index 00cfb32..739a651 100644 --- a/js/notifications.js +++ b/js/notifications.js @@ -1,9 +1,8 @@ var weechat = angular.module('weechat'); -weechat.factory('notifications', ['$rootScope', '$log', 'models', function($rootScope, $log, models) { - var notifications = []; - +weechat.factory('notifications', ['$rootScope', '$log', 'models', 'settings', function($rootScope, $log, models, settings) { // Ask for permission to display desktop notifications + var notifications = []; var requestNotificationPermission = function() { // Firefox if (window.Notification) { @@ -135,7 +134,7 @@ weechat.factory('notifications', ['$rootScope', '$log', 'models', function($root delete notifications[this.id]; }; - if ($rootScope.soundnotification) { + if (settings.soundnotification) { // TODO fill in a sound file var audioFile = "assets/audio/sonar"; var soundHTML = ''; @@ -153,10 +152,10 @@ weechat.factory('notifications', ['$rootScope', '$log', 'models', function($root }; return { - requestNotificationPermission: requestNotificationPermission, - updateTitle: updateTitle, - updateFavico: updateFavico, - createHighlight: createHighlight, + requestNotificationPermission: requestNotificationPermission, + updateTitle: updateTitle, + updateFavico: updateFavico, + createHighlight: createHighlight, cancelAll: cancelAll, }; }]); diff --git a/js/plugin-directive.js b/js/plugin-directive.js index f61cba4..94d19f5 100644 --- a/js/plugin-directive.js +++ b/js/plugin-directive.js @@ -3,7 +3,7 @@ var weechat = angular.module('weechat'); -weechat.directive('plugin', ['$rootScope', function($rootScope) { +weechat.directive('plugin', ['$rootScope', 'settings', function($rootScope, settings) { /* * Plugin directive * Shows additional plugin content @@ -20,7 +20,7 @@ weechat.directive('plugin', ['$rootScope', function($rootScope) { $scope.displayedContent = ""; // Auto-display embedded content only if it isn't NSFW - $scope.plugin.visible = $rootScope.auto_display_embedded_content && !$scope.plugin.nsfw; + $scope.plugin.visible = !settings.noembed && !$scope.plugin.nsfw; // user-accessible hash key that is a valid CSS class name $scope.plugin.className = "embed_" + $scope.plugin.$$hashKey.replace(':','_'); diff --git a/js/settings.js b/js/settings.js new file mode 100644 index 0000000..3da50d3 --- /dev/null +++ b/js/settings.js @@ -0,0 +1,59 @@ +(function() { +'use strict'; + +var weechat = angular.module('weechat'); + +weechat.factory('settings', ['$store', '$rootScope', function($store, $rootScope) { + var that = this; + this.callbacks = {}; + + // Define a property for a setting, retrieving it on read + // and writing it to localStorage on write + var defineProperty = function(key) { + Object.defineProperty(that, key, { + enumerable: true, + key: key, + get: function() { + return $store.get(key); + }, + set: function(newVal) { + $store.set(key, newVal); + // Call any callbacks + var callbacks = that.callbacks[key]; + for (var i = 0; callbacks !== undefined && i < callbacks.length; i++) { + callbacks[i](newVal); + } + // Update the page (might be needed) + setTimeout(function() { + $rootScope.$apply(); + }, 0); + } + }); + }; + + // Define properties for all settings + var keys = $store.enumerateKeys(); + for (var keyIdx in keys) { + var key = keys[keyIdx]; + defineProperty(key); + } + + // Add a callback to be called whenever the value is changed + // It's like a free $watch and used to be called the observer + // pattern, but I guess that's too old-school for JS kids :> + this.addCallback = function(key, callback, callNow) { + if (this.callbacks[key] === undefined) { + this.callbacks[key] = [callback]; + } else { + this.callbacks[key].push(callback); + } + // call now to emulate $watch behaviour + setTimeout(function() { + callback($store.get(key)); + }, 0); + }; + + return this; +}]); + +})(); From 54bb9dad139b8541588533a0fc8e365d34c54743 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lorenz=20H=C3=BCbschle-Schneider?= Date: Tue, 30 Dec 2014 20:24:33 +0100 Subject: [PATCH 20/31] Add default settings again --- js/glowingbear.js | 20 ++++++++++++++++++++ js/settings.js | 9 +++++++++ 2 files changed, 29 insertions(+) diff --git a/js/glowingbear.js b/js/glowingbear.js index ae4a92a..cc72e6b 100644 --- a/js/glowingbear.js +++ b/js/glowingbear.js @@ -17,6 +17,26 @@ weechat.controller('WeechatCtrl', ['$rootScope', '$scope', '$store', '$timeout', $scope.command = ''; $scope.themes = ['dark', 'light']; + + settings.setDefaults({ + 'theme': 'dark', + 'host': 'localhost', + 'port': 9001, + 'ssl': (window.location.protocol === "https:"), + 'savepassword': false, + 'autoconnect': false, + 'nonicklist': utils.isMobileUi(), + 'noembed': utils.isMobileUi(), + 'onlyUnread': false, + 'hotlistsync': true, + 'orderbyserver': true, + 'useFavico': true, + 'showtimestamp': true, + 'showtimestampSeconds': false, + 'fontsize': '14px', + 'fontfamily': (utils.isMobileUi() ? 'sans-serif' : 'Inconsolata, Consolas, Monaco, Ubuntu Mono, monospace'), + 'readlineBindings': false + }); $scope.settings = settings; // From: http://stackoverflow.com/a/18539624 by StackOverflow user "plantian" diff --git a/js/settings.js b/js/settings.js index 3da50d3..8c312fc 100644 --- a/js/settings.js +++ b/js/settings.js @@ -53,6 +53,15 @@ weechat.factory('settings', ['$store', '$rootScope', function($store, $rootScope }, 0); }; + this.setDefaults = function(defaults) { + for (var key in defaults) { + // null means the key isn't set + if ($store.get(key) === null) { + this[key] = defaults[key]; + } + } + }; + return this; }]); From d9c230ac44959f19f5715b2836d5c4c3aebfcd7e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lorenz=20H=C3=BCbschle-Schneider?= Date: Wed, 4 Feb 2015 22:50:35 +0100 Subject: [PATCH 21/31] Include settings.js in minification process; fix strict DI --- js/glowingbear.js | 4 ++-- package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/js/glowingbear.js b/js/glowingbear.js index cc72e6b..1be7dee 100644 --- a/js/glowingbear.js +++ b/js/glowingbear.js @@ -1,10 +1,10 @@ (function() { 'use strict'; -var weechat = angular.module('weechat', ['ngRoute', 'localStorage', 'weechatModels', 'plugins', 'IrcUtils', 'ngSanitize', 'ngWebsockets', 'ngTouch'], function($compileProvider) { +var weechat = angular.module('weechat', ['ngRoute', 'localStorage', 'weechatModels', 'plugins', 'IrcUtils', 'ngSanitize', 'ngWebsockets', 'ngTouch'], ['$compileProvider', function($compileProvider) { // hacky way to be able to find out if we're in debug mode weechat.compileProvider = $compileProvider; -}); +}]); weechat.config(['$compileProvider', function ($compileProvider) { // hack to determine whether we're executing the tests if (typeof(it) === "undefined" && typeof(describe) === "undefined") { diff --git a/package.json b/package.json index 3d509e6..fdf3709 100644 --- a/package.json +++ b/package.json @@ -18,7 +18,7 @@ "scripts": { "postinstall": "bower install", - "minify": " uglifyjs js/localstorage.js js/weechat.js js/irc-utils.js js/glowingbear.js js/utils.js js/notifications.js js/filters.js js/handlers.js js/connection.js js/inputbar.js js/plugin-directive.js js/websockets.js js/models.js js/plugins.js -c -m --screw-ie8 -o min.js --source-map min.map", + "minify": " uglifyjs js/localstorage.js js/weechat.js js/irc-utils.js js/glowingbear.js js/settings.js js/utils.js js/notifications.js js/filters.js js/handlers.js js/connection.js js/inputbar.js js/plugin-directive.js js/websockets.js js/models.js js/plugins.js -c -m --screw-ie8 -o min.js --source-map min.map", "prestart": "npm install", "start": "http-server -a localhost -p 8000", From 6522cd72d35e05df2d4eabeff45d67db1ad4ad0b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lorenz=20H=C3=BCbschle-Schneider?= Date: Sun, 8 Feb 2015 10:41:10 +0100 Subject: [PATCH 22/31] Fix Chrome application shortcut closes #521 --- index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/index.html b/index.html index 1cd5e00..4ef87ee 100644 --- a/index.html +++ b/index.html @@ -189,7 +189,7 @@ $ openssl req -nodes -newkey rsa:4096 -keyout relay.pem -x509 -days 365 -out rel

        If you have a recent version of Firefox you can install glowing bear as an app. Click the button to install.

        Chrome

        -

        To install glowing bear as an app in Chrome, select Menu - Add to home screen (Android) or Menu - Tools - Create Application Shortcuts (desktop version).

        +

        To install glowing bear as an app in Chrome, select Menu - Add to home screen (Android) or Menu - More tools - Create application shortcuts (desktop version).

        From 18c0db6c4fba86fb125f34901b7dc2f4d612a624 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lorenz=20H=C3=BCbschle-Schneider?= Date: Sun, 8 Feb 2015 11:16:55 +0100 Subject: [PATCH 23/31] Fix Asciinema plugin --- js/plugins.js | 20 +++++++++++++------- 1 file changed, 13 insertions(+), 7 deletions(-) diff --git a/js/plugins.js b/js/plugins.js index fd9bf51..1d971cd 100644 --- a/js/plugins.js +++ b/js/plugins.js @@ -312,15 +312,21 @@ plugins.factory('userPlugins', function() { /* * Asciinema plugin */ - var asciinemaPlugin = new Plugin(function(message) { - - var regexp = /^https?:\/\/(www\.)?asciinema.org\/a\/(\d+)/; - var match = message.match(regexp); + var asciinemaPlugin = new Plugin(urlPlugin(function(url) { + var regexp = /^https?:\/\/(?:www\.)?asciinema.org\/a\/(\d+)/i; + var match = url.match(regexp); if (match) { - var id = match[3]; - return ""; + var id = match[1]; + return function() { + var element = this.getElement(); + var scriptElem = document.createElement('script'); + scriptElem.src = 'https://asciinema.org/a/' + id + '.js'; + scriptElem.id = 'asciicast-' + id; + scriptElem.async = true; + element.appendChild(scriptElem); + }; } - }); + })); asciinemaPlugin.name = "ascii cast"; var yrPlugin = new Plugin( From beaa3426dd929df1c6a08b9cce05dbf4def25b81 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lorenz=20H=C3=BCbschle-Schneider?= Date: Sun, 8 Feb 2015 11:17:06 +0100 Subject: [PATCH 24/31] Rewrite vine plugin with urlPlugin --- js/plugins.js | 27 +++++++++++---------------- 1 file changed, 11 insertions(+), 16 deletions(-) diff --git a/js/plugins.js b/js/plugins.js index 1d971cd..0b3211e 100644 --- a/js/plugins.js +++ b/js/plugins.js @@ -400,22 +400,17 @@ plugins.factory('userPlugins', function() { /* * Vine plugin */ - var vinePlugin = new Plugin(function(message) { - - var regexp = /https?:\/\/(www\.)?vine.co\/v\/([a-zA-Z0-9]+)(\/.*)?/g; - var content = []; - var match; - - // Iterate over all matches - while ((match = regexp.exec(message)) !== null) { - var id = match[2]; - var embedurl = "https://vine.co/v/" + id + "/embed/simple?audio=1"; - content.push(''); - } - - return content; - }); - vinePlugin.name = "Vine"; + var vinePlugin = new Plugin( + urlPlugin(function (url) { + var regexp = /^https?:\/\/(www\.)?vine.co\/v\/([a-zA-Z0-9]+)(\/.*)?/i, + match = url.match(regexp); + if (match) { + var id = match[2], embedurl = "https://vine.co/v/" + id + "/embed/simple?audio=1"; + return ''; + } + }) + ); + vinePlugin.name = 'Vine'; return { plugins: [youtubePlugin, dailymotionPlugin, allocinePlugin, imagePlugin, spotifyPlugin, cloudmusicPlugin, googlemapPlugin, asciinemaPlugin, yrPlugin, gistPlugin, tweetPlugin, vinePlugin] From 4b65847b468badc4d2e43edcd930ffbd0993359c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lorenz=20H=C3=BCbschle-Schneider?= Date: Sun, 8 Feb 2015 11:23:32 +0100 Subject: [PATCH 25/31] Rewrite youtube plugin with urlPlugin Remove URL without protocol from tests --- js/plugins.js | 19 ++++++------------- test/unit/plugins.js | 1 - 2 files changed, 6 insertions(+), 14 deletions(-) diff --git a/js/plugins.js b/js/plugins.js index 0b3211e..5249b63 100644 --- a/js/plugins.js +++ b/js/plugins.js @@ -189,23 +189,16 @@ plugins.factory('userPlugins', function() { * * See: https://developers.google.com/youtube/player_parameters */ - var youtubePlugin = new Plugin(function(message) { + var youtubePlugin = new Plugin(urlPlugin(function(url) { + var regex = /(?:youtube.com|youtu.be)\/(?:v\/|embed\/|watch(?:\?v=|\/))?([a-zA-Z0-9-]+)/i, + match = url.match(regex); - var regExp = /(?:https?:\/\/)?(?:www\.)?(?:youtube.com|youtu.be)\/(?:v\/|embed\/|watch(?:\?v=|\/))?([a-zA-Z0-9-]+)/gm; - var match = regExp.exec(message); - var content = []; - - // iterate over all matches - while (match !== null){ + if (match){ var token = match[1]; var embedurl = "https://www.youtube.com/embed/" + token + "?html5=1&iv_load_policy=3&modestbranding=1&rel=0&showinfo=0"; - content.push(''); - // next match - match = regExp.exec(message); + return ''; } - - return content; - }); + })); youtubePlugin.name = 'YouTube video'; /* diff --git a/test/unit/plugins.js b/test/unit/plugins.js index af3229c..537f86a 100644 --- a/test/unit/plugins.js +++ b/test/unit/plugins.js @@ -47,7 +47,6 @@ describe('filter', function() { 'https://youtu.be/J6vIS8jb6Fs', 'http://www.youtube.com/embed/dQw4w9WgXcQ', 'https://www.youtube.com/embed/dQw4w9WgXcQ', - 'youtu.be/dQw4w9WgXcQ' ], 'YouTube video', plugins); From 07d60f96c37450909f03c6f7f34474b18fd2fd1f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lorenz=20H=C3=BCbschle-Schneider?= Date: Sun, 8 Feb 2015 11:50:49 +0100 Subject: [PATCH 26/31] Fix automatic embedding of asynchronous plugins (jsonp) Closes #540 --- js/plugin-directive.js | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/js/plugin-directive.js b/js/plugin-directive.js index 94d19f5..4510f04 100644 --- a/js/plugin-directive.js +++ b/js/plugin-directive.js @@ -47,8 +47,12 @@ weechat.directive('plugin', ['$rootScope', 'settings', function($rootScope, sett // TODO store the result between channel switches if ($scope.plugin.content instanceof Function){ // Don't rerun if the result is already there - if (embed.innerHTML === "") { - $scope.plugin.content(); + if (!embed || embed.innerHTML === "") { + // if we're autoshowing, the element doesn't exist yet, and we need + // to do this async (wrapped in a setTimeout) + setTimeout(function() { + $scope.plugin.content(); + }); } } else { $scope.displayedContent = $scope.plugin.content; From c57911db6477cb24c55a5599eb61f3056e0759e4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lorenz=20H=C3=BCbschle-Schneider?= Date: Sun, 8 Feb 2015 11:08:12 +0100 Subject: [PATCH 27/31] Restructure plugin constructors to make them more convenient --- js/plugins.js | 313 ++++++++++++++++++++++++-------------------------- 1 file changed, 148 insertions(+), 165 deletions(-) diff --git a/js/plugins.js b/js/plugins.js index 5249b63..296fa0e 100644 --- a/js/plugins.js +++ b/js/plugins.js @@ -10,15 +10,42 @@ var plugins = angular.module('plugins', []); /* * Definition of a user provided plugin with sensible default values * - * User plugins are created by providing a contentForMessage function - * that parses a string and return any additional content. + * User plugins are created by providing a name and a contentForMessage + * function that parses a string and returns any additional content. */ -var Plugin = function(contentForMessage) { - +var Plugin = function(name, contentForMessage) { return { contentForMessage: contentForMessage, exclusive: false, - name: "additional content" + name: name + }; +}; + + +// Regular expression that detects URLs for UrlPlugin +var urlRegexp = /(?:ftp|https?):\/\/\S*[^\s.;,(){}<>]/g; +/* + * Definition of a user provided plugin that consumes URLs + * + * URL plugins are created by providing a name and a function that + * that parses a URL and returns any additional content. + */ +var UrlPlugin = function(name, urlCallback) { + return { + contentForMessage: function(message) { + var urls = message.match(urlRegexp); + var content = []; + + for (var i = 0; urls && i < urls.length; i++) { + var result = urlCallback(urls[i]); + if (result) { + content.push(result); + } + } + return content; + }, + exclusive: false, + name: name }; }; @@ -32,8 +59,6 @@ var Plugin = function(contentForMessage) { */ plugins.service('plugins', ['userPlugins', '$sce', function(userPlugins, $sce) { - var nsfwRegexp = new RegExp('nsfw', 'i'); - /* * Defines the plugin manager object */ @@ -52,6 +77,8 @@ plugins.service('plugins', ['userPlugins', '$sce', function(userPlugins, $sce) { } }; + var nsfwRegexp = new RegExp('nsfw', 'i'); + /* * Iterates through all the registered plugins * and run their contentForMessage function. @@ -146,23 +173,6 @@ plugins.factory('userPlugins', function() { document.body.appendChild(script); }; - var urlRegexp = new RegExp(/(?:ftp|https?):\/\/\S*[^\s.;,(){}<>]/g); - - var urlPlugin = function(callback) { - return function(message) { - var urls = message.match(urlRegexp); - var content = []; - - for (var i = 0; urls && i < urls.length; i++) { - var result = callback(urls[i]); - if (result) { - content.push(result); - } - } - return content; - }; - }; - /* * Spotify Embedded Player * @@ -170,7 +180,7 @@ plugins.factory('userPlugins', function() { * */ - var spotifyPlugin = new Plugin(function(message) { + var spotifyPlugin = new Plugin('Spotify track', function(message) { var content = []; var addMatch = function(match) { for (var i = 0; match && i < match.length; i++) { @@ -182,32 +192,29 @@ plugins.factory('userPlugins', function() { addMatch(message.match(/open.spotify.com\/track\/([a-zA-Z-0-9]{22})/g)); return content; }); - spotifyPlugin.name = 'Spotify track'; /* * YouTube Embedded Player * * See: https://developers.google.com/youtube/player_parameters */ - var youtubePlugin = new Plugin(urlPlugin(function(url) { + var youtubePlugin = new UrlPlugin('YouTube video', function(url) { var regex = /(?:youtube.com|youtu.be)\/(?:v\/|embed\/|watch(?:\?v=|\/))?([a-zA-Z0-9-]+)/i, match = url.match(regex); if (match){ - var token = match[1]; - var embedurl = "https://www.youtube.com/embed/" + token + "?html5=1&iv_load_policy=3&modestbranding=1&rel=0&showinfo=0"; + var token = match[1], + embedurl = "https://www.youtube.com/embed/" + token + "?html5=1&iv_load_policy=3&modestbranding=1&rel=0&showinfo=0"; return ''; } - })); - youtubePlugin.name = 'YouTube video'; + }); /* * Dailymotion Embedded Player * * See: http://www.dailymotion.com/doc/api/player.html */ - var dailymotionPlugin = new Plugin(function(message) { - + var dailymotionPlugin = new Plugin('Dailymotion video', function(message) { var rPath = /dailymotion.com\/.*video\/([^_?# ]+)/; var rAnchor = /dailymotion.com\/.*#video=([^_& ]+)/; var rShorten = /dai.ly\/([^_?# ]+)/; @@ -221,13 +228,11 @@ plugins.factory('userPlugins', function() { return null; }); - dailymotionPlugin.name = 'Dailymotion video'; /* * AlloCine Embedded Player */ - var allocinePlugin = new Plugin(function(message) { - + var allocinePlugin = new Plugin('AlloCine video', function(message) { var rVideokast = /allocine.fr\/videokast\/video-(\d+)/; var rCmedia = /allocine.fr\/.*cmedia=(\d+)/; @@ -240,74 +245,65 @@ plugins.factory('userPlugins', function() { return null; }); - allocinePlugin.name = 'AlloCine video'; /* * Image Preview */ - var imagePlugin = new Plugin( - urlPlugin(function(url) { - var embed = false; - // Check the get parameters as well, they might contain an image to load - var segments = url.split(/[?&]/).forEach(function(param) { - if (param.match(/\.(png|gif|jpg|jpeg)(:(small|medium|large))?$/i)) { - embed = true; - } - }); - if (embed) { - /* A fukung.net URL may end by an image extension but is not a direct link. */ - if (url.indexOf("^https?://fukung.net/v/") != -1) { - url = url.replace(/.*\//, "http://media.fukung.net/imgs/"); - } else if (url.match(/^http:\/\/(i\.)?imgur\.com\//i)) { - // remove protocol specification to load over https if used by g-b - url = url.replace(/http:/, ""); - } else if (url.match(/^https:\/\/www\.dropbox\.com\/s\/[a-z0-9]+\/[^?]+$/i)) { - // Dropbox requires a get parameter, dl=1 - url = url + "?dl=1"; - } - - return ''; + var imagePlugin = new UrlPlugin('image', function(url) { + var embed = false; + // Check the get parameters as well, they might contain an image to load + var segments = url.split(/[?&]/).forEach(function(param) { + if (param.match(/\.(png|gif|jpg|jpeg)(:(small|medium|large))?$/i)) { + embed = true; + } + }); + if (embed) { + /* A fukung.net URL may end by an image extension but is not a direct link. */ + if (url.indexOf("^https?://fukung.net/v/") != -1) { + url = url.replace(/.*\//, "http://media.fukung.net/imgs/"); + } else if (url.match(/^http:\/\/(i\.)?imgur\.com\//i)) { + // remove protocol specification to load over https if used by g-b + url = url.replace(/http:/, ""); + } else if (url.match(/^https:\/\/www\.dropbox\.com\/s\/[a-z0-9]+\/[^?]+$/i)) { + // Dropbox requires a get parameter, dl=1 + // TODO strip an existing dl=0 parameter + url = url + "?dl=1"; } - }) - ); - imagePlugin.name = 'image'; + + return ''; + } + }); /* * Cloud Music Embedded Players */ - var cloudmusicPlugin = new Plugin( - urlPlugin(function(url) { - /* SoundCloud http://help.soundcloud.com/customer/portal/articles/247785-what-widgets-can-i-use-from-soundcloud- */ - if (url.match(/^https?:\/\/soundcloud.com\//)) { - return ''; - } + var cloudmusicPlugin = new UrlPlugin('cloud music', function(url) { + /* SoundCloud http://help.soundcloud.com/customer/portal/articles/247785-what-widgets-can-i-use-from-soundcloud- */ + if (url.match(/^https?:\/\/soundcloud.com\//)) { + return ''; + } - /* MixCloud */ - if (url.match(/^https?:\/\/([a-z]+\.)?mixcloud.com\//)) { - return ''; - } - }) - ); - cloudmusicPlugin.name = 'cloud music'; + /* MixCloud */ + if (url.match(/^https?:\/\/([a-z]+\.)?mixcloud.com\//)) { + return ''; + } + }); /* * Google Maps */ - var googlemapPlugin = new Plugin( - urlPlugin(function(url) { - if (url.match(/^https?:\/\/maps\.google\./i) || url.match(/^https?:\/\/(?:[\w]+\.)?google\.[\w]+\/maps/i)) { - return ''; - } - }) - ); - googlemapPlugin.name = 'Google Map'; + var googlemapPlugin = new UrlPlugin('Google Map', function(url) { + if (url.match(/^https?:\/\/maps\.google\./i) || url.match(/^https?:\/\/(?:[\w]+\.)?google\.[\w]+\/maps/i)) { + return ''; + } + }); /* * Asciinema plugin */ - var asciinemaPlugin = new Plugin(urlPlugin(function(url) { - var regexp = /^https?:\/\/(?:www\.)?asciinema.org\/a\/(\d+)/i; - var match = url.match(regexp); + var asciinemaPlugin = new UrlPlugin('ascii cast', function(url) { + var regexp = /^https?:\/\/(?:www\.)?asciinema.org\/a\/(\d+)/i, + match = url.match(regexp); if (match) { var id = match[1]; return function() { @@ -319,91 +315,78 @@ plugins.factory('userPlugins', function() { element.appendChild(scriptElem); }; } - })); - asciinemaPlugin.name = "ascii cast"; - - var yrPlugin = new Plugin( - urlPlugin(function(url) { - var regexp = /^https?:\/\/(?:www\.)?yr\.no\/(place|stad|sted|sadji|paikka)\/(([^\s.;,(){}<>\/]+\/){3,})/; - var match = url.match(regexp); - if (match) { - var language = match[1]; - var location = match[2]; - var city = match[match.length - 1].slice(0, -1); - url = "http://www.yr.no/" + language + "/" + location + "avansert_meteogram.png"; - return "Meteogram for " + city + ""; - } - }) - ); - yrPlugin.name = "meteogram"; + }); + + var yrPlugin = new UrlPlugin('meteogram', function(url) { + var regexp = /^https?:\/\/(?:www\.)?yr\.no\/(place|stad|sted|sadji|paikka)\/(([^\s.;,(){}<>\/]+\/){3,})/; + var match = url.match(regexp); + if (match) { + var language = match[1]; + var location = match[2]; + var city = match[match.length - 1].slice(0, -1); + url = "http://www.yr.no/" + language + "/" + location + "avansert_meteogram.png"; + return "Meteogram for " + city + ""; + } + }); // Embed GitHub gists - var gistPlugin = new Plugin( - urlPlugin(function(url) { - var regexp = /^https:\/\/gist\.github.com\/[^.?]+/i; - var match = url.match(regexp); - if (match) { - // get the URL from the match to trim away pseudo file endings and request parameters - url = match[0] + '.json'; - // load gist asynchronously -- return a function here - return function() { - var element = this.getElement(); - jsonp(url, function(data) { - // Add the gist stylesheet only once - if (document.querySelectorAll('link[rel=stylesheet][href="' + data.stylesheet + '"]').length < 1) { - var stylesheet = ''; - document.getElementsByTagName('head')[0].innerHTML += stylesheet; - } - element.innerHTML = '
        ' + data.div + '
        '; - }); - }; - } - }) - ); - gistPlugin.name = 'Gist'; - - var tweetPlugin = new Plugin( - urlPlugin(function(url) { - var regexp = /^https?:\/\/twitter\.com\/(?:#!\/)?(\w+)\/status(?:es)?\/(\d+)/i; - var match = url.match(regexp); - if (match) { - url = 'https://api.twitter.com/1/statuses/oembed.json?id=' + match[2]; - return function() { - var element = this.getElement(); - jsonp(url, function(data) { - // sepearate the HTML into content and script tag - var scriptIndex = data.html.indexOf("'; - } - }) - ); - vinePlugin.name = 'Vine'; + var vinePlugin = new UrlPlugin('Vine', function (url) { + var regexp = /^https?:\/\/(www\.)?vine.co\/v\/([a-zA-Z0-9]+)(\/.*)?/i, + match = url.match(regexp); + if (match) { + var id = match[2], embedurl = "https://vine.co/v/" + id + "/embed/simple?audio=1"; + return ''; + } + }); return { plugins: [youtubePlugin, dailymotionPlugin, allocinePlugin, imagePlugin, spotifyPlugin, cloudmusicPlugin, googlemapPlugin, asciinemaPlugin, yrPlugin, gistPlugin, tweetPlugin, vinePlugin] From c4bddc993d5a598a52dc693502bc03a709cf4285 Mon Sep 17 00:00:00 2001 From: Vivien Didelot Date: Sun, 8 Feb 2015 16:09:01 -0500 Subject: [PATCH 28/31] Revert "Check GET parameters in image embedding" There's no need to check for every GET parameters (using the line ending '$' character), we can just add a word boundary (the '\b' character) instead. Thus, revert commit b939bc7ca8d189af19efad90d38ff49479964ab9. --- js/plugins.js | 9 +-------- 1 file changed, 1 insertion(+), 8 deletions(-) diff --git a/js/plugins.js b/js/plugins.js index 296fa0e..b282d13 100644 --- a/js/plugins.js +++ b/js/plugins.js @@ -250,14 +250,7 @@ plugins.factory('userPlugins', function() { * Image Preview */ var imagePlugin = new UrlPlugin('image', function(url) { - var embed = false; - // Check the get parameters as well, they might contain an image to load - var segments = url.split(/[?&]/).forEach(function(param) { - if (param.match(/\.(png|gif|jpg|jpeg)(:(small|medium|large))?$/i)) { - embed = true; - } - }); - if (embed) { + if (url.match(/\.(png|gif|jpg|jpeg)(:(small|medium|large))?\b/i)) { /* A fukung.net URL may end by an image extension but is not a direct link. */ if (url.indexOf("^https?://fukung.net/v/") != -1) { url = url.replace(/.*\//, "http://media.fukung.net/imgs/"); From d8c4ba78c1eec24ec99c49c198eb5f1fe64cbc06 Mon Sep 17 00:00:00 2001 From: David Cormier Date: Fri, 2 Jan 2015 23:59:51 -0500 Subject: [PATCH 29/31] plugins: add html5 video support Shows ogv, mp4 and webm files inside a video tag --- css/glowingbear.css | 4 ++++ js/plugins.js | 11 ++++++++++- 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/css/glowingbear.css b/css/glowingbear.css index d171615..cf54fed 100644 --- a/css/glowingbear.css +++ b/css/glowingbear.css @@ -309,6 +309,10 @@ div.embed img.embed { max-width: 100%; } +video.embed { + max-width: 100%; +} + div.colourbox { display: inline-block; border-radius: 3px; diff --git a/js/plugins.js b/js/plugins.js index b282d13..8a84758 100644 --- a/js/plugins.js +++ b/js/plugins.js @@ -267,6 +267,15 @@ plugins.factory('userPlugins', function() { } }); + /* + * mp4 video Preview + */ + var videoPlugin = new UrlPlugin('video', function(url) { + if (url.match(/\.(mp4|webm|ogv)\b/i)) { + return ''; + } + }); + /* * Cloud Music Embedded Players */ @@ -382,7 +391,7 @@ plugins.factory('userPlugins', function() { }); return { - plugins: [youtubePlugin, dailymotionPlugin, allocinePlugin, imagePlugin, spotifyPlugin, cloudmusicPlugin, googlemapPlugin, asciinemaPlugin, yrPlugin, gistPlugin, tweetPlugin, vinePlugin] + plugins: [youtubePlugin, dailymotionPlugin, allocinePlugin, imagePlugin, videoPlugin, spotifyPlugin, cloudmusicPlugin, googlemapPlugin, asciinemaPlugin, yrPlugin, gistPlugin, tweetPlugin, vinePlugin] }; From 0165e5e3207ce28f4bd4b8bfd16a5590292bbbe0 Mon Sep 17 00:00:00 2001 From: David Cormier Date: Sat, 3 Jan 2015 00:01:46 -0500 Subject: [PATCH 30/31] plugins: add tests for video plugin Test ogv, webm and mp4 urls --- test/unit/plugins.js | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/test/unit/plugins.js b/test/unit/plugins.js index 537f86a..e179c0a 100644 --- a/test/unit/plugins.js +++ b/test/unit/plugins.js @@ -71,6 +71,16 @@ describe('filter', function() { plugins); })); + it('should recognize html5 videos', inject(function(plugins) { + expectTheseMessagesToContain([ + 'http://www.quirksmode.org/html5/videos/big_buck_bunny.mp4', + 'http://www.quirksmode.org/html5/videos/big_buck_bunny.webm', + 'http://www.quirksmode.org/html5/videos/big_buck_bunny.ogv', + ], + 'video', + plugins); + })); + it('should recognize images', inject(function(plugins) { expectTheseMessagesToContain([ 'http://i.imgur.com/BTNIDBR.gif', From b56309be6ec397c68283b91c06ebea9c18d113f7 Mon Sep 17 00:00:00 2001 From: David Cormier Date: Wed, 18 Feb 2015 12:49:15 -0500 Subject: [PATCH 31/31] Bump version to 0.4.5 --- manifest.json | 2 +- manifest.webapp | 2 +- package.json | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/manifest.json b/manifest.json index 9c8eb96..583dd44 100644 --- a/manifest.json +++ b/manifest.json @@ -1,7 +1,7 @@ { "name": "Glowing Bear", "description": "WeeChat Web frontend", - "version": "0.4.4", + "version": "0.4.5", "manifest_version": 2, "icons": { "32": "assets/img/favicon.png", diff --git a/manifest.webapp b/manifest.webapp index d17bbfa..2b4a2ba 100644 --- a/manifest.webapp +++ b/manifest.webapp @@ -25,5 +25,5 @@ "desktop-notification":{} }, "default_locale": "en", - "version": "0.4.4" + "version": "0.4.5" } diff --git a/package.json b/package.json index fdf3709..33f59f2 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "glowing-bear", "private": true, - "version": "0.4.4", + "version": "0.4.5", "description": "A web client for Weechat", "repository": "https://github.com/glowing-bear/glowing-bear", "license": "GPLv3",