feat: add MentionNotificationHandler and create notifications on mention

This commit is contained in:
CREDO23 2026-01-19 17:06:51 +02:00
parent 7f5f4a6893
commit ae4297a02f
2 changed files with 130 additions and 14 deletions

View file

@ -30,6 +30,7 @@ from app.schemas.chat_comments import (
MentionListResponse,
MentionResponse,
)
from app.services.notification_service import NotificationService
from app.utils.chat_comments import parse_mentions, render_mentions
from app.utils.rbac import check_permission, get_user_permissions
@ -62,7 +63,7 @@ async def process_mentions(
comment_id: int,
content: str,
search_space_id: int,
) -> None:
) -> dict[UUID, int]:
"""
Parse mentions from content, validate users are members, and insert mention records.
@ -71,10 +72,13 @@ async def process_mentions(
comment_id: ID of the comment containing mentions
content: Comment text with @[uuid] mentions
search_space_id: ID of the search space for membership validation
Returns:
Dictionary mapping mentioned user UUID to their mention record ID
"""
mentioned_uuids = parse_mentions(content)
if not mentioned_uuids:
return
return {}
# Get valid members from the mentioned UUIDs
result = await session.execute(
@ -85,15 +89,18 @@ async def process_mentions(
)
valid_member_ids = result.scalars().all()
# Insert mention records for valid members
# Insert mention records for valid members and collect their IDs
mentions_map: dict[UUID, int] = {}
for user_id in valid_member_ids:
mention = ChatCommentMention(
comment_id=comment_id,
mentioned_user_id=user_id,
)
session.add(mention)
await session.flush()
mentions_map[user_id] = mention.id
await session.flush()
return mentions_map
async def get_comments_for_message(
@ -282,15 +289,34 @@ async def create_comment(
session.add(comment)
await session.flush()
# Process mentions
await process_mentions(session, comment.id, content, search_space_id)
# Process mentions - returns map of user_id -> mention_id
mentions_map = await process_mentions(session, comment.id, content, search_space_id)
await session.commit()
await session.refresh(comment)
# Fetch user names for rendering mentions
mentioned_uuids = set(parse_mentions(content))
user_names = await get_user_names_for_mentions(session, mentioned_uuids)
# Fetch user names for rendering mentions (reuse mentions_map keys)
user_names = await get_user_names_for_mentions(session, set(mentions_map.keys()))
# Create notifications for mentioned users (excluding author)
thread = message.thread
author_name = user.display_name or user.email
for mentioned_user_id, mention_id in mentions_map.items():
if mentioned_user_id == user.id:
continue # Don't notify yourself
await NotificationService.mention.notify_new_mention(
session=session,
mentioned_user_id=mentioned_user_id,
mention_id=mention_id,
comment_id=comment.id,
message_id=message_id,
thread_id=thread.id,
thread_title=thread.title or "Untitled thread",
author_id=str(user.id),
author_name=author_name,
content_preview=content[:200],
search_space_id=search_space_id,
)
author = AuthorResponse(
id=user.id,
@ -373,15 +399,34 @@ async def create_reply(
session.add(reply)
await session.flush()
# Process mentions
await process_mentions(session, reply.id, content, search_space_id)
# Process mentions - returns map of user_id -> mention_id
mentions_map = await process_mentions(session, reply.id, content, search_space_id)
await session.commit()
await session.refresh(reply)
# Fetch user names for rendering mentions
mentioned_uuids = set(parse_mentions(content))
user_names = await get_user_names_for_mentions(session, mentioned_uuids)
# Fetch user names for rendering mentions (reuse mentions_map keys)
user_names = await get_user_names_for_mentions(session, set(mentions_map.keys()))
# Create notifications for mentioned users (excluding author)
thread = parent_comment.message.thread
author_name = user.display_name or user.email
for mentioned_user_id, mention_id in mentions_map.items():
if mentioned_user_id == user.id:
continue # Don't notify yourself
await NotificationService.mention.notify_new_mention(
session=session,
mentioned_user_id=mentioned_user_id,
mention_id=mention_id,
comment_id=reply.id,
message_id=parent_comment.message_id,
thread_id=thread.id,
thread_title=thread.title or "Untitled thread",
author_id=str(user.id),
author_name=author_name,
content_preview=content[:200],
search_space_id=search_space_id,
)
author = AuthorResponse(
id=user.id,

View file

@ -617,12 +617,83 @@ class DocumentProcessingNotificationHandler(BaseNotificationHandler):
)
class MentionNotificationHandler(BaseNotificationHandler):
"""Handler for new mention notifications."""
def __init__(self):
super().__init__("new_mention")
async def notify_new_mention(
self,
session: AsyncSession,
mentioned_user_id: UUID,
mention_id: int,
comment_id: int,
message_id: int,
thread_id: int,
thread_title: str,
author_id: str,
author_name: str,
content_preview: str,
search_space_id: int,
) -> Notification:
"""
Create notification when a user is @mentioned in a comment.
Args:
session: Database session
mentioned_user_id: User who was mentioned
mention_id: ID of the mention record
comment_id: ID of the comment containing the mention
message_id: ID of the message being commented on
thread_id: ID of the chat thread
thread_title: Title of the chat thread
author_id: ID of the comment author
author_name: Display name of the comment author
content_preview: First ~100 chars of the comment
search_space_id: Search space ID
Returns:
Notification: The created notification
"""
title = f"{author_name} mentioned you"
message = content_preview[:100] + ("..." if len(content_preview) > 100 else "")
metadata = {
"mention_id": mention_id,
"comment_id": comment_id,
"message_id": message_id,
"thread_id": thread_id,
"thread_title": thread_title,
"author_id": author_id,
"author_name": author_name,
"content_preview": content_preview[:200],
}
notification = Notification(
user_id=mentioned_user_id,
search_space_id=search_space_id,
type=self.notification_type,
title=title,
message=message,
notification_metadata=metadata,
)
session.add(notification)
await session.commit()
await session.refresh(notification)
logger.info(
f"Created new_mention notification {notification.id} for user {mentioned_user_id}"
)
return notification
class NotificationService:
"""Service for creating and managing notifications that sync via Electric SQL."""
# Handler instances
connector_indexing = ConnectorIndexingNotificationHandler()
document_processing = DocumentProcessingNotificationHandler()
mention = MentionNotificationHandler()
@staticmethod
async def create_notification(