Files
ds-chat/frontend/src/components/MentionAutocomplete.tsx
T
ksmithandClaude Sonnet 5 4bac502b2c 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>
2026-08-17 08:56:40 -06:00

37 lines
1.3 KiB
TypeScript

import type { RoomMember } from '../types'
import './MentionAutocomplete.css'
interface MentionAutocompleteProps {
matches: RoomMember[]
activeIndex: number
onPick: (username: string) => void
onHover: (index: number) => void
}
export function MentionAutocomplete({ matches, activeIndex, onPick, onHover }: MentionAutocompleteProps) {
return (
<div className="mention-autocomplete" role="listbox">
{matches.map((member, i) => (
<button
key={member.user_id}
type="button"
role="option"
aria-selected={i === activeIndex}
className={`mention-autocomplete-item${i === activeIndex ? ' mention-autocomplete-item-active' : ''}`}
// Selecting must survive the textarea's blur (which would
// otherwise fire first and could dismiss the dropdown) --
// onMouseDown fires before blur, onClick fires after.
onMouseDown={(e) => e.preventDefault()}
onClick={() => onPick(member.username)}
onMouseEnter={() => onHover(i)}
>
<span className="mention-autocomplete-username">@{member.username}</span>
{member.display_name && (
<span className="mention-autocomplete-display-name">{member.display_name}</span>
)}
</button>
))}
</div>
)
}