From 4ac8f02fcb862033e6cfc49fe3851e6d7067c247 Mon Sep 17 00:00:00 2001 From: David Cormier Date: Fri, 29 Aug 2014 11:48:53 -0400 Subject: [PATCH 01/57] Bump vesion of manifest.webapp --- manifest.webapp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/manifest.webapp b/manifest.webapp index 7ceda1d..2521b92 100644 --- a/manifest.webapp +++ b/manifest.webapp @@ -16,5 +16,5 @@ "url": "https://github.com/glowing-bear" }, "default_locale": "en", - "version": "0.4.0" + "version": "0.4.1" } From 0c2e7635ed4f325cbae4618b0ddb38752f033f19 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lorenz=20H=C3=BCbschle-Schneider?= Date: Sun, 31 Aug 2014 14:57:16 +0100 Subject: [PATCH 02/57] Fix Alt+[0-9] buffer access order WeeChat sends them in no particular order, we need to sort the buffers by their WeeChat number. To avoid copying the potentially very large buffer objects around needlessly, extract the relevant keys and sort, then access. This is based on https://github.com/ailin-nemui/glowing-bear/commit/ad50220bfdfcf029fc47093ce5cfea04bf90fe79 --- js/glowingbear.js | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/js/glowingbear.js b/js/glowingbear.js index 39e9f14..6ed213c 100644 --- a/js/glowingbear.js +++ b/js/glowingbear.js @@ -1529,9 +1529,19 @@ weechat.directive('inputBar', function() { } var bufferNumber = code - 48 - 1 ; - var activeBufferId = Object.keys(models.getBuffers())[bufferNumber]; + // Map the buffers to only their numbers and IDs so we don't have to + // copy the entire (possibly very large) buffer object, and then sort + // the buffers according to their WeeChat number + var sortedBuffers = _.map(models.getBuffers(), function(buffer) { + return [buffer.number, buffer.id]; + }).sort(function(left, right) { + // By default, Array.prototype.sort() sorts alphabetically. + // Pass an ordering function to sort by first element. + return left[0] - right[0]; + }); + var activeBufferId = sortedBuffers[bufferNumber]; if (activeBufferId) { - models.setActiveBuffer(activeBufferId); + models.setActiveBuffer(activeBufferId[1]); $event.preventDefault(); } } From 21f8f47efeb51b74a26209cd11f71a6274077448 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lorenz=20H=C3=BCbschle-Schneider?= Date: Thu, 28 Aug 2014 17:25:40 +0100 Subject: [PATCH 03/57] Split things up into logical blocks --- index.html | 7 + js/connection.js | 280 ++++++++++ js/filters.js | 53 ++ js/glowingbear.js | 1113 ++-------------------------------------- js/handlers.js | 207 ++++++++ js/inputbar.js | 307 +++++++++++ js/notifications.js | 141 +++++ js/plugin-directive.js | 56 ++ js/utils.js | 29 ++ package.json | 2 +- 10 files changed, 1112 insertions(+), 1083 deletions(-) create mode 100644 js/connection.js create mode 100644 js/filters.js create mode 100644 js/handlers.js create mode 100644 js/inputbar.js create mode 100644 js/notifications.js create mode 100644 js/plugin-directive.js create mode 100644 js/utils.js diff --git a/index.html b/index.html index d4df8b6..af653d8 100644 --- a/index.html +++ b/index.html @@ -24,6 +24,13 @@ + + + + + + + diff --git a/js/connection.js b/js/connection.js new file mode 100644 index 0000000..2945d4a --- /dev/null +++ b/js/connection.js @@ -0,0 +1,280 @@ +var weechat = angular.module('weechat'); + +weechat.factory('connection', + ['$rootScope', '$log', 'handlers', 'models', 'ngWebsockets', function($rootScope, + $log, + handlers, + models, + ngWebsockets) { + + protocol = new weeChat.Protocol(); + + // Takes care of the connection and websocket hooks + + var connect = function (host, port, passwd, ssl, noCompression) { + var proto = ssl ? 'wss' : 'ws'; + // If host is an IPv6 literal wrap it in brackets + if (host.indexOf(":") !== -1) { + host = "[" + host + "]"; + } + var url = proto + "://" + host + ":" + port + "/weechat"; + $log.debug('Connecting to URL: ', url); + + var onopen = function () { + + + // Helper methods for initialization commands + var _initializeConnection = function(passwd) { + // This is not the proper way to do this. + // WeeChat does not send a confirmation for the init. + // Until it does, We need to "assume" that formatInit + // will be received before formatInfo + ngWebsockets.send( + weeChat.Protocol.formatInit({ + password: passwd, + compression: noCompression ? 'off' : 'zlib' + }) + ); + + return ngWebsockets.send( + weeChat.Protocol.formatInfo({ + name: 'version' + }) + ); + }; + + var _requestHotlist = function() { + return ngWebsockets.send( + weeChat.Protocol.formatHdata({ + path: "hotlist:gui_hotlist(*)", + keys: [] + }) + ); + }; + + var _requestBufferInfos = function() { + return ngWebsockets.send( + weeChat.Protocol.formatHdata({ + path: 'buffer:gui_buffers(*)', + keys: ['local_variables,notify,number,full_name,short_name,title'] + }) + ); + }; + + var _requestSync = function() { + return ngWebsockets.send( + weeChat.Protocol.formatSync({}) + ); + }; + + + // First command asks for the password and issues + // a version command. If it fails, it means the we + // did not provide the proper password. + _initializeConnection(passwd).then( + function() { + // Connection is successful + // Send all the other commands required for initialization + _requestBufferInfos().then(function(bufinfo) { + //XXX move to handlers? + var bufferInfos = bufinfo.objects[0].content; + // buffers objects + for (var i = 0; i < bufferInfos.length ; i++) { + var buffer = new models.Buffer(bufferInfos[i]); + models.addBuffer(buffer); + // Switch to first buffer on startup + if (i === 0) { + models.setActiveBuffer(buffer.id); + } + } + }); + + _requestHotlist().then(function(hotlist) { + handlers.handleHotlistInfo(hotlist); + }); + + _requestSync(); + $log.info("Connected to relay"); + $rootScope.connected = true; + }, + function() { + // Connection got closed, lets check if we ever was connected successfully + if (!$rootScope.waseverconnected) { + $rootScope.passwordError = true; + } + } + ); + + }; + + var onmessage = function() { + // If we recieve a message from WeeChat it means that + // password was OK. Store that result and check for it + // in the failure handler. + $rootScope.waseverconnected = true; + }; + + + var onclose = function (evt) { + /* + * Handles websocket disconnection + */ + $log.info("Disconnected from relay"); + failCallbacks('disconnection'); + $rootScope.connected = false; + $rootScope.$emit('relayDisconnect'); + if (ssl && evt.code === 1006) { + // A password error doesn't trigger onerror, but certificate issues do. Check time of last error. + if (typeof $rootScope.lastError !== "undefined" && (Date.now() - $rootScope.lastError) < 1000) { + // abnormal disconnect by client, most likely ssl error + $rootScope.sslError = true; + } + } + $rootScope.$apply(); + }; + + var onerror = function (evt) { + /* + * Handles cases when connection issues come from + * the relay. + */ + $log.error("Relay error", evt); + $rootScope.lastError = Date.now(); + + if (evt.type === "error" && this.readyState !== 1) { + failCallbacks('error'); + $rootScope.errorMessage = true; + } + }; + + protocol.setId = function(id, message) { + return '(' + id + ') ' + message; + }; + + + try { + ngWebsockets.connect(url, + protocol, + { + 'binaryType': "arraybuffer", + 'onopen': onopen, + 'onclose': onclose, + 'onmessage': onmessage, + 'onerror': onerror + }); + } catch(e) { + $log.debug("Websocket caught DOMException:", e); + $rootScope.lastError = Date.now(); + $rootScope.errorMessage = true; + $rootScope.securityError = true; + $rootScope.$emit('relayDisconnect'); + } + + }; + + var disconnect = function() { + ngWebsockets.send(weeChat.Protocol.formatQuit()); + }; + + /* + * Format and send a weechat message + * + * @returns the angular promise + */ + var sendMessage = function(message) { + ngWebsockets.send(weeChat.Protocol.formatInput({ + buffer: models.getActiveBuffer().fullName, + data: message + })); + }; + + var sendCoreCommand = function(command) { + ngWebsockets.send(weeChat.Protocol.formatInput({ + buffer: 'core.weechat', + data: command + })); + }; + + + var requestNicklist = function(bufferId, callback) { + bufferId = bufferId || null; + ngWebsockets.send( + weeChat.Protocol.formatNicklist({ + buffer: bufferId + }) + ).then(function(nicklist) { + handlers.handleNicklist(nicklist); + if (callback !== undefined) { + callback(); + } + }); + }; + + + var fetchMoreLines = function(numLines) { + $log.debug('Fetching ', numLines, ' lines'); + var buffer = models.getActiveBuffer(); + if (numLines === undefined) { + // Math.max(undefined, *) = NaN -> need a number here + numLines = 0; + } + // Calculate number of lines to fetch, at least as many as the parameter + numLines = Math.max(numLines, buffer.requestedLines * 2); + + // Indicator that we are loading lines, hides "load more lines" link + $rootScope.loadingLines = true; + // Send hdata request to fetch lines for this particular buffer + return ngWebsockets.send( + weeChat.Protocol.formatHdata({ + // "0x" is important, otherwise it won't work + path: "buffer:0x" + buffer.id + "/own_lines/last_line(-" + numLines + ")/data", + keys: [] + }) + ).then(function(lineinfo) { +//XXX move to handlers? + // delete old lines and add new ones + var oldLength = buffer.lines.length; + // Whether to set the readmarker to the middle position + // Don't do that if we didn't get any more lines than we already had + var setReadmarker = (buffer.lastSeen >= 0) && (oldLength !== buffer.lines.length); + buffer.lines.length = 0; + // We need to set the number of requested lines to 0 here, because parsing a line + // increments it. This is needed to also count newly arriving lines while we're + // already connected. + buffer.requestedLines = 0; + // Count number of lines recieved + var linesReceivedCount = lineinfo.objects[0].content.length; + + // Parse the lines + handlers.handleLineInfo(lineinfo, true); + + if (setReadmarker) { + // Read marker was somewhere in the old lines - we don't need it any more, + // set it to the boundary between old and new. This way, we stay at the exact + // same position in the text through the scrollWithBuffer below + buffer.lastSeen = buffer.lines.length - oldLength - 1; + } else { + // We are currently fetching at least some unread lines, so we need to keep + // the read marker position correct + buffer.lastSeen -= oldLength; + } + // We requested more lines than we got, no more lines. + if (linesReceivedCount < numLines) { + buffer.allLinesFetched = true; + } + $rootScope.loadingLines = false; + // Scroll read marker to the center of the screen + $rootScope.scrollWithBuffer(true); + }); + }; + + + return { + connect: connect, + disconnect: disconnect, + sendMessage: sendMessage, + sendCoreCommand: sendCoreCommand, + fetchMoreLines: fetchMoreLines, + requestNicklist: requestNicklist + }; +}]); diff --git a/js/filters.js b/js/filters.js new file mode 100644 index 0000000..418b91c --- /dev/null +++ b/js/filters.js @@ -0,0 +1,53 @@ +var weechat = angular.module('weechat'); + +weechat.filter('toArray', function () { + 'use strict'; + + return function (obj) { + if (!(obj instanceof Object)) { + return obj; + } + + return Object.keys(obj).map(function (key) { + return Object.defineProperty(obj[key], '$key', { value: key }); + }); + }; +}); + +weechat.filter('irclinky', ['$filter', function($filter) { + 'use strict'; + return function(text, target) { + if (!text) { + return text; + } + + var linkiedText = $filter('linky')(text, target); + + // This regex in no way matches all IRC channel names (they could begin with a +, an &, 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. "#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,.:;?!"'()+@-])(#+[a-z0-9-_]*[a-z][a-z0-9-_]*)/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. + linkiedText = linkiedText.replace(channelRegex, '$1$2'); + return linkiedText; + }; +}]); + +weechat.filter('inlinecolour', ['$sce', function($sce) { + 'use strict'; + + return function(text) { + if (!text) { + return text; + } + + // 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 $sce.trustAsHtml(text.replace(hexColourRegex, substitute)); + }; +}]); diff --git a/js/glowingbear.js b/js/glowingbear.js index 6ed213c..ec8fbb1 100644 --- a/js/glowingbear.js +++ b/js/glowingbear.js @@ -1,559 +1,6 @@ var weechat = angular.module('weechat', ['ngRoute', 'localStorage', 'weechatModels', 'plugins', 'ngSanitize', 'ngWebsockets', 'ngTouch']); -weechat.filter('toArray', function () { - 'use strict'; - - return function (obj) { - if (!(obj instanceof Object)) { - return obj; - } - - return Object.keys(obj).map(function (key) { - return Object.defineProperty(obj[key], '$key', { value: key }); - }); - }; -}); - -// Helper to change style of a class -var changeClassStyle = function(classSelector, attr, value) { - _.each(document.getElementsByClassName(classSelector), function(e) { - e.style[attr] = value; - }); -}; -// Helper to get style from a class -var getClassStyle = function(classSelector, attr) { - _.each(document.getElementsByClassName(classSelector), function(e) { - return e.style[attr]; - }); -}; - -weechat.filter('irclinky', ['$filter', function($filter) { - 'use strict'; - return function(text, target) { - if (!text) { - return text; - } - - var linkiedText = $filter('linky')(text, target); - - // This regex in no way matches all IRC channel names (they could begin with a +, an &, 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. "#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,.:;?!"'()+@-])(#+[a-z0-9-_]*[a-z][a-z0-9-_]*)/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. - linkiedText = linkiedText.replace(channelRegex, '$1$2'); - return linkiedText; - }; -}]); - -weechat.filter('inlinecolour', ['$sce', function($sce) { - 'use strict'; - - return function(text) { - if (!text) { - return text; - } - - // 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 $sce.trustAsHtml(text.replace(hexColourRegex, substitute)); - }; -}]); - -weechat.factory('handlers', ['$rootScope', '$log', 'models', 'plugins', function($rootScope, $log, models, plugins) { - - var handleBufferClosing = function(message) { - var bufferMessage = message.objects[0].content[0]; - var bufferId = bufferMessage.pointers[0]; - models.closeBuffer(bufferId); - }; - - var handleLine = function(line, manually) { - var message = new models.BufferLine(line); - var buffer = models.getBuffer(message.buffer); - buffer.requestedLines++; - // Only react to line if its displayed - if (message.displayed) { - message = plugins.PluginManager.contentForMessage(message); - buffer.addLine(message); - - if (manually) { - buffer.lastSeen++; - } - - if (buffer.active && !manually) { - $rootScope.scrollWithBuffer(); - } - - if (!manually && (!buffer.active || !$rootScope.isWindowFocused())) { - if (buffer.notify > 1 && _.contains(message.tags, 'notify_message') && !_.contains(message.tags, 'notify_none')) { - buffer.unread++; - $rootScope.$emit('notificationChanged'); - } - - if ((buffer.notify !== 0 && message.highlight) || _.contains(message.tags, 'notify_private')) { - buffer.notification++; - $rootScope.createHighlight(buffer, message); - $rootScope.$emit('notificationChanged'); - } - } - } - }; - - var handleBufferLineAdded = function(message) { - message.objects[0].content.forEach(function(l) { - handleLine(l, false); - }); - }; - - var handleBufferOpened = function(message) { - var bufferMessage = message.objects[0].content[0]; - var buffer = new models.Buffer(bufferMessage); - models.addBuffer(buffer); - /* Until we can decide if user asked for this buffer to be opened - * or not we will let user click opened buffers. - models.setActiveBuffer(buffer.id); - */ - }; - - var handleBufferTitleChanged = function(message) { - var obj = message.objects[0].content[0]; - var buffer = obj.pointers[0]; - var old = models.getBuffer(buffer); - old.fullName = obj.full_name; - old.title = obj.title; - old.number = obj.number; - }; - - var handleBufferRenamed = function(message) { - var obj = message.objects[0].content[0]; - var buffer = obj.pointers[0]; - var old = models.getBuffer(buffer); - old.fullName = obj.full_name; - old.shortName = obj.short_name; - }; - - var handleBufferLocalvarChanged = function(message) { - var obj = message.objects[0].content[0]; - var buffer = obj.pointers[0]; - var old = models.getBuffer(buffer); - - var localvars = obj.local_variables; - if (old !== undefined && localvars !== undefined) { - // Update indendation status - old.indent = (['channel', 'private'].indexOf(localvars.type) >= 0); - } - }; - - /* - * Handle answers to (lineinfo) messages - * - * (lineinfo) messages are specified by this client. It is request after bufinfo completes - */ - var handleLineInfo = function(message, manually) { - var lines = message.objects[0].content.reverse(); - if (manually === undefined) { - manually = true; - } - lines.forEach(function(l) { - handleLine(l, manually); - }); - }; - - /* - * Handle answers to hotlist request - */ - var handleHotlistInfo = function(message) { - if (message.objects.length === 0) { - return; - } - var hotlist = message.objects[0].content; - hotlist.forEach(function(l) { - var buffer = models.getBuffer(l.buffer); - // 1 is message - buffer.unread += l.count[1]; - // 2 is private - buffer.notification += l.count[2]; - // 3 is highlight - buffer.notification += l.count[3]; - /* Since there is unread messages, we can guess - * what the last read line is and update it accordingly - */ - var unreadSum = _.reduce(l.count, function(memo, num) { return memo + num; }, 0); - buffer.lastSeen = buffer.lines.length - 1 - unreadSum; - }); - }; - - /* - * Handle nicklist event - */ - var handleNicklist = function(message) { - var nicklist = message.objects[0].content; - var group = 'root'; - nicklist.forEach(function(n) { - var buffer = models.getBuffer(n.pointers[0]); - if (n.group === 1) { - var g = new models.NickGroup(n); - group = g.name; - buffer.nicklist[group] = g; - } else { - var nick = new models.Nick(n); - buffer.addNick(group, nick); - } - }); - }; - /* - * Handle nicklist diff event - */ - var handleNicklistDiff = function(message) { - var nicklist = message.objects[0].content; - var group; - nicklist.forEach(function(n) { - var buffer = models.getBuffer(n.pointers[0]); - var d = n._diff; - if (n.group === 1) { - group = n.name; - if (group === undefined) { - var g = new models.NickGroup(n); - buffer.nicklist[group] = g; - group = g.name; - } - } else { - var nick = new models.Nick(n); - if (d === 43) { // + - buffer.addNick(group, nick); - } else if (d === 45) { // - - buffer.delNick(group, nick); - } else if (d === 42) { // * - buffer.updateNick(group, nick); - } - } - }); - }; - - var eventHandlers = { - _buffer_closing: handleBufferClosing, - _buffer_line_added: handleBufferLineAdded, - _buffer_localvar_added: handleBufferLocalvarChanged, - _buffer_localvar_removed: handleBufferLocalvarChanged, - _buffer_opened: handleBufferOpened, - _buffer_title_changed: handleBufferTitleChanged, - _buffer_renamed: handleBufferRenamed, - _nicklist: handleNicklist, - _nicklist_diff: handleNicklistDiff - }; - - $rootScope.$on('onMessage', function(event, message) { - if (_.has(eventHandlers, message.id)) { - eventHandlers[message.id](message); - } else { - $log.debug('Unhandled event received: ' + message.id); - } - }); - - var handleEvent = function(event) { - if (_.has(eventHandlers, event.id)) { - eventHandlers[event.id](event); - } - }; - - return { - handleEvent: handleEvent, - handleLineInfo: handleLineInfo, - handleHotlistInfo: handleHotlistInfo, - handleNicklist: handleNicklist - }; - -}]); - -weechat.factory('connection', - ['$rootScope', - '$log', - 'handlers', - 'models', - 'ngWebsockets', -function($rootScope, - $log, - handlers, - models, - ngWebsockets) { - - protocol = new weeChat.Protocol(); - - // Takes care of the connection and websocket hooks - - var connect = function (host, port, passwd, ssl, noCompression) { - var proto = ssl ? 'wss' : 'ws'; - // If host is an IPv6 literal wrap it in brackets - if (host.indexOf(":") !== -1) { - host = "[" + host + "]"; - } - var url = proto + "://" + host + ":" + port + "/weechat"; - $log.debug('Connecting to URL: ', url); - - var onopen = function () { - - - // Helper methods for initialization commands - var _initializeConnection = function(passwd) { - // This is not the proper way to do this. - // WeeChat does not send a confirmation for the init. - // Until it does, We need to "assume" that formatInit - // will be received before formatInfo - ngWebsockets.send( - weeChat.Protocol.formatInit({ - password: passwd, - compression: noCompression ? 'off' : 'zlib' - }) - ); - - return ngWebsockets.send( - weeChat.Protocol.formatInfo({ - name: 'version' - }) - ); - }; - - var _requestHotlist = function() { - return ngWebsockets.send( - weeChat.Protocol.formatHdata({ - path: "hotlist:gui_hotlist(*)", - keys: [] - }) - ); - }; - - var _requestBufferInfos = function() { - return ngWebsockets.send( - weeChat.Protocol.formatHdata({ - path: 'buffer:gui_buffers(*)', - keys: ['local_variables,notify,number,full_name,short_name,title'] - }) - ); - }; - - var _requestSync = function() { - return ngWebsockets.send( - weeChat.Protocol.formatSync({}) - ); - }; - - - // First command asks for the password and issues - // a version command. If it fails, it means the we - // did not provide the proper password. - _initializeConnection(passwd).then( - function() { - // Connection is successful - // Send all the other commands required for initialization - _requestBufferInfos().then(function(bufinfo) { - var bufferInfos = bufinfo.objects[0].content; - // buffers objects - for (var i = 0; i < bufferInfos.length ; i++) { - var buffer = new models.Buffer(bufferInfos[i]); - models.addBuffer(buffer); - // Switch to first buffer on startup - if (i === 0) { - models.setActiveBuffer(buffer.id); - } - } - }); - - _requestHotlist().then(function(hotlist) { - handlers.handleHotlistInfo(hotlist); - }); - - _requestSync(); - $log.info("Connected to relay"); - $rootScope.connected = true; - }, - function() { - // Connection got closed, lets check if we ever was connected successfully - if (!$rootScope.waseverconnected) { - $rootScope.passwordError = true; - } - } - ); - - }; - - var onmessage = function() { - // If we recieve a message from WeeChat it means that - // password was OK. Store that result and check for it - // in the failure handler. - $rootScope.waseverconnected = true; - }; - - - var onclose = function (evt) { - /* - * Handles websocket disconnection - */ - $log.info("Disconnected from relay"); - failCallbacks('disconnection'); - $rootScope.connected = false; - $rootScope.$emit('relayDisconnect'); - if (ssl && evt.code === 1006) { - // A password error doesn't trigger onerror, but certificate issues do. Check time of last error. - if (typeof $rootScope.lastError !== "undefined" && (Date.now() - $rootScope.lastError) < 1000) { - // abnormal disconnect by client, most likely ssl error - $rootScope.sslError = true; - } - } - $rootScope.$apply(); - }; - - var onerror = function (evt) { - /* - * Handles cases when connection issues come from - * the relay. - */ - $log.error("Relay error", evt); - $rootScope.lastError = Date.now(); - - if (evt.type === "error" && this.readyState !== 1) { - failCallbacks('error'); - $rootScope.errorMessage = true; - } - }; - - protocol.setId = function(id, message) { - return '(' + id + ') ' + message; - }; - - - try { - ngWebsockets.connect(url, - protocol, - { - 'binaryType': "arraybuffer", - 'onopen': onopen, - 'onclose': onclose, - 'onmessage': onmessage, - 'onerror': onerror - }); - } catch(e) { - $log.debug("Websocket caught DOMException:", e); - $rootScope.lastError = Date.now(); - $rootScope.errorMessage = true; - $rootScope.securityError = true; - $rootScope.$emit('relayDisconnect'); - } - - }; - - var disconnect = function() { - ngWebsockets.send(weeChat.Protocol.formatQuit()); - }; - - /* - * Format and send a weechat message - * - * @returns the angular promise - */ - var sendMessage = function(message) { - ngWebsockets.send(weeChat.Protocol.formatInput({ - buffer: models.getActiveBuffer().fullName, - data: message - })); - }; - - var sendCoreCommand = function(command) { - ngWebsockets.send(weeChat.Protocol.formatInput({ - buffer: 'core.weechat', - data: command - })); - }; - - - var requestNicklist = function(bufferId, callback) { - bufferId = bufferId || null; - ngWebsockets.send( - weeChat.Protocol.formatNicklist({ - buffer: bufferId - }) - ).then(function(nicklist) { - handlers.handleNicklist(nicklist); - if (callback !== undefined) { - callback(); - } - }); - }; - - - var fetchMoreLines = function(numLines) { - $log.debug('Fetching ', numLines, ' lines'); - var buffer = models.getActiveBuffer(); - if (numLines === undefined) { - // Math.max(undefined, *) = NaN -> need a number here - numLines = 0; - } - // Calculate number of lines to fetch, at least as many as the parameter - numLines = Math.max(numLines, buffer.requestedLines * 2); - - // Indicator that we are loading lines, hides "load more lines" link - $rootScope.loadingLines = true; - // Send hdata request to fetch lines for this particular buffer - return ngWebsockets.send( - weeChat.Protocol.formatHdata({ - // "0x" is important, otherwise it won't work - path: "buffer:0x" + buffer.id + "/own_lines/last_line(-" + numLines + ")/data", - keys: [] - }) - ).then(function(lineinfo) { - // delete old lines and add new ones - var oldLength = buffer.lines.length; - // Whether to set the readmarker to the middle position - // Don't do that if we didn't get any more lines than we already had - var setReadmarker = (buffer.lastSeen >= 0) && (oldLength !== buffer.lines.length); - buffer.lines.length = 0; - // We need to set the number of requested lines to 0 here, because parsing a line - // increments it. This is needed to also count newly arriving lines while we're - // already connected. - buffer.requestedLines = 0; - // Count number of lines recieved - var linesReceivedCount = lineinfo.objects[0].content.length; - - // Parse the lines - handlers.handleLineInfo(lineinfo, true); - - if (setReadmarker) { - // Read marker was somewhere in the old lines - we don't need it any more, - // set it to the boundary between old and new. This way, we stay at the exact - // same position in the text through the scrollWithBuffer below - buffer.lastSeen = buffer.lines.length - oldLength - 1; - } else { - // We are currently fetching at least some unread lines, so we need to keep - // the read marker position correct - buffer.lastSeen -= oldLength; - } - // We requested more lines than we got, no more lines. - if (linesReceivedCount < numLines) { - buffer.allLinesFetched = true; - } - $rootScope.loadingLines = false; - // Scroll read marker to the center of the screen - $rootScope.scrollWithBuffer(true); - }); - }; - - - return { - connect: connect, - disconnect: disconnect, - sendMessage: sendMessage, - sendCoreCommand: sendCoreCommand, - fetchMoreLines: fetchMoreLines, - requestNicklist: requestNicklist - }; -}]); - -weechat.controller('WeechatCtrl', ['$rootScope', '$scope', '$store', '$timeout', '$log', 'models', 'connection', function ($rootScope, $scope, $store, $timeout, $log, models, connection) { +weechat.controller('WeechatCtrl', ['$rootScope', '$scope', '$store', '$timeout', '$log', 'models', 'connection', 'notifications', 'utils', function ($rootScope, $scope, $store, $timeout, $log, models, connection, notifications, utils) { // From: http://stackoverflow.com/a/18539624 by StackOverflow user "plantian" $rootScope.countWatchers = function () { @@ -573,38 +20,6 @@ weechat.controller('WeechatCtrl', ['$rootScope', '$scope', '$store', '$timeout', $log.debug(watchers); }; - - $rootScope.isMobileUi = function() { - // TODO don't base detection solely on screen width - // You are right. In the meantime I am renaming isMobileDevice to isMobileUi - var mobile_cutoff = 968; - return (document.body.clientWidth < mobile_cutoff); - }; - - - // Ask for permission to display desktop notifications - $scope.requestNotificationPermission = function() { - // Firefox - if (window.Notification) { - Notification.requestPermission(function(status) { - $log.info('Notification permission status: ', status); - if (Notification.permission !== status) { - Notification.permission = status; - } - }); - } - - // Webkit - if (window.webkitNotifications !== undefined) { - var havePermission = window.webkitNotifications.checkPermission(); - if (havePermission !== 0) { // 0 is PERMISSION_ALLOWED - $log.info('Notification permission status: ', havePermission === 0); - window.webkitNotifications.requestPermission(); - } - } - }; - - $scope.isinstalled = (function() { // Check for firefox & app installed if (navigator.mozApps !== undefined) { @@ -684,58 +99,6 @@ weechat.controller('WeechatCtrl', ['$rootScope', '$scope', '$store', '$timeout', } - // Reduce buffers with "+" operation over a key. Mostly useful for unread/notification counts. - $rootScope.unreadCount = function(type) { - if (!type) { - type = "unread"; - } - - // Do this the old-fashioned way with iterating over the keys, as underscore proved to be error-prone - var keys = Object.keys(models.model.buffers); - var count = 0; - for (var key in keys) { - count += models.model.buffers[keys[key]][type]; - } - - return count; - }; - - $rootScope.updateTitle = function() { - var notifications = $rootScope.unreadCount('notification'); - if (notifications > 0) { - // New notifications deserve an exclamation mark - $rootScope.notificationStatus = '(' + notifications + ') '; - } else { - $rootScope.notificationStatus = ''; - } - - var activeBuffer = models.getActiveBuffer(); - if (activeBuffer) { - $rootScope.pageTitle = activeBuffer.shortName + ' | ' + activeBuffer.title; - } - }; - - $scope.updateFavico = function() { - var notifications = $rootScope.unreadCount('notification'); - if (notifications > 0) { - $scope.favico.badge(notifications, { - bgColor: '#d00', - textColor: '#fff' - }); - } else { - var unread = $rootScope.unreadCount('unread'); - if (unread === 0) { - $scope.favico.reset(); - } else { - $scope.favico.badge(unread, { - bgColor: '#5CB85C', - textColor: '#ff0' - }); - } - } - }; - - $rootScope.$on('activeBufferChanged', function(event, unreadSum) { var ab = models.getActiveBuffer(); @@ -782,7 +145,7 @@ weechat.controller('WeechatCtrl', ['$rootScope', '$scope', '$store', '$timeout', } ); } - $rootScope.updateTitle(ab); + notifications.updateTitle(ab); $rootScope.scrollWithBuffer(true); @@ -797,7 +160,7 @@ weechat.controller('WeechatCtrl', ['$rootScope', '$scope', '$store', '$timeout', // Clear search term on buffer change $scope.search = ''; - if (!$rootScope.isMobileUi()) { + if (!utils.isMobileUi()) { // This needs to happen asynchronously to prevent the enter key handler // of the input bar to be triggered on buffer switch via the search. // Otherwise its current contents would be sent to the new buffer @@ -807,13 +170,13 @@ weechat.controller('WeechatCtrl', ['$rootScope', '$scope', '$store', '$timeout', } }); - $scope.favico = new Favico({animation: 'none'}); + $rootScope.favico = new Favico({animation: 'none'}); $rootScope.$on('notificationChanged', function() { - $rootScope.updateTitle(); + notifications.updateTitle(); - if ($scope.useFavico && $scope.favico) { - $scope.updateFavico(); + if ($scope.useFavico && $rootScope.favico) { + notifications.updateFavico(); } }); @@ -856,7 +219,7 @@ weechat.controller('WeechatCtrl', ['$rootScope', '$scope', '$store', '$timeout', $rootScope.wasMobileUi = false; - if ($rootScope.isMobileUi()) { + if (utils.isMobileUi()) { nonicklist = true; noembed = true; $rootScope.wasMobileUi = true; @@ -883,14 +246,14 @@ weechat.controller('WeechatCtrl', ['$rootScope', '$scope', '$store', '$timeout', // Save setting for playing sound on notification $store.bind($scope, "soundnotification", false); // Save setting for font family - $store.bind($scope, "fontfamily", getClassStyle('favorite-font', 'fontFamily')); + $store.bind($scope, "fontfamily", utils.getClassStyle('favorite-font', 'fontFamily')); // Save setting for font size - $store.bind($scope, "fontsize", getClassStyle('favorite-font', 'fontSize')); + $store.bind($scope, "fontsize", utils.getClassStyle('favorite-font', 'fontSize')); // Save setting for readline keybindings $store.bind($scope, "readlineBindings", false); if (!$scope.fontfamily) { - if ($rootScope.isMobileUi()) { + if (utils.isMobileUi()) { $scope.fontfamily = 'sans-serif'; } else { $scope.fontfamily = "Inconsolata, Consolas, Monaco, Ubuntu Mono, monospace"; @@ -907,7 +270,7 @@ weechat.controller('WeechatCtrl', ['$rootScope', '$scope', '$store', '$timeout', $scope.showSidebar = function() { document.getElementById('sidebar').setAttribute('data-state', 'visible'); document.getElementById('content').setAttribute('sidebar-state', 'visible'); - if ($rootScope.isMobileUi()) { + if (utils.isMobileUi()) { // de-focus the input bar when opening the sidebar on mobile, so that the keyboard goes down _.each(document.getElementsByTagName('textarea'), function(elem) { elem.blur(); @@ -916,7 +279,7 @@ weechat.controller('WeechatCtrl', ['$rootScope', '$scope', '$store', '$timeout', }; $rootScope.hideSidebar = function() { - if ($rootScope.isMobileUi()) { + if (utils.isMobileUi()) { document.getElementById('sidebar').setAttribute('data-state', 'hidden'); document.getElementById('content').setAttribute('sidebar-state', 'hidden'); } @@ -930,7 +293,7 @@ weechat.controller('WeechatCtrl', ['$rootScope', '$scope', '$store', '$timeout', // toggle sidebar (if on mobile) $scope.toggleSidebar = function() { - if ($rootScope.isMobileUi()) { + if (utils.isMobileUi()) { if ($scope.isSidebarVisible()) { $scope.hideSidebar(); } else { @@ -941,7 +304,7 @@ weechat.controller('WeechatCtrl', ['$rootScope', '$scope', '$store', '$timeout', // Open and close panels while on mobile devices through swiping $scope.openNick = function() { - if ($rootScope.isMobileUi()) { + if (utils.isMobileUi()) { if ($scope.nonicklist) { $scope.nonicklist = false; } @@ -949,7 +312,7 @@ weechat.controller('WeechatCtrl', ['$rootScope', '$scope', '$store', '$timeout', }; $scope.closeNick = function() { - if ($rootScope.isMobileUi()) { + if (utils.isMobileUi()) { if (!$scope.nonicklist) { $scope.nonicklist = true; } @@ -971,19 +334,19 @@ weechat.controller('WeechatCtrl', ['$rootScope', '$scope', '$store', '$timeout', return; } if ($scope.useFavico) { - $scope.updateFavico(); + notifications.updateFavico(); } else { - $scope.favico.reset(); + $rootScope.favico.reset(); } }); // Update font family when changed $scope.$watch('fontfamily', function() { - changeClassStyle('favorite-font', 'fontFamily', $scope.fontfamily); + utils.changeClassStyle('favorite-font', 'fontFamily', $scope.fontfamily); }); // Update font size when changed $scope.$watch('fontsize', function() { - changeClassStyle('favorite-font', 'fontSize', $scope.fontsize); + 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. @@ -994,7 +357,7 @@ weechat.controller('WeechatCtrl', ['$rootScope', '$scope', '$store', '$timeout', $scope.setActiveBuffer = function(bufferId, key) { // If we are on mobile we need to collapse the menu on sidebar clicks // We use 968 px as the cutoff, which should match the value in glowingbear.css - if ($rootScope.isMobileUi()) { + if (utils.isMobileUi()) { $scope.hideSidebar(); } return models.setActiveBuffer(bufferId, key); @@ -1014,6 +377,7 @@ weechat.controller('WeechatCtrl', ['$rootScope', '$scope', '$store', '$timeout', }; +//XXX this does not belong here (or does it?) // Calculate number of lines to fetch $scope.calculateNumLines = function() { var bufferlineElements = document.querySelectorAll(".bufferline"); @@ -1035,10 +399,10 @@ weechat.controller('WeechatCtrl', ['$rootScope', '$scope', '$store', '$timeout', if ($rootScope.connected) { // Show the sidebar if switching away from mobile view, hide it when switching to mobile // Wrap in a condition so we save ourselves the $apply if nothing changes (50ms or more) - if ($scope.wasMobileUi && !$scope.isMobileUi()) { + if ($scope.wasMobileUi && !utils.isMobileUi()) { $scope.showSidebar(); } - $scope.wasMobileUi = $scope.isMobileUi(); + $scope.wasMobileUi = utils.isMobileUi(); $scope.calculateNumLines(); // if we're scrolled to the bottom, scroll down to the same position after the resize @@ -1093,7 +457,7 @@ weechat.controller('WeechatCtrl', ['$rootScope', '$scope', '$store', '$timeout', $scope.connect = function() { - $scope.requestNotificationPermission(); + notifications.requestNotificationPermission(); $rootScope.sslError = false; $rootScope.securityError = false; $rootScope.errorMessage = false; @@ -1104,6 +468,8 @@ weechat.controller('WeechatCtrl', ['$rootScope', '$scope', '$store', '$timeout', $scope.connectbutton = 'Connect'; connection.disconnect(); }; + +//XXX this is a bit out of place here, either move up to the rest of the firefox install code or remove $scope.install = function() { if (navigator.mozApps !== undefined) { // Find absolute url with trailing '/' or '/index.html' removed @@ -1158,62 +524,7 @@ weechat.controller('WeechatCtrl', ['$rootScope', '$scope', '$store', '$timeout', } }; - /* Function gets called from bufferLineAdded code if user should be notified */ - $rootScope.createHighlight = function(buffer, message) { - var title = ''; - var body = ''; - var numNotifications = buffer.notification; - - if (['#', '&', '+', '!'].indexOf(buffer.shortName.charAt(0)) < 0) { - if (numNotifications > 1) { - title = numNotifications.toString() + ' private messages from '; - } else { - title = 'Private message from '; - } - body = message.text; - } else { - if (numNotifications > 1) { - title = numNotifications.toString() + ' highlights in '; - } else { - title = 'Highlight in '; - } - var prefix = ''; - for (var i = 0; i < message.prefix.length; i++) { - prefix += message.prefix[i].text; - } - body = '<' + prefix + '> ' + message.text; - } - title += buffer.shortName; - title += buffer.fullName.replace(/irc.([^\.]+)\..+/, " ($1)"); - - var notification = new Notification(title, { - body: body, - icon: 'assets/img/favicon.png' - }); - - // Cancel notification automatically - var timeout = 15*1000; - notification.onshow = function() { - setTimeout(function() { - notification.close(); - }, timeout); - }; - - // Click takes the user to the buffer - notification.onclick = function() { - models.setActiveBuffer(buffer.id); - window.focus(); - notification.close(); - }; - - if ($scope.soundnotification) { - // TODO fill in a sound file - var audioFile = "assets/audio/sonar"; - var soundHTML = ''; - document.getElementById("soundNotification").innerHTML = soundHTML; - } - }; - +//XXX what do we do with this? $scope.hasUnread = function(buffer) { // if search is set, return every buffer if ($scope.search && $scope.search !== "") { @@ -1253,6 +564,7 @@ weechat.controller('WeechatCtrl', ['$rootScope', '$scope', '$store', '$timeout', return true; }; +//XXX not sure whether this belongs here $rootScope.switchToActivityBuffer = function() { // Find next buffer with activity and switch to it var sortedBuffers = _.sortBy($scope.getBuffers(), 'number'); @@ -1304,11 +616,10 @@ weechat.controller('WeechatCtrl', ['$rootScope', '$scope', '$store', '$timeout', // Chrome requires us to set this or it will not show the dialog event.returnValue = "You have an active connection to your WeeChat relay. Please disconnect using the button in the top-right corner or by double-tapping the Escape key."; } - $scope.favico.reset(); + $rootScope.favico.reset(); }; -}] -); +}]); weechat.config(['$routeProvider', function($routeProvider) { @@ -1318,365 +629,3 @@ weechat.config(['$routeProvider', }); } ]); - - -weechat.directive('plugin', ['$rootScope', function($rootScope) { - /* - * Plugin directive - * Shows additional plugin content - */ - return { - templateUrl: 'directives/plugin.html', - - scope: { - plugin: '=data' - }, - - controller: ["$scope", function($scope) { - - $scope.displayedContent = ""; - - $scope.plugin.visible = $rootScope.auto_display_embedded_content; - - $scope.hideContent = function() { - $scope.plugin.visible = false; - }; - - $scope.showContent = function() { - /* - * Shows the plugin content. - * displayedContent is bound to the DOM. - * Actual plugin content is only fetched when - * content is shown. - */ - - // If the plugin is asynchronous / lazy, execute it now and store - // the result. This ensures that the callback is executed only once - if ($scope.plugin.content instanceof Function) { - $scope.plugin.content = $scope.plugin.content(); - } - $scope.displayedContent = $scope.plugin.content; - $scope.plugin.visible = true; - - // Scroll embed content into view - var scroll = function() { - var embed = document.querySelector(".embed_" + $scope.plugin.$$hashKey); - if (embed) { - embed.scrollIntoViewIfNeeded(); - } - }; - setTimeout(scroll, 100); - }; - - if ($scope.plugin.visible) { - $scope.showContent(); - } - }] - }; -}]); - - -weechat.directive('inputBar', function() { - - return { - - templateUrl: 'directives/input.html', - - scope: { - inputId: '@inputId' - }, - - controller: ['$rootScope', '$scope', '$element', '$log', 'connection', 'models', - function($rootScope, - $scope, - $element, - $log, - connection, - models) { - - /* - * Returns the input element - */ - $scope.getInputNode = function() { - return document.querySelector('textarea#' + $scope.inputId); - }; - - $scope.hideSidebar = function() { - $rootScope.hideSidebar(); - }; - - $scope.completeNick = function() { - // input DOM node - var inputNode = $scope.getInputNode(); - - // get current caret position - var caretPos = inputNode.selectionStart; - - // get current active buffer - var activeBuffer = models.getActiveBuffer(); - - // Empty input makes $scope.command undefined -- use empty string instead - var input = $scope.command || ''; - - // complete nick - var nickComp = IrcUtils.completeNick(input, caretPos, $scope.iterCandidate, - activeBuffer.getNicklistByTime(), ':'); - - // remember iteration candidate - $scope.iterCandidate = nickComp.iterCandidate; - - // update current input - $scope.command = nickComp.text; - - // update current caret position - setTimeout(function() { - inputNode.focus(); - inputNode.setSelectionRange(nickComp.caretPos, nickComp.caretPos); - }, 0); - }; - - - // Send the message to the websocket - $scope.sendMessage = function() { - var ab = models.getActiveBuffer(); - - // It's undefined early in the lifecycle of the program. - // Don't send empty commands - if($scope.command !== undefined && $scope.command !== '') { - - // log to buffer history - ab.addToHistory($scope.command); - - // Split the command into multiple commands based on line breaks - _.each($scope.command.split(/\r?\n/), function(line) { - connection.sendMessage(line); - }); - - // Check for /clear command - if ($scope.command === '/buffer clear' || $scope.command === '/c') { - $log.debug('Clearing lines'); - ab.clear(); - } - - // Empty the input after it's sent - $scope.command = ''; - } - - $scope.getInputNode().focus(); - }; - - $rootScope.addMention = function(prefix) { - // Extract nick from bufferline prefix - var nick = prefix[prefix.length - 1].text; - - var newValue = $scope.command || ''; // can be undefined, in that case, use the empty string - var addColon = newValue.length === 0; - if (newValue.length > 0) { - // Try to determine if it's a sequence of nicks - var trimmedValue = newValue.trim(); - if (trimmedValue.charAt(trimmedValue.length - 1) === ':') { - // get last word - var lastSpace = trimmedValue.lastIndexOf(' ') + 1; - var lastWord = trimmedValue.slice(lastSpace, trimmedValue.length - 1); - var nicklist = models.getActiveBuffer().getNicklistByTime(); - // check against nicklist to see if it's a list of highlights - for (var index in nicklist) { - if (nicklist[index].name === lastWord) { - // It's another highlight! - newValue = newValue.slice(0, newValue.lastIndexOf(':')) + ' '; - addColon = true; - break; - } - } - } - - // Add a space before the nick if there isn't one already - // Last char might have changed above, so re-check - if (newValue.charAt(newValue.length - 1) !== ' ') { - newValue += ' '; - } - } - // Add highlight to nicklist - newValue += nick; - if (addColon) { - newValue += ': '; - } - $scope.command = newValue; - $scope.getInputNode().focus(); - }; - - - // Handle key presses in the input bar - $rootScope.handleKeyPress = function($event) { - // don't do anything if not connected - if (!$rootScope.connected) { - return true; - } - - var inputNode = $scope.getInputNode(); - - // Support different browser quirks - var code = $event.keyCode ? $event.keyCode : $event.charCode; - - // any other key than Tab resets nick completion iteration - var tmpIterCandidate = $scope.iterCandidate; - $scope.iterCandidate = null; - - // Left Alt+[0-9] -> jump to buffer - if ($event.altKey && !$event.ctrlKey && (code > 47 && code < 58)) { - if (code === 48) { - code = 58; - } - - var bufferNumber = code - 48 - 1 ; - // Map the buffers to only their numbers and IDs so we don't have to - // copy the entire (possibly very large) buffer object, and then sort - // the buffers according to their WeeChat number - var sortedBuffers = _.map(models.getBuffers(), function(buffer) { - return [buffer.number, buffer.id]; - }).sort(function(left, right) { - // By default, Array.prototype.sort() sorts alphabetically. - // Pass an ordering function to sort by first element. - return left[0] - right[0]; - }); - var activeBufferId = sortedBuffers[bufferNumber]; - if (activeBufferId) { - models.setActiveBuffer(activeBufferId[1]); - $event.preventDefault(); - } - } - - // Tab -> nick completion - if (code === 9 && !$event.altKey && !$event.ctrlKey) { - $event.preventDefault(); - $scope.iterCandidate = tmpIterCandidate; - $scope.completeNick(); - return true; - } - - // Left Alt+n -> toggle nicklist - if ($event.altKey && !$event.ctrlKey && code === 78) { - $event.preventDefault(); - $rootScope.toggleNicklist(); - return true; - } - - // Alt+A -> switch to buffer with activity - if ($event.altKey && (code === 97 || code === 65)) { - $event.preventDefault(); - $rootScope.switchToActivityBuffer(); - return true; - } - - // Alt+L -> focus on input bar - if ($event.altKey && (code === 76 || code === 108)) { - $event.preventDefault(); - inputNode.focus(); - inputNode.setSelectionRange($scope.command.length, $scope.command.length); - return true; - } - - // Alt+< -> switch to previous buffer - if ($event.altKey && (code === 60 || code === 226)) { - var previousBuffer = models.getPreviousBuffer(); - if (previousBuffer) { - models.setActiveBuffer(previousBuffer.id); - $event.preventDefault(); - return true; - } - } - - // Double-tap Escape -> disconnect - if (code === 27) { - $event.preventDefault(); - - // Check if a modal is visible. If so, close it instead of disconnecting - var modals = document.querySelectorAll('.gb-modal'); - for (var modalId = 0; modalId < modals.length; modalId++) { - if (modals[modalId].getAttribute('data-state') === 'visible') { - modals[modalId].setAttribute('data-state', 'hidden'); - return true; - } - } - - if (typeof $scope.lastEscape !== "undefined" && (Date.now() - $scope.lastEscape) <= 500) { - // Double-tap - connection.disconnect(); - } - $scope.lastEscape = Date.now(); - return true; - } - - // Alt+G -> focus on buffer filter input - if ($event.altKey && (code === 103 || code === 71)) { - $event.preventDefault(); - document.getElementById('bufferFilter').focus(); - return true; - } - - // Arrow up -> go up in history - if ($event.type === "keydown" && code === 38) { - $scope.command = models.getActiveBuffer().getHistoryUp($scope.command); - // Set cursor to last position. Need 0ms timeout because browser sets cursor - // position to the beginning after this key handler returns. - setTimeout(function() { - if ($scope.command) { - inputNode.setSelectionRange($scope.command.length, $scope.command.length); - } - }, 0); - return true; - } - - // Arrow down -> go down in history - if ($event.type === "keydown" && code === 40) { - $scope.command = models.getActiveBuffer().getHistoryDown($scope.command); - // We don't need to set the cursor to the rightmost position here, the browser does that for us - return true; - } - - // Enter to submit, shift-enter for newline - if (code == 13 && !$event.shiftKey && document.activeElement === inputNode) { - $event.preventDefault(); - $scope.sendMessage(); - return true; - } - // Some readline keybindings - if ($rootScope.readlineBindings && $event.ctrlKey && !$event.altKey && !$event.shiftKey && document.activeElement === inputNode) { - // get current caret position - var caretPos = inputNode.selectionStart; - // Ctrl-a - if (code == 65) { - inputNode.setSelectionRange(0, 0); - // Ctrl-e - } else if (code == 69) { - inputNode.setSelectionRange($scope.command.length, $scope.command.length); - // Ctrl-u - } else if (code == 85) { - $scope.command = $scope.command.slice(caretPos); - setTimeout(function() { - inputNode.setSelectionRange(0, 0); - }); - // Ctrl-k - } else if (code == 75) { - $scope.command = $scope.command.slice(0, caretPos); - setTimeout(function() { - inputNode.setSelectionRange($scope.command.length, $scope.command.length); - }); - // Ctrl-w - } else if (code == 87) { - var trimmedValue = $scope.command.slice(0, caretPos); - var lastSpace = trimmedValue.lastIndexOf(' ') + 1; - $scope.command = $scope.command.slice(0, lastSpace) + $scope.command.slice(caretPos, $scope.command.length); - setTimeout(function() { - inputNode.setSelectionRange(lastSpace, lastSpace); - }); - } else { - return false; - } - $event.preventDefault(); - return true; - } - }; - }] - }; -}); diff --git a/js/handlers.js b/js/handlers.js new file mode 100644 index 0000000..4bd7247 --- /dev/null +++ b/js/handlers.js @@ -0,0 +1,207 @@ +var weechat = angular.module('weechat'); + +weechat.factory('handlers', ['$rootScope', '$log', 'models', 'plugins', 'notifications', function($rootScope, $log, models, plugins, notifications) { + + var handleBufferClosing = function(message) { + var bufferMessage = message.objects[0].content[0]; + var bufferId = bufferMessage.pointers[0]; + models.closeBuffer(bufferId); + }; + + var handleLine = function(line, manually) { + var message = new models.BufferLine(line); + var buffer = models.getBuffer(message.buffer); + buffer.requestedLines++; + // Only react to line if its displayed + if (message.displayed) { + message = plugins.PluginManager.contentForMessage(message); + buffer.addLine(message); + + if (manually) { + buffer.lastSeen++; + } + + if (buffer.active && !manually) { + $rootScope.scrollWithBuffer(); + } + + if (!manually && (!buffer.active || !$rootScope.isWindowFocused())) { + if (buffer.notify > 1 && _.contains(message.tags, 'notify_message') && !_.contains(message.tags, 'notify_none')) { + buffer.unread++; + $rootScope.$emit('notificationChanged'); + } + + if ((buffer.notify !== 0 && message.highlight) || _.contains(message.tags, 'notify_private')) { + buffer.notification++; + notifications.createHighlight(buffer, message); + $rootScope.$emit('notificationChanged'); + } + } + } + }; + + var handleBufferLineAdded = function(message) { + message.objects[0].content.forEach(function(l) { + handleLine(l, false); + }); + }; + + var handleBufferOpened = function(message) { + var bufferMessage = message.objects[0].content[0]; + var buffer = new models.Buffer(bufferMessage); + models.addBuffer(buffer); + /* Until we can decide if user asked for this buffer to be opened + * or not we will let user click opened buffers. + models.setActiveBuffer(buffer.id); + */ + }; + + var handleBufferTitleChanged = function(message) { + var obj = message.objects[0].content[0]; + var buffer = obj.pointers[0]; + var old = models.getBuffer(buffer); + old.fullName = obj.full_name; + old.title = obj.title; + old.number = obj.number; + }; + + var handleBufferRenamed = function(message) { + var obj = message.objects[0].content[0]; + var buffer = obj.pointers[0]; + var old = models.getBuffer(buffer); + old.fullName = obj.full_name; + old.shortName = obj.short_name; + }; + + var handleBufferLocalvarChanged = function(message) { + var obj = message.objects[0].content[0]; + var buffer = obj.pointers[0]; + var old = models.getBuffer(buffer); + + var localvars = obj.local_variables; + if (old !== undefined && localvars !== undefined) { + // Update indendation status + old.indent = (['channel', 'private'].indexOf(localvars.type) >= 0); + } + }; + + /* + * Handle answers to (lineinfo) messages + * + * (lineinfo) messages are specified by this client. It is request after bufinfo completes + */ + var handleLineInfo = function(message, manually) { + var lines = message.objects[0].content.reverse(); + if (manually === undefined) { + manually = true; + } + lines.forEach(function(l) { + handleLine(l, manually); + }); + }; + + /* + * Handle answers to hotlist request + */ + var handleHotlistInfo = function(message) { + if (message.objects.length === 0) { + return; + } + var hotlist = message.objects[0].content; + hotlist.forEach(function(l) { + var buffer = models.getBuffer(l.buffer); + // 1 is message + buffer.unread += l.count[1]; + // 2 is private + buffer.notification += l.count[2]; + // 3 is highlight + buffer.notification += l.count[3]; + /* Since there is unread messages, we can guess + * what the last read line is and update it accordingly + */ + var unreadSum = _.reduce(l.count, function(memo, num) { return memo + num; }, 0); + buffer.lastSeen = buffer.lines.length - 1 - unreadSum; + }); + }; + + /* + * Handle nicklist event + */ + var handleNicklist = function(message) { + var nicklist = message.objects[0].content; + var group = 'root'; + nicklist.forEach(function(n) { + var buffer = models.getBuffer(n.pointers[0]); + if (n.group === 1) { + var g = new models.NickGroup(n); + group = g.name; + buffer.nicklist[group] = g; + } else { + var nick = new models.Nick(n); + buffer.addNick(group, nick); + } + }); + }; + /* + * Handle nicklist diff event + */ + var handleNicklistDiff = function(message) { + var nicklist = message.objects[0].content; + var group; + nicklist.forEach(function(n) { + var buffer = models.getBuffer(n.pointers[0]); + var d = n._diff; + if (n.group === 1) { + group = n.name; + if (group === undefined) { + var g = new models.NickGroup(n); + buffer.nicklist[group] = g; + group = g.name; + } + } else { + var nick = new models.Nick(n); + if (d === 43) { // + + buffer.addNick(group, nick); + } else if (d === 45) { // - + buffer.delNick(group, nick); + } else if (d === 42) { // * + buffer.updateNick(group, nick); + } + } + }); + }; + + var eventHandlers = { + _buffer_closing: handleBufferClosing, + _buffer_line_added: handleBufferLineAdded, + _buffer_localvar_added: handleBufferLocalvarChanged, + _buffer_localvar_removed: handleBufferLocalvarChanged, + _buffer_opened: handleBufferOpened, + _buffer_title_changed: handleBufferTitleChanged, + _buffer_renamed: handleBufferRenamed, + _nicklist: handleNicklist, + _nicklist_diff: handleNicklistDiff + }; + + $rootScope.$on('onMessage', function(event, message) { + if (_.has(eventHandlers, message.id)) { + eventHandlers[message.id](message); + } else { + $log.debug('Unhandled event received: ' + message.id); + } + }); + + var handleEvent = function(event) { + if (_.has(eventHandlers, event.id)) { + eventHandlers[event.id](event); + } + }; + + return { + handleEvent: handleEvent, + handleLineInfo: handleLineInfo, + handleHotlistInfo: handleHotlistInfo, + handleNicklist: handleNicklist + }; + +}]); diff --git a/js/inputbar.js b/js/inputbar.js new file mode 100644 index 0000000..8f3ddd2 --- /dev/null +++ b/js/inputbar.js @@ -0,0 +1,307 @@ +var weechat = angular.module('weechat'); + +weechat.directive('inputBar', function() { + + return { + + templateUrl: 'directives/input.html', + + scope: { + inputId: '@inputId' + }, + + controller: ['$rootScope', '$scope', '$element', '$log', 'connection', 'models', 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) { + + /* + * Returns the input element + */ + $scope.getInputNode = function() { + return document.querySelector('textarea#' + $scope.inputId); + }; + + $scope.hideSidebar = function() { + $rootScope.hideSidebar(); + }; + + $scope.completeNick = function() { + // input DOM node + var inputNode = $scope.getInputNode(); + + // get current caret position + var caretPos = inputNode.selectionStart; + + // get current active buffer + var activeBuffer = models.getActiveBuffer(); + + // Empty input makes $scope.command undefined -- use empty string instead + var input = $scope.command || ''; + + // complete nick + var nickComp = IrcUtils.completeNick(input, caretPos, $scope.iterCandidate, + activeBuffer.getNicklistByTime(), ':'); + + // remember iteration candidate + $scope.iterCandidate = nickComp.iterCandidate; + + // update current input + $scope.command = nickComp.text; + + // update current caret position + setTimeout(function() { + inputNode.focus(); + inputNode.setSelectionRange(nickComp.caretPos, nickComp.caretPos); + }, 0); + }; + + + // Send the message to the websocket + $scope.sendMessage = function() { + //XXX Use a signal here + var ab = models.getActiveBuffer(); + + // It's undefined early in the lifecycle of the program. + // Don't send empty commands + if($scope.command !== undefined && $scope.command !== '') { + + // log to buffer history + ab.addToHistory($scope.command); + + // Split the command into multiple commands based on line breaks + _.each($scope.command.split(/\r?\n/), function(line) { + connection.sendMessage(line); + }); + + // Check for /clear command + if ($scope.command === '/buffer clear' || $scope.command === '/c') { + $log.debug('Clearing lines'); + ab.clear(); + } + + // Empty the input after it's sent + $scope.command = ''; + } + + $scope.getInputNode().focus(); + }; + + //XXX THIS DOES NOT BELONG HERE! + $rootScope.addMention = function(prefix) { + // Extract nick from bufferline prefix + var nick = prefix[prefix.length - 1].text; + + var newValue = $scope.command || ''; // can be undefined, in that case, use the empty string + var addColon = newValue.length === 0; + if (newValue.length > 0) { + // Try to determine if it's a sequence of nicks + var trimmedValue = newValue.trim(); + if (trimmedValue.charAt(trimmedValue.length - 1) === ':') { + // get last word + var lastSpace = trimmedValue.lastIndexOf(' ') + 1; + var lastWord = trimmedValue.slice(lastSpace, trimmedValue.length - 1); + var nicklist = models.getActiveBuffer().getNicklistByTime(); + // check against nicklist to see if it's a list of highlights + for (var index in nicklist) { + if (nicklist[index].name === lastWord) { + // It's another highlight! + newValue = newValue.slice(0, newValue.lastIndexOf(':')) + ' '; + addColon = true; + break; + } + } + } + + // Add a space before the nick if there isn't one already + // Last char might have changed above, so re-check + if (newValue.charAt(newValue.length - 1) !== ' ') { + newValue += ' '; + } + } + // Add highlight to nicklist + newValue += nick; + if (addColon) { + newValue += ': '; + } + $scope.command = newValue; + $scope.getInputNode().focus(); + }; + + + // Handle key presses in the input bar + $rootScope.handleKeyPress = function($event) { + // don't do anything if not connected + if (!$rootScope.connected) { + return true; + } + + var inputNode = $scope.getInputNode(); + + // Support different browser quirks + var code = $event.keyCode ? $event.keyCode : $event.charCode; + + // any other key than Tab resets nick completion iteration + var tmpIterCandidate = $scope.iterCandidate; + $scope.iterCandidate = null; + + // Left Alt+[0-9] -> jump to buffer + if ($event.altKey && !$event.ctrlKey && (code > 47 && code < 58)) { + if (code === 48) { + code = 58; + } + + var bufferNumber = code - 48 - 1 ; + // Map the buffers to only their numbers and IDs so we don't have to + // copy the entire (possibly very large) buffer object, and then sort + // the buffers according to their WeeChat number + var sortedBuffers = _.map(models.getBuffers(), function(buffer) { + return [buffer.number, buffer.id]; + }).sort(function(left, right) { + // By default, Array.prototype.sort() sorts alphabetically. + // Pass an ordering function to sort by first element. + return left[0] - right[0]; + }); + var activeBufferId = sortedBuffers[bufferNumber]; + if (activeBufferId) { + models.setActiveBuffer(activeBufferId[1]); + $event.preventDefault(); + } + } + + // Tab -> nick completion + if (code === 9 && !$event.altKey && !$event.ctrlKey) { + $event.preventDefault(); + $scope.iterCandidate = tmpIterCandidate; + $scope.completeNick(); + return true; + } + + // Left Alt+n -> toggle nicklist + if ($event.altKey && !$event.ctrlKey && code === 78) { + $event.preventDefault(); + $rootScope.toggleNicklist(); + return true; + } + + // Alt+A -> switch to buffer with activity + if ($event.altKey && (code === 97 || code === 65)) { + $event.preventDefault(); + $rootScope.switchToActivityBuffer(); + return true; + } + + // Alt+L -> focus on input bar + if ($event.altKey && (code === 76 || code === 108)) { + $event.preventDefault(); + inputNode.focus(); + inputNode.setSelectionRange($scope.command.length, $scope.command.length); + return true; + } + + // Alt+< -> switch to previous buffer + if ($event.altKey && (code === 60 || code === 226)) { + var previousBuffer = models.getPreviousBuffer(); + if (previousBuffer) { + models.setActiveBuffer(previousBuffer.id); + $event.preventDefault(); + return true; + } + } + + // Double-tap Escape -> disconnect + if (code === 27) { + $event.preventDefault(); + + // Check if a modal is visible. If so, close it instead of disconnecting + var modals = document.querySelectorAll('.gb-modal'); + for (var modalId = 0; modalId < modals.length; modalId++) { + if (modals[modalId].getAttribute('data-state') === 'visible') { + modals[modalId].setAttribute('data-state', 'hidden'); + return true; + } + } + + if (typeof $scope.lastEscape !== "undefined" && (Date.now() - $scope.lastEscape) <= 500) { + // Double-tap + connection.disconnect(); + } + $scope.lastEscape = Date.now(); + return true; + } + + // Alt+G -> focus on buffer filter input + if ($event.altKey && (code === 103 || code === 71)) { + $event.preventDefault(); + document.getElementById('bufferFilter').focus(); + return true; + } + + // Arrow up -> go up in history + if ($event.type === "keydown" && code === 38) { + $scope.command = models.getActiveBuffer().getHistoryUp($scope.command); + // Set cursor to last position. Need 0ms timeout because browser sets cursor + // position to the beginning after this key handler returns. + setTimeout(function() { + if ($scope.command) { + inputNode.setSelectionRange($scope.command.length, $scope.command.length); + } + }, 0); + return true; + } + + // Arrow down -> go down in history + if ($event.type === "keydown" && code === 40) { + $scope.command = models.getActiveBuffer().getHistoryDown($scope.command); + // We don't need to set the cursor to the rightmost position here, the browser does that for us + return true; + } + + // Enter to submit, shift-enter for newline + if (code == 13 && !$event.shiftKey && document.activeElement === inputNode) { + $event.preventDefault(); + $scope.sendMessage(); + return true; + } + // Some readline keybindings + if ($rootScope.readlineBindings && $event.ctrlKey && !$event.altKey && !$event.shiftKey && document.activeElement === inputNode) { + // get current caret position + var caretPos = inputNode.selectionStart; + // Ctrl-a + if (code == 65) { + inputNode.setSelectionRange(0, 0); + // Ctrl-e + } else if (code == 69) { + inputNode.setSelectionRange($scope.command.length, $scope.command.length); + // Ctrl-u + } else if (code == 85) { + $scope.command = $scope.command.slice(caretPos); + setTimeout(function() { + inputNode.setSelectionRange(0, 0); + }); + // Ctrl-k + } else if (code == 75) { + $scope.command = $scope.command.slice(0, caretPos); + setTimeout(function() { + inputNode.setSelectionRange($scope.command.length, $scope.command.length); + }); + // Ctrl-w + } else if (code == 87) { + var trimmedValue = $scope.command.slice(0, caretPos); + var lastSpace = trimmedValue.lastIndexOf(' ') + 1; + $scope.command = $scope.command.slice(0, lastSpace) + $scope.command.slice(caretPos, $scope.command.length); + setTimeout(function() { + inputNode.setSelectionRange(lastSpace, lastSpace); + }); + } else { + return false; + } + $event.preventDefault(); + return true; + } + }; + }] + }; +}); diff --git a/js/notifications.js b/js/notifications.js new file mode 100644 index 0000000..fbeba09 --- /dev/null +++ b/js/notifications.js @@ -0,0 +1,141 @@ +var weechat = angular.module('weechat'); + +weechat.factory('notifications', ['$rootScope', '$log', 'models', function($rootScope, $log, models) { + // Ask for permission to display desktop notifications + var requestNotificationPermission = function() { + // Firefox + if (window.Notification) { + Notification.requestPermission(function(status) { + $log.info('Notification permission status: ', status); + if (Notification.permission !== status) { + Notification.permission = status; + } + }); + } + + // Webkit + if (window.webkitNotifications !== undefined) { + var havePermission = window.webkitNotifications.checkPermission(); + if (havePermission !== 0) { // 0 is PERMISSION_ALLOWED + $log.info('Notification permission status: ', havePermission === 0); + window.webkitNotifications.requestPermission(); + } + } + }; + + + // Reduce buffers with "+" operation over a key. Mostly useful for unread/notification counts. + var unreadCount = function(type) { + if (!type) { + type = "unread"; + } + + // Do this the old-fashioned way with iterating over the keys, as underscore proved to be error-prone + var keys = Object.keys(models.model.buffers); + var count = 0; + for (var key in keys) { + count += models.model.buffers[keys[key]][type]; + } + + return count; + }; + + + var updateTitle = function() { + var notifications = unreadCount('notification'); + if (notifications > 0) { + // New notifications deserve an exclamation mark + $rootScope.notificationStatus = '(' + notifications + ') '; + } else { + $rootScope.notificationStatus = ''; + } + + var activeBuffer = models.getActiveBuffer(); + if (activeBuffer) { + $rootScope.pageTitle = activeBuffer.shortName + ' | ' + activeBuffer.title; + } + }; + + var updateFavico = function() { + var notifications = unreadCount('notification'); + if (notifications > 0) { + $rootScope.favico.badge(notifications, { + bgColor: '#d00', + textColor: '#fff' + }); + } else { + var unread = unreadCount('unread'); + if (unread === 0) { + $rootScope.favico.reset(); + } else { + $rootScope.favico.badge(unread, { + bgColor: '#5CB85C', + textColor: '#ff0' + }); + } + } + }; + + /* Function gets called from bufferLineAdded code if user should be notified */ + var createHighlight = function(buffer, message) { + var title = ''; + var body = ''; + var numNotifications = buffer.notification; + + if (['#', '&', '+', '!'].indexOf(buffer.shortName.charAt(0)) < 0) { + if (numNotifications > 1) { + title = numNotifications.toString() + ' private messages from '; + } else { + title = 'Private message from '; + } + body = message.text; + } else { + if (numNotifications > 1) { + title = numNotifications.toString() + ' highlights in '; + } else { + title = 'Highlight in '; + } + var prefix = ''; + for (var i = 0; i < message.prefix.length; i++) { + prefix += message.prefix[i].text; + } + body = '<' + prefix + '> ' + message.text; + } + title += buffer.shortName; + title += buffer.fullName.replace(/irc.([^\.]+)\..+/, " ($1)"); + + var notification = new Notification(title, { + body: body, + icon: 'assets/img/favicon.png' + }); + + // Cancel notification automatically + var timeout = 15*1000; + notification.onshow = function() { + setTimeout(function() { + notification.close(); + }, timeout); + }; + + // Click takes the user to the buffer + notification.onclick = function() { + models.setActiveBuffer(buffer.id); + window.focus(); + notification.close(); + }; + + if ($rootScope.soundnotification) { + // TODO fill in a sound file + var audioFile = "assets/audio/sonar"; + var soundHTML = ''; + document.getElementById("soundNotification").innerHTML = soundHTML; + } + }; + + return { + requestNotificationPermission: requestNotificationPermission, + updateTitle: updateTitle, + updateFavico: updateFavico, + createHighlight: createHighlight, + }; +}]); diff --git a/js/plugin-directive.js b/js/plugin-directive.js new file mode 100644 index 0000000..62ea463 --- /dev/null +++ b/js/plugin-directive.js @@ -0,0 +1,56 @@ +var weechat = angular.module('weechat'); + +weechat.directive('plugin', ['$rootScope', function($rootScope) { + /* + * Plugin directive + * Shows additional plugin content + */ + return { + templateUrl: 'directives/plugin.html', + + scope: { + plugin: '=data' + }, + + controller: ['$scope', function($scope) { + + $scope.displayedContent = ""; + + $scope.plugin.visible = $rootScope.auto_display_embedded_content; + + $scope.hideContent = function() { + $scope.plugin.visible = false; + }; + + $scope.showContent = function() { + /* + * Shows the plugin content. + * displayedContent is bound to the DOM. + * Actual plugin content is only fetched when + * content is shown. + */ + + // If the plugin is asynchronous / lazy, execute it now and store + // the result. This ensures that the callback is executed only once + if ($scope.plugin.content instanceof Function) { + $scope.plugin.content = $scope.plugin.content(); + } + $scope.displayedContent = $scope.plugin.content; + $scope.plugin.visible = true; + + // Scroll embed content into view + var scroll = function() { + var embed = document.querySelector(".embed_" + $scope.plugin.$$hashKey); + if (embed) { + embed.scrollIntoViewIfNeeded(); + } + }; + setTimeout(scroll, 100); + }; + + if ($scope.plugin.visible) { + $scope.showContent(); + } + }] + }; +}]); diff --git a/js/utils.js b/js/utils.js new file mode 100644 index 0000000..5a10c7a --- /dev/null +++ b/js/utils.js @@ -0,0 +1,29 @@ +var weechat = angular.module('weechat'); + +weechat.factory('utils', function() { + // Helper to change style of a class + var changeClassStyle = function(classSelector, attr, value) { + _.each(document.getElementsByClassName(classSelector), function(e) { + e.style[attr] = value; + }); + }; + // Helper to get style from a class + var getClassStyle = function(classSelector, attr) { + _.each(document.getElementsByClassName(classSelector), function(e) { + return e.style[attr]; + }); + }; + + var isMobileUi = function() { + // TODO don't base detection solely on screen width + // You are right. In the meantime I am renaming isMobileDevice to isMobileUi + var mobile_cutoff = 968; + return (document.body.clientWidth < mobile_cutoff); + }; + + return { + changeClassStyle: changeClassStyle, + getClassStyle: getClassStyle, + isMobileUi: isMobileUi + }; +}); diff --git a/package.json b/package.json index 4fe93af..7913fa2 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/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/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 fe24302a73912ad5ec783aa72e5d722e65e87ba3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lorenz=20H=C3=BCbschle-Schneider?= Date: Thu, 28 Aug 2014 17:49:47 +0100 Subject: [PATCH 04/57] Karma: Include files in the right order glowing-bear.js needs to be loaded before the modules building upon it --- test/karma.conf.js | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/test/karma.conf.js b/test/karma.conf.js index 379b976..94d1739 100644 --- a/test/karma.conf.js +++ b/test/karma.conf.js @@ -7,7 +7,20 @@ module.exports = function(config){ 'bower_components/angular/angular.js', 'bower_components/angular-route/angular-route.js', 'bower_components/angular-mocks/angular-mocks.js', - 'js/**/*.js', + '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', 'test/unit/**/*.js' ], From 24f63be25c2148201899fba7fd9fb6ffec3bb7a6 Mon Sep 17 00:00:00 2001 From: David Cormier Date: Sun, 31 Aug 2014 09:59:10 -0400 Subject: [PATCH 05/57] When closing the window, disconnect instead of asking the user to do it --- js/glowingbear.js | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/js/glowingbear.js b/js/glowingbear.js index ec8fbb1..161b9e2 100644 --- a/js/glowingbear.js +++ b/js/glowingbear.js @@ -612,9 +612,7 @@ weechat.controller('WeechatCtrl', ['$rootScope', '$scope', '$store', '$timeout', // Prevent user from accidentally leaving the page window.onbeforeunload = function(event) { if ($rootScope.connected) { - event.preventDefault(); - // Chrome requires us to set this or it will not show the dialog - event.returnValue = "You have an active connection to your WeeChat relay. Please disconnect using the button in the top-right corner or by double-tapping the Escape key."; + $scope.disconnect(); } $rootScope.favico.reset(); }; From 9438c3844f199dfc41e48957b75ec27a46cdc7b2 Mon Sep 17 00:00:00 2001 From: David Cormier Date: Sun, 31 Aug 2014 10:29:13 -0400 Subject: [PATCH 06/57] Ask confirmation before closing if user has unsent input --- index.html | 2 +- js/glowingbear.js | 17 ++++++++++++++--- js/inputbar.js | 3 ++- 3 files changed, 17 insertions(+), 5 deletions(-) diff --git a/index.html b/index.html index af653d8..45327bc 100644 --- a/index.html +++ b/index.html @@ -280,7 +280,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 161b9e2..88801cc 100644 --- a/js/glowingbear.js +++ b/js/glowingbear.js @@ -2,6 +2,8 @@ var weechat = angular.module('weechat', ['ngRoute', 'localStorage', 'weechatMode weechat.controller('WeechatCtrl', ['$rootScope', '$scope', '$store', '$timeout', '$log', 'models', 'connection', 'notifications', 'utils', function ($rootScope, $scope, $store, $timeout, $log, models, connection, notifications, utils) { + $scope.command = ''; + // From: http://stackoverflow.com/a/18539624 by StackOverflow user "plantian" $rootScope.countWatchers = function () { var q = [$rootScope], watchers = 0, scope; @@ -611,10 +613,19 @@ weechat.controller('WeechatCtrl', ['$rootScope', '$scope', '$store', '$timeout', // Prevent user from accidentally leaving the page window.onbeforeunload = function(event) { - if ($rootScope.connected) { - $scope.disconnect(); + + if ($scope.command !== null && $scope.command !== '') { + event.preventDefault(); + // Chrome requires this + // Firefox does not show the site provides message + event.returnValue = "Any unsent input will be lost. Are you sure that you want to quit?"; + + } else { + if ($rootScope.connected) { + $scope.disconnect(); + } + $scope.favico.reset(); } - $rootScope.favico.reset(); }; }]); diff --git a/js/inputbar.js b/js/inputbar.js index 8f3ddd2..54ea52c 100644 --- a/js/inputbar.js +++ b/js/inputbar.js @@ -7,7 +7,8 @@ weechat.directive('inputBar', function() { templateUrl: 'directives/input.html', scope: { - inputId: '@inputId' + inputId: '@inputId', + command: '=command' }, controller: ['$rootScope', '$scope', '$element', '$log', 'connection', 'models', function($rootScope, From 658e876643942436169632e8b83b9d360eeca7a7 Mon Sep 17 00:00:00 2001 From: David Cormier Date: Mon, 1 Sep 2014 09:20:37 -0400 Subject: [PATCH 07/57] fixup font-family and font-size setting glitches Replace fontSize placeholder by default value. Placeholders should be used as suggestions, not effective values. Remove spurious default value for fontFamily (it is defined right below) --- index.html | 2 +- js/glowingbear.js | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/index.html b/index.html index 45327bc..59ee79f 100644 --- a/index.html +++ b/index.html @@ -305,7 +305,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 88801cc..78da583 100644 --- a/js/glowingbear.js +++ b/js/glowingbear.js @@ -248,9 +248,9 @@ weechat.controller('WeechatCtrl', ['$rootScope', '$scope', '$store', '$timeout', // Save setting for playing sound on notification $store.bind($scope, "soundnotification", false); // Save setting for font family - $store.bind($scope, "fontfamily", utils.getClassStyle('favorite-font', 'fontFamily')); + $store.bind($scope, "fontfamily"); // Save setting for font size - $store.bind($scope, "fontsize", utils.getClassStyle('favorite-font', 'fontSize')); + $store.bind($scope, "fontsize", "14px"); // Save setting for readline keybindings $store.bind($scope, "readlineBindings", false); From 89391c0c61c8103cd6a348cec678fb9b32fe177c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lorenz=20H=C3=BCbschle-Schneider?= Date: Tue, 2 Sep 2014 19:06:31 +0100 Subject: [PATCH 08/57] Don't capitalise host name Fixes #439 --- index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/index.html b/index.html index 59ee79f..48b1963 100644 --- a/index.html +++ b/index.html @@ -69,7 +69,7 @@
- +
From c501215bae5f00cc6f619a8d7a6de49763903585 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lorenz=20H=C3=BCbschle-Schneider?= Date: Thu, 4 Sep 2014 22:34:52 +0100 Subject: [PATCH 09/57] Re-enable "fetch more lines" after trimming lines on buffer switch Fixes #404 --- js/glowingbear.js | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/js/glowingbear.js b/js/glowingbear.js index 78da583..60ef1af 100644 --- a/js/glowingbear.js +++ b/js/glowingbear.js @@ -108,10 +108,14 @@ weechat.controller('WeechatCtrl', ['$rootScope', '$scope', '$store', '$timeout', // case where a buffer is opened for the first time ;) var minRetainUnread = ab.lines.length - unreadSum + 5; // do not discard unread lines and keep 5 additional lines for context var surplusLines = ab.lines.length - (2 * $scope.lines_per_screen + 10); // retain up to 2*(screenful + 10) + 10 lines because magic numbers - var linesToRemove = Math.max(0, Math.min(minRetainUnread, surplusLines)); - ab.lines.splice(0, linesToRemove); // remove the lines from the buffer - ab.requestedLines -= linesToRemove; // to ensure that the correct amount of lines is fetched should more be requested - ab.lastSeen -= linesToRemove; // adjust readmarker + var linesToRemove = Math.min(minRetainUnread, surplusLines); + + if (linesToRemove > 0) { + ab.lines.splice(0, linesToRemove); // remove the lines from the buffer + ab.requestedLines -= linesToRemove; // to ensure that the correct amount of lines is fetched should more be requested + ab.lastSeen -= linesToRemove; // adjust readmarker + ab.allLinesFetched = false; // we just removed lines, so we don't have all of them. re-enable "fetch more lines" + } $scope.bufferlines = ab.lines; $scope.nicklist = ab.nicklist; From 91480e91ab59aff1d762d937640d822f25868e47 Mon Sep 17 00:00:00 2001 From: Ailin Nemui Date: Mon, 1 Sep 2014 17:22:17 +0200 Subject: [PATCH 10/57] make the help more pleasant by adding kbd tags --- index.html | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/index.html b/index.html index 48b1963..ebcb42a 100644 --- a/index.html +++ b/index.html @@ -129,16 +129,16 @@

