fix: chat — 4 fixes for mentions, role display, header, close-on-outside
1. Role tooltip is now absolute-positioned (no layout impact on bubble width) 2. Outside-click uses overlay div instead of document mousedown (fixes mention dropdown closing chat) 3. Header: role shown in small white/50 text, separate from green 'в сети' 4. @mentions: highlighted in messages (yellow for self, bold for others); bypass mute if mentioned Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -105,6 +105,24 @@ interface ToastNotif {
|
||||
id: string; roomId: string; roomName: string; senderName: string; text: string
|
||||
}
|
||||
|
||||
function renderWithMentions(text: string, myName?: string) {
|
||||
const parts = text.split(/(@\S+)/g)
|
||||
if (parts.length === 1) return <>{text}</>
|
||||
const myFirst = myName?.split(' ')[0]?.toLowerCase()
|
||||
return <>
|
||||
{parts.map((part, i) => {
|
||||
if (!part.startsWith('@')) return <span key={i}>{part}</span>
|
||||
const mentioned = part.slice(1).toLowerCase()
|
||||
const isMe = myFirst && mentioned.startsWith(myFirst)
|
||||
return (
|
||||
<span key={i} className={cn('font-semibold', isMe ? 'bg-yellow-300/20 text-yellow-200 rounded px-0.5' : 'opacity-90')}>
|
||||
{part}
|
||||
</span>
|
||||
)
|
||||
})}
|
||||
</>
|
||||
}
|
||||
|
||||
// ── Main ───────────────────────────────────────────────────────────────────
|
||||
|
||||
type View = 'rooms' | 'messages' | 'search' | 'settings'
|
||||
@@ -204,7 +222,10 @@ export function ChatWidget() {
|
||||
if (curr > prev) {
|
||||
const isViewing = open && view === 'messages' && activeRoomIdRef.current === room.id
|
||||
const isMuted = mutedIds.includes(room.id)
|
||||
if (!isViewing && !isMuted) {
|
||||
// Bypass mute if current user is mentioned
|
||||
const myFirst = (user as { name?: string } | null | undefined)?.name?.split(' ')[0]
|
||||
const isMentioned = myFirst ? (room.lastMessage ?? '').includes(`@${myFirst}`) : false
|
||||
if (!isViewing && (!isMuted || isMentioned)) {
|
||||
if (settings.soundEnabled) playNotifSound()
|
||||
if (settings.popupEnabled !== false) {
|
||||
const rName = room.type === 'general' ? 'Общий чат'
|
||||
@@ -318,17 +339,7 @@ export function ChatWidget() {
|
||||
return () => window.removeEventListener('click', h)
|
||||
}, [ctxMenu])
|
||||
|
||||
// Close chat when clicking outside the widget
|
||||
useEffect(() => {
|
||||
if (!open) return
|
||||
const h = (e: MouseEvent) => {
|
||||
if (widgetRef.current && !widgetRef.current.contains(e.target as Node)) {
|
||||
setOpen(false)
|
||||
}
|
||||
}
|
||||
document.addEventListener('mousedown', h)
|
||||
return () => document.removeEventListener('mousedown', h)
|
||||
}, [open])
|
||||
// close-on-outside handled by overlay div in JSX
|
||||
|
||||
// ── Typing helpers ────────────────────────────────────────────────────────
|
||||
|
||||
@@ -530,6 +541,8 @@ export function ChatWidget() {
|
||||
|
||||
return (
|
||||
<div ref={widgetRef}>
|
||||
{/* Transparent overlay — closes chat when clicking outside panel */}
|
||||
{open && <div className="fixed inset-0 z-40" onMouseDown={() => setOpen(false)} />}
|
||||
{/* Float button */}
|
||||
<button onClick={() => setOpen(v => !v)}
|
||||
className={cn('fixed bottom-6 right-6 z-50 w-14 h-14 rounded-full shadow-lg bg-brand-600 hover:bg-brand-700 text-white transition-all flex items-center justify-center', open && 'scale-90')}>
|
||||
@@ -572,11 +585,13 @@ export function ChatWidget() {
|
||||
receptionist: 'Ресепшн', accountant: 'Бухгалтер', security: 'Охрана', technician: 'Техник',
|
||||
}
|
||||
const roleLabel = activeRoom.otherUserRole ? roleLabels[activeRoom.otherUserRole] : null
|
||||
const online = isOtherOnline(activeRoom)
|
||||
if (!online && !roleLabel) return null
|
||||
return (
|
||||
<p className="text-xs text-white/70">
|
||||
{isOtherOnline(activeRoom)
|
||||
? <span className="text-green-300">в сети{roleLabel ? ` · ${roleLabel}` : ''}</span>
|
||||
: roleLabel ?? ''}
|
||||
<p className="text-xs flex items-center gap-1">
|
||||
{online && <span className="text-green-300">в сети</span>}
|
||||
{online && roleLabel && <span className="text-white/40">·</span>}
|
||||
{roleLabel && <span className="text-white/50 text-[10px]">{roleLabel}</span>}
|
||||
</p>
|
||||
)
|
||||
})()}
|
||||
@@ -759,6 +774,7 @@ export function ChatWidget() {
|
||||
isEditing={editingMsgId === msg.id}
|
||||
editText={editText}
|
||||
isRead={isRead(msg)}
|
||||
currentUserName={user?.name}
|
||||
onContextMenu={e => {
|
||||
if (msg.deletedAt || msg.isSystem || activeRoom?.type === 'notifications') return
|
||||
e.preventDefault()
|
||||
@@ -984,8 +1000,9 @@ function RoomRow({ room, isPinned, isMuted, isOnline, onClick, onMenuClick }: {
|
||||
|
||||
// ── MessageBubble ──────────────────────────────────────────────────────────
|
||||
|
||||
function MessageBubble({ msg, isOwn, isEditing, editText, isRead, onContextMenu, onEditTextChange, onSaveEdit, onCancelEdit, onReaction }: {
|
||||
function MessageBubble({ msg, isOwn, isEditing, editText, isRead, currentUserName, onContextMenu, onEditTextChange, onSaveEdit, onCancelEdit, onReaction }: {
|
||||
msg: ChatMessage; isOwn: boolean; isEditing: boolean; editText: string; isRead: boolean
|
||||
currentUserName?: string
|
||||
onContextMenu: (e: React.MouseEvent) => void
|
||||
onEditTextChange: (v: string) => void
|
||||
onSaveEdit: () => void; onCancelEdit: () => void
|
||||
@@ -1003,13 +1020,13 @@ function MessageBubble({ msg, isOwn, isEditing, editText, isRead, onContextMenu,
|
||||
}
|
||||
const roleLabel = !isSystem && msg.senderRole ? roleLabels[msg.senderRole] : null
|
||||
return (
|
||||
<div className="group/name relative inline-flex items-baseline gap-1 mb-0.5 ml-1 cursor-default">
|
||||
<div className="group/name relative inline-block mb-0.5 ml-1 cursor-default">
|
||||
<p className="text-[10px] text-slate-500 dark:text-slate-400 font-medium">
|
||||
{isSystem ? msg.systemName ?? 'Система' : msg.senderName.split(' ')[0]}
|
||||
</p>
|
||||
{roleLabel && (
|
||||
<span className="hidden group-hover/name:inline text-[9px] text-slate-400 dark:text-slate-500 transition-all">
|
||||
· {roleLabel}
|
||||
<span className="absolute left-full top-0 ml-1.5 px-1.5 py-0.5 rounded bg-slate-700 dark:bg-slate-900 text-white text-[9px] whitespace-nowrap opacity-0 group-hover/name:opacity-100 transition-opacity z-10 pointer-events-none shadow-sm">
|
||||
{roleLabel}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
@@ -1041,7 +1058,7 @@ function MessageBubble({ msg, isOwn, isEditing, editText, isRead, onContextMenu,
|
||||
)}
|
||||
{msg.text && (
|
||||
<p className={cn('px-3 py-2', isSystem ? 'text-amber-900 dark:text-amber-200' : isOwn ? 'text-white' : 'text-slate-800 dark:text-slate-200')}>
|
||||
{msg.text}
|
||||
{renderWithMentions(msg.text, currentUserName)}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user