Proyectos de Subversion Moodle

Rev

| Ultima modificación | Ver Log |

Rev Autor Línea Nro. Línea
1 efrain 1
{"version":3,"file":"message_drawer_view_conversation.min.js","sources":["../src/message_drawer_view_conversation.js"],"sourcesContent":["// This file is part of Moodle - http://moodle.org/\n//\n// Moodle is free software: you can redistribute it and/or modify\n// it under the terms of the GNU General Public License as published by\n// the Free Software Foundation, either version 3 of the License, or\n// (at your option) any later version.\n//\n// Moodle is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n// GNU General Public License for more details.\n//\n// You should have received a copy of the GNU General Public License\n// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.\n\n/**\n * Controls the conversation page in the message drawer.\n *\n * This function handles all of the user actions that the user can take\n * when interacting with the conversation page.\n *\n * It maintains a view state which is a data representation of the view\n * and only operates on that data.\n *\n * The view state is immutable and should never be modified directly. Instead\n * all changes to the view state should be done using the StateManager which\n * will generate a new version of the view state with the requested changes.\n *\n * After any changes to the view state the module will call the render function\n * to ask the renderer to update the UI.\n *\n * General rules for this module:\n * 1.) Never modify viewState directly. All changes should be via the StateManager.\n * 2.) Call render() with the new state when you want to update the UI\n * 3.) Never modify the UI directly in this module. This module is only concerned\n *     with the data in the view state.\n *\n * The general flow for a user interaction will be something like:\n * User interaction: User clicks \"confirm block\" button to block the other user\n *      1.) This module is hears the click\n *      2.) This module sends a request to the server to block the user\n *      3.) The server responds with the new user profile\n *      4.) This module generates a new state using the StateManager with the updated\n *          user profile.\n *      5.) This module asks the Patcher to generate a patch from the current state and\n *          the newly generated state. This patch tells the renderer what has changed\n *          between the states.\n *      6.) This module gives the Renderer the generated patch. The renderer updates\n *          the UI with changes according to the patch.\n *\n * @module     core_message/message_drawer_view_conversation\n * @copyright  2018 Ryan Wyllie <ryan@moodle.com>\n * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\ndefine(\n[\n    'jquery',\n    'core/auto_rows',\n    'core/backoff_timer',\n    'core/custom_interaction_events',\n    'core/notification',\n    'core/pending',\n    'core/pubsub',\n    'core/str',\n    'core_message/message_repository',\n    'core_message/message_drawer_events',\n    'core_message/message_drawer_view_conversation_constants',\n    'core_message/message_drawer_view_conversation_patcher',\n    'core_message/message_drawer_view_conversation_renderer',\n    'core_message/message_drawer_view_conversation_state_manager',\n    'core_message/message_drawer_router',\n    'core_message/message_drawer_routes',\n    'core/emoji/auto_complete',\n    'core/emoji/picker'\n],\nfunction(\n    $,\n    AutoRows,\n    BackOffTimer,\n    CustomEvents,\n    Notification,\n    Pending,\n    PubSub,\n    Str,\n    Repository,\n    MessageDrawerEvents,\n    Constants,\n    Patcher,\n    Renderer,\n    StateManager,\n    MessageDrawerRouter,\n    MessageDrawerRoutes,\n    initialiseEmojiAutoComplete,\n    initialiseEmojiPicker\n) {\n\n    // Contains a cache of all view states that have been loaded so far\n    // which saves us having to reload stuff with network requests when\n    // switching between conversations.\n    var stateCache = {};\n    // The current data representation of the view.\n    var viewState = null;\n    var loadedAllMessages = false;\n    var messagesOffset = 0;\n    var newMessagesPollTimer = null;\n    var isRendering = false;\n    var renderBuffer = [];\n    // If the UI is currently resetting.\n    var isResetting = true;\n    // If the UI is currently sending a message.\n    var isSendingMessage = false;\n    // If the UI is currently deleting a conversation.\n    var isDeletingConversationContent = false;\n    // A buffer of messages to send.\n    var sendMessageBuffer = [];\n    // These functions which will be generated when this module is\n    // first called. See generateRenderFunction for details.\n    var render = null;\n    // The list of renderers that have been registered to render\n    // this conversation. See generateRenderFunction for details.\n    var renderers = [];\n\n    var NEWEST_FIRST = Constants.NEWEST_MESSAGES_FIRST;\n    var LOAD_MESSAGE_LIMIT = Constants.LOAD_MESSAGE_LIMIT;\n    var MILLISECONDS_IN_SEC = Constants.MILLISECONDS_IN_SEC;\n    var SELECTORS = Constants.SELECTORS;\n    var CONVERSATION_TYPES = Constants.CONVERSATION_TYPES;\n\n    /**\n     * Get the other user userid.\n     *\n     * @return {Number} Userid.\n     */\n    var getOtherUserId = function() {\n        if (!viewState || viewState.type == CONVERSATION_TYPES.PUBLIC) {\n            return null;\n        }\n\n        var loggedInUserId = viewState.loggedInUserId;\n        if (viewState.type == CONVERSATION_TYPES.SELF) {\n            // It's a self-conversation, so the other user is the one logged in.\n            return loggedInUserId;\n        }\n\n        var otherUserIds = Object.keys(viewState.members).filter(function(userId) {\n            return loggedInUserId != userId;\n        });\n\n        return otherUserIds.length ? otherUserIds[0] : null;\n    };\n\n    /**\n     * Search the cache to see if we've already loaded a private conversation\n     * with the given user id.\n     *\n     * @param {Number} userId The id of the other user.\n     * @return {Number|null} Conversation id.\n     */\n    var getCachedPrivateConversationIdFromUserId = function(userId) {\n        return Object.keys(stateCache).reduce(function(carry, id) {\n            if (!carry) {\n                var state = stateCache[id].state;\n\n                if (state.type != CONVERSATION_TYPES.PUBLIC) {\n                    if (userId in state.members) {\n                        // We've found a cached conversation for this user!\n                        carry = state.id;\n                    }\n                }\n            }\n\n            return carry;\n        }, null);\n    };\n\n    /**\n     * Get profile info for logged in user.\n     *\n     * @param {Object} body Conversation body container element.\n     * @return {Object}\n     */\n    var getLoggedInUserProfile = function(body) {\n        return {\n            id: parseInt(body.attr('data-user-id'), 10),\n            fullname: null,\n            profileimageurl: null,\n            profileimageurlsmall: null,\n            isonline:  null,\n            showonlinestatus: null,\n            isblocked: null,\n            iscontact: null,\n            isdeleted: null,\n            canmessage: null,\n            canmessageevenifblocked: null,\n            requirescontact: null,\n            contactrequests: []\n        };\n    };\n\n    /**\n     * Get the messages offset value to load more messages.\n     *\n     * @return {Number}\n     */\n    var getMessagesOffset = function() {\n        return messagesOffset;\n    };\n\n    /**\n     * Set the messages offset value for loading more messages.\n     *\n     * @param {Number} value The offset value\n     */\n    var setMessagesOffset = function(value) {\n        messagesOffset = value;\n        stateCache[viewState.id].messagesOffset = value;\n    };\n\n    /**\n     * Check if all messages have been loaded.\n     *\n     * @return {Bool}\n     */\n    var hasLoadedAllMessages = function() {\n        return loadedAllMessages;\n    };\n\n    /**\n     * Set whether all messages have been loaded or not.\n     *\n     * @param {Bool} value If all messages have been loaded.\n     */\n    var setLoadedAllMessages = function(value) {\n        loadedAllMessages = value;\n        stateCache[viewState.id].loadedAllMessages = value;\n    };\n\n    /**\n     * Get the messages container element.\n     *\n     * @param  {Object} body Conversation body container element.\n     * @return {Object} The messages container element.\n     */\n    var getMessagesContainer = function(body) {\n        return body.find(SELECTORS.MESSAGES_CONTAINER);\n    };\n\n    /**\n     * Reformat the conversation for an event payload.\n     *\n     * @param  {Object} state The view state.\n     * @return {Object} New formatted conversation.\n     */\n    var formatConversationForEvent = function(state) {\n        return {\n            id: state.id,\n            name: state.name,\n            subname: state.subname,\n            imageUrl: state.imageUrl,\n            isFavourite: state.isFavourite,\n            isMuted: state.isMuted,\n            type: state.type,\n            totalMemberCount: state.totalMemberCount,\n            loggedInUserId: state.loggedInUserId,\n            messages: state.messages.map(function(message) {\n                return $.extend({}, message);\n            }),\n            members: Object.keys(state.members).map(function(id) {\n                var formattedMember = $.extend({}, state.members[id]);\n                formattedMember.contactrequests = state.members[id].contactrequests.map(function(request) {\n                    return $.extend({}, request);\n                });\n                return formattedMember;\n            })\n        };\n    };\n\n    /**\n     * Load up an empty private conversation between the logged in user and the\n     * other user. Sets all of the conversation details based on the other user.\n     *\n     * A conversation isn't created until the user sends the first message.\n     *\n     * @param  {Object} loggedInUserProfile The logged in user profile.\n     * @param  {Number} otherUserId The other user id.\n     * @return {Object} Profile returned from repository.\n     */\n    var loadEmptyPrivateConversation = function(loggedInUserProfile, otherUserId) {\n        var loggedInUserId = loggedInUserProfile.id;\n        // If the other user id is the same as the logged in user then this is a self\n        // conversation.\n        var conversationType = loggedInUserId == otherUserId ? CONVERSATION_TYPES.SELF : CONVERSATION_TYPES.PRIVATE;\n        var newState = StateManager.setLoadingMembers(viewState, true);\n        newState = StateManager.setLoadingMessages(newState, true);\n        render(newState);\n\n        return Repository.getMemberInfo(loggedInUserId, [otherUserId], true, true)\n            .then(function(profiles) {\n                if (profiles.length) {\n                    return profiles[0];\n                } else {\n                    throw new Error('Unable to load other user profile');\n                }\n            })\n            .then(function(profile) {\n                // If the conversation is a self conversation then the profile loaded is the\n                // logged in user so only add that to the members array.\n                var members = conversationType == CONVERSATION_TYPES.SELF ? [profile] : [profile, loggedInUserProfile];\n                var newState = StateManager.addMembers(viewState, members);\n                newState = StateManager.setLoadingMembers(newState, false);\n                newState = StateManager.setLoadingMessages(newState, false);\n                newState = StateManager.setName(newState, profile.fullname);\n                newState = StateManager.setType(newState, conversationType);\n                newState = StateManager.setImageUrl(newState, profile.profileimageurl);\n                newState = StateManager.setTotalMemberCount(newState, members.length);\n                render(newState);\n                return profile;\n            })\n            .catch(function(error) {\n                var newState = StateManager.setLoadingMembers(viewState, false);\n                render(newState);\n                Notification.exception(error);\n            });\n    };\n\n    /**\n     * Create a new state from a conversation object.\n     *\n     * @param {Object} conversation The conversation object.\n     * @param {Number} loggedInUserId The logged in user id.\n     * @return {Object} new state.\n     */\n    var updateStateFromConversation = function(conversation, loggedInUserId) {\n        var otherUser = null;\n        if (conversation.type == CONVERSATION_TYPES.PRIVATE) {\n            // For private conversations, remove current logged in user from the members list to get the other user.\n            var otherUsers = conversation.members.filter(function(member) {\n                return member.id != loggedInUserId;\n            });\n            otherUser = otherUsers.length ? otherUsers[0] : null;\n        } else if (conversation.type == CONVERSATION_TYPES.SELF) {\n            // Self-conversations have only one member.\n            otherUser = conversation.members[0];\n        }\n\n        var name = conversation.name;\n        var imageUrl = conversation.imageurl;\n\n        if (conversation.type != CONVERSATION_TYPES.PUBLIC) {\n            name = name || otherUser ? otherUser.fullname : '';\n            imageUrl = imageUrl || otherUser ? otherUser.profileimageurl : '';\n        }\n\n        var newState = StateManager.addMembers(viewState, conversation.members);\n        newState = StateManager.setName(newState, name);\n        newState = StateManager.setSubname(newState, conversation.subname);\n        newState = StateManager.setType(newState, conversation.type);\n        newState = StateManager.setImageUrl(newState, imageUrl);\n        newState = StateManager.setTotalMemberCount(newState, conversation.membercount);\n        newState = StateManager.setIsFavourite(newState, conversation.isfavourite);\n        newState = StateManager.setIsMuted(newState, conversation.ismuted);\n        newState = StateManager.addMessages(newState, conversation.messages);\n        newState = StateManager.setCanDeleteMessagesForAllUsers(newState, conversation.candeletemessagesforallusers);\n        return newState;\n    };\n\n    /**\n     * Get the details for a conversation from the conversation id.\n     *\n     * @param  {Number} conversationId The conversation id.\n     * @param  {Object} loggedInUserProfile The logged in user profile.\n     * @param  {Number} messageLimit The number of messages to include.\n     * @param  {Number} messageOffset The number of messages to skip.\n     * @param  {Bool} newestFirst Order messages newest first.\n     * @return {Object} Promise resolved when loaded.\n     */\n    var loadNewConversation = function(\n        conversationId,\n        loggedInUserProfile,\n        messageLimit,\n        messageOffset,\n        newestFirst\n    ) {\n        var loggedInUserId = loggedInUserProfile.id;\n        var newState = StateManager.setLoadingMembers(viewState, true);\n        newState = StateManager.setLoadingMessages(newState, true);\n        render(newState);\n\n        return Repository.getConversation(\n            loggedInUserId,\n            conversationId,\n            true,\n            true,\n            0,\n            0,\n            messageLimit + 1,\n            messageOffset,\n            newestFirst\n        )\n            .then(function(conversation) {\n                if (conversation.messages.length > messageLimit) {\n                    conversation.messages = conversation.messages.slice(1);\n                } else {\n                    setLoadedAllMessages(true);\n                }\n\n                setMessagesOffset(messageOffset + messageLimit);\n\n                return conversation;\n            })\n            .then(function(conversation) {\n                var hasLoggedInUser = conversation.members.filter(function(member) {\n                    return member.id == loggedInUserProfile.id;\n                });\n\n                if (hasLoggedInUser.length < 1) {\n                    conversation.members = conversation.members.concat([loggedInUserProfile]);\n                }\n\n                var newState = updateStateFromConversation(conversation, loggedInUserProfile.id);\n                newState = StateManager.setLoadingMembers(newState, false);\n                newState = StateManager.setLoadingMessages(newState, false);\n                return render(newState)\n                    .then(function() {\n                        return conversation;\n                    });\n            })\n            .then(function() {\n                return markConversationAsRead(conversationId);\n            })\n            .catch(function(error) {\n                var newState = StateManager.setLoadingMembers(viewState, false);\n                newState = StateManager.setLoadingMessages(newState, false);\n                render(newState);\n                Notification.exception(error);\n            });\n    };\n\n    /**\n     * Get the details for a conversation from and existing conversation object.\n     *\n     * @param  {Object} conversation The conversation object.\n     * @param  {Object} loggedInUserProfile The logged in user profile.\n     * @param  {Number} messageLimit The number of messages to include.\n     * @param  {Bool} newestFirst Order messages newest first.\n     * @return {Object} Promise resolved when loaded.\n     */\n    var loadExistingConversation = function(\n        conversation,\n        loggedInUserProfile,\n        messageLimit,\n        newestFirst\n    ) {\n        var hasLoggedInUser = conversation.members.filter(function(member) {\n            return member.id == loggedInUserProfile.id;\n        });\n\n        if (hasLoggedInUser.length < 1) {\n            conversation.members = conversation.members.concat([loggedInUserProfile]);\n        }\n\n        var messageCount = conversation.messages.length;\n        var hasLoadedEnoughMessages = messageCount >= messageLimit;\n        var newState = updateStateFromConversation(conversation, loggedInUserProfile.id);\n        newState = StateManager.setLoadingMembers(newState, false);\n        newState = StateManager.setLoadingMessages(newState, !hasLoadedEnoughMessages);\n        var renderPromise = render(newState);\n\n        return renderPromise.then(function() {\n                if (!hasLoadedEnoughMessages) {\n                    // We haven't got enough messages so let's load some more.\n                    return loadMessages(conversation.id, messageLimit, messageCount, newestFirst, []);\n                } else {\n                    // We've got enough messages. No need to load any more for now.\n                    return {messages: conversation.messages};\n                }\n            })\n            .then(function() {\n                var messages = viewState.messages;\n                // Update the offset to reflect the number of messages we've loaded.\n                setMessagesOffset(messages.length);\n                markConversationAsRead(viewState.id);\n\n                return messages;\n            })\n            .catch(Notification.exception);\n    };\n\n    /**\n     * Load messages for this conversation and pass them to the renderer.\n     *\n     * @param  {Number} conversationId Conversation id.\n     * @param  {Number} limit Number of messages to load.\n     * @param  {Number} offset Get messages from offset.\n     * @param  {Bool} newestFirst Get newest messages first.\n     * @param  {Array} ignoreList Ignore any messages with ids in this list.\n     * @param  {Number|null} timeFrom Only get messages from this time onwards.\n     * @return {Promise} renderer promise.\n     */\n    var loadMessages = function(conversationId, limit, offset, newestFirst, ignoreList, timeFrom) {\n        return Repository.getMessages(\n                viewState.loggedInUserId,\n                conversationId,\n                limit ? limit + 1 : limit,\n                offset,\n                newestFirst,\n                timeFrom\n            )\n            .then(function(result) {\n                // Prevent older requests from contaminating the current view.\n                if (result.id != viewState.id) {\n                    result.messages = [];\n                    // Purge old conversation cache to prevent messages lose.\n                    if (result.id in stateCache) {\n                        delete stateCache[result.id];\n                    }\n                }\n\n                return result;\n            })\n            .then(function(result) {\n                if (result.messages.length && ignoreList.length) {\n                    result.messages = result.messages.filter(function(message) {\n                        // Skip any messages in our ignore list.\n                        return ignoreList.indexOf(parseInt(message.id, 10)) < 0;\n                    });\n                }\n\n                return result;\n            })\n            .then(function(result) {\n                if (!limit) {\n                    return result;\n                } else if (result.messages.length > limit) {\n                    // Ignore the last result which was just to test if there are more\n                    // to load.\n                    result.messages = result.messages.slice(0, -1);\n                } else {\n                    setLoadedAllMessages(true);\n                }\n\n                return result;\n            })\n            .then(function(result) {\n                var membersToAdd = result.members.filter(function(member) {\n                    return !(member.id in viewState.members);\n                });\n                var newState = StateManager.addMembers(viewState, membersToAdd);\n                newState = StateManager.addMessages(newState, result.messages);\n                newState = StateManager.setLoadingMessages(newState, false);\n                return render(newState)\n                    .then(function() {\n                        return result;\n                    });\n            })\n            .catch(function(error) {\n                var newState = StateManager.setLoadingMessages(viewState, false);\n                render(newState);\n                // Re-throw the error for other error handlers.\n                throw error;\n            });\n    };\n\n    /**\n     * Create a callback function for getting new messages for this conversation.\n     *\n     * @param  {Number} conversationId Conversation id.\n     * @param  {Bool} newestFirst Show newest messages first\n     * @return {Function} Callback function that returns a renderer promise.\n     */\n    var getLoadNewMessagesCallback = function(conversationId, newestFirst) {\n        return function() {\n            var messages = viewState.messages;\n            var mostRecentMessage = messages.length ? messages[messages.length - 1] : null;\n            var lastTimeCreated = mostRecentMessage ? mostRecentMessage.timeCreated : null;\n\n            if (lastTimeCreated && !isResetting && !isSendingMessage && !isDeletingConversationContent) {\n                // There may be multiple messages with the same time created value since\n                // the accuracy is only down to the second. The server will include these\n                // messages in the result (since it does a >= comparison on time from) so\n                // we need to filter them back out of the result so that we're left only\n                // with the new messages.\n                var ignoreMessageIds = [];\n                for (var i = messages.length - 1; i >= 0; i--) {\n                    var message = messages[i];\n                    if (message.timeCreated === lastTimeCreated) {\n                        ignoreMessageIds.push(message.id);\n                    } else {\n                        // Since the messages are ordered in ascending order of time created\n                        // we can break as soon as we hit a message with a different time created\n                        // because we know all other messages will have lower values.\n                        break;\n                    }\n                }\n\n                return loadMessages(\n                        conversationId,\n                        0,\n                        0,\n                        newestFirst,\n                        ignoreMessageIds,\n                        lastTimeCreated\n                    )\n                    .then(function(result) {\n                        if (result.messages.length) {\n                            // If we found some results then restart the polling timer\n                            // because the other user might be sending messages.\n                            newMessagesPollTimer.restart();\n                            // We've also got a new last message so publish that for other\n                            // components to update.\n                            var conversation = formatConversationForEvent(viewState);\n                            PubSub.publish(MessageDrawerEvents.CONVERSATION_NEW_LAST_MESSAGE, conversation);\n                            return markConversationAsRead(conversationId);\n                        } else {\n                            return result;\n                        }\n                    });\n            }\n\n            return $.Deferred().resolve().promise();\n        };\n    };\n\n    /**\n     * Mark a conversation as read.\n     *\n     * @param  {Number} conversationId The conversation id.\n     * @return {Promise} The renderer promise.\n     */\n    var markConversationAsRead = function(conversationId) {\n        var loggedInUserId = viewState.loggedInUserId;\n        var pendingPromise = new Pending('core_message/message_drawer_view_conversation:markConversationAsRead');\n\n        return Repository.markAllConversationMessagesAsRead(loggedInUserId, conversationId)\n            .then(function() {\n                var newState = StateManager.markMessagesAsRead(viewState, viewState.messages);\n                PubSub.publish(MessageDrawerEvents.CONVERSATION_READ, conversationId);\n                return render(newState);\n            })\n            .then(function(result) {\n                pendingPromise.resolve();\n\n                return result;\n            });\n    };\n\n    /**\n     * Tell the statemanager there is request to block a user and run the renderer\n     * to show the block user dialogue.\n     *\n     * @param {Number} userId User id.\n     */\n    var requestBlockUser = function(userId) {\n        cancelRequest(userId);\n        var newState = StateManager.addPendingBlockUsersById(viewState, [userId]);\n        render(newState);\n    };\n\n    /**\n     * Send the repository a request to block a user, update the statemanager and publish\n     * a contact has been blocked.\n     *\n     * @param  {Number} userId User id of user to block.\n     * @return {Promise} Renderer promise.\n     */\n    var blockUser = function(userId) {\n        var newState = StateManager.setLoadingConfirmAction(viewState, true);\n        var pendingPromise = new Pending('core_message/message_drawer_view_conversation:blockUser');\n\n        render(newState);\n\n        return Repository.blockUser(viewState.loggedInUserId, userId)\n            .then(function(profile) {\n                var newState = StateManager.addMembers(viewState, [profile]);\n                newState = StateManager.removePendingBlockUsersById(newState, [userId]);\n                newState = StateManager.setLoadingConfirmAction(newState, false);\n                PubSub.publish(MessageDrawerEvents.CONTACT_BLOCKED, userId);\n                return render(newState);\n            })\n            .then(function(result) {\n                pendingPromise.resolve();\n\n                return result;\n            });\n    };\n\n    /**\n     * Tell the statemanager there is a request to unblock a user and run the renderer\n     * to show the unblock user dialogue.\n     *\n     * @param {Number} userId User id of user to unblock.\n     */\n    var requestUnblockUser = function(userId) {\n        cancelRequest(userId);\n        var newState = StateManager.addPendingUnblockUsersById(viewState, [userId]);\n        render(newState);\n    };\n\n    /**\n     * Send the repository a request to unblock a user, update the statemanager and publish\n     * a contact has been unblocked.\n     *\n     * @param  {Number} userId User id of user to unblock.\n     * @return {Promise} Renderer promise.\n     */\n    var unblockUser = function(userId) {\n        var newState = StateManager.setLoadingConfirmAction(viewState, true);\n        var pendingPromise = new Pending('core_message/message_drawer_view_conversation:unblockUser');\n        render(newState);\n\n        return Repository.unblockUser(viewState.loggedInUserId, userId)\n            .then(function(profile) {\n                var newState = StateManager.addMembers(viewState, [profile]);\n                newState = StateManager.removePendingUnblockUsersById(newState, [userId]);\n                newState = StateManager.setLoadingConfirmAction(newState, false);\n                PubSub.publish(MessageDrawerEvents.CONTACT_UNBLOCKED, userId);\n                return render(newState);\n            })\n            .then(function(result) {\n                pendingPromise.resolve();\n\n                return result;\n            });\n    };\n\n    /**\n     * Tell the statemanager there is a request to remove a user from the contact list\n     * and run the renderer to show the remove user from contacts dialogue.\n     *\n     * @param {Number} userId User id of user to remove from contacts.\n     */\n    var requestRemoveContact = function(userId) {\n        cancelRequest(userId);\n        var newState = StateManager.addPendingRemoveContactsById(viewState, [userId]);\n        render(newState);\n    };\n\n    /**\n     * Send the repository a request to remove a user from the contacts list. update the statemanager\n     * and publish a contact has been removed.\n     *\n     * @param  {Number} userId User id of user to remove from contacts.\n     * @return {Promise} Renderer promise.\n     */\n    var removeContact = function(userId) {\n        var newState = StateManager.setLoadingConfirmAction(viewState, true);\n        var pendingPromise = new Pending('core_message/message_drawer_view_conversation:removeContact');\n        render(newState);\n\n        return Repository.deleteContacts(viewState.loggedInUserId, [userId])\n            .then(function(profiles) {\n                var newState = StateManager.addMembers(viewState, profiles);\n                newState = StateManager.removePendingRemoveContactsById(newState, [userId]);\n                newState = StateManager.setLoadingConfirmAction(newState, false);\n                PubSub.publish(MessageDrawerEvents.CONTACT_REMOVED, userId);\n                return render(newState);\n            })\n            .then(function(result) {\n                pendingPromise.resolve();\n\n                return result;\n            });\n    };\n\n    /**\n     * Tell the statemanager there is a request to add a user to the contact list\n     * and run the renderer to show the add user to contacts dialogue.\n     *\n     * @param {Number} userId User id of user to add to contacts.\n     */\n    var requestAddContact = function(userId) {\n        cancelRequest(userId);\n        var newState = StateManager.addPendingAddContactsById(viewState, [userId]);\n        render(newState);\n    };\n\n    /**\n     * Send the repository a request to add a user to the contacts list. update the statemanager\n     * and publish a contact has been added.\n     *\n     * @param  {Number} userId User id of user to add to contacts.\n     * @return {Promise} Renderer promise.\n     */\n    var addContact = function(userId) {\n        var newState = StateManager.setLoadingConfirmAction(viewState, true);\n        var pendingPromise = new Pending('core_message/message_drawer_view_conversation:addContactRequests');\n        render(newState);\n\n        return Repository.createContactRequest(viewState.loggedInUserId, userId)\n            .then(function(response) {\n                if (!response.request) {\n                    throw new Error(response.warnings[0].message);\n                }\n\n                return response.request;\n            })\n            .then(function(request) {\n                var newState = StateManager.removePendingAddContactsById(viewState, [userId]);\n                newState = StateManager.addContactRequests(newState, [request]);\n                newState = StateManager.setLoadingConfirmAction(newState, false);\n                return render(newState);\n            })\n            .then(function(result) {\n                pendingPromise.resolve();\n\n                return result;\n            });\n    };\n\n    /**\n     * Set the current conversation as a favourite conversation.\n     *\n     * @return {Promise} Renderer promise.\n     */\n    var setFavourite = function() {\n        var userId = viewState.loggedInUserId;\n        var conversationId = viewState.id;\n        var pendingPromise = new Pending('core_message/message_drawer_view_conversation:setFavourite');\n\n        return Repository.setFavouriteConversations(userId, [conversationId])\n            .then(function() {\n                var newState = StateManager.setIsFavourite(viewState, true);\n                return render(newState);\n            })\n            .then(function() {\n                return PubSub.publish(\n                    MessageDrawerEvents.CONVERSATION_SET_FAVOURITE,\n                    formatConversationForEvent(viewState)\n                );\n            })\n            .then(function(result) {\n                pendingPromise.resolve();\n\n                return result;\n            });\n    };\n\n    /**\n     * Unset the current conversation as a favourite conversation.\n     *\n     * @return {Promise} Renderer promise.\n     */\n    var unsetFavourite = function() {\n        var userId = viewState.loggedInUserId;\n        var conversationId = viewState.id;\n        var pendingPromise = new Pending('core_message/message_drawer_view_conversation:unsetFavourite');\n\n        return Repository.unsetFavouriteConversations(userId, [conversationId])\n            .then(function() {\n                var newState = StateManager.setIsFavourite(viewState, false);\n                return render(newState);\n            })\n            .then(function() {\n                return PubSub.publish(\n                    MessageDrawerEvents.CONVERSATION_UNSET_FAVOURITE,\n                    formatConversationForEvent(viewState)\n                );\n            })\n            .then(function(result) {\n                pendingPromise.resolve();\n\n                return result;\n            });\n    };\n\n    /**\n     * Set the current conversation as a muted conversation.\n     *\n     * @return {Promise} Renderer promise.\n     */\n    var setMuted = function() {\n        var userId = viewState.loggedInUserId;\n        var conversationId = viewState.id;\n        var pendingPromise = new Pending('core_message/message_drawer_view_conversation:markConversationAsRead');\n\n        return Repository.setMutedConversations(userId, [conversationId])\n            .then(function() {\n                var newState = StateManager.setIsMuted(viewState, true);\n                return render(newState);\n            })\n            .then(function() {\n                return PubSub.publish(\n                    MessageDrawerEvents.CONVERSATION_SET_MUTED,\n                    formatConversationForEvent(viewState)\n                );\n            })\n            .then(function(result) {\n                pendingPromise.resolve();\n\n                return result;\n            });\n    };\n\n    /**\n     * Unset the current conversation as a muted conversation.\n     *\n     * @return {Promise} Renderer promise.\n     */\n    var unsetMuted = function() {\n        var userId = viewState.loggedInUserId;\n        var conversationId = viewState.id;\n\n        return Repository.unsetMutedConversations(userId, [conversationId])\n            .then(function() {\n                var newState = StateManager.setIsMuted(viewState, false);\n                return render(newState);\n            })\n            .then(function() {\n                return PubSub.publish(\n                    MessageDrawerEvents.CONVERSATION_UNSET_MUTED,\n                    formatConversationForEvent(viewState)\n                );\n            });\n    };\n\n    /**\n     * Tell the statemanager there is a request to delete the selected messages\n     * and run the renderer to show confirm delete messages dialogue.\n     *\n     * @param {Number} userId User id.\n     */\n    var requestDeleteSelectedMessages = function(userId) {\n        var selectedMessageIds = viewState.selectedMessageIds;\n        cancelRequest(userId);\n        var newState = StateManager.addPendingDeleteMessagesById(viewState, selectedMessageIds);\n        render(newState);\n    };\n\n    /**\n     * Send the repository a request to delete the messages pending deletion. Update the statemanager\n     * and publish a message deletion event.\n     *\n     * @return {Promise} Renderer promise.\n     */\n    var deleteSelectedMessages = function() {\n        var pendingPromise = new Pending('core_message/message_drawer_view_conversation:deleteSelectedMessages');\n        var messageIds = viewState.pendingDeleteMessageIds;\n        var sentMessages = viewState.messages.filter(function(message) {\n            // If a message sendState is null then it means it was loaded from the server or if it's\n            // set to sent then it means the user has successfully sent it in this page load.\n            return messageIds.indexOf(message.id) >= 0 && (message.sendState == 'sent' || message.sendState === null);\n        });\n        var newState = StateManager.setLoadingConfirmAction(viewState, true);\n\n        render(newState);\n\n        var deleteMessagesPromise = $.Deferred().resolve().promise();\n\n\n        if (sentMessages.length) {\n            // We only need to send a request to the server if we're trying to delete messages that\n            // have successfully been sent.\n            var sentMessageIds = sentMessages.map(function(message) {\n                return message.id;\n            });\n            if (newState.deleteMessagesForAllUsers) {\n                deleteMessagesPromise = Repository.deleteMessagesForAllUsers(viewState.loggedInUserId, sentMessageIds);\n            } else {\n                deleteMessagesPromise = Repository.deleteMessages(viewState.loggedInUserId, sentMessageIds);\n            }\n        }\n\n        // Mark that we are deleting content from the  conversation to prevent updates of it.\n        isDeletingConversationContent = true;\n\n        // Stop polling for new messages to the open conversation.\n        if (newMessagesPollTimer) {\n            newMessagesPollTimer.stop();\n        }\n\n        return deleteMessagesPromise.then(function() {\n                var newState = StateManager.removeMessagesById(viewState, messageIds);\n                newState = StateManager.removePendingDeleteMessagesById(newState, messageIds);\n                newState = StateManager.removeSelectedMessagesById(newState, messageIds);\n                newState = StateManager.setLoadingConfirmAction(newState, false);\n                newState = StateManager.setDeleteMessagesForAllUsers(newState, false);\n\n                var prevLastMessage = viewState.messages[viewState.messages.length - 1];\n                var newLastMessage = newState.messages.length ? newState.messages[newState.messages.length - 1] : null;\n\n                if (newLastMessage && newLastMessage.id != prevLastMessage.id) {\n                    var conversation = formatConversationForEvent(newState);\n                    PubSub.publish(MessageDrawerEvents.CONVERSATION_NEW_LAST_MESSAGE, conversation);\n                } else if (!newState.messages.length) {\n                    PubSub.publish(MessageDrawerEvents.CONVERSATION_DELETED, newState.id);\n                }\n\n                isDeletingConversationContent = false;\n                return render(newState);\n            })\n            .then(function(result) {\n                pendingPromise.resolve();\n\n                return result;\n            })\n            .catch(Notification.exception);\n    };\n\n    /**\n     * Tell the statemanager there is a request to delete a conversation\n     * and run the renderer to show confirm delete conversation dialogue.\n     *\n     * @param {Number} userId User id of other user.\n     */\n    var requestDeleteConversation = function(userId) {\n        cancelRequest(userId);\n        var newState = StateManager.setPendingDeleteConversation(viewState, true);\n        render(newState);\n    };\n\n    /**\n     * Send the repository a request to delete a conversation. Update the statemanager\n     * and publish a conversation deleted event.\n     *\n     * @return {Promise} Renderer promise.\n     */\n    var deleteConversation = function() {\n        var pendingPromise = new Pending('core_message/message_drawer_view_conversation:markConversationAsRead');\n        var newState = StateManager.setLoadingConfirmAction(viewState, true);\n        render(newState);\n\n        // Mark that we are deleting the conversation to prevent updates of it.\n        isDeletingConversationContent = true;\n\n        // Stop polling for new messages to the open conversation.\n        if (newMessagesPollTimer) {\n            newMessagesPollTimer.stop();\n        }\n\n        return Repository.deleteConversation(viewState.loggedInUserId, viewState.id)\n            .then(function() {\n                var newState = StateManager.removeMessages(viewState, viewState.messages);\n                newState = StateManager.removeSelectedMessagesById(newState, viewState.selectedMessageIds);\n                newState = StateManager.setPendingDeleteConversation(newState, false);\n                newState = StateManager.setLoadingConfirmAction(newState, false);\n                PubSub.publish(MessageDrawerEvents.CONVERSATION_DELETED, newState.id);\n\n                isDeletingConversationContent = false;\n\n                return render(newState);\n            })\n            .then(function(result) {\n                pendingPromise.resolve();\n\n                return result;\n            });\n    };\n\n    /**\n     * Tell the statemanager to cancel all pending actions.\n     *\n     * @param  {Number} userId User id.\n     */\n    var cancelRequest = function(userId) {\n        var pendingDeleteMessageIds = viewState.pendingDeleteMessageIds;\n        var newState = StateManager.removePendingAddContactsById(viewState, [userId]);\n        newState = StateManager.removePendingRemoveContactsById(newState, [userId]);\n        newState = StateManager.removePendingUnblockUsersById(newState, [userId]);\n        newState = StateManager.removePendingBlockUsersById(newState, [userId]);\n        newState = StateManager.removePendingDeleteMessagesById(newState, pendingDeleteMessageIds);\n        newState = StateManager.setPendingDeleteConversation(newState, false);\n        newState = StateManager.setDeleteMessagesForAllUsers(newState, false);\n        render(newState);\n    };\n\n    /**\n     * Accept the contact request from the given user.\n     *\n     * @param  {Number} userId User id of other user.\n     * @return {Promise} Renderer promise.\n     */\n    var acceptContactRequest = function(userId) {\n        var pendingPromise = new Pending('core_message/message_drawer_view_conversation:acceptContactRequest');\n\n        // Search the list of the logged in user's contact requests to find the\n        // one from this user.\n        var loggedInUserId = viewState.loggedInUserId;\n        var requests = viewState.members[userId].contactrequests.filter(function(request) {\n            return request.requesteduserid == loggedInUserId;\n        });\n        var request = requests[0];\n        var newState = StateManager.setLoadingConfirmAction(viewState, true);\n        render(newState);\n\n        return Repository.acceptContactRequest(userId, loggedInUserId)\n            .then(function(profile) {\n                var newState = StateManager.removeContactRequests(viewState, [request]);\n                newState = StateManager.addMembers(viewState, [profile]);\n                newState = StateManager.setLoadingConfirmAction(newState, false);\n                return render(newState);\n            })\n            .then(function() {\n                PubSub.publish(MessageDrawerEvents.CONTACT_ADDED, viewState.members[userId]);\n                PubSub.publish(MessageDrawerEvents.CONTACT_REQUEST_ACCEPTED, request);\n                return;\n            })\n            .then(function(result) {\n                pendingPromise.resolve();\n\n                return result;\n            });\n    };\n\n    /**\n     * Decline the contact request from the given user.\n     *\n     * @param  {Number} userId User id of other user.\n     * @return {Promise} Renderer promise.\n     */\n    var declineContactRequest = function(userId) {\n        var pendingPromise = new Pending('core_message/message_drawer_view_conversation:declineContactRequest');\n\n        // Search the list of the logged in user's contact requests to find the\n        // one from this user.\n        var loggedInUserId = viewState.loggedInUserId;\n        var requests = viewState.members[userId].contactrequests.filter(function(request) {\n            return request.requesteduserid == loggedInUserId;\n        });\n        var request = requests[0];\n        var newState = StateManager.setLoadingConfirmAction(viewState, true);\n        render(newState);\n\n        return Repository.declineContactRequest(userId, loggedInUserId)\n            .then(function(profile) {\n                var newState = StateManager.removeContactRequests(viewState, [request]);\n                newState = StateManager.addMembers(viewState, [profile]);\n                newState = StateManager.setLoadingConfirmAction(newState, false);\n                return render(newState);\n            })\n            .then(function() {\n                PubSub.publish(MessageDrawerEvents.CONTACT_REQUEST_DECLINED, request);\n                return;\n            })\n            .then(function(result) {\n                pendingPromise.resolve();\n\n                return result;\n            });\n    };\n\n    /**\n     * Send all of the messages in the buffer to the server to be created. Update the\n     * UI with the newly created message information.\n     *\n     * This function will recursively call itself in order to make sure the buffer is\n     * always being processed.\n     */\n    var processSendMessageBuffer = function() {\n        if (isSendingMessage) {\n            // We're already sending messages so nothing to do.\n            return;\n        }\n        if (!sendMessageBuffer.length) {\n            // No messages waiting to send. Nothing to do.\n            return;\n        }\n\n        var pendingPromise = new Pending('core_message/message_drawer_view_conversation:processSendMessageBuffer');\n\n        // Flag that we're processing the queue.\n        isSendingMessage = true;\n        // Grab all of the messages in the buffer.\n        var messagesToSend = sendMessageBuffer.slice();\n        // Empty the buffer since we're processing it.\n        sendMessageBuffer = [];\n        var conversationId = viewState.id;\n        var newConversationId = null;\n        var messagesText = messagesToSend.map(function(message) {\n            return message.text;\n        });\n        var messageIds = messagesToSend.map(function(message) {\n            return message.id;\n        });\n        var sendMessagePromise = null;\n        var newCanDeleteMessagesForAllUsers = null;\n        if (!conversationId && (viewState.type != CONVERSATION_TYPES.PUBLIC)) {\n            // If it's a new private conversation then we need to use the old\n            // web service function to create the conversation.\n            var otherUserId = getOtherUserId();\n            sendMessagePromise = Repository.sendMessagesToUser(otherUserId, messagesText)\n                .then(function(messages) {\n                    if (messages.length) {\n                        newConversationId = parseInt(messages[0].conversationid, 10);\n                        newCanDeleteMessagesForAllUsers = messages[0].candeletemessagesforallusers;\n                    }\n                    return messages;\n                });\n        } else {\n            sendMessagePromise = Repository.sendMessagesToConversation(conversationId, messagesText);\n        }\n\n        sendMessagePromise\n            .then(function(messages) {\n                var newMessageIds = messages.map(function(message) {\n                    return message.id;\n                });\n                var data = [];\n                var selectedToRemove = [];\n                var selectedToAdd = [];\n\n                messagesToSend.forEach(function(oldMessage, index) {\n                    var newMessage = messages[index];\n                    // Update messages expects and array of arrays where the first value\n                    // is the old message to update and the second value is the new values\n                    // to set.\n                    data.push([oldMessage, newMessage]);\n\n                    if (viewState.selectedMessageIds.indexOf(oldMessage.id) >= 0) {\n                        // If the message was added to the \"selected messages\" list while it was still\n                        // being sent then we should update it's id in that list now to make sure future\n                        // actions work.\n                        selectedToRemove.push(oldMessage.id);\n                        selectedToAdd.push(newMessage.id);\n                    }\n                });\n                var newState = StateManager.updateMessages(viewState, data);\n                newState = StateManager.setMessagesSendSuccessById(newState, newMessageIds);\n\n                if (selectedToRemove.length) {\n                    newState = StateManager.removeSelectedMessagesById(newState, selectedToRemove);\n                }\n\n                if (selectedToAdd.length) {\n                    newState = StateManager.addSelectedMessagesById(newState, selectedToAdd);\n                }\n\n                var conversation = formatConversationForEvent(newState);\n\n                if (!newState.id) {\n                    // If this message created the conversation then save the conversation\n                    // id.\n                    newState = StateManager.setId(newState, newConversationId);\n                    conversation.id = newConversationId;\n                    resetMessagePollTimer(newConversationId);\n                    PubSub.publish(MessageDrawerEvents.CONVERSATION_CREATED, conversation);\n                    newState = StateManager.setCanDeleteMessagesForAllUsers(newState, newCanDeleteMessagesForAllUsers);\n                }\n\n                // Update the UI with the new message values from the server.\n                render(newState);\n                // Recurse just in case there has been more messages added to the buffer.\n                isSendingMessage = false;\n                processSendMessageBuffer();\n                PubSub.publish(MessageDrawerEvents.CONVERSATION_NEW_LAST_MESSAGE, conversation);\n                return;\n            })\n            .then(function(result) {\n                pendingPromise.resolve();\n\n                return result;\n            })\n            .catch(function(e) {\n                var errorMessage;\n                if (e.message) {\n                    errorMessage = $.Deferred().resolve(e.message).promise();\n                } else {\n                    errorMessage = Str.get_string('unknownerror', 'core');\n                }\n\n                var handleFailedMessages = function(errorMessage) {\n                    // We failed to create messages so remove the old messages from the pending queue\n                    // and update the UI to indicate that the message failed.\n                    var newState = StateManager.setMessagesSendFailById(viewState, messageIds, errorMessage);\n                    render(newState);\n                    isSendingMessage = false;\n                    processSendMessageBuffer();\n                };\n\n                errorMessage.then(handleFailedMessages)\n                    .then(function(result) {\n                        pendingPromise.resolve();\n\n                        return result;\n                    })\n                    .catch(function(e) {\n                        // Hrmm, we can't even load the error messages string! We'll have to\n                        // hard code something in English here if we still haven't got a message\n                        // to show.\n                        var finalError = e.message || 'Something went wrong!';\n                        handleFailedMessages(finalError);\n                    });\n            });\n    };\n\n    /**\n     * Create a plain version of an HTML text.\n     *\n     * This texts is used as a message preview while is sent to the server. This way\n     * it is possible to prevent self-xss.\n     *\n     * @param {String} text Text to send.\n     * @return {String} The plain text version of the text.\n     */\n    const previewText = function(text) {\n        // Remove all script and styles from text (we don't want it there).\n        let plaintext = text.replace(/<style([\\s\\S]*?)<\\/style>/gi, '');\n        plaintext = plaintext.replace(/<script([\\s\\S]*?)<\\/script>/gi, '');\n        // Beautify a bit the output adding some line breaks.\n        plaintext = plaintext.replace(/<\\/div>/ig, '\\n');\n        plaintext = plaintext.replace(/<\\/li>/ig, '\\n');\n        plaintext = plaintext.replace(/<li>/ig, '  *  ');\n        plaintext = plaintext.replace(/<\\/ul>/ig, '\\n');\n        plaintext = plaintext.replace(/<\\/p>/ig, '\\n');\n        plaintext = plaintext.replace(/<br[^>]*>/gi, '\\n');\n        // Remove all remaining tags and convert line breaks into html.\n        plaintext = plaintext.replace(/<[^>]+>/ig, '');\n        plaintext = plaintext.replace(/\\n+/ig, '\\n');\n        return plaintext.replace(/\\n/ig, '<br>');\n    };\n\n    /**\n     * Buffers messages to be sent to the server. We use a buffer here to allow the\n     * user to freely input messages without blocking the interface for them.\n     *\n     * Instead we just queue all of their messages up and send them as fast as we can.\n     *\n     * @param {String} text Text to send.\n     */\n    var sendMessage = function(text) {\n        var id = 'temp' + Date.now();\n        // Render a preview version of the message while sending.\n        let loadingmessage = {\n            id: id,\n            useridfrom: viewState.loggedInUserId,\n            text:  previewText(text),\n            timecreated: null\n        };\n        var newState = StateManager.addMessages(viewState, [loadingmessage]);\n        render(newState);\n        // Send the real message.\n        var message = {\n            id: id,\n            useridfrom: viewState.loggedInUserId,\n            text: text,\n            timecreated: null\n        };\n        sendMessageBuffer.push(message);\n        processSendMessageBuffer();\n    };\n\n    /**\n     * Retry sending a message that failed.\n     *\n     * @param {Object} message The message to send.\n     */\n    var retrySendMessage = function(message) {\n        var newState = StateManager.setMessagesSendPendingById(viewState, [message.id]);\n        render(newState);\n        sendMessageBuffer.push(message);\n        processSendMessageBuffer();\n    };\n\n    /**\n     * Toggle the selected messages update the statemanager and render the result.\n     *\n     * @param  {Number} messageId The id of the message to be toggled\n     */\n    var toggleSelectMessage = function(messageId) {\n        var newState = viewState;\n\n        if (viewState.selectedMessageIds.indexOf(messageId) > -1) {\n            newState = StateManager.removeSelectedMessagesById(viewState, [messageId]);\n        } else {\n            newState = StateManager.addSelectedMessagesById(viewState, [messageId]);\n        }\n\n        render(newState);\n    };\n\n    /**\n     * Cancel edit mode (selecting the messages).\n     */\n    var cancelEditMode = function() {\n        cancelRequest(getOtherUserId());\n        var newState = StateManager.removeSelectedMessagesById(viewState, viewState.selectedMessageIds);\n        render(newState);\n    };\n\n    /**\n     * Process the patches in the render buffer one at a time in order until the\n     * buffer is empty.\n     *\n     * @param {Object} header The conversation header container element.\n     * @param {Object} body The conversation body container element.\n     * @param {Object} footer The conversation footer container element.\n     */\n    var processRenderBuffer = function(header, body, footer) {\n        if (isRendering) {\n            return;\n        }\n\n        if (!renderBuffer.length) {\n            return;\n        }\n\n        isRendering = true;\n        var renderable = renderBuffer.shift();\n        var renderPromises = renderers.map(function(renderFunc) {\n            return renderFunc(renderable.patch);\n        });\n\n        $.when.apply(null, renderPromises)\n            .then(function() {\n                isRendering = false;\n                renderable.deferred.resolve(true);\n                // Keep processing the buffer until it's empty.\n                processRenderBuffer(header, body, footer);\n\n                return;\n            })\n            .catch(function(error) {\n                isRendering = false;\n                renderable.deferred.reject(error);\n                Notification.exception(error);\n            });\n    };\n\n    /**\n     * Create a function to render the Conversation.\n     *\n     * @param  {Object} header The conversation header container element.\n     * @param  {Object} body The conversation body container element.\n     * @param  {Object} footer The conversation footer container element.\n     * @param  {Bool} isNewConversation Has someone else already initialised a conversation?\n     * @return {Promise} Renderer promise.\n     */\n    var generateRenderFunction = function(header, body, footer, isNewConversation) {\n        var rendererFunc = function(patch) {\n            return Renderer.render(header, body, footer, patch);\n        };\n\n        if (!isNewConversation) {\n            // Looks like someone got here before us! We'd better update our\n            // UI to make sure it matches.\n            var initialState = StateManager.buildInitialState(viewState.midnight, viewState.loggedInUserId, viewState.id);\n            var syncPatch = Patcher.buildPatch(initialState, viewState);\n            rendererFunc(syncPatch);\n        }\n\n        renderers.push(rendererFunc);\n\n        return function(newState) {\n            var patch = Patcher.buildPatch(viewState, newState);\n            var deferred = $.Deferred();\n\n            // Check if the patch has any data. Ignore empty patches.\n            if (Object.keys(patch).length) {\n                // Add the patch to the render buffer which gets processed in order.\n                renderBuffer.push({\n                    patch: patch,\n                    deferred: deferred\n                });\n            } else {\n                deferred.resolve(true);\n            }\n            // This is a great place to add in some console logging if you need\n            // to debug something. You can log the current state, the next state,\n            // and the generated patch and see exactly what will be updated.\n\n            // Optimistically update the state. We're going to assume that the rendering\n            // will always succeed. The rendering is asynchronous (annoyingly) so it's buffered\n            // but it'll reach eventual consistency with the current state.\n            viewState = newState;\n            if (newState.id) {\n                // Only cache created conversations.\n                stateCache[newState.id] = {\n                    state: newState,\n                    messagesOffset: getMessagesOffset(),\n                    loadedAllMessages: hasLoadedAllMessages()\n                };\n            }\n\n            // Start processing the buffer.\n            processRenderBuffer(header, body, footer);\n\n            return deferred.promise();\n        };\n    };\n\n    /**\n     * Create a confirm action function.\n     *\n     * @param {Function} actionCallback The callback function.\n     * @return {Function} Confirm action handler.\n     */\n    var generateConfirmActionHandler = function(actionCallback) {\n        return function(e, data) {\n            if (!viewState.loadingConfirmAction) {\n                actionCallback(getOtherUserId());\n                var newState = StateManager.setLoadingConfirmAction(viewState, false);\n                render(newState);\n            }\n            data.originalEvent.preventDefault();\n        };\n    };\n\n    /**\n     * Send message event handler.\n     *\n     * @param {Object} e Element this event handler is called on.\n     * @param {Object} data Data for this event.\n     */\n    var handleSendMessage = function(e, data) {\n        var target = $(e.target);\n        var footerContainer = target.closest(SELECTORS.FOOTER_CONTAINER);\n        var textArea = footerContainer.find(SELECTORS.MESSAGE_TEXT_AREA);\n        var text = textArea.val().trim();\n\n        if (text !== '') {\n            sendMessage(text);\n            textArea.val('');\n            textArea.focus();\n        }\n\n        data.originalEvent.preventDefault();\n    };\n\n    /**\n     * Select message event handler.\n     *\n     * @param {Object} e Element this event handler is called on.\n     * @param {Object} data Data for this event.\n     */\n    var handleSelectMessage = function(e, data) {\n        var selection = window.getSelection();\n        var target = $(e.target);\n\n        if (selection.toString() != '') {\n            // Bail if we're selecting.\n            return;\n        }\n\n        if (target.is('a')) {\n            // Clicking on a link in the message so ignore it.\n            return;\n        }\n\n        var element = target.closest(SELECTORS.MESSAGE);\n        var messageId = element.attr('data-message-id');\n\n        toggleSelectMessage(messageId);\n\n        data.originalEvent.preventDefault();\n    };\n\n    /**\n     * Handle retry sending of message.\n     *\n     * @param {Object} e Element this event handler is called on.\n     * @param {Object} data Data for this event.\n     */\n    var handleRetrySendMessage = function(e, data) {\n        var target = $(e.target);\n        var element = target.closest(SELECTORS.MESSAGE);\n        var messageId = element.attr('data-message-id');\n        var messages = viewState.messages.filter(function(message) {\n            return message.id == messageId;\n        });\n        var message = messages.length ? messages[0] : null;\n\n        if (message) {\n            retrySendMessage(message);\n        }\n\n        data.originalEvent.preventDefault();\n        data.originalEvent.stopPropagation();\n        e.stopPropagation();\n    };\n\n    /**\n     * Cancel edit mode event handler.\n     *\n     * @param {Object} e Element this event handler is called on.\n     * @param {Object} data Data for this event.\n     */\n    var handleCancelEditMode = function(e, data) {\n        cancelEditMode();\n        data.originalEvent.preventDefault();\n    };\n\n    /**\n     * Show the view contact page.\n     *\n     * @param {String} namespace Unique identifier for the Routes\n     * @return {Function} View contact handler.\n     */\n    var generateHandleViewContact = function(namespace) {\n        return function(e, data) {\n            var otherUserId = getOtherUserId();\n            var otherUser = viewState.members[otherUserId];\n            MessageDrawerRouter.go(namespace, MessageDrawerRoutes.VIEW_CONTACT, otherUser);\n            data.originalEvent.preventDefault();\n        };\n    };\n\n    /**\n     * Set this conversation as a favourite.\n     *\n     * @param {Object} e Element this event handler is called on.\n     * @param {Object} data Data for this event.\n     */\n    var handleSetFavourite = function(e, data) {\n        setFavourite().catch(Notification.exception);\n        data.originalEvent.preventDefault();\n    };\n\n    /**\n     * Unset this conversation as a favourite.\n     *\n     * @param {Object} e Element this event handler is called on.\n     * @param {Object} data Data for this event.\n     */\n    var handleUnsetFavourite = function(e, data) {\n        unsetFavourite().catch(Notification.exception);\n        data.originalEvent.preventDefault();\n    };\n\n    /**\n     * Show the view group info page.\n     * Set this conversation as muted.\n     *\n     * @param {Object} e Element this event handler is called on.\n     * @param {Object} data Data for this event.\n     */\n    var handleSetMuted = function(e, data) {\n        setMuted().catch(Notification.exception);\n        data.originalEvent.preventDefault();\n    };\n\n    /**\n     * Unset this conversation as muted.\n     *\n     * @param {Object} e Element this event handler is called on.\n     * @param {Object} data Data for this event.\n     */\n    var handleUnsetMuted = function(e, data) {\n        unsetMuted().catch(Notification.exception);\n        data.originalEvent.preventDefault();\n    };\n\n    /**\n     * Handle clicking on the checkbox that toggles deleting messages for\n     * all users.\n     *\n     * @param {Object} e Element this event handler is called on.\n     */\n    var handleDeleteMessagesForAllUsersToggle = function(e) {\n        var newValue = $(e.target).prop('checked');\n        var newState = StateManager.setDeleteMessagesForAllUsers(viewState, newValue);\n        render(newState);\n    };\n\n    /**\n     * Show the view contact page.\n     *\n     * @param {String} namespace Unique identifier for the Routes\n     * @return {Function} View group info handler.\n     */\n    var generateHandleViewGroupInfo = function(namespace) {\n        return function(e, data) {\n            MessageDrawerRouter.go(\n                namespace,\n                MessageDrawerRoutes.VIEW_GROUP_INFO,\n                {\n                    id: viewState.id,\n                    name: viewState.name,\n                    subname: viewState.subname,\n                    imageUrl: viewState.imageUrl,\n                    totalMemberCount: viewState.totalMemberCount\n                },\n                viewState.loggedInUserId\n            );\n            data.originalEvent.preventDefault();\n        };\n    };\n\n    /**\n     * Handle clicking on the emoji toggle button.\n     *\n     * @param {Object} e The event\n     * @param {Object} data The custom interaction event data\n     */\n    var handleToggleEmojiPicker = function(e, data) {\n        var newState = StateManager.setShowEmojiPicker(viewState, !viewState.showEmojiPicker);\n        render(newState);\n        data.originalEvent.preventDefault();\n    };\n\n    /**\n     * Handle clicking outside the emoji picker to close it.\n     *\n     * @param {Object} e The event\n     */\n    var handleCloseEmojiPicker = function(e) {\n        var target = $(e.target);\n\n        if (\n            viewState.showEmojiPicker &&\n            !target.closest(SELECTORS.EMOJI_PICKER_CONTAINER).length &&\n            !target.closest(SELECTORS.TOGGLE_EMOJI_PICKER_BUTTON).length\n        ) {\n            var newState = StateManager.setShowEmojiPicker(viewState, false);\n            render(newState);\n        }\n    };\n\n    /**\n     * Listen to, and handle events for conversations.\n     *\n     * @param {string} namespace The route namespace.\n     * @param {Object} header Conversation header container element.\n     * @param {Object} body Conversation body container element.\n     * @param {Object} footer Conversation footer container element.\n     */\n    var registerEventListeners = function(namespace, header, body, footer) {\n        var isLoadingMoreMessages = false;\n        var messagesContainer = getMessagesContainer(body);\n        var emojiPickerElement = footer.find(SELECTORS.EMOJI_PICKER);\n        var emojiAutoCompleteContainer = footer.find(SELECTORS.EMOJI_AUTO_COMPLETE_CONTAINER);\n        var messageTextArea = footer.find(SELECTORS.MESSAGE_TEXT_AREA);\n        var headerActivateHandlers = [\n            [SELECTORS.ACTION_REQUEST_BLOCK, generateConfirmActionHandler(requestBlockUser)],\n            [SELECTORS.ACTION_REQUEST_UNBLOCK, generateConfirmActionHandler(requestUnblockUser)],\n            [SELECTORS.ACTION_REQUEST_ADD_CONTACT, generateConfirmActionHandler(requestAddContact)],\n            [SELECTORS.ACTION_REQUEST_REMOVE_CONTACT, generateConfirmActionHandler(requestRemoveContact)],\n            [SELECTORS.ACTION_REQUEST_DELETE_CONVERSATION, generateConfirmActionHandler(requestDeleteConversation)],\n            [SELECTORS.ACTION_CANCEL_EDIT_MODE, handleCancelEditMode],\n            [SELECTORS.ACTION_VIEW_CONTACT, generateHandleViewContact(namespace)],\n            [SELECTORS.ACTION_VIEW_GROUP_INFO, generateHandleViewGroupInfo(namespace)],\n            [SELECTORS.ACTION_CONFIRM_FAVOURITE, handleSetFavourite],\n            [SELECTORS.ACTION_CONFIRM_MUTE, handleSetMuted],\n            [SELECTORS.ACTION_CONFIRM_UNFAVOURITE, handleUnsetFavourite],\n            [SELECTORS.ACTION_CONFIRM_UNMUTE, handleUnsetMuted]\n        ];\n        var bodyActivateHandlers = [\n            [SELECTORS.ACTION_CANCEL_CONFIRM, generateConfirmActionHandler(cancelRequest)],\n            [SELECTORS.ACTION_CONFIRM_BLOCK, generateConfirmActionHandler(blockUser)],\n            [SELECTORS.ACTION_CONFIRM_UNBLOCK, generateConfirmActionHandler(unblockUser)],\n            [SELECTORS.ACTION_CONFIRM_ADD_CONTACT, generateConfirmActionHandler(addContact)],\n            [SELECTORS.ACTION_CONFIRM_REMOVE_CONTACT, generateConfirmActionHandler(removeContact)],\n            [SELECTORS.ACTION_CONFIRM_DELETE_SELECTED_MESSAGES, generateConfirmActionHandler(deleteSelectedMessages)],\n            [SELECTORS.ACTION_CONFIRM_DELETE_CONVERSATION, generateConfirmActionHandler(deleteConversation)],\n            [SELECTORS.ACTION_OKAY_CONFIRM, generateConfirmActionHandler(cancelRequest)],\n            [SELECTORS.ACTION_REQUEST_ADD_CONTACT, generateConfirmActionHandler(requestAddContact)],\n            [SELECTORS.ACTION_ACCEPT_CONTACT_REQUEST, generateConfirmActionHandler(acceptContactRequest)],\n            [SELECTORS.ACTION_DECLINE_CONTACT_REQUEST, generateConfirmActionHandler(declineContactRequest)],\n            [SELECTORS.MESSAGE, handleSelectMessage],\n            [SELECTORS.DELETE_MESSAGES_FOR_ALL_USERS_TOGGLE, handleDeleteMessagesForAllUsersToggle],\n            [SELECTORS.RETRY_SEND, handleRetrySendMessage]\n        ];\n        var footerActivateHandlers = [\n            [SELECTORS.SEND_MESSAGE_BUTTON, handleSendMessage],\n            [SELECTORS.TOGGLE_EMOJI_PICKER_BUTTON, handleToggleEmojiPicker],\n            [SELECTORS.ACTION_REQUEST_DELETE_SELECTED_MESSAGES, generateConfirmActionHandler(requestDeleteSelectedMessages)],\n            [SELECTORS.ACTION_REQUEST_ADD_CONTACT, generateConfirmActionHandler(requestAddContact)],\n            [SELECTORS.ACTION_REQUEST_UNBLOCK, generateConfirmActionHandler(requestUnblockUser)],\n        ];\n\n        AutoRows.init(footer);\n\n        if (emojiAutoCompleteContainer.length) {\n            initialiseEmojiAutoComplete(\n                emojiAutoCompleteContainer[0],\n                messageTextArea[0],\n                function(hasSuggestions) {\n                    var newState = StateManager.setShowEmojiAutoComplete(viewState, hasSuggestions);\n                    render(newState);\n                },\n                function(emoji) {\n                    var newState = StateManager.setShowEmojiAutoComplete(viewState, false);\n                    render(newState);\n\n                    messageTextArea.focus();\n                    var cursorPos = messageTextArea.prop('selectionStart');\n                    var currentText = messageTextArea.val();\n                    var textBefore = currentText.substring(0, cursorPos).replace(/\\S*$/, '');\n                    var textAfter = currentText.substring(cursorPos).replace(/^\\S*/, '');\n\n                    messageTextArea.val(textBefore + emoji + textAfter);\n                    // Set the cursor position to after the inserted emoji.\n                    messageTextArea.prop('selectionStart', textBefore.length + emoji.length);\n                    messageTextArea.prop('selectionEnd', textBefore.length + emoji.length);\n                }\n            );\n        }\n\n        if (emojiPickerElement.length) {\n            initialiseEmojiPicker(emojiPickerElement[0], function(emoji) {\n                var newState = StateManager.setShowEmojiPicker(viewState, !viewState.showEmojiPicker);\n                render(newState);\n\n                messageTextArea.focus();\n                var cursorPos = messageTextArea.prop('selectionStart');\n                var currentText = messageTextArea.val();\n                var textBefore = currentText.substring(0, cursorPos);\n                var textAfter = currentText.substring(cursorPos, currentText.length);\n\n                messageTextArea.val(textBefore + emoji + textAfter);\n                // Set the cursor position to after the inserted emoji.\n                messageTextArea.prop('selectionStart', cursorPos + emoji.length);\n                messageTextArea.prop('selectionEnd', cursorPos + emoji.length);\n            });\n        }\n\n        CustomEvents.define(header, [\n            CustomEvents.events.activate\n        ]);\n        CustomEvents.define(body, [\n            CustomEvents.events.activate\n        ]);\n        CustomEvents.define(footer, [\n            CustomEvents.events.activate,\n            CustomEvents.events.enter,\n            CustomEvents.events.escape\n        ]);\n        CustomEvents.define(messagesContainer, [\n            CustomEvents.events.scrollTop,\n            CustomEvents.events.scrollLock\n        ]);\n\n        messagesContainer.on(CustomEvents.events.scrollTop, function(e, data) {\n            var hasMembers = Object.keys(viewState.members).length > 1;\n\n            if (!isResetting && !isLoadingMoreMessages && !hasLoadedAllMessages() && hasMembers) {\n                isLoadingMoreMessages = true;\n                var newState = StateManager.setLoadingMessages(viewState, true);\n                render(newState);\n\n                loadMessages(viewState.id, LOAD_MESSAGE_LIMIT, getMessagesOffset(), NEWEST_FIRST, [])\n                    .then(function() {\n                        isLoadingMoreMessages = false;\n                        setMessagesOffset(getMessagesOffset() + LOAD_MESSAGE_LIMIT);\n                        return;\n                    })\n                    .catch(function(error) {\n                        isLoadingMoreMessages = false;\n                        Notification.exception(error);\n                    });\n            }\n\n            data.originalEvent.preventDefault();\n        });\n\n        headerActivateHandlers.forEach(function(handler) {\n            var selector = handler[0];\n            var handlerFunction = handler[1];\n            header.on(CustomEvents.events.activate, selector, handlerFunction);\n        });\n\n        bodyActivateHandlers.forEach(function(handler) {\n            var selector = handler[0];\n            var handlerFunction = handler[1];\n            body.on(CustomEvents.events.activate, selector, handlerFunction);\n        });\n\n        footerActivateHandlers.forEach(function(handler) {\n            var selector = handler[0];\n            var handlerFunction = handler[1];\n            footer.on(CustomEvents.events.activate, selector, handlerFunction);\n        });\n\n        footer.on(CustomEvents.events.enter, SELECTORS.MESSAGE_TEXT_AREA, function(e, data) {\n            var enterToSend = footer.attr('data-enter-to-send');\n            if (enterToSend && enterToSend != 'false' && enterToSend != '0') {\n                handleSendMessage(e, data);\n            }\n        });\n\n        footer.on(CustomEvents.events.escape, SELECTORS.EMOJI_PICKER_CONTAINER, handleToggleEmojiPicker);\n        $(document.body).on('click', handleCloseEmojiPicker);\n\n        PubSub.subscribe(MessageDrawerEvents.ROUTE_CHANGED, function(newRouteData) {\n            if (newMessagesPollTimer) {\n                if (newRouteData.route != MessageDrawerRoutes.VIEW_CONVERSATION) {\n                    newMessagesPollTimer.stop();\n                }\n            }\n        });\n    };\n\n    /**\n     * Reset the timer that polls for new messages.\n     *\n     * @param  {Number} conversationId The conversation id\n     */\n    var resetMessagePollTimer = function(conversationId) {\n        if (newMessagesPollTimer) {\n            newMessagesPollTimer.stop();\n        }\n\n        newMessagesPollTimer = new BackOffTimer(\n            getLoadNewMessagesCallback(conversationId, NEWEST_FIRST),\n            BackOffTimer.getIncrementalCallback(\n                viewState.messagePollMin * MILLISECONDS_IN_SEC,\n                MILLISECONDS_IN_SEC,\n                viewState.messagePollMax * MILLISECONDS_IN_SEC,\n                viewState.messagePollAfterMax * MILLISECONDS_IN_SEC\n            )\n        );\n\n        newMessagesPollTimer.start();\n    };\n\n    /**\n     * Reset the state to the initial state and render the UI.\n     *\n     * @param  {Object} body Conversation body container element.\n     * @param  {Number|null} conversationId The conversation id.\n     * @param  {Object} loggedInUserProfile The logged in user's profile.\n     */\n    var resetState = function(body, conversationId, loggedInUserProfile) {\n        // Reset all of the states back to the beginning if we're loading a new\n        // conversation.\n        if (newMessagesPollTimer) {\n            newMessagesPollTimer.stop();\n        }\n        loadedAllMessages = false;\n        messagesOffset = 0;\n        newMessagesPollTimer = null;\n        isRendering = false;\n        renderBuffer = [];\n        isResetting = true;\n        isSendingMessage = false;\n        isDeletingConversationContent = false;\n        sendMessageBuffer = [];\n\n        var loggedInUserId = loggedInUserProfile.id;\n        var midnight = parseInt(body.attr('data-midnight'), 10);\n        var messagePollMin = parseInt(body.attr('data-message-poll-min'), 10);\n        var messagePollMax = parseInt(body.attr('data-message-poll-max'), 10);\n        var messagePollAfterMax = parseInt(body.attr('data-message-poll-after-max'), 10);\n        var initialState = StateManager.buildInitialState(\n            midnight,\n            loggedInUserId,\n            conversationId,\n            messagePollMin,\n            messagePollMax,\n            messagePollAfterMax\n        );\n\n        if (!viewState) {\n            viewState = initialState;\n        }\n\n        render(initialState);\n    };\n\n    /**\n     * Load a new empty private conversation between two users or self-conversation.\n     *\n     * @param  {Object} body Conversation body container element.\n     * @param  {Object} loggedInUserProfile The logged in user's profile.\n     * @param  {Int} otherUserId The other user's id.\n     * @return {Promise} Renderer promise.\n     */\n    var resetNoConversation = function(body, loggedInUserProfile, otherUserId) {\n        // Always reset the state back to the initial state so that the\n        // state manager and patcher can work correctly.\n        resetState(body, null, loggedInUserProfile);\n\n        var resetNoConversationPromise = null;\n\n        if (loggedInUserProfile.id != otherUserId) {\n            // Private conversation between two different users.\n            resetNoConversationPromise = Repository.getConversationBetweenUsers(\n                loggedInUserProfile.id,\n                otherUserId,\n                true,\n                true,\n                0,\n                0,\n                LOAD_MESSAGE_LIMIT,\n                0,\n                NEWEST_FIRST\n            );\n        } else {\n            // Self conversation.\n            resetNoConversationPromise = Repository.getSelfConversation(\n                loggedInUserProfile.id,\n                LOAD_MESSAGE_LIMIT,\n                0,\n                NEWEST_FIRST\n            );\n        }\n\n        return resetNoConversationPromise.then(function(conversation) {\n                // Looks like we have a conversation after all! Let's use that.\n                return resetByConversation(body, conversation, loggedInUserProfile);\n            })\n            .catch(function() {\n                // Can't find a conversation. Oh well. Just load up a blank one.\n                return loadEmptyPrivateConversation(loggedInUserProfile, otherUserId);\n            });\n    };\n\n    /**\n     * Load new messages into the conversation based on a time interval.\n     *\n     * @param  {Object} body Conversation body container element.\n     * @param  {Number} conversationId The conversation id.\n     * @param  {Object} loggedInUserProfile The logged in user's profile.\n     * @return {Promise} Renderer promise.\n     */\n    var resetById = function(body, conversationId, loggedInUserProfile) {\n        var cache = null;\n        if (conversationId in stateCache) {\n            cache = stateCache[conversationId];\n        }\n\n        // Always reset the state back to the initial state so that the\n        // state manager and patcher can work correctly.\n        resetState(body, conversationId, loggedInUserProfile);\n\n        var promise = $.Deferred().resolve({}).promise();\n        if (cache) {\n            // We've seen this conversation before so there is no need to\n            // send any network requests.\n            var newState = cache.state;\n            // Reset some loading states just in case they were left weirdly.\n            newState = StateManager.setLoadingMessages(newState, false);\n            newState = StateManager.setLoadingMembers(newState, false);\n            setMessagesOffset(cache.messagesOffset);\n            setLoadedAllMessages(cache.loadedAllMessages);\n            render(newState);\n        } else {\n            promise = loadNewConversation(\n                conversationId,\n                loggedInUserProfile,\n                LOAD_MESSAGE_LIMIT,\n                0,\n                NEWEST_FIRST\n            );\n        }\n\n        return promise.then(function() {\n            return resetMessagePollTimer(conversationId);\n        });\n    };\n\n    /**\n     * Load new messages into the conversation based on a time interval.\n     *\n     * @param  {Object} body Conversation body container element.\n     * @param  {Object} conversation The conversation.\n     * @param  {Object} loggedInUserProfile The logged in user's profile.\n     * @return {Promise} Renderer promise.\n     */\n    var resetByConversation = function(body, conversation, loggedInUserProfile) {\n        var cache = null;\n        if (conversation.id in stateCache) {\n            cache = stateCache[conversation.id];\n        }\n\n        // Always reset the state back to the initial state so that the\n        // state manager and patcher can work correctly.\n        resetState(body, conversation.id, loggedInUserProfile);\n\n        var promise = $.Deferred().resolve({}).promise();\n        if (cache) {\n            // We've seen this conversation before so there is no need to\n            // send any network requests.\n            var newState = cache.state;\n            // Reset some loading states just in case they were left weirdly.\n            newState = StateManager.setLoadingMessages(newState, false);\n            newState = StateManager.setLoadingMembers(newState, false);\n            setMessagesOffset(cache.messagesOffset);\n            setLoadedAllMessages(cache.loadedAllMessages);\n            render(newState);\n        } else {\n            promise = loadExistingConversation(\n                conversation,\n                loggedInUserProfile,\n                LOAD_MESSAGE_LIMIT,\n                NEWEST_FIRST\n            );\n        }\n\n        return promise.then(function() {\n            return resetMessagePollTimer(conversation.id);\n        });\n    };\n\n    /**\n     * Setup the conversation page. This is a rather complex function because there are a\n     * few combinations of arguments that can be provided to this function to show the\n     * conversation.\n     *\n     * There are:\n     * 1.) A conversation object with no action or other user id (e.g. from the overview page)\n     * 2.) A conversation id with no action or other user id (e.g. from the contacts page)\n     * 3.) No conversation/id with an action and other other user id. (e.g. from contact page)\n     *\n     * @param {string} namespace The route namespace.\n     * @param {Object} header Conversation header container element.\n     * @param {Object} body Conversation body container element.\n     * @param {Object} footer Conversation footer container element.\n     * @param {Object|Number|null} conversationOrId Conversation or id or null\n     * @param {String} action An action to take on the conversation\n     * @param {Number} otherUserId The other user id for a private conversation\n     * @return {Object} jQuery promise\n     */\n    var show = function(namespace, header, body, footer, conversationOrId, action, otherUserId) {\n        var conversation = null;\n        var conversationId = null;\n\n        // Check what we were given to identify the conversation.\n        if (conversationOrId && conversationOrId !== null && typeof conversationOrId == 'object') {\n            conversation = conversationOrId;\n            conversationId = parseInt(conversation.id, 10);\n        } else {\n            conversation = null;\n            conversationId = parseInt(conversationOrId, 10);\n            conversationId = isNaN(conversationId) ? null : conversationId;\n        }\n\n        if (!conversationId && action && otherUserId) {\n            // If we didn't get a conversation id got a user id then let's see if we've\n            // previously loaded a private conversation with this user.\n            conversationId = getCachedPrivateConversationIdFromUserId(otherUserId);\n        }\n\n        // This is a new conversation if:\n        // 1. We don't already have a state\n        // 2. The given conversation doesn't match the one currently loaded\n        // 3. We have a view state without a conversation id and we weren't given one\n        //    but we were given a different other user id. This happens when the user\n        //    goes from viewing a user that they haven't yet initialised a conversation\n        //    with to viewing a different user that they also haven't initialised a\n        //    conversation with.\n        var isNewConversation = !viewState || (viewState.id != conversationId) || (otherUserId && otherUserId != getOtherUserId());\n\n        if (!body.attr('data-init')) {\n            // Generate the render function to bind the header, body, and footer\n            // elements to it so that we don't need to pass them around this module.\n            render = generateRenderFunction(header, body, footer, isNewConversation);\n            registerEventListeners(namespace, header, body, footer);\n            body.attr('data-init', true);\n        }\n\n        if (isNewConversation) {\n            var renderPromise = null;\n            var loggedInUserProfile = getLoggedInUserProfile(body);\n\n            if (conversation) {\n                renderPromise = resetByConversation(body, conversation, loggedInUserProfile, otherUserId);\n            } else if (conversationId) {\n                renderPromise = resetById(body, conversationId, loggedInUserProfile, otherUserId);\n            } else {\n                renderPromise = resetNoConversation(body, loggedInUserProfile, otherUserId);\n            }\n\n            return renderPromise\n                .then(function() {\n                    isResetting = false;\n                    // Focus the first element that can receieve it in the header.\n                    header.find(Constants.SELECTORS.CAN_RECEIVE_FOCUS).first().focus();\n                    return;\n                })\n                .catch(function(error) {\n                    isResetting = false;\n                    Notification.exception(error);\n                });\n        }\n\n        // We're not loading a new conversation so we should reset the poll timer to try to load\n        // new messages.\n        resetMessagePollTimer(conversationId);\n\n        if (viewState.type == CONVERSATION_TYPES.PRIVATE && action) {\n            // There are special actions that the user can perform in a private (aka 1-to-1)\n            // conversation.\n            var currentOtherUserId = getOtherUserId();\n\n            switch (action) {\n                case 'block':\n                    return requestBlockUser(currentOtherUserId);\n                case 'unblock':\n                    return requestUnblockUser(currentOtherUserId);\n                case 'add-contact':\n                    return requestAddContact(currentOtherUserId);\n                case 'remove-contact':\n                    return requestRemoveContact(currentOtherUserId);\n            }\n        }\n\n        // Final fallback to return a promise if we didn't need to do anything.\n        return $.Deferred().resolve().promise();\n    };\n\n    /**\n     * String describing this page used for aria-labels.\n     *\n     * @return {Object} jQuery promise\n     */\n    var description = function() {\n        return Str.get_string('messagedrawerviewconversation', 'core_message', viewState.name);\n    };\n\n    return {\n        show: show,\n        description: description\n    };\n});\n"],"names":["define","$","AutoRows","BackOffTimer","CustomEvents","Notification","Pending","PubSub","Str","Repository","MessageDrawerEvents","Constants","Patcher","Renderer","StateManager","MessageDrawerRouter","MessageDrawerRoutes","initialiseEmojiAutoComplete","initialiseEmojiPicker","stateCache","viewState","loadedAllMessages","messagesOffset","newMessagesPollTimer","isRendering","renderBuffer","isResetting","isSendingMessage","isDeletingConversationContent","sendMessageBuffer","render","renderers","NEWEST_FIRST","NEWEST_MESSAGES_FIRST","LOAD_MESSAGE_LIMIT","MILLISECONDS_IN_SEC","SELECTORS","CONVERSATION_TYPES","getOtherUserId","type","PUBLIC","loggedInUserId","SELF","otherUserIds","Object","keys","members","filter","userId","length","getMessagesOffset","setMessagesOffset","value","id","hasLoadedAllMessages","setLoadedAllMessages","formatConversationForEvent","state","name","subname","imageUrl","isFavourite","isMuted","totalMemberCount","messages","map","message","extend","formattedMember","contactrequests","request","updateStateFromConversation","conversation","otherUser","PRIVATE","otherUsers","member","imageurl","fullname","profileimageurl","newState","addMembers","setName","setSubname","setType","setImageUrl","setTotalMemberCount","membercount","setIsFavourite","isfavourite","setIsMuted","ismuted","addMessages","setCanDeleteMessagesForAllUsers","candeletemessagesforallusers","loadMessages","conversationId","limit","offset","newestFirst","ignoreList","timeFrom","getMessages","then","result","indexOf","parseInt","slice","membersToAdd","setLoadingMessages","catch","error","markConversationAsRead","pendingPromise","markAllConversationMessagesAsRead","markMessagesAsRead","publish","CONVERSATION_READ","resolve","requestBlockUser","cancelRequest","addPendingBlockUsersById","blockUser","setLoadingConfirmAction","profile","removePendingBlockUsersById","CONTACT_BLOCKED","requestUnblockUser","addPendingUnblockUsersById","unblockUser","removePendingUnblockUsersById","CONTACT_UNBLOCKED","requestRemoveContact","addPendingRemoveContactsById","removeContact","deleteContacts","profiles","removePendingRemoveContactsById","CONTACT_REMOVED","requestAddContact","addPendingAddContactsById","addContact","createContactRequest","response","Error","warnings","removePendingAddContactsById","addContactRequests","requestDeleteSelectedMessages","selectedMessageIds","addPendingDeleteMessagesById","deleteSelectedMessages","messageIds","pendingDeleteMessageIds","sentMessages","sendState","deleteMessagesPromise","Deferred","promise","sentMessageIds","deleteMessagesForAllUsers","deleteMessages","stop","removeMessagesById","removePendingDeleteMessagesById","removeSelectedMessagesById","setDeleteMessagesForAllUsers","prevLastMessage","newLastMessage","CONVERSATION_NEW_LAST_MESSAGE","CONVERSATION_DELETED","exception","requestDeleteConversation","setPendingDeleteConversation","deleteConversation","removeMessages","acceptContactRequest","requests","requesteduserid","removeContactRequests","CONTACT_ADDED","CONTACT_REQUEST_ACCEPTED","declineContactRequest","CONTACT_REQUEST_DECLINED","processSendMessageBuffer","messagesToSend","newConversationId","messagesText","text","sendMessagePromise","newCanDeleteMessagesForAllUsers","sendMessagesToConversation","otherUserId","sendMessagesToUser","conversationid","newMessageIds","data","selectedToRemove","selectedToAdd","forEach","oldMessage","index","newMessage","push","updateMessages","setMessagesSendSuccessById","addSelectedMessagesById","setId","resetMessagePollTimer","CONVERSATION_CREATED","e","errorMessage","get_string","handleFailedMessages","setMessagesSendFailById","finalError","previewText","plaintext","replace","processRenderBuffer","header","body","footer","renderable","shift","renderPromises","renderFunc","patch","when","apply","deferred","reject","generateConfirmActionHandler","actionCallback","loadingConfirmAction","originalEvent","preventDefault","handleSendMessage","textArea","target","closest","FOOTER_CONTAINER","find","MESSAGE_TEXT_AREA","val","trim","Date","now","loadingmessage","useridfrom","timecreated","sendMessage","focus","handleSelectMessage","selection","window","getSelection","toString","is","messageId","toggleSelectMessage","MESSAGE","attr","handleRetrySendMessage","setMessagesSendPendingById","retrySendMessage","stopPropagation","handleCancelEditMode","cancelEditMode","generateHandleViewContact","namespace","go","VIEW_CONTACT","handleSetFavourite","setFavouriteConversations","CONVERSATION_SET_FAVOURITE","handleUnsetFavourite","unsetFavouriteConversations","CONVERSATION_UNSET_FAVOURITE","handleSetMuted","setMutedConversations","CONVERSATION_SET_MUTED","handleUnsetMuted","unsetMutedConversations","CONVERSATION_UNSET_MUTED","handleDeleteMessagesForAllUsersToggle","newValue","prop","generateHandleViewGroupInfo","VIEW_GROUP_INFO","handleToggleEmojiPicker","setShowEmojiPicker","showEmojiPicker","handleCloseEmojiPicker","EMOJI_PICKER_CONTAINER","TOGGLE_EMOJI_PICKER_BUTTON","registerEventListeners","isLoadingMoreMessages","messagesContainer","MESSAGES_CONTAINER","getMessagesContainer","emojiPickerElement","EMOJI_PICKER","emojiAutoCompleteContainer","EMOJI_AUTO_COMPLETE_CONTAINER","messageTextArea","headerActivateHandlers","ACTION_REQUEST_BLOCK","ACTION_REQUEST_UNBLOCK","ACTION_REQUEST_ADD_CONTACT","ACTION_REQUEST_REMOVE_CONTACT","ACTION_REQUEST_DELETE_CONVERSATION","ACTION_CANCEL_EDIT_MODE","ACTION_VIEW_CONTACT","ACTION_VIEW_GROUP_INFO","ACTION_CONFIRM_FAVOURITE","ACTION_CONFIRM_MUTE","ACTION_CONFIRM_UNFAVOURITE","ACTION_CONFIRM_UNMUTE","bodyActivateHandlers","ACTION_CANCEL_CONFIRM","ACTION_CONFIRM_BLOCK","ACTION_CONFIRM_UNBLOCK","ACTION_CONFIRM_ADD_CONTACT","ACTION_CONFIRM_REMOVE_CONTACT","ACTION_CONFIRM_DELETE_SELECTED_MESSAGES","ACTION_CONFIRM_DELETE_CONVERSATION","ACTION_OKAY_CONFIRM","ACTION_ACCEPT_CONTACT_REQUEST","ACTION_DECLINE_CONTACT_REQUEST","DELETE_MESSAGES_FOR_ALL_USERS_TOGGLE","RETRY_SEND","footerActivateHandlers","SEND_MESSAGE_BUTTON","ACTION_REQUEST_DELETE_SELECTED_MESSAGES","init","hasSuggestions","setShowEmojiAutoComplete","emoji","cursorPos","currentText","textBefore","substring","textAfter","events","activate","enter","escape","scrollTop","scrollLock","on","hasMembers","handler","selector","handlerFunction","enterToSend","document","subscribe","ROUTE_CHANGED","newRouteData","route","VIEW_CONVERSATION","mostRecentMessage","lastTimeCreated","timeCreated","ignoreMessageIds","i","restart","getLoadNewMessagesCallback","getIncrementalCallback","messagePollMin","messagePollMax","messagePollAfterMax","start","resetState","loggedInUserProfile","midnight","initialState","buildInitialState","resetNoConversation","getConversationBetweenUsers","getSelfConversation","resetByConversation","conversationType","setLoadingMembers","getMemberInfo","loadEmptyPrivateConversation","resetById","cache","messageLimit","messageOffset","getConversation","concat","loadNewConversation","messageCount","hasLoadedEnoughMessages","loadExistingConversation","show","conversationOrId","action","isNaN","reduce","carry","isNewConversation","rendererFunc","buildPatch","generateRenderFunction","profileimageurlsmall","isonline","showonlinestatus","isblocked","iscontact","isdeleted","canmessage","canmessageevenifblocked","requirescontact","getLoggedInUserProfile","CAN_RECEIVE_FOCUS","first","currentOtherUserId","description"],"mappings":"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"}