Shortcuts

Glowing Bear has a few shortcuts:
    -
  • ALT-n: Toggle nicklist
  • -
  • ALT-l: Focus on input bar
  • -
  • ALT-[0-9]: Switch to buffer number N
  • -
  • ALT-a: Focus on next buffer with activity
  • -
  • ALT-<: Switch to previous active buffer
  • -
  • ALT-g: Focus on buffer list filter
  • -
  • Esc-Esc: Disconnect (double-tap)
  • +
  • ALT-n: Toggle nicklist
  • +
  • ALT-l: Focus on input bar
  • +
  • ALT-[0-9]: Switch to buffer number N
  • +
  • ALT-a: Focus on next buffer with activity
  • +
  • ALT-<: Switch to previous active buffer
  • +
  • ALT-g: Focus on buffer list filter
  • +
  • Esc-Esc: Disconnect (double-tap)
  • Arrow keys: Navigate history
  • -
  • Tab key: Complete nick
  • -
  • The following readline/emacs style keybindings can be enabled with a setting: Ctrl-a, Ctrl-e, Ctrl-u, Ctrl-k, Ctrl-w
  • +
  • Tab: Complete nick
  • +
  • The following readline/emacs style keybindings can be enabled with a setting: Ctrl-a, Ctrl-e, Ctrl-u, Ctrl-k, Ctrl-w
