mirror of
https://github.com/duckietm/Nitro-V3.git
synced 2026-06-19 15:06:20 +00:00
dcbf44aedb
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.
153 lines
6.4 KiB
TypeScript
153 lines
6.4 KiB
TypeScript
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';
|
|
|
|
interface MentionsViewProps
|
|
{
|
|
onClose: () => void;
|
|
}
|
|
|
|
type MentionFilter = 'all' | 'unread' | 'direct' | 'room';
|
|
|
|
const FILTERS: ReadonlyArray<{ key: MentionFilter; label: string }> = [
|
|
{ key: 'all', label: 'mentions.filter.all' },
|
|
{ key: 'unread', label: 'mentions.filter.unread' },
|
|
{ key: 'direct', label: 'mentions.filter.direct' },
|
|
{ key: 'room', label: 'mentions.filter.room' }
|
|
];
|
|
|
|
const GROUP_ORDER: ReadonlyArray<MentionDateGroup> = [ 'today', 'yesterday', 'older' ];
|
|
const GROUP_LABEL: Record<MentionDateGroup, string> = {
|
|
today: 'mentions.group.today',
|
|
yesterday: 'mentions.group.yesterday',
|
|
older: 'mentions.group.older'
|
|
};
|
|
|
|
const matchesFilter = (mention: IMentionEntry, filter: MentionFilter): boolean =>
|
|
{
|
|
switch(filter)
|
|
{
|
|
case 'unread': return !mention.read;
|
|
case 'direct': return mention.mentionType === MentionType.DIRECT;
|
|
case 'room': return mention.mentionType === MentionType.ROOM;
|
|
default: return true;
|
|
}
|
|
};
|
|
|
|
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;
|
|
const { mentions, unreadCount } = useMentionsSnapshot();
|
|
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(() =>
|
|
{
|
|
markAllRead();
|
|
SendMessageComposer(new MarkMentionsReadComposer(0, 0));
|
|
}, []);
|
|
|
|
const groups = useMemo(() =>
|
|
{
|
|
const buckets: Record<MentionDateGroup, IMentionEntry[]> = { today: [], yesterday: [], older: [] };
|
|
|
|
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, query ]);
|
|
|
|
const hasAny = groups.length > 0;
|
|
const title = `${ LocalizeText('mentions.window.title') }${ (unreadCount > 0) ? ` (${ unreadCount })` : '' }`;
|
|
|
|
return (
|
|
<NitroCardView className="mentions-window w-[360px] has-classic-scrollbar" theme="primary-slim" uniqueKey="mentions">
|
|
<NitroCardHeaderView headerText={ title } onCloseClick={ onClose } />
|
|
<NitroCardContentView gap={ 1 }>
|
|
<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" 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 &&
|
|
<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 => (
|
|
<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 }
|
|
mention={ mention }
|
|
onGoto={ goto }
|
|
onOpen={ open }
|
|
onRemove={ remove }
|
|
ownUsername={ ownUsername } />
|
|
)) }
|
|
</div>
|
|
)) }
|
|
</div>
|
|
{ (unreadCount > 0) &&
|
|
<Button variant="primary" onClick={ onMarkAll }>{ LocalizeText('mentions.window.markall') }</Button> }
|
|
</NitroCardContentView>
|
|
</NitroCardView>
|
|
);
|
|
};
|