Merge branch 'master' into gh-pages

gh-pages
Lorenz Hübschle-Schneider 5 years ago
commit c2f06c110d
  1. 54
      css/glowingbear.css
  2. 11
      css/themes/base16-default.css
  3. 7
      css/themes/blue.css
  4. 11
      css/themes/dark.css
  5. 12
      css/themes/light.css
  6. 354
      electron-main.js
  7. 41
      electron-start.html
  8. 6
      electron.makefile
  9. 128
      index.html
  10. 2
      js/bufferResume.js
  11. 19
      js/connection.js
  12. 144
      js/glowingbear.js
  13. 17
      js/handlers.js
  14. 27
      js/imgur.js
  15. 12
      js/inputbar.js
  16. 14
      js/irc-utils.js
  17. 27
      js/models.js
  18. 26
      js/plugins.js
  19. 6
      js/weechat.js
  20. 18
      package.json
  21. 9
      test/unit/plugins.js

@ -146,7 +146,7 @@ input[type=text], input[type=password], #sendMessage {
.col-sm-9 { .col-sm-9 {
padding-right: 5px !important; padding-right: 5px !important;
} }
.glyphicon { #topbar .glyphicon {
top: 0; /* Fixes alignment issue in top bar */ top: 0; /* Fixes alignment issue in top bar */
} }
#topbar { #topbar {
@ -263,6 +263,11 @@ input[type=text], input[type=password], #sendMessage {
#sidebar.ng-hide { #sidebar.ng-hide {
width: 0; width: 0;
} }
#sidebar[data-state=hidden] {
transform: translate(-200px,0);
-webkit-transform: translate(-200px,0);
}
#nicklist { #nicklist {
position: fixed; position: fixed;
@ -400,6 +405,7 @@ td.time {
margin-left: 0; margin-left: 0;
padding-left: 145px; padding-left: 145px;
} }
.footer.withnicklist { .footer.withnicklist {
padding-right: 100px; padding-right: 100px;
} }
@ -610,7 +616,6 @@ h2 span, h2 small {
.panel[data-state=active] .panel-collapse { .panel[data-state=active] .panel-collapse {
transition: max-height 0.5s; transition: max-height 0.5s;
max-height: 60em;
height: auto; height: auto;
display: block; display: block;
} }
@ -676,6 +681,10 @@ li.buffer.indent.private a {
font-size: small; font-size: small;
} }
.lessleftpad {
padding-left: 5px;
}
.unselectable { .unselectable {
-webkit-user-select: none; -webkit-user-select: none;
-moz-user-select: none; -moz-user-select: none;
@ -699,6 +708,30 @@ img.emojione {
width: auto; 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 { .glyphicon-spin {
-webkit-animation: spin 1000ms infinite linear; -webkit-animation: spin 1000ms infinite linear;
animation: spin 1000ms infinite linear; animation: spin 1000ms infinite linear;
@ -794,11 +827,6 @@ img.emojione {
-webkit-transform: translate(0,0); /* Safari */ -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 { .content[sidebar-state=visible] #bufferlines, .content[sidebar-state=visible] .footer {
margin-left: 0px; margin-left: 0px;
transform: translate(200px,0); transform: translate(200px,0);
@ -811,17 +839,17 @@ img.emojione {
text-align: center; text-align: center;
font-size: 18px; font-size: 18px;
width: initial; width: initial;
z-index: -1;
} }
#topbar .brand img { #topbar .brand a {
height: 28px; padding: 0 2px 0 10px;
} }
#topbar .badge { #topbar .brand img {
display: none; height: 28px;
} }
#bufferlines, #nicklist { #bufferlines, #nicklist {
position: relative; position: relative;
min-height: 0; min-height: 0;
@ -931,4 +959,4 @@ code {
padding: 0px 2px; padding: 0px 2px;
color: #444; color: #444;
border: 1pt solid #444; border: 1pt solid #444;
} }

@ -65,6 +65,10 @@ a:visited:hover, a:visited:active, a:visited:focus {
border: 0px none; border: 0px none;
} }
.form-control[disabled] {
background: var(--base03);
}
.form-control:focus { .form-control:focus {
color: var(--base06); 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; 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 { 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; 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 { input[type=text], input[type=password], #sendMessage, .btn-send, .btn-send-image, .btn-complete-nick {
color: var(--base05); color: var(--base05);
@ -416,6 +423,10 @@ button.close:hover {
color: var(--base01); color: var(--base01);
} }
#toast {
background-color: var(--base01);
}
/****************************/ /****************************/
/* Weechat colors and style */ /* Weechat colors and style */
/****************************/ /****************************/

@ -134,6 +134,13 @@ input[type=text], input[type=password], #sendMessage, .badge, .btn-send, .btn-se
border: 1px solid #363943; border: 1px solid #363943;
} }
#toast {
background-color: #283244;
border: 1px solid;
border-color: rgb(29, 94, 152);
border-radius: 0px;
}
.horizontal-line { .horizontal-line {
-webkit-box-shadow: rgba(255, 255, 255, 0.07) 0 1px 0; -webkit-box-shadow: rgba(255, 255, 255, 0.07) 0 1px 0;
-moz-box-shadow: rgba(255, 255, 255, 0.07) 0 1px 0; -moz-box-shadow: rgba(255, 255, 255, 0.07) 0 1px 0;

@ -10,6 +10,10 @@ body {
border: 0px none; border: 0px none;
} }
.form-control[disabled] {
background: none repeat scroll 0% 0% rgba(63, 63, 63, 0.3);
}
.form-control option { .form-control option {
color: #eee; color: #eee;
background: #282828; background: #282828;
@ -86,6 +90,9 @@ input[type=text], input[type=password], #sendMessage, .badge, .btn-send, .btn-se
color: #ccc; color: #ccc;
background: none repeat scroll 0% 0% rgba(0, 0, 0, 0.3); 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-complete-nick:hover, .btn-complete-nick:focus,
.btn-send:hover, .btn-send:focus, .btn-send:hover, .btn-send:focus,
@ -2119,6 +2126,10 @@ code {
color: #fff; color: #fff;
} }
#toast {
background-color: #333;
}
/* */ /* */
/* Mobile layout */ /* Mobile layout */
/* */ /* */

@ -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); 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 { #connection-infos {
color: #aaa; color: #aaa;
} }
@ -2084,6 +2092,10 @@ select.form-control, select option, input[type=text], input[type=password], #sen
font-weight: bold; font-weight: bold;
} }
#toast {
background-color: #ddd;
}
/* */ /* */
/* Mobile layout */ /* Mobile layout */
/* */ /* */

