glowing-bear/js/models.js

616 lines
19 KiB
JavaScript

/*
* This file contains the weechat models and various
* helper methods to work with them.
*/
(function() {
'use strict';
var models = angular.module('weechatModels', []);
models.service('models', ['$rootScope', '$filter', function($rootScope, $filter) {
// WeeChat version
this.version = null;
// WeeChat configuration values
this.wconfig = {};
// Save outgoing queries
this.outgoingQueries = [];
var parseRichText = function(text) {
var textElements = weeChat.Protocol.rawText2Rich(text),
typeToClassPrefixFg = {
'option': 'cof-',
'weechat': 'cwf-',
'ext': 'cef-'
},
typeToClassPrefixBg = {
'option': 'cob-',
'weechat': 'cwb-',
'ext': 'ceb-'
};
textElements.forEach(function(textEl) {
textEl.classes = [];
// foreground color
var prefix = typeToClassPrefixFg[textEl.fgColor.type];
textEl.classes.push(prefix + textEl.fgColor.name);
// background color
prefix = typeToClassPrefixBg[textEl.bgColor.type];
textEl.classes.push(prefix + textEl.bgColor.name);
// attributes
if (textEl.attrs.name !== null) {
textEl.classes.push('coa-' + textEl.attrs.name);
}
var attr, val;
for (attr in textEl.attrs.override) {
val = textEl.attrs.override[attr];
if (val) {
textEl.classes.push('a-' + attr);
} else {
textEl.classes.push('a-no-' + attr);
}
}
});
return textElements;
};
this.parseRichText = parseRichText;
/*
* Buffer class
*/
this.Buffer = function(message) {
// weechat properties
var fullName = message.full_name;
var shortName = message.short_name;
var hidden = message.hidden;
// If it's a channel, trim away the prefix (#, &, or +). If that is empty and the buffer
// has a short name, use a space (because the prefix will be displayed separately, and we don't want
// prefix + fullname, which would happen otherwise). Else, use null so that full_name is used
var trimmedName = shortName.replace(/^[#&+]/, '') || (shortName ? ' ' : null);
// get channel identifier
var prefix = ['#', '&', '+'].indexOf(shortName.charAt(0)) >= 0 ? shortName.charAt(0) : '';
var title = parseRichText(message.title);
var number = message.number;
var pointer = message.pointers[0];
var notify = 3; // Default 3 == message
var lines = [];
var requestedLines = 0;
var allLinesFetched = false;
var nicklist = {};
var history = [];
var historyPos = 0;
var active = false;
var notification = 0;
var unread = 0;
var lastSeen = -1;
// There are two kinds of types: bufferType (free vs formatted) and
// the kind of type that distinguishes queries from channels etc
var bufferType = message.type;
var type = message.local_variables.type;
var indent = (['channel', 'private'].indexOf(type) >= 0);
var plugin = message.local_variables.plugin;
var server = message.local_variables.server;
// Server buffers have this "irc.server.freenode" naming schema, which
// messes the sorting up. We need it to be "irc.freenode" instead.
var serverSortKey = plugin + "." + server +
(type === "server" ? "" : ("." + shortName));
// Lowercase it so alt+up/down traverses buffers in the same order
// angular's sortBy directive puts them in
serverSortKey = serverSortKey.toLowerCase();
// Buffer opened message does not include notify level
if (message.notify !== undefined) {
notify = message.notify;
}
var rtitle = "";
for (var i = 0; i < title.length; ++i) {
rtitle += title[i].text;
}
/*
* Adds a line to this buffer
*
* @param line the BufferLine object
* @return undefined
*/
var addLine = function(line) {
lines.push(line);
updateNickSpeak(line);
};
/*
* Adds a nick to nicklist
*/
var addNick = function(group, nick) {
if (nicklistRequested()) {
nick.spokeAt = Date.now();
nicklist[group].nicks.push(nick);
}
};
/*
* Deletes a nick from nicklist
*/
var delNick = function(group, nick) {
group = nicklist[group];
if (group === undefined) {
return;
}
group.nicks = _.filter(group.nicks, function(n) { return n.name !== nick.name;});
/*
for (i in group.nicks) {
if (group.nicks[i].name == nick.name) {
delete group.nicks[i];
break;
}
}
*/
};
/*
* Updates a nick in nicklist
*/
var updateNick = function(group, nick) {
group = nicklist[group];
if (group === undefined) {
// We are getting nicklist events for a buffer where not yet
// have populated the nicklist, so there will be nothing to
// update. Just ignore the event.
return;
}
for(var i in group.nicks) {
if (group.nicks[i].name === nick.name) {
group.nicks[i] = nick;
break;
}
}
};
/*
* Update a nick with a fresh timestamp so tab completion
* can use time to complete recent speakers
*/
var updateNickSpeak = function(line) {
// Try to find nick from prefix
var prefix = line.prefix;
if (prefix.length === 0) {
// some scripts produce lines without a prefix
return;
}
var nick = prefix[prefix.length - 1].text;
// Action / me, find the nick as the first word of the message
if (nick === " *") {
var match = line.text.match(/^(.+)\s/);
if (match) {
nick = match[1];
}
}
else if (nick === "" || nick === "=!=") {
return;
}
_.each(nicklist, function(nickGroup) {
_.each(nickGroup.nicks, function(nickObj) {
if (nickObj.name === nick) {
// Use the order the line arrive in for simplicity
// instead of using weechat's own timestamp
nickObj.spokeAt = Date.now();
}
});
});
};
/*
* Get a flat nicklist sorted by speaker time. This function is
* called for every tab key press by the user.
*
*/
var getNicklistByTime = function() {
var newlist = [];
_.each(nicklist, function(nickGroup) {
_.each(nickGroup.nicks, function(nickObj) {
newlist.push(nickObj);
});
});
newlist.sort(function(a, b) {
return a.spokeAt < b.spokeAt;
});
return newlist;
};
var addToHistory = function(line) {
var result = "";
if (historyPos !== history.length) {
// Pop cached line from history. Occurs if we submit something from history
result = history.pop();
}
history.push(line);
historyPos = history.length; // Go to end of history
return result;
};
var getHistoryUp = function(currentLine) {
if (historyPos >= history.length) {
// cache current line in history
history.push(currentLine);
}
if (historyPos <= 0 || historyPos >= history.length) {
// Can't go up from first message or from out-of-bounds index
return currentLine;
} else {
// Go up in history
historyPos--;
var line = history[historyPos];
return line;
}
};
var getHistoryDown = function(currentLine) {
if (historyPos === history.length) {
// stash on history like weechat does
if (currentLine !== undefined && currentLine !== '') {
history.push(currentLine);
historyPos++;
}
return '';
} else if (historyPos < 0 || historyPos > history.length) {
// Can't go down from out of bounds or last message
return currentLine;
} else {
historyPos++;
if (history.length > 0 && historyPos == (history.length-1)) {
// return cached line and remove from cache
return history.pop();
} else {
// Go down in history
return history[historyPos];
}
}
};
// Check if the nicklist is empty, i.e., no nicks present
// This checks for the presence of people, not whether a
// request for the nicklist has been made
var isNicklistEmpty = function() {
for (var obj in nicklist) {
if (obj !== 'root') {
return false;
}
}
return true;
};
var nicklistRequested = function() {
// If the nicklist has been requested but is empty, it
// still has a 'root' property. Check for its existence.
return nicklist.hasOwnProperty('root');
};
/* Clear all our buffer lines */
var clear = function() {
while(lines.length > 0) {
lines.pop();
}
requestedLines = 0;
};
return {
id: pointer,
fullName: fullName,
shortName: shortName,
hidden: hidden,
trimmedName: trimmedName,
prefix: prefix,
number: number,
title: title,
rtitle: rtitle,
lines: lines,
clear: clear,
requestedLines: requestedLines,
addLine: addLine,
lastSeen: lastSeen,
unread: unread,
notification: notification,
notify: notify,
nicklist: nicklist,
addNick: addNick,
delNick: delNick,
updateNick: updateNick,
getNicklistByTime: getNicklistByTime,
serverSortKey: serverSortKey,
indent: indent,
bufferType: bufferType,
type: type,
plugin: plugin,
server: server,
history: history,
addToHistory: addToHistory,
getHistoryUp: getHistoryUp,
getHistoryDown: getHistoryDown,
isNicklistEmpty: isNicklistEmpty,
nicklistRequested: nicklistRequested
};
};
/*
* BufferLine class
*/
this.BufferLine = function(message) {
var buffer = message.buffer;
var date = message.date;
var shortTime = $filter('date')(date, 'HH:mm');
var formattedTime = "";
if ($rootScope.supports_formatting_date) {
formattedTime = date.toLocaleTimeString([], {hour: "2-digit",
minute: "2-digit",
second: "2-digit"});
} else {
formattedTime = $filter('date')(date, 'HH:mm:ss');
}
formattedTime = formattedTime.replace(/ /g, "&nbsp;");
// If in 12h time the hour field has only one number, zero pad it
formattedTime = formattedTime.replace(/^(\d)(:|\.)/, '0$1$2');
// Wrap the first time separator in a span
formattedTime = formattedTime.replace(/(:|\.)/, '<span class="cof-chat_time_delimiters cob-chat_time_delimiters coa-chat_time_delimiters">$1</span>');
// Wrap the second time separator and seconds field in another span
// so that we can easily hide seconds using a CSS selector
formattedTime = formattedTime.replace(/(\d\d)(:|\.)(\d\d)/, '$1<span class="seconds"><span class="cof-chat_time_delimiters cob-chat_time_delimiters coa-chat_time_delimiters">$2</span>$3</span>');
var prefix = parseRichText(message.prefix);
var tags_array = message.tags_array;
var displayed = message.displayed;
var highlight = message.highlight;
var content = parseRichText(message.message);
if (highlight) {
prefix.forEach(function(textEl) {
textEl.classes.push('highlight');
});
}
var rtext = "";
for (var i = 0; i < content.length; ++i) {
rtext += content[i].text;
}
return {
prefix: prefix,
content: content,
date: date,
shortTime: shortTime,
formattedTime: formattedTime,
buffer: buffer,
tags: tags_array,
highlight: highlight,
displayed: displayed,
text: rtext
};
};
function nickGetColorClasses(nickMsg, propName) {
if (propName in nickMsg && nickMsg[propName] && nickMsg[propName].length > 0) {
var color = nickMsg[propName];
if (color.match(/^weechat/)) {
// color option
var colorName = color.match(/[a-zA-Z0-9_]+$/)[0];
return [
'cof-' + colorName,
'cob-' + colorName,
'coa-' + colorName
];
} else if (color.match(/^[a-zA-Z]+$/)) {
// WeeChat color name
return [
'cwf-' + color
];
} else if (color.match(/^[0-9]+$/)) {
// extended color
return [
'cef-' + color
];
}
}
return [
'cwf-default'
];
}
function nickGetClasses(nickMsg) {
return {
'name': nickGetColorClasses(nickMsg, 'color'),
'prefix': nickGetColorClasses(nickMsg, 'prefix_color')
};
}
/*
* Nick class
*/
this.Nick = function(message) {
var prefix = message.prefix;
var visible = message.visible;
var name = message.name;
var colorClasses = nickGetClasses(message);
return {
prefix: prefix,
visible: visible,
name: name,
prefixClasses: colorClasses.prefix,
nameClasses: colorClasses.name
};
};
/*
* Nicklist Group class
*/
this.NickGroup = function(message) {
var name = message.name;
var visible = message.visible;
var nicks = [];
return {
name: name,
visible: visible,
nicks: nicks
};
};
var activeBuffer = null;
var previousBuffer = null;
this.model = { 'buffers': {} };
/*
* Adds a buffer to the list
*
* @param buffer buffer object
* @return undefined
*/
this.addBuffer = function(buffer) {
this.model.buffers[buffer.id] = buffer;
};
/*
* Returns the current active buffer
*
* @return active buffer object
*/
this.getActiveBuffer = function() {
return activeBuffer;
};
/*
* Returns a reference to the currently active buffer that
* WeeChat understands without crashing, even if it's invalid
*
* @return active buffer pointer (WeeChat 1.0+) or fullname (older versions)
*/
this.getActiveBufferReference = function() {
if (this.version !== null && this.version[0] >= 1) {
// pointers are being validated, they're more reliable than
// fullName (e.g. if fullName contains spaces)
return "0x"+activeBuffer.id;
} else {
return activeBuffer.fullName;
}
};
/*
* Returns the previous current active buffer
*
* @return previous buffer object
*/
this.getPreviousBuffer = function() {
return previousBuffer;
};
/*
* Sets the buffer specifiee by bufferId as active.
* Deactivates the previous current buffer.
*
* @param bufferId id of the new active buffer
* @return true on success, false if buffer was not found
*/
this.setActiveBuffer = function(bufferId, key) {
if (key === undefined) {
key = 'id';
}
previousBuffer = this.getActiveBuffer();
if (key === 'id') {
activeBuffer = this.model.buffers[bufferId];
}
else {
activeBuffer = _.find(this.model.buffers, function(buffer) {
if (buffer[key] === bufferId) {
return buffer;
}
});
}
if (activeBuffer === undefined) {
// Buffer not found, undo assignment
activeBuffer = previousBuffer;
return false;
}
if (previousBuffer) {
// turn off the active status for the previous buffer
previousBuffer.active = false;
// Save the last line we saw
previousBuffer.lastSeen = previousBuffer.lines.length-1;
}
var unreadSum = activeBuffer.unread + activeBuffer.notification;
activeBuffer.active = true;
activeBuffer.unread = 0;
activeBuffer.notification = 0;
$rootScope.$emit('activeBufferChanged', unreadSum);
$rootScope.$emit('notificationChanged');
return true;
};
/*
* Returns the buffer list
*/
this.getBuffers = function() {
return this.model.buffers;
};
/*
* Reinitializes the model
*/
this.reinitialize = function() {
this.model.buffers = {};
};
/*
* Returns a specific buffer object
*
* @param bufferId id of the buffer
* @return the buffer object
*/
this.getBuffer = function(bufferId) {
return this.model.buffers[bufferId];
};
/*
* Closes a weechat buffer. Sets the first buffer
* as active, if the closing buffer was active before
*
* @param bufferId id of the buffer to close
* @return undefined
*/
this.closeBuffer = function(bufferId) {
var buffer = this.getBuffer(bufferId);
// Check if the buffer really exists, just in case
if (buffer === undefined) {
return;
}
if (buffer.active) {
var firstBuffer = _.keys(this.model.buffers)[0];
this.setActiveBuffer(firstBuffer);
}
// Can't use `buffer` here, needs to be deleted from the list
delete(this.model.buffers[bufferId]);
};
}]);
})();