diff --git a/install/package.json b/install/package.json index 844767b426..ab09451b01 100644 --- a/install/package.json +++ b/install/package.json @@ -100,10 +100,10 @@ "nodebb-plugin-ntfy": "1.0.15", "nodebb-plugin-spam-be-gone": "2.0.6", "nodebb-rewards-essentials": "0.2.3", - "nodebb-theme-harmony": "1.0.4", + "nodebb-theme-harmony": "1.0.5", "nodebb-theme-lavender": "7.0.9", - "nodebb-theme-peace": "2.0.19", - "nodebb-theme-persona": "13.0.56", + "nodebb-theme-peace": "2.0.20", + "nodebb-theme-persona": "13.0.57", "nodebb-widget-essentials": "7.0.11", "nodemailer": "6.9.1", "nprogress": "0.2.0", diff --git a/public/language/en-GB/global.json b/public/language/en-GB/global.json index 2fd5e724bf..c5dee756b7 100644 --- a/public/language/en-GB/global.json +++ b/public/language/en-GB/global.json @@ -24,6 +24,7 @@ "save_changes": "Save Changes", "save": "Save", + "cancel": "Cancel", "close": "Close", "pagination": "Pagination", diff --git a/public/src/client/chats.js b/public/src/client/chats.js index 4c54e750a4..df57760977 100644 --- a/public/src/client/chats.js +++ b/public/src/client/chats.js @@ -379,7 +379,7 @@ define('forum/chats', [ }); }; - Chats.createAutoComplete = function (roomId, element) { + Chats.createAutoComplete = function (roomId, element, options = {}) { if (!element.length) { return; } @@ -395,12 +395,17 @@ define('forum/chats', [ }, placement: 'top', className: `chat-autocomplete-dropdown-${roomId} dropdown-menu textcomplete-dropdown`, + ...options, }, }; $(window).trigger('chat:autocomplete:init', data); if (data.strategies.length) { - Chats.activeAutocomplete[roomId] = autocomplete.setup(data); + const autocompleteEl = autocomplete.setup(data); + if (roomId) { + Chats.activeAutocomplete[roomId] = autocompleteEl; + } + return autocompleteEl; } }; diff --git a/public/src/client/chats/messages.js b/public/src/client/chats/messages.js index a0c05cae9f..e3bccacdca 100644 --- a/public/src/client/chats/messages.js +++ b/public/src/client/chats/messages.js @@ -9,47 +9,34 @@ define('forum/chats/messages', [ messages.sendMessage = async function (roomId, inputEl) { let message = inputEl.val(); - let mid = inputEl.attr('data-mid'); - if (!message.trim().length) { return; } const chatContent = inputEl.parents(`[component="chat/messages"][data-roomid="${roomId}"]`); inputEl.val('').trigger('input'); - inputEl.removeAttr('data-mid'); + messages.updateRemainingLength(inputEl.parent()); messages.updateTextAreaHeight(chatContent); - const payload = { roomId, message, mid }; - ({ roomId, message, mid } = await hooks.fire('filter:chat.send', payload)); + const payload = { roomId, message }; + ({ roomId, message } = await hooks.fire('filter:chat.send', payload)); - if (!mid) { - api.post(`/chats/${roomId}`, { message }).then(() => { - hooks.fire('action:chat.sent', { roomId, message, mid }); - }).catch((err) => { - inputEl.val(message).trigger('input'); - messages.updateRemainingLength(inputEl.parent()); - if (err.message === '[[error:email-not-confirmed-chat]]') { - return messagesModule.showEmailConfirmWarning(err.message); - } + api.post(`/chats/${roomId}`, { message }).then(() => { + hooks.fire('action:chat.sent', { roomId, message }); + }).catch((err) => { + inputEl.val(message).trigger('input'); + messages.updateRemainingLength(inputEl.parent()); + if (err.message === '[[error:email-not-confirmed-chat]]') { + return messagesModule.showEmailConfirmWarning(err.message); + } - return alerts.alert({ - alert_id: 'chat_spam_error', - title: '[[global:alert.error]]', - message: err.message, - type: 'danger', - timeout: 10000, - }); - }); - } else { - api.put(`/chats/${roomId}/messages/${mid}`, { message }).then(() => { - hooks.fire('action:chat.edited', { roomId, message, mid }); - }).catch((err) => { - inputEl.val(message).trigger('input'); - inputEl.attr('data-mid', mid); - messages.updateRemainingLength(inputEl.parent()); - return alerts.error(err); + return alerts.alert({ + alert_id: 'chat_spam_error', + title: '[[global:alert.error]]', + message: err.message, + type: 'danger', + timeout: 10000, }); - } + }); }; messages.updateRemainingLength = function (parent) { @@ -76,6 +63,15 @@ define('forum/chats/messages', [ }); }; + function autoresizeTextArea(textarea) { + const scrollHeight = textarea.prop('scrollHeight'); + textarea.css({ height: scrollHeight + 'px' }); + textarea.on('input', function () { + textarea.css({ height: 0 }); + textarea.css({ height: textarea.prop('scrollHeight') + 'px' }); + }); + } + messages.appendChatMessage = function (chatContentEl, data) { const lastSpeaker = parseInt(chatContentEl.find('.chat-message').last().attr('data-uid'), 10); const lasttimestamp = parseInt(chatContentEl.find('.chat-message').last().attr('data-timestamp'), 10); @@ -145,24 +141,63 @@ define('forum/chats/messages', [ .toggleClass('hidden', isAtBottom); }; - messages.prepEdit = function (inputEl, messageId, roomId) { - socket.emit('modules.chats.getRaw', { mid: messageId, roomId: roomId }, function (err, raw) { - if (err) { - return alerts.error(err); + messages.prepEdit = async function (inputEl, mid, roomId) { + const raw = await socket.emit('modules.chats.getRaw', { mid: mid, roomId: roomId }); + const editEl = await app.parseAndTranslate('partials/chats/edit-message', { + rawContent: raw, + }); + const messageBody = $(`[data-roomid="${roomId}"] [data-mid="${mid}"] [component="chat/message/body"]`); + const messageControls = $(`[data-roomid="${roomId}"] [data-mid="${mid}"] [component="chat/message/controls"]`); + const chatContent = messageBody.parents('.chat-content'); + + messageBody.addClass('hidden'); + messageControls.addClass('hidden'); + editEl.insertAfter(messageBody); + + const textarea = editEl.find('textarea'); + + textarea.focus().putCursorAtEnd(); + autoresizeTextArea(textarea); + + if (messages.isAtBottom(chatContent)) { + messages.scrollToBottom(chatContent); + } + + const chats = await app.require('forum/chats'); + const autoCompleteEl = chats.createAutoComplete(0, textarea, { + placement: 'bottom', + }); + + function finishEdit() { + messageBody.removeClass('hidden'); + messageControls.removeClass('hidden'); + editEl.remove(); + if (autoCompleteEl) { + autoCompleteEl.destroy(); } - // Populate the input field with the raw message content - if (inputEl.val().length === 0) { - // By setting the `data-mid` attribute, I tell the chat code that I am editing a - // message, instead of posting a new one. - inputEl.attr('data-mid', messageId).addClass('editing'); - inputEl.val(raw).trigger('input').focus(); - - hooks.fire('action:chat.prepEdit', { - inputEl: inputEl, - messageId: messageId, - roomId: roomId, - }); + } + editEl.find('[data-action="cancel"]').on('click', finishEdit); + + editEl.find('[data-action="save"]').on('click', function () { + const message = textarea.val(); + if (!message.trim().length) { + return; } + api.put(`/chats/${roomId}/messages/${mid}`, { message }).then(() => { + finishEdit(); + hooks.fire('action:chat.edited', { roomId, message, mid }); + }).catch((err) => { + textarea.val(message).trigger('input'); + alerts.error(err); + }); + }); + + hooks.fire('action:chat.prepEdit', { + inputEl: inputEl, + messageId: mid, + roomId: roomId, + editEl: editEl, + messageBody: messageBody, }); }; diff --git a/src/views/partials/chats/edit-message.tpl b/src/views/partials/chats/edit-message.tpl new file mode 100644 index 0000000000..4777eff031 --- /dev/null +++ b/src/views/partials/chats/edit-message.tpl @@ -0,0 +1,7 @@ +
+ +
+ + +
+
\ No newline at end of file