Place to store the code and config used for the next-Iterations live event.
https://iterations.space/live/
You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
9518 lines
317 KiB
9518 lines
317 KiB
(function (global, undefined) {
|
|
|
|
// Holds anything kiwi client specific (ie. front, gateway, _kiwi.plugs..)
|
|
/**
|
|
* @namespace
|
|
*/
|
|
var _kiwi = {};
|
|
|
|
_kiwi.misc = {};
|
|
_kiwi.model = {};
|
|
_kiwi.view = {};
|
|
_kiwi.applets = {};
|
|
_kiwi.utils = {};
|
|
|
|
|
|
/**
|
|
* A global container for third party access
|
|
* Will be used to access a limited subset of kiwi functionality
|
|
* and data (think: plugins)
|
|
*/
|
|
_kiwi.global = {
|
|
build_version: '', // Kiwi IRC version this is built from (Set from index.html)
|
|
settings: undefined, // Instance of _kiwi.model.DataStore
|
|
plugins: undefined, // Instance of _kiwi.model.PluginManager
|
|
events: undefined, // Instance of PluginInterface
|
|
rpc: undefined, // Instance of WebsocketRpc
|
|
utils: {}, // References to misc. re-usable helpers / functions
|
|
|
|
// Make public some internal utils for plugins to make use of
|
|
initUtils: function() {
|
|
this.utils.randomString = randomString;
|
|
this.utils.secondsToTime = secondsToTime;
|
|
this.utils.parseISO8601 = parseISO8601;
|
|
this.utils.escapeRegex = escapeRegex;
|
|
this.utils.formatIRCMsg = formatIRCMsg;
|
|
this.utils.styleText = styleText;
|
|
this.utils.hsl2rgb = hsl2rgb;
|
|
this.utils.toUserMask = toUserMask;
|
|
|
|
this.utils.notifications = _kiwi.utils.notifications;
|
|
this.utils.formatDate = _kiwi.utils.formatDate;
|
|
},
|
|
|
|
addMediaMessageType: function(match, buildHtml) {
|
|
_kiwi.view.MediaMessage.addType(match, buildHtml);
|
|
},
|
|
|
|
// Event managers for plugins
|
|
components: {
|
|
EventComponent: function(event_source, proxy_event_name) {
|
|
/*
|
|
* proxyEvent() listens for events then re-triggers them on its own
|
|
* event emitter. Why? So we can .off() on this emitter without
|
|
* effecting the source of events. Handy for plugins that we don't
|
|
* trust meddling with the core events.
|
|
*
|
|
* If listening for 'all' events the arguments are as follows:
|
|
* 1. Name of the triggered event
|
|
* 2. The event data
|
|
* For all other events, we only have one argument:
|
|
* 1. The event data
|
|
*
|
|
* When this is used via `new kiwi.components.Network()`, this listens
|
|
* for 'all' events so the first argument is the event name which is
|
|
* the connection ID. We don't want to re-trigger this event name so
|
|
* we need to juggle the arguments to find the real event name we want
|
|
* to emit.
|
|
*/
|
|
function proxyEvent(event_name, event_data) {
|
|
if (proxy_event_name == 'all') {
|
|
} else {
|
|
event_data = event_name.event_data;
|
|
event_name = event_name.event_name;
|
|
}
|
|
|
|
this.trigger(event_name, event_data);
|
|
}
|
|
|
|
// The event we are to proxy
|
|
proxy_event_name = proxy_event_name || 'all';
|
|
|
|
_.extend(this, Backbone.Events);
|
|
this._source = event_source;
|
|
|
|
// Proxy the events to this dispatcher
|
|
event_source.on(proxy_event_name, proxyEvent, this);
|
|
|
|
// Clean up this object
|
|
this.dispose = function () {
|
|
event_source.off(proxy_event_name, proxyEvent);
|
|
this.off();
|
|
delete this.event_source;
|
|
};
|
|
},
|
|
|
|
Network: function(connection_id) {
|
|
var connection_event;
|
|
|
|
// If no connection id given, use all connections
|
|
if (typeof connection_id !== 'undefined') {
|
|
connection_event = 'connection:' + connection_id.toString();
|
|
} else {
|
|
connection_event = 'connection';
|
|
}
|
|
|
|
// Helper to get the network object
|
|
var getNetwork = function() {
|
|
var network = typeof connection_id === 'undefined' ?
|
|
_kiwi.app.connections.active_connection :
|
|
_kiwi.app.connections.getByConnectionId(connection_id);
|
|
|
|
return network ?
|
|
network :
|
|
undefined;
|
|
};
|
|
|
|
// Create the return object (events proxy from the gateway)
|
|
var obj = new this.EventComponent(_kiwi.gateway, connection_event);
|
|
|
|
// Proxy several gateway functions onto the return object
|
|
var funcs = {
|
|
kiwi: 'kiwi', raw: 'raw', kick: 'kick', topic: 'topic',
|
|
part: 'part', join: 'join', action: 'action', ctcp: 'ctcp',
|
|
ctcpRequest: 'ctcpRequest', ctcpResponse: 'ctcpResponse',
|
|
notice: 'notice', msg: 'privmsg', say: 'privmsg',
|
|
changeNick: 'changeNick', channelInfo: 'channelInfo',
|
|
mode: 'mode', quit: 'quit'
|
|
};
|
|
|
|
_.each(funcs, function(gateway_fn, func_name) {
|
|
obj[func_name] = function() {
|
|
var fn_name = gateway_fn;
|
|
|
|
// Add connection_id to the argument list
|
|
var args = Array.prototype.slice.call(arguments, 0);
|
|
args.unshift(connection_id);
|
|
|
|
// Call the gateway function on behalf of this connection
|
|
return _kiwi.gateway[fn_name].apply(_kiwi.gateway, args);
|
|
};
|
|
});
|
|
|
|
// Now for some network related functions...
|
|
obj.createQuery = function(nick) {
|
|
var network, restricted_keys;
|
|
|
|
network = getNetwork();
|
|
if (!network) {
|
|
return;
|
|
}
|
|
|
|
return network.createQuery(nick);
|
|
};
|
|
|
|
obj.ignoreMask = function(mask) {
|
|
var network = getNetwork();
|
|
if (!network) {
|
|
return;
|
|
}
|
|
|
|
return network.ignore_list.addMask(mask);
|
|
};
|
|
|
|
obj.unignoreMask = function(mask) {
|
|
var network = getNetwork();
|
|
if (!network) {
|
|
return;
|
|
}
|
|
|
|
return network.ignore_list.removeMask(mask);
|
|
};
|
|
|
|
// Add the networks getters/setters
|
|
obj.get = function(name) {
|
|
var network, restricted_keys;
|
|
|
|
network = getNetwork();
|
|
if (!network) {
|
|
return;
|
|
}
|
|
|
|
restricted_keys = [
|
|
'password'
|
|
];
|
|
if (restricted_keys.indexOf(name) > -1) {
|
|
return undefined;
|
|
}
|
|
|
|
return network.get(name);
|
|
};
|
|
|
|
obj.set = function() {
|
|
var network = getNetwork();
|
|
if (!network) {
|
|
return;
|
|
}
|
|
|
|
return network.set.apply(network, arguments);
|
|
};
|
|
|
|
return obj;
|
|
},
|
|
|
|
ControlInput: function() {
|
|
var obj = new this.EventComponent(_kiwi.app.controlbox);
|
|
var funcs = {
|
|
run: 'processInput', addPluginIcon: 'addPluginIcon'
|
|
};
|
|
|
|
_.each(funcs, function(controlbox_fn, func_name) {
|
|
obj[func_name] = function() {
|
|
var fn_name = controlbox_fn;
|
|
return _kiwi.app.controlbox[fn_name].apply(_kiwi.app.controlbox, arguments);
|
|
};
|
|
});
|
|
|
|
// Give access to the control input textarea
|
|
obj.input = _kiwi.app.controlbox.$('.inp');
|
|
|
|
return obj;
|
|
}
|
|
},
|
|
|
|
// Entry point to start the kiwi application
|
|
init: function (opts, callback) {
|
|
var locale_promise, theme_promise,
|
|
that = this;
|
|
|
|
opts = opts || {};
|
|
|
|
this.initUtils();
|
|
|
|
// Set up the settings datastore
|
|
_kiwi.global.settings = _kiwi.model.DataStore.instance('kiwi.settings');
|
|
_kiwi.global.settings.load();
|
|
|
|
// Set the window title
|
|
window.document.title = opts.server_settings.client.window_title || 'Kiwi IRC';
|
|
|
|
locale_promise = new Promise(function (resolve) {
|
|
// In order, find a locale from the users saved settings, the URL, default settings on the server, or auto detect
|
|
var locale = _kiwi.global.settings.get('locale') || opts.locale || opts.server_settings.client.settings.locale || 'magic';
|
|
$.getJSON(opts.base_path + '/assets/locales/' + locale + '.json', function (locale) {
|
|
if (locale) {
|
|
that.i18n = new Jed(locale);
|
|
} else {
|
|
that.i18n = new Jed();
|
|
}
|
|
resolve();
|
|
});
|
|
});
|
|
|
|
theme_promise = new Promise(function (resolve) {
|
|
var text_theme = opts.server_settings.client.settings.text_theme || 'default';
|
|
$.getJSON(opts.base_path + '/assets/text_themes/' + text_theme + '.json', function(text_theme) {
|
|
opts.text_theme = text_theme;
|
|
resolve();
|
|
});
|
|
});
|
|
|
|
|
|
Promise.all([locale_promise, theme_promise]).then(function () {
|
|
_kiwi.app = new _kiwi.model.Application(opts);
|
|
|
|
// Start the client up
|
|
_kiwi.app.initializeInterfaces();
|
|
|
|
// Event emitter to let plugins interface with parts of kiwi
|
|
_kiwi.global.events = new PluginInterface();
|
|
|
|
// Now everything has started up, load the plugin manager for third party plugins
|
|
_kiwi.global.plugins = new _kiwi.model.PluginManager();
|
|
|
|
callback();
|
|
|
|
}).then(null, function(err) {
|
|
console.error(err.stack);
|
|
});
|
|
},
|
|
|
|
start: function() {
|
|
_kiwi.app.showStartup();
|
|
},
|
|
|
|
// Allow plugins to change the startup applet
|
|
registerStartupApplet: function(startup_applet_name) {
|
|
_kiwi.app.startup_applet_name = startup_applet_name;
|
|
},
|
|
|
|
/**
|
|
* Open a new IRC connection
|
|
* @param {Object} connection_details {nick, host, port, ssl, password, options}
|
|
* @param {Function} callback function(err, network){}
|
|
*/
|
|
newIrcConnection: function(connection_details, callback) {
|
|
_kiwi.gateway.newConnection(connection_details, callback);
|
|
},
|
|
|
|
|
|
/**
|
|
* Taking settings from the server and URL, extract the default server/channel/nick settings
|
|
*/
|
|
defaultServerSettings: function () {
|
|
var parts;
|
|
var defaults = {
|
|
nick: '',
|
|
server: '',
|
|
port: 6667,
|
|
ssl: false,
|
|
channel: '',
|
|
channel_key: ''
|
|
};
|
|
var uricheck;
|
|
|
|
|
|
/**
|
|
* Get any settings set by the server
|
|
* These settings may be changed in the server selection dialog or via URL parameters
|
|
*/
|
|
if (_kiwi.app.server_settings.client) {
|
|
if (_kiwi.app.server_settings.client.nick)
|
|
defaults.nick = _kiwi.app.server_settings.client.nick;
|
|
|
|
if (_kiwi.app.server_settings.client.server)
|
|
defaults.server = _kiwi.app.server_settings.client.server;
|
|
|
|
if (_kiwi.app.server_settings.client.port)
|
|
defaults.port = _kiwi.app.server_settings.client.port;
|
|
|
|
if (_kiwi.app.server_settings.client.ssl)
|
|
defaults.ssl = _kiwi.app.server_settings.client.ssl;
|
|
|
|
if (_kiwi.app.server_settings.client.channel)
|
|
defaults.channel = _kiwi.app.server_settings.client.channel;
|
|
|
|
if (_kiwi.app.server_settings.client.channel_key)
|
|
defaults.channel_key = _kiwi.app.server_settings.client.channel_key;
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
* Get any settings passed in the URL
|
|
* These settings may be changed in the server selection dialog
|
|
*/
|
|
|
|
// Any query parameters first
|
|
if (getQueryVariable('nick'))
|
|
defaults.nick = getQueryVariable('nick');
|
|
|
|
if (window.location.hash)
|
|
defaults.channel = window.location.hash;
|
|
|
|
|
|
// Process the URL part by part, extracting as we go
|
|
parts = window.location.pathname.toString().replace(_kiwi.app.get('base_path'), '').split('/');
|
|
|
|
if (parts.length > 0) {
|
|
parts.shift();
|
|
|
|
if (parts.length > 0 && parts[0]) {
|
|
// Check to see if we're dealing with an irc: uri, or whether we need to extract the server/channel info from the HTTP URL path.
|
|
uricheck = parts[0].substr(0, 7).toLowerCase();
|
|
if ((uricheck === 'ircs%3a') || (uricheck.substr(0,6) === 'irc%3a')) {
|
|
parts[0] = decodeURIComponent(parts[0]);
|
|
// irc[s]://<host>[:<port>]/[<channel>[?<password>]]
|
|
uricheck = /^irc(s)?:(?:\/\/?)?([^:\/]+)(?::([0-9]+))?(?:(?:\/)([^\?]*)(?:(?:\?)(.*))?)?$/.exec(parts[0]);
|
|
/*
|
|
uricheck[1] = ssl (optional)
|
|
uricheck[2] = host
|
|
uricheck[3] = port (optional)
|
|
uricheck[4] = channel (optional)
|
|
uricheck[5] = channel key (optional, channel must also be set)
|
|
*/
|
|
if (uricheck) {
|
|
if (typeof uricheck[1] !== 'undefined') {
|
|
defaults.ssl = true;
|
|
if (defaults.port === 6667) {
|
|
defaults.port = 6697;
|
|
}
|
|
}
|
|
defaults.server = uricheck[2];
|
|
if (typeof uricheck[3] !== 'undefined') {
|
|
defaults.port = uricheck[3];
|
|
}
|
|
if (typeof uricheck[4] !== 'undefined') {
|
|
// Only prepend # if it doesn't already start with it
|
|
defaults.channel = (uricheck[4][0] === '#') ?
|
|
uricheck[4] :
|
|
'#' + uricheck[4];
|
|
|
|
if (typeof uricheck[5] !== 'undefined') {
|
|
defaults.channel_key = uricheck[5];
|
|
}
|
|
}
|
|
}
|
|
parts = [];
|
|
} else {
|
|
// Extract the port+ssl if we find one
|
|
if (parts[0].search(/:/) > 0) {
|
|
defaults.port = parts[0].substring(parts[0].search(/:/) + 1);
|
|
defaults.server = parts[0].substring(0, parts[0].search(/:/));
|
|
if (defaults.port[0] === '+') {
|
|
defaults.port = parseInt(defaults.port.substring(1), 10);
|
|
defaults.ssl = true;
|
|
} else {
|
|
defaults.ssl = false;
|
|
}
|
|
|
|
} else {
|
|
defaults.server = parts[0];
|
|
}
|
|
|
|
parts.shift();
|
|
}
|
|
}
|
|
|
|
if (parts.length > 0 && parts[0]) {
|
|
defaults.channel = '#' + parts[0];
|
|
parts.shift();
|
|
}
|
|
}
|
|
|
|
// If any settings have been given by the server.. override any auto detected settings
|
|
/**
|
|
* Get any server restrictions as set in the server config
|
|
* These settings can not be changed in the server selection dialog
|
|
*/
|
|
if (_kiwi.app.server_settings && _kiwi.app.server_settings.connection) {
|
|
if (_kiwi.app.server_settings.connection.server) {
|
|
defaults.server = _kiwi.app.server_settings.connection.server;
|
|
}
|
|
|
|
if (_kiwi.app.server_settings.connection.port) {
|
|
defaults.port = _kiwi.app.server_settings.connection.port;
|
|
}
|
|
|
|
if (_kiwi.app.server_settings.connection.ssl) {
|
|
defaults.ssl = _kiwi.app.server_settings.connection.ssl;
|
|
}
|
|
}
|
|
|
|
// Set any random numbers if needed
|
|
defaults.nick = defaults.nick.replace(/\?/g, Math.floor(Math.random() * 100000).toString());
|
|
|
|
if (getQueryVariable('encoding'))
|
|
defaults.encoding = getQueryVariable('encoding');
|
|
|
|
return defaults;
|
|
},
|
|
};
|
|
|
|
|
|
|
|
// If within a closure, expose the kiwi globals
|
|
if (typeof global !== 'undefined') {
|
|
global.kiwi = _kiwi.global;
|
|
} else {
|
|
// Not within a closure so set a var in the current scope
|
|
var kiwi = _kiwi.global;
|
|
}
|
|
|
|
|
|
|
|
(function () {
|
|
|
|
_kiwi.model.Application = Backbone.Model.extend({
|
|
/** _kiwi.view.Application */
|
|
view: null,
|
|
|
|
/** _kiwi.view.StatusMessage */
|
|
message: null,
|
|
|
|
initialize: function (options) {
|
|
this.app_options = options;
|
|
|
|
if (options.container) {
|
|
this.set('container', options.container);
|
|
}
|
|
|
|
// The base url to the kiwi server
|
|
this.set('base_path', options.base_path ? options.base_path : '');
|
|
|
|
// Path for the settings.json file
|
|
this.set('settings_path', options.settings_path ?
|
|
options.settings_path :
|
|
this.get('base_path') + '/assets/settings.json'
|
|
);
|
|
|
|
// Any options sent down from the server
|
|
this.server_settings = options.server_settings || {};
|
|
this.translations = options.translations || {};
|
|
this.themes = options.themes || [];
|
|
this.text_theme = options.text_theme || {};
|
|
|
|
// The applet to initially load
|
|
this.startup_applet_name = options.startup || 'kiwi_startup';
|
|
|
|
// Set any default settings before anything else is applied
|
|
if (this.server_settings && this.server_settings.client && this.server_settings.client.settings) {
|
|
this.applyDefaultClientSettings(this.server_settings.client.settings);
|
|
}
|
|
},
|
|
|
|
|
|
initializeInterfaces: function () {
|
|
var kiwi_server = '';
|
|
|
|
// The kiwi server to connect to may be a string for a single option,
|
|
// or an array of kiwi servers to pick one at random from.
|
|
if (typeof this.app_options.kiwi_server === 'string') {
|
|
kiwi_server = this.app_options.kiwi_server;
|
|
} else if (_.isArray(this.app_options.kiwi_server)) {
|
|
kiwi_server = _.sample(this.app_options.kiwi_server);
|
|
} else {
|
|
// Best guess at where the kiwi server is
|
|
kiwi_server = this.detectKiwiServer();
|
|
}
|
|
|
|
// Set the gateway up
|
|
_kiwi.gateway = new _kiwi.model.Gateway({kiwi_server: kiwi_server});
|
|
this.bindGatewayCommands(_kiwi.gateway);
|
|
|
|
this.initializeClient();
|
|
this.initializeGlobals();
|
|
|
|
this.view.barsHide(true);
|
|
},
|
|
|
|
|
|
detectKiwiServer: function () {
|
|
// If running from file, default to localhost:7777 by default
|
|
if (window.location.protocol === 'file:') {
|
|
return 'http://localhost:7778';
|
|
} else {
|
|
// Assume the kiwi server is on the same server
|
|
return window.location.protocol + '//' + window.location.host;
|
|
}
|
|
},
|
|
|
|
|
|
showStartup: function() {
|
|
this.startup_applet = _kiwi.model.Applet.load(this.startup_applet_name, {no_tab: true});
|
|
this.startup_applet.tab = this.view.$('.console');
|
|
this.startup_applet.view.show();
|
|
|
|
_kiwi.global.events.emit('loaded');
|
|
},
|
|
|
|
|
|
initializeClient: function () {
|
|
this.view = new _kiwi.view.Application({model: this, el: this.get('container')});
|
|
|
|
// Takes instances of model_network
|
|
this.connections = new _kiwi.model.NetworkPanelList();
|
|
|
|
// If all connections are removed at some point, hide the bars
|
|
this.connections.on('remove', _.bind(function() {
|
|
if (this.connections.length === 0) {
|
|
this.view.barsHide();
|
|
}
|
|
}, this));
|
|
|
|
// Applets panel list
|
|
this.applet_panels = new _kiwi.model.PanelList();
|
|
this.applet_panels.view.$el.addClass('panellist applets');
|
|
this.view.$el.find('.tabs').append(this.applet_panels.view.$el);
|
|
|
|
/**
|
|
* Set the UI components up
|
|
*/
|
|
this.controlbox = (new _kiwi.view.ControlBox({el: $('#kiwi .controlbox')[0]})).render();
|
|
this.client_ui_commands = new _kiwi.misc.ClientUiCommands(this, this.controlbox);
|
|
|
|
this.rightbar = new _kiwi.view.RightBar({el: this.view.$('.right_bar')[0]});
|
|
this.topicbar = new _kiwi.view.TopicBar({el: this.view.$el.find('.topic')[0]});
|
|
|
|
new _kiwi.view.AppToolbar({el: _kiwi.app.view.$el.find('.toolbar .app_tools')[0]});
|
|
new _kiwi.view.ChannelTools({el: _kiwi.app.view.$el.find('.channel_tools')[0]});
|
|
|
|
this.message = new _kiwi.view.StatusMessage({el: this.view.$el.find('.status_message')[0]});
|
|
|
|
this.resize_handle = new _kiwi.view.ResizeHandler({el: this.view.$el.find('.memberlists_resize_handle')[0]});
|
|
|
|
// Rejigg the UI sizes
|
|
this.view.doLayout();
|
|
},
|
|
|
|
|
|
initializeGlobals: function () {
|
|
_kiwi.global.connections = this.connections;
|
|
|
|
_kiwi.global.panels = this.panels;
|
|
_kiwi.global.panels.applets = this.applet_panels;
|
|
|
|
_kiwi.global.components.Applet = _kiwi.model.Applet;
|
|
_kiwi.global.components.Panel =_kiwi.model.Panel;
|
|
_kiwi.global.components.MenuBox = _kiwi.view.MenuBox;
|
|
_kiwi.global.components.DataStore = _kiwi.model.DataStore;
|
|
_kiwi.global.components.Notification = _kiwi.view.Notification;
|
|
_kiwi.global.components.Events = function() {
|
|
return _kiwi.global.events.createProxy();
|
|
};
|
|
},
|
|
|
|
|
|
applyDefaultClientSettings: function (settings) {
|
|
_.each(settings, function (value, setting) {
|
|
if (typeof _kiwi.global.settings.get(setting) === 'undefined') {
|
|
_kiwi.global.settings.set(setting, value);
|
|
}
|
|
});
|
|
},
|
|
|
|
|
|
panels: (function() {
|
|
var active_panel;
|
|
|
|
var fn = function(panel_type) {
|
|
var app = _kiwi.app,
|
|
panels;
|
|
|
|
// Default panel type
|
|
panel_type = panel_type || 'connections';
|
|
|
|
switch (panel_type) {
|
|
case 'connections':
|
|
panels = app.connections.panels();
|
|
break;
|
|
case 'applets':
|
|
panels = app.applet_panels.models;
|
|
break;
|
|
}
|
|
|
|
// Active panels / server
|
|
panels.active = active_panel;
|
|
panels.server = app.connections.active_connection ?
|
|
app.connections.active_connection.panels.server :
|
|
null;
|
|
|
|
return panels;
|
|
};
|
|
|
|
_.extend(fn, Backbone.Events);
|
|
|
|
// Keep track of the active panel. Channel/query/server or applet
|
|
fn.bind('active', function (new_active_panel) {
|
|
var previous_panel = active_panel;
|
|
active_panel = new_active_panel;
|
|
|
|
_kiwi.global.events.emit('panel:active', {previous: previous_panel, active: active_panel});
|
|
});
|
|
|
|
return fn;
|
|
})(),
|
|
|
|
|
|
bindGatewayCommands: function (gw) {
|
|
var that = this;
|
|
|
|
// As soon as an IRC connection is made, show the full client UI
|
|
gw.on('connection:connect', function (event) {
|
|
that.view.barsShow();
|
|
});
|
|
|
|
|
|
/**
|
|
* Handle the reconnections to the kiwi server
|
|
*/
|
|
(function () {
|
|
// 0 = non-reconnecting state. 1 = reconnecting state.
|
|
var gw_stat = 0;
|
|
|
|
gw.on('disconnect', function (event) {
|
|
that.view.$el.removeClass('connected');
|
|
|
|
// Reconnection phase will start to kick in
|
|
gw_stat = 1;
|
|
});
|
|
|
|
|
|
gw.on('reconnecting', function (event) {
|
|
var msg = translateText('client_models_application_reconnect_in_x_seconds', [event.delay/1000]) + '...';
|
|
|
|
// Only need to mention the repeating re-connection messages on server panels
|
|
_kiwi.app.connections.forEach(function(connection) {
|
|
connection.panels.server.addMsg('', styleText('quit', {text: msg}), 'action quit');
|
|
});
|
|
});
|
|
|
|
|
|
// After the socket has connected, kiwi handshakes and then triggers a kiwi:connected event
|
|
gw.on('kiwi:connected', function (event) {
|
|
var msg;
|
|
|
|
that.view.$el.addClass('connected');
|
|
|
|
// Make the rpc globally available for plugins
|
|
_kiwi.global.rpc = _kiwi.gateway.rpc;
|
|
|
|
_kiwi.global.events.emit('connected');
|
|
|
|
// If we were reconnecting, show some messages we have connected back OK
|
|
if (gw_stat === 1) {
|
|
|
|
// No longer in the reconnection state
|
|
gw_stat = 0;
|
|
|
|
msg = translateText('client_models_application_reconnect_successfully') + ' :)';
|
|
that.message.text(msg, {timeout: 5000});
|
|
|
|
// Mention the re-connection on every channel
|
|
_kiwi.app.connections.forEach(function(connection) {
|
|
connection.reconnect();
|
|
|
|
connection.panels.server.addMsg('', styleText('rejoin', {text: msg}), 'action join');
|
|
|
|
connection.panels.forEach(function(panel) {
|
|
if (!panel.isChannel())
|
|
return;
|
|
|
|
// The memberlist will reset itself and be updated with NAMES output
|
|
panel.get('members').reset();
|
|
|
|
panel.addMsg('', styleText('rejoin', {text: msg}), 'action join');
|
|
});
|
|
});
|
|
}
|
|
|
|
});
|
|
})();
|
|
|
|
|
|
gw.on('kiwi:reconfig', function () {
|
|
$.getJSON(that.get('settings_path'), function (data) {
|
|
that.server_settings = data.server_settings || {};
|
|
that.translations = data.translations || {};
|
|
});
|
|
});
|
|
|
|
|
|
gw.on('kiwi:jumpserver', function (data) {
|
|
var serv;
|
|
// No server set? Then nowhere to jump to.
|
|
if (typeof data.kiwi_server === 'undefined')
|
|
return;
|
|
|
|
serv = data.kiwi_server;
|
|
|
|
// Strip any trailing slash from the end
|
|
if (serv[serv.length-1] === '/')
|
|
serv = serv.substring(0, serv.length-1);
|
|
|
|
// Force the jumpserver now?
|
|
if (data.force) {
|
|
// Get an interval between 5 and 6 minutes so everyone doesn't reconnect it all at once
|
|
var jump_server_interval = Math.random() * (360 - 300) + 300;
|
|
jump_server_interval = 1;
|
|
|
|
// Tell the user we are going to disconnect, wait 5 minutes then do the actual reconnect
|
|
var msg = _kiwi.global.i18n.translate('client_models_application_jumpserver_prepare').fetch();
|
|
that.message.text(msg, {timeout: 10000});
|
|
|
|
setTimeout(function forcedReconnect() {
|
|
var msg = _kiwi.global.i18n.translate('client_models_application_jumpserver_reconnect').fetch();
|
|
that.message.text(msg, {timeout: 8000});
|
|
|
|
setTimeout(function forcedReconnectPartTwo() {
|
|
_kiwi.gateway.set('kiwi_server', serv);
|
|
|
|
_kiwi.gateway.reconnect(function() {
|
|
// Reconnect all the IRC connections
|
|
that.connections.forEach(function(con){ con.reconnect(); });
|
|
});
|
|
}, 5000);
|
|
|
|
}, jump_server_interval * 1000);
|
|
}
|
|
});
|
|
|
|
|
|
gw.on('kiwi:asset_files_changes', function (data) {
|
|
that.view.reloadStyles();
|
|
});
|
|
}
|
|
|
|
});
|
|
|
|
})();
|
|
|
|
|
|
|
|
_kiwi.model.Gateway = Backbone.Model.extend({
|
|
|
|
initialize: function () {
|
|
|
|
// For ease of access. The socket.io object
|
|
this.socket = this.get('socket');
|
|
|
|
// Used to check if a disconnection was unplanned
|
|
this.disconnect_requested = false;
|
|
},
|
|
|
|
|
|
|
|
reconnect: function (callback) {
|
|
this.disconnect_requested = true;
|
|
this.socket.close();
|
|
|
|
this.socket = null;
|
|
this.connect(callback);
|
|
},
|
|
|
|
|
|
|
|
/**
|
|
* Connects to the server
|
|
* @param {Function} callback A callback function to be invoked once Kiwi's server has connected to the IRC server
|
|
*/
|
|
connect: function (callback) {
|
|
var that = this;
|
|
|
|
this.connect_callback = callback;
|
|
|
|
this.socket = new EngineioTools.ReconnectingSocket(this.get('kiwi_server'), {
|
|
transports: _kiwi.app.server_settings.transports || ['polling', 'websocket'],
|
|
path: _kiwi.app.get('base_path') + '/transport',
|
|
reconnect_max_attempts: 5,
|
|
reconnect_delay: 2000
|
|
});
|
|
|
|
// If we have an existing RPC object, clean it up before replacing it
|
|
if (this.rpc) {
|
|
rpc.dispose();
|
|
}
|
|
this.rpc = new EngineioTools.Rpc(this.socket);
|
|
|
|
this.socket.on('connect_failed', function (reason) {
|
|
this.socket.disconnect();
|
|
this.trigger("connect_fail", {reason: reason});
|
|
});
|
|
|
|
this.socket.on('error', function (e) {
|
|
console.log("_kiwi.gateway.socket.on('error')", {reason: e});
|
|
if (that.connect_callback) {
|
|
that.connect_callback(e);
|
|
delete that.connect_callback;
|
|
}
|
|
|
|
that.trigger("connect_fail", {reason: e});
|
|
});
|
|
|
|
this.socket.on('connecting', function (transport_type) {
|
|
console.log("_kiwi.gateway.socket.on('connecting')");
|
|
that.trigger("connecting");
|
|
});
|
|
|
|
/**
|
|
* Once connected to the kiwi server send the IRC connect command along
|
|
* with the IRC server details.
|
|
* A `connect` event is sent from the kiwi server once connected to the
|
|
* IRCD and the nick has been accepted.
|
|
*/
|
|
this.socket.on('open', function () {
|
|
// Reset the disconnect_requested flag
|
|
that.disconnect_requested = false;
|
|
|
|
// Each minute we need to trigger a heartbeat. Server expects 2min, but to be safe we do it every 1min
|
|
var heartbeat = function() {
|
|
if (!that.rpc) return;
|
|
|
|
that.rpc('kiwi.heartbeat');
|
|
that._heartbeat_tmr = setTimeout(heartbeat, 60000);
|
|
};
|
|
|
|
heartbeat();
|
|
|
|
console.log("_kiwi.gateway.socket.on('open')");
|
|
});
|
|
|
|
this.rpc.on('too_many_connections', function () {
|
|
that.trigger("connect_fail", {reason: 'too_many_connections'});
|
|
});
|
|
|
|
this.rpc.on('irc', function (response, data) {
|
|
that.parse(data.command, data.data);
|
|
});
|
|
|
|
this.rpc.on('kiwi', function (response, data) {
|
|
that.parseKiwi(data.command, data.data);
|
|
});
|
|
|
|
this.socket.on('close', function () {
|
|
that.trigger("disconnect", {});
|
|
console.log("_kiwi.gateway.socket.on('close')");
|
|
});
|
|
|
|
this.socket.on('reconnecting', function (status) {
|
|
console.log("_kiwi.gateway.socket.on('reconnecting')");
|
|
that.trigger("reconnecting", {delay: status.delay, attempts: status.attempts});
|
|
});
|
|
|
|
this.socket.on('reconnecting_failed', function () {
|
|
console.log("_kiwi.gateway.socket.on('reconnect_failed')");
|
|
});
|
|
},
|
|
|
|
|
|
/**
|
|
* Return a new network object with the new connection details
|
|
*/
|
|
newConnection: function(connection_info, callback_fn) {
|
|
var that = this;
|
|
|
|
// If not connected, connect first then re-call this function
|
|
if (!this.isConnected()) {
|
|
this.connect(function(err) {
|
|
if (err) {
|
|
callback_fn(err);
|
|
return;
|
|
}
|
|
|
|
that.newConnection(connection_info, callback_fn);
|
|
});
|
|
|
|
return;
|
|
}
|
|
|
|
this.makeIrcConnection(connection_info, function(err, server_num) {
|
|
var connection;
|
|
|
|
if (!err) {
|
|
if (!_kiwi.app.connections.getByConnectionId(server_num)){
|
|
var inf = {
|
|
connection_id: server_num,
|
|
nick: connection_info.nick,
|
|
address: connection_info.host,
|
|
port: connection_info.port,
|
|
ssl: connection_info.ssl,
|
|
password: connection_info.password
|
|
};
|
|
connection = new _kiwi.model.Network(inf);
|
|
_kiwi.app.connections.add(connection);
|
|
}
|
|
|
|
console.log("_kiwi.gateway.socket.on('connect')", connection);
|
|
callback_fn && callback_fn(err, connection);
|
|
|
|
} else {
|
|
console.log("_kiwi.gateway.socket.on('error')", {reason: err});
|
|
callback_fn && callback_fn(err);
|
|
}
|
|
});
|
|
},
|
|
|
|
|
|
/**
|
|
* Make a new IRC connection and return its connection ID
|
|
*/
|
|
makeIrcConnection: function(connection_info, callback_fn) {
|
|
var server_info = {
|
|
nick: connection_info.nick,
|
|
hostname: connection_info.host,
|
|
port: connection_info.port,
|
|
ssl: connection_info.ssl,
|
|
password: connection_info.password
|
|
};
|
|
|
|
connection_info.options = connection_info.options || {};
|
|
|
|
// A few optional parameters
|
|
if (connection_info.options.encoding)
|
|
server_info.encoding = connection_info.options.encoding;
|
|
|
|
this.rpc('kiwi.connect_irc', server_info, function (err, server_num) {
|
|
if (!err) {
|
|
callback_fn && callback_fn(err, server_num);
|
|
|
|
} else {
|
|
callback_fn && callback_fn(err);
|
|
}
|
|
});
|
|
},
|
|
|
|
|
|
isConnected: function () {
|
|
// TODO: Check this. Might want to use .readyState
|
|
return this.socket;
|
|
},
|
|
|
|
|
|
|
|
parseKiwi: function (command, data) {
|
|
var args;
|
|
|
|
switch (command) {
|
|
case 'connected':
|
|
// Send some info on this client to the server
|
|
args = {
|
|
build_version: _kiwi.global.build_version
|
|
};
|
|
this.rpc('kiwi.client_info', args);
|
|
|
|
this.connect_callback && this.connect_callback();
|
|
delete this.connect_callback;
|
|
|
|
break;
|
|
}
|
|
|
|
this.trigger('kiwi:' + command, data);
|
|
this.trigger('kiwi', data);
|
|
},
|
|
|
|
/**
|
|
* Parses the response from the server
|
|
*/
|
|
parse: function (command, data) {
|
|
var network_trigger = '';
|
|
|
|
// Trigger the connection specific events (used by Network objects)
|
|
if (typeof data.connection_id !== 'undefined') {
|
|
network_trigger = 'connection:' + data.connection_id.toString();
|
|
|
|
this.trigger(network_trigger, {
|
|
event_name: command,
|
|
event_data: data
|
|
});
|
|
|
|
// Some events trigger a more in-depth event name
|
|
if (command == 'message' && data.type) {
|
|
this.trigger('connection ' + network_trigger, {
|
|
event_name: 'message:' + data.type,
|
|
event_data: data
|
|
});
|
|
}
|
|
|
|
if (command == 'channel' && data.type) {
|
|
this.trigger('connection ' + network_trigger, {
|
|
event_name: 'channel:' + data.type,
|
|
event_data: data
|
|
});
|
|
}
|
|
}
|
|
|
|
// Trigger the global events
|
|
this.trigger('connection', {event_name: command, event_data: data});
|
|
this.trigger('connection:' + command, data);
|
|
},
|
|
|
|
/**
|
|
* Make an RPC call with the connection_id as the first argument
|
|
* @param {String} method RPC method name
|
|
* @param {Number} connection_id Connection ID this call relates to
|
|
*/
|
|
rpcCall: function(method, connection_id) {
|
|
var args = Array.prototype.slice.call(arguments, 0);
|
|
|
|
if (typeof args[1] === 'undefined' || args[1] === null)
|
|
args[1] = _kiwi.app.connections.active_connection.get('connection_id');
|
|
|
|
return this.rpc.apply(this.rpc, args);
|
|
},
|
|
|
|
/**
|
|
* Sends a PRIVMSG message
|
|
* @param {String} target The target of the message (e.g. a channel or nick)
|
|
* @param {String} msg The message to send
|
|
* @param {Function} callback A callback function
|
|
*/
|
|
privmsg: function (connection_id, target, msg, callback) {
|
|
var args = {
|
|
target: target,
|
|
msg: msg
|
|
};
|
|
|
|
this.rpcCall('irc.privmsg', connection_id, args, callback);
|
|
},
|
|
|
|
/**
|
|
* Sends a NOTICE message
|
|
* @param {String} target The target of the message (e.g. a channel or nick)
|
|
* @param {String} msg The message to send
|
|
* @param {Function} callback A callback function
|
|
*/
|
|
notice: function (connection_id, target, msg, callback) {
|
|
var args = {
|
|
target: target,
|
|
msg: msg
|
|
};
|
|
|
|
this.rpcCall('irc.notice', connection_id, args, callback);
|
|
},
|
|
|
|
/**
|
|
* Sends a CTCP message
|
|
* @param {Boolean} request Indicates whether this is a CTCP request (true) or reply (false)
|
|
* @param {String} type The type of CTCP message, e.g. 'VERSION', 'TIME', 'PING' etc.
|
|
* @param {String} target The target of the message, e.g a channel or nick
|
|
* @param {String} params Additional paramaters
|
|
* @param {Function} callback A callback function
|
|
*/
|
|
ctcp: function (connection_id, is_request, type, target, params, callback) {
|
|
var args = {
|
|
is_request: is_request,
|
|
type: type,
|
|
target: target,
|
|
params: params
|
|
};
|
|
|
|
this.rpcCall('irc.ctcp', connection_id, args, callback);
|
|
},
|
|
|
|
ctcpRequest: function (connection_id, type, target, params, callback) {
|
|
this.ctcp(connection_id, true, type, target, params, callback);
|
|
},
|
|
ctcpResponse: function (connection_id, type, target, params, callback) {
|
|
this.ctcp(connection_id, false, type, target, params, callback);
|
|
},
|
|
|
|
/**
|
|
* @param {String} target The target of the message (e.g. a channel or nick)
|
|
* @param {String} msg The message to send
|
|
* @param {Function} callback A callback function
|
|
*/
|
|
action: function (connection_id, target, msg, callback) {
|
|
this.ctcp(connection_id, true, 'ACTION', target, msg, callback);
|
|
},
|
|
|
|
/**
|
|
* Joins a channel
|
|
* @param {String} channel The channel to join
|
|
* @param {String} key The key to the channel
|
|
* @param {Function} callback A callback function
|
|
*/
|
|
join: function (connection_id, channel, key, callback) {
|
|
var args = {
|
|
channel: channel,
|
|
key: key
|
|
};
|
|
|
|
this.rpcCall('irc.join', connection_id, args, callback);
|
|
},
|
|
|
|
/**
|
|
* Retrieves channel information
|
|
*/
|
|
channelInfo: function (connection_id, channel, callback) {
|
|
var args = {
|
|
channel: channel
|
|
};
|
|
|
|
this.rpcCall('irc.channel_info', connection_id, args, callback);
|
|
},
|
|
|
|
/**
|
|
* Leaves a channel
|
|
* @param {String} channel The channel to part
|
|
* @param {String} message Optional part message
|
|
* @param {Function} callback A callback function
|
|
*/
|
|
part: function (connection_id, channel, message, callback) {
|
|
"use strict";
|
|
|
|
// The message param is optional, so juggle args if it is missing
|
|
if (typeof arguments[2] === 'function') {
|
|
callback = arguments[2];
|
|
message = undefined;
|
|
}
|
|
var args = {
|
|
channel: channel,
|
|
message: message
|
|
};
|
|
|
|
this.rpcCall('irc.part', connection_id, args, callback);
|
|
},
|
|
|
|
/**
|
|
* Queries or modifies a channell topic
|
|
* @param {String} channel The channel to query or modify
|
|
* @param {String} new_topic The new topic to set
|
|
* @param {Function} callback A callback function
|
|
*/
|
|
topic: function (connection_id, channel, new_topic, callback) {
|
|
var args = {
|
|
channel: channel,
|
|
topic: new_topic
|
|
};
|
|
|
|
this.rpcCall('irc.topic', connection_id, args, callback);
|
|
},
|
|
|
|
/**
|
|
* Kicks a user from a channel
|
|
* @param {String} channel The channel to kick the user from
|
|
* @param {String} nick The nick of the user to kick
|
|
* @param {String} reason The reason for kicking the user
|
|
* @param {Function} callback A callback function
|
|
*/
|
|
kick: function (connection_id, channel, nick, reason, callback) {
|
|
var args = {
|
|
channel: channel,
|
|
nick: nick,
|
|
reason: reason
|
|
};
|
|
|
|
this.rpcCall('irc.kick', connection_id, args, callback);
|
|
},
|
|
|
|
/**
|
|
* Disconnects us from the server
|
|
* @param {String} msg The quit message to send to the IRC server
|
|
* @param {Function} callback A callback function
|
|
*/
|
|
quit: function (connection_id, msg, callback) {
|
|
msg = msg || "";
|
|
|
|
var args = {
|
|
message: msg
|
|
};
|
|
|
|
this.rpcCall('irc.quit', connection_id, args, callback);
|
|
},
|
|
|
|
/**
|
|
* Sends a string unmodified to the IRC server
|
|
* @param {String} data The data to send to the IRC server
|
|
* @param {Function} callback A callback function
|
|
*/
|
|
raw: function (connection_id, data, callback) {
|
|
var args = {
|
|
data: data
|
|
};
|
|
|
|
this.rpcCall('irc.raw', connection_id, args, callback);
|
|
},
|
|
|
|
/**
|
|
* Changes our nickname
|
|
* @param {String} new_nick Our new nickname
|
|
* @param {Function} callback A callback function
|
|
*/
|
|
changeNick: function (connection_id, new_nick, callback) {
|
|
var args = {
|
|
nick: new_nick
|
|
};
|
|
|
|
this.rpcCall('irc.nick', connection_id, args, callback);
|
|
},
|
|
|
|
/**
|
|
* Sets a mode for a target
|
|
*/
|
|
mode: function (connection_id, target, mode_string, callback) {
|
|
var args = {
|
|
data: 'MODE ' + target + ' ' + mode_string
|
|
};
|
|
|
|
this.rpcCall('irc.raw', connection_id, args, callback);
|
|
},
|
|
|
|
/**
|
|
* Sends ENCODING change request to server.
|
|
* @param {String} new_encoding The new proposed encode
|
|
* @param {Fucntion} callback A callback function
|
|
*/
|
|
setEncoding: function (connection_id, new_encoding, callback) {
|
|
var args = {
|
|
encoding: new_encoding
|
|
};
|
|
|
|
this.rpcCall('irc.encoding', connection_id, args, callback);
|
|
}
|
|
});
|
|
|
|
|
|
|
|
(function () {
|
|
|
|
_kiwi.model.Network = Backbone.Model.extend({
|
|
defaults: {
|
|
connection_id: 0,
|
|
/**
|
|
* The name of the network
|
|
* @type String
|
|
*/
|
|
name: 'Network',
|
|
|
|
/**
|
|
* The address (URL) of the network
|
|
* @type String
|
|
*/
|
|
address: '',
|
|
|
|
/**
|
|
* The port for the network
|
|
* @type Int
|
|
*/
|
|
port: 6667,
|
|
|
|
/**
|
|
* If this network uses SSL
|
|
* @type Bool
|
|
*/
|
|
ssl: false,
|
|
|
|
/**
|
|
* The password to connect to this network
|
|
* @type String
|
|
*/
|
|
password: '',
|
|
|
|
/**
|
|
* The current nickname
|
|
* @type String
|
|
*/
|
|
nick: '',
|
|
|
|
/**
|
|
* The channel prefix for this network
|
|
* @type String
|
|
*/
|
|
channel_prefix: '#',
|
|
|
|
/**
|
|
* The user prefixes for channel owner/admin/op/voice etc. on this network
|
|
* @type Array
|
|
*/
|
|
user_prefixes: [
|
|
{symbol: '~', mode: 'q'},
|
|
{symbol: '&', mode: 'a'},
|
|
{symbol: '@', mode: 'o'},
|
|
{symbol: '%', mode: 'h'},
|
|
{symbol: '+', mode: 'v'}
|
|
]
|
|
},
|
|
|
|
|
|
initialize: function () {
|
|
// If we already have a connection, bind our events
|
|
if (typeof this.get('connection_id') !== 'undefined') {
|
|
this.gateway = _kiwi.global.components.Network(this.get('connection_id'));
|
|
this.bindGatewayEvents();
|
|
}
|
|
|
|
// Create our panel list (tabs)
|
|
this.panels = new _kiwi.model.PanelList([], this);
|
|
//this.panels.network = this;
|
|
|
|
// Automatically create a server tab
|
|
var server_panel = new _kiwi.model.Server({name: 'Server', network: this});
|
|
this.panels.add(server_panel);
|
|
this.panels.server = this.panels.active = server_panel;
|
|
|
|
this.ignore_list = new _kiwi.model.IgnoreList();
|
|
},
|
|
|
|
|
|
reconnect: function(callback_fn) {
|
|
var that = this,
|
|
server_info = {
|
|
nick: this.get('nick'),
|
|
host: this.get('address'),
|
|
port: this.get('port'),
|
|
ssl: this.get('ssl'),
|
|
password: this.get('password')
|
|
};
|
|
|
|
_kiwi.gateway.makeIrcConnection(server_info, function(err, connection_id) {
|
|
if (!err) {
|
|
that.gateway.dispose();
|
|
|
|
that.set('connection_id', connection_id);
|
|
that.gateway = _kiwi.global.components.Network(that.get('connection_id'));
|
|
that.bindGatewayEvents();
|
|
|
|
// Reset each of the panels connection ID
|
|
that.panels.forEach(function(panel) {
|
|
panel.set('connection_id', connection_id);
|
|
});
|
|
|
|
callback_fn && callback_fn(err);
|
|
|
|
} else {
|
|
console.log("_kiwi.gateway.socket.on('error')", {reason: err});
|
|
callback_fn && callback_fn(err);
|
|
}
|
|
});
|
|
},
|
|
|
|
|
|
bindGatewayEvents: function () {
|
|
//this.gateway.on('all', function() {console.log('ALL', this.get('connection_id'), arguments);});
|
|
|
|
this.gateway.on('connect', onConnect, this);
|
|
this.gateway.on('disconnect', onDisconnect, this);
|
|
|
|
this.gateway.on('nick', function(event) {
|
|
if (event.nick === this.get('nick')) {
|
|
this.set('nick', event.newnick);
|
|
}
|
|
}, this);
|
|
|
|
this.gateway.on('options', onOptions, this);
|
|
this.gateway.on('motd', onMotd, this);
|
|
this.gateway.on('channel:join', onJoin, this);
|
|
this.gateway.on('channel:part', onPart, this);
|
|
this.gateway.on('channel:kick', onKick, this);
|
|
this.gateway.on('quit', onQuit, this);
|
|
this.gateway.on('message', onMessage, this);
|
|
this.gateway.on('nick', onNick, this);
|
|
this.gateway.on('ctcp_request', onCtcpRequest, this);
|
|
this.gateway.on('ctcp_response', onCtcpResponse, this);
|
|
this.gateway.on('topic', onTopic, this);
|
|
this.gateway.on('topicsetby', onTopicSetBy, this);
|
|
this.gateway.on('userlist', onUserlist, this);
|
|
this.gateway.on('userlist_end', onUserlistEnd, this);
|
|
this.gateway.on('banlist', onBanlist, this);
|
|
this.gateway.on('mode', onMode, this);
|
|
this.gateway.on('whois', onWhois, this);
|
|
this.gateway.on('whowas', onWhowas, this);
|
|
this.gateway.on('away', onAway, this);
|
|
this.gateway.on('list_start', onListStart, this);
|
|
this.gateway.on('irc_error', onIrcError, this);
|
|
this.gateway.on('unknown_command', onUnknownCommand, this);
|
|
this.gateway.on('channel_info', onChannelInfo, this);
|
|
this.gateway.on('wallops', onWallops, this);
|
|
},
|
|
|
|
|
|
/**
|
|
* Create panels and join the channel
|
|
* This will not wait for the join event to create a panel. This
|
|
* increases responsiveness in case of network lag
|
|
*/
|
|
createAndJoinChannels: function (channels) {
|
|
var that = this,
|
|
panels = [];
|
|
|
|
// Multiple channels may come as comma-delimited
|
|
if (typeof channels === 'string') {
|
|
channels = channels.split(',');
|
|
}
|
|
|
|
$.each(channels, function (index, channel_name_key) {
|
|
// We may have a channel key so split it off
|
|
var spli = channel_name_key.trim().split(' '),
|
|
channel_name = spli[0],
|
|
channel_key = spli[1] || '';
|
|
|
|
// Trim any whitespace off the name
|
|
channel_name = channel_name.trim();
|
|
|
|
// Add channel_prefix in front of the first channel if missing
|
|
if (that.get('channel_prefix').indexOf(channel_name[0]) === -1) {
|
|
// Could be many prefixes but '#' is highly likely the required one
|
|
channel_name = '#' + channel_name;
|
|
}
|
|
|
|
// Check if we have the panel already. If not, create it
|
|
channel = that.panels.getByName(channel_name);
|
|
if (!channel) {
|
|
channel = new _kiwi.model.Channel({name: channel_name, network: that, key: channel_key||undefined});
|
|
that.panels.add(channel);
|
|
}
|
|
|
|
panels.push(channel);
|
|
|
|
that.gateway.join(channel_name, channel_key);
|
|
});
|
|
|
|
|
|
return panels;
|
|
},
|
|
|
|
|
|
/**
|
|
* Join all the open channels we have open
|
|
* Reconnecting to a network would typically call this.
|
|
*/
|
|
rejoinAllChannels: function() {
|
|
var that = this;
|
|
|
|
this.panels.forEach(function(panel) {
|
|
if (!panel.isChannel())
|
|
return;
|
|
|
|
that.gateway.join(panel.get('name'), panel.get('key') || undefined);
|
|
});
|
|
},
|
|
|
|
isChannelName: function (channel_name) {
|
|
var channel_prefix = this.get('channel_prefix');
|
|
|
|
if (!channel_name || !channel_name.length) return false;
|
|
return (channel_prefix.indexOf(channel_name[0]) > -1);
|
|
},
|
|
|
|
// Check if a user is ignored.
|
|
// Accepts an object with nick, ident and hostname OR a string.
|
|
isUserIgnored: function (mask) {
|
|
var found_mask;
|
|
|
|
if (typeof mask === "object") {
|
|
mask = (mask.nick||'*')+'!'+(mask.ident||'*')+'@'+(mask.hostname||'*');
|
|
} else if (typeof mask === "string") {
|
|
mask = toUserMask(mask);
|
|
}
|
|
|
|
found_mask = this.ignore_list.find(function(entry) {
|
|
return entry.get('regex').test(mask);
|
|
});
|
|
|
|
return !!found_mask;
|
|
},
|
|
|
|
// Create a new query panel
|
|
createQuery: function (nick) {
|
|
var that = this,
|
|
query;
|
|
|
|
// Check if we have the panel already. If not, create it
|
|
query = that.panels.getByName(nick);
|
|
if (!query) {
|
|
query = new _kiwi.model.Query({name: nick, network: this});
|
|
that.panels.add(query);
|
|
}
|
|
|
|
// In all cases, show the demanded query
|
|
query.view.show();
|
|
|
|
return query;
|
|
}
|
|
});
|
|
|
|
|
|
|
|
function onDisconnect(event) {
|
|
this.set('connected', false);
|
|
|
|
$.each(this.panels.models, function (index, panel) {
|
|
if (!panel.isApplet()) {
|
|
panel.addMsg('', styleText('network_disconnected', {text: translateText('client_models_network_disconnected', [])}), 'action quit');
|
|
}
|
|
});
|
|
}
|
|
|
|
|
|
|
|
function onConnect(event) {
|
|
var panels, channel_names;
|
|
|
|
// Update our nick with what the network gave us
|
|
this.set('nick', event.nick);
|
|
|
|
this.set('connected', true);
|
|
|
|
this.ignore_list.loadFromNetwork(this);
|
|
|
|
// If this is a re-connection then we may have some channels to re-join
|
|
this.rejoinAllChannels();
|
|
|
|
// Auto joining channels
|
|
if (this.auto_join && this.auto_join.channel) {
|
|
panels = this.createAndJoinChannels(this.auto_join.channel + ' ' + (this.auto_join.key || ''));
|
|
|
|
// Show the last channel if we have one
|
|
if (panels)
|
|
panels[panels.length - 1].view.show();
|
|
|
|
delete this.auto_join;
|
|
}
|
|
}
|
|
|
|
|
|
|
|
function onOptions(event) {
|
|
var that = this;
|
|
|
|
$.each(event.options, function (name, value) {
|
|
switch (name) {
|
|
case 'CHANTYPES':
|
|
that.set('channel_prefix', value.join(''));
|
|
break;
|
|
case 'NETWORK':
|
|
that.set('name', value);
|
|
break;
|
|
case 'PREFIX':
|
|
that.set('user_prefixes', value);
|
|
break;
|
|
case 'NICKLEN':
|
|
that.set('nick_max_length', parseInt(value, 10));
|
|
break;
|
|
case 'CHANNELLEN':
|
|
that.set('channel_max_length', parseInt(value, 10));
|
|
break;
|
|
case 'TOPICLEN':
|
|
that.set('topic_max_length', parseInt(value, 10));
|
|
break;
|
|
}
|
|
});
|
|
|
|
this.set('cap', event.cap);
|
|
}
|
|
|
|
|
|
|
|
function onMotd(event) {
|
|
this.panels.server.addMsg(this.get('name'), styleText('motd', {text: event.msg}), 'motd');
|
|
}
|
|
|
|
|
|
|
|
function onJoin(event) {
|
|
var c, members, user;
|
|
c = this.panels.getByName(event.channel);
|
|
if (!c) {
|
|
c = new _kiwi.model.Channel({name: event.channel, network: this});
|
|
this.panels.add(c);
|
|
}
|
|
|
|
members = c.get('members');
|
|
if (!members) return;
|
|
|
|
// Do we already have this member?
|
|
if (members.getByNick(event.nick)) {
|
|
return;
|
|
}
|
|
|
|
user = new _kiwi.model.Member({
|
|
nick: event.nick,
|
|
ident: event.ident,
|
|
hostname: event.hostname,
|
|
user_prefixes: this.get('user_prefixes')
|
|
});
|
|
|
|
_kiwi.global.events.emit('channel:join', {channel: event.channel, user: user, network: this.gateway})
|
|
.then(function() {
|
|
members.add(user, {kiwi: event});
|
|
});
|
|
}
|
|
|
|
|
|
|
|
function onPart(event) {
|
|
var channel, members, user,
|
|
part_options = {};
|
|
|
|
part_options.type = 'part';
|
|
part_options.message = event.message || '';
|
|
part_options.time = event.time;
|
|
|
|
channel = this.panels.getByName(event.channel);
|
|
if (!channel) return;
|
|
|
|
// If this is us, close the panel
|
|
if (event.nick === this.get('nick')) {
|
|
channel.close();
|
|
return;
|
|
}
|
|
|
|
members = channel.get('members');
|
|
if (!members) return;
|
|
|
|
user = members.getByNick(event.nick);
|
|
if (!user) return;
|
|
|
|
_kiwi.global.events.emit('channel:leave', {channel: event.channel, user: user, type: 'part', message: part_options.message, network: this.gateway})
|
|
.then(function() {
|
|
members.remove(user, {kiwi: part_options});
|
|
});
|
|
}
|
|
|
|
|
|
|
|
function onQuit(event) {
|
|
var member, members,
|
|
quit_options = {};
|
|
|
|
quit_options.type = 'quit';
|
|
quit_options.message = event.message || '';
|
|
quit_options.time = event.time;
|
|
|
|
$.each(this.panels.models, function (index, panel) {
|
|
// Let any query panels know they quit
|
|
if (panel.isQuery() && panel.get('name').toLowerCase() === event.nick.toLowerCase()) {
|
|
panel.addMsg(' ', styleText('channel_quit', {
|
|
nick: event.nick,
|
|
text: translateText('client_models_channel_quit', [quit_options.message])
|
|
}), 'action quit', {time: quit_options.time});
|
|
}
|
|
|
|
// Remove the nick from any channels
|
|
if (panel.isChannel()) {
|
|
member = panel.get('members').getByNick(event.nick);
|
|
if (member) {
|
|
_kiwi.global.events.emit('channel:leave', {channel: panel.get('name'), user: member, type: 'quit', message: quit_options.message, network: this.gateway})
|
|
.then(function() {
|
|
panel.get('members').remove(member, {kiwi: quit_options});
|
|
});
|
|
}
|
|
}
|
|
});
|
|
}
|
|
|
|
|
|
|
|
function onKick(event) {
|
|
var channel, members, user,
|
|
part_options = {};
|
|
|
|
part_options.type = 'kick';
|
|
part_options.by = event.nick;
|
|
part_options.message = event.message || '';
|
|
part_options.current_user_kicked = (event.kicked == this.get('nick'));
|
|
part_options.current_user_initiated = (event.nick == this.get('nick'));
|
|
part_options.time = event.time;
|
|
|
|
channel = this.panels.getByName(event.channel);
|
|
if (!channel) return;
|
|
|
|
members = channel.get('members');
|
|
if (!members) return;
|
|
|
|
user = members.getByNick(event.kicked);
|
|
if (!user) return;
|
|
|
|
|
|
_kiwi.global.events.emit('channel:leave', {channel: event.channel, user: user, type: 'kick', message: part_options.message, network: this.gateway})
|
|
.then(function() {
|
|
members.remove(user, {kiwi: part_options});
|
|
|
|
if (part_options.current_user_kicked) {
|
|
members.reset([]);
|
|
}
|
|
});
|
|
}
|
|
|
|
|
|
|
|
function onMessage(event) {
|
|
_kiwi.global.events.emit('message:new', {network: this.gateway, message: event})
|
|
.then(_.bind(function() {
|
|
var panel,
|
|
is_pm = ((event.target || '').toLowerCase() == this.get('nick').toLowerCase());
|
|
|
|
// An ignored user? don't do anything with it
|
|
if (this.isUserIgnored(event)) {
|
|
return;
|
|
}
|
|
|
|
if (event.type == 'notice') {
|
|
if (event.from_server) {
|
|
panel = this.panels.server;
|
|
|
|
} else {
|
|
panel = this.panels.getByName(event.target) || this.panels.getByName(event.nick);
|
|
|
|
// Forward ChanServ messages to its associated channel
|
|
if (event.nick && event.nick.toLowerCase() == 'chanserv' && event.msg.charAt(0) == '[') {
|
|
channel_name = /\[([^ \]]+)\]/gi.exec(event.msg);
|
|
if (channel_name && channel_name[1]) {
|
|
channel_name = channel_name[1];
|
|
|
|
panel = this.panels.getByName(channel_name);
|
|
}
|
|
}
|
|
|
|
}
|
|
|
|
if (!panel) {
|
|
panel = this.panels.server;
|
|
}
|
|
|
|
} else if (is_pm) {
|
|
// If a panel isn't found for this PM and we allow new queries, create one
|
|
panel = this.panels.getByName(event.nick);
|
|
if (!panel && !_kiwi.global.settings.get('ignore_new_queries')) {
|
|
panel = new _kiwi.model.Query({name: event.nick, network: this});
|
|
this.panels.add(panel);
|
|
} else if(!panel) {
|
|
// We have not allowed new queries and we have not opened the panel ourselves, don't process the message
|
|
return;
|
|
}
|
|
|
|
} else {
|
|
// If a panel isn't found for this target, reroute to the
|
|
// server panel
|
|
panel = this.panels.getByName(event.target);
|
|
if (!panel) {
|
|
panel = this.panels.server;
|
|
}
|
|
}
|
|
|
|
switch (event.type){
|
|
case 'message':
|
|
panel.addMsg(event.nick, styleText('privmsg', {text: event.msg}), 'privmsg', {time: event.time});
|
|
break;
|
|
|
|
case 'action':
|
|
panel.addMsg('', styleText('action', {nick: event.nick, text: event.msg}), 'action', {time: event.time});
|
|
break;
|
|
|
|
case 'notice':
|
|
panel.addMsg('[' + (event.nick||'') + ']', styleText('notice', {text: event.msg}), 'notice', {time: event.time});
|
|
|
|
// Show this notice to the active panel if it didn't have a set target, but only in an active channel or query window
|
|
active_panel = _kiwi.app.panels().active;
|
|
|
|
if (!event.from_server && panel === this.panels.server && active_panel !== this.panels.server) {
|
|
if (active_panel.get('network') === this && (active_panel.isChannel() || active_panel.isQuery()))
|
|
active_panel.addMsg('[' + (event.nick||'') + ']', styleText('notice', {text: event.msg}), 'notice', {time: event.time});
|
|
}
|
|
break;
|
|
}
|
|
}, this));
|
|
}
|
|
|
|
|
|
|
|
function onNick(event) {
|
|
var member;
|
|
|
|
$.each(this.panels.models, function (index, panel) {
|
|
if (panel.get('name') == event.nick)
|
|
panel.set('name', event.newnick);
|
|
|
|
if (!panel.isChannel()) return;
|
|
|
|
member = panel.get('members').getByNick(event.nick);
|
|
if (member) {
|
|
member.set('nick', event.newnick);
|
|
panel.addMsg('', styleText('nick_changed', {nick: event.nick, text: translateText('client_models_network_nickname_changed', [event.newnick]), channel: name}), 'action nick', {time: event.time});
|
|
}
|
|
});
|
|
}
|
|
|
|
|
|
|
|
function onCtcpRequest(event) {
|
|
// An ignored user? don't do anything with it
|
|
if (this.isUserIgnored(event)) {
|
|
return;
|
|
}
|
|
|
|
// Reply to a TIME ctcp
|
|
if (event.msg.toUpperCase() === 'TIME') {
|
|
this.gateway.ctcpResponse(event.type, event.nick, (new Date()).toString());
|
|
} else if(event.type.toUpperCase() === 'PING') { // CTCP PING reply
|
|
this.gateway.ctcpResponse(event.type, event.nick, event.msg.substr(5));
|
|
}
|
|
}
|
|
|
|
|
|
|
|
function onCtcpResponse(event) {
|
|
// An ignored user? don't do anything with it
|
|
if (this.isUserIgnored(event)) {
|
|
return;
|
|
}
|
|
|
|
this.panels.server.addMsg('[' + event.nick + ']', styleText('ctcp', {text: event.msg}), 'ctcp', {time: event.time});
|
|
}
|
|
|
|
|
|
|
|
function onTopic(event) {
|
|
var c;
|
|
c = this.panels.getByName(event.channel);
|
|
if (!c) return;
|
|
|
|
// Set the channels topic
|
|
c.set('topic', event.topic);
|
|
|
|
// If this is the active channel, update the topic bar too
|
|
if (c.get('name') === this.panels.active.get('name')) {
|
|
_kiwi.app.topicbar.setCurrentTopic(event.topic);
|
|
}
|
|
}
|
|
|
|
|
|
|
|
function onTopicSetBy(event) {
|
|
var c, when;
|
|
c = this.panels.getByName(event.channel);
|
|
if (!c) return;
|
|
|
|
when = new Date(event.when * 1000);
|
|
c.set('topic_set_by', {nick: event.nick, when: when});
|
|
}
|
|
|
|
|
|
|
|
function onChannelInfo(event) {
|
|
var channel = this.panels.getByName(event.channel);
|
|
if (!channel) return;
|
|
|
|
if (event.url) {
|
|
channel.set('info_url', event.url);
|
|
} else if (event.modes) {
|
|
channel.set('info_modes', event.modes);
|
|
}
|
|
}
|
|
|
|
|
|
|
|
function onUserlist(event) {
|
|
var that = this,
|
|
channel = this.panels.getByName(event.channel);
|
|
|
|
// If we didn't find a channel for this, may aswell leave
|
|
if (!channel) return;
|
|
|
|
channel.temp_userlist = channel.temp_userlist || [];
|
|
_.each(event.users, function (item) {
|
|
var user = new _kiwi.model.Member({
|
|
nick: item.nick,
|
|
modes: item.modes,
|
|
user_prefixes: that.get('user_prefixes')
|
|
});
|
|
channel.temp_userlist.push(user);
|
|
});
|
|
}
|
|
|
|
|
|
|
|
function onUserlistEnd(event) {
|
|
var channel;
|
|
channel = this.panels.getByName(event.channel);
|
|
|
|
// If we didn't find a channel for this, may aswell leave
|
|
if (!channel) return;
|
|
|
|
// Update the members list with the new list
|
|
channel.get('members').reset(channel.temp_userlist || []);
|
|
|
|
// Clear the temporary userlist
|
|
delete channel.temp_userlist;
|
|
}
|
|
|
|
|
|
|
|
function onBanlist(event) {
|
|
var channel = this.panels.getByName(event.channel);
|
|
if (!channel)
|
|
return;
|
|
|
|
channel.set('banlist', event.bans || []);
|
|
}
|
|
|
|
|
|
|
|
function onMode(event) {
|
|
var channel, i, prefixes, members, member, find_prefix,
|
|
request_updated_banlist = false;
|
|
|
|
// Build a nicely formatted string to be displayed to a regular human
|
|
function friendlyModeString (event_modes, alt_target) {
|
|
var modes = {}, return_string;
|
|
|
|
// If no default given, use the main event info
|
|
if (!event_modes) {
|
|
event_modes = event.modes;
|
|
alt_target = event.target;
|
|
}
|
|
|
|
// Reformat the mode object to make it easier to work with
|
|
_.each(event_modes, function (mode){
|
|
var param = mode.param || alt_target || '';
|
|
|
|
// Make sure we have some modes for this param
|
|
if (!modes[param]) {
|
|
modes[param] = {'+':'', '-':''};
|
|
}
|
|
|
|
modes[param][mode.mode[0]] += mode.mode.substr(1);
|
|
});
|
|
|
|
// Put the string together from each mode
|
|
return_string = [];
|
|
_.each(modes, function (modeset, param) {
|
|
var str = '';
|
|
if (modeset['+']) str += '+' + modeset['+'];
|
|
if (modeset['-']) str += '-' + modeset['-'];
|
|
return_string.push(str + ' ' + param);
|
|
});
|
|
return_string = return_string.join(', ');
|
|
|
|
return return_string;
|
|
}
|
|
|
|
|
|
channel = this.panels.getByName(event.target);
|
|
if (channel) {
|
|
prefixes = this.get('user_prefixes');
|
|
find_prefix = function (p) {
|
|
return event.modes[i].mode[1] === p.mode;
|
|
};
|
|
for (i = 0; i < event.modes.length; i++) {
|
|
if (_.any(prefixes, find_prefix)) {
|
|
if (!members) {
|
|
members = channel.get('members');
|
|
}
|
|
member = members.getByNick(event.modes[i].param);
|
|
if (!member) {
|
|
console.log('MODE command recieved for unknown member %s on channel %s', event.modes[i].param, event.target);
|
|
return;
|
|
} else {
|
|
if (event.modes[i].mode[0] === '+') {
|
|
member.addMode(event.modes[i].mode[1]);
|
|
} else if (event.modes[i].mode[0] === '-') {
|
|
member.removeMode(event.modes[i].mode[1]);
|
|
}
|
|
members.sort();
|
|
}
|
|
} else {
|
|
// Channel mode being set
|
|
// TODO: Store this somewhere?
|
|
//channel.addMsg('', 'CHANNEL === ' + event.nick + ' set mode ' + event.modes[i].mode + ' on ' + event.target, 'action mode');
|
|
}
|
|
|
|
// TODO: Be smart, remove this specific ban from the banlist rather than request a whole banlist
|
|
if (event.modes[i].mode[1] == 'b')
|
|
request_updated_banlist = true;
|
|
|
|
// Remember the key being set
|
|
if (event.modes[i].mode[1] == 'k') {
|
|
if (event.modes[i].mode[0] === '+') {
|
|
channel.set('key', event.modes[i].param);
|
|
} else if (event.modes[i].mode[0] === '-') {
|
|
channel.set('key', undefined);
|
|
}
|
|
}
|
|
}
|
|
|
|
channel.addMsg('', styleText('mode', {nick: event.nick, text: translateText('client_models_network_mode', [friendlyModeString()]), channel: event.target}), 'action mode', {time: event.time});
|
|
|
|
// TODO: Be smart, remove the specific ban from the banlist rather than request a whole banlist
|
|
if (request_updated_banlist)
|
|
this.gateway.raw('MODE ' + channel.get('name') + ' +b');
|
|
|
|
} else {
|
|
// This is probably a mode being set on us.
|
|
if (event.target.toLowerCase() === this.get("nick").toLowerCase()) {
|
|
this.panels.server.addMsg('', styleText('selfmode', {nick: event.nick, text: translateText('client_models_network_mode', [friendlyModeString()]), channel: event.target}), 'action mode');
|
|
} else {
|
|
console.log('MODE command recieved for unknown target %s: ', event.target, event);
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
|
|
function onWhois(event) {
|
|
_kiwi.global.events.emit('whois', {nick: event.nick, network: this.gateway, whois: event})
|
|
.then(function() {
|
|
var logon_date, idle_time = '', panel;
|
|
|
|
if (event.end)
|
|
return;
|
|
|
|
if (typeof event.idle !== 'undefined') {
|
|
idle_time = secondsToTime(parseInt(event.idle, 10));
|
|
idle_time = idle_time.h.toString().lpad(2, "0") + ':' + idle_time.m.toString().lpad(2, "0") + ':' + idle_time.s.toString().lpad(2, "0");
|
|
}
|
|
|
|
panel = _kiwi.app.panels().active;
|
|
if (event.ident) {
|
|
panel.addMsg(event.nick, styleText('whois_ident', {nick: event.nick, ident: event.ident, host: event.hostname, text: event.msg}), 'whois');
|
|
|
|
} else if (event.chans) {
|
|
panel.addMsg(event.nick, styleText('whois_channels', {nick: event.nick, text: translateText('client_models_network_channels', [event.chans])}), 'whois');
|
|
} else if (event.irc_server) {
|
|
panel.addMsg(event.nick, styleText('whois_server', {nick: event.nick, text: translateText('client_models_network_server', [event.irc_server, event.server_info])}), 'whois');
|
|
} else if (event.msg) {
|
|
panel.addMsg(event.nick, styleText('whois', {text: event.msg}), 'whois');
|
|
} else if (event.logon) {
|
|
logon_date = new Date();
|
|
logon_date.setTime(event.logon * 1000);
|
|
logon_date = _kiwi.utils.formatDate(logon_date);
|
|
|
|
panel.addMsg(event.nick, styleText('whois_idle_and_signon', {nick: event.nick, text: translateText('client_models_network_idle_and_signon', [idle_time, logon_date])}), 'whois');
|
|
} else if (event.away_reason) {
|
|
panel.addMsg(event.nick, styleText('whois_away', {nick: event.nick, text: translateText('client_models_network_away', [event.away_reason])}), 'whois');
|
|
} else {
|
|
panel.addMsg(event.nick, styleText('whois_idle', {nick: event.nick, text: translateText('client_models_network_idle', [idle_time])}), 'whois');
|
|
}
|
|
});
|
|
}
|
|
|
|
function onWhowas(event) {
|
|
var panel;
|
|
|
|
if (event.end)
|
|
return;
|
|
|
|
panel = _kiwi.app.panels().active;
|
|
if (event.hostname) {
|
|
panel.addMsg(event.nick, styleText('who', {nick: event.nick, ident: event.ident, host: event.hostname, realname: event.real_name, text: event.msg}), 'whois');
|
|
} else {
|
|
panel.addMsg(event.nick, styleText('whois_notfound', {nick: event.nick, text: translateText('client_models_network_nickname_notfound', [])}), 'whois');
|
|
}
|
|
}
|
|
|
|
|
|
function onAway(event) {
|
|
$.each(this.panels.models, function (index, panel) {
|
|
if (!panel.isChannel()) return;
|
|
|
|
member = panel.get('members').getByNick(event.nick);
|
|
if (member) {
|
|
member.set('away', !(!event.reason));
|
|
}
|
|
});
|
|
}
|
|
|
|
|
|
|
|
function onListStart(event) {
|
|
var chanlist = _kiwi.model.Applet.loadOnce('kiwi_chanlist');
|
|
chanlist.view.show();
|
|
}
|
|
|
|
|
|
|
|
function onIrcError(event) {
|
|
var panel, tmp;
|
|
|
|
if (event.channel !== undefined && !(panel = this.panels.getByName(event.channel))) {
|
|
panel = this.panels.server;
|
|
}
|
|
|
|
switch (event.error) {
|
|
case 'banned_from_channel':
|
|
panel.addMsg(' ', styleText('channel_banned', {nick: event.nick, text: translateText('client_models_network_banned', [event.channel, event.reason]), channel: event.channel}), 'status');
|
|
_kiwi.app.message.text(_kiwi.global.i18n.translate('client_models_network_banned').fetch(event.channel, event.reason));
|
|
break;
|
|
case 'bad_channel_key':
|
|
panel.addMsg(' ', styleText('channel_badkey', {nick: event.nick, text: translateText('client_models_network_channel_badkey', [event.channel]), channel: event.channel}), 'status');
|
|
_kiwi.app.message.text(_kiwi.global.i18n.translate('client_models_network_channel_badkey').fetch(event.channel));
|
|
break;
|
|
case 'invite_only_channel':
|
|
panel.addMsg(' ', styleText('channel_inviteonly', {nick: event.nick, text: translateText('client_models_network_channel_inviteonly', [event.nick, event.channel]), channel: event.channel}), 'status');
|
|
_kiwi.app.message.text(event.channel + ' ' + _kiwi.global.i18n.translate('client_models_network_channel_inviteonly').fetch());
|
|
break;
|
|
case 'user_on_channel':
|
|
panel.addMsg(' ', styleText('channel_alreadyin', {nick: event.nick, text: translateText('client_models_network_channel_alreadyin'), channel: event.channel}));
|
|
break;
|
|
case 'channel_is_full':
|
|
panel.addMsg(' ', styleText('channel_limitreached', {nick: event.nick, text: translateText('client_models_network_channel_limitreached', [event.channel]), channel: event.channel}), 'status');
|
|
_kiwi.app.message.text(event.channel + ' ' + _kiwi.global.i18n.translate('client_models_network_channel_limitreached').fetch(event.channel));
|
|
break;
|
|
case 'chanop_privs_needed':
|
|
panel.addMsg(' ', styleText('chanop_privs_needed', {text: event.reason, channel: event.channel}), 'status');
|
|
_kiwi.app.message.text(event.reason + ' (' + event.channel + ')');
|
|
break;
|
|
case 'cannot_send_to_channel':
|
|
panel.addMsg(' ', '== ' + event.reason, 'status');
|
|
break;
|
|
case 'no_such_nick':
|
|
tmp = this.panels.getByName(event.nick);
|
|
if (tmp) {
|
|
tmp.addMsg(' ', styleText('no_such_nick', {nick: event.nick, text: event.reason, channel: event.channel}), 'status');
|
|
} else {
|
|
this.panels.server.addMsg(' ', styleText('no_such_nick', {nick: event.nick, text: event.reason, channel: event.channel}), 'status');
|
|
}
|
|
break;
|
|
case 'nickname_in_use':
|
|
this.panels.server.addMsg(' ', styleText('nickname_alreadyinuse', {nick: event.nick, text: translateText('client_models_network_nickname_alreadyinuse', [event.nick]), channel: event.channel}), 'status');
|
|
if (this.panels.server !== this.panels.active) {
|
|
_kiwi.app.message.text(_kiwi.global.i18n.translate('client_models_network_nickname_alreadyinuse').fetch(event.nick));
|
|
}
|
|
|
|
// Only show the nickchange component if the controlbox is open
|
|
if (_kiwi.app.controlbox.$el.css('display') !== 'none') {
|
|
(new _kiwi.view.NickChangeBox()).render();
|
|
}
|
|
|
|
break;
|
|
|
|
case 'password_mismatch':
|
|
this.panels.server.addMsg(' ', styleText('channel_badpassword', {nick: event.nick, text: translateText('client_models_network_badpassword', []), channel: event.channel}), 'status');
|
|
break;
|
|
|
|
case 'error':
|
|
if (event.reason) {
|
|
this.panels.server.addMsg(' ', styleText('general_error', {text: event.reason}), 'status');
|
|
}
|
|
break;
|
|
|
|
default:
|
|
// We don't know what data contains, so don't do anything with it.
|
|
//_kiwi.front.tabviews.server.addMsg(null, ' ', '== ' + data, 'status');
|
|
}
|
|
}
|
|
|
|
|
|
function onUnknownCommand(event) {
|
|
var display_params = _.clone(event.params);
|
|
|
|
// A lot of commands have our nick as the first parameter. This is redundant for us
|
|
if (display_params[0] && display_params[0] == this.get('nick')) {
|
|
display_params.shift();
|
|
}
|
|
|
|
this.panels.server.addMsg('', styleText('unknown_command', {text: '[' + event.command + '] ' + display_params.join(', ', '')}));
|
|
}
|
|
|
|
|
|
function onWallops(event) {
|
|
var active_panel = _kiwi.app.panels().active;
|
|
|
|
// Send to server panel
|
|
this.panels.server.addMsg('[' + (event.nick||'') + ']', styleText('wallops', {text: event.msg}), 'wallops', {time: event.time});
|
|
|
|
// Send to active panel if its a channel/query *and* it's related to this network
|
|
if (active_panel !== this.panels.server && (active_panel.isChannel() || active_panel.isQuery()) && active_panel.get('network') === this)
|
|
active_panel.addMsg('[' + (event.nick||'') + ']', styleText('wallops', {text: event.msg}), 'wallops', {time: event.time});
|
|
}
|
|
|
|
}
|
|
|
|
)();
|
|
|
|
|
|
|
|
_kiwi.model.Member = Backbone.Model.extend({
|
|
initialize: function (attributes) {
|
|
var nick, modes, prefix;
|
|
|
|
// The nick may have a mode prefix, we don't want this
|
|
nick = this.stripPrefix(this.get("nick"));
|
|
|
|
// Make sure we have a mode array, and that it's sorted
|
|
modes = this.get("modes");
|
|
modes = modes || [];
|
|
this.sortModes(modes);
|
|
|
|
this.set({"nick": nick, "modes": modes, "prefix": this.getPrefix(modes)}, {silent: true});
|
|
|
|
this.updateOpStatus();
|
|
|
|
this.view = new _kiwi.view.Member({"model": this});
|
|
},
|
|
|
|
|
|
/**
|
|
* Sort modes in order of importance
|
|
*/
|
|
sortModes: function (modes) {
|
|
var that = this;
|
|
|
|
return modes.sort(function (a, b) {
|
|
var a_idx, b_idx, i;
|
|
var user_prefixes = that.get('user_prefixes');
|
|
|
|
for (i = 0; i < user_prefixes.length; i++) {
|
|
if (user_prefixes[i].mode === a) {
|
|
a_idx = i;
|
|
}
|
|
}
|
|
|
|
for (i = 0; i < user_prefixes.length; i++) {
|
|
if (user_prefixes[i].mode === b) {
|
|
b_idx = i;
|
|
}
|
|
}
|
|
|
|
if (a_idx < b_idx) {
|
|
return -1;
|
|
} else if (a_idx > b_idx) {
|
|
return 1;
|
|
} else {
|
|
return 0;
|
|
}
|
|
});
|
|
},
|
|
|
|
|
|
addMode: function (mode) {
|
|
var modes_to_add = mode.split(''),
|
|
modes, prefix;
|
|
|
|
modes = this.get("modes");
|
|
$.each(modes_to_add, function (index, item) {
|
|
modes.push(item);
|
|
});
|
|
|
|
modes = this.sortModes(modes);
|
|
this.set({"prefix": this.getPrefix(modes), "modes": modes});
|
|
|
|
this.updateOpStatus();
|
|
|
|
this.view.render();
|
|
},
|
|
|
|
|
|
removeMode: function (mode) {
|
|
var modes_to_remove = mode.split(''),
|
|
modes, prefix;
|
|
|
|
modes = this.get("modes");
|
|
modes = _.reject(modes, function (m) {
|
|
return (_.indexOf(modes_to_remove, m) !== -1);
|
|
});
|
|
|
|
this.set({"prefix": this.getPrefix(modes), "modes": modes});
|
|
|
|
this.updateOpStatus();
|
|
|
|
this.view.render();
|
|
},
|
|
|
|
|
|
/**
|
|
* Figure out a valid prefix given modes.
|
|
* If a user is an op but also has voice, the prefix
|
|
* should be the op as it is more important.
|
|
*/
|
|
getPrefix: function (modes) {
|
|
var prefix = '';
|
|
var user_prefixes = this.get('user_prefixes');
|
|
|
|
if (typeof modes[0] !== 'undefined') {
|
|
prefix = _.detect(user_prefixes, function (prefix) {
|
|
return prefix.mode === modes[0];
|
|
});
|
|
|
|
prefix = (prefix) ? prefix.symbol : '';
|
|
}
|
|
|
|
return prefix;
|
|
},
|
|
|
|
|
|
/**
|
|
* Remove any recognised prefix from a nick
|
|
*/
|
|
stripPrefix: function (nick) {
|
|
var tmp = nick, i, j, k, nick_char;
|
|
var user_prefixes = this.get('user_prefixes');
|
|
|
|
i = 0;
|
|
|
|
nick_character_loop:
|
|
for (j = 0; j < nick.length; j++) {
|
|
nick_char = nick.charAt(j);
|
|
|
|
for (k = 0; k < user_prefixes.length; k++) {
|
|
if (nick_char === user_prefixes[k].symbol) {
|
|
i++;
|
|
continue nick_character_loop;
|
|
}
|
|
}
|
|
|
|
break;
|
|
}
|
|
|
|
return tmp.substr(i);
|
|
},
|
|
|
|
|
|
|
|
/**
|
|
* Format this nick into readable format (eg. nick [ident@hostname])
|
|
*/
|
|
displayNick: function (full) {
|
|
var display = this.get('nick');
|
|
|
|
if (full) {
|
|
if (this.get("ident")) {
|
|
display += ' [' + this.get("ident") + '@' + this.get("hostname") + ']';
|
|
}
|
|
}
|
|
|
|
return display;
|
|
},
|
|
|
|
|
|
// Helper to quickly get user mask details
|
|
getMaskParts: function () {
|
|
return {
|
|
nick: this.get('nick') || '',
|
|
ident: this.get('ident') || '',
|
|
hostname: this.get('hostname') || ''
|
|
};
|
|
},
|
|
|
|
|
|
/**
|
|
* With the modes set on the user, make note if we have some sort of op status
|
|
*/
|
|
updateOpStatus: function () {
|
|
var user_prefixes = this.get('user_prefixes'),
|
|
modes = this.get('modes'),
|
|
o, max_mode;
|
|
|
|
if (modes.length > 0) {
|
|
o = _.indexOf(user_prefixes, _.find(user_prefixes, function (prefix) {
|
|
return prefix.mode === 'o';
|
|
}));
|
|
|
|
max_mode = _.indexOf(user_prefixes, _.find(user_prefixes, function (prefix) {
|
|
return prefix.mode === modes[0];
|
|
}));
|
|
|
|
if ((max_mode === -1) || (max_mode > o)) {
|
|
this.set({"is_op": false}, {silent: true});
|
|
} else {
|
|
this.set({"is_op": true}, {silent: true});
|
|
}
|
|
|
|
} else {
|
|
this.set({"is_op": false}, {silent: true});
|
|
}
|
|
}
|
|
});
|
|
|
|
|
|
_kiwi.model.MemberList = Backbone.Collection.extend({
|
|
model: _kiwi.model.Member,
|
|
comparator: function (a, b) {
|
|
var i, a_modes, b_modes, a_idx, b_idx, a_nick, b_nick;
|
|
var user_prefixes = this.channel.get('network').get('user_prefixes');
|
|
|
|
a_modes = a.get("modes");
|
|
b_modes = b.get("modes");
|
|
|
|
// Try to sort by modes first
|
|
if (a_modes.length > 0) {
|
|
// a has modes, but b doesn't so a should appear first
|
|
if (b_modes.length === 0) {
|
|
return -1;
|
|
}
|
|
a_idx = b_idx = -1;
|
|
// Compare the first (highest) mode
|
|
for (i = 0; i < user_prefixes.length; i++) {
|
|
if (user_prefixes[i].mode === a_modes[0]) {
|
|
a_idx = i;
|
|
}
|
|
}
|
|
for (i = 0; i < user_prefixes.length; i++) {
|
|
if (user_prefixes[i].mode === b_modes[0]) {
|
|
b_idx = i;
|
|
}
|
|
}
|
|
if (a_idx < b_idx) {
|
|
return -1;
|
|
} else if (a_idx > b_idx) {
|
|
return 1;
|
|
}
|
|
// If we get to here both a and b have the same highest mode so have to resort to lexicographical sorting
|
|
|
|
} else if (b_modes.length > 0) {
|
|
// b has modes but a doesn't so b should appear first
|
|
return 1;
|
|
}
|
|
a_nick = a.get("nick").toLocaleLowerCase();
|
|
b_nick = b.get("nick").toLocaleLowerCase();
|
|
// Lexicographical sorting
|
|
if (a_nick < b_nick) {
|
|
return -1;
|
|
} else if (a_nick > b_nick) {
|
|
return 1;
|
|
} else {
|
|
return 0;
|
|
}
|
|
},
|
|
|
|
|
|
initialize: function (options) {
|
|
this.view = new _kiwi.view.MemberList({"model": this});
|
|
this.initNickCache();
|
|
},
|
|
|
|
|
|
/*
|
|
* Keep a reference to each member by the nick. Speeds up .getByNick()
|
|
* so it doesn't need to loop over every model for each nick lookup
|
|
*/
|
|
initNickCache: function() {
|
|
var updateRegex = _.bind(function () {
|
|
// Allows checking for a nick that contains 'the_nick' or '<punctuation>the_nick<punctuation>'
|
|
// .. where <punctuation> is any character not allowed in an IRC nick
|
|
var regex_valid_nick_chars = 'a-z0-9_\\-{}[\\]^`|\\\\';
|
|
var regex_nicks = Object.keys(this.nick_cache)
|
|
.map(_.escapeRegExp)
|
|
.join('|');
|
|
|
|
this.nick_regex = new RegExp(
|
|
'^[^'+regex_valid_nick_chars+']?(' + regex_nicks + ')[^'+regex_valid_nick_chars+']?$', 'i'
|
|
);
|
|
}, this);
|
|
|
|
function getNick (member) {
|
|
return member.get('nick').toLowerCase();
|
|
};
|
|
|
|
this.nick_cache = Object.create(null);
|
|
this.nick_regex = null;
|
|
|
|
this.on('reset', function() {
|
|
this.nick_cache = _.reduce(this.models, function(memo, member) {
|
|
memo[getNick(member)] = member;
|
|
return memo;
|
|
}, Object.create(null));
|
|
updateRegex();
|
|
});
|
|
|
|
this.on('add', function(member) {
|
|
this.nick_cache[getNick(member)] = member;
|
|
updateRegex();
|
|
});
|
|
|
|
this.on('remove', function(member) {
|
|
delete this.nick_cache[getNick(member)];
|
|
updateRegex();
|
|
});
|
|
|
|
this.on('change:nick', function(member) {
|
|
this.nick_cache[getNick(member)] = member;
|
|
delete this.nick_cache[member.previous('nick').toLowerCase()];
|
|
updateRegex();
|
|
});
|
|
},
|
|
|
|
getByNick: function (nick) {
|
|
var matches;
|
|
if (this.nick_regex && (matches = this.nick_regex.exec(nick))) {
|
|
return this.nick_cache[matches[1].toLowerCase()];
|
|
}
|
|
}
|
|
});
|
|
|
|
|
|
|
|
_kiwi.model.IgnoreList = Backbone.Collection.extend({
|
|
initialize: function() {
|
|
this.network_address = '';
|
|
this.ignore_data = _kiwi.model.DataStore.instance('kiwi.ignore_list');
|
|
this.ignore_data.load();
|
|
|
|
this.on('add', _.bind(this.onAdd, this));
|
|
this.on('add', _.bind(this.saveList, this));
|
|
this.on('remove', _.bind(this.saveList, this));
|
|
},
|
|
|
|
|
|
onAdd: function(entry) {
|
|
if (!entry.get('mask')) return;
|
|
|
|
if (!entry.get('time')) {
|
|
entry.set('time', (new Date()).getTime());
|
|
}
|
|
|
|
if (!entry.get('regex')) {
|
|
entry.set('regex', toUserMask(entry.get('mask'), true)[1]);
|
|
}
|
|
},
|
|
|
|
|
|
loadFromNetwork: function(network) {
|
|
this.network_address = network.get('address').toLowerCase();
|
|
|
|
var ignore_list = this.ignore_data.get(this.network_address) || [];
|
|
|
|
_.each(ignore_list, function(item, idx) {
|
|
if (!item || !item.mask) return;
|
|
|
|
// Make the regex for the given user mask
|
|
item.regex = toUserMask(item.mask, true)[1];
|
|
});
|
|
|
|
this.reset(ignore_list);
|
|
},
|
|
|
|
|
|
saveList: function() {
|
|
var list = [];
|
|
|
|
this.forEach(function(entry) {
|
|
var obj = _.clone(entry.attributes);
|
|
delete obj.regex;
|
|
list.push(obj);
|
|
});
|
|
|
|
this.ignore_data.set(this.network_address, list);
|
|
this.ignore_data.save();
|
|
},
|
|
|
|
|
|
addMask: function(mask) {
|
|
return this.add({mask: mask});
|
|
},
|
|
|
|
|
|
removeMask: function(mask) {
|
|
var entry = this.find(function(entry) {
|
|
return entry.get('mask') == mask;
|
|
});
|
|
|
|
if (entry) {
|
|
this.remove(entry);
|
|
}
|
|
}
|
|
});
|
|
|
|
|
|
|
|
_kiwi.model.NewConnection = Backbone.Collection.extend({
|
|
initialize: function() {
|
|
this.view = new _kiwi.view.ServerSelect({model: this});
|
|
|
|
this.view.bind('server_connect', this.onMakeConnection, this);
|
|
|
|
},
|
|
|
|
|
|
populateDefaultServerSettings: function() {
|
|
var defaults = _kiwi.global.defaultServerSettings();
|
|
this.view.populateFields(defaults);
|
|
},
|
|
|
|
|
|
onMakeConnection: function(new_connection_event) {
|
|
var that = this;
|
|
|
|
this.connect_details = new_connection_event;
|
|
|
|
this.view.networkConnecting();
|
|
|
|
_kiwi.gateway.newConnection({
|
|
nick: new_connection_event.nick,
|
|
host: new_connection_event.server,
|
|
port: new_connection_event.port,
|
|
ssl: new_connection_event.ssl,
|
|
password: new_connection_event.password,
|
|
options: new_connection_event.options
|
|
}, function(err, network) {
|
|
that.onNewNetwork(err, network);
|
|
});
|
|
},
|
|
|
|
|
|
onNewNetwork: function(err, network) {
|
|
// Show any errors if given
|
|
if (err) {
|
|
this.view.showError(err);
|
|
}
|
|
|
|
if (network && this.connect_details) {
|
|
network.auto_join = {
|
|
channel: this.connect_details.channel,
|
|
key: this.connect_details.channel_key
|
|
};
|
|
|
|
this.trigger('new_network', network);
|
|
}
|
|
}
|
|
});
|
|
|
|
|
|
_kiwi.model.Panel = Backbone.Model.extend({
|
|
initialize: function (attributes) {
|
|
var name = this.get("name") || "";
|
|
this.view = new _kiwi.view.Panel({"model": this, "name": name});
|
|
this.set({
|
|
"scrollback": [],
|
|
"name": name
|
|
}, {"silent": true});
|
|
|
|
_kiwi.global.events.emit('panel:created', {panel: this});
|
|
},
|
|
|
|
close: function () {
|
|
_kiwi.app.panels.trigger('close', this);
|
|
_kiwi.global.events.emit('panel:close', {panel: this});
|
|
|
|
if (this.view) {
|
|
this.view.unbind();
|
|
this.view.remove();
|
|
this.view = undefined;
|
|
delete this.view;
|
|
}
|
|
|
|
var members = this.get('members');
|
|
if (members) {
|
|
members.reset([]);
|
|
this.unset('members');
|
|
}
|
|
|
|
this.get('panel_list').remove(this);
|
|
|
|
this.unbind();
|
|
this.destroy();
|
|
},
|
|
|
|
isChannel: function () {
|
|
return false;
|
|
},
|
|
|
|
isQuery: function () {
|
|
return false;
|
|
},
|
|
|
|
isApplet: function () {
|
|
return false;
|
|
},
|
|
|
|
isServer: function () {
|
|
return false;
|
|
},
|
|
|
|
isActive: function () {
|
|
return (_kiwi.app.panels().active === this);
|
|
}
|
|
});
|
|
|
|
|
|
_kiwi.model.PanelList = Backbone.Collection.extend({
|
|
model: _kiwi.model.Panel,
|
|
|
|
comparator: function (chan) {
|
|
return chan.get('name');
|
|
},
|
|
initialize: function (elements, network) {
|
|
var that = this;
|
|
|
|
// If this PanelList is associated with a network/connection
|
|
if (network) {
|
|
this.network = network;
|
|
}
|
|
|
|
this.view = new _kiwi.view.Tabs({model: this});
|
|
|
|
// Holds the active panel
|
|
this.active = null;
|
|
|
|
// Keep a tab on the active panel
|
|
this.bind('active', function (active_panel) {
|
|
this.active = active_panel;
|
|
}, this);
|
|
|
|
this.bind('add', function(panel) {
|
|
panel.set('panel_list', this);
|
|
});
|
|
},
|
|
|
|
|
|
|
|
getByCid: function (cid) {
|
|
if (typeof name !== 'string') return;
|
|
|
|
return this.find(function (c) {
|
|
return cid === c.cid;
|
|
});
|
|
},
|
|
|
|
|
|
|
|
getByName: function (name) {
|
|
if (typeof name !== 'string') return;
|
|
|
|
return this.find(function (c) {
|
|
return name.toLowerCase() === c.get('name').toLowerCase();
|
|
});
|
|
}
|
|
});
|
|
|
|
|
|
|
|
_kiwi.model.NetworkPanelList = Backbone.Collection.extend({
|
|
model: _kiwi.model.Network,
|
|
|
|
initialize: function() {
|
|
this.view = new _kiwi.view.NetworkTabs({model: this});
|
|
|
|
this.on('add', this.onNetworkAdd, this);
|
|
this.on('remove', this.onNetworkRemove, this);
|
|
|
|
// Current active connection / panel
|
|
this.active_connection = undefined;
|
|
this.active_panel = undefined;
|
|
|
|
// TODO: Remove this - legacy
|
|
this.active = undefined;
|
|
},
|
|
|
|
getByConnectionId: function(id) {
|
|
return this.find(function(connection){
|
|
return connection.get('connection_id') == id;
|
|
});
|
|
},
|
|
|
|
panels: function() {
|
|
var panels = [];
|
|
|
|
this.each(function(network) {
|
|
panels = panels.concat(network.panels.models);
|
|
});
|
|
|
|
return panels;
|
|
},
|
|
|
|
|
|
onNetworkAdd: function(network) {
|
|
network.panels.on('active', this.onPanelActive, this);
|
|
|
|
// if it's our first connection, set it active
|
|
if (this.models.length === 1) {
|
|
this.active_connection = network;
|
|
this.active_panel = network.panels.server;
|
|
|
|
// TODO: Remove this - legacy
|
|
this.active = this.active_panel;
|
|
}
|
|
},
|
|
|
|
onNetworkRemove: function(network) {
|
|
network.panels.off('active', this.onPanelActive, this);
|
|
},
|
|
|
|
onPanelActive: function(panel) {
|
|
var connection = this.getByConnectionId(panel.tab.data('connection_id'));
|
|
this.trigger('active', panel, connection);
|
|
|
|
this.active_connection = connection;
|
|
this.active_panel = panel;
|
|
|
|
// TODO: Remove this - legacy
|
|
this.active = panel;
|
|
}
|
|
});
|
|
|
|
|
|
// TODO: Channel modes
|
|
// TODO: Listen to gateway events for anythign related to this channel
|
|
_kiwi.model.Channel = _kiwi.model.Panel.extend({
|
|
initialize: function (attributes) {
|
|
var name = this.get("name") || "",
|
|
members;
|
|
|
|
this.set({
|
|
"members": new _kiwi.model.MemberList(),
|
|
"name": name,
|
|
"scrollback": [],
|
|
"topic": ""
|
|
}, {"silent": true});
|
|
|
|
this.view = new _kiwi.view.Channel({"model": this, "name": name});
|
|
|
|
members = this.get("members");
|
|
members.channel = this;
|
|
members.bind("add", function (member, members, options) {
|
|
var show_message = _kiwi.global.settings.get('show_joins_parts');
|
|
if (show_message === false) {
|
|
return;
|
|
}
|
|
|
|
this.addMsg(' ', styleText('channel_join', {member: member.getMaskParts(), text: translateText('client_models_channel_join'), channel: name}), 'action join', {time: options.kiwi.time});
|
|
}, this);
|
|
|
|
members.bind("remove", function (member, members, options) {
|
|
var show_message = _kiwi.global.settings.get('show_joins_parts');
|
|
var msg = (options.kiwi.message) ? '(' + options.kiwi.message + ')' : '';
|
|
|
|
if (options.kiwi.type === 'quit' && show_message) {
|
|
this.addMsg(' ', styleText('channel_quit', {member: member.getMaskParts(), text: translateText('client_models_channel_quit', [msg]), channel: name}), 'action quit', {time: options.kiwi.time});
|
|
|
|
} else if (options.kiwi.type === 'kick') {
|
|
|
|
if (!options.kiwi.current_user_kicked) {
|
|
//If user kicked someone, show the message regardless of settings.
|
|
if (show_message || options.kiwi.current_user_initiated) {
|
|
this.addMsg(' ', styleText('channel_kicked', {member: member.getMaskParts(), text: translateText('client_models_channel_kicked', [options.kiwi.by, msg]), channel: name}), 'action kick', {time: options.kiwi.time});
|
|
}
|
|
} else {
|
|
this.addMsg(' ', styleText('channel_selfkick', {text: translateText('client_models_channel_selfkick', [options.kiwi.by, msg]), channel: name}), 'action kick', {time: options.kiwi.time});
|
|
}
|
|
} else if (show_message) {
|
|
this.addMsg(' ', styleText('channel_part', {member: member.getMaskParts(), text: translateText('client_models_channel_part', [msg]), channel: name}), 'action part', {time: options.kiwi.time});
|
|
|
|
}
|
|
}, this);
|
|
|
|
_kiwi.global.events.emit('panel:created', {panel: this});
|
|
},
|
|
|
|
|
|
addMsg: function (nick, msg, type, opts) {
|
|
var message_obj, bs, d, members, member,
|
|
scrollback = (parseInt(_kiwi.global.settings.get('scrollback'), 10) || 250);
|
|
|
|
opts = opts || {};
|
|
|
|
// Time defaults to now
|
|
if (typeof opts.time === 'number') {
|
|
opts.time = new Date(opts.time);
|
|
} else {
|
|
opts.time = new Date();
|
|
}
|
|
|
|
// CSS style defaults to empty string
|
|
if (!opts || typeof opts.style === 'undefined') {
|
|
opts.style = '';
|
|
}
|
|
|
|
// Create a message object
|
|
message_obj = {"msg": msg, "date": opts.date, "time": opts.time, "nick": nick, "chan": this.get("name"), "type": type, "style": opts.style};
|
|
|
|
// If this user has one, get its prefix
|
|
members = this.get('members');
|
|
if (members) {
|
|
member = members.getByNick(message_obj.nick);
|
|
if (member) {
|
|
message_obj.nick_prefix = member.get('prefix');
|
|
}
|
|
}
|
|
|
|
// The CSS class (action, topic, notice, etc)
|
|
if (typeof message_obj.type !== "string") {
|
|
message_obj.type = '';
|
|
}
|
|
|
|
// Make sure we don't have NaN or something
|
|
if (typeof message_obj.msg !== "string") {
|
|
message_obj.msg = '';
|
|
}
|
|
|
|
// Update the scrollback
|
|
bs = this.get("scrollback");
|
|
if (bs) {
|
|
bs.push(message_obj);
|
|
|
|
// Keep the scrolback limited
|
|
if (bs.length > scrollback) {
|
|
bs = _.takeRight(bs, scrollback);
|
|
}
|
|
this.set({"scrollback": bs}, {silent: true});
|
|
}
|
|
|
|
this.trigger("msg", message_obj);
|
|
},
|
|
|
|
|
|
clearMessages: function () {
|
|
this.set({'scrollback': []}, {silent: true});
|
|
this.addMsg('', 'Window cleared');
|
|
|
|
this.view.render();
|
|
},
|
|
|
|
|
|
setMode: function(mode_string) {
|
|
this.get('network').gateway.mode(this.get('name'), mode_string);
|
|
},
|
|
|
|
isChannel: function() {
|
|
return true;
|
|
}
|
|
});
|
|
|
|
|
|
|
|
_kiwi.model.Query = _kiwi.model.Channel.extend({
|
|
initialize: function (attributes) {
|
|
var name = this.get("name") || "",
|
|
members;
|
|
|
|
this.view = new _kiwi.view.Channel({"model": this, "name": name});
|
|
this.set({
|
|
"name": name,
|
|
"scrollback": []
|
|
}, {"silent": true});
|
|
|
|
_kiwi.global.events.emit('panel:created', {panel: this});
|
|
},
|
|
|
|
isChannel: function () {
|
|
return false;
|
|
},
|
|
|
|
isQuery: function () {
|
|
return true;
|
|
}
|
|
});
|
|
|
|
|
|
_kiwi.model.Server = _kiwi.model.Channel.extend({
|
|
initialize: function (attributes) {
|
|
var name = "Server";
|
|
this.view = new _kiwi.view.Channel({"model": this, "name": name});
|
|
this.set({
|
|
"scrollback": [],
|
|
"name": name
|
|
}, {"silent": true});
|
|
|
|
_kiwi.global.events.emit('panel:created', {panel: this});
|
|
},
|
|
|
|
isServer: function () {
|
|
return true;
|
|
},
|
|
|
|
isChannel: function () {
|
|
return false;
|
|
}
|
|
});
|
|
|
|
|
|
_kiwi.model.Applet = _kiwi.model.Panel.extend({
|
|
initialize: function (attributes) {
|
|
// Temporary name
|
|
var name = "applet_"+(new Date().getTime().toString()) + Math.ceil(Math.random()*100).toString();
|
|
this.view = new _kiwi.view.Applet({model: this, name: name});
|
|
|
|
this.set({
|
|
"name": name
|
|
}, {"silent": true});
|
|
|
|
// Holds the loaded applet
|
|
this.loaded_applet = null;
|
|
},
|
|
|
|
|
|
// Load an applet within this panel
|
|
load: function (applet_object, applet_name) {
|
|
if (typeof applet_object === 'object') {
|
|
// Make sure this is a valid Applet
|
|
if (applet_object.get || applet_object.extend) {
|
|
|
|
// Try find a title for the applet
|
|
this.set('title', applet_object.get('title') || _kiwi.global.i18n.translate('client_models_applet_unknown').fetch());
|
|
|
|
// Update the tabs title if the applet changes it
|
|
applet_object.bind('change:title', function (obj, new_value) {
|
|
this.set('title', new_value);
|
|
}, this);
|
|
|
|
// If this applet has a UI, add it now
|
|
this.view.$el.html('');
|
|
if (applet_object.view) {
|
|
this.view.$el.append(applet_object.view.$el);
|
|
}
|
|
|
|
// Keep a reference to this applet
|
|
this.loaded_applet = applet_object;
|
|
|
|
this.loaded_applet.trigger('applet_loaded');
|
|
}
|
|
|
|
} else if (typeof applet_object === 'string') {
|
|
// Treat this as a URL to an applet script and load it
|
|
this.loadFromUrl(applet_object, applet_name);
|
|
}
|
|
|
|
return this;
|
|
},
|
|
|
|
|
|
loadFromUrl: function(applet_url, applet_name) {
|
|
var that = this;
|
|
|
|
this.view.$el.html(_kiwi.global.i18n.translate('client_models_applet_loading').fetch());
|
|
$script(applet_url, function () {
|
|
// Check if the applet loaded OK
|
|
if (!_kiwi.applets[applet_name]) {
|
|
that.view.$el.html(_kiwi.global.i18n.translate('client_models_applet_notfound').fetch());
|
|
return;
|
|
}
|
|
|
|
// Load a new instance of this applet
|
|
that.load(new _kiwi.applets[applet_name]());
|
|
});
|
|
},
|
|
|
|
|
|
close: function () {
|
|
this.view.$el.remove();
|
|
this.destroy();
|
|
|
|
this.view = undefined;
|
|
|
|
// Call the applets dispose method if it has one
|
|
if (this.loaded_applet && this.loaded_applet.dispose) {
|
|
this.loaded_applet.dispose();
|
|
}
|
|
|
|
// Call the inherited close()
|
|
this.constructor.__super__.close.apply(this, arguments);
|
|
},
|
|
|
|
isApplet: function () {
|
|
return true;
|
|
}
|
|
},
|
|
|
|
|
|
{
|
|
// Load an applet type once only. If it already exists, return that
|
|
loadOnce: function (applet_name) {
|
|
|
|
// See if we have an instance loaded already
|
|
var applet = _.find(_kiwi.app.panels('applets'), function(panel) {
|
|
// Ignore if it's not an applet
|
|
if (!panel.isApplet()) return;
|
|
|
|
// Ignore if it doesn't have an applet loaded
|
|
if (!panel.loaded_applet) return;
|
|
|
|
if (panel.loaded_applet.get('_applet_name') === applet_name) {
|
|
return true;
|
|
}
|
|
});
|
|
|
|
if (applet) return applet;
|
|
|
|
|
|
// If we didn't find an instance, load a new one up
|
|
return this.load(applet_name);
|
|
},
|
|
|
|
|
|
load: function (applet_name, options) {
|
|
var applet, applet_obj;
|
|
|
|
options = options || {};
|
|
|
|
applet_obj = this.getApplet(applet_name);
|
|
|
|
if (!applet_obj)
|
|
return;
|
|
|
|
// Create the applet and load the content
|
|
applet = new _kiwi.model.Applet();
|
|
applet.load(new applet_obj({_applet_name: applet_name}));
|
|
|
|
// Add it into the tab list if needed (default)
|
|
if (!options.no_tab)
|
|
_kiwi.app.applet_panels.add(applet);
|
|
|
|
|
|
return applet;
|
|
},
|
|
|
|
|
|
getApplet: function (applet_name) {
|
|
return _kiwi.applets[applet_name] || null;
|
|
},
|
|
|
|
|
|
register: function (applet_name, applet) {
|
|
_kiwi.applets[applet_name] = applet;
|
|
}
|
|
});
|
|
|
|
|
|
_kiwi.model.PluginManager = Backbone.Model.extend({
|
|
initialize: function () {
|
|
this.$plugin_holder = $('<div id="kiwi_plugins" style="display:none;"></div>')
|
|
.appendTo(_kiwi.app.view.$el);
|
|
|
|
this.loading_plugins = 0;
|
|
this.loaded_plugins = {};
|
|
},
|
|
|
|
// Load an applet within this panel
|
|
load: function (url) {
|
|
var that = this;
|
|
|
|
if (this.loaded_plugins[url]) {
|
|
this.unload(url);
|
|
}
|
|
|
|
this.loading_plugins++;
|
|
|
|
this.loaded_plugins[url] = $('<div></div>');
|
|
this.loaded_plugins[url].appendTo(this.$plugin_holder)
|
|
.load(url, _.bind(that.pluginLoaded, that));
|
|
},
|
|
|
|
|
|
unload: function (url) {
|
|
if (!this.loaded_plugins[url]) {
|
|
return;
|
|
}
|
|
|
|
this.loaded_plugins[url].remove();
|
|
delete this.loaded_plugins[url];
|
|
},
|
|
|
|
|
|
// Called after each plugin is loaded
|
|
pluginLoaded: function() {
|
|
this.loading_plugins--;
|
|
|
|
if (this.loading_plugins === 0) {
|
|
this.trigger('loaded');
|
|
}
|
|
},
|
|
});
|
|
|
|
|
|
_kiwi.model.DataStore = Backbone.Model.extend({
|
|
initialize: function () {
|
|
this._namespace = '';
|
|
this.new_data = {};
|
|
this.stored_attributes = {};
|
|
},
|
|
|
|
namespace: function (new_namespace) {
|
|
if (new_namespace) this._namespace = new_namespace;
|
|
return this._namespace;
|
|
},
|
|
|
|
// Overload the original save() method
|
|
save: function () {
|
|
// Save the current data and update the stored_attributes with a copy
|
|
var stringified = JSON.stringify(this.attributes);
|
|
localStorage.setItem(this._namespace, stringified);
|
|
this.stored_attributes = JSON.parse(stringified);
|
|
},
|
|
|
|
// Save only one attribute to storage
|
|
saveOne: function (key_name) {
|
|
this.stored_attributes[key_name] = this.get(key_name);
|
|
localStorage.setItem(this._namespace, JSON.stringify(this.stored_attributes));
|
|
},
|
|
|
|
// Overload the original load() method
|
|
load: function () {
|
|
if (!localStorage) return;
|
|
|
|
var raw, data, stored_data;
|
|
|
|
try {
|
|
raw = localStorage.getItem(this._namespace);
|
|
data = JSON.parse(raw) || {};
|
|
stored_data = JSON.parse(raw) || {};
|
|
} catch (error) {
|
|
data = {};
|
|
stored_data = {};
|
|
}
|
|
|
|
this.attributes = data;
|
|
this.stored_attributes = stored_data;
|
|
}
|
|
},
|
|
|
|
{
|
|
// Generates a new instance of DataStore with a set namespace
|
|
instance: function (namespace, attributes) {
|
|
var datastore = new _kiwi.model.DataStore(attributes);
|
|
datastore.namespace(namespace);
|
|
return datastore;
|
|
}
|
|
});
|
|
|
|
|
|
_kiwi.model.ChannelInfo = Backbone.Model.extend({
|
|
initialize: function () {
|
|
this.view = new _kiwi.view.ChannelInfo({"model": this});
|
|
}
|
|
});
|
|
|
|
|
|
_kiwi.view.Panel = Backbone.View.extend({
|
|
tagName: "div",
|
|
className: "panel",
|
|
|
|
events: {
|
|
},
|
|
|
|
initialize: function (options) {
|
|
this.initializePanel(options);
|
|
},
|
|
|
|
initializePanel: function (options) {
|
|
this.$el.css('display', 'none');
|
|
options = options || {};
|
|
|
|
// Containing element for this panel
|
|
if (options.container) {
|
|
this.$container = $(options.container);
|
|
} else {
|
|
this.$container = $('#kiwi .panels .container1');
|
|
}
|
|
|
|
this.$el.appendTo(this.$container);
|
|
|
|
this.alert_level = 0;
|
|
|
|
this.model.set({"view": this}, {"silent": true});
|
|
|
|
this.listenTo(this.model, 'change:activity_counter', function(model, new_count) {
|
|
var $act = this.model.tab.find('.activity');
|
|
|
|
if (new_count > 999) {
|
|
$act.text('999+');
|
|
} else {
|
|
$act.text(new_count);
|
|
}
|
|
|
|
if (new_count === 0) {
|
|
$act.addClass('zero');
|
|
} else {
|
|
$act.removeClass('zero');
|
|
}
|
|
});
|
|
},
|
|
|
|
render: function () {
|
|
},
|
|
|
|
|
|
show: function () {
|
|
var $this = this.$el;
|
|
|
|
// Hide all other panels and show this one
|
|
this.$container.children('.panel').css('display', 'none');
|
|
$this.css('display', 'block');
|
|
|
|
// Show this panels memberlist
|
|
var members = this.model.get("members");
|
|
if (members) {
|
|
_kiwi.app.rightbar.show();
|
|
members.view.show();
|
|
} else {
|
|
_kiwi.app.rightbar.hide();
|
|
}
|
|
|
|
// Remove any alerts and activity counters for this panel
|
|
this.alert('none');
|
|
this.model.set('activity_counter', 0);
|
|
|
|
_kiwi.app.panels.trigger('active', this.model, _kiwi.app.panels().active);
|
|
this.model.trigger('active', this.model);
|
|
|
|
_kiwi.app.view.doLayout();
|
|
|
|
if (!this.model.isApplet())
|
|
this.scrollToBottom(true);
|
|
},
|
|
|
|
|
|
alert: function (level) {
|
|
// No need to highlight if this si the active panel
|
|
if (this.model == _kiwi.app.panels().active) return;
|
|
|
|
var types, type_idx;
|
|
types = ['none', 'action', 'activity', 'highlight'];
|
|
|
|
// Default alert level
|
|
level = level || 'none';
|
|
|
|
// If this alert level does not exist, assume clearing current level
|
|
type_idx = _.indexOf(types, level);
|
|
if (!type_idx) {
|
|
level = 'none';
|
|
type_idx = 0;
|
|
}
|
|
|
|
// Only 'upgrade' the alert. Never down (unless clearing)
|
|
if (type_idx !== 0 && type_idx <= this.alert_level) {
|
|
return;
|
|
}
|
|
|
|
// Clear any existing levels
|
|
this.model.tab.removeClass(function (i, css) {
|
|
return (css.match(/\balert_\S+/g) || []).join(' ');
|
|
});
|
|
|
|
// Add the new level if there is one
|
|
if (level !== 'none') {
|
|
this.model.tab.addClass('alert_' + level);
|
|
}
|
|
|
|
this.alert_level = type_idx;
|
|
},
|
|
|
|
|
|
// Scroll to the bottom of the panel
|
|
scrollToBottom: function (force_down) {
|
|
// If this isn't the active panel, don't scroll
|
|
if (this.model !== _kiwi.app.panels().active) return;
|
|
|
|
// Don't scroll down if we're scrolled up the panel a little
|
|
if (force_down || this.$container.scrollTop() + this.$container.height() > this.$el.outerHeight() - 150) {
|
|
this.$container[0].scrollTop = this.$container[0].scrollHeight;
|
|
}
|
|
}
|
|
});
|
|
|
|
|
|
_kiwi.view.Channel = _kiwi.view.Panel.extend({
|
|
events: function(){
|
|
var parent_events = this.constructor.__super__.events;
|
|
|
|
if(_.isFunction(parent_events)){
|
|
parent_events = parent_events();
|
|
}
|
|
return _.extend({}, parent_events, {
|
|
'click .msg .nick' : 'nickClick',
|
|
'click .msg .inline-nick' : 'nickClick',
|
|
'contextmenu .msg .nick' : 'nickClick',
|
|
'contextmenu .msg .inline-nick' : 'nickClick',
|
|
'dblclick .msg .nick' : 'nickClick',
|
|
'dblclick .msg .inline-nick' : 'nickClick',
|
|
'click .chan': 'chanClick',
|
|
'click .media .open': 'mediaClick',
|
|
'mouseenter .msg .nick': 'msgEnter',
|
|
'mouseleave .msg .nick': 'msgLeave'
|
|
});
|
|
},
|
|
|
|
initialize: function (options) {
|
|
this.initializePanel(options);
|
|
|
|
// Container for all the messages
|
|
this.$messages = $('<div class="messages"></div>');
|
|
this.$el.append(this.$messages);
|
|
|
|
this.model.bind('change:topic', this.topic, this);
|
|
this.model.bind('change:topic_set_by', this.topicSetBy, this);
|
|
|
|
if (this.model.get('members')) {
|
|
// When we join the memberlist, we have officially joined the channel
|
|
this.model.get('members').bind('add', function (member) {
|
|
if (member.get('nick') === this.model.collection.network.get('nick')) {
|
|
this.$el.find('.initial_loader').slideUp(function () {
|
|
$(this).remove();
|
|
});
|
|
}
|
|
}, this);
|
|
|
|
// Memberlist reset with a new nicklist? Consider we have joined
|
|
this.model.get('members').bind('reset', function(members) {
|
|
if (members.getByNick(this.model.collection.network.get('nick'))) {
|
|
this.$el.find('.initial_loader').slideUp(function () {
|
|
$(this).remove();
|
|
});
|
|
}
|
|
}, this);
|
|
}
|
|
|
|
// Only show the loader if this is a channel (ie. not a query)
|
|
if (this.model.isChannel()) {
|
|
this.$el.append('<div class="initial_loader" style="margin:1em;text-align:center;"> ' + _kiwi.global.i18n.translate('client_views_channel_joining').fetch() + ' <span class="loader"></span></div>');
|
|
}
|
|
|
|
// Move our lastSeenMarker to the bottom if moving away from this tab
|
|
this.listenTo(_kiwi.app.panels, 'active', function(new_panel, previous_panel) {
|
|
if (previous_panel === this.model) {
|
|
this.updateLastSeenMarker();
|
|
}
|
|
});
|
|
|
|
this.model.bind('msg', this.newMsg, this);
|
|
this.msg_count = 0;
|
|
},
|
|
|
|
|
|
render: function () {
|
|
var that = this;
|
|
|
|
this.$messages.empty();
|
|
_.each(this.model.get('scrollback'), function (msg) {
|
|
that.newMsg(msg);
|
|
});
|
|
},
|
|
|
|
|
|
newMsg: function(msg) {
|
|
|
|
// Parse the msg object into properties fit for displaying
|
|
msg = this.generateMessageDisplayObj(msg);
|
|
|
|
_kiwi.global.events.emit('message:display', {panel: this.model, message: msg})
|
|
.then(_.bind(function() {
|
|
var line_msg;
|
|
|
|
// Format the nick to the config defined format
|
|
var display_obj = _.clone(msg);
|
|
display_obj.nick = styleText('message_nick', {nick: msg.nick, prefix: msg.nick_prefix || ''});
|
|
|
|
line_msg = '<div class="msg <%= type %> <%= css_classes %>"><div class="time"><%- time_string %></div><div class="nick" style="<%= nick_style %>"><%- nick %></div><div class="text" style="<%= style %>"><%= msg %> </div></div>';
|
|
this.$messages.append($(_.template(line_msg)(display_obj)).data('message', msg));
|
|
|
|
// Activity/alerts based on the type of new message. We only do this if we have
|
|
// an associated network (think: could be a broadcasted channel so alerts are not needed)
|
|
if (this.model.get('network')) {
|
|
if (msg.type.match(/^action /)) {
|
|
this.alert('action');
|
|
|
|
} else if (msg.is_highlight) {
|
|
_kiwi.app.view.alertWindow('* ' + _kiwi.global.i18n.translate('client_views_panel_activity').fetch());
|
|
_kiwi.app.view.favicon.newHighlight();
|
|
_kiwi.app.view.playSound('highlight');
|
|
_kiwi.app.view.showNotification(this.model.get('name'), msg.unparsed_msg);
|
|
this.alert('highlight');
|
|
|
|
} else {
|
|
// If this is the active panel, send an alert out
|
|
if (this.model.isActive()) {
|
|
_kiwi.app.view.alertWindow('* ' + _kiwi.global.i18n.translate('client_views_panel_activity').fetch());
|
|
}
|
|
this.alert('activity');
|
|
}
|
|
|
|
if (this.model.isQuery() && !this.model.isActive()) {
|
|
_kiwi.app.view.alertWindow('* ' + _kiwi.global.i18n.translate('client_views_panel_activity').fetch());
|
|
|
|
// Highlights have already been dealt with above
|
|
if (!msg.is_highlight) {
|
|
_kiwi.app.view.favicon.newHighlight();
|
|
}
|
|
|
|
_kiwi.app.view.showNotification(this.model.get('name'), msg.unparsed_msg);
|
|
_kiwi.app.view.playSound('highlight');
|
|
}
|
|
|
|
// Update the activity counters
|
|
(function () {
|
|
// Only inrement the counters if we're not the active panel
|
|
if (this.model.isActive()) return;
|
|
|
|
var count_all_activity = _kiwi.global.settings.get('count_all_activity'),
|
|
exclude_message_types, new_count;
|
|
|
|
// Set the default config value
|
|
if (typeof count_all_activity === 'undefined') {
|
|
count_all_activity = false;
|
|
}
|
|
|
|
// Do not increment the counter for these message types
|
|
exclude_message_types = [
|
|
'action join',
|
|
'action quit',
|
|
'action part',
|
|
'action kick',
|
|
'action nick',
|
|
'action mode'
|
|
];
|
|
|
|
if (count_all_activity || _.indexOf(exclude_message_types, msg.type) === -1) {
|
|
new_count = this.model.get('activity_counter') || 0;
|
|
new_count++;
|
|
this.model.set('activity_counter', new_count);
|
|
}
|
|
|
|
}).apply(this);
|
|
}
|
|
|
|
if(this.model.isActive()) this.scrollToBottom();
|
|
|
|
// Make sure our DOM isn't getting too large (Acts as scrollback)
|
|
this.msg_count++;
|
|
if (this.msg_count > (parseInt(_kiwi.global.settings.get('scrollback'), 10) || 250)) {
|
|
$('.msg:first', this.$messages).remove();
|
|
this.msg_count--;
|
|
}
|
|
|
|
}, this));
|
|
},
|
|
|
|
|
|
// Let nicks be clickable + colourise within messages
|
|
parseMessageNicks: function(word, colourise) {
|
|
var members, member, nick, nick_re, style = '';
|
|
|
|
if (!(members = this.model.get('members')) || !(member = members.getByNick(word))) {
|
|
return;
|
|
}
|
|
|
|
nick = member.get('nick');
|
|
|
|
if (colourise !== false) {
|
|
// Use the nick from the member object so the style matches the letter casing
|
|
style = this.getNickStyles(nick).asCssString();
|
|
}
|
|
nick_re = new RegExp('(.*)(' + _.escapeRegExp(nick) + ')(.*)', 'i');
|
|
return word.replace(nick_re, function (__, before, nick_in_orig_case, after) {
|
|
return _.escape(before)
|
|
+ '<span class="inline-nick" style="' + style + '; cursor:pointer" data-nick="' + _.escape(nick) + '">'
|
|
+ _.escape(nick_in_orig_case)
|
|
+ '</span>'
|
|
+ _.escape(after);
|
|
});
|
|
},
|
|
|
|
|
|
// Make channels clickable
|
|
parseMessageChannels: function(word) {
|
|
var re,
|
|
parsed = false,
|
|
network = this.model.get('network');
|
|
|
|
if (!network) {
|
|
return;
|
|
}
|
|
|
|
re = new RegExp('(^|\\s)([' + _.escapeRegExp(network.get('channel_prefix')) + '][^ .,\\007]+)', 'g');
|
|
|
|
if (!word.match(re)) {
|
|
return parsed;
|
|
}
|
|
|
|
parsed = word.replace(re, function (m1, m2) {
|
|
return m2 + '<a class="chan" data-channel="' + _.escape(m1.trim()) + '">' + _.escape(m1.trim()) + '</a>';
|
|
});
|
|
|
|
return parsed;
|
|
},
|
|
|
|
|
|
parseMessageUrls: function(word) {
|
|
var found_a_url = false,
|
|
parsed_url;
|
|
|
|
parsed_url = word.replace(/^(([A-Za-z][A-Za-z0-9\-]*\:\/\/)|(www\.))([\w\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF.\-]+)([a-zA-Z]{2,6})(:[0-9]+)?(\/[\w\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF!:.?$'()[\]*,;~+=&%@!\-\/]*)?(#.*)?$/gi, function (url) {
|
|
var nice = url,
|
|
extra_html = '';
|
|
|
|
// Don't allow javascript execution
|
|
if (url.match(/^javascript:/i)) {
|
|
return url;
|
|
}
|
|
|
|
found_a_url = true;
|
|
|
|
// Add the http if no protoocol was found
|
|
if (url.match(/^www\./i)) {
|
|
url = 'http://' + url;
|
|
}
|
|
|
|
// Shorten the displayed URL if it's going to be too long
|
|
if (nice.length > 100) {
|
|
nice = nice.substr(0, 100) + '...';
|
|
}
|
|
|
|
// Get any media HTML if supported
|
|
extra_html = _kiwi.view.MediaMessage.buildHtml(url);
|
|
|
|
// Make the link clickable
|
|
return '<a class="link_ext" target="_blank" rel="nofollow" href="' + url.replace(/"/g, '%22') + '">' + _.escape(nice) + '</a>' + extra_html;
|
|
});
|
|
|
|
return found_a_url ? parsed_url : false;
|
|
},
|
|
|
|
|
|
// Generate a css style for a nick
|
|
getNickStyles: (function () {
|
|
|
|
// Get a colour from a nick (Method based on IRSSIs nickcolor.pl)
|
|
return function (nick) {
|
|
var nick_lightness, nick_int, rgb;
|
|
|
|
// Get the lightness option from the theme. Defaults to 35.
|
|
nick_lightness = (_.find(_kiwi.app.themes, function (theme) {
|
|
return theme.name.toLowerCase() === _kiwi.global.settings.get('theme').toLowerCase();
|
|
}) || {}).nick_lightness;
|
|
|
|
if (typeof nick_lightness !== 'number') {
|
|
nick_lightness = 35;
|
|
} else {
|
|
nick_lightness = Math.max(0, Math.min(100, nick_lightness));
|
|
}
|
|
|
|
nick_int = _.reduce(nick.split(''), sumCharCodes, 0);
|
|
rgb = hsl2rgb(nick_int % 256, 70, nick_lightness);
|
|
|
|
return {
|
|
color: '#' + ('000000' + (rgb[2] | (rgb[1] << 8) | (rgb[0] << 16)).toString(16)).substr(-6),
|
|
asCssString: asCssString
|
|
};
|
|
};
|
|
|
|
function toCssProperty(result, item, key) {
|
|
return result + (typeof item === 'string' || typeof item === 'number' ? key + ':' + item + ';' : '');
|
|
}
|
|
function asCssString() {
|
|
return _.reduce(this, toCssProperty, '');
|
|
}
|
|
function sumCharCodes(total, i) {
|
|
return total + i.charCodeAt(0);
|
|
}
|
|
}()),
|
|
|
|
|
|
// Takes an IRC message object and parses it for displaying
|
|
generateMessageDisplayObj: function(msg) {
|
|
var nick_hex, time_difference,
|
|
message_words,
|
|
sb = this.model.get('scrollback'),
|
|
network = this.model.get('network'),
|
|
nick,
|
|
regexpStr,
|
|
prev_msg = sb[sb.length-2],
|
|
hour, pm, am_pm_locale_key;
|
|
|
|
// Clone the msg object so we dont modify the original
|
|
msg = _.clone(msg);
|
|
|
|
// Defaults
|
|
msg.css_classes = '';
|
|
msg.nick_style = '';
|
|
msg.is_highlight = false;
|
|
msg.time_string = '';
|
|
|
|
// Nick + custom highlight detecting
|
|
nick = network ? network.get('nick') : '';
|
|
if (nick && msg.nick.localeCompare(nick) !== 0) {
|
|
// Build a list of all highlights and escape them for regex
|
|
regexpStr = _.chain((_kiwi.global.settings.get('custom_highlights') || '').split(/[\s,]+/))
|
|
.compact()
|
|
.concat(nick)
|
|
.map(_.escapeRegExp)
|
|
.join('|')
|
|
.value();
|
|
|
|
if (msg.msg.search(new RegExp('(\\b|\\W|^)(' + regexpStr + ')(\\b|\\W|$)', 'i')) > -1) {
|
|
msg.is_highlight = true;
|
|
msg.css_classes += ' highlight';
|
|
}
|
|
}
|
|
|
|
message_words = msg.msg.split(' ');
|
|
message_words = _.map(message_words, function(word) {
|
|
var parsed_word;
|
|
|
|
parsed_word = this.parseMessageUrls(word);
|
|
if (typeof parsed_word === 'string') return parsed_word;
|
|
|
|
parsed_word = this.parseMessageChannels(word);
|
|
if (typeof parsed_word === 'string') return parsed_word;
|
|
|
|
parsed_word = this.parseMessageNicks(word, (msg.type === 'privmsg'));
|
|
if (typeof parsed_word === 'string') return parsed_word;
|
|
|
|
parsed_word = _.escape(word);
|
|
|
|
// Replace text emoticons with images
|
|
if (_kiwi.global.settings.get('show_emoticons')) {
|
|
parsed_word = emoticonFromText(parsed_word);
|
|
}
|
|
|
|
return parsed_word;
|
|
}, this);
|
|
|
|
msg.unparsed_msg = msg.msg;
|
|
msg.msg = message_words.join(' ');
|
|
|
|
// Convert IRC formatting into HTML formatting
|
|
msg.msg = formatIRCMsg(msg.msg);
|
|
|
|
// Add some style to the nick
|
|
msg.nick_style = this.getNickStyles(msg.nick).asCssString();
|
|
|
|
// Generate a hex string from the nick to be used as a CSS class name
|
|
nick_hex = '';
|
|
if (msg.nick) {
|
|
_.map(msg.nick.split(''), function (char) {
|
|
nick_hex += char.charCodeAt(0).toString(16);
|
|
});
|
|
msg.css_classes += ' nick_' + nick_hex;
|
|
}
|
|
|
|
if (prev_msg) {
|
|
// Time difference between this message and the last (in minutes)
|
|
time_difference = (msg.time.getTime() - prev_msg.time.getTime())/1000/60;
|
|
if (prev_msg.nick === msg.nick && time_difference < 1) {
|
|
msg.css_classes += ' repeated_nick';
|
|
}
|
|
}
|
|
|
|
// Build up and add the line
|
|
if (_kiwi.global.settings.get('use_24_hour_timestamps')) {
|
|
msg.time_string = msg.time.getHours().toString().lpad(2, "0") + ":" + msg.time.getMinutes().toString().lpad(2, "0") + ":" + msg.time.getSeconds().toString().lpad(2, "0");
|
|
} else {
|
|
hour = msg.time.getHours();
|
|
pm = hour > 11;
|
|
|
|
hour = hour % 12;
|
|
if (hour === 0)
|
|
hour = 12;
|
|
|
|
am_pm_locale_key = pm ?
|
|
'client_views_panel_timestamp_pm' :
|
|
'client_views_panel_timestamp_am';
|
|
|
|
msg.time_string = translateText(am_pm_locale_key, hour + ":" + msg.time.getMinutes().toString().lpad(2, "0") + ":" + msg.time.getSeconds().toString().lpad(2, "0"));
|
|
}
|
|
|
|
return msg;
|
|
},
|
|
|
|
|
|
topic: function (topic) {
|
|
if (typeof topic !== 'string' || !topic) {
|
|
topic = this.model.get("topic");
|
|
}
|
|
|
|
this.model.addMsg('', styleText('channel_topic', {text: topic, channel: this.model.get('name')}), 'topic');
|
|
|
|
// If this is the active channel then update the topic bar
|
|
if (_kiwi.app.panels().active === this.model) {
|
|
_kiwi.app.topicbar.setCurrentTopicFromChannel(this.model);
|
|
}
|
|
},
|
|
|
|
topicSetBy: function (topic) {
|
|
// If this is the active channel then update the topic bar
|
|
if (_kiwi.app.panels().active === this.model) {
|
|
_kiwi.app.topicbar.setCurrentTopicFromChannel(this.model);
|
|
}
|
|
},
|
|
|
|
// Click on a nickname
|
|
nickClick: function (event) {
|
|
var $target = $(event.currentTarget),
|
|
nick,
|
|
members = this.model.get('members'),
|
|
member;
|
|
|
|
event.stopPropagation();
|
|
|
|
// Check this current element for a nick before resorting to the main message
|
|
// (eg. inline nicks has the nick on its own element within the message)
|
|
nick = $target.data('nick');
|
|
if (!nick) {
|
|
nick = $target.parent('.msg').data('message').nick;
|
|
}
|
|
|
|
// Make sure this nick is still in the channel
|
|
member = members ? members.getByNick(nick) : null;
|
|
if (!member) {
|
|
return;
|
|
}
|
|
|
|
_kiwi.global.events.emit('nick:select', {
|
|
target: $target,
|
|
member: member,
|
|
network: this.model.get('network'),
|
|
source: 'message',
|
|
$event: event
|
|
})
|
|
.then(_.bind(this.openUserMenuForNick, this, $target, member));
|
|
},
|
|
|
|
|
|
updateLastSeenMarker: function() {
|
|
// Remove the previous last seen classes
|
|
this.$(".last_seen").removeClass("last_seen");
|
|
|
|
// Mark the last message the user saw
|
|
this.$messages.children().last().addClass("last_seen");
|
|
},
|
|
|
|
|
|
openUserMenuForNick: function ($target, member) {
|
|
var members = this.model.get('members'),
|
|
network = this.model.get('network'),
|
|
are_we_an_op = network ? !!members.getByNick(network.get('nick')).get('is_op') : false,
|
|
userbox, menubox;
|
|
|
|
// Can only do user related functions if we have an associated network
|
|
if (!network) {
|
|
return;
|
|
}
|
|
|
|
userbox = new _kiwi.view.UserBox();
|
|
userbox.setTargets(member, this.model);
|
|
userbox.displayOpItems(are_we_an_op);
|
|
|
|
menubox = new _kiwi.view.MenuBox(member.get('nick') || 'User');
|
|
menubox.addItem('userbox', userbox.$el);
|
|
menubox.showFooter(false);
|
|
|
|
_kiwi.global.events.emit('usermenu:created', {menu: menubox, userbox: userbox, user: member})
|
|
.then(_.bind(function() {
|
|
menubox.show();
|
|
|
|
// Position the userbox + menubox
|
|
var target_offset = $target.offset(),
|
|
t = target_offset.top,
|
|
m_bottom = t + menubox.$el.outerHeight(), // Where the bottom of menu will be
|
|
memberlist_bottom = this.$el.parent().offset().top + this.$el.parent().outerHeight();
|
|
|
|
// If the bottom of the userbox is going to be too low.. raise it
|
|
if (m_bottom > memberlist_bottom){
|
|
t = memberlist_bottom - menubox.$el.outerHeight();
|
|
}
|
|
|
|
// Set the new positon
|
|
menubox.$el.offset({
|
|
left: target_offset.left,
|
|
top: t
|
|
});
|
|
}, this))
|
|
.then(null, _.bind(function() {
|
|
userbox = null;
|
|
|
|
menu.dispose();
|
|
menu = null;
|
|
}, this));
|
|
},
|
|
|
|
|
|
chanClick: function (event) {
|
|
var target = (event.target) ? $(event.target).data('channel') : $(event.srcElement).data('channel');
|
|
|
|
this.model.get('network').gateway.join(target);
|
|
},
|
|
|
|
|
|
mediaClick: function (event) {
|
|
var $media = $(event.target).parents('.media');
|
|
var media_message;
|
|
|
|
if ($media.data('media')) {
|
|
media_message = $media.data('media');
|
|
} else {
|
|
media_message = new _kiwi.view.MediaMessage({el: $media[0]});
|
|
|
|
// Cache this MediaMessage instance for when it's opened again
|
|
$media.data('media', media_message);
|
|
}
|
|
|
|
media_message.toggle();
|
|
},
|
|
|
|
|
|
// Cursor hovers over a message
|
|
msgEnter: function (event) {
|
|
var nick_class;
|
|
|
|
// Find a valid class that this element has
|
|
_.each($(event.currentTarget).parent('.msg').attr('class').split(' '), function (css_class) {
|
|
if (css_class.match(/^nick_[a-z0-9]+/i)) {
|
|
nick_class = css_class;
|
|
}
|
|
});
|
|
|
|
// If no class was found..
|
|
if (!nick_class) return;
|
|
|
|
$('.'+nick_class).addClass('global_nick_highlight');
|
|
},
|
|
|
|
|
|
// Cursor leaves message
|
|
msgLeave: function (event) {
|
|
var nick_class;
|
|
|
|
// Find a valid class that this element has
|
|
_.each($(event.currentTarget).parent('.msg').attr('class').split(' '), function (css_class) {
|
|
if (css_class.match(/^nick_[a-z0-9]+/i)) {
|
|
nick_class = css_class;
|
|
}
|
|
});
|
|
|
|
// If no class was found..
|
|
if (!nick_class) return;
|
|
|
|
$('.'+nick_class).removeClass('global_nick_highlight');
|
|
}
|
|
});
|
|
|
|
|
|
|
|
_kiwi.view.Applet = _kiwi.view.Panel.extend({
|
|
className: 'panel applet',
|
|
initialize: function (options) {
|
|
this.initializePanel(options);
|
|
}
|
|
});
|
|
|
|
|
|
_kiwi.view.Application = Backbone.View.extend({
|
|
initialize: function () {
|
|
var that = this;
|
|
|
|
this.$el = $($('#tmpl_application').html().trim());
|
|
this.el = this.$el[0];
|
|
|
|
$(this.model.get('container') || 'body').append(this.$el);
|
|
|
|
this.elements = {
|
|
panels: this.$el.find('.panels'),
|
|
right_bar: this.$el.find('.right_bar'),
|
|
toolbar: this.$el.find('.toolbar'),
|
|
controlbox: this.$el.find('.controlbox'),
|
|
resize_handle: this.$el.find('.memberlists_resize_handle')
|
|
};
|
|
|
|
$(window).resize(function() { that.doLayout.apply(that); });
|
|
this.elements.toolbar.resize(function() { that.doLayout.apply(that); });
|
|
this.elements.controlbox.resize(function() { that.doLayout.apply(that); });
|
|
|
|
// Change the theme when the config is changed
|
|
_kiwi.global.settings.on('change:theme', this.updateTheme, this);
|
|
this.updateTheme(getQueryVariable('theme'));
|
|
|
|
_kiwi.global.settings.on('change:channel_list_style', this.setTabLayout, this);
|
|
this.setTabLayout(_kiwi.global.settings.get('channel_list_style'));
|
|
|
|
_kiwi.global.settings.on('change:show_timestamps', this.displayTimestamps, this);
|
|
this.displayTimestamps(_kiwi.global.settings.get('show_timestamps'));
|
|
|
|
this.$el.appendTo($('body'));
|
|
this.doLayout();
|
|
|
|
$(document).keydown(this.setKeyFocus);
|
|
|
|
// Confirmation require to leave the page
|
|
window.onbeforeunload = function () {
|
|
if (_kiwi.gateway.isConnected()) {
|
|
return _kiwi.global.i18n.translate('client_views_application_close_notice').fetch();
|
|
}
|
|
};
|
|
|
|
// Keep tabs on the browser having focus
|
|
this.has_focus = true;
|
|
|
|
$(window).on('focus', function windowOnFocus() {
|
|
that.has_focus = true;
|
|
});
|
|
|
|
$(window).on('blur', function windowOnBlur() {
|
|
var active_panel = that.model.panels().active;
|
|
if (active_panel && active_panel.view.updateLastSeenMarker) {
|
|
active_panel.view.updateLastSeenMarker();
|
|
}
|
|
|
|
that.has_focus = false;
|
|
});
|
|
|
|
// If we get a touchstart event, make note of it so we know we're using a touchscreen
|
|
$(window).on('touchstart', function windowOnTouchstart() {
|
|
that.$el.addClass('touch');
|
|
$(window).off('touchstart', windowOnTouchstart);
|
|
});
|
|
|
|
|
|
this.favicon = new _kiwi.view.Favicon();
|
|
this.initSound();
|
|
|
|
this.monitorPanelFallback();
|
|
},
|
|
|
|
|
|
|
|
updateTheme: function (theme_name) {
|
|
// If called by the settings callback, get the correct new_value
|
|
if (theme_name === _kiwi.global.settings) {
|
|
theme_name = arguments[1];
|
|
}
|
|
|
|
// If we have no theme specified, get it from the settings
|
|
if (!theme_name) theme_name = _kiwi.global.settings.get('theme') || 'relaxed';
|
|
|
|
theme_name = theme_name.toLowerCase();
|
|
|
|
// Clear any current theme
|
|
$('[data-theme]').each(function (idx, link) {
|
|
link.disabled = true;
|
|
});
|
|
|
|
// Apply the new theme
|
|
var link = $('[data-theme][title=' + theme_name + ']');
|
|
if (link.length > 0) {
|
|
link[0].disabled = false;
|
|
}
|
|
|
|
this.doLayout();
|
|
},
|
|
|
|
|
|
reloadStyles: function() {
|
|
var query_string = '?reload=' + new Date().getTime();
|
|
$('link[rel="stylesheet"]').each(function() {
|
|
this.href = this.href.replace(/\?.*|$/, query_string);
|
|
});
|
|
},
|
|
|
|
|
|
setTabLayout: function (layout_style) {
|
|
// If called by the settings callback, get the correct new_value
|
|
if (layout_style === _kiwi.global.settings) {
|
|
layout_style = arguments[1];
|
|
}
|
|
|
|
if (layout_style == 'list') {
|
|
this.$el.addClass('chanlist_treeview');
|
|
} else {
|
|
this.$el.removeClass('chanlist_treeview');
|
|
}
|
|
|
|
this.doLayout();
|
|
},
|
|
|
|
|
|
displayTimestamps: function (show_timestamps) {
|
|
// If called by the settings callback, get the correct new_value
|
|
if (show_timestamps === _kiwi.global.settings) {
|
|
show_timestamps = arguments[1];
|
|
}
|
|
|
|
if (show_timestamps) {
|
|
this.$el.addClass('timestamps');
|
|
} else {
|
|
this.$el.removeClass('timestamps');
|
|
}
|
|
},
|
|
|
|
|
|
// Globally shift focus to the command input box on a keypress
|
|
setKeyFocus: function (ev) {
|
|
// If we're copying text, don't shift focus
|
|
if (ev.ctrlKey || ev.altKey || ev.metaKey) {
|
|
return;
|
|
}
|
|
|
|
// If we're typing into an input box somewhere, ignore
|
|
var elements = ['input', 'select', 'textarea', 'button', 'datalist', 'keygen'];
|
|
var do_not_refocus =
|
|
elements.indexOf(ev.target.tagName.toLowerCase()) > -1 ||
|
|
$(ev.target).attr('contenteditable');
|
|
|
|
if (do_not_refocus) {
|
|
return;
|
|
}
|
|
|
|
$('#kiwi .controlbox .inp').focus();
|
|
},
|
|
|
|
|
|
doLayout: function () {
|
|
var $kiwi = this.$el;
|
|
var $panels = this.elements.panels;
|
|
var $right_bar = this.elements.right_bar;
|
|
var $toolbar = this.elements.toolbar;
|
|
var $controlbox = this.elements.controlbox;
|
|
var $resize_handle = this.elements.resize_handle;
|
|
|
|
if (!$kiwi.is(':visible')) {
|
|
return;
|
|
}
|
|
|
|
var css_heights = {
|
|
top: $toolbar.outerHeight(true),
|
|
bottom: $controlbox.outerHeight(true)
|
|
};
|
|
|
|
|
|
// If any elements are not visible, full size the panals instead
|
|
if (!$toolbar.is(':visible')) {
|
|
css_heights.top = 0;
|
|
}
|
|
|
|
if (!$controlbox.is(':visible')) {
|
|
css_heights.bottom = 0;
|
|
}
|
|
|
|
// Apply the CSS sizes
|
|
$panels.css(css_heights);
|
|
$right_bar.css(css_heights);
|
|
$resize_handle.css(css_heights);
|
|
|
|
// If we have channel tabs on the side, adjust the height
|
|
if ($kiwi.hasClass('chanlist_treeview')) {
|
|
this.$el.find('.tabs', $kiwi).css(css_heights);
|
|
}
|
|
|
|
// Determine if we have a narrow window (mobile/tablet/or even small desktop window)
|
|
if ($kiwi.outerWidth() < 700) {
|
|
$kiwi.addClass('narrow');
|
|
if (this.model.rightbar && this.model.rightbar.keep_hidden !== true)
|
|
this.model.rightbar.toggle(true);
|
|
} else {
|
|
$kiwi.removeClass('narrow');
|
|
if (this.model.rightbar && this.model.rightbar.keep_hidden !== false)
|
|
this.model.rightbar.toggle(false);
|
|
}
|
|
|
|
// Set the panels width depending on the memberlist visibility
|
|
if (!$right_bar.hasClass('disabled')) {
|
|
// Panels to the side of the memberlist
|
|
$panels.css('right', $right_bar.outerWidth(true));
|
|
// The resize handle sits overlapping the panels and memberlist
|
|
$resize_handle.css('left', $right_bar.position().left - ($resize_handle.outerWidth(true) / 2));
|
|
} else {
|
|
// Memberlist is hidden so panels to the right edge
|
|
$panels.css('right', 0);
|
|
// And move the handle just out of sight to the right
|
|
$resize_handle.css('left', $panels.outerWidth(true));
|
|
}
|
|
|
|
var input_wrap_width = parseInt($controlbox.find('.input_tools').outerWidth(), 10);
|
|
$controlbox.find('.input_wrap').css('right', input_wrap_width + 7);
|
|
},
|
|
|
|
|
|
alertWindow: function (title) {
|
|
if (!this.alertWindowTimer) {
|
|
this.alertWindowTimer = new (function () {
|
|
var that = this;
|
|
var tmr;
|
|
var has_focus = true;
|
|
var state = 0;
|
|
var default_title = _kiwi.app.server_settings.client.window_title || 'Kiwi IRC';
|
|
var title = 'Kiwi IRC';
|
|
|
|
this.setTitle = function (new_title) {
|
|
new_title = new_title || default_title;
|
|
window.document.title = new_title;
|
|
return new_title;
|
|
};
|
|
|
|
this.start = function (new_title) {
|
|
// Don't alert if we already have focus
|
|
if (has_focus) return;
|
|
|
|
title = new_title;
|
|
if (tmr) return;
|
|
tmr = setInterval(this.update, 1000);
|
|
};
|
|
|
|
this.stop = function () {
|
|
// Stop the timer and clear the title
|
|
if (tmr) clearInterval(tmr);
|
|
tmr = null;
|
|
this.setTitle();
|
|
|
|
// Some browsers don't always update the last title correctly
|
|
// Wait a few seconds and then reset
|
|
setTimeout(this.reset, 2000);
|
|
};
|
|
|
|
this.reset = function () {
|
|
if (tmr) return;
|
|
that.setTitle();
|
|
};
|
|
|
|
|
|
this.update = function () {
|
|
if (state === 0) {
|
|
that.setTitle(title);
|
|
state = 1;
|
|
} else {
|
|
that.setTitle();
|
|
state = 0;
|
|
}
|
|
};
|
|
|
|
$(window).focus(function (event) {
|
|
has_focus = true;
|
|
that.stop();
|
|
|
|
// Some browsers don't always update the last title correctly
|
|
// Wait a few seconds and then reset
|
|
setTimeout(that.reset, 2000);
|
|
});
|
|
|
|
$(window).blur(function (event) {
|
|
has_focus = false;
|
|
});
|
|
})();
|
|
}
|
|
|
|
this.alertWindowTimer.start(title);
|
|
},
|
|
|
|
|
|
barsHide: function (instant) {
|
|
var that = this;
|
|
|
|
if (!instant) {
|
|
this.$el.find('.toolbar').slideUp({queue: false, duration: 400, step: $.proxy(this.doLayout, this)});
|
|
$('#kiwi .controlbox').slideUp({queue: false, duration: 400, step: $.proxy(this.doLayout, this)});
|
|
} else {
|
|
this.$el.find('.toolbar').slideUp(0);
|
|
$('#kiwi .controlbox').slideUp(0);
|
|
this.doLayout();
|
|
}
|
|
},
|
|
|
|
barsShow: function (instant) {
|
|
var that = this;
|
|
|
|
if (!instant) {
|
|
this.$el.find('.toolbar').slideDown({queue: false, duration: 400, step: $.proxy(this.doLayout, this)});
|
|
$('#kiwi .controlbox').slideDown({queue: false, duration: 400, step: $.proxy(this.doLayout, this)});
|
|
} else {
|
|
this.$el.find('.toolbar').slideDown(0);
|
|
$('#kiwi .controlbox').slideDown(0);
|
|
this.doLayout();
|
|
}
|
|
},
|
|
|
|
|
|
initSound: function () {
|
|
var that = this,
|
|
base_path = this.model.get('base_path');
|
|
|
|
$script(base_path + '/assets/libs/soundmanager2/soundmanager2-nodebug-jsmin.js', function() {
|
|
if (typeof soundManager === 'undefined')
|
|
return;
|
|
|
|
soundManager.setup({
|
|
url: base_path + '/assets/libs/soundmanager2/',
|
|
flashVersion: 9, // optional: shiny features (default = 8)// optional: ignore Flash where possible, use 100% HTML5 mode
|
|
preferFlash: false,
|
|
|
|
onready: function() {
|
|
that.sound_object = soundManager.createSound({
|
|
id: 'highlight',
|
|
url: base_path + '/assets/sound/highlight.mp3'
|
|
});
|
|
}
|
|
});
|
|
});
|
|
},
|
|
|
|
|
|
playSound: function (sound_id) {
|
|
if (!this.sound_object) return;
|
|
|
|
if (_kiwi.global.settings.get('mute_sounds'))
|
|
return;
|
|
|
|
soundManager.play(sound_id);
|
|
},
|
|
|
|
|
|
showNotification: function(title, message) {
|
|
var icon = this.model.get('base_path') + '/assets/img/ico.png',
|
|
notifications = _kiwi.utils.notifications;
|
|
|
|
if (!this.has_focus && notifications.allowed()) {
|
|
notifications
|
|
.create(title, { icon: icon, body: message })
|
|
.closeAfter(5000)
|
|
.on('click', _.bind(window.focus, window));
|
|
}
|
|
},
|
|
|
|
monitorPanelFallback: function() {
|
|
var panel_access = [];
|
|
|
|
this.model.panels.on('active', function() {
|
|
var panel = _kiwi.app.panels().active,
|
|
panel_index;
|
|
|
|
// If the panel is already open, remove it so we can put it back in first place
|
|
panel_index = _.indexOf(panel_access, panel.cid);
|
|
|
|
if (panel_index > -1) {
|
|
panel_access.splice(panel_index, 1);
|
|
}
|
|
|
|
//Make this panel the most recently accessed
|
|
panel_access.unshift(panel.cid);
|
|
});
|
|
|
|
this.model.panels.on('remove', function(panel) {
|
|
// If closing the active panel, switch to the last-accessed panel
|
|
if (panel_access[0] === panel.cid) {
|
|
panel_access.shift();
|
|
|
|
//Get the last-accessed panel model now that we removed the closed one
|
|
var model = _.find(_kiwi.app.panels('applets').concat(_kiwi.app.panels('connections')), {cid: panel_access[0]});
|
|
|
|
if (model) {
|
|
model.view.show();
|
|
}
|
|
}
|
|
});
|
|
}
|
|
});
|
|
|
|
|
|
|
|
_kiwi.view.AppToolbar = Backbone.View.extend({
|
|
events: {
|
|
'click .settings': 'clickSettings',
|
|
'click .startup': 'clickStartup'
|
|
},
|
|
|
|
initialize: function () {
|
|
// Remove the new connection/startup link if the server has disabled server changing
|
|
if (_kiwi.app.server_settings.connection && !_kiwi.app.server_settings.connection.allow_change) {
|
|
this.$('.startup').css('display', 'none');
|
|
}
|
|
},
|
|
|
|
clickSettings: function (event) {
|
|
event.preventDefault();
|
|
_kiwi.app.controlbox.processInput('/settings');
|
|
},
|
|
|
|
clickStartup: function (event) {
|
|
event.preventDefault();
|
|
_kiwi.app.startup_applet.view.show();
|
|
}
|
|
});
|
|
|
|
|
|
|
|
_kiwi.view.ControlBox = Backbone.View.extend({
|
|
events: {
|
|
'keydown .inp': 'inputKeyDown',
|
|
'keyup .inp': 'inputKeyUp',
|
|
'blur .inp': 'inputBlur',
|
|
'click .nick': 'showNickChange'
|
|
},
|
|
|
|
initialize: function () {
|
|
var that = this;
|
|
|
|
this.buffer = []; // Stores previously run commands
|
|
this.buffer_pos = 0; // The current position in the buffer
|
|
|
|
this.preprocessor = new InputPreProcessor();
|
|
this.preprocessor.recursive_depth = 5;
|
|
|
|
this.autocomplete = new AutoComplete({el: this.$('.autocomplete')[0]});
|
|
this.autocomplete_command_list = [];
|
|
this.bindAutocomplete();
|
|
|
|
// Keep the nick view updated with nick changes
|
|
_kiwi.app.connections.on('change:nick', function(connection) {
|
|
// Only update the nick view if it's the active connection
|
|
if (connection !== _kiwi.app.connections.active_connection)
|
|
return;
|
|
|
|
$('.nick', that.$el).text(connection.get('nick'));
|
|
});
|
|
|
|
// Update our nick view as we flick between connections
|
|
_kiwi.app.connections.on('active', function(panel, connection) {
|
|
$('.nick', that.$el).text(connection.get('nick'));
|
|
});
|
|
},
|
|
|
|
render: function() {
|
|
var send_message_text = translateText('client_views_controlbox_message');
|
|
this.$('.inp').attr('placeholder', send_message_text);
|
|
|
|
return this;
|
|
},
|
|
|
|
bindAutocomplete: function() {
|
|
this.listenTo(this.autocomplete, 'match', function(word, matched) {
|
|
// A final word is selected. Either by clicking or hitting enter
|
|
var trailing = '';
|
|
if (matched.type === 'nick' && this.autocomplete_token_idx === 0) {
|
|
trailing = ': ';
|
|
}
|
|
this.autoCompleteFillWord(word + trailing, true);
|
|
this.autocomplete.close();
|
|
});
|
|
|
|
this.listenTo(this.autocomplete, 'selected', function(word, matched) {
|
|
// Words are selected while scrolling through the available options
|
|
var trailing = '';
|
|
if (matched && matched.type === 'nick' && this.autocomplete_token_idx === 0) {
|
|
trailing = ': ';
|
|
}
|
|
|
|
// Only display the match if we're not filtering through the UI
|
|
if (!this.autocomplete.filter_list) {
|
|
this.autoCompleteFillWord(word ? word + trailing : this.autocomplete.matching_against_word, true);
|
|
}
|
|
});
|
|
|
|
|
|
var focus_after_close = true;
|
|
this.listenTo(this.autocomplete, 'cancel', function(reason) {
|
|
var $inp = this.$('.inp'),
|
|
inp = $inp[0],
|
|
inp_val = $inp.val(),
|
|
caret_pos = $inp.selectRange();
|
|
|
|
// If we hit space while typing a word, then take chars 0->caret_pos, remove rest of word, include rest of input value
|
|
if (reason === 'typing' || reason === 'lost_focus') {
|
|
var trailing_start_pos = inp_val.indexOf(' ', caret_pos);
|
|
if (trailing_start_pos === -1 ) trailing_start_pos = inp_val.length;
|
|
$inp.val(inp_val.substr(0, caret_pos) + inp_val.substr(trailing_start_pos+1, inp_val.length));
|
|
} else {
|
|
$inp.val(this.autocomplete_before.value);
|
|
}
|
|
|
|
focus_after_close = (reason === 'lost_focus') ?
|
|
false :
|
|
true;
|
|
|
|
if (focus_after_close) {
|
|
// Move the cursor position back to where it was
|
|
$inp.selectRange(caret_pos);
|
|
}
|
|
|
|
this.autocomplete.close();
|
|
});
|
|
|
|
this.listenTo(this.autocomplete, 'close', function() {
|
|
if (focus_after_close) this.$('.inp').focus();
|
|
});
|
|
|
|
this.listenTo(this.autocomplete, 'action-message', function(nick) {
|
|
_kiwi.app.connections.active_connection.createQuery(nick);
|
|
this.autocomplete.close();
|
|
this.$('.inp').val('');
|
|
});
|
|
|
|
this.listenTo(this.autocomplete, 'action-more', function(nick) {
|
|
var active_panel = _kiwi.app.panels().active,
|
|
members = active_panel.get('members'),
|
|
member = members.getByNick(nick),
|
|
userbox,
|
|
are_we_an_op = !!members.getByNick(_kiwi.app.connections.active_connection.get('nick')).get('is_op');
|
|
|
|
userbox = new _kiwi.view.UserBox();
|
|
userbox.setTargets(member, active_panel);
|
|
userbox.displayOpItems(are_we_an_op);
|
|
|
|
var menu = new _kiwi.view.MenuBox(member.get('nick') || 'User');
|
|
menu.addItem('userbox', userbox.$el);
|
|
menu.showFooter(false);
|
|
|
|
_kiwi.global.events.emit('usermenu:created', {menu: menu, userbox: userbox, user: member})
|
|
.then(_.bind(function() {
|
|
menu.show();
|
|
|
|
var t = _kiwi.app.view.$el.height() - this.autocomplete.$el.outerHeight() - menu.$el.outerHeight();
|
|
var l = _kiwi.app.view.$el.width() - menu.$el.outerWidth();
|
|
|
|
// Set the new positon
|
|
menu.$el.offset({
|
|
left: l,
|
|
top: t
|
|
});
|
|
|
|
}, this))
|
|
.then(null, _.bind(function() {
|
|
userbox = null;
|
|
|
|
menu.dispose();
|
|
menu = null;
|
|
}, this));
|
|
});
|
|
},
|
|
|
|
// Taking a matched word form the auto completion, fill it into the current
|
|
// selected word (where the cursor position is)
|
|
autoCompleteFillWord: function(word, place_cursor_after_word) {
|
|
var $inp = this.$('.inp'),
|
|
inp_val = $inp.val(),
|
|
caret_pos = $inp[0].selectionStart;
|
|
|
|
// If we have the trailing ': ' after nicks, we need to check further back to find
|
|
// the start of the current word
|
|
var trailing_found = inp_val.substr(caret_pos-2, 2) === ': ';
|
|
|
|
var word_start_pos = inp_val.lastIndexOf(' ', caret_pos - (trailing_found ? 2 : 1));
|
|
word_start_pos = (word_start_pos === -1) ? 0 : word_start_pos + 1; // If no space found, start from 0. Otherwise, add 1 to include the space
|
|
var word_end_pos = inp_val.indexOf(' ', word_start_pos);
|
|
if (word_end_pos === -1) word_end_pos = inp_val.length;
|
|
|
|
var start_of_inp = inp_val.substr(0, word_start_pos); // Get text before current selected word
|
|
var rest_of_inp = inp_val.substr(word_end_pos); // Get text after current selected word
|
|
var new_val = start_of_inp + word + rest_of_inp; // Join strings before word, the new word, and after word
|
|
|
|
$inp.val(new_val);
|
|
|
|
// Set the cursor to the same posiiton of the current word as was previous
|
|
var new_position = word_start_pos + this.autocomplete.matching_against_word.length;
|
|
if (place_cursor_after_word) {
|
|
new_position = word_start_pos + word.length;
|
|
}
|
|
|
|
// Move the cursor position to the new position
|
|
if ($inp[0].setSelectionRange) {
|
|
$inp[0].setSelectionRange(new_position, new_position);
|
|
} else if ($inp[0].createTextRange) { // IE8 support
|
|
range = $inp[0].createTextRange();
|
|
range.collapse(true);
|
|
range.moveEnd('character', new_position);
|
|
range.moveStart('character', new_position);
|
|
range.select();
|
|
}
|
|
},
|
|
|
|
showNickChange: function (ev) {
|
|
// Nick box already open? Don't do it again
|
|
if (this.nick_change)
|
|
return;
|
|
|
|
this.nick_change = new _kiwi.view.NickChangeBox();
|
|
this.nick_change.render();
|
|
|
|
this.listenTo(this.nick_change, 'close', function() {
|
|
delete this.nick_change;
|
|
});
|
|
},
|
|
|
|
inputKeyUp: function (ev) {
|
|
// If we're filtering the auto complete list, update the UI with our updated word
|
|
if (this.autocomplete.open && this.autocomplete.filter_list) {
|
|
var $inp = $(ev.currentTarget);
|
|
var tokens = $inp.val().trim().substring(0, $inp[0].selectionStart).split(' ');
|
|
this.autocomplete.update(tokens[tokens.length - 1]);
|
|
}
|
|
},
|
|
|
|
inputKeyDown: function (ev) {
|
|
var that = this,
|
|
inp = $(ev.currentTarget),
|
|
inp_val = inp.val(),
|
|
meta;
|
|
|
|
if (navigator.appVersion.indexOf("Mac") !== -1) {
|
|
meta = ev.metaKey;
|
|
} else {
|
|
meta = ev.altKey;
|
|
}
|
|
|
|
if (this.autocomplete.open) {
|
|
// A return value of true = dont process any other keys
|
|
if (this.autocomplete.onKeyDown(ev)) {
|
|
return;
|
|
}
|
|
}
|
|
|
|
switch (true) {
|
|
case (ev.keyCode === 13): // return
|
|
inp_val = inp_val.trim();
|
|
|
|
if (inp_val) {
|
|
$.each(inp_val.split('\n'), function (idx, line) {
|
|
try {
|
|
that.processInput(line);
|
|
} catch (err) {
|
|
window.console && console.error(err);
|
|
}
|
|
});
|
|
|
|
this.buffer.push(inp_val);
|
|
this.buffer_pos = this.buffer.length;
|
|
}
|
|
|
|
inp.val('');
|
|
|
|
// The auto complete may not have thrown a match if it was empty, so
|
|
// just make sure it's closed
|
|
if (this.autocomplete.open) {
|
|
this.autocomplete.close();
|
|
}
|
|
|
|
return false;
|
|
|
|
break;
|
|
|
|
case (ev.keyCode === 38): // up
|
|
if (this.buffer_pos > 0) {
|
|
this.buffer_pos--;
|
|
inp.val(this.buffer[this.buffer_pos]);
|
|
}
|
|
//suppress browsers default behavior as it would set the cursor at the beginning
|
|
return false;
|
|
|
|
case (ev.keyCode === 40): // down
|
|
if (this.buffer_pos < this.buffer.length) {
|
|
this.buffer_pos++;
|
|
inp.val(this.buffer[this.buffer_pos]);
|
|
}
|
|
break;
|
|
|
|
case (ev.keyCode === 219 && meta): // [ + meta
|
|
// Find all the tab elements and get the index of the active tab
|
|
var $tabs = $('#kiwi .tabs').find('li[class!=connection]');
|
|
var cur_tab_ind = (function() {
|
|
for (var idx=0; idx<$tabs.length; idx++){
|
|
if ($($tabs[idx]).hasClass('active'))
|
|
return idx;
|
|
}
|
|
})();
|
|
|
|
// Work out the previous tab along. Wrap around if needed
|
|
if (cur_tab_ind === 0) {
|
|
$prev_tab = $($tabs[$tabs.length - 1]);
|
|
} else {
|
|
$prev_tab = $($tabs[cur_tab_ind - 1]);
|
|
}
|
|
|
|
$prev_tab.click();
|
|
return false;
|
|
|
|
case (ev.keyCode === 221 && meta): // ] + meta
|
|
// Find all the tab elements and get the index of the active tab
|
|
var $tabs = $('#kiwi .tabs').find('li[class!=connection]');
|
|
var cur_tab_ind = (function() {
|
|
for (var idx=0; idx<$tabs.length; idx++){
|
|
if ($($tabs[idx]).hasClass('active'))
|
|
return idx;
|
|
}
|
|
})();
|
|
|
|
// Work out the next tab along. Wrap around if needed
|
|
if (cur_tab_ind === $tabs.length - 1) {
|
|
$next_tab = $($tabs[0]);
|
|
} else {
|
|
$next_tab = $($tabs[cur_tab_ind + 1]);
|
|
}
|
|
|
|
$next_tab.click();
|
|
return false;
|
|
|
|
case (ev.keyCode === 9 //Check if ONLY tab is pressed
|
|
&& !ev.shiftKey //(user could be using some browser
|
|
&& !ev.altKey //keyboard shortcut)
|
|
&& !ev.metaKey
|
|
&& !ev.ctrlKey):
|
|
|
|
ev.preventDefault();
|
|
|
|
// Get possible autocompletions
|
|
var autocomplete_list = [],
|
|
members = _kiwi.app.panels().active.get('members');
|
|
|
|
if (members) {
|
|
members.forEach(function (member) {
|
|
if (!member) return;
|
|
autocomplete_list.push({match: [member.get('nick')], type: 'nick'});
|
|
});
|
|
}
|
|
|
|
// Add this channels name into the auto complete list
|
|
autocomplete_list.push(_kiwi.app.panels().active.get('name'));
|
|
|
|
// Sort what we have alphabetically
|
|
autocomplete_list = _.sortBy(autocomplete_list, function (entry) {
|
|
// Nicks have a .type property of 'nick'
|
|
return entry.type === 'nick' ?
|
|
entry.match[0].toLowerCase() :
|
|
entry.toLowerCase();
|
|
});
|
|
|
|
this.showAutocomplete(autocomplete_list, 'nicks');
|
|
|
|
break;
|
|
|
|
case (ev.keyCode === 191 && inp_val === ''): // Forward slash in an empty box
|
|
this.showAutocomplete(this.autocomplete_command_list, 'command', true);
|
|
break;
|
|
}
|
|
},
|
|
|
|
|
|
setAutoCompleteCommands: function(commands) {
|
|
_.each(commands, function(command) {
|
|
this.autocomplete_command_list.push({
|
|
match: command.matches || [],
|
|
description: command.description
|
|
});
|
|
}, this);
|
|
|
|
/*
|
|
var command_list = [
|
|
{match: ['/join'], description: 'Join or start a channel'},
|
|
{match: ['/part', '/leave'], description: 'Leave the channel'},
|
|
{match: ['/me', '/action'], description: 'Do something physical'},
|
|
{match: ['/nick'], description: 'Change your nickname'},
|
|
{match: ['/topic'], description: 'Set the topic for the channel'},
|
|
];
|
|
*/
|
|
},
|
|
|
|
|
|
inputBlur: function(event) {
|
|
// IE hack. Mouse down on auto complete UI sets cancel_blur so we don't loose
|
|
// focus here.
|
|
if (this.autocomplete.cancel_blur) {
|
|
delete this.autocomplete.cancel_blur;
|
|
return;
|
|
}
|
|
|
|
this.autocomplete.cancel('lost_focus');
|
|
},
|
|
|
|
|
|
processInput: function (command_raw) {
|
|
var that = this,
|
|
command, params, events_data,
|
|
pre_processed;
|
|
|
|
// If sending a message when not in a channel or query window, automatically
|
|
// convert it into a command
|
|
if (command_raw[0] !== '/' && !_kiwi.app.panels().active.isChannel() && !_kiwi.app.panels().active.isQuery()) {
|
|
command_raw = '/' + command_raw;
|
|
}
|
|
|
|
// The default command
|
|
if (command_raw[0] !== '/' || command_raw.substr(0, 2) === '//') {
|
|
// Remove any slash escaping at the start (ie. //)
|
|
command_raw = command_raw.replace(/^\/\//, '/');
|
|
|
|
// Prepend the default command
|
|
command_raw = '/msg ' + _kiwi.app.panels().active.get('name') + ' ' + command_raw;
|
|
}
|
|
|
|
// Process the raw command for any aliases
|
|
this.preprocessor.vars.server = _kiwi.app.connections.active_connection.get('name');
|
|
this.preprocessor.vars.channel = _kiwi.app.panels().active.get('name');
|
|
this.preprocessor.vars.destination = this.preprocessor.vars.channel;
|
|
command_raw = this.preprocessor.process(command_raw);
|
|
|
|
// Extract the command and parameters
|
|
params = command_raw.split(/\s/);
|
|
if (params[0][0] === '/') {
|
|
command = params[0].substr(1).toLowerCase();
|
|
params = params.splice(1, params.length - 1);
|
|
} else {
|
|
// Default command
|
|
command = 'msg';
|
|
params.unshift(_kiwi.app.panels().active.get('name'));
|
|
}
|
|
|
|
// Emit a plugin event for any modifications
|
|
events_data = {command: command, params: params};
|
|
|
|
_kiwi.global.events.emit('command', events_data)
|
|
.then(function() {
|
|
// Trigger the command events
|
|
that.trigger('command', {command: events_data.command, params: events_data.params});
|
|
that.trigger('command:' + events_data.command, {command: events_data.command, params: events_data.params});
|
|
|
|
// If we didn't have any listeners for this event, fire a special case
|
|
// TODO: This feels dirty. Should this really be done..?
|
|
if (!that._events['command:' + events_data.command]) {
|
|
that.trigger('unknown_command', {command: events_data.command, params: events_data.params});
|
|
}
|
|
});
|
|
},
|
|
|
|
|
|
showAutocomplete: function(list, type, filter_list) {
|
|
var $inp = this.$('.inp'),
|
|
tokens = $inp.val().trim().substring(0, $inp[0].selectionStart).split(' ');
|
|
|
|
this.autocomplete_token_idx = tokens.length - 1;
|
|
this.autocomplete_before = {
|
|
value: $inp.val(),
|
|
caret_pos: $inp[0].selectionStart
|
|
};
|
|
|
|
this.autocomplete.showUi(!!_kiwi.global.settings.get('show_autocomplete_slideout'));
|
|
this.autocomplete.setTitle(type);
|
|
this.autocomplete.setWords(list, filter_list);
|
|
this.autocomplete.update(tokens[tokens.length - 1]);
|
|
this.autocomplete.show();
|
|
},
|
|
|
|
|
|
addPluginIcon: function ($icon) {
|
|
var $tool = $('<div class="tool"></div>').append($icon);
|
|
this.$el.find('.input_tools').append($tool);
|
|
_kiwi.app.view.doLayout();
|
|
}
|
|
});
|
|
|
|
|
|
|
|
var AutoComplete = Backbone.View.extend({
|
|
events: {
|
|
'click .autocomplete-item': 'onItemClick',
|
|
'mousemove .autocomplete-item': 'onItemMouseMove',
|
|
'mousedown' : 'onMouseDown',
|
|
'click .autocomplete-item .action': 'onActionClick'
|
|
},
|
|
|
|
initialize: function() {
|
|
this.$list = $('<ul class="autocomplete-list"></ul>');
|
|
this.$list.appendTo(this.$el);
|
|
|
|
this.reset();
|
|
this.open = false;
|
|
this._show_ui = true;
|
|
this.filter_list = false;
|
|
},
|
|
|
|
render: function() {
|
|
return this;
|
|
},
|
|
|
|
|
|
showUi: function(show_ui) {
|
|
this._show_ui = show_ui;
|
|
},
|
|
|
|
|
|
// Set the list of words to be searching through
|
|
setWords: function(word_list, filter_list) {
|
|
var new_list = [];
|
|
var template_str_default = '<li class="autocomplete-item"><span class="word"><%= word %></span><span class="matches"><%= match_list %></span><span class="description"><%= description %></span></li>';
|
|
var template_str_nicks = '<li class="autocomplete-item autocomplete-nick" data-nick="<%= word %>"><span class="word"><%= match_list %></span><span class="actions"><a class="action" data-event="message">Message</a><a class="action" data-event="more">More...</a></span></li>';
|
|
var template = {};
|
|
|
|
this.reset();
|
|
|
|
this.filter_list = !!filter_list;
|
|
|
|
_.each(word_list, function(word) {
|
|
var template_str, $el, $word;
|
|
|
|
if (this._show_ui) {
|
|
if (typeof word === 'string') {
|
|
template.match_list = '';
|
|
template.word = word;
|
|
template.description = '';
|
|
} else {
|
|
// Only show the alternative matches for this word if there is more than 1
|
|
// Eg. for matching /part show the alternatives ['/part', '/leave']
|
|
template.match_list = template.word = (word.match.length > 1 ? word.match.join(', ') : '');
|
|
template.description = word.description || '';
|
|
}
|
|
|
|
template_str = (word.type === 'nick') ? template_str_nicks : template_str_default;
|
|
|
|
$el = $(_.template(template_str)(template)).hide();
|
|
$word = $el.find('.word');
|
|
} else {
|
|
template_str = '';
|
|
$el = null;
|
|
$word = null;
|
|
}
|
|
|
|
var list_entry = {
|
|
match: (typeof word === 'string') ? [word] : word.match,
|
|
type: (word.type === 'nick') ? 'nick' : 'default',
|
|
$el: $el,
|
|
$word: $word
|
|
};
|
|
|
|
new_list.push(list_entry);
|
|
$el && $el.data('word', list_entry);
|
|
$el && $el.appendTo(this.$list);
|
|
}, this);
|
|
|
|
this.list = new_list;
|
|
},
|
|
|
|
setTitle: function(type) {
|
|
var texts = {
|
|
nicks: 'People or channels',
|
|
command: 'Commands'
|
|
};
|
|
|
|
this.$('.autocomplete-header-label').text(texts[type] || texts['nicks']);
|
|
},
|
|
|
|
|
|
// Update the list with a word to search for
|
|
update: function(word) {
|
|
var first_match = null;
|
|
|
|
// No need to update the list if it's the same search
|
|
if (this.matching_against_word !== null && word.toLowerCase() === this.matching_against_word.toLowerCase()) {
|
|
return false;
|
|
}
|
|
|
|
// Filter our available auto complete list down to ones that match
|
|
this.matches = _.filter(this.list, function(item) {
|
|
var matched_word = _.find(item.match, function(match_word) {
|
|
if (match_word.toLowerCase().indexOf(word.toLowerCase()) === 0) {
|
|
return match_word;
|
|
}
|
|
});
|
|
|
|
if (matched_word) {
|
|
item.matched_word = matched_word;
|
|
|
|
if (this._show_ui) {
|
|
item.$word.text(matched_word);
|
|
item.$el.show();
|
|
}
|
|
|
|
if (!first_match) {
|
|
first_match = item;
|
|
}
|
|
|
|
} else {
|
|
item.matched_word = null;
|
|
if (this._show_ui) {
|
|
item.$el.hide();
|
|
}
|
|
}
|
|
|
|
return matched_word;
|
|
}, this);
|
|
|
|
this.matching_against_word = word;
|
|
|
|
this.$('.selected').removeClass('selected');
|
|
// Reset the selected match to the first
|
|
this.selected_idx = 0;
|
|
|
|
if (first_match) {
|
|
this.selectEl(this.matches[0].$el);
|
|
this.trigger('selected', first_match.matched_word, first_match);
|
|
} else {
|
|
this.trigger('selected', null);
|
|
}
|
|
},
|
|
|
|
|
|
show: function() {
|
|
this.open = true;
|
|
if (this._show_ui) {
|
|
this.$el.css('max-height', (_kiwi.app.view.$el.height() / 2) + 'px').show();
|
|
this.$list.css('max-height', (_kiwi.app.view.$el.height() / 2)-32 + 'px').show();
|
|
}
|
|
},
|
|
|
|
|
|
close: function() {
|
|
this.open = false;
|
|
this._show_ui && this.$el.hide();
|
|
this.reset();
|
|
this.trigger('close');
|
|
},
|
|
|
|
|
|
reset: function() {
|
|
this.matching_against_word = null;
|
|
this._show_ui && this.$list.empty();
|
|
this.list = [];
|
|
this.matches = [];
|
|
this.selected_idx = 0;
|
|
},
|
|
|
|
|
|
onMouseDown: function(event) {
|
|
// This stops the control input box from loosing focus when clicking here
|
|
event.preventDefault();
|
|
|
|
// IE doesn't prevent moving focus even with event.preventDefault()
|
|
// so we set a flag to know when we should ignore the blur event
|
|
this.cancel_blur = true;
|
|
_.defer(_.bind(function() {
|
|
delete this.cancel_blur;
|
|
}, this));
|
|
},
|
|
|
|
|
|
onItemClick: function(event) {
|
|
var el_data = $(event.currentTarget).data('word');
|
|
if (!el_data) return;
|
|
this.trigger('match', el_data.matched_word, el_data);
|
|
},
|
|
|
|
|
|
onItemMouseMove: function(event) {
|
|
$this = $(event.currentTarget);
|
|
|
|
// No need to re-add the class if it already has it
|
|
if ($this.hasClass('selected')) {
|
|
return;
|
|
}
|
|
|
|
var idx = null;
|
|
_.each(this.matches, function(match, match_idx) {
|
|
if (match.$el[0] === $this[0]) {
|
|
idx = match_idx;
|
|
return false;
|
|
}
|
|
});
|
|
|
|
if (idx !== null) {
|
|
this.selected_idx = idx;
|
|
this.selectEl($this);
|
|
}
|
|
},
|
|
|
|
|
|
onActionClick: function(event) {
|
|
event.stopPropagation();
|
|
|
|
var $this = $(event.currentTarget),
|
|
event_name = $this.data('event'),
|
|
$item = $this.parents('.autocomplete-item'),
|
|
el_data = $item.data('word');
|
|
|
|
this.trigger('action-'+event_name, el_data.matched_word, el_data);
|
|
},
|
|
|
|
|
|
previous: function() {
|
|
this.selected_idx = this.matches[this.selected_idx-1] ? this.selected_idx-1 : this.matches.length-1;
|
|
|
|
if (this.matches[this.selected_idx]) {
|
|
this.selectEl(this.matches[this.selected_idx].$el, true);
|
|
this.trigger('selected', this.matches[this.selected_idx].matched_word, this.matches[this.selected_idx]);
|
|
}
|
|
},
|
|
|
|
|
|
next: function() {
|
|
this.selected_idx = this.matches[this.selected_idx+1] ? this.selected_idx+1 : 0;
|
|
|
|
if (this.matches[this.selected_idx]) {
|
|
this.selectEl(this.matches[this.selected_idx].$el, true);
|
|
this.trigger('selected', this.matches[this.selected_idx].matched_word, this.matches[this.selected_idx]);
|
|
}
|
|
},
|
|
|
|
|
|
cancel: function(reason) {
|
|
this.trigger('cancel', reason);
|
|
},
|
|
|
|
|
|
selectEl: function($el, scroll_in_view) {
|
|
var el, this_height;
|
|
|
|
if (!this._show_ui) return;
|
|
|
|
this.$('.selected').removeClass('selected');
|
|
if ($el) {
|
|
$el.addClass('selected');
|
|
}
|
|
|
|
if ($el && scroll_in_view) {
|
|
el = this.$el[0];
|
|
this_height = this.$el.height();
|
|
|
|
$el[0].scrollIntoView();
|
|
|
|
if($el.position().top + $el.outerHeight() > this_height / 2){
|
|
el.scrollTop = el.scrollHeight;
|
|
} else {
|
|
el.scrollTop -= (this_height / 2);
|
|
}
|
|
}
|
|
},
|
|
|
|
|
|
currentMatch: function() {
|
|
return this.matches[this.selected_idx] ?
|
|
this.matches[this.selected_idx].matched_word :
|
|
null;
|
|
},
|
|
|
|
|
|
onKeyDown: function(event) {
|
|
if (!this.open) return;
|
|
|
|
var $inp = $(event.currentTarget);
|
|
var dont_process_other_input_keys = false;
|
|
|
|
// Handling input box caret positioning
|
|
var caret_pos = $inp[0].selectionStart,
|
|
new_position = 0,
|
|
text_range;
|
|
|
|
if (event.keyCode === 38 || (event.keyCode === 9 && event.shiftKey)) { // up or tab+shift
|
|
this.previous();
|
|
event.preventDefault();
|
|
dont_process_other_input_keys = true;
|
|
}
|
|
else if (event.keyCode === 40 || event.keyCode === 9) { // down or tab
|
|
this.next();
|
|
event.preventDefault();
|
|
dont_process_other_input_keys = true;
|
|
}
|
|
else if (caret_pos === 1 && (event.keyCode === 37 || event.keyCode === 8)) { // Caret about to move to the beginning of the box
|
|
event.preventDefault();
|
|
this.cancel('caret_moved');
|
|
}
|
|
else if (0 && event.keyCode === 37) { // left
|
|
// If the caret is moved before the current word, stop autocompleting
|
|
if (caret_pos > 0 && $inp.val().toUpperCase()[caret_pos-1] === ' ') {
|
|
event.preventDefault();
|
|
this.cancel('caret_moved');
|
|
}
|
|
}
|
|
else if (event.keyCode === 13) { // return
|
|
if (this.matches[this.selected_idx]) {
|
|
this.trigger('match', this.currentMatch(), this.matches[this.selected_idx]);
|
|
event.preventDefault();
|
|
|
|
// If the UI is not open, let the return key keep processing as normal.
|
|
// If we did not let this happen, since there is no visual UI it would look
|
|
// weird to the user if they had to press return twice for something to happen.
|
|
dont_process_other_input_keys = this._show_ui ? true : false;
|
|
}
|
|
}
|
|
else if (event.keyCode === 27) { // escape
|
|
this.cancel();
|
|
}
|
|
else if (event.keyCode === 32) { // space
|
|
this.cancel('typing');
|
|
}
|
|
else if (event.keyCode === 16) { // shift
|
|
// Shift is used to tab+shift
|
|
dont_process_other_input_keys = true;
|
|
}
|
|
else if (!this.filter_list) {
|
|
// If we have started typing again, cancel the autocomplete
|
|
this.cancel('typing');
|
|
}
|
|
|
|
return dont_process_other_input_keys;
|
|
}
|
|
});
|
|
|
|
|
|
_kiwi.view.Favicon = Backbone.View.extend({
|
|
initialize: function () {
|
|
var that = this,
|
|
$win = $(window);
|
|
|
|
this.has_focus = true;
|
|
this.highlight_count = 0;
|
|
// Check for html5 canvas support
|
|
this.has_canvas_support = !!window.CanvasRenderingContext2D;
|
|
|
|
// Store the original favicon
|
|
this.original_favicon = $('link[rel~="icon"]')[0].href;
|
|
|
|
// Create our favicon canvas
|
|
this._createCanvas();
|
|
|
|
// Reset favicon notifications when user focuses window
|
|
$win.on('focus', function () {
|
|
that.has_focus = true;
|
|
that._resetHighlights();
|
|
});
|
|
$win.on('blur', function () {
|
|
that.has_focus = false;
|
|
});
|
|
},
|
|
|
|
newHighlight: function () {
|
|
var that = this;
|
|
if (!this.has_focus) {
|
|
this.highlight_count++;
|
|
if (this.has_canvas_support) {
|
|
this._drawFavicon(function() {
|
|
that._drawBubble(that.highlight_count.toString());
|
|
that._refreshFavicon(that.canvas.toDataURL());
|
|
});
|
|
}
|
|
}
|
|
},
|
|
|
|
_resetHighlights: function () {
|
|
var that = this;
|
|
this.highlight_count = 0;
|
|
this._refreshFavicon(this.original_favicon);
|
|
},
|
|
|
|
_drawFavicon: function (callback) {
|
|
var that = this,
|
|
canvas = this.canvas,
|
|
context = canvas.getContext('2d'),
|
|
favicon_image = new Image();
|
|
|
|
// Allow cross origin resource requests
|
|
favicon_image.crossOrigin = 'anonymous';
|
|
// Trigger the load event
|
|
favicon_image.src = this.original_favicon;
|
|
|
|
favicon_image.onload = function() {
|
|
// Clear canvas from prevous iteration
|
|
context.clearRect(0, 0, canvas.width, canvas.height);
|
|
// Draw the favicon itself
|
|
context.drawImage(favicon_image, 0, 0, canvas.width, canvas.height);
|
|
callback();
|
|
};
|
|
},
|
|
|
|
_drawBubble: function (label) {
|
|
var letter_spacing,
|
|
bubble_width = 0, bubble_height = 0,
|
|
canvas = this.canvas,
|
|
context = test_context = canvas.getContext('2d'),
|
|
canvas_width = canvas.width,
|
|
canvas_height = canvas.height;
|
|
|
|
// Different letter spacing for MacOS
|
|
if (navigator.appVersion.indexOf("Mac") !== -1) {
|
|
letter_spacing = -1.5;
|
|
}
|
|
else {
|
|
letter_spacing = -1;
|
|
}
|
|
|
|
// Setup a test canvas to get text width
|
|
test_context.font = context.font = 'bold 10px Arial';
|
|
test_context.textAlign = 'right';
|
|
this._renderText(test_context, label, 0, 0, letter_spacing);
|
|
|
|
// Calculate bubble width based on letter spacing and padding
|
|
bubble_width = test_context.measureText(label).width + letter_spacing * (label.length - 1) + 2;
|
|
// Canvas does not have any way of measuring text height, so we just do it manually and add 1px top/bottom padding
|
|
bubble_height = 9;
|
|
|
|
// Set bubble coordinates
|
|
bubbleX = canvas_width - bubble_width;
|
|
bubbleY = canvas_height - bubble_height;
|
|
|
|
// Draw bubble background
|
|
context.fillStyle = 'red';
|
|
context.fillRect(bubbleX, bubbleY, bubble_width, bubble_height);
|
|
|
|
// Draw the text
|
|
context.fillStyle = 'white';
|
|
this._renderText(context, label, canvas_width - 1, canvas_height - 1, letter_spacing);
|
|
},
|
|
|
|
_refreshFavicon: function (url) {
|
|
$('link[rel~="icon"]').remove();
|
|
$('<link rel="shortcut icon" href="' + url + '">').appendTo($('head'));
|
|
},
|
|
|
|
_createCanvas: function () {
|
|
var canvas = document.createElement('canvas');
|
|
canvas.width = 16;
|
|
canvas.height = 16;
|
|
|
|
this.canvas = canvas;
|
|
},
|
|
|
|
_renderText: function (context, text, x, y, letter_spacing) {
|
|
// A hacky solution for letter-spacing, but works well with small favicon text
|
|
// Modified from http://jsfiddle.net/davidhong/hKbJ4/
|
|
var current,
|
|
characters = text.split('').reverse(),
|
|
index = 0,
|
|
currentPosition = x;
|
|
|
|
while (index < text.length) {
|
|
current = characters[index++];
|
|
context.fillText(current, currentPosition, y);
|
|
currentPosition += (-1 * (context.measureText(current).width + letter_spacing));
|
|
}
|
|
|
|
return context;
|
|
}
|
|
});
|
|
|
|
|
|
|
|
_kiwi.view.MediaMessage = Backbone.View.extend({
|
|
events: {
|
|
'click .media_close': 'close'
|
|
},
|
|
|
|
initialize: function () {
|
|
// Get the URL from the data
|
|
this.url = this.$el.data('url');
|
|
},
|
|
|
|
toggle: function () {
|
|
if (!this.$content || !this.$content.is(':visible')) {
|
|
this.open();
|
|
} else {
|
|
this.close();
|
|
}
|
|
},
|
|
|
|
// Close the media content and remove it from display
|
|
close: function () {
|
|
var that = this;
|
|
this.$content.slideUp('fast', function () {
|
|
that.$content.remove();
|
|
});
|
|
},
|
|
|
|
// Open the media content within its wrapper
|
|
open: function () {
|
|
// Create the content div if we haven't already
|
|
if (!this.$content) {
|
|
this.$content = $('<div class="media_content"><a class="media_close"><i class="fa fa-chevron-up"></i> ' + _kiwi.global.i18n.translate('client_views_mediamessage_close').fetch() + '</a><br /><div class="content"></div></div>');
|
|
this.$content.find('.content').append(this.mediaTypes[this.$el.data('type')].apply(this, []) || _kiwi.global.i18n.translate('client_views_mediamessage_notfound').fetch() + ' :(');
|
|
}
|
|
|
|
// Now show the content if not already
|
|
if (!this.$content.is(':visible')) {
|
|
// Hide it first so the slideDown always plays
|
|
this.$content.hide();
|
|
|
|
// Add the media content and slide it into view
|
|
this.$el.append(this.$content);
|
|
this.$content.slideDown();
|
|
}
|
|
},
|
|
|
|
|
|
|
|
// Generate the media content for each recognised type
|
|
mediaTypes: {
|
|
twitter: function () {
|
|
var tweet_id = this.$el.data('tweetid');
|
|
var that = this;
|
|
|
|
$.getJSON('https://api.twitter.com/1/statuses/oembed.json?id=' + tweet_id + '&callback=?', function (data) {
|
|
that.$content.find('.content').html(data.html);
|
|
});
|
|
|
|
return $('<div>' + _kiwi.global.i18n.translate('client_views_mediamessage_load_tweet').fetch() + '...</div>');
|
|
},
|
|
|
|
|
|
image: function () {
|
|
return $('<a href="' + _.escape(this.url) + '" target="_blank"><img height="100" src="' + _.escape(this.url) + '" /></a>');
|
|
},
|
|
|
|
|
|
imgur: function () {
|
|
var that = this;
|
|
$.getJSON('https://api.imgur.com/oembed?url=' + this.$el.data('id') + '&maxheight=100&maxwidth=100', function (data) {
|
|
that.$content.find('.content').html(data.html);
|
|
}).fail(function () {
|
|
that.$content.find('.content').html('<i class="fa fa-exclamation-triangle"></i> No content.');
|
|
});
|
|
return $('<div>' + _kiwi.global.i18n.translate('client_views_mediamessage_load_image').fetch() + '...</div>');
|
|
},
|
|
|
|
|
|
reddit: function () {
|
|
var that = this;
|
|
var matches = (/reddit\.com\/r\/([a-zA-Z0-9_\-]+)\/comments\/([a-z0-9]+)\/([^\/]+)?/gi).exec(this.url);
|
|
|
|
$.getJSON('https://www.' + matches[0] + '.json?jsonp=?', function (data) {
|
|
console.log('Loaded reddit data', data);
|
|
var post = data[0].data.children[0].data;
|
|
var thumb = '';
|
|
|
|
// Show a thumbnail if there is one
|
|
if (post.thumbnail) {
|
|
//post.thumbnail = 'http://www.eurotunnel.com/uploadedImages/commercial/back-steps-icon-arrow.png';
|
|
var thumbnail = 'https' + post.thumbnail.substr(4);
|
|
|
|
// Hide the thumbnail if an over_18 image
|
|
if (post.over_18) {
|
|
thumb = '<span class="thumbnail_nsfw" onclick="$(this).find(\'p\').remove(); $(this).find(\'img\').css(\'visibility\', \'visible\');">';
|
|
thumb += '<p style="font-size:0.9em;line-height:1.2em;cursor:pointer;">Show<br />NSFW</p>';
|
|
thumb += '<img src="' + thumbnail + '" class="thumbnail" style="visibility:hidden;" />';
|
|
thumb += '</span>';
|
|
} else {
|
|
thumb = '<img src="' + thumbnail + '" class="thumbnail" />';
|
|
}
|
|
}
|
|
|
|
// Build the template string up
|
|
var tmpl = '<div>' + thumb + '<b><%- title %></b><br />Posted by <%- author %>. ';
|
|
tmpl += '<i class="fa fa-arrow-up"></i> <%- ups %> <i class="fa fa-arrow-down"></i> <%- downs %><br />';
|
|
tmpl += '<%- num_comments %> comments made. <a href="https://www.reddit.com<%- permalink %>">View post</a></div>';
|
|
|
|
that.$content.find('.content').html(_.template(tmpl)(post));
|
|
});
|
|
|
|
return $('<div>' + _kiwi.global.i18n.translate('client_views_mediamessage_load_reddit').fetch() + '...</div>');
|
|
},
|
|
|
|
|
|
youtube: function () {
|
|
var ytid = this.$el.data('ytid');
|
|
var that = this;
|
|
var yt_html = '<iframe width="480" height="270" src="https://www.youtube.com/embed/'+ ytid +'?feature=oembed" frameborder="0" allowfullscreen=""></iframe>';
|
|
that.$content.find('.content').html(yt_html);
|
|
|
|
return $('');
|
|
},
|
|
|
|
|
|
gist: function () {
|
|
var that = this,
|
|
matches = (/https?:\/\/gist\.github\.com\/(?:[a-z0-9-]*\/)?([a-z0-9]+)(\#(.+))?$/i).exec(this.url);
|
|
|
|
$.getJSON('https://gist.github.com/'+matches[1]+'.json?callback=?' + (matches[2] || ''), function (data) {
|
|
$('body').append('<link rel="stylesheet" href="' + data.stylesheet + '" type="text/css" />');
|
|
that.$content.find('.content').html(data.div);
|
|
});
|
|
|
|
return $('<div>' + _kiwi.global.i18n.translate('client_views_mediamessage_load_gist').fetch() + '...</div>');
|
|
},
|
|
|
|
spotify: function () {
|
|
var uri = this.$el.data('uri'),
|
|
method = this.$el.data('method'),
|
|
spot, html;
|
|
|
|
switch (method) {
|
|
case "track":
|
|
case "album":
|
|
spot = {
|
|
url: 'https://embed.spotify.com/?uri=' + uri,
|
|
width: 300,
|
|
height: 80
|
|
};
|
|
break;
|
|
case "artist":
|
|
spot = {
|
|
url: 'https://embed.spotify.com/follow/1/?uri=' + uri +'&size=detail&theme=dark',
|
|
width: 300,
|
|
height: 56
|
|
};
|
|
break;
|
|
}
|
|
|
|
html = '<iframe src="' + spot.url + '" width="' + spot.width + '" height="' + spot.height + '" frameborder="0" allowtransparency="true"></iframe>';
|
|
|
|
return $(html);
|
|
},
|
|
|
|
soundcloud: function () {
|
|
var url = this.$el.data('url'),
|
|
$content = $('<div></div>').text(_kiwi.global.i18n.translate('client_models_applet_loading').fetch());
|
|
|
|
$.getJSON('https://soundcloud.com/oembed', { url: url })
|
|
.then(function (data) {
|
|
$content.empty().append(
|
|
$(data.html).attr('height', data.height - 100)
|
|
);
|
|
}, function () {
|
|
$content.text(_kiwi.global.i18n.translate('client_views_mediamessage_notfound').fetch());
|
|
});
|
|
|
|
return $content;
|
|
},
|
|
|
|
streamable: function () {
|
|
var that = this;
|
|
$.getJSON('http://api.streamable.com/oembed.json?url=' + this.$el.data('url') + '&maxwidth=300', function (data) {
|
|
that.$content.find('.content').html(data.html);
|
|
}).fail(function () {
|
|
that.$content.find('.content').text(_kiwi.global.i18n.translate('client_views_mediamessage_notfound').fetch());
|
|
});
|
|
return $('<div>' + _kiwi.global.i18n.translate('client_models_applet_loading').fetch() + '</div>');
|
|
},
|
|
|
|
custom: function() {
|
|
var type = this.constructor.types[this.$el.data('index')];
|
|
|
|
if (!type)
|
|
return;
|
|
|
|
return $(type.buildHtml(this.$el.data('url')));
|
|
}
|
|
|
|
}
|
|
}, {
|
|
|
|
/**
|
|
* Add a media message type to append HTML after a matching URL
|
|
* match() should return a truthy value if it wants to handle this URL
|
|
* buildHtml() should return the HTML string to be used within the drop down
|
|
*/
|
|
addType: function(match, buildHtml) {
|
|
if (typeof match !== 'function' || typeof buildHtml !== 'function')
|
|
return;
|
|
|
|
this.types = this.types || [];
|
|
this.types.push({match: match, buildHtml: buildHtml});
|
|
},
|
|
|
|
|
|
// Build the closed media HTML from a URL
|
|
buildHtml: function (url) {
|
|
var html = '', matches;
|
|
|
|
_.each(this.types || [], function(type, type_idx) {
|
|
if (!type.match(url))
|
|
return;
|
|
|
|
// Add which media type should handle this media message. Will be read when it's clicked on
|
|
html += '<span class="media" title="Open" data-type="custom" data-index="'+type_idx+'" data-url="' + _.escape(url) + '"><a class="open"><i class="fa fa-chevron-right"></i></a></span>';
|
|
});
|
|
|
|
// Is it an image?
|
|
if (url.match(/(\.jpe?g|\.gif|\.bmp|\.png)\??$/i)) {
|
|
html += '<span class="media image" data-type="image" data-url="' + _.escape(url) + '" title="Open Image"><a class="open"><i class="fa fa-chevron-right"></i></a></span>';
|
|
}
|
|
|
|
// Is this an imgur link not picked up by the images regex? Only need the image ID.
|
|
matches = (/imgur.com\/((?:.[^\/]+)|(?:a\/.+)|(?:.*\/(.+)))/gi).exec(url);
|
|
if (matches && !url.match(/(\.jpe?g|\.gif|\.bmp|\.png)\??$/i)) {
|
|
html += '<span class="media imgur" data-type="imgur" data-id="' + matches[(matches[2]?2:1)] + '" title="Open Image"><a class="open"><i class="fa fa-chevron-right"></i></a></span>';
|
|
}
|
|
|
|
// Is it a tweet?
|
|
matches = (/https?:\/\/twitter.com\/([a-zA-Z0-9_]+)\/status\/([0-9]+)/ig).exec(url);
|
|
if (matches) {
|
|
html += '<span class="media twitter" data-type="twitter" data-url="' + url + '" data-tweetid="' + matches[2] + '" title="Show tweet information"><a class="open"><i class="fa fa-chevron-right"></i></a></span>';
|
|
}
|
|
|
|
// Is reddit?
|
|
matches = (/reddit\.com\/r\/([a-zA-Z0-9_\-]+)\/comments\/([a-z0-9]+)\/([^\/]+)?/gi).exec(url);
|
|
if (matches) {
|
|
html += '<span class="media reddit" data-type="reddit" data-url="' + url + '" title="Reddit thread"><a class="open"><i class="fa fa-chevron-right"></i></a></span>';
|
|
}
|
|
|
|
// Is youtube?
|
|
matches = (/(?:youtube\.com\/(?:[^\/]+\/.+\/|(?:v|e(?:mbed)?)\/|.*[?&]v=)|youtu\.be\/)([^"&?\/ ]{11})/gi).exec(url);
|
|
if (matches) {
|
|
html += '<span class="media youtube" data-type="youtube" data-url="' + url + '" data-ytid="' + matches[1] + '" title="YouTube Video"><a class="open"><i class="fa fa-chevron-right"></i></a></span>';
|
|
}
|
|
|
|
// Is a github gist?
|
|
matches = (/https?:\/\/gist\.github\.com\/(?:[a-z0-9-]*\/)?([a-z0-9]+)(\#(.+))?$/i).exec(url);
|
|
if (matches) {
|
|
html += '<span class="media gist" data-type="gist" data-url="' + url + '" data-gist_id="' + matches[1] + '" title="GitHub Gist"><a class="open"><i class="fa fa-chevron-right"></i></a></span>';
|
|
}
|
|
|
|
// Is this a spotify link?
|
|
matches = (/https?:\/\/(?:play|open\.)?spotify.com\/(album|track|artist)\/([a-zA-Z0-9]+)\/?/i).exec(url);
|
|
if (matches) {
|
|
// Make it a Spotify URI! (spotify:<type>:<id>)
|
|
var method = matches[1],
|
|
uri = "spotify:" + matches[1] + ":" + matches[2];
|
|
html += '<span class="media spotify" data-type="spotify" data-uri="' + uri + '" data-method="' + method + '" title="Spotify ' + method + '"><a class="open"><i class="fa fa-chevron-right"></i></a></span>';
|
|
}
|
|
|
|
matches = (/(?:m\.)?(soundcloud\.com(?:\/.+))/i).exec(url);
|
|
if (matches) {
|
|
html += '<span class="media soundcloud" data-type="soundcloud" data-url="http://' + matches[1] + '" title="SoundCloud player"><a class="open"><i class="fa fa-chevron-right"></i></a></span>';
|
|
}
|
|
|
|
// Is this a streamable link?
|
|
matches = (/https?:\/\/streamable.com\/[a-z0-9]+$/i).exec(url);
|
|
if (matches) {
|
|
html += '<span class="media streamable" data-type="streamable" data-url="' + url +'" title="Streamable"><a class="open"><i class="fa fa-chevron-right"></i></a></span>';
|
|
}
|
|
|
|
return html;
|
|
}
|
|
});
|
|
|
|
|
|
|
|
_kiwi.view.Member = Backbone.View.extend({
|
|
tagName: "li",
|
|
initialize: function (options) {
|
|
this.model.bind('change', this.render, this);
|
|
this.render();
|
|
},
|
|
render: function () {
|
|
var $this = this.$el,
|
|
prefix_css_class = (this.model.get('modes') || []).join(' ');
|
|
|
|
$this.attr('class', 'mode ' + prefix_css_class);
|
|
$this.html('<a class="nick"><span class="prefix">' + this.model.get("prefix") + '</span>' + this.model.get("nick") + '</a>');
|
|
|
|
return this;
|
|
}
|
|
});
|
|
|
|
|
|
_kiwi.view.MemberList = Backbone.View.extend({
|
|
tagName: "div",
|
|
events: {
|
|
"click .nick": "nickClick",
|
|
"contextmenu .nick": "nickClick",
|
|
"dblclick .nick": "nickClick",
|
|
"click .channel_info": "channelInfoClick"
|
|
},
|
|
|
|
initialize: function (options) {
|
|
this.model.bind('all', this.render, this);
|
|
this.$el.appendTo('#kiwi .memberlists');
|
|
|
|
// Holds meta data. User counts, etc
|
|
this.$meta = $('<div class="meta"></div>').appendTo(this.$el);
|
|
|
|
// The list for holding the nicks
|
|
this.$list = $('<ul></ul>').appendTo(this.$el);
|
|
},
|
|
render: function () {
|
|
var that = this;
|
|
|
|
this.$list.empty();
|
|
this.model.forEach(function (member) {
|
|
member.view.$el.data('member', member);
|
|
that.$list.append(member.view.$el);
|
|
});
|
|
|
|
// User count
|
|
if(this.model.channel.isActive()) {
|
|
this.renderMeta();
|
|
}
|
|
|
|
return this;
|
|
},
|
|
|
|
renderMeta: function() {
|
|
var members_count = this.model.length + ' ' + translateText('client_applets_chanlist_users');
|
|
this.$meta.text(members_count);
|
|
},
|
|
|
|
nickClick: function (event) {
|
|
var $target = $(event.currentTarget).parent('li'),
|
|
member = $target.data('member');
|
|
|
|
_kiwi.global.events.emit('nick:select', {
|
|
target: $target,
|
|
member: member,
|
|
network: this.model.channel.get('network'),
|
|
source: 'nicklist',
|
|
$event: event
|
|
})
|
|
.then(_.bind(this.openUserMenuForItem, this, $target));
|
|
},
|
|
|
|
|
|
// Open a user menu for the given userlist item (<li>)
|
|
openUserMenuForItem: function($target) {
|
|
var member = $target.data('member'),
|
|
userbox,
|
|
are_we_an_op = !!this.model.getByNick(_kiwi.app.connections.active_connection.get('nick')).get('is_op');
|
|
|
|
userbox = new _kiwi.view.UserBox();
|
|
userbox.setTargets(member, this.model.channel);
|
|
userbox.displayOpItems(are_we_an_op);
|
|
|
|
var menu = new _kiwi.view.MenuBox(member.get('nick') || 'User');
|
|
menu.addItem('userbox', userbox.$el);
|
|
menu.showFooter(false);
|
|
|
|
_kiwi.global.events.emit('usermenu:created', {menu: menu, userbox: userbox, user: member})
|
|
.then(_.bind(function() {
|
|
menu.show();
|
|
|
|
var target_offset = $target.offset(),
|
|
t = target_offset.top,
|
|
m_bottom = t + menu.$el.outerHeight(), // Where the bottom of menu will be
|
|
memberlist_bottom = this.$el.parent().offset().top + this.$el.parent().outerHeight(),
|
|
l = target_offset.left,
|
|
m_right = l + menu.$el.outerWidth(), // Where the left of menu will be
|
|
memberlist_right = this.$el.parent().offset().left + this.$el.parent().outerWidth();
|
|
|
|
// If the bottom of the userbox is going to be too low.. raise it
|
|
if (m_bottom > memberlist_bottom){
|
|
t = memberlist_bottom - menu.$el.outerHeight();
|
|
}
|
|
|
|
// If the top of the userbox is going to be too high.. lower it
|
|
if (t < 0){
|
|
t = 0;
|
|
}
|
|
|
|
// If the right of the userbox is going off screen.. bring it in
|
|
if (m_right > memberlist_right){
|
|
l = memberlist_right - menu.$el.outerWidth();
|
|
}
|
|
|
|
// Set the new positon
|
|
menu.$el.offset({
|
|
left: l,
|
|
top: t
|
|
});
|
|
|
|
}, this))
|
|
.then(null, _.bind(function() {
|
|
userbox = null;
|
|
|
|
menu.dispose();
|
|
menu = null;
|
|
}, this));
|
|
},
|
|
|
|
|
|
channelInfoClick: function(event) {
|
|
new _kiwi.model.ChannelInfo({channel: this.model.channel});
|
|
},
|
|
|
|
|
|
show: function () {
|
|
$('#kiwi .memberlists').children().removeClass('active');
|
|
$(this.el).addClass('active');
|
|
|
|
this.renderMeta();
|
|
}
|
|
});
|
|
|
|
|
|
_kiwi.view.MenuBox = Backbone.View.extend({
|
|
events: {
|
|
'click .ui_menu_foot .close, a.close_menu': 'dispose'
|
|
},
|
|
|
|
initialize: function(title) {
|
|
var that = this;
|
|
|
|
this.$el = $('<div class="ui_menu"><div class="items"></div></div>');
|
|
|
|
this._title = title || '';
|
|
this._items = {};
|
|
this._display_footer = true;
|
|
this._close_on_blur = true;
|
|
},
|
|
|
|
|
|
render: function() {
|
|
var that = this,
|
|
$title,
|
|
$items = that.$el.find('.items');
|
|
|
|
$items.find('*').remove();
|
|
|
|
if (this._title) {
|
|
$title = $('<div class="ui_menu_title"></div>')
|
|
.text(this._title);
|
|
|
|
this.$el.prepend($title);
|
|
}
|
|
|
|
_.each(this._items, function(item) {
|
|
var $item = $('<div class="ui_menu_content hover"></div>')
|
|
.append(item);
|
|
|
|
$items.append($item);
|
|
});
|
|
|
|
if (this._display_footer)
|
|
this.$el.append('<div class="ui_menu_foot"><a class="close" onclick="">Close <i class="fa fa-times"></i></a></div>');
|
|
|
|
},
|
|
|
|
|
|
setTitle: function(new_title) {
|
|
this._title = new_title;
|
|
|
|
if (!this._title)
|
|
return;
|
|
|
|
this.$el.find('.ui_menu_title').text(this._title);
|
|
},
|
|
|
|
|
|
onDocumentClick: function(event) {
|
|
var $target = $(event.target);
|
|
|
|
if (!this._close_on_blur)
|
|
return;
|
|
|
|
// If this is not itself AND we don't contain this element, dispose $el
|
|
if ($target[0] != this.$el[0] && this.$el.has($target).length === 0)
|
|
this.dispose();
|
|
},
|
|
|
|
|
|
dispose: function() {
|
|
_.each(this._items, function(item) {
|
|
item.dispose && item.dispose();
|
|
item.remove && item.remove();
|
|
});
|
|
|
|
this._items = null;
|
|
this.remove();
|
|
|
|
if (this._close_proxy)
|
|
$(document).off('click', this._close_proxy);
|
|
},
|
|
|
|
|
|
addItem: function(item_name, $item) {
|
|
if ($item.is('a')) $item.addClass('fa fa-chevron-right');
|
|
this._items[item_name] = $item;
|
|
},
|
|
|
|
|
|
removeItem: function(item_name) {
|
|
delete this._items[item_name];
|
|
},
|
|
|
|
|
|
showFooter: function(show) {
|
|
this._display_footer = show;
|
|
},
|
|
|
|
|
|
closeOnBlur: function(close_it) {
|
|
this._close_on_blur = close_it;
|
|
},
|
|
|
|
|
|
show: function() {
|
|
var that = this,
|
|
$controlbox, menu_height;
|
|
|
|
this.render();
|
|
this.$el.appendTo(_kiwi.app.view.$el);
|
|
|
|
// Ensure the menu doesn't get too tall to overlap the input bar at the bottom
|
|
$controlbox = _kiwi.app.view.$el.find('.controlbox');
|
|
$items = this.$el.find('.items');
|
|
menu_height = this.$el.outerHeight() - $items.outerHeight();
|
|
|
|
$items.css({
|
|
'overflow-y': 'auto',
|
|
'max-height': $controlbox.offset().top - this.$el.offset().top - menu_height
|
|
});
|
|
|
|
// We add this document click listener on the next javascript tick.
|
|
// If the current tick is handling an existing click event (such as the nicklist click handler),
|
|
// the click event bubbles up and hits the document therefore calling this callback to
|
|
// remove this menubox before it's even shown.
|
|
setTimeout(function() {
|
|
that._close_proxy = function(event) {
|
|
that.onDocumentClick(event);
|
|
};
|
|
$(document).on('click', that._close_proxy);
|
|
}, 0);
|
|
}
|
|
});
|
|
|
|
|
|
|
|
// Model for this = _kiwi.model.NetworkPanelList
|
|
_kiwi.view.NetworkTabs = Backbone.View.extend({
|
|
tagName: 'ul',
|
|
className: 'connections',
|
|
|
|
initialize: function() {
|
|
this.model.on('add', this.networkAdded, this);
|
|
this.model.on('remove', this.networkRemoved, this);
|
|
|
|
this.$el.appendTo(_kiwi.app.view.$el.find('.tabs'));
|
|
},
|
|
|
|
networkAdded: function(network) {
|
|
$('<li class="connection"></li>')
|
|
.append(network.panels.view.$el)
|
|
.appendTo(this.$el);
|
|
},
|
|
|
|
networkRemoved: function(network) {
|
|
// Remove the containing list element
|
|
network.panels.view.$el.parent().remove();
|
|
|
|
network.panels.view.remove();
|
|
|
|
_kiwi.app.view.doLayout();
|
|
}
|
|
});
|
|
|
|
|
|
_kiwi.view.NickChangeBox = Backbone.View.extend({
|
|
events: {
|
|
'submit': 'changeNick',
|
|
'click .cancel': 'close'
|
|
},
|
|
|
|
initialize: function () {
|
|
var text = {
|
|
new_nick: _kiwi.global.i18n.translate('client_views_nickchangebox_new').fetch(),
|
|
change: _kiwi.global.i18n.translate('client_views_nickchangebox_change').fetch(),
|
|
cancel: _kiwi.global.i18n.translate('client_views_nickchangebox_cancel').fetch()
|
|
};
|
|
this.$el = $(_.template($('#tmpl_nickchange').html().trim())(text));
|
|
},
|
|
|
|
render: function () {
|
|
// Add the UI component and give it focus
|
|
_kiwi.app.controlbox.$el.prepend(this.$el);
|
|
this.$el.find('input').focus();
|
|
|
|
this.$el.css('bottom', _kiwi.app.controlbox.$el.outerHeight(true));
|
|
},
|
|
|
|
close: function () {
|
|
this.$el.remove();
|
|
this.trigger('close');
|
|
},
|
|
|
|
changeNick: function (event) {
|
|
event.preventDefault();
|
|
|
|
var connection = _kiwi.app.connections.active_connection;
|
|
this.listenTo(connection, 'change:nick', function() {
|
|
this.close();
|
|
});
|
|
|
|
connection.gateway.changeNick(this.$('input').val());
|
|
}
|
|
});
|
|
|
|
|
|
_kiwi.view.ResizeHandler = Backbone.View.extend({
|
|
events: {
|
|
'mousedown': 'startDrag',
|
|
'mouseup': 'stopDrag'
|
|
},
|
|
|
|
initialize: function () {
|
|
this.dragging = false;
|
|
this.starting_width = {};
|
|
|
|
$(window).on('mousemove', $.proxy(this.onDrag, this));
|
|
},
|
|
|
|
startDrag: function (event) {
|
|
this.dragging = true;
|
|
},
|
|
|
|
stopDrag: function (event) {
|
|
this.dragging = false;
|
|
},
|
|
|
|
onDrag: function (event) {
|
|
if (!this.dragging) return;
|
|
|
|
var offset = $('#kiwi').offset().left;
|
|
|
|
this.$el.css('left', event.clientX - (this.$el.outerWidth(true) / 2) - offset);
|
|
$('#kiwi .right_bar').css('width', this.$el.parent().width() - (this.$el.position().left + this.$el.outerWidth()));
|
|
_kiwi.app.view.doLayout();
|
|
}
|
|
});
|
|
|
|
|
|
_kiwi.view.ServerSelect = Backbone.View.extend({
|
|
events: {
|
|
'submit form': 'submitForm',
|
|
'click .show_more': 'showMore',
|
|
'change .have_pass input': 'showPass',
|
|
'change .have_key input': 'showKey',
|
|
'click .fa-key': 'channelKeyIconClick',
|
|
'click .show_server': 'showServer'
|
|
},
|
|
|
|
initialize: function () {
|
|
var that = this,
|
|
text = {
|
|
think_nick: _kiwi.global.i18n.translate('client_views_serverselect_form_title').fetch(),
|
|
nickname: _kiwi.global.i18n.translate('client_views_serverselect_nickname').fetch(),
|
|
have_password: _kiwi.global.i18n.translate('client_views_serverselect_enable_password').fetch(),
|
|
password: _kiwi.global.i18n.translate('client_views_serverselect_password').fetch(),
|
|
channel: _kiwi.global.i18n.translate('client_views_serverselect_channel').fetch(),
|
|
channel_key: _kiwi.global.i18n.translate('client_views_serverselect_channelkey').fetch(),
|
|
require_key: _kiwi.global.i18n.translate('client_views_serverselect_channelkey_required').fetch(),
|
|
key: _kiwi.global.i18n.translate('client_views_serverselect_key').fetch(),
|
|
start: _kiwi.global.i18n.translate('client_views_serverselect_connection_start').fetch(),
|
|
server_network: _kiwi.global.i18n.translate('client_views_serverselect_server_and_network').fetch(),
|
|
server: _kiwi.global.i18n.translate('client_views_serverselect_server').fetch(),
|
|
port: _kiwi.global.i18n.translate('client_views_serverselect_port').fetch(),
|
|
powered_by: _kiwi.global.i18n.translate('client_views_serverselect_poweredby').fetch()
|
|
};
|
|
|
|
this.$el = $(_.template($('#tmpl_server_select').html().trim())(text));
|
|
|
|
// Remove the 'more' link if the server has disabled server changing
|
|
if (_kiwi.app.server_settings && _kiwi.app.server_settings.connection) {
|
|
if (!_kiwi.app.server_settings.connection.allow_change) {
|
|
this.$el.find('.show_more').remove();
|
|
this.$el.addClass('single_server');
|
|
}
|
|
}
|
|
|
|
// Are currently showing all the controlls or just a nick_change box?
|
|
this.state = 'all';
|
|
|
|
this.more_shown = false;
|
|
|
|
this.model.bind('new_network', this.newNetwork, this);
|
|
|
|
this.gateway = _kiwi.global.components.Network();
|
|
this.gateway.on('connect', this.networkConnected, this);
|
|
this.gateway.on('connecting', this.networkConnecting, this);
|
|
this.gateway.on('disconnect', this.networkDisconnected, this);
|
|
this.gateway.on('irc_error', this.onIrcError, this);
|
|
},
|
|
|
|
dispose: function() {
|
|
this.model.off('new_network', this.newNetwork, this);
|
|
this.gateway.off();
|
|
|
|
this.remove();
|
|
},
|
|
|
|
submitForm: function (event) {
|
|
event.preventDefault();
|
|
|
|
// Make sure a nick is chosen
|
|
if (!$('input.nick', this.$el).val().trim()) {
|
|
this.setStatus(_kiwi.global.i18n.translate('client_views_serverselect_nickname_error_empty').fetch());
|
|
$('input.nick', this.$el).select();
|
|
return;
|
|
}
|
|
|
|
if (this.state === 'nick_change') {
|
|
this.submitNickChange(event);
|
|
} else {
|
|
this.submitLogin(event);
|
|
}
|
|
|
|
$('button', this.$el).attr('disabled', 1);
|
|
return;
|
|
},
|
|
|
|
submitLogin: function (event) {
|
|
// If submitting is disabled, don't do anything
|
|
if ($('button', this.$el).attr('disabled')) return;
|
|
|
|
var values = {
|
|
nick: $('input.nick', this.$el).val(),
|
|
server: $('input.server', this.$el).val(),
|
|
port: $('input.port', this.$el).val(),
|
|
ssl: $('input.ssl', this.$el).prop('checked'),
|
|
password: $('input.password', this.$el).val(),
|
|
channel: $('input.channel', this.$el).val(),
|
|
channel_key: $('input.channel_key', this.$el).val(),
|
|
options: this.server_options
|
|
};
|
|
|
|
this.trigger('server_connect', values);
|
|
},
|
|
|
|
submitNickChange: function (event) {
|
|
_kiwi.gateway.changeNick(null, $('input.nick', this.$el).val());
|
|
this.networkConnecting();
|
|
},
|
|
|
|
showPass: function (event) {
|
|
if (this.$el.find('tr.have_pass input').is(':checked')) {
|
|
this.$el.find('tr.pass').show().find('input').focus();
|
|
} else {
|
|
this.$el.find('tr.pass').hide().find('input').val('');
|
|
}
|
|
},
|
|
|
|
channelKeyIconClick: function (event) {
|
|
this.$el.find('tr.have_key input').click();
|
|
},
|
|
|
|
showKey: function (event) {
|
|
if (this.$el.find('tr.have_key input').is(':checked')) {
|
|
this.$el.find('tr.key').show().find('input').focus();
|
|
} else {
|
|
this.$el.find('tr.key').hide().find('input').val('');
|
|
}
|
|
},
|
|
|
|
showMore: function (event) {
|
|
if (!this.more_shown) {
|
|
$('.more', this.$el).slideDown('fast');
|
|
$('.show_more', this.$el)
|
|
.children('.fa-caret-down')
|
|
.removeClass('fa-caret-down')
|
|
.addClass('fa-caret-up');
|
|
$('input.server', this.$el).select();
|
|
this.more_shown = true;
|
|
} else {
|
|
$('.more', this.$el).slideUp('fast');
|
|
$('.show_more', this.$el)
|
|
.children('.fs-caret-up')
|
|
.removeClass('fa-caret-up')
|
|
.addClass('fa-caret-down');
|
|
$('input.nick', this.$el).select();
|
|
this.more_shown = false;
|
|
}
|
|
},
|
|
|
|
populateFields: function (defaults) {
|
|
var nick, server, port, channel, channel_key, ssl, password;
|
|
|
|
defaults = defaults || {};
|
|
|
|
nick = defaults.nick || '';
|
|
server = defaults.server || '';
|
|
port = defaults.port || 6667;
|
|
ssl = defaults.ssl || 0;
|
|
password = defaults.password || '';
|
|
channel = defaults.channel || '';
|
|
channel_key = defaults.channel_key || '';
|
|
|
|
$('input.nick', this.$el).val(nick);
|
|
$('input.server', this.$el).val(server);
|
|
$('input.port', this.$el).val(port);
|
|
$('input.ssl', this.$el).prop('checked', ssl);
|
|
$('input#server_select_show_pass', this.$el).prop('checked', !(!password));
|
|
$('input.password', this.$el).val(password);
|
|
if (!(!password)) {
|
|
$('tr.pass', this.$el).show();
|
|
}
|
|
$('input.channel', this.$el).val(channel);
|
|
$('input#server_select_show_channel_key', this.$el).prop('checked', !(!channel_key));
|
|
$('input.channel_key', this.$el).val(channel_key);
|
|
if (!(!channel_key)) {
|
|
$('tr.key', this.$el).show();
|
|
}
|
|
|
|
// Temporary values
|
|
this.server_options = {};
|
|
|
|
if (defaults.encoding)
|
|
this.server_options.encoding = defaults.encoding;
|
|
},
|
|
|
|
hide: function () {
|
|
this.$el.slideUp();
|
|
},
|
|
|
|
show: function (new_state) {
|
|
new_state = new_state || 'all';
|
|
|
|
this.$el.show();
|
|
|
|
if (new_state === 'all') {
|
|
$('.show_more', this.$el).show();
|
|
|
|
} else if (new_state === 'more') {
|
|
$('.more', this.$el).slideDown('fast');
|
|
|
|
} else if (new_state === 'nick_change') {
|
|
$('.more', this.$el).hide();
|
|
$('.show_more', this.$el).hide();
|
|
$('input.nick', this.$el).select();
|
|
|
|
} else if (new_state === 'enter_password') {
|
|
$('.more', this.$el).hide();
|
|
$('.show_more', this.$el).hide();
|
|
$('input.password', this.$el).select();
|
|
}
|
|
|
|
this.state = new_state;
|
|
},
|
|
|
|
infoBoxShow: function() {
|
|
var $side_panel = this.$el.find('.side_panel');
|
|
|
|
// Some theme may hide the info panel so check before we
|
|
// resize ourselves
|
|
if (!$side_panel.is(':visible'))
|
|
return;
|
|
|
|
this.$el.animate({
|
|
width: parseInt($side_panel.css('left'), 10) + $side_panel.find('.content:first').outerWidth()
|
|
});
|
|
},
|
|
|
|
infoBoxHide: function() {
|
|
var $side_panel = this.$el.find('.side_panel');
|
|
this.$el.animate({
|
|
width: parseInt($side_panel.css('left'), 10)
|
|
});
|
|
},
|
|
|
|
infoBoxSet: function($info_view) {
|
|
this.$el.find('.side_panel .content')
|
|
.empty()
|
|
.append($info_view);
|
|
},
|
|
|
|
setStatus: function (text, class_name) {
|
|
$('.status', this.$el)
|
|
.text(text)
|
|
.attr('class', 'status')
|
|
.addClass(class_name||'')
|
|
.show();
|
|
},
|
|
clearStatus: function () {
|
|
$('.status', this.$el).hide();
|
|
},
|
|
|
|
reset: function() {
|
|
this.populateFields();
|
|
this.clearStatus();
|
|
|
|
this.$('button').attr('disabled', null);
|
|
},
|
|
|
|
newNetwork: function(network) {
|
|
// Keep a reference to this network so we can interact with it
|
|
this.model.current_connecting_network = network;
|
|
},
|
|
|
|
networkConnected: function (event) {
|
|
this.model.trigger('connected', _kiwi.app.connections.getByConnectionId(event.server));
|
|
this.model.current_connecting_network = null;
|
|
},
|
|
|
|
networkDisconnected: function () {
|
|
this.model.current_connecting_network = null;
|
|
this.state = 'all';
|
|
},
|
|
|
|
networkConnecting: function (event) {
|
|
this.model.trigger('connecting');
|
|
this.setStatus(_kiwi.global.i18n.translate('client_views_serverselect_connection_trying').fetch(), 'ok');
|
|
|
|
this.$('.status').append('<a class="show_server"><i class="fa fa-info-circle"></i></a>');
|
|
},
|
|
|
|
showServer: function() {
|
|
// If we don't have a current connection in the making then we have nothing to show
|
|
if (!this.model.current_connecting_network)
|
|
return;
|
|
|
|
_kiwi.app.view.barsShow();
|
|
this.model.current_connecting_network.panels.server.view.show();
|
|
},
|
|
|
|
onIrcError: function (data) {
|
|
$('button', this.$el).attr('disabled', null);
|
|
|
|
switch(data.error) {
|
|
case 'nickname_in_use':
|
|
this.setStatus(_kiwi.global.i18n.translate('client_views_serverselect_nickname_error_alreadyinuse').fetch());
|
|
this.show('nick_change');
|
|
this.$el.find('.nick').select();
|
|
break;
|
|
case 'erroneus_nickname':
|
|
if (data.reason) {
|
|
this.setStatus(data.reason);
|
|
} else {
|
|
this.setStatus(_kiwi.global.i18n.translate('client_views_serverselect_nickname_invalid').fetch());
|
|
}
|
|
this.show('nick_change');
|
|
this.$el.find('.nick').select();
|
|
break;
|
|
case 'password_mismatch':
|
|
this.setStatus(_kiwi.global.i18n.translate('client_views_serverselect_password_incorrect').fetch());
|
|
this.show('enter_password');
|
|
this.$el.find('.password').select();
|
|
break;
|
|
default:
|
|
this.showError(data.reason || '');
|
|
break;
|
|
}
|
|
},
|
|
|
|
showError: function (error_reason) {
|
|
var err_text = _kiwi.global.i18n.translate('client_views_serverselect_connection_error').fetch();
|
|
|
|
if (error_reason) {
|
|
switch (error_reason) {
|
|
case 'ENOTFOUND':
|
|
err_text = _kiwi.global.i18n.translate('client_views_serverselect_server_notfound').fetch();
|
|
break;
|
|
|
|
case 'ECONNREFUSED':
|
|
err_text += ' (' + _kiwi.global.i18n.translate('client_views_serverselect_connection_refused').fetch() + ')';
|
|
break;
|
|
|
|
default:
|
|
err_text += ' (' + error_reason + ')';
|
|
}
|
|
}
|
|
|
|
this.setStatus(err_text, 'error');
|
|
$('button', this.$el).attr('disabled', null);
|
|
this.show();
|
|
}
|
|
});
|
|
|
|
|
|
_kiwi.view.StatusMessage = Backbone.View.extend({
|
|
initialize: function () {
|
|
this.$el.hide();
|
|
|
|
// Timer for hiding the message after X seconds
|
|
this.tmr = null;
|
|
},
|
|
|
|
text: function (text, opt) {
|
|
// Defaults
|
|
opt = opt || {};
|
|
opt.type = opt.type || '';
|
|
opt.timeout = opt.timeout || 5000;
|
|
|
|
this.$el.text(text).addClass(opt.type);
|
|
this.$el.slideDown($.proxy(_kiwi.app.view.doLayout, _kiwi.app.view));
|
|
|
|
if (opt.timeout) this.doTimeout(opt.timeout);
|
|
},
|
|
|
|
html: function (html, opt) {
|
|
// Defaults
|
|
opt = opt || {};
|
|
opt.type = opt.type || '';
|
|
opt.timeout = opt.timeout || 5000;
|
|
|
|
this.$el.html(html).addClass(opt.type);
|
|
this.$el.slideDown($.proxy(_kiwi.app.view.doLayout, _kiwi.app.view));
|
|
|
|
if (opt.timeout) this.doTimeout(opt.timeout);
|
|
},
|
|
|
|
hide: function () {
|
|
this.$el.slideUp($.proxy(_kiwi.app.view.doLayout, _kiwi.app.view));
|
|
},
|
|
|
|
doTimeout: function (length) {
|
|
if (this.tmr) clearTimeout(this.tmr);
|
|
var that = this;
|
|
this.tmr = setTimeout(function () { that.hide(); }, length);
|
|
}
|
|
});
|
|
|
|
|
|
// Model for this = _kiwi.model.PanelList
|
|
_kiwi.view.Tabs = Backbone.View.extend({
|
|
tagName: 'ul',
|
|
className: 'panellist',
|
|
|
|
events: {
|
|
'click li': 'tabClick',
|
|
'click li .part': 'partClick'
|
|
},
|
|
|
|
initialize: function () {
|
|
this.model.on("add", this.panelAdded, this);
|
|
this.model.on("remove", this.panelRemoved, this);
|
|
this.model.on("reset", this.render, this);
|
|
|
|
this.model.on('active', this.panelActive, this);
|
|
|
|
// Network tabs start with a server, so determine what we are now
|
|
this.is_network = false;
|
|
|
|
if (this.model.network) {
|
|
this.is_network = true;
|
|
|
|
this.model.network.on('change:name', function (network, new_val) {
|
|
$('span', this.model.server.tab).text(new_val);
|
|
}, this);
|
|
|
|
this.model.network.on('change:connection_id', function (network, new_val) {
|
|
this.model.forEach(function(panel) {
|
|
panel.tab.data('connection_id', new_val);
|
|
});
|
|
}, this);
|
|
}
|
|
},
|
|
|
|
render: function () {
|
|
var that = this;
|
|
|
|
this.$el.empty();
|
|
|
|
if (this.is_network) {
|
|
// Add the server tab first
|
|
this.model.server.tab
|
|
.data('panel', this.model.server)
|
|
.data('connection_id', this.model.network.get('connection_id'))
|
|
.appendTo(this.$el);
|
|
}
|
|
|
|
// Go through each panel adding its tab
|
|
this.model.forEach(function (panel) {
|
|
// If this is the server panel, ignore as it's already added
|
|
if (this.is_network && panel == that.model.server)
|
|
return;
|
|
|
|
panel.tab.data('panel', panel);
|
|
|
|
if (this.is_network)
|
|
panel.tab.data('connection_id', this.model.network.get('connection_id'));
|
|
|
|
panel.tab.appendTo(that.$el);
|
|
});
|
|
|
|
_kiwi.app.view.doLayout();
|
|
},
|
|
|
|
updateTabTitle: function (panel, new_title) {
|
|
$('span', panel.tab).text(new_title);
|
|
},
|
|
|
|
panelAdded: function (panel) {
|
|
// Add a tab to the panel
|
|
panel.tab = $('<li><span></span><div class="activity"></div></li>');
|
|
panel.tab.find('span').text(panel.get('title') || panel.get('name'));
|
|
|
|
if (panel.isServer()) {
|
|
panel.tab.addClass('server');
|
|
panel.tab.addClass('fa');
|
|
panel.tab.addClass('fa-nonexistant');
|
|
} else if (panel.isChannel()) {
|
|
panel.tab.addClass('channel');
|
|
} else if (panel.isQuery()) {
|
|
panel.tab.addClass('query');
|
|
}
|
|
|
|
panel.tab.data('panel', panel);
|
|
|
|
if (this.is_network)
|
|
panel.tab.data('connection_id', this.model.network.get('connection_id'));
|
|
|
|
this.sortTabs();
|
|
|
|
panel.bind('change:title', this.updateTabTitle);
|
|
panel.bind('change:name', this.updateTabTitle);
|
|
|
|
_kiwi.app.view.doLayout();
|
|
},
|
|
panelRemoved: function (panel) {
|
|
var connection = _kiwi.app.connections.active_connection;
|
|
|
|
panel.tab.remove();
|
|
delete panel.tab;
|
|
|
|
_kiwi.app.panels.trigger('remove', panel);
|
|
|
|
_kiwi.app.view.doLayout();
|
|
},
|
|
|
|
panelActive: function (panel, previously_active_panel) {
|
|
// Remove any existing tabs or part images
|
|
_kiwi.app.view.$el.find('.panellist .part').remove();
|
|
_kiwi.app.view.$el.find('.panellist .active').removeClass('active');
|
|
|
|
panel.tab.addClass('active');
|
|
|
|
panel.tab.append('<span class="part fa fa-nonexistant"></span>');
|
|
},
|
|
|
|
tabClick: function (e) {
|
|
var tab = $(e.currentTarget);
|
|
|
|
var panel = tab.data('panel');
|
|
if (!panel) {
|
|
// A panel wasn't found for this tab... wadda fuck
|
|
return;
|
|
}
|
|
|
|
panel.view.show();
|
|
},
|
|
|
|
partClick: function (e) {
|
|
var tab = $(e.currentTarget).parent();
|
|
var panel = tab.data('panel');
|
|
|
|
if (!panel) return;
|
|
|
|
// If the nicklist is empty, we haven't joined the channel as yet
|
|
// If we part a server, then we need to disconnect from server, close channel tabs,
|
|
// close server tab, then bring client back to homepage
|
|
if (panel.isChannel() && panel.get('members').models.length > 0) {
|
|
this.model.network.gateway.part(panel.get('name'));
|
|
|
|
} else if(panel.isServer()) {
|
|
if (!this.model.network.get('connected') || confirm(translateText('disconnect_from_server'))) {
|
|
this.model.network.gateway.quit("Leaving");
|
|
_kiwi.app.connections.remove(this.model.network);
|
|
_kiwi.app.startup_applet.view.show();
|
|
}
|
|
|
|
} else {
|
|
panel.close();
|
|
}
|
|
},
|
|
|
|
sortTabs: function() {
|
|
var that = this,
|
|
panels = [];
|
|
|
|
this.model.forEach(function (panel) {
|
|
// Ignore the server tab, so all others get added after it
|
|
if (that.is_network && panel == that.model.server)
|
|
return;
|
|
|
|
panels.push([panel.get('title') || panel.get('name'), panel]);
|
|
});
|
|
|
|
// Sort by the panel name..
|
|
panels.sort(function(a, b) {
|
|
if (a[0].toLowerCase() > b[0].toLowerCase()) {
|
|
return 1;
|
|
} else if (a[0].toLowerCase() < b[0].toLowerCase()) {
|
|
return -1;
|
|
} else {
|
|
return 0;
|
|
}
|
|
});
|
|
|
|
// And add them all back in order.
|
|
_.each(panels, function(panel) {
|
|
panel[1].tab.appendTo(that.$el);
|
|
});
|
|
}
|
|
});
|
|
|
|
|
|
_kiwi.view.TopicBar = Backbone.View.extend({
|
|
events: {
|
|
'keydown div': 'process'
|
|
},
|
|
|
|
initialize: function () {
|
|
_kiwi.app.panels.bind('active', function (active_panel) {
|
|
// If it's a channel topic, update and make editable
|
|
if (active_panel.isChannel()) {
|
|
this.setCurrentTopicFromChannel(active_panel);
|
|
this.$el.find('div').attr('contentEditable', true);
|
|
|
|
} else {
|
|
// Not a channel topic.. clear and make uneditable
|
|
this.$el.find('div').attr('contentEditable', false)
|
|
.text('');
|
|
}
|
|
}, this);
|
|
},
|
|
|
|
process: function (ev) {
|
|
var inp = $(ev.currentTarget),
|
|
inp_val = inp.text();
|
|
|
|
// Only allow topic editing if this is a channel panel
|
|
if (!_kiwi.app.panels().active.isChannel()) {
|
|
return false;
|
|
}
|
|
|
|
// If hit return key, update the current topic
|
|
if (ev.keyCode === 13) {
|
|
_kiwi.app.connections.active_connection.gateway.topic(_kiwi.app.panels().active.get('name'), inp_val);
|
|
return false;
|
|
}
|
|
},
|
|
|
|
setCurrentTopic: function (new_topic) {
|
|
new_topic = new_topic || '';
|
|
|
|
// We only want a plain text version
|
|
$('div', this.$el).html(formatIRCMsg(_.escape(new_topic)));
|
|
},
|
|
|
|
setCurrentTopicFromChannel: function(channel) {
|
|
var set_by = channel.get('topic_set_by'),
|
|
set_by_text = '';
|
|
|
|
this.setCurrentTopic(channel.get("topic"));
|
|
|
|
if (set_by) {
|
|
set_by_text += translateText('client_models_network_topic', [set_by.nick, _kiwi.utils.formatDate(set_by.when)]);
|
|
this.$el.attr('title', set_by_text);
|
|
} else {
|
|
this.$el.attr('title', '');
|
|
}
|
|
}
|
|
});
|
|
|
|
|
|
_kiwi.view.UserBox = Backbone.View.extend({
|
|
events: {
|
|
'click .query': 'queryClick',
|
|
'click .info': 'infoClick',
|
|
'change .ignore': 'ignoreChange',
|
|
'click .ignore': 'ignoreClick',
|
|
'click .op': 'opClick',
|
|
'click .deop': 'deopClick',
|
|
'click .voice': 'voiceClick',
|
|
'click .devoice': 'devoiceClick',
|
|
'click .kick': 'kickClick',
|
|
'click .ban': 'banClick'
|
|
},
|
|
|
|
initialize: function () {
|
|
var text = {
|
|
op: _kiwi.global.i18n.translate('client_views_userbox_op').fetch(),
|
|
de_op: _kiwi.global.i18n.translate('client_views_userbox_deop').fetch(),
|
|
voice: _kiwi.global.i18n.translate('client_views_userbox_voice').fetch(),
|
|
de_voice: _kiwi.global.i18n.translate('client_views_userbox_devoice').fetch(),
|
|
kick: _kiwi.global.i18n.translate('client_views_userbox_kick').fetch(),
|
|
ban: _kiwi.global.i18n.translate('client_views_userbox_ban').fetch(),
|
|
message: _kiwi.global.i18n.translate('client_views_userbox_query').fetch(),
|
|
info: _kiwi.global.i18n.translate('client_views_userbox_whois').fetch(),
|
|
ignore: _kiwi.global.i18n.translate('client_views_userbox_ignore').fetch()
|
|
};
|
|
this.$el = $(_.template($('#tmpl_userbox').html().trim())(text));
|
|
},
|
|
|
|
setTargets: function (user, channel) {
|
|
this.user = user;
|
|
this.channel = channel;
|
|
|
|
var user_mask = toUserMask(this.user.get('nick')),
|
|
is_ignored = _kiwi.app.connections.active_connection.isUserIgnored(user_mask);
|
|
|
|
this.$('.ignore input').attr('checked', is_ignored ? 'checked' : false);
|
|
},
|
|
|
|
displayOpItems: function(display_items) {
|
|
if (display_items) {
|
|
this.$el.find('.if_op').css('display', 'block');
|
|
} else {
|
|
this.$el.find('.if_op').css('display', 'none');
|
|
}
|
|
},
|
|
|
|
queryClick: function (event) {
|
|
var nick = this.user.get('nick');
|
|
_kiwi.app.connections.active_connection.createQuery(nick);
|
|
},
|
|
|
|
infoClick: function (event) {
|
|
_kiwi.app.controlbox.processInput('/whois ' + this.user.get('nick'));
|
|
},
|
|
|
|
ignoreClick: function (event) {
|
|
// Stop the menubox from closing since it will not update the checkbox otherwise
|
|
event.stopPropagation();
|
|
},
|
|
|
|
ignoreChange: function (event) {
|
|
if ($(event.currentTarget).find('input').is(':checked')) {
|
|
_kiwi.app.controlbox.processInput('/ignore ' + this.user.get('nick'));
|
|
} else {
|
|
_kiwi.app.controlbox.processInput('/unignore ' + this.user.get('nick'));
|
|
}
|
|
},
|
|
|
|
opClick: function (event) {
|
|
_kiwi.app.controlbox.processInput('/mode ' + this.channel.get('name') + ' +o ' + this.user.get('nick'));
|
|
},
|
|
|
|
deopClick: function (event) {
|
|
_kiwi.app.controlbox.processInput('/mode ' + this.channel.get('name') + ' -o ' + this.user.get('nick'));
|
|
},
|
|
|
|
voiceClick: function (event) {
|
|
_kiwi.app.controlbox.processInput('/mode ' + this.channel.get('name') + ' +v ' + this.user.get('nick'));
|
|
},
|
|
|
|
devoiceClick: function (event) {
|
|
_kiwi.app.controlbox.processInput('/mode ' + this.channel.get('name') + ' -v ' + this.user.get('nick'));
|
|
},
|
|
|
|
kickClick: function (event) {
|
|
// TODO: Enable the use of a custom kick message
|
|
_kiwi.app.controlbox.processInput('/kick ' + this.user.get('nick') + ' Bye!');
|
|
},
|
|
|
|
banClick: function (event) {
|
|
// TODO: Set ban on host, not just on nick
|
|
_kiwi.app.controlbox.processInput('/mode ' + this.channel.get('name') + ' +b ' + this.user.get('nick') + '!*');
|
|
}
|
|
});
|
|
|
|
|
|
|
|
_kiwi.view.ChannelTools = Backbone.View.extend({
|
|
events: {
|
|
'click .channel_info': 'infoClick',
|
|
'click .channel_part': 'partClick'
|
|
},
|
|
|
|
initialize: function () {},
|
|
|
|
infoClick: function (event) {
|
|
new _kiwi.model.ChannelInfo({channel: _kiwi.app.panels().active});
|
|
},
|
|
|
|
partClick: function (event) {
|
|
_kiwi.app.connections.active_connection.gateway.part(_kiwi.app.panels().active.get('name'));
|
|
}
|
|
});
|
|
|
|
|
|
// var f = new _kiwi.model.ChannelInfo({channel: _kiwi.app.panels().active});
|
|
|
|
_kiwi.view.ChannelInfo = Backbone.View.extend({
|
|
events: {
|
|
'click .toggle_banlist': 'toggleBanList',
|
|
'change .channel-mode': 'onModeChange',
|
|
'click .remove-ban': 'onRemoveBanClick'
|
|
},
|
|
|
|
|
|
initialize: function () {
|
|
var that = this,
|
|
network,
|
|
channel = this.model.get('channel'),
|
|
text;
|
|
|
|
text = {
|
|
moderated_chat: translateText('client_views_channelinfo_moderated'),
|
|
invite_only: translateText('client_views_channelinfo_inviteonly'),
|
|
ops_change_topic: translateText('client_views_channelinfo_opschangechannel'),
|
|
external_messages: translateText('client_views_channelinfo_externalmessages'),
|
|
toggle_banlist: translateText('client_views_channelinfo_togglebanlist'),
|
|
channel_name: channel.get('name')
|
|
};
|
|
|
|
this.$el = $(_.template($('#tmpl_channel_info').html().trim())(text));
|
|
|
|
// Create the menu box this view will sit inside
|
|
this.menu = new _kiwi.view.MenuBox(channel.get('name'));
|
|
this.menu.addItem('channel_info', this.$el);
|
|
this.menu.$el.appendTo(channel.view.$container);
|
|
this.menu.show();
|
|
|
|
this.menu.$el.offset({top: _kiwi.app.view.$el.find('.panels').offset().top});
|
|
|
|
// Menu box will call this destroy on closing
|
|
this.$el.dispose = _.bind(this.dispose, this);
|
|
|
|
// Display the info we have, then listen for further changes
|
|
this.updateInfo(channel);
|
|
channel.on('change:info_modes change:info_url change:banlist', this.updateInfo, this);
|
|
|
|
// Request the latest info for ths channel from the network
|
|
channel.get('network').gateway.channelInfo(channel.get('name'));
|
|
},
|
|
|
|
|
|
render: function () {
|
|
},
|
|
|
|
|
|
onModeChange: function(event) {
|
|
var $this = $(event.currentTarget),
|
|
channel = this.model.get('channel'),
|
|
mode = $this.data('mode'),
|
|
mode_string = '';
|
|
|
|
if ($this.attr('type') == 'checkbox') {
|
|
mode_string = $this.is(':checked') ? '+' : '-';
|
|
mode_string += mode;
|
|
channel.setMode(mode_string);
|
|
|
|
return;
|
|
}
|
|
|
|
if ($this.attr('type') == 'text') {
|
|
mode_string = $this.val() ?
|
|
'+' + mode + ' ' + $this.val() :
|
|
'-' + mode;
|
|
|
|
channel.setMode(mode_string);
|
|
|
|
return;
|
|
}
|
|
},
|
|
|
|
|
|
onRemoveBanClick: function (event) {
|
|
event.preventDefault();
|
|
event.stopPropagation();
|
|
|
|
var $this = $(event.currentTarget),
|
|
$tr = $this.parents('tr:first'),
|
|
ban = $tr.data('ban');
|
|
|
|
if (!ban)
|
|
return;
|
|
|
|
var channel = this.model.get('channel');
|
|
channel.setMode('-b ' + ban.banned);
|
|
|
|
$tr.remove();
|
|
},
|
|
|
|
|
|
updateInfo: function (channel, new_val) {
|
|
var that = this,
|
|
title, modes, url, banlist;
|
|
|
|
modes = channel.get('info_modes');
|
|
if (modes) {
|
|
_.each(modes, function(mode, idx) {
|
|
mode.mode = mode.mode.toLowerCase();
|
|
|
|
if (mode.mode == '+k') {
|
|
that.$el.find('[name="channel_key"]').val(mode.param);
|
|
} else if (mode.mode == '+m') {
|
|
that.$el.find('[name="channel_mute"]').attr('checked', 'checked');
|
|
} else if (mode.mode == '+i') {
|
|
that.$el.find('[name="channel_invite"]').attr('checked', 'checked');
|
|
} else if (mode.mode == '+n') {
|
|
that.$el.find('[name="channel_external_messages"]').attr('checked', 'checked');
|
|
} else if (mode.mode == '+t') {
|
|
that.$el.find('[name="channel_topic"]').attr('checked', 'checked');
|
|
}
|
|
});
|
|
}
|
|
|
|
url = channel.get('info_url');
|
|
if (url) {
|
|
this.$el.find('.channel_url')
|
|
.text(url)
|
|
.attr('href', url);
|
|
|
|
this.$el.find('.channel_url').slideDown();
|
|
}
|
|
|
|
banlist = channel.get('banlist');
|
|
if (banlist && banlist.length) {
|
|
var $table = this.$el.find('.channel-banlist table tbody');
|
|
|
|
this.$el.find('.banlist-status').text('');
|
|
|
|
$table.empty();
|
|
_.each(banlist, function(ban) {
|
|
var $tr = $('<tr></tr>').data('ban', ban);
|
|
|
|
$('<td></td>').text(ban.banned).appendTo($tr);
|
|
$('<td></td>').text(ban.banned_by.split(/[!@]/)[0]).appendTo($tr);
|
|
$('<td></td>').text(_kiwi.utils.formatDate(new Date(parseInt(ban.banned_at, 10) * 1000))).appendTo($tr);
|
|
$('<td><i class="fa fa-rtimes remove-ban"></i></td>').appendTo($tr);
|
|
|
|
$table.append($tr);
|
|
});
|
|
|
|
this.$el.find('.channel-banlist table').slideDown();
|
|
} else {
|
|
this.$el.find('.banlist-status').text('Banlist empty');
|
|
this.$el.find('.channel-banlist table').hide();
|
|
}
|
|
},
|
|
|
|
toggleBanList: function (event) {
|
|
event.preventDefault();
|
|
this.$el.find('.channel-banlist table').toggle();
|
|
|
|
if(!this.$el.find('.channel-banlist table').is(':visible'))
|
|
return;
|
|
|
|
var channel = this.model.get('channel'),
|
|
network = channel.get('network');
|
|
|
|
network.gateway.raw('MODE ' + channel.get('name') + ' +b');
|
|
},
|
|
|
|
dispose: function () {
|
|
this.model.get('channel').off('change:info_modes change:info_url change:banlist', this.updateInfo, this);
|
|
|
|
this.$el.remove();
|
|
}
|
|
});
|
|
|
|
|
|
|
|
_kiwi.view.RightBar = Backbone.View.extend({
|
|
events: {
|
|
'click .right-bar-toggle': 'onClickToggle',
|
|
'click .right-bar-toggle-inner': 'onClickToggle'
|
|
},
|
|
|
|
initialize: function() {
|
|
this.keep_hidden = false;
|
|
this.hidden = this.$el.hasClass('disabled');
|
|
|
|
this.updateIcon();
|
|
},
|
|
|
|
|
|
hide: function() {
|
|
this.hidden = true;
|
|
this.$el.addClass('disabled');
|
|
|
|
this.updateIcon();
|
|
},
|
|
|
|
|
|
show: function() {
|
|
this.hidden = false;
|
|
|
|
if (!this.keep_hidden)
|
|
this.$el.removeClass('disabled');
|
|
|
|
this.updateIcon();
|
|
},
|
|
|
|
|
|
// Toggle if the rightbar should be shown or not
|
|
toggle: function(keep_hidden) {
|
|
// Hacky, but we need to ignore the toggle() call from doLayout() as we are overriding it
|
|
if (this.ignore_layout)
|
|
return true;
|
|
|
|
if (typeof keep_hidden === 'undefined') {
|
|
this.keep_hidden = !this.keep_hidden;
|
|
} else {
|
|
this.keep_hidden = keep_hidden;
|
|
}
|
|
|
|
if (this.keep_hidden || this.hidden) {
|
|
this.$el.addClass('disabled');
|
|
// Remove the right bar width if it has been set manually
|
|
this.$el.css('width', '');
|
|
} else {
|
|
this.$el.removeClass('disabled');
|
|
}
|
|
|
|
this.updateIcon();
|
|
},
|
|
|
|
|
|
updateIcon: function() {
|
|
var $toggle = this.$('.right-bar-toggle'),
|
|
$icon = $toggle.find('i');
|
|
|
|
if (!this.hidden && this.keep_hidden) {
|
|
$toggle.show();
|
|
} else {
|
|
$toggle.hide();
|
|
}
|
|
|
|
if (this.keep_hidden) {
|
|
$icon.removeClass('fa fa-angle-double-right').addClass('fa fa-users');
|
|
} else {
|
|
$icon.removeClass('fa fa-users').addClass('fa fa-angle-double-right');
|
|
}
|
|
},
|
|
|
|
|
|
onClickToggle: function(event) {
|
|
this.toggle();
|
|
|
|
// Hacky, but we need to ignore the toggle() call from doLayout() as we are overriding it
|
|
this.ignore_layout = true;
|
|
_kiwi.app.view.doLayout();
|
|
|
|
// No longer ignoring the toggle() call from doLayout()
|
|
delete this.ignore_layout;
|
|
}
|
|
});
|
|
|
|
|
|
|
|
_kiwi.view.Notification = Backbone.View.extend({
|
|
className: 'notification',
|
|
|
|
events: {
|
|
'click .close': 'close'
|
|
},
|
|
|
|
initialize: function(title, content) {
|
|
this.title = title;
|
|
this.content = content;
|
|
},
|
|
|
|
render: function() {
|
|
this.$el.html($('#tmpl_notifications').html());
|
|
this.$('h6').text(this.title);
|
|
|
|
// HTML string or jquery object
|
|
if (typeof this.content === 'string') {
|
|
this.$('.content').html(this.content);
|
|
} else if (typeof this.content === 'object') {
|
|
this.$('.content').empty().append(this.content);
|
|
}
|
|
|
|
return this;
|
|
},
|
|
|
|
show: function() {
|
|
var that = this;
|
|
|
|
this.render().$el.appendTo(_kiwi.app.view.$el);
|
|
|
|
// The element won't have any CSS transitions applied
|
|
// until after a tick + paint.
|
|
_.defer(function() {
|
|
that.$el.addClass('show');
|
|
});
|
|
},
|
|
|
|
close: function() {
|
|
this.remove();
|
|
}
|
|
});
|
|
|
|
|
|
(function() {
|
|
|
|
function ClientUiCommands(app, controlbox) {
|
|
this.app = app;
|
|
this.controlbox = controlbox;
|
|
|
|
this.addDefaultAliases();
|
|
this.bindCommand(buildCommandFunctions());
|
|
}
|
|
|
|
_kiwi.misc.ClientUiCommands = ClientUiCommands;
|
|
|
|
|
|
// Add the default user command aliases
|
|
ClientUiCommands.prototype.addDefaultAliases = function() {
|
|
$.extend(this.controlbox.preprocessor.aliases, {
|
|
// General aliases
|
|
'/p': '/part $1+',
|
|
'/me': '/action $1+',
|
|
'/j': '/join $1+',
|
|
'/q': '/query $1+',
|
|
'/w': '/whois $1+',
|
|
'/raw': '/quote $1+',
|
|
'/connect': '/server $1+',
|
|
|
|
// Op related aliases
|
|
'/op': '/quote mode $channel +o $1+',
|
|
'/deop': '/quote mode $channel -o $1+',
|
|
'/hop': '/quote mode $channel +h $1+',
|
|
'/dehop': '/quote mode $channel -h $1+',
|
|
'/voice': '/quote mode $channel +v $1+',
|
|
'/devoice': '/quote mode $channel -v $1+',
|
|
'/k': '/kick $channel $1+',
|
|
'/ban': '/quote mode $channel +b $1+',
|
|
'/unban': '/quote mode $channel -b $1+',
|
|
|
|
// Misc aliases
|
|
'/slap': '/me slaps $1 around a bit with a large trout',
|
|
'/tick': '/msg $channel ✔'
|
|
});
|
|
};
|
|
|
|
|
|
/**
|
|
* Add a new command action
|
|
* @var command Object {'command:the_command': fn}
|
|
*/
|
|
ClientUiCommands.prototype.bindCommand = function(command) {
|
|
var that = this,
|
|
descriptions = {};
|
|
|
|
_.each(command, function(fn, event_name) {
|
|
var command_fn, matches;
|
|
if (typeof fn === 'function') {
|
|
command_fn = fn;
|
|
|
|
} else {
|
|
command_fn = fn.fn;
|
|
matches = ['/' + event_name.split(':')[1]];
|
|
if (fn.aliases) {
|
|
matches = matches.concat(_.map(fn.aliases, function(a) {
|
|
return '/' + a;
|
|
}));
|
|
}
|
|
descriptions['/' + event_name.split(':')[1]] = {
|
|
description: fn.description,
|
|
matches: matches
|
|
};
|
|
}
|
|
|
|
that.controlbox.on(event_name, _.bind(command_fn, that));
|
|
});
|
|
|
|
this.controlbox.setAutoCompleteCommands(descriptions);
|
|
};
|
|
|
|
|
|
|
|
|
|
/**
|
|
* Default functions to bind to controlbox events
|
|
**/
|
|
|
|
function buildCommandFunctions() {
|
|
var fn_to_bind = {
|
|
'unknown_command': unknownCommand,
|
|
'command': allCommands,
|
|
'command:msg': {fn: msgCommand, description: translateText('command_description_msg')},
|
|
'command:action': {fn: actionCommand, description: translateText('command_description_action'), aliases: ['me']},
|
|
'command:join': {fn: joinCommand, description: translateText('command_description_join'), aliases: ['j']},
|
|
'command:part': {fn: partCommand, description: translateText('command_description_part'), aliases: ['p']},
|
|
'command:cycle': {fn: cycleCommand, description: translateText('command_description_cycle')},
|
|
'command:nick': {fn: nickCommand, description: translateText('command_description_nick')},
|
|
'command:query': {fn: queryCommand, description: translateText('command_description_query')},
|
|
'command:invite': {fn: inviteCommand, description: translateText('command_description_invite')},
|
|
'command:topic': {fn: topicCommand, description: translateText('command_description_topic')},
|
|
'command:notice': {fn: noticeCommand, description: translateText('command_description_notice')},
|
|
'command:quote': {fn: quoteCommand, description: translateText('command_description_quote'), aliases: ['raw']},
|
|
'command:kick': {fn: kickCommand, description: translateText('command_description_kick')},
|
|
'command:names': {fn: namesCommand, description: ''},
|
|
'command:mode': {fn: modeCommand, description: ''},
|
|
'command:clear': {fn: clearCommand, description: translateText('command_description_clear')},
|
|
'command:ctcp': {fn: ctcpCommand, description: translateText('command_description_ctcp')},
|
|
'command:quit': {fn: quitCommand, description: translateText('command_description_quit'), aliases: ['q']},
|
|
'command:server': {fn: serverCommand, description: translateText('command_description_server')},
|
|
'command:whois': {fn: whoisCommand, description: translateText('command_description_whois'), aliases: ['w']},
|
|
'command:whowas': {fn: whowasCommand, description: translateText('command_description_whowas')},
|
|
'command:away': {fn: awayCommand, description: translateText('command_description_away')},
|
|
'command:encoding': {fn: encodingCommand, description: translateText('command_description_encoding')},
|
|
'command:channel': {fn: channelCommand, description: ''},
|
|
'command:applet': {fn: appletCommand, description: ''},
|
|
'command:settings': {fn: settingsCommand, description: translateText('command_description_settings')},
|
|
'command:script': {fn: scriptCommand, description: translateText('command_description_script')}
|
|
};
|
|
|
|
|
|
fn_to_bind['command:css'] = {
|
|
description: translateText('command_description_css'),
|
|
fn: function(ev) {
|
|
this.app.view.reloadStyles();
|
|
}
|
|
};
|
|
|
|
|
|
fn_to_bind['command:js'] = {
|
|
description: translateText('command_description_js'),
|
|
fn: function(ev) {
|
|
if (!ev.params[0]) return;
|
|
$script(ev.params[0] + '?' + (new Date().getTime()));
|
|
}
|
|
};
|
|
|
|
|
|
fn_to_bind['command:set'] = {
|
|
description: translateText('command_description_set'),
|
|
fn: function(ev) {
|
|
if (!ev.params[0]) return;
|
|
|
|
var setting = ev.params[0],
|
|
value;
|
|
|
|
// Do we have a second param to set a value?
|
|
if (ev.params[1]) {
|
|
ev.params.shift();
|
|
|
|
value = ev.params.join(' ');
|
|
|
|
// If we're setting a true boolean value..
|
|
if (value === 'true')
|
|
value = true;
|
|
|
|
// If we're setting a false boolean value..
|
|
if (value === 'false')
|
|
value = false;
|
|
|
|
// If we're setting a number..
|
|
if (parseInt(value, 10).toString() === value)
|
|
value = parseInt(value, 10);
|
|
|
|
_kiwi.global.settings.set(setting, value);
|
|
}
|
|
|
|
// Read the value to the user
|
|
this.app.panels().active.addMsg('', styleText('set_setting', {text: setting + ' = ' + _kiwi.global.settings.get(setting).toString()}));
|
|
}
|
|
};
|
|
|
|
|
|
fn_to_bind['command:save'] = {
|
|
description: translateText('command_description_save'),
|
|
fn: function(ev) {
|
|
_kiwi.global.settings.save();
|
|
this.app.panels().active.addMsg('', styleText('settings_saved', {text: translateText('client_models_application_settings_saved')}));
|
|
}
|
|
};
|
|
|
|
|
|
fn_to_bind['command:alias'] = {
|
|
description: translateText('command_description_alias'),
|
|
fn: function(ev) {
|
|
var that = this,
|
|
name, rule;
|
|
|
|
// No parameters passed so list them
|
|
if (!ev.params[1]) {
|
|
$.each(this.controlbox.preprocessor.aliases, function (name, rule) {
|
|
that.app.panels().server.addMsg(' ', styleText('list_aliases', {text: name + ' => ' + rule}));
|
|
});
|
|
return;
|
|
}
|
|
|
|
// Deleting an alias?
|
|
if (ev.params[0] === 'del' || ev.params[0] === 'delete') {
|
|
name = ev.params[1];
|
|
if (name[0] !== '/') name = '/' + name;
|
|
delete this.controlbox.preprocessor.aliases[name];
|
|
return;
|
|
}
|
|
|
|
// Add the alias
|
|
name = ev.params[0];
|
|
ev.params.shift();
|
|
rule = ev.params.join(' ');
|
|
|
|
// Make sure the name starts with a slash
|
|
if (name[0] !== '/') name = '/' + name;
|
|
|
|
// Now actually add the alias
|
|
this.controlbox.preprocessor.aliases[name] = rule;
|
|
}
|
|
};
|
|
|
|
|
|
fn_to_bind['command:ignore'] = {
|
|
description: translateText('command_description_ignore'),
|
|
fn: function(ev) {
|
|
var that = this,
|
|
ignore_list = this.app.connections.active_connection.ignore_list,
|
|
user_mask;
|
|
|
|
// No parameters passed so list them
|
|
if (!ev.params[0]) {
|
|
if (ignore_list.length > 0) {
|
|
this.app.panels().active.addMsg(' ', styleText('ignore_title', {text: translateText('client_models_application_ignore_title')}));
|
|
ignore_list.forEach(function(ignored) {
|
|
that.app.panels().active.addMsg(' ', styleText('ignored_pattern', {text: ignored.get('mask')}));
|
|
});
|
|
} else {
|
|
this.app.panels().active.addMsg(' ', styleText('ignore_none', {text: translateText('client_models_application_ignore_none')}));
|
|
}
|
|
return;
|
|
}
|
|
|
|
// We have a parameter, so add it, first convert it to a full mask.
|
|
user_mask = toUserMask(ev.params[0]);
|
|
ignore_list.addMask(user_mask);
|
|
|
|
this.app.panels().active.addMsg(' ', styleText('ignore_nick', {text: translateText('client_models_application_ignore_nick', [user_mask])}));
|
|
}
|
|
};
|
|
|
|
|
|
fn_to_bind['command:unignore'] = {
|
|
description: translateText('command_description_unignore'),
|
|
fn: function(ev) {
|
|
var ignore_list = this.app.connections.active_connection.ignore_list,
|
|
user_mask, matches;
|
|
|
|
if (!ev.params[0]) {
|
|
this.app.panels().active.addMsg(' ', styleText('ignore_stop_notice', {text: translateText('client_models_application_ignore_stop_notice')}));
|
|
return;
|
|
}
|
|
|
|
user_mask = toUserMask(ev.params[0]);
|
|
matches = ignore_list.where({mask: user_mask});
|
|
if (matches) {
|
|
ignore_list.remove(matches);
|
|
}
|
|
|
|
this.app.panels().active.addMsg(' ', styleText('ignore_stopped', {text: translateText('client_models_application_ignore_stopped', [user_mask])}));
|
|
}
|
|
};
|
|
|
|
|
|
return fn_to_bind;
|
|
}
|
|
|
|
|
|
|
|
|
|
// A fallback action. Send a raw command to the server
|
|
function unknownCommand (ev) {
|
|
var raw_cmd = ev.command + ' ' + ev.params.join(' ');
|
|
this.app.connections.active_connection.gateway.raw(raw_cmd);
|
|
}
|
|
|
|
|
|
function allCommands (ev) {}
|
|
|
|
|
|
function joinCommand (ev) {
|
|
var panels, channel_names;
|
|
|
|
channel_names = ev.params.join(' ').split(',');
|
|
panels = this.app.connections.active_connection.createAndJoinChannels(channel_names);
|
|
|
|
// Show the last channel if we have one
|
|
if (panels.length)
|
|
panels[panels.length - 1].view.show();
|
|
}
|
|
|
|
|
|
function queryCommand (ev) {
|
|
var destination, message, panel;
|
|
|
|
destination = ev.params[0];
|
|
ev.params.shift();
|
|
|
|
message = ev.params.join(' ');
|
|
|
|
// Check if we have the panel already. If not, create it
|
|
panel = this.app.connections.active_connection.panels.getByName(destination);
|
|
if (!panel) {
|
|
panel = new _kiwi.model.Query({name: destination, network: this.app.connections.active_connection});
|
|
this.app.connections.active_connection.panels.add(panel);
|
|
}
|
|
|
|
if (panel) panel.view.show();
|
|
|
|
if (message) {
|
|
this.app.connections.active_connection.gateway.msg(panel.get('name'), message);
|
|
panel.addMsg(this.app.connections.active_connection.get('nick'), styleText('privmsg', {text: message}), 'privmsg');
|
|
}
|
|
|
|
}
|
|
|
|
|
|
function msgCommand (ev) {
|
|
var message,
|
|
destination = ev.params[0],
|
|
panel = this.app.connections.active_connection.panels.getByName(destination) || this.app.panels().server;
|
|
|
|
ev.params.shift();
|
|
message = ev.params.join(' ');
|
|
|
|
panel.addMsg(this.app.connections.active_connection.get('nick'), styleText('privmsg', {text: message}), 'privmsg');
|
|
this.app.connections.active_connection.gateway.msg(destination, message);
|
|
}
|
|
|
|
|
|
function actionCommand (ev) {
|
|
if (this.app.panels().active.isServer()) {
|
|
return;
|
|
}
|
|
|
|
var panel = this.app.panels().active;
|
|
panel.addMsg('', styleText('action', {nick: this.app.connections.active_connection.get('nick'), text: ev.params.join(' ')}), 'action');
|
|
this.app.connections.active_connection.gateway.action(panel.get('name'), ev.params.join(' '));
|
|
}
|
|
|
|
|
|
function partCommand (ev) {
|
|
var that = this,
|
|
chans,
|
|
msg;
|
|
if (ev.params.length === 0) {
|
|
this.app.connections.active_connection.gateway.part(this.app.panels().active.get('name'));
|
|
} else {
|
|
chans = ev.params[0].split(',');
|
|
msg = ev.params.slice(1).join(' ');
|
|
_.each(chans, function (channel) {
|
|
that.app.connections.active_connection.gateway.part(channel, msg);
|
|
});
|
|
}
|
|
}
|
|
|
|
|
|
function cycleCommand (ev) {
|
|
var that = this,
|
|
chan_name;
|
|
|
|
if (ev.params.length === 0) {
|
|
chan_name = this.app.panels().active.get('name');
|
|
} else {
|
|
chan_name = ev.params[0];
|
|
}
|
|
|
|
this.app.connections.active_connection.gateway.part(chan_name);
|
|
|
|
// Wait for a second to give the network time to register the part command
|
|
setTimeout(function() {
|
|
// Use createAndJoinChannels() here as it auto-creates panels instead of waiting for the network
|
|
that.app.connections.active_connection.createAndJoinChannels(chan_name);
|
|
that.app.connections.active_connection.panels.getByName(chan_name).show();
|
|
}, 1000);
|
|
}
|
|
|
|
|
|
function nickCommand (ev) {
|
|
this.app.connections.active_connection.gateway.changeNick(ev.params[0]);
|
|
}
|
|
|
|
|
|
function topicCommand (ev) {
|
|
var channel_name;
|
|
|
|
if (ev.params.length === 0) return;
|
|
|
|
if (this.app.connections.active_connection.isChannelName(ev.params[0])) {
|
|
channel_name = ev.params[0];
|
|
ev.params.shift();
|
|
} else {
|
|
channel_name = this.app.panels().active.get('name');
|
|
}
|
|
|
|
this.app.connections.active_connection.gateway.topic(channel_name, ev.params.join(' '));
|
|
}
|
|
|
|
|
|
function noticeCommand (ev) {
|
|
var destination;
|
|
|
|
// Make sure we have a destination and some sort of message
|
|
if (ev.params.length <= 1) return;
|
|
|
|
destination = ev.params[0];
|
|
ev.params.shift();
|
|
|
|
this.app.connections.active_connection.gateway.notice(destination, ev.params.join(' '));
|
|
}
|
|
|
|
|
|
function quoteCommand (ev) {
|
|
var raw = ev.params.join(' ');
|
|
this.app.connections.active_connection.gateway.raw(raw);
|
|
}
|
|
|
|
|
|
function kickCommand (ev) {
|
|
var nick, panel = this.app.panels().active;
|
|
|
|
if (!panel.isChannel()) return;
|
|
|
|
// Make sure we have a nick
|
|
if (ev.params.length === 0) return;
|
|
|
|
nick = ev.params[0];
|
|
ev.params.shift();
|
|
|
|
this.app.connections.active_connection.gateway.kick(panel.get('name'), nick, ev.params.join(' '));
|
|
}
|
|
|
|
|
|
function namesCommand (ev) {
|
|
var channel, panel = this.app.panels().active;
|
|
|
|
if (!panel.isChannel()) return;
|
|
|
|
// Make sure we have a channel
|
|
channel = ev.params.length === 0 ?
|
|
panel.get('name') :
|
|
ev.params[0];
|
|
|
|
this.app.connections.active_connection.gateway.raw('NAMES ' + channel);
|
|
}
|
|
|
|
|
|
function modeCommand (ev) {
|
|
var params, for_channel, network,
|
|
panel = this.app.panels().active;
|
|
|
|
network = panel.get('network');
|
|
if (!network) {
|
|
return;
|
|
}
|
|
|
|
// Use the specified channel is one is given..
|
|
if (network.isChannelName(ev.params[0])) {
|
|
for_channel = ev.params[0];
|
|
params = ev.params.slice(1).join(' ');
|
|
|
|
// Use the current channel..
|
|
} else if(panel.isChannel()) {
|
|
for_channel = panel.get('name');
|
|
params = ev.params.join(' ');
|
|
|
|
// Nothing to get a mode for..
|
|
} else {
|
|
return;
|
|
}
|
|
|
|
// Due to a flaw on the server-side we can't actually get modes for channels
|
|
// that we are not joined.
|
|
if (!params) {
|
|
network.gateway.on('channel_info', function onChanInfo(event) {
|
|
if (event.channel.toLowerCase() !== for_channel.toLowerCase() || typeof event.modes === 'undefined') {
|
|
return;
|
|
}
|
|
|
|
// No need to listen any more
|
|
network.gateway.off('channel_info', onChanInfo);
|
|
|
|
// Convert the modes into human readable format (+nt -xyz)
|
|
var mode_string = _.chain(event.modes)
|
|
.reduce(function(res, mode) {
|
|
var type = mode.mode[0]==='-'?'-':'+';
|
|
res[type].push(mode.mode.substr(1));
|
|
return res;
|
|
}, {'+':[], '-':[]})
|
|
.reduce(function(res, modes, type) {
|
|
if (modes.length > 0) res += type + modes.join('') + ' ';
|
|
return res;
|
|
}, '')
|
|
.value();
|
|
|
|
panel.addMsg('', event.channel + ' ' + mode_string);
|
|
});
|
|
}
|
|
|
|
this.app.connections.active_connection.gateway.raw('MODE ' + for_channel + ' ' + params);
|
|
}
|
|
|
|
|
|
function clearCommand (ev) {
|
|
// Can't clear a server or applet panel
|
|
if (this.app.panels().active.isServer() || this.app.panels().active.isApplet()) {
|
|
return;
|
|
}
|
|
|
|
if (this.app.panels().active.clearMessages) {
|
|
this.app.panels().active.clearMessages();
|
|
}
|
|
}
|
|
|
|
|
|
function ctcpCommand(ev) {
|
|
var target, type;
|
|
|
|
// Make sure we have a target and a ctcp type (eg. version, time)
|
|
if (ev.params.length < 2) return;
|
|
|
|
target = ev.params[0];
|
|
ev.params.shift();
|
|
|
|
type = ev.params[0];
|
|
ev.params.shift();
|
|
|
|
this.app.connections.active_connection.gateway.ctcpRequest(type, target, ev.params.join(' '));
|
|
}
|
|
|
|
|
|
function settingsCommand (ev) {
|
|
var settings = _kiwi.model.Applet.loadOnce('kiwi_settings');
|
|
settings.view.show();
|
|
}
|
|
|
|
|
|
function scriptCommand (ev) {
|
|
var editor = _kiwi.model.Applet.loadOnce('kiwi_script_editor');
|
|
editor.view.show();
|
|
}
|
|
|
|
|
|
function appletCommand (ev) {
|
|
if (!ev.params[0]) return;
|
|
|
|
var panel;
|
|
|
|
if (ev.params[1]) {
|
|
// Url and name given
|
|
panel = panel.loadFromUrl(ev.params[0], ev.params[1]);
|
|
} else {
|
|
// Load a pre-loaded applet
|
|
panel = _kiwi.model.Applet.loadOnce(ev.params[0]);
|
|
if (!panel) {
|
|
this.app.panels().server.addMsg('', styleText('applet_notfound', {text: translateText('client_models_application_applet_notfound', [ev.params[0]])}));
|
|
return;
|
|
}
|
|
}
|
|
|
|
panel.view.show();
|
|
}
|
|
|
|
|
|
function inviteCommand (ev) {
|
|
var nick, channel;
|
|
|
|
// A nick must be specified
|
|
if (!ev.params[0])
|
|
return;
|
|
|
|
// Can only invite into channels
|
|
if (!this.app.panels().active.isChannel())
|
|
return;
|
|
|
|
nick = ev.params[0];
|
|
channel = this.app.panels().active.get('name');
|
|
|
|
this.app.connections.active_connection.gateway.raw('INVITE ' + nick + ' ' + channel);
|
|
|
|
this.app.panels().active.addMsg('', styleText('channel_has_been_invited', {nick: nick, text: translateText('client_models_application_has_been_invited', [channel])}), 'action');
|
|
}
|
|
|
|
|
|
function whoisCommand (ev) {
|
|
var nick;
|
|
|
|
if (ev.params[0]) {
|
|
nick = ev.params[0];
|
|
} else if (this.app.panels().active.isQuery()) {
|
|
nick = this.app.panels().active.get('name');
|
|
}
|
|
|
|
if (nick)
|
|
this.app.connections.active_connection.gateway.raw('WHOIS ' + nick + ' ' + nick);
|
|
}
|
|
|
|
|
|
function whowasCommand (ev) {
|
|
var nick;
|
|
|
|
if (ev.params[0]) {
|
|
nick = ev.params[0];
|
|
} else if (this.app.panels().active.isQuery()) {
|
|
nick = this.app.panels().active.get('name');
|
|
}
|
|
|
|
if (nick)
|
|
this.app.connections.active_connection.gateway.raw('WHOWAS ' + nick);
|
|
}
|
|
|
|
|
|
function awayCommand (ev) {
|
|
this.app.connections.active_connection.gateway.raw('AWAY :' + ev.params.join(' '));
|
|
}
|
|
|
|
|
|
function encodingCommand (ev) {
|
|
var that = this;
|
|
|
|
if (ev.params[0]) {
|
|
_kiwi.gateway.setEncoding(null, ev.params[0], function (success) {
|
|
if (success) {
|
|
that.app.panels().active.addMsg('', styleText('encoding_changed', {text: translateText('client_models_application_encoding_changed', [ev.params[0]])}));
|
|
} else {
|
|
that.app.panels().active.addMsg('', styleText('encoding_invalid', {text: translateText('client_models_application_encoding_invalid', [ev.params[0]])}));
|
|
}
|
|
});
|
|
} else {
|
|
this.app.panels().active.addMsg('', styleText('client_models_application_encoding_notspecified', {text: translateText('client_models_application_encoding_notspecified')}));
|
|
this.app.panels().active.addMsg('', styleText('client_models_application_encoding_usage', {text: translateText('client_models_application_encoding_usage')}));
|
|
}
|
|
}
|
|
|
|
|
|
function channelCommand (ev) {
|
|
var active_panel = this.app.panels().active;
|
|
|
|
if (!active_panel.isChannel())
|
|
return;
|
|
|
|
new _kiwi.model.ChannelInfo({channel: this.app.panels().active});
|
|
}
|
|
|
|
|
|
function quitCommand (ev) {
|
|
var network = this.app.connections.active_connection;
|
|
|
|
if (!network)
|
|
return;
|
|
|
|
network.gateway.quit(ev.params.join(' '));
|
|
}
|
|
|
|
|
|
function serverCommand (ev) {
|
|
var that = this,
|
|
server, port, ssl, password, nick,
|
|
tmp;
|
|
|
|
// If no server address given, show the new connection dialog
|
|
if (!ev.params[0]) {
|
|
tmp = new _kiwi.view.MenuBox(_kiwi.global.i18n.translate('client_models_application_connection_create').fetch());
|
|
tmp.addItem('new_connection', new _kiwi.model.NewConnection().view.$el);
|
|
tmp.show();
|
|
|
|
// Center screen the dialog
|
|
tmp.$el.offset({
|
|
top: (this.app.view.$el.height() / 2) - (tmp.$el.height() / 2),
|
|
left: (this.app.view.$el.width() / 2) - (tmp.$el.width() / 2)
|
|
});
|
|
|
|
return;
|
|
}
|
|
|
|
// Port given in 'host:port' format and no specific port given after a space
|
|
if (ev.params[0].indexOf(':') > 0) {
|
|
tmp = ev.params[0].split(':');
|
|
server = tmp[0];
|
|
port = tmp[1];
|
|
|
|
password = ev.params[1] || undefined;
|
|
|
|
} else {
|
|
// Server + port given as 'host port'
|
|
server = ev.params[0];
|
|
port = ev.params[1] || 6667;
|
|
|
|
password = ev.params[2] || undefined;
|
|
}
|
|
|
|
// + in the port means SSL
|
|
if (port.toString()[0] === '+') {
|
|
ssl = true;
|
|
port = parseInt(port.substring(1), 10);
|
|
} else {
|
|
ssl = false;
|
|
}
|
|
|
|
// Default port if one wasn't found
|
|
port = port || 6667;
|
|
|
|
// Use the same nick as we currently have
|
|
nick = this.app.connections.active_connection.get('nick');
|
|
|
|
this.app.panels().active.addMsg('', styleText('server_connecting', {text: translateText('client_models_application_connection_connecting', [server, port.toString()])}));
|
|
|
|
_kiwi.gateway.newConnection({
|
|
nick: nick,
|
|
host: server,
|
|
port: port,
|
|
ssl: ssl,
|
|
password: password
|
|
}, function(err, new_connection) {
|
|
var translated_err;
|
|
|
|
if (err) {
|
|
translated_err = translateText('client_models_application_connection_error', [server, port.toString(), err.toString()]);
|
|
that.app.panels().active.addMsg('', styleText('server_connecting_error', {text: translated_err}));
|
|
}
|
|
});
|
|
}
|
|
|
|
})();
|
|
|
|
|
|
|
|
(function () {
|
|
var View = Backbone.View.extend({
|
|
events: {
|
|
'change [data-setting]': 'saveSettings',
|
|
'click [data-setting="theme"]': 'selectTheme',
|
|
'click .register_protocol': 'registerProtocol',
|
|
'click .enable_notifications': 'enableNotifications',
|
|
'click .show-category': 'onClickShowCategory'
|
|
},
|
|
|
|
initialize: function (options) {
|
|
var text = {
|
|
messages : translateText('client_applets_settings_messages'),
|
|
chat_messages : translateText('client_applets_settings_chat_messages'),
|
|
alerts_notifications : translateText('client_applets_settings_alerts_notifications'),
|
|
appearance : translateText('client_applets_settings_appearance'),
|
|
theme : translateText('client_applets_settings_theme'),
|
|
channels : translateText('client_applets_settings_channels'),
|
|
tabs : translateText('client_applets_settings_channelview_tabs'),
|
|
list : translateText('client_applets_settings_channelview_list'),
|
|
large_amounts_of_chans: translateText('client_applets_settings_channelview_list_notice'),
|
|
language : translateText('client_applets_settings_language'),
|
|
join_part : translateText('client_applets_settings_notification_joinpart'),
|
|
count_all_activity : translateText('client_applets_settings_notification_count_all_activity'),
|
|
timestamps : translateText('client_applets_settings_timestamp'),
|
|
timestamp_24 : translateText('client_applets_settings_timestamp_24_hour'),
|
|
mute : translateText('client_applets_settings_notification_sound'),
|
|
emoticons : translateText('client_applets_settings_emoticons'),
|
|
queries : translateText('client_applets_settings_ignore_new_queries'),
|
|
scroll_history : translateText('client_applets_settings_history_length'),
|
|
languages : _kiwi.app.translations,
|
|
default_client : translateText('client_applets_settings_default_client'),
|
|
make_default : translateText('client_applets_settings_default_client_enable'),
|
|
locale_restart_needed : translateText('client_applets_settings_locale_restart_needed'),
|
|
default_note : translateText('client_applets_settings_default_client_notice', '<a href="chrome://settings/handlers">chrome://settings/handlers</a>'),
|
|
html5_notifications : translateText('client_applets_settings_html5_notifications'),
|
|
enable_notifications : translateText('client_applets_settings_enable_notifications'),
|
|
custom_highlights : translateText('client_applets_settings_custom_highlights'),
|
|
autocomplete_slideout : translateText('client_applets_settings_autocomplete_slideout'),
|
|
theme_thumbnails: _.map(_kiwi.app.themes, function (theme) {
|
|
return _.template($('#tmpl_theme_thumbnail').html().trim())(theme);
|
|
})
|
|
};
|
|
this.$el = $(_.template($('#tmpl_applet_settings').html().trim())(text));
|
|
|
|
if (!navigator.registerProtocolHandler) {
|
|
this.$('.protocol_handler').remove();
|
|
}
|
|
|
|
if (_kiwi.utils.notifications.allowed() !== null) {
|
|
this.$('.notification_enabler').remove();
|
|
}
|
|
|
|
// Incase any settings change while we have this open, update them
|
|
_kiwi.global.settings.on('change', this.loadSettings, this);
|
|
|
|
// Now actually show the first cetegory of settings
|
|
this.showCategory('appearance');
|
|
|
|
},
|
|
|
|
loadSettings: function () {
|
|
|
|
_.each(_kiwi.global.settings.attributes, function(value, key) {
|
|
|
|
var $el = this.$('[data-setting="' + key + '"]');
|
|
|
|
// Only deal with settings we have a UI element for
|
|
if (!$el.length)
|
|
return;
|
|
|
|
switch ($el.prop('type')) {
|
|
case 'checkbox':
|
|
$el.prop('checked', value);
|
|
break;
|
|
case 'radio':
|
|
this.$('[data-setting="' + key + '"][value="' + value + '"]').prop('checked', true);
|
|
break;
|
|
case 'text':
|
|
$el.val(value);
|
|
break;
|
|
case 'select-one':
|
|
this.$('[value="' + value + '"]').prop('selected', true);
|
|
break;
|
|
default:
|
|
this.$('[data-setting="' + key + '"][data-value="' + value + '"]').addClass('active');
|
|
break;
|
|
}
|
|
}, this);
|
|
},
|
|
|
|
saveSettings: function (event) {
|
|
var value,
|
|
settings = _kiwi.global.settings,
|
|
$setting = $(event.currentTarget);
|
|
|
|
switch (event.currentTarget.type) {
|
|
case 'checkbox':
|
|
value = $setting.is(':checked');
|
|
break;
|
|
case 'radio':
|
|
case 'text':
|
|
value = $setting.val();
|
|
break;
|
|
case 'select-one':
|
|
value = $(event.currentTarget[$setting.prop('selectedIndex')]).val();
|
|
break;
|
|
default:
|
|
value = $setting.data('value');
|
|
break;
|
|
}
|
|
|
|
settings.set($setting.data('setting'), value);
|
|
settings.saveOne($setting.data('setting'));
|
|
},
|
|
|
|
selectTheme: function(event) {
|
|
event.preventDefault();
|
|
|
|
this.$('[data-setting="theme"].active').removeClass('active');
|
|
$(event.currentTarget).addClass('active').trigger('change');
|
|
},
|
|
|
|
registerProtocol: function (event) {
|
|
event.preventDefault();
|
|
|
|
navigator.registerProtocolHandler('irc', document.location.origin + _kiwi.app.get('base_path') + '/%s', 'Kiwi IRC');
|
|
navigator.registerProtocolHandler('ircs', document.location.origin + _kiwi.app.get('base_path') + '/%s', 'Kiwi IRC');
|
|
},
|
|
|
|
enableNotifications: function(event){
|
|
event.preventDefault();
|
|
var notifications = _kiwi.utils.notifications;
|
|
|
|
notifications.requestPermission().always(_.bind(function () {
|
|
if (notifications.allowed() !== null) {
|
|
this.$('.notification_enabler').remove();
|
|
}
|
|
}, this));
|
|
},
|
|
|
|
|
|
showCategory: function(category) {
|
|
this.$('.settings-category').removeClass('active');
|
|
this.$('.settings-category-' + category).addClass('active');
|
|
|
|
this.$('.show-category').removeClass('active');
|
|
this.$('.show-category-' + category).addClass('active');
|
|
|
|
// Load the current settings
|
|
this.loadSettings();
|
|
},
|
|
|
|
|
|
onClickShowCategory: function(event) {
|
|
var category = $(event.currentTarget).data('category');
|
|
if (category) {
|
|
this.showCategory(category);
|
|
}
|
|
}
|
|
|
|
});
|
|
|
|
|
|
var Applet = Backbone.Model.extend({
|
|
initialize: function () {
|
|
this.set('title', translateText('client_applets_settings_title'));
|
|
this.view = new View();
|
|
}
|
|
});
|
|
|
|
|
|
_kiwi.model.Applet.register('kiwi_settings', Applet);
|
|
})();
|
|
|
|
|
|
|
|
(function () {
|
|
|
|
var View = Backbone.View.extend({
|
|
events: {
|
|
"click .chan": "chanClick",
|
|
"click .channel_name_title": "sortChannelsByNameClick",
|
|
"click .users_title": "sortChannelsByUsersClick"
|
|
},
|
|
|
|
|
|
|
|
initialize: function (options) {
|
|
var text = {
|
|
channel_name: _kiwi.global.i18n.translate('client_applets_chanlist_channelname').fetch(),
|
|
users: _kiwi.global.i18n.translate('client_applets_chanlist_users').fetch(),
|
|
topic: _kiwi.global.i18n.translate('client_applets_chanlist_topic').fetch()
|
|
};
|
|
this.$el = $(_.template($('#tmpl_channel_list').html().trim())(text));
|
|
|
|
this.channels = [];
|
|
|
|
// Sort the table
|
|
this.order = '';
|
|
|
|
// Waiting to add the table back into the DOM?
|
|
this.waiting = false;
|
|
},
|
|
|
|
render: function () {
|
|
var table = $('table', this.$el),
|
|
tbody = table.children('tbody:first').detach(),
|
|
that = this,
|
|
i;
|
|
|
|
// Create the sort icon container and clean previous any previous ones
|
|
if($('.applet_chanlist .users_title').find('span.chanlist_sort_users').length == 0) {
|
|
this.$('.users_title').append('<span class="chanlist_sort_users"> </span>');
|
|
} else {
|
|
this.$('.users_title span.chanlist_sort_users').removeClass('fa fa-sort-desc');
|
|
this.$('.users_title span.chanlist_sort_users').removeClass('fa fa-sort-asc');
|
|
}
|
|
if ($('.applet_chanlist .channel_name_title').find('span.chanlist_sort_names').length == 0) {
|
|
this.$('.channel_name_title').append('<span class="chanlist_sort_names"> </span>');
|
|
} else {
|
|
this.$('.channel_name_title span.chanlist_sort_names').removeClass('fa fa-sort-desc');
|
|
this.$('.channel_name_title span.chanlist_sort_names').removeClass('fa fa-sort-asc');
|
|
}
|
|
|
|
// Push the new sort icon
|
|
switch (this.order) {
|
|
case 'user_desc':
|
|
default:
|
|
this.$('.users_title span.chanlist_sort_users').addClass('fa fa-sort-asc');
|
|
break;
|
|
case 'user_asc':
|
|
this.$('.users_title span.chanlist_sort_users').addClass('fa fa-sort-desc');
|
|
break;
|
|
case 'name_asc':
|
|
this.$('.channel_name_title span.chanlist_sort_names').addClass('fa fa-sort-desc');
|
|
break;
|
|
case 'name_desc':
|
|
this.$('.channel_name_title span.chanlist_sort_names').addClass('fa fa-sort-asc');
|
|
break;
|
|
}
|
|
|
|
this.channels = this.sortChannels(this.channels, this.order);
|
|
|
|
// Make sure all the channel DOM nodes are inserted in order
|
|
for (i = 0; i < this.channels.length; i++) {
|
|
tbody[0].appendChild(this.channels[i].dom);
|
|
}
|
|
|
|
table[0].appendChild(tbody[0]);
|
|
},
|
|
|
|
|
|
reset: function() {
|
|
this.$('tbody').empty();
|
|
this.channels = [];
|
|
this.order = '';
|
|
this.waiting = false;
|
|
},
|
|
|
|
|
|
chanClick: function (event) {
|
|
if (event.target) {
|
|
_kiwi.gateway.join(null, $(event.target).data('channel'));
|
|
} else {
|
|
// IE...
|
|
_kiwi.gateway.join(null, $(event.srcElement).data('channel'));
|
|
}
|
|
},
|
|
|
|
sortChannelsByNameClick: function (event) {
|
|
// Revert the sorting to switch between orders
|
|
this.order = (this.order == 'name_asc') ? 'name_desc' : 'name_asc';
|
|
|
|
this.sortChannelsClick();
|
|
},
|
|
|
|
sortChannelsByUsersClick: function (event) {
|
|
// Revert the sorting to switch between orders
|
|
this.order = (this.order == 'user_desc' || this.order == '') ? 'user_asc' : 'user_desc';
|
|
|
|
this.sortChannelsClick();
|
|
},
|
|
|
|
sortChannelsClick: function() {
|
|
this.render();
|
|
},
|
|
|
|
sortChannels: function (channels, order) {
|
|
var sort_channels = [],
|
|
new_channels = [];
|
|
|
|
|
|
// First we create a light copy of the channels object to do the sorting
|
|
_.each(channels, function (chan, chan_idx) {
|
|
sort_channels.push({'chan_idx': chan_idx, 'num_users': chan.num_users, 'channel': chan.channel});
|
|
});
|
|
|
|
// Second, we apply the sorting
|
|
sort_channels.sort(function (a, b) {
|
|
switch (order) {
|
|
case 'user_asc':
|
|
return a.num_users - b.num_users;
|
|
case 'user_desc':
|
|
return b.num_users - a.num_users;
|
|
case 'name_asc':
|
|
if (a.channel.toLowerCase() > b.channel.toLowerCase()) return 1;
|
|
if (a.channel.toLowerCase() < b.channel.toLowerCase()) return -1;
|
|
case 'name_desc':
|
|
if (a.channel.toLowerCase() < b.channel.toLowerCase()) return 1;
|
|
if (a.channel.toLowerCase() > b.channel.toLowerCase()) return -1;
|
|
default:
|
|
return b.num_users - a.num_users;
|
|
}
|
|
return 0;
|
|
});
|
|
|
|
// Third, we re-shuffle the chanlist according to the sort order
|
|
_.each(sort_channels, function (chan) {
|
|
new_channels.push(channels[chan.chan_idx]);
|
|
});
|
|
|
|
return new_channels;
|
|
}
|
|
});
|
|
|
|
|
|
|
|
var Applet = Backbone.Model.extend({
|
|
initialize: function () {
|
|
this.set('title', _kiwi.global.i18n.translate('client_applets_chanlist_channellist').fetch());
|
|
this.view = new View();
|
|
|
|
this.network = _kiwi.global.components.Network();
|
|
this.network.on('list_channel', this.onListChannel, this);
|
|
this.network.on('list_start', this.onListStart, this);
|
|
},
|
|
|
|
|
|
// New channels to add to our list
|
|
onListChannel: function (event) {
|
|
this.addChannel(event.chans);
|
|
},
|
|
|
|
// A new, fresh channel list starting
|
|
onListStart: function (event) {
|
|
this.view.reset();
|
|
},
|
|
|
|
addChannel: function (channels) {
|
|
var that = this;
|
|
|
|
if (!_.isArray(channels)) {
|
|
channels = [channels];
|
|
}
|
|
_.each(channels, function (chan) {
|
|
var row;
|
|
row = document.createElement("tr");
|
|
row.innerHTML = '<td class="chanlist_name"><a class="chan" data-channel="' + chan.channel + '">' + _.escape(chan.channel) + '</a></td><td class="chanlist_num_users" style="text-align: center;">' + chan.num_users + '</td><td style="padding-left: 2em;" class="chanlist_topic">' + formatIRCMsg(_.escape(chan.topic)) + '</td>';
|
|
chan.dom = row;
|
|
that.view.channels.push(chan);
|
|
});
|
|
|
|
if (!that.view.waiting) {
|
|
that.view.waiting = true;
|
|
_.defer(function () {
|
|
that.view.render();
|
|
that.view.waiting = false;
|
|
});
|
|
}
|
|
},
|
|
|
|
|
|
dispose: function () {
|
|
this.view.channels = null;
|
|
this.view.unbind();
|
|
this.view.$el.html('');
|
|
this.view.remove();
|
|
this.view = null;
|
|
|
|
// Remove any network event bindings
|
|
this.network.off();
|
|
}
|
|
});
|
|
|
|
|
|
|
|
_kiwi.model.Applet.register('kiwi_chanlist', Applet);
|
|
})();
|
|
|
|
|
|
(function () {
|
|
var view = Backbone.View.extend({
|
|
events: {
|
|
'click .btn_save': 'onSave'
|
|
},
|
|
|
|
initialize: function (options) {
|
|
var that = this,
|
|
text = {
|
|
save: _kiwi.global.i18n.translate('client_applets_scripteditor_save').fetch()
|
|
};
|
|
this.$el = $(_.template($('#tmpl_script_editor').html().trim())(text));
|
|
|
|
this.model.on('applet_loaded', function () {
|
|
that.$el.parent().css('height', '100%');
|
|
$script(_kiwi.app.get('base_path') + '/assets/libs/ace/ace.js', function (){ that.createAce(); });
|
|
});
|
|
},
|
|
|
|
|
|
createAce: function () {
|
|
var editor_id = 'editor_' + Math.floor(Math.random()*10000000).toString();
|
|
this.editor_id = editor_id;
|
|
|
|
this.$el.find('.editor').attr('id', editor_id);
|
|
|
|
this.editor = ace.edit(editor_id);
|
|
this.editor.setTheme("ace/theme/monokai");
|
|
this.editor.getSession().setMode("ace/mode/javascript");
|
|
|
|
var script_content = _kiwi.global.settings.get('user_script') || '';
|
|
this.editor.setValue(script_content);
|
|
},
|
|
|
|
|
|
onSave: function (event) {
|
|
var script_content, user_fn;
|
|
|
|
// Build the user script up with some pre-defined components
|
|
script_content = 'var network = kiwi.components.Network();\n';
|
|
script_content += 'var input = kiwi.components.ControlInput();\n';
|
|
script_content += 'var events = kiwi.components.Events();\n';
|
|
script_content += this.editor.getValue() + '\n';
|
|
|
|
// Add a dispose method to the user script for cleaning up
|
|
script_content += 'this._dispose = function(){ network.off(); input.off(); events.dispose(); if(this.dispose) this.dispose(); }';
|
|
|
|
// Try to compile the user script
|
|
try {
|
|
user_fn = new Function(script_content);
|
|
|
|
// Dispose any existing user script
|
|
if (_kiwi.user_script && _kiwi.user_script._dispose)
|
|
_kiwi.user_script._dispose();
|
|
|
|
// Create and run the new user script
|
|
_kiwi.user_script = new user_fn();
|
|
|
|
} catch (err) {
|
|
this.setStatus(_kiwi.global.i18n.translate('client_applets_scripteditor_error').fetch(err.toString()));
|
|
return;
|
|
}
|
|
|
|
// If we're this far, no errors occured. Save the user script
|
|
_kiwi.global.settings.set('user_script', this.editor.getValue());
|
|
_kiwi.global.settings.save();
|
|
|
|
this.setStatus(_kiwi.global.i18n.translate('client_applets_scripteditor_saved').fetch() + ' :)');
|
|
},
|
|
|
|
|
|
setStatus: function (status_text) {
|
|
var $status = this.$el.find('.toolbar .status');
|
|
|
|
status_text = status_text || '';
|
|
$status.slideUp('fast', function() {
|
|
$status.text(status_text);
|
|
$status.slideDown();
|
|
});
|
|
}
|
|
});
|
|
|
|
|
|
|
|
var applet = Backbone.Model.extend({
|
|
initialize: function () {
|
|
var that = this;
|
|
|
|
this.set('title', _kiwi.global.i18n.translate('client_applets_scripteditor_title').fetch());
|
|
this.view = new view({model: this});
|
|
|
|
}
|
|
});
|
|
|
|
|
|
_kiwi.model.Applet.register('kiwi_script_editor', applet);
|
|
//_kiwi.model.Applet.loadOnce('kiwi_script_editor');
|
|
})();
|
|
|
|
|
|
(function () {
|
|
var view = Backbone.View.extend({
|
|
events: {},
|
|
|
|
|
|
initialize: function (options) {
|
|
this.showConnectionDialog();
|
|
},
|
|
|
|
|
|
showConnectionDialog: function() {
|
|
var connection_dialog = this.connection_dialog = new _kiwi.model.NewConnection();
|
|
connection_dialog.populateDefaultServerSettings();
|
|
|
|
connection_dialog.view.$el.addClass('initial');
|
|
this.$el.append(connection_dialog.view.$el);
|
|
|
|
var $info = $($('#tmpl_new_connection_info').html().trim());
|
|
|
|
if ($info.html()) {
|
|
connection_dialog.view.infoBoxSet($info);
|
|
} else {
|
|
$info = null;
|
|
}
|
|
|
|
this.listenTo(connection_dialog, 'connected', this.newConnectionConnected);
|
|
|
|
_.defer(function(){
|
|
if ($info) {
|
|
connection_dialog.view.infoBoxShow();
|
|
}
|
|
|
|
// Only set focus if we're not within an iframe. (firefox auto scrolls to the embedded client on page load - bad)
|
|
if (window == window.top) {
|
|
connection_dialog.view.$el.find('.nick').select();
|
|
}
|
|
});
|
|
},
|
|
|
|
|
|
newConnectionConnected: function(network) {
|
|
// Once connected, reset the connection form to be used again in future
|
|
this.connection_dialog.view.reset();
|
|
}
|
|
});
|
|
|
|
|
|
|
|
var applet = Backbone.Model.extend({
|
|
initialize: function () {
|
|
this.view = new view({model: this});
|
|
}
|
|
});
|
|
|
|
|
|
_kiwi.model.Applet.register('kiwi_startup', applet);
|
|
})();
|
|
|
|
|
|
|
|
_kiwi.utils.notifications = (function () {
|
|
if (!window.Notification) {
|
|
return {
|
|
allowed: _.constant(false),
|
|
requestPermission: _.constant($.Deferred().reject())
|
|
};
|
|
}
|
|
|
|
var notifications = {
|
|
/**
|
|
* Check if desktop notifications have been allowed by the user.
|
|
*
|
|
* @returns {?Boolean} `true` - they have been allowed.
|
|
* `false` - they have been blocked.
|
|
* `null` - the user hasn't answered yet.
|
|
*/
|
|
allowed: function () {
|
|
return Notification.permission === 'granted' ? true
|
|
: Notification.permission === 'denied' ? false
|
|
: null;
|
|
},
|
|
|
|
/**
|
|
* Ask the user their permission to display desktop notifications.
|
|
* This will return a promise which will be resolved if the user allows notifications, or rejected if they blocked
|
|
* notifictions or simply closed the dialog. If the user had previously given their preference, the promise will be
|
|
* immediately resolved or rejected with their previous answer.
|
|
*
|
|
* @example
|
|
* notifications.requestPermission().then(function () { 'allowed' }, function () { 'not allowed' });
|
|
*
|
|
* @returns {Promise}
|
|
*/
|
|
requestPermission: function () {
|
|
var deferred = $.Deferred();
|
|
Notification.requestPermission(function (permission) {
|
|
deferred[(permission === 'granted') ? 'resolve' : 'reject']();
|
|
});
|
|
return deferred.promise();
|
|
},
|
|
|
|
/**
|
|
* Create a new notification. If the user has not yet given permission to display notifications, they will be asked
|
|
* to confirm first. The notification will show afterwards if they allow it.
|
|
*
|
|
* Notifications implement Backbone.Events (so you can use `on` and `off`). They trigger four different events:
|
|
* - 'click'
|
|
* - 'close'
|
|
* - 'error'
|
|
* - 'show'
|
|
*
|
|
* @example
|
|
* notifications
|
|
* .create('Cool notification', { icon: 'logo.png' })
|
|
* .on('click', function () {
|
|
* window.focus();
|
|
* })
|
|
* .closeAfter(5000);
|
|
*
|
|
* @param {String} title
|
|
* @param {Object} options
|
|
* @param {String=} options.body A string representing an extra content to display within the notification
|
|
* @param {String=} options.dir The direction of the notification; it can be auto, ltr, or rtl
|
|
* @param {String=} options.lang Specify the lang used within the notification. This string must be a valid BCP
|
|
* 47 language tag.
|
|
* @param {String=} options.tag An ID for a given notification that allows to retrieve, replace or remove it if necessary
|
|
* @param {String=} options.icon The URL of an image to be used as an icon by the notification
|
|
* @returns {Notifier}
|
|
*/
|
|
create: function (title, options) {
|
|
return new Notifier(title, options);
|
|
}
|
|
};
|
|
|
|
function Notifier(title, options) {
|
|
createNotification.call(this, title, options);
|
|
}
|
|
_.extend(Notifier.prototype, Backbone.Events, {
|
|
closed: false,
|
|
_closeTimeout: null,
|
|
|
|
/**
|
|
* Close the notification after a given number of milliseconds.
|
|
* @param {Number} timeout
|
|
* @returns {this}
|
|
*/
|
|
closeAfter: function (timeout) {
|
|
if (!this.closed) {
|
|
if (this.notification) {
|
|
this._closeTimeout = this._closeTimeout || setTimeout(_.bind(this.close, this), timeout);
|
|
} else {
|
|
this.once('show', _.bind(this.closeAfter, this, timeout));
|
|
}
|
|
}
|
|
return this;
|
|
},
|
|
|
|
/**
|
|
* Close the notification immediately.
|
|
* @returns {this}
|
|
*/
|
|
close: function () {
|
|
if (this.notification && !this.closed) {
|
|
this.notification.close();
|
|
this.closed = true;
|
|
}
|
|
return this;
|
|
}
|
|
});
|
|
|
|
function createNotification(title, options) {
|
|
switch (notifications.allowed()) {
|
|
case true:
|
|
this.notification = new Notification(title, options);
|
|
_.each(['click', 'close', 'error', 'show'], function (eventName) {
|
|
this.notification['on' + eventName] = _.bind(this.trigger, this, eventName);
|
|
}, this);
|
|
break;
|
|
case null:
|
|
notifications.requestPermission().done(_.bind(createNotification, this, title, options));
|
|
break;
|
|
}
|
|
}
|
|
|
|
return notifications;
|
|
}());
|
|
|
|
|
|
|
|
_kiwi.utils.formatDate = (function() {
|
|
/*
|
|
Modified version of date.format.js
|
|
https://github.com/jacwright/date.format
|
|
*/
|
|
var locale_init = false, // Once the loales have been loaded, this is set to true
|
|
shortMonths, longMonths, shortDays, longDays;
|
|
|
|
// defining patterns
|
|
var replaceChars = {
|
|
// Day
|
|
d: function() { return (this.getDate() < 10 ? '0' : '') + this.getDate(); },
|
|
D: function() { return Date.shortDays[this.getDay()]; },
|
|
j: function() { return this.getDate(); },
|
|
l: function() { return Date.longDays[this.getDay()]; },
|
|
N: function() { return this.getDay() + 1; },
|
|
S: function() { return (this.getDate() % 10 == 1 && this.getDate() != 11 ? 'st' : (this.getDate() % 10 == 2 && this.getDate() != 12 ? 'nd' : (this.getDate() % 10 == 3 && this.getDate() != 13 ? 'rd' : 'th'))); },
|
|
w: function() { return this.getDay(); },
|
|
z: function() { var d = new Date(this.getFullYear(),0,1); return Math.ceil((this - d) / 86400000); }, // Fixed now
|
|
// Week
|
|
W: function() { var d = new Date(this.getFullYear(), 0, 1); return Math.ceil((((this - d) / 86400000) + d.getDay() + 1) / 7); }, // Fixed now
|
|
// Month
|
|
F: function() { return Date.longMonths[this.getMonth()]; },
|
|
m: function() { return (this.getMonth() < 9 ? '0' : '') + (this.getMonth() + 1); },
|
|
M: function() { return Date.shortMonths[this.getMonth()]; },
|
|
n: function() { return this.getMonth() + 1; },
|
|
t: function() { var d = new Date(); return new Date(d.getFullYear(), d.getMonth(), 0).getDate(); }, // Fixed now, gets #days of date
|
|
// Year
|
|
L: function() { var year = this.getFullYear(); return (year % 400 == 0 || (year % 100 != 0 && year % 4 == 0)); }, // Fixed now
|
|
o: function() { var d = new Date(this.valueOf()); d.setDate(d.getDate() - ((this.getDay() + 6) % 7) + 3); return d.getFullYear();}, //Fixed now
|
|
Y: function() { return this.getFullYear(); },
|
|
y: function() { return ('' + this.getFullYear()).substr(2); },
|
|
// Time
|
|
a: function() { return this.getHours() < 12 ? 'am' : 'pm'; },
|
|
A: function() { return this.getHours() < 12 ? 'AM' : 'PM'; },
|
|
B: function() { return Math.floor((((this.getUTCHours() + 1) % 24) + this.getUTCMinutes() / 60 + this.getUTCSeconds() / 3600) * 1000 / 24); }, // Fixed now
|
|
g: function() { return this.getHours() % 12 || 12; },
|
|
G: function() { return this.getHours(); },
|
|
h: function() { return ((this.getHours() % 12 || 12) < 10 ? '0' : '') + (this.getHours() % 12 || 12); },
|
|
H: function() { return (this.getHours() < 10 ? '0' : '') + this.getHours(); },
|
|
i: function() { return (this.getMinutes() < 10 ? '0' : '') + this.getMinutes(); },
|
|
s: function() { return (this.getSeconds() < 10 ? '0' : '') + this.getSeconds(); },
|
|
u: function() { var m = this.getMilliseconds(); return (m < 10 ? '00' : (m < 100 ? '0' : '')) + m; },
|
|
// Timezone
|
|
e: function() { return "Not Yet Supported"; },
|
|
I: function() {
|
|
var DST = null;
|
|
for (var i = 0; i < 12; ++i) {
|
|
var d = new Date(this.getFullYear(), i, 1);
|
|
var offset = d.getTimezoneOffset();
|
|
|
|
if (DST === null) DST = offset;
|
|
else if (offset < DST) { DST = offset; break; }
|
|
else if (offset > DST) break;
|
|
}
|
|
return (this.getTimezoneOffset() == DST) | 0;
|
|
},
|
|
O: function() { return (-this.getTimezoneOffset() < 0 ? '-' : '+') + (Math.abs(this.getTimezoneOffset() / 60) < 10 ? '0' : '') + (Math.abs(this.getTimezoneOffset() / 60)) + '00'; },
|
|
P: function() { return (-this.getTimezoneOffset() < 0 ? '-' : '+') + (Math.abs(this.getTimezoneOffset() / 60) < 10 ? '0' : '') + (Math.abs(this.getTimezoneOffset() / 60)) + ':00'; }, // Fixed now
|
|
T: function() { var m = this.getMonth(); this.setMonth(0); var result = this.toTimeString().replace(/^.+ \(?([^\)]+)\)?$/, '$1'); this.setMonth(m); return result;},
|
|
Z: function() { return -this.getTimezoneOffset() * 60; },
|
|
// Full Date/Time
|
|
c: function() { return this.format("Y-m-d\\TH:i:sP"); }, // Fixed now
|
|
r: function() { return this.toString(); },
|
|
U: function() { return this.getTime() / 1000; }
|
|
};
|
|
|
|
|
|
var initLocaleFormats = function() {
|
|
shortMonths = [
|
|
_kiwi.global.i18n.translate('client.libs.date_format.short_months.january').fetch(),
|
|
_kiwi.global.i18n.translate('client.libs.date_format.short_months.february').fetch(),
|
|
_kiwi.global.i18n.translate('client.libs.date_format.short_months.march').fetch(),
|
|
_kiwi.global.i18n.translate('client.libs.date_format.short_months.april').fetch(),
|
|
_kiwi.global.i18n.translate('client.libs.date_format.short_months.may').fetch(),
|
|
_kiwi.global.i18n.translate('client.libs.date_format.short_months.june').fetch(),
|
|
_kiwi.global.i18n.translate('client.libs.date_format.short_months.july').fetch(),
|
|
_kiwi.global.i18n.translate('client.libs.date_format.short_months.august').fetch(),
|
|
_kiwi.global.i18n.translate('client.libs.date_format.short_months.september').fetch(),
|
|
_kiwi.global.i18n.translate('client.libs.date_format.short_months.october').fetch(),
|
|
_kiwi.global.i18n.translate('client.libs.date_format.short_months.november').fetch(),
|
|
_kiwi.global.i18n.translate('client.libs.date_format.short_months.december').fetch()
|
|
];
|
|
longMonths = [
|
|
_kiwi.global.i18n.translate('client.libs.date_format.long_months.january').fetch(),
|
|
_kiwi.global.i18n.translate('client.libs.date_format.long_months.february').fetch(),
|
|
_kiwi.global.i18n.translate('client.libs.date_format.long_months.march').fetch(),
|
|
_kiwi.global.i18n.translate('client.libs.date_format.long_months.april').fetch(),
|
|
_kiwi.global.i18n.translate('client.libs.date_format.long_months.may').fetch(),
|
|
_kiwi.global.i18n.translate('client.libs.date_format.long_months.june').fetch(),
|
|
_kiwi.global.i18n.translate('client.libs.date_format.long_months.july').fetch(),
|
|
_kiwi.global.i18n.translate('client.libs.date_format.long_months.august').fetch(),
|
|
_kiwi.global.i18n.translate('client.libs.date_format.long_months.september').fetch(),
|
|
_kiwi.global.i18n.translate('client.libs.date_format.long_months.october').fetch(),
|
|
_kiwi.global.i18n.translate('client.libs.date_format.long_months.november').fetch(),
|
|
_kiwi.global.i18n.translate('client.libs.date_format.long_months.december').fetch()
|
|
];
|
|
shortDays = [
|
|
_kiwi.global.i18n.translate('client.libs.date_format.short_days.monday').fetch(),
|
|
_kiwi.global.i18n.translate('client.libs.date_format.short_days.tuesday').fetch(),
|
|
_kiwi.global.i18n.translate('client.libs.date_format.short_days.wednesday').fetch(),
|
|
_kiwi.global.i18n.translate('client.libs.date_format.short_days.thursday').fetch(),
|
|
_kiwi.global.i18n.translate('client.libs.date_format.short_days.friday').fetch(),
|
|
_kiwi.global.i18n.translate('client.libs.date_format.short_days.saturday').fetch(),
|
|
_kiwi.global.i18n.translate('client.libs.date_format.short_days.sunday').fetch()
|
|
];
|
|
longDays = [
|
|
_kiwi.global.i18n.translate('client.libs.date_format.long_days.monday').fetch(),
|
|
_kiwi.global.i18n.translate('client.libs.date_format.long_days.tuesday').fetch(),
|
|
_kiwi.global.i18n.translate('client.libs.date_format.long_days.wednesday').fetch(),
|
|
_kiwi.global.i18n.translate('client.libs.date_format.long_days.thursday').fetch(),
|
|
_kiwi.global.i18n.translate('client.libs.date_format.long_days.friday').fetch(),
|
|
_kiwi.global.i18n.translate('client.libs.date_format.long_days.saturday').fetch(),
|
|
_kiwi.global.i18n.translate('client.libs.date_format.long_days.sunday').fetch()
|
|
];
|
|
|
|
locale_init = true;
|
|
};
|
|
/* End of date.format */
|
|
|
|
|
|
// Finally.. the actuall formatDate function
|
|
return function(working_date, format) {
|
|
if (!locale_init)
|
|
initLocaleFormats();
|
|
|
|
working_date = working_date || new Date();
|
|
format = format || _kiwi.global.i18n.translate('client_date_format').fetch();
|
|
|
|
return format.replace(/(\\?)(.)/g, function(_, esc, chr) {
|
|
return (esc === '' && replaceChars[chr]) ? replaceChars[chr].call(working_date) : chr;
|
|
});
|
|
};
|
|
})();
|
|
|
|
|
|
/*
|
|
* The same functionality as EventEmitter but with the inclusion of callbacks
|
|
*/
|
|
|
|
|
|
|
|
function PluginInterface () {
|
|
// Holder for all the bound listeners by this module
|
|
this._listeners = {};
|
|
|
|
// Event proxies
|
|
this._parent = null;
|
|
this._children = [];
|
|
}
|
|
|
|
|
|
|
|
PluginInterface.prototype.on = function (event_name, fn, scope) {
|
|
this._listeners[event_name] = this._listeners[event_name] || [];
|
|
this._listeners[event_name].push(['on', fn, scope]);
|
|
};
|
|
|
|
|
|
|
|
PluginInterface.prototype.once = function (event_name, fn, scope) {
|
|
this._listeners[event_name] = this._listeners[event_name] || [];
|
|
this._listeners[event_name].push(['once', fn, scope]);
|
|
};
|
|
|
|
|
|
|
|
PluginInterface.prototype.off = function (event_name, fn, scope) {
|
|
var idx;
|
|
|
|
if (typeof event_name === 'undefined') {
|
|
// Remove all listeners
|
|
this._listeners = {};
|
|
|
|
} else if (typeof fn === 'undefined') {
|
|
// Remove all of 1 event type
|
|
delete this._listeners[event_name];
|
|
|
|
} else if (typeof scope === 'undefined') {
|
|
// Remove a single event type + callback
|
|
for (idx=0; idx<(this._listeners[event_name] || []).length; idx++) {
|
|
if (this._listeners[event_name][idx][1] === fn) {
|
|
//delete this._listeners[event_name][idx];
|
|
this._listeners[event_name].splice(idx, 1);
|
|
idx--;
|
|
}
|
|
}
|
|
} else {
|
|
// Remove a single event type + callback + scope
|
|
for (idx=0; idx<(this._listeners[event_name] || []).length; idx++) {
|
|
if (this._listeners[event_name][idx][1] === fn && this._listeners[event_name][idx][2] === scope) {
|
|
//delete this._listeners[event_name][idx];
|
|
this._listeners[event_name].splice(idx, 1);
|
|
idx--;
|
|
}
|
|
}
|
|
}
|
|
};
|
|
|
|
|
|
|
|
PluginInterface.prototype.getListeners = function(event_name) {
|
|
return this._listeners[event_name] || [];
|
|
};
|
|
|
|
|
|
|
|
PluginInterface.prototype.createProxy = function() {
|
|
var proxy = new PluginInterface();
|
|
proxy._parent = this._parent || this;
|
|
proxy._parent._children.push(proxy);
|
|
|
|
return proxy;
|
|
};
|
|
|
|
|
|
|
|
PluginInterface.prototype.dispose = function() {
|
|
this.off();
|
|
|
|
if (this._parent) {
|
|
var idx = this._parent._children.indexOf(this);
|
|
if (idx > -1) {
|
|
this._parent._children.splice(idx, 1);
|
|
}
|
|
}
|
|
};
|
|
|
|
|
|
|
|
// Call all the listeners for a certain event, passing them some event data that may be changed
|
|
PluginInterface.prototype.emit = function (event_name, event_data) {
|
|
var emitter = new this.EmitCall(event_name, event_data),
|
|
listeners = [],
|
|
child_idx;
|
|
|
|
// Get each childs event listeners in order of last created
|
|
for(child_idx=this._children.length-1; child_idx>=0; child_idx--) {
|
|
listeners = listeners.concat(this._children[child_idx].getListeners(event_name));
|
|
}
|
|
|
|
// Now include any listeners directly on this instance
|
|
listeners = listeners.concat(this.getListeners(event_name));
|
|
|
|
// Once emitted, remove any 'once' bound listeners
|
|
emitter.then(function () {
|
|
var len = listeners.length,
|
|
idx;
|
|
|
|
for(idx = 0; idx < len; idx++) {
|
|
if (listeners[idx][0] === 'once') {
|
|
listeners[idx] = undefined;
|
|
}
|
|
}
|
|
});
|
|
|
|
// Emit the event to the listeners and return
|
|
emitter.callListeners(listeners);
|
|
return emitter;
|
|
};
|
|
|
|
|
|
|
|
// Promise style object to emit events to listeners
|
|
PluginInterface.prototype.EmitCall = function EmitCall (event_name, event_data) {
|
|
var that = this,
|
|
completed = false,
|
|
completed_fn = [],
|
|
|
|
// Has event.preventDefault() been called
|
|
prevented = false,
|
|
prevented_fn = [];
|
|
|
|
|
|
// Emit this event to an array of listeners
|
|
function callListeners(listeners) {
|
|
var current_event_idx = -1;
|
|
|
|
// Make sure we have some data to pass to the listeners
|
|
event_data = event_data || undefined;
|
|
|
|
// If no bound listeners for this event, leave now
|
|
if (listeners.length === 0) {
|
|
emitComplete();
|
|
return;
|
|
}
|
|
|
|
|
|
// Call the next listener in our array
|
|
function nextListener() {
|
|
var listener, event_obj;
|
|
|
|
// We want the next listener
|
|
current_event_idx++;
|
|
|
|
// If we've ran out of listeners end this emit call
|
|
if (!listeners[current_event_idx]) {
|
|
emitComplete();
|
|
return;
|
|
}
|
|
|
|
// Object the listener ammends to tell us what it's going to do
|
|
event_obj = {
|
|
// If changed to true, expect this listener is going to callback
|
|
wait: false,
|
|
|
|
// If wait is true, this callback must be called to continue running listeners
|
|
callback: function () {
|
|
// Invalidate this callback incase a listener decides to call it again
|
|
event_obj.callback = undefined;
|
|
|
|
nextListener.apply(that);
|
|
},
|
|
|
|
// Prevents the default 'done' functions from executing
|
|
preventDefault: function () {
|
|
prevented = true;
|
|
}
|
|
};
|
|
|
|
|
|
listener = listeners[current_event_idx];
|
|
listener[1].call(listener[2] || that, event_obj, event_data);
|
|
|
|
// If the listener hasn't signalled it's going to wait, proceed to next listener
|
|
if (!event_obj.wait) {
|
|
// Invalidate the callback just incase a listener decides to call it anyway
|
|
event_obj.callback = undefined;
|
|
|
|
nextListener();
|
|
}
|
|
}
|
|
|
|
nextListener();
|
|
}
|
|
|
|
|
|
|
|
function emitComplete() {
|
|
completed = true;
|
|
|
|
var funcs = prevented ? prevented_fn : completed_fn;
|
|
funcs = funcs || [];
|
|
|
|
// Call the completed/prevented functions
|
|
for (var idx = 0; idx < funcs.length; idx++) {
|
|
if (typeof funcs[idx] === 'function') funcs[idx]();
|
|
}
|
|
}
|
|
|
|
|
|
|
|
function addCompletedFunc(fn) {
|
|
// Only accept functions
|
|
if (typeof fn !== 'function') return false;
|
|
|
|
completed_fn.push(fn);
|
|
|
|
// If we have already completed the emits, call this now
|
|
if (completed && !prevented) fn();
|
|
|
|
return this;
|
|
}
|
|
|
|
|
|
|
|
function addPreventedFunc(fn) {
|
|
// Only accept functions
|
|
if (typeof fn !== 'function') return false;
|
|
|
|
prevented_fn.push(fn);
|
|
|
|
// If we have already completed the emits, call this now
|
|
if (completed && prevented) fn();
|
|
|
|
return this;
|
|
}
|
|
|
|
|
|
return {
|
|
callListeners: callListeners,
|
|
then: addCompletedFunc,
|
|
catch: addPreventedFunc
|
|
};
|
|
};
|
|
|
|
|
|
|
|
// If running a node module, set the exports
|
|
if (typeof module === 'object' && typeof module.exports !== 'undefined') {
|
|
module.exports = PluginInterface;
|
|
}
|
|
|
|
|
|
|
|
/*
|
|
* Example usage
|
|
*/
|
|
|
|
|
|
/*
|
|
var modules = new PluginInterface();
|
|
|
|
|
|
|
|
// A plugin
|
|
modules.on('irc message', function (event, data) {
|
|
//event.wait = true;
|
|
setTimeout(event.callback, 2000);
|
|
});
|
|
|
|
|
|
|
|
|
|
// Core code that is being extended by plugins
|
|
var data = {
|
|
nick: 'prawnsalald',
|
|
command: '/dothis'
|
|
};
|
|
|
|
modules.emit('irc message', data).done(function () {
|
|
console.log('Your command is: ' + data.command);
|
|
});
|
|
*/
|
|
|
|
|
|
/*jslint devel: true, browser: true, continue: true, sloppy: true, forin: true, plusplus: true, maxerr: 50, indent: 4, nomen: true, regexp: true*/
|
|
/*globals $, front, gateway, Utilityview */
|
|
|
|
|
|
|
|
/**
|
|
* Generate a random string of given length
|
|
* @param {Number} string_length The length of the random string
|
|
* @returns {String} The random string
|
|
*/
|
|
function randomString(string_length) {
|
|
var chars = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXTZabcdefghiklmnopqrstuvwxyz",
|
|
randomstring = '',
|
|
i,
|
|
rnum;
|
|
for (i = 0; i < string_length; i++) {
|
|
rnum = Math.floor(Math.random() * chars.length);
|
|
randomstring += chars.substring(rnum, rnum + 1);
|
|
}
|
|
return randomstring;
|
|
}
|
|
|
|
/**
|
|
* String.trim shim
|
|
*/
|
|
if (typeof String.prototype.trim === 'undefined') {
|
|
String.prototype.trim = function () {
|
|
return this.replace(/^\s+|\s+$/g, "");
|
|
};
|
|
}
|
|
|
|
/**
|
|
* String.lpad shim
|
|
* @param {Number} length The length of padding
|
|
* @param {String} characher The character to pad with
|
|
* @returns {String} The padded string
|
|
*/
|
|
if (typeof String.prototype.lpad === 'undefined') {
|
|
String.prototype.lpad = function (length, character) {
|
|
var padding = "",
|
|
i;
|
|
for (i = 0; i < length; i++) {
|
|
padding += character;
|
|
}
|
|
return (padding + this).slice(-length);
|
|
};
|
|
}
|
|
|
|
|
|
/**
|
|
* Convert seconds into hours:minutes:seconds
|
|
* @param {Number} secs The number of seconds to converts
|
|
* @returns {Object} An object representing the hours/minutes/second conversion of secs
|
|
*/
|
|
function secondsToTime(secs) {
|
|
var hours, minutes, seconds, divisor_for_minutes, divisor_for_seconds, obj;
|
|
hours = Math.floor(secs / (60 * 60));
|
|
|
|
divisor_for_minutes = secs % (60 * 60);
|
|
minutes = Math.floor(divisor_for_minutes / 60);
|
|
|
|
divisor_for_seconds = divisor_for_minutes % 60;
|
|
seconds = Math.ceil(divisor_for_seconds);
|
|
|
|
obj = {
|
|
"h": hours,
|
|
"m": minutes,
|
|
"s": seconds
|
|
};
|
|
return obj;
|
|
}
|
|
|
|
|
|
/* Set or get the caret position or selection range of inputs and textareas */
|
|
$.fn.selectRange = function(start, end) {
|
|
var e = $(this)[0];
|
|
if (!e) return;
|
|
|
|
if (typeof start === 'undefined') {
|
|
var caret_pos = 0;
|
|
|
|
if (document.selection) {
|
|
var sel = document.selection.createRange ();
|
|
sel.moveStart ('character', -e.value.length);
|
|
caret_pos = sel.text.length;
|
|
} else if (e.selectionStart || e.selectionStart == '0') {
|
|
caret_pos = e.selectionStart;
|
|
}
|
|
|
|
return caret_pos;
|
|
|
|
} else {
|
|
if (typeof end === 'undefined') {
|
|
end = start;
|
|
}
|
|
|
|
// WebKit
|
|
if (e.setSelectionRange) {
|
|
e.focus();
|
|
e.setSelectionRange(start, end);
|
|
}
|
|
// IE
|
|
else if (e.createTextRange) {
|
|
var range = e.createTextRange();
|
|
range.collapse(true);
|
|
range.moveEnd('character', end);
|
|
range.moveStart('character', start);
|
|
range.select();
|
|
}
|
|
else if (e.selectionStart) {
|
|
e.selectionStart = start;
|
|
e.selectionEnd = end;
|
|
}
|
|
}
|
|
};
|
|
|
|
|
|
/* Command input Alias + re-writing */
|
|
function InputPreProcessor () {
|
|
this.recursive_depth = 3;
|
|
|
|
this.aliases = {};
|
|
this.vars = {version: 1};
|
|
|
|
// Current recursive depth
|
|
var depth = 0;
|
|
|
|
|
|
// Takes an array of words to process!
|
|
this.processInput = function (input) {
|
|
var words = input || [],
|
|
alias = this.aliases[words[0].toLowerCase()],
|
|
alias_len,
|
|
current_alias_word = '',
|
|
compiled = [];
|
|
|
|
// If an alias wasn't found, return the original input
|
|
if (!alias) return input;
|
|
|
|
// Split the alias up into useable words
|
|
alias = alias.split(' ');
|
|
alias_len = alias.length;
|
|
|
|
// Iterate over each word and pop them into the final compiled array.
|
|
// Any $ words are processed with the result ending into the compiled array.
|
|
for (var i=0; i<alias_len; i++) {
|
|
current_alias_word = alias[i];
|
|
|
|
// Non $ word
|
|
if (current_alias_word[0] !== '$') {
|
|
compiled.push(current_alias_word);
|
|
continue;
|
|
}
|
|
|
|
// Refering to an input word ($N)
|
|
if (!isNaN(current_alias_word[1])) {
|
|
var num = current_alias_word.match(/\$(\d+)(\+)?(\d+)?/);
|
|
|
|
// Did we find anything or does the word it refers to non-existant?
|
|
if (!num || !words[num[1]]) continue;
|
|
|
|
if (num[2] === '+' && num[3]) {
|
|
// Add X number of words
|
|
compiled = compiled.concat(words.slice(parseInt(num[1], 10), parseInt(num[1], 10) + parseInt(num[3], 10)));
|
|
} else if (num[2] === '+') {
|
|
// Add the remaining of the words
|
|
compiled = compiled.concat(words.slice(parseInt(num[1], 10)));
|
|
} else {
|
|
// Add a single word
|
|
compiled.push(words[parseInt(num[1], 10)]);
|
|
}
|
|
|
|
continue;
|
|
}
|
|
|
|
|
|
// Refering to a variable
|
|
if (typeof this.vars[current_alias_word.substr(1)] !== 'undefined') {
|
|
|
|
// Get the variable
|
|
compiled.push(this.vars[current_alias_word.substr(1)]);
|
|
|
|
continue;
|
|
}
|
|
|
|
}
|
|
|
|
return compiled;
|
|
};
|
|
|
|
|
|
this.process = function (input) {
|
|
input = input || '';
|
|
|
|
var words = input.split(' ');
|
|
var first_word = (words[0] || '').toLowerCase();
|
|
|
|
depth++;
|
|
if (depth >= this.recursive_depth) {
|
|
depth--;
|
|
return input;
|
|
}
|
|
|
|
if (this.aliases[first_word]) {
|
|
words = this.processInput(words);
|
|
first_word = (words[0] || '').toLowerCase();
|
|
|
|
if (this.aliases[first_word]) {
|
|
words = this.process(words.join(' ')).split(' ');
|
|
}
|
|
|
|
}
|
|
|
|
depth--;
|
|
return words.join(' ');
|
|
};
|
|
}
|
|
|
|
|
|
/**
|
|
* Convert HSL to RGB formatted colour
|
|
*/
|
|
function hsl2rgb(h, s, l) {
|
|
var m1, m2, hue;
|
|
var r, g, b
|
|
s /=100;
|
|
l /= 100;
|
|
if (s == 0)
|
|
r = g = b = (l * 255);
|
|
else {
|
|
function HueToRgb(m1, m2, hue) {
|
|
var v;
|
|
if (hue < 0)
|
|
hue += 1;
|
|
else if (hue > 1)
|
|
hue -= 1;
|
|
|
|
if (6 * hue < 1)
|
|
v = m1 + (m2 - m1) * hue * 6;
|
|
else if (2 * hue < 1)
|
|
v = m2;
|
|
else if (3 * hue < 2)
|
|
v = m1 + (m2 - m1) * (2/3 - hue) * 6;
|
|
else
|
|
v = m1;
|
|
|
|
return 255 * v;
|
|
}
|
|
if (l <= 0.5)
|
|
m2 = l * (s + 1);
|
|
else
|
|
m2 = l + s - l * s;
|
|
m1 = l * 2 - m2;
|
|
hue = h / 360;
|
|
r = HueToRgb(m1, m2, hue + 1/3);
|
|
g = HueToRgb(m1, m2, hue);
|
|
b = HueToRgb(m1, m2, hue - 1/3);
|
|
}
|
|
return [r,g,b];
|
|
}
|
|
|
|
|
|
/**
|
|
* Formats a kiwi message to IRC format
|
|
*/
|
|
function formatToIrcMsg(message) {
|
|
// Format any colour codes (eg. $c4)
|
|
message = message.replace(/%C(\d)/g, function(match, colour_number) {
|
|
return String.fromCharCode(3) + colour_number.toString();
|
|
});
|
|
|
|
var formatters = {
|
|
B: '\x02', // Bold
|
|
I: '\x1D', // Italics
|
|
U: '\x1F', // Underline
|
|
O: '\x0F' // Out / Clear formatting
|
|
};
|
|
message = message.replace(/%([BIUO])/g, function(match, format_code) {
|
|
if (typeof formatters[format_code.toUpperCase()] !== 'undefined')
|
|
return formatters[format_code.toUpperCase()];
|
|
});
|
|
|
|
return message;
|
|
}
|
|
|
|
|
|
/**
|
|
* Formats a message. Adds bold, underline and colouring
|
|
* @param {String} msg The message to format
|
|
* @returns {String} The HTML formatted message
|
|
*/
|
|
function formatIRCMsg (msg) {
|
|
"use strict";
|
|
var out = '',
|
|
currentTag = '',
|
|
openTags = {
|
|
bold: false,
|
|
italic: false,
|
|
underline: false,
|
|
colour: false
|
|
},
|
|
spanFromOpen = function () {
|
|
var style = '',
|
|
colours;
|
|
if (!(openTags.bold || openTags.italic || openTags.underline || openTags.colour)) {
|
|
return '';
|
|
} else {
|
|
style += (openTags.bold) ? 'font-weight: bold; ' : '';
|
|
style += (openTags.italic) ? 'font-style: italic; ' : '';
|
|
style += (openTags.underline) ? 'text-decoration: underline; ' : '';
|
|
if (openTags.colour) {
|
|
colours = openTags.colour.split(',');
|
|
style += 'color: ' + colours[0] + ((colours[1]) ? '; background-color: ' + colours[1] + ';' : '');
|
|
}
|
|
return '<span class="format_span" style="' + style + '">';
|
|
}
|
|
},
|
|
colourMatch = function (str) {
|
|
var re = /^\x03(([0-9][0-9]?)(,([0-9][0-9]?))?)/;
|
|
return re.exec(str);
|
|
},
|
|
hexFromNum = function (num) {
|
|
switch (parseInt(num, 10)) {
|
|
case 0:
|
|
return '#FFFFFF';
|
|
case 1:
|
|
return '#000000';
|
|
case 2:
|
|
return '#000080';
|
|
case 3:
|
|
return '#008000';
|
|
case 4:
|
|
return '#FF0000';
|
|
case 5:
|
|
return '#800040';
|
|
case 6:
|
|
return '#800080';
|
|
case 7:
|
|
return '#FF8040';
|
|
case 8:
|
|
return '#FFFF00';
|
|
case 9:
|
|
return '#80FF00';
|
|
case 10:
|
|
return '#008080';
|
|
case 11:
|
|
return '#00FFFF';
|
|
case 12:
|
|
return '#0000FF';
|
|
case 13:
|
|
return '#FF55FF';
|
|
case 14:
|
|
return '#808080';
|
|
case 15:
|
|
return '#C0C0C0';
|
|
default:
|
|
return null;
|
|
}
|
|
},
|
|
i = 0,
|
|
colours = [],
|
|
match;
|
|
|
|
for (i = 0; i < msg.length; i++) {
|
|
switch (msg[i]) {
|
|
case '\x02':
|
|
if ((openTags.bold || openTags.italic || openTags.underline || openTags.colour)) {
|
|
out += currentTag + '</span>';
|
|
}
|
|
openTags.bold = !openTags.bold;
|
|
currentTag = spanFromOpen();
|
|
break;
|
|
case '\x1D':
|
|
if ((openTags.bold || openTags.italic || openTags.underline || openTags.colour)) {
|
|
out += currentTag + '</span>';
|
|
}
|
|
openTags.italic = !openTags.italic;
|
|
currentTag = spanFromOpen();
|
|
break;
|
|
case '\x1F':
|
|
if ((openTags.bold || openTags.italic || openTags.underline || openTags.colour)) {
|
|
out += currentTag + '</span>';
|
|
}
|
|
openTags.underline = !openTags.underline;
|
|
currentTag = spanFromOpen();
|
|
break;
|
|
case '\x03':
|
|
if ((openTags.bold || openTags.italic || openTags.underline || openTags.colour)) {
|
|
out += currentTag + '</span>';
|
|
}
|
|
match = colourMatch(msg.substr(i, 6));
|
|
if (match) {
|
|
i += match[1].length;
|
|
// 2 & 4
|
|
colours[0] = hexFromNum(match[2]);
|
|
if (match[4]) {
|
|
colours[1] = hexFromNum(match[4]);
|
|
}
|
|
openTags.colour = colours.join(',');
|
|
} else {
|
|
openTags.colour = false;
|
|
}
|
|
currentTag = spanFromOpen();
|
|
break;
|
|
case '\x0F':
|
|
if ((openTags.bold || openTags.italic || openTags.underline || openTags.colour)) {
|
|
out += currentTag + '</span>';
|
|
}
|
|
openTags.bold = openTags.italic = openTags.underline = openTags.colour = false;
|
|
break;
|
|
default:
|
|
if ((openTags.bold || openTags.italic || openTags.underline || openTags.colour)) {
|
|
currentTag += msg[i];
|
|
} else {
|
|
out += msg[i];
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
if ((openTags.bold || openTags.italic || openTags.underline || openTags.colour)) {
|
|
out += currentTag + '</span>';
|
|
}
|
|
return out;
|
|
}
|
|
|
|
function escapeRegex (str) {
|
|
return str.replace(/[\[\]\\\^\$\.\|\?\*\+\-\(\)]/g, '\\$&');
|
|
}
|
|
|
|
function emoticonFromText(str) {
|
|
var words_in = str.split(' '),
|
|
words_out = [],
|
|
i,
|
|
pushEmoticon = function (alt, emote_name) {
|
|
words_out.push('<i class="emoticon ' + emote_name + '">' + alt + '</i>');
|
|
};
|
|
|
|
for (i = 0; i < words_in.length; i++) {
|
|
switch(words_in[i]) {
|
|
case ':)':
|
|
pushEmoticon(':)', 'smile');
|
|
break;
|
|
case ':(':
|
|
pushEmoticon(':(', 'sad');
|
|
break;
|
|
case ':3':
|
|
pushEmoticon(':3', 'lion');
|
|
break;
|
|
case ';3':
|
|
pushEmoticon(';3', 'winky_lion');
|
|
break;
|
|
case ':s':
|
|
case ':S':
|
|
pushEmoticon(':s', 'confused');
|
|
break;
|
|
case ';(':
|
|
case ';_;':
|
|
pushEmoticon(';(', 'cry');
|
|
break;
|
|
case ';)':
|
|
pushEmoticon(';)', 'wink');
|
|
break;
|
|
case ';D':
|
|
pushEmoticon(';D', 'wink_happy');
|
|
break;
|
|
case ':P':
|
|
case ':p':
|
|
pushEmoticon(':P', 'tongue');
|
|
break;
|
|
case 'xP':
|
|
pushEmoticon('xP', 'cringe_tongue');
|
|
break;
|
|
case ':o':
|
|
case ':O':
|
|
case ':0':
|
|
pushEmoticon(':o', 'shocked');
|
|
break;
|
|
case ':D':
|
|
pushEmoticon(':D', 'happy');
|
|
break;
|
|
case '^^':
|
|
case '^.^':
|
|
pushEmoticon('^^,', 'eyebrows');
|
|
break;
|
|
case '<3':
|
|
pushEmoticon('<3', 'heart');
|
|
break;
|
|
case '>_<':
|
|
case '>.<':
|
|
pushEmoticon('>_<', 'doh');
|
|
break;
|
|
case 'XD':
|
|
case 'xD':
|
|
pushEmoticon('xD', 'big_grin');
|
|
break;
|
|
case 'o.0':
|
|
case 'o.O':
|
|
pushEmoticon('o.0', 'wide_eye_right');
|
|
break;
|
|
case '0.o':
|
|
case 'O.o':
|
|
pushEmoticon('0.o', 'wide_eye_left');
|
|
break;
|
|
case ':\\':
|
|
case '=\\':
|
|
case ':/':
|
|
case '=/':
|
|
pushEmoticon(':\\', 'unsure');
|
|
break;
|
|
default:
|
|
words_out.push(words_in[i]);
|
|
}
|
|
}
|
|
|
|
return words_out.join(' ');
|
|
}
|
|
|
|
// Code based on http://anentropic.wordpress.com/2009/06/25/javascript-iso8601-parser-and-pretty-dates/#comment-154
|
|
function parseISO8601(str) {
|
|
if (Date.prototype.toISOString) {
|
|
return new Date(str);
|
|
} else {
|
|
var parts = str.split('T'),
|
|
dateParts = parts[0].split('-'),
|
|
timeParts = parts[1].split('Z'),
|
|
timeSubParts = timeParts[0].split(':'),
|
|
timeSecParts = timeSubParts[2].split('.'),
|
|
timeHours = Number(timeSubParts[0]),
|
|
_date = new Date();
|
|
|
|
_date.setUTCFullYear(Number(dateParts[0]));
|
|
_date.setUTCDate(1);
|
|
_date.setUTCMonth(Number(dateParts[1])-1);
|
|
_date.setUTCDate(Number(dateParts[2]));
|
|
_date.setUTCHours(Number(timeHours));
|
|
_date.setUTCMinutes(Number(timeSubParts[1]));
|
|
_date.setUTCSeconds(Number(timeSecParts[0]));
|
|
if (timeSecParts[1]) {
|
|
_date.setUTCMilliseconds(Number(timeSecParts[1]));
|
|
}
|
|
|
|
return _date;
|
|
}
|
|
}
|
|
|
|
// Simplyfy the translation syntax
|
|
function translateText(string_id, params) {
|
|
params = params || '';
|
|
|
|
return _kiwi.global.i18n.translate(string_id).fetch(params);
|
|
}
|
|
|
|
/**
|
|
* Simplyfy the text styling syntax
|
|
*
|
|
* Syntax:
|
|
* %nick: nickname
|
|
* %channel: channel
|
|
* %ident: ident
|
|
* %host: host
|
|
* %realname: realname
|
|
* %text: translated text
|
|
* %C[digit]: color
|
|
* %B: bold
|
|
* %I: italic
|
|
* %U: underline
|
|
* %O: cancel styles
|
|
**/
|
|
function styleText(string_id, params) {
|
|
var style, text;
|
|
|
|
//style = formatToIrcMsg(_kiwi.app.text_theme[string_id]);
|
|
style = _kiwi.app.text_theme[string_id];
|
|
style = formatToIrcMsg(style);
|
|
|
|
// Expand a member mask into its individual parts (nick, ident, hostname)
|
|
if (params.member) {
|
|
params.nick = params.member.nick || '';
|
|
params.ident = params.member.ident || '';
|
|
params.host = params.member.hostname || '';
|
|
params.prefix = params.member.prefix || '';
|
|
}
|
|
|
|
// Do the magic. Use the %shorthand syntax to produce output.
|
|
text = style.replace(/%([A-Z]{2,})/ig, function(match, key) {
|
|
if (typeof params[key] !== 'undefined')
|
|
return params[key];
|
|
});
|
|
|
|
return text;
|
|
}
|
|
|
|
|
|
/*
|
|
* Convert input to valid ignore regex.
|
|
* @param {String} host The user mask to format.
|
|
* @returns {Array} An array with the full user mask and regex.
|
|
*/
|
|
function toUserMask(inp, return_regex) {
|
|
// Convert input to full user masks.
|
|
var tmp = inp.match(/([^!@]+)!?([^!@]+)?@?(.+)?/),
|
|
res = (tmp[1]||'*') + '!' + (tmp[2]||'*') + '@' + (tmp[3]||'*');
|
|
|
|
// Return the generated user mask only if no_array is true.
|
|
if (!return_regex) {
|
|
return res;
|
|
} else {
|
|
// Return an array with the full user mask and RegEx.
|
|
return [res, new RegExp('^'+res.toLowerCase().replace(/\./g,'\\.').replace(/\*/g,'(.[^!@]*?)')+'$','i')];
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
})(window);
|