mirror of
https://github.com/NodeBB/NodeBB.git
synced 2025-11-17 19:21:04 +01:00
ability to pin chat messages (#11964)
This commit is contained in:
committed by
GitHub
parent
94f07c149a
commit
54706b1182
@@ -30,6 +30,10 @@
|
||||
"chat.delete_message_confirm": "Are you sure you wish to delete this message?",
|
||||
"chat.retrieving-users": "Retrieving users...",
|
||||
"chat.view-users-list": "View users list",
|
||||
"chat.pinned-messages": "Pinned Messages",
|
||||
"chat.no-pinned-messages": "There are no pinned messages",
|
||||
"chat.pin-message": "Pin Message",
|
||||
"chat.unpin-message": "Unpin Message",
|
||||
"chat.public-rooms": "Public Rooms (%1)",
|
||||
"chat.private-rooms": "Private Rooms (%1)",
|
||||
"chat.create-room": "Create Chat Room",
|
||||
|
||||
@@ -184,6 +184,8 @@ paths:
|
||||
$ref: 'write/chats/roomId/messages.yaml'
|
||||
/chats/{roomId}/messages/{mid}:
|
||||
$ref: 'write/chats/roomId/messages/mid.yaml'
|
||||
/chats/{roomId}/messages/{mid}/pin:
|
||||
$ref: 'write/chats/roomId/messages/mid/pin.yaml'
|
||||
/flags/:
|
||||
$ref: 'write/flags.yaml'
|
||||
/flags/{flagId}:
|
||||
|
||||
66
public/openapi/write/chats/roomId/messages/mid/pin.yaml
Normal file
66
public/openapi/write/chats/roomId/messages/mid/pin.yaml
Normal file
@@ -0,0 +1,66 @@
|
||||
put:
|
||||
tags:
|
||||
- chats
|
||||
summary: pin a chat message
|
||||
description: This operation pins an existing chat message in a chat room
|
||||
parameters:
|
||||
- in: path
|
||||
name: roomId
|
||||
schema:
|
||||
type: string
|
||||
required: true
|
||||
description: a valid chat room id
|
||||
example: 1
|
||||
- in: path
|
||||
name: mid
|
||||
schema:
|
||||
type: string
|
||||
required: true
|
||||
description: a valid chat message id
|
||||
example: 1
|
||||
responses:
|
||||
'200':
|
||||
description: Chat message successfully pinned
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
properties:
|
||||
status:
|
||||
$ref: ../../../../../components/schemas/Status.yaml#/Status
|
||||
response:
|
||||
type: object
|
||||
properties: {}
|
||||
delete:
|
||||
tags:
|
||||
- chats
|
||||
summary: unpin a chat message
|
||||
description: This operation unpins a chat message in a room
|
||||
parameters:
|
||||
- in: path
|
||||
name: roomId
|
||||
schema:
|
||||
type: string
|
||||
required: true
|
||||
description: a valid chat room id
|
||||
example: 1
|
||||
- in: path
|
||||
name: mid
|
||||
schema:
|
||||
type: string
|
||||
required: true
|
||||
description: a valid chat message id
|
||||
example: 1
|
||||
responses:
|
||||
'200':
|
||||
description: Chat message successfully unpinned
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
properties:
|
||||
status:
|
||||
$ref: ../../../../../components/schemas/Status.yaml#/Status
|
||||
response:
|
||||
type: object
|
||||
properties: {}
|
||||
@@ -10,6 +10,7 @@ define('forum/chats', [
|
||||
'forum/chats/messages',
|
||||
'forum/chats/user-list',
|
||||
'forum/chats/message-search',
|
||||
'forum/chats/pinned-messages',
|
||||
'composer/autocomplete',
|
||||
'hooks',
|
||||
'bootbox',
|
||||
@@ -19,8 +20,9 @@ define('forum/chats', [
|
||||
'uploadHelpers',
|
||||
], function (
|
||||
components, mousetrap, recentChats, create,
|
||||
manage, messages, userList, messageSearch, autocomplete,
|
||||
hooks, bootbox, alerts, chatModule, api, uploadHelpers
|
||||
manage, messages, userList, messageSearch, pinnedMessages,
|
||||
autocomplete, hooks, bootbox, alerts, chatModule, api,
|
||||
uploadHelpers
|
||||
) {
|
||||
const Chats = {
|
||||
initialised: false,
|
||||
@@ -66,6 +68,7 @@ define('forum/chats', [
|
||||
messages.wrapImagesInLinks(changeContentEl);
|
||||
messages.scrollToBottomAfterImageLoad(changeContentEl);
|
||||
create.init();
|
||||
pinnedMessages.init($('[component="chat/main-wrapper"]'));
|
||||
|
||||
hooks.fire('action:chat.loaded', $('.chats-full'));
|
||||
};
|
||||
@@ -77,7 +80,7 @@ define('forum/chats', [
|
||||
const chatControls = components.get('chat/controls');
|
||||
Chats.addSendHandlers(roomId, $('.chat-input'), $('.expanded-chat button[data-action="send"]'));
|
||||
Chats.addPopoutHandler();
|
||||
Chats.addActionHandlers(components.get('chat/messages'), roomId);
|
||||
Chats.addActionHandlers(components.get('chat/message/window'), roomId);
|
||||
Chats.addManageHandler(roomId, chatControls.find('[data-action="manage"]'));
|
||||
Chats.addRenameHandler(roomId, chatControls.find('[data-action="rename"]'));
|
||||
Chats.addLeaveHandler(roomId, chatControls.find('[data-action="leave"]'));
|
||||
@@ -152,6 +155,7 @@ define('forum/chats', [
|
||||
placement: 'top',
|
||||
container: '#content',
|
||||
animation: false,
|
||||
trigger: 'hover',
|
||||
});
|
||||
};
|
||||
|
||||
@@ -310,10 +314,10 @@ define('forum/chats', [
|
||||
const msgEl = $(this).parents('[data-mid]');
|
||||
const messageId = msgEl.attr('data-mid');
|
||||
const action = this.getAttribute('data-action');
|
||||
|
||||
$(this).tooltip('dispose');
|
||||
switch (action) {
|
||||
case 'reply':
|
||||
messages.prepReplyTo(msgEl, roomId);
|
||||
messages.prepReplyTo(msgEl, element);
|
||||
break;
|
||||
case 'edit':
|
||||
messages.prepEdit(msgEl, messageId, roomId);
|
||||
@@ -324,6 +328,12 @@ define('forum/chats', [
|
||||
case 'restore':
|
||||
messages.restore(messageId, roomId);
|
||||
break;
|
||||
case 'pin':
|
||||
pinnedMessages.pin(messageId, roomId);
|
||||
break;
|
||||
case 'unpin':
|
||||
pinnedMessages.unpin(messageId, roomId);
|
||||
break;
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
@@ -168,10 +168,9 @@ define('forum/chats/messages', [
|
||||
.toggleClass('hidden', isAtBottom);
|
||||
};
|
||||
|
||||
messages.prepReplyTo = async function (msgEl, roomId) {
|
||||
const chatMessages = msgEl.parents(`[component="chat/messages"][data-roomid="${roomId}"]`);
|
||||
const chatContent = chatMessages.find('[component="chat/message/content"]');
|
||||
const composerEl = chatMessages.find('[component="chat/composer"]');
|
||||
messages.prepReplyTo = async function (msgEl, chatMessageWindow) {
|
||||
const chatContent = chatMessageWindow.find('[component="chat/message/content"]');
|
||||
const composerEl = chatMessageWindow.find('[component="chat/composer"]');
|
||||
const mid = msgEl.attr('data-mid');
|
||||
const replyToEl = composerEl.find('[component="chat/composer/replying-to"]');
|
||||
replyToEl.attr('data-tomid', mid)
|
||||
|
||||
86
public/src/client/chats/pinned-messages.js
Normal file
86
public/src/client/chats/pinned-messages.js
Normal file
@@ -0,0 +1,86 @@
|
||||
'use strict';
|
||||
|
||||
|
||||
define('forum/chats/pinned-messages', ['api', 'alerts'], function (api, alerts) {
|
||||
const pinnedMessages = {};
|
||||
let container;
|
||||
pinnedMessages.init = function (_container) {
|
||||
container = _container;
|
||||
$('[component="chat/pinned/messages/btn"]').on('click', async () => {
|
||||
const pinnedMessagesContainer = container.find('[component="chat/messages/pinned/container"]');
|
||||
if (!pinnedMessagesContainer.hasClass('hidden')) {
|
||||
return pinnedMessagesContainer.addClass('hidden');
|
||||
}
|
||||
const userListEl = container.find('[component="chat/user/list"]');
|
||||
userListEl.addClass('hidden');
|
||||
await pinnedMessages.refreshList();
|
||||
pinnedMessagesContainer.removeClass('hidden');
|
||||
});
|
||||
|
||||
handleInfiniteScroll(container);
|
||||
};
|
||||
|
||||
function handleInfiniteScroll(container) {
|
||||
const listEl = container.find('[component="chat/messages/pinned"]');
|
||||
listEl.on('scroll', utils.debounce(async () => {
|
||||
const bottom = (listEl[0].scrollHeight - listEl.height()) * 0.85;
|
||||
if (listEl.scrollTop() > bottom) {
|
||||
const lastIndex = listEl.find('[data-index]').last().attr('data-index');
|
||||
const data = await loadData(parseInt(lastIndex, 10) + 1);
|
||||
if (data && data.length) {
|
||||
const html = await parseMessages(data);
|
||||
container.find('[component="chat/messages/pinned"]').append(html);
|
||||
}
|
||||
}
|
||||
}, 200));
|
||||
}
|
||||
|
||||
pinnedMessages.refreshList = async function () {
|
||||
const data = await loadData(0);
|
||||
|
||||
if (!data.length) {
|
||||
container.find('[component="chat/messages/pinned/empty"]').removeClass('hidden');
|
||||
container.find('[component="chat/messages/pinned"]').html('');
|
||||
return;
|
||||
}
|
||||
container.find('[component="chat/messages/pinned/empty"]').addClass('hidden');
|
||||
const html = await parseMessages(data);
|
||||
container.find('[component="chat/messages/pinned"]').html(html);
|
||||
html.find('.timeago').timeago();
|
||||
};
|
||||
|
||||
async function parseMessages(data) {
|
||||
return await app.parseAndTranslate('partials/chats/pinned-messages', 'messages', {
|
||||
isOwner: ajaxify.data.isOwner,
|
||||
isAdminOrGlobalMod: ajaxify.data.isAdminOrGlobalMod,
|
||||
messages: data,
|
||||
});
|
||||
}
|
||||
|
||||
async function loadData(start) {
|
||||
const data = await socket.emit('modules.chats.loadPinnedMessages', {
|
||||
roomId: ajaxify.data.roomId,
|
||||
start: start,
|
||||
});
|
||||
return data;
|
||||
}
|
||||
|
||||
pinnedMessages.pin = function (mid, roomId) {
|
||||
api.put(`/chats/${roomId}/messages/${mid}/pin`, {}).then(() => {
|
||||
$(`[component="chat/message"][data-mid="${mid}"]`).toggleClass('pinned', true);
|
||||
pinnedMessages.refreshList();
|
||||
}).catch(alerts.error);
|
||||
};
|
||||
|
||||
pinnedMessages.unpin = function (mid, roomId) {
|
||||
api.del(`/chats/${roomId}/messages/${mid}/pin`, {}).then(() => {
|
||||
$(`[component="chat/message"][data-mid="${mid}"]`).toggleClass('pinned', false);
|
||||
container.find(`[component="chat/messages/pinned"] [data-mid="${mid}"]`).remove();
|
||||
if (!container.find(`[component="chat/messages/pinned"] [data-mid]`).length) {
|
||||
container.find('[component="chat/messages/pinned/empty"]').removeClass('hidden');
|
||||
}
|
||||
}).catch(alerts.error);
|
||||
};
|
||||
|
||||
return pinnedMessages;
|
||||
});
|
||||
@@ -11,11 +11,13 @@ define('forum/chats/user-list', ['api'], function (api) {
|
||||
if (!userListEl.length) {
|
||||
return;
|
||||
}
|
||||
const pinnedMessageListEl = container.find('[component="chat/messages/pinned/container"]');
|
||||
container.find('[component="chat/user/list/btn"]').on('click', () => {
|
||||
userListEl.toggleClass('hidden');
|
||||
if (userListEl.hasClass('hidden')) {
|
||||
stopUpdating();
|
||||
} else {
|
||||
pinnedMessageListEl.addClass('hidden');
|
||||
startUpdating(roomId, userListEl);
|
||||
}
|
||||
});
|
||||
@@ -29,6 +31,9 @@ define('forum/chats/user-list', ['api'], function (api) {
|
||||
};
|
||||
|
||||
function startUpdating(roomId, userListEl) {
|
||||
if (updateInterval) {
|
||||
clearInterval(updateInterval);
|
||||
}
|
||||
updateInterval = setInterval(() => {
|
||||
updateUserList(roomId, userListEl);
|
||||
}, 5000);
|
||||
|
||||
@@ -356,7 +356,7 @@ define('chat', [
|
||||
}
|
||||
});
|
||||
|
||||
Chats.addActionHandlers(chatModal.find('[component="chat/messages"]'), roomId);
|
||||
Chats.addActionHandlers(chatModal.find('[component="chat/message/window"]'), roomId);
|
||||
Chats.addRenameHandler(roomId, chatModal.find('[data-action="rename"]'));
|
||||
Chats.addLeaveHandler(roomId, chatModal.find('[data-action="leave"]'));
|
||||
Chats.addDeleteHandler(roomId, chatModal.find('[data-action="delete"]'));
|
||||
|
||||
@@ -272,3 +272,13 @@ chatsAPI.restoreMessage = async (caller, { mid }) => {
|
||||
await messaging.canDelete(mid, caller.uid);
|
||||
await messaging.restoreMessage(mid, caller.uid);
|
||||
};
|
||||
|
||||
chatsAPI.pinMessage = async (caller, { roomId, mid }) => {
|
||||
await messaging.canPin(roomId, caller.uid);
|
||||
await messaging.pinMessage(mid, roomId);
|
||||
};
|
||||
|
||||
chatsAPI.unpinMessage = async (caller, { roomId, mid }) => {
|
||||
await messaging.canPin(roomId, caller.uid);
|
||||
await messaging.unpinMessage(mid, roomId);
|
||||
};
|
||||
|
||||
@@ -142,3 +142,17 @@ Chats.messages.restore = async (req, res) => {
|
||||
|
||||
helpers.formatApiResponse(200, res);
|
||||
};
|
||||
|
||||
Chats.messages.pin = async (req, res) => {
|
||||
const { mid, roomId } = req.params;
|
||||
await api.chats.pinMessage(req, { mid, roomId });
|
||||
|
||||
helpers.formatApiResponse(200, res);
|
||||
};
|
||||
|
||||
Chats.messages.unpin = async (req, res) => {
|
||||
const { mid, roomId } = req.params;
|
||||
await api.chats.unpinMessage(req, { mid, roomId });
|
||||
|
||||
helpers.formatApiResponse(200, res);
|
||||
};
|
||||
|
||||
@@ -46,21 +46,17 @@ module.exports = function (Messaging) {
|
||||
|
||||
Messaging.getMessagesData = async (mids, uid, roomId, isNew) => {
|
||||
let messages = await Messaging.getMessagesFields(mids, []);
|
||||
messages = await user.blocks.filter(uid, 'fromuid', messages);
|
||||
messages = messages
|
||||
.map((msg, idx) => {
|
||||
if (msg) {
|
||||
msg.messageId = parseInt(mids[idx], 10);
|
||||
msg.ip = undefined;
|
||||
msg.isOwner = msg.fromuid === parseInt(uid, 10);
|
||||
if (msg.deleted && !msg.isOwner) {
|
||||
msg.content = `<p>[[modules:chat.message-deleted]]</p>`;
|
||||
}
|
||||
}
|
||||
return msg;
|
||||
})
|
||||
.filter(Boolean);
|
||||
|
||||
messages = await user.blocks.filter(uid, 'fromuid', messages);
|
||||
const users = await user.getUsersFields(
|
||||
messages.map(msg => msg && msg.fromuid),
|
||||
['uid', 'username', 'userslug', 'picture', 'status', 'banned']
|
||||
@@ -175,8 +171,12 @@ module.exports = function (Messaging) {
|
||||
}
|
||||
|
||||
async function parseMessages(messages, uid, roomId, isNew) {
|
||||
await Promise.all(messages.map(async (message) => {
|
||||
message.content = await parseMessage(message, uid, roomId, isNew);
|
||||
await Promise.all(messages.map(async (msg) => {
|
||||
if (msg.deleted && !msg.isOwner) {
|
||||
msg.content = `<p>[[modules:chat.message-deleted]]</p>`;
|
||||
return;
|
||||
}
|
||||
msg.content = await parseMessage(msg, uid, roomId, isNew);
|
||||
}));
|
||||
}
|
||||
async function parseMessage(message, uid, roomId, isNew) {
|
||||
|
||||
@@ -90,4 +90,16 @@ module.exports = function (Messaging) {
|
||||
|
||||
Messaging.canEdit = async (messageId, uid) => await canEditDelete(messageId, uid, 'edit');
|
||||
Messaging.canDelete = async (messageId, uid) => await canEditDelete(messageId, uid, 'delete');
|
||||
|
||||
Messaging.canPin = async (roomId, uid) => {
|
||||
const [isAdmin, isGlobalMod, inRoom, isRoomOwner] = await Promise.all([
|
||||
user.isAdministrator(uid),
|
||||
user.isGlobalModerator(uid),
|
||||
Messaging.isUserInRoom(uid, roomId),
|
||||
Messaging.isRoomOwner(uid, roomId),
|
||||
]);
|
||||
if (!isAdmin && !isGlobalMod && (!inRoom || !isRoomOwner)) {
|
||||
throw new Error('[[error:no-privileges]]');
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
@@ -24,6 +24,7 @@ require('./edit')(Messaging);
|
||||
require('./rooms')(Messaging);
|
||||
require('./unread')(Messaging);
|
||||
require('./notifications')(Messaging);
|
||||
require('./pins')(Messaging);
|
||||
|
||||
Messaging.notificationSettings = Object.create(null);
|
||||
Messaging.notificationSettings.NONE = 1;
|
||||
|
||||
36
src/messaging/pins.js
Normal file
36
src/messaging/pins.js
Normal file
@@ -0,0 +1,36 @@
|
||||
'use strict';
|
||||
|
||||
const db = require('../database');
|
||||
|
||||
module.exports = function (Messaging) {
|
||||
Messaging.pinMessage = async (mid, roomId) => {
|
||||
const isMessageInRoom = await db.isSortedSetMember(`chat:room:${roomId}:mids`, mid);
|
||||
if (isMessageInRoom) {
|
||||
await db.sortedSetAdd(`chat:room:${roomId}:mids:pinned`, Date.now(), mid);
|
||||
await Messaging.setMessageFields(mid, { pinned: 1 });
|
||||
}
|
||||
};
|
||||
|
||||
Messaging.unpinMessage = async (mid, roomId) => {
|
||||
const isMessageInRoom = await db.isSortedSetMember(`chat:room:${roomId}:mids`, mid);
|
||||
if (isMessageInRoom) {
|
||||
await db.sortedSetRemove(`chat:room:${roomId}:mids:pinned`, mid);
|
||||
await Messaging.setMessageFields(mid, { pinned: 0 });
|
||||
}
|
||||
};
|
||||
|
||||
Messaging.getPinnedMessages = async (roomId, uid, start, stop) => {
|
||||
const mids = await db.getSortedSetRevRange(`chat:room:${roomId}:mids:pinned`, start, stop);
|
||||
if (!mids.length) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const messageData = await Messaging.getMessagesData(mids, uid, roomId, true);
|
||||
messageData.forEach((msg, i) => {
|
||||
if (msg) {
|
||||
msg.index = start + i;
|
||||
}
|
||||
});
|
||||
return messageData;
|
||||
};
|
||||
};
|
||||
@@ -32,5 +32,8 @@ module.exports = function () {
|
||||
setupApiRoute(router, 'post', '/:roomId/messages/:mid', [...middlewares, middleware.assert.room, middleware.assert.message], controllers.write.chats.messages.restore);
|
||||
setupApiRoute(router, 'delete', '/:roomId/messages/:mid', [...middlewares, middleware.assert.room, middleware.assert.message], controllers.write.chats.messages.delete);
|
||||
|
||||
setupApiRoute(router, 'put', '/:roomId/messages/:mid/pin', [...middlewares, middleware.assert.room, middleware.assert.message], controllers.write.chats.messages.pin);
|
||||
setupApiRoute(router, 'delete', '/:roomId/messages/:mid/pin', [...middlewares, middleware.assert.room, middleware.assert.message], controllers.write.chats.messages.unpin);
|
||||
|
||||
return router;
|
||||
};
|
||||
|
||||
@@ -251,4 +251,18 @@ SocketModules.chats.searchMessages = async (socket, data) => {
|
||||
return messageData.filter(msg => msg && !msg.deleted && msg.timestamp > userjoinTimestamp);
|
||||
};
|
||||
|
||||
SocketModules.chats.loadPinnedMessages = async (socket, data) => {
|
||||
if (!data || !data.roomId || !utils.isNumber(data.start)) {
|
||||
throw new Error('[[error:invalid-data]]');
|
||||
}
|
||||
const isInRoom = await Messaging.isUserInRoom(socket.uid, data.roomId);
|
||||
if (!isInRoom) {
|
||||
throw new Error('[[error:no-privileges]]');
|
||||
}
|
||||
const start = parseInt(data.start, 10) || 0;
|
||||
const pinnedMsgs = await Messaging.getPinnedMessages(data.roomId, socket.uid, start, start + 49);
|
||||
return pinnedMsgs;
|
||||
};
|
||||
|
||||
|
||||
require('../promisify')(SocketModules);
|
||||
|
||||
Reference in New Issue
Block a user