Add notifications web module (#28983)

* Add notifications web module

* implement getThreadMessage in chat

* Save comment mention notification

* check if recipient is a real user

* move commentMentionDelay

* use module-hooks types

* remove router

* updated collection name

GitOrigin-RevId: cf8240c88aac7d7e4de4bf51cfe2608b6b7e7918
This commit is contained in:
Domagoj Kriskovic
2025-10-17 10:57:49 +02:00
committed by Copybot
parent 387ef81a31
commit 71457d74cb
7 changed files with 79 additions and 0 deletions

View File

@@ -62,6 +62,10 @@ export async function getThread(context) {
return await callMessageHttpController(context, _getThread)
}
export async function getThreadMessage(context) {
return await callMessageHttpController(context, _getThreadMessage)
}
export async function resolveThread(context) {
return await callMessageHttpController(context, _resolveThread)
}
@@ -208,6 +212,30 @@ const _getThread = async (req, res) => {
}
}
const _getThreadMessage = async (req, res) => {
const { projectId, threadId, messageId } = req.params
logger.debug(
{ projectId, threadId, messageId },
'getting single thread message'
)
try {
const room = await ThreadManager.findThread(projectId, threadId)
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
}
}
const _resolveThread = async (req, res) => {
const { projectId, threadId } = req.params
const { user_id: userId } = req.body

View File

@@ -243,6 +243,20 @@ paths:
name: messageId
in: path
required: true
get:
summary: Get thread message
tags: []
responses:
'200':
description: OK
content:
application/json:
schema:
$ref: '#/components/schemas/Message'
'404':
description: Message not found
operationId: getThreadMessage
description: Get a specific message by message ID from the thread with Thread ID and Project ID provided
delete:
summary: Delete message
operationId: deleteMessage

View File

@@ -8,6 +8,12 @@ async function getThread(projectId, threadId) {
return await fetchJson(chatApiUrl(`/project/${projectId}/thread/${threadId}`))
}
async function getThreadMessage(projectId, threadId, messageId) {
return await fetchJson(
chatApiUrl(`/project/${projectId}/thread/${threadId}/messages/${messageId}`)
)
}
async function getThreads(projectId) {
return await fetchJson(chatApiUrl(`/project/${projectId}/threads`))
}
@@ -161,6 +167,7 @@ function chatApiUrl(path) {
module.exports = {
getThread: callbackify(getThread),
getThreadMessage: callbackify(getThreadMessage),
getThreads: callbackify(getThreads),
destroyProject: callbackify(destroyProject),
sendGlobalMessage: callbackify(sendGlobalMessage),
@@ -180,6 +187,7 @@ module.exports = {
generateThreadData: callbackify(generateThreadData),
promises: {
getThread,
getThreadMessage,
getThreads,
destroyProject,
sendGlobalMessage,

View File

@@ -54,6 +54,7 @@ const db = {
messages: internalDb.collection('messages'),
migrations: internalDb.collection('migrations'),
notifications: internalDb.collection('notifications'),
emailNotifications: internalDb.collection('emailNotifications'),
oauthAccessTokens: internalDb.collection('oauthAccessTokens'),
oauthApplications: internalDb.collection('oauthApplications'),
oauthAuthorizationCodes: internalDb.collection('oauthAuthorizationCodes'),

View File

@@ -715,6 +715,10 @@ module.exports = {
parseInt(process.env.OVERLEAF_PROJECT_HARD_DELETION_DELAY, 10) ||
1000 * 60 * 60 * 24 * 90, // 90 days
// Delay before sending comment mention notifications
commentMentionDelay:
parseInt(process.env.COMMENT_MENTION_DELAY_MINUTES) || 30 * 60 * 1000, // 30 minutes
// Maximum JSON size in HTTP requests
// We should be able to process twice the max doc length, to allow for
// - the doc content

View File

@@ -58,6 +58,20 @@ class MockChatApi extends AbstractMockApi {
res.json(this.getThread(req.params.project_id, req.params.thread_id))
}
)
this.app.get(
'/project/:project_id/thread/:thread_id/messages/:message_id',
(req, res) => {
const projectId = req.params.project_id
const threadId = req.params.thread_id
const messageId = req.params.message_id
const thread = this.getThread(projectId, threadId)
const message = thread.find(msg => msg.id === messageId)
if (!message) {
return res.status(404).json({ error: 'Message not found' })
}
res.json(message)
}
)
this.app.post(
'/project/:project_id/thread/:thread_id/messages',
(req, res) => {

View File

@@ -0,0 +1,10 @@
/**
* Types for module hook events fired across the application
*/
export type CommentAddedEvent = {
projectId: string
userId: string
threadId: string
messageId: string
}