feat(mentions): overhaul, refactor, notification bubble & window update

Chat tagging:
- Any @user is a visible tag in chat bubbles (the .mention-tag CSS never
  existed, so highlighting was invisible); self/alias mentions get a gold
  emphasis. Fixes cross-room tags not being highlighted.

Mentions window:
- Redesigned: unread count in the header, restyled filter chips + a refresh
  button, CSS-driven list/date-groups, adaptive height (compact when few,
  capped + scroll when many), polished empty state.
- Rows: framed avatar (friends-list head crop so the face is never clipped),
  per-row unread dot, type marker, icon action buttons (goto / remove).
- Re-requests from the server each time it opens.

Autocomplete:
- Never suggests the viewer themselves; suggests room users + online friends +
  aliases.

Notifications:
- Mention toast removed; mentions flow through the client's standard
  notification stream via a dedicated mention bubble (avatar + actions) in the
  default position. EVERY received mention surfaces (independent of the generic
  info-feed toggle, gated only by mentions_ui.enabled).

Refactor (behaviour-preserving):
- Centralised @-token classification in api/mentions/mentionTokens.
- Moved mentionsFormat -> api/mentions, useMentionActions -> hooks/mentions.
- Extracted ChatInputView @-autocomplete into a tested useChatMentions hook +
  pure helper; removed the dead duplicate useMentionAutocomplete.
This commit is contained in:
simoleo89
2026-06-06 23:35:33 +02:00
parent 110363ab1c
commit dcbf44aedb
35 changed files with 1220 additions and 657 deletions
+26 -34
View File
@@ -1,8 +1,8 @@
import { FC, MouseEvent } from 'react';
import { IMentionEntry, LocalizeText, MentionType } from '../../api';
import { Flex, Text } from '../../common';
import { FaArrowRight, FaTimes } from 'react-icons/fa';
import { formatMentionTime, IMentionEntry, LocalizeText, MentionType } from '../../api';
import { LayoutAvatarImageView } from '../../common';
import { MentionMessageView } from './MentionMessageView';
import { formatMentionTime } from './mentionsFormat';
interface MentionRowViewProps
{
@@ -28,39 +28,31 @@ export const MentionRowView: FC<MentionRowViewProps> = props =>
};
return (
<Flex pointer alignItems="center" className="group relative px-1 py-[3px] rounded hover:bg-black/5" gap={ 2 } onClick={ () => onOpen(mention) }>
<span
className={ `inline-block w-[8px] h-[8px] rounded-full shrink-0 ${ mention.read ? 'bg-transparent' : 'bg-[#1e7295]' }` }
title={ mention.read ? '' : LocalizeText('mentions.filter.unread') } />
<span
title={ typeTitle }
className={ `flex items-center justify-center shrink-0 w-[18px] h-[18px] rounded text-[10px] font-bold leading-none text-white ${ isRoom ? 'bg-[#d08a1e]' : 'bg-[#1e7295]' }` }>
{ isRoom ? '@' : '@' }
</span>
<Flex grow column className="min-w-0" gap={ 0 }>
<Flex alignItems="center" gap={ 1 } className="min-w-0">
<Text bold={ !mention.read } truncate variant="primary">{ mention.senderUsername }</Text>
{ (mention.roomName && mention.roomName.length > 0) &&
<Text small truncate variant="gray">· { mention.roomName }</Text> }
</Flex>
<MentionMessageView className="block truncate text-black text-sm" ownUsername={ ownUsername } text={ mention.message } />
</Flex>
<Flex alignItems="center" gap={ 1 } className="shrink-0">
<div className={ `mention-row ${ mention.read ? '' : 'is-unread' }` } onClick={ () => onOpen(mention) }>
{ !mention.read &&
<span className="mention-row-unread-dot" aria-hidden /> }
<div className="mention-row-avatar" title={ typeTitle }>
<LayoutAvatarImageView headOnly direction={ 2 } figure={ mention.senderFigure } />
<span className={ `mention-row-type ${ isRoom ? 'is-room' : 'is-direct' }` }>{ isRoom ? '' : '@' }</span>
</div>
<div className="mention-row-body">
<div className="mention-row-head">
<span className="mention-row-name">{ mention.senderUsername }</span>
{ (mention.roomName && (mention.roomName.length > 0)) &&
<span className="mention-row-room">· { mention.roomName }</span> }
</div>
<MentionMessageView className="mention-row-msg" ownUsername={ ownUsername } text={ mention.message } />
</div>
<div className="mention-row-meta">
{ (time.length > 0) &&
<Text small variant="gray" className="tabular-nums group-hover:hidden">{ time }</Text> }
<Flex alignItems="center" gap={ 1 } className="hidden group-hover:flex">
<span className="mention-row-time">{ time }</span> }
<div className="mention-row-actions">
{ onGoto &&
<span
title={ LocalizeText('mentions.action.goto') }
className="flex items-center justify-center w-[18px] h-[18px] rounded bg-black/10 hover:bg-black/20 text-[12px] leading-none"
onClick={ event => stop(event, () => onGoto(mention)) }></span> }
<button type="button" className="mention-row-action" title={ LocalizeText('mentions.action.goto') } onClick={ event => stop(event, () => onGoto(mention)) }><FaArrowRight /></button> }
{ onRemove &&
<span
title={ LocalizeText('mentions.action.remove') }
className="flex items-center justify-center w-[18px] h-[18px] rounded bg-black/10 hover:bg-red-500 hover:text-white text-[11px] leading-none"
onClick={ event => stop(event, () => onRemove(mention)) }></span> }
</Flex>
</Flex>
</Flex>
<button type="button" className="mention-row-action is-remove" title={ LocalizeText('mentions.action.remove') } onClick={ event => stop(event, () => onRemove(mention)) }><FaTimes /></button> }
</div>
</div>
</div>
);
};