Private
Public Access
Add @-mention highlighting, sidebar badge, and push customization (#39)
@username tokens in a sent message are parsed against the room's actual members (skipping fenced/inline code, so pasted code isn't misread) and recorded as MessageMention rows, reusing #38's read-tracking and offline-member broadcast infrastructure rather than building a parallel notification path: - Sidebar: a mentioned-and-unread room shows a distinct highlight- colored badge instead of (not alongside) the plain unread dot -- computed the same way as has_unread, just scoped to messages that mention the caller, and cleared by the same last_read_at mark-read flow. - Push notifications: a mentioned offline recipient gets "X mentioned you: ..." instead of the generic "X: ...", still per-recipient since the same message can page some room members and not others. - Message rendering: a validated @username is highlighted inline, implemented by turning it into a `[@username](mention:username)` link before markdown parsing and overriding link rendering to style `mention:`-scheme links as a span instead of an anchor -- reuses markdown-to-jsx's existing parser rather than hand-rolling text-node splitting. - Composer: typing @ opens an autocomplete dropdown of matching room members (arrow keys to navigate, Enter/Tab/click to insert, Escape or moving the cursor away to dismiss). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -3,7 +3,7 @@ import uuid
|
||||
from sqlalchemy import select
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from app.models import Message, MessageFile, Room, RoomMembership, User
|
||||
from app.models import Message, MessageFile, MessageMention, Room, RoomMembership, User
|
||||
from app.schemas.message import ReactionSummary
|
||||
from app.services.push_service import send_push_to_user
|
||||
from app.services.webhook_service import dispatch_event
|
||||
@@ -31,6 +31,11 @@ async def _notify_offline_members(
|
||||
if not offline_ids:
|
||||
return
|
||||
|
||||
result = await db.execute(
|
||||
select(MessageMention.user_id).where(MessageMention.message_id == message.id)
|
||||
)
|
||||
mentioned_ids = {row[0] for row in result.all()}
|
||||
|
||||
# This is also exactly the right audience for "give this room an unread
|
||||
# dot": presence.connected_user_ids(room_id) means "has this room's
|
||||
# channel joined right now" -- which the client only does while the tab
|
||||
@@ -39,22 +44,29 @@ async def _notify_offline_members(
|
||||
# not just rooms that aren't open at all.
|
||||
for user_id in offline_ids:
|
||||
await broadcaster.publish_to_user(
|
||||
user_id, {"type": "unread_update", "room_id": str(room_id)}
|
||||
user_id,
|
||||
{
|
||||
"type": "unread_update",
|
||||
"room_id": str(room_id),
|
||||
"mentioned": user_id in mentioned_ids,
|
||||
},
|
||||
)
|
||||
|
||||
room = await db.get(Room, room_id)
|
||||
if message.content:
|
||||
body = f"{sender.username}: {message.content}"[:120]
|
||||
elif message.file_id:
|
||||
body = f"{sender.username} sent a file"
|
||||
else:
|
||||
body = f"{sender.username} sent an image"
|
||||
payload = {
|
||||
"title": f"#{room.name}" if room else "New message",
|
||||
"body": body,
|
||||
"room_id": str(room_id),
|
||||
}
|
||||
for user_id in offline_ids:
|
||||
mentioned = user_id in mentioned_ids
|
||||
if message.content:
|
||||
prefix = f"{sender.username} mentioned you: " if mentioned else f"{sender.username}: "
|
||||
body = (prefix + message.content)[:120]
|
||||
elif message.file_id:
|
||||
body = f"{sender.username} sent a file"
|
||||
else:
|
||||
body = f"{sender.username} sent an image"
|
||||
payload = {
|
||||
"title": f"#{room.name}" if room else "New message",
|
||||
"body": body,
|
||||
"room_id": str(room_id),
|
||||
}
|
||||
await send_push_to_user(db, user_id, payload)
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user