/tai/service/static/js/chat.js
JavaScript | 1485 lines | 1240 code | 201 blank | 44 comment | 118 complexity | c60762145599ae246f3046132c524ea3 MD5 | raw file
- /* jshint supernew: true */
- // TODO: periodically send NAMES message to update user list
- if (!window.console) window.console = {};
- if (!window.console.log) window.console.log = function() {};
- var connection = new function() {
- var self = this;
- _.extend(this, {
- socket: null,
- onopen: function() {},
- onerror: function() {},
- onclose: function() {},
- onmessage: function() {},
- initialReconnectTimeout: 5 * 1000,
- reconnectTimeout: null,
- start: function() {
- self.socket = new SockJS(config.sockURL);
- self.socket.onopen = function() {
- console.log('Connected.');
- self.reconnectTimeout = null;
- var message = {};
- message[config.sessionKeyName] = getCookie(config.sessionKeyName);
- self.socket.send(JSON.stringify(message));
- if (self.onopen) {
- self.onopen();
- }
- };
- self.socket.onerror = function() {
- console.log('Failed to connect server.');
- if (self.onerror) {
- self.onerror();
- }
- };
- self.socket.onclose = function() {
- if (!self.reconnectTimeout) {
- self.reconnectTimeout = self.initialReconnectTimeout;
- } else {
- self.reconnectTimeout = _.min([self.reconnectTimeout * 2, 60 * 1000]);
- }
- console.log('Server connection closed. Reconnecting in ' + self.reconnectTimeout / 1000 + ' seconds ...');
- if (self.onclose) {
- self.onclose();
- }
- window.setTimeout(self.start, self.reconnectTimeout);
- };
- self.socket.onmessage = function(event) {
- console.log(event.data);
- var message = event.data;
- if (_.isString(message)) {
- message = JSON.parse(message);
- }
- if (self.onmessage) {
- self.onmessage(message);
- }
- };
- },
- stop: function() {
- if (self.socket) {
- self.socket.close();
- self.socket = null;
- }
- },
- send: function (command, params) {
- self.socket.send(JSON.stringify({command: command, params: params}));
- },
- sendMessage: function (message) {
- self.send(message.command, message.params);
- }
- });
- };
- var notifier = new function() {
- // display `*` on window title, if window don't have focus
- // also use webkitNotifications if exists
- var self = {
- originalTitle: undefined,
- windowHasFocus: true,
- notify: function(icon, title, body) {
- if (window.webkitNotifications && window.webkitNotifications.checkPermission() === 0) {
- var n = window.webkitNotifications.createNotification(icon, title, body);
- n.onclick = function() {
- // TODO: focus browser window
- // close notification
- n.cancel();
- };
- n.show();
- }
- if (!self.windowHasFocus && self.originalTitle === undefined) {
- self.originalTitle = document.title;
- document.title = '* ' + document.title;
- }
- },
- resetTitle: function() {
- if (self.originalTitle !== undefined) {
- document.title = self.originalTitle;
- self.originalTitle = undefined;
- }
- }
- };
- $(window).on('focus', function() {
- self.windowHasFocus = true;
- self.resetTitle();
- });
- $(window).on('blur', function() {
- self.windowHasFocus = false;
- });
- return self;
- };
- // Models
- // ======
- // IRC Message model
- var Message = Backbone.Model.extend({
- // XXX: I want to these methods acting as a property
- getNickname: function() {
- // FIXME: if this message is from server, prefix doesn't contain "!"
- return this.get('prefix').split('!')[0];
- },
- getTarget: function() {
- switch(this.get('command')) {
- case 'JOIN':
- case 'NOTICE':
- case 'PART':
- case 'PRIVMSG':
- case 'TOPIC':
- case 'RPL_QUERY':
- return this.get('params')[0];
- case 'RPL_NAMREPLY':
- case 'RPL_TOPIC':
- return this.get('params')[1];
- default:
- console.log('call getTarget for unknown command:', this.get('command'));
- return null;
- }
- },
- set: function() {
- var ret = Backbone.Model.prototype.set.apply(this, arguments);
- // as we don't have database to save it,
- // trigger change event at here.
- // XXX: may need to pass more arguments
- this.trigger('change');
- return ret;
- }
- });
- var MessageCollection = Backbone.Collection.extend({
- model: Message,
- comparator: function(message) {
- return message.get('id');
- }
- });
- var ChannelMap = Backbone.Model.extend({
- unread: function() {
- return sum(_.invoke(this.toJSON(), 'get', 'unread'));
- },
- set: function(key, val, options) {
- if (key === null) {
- return this;
- }
- var attrs;
- if (typeof key === 'object') {
- attrs = key;
- options = val;
- } else {
- (attrs = {})[key] = val;
- }
- var self = this;
- // proxy events from channels
- _.each(attrs, function(channel, name) {
- var current = self.get(name);
- if (current) {
- current.off(null, null, self);
- }
- channel.on('all', function(eventName) {
- self.trigger(eventName, channel);
- }, self);
- });
- return Backbone.Model.prototype.set.apply(this, arguments);
- },
- unset: function(attr, options) {
- var current = this.get(attr);
- if (current) {
- current.off(null, null, this);
- }
- return Backbone.Model.prototype.unset.apply(this, attr, options);
- }
- });
- // IRC Channel model
- var Channel = Backbone.Model.extend({
- defaults: {
- name: null,
- topic: null,
- isJoined: false,
- unread: 0,
- userNames: [],
- messages: null
- },
- initialize: function() {
- if (!this.get('messages')) {
- this.set('messages', new MessageCollection());
- }
- },
- // send QUERY message to the server
- query: function(options) {
- if (!this.get('isJoined')) {
- console.log("you can't query to not joined channel", this.get('name'));
- return false;
- }
- connection.send('QUERY', [this.get('name'), JSON.stringify(options)]);
- },
- isChannel: function() {
- return this.get('name').match(/^[#&]/);
- },
- isUser: function() {
- return !this.isChannel();
- },
- addNames: function(names) {
- var currentNames = this.get('userNames');
- var newNames = _.union(currentNames, names);
- newNames.sort();
- if (newNames != currentNames) {
- this.set('userNames', newNames);
- }
- },
- addName: function(name) {
- this.addNames([name]);
- },
- removeName: function(name) {
- var names = this.get('userNames');
- if (_.contains(names, name)) {
- names.splice(_.indexOf(names, name), 1);
- // because we modify names array in place,
- // we need to trigger `change` event manually
- this.trigger('change:userNames', this);
- }
- }
- });
- var ApplicationState = Backbone.Model.extend({
- defaults: {
- currentChannel: null
- }
- });
- var Bookmarks = Backbone.Model.extend({
- initialize: function() {
- this.on('change', function() {
- _.each(this.changed, function(value, key) {
- connection.send('BOOKMARK', [key, value]);
- });
- });
- },
- get: function(attr) {
- var value = Backbone.Model.prototype.get.call(this, attr);
- if (value === undefined) {
- // because message ID start from 0,
- // -1 is the value always smaller than message ID.
- value = -1;
- }
- return value;
- },
- validate: function(attrs) {
- var keys = _.keys(attrs);
- for (i=0; i<keys.length; i++) {
- var key = keys[i];
- var value = attrs[key];
- if (! _.isNumber(value)) {
- var error = "only numbers are allowed, but you pass " + key + ": " + value;
- console.log(error);
- return error;
- }
- }
- },
- _validate: function(attrs, options) {
- // because I don't call `save` method,
- // validate attrs on `set` using `validate` option
- options = _.extend({validate: true}, options);
- return Backbone.Model.prototype._validate.call(this, attrs, options);
- }
- });
- // Views
- // =====
- var ChannelListView = Backbone.View.extend({
- template: Mustache.compile($('#template-channel-name').html()),
- listEl: '#channel-list',
- unreadIcon: '#channels-have-unreads',
- defaults: {
- 'activeChannel': null
- },
- initialize: function() {
- this.$listEl = this.$(this.listEl);
- this.$unreadIcon = $(this.unreadIcon);
- },
- render: function() {
- this.stopListening();
- this.$listEl.empty();
- this.listenTo(this.model, 'change', function(model) {
- if (model === this.model) {
- this.render();
- }
- });
- var sortedChannels = _.sortBy(this.model.toJSON(), function(c) { c.get('name'); });
- _.each(sortedChannels, function(channel) {
- var div = $('<div/>').html(this.template(channel.toJSON()));
- this.$listEl.append(div);
- div.addClass('channel-name-line');
- div.on('click', function() {
- var channelName = this.getElementsByClassName('channel-name')[0].name;
- window.location.hash = '#channel/' + channelName;
- return false;
- });
- if (channel.get('name') == this.activeChannel) {
- div.addClass('active');
- }
- div.find('.part-btn').tooltip({
- title: tr['click to part from this channel'],
- placement: 'right'
- });
- div.find('.part-link').on('click', function() {
- connection.send('PART', [channel.get('name')]);
- return false;
- });
- this.listenTo(channel, 'change', function() {
- div.html(this.template(channel.toJSON()));
- if (channel.get('name') == this.activeChannel) {
- div.addClass('active');
- }
- div.find('.part-btn').tooltip({
- title: tr['click to part from this channel'],
- placement: 'right'
- });
- div.find('.part-link').on('click', function() {
- connection.send('PART', [channel.get('name')]);
- return false;
- });
- this.updateUnreadIcon();
- });
- }, this);
- this.updateUnreadIcon();
- return this;
- },
- updateUnreadIcon: function() {
- if (this.model.unread()) {
- this.$unreadIcon.show();
- } else {
- this.$unreadIcon.hide();
- }
- },
- setActiveChannel: function(channelName) {
- var self = this;
- this.$listEl.find('.channel-name').each(function() {
- if (this.name == channelName) {
- this.parentNode.className = "channel-name-line active";
- self.scrollChannelIntoViewIfNeeded();
- } else {
- this.parentNode.className = "channel-name-line";
- }
- });
- this.activeChannel = channelName;
- },
- isActive: function() {
- return this.$el.hasClass('active');
- },
- _getSelected: function() {
- var selected = this.$el.find('.channel-name-line.selected');
- if (selected.length) {
- return selected;
- }
- var active = this.$el.find('.channel-name-line.active');
- if (active.length) {
- return active;
- }
- return null;
- },
- selectNext: function() {
- var selected = this._getSelected();
- var next;
- if (selected) {
- next = selected.next();
- } else {
- selected = next = this.$el.find('.channel-name-line').first();
- }
- if (next.length) {
- selected.removeClass('selected');
- next.addClass('selected');
- this.scrollChannelIntoViewIfNeeded(next[0]);
- }
- },
- selectPrev: function() {
- var selected = this._getSelected();
- var prev;
- if (selected) {
- prev = selected.prev();
- } else {
- selected = prev = this.$el.find('.channel-name-line').last();
- }
- if (prev.length) {
- selected.removeClass('selected');
- prev.addClass('selected');
- this.scrollChannelIntoViewIfNeeded(prev[0]);
- }
- },
- scrollChannelIntoViewIfNeeded: function(elem) {
- if (elem === undefined) {
- var $elem = this._getSelected();
- if ($elem === null) {
- return;
- }
- elem = $elem[0];
- }
- var parent = $('.tab-content')[0];
- var overTop = elem.offsetTop - parent.offsetTop < parent.scrollTop;
- var overBottom = (elem.offsetTop - parent.offsetTop + elem.clientHeight) > (parent.scrollTop + parent.clientHeight);
- var alignWithTop = overTop && !overBottom;
- if (overTop || overBottom) {
- elem.scrollIntoView(alignWithTop);
- }
- },
- openSelected: function() {
- var selected = this._getSelected();
- if (selected.length) {
- var channelName = selected.find('.channel-name').attr('name');
- window.location.hash = 'channel/' + channelName;
- }
- }
- });
- var UserListView = ChannelListView.extend({
- template: Mustache.compile($('#template-user-name').html()),
- listEl: '#user-list',
- unreadIcon: '#users-have-unreads'
- });
- var ChannelView = Backbone.View.extend({
- events: {
- 'submit #message-form': 'submitMessage',
- 'keydown #message-form': 'messageFormKeyPressed',
- 'click #upload-button': 'toggleUploadForm',
- 'submit #paste-form': 'submitPaste',
- 'click #edit-topic-icon': 'editTopic',
- 'click #save-topic-btn': 'saveTopic',
- 'click #cancel-edit-topic-btn': 'stopEditingTopic'
- },
- templateChannelUserName: Mustache.compile($('#template-channel-user-name').html()),
- templateArchiveLink: Mustache.compile($('#template-archive-link').html()),
- scrollStep: 100,
- initialize: function() {
- if (!this.model) {
- this.model = new Backbone.Model();
- }
- this.model.on('change:currentChannel', this.openChannel, this);
- this.bookmarks = this.attributes.bookmarks;
- this.bookmarkedId = -1;
- this.$messagesAndArchiveLinkEl = this.$('#messages-and-archive-link');
- this.$archiveLinkEl = this.$('#archive-link');
- this.$containerEl = this.$('#message-container');
- this.$namesEl = $('#channel-names-list');
- this.$channelTitle = this.$('#channel-title');
- this.$channelTopic = this.$('#channel-topic');
- this.$channelTopicContainer = this.$('#channel-topic-container');
- this.$channelTopicInput = this.$('#channel-topic-input');
- this.$messageForm = this.$('#message-form');
- this.$('#channel-title-container .icon-edit').tooltip({
- title: tr['edit topic of this channel'],
- placement: 'right'
- });
- var self = this;
- this.$('#message-text').typeahead({source: function() {
- var channel = self.model.get('currentChannel');
- if (channel) {
- return _.map(channel.get('userNames'), function(name) {
- return name + ': ';
- });
- }
- }});
- },
- openChannel: function() {
- this.stopListening();
- var channel = this.model.get('currentChannel');
- if (!channel) {
- this.$channelTitle.empty();
- this.$channelTopic.empty();
- this.$archiveLinkEl.empty();
- this.$archiveLinkEl.hide();
- this.$containerEl.empty();
- this.$namesEl.empty();
- this.$messageForm.children().attr('disabled', true);
- this.$channelTopicContainer.hide();
- return;
- }
- // remember bookmarked ID when channel is opened
- // to detect that latter received RPL_QUERY messages are unread or not
- this.bookmarkedId = this.bookmarks.get(channel.get('name'));
- this.renderTitle();
- this.$containerEl.empty();
- this.$archiveLinkEl.empty();
- this.$archiveLinkEl.hide();
- var messages = channel.get('messages');
- if (messages.length) {
- messages.each(function(message) {
- this.insertMessageView(new MessageView({model: message}));
- }, this);
- this.scrollToBottom();
- var firstId = messages.first().get('id');
- if (messages.length < 50 && firstId > 0) {
- channel.query({limit: 50, untilId: firstId, reverse: true});
- }
- this.bookmarks.set(channel.get('name'), messages.last().get('id'));
- } else {
- channel.query({limit: 50, reverse: true});
- }
- this.renderNames(channel);
- this.$messageForm.children().attr('disabled', false);
- this.$channelTopicContainer.show();
- channel.set('unread', 0);
- this.listenTo(channel, 'change:userNames', this.renderNames);
- this.listenTo(channel, 'change:topic', this.renderTitle);
- this.listenTo(messages, 'add', this.messageAdded);
- this.listenTo(messages, 'bulkAdd', this.messageBulkAdded);
- },
- renderTitle: function() {
- var channel = this.model.get('currentChannel');
- var title = channel.get('name');
- var topic = channel.get('topic');
- this.$channelTitle.text(title);
- if (topic) {
- topic = ' - ' + topic;
- }
- this.$channelTopic.text(topic);
- },
- renderNames: function(channel) {
- var names = channel.get('userNames');
- this.$namesEl.empty();
- _.each(names, function(name) {
- this.$namesEl.append(this.templateChannelUserName({name: name}));
- }, this);
- },
- messageAdded: function(message) {
- var doScroll = this.isBottom();
- var channelName = this.model.get('currentChannel').get('name');
- if (message.getTarget() == channelName ||
- (message.getTarget() == config.myNick && message.getNickname() == channelName)) {
- this.insertMessageView(new MessageView({model: message}));
- } else {
- console.log('ERROR: channel name mismatch: ', message.getTarget(), channelName);
- return;
- }
- if (doScroll) {
- this.scrollToBottom(true);
- }
- var messageId = message.get('id');
- if (this.bookmarks.get(channelName) < messageId) {
- this.bookmarks.set(channelName, messageId);
- }
- },
- messageBulkAdded: function(messages) {
- var doScroll = this.isBottom();
- var channelName = this.model.get('currentChannel').get('name');
- _.each(messages, function(message) {
- if (message.getTarget() == channelName ||
- (message.getTarget() == config.myNick && message.getNickname() == channelName)) {
- this.insertMessageView(new MessageView({model: message}));
- } else {
- console.log('ERROR: channel name mismatch: ', message.getTarget(), channelName);
- return;
- }
- }, this);
- if (doScroll) {
- this.scrollToBottom();
- }
- var messageId = _.max(_.pluck(messages, 'id'));
- if (this.bookmarks.get(channelName) < messageId) {
- this.bookmarks.set(channelName, messageId);
- }
- },
- insertMessageView: function(view) {
- view.render();
- if (view.model.get('id') > this.bookmarkedId) {
- view.setUnread();
- }
- var firstElem = this.$containerEl.children().first();
- var thisId = view.model.get('id');
- if (thisId === 0) {
- // this is first message of this channel.
- // no need to display link to archive page.
- this.$archiveLinkEl.hide();
- } else if (!firstElem.length || thisId < firstElem.attr('message-id')) {
- this.showArchiveLinkFor(this.model.get('currentChannel'), view.model);
- }
- if (!this.$containerEl.children().length) {
- this.$containerEl.append(view.el);
- return;
- }
- var lastElem = this.$containerEl.children().last();
- if (view.model.get('id') > lastElem.attr('message-id')) {
- this.$containerEl.append(view.el);
- } else if (view.model.get('id') < firstElem.attr('message-id')) {
- this.$containerEl.prepend(view.el);
- } else {
- // TODO: binary search
- var children = this.$containerEl.children();
- for (var i = 0; i < children.length; i++) {
- if (view.model.get('id') < children[i].getAttribute('message-id')) {
- view.$el.insertBefore(children[i]);
- return;
- }
- }
- }
- },
- showArchiveLinkFor: function(channel, message) {
- this.$archiveLinkEl.html(this.templateArchiveLink({
- name: encodeURIComponent(channel.get('name')),
- page: 1 + Math.floor(message.get('id') / initialState.messagesPerPage),
- id: message.get('id')
- }));
- this.$archiveLinkEl.show();
- },
- editTopic: function() {
- var channel = this.model.get('currentChannel');
- if (!channel) {
- console.log('no currentChannel');
- return false;
- }
- this.$channelTopicInput.val(channel.get('topic'));
- this.$channelTopicContainer.addClass('editing');
- this.$channelTopicInput.focus();
- return false;
- },
- saveTopic: function() {
- var channel = this.model.get('currentChannel');
- if (!channel) {
- console.log('no currentChannel');
- return false;
- }
- connection.send('TOPIC', [channel.get('name'), this.$channelTopicInput.val()]);
- this.stopEditingTopic();
- return false;
- },
- stopEditingTopic: function() {
- this.$channelTopicContainer.removeClass('editing');
- this.$channelTopicInput.val('');
- return false;
- },
- isBottom: function() {
- var el = this.$messagesAndArchiveLinkEl[0];
- return (el.scrollTop == el.scrollHeight - el.offsetHeight);
- },
- scrollToBottom: function(animate) {
- var el = this.$messagesAndArchiveLinkEl[0];
- var scrollTopMax = el.scrollHeight - el.offsetHeight;
- if (animate) {
- this.$messagesAndArchiveLinkEl.animate({
- scrollTop: scrollTopMax
- });
- } else {
- this.$messagesAndArchiveLinkEl.scrollTop(scrollTopMax);
- }
- },
- scrollUp: function() {
- this.$messagesAndArchiveLinkEl.scrollTop(
- this.$messagesAndArchiveLinkEl.scrollTop() - this.scrollStep
- );
- },
- scrollDown: function() {
- this.$messagesAndArchiveLinkEl.scrollTop(
- this.$messagesAndArchiveLinkEl.scrollTop() + this.scrollStep
- );
- },
- focusMessageInput: function() {
- this.$messageForm.find('#message-text').focus();
- },
- messageFormKeyPressed: function(event) {
- event = event || window.event;
- switch (event.keyCode) {
- case 27:
- // escape
- // move focus from input area
- this.$messageForm.find('#message-text').blur();
- break;
- case 13:
- // enter
- return this.submitMessage(event);
- }
- },
- submitMessage: function(event) {
- // TODO: try your best to return false, to avoid HTTP POSTing form
- var form = this.$messageForm[0];
- var text = form.text.value;
- if (text) {
- var channelName = this.model.get('currentChannel').get('name');
- var command;
- if (event.ctrlKey) {
- command = 'NOTICE';
- } else {
- command = 'PRIVMSG';
- }
- connection.send(command, [channelName, text]);
- form.text.value = '';
- }
- return false;
- },
- toggleUploadForm: function() {
- this.$('#upload-button').button('toggle');
- this.$('#paste-form').toggle();
- if (window.onresize) {
- window.onresize();
- }
- },
- submitPaste: function(event) {
- var form = event.target;
- if (!form.paste.value) {
- // no file selected
- return false;
- }
- form.channel.value = this.model.get('currentChannel').get('name');
- // FIXME: it doesn't work on old browsers
- var data = new FormData(form);
- var messageForm = this.$('#message-form')[0];
- var self = this;
- $.ajax({
- url: config.pasteURL,
- type: 'POST',
- success: function(result) {
- if (messageForm.text.value) {
- messageForm.text.value += " " + result.url;
- } else {
- messageForm.text.value = result.url;
- }
- form.paste.value = "";
- self.toggleUploadForm();
- },
- data: data,
- cache: false,
- contentType: false,
- processData: false
- });
- return false;
- },
- markAllAsRead: function() {
- this.$messagesAndArchiveLinkEl.find('.message-unread-bar').fadeOut();
- this.$messagesAndArchiveLinkEl.find('.unread').removeClass('unread');
- }
- });
- var MessageView = Backbone.View.extend({
- events: {
- 'mousemove': 'clearUnread',
- 'click .remove-icon': 'deleteThisMessage'
- },
- templatePriv: Mustache.compile($('#template-priv-message').html()),
- templateNotice: Mustache.compile($('#template-notice-message').html()),
- templateTopic: Mustache.compile($('#template-topic-message').html()),
- regex: RegExp('\\b[:@]?' + config.myNick + '\\b'),
- initialize: function() {
- // TODO: assert this.model is for PRIVMSG (or NOTICE)
- this.$el.addClass('message');
- this.$el.attr('message-id', this.model.get('id'));
- this.model.on('change', this.render, this);
- },
- render: function() {
- var text = _.last(this.model.get('params'));
- var template = null;
- switch (this.model.get('command')) {
- case 'NOTICE':
- template = this.templateNotice;
- // I don't expect break here
- /* jshint -W086 */
- case 'PRIVMSG':
- /* jshint +W086 */
- template = template || this.templatePriv;
- this.$el.html(template({
- nickname: this.model.getNickname(),
- datetime: showTime(this.model.get('time')),
- text: linkify(text)
- }));
- if (text.match(this.regex)) {
- this.$el.addClass('mention');
- }
- var nickname = this.model.getNickname();
- if (nickname == config.myNick) {
- this.$el.addClass('mine');
- }
- if (nickname == config.myNick && text) {
- this.$el.addClass('show-delete-icon');
- } else {
- this.$el.removeClass('show-delete-icon');
- }
- break;
- case 'TOPIC':
- this.$el.html(this.templateTopic({
- nickname: this.model.getNickname(),
- datetime: showTime(this.model.get('time')),
- topic: text
- }));
- break;
- }
- return this;
- },
- setUnread: function() {
- this.$('.message-unread-bar').show();
- this.$el.addClass('unread');
- },
- clearUnread: function() {
- this.$('.message-unread-bar').fadeOut();
- this.$el.removeClass('unread');
- },
- deleteThisMessage: function() {
- var channelName = this.model.getTarget();
- var messageId = this.model.get('id');
- var text = _.last(this.model.get('params'));
- var $dialog = $('#confirm-message-deletion');
- $dialog.find('.quoted-message-text').text(text);
- $dialog.find('.btn-danger').on('click', function() {
- connection.send('DELETE', [channelName, messageId]);
- $dialog.modal('hide');
- });
- $dialog.modal('show');
- // to avoid updating URL
- return false;
- }
- });
- // Utility functions
- // =================
- function getCookie(key) {
- var alist = [];
- _.each(document.cookie.split(';'), function(kv) {
- var kvList = kv.split('=');
- var k = $.trim(kvList[0]);
- var v = $.trim(kvList[1]);
- alist.push([k, v]);
- });
- for (var i in alist) {
- var k = alist[i][0];
- var v = alist[i][1];
- if (k == key) {
- return v;
- }
- }
- }
- function showTime(epocseconds) {
- // UTC to local time
- var n = new Date();
- var t = new Date(epocseconds * 1000 - n.getTimezoneOffset() * 60000);
- return t.getFullYear() + "-" + pad20(t.getMonth() + 1) + "-" + pad20(t.getDate()) + " " + pad20(t.getHours()) + ":" + pad20(t.getMinutes());
- }
- function pad(str, len, chr) {
- str = str.toString();
- if (!chr) {
- chr = ' ';
- }
- if (str.length >= len) {
- return str;
- } else {
- return pad(chr + str, len-1);
- }
- }
- function pad20(str) {
- return pad(str, 2, '0');
- }
- var _LINKABLE_RE = /(?:\b((?:([\w-]+):(\/{1,3})|www[.])(?:(?:(?:[^\s&()]|&|")*(?:[^!"#$%&'()*+,.:;<=>?@\[\]^`{|}~\s]))|(?:\((?:[^\s&()]|&|")*\)))+)|(#[^ \t\r\n]+))/g;
- function linkify(text) {
- var escaped = $('<p/>').text(text).html();
- return escaped.replace(_LINKABLE_RE, function(m) {
- if (m.match(/^https?:/)) {
- return '<a href="' + m + '" rel="noreferrer" target="_blank">' + m + '</a>';
- } else if (m.match(/^[#&]/)) {
- return '<a href="#channel/' + m + '">' + m + '</a>';
- }
- return m;
- });
- }
- function sum(array) {
- return _.reduce(array, function(acc, n) { return acc + n; });
- }
- // Router
- // ======
- var Application = Backbone.Router.extend({
- routes: {
- '': 'index',
- 'channel/*id': 'openChannel'
- },
- initialize: function(options) {
- this.state = new ApplicationState();
- this.channels = new ChannelMap();
- _.each(options.channels, function(channel) {
- this.channels.set(channel.name, new Channel(channel));
- }, this);
- this.channelList = new ChannelListView({el: $('#tab-list'), model: this.channels});
- this.channelList.render();
- this.users = new ChannelMap();
- _.each(options.users, function(user) {
- this.users.set(user.name, new Channel(user));
- }, this);
- this.userList = new UserListView({el: $('#tab-names'), model: this.users});
- this.userList.render();
- this.state.on('change:currentChannel', function() {
- var channel = this.state.get('currentChannel');
- var channelName;
- if (channel) {
- channelName = channel.get('name');
- } else {
- channelName = '';
- }
- this.channelList.setActiveChannel(channelName);
- this.userList.setActiveChannel(channelName);
- }, this);
- this.bookmarks = new Bookmarks(options.bookmarks);
- this.channelView = new ChannelView({el: $('#channel-view'),
- model: this.state,
- attributes: {bookmarks: this.bookmarks}});
- $('#join-form').on('submit', function() {
- if (this.channel.value) {
- var channelName = '#' + this.channel.value;
- connection.send('JOIN', [channelName]);
- this.channel.value = '';
- }
- return false;
- });
- var self = this;
- connection.onerror = function() {
- // TODO: show error message to user
- };
- connection.onclose = function() {
- // TODO: show error message to user
- };
- connection.onmessage = function(message) {
- self.messageReceived(new Message(message));
- };
- connection.start();
- // initialize desktop notification
- if (window.webkitNotifications && window.webkitNotifications.checkPermission() == 1) {
- // PERMISSION_NOT_ALLOWED(1)
- var $dialog = $('#configure-desktop-notification');
- $dialog.find('.btn-primary').on('click', function() {
- window.webkitNotifications.requestPermission();
- $dialog.modal('hide');
- });
- $dialog.modal('show');
- }
- this.enableKeyboardShortcut();
- },
- index: function() {
- this.state.set('currentChannel', null);
- connection.onopen = null;
- },
- openChannel: function(id) {
- var channel;
- if (id.match(/^[#&]/)) {
- channel = this.channels.get(id);
- if (!channel) {
- // XXX: channel may be created after I get LIST information
- console.log('unknown channel name:', id);
- return false;
- }
- } else {
- channel = this.users.get(id);
- if (!channel) {
- // XXX: channel may be created after I get NAMES information
- console.log('unknown user name:', id);
- return false;
- }
- }
- if (channel.isUser() || channel.get('isJoined')) {
- this.state.set('currentChannel', channel);
- connection.onopen = function() {
- // query messages after (re)connection
- var lastMessage = channel.get('messages').last();
- if (lastMessage) {
- channel.query({sinceId: lastMessage.get('id')});
- } else {
- channel.query({limit: 50, reverse: true});
- }
- };
- } else {
- // just send JOIN message.
- // actual channel switching will be occured
- // in response to the echo-backed JOIN message.
- connection.send('JOIN', [channel.get('name')]);
- }
- },
- messageReceived: function(message) {
- var command = message.get('command');
- var callback;
- if (command && (callback = this['receive_' + command.toUpperCase()])) {
- return callback.call(this, message);
- } else {
- console.log('unknown message', message);
- }
- },
- receive_PRIVMSG: function(message) {
- var target = message.getTarget();
- if (target == config.myNick) {
- // direct message
- target = message.getNickname();
- }
- var channel = this.getChannelOrUser(target);
- if (!channel) {
- // XXX: is this possible in normal case?
- console.log('unknow target:', target);
- return;
- }
- channel.get('messages').add(message);
- if (this.state.get('currentChannel') != channel) {
- channel.set('unread', channel.get('unread') + 1);
- }
- var nickname = message.getNickname();
- if (nickname != config.myNick) {
- notifier.notify(
- config.avatarImageURL,
- nickname + ' on ' + target,
- _.last(message.get('params'))
- );
- }
- },
- receive_NOTICE: function(message) {
- return this.receive_PRIVMSG(message);
- },
- receive_RPL_QUERY: function(message) {
- var channel = this.getChannelOrUser(message.getTarget());
- var messages = _.map(message.get('params').slice(1),
- function(m) { return new Message(m); });
- var collection = channel.get('messages');
- collection.add(messages, {silent: true});
- // FIXME: trigger with messages actually added (don't send messages already in the collection)
- collection.trigger('bulkAdd', messages);
- },
- receive_JOIN: function(message) {
- var channelName = message.getTarget();
- var channel = this.getChannelOrUser(channelName);
- if (message.getNickname() == config.myNick) {
- if (channel) {
- channel.set('isJoined', true);
- } else {
- channel = new Channel({
- name: channelName,
- topic: '',
- isJoined: true,
- unread: 0,
- userNames: []
- });
- if (channel.isChannel()) {
- this.channels.set(channelName, channel);
- } else {
- this.users.set(channelName, channel);
- }
- }
- var hashURL = '#channel/' + channelName;
- if (window.location.hash != hashURL) {
- window.location.hash = hashURL;
- } else {
- this.openChannel(channelName);
- }
- } else {
- if (channel) {
- channel.addName(message.getNickname());
- }
- }
- },
- receive_PART: function(message) {
- var channelName = message.getTarget();
- var channel = this.getChannelOrUser(channelName);
- if (message.getNickname() == config.myNick) {
- if (channel) {
- channel.set('isJoined', false);
- if (this.state.get('currentChannel') == channel) {
- this.state.set('currentChannel', null);
- window.location.hash = '';
- }
- } else {
- console.log('receive PART message for unknown channel:', channelName);
- }
- } else {
- if (channel) {
- channel.removeName(message.getNickname());
- }
- }
- },
- receive_TOPIC: function(message) {
- var channelName = message.getTarget();
- var channel = this.channels.get(channelName);
- if (!channel) {
- console.log('unknown channel name', channelName);
- } else {
- channel.set('topic', message.get('params')[1]);
- channel.get('messages').add(message);
- }
- },
- receive_RPL_NAMREPLY: function(message) {
- var channelName = message.getTarget();
- var channel = this.channels.get(channelName);
- if (!channel) {
- console.log('unknown channel name', channelName);
- } else {
- var names = _.last(message.get('params')).split(' ');
- channel.addNames(names);
- }
- },
- receive_RPL_ENDOFNAMES: function(message) {
- // do nothing
- return;
- },
- receive_RPL_TOPIC: function(message) {
- var channelName = message.getTarget();
- var channel = this.channels.get(channelName);
- if (!channel) {
- console.log('unknown channel name', channelName);
- } else {
- channel.set('topic', message.get('params')[2]);
- }
- },
- receive_RPL_LISTSTART: function(message) {
- // do nothing
- return;
- },
- receive_RPL_LIST: function(message) {
- // TODO: remove channels which do not exist anymore
- var channelName = message.get('params')[1];
- var channelTopic = message.get('params')[3];
- if (!this.channels.get(channelName)) {
- this.channels.set(channelName, new Channel({
- isJoined: false,
- name: channelName,
- topic: channelTopic,
- userNames: [],
- unread: 0
- }));
- }
- },
- receive_RPL_LISTEND: function(message) {
- // do nothing
- return;
- },
- receive_RPL_DELETE: function(message) {
- var channelName = message.get('params')[0];
- var messageId = message.get('params')[1];
- var channel = this.channels.get(channelName);
- if (!channel) {
- console.log('unknown channel name', channelName);
- return false;
- }
- var deletedMessage = channel.get('messages').get(messageId);
- if (deletedMessage) {
- var params = deletedMessage.get('params');
- params[params.length - 1] = '';
- deletedMessage.set('params', params);
- }
- },
- getChannelOrUser: function(target) {
- var collection;
- if (target.match(/^[#&]/)) {
- collection = this.channels;
- } else {
- collection = this.users;
- }
- return collection.get(target);
- },
- enableKeyboardShortcut: function() {
- var self = this;
- Mousetrap.bind('A', function() { self.channelView.markAllAsRead(); });
- Mousetrap.bind('C', function() { $('#tab-for-channel').tab('show'); });
- Mousetrap.bind('N', function() {
- if (self.channelList.isActive()) {
- self.channelList.selectNext();
- } else if (self.userList.isActive()) {
- self.userList.selectNext();
- }
- });
- Mousetrap.bind('O', function() {
- if (self.channelList.isActive()) {
- self.channelList.openSelected();
- } else if (self.userList.isActive()) {
- self.userList.openSelected();
- }
- });
- Mousetrap.bind('P', function() {
- if (self.channelList.isActive()) {
- self.channelList.selectPrev();
- } else if (self.userList.isActive()) {
- self.userList.selectPrev();
- }
- });
- Mousetrap.bind('U', function() { $('#tab-for-names').tab('show'); });
- Mousetrap.bind('i', function() {
- self.channelView.focusMessageInput();
- // return false to avoid inputting "i"
- return false;
- });
- Mousetrap.bind('j', function() { self.channelView.scrollDown(); });
- Mousetrap.bind('k', function() { self.channelView.scrollUp(); });
- },
- disableKeyboardShortcut: function() {
- Moustrap.reset();
- }
- });
- $(document).ready(function() {
- var app = window.app = new Application(initialState);
- connection.onopen = function() {
- // conneciton is opened. start routing
- Backbone.history.start();
- setInterval(function() {
- // periodically send LIST command to update channel list
- connection.send('LIST', []);
- }, 10 * 60 * 1000);
- // remove callback to avoid starting history twice
- connection.onopen = null;
- };
- });
- $(window).ready(function() {
- function autoResize() {
- var fizedSize = sum(_.pluck($('.fixed-size'), 'clientHeight'));
- // XXX: remove magic number (120)
- var appColumnHeight = window.innerHeight - fizedSize - 120;
- if (appColumnHeight < 200) {
- appColumnHeight = 200;
- }
- $('.app-column').height(appColumnHeight);
- $('.app-column-small').height(appColumnHeight - 100);
- if (app.channelList.isActive()) {
- app.channelList.scrollChannelIntoViewIfNeeded();
- } else if (app.userList.isActive()) {
- app.userList.scrollChannelIntoViewIfNeeded();
- }
- }
- window.onresize = autoResize;
- autoResize();
- });