Commit c508b996 authored by JC Brand's avatar JC Brand

Merge pull request #442 from jcbrand/mam

Message Archive Management
parents 8ab0d7e8 96253f96
......@@ -90,41 +90,6 @@
return [components.shift(), components.join(delimiter)];
};
$.fn.addEmoticons = function () {
if (converse.visible_toolbar_buttons.emoticons) {
if (this.length > 0) {
this.each(function (i, obj) {
var text = $(obj).html();
text = text.replace(/&gt;:\)/g, '<span class="emoticon icon-evil"></span>');
text = text.replace(/:\)/g, '<span class="emoticon icon-smiley"></span>');
text = text.replace(/:\-\)/g, '<span class="emoticon icon-smiley"></span>');
text = text.replace(/;\)/g, '<span class="emoticon icon-wink"></span>');
text = text.replace(/;\-\)/g, '<span class="emoticon icon-wink"></span>');
text = text.replace(/:D/g, '<span class="emoticon icon-grin"></span>');
text = text.replace(/:\-D/g, '<span class="emoticon icon-grin"></span>');
text = text.replace(/:P/g, '<span class="emoticon icon-tongue"></span>');
text = text.replace(/:\-P/g, '<span class="emoticon icon-tongue"></span>');
text = text.replace(/:p/g, '<span class="emoticon icon-tongue"></span>');
text = text.replace(/:\-p/g, '<span class="emoticon icon-tongue"></span>');
text = text.replace(/8\)/g, '<span class="emoticon icon-cool"></span>');
text = text.replace(/:S/g, '<span class="emoticon icon-confused"></span>');
text = text.replace(/:\\/g, '<span class="emoticon icon-wondering"></span>');
text = text.replace(/:\/ /g, '<span class="emoticon icon-wondering"></span>');
text = text.replace(/&gt;:\(/g, '<span class="emoticon icon-angry"></span>');
text = text.replace(/:\(/g, '<span class="emoticon icon-sad"></span>');
text = text.replace(/:\-\(/g, '<span class="emoticon icon-sad"></span>');
text = text.replace(/:O/g, '<span class="emoticon icon-shocked"></span>');
text = text.replace(/:\-O/g, '<span class="emoticon icon-shocked"></span>');
text = text.replace(/\=\-O/g, '<span class="emoticon icon-shocked"></span>');
text = text.replace(/\(\^.\^\)b/g, '<span class="emoticon icon-thumbs-up"></span>');
text = text.replace(/&lt;3/g, '<span class="emoticon icon-heart"></span>');
$(obj).html(text);
});
}
}
return this;
};
var converse = {
plugins: {},
templates: templates,
......@@ -153,6 +118,13 @@
}
};
// Global constants
// XEP-0059 Result Set Management
var RSM_ATTRIBUTES = ['max', 'first', 'last', 'after', 'before', 'index', 'count'];
// XEP-0313 Message Archive Management
var MAM_ATTRIBUTES = ['with', 'start', 'end'];
var STATUS_WEIGHTS = {
'offline': 6,
'unavailable': 5,
......@@ -184,7 +156,10 @@
Strophe.error = function (msg) { converse.log(msg, 'error'); };
// Add Strophe Namespaces
Strophe.addNamespace('CARBONS', 'urn:xmpp:carbons:2');
Strophe.addNamespace('CHATSTATES', 'http://jabber.org/protocol/chatstates');
Strophe.addNamespace('CSI', 'urn:xmpp:csi:0');
Strophe.addNamespace('MAM', 'urn:xmpp:mam:0');
Strophe.addNamespace('MUC_ADMIN', Strophe.NS.MUC + "#admin");
Strophe.addNamespace('MUC_OWNER', Strophe.NS.MUC + "#owner");
Strophe.addNamespace('MUC_REGISTER', "jabber:iq:register");
......@@ -192,8 +167,8 @@
Strophe.addNamespace('MUC_USER', Strophe.NS.MUC + "#user");
Strophe.addNamespace('REGISTER', 'jabber:iq:register');
Strophe.addNamespace('ROSTERX', 'http://jabber.org/protocol/rosterx');
Strophe.addNamespace('RSM', 'http://jabber.org/protocol/rsm');
Strophe.addNamespace('XFORM', 'jabber:x:data');
Strophe.addNamespace('CSI', 'urn:xmpp:csi:0');
// Add Strophe Statuses
var i = 0;
......@@ -326,6 +301,7 @@
allow_logout: true,
allow_muc: true,
allow_otr: true,
archived_messages_page_size: '20',
auto_away: 0, // Seconds after which user status is set to 'away'
auto_xa: 0, // Seconds after which user status is set to 'xa'
allow_registration: true,
......@@ -346,7 +322,9 @@
hide_offline_users: false,
jid: undefined,
keepalive: false,
message_archiving: 'never', // Supported values are 'always', 'never', 'roster' (See https://xmpp.org/extensions/xep-0313.html#prefs )
message_carbons: false, // Support for XEP-280
muc_history_max_stanzas: undefined, // Takes an integer, limits the amount of messages to fetch from chat room's history
no_trimming: false, // Set to true for phantomjs tests (where browser apparently has no width)
ping_interval: 180, //in seconds
play_sounds: false,
......@@ -570,7 +548,8 @@
this.getVCard = function (jid, callback, errback) {
/* Request the VCard of another user.
* Parameters:
*
* Parameters:
* (String) jid - The Jabber ID of the user whose VCard is being requested.
* (Function) callback - A function to call once the VCard is returned
* (Function) errback - A function to call if an error occured
......@@ -874,7 +853,7 @@
id: 'enablecarbons',
type: 'set'
})
.c('enable', {xmlns: 'urn:xmpp:carbons:2'});
.c('enable', {xmlns: Strophe.NS.CARBONS});
this.connection.addHandler(function (iq) {
if ($(iq).find('error').length > 0) {
converse.log('ERROR: An error occured while trying to enable message carbons.');
......@@ -957,7 +936,8 @@
this.Message = Backbone.Model;
this.Messages = Backbone.Collection.extend({
model: converse.Message
model: converse.Message,
comparator: 'time'
});
this.ChatBox = Backbone.Model.extend({
......@@ -1130,9 +1110,10 @@
this.save({'otr_status': UNENCRYPTED});
},
createMessage: function ($message) {
createMessage: function ($message, $delay, archive_id) {
$delay = $delay || $message.find('delay');
var body = $message.children('body').text(),
delayed = $message.find('delay').length > 0,
delayed = $delay.length > 0,
fullname = this.get('fullname'),
is_groupchat = $message.attr('type') === 'groupchat',
msgid = $message.attr('id'),
......@@ -1150,7 +1131,7 @@
}
fullname = (_.isEmpty(fullname) ? from: fullname).split(' ')[0];
if (delayed) {
stamp = $message.find('delay').attr('stamp');
stamp = $delay.attr('stamp');
time = stamp;
} else {
time = moment().format();
......@@ -1167,15 +1148,16 @@
message: body || undefined,
msgid: msgid,
sender: sender,
time: time
time: time,
archive_id: archive_id
});
},
receiveMessage: function ($message) {
receiveMessage: function ($message, $delay, archive_id) {
var $body = $message.children('body');
var text = ($body.length > 0 ? $body.text() : undefined);
if ((!text) || (!converse.allow_otr)) {
return this.createMessage($message);
return this.createMessage($message, $delay, archive_id);
}
if (text.match(/^\?OTRv23?/)) {
this.initiateOTR(text);
......@@ -1191,7 +1173,7 @@
}
} else {
// Normal unencrypted message.
this.createMessage($message);
this.createMessage($message, $delay, archive_id);
}
}
}
......@@ -1239,17 +1221,13 @@
this.model.on('showReceivedOTRMessage', function (text) {
this.showMessage({'message': text, 'sender': 'them'});
}, this);
this.updateVCard().insertIntoPage();
this.hide().render().model.messages.fetch({add: true});
this.updateVCard().render().fetchMessages().insertIntoPage().hide();
if ((_.contains([UNVERIFIED, VERIFIED], this.model.get('otr_status'))) || converse.use_otr_by_default) {
this.model.initiateOTR();
}
},
insertIntoPage: function () {
this.$el.insertAfter(converse.chatboxviews.get("controlbox").$el);
},
render: function () {
this.$el.attr('id', this.model.get('box_id'))
.html(converse.templates.chatbox(
......@@ -1260,12 +1238,80 @@
)
)
);
this.$content = this.$el.find('.chat-content');
this.renderToolbar().renderAvatar();
this.$content.on('scroll', _.debounce(this.onScroll.bind(this), 100));
converse.emit('chatBoxOpened', this);
setTimeout(converse.refreshWebkit, 50);
return this.showStatusMessage();
},
onScroll: function (ev) {
if ($(ev.target).scrollTop() === 0 && this.model.messages.length) {
if (!this.$content.first().hasClass('spinner')) {
this.$content.prepend('<span class="spinner"/>');
}
this.fetchArchivedMessages({
'before': this.model.messages.at(0).get('archive_id'),
'with': this.model.get('jid'),
'max': converse.archived_messages_page_size
});
}
},
fetchMessages: function () {
/* Responsible for fetching previously sent messages, first
* from session storage, and then once that's done by calling
* fetchArchivedMessages, which fetches from the XMPP server if
* applicable.
*/
this.model.messages.fetch({
'add': true,
'success': function () {
if (!converse.features.findWhere({'var': Strophe.NS.MAM})) {
return;
}
if (this.model.messages.length < converse.archived_messages_page_size) {
this.fetchArchivedMessages({
'before': '', // Page backwards from the most recent message
'with': this.model.get('jid'),
'max': converse.archived_messages_page_size
});
}
}.bind(this)
});
return this;
},
fetchArchivedMessages: function (options) {
/* Fetch archived chat messages from the XMPP server.
*
* Then, upon receiving them, call onMessage on the chat box,
* so that they are displayed inside it.
*/
API.archive.query(_.extend(options, {'groupchat': this.is_chatroom}),
function (messages) {
this.clearSpinner();
if (messages.length) {
if (this.is_chatroom) {
_.map(messages, this.onChatRoomMessage.bind(this));
} else {
_.map(messages, converse.chatboxes.onMessage.bind(converse.chatboxes));
}
}
}.bind(this),
function (iq) {
this.clearSpinner();
converse.log("Error while trying to fetch archived messages", "error");
}.bind(this)
);
},
insertIntoPage: function () {
this.$el.insertAfter(converse.chatboxviews.get("controlbox").$el);
return this;
},
initDragResize: function () {
this.prev_pageY = 0; // To store last known mouse position
if (converse.connection.connected) {
......@@ -1275,11 +1321,10 @@
},
showStatusNotification: function (message, keep_old) {
var $chat_content = this.$el.find('.chat-content');
if (!keep_old) {
$chat_content.find('div.chat-event').remove();
this.$content.find('div.chat-event').remove();
}
$chat_content.append($('<div class="chat-event"></div>').text(message));
this.$content.append($('<div class="chat-event"></div>').text(message));
this.scrollDown();
},
......@@ -1287,18 +1332,144 @@
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.$el.find('.chat-content').empty();
this.$content.empty();
}
return this;
},
showMessage: function (msg_dict) {
var $content = this.$el.find('.chat-content'),
msg_time = moment(msg_dict.time) || moment,
text = msg_dict.message,
clearSpinner: function () {
if (this.$content.children(':first').is('span.spinner')) {
this.$content.children(':first').remove();
}
},
prependDayIndicator: function (date) {
/* Prepends an indicator into the chat area, showing the day as
* given by the passed in date.
*
* Parameters:
* (String) date - An ISO8601 date string.
*/
var day_date = moment(date).startOf('day');
this.$content.prepend(converse.templates.new_day({
isodate: day_date.format(),
datestring: day_date.format("dddd MMM Do YYYY")
}));
},
appendMessage: function (attrs) {
/* Helper method which appends a message to the end of the chat
* box's content area.
*
* Parameters:
* (Object) attrs: An object containing the message attributes.
*/
_.compose(
_.debounce(this.scrollDown.bind(this), 50),
this.$content.append.bind(this.$content)
)(this.renderMessage(attrs));
},
showMessage: function (attrs) {
/* Inserts a chat message into the content area of the chat box.
* Will also insert a new day indicator if the message is on a
* different day.
*
* The message to show may either be newer than the newest
* message, or older than the oldest message.
*
* Parameters:
* (Object) attrs: An object containing the message attributes.
*/
var $first_msg = this.$content.children('.chat-message:first'),
first_msg_date = $first_msg.data('isodate'),
last_msg_date, current_msg_date, day_date, $msgs, msg_dates, idx;
if (typeof first_msg_date === "undefined") {
this.appendMessage(attrs);
return;
}
current_msg_date = moment(attrs.time) || moment;
last_msg_date = this.$content.children('.chat-message:last').data('isodate');
if (typeof last_msg_date !== "undefined" && (current_msg_date.isAfter(last_msg_date) || current_msg_date.isSame(last_msg_date))) {
// The new message is after the last message
if (current_msg_date.isAfter(last_msg_date, 'day')) {
// Append a new day indicator
day_date = moment(current_msg_date).startOf('day');
this.$content.append(converse.templates.new_day({
isodate: current_msg_date.format(),
datestring: current_msg_date.format("dddd MMM Do YYYY")
}));
}
this.appendMessage(attrs);
return;
}
if (typeof first_msg_date !== "undefined" &&
(current_msg_date.isBefore(first_msg_date) ||
(current_msg_date.isSame(first_msg_date) && !current_msg_date.isSame(last_msg_date)))) {
// The new message is before the first message
if ($first_msg.prev().length === 0) {
// There's no day indicator before the first message, so we prepend one.
this.prependDayIndicator(first_msg_date);
}
if (current_msg_date.isBefore(first_msg_date, 'day')) {
_.compose(
this.scrollDownMessageHeight.bind(this),
function ($el) {
this.$content.prepend($el);
return $el;
}.bind(this)
)(this.renderMessage(attrs));
// This message is on a different day, so we add a day indicator.
this.prependDayIndicator(current_msg_date);
} else {
// The message is before the first, but on the same day.
// We need to prepend the message immediately before the
// first message (so that it'll still be after the day indicator).
_.compose(
this.scrollDownMessageHeight.bind(this),
function ($el) {
$el.insertBefore($first_msg);
return $el;
}
)(this.renderMessage(attrs));
}
} else {
// We need to find the correct place to position the message
current_msg_date = current_msg_date.format();
$msgs = this.$content.children('.chat-message');
msg_dates = _.map($msgs, function (el) {
return $(el).data('isodate');
});
msg_dates.push(current_msg_date);
msg_dates.sort();
idx = msg_dates.indexOf(current_msg_date)-1;
_.compose(
this.scrollDownMessageHeight.bind(this),
function ($el) {
$el.insertAfter(this.$content.find('.chat-message[data-isodate="'+msg_dates[idx]+'"]'));
return $el;
}.bind(this)
)(this.renderMessage(attrs));
}
},
renderMessage: function (attrs) {
/* Renders a chat message based on the passed in attributes.
*
* Parameters:
* (Object) attrs: An object containing the message attributes.
*
* Returns:
* The DOM element representing the message.
*/
var msg_time = moment(attrs.time) || moment,
text = attrs.message,
match = text.match(/^\/(.*?)(?: (.*))?$/),
fullname = this.model.get('fullname') || msg_dict.fullname,
extra_classes = msg_dict.delayed && 'delayed' || '',
fullname = this.model.get('fullname') || attrs.fullname,
extra_classes = attrs.delayed && 'delayed' || '',
template, username;
if ((match) && (match[1] === 'me')) {
......@@ -1307,59 +1478,46 @@
username = fullname;
} else {
template = converse.templates.message;
username = msg_dict.sender === 'me' && __('me') || fullname;
username = attrs.sender === 'me' && __('me') || fullname;
}
$content.find('div.chat-event').remove();
this.$content.find('div.chat-event').remove();
if (this.is_chatroom && msg_dict.sender == 'them' && (new RegExp("\\b"+this.model.get('nick')+"\\b")).test(text)) {
if (this.is_chatroom && attrs.sender == 'them' && (new RegExp("\\b"+this.model.get('nick')+"\\b")).test(text)) {
// Add special class to mark groupchat messages in which we
// are mentioned.
extra_classes += ' mentioned';
}
var message = template({
'sender': msg_dict.sender,
'time': msg_time.format('hh:mm'),
'username': username,
'message': '',
'extra_classes': extra_classes
});
$content.append($(message).children('.chat-message-content').first().text(text).addHyperlinks().addEmoticons().parent());
this.scrollDown();
return $(template({
'sender': attrs.sender,
'time': msg_time.format('hh:mm'),
'isodate': msg_time.format(),
'username': username,
'message': '',
'extra_classes': extra_classes
})).children('.chat-message-content').first().text(text)
.addHyperlinks()
.addEmoticons(converse.visible_toolbar_buttons.emoticons).parent();
},
showHelpMessages: function (msgs, type, spinner) {
var $chat_content = this.$el.find('.chat-content'), i,
msgs_length = msgs.length;
var i, msgs_length = msgs.length;
for (i=0; i<msgs_length; i++) {
$chat_content.append($('<div class="chat-'+(type||'info')+'">'+msgs[i]+'</div>'));
this.$content.append($('<div class="chat-'+(type||'info')+'">'+msgs[i]+'</div>'));
}
if (spinner === true) {
$chat_content.append('<span class="spinner"/>');
this.$content.append('<span class="spinner"/>');
} else if (spinner === false) {
$chat_content.find('span.spinner').remove();
this.$content.find('span.spinner').remove();
}
return this.scrollDown();
},
onMessageAdded: function (message) {
var time = message.get('time'),
times = this.model.messages.pluck('time'),
previous_message, idx, this_date, prev_date, text, match;
// If this message is on a different day than the one received
// prior, then indicate it on the chatbox.
idx = _.indexOf(times, time)-1;
if (idx >= 0) {
previous_message = this.model.messages.at(idx);
prev_date = moment(previous_message.get('time'));
if (prev_date.isBefore(time, 'day')) {
this_date = moment(time);
this.$el.find('.chat-content').append(converse.templates.new_day({
isodate: this_date.format("YYYY-MM-DD"),
datestring: this_date.format("dddd MMM Do YYYY")
}));
}
}
/* Handler that gets called when a new message object is created.
*
* Parameters:
* (Object) message - The message Backbone object that was added.
*/
if (!message.get('message')) {
if (message.get('chat_state') === COMPOSING) {
this.showStatusNotification(message.get('fullname')+' '+__('is typing'));
......@@ -1368,7 +1526,7 @@
this.showStatusNotification(message.get('fullname')+' '+__('has stopped typing'));
return;
} else if (_.contains([INACTIVE, ACTIVE], message.get('chat_state'))) {
this.$el.find('.chat-content div.chat-event').remove();
this.$content.find('div.chat-event').remove();
return;
} else if (message.get('chat_state') === GONE) {
this.showStatusNotification(message.get('fullname')+' '+__('has gone away'));
......@@ -1380,9 +1538,8 @@
if ((message.get('sender') != 'me') && (converse.windowState == 'blur')) {
converse.incrementMsgCounter();
}
this.scrollDown();
if (!this.model.get('minimized') && !this.$el.is(':visible')) {
this.show();
_.debounce(this.show.bind(this), 100)();
}
},
......@@ -1392,8 +1549,7 @@
* Parameters:
* (string) text - The chat message text.
*/
// TODO: We might want to send to specfic resources. Especially
// in the OTR case.
// TODO: We might want to send to specfic resources. Especially in the OTR case.
var timestamp = (new Date()).getTime();
var bare_jid = this.model.get('jid');
var message = $msg({from: converse.connection.jid, to: bare_jid, type: 'chat', id: timestamp})
......@@ -1402,7 +1558,7 @@
if (this.model.get('otr_status') != UNENCRYPTED) {
// OTR messages aren't carbon copied
message.c('private', {'xmlns': 'urn:xmpp:carbons:2'});
message.c('private', {'xmlns': Strophe.NS.CARBONS});
}
converse.connection.send(message);
if (converse.forward_messages) {
......@@ -1483,7 +1639,7 @@
*
* Parameters:
* (string) state - The chat state (consts ACTIVE, COMPOSING, PAUSED, INACTIVE, GONE)
* (no_save) no_save - Just do the cleanup or setup but don't actually save the state.
* (Boolean) no_save - Just do the cleanup or setup but don't actually save the state.
*/
if (typeof this.chat_state_timeout !== 'undefined') {
clearTimeout(this.chat_state_timeout);
......@@ -1552,7 +1708,7 @@
if (ev && ev.preventDefault) { ev.preventDefault(); }
var result = confirm(__("Are you sure you want to clear the messages from this chat box?"));
if (result === true) {
this.$el.find('.chat-content').empty();
this.$content.empty();
this.model.messages.reset();
this.model.messages.browserStorage._clear();
}
......@@ -1846,21 +2002,32 @@
if (this.$el.is(':visible') && this.$el.css('opacity') == "1") {
return this.focus();
}
this.$el.fadeIn(callback);
if (converse.connection.connected) {
// Without a connection, we haven't yet initialized
// localstorage
this.model.save();
this.initDragResize();
this.$el.fadeIn(function () {
if (typeof callback == "function") {
callback.apply(this, arguments);
}
if (converse.connection.connected) {
// Without a connection, we haven't yet initialized
// localstorage
this.model.save();
this.initDragResize();
}
this.setChatState(ACTIVE);
this.scrollDown().focus();
}.bind(this));
return this;
},
scrollDownMessageHeight: function ($message) {
if (this.$content.is(':visible')) {
this.$content.scrollTop(this.$content.scrollTop() + $message[0].scrollHeight);
}
this.setChatState(ACTIVE);
return this.focus();
return this;
},
scrollDown: function () {
var $content = this.$('.chat-content');
if ($content.is(':visible')) {
$content.scrollTop($content[0].scrollHeight);
if (this.$content.is(':visible')) {
this.$content.scrollTop(this.$content[0].scrollHeight);
}
return this;
}
......@@ -2584,11 +2751,11 @@
this.occupantsview.chatroomview = this;
this.render();
this.occupantsview.model.fetch({add:true});
this.join(null);
this.join(null, {'maxstanzas': converse.muc_history_max_stanzas});
this.fetchMessages();
converse.emit('chatRoomOpened', this);
this.$el.insertAfter(converse.chatboxviews.get("controlbox").$el);
this.model.messages.fetch({add: true});
if (this.model.get('minimized')) {
this.hide();
} else {
......@@ -2600,6 +2767,7 @@
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));
setTimeout(converse.refreshWebkit, 50);
return this;
},
......@@ -2614,6 +2782,7 @@
}))
.append(this.occupantsview.render().$el);
this.renderToolbar();
this.$content = this.$el.find('.chat-content');
}
// XXX: This is a bit of a hack, to make sure that the
// sidebar's state is remembered.
......@@ -2632,16 +2801,12 @@
this.model.save({hidden_occupants: true});
$el.removeClass('icon-hide-users').addClass('icon-show-users');
this.$('form.sendXMPPMessage, .chat-area').animate({width: '100%'});
this.$('div.participants').animate({width: 0}, function () {
this.scrollDown();
}.bind(this));
this.$('div.participants').animate({width: 0}, this.scrollDown.bind(this));
} else {
this.model.save({hidden_occupants: false});
$el.removeClass('icon-show-users').addClass('icon-hide-users');
this.$('.chat-area, form.sendXMPPMessage').css({width: ''});
this.$('div.participants').show().animate({width: 'auto'}, function () {
this.scrollDown();
}.bind(this));
this.$('div.participants').show().animate({width: 'auto'}, this.scrollDown.bind(this));
}
},
......@@ -2820,11 +2985,12 @@
handleMUCStanza: function (stanza) {
var xmlns, xquery, i;
var from = stanza.getAttribute('from');
if (!from || (this.model.get('id') !== from.split("/")[0])) {
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") {
this.onChatRoomMessage(stanza);
_.compose(this.onChatRoomMessage.bind(this), this.showStatusMessages.bind(this))(stanza);
} else if (stanza.nodeName === "presence") {
xquery = stanza.getElementsByTagName("x");
if (xquery.length > 0) {
......@@ -2849,26 +3015,26 @@
},
join: function (password, history_attrs, extended_presence) {
var msg = $pres({
var stanza = $pres({
from: converse.connection.jid,
to: this.getRoomJIDAndNick()
}).c("x", {
xmlns: Strophe.NS.MUC
});
if (typeof history_attrs === "object" && history_attrs.length) {
msg = msg.c("history", history_attrs).up();
if (typeof history_attrs === "object" && Object.keys(history_attrs).length) {
stanza = stanza.c("history", history_attrs).up();
}
if (password) {
msg.cnode(Strophe.xmlElement("password", [], password));
stanza.cnode(Strophe.xmlElement("password", [], password));
}
if (typeof extended_presence !== "undefined" && extended_presence !== null) {
msg.up.cnode(extended_presence);
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(msg);
return converse.connection.send(stanza);
},
leave: function(exit_msg) {
......@@ -2912,7 +3078,7 @@
// 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: "jabber:x:data", type: "submit"});
.c("x", {xmlns: Strophe.NS.XFORM, type: "submit"});
_.each(config, function (node) { iq.cnode(node).up(); });
return converse.connection.sendIQ(iq.tree(), onSuccess, onError);
},
......@@ -3073,12 +3239,12 @@
303: ___('Your nickname has been changed to: <strong>%1$s</strong>')
},
showStatusMessages: function ($el, is_self) {
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 $chat_content,
var $el = $(el),
disconnect_msgs = [],
msgs = [],
reasons = [];
......@@ -3123,14 +3289,14 @@
this.model.set('connection_status', Strophe.Status.DISCONNECTED);
return;
}
$chat_content = this.$el.find('.chat-content');
for (i=0; i<msgs.length; i++) {
$chat_content.append(converse.templates.info({message: msgs[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);
}
return this.scrollDown();
this.scrollDown();
return el;
},
showErrorMessage: function ($error) {
......@@ -3179,30 +3345,38 @@
this.$('span.centered.spinner').remove();
this.$el.find('.chat-body').children().show();
}
this.showStatusMessages($presence, is_self);
this.showStatusMessages(pres, is_self);
}
this.occupantsview.updateOccupantsOnPresence(pres);
},
onChatRoomMessage: function (message) {
var $message = $(message),
body = $message.children('body').text(),
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) || '',
delayed = $message.find('delay').length > 0,
subject = $message.children('subject').text();
if (msgid && this.model.messages.findWhere({msgid: msgid})) {
return true; // We already have this message stored.
}
this.showStatusMessages($message);
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.$el.find('.chat-content').append(
this.$content.append(
converse.templates.info({
'message': __('Topic set by %1$s to: %2$s', sender, subject)
}));
......@@ -3210,7 +3384,7 @@
if (sender === '') {
return true;
}
this.model.createMessage($message);
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();
}
......@@ -3318,12 +3492,12 @@
/* Handler method for all incoming single-user chat "message" stanzas.
*/
var $message = $(message),
contact_jid, $forwarded, $received, $sent, from_bare_jid, from_resource, is_me,
msgid = $message.attr('id'),
contact_jid, $forwarded, $delay, from_bare_jid, from_resource, is_me, msgid,
chatbox, resource, roster_item,
from_jid = $message.attr('from'),
to_jid = $message.attr('to'),
to_resource = Strophe.getResourceFromJid(to_jid);
to_resource = Strophe.getResourceFromJid(to_jid),
archive_id = $message.find('result[xmlns="'+Strophe.NS.MAM+'"]').attr('id');
if (to_resource && to_resource !== converse.resource) {
converse.log('Ignore incoming message intended for a different resource: '+to_jid, 'info');
......@@ -3334,23 +3508,17 @@
converse.log("Ignore incoming message sent from this client's JID: "+from_jid, 'info');
return true;
}
$forwarded = $message.children('forwarded');
$received = $message.children('received[xmlns="urn:xmpp:carbons:2"]');
$sent = $message.children('sent[xmlns="urn:xmpp:carbons:2"]');
$forwarded = $message.find('forwarded');
if ($forwarded.length) {
$message = $forwarded.children('message');
} else if ($received.length) {
$message = $received.children('forwarded').children('message');
from_jid = $message.attr('from');
} else if ($sent.length) {
$message = $sent.children('forwarded').children('message');
$delay = $forwarded.children('delay');
from_jid = $message.attr('from');
to_jid = $message.attr('to');
}
from_bare_jid = Strophe.getBareJidFromJid(from_jid);
from_resource = Strophe.getResourceFromJid(from_jid);
is_me = from_bare_jid == converse.bare_jid;
msgid = $message.attr('id');
if (is_me) {
// I am the sender, so this must be a forwarded message...
......@@ -3360,46 +3528,49 @@
contact_jid = from_bare_jid;
resource = from_resource;
}
roster_item = converse.roster.get(contact_jid);
if (roster_item === undefined) {
// The contact was likely removed
converse.log('Could not get roster item for JID '+contact_jid, 'error');
return true;
}
chatbox = this.get(contact_jid);
// Get chat box, but only create a new one when the message has a body.
chatbox = this.getChatBox(contact_jid, $message.find('body').length > 0);
if (!chatbox) {
/* If chat state notifications (because a roster contact
* closed a chat box of yours they had open) are received
* and we don't have a chat with the user, then we do not
* want to open a chat box. We only open a new chat box when
* the message has a body.
*/
if ($message.find('body').length === 0) {
return true;
}
var fullname = roster_item.get('fullname');
fullname = _.isEmpty(fullname)? contact_jid: fullname;
chatbox = this.create({
'id': contact_jid,
'jid': contact_jid,
'fullname': fullname,
'image_type': roster_item.get('image_type'),
'image': roster_item.get('image'),
'url': roster_item.get('url')
});
return true;
}
if (msgid && chatbox.messages.findWhere({msgid: msgid})) {
return true; // We already have this message stored.
}
if (!this.isOnlyChatStateNotification($message) && !is_me) {
if (!this.isOnlyChatStateNotification($message) && !is_me && !$forwarded.length) {
converse.playNotification();
}
chatbox.receiveMessage($message);
chatbox.receiveMessage($message, $delay, archive_id);
converse.roster.addResource(contact_jid, resource);
converse.emit('message', message);
return true;
},
getChatBox: function (jid, create) {
/* Returns a chat box or optionally return a newly
* created one if one doesn't exist.
*
* Parameters:
* (String) jid - The JID of the user whose chat box we want
* (Boolean) create - Should a new chat box be created if none exists?
*/
var bare_jid = Strophe.getBareJidFromJid(jid);
var chatbox = this.get(bare_jid);
if (!chatbox && create) {
var roster_item = converse.roster.get(bare_jid);
if (roster_item === undefined) {
converse.log('Could not get roster item for JID '+bare_jid, 'error');
return;
}
chatbox = this.create({
'id': bare_jid,
'jid': bare_jid,
'fullname': _.isEmpty(roster_item.get('fullname'))? jid: roster_item.get('fullname'),
'image_type': roster_item.get('image_type'),
'image': roster_item.get('image'),
'url': roster_item.get('url')
});
}
return chatbox;
}
});
......@@ -4135,6 +4306,7 @@
onRosterPush: function (iq) {
/* Handle roster updates from the XMPP server.
* See: https://xmpp.org/rfcs/rfc6121.html#roster-syntax-actions-push
*
* Parameters:
* (XMLElement) IQ - The IQ stanza received from the XMPP server.
*/
......@@ -5075,6 +5247,7 @@
this.addClientIdentities().addClientFeatures();
this.browserStorage = new Backbone.BrowserStorage[converse.storage](
b64_sha1('converse.features'+converse.bare_jid));
this.on('add', this.onFeatureAdded, this);
if (this.browserStorage.records.length === 0) {
// browserStorage is empty, so we've likely never queried this
// domain for features yet
......@@ -5085,6 +5258,60 @@
}
},
onFeatureAdded: function (feature) {
var prefs = feature.get('preferences') || {};
converse.emit('serviceDiscovered', feature);
if (feature.get('var') == Strophe.NS.MAM && prefs['default'] !== converse.message_archiving) {
// Ask the server for archiving preferences
converse.connection.sendIQ(
$iq({'type': 'get'}).c('prefs', {'xmlns': Strophe.NS.MAM}),
_.bind(this.onMAMPreferences, this, feature),
_.bind(this.onMAMError, this, feature)
);
}
},
onMAMPreferences: function (feature, iq) {
/* Handle returned IQ stanza containing Message Archive
* Management (XEP-0313) preferences.
*
* XXX: For now we only handle the global default preference.
* The XEP also provides for per-JID preferences, which is
* currently not supported in converse.js.
*
* Per JID preferences will be set in chat boxes, so it'll
* probbaly be handled elsewhere in any case.
*/
var $prefs = $(iq).find('prefs[xmlns="'+Strophe.NS.MAM+'"]');
var default_pref = $prefs.attr('default');
var stanza;
if (default_pref !== converse.message_archiving) {
stanza = $iq({'type': 'set'}).c('prefs', {'xmlns':Strophe.NS.MAM, 'default':converse.message_archiving});
$prefs.children().each(function (idx, child) {
stanza.cnode(child).up();
});
converse.connection.sendIQ(stanza, _.bind(function (feature, iq) {
// XXX: Strictly speaking, the server should respond with the updated prefs
// (see example 18: https://xmpp.org/extensions/xep-0313.html#config)
// but Prosody doesn't do this, so we don't rely on it.
feature.save({'preferences': {'default':converse.message_archiving}});
}, this, feature),
_.bind(this.onMAMError, this, feature)
);
} else {
feature.save({'preferences': {'default':converse.message_archiving}});
}
},
onMAMError: function (iq) {
if ($(iq).find('feature-not-implemented').length) {
converse.log("Message Archive Management (XEP-0313) not supported by this browser");
} else {
converse.log("An error occured while trying to set archiving preferences.");
converse.log(iq);
}
},
addClientIdentities: function () {
/* See http://xmpp.org/registrar/disco-categories.html
*/
......@@ -5097,19 +5324,23 @@
* it will advertise to any #info queries made to it.
*
* See: http://xmpp.org/extensions/xep-0030.html#info
*
* TODO: these features need to be added in the relevant
* feature-providing Models, not here
*/
converse.connection.disco.addFeature(Strophe.NS.CHATSTATES);
converse.connection.disco.addFeature(Strophe.NS.ROSTERX); // Limited support
converse.connection.disco.addFeature('jabber:x:conference');
converse.connection.disco.addFeature('urn:xmpp:carbons:2');
converse.connection.disco.addFeature(Strophe.NS.VCARD);
converse.connection.disco.addFeature(Strophe.NS.BOSH);
converse.connection.disco.addFeature(Strophe.NS.DISCO_INFO);
converse.connection.disco.addFeature(Strophe.NS.MUC);
return this;
converse.connection.disco.addFeature('jabber:x:conference');
converse.connection.disco.addFeature(Strophe.NS.BOSH);
converse.connection.disco.addFeature(Strophe.NS.CHATSTATES);
converse.connection.disco.addFeature(Strophe.NS.DISCO_INFO);
converse.connection.disco.addFeature(Strophe.NS.MAM);
converse.connection.disco.addFeature(Strophe.NS.ROSTERX); // Limited support
if (converse.use_vcards) {
converse.connection.disco.addFeature(Strophe.NS.VCARD);
}
if (converse.allow_muc) {
converse.connection.disco.addFeature(Strophe.NS.MUC);
}
if (converse.message_carbons) {
converse.connection.disco.addFeature(Strophe.NS.CARBONS);
}
return this;
},
onItems: function (stanza) {
......@@ -5940,6 +6171,7 @@
};
var wrappedChatBox = function (chatbox) {
if (!chatbox) { return; }
var view = converse.chatboxviews.get(chatbox.get('jid'));
return {
'close': view.close.bind(view),
......@@ -5955,28 +6187,7 @@
};
};
var getWrappedChatBox = function (jid) {
var bare_jid = Strophe.getBareJidFromJid(jid);
var chatbox = converse.chatboxes.get(bare_jid);
if (!chatbox) {
var roster_item = converse.roster.get(bare_jid);
if (roster_item === undefined) {
converse.log('Could not get roster item for JID '+bare_jid, 'error');
return null;
}
chatbox = converse.chatboxes.create({
'id': bare_jid,
'jid': bare_jid,
'fullname': _.isEmpty(roster_item.get('fullname'))? jid: roster_item.get('fullname'),
'image_type': roster_item.get('image_type'),
'image': roster_item.get('image'),
'url': roster_item.get('url')
});
}
return wrappedChatBox(chatbox);
};
return {
var API = {
'initialize': function (settings, callback) {
converse.initialize(settings, callback);
},
......@@ -6063,12 +6274,12 @@
converse.log("chats.open: You need to provide at least one JID", "error");
return null;
} else if (typeof jids === "string") {
chatbox = getWrappedChatBox(jids);
chatbox = wrappedChatBox(converse.chatboxes.getChatBox(jids, true));
chatbox.open();
return chatbox;
}
return _.map(jids, function (jid) {
var chatbox = getWrappedChatBox(jid);
chatbox = wrappedChatBox(converse.chatboxes.getChatBox(jid, true));
chatbox.open();
return chatbox;
});
......@@ -6078,9 +6289,91 @@
converse.log("chats.get: You need to provide at least one JID", "error");
return null;
} else if (typeof jids === "string") {
return getWrappedChatBox(jids);
return wrappedChatBox(converse.chatboxes.getChatBox(jids, true));
}
return _.map(jids, getWrappedChatBox);
return _.map(jids, _.partial(_.compose(wrappedChatBox, converse.chatboxes.getChatBox.bind(converse.chatboxes)), _, true));
}
},
'archive': {
'query': function (options, callback, errback) {
/* Do a MAM (XEP-0313) query for archived messages.
*
* Parameters:
* (Object) options - Query parameters, either MAM-specific or also for Result Set Management.
* (Function) callback - A function to call whenever we receive query-relevant stanza.
* (Function) errback - A function to call when an error stanza is received.
*
* The options parameter can also be an instance of
* Strophe.RSM to enable easy querying between results pages.
*
* The callback function may be called multiple times, first
* for the initial IQ result and then for each message
* returned. The last time the callback is called, a
* Strophe.RSM object is returned on which "next" or "previous"
* can be called before passing it in again to this method, to
* get the next or previous page in the result set.
*/
var date, messages = [];
if (typeof options == "function") {
callback = options;
errback = callback;
}
if (!converse.features.findWhere({'var': Strophe.NS.MAM})) {
throw new Error('This server does not support XEP-0313, Message Archive Management');
}
var queryid = converse.connection.getUniqueId();
var attrs = {'type':'set'};
if (typeof options != "undefined" && options.groupchat) {
if (!options['with']) {
throw new Error('You need to specify a "with" value containing the chat room JID, when querying groupchat messages.');
}
attrs.to = options['with'];
}
var stanza = $iq(attrs).c('query', {'xmlns':Strophe.NS.MAM, 'queryid':queryid});
if (typeof options != "undefined") {
stanza.c('x', {'xmlns':Strophe.NS.XFORM})
.c('field', {'var':'FORM_TYPE'})
.c('value').t(Strophe.NS.MAM).up().up();
if (options['with'] && !options.groupchat) {
stanza.c('field', {'var':'with'}).c('value').t(options['with']).up().up();
}
_.each(['start', 'end'], function (t) {
if (options[t]) {
date = moment(options[t]);
if (date.isValid()) {
stanza.c('field', {'var':t}).c('value').t(date.format()).up().up();
} else {
throw new TypeError('archive.query: invalid date provided for: '+t);
}
}
});
stanza.up();
if (options instanceof Strophe.RSM) {
stanza.cnode(options.toXML());
} else if (_.intersection(RSM_ATTRIBUTES, _.keys(options)).length) {
stanza.cnode(new Strophe.RSM(options).toXML());
}
}
converse.connection.addHandler(function (message) {
var $msg = $(message), $fin, rsm, i;
if (typeof callback == "function") {
$fin = $msg.find('fin[xmlns="'+Strophe.NS.MAM+'"]');
if ($fin.length) {
rsm = new Strophe.RSM({xml: $fin.find('set')[0]});
_.extend(rsm, _.pick(options, ['max']));
_.extend(rsm, _.pick(options, MAM_ATTRIBUTES));
callback(messages, rsm);
return false; // We've received all messages, decommission this handler
} else if (queryid == $msg.find('result').attr('queryid')) {
messages.push(message);
}
return true;
} else {
return false; // There's no callback, so no use in continuing this handler.
}
}, Strophe.NS.MAM);
converse.connection.sendIQ(stanza, null, errback);
}
},
'rooms': {
......@@ -6104,7 +6397,7 @@
'box_id' : b64_sha1(jid)
});
}
return wrappedChatBox(chatroom);
return wrappedChatBox(converse.chatboxes.getChatBox(chatroom, true));
};
if (typeof jids === "undefined") {
throw new TypeError('rooms.open: You need to provide at least one JID');
......@@ -6117,9 +6410,10 @@
if (typeof jids === "undefined") {
throw new TypeError("rooms.get: You need to provide at least one JID");
} else if (typeof jids === "string") {
return getWrappedChatBox(jids);
return wrappedChatBox(converse.chatboxes.getChatBox(jids, true));
}
return _.map(jids, getWrappedChatBox);
return _.map(jids, _.partial(wrappedChatBox, _.bind(converse.chatboxes.getChatBox, converse.chatboxes, _, true)));
}
},
'tokens': {
......@@ -6195,4 +6489,5 @@
'moment': moment
}
};
return API;
}));
......@@ -6,6 +6,8 @@ Changelog
* #439 auto_login and keepalive not working [jcbrand]
* #440 null added as resource to contact [jcbrand]
* Add new event serviceDiscovered [jcbrand]
* Add a new configuration setting `muc_history_max_stanzas`. [jcbrand]
0.9.4 (2015-07-04)
------------------
......
......@@ -53,6 +53,23 @@ This enables anonymous login if the XMPP server supports it. This option can be
used together with `auto_login`_ to automatically and anonymously log a user in
as soon as the page loads.
archived_messages_page_size
---------------------------
* Default: ``20``
See also: `message_archiving`
This feature applies to `XEP-0313: Message Archive Management (MAM) <https://xmpp.org/extensions/xep-0313.html>`_
and will only take effect if your server supports MAM.
It allows you to specify the maximum amount of archived messages to be returned per query.
When you open a chat box or room, archived messages will be displayed (if
available) and the amount returned will be no more than the page size.
You will be able to query for even older messages by scrolling upwards in the chat box or room
(the so-called infinite scrolling pattern).
prebind
~~~~~~~
......@@ -327,6 +344,19 @@ See also:
`XEP-0198 <http://xmpp.org/extensions/xep-0198.html>`_, specifically
with regards to "stream resumption".
message_archiving
-----------------
* Default: ``never``
Provides support for `XEP-0313: Message Archive Management <https://xmpp.org/extensions/xep-0313.html>`_
This sets the default archiving preference. Valid values are ``never``, ``always`` and ``roster``.
``roster`` means that only messages to and from JIDs in your roster will be
archived. The other two values are self-explanatory.
message_carbons
---------------
......@@ -348,6 +378,23 @@ Message carbons is the XEP (Jabber protocol extension) specifically drafted to
solve this problem, while `forward_messages`_ uses
`stanza forwarding <http://www.xmpp.org/extensions/xep-0297.html>`_
muc_history_max_stanzas
-----------------------
* Default: ``undefined``
This option allows you to specify the maximum amount of messages to be shown in a
chat room when you enter it. By default, the amount specified in the room
configuration or determined by the server will be returned.
Please note, this option is not related to
`XEP-0313 Message Archive Management <https://xmpp.org/extensions/xep-0313.html>`_,
which also allows you to show archived chat room messages, but follows a
different approach.
If you're using MAM for archiving chat room messages, you might want to set
this option to zero.
expose_rid_and_sid
------------------
......
......@@ -51,14 +51,14 @@ directory:
On Windows you need to specify Makefile.win to be used by running: ::
make -f Makefile.win dev
Or alternatively, if you don't have GNU Make:
::
npm install
bower update
This will first install the Node.js development tools (like Grunt and Bower)
and then use Bower to install all of Converse.js's front-end dependencies.
......@@ -125,7 +125,7 @@ Please read the `style guide </docs/html/style_guide.html>`_ and make sure that
Add tests for your bugfix or feature
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Add a test for any bug fixed or feature added. We use Jasmine
for testing.
for testing.
Take a look at `tests.html <https://github.com/jcbrand/converse.js/blob/master/tests.html>`_
and the `spec files <https://github.com/jcbrand/converse.js/blob/master/tests.html>`_
......@@ -146,7 +146,7 @@ Developer API
Earlier versions of Converse.js might have different API methods or none at all.
In the Converse.js API, you traverse towards a logical grouping, from
which you can then call certain standardised accessors and mutators, like::
which you can then call certain standardised accessors and mutators, such as::
.get
.set
......@@ -202,6 +202,165 @@ Example:
roster_groups: true
});
The "archive" grouping
----------------------
Converse.js supports the *Message Archive Management*
(`XEP-0313 <https://xmpp.org/extensions/xep-0313.html>`_) protocol,
through which it is able to query an XMPP server for archived messages.
See also the **message_archiving** option in the :ref:`configuration-variables` section, which you'll usually
want to in conjunction with this API.
query
~~~~~
The ``query`` method is used to query for archived messages.
It accepts the following optional parameters:
* **options** an object containing the query parameters. Valid query parameters
are ``with``, ``start``, ``end``, ``first``, ``last``, ``after``, ``before``, ``index`` and ``count``.
* **callback** is the callback method that will be called when all the messages
have been received.
* **errback** is the callback method to be called when an error is returned by
the XMPP server, for example when it doesn't support message archiving.
Examples
^^^^^^^^
**Requesting all archived messages**
The simplest query that can be made is to simply not pass in any parameters.
Such a query will return all archived messages for the current user.
Generally, you'll however always want to pass in a callback method, to receive
the returned messages.
.. code-block:: javascript
var errback = function (iq) {
// The query was not successful, perhaps inform the user?
// The IQ stanza returned by the XMPP server is passed in, so that you
// may inspect it and determine what the problem was.
}
var callback = function (messages) {
// Do something with the messages, like showing them in your webpage.
}
converse.archive.query(callback, errback))
**Waiting until server support has been determined**
The query method will only work if converse.js has been able to determine that
the server supports MAM queries, otherwise the following error will be raised:
- *This server does not support XEP-0313, Message Archive Management*
The very first time converse.js loads in a browser tab, if you call the query
API too quickly, the above error might appear because service discovery has not
yet been completed.
To work solve this problem, you can first listen for the ``serviceDiscovered`` event,
through which you can be informed once support for MAM has been determined.
For example:
.. code-block:: javascript
converse.listen.on('serviceDiscovered', function (event, feature) {
if (feature.get('var') === converse.env.Strophe.NS.MAM) {
converse.archive.query()
}
});
**Requesting all archived messages for a particular contact or room**
To query for messages sent between the current user and another user or room,
the query options need to contain the the JID (Jabber ID) of the user or
room under the ``with`` key.
.. code-block:: javascript
// For a particular user
converse.archive.query({'with': 'john@doe.net'}, callback, errback);)
// For a particular room
converse.archive.query({'with': 'discuss@conference.doglovers.net'}, callback, errback);)
**Requesting all archived messages before or after a certain date**
The ``start`` and ``end`` parameters are used to query for messages
within a certain timeframe. The passed in date values may either be ISO8601
formatted date strings, or Javascript Date objects.
.. code-block:: javascript
var options = {
'with': 'john@doe.net',
'start': '2010-06-07T00:00:00Z',
'end': '2010-07-07T13:23:54Z'
};
converse.archive.query(options, callback, errback);
**Limiting the amount of messages returned**
The amount of returned messages may be limited with the ``max`` parameter.
By default, the messages are returned from oldest to newest.
.. code-block:: javascript
// Return maximum 10 archived messages
converse.archive.query({'with': 'john@doe.net', 'max':10}, callback, errback);
**Paging forwards through a set of archived messages**
When limiting the amount of messages returned per query, you might want to
repeatedly make a further query to fetch the next batch of messages.
To simplify this usecase for you, the callback method receives not only an array
with the returned archived messages, but also a special RSM (*Result Set
Management*) object which contains the query parameters you passed in, as well
as two utility methods ``next``, and ``previous``.
When you call one of these utility methods on the returned RSM object, and then
pass the result into a new query, you'll receive the next or previous batch of
archived messages. Please note, when calling these methods, pass in an integer
to limit your results.
.. code-block:: javascript
var callback = function (messages, rsm) {
// Do something with the messages, like showing them in your webpage.
// ...
// You can now use the returned "rsm" object, to fetch the next batch of messages:
converse.archive.query(rsm.next(10), callback, errback))
}
converse.archive.query({'with': 'john@doe.net', 'max':10}, callback, errback);
**Paging backwards through a set of archived messages**
To page backwards through the archive, you need to know the UID of the message
which you'd like to page backwards from and then pass that as value for the
``before`` parameter. If you simply want to page backwards from the most recent
message, pass in the ``before`` parameter with an empty string value ``''``.
.. code-block:: javascript
converse.archive.query({'before': '', 'max':5}, function (message, rsm) {
// Do something with the messages, like showing them in your webpage.
// ...
// You can now use the returned "rsm" object, to fetch the previous batch of messages:
rsm.previous(5); // Call previous method, to update the object's parameters,
// passing in a limit value of 5.
// Now we query again, to get the previous batch.
converse.archive.query(rsm, callback, errback);
}
The "user" grouping
-------------------
......@@ -213,7 +372,7 @@ logout
Log the user out of the current XMPP session.
.. code-block:: javascript
.. code-block:: javascript
converse.user.logout();
......@@ -228,7 +387,7 @@ get
Return the current user's availability status:
.. code-block:: javascript
.. code-block:: javascript
converse.user.status.get(); // Returns for example "dnd"
......@@ -246,7 +405,7 @@ The user's status can be set to one of the following values:
For example:
.. code-block:: javascript
.. code-block:: javascript
converse.user.status.set('dnd');
......@@ -254,7 +413,7 @@ Because the user's availability is often set together with a custom status
message, this method also allows you to pass in a status message as a
second parameter:
.. code-block:: javascript
.. code-block:: javascript
converse.user.status.set('dnd', 'In a meeting');
......@@ -264,7 +423,7 @@ The "message" sub-grouping
The ``user.status.message`` sub-grouping exposes methods for setting and
retrieving the user's custom status message.
.. code-block:: javascript
.. code-block:: javascript
converse.user.status.message.set('In a meeting');
......@@ -344,7 +503,7 @@ Provide the JID of the contact you want to add:
.. code-block:: javascript
converse.contacts.add('buddy@example.com')
You may also provide the fullname. If not present, we use the jid as fullname:
.. code-block:: javascript
......@@ -580,43 +739,45 @@ Here are the different events that are emitted:
+---------------------------------+---------------------------------------------------------------------------------------------------+------------------------------------------------------------------------------------------------------+
| Event Type | When is it triggered? | Example |
+=================================+===================================================================================================+======================================================================================================+
| **initialized** | Once converse.js has been initialized. | ``converse.listen.on('initialized', function (event) { ... });`` |
| **callButtonClicked** | When a call button (i.e. with class .toggle-call) on a chat box has been clicked. | ``converse.listen.on('callButtonClicked', function (event, connection, model) { ... });`` |
+---------------------------------+---------------------------------------------------------------------------------------------------+------------------------------------------------------------------------------------------------------+
| **ready** | After connection has been established and converse.js has got all its ducks in a row. | ``converse.listen.on('ready', function (event) { ... });`` |
| **chatBoxOpened** | When a chat box has been opened. | ``converse.listen.on('chatBoxOpened', function (event, chatbox) { ... });`` |
+---------------------------------+---------------------------------------------------------------------------------------------------+------------------------------------------------------------------------------------------------------+
| **reconnect** | After the connection has dropped. Converse.js will attempt to reconnect when not in prebind mode. | ``converse.listen.on('reconnect', function (event) { ... });`` |
| **chatRoomOpened** | When a chat room has been opened. | ``converse.listen.on('chatRoomOpened', function (event, chatbox) { ... });`` |
+---------------------------------+---------------------------------------------------------------------------------------------------+------------------------------------------------------------------------------------------------------+
| **message** | When a message is received. | ``converse.listen.on('message', function (event, messageXML) { ... });`` |
| **chatBoxClosed** | When a chat box has been closed. | ``converse.listen.on('chatBoxClosed', function (event, chatbox) { ... });`` |
+---------------------------------+---------------------------------------------------------------------------------------------------+------------------------------------------------------------------------------------------------------+
| **messageSend** | When a message will be sent out. | ``storage_memoryconverse.listen.on('messageSend', function (event, messageText) { ... });`` |
| **chatBoxFocused** | When the focus has been moved to a chat box. | ``converse.listen.on('chatBoxFocused', function (event, chatbox) { ... });`` |
+---------------------------------+---------------------------------------------------------------------------------------------------+------------------------------------------------------------------------------------------------------+
| **noResumeableSession** | When keepalive=true but there aren't any stored prebind tokens. | ``converse.listen.on('noResumeableSession', function (event) { ... });`` |
| **chatBoxToggled** | When a chat box has been minimized or maximized. | ``converse.listen.on('chatBoxToggled', function (event, chatbox) { ... });`` |
+---------------------------------+---------------------------------------------------------------------------------------------------+------------------------------------------------------------------------------------------------------+
| **roster** | When the roster is updated. | ``converse.listen.on('roster', function (event, items) { ... });`` |
| **contactStatusChanged** | When a chat buddy's chat status has changed. | ``converse.listen.on('contactStatusChanged', function (event, buddy, status) { ... });`` |
+---------------------------------+---------------------------------------------------------------------------------------------------+------------------------------------------------------------------------------------------------------+
| **callButtonClicked** | When a call button (i.e. with class .toggle-call) on a chat box has been clicked. | ``converse.listen.on('callButtonClicked', function (event, connection, model) { ... });`` |
| **contactStatusMessageChanged** | When a chat buddy's custom status message has changed. | ``converse.listen.on('contactStatusMessageChanged', function (event, buddy, messageText) { ... });`` |
+---------------------------------+---------------------------------------------------------------------------------------------------+------------------------------------------------------------------------------------------------------+
| **chatBoxOpened** | When a chat box has been opened. | ``converse.listen.on('chatBoxOpened', function (event, chatbox) { ... });`` |
| **message** | When a message is received. | ``converse.listen.on('message', function (event, messageXML) { ... });`` |
+---------------------------------+---------------------------------------------------------------------------------------------------+------------------------------------------------------------------------------------------------------+
| **chatRoomOpened** | When a chat room has been opened. | ``converse.listen.on('chatRoomOpened', function (event, chatbox) { ... });`` |
| **messageSend** | When a message will be sent out. | ``storage_memoryconverse.listen.on('messageSend', function (event, messageText) { ... });`` |
+---------------------------------+---------------------------------------------------------------------------------------------------+------------------------------------------------------------------------------------------------------+
| **chatBoxClosed** | When a chat box has been closed. | ``converse.listen.on('chatBoxClosed', function (event, chatbox) { ... });`` |
| **noResumeableSession** | When keepalive=true but there aren't any stored prebind tokens. | ``converse.listen.on('noResumeableSession', function (event) { ... });`` |
+---------------------------------+---------------------------------------------------------------------------------------------------+------------------------------------------------------------------------------------------------------+
| **chatBoxFocused** | When the focus has been moved to a chat box. | ``converse.listen.on('chatBoxFocused', function (event, chatbox) { ... });`` |
| **initialized** | Once converse.js has been initialized. | ``converse.listen.on('initialized', function (event) { ... });`` |
+---------------------------------+---------------------------------------------------------------------------------------------------+------------------------------------------------------------------------------------------------------+
| **chatBoxToggled** | When a chat box has been minimized or maximized. | ``converse.listen.on('chatBoxToggled', function (event, chatbox) { ... });`` |
| **ready** | After connection has been established and converse.js has got all its ducks in a row. | ``converse.listen.on('ready', function (event) { ... });`` |
+---------------------------------+---------------------------------------------------------------------------------------------------+------------------------------------------------------------------------------------------------------+
| **reconnect** | After the connection has dropped. Converse.js will attempt to reconnect when not in prebind mode. | ``converse.listen.on('reconnect', function (event) { ... });`` |
+---------------------------------+---------------------------------------------------------------------------------------------------+------------------------------------------------------------------------------------------------------+
| **roomInviteSent** | After the user has sent out a direct invitation, to a roster contact, asking them to join a room. | ``converse.listen.on('roomInvite', function (event, roomview, invitee_jid, reason) { ... });`` |
+---------------------------------+---------------------------------------------------------------------------------------------------+------------------------------------------------------------------------------------------------------+
| **roomInviteReceived** | After the user has sent out a direct invitation, to a roster contact, asking them to join a room. | ``converse.listen.on('roomInvite', function (event, roomview, invitee_jid, reason) { ... });`` |
+---------------------------------+---------------------------------------------------------------------------------------------------+------------------------------------------------------------------------------------------------------+
| **roster** | When the roster is updated. | ``converse.listen.on('roster', function (event, items) { ... });`` |
+---------------------------------+---------------------------------------------------------------------------------------------------+------------------------------------------------------------------------------------------------------+
| **statusChanged** | When own chat status has changed. | ``converse.listen.on('statusChanged', function (event, status) { ... });`` |
+---------------------------------+---------------------------------------------------------------------------------------------------+------------------------------------------------------------------------------------------------------+
| **statusMessageChanged** | When own custom status message has changed. | ``converse.listen.on('statusMessageChanged', function (event, message) { ... });`` |
+---------------------------------+---------------------------------------------------------------------------------------------------+------------------------------------------------------------------------------------------------------+
| **contactStatusChanged** | When a chat buddy's chat status has changed. | ``converse.listen.on('contactStatusChanged', function (event, buddy, status) { ... });`` |
+---------------------------------+---------------------------------------------------------------------------------------------------+------------------------------------------------------------------------------------------------------+
| **contactStatusMessageChanged** | When a chat buddy's custom status message has changed. | ``converse.listen.on('contactStatusMessageChanged', function (event, buddy, messageText) { ... });`` |
| **serviceDiscovered** | When converse.js has learned of a service provided by the XMPP server. See XEP-0030. | ``converse.listen.on('serviceDiscovered', function (event, service) { ... });`` |
+---------------------------------+---------------------------------------------------------------------------------------------------+------------------------------------------------------------------------------------------------------+
......@@ -663,7 +824,7 @@ An example plugin
}(this, function ($, strophe, utils, converse_api) {
// Wrap your UI strings with the __ function for translation support.
var __ = $.proxy(utils.__, this);
var __ = $.proxy(utils.__, this);
// Strophe methods for building stanzas
var Strophe = strophe.Strophe;
......
......@@ -26,18 +26,19 @@ require.config({
"jquery-private": "src/jquery-private",
"jquery.browser": "components/jquery.browser/dist/jquery.browser",
"jquery.easing": "components/jquery-easing-original/index", // XXX: Only required for https://conversejs.org website
"moment": "components/momentjs/min/moment.min",
"moment": "components/momentjs/moment",
"strophe": "components/strophejs/src/wrapper",
"strophe-base64": "components/strophejs/src/base64",
"strophe-bosh": "components/strophejs/src/bosh",
"strophe-core": "components/strophejs/src/core",
"strophe": "components/strophejs/src/wrapper",
"strophe-md5": "components/strophejs/src/md5",
"strophe-polyfill": "components/strophejs/src/polyfills",
"strophe-sha1": "components/strophejs/src/sha1",
"strophe-websocket": "components/strophejs/src/websocket",
"strophe-polyfill": "components/strophejs/src/polyfills",
"strophe.disco": "components/strophejs-plugins/disco/strophe.disco",
"strophe.vcard": "src/strophe.vcard",
"strophe.ping": "src/strophe.ping",
"strophe.rsm": "components/strophejs-plugins/rsm/strophe.rsm",
"strophe.vcard": "src/strophe.vcard",
"text": 'components/requirejs-text/text',
"tpl": 'components/requirejs-tpl-jcbrand/tpl',
"typeahead": "components/typeahead.js/index",
......@@ -185,10 +186,9 @@ require.config({
'crypto.sha1': { deps: ['crypto.core'] },
'crypto.sha256': { deps: ['crypto.core'] },
'bigint': { deps: ['crypto'] },
'strophe.disco': { deps: ['strophe'] },
'strophe.ping': { deps: ['strophe'] },
'strophe.register': { deps: ['strophe'] },
'strophe.vcard': { deps: ['strophe'] },
'strophe.ping': { deps: ['strophe'] }
'strophe.vcard': { deps: ['strophe'] }
}
});
......
......@@ -652,7 +652,7 @@
var message_date = new Date();
expect($time.length).toEqual(1);
expect($time.attr('class')).toEqual('chat-date');
expect($time.attr('datetime')).toEqual(moment(message_date).format("YYYY-MM-DD"));
expect($time.data('isodate')).toEqual(moment(message_date).format());
expect($time.text()).toEqual(moment(message_date).format("dddd MMM Do YYYY"));
// Normal checks for the 2nd message
......
......@@ -299,6 +299,10 @@
var box = converse_api.chats.open(jid);
expect(box instanceof Object).toBeTruthy();
expect(box.get('box_id')).toBe(b64_sha1(jid));
expect(
Object.keys(box),
['close', 'endOTR', 'focus', 'get', 'initiateOTR', 'is_chatroom', 'maximize', 'minimize', 'open', 'set']
);
var chatboxview = this.chatboxviews.get(jid);
expect(chatboxview.$el.is(':visible')).toBeTruthy();
......
(function (root, factory) {
define([
"jquery",
"mock",
"test_utils"
], function ($, mock, test_utils) {
return factory($, mock, test_utils);
}
);
} (this, function ($, mock, test_utils) {
"use strict";
var Strophe = converse_api.env.Strophe;
describe("Service Discovery", $.proxy(function (mock, test_utils) {
describe("Whenever converse.js discovers a new server feature", $.proxy(function (mock, test_utils) {
it("emits the serviceDiscovered event", function () {
spyOn(converse, 'emit');
converse.features.create({'var': Strophe.NS.MAM});
expect(converse.emit).toHaveBeenCalled();
expect(converse.emit.argsForCall[0][1].get('var')).toBe(Strophe.NS.MAM);
});
}, converse, mock, test_utils));
}, converse, mock, test_utils));
}));
(function (root, factory) {
define([
"jquery",
"mock",
"test_utils"
], function ($, mock, test_utils) {
return factory($, mock, test_utils);
}
);
} (this, function ($, mock, test_utils) {
"use strict";
var Strophe = converse_api.env.Strophe;
var $iq = converse_api.env.$iq;
var $pres = converse_api.env.$pres;
var $msg = converse_api.env.$msg;
var moment = converse_api.env.moment;
// See: https://xmpp.org/rfcs/rfc3921.html
describe("Message Archive Management", $.proxy(function (mock, test_utils) {
// Implement the protocol defined in https://xmpp.org/extensions/xep-0313.html#config
describe("The archive.query API", $.proxy(function (mock, test_utils) {
it("can be used to query for all archived messages", function () {
var sent_stanza, IQ_id;
var sendIQ = converse.connection.sendIQ;
spyOn(converse.connection, 'sendIQ').andCallFake(function (iq, callback, errback) {
sent_stanza = iq;
IQ_id = sendIQ.bind(this)(iq, callback, errback);
});
if (!converse.features.findWhere({'var': Strophe.NS.MAM})) {
converse.features.create({'var': Strophe.NS.MAM});
}
converse_api.archive.query();
var queryid = $(sent_stanza.toString()).find('query').attr('queryid');
expect(sent_stanza.toString()).toBe(
"<iq type='set' xmlns='jabber:client' id='"+IQ_id+"'><query xmlns='urn:xmpp:mam:0' queryid='"+queryid+"'/></iq>");
});
it("can be used to query for all messages to/from a particular JID", function () {
var sent_stanza, IQ_id;
var sendIQ = converse.connection.sendIQ;
spyOn(converse.connection, 'sendIQ').andCallFake(function (iq, callback, errback) {
sent_stanza = iq;
IQ_id = sendIQ.bind(this)(iq, callback, errback);
});
if (!converse.features.findWhere({'var': Strophe.NS.MAM})) {
converse.features.create({'var': Strophe.NS.MAM});
}
converse_api.archive.query({'with':'juliet@capulet.lit'});
var queryid = $(sent_stanza.toString()).find('query').attr('queryid');
expect(sent_stanza.toString()).toBe(
"<iq type='set' xmlns='jabber:client' id='"+IQ_id+"'>"+
"<query xmlns='urn:xmpp:mam:0' queryid='"+queryid+"'>"+
"<x xmlns='jabber:x:data'>"+
"<field var='FORM_TYPE'>"+
"<value>urn:xmpp:mam:0</value>"+
"</field>"+
"<field var='with'>"+
"<value>juliet@capulet.lit</value>"+
"</field>"+
"</x>"+
"</query>"+
"</iq>"
);
});
it("can be used to query for all messages in a certain timespan", function () {
var sent_stanza, IQ_id;
var sendIQ = converse.connection.sendIQ;
spyOn(converse.connection, 'sendIQ').andCallFake(function (iq, callback, errback) {
sent_stanza = iq;
IQ_id = sendIQ.bind(this)(iq, callback, errback);
});
if (!converse.features.findWhere({'var': Strophe.NS.MAM})) {
converse.features.create({'var': Strophe.NS.MAM});
}
var start = '2010-06-07T00:00:00Z';
var end = '2010-07-07T13:23:54Z';
converse_api.archive.query({
'start': start,
'end': end
});
var queryid = $(sent_stanza.toString()).find('query').attr('queryid');
expect(sent_stanza.toString()).toBe(
"<iq type='set' xmlns='jabber:client' id='"+IQ_id+"'>"+
"<query xmlns='urn:xmpp:mam:0' queryid='"+queryid+"'>"+
"<x xmlns='jabber:x:data'>"+
"<field var='FORM_TYPE'>"+
"<value>urn:xmpp:mam:0</value>"+
"</field>"+
"<field var='start'>"+
"<value>"+moment(start).format()+"</value>"+
"</field>"+
"<field var='end'>"+
"<value>"+moment(end).format()+"</value>"+
"</field>"+
"</x>"+
"</query>"+
"</iq>"
);
});
it("throws a TypeError if an invalid date is provided", function () {
expect(_.partial(converse_api.archive.query, {'start': 'not a real date'})).toThrow(
new TypeError('archive.query: invalid date provided for: start')
);
});
it("can be used to query for all messages after a certain time", function () {
var sent_stanza, IQ_id;
var sendIQ = converse.connection.sendIQ;
spyOn(converse.connection, 'sendIQ').andCallFake(function (iq, callback, errback) {
sent_stanza = iq;
IQ_id = sendIQ.bind(this)(iq, callback, errback);
});
if (!converse.features.findWhere({'var': Strophe.NS.MAM})) {
converse.features.create({'var': Strophe.NS.MAM});
}
var start = '2010-06-07T00:00:00Z';
converse_api.archive.query({'start': start});
var queryid = $(sent_stanza.toString()).find('query').attr('queryid');
expect(sent_stanza.toString()).toBe(
"<iq type='set' xmlns='jabber:client' id='"+IQ_id+"'>"+
"<query xmlns='urn:xmpp:mam:0' queryid='"+queryid+"'>"+
"<x xmlns='jabber:x:data'>"+
"<field var='FORM_TYPE'>"+
"<value>urn:xmpp:mam:0</value>"+
"</field>"+
"<field var='start'>"+
"<value>"+moment(start).format()+"</value>"+
"</field>"+
"</x>"+
"</query>"+
"</iq>"
);
});
it("can be used to query for a limited set of results", function () {
var sent_stanza, IQ_id;
var sendIQ = converse.connection.sendIQ;
spyOn(converse.connection, 'sendIQ').andCallFake(function (iq, callback, errback) {
sent_stanza = iq;
IQ_id = sendIQ.bind(this)(iq, callback, errback);
});
if (!converse.features.findWhere({'var': Strophe.NS.MAM})) {
converse.features.create({'var': Strophe.NS.MAM});
}
var start = '2010-06-07T00:00:00Z';
converse_api.archive.query({'start': start, 'max':10});
var queryid = $(sent_stanza.toString()).find('query').attr('queryid');
expect(sent_stanza.toString()).toBe(
"<iq type='set' xmlns='jabber:client' id='"+IQ_id+"'>"+
"<query xmlns='urn:xmpp:mam:0' queryid='"+queryid+"'>"+
"<x xmlns='jabber:x:data'>"+
"<field var='FORM_TYPE'>"+
"<value>urn:xmpp:mam:0</value>"+
"</field>"+
"<field var='start'>"+
"<value>"+moment(start).format()+"</value>"+
"</field>"+
"</x>"+
"<set xmlns='http://jabber.org/protocol/rsm'>"+
"<max>10</max>"+
"</set>"+
"</query>"+
"</iq>"
);
});
it("can be used to page through results", function () {
var sent_stanza, IQ_id;
var sendIQ = converse.connection.sendIQ;
spyOn(converse.connection, 'sendIQ').andCallFake(function (iq, callback, errback) {
sent_stanza = iq;
IQ_id = sendIQ.bind(this)(iq, callback, errback);
});
if (!converse.features.findWhere({'var': Strophe.NS.MAM})) {
converse.features.create({'var': Strophe.NS.MAM});
}
var start = '2010-06-07T00:00:00Z';
converse_api.archive.query({
'start': start,
'after': '09af3-cc343-b409f',
'max':10
});
var queryid = $(sent_stanza.toString()).find('query').attr('queryid');
expect(sent_stanza.toString()).toBe(
"<iq type='set' xmlns='jabber:client' id='"+IQ_id+"'>"+
"<query xmlns='urn:xmpp:mam:0' queryid='"+queryid+"'>"+
"<x xmlns='jabber:x:data'>"+
"<field var='FORM_TYPE'>"+
"<value>urn:xmpp:mam:0</value>"+
"</field>"+
"<field var='start'>"+
"<value>"+moment(start).format()+"</value>"+
"</field>"+
"</x>"+
"<set xmlns='http://jabber.org/protocol/rsm'>"+
"<max>10</max>"+
"<after>09af3-cc343-b409f</after>"+
"</set>"+
"</query>"+
"</iq>"
);
});
it("accepts \"before\" with an empty string as value to reverse the order", function () {
var sent_stanza, IQ_id;
var sendIQ = converse.connection.sendIQ;
spyOn(converse.connection, 'sendIQ').andCallFake(function (iq, callback, errback) {
sent_stanza = iq;
IQ_id = sendIQ.bind(this)(iq, callback, errback);
});
if (!converse.features.findWhere({'var': Strophe.NS.MAM})) {
converse.features.create({'var': Strophe.NS.MAM});
}
converse_api.archive.query({'before': '', 'max':10});
var queryid = $(sent_stanza.toString()).find('query').attr('queryid');
expect(sent_stanza.toString()).toBe(
"<iq type='set' xmlns='jabber:client' id='"+IQ_id+"'>"+
"<query xmlns='urn:xmpp:mam:0' queryid='"+queryid+"'>"+
"<x xmlns='jabber:x:data'>"+
"<field var='FORM_TYPE'>"+
"<value>urn:xmpp:mam:0</value>"+
"</field>"+
"</x>"+
"<set xmlns='http://jabber.org/protocol/rsm'>"+
"<max>10</max>"+
"<before></before>"+
"</set>"+
"</query>"+
"</iq>"
);
});
it("accepts a Strophe.RSM object for the query options", function () {
// Normally the user wouldn't manually make a Strophe.RSM object
// and pass it in. However, in the callback method an RSM object is
// returned which can be reused for easy paging. This test is
// more for that usecase.
if (!converse.features.findWhere({'var': Strophe.NS.MAM})) {
converse.features.create({'var': Strophe.NS.MAM});
}
var sent_stanza, IQ_id;
var sendIQ = converse.connection.sendIQ;
spyOn(converse.connection, 'sendIQ').andCallFake(function (iq, callback, errback) {
sent_stanza = iq;
IQ_id = sendIQ.bind(this)(iq, callback, errback);
});
var rsm = new Strophe.RSM({'max': '10'});
rsm['with'] = 'romeo@montague.lit';
rsm.start = '2010-06-07T00:00:00Z';
converse_api.archive.query(rsm);
var queryid = $(sent_stanza.toString()).find('query').attr('queryid');
expect(sent_stanza.toString()).toBe(
"<iq type='set' xmlns='jabber:client' id='"+IQ_id+"'>"+
"<query xmlns='urn:xmpp:mam:0' queryid='"+queryid+"'>"+
"<x xmlns='jabber:x:data'>"+
"<field var='FORM_TYPE'>"+
"<value>urn:xmpp:mam:0</value>"+
"</field>"+
"<field var='with'>"+
"<value>romeo@montague.lit</value>"+
"</field>"+
"<field var='start'>"+
"<value>"+moment(rsm.start).format()+"</value>"+
"</field>"+
"</x>"+
"<set xmlns='http://jabber.org/protocol/rsm'>"+
"<max>10</max>"+
"</set>"+
"</query>"+
"</iq>"
);
});
it("accepts a callback function, which it passes the messages and a Strophe.RSM object", function () {
if (!converse.features.findWhere({'var': Strophe.NS.MAM})) {
converse.features.create({'var': Strophe.NS.MAM});
}
var sent_stanza, IQ_id;
var sendIQ = converse.connection.sendIQ;
spyOn(converse.connection, 'sendIQ').andCallFake(function (iq, callback, errback) {
sent_stanza = iq;
IQ_id = sendIQ.bind(this)(iq, callback, errback);
});
var callback = jasmine.createSpy('callback');
converse_api.archive.query({'with': 'romeo@capulet.lit', 'max':'10'}, callback);
var queryid = $(sent_stanza.toString()).find('query').attr('queryid');
// Send the result stanza, so that the callback is called.
var stanza = $iq({'type': 'result', 'id': IQ_id});
converse.connection._dataRecv(test_utils.createRequest(stanza));
/* <message id='aeb213' to='juliet@capulet.lit/chamber'>
* <result xmlns='urn:xmpp:mam:0' queryid='f27' id='28482-98726-73623'>
* <forwarded xmlns='urn:xmpp:forward:0'>
* <delay xmlns='urn:xmpp:delay' stamp='2010-07-10T23:08:25Z'/>
* <message
* to='juliet@capulet.lit/balcony'
* from='romeo@montague.lit/orchard'
* type='chat'
* xmlns='jabber:client'>
* <body>Call me but love, and I'll be new baptized; Henceforth I never will be Romeo.</body>
* </message>
* </forwarded>
* </result>
* </message>
*/
var msg1 = $msg({'id':'aeb213', 'to':'juliet@capulet.lit/chamber'})
.c('result', {'xmlns': 'urn:xmpp:mam:0', 'queryid':queryid, 'id':'28482-98726-73623'})
.c('forwarded', {'xmlns':'urn:xmpp:forward:0'})
.c('delay', {'xmlns':'urn:xmpp:delay', 'stamp':'2010-07-10T23:08:25Z'}).up()
.c('message', {
'xmlns':'jabber:client',
'to':'juliet@capulet.lit/balcony',
'from':'romeo@montague.lit/orchard',
'type':'chat' })
.c('body').t("Call me but love, and I'll be new baptized;");
converse.connection._dataRecv(test_utils.createRequest(msg1));
var msg2 = $msg({'id':'aeb213', 'to':'juliet@capulet.lit/chamber'})
.c('result', {'xmlns': 'urn:xmpp:mam:0', 'queryid':queryid, 'id':'28482-98726-73624'})
.c('forwarded', {'xmlns':'urn:xmpp:forward:0'})
.c('delay', {'xmlns':'urn:xmpp:delay', 'stamp':'2010-07-10T23:08:25Z'}).up()
.c('message', {
'xmlns':'jabber:client',
'to':'juliet@capulet.lit/balcony',
'from':'romeo@montague.lit/orchard',
'type':'chat' })
.c('body').t("Henceforth I never will be Romeo.");
converse.connection._dataRecv(test_utils.createRequest(msg2));
/* Send a <fin> message to indicate the end of the result set.
*
* <message>
* <fin xmlns='urn:xmpp:mam:0' complete='true'>
* <set xmlns='http://jabber.org/protocol/rsm'>
* <first index='0'>23452-4534-1</first>
* <last>390-2342-22</last>
* <count>16</count>
* </set>
* </fin>
* </message>
*/
stanza = $msg().c('fin', {'xmlns': 'urn:xmpp:mam:0', 'complete': 'true'})
.c('set', {'xmlns': 'http://jabber.org/protocol/rsm'})
.c('first', {'index': '0'}).t('23452-4534-1').up()
.c('last').t('390-2342-22').up()
.c('count').t('16');
converse.connection._dataRecv(test_utils.createRequest(stanza));
expect(callback).toHaveBeenCalled();
var args = callback.argsForCall[0];
expect(args[0].length).toBe(2);
expect(args[0][0].outerHTML).toBe(msg1.nodeTree.outerHTML);
expect(args[0][1].outerHTML).toBe(msg2.nodeTree.outerHTML);
expect(args[1]['with']).toBe('romeo@capulet.lit');
expect(args[1].max).toBe('10');
expect(args[1].count).toBe('16');
expect(args[1].first).toBe('23452-4534-1');
expect(args[1].last).toBe('390-2342-22');
});
}, converse, mock, test_utils));
describe("The default preference", $.proxy(function (mock, test_utils) {
it("is set once server support for MAM has been confirmed", function () {
var sent_stanza, IQ_id;
var sendIQ = converse.connection.sendIQ;
spyOn(converse.connection, 'sendIQ').andCallFake(function (iq, callback, errback) {
sent_stanza = iq;
IQ_id = sendIQ.bind(this)(iq, callback, errback);
});
spyOn(converse.features, 'onMAMPreferences').andCallThrough();
var feature = new converse.Feature({
'var': Strophe.NS.MAM
});
spyOn(feature, 'save').andCallFake(feature.set); // Save will complain about a url not being set
converse.features.onFeatureAdded(feature);
expect(converse.connection.sendIQ).toHaveBeenCalled();
expect(sent_stanza.toLocaleString()).toBe(
"<iq type='get' xmlns='jabber:client' id='"+IQ_id+"'>"+
"<prefs xmlns='urn:xmpp:mam:0'/>"+
"</iq>"
);
converse.message_archiving = 'never';
/* Example 15. Server responds with current preferences
*
* <iq type='result' id='juliet2'>
* <prefs xmlns='urn:xmpp:mam:0' default='roster'>
* <always/>
* <never/>
* </prefs>
* </iq>
*/
var stanza = $iq({'type': 'result', 'id': IQ_id})
.c('prefs', {'xmlns': Strophe.NS.MAM, 'default':'roster'})
.c('always').c('jid').t('romeo@montague.lit').up().up()
.c('never').c('jid').t('montague@montague.lit');
converse.connection._dataRecv(test_utils.createRequest(stanza));
expect(converse.features.onMAMPreferences).toHaveBeenCalled();
expect(converse.connection.sendIQ.callCount).toBe(2);
expect(sent_stanza.toString()).toBe(
"<iq type='set' xmlns='jabber:client' id='"+IQ_id+"'>"+
"<prefs xmlns='urn:xmpp:mam:0' default='never'>"+
"<always><jid>romeo@montague.lit</jid></always>"+
"<never><jid>montague@montague.lit</jid></never>"+
"</prefs>"+
"</iq>"
);
expect(feature.get('preference')).toBe(undefined);
/* <iq type='result' id='juliet3'>
* <prefs xmlns='urn:xmpp:mam:0' default='always'>
* <always>
* <jid>romeo@montague.lit</jid>
* </always>
* <never>
* <jid>montague@montague.lit</jid>
* </never>
* </prefs>
* </iq>
*/
stanza = $iq({'type': 'result', 'id': IQ_id})
.c('prefs', {'xmlns': Strophe.NS.MAM, 'default':'always'})
.c('always').up()
.c('never').up();
converse.connection._dataRecv(test_utils.createRequest(stanza));
expect(feature.save).toHaveBeenCalled();
expect(feature.get('preferences').default).toBe('never');
// Restore
converse.message_archiving = 'never';
});
}, converse, mock, test_utils));
}, converse, mock, test_utils));
}));
......@@ -4,9 +4,10 @@ define("converse-dependencies", [
"otr",
"moment_with_locales",
"strophe",
"strophe.vcard",
"strophe.disco",
"strophe.ping",
"strophe.rsm",
"strophe.vcard",
"backbone.browserStorage",
"backbone.overview",
"jquery.browser",
......
......@@ -3,9 +3,10 @@ define("converse-dependencies", [
"utils",
"moment_with_locales",
"strophe",
"strophe.vcard",
"strophe.disco",
"strophe.ping",
"strophe.rsm",
"strophe.vcard",
"backbone.browserStorage",
"backbone.overview",
"jquery.browser",
......
......@@ -3,9 +3,10 @@ define("converse-dependencies", [
"utils",
"moment_with_locales",
"strophe",
"strophe.vcard",
"strophe.disco",
"strophe.ping",
"strophe.rsm",
"strophe.vcard",
"bootstrapJS", // XXX: Can be removed, only for https://conversejs.org
"backbone.browserStorage",
"backbone.overview",
......
......@@ -5,9 +5,10 @@ define("converse-dependencies", [
"otr",
"moment_with_locales",
"strophe",
"strophe.vcard",
"strophe.disco",
"strophe.ping",
"strophe.rsm",
"strophe.vcard",
"bootstrapJS", // XXX: Only for https://conversejs.org
"backbone.browserStorage",
"backbone.overview",
......
<div class="chat-message {{extra_classes}}">
<div class="chat-message {{extra_classes}}" data-isodate="{{isodate}}">
<span class="chat-message-{{sender}}">{{time}} **{{username}} </span>
<span class="chat-message-content">{{message}}</span>
</div>
<div class="chat-message {{extra_classes}}">
<div class="chat-message {{extra_classes}}" data-isodate="{{isodate}}">
<span class="chat-message-{{sender}}">{{time}} {{username}}:&nbsp;</span>
<span class="chat-message-content">{{message}}</span>
</div>
<time class="chat-date" datetime="{{isodate}}">{{datestring}}</time>
<time class="chat-date" data-isodate="{{isodate}}">{{datestring}}</time>
......@@ -50,6 +50,41 @@
return this;
};
$.fn.addEmoticons = function (allowed) {
if (allowed) {
if (this.length > 0) {
this.each(function (i, obj) {
var text = $(obj).html();
text = text.replace(/&gt;:\)/g, '<span class="emoticon icon-evil"></span>');
text = text.replace(/:\)/g, '<span class="emoticon icon-smiley"></span>');
text = text.replace(/:\-\)/g, '<span class="emoticon icon-smiley"></span>');
text = text.replace(/;\)/g, '<span class="emoticon icon-wink"></span>');
text = text.replace(/;\-\)/g, '<span class="emoticon icon-wink"></span>');
text = text.replace(/:D/g, '<span class="emoticon icon-grin"></span>');
text = text.replace(/:\-D/g, '<span class="emoticon icon-grin"></span>');
text = text.replace(/:P/g, '<span class="emoticon icon-tongue"></span>');
text = text.replace(/:\-P/g, '<span class="emoticon icon-tongue"></span>');
text = text.replace(/:p/g, '<span class="emoticon icon-tongue"></span>');
text = text.replace(/:\-p/g, '<span class="emoticon icon-tongue"></span>');
text = text.replace(/8\)/g, '<span class="emoticon icon-cool"></span>');
text = text.replace(/:S/g, '<span class="emoticon icon-confused"></span>');
text = text.replace(/:\\/g, '<span class="emoticon icon-wondering"></span>');
text = text.replace(/:\/ /g, '<span class="emoticon icon-wondering"></span>');
text = text.replace(/&gt;:\(/g, '<span class="emoticon icon-angry"></span>');
text = text.replace(/:\(/g, '<span class="emoticon icon-sad"></span>');
text = text.replace(/:\-\(/g, '<span class="emoticon icon-sad"></span>');
text = text.replace(/:O/g, '<span class="emoticon icon-shocked"></span>');
text = text.replace(/:\-O/g, '<span class="emoticon icon-shocked"></span>');
text = text.replace(/\=\-O/g, '<span class="emoticon icon-shocked"></span>');
text = text.replace(/\(\^.\^\)b/g, '<span class="emoticon icon-thumbs-up"></span>');
text = text.replace(/&lt;3/g, '<span class="emoticon icon-heart"></span>');
$(obj).html(text);
});
}
}
return this;
};
var utils = {
// Translation machinery
// ---------------------
......
......@@ -60,7 +60,9 @@ require([
require([
"console-runner",
"spec/converse",
"spec/disco",
"spec/protocol",
"spec/mam",
"spec/otr",
"spec/eventemitter",
"spec/controlbox",
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment