// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
* This module updates the UI for the conversation page in the message
* drawer.
*
* The module will take a patch from the message_drawer_view_conversation_patcher
* module and update the UI to reflect the changes.
*
* This is the only module that ever modifies the UI of the conversation page.
*
* @module core_message/message_drawer_view_conversation_renderer
* @copyright 2018 Ryan Wyllie <ryan@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
define(
[
'jquery',
'core/notification',
'core/str',
'core/templates',
'core/user_date',
'core_message/message_drawer_view_conversation_constants',
'core/aria',
],
function(
$,
Notification,
Str,
Templates,
UserDate,
Constants,
Aria
) {
var SELECTORS = Constants.SELECTORS;
var TEMPLATES = Constants.TEMPLATES;
var CONVERSATION_TYPES = Constants.CONVERSATION_TYPES;
/**
* Get the messages container element.
*
* @param {Object} body Conversation body container element.
* @return {Object} The messages container element.
*/
var getMessagesContainer = function(body) {
return body.find(SELECTORS.CONTENT_MESSAGES_CONTAINER);
};
/**
* Show the messages container element.
*
* @param {Object} body Conversation body container element.
*/
var showMessagesContainer = function(body) {
getMessagesContainer(body).removeClass('hidden');
};
/**
* Hide the messages container element.
*
* @param {Object} body Conversation body container element.
*/
var hideMessagesContainer = function(body) {
getMessagesContainer(body).addClass('hidden');
};
/**
* Get the self-conversation message container element.
*
* @param {Object} body Conversation body container element.
* @return {Object} The messages container element.
*/
var getSelfConversationMessageContainer = function(body) {
return body.find(SELECTORS.SELF_CONVERSATION_MESSAGE_CONTAINER);
};
/**
* Hide the self-conversation message container element.
*
* @param {Object} body Conversation body container element.
* @return {Object} The messages container element.
*/
var hideSelfConversationMessageContainer = function(body) {
return getSelfConversationMessageContainer(body).addClass('hidden');
};
/**
* Get the contact request sent container element.
*
* @param {Object} body Conversation body container element.
* @return {Object} The messages container element.
*/
var getContactRequestSentContainer = function(body) {
return body.find(SELECTORS.CONTACT_REQUEST_SENT_MESSAGE_CONTAINER);
};
/**
* Hide the contact request sent container element.
*
* @param {Object} body Conversation body container element.
* @return {Object} The messages container element.
*/
var hideContactRequestSentContainer = function(body) {
return getContactRequestSentContainer(body).addClass('hidden');
};
/**
* Get the footer container element.
*
* @param {Object} footer Conversation footer container element.
* @return {Object} The footer container element.
*/
var getFooterContentContainer = function(footer) {
return footer.find(SELECTORS.CONTENT_MESSAGES_FOOTER_CONTAINER);
};
/**
* Show the footer container element.
*
* @param {Object} footer Conversation footer container element.
*/
var showFooterContent = function(footer) {
getFooterContentContainer(footer).removeClass('hidden');
};
/**
* Hide the footer container element.
*
* @param {Object} footer Conversation footer container element.
*/
var hideFooterContent = function(footer) {
getFooterContentContainer(footer).addClass('hidden');
};
/**
* Get the footer edit mode container element.
*
* @param {Object} footer Conversation footer container element.
* @return {Object} The footer container element.
*/
var getFooterEditModeContainer = function(footer) {
return footer.find(SELECTORS.CONTENT_MESSAGES_FOOTER_EDIT_MODE_CONTAINER);
};
/**
* Show the footer edit mode container element.
*
* @param {Object} footer Conversation footer container element.
*/
var showFooterEditMode = function(footer) {
getFooterEditModeContainer(footer).removeClass('hidden');
};
/**
* Hide the footer edit mode container element.
*
* @param {Object} footer Conversation footer container element.
*/
var hideFooterEditMode = function(footer) {
getFooterEditModeContainer(footer).addClass('hidden');
};
/**
* Get the footer placeholder.
*
* @param {Object} footer Conversation footer container element.
* @return {Object} The footer placeholder container element.
*/
var getFooterPlaceholderContainer = function(footer) {
return footer.find(SELECTORS.PLACEHOLDER_CONTAINER);
};
/**
* Show the footer placeholder
*
* @param {Object} footer Conversation footer container element.
*/
var showFooterPlaceholder = function(footer) {
getFooterPlaceholderContainer(footer).removeClass('hidden');
};
/**
* Hide the footer placeholder
*
* @param {Object} footer Conversation footer container element.
*/
var hideFooterPlaceholder = function(footer) {
getFooterPlaceholderContainer(footer).addClass('hidden');
};
/**
* Get the footer Require add as contact container element.
*
* @param {Object} footer Conversation footer container element.
* @return {Object} The footer Require add as contact container element.
*/
var getFooterRequireContactContainer = function(footer) {
return footer.find(SELECTORS.CONTENT_MESSAGES_FOOTER_REQUIRE_CONTACT_CONTAINER);
};
/**
* Show the footer add as contact dialogue.
*
* @param {Object} footer Conversation footer container element.
*/
var showFooterRequireContact = function(footer) {
getFooterRequireContactContainer(footer).removeClass('hidden');
};
/**
* Hide the footer add as contact dialogue.
*
* @param {Object} footer Conversation footer container element.
*/
var hideFooterRequireContact = function(footer) {
getFooterRequireContactContainer(footer).addClass('hidden');
};
/**
* Get the footer Required to unblock contact container element.
*
* @param {Object} footer Conversation footer container element.
* @return {Object} The footer Required to unblock contact container element.
*/
var getFooterRequireUnblockContainer = function(footer) {
return footer.find(SELECTORS.CONTENT_MESSAGES_FOOTER_REQUIRE_UNBLOCK_CONTAINER);
};
/**
* Show the footer Required to unblock contact container element.
*
* @param {Object} footer Conversation footer container element.
*/
var showFooterRequireUnblock = function(footer) {
getFooterRequireUnblockContainer(footer).removeClass('hidden');
};
/**
* Hide the footer Required to unblock contact container element.
*
* @param {Object} footer Conversation footer container element.
*/
var hideFooterRequireUnblock = function(footer) {
getFooterRequireUnblockContainer(footer).addClass('hidden');
};
/**
* Get the footer Unable to message contact container element.
*
* @param {Object} footer Conversation footer container element.
* @return {Object} The footer Unable to message contact container element.
*/
var getFooterUnableToMessageContainer = function(footer) {
return footer.find(SELECTORS.CONTENT_MESSAGES_FOOTER_UNABLE_TO_MESSAGE_CONTAINER);
};
/**
* Show the footer Unable to message contact container element.
*
* @param {Object} footer Conversation footer container element.
*/
var showFooterUnableToMessage = function(footer) {
getFooterUnableToMessageContainer(footer).removeClass('hidden');
};
/**
* Hide the footer Unable to message contact container element.
*
* @param {Object} footer Conversation footer container element.
*/
var hideFooterUnableToMessage = function(footer) {
getFooterUnableToMessageContainer(footer).addClass('hidden');
};
/**
* Hide all header elements.
*
* @param {Object} header Conversation header container element.
*/
var hideAllHeaderElements = function(header) {
hideHeaderContent(header);
hideHeaderEditMode(header);
hideHeaderPlaceholder(header);
};
/**
* Hide all footer dialogues and messages.
*
* @param {Object} footer Conversation footer container element.
*/
var hideAllFooterElements = function(footer) {
hideFooterContent(footer);
hideFooterEditMode(footer);
hideFooterPlaceholder(footer);
hideFooterRequireContact(footer);
hideFooterRequireUnblock(footer);
hideFooterUnableToMessage(footer);
};
/**
* Get the content placeholder container element.
*
* @param {Object} body Conversation body container element.
* @return {Object} The body placeholder container element.
*/
var getContentPlaceholderContainer = function(body) {
return body.find(SELECTORS.CONTENT_PLACEHOLDER_CONTAINER);
};
/**
* Show the content placeholder.
*
* @param {Object} body Conversation body container element.
*/
var showContentPlaceholder = function(body) {
getContentPlaceholderContainer(body).removeClass('hidden');
};
/**
* Hide the content placeholder.
*
* @param {Object} body Conversation body container element.
*/
var hideContentPlaceholder = function(body) {
getContentPlaceholderContainer(body).addClass('hidden');
};
/**
* Get the header content container element.
*
* @param {Object} header Conversation header container element.
* @return {Object} The header content container element.
*/
var getHeaderContent = function(header) {
return header.find(SELECTORS.HEADER);
};
/**
* Show the header content.
*
* @param {Object} header Conversation header container element.
*/
var showHeaderContent = function(header) {
getHeaderContent(header).removeClass('hidden');
};
/**
* Hide the header content.
*
* @param {Object} header Conversation header container element.
*/
var hideHeaderContent = function(header) {
getHeaderContent(header).addClass('hidden');
};
/**
* Get the header edit mode container element.
*
* @param {Object} header Conversation header container element.
* @return {Object} The header content container element.
*/
var getHeaderEditMode = function(header) {
return header.find(SELECTORS.HEADER_EDIT_MODE);
};
/**
* Show the header edit mode container.
*
* @param {Object} header Conversation header container element.
*/
var showHeaderEditMode = function(header) {
getHeaderEditMode(header).removeClass('hidden');
};
/**
* Hide the header edit mode container.
*
* @param {Object} header Conversation header container element.
*/
var hideHeaderEditMode = function(header) {
getHeaderEditMode(header).addClass('hidden');
};
/**
* Get the header placeholder container element.
*
* @param {Object} header Conversation header container element.
* @return {Object} The header placeholder container element.
*/
var getHeaderPlaceholderContainer = function(header) {
return header.find(SELECTORS.HEADER_PLACEHOLDER_CONTAINER);
};
/**
* Show the header placeholder.
*
* @param {Object} header Conversation header container element.
*/
var showHeaderPlaceholder = function(header) {
getHeaderPlaceholderContainer(header).removeClass('hidden');
};
/**
* Hide the header placeholder.
*
* @param {Object} header Conversation header container element.
*/
var hideHeaderPlaceholder = function(header) {
getHeaderPlaceholderContainer(header).addClass('hidden');
};
/**
* Get the emoji picker container element.
*
* @param {Object} footer Conversation footer container element.
* @return {Object} The emoji picker container element.
*/
var getEmojiPickerContainer = function(footer) {
return footer.find(SELECTORS.EMOJI_PICKER_CONTAINER);
};
/**
* Get the emoji picker container element.
*
* @param {Object} footer Conversation footer container element.
* @return {Object} The emoji picker container element.
*/
var getEmojiAutoCompleteContainer = function(footer) {
return footer.find(SELECTORS.EMOJI_AUTO_COMPLETE_CONTAINER);
};
/**
* Get a message element.
*
* @param {Object} body Conversation body container element.
* @param {Number} messageId the Message id.
* @return {Object} A message element from the conversation.
*/
var getMessageElement = function(body, messageId) {
var messagesContainer = getMessagesContainer(body);
return messagesContainer.find('[data-message-id="' + messageId + '"]');
};
/**
* Get the day container element. The day container element holds a list of messages for that day.
*
* @param {Object} body Conversation body container element.
* @param {Number} dayTimeCreated Midnight timestamp for the day.
* @return {Object} jQuery object
*/
var getDayElement = function(body, dayTimeCreated) {
var messagesContainer = getMessagesContainer(body);
return messagesContainer.find('[data-day-id="' + dayTimeCreated + '"]');
};
/**
* Get the more messages loading icon container element.
*
* @param {Object} body Conversation body container element.
* @return {Object} The more messages loading container element.
*/
var getMoreMessagesLoadingIconContainer = function(body) {
return body.find(SELECTORS.MORE_MESSAGES_LOADING_ICON_CONTAINER);
};
/**
* Show the more messages loading icon.
*
* @param {Object} body Conversation body container element.
*/
var showMoreMessagesLoadingIcon = function(body) {
getMoreMessagesLoadingIconContainer(body).removeClass('hidden');
};
/**
* Hide the more messages loading icon.
*
* @param {Object} body Conversation body container element.
*/
var hideMoreMessagesLoadingIcon = function(body) {
getMoreMessagesLoadingIconContainer(body).addClass('hidden');
};
/**
* Get the confirm dialogue container element.
*
* @param {Object} root The container element to search.
* @return {Object} The confirm dialogue container element.
*/
var getConfirmDialogueContainer = function(root) {
return root.find(SELECTORS.CONFIRM_DIALOGUE_CONTAINER);
};
/**
* Show the confirm dialogue container element.
*
* @param {Object} root The container element containing a dialogue.
*/
var showConfirmDialogueContainer = function(root) {
var container = getConfirmDialogueContainer(root);
var siblings = container.siblings(':not(.hidden)');
Aria.hide(siblings.get());
siblings.attr('data-confirm-dialogue-hidden', true);
container.removeClass('hidden');
};
/**
* Hide the confirm dialogue container element.
*
* @param {Object} root The container element containing a dialogue.
*/
var hideConfirmDialogueContainer = function(root) {
var container = getConfirmDialogueContainer(root);
var siblings = container.siblings('[data-confirm-dialogue-hidden="true"]');
Aria.unhide(siblings.get());
siblings.removeAttr('data-confirm-dialogue-hidden');
container.addClass('hidden');
};
/**
* Set the number of selected messages.
*
* @param {Object} header The header container element.
* @param {Number} value The new number to display.
*/
var setMessagesSelectedCount = function(header, value) {
getHeaderEditMode(header).find(SELECTORS.MESSAGES_SELECTED_COUNT).text(value);
};
/**
* Format message for the mustache template, transform camelCase properties to lowercase properties.
*
* @param {Array} messages Array of message objects.
* @param {Object} datesCache Cache timestamps and their formatted date string.
* @return {Array} Messages formated for mustache template.
*/
var formatMessagesForTemplate = function(messages, datesCache) {
return messages.map(function(message) {
return {
id: message.id,
isread: message.isRead,
fromloggedinuser: message.fromLoggedInUser,
userfrom: message.userFrom,
text: message.text,
formattedtime: message.timeCreated ? datesCache[message.timeCreated] : null
};
});
};
/**
* Create rendering promises for each day containing messages.
*
* @param {Object} header The header container element.
* @param {Object} body The body container element.
* @param {Object} footer The footer container element.
* @param {Array} days Array of days containing messages.
* @param {Object} datesCache Cache timestamps and their formatted date string.
* @return {Promise} Days rendering promises.
*/
var renderAddDays = function(header, body, footer, days, datesCache) {
var messagesContainer = getMessagesContainer(body);
var daysRenderPromises = days.map(function(data) {
var timestampDate = new Date(data.value.timestamp * 1000);
return Templates.render(TEMPLATES.DAY, {
timestamp: data.value.timestamp,
currentyear: timestampDate.getFullYear() === (new Date()).getFullYear(),
messages: formatMessagesForTemplate(data.value.messages, datesCache)
});
});
return $.when.apply($, daysRenderPromises).then(function() {
// Wait until all of the rendering is done for each of the days
// to ensure they are added to the page in the correct order.
days.forEach(function(data, index) {
daysRenderPromises[index]
.then(function(html) {
if (data.before) {
var element = getDayElement(body, data.before.timestamp);
return $(html).insertBefore(element);
} else {
return messagesContainer.append(html);
}
})
.catch(function() {
// Fail silently.
});
});
return;
});
};
/**
* Add (more) messages to day containers.
*
* @param {Object} header The header container element.
* @param {Object} body The body container element.
* @param {Object} footer The footer container element.
* @param {Array} messages List of messages.
* @param {Object} datesCache Cache timestamps and their formatted date string.
* @return {Promise} Messages rendering promises.
*/
var renderAddMessages = function(header, body, footer, messages, datesCache) {
var messagesData = messages.map(function(data) {
return data.value;
});
var formattedMessages = formatMessagesForTemplate(messagesData, datesCache);
return Templates.render(TEMPLATES.MESSAGES, {messages: formattedMessages})
.then(function(html) {
var messageList = $(html);
messages.forEach(function(data) {
var messageHtml = messageList.find('[data-message-id="' + data.value.id + '"]');
if (data.before) {
var element = getMessageElement(body, data.before.id);
return messageHtml.insertBefore(element);
} else {
var dayContainer = getDayElement(body, data.day.timestamp);
var dayMessagesContainer = dayContainer.find(SELECTORS.DAY_MESSAGES_CONTAINER);
return dayMessagesContainer.append(messageHtml);
}
});
return;
});
};
/**
* Update existing messages.
*
* @param {Object} header The header container element.
* @param {Object} body The body container element.
* @param {Object} footer The footer container element.
* @param {Array} messages List of messages.
* @param {Object} datesCache Cache timestamps and their formatted date string.
*/
var renderUpdateMessages = function(header, body, footer, messages, datesCache) {
messages.forEach(function(message) {
var before = message.before;
var after = message.after;
var element = getMessageElement(body, before.id);
if (before.id != after.id) {
element.attr('data-message-id', after.id);
}
if (before.timeCreated != after.timeCreated) {
var formattedTime = datesCache[after.timeCreated];
element.find(SELECTORS.LOADING_ICON_CONTAINER).addClass('hidden');
element.find(SELECTORS.TIME_CREATED).text(formattedTime).removeClass('hidden');
}
if (before.sendState != after.sendState) {
var loading = element.find(SELECTORS.LOADING_ICON_CONTAINER);
var time = element.find(SELECTORS.TIME_CREATED);
var retry = element.find(SELECTORS.RETRY_SEND);
loading.addClass('hidden');
Aria.hide(loading.get());
time.addClass('hidden');
Aria.hide(time.get());
retry.addClass('hidden');
Aria.hide(retry.get());
element.removeClass('border border-danger');
switch (after.sendState) {
case 'pending':
loading.removeClass('hidden');
Aria.unhide(loading.get());
break;
case 'error':
retry.removeClass('hidden');
Aria.unhide(retry.get());
element.addClass('border border-danger');
break;
case 'sent':
time.removeClass('hidden');
Aria.unhide(time.get());
break;
}
}
if (before.text != after.text) {
element.find(SELECTORS.TEXT_CONTAINER).html(after.text);
}
if (before.errorMessage != after.errorMessage) {
var messageContainer = element.find(SELECTORS.ERROR_MESSAGE_CONTAINER);
var message = messageContainer.find(SELECTORS.ERROR_MESSAGE);
if (after.errorMessage) {
messageContainer.removeClass('hidden');
Aria.unhide(messageContainer.get());
message.text(after.errorMessage);
} else {
messageContainer.addClass('hidden');
Aria.unhide(messageContainer.get());
message.text('');
}
}
});
};
/**
* Remove days from conversation.
*
* @param {Object} body The body container element.
* @param {Array} days Array of days to be removed.
*/
var renderRemoveDays = function(body, days) {
days.forEach(function(data) {
getDayElement(body, data.timestamp).remove();
});
};
/**
* Remove messages from conversation.
*
* @param {Object} body The body container element.
* @param {Array} messages Array of messages to be removed.
*/
var renderRemoveMessages = function(body, messages) {
messages.forEach(function(data) {
getMessageElement(body, data.id).remove();
});
};
/**
* Render the full conversation base on input from the statemanager.
*
* This will pre-load all of the formatted timestamps for each message that
* needs to render to reduce the number of networks requests.
*
* @param {Object} header The header container element.
* @param {Object} body The body container element.
* @param {Object} footer The footer container element.
* @param {Object} data The conversation diff.
* @return {Object} jQuery promise.
*/
var renderConversation = function(header, body, footer, data) {
var renderingPromises = [];
var hasAddDays = data.days.add.length > 0;
var hasAddMessages = data.messages.add.length > 0;
var hasUpdateMessages = data.messages.update.length > 0;
var timestampsToFormat = [];
var datesCachePromise = $.Deferred().resolve({}).promise();
if (hasAddDays) {
// Search for all of the timeCreated values in all of the messages in all of
// the days that we need to render.
timestampsToFormat = timestampsToFormat.concat(data.days.add.reduce(function(carry, day) {
return carry.concat(day.value.messages.reduce(function(timestamps, message) {
if (message.timeCreated) {
timestamps.push(message.timeCreated);
}
return timestamps;
}, []));
}, []));
}
if (hasAddMessages) {
// Search for all of the timeCreated values in all of the messages that we
// need to render.
timestampsToFormat = timestampsToFormat.concat(data.messages.add.reduce(function(timestamps, message) {
if (message.value.timeCreated) {
timestamps.push(message.value.timeCreated);
}
return timestamps;
}, []));
}
if (hasUpdateMessages) {
timestampsToFormat = timestampsToFormat.concat(data.messages.update.reduce(function(timestamps, message) {
if (message.before.timeCreated != message.after.timeCreated) {
timestamps.push(message.after.timeCreated);
}
return timestamps;
}, []));
}
if (timestampsToFormat.length) {
// If we have timestamps then pre-load the formatted version of each of them
// in a single request to the server. This saves the templates doing multiple
// individual requests.
datesCachePromise = Str.get_string('strftimetime24', 'core_langconfig')
.then(function(format) {
var requests = timestampsToFormat.map(function(timestamp) {
return {
timestamp: timestamp,
format: format
};
});
return UserDate.get(requests);
})
.then(function(formattedTimes) {
return timestampsToFormat.reduce(function(carry, timestamp, index) {
carry[timestamp] = formattedTimes[index];
return carry;
}, {});
});
}
if (hasAddDays) {
renderingPromises.push(datesCachePromise.then(function(datesCache) {
return renderAddDays(header, body, footer, data.days.add, datesCache);
}));
}
if (hasAddMessages) {
renderingPromises.push(datesCachePromise.then(function(datesCache) {
return renderAddMessages(header, body, footer, data.messages.add, datesCache);
}));
}
if (hasUpdateMessages) {
renderingPromises.push(datesCachePromise.then(function(datesCache) {
return renderUpdateMessages(header, body, footer, data.messages.update, datesCache);
}));
}
if (data.days.remove.length > 0) {
renderRemoveDays(body, data.days.remove);
}
if (data.messages.remove.length > 0) {
renderRemoveMessages(body, data.messages.remove);
}
return $.when.apply($, renderingPromises);
};
/**
* Render the conversation header.
*
* @param {Object} header The header container element.
* @param {Object} body The body container element.
* @param {Object} footer The footer container element.
* @param {Object} data Data for header.
* @return {Object} jQuery promise
*/
var renderHeader = function(header, body, footer, data) {
var headerContainer = getHeaderContent(header);
var template = TEMPLATES.HEADER_PUBLIC;
data.context.showrouteback = (header.attr('data-from-panel') === "false");
if (data.type == CONVERSATION_TYPES.PRIVATE) {
template = data.showControls ? TEMPLATES.HEADER_PRIVATE : TEMPLATES.HEADER_PRIVATE_NO_CONTROLS;
} else if (data.type == CONVERSATION_TYPES.SELF) {
template = TEMPLATES.HEADER_SELF;
}
return Templates.render(template, data.context)
.then(function(html, js) {
Templates.replaceNodeContents(headerContainer, html, js);
return;
});
};
/**
* Render the conversation footer.
*
* @param {Object} header The header container element.
* @param {Object} body The body container element.
* @param {Object} footer The footer container element.
* @param {Object} data Data for footer.
* @return {Object} jQuery promise.
*/
var renderFooter = function(header, body, footer, data) {
hideAllFooterElements(footer);
switch (data.type) {
case 'placeholder':
return showFooterPlaceholder(footer);
case 'add-contact':
return Str.get_strings([
{
key: 'requirecontacttomessage',
component: 'core_message',
param: data.user.fullname
},
{
key: 'isnotinyourcontacts',
component: 'core_message',
param: data.user.fullname
}
])
.then(function(strings) {
var title = strings[1];
var text = strings[0];
var footerContainer = getFooterRequireContactContainer(footer);
footerContainer.find(SELECTORS.TITLE).text(title);
footerContainer.find(SELECTORS.TEXT).text(text);
showFooterRequireContact(footer);
return strings;
});
case 'edit-mode':
return showFooterEditMode(footer);
case 'content':
return showFooterContent(footer);
case 'unblock':
return showFooterRequireUnblock(footer);
case 'unable-to-message':
return showFooterUnableToMessage(footer);
}
return true;
};
/**
* Scroll to a message in the conversation.
*
* @param {Object} header The header container element.
* @param {Object} body The body container element.
* @param {Object} footer The footer container element.
* @param {Number} messageId Message id.
*/
var renderScrollToMessage = function(header, body, footer, messageId) {
var messagesContainer = getMessagesContainer(body);
var messageElement = getMessageElement(body, messageId);
var position = messageElement.position();
// Scroll the message container down to the top of the message element.
if (position) {
var scrollTop = messagesContainer.scrollTop() + position.top;
messagesContainer.scrollTop(scrollTop);
}
};
/**
* Hide or show the conversation header.
*
* @param {Object} header The header container element.
* @param {Object} body The body container element.
* @param {Object} footer The footer container element.
* @param {Bool} isLoadingMembers Members loading.
*/
var renderLoadingMembers = function(header, body, footer, isLoadingMembers) {
if (isLoadingMembers) {
hideHeaderContent(header);
showHeaderPlaceholder(header);
} else {
showHeaderContent(header);
hideHeaderPlaceholder(header);
}
};
/**
* Hide or show loading conversation messages.
*
* @param {Object} header The header container element.
* @param {Object} body The body container element.
* @param {Object} footer The footer container element.
* @param {Bool} isLoadingFirstMessages Messages loading.
*/
var renderLoadingFirstMessages = function(header, body, footer, isLoadingFirstMessages) {
if (isLoadingFirstMessages) {
hideMessagesContainer(body);
showContentPlaceholder(body);
} else {
showMessagesContainer(body);
hideContentPlaceholder(body);
}
};
/**
* Hide or show loading more messages.
*
* @param {Object} header The header container element.
* @param {Object} body The body container element.
* @param {Object} footer The footer container element.
* @param {Bool} isLoading Messages loading.
*/
var renderLoadingMessages = function(header, body, footer, isLoading) {
if (isLoading) {
showMoreMessagesLoadingIcon(body);
} else {
hideMoreMessagesLoadingIcon(body);
}
};
/**
* Hide or show the emoji picker.
*
* @param {Object} header The header container element.
* @param {Object} body The body container element.
* @param {Object} footer The footer container element.
* @param {Bool} show Should the emoji picker be visible.
*/
var renderShowEmojiPicker = function(header, body, footer, show) {
var container = getEmojiPickerContainer(footer);
if (show) {
container.removeClass('hidden');
Aria.unhide(container.get());
container.find(SELECTORS.EMOJI_PICKER_SEARCH_INPUT).focus();
} else {
container.addClass('hidden');
Aria.hide(container.get());
}
};
/**
* Hide or show the emoji auto complete.
*
* @param {Object} header The header container element.
* @param {Object} body The body container element.
* @param {Object} footer The footer container element.
* @param {Bool} show Should the emoji picker be visible.
*/
var renderShowEmojiAutoComplete = function(header, body, footer, show) {
var container = getEmojiAutoCompleteContainer(footer);
if (show) {
container.removeClass('hidden');
Aria.unhide(container.get());
} else {
container.addClass('hidden');
Aria.hide(container.get());
}
};
/**
* Show a confirmation dialogue
*
* @param {Object} header The header container element.
* @param {Object} body The body container element.
* @param {Object} footer The footer container element.
* @param {String} buttonSelectors Selectors for the buttons to show.
* @param {String} bodyText Text to show in dialogue.
* @param {String} headerText Text to show in dialogue header.
* @param {Bool} canCancel Can this dialogue be cancelled.
* @param {Bool} skipHeader Skip blanking out the header
* @param {Bool} showOk Show an 'Okay' button for a dialogue which will close it
*/
var showConfirmDialogue = function(
header,
body,
footer,
buttonSelectors,
bodyText,
headerText,
canCancel,
skipHeader,
showOk
) {
var dialogue = getConfirmDialogueContainer(body);
var buttons = buttonSelectors.map(function(selector) {
return dialogue.find(selector);
});
var cancelButton = dialogue.find(SELECTORS.CONFIRM_DIALOGUE_CANCEL_BUTTON);
var okayButton = dialogue.find(SELECTORS.CONFIRM_DIALOGUE_OKAY_BUTTON);
var text = dialogue.find(SELECTORS.CONFIRM_DIALOGUE_TEXT);
var dialogueHeader = dialogue.find(SELECTORS.CONFIRM_DIALOGUE_HEADER);
dialogue.find('button').addClass('hidden');
if (canCancel) {
cancelButton.removeClass('hidden');
} else {
cancelButton.addClass('hidden');
}
if (showOk) {
okayButton.removeClass('hidden');
} else {
okayButton.addClass('hidden');
}
if (headerText) {
// Create the dialogue header.
dialogueHeader = $('<h3 class="h6" data-region="dialogue-header"></h3>');
dialogueHeader.text(headerText);
// Prepend it to the confirmation body.
var confirmDialogue = dialogue.find(SELECTORS.CONFIRM_DIALOGUE);
confirmDialogue.prepend(dialogueHeader);
} else if (dialogueHeader.length) {
// Header text is empty but dialogue header is present, so remove it.
dialogueHeader.remove();
}
buttons.forEach(function(button) {
button.removeClass('hidden');
});
text.text(bodyText);
showConfirmDialogueContainer(footer);
showConfirmDialogueContainer(body);
if (!skipHeader) {
showConfirmDialogueContainer(header);
}
dialogue.find(SELECTORS.CAN_RECEIVE_FOCUS).filter(':visible').first().focus();
};
/**
* Hide the dialogue
*
* @param {Object} header The header container element.
* @param {Object} body The body container element.
* @param {Object} footer The footer container element.
* @return {Bool} always true.
*/
var hideConfirmDialogue = function(header, body, footer) {
var dialogue = getConfirmDialogueContainer(body);
var cancelButton = dialogue.find(SELECTORS.CONFIRM_DIALOGUE_CANCEL_BUTTON);
var okayButton = dialogue.find(SELECTORS.CONFIRM_DIALOGUE_OKAY_BUTTON);
var text = dialogue.find(SELECTORS.CONFIRM_DIALOGUE_TEXT);
var dialogueHeader = dialogue.find(SELECTORS.CONFIRM_DIALOGUE_HEADER);
hideCheckDeleteDialogue(body);
hideConfirmDialogueContainer(body);
hideConfirmDialogueContainer(footer);
hideConfirmDialogueContainer(header);
dialogue.find('button').addClass('hidden');
cancelButton.removeClass('hidden');
okayButton.removeClass('hidden');
text.text('');
// Remove dialogue header if present.
if (dialogueHeader.length) {
dialogueHeader.remove();
}
header.find(SELECTORS.CAN_RECEIVE_FOCUS).first().focus();
return true;
};
/**
* Render the confirm block user dialogue.
*
* @param {Object} header The header container element.
* @param {Object} body The body container element.
* @param {Object} footer The footer container element.
* @param {Object} user User to block.
* @return {Object} jQuery promise
*/
var renderConfirmBlockUser = function(header, body, footer, user) {
if (user) {
if (user.canmessageevenifblocked) {
return Str.get_string('cantblockuser', 'core_message', user.fullname)
.then(function(string) {
return showConfirmDialogue(header, body, footer, [], string, '', false, false, true);
});
} else {
return Str.get_string('blockuserconfirm', 'core_message', user.fullname)
.then(function(string) {
return showConfirmDialogue(header, body, footer, [SELECTORS.ACTION_CONFIRM_BLOCK], string, '', true, false);
});
}
} else {
return hideConfirmDialogue(header, body, footer);
}
};
/**
* Render the confirm unblock user dialogue.
*
* @param {Object} header The header container element.
* @param {Object} body The body container element.
* @param {Object} footer The footer container element.
* @param {Object} user User to unblock.
* @return {Object} jQuery promise
*/
var renderConfirmUnblockUser = function(header, body, footer, user) {
if (user) {
return Str.get_string('unblockuserconfirm', 'core_message', user.fullname)
.then(function(string) {
return showConfirmDialogue(header, body, footer, [SELECTORS.ACTION_CONFIRM_UNBLOCK], string, '', true, false);
});
} else {
return hideConfirmDialogue(header, body, footer);
}
};
/**
* Render the add user as contact dialogue.
*
* @param {Object} header The header container element.
* @param {Object} body The body container element.
* @param {Object} footer The footer container element.
* @param {Object} user User to add as contact.
* @return {Object} jQuery promise
*/
var renderConfirmAddContact = function(header, body, footer, user) {
if (user) {
return Str.get_string('addcontactconfirm', 'core_message', user.fullname)
.then(function(string) {
return showConfirmDialogue(
header,
body,
footer,
[SELECTORS.ACTION_CONFIRM_ADD_CONTACT],
string,
'',
true,
false
);
});
} else {
return hideConfirmDialogue(header, body, footer);
}
};
/**
* Render the remove user from contacts dialogue.
*
* @param {Object} header The header container element.
* @param {Object} body The body container element.
* @param {Object} footer The footer container element.
* @param {Object} user User to remove from contacts.
* @return {Object} jQuery promise
*/
var renderConfirmRemoveContact = function(header, body, footer, user) {
if (user) {
return Str.get_string('removecontactconfirm', 'core_message', user.fullname)
.then(function(string) {
return showConfirmDialogue(
header,
body,
footer,
[SELECTORS.ACTION_CONFIRM_REMOVE_CONTACT],
string,
'',
true,
false
);
});
} else {
return hideConfirmDialogue(header, body, footer);
}
};
/**
* Render the delete selected messages dialogue.
*
* @param {Object} header The header container element.
* @param {Object} body The body container element.
* @param {Object} footer The footer container element.
* @param {Object} data If the dialogue should show and checkbox shows to delete message for all users.
* @return {Object} jQuery promise
*/
var renderConfirmDeleteSelectedMessages = function(header, body, footer, data) {
var showmessage = null;
if (data.type == CONVERSATION_TYPES.SELF) {
// Message displayed to self-conversations is slighly different.
showmessage = 'deleteselectedmessagesconfirmselfconversation';
} else {
// This other message should be displayed.
if (data.canDeleteMessagesForAllUsers) {
showCheckDeleteDialogue(body);
showmessage = 'deleteforeveryoneselectedmessagesconfirm';
} else {
showmessage = 'deleteselectedmessagesconfirm';
}
}
if (data.show) {
return Str.get_string(showmessage, 'core_message')
.then(function(string) {
return showConfirmDialogue(
header,
body,
footer,
[SELECTORS.ACTION_CONFIRM_DELETE_SELECTED_MESSAGES],
string,
'',
true,
false
);
});
} else {
return hideConfirmDialogue(header, body, footer);
}
};
/**
* Render the confirm delete conversation dialogue.
*
* @param {Object} header The header container element.
* @param {Object} body The body container element.
* @param {Object} footer The footer container element.
* @param {int|Null} type The conversation type to be removed.
* @return {Object} jQuery promise
*/
var renderConfirmDeleteConversation = function(header, body, footer, type) {
var showmessage = null;
if (type == CONVERSATION_TYPES.SELF) {
// Message displayed to self-conversations is slighly different.
showmessage = 'deleteallselfconfirm';
} else if (type) {
// This other message should be displayed.
showmessage = 'deleteallconfirm';
}
if (showmessage) {
return Str.get_string(showmessage, 'core_message')
.then(function(string) {
return showConfirmDialogue(
header,
body,
footer,
[SELECTORS.ACTION_CONFIRM_DELETE_CONVERSATION],
string,
'',
true,
false
);
});
} else {
return hideConfirmDialogue(header, body, footer);
}
};
/**
* Render the confirm delete conversation dialogue.
*
* @param {Object} header The header container element.
* @param {Object} body The body container element.
* @param {Object} footer The footer container element.
* @param {Bool} user The other user object.
* @return {Object} jQuery promise
*/
var renderConfirmContactRequest = function(header, body, footer, user) {
if (user) {
return Str.get_string('userwouldliketocontactyou', 'core_message', user.fullname)
.then(function(string) {
var buttonSelectors = [
SELECTORS.ACTION_ACCEPT_CONTACT_REQUEST,
SELECTORS.ACTION_DECLINE_CONTACT_REQUEST
];
return showConfirmDialogue(header, body, footer, buttonSelectors, string, '', false, true);
});
} else {
return hideConfirmDialogue(header, body, footer);
}
};
/**
* Show the checkbox to allow delete message for all.
*
* @param {Object} body The body container element.
*/
var showCheckDeleteDialogue = function(body) {
var dialogue = getConfirmDialogueContainer(body);
var checkboxRegion = dialogue.find(SELECTORS.DELETE_MESSAGES_FOR_ALL_USERS_TOGGLE_CONTAINER);
checkboxRegion.removeClass('hidden');
};
/**
* Hide the checkbox to allow delete message for all.
*
* @param {Object} body The body container element.
*/
var hideCheckDeleteDialogue = function(body) {
var dialogue = getConfirmDialogueContainer(body);
var checkboxRegion = dialogue.find(SELECTORS.DELETE_MESSAGES_FOR_ALL_USERS_TOGGLE_CONTAINER);
var checkbox = dialogue.find(SELECTORS.DELETE_MESSAGES_FOR_ALL_USERS_TOGGLE);
checkbox.prop('checked', false);
checkboxRegion.addClass('hidden');
};
/**
* Show or hide the block / unblock option in the header dropdown menu.
*
* @param {Object} header The header container element.
* @param {Object} body The body container element.
* @param {Object} footer The footer container element.
* @param {Bool} isBlocked is user blocked.
*/
var renderIsBlocked = function(header, body, footer, isBlocked) {
if (isBlocked) {
header.find(SELECTORS.ACTION_REQUEST_BLOCK).addClass('hidden');
header.find(SELECTORS.ACTION_REQUEST_UNBLOCK).removeClass('hidden');
} else {
header.find(SELECTORS.ACTION_REQUEST_BLOCK).removeClass('hidden');
header.find(SELECTORS.ACTION_REQUEST_UNBLOCK).addClass('hidden');
}
};
/**
* Show or hide the favourite / unfavourite option in the header dropdown menu
* and the favourite star in the header title.
*
* @param {Object} header The header container element.
* @param {Object} body The body container element.
* @param {Object} footer The footer container element.
* @param {Bool} state is this conversation a favourite.
*/
var renderIsFavourite = function(header, body, footer, state) {
var favouriteIcon = header.find(SELECTORS.FAVOURITE_ICON_CONTAINER);
var addFavourite = header.find(SELECTORS.ACTION_CONFIRM_FAVOURITE);
var removeFavourite = header.find(SELECTORS.ACTION_CONFIRM_UNFAVOURITE);
switch (state) {
case 'hide':
favouriteIcon.addClass('hidden');
addFavourite.addClass('hidden');
removeFavourite.addClass('hidden');
break;
case 'show-add':
favouriteIcon.addClass('hidden');
addFavourite.removeClass('hidden');
removeFavourite.addClass('hidden');
break;
case 'show-remove':
favouriteIcon.removeClass('hidden');
addFavourite.addClass('hidden');
removeFavourite.removeClass('hidden');
break;
}
};
/**
* Show or hide the mute / unmute option in the header dropdown menu
* and the muted icon in the header title.
*
* @param {Object} header The header container element.
* @param {Object} body The body container element.
* @param {Object} footer The footer container element.
* @param {string} state The state of the conversation as defined by the patcher.
*/
var renderIsMuted = function(header, body, footer, state) {
var muteIcon = header.find(SELECTORS.MUTED_ICON_CONTAINER);
var setMuted = header.find(SELECTORS.ACTION_CONFIRM_MUTE);
var unsetMuted = header.find(SELECTORS.ACTION_CONFIRM_UNMUTE);
switch (state) {
case 'hide':
muteIcon.addClass('hidden');
setMuted.addClass('hidden');
unsetMuted.addClass('hidden');
break;
case 'show-mute':
muteIcon.addClass('hidden');
setMuted.removeClass('hidden');
unsetMuted.addClass('hidden');
break;
case 'show-unmute':
muteIcon.removeClass('hidden');
setMuted.addClass('hidden');
unsetMuted.removeClass('hidden');
break;
}
};
/**
* Show or hide the add / remove user as contact option in the header dropdown menu.
*
* @param {Object} header The header container element.
* @param {Object} body The body container element.
* @param {Object} footer The footer container element.
* @param {Bool} state the contact state.
*/
var renderIsContact = function(header, body, footer, state) {
var addContact = header.find(SELECTORS.ACTION_REQUEST_ADD_CONTACT);
var removeContact = header.find(SELECTORS.ACTION_REQUEST_REMOVE_CONTACT);
switch (state) {
case 'pending-contact':
addContact.addClass('hidden');
removeContact.addClass('hidden');
break;
case 'contact':
addContact.addClass('hidden');
removeContact.removeClass('hidden');
break;
case 'non-contact':
addContact.removeClass('hidden');
removeContact.addClass('hidden');
break;
}
};
/**
* Show or hide confirm action from confirm dialogue is loading.
*
* @param {Object} header The header container element.
* @param {Object} body The body container element.
* @param {Object} footer The footer container element.
* @param {Bool} isLoading confirm action is loading.
*/
var renderLoadingConfirmAction = function(header, body, footer, isLoading) {
var dialogue = getConfirmDialogueContainer(body);
var buttons = dialogue.find('button');
var buttonText = dialogue.find(SELECTORS.CONFIRM_DIALOGUE_BUTTON_TEXT);
var loadingIcon = dialogue.find(SELECTORS.LOADING_ICON_CONTAINER);
if (isLoading) {
buttons.prop('disabled', true);
buttonText.addClass('hidden');
loadingIcon.removeClass('hidden');
} else {
buttons.prop('disabled', false);
buttonText.removeClass('hidden');
loadingIcon.addClass('hidden');
}
};
/**
* Show or hide the header and footer content for edit mode.
*
* @param {Object} header The header container element.
* @param {Object} body The body container element.
* @param {Object} footer The footer container element.
* @param {Bool} inEditMode In edit mode or not.
*/
var renderInEditMode = function(header, body, footer, inEditMode) {
var messages = null;
if (inEditMode) {
messages = body.find(SELECTORS.MESSAGE_NOT_SELECTED);
messages.find(SELECTORS.MESSAGE_NOT_SELECTED_ICON).removeClass('hidden');
hideHeaderContent(header);
showHeaderEditMode(header);
} else {
messages = getMessagesContainer(body);
messages.find(SELECTORS.MESSAGE_NOT_SELECTED_ICON).addClass('hidden');
messages.find(SELECTORS.MESSAGE_SELECTED_ICON).addClass('hidden');
showHeaderContent(header);
hideHeaderEditMode(header);
}
};
/**
* Select or unselect messages.
*
* @param {Object} header The header container element.
* @param {Object} body The body container element.
* @param {Object} footer The footer container element.
* @param {Object} data The messages to select or unselect.
*/
var renderSelectedMessages = function(header, body, footer, data) {
var hasSelectedMessages = data.count > 0;
if (data.add.length) {
data.add.forEach(function(messageId) {
var message = getMessageElement(body, messageId);
message.find(SELECTORS.MESSAGE_NOT_SELECTED_ICON).addClass('hidden');
message.find(SELECTORS.MESSAGE_SELECTED_ICON).removeClass('hidden');
message.attr('aria-checked', true);
});
}
if (data.remove.length) {
data.remove.forEach(function(messageId) {
var message = getMessageElement(body, messageId);
if (hasSelectedMessages) {
message.find(SELECTORS.MESSAGE_NOT_SELECTED_ICON).removeClass('hidden');
}
message.find(SELECTORS.MESSAGE_SELECTED_ICON).addClass('hidden');
message.attr('aria-checked', false);
});
}
setMessagesSelectedCount(header, data.count);
};
/**
* Show or hide the require add contact panel.
*
* @param {Object} header The header container element.
* @param {Object} body The body container element.
* @param {Object} footer The footer container element.
* @param {Object} data Whether the user has to be added a a contact.
* @return {Object} jQuery promise
*/
var renderRequireAddContact = function(header, body, footer, data) {
if (data.show && !data.hasMessages) {
return Str.get_strings([
{
key: 'requirecontacttomessage',
component: 'core_message',
param: data.user.fullname
},
{
key: 'isnotinyourcontacts',
component: 'core_message',
param: data.user.fullname
}
])
.then(function(strings) {
var title = strings[1];
var text = strings[0];
return showConfirmDialogue(
header,
body,
footer,
[SELECTORS.ACTION_REQUEST_ADD_CONTACT],
text,
title,
false,
true
);
});
} else {
return hideConfirmDialogue(header, body, footer);
}
};
/**
* Show or hide the self-conversation message.
*
* @param {Object} header The header container element.
* @param {Object} body The body container element.
* @param {Object} footer The footer container element.
* @param {Object} displayMessage should the message be displayed?.
* @return {Object|true} jQuery promise
*/
var renderSelfConversationMessage = function(header, body, footer, displayMessage) {
var container = getSelfConversationMessageContainer(body);
if (displayMessage) {
container.removeClass('hidden');
} else {
container.addClass('hidden');
}
return true;
};
/**
* Show or hide the require add contact panel.
*
* @param {Object} header The header container element.
* @param {Object} body The body container element.
* @param {Object} footer The footer container element.
* @param {Object} userFullName Full name of the other user.
* @return {Object|true} jQuery promise
*/
var renderContactRequestSent = function(header, body, footer, userFullName) {
var container = getContactRequestSentContainer(body);
if (userFullName) {
return Str.get_string('yourcontactrequestpending', 'core_message', userFullName)
.then(function(string) {
container.find(SELECTORS.TEXT).text(string);
container.removeClass('hidden');
return string;
});
} else {
container.addClass('hidden');
return true;
}
};
/**
* Reset the UI to the initial state.
*
* @param {Object} header The header container element.
* @param {Object} body The body container element.
* @param {Object} footer The footer container element.
* @return {Bool}
*/
var renderReset = function(header, body, footer) {
hideConfirmDialogue(header, body, footer);
hideContactRequestSentContainer(body);
hideSelfConversationMessageContainer(body);
hideAllHeaderElements(header);
showHeaderPlaceholder(header);
hideAllFooterElements(footer);
showFooterPlaceholder(footer);
return true;
};
var render = function(header, body, footer, patch) {
var configs = [
{
// Resetting the UI needs to come first, if it's required.
reset: renderReset
},
{
// Any async rendering (stuff that requires templates, strings etc) should
// go in here.
conversation: renderConversation,
header: renderHeader,
footer: renderFooter,
confirmBlockUser: renderConfirmBlockUser,
confirmUnblockUser: renderConfirmUnblockUser,
confirmAddContact: renderConfirmAddContact,
confirmRemoveContact: renderConfirmRemoveContact,
confirmDeleteSelectedMessages: renderConfirmDeleteSelectedMessages,
confirmDeleteConversation: renderConfirmDeleteConversation,
confirmContactRequest: renderConfirmContactRequest,
requireAddContact: renderRequireAddContact,
selfConversationMessage: renderSelfConversationMessage,
contactRequestSent: renderContactRequestSent
},
{
loadingMembers: renderLoadingMembers,
loadingFirstMessages: renderLoadingFirstMessages,
loadingMessages: renderLoadingMessages,
isBlocked: renderIsBlocked,
isContact: renderIsContact,
isFavourite: renderIsFavourite,
isMuted: renderIsMuted,
loadingConfirmAction: renderLoadingConfirmAction,
inEditMode: renderInEditMode,
showEmojiPicker: renderShowEmojiPicker,
showEmojiAutoComplete: renderShowEmojiAutoComplete,
},
{
// Scrolling should be last to make sure everything
// on the page is visible.
scrollToMessage: renderScrollToMessage,
selectedMessages: renderSelectedMessages
}
];
// Helper function to process each of the configs above.
var processConfig = function(config) {
var results = [];
for (var key in patch) {
if (config.hasOwnProperty(key)) {
var renderFunc = config[key];
var patchValue = patch[key];
results.push(renderFunc(header, body, footer, patchValue));
}
}
return results;
};
// The first config is special because it resets the UI.
var renderingPromises = processConfig(configs[0]);
// The second config is special because it contains async rendering.
renderingPromises = renderingPromises.concat(processConfig(configs[1]));
// Wait for the async rendering to complete before processing the
// rest of the configs, in order.
return $.when.apply($, renderingPromises)
.then(function() {
for (var i = 2; i < configs.length; i++) {
processConfig(configs[i]);
}
return;
})
.catch(Notification.exception);
};
return {
render: render,
};
});