ability to pin chat messages (#11964)

isekai-main
Barış Soner Uşaklı 1 year ago committed by GitHub
parent 94f07c149a
commit 54706b1182
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -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}:

@ -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)

@ -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;

@ -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);

Loading…
Cancel
Save