diff --git a/install/package.json b/install/package.json index b0c7f40171..6aac8d3c23 100644 --- a/install/package.json +++ b/install/package.json @@ -93,7 +93,7 @@ "nconf": "0.12.0", "nodebb-plugin-2factor": "7.1.3", "nodebb-plugin-composer-default": "10.2.6", - "nodebb-plugin-dbsearch": "6.1.0", + "nodebb-plugin-dbsearch": "6.2.0", "nodebb-plugin-emoji": "5.1.3", "nodebb-plugin-emoji-android": "4.0.0", "nodebb-plugin-markdown": "12.1.7", @@ -101,10 +101,10 @@ "nodebb-plugin-ntfy": "1.1.0", "nodebb-plugin-spam-be-gone": "2.1.1", "nodebb-rewards-essentials": "0.2.3", - "nodebb-theme-harmony": "1.1.16", + "nodebb-theme-harmony": "1.1.17", "nodebb-theme-lavender": "7.1.3", - "nodebb-theme-peace": "2.1.4", - "nodebb-theme-persona": "13.2.8", + "nodebb-theme-peace": "2.1.5", + "nodebb-theme-persona": "13.2.9", "nodebb-widget-essentials": "7.0.13", "nodemailer": "6.9.4", "nprogress": "0.2.0", diff --git a/public/src/client/chats.js b/public/src/client/chats.js index b329310bd8..7dab41cd22 100644 --- a/public/src/client/chats.js +++ b/public/src/client/chats.js @@ -9,6 +9,7 @@ define('forum/chats', [ 'forum/chats/manage', 'forum/chats/messages', 'forum/chats/user-list', + 'forum/chats/message-search', 'composer/autocomplete', 'hooks', 'bootbox', @@ -17,10 +18,9 @@ define('forum/chats', [ 'api', 'uploadHelpers', ], function ( - components, mousetrap, - recentChats, create, manage, messages, - userList, autocomplete, hooks, bootbox, - alerts, chatModule, api, uploadHelpers + components, mousetrap, recentChats, create, + manage, messages, userList, messageSearch, autocomplete, + hooks, bootbox, alerts, chatModule, api, uploadHelpers ) { const Chats = { initialised: false, @@ -62,8 +62,8 @@ define('forum/chats', [ } Chats.initialised = true; - messages.scrollToBottom($('.expanded-chat ul.chat-content')); - messages.wrapImagesInLinks($('.expanded-chat ul.chat-content')); + messages.scrollToBottom($('[component="chat/message/content"]')); + messages.wrapImagesInLinks($('[component="chat/message/content"]')); create.init(); hooks.fire('action:chat.loaded', $('.chats-full')); @@ -80,8 +80,8 @@ define('forum/chats', [ Chats.addRenameHandler(roomId, chatControls.find('[data-action="rename"]')); Chats.addLeaveHandler(roomId, chatControls.find('[data-action="leave"]')); Chats.addDeleteHandler(roomId, chatControls.find('[data-action="delete"]')); - Chats.addScrollHandler(roomId, ajaxify.data.uid, $('.chat-content')); - Chats.addScrollBottomHandler($('.chat-content')); + Chats.addScrollHandler(roomId, ajaxify.data.uid, $('[component="chat/message/content"]')); + Chats.addScrollBottomHandler($('[component="chat/message/content"]')); Chats.addCharactersLeftHandler(mainWrapper); Chats.addTextareaResizeHandler(mainWrapper); Chats.addIPHandler(mainWrapper); @@ -98,6 +98,7 @@ define('forum/chats', [ Chats.switchChat(); }); userList.init(roomId, mainWrapper); + messageSearch.init(roomId); Chats.addPublicRoomSortHandler(); Chats.addTooltipHandler(); Chats.addNotificationSettingHandler(); @@ -268,24 +269,24 @@ define('forum/chats', [ // https://stackoverflow.com/questions/454202/creating-a-textarea-with-auto-resize const textarea = parent.find('[component="chat/input"]'); textarea.on('input', function () { - const isAtBottom = messages.isAtBottom(parent.find('.chat-content')); + const isAtBottom = messages.isAtBottom(parent.find('[component="chat/message/content"]')); textarea.css({ height: 0 }); textarea.css({ height: messages.calcAutoTextAreaHeight(textarea) + 'px' }); if (isAtBottom) { - messages.scrollToBottom(parent.find('.chat-content')); + messages.scrollToBottom(parent.find('[component="chat/message/content"]')); } }); }; Chats.addActionHandlers = function (element, roomId) { element.on('click', '[data-mid] [data-action]', function () { - const messageId = $(this).parents('[data-mid]').attr('data-mid'); + const msgEl = $(this).parents('[data-mid]'); + const messageId = msgEl.attr('data-mid'); const action = this.getAttribute('data-action'); switch (action) { case 'edit': { - const inputEl = $('[data-roomid="' + roomId + '"] [component="chat/input"]'); - messages.prepEdit(inputEl, messageId, roomId); + messages.prepEdit(msgEl, messageId, roomId); break; } case 'delete': @@ -509,7 +510,7 @@ define('forum/chats', [ Chats.setActive(roomId); Chats.addEventListeners(); hooks.fire('action:chat.loaded', $('.chats-full')); - messages.scrollToBottom(mainWrapper.find('.expanded-chat ul.chat-content')); + messages.scrollToBottom(mainWrapper.find('[component="chat/message/content"]')); if (history.pushState) { history.pushState({ url: url, @@ -543,7 +544,7 @@ define('forum/chats', [ data.message.self = data.self; data.message.timestamp = Math.min(Date.now(), data.message.timestamp); data.message.timestampISO = utils.toISOString(data.message.timestamp); - messages.appendChatMessage($('.expanded-chat .chat-content'), data.message); + messages.appendChatMessage($('[component="chat/message/content"]'), data.message); } }); diff --git a/public/src/client/chats/create.js b/public/src/client/chats/create.js index 6f5077e2e1..f9e48c2f13 100644 --- a/public/src/client/chats/create.js +++ b/public/src/client/chats/create.js @@ -2,8 +2,8 @@ define('forum/chats/create', [ - 'components', 'api', 'alerts', 'forum/chats/search', -], function (components, api, alerts, search) { + 'components', 'api', 'alerts', 'forum/chats/user-search', +], function (components, api, alerts, userSearch) { const create = {}; create.init = function () { components.get('chat/create').on('click', handleCreate); @@ -65,7 +65,7 @@ define('forum/chats/create', [ const chatRoomUsersList = modal.find('[component="chat/room/users"]'); - search.init({ + userSearch.init({ onSelect: async function (user) { const html = await app.parseAndTranslate('modals/create-room', 'selectedUsers', { selectedUsers: [user] }); chatRoomUsersList.append(html); diff --git a/public/src/client/chats/message-search.js b/public/src/client/chats/message-search.js new file mode 100644 index 0000000000..1da91577fc --- /dev/null +++ b/public/src/client/chats/message-search.js @@ -0,0 +1,82 @@ +'use strict'; + + +define('forum/chats/message-search', [ + 'components', 'alerts', 'forum/chats/messages', +], function (components, alerts, messages) { + const messageSearch = {}; + let roomId = 0; + let resultListEl; + let chatContent; + let clearEl; + + messageSearch.init = function (_roomId) { + roomId = _roomId; + const searchInput = $('[component="chat/room/search"]'); + searchInput.on('keyup', utils.debounce(doSearch, 250)) + .on('focus', () => { + if (searchInput.val()) { + doSearch(); + } + }); + resultListEl = $('[component="chat/message/search/results"]'); + chatContent = $('[component="chat/message/content"]'); + clearEl = $('[component="chat/room/search/clear"]'); + $('[component="chat/input"]').on('focus', () => { + resultListEl.addClass('hidden'); + chatContent.removeClass('hidden'); + }); + clearEl.on('click', clearInputAndResults); + }; + + function clearInputAndResults() { + components.get('chat/room/search').val(''); + removeResults(); + resultListEl.addClass('hidden'); + chatContent.removeClass('hidden'); + clearEl.addClass('hidden'); + } + + async function doSearch() { + const query = components.get('chat/room/search').val(); + if (!query) { + return clearInputAndResults(); + } + if (query.length <= 2) { + return; + } + clearEl.removeClass('hidden'); + socket.emit('modules.chats.searchMessages', { + content: query, + roomId: roomId, + }).then(displayResults) + .catch(alerts.error); + } + + function removeResults() { + resultListEl.children('[data-mid]').remove(); + } + + async function displayResults(data) { + removeResults(); + + if (!data.length) { + resultListEl.removeClass('hidden'); + chatContent.addClass('hidden'); + return resultListEl.find('[component="chat/message/search/no-results"]').removeClass('hidden'); + } + resultListEl.find('[component="chat/message/search/no-results"]').addClass('hidden'); + + const html = await app.parseAndTranslate('partials/chats/messages', { + messages: data, + isAdminOrGlobalMod: app.user.isAdmin || app.user.isGlobalMod, + }); + + resultListEl.append(html); + messages.onMessagesAddedToDom(resultListEl.find('[component="chat/message"]')); + chatContent.addClass('hidden'); + resultListEl.removeClass('hidden'); + } + + return messageSearch; +}); diff --git a/public/src/client/chats/messages.js b/public/src/client/chats/messages.js index 0105c82666..041cee4585 100644 --- a/public/src/client/chats/messages.js +++ b/public/src/client/chats/messages.js @@ -108,14 +108,15 @@ define('forum/chats/messages', [ messages.onMessagesAddedToDom = function (messageEls) { messageEls.find('.timeago').timeago(); messageEls.find('img:not(.not-responsive)').addClass('img-fluid'); - messages.wrapImagesInLinks(messageEls.first().parent()); + messageEls.find('img:not(.emoji)').each(function () { + images.wrapImageInLink($(this)); + }); }; messages.parseMessage = function (data, callback) { const tplData = { messages: data, isAdminOrGlobalMod: app.user.isAdmin || app.user.isGlobalMod, - }; if (Array.isArray(data)) { app.parseAndTranslate('partials/chats/messages', tplData).then(callback); @@ -155,14 +156,14 @@ define('forum/chats/messages', [ .toggleClass('hidden', isAtBottom); }; - messages.prepEdit = async function (inputEl, mid, roomId) { + messages.prepEdit = async function (msgEl, 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'); + const messageBody = msgEl.find(`[component="chat/message/body"]`); + const messageControls = msgEl.find(`[component="chat/message/controls"]`); + const chatContent = messageBody.parents('[component="chat/message/content"]'); messageBody.addClass('hidden'); messageControls.addClass('hidden'); @@ -173,7 +174,7 @@ define('forum/chats/messages', [ textarea.focus().putCursorAtEnd(); autoresizeTextArea(textarea); - if (messages.isAtBottom(chatContent)) { + if (chatContent.length && messages.isAtBottom(chatContent)) { messages.scrollToBottom(chatContent); } @@ -212,7 +213,7 @@ define('forum/chats/messages', [ }); hooks.fire('action:chat.prepEdit', { - inputEl: inputEl, + msgEl: msgEl, messageId: mid, roomId: roomId, editEl: editEl, @@ -236,10 +237,10 @@ define('forum/chats/messages', [ const self = parseInt(message.fromuid, 10) === parseInt(app.user.uid, 10); message.self = self ? 1 : 0; messages.parseMessage(message, function (html) { - const body = components.get('chat/message', message.messageId); - if (body.length) { - body.replaceWith(html); - messages.onMessagesAddedToDom(html); + const msgEl = components.get('chat/message', message.mid); + if (msgEl.length) { + msgEl.replaceWith(html); + messages.onMessagesAddedToDom(components.get('chat/message', message.mid)); } }); }); diff --git a/public/src/client/chats/search.js b/public/src/client/chats/user-search.js similarity index 94% rename from public/src/client/chats/search.js rename to public/src/client/chats/user-search.js index bb0d160328..a07ebd7099 100644 --- a/public/src/client/chats/search.js +++ b/public/src/client/chats/user-search.js @@ -1,13 +1,13 @@ 'use strict'; -define('forum/chats/search', [ +define('forum/chats/user-search', [ 'components', 'api', 'alerts', ], function (components, api, alerts) { - const search = {}; + const userSearch = {}; let users = []; - search.init = function (options) { + userSearch.init = function (options) { options = options || {}; users.length = 0; components.get('chat/search').on('keyup', utils.debounce(doSearch, 250)); @@ -65,5 +65,5 @@ define('forum/chats/search', [ chatsListEl.parent().toggleClass('show', true); } - return search; + return userSearch; }); diff --git a/src/socket.io/modules.js b/src/socket.io/modules.js index 7b233d7d35..0595e1b59f 100644 --- a/src/socket.io/modules.js +++ b/src/socket.io/modules.js @@ -6,6 +6,7 @@ const db = require('../database'); const Messaging = require('../messaging'); const utils = require('../utils'); const user = require('../user'); +const plugins = require('../plugins'); const privileges = require('../privileges'); const groups = require('../groups'); @@ -213,4 +214,41 @@ SocketModules.chats.setNotificationSetting = async (socket, data) => { await Messaging.setUserNotificationSetting(socket.uid, data.roomId, data.value); }; +SocketModules.chats.searchMessages = async (socket, data) => { + if (!data || !utils.isNumber(data.roomId) || !data.content) { + throw new Error('[[error:invalid-data]]'); + } + const [roomData, inRoom] = await Promise.all([ + Messaging.getRoomData(data.roomId), + Messaging.isUserInRoom(socket.uid, data.roomId), + ]); + + if (!roomData) { + throw new Error('[[error:no-room]]'); + } + if (!inRoom) { + throw new Error('[[error:no-privileges]]'); + } + const { ids } = await plugins.hooks.fire('filter:messaging.searchMessages', { + content: data.content, + roomId: [data.roomId], + uid: [data.uid], + matchWords: 'any', + ids: [], + }); + + let userjoinTimestamp = 0; + if (!roomData.public) { + userjoinTimestamp = await db.sortedSetScore(`chat:room:${data.roomId}:uids`, socket.uid); + } + const messageData = await Messaging.getMessagesData(ids, socket.uid, data.roomId, false); + messageData.forEach((msg) => { + if (msg) { + msg.newSet = true; + } + }); + + return messageData.filter(msg => msg && !msg.deleted && msg.timestamp > userjoinTimestamp); +}; + require('../promisify')(SocketModules); diff --git a/test/messaging.js b/test/messaging.js index cb9e9f1555..1501f28e17 100644 --- a/test/messaging.js +++ b/test/messaging.js @@ -154,7 +154,7 @@ describe('Messaging Library', () => { const { body } = await callv3API('get', `/chats/${roomId}`, {}, 'foo'); const { messages } = body.response; assert.equal(messages.length, 2); - assert.strictEqual(messages[0].system, true); + assert.strictEqual(messages[0].system, 1); assert.strictEqual(messages[0].content, 'user-join'); const { statusCode, body: body2 } = await callv3API('put', `/chats/${roomId}/messages/${messages[0].messageId}`, { @@ -233,7 +233,7 @@ describe('Messaging Library', () => { const { body } = await callv3API('get', `/chats/${roomId}`, {}, 'foo'); const { messages } = body.response; const message = messages.pop(); - assert.strictEqual(message.system, true); + assert.strictEqual(message.system, 1); assert.strictEqual(message.content, 'user-leave'); }); @@ -244,12 +244,12 @@ describe('Messaging Library', () => { assert.equal(messages.length, 4); let message = messages.pop(); - assert.strictEqual(message.system, true); + assert.strictEqual(message.system, 1); assert.strictEqual(message.content, 'user-leave'); // The message before should still be a user-join message = messages.pop(); - assert.strictEqual(message.system, true); + assert.strictEqual(message.system, 1); assert.strictEqual(message.content, 'user-join'); }); @@ -466,7 +466,7 @@ describe('Messaging Library', () => { const { messages } = body.response; const message = messages.pop(); - assert.strictEqual(message.system, true); + assert.strictEqual(message.system, 1); assert.strictEqual(message.content, 'room-rename, new room name'); });