mirror of
https://github.com/yu-i-i/overleaf-cep.git
synced 2026-05-31 12:51:35 +02:00
* Initial server-side delete of chat message plus dropdown * Update chat pane after deleting message * Chat message dropdown styling * Add confirmation dialog for deleting a message * Refactor chat message grouping to allow deletion of individual messages * Delete other user's deleted message from chat pane * Implement message editing * Styling * Make the dropdown appear overlap with the button slightly so that the menu stays visible when the user moves their cursor into the menu when the menu is positioned above the button * Submit edit with Enter key * Add edited indicator to edited chat messages * Add animation to chat message deletion * Tidying, edit chat message textarea improvements * Add types to message-list-utils * update dependencies * edit/delete for ide-redesign * fix type errors in tests * filter deleted messages from group * promisify ChatController * fix tests and translations * add new tests * chat-context tests * fix message-list-appender tests * add new tests for message-list-utils * Update services/web/test/frontend/features/chat/context/chat-context.test.tsx Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * preserve original content when canceling edits * update delete message translation * hide dropdown only if not already shown * remove delete animation * fix lint error * fix chat.yaml * hide under feature flag --------- Co-authored-by: Tim Down <158919+timdown@users.noreply.github.com> Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> GitOrigin-RevId: 12521886a1a59ccd564851df19e5d46c70d328f5
411 lines
12 KiB
JavaScript
411 lines
12 KiB
JavaScript
import logger from '@overleaf/logger'
|
|
import * as MessageManager from './MessageManager.js'
|
|
import * as MessageFormatter from './MessageFormatter.js'
|
|
import * as ThreadManager from '../Threads/ThreadManager.js'
|
|
import { ObjectId } from '../../mongodb.js'
|
|
|
|
const DEFAULT_MESSAGE_LIMIT = 50
|
|
const MAX_MESSAGE_LENGTH = 10 * 1024 // 10kb, about 1,500 words
|
|
|
|
function readContext(context, req) {
|
|
req.body = context.requestBody
|
|
req.params = context.params.path
|
|
req.query = context.params.query
|
|
if (typeof req.params.projectId !== 'undefined') {
|
|
if (!ObjectId.isValid(req.params.projectId)) {
|
|
context.res.status(400).setBody('Invalid projectId')
|
|
}
|
|
}
|
|
if (typeof req.params.threadId !== 'undefined') {
|
|
if (!ObjectId.isValid(req.params.threadId)) {
|
|
context.res.status(400).setBody('Invalid threadId')
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @param context
|
|
* @param {(req: unknown, res: unknown) => Promise<unknown>} ControllerMethod
|
|
* @returns {Promise<*>}
|
|
*/
|
|
export async function callMessageHttpController(context, ControllerMethod) {
|
|
const req = {}
|
|
readContext(context, req)
|
|
if (context.res.statusCode !== 400) {
|
|
return await ControllerMethod(req, context.res)
|
|
} else {
|
|
return context.res.body
|
|
}
|
|
}
|
|
|
|
export async function getGlobalMessages(context) {
|
|
return await callMessageHttpController(context, _getGlobalMessages)
|
|
}
|
|
|
|
export async function getGlobalMessage(context) {
|
|
return await callMessageHttpController(context, _getGlobalMessage)
|
|
}
|
|
|
|
export async function sendGlobalMessage(context) {
|
|
return await callMessageHttpController(context, _sendGlobalMessage)
|
|
}
|
|
|
|
export async function sendMessage(context) {
|
|
return await callMessageHttpController(context, _sendThreadMessage)
|
|
}
|
|
|
|
export async function getThreads(context) {
|
|
return await callMessageHttpController(context, _getAllThreads)
|
|
}
|
|
|
|
export async function getThread(context) {
|
|
return await callMessageHttpController(context, _getThread)
|
|
}
|
|
|
|
export async function resolveThread(context) {
|
|
return await callMessageHttpController(context, _resolveThread)
|
|
}
|
|
|
|
export async function reopenThread(context) {
|
|
return await callMessageHttpController(context, _reopenThread)
|
|
}
|
|
|
|
export async function deleteThread(context) {
|
|
return await callMessageHttpController(context, _deleteThread)
|
|
}
|
|
|
|
export async function editMessage(context) {
|
|
return await callMessageHttpController(context, _editMessage)
|
|
}
|
|
|
|
export async function editGlobalMessage(context) {
|
|
return await callMessageHttpController(context, _editGlobalMessage)
|
|
}
|
|
|
|
export async function deleteMessage(context) {
|
|
return await callMessageHttpController(context, _deleteMessage)
|
|
}
|
|
|
|
export async function deleteUserMessage(context) {
|
|
return await callMessageHttpController(context, _deleteUserMessage)
|
|
}
|
|
|
|
export async function deleteGlobalMessage(context) {
|
|
return await callMessageHttpController(context, _deleteGlobalMessage)
|
|
}
|
|
|
|
export async function getResolvedThreadIds(context) {
|
|
return await callMessageHttpController(context, _getResolvedThreadIds)
|
|
}
|
|
|
|
export async function destroyProject(context) {
|
|
return await callMessageHttpController(context, _destroyProject)
|
|
}
|
|
|
|
export async function duplicateCommentThreads(context) {
|
|
return await callMessageHttpController(context, _duplicateCommentThreads)
|
|
}
|
|
|
|
export async function generateThreadData(context) {
|
|
return await callMessageHttpController(context, _generateThreadData)
|
|
}
|
|
|
|
export async function getStatus(context) {
|
|
const message = 'chat is alive'
|
|
context.res.status(200).setBody(message)
|
|
return message
|
|
}
|
|
|
|
const _getGlobalMessages = async (req, res) => {
|
|
await _getMessages(ThreadManager.GLOBAL_THREAD, req, res)
|
|
}
|
|
|
|
const _getGlobalMessage = async (req, res) => {
|
|
const { projectId, messageId } = req.params
|
|
logger.debug({ projectId, messageId }, 'getting single global message')
|
|
try {
|
|
const room = await ThreadManager.findThread(
|
|
projectId,
|
|
ThreadManager.GLOBAL_THREAD
|
|
)
|
|
|
|
const message = await MessageManager.getMessage(room._id, messageId)
|
|
const formattedMsg = MessageFormatter.formatMessageForClientSide(message)
|
|
|
|
res.status(200).setBody(formattedMsg)
|
|
} catch (error) {
|
|
if (
|
|
error instanceof ThreadManager.MissingThreadError ||
|
|
error instanceof MessageManager.MissingMessageError
|
|
) {
|
|
res.status(404)
|
|
return
|
|
}
|
|
throw error
|
|
}
|
|
}
|
|
|
|
async function _sendGlobalMessage(req, res) {
|
|
const { user_id: userId, content } = req.body
|
|
const { projectId } = req.params
|
|
return await _sendMessage(
|
|
userId,
|
|
projectId,
|
|
content,
|
|
ThreadManager.GLOBAL_THREAD,
|
|
res
|
|
)
|
|
}
|
|
|
|
async function _sendThreadMessage(req, res) {
|
|
const { user_id: userId, content } = req.body
|
|
const { projectId, threadId } = req.params
|
|
return await _sendMessage(userId, projectId, content, threadId, res)
|
|
}
|
|
|
|
const _getAllThreads = async (req, res) => {
|
|
const { projectId } = req.params
|
|
logger.debug({ projectId }, 'getting all threads')
|
|
const rooms = await ThreadManager.findAllThreadRooms(projectId)
|
|
const roomIds = rooms.map(r => r._id)
|
|
const messages = await MessageManager.findAllMessagesInRooms(roomIds)
|
|
const threads = MessageFormatter.groupMessagesByThreads(rooms, messages)
|
|
res.json(threads)
|
|
}
|
|
|
|
const _generateThreadData = async (req, res) => {
|
|
const { projectId } = req.params
|
|
const { threads } = req.body
|
|
logger.debug({ projectId }, 'getting all threads')
|
|
const rooms = await ThreadManager.findThreadsById(projectId, threads)
|
|
const roomIds = rooms.map(r => r._id)
|
|
const messages = await MessageManager.findAllMessagesInRooms(roomIds)
|
|
logger.debug({ rooms, messages }, 'looked up messages in the rooms')
|
|
const threadData = MessageFormatter.groupMessagesByThreads(rooms, messages)
|
|
res.json(threadData)
|
|
}
|
|
|
|
const _getThread = async (req, res) => {
|
|
const { projectId, threadId } = req.params
|
|
logger.debug({ projectId, threadId }, 'getting specific thread')
|
|
try {
|
|
const room = await ThreadManager.findThread(projectId, threadId)
|
|
const messages = await MessageManager.findAllMessagesInRooms([room._id])
|
|
const threads = MessageFormatter.groupMessagesByThreads([room], messages)
|
|
|
|
const thread = threads[threadId] || null
|
|
if (!thread) {
|
|
res.status(404)
|
|
return
|
|
}
|
|
res.json(thread)
|
|
} catch (error) {
|
|
if (error instanceof ThreadManager.MissingThreadError) {
|
|
res.status(404)
|
|
return
|
|
}
|
|
throw error
|
|
}
|
|
}
|
|
|
|
const _resolveThread = async (req, res) => {
|
|
const { projectId, threadId } = req.params
|
|
const { user_id: userId } = req.body
|
|
logger.debug({ userId, projectId, threadId }, 'marking thread as resolved')
|
|
await ThreadManager.resolveThread(projectId, threadId, userId)
|
|
res.status(204)
|
|
}
|
|
|
|
const _reopenThread = async (req, res) => {
|
|
const { projectId, threadId } = req.params
|
|
logger.debug({ projectId, threadId }, 'reopening thread')
|
|
await ThreadManager.reopenThread(projectId, threadId)
|
|
res.status(204)
|
|
}
|
|
|
|
const _deleteThread = async (req, res) => {
|
|
const { projectId, threadId } = req.params
|
|
logger.debug({ projectId, threadId }, 'deleting thread')
|
|
const roomId = await ThreadManager.deleteThread(projectId, threadId)
|
|
await MessageManager.deleteAllMessagesInRoom(roomId)
|
|
res.status(204)
|
|
}
|
|
|
|
const _editMessage = async (req, res) => {
|
|
const { content, userId } = req.body
|
|
const { projectId, threadId, messageId } = req.params
|
|
logger.debug({ projectId, threadId, messageId, content }, 'editing message')
|
|
const room = await ThreadManager.findOrCreateThread(projectId, threadId)
|
|
const found = await MessageManager.updateMessage(
|
|
room._id,
|
|
messageId,
|
|
userId,
|
|
content,
|
|
Date.now()
|
|
)
|
|
if (!found) {
|
|
res.status(404)
|
|
return
|
|
}
|
|
res.status(204)
|
|
}
|
|
|
|
const _editGlobalMessage = async (req, res) => {
|
|
const { content, userId } = req.body
|
|
const { projectId, messageId } = req.params
|
|
logger.debug({ projectId, messageId, content }, 'editing global message')
|
|
const room = await ThreadManager.findOrCreateThread(
|
|
projectId,
|
|
ThreadManager.GLOBAL_THREAD
|
|
)
|
|
const found = await MessageManager.updateMessage(
|
|
room._id,
|
|
messageId,
|
|
userId,
|
|
content,
|
|
Date.now()
|
|
)
|
|
if (!found) {
|
|
res.status(404)
|
|
return
|
|
}
|
|
res.status(204)
|
|
}
|
|
|
|
const _deleteMessage = async (req, res) => {
|
|
const { projectId, threadId, messageId } = req.params
|
|
logger.debug({ projectId, threadId, messageId }, 'deleting message')
|
|
const room = await ThreadManager.findOrCreateThread(projectId, threadId)
|
|
await MessageManager.deleteMessage(room._id, messageId)
|
|
res.status(204)
|
|
}
|
|
|
|
const _deleteUserMessage = async (req, res) => {
|
|
const { projectId, threadId, userId, messageId } = req.params
|
|
const room = await ThreadManager.findOrCreateThread(projectId, threadId)
|
|
await MessageManager.deleteUserMessage(userId, room._id, messageId)
|
|
res.status(204)
|
|
}
|
|
|
|
const _deleteGlobalMessage = async (req, res) => {
|
|
const { projectId, messageId } = req.params
|
|
const room = await ThreadManager.findOrCreateThread(
|
|
projectId,
|
|
ThreadManager.GLOBAL_THREAD
|
|
)
|
|
await MessageManager.deleteMessage(room._id, messageId)
|
|
res.status(204)
|
|
}
|
|
|
|
const _getResolvedThreadIds = async (req, res) => {
|
|
const { projectId } = req.params
|
|
const resolvedThreadIds = await ThreadManager.getResolvedThreadIds(projectId)
|
|
res.json({ resolvedThreadIds })
|
|
}
|
|
|
|
const _destroyProject = async (req, res) => {
|
|
const { projectId } = req.params
|
|
logger.debug({ projectId }, 'destroying project')
|
|
const rooms = await ThreadManager.findAllThreadRoomsAndGlobalThread(projectId)
|
|
const roomIds = rooms.map(r => r._id)
|
|
logger.debug({ projectId, roomIds }, 'deleting all messages in rooms')
|
|
await MessageManager.deleteAllMessagesInRooms(roomIds)
|
|
logger.debug({ projectId }, 'deleting all threads in project')
|
|
await ThreadManager.deleteAllThreadsInProject(projectId)
|
|
res.status(204)
|
|
}
|
|
|
|
async function _sendMessage(userId, projectId, content, clientThreadId, res) {
|
|
if (!ObjectId.isValid(userId)) {
|
|
const message = 'Invalid userId'
|
|
res.status(400).setBody(message)
|
|
return message
|
|
}
|
|
if (!content) {
|
|
const message = 'No content provided'
|
|
res.status(400).setBody(message)
|
|
return message
|
|
}
|
|
if (content.length > MAX_MESSAGE_LENGTH) {
|
|
const message = `Content too long (> ${MAX_MESSAGE_LENGTH} bytes)`
|
|
res.status(400).setBody(message)
|
|
return message
|
|
}
|
|
logger.debug(
|
|
{ clientThreadId, projectId, userId, content },
|
|
'new message received'
|
|
)
|
|
const thread = await ThreadManager.findOrCreateThread(
|
|
projectId,
|
|
clientThreadId
|
|
)
|
|
let message = await MessageManager.createMessage(
|
|
thread._id,
|
|
userId,
|
|
content,
|
|
Date.now()
|
|
)
|
|
message = MessageFormatter.formatMessageForClientSide(message)
|
|
message.room_id = projectId
|
|
|
|
res.status(201).setBody(message)
|
|
}
|
|
|
|
async function _getMessages(clientThreadId, req, res) {
|
|
let before, limit
|
|
const { projectId } = req.params
|
|
if (req.query.before) {
|
|
before = parseInt(req.query.before, 10)
|
|
} else {
|
|
before = null
|
|
}
|
|
if (req.query.limit) {
|
|
limit = parseInt(req.query.limit, 10)
|
|
} else {
|
|
limit = DEFAULT_MESSAGE_LIMIT
|
|
}
|
|
logger.debug(
|
|
{ limit, before, projectId, clientThreadId },
|
|
'get message request received'
|
|
)
|
|
const thread = await ThreadManager.findOrCreateThread(
|
|
projectId,
|
|
clientThreadId
|
|
)
|
|
const threadObjectId = thread._id
|
|
logger.debug(
|
|
{ limit, before, projectId, clientThreadId, threadObjectId },
|
|
'found or created thread'
|
|
)
|
|
let messages = await MessageManager.getMessages(threadObjectId, limit, before)
|
|
messages = MessageFormatter.formatMessagesForClientSide(messages)
|
|
logger.debug({ projectId, messages }, 'got messages')
|
|
res.status(200).setBody(messages)
|
|
}
|
|
|
|
async function _duplicateCommentThreads(req, res) {
|
|
const { projectId } = req.params
|
|
const { threads } = req.body
|
|
const result = {}
|
|
for (const id of threads) {
|
|
logger.debug({ projectId, thread: id }, 'duplicating thread')
|
|
try {
|
|
const { oldRoom, newRoom } = await ThreadManager.duplicateThread(
|
|
projectId,
|
|
id
|
|
)
|
|
await MessageManager.duplicateRoomToOtherRoom(oldRoom._id, newRoom._id)
|
|
result[id] = { duplicateId: newRoom.thread_id }
|
|
} catch (error) {
|
|
if (error instanceof ThreadManager.MissingThreadError) {
|
|
// Expected error when the comment has been deleted prior to duplication
|
|
result[id] = { error: 'not found' }
|
|
} else {
|
|
logger.err({ error }, 'error duplicating thread')
|
|
result[id] = { error: 'unknown' }
|
|
}
|
|
}
|
|
}
|
|
res.json({ newThreads: result })
|
|
}
|