From 156640c3ba49339b5ce77fb6a12a3c2394f67eae Mon Sep 17 00:00:00 2001 From: Ailin Nemui Date: Wed, 3 Sep 2014 14:39:38 +0200 Subject: [PATCH 11/57] Properly Show and hide the sidebar on mobile layout when using Alt+g --- js/glowingbear.js | 2 +- js/inputbar.js | 5 ++++- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/js/glowingbear.js b/js/glowingbear.js index 78da583..20ea209 100644 --- a/js/glowingbear.js +++ b/js/glowingbear.js @@ -605,7 +605,7 @@ weechat.controller('WeechatCtrl', ['$rootScope', '$scope', '$store', '$timeout', else if (code === 13) { $event.preventDefault(); if ($scope.filteredBuffers.length > 0) { - models.setActiveBuffer($scope.filteredBuffers[0].id); + $scope.setActiveBuffer($scope.filteredBuffers[0].id); } $scope.search = ''; } diff --git a/js/inputbar.js b/js/inputbar.js index 54ea52c..a7fa229 100644 --- a/js/inputbar.js +++ b/js/inputbar.js @@ -167,7 +167,7 @@ weechat.directive('inputBar', function() { }); var activeBufferId = sortedBuffers[bufferNumber]; if (activeBufferId) { - models.setActiveBuffer(activeBufferId[1]); + $scope.$parent.setActiveBuffer(activeBufferId[1]); $event.preventDefault(); } } @@ -236,6 +236,9 @@ weechat.directive('inputBar', function() { // Alt+G -> focus on buffer filter input if ($event.altKey && (code === 103 || code === 71)) { $event.preventDefault(); + if (!$scope.$parent.isSidebarVisible()) { + $scope.$parent.showSidebar(); + } document.getElementById('bufferFilter').focus(); return true; } From a63150e697cfb426b7d6173d3601bcea7884a709 Mon Sep 17 00:00:00 2001 From: Ailin Nemui Date: Fri, 5 Sep 2014 09:12:07 +0200 Subject: [PATCH 12/57] add pgup/pgdn keys and fix history in multiline edits --- js/inputbar.js | 45 ++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 42 insertions(+), 3 deletions(-) diff --git a/js/inputbar.js b/js/inputbar.js index 54ea52c..96baf63 100644 --- a/js/inputbar.js +++ b/js/inputbar.js @@ -240,8 +240,14 @@ weechat.directive('inputBar', function() { return true; } + var caretPos; + // Arrow up -> go up in history - if ($event.type === "keydown" && code === 38) { + if ($event.type === "keydown" && code === 38 && document.activeElement === inputNode) { + caretPos = inputNode.selectionStart; + if ($scope.command.slice(0, caretPos).indexOf("\n") !== -1) { + return false; + } $scope.command = models.getActiveBuffer().getHistoryUp($scope.command); // Set cursor to last position. Need 0ms timeout because browser sets cursor // position to the beginning after this key handler returns. @@ -254,7 +260,11 @@ weechat.directive('inputBar', function() { } // Arrow down -> go down in history - if ($event.type === "keydown" && code === 40) { + if ($event.type === "keydown" && code === 40 && document.activeElement === inputNode) { + caretPos = inputNode.selectionStart; + if ($scope.command.slice(caretPos).indexOf("\n") !== -1) { + return false; + } $scope.command = models.getActiveBuffer().getHistoryDown($scope.command); // We don't need to set the cursor to the rightmost position here, the browser does that for us return true; @@ -266,10 +276,39 @@ weechat.directive('inputBar', function() { $scope.sendMessage(); return true; } + + var bufferlines = document.getElementById("bufferlines"); + var lines; + var i; + + // Page up -> scroll up + if ($event.type === "keydown" && code === 33 && document.activeElement === inputNode && !$event.ctrlKey && !$event.altKey && !$event.shiftKey) { + lines = bufferlines.querySelectorAll("tr"); + for (i = lines.length - 1; i >= 0; i--) { + if ((lines[i].offsetTop-bufferlines.scrollTop) scroll down + if ($event.type === "keydown" && code === 34 && document.activeElement === inputNode && !$event.ctrlKey && !$event.altKey && !$event.shiftKey) { + lines = bufferlines.querySelectorAll("tr"); + for (i = 0; i < lines.length; i++) { + if ((lines[i].offsetTop-bufferlines.scrollTop)>bufferlines.clientHeight/2) { + lines[i].scrollIntoView(true); + break; + } + } + return true; + } + // Some readline keybindings if ($rootScope.readlineBindings && $event.ctrlKey && !$event.altKey && !$event.shiftKey && document.activeElement === inputNode) { // get current caret position - var caretPos = inputNode.selectionStart; + caretPos = inputNode.selectionStart; // Ctrl-a if (code == 65) { inputNode.setSelectionRange(0, 0); From a80db339f8d2a2b1b712c72805a9ff7d854cd38e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lorenz=20H=C3=BCbschle-Schneider?= Date: Sun, 24 Aug 2014 17:53:55 +0100 Subject: [PATCH 13/57] Use strict Requires turning IrcUtils into an Angular service, because the global variable trick won't work with use strict. Reuse is still easily possible by removing the angular wrapping around it. --- .jshintrc | 9 ++++++--- js/connection.js | 15 +++++++-------- js/filters.js | 9 ++++----- js/glowingbear.js | 7 ++++++- js/handlers.js | 4 ++++ js/inputbar.js | 9 +++++++-- js/irc-utils.js | 42 ++++++++++++++++++++++++++---------------- js/models.js | 7 ++++++- js/plugin-directive.js | 4 ++++ js/plugins.js | 10 +++++++--- js/websockets.js | 15 ++++++++++----- js/weechat.js | 24 +++++++++++++----------- 12 files changed, 100 insertions(+), 55 deletions(-) diff --git a/.jshintrc b/.jshintrc index f096376..37932d6 100644 --- a/.jshintrc +++ b/.jshintrc @@ -1,8 +1,11 @@ { + "browser": true, + "devel": true, "globals": { "angular": false, - "$": false, - "window": false, - "console": false + "weeChat": false, + "_": false, + "Notification": false, + "Favico": false } } diff --git a/js/connection.js b/js/connection.js index 2945d4a..e9cc0e9 100644 --- a/js/connection.js +++ b/js/connection.js @@ -1,3 +1,6 @@ +(function() { +'use strict'; + var weechat = angular.module('weechat'); weechat.factory('connection', @@ -7,7 +10,7 @@ weechat.factory('connection', models, ngWebsockets) { - protocol = new weeChat.Protocol(); + var protocol = new weeChat.Protocol(); // Takes care of the connection and websocket hooks @@ -120,7 +123,7 @@ weechat.factory('connection', * Handles websocket disconnection */ $log.info("Disconnected from relay"); - failCallbacks('disconnection'); + ngWebsockets.failCallbacks('disconnection'); $rootScope.connected = false; $rootScope.$emit('relayDisconnect'); if (ssl && evt.code === 1006) { @@ -142,16 +145,11 @@ weechat.factory('connection', $rootScope.lastError = Date.now(); if (evt.type === "error" && this.readyState !== 1) { - failCallbacks('error'); + ngWebsockets.failCallbacks('error'); $rootScope.errorMessage = true; } }; - protocol.setId = function(id, message) { - return '(' + id + ') ' + message; - }; - - try { ngWebsockets.connect(url, protocol, @@ -278,3 +276,4 @@ weechat.factory('connection', requestNicklist: requestNicklist }; }]); +})(); diff --git a/js/filters.js b/js/filters.js index 418b91c..cecf1fe 100644 --- a/js/filters.js +++ b/js/filters.js @@ -1,8 +1,9 @@ +(function() { +'use strict'; + var weechat = angular.module('weechat'); weechat.filter('toArray', function () { - 'use strict'; - return function (obj) { if (!(obj instanceof Object)) { return obj; @@ -15,7 +16,6 @@ weechat.filter('toArray', function () { }); weechat.filter('irclinky', ['$filter', function($filter) { - 'use strict'; return function(text, target) { if (!text) { return text; @@ -37,8 +37,6 @@ weechat.filter('irclinky', ['$filter', function($filter) { }]); weechat.filter('inlinecolour', ['$sce', function($sce) { - 'use strict'; - return function(text) { if (!text) { return text; @@ -51,3 +49,4 @@ weechat.filter('inlinecolour', ['$sce', function($sce) { return $sce.trustAsHtml(text.replace(hexColourRegex, substitute)); }; }]); +})(); diff --git a/js/glowingbear.js b/js/glowingbear.js index d307890..3e0093c 100644 --- a/js/glowingbear.js +++ b/js/glowingbear.js @@ -1,4 +1,7 @@ -var weechat = angular.module('weechat', ['ngRoute', 'localStorage', 'weechatModels', 'plugins', 'ngSanitize', 'ngWebsockets', 'ngTouch']); +(function() { +'use strict'; + +var weechat = angular.module('weechat', ['ngRoute', 'localStorage', 'weechatModels', 'plugins', 'IrcUtils', 'ngSanitize', 'ngWebsockets', 'ngTouch']); weechat.controller('WeechatCtrl', ['$rootScope', '$scope', '$store', '$timeout', '$log', 'models', 'connection', 'notifications', 'utils', function ($rootScope, $scope, $store, $timeout, $log, models, connection, notifications, utils) { @@ -642,3 +645,5 @@ weechat.config(['$routeProvider', }); } ]); + +})(); diff --git a/js/handlers.js b/js/handlers.js index 4bd7247..cd2e8a7 100644 --- a/js/handlers.js +++ b/js/handlers.js @@ -1,3 +1,6 @@ +(function() { +'use strict'; + var weechat = angular.module('weechat'); weechat.factory('handlers', ['$rootScope', '$log', 'models', 'plugins', 'notifications', function($rootScope, $log, models, plugins, notifications) { @@ -205,3 +208,4 @@ weechat.factory('handlers', ['$rootScope', '$log', 'models', 'plugins', 'notific }; }]); +})(); diff --git a/js/inputbar.js b/js/inputbar.js index a7fa229..1d21a2e 100644 --- a/js/inputbar.js +++ b/js/inputbar.js @@ -1,3 +1,6 @@ +(function() { +'use strict'; + var weechat = angular.module('weechat'); weechat.directive('inputBar', function() { @@ -11,12 +14,13 @@ weechat.directive('inputBar', function() { command: '=command' }, - controller: ['$rootScope', '$scope', '$element', '$log', 'connection', 'models', function($rootScope, + controller: ['$rootScope', '$scope', '$element', '$log', 'connection', 'models', 'IrcUtils', 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) { + models, + IrcUtils) { /* * Returns the input element @@ -309,3 +313,4 @@ weechat.directive('inputBar', function() { }] }; }); +})(); diff --git a/js/irc-utils.js b/js/irc-utils.js index 11e2465..6c596d5 100644 --- a/js/irc-utils.js +++ b/js/irc-utils.js @@ -2,14 +2,19 @@ * Portable utilities for IRC. */ -var IrcUtils = { +(function() { +'use strict'; + +var IrcUtils = angular.module('IrcUtils', []); + +IrcUtils.service('IrcUtils', [function() { /** * Get a new version of a nick list, sorted by last speaker * * @param nickList Original nick list * @return Sorted nick list */ - _ciNickList: function(nickList) { + var _ciNickList = function(nickList) { var newList = _(nickList).sortBy(function(nickObj) { return -nickObj.spokeAt; @@ -17,7 +22,7 @@ var IrcUtils = { newList = _(newList).pluck('name'); return newList; - }, + }; /** * Completes a single nick. @@ -26,7 +31,7 @@ var IrcUtils = { * @param nickList Array of current nicks sorted for case insensitive searching * @return Completed nick (null if not found) */ - _completeSingleNick: function(candidate, nickList) { + var _completeSingleNick = function(candidate, nickList) { var foundNick = null; nickList.some(function(nick) { @@ -39,7 +44,7 @@ var IrcUtils = { }); return foundNick; - }, + }; /** * Get the next nick when iterating nicks. @@ -49,7 +54,7 @@ var IrcUtils = { * @param nickList Array of current nicks sorted for case insensitive searching * @return Next nick (may be the same) */ - _nextNick: function(iterCandidate, currentNick, nickList) { + var _nextNick = function(iterCandidate, currentNick, nickList) { var matchingNicks = []; var at = null; var lcIterCandidate = iterCandidate.toLowerCase(); @@ -63,7 +68,7 @@ var IrcUtils = { if (lcCurrentNick === lcNick) { at = matchingNicks.length - 1; } - } + } /* Since we aren't sorted any more torhve disabled this: else if (matchingNicks.length > 0) { // end of group, no need to check after this @@ -82,7 +87,7 @@ var IrcUtils = { } return matchingNicks[at]; } - }, + }; /** * Nicks tab completion. @@ -98,14 +103,14 @@ var IrcUtils = { * foundNick: completed nick (or null if not possible) * iterCandidate: current iterating candidate */ - completeNick: function(text, caretPos, iterCandidate, nickList, suf) { + var completeNick = function(text, caretPos, iterCandidate, nickList, suf) { var doIterate = (iterCandidate !== null); if (suf === null) { suf = ':'; } // new nick list to search in - var searchNickList = IrcUtils._ciNickList(nickList); + var searchNickList = _ciNickList(nickList); // text before and after caret var beforeCaret = text.substring(0, caretPos); @@ -126,7 +131,7 @@ var IrcUtils = { if (m) { if (doIterate) { // try iterating - newNick = IrcUtils._nextNick(iterCandidate, m[1], searchNickList); + newNick = _nextNick(iterCandidate, m[1], searchNickList); beforeCaret = newNick + suf + ' '; return { text: beforeCaret + afterCaret, @@ -144,7 +149,7 @@ var IrcUtils = { m = beforeCaret.match(/^([a-zA-Z0-9_\\\[\]{}^`|-]+)$/); if (m) { // try completing - newNick = IrcUtils._completeSingleNick(m[1], searchNickList); + newNick = _completeSingleNick(m[1], searchNickList); if (newNick === null) { // no match return ret; @@ -167,7 +172,7 @@ var IrcUtils = { if (m) { if (doIterate) { // try iterating - newNick = IrcUtils._nextNick(iterCandidate, m[2], searchNickList); + newNick = _nextNick(iterCandidate, m[2], searchNickList); beforeCaret = m[1] + newNick + ' '; return { text: beforeCaret + afterCaret, @@ -185,7 +190,7 @@ var IrcUtils = { m = beforeCaret.match(/^(.* )([a-zA-Z0-9_\\\[\]{}^`|-]+)$/); if (m) { // try completing - newNick = IrcUtils._completeSingleNick(m[2], searchNickList); + newNick = _completeSingleNick(m[2], searchNickList); if (newNick === null) { // no match return ret; @@ -205,5 +210,10 @@ var IrcUtils = { // completion not possible return ret; - } -}; + }; + + return { + 'completeNick': completeNick + }; +}]); +})(); diff --git a/js/models.js b/js/models.js index 07b78c3..e8710f6 100644 --- a/js/models.js +++ b/js/models.js @@ -2,6 +2,9 @@ * This file contains the weechat models and various * helper methods to work with them. */ +(function() { +'use strict'; + var models = angular.module('weechatModels', []); models.service('models', ['$rootScope', '$filter', function($rootScope, $filter) { @@ -282,6 +285,7 @@ models.service('models', ['$rootScope', '$filter', function($rootScope, $filter) 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) { @@ -449,7 +453,7 @@ models.service('models', ['$rootScope', '$filter', function($rootScope, $filter) if (key === 'id') { activeBuffer = this.model.buffers[bufferId]; } - else { + else { activeBuffer = _.find(this.model.buffers, function(buffer) { if (buffer[key] === bufferId) { return buffer; @@ -526,3 +530,4 @@ models.service('models', ['$rootScope', '$filter', function($rootScope, $filter) delete(this.model.buffers[bufferId]); }; }]); +})(); diff --git a/js/plugin-directive.js b/js/plugin-directive.js index 62ea463..4a60af4 100644 --- a/js/plugin-directive.js +++ b/js/plugin-directive.js @@ -1,3 +1,6 @@ +(function() { +'use strict'; + var weechat = angular.module('weechat'); weechat.directive('plugin', ['$rootScope', function($rootScope) { @@ -54,3 +57,4 @@ weechat.directive('plugin', ['$rootScope', function($rootScope) { }] }; }]); +})(); diff --git a/js/plugins.js b/js/plugins.js index d5f1bc4..7c749cd 100644 --- a/js/plugins.js +++ b/js/plugins.js @@ -2,7 +2,10 @@ * This file contains the plugin definitions */ -plugins = angular.module('plugins', []); +(function() { +'use strict'; + +var plugins = angular.module('plugins', []); /* * Definition of a user provided plugin with sensible default values @@ -143,7 +146,7 @@ plugins.factory('userPlugins', function() { document.body.appendChild(script); }; - var urlRegexp = RegExp(/(?:ftp|https?):\/\/\S*[^\s.;,(){}<>]/g); + var urlRegexp = new RegExp(/(?:ftp|https?):\/\/\S*[^\s.;,(){}<>]/g); var urlPlugin = function(callback) { return function(message) { @@ -168,7 +171,7 @@ plugins.factory('userPlugins', function() { */ var spotifyPlugin = new Plugin(function(message) { - content = []; + var content = []; var addMatch = function(match) { for (var i = 0; match && i < match.length; i++) { var id = match[i].substr(match[i].length - 22, match[i].length); @@ -394,3 +397,4 @@ plugins.factory('userPlugins', function() { }); +})(); diff --git a/js/websockets.js b/js/websockets.js index 598da14..939b31a 100644 --- a/js/websockets.js +++ b/js/websockets.js @@ -1,3 +1,6 @@ +(function() { +'use strict'; + var websockets = angular.module('ngWebsockets', []); websockets.factory('ngWebsockets', @@ -5,7 +8,7 @@ websockets.factory('ngWebsockets', function($rootScope, $q) { - this.protocol = null; + var protocol = null; var ws = null; var callbacks = {}; @@ -17,7 +20,7 @@ function($rootScope, $q) { * * @param reason reason for failure */ - failCallbacks = function(reason) { + var failCallbacks = function(reason) { for (var i in callbacks) { callbacks[i].cb.reject(reason); } @@ -111,11 +114,11 @@ function($rootScope, $q) { }; var connect = function(url, - protocol, + protocol_, properties) { ws = new WebSocket(url); - protocol = protocol; + protocol = protocol_; for (var property in properties) { ws[property] = properties[property]; } @@ -138,7 +141,9 @@ function($rootScope, $q) { send: send, sendAll: sendAll, connect: connect, - disconnect: disconnect + disconnect: disconnect, + failCallbacks: failCallbacks }; }]); +})(); diff --git a/js/weechat.js b/js/weechat.js index c2698fd..126eda6 100644 --- a/js/weechat.js +++ b/js/weechat.js @@ -1,4 +1,5 @@ (function(exports) {// http://weechat.org/files/doc/devel/weechat_dev.en.html#color_codes_in_strings +'use strict'; /** * WeeChat protocol handling. @@ -604,16 +605,6 @@ return defaults; }; - /** - * Add the ID to the previously formatted command - * - * @param id Command ID - * @param command previously formatted command - */ - WeeChatProtocol.setId = function(id, command) { - return '(' + id + ') ' + command; - }; - /** * Formats a command. * @@ -966,7 +957,7 @@ var objs = []; var hpath = this._getString(); - keys = this._getString().split(','); + var keys = this._getString().split(','); paths = hpath.split('/'); count = this._getInt(); @@ -1179,6 +1170,17 @@ this._data = data; }, + + /** + * Add the ID to the previously formatted command + * + * @param id Command ID + * @param command previously formatted command + */ + setId: function(id, command) { + return '(' + id + ') ' + command; + }, + /** * Parses a WeeChat message. * From c6e1bca78d89181010f57ccbb95116ab1955da05 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lorenz=20H=C3=BCbschle-Schneider?= Date: Sat, 13 Sep 2014 17:39:29 +0100 Subject: [PATCH 14/57] Keep core buffer visible with 'only show unread' --- js/glowingbear.js | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/js/glowingbear.js b/js/glowingbear.js index d307890..831612a 100644 --- a/js/glowingbear.js +++ b/js/glowingbear.js @@ -541,6 +541,10 @@ weechat.controller('WeechatCtrl', ['$rootScope', '$scope', '$store', '$timeout', if (models.getActiveBuffer() === buffer) { return true; } + // Always show core buffer in the list (issue #438) + if (buffer.fullName === "core.weechat") { + return true; + } return buffer.unread > 0 || buffer.notification > 0; } return true; From 121c165a39bb533a1670b0be3e7a2311ca418f73 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lorenz=20H=C3=BCbschle-Schneider?= Date: Sat, 13 Sep 2014 17:58:31 +0100 Subject: [PATCH 15/57] Scroll to correct position when fetching more lines Fixes #406 --- js/connection.js | 27 ++++++++++++--------------- js/glowingbear.js | 10 +++++++--- 2 files changed, 19 insertions(+), 18 deletions(-) diff --git a/js/connection.js b/js/connection.js index 2945d4a..f938783 100644 --- a/js/connection.js +++ b/js/connection.js @@ -234,9 +234,10 @@ weechat.factory('connection', //XXX move to handlers? // delete old lines and add new ones var oldLength = buffer.lines.length; - // Whether to set the readmarker to the middle position - // Don't do that if we didn't get any more lines than we already had - var setReadmarker = (buffer.lastSeen >= 0) && (oldLength !== buffer.lines.length); + // whether we already had all unread lines + var hadAllUnreadLines = buffer.lastSeen >= 0; + + // clear the old lines buffer.lines.length = 0; // We need to set the number of requested lines to 0 here, because parsing a line // increments it. This is needed to also count newly arriving lines while we're @@ -248,23 +249,19 @@ weechat.factory('connection', // Parse the lines handlers.handleLineInfo(lineinfo, true); - if (setReadmarker) { - // Read marker was somewhere in the old lines - we don't need it any more, - // set it to the boundary between old and new. This way, we stay at the exact - // same position in the text through the scrollWithBuffer below - buffer.lastSeen = buffer.lines.length - oldLength - 1; - } else { - // We are currently fetching at least some unread lines, so we need to keep - // the read marker position correct - buffer.lastSeen -= oldLength; - } + // Correct the read marker for the lines that were counted twice + buffer.lastSeen -= oldLength; + // We requested more lines than we got, no more lines. if (linesReceivedCount < numLines) { buffer.allLinesFetched = true; } $rootScope.loadingLines = false; - // Scroll read marker to the center of the screen - $rootScope.scrollWithBuffer(true); + + // Only scroll to read marker if we didn't have all unread lines previously, but have them now + var scrollToReadmarker = !hadAllUnreadLines && buffer.lastSeen >= 0; + // Scroll to correct position + $rootScope.scrollWithBuffer(scrollToReadmarker, true); }); }; diff --git a/js/glowingbear.js b/js/glowingbear.js index d307890..0b2a5b6 100644 --- a/js/glowingbear.js +++ b/js/glowingbear.js @@ -432,7 +432,7 @@ weechat.controller('WeechatCtrl', ['$rootScope', '$scope', '$store', '$timeout', return connection.fetchMoreLines(numLines); }; - $rootScope.scrollWithBuffer = function(nonIncremental) { + $rootScope.scrollWithBuffer = function(scrollToReadmarker, moreLines) { // First, get scrolling status *before* modification // This is required to determine where we were in the buffer pre-change var bl = document.getElementById('bufferlines'); @@ -443,11 +443,15 @@ weechat.controller('WeechatCtrl', ['$rootScope', '$scope', '$store', '$timeout', // Determine if we want to scroll at all // Give the check 3 pixels of slack so you don't have to hit // the exact spot. This fixes a bug in some browsers - if ((nonIncremental && sTop < sVal) || (Math.abs(sTop - sVal) < 3)) { + if (((scrollToReadmarker || moreLines) && sTop < sVal) || (Math.abs(sTop - sVal) < 3)) { var readmarker = document.querySelector(".readmarker"); - if (nonIncremental && readmarker) { + if (scrollToReadmarker && readmarker) { // Switching channels, scroll to read marker bl.scrollTop = readmarker.offsetTop - readmarker.parentElement.scrollHeight + readmarker.scrollHeight; + } else if (moreLines) { + // We fetched more lines but the read marker is still out of view + // Keep the scroll position constant + bl.scrollTop = bl.scrollHeight - bl.clientHeight - sVal; } else { // New message, scroll with buffer (i.e. to bottom) bl.scrollTop = bl.scrollHeight - bl.clientHeight; From 5e9fd27c89f6ed5d1c7178c66a24d79ac9330754 Mon Sep 17 00:00:00 2001 From: Ailin Nemui Date: Wed, 27 Aug 2014 10:53:57 +0200 Subject: [PATCH 16/57] embedded content: check if scrollIntoViewIfNeeded vendor method is available --- js/plugin-directive.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/js/plugin-directive.js b/js/plugin-directive.js index 62ea463..f90c70c 100644 --- a/js/plugin-directive.js +++ b/js/plugin-directive.js @@ -41,7 +41,7 @@ weechat.directive('plugin', ['$rootScope', function($rootScope) { // Scroll embed content into view var scroll = function() { var embed = document.querySelector(".embed_" + $scope.plugin.$$hashKey); - if (embed) { + if (embed && embed.scrollIntoViewIfNeeded !== undefined) { embed.scrollIntoViewIfNeeded(); } }; From 2475ad2c61cb5f835ef6ef3d4d7234ce82cb9dee Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lorenz=20H=C3=BCbschle-Schneider?= Date: Wed, 30 Jul 2014 16:29:55 +0100 Subject: [PATCH 17/57] Bufferlist design attempt --- css/glowingbear.css | 23 +++++++++++++++++++++-- index.html | 4 ++-- js/handlers.js | 4 +++- js/models.js | 3 +++ 4 files changed, 29 insertions(+), 5 deletions(-) diff --git a/css/glowingbear.css b/css/glowingbear.css index ac263eb..00bf860 100644 --- a/css/glowingbear.css +++ b/css/glowingbear.css @@ -204,7 +204,7 @@ input[type=text], input[type=password], #sendMessage, .badge { #sidebar .badge { border-radius: 0; - margin-right: -15px; + margin-right: -10px; } #sidebar ul.indented li.indent span.buffername { @@ -224,7 +224,7 @@ input[type=text], input[type=password], #sendMessage, .badge { overflow-x: hidden; right: 0; top: 0; - padding-top: 35px; + padding-top: 39px; padding-left: 5px; padding-bottom: 35px; z-index: 2; @@ -256,6 +256,7 @@ input[type=text], input[type=password], #sendMessage, .badge { .nav-pills > li > a { border-radius: 0; color: #ddd; + padding: 5px 10px; } .nav-pills > li > a:hover, .nav-pills > li > a:hover span { color: #222; @@ -511,6 +512,24 @@ h2 span, h2 small { display: none; } } +/* bold hash before channels */ +li.buffer.channel a span:last-of-type:before { + color: #888; + content: "#"; + font-weight: bold; +} + +li.buffer.channel.active a span:last-of-type:before { + color: #444; +} + +li.buffer.channel.active a:hover span:last-of-type:before { + color: #222; +} + +li.buffer.indent.private a { + padding-left: 17px; +} .make-thinner { padding-right: -15px; diff --git a/index.html b/index.html index ebcb42a..e5bef28 100644 --- a/index.html +++ b/index.html @@ -233,10 +233,10 @@ $ openssl req -nodes -newkey rsa:4096 -keyout relay.pem -x509 -days 365 -out rel -
  • +
  • - {{ buffer.shortName || buffer.fullName }} + {{ buffer.trimmedName || buffer.fullName }}
  • diff --git a/js/handlers.js b/js/handlers.js index 4bd7247..ee099a7 100644 --- a/js/handlers.js +++ b/js/handlers.js @@ -71,6 +71,7 @@ weechat.factory('handlers', ['$rootScope', '$log', 'models', 'plugins', 'notific var old = models.getBuffer(buffer); old.fullName = obj.full_name; old.shortName = obj.short_name; + old.trimmedName = obj.short_name.replace(/^[#&+]/, ''); }; var handleBufferLocalvarChanged = function(message) { @@ -80,7 +81,8 @@ weechat.factory('handlers', ['$rootScope', '$log', 'models', 'plugins', 'notific var localvars = obj.local_variables; if (old !== undefined && localvars !== undefined) { - // Update indendation status + // Update indentation status + old.type = localvars.type; old.indent = (['channel', 'private'].indexOf(localvars.type) >= 0); } }; diff --git a/js/models.js b/js/models.js index 07b78c3..b01fa58 100644 --- a/js/models.js +++ b/js/models.js @@ -12,6 +12,7 @@ models.service('models', ['$rootScope', '$filter', function($rootScope, $filter) // weechat properties var fullName = message.full_name; var shortName = message.short_name; + var trimmedName = shortName.replace(/^[#&+]/, ''); var title = message.title; var number = message.number; var pointer = message.pointers[0]; @@ -221,6 +222,7 @@ models.service('models', ['$rootScope', '$filter', function($rootScope, $filter) id: pointer, fullName: fullName, shortName: shortName, + trimmedName: trimmedName, number: number, title: title, lines: lines, @@ -238,6 +240,7 @@ models.service('models', ['$rootScope', '$filter', function($rootScope, $filter) getNicklistByTime: getNicklistByTime, serverSortKey: serverSortKey, indent: indent, + type: type, history: history, addToHistory: addToHistory, getHistoryUp: getHistoryUp, From 7fba43c2f9823ef7b11331097b9eab611bf7ec05 Mon Sep 17 00:00:00 2001 From: Ailin Nemui Date: Mon, 15 Sep 2014 15:52:26 +0200 Subject: [PATCH 18/57] [themes] make nicklist width fixed on mobile UI so text doesn't get cut off --- css/glowingbear.css | 1 - 1 file changed, 1 deletion(-) diff --git a/css/glowingbear.css b/css/glowingbear.css index ac263eb..0c43696 100644 --- a/css/glowingbear.css +++ b/css/glowingbear.css @@ -572,7 +572,6 @@ h2 span, h2 small { #nicklist { height: auto; - width: auto; padding: 35px 7px 35px 10px; text-align: center; -webkit-box-shadow: 0px 0px 120px #000; From 081c14176004a456f6917396e8c41358dedbef9d Mon Sep 17 00:00:00 2001 From: Ailin Nemui Date: Sat, 20 Sep 2014 06:02:12 +0200 Subject: [PATCH 19/57] apply-exception in inputbar/bufferFilter.focus --- js/inputbar.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/js/inputbar.js b/js/inputbar.js index f5d19c7..5c82fca 100644 --- a/js/inputbar.js +++ b/js/inputbar.js @@ -243,7 +243,9 @@ weechat.directive('inputBar', function() { if (!$scope.$parent.isSidebarVisible()) { $scope.$parent.showSidebar(); } - document.getElementById('bufferFilter').focus(); + setTimeout(function() { + document.getElementById('bufferFilter').focus(); + }); return true; } From e535099390964032182f6d18e8113679a117d2d1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lorenz=20H=C3=BCbschle-Schneider?= Date: Wed, 24 Sep 2014 21:16:33 +0200 Subject: [PATCH 20/57] update angular to 1.3.0-rc3, underscore to 1.7.0, and favico to 0.3.5 --- 3rdparty/favico-0.3.4-mod.min.js | 7 ------- 3rdparty/favico-0.3.5.min.js | 7 +++++++ index.html | 12 ++++++------ 3 files changed, 13 insertions(+), 13 deletions(-) delete mode 100644 3rdparty/favico-0.3.4-mod.min.js create mode 100644 3rdparty/favico-0.3.5.min.js diff --git a/3rdparty/favico-0.3.4-mod.min.js b/3rdparty/favico-0.3.4-mod.min.js deleted file mode 100644 index 6d5f819..0000000 --- a/3rdparty/favico-0.3.4-mod.min.js +++ /dev/null @@ -1,7 +0,0 @@ -/** - * @license MIT - * @fileOverview Favico animations - * @author Miroslav Magda, http://blog.ejci.net - * @version 0.3.4 - */ -!function(){var e=function(e){"use strict";function t(e){if(e.paused||e.ended||w)return!1;try{f.clearRect(0,0,h,s),f.drawImage(e,0,0,h,s)}catch(o){}p=setTimeout(t,k.duration,e),R.setIcon(c)}function o(e){var t=/^#?([a-f\d])([a-f\d])([a-f\d])$/i;e=e.replace(t,function(e,t,o,n){return t+t+o+o+n+n});var o=/^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(e);return o?{r:parseInt(o[1],16),g:parseInt(o[2],16),b:parseInt(o[3],16)}:!1}function n(e,t){var o,n={};for(o in e)n[o]=e[o];for(o in t)n[o]=t[o];return n}function r(){return document.hidden||document.msHidden||document.webkitHidden||document.mozHidden}e=e?e:{};var i,a,s,h,c,f,l,d,u,y,g,w,m,x,p,b={bgColor:"#d00",textColor:"#fff",fontFamily:"sans-serif",fontStyle:"bold",type:"circle",position:"down",animation:"slide",elementId:!1};m={},m.ff=/firefox/i.test(navigator.userAgent.toLowerCase()),m.chrome=/chrome/i.test(navigator.userAgent.toLowerCase()),m.opera=/opera/i.test(navigator.userAgent.toLowerCase()),m.ie=/msie/i.test(navigator.userAgent.toLowerCase())||/trident/i.test(navigator.userAgent.toLowerCase()),m.supported=m.chrome||m.ff||m.opera;var v=[];g=function(){},d=w=!1;var C=function(){i=n(b,e),i.bgColor=o(i.bgColor),i.textColor=o(i.textColor),i.position=i.position.toLowerCase(),i.animation=k.types[""+i.animation]?i.animation:b.animation;var t=i.position.indexOf("up")>-1,r=i.position.indexOf("left")>-1;if(t||r)for(var d=0;d0?l.height:32,h=l.width>0?l.width:32,c.height=s,c.width=h,f=c.getContext("2d"),M.ready()}):(l.setAttribute("src",""),s=32,h=32,l.height=s,l.width=h,c.height=s,c.width=h,f=c.getContext("2d"),M.ready())}catch(y){throw"Error initializing favico. Message: "+y.message}},M={};M.ready=function(){d=!0,M.reset(),g()},M.reset=function(){v=[],u=!1,f.clearRect(0,0,h,s),f.drawImage(l,0,0,h,s),R.setIcon(c),window.clearTimeout(x),window.clearTimeout(p)},M.start=function(){if(d&&!y){var e=function(){u=v[0],y=!1,v.length>0&&(v.shift(),M.start())};if(v.length>0){y=!0;var t=function(){["type","animation","bgColor","textColor","fontFamily","fontStyle"].forEach(function(e){e in v[0].options&&(i[e]=v[0].options[e])}),k.run(v[0].options,function(){e()},!1)};u?k.run(u.options,function(){t()},!0):t()}}};var I={},A=function(e){return e.n="number"==typeof e.n?Math.abs(0|e.n):e.n,e.x=h*e.x,e.y=s*e.y,e.w=h*e.w,e.h=s*e.h,e.len=(""+e.n).length,e};I.circle=function(e){e=A(e);var t=!1;2===e.len?(e.x=e.x-.4*e.w,e.w=1.4*e.w,t=!0):e.len>=3&&(e.x=e.x-.65*e.w,e.w=1.65*e.w,t=!0),f.clearRect(0,0,h,s),f.drawImage(l,0,0,h,s),f.beginPath(),f.font=i.fontStyle+" "+Math.floor(e.h*(e.n>99?.85:1))+"px "+i.fontFamily,f.textAlign="center",t?(f.moveTo(e.x+e.w/2,e.y),f.lineTo(e.x+e.w-e.h/2,e.y),f.quadraticCurveTo(e.x+e.w,e.y,e.x+e.w,e.y+e.h/2),f.lineTo(e.x+e.w,e.y+e.h-e.h/2),f.quadraticCurveTo(e.x+e.w,e.y+e.h,e.x+e.w-e.h/2,e.y+e.h),f.lineTo(e.x+e.h/2,e.y+e.h),f.quadraticCurveTo(e.x,e.y+e.h,e.x,e.y+e.h-e.h/2),f.lineTo(e.x,e.y+e.h/2),f.quadraticCurveTo(e.x,e.y,e.x+e.h/2,e.y)):f.arc(e.x+e.w/2,e.y+e.h/2,e.h/2,0,2*Math.PI),f.fillStyle="rgba("+i.bgColor.r+","+i.bgColor.g+","+i.bgColor.b+","+e.o+")",f.fill(),f.closePath(),f.beginPath(),f.stroke(),f.fillStyle="rgba("+i.textColor.r+","+i.textColor.g+","+i.textColor.b+","+e.o+")","number"==typeof e.n&&e.n>999?f.fillText((e.n>9999?9:Math.floor(e.n/1e3))+"k+",Math.floor(e.x+e.w/2),Math.floor(e.y+e.h-.2*e.h)):f.fillText(e.n,Math.floor(e.x+e.w/2),Math.floor(e.y+e.h-.15*e.h)),f.closePath()},I.rectangle=function(e){e=A(e);var t=!1;2===e.len?(e.x=e.x-.4*e.w,e.w=1.4*e.w,t=!0):e.len>=3&&(e.x=e.x-.65*e.w,e.w=1.65*e.w,t=!0),f.clearRect(0,0,h,s),f.drawImage(l,0,0,h,s),f.beginPath(),f.font="bold "+Math.floor(e.h*(e.n>99?.9:1))+"px sans-serif",f.textAlign="center",f.fillStyle="rgba("+i.bgColor.r+","+i.bgColor.g+","+i.bgColor.b+","+e.o+")",f.fillRect(e.x,e.y,e.w,e.h),f.fillStyle="rgba("+i.textColor.r+","+i.textColor.g+","+i.textColor.b+","+e.o+")","number"==typeof e.n&&e.len>3?f.fillText((e.n>9999?9:Math.floor(e.n/1e3))+"k+",Math.floor(e.x+e.w/2),Math.floor(e.y+e.h-.2*e.h)):f.fillText(e.n,Math.floor(e.x+e.w/2),Math.floor(e.y+e.h-.15*e.h)),f.closePath()};var E=function(e,t){t=("string"==typeof t?{animation:t}:t)||{},g=function(){try{if("number"==typeof e?e>0:""!==e){var n={type:"badge",options:{n:e}};if("animation"in t&&k.types[""+t.animation]&&(n.options.animation=""+t.animation),"type"in t&&I[""+t.type]&&(n.options.type=""+t.type),["bgColor","textColor"].forEach(function(e){e in t&&(n.options[e]=o(t[e]))}),["fontStyle","fontFamily"].forEach(function(e){e in t&&(n.options[e]=t[e])}),v.push(n),v.length>100)throw"Too many badges requests in queue.";M.start()}else M.reset()}catch(r){throw"Error setting badge. Message: "+r.message}},d&&g()},T=function(e){g=function(){try{var t=e.width,o=e.height,n=document.createElement("img"),r=o/s>t/h?t/h:o/s;n.setAttribute("src",e.getAttribute("src")),n.height=o/r,n.width=t/r,f.clearRect(0,0,h,s),f.drawImage(n,0,0,h,s),R.setIcon(c)}catch(i){throw"Error setting image. Message: "+i.message}},d&&g()},L=function(e){g=function(){try{if("stop"===e)return w=!0,M.reset(),void(w=!1);e.addEventListener("play",function(){t(this)},!1)}catch(o){throw"Error setting video. Message: "+o.message}},d&&g()},U=function(e){if(window.URL&&window.URL.createObjectURL||(window.URL=window.URL||{},window.URL.createObjectURL=function(e){return e}),m.supported){var o=!1;navigator.getUserMedia=navigator.getUserMedia||navigator.oGetUserMedia||navigator.msGetUserMedia||navigator.mozGetUserMedia||navigator.webkitGetUserMedia,g=function(){try{if("stop"===e)return w=!0,M.reset(),void(w=!1);o=document.createElement("video"),o.width=h,o.height=s,navigator.getUserMedia({video:!0,audio:!1},function(e){o.src=URL.createObjectURL(e),o.play(),t(o)},function(){})}catch(n){throw"Error setting webcam. Message: "+n.message}},d&&g()}},R={};R.getIcon=function(){var e=!1,t="",o=function(){for(var e=document.getElementsByTagName("head")[0].getElementsByTagName("link"),t=e.length,o=t-1;o>=0;o--)if(/(^|\s)icon(\s|$)/i.test(e[o].getAttribute("rel")))return e[o];return!1};if(i.elementId?(e=document.getElementById(i.elementId),e.setAttribute("href",e.getAttribute("src"))):(e=o(),e===!1&&(e=document.createElement("link"),e.setAttribute("rel","icon"),document.getElementsByTagName("head")[0].appendChild(e))),t=i.elementId?e.src:e.href,"data:"!==t.substr(0,5)&&-1===t.indexOf(document.location.hostname))throw new Error("Error setting favicon. Favicon image is on different domain (Icon: "+t+", Domain: "+document.location.hostname+")");return e.setAttribute("type","image/png"),e},R.setIcon=function(e){var t=e.toDataURL("image/png");if(i.elementId)document.getElementById(i.elementId).setAttribute("src",t);else if(m.ff||m.opera){var o=a;a=document.createElement("link"),m.opera&&a.setAttribute("rel","icon"),a.setAttribute("rel","icon"),a.setAttribute("type","image/png"),document.getElementsByTagName("head")[0].appendChild(a),a.setAttribute("href",t),o.parentNode&&o.parentNode.removeChild(o)}else a.setAttribute("href",t)};var k={};return k.duration=40,k.types={},k.types.fade=[{x:.4,y:.4,w:.6,h:.6,o:0},{x:.4,y:.4,w:.6,h:.6,o:.1},{x:.4,y:.4,w:.6,h:.6,o:.2},{x:.4,y:.4,w:.6,h:.6,o:.3},{x:.4,y:.4,w:.6,h:.6,o:.4},{x:.4,y:.4,w:.6,h:.6,o:.5},{x:.4,y:.4,w:.6,h:.6,o:.6},{x:.4,y:.4,w:.6,h:.6,o:.7},{x:.4,y:.4,w:.6,h:.6,o:.8},{x:.4,y:.4,w:.6,h:.6,o:.9},{x:.4,y:.4,w:.6,h:.6,o:1}],k.types.none=[{x:.4,y:.4,w:.6,h:.6,o:1}],k.types.pop=[{x:1,y:1,w:0,h:0,o:1},{x:.9,y:.9,w:.1,h:.1,o:1},{x:.8,y:.8,w:.2,h:.2,o:1},{x:.7,y:.7,w:.3,h:.3,o:1},{x:.6,y:.6,w:.4,h:.4,o:1},{x:.5,y:.5,w:.5,h:.5,o:1},{x:.4,y:.4,w:.6,h:.6,o:1}],k.types.popFade=[{x:.75,y:.75,w:0,h:0,o:0},{x:.65,y:.65,w:.1,h:.1,o:.2},{x:.6,y:.6,w:.2,h:.2,o:.4},{x:.55,y:.55,w:.3,h:.3,o:.6},{x:.5,y:.5,w:.4,h:.4,o:.8},{x:.45,y:.45,w:.5,h:.5,o:.9},{x:.4,y:.4,w:.6,h:.6,o:1}],k.types.slide=[{x:.4,y:1,w:.6,h:.6,o:1},{x:.4,y:.9,w:.6,h:.6,o:1},{x:.4,y:.9,w:.6,h:.6,o:1},{x:.4,y:.8,w:.6,h:.6,o:1},{x:.4,y:.7,w:.6,h:.6,o:1},{x:.4,y:.6,w:.6,h:.6,o:1},{x:.4,y:.5,w:.6,h:.6,o:1},{x:.4,y:.4,w:.6,h:.6,o:1}],k.run=function(e,t,o,a){var s=k.types[r()?"none":i.animation];return a=o===!0?"undefined"!=typeof a?a:s.length-1:"undefined"!=typeof a?a:0,t=t?t:function(){},a=0?(I[i.type](n(e,s[a])),x=setTimeout(function(){o?a-=1:a+=1,k.run(e,t,o,a)},k.duration),R.setIcon(c),void 0):void t()},C(),{badge:E,video:L,image:T,webcam:U,reset:M.reset}};"undefined"!=typeof define&&define.amd?define([],function(){return e}):"undefined"!=typeof module&&module.exports?module.exports=e:this.Favico=e}(); \ No newline at end of file diff --git a/3rdparty/favico-0.3.5.min.js b/3rdparty/favico-0.3.5.min.js new file mode 100644 index 0000000..c4630af --- /dev/null +++ b/3rdparty/favico-0.3.5.min.js @@ -0,0 +1,7 @@ +/** + * @license MIT + * @fileOverview Favico animations + * @author Miroslav Magda, http://blog.ejci.net + * @version 0.3.5 + */ +!function(){var e=function(e){"use strict";function t(e){if(e.paused||e.ended||w)return!1;try{l.clearRect(0,0,s,h),l.drawImage(e,0,0,s,h)}catch(o){}p=setTimeout(t,O.duration,e),L.setIcon(c)}function o(e){var t=/^#?([a-f\d])([a-f\d])([a-f\d])$/i;e=e.replace(t,function(e,t,o,n){return t+t+o+o+n+n});var o=/^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(e);return o?{r:parseInt(o[1],16),g:parseInt(o[2],16),b:parseInt(o[3],16)}:!1}function n(e,t){var o,n={};for(o in e)n[o]=e[o];for(o in t)n[o]=t[o];return n}function i(){return document.hidden||document.msHidden||document.webkitHidden||document.mozHidden}e=e?e:{};var r,a,h,s,c,l,f,d,u,y,g,w,m,x,p,b={bgColor:"#d00",textColor:"#fff",fontFamily:"sans-serif",fontStyle:"bold",type:"circle",position:"down",animation:"slide",elementId:!1};m={},m.ff="undefined"!=typeof InstallTrigger,m.chrome=!!window.chrome,m.opera=!!window.opera||navigator.userAgent.indexOf("Opera")>=0,m.ie=/*@cc_on!@*/!1,m.safari=Object.prototype.toString.call(window.HTMLElement).indexOf("Constructor")>0,m.supported=m.chrome||m.ff||m.opera;var v=[];g=function(){},d=w=!1;var C=function(){r=n(b,e),r.bgColor=o(r.bgColor),r.textColor=o(r.textColor),r.position=r.position.toLowerCase(),r.animation=O.types[""+r.animation]?r.animation:b.animation;var t=r.position.indexOf("up")>-1,i=r.position.indexOf("left")>-1;if(t||i)for(var d=0;d0?f.height:32,s=f.width>0?f.width:32,c.height=h,c.width=s,l=c.getContext("2d"),M.ready()}):(f.setAttribute("src",""),h=32,s=32,f.height=h,f.width=s,c.height=h,c.width=s,l=c.getContext("2d"),M.ready())}catch(y){throw"Error initializing favico. Message: "+y.message}},M={};M.ready=function(){d=!0,M.reset(),g()},M.reset=function(){d&&(v=[],u=!1,l.clearRect(0,0,s,h),l.drawImage(f,0,0,s,h),L.setIcon(c),window.clearTimeout(x),window.clearTimeout(p))},M.start=function(){if(d&&!y){var e=function(){u=v[0],y=!1,v.length>0&&(v.shift(),M.start())};if(v.length>0){y=!0;var t=function(){["type","animation","bgColor","textColor","fontFamily","fontStyle"].forEach(function(e){e in v[0].options&&(r[e]=v[0].options[e])}),O.run(v[0].options,function(){e()},!1)};u?O.run(u.options,function(){t()},!0):t()}}};var I={},E=function(e){return e.n="number"==typeof e.n?Math.abs(0|e.n):e.n,e.x=s*e.x,e.y=h*e.y,e.w=s*e.w,e.h=h*e.h,e.len=(""+e.n).length,e};I.circle=function(e){e=E(e);var t=!1;2===e.len?(e.x=e.x-.4*e.w,e.w=1.4*e.w,t=!0):e.len>=3&&(e.x=e.x-.65*e.w,e.w=1.65*e.w,t=!0),l.clearRect(0,0,s,h),l.drawImage(f,0,0,s,h),l.beginPath(),l.font=r.fontStyle+" "+Math.floor(e.h*(e.n>99?.85:1))+"px "+r.fontFamily,l.textAlign="center",t?(l.moveTo(e.x+e.w/2,e.y),l.lineTo(e.x+e.w-e.h/2,e.y),l.quadraticCurveTo(e.x+e.w,e.y,e.x+e.w,e.y+e.h/2),l.lineTo(e.x+e.w,e.y+e.h-e.h/2),l.quadraticCurveTo(e.x+e.w,e.y+e.h,e.x+e.w-e.h/2,e.y+e.h),l.lineTo(e.x+e.h/2,e.y+e.h),l.quadraticCurveTo(e.x,e.y+e.h,e.x,e.y+e.h-e.h/2),l.lineTo(e.x,e.y+e.h/2),l.quadraticCurveTo(e.x,e.y,e.x+e.h/2,e.y)):l.arc(e.x+e.w/2,e.y+e.h/2,e.h/2,0,2*Math.PI),l.fillStyle="rgba("+r.bgColor.r+","+r.bgColor.g+","+r.bgColor.b+","+e.o+")",l.fill(),l.closePath(),l.beginPath(),l.stroke(),l.fillStyle="rgba("+r.textColor.r+","+r.textColor.g+","+r.textColor.b+","+e.o+")","number"==typeof e.n&&e.n>999?l.fillText((e.n>9999?9:Math.floor(e.n/1e3))+"k+",Math.floor(e.x+e.w/2),Math.floor(e.y+e.h-.2*e.h)):l.fillText(e.n,Math.floor(e.x+e.w/2),Math.floor(e.y+e.h-.15*e.h)),l.closePath()},I.rectangle=function(e){e=E(e);var t=!1;2===e.len?(e.x=e.x-.4*e.w,e.w=1.4*e.w,t=!0):e.len>=3&&(e.x=e.x-.65*e.w,e.w=1.65*e.w,t=!0),l.clearRect(0,0,s,h),l.drawImage(f,0,0,s,h),l.beginPath(),l.font=r.fontStyle+" "+Math.floor(e.h*(e.n>99?.9:1))+"px "+r.fontFamily,l.textAlign="center",l.fillStyle="rgba("+r.bgColor.r+","+r.bgColor.g+","+r.bgColor.b+","+e.o+")",l.fillRect(e.x,e.y,e.w,e.h),l.fillStyle="rgba("+r.textColor.r+","+r.textColor.g+","+r.textColor.b+","+e.o+")","number"==typeof e.n&&e.n>999?l.fillText((e.n>9999?9:Math.floor(e.n/1e3))+"k+",Math.floor(e.x+e.w/2),Math.floor(e.y+e.h-.2*e.h)):l.fillText(e.n,Math.floor(e.x+e.w/2),Math.floor(e.y+e.h-.15*e.h)),l.closePath()};var T=function(e,t){t=("string"==typeof t?{animation:t}:t)||{},g=function(){try{if("number"==typeof e?e>0:""!==e){var n={type:"badge",options:{n:e}};if("animation"in t&&O.types[""+t.animation]&&(n.options.animation=""+t.animation),"type"in t&&I[""+t.type]&&(n.options.type=""+t.type),["bgColor","textColor"].forEach(function(e){e in t&&(n.options[e]=o(t[e]))}),["fontStyle","fontFamily"].forEach(function(e){e in t&&(n.options[e]=t[e])}),v.push(n),v.length>100)throw"Too many badges requests in queue.";M.start()}else M.reset()}catch(i){throw"Error setting badge. Message: "+i.message}},d&&g()},A=function(e){g=function(){try{var t=e.width,o=e.height,n=document.createElement("img"),i=o/h>t/s?t/s:o/h;n.setAttribute("src",e.getAttribute("src")),n.height=o/i,n.width=t/i,l.clearRect(0,0,s,h),l.drawImage(n,0,0,s,h),L.setIcon(c)}catch(r){throw"Error setting image. Message: "+r.message}},d&&g()},U=function(e){g=function(){try{if("stop"===e)return w=!0,M.reset(),w=!1,void 0;e.addEventListener("play",function(){t(this)},!1)}catch(o){throw"Error setting video. Message: "+o.message}},d&&g()},R=function(e){if(window.URL&&window.URL.createObjectURL||(window.URL=window.URL||{},window.URL.createObjectURL=function(e){return e}),m.supported){var o=!1;navigator.getUserMedia=navigator.getUserMedia||navigator.oGetUserMedia||navigator.msGetUserMedia||navigator.mozGetUserMedia||navigator.webkitGetUserMedia,g=function(){try{if("stop"===e)return w=!0,M.reset(),w=!1,void 0;o=document.createElement("video"),o.width=s,o.height=h,navigator.getUserMedia({video:!0,audio:!1},function(e){o.src=URL.createObjectURL(e),o.play(),t(o)},function(){})}catch(n){throw"Error setting webcam. Message: "+n.message}},d&&g()}},L={};L.getIcon=function(){var e=!1,t="",o=function(){for(var e=document.getElementsByTagName("head")[0].getElementsByTagName("link"),t=e.length,o=t-1;o>=0;o--)if(/(^|\s)icon(\s|$)/i.test(e[o].getAttribute("rel")))return e[o];return!1};if(r.elementId?(e=document.getElementById(r.elementId),e.setAttribute("href",e.getAttribute("src"))):(e=o(),e===!1&&(e=document.createElement("link"),e.setAttribute("rel","icon"),document.getElementsByTagName("head")[0].appendChild(e))),t=r.elementId?e.src:e.href,"data:"!==t.substr(0,5)&&-1===t.indexOf(document.location.hostname))throw new Error("Error setting favicon. Favicon image is on different domain (Icon: "+t+", Domain: "+document.location.hostname+")");return e.setAttribute("type","image/png"),e},L.setIcon=function(e){var t=e.toDataURL("image/png");if(r.elementId)document.getElementById(r.elementId).setAttribute("src",t);else if(m.ff||m.opera){var o=a;a=document.createElement("link"),m.opera&&a.setAttribute("rel","icon"),a.setAttribute("rel","icon"),a.setAttribute("type","image/png"),document.getElementsByTagName("head")[0].appendChild(a),a.setAttribute("href",t),o.parentNode&&o.parentNode.removeChild(o)}else a.setAttribute("href",t)};var O={};return O.duration=40,O.types={},O.types.fade=[{x:.4,y:.4,w:.6,h:.6,o:0},{x:.4,y:.4,w:.6,h:.6,o:.1},{x:.4,y:.4,w:.6,h:.6,o:.2},{x:.4,y:.4,w:.6,h:.6,o:.3},{x:.4,y:.4,w:.6,h:.6,o:.4},{x:.4,y:.4,w:.6,h:.6,o:.5},{x:.4,y:.4,w:.6,h:.6,o:.6},{x:.4,y:.4,w:.6,h:.6,o:.7},{x:.4,y:.4,w:.6,h:.6,o:.8},{x:.4,y:.4,w:.6,h:.6,o:.9},{x:.4,y:.4,w:.6,h:.6,o:1}],O.types.none=[{x:.4,y:.4,w:.6,h:.6,o:1}],O.types.pop=[{x:1,y:1,w:0,h:0,o:1},{x:.9,y:.9,w:.1,h:.1,o:1},{x:.8,y:.8,w:.2,h:.2,o:1},{x:.7,y:.7,w:.3,h:.3,o:1},{x:.6,y:.6,w:.4,h:.4,o:1},{x:.5,y:.5,w:.5,h:.5,o:1},{x:.4,y:.4,w:.6,h:.6,o:1}],O.types.popFade=[{x:.75,y:.75,w:0,h:0,o:0},{x:.65,y:.65,w:.1,h:.1,o:.2},{x:.6,y:.6,w:.2,h:.2,o:.4},{x:.55,y:.55,w:.3,h:.3,o:.6},{x:.5,y:.5,w:.4,h:.4,o:.8},{x:.45,y:.45,w:.5,h:.5,o:.9},{x:.4,y:.4,w:.6,h:.6,o:1}],O.types.slide=[{x:.4,y:1,w:.6,h:.6,o:1},{x:.4,y:.9,w:.6,h:.6,o:1},{x:.4,y:.9,w:.6,h:.6,o:1},{x:.4,y:.8,w:.6,h:.6,o:1},{x:.4,y:.7,w:.6,h:.6,o:1},{x:.4,y:.6,w:.6,h:.6,o:1},{x:.4,y:.5,w:.6,h:.6,o:1},{x:.4,y:.4,w:.6,h:.6,o:1}],O.run=function(e,t,o,a){var h=O.types[i()?"none":r.animation];return a=o===!0?"undefined"!=typeof a?a:h.length-1:"undefined"!=typeof a?a:0,t=t?t:function(){},a=0?(I[r.type](n(e,h[a])),x=setTimeout(function(){o?a-=1:a+=1,O.run(e,t,o,a)},O.duration),L.setIcon(c),void 0):(t(),void 0)},C(),{badge:T,video:U,image:A,webcam:R,reset:M.reset,browser:{supported:m.supported}}};"undefined"!=typeof define&&define.amd?define([],function(){return e}):"undefined"!=typeof module&&module.exports?module.exports=e:this.Favico=e}(); \ No newline at end of file diff --git a/index.html b/index.html index e5bef28..7ab17d2 100644 --- a/index.html +++ b/index.html @@ -14,11 +14,11 @@ - - - - - + + + + + @@ -34,7 +34,7 @@ - +
    From 565ee169901b120dfab8f5dee81e738ce372a4eb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lorenz=20H=C3=BCbschle-Schneider?= Date: Thu, 25 Sep 2014 17:26:18 +0200 Subject: [PATCH 21/57] Fix linkification of IRC channels Closes #451 --- js/filters.js | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/js/filters.js b/js/filters.js index cecf1fe..188424b 100644 --- a/js/filters.js +++ b/js/filters.js @@ -23,12 +23,12 @@ weechat.filter('irclinky', ['$filter', function($filter) { var linkiedText = $filter('linky')(text, target); - // This regex in no way matches all IRC channel names (they could begin with a +, an &, 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. "#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,.:;?!"'()+@-])(#+[a-z0-9-_]*[a-z][a-z0-9-_]*)/gmi; + // This regex in no way matches all IRC channel names (they could also begin with a + 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. + // "#1" is much more likely to be "number 1" than "IRC channel #1". + // Thus, we only match channels beginning with a # or & and having at least one letter in them. + 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. linkiedText = linkiedText.replace(channelRegex, '$1$2'); From de6ea98c72963204513191e8d79caef633d757d8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lorenz=20H=C3=BCbschle-Schneider?= Date: Thu, 25 Sep 2014 18:00:54 +0200 Subject: [PATCH 22/57] Rerun async plugins after buffer switch TODO: cache result. This is necessary at the moment because currently, they just stop working after a buffer switch --- js/plugin-directive.js | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/js/plugin-directive.js b/js/plugin-directive.js index 7006a3e..d843d6e 100644 --- a/js/plugin-directive.js +++ b/js/plugin-directive.js @@ -33,17 +33,22 @@ weechat.directive('plugin', ['$rootScope', function($rootScope) { * content is shown. */ - // If the plugin is asynchronous / lazy, execute it now and store - // the result. This ensures that the callback is executed only once - if ($scope.plugin.content instanceof Function) { - $scope.plugin.content = $scope.plugin.content(); + var embed = document.querySelector(".embed_" + $scope.plugin.$$hashKey); + + // If the plugin is asynchronous / lazy, execute it now and let it insert itself + // 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(); + } + } else { + $scope.displayedContent = $scope.plugin.content; } - $scope.displayedContent = $scope.plugin.content; $scope.plugin.visible = true; // Scroll embed content into view var scroll = function() { - var embed = document.querySelector(".embed_" + $scope.plugin.$$hashKey); if (embed && embed.scrollIntoViewIfNeeded !== undefined) { embed.scrollIntoViewIfNeeded(); } From 68adfb6cc242b611d6113cda7ab85ed9708979b1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lorenz=20H=C3=BCbschle-Schneider?= Date: Sun, 28 Sep 2014 20:45:29 +0200 Subject: [PATCH 23/57] Hotfix irclinky filter: only #channels &channels match HTML-escaped special chars like " m( --- js/filters.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/js/filters.js b/js/filters.js index 188424b..42335bd 100644 --- a/js/filters.js +++ b/js/filters.js @@ -23,12 +23,12 @@ weechat.filter('irclinky', ['$filter', function($filter) { var linkiedText = $filter('linky')(text, target); - // This regex in no way matches all IRC channel names (they could also begin with a + or an + // 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. // "#1" is much more likely to be "number 1" than "IRC channel #1". - // Thus, we only match channels beginning with a # or & and having at least one letter in them. - var channelRegex = /(^|[\s,.:;?!"'()+@-])((&|#+)[^\x00\x07\r\n\s,:]*[a-z][^\x00\x07\r\n\s,:]*)/gmi; + // 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; // 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. linkiedText = linkiedText.replace(channelRegex, '$1$2'); From bc037720bd7444f1d6798e3b581fb1cf29a0801a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lorenz=20H=C3=BCbschle-Schneider?= Date: Sun, 28 Sep 2014 20:52:53 +0200 Subject: [PATCH 24/57] Fix plugin selectors after angular upgrade The recent angular update changed the format of the hash keys. They're now in the format 'object:123' (etc), which isn't a valid CSS class name any more, but we used the $$hashKey as such. I used this opportunity to introduce a bit of abstraction there as well --- directives/plugin.html | 2 +- js/plugin-directive.js | 9 ++++++++- js/plugins.js | 4 ++-- 3 files changed, 11 insertions(+), 4 deletions(-) diff --git a/directives/plugin.html b/directives/plugin.html index 67cb8fa..9c9afee 100644 --- a/directives/plugin.html +++ b/directives/plugin.html @@ -4,7 +4,7 @@ Hide {{ ::plugin.name }} -
    +
    diff --git a/js/plugin-directive.js b/js/plugin-directive.js index d843d6e..fca573b 100644 --- a/js/plugin-directive.js +++ b/js/plugin-directive.js @@ -21,6 +21,13 @@ weechat.directive('plugin', ['$rootScope', function($rootScope) { $scope.plugin.visible = $rootScope.auto_display_embedded_content; + // user-accessible hash key that is a valid CSS class name + $scope.plugin.className = "embed_" + $scope.plugin.$$hashKey.replace(':','_'); + + $scope.plugin.getElement = function() { + return document.querySelector("." + $scope.plugin.className); + }; + $scope.hideContent = function() { $scope.plugin.visible = false; }; @@ -33,7 +40,7 @@ weechat.directive('plugin', ['$rootScope', function($rootScope) { * content is shown. */ - var embed = document.querySelector(".embed_" + $scope.plugin.$$hashKey); + var embed = $scope.plugin.getElement(); // If the plugin is asynchronous / lazy, execute it now and let it insert itself // TODO store the result between channel switches diff --git a/js/plugins.js b/js/plugins.js index 7c749cd..5aab8fc 100644 --- a/js/plugins.js +++ b/js/plugins.js @@ -348,7 +348,7 @@ plugins.factory('userPlugins', function() { url = match[0] + '.json'; // load gist asynchronously -- return a function here return function() { - var element = document.querySelector('.embed_' + this.$$hashKey); + 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) { @@ -370,7 +370,7 @@ plugins.factory('userPlugins', function() { if (match) { url = 'https://api.twitter.com/1/statuses/oembed.json?id=' + match[2]; return function() { - var element = document.querySelector('.embed_' + this.$$hashKey); + var element = this.getElement(); jsonp(url, function(data) { // sepearate the HTML into content and script tag var scriptIndex = data.html.indexOf(" - - - + + + + From fdc598c00f0e50dd7e17a366754e188b07b5e2c6 Mon Sep 17 00:00:00 2001 From: Kenneth Chung Date: Tue, 14 Oct 2014 22:21:22 -0700 Subject: [PATCH 28/57] fix a typo and some grammar mistakes --- js/plugins.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/js/plugins.js b/js/plugins.js index 5aab8fc..88d9cae 100644 --- a/js/plugins.js +++ b/js/plugins.js @@ -122,10 +122,10 @@ plugins.service('plugins', ['userPlugins', '$sce', function(userPlugins, $sce) { * * To create your own plugin, you need to: * - * 1. Define it's contentForMessage function. The contentForMessage + * 1. Define its contentForMessage function. The contentForMessage * function takes a string as a parameter and returns a HTML string. * - * 2. Instanciate a Plugin object with contentForMessage function as it's + * 2. Instantiate a Plugin object with contentForMessage function as its * argument. * * 3. Add it to the plugins array. From 4fd5ceadb24d880985de2591d7cdb7a1a13c9f60 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lorenz=20H=C3=BCbschle-Schneider?= Date: Thu, 16 Oct 2014 15:50:36 +0200 Subject: [PATCH 29/57] Don't automatically show NSFW content Fixes #472 --- css/glowingbear.css | 6 ++++++ index.html | 2 +- js/plugin-directive.js | 3 ++- 3 files changed, 9 insertions(+), 2 deletions(-) diff --git a/css/glowingbear.css b/css/glowingbear.css index 47bb717..90d3a5a 100644 --- a/css/glowingbear.css +++ b/css/glowingbear.css @@ -535,6 +535,12 @@ li.buffer.indent.private a { padding-right: -15px; } +.settings-help { + display: block; + margin: -5px 0 -3px 19px; + font-size: small; +} + /* */ /* Mobile layout */ /* */ diff --git a/index.html b/index.html index d459bb6..f50e093 100644 --- a/index.html +++ b/index.html @@ -348,7 +348,7 @@ $ openssl req -nodes -newkey rsa:4096 -keyout relay.pem -x509 -days 365 -out rel
    diff --git a/js/plugin-directive.js b/js/plugin-directive.js index fca573b..fa8dabd 100644 --- a/js/plugin-directive.js +++ b/js/plugin-directive.js @@ -19,7 +19,8 @@ weechat.directive('plugin', ['$rootScope', function($rootScope) { $scope.displayedContent = ""; - $scope.plugin.visible = $rootScope.auto_display_embedded_content; + // Auto-display embedded content only if it isn't NSFW + $scope.plugin.visible = $rootScope.auto_display_embedded_content && !$scope.plugin.nsfw; // user-accessible hash key that is a valid CSS class name $scope.plugin.className = "embed_" + $scope.plugin.$$hashKey.replace(':','_'); From 66b48d83fb9326218a2f1ee95cf81ec4b1600b70 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lorenz=20H=C3=BCbschle-Schneider?= Date: Thu, 16 Oct 2014 16:32:53 +0200 Subject: [PATCH 30/57] Fix clicking the bear on mobile closes #469 --- js/glowingbear.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/js/glowingbear.js b/js/glowingbear.js index 6997ad0..ab293df 100644 --- a/js/glowingbear.js +++ b/js/glowingbear.js @@ -273,7 +273,7 @@ weechat.controller('WeechatCtrl', ['$rootScope', '$scope', '$store', '$timeout', $rootScope.auto_display_embedded_content = $scope.noembed === false; $scope.isSidebarVisible = function() { - return document.getElementById('sidebar').getAttribute('sidebar-state') === 'visible'; + return document.getElementById('content').getAttribute('sidebar-state') === 'visible'; }; $scope.showSidebar = function() { From 7fa1b7cc9d7db591aa8638b6b06f1bbf2a2bf5a5 Mon Sep 17 00:00:00 2001 From: David Cormier Date: Sat, 30 Aug 2014 11:21:32 -0400 Subject: [PATCH 31/57] Create dark theme by extracting theme settings from glowingbear.css * Remove style.css as those values should be themable as well. * Update urls in index.html to link to dark theme by default --- css/glowingbear.css | 103 +++---------------- css/{style.css => themes/dark.css} | 157 +++++++++++++++++++++++++++++ index.html | 15 ++- js/glowingbear.js | 3 + 4 files changed, 188 insertions(+), 90 deletions(-) rename css/{style.css => themes/dark.css} (91%) diff --git a/css/glowingbear.css b/css/glowingbear.css index 90d3a5a..7eed294 100644 --- a/css/glowingbear.css +++ b/css/glowingbear.css @@ -2,52 +2,15 @@ html, body { height: 100%; /* The html and body elements cannot have any padding or margin. */ - color: #ddd; - background-color: #181818; } - .no-overflow { overflow: hidden; } -.horizontal-line { - -webkit-box-shadow: rgba(255, 255, 255, 0.07) 0 1px 0; - -moz-box-shadow: rgba(255, 255, 255, 0.07) 0 1px 0; - box-shadow: rgba(255, 255, 255, 0.07) 0 1px 0; - border-bottom: 1px solid #121212; -} -.vertical-line { - -webkit-box-shadow: rgba(255, 255, 255, 0.07) 1px 0 0; - -moz-box-shadow: rgba(255, 255, 255, 0.07) 1px 0 0; - box-shadow: rgba(255, 255, 255, 0.07) 1px 0 0; - border-right: 1px solid #121212; -} -.vertical-line-left { - -webkit-box-shadow: rgba(255, 255, 255, 0.07) -1px 0 0; - -moz-box-shadow: rgba(255, 255, 255, 0.07) -1px 0 0; - box-shadow: rgba(255, 255, 255, 0.07) -1px 0 0; - border-left: 1px solid #121212; -} -.panel-group .panel-heading + .panel-collapse .panel-body, .modal-body, .modal-header, .modal-footer { - -webkit-box-shadow: rgba(255, 255, 255, 0.07) 0 -1px 0; - -moz-box-shadow: rgba(255, 255, 255, 0.07) 0 -1px 0; - box-shadow: rgba(255, 255, 255, 0.07) 0 -1px 0; - border-top: 1px solid #121212; -} a { cursor: pointer; } -.repeated-time { -} -.repeated-time .cof-chat_time, -.repeated-time .cof-chat_time_delimiters { - color: #333; -} -.repeated-time .cob-chat_time, -.repeated-time .cob-chat_time_delimiters { - background-color: transparent; -} td.prefix { text-align: right; vertical-align: top; @@ -72,13 +35,15 @@ td.message { hyphens: auto; } + #readmarker { margin-top: 5px; margin-bottom: 5px; - border-top: 1px solid rgba(255, 255, 255, 0.3); - border-bottom: 1px solid #121212; + border-top: 1px solid; + border-bottom: 1px solid; height: 2px; } + .text { white-space: pre-wrap; } @@ -97,11 +62,9 @@ td.message { input[type=text], input[type=password], #sendMessage, .badge { border: 0; border-radius: 0; - color: #ccc; - box-shadow: 0px 1px 0px rgba(255, 255, 255, 0.1), 0px 1px 7px 0px rgba(0, 0, 0, 0.8) inset; - background: none repeat scroll 0% 0% rgba(0, 0, 0, 0.3); margin-bottom: 5px !important; } + .input-group { width: 100%; } @@ -162,8 +125,8 @@ input[type=text], input[type=password], #sendMessage, .badge { font-size: 30px; position: fixed; right: 0; - background: #282828; } + #topbar .actions > * { padding-left: 5px; } @@ -172,9 +135,6 @@ input[type=text], input[type=password], #sendMessage, .badge { padding-right: 6px; } -#topbar, #sidebar, .panel, .dropdown-menu, .modal-content { - background: #282828; -} #sidebar { position: fixed; width: 140px; @@ -240,16 +200,12 @@ input[type=text], input[type=password], #sendMessage, .badge { #nicklist a { text-decoration: none; } -#nicklist a:hover { - background: #3b3b3b; -} #connection-infos { float: left; max-width: 10%; padding-left: 5px; font-size: 12px; - color: #aaa; overflow: hidden; } @@ -267,6 +223,7 @@ input[type=text], input[type=password], #sendMessage, .badge { background-color: #eee; color: #222; } + .content { height: 100%; min-height: 100%; @@ -289,9 +246,7 @@ input[type=text], input[type=password], #sendMessage, .badge { tr.bufferline { line-height: 100%; } -tr.bufferline:hover { - background-color: #222222; -} + td.time { padding: 1px 5px 1px 1px; vertical-align: top; @@ -324,25 +279,9 @@ td.time { padding-right: 100px; } -.color-light-green { - color: chartreuse; -} - -.color-27 { - color: deepskyblue; -} - -.danger, .alert-danger, .badge .alert-danger { - background-color: rgb(217, 83, 79); - color: #ddd; -} -.alert-danger { - border-color: #121212; - color: black; -} - -li.notification { - color: green; +/* fix for mobile firefox which ignores :hover */ +.nav-pills > li > a:active, .nav-pills > li > a:active span { + text-decoration: none; } [ng-click], @@ -354,12 +293,9 @@ li.notification { width: 10px; height: 10px; } -::-webkit-scrollbar-track-piece { - background-color: black; -} + ::-webkit-scrollbar-thumb:vertical { height: 15px; - background: rgba(255,255,255,0.5); } div.embed * { @@ -428,8 +364,8 @@ table.notimestampseconds td.time span.seconds { height: 100%; width: 100%; overflow: none; - background-color:rgba(0, 0, 0, 0.5) } + .gb-modal .modal-dialog { z-index: 1001; position: absolute; @@ -459,11 +395,12 @@ table.notimestampseconds td.time span.seconds { border-bottom: 0; } -#fontchoice label { +.standard-labels label { font-weight: normal; text-align: left; } + h2 { padding-bottom: 5px; height: 72px; @@ -599,11 +536,8 @@ li.buffer.indent.private a { height: auto; padding: 35px 7px 35px 10px; text-align: center; - -webkit-box-shadow: 0px 0px 120px #000; - box-shadow: 0px 0px 120px #000; position: fixed; margin-top: 10px; - background: #282828; bottom: 0px; } @@ -648,17 +582,10 @@ li.buffer.indent.private a { width: 96%; } - /* a different colour is too irregular on mobile */ - .repeated-time .cof-chat_time, - .repeated-time .cof-chat_time_delimiters { - color: #999; - } - .footer { padding-left: 0px !important; padding-right: 0px !important; width: 100% !important; - background: rgb(24,24,24); } .footer.withnicklist { diff --git a/css/style.css b/css/themes/dark.css similarity index 91% rename from css/style.css rename to css/themes/dark.css index 179587a..6bd53c8 100644 --- a/css/style.css +++ b/css/themes/dark.css @@ -1,3 +1,131 @@ +body { + background-color: #181818; + color: #ddd; +} + +.form-control { + color: #ccc; + box-shadow: 0px 1px 0px rgba(255, 255, 255, 0.1), 0px 1px 7px 0px rgba(0, 0, 0, 0.8) inset; + background: none repeat scroll 0% 0% rgba(0, 0, 0, 0.3); + border: 0px none; +} + +html { + background-color: inherit; +} + +.repeated-time .cob-chat_time, +.repeated-time .cob-chat_time_delimiters { + background-color: transparent; + color: #333; +} + +/* fix for mobile firefox which ignores :hover */ +.nav-pills > li > a:active, .nav-pills > li > a:active span { + background-color: #eee; + color: #222; +} + +tr.bufferline:hover { + background-color: #222222; +} + +.danger, .alert-danger, .badge .alert-danger { + background-color: rgb(217, 83, 79); + color: #ddd; +} +.alert-danger { + border-color: #121212; + color: black; +} + +li.notification { + color: green; +} + +::-webkit-scrollbar-track-piece { + background-color: black; +} +::-webkit-scrollbar-thumb:vertical { + height: 15px; + background: rgba(255,255,255,0.5); +} + +.gb-modal .backdrop { + background-color:rgba(0, 0, 0, 0.5) +} + +input[type=text], input[type=password], #sendMessage, .badge { + color: #ccc; + box-shadow: 0px 1px 0px rgba(255, 255, 255, 0.1), 0px 1px 7px 0px rgba(0, 0, 0, 0.8) inset; + background: none repeat scroll 0% 0% rgba(0, 0, 0, 0.3); +} + +#connection-infos { + color: #aaa; +} + +.nav-pills > li > a { + color: #ddd; +} + +.nav-pills > li > a:hover, .nav-pills > li > a:hover span { + color: #222; +} + +.color-light-green { + color: chartreuse; +} + +.color-27 { + color: deepskyblue; +} + +#topbar .actions { + background: #282828; +} + +#topbar, #sidebar, .panel, .dropdown-menu, .modal-content { + background: #282828; +} + +#nicklist a:hover { + background: #3b3b3b; +} + +.horizontal-line { + -webkit-box-shadow: rgba(255, 255, 255, 0.07) 0 1px 0; + -moz-box-shadow: rgba(255, 255, 255, 0.07) 0 1px 0; + box-shadow: rgba(255, 255, 255, 0.07) 0 1px 0; + border-bottom: 1px solid #121212; +} +.vertical-line { + -webkit-box-shadow: rgba(255, 255, 255, 0.07) 1px 0 0; + -moz-box-shadow: rgba(255, 255, 255, 0.07) 1px 0 0; + box-shadow: rgba(255, 255, 255, 0.07) 1px 0 0; + border-right: 1px solid #121212; +} +.vertical-line-left { + -webkit-box-shadow: rgba(255, 255, 255, 0.07) -1px 0 0; + -moz-box-shadow: rgba(255, 255, 255, 0.07) -1px 0 0; + box-shadow: rgba(255, 255, 255, 0.07) -1px 0 0; + border-left: 1px solid #121212; +} +.panel-group .panel-heading + .panel-collapse .panel-body, .modal-body, .modal-header, .modal-footer { + -webkit-box-shadow: rgba(255, 255, 255, 0.07) 0 -1px 0; + -moz-box-shadow: rgba(255, 255, 255, 0.07) 0 -1px 0; + box-shadow: rgba(255, 255, 255, 0.07) 0 -1px 0; + border-top: 1px solid #121212; +} + +#readmarker { + border-top-color: rgba(255, 255, 255, 0.3); + border-bottom-color: #121212; +} + +/****************************/ +/* Weechat colors and style */ +/****************************/ /* style options, foreground */ .cof-separator { color: #68b5d4; @@ -263,6 +391,9 @@ .cwf-default { color: #d9d9d9; } +.light-theme .cwf-default { + color: #282828; +} .cwf-black { color: #000000; } @@ -1935,3 +2066,29 @@ color: yellow; font-weight: bold; } + +/* */ +/* Mobile layout */ +/* */ +@media (max-width: 968px) { + /* a different colour is too irregular on mobile */ + .repeated-time .cof-chat_time, + .repeated-time .cof-chat_time_delimiters { + color: #999; + } + + #nicklist { + -webkit-box-shadow: 0px 0px 120px #000; + box-shadow: 0px 0px 120px #000; + background: #282828; + } + + .footer { + background: rgb(24,24,24); + } + +} + + + + diff --git a/index.html b/index.html index f50e093..62b3ef3 100644 --- a/index.html +++ b/index.html @@ -12,7 +12,6 @@ - @@ -37,6 +36,7 @@ +

    logo @@ -296,7 +296,7 @@ $ openssl req -nodes -newkey rsa:4096 -keyout relay.pem -x509 -days 365 -out rel

    -
    +
    @@ -270,7 +270,7 @@ $ openssl req -nodes -newkey rsa:4096 -keyout relay.pem -x509 -days 365 -out rel
    + --> diff --git a/js/filters.js b/js/filters.js index bfc2719..2e4d312 100644 --- a/js/filters.js +++ b/js/filters.js @@ -30,8 +30,6 @@ weechat.filter('irclinky', ['$filter', function($filter) { return text; } - var linkiedText = $filter('linky')(text, target); - // 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. @@ -40,8 +38,8 @@ weechat.filter('irclinky', ['$filter', function($filter) { 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. - linkiedText = linkiedText.replace(channelRegex, '$1$2'); - return linkiedText; + var substitute = '$1$2'; + return text.replace(channelRegex, substitute); }; }]); @@ -66,6 +64,9 @@ weechat.filter('DOMfilter', ['$filter', '$sce', function($filter, $sce) { return text; } + // hacky way to pass an extra argument without using .apply, which + // would require assembling an argument array. PERFORMANCE!!! + var extraArgument = (arguments.length > 2) ? arguments[2] : null; var filterFunction = $filter(filter); var el = document.createElement('div'); el.innerHTML = text; @@ -73,7 +74,7 @@ weechat.filter('DOMfilter', ['$filter', '$sce', function($filter, $sce) { // Recursive DOM-walking function applying the filter to the text nodes var process = function(node) { if (node.nodeType === 3) { // text node - var value = filterFunction(node.nodeValue); + var value = filterFunction(node.nodeValue, extraArgument); if (value !== node.nodeValue) { // we changed something. create a new node to replace the current one // we could also only add its children but that would probably incur From 4a41e32fef0f3b2c3e2024d341e384d67ea5d081 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lorenz=20H=C3=BCbschle-Schneider?= Date: Thu, 6 Nov 2014 14:21:40 +0100 Subject: [PATCH 54/57] Ask before sending /quit --- js/inputbar.js | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/js/inputbar.js b/js/inputbar.js index e83919b..e65a327 100644 --- a/js/inputbar.js +++ b/js/inputbar.js @@ -78,6 +78,13 @@ weechat.directive('inputBar', function() { // Split the command into multiple commands based on line breaks _.each($scope.command.split(/\r?\n/), function(line) { + // Ask before a /quit + if (line === '/quit' || line.indexOf('/quit ') === 0) { + if (!window.confirm("Are you sure you want to quit WeeChat? This will prevent you from connecting with Glowing Bear until you restart WeeChat on the command line!")) { + // skip this line + return; + } + } connection.sendMessage(line); }); From 1f601e56c8f5ac75f6f61df0e731cf06a395c1f2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lorenz=20H=C3=BCbschle-Schneider?= Date: Thu, 6 Nov 2014 16:31:31 +0100 Subject: [PATCH 55/57] Fix linkification target The target attribute was passed to the wrong filter --- index.html | 4 ++-- js/filters.js | 7 ++----- 2 files changed, 4 insertions(+), 7 deletions(-) diff --git a/index.html b/index.html index e35094a..012fac2 100644 --- a/index.html +++ b/index.html @@ -215,7 +215,7 @@ $ openssl req -nodes -newkey rsa:4096 -keyout relay.pem -x509 -days 365 -out rel
    -
    +
    @@ -270,7 +270,7 @@ $ openssl req -nodes -newkey rsa:4096 -keyout relay.pem -x509 -days 365 -out rel
    + --> diff --git a/js/filters.js b/js/filters.js index 2e4d312..3ca8297 100644 --- a/js/filters.js +++ b/js/filters.js @@ -25,7 +25,7 @@ weechat.filter('toArray', function () { }); weechat.filter('irclinky', ['$filter', function($filter) { - return function(text, target) { + return function(text) { if (!text) { return text; } @@ -64,9 +64,6 @@ weechat.filter('DOMfilter', ['$filter', '$sce', function($filter, $sce) { return text; } - // hacky way to pass an extra argument without using .apply, which - // would require assembling an argument array. PERFORMANCE!!! - var extraArgument = (arguments.length > 2) ? arguments[2] : null; var filterFunction = $filter(filter); var el = document.createElement('div'); el.innerHTML = text; @@ -74,7 +71,7 @@ weechat.filter('DOMfilter', ['$filter', '$sce', function($filter, $sce) { // Recursive DOM-walking function applying the filter to the text nodes var process = function(node) { if (node.nodeType === 3) { // text node - var value = filterFunction(node.nodeValue, extraArgument); + var value = filterFunction(node.nodeValue); if (value !== node.nodeValue) { // we changed something. create a new node to replace the current one // we could also only add its children but that would probably incur From 640eda868239987e5af5c7f299d603074ae7d193 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lorenz=20H=C3=BCbschle-Schneider?= Date: Sat, 8 Nov 2014 10:30:35 +0100 Subject: [PATCH 56/57] =?UTF-8?q?Update=20AngularJS=201.3.0=20=E2=86=92=20?= =?UTF-8?q?1.3.2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- index.html | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/index.html b/index.html index 012fac2..9e26d30 100644 --- a/index.html +++ b/index.html @@ -13,10 +13,10 @@ - - - - + + + + From ec21b21e38afed03eb496c09e37aaa25524d0dcf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lorenz=20H=C3=BCbschle-Schneider?= Date: Thu, 20 Nov 2014 12:04:21 +0100 Subject: [PATCH 57/57] Release version 0.4.4 skipping 0.4.{2,3} as these were used in the cordova branch independently --- 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 237d267..9c8eb96 100644 --- a/manifest.json +++ b/manifest.json @@ -1,7 +1,7 @@ { "name": "Glowing Bear", "description": "WeeChat Web frontend", - "version": "0.4.1", + "version": "0.4.4", "manifest_version": 2, "icons": { "32": "assets/img/favicon.png", diff --git a/manifest.webapp b/manifest.webapp index 2521b92..965eb2f 100644 --- a/manifest.webapp +++ b/manifest.webapp @@ -16,5 +16,5 @@ "url": "https://github.com/glowing-bear" }, "default_locale": "en", - "version": "0.4.1" + "version": "0.4.4" } diff --git a/package.json b/package.json index 7913fa2..3d509e6 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "glowing-bear", "private": true, - "version": "0.4.1", + "version": "0.4.4", "description": "A web client for Weechat", "repository": "https://github.com/glowing-bear/glowing-bear", "license": "GPLv3",