|
@@ -0,0 +1,1155 @@
|
|
|
+// Converse.js (A browser based XMPP chat client)
|
|
|
+// http://conversejs.org
|
|
|
+//
|
|
|
+// Copyright (c) 2012-2016, Jan-Carel Brand <jc@opkode.com>
|
|
|
+// Licensed under the Mozilla Public License (MPLv2)
|
|
|
+//
|
|
|
+/*global converse, utils, Backbone, define, window, setTimeout */
|
|
|
+
|
|
|
+(function (root, factory) {
|
|
|
+ if (typeof define === 'function' && define.amd) {
|
|
|
+ // AMD module loading
|
|
|
+ define("converse-muc", ["converse-core", "utils"], factory);
|
|
|
+ } else {
|
|
|
+ // When not using a module loader
|
|
|
+ // -------------------------------
|
|
|
+ // In this case, the dependencies need to be available already as
|
|
|
+ // global variables, and should be loaded separately via *script* tags.
|
|
|
+ // See the file **non_amd.html** for an example of this usecase.
|
|
|
+ root.converse = factory(converse, utils);
|
|
|
+ }
|
|
|
+}(this, function (converse_api, utils) {
|
|
|
+ // Strophe methods for building stanzas
|
|
|
+ var Strophe = converse_api.env.Strophe,
|
|
|
+ $iq = converse_api.env.$iq,
|
|
|
+ $msg = converse_api.env.$msg,
|
|
|
+ $pres = converse_api.env.$pres,
|
|
|
+ $build = converse_api.env.$build,
|
|
|
+ b64_sha1 = converse_api.env.b64_sha1;
|
|
|
+ // Other necessary globals
|
|
|
+ var $ = converse_api.env.jQuery,
|
|
|
+ _ = converse_api.env._,
|
|
|
+ moment = converse_api.env.moment;
|
|
|
+
|
|
|
+ // Translation machinery
|
|
|
+ // ---------------------
|
|
|
+ var __ = utils.__.bind(this);
|
|
|
+ var ___ = utils.___;
|
|
|
+
|
|
|
+ // Add Strophe Namespaces
|
|
|
+ Strophe.addNamespace('MUC_ADMIN', Strophe.NS.MUC + "#admin");
|
|
|
+ Strophe.addNamespace('MUC_OWNER', Strophe.NS.MUC + "#owner");
|
|
|
+ Strophe.addNamespace('MUC_REGISTER', "jabber:iq:register");
|
|
|
+ Strophe.addNamespace('MUC_ROOMCONF', Strophe.NS.MUC + "#roomconfig");
|
|
|
+ Strophe.addNamespace('MUC_USER', Strophe.NS.MUC + "#user");
|
|
|
+
|
|
|
+ converse_api.plugins.add('muc', {
|
|
|
+ /* This plugin adds support for XEP-0045 Multi-user chat
|
|
|
+ */
|
|
|
+
|
|
|
+ overrides: {
|
|
|
+ // Overrides mentioned here will be picked up by converse.js's
|
|
|
+ // plugin architecture they will replace existing methods on the
|
|
|
+ // relevant objects or classes.
|
|
|
+ //
|
|
|
+ // New functions which don't exist yet can also be added.
|
|
|
+ ChatBoxView: {
|
|
|
+ clearChatRoomMessages: function (ev) {
|
|
|
+ /* New method added to the ChatBox model which allows all
|
|
|
+ * messages in a chatroom to be cleared.
|
|
|
+ */
|
|
|
+ if (typeof ev !== "undefined") { ev.stopPropagation(); }
|
|
|
+ var result = confirm(__("Are you sure you want to clear the messages from this room?"));
|
|
|
+ if (result === true) {
|
|
|
+ this.$content.empty();
|
|
|
+ }
|
|
|
+ return this;
|
|
|
+ },
|
|
|
+ },
|
|
|
+
|
|
|
+ ChatBoxes: {
|
|
|
+ registerMessageHandler: function () {
|
|
|
+ /* Override so that we can register a handler
|
|
|
+ * for chat room invites.
|
|
|
+ */
|
|
|
+ this._super.registerMessageHandler(); // First call the original
|
|
|
+ this._super.converse.connection.addHandler(
|
|
|
+ function (message) {
|
|
|
+ this.onInvite(message);
|
|
|
+ return true;
|
|
|
+ }.bind(this), 'jabber:x:conference', 'message');
|
|
|
+ },
|
|
|
+
|
|
|
+ onInvite: function (message) {
|
|
|
+ /* An invitation to join a chat room has been received */
|
|
|
+ var converse = this._super.converse,
|
|
|
+ $message = $(message),
|
|
|
+ $x = $message.children('x[xmlns="jabber:x:conference"]'),
|
|
|
+ from = Strophe.getBareJidFromJid($message.attr('from')),
|
|
|
+ room_jid = $x.attr('jid'),
|
|
|
+ reason = $x.attr('reason'),
|
|
|
+ contact = converse.roster.get(from),
|
|
|
+ result;
|
|
|
+
|
|
|
+ if (converse.auto_join_on_invite) {
|
|
|
+ result = true;
|
|
|
+ } else {
|
|
|
+ contact = contact? contact.get('fullname'): Strophe.getNodeFromJid(from); // Invite request might come from someone not your roster list
|
|
|
+ if (!reason) {
|
|
|
+ result = confirm(
|
|
|
+ __(___("%1$s has invited you to join a chat room: %2$s"), contact, room_jid)
|
|
|
+ );
|
|
|
+ } else {
|
|
|
+ result = confirm(
|
|
|
+ __(___('%1$s has invited you to join a chat room: %2$s, and left the following reason: "%3$s"'), contact, room_jid, reason)
|
|
|
+ );
|
|
|
+ }
|
|
|
+ }
|
|
|
+ if (result === true) {
|
|
|
+ var chatroom = converse.chatboxviews.showChat({
|
|
|
+ 'id': room_jid,
|
|
|
+ 'jid': room_jid,
|
|
|
+ 'name': Strophe.unescapeNode(Strophe.getNodeFromJid(room_jid)),
|
|
|
+ 'nick': Strophe.unescapeNode(Strophe.getNodeFromJid(converse.connection.jid)),
|
|
|
+ 'chatroom': true,
|
|
|
+ 'box_id': b64_sha1(room_jid),
|
|
|
+ 'password': $x.attr('password')
|
|
|
+ });
|
|
|
+ if (!_.contains(
|
|
|
+ [Strophe.Status.CONNECTING, Strophe.Status.CONNECTED],
|
|
|
+ chatroom.get('connection_status'))
|
|
|
+ ) {
|
|
|
+ converse.chatboxviews.get(room_jid).join(null);
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ },
|
|
|
+ },
|
|
|
+
|
|
|
+ initialize: function () {
|
|
|
+ /* The initialize function gets called as soon as the plugin is
|
|
|
+ * loaded by converse.js's plugin machinery.
|
|
|
+ */
|
|
|
+
|
|
|
+ var converse = this.converse;
|
|
|
+ // Configuration values for this plugin
|
|
|
+ var settings = {
|
|
|
+ auto_join_on_invite: false // Auto-join chatroom on invite
|
|
|
+ };
|
|
|
+ _.extend(this, settings);
|
|
|
+ _.extend(this, _.pick(converse.user_settings, Object.keys(settings)));
|
|
|
+
|
|
|
+ converse.ChatRoomView = converse.ChatBoxView.extend({
|
|
|
+ /* Backbone View which renders a chat room, based upon the view
|
|
|
+ * for normal one-on-one chat boxes.
|
|
|
+ */
|
|
|
+ length: 300,
|
|
|
+ tagName: 'div',
|
|
|
+ className: 'chatbox chatroom',
|
|
|
+ events: {
|
|
|
+ 'click .close-chatbox-button': 'close',
|
|
|
+ 'click .toggle-chatbox-button': 'minimize',
|
|
|
+ 'click .configure-chatroom-button': 'configureChatRoom',
|
|
|
+ 'click .toggle-smiley': 'toggleEmoticonMenu',
|
|
|
+ 'click .toggle-smiley ul li': 'insertEmoticon',
|
|
|
+ 'click .toggle-clear': 'clearChatRoomMessages',
|
|
|
+ 'click .toggle-call': 'toggleCall',
|
|
|
+ 'click .toggle-occupants a': 'toggleOccupants',
|
|
|
+ 'keypress textarea.chat-textarea': 'keyPressed',
|
|
|
+ 'mousedown .dragresize-top': 'onStartVerticalResize',
|
|
|
+ 'mousedown .dragresize-left': 'onStartHorizontalResize',
|
|
|
+ 'mousedown .dragresize-topleft': 'onStartDiagonalResize'
|
|
|
+ },
|
|
|
+ is_chatroom: true,
|
|
|
+
|
|
|
+ initialize: function () {
|
|
|
+ $(window).on('resize', _.debounce(this.setDimensions.bind(this), 100));
|
|
|
+ this.model.messages.on('add', this.onMessageAdded, this);
|
|
|
+ this.model.on('change:minimized', function (item) {
|
|
|
+ if (item.get('minimized')) {
|
|
|
+ this.hide();
|
|
|
+ } else {
|
|
|
+ this.maximize();
|
|
|
+ }
|
|
|
+ }, this);
|
|
|
+ this.model.on('destroy', function () {
|
|
|
+ this.hide().leave();
|
|
|
+ }, this);
|
|
|
+
|
|
|
+ this.occupantsview = new converse.ChatRoomOccupantsView({
|
|
|
+ model: new converse.ChatRoomOccupants({nick: this.model.get('nick')})
|
|
|
+ });
|
|
|
+ var id = b64_sha1('converse.occupants'+converse.bare_jid+this.model.get('id')+this.model.get('nick'));
|
|
|
+ this.occupantsview.model.browserStorage = new Backbone.BrowserStorage[converse.storage](id);
|
|
|
+
|
|
|
+ this.occupantsview.chatroomview = this;
|
|
|
+ this.render().$el.hide();
|
|
|
+ this.occupantsview.model.fetch({add:true});
|
|
|
+ this.join(null, {'maxstanzas': converse.muc_history_max_stanzas});
|
|
|
+ this.fetchMessages();
|
|
|
+ converse.emit('chatRoomOpened', this);
|
|
|
+
|
|
|
+ this.$el.insertAfter(converse.chatboxviews.get("controlbox").$el);
|
|
|
+ if (this.model.get('minimized')) {
|
|
|
+ this.hide();
|
|
|
+ } else {
|
|
|
+ this.show();
|
|
|
+ }
|
|
|
+ },
|
|
|
+
|
|
|
+ render: function () {
|
|
|
+ this.$el.attr('id', this.model.get('box_id'))
|
|
|
+ .html(converse.templates.chatroom(this.model.toJSON()));
|
|
|
+ this.renderChatArea();
|
|
|
+ this.$content.on('scroll', _.debounce(this.onScroll.bind(this), 100));
|
|
|
+ this.setWidth();
|
|
|
+ setTimeout(converse.refreshWebkit, 50);
|
|
|
+ return this;
|
|
|
+ },
|
|
|
+
|
|
|
+ renderChatArea: function () {
|
|
|
+ if (!this.$('.chat-area').length) {
|
|
|
+ this.$('.chatroom-body').empty()
|
|
|
+ .append(
|
|
|
+ converse.templates.chatarea({
|
|
|
+ 'show_toolbar': converse.show_toolbar,
|
|
|
+ 'label_message': __('Message')
|
|
|
+ }))
|
|
|
+ .append(this.occupantsview.render().$el);
|
|
|
+ this.renderToolbar();
|
|
|
+ this.$content = this.$el.find('.chat-content');
|
|
|
+ }
|
|
|
+ this.toggleOccupants(null, true);
|
|
|
+ return this;
|
|
|
+ },
|
|
|
+
|
|
|
+ toggleOccupants: function (ev, preserve_state) {
|
|
|
+ if (ev) {
|
|
|
+ ev.preventDefault();
|
|
|
+ ev.stopPropagation();
|
|
|
+ }
|
|
|
+ if (preserve_state) {
|
|
|
+ // Bit of a hack, to make sure that the sidebar's state doesn't change
|
|
|
+ this.model.set({hidden_occupants: !this.model.get('hidden_occupants')});
|
|
|
+ }
|
|
|
+ var $el = this.$('.icon-hide-users');
|
|
|
+ if (!this.model.get('hidden_occupants')) {
|
|
|
+ this.model.save({hidden_occupants: true});
|
|
|
+ $el.removeClass('icon-hide-users').addClass('icon-show-users');
|
|
|
+ this.$('.occupants').addClass('hidden');
|
|
|
+ this.$('.chat-area').addClass('full');
|
|
|
+ this.scrollDown();
|
|
|
+ } else {
|
|
|
+ this.model.save({hidden_occupants: false});
|
|
|
+ $el.removeClass('icon-show-users').addClass('icon-hide-users');
|
|
|
+ this.$('.chat-area').removeClass('full');
|
|
|
+ this.$('div.occupants').removeClass('hidden');
|
|
|
+ this.scrollDown();
|
|
|
+ }
|
|
|
+ },
|
|
|
+
|
|
|
+ directInvite: function (receiver, reason) {
|
|
|
+ var attrs = {
|
|
|
+ xmlns: 'jabber:x:conference',
|
|
|
+ jid: this.model.get('jid')
|
|
|
+ };
|
|
|
+ if (reason !== null) { attrs.reason = reason; }
|
|
|
+ if (this.model.get('password')) { attrs.password = this.model.get('password'); }
|
|
|
+ var invitation = $msg({
|
|
|
+ from: converse.connection.jid,
|
|
|
+ to: receiver,
|
|
|
+ id: converse.connection.getUniqueId()
|
|
|
+ }).c('x', attrs);
|
|
|
+ converse.connection.send(invitation);
|
|
|
+ converse.emit('roomInviteSent', this, receiver, reason);
|
|
|
+ },
|
|
|
+
|
|
|
+ onCommandError: function (stanza) {
|
|
|
+ this.showStatusNotification(__("Error: could not execute the command"), true);
|
|
|
+ },
|
|
|
+
|
|
|
+ sendChatRoomMessage: function (text) {
|
|
|
+ var msgid = converse.connection.getUniqueId();
|
|
|
+ var msg = $msg({
|
|
|
+ to: this.model.get('jid'),
|
|
|
+ from: converse.connection.jid,
|
|
|
+ type: 'groupchat',
|
|
|
+ id: msgid
|
|
|
+ }).c("body").t(text).up()
|
|
|
+ .c("x", {xmlns: "jabber:x:event"}).c("composing");
|
|
|
+ converse.connection.send(msg);
|
|
|
+
|
|
|
+ var fullname = converse.xmppstatus.get('fullname');
|
|
|
+ this.model.messages.create({
|
|
|
+ fullname: _.isEmpty(fullname)? converse.bare_jid: fullname,
|
|
|
+ sender: 'me',
|
|
|
+ time: moment().format(),
|
|
|
+ message: text,
|
|
|
+ msgid: msgid
|
|
|
+ });
|
|
|
+ },
|
|
|
+
|
|
|
+ setAffiliation: function(room, jid, affiliation, reason, onSuccess, onError) {
|
|
|
+ var item = $build("item", {jid: jid, affiliation: affiliation});
|
|
|
+ var iq = $iq({to: room, type: "set"}).c("query", {xmlns: Strophe.NS.MUC_ADMIN}).cnode(item.node);
|
|
|
+ if (reason !== null) { iq.c("reason", reason); }
|
|
|
+ return converse.connection.sendIQ(iq.tree(), onSuccess, onError);
|
|
|
+ },
|
|
|
+
|
|
|
+ modifyRole: function(room, nick, role, reason, onSuccess, onError) {
|
|
|
+ var item = $build("item", {nick: nick, role: role});
|
|
|
+ var iq = $iq({to: room, type: "set"}).c("query", {xmlns: Strophe.NS.MUC_ADMIN}).cnode(item.node);
|
|
|
+ if (reason !== null) { iq.c("reason", reason); }
|
|
|
+ return converse.connection.sendIQ(iq.tree(), onSuccess, onError);
|
|
|
+ },
|
|
|
+
|
|
|
+ member: function(room, jid, reason, handler_cb, error_cb) {
|
|
|
+ return this.setAffiliation(room, jid, 'member', reason, handler_cb, error_cb);
|
|
|
+ },
|
|
|
+ revoke: function(room, jid, reason, handler_cb, error_cb) {
|
|
|
+ return this.setAffiliation(room, jid, 'none', reason, handler_cb, error_cb);
|
|
|
+ },
|
|
|
+ owner: function(room, jid, reason, handler_cb, error_cb) {
|
|
|
+ return this.setAffiliation(room, jid, 'owner', reason, handler_cb, error_cb);
|
|
|
+ },
|
|
|
+ admin: function(room, jid, reason, handler_cb, error_cb) {
|
|
|
+ return this.setAffiliation(room, jid, 'admin', reason, handler_cb, error_cb);
|
|
|
+ },
|
|
|
+
|
|
|
+ validateRoleChangeCommand: function (command, args) {
|
|
|
+ /* Check that a command to change a chat room user's role or
|
|
|
+ * affiliation has anough arguments.
|
|
|
+ */
|
|
|
+ // TODO check if first argument is valid
|
|
|
+ if (args.length < 1 || args.length > 2) {
|
|
|
+ this.showStatusNotification(
|
|
|
+ __("Error: the \""+command+"\" command takes two arguments, the user's nickname and optionally a reason."),
|
|
|
+ true
|
|
|
+ );
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+ return true;
|
|
|
+ },
|
|
|
+
|
|
|
+ onChatRoomMessageSubmitted: function (text) {
|
|
|
+ /* Gets called when the user presses enter to send off a
|
|
|
+ * message in a chat room.
|
|
|
+ *
|
|
|
+ * Parameters:
|
|
|
+ * (String) text - The message text.
|
|
|
+ */
|
|
|
+ var match = text.replace(/^\s*/, "").match(/^\/(.*?)(?: (.*))?$/) || [false, '', ''],
|
|
|
+ args = match[2] && match[2].splitOnce(' ') || [];
|
|
|
+ switch (match[1]) {
|
|
|
+ case 'admin':
|
|
|
+ if (!this.validateRoleChangeCommand(match[1], args)) { break; }
|
|
|
+ this.setAffiliation(
|
|
|
+ this.model.get('jid'), args[0], 'admin', args[1],
|
|
|
+ undefined, this.onCommandError.bind(this));
|
|
|
+ break;
|
|
|
+ case 'ban':
|
|
|
+ if (!this.validateRoleChangeCommand(match[1], args)) { break; }
|
|
|
+ this.setAffiliation(
|
|
|
+ this.model.get('jid'), args[0], 'outcast', args[1],
|
|
|
+ undefined, this.onCommandError.bind(this));
|
|
|
+ break;
|
|
|
+ case 'clear':
|
|
|
+ this.clearChatRoomMessages();
|
|
|
+ break;
|
|
|
+ case 'deop':
|
|
|
+ if (!this.validateRoleChangeCommand(match[1], args)) { break; }
|
|
|
+ this.modifyRole(
|
|
|
+ this.model.get('jid'), args[0], 'occupant', args[1],
|
|
|
+ undefined, this.onCommandError.bind(this));
|
|
|
+ break;
|
|
|
+ case 'help':
|
|
|
+ this.showHelpMessages([
|
|
|
+ '<strong>/admin</strong>: ' +__("Change user's affiliation to admin"),
|
|
|
+ '<strong>/ban</strong>: ' +__('Ban user from room'),
|
|
|
+ '<strong>/clear</strong>: ' +__('Remove messages'),
|
|
|
+ '<strong>/deop</strong>: ' +__('Change user role to occupant'),
|
|
|
+ '<strong>/help</strong>: ' +__('Show this menu'),
|
|
|
+ '<strong>/kick</strong>: ' +__('Kick user from room'),
|
|
|
+ '<strong>/me</strong>: ' +__('Write in 3rd person'),
|
|
|
+ '<strong>/member</strong>: '+__('Grant membership to a user'),
|
|
|
+ '<strong>/mute</strong>: ' +__("Remove user's ability to post messages"),
|
|
|
+ '<strong>/nick</strong>: ' +__('Change your nickname'),
|
|
|
+ '<strong>/op</strong>: ' +__('Grant moderator role to user'),
|
|
|
+ '<strong>/owner</strong>: ' +__('Grant ownership of this room'),
|
|
|
+ '<strong>/revoke</strong>: '+__("Revoke user's membership"),
|
|
|
+ '<strong>/topic</strong>: ' +__('Set room topic'),
|
|
|
+ '<strong>/voice</strong>: ' +__('Allow muted user to post messages')
|
|
|
+ ]);
|
|
|
+ break;
|
|
|
+ case 'kick':
|
|
|
+ if (!this.validateRoleChangeCommand(match[1], args)) { break; }
|
|
|
+ this.modifyRole(
|
|
|
+ this.model.get('jid'), args[0], 'none', args[1],
|
|
|
+ undefined, this.onCommandError.bind(this));
|
|
|
+ break;
|
|
|
+ case 'mute':
|
|
|
+ if (!this.validateRoleChangeCommand(match[1], args)) { break; }
|
|
|
+ this.modifyRole(
|
|
|
+ this.model.get('jid'), args[0], 'visitor', args[1],
|
|
|
+ undefined, this.onCommandError.bind(this));
|
|
|
+ break;
|
|
|
+ case 'member':
|
|
|
+ if (!this.validateRoleChangeCommand(match[1], args)) { break; }
|
|
|
+ this.setAffiliation(
|
|
|
+ this.model.get('jid'), args[0], 'member', args[1],
|
|
|
+ undefined, this.onCommandError.bind(this));
|
|
|
+ break;
|
|
|
+ case 'nick':
|
|
|
+ converse.connection.send($pres({
|
|
|
+ from: converse.connection.jid,
|
|
|
+ to: this.getRoomJIDAndNick(match[2]),
|
|
|
+ id: converse.connection.getUniqueId()
|
|
|
+ }).tree());
|
|
|
+ break;
|
|
|
+ case 'owner':
|
|
|
+ if (!this.validateRoleChangeCommand(match[1], args)) { break; }
|
|
|
+ this.setAffiliation(
|
|
|
+ this.model.get('jid'), args[0], 'owner', args[1],
|
|
|
+ undefined, this.onCommandError.bind(this));
|
|
|
+ break;
|
|
|
+ case 'op':
|
|
|
+ if (!this.validateRoleChangeCommand(match[1], args)) { break; }
|
|
|
+ this.modifyRole(
|
|
|
+ this.model.get('jid'), args[0], 'moderator', args[1],
|
|
|
+ undefined, this.onCommandError.bind(this));
|
|
|
+ break;
|
|
|
+ case 'revoke':
|
|
|
+ if (!this.validateRoleChangeCommand(match[1], args)) { break; }
|
|
|
+ this.setAffiliation(
|
|
|
+ this.model.get('jid'), args[0], 'none', args[1],
|
|
|
+ undefined, this.onCommandError.bind(this));
|
|
|
+ break;
|
|
|
+ case 'topic':
|
|
|
+ converse.connection.send(
|
|
|
+ $msg({
|
|
|
+ to: this.model.get('jid'),
|
|
|
+ from: converse.connection.jid,
|
|
|
+ type: "groupchat"
|
|
|
+ }).c("subject", {xmlns: "jabber:client"}).t(match[2]).tree()
|
|
|
+ );
|
|
|
+ break;
|
|
|
+ case 'voice':
|
|
|
+ if (!this.validateRoleChangeCommand(match[1], args)) { break; }
|
|
|
+ this.modifyRole(
|
|
|
+ this.model.get('jid'), args[0], 'occupant', args[1],
|
|
|
+ undefined, this.onCommandError.bind(this));
|
|
|
+ break;
|
|
|
+ default:
|
|
|
+ this.sendChatRoomMessage(text);
|
|
|
+ break;
|
|
|
+ }
|
|
|
+ },
|
|
|
+
|
|
|
+ handleMUCStanza: function (stanza) {
|
|
|
+ var xmlns, xquery, i;
|
|
|
+ var from = stanza.getAttribute('from');
|
|
|
+ var is_mam = $(stanza).find('[xmlns="'+Strophe.NS.MAM+'"]').length > 0;
|
|
|
+ if (!from || (this.model.get('id') !== from.split("/")[0]) || is_mam) {
|
|
|
+ return true;
|
|
|
+ }
|
|
|
+ if (stanza.nodeName === "message") {
|
|
|
+ _.compose(this.onChatRoomMessage.bind(this), this.showStatusMessages.bind(this))(stanza);
|
|
|
+ } else if (stanza.nodeName === "presence") {
|
|
|
+ xquery = stanza.getElementsByTagName("x");
|
|
|
+ if (xquery.length > 0) {
|
|
|
+ for (i = 0; i < xquery.length; i++) {
|
|
|
+ xmlns = xquery[i].getAttribute("xmlns");
|
|
|
+ if (xmlns && xmlns.match(Strophe.NS.MUC)) {
|
|
|
+ this.onChatRoomPresence(stanza);
|
|
|
+ break;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ return true;
|
|
|
+ },
|
|
|
+
|
|
|
+ getRoomJIDAndNick: function (nick) {
|
|
|
+ nick = nick || this.model.get('nick');
|
|
|
+ var room = this.model.get('jid');
|
|
|
+ var node = Strophe.getNodeFromJid(room);
|
|
|
+ var domain = Strophe.getDomainFromJid(room);
|
|
|
+ return node + "@" + domain + (nick !== null ? "/" + nick : "");
|
|
|
+ },
|
|
|
+
|
|
|
+ join: function (password, history_attrs, extended_presence) {
|
|
|
+ var stanza = $pres({
|
|
|
+ from: converse.connection.jid,
|
|
|
+ to: this.getRoomJIDAndNick()
|
|
|
+ }).c("x", {
|
|
|
+ xmlns: Strophe.NS.MUC
|
|
|
+ });
|
|
|
+ if (typeof history_attrs === "object" && Object.keys(history_attrs).length) {
|
|
|
+ stanza = stanza.c("history", history_attrs).up();
|
|
|
+ }
|
|
|
+ if (password) {
|
|
|
+ stanza.cnode(Strophe.xmlElement("password", [], password));
|
|
|
+ }
|
|
|
+ if (typeof extended_presence !== "undefined" && extended_presence !== null) {
|
|
|
+ stanza.up.cnode(extended_presence);
|
|
|
+ }
|
|
|
+ if (!this.handler) {
|
|
|
+ this.handler = converse.connection.addHandler(this.handleMUCStanza.bind(this));
|
|
|
+ }
|
|
|
+ this.model.set('connection_status', Strophe.Status.CONNECTING);
|
|
|
+ return converse.connection.send(stanza);
|
|
|
+ },
|
|
|
+
|
|
|
+ leave: function(exit_msg) {
|
|
|
+ var presenceid = converse.connection.getUniqueId();
|
|
|
+ var presence = $pres({
|
|
|
+ type: "unavailable",
|
|
|
+ id: presenceid,
|
|
|
+ from: converse.connection.jid,
|
|
|
+ to: this.getRoomJIDAndNick()
|
|
|
+ });
|
|
|
+ if (exit_msg !== null) {
|
|
|
+ presence.c("status", exit_msg);
|
|
|
+ }
|
|
|
+ converse.connection.addHandler(
|
|
|
+ function () { this.model.set('connection_status', Strophe.Status.DISCONNECTED); }.bind(this),
|
|
|
+ null, "presence", null, presenceid);
|
|
|
+ converse.connection.send(presence);
|
|
|
+ },
|
|
|
+
|
|
|
+ renderConfigurationForm: function (stanza) {
|
|
|
+ var $form = this.$el.find('form.chatroom-form'),
|
|
|
+ $fieldset = $form.children('fieldset:first'),
|
|
|
+ $stanza = $(stanza),
|
|
|
+ $fields = $stanza.find('field'),
|
|
|
+ title = $stanza.find('title').text(),
|
|
|
+ instructions = $stanza.find('instructions').text();
|
|
|
+ $fieldset.find('span.spinner').remove();
|
|
|
+ $fieldset.append($('<legend>').text(title));
|
|
|
+ if (instructions && instructions !== title) {
|
|
|
+ $fieldset.append($('<p class="instructions">').text(instructions));
|
|
|
+ }
|
|
|
+ _.each($fields, function (field) {
|
|
|
+ $fieldset.append(utils.xForm2webForm($(field), $stanza));
|
|
|
+ });
|
|
|
+ $form.append('<fieldset></fieldset>');
|
|
|
+ $fieldset = $form.children('fieldset:last');
|
|
|
+ $fieldset.append('<input type="submit" class="pure-button button-primary" value="'+__('Save')+'"/>');
|
|
|
+ $fieldset.append('<input type="button" class="pure-button button-cancel" value="'+__('Cancel')+'"/>');
|
|
|
+ $fieldset.find('input[type=button]').on('click', this.cancelConfiguration.bind(this));
|
|
|
+ $form.on('submit', this.saveConfiguration.bind(this));
|
|
|
+ },
|
|
|
+
|
|
|
+ sendConfiguration: function(config, onSuccess, onError) {
|
|
|
+ // Send an IQ stanza with the room configuration.
|
|
|
+ var iq = $iq({to: this.model.get('jid'), type: "set"})
|
|
|
+ .c("query", {xmlns: Strophe.NS.MUC_OWNER})
|
|
|
+ .c("x", {xmlns: Strophe.NS.XFORM, type: "submit"});
|
|
|
+ _.each(config, function (node) { iq.cnode(node).up(); });
|
|
|
+ return converse.connection.sendIQ(iq.tree(), onSuccess, onError);
|
|
|
+ },
|
|
|
+
|
|
|
+ saveConfiguration: function (ev) {
|
|
|
+ ev.preventDefault();
|
|
|
+ var that = this;
|
|
|
+ var $inputs = $(ev.target).find(':input:not([type=button]):not([type=submit])'),
|
|
|
+ count = $inputs.length,
|
|
|
+ configArray = [];
|
|
|
+ $inputs.each(function () {
|
|
|
+ configArray.push(utils.webForm2xForm(this));
|
|
|
+ if (!--count) {
|
|
|
+ that.sendConfiguration(
|
|
|
+ configArray,
|
|
|
+ that.onConfigSaved.bind(that),
|
|
|
+ that.onErrorConfigSaved.bind(that)
|
|
|
+ );
|
|
|
+ }
|
|
|
+ });
|
|
|
+ this.$el.find('div.chatroom-form-container').hide(
|
|
|
+ function () {
|
|
|
+ $(this).remove();
|
|
|
+ that.$el.find('.chat-area').removeClass('hidden');
|
|
|
+ that.$el.find('.occupants').removeClass('hidden');
|
|
|
+ });
|
|
|
+ },
|
|
|
+
|
|
|
+ onConfigSaved: function (stanza) {
|
|
|
+ // TODO: provide feedback
|
|
|
+ },
|
|
|
+
|
|
|
+ onErrorConfigSaved: function (stanza) {
|
|
|
+ this.showStatusNotification(__("An error occurred while trying to save the form."));
|
|
|
+ },
|
|
|
+
|
|
|
+ cancelConfiguration: function (ev) {
|
|
|
+ ev.preventDefault();
|
|
|
+ var that = this;
|
|
|
+ this.$el.find('div.chatroom-form-container').hide(
|
|
|
+ function () {
|
|
|
+ $(this).remove();
|
|
|
+ that.$el.find('.chat-area').removeClass('hidden');
|
|
|
+ that.$el.find('.occupants').removeClass('hidden');
|
|
|
+ });
|
|
|
+ },
|
|
|
+
|
|
|
+ configureChatRoom: function (ev) {
|
|
|
+ ev.preventDefault();
|
|
|
+ if (this.$el.find('div.chatroom-form-container').length) {
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ this.$('.chatroom-body').children().addClass('hidden');
|
|
|
+ this.$('.chatroom-body').append(converse.templates.chatroom_form());
|
|
|
+ converse.connection.sendIQ(
|
|
|
+ $iq({
|
|
|
+ to: this.model.get('jid'),
|
|
|
+ type: "get"
|
|
|
+ }).c("query", {xmlns: Strophe.NS.MUC_OWNER}).tree(),
|
|
|
+ this.renderConfigurationForm.bind(this)
|
|
|
+ );
|
|
|
+ },
|
|
|
+
|
|
|
+ submitPassword: function (ev) {
|
|
|
+ ev.preventDefault();
|
|
|
+ var password = this.$el.find('.chatroom-form').find('input[type=password]').val();
|
|
|
+ this.$el.find('.chatroom-form-container').replaceWith('<span class="spinner centered"/>');
|
|
|
+ this.join(password);
|
|
|
+ },
|
|
|
+
|
|
|
+ renderPasswordForm: function () {
|
|
|
+ this.$('.chatroom-body').children().hide();
|
|
|
+ this.$('span.centered.spinner').remove();
|
|
|
+ this.$('.chatroom-body').append(
|
|
|
+ converse.templates.chatroom_password_form({
|
|
|
+ heading: __('This chatroom requires a password'),
|
|
|
+ label_password: __('Password: '),
|
|
|
+ label_submit: __('Submit')
|
|
|
+ }));
|
|
|
+ this.$('.chatroom-form').on('submit', this.submitPassword.bind(this));
|
|
|
+ },
|
|
|
+
|
|
|
+ showDisconnectMessage: function (msg) {
|
|
|
+ this.$('.chat-area').hide();
|
|
|
+ this.$('.occupants').hide();
|
|
|
+ this.$('span.centered.spinner').remove();
|
|
|
+ this.$('.chatroom-body').append($('<p>'+msg+'</p>'));
|
|
|
+ },
|
|
|
+
|
|
|
+ /* http://xmpp.org/extensions/xep-0045.html
|
|
|
+ * ----------------------------------------
|
|
|
+ * 100 message Entering a room Inform user that any occupant is allowed to see the user's full JID
|
|
|
+ * 101 message (out of band) Affiliation change Inform user that his or her affiliation changed while not in the room
|
|
|
+ * 102 message Configuration change Inform occupants that room now shows unavailable members
|
|
|
+ * 103 message Configuration change Inform occupants that room now does not show unavailable members
|
|
|
+ * 104 message Configuration change Inform occupants that a non-privacy-related room configuration change has occurred
|
|
|
+ * 110 presence Any room presence Inform user that presence refers to one of its own room occupants
|
|
|
+ * 170 message or initial presence Configuration change Inform occupants that room logging is now enabled
|
|
|
+ * 171 message Configuration change Inform occupants that room logging is now disabled
|
|
|
+ * 172 message Configuration change Inform occupants that the room is now non-anonymous
|
|
|
+ * 173 message Configuration change Inform occupants that the room is now semi-anonymous
|
|
|
+ * 174 message Configuration change Inform occupants that the room is now fully-anonymous
|
|
|
+ * 201 presence Entering a room Inform user that a new room has been created
|
|
|
+ * 210 presence Entering a room Inform user that the service has assigned or modified the occupant's roomnick
|
|
|
+ * 301 presence Removal from room Inform user that he or she has been banned from the room
|
|
|
+ * 303 presence Exiting a room Inform all occupants of new room nickname
|
|
|
+ * 307 presence Removal from room Inform user that he or she has been kicked from the room
|
|
|
+ * 321 presence Removal from room Inform user that he or she is being removed from the room because of an affiliation change
|
|
|
+ * 322 presence Removal from room Inform user that he or she is being removed from the room because the room has been changed to members-only and the user is not a member
|
|
|
+ * 332 presence Removal from room Inform user that he or she is being removed from the room because of a system shutdown
|
|
|
+ */
|
|
|
+ infoMessages: {
|
|
|
+ 100: __('This room is not anonymous'),
|
|
|
+ 102: __('This room now shows unavailable members'),
|
|
|
+ 103: __('This room does not show unavailable members'),
|
|
|
+ 104: __('Non-privacy-related room configuration has changed'),
|
|
|
+ 170: __('Room logging is now enabled'),
|
|
|
+ 171: __('Room logging is now disabled'),
|
|
|
+ 172: __('This room is now non-anonymous'),
|
|
|
+ 173: __('This room is now semi-anonymous'),
|
|
|
+ 174: __('This room is now fully-anonymous'),
|
|
|
+ 201: __('A new room has been created')
|
|
|
+ },
|
|
|
+
|
|
|
+ disconnectMessages: {
|
|
|
+ 301: __('You have been banned from this room'),
|
|
|
+ 307: __('You have been kicked from this room'),
|
|
|
+ 321: __("You have been removed from this room because of an affiliation change"),
|
|
|
+ 322: __("You have been removed from this room because the room has changed to members-only and you're not a member"),
|
|
|
+ 332: __("You have been removed from this room because the MUC (Multi-user chat) service is being shut down.")
|
|
|
+ },
|
|
|
+
|
|
|
+ actionInfoMessages: {
|
|
|
+ /* XXX: Note the triple underscore function and not double
|
|
|
+ * underscore.
|
|
|
+ *
|
|
|
+ * This is a hack. We can't pass the strings to __ because we
|
|
|
+ * don't yet know what the variable to interpolate is.
|
|
|
+ *
|
|
|
+ * Triple underscore will just return the string again, but we
|
|
|
+ * can then at least tell gettext to scan for it so that these
|
|
|
+ * strings are picked up by the translation machinery.
|
|
|
+ */
|
|
|
+ 301: ___("<strong>%1$s</strong> has been banned"),
|
|
|
+ 303: ___("<strong>%1$s</strong>'s nickname has changed"),
|
|
|
+ 307: ___("<strong>%1$s</strong> has been kicked out"),
|
|
|
+ 321: ___("<strong>%1$s</strong> has been removed because of an affiliation change"),
|
|
|
+ 322: ___("<strong>%1$s</strong> has been removed for not being a member")
|
|
|
+ },
|
|
|
+
|
|
|
+ newNicknameMessages: {
|
|
|
+ 210: ___('Your nickname has been automatically changed to: <strong>%1$s</strong>'),
|
|
|
+ 303: ___('Your nickname has been changed to: <strong>%1$s</strong>')
|
|
|
+ },
|
|
|
+
|
|
|
+ showStatusMessages: function (el, is_self) {
|
|
|
+ /* Check for status codes and communicate their purpose to the user.
|
|
|
+ * Allow user to configure chat room if they are the owner.
|
|
|
+ * See: http://xmpp.org/registrar/mucstatus.html
|
|
|
+ */
|
|
|
+ var $el = $(el),
|
|
|
+ i, disconnect_msgs = [], msgs = [], reasons = [];
|
|
|
+
|
|
|
+ $el.find('x[xmlns="'+Strophe.NS.MUC_USER+'"]').each(function (idx, x) {
|
|
|
+ var $item = $(x).find('item');
|
|
|
+ if (Strophe.getBareJidFromJid($item.attr('jid')) === converse.bare_jid && $item.attr('affiliation') === 'owner') {
|
|
|
+ this.$el.find('a.configure-chatroom-button').show();
|
|
|
+ }
|
|
|
+ $(x).find('item reason').each(function (idx, reason) {
|
|
|
+ if ($(reason).text()) {
|
|
|
+ reasons.push($(reason).text());
|
|
|
+ }
|
|
|
+ });
|
|
|
+ $(x).find('status').each(function (idx, stat) {
|
|
|
+ var code = stat.getAttribute('code');
|
|
|
+ var from_nick = Strophe.unescapeNode(Strophe.getResourceFromJid($el.attr('from')));
|
|
|
+ if (is_self && code === "210") {
|
|
|
+ msgs.push(__(this.newNicknameMessages[code], from_nick));
|
|
|
+ } else if (is_self && code === "303") {
|
|
|
+ msgs.push(__(this.newNicknameMessages[code], $item.attr('nick')));
|
|
|
+ } else if (is_self && _.contains(_.keys(this.disconnectMessages), code)) {
|
|
|
+ disconnect_msgs.push(this.disconnectMessages[code]);
|
|
|
+ } else if (!is_self && _.contains(_.keys(this.actionInfoMessages), code)) {
|
|
|
+ msgs.push(__(this.actionInfoMessages[code], from_nick));
|
|
|
+ } else if (_.contains(_.keys(this.infoMessages), code)) {
|
|
|
+ msgs.push(this.infoMessages[code]);
|
|
|
+ } else if (code !== '110') {
|
|
|
+ if ($(stat).text()) {
|
|
|
+ msgs.push($(stat).text()); // Sometimes the status contains human readable text and not a code.
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }.bind(this));
|
|
|
+ }.bind(this));
|
|
|
+
|
|
|
+ if (disconnect_msgs.length > 0) {
|
|
|
+ for (i=0; i<disconnect_msgs.length; i++) {
|
|
|
+ this.showDisconnectMessage(disconnect_msgs[i]);
|
|
|
+ }
|
|
|
+ for (i=0; i<reasons.length; i++) {
|
|
|
+ this.showDisconnectMessage(__('The reason given is: "'+reasons[i]+'"'), true);
|
|
|
+ }
|
|
|
+ this.model.set('connection_status', Strophe.Status.DISCONNECTED);
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ for (i=0; i<msgs.length; i++) {
|
|
|
+ this.$content.append(converse.templates.info({message: msgs[i]}));
|
|
|
+ }
|
|
|
+ for (i=0; i<reasons.length; i++) {
|
|
|
+ this.showStatusNotification(__('The reason given is: "'+reasons[i]+'"'), true);
|
|
|
+ }
|
|
|
+ this.scrollDown();
|
|
|
+ return el;
|
|
|
+ },
|
|
|
+
|
|
|
+ showErrorMessage: function ($error) {
|
|
|
+ // We didn't enter the room, so we must remove it from the MUC
|
|
|
+ // add-on
|
|
|
+ if ($error.attr('type') === 'auth') {
|
|
|
+ if ($error.find('not-authorized').length) {
|
|
|
+ this.renderPasswordForm();
|
|
|
+ } else if ($error.find('registration-required').length) {
|
|
|
+ this.showDisconnectMessage(__('You are not on the member list of this room'));
|
|
|
+ } else if ($error.find('forbidden').length) {
|
|
|
+ this.showDisconnectMessage(__('You have been banned from this room'));
|
|
|
+ }
|
|
|
+ } else if ($error.attr('type') === 'modify') {
|
|
|
+ if ($error.find('jid-malformed').length) {
|
|
|
+ this.showDisconnectMessage(__('No nickname was specified'));
|
|
|
+ }
|
|
|
+ } else if ($error.attr('type') === 'cancel') {
|
|
|
+ if ($error.find('not-allowed').length) {
|
|
|
+ this.showDisconnectMessage(__('You are not allowed to create new rooms'));
|
|
|
+ } else if ($error.find('not-acceptable').length) {
|
|
|
+ this.showDisconnectMessage(__("Your nickname doesn't conform to this room's policies"));
|
|
|
+ } else if ($error.find('conflict').length) {
|
|
|
+ this.showDisconnectMessage(__("Your nickname is already taken"));
|
|
|
+ // TODO: give user the option of choosing a different nickname
|
|
|
+ } else if ($error.find('item-not-found').length) {
|
|
|
+ this.showDisconnectMessage(__("This room does not (yet) exist"));
|
|
|
+ } else if ($error.find('service-unavailable').length) {
|
|
|
+ this.showDisconnectMessage(__("This room has reached it's maximum number of occupants"));
|
|
|
+ }
|
|
|
+ }
|
|
|
+ },
|
|
|
+
|
|
|
+ onChatRoomPresence: function (pres) {
|
|
|
+ var $presence = $(pres), is_self;
|
|
|
+ var nick = this.model.get('nick');
|
|
|
+ if ($presence.attr('type') === 'error') {
|
|
|
+ this.model.set('connection_status', Strophe.Status.DISCONNECTED);
|
|
|
+ this.showErrorMessage($presence.find('error'));
|
|
|
+ } else {
|
|
|
+ is_self = ($presence.find("status[code='110']").length) ||
|
|
|
+ ($presence.attr('from') === this.model.get('id')+'/'+Strophe.escapeNode(nick));
|
|
|
+ if (this.model.get('connection_status') !== Strophe.Status.CONNECTED) {
|
|
|
+ this.model.set('connection_status', Strophe.Status.CONNECTED);
|
|
|
+ }
|
|
|
+ this.showStatusMessages(pres, is_self);
|
|
|
+ }
|
|
|
+ this.occupantsview.updateOccupantsOnPresence(pres);
|
|
|
+ },
|
|
|
+
|
|
|
+ onChatRoomMessage: function (message) {
|
|
|
+ var $message = $(message),
|
|
|
+ archive_id = $message.find('result[xmlns="'+Strophe.NS.MAM+'"]').attr('id'),
|
|
|
+ delayed = $message.find('delay').length > 0,
|
|
|
+ $forwarded = $message.find('forwarded'),
|
|
|
+ $delay;
|
|
|
+
|
|
|
+ if ($forwarded.length) {
|
|
|
+ $message = $forwarded.children('message');
|
|
|
+ $delay = $forwarded.children('delay');
|
|
|
+ delayed = $delay.length > 0;
|
|
|
+ }
|
|
|
+ var body = $message.children('body').text(),
|
|
|
+ jid = $message.attr('from'),
|
|
|
+ msgid = $message.attr('id'),
|
|
|
+ resource = Strophe.getResourceFromJid(jid),
|
|
|
+ sender = resource && Strophe.unescapeNode(resource) || '',
|
|
|
+ subject = $message.children('subject').text();
|
|
|
+
|
|
|
+ if (msgid && this.model.messages.findWhere({msgid: msgid})) {
|
|
|
+ return true; // We already have this message stored.
|
|
|
+ }
|
|
|
+ if (subject) {
|
|
|
+ this.$el.find('.chatroom-topic').text(subject).attr('title', subject);
|
|
|
+ // For translators: the %1$s and %2$s parts will get replaced by the user and topic text respectively
|
|
|
+ // Example: Topic set by JC Brand to: Hello World!
|
|
|
+ this.$content.append(
|
|
|
+ converse.templates.info({
|
|
|
+ 'message': __('Topic set by %1$s to: %2$s', sender, subject)
|
|
|
+ }));
|
|
|
+ }
|
|
|
+ if (sender === '') {
|
|
|
+ return true;
|
|
|
+ }
|
|
|
+ this.model.createMessage($message, $delay, archive_id);
|
|
|
+ if (!delayed && sender !== this.model.get('nick') && (new RegExp("\\b"+this.model.get('nick')+"\\b")).test(body)) {
|
|
|
+ converse.playNotification();
|
|
|
+ }
|
|
|
+ if (sender !== this.model.get('nick')) {
|
|
|
+ // We only emit an event if it's not our own message
|
|
|
+ converse.emit('message', message);
|
|
|
+ }
|
|
|
+ return true;
|
|
|
+ },
|
|
|
+
|
|
|
+ fetchArchivedMessages: function (options) {
|
|
|
+ /* Fetch archived chat messages from the XMPP server.
|
|
|
+ *
|
|
|
+ * Then, upon receiving them, call onChatRoomMessage
|
|
|
+ * so that they are displayed inside it.
|
|
|
+ */
|
|
|
+ if (!converse.features.findWhere({'var': Strophe.NS.MAM})) {
|
|
|
+ converse.log("Attempted to fetch archived messages but this user's server doesn't support XEP-0313");
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ this.addSpinner();
|
|
|
+ converse_api.archive.query(_.extend(options, {'groupchat': true}),
|
|
|
+ function (messages) {
|
|
|
+ this.clearSpinner();
|
|
|
+ if (messages.length) {
|
|
|
+ _.map(messages, this.onChatRoomMessage.bind(this));
|
|
|
+ }
|
|
|
+ }.bind(this),
|
|
|
+ function () {
|
|
|
+ this.clearSpinner();
|
|
|
+ converse.log("Error while trying to fetch archived messages", "error");
|
|
|
+ }.bind(this)
|
|
|
+ );
|
|
|
+ }
|
|
|
+ });
|
|
|
+
|
|
|
+ converse.RoomsPanel = Backbone.View.extend({
|
|
|
+ /* Backbone View which renders the "Rooms" tab and accompanying
|
|
|
+ * panel in the control box.
|
|
|
+ *
|
|
|
+ * In this panel, chat rooms can be listed, joined and new rooms
|
|
|
+ * can be created.
|
|
|
+ */
|
|
|
+ tagName: 'div',
|
|
|
+ className: 'controlbox-pane',
|
|
|
+ id: 'chatrooms',
|
|
|
+ events: {
|
|
|
+ 'submit form.add-chatroom': 'createChatRoom',
|
|
|
+ 'click input#show-rooms': 'showRooms',
|
|
|
+ 'click a.open-room': 'createChatRoom',
|
|
|
+ 'click a.room-info': 'showRoomInfo',
|
|
|
+ 'change input[name=server]': 'setDomain',
|
|
|
+ 'change input[name=nick]': 'setNick'
|
|
|
+ },
|
|
|
+
|
|
|
+ initialize: function (cfg) {
|
|
|
+ this.$parent = cfg.$parent;
|
|
|
+ this.model.on('change:muc_domain', this.onDomainChange, this);
|
|
|
+ this.model.on('change:nick', this.onNickChange, this);
|
|
|
+ },
|
|
|
+
|
|
|
+ render: function () {
|
|
|
+ this.$parent.append(
|
|
|
+ this.$el.html(
|
|
|
+ converse.templates.room_panel({
|
|
|
+ 'server_input_type': converse.hide_muc_server && 'hidden' || 'text',
|
|
|
+ 'server_label_global_attr': converse.hide_muc_server && ' hidden' || '',
|
|
|
+ 'label_room_name': __('Room name'),
|
|
|
+ 'label_nickname': __('Nickname'),
|
|
|
+ 'label_server': __('Server'),
|
|
|
+ 'label_join': __('Join Room'),
|
|
|
+ 'label_show_rooms': __('Show rooms')
|
|
|
+ })
|
|
|
+ ).hide());
|
|
|
+ this.$tabs = this.$parent.parent().find('#controlbox-tabs');
|
|
|
+ this.$tabs.append(converse.templates.chatrooms_tab({label_rooms: __('Rooms')}));
|
|
|
+ return this;
|
|
|
+ },
|
|
|
+
|
|
|
+ onDomainChange: function (model) {
|
|
|
+ var $server = this.$el.find('input.new-chatroom-server');
|
|
|
+ $server.val(model.get('muc_domain'));
|
|
|
+ if (converse.auto_list_rooms) {
|
|
|
+ this.updateRoomsList();
|
|
|
+ }
|
|
|
+ },
|
|
|
+
|
|
|
+ onNickChange: function (model) {
|
|
|
+ var $nick = this.$el.find('input.new-chatroom-nick');
|
|
|
+ $nick.val(model.get('nick'));
|
|
|
+ },
|
|
|
+
|
|
|
+ informNoRoomsFound: function () {
|
|
|
+ var $available_chatrooms = this.$el.find('#available-chatrooms');
|
|
|
+ // For translators: %1$s is a variable and will be replaced with the XMPP server name
|
|
|
+ $available_chatrooms.html('<dt>'+__('No rooms on %1$s',this.model.get('muc_domain'))+'</dt>');
|
|
|
+ $('input#show-rooms').show().siblings('span.spinner').remove();
|
|
|
+ },
|
|
|
+
|
|
|
+ onRoomsFound: function (iq) {
|
|
|
+ /* Handle the IQ stanza returned from the server, containing
|
|
|
+ * all its public rooms.
|
|
|
+ */
|
|
|
+ var name, jid, i, fragment,
|
|
|
+ $available_chatrooms = this.$el.find('#available-chatrooms');
|
|
|
+ this.rooms = $(iq).find('query').find('item');
|
|
|
+ if (this.rooms.length) {
|
|
|
+ // For translators: %1$s is a variable and will be
|
|
|
+ // replaced with the XMPP server name
|
|
|
+ $available_chatrooms.html('<dt>'+__('Rooms on %1$s',this.model.get('muc_domain'))+'</dt>');
|
|
|
+ fragment = document.createDocumentFragment();
|
|
|
+ for (i=0; i<this.rooms.length; i++) {
|
|
|
+ name = Strophe.unescapeNode($(this.rooms[i]).attr('name')||$(this.rooms[i]).attr('jid'));
|
|
|
+ jid = $(this.rooms[i]).attr('jid');
|
|
|
+ fragment.appendChild($(
|
|
|
+ converse.templates.room_item({
|
|
|
+ 'name':name,
|
|
|
+ 'jid':jid,
|
|
|
+ 'open_title': __('Click to open this room'),
|
|
|
+ 'info_title': __('Show more information on this room')
|
|
|
+ })
|
|
|
+ )[0]);
|
|
|
+ }
|
|
|
+ $available_chatrooms.append(fragment);
|
|
|
+ $('input#show-rooms').show().siblings('span.spinner').remove();
|
|
|
+ } else {
|
|
|
+ this.informNoRoomsFound();
|
|
|
+ }
|
|
|
+ return true;
|
|
|
+ },
|
|
|
+
|
|
|
+ updateRoomsList: function () {
|
|
|
+ /* Send and IQ stanza to the server asking for all rooms
|
|
|
+ */
|
|
|
+ converse.connection.sendIQ(
|
|
|
+ $iq({
|
|
|
+ to: this.model.get('muc_domain'),
|
|
|
+ from: converse.connection.jid,
|
|
|
+ type: "get"
|
|
|
+ }).c("query", {xmlns: Strophe.NS.DISCO_ITEMS}),
|
|
|
+ this.onRoomsFound.bind(this),
|
|
|
+ this.informNoRoomsFound.bind(this)
|
|
|
+ );
|
|
|
+ },
|
|
|
+
|
|
|
+ showRooms: function () {
|
|
|
+ var $available_chatrooms = this.$el.find('#available-chatrooms');
|
|
|
+ var $server = this.$el.find('input.new-chatroom-server');
|
|
|
+ var server = $server.val();
|
|
|
+ if (!server) {
|
|
|
+ $server.addClass('error');
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ this.$el.find('input.new-chatroom-name').removeClass('error');
|
|
|
+ $server.removeClass('error');
|
|
|
+ $available_chatrooms.empty();
|
|
|
+ $('input#show-rooms').hide().after('<span class="spinner"/>');
|
|
|
+ this.model.save({muc_domain: server});
|
|
|
+ this.updateRoomsList();
|
|
|
+ },
|
|
|
+
|
|
|
+ showRoomInfo: function (ev) {
|
|
|
+ var target = ev.target,
|
|
|
+ $dd = $(target).parent('dd'),
|
|
|
+ $div = $dd.find('div.room-info');
|
|
|
+ if ($div.length) {
|
|
|
+ $div.remove();
|
|
|
+ } else {
|
|
|
+ $dd.find('span.spinner').remove();
|
|
|
+ $dd.append('<span class="spinner hor_centered"/>');
|
|
|
+ converse.connection.disco.info(
|
|
|
+ $(target).attr('data-room-jid'),
|
|
|
+ null,
|
|
|
+ function (stanza) {
|
|
|
+ var $stanza = $(stanza);
|
|
|
+ // All MUC features found here: http://xmpp.org/registrar/disco-features.html
|
|
|
+ $dd.find('span.spinner').replaceWith(
|
|
|
+ converse.templates.room_description({
|
|
|
+ 'desc': $stanza.find('field[var="muc#roominfo_description"] value').text(),
|
|
|
+ 'occ': $stanza.find('field[var="muc#roominfo_occupants"] value').text(),
|
|
|
+ 'hidden': $stanza.find('feature[var="muc_hidden"]').length,
|
|
|
+ 'membersonly': $stanza.find('feature[var="muc_membersonly"]').length,
|
|
|
+ 'moderated': $stanza.find('feature[var="muc_moderated"]').length,
|
|
|
+ 'nonanonymous': $stanza.find('feature[var="muc_nonanonymous"]').length,
|
|
|
+ 'open': $stanza.find('feature[var="muc_open"]').length,
|
|
|
+ 'passwordprotected': $stanza.find('feature[var="muc_passwordprotected"]').length,
|
|
|
+ 'persistent': $stanza.find('feature[var="muc_persistent"]').length,
|
|
|
+ 'publicroom': $stanza.find('feature[var="muc_public"]').length,
|
|
|
+ 'semianonymous': $stanza.find('feature[var="muc_semianonymous"]').length,
|
|
|
+ 'temporary': $stanza.find('feature[var="muc_temporary"]').length,
|
|
|
+ 'unmoderated': $stanza.find('feature[var="muc_unmoderated"]').length,
|
|
|
+ 'label_desc': __('Description:'),
|
|
|
+ 'label_occ': __('Occupants:'),
|
|
|
+ 'label_features': __('Features:'),
|
|
|
+ 'label_requires_auth': __('Requires authentication'),
|
|
|
+ 'label_hidden': __('Hidden'),
|
|
|
+ 'label_requires_invite': __('Requires an invitation'),
|
|
|
+ 'label_moderated': __('Moderated'),
|
|
|
+ 'label_non_anon': __('Non-anonymous'),
|
|
|
+ 'label_open_room': __('Open room'),
|
|
|
+ 'label_permanent_room': __('Permanent room'),
|
|
|
+ 'label_public': __('Public'),
|
|
|
+ 'label_semi_anon': __('Semi-anonymous'),
|
|
|
+ 'label_temp_room': __('Temporary room'),
|
|
|
+ 'label_unmoderated': __('Unmoderated')
|
|
|
+ }));
|
|
|
+ }.bind(this));
|
|
|
+ }
|
|
|
+ },
|
|
|
+
|
|
|
+ createChatRoom: function (ev) {
|
|
|
+ ev.preventDefault();
|
|
|
+ var name, $name,
|
|
|
+ server, $server,
|
|
|
+ jid,
|
|
|
+ $nick = this.$el.find('input.new-chatroom-nick'),
|
|
|
+ nick = $nick.val(),
|
|
|
+ chatroom;
|
|
|
+
|
|
|
+ if (!nick) { $nick.addClass('error'); }
|
|
|
+ else { $nick.removeClass('error'); }
|
|
|
+
|
|
|
+ if (ev.type === 'click') {
|
|
|
+ name = $(ev.target).text();
|
|
|
+ jid = $(ev.target).attr('data-room-jid');
|
|
|
+ } else {
|
|
|
+ $name = this.$el.find('input.new-chatroom-name');
|
|
|
+ $server= this.$el.find('input.new-chatroom-server');
|
|
|
+ server = $server.val();
|
|
|
+ name = $name.val().trim();
|
|
|
+ $name.val(''); // Clear the input
|
|
|
+ if (name && server) {
|
|
|
+ jid = Strophe.escapeNode(name.toLowerCase()) + '@' + server.toLowerCase();
|
|
|
+ $name.removeClass('error');
|
|
|
+ $server.removeClass('error');
|
|
|
+ this.model.save({muc_domain: server});
|
|
|
+ } else {
|
|
|
+ if (!name) { $name.addClass('error'); }
|
|
|
+ if (!server) { $server.addClass('error'); }
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ if (!nick) { return; }
|
|
|
+ chatroom = converse.chatboxviews.showChat({
|
|
|
+ 'id': jid,
|
|
|
+ 'jid': jid,
|
|
|
+ 'name': name || Strophe.unescapeNode(Strophe.getNodeFromJid(jid)),
|
|
|
+ 'nick': nick,
|
|
|
+ 'chatroom': true,
|
|
|
+ 'box_id': b64_sha1(jid)
|
|
|
+ });
|
|
|
+ },
|
|
|
+
|
|
|
+ setDomain: function (ev) {
|
|
|
+ this.model.save({muc_domain: ev.target.value});
|
|
|
+ },
|
|
|
+
|
|
|
+ setNick: function (ev) {
|
|
|
+ this.model.save({nick: ev.target.value});
|
|
|
+ }
|
|
|
+ });
|
|
|
+
|
|
|
+ _.extend(converse_api, {
|
|
|
+ /* We extend the default converse.js API to add methods specific to MUC
|
|
|
+ * chat rooms.
|
|
|
+ */
|
|
|
+ 'rooms': {
|
|
|
+ 'open': function (jids, nick) {
|
|
|
+ if (!nick) {
|
|
|
+ nick = Strophe.getNodeFromJid(converse.bare_jid);
|
|
|
+ }
|
|
|
+ if (typeof nick !== "string") {
|
|
|
+ throw new TypeError('rooms.open: invalid nick, must be string');
|
|
|
+ }
|
|
|
+ var _transform = function (jid) {
|
|
|
+ jid = jid.toLowerCase();
|
|
|
+ var chatroom = converse.chatboxes.get(jid);
|
|
|
+ converse.log('jid');
|
|
|
+ if (!chatroom) {
|
|
|
+ chatroom = converse.chatboxviews.showChat({
|
|
|
+ 'id': jid,
|
|
|
+ 'jid': jid,
|
|
|
+ 'name': Strophe.unescapeNode(Strophe.getNodeFromJid(jid)),
|
|
|
+ 'nick': nick,
|
|
|
+ 'chatroom': true,
|
|
|
+ 'box_id': b64_sha1(jid)
|
|
|
+ });
|
|
|
+ }
|
|
|
+ return converse.wrappedChatBox(converse.chatboxes.getChatBox(jid, true));
|
|
|
+ };
|
|
|
+ if (typeof jids === "undefined") {
|
|
|
+ throw new TypeError('rooms.open: You need to provide at least one JID');
|
|
|
+ } else if (typeof jids === "string") {
|
|
|
+ return _transform(jids);
|
|
|
+ }
|
|
|
+ return _.map(jids, _transform);
|
|
|
+ },
|
|
|
+ 'get': function (jids) {
|
|
|
+ if (typeof jids === "undefined") {
|
|
|
+ throw new TypeError("rooms.get: You need to provide at least one JID");
|
|
|
+ } else if (typeof jids === "string") {
|
|
|
+ return converse.wrappedChatBox(converse.chatboxes.getChatBox(jids, true));
|
|
|
+ }
|
|
|
+ return _.map(jids, _.partial(converse.wrappedChatBox, _.bind(converse.chatboxes.getChatBox, converse.chatboxes, _, true)));
|
|
|
+
|
|
|
+ }
|
|
|
+ }
|
|
|
+ });
|
|
|
+ }
|
|
|
+ });
|
|
|
+}));
|