This merge includes a minification run Conflicts: index.htmlgh-pages
commit
a28a304e60
@ -1,8 +1,11 @@ |
||||
{ |
||||
"browser": true, |
||||
"devel": true, |
||||
"globals": { |
||||
"angular": false, |
||||
"$": false, |
||||
"window": false, |
||||
"console": false |
||||
"weeChat": false, |
||||
"_": false, |
||||
"Notification": false, |
||||
"Favico": false |
||||
} |
||||
} |
||||
|
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,276 @@ |
||||
(function() { |
||||
'use strict'; |
||||
|
||||
var weechat = angular.module('weechat'); |
||||
|
||||
weechat.factory('connection', |
||||
['$rootScope', '$log', 'handlers', 'models', 'ngWebsockets', function($rootScope, |
||||
$log, |
||||
handlers, |
||||
models, |
||||
ngWebsockets) { |
||||
|
||||
var protocol = new weeChat.Protocol(); |
||||
|
||||
// Takes care of the connection and websocket hooks
|
||||
|
||||
var connect = function (host, port, passwd, ssl, noCompression) { |
||||
var proto = ssl ? 'wss' : 'ws'; |
||||
// If host is an IPv6 literal wrap it in brackets
|
||||
if (host.indexOf(":") !== -1) { |
||||
host = "[" + host + "]"; |
||||
} |
||||
var url = proto + "://" + host + ":" + port + "/weechat"; |
||||
$log.debug('Connecting to URL: ', url); |
||||
|
||||
var onopen = function () { |
||||
|
||||
|
||||
// Helper methods for initialization commands
|
||||
var _initializeConnection = function(passwd) { |
||||
// This is not the proper way to do this.
|
||||
// WeeChat does not send a confirmation for the init.
|
||||
// Until it does, We need to "assume" that formatInit
|
||||
// will be received before formatInfo
|
||||
ngWebsockets.send( |
||||
weeChat.Protocol.formatInit({ |
||||
password: passwd, |
||||
compression: noCompression ? 'off' : 'zlib' |
||||
}) |
||||
); |
||||
|
||||
return ngWebsockets.send( |
||||
weeChat.Protocol.formatInfo({ |
||||
name: 'version' |
||||
}) |
||||
); |
||||
}; |
||||
|
||||
var _requestHotlist = function() { |
||||
return ngWebsockets.send( |
||||
weeChat.Protocol.formatHdata({ |
||||
path: "hotlist:gui_hotlist(*)", |
||||
keys: [] |
||||
}) |
||||
); |
||||
}; |
||||
|
||||
var _requestBufferInfos = function() { |
||||
return ngWebsockets.send( |
||||
weeChat.Protocol.formatHdata({ |
||||
path: 'buffer:gui_buffers(*)', |
||||
keys: ['local_variables,notify,number,full_name,short_name,title'] |
||||
}) |
||||
); |
||||
}; |
||||
|
||||
var _requestSync = function() { |
||||
return ngWebsockets.send( |
||||
weeChat.Protocol.formatSync({}) |
||||
); |
||||
}; |
||||
|
||||
|
||||
// First command asks for the password and issues
|
||||
// a version command. If it fails, it means the we
|
||||
// did not provide the proper password.
|
||||
_initializeConnection(passwd).then( |
||||
function() { |
||||
// Connection is successful
|
||||
// Send all the other commands required for initialization
|
||||
_requestBufferInfos().then(function(bufinfo) { |
||||
//XXX move to handlers?
|
||||
var bufferInfos = bufinfo.objects[0].content; |
||||
// buffers objects
|
||||
for (var i = 0; i < bufferInfos.length ; i++) { |
||||
var buffer = new models.Buffer(bufferInfos[i]); |
||||
models.addBuffer(buffer); |
||||
// Switch to first buffer on startup
|
||||
if (i === 0) { |
||||
models.setActiveBuffer(buffer.id); |
||||
} |
||||
} |
||||
}); |
||||
|
||||
_requestHotlist().then(function(hotlist) { |
||||
handlers.handleHotlistInfo(hotlist); |
||||
}); |
||||
|
||||
_requestSync(); |
||||
$log.info("Connected to relay"); |
||||
$rootScope.connected = true; |
||||
}, |
||||
function() { |
||||
// Connection got closed, lets check if we ever was connected successfully
|
||||
if (!$rootScope.waseverconnected) { |
||||
$rootScope.passwordError = true; |
||||
} |
||||
} |
||||
); |
||||
|
||||
}; |
||||
|
||||
var onmessage = function() { |
||||
// If we recieve a message from WeeChat it means that
|
||||
// password was OK. Store that result and check for it
|
||||
// in the failure handler.
|
||||
$rootScope.waseverconnected = true; |
||||
}; |
||||
|
||||
|
||||
var onclose = function (evt) { |
||||
/* |
||||
* Handles websocket disconnection |
||||
*/ |
||||
$log.info("Disconnected from relay"); |
||||
ngWebsockets.failCallbacks('disconnection'); |
||||
$rootScope.connected = false; |
||||
$rootScope.$emit('relayDisconnect'); |
||||
if (ssl && evt.code === 1006) { |
||||
// A password error doesn't trigger onerror, but certificate issues do. Check time of last error.
|
||||
if (typeof $rootScope.lastError !== "undefined" && (Date.now() - $rootScope.lastError) < 1000) { |
||||
// abnormal disconnect by client, most likely ssl error
|
||||
$rootScope.sslError = true; |
||||
} |
||||
} |
||||
$rootScope.$apply(); |
||||
}; |
||||
|
||||
var onerror = function (evt) { |
||||
/* |
||||
* Handles cases when connection issues come from |
||||
* the relay. |
||||
*/ |
||||
$log.error("Relay error", evt); |
||||
$rootScope.lastError = Date.now(); |
||||
|
||||
if (evt.type === "error" && this.readyState !== 1) { |
||||
ngWebsockets.failCallbacks('error'); |
||||
$rootScope.errorMessage = true; |
||||
} |
||||
}; |
||||
|
||||
try { |
||||
ngWebsockets.connect(url, |
||||
protocol, |
||||
{ |
||||
'binaryType': "arraybuffer", |
||||
'onopen': onopen, |
||||
'onclose': onclose, |
||||
'onmessage': onmessage, |
||||
'onerror': onerror |
||||
}); |
||||
} catch(e) { |
||||
$log.debug("Websocket caught DOMException:", e); |
||||
$rootScope.lastError = Date.now(); |
||||
$rootScope.errorMessage = true; |
||||
$rootScope.securityError = true; |
||||
$rootScope.$emit('relayDisconnect'); |
||||
} |
||||
|
||||
}; |
||||
|
||||
var disconnect = function() { |
||||
ngWebsockets.send(weeChat.Protocol.formatQuit()); |
||||
}; |
||||
|
||||
/* |
||||
* Format and send a weechat message |
||||
* |
||||
* @returns the angular promise |
||||
*/ |
||||
var sendMessage = function(message) { |
||||
ngWebsockets.send(weeChat.Protocol.formatInput({ |
||||
buffer: models.getActiveBuffer().fullName, |
||||
data: message |
||||
})); |
||||
}; |
||||
|
||||
var sendCoreCommand = function(command) { |
||||
ngWebsockets.send(weeChat.Protocol.formatInput({ |
||||
buffer: 'core.weechat', |
||||
data: command |
||||
})); |
||||
}; |
||||
|
||||
|
||||
var requestNicklist = function(bufferId, callback) { |
||||
bufferId = bufferId || null; |
||||
ngWebsockets.send( |
||||
weeChat.Protocol.formatNicklist({ |
||||
buffer: bufferId |
||||
}) |
||||
).then(function(nicklist) { |
||||
handlers.handleNicklist(nicklist); |
||||
if (callback !== undefined) { |
||||
callback(); |
||||
} |
||||
}); |
||||
}; |
||||
|
||||
|
||||
var fetchMoreLines = function(numLines) { |
||||
$log.debug('Fetching ', numLines, ' lines'); |
||||
var buffer = models.getActiveBuffer(); |
||||
if (numLines === undefined) { |
||||
// Math.max(undefined, *) = NaN -> need a number here
|
||||
numLines = 0; |
||||
} |
||||
// Calculate number of lines to fetch, at least as many as the parameter
|
||||
numLines = Math.max(numLines, buffer.requestedLines * 2); |
||||
|
||||
// Indicator that we are loading lines, hides "load more lines" link
|
||||
$rootScope.loadingLines = true; |
||||
// Send hdata request to fetch lines for this particular buffer
|
||||
return ngWebsockets.send( |
||||
weeChat.Protocol.formatHdata({ |
||||
// "0x" is important, otherwise it won't work
|
||||
path: "buffer:0x" + buffer.id + "/own_lines/last_line(-" + numLines + ")/data", |
||||
keys: [] |
||||
}) |
||||
).then(function(lineinfo) { |
||||
//XXX move to handlers?
|
||||
// delete old lines and add new ones
|
||||
var oldLength = buffer.lines.length; |
||||
// whether we already had all unread lines
|
||||
var hadAllUnreadLines = buffer.lastSeen >= 0; |
||||
|
||||
// clear the old lines
|
||||
buffer.lines.length = 0; |
||||
// We need to set the number of requested lines to 0 here, because parsing a line
|
||||
// increments it. This is needed to also count newly arriving lines while we're
|
||||
// already connected.
|
||||
buffer.requestedLines = 0; |
||||
// Count number of lines recieved
|
||||
var linesReceivedCount = lineinfo.objects[0].content.length; |
||||
|
||||
// Parse the lines
|
||||
handlers.handleLineInfo(lineinfo, true); |
||||
|
||||
// Correct the read marker for the lines that were counted twice
|
||||
buffer.lastSeen -= oldLength; |
||||
|
||||
// We requested more lines than we got, no more lines.
|
||||
if (linesReceivedCount < numLines) { |
||||
buffer.allLinesFetched = true; |
||||
} |
||||
$rootScope.loadingLines = false; |
||||
|
||||
// Only scroll to read marker if we didn't have all unread lines previously, but have them now
|
||||
var scrollToReadmarker = !hadAllUnreadLines && buffer.lastSeen >= 0; |
||||
// Scroll to correct position
|
||||
$rootScope.scrollWithBuffer(scrollToReadmarker, true); |
||||
}); |
||||
}; |
||||
|
||||
|
||||
return { |
||||
connect: connect, |
||||
disconnect: disconnect, |
||||
sendMessage: sendMessage, |
||||
sendCoreCommand: sendCoreCommand, |
||||
fetchMoreLines: fetchMoreLines, |
||||
requestNicklist: requestNicklist |
||||
}; |
||||
}]); |
||||
})(); |
@ -0,0 +1,128 @@ |
||||
(function() { |
||||
'use strict'; |
||||
|
||||
var weechat = angular.module('weechat'); |
||||
|
||||
weechat.filter('toArray', function () { |
||||
return function (obj, storeIdx) { |
||||
if (!(obj instanceof Object)) { |
||||
return obj; |
||||
} |
||||
|
||||
if (storeIdx) { |
||||
return Object.keys(obj).map(function (key, idx) { |
||||
return Object.defineProperties(obj[key], { |
||||
'$key' : { value: key }, |
||||
'$idx' : { value: idx, configurable: true } |
||||
}); |
||||
}); |
||||
} |
||||
|
||||
return Object.keys(obj).map(function (key) { |
||||
return Object.defineProperty(obj[key], '$key', { value: key }); |
||||
}); |
||||
}; |
||||
}); |
||||
|
||||
weechat.filter('irclinky', ['$filter', function($filter) { |
||||
return function(text) { |
||||
if (!text) { |
||||
return text; |
||||
} |
||||
|
||||
// This regex in no way matches all IRC channel names (they could also begin with &, + or an
|
||||
// exclamation mark followed by 5 alphanumeric characters, and are bounded in length by 50).
|
||||
// However, it matches all *common* IRC channels while trying to minimise false positives.
|
||||
// "#1" is much more likely to be "number 1" than "IRC channel #1".
|
||||
// Thus, we only match channels beginning with a # and having at least one letter in them.
|
||||
var channelRegex = /(^|[\s,.:;?!"'()+@-])(#+[^\x00\x07\r\n\s,:]*[a-z][^\x00\x07\r\n\s,:]*)/gmi; |
||||
// This is SUPER nasty, but ng-click does not work inside a filter, as the markup has to be $compiled first, which is not possible in filter afaik.
|
||||
// Therefore, get the scope, fire the method, and $apply. Yuck. I sincerely hope someone finds a better way of doing this.
|
||||
var substitute = '$1<a href="#" onclick="var $scope = angular.element(event.target).scope(); $scope.openBuffer(\'$2\'); $scope.$apply();">$2</a>'; |
||||
return text.replace(channelRegex, substitute); |
||||
}; |
||||
}]); |
||||
|
||||
weechat.filter('inlinecolour', function() { |
||||
return function(text) { |
||||
if (!text) { |
||||
return text; |
||||
} |
||||
|
||||
// only match 6-digit colour codes, 3-digit ones have too many false positives (issue numbers, etc)
|
||||
var hexColourRegex = /(^|[^&])\#([0-9a-f]{6})($|[^\w'"])/gmi; |
||||
var substitute = '$1#$2 <div class="colourbox" style="background-color:#$2"></div> $3'; |
||||
|
||||
return text.replace(hexColourRegex, substitute); |
||||
}; |
||||
}); |
||||
|
||||
// apply a filter to an HTML string's text nodes, and do so with not exceedingly terrible performance
|
||||
weechat.filter('DOMfilter', ['$filter', '$sce', function($filter, $sce) { |
||||
return function(text, filter) { |
||||
if (!text || !filter) { |
||||
return text; |
||||
} |
||||
|
||||
var filterFunction = $filter(filter); |
||||
var el = document.createElement('div'); |
||||
el.innerHTML = text; |
||||
|
||||
// Recursive DOM-walking function applying the filter to the text nodes
|
||||
var process = function(node) { |
||||
if (node.nodeType === 3) { // text node
|
||||
var value = filterFunction(node.nodeValue); |
||||
if (value !== node.nodeValue) { |
||||
// we changed something. create a new node to replace the current one
|
||||
// we could also only add its children but that would probably incur
|
||||
// more overhead than it would gain us
|
||||
var newNode = document.createElement('span'); |
||||
newNode.innerHTML = value; |
||||
|
||||
var parent = node.parentNode; |
||||
var sibling = node.nextSibling; |
||||
parent.removeChild(node); |
||||
if (sibling) { |
||||
parent.insertBefore(newNode, sibling); |
||||
} else { |
||||
parent.appendChild(newNode); |
||||
} |
||||
} |
||||
} |
||||
// recurse
|
||||
node = node.firstChild; |
||||
while (node) { |
||||
process(node); |
||||
node = node.nextSibling; |
||||
} |
||||
}; |
||||
|
||||
process(el); |
||||
|
||||
return $sce.trustAsHtml(el.innerHTML); |
||||
}; |
||||
}]); |
||||
|
||||
weechat.filter('getBufferQuickKeys', function () { |
||||
return function (obj, $scope) { |
||||
if (!$scope) { return obj; } |
||||
if (($scope.search !== undefined && $scope.search.length) || $scope.onlyUnread) { |
||||
obj.forEach(function(buf, idx) { |
||||
buf.$quickKey = idx < 10 ? (idx + 1) % 10 : ''; |
||||
}); |
||||
} else { |
||||
_.map(obj, function(buffer, idx) { |
||||
return [buffer.number, buffer.$idx, idx]; |
||||
}).sort(function(left, right) { |
||||
// By default, Array.prototype.sort() sorts alphabetically.
|
||||
// Pass an ordering function to sort by first element.
|
||||
return left[0] - right[0] || left[1] - right[1]; |
||||
}).forEach(function(info, keyIdx) { |
||||
obj[ info[2] ].$quickKey = keyIdx < 10 ? (keyIdx + 1) % 10 : ''; |
||||
}); |
||||
} |
||||
return obj; |
||||
}; |
||||
}); |
||||
|
||||
})(); |
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,213 @@ |
||||
(function() { |
||||
'use strict'; |
||||
|
||||
var weechat = angular.module('weechat'); |
||||
|
||||
weechat.factory('handlers', ['$rootScope', '$log', 'models', 'plugins', 'notifications', function($rootScope, $log, models, plugins, notifications) { |
||||
|
||||
var handleBufferClosing = function(message) { |
||||
var bufferMessage = message.objects[0].content[0]; |
||||
var bufferId = bufferMessage.pointers[0]; |
||||
models.closeBuffer(bufferId); |
||||
}; |
||||
|
||||
var handleLine = function(line, manually) { |
||||
var message = new models.BufferLine(line); |
||||
var buffer = models.getBuffer(message.buffer); |
||||
buffer.requestedLines++; |
||||
// Only react to line if its displayed
|
||||
if (message.displayed) { |
||||
message = plugins.PluginManager.contentForMessage(message); |
||||
buffer.addLine(message); |
||||
|
||||
if (manually) { |
||||
buffer.lastSeen++; |
||||
} |
||||
|
||||
if (buffer.active && !manually) { |
||||
$rootScope.scrollWithBuffer(); |
||||
} |
||||
|
||||
if (!manually && (!buffer.active || !$rootScope.isWindowFocused())) { |
||||
if (buffer.notify > 1 && _.contains(message.tags, 'notify_message') && !_.contains(message.tags, 'notify_none')) { |
||||
buffer.unread++; |
||||
$rootScope.$emit('notificationChanged'); |
||||
} |
||||
|
||||
if ((buffer.notify !== 0 && message.highlight) || _.contains(message.tags, 'notify_private')) { |
||||
buffer.notification++; |
||||
notifications.createHighlight(buffer, message); |
||||
$rootScope.$emit('notificationChanged'); |
||||
} |
||||
} |
||||
} |
||||
}; |
||||
|
||||
var handleBufferLineAdded = function(message) { |
||||
message.objects[0].content.forEach(function(l) { |
||||
handleLine(l, false); |
||||
}); |
||||
}; |
||||
|
||||
var handleBufferOpened = function(message) { |
||||
var bufferMessage = message.objects[0].content[0]; |
||||
var buffer = new models.Buffer(bufferMessage); |
||||
models.addBuffer(buffer); |
||||
/* Until we can decide if user asked for this buffer to be opened |
||||
* or not we will let user click opened buffers. |
||||
models.setActiveBuffer(buffer.id); |
||||
*/ |
||||
}; |
||||
|
||||
var handleBufferTitleChanged = function(message) { |
||||
var obj = message.objects[0].content[0]; |
||||
var buffer = obj.pointers[0]; |
||||
var old = models.getBuffer(buffer); |
||||
old.fullName = obj.full_name; |
||||
old.title = obj.title; |
||||
old.number = obj.number; |
||||
}; |
||||
|
||||
var handleBufferRenamed = function(message) { |
||||
var obj = message.objects[0].content[0]; |
||||
var buffer = obj.pointers[0]; |
||||
var old = models.getBuffer(buffer); |
||||
old.fullName = obj.full_name; |
||||
old.shortName = obj.short_name; |
||||
old.trimmedName = obj.short_name.replace(/^[#&+]/, ''); |
||||
}; |
||||
|
||||
var handleBufferLocalvarChanged = function(message) { |
||||
var obj = message.objects[0].content[0]; |
||||
var buffer = obj.pointers[0]; |
||||
var old = models.getBuffer(buffer); |
||||
|
||||
var localvars = obj.local_variables; |
||||
if (old !== undefined && localvars !== undefined) { |
||||
// Update indentation status
|
||||
old.type = localvars.type; |
||||
old.indent = (['channel', 'private'].indexOf(localvars.type) >= 0); |
||||
} |
||||
}; |
||||
|
||||
/* |
||||
* Handle answers to (lineinfo) messages |
||||
* |
||||
* (lineinfo) messages are specified by this client. It is request after bufinfo completes |
||||
*/ |
||||
var handleLineInfo = function(message, manually) { |
||||
var lines = message.objects[0].content.reverse(); |
||||
if (manually === undefined) { |
||||
manually = true; |
||||
} |
||||
lines.forEach(function(l) { |
||||
handleLine(l, manually); |
||||
}); |
||||
}; |
||||
|
||||
/* |
||||
* Handle answers to hotlist request |
||||
*/ |
||||
var handleHotlistInfo = function(message) { |
||||
if (message.objects.length === 0) { |
||||
return; |
||||
} |
||||
var hotlist = message.objects[0].content; |
||||
hotlist.forEach(function(l) { |
||||
var buffer = models.getBuffer(l.buffer); |
||||
// 1 is message
|
||||
buffer.unread += l.count[1]; |
||||
// 2 is private
|
||||
buffer.notification += l.count[2]; |
||||
// 3 is highlight
|
||||
buffer.notification += l.count[3]; |
||||
/* Since there is unread messages, we can guess |
||||
* what the last read line is and update it accordingly |
||||
*/ |
||||
var unreadSum = _.reduce(l.count, function(memo, num) { return memo + num; }, 0); |
||||
buffer.lastSeen = buffer.lines.length - 1 - unreadSum; |
||||
}); |
||||
}; |
||||
|
||||
/* |
||||
* Handle nicklist event |
||||
*/ |
||||
var handleNicklist = function(message) { |
||||
var nicklist = message.objects[0].content; |
||||
var group = 'root'; |
||||
nicklist.forEach(function(n) { |
||||
var buffer = models.getBuffer(n.pointers[0]); |
||||
if (n.group === 1) { |
||||
var g = new models.NickGroup(n); |
||||
group = g.name; |
||||
buffer.nicklist[group] = g; |
||||
} else { |
||||
var nick = new models.Nick(n); |
||||
buffer.addNick(group, nick); |
||||
} |
||||
}); |
||||
}; |
||||
/* |
||||
* Handle nicklist diff event |
||||
*/ |
||||
var handleNicklistDiff = function(message) { |
||||
var nicklist = message.objects[0].content; |
||||
var group; |
||||
nicklist.forEach(function(n) { |
||||
var buffer = models.getBuffer(n.pointers[0]); |
||||
var d = n._diff; |
||||
if (n.group === 1) { |
||||
group = n.name; |
||||
if (group === undefined) { |
||||
var g = new models.NickGroup(n); |
||||
buffer.nicklist[group] = g; |
||||
group = g.name; |
||||
} |
||||
} else { |
||||
var nick = new models.Nick(n); |
||||
if (d === 43) { // +
|
||||
buffer.addNick(group, nick); |
||||
} else if (d === 45) { // -
|
||||
buffer.delNick(group, nick); |
||||
} else if (d === 42) { // *
|
||||
buffer.updateNick(group, nick); |
||||
} |
||||
} |
||||
}); |
||||
}; |
||||
|
||||
var eventHandlers = { |
||||
_buffer_closing: handleBufferClosing, |
||||
_buffer_line_added: handleBufferLineAdded, |
||||
_buffer_localvar_added: handleBufferLocalvarChanged, |
||||
_buffer_localvar_removed: handleBufferLocalvarChanged, |
||||
_buffer_opened: handleBufferOpened, |
||||
_buffer_title_changed: handleBufferTitleChanged, |
||||
_buffer_renamed: handleBufferRenamed, |
||||
_nicklist: handleNicklist, |
||||
_nicklist_diff: handleNicklistDiff |
||||
}; |
||||
|
||||
$rootScope.$on('onMessage', function(event, message) { |
||||
if (_.has(eventHandlers, message.id)) { |
||||
eventHandlers[message.id](message); |
||||
} else { |
||||
$log.debug('Unhandled event received: ' + message.id); |
||||
} |
||||
}); |
||||
|
||||
var handleEvent = function(event) { |
||||
if (_.has(eventHandlers, event.id)) { |
||||
eventHandlers[event.id](event); |
||||
} |
||||
}; |
||||
|
||||
return { |
||||
handleEvent: handleEvent, |
||||
handleLineInfo: handleLineInfo, |
||||
handleHotlistInfo: handleHotlistInfo, |
||||
handleNicklist: handleNicklist |
||||
}; |
||||
|
||||
}]); |
||||
})(); |
@ -0,0 +1,404 @@ |
||||
(function() { |
||||
'use strict'; |
||||
|
||||
var weechat = angular.module('weechat'); |
||||
|
||||
weechat.directive('inputBar', function() { |
||||
|
||||
return { |
||||
|
||||
templateUrl: 'directives/input.html', |
||||
|
||||
scope: { |
||||
inputId: '@inputId', |
||||
command: '=command' |
||||
}, |
||||
|
||||
controller: ['$rootScope', '$scope', '$element', '$log', 'connection', 'models', 'IrcUtils', function($rootScope, |
||||
$scope, |
||||
$element, //XXX do we need this? don't seem to be using it
|
||||
$log, |
||||
connection, //XXX we should eliminate this dependency and use signals instead
|
||||
models, |
||||
IrcUtils) { |
||||
|
||||
/* |
||||
* Returns the input element |
||||
*/ |
||||
$scope.getInputNode = function() { |
||||
return document.querySelector('textarea#' + $scope.inputId); |
||||
}; |
||||
|
||||
$scope.hideSidebar = function() { |
||||
$rootScope.hideSidebar(); |
||||
}; |
||||
|
||||
$scope.completeNick = function() { |
||||
// input DOM node
|
||||
var inputNode = $scope.getInputNode(); |
||||
|
||||
// get current caret position
|
||||
var caretPos = inputNode.selectionStart; |
||||
|
||||
// get current active buffer
|
||||
var activeBuffer = models.getActiveBuffer(); |
||||
|
||||
// Empty input makes $scope.command undefined -- use empty string instead
|
||||
var input = $scope.command || ''; |
||||
|
||||
// complete nick
|
||||
var nickComp = IrcUtils.completeNick(input, caretPos, $scope.iterCandidate, |
||||
activeBuffer.getNicklistByTime(), ':'); |
||||
|
||||
// remember iteration candidate
|
||||
$scope.iterCandidate = nickComp.iterCandidate; |
||||
|
||||
// update current input
|
||||
$scope.command = nickComp.text; |
||||
|
||||
// update current caret position
|
||||
setTimeout(function() { |
||||
inputNode.focus(); |
||||
inputNode.setSelectionRange(nickComp.caretPos, nickComp.caretPos); |
||||
}, 0); |
||||
}; |
||||
|
||||
|
||||
// Send the message to the websocket
|
||||
$scope.sendMessage = function() { |
||||
//XXX Use a signal here
|
||||
var ab = models.getActiveBuffer(); |
||||
|
||||
// It's undefined early in the lifecycle of the program.
|
||||
// Don't send empty commands
|
||||
if($scope.command !== undefined && $scope.command !== '') { |
||||
|
||||
// log to buffer history
|
||||
ab.addToHistory($scope.command); |
||||
|
||||
// Split the command into multiple commands based on line breaks
|
||||
_.each($scope.command.split(/\r?\n/), function(line) { |
||||
// Ask before a /quit
|
||||
if (line === '/quit' || line.indexOf('/quit ') === 0) { |
||||
if (!window.confirm("Are you sure you want to quit WeeChat? This will prevent you from connecting with Glowing Bear until you restart WeeChat on the command line!")) { |
||||
// skip this line
|
||||
return; |
||||
} |
||||
} |
||||
connection.sendMessage(line); |
||||
}); |
||||
|
||||
// Check for /clear command
|
||||
if ($scope.command === '/buffer clear' || $scope.command === '/c') { |
||||
$log.debug('Clearing lines'); |
||||
ab.clear(); |
||||
} |
||||
|
||||
// Empty the input after it's sent
|
||||
$scope.command = ''; |
||||
} |
||||
|
||||
$scope.getInputNode().focus(); |
||||
}; |
||||
|
||||
//XXX THIS DOES NOT BELONG HERE!
|
||||
$rootScope.addMention = function(prefix) { |
||||
// Extract nick from bufferline prefix
|
||||
var nick = prefix[prefix.length - 1].text; |
||||
|
||||
var newValue = $scope.command || ''; // can be undefined, in that case, use the empty string
|
||||
var addColon = newValue.length === 0; |
||||
if (newValue.length > 0) { |
||||
// Try to determine if it's a sequence of nicks
|
||||
var trimmedValue = newValue.trim(); |
||||
if (trimmedValue.charAt(trimmedValue.length - 1) === ':') { |
||||
// get last word
|
||||
var lastSpace = trimmedValue.lastIndexOf(' ') + 1; |
||||
var lastWord = trimmedValue.slice(lastSpace, trimmedValue.length - 1); |
||||
var nicklist = models.getActiveBuffer().getNicklistByTime(); |
||||
// check against nicklist to see if it's a list of highlights
|
||||
for (var index in nicklist) { |
||||
if (nicklist[index].name === lastWord) { |
||||
// It's another highlight!
|
||||
newValue = newValue.slice(0, newValue.lastIndexOf(':')) + ' '; |
||||
addColon = true; |
||||
break; |
||||
} |
||||
} |
||||
} |
||||
|
||||
// Add a space before the nick if there isn't one already
|
||||
// Last char might have changed above, so re-check
|
||||
if (newValue.charAt(newValue.length - 1) !== ' ') { |
||||
newValue += ' '; |
||||
} |
||||
} |
||||
// Add highlight to nicklist
|
||||
newValue += nick; |
||||
if (addColon) { |
||||
newValue += ': '; |
||||
} |
||||
$scope.command = newValue; |
||||
$scope.getInputNode().focus(); |
||||
}; |
||||
|
||||
|
||||
// Handle key presses in the input bar
|
||||
$rootScope.handleKeyPress = function($event) { |
||||
// don't do anything if not connected
|
||||
if (!$rootScope.connected) { |
||||
return true; |
||||
} |
||||
|
||||
var inputNode = $scope.getInputNode(); |
||||
|
||||
// Support different browser quirks
|
||||
var code = $event.keyCode ? $event.keyCode : $event.charCode; |
||||
|
||||
// reset quick keys display
|
||||
$rootScope.showQuickKeys = false; |
||||
|
||||
// any other key than Tab resets nick completion iteration
|
||||
var tmpIterCandidate = $scope.iterCandidate; |
||||
$scope.iterCandidate = null; |
||||
|
||||
// Left Alt+[0-9] -> jump to buffer
|
||||
if ($event.altKey && !$event.ctrlKey && (code > 47 && code < 58)) { |
||||
if (code === 48) { |
||||
code = 58; |
||||
} |
||||
var bufferNumber = code - 48 - 1 ; |
||||
|
||||
var activeBufferId; |
||||
// quick select filtered entries
|
||||
if (($scope.$parent.search.length || $scope.$parent.onlyUnread) && $scope.$parent.filteredBuffers.length) { |
||||
var filteredBufferNum = $scope.$parent.filteredBuffers[bufferNumber]; |
||||
if (filteredBufferNum !== undefined) { |
||||
activeBufferId = [filteredBufferNum.number, filteredBufferNum.id]; |
||||
} |
||||
} else { |
||||
// Map the buffers to only their numbers and IDs so we don't have to
|
||||
// copy the entire (possibly very large) buffer object, and then sort
|
||||
// the buffers according to their WeeChat number
|
||||
var sortedBuffers = _.map(models.getBuffers(), function(buffer) { |
||||
return [buffer.number, buffer.id]; |
||||
}).sort(function(left, right) { |
||||
// By default, Array.prototype.sort() sorts alphabetically.
|
||||
// Pass an ordering function to sort by first element.
|
||||
return left[0] - right[0]; |
||||
}); |
||||
activeBufferId = sortedBuffers[bufferNumber]; |
||||
} |
||||
if (activeBufferId) { |
||||
$scope.$parent.setActiveBuffer(activeBufferId[1]); |
||||
$event.preventDefault(); |
||||
} |
||||
} |
||||
|
||||
// Tab -> nick completion
|
||||
if (code === 9 && !$event.altKey && !$event.ctrlKey) { |
||||
$event.preventDefault(); |
||||
$scope.iterCandidate = tmpIterCandidate; |
||||
$scope.completeNick(); |
||||
return true; |
||||
} |
||||
|
||||
// Left Alt+n -> toggle nicklist
|
||||
if ($event.altKey && !$event.ctrlKey && code === 78) { |
||||
$event.preventDefault(); |
||||
$rootScope.toggleNicklist(); |
||||
return true; |
||||
} |
||||
|
||||
// Alt+A -> switch to buffer with activity
|
||||
if ($event.altKey && (code === 97 || code === 65)) { |
||||
$event.preventDefault(); |
||||
$rootScope.switchToActivityBuffer(); |
||||
return true; |
||||
} |
||||
|
||||
// Alt+L -> focus on input bar
|
||||
if ($event.altKey && (code === 76 || code === 108)) { |
||||
$event.preventDefault(); |
||||
inputNode.focus(); |
||||
inputNode.setSelectionRange($scope.command.length, $scope.command.length); |
||||
return true; |
||||
} |
||||
|
||||
// Alt+< -> switch to previous buffer
|
||||
if ($event.altKey && (code === 60 || code === 226)) { |
||||
var previousBuffer = models.getPreviousBuffer(); |
||||
if (previousBuffer) { |
||||
models.setActiveBuffer(previousBuffer.id); |
||||
$event.preventDefault(); |
||||
return true; |
||||
} |
||||
} |
||||
|
||||
// Double-tap Escape -> disconnect
|
||||
if (code === 27) { |
||||
$event.preventDefault(); |
||||
|
||||
// Check if a modal is visible. If so, close it instead of disconnecting
|
||||
var modals = document.querySelectorAll('.gb-modal'); |
||||
for (var modalId = 0; modalId < modals.length; modalId++) { |
||||
if (modals[modalId].getAttribute('data-state') === 'visible') { |
||||
modals[modalId].setAttribute('data-state', 'hidden'); |
||||
return true; |
||||
} |
||||
} |
||||
|
||||
if (typeof $scope.lastEscape !== "undefined" && (Date.now() - $scope.lastEscape) <= 500) { |
||||
// Double-tap
|
||||
connection.disconnect(); |
||||
} |
||||
$scope.lastEscape = Date.now(); |
||||
return true; |
||||
} |
||||
|
||||
// Alt+G -> focus on buffer filter input
|
||||
if ($event.altKey && (code === 103 || code === 71)) { |
||||
$event.preventDefault(); |
||||
if (!$scope.$parent.isSidebarVisible()) { |
||||
$scope.$parent.showSidebar(); |
||||
} |
||||
setTimeout(function() { |
||||
document.getElementById('bufferFilter').focus(); |
||||
}); |
||||
return true; |
||||
} |
||||
|
||||
var caretPos; |
||||
|
||||
// Arrow up -> go up in history
|
||||
if ($event.type === "keydown" && code === 38 && document.activeElement === inputNode) { |
||||
caretPos = inputNode.selectionStart; |
||||
if ($scope.command.slice(0, caretPos).indexOf("\n") !== -1) { |
||||
return false; |
||||
} |
||||
$scope.command = models.getActiveBuffer().getHistoryUp($scope.command); |
||||
// Set cursor to last position. Need 0ms timeout because browser sets cursor
|
||||
// position to the beginning after this key handler returns.
|
||||
setTimeout(function() { |
||||
if ($scope.command) { |
||||
inputNode.setSelectionRange($scope.command.length, $scope.command.length); |
||||
} |
||||
}, 0); |
||||
return true; |
||||
} |
||||
|
||||
// Arrow down -> go down in history
|
||||
if ($event.type === "keydown" && code === 40 && document.activeElement === inputNode) { |
||||
caretPos = inputNode.selectionStart; |
||||
if ($scope.command.slice(caretPos).indexOf("\n") !== -1) { |
||||
return false; |
||||
} |
||||
$scope.command = models.getActiveBuffer().getHistoryDown($scope.command); |
||||
// We don't need to set the cursor to the rightmost position here, the browser does that for us
|
||||
return true; |
||||
} |
||||
|
||||
// Enter to submit, shift-enter for newline
|
||||
if (code == 13 && !$event.shiftKey && document.activeElement === inputNode) { |
||||
$event.preventDefault(); |
||||
$scope.sendMessage(); |
||||
return true; |
||||
} |
||||
|
||||
var bufferlines = document.getElementById("bufferlines"); |
||||
var lines; |
||||
var i; |
||||
|
||||
// Page up -> scroll up
|
||||
if ($event.type === "keydown" && code === 33 && document.activeElement === inputNode && !$event.ctrlKey && !$event.altKey && !$event.shiftKey) { |
||||
if (bufferlines.scrollTop === 0) { |
||||
if (!$rootScope.loadingLines) { |
||||
$scope.$parent.fetchMoreLines(); |
||||
} |
||||
return true; |
||||
} |
||||
lines = bufferlines.querySelectorAll("tr"); |
||||
for (i = lines.length - 1; i >= 0; i--) { |
||||
if ((lines[i].offsetTop-bufferlines.scrollTop)<bufferlines.clientHeight/2) { |
||||
lines[i].scrollIntoView(false); |
||||
break; |
||||
} |
||||
} |
||||
return true; |
||||
} |
||||
|
||||
// Page down -> scroll down
|
||||
if ($event.type === "keydown" && code === 34 && document.activeElement === inputNode && !$event.ctrlKey && !$event.altKey && !$event.shiftKey) { |
||||
lines = bufferlines.querySelectorAll("tr"); |
||||
for (i = 0; i < lines.length; i++) { |
||||
if ((lines[i].offsetTop-bufferlines.scrollTop)>bufferlines.clientHeight/2) { |
||||
lines[i].scrollIntoView(true); |
||||
break; |
||||
} |
||||
} |
||||
return true; |
||||
} |
||||
|
||||
// Some readline keybindings
|
||||
if ($rootScope.readlineBindings && $event.ctrlKey && !$event.altKey && !$event.shiftKey && document.activeElement === inputNode) { |
||||
// get current caret position
|
||||
caretPos = inputNode.selectionStart; |
||||
// Ctrl-a
|
||||
if (code == 65) { |
||||
inputNode.setSelectionRange(0, 0); |
||||
// Ctrl-e
|
||||
} else if (code == 69) { |
||||
inputNode.setSelectionRange($scope.command.length, $scope.command.length); |
||||
// Ctrl-u
|
||||
} else if (code == 85) { |
||||
$scope.command = $scope.command.slice(caretPos); |
||||
setTimeout(function() { |
||||
inputNode.setSelectionRange(0, 0); |
||||
}); |
||||
// Ctrl-k
|
||||
} else if (code == 75) { |
||||
$scope.command = $scope.command.slice(0, caretPos); |
||||
setTimeout(function() { |
||||
inputNode.setSelectionRange($scope.command.length, $scope.command.length); |
||||
}); |
||||
// Ctrl-w
|
||||
} else if (code == 87) { |
||||
var trimmedValue = $scope.command.slice(0, caretPos); |
||||
var lastSpace = trimmedValue.lastIndexOf(' ') + 1; |
||||
$scope.command = $scope.command.slice(0, lastSpace) + $scope.command.slice(caretPos, $scope.command.length); |
||||
setTimeout(function() { |
||||
inputNode.setSelectionRange(lastSpace, lastSpace); |
||||
}); |
||||
} else { |
||||
return false; |
||||
} |
||||
$event.preventDefault(); |
||||
return true; |
||||
} |
||||
|
||||
// Alt key down -> display quick key legend
|
||||
if ($event.type === "keydown" && code === 18 && !$event.ctrlKey && !$event.shiftKey) { |
||||
$rootScope.showQuickKeys = true; |
||||
} |
||||
}; |
||||
|
||||
$rootScope.handleKeyRelease = function($event) { |
||||
// Alt key up -> remove quick key legend
|
||||
if ($event.keyCode === 18) { |
||||
if ($rootScope.quickKeysTimer !== undefined) { |
||||
clearTimeout($rootScope.quickKeysTimer); |
||||
} |
||||
$rootScope.quickKeysTimer = setTimeout(function() { |
||||
if ($rootScope.showQuickKeys) { |
||||
$rootScope.showQuickKeys = false; |
||||
$rootScope.$apply(); |
||||
} |
||||
delete $rootScope.quickKeysTimer; |
||||
}, 1000); |
||||
return true; |
||||
} |
||||
}; |
||||
}] |
||||
}; |
||||
}); |
||||
})(); |
@ -0,0 +1,141 @@ |
||||
var weechat = angular.module('weechat'); |
||||
|
||||
weechat.factory('notifications', ['$rootScope', '$log', 'models', function($rootScope, $log, models) { |
||||
// Ask for permission to display desktop notifications
|
||||
var requestNotificationPermission = function() { |
||||
// Firefox
|
||||
if (window.Notification) { |
||||
Notification.requestPermission(function(status) { |
||||
$log.info('Notification permission status: ', status); |
||||
if (Notification.permission !== status) { |
||||
Notification.permission = status; |
||||
} |
||||
}); |
||||
} |
||||
|
||||
// Webkit
|
||||
if (window.webkitNotifications !== undefined) { |
||||
var havePermission = window.webkitNotifications.checkPermission(); |
||||
if (havePermission !== 0) { // 0 is PERMISSION_ALLOWED
|
||||
$log.info('Notification permission status: ', havePermission === 0); |
||||
window.webkitNotifications.requestPermission(); |
||||
} |
||||
} |
||||
}; |
||||
|
||||
|
||||
// Reduce buffers with "+" operation over a key. Mostly useful for unread/notification counts.
|
||||
var unreadCount = function(type) { |
||||
if (!type) { |
||||
type = "unread"; |
||||
} |
||||
|
||||
// Do this the old-fashioned way with iterating over the keys, as underscore proved to be error-prone
|
||||
var keys = Object.keys(models.model.buffers); |
||||
var count = 0; |
||||
for (var key in keys) { |
||||
count += models.model.buffers[keys[key]][type]; |
||||
} |
||||
|
||||
return count; |
||||
}; |
||||
|
||||
|
||||
var updateTitle = function() { |
||||
var notifications = unreadCount('notification'); |
||||
if (notifications > 0) { |
||||
// New notifications deserve an exclamation mark
|
||||
$rootScope.notificationStatus = '(' + notifications + ') '; |
||||
} else { |
||||
$rootScope.notificationStatus = ''; |
||||
} |
||||
|
||||
var activeBuffer = models.getActiveBuffer(); |
||||
if (activeBuffer) { |
||||
$rootScope.pageTitle = activeBuffer.shortName + ' | ' + activeBuffer.title; |
||||
} |
||||
}; |
||||
|
||||
var updateFavico = function() { |
||||
var notifications = unreadCount('notification'); |
||||
if (notifications > 0) { |
||||
$rootScope.favico.badge(notifications, { |
||||
bgColor: '#d00', |
||||
textColor: '#fff' |
||||
}); |
||||
} else { |
||||
var unread = unreadCount('unread'); |
||||
if (unread === 0) { |
||||
$rootScope.favico.reset(); |
||||
} else { |
||||
$rootScope.favico.badge(unread, { |
||||
bgColor: '#5CB85C', |
||||
textColor: '#ff0' |
||||
}); |
||||
} |
||||
} |
||||
}; |
||||
|
||||
/* Function gets called from bufferLineAdded code if user should be notified */ |
||||
var createHighlight = function(buffer, message) { |
||||
var title = ''; |
||||
var body = ''; |
||||
var numNotifications = buffer.notification; |
||||
|
||||
if (['#', '&', '+', '!'].indexOf(buffer.shortName.charAt(0)) < 0) { |
||||
if (numNotifications > 1) { |
||||
title = numNotifications.toString() + ' private messages from '; |
||||
} else { |
||||
title = 'Private message from '; |
||||
} |
||||
body = message.text; |
||||
} else { |
||||
if (numNotifications > 1) { |
||||
title = numNotifications.toString() + ' highlights in '; |
||||
} else { |
||||
title = 'Highlight in '; |
||||
} |
||||
var prefix = ''; |
||||
for (var i = 0; i < message.prefix.length; i++) { |
||||
prefix += message.prefix[i].text; |
||||
} |
||||
body = '<' + prefix + '> ' + message.text; |
||||
} |
||||
title += buffer.shortName; |
||||
title += buffer.fullName.replace(/irc.([^\.]+)\..+/, " ($1)"); |
||||
|
||||
var notification = new Notification(title, { |
||||
body: body, |
||||
icon: 'assets/img/favicon.png' |
||||
}); |
||||
|
||||
// Cancel notification automatically
|
||||
var timeout = 15*1000; |
||||
notification.onshow = function() { |
||||
setTimeout(function() { |
||||
notification.close(); |
||||
}, timeout); |
||||
}; |
||||
|
||||
// Click takes the user to the buffer
|
||||
notification.onclick = function() { |
||||
models.setActiveBuffer(buffer.id); |
||||
window.focus(); |
||||
notification.close(); |
||||
}; |
||||
|
||||
if ($rootScope.soundnotification) { |
||||
// TODO fill in a sound file
|
||||
var audioFile = "assets/audio/sonar"; |
||||
var soundHTML = '<audio autoplay="autoplay"><source src="' + audioFile + '.ogg" type="audio/ogg" /><source src="' + audioFile + '.mp3" type="audio/mpeg" /></audio>'; |
||||
document.getElementById("soundNotification").innerHTML = soundHTML; |
||||
} |
||||
}; |
||||
|
||||
return { |
||||
requestNotificationPermission: requestNotificationPermission, |
||||
updateTitle: updateTitle, |
||||
updateFavico: updateFavico, |
||||
createHighlight: createHighlight, |
||||
}; |
||||
}]); |
@ -0,0 +1,82 @@ |
||||
(function() { |
||||
'use strict'; |
||||
|
||||
var weechat = angular.module('weechat'); |
||||
|
||||
weechat.directive('plugin', ['$rootScope', function($rootScope) { |
||||
/* |
||||
* Plugin directive |
||||
* Shows additional plugin content |
||||
*/ |
||||
return { |
||||
templateUrl: 'directives/plugin.html', |
||||
|
||||
scope: { |
||||
plugin: '=data' |
||||
}, |
||||
|
||||
controller: ['$scope', function($scope) { |
||||
|
||||
$scope.displayedContent = ""; |
||||
|
||||
// Auto-display embedded content only if it isn't NSFW
|
||||
$scope.plugin.visible = $rootScope.auto_display_embedded_content && !$scope.plugin.nsfw; |
||||
|
||||
// user-accessible hash key that is a valid CSS class name
|
||||
$scope.plugin.className = "embed_" + $scope.plugin.$$hashKey.replace(':','_'); |
||||
|
||||
$scope.plugin.getElement = function() { |
||||
return document.querySelector("." + $scope.plugin.className); |
||||
}; |
||||
|
||||
$scope.hideContent = function() { |
||||
$scope.plugin.visible = false; |
||||
}; |
||||
|
||||
$scope.showContent = function(automated) { |
||||
/* |
||||
* Shows the plugin content. |
||||
* displayedContent is bound to the DOM. |
||||
* Actual plugin content is only fetched when |
||||
* content is shown. |
||||
*/ |
||||
|
||||
var embed = $scope.plugin.getElement(); |
||||
|
||||
// If the plugin is asynchronous / lazy, execute it now and let it insert itself
|
||||
// TODO store the result between channel switches
|
||||
if ($scope.plugin.content instanceof Function){ |
||||
// Don't rerun if the result is already there
|
||||
if (embed.innerHTML === "") { |
||||
$scope.plugin.content(); |
||||
} |
||||
} else { |
||||
$scope.displayedContent = $scope.plugin.content; |
||||
} |
||||
$scope.plugin.visible = true; |
||||
|
||||
// Scroll embed content into view
|
||||
var scroll; |
||||
if (automated) { |
||||
var wasBottom = $rootScope.bufferBottom; |
||||
scroll = function() { |
||||
$rootScope.updateBufferBottom(wasBottom); |
||||
}; |
||||
} else { |
||||
scroll = function() { |
||||
if (embed && embed.scrollIntoViewIfNeeded !== undefined) { |
||||
embed.scrollIntoViewIfNeeded(); |
||||
$rootScope.updateBufferBottom(); |
||||
} |
||||
}; |
||||
} |
||||
setTimeout(scroll, 500); |
||||
}; |
||||
|
||||
if ($scope.plugin.visible) { |
||||
$scope.showContent(true); |
||||
} |
||||
}] |
||||
}; |
||||
}]); |
||||
})(); |
@ -0,0 +1,29 @@ |
||||
var weechat = angular.module('weechat'); |
||||
|
||||
weechat.factory('utils', function() { |
||||
// Helper to change style of a class
|
||||
var changeClassStyle = function(classSelector, attr, value) { |
||||
_.each(document.getElementsByClassName(classSelector), function(e) { |
||||
e.style[attr] = value; |
||||
}); |
||||
}; |
||||
// Helper to get style from a class
|
||||
var getClassStyle = function(classSelector, attr) { |
||||
_.each(document.getElementsByClassName(classSelector), function(e) { |
||||
return e.style[attr]; |
||||
}); |
||||
}; |
||||
|
||||
var isMobileUi = function() { |
||||
// TODO don't base detection solely on screen width
|
||||
// You are right. In the meantime I am renaming isMobileDevice to isMobileUi
|
||||
var mobile_cutoff = 968; |
||||
return (document.body.clientWidth < mobile_cutoff); |
||||
}; |
||||
|
||||
return { |
||||
changeClassStyle: changeClassStyle, |
||||
getClassStyle: getClassStyle, |
||||
isMobileUi: isMobileUi |
||||
}; |
||||
}); |
Loading…
Reference in new issue