diff --git a/css/glowingbear.css b/css/glowingbear.css index ca11870..62a10fb 100644 --- a/css/glowingbear.css +++ b/css/glowingbear.css @@ -146,7 +146,7 @@ input[type=text], input[type=password], #sendMessage { .col-sm-9 { padding-right: 5px !important; } -.glyphicon { +#topbar .glyphicon { top: 0; /* Fixes alignment issue in top bar */ } #topbar { @@ -263,6 +263,11 @@ input[type=text], input[type=password], #sendMessage { #sidebar.ng-hide { width: 0; } + +#sidebar[data-state=hidden] { + transform: translate(-200px,0); + -webkit-transform: translate(-200px,0); +} #nicklist { position: fixed; @@ -400,6 +405,7 @@ td.time { margin-left: 0; padding-left: 145px; } + .footer.withnicklist { padding-right: 100px; } @@ -610,7 +616,6 @@ h2 span, h2 small { .panel[data-state=active] .panel-collapse { transition: max-height 0.5s; - max-height: 60em; height: auto; display: block; } @@ -676,6 +681,10 @@ li.buffer.indent.private a { font-size: small; } +.lessleftpad { + padding-left: 5px; +} + .unselectable { -webkit-user-select: none; -moz-user-select: none; @@ -699,6 +708,30 @@ img.emojione { width: auto; } +#toast { + position: fixed; + left: 50%; + bottom: 50px; + width: 250px; + margin-left: -125px; + + text-align: center; + border-radius: 3px; + padding: 10px 15px; + z-index: 100; + animation: fadein 0.5s, fadeout 0.5s 4.5s; +} + +@keyframes fadein { + from { bottom: 0; opacity: 0; } + to { bottom: 50px; opacity: 1; } +} + +@keyframes fadeout { + from { bottom: 50px; opacity: 1; } + to { bottom: 0; opacity: 0; } +} + .glyphicon-spin { -webkit-animation: spin 1000ms infinite linear; animation: spin 1000ms infinite linear; @@ -794,11 +827,6 @@ img.emojione { -webkit-transform: translate(0,0); /* Safari */ } - #sidebar[data-state=hidden] { - 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); @@ -811,17 +839,17 @@ img.emojione { text-align: center; font-size: 18px; width: initial; + z-index: -1; } - #topbar .brand img { - height: 28px; + #topbar .brand a { + padding: 0 2px 0 10px; } - #topbar .badge { - display: none; + #topbar .brand img { + height: 28px; } - #bufferlines, #nicklist { position: relative; min-height: 0; @@ -931,4 +959,4 @@ code { padding: 0px 2px; color: #444; border: 1pt solid #444; -} \ No newline at end of file +} diff --git a/css/themes/base16-default.css b/css/themes/base16-default.css index ec40fcb..2324240 100644 --- a/css/themes/base16-default.css +++ b/css/themes/base16-default.css @@ -65,6 +65,10 @@ a:visited:hover, a:visited:active, a:visited:focus { border: 0px none; } +.form-control[disabled] { + background: var(--base03); +} + .form-control:focus { color: var(--base06); box-shadow: 0px 1px 0px rgba(255, 255, 255, 0.1), 0px 1px 7px 0px rgba(0, 0, 0, 0.2) inset; @@ -301,6 +305,9 @@ tr.bufferline:hover { input[type=text], input[type=password], #sendMessage, .badge { box-shadow: 0px 1px 0px rgba(255, 255, 255, 0.1), 0px 1px 7px 0px rgba(0, 0, 0, 0.2) inset; } +input[type=text].is-invalid{ + box-shadow: 0px 1px 0px rgba(255, 255, 255, 0.1), 0px 1px 7px 0px rgba(255, 0, 0, 0.6) inset; +} input[type=text], input[type=password], #sendMessage, .btn-send, .btn-send-image, .btn-complete-nick { color: var(--base05); @@ -416,6 +423,10 @@ button.close:hover { color: var(--base01); } +#toast { + background-color: var(--base01); +} + /****************************/ /* Weechat colors and style */ /****************************/ diff --git a/css/themes/blue.css b/css/themes/blue.css index c093a43..cb666bc 100644 --- a/css/themes/blue.css +++ b/css/themes/blue.css @@ -134,6 +134,13 @@ input[type=text], input[type=password], #sendMessage, .badge, .btn-send, .btn-se border: 1px solid #363943; } +#toast { + background-color: #283244; + border: 1px solid; + border-color: rgb(29, 94, 152); + border-radius: 0px; +} + .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; diff --git a/css/themes/dark.css b/css/themes/dark.css index da260c8..4d845fe 100644 --- a/css/themes/dark.css +++ b/css/themes/dark.css @@ -10,6 +10,10 @@ body { border: 0px none; } +.form-control[disabled] { + background: none repeat scroll 0% 0% rgba(63, 63, 63, 0.3); +} + .form-control option { color: #eee; background: #282828; @@ -86,6 +90,9 @@ input[type=text], input[type=password], #sendMessage, .badge, .btn-send, .btn-se color: #ccc; background: none repeat scroll 0% 0% rgba(0, 0, 0, 0.3); } +input[type=text].is-invalid{ + box-shadow: 0px 1px 0px rgba(255, 255, 255, 0.1), 0px 1px 7px 0px rgba(255, 0, 0, 0.8) inset; +} .btn-complete-nick:hover, .btn-complete-nick:focus, .btn-send:hover, .btn-send:focus, @@ -2119,6 +2126,10 @@ code { color: #fff; } +#toast { + background-color: #333; +} + /* */ /* Mobile layout */ /* */ diff --git a/css/themes/light.css b/css/themes/light.css index be8012e..900404d 100644 --- a/css/themes/light.css +++ b/css/themes/light.css @@ -69,6 +69,14 @@ select.form-control, select option, input[type=text], input[type=password], #sen background: none repeat scroll 0% 0% rgba(255, 255, 255, 0.3); } +.form-control[disabled] { + background: none repeat scroll 0% 0% rgba(134, 134, 134, 0.3); +} + +input[type=text].is-invalid{ + box-shadow: 0px 1px 0px rgba(0, 0, 0, 0.1), 0px 1px 7px 0px rgba(255, 0, 0, 0.8) inset; +} + #connection-infos { color: #aaa; } @@ -2084,6 +2092,10 @@ select.form-control, select option, input[type=text], input[type=password], #sen font-weight: bold; } +#toast { + background-color: #ddd; +} + /* */ /* Mobile layout */ /* */ diff --git a/electron-main.js b/electron-main.js old mode 100755 new mode 100644 index 36ef9dc..6b62abf --- a/electron-main.js +++ b/electron-main.js @@ -1,258 +1,122 @@ -(function() { - 'use strict'; - const electron = require('electron'); - const app = electron.app; // Module to control application life. - const BrowserWindow = electron.BrowserWindow; // Module to create native browser window. +// Modules to control application life and create native browser window +const {app, BrowserWindow, shell, ipcMain} = require('electron') +const path = require('path') +const fs = require('fs') - const ipcMain = require('electron').ipcMain; - const nativeImage = require('electron').nativeImage; - const Menu = require('electron').Menu; - // Node fs module - const fs = require("fs"); - var template; +// Keep a global reference of the window object, if you don't, the window will +// be closed automatically when the JavaScript object is garbage collected. +let mainWindow - template = [ - { - label: 'Edit', - submenu: [ - { - label: 'Undo', - accelerator: 'CmdOrCtrl+Z', - role: 'undo' - }, - { - label: 'Redo', - accelerator: 'Shift+CmdOrCtrl+Z', - role: 'redo' - }, - { - type: 'separator' - }, - { - label: 'Cut', - accelerator: 'CmdOrCtrl+X', - role: 'cut' - }, - { - label: 'Copy', - accelerator: 'CmdOrCtrl+C', - role: 'copy' - }, - { - label: 'Paste', - accelerator: 'CmdOrCtrl+V', - role: 'paste' - }, - { - label: 'Select All', - accelerator: 'CmdOrCtrl+A', - role: 'selectall' - }, - ] - }, - { - label: 'View', - submenu: [ - { - label: 'Reload', - accelerator: 'CmdOrCtrl+R', - click: function(item, focusedWindow) { - if (focusedWindow) - focusedWindow.reload(); - } - }, - { - label: 'Toggle Full Screen', - accelerator: (function() { - if (process.platform == 'darwin') - return 'Ctrl+Command+F'; - else - return 'F11'; - })(), - click: function(item, focusedWindow) { - if (focusedWindow) - focusedWindow.setFullScreen(!focusedWindow.isFullScreen()); - } - }, - { - label: 'Electron Developer Tools', - accelerator: (function() { - if (process.platform == 'darwin') - return 'Alt+Command+E'; - else - return 'Ctrl+Shift+E'; - })(), - click: function(item, focusedWindow) { - if (focusedWindow) - focusedWindow.toggleDevTools(); - } - }, - { - label: 'Web Developer Tools', - accelerator: (function() { - if (process.platform == 'darwin') - return 'Alt+Command+I'; - else - return 'Ctrl+Shift+I'; - })(), - click: function(item, focusedWindow) { - if ( focusedWindow ) { - focusedWindow.webContents.send( 'openDevTools' ); - } - } - } - ] - }, - { - label: 'Window', - role: 'window', - submenu: [ - { - label: 'Minimize', - accelerator: 'CmdOrCtrl+M', - role: 'minimize' - }, - { - label: 'Close', - accelerator: 'CmdOrCtrl+Q', - role: 'close' - }, - ] - }, - { - label: 'Help', - role: 'help', - submenu: [ - { - label: 'Learn More', - click: function() { require('electron').shell.openExternal('https://github.com/glowing-bear/glowing-bear'); } - }, - ] - }, - ]; +// We use this to store some tiny amount of preferences specific to electron +// things like window bounds and location +const initPath = "init.json" - if (process.platform == 'darwin') { - var name = app.getName(); - template.unshift({ - label: name, - submenu: [ - { - label: 'About ' + name, - role: 'about' - }, - { - type: 'separator' - }, - { - label: 'Services', - role: 'services', - submenu: [] - }, - { - type: 'separator' - }, - { - label: 'Hide ' + name, - accelerator: 'Command+H', - role: 'hide' - }, - { - label: 'Hide Others', - accelerator: 'Command+Alt+H', - role: 'hideothers' - }, - { - label: 'Show All', - role: 'unhide' - }, - { - type: 'separator' - }, - { - label: 'Quit', - accelerator: 'Command+Q', - click: function() { app.quit(); } - }, - ] - }); - // Window menu. - template[3].submenu.push( - { - type: 'separator' - }, - { - label: 'Bring All to Front', - role: 'front' - } - ); +function createWindow () { + let data + // read saved state from file (e.g. window bounds) + try { + data = JSON.parse(fs.readFileSync(initPath, 'utf8')) + } + catch(e) { + console.log('Unable to read init.json: ', e) + } + // Create the browser window. + const bounds = (data && data.bounds) ? data.bounds : {width: 1280, height:800 } + mainWindow = new BrowserWindow({ + width: bounds.width, + height: bounds.height, + webPreferences: { + preload: path.join(__dirname, 'electron-globals.js') } + }) - // Keep a global reference of the window object, if you don't, the window will - // be closed automatically when the JavaScript object is garbage collected. - var mainWindow = null; + // Remember window position + if (data && data.bounds.x && data.bounds.y) { + mainWindow.x = data.bounds.x; + mainWindow.y = data.bounds.y; + } - app.on('browser-window-focus', function(e, w) { - w.webContents.send('browser-window-focus'); - }); + mainWindow.setMenu(null) + mainWindow.setMenuBarVisibility(false) + mainWindow.setAutoHideMenuBar(true) - app.on('ready', function() { - var menu = Menu.buildFromTemplate(template); - Menu.setApplicationMenu(menu); - const initPath = __dirname + "/init.json"; - var data; + // and load the index.html of the app. + mainWindow.loadFile('index.html') - // read saved state from file (e.g. window bounds) - try { - data = JSON.parse(fs.readFileSync(initPath, 'utf8')); - } - catch(e) { - console.log('Unable to read init.json: ', e); - } - const bounds = (data && data.bounds) ? data.bounds : {width: 1280, height:800 }; - var bwdata = {width: bounds.width, height: bounds.height, 'min-width': 1024, 'min-height': 600, 'autoHideMenuBar': true, 'web-security': true, 'java': false, 'accept-first-mouse': true, defaultEncoding: 'UTF-8', 'icon':'file://'+__dirname + '/assets/img/favicon.png'}; - // Remembe window position - if (data && data.bounds.x && data.bounds.y) { - bwdata.x = data.bounds.x; - bwdata.y = data.bounds.y; - } + // Open the DevTools. + // mainWindow.webContents.openDevTools() + + var handleLink = (e, url) => { + if(url != mainWindow.webContents.getURL()) { + e.preventDefault() + shell.openExternal(url) + } + } + + mainWindow.webContents.on('will-navigate', handleLink) + mainWindow.webContents.on('new-window', handleLink) + + // Emitted when the window is closing. + mainWindow.on('close', function () { + let data = { + bounds: mainWindow.getBounds() + } + fs.writeFileSync(initPath, JSON.stringify(data)) + }) + + // Emitted when the window is closed. + mainWindow.on('closed', function () { + // Dereference the window object, usually you would store windows + // in an array if your app supports multi windows, this is the time + // when you should delete the corresponding element. + mainWindow = null + }) + + app.on('browser-window-focus', function() { + setTimeout(function() { mainWindow.webContents.focus() }, 0) + setTimeout(function() { mainWindow.webContents.executeJavaScript("document.getElementById(\"sendMessage\").focus()") }, 0) + }) +} + +// This method will be called when Electron has finished +// initialization and is ready to create browser windows. +// Some APIs can only be used after this event occurs. +app.on('ready', function() { + createWindow() +}) - mainWindow = new BrowserWindow(bwdata); - mainWindow.loadURL('file://' + __dirname + '/electron-start.html'); - mainWindow.focus(); - // Listen for badge changes - ipcMain.on('badge', function(event, arg) { - if (process.platform === "darwin") { - app.dock.setBadge(String(arg)); - } - else if (process.platform === "win32") { - let n = parseInt(arg, 10); - // Only show notifications with number - if (isNaN(n)) { - return; - } - if (n > 0) { - mainWindow.setOverlayIcon(__dirname + '/assets/img/favicon.ico', String(arg)); - } else { - mainWindow.setOverlayIcon(null, ''); - } - } - }); +// Listen for badge changes +ipcMain.on('badge', function(event, arg) { + if (process.platform === "darwin") { + app.dock.setBadge(String(arg)) + } + else if (process.platform === "win32") { + let n = parseInt(arg, 10) + // Only show notifications with number + if (isNaN(n)) { + return + } + if (n > 0) { + mainWindow.setOverlayIcon(__dirname + '/assets/img/favicon.ico', String(arg)) + } else { + mainWindow.setOverlayIcon(null, '') + } + } +}) - mainWindow.on('devtools-opened', function() { - mainWindow.webContents.executeJavaScript("document.getElementById('glowingbear').openDevTools();"); - }); +// Quit when all windows are closed. +app.on('window-all-closed', function () { + // On macOS it is common for applications and their menu bar + // to stay active until the user quits explicitly with Cmd + Q + if (process.platform !== 'darwin') app.quit() +}) - mainWindow.on('close', function() { - // Save window bounds to disk - var data = { - bounds: mainWindow.getBounds() - }; - fs.writeFileSync(initPath, JSON.stringify(data)); - }); +app.on('activate', function () { + // On macOS it's common to re-create a window in the app when the + // dock icon is clicked and there are no other windows open. + if (mainWindow === null) createWindow() +}) - mainWindow.on('closed', function() { - app.quit(); - }); - }); -})(); +// In this file you can include the rest of your app's specific main process +// code. You can also put them in separate files and require them here. diff --git a/electron-start.html b/electron-start.html deleted file mode 100644 index 2844bfa..0000000 --- a/electron-start.html +++ /dev/null @@ -1,41 +0,0 @@ - - - - - - - - - - diff --git a/electron.makefile b/electron.makefile index bb0b721..966c2ca 100644 --- a/electron.makefile +++ b/electron.makefile @@ -18,10 +18,10 @@ uselocal: copylocal # build the electron app for various platforms build-electron-windows: uselocal - electron-packager ${ELECTRON_COMMON} --platform=win32 --arch=ia32 --electron-version=3.0.6 --icon=assets/img/favicon.ico --asar=true + electron-packager ${ELECTRON_COMMON} --platform=win32 --arch=ia32 --electron-version=8.0.1 --icon=assets/img/favicon.ico --asar=true build-electron-darwin: uselocal - electron-packager ${ELECTRON_COMMON} --platform=darwin --arch=x64 --electron-version=3.0.6 --icon=assets/img/glowing-bear.icns + electron-packager ${ELECTRON_COMMON} --platform=darwin --arch=x64 --electron-version=8.0.1 --icon=assets/img/glowing-bear.icns build-electron-linux: uselocal - electron-packager ${ELECTRON_COMMON} --platform=linux --arch=x64 --electron-version=3.0.6 --icon=assets/img/favicon.ico + electron-packager ${ELECTRON_COMMON} --platform=linux --arch=x64 --electron-version=8.0.1 --icon=assets/img/favicon.ico diff --git a/index.html b/index.html index 2b95c4e..079582b 100644 --- a/index.html +++ b/index.html @@ -81,33 +81,41 @@
-
- + +
- + +
- - +
+
+ + +
+
+ + +
+
- Error: wrong password + Error: wrong password or token
-
-
-
- +
@@ -132,6 +146,7 @@
+

