diff --git a/.travis.yml b/.travis.yml index 1f369fd..65e08bb 100644 --- a/.travis.yml +++ b/.travis.yml @@ -11,3 +11,4 @@ notifications: skip_join: true on_success: never on_failure: always +sudo: false diff --git a/css/glowingbear.css b/css/glowingbear.css index cf54fed..46e1243 100644 --- a/css/glowingbear.css +++ b/css/glowingbear.css @@ -7,10 +7,20 @@ body { overflow: hidden; } +.mobile { + display: none; +} + a { cursor: pointer; } +.hidden-bracket { + position: absolute; + left: -1000px; + overflow: hidden; +} + td.prefix { text-align: right; vertical-align: top; @@ -50,16 +60,25 @@ td.message { #sendMessage { width: 100%; - height: 36px; + height: 35px; resize: none; } + +#sendMessage:focus, #sendMessage:active { + border-bottom: 2px solid #555; +} + +.input-group-addon, .input-group-btn { + vertical-align: top; +} + .footer button { border-radius: 0; } .panel input, .panel .input-group { max-width: 300px; } -input[type=text], input[type=password], #sendMessage, .badge { +input[type=text], input[type=password], #sendMessage { border: 0; border-radius: 0; margin-bottom: 5px !important; @@ -81,10 +100,6 @@ input[type=text], input[type=password], #sendMessage, .badge { .glyphicon { top: 0; /* Fixes alignment issue in top bar */ } -.glyphicon-off { - top: 1px; /* Fixes for relative glyphicon size */ - font-size: 28px; -} #topbar { position: fixed; width: 100%; @@ -97,7 +112,10 @@ input[type=text], input[type=password], #sendMessage, .badge { #topbar .brand { float: left; height: 35px; - padding-left: 5px; +} +#topbar .brand a { + display: inline-block; + padding: 0 10px; } #topbar .brand img { height: 32px; @@ -114,21 +132,25 @@ input[type=text], input[type=password], #sendMessage, .badge { left: 145px; /* sidebar */ overflow: hidden; } + #topbar .actions { margin-left: 5px; padding-left: 5px; margin-right: 0; padding-right: 5px; - padding-top: 2px; height: 35px; line-height: 35px; - font-size: 30px; + font-size: 22px; position: fixed; right: 0; } - #topbar .actions > * { - padding-left: 5px; + padding: 0 5px; + display: inline-block; +} +#topbar .actions .glyphicon { + line-height: 35px; + top: 0; } #topbar .dropdown-menu form { padding-left: 6px; @@ -165,6 +187,7 @@ input[type=text], input[type=password], #sendMessage, .badge { #sidebar .badge { border-radius: 0; margin-right: -10px; + padding: 4px 7px; } #sidebar ul.indented li.indent span.buffername { @@ -209,6 +232,12 @@ input[type=text], input[type=password], #sendMessage, .badge { overflow: hidden; } +.nav-pills li { + min-height: 20px; +} +.nav-pills li+li { + margin-top: 0; +} .nav-pills > li > a { border-radius: 0; color: #ddd; @@ -224,6 +253,13 @@ input[type=text], input[type=password], #sendMessage, .badge { color: #222; } +.nav-pills > li > a { + display: block; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + .content { height: 100%; min-height: 100%; @@ -237,8 +273,9 @@ input[type=text], input[type=password], #sendMessage, .badge { bottom: 35px; /* input bar */ padding-top: 42px; /* topbar */ padding-bottom: 7px; - -webkit-transition:0.35s ease all; - transition:0.35s ease all; + -webkit-transition:0.2 ease-in-out all; + transition:0.2s ease-in-out all; + -webkit-overflow-scrolling: touch; /* Native scroll on ios */ } #bufferlines > table { margin-top: 35px; @@ -263,13 +300,26 @@ td.time { font-family: sans-serif; } +#reconnect { + top: 35px; + position: fixed; + z-index: 9999; + width: 80%; + margin: 0; + padding: 5px; + left: 10%; +} +#reconnect a { + color: white; +} + .footer { position: fixed; bottom: 0; height: 35px; width: 100%; - -webkit-transition:0.35s ease all; - transition:0.35s ease all; + -webkit-transition:0.2s ease-in-out all; + transition:0.2s ease-in-out all; z-index: 1; } .content[sidebar-state=visible] .footer { @@ -332,8 +382,8 @@ table.notimestampseconds td.time span.seconds { } #sidebar .showquickkeys .buffer .buffer-quick-key { - transition: all ease 0.5s; - -webkit-transition: all ease 0.5s; + transition: all ease-in-out 0.5s; + -webkit-transition: all ease-in-out 0.5s; transition-delay: 0.2s; -webkit-transition-delay: 0.2s; opacity: 0.7; @@ -342,8 +392,8 @@ table.notimestampseconds td.time span.seconds { margin-left: -0.7em; margin-right: -0.2em; font-size: smaller; - transition: all ease 0.5s; - -webkit-transition: all ease 0.5s; + transition: all ease-in-out 0.5s; + -webkit-transition: all ease-in-out 0.5s; opacity: 0; text-shadow: -1px 0px 4px rgba(255, 255, 255, 0.4), 0px -1px 4px rgba(255, 255, 255, 0.4), @@ -479,20 +529,20 @@ h2 span, h2 small { } } /* bold hash before channels */ -li.buffer.channel a span:last-of-type:before { +li.buffer.channel a span:last-of-type:before, #topbar .title .channel:before { color: #888; font-weight: bold; } -li.buffer.channel_hash a span:last-of-type:before { +li.buffer.channel_hash a span:last-of-type:before, #topbar .title .channel_hash:before { content: '#'; } -li.buffer.channel_plus a span:last-of-type:before { +li.buffer.channel_plus a span:last-of-type:before, #topbar .title .channel_plus:before { content: '+'; } -li.buffer.channel_ampersand a span:last-of-type:before { +li.buffer.channel_ampersand a span:last-of-type:before, #topbar .title .channel_ampersand:before { content: '&'; } @@ -526,12 +576,52 @@ li.buffer.indent.private a { user-select: none; } -/* Scales emoji to font size */ -img.emoji { - height: 1em; - width: 1em; - margin: 0 .05em 0 .1em; - vertical-align: -0.1em; +.emojione { + font-size: inherit; + height: 1em; + width: 1.1em; + min-height: 16px; + min-width: 16px; + display: inline-block; + margin: -.2ex .15em .2ex; + line-height: normal; + vertical-align: middle; +} +img.emojione { + width: auto; +} + +@media (min-width: 1400px) { + #sidebar[data-state=visible], #sidebarĀ { + width: 200px; + } + .content[sidebar-state="visible"] #bufferlines { + margin-left: 205px; + } + #topbar .title { + left: 205px; + } + .content[sidebar-state=visible] .footer { + padding-left: 200px; + } + + .nav-pills { + font-size: 14px; + } + + .nav-pills li a { + padding: 10px 15px; + } + + #nicklist { + width: 140px; + } + .withnicklist { + margin-right: 140px !important; /* nicklist */ + } + .footer.withnicklist { + padding-right: 148px !important; + } } /* */ @@ -539,6 +629,14 @@ img.emoji { /* */ @media (max-width: 968px) { + .mobile { + display: inherit; + } + + .desktop { + display: none; + } + #bufferlines table { border-collapse: separate; border-spacing: 2px 3px; @@ -549,6 +647,7 @@ img.emoji { bottom: 0px; top: 0px; padding-bottom: 35px; + width: 200px; } #sidebar.in, #sidebar.collapsing { @@ -558,28 +657,37 @@ img.emoji { } #sidebar[data-state=visible] { - width: 200px; + transform: translate(0,0); + -webkit-transform: translate(0,0); /* Safari */ } #sidebar[data-state=hidden] { - left: -200px; + transform: translate(-200px,0); + -webkit-transform: translate(-200px,0); } .content[sidebar-state=visible] #bufferlines, .content[sidebar-state=visible] .footer { margin-left: 0px; + transform: translate(200px,0); + -webkit-transform: translate(200px,0); } #topbar .title { left: 40px; + right: 60px; + text-align: center; + font-size: 18px; } - #topbar .actions { - line-height: 35px; - height: 35px; - font-size: 31px; - margin-right: 0; + #topbar .brand img { + height: 28px; } + #topbar .badge { + display: none; + } + + #bufferlines, #nicklist { position: relative; min-height: 0; @@ -605,7 +713,11 @@ img.emoji { min-height: 0%; } - .nav-pills > li > a { + .nav-pills { + font-size: 14px; + } + + .nav-pills li a { padding: 10px 15px; } diff --git a/css/themes/dark.css b/css/themes/dark.css index 1849b63..9e196f7 100644 --- a/css/themes/dark.css +++ b/css/themes/dark.css @@ -20,8 +20,12 @@ html { color: #333; } +.nav-pills > li.active > a { + background-color: #555; +} + /* fix for mobile firefox which ignores :hover */ -.nav-pills > li > a:active, .nav-pills > li > a:active span { +.nav-pills > li > a:active, .nav-pills > li > a:active span, .nav-pills > li.active > a:hover { background-color: #eee; color: #222; } @@ -39,6 +43,15 @@ tr.bufferline:hover { color: black; } +.btn-default { + background-color: #555; + border-color: #444; +} +.btn-default:hover { + background-color: #666; + border-color: #555; +} + li.notification { color: green; } @@ -56,15 +69,27 @@ li.notification { } 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; +} + +input[type=text], input[type=password], #sendMessage, .badge, .btn-send { + color: #ccc; background: none repeat scroll 0% 0% rgba(0, 0, 0, 0.3); } +.btn-send:hover, .btn-send:focus { + background-color: #555; + color: white; +} + #connection-infos { color: #aaa; } +.nav-pills li:nth-child(2n) { + background: #232323; +} + .nav-pills > li > a { color: #ddd; } @@ -83,6 +108,7 @@ input[type=text], input[type=password], #sendMessage, .badge { #topbar .actions { background: #282828; + color: #666; } #topbar, #sidebar, .panel, .dropdown-menu, .modal-content { diff --git a/css/themes/light.css b/css/themes/light.css index 1c9cfef..b292136 100644 --- a/css/themes/light.css +++ b/css/themes/light.css @@ -22,6 +22,11 @@ html { background-color: #222; } +.btn-send { + background: none repeat scroll 0% 0% rgba(255, 255, 255, 0.3); + color: #428BCA; +} + tr.bufferline:hover { background-color: #efefef; } @@ -61,6 +66,10 @@ select.form-control, select option, input[type=text], input[type=password], #sen color: #aaa; } +.nav-pills li:nth-child(2n) { + background: #e1e1e1; +} + .nav-pills > li > a { color: #222; } diff --git a/directives/input.html b/directives/input.html index e373298..9b23817 100644 --- a/directives/input.html +++ b/directives/input.html @@ -1,9 +1,9 @@
- - +
diff --git a/directives/plugin.html b/directives/plugin.html index f3cd7b3..0ae78eb 100644 --- a/directives/plugin.html +++ b/directives/plugin.html @@ -1,5 +1,5 @@
- @@ -8,7 +8,7 @@
-
+
- + + {{ activeBuffer().trimmedName || activeBuffer().fullName }}
+
-
- - - -
+ + + @@ -262,10 +267,10 @@ $ openssl req -nodes -newkey rsa:4096 -keyout relay.pem -x509 -days 365 -out rel :: -
+ --> @@ -281,6 +286,11 @@ $ openssl req -nodes -newkey rsa:4096 -keyout relay.pem -x509 -days 365 -out rel
+
+

Connection to WeeChat lost

+ + Reconnecting... Click to try to reconnect now +
+
  • +
    +
    + +
    +
    +
  • +
    +
    + +
    diff --git a/js/connection.js b/js/connection.js index 5c2eb06..c6a60c9 100644 --- a/js/connection.js +++ b/js/connection.js @@ -12,9 +12,12 @@ weechat.factory('connection', var protocol = new weeChat.Protocol(); - // Takes care of the connection and websocket hooks + var connectionData = []; + var reconnectTimer; - var connect = function (host, port, passwd, ssl, noCompression) { + // Takes care of the connection and websocket hooks + var connect = function (host, port, passwd, ssl, noCompression, successCallback, failCallback) { + connectionData = [host, port, passwd, ssl, noCompression]; var proto = ssl ? 'wss' : 'ws'; // If host is an IPv6 literal wrap it in brackets if (host.indexOf(":") !== -1) { @@ -59,7 +62,7 @@ weechat.factory('connection', return ngWebsockets.send( weeChat.Protocol.formatHdata({ path: 'buffer:gui_buffers(*)', - keys: ['local_variables,notify,number,full_name,short_name,title'] + keys: ['local_variables,notify,number,full_name,short_name,title,hidden'] }) ); }; @@ -75,25 +78,20 @@ weechat.factory('connection', // a version command. If it fails, it means the we // did not provide the proper password. _initializeConnection(passwd).then( - function() { + function(version) { + handlers.handleVersionInfo(version); // 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); - } - } + handlers.handleBufferInfo(bufinfo); }); _requestHotlist().then(function(hotlist) { handlers.handleHotlistInfo(hotlist); + + if (successCallback) { + successCallback(); + } }); _requestSync(); @@ -123,17 +121,23 @@ weechat.factory('connection', * Handles websocket disconnection */ $log.info("Disconnected from relay"); - ngWebsockets.failCallbacks('disconnection'); - $rootScope.connected = false; - $rootScope.$emit('relayDisconnect'); - if (ssl && evt.code === 1006) { + if ($rootScope.userdisconnect || !$rootScope.waseverconnected) { + handleClose(evt); + $rootScope.userdisconnect = false; + } else { + reconnect(evt); + } + }; + + var handleClose = function (evt) { + if (ssl && evt && 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(); } } - $rootScope.$apply(); }; var onerror = function (evt) { @@ -166,12 +170,81 @@ weechat.factory('connection', $rootScope.errorMessage = true; $rootScope.securityError = true; $rootScope.$emit('relayDisconnect'); + + if (failCallback) { + failCallback(); + } + } + + }; + + var attemptReconnect = function (bufferId, timeout) { + $log.info('Attempting to reconnect...'); + var d = connectionData; + connect(d[0], d[1], d[2], d[3], d[4], function() { + $rootScope.reconnecting = false; + // on success, update active buffer + models.setActiveBuffer(bufferId); + $log.info('Sucessfully reconnected to relay'); + }, function() { + // on failure, schedule another attempt + if (timeout >= 600000) { + // If timeout is ten minutes or more, give up + $log.info('Failed to reconnect, giving up'); + handleClose(); + } else { + $log.info('Failed to reconnect, scheduling next attempt in', timeout/1000, 'seconds'); + // Clear previous timer, if exists + if (reconnectTimer !== undefined) { + clearTimeout(reconnectTimer); + } + reconnectTimer = setTimeout(function() { + // exponential timeout increase + attemptReconnect(bufferId, timeout * 1.5); + }, timeout); + } + }); + }; + + + var reconnect = function (evt) { + if (connectionData.length < 5) { + // something is wrong + $log.error('Cannot reconnect, connection information is missing'); + return; } + // reinitialise everything, clear all buffers + // TODO: this can be further extended in the future by looking + // at the last line in ever buffer and request more buffers from + // WeeChat based on that + models.reinitialize(); + $rootScope.reconnecting = true; + // Have to do this to get the reconnect banner to show + $rootScope.$apply(); + + var bufferId = models.getActiveBuffer().id, + timeout = 3000; // start with a three-second timeout + + reconnectTimer = setTimeout(function() { + attemptReconnect(bufferId, timeout); + }, timeout); }; var disconnect = function() { + $log.info('Disconnecting from relay'); + $rootScope.userdisconnect = true; ngWebsockets.send(weeChat.Protocol.formatQuit()); + // In case the backend doesn't repond we will close from our end + var closeTimer = setTimeout(function() { + ngWebsockets.disconnect(); + // We pretend we are not connected anymore + // The connection can time out on its own + ngWebsockets.failCallbacks('disconnection'); + $rootScope.connected = false; + $rootScope.$emit('relayDisconnect'); + $rootScope.$apply(); + }); }; /* @@ -181,7 +254,7 @@ weechat.factory('connection', */ var sendMessage = function(message) { ngWebsockets.send(weeChat.Protocol.formatInput({ - buffer: models.getActiveBuffer().fullName, + buffer: models.getActiveBufferReference(), data: message })); }; @@ -193,6 +266,20 @@ weechat.factory('connection', })); }; + var sendHotlistClear = function() { + if (models.version[0] >= 1) { + // WeeChat >= 1 supports clearing hotlist with this command + sendMessage('/buffer set hotlist -1'); + // Also move read marker + sendMessage('/input set_unread_current_buffer'); + } else { + // If user wants to sync hotlist with weechat + // we will send a /buffer bufferName command every time + // the user switches a buffer. This will ensure that notifications + // are cleared in the buffer the user switches to + sendCoreCommand('/buffer ' + models.getActiveBuffer().fullName); + } + }; var requestNicklist = function(bufferId, callback) { bufferId = bufferId || null; @@ -269,8 +356,10 @@ weechat.factory('connection', disconnect: disconnect, sendMessage: sendMessage, sendCoreCommand: sendCoreCommand, + sendHotlistClear: sendHotlistClear, fetchMoreLines: fetchMoreLines, - requestNicklist: requestNicklist + requestNicklist: requestNicklist, + attemptReconnect: attemptReconnect }; }]); })(); diff --git a/js/filters.js b/js/filters.js index f43d1ca..71ecee9 100644 --- a/js/filters.js +++ b/js/filters.js @@ -24,7 +24,7 @@ weechat.filter('toArray', function () { }; }); -weechat.filter('irclinky', ['$filter', function($filter) { +weechat.filter('irclinky', function() { return function(text) { if (!text) { return text; @@ -36,12 +36,12 @@ weechat.filter('irclinky', ['$filter', function($filter) { // "#1" is much more likely to be "number 1" than "IRC channel #1". // Thus, we only match channels beginning with a # and having at least one letter in them. var channelRegex = /(^|[\s,.:;?!"'()+@-\~%])(#+[^\x00\x07\r\n\s,:]*[a-z][^\x00\x07\r\n\s,:]*)/gmi; - // This is SUPER nasty, but ng-click does not work inside a filter, as the markup has to be $compiled first, which is not possible in filter afaik. - // Therefore, get the scope, fire the method, and $apply. Yuck. I sincerely hope someone finds a better way of doing this. - var substitute = '$1$2'; + // Call the method we bound to window.openBuffer when we instantiated + // the Weechat controller. + var substitute = '$1$2'; return text.replace(channelRegex, substitute); }; -}]); +}); weechat.filter('inlinecolour', function() { return function(text) { @@ -75,9 +75,11 @@ weechat.filter('DOMfilter', ['$filter', '$sce', function($filter, $sce) { }); }; - // hacky way to pass an extra argument without using .apply, which + // hacky way to pass extra arguments without using .apply, which // would require assembling an argument array. PERFORMANCE!!! var extraArgument = (arguments.length > 2) ? arguments[2] : null; + var thirdArgument = (arguments.length > 3) ? arguments[3] : null; + var filterFunction = $filter(filter); var el = document.createElement('div'); el.innerHTML = text; @@ -89,7 +91,7 @@ weechat.filter('DOMfilter', ['$filter', '$sce', function($filter, $sce) { // it changed the escaped value. This is because setting the result // as innerHTML causes it to be unescaped. var input = escape_html(node.nodeValue); - var value = filterFunction(input, extraArgument); + var value = filterFunction(input, extraArgument, thirdArgument); if (value !== input) { // we changed something. create a new node to replace the current one // we could also only add its children but that would probably incur @@ -145,15 +147,42 @@ weechat.filter('getBufferQuickKeys', function () { }; }); -// Emojifis the string using https://github.com/twitter/twemoji +// Emojifis the string using https://github.com/Ranks/emojione weechat.filter('emojify', function() { return function(text, enable_JS_Emoji) { - if (enable_JS_Emoji === true) { - return twemoji.parse(text); + if (enable_JS_Emoji === true && window.emojione !== undefined) { + return emojione.unicodeToImage(text); } else { return(text); } }; }); +weechat.filter('mathjax', function() { + return function(text, selector, enabled) { + if (!enabled || typeof(MathJax) === "undefined") { + return text; + } + if (text.indexOf("$$") != -1 || text.indexOf("\\[") != -1 || text.indexOf("\\(") != -1) { + // contains math + var math = document.querySelector(selector); + MathJax.Hub.Queue(["Typeset",MathJax.Hub,math]); + } + + return text; + }; +}); + +weechat.filter('prefixlimit', function() { + return function(input, chars) { + if (isNaN(chars)) return input; + if (chars <= 0) return ''; + if (input && input.length > chars) { + input = input.substring(0, chars); + return input + '+'; + } + return input; + }; +}); + })(); diff --git a/js/glowingbear.js b/js/glowingbear.js index edd0724..c0b746a 100644 --- a/js/glowingbear.js +++ b/js/glowingbear.js @@ -15,6 +15,11 @@ weechat.config(['$compileProvider', function ($compileProvider) { weechat.controller('WeechatCtrl', ['$rootScope', '$scope', '$store', '$timeout', '$log', 'models', 'connection', 'notifications', 'utils', 'settings', function ($rootScope, $scope, $store, $timeout, $log, models, connection, notifications, utils, settings) { + window.openBuffer = function(channel) { + $scope.openBuffer(channel); + $scope.$apply(); + }; + $scope.command = ''; $scope.themes = ['dark', 'light']; @@ -26,7 +31,7 @@ weechat.controller('WeechatCtrl', ['$rootScope', '$scope', '$store', '$timeout', 'savepassword': false, 'autoconnect': false, 'nonicklist': utils.isMobileUi(), - 'noembed': utils.isMobileUi(), + 'noembed': true, 'onlyUnread': false, 'hotlistsync': true, 'orderbyserver': true, @@ -36,7 +41,8 @@ weechat.controller('WeechatCtrl', ['$rootScope', '$scope', '$store', '$timeout', 'fontsize': '14px', 'fontfamily': (utils.isMobileUi() ? 'sans-serif' : 'Inconsolata, Consolas, Monaco, Ubuntu Mono, monospace'), 'readlineBindings': false, - 'enableJSEmoji': false + 'enableJSEmoji': (utils.isMobileUi() ? false : true), + 'enableMathjax': false, }); $scope.settings = settings; @@ -208,17 +214,12 @@ weechat.controller('WeechatCtrl', ['$rootScope', '$scope', '$store', '$timeout', ); } notifications.updateTitle(ab); + $scope.notifications = notifications.unreadCount('notification'); + $scope.unread = notifications.unreadCount('unread'); $timeout(function() { $rootScope.scrollWithBuffer(true); }); - // If user wants to sync hotlist with weechat - // we will send a /buffer bufferName command every time - // the user switches a buffer. This will ensure that notifications - // are cleared in the buffer the user switches to - if (settings.hotlistsync && ab.fullName) { - connection.sendCoreCommand('/buffer ' + ab.fullName); - } // Clear search term on buffer change $scope.search = ''; @@ -231,12 +232,21 @@ weechat.controller('WeechatCtrl', ['$rootScope', '$scope', '$store', '$timeout', document.getElementById('sendMessage').focus(); }, 0); } + + // Do this part last since it's not important for the UI + if (settings.hotlistsync && ab.fullName) { + connection.sendHotlistClear(); + } }); $rootScope.favico = new Favico({animation: 'none'}); + $scope.notifications = notifications.unreadCount('notification'); + $scope.unread = notifications.unreadCount('unread'); $rootScope.$on('notificationChanged', function() { notifications.updateTitle(); + $scope.notifications = notifications.unreadCount('notification'); + $scope.unread = notifications.unreadCount('unread'); if (settings.useFavico && $rootScope.favico) { notifications.updateFavico(); @@ -264,6 +274,8 @@ weechat.controller('WeechatCtrl', ['$rootScope', '$scope', '$store', '$timeout', $rootScope.connected = false; $rootScope.waseverconnected = false; + $rootScope.userdisconnect = false; + $rootScope.reconnecting = false; $rootScope.models = models; @@ -375,6 +387,51 @@ weechat.controller('WeechatCtrl', ['$rootScope', '$scope', '$store', '$timeout', } }); + // To prevent unnecessary loading times for users who don't + // want MathJax, load it only if the setting is enabled. + // This also fires when the page is loaded if enabled. + settings.addCallback('enableMathjax', function(enabled) { + if (enabled && !$rootScope.mathjax_init) { + // Load MathJax only once + $rootScope.mathjax_init = true; + (function () { + var head = document.getElementsByTagName("head")[0], script; + script = document.createElement("script"); + script.type = "text/x-mathjax-config"; + script[(window.opera ? "innerHTML" : "text")] = + "MathJax.Hub.Config({\n" + + " tex2jax: { inlineMath: [['$$','$$'], ['\\\\(','\\\\)']], displayMath: [['\\\\[','\\\\]']] },\n" + + "});"; + head.appendChild(script); + script = document.createElement("script"); + script.type = "text/javascript"; + script.src = "//cdn.mathjax.org/mathjax/latest/MathJax.js?config=TeX-AMS_HTML"; + head.appendChild(script); + })(); + } + }); + + + // Inject theme CSS + settings.addCallback('theme', function(theme) { + // Unload old theme + var oldThemeCSS = document.getElementById("themeCSS"); + if (oldThemeCSS) { + oldThemeCSS.parentNode.removeChild(oldThemeCSS); + } + + // Load new theme + (function() { + var elem = document.createElement("link"); + elem.rel = "stylesheet"; + elem.href = "css/themes/" + theme + ".css"; + elem.media = "screen"; + elem.id = "themeCSS"; + document.getElementsByTagName("head")[0].appendChild(elem); + })(); + }); + + // Update font family when changed settings.addCallback('fontfamily', function(fontfamily) { utils.changeClassStyle('favorite-font', 'fontFamily', fontfamily); @@ -390,6 +447,15 @@ weechat.controller('WeechatCtrl', ['$rootScope', '$scope', '$store', '$timeout', if (utils.isMobileUi()) { $scope.hideSidebar(); } + + // Clear the hotlist for this buffer, because presumable you have read + // the messages in this buffer before you switched to the new one + // this is only needed with new type of clearing since in the old + // way WeeChat itself takes care of that part + if (models.version[0] >= 1) { + connection.sendHotlistClear(); + } + return models.setActiveBuffer(bufferId, key); }; @@ -398,9 +464,17 @@ weechat.controller('WeechatCtrl', ['$rootScope', '$scope', '$store', '$timeout', fullName = fullName.substring(0, fullName.lastIndexOf('.') + 1) + bufferName; // substitute the last part if (!$scope.setActiveBuffer(fullName, 'fullName')) { - var command = 'join'; + // WeeChat 0.4.0+ supports /join -noswitch + // As Glowing Bear requires 0.4.2+, we don't need to check the version + var command = 'join -noswitch'; + + // Check if it's a query and we need to use /query instead if (['#', '&', '+', '!'].indexOf(bufferName.charAt(0)) < 0) { // these are the characters a channel name can start with (RFC 2813-2813) command = 'query'; + // WeeChat 1.2+ supports /query -noswitch. See also #577 (different context) + if ((models.version[0] == 1 && models.version[1] >= 2) || models.version[1] > 1) { + command += " -noswitch"; + } } connection.sendMessage('/' + command + ' ' + bufferName); } @@ -513,6 +587,10 @@ weechat.controller('WeechatCtrl', ['$rootScope', '$scope', '$store', '$timeout', $scope.connectbutton = 'Connect'; connection.disconnect(); }; + $scope.reconnect = function() { + var bufferId = models.getActiveBuffer().id; + connection.attemptReconnect(bufferId, 3000); + }; //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() { @@ -581,12 +659,13 @@ weechat.controller('WeechatCtrl', ['$rootScope', '$scope', '$store', '$timeout', return true; } // Always show core buffer in the list (issue #438) - if (buffer.fullName === "core.weechat") { + // Also show server buffers in hierarchical view + if (buffer.fullName === "core.weechat" || (settings.orderbyserver && buffer.type === 'server')) { return true; } - return buffer.unread > 0 || buffer.notification > 0; + return (buffer.unread > 0 || buffer.notification > 0) && !buffer.hidden; } - return true; + return !buffer.hidden; }; // Watch model and update show setting when it changes @@ -681,6 +760,25 @@ weechat.controller('WeechatCtrl', ['$rootScope', '$scope', '$store', '$timeout', } }; + $scope.init = function() { + if (window.location.hash) { + var rawStr = atob(window.location.hash.substring(1)); + window.location.hash = ""; + var spl = rawStr.split(":"); + var host = spl[0]; + var port = parseInt(spl[1]); + var password = spl[2]; + var ssl = spl.length > 3; + notifications.requestNotificationPermission(); + $rootScope.sslError = false; + $rootScope.securityError = false; + $rootScope.errorMessage = false; + $rootScope.bufferBottom = true; + $scope.connectbutton = 'Connecting ...'; + connection.connect(host, port, password, ssl); + } + }; + }]); weechat.config(['$routeProvider', diff --git a/js/handlers.js b/js/handlers.js index 065a4b8..98f088c 100644 --- a/js/handlers.js +++ b/js/handlers.js @@ -5,6 +5,14 @@ var weechat = angular.module('weechat'); weechat.factory('handlers', ['$rootScope', '$log', 'models', 'plugins', 'notifications', function($rootScope, $log, models, plugins, notifications) { + var handleVersionInfo = function(message) { + var content = message.objects[0].content; + var version = content.value; + // Store the WeeChat version in models + // this eats things like 1.3-dev -> [1,3] + models.version = version.split(".").map(function(c) { return parseInt(c); }); + }; + var handleBufferClosing = function(message) { var bufferMessage = message.objects[0].content[0]; var bufferId = bufferMessage.pointers[0]; @@ -43,6 +51,54 @@ weechat.factory('handlers', ['$rootScope', '$log', 'models', 'plugins', 'notific } }; + var handleBufferInfo = function(message) { + var bufferInfos = message.objects[0].content; + // buffers objects + for (var i = 0; i < bufferInfos.length ; i++) { + var bufferId = bufferInfos[i].pointers[0]; + var buffer = models.getBuffer(bufferId); + if (buffer !== undefined) { + // We already know this buffer + handleBufferUpdate(buffer, bufferInfos[i]); + } else { + buffer = new models.Buffer(bufferInfos[i]); + models.addBuffer(buffer); + // Switch to first buffer on startup + if (i === 0) { + models.setActiveBuffer(buffer.id); + } + } + } + }; + + var handleBufferUpdate = function(buffer, message) { + if (message.pointers[0] !== buffer.id) { + // this is information about some other buffer! + return; + } + + // weechat properties -- short name can be changed + buffer.shortName = message.short_name; + buffer.trimmedName = buffer.shortName.replace(/^[#&+]/, ''); + buffer.title = message.title; + buffer.number = message.number; + buffer.hidden = message.hidden; + + // reset these, hotlist info will arrive shortly + buffer.notification = 0; + buffer.unread = 0; + buffer.lastSeen = -1; + + if (message.local_variables.type !== undefined) { + buffer.type = message.local_variables.type; + buffer.indent = (['channel', 'private'].indexOf(buffer.type) >= 0); + } + + if (message.notify !== undefined) { + buffer.notify = message.notify; + } + }; + var handleBufferLineAdded = function(message) { message.objects[0].content.forEach(function(l) { handleLine(l, false); @@ -53,10 +109,6 @@ weechat.factory('handlers', ['$rootScope', '$log', 'models', 'plugins', 'notific 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) { @@ -84,6 +136,29 @@ weechat.factory('handlers', ['$rootScope', '$log', 'models', 'plugins', 'notific // prefix + fullname, which would happen otherwise). Else, use null so that full_name is used old.trimmedName = obj.short_name.replace(/^[#&+]/, '') || (obj.short_name ? ' ' : null); old.prefix = ['#', '&', '+'].indexOf(obj.short_name.charAt(0)) >= 0 ? obj.short_name.charAt(0) : ''; + + // After a buffer openes we get the name change event from relay protocol + // Here we check our outgoing commands that openes a buffer and switch + // to it if we find the buffer name it the list + var position = models.outgoingQueries.indexOf(old.shortName); + if (position >= 0) { + models.outgoingQueries.splice(position, 1); + models.setActiveBuffer(old.id); + } + }; + + var handleBufferHidden = function(message) { + var obj = message.objects[0].content[0]; + var buffer = obj.pointers[0]; + var old = models.getBuffer(buffer); + old.hidden = true; + }; + + var handleBufferUnhidden = function(message) { + var obj = message.objects[0].content[0]; + var buffer = obj.pointers[0]; + var old = models.getBuffer(buffer); + old.hidden = false; }; var handleBufferLocalvarChanged = function(message) { @@ -190,9 +265,12 @@ weechat.factory('handlers', ['$rootScope', '$log', 'models', 'plugins', 'notific _buffer_line_added: handleBufferLineAdded, _buffer_localvar_added: handleBufferLocalvarChanged, _buffer_localvar_removed: handleBufferLocalvarChanged, + _buffer_localvar_changed: handleBufferLocalvarChanged, _buffer_opened: handleBufferOpened, _buffer_title_changed: handleBufferTitleChanged, _buffer_renamed: handleBufferRenamed, + _buffer_hidden: handleBufferHidden, + _buffer_unhidden: handleBufferUnhidden, _nicklist: handleNicklist, _nicklist_diff: handleNicklistDiff }; @@ -212,10 +290,12 @@ weechat.factory('handlers', ['$rootScope', '$log', 'models', 'plugins', 'notific }; return { + handleVersionInfo: handleVersionInfo, handleEvent: handleEvent, handleLineInfo: handleLineInfo, handleHotlistInfo: handleHotlistInfo, - handleNicklist: handleNicklist + handleNicklist: handleNicklist, + handleBufferInfo: handleBufferInfo }; }]); diff --git a/js/inputbar.js b/js/inputbar.js index ab4394a..e57144a 100644 --- a/js/inputbar.js +++ b/js/inputbar.js @@ -23,6 +23,11 @@ weechat.directive('inputBar', function() { IrcUtils, settings) { + // E.g. Turn :smile: into the unicode equivalent + $scope.inputChanged = function() { + $scope.command = emojione.shortnameToUnicode($scope.command); + }; + /* * Returns the input element */ @@ -95,10 +100,28 @@ weechat.directive('inputBar', function() { ab.clear(); } + // Check against a list of commands that opens a new + // buffer and save the name of the buffer so we can + // also automatically switch to the new buffer in gb + var opencommands = ['/query', '/join', '/j', '/q']; + var spacepos = $scope.command.indexOf(' '); + var firstword = $scope.command.substr(0, spacepos); + var index = opencommands.indexOf(firstword); + if (index >= 0) { + var queryName = $scope.command.substring(spacepos + 1); + // Cache our queries so when a buffer gets opened we can open in UI + models.outgoingQueries.push(queryName); + } + // Empty the input after it's sent $scope.command = ''; } + // New style clearing requires this, old does not + if (models.version[0] >= 1) { + connection.sendHotlistClear(); + } + $scope.getInputNode().focus(); }; @@ -156,6 +179,15 @@ weechat.directive('inputBar', function() { // Support different browser quirks var code = $event.keyCode ? $event.keyCode : $event.charCode; + // Safari doesn't implement DOM 3 input events yet as of 8.0.6 + var altg = $event.getModifierState ? $event.getModifierState('AltGraph') : false; + + // Mac OSX behaves differntly for altgr, so we check for that + if (altg) { + // We don't handle any anything with altgr + return false; + } + // reset quick keys display $rootScope.showQuickKeys = false; diff --git a/js/irc-utils.js b/js/irc-utils.js index 6c596d5..a6deef0 100644 --- a/js/irc-utils.js +++ b/js/irc-utils.js @@ -8,6 +8,15 @@ var IrcUtils = angular.module('IrcUtils', []); IrcUtils.service('IrcUtils', [function() { + /** + * Escape a string for usage in a larger regexp + * @param str String to escape + * @return Escaped string + */ + var escapeRegExp = function(str) { + return str.replace(/[\-\[\]\/\{\}\(\)\*\+\?\.\\\^\$\|]/g, "\\$&"); + }; + /** * Get a new version of a nick list, sorted by last speaker * @@ -63,7 +72,7 @@ IrcUtils.service('IrcUtils', [function() { // collect matching nicks for (var i = 0; i < nickList.length; ++i) { var lcNick = nickList[i].toLowerCase(); - if (lcNick.search(lcIterCandidate) === 0) { + if (lcNick.search(escapeRegExp(lcIterCandidate)) === 0) { matchingNicks.push(nickList[i]); if (lcCurrentNick === lcNick) { at = matchingNicks.length - 1; @@ -149,7 +158,7 @@ IrcUtils.service('IrcUtils', [function() { m = beforeCaret.match(/^([a-zA-Z0-9_\\\[\]{}^`|-]+)$/); if (m) { // try completing - newNick = _completeSingleNick(m[1], searchNickList); + newNick = _completeSingleNick(escapeRegExp(m[1]), searchNickList); if (newNick === null) { // no match return ret; diff --git a/js/models.js b/js/models.js index 6ee7295..baa1aec 100644 --- a/js/models.js +++ b/js/models.js @@ -8,6 +8,12 @@ var models = angular.module('weechatModels', []); models.service('models', ['$rootScope', '$filter', function($rootScope, $filter) { + // WeeChat version + this.version = null; + + // Save outgoing queries + this.outgoingQueries = []; + var parseRichText = function(text) { var textElements = weeChat.Protocol.rawText2Rich(text), typeToClassPrefixFg = { @@ -57,6 +63,7 @@ models.service('models', ['$rootScope', '$filter', function($rootScope, $filter) // weechat properties var fullName = message.full_name; var shortName = message.short_name; + var hidden = message.hidden; // If it's a channel, trim away the prefix (#, &, or +). If that is empty and the buffer // has a short name, use a space (because the prefix will be displayed separately, and we don't want // prefix + fullname, which would happen otherwise). Else, use null so that full_name is used @@ -134,6 +141,12 @@ models.service('models', ['$rootScope', '$filter', function($rootScope, $filter) */ var updateNick = function(group, nick) { group = nicklist[group]; + if (group === undefined) { + // We are getting nicklist events for a buffer where not yet + // have populated the nicklist, so there will be nothing to + // update. Just ignore the event. + return; + } for(var i in group.nicks) { if (group.nicks[i].name === nick.name) { group.nicks[i] = nick; @@ -277,6 +290,7 @@ models.service('models', ['$rootScope', '$filter', function($rootScope, $filter) id: pointer, fullName: fullName, shortName: shortName, + hidden: hidden, trimmedName: trimmedName, prefix: prefix, number: number, @@ -442,6 +456,22 @@ models.service('models', ['$rootScope', '$filter', function($rootScope, $filter) return activeBuffer; }; + /* + * Returns a reference to the currently active buffer that + * WeeChat understands without crashing, even if it's invalid + * + * @return active buffer pointer (WeeChat 1.0+) or fullname (older versions) + */ + this.getActiveBufferReference = function() { + if (this.version !== null && this.version[0] >= 1) { + // pointers are being validated, they're more reliable than + // fullName (e.g. if fullName contains spaces) + return "0x"+activeBuffer.id; + } else { + return activeBuffer.fullName; + } + }; + /* * Returns the previous current active buffer * diff --git a/js/notifications.js b/js/notifications.js index 739a651..2caca39 100644 --- a/js/notifications.js +++ b/js/notifications.js @@ -157,5 +157,6 @@ weechat.factory('notifications', ['$rootScope', '$log', 'models', 'settings', fu updateFavico: updateFavico, createHighlight: createHighlight, cancelAll: cancelAll, + unreadCount: unreadCount }; }]); diff --git a/js/settings.js b/js/settings.js index b153d1a..4e19ec9 100644 --- a/js/settings.js +++ b/js/settings.js @@ -6,6 +6,13 @@ var weechat = angular.module('weechat'); weechat.factory('settings', ['$store', '$rootScope', function($store, $rootScope) { var that = this; this.callbacks = {}; + // This cache is important for two reasons. One, angular hits it up really often + // (because it needs to check for changes and it's not very clever about it). + // Two, it prevents weird type conversion issues that otherwise arise in + // $store.parseValue (e.g. converting "123." to the number 123 even though it + // actually was the beginning of an IP address that the user was in the + // process of entering) + this.cache = {}; // Define a property for a setting, retrieving it on read // and writing it to localStorage on write @@ -14,9 +21,13 @@ weechat.factory('settings', ['$store', '$rootScope', function($store, $rootScope enumerable: true, key: key, get: function() { - return $store.get(key); + if (!(key in this.cache)) { + this.cache[key] = $store.get(key); + } + return this.cache[key]; }, set: function(newVal) { + this.cache[key] = newVal; $store.set(key, newVal); // Call any callbacks var callbacks = that.callbacks[key]; diff --git a/js/websockets.js b/js/websockets.js index 939b31a..f7c632b 100644 --- a/js/websockets.js +++ b/js/websockets.js @@ -109,8 +109,9 @@ function($rootScope, $q) { // otherwise emit it $rootScope.$emit('onMessage', message); } - + // Make sure all UI is updated with new data $rootScope.$apply(); + }; var connect = function(url, diff --git a/manifest.json b/manifest.json index 13e0edb..ebb6d24 100644 --- a/manifest.json +++ b/manifest.json @@ -1,7 +1,7 @@ { "name": "Glowing Bear", "description": "WeeChat Web frontend", - "version": "0.4.8", + "version": "0.5.0", "manifest_version": 2, "icons": { "32": "assets/img/favicon.png", diff --git a/manifest.webapp b/manifest.webapp index c367612..be1774c 100644 --- a/manifest.webapp +++ b/manifest.webapp @@ -25,5 +25,5 @@ "desktop-notification":{} }, "default_locale": "en", - "version": "0.4.8" + "version": "0.5.0" } diff --git a/package.json b/package.json index 711af47..32f913d 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "glowing-bear", "private": true, - "version": "0.4.8", + "version": "0.5.0", "description": "A web client for Weechat", "repository": "https://github.com/glowing-bear/glowing-bear", "license": "GPLv3", diff --git a/test/unit/filters.js b/test/unit/filters.js index c8ea29e..ca1228e 100644 --- a/test/unit/filters.js +++ b/test/unit/filters.js @@ -16,11 +16,11 @@ describe('Filters', function() { })); it('should linkify IRC channels', inject(function(irclinkyFilter) { - expect(irclinkyFilter('#foo')).toEqual('#foo'); + expect(irclinkyFilter('#foo')).toEqual('#foo'); })); it('should not mess up IRC channels surrounded by HTML entities', inject(function(irclinkyFilter) { - expect(irclinkyFilter('<"#foo">')).toEqual('<"\'); $scope.$apply();">#foo">'); + expect(irclinkyFilter('<"#foo">')).toEqual('<"\');">#foo">'); })); });