/**
* Implements Multi-User Chat (XEP-0045).
*
* @namespace jsxc.muc
*/
jsxc.muc = {
/** strophe connection */
conn: null,
/** some constants */
CONST: {
AFFILIATION: {
ADMIN: 'admin',
MEMBER: 'member',
OUTCAST: 'outcast',
OWNER: 'owner',
NONE: 'none'
},
ROLE: {
MODERATOR: 'moderator',
PARTICIPANT: 'participant',
VISITOR: 'visitor',
NONE: 'none'
},
ROOMSTATE: {
INIT: 0,
ENTERED: 1,
EXITED: 2,
AWAIT_DESTRUCTION: 3,
DESTROYED: 4
},
ROOMCONFIG: {
INSTANT: 'instant'
}
},
/**
* Initialize muc plugin.
*
* @private
* @memberof jsxc.muc
* @param {object} o Options
*/
init: function(o) {
var self = jsxc.muc;
self.conn = jsxc.xmpp.conn;
var options = o || jsxc.options.get('muc');
if (!options || typeof options.server !== 'string') {
jsxc.debug('Discover muc service');
// prosody does not respond, if we send query before initial presence was sent
setTimeout(function() {
self.conn.disco.items(Strophe.getDomainFromJid(self.conn.jid), null, function(items) {
$(items).find('item').each(function() {
var jid = $(this).attr('jid');
var discovered = false;
self.conn.disco.info(jid, null, function(info) {
var mucFeature = $(info).find('feature[var="' + Strophe.NS.MUC + '"]');
var mucIdentity = $(info).find('identity[category="conference"][type="text"]');
if (mucFeature.length > 0 && mucIdentity.length > 0) {
jsxc.debug('muc service found', jid);
jsxc.options.set('muc', {
server: jid,
name: $(info).find('identity').attr('name')
});
discovered = true;
self.init();
}
});
return !discovered;
});
});
}, 1000);
return;
}
if (jsxc.gui.roster.ready) {
self.initMenu();
} else {
$(document).one('ready.roster.jsxc', jsxc.muc.initMenu);
}
// remove maybe previously attached handlers
$(document).off('presence.jsxc', jsxc.muc.onPresence);
$(document).off('error.presence.jsxc', jsxc.muc.onPresenceError);
$(document).on('presence.jsxc', jsxc.muc.onPresence);
$(document).on('error.presence.jsxc', jsxc.muc.onPresenceError);
self.conn.addHandler(self.onGroupchatMessage, null, 'message', 'groupchat');
self.conn.muc.roomNames = jsxc.storage.getUserItem('roomNames') || [];
},
/**
* Add entry to menu.
*
* @memberOf jsxc.muc
*/
initMenu: function() {
var li = $('<li>').attr('class', 'jsxc_joinChat jsxc_groupcontacticon').text($.t('Join_chat'));
li.click(jsxc.muc.showJoinChat);
if ($('#jsxc_menu .jsxc_joinChat').length === 0) {
$('#jsxc_menu ul .jsxc_about').before(li);
}
},
/**
* Open join dialog.
*
* @memberOf jsxc.muc
* @param {string} [r] - room jid
* @param {string} [p] - room password
*/
showJoinChat: function(r, p) {
var self = jsxc.muc;
var dialog = jsxc.gui.dialog.open(jsxc.gui.template.get('joinChat'));
// @TODO split this monster function apart
// hide second step button
dialog.find('.jsxc_join').hide();
// prepopulate room jid
if (typeof r === 'string') {
dialog.find('#jsxc_room').val(r);
}
// prepopulate room password
if (typeof p === 'string') {
dialog.find('#jsxc_password').val(p);
}
// display conference server
var serverInputTimeout;
dialog.find('#jsxc_server').val(jsxc.options.get('muc').server);
dialog.find('#jsxc_server').on('input', function() {
var self = $(this);
if (serverInputTimeout) {
clearTimeout(serverInputTimeout);
dialog.find('.jsxc_inputinfo.jsxc_room').hide();
}
dialog.find('.jsxc_inputinfo.jsxc_server').hide().text('');
dialog.find('#jsxc_server').removeClass('jsxc_invalid');
if (self.val() && self.val().match(/^[.-0-9a-zA-Z]+$/i)) {
dialog.find('.jsxc_inputinfo.jsxc_room').show().addClass('jsxc_waiting');
serverInputTimeout = setTimeout(function() {
loadRoomList(self.val());
}, 1800);
}
}).trigger('input');
// handle error response
var error_handler = function(event, condition, room) {
var msg;
switch (condition) {
case 'not-authorized':
// password-protected room
msg = $.t('A_password_is_required');
break;
case 'registration-required':
// members-only room
msg = $.t('You_are_not_on_the_member_list');
break;
case 'forbidden':
// banned users
msg = $.t('You_are_banned_from_this_room');
break;
case 'conflict':
// nickname conflict
msg = $.t('Your_desired_nickname_');
break;
case 'service-unavailable':
// max users
msg = $.t('The_maximum_number_');
break;
case 'item-not-found':
// locked or non-existing room
msg = $.t('This_room_is_locked_');
break;
case 'not-allowed':
// room creation is restricted
msg = $.t('You_are_not_allowed_to_create_');
break;
default:
jsxc.warn('Unknown muc error condition: ' + condition);
msg = $.t('Error') + ': ' + condition;
}
// clean up strophe.muc rooms
var roomIndex = self.conn.muc.roomNames.indexOf(room);
if (roomIndex > -1) {
self.conn.muc.roomNames.splice(roomIndex, 1);
delete self.conn.muc.rooms[room];
}
$('<p>').addClass('jsxc_warning').text(msg).appendTo(dialog.find('.jsxc_msg'));
};
$(document).on('error.muc.jsxc', error_handler);
$(document).on('close.dialog.jsxc', function() {
$(document).off('error.muc.jsxc', error_handler);
});
dialog.find('#jsxc_nickname').attr('placeholder', Strophe.getNodeFromJid(self.conn.jid));
dialog.find('#jsxc_bookmark').change(function() {
if ($(this).prop('checked')) {
$('#jsxc_autojoin').prop('disabled', false);
$('#jsxc_autojoin').parent('.checkbox').removeClass('disabled');
} else {
$('#jsxc_autojoin').prop('disabled', true).prop('checked', false);
$('#jsxc_autojoin').parent('.checkbox').addClass('disabled');
}
});
dialog.find('.jsxc_continue').click(function(ev) {
ev.preventDefault();
var room = ($('#jsxc_room').val()) ? jsxc.jidToBid($('#jsxc_room').val()) : null;
var nickname = $('#jsxc_nickname').val() || Strophe.getNodeFromJid(self.conn.jid);
var server = dialog.find('#jsxc_server').val();
if (!room || !room.match(/^[^"&\'\/:<>@\s]+$/i)) {
$('#jsxc_room').addClass('jsxc_invalid').keyup(function() {
if ($(this).val()) {
$(this).removeClass('jsxc_invalid');
}
});
return false;
}
if (dialog.find('#jsxc_server').hasClass('jsxc_invalid')) {
return false;
}
if (!room.match(/@(.*)$/)) {
room += '@' + server;
}
if (jsxc.xmpp.conn.muc.roomNames.indexOf(room) < 0) {
// not already joined
var discoReceived = function(roomName, subject) {
// we received the room information
jsxc.gui.dialog.resize();
dialog.find('.jsxc_continue').hide();
dialog.find('.jsxc_join').show().effect('highlight', {
color: 'green'
}, 4000);
dialog.find('.jsxc_join').click(function(ev) {
ev.preventDefault();
var bookmark = $("#jsxc_bookmark").prop("checked");
var autojoin = $('#jsxc_autojoin').prop('checked');
var password = $('#jsxc_password').val() || null;
// clean up
jsxc.gui.window.clear(room);
jsxc.storage.setUserItem('member', room, {});
self.join(room, nickname, password, roomName, subject, bookmark, autojoin);
return false;
});
};
dialog.find('.jsxc_msg').append($('<p>').text($.t('Loading_room_information')).addClass('jsxc_waiting'));
jsxc.gui.dialog.resize();
self.conn.disco.info(room, null, function(stanza) {
dialog.find('.jsxc_msg').html('<p>' + $.t('This_room_is') + '</p>');
var table = $('<table>');
$(stanza).find('feature').each(function() {
var feature = $(this).attr('var');
if (feature !== '' && i18next.exists(feature)) {
var tr = $('<tr>');
$('<td>').text($.t(feature + '.keyword')).appendTo(tr);
$('<td>').text($.t(feature + '.description')).appendTo(tr);
tr.appendTo(table);
}
if (feature === 'muc_passwordprotected') {
dialog.find('#jsxc_password').parents('.form-group').removeClass('jsxc_hidden');
dialog.find('#jsxc_password').attr('required', 'required');
dialog.find('#jsxc_password').addClass('jsxc_invalid');
}
});
dialog.find('.jsxc_msg').append(table);
var roomName = $(stanza).find('identity').attr('name');
var subject = $(stanza).find('field[var="muc#roominfo_subject"]').attr('label');
//@TODO display subject, number of occupants, etc.
discoReceived(roomName, subject);
}, function() {
dialog.find('.jsxc_msg').empty();
$('<p>').text($.t('Room_not_found_')).appendTo(dialog.find('.jsxc_msg'));
discoReceived();
});
} else {
$('<p>').addClass('jsxc_warning').text($.t('You_already_joined_this_room')).appendTo(dialog.find('.jsxc_msg'));
}
return false;
});
dialog.find('input').keydown(function(ev) {
if (ev.which !== 13) {
// reset messages and room information
dialog.find('.jsxc_warning').remove();
if (dialog.find('.jsxc_continue').is(":hidden") && $(this).attr('id') !== 'jsxc_password') {
dialog.find('.jsxc_continue').show();
dialog.find('.jsxc_join').hide().off('click');
dialog.find('.jsxc_msg').empty();
dialog.find('#jsxc_password').parents('.form-group').addClass('jsxc_hidden');
dialog.find('#jsxc_password').attr('required', '');
dialog.find('#jsxc_password').removeClass('jsxc_invalid');
jsxc.gui.dialog.resize();
}
return;
}
if (!dialog.find('.jsxc_continue').is(":hidden")) {
dialog.find('.jsxc_continue').click();
} else {
dialog.find('.jsxc_join').click();
}
});
function loadRoomList(server) {
if (!server) {
dialog.find('.jsxc_inputinfo').hide();
return;
}
// load room list
self.conn.muc.listRooms(server, function(stanza) {
// workaround: chrome does not display dropdown arrow for dynamically filled datalists
$('#jsxc_roomlist option:last').remove();
$(stanza).find('item').each(function() {
var r = $('<option>');
var rjid = $(this).attr('jid').toLowerCase();
var rnode = Strophe.getNodeFromJid(rjid);
var rname = $(this).attr('name') || rnode;
r.text(rname);
r.attr('data-jid', rjid);
r.attr('value', rnode);
$('#jsxc_roomlist select').append(r);
});
var set = $(stanza).find('set[xmlns="http://jabber.org/protocol/rsm"]');
if (set.length > 0) {
var count = set.find('count').text() || '?';
dialog.find('.jsxc_inputinfo').show().removeClass('jsxc_waiting').text($.t('Could_load_only', {
count: count
}));
} else {
dialog.find('.jsxc_inputinfo').hide();
}
}, function(stanza) {
var errTextMsg = $(stanza).find('error text').text() || null;
jsxc.warn('Could not load rooms', errTextMsg);
if (errTextMsg) {
dialog.find('.jsxc_inputinfo.jsxc_server').show().text(errTextMsg);
}
if ($(stanza).find('error remote-server-not-found')) {
dialog.find('#jsxc_server').addClass('jsxc_invalid');
}
dialog.find('.jsxc_inputinfo.jsxc_room').hide();
});
}
},
/**
* Request and show room configuration.
*
* @memberOf jsxc.muc
* @param {string} room - room jid
*/
showRoomConfiguration: function(room) {
var self = jsxc.muc;
self.conn.muc.configure(room, function(stanza) {
var form = Strophe.x.Form.fromXML(stanza);
window.f = form;
self._showRoomConfiguration(room, form);
}, function() {
jsxc.debug('Could not load room configuration');
//@TODO show error
});
},
/**
* Show room configuration.
*
* @private
* @memberOf jsxc.muc
* @param {string} room - room jid
* @param {Strophe.x.Form} config - current room config as Form object
*/
_showRoomConfiguration: function(room, config) {
var self = jsxc.muc;
var dialog = jsxc.gui.dialog.open(jsxc.muc.helper.formToHTML(config));
var form = dialog.find('form');
// work around Strophe.x behaviour
form.find('[type="checkbox"]').change(function() {
$(this).val(this.checked ? 1 : 0);
});
var submit = $('<button>');
submit.addClass('btn btn-primary');
submit.attr('type', 'submit');
submit.text($.t('Save'));
var cancel = $('<button>');
cancel.addClass('btn btn-default');
cancel.attr('type', 'button');
cancel.text($.t('Cancel'));
var formGroup = $('<div>');
formGroup.addClass('form-group');
$('<div>').addClass('col-sm-offset-6 col-sm-6').appendTo(formGroup);
formGroup.find('>div').append(cancel);
formGroup.find('>div').append(submit);
form.append(formGroup);
form.submit(function(ev) {
ev.preventDefault();
var config = Strophe.x.Form.fromHTML(form.get(0));
self.conn.muc.saveConfiguration(room, config, function() {
jsxc.storage.updateUserItem('buddy', room, 'config', config);
jsxc.debug('Room configuration saved.');
}, function() {
jsxc.warn('Could not save room configuration.');
//@TODO display error
});
jsxc.gui.dialog.close();
return false;
});
cancel.click(function() {
self.conn.muc.cancelConfigure(room);
jsxc.gui.dialog.close();
});
},
/**
* Join the given room.
*
* @memberOf jsxc.muc
* @param {string} room Room jid
* @param {string} nickname Desired nickname
* @param {string} [password] Password
* @param {string} [roomName] Room alias
* @param {string} [subject] Current subject
*/
join: function(room, nickname, password, roomName, subject, bookmark, autojoin) {
var self = jsxc.muc;
jsxc.storage.setUserItem('buddy', room, {
jid: room,
name: roomName || room,
sub: 'both',
type: 'groupchat',
state: self.CONST.ROOMSTATE.INIT,
subject: subject,
bookmarked: bookmark || false,
autojoin: autojoin || false,
nickname: nickname,
config: null
});
jsxc.xmpp.conn.muc.join(room, nickname, null, null, null, password);
if (bookmark) {
jsxc.xmpp.bookmarks.add(room, roomName, nickname, autojoin);
}
},
/**
* Leave given room.
*
* @memberOf jsxc.muc
* @param {string} room Room jid
*/
leave: function(room) {
if (!jsxc.master) {
jsxc.tab.execMaster('muc.leave', room);
return;
}
var self = jsxc.muc;
var own = jsxc.storage.getUserItem('ownNicknames') || {};
var data = jsxc.storage.getUserItem('buddy', room) || {};
if (data.state === self.CONST.ROOMSTATE.ENTERED) {
self.conn.muc.leave(room, own[room], function() {
self.onExited(room);
});
} else {
self.onExited(room);
}
},
/**
* Clean up after we exited a room.
*
* @private
* @memberOf jsxc.muc
* @param {string} room Room jid
*/
onExited: function(room) {
var self = jsxc.muc;
var own = jsxc.storage.getUserItem('ownNicknames') || {};
var roomdata = jsxc.storage.getUserItem('buddy', room) || {};
jsxc.storage.setUserItem('roomNames', self.conn.muc.roomNames);
delete own[room];
jsxc.storage.setUserItem('ownNicknames', own);
jsxc.storage.removeUserItem('member', room);
jsxc.storage.removeUserItem('chat', room);
jsxc.gui.window.close(room);
jsxc.storage.updateUserItem('buddy', room, 'state', self.CONST.ROOMSTATE.EXITED);
if (!roomdata.bookmarked) {
jsxc.gui.roster.purge(room);
}
},
/**
* Destroy the given room.
*
* @memberOf jsxc.muc
* @param {string} room Room jid
* @param {function} handler_cb Function to handle the successful destruction
* @param {function} error_cb Function to handle an error
*/
destroy: function(room, handler_cb, error_cb) {
if (!jsxc.master) {
jsxc.tab.execMaster('muc.destroy', room);
return;
}
var self = jsxc.muc;
var roomdata = jsxc.storage.getUserItem('buddy', room);
jsxc.storage.updateUserItem('buddy', room, 'state', self.CONST.ROOMSTATE.AWAIT_DESTRUCTION);
jsxc.gui.window.postMessage({
bid: room,
direction: jsxc.Message.SYS,
msg: $.t('This_room_will_be_closed')
});
var iq = $iq({
to: room,
type: "set"
}).c("query", {
xmlns: Strophe.NS.MUC_OWNER
}).c("destroy");
jsxc.muc.conn.sendIQ(iq.tree(), handler_cb, error_cb);
if (roomdata.bookmarked) {
jsxc.xmpp.bookmarks.delete(room);
}
},
/**
* Close the given room.
*
* @memberOf jsxc.muc
* @param room Room jid
*/
close: function(room) {
var self = jsxc.muc;
var roomdata = jsxc.storage.getUserItem('buddy', room) || {};
self.emptyMembers(room);
var roomIndex = self.conn.muc.roomNames.indexOf(room);
if (roomIndex > -1) {
self.conn.muc.roomNames.splice(roomIndex, 1);
delete self.conn.muc.rooms[room];
}
jsxc.storage.setUserItem('roomNames', self.conn.muc.roomNames);
if (roomdata.state === self.CONST.ROOMSTATE.AWAIT_DESTRUCTION) {
self.onExited(room);
}
if (jsxc.storage.getUserItem('budy', room)) {
roomdata.state = self.CONST.ROOMSTATE.DESTROYED;
jsxc.storage.setUserItem('buddy', room, roomdata);
}
},
/**
* Init group chat window.
*
* @private
* @memberOf jsxc.muc
* @param event Event
* @param {jQuery} win Window object
*/
initWindow: function(event, win) {
var self = jsxc.muc;
if (!jsxc.xmpp.conn && jsxc.master) {
$(document).one('attached.jsxc', function() {
self.initWindow(null, win);
});
return;
}
var data = win.data();
var bid = jsxc.jidToBid(data.jid);
var roomdata = jsxc.storage.getUserItem('buddy', bid);
if (roomdata.type !== 'groupchat') {
return;
}
win.addClass('jsxc_groupchat');
var own = jsxc.storage.getUserItem('ownNicknames') || {};
var ownNickname = own[bid];
var mlIcon = $('<div class="jsxc_members"></div>');
win.find('.jsxc_tools > .jsxc_settings').after(mlIcon);
var ml = $('<div class="jsxc_memberlist"><ul></ul></div>');
win.find('.jsxc_fade').prepend(ml);
ml.on('wheel', function(ev) {
jsxc.muc.scrollMemberListBy(bid, (ev.originalEvent.wheelDelta > 0) ? 50 : -50);
});
// toggle member list
var toggleMl = function(ev) {
if (ev) {
ev.preventDefault();
}
var slimOptions = {};
var ul = ml.find('ul:first');
var slimHeight = null;
ml.toggleClass('jsxc_expand');
if (ml.hasClass('jsxc_expand')) {
$('body').click();
$('body').one('click', toggleMl);
ul.mouseleave(function() {
ul.data('timer', window.setTimeout(toggleMl, 2000));
}).mouseenter(function() {
window.clearTimeout(ul.data('timer'));
}).css('left', '0px');
var maxHeight = win.find(".jsxc_textarea").height() * 0.8;
var innerHeight = ml.find('ul').height() + 3;
slimHeight = (innerHeight > maxHeight) ? maxHeight : innerHeight;
slimOptions = {
distance: '3px',
height: slimHeight + 'px',
width: '100%',
color: '#fff',
opacity: '0.5'
};
ml.css('height', slimHeight + 'px');
} else {
slimOptions = {
destroy: true
};
ul.attr('style', '');
ml.css('height', '');
window.clearTimeout(ul.data('timer'));
$('body').off('click', null, toggleMl);
ul.off('mouseleave mouseenter');
}
ul.slimscroll(slimOptions);
return false;
};
mlIcon.click(toggleMl);
win.on('resize', function() {
// update member list position
jsxc.muc.scrollMemberListBy(bid, 0);
});
var destroy = $('<a>');
destroy.attr('href', '#');
destroy.text($.t('Destroy'));
destroy.addClass('jsxc_destroy');
destroy.hide();
destroy.click(function() {
self.destroy(bid);
});
win.find('.jsxc_settings ul').append($('<li>').append(destroy));
var configure = $('<a>');
configure.attr('href', '#');
configure.text($.t('Configure'));
configure.addClass('jsxc_configure');
configure.hide();
configure.click(function() {
self.showRoomConfiguration(bid);
});
if (self.conn) {
win.find('.jsxc_settings ul').append($('<li>').append(configure));
}
if (roomdata.state > self.CONST.ROOMSTATE.INIT) {
var member = jsxc.storage.getUserItem('member', bid) || {};
$.each(member, function(nickname, val) {
self.insertMember(bid, nickname, val);
if (nickname === ownNickname && val.affiliation === self.CONST.AFFILIATION.OWNER) {
destroy.show();
}
if (nickname === ownNickname && (val.affiliation === self.CONST.AFFILIATION.OWNER || val.affiliation === self.CONST.AFFILIATION.OWNER)) {
configure.show();
}
});
}
var leave = $('<a>');
leave.attr('href', '#');
leave.text($.t('Leave'));
leave.addClass('jsxc_leave');
leave.click(function() {
self.leave(bid);
});
win.find('.jsxc_settings ul').append($('<li>').append(leave));
},
/**
* Triggered on incoming presence stanzas.
*
* @private
* @memberOf jsxc.muc
* @param event
* @param {string} from Jid
* @param {integer} status Online status between 0 and 5
* @param {string} presence Presence stanza
*/
onPresence: function(event, from, status, presence) {
var self = jsxc.muc;
var room = jsxc.jidToBid(from);
var roomdata = jsxc.storage.getUserItem('buddy', room);
var xdata = $(presence).find('x[xmlns^="' + Strophe.NS.MUC + '"]');
if (self.conn.muc.roomNames.indexOf(room) < 0 || xdata.length === 0) {
return true;
}
var res = Strophe.getResourceFromJid(from) || '';
var nickname = Strophe.unescapeNode(res);
var own = jsxc.storage.getUserItem('ownNicknames') || {};
var member = jsxc.storage.getUserItem('member', room) || {};
var codes = [];
xdata.find('status').each(function() {
var code = $(this).attr('code');
jsxc.debug('[muc][code]', code);
codes.push(code);
});
if (roomdata.state === self.CONST.ROOMSTATE.INIT) {
// successfully joined
roomdata.status = jsxc.CONST.STATUS.indexOf('online');
jsxc.storage.setUserItem('buddy', room, roomdata);
jsxc.storage.setUserItem('roomNames', jsxc.xmpp.conn.muc.roomNames);
if (jsxc.gui.roster.getItem(room).length === 0) {
var bl = jsxc.storage.getUserItem('buddylist');
bl.push(room);
jsxc.storage.setUserItem('buddylist', bl);
jsxc.gui.roster.add(room);
}
if ($('#jsxc_dialog').length > 0) {
// User joined the room manually
jsxc.gui.dialog.close();
jsxc.gui.window.open(room);
}
}
var jid = xdata.find('item').attr('jid') || null;
if (status === 0) {
if (xdata.find('destroy').length > 0) {
// room has been destroyed
member = {};
jsxc.gui.window.postMessage({
bid: room,
direction: jsxc.Message.SYS,
msg: $.t('This_room_has_been_closed')
});
self.close(room);
} else {
delete member[nickname];
self.removeMember(room, nickname);
var newNickname = xdata.find('item').attr('nick');
if (codes.indexOf('303') > -1 && newNickname) {
// user changed his nickname
newNickname = Strophe.unescapeNode(newNickname);
// prevent to display enter message
member[newNickname] = {};
jsxc.gui.window.postMessage({
bid: room,
direction: jsxc.Message.SYS,
msg: $.t('is_now_known_as', {
oldNickname: nickname,
newNickname: newNickname,
escapeInterpolation: true
})
});
} else if (codes.length === 0 || (codes.length === 1 && codes.indexOf('110') > -1)) {
// normal user exit
jsxc.gui.window.postMessage({
bid: room,
direction: jsxc.Message.SYS,
msg: $.t('left_the_building', {
nickname: nickname,
escapeInterpolation: true
})
});
}
}
} else {
// new member joined
if (!member[nickname] && own[room]) {
jsxc.gui.window.postMessage({
bid: room,
direction: jsxc.Message.SYS,
msg: $.t('entered_the_room', {
nickname: nickname,
escapeInterpolation: true
})
});
}
member[nickname] = {
jid: jid,
status: status,
roomJid: from,
affiliation: xdata.find('item').attr('affiliation'),
role: xdata.find('item').attr('role')
};
self.insertMember(room, nickname, member[nickname]);
}
jsxc.storage.setUserItem('member', room, member);
$.each(codes, function(index, code) {
// call code functions and trigger event
if (typeof self.onStatus[code] === 'function') {
self.onStatus[code].call(this, room, nickname, member[nickname] || {}, xdata);
}
$(document).trigger('status.muc.jsxc', [code, room, nickname, member[nickname] || {}, presence]);
});
return true;
},
/**
* Handle group chat presence errors.
*
* @memberOf jsxc.muc
* @param event
* @param {string} from Jid
* @param {string} presence Presence stanza
* @returns {Boolean} Returns true on success
*/
onPresenceError: function(event, from, presence) {
var self = jsxc.muc;
var xdata = $(presence).find('x[xmlns="' + Strophe.NS.MUC + '"]');
var room = jsxc.jidToBid(from);
if (xdata.length === 0 || self.conn.muc.roomNames.indexOf(room) < 0) {
return true;
}
var error = $(presence).find('error');
var condition = error.children()[0].tagName;
jsxc.debug('[muc][error]', condition);
$(document).trigger('error.muc.jsxc', [condition, room]);
return true;
},
/**
* Handle status codes. Every function gets room jid, nickname, member data and xdata.
*
* @memberOf jsxc.muc
*/
onStatus: {
/** Inform user that presence refers to itself */
110: function(room, nickname, data) {
var self = jsxc.muc;
var own = jsxc.storage.getUserItem('ownNicknames') || {};
own[room] = nickname;
jsxc.storage.setUserItem('ownNicknames', own);
if (data.affiliation === self.CONST.AFFILIATION.OWNER) {
jsxc.gui.window.get(room).find('.jsxc_destroy').show();
}
var roomdata = jsxc.storage.getUserItem('buddy', room);
if (roomdata.state === self.CONST.ROOMSTATE.INIT) {
roomdata.state = self.CONST.ROOMSTATE.ENTERED;
jsxc.storage.setUserItem('buddy', room, roomdata);
}
},
/** Inform occupants that room logging is now enabled */
170: function(room) {
jsxc.gui.window.postMessage({
bid: room,
direction: jsxc.Message.SYS,
msg: $.t('Room_logging_is_enabled')
});
},
/** Inform occupants that room logging is now disabled */
171: function(room) {
jsxc.gui.window.postMessage({
bid: room,
direction: jsxc.Message.SYS,
msg: $.t('Room_logging_is_disabled')
});
},
/** Inform occupants that the room is now non-anonymous */
172: function(room) {
jsxc.gui.window.postMessage({
bid: room,
direction: jsxc.Message.SYS,
msg: $.t('Room_is_now_non-anoymous')
});
},
/** Inform occupants that the room is now semi-anonymous */
173: function(room) {
jsxc.gui.window.postMessage({
bid: room,
direction: jsxc.Message.SYS,
msg: $.t('Room_is_now_semi-anonymous')
});
},
/** Inform user that a new room has been created */
201: function(room) {
var self = jsxc.muc;
var roomdata = jsxc.storage.getUserItem('buddy', room) || {};
if (roomdata.autojoin && roomdata.config === self.CONST.ROOMCONFIG.INSTANT) {
self.conn.muc.createInstantRoom(room);
} else if (roomdata.autojoin && typeof roomdata.config !== 'undefined' && roomdata.config !== null) {
self.conn.muc.saveConfiguration(room, roomdata.config, function() {
jsxc.debug('Cached room configuration saved.');
}, function() {
jsxc.warn('Could not save cached room configuration.');
//@TODO display error
});
} else {
jsxc.gui.showSelectionDialog({
header: $.t('Room_creation'),
msg: $.t('Do_you_want_to_change_the_default_room_configuration'),
primary: {
label: $.t('Default'),
cb: function() {
jsxc.gui.dialog.close();
self.conn.muc.createInstantRoom(room);
jsxc.storage.updateUserItem('buddy', room, 'config', self.CONST.ROOMCONFIG.INSTANT);
}
},
option: {
label: $.t('Change'),
cb: function() {
self.showRoomConfiguration(room);
}
}
});
}
},
/** Inform user that he or she has been banned */
301: function(room, nickname, data, xdata) {
var own = jsxc.storage.getUserItem('ownNicknames') || {};
if (own[room] === nickname) {
jsxc.muc.close(room);
jsxc.gui.window.postMessage({
bid: room,
direction: jsxc.Message.SYS,
msg: $.t('muc_removed_banned')
});
jsxc.muc.postReason(room, xdata);
} else {
jsxc.gui.window.postMessage({
bid: room,
direction: jsxc.Message.SYS,
msg: $.t('muc_removed_info_banned', {
nickname: nickname,
escapeInterpolation: true
})
});
}
},
/** Inform user that he or she has been kicked */
307: function(room, nickname, data, xdata) {
var own = jsxc.storage.getUserItem('ownNicknames') || {};
if (own[room] === nickname) {
jsxc.muc.close(room);
jsxc.gui.window.postMessage({
bid: room,
direction: jsxc.Message.SYS,
msg: $.t('muc_removed_kicked')
});
jsxc.muc.postReason(room, xdata);
} else {
jsxc.gui.window.postMessage({
bid: room,
direction: jsxc.Message.SYS,
msg: $.t('muc_removed_info_kicked', {
nickname: nickname,
escapeInterpolation: true
})
});
}
},
/** Inform user that he or she is beeing removed from the room because of an affiliation change */
321: function(room, nickname) {
var own = jsxc.storage.getUserItem('ownNicknames') || {};
if (own[room] === nickname) {
jsxc.muc.close(room);
jsxc.gui.window.postMessage({
bid: room,
direction: jsxc.Message.SYS,
msg: $.t('muc_removed_affiliation')
});
} else {
jsxc.gui.window.postMessage({
bid: room,
direction: jsxc.Message.SYS,
msg: $.t('muc_removed_info_affiliation', {
nickname: nickname,
escapeInterpolation: true
})
});
}
},
/**
* Inform user that he or she is beeing removed from the room because the room has been
* changed to members-only and the user is not a member
*/
322: function(room, nickname) {
var own = jsxc.storage.getUserItem('ownNicknames') || {};
if (own[room] === nickname) {
jsxc.muc.close(room);
jsxc.gui.window.postMessage({
bid: room,
direction: jsxc.Message.SYS,
msg: $.t('muc_removed_membersonly')
});
} else {
jsxc.gui.window.postMessage({
bid: room,
direction: jsxc.Message.SYS,
msg: $.t('muc_removed_info_membersonly', {
nickname: nickname,
escapeInterpolation: true
})
});
}
},
/**
* Inform user that he or she is beeing removed from the room because the MUC service
* is being shut down
*/
332: function(room) {
jsxc.muc.close(room);
jsxc.gui.window.postMessage({
bid: room,
direction: jsxc.Message.SYS,
msg: $.t('muc_removed_shutdown')
});
}
},
/**
* Extract reason from xdata and if available post it to room.
*
* @memberOf jsxc.muc
* @param {string} room Room jid
* @param {jQuery} xdata Xdata
*/
postReason: function(room, xdata) {
var actor = {
name: xdata.find('actor').attr('nick'),
jid: xdata.find('actor').attr('jid')
};
var reason = xdata.find('reason').text();
if (reason !== '') {
reason = $.t('Reason') + ': ' + reason;
if (typeof actor.name === 'string' || typeof actor.jid === 'string') {
jsxc.gui.window.postMessage({
bid: room,
direction: jsxc.Message.IN,
msg: reason,
sender: actor
});
} else {
jsxc.gui.window.postMessage({
bid: room,
direction: jsxc.Message.SYS,
msg: reason
});
}
}
},
/**
* Insert member to room member list.
*
* @memberOf jsxc.muc
* @param {string} room Room jid
* @param {string} nickname Nickname
* @param {string} memberdata Member data
*/
insertMember: function(room, nickname, memberdata) {
var win = jsxc.gui.window.get(room);
var jid = memberdata.jid;
var ownBid = jsxc.jidToBid(jsxc.storage.getItem('jid'));
var m = win.find('.jsxc_memberlist li[data-nickname="' + nickname + '"]');
if (m.length === 0) {
var title = jsxc.escapeHTML(nickname);
m = $('<li><div class="jsxc_avatar"></div><div class="jsxc_name"/></li>');
m.attr('data-nickname', nickname);
win.find('.jsxc_memberlist ul').append(m);
if (typeof jid === 'string') {
m.find('.jsxc_name').text(jsxc.jidToBid(jid));
title = title + '\n' + jsxc.jidToBid(jid);
var data = jsxc.storage.getUserItem('buddy', jsxc.jidToBid(jid));
if (data !== null && typeof data === 'object') {
jsxc.gui.avatar.update(m, jsxc.jidToBid(jid), data.avatar);
} else if (jsxc.jidToBid(jid) === ownBid) {
jsxc.gui.avatar.update(m, jsxc.jidToBid(jid), 'own');
}
} else {
m.find('.jsxc_name').text(nickname);
jsxc.gui.avatarPlaceholder(m.find('.jsxc_avatar'), nickname);
}
m.attr('title', title);
}
},
/**
* Remove member from room member list.
*
* @memberOf jsxc.muc
* @param {string} room Room jid
* @param {string} nickname Nickname
*/
removeMember: function(room, nickname) {
var win = jsxc.gui.window.get(room);
var m = win.find('.jsxc_memberlist li[data-nickname="' + nickname + '"]');
if (m.length > 0) {
m.remove();
}
},
/**
* Scroll or update member list position.
*
* @memberOf jsxc.muc
* @param {string} room Room jid
* @param {integer} offset =0: update position; >0: Scroll to left; <0: Scroll to right
*/
scrollMemberListBy: function(room, offset) {
var win = jsxc.gui.window.get(room);
if (win.find('.jsxc_memberlist').hasClass('jsxc_expand')) {
return;
}
var el = win.find('.jsxc_memberlist ul:first');
var scrollWidth = el.width();
var width = win.find('.jsxc_memberlist').width();
var left = parseInt(el.css('left'));
left = (isNaN(left)) ? 0 - offset : left - offset;
if (scrollWidth < width || left > 0) {
left = 0;
} else if (left < width - scrollWidth) {
left = width - scrollWidth;
}
el.css('left', left + 'px');
},
/**
* Empty member list.
*
* @memberOf jsxc.muc
* @param {string} room Room jid
*/
emptyMembers: function(room) {
var win = jsxc.gui.window.get(room);
win.find('.jsxc_memberlist').empty();
jsxc.storage.setUserItem('member', room, {});
},
/**
* Handle incoming group chat message.
*
* @private
* @memberOf jsxc.muc
* @param {string} message Message stanza
* @returns {boolean} True on success
*/
onGroupchatMessage: function(message) {
var id = $(message).attr('id');
if (id && jsxc.el_exists(jsxc.Message.getDOM(id))) {
// ignore own incoming messages
return true;
}
var from = $(message).attr('from');
var body = $(message).find('body:first').text();
var room = jsxc.jidToBid(from);
var nickname = Strophe.unescapeNode(Strophe.getResourceFromJid(from));
if (body !== '') {
var delay = $(message).find('delay[xmlns="urn:xmpp:delay"]');
var stamp = (delay.length > 0) ? new Date(delay.attr('stamp')) : new Date();
stamp = stamp.getTime();
var member = jsxc.storage.getUserItem('member', room) || {};
var sender = {};
sender.name = nickname;
if (member[nickname] && typeof member[nickname].jid === 'string') {
sender.jid = member[nickname].jid;
}
jsxc.gui.window.init(room);
jsxc.gui.window.postMessage({
bid: room,
direction: jsxc.Message.IN,
msg: body,
stamp: stamp,
sender: sender
});
}
var subject = $(message).find('subject');
if (subject.length > 0) {
var roomdata = jsxc.storage.getUserItem('buddy', room);
roomdata.subject = subject.text();
jsxc.storage.setUserItem('buddy', room, roomdata);
jsxc.gui.window.postMessage({
bid: room,
direction: jsxc.Message.SYS,
msg: $.t('changed_subject_to', {
nickname: nickname,
subject: subject.text()
})
});
}
return true;
},
/**
* Prepare group chat roster item.
*
* @private
* @memberOf jsxc.muc
* @param event
* @param {string} room Room jid
* @param {object} data Room data
* @param {jQuery} bud Roster item
*/
onAddRoster: function(event, room, data, bud) {
var self = jsxc.muc;
if (data.type !== 'groupchat') {
return;
}
var bo = $('<a>');
$('<span>').addClass('jsxc_icon jsxc_bookmarkicon').appendTo(bo);
$('<span>').text($.t('Bookmark')).appendTo(bo);
bo.addClass('jsxc_bookmarkOptions');
bo.click(function(ev) {
ev.preventDefault();
jsxc.xmpp.bookmarks.showDialog(room);
return false;
});
bud.find('.jsxc_menu ul').append($('<li>').append(bo));
if (data.bookmarked) {
bud.addClass('jsxc_bookmarked');
}
bud.off('click').click(function() {
var data = jsxc.storage.getUserItem('buddy', room);
if (data.state === self.CONST.ROOMSTATE.INIT || data.state === self.CONST.ROOMSTATE.EXITED) {
self.showJoinChat();
$('#jsxc_room').val(Strophe.getNodeFromJid(data.jid));
$('#jsxc_nickname').val(data.nickname);
$('#jsxc_bookmark').prop('checked', data.bookmarked);
$('#jsxc_autojoin').prop('checked', data.autojoin);
$('#jsxc_dialog .jsxc_bookmark').hide();
} else {
jsxc.gui.window.open(room);
}
});
bud.find('.jsxc_delete').click(function() {
if (data.bookmarked) {
jsxc.xmpp.bookmarks.delete(room);
}
self.leave(room);
return false;
});
},
/**
* Some helper functions.
*
* @type {Object}
*/
helper: {
/**
* Convert x:data form to html.
*
* @param {Strophe.x.Form} form - x:data form
* @return {jQuery} jQuery representation of x:data field
*/
formToHTML: function(form) {
if (!(form instanceof Strophe.x.Form)) {
return;
}
var html = $('<form>');
html.attr('data-type', form.type);
html.addClass('form-horizontal');
if (form.title) {
html.append("<h3>" + form.title + "</h3>");
}
if (form.instructions) {
html.append("<p>" + form.instructions + "</p>");
}
if (form.fields.length > 0) {
var i;
for (i = 0; i < form.fields.length; i++) {
html.append(jsxc.muc.helper.fieldToHtml(form.fields[i]));
}
}
return $('<div>').append(html).html();
},
/**
* Convert x:data field to html.
*
* @param {Strophe.x.Field} field - x:data field
* @return {html} html representation of x:data field
*/
fieldToHtml: function(field) {
var self = field || this;
field = null;
var el, val, opt, i, o, j, k, txt, line, _ref2;
var id = "Strophe.x.Field-" + self['type'] + "-" + self['var'];
var html = $('<div>');
html.addClass('form-group');
if (self.label) {
var label = $('<label>');
label.attr('for', id);
label.addClass('col-sm-6 control-label');
label.text(self.label);
label.appendTo(html);
}
switch (self.type.toLowerCase()) {
case 'list-single':
case 'list-multi':
el = $('<select>');
if (self.type === 'list-multi') {
el.attr('multiple', 'multiple');
}
for (i = 0; i < self.options.length; i++) {
opt = self.options[i];
if (!opt) {
continue;
}
o = $(opt.toHTML());
for (j = 0; j < self.values.length; j++) {
k = self.values[j];
if (k.toString() === opt.value.toString()) {
o.attr('selected', 'selected');
}
}
o.appendTo(el);
}
break;
case 'text-multi':
case 'jid-multi':
el = $("<textarea>");
txt = ((function() {
var i, _results;
_results = [];
for (i = 0; i < self.values.length; i++) {
line = self.values[i];
_results.push(line);
}
return _results;
}).call(this)).join('\n');
if (txt) {
el.text(txt);
}
break;
case 'text-single':
case 'boolean':
case 'text-private':
case 'hidden':
case 'fixed':
case 'jid-single':
el = $("<input>");
if (self.values) {
el.attr('value', self.values[0]);
}
switch (self.type.toLowerCase()) {
case 'text-single':
el.attr('type', 'text');
el.attr('placeholder', self.desc);
el.addClass('form-control');
break;
case 'boolean':
el.attr('type', 'checkbox');
val = (_ref2 = self.values[0]) != null ? typeof _ref2.toString === "function" ? _ref2.toString() : void 0 : void 0;
if (val && (val === "true" || val === "1")) {
el.attr('checked', 'checked');
}
break;
case 'text-private':
el.attr('type', 'password');
el.addClass('form-control');
break;
case 'hidden':
el.attr('type', 'hidden');
break;
case 'fixed':
el.attr('type', 'text').attr('readonly', 'readonly');
el.addClass('form-control');
break;
case 'jid-single':
el.attr('type', 'email');
el.addClass('form-control');
}
break;
default:
el = $("<input type='text'>");
}
el.attr('id', id);
el.attr('name', self["var"]);
if (self.required) {
el.attr('required', self.required);
}
var inner = el;
el = $('<div>');
el.addClass('col-sm-6');
el.append(inner);
html.append(el);
return html.get(0);
}
},
isGroupchat: function(jid) {
var bid = jsxc.jidToBid(jid);
var userData = jsxc.storage.setUserItem('buddy', bid) || {};
return userData.type === 'groupchat';
}
};
$(document).on('init.window.jsxc', jsxc.muc.initWindow);
$(document).on('add.roster.jsxc', jsxc.muc.onAddRoster);
$(document).on('attached.jsxc', function() {
jsxc.muc.init();
});
$(document).one('connected.jsxc', function() {
jsxc.storage.removeUserItem('roomNames');
jsxc.storage.removeUserItem('ownNicknames');
});