mirror of
https://github.com/duckietm/Nitro-V3.git
synced 2026-06-20 07:26:19 +00:00
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:
@@ -1,13 +1,12 @@
|
||||
import { MarkMentionsReadComposer } from '@nitrots/nitro-renderer';
|
||||
import { FC, useCallback, useMemo, useState } from 'react';
|
||||
import { IMentionEntry, LocalizeText, MentionType, SendMessageComposer } from '../../api';
|
||||
import { Button, Flex, NitroCardContentView, NitroCardHeaderView, NitroCardView, Text } from '../../common';
|
||||
import { useMentionsSnapshot } from '../../hooks';
|
||||
import { MarkMentionsReadComposer, RequestMentionsComposer } from '@nitrots/nitro-renderer';
|
||||
import { FC, useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import { FaSearch, FaSync } from 'react-icons/fa';
|
||||
import { getMentionDateGroup, IMentionEntry, LocalizeText, MentionDateGroup, MentionType, SendMessageComposer } from '../../api';
|
||||
import { Button, NitroCardContentView, NitroCardHeaderView, NitroCardView } from '../../common';
|
||||
import { useMentionActions, useMentionsSnapshot } from '../../hooks';
|
||||
import { markAllRead } from '../../hooks/mentions/mentionsStore';
|
||||
import { useUserDataSnapshot } from '../../hooks/session/useSessionSnapshots';
|
||||
import { MentionRowView } from './MentionRowView';
|
||||
import { getMentionDateGroup, MentionDateGroup } from './mentionsFormat';
|
||||
import { useMentionActions } from './useMentionActions';
|
||||
|
||||
interface MentionsViewProps
|
||||
{
|
||||
@@ -41,6 +40,17 @@ const matchesFilter = (mention: IMentionEntry, filter: MentionFilter): boolean =
|
||||
}
|
||||
};
|
||||
|
||||
const matchesQuery = (mention: IMentionEntry, query: string): boolean =>
|
||||
{
|
||||
if(!query) return true;
|
||||
|
||||
const q = query.toLowerCase();
|
||||
|
||||
return ((mention.senderUsername || '').toLowerCase().includes(q)
|
||||
|| (mention.roomName || '').toLowerCase().includes(q)
|
||||
|| (mention.message || '').toLowerCase().includes(q));
|
||||
};
|
||||
|
||||
export const MentionsView: FC<MentionsViewProps> = props =>
|
||||
{
|
||||
const { onClose } = props;
|
||||
@@ -48,6 +58,15 @@ export const MentionsView: FC<MentionsViewProps> = props =>
|
||||
const { userName: ownUsername = '' } = useUserDataSnapshot();
|
||||
const { open, goto, remove } = useMentionActions();
|
||||
const [ filter, setFilter ] = useState<MentionFilter>('all');
|
||||
const [ query, setQuery ] = useState('');
|
||||
|
||||
// Re-request from the server: once on open, and on the manual refresh button.
|
||||
const refresh = useCallback(() => SendMessageComposer(new RequestMentionsComposer()), []);
|
||||
|
||||
useEffect(() =>
|
||||
{
|
||||
refresh();
|
||||
}, [ refresh ]);
|
||||
|
||||
const onMarkAll = useCallback(() =>
|
||||
{
|
||||
@@ -62,48 +81,57 @@ export const MentionsView: FC<MentionsViewProps> = props =>
|
||||
for(const mention of mentions)
|
||||
{
|
||||
if(!matchesFilter(mention, filter)) continue;
|
||||
if(!matchesQuery(mention, query)) continue;
|
||||
buckets[getMentionDateGroup(mention.timestamp)].push(mention);
|
||||
}
|
||||
|
||||
return GROUP_ORDER
|
||||
.map(key => ({ key, items: buckets[key] }))
|
||||
.filter(group => group.items.length > 0);
|
||||
}, [ mentions, filter ]);
|
||||
}, [ mentions, filter, query ]);
|
||||
|
||||
const hasAny = groups.length > 0;
|
||||
const title = `${ LocalizeText('mentions.window.title') }${ (unreadCount > 0) ? ` (${ unreadCount })` : '' }`;
|
||||
|
||||
return (
|
||||
<NitroCardView className="w-[360px] h-[440px] has-classic-scrollbar" theme="primary-slim" uniqueKey="mentions">
|
||||
<NitroCardHeaderView headerText={ LocalizeText('mentions.window.title') } onCloseClick={ onClose } />
|
||||
<NitroCardView className="mentions-window w-[360px] has-classic-scrollbar" theme="primary-slim" uniqueKey="mentions">
|
||||
<NitroCardHeaderView headerText={ title } onCloseClick={ onClose } />
|
||||
<NitroCardContentView gap={ 1 }>
|
||||
<Flex alignItems="center" className="flex-wrap" gap={ 1 }>
|
||||
{ FILTERS.map(({ key, label }) =>
|
||||
{
|
||||
const active = (filter === key);
|
||||
const showCount = ((key === 'unread') && (unreadCount > 0));
|
||||
<div className="mentions-search">
|
||||
<FaSearch className="mentions-search-icon" />
|
||||
<input
|
||||
type="text"
|
||||
value={ query }
|
||||
placeholder={ LocalizeText('generic.search') }
|
||||
onChange={ event => setQuery(event.target.value) } />
|
||||
</div>
|
||||
<div className="mentions-toolbar">
|
||||
<div className="mentions-filters">
|
||||
{ FILTERS.map(({ key, label }) =>
|
||||
{
|
||||
const active = (filter === key);
|
||||
const showCount = ((key === 'unread') && (unreadCount > 0));
|
||||
|
||||
return (
|
||||
<button
|
||||
key={ key }
|
||||
type="button"
|
||||
onClick={ () => setFilter(key) }
|
||||
className={ `px-2 py-[2px] rounded-full text-xs border transition-colors ${ active ? 'bg-[#1e7295] text-white border-[#1e7295]' : 'bg-black/5 text-black/70 border-transparent hover:bg-black/10' }` }>
|
||||
{ LocalizeText(label) }{ showCount ? ` (${ unreadCount })` : '' }
|
||||
</button>
|
||||
);
|
||||
}) }
|
||||
</Flex>
|
||||
<Flex grow column className="min-h-0 overflow-y-auto" gap={ 0 }>
|
||||
return (
|
||||
<button key={ key } type="button" className={ `mentions-filter ${ active ? 'is-active' : '' }` } onClick={ () => setFilter(key) }>
|
||||
{ LocalizeText(label) }{ showCount ? ` ${ unreadCount }` : '' }
|
||||
</button>
|
||||
);
|
||||
}) }
|
||||
</div>
|
||||
<button type="button" className="mentions-refresh" title="Aggiorna" onClick={ refresh }>
|
||||
<FaSync />
|
||||
</button>
|
||||
</div>
|
||||
<div className="mentions-list">
|
||||
{ !hasAny &&
|
||||
<Flex grow column center gap={ 2 } className="py-6 text-center">
|
||||
<span className="flex items-center justify-center w-[44px] h-[44px] rounded-full bg-black/5 text-[#1e7295] text-[22px] font-bold">@</span>
|
||||
<Text center variant="gray">{ LocalizeText('mentions.window.empty') }</Text>
|
||||
</Flex> }
|
||||
<div className="mentions-empty">
|
||||
<span className="mentions-empty-glyph">@</span>
|
||||
<span className="mentions-empty-text">{ LocalizeText('mentions.window.empty') }</span>
|
||||
</div> }
|
||||
{ hasAny && groups.map(group => (
|
||||
<Flex key={ group.key } column gap={ 0 }>
|
||||
<Text small bold variant="gray" className="px-1 pt-2 pb-[2px] uppercase tracking-wide">
|
||||
{ LocalizeText(GROUP_LABEL[group.key]) }
|
||||
</Text>
|
||||
<div key={ group.key } className="mentions-group">
|
||||
<div className="mentions-group-label">{ LocalizeText(GROUP_LABEL[group.key]) }</div>
|
||||
{ group.items.map(mention => (
|
||||
<MentionRowView
|
||||
key={ mention.mentionId }
|
||||
@@ -113,9 +141,9 @@ export const MentionsView: FC<MentionsViewProps> = props =>
|
||||
onRemove={ remove }
|
||||
ownUsername={ ownUsername } />
|
||||
)) }
|
||||
</Flex>
|
||||
</div>
|
||||
)) }
|
||||
</Flex>
|
||||
</div>
|
||||
{ (unreadCount > 0) &&
|
||||
<Button variant="primary" onClick={ onMarkAll }>{ LocalizeText('mentions.window.markall') }</Button> }
|
||||
</NitroCardContentView>
|
||||
|
||||
Reference in New Issue
Block a user