@ -1,258 +1,122 @@
(function() { // Modules to control application life and create native browser window
'use strict'; const {app, BrowserWindow, shell, ipcMain} = require('electron')
const electron = require('electron'); const path = require('path')
const app = electron.app; // Module to control application life. const fs = require('fs')
const BrowserWindow = electron.BrowserWindow; // Module to create native browser window.
const ipcMain = require('electron').ipcMain; // Keep a global reference of the window object, if you don't, the window will
const nativeImage = require('electron').nativeImage; // be closed automatically when the JavaScript object is garbage collected.
const Menu = require('electron').Menu; let mainWindow
// Node fs module
const fs = require("fs");
var template;
template = [ // We use this to store some tiny amount of preferences specific to electron
{ // things like window bounds and location
label: 'Edit', const initPath = "init.json"
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'); }
},
]
},
];
if (process.platform == 'darwin') { function createWindow () {
var name = app.getName(); let data
template.unshift({ // read saved state from file (e.g. window bounds)
label: name, try {
submenu: [ data = JSON.parse(fs.readFileSync(initPath, 'utf8'))
{ }
label: 'About ' + name, catch(e) {
role: 'about' console.log('Unable to read init.json: ', e)
}, }
{ // Create the browser window.
type: 'separator' const bounds = (data && data.bounds) ? data.bounds : {width: 1280, height:800 }
}, mainWindow = new BrowserWindow({
{ width: bounds.width,
label: 'Services', height: bounds.height,
role: 'services', webPreferences: {
submenu: [] preload: path.join(__dirname, 'electron-globals.js')
},
{
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'
}
);
} }
})
// Keep a global reference of the window object, if you don't, the window will // Remember window position
// be closed automatically when the JavaScript object is garbage collected. if (data && data.bounds.x && data.bounds.y) {
var mainWindow = null; mainWindow.x = data.bounds.x;
mainWindow.y = data.bounds.y;
}
app.on('browser-window-focus', function(e, w) { mainWindow.setMenu(null)
w.webContents.send('browser-window-focus'); mainWindow.setMenuBarVisibility(false)
}); mainWindow.setAutoHideMenuBar(true)
app.on('ready', function() { // and load the index.html of the app.
var menu = Menu.buildFromTemplate(template); mainWindow.loadFile('index.html')
Menu.setApplicationMenu(menu);
const initPath = __dirname + "/init.json";
var data;
// read saved state from file (e.g. window bounds) // Open the DevTools.
try { // mainWindow.webContents.openDevTools()
data = JSON.parse(fs.readFileSync(initPath, 'utf8'));
} var handleLink = (e, url) => {
catch(e) { if(url != mainWindow.webContents.getURL()) {
console.log('Unable to read init.json: ', e); e.preventDefault()
} shell.openExternal(url)
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) { mainWindow.webContents.on('will-navigate', handleLink)
bwdata.x = data.bounds.x; mainWindow.webContents.on('new-window', handleLink)
bwdata.y = data.bounds.y;
} // 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 // Listen for badge changes
ipcMain.on('badge', function(event, arg) { ipcMain.on('badge', function(event, arg) {
if (process.platform === "darwin") { if (process.platform === "darwin") {
app.dock.setBadge(String(arg)); app.dock.setBadge(String(arg))
} }
else if (process.platform === "win32") { else if (process.platform === "win32") {
let n = parseInt(arg, 10); let n = parseInt(arg, 10)
// Only show notifications with number // Only show notifications with number
if (isNaN(n)) { if (isNaN(n)) {
return; return
} }
if (n > 0) { if (n > 0) {
mainWindow.setOverlayIcon(__dirname + '/assets/img/favicon.ico', String(arg)); mainWindow.setOverlayIcon(__dirname + '/assets/img/favicon.ico', String(arg))
} else { } else {
mainWindow.setOverlayIcon(null, ''); mainWindow.setOverlayIcon(null, '')
} }
} }
}); })
mainWindow.on('devtools-opened', function() { // Quit when all windows are closed.
mainWindow.webContents.executeJavaScript("document.getElementById('glowingbear').openDevTools();"); 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() { app.on('activate', function () {
// Save window bounds to disk // On macOS it's common to re-create a window in the app when the
var data = { // dock icon is clicked and there are no other windows open.
bounds: mainWindow.getBounds() if (mainWindow === null) createWindow()
}; })
fs.writeFileSync(initPath, JSON.stringify(data));
});
mainWindow.on('closed', function() { // In this file you can include the rest of your app's specific main process
app.quit(); // code. You can also put them in separate files and require them here.
});
});
})();

@ -1,41 +0,0 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<script>
onload = function() {
const ipc= require('electron').ipcRenderer;
const remote = require('electron').remote;
const nativeImage = require('electron').nativeImage;
const shell = require('electron').shell;
var webview = document.getElementById("glowingbear");
var handleconsole = function(e) {
console.log("webview: " + e.message);
}
var handlenewwindow = function(e) {
shell.openExternal(e.url);
}
var handletitleset = function(e) {
document.title = e.title;
}
webview.addEventListener("console-message", handleconsole);
webview.addEventListener("new-window", handlenewwindow);
webview.addEventListener("page-title-set", handletitleset);
ipc.on('openDevTools', function() {
setTimeout(function() { webview.openDevTools(); }, 0 );
});
ipc.on('browser-window-focus', function() {
setTimeout(function() { webview.focus(); }, 0);
setTimeout(function() { webview.executeJavaScript("document.getElementById(\"sendMessage\").focus();") }, 0);
});
}
</script>
</head>
<body>
<webview preload="electron-globals.js" id="glowingbear" src="index.html" style="position:fixed; top:0; left:0; bottom:0; right:0;"></webview>
</body>
</html>

@ -18,10 +18,10 @@ uselocal: copylocal
# build the electron app for various platforms # build the electron app for various platforms
build-electron-windows: uselocal 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 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 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

@ -81,33 +81,41 @@
<div class="panel-body"> <div class="panel-body">
<form class="form-signin" role="form"> <form class="form-signin" role="form">
<div class="form-group"> <div class="form-group">
<label class="control-label" for="host">WeeChat relay hostname and port number</label>
<div class="input-group"> <div class="input-group">
<div class="row no-gutter"> <div class="row no-gutter">
<div class="col-sm-9"> <div class="col-sm-9">
<input type="text" class="form-control favorite-font" id="host" ng-model="settings.host" placeholder="Address" autocapitalize="off"> <label class="control-label" for="host">WeeChat relay hostname</label>
<input type="text" class="form-control favorite-font" id="host" ng-model="settings.hostField" ng-change="parseHost()" ng-class="{'is-invalid': hostInvalid}" placeholder="Address" autocapitalize="off">
</div> </div>
<div class="col-sm-3"> <div class="col-sm-3">
<input type="text" class="form-control favorite-font" id="port" ng-model="settings.port" placeholder="Port"> <label class="control-label" for="port">Port</label>
<input type="text" class="form-control favorite-font" id="port" ng-model="settings.port" ng-disabled="portDisabled" placeholder="Port">
</div> </div>
</div> </div>
</div> </div>
<label class="control-label" for="password">WeeChat relay password</label> <div class="row no-gutter">
<input type="password" class="form-control favorite-font" id="password" ng-model="password" placeholder="Password"> <div ng-class="settings.useTotp ? 'col-sm-9' : 'col-sm-12'" ng>
<label class="control-label" for="password">WeeChat relay password</label>
<input type="password" class="form-control favorite-font" id="password" ng-model="password" placeholder="Password">
</div>
<div class="col-sm-3" ng-Show="settings.useTotp">
<label class="control-label" for="totp">Token</label>
<input type="text" class="form-control favorite-font" id="totp" ng-model="totp" ng-change="parseTotp()" ng-class="{'is-invalid': totpInvalid}" autocomplete="off">
</div>
</div>
<div class="alert alert-danger" ng-show="passwordError" ng-cloak> <div class="alert alert-danger" ng-show="passwordError" ng-cloak>
Error: wrong password Error: wrong password or token
</div> </div>
<div class="checkbox"> <div class="checkbox">
<label class="control-label" for="savepassword"> <label class="control-label" for="savepassword">
<input type="checkbox" id="savepassword" ng-model="settings.savepassword"> <input type="checkbox" id="savepassword" ng-model="settings.savepassword">
Save password in your browser Save password in your browser
</label> </label>
</div> </div>
<div class="checkbox" ng-show="settings.savepassword"> <div class="checkbox">
<label class="control-label" for="autoconnect"> <label class="control-label" for="useTotp">
<input type="checkbox" id="autoconnect" ng-model="settings.autoconnect"> <input type="checkbox" id="useTotp" ng-model="settings.useTotp">
Automatically connect Use Time-based One-Time Password <a href="https://blog.weechat.org/post/2019/01/14/Support-of-TOTP" target="_blank"><i class="glyphicon glyphicon-info-sign"></i></a>
</label> </label>
</div> </div>
<div class="checkbox"> <div class="checkbox">
@ -116,8 +124,14 @@
Encryption. <strong>Strongly recommended!</strong> Need help? Check below. Encryption. <strong>Strongly recommended!</strong> Need help? Check below.
</label> </label>
</div> </div>
<div class="checkbox" ng-show="settings.savepassword || settings.autoconnect">
<label class="control-label" for="autoconnect">
<input type="checkbox" id="autoconnect" ng-model="settings.autoconnect" ng-disabled="settings.useTotp">
Automatically connect
</label>
</div>
</div> </div>
<button class="btn btn-lg btn-primary" ng-click="connect()" ng-cloak>{{ connectbutton }} <i ng-class="connectbuttonicon" class="glyphicon"></i></button> <button class="btn btn-lg btn-primary" ng-disabled="hostInvalid || (totpInvalid && settings.useTotp)" ng-click="connect()" ng-cloak>{{ connectbutton }} <i ng-class="connectbuttonicon" class="glyphicon"></i></button>
</form> </form>
</div> </div>
</div> </div>
@ -132,6 +146,7 @@
</div> </div>
<div id="collapseTwo" class="panel-collapse collapse"> <div id="collapseTwo" class="panel-collapse collapse">
<div class="panel-body"> <div class="panel-body">
<h3>Use TLS encryption</h3>
<p><span class="label label-danger">WeeChat version 0.4.2 or higher is required—we recommend at least 1.0.</p> <p><span class="label label-danger">WeeChat version 0.4.2 or higher is required—we recommend at least 1.0.</p>
<p>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.</p> <p>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.</p>
<div class="alert alert-warning" ng-show="show_tls_warning"><strong>You're using Glowing Bear over an unencrypted connection (http://). This is not recommended!</strong> We recommend using our secure hosted version at <a href="https://www.glowing-bear.org/">https://www.glowing-bear.org/</a>, or <a href="https://latest.glowing-bear.org/">https://latest.glowing-bear.org</a> for the latest and greatest development version. You can still follow the instructions below to set up an encrypted relay, though.</div> <div class="alert alert-warning" ng-show="show_tls_warning"><strong>You're using Glowing Bear over an unencrypted connection (http://). This is not recommended!</strong> We recommend using our secure hosted version at <a href="https://www.glowing-bear.org/">https://www.glowing-bear.org/</a>, or <a href="https://latest.glowing-bear.org/">https://latest.glowing-bear.org</a> for the latest and greatest development version. You can still follow the instructions below to set up an encrypted relay, though.</div>
@ -149,6 +164,11 @@ chown -R <strong>username</strong>:<strong>username</strong> ~<strong>username</
/relay add ssl.weechat {{ settings.port || 9001 }} /relay add ssl.weechat {{ settings.port || 9001 }}
</pre> </pre>
<p>Your certificate needs to be renewed every couple of months. Either follow the instructions for automatic renewal at <a href="https://certbot.eff.org/">https://certbot.eff.org</a>, or run <code>certbot renew</code> manually when renewal is due. <strong>Important:</strong> You'll need to follow the instructions for copying the certificate to the right place again, and re-run <code>/relay sslcertkey</code> in WeeChat.</p> <p>Your certificate needs to be renewed every couple of months. Either follow the instructions for automatic renewal at <a href="https://certbot.eff.org/">https://certbot.eff.org</a>, or run <code>certbot renew</code> manually when renewal is due. <strong>Important:</strong> You'll need to follow the instructions for copying the certificate to the right place again, and re-run <code>/relay sslcertkey</code> in WeeChat.</p>
<h3>Use TOTP (Time-based One-Time Password)</h3>
<p><a href="https://blog.weechat.org/post/2019/01/14/Support-of-TOTP">Configure</a> WeeChat for TOTP. The secret key has to be a base 32 string.</p>
<pre>/secure set relay_totp_secret xxxxx
/set relay.network.totp_secret "${sec.data.relay_totp_secret}"</pre>
<p>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.</p>
</div> </div>
</div> </div>
</div> </div>
@ -188,6 +208,65 @@ chown -R <strong>username</strong>:<strong>username</strong> ~<strong>username</
<p> <p>
Helpful trigger to automatically repin a buffer (in this instance, <var>irc.freenode.#weechat</var>): <pre><code>/trigger add autopin signal "buffer_opened" "${buffer[${tg_signal_data}].full_name} =~ <var>irc.freenode.#weechat</var>" "" "/command -buffer ${buffer[${tg_signal_data}].full_name} * /buffer set localvar_set_pinned true"</code></pre> Helpful trigger to automatically repin a buffer (in this instance, <var>irc.freenode.#weechat</var>): <pre><code>/trigger add autopin signal "buffer_opened" "${buffer[${tg_signal_data}].full_name} =~ <var>irc.freenode.#weechat</var>" "" "/command -buffer ${buffer[${tg_signal_data}].full_name} * /buffer set localvar_set_pinned true"</code></pre>
</p> </p>
<h3>Setting a custom path</h3>
<p>
The hostname field can be used to set a custom path. Or a URL parameter can be used see section 'URL Parameters'.
</p>
<p>
To connect to the weechat relay service we connect using a URL. A typical URL consists of 4 parts. <code>{scheme}://{host}:{port}/{path}</code>. The path can be changed by entering the relay's full URL (except the scheme).
</p>
<ul>
<li><b>scheme</b>: The scheme must never be input. The scheme is "ws" if TLS isn't used and it is "wss" if TLS is used.</li>
<li><b>host</b>: Can be an IPv4, IPv6 or a FQDN. IPv6 addresses must be wrapped in square brackets.</li>
<li><b>port</b>: can be specified in the host field or the seperate port field. However if the path is specified in the host field the port must also be specified.</li>
<li><b>path</b>: by defautl this is "weechat". In case <a href="https://github.com/glowing-bear/glowing-bear/wiki/Proxying-WeeChat-relay-with-a-web-server">a proxy</a> is used the path can be changed by entering it in the host field.</li>
</ul>
<p>
Examples of correct input for the host field are:
</p>
<ul>
<li>192.168.0.1</li>
<li>192.168.0.1:8000</li>
<li>192.168.0.1:8000/weechat2</li>
<li>[2001:db8:85a3::8a2e:370:7334]</li>
<li>[2001:db8:85a3::8a2e:370:7334]:8000</li>
<li>[2001:db8:85a3::8a2e:370:7334]:8000/weechat2</li>
<li>yourhost.yourdomain.com</li>
<li>yourhost.yourdomain.com:8000</li>
<li>yourhost.yourdomain.com:8000/weechat2</li>
</ul>
<p>
Incorrect input for the host field:
</p>
<ul>
<li><span class="text-danger">ws://192.168.0.1</span> (do not specify the scheme)</li>
<li><span class="text-danger">192.168.0.1/weechat2</span> (must specify port when specifying path)</li>
<li><span class="text-danger">[2001:db8:85a3::8a2e:370:7334]/weechat2</span> (must specify port when specifying path)</li>
<li><span class="text-danger">yourhost.yourdomain.com/weechat2</span> (must specify port when specifying path)</li>
<li><span class="text-danger">2001:db8:85a3::8a2e:370:7334</span> (must wrap IPv6 address in square brackets)</li>
<li><span class="text-danger">2001:db8:85a3::8a2e:370:7334:8000</span> (must wrap IPv6 address in square brackets)</li>
</ul>
<h3>URL Parameters</h3>
<p>
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 <a href="https://www.w3schools.com/tags/ref_urlencode.asp">URL encoded</a>.
</p>
<p>
If we want just the path for example: <code>https://glowing-bear.org/#path=weechat2</code>
</p>
<p>
An example: <code>https://glowing-bear.org/#host=my.domain.com&port=8000&password=hunter2&autoconnect=true</code>
</p>
<p>
Available parameters:
<ul>
<li>host</li>
<li>port</li>
<li>path</li>
<li>password</li>
<li>autoconnect</li>
</ul>
</p>
</div> </div>
</div> </div>
</div> </div>
@ -236,8 +315,8 @@ npm run build-electron-{windows, darwin, linux}</pre>
</div> </div>
<div class="content" id="content" sidebar-state="visible" ng-show="connected" ng-cloak> <div class="content" id="content" sidebar-state="visible" ng-show="connected" ng-cloak>
<div id="topbar"> <div id="topbar">
<div class="brand"> <div class="brand" ng-click="toggleSidebar()">
<a href="#" ng-click="toggleSidebar()"> <a href="#">
<img alt="brand" src="assets/img/favicon.png" title="Connected to {{ settings.host }}:{{ settings.port}}"> <img alt="brand" src="assets/img/favicon.png" title="Connected to {{ settings.host }}:{{ settings.port}}">
</a> </a>
<span class="badge" ng-show="unread > 0">{{unread}}</span> <span class="badge" ng-show="unread > 0">{{unread}}</span>
@ -359,7 +438,7 @@ npm run build-electron-{windows, darwin, linux}</pre>
</div> </div>
<label for="size" class="col-sm-1 control-label">Size</label> <label for="size" class="col-sm-1 control-label">Size</label>
<div class="col-sm-2"> <div class="col-sm-3">
<input type="text" ng-model="settings.fontsize" class="form-control" id="size"> <input type="text" ng-model="settings.fontsize" class="form-control" id="size">
</div> </div>
</div> </div>
@ -369,7 +448,7 @@ npm run build-electron-{windows, darwin, linux}</pre>
<form class="form-horizontal" role="form"> <form class="form-horizontal" role="form">
<div class="form-group"> <div class="form-group">
<label for="theme" class="col-sm-3 control-label make-thinner">Theme</label> <label for="theme" class="col-sm-3 control-label make-thinner">Theme</label>
<div class="col-sm-7"> <div class="col-sm-8">
<select id="theme" class="form-control" ng-model="settings.theme" ng-options="theme for theme in themes"></select> <select id="theme" class="form-control" ng-model="settings.theme" ng-options="theme for theme in themes"></select>
</div> </div>
</div> </div>
@ -380,13 +459,28 @@ npm run build-electron-{windows, darwin, linux}</pre>
<form class="form-horizontal" role="form"> <form class="form-horizontal" role="form">
<div class="form-group"> <div class="form-group">
<label for="custom-css" class="col-sm-3 control-label make-thinner">Custom CSS</label> <label for="custom-css" class="col-sm-3 control-label make-thinner">Custom CSS</label>
<div class="col-sm-7"> <div class="col-sm-8">
<textarea id="custom-css" class="form-control" ng-model="settings.customCSS"></textarea> <textarea id="custom-css" class="form-control" ng-model="settings.customCSS"></textarea>
</div> </div>
</div> </div>
</form> </form>
</li> </li>
<li class="standard-labels">
<form class="form-horizontal" role="form">
<div class="form-group">
<label class="control-label col-sm-3" for="iToken">Imgur Token <a target="_blank" href="https://github.com/glowing-bear/glowing-bear/wiki/Getting-an-imgur-token-&-album-hash"><i class="glyphicon glyphicon-info-sign"></i></a></label>
<div class="col-sm-4">
<input type="text" class="form-control" ng-model="settings.iToken" placeholder="Access Token" autocapitalize="off">
</div>
<label class="control-label col-sm-1 lessleftpad" for="iAlb">Album</label>
<div class="col-sm-3">
<input type="text" class="form-control" ng-model="settings.iAlb" placeholder="Album Hash" autocapitalize="off">
</div>
</div>
</form>
</li>
<li> <li>
<form class="form-inline" role="form"> <form class="form-inline" role="form">
<div class="checkbox"> <div class="checkbox">

@ -11,7 +11,7 @@ var bufferResume = angular.module('bufferResume', []);
bufferResume.service('bufferResume', ['settings', function(settings) { bufferResume.service('bufferResume', ['settings', function(settings) {
var resumer = {}; 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 // 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 // and reload it. If we cannot, we'll need to know so we can load the default

@ -20,15 +20,15 @@ weechat.factory('connection',
var locked = false; var locked = false;
// Takes care of the connection and websocket hooks // 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; $rootScope.passwordError = false;
connectionData = [host, port, passwd, ssl, noCompression]; connectionData = [host, port, path, passwd, ssl, noCompression];
var proto = ssl ? 'wss' : 'ws'; var proto = ssl ? 'wss' : 'ws';
// If host is an IPv6 literal wrap it in brackets // If host is an IPv6 literal wrap it in brackets
if (host.indexOf(":") !== -1 && host[0] !== "[" && host[host.length-1] !== "]") { if (host.indexOf(":") !== -1 && host[0] !== "[" && host[host.length-1] !== "]") {
host = "[" + host + "]"; host = "[" + host + "]";
} }
var url = proto + "://" + host + ":" + port + "/weechat"; var url = proto + "://" + host + ":" + port + "/" + path;
$log.debug('Connecting to URL: ', url); $log.debug('Connecting to URL: ', url);
var onopen = function () { var onopen = function () {
@ -45,7 +45,9 @@ weechat.factory('connection',
ngWebsockets.send( ngWebsockets.send(
weeChat.Protocol.formatInit({ weeChat.Protocol.formatInit({
password: passwd, 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) { 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...'); $log.info('Attempting to reconnect...');
var d = connectionData; 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; $rootScope.reconnecting = false;
// on success, update active buffer // on success, update active buffer
models.setActiveBuffer(bufferId); models.setActiveBuffer(bufferId);

@ -19,8 +19,8 @@ weechat.config(['$compileProvider', function ($compileProvider) {
} }
}]); }]);
weechat.controller('WeechatCtrl', ['$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, $log, models, bufferResume, connection, notifications, utils, settings) function ($rootScope, $scope, $store, $timeout, $location, $log, models, bufferResume, connection, notifications, utils, settings)
{ {
window.openBuffer = function(channel) { 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. // or else they won't be saved to the localStorage.
settings.setDefaults({ settings.setDefaults({
'theme': 'dark', 'theme': 'dark',
'host': 'localhost', 'hostField': 'localhost',
'port': 9001, 'port': 9001,
'path': 'weechat',
'ssl': (window.location.protocol === "https:"), 'ssl': (window.location.protocol === "https:"),
'useTotp': false,
'savepassword': false, 'savepassword': false,
'autoconnect': false, 'autoconnect': false,
'nonicklist': utils.isMobileUi(), 'nonicklist': utils.isMobileUi(),
@ -62,9 +64,17 @@ weechat.controller('WeechatCtrl', ['$rootScope', '$scope', '$store', '$timeout',
'enableQuickKeys': true, 'enableQuickKeys': true,
'customCSS': '', 'customCSS': '',
"currentlyViewedBuffers":{}, "currentlyViewedBuffers":{},
'iToken': '',
'iAlb': '',
}); });
$scope.settings = settings; $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 () { $rootScope.countWatchers = function () {
$log.debug($rootScope.$$watchersCount); $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, // Show a TLS warning if GB was loaded over an unencrypted connection,
// except for local instances (testing, cordova, or electron) // except for local instances (local files, testing, cordova, or electron)
$scope.show_tls_warning = (window.location.protocol !== "https:") && $scope.show_tls_warning = (["https:", "file:"].indexOf(window.location.protocol) === -1) &&
(["localhost", "127.0.0.1", "::1"].indexOf(window.location.hostname) === -1) && (["localhost", "127.0.0.1", "::1"].indexOf(window.location.hostname) === -1) &&
!window.is_electron && !utils.isCordova(); !window.is_electron && !utils.isCordova();
@ -149,6 +159,9 @@ weechat.controller('WeechatCtrl', ['$rootScope', '$scope', '$store', '$timeout',
}, false); }, false);
} }
$rootScope.$on('nickListChanged', function() {
$scope.updateShowNicklist();
});
$rootScope.$on('activeBufferChanged', function(event, unreadSum) { $rootScope.$on('activeBufferChanged', function(event, unreadSum) {
var ab = models.getActiveBuffer(); 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 (!settings.fontfamily) {
if (utils.isMobileUi()) { if (utils.isMobileUi()) {
settings.fontfamily = 'sans-serif'; settings.fontfamily = 'sans-serif';
@ -573,12 +581,10 @@ weechat.controller('WeechatCtrl', ['$rootScope', '$scope', '$store', '$timeout',
// Recalculation fails when not connected // Recalculation fails when not connected
if ($rootScope.connected) { if ($rootScope.connected) {
// Show the sidebar if switching away from mobile view, hide it when switching to mobile // 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 (!utils.isMobileUi()) {
if ($scope.wasMobileUi && !utils.isMobileUi()) {
$scope.showSidebar(); $scope.showSidebar();
$scope.updateShowNicklist(); $scope.updateShowNicklist();
} }
$scope.wasMobileUi = utils.isMobileUi();
$scope.calculateNumLines(); $scope.calculateNumLines();
// if we're scrolled to the bottom, scroll down to the same position after the resize // 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.bufferBottom = eob.offsetTop <= bl.scrollTop + bl.clientHeight;
}; };
$rootScope.scrollWithBuffer = function(scrollToReadmarker, moreLines) { $rootScope.scrollWithBuffer = function(scrollToReadmarker, moreLines) {
// First, get scrolling status *before* modification // First, get scrolling status *before* modification
// This is required to determine where we were in the buffer pre-change // 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); 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() { $scope.connect = function() {
notifications.requestNotificationPermission(); notifications.requestNotificationPermission();
$rootScope.sslError = false; $rootScope.sslError = false;
$rootScope.securityError = false; $rootScope.securityError = false;
@ -662,14 +737,17 @@ weechat.controller('WeechatCtrl', ['$rootScope', '$scope', '$store', '$timeout',
$rootScope.bufferBottom = true; $rootScope.bufferBottom = true;
$scope.connectbutton = 'Connecting'; $scope.connectbutton = 'Connecting';
$scope.connectbuttonicon = 'glyphicon-refresh glyphicon-spin'; $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.disconnect = function() {
$scope.connectbutton = 'Connect'; $scope.connectbutton = 'Connect';
$scope.connectbuttonicon = 'glyphicon-chevron-right'; $scope.connectbuttonicon = 'glyphicon-chevron-right';
bufferResume.reset(); bufferResume.reset();
connection.disconnect(); connection.disconnect();
}; };
$scope.reconnect = function() { $scope.reconnect = function() {
var bufferId = models.getActiveBuffer().id; var bufferId = models.getActiveBuffer().id;
connection.attemptReconnect(bufferId, 3000); connection.attemptReconnect(bufferId, 3000);
@ -678,6 +756,7 @@ weechat.controller('WeechatCtrl', ['$rootScope', '$scope', '$store', '$timeout',
$scope.showModal = function(elementId) { $scope.showModal = function(elementId) {
document.getElementById(elementId).setAttribute('data-state', 'visible'); document.getElementById(elementId).setAttribute('data-state', 'visible');
}; };
$scope.closeModal = function($event) { $scope.closeModal = function($event) {
function closest(elem, selector) { function closest(elem, selector) {
var matchesSelector = elem.matches || elem.webkitMatchesSelector || elem.mozMatchesSelector || elem.msMatchesSelector; var matchesSelector = elem.matches || elem.webkitMatchesSelector || elem.mozMatchesSelector || elem.msMatchesSelector;
@ -776,7 +855,7 @@ weechat.controller('WeechatCtrl', ['$rootScope', '$scope', '$store', '$timeout',
$scope.updateShowNicklist = function() { $scope.updateShowNicklist = function() {
var ab = models.getActiveBuffer(); var ab = models.getActiveBuffer();
// Check whether buffer exists and nicklist is non-empty // Check whether buffer exists and nicklist is non-empty
if (!ab || ab.isNicklistEmpty()) { if (!ab || !ab.nicklistRequested() || ab.isNicklistEmpty()) {
$scope.showNicklist = false; $scope.showNicklist = false;
return false; return false;
} }
@ -923,34 +1002,29 @@ weechat.controller('WeechatCtrl', ['$rootScope', '$scope', '$store', '$timeout',
} }
}; };
window.onhashchange = function() {
$scope.parseHash();
};
$scope.init = function() { $scope.init = function() {
if (window.location.hash) { $scope.parseHost();
var rawStr = atob(window.location.hash.substring(1)); $scope.parseHash();
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);
}
}; };
}]); }]);
weechat.config(['$routeProvider', weechat.config(['$routeProvider', '$locationProvider',
function($routeProvider) { function($routeProvider, $locationProvider) {
$routeProvider.when('/', { $routeProvider.when('', {
templateUrl: 'index.html', templateUrl: 'index.html',
controller: 'WeechatCtrl' controller: 'WeechatCtrl'
}); });
//remove hashbang from url
$locationProvider.html5Mode({
enabled: true,
requireBase: false
});
} }
]); ]);

@ -168,7 +168,7 @@ weechat.factory('handlers', ['$rootScope', '$log', 'models', 'plugins', 'notific
$rootScope.$emit('notificationChanged'); $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++; buffer.notification++;
server.unread++; server.unread++;
notifications.createHighlight(buffer, message); notifications.createHighlight(buffer, message);
@ -431,12 +431,24 @@ weechat.factory('handlers', ['$rootScope', '$log', 'models', 'plugins', 'notific
/* /*
* Handle nicklist event * Handle nicklist event
*
* This event can either fill or clear a nicklist. It is always a complete nicklist.
*/ */
var handleNicklist = function(message) { var handleNicklist = function(message) {
var nicklist = message.objects[0].content; var nicklist = message.objects[0].content;
var group = 'root'; 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) { nicklist.forEach(function(n) {
var buffer = models.getBuffer(n.pointers[0]); var buffer = models.getBuffer(n.pointers[0]);
//buffer nicklist
if (n.group === 1) { if (n.group === 1) {
var g = new models.NickGroup(n); var g = new models.NickGroup(n);
group = g.name; group = g.name;
@ -446,6 +458,9 @@ weechat.factory('handlers', ['$rootScope', '$log', 'models', 'plugins', 'notific
buffer.addNick(group, nick); buffer.addNick(group, nick);
} }
}); });
//check if nicklist should be hidden or not
$rootScope.$emit('nickListChanged');
}; };
/* /*
* Handle nicklist diff event * Handle nicklist diff event

@ -3,7 +3,7 @@
var weechat = angular.module('weechat'); var weechat = angular.module('weechat');
weechat.factory('imgur', ['$rootScope', function($rootScope) { weechat.factory('imgur', ['$rootScope', 'settings', function($rootScope, settings) {
var process = function(image, callback) { var process = function(image, callback) {
@ -26,8 +26,18 @@ weechat.factory('imgur', ['$rootScope', function($rootScope) {
// Upload image to imgur from base64 // Upload image to imgur from base64
var upload = function( base64img, callback ) { 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 // Progress bars container
var progressBars = document.getElementById("imgur-upload-progress"), 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("image", base64img); // Append the file
fd.append("type", "base64"); // Set image type to base64 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 // Create new XMLHttpRequest
var xhttp = 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); xhttp.open("POST", "https://api.imgur.com/3/image", true);
// Set headers // 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"); xhttp.setRequestHeader("Accept", "application/json");
// Handler for response // Handler for response

@ -217,6 +217,18 @@ weechat.directive('inputBar', function() {
// Extract nick from bufferline prefix // Extract nick from bufferline prefix
var nick = prefix[prefix.length - 1].text; 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 newValue = $scope.command || ''; // can be undefined, in that case, use the empty string
var addColon = newValue.length === 0; var addColon = newValue.length === 0;
if (newValue.length > 0) { if (newValue.length > 0) {

@ -119,7 +119,7 @@ IrcUtils.service('IrcUtils', [function() {
suf = ':'; suf = ':';
} }
// addSpace defaults to true // addSpace defaults to true
var addSpaceChar = (addSpace === undefined || addSpace === true) ? ' ' : ''; var addSpaceChar = (addSpace === undefined || addSpace === 'on') ? ' ' : '';
// new nick list to search in // new nick list to search in
var searchNickList = _ciNickList(nickList); var searchNickList = _ciNickList(nickList);
@ -144,7 +144,11 @@ IrcUtils.service('IrcUtils', [function() {
if (doIterate) { if (doIterate) {
// try iterating // try iterating
newNick = _nextNick(iterCandidate, m[1], searchNickList); newNick = _nextNick(iterCandidate, m[1], searchNickList);
beforeCaret = newNick + suf + ' '; if (suf.endsWith(' ')) {
beforeCaret = newNick + suf;
} else {
beforeCaret = newNick + suf + ' ';
}
return { return {
text: beforeCaret + afterCaret, text: beforeCaret + afterCaret,
caretPos: beforeCaret.length, caretPos: beforeCaret.length,
@ -166,7 +170,11 @@ IrcUtils.service('IrcUtils', [function() {
// no match // no match
return ret; return ret;
} }
beforeCaret = newNick + suf + ' '; if (suf.endsWith(' ')) {
beforeCaret = newNick + suf;
} else {
beforeCaret = newNick + suf + ' ';
}
if (afterCaret[0] === ' ') { if (afterCaret[0] === ' ') {
// swallow first space after caret if any // swallow first space after caret if any
afterCaret = afterCaret.substring(1); afterCaret = afterCaret.substring(1);

@ -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 * Updates a nick in nicklist
*/ */
@ -296,6 +308,19 @@ models.service('models', ['$rootScope', '$filter', 'bufferResume', function($roo
return nicklist.hasOwnProperty('root'); 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 */ /* Clear all our buffer lines */
var clear = function() { var clear = function() {
while(lines.length > 0) { while(lines.length > 0) {
@ -325,6 +350,7 @@ models.service('models', ['$rootScope', '$filter', 'bufferResume', function($roo
nicklist: nicklist, nicklist: nicklist,
addNick: addNick, addNick: addNick,
delNick: delNick, delNick: delNick,
clearNicklist: clearNicklist,
updateNick: updateNick, updateNick: updateNick,
getNicklistByTime: getNicklistByTime, getNicklistByTime: getNicklistByTime,
serverSortKey: serverSortKey, serverSortKey: serverSortKey,
@ -340,6 +366,7 @@ models.service('models', ['$rootScope', '$filter', 'bufferResume', function($roo
isNicklistEmpty: isNicklistEmpty, isNicklistEmpty: isNicklistEmpty,
nicklistRequested: nicklistRequested, nicklistRequested: nicklistRequested,
pinned: pinned, pinned: pinned,
queryNicklist: queryNicklist,
}; };
}; };

@ -463,8 +463,10 @@ plugins.factory('userPlugins', function() {
jsonp(url, function(data) { jsonp(url, function(data) {
// Add the gist stylesheet only once // Add the gist stylesheet only once
if (document.querySelectorAll('link[rel=stylesheet][href="' + data.stylesheet + '"]').length < 1) { if (document.querySelectorAll('link[rel=stylesheet][href="' + data.stylesheet + '"]').length < 1) {
var stylesheet = '<link rel="stylesheet" href="' + data.stylesheet + '"></link>'; var stylesheet = document.createElement("link");
document.getElementsByTagName('head')[0].innerHTML += stylesheet; stylesheet.href = data.stylesheet;
stylesheet.setAttribute('rel', 'stylesheet');
document.head.appendChild(stylesheet);
} }
element.innerHTML = '<div style="clear:both">' + data.div + '</div>'; element.innerHTML = '<div style="clear:both">' + data.div + '</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('<iframe></iframe>')
.addClass('vine-embed')
.attr('src', embedurl)
.attr('width', '600')
.attr('height', '600')
.attr('frameborder', '0');
return element.prop('outerHTML') + '<script async src="https://platform.vine.co/static/scripts/embed.js" charset="utf-8"></script>';
}
});
/* /*
* Streamable Embedded Player * Streamable Embedded Player
*/ */
@ -570,7 +554,7 @@ plugins.factory('userPlugins', function() {
}); });
return { 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]
}; };

@ -363,7 +363,8 @@
// "*" + (A)STD + "," + EXT // "*" + (A)STD + "," + EXT
// "*" + (A)EXT + "," + STD // "*" + (A)EXT + "," + STD
// "*" + (A)EXT + "," + EXT // "*" + (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) { fn: function(m) {
var ret = {}; var ret = {};
@ -647,6 +648,9 @@
if (params.password !== null) { if (params.password !== null) {
keys.push('password=' + params.password); keys.push('password=' + params.password);
} }
if (params.useTotp) {
keys.push('totp=' + params.totp);
}
parts.push(keys.join(',')); parts.push(keys.join(','));
return WeeChatProtocol._formatCmd(null, 'init', parts); return WeeChatProtocol._formatCmd(null, 'init', parts);

@ -8,17 +8,17 @@
"license": "GPLv3", "license": "GPLv3",
"devDependencies": { "devDependencies": {
"bower": "^1.8.8", "bower": "^1.8.8",
"electron-packager": "^12.2.0", "electron-packager": "~14.2",
"http-server": "^0.11", "http-server": "^0.12.0",
"jasmine-core": "^3.1", "jasmine-core": "^3.5.0",
"jshint": "^2.9.6", "jshint": "^2.11.0",
"karma": "^4.2.0", "karma": "~4.4",
"karma-jasmine": "~1.1", "karma-jasmine": "~3.1",
"karma-junit-reporter": "^1.2", "karma-junit-reporter": "~2.0",
"karma-phantomjs-launcher": "^1.0.0", "karma-phantomjs-launcher": "^1.0.0",
"protractor": "^5.4.1", "protractor": "^5.4.2",
"shelljs": "^0.8.0", "shelljs": "^0.8.0",
"uglify-js": "^3.4.9" "uglify-js": "^3.6.9"
}, },
"scripts": { "scripts": {
"postinstall": "bower install -p", "postinstall": "bower install -p",

@ -169,14 +169,5 @@ describe('filter', function() {
plugins); plugins);
})); }));
it('should recognize vines', inject(function(plugins) {
expectTheseMessagesToContain([
'https://vine.co/v/hWh262H9HM5',
'https://vine.co/v/hWh262H9HM5/embed',
],
'Vine',
plugins);
}));
}); });
}); });

Loading…
Cancel
Save