Use TLS encryption

WeeChat version 0.4.2 or higher is required—we recommend at least 1.0.

To start using Glowing Bear, follow the instructions below to set up an encrypted relay. All communication goes directly between your browser and your WeeChat relay! This means that your server must be accessible. We never see any of your data or your password, and you don't need to trust a "cloud". All settings, including your password, are saved locally in your own browser between sessions.

You're using Glowing Bear over an unencrypted connection (http://). This is not recommended! We recommend using our secure hosted version at https://www.glowing-bear.org/, or https://latest.glowing-bear.org for the latest and greatest development version. You can still follow the instructions below to set up an encrypted relay, though.
@@ -149,6 +164,11 @@ chown -R username:username ~username

Your certificate needs to be renewed every couple of months. Either follow the instructions for automatic renewal at https://certbot.eff.org, or run certbot renew manually when renewal is due. Important: You'll need to follow the instructions for copying the certificate to the right place again, and re-run /relay sslcertkey in WeeChat.

+

Use TOTP (Time-based One-Time Password)

+

Configure WeeChat for TOTP. The secret key has to be a base 32 string.

+
/secure set relay_totp_secret xxxxx
+/set relay.network.totp_secret "${sec.data.relay_totp_secret}"
+

Open an authenticator app and create an entry with the same secret. In Glowing Bear check the checkbox for "use Time-based One-Time Password" and fill in the one time password as you see it in the authenticator app.

