Files
backspace/packages/web/src/components/chat/MentionBadge.tsx
T
Jannis Braun c9f9787b99 feat: rich markdown renderer with @mention badges and autocomplete
- Add MarkdownRenderer with syntax-highlighted code blocks (prism-react-renderer),
  GFM tables, and Discord-style theming
- Add MentionBadge that resolves user IDs to display names with role colors
- Add MentionPopover autocomplete triggered by @ in MessageInput
- Fix mention:// URL sanitization — allowlist the scheme in urlTransform so
  react-markdown v9 passes it through to the component override
- Highlight messages that mention the current user (amber border)
- Bump rate limit from 60 to 200 req/min
2026-02-25 23:49:41 +01:00

60 lines
1.8 KiB
TypeScript

import React from 'react';
import { useServerStore } from '../../stores/serverStore';
import { useUIStore } from '../../stores/uiStore';
interface MentionBadgeProps {
userId: string;
}
export const MentionBadge = React.memo(function MentionBadge({ userId }: MentionBadgeProps) {
const members = useServerStore((s) => s.members);
const servers = useServerStore((s) => s.servers);
const currentServerId = useServerStore((s) => s.currentServerId);
const openUserProfile = useUIStore((s) => s.openUserProfile);
const member = members.find((m) => m.userId === userId);
const server = servers.find((s) => s.id === currentServerId);
const ownerId = server?.ownerId;
let displayName: string;
let color: string;
if (member) {
displayName = member.user.displayName ?? member.user.username;
if (member.roles && member.roles.length > 0) {
const sorted = [...member.roles].sort((a, b) => b.position - a.position);
color = sorted[0]!.color;
} else if (ownerId && userId === ownerId) {
color = '#f23f43';
} else {
color = '#5865f2'; // blurple default
}
} else {
displayName = 'Unknown User';
color = '#a3a6aa'; // muted fallback
}
const handleClick = (e: React.MouseEvent) => {
if (!member) return;
e.stopPropagation();
const rect = e.currentTarget.getBoundingClientRect();
openUserProfile(member.user, {
top: Math.min(rect.top, window.innerHeight - 450),
left: rect.right + 8,
});
};
// Build inline styles: role-colored text with tinted background
const bgColor = color + '1a'; // ~10% opacity hex
return (
<span
onClick={handleClick}
className="inline-flex items-center rounded-[3px] px-[2px] font-medium cursor-pointer transition-colors hover:brightness-125"
style={{ color, backgroundColor: bgColor }}
>
@{displayName}
</span>
);
});