diff --git a/converse.js b/converse.js
index 6b4935faa..742cc415d 100644
--- a/converse.js
+++ b/converse.js
@@ -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(/>:\)/g, '');
- text = text.replace(/:\)/g, '');
- text = text.replace(/:\-\)/g, '');
- text = text.replace(/;\)/g, '');
- text = text.replace(/;\-\)/g, '');
- text = text.replace(/:D/g, '');
- text = text.replace(/:\-D/g, '');
- text = text.replace(/:P/g, '');
- text = text.replace(/:\-P/g, '');
- text = text.replace(/:p/g, '');
- text = text.replace(/:\-p/g, '');
- text = text.replace(/8\)/g, '');
- text = text.replace(/:S/g, '');
- text = text.replace(/:\\/g, '');
- text = text.replace(/:\/ /g, '');
- text = text.replace(/>:\(/g, '');
- text = text.replace(/:\(/g, '');
- text = text.replace(/:\-\(/g, '');
- text = text.replace(/:O/g, '');
- text = text.replace(/:\-O/g, '');
- text = text.replace(/\=\-O/g, '');
- text = text.replace(/\(\^.\^\)b/g, '');
- text = text.replace(/<3/g, '');
- $(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('');
+ }
+ 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($('
').text(message));
+ this.$content.append($('').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[i]+''));
+ this.$content.append($(''+msgs[i]+'
'));
}
if (spinner === true) {
- $chat_content.append('');
+ this.$content.append('');
} 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: %1$s')
},
- 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 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');
+ // 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) {
return true;
}
+ if (msgid && chatbox.messages.findWhere({msgid: msgid})) {
+ return true; // We already have this message stored.
+ }
+ if (!this.isOnlyChatStateNotification($message) && !is_me && !$forwarded.length) {
+ converse.playNotification();
+ }
+ chatbox.receiveMessage($message, $delay, archive_id);
+ converse.roster.addResource(contact_jid, resource);
+ converse.emit('message', message);
+ return true;
+ },
- chatbox = this.get(contact_jid);
- 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;
+ 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;
}
- var fullname = roster_item.get('fullname');
- fullname = _.isEmpty(fullname)? contact_jid: fullname;
chatbox = this.create({
- 'id': contact_jid,
- 'jid': contact_jid,
- 'fullname': fullname,
+ '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')
});
}
- if (msgid && chatbox.messages.findWhere({msgid: msgid})) {
- return true; // We already have this message stored.
- }
- if (!this.isOnlyChatStateNotification($message) && !is_me) {
- converse.playNotification();
- }
- chatbox.receiveMessage($message);
- converse.roster.addResource(contact_jid, resource);
- converse.emit('message', message);
- return true;
+ 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;
}));
diff --git a/docs/CHANGES.rst b/docs/CHANGES.rst
index 825f819b7..65415ab0c 100644
--- a/docs/CHANGES.rst
+++ b/docs/CHANGES.rst
@@ -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)
------------------
diff --git a/docs/source/configuration.rst b/docs/source/configuration.rst
index 3cc591a1b..8d7857a42 100644
--- a/docs/source/configuration.rst
+++ b/docs/source/configuration.rst
@@ -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) `_
+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 `_, specifically
with regards to "stream resumption".
+
+message_archiving
+-----------------
+
+* Default: ``never``
+
+Provides support for `XEP-0313: Message Archive Management `_
+
+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 `_
+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 `_,
+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
------------------
diff --git a/docs/source/development.rst b/docs/source/development.rst
index a056ef1e2..21e63368b 100644
--- a/docs/source/development.rst
+++ b/docs/source/development.rst
@@ -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 `_ 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 `_
and the `spec files `_
@@ -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 `_) 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,20 +739,6 @@ 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) { ... });`` |
-+---------------------------------+---------------------------------------------------------------------------------------------------+------------------------------------------------------------------------------------------------------+
-| **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) { ... });`` |
-+---------------------------------+---------------------------------------------------------------------------------------------------+------------------------------------------------------------------------------------------------------+
-| **message** | When a message is received. | ``converse.listen.on('message', function (event, messageXML) { ... });`` |
-+---------------------------------+---------------------------------------------------------------------------------------------------+------------------------------------------------------------------------------------------------------+
-| **messageSend** | When a message will be sent out. | ``storage_memoryconverse.listen.on('messageSend', function (event, messageText) { ... });`` |
-+---------------------------------+---------------------------------------------------------------------------------------------------+------------------------------------------------------------------------------------------------------+
-| **noResumeableSession** | When keepalive=true but there aren't any stored prebind tokens. | ``converse.listen.on('noResumeableSession', function (event) { ... });`` |
-+---------------------------------+---------------------------------------------------------------------------------------------------+------------------------------------------------------------------------------------------------------+
-| **roster** | When the roster is updated. | ``converse.listen.on('roster', function (event, items) { ... });`` |
-+---------------------------------+---------------------------------------------------------------------------------------------------+------------------------------------------------------------------------------------------------------+
| **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) { ... });`` |
+---------------------------------+---------------------------------------------------------------------------------------------------+------------------------------------------------------------------------------------------------------+
| **chatBoxOpened** | When a chat box has been opened. | ``converse.listen.on('chatBoxOpened', function (event, chatbox) { ... });`` |
@@ -606,17 +751,33 @@ Here are the different events that are emitted:
+---------------------------------+---------------------------------------------------------------------------------------------------+------------------------------------------------------------------------------------------------------+
| **chatBoxToggled** | When a chat box has been minimized or maximized. | ``converse.listen.on('chatBoxToggled', function (event, chatbox) { ... });`` |
+---------------------------------+---------------------------------------------------------------------------------------------------+------------------------------------------------------------------------------------------------------+
+| **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) { ... });`` |
++---------------------------------+---------------------------------------------------------------------------------------------------+------------------------------------------------------------------------------------------------------+
+| **message** | When a message is received. | ``converse.listen.on('message', function (event, messageXML) { ... });`` |
++---------------------------------+---------------------------------------------------------------------------------------------------+------------------------------------------------------------------------------------------------------+
+| **messageSend** | When a message will be sent out. | ``storage_memoryconverse.listen.on('messageSend', function (event, messageText) { ... });`` |
++---------------------------------+---------------------------------------------------------------------------------------------------+------------------------------------------------------------------------------------------------------+
+| **noResumeableSession** | When keepalive=true but there aren't any stored prebind tokens. | ``converse.listen.on('noResumeableSession', function (event) { ... });`` |
++---------------------------------+---------------------------------------------------------------------------------------------------+------------------------------------------------------------------------------------------------------+
+| **initialized** | Once converse.js has been initialized. | ``converse.listen.on('initialized', function (event) { ... });`` |
++---------------------------------+---------------------------------------------------------------------------------------------------+------------------------------------------------------------------------------------------------------+
+| **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;
diff --git a/main.js b/main.js
index 0797a2f8f..a4d1bb1a5 100644
--- a/main.js
+++ b/main.js
@@ -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'] }
}
});
diff --git a/spec/chatbox.js b/spec/chatbox.js
index db9a30ef9..e2d5f60fa 100644
--- a/spec/chatbox.js
+++ b/spec/chatbox.js
@@ -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
diff --git a/spec/converse.js b/spec/converse.js
index 093c130f0..7b4dcfba7 100644
--- a/spec/converse.js
+++ b/spec/converse.js
@@ -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();
diff --git a/spec/disco.js b/spec/disco.js
new file mode 100644
index 000000000..c26d997d3
--- /dev/null
+++ b/spec/disco.js
@@ -0,0 +1,25 @@
+(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));
+}));
diff --git a/spec/mam.js b/spec/mam.js
new file mode 100644
index 000000000..8ac6d9315
--- /dev/null
+++ b/spec/mam.js
@@ -0,0 +1,448 @@
+(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(
+ "");
+ });
+
+ 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(
+ ""+
+ ""+
+ ""+
+ ""+
+ "urn:xmpp:mam:0"+
+ ""+
+ ""+
+ "juliet@capulet.lit"+
+ ""+
+ ""+
+ ""+
+ ""
+ );
+ });
+
+ 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(
+ ""+
+ ""+
+ ""+
+ ""+
+ "urn:xmpp:mam:0"+
+ ""+
+ ""+
+ ""+moment(start).format()+""+
+ ""+
+ ""+
+ ""+moment(end).format()+""+
+ ""+
+ ""+
+ ""+
+ ""
+ );
+ });
+
+ 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(
+ ""+
+ ""+
+ ""+
+ ""+
+ "urn:xmpp:mam:0"+
+ ""+
+ ""+
+ ""+moment(start).format()+""+
+ ""+
+ ""+
+ ""+
+ ""
+ );
+ });
+
+ 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(
+ ""+
+ ""+
+ ""+
+ ""+
+ "urn:xmpp:mam:0"+
+ ""+
+ ""+
+ ""+moment(start).format()+""+
+ ""+
+ ""+
+ ""+
+ "10"+
+ ""+
+ ""+
+ ""
+ );
+ });
+
+ 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(
+ ""+
+ ""+
+ ""+
+ ""+
+ "urn:xmpp:mam:0"+
+ ""+
+ ""+
+ ""+moment(start).format()+""+
+ ""+
+ ""+
+ ""+
+ "10"+
+ "09af3-cc343-b409f"+
+ ""+
+ ""+
+ ""
+ );
+ });
+
+ 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(
+ ""+
+ ""+
+ ""+
+ ""+
+ "urn:xmpp:mam:0"+
+ ""+
+ ""+
+ ""+
+ "10"+
+ ""+
+ ""+
+ ""+
+ ""
+ );
+ });
+
+ 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(
+ ""+
+ ""+
+ ""+
+ ""+
+ "urn:xmpp:mam:0"+
+ ""+
+ ""+
+ "romeo@montague.lit"+
+ ""+
+ ""+
+ ""+moment(rsm.start).format()+""+
+ ""+
+ ""+
+ ""+
+ "10"+
+ ""+
+ ""+
+ ""
+ );
+ });
+
+ 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));
+
+ /*
+ *
+ *
+ *
+ *
+ * Call me but love, and I'll be new baptized; Henceforth I never will be Romeo.
+ *
+ *
+ *
+ *
+ */
+ 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 message to indicate the end of the result set.
+ *
+ *
+ *
+ *
+ * 23452-4534-1
+ * 390-2342-22
+ * 16
+ *
+ *
+ *
+ */
+ 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(
+ ""+
+ ""+
+ ""
+ );
+
+ converse.message_archiving = 'never';
+ /* Example 15. Server responds with current preferences
+ *
+ *
+ *
+ *
+ *
+ *
+ *
+ */
+ 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(
+ ""+
+ ""+
+ "romeo@montague.lit"+
+ "montague@montague.lit"+
+ ""+
+ ""
+ );
+
+ expect(feature.get('preference')).toBe(undefined);
+ /*
+ *
+ *
+ * romeo@montague.lit
+ *
+ *
+ * montague@montague.lit
+ *
+ *
+ *
+ */
+ 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));
+}));
diff --git a/src/deps-full.js b/src/deps-full.js
index 95f60af01..df4c4a237 100644
--- a/src/deps-full.js
+++ b/src/deps-full.js
@@ -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",
diff --git a/src/deps-no-otr.js b/src/deps-no-otr.js
index 325bfbde5..acab8b5d8 100644
--- a/src/deps-no-otr.js
+++ b/src/deps-no-otr.js
@@ -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",
diff --git a/src/deps-website-no-otr.js b/src/deps-website-no-otr.js
index fbe3450c3..c9002c8d7 100644
--- a/src/deps-website-no-otr.js
+++ b/src/deps-website-no-otr.js
@@ -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",
diff --git a/src/deps-website.js b/src/deps-website.js
index 20cf17907..958429cb8 100644
--- a/src/deps-website.js
+++ b/src/deps-website.js
@@ -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",
diff --git a/src/templates/action.html b/src/templates/action.html
index b2e9e7904..107d28c01 100644
--- a/src/templates/action.html
+++ b/src/templates/action.html
@@ -1,4 +1,4 @@
-