@@ -188,6 +208,65 @@ chown -R username:username ~username Helpful trigger to automatically repin a buffer (in this instance, irc.freenode.#weechat):
/trigger add autopin signal "buffer_opened" "${buffer[${tg_signal_data}].full_name} =~ irc.freenode.#weechat" "" "/command -buffer ${buffer[${tg_signal_data}].full_name} * /buffer set localvar_set_pinned true"

+

Setting a custom path

+

+ The hostname field can be used to set a custom path. Or a URL parameter can be used see section 'URL Parameters'. +

+

+ To connect to the weechat relay service we connect using a URL. A typical URL consists of 4 parts. {scheme}://{host}:{port}/{path}. The path can be changed by entering the relay's full URL (except the scheme). +

+ +

+ Examples of correct input for the host field are: +

+ +

+ Incorrect input for the host field: +

+ +

URL Parameters

+

+ Parameters can be passed in the URL to prefill the fields. This can be useful when you have multiple relays and want to use bookmarks to manage them. + We do not recommend passing the password in this way as it will be visible in plain text and stored in history/bookmarks but it is possible. Special characters should be URL encoded. +

+

+ If we want just the path for example: https://glowing-bear.org/#path=weechat2 +

+

+ An example: https://glowing-bear.org/#host=my.domain.com&port=8000&password=hunter2&autoconnect=true +

+

+ Available parameters: +

+

@@ -236,8 +315,8 @@ npm run build-electron-{windows, darwin, linux}
-
- +
+ brand {{unread}} @@ -359,7 +438,7 @@ npm run build-electron-{windows, darwin, linux}
-
+
@@ -369,7 +448,7 @@ npm run build-electron-{windows, darwin, linux}
-
+
@@ -380,13 +459,28 @@ npm run build-electron-{windows, darwin, linux}
-
+
+
  • +
    +
    + +
    + +
    + +
    + +
    +
    +
    +
  • +
  • diff --git a/js/bufferResume.js b/js/bufferResume.js index 1af4f4e..536e715 100644 --- a/js/bufferResume.js +++ b/js/bufferResume.js @@ -11,7 +11,7 @@ var bufferResume = angular.module('bufferResume', []); bufferResume.service('bufferResume', ['settings', function(settings) { var resumer = {}; - var key = settings.host + ":" + settings.port; + var key = settings.host + ":" + settings.port + "/" + settings.path; // Hold the status that we were able to find the previously accessed buffer // and reload it. If we cannot, we'll need to know so we can load the default diff --git a/js/connection.js b/js/connection.js index 3f0933d..a8f813d 100644 --- a/js/connection.js +++ b/js/connection.js @@ -20,15 +20,15 @@ weechat.factory('connection', var locked = false; // Takes care of the connection and websocket hooks - var connect = function (host, port, passwd, ssl, noCompression, successCallback, failCallback) { + var connect = function (host, port, path, passwd, ssl, useTotp, totp, noCompression, successCallback, failCallback) { $rootScope.passwordError = false; - connectionData = [host, port, passwd, ssl, noCompression]; + connectionData = [host, port, path, passwd, ssl, noCompression]; var proto = ssl ? 'wss' : 'ws'; // If host is an IPv6 literal wrap it in brackets if (host.indexOf(":") !== -1 && host[0] !== "[" && host[host.length-1] !== "]") { host = "[" + host + "]"; } - var url = proto + "://" + host + ":" + port + "/weechat"; + var url = proto + "://" + host + ":" + port + "/" + path; $log.debug('Connecting to URL: ', url); var onopen = function () { @@ -45,7 +45,9 @@ weechat.factory('connection', ngWebsockets.send( weeChat.Protocol.formatInit({ password: passwd, - compression: noCompression ? 'off' : 'zlib' + compression: noCompression ? 'off' : 'zlib', + useTotp: useTotp, + totp: totp }) ); @@ -326,9 +328,16 @@ weechat.factory('connection', }; var attemptReconnect = function (bufferId, timeout) { + // won't work if totp is mandatory + if (settings.useTotp) + { + $log.info('Not reconnecting because totp will be expired.'); + return; + } + $log.info('Attempting to reconnect...'); var d = connectionData; - connect(d[0], d[1], d[2], d[3], d[4], function() { + connect(d[0], d[1], d[2], d[3], d[4], false, "", d[5], function() { $rootScope.reconnecting = false; // on success, update active buffer models.setActiveBuffer(bufferId); diff --git a/js/glowingbear.js b/js/glowingbear.js index f8bc47b..04519bb 100644 --- a/js/glowingbear.js +++ b/js/glowingbear.js @@ -19,8 +19,8 @@ weechat.config(['$compileProvider', function ($compileProvider) { } }]); -weechat.controller('WeechatCtrl', ['$rootScope', '$scope', '$store', '$timeout', '$log', 'models', 'bufferResume', 'connection', 'notifications', 'utils', 'settings', - function ($rootScope, $scope, $store, $timeout, $log, models, bufferResume, connection, notifications, utils, settings) +weechat.controller('WeechatCtrl', ['$rootScope', '$scope', '$store', '$timeout','$location', '$log', 'models', 'bufferResume', 'connection', 'notifications', 'utils', 'settings', + function ($rootScope, $scope, $store, $timeout, $location, $log, models, bufferResume, connection, notifications, utils, settings) { window.openBuffer = function(channel) { @@ -41,9 +41,11 @@ weechat.controller('WeechatCtrl', ['$rootScope', '$scope', '$store', '$timeout', // or else they won't be saved to the localStorage. settings.setDefaults({ 'theme': 'dark', - 'host': 'localhost', + 'hostField': 'localhost', 'port': 9001, + 'path': 'weechat', 'ssl': (window.location.protocol === "https:"), + 'useTotp': false, 'savepassword': false, 'autoconnect': false, 'nonicklist': utils.isMobileUi(), @@ -62,9 +64,17 @@ weechat.controller('WeechatCtrl', ['$rootScope', '$scope', '$store', '$timeout', 'enableQuickKeys': true, 'customCSS': '', "currentlyViewedBuffers":{}, + 'iToken': '', + 'iAlb': '', }); $scope.settings = settings; + //For upgrade reasons because we changed the name of host to hostField + //check if the value might still be in the host key instead of the hostField key + if (!settings.hostField && settings.host) { + settings.hostField = settings.host; + } + $rootScope.countWatchers = function () { $log.debug($rootScope.$$watchersCount); }; @@ -104,8 +114,8 @@ weechat.controller('WeechatCtrl', ['$rootScope', '$scope', '$store', '$timeout', })(); // Show a TLS warning if GB was loaded over an unencrypted connection, - // except for local instances (testing, cordova, or electron) - $scope.show_tls_warning = (window.location.protocol !== "https:") && + // except for local instances (local files, testing, cordova, or electron) + $scope.show_tls_warning = (["https:", "file:"].indexOf(window.location.protocol) === -1) && (["localhost", "127.0.0.1", "::1"].indexOf(window.location.hostname) === -1) && !window.is_electron && !utils.isCordova(); @@ -149,6 +159,9 @@ weechat.controller('WeechatCtrl', ['$rootScope', '$scope', '$store', '$timeout', }, false); } + $rootScope.$on('nickListChanged', function() { + $scope.updateShowNicklist(); + }); $rootScope.$on('activeBufferChanged', function(event, unreadSum) { var ab = models.getActiveBuffer(); @@ -316,11 +329,6 @@ weechat.controller('WeechatCtrl', ['$rootScope', '$scope', '$store', '$timeout', } }); - $rootScope.wasMobileUi = false; - if (utils.isMobileUi()) { - $rootScope.wasMobileUi = true; - } - if (!settings.fontfamily) { if (utils.isMobileUi()) { settings.fontfamily = 'sans-serif'; @@ -573,12 +581,10 @@ weechat.controller('WeechatCtrl', ['$rootScope', '$scope', '$store', '$timeout', // Recalculation fails when not connected 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 && !utils.isMobileUi()) { + if (!utils.isMobileUi()) { $scope.showSidebar(); $scope.updateShowNicklist(); } - $scope.wasMobileUi = utils.isMobileUi(); $scope.calculateNumLines(); // if we're scrolled to the bottom, scroll down to the same position after the resize @@ -620,6 +626,7 @@ weechat.controller('WeechatCtrl', ['$rootScope', '$scope', '$store', '$timeout', } $rootScope.bufferBottom = eob.offsetTop <= bl.scrollTop + bl.clientHeight; }; + $rootScope.scrollWithBuffer = function(scrollToReadmarker, moreLines) { // First, get scrolling status *before* modification // This is required to determine where we were in the buffer pre-change @@ -653,8 +660,76 @@ weechat.controller('WeechatCtrl', ['$rootScope', '$scope', '$store', '$timeout', window.requestAnimationFrame(scroll); }; + $scope.parseHost = function() { + //The host field is multi purpose for advanced users + //There can be a combination of host, port and path + //If host is specified here the dedicated port field is disabled + $rootScope.hostInvalid = false; + + var parts; + var regexHost = /^([^:\/]*|\[.*\])$/; + var regexHostPort = /^([^:]*|\[.*\]):(\d+)$/; + var regexHostPortPath = /^([^:]*|\[.*\]):(\d*)\/(.+)$/; + + if ((parts = regexHost.exec(settings.hostField)) !== null) { //host only + settings.host = parts[1]; + settings.path = "weechat"; + $rootScope.portDisabled = false; + } else if ((parts = regexHostPort.exec(settings.hostField)) !== null) { //host:port + settings.host = parts[1]; + settings.port = parts[2]; + settings.path = "weechat"; + $rootScope.portDisabled = true; + } else if ((parts = regexHostPortPath.exec(settings.hostField)) !== null) { //host:port/path + settings.host = parts[1]; + settings.port = parts[2]; + settings.path = parts[3]; + $rootScope.portDisabled = true; + } else { + $rootScope.hostInvalid = true; + } + }; + + settings.addCallback('useTotp', function() { + if (settings.useTotp) { + settings.autoconnect = false; + } + }); + + $scope.parseTotp = function() { + $scope.totpInvalid = !/^\d{4,10}$/.test($scope.totp); + }; + + $scope.parseHash = function() { + + //Fill in url parameters, they take precedence over the stored settings, but store them + var params = {}; + $location.$$hash.split('&').map(function(val) { + var segs = val.split('='); + params[segs[0]] = segs[1]; + }); + if (params.host) { + $scope.settings.host = params.host; + $scope.settings.hostField = params.host; + } + if (params.port) { + $scope.settings.port = parseInt(params.port); + } + if (params.path) { + $scope.settings.path = params.path; + $scope.settings.hostField = $scope.settings.host + ":" + $scope.settings.port + "/" + $scope.settings.path; + } + if (params.password) { + $scope.password = params.password; + } + if (params.autoconnect) { + $scope.settings.autoconnect = params.autoconnect === 'true'; + } + + }; $scope.connect = function() { + notifications.requestNotificationPermission(); $rootScope.sslError = false; $rootScope.securityError = false; @@ -662,14 +737,17 @@ weechat.controller('WeechatCtrl', ['$rootScope', '$scope', '$store', '$timeout', $rootScope.bufferBottom = true; $scope.connectbutton = 'Connecting'; $scope.connectbuttonicon = 'glyphicon-refresh glyphicon-spin'; - connection.connect(settings.host, settings.port, $scope.password, settings.ssl); + connection.connect(settings.host, settings.port, settings.path, $scope.password, settings.ssl, settings.useTotp, $scope.totp); + $scope.totp = "";//clear for next time }; + $scope.disconnect = function() { $scope.connectbutton = 'Connect'; $scope.connectbuttonicon = 'glyphicon-chevron-right'; bufferResume.reset(); connection.disconnect(); }; + $scope.reconnect = function() { var bufferId = models.getActiveBuffer().id; connection.attemptReconnect(bufferId, 3000); @@ -678,6 +756,7 @@ weechat.controller('WeechatCtrl', ['$rootScope', '$scope', '$store', '$timeout', $scope.showModal = function(elementId) { document.getElementById(elementId).setAttribute('data-state', 'visible'); }; + $scope.closeModal = function($event) { function closest(elem, selector) { var matchesSelector = elem.matches || elem.webkitMatchesSelector || elem.mozMatchesSelector || elem.msMatchesSelector; @@ -776,7 +855,7 @@ weechat.controller('WeechatCtrl', ['$rootScope', '$scope', '$store', '$timeout', $scope.updateShowNicklist = function() { var ab = models.getActiveBuffer(); // Check whether buffer exists and nicklist is non-empty - if (!ab || ab.isNicklistEmpty()) { + if (!ab || !ab.nicklistRequested() || ab.isNicklistEmpty()) { $scope.showNicklist = false; return false; } @@ -923,34 +1002,29 @@ weechat.controller('WeechatCtrl', ['$rootScope', '$scope', '$store', '$timeout', } }; + window.onhashchange = function() { + $scope.parseHash(); + }; + $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'; - $scope.connectbuttonicon = 'glyphicon-chevron-right'; - connection.connect(host, port, password, ssl); - } + $scope.parseHost(); + $scope.parseHash(); }; }]); -weechat.config(['$routeProvider', - function($routeProvider) { - $routeProvider.when('/', { +weechat.config(['$routeProvider', '$locationProvider', + function($routeProvider, $locationProvider) { + $routeProvider.when('', { templateUrl: 'index.html', controller: 'WeechatCtrl' }); + + //remove hashbang from url + $locationProvider.html5Mode({ + enabled: true, + requireBase: false + }); } ]); diff --git a/js/handlers.js b/js/handlers.js index 2e749cf..9891e8f 100644 --- a/js/handlers.js +++ b/js/handlers.js @@ -168,7 +168,7 @@ weechat.factory('handlers', ['$rootScope', '$log', 'models', 'plugins', 'notific $rootScope.$emit('notificationChanged'); } - if ((buffer.notify !== 0 && message.highlight) || _.contains(message.tags, 'notify_private')) { + if ((buffer.notify !== 0) && (message.highlight || _.contains(message.tags, 'notify_private'))) { buffer.notification++; server.unread++; notifications.createHighlight(buffer, message); @@ -431,12 +431,24 @@ weechat.factory('handlers', ['$rootScope', '$log', 'models', 'plugins', 'notific /* * Handle nicklist event + * + * This event can either fill or clear a nicklist. It is always a complete nicklist. */ var handleNicklist = function(message) { var nicklist = message.objects[0].content; var group = 'root'; + + //clear the nicklists in case we are clearing + if (nicklist.length==1) + { + models.getBuffer(nicklist[0].pointers[0]).clearNicklist(); + } + + //fill the nicklist nicklist.forEach(function(n) { var buffer = models.getBuffer(n.pointers[0]); + + //buffer nicklist if (n.group === 1) { var g = new models.NickGroup(n); group = g.name; @@ -446,6 +458,9 @@ weechat.factory('handlers', ['$rootScope', '$log', 'models', 'plugins', 'notific buffer.addNick(group, nick); } }); + + //check if nicklist should be hidden or not + $rootScope.$emit('nickListChanged'); }; /* * Handle nicklist diff event diff --git a/js/imgur.js b/js/imgur.js index fdcacf6..0ec0f97 100644 --- a/js/imgur.js +++ b/js/imgur.js @@ -3,7 +3,7 @@ var weechat = angular.module('weechat'); -weechat.factory('imgur', ['$rootScope', function($rootScope) { +weechat.factory('imgur', ['$rootScope', 'settings', function($rootScope, settings) { var process = function(image, callback) { @@ -26,8 +26,18 @@ weechat.factory('imgur', ['$rootScope', function($rootScope) { // Upload image to imgur from base64 var upload = function( base64img, callback ) { - // Set client ID (Glowing Bear) - var clientId = "164efef8979cd4b"; + + // API authorization, either via Client ID (anonymous) or access token + // (add to user's imgur account), see also: + // https://github.com/glowing-bear/glowing-bear/wiki/Getting-an-imgur-token-&-album-hash + var accessToken = "164efef8979cd4b"; + var isClientID = true; + + // Check whether the user has provided an access token + if (settings.iToken.length > 37){ + accessToken = settings.iToken; + isClientID = false; + } // Progress bars container var progressBars = document.getElementById("imgur-upload-progress"), @@ -45,6 +55,11 @@ weechat.factory('imgur', ['$rootScope', function($rootScope) { fd.append("image", base64img); // Append the file fd.append("type", "base64"); // Set image type to base64 + // Add the image to the provided album if configured to do so + if (!isClientID && settings.iAlb.length >= 6) { + fd.append("album", settings.iAlb); + } + // Create new XMLHttpRequest var xhttp = new XMLHttpRequest(); @@ -52,7 +67,11 @@ weechat.factory('imgur', ['$rootScope', function($rootScope) { xhttp.open("POST", "https://api.imgur.com/3/image", true); // Set headers - xhttp.setRequestHeader("Authorization", "Client-ID " + clientId); + if (isClientID) { + xhttp.setRequestHeader("Authorization", "Client-ID " + accessToken); + } else { + xhttp.setRequestHeader("Authorization", "Bearer " + accessToken); + } xhttp.setRequestHeader("Accept", "application/json"); // Handler for response diff --git a/js/inputbar.js b/js/inputbar.js index ceb2265..656a4a7 100644 --- a/js/inputbar.js +++ b/js/inputbar.js @@ -217,6 +217,18 @@ weechat.directive('inputBar', function() { // Extract nick from bufferline prefix var nick = prefix[prefix.length - 1].text; + // Check whether the user is still online + var buffer = models.getBuffer(bufferline.buffer); + var is_online = buffer.queryNicklist(nick); + if (!is_online) { + // show a toast that the user left + var toast = document.createElement('div'); + toast.id = "toast"; + toast.innerHTML = nick + " has left the room"; + document.body.appendChild(toast); + setTimeout(function() { document.body.removeChild(toast); }, 5000); + } + var newValue = $scope.command || ''; // can be undefined, in that case, use the empty string var addColon = newValue.length === 0; if (newValue.length > 0) { diff --git a/js/irc-utils.js b/js/irc-utils.js index ffab2b5..8acde89 100644 --- a/js/irc-utils.js +++ b/js/irc-utils.js @@ -119,7 +119,7 @@ IrcUtils.service('IrcUtils', [function() { suf = ':'; } // addSpace defaults to true - var addSpaceChar = (addSpace === undefined || addSpace === true) ? ' ' : ''; + var addSpaceChar = (addSpace === undefined || addSpace === 'on') ? ' ' : ''; // new nick list to search in var searchNickList = _ciNickList(nickList); @@ -144,7 +144,11 @@ IrcUtils.service('IrcUtils', [function() { if (doIterate) { // try iterating newNick = _nextNick(iterCandidate, m[1], searchNickList); - beforeCaret = newNick + suf + ' '; + if (suf.endsWith(' ')) { + beforeCaret = newNick + suf; + } else { + beforeCaret = newNick + suf + ' '; + } return { text: beforeCaret + afterCaret, caretPos: beforeCaret.length, @@ -166,7 +170,11 @@ IrcUtils.service('IrcUtils', [function() { // no match return ret; } - beforeCaret = newNick + suf + ' '; + if (suf.endsWith(' ')) { + beforeCaret = newNick + suf; + } else { + beforeCaret = newNick + suf + ' '; + } if (afterCaret[0] === ' ') { // swallow first space after caret if any afterCaret = afterCaret.substring(1); diff --git a/js/models.js b/js/models.js index ed3e6f1..1ebc411 100644 --- a/js/models.js +++ b/js/models.js @@ -154,6 +154,18 @@ models.service('models', ['$rootScope', '$filter', 'bufferResume', function($roo } */ }; + /* + * Clear the nicklist + */ + var clearNicklist = function() { + //only keep the root node + for (var obj in nicklist) { + if (obj !== 'root') { + delete nicklist[obj]; + } + } + }; + /* * Updates a nick in nicklist */ @@ -296,6 +308,19 @@ models.service('models', ['$rootScope', '$filter', 'bufferResume', function($roo return nicklist.hasOwnProperty('root'); }; + // Check whether a particular nick is in the nicklist + var queryNicklist = function(nick) { + for (var groupIdx in nicklist) { + var nicks = nicklist[groupIdx].nicks; + for (var nickIdx in nicks) { + if (nicks[nickIdx].name === nick) { + return true; + } + } + } + return false; + }; + /* Clear all our buffer lines */ var clear = function() { while(lines.length > 0) { @@ -325,6 +350,7 @@ models.service('models', ['$rootScope', '$filter', 'bufferResume', function($roo nicklist: nicklist, addNick: addNick, delNick: delNick, + clearNicklist: clearNicklist, updateNick: updateNick, getNicklistByTime: getNicklistByTime, serverSortKey: serverSortKey, @@ -340,6 +366,7 @@ models.service('models', ['$rootScope', '$filter', 'bufferResume', function($roo isNicklistEmpty: isNicklistEmpty, nicklistRequested: nicklistRequested, pinned: pinned, + queryNicklist: queryNicklist, }; }; diff --git a/js/plugins.js b/js/plugins.js index 32c8fa3..2dcabc4 100644 --- a/js/plugins.js +++ b/js/plugins.js @@ -463,8 +463,10 @@ plugins.factory('userPlugins', function() { jsonp(url, function(data) { // Add the gist stylesheet only once if (document.querySelectorAll('link[rel=stylesheet][href="' + data.stylesheet + '"]').length < 1) { - var stylesheet = ''; - document.getElementsByTagName('head')[0].innerHTML += stylesheet; + var stylesheet = document.createElement("link"); + stylesheet.href = data.stylesheet; + stylesheet.setAttribute('rel', 'stylesheet'); + document.head.appendChild(stylesheet); } element.innerHTML = '
    ' + data.div + '
    '; }); @@ -534,24 +536,6 @@ plugins.factory('userPlugins', function() { } }); - /* - * Vine plugin - */ - var vinePlugin = new UrlPlugin('Vine', function (url) { - var regexp = /^https?:\/\/(www\.)?vine\.co\/v\/([a-zA-Z0-9]+)(\/.*)?/i, - match = url.match(regexp); - if (match) { - var id = match[2], embedurl = "https://vine.co/v/" + id + "/embed/simple?audio=1"; - var element = angular.element('') - .addClass('vine-embed') - .attr('src', embedurl) - .attr('width', '600') - .attr('height', '600') - .attr('frameborder', '0'); - return element.prop('outerHTML') + ''; - } - }); - /* * Streamable Embedded Player */ @@ -570,7 +554,7 @@ plugins.factory('userPlugins', function() { }); return { - plugins: [youtubePlugin, dailymotionPlugin, allocinePlugin, imagePlugin, videoPlugin, audioPlugin, spotifyPlugin, cloudmusicPlugin, googlemapPlugin, asciinemaPlugin, yrPlugin, gistPlugin, pastebinPlugin, giphyPlugin, tweetPlugin, vinePlugin, streamablePlugin] + plugins: [youtubePlugin, dailymotionPlugin, allocinePlugin, imagePlugin, videoPlugin, audioPlugin, spotifyPlugin, cloudmusicPlugin, googlemapPlugin, asciinemaPlugin, yrPlugin, gistPlugin, pastebinPlugin, giphyPlugin, tweetPlugin, streamablePlugin] }; diff --git a/js/weechat.js b/js/weechat.js index 640ccbd..f46e3ef 100644 --- a/js/weechat.js +++ b/js/weechat.js @@ -363,7 +363,8 @@ // "*" + (A)STD + "," + EXT // "*" + (A)EXT + "," + STD // "*" + (A)EXT + "," + EXT - regex: /^\*(?:([\x01\x02\x03\x04*!\/_|]*)(\d{2})|@([\x01\x02\x03\x04*!\/_|]*)(\d{5})),(\d{2}|@\d{5})/, + // WeeChat 2.6+ use a tilde (~) instead of a comma (,) so recognise both + regex: /^\*(?:([\x01\x02\x03\x04*!\/_|]*)(\d{2})|@([\x01\x02\x03\x04*!\/_|]*)(\d{5}))[,~](\d{2}|@\d{5})/, fn: function(m) { var ret = {}; @@ -647,6 +648,9 @@ if (params.password !== null) { keys.push('password=' + params.password); } + if (params.useTotp) { + keys.push('totp=' + params.totp); + } parts.push(keys.join(',')); return WeeChatProtocol._formatCmd(null, 'init', parts); diff --git a/package.json b/package.json index 1a8861b..83e87e1 100644 --- a/package.json +++ b/package.json @@ -8,17 +8,17 @@ "license": "GPLv3", "devDependencies": { "bower": "^1.8.8", - "electron-packager": "^12.2.0", - "http-server": "^0.11", - "jasmine-core": "^3.1", - "jshint": "^2.9.6", - "karma": "^4.2.0", - "karma-jasmine": "~1.1", - "karma-junit-reporter": "^1.2", + "electron-packager": "~14.2", + "http-server": "^0.12.0", + "jasmine-core": "^3.5.0", + "jshint": "^2.11.0", + "karma": "~4.4", + "karma-jasmine": "~3.1", + "karma-junit-reporter": "~2.0", "karma-phantomjs-launcher": "^1.0.0", - "protractor": "^5.4.1", + "protractor": "^5.4.2", "shelljs": "^0.8.0", - "uglify-js": "^3.4.9" + "uglify-js": "^3.6.9" }, "scripts": { "postinstall": "bower install -p", diff --git a/test/unit/plugins.js b/test/unit/plugins.js index eaec98b..10a7e53 100644 --- a/test/unit/plugins.js +++ b/test/unit/plugins.js @@ -169,14 +169,5 @@ describe('filter', function() { plugins); })); - it('should recognize vines', inject(function(plugins) { - expectTheseMessagesToContain([ - 'https://vine.co/v/hWh262H9HM5', - 'https://vine.co/v/hWh262H9HM5/embed', - ], - 'Vine', - plugins); - })